Skip to content

Commit b75de92

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

File tree

31 files changed

+34730
-38919
lines changed

31 files changed

+34730
-38919
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.

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -67,14 +67,14 @@
6767
"rollup-plugin-node-globals": "^1.4.0",
6868
"rollup-plugin-terser": "^7.0.2",
6969
"stream-http": "^3.2.0",
70-
"typedoc": "^0.22.13",
70+
"typedoc": "^0.22.15",
7171
"typescript": "^4.2.4",
7272
"vite-compatible-readable-stream": "^3.6.0"
7373
},
7474
"scripts": {
7575
"bootstrap": "lerna bootstrap",
7676
"build": "lerna run build",
77-
"build:docs": "rimraf docs && typedoc --tsconfig tsconfig.typedoc.json packages/**/src/index.ts --release-version $(node -p \"require('./lerna.json').version\")",
77+
"build:docs": "rimraf docs && RELEASE_VERSION=$(node -p \"require('./lerna.json').version\") && typedoc --tsconfig tsconfig.typedoc.json packages/**/src/index.ts --release-version $RELEASE_VERSION",
7878
"clean": "lerna clean",
7979
"lerna": "lerna",
8080
"lint": "npm run lint:eslint && npm run lint:prettier",

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: 148 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -7,32 +7,168 @@ const defaultFetchOpts: RequestInit = {
77
referrerPolicy: 'origin', // Use origin value for referrer policy
88
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Referrer-Policy
99
};
10-
/*
10+
11+
/**
1112
* Get fetch options
12-
* @return fetchOptions
13+
* @category Network
1314
*/
1415
export const getFetchOptions = () => {
1516
return defaultFetchOpts;
1617
};
17-
/*
18-
* Set fetch options
19-
* Users can change default referrer as well as other options when fetch is used internally by stacks.js libraries or from server side
18+
19+
/**
20+
* Sets global fetch options for stacks.js network calls.
21+
*
2022
* @example
21-
* Reference: https://developer.mozilla.org/en-US/docs/Web/API/Request/Request
22-
* setFetchOptions({ referrer: 'no-referrer', referrerPolicy: 'no-referrer', ... other options as per above reference });
23-
* Now all the subsequent fetchPrivate will use above options
24-
* @return fetchOptions
23+
* Users can change the default referrer as well as other options when fetch is used internally by stacks.js:
24+
* ```
25+
* setFetchOptions({ referrer: 'no-referrer', referrerPolicy: 'no-referrer', ...otherRequestOptions });
26+
* ```
27+
* After calling {@link setFetchOptions} all subsequent network calls will use the specified options above.
28+
*
29+
* @see MDN Request: https://developer.mozilla.org/en-US/docs/Web/API/Request/Request
30+
* @returns global fetch options after merging with previous options (or defaults)
31+
* @category Network
32+
* @related {@link getFetchOptions}
2533
*/
26-
export const setFetchOptions = (ops: RequestInit) => {
34+
export const setFetchOptions = (ops: RequestInit): RequestInit => {
2735
return Object.assign(defaultFetchOpts, ops);
2836
};
2937

30-
/** @ignore */
31-
export async function fetchPrivate(input: RequestInfo, init?: RequestInit): Promise<Response> {
38+
/** @internal */
39+
export async function fetchWrapper(input: RequestInfo, init?: RequestInit): Promise<Response> {
3240
const fetchOpts = {};
3341
// Use the provided options in request options along with default or user provided values
3442
Object.assign(fetchOpts, init, defaultFetchOpts);
3543

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

packages/common/tests/fetch.test.ts

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,17 @@
1-
import { fetchPrivate, getFetchOptions, setFetchOptions } from '../src'
2-
import fetchMock from "jest-fetch-mock";
1+
import { fetchWrapper, getFetchOptions, setFetchOptions } from '../src';
2+
import fetchMock from 'jest-fetch-mock';
33

44
test('Verify fetch private options', async () => {
5-
const defaultOptioins = getFetchOptions();
5+
const defaultOptioins = getFetchOptions();
66

77
expect(defaultOptioins).toEqual({ referrerPolicy: 'origin' });
88

99
// Override default options when fetchPrivate is called internally by other stacks.js libraries like transactions or from server side
1010
// This is for developers as they cannot directly pass options directly in fetchPrivate
11-
const modifiedOptions: RequestInit= { referrer: 'http://test.com', referrerPolicy: 'same-origin' };
11+
const modifiedOptions: RequestInit = {
12+
referrer: 'http://test.com',
13+
referrerPolicy: 'same-origin',
14+
};
1215

1316
// Developers can set fetch options globally one time specifically when fetchPrivate is used internally by stacks.js libraries
1417
setFetchOptions(modifiedOptions);
@@ -18,11 +21,10 @@ test('Verify fetch private options', async () => {
1821
// Browser will replace about:client with actual url but it will not be visible in test case
1922
fetchMock.mockOnce(`{ status: 'success'}`, { headers: modifiedOptions as any });
2023

21-
const result = await fetchPrivate('https://example.com');
24+
const result = await fetchWrapper('https://example.com');
2225

2326
// Verify the request options
2427
expect(result.status).toEqual(200);
2528
expect(result.headers.get('referrer')).toEqual(modifiedOptions.referrer);
2629
expect(result.headers.get('referrerPolicy')).toEqual(modifiedOptions.referrerPolicy);
27-
})
28-
30+
});

0 commit comments

Comments
 (0)