@openstax/ts-utils 1.39.0 → 1.40.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/coolFile.d.ts +1 -0
- package/dist/cjs/misc/asyncPool.d.ts +24 -0
- package/dist/cjs/misc/asyncPool.js +58 -0
- package/dist/cjs/misc/hashValue.d.ts +1 -1
- package/dist/cjs/misc/hashValue.js +2 -1
- package/dist/cjs/routing/index.d.ts +18 -1
- package/dist/cjs/routing/index.js +39 -21
- package/dist/cjs/routing/lambdaBatchRequestHandler.d.ts +13 -0
- package/dist/cjs/routing/lambdaBatchRequestHandler.js +71 -0
- package/dist/cjs/services/authProvider/subrequest.js +4 -2
- package/dist/cjs/services/authProvider/utils/userSubrequest.d.ts +1 -1
- package/dist/cjs/services/httpMessageVerifier/index.js +50 -10
- package/dist/cjs/tsconfig.without-specs.cjs.tsbuildinfo +1 -1
- package/dist/esm/misc/asyncPool.d.ts +24 -0
- package/dist/esm/misc/asyncPool.js +54 -0
- package/dist/esm/misc/hashValue.d.ts +1 -1
- package/dist/esm/misc/hashValue.js +2 -1
- package/dist/esm/routing/index.d.ts +18 -1
- package/dist/esm/routing/index.js +39 -21
- package/dist/esm/routing/lambdaBatchRequestHandler.d.ts +13 -0
- package/dist/esm/routing/lambdaBatchRequestHandler.js +67 -0
- package/dist/esm/services/authProvider/subrequest.js +4 -2
- package/dist/esm/services/authProvider/utils/userSubrequest.d.ts +1 -1
- package/dist/esm/services/httpMessageVerifier/index.js +50 -10
- package/dist/esm/tsconfig.without-specs.esm.tsbuildinfo +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -0
- package/package.json +2 -1
- package/script/bin/.init-params-script.bash.swp +0 -0
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
type PromiseInvoker<T> = () => Promise<T>;
|
|
2
|
+
type UnwrapPromiseInvoker<T> = T extends PromiseInvoker<infer U> ? U : never;
|
|
3
|
+
type UnwrapPromiseInvokers<T extends readonly PromiseInvoker<any>[]> = {
|
|
4
|
+
[K in keyof T]: UnwrapPromiseInvoker<T[K]>;
|
|
5
|
+
};
|
|
6
|
+
/**
|
|
7
|
+
* Executes a tuple of promise invokers with a concurrency limit.
|
|
8
|
+
* Similar to Promise.all but limits the number of in-flight promises.
|
|
9
|
+
*
|
|
10
|
+
* @param invokers Tuple of functions that return promises
|
|
11
|
+
* @param concurrency Maximum number of promises to execute concurrently
|
|
12
|
+
* @returns Promise that resolves with a tuple of results in the same order as the invokers
|
|
13
|
+
* @throws Rejects with the first error encountered (similar to Promise.all behavior)
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* const results = await asyncPool([
|
|
17
|
+
* () => fetch('/api/1').then(r => r.json() as Promise<number>),
|
|
18
|
+
* () => fetch('/api/2').then(r => r.text()),
|
|
19
|
+
* () => fetch('/api/3').then(r => r.json() as Promise<boolean>),
|
|
20
|
+
* ], 2);
|
|
21
|
+
* // results has type [number, string, boolean]
|
|
22
|
+
*/
|
|
23
|
+
export declare const asyncPool: <T extends readonly PromiseInvoker<any>[]>(invokers: T, concurrency: number) => Promise<UnwrapPromiseInvokers<T>>;
|
|
24
|
+
export {};
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Executes a tuple of promise invokers with a concurrency limit.
|
|
3
|
+
* Similar to Promise.all but limits the number of in-flight promises.
|
|
4
|
+
*
|
|
5
|
+
* @param invokers Tuple of functions that return promises
|
|
6
|
+
* @param concurrency Maximum number of promises to execute concurrently
|
|
7
|
+
* @returns Promise that resolves with a tuple of results in the same order as the invokers
|
|
8
|
+
* @throws Rejects with the first error encountered (similar to Promise.all behavior)
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* const results = await asyncPool([
|
|
12
|
+
* () => fetch('/api/1').then(r => r.json() as Promise<number>),
|
|
13
|
+
* () => fetch('/api/2').then(r => r.text()),
|
|
14
|
+
* () => fetch('/api/3').then(r => r.json() as Promise<boolean>),
|
|
15
|
+
* ], 2);
|
|
16
|
+
* // results has type [number, string, boolean]
|
|
17
|
+
*/
|
|
18
|
+
export const asyncPool = async (invokers, concurrency) => {
|
|
19
|
+
if (concurrency <= 0) {
|
|
20
|
+
throw new Error('Concurrency must be greater than 0');
|
|
21
|
+
}
|
|
22
|
+
const results = new Array(invokers.length);
|
|
23
|
+
let rejected = false;
|
|
24
|
+
let currentIndex = 0;
|
|
25
|
+
let completedCount = 0;
|
|
26
|
+
return new Promise((resolve, reject) => {
|
|
27
|
+
const executeNext = () => {
|
|
28
|
+
if (completedCount === invokers.length) {
|
|
29
|
+
resolve(results);
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
while (currentIndex < invokers.length && (currentIndex - completedCount) < concurrency) {
|
|
33
|
+
const index = currentIndex++;
|
|
34
|
+
const invoker = invokers[index];
|
|
35
|
+
invoker()
|
|
36
|
+
.then((result) => {
|
|
37
|
+
if (rejected) {
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
results[index] = result;
|
|
41
|
+
completedCount++;
|
|
42
|
+
executeNext();
|
|
43
|
+
})
|
|
44
|
+
.catch((error) => {
|
|
45
|
+
if (!rejected) {
|
|
46
|
+
rejected = true;
|
|
47
|
+
reject(error);
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
executeNext();
|
|
53
|
+
});
|
|
54
|
+
};
|
|
@@ -5,9 +5,10 @@ import { createHash } from 'crypto';
|
|
|
5
5
|
* @example hashValue({someKey: 'someValue'})
|
|
6
6
|
*/
|
|
7
7
|
export const hashValue = (value) => {
|
|
8
|
+
var _a;
|
|
8
9
|
// hack for sorting keys https://stackoverflow.com/a/53593328/14809536
|
|
9
10
|
const allKeys = new Set();
|
|
10
11
|
JSON.stringify(value, (k, v) => (allKeys.add(k), v));
|
|
11
|
-
const strValue = JSON.stringify(value, Array.from(allKeys).sort());
|
|
12
|
+
const strValue = (_a = JSON.stringify(value, Array.from(allKeys).sort())) !== null && _a !== void 0 ? _a : 'undefined';
|
|
12
13
|
return createHash('sha1').update(strValue).digest('hex');
|
|
13
14
|
};
|
|
@@ -187,6 +187,22 @@ type RequestResponder<Sa, Ri, Ro> = {
|
|
|
187
187
|
logger: Logger;
|
|
188
188
|
}) => RoF): (request: Ri) => RoF;
|
|
189
189
|
};
|
|
190
|
+
export type BatchItem<Ro> = {
|
|
191
|
+
id: string;
|
|
192
|
+
makeRequest: () => undefined | Ro;
|
|
193
|
+
};
|
|
194
|
+
export type BatchHandler<Sa, Ri, Ro> = (services: CompatibleServices<Sa>) => ({
|
|
195
|
+
isBatchRequest: (request: Ri) => boolean;
|
|
196
|
+
extractBatchRequests: (request: Ri) => {
|
|
197
|
+
id: string;
|
|
198
|
+
request: Ri;
|
|
199
|
+
}[];
|
|
200
|
+
responseMiddleware?: (app: Sa) => (response: Ro | undefined, request: {
|
|
201
|
+
request: Ri;
|
|
202
|
+
logger: Logger;
|
|
203
|
+
}) => Ro;
|
|
204
|
+
composeBatchResponse: (items: BatchItem<Ro>[]) => Ro;
|
|
205
|
+
});
|
|
190
206
|
/**
|
|
191
207
|
* A factory factory for creating request responders (functions that take a request and return a
|
|
192
208
|
* response -- these functions let us implement Lambda `handler` functions).
|
|
@@ -217,12 +233,13 @@ type RequestResponder<Sa, Ri, Ro> = {
|
|
|
217
233
|
* );
|
|
218
234
|
* ```
|
|
219
235
|
*/
|
|
220
|
-
export declare const makeGetRequestResponder: <Sa, Ru, Ri, Ro>() => ({ routes, pathExtractor, routeMatcher, errorHandler, logExtractor }: {
|
|
236
|
+
export declare const makeGetRequestResponder: <Sa, Ru, Ri, Ro>() => ({ routes, pathExtractor, routeMatcher, errorHandler, logExtractor, batch }: {
|
|
221
237
|
routes: () => AnySpecificRoute<Ru, Sa, Ri, Ro>[];
|
|
222
238
|
pathExtractor: RequestPathExtractor<Ri>;
|
|
223
239
|
logExtractor?: RequestLogExtractor<Ri>;
|
|
224
240
|
routeMatcher?: RequestRouteMatcher<Ri, AnySpecificRoute<Ru, Sa, Ri, Ro>>;
|
|
225
241
|
errorHandler?: (e: Error, logger: Logger) => Ro;
|
|
242
|
+
batch?: BatchHandler<Sa, Ri, Ro>;
|
|
226
243
|
}) => RequestResponder<Sa, Ri, Ro>;
|
|
227
244
|
/** HTTP Headers */
|
|
228
245
|
export type HttpHeaders = {
|
|
@@ -150,11 +150,13 @@ const bindRoute = (services, appBinder, pathExtractor, matcher) => (route) => {
|
|
|
150
150
|
* );
|
|
151
151
|
* ```
|
|
152
152
|
*/
|
|
153
|
-
export const makeGetRequestResponder = () => ({ routes, pathExtractor, routeMatcher, errorHandler, logExtractor }) => (services, responseMiddleware) => {
|
|
153
|
+
export const makeGetRequestResponder = () => ({ routes, pathExtractor, routeMatcher, errorHandler, logExtractor, batch }) => (services, responseMiddleware) => {
|
|
154
154
|
const appBinderImpl = (app, middleware) => middleware(app, appBinder);
|
|
155
155
|
const appBinder = memoize(appBinderImpl);
|
|
156
156
|
const boundRoutes = routes().map(bindRoute(services, appBinder, pathExtractor, routeMatcher));
|
|
157
157
|
const boundResponseMiddleware = responseMiddleware ? responseMiddleware(services) : undefined;
|
|
158
|
+
const boundBatch = batch ? batch(services) : undefined;
|
|
159
|
+
const boundBatchResponseMiddleware = (boundBatch === null || boundBatch === void 0 ? void 0 : boundBatch.responseMiddleware) ? boundBatch.responseMiddleware(services) : undefined;
|
|
158
160
|
const appLogger = services.logger || createConsoleLogger();
|
|
159
161
|
// *note* this opaque promise guard is less generic than i hoped so
|
|
160
162
|
// i'm leaving it here instead of the guards file.
|
|
@@ -164,42 +166,58 @@ export const makeGetRequestResponder = () => ({ routes, pathExtractor, routeMatc
|
|
|
164
166
|
// promise or a non-promise value and the promise figures it out, but those
|
|
165
167
|
// types are getting complicated quickly here.
|
|
166
168
|
const isPromise = (thing) => thing instanceof Promise;
|
|
167
|
-
|
|
168
|
-
const
|
|
169
|
-
if (logExtractor) {
|
|
170
|
-
logger.setContext(logExtractor(request));
|
|
171
|
-
}
|
|
172
|
-
logger.log('begin request');
|
|
169
|
+
const requestResponder = (request, logger) => {
|
|
170
|
+
const boundErrorHandler = errorHandler ? (e) => errorHandler(e, logger) : undefined;
|
|
173
171
|
try {
|
|
174
172
|
const route = mapFind(boundRoutes, (route) => route(request, logger));
|
|
175
173
|
if (route) {
|
|
176
174
|
logger.log(`route matched ${route.name}`);
|
|
177
|
-
const result =
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
const errorHandlerWithMiddleware = (e) => boundResponseMiddleware ?
|
|
181
|
-
boundResponseMiddleware(errorHandler(e, logger), { request, logger }) : errorHandler(e, logger);
|
|
182
|
-
return result.catch(errorHandlerWithMiddleware);
|
|
175
|
+
const result = route.executor();
|
|
176
|
+
if (isPromise(result) && boundErrorHandler) {
|
|
177
|
+
return result.catch(boundErrorHandler);
|
|
183
178
|
}
|
|
184
179
|
else {
|
|
185
180
|
return result;
|
|
186
181
|
}
|
|
187
182
|
}
|
|
188
|
-
else if (boundResponseMiddleware) {
|
|
189
|
-
logger.log('no route matched, returning 404');
|
|
190
|
-
return boundResponseMiddleware(undefined, { request, logger });
|
|
191
|
-
}
|
|
192
183
|
}
|
|
193
184
|
catch (e) {
|
|
194
|
-
if (
|
|
195
|
-
return
|
|
196
|
-
? boundResponseMiddleware(errorHandler(e, logger), { request, logger })
|
|
197
|
-
: errorHandler(e, logger);
|
|
185
|
+
if (boundErrorHandler && e instanceof Error) {
|
|
186
|
+
return boundErrorHandler(e);
|
|
198
187
|
}
|
|
199
188
|
throw e;
|
|
200
189
|
}
|
|
201
190
|
return undefined;
|
|
202
191
|
};
|
|
192
|
+
return (request) => {
|
|
193
|
+
const logger = appLogger.createSubContext();
|
|
194
|
+
if (logExtractor)
|
|
195
|
+
logger.setContext(logExtractor(request));
|
|
196
|
+
logger.log('begin request');
|
|
197
|
+
let response = undefined;
|
|
198
|
+
if (boundBatch === null || boundBatch === void 0 ? void 0 : boundBatch.isBatchRequest(request)) {
|
|
199
|
+
const batchRequests = boundBatch.extractBatchRequests(request);
|
|
200
|
+
const batchResponses = [];
|
|
201
|
+
for (const batchRequest of batchRequests) {
|
|
202
|
+
const subRequestLogger = logger.createSubContext();
|
|
203
|
+
subRequestLogger.setContext({ batchRequestId: batchRequest.id });
|
|
204
|
+
if (logExtractor)
|
|
205
|
+
subRequestLogger.setContext(logExtractor(batchRequest.request));
|
|
206
|
+
batchResponses.push({
|
|
207
|
+
id: batchRequest.id,
|
|
208
|
+
makeRequest: () => {
|
|
209
|
+
const batchItemResponse = requestResponder(batchRequest.request, subRequestLogger);
|
|
210
|
+
return boundBatchResponseMiddleware ? boundBatchResponseMiddleware(batchItemResponse, { request: batchRequest.request, logger: subRequestLogger }) : batchItemResponse;
|
|
211
|
+
}
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
response = boundBatch.composeBatchResponse(batchResponses);
|
|
215
|
+
}
|
|
216
|
+
else {
|
|
217
|
+
response = requestResponder(request, logger);
|
|
218
|
+
}
|
|
219
|
+
return boundResponseMiddleware ? boundResponseMiddleware(response, { request, logger }) : response;
|
|
220
|
+
};
|
|
203
221
|
};
|
|
204
222
|
/**
|
|
205
223
|
* Returns a JSON response. Handles serializing the data to JSON and setting the content-type header.
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { APIGatewayProxyEventV2 } from 'aws-lambda';
|
|
2
|
+
import { Logger } from '../services/logger';
|
|
3
|
+
import { ApiResponse, BatchHandler } from '.';
|
|
4
|
+
type Ro = Promise<ApiResponse<number, any>>;
|
|
5
|
+
export declare const lambdaBatchRequestHandler: <Sa, Ri extends APIGatewayProxyEventV2>(config: {
|
|
6
|
+
batchPath: string;
|
|
7
|
+
concurrency: number;
|
|
8
|
+
responseMiddleware?: (app: Sa) => (response: Ro | undefined, request: {
|
|
9
|
+
request: Ri;
|
|
10
|
+
logger: Logger;
|
|
11
|
+
}) => Ro;
|
|
12
|
+
}) => BatchHandler<Sa, Ri, Ro>;
|
|
13
|
+
export {};
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { asyncPool } from '../misc/asyncPool';
|
|
3
|
+
import { getRequestBody } from './helpers';
|
|
4
|
+
import { apiJsonResponse } from '.';
|
|
5
|
+
const BatchItemSchema = z.object({
|
|
6
|
+
id: z.string(),
|
|
7
|
+
path: z.string(),
|
|
8
|
+
method: z.string(),
|
|
9
|
+
queryParams: z.record(z.string(), z.string()).optional(),
|
|
10
|
+
body: z.string().optional(),
|
|
11
|
+
headers: z.record(z.string(), z.string()).optional(),
|
|
12
|
+
});
|
|
13
|
+
const BatchRequestSchema = z.object({
|
|
14
|
+
requests: z.array(BatchItemSchema),
|
|
15
|
+
});
|
|
16
|
+
export const lambdaBatchRequestHandler = (config) => {
|
|
17
|
+
const { batchPath, concurrency, responseMiddleware } = config;
|
|
18
|
+
return () => ({
|
|
19
|
+
responseMiddleware,
|
|
20
|
+
isBatchRequest: (request) => {
|
|
21
|
+
return request.requestContext.http.path === batchPath;
|
|
22
|
+
},
|
|
23
|
+
extractBatchRequests: (request) => {
|
|
24
|
+
// Parse and validate payload
|
|
25
|
+
const payload = getRequestBody(request);
|
|
26
|
+
const parsed = BatchRequestSchema.parse(payload);
|
|
27
|
+
// Create modified requests for each batch item
|
|
28
|
+
return parsed.requests.map(item => {
|
|
29
|
+
var _a;
|
|
30
|
+
return ({
|
|
31
|
+
id: item.id,
|
|
32
|
+
request: {
|
|
33
|
+
...request,
|
|
34
|
+
requestContext: {
|
|
35
|
+
...request.requestContext,
|
|
36
|
+
http: {
|
|
37
|
+
...request.requestContext.http,
|
|
38
|
+
path: item.path,
|
|
39
|
+
method: item.method,
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
headers: { ...request.headers, ...item.headers },
|
|
43
|
+
body: (_a = item.body) !== null && _a !== void 0 ? _a : request.body,
|
|
44
|
+
rawQueryString: item.queryParams
|
|
45
|
+
? new URLSearchParams(item.queryParams).toString()
|
|
46
|
+
: request.rawQueryString,
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
},
|
|
51
|
+
composeBatchResponse: async (items) => {
|
|
52
|
+
const results = await asyncPool(items.map(({ id, makeRequest }) => async () => {
|
|
53
|
+
const response = await makeRequest();
|
|
54
|
+
return { id, response };
|
|
55
|
+
}), concurrency);
|
|
56
|
+
const data = {
|
|
57
|
+
results: results.map(({ id, response }) => ({
|
|
58
|
+
id,
|
|
59
|
+
statusCode: (response === null || response === void 0 ? void 0 : response.statusCode) || 500,
|
|
60
|
+
body: (response === null || response === void 0 ? void 0 : response.body) || '',
|
|
61
|
+
headers: (response === null || response === void 0 ? void 0 : response.headers) || {}
|
|
62
|
+
}))
|
|
63
|
+
};
|
|
64
|
+
return apiJsonResponse(200, data);
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
};
|
|
@@ -24,10 +24,12 @@ export const subrequestAuthProvider = (initializer) => (configProvider) => {
|
|
|
24
24
|
return undefined;
|
|
25
25
|
}
|
|
26
26
|
const user = await loadUserData(initializer.fetch, await accountsBase(), resolvedCookieName, token);
|
|
27
|
-
|
|
27
|
+
// this returns `{"error_id":null}` when the token is invalid
|
|
28
|
+
if (user.uuid) {
|
|
28
29
|
logger.setContext({ user: user.uuid });
|
|
30
|
+
return user;
|
|
29
31
|
}
|
|
30
|
-
return
|
|
32
|
+
return undefined;
|
|
31
33
|
};
|
|
32
34
|
const getUser = async () => {
|
|
33
35
|
if (!user) {
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
import { ApiUser } from '..';
|
|
2
2
|
import { GenericFetch } from '../../../fetch';
|
|
3
|
-
export declare const loadUserData: (fetch: GenericFetch, accountsBase: string, cookieName: string, token: string) => Promise<ApiUser
|
|
3
|
+
export declare const loadUserData: (fetch: GenericFetch, accountsBase: string, cookieName: string, token: string) => Promise<ApiUser>;
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { createHash } from 'crypto';
|
|
3
3
|
import { createVerifier, httpbis } from 'http-message-signatures';
|
|
4
4
|
import { SigningKeyNotFoundError } from 'jwks-rsa';
|
|
5
|
+
import { ByteSequence, parseDictionary, parseItem, Token } from 'structured-headers';
|
|
5
6
|
import { assertString } from '../../assertions';
|
|
6
7
|
import { resolveConfigValue } from '../../config';
|
|
7
8
|
import { InvalidRequestError } from '../../errors';
|
|
@@ -28,9 +29,25 @@ export const createHttpMessageVerifier = ({ configSpace, fetcher }) => (configPr
|
|
|
28
29
|
// but we do not bother matching the Signature-Agent names with the Signature names
|
|
29
30
|
// as that would be awkward with the packages we use
|
|
30
31
|
const signatureAgentString = (_a = headers['signature-agent']) !== null && _a !== void 0 ? _a : '';
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
32
|
+
let rawValues;
|
|
33
|
+
// Try parsing as RFC 8941 Item first (e.g., "https://url" or bare token)
|
|
34
|
+
try {
|
|
35
|
+
rawValues = [parseItem(signatureAgentString)];
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
// If item parsing fails, try parsing as a Dictionary (e.g., sig="https://url")
|
|
39
|
+
rawValues = Array.from(parseDictionary(signatureAgentString).values());
|
|
40
|
+
}
|
|
41
|
+
// Convert values to strings (handle Token, string, and reject others) and deduplicate
|
|
42
|
+
const signatureAgents = [...new Set(rawValues.map(([bareItem]) => {
|
|
43
|
+
if (bareItem instanceof Token) {
|
|
44
|
+
return bareItem.toString();
|
|
45
|
+
}
|
|
46
|
+
return assertString(bareItem, new InvalidRequestError('Signature-Agent values must be strings or tokens'));
|
|
47
|
+
}))];
|
|
48
|
+
if (signatureAgents.length === 0) {
|
|
49
|
+
throw new InvalidRequestError('Signature-Agent header is required');
|
|
50
|
+
}
|
|
34
51
|
const keys = (await Promise.all(signatureAgents.map(async (signatureAgent) => {
|
|
35
52
|
if (!await signatureAgentVerifier(signatureAgent)) {
|
|
36
53
|
throw new InvalidRequestError('Signature-Agent verification failed');
|
|
@@ -75,14 +92,37 @@ export const createHttpMessageVerifier = ({ configSpace, fetcher }) => (configPr
|
|
|
75
92
|
// For example, if a GET request uses configOverride to make content-digest not required
|
|
76
93
|
if (!headers['content-digest'])
|
|
77
94
|
return true;
|
|
78
|
-
|
|
79
|
-
|
|
95
|
+
let contentDigest;
|
|
96
|
+
try {
|
|
97
|
+
contentDigest = parseDictionary(headers['content-digest']);
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
80
100
|
throw new InvalidRequestError('Unsupported Content-Digest header format');
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
101
|
+
}
|
|
102
|
+
if (contentDigest.size === 0) {
|
|
103
|
+
throw new InvalidRequestError('Content-Digest header is required');
|
|
104
|
+
}
|
|
105
|
+
// Map Content-Digest algorithm names to Node's crypto algorithm names
|
|
106
|
+
const algorithmMap = {
|
|
107
|
+
'sha-256': 'sha256',
|
|
108
|
+
'sha-512': 'sha512',
|
|
109
|
+
};
|
|
110
|
+
for (const [algorithm, value] of contentDigest.entries()) {
|
|
111
|
+
const [bareItem] = value;
|
|
112
|
+
// Convert ByteSequence to string, or assert it's already a string
|
|
113
|
+
const hashValue = bareItem instanceof ByteSequence
|
|
114
|
+
? bareItem.toBase64()
|
|
115
|
+
: assertString(bareItem, new InvalidRequestError('Content-Digest values must be strings'));
|
|
116
|
+
const cryptoAlgorithm = algorithmMap[algorithm];
|
|
117
|
+
if (!cryptoAlgorithm) {
|
|
118
|
+
throw new InvalidRequestError(`Unsupported Content-Digest algorithm: ${algorithm}`);
|
|
119
|
+
}
|
|
120
|
+
// Calculate the hash
|
|
121
|
+
const calculatedHash = createHash(cryptoAlgorithm).update(assertString(body, new InvalidRequestError('Request body is required when Content-Digest is present'))).digest('base64');
|
|
122
|
+
// Compare with the provided hash
|
|
123
|
+
if (calculatedHash !== hashValue) {
|
|
124
|
+
throw new InvalidRequestError(`Calculated Content-Digest value did not match header for algorithm ${algorithm}`);
|
|
125
|
+
}
|
|
86
126
|
}
|
|
87
127
|
return true;
|
|
88
128
|
},
|