Skip to content

Commit ab648c9

Browse files
committed
feat: add fetch middleware for api keys and request init
chore: remove file wip
1 parent 2add622 commit ab648c9

File tree

27 files changed

+34679
-38886
lines changed

27 files changed

+34679
-38886
lines changed

package-lock.json

Lines changed: 34078 additions & 38523 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/auth/src/profile.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import { resolveZoneFileToProfile } from '@stacks/profile';
2-
import { fetchPrivate } from '@stacks/common';
32
import { StacksMainnet, StacksNetwork, StacksNetworkName } from '@stacks/network';
43

54
export interface ProfileLookupOptions {
@@ -31,13 +30,13 @@ export function lookupProfile(lookupOptions: ProfileLookupOptions): Promise<Reco
3130
let lookupPromise;
3231
if (options.zoneFileLookupURL) {
3332
const url = `${options.zoneFileLookupURL.replace(/\/$/, '')}/${options.username}`;
34-
lookupPromise = fetchPrivate(url).then(response => response.json());
33+
lookupPromise = network.fetchFn(url).then(response => response.json());
3534
} else {
3635
lookupPromise = network.getNameInfo(options.username);
3736
}
3837
return lookupPromise.then((responseJSON: any) => {
3938
if (responseJSON.hasOwnProperty('zonefile') && responseJSON.hasOwnProperty('address')) {
40-
return resolveZoneFileToProfile(responseJSON.zonefile, responseJSON.address);
39+
return resolveZoneFileToProfile(responseJSON.zonefile, responseJSON.address, network.fetchFn);
4140
} else {
4241
throw new Error(
4342
'Invalid zonefile lookup response: did not contain `address`' + ' or `zonefile` field'

packages/auth/src/provider.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import * as queryString from 'query-string';
22
import { decodeToken } from 'jsontokens';
3-
import { BLOCKSTACK_HANDLER, getGlobalObject, fetchPrivate } from '@stacks/common';
3+
import { BLOCKSTACK_HANDLER, getGlobalObject, createFetchFn, FetchFn } from '@stacks/common';
44

55
/**
66
* Retrieves the authentication request from the query string
@@ -36,7 +36,10 @@ export function getAuthRequestFromURL() {
3636
* @private
3737
* @ignore
3838
*/
39-
export async function fetchAppManifest(authRequest: string): Promise<any> {
39+
export async function fetchAppManifest(
40+
authRequest: string,
41+
fetchFn: FetchFn = createFetchFn()
42+
): Promise<any> {
4043
if (!authRequest) {
4144
throw new Error('Invalid auth request');
4245
}
@@ -47,7 +50,7 @@ export async function fetchAppManifest(authRequest: string): Promise<any> {
4750
const manifestURI = payload.manifest_uri as string;
4851
try {
4952
// Logger.debug(`Fetching manifest from ${manifestURI}`)
50-
const response = await fetchPrivate(manifestURI);
53+
const response = await fetchFn(manifestURI);
5154
const responseText = await response.text();
5255
const responseJSON = JSON.parse(responseText);
5356
return { ...responseJSON, manifestURI };

packages/auth/src/userSession.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
// @ts-ignore
2-
import { Buffer } from '@stacks/common';
2+
import { Buffer, FetchFn, createFetchFn } from '@stacks/common';
33
import { AppConfig } from './appConfig';
44
import { SessionOptions } from './sessionData';
55
import { InstanceDataStore, LocalStorageStore, SessionDataStore } from './sessionStore';
@@ -15,7 +15,6 @@ import {
1515
import { getAddressFromDID } from './dids';
1616
import {
1717
BLOCKSTACK_DEFAULT_GAIA_HUB_URL,
18-
fetchPrivate,
1918
getGlobalObject,
2019
InvalidStateError,
2120
isLaterVersion,
@@ -214,7 +213,8 @@ export class UserSession {
214213
* if handling the sign in request fails or there was no pending sign in request.
215214
*/
216215
async handlePendingSignIn(
217-
authResponseToken: string = this.getAuthResponseToken()
216+
authResponseToken: string = this.getAuthResponseToken(),
217+
fetchFn: FetchFn = createFetchFn()
218218
): Promise<UserData> {
219219
const sessionData = this.store.getSessionData();
220220

@@ -312,7 +312,7 @@ export class UserSession {
312312
};
313313
const profileURL = tokenPayload.profile_url as string;
314314
if (!userData.profile && profileURL) {
315-
const response = await fetchPrivate(profileURL);
315+
const response = await fetchFn(profileURL);
316316
if (!response.ok) {
317317
// return blank profile if we fail to fetch
318318
userData.profile = Object.assign({}, DEFAULT_PROFILE);

packages/cli/src/network.ts

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
import blockstack from 'blockstack';
22
import * as bitcoin from 'bitcoinjs-lib';
33
import BN from 'bn.js';
4-
import fetch from 'node-fetch';
54

65
import { CLI_CONFIG_TYPE } from './argparse';
76

87
import { BlockstackNetwork } from 'blockstack/lib/network';
8+
import { FetchFn, createFetchFn } from '@stacks/common';
99

1010
export interface CLI_NETWORK_OPTS {
1111
consensusHash: string | null;
@@ -187,15 +187,16 @@ export class CLINetworkAdapter {
187187
getNamespaceBurnAddress(
188188
namespace: string,
189189
useCLI: boolean = true,
190-
receiveFeesPeriod: number = -1
190+
receiveFeesPeriod: number = -1,
191+
fetchFn: FetchFn = createFetchFn()
191192
): Promise<string> {
192193
// override with CLI option
193194
if (this.namespaceBurnAddress && useCLI) {
194195
return new Promise((resolve: any) => resolve(this.namespaceBurnAddress));
195196
}
196197

197198
return Promise.all([
198-
fetch(`${this.legacyNetwork.blockstackAPIUrl}/v1/namespaces/${namespace}`),
199+
fetchFn(`${this.legacyNetwork.blockstackAPIUrl}/v1/namespaces/${namespace}`),
199200
this.legacyNetwork.getBlockHeight(),
200201
])
201202
.then(([resp, blockHeight]: [any, number]) => {
@@ -245,10 +246,10 @@ export class CLINetworkAdapter {
245246
});
246247
}
247248

248-
getBlockchainNameRecord(name: string): Promise<any> {
249+
getBlockchainNameRecord(name: string, fetchFn: FetchFn = createFetchFn()): Promise<any> {
249250
// TODO: send to blockstack.js
250251
const url = `${this.legacyNetwork.blockstackAPIUrl}/v1/blockchains/bitcoin/names/${name}`;
251-
return fetch(url)
252+
return fetchFn(url)
252253
.then(resp => {
253254
if (resp.status !== 200) {
254255
throw new Error(`Bad response status: ${resp.status}`);
@@ -268,10 +269,14 @@ export class CLINetworkAdapter {
268269
});
269270
}
270271

271-
getNameHistory(name: string, page: number): Promise<Record<string, any[]>> {
272+
getNameHistory(
273+
name: string,
274+
page: number,
275+
fetchFn: FetchFn = createFetchFn()
276+
): Promise<Record<string, any[]>> {
272277
// TODO: send to blockstack.js
273278
const url = `${this.legacyNetwork.blockstackAPIUrl}/v1/names/${name}/history?page=${page}`;
274-
return fetch(url)
279+
return fetchFn(url)
275280
.then(resp => {
276281
if (resp.status !== 200) {
277282
throw new Error(`Bad response status: ${resp.status}`);

packages/common/src/fetchUtil.ts

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,3 +36,120 @@ export async function fetchPrivate(input: RequestInfo, init?: RequestInit): Prom
3636
const fetchResult = await fetch(input, fetchOpts);
3737
return fetchResult;
3838
}
39+
40+
export type FetchFn = (url: string, init?: RequestInit) => Promise<Response>;
41+
42+
export interface RequestContext {
43+
fetch: FetchFn;
44+
url: string;
45+
init: RequestInit;
46+
}
47+
48+
export interface ResponseContext {
49+
fetch: FetchFn;
50+
url: string;
51+
init: RequestInit;
52+
response: Response;
53+
}
54+
55+
export interface FetchParams {
56+
url: string;
57+
init: RequestInit;
58+
}
59+
60+
export interface FetchMiddleware {
61+
pre?: (context: RequestContext) => PromiseLike<FetchParams | void> | FetchParams | void;
62+
post?: (context: ResponseContext) => Promise<Response | void> | Response | void;
63+
}
64+
export interface ApiKeyMiddlewareOpts {
65+
/** The middleware / API key header will only be added to requests matching this host. */
66+
host?: RegExp | string;
67+
/** The http header name used for specifying the API key value. */
68+
httpHeader?: string;
69+
/** The API key string to specify as an http header value. */
70+
apiKey: string;
71+
}
72+
73+
/** @internal */
74+
export function hostMatches(host: string, pattern: string | RegExp) {
75+
if (typeof pattern === 'string') return pattern === host;
76+
return (pattern as RegExp).exec(host);
77+
}
78+
79+
/**
80+
* Creates a new middleware from an API key.
81+
* @example
82+
* ```
83+
* const apiMiddleware = createApiKeyMiddleware("example_e8e044a3_41d8b0fe_3dd3988ef302");
84+
* const fetchFn = createFetchFn(apiMiddleware);
85+
* const network = new StacksMainnet({ fetchFn });
86+
* ```
87+
* @category Network
88+
* @related {@link createFetchFn}, {@link StacksNetwork}
89+
*/
90+
export function createApiKeyMiddleware({
91+
apiKey,
92+
host = /(.*)api(.*)\.stacks\.co$/i,
93+
httpHeader = 'x-api-key',
94+
}: ApiKeyMiddlewareOpts): FetchMiddleware {
95+
return {
96+
pre: context => {
97+
const reqUrl = new URL(context.url);
98+
if (!hostMatches(reqUrl.host, host)) return; // Skip middleware if host does not match pattern
99+
100+
const headers = new Headers(context.init.headers);
101+
headers.set(httpHeader, apiKey);
102+
context.init.headers = headers;
103+
},
104+
};
105+
}
106+
107+
function argsForCreateFetchFn(args: any[]): { middlewares: FetchMiddleware[]; fetchLib: FetchFn } {
108+
let fetchLib: FetchFn = fetch;
109+
let middlewares: FetchMiddleware[] = [];
110+
if (args.length > 0 && typeof args[0] === 'function') {
111+
fetchLib = args.shift();
112+
}
113+
if (args.length > 0) {
114+
middlewares = args;
115+
}
116+
return { middlewares, fetchLib };
117+
}
118+
119+
export function createFetchFn(fetchLib: FetchFn, ...middleware: FetchMiddleware[]): FetchFn;
120+
export function createFetchFn(...middleware: FetchMiddleware[]): FetchFn;
121+
export function createFetchFn(...args: any[]): FetchFn {
122+
const { middlewares: middlewareArgs, fetchLib } = argsForCreateFetchFn(args);
123+
const middlewares = [...middlewareArgs]; // shallow copy
124+
125+
const fetchFn = async (url: string, init?: RequestInit | undefined): Promise<Response> => {
126+
let fetchParams = { url, init: init ?? {} };
127+
for (const middleware of middlewares) {
128+
if (typeof middleware.pre !== 'function') continue;
129+
const result = await Promise.resolve(
130+
middleware.pre({
131+
fetch: fetchLib,
132+
...fetchParams,
133+
})
134+
);
135+
fetchParams = result ?? fetchParams;
136+
}
137+
138+
let response = await fetchLib(fetchParams.url, fetchParams.init);
139+
140+
for (const middleware of middlewares) {
141+
if (typeof middleware.post !== 'function') continue;
142+
const result = await Promise.resolve(
143+
middleware.post({
144+
fetch: fetchLib,
145+
url: fetchParams.url,
146+
init: fetchParams.init,
147+
response: response.clone(),
148+
})
149+
);
150+
response = result ?? response;
151+
}
152+
return response;
153+
};
154+
return fetchFn;
155+
}

packages/keychain/src/identity.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { makeAuthResponse } from '@stacks/auth';
2+
import { FetchFn, createFetchFn } from '@stacks/common';
23
import { getPublicKeyFromPrivate, publicKeyToAddress } from '@stacks/encryption';
34
import { bip32 } from 'bitcoinjs-lib';
45
import { Identity as IdentifyInterface, Profile } from './common';
@@ -130,9 +131,9 @@ export class Identity implements IdentifyInterface {
130131
return `${gaiaUrl}${this.address}/profile.json`;
131132
}
132133

133-
async fetchNames() {
134+
async fetchNames(fetchFn: FetchFn = createFetchFn()) {
134135
const getNamesUrl = `https://stacks-node-api.stacks.co/v1/addresses/bitcoin/${this.address}`;
135-
const res = await fetchPrivate(getNamesUrl);
136+
const res = await fetchFn(getNamesUrl);
136137
const data = await res.json();
137138
const { names }: { names: string[] } = data;
138139
return names;

packages/keychain/src/profiles.ts

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,9 @@
1-
import { connectToGaiaHub } from '@stacks/storage';
2-
import { signProfileToken, wrapProfileToken, makeProfileZoneFile } from '@stacks/profile';
1+
import { createFetchFn, FetchFn } from '@stacks/common';
2+
import { makeProfileZoneFile, signProfileToken, wrapProfileToken } from '@stacks/profile';
3+
import { connectToGaiaHub, GaiaHubConfig } from '@stacks/storage';
34
import { Identity, Profile } from './common';
45
import { IdentityKeyPair } from './utils';
56
import { uploadToGaiaHub } from './utils/gaia';
6-
import { GaiaHubConfig } from '@stacks/storage';
7-
import { fetchPrivate } from '@stacks/common';
87

98
export const DEFAULT_PROFILE: Profile = {
109
'@type': 'Person',
@@ -61,13 +60,15 @@ interface SendToRegistrarParams {
6160
subdomain: Subdomains;
6261
zoneFile: string;
6362
identity: Identity;
63+
fetchFn?: FetchFn;
6464
}
6565

6666
const sendUsernameToRegistrar = async ({
6767
username,
6868
subdomain,
6969
zoneFile,
7070
identity,
71+
fetchFn = createFetchFn(),
7172
}: SendToRegistrarParams) => {
7273
const { registerUrl } = registrars[subdomain];
7374

@@ -82,7 +83,7 @@ const sendUsernameToRegistrar = async ({
8283
'Content-Type': 'application/json',
8384
};
8485

85-
const response = await fetchPrivate(registerUrl, {
86+
const response = await fetchFn(registerUrl, {
8687
method: 'POST',
8788
headers: requestHeaders,
8889
body: registrationRequestBody,
@@ -151,13 +152,15 @@ export const signAndUploadProfile = async ({
151152
export const fetchProfile = async ({
152153
identity,
153154
gaiaUrl,
155+
fetchFn = createFetchFn(),
154156
}: {
155157
identity: Identity;
156158
gaiaUrl: string;
159+
fetchFn?: FetchFn;
157160
}) => {
158161
try {
159162
const url = await identity.profileUrl(gaiaUrl);
160-
const res = await fetchPrivate(url);
163+
const res = await fetchFn(url);
161164
if (res.ok) {
162165
const json = await res.json();
163166
const { decodedToken } = json[0];

0 commit comments

Comments
 (0)