lambder 6.0.1 → 7.0.1
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/CHANGELOG.md +2316 -0
- package/README.md +60 -33
- package/dist/api/LambderApiAnswer.d.ts +40 -0
- package/dist/api/LambderApiAnswer.js +19 -0
- package/dist/api/LambderApiCallContext.d.ts +38 -0
- package/dist/api/LambderApiCallContext.js +13 -0
- package/dist/api/LambderApiDefinition.d.ts +18 -0
- package/dist/api/LambderApiDefinition.js +1 -0
- package/dist/api/LambderApiEnvelope.d.ts +67 -0
- package/dist/api/LambderApiEnvelope.js +180 -0
- package/dist/api/LambderApiGuards.d.ts +302 -0
- package/dist/api/LambderApiGuards.js +134 -0
- package/dist/api/LambderApiIdempotency.d.ts +122 -0
- package/dist/api/LambderApiIdempotency.js +330 -0
- package/dist/api/LambderApiPipeline.d.ts +134 -0
- package/dist/api/LambderApiPipeline.js +221 -0
- package/dist/api/LambderApiPolicyEngine.d.ts +36 -0
- package/dist/api/LambderApiPolicyEngine.js +77 -0
- package/dist/api/LambderApiRateLimits.d.ts +206 -0
- package/dist/api/LambderApiRateLimits.js +239 -0
- package/dist/api/LambderApiRequest.d.ts +101 -0
- package/dist/api/LambderApiRequest.js +129 -0
- package/dist/api/LambderApiValidationRefusal.d.ts +32 -0
- package/dist/api/LambderApiValidationRefusal.js +40 -0
- package/dist/client/LambderCaller.d.ts +62 -55
- package/dist/client/LambderCaller.js +147 -90
- package/dist/client/lambderFetchTransport.d.ts +9 -0
- package/dist/client/lambderFetchTransport.js +71 -0
- package/dist/client.d.ts +20 -10
- package/dist/client.js +11 -5
- package/dist/core/Lambder.d.ts +117 -253
- package/dist/core/Lambder.js +374 -341
- package/dist/core/LambderContext.d.ts +54 -44
- package/dist/core/LambderContext.js +41 -110
- package/dist/core/LambderCreateOptions.d.ts +285 -0
- package/dist/core/LambderCreateOptions.js +44 -0
- package/dist/core/LambderFiles.d.ts +1 -45
- package/dist/core/LambderFiles.js +18 -38
- package/dist/core/LambderIndexHtml.d.ts +37 -0
- package/dist/core/LambderIndexHtml.js +87 -0
- package/dist/core/LambderPolicyBuilders.d.ts +17 -0
- package/dist/core/LambderPolicyBuilders.js +16 -0
- package/dist/core/LambderPublicFiles.d.ts +5 -2
- package/dist/core/LambderPublicFiles.js +7 -2
- package/dist/core/LambderResolver.d.ts +8 -6
- package/dist/core/LambderResponse.d.ts +29 -11
- package/dist/core/LambderResponse.js +96 -49
- package/dist/core/LambderResponseBuilder.d.ts +18 -14
- package/dist/core/LambderResponseBuilder.js +19 -25
- package/dist/core/LambderRouting.d.ts +18 -7
- package/dist/core/LambderRouting.js +17 -7
- package/dist/core/LambderTemplatingEngine.d.ts +0 -62
- package/dist/core/LambderTemplatingEngine.js +7 -3
- package/dist/index.d.ts +85 -32
- package/dist/index.js +44 -16
- package/dist/invoke/LambderInvokeCaller.d.ts +46 -139
- package/dist/invoke/LambderInvokeCaller.js +140 -335
- package/dist/invoke/LambderInvokeOutcome.d.ts +165 -0
- package/dist/invoke/LambderInvokeOutcome.js +129 -0
- package/dist/invoke/LambderLambdaEvent.d.ts +81 -0
- package/dist/invoke/LambderLambdaEvent.js +187 -0
- package/dist/invoke/lambderHandlerTransport.d.ts +36 -0
- package/dist/invoke/lambderHandlerTransport.js +89 -0
- package/dist/mock/LambderMockApp.d.ts +352 -0
- package/dist/mock/LambderMockApp.js +815 -0
- package/dist/mock/LambderMockBrowserCookies.d.ts +55 -0
- package/dist/mock/LambderMockBrowserCookies.js +76 -0
- package/dist/mock/LambderMockCallRecorder.d.ts +85 -0
- package/dist/mock/LambderMockCallRecorder.js +183 -0
- package/dist/mock/LambderMockCreateOptions.d.ts +161 -0
- package/dist/mock/LambderMockCreateOptions.js +9 -0
- package/dist/mock/LambderMockEntryRegistry.d.ts +52 -0
- package/dist/mock/LambderMockEntryRegistry.js +126 -0
- package/dist/mock/LambderMockFailureInjector.d.ts +60 -0
- package/dist/mock/LambderMockFailureInjector.js +138 -0
- package/dist/mock/LambderMockTypes.d.ts +421 -0
- package/dist/mock/LambderMockTypes.js +8 -0
- package/dist/mock/lambderMockConsoleLogger.d.ts +16 -0
- package/dist/mock/lambderMockConsoleLogger.js +35 -0
- package/dist/mock/lambderMockInvokeTransport.d.ts +50 -0
- package/dist/mock/lambderMockInvokeTransport.js +52 -0
- package/dist/mock/lambderMockMswHandler.d.ts +99 -0
- package/dist/mock/lambderMockMswHandler.js +126 -0
- package/dist/mock.d.ts +34 -0
- package/dist/mock.js +27 -0
- package/dist/session/LambderSessionController.d.ts +199 -30
- package/dist/session/LambderSessionController.js +396 -82
- package/dist/session/LambderSessionCrypto.d.ts +66 -0
- package/dist/session/LambderSessionCrypto.js +101 -0
- package/dist/session/LambderSessionManager.d.ts +118 -80
- package/dist/session/LambderSessionManager.js +212 -184
- package/dist/shared/LambderI18n.d.ts +6 -6
- package/dist/shared/LambderI18n.js +1 -1
- package/dist/shared/contracts/LambderFileSource.d.ts +33 -0
- package/dist/shared/contracts/LambderFileSource.js +19 -0
- package/dist/shared/contracts/LambderIdempotencyStore.d.ts +66 -0
- package/dist/shared/contracts/LambderIdempotencyStore.js +12 -0
- package/dist/shared/contracts/LambderRateLimiter.d.ts +71 -0
- package/dist/shared/contracts/LambderRateLimiter.js +24 -0
- package/dist/shared/contracts/LambderSessionStore.d.ts +72 -0
- package/dist/shared/contracts/LambderSessionStore.js +13 -0
- package/dist/shared/transport/LambderApiTransport.d.ts +139 -0
- package/dist/shared/transport/LambderApiTransport.js +65 -0
- package/dist/shared/transport/LambderCookieJar.d.ts +121 -0
- package/dist/shared/transport/LambderCookieJar.js +246 -0
- package/dist/shared/transport/lambderCookieJarTransport.d.ts +30 -0
- package/dist/shared/transport/lambderCookieJarTransport.js +60 -0
- package/dist/shared/util/LambderBase64.d.ts +10 -0
- package/dist/shared/util/LambderBase64.js +27 -0
- package/dist/shared/util/LambderCallAbort.d.ts +62 -0
- package/dist/shared/util/LambderCallAbort.js +80 -0
- package/dist/shared/util/LambderClientIp.d.ts +32 -0
- package/dist/shared/util/LambderClientIp.js +56 -0
- package/dist/shared/util/LambderExpiringMap.d.ts +119 -0
- package/dist/shared/util/LambderExpiringMap.js +217 -0
- package/dist/shared/util/LambderKeyFields.d.ts +32 -0
- package/dist/shared/util/LambderKeyFields.js +34 -0
- package/dist/shared/util/LambderNodeModules.d.ts +9 -0
- package/dist/shared/util/LambderNodeModules.js +39 -0
- package/dist/shared/util/LambderOptionChecks.d.ts +17 -0
- package/dist/shared/util/LambderOptionChecks.js +33 -0
- package/dist/shared/util/LambderResponseBrand.d.ts +20 -0
- package/dist/shared/util/LambderResponseBrand.js +18 -0
- package/dist/shared/util/LambderTextDigest.d.ts +17 -0
- package/dist/shared/util/LambderTextDigest.js +34 -0
- package/dist/shared/util/LambderTypeUtilities.d.ts +33 -0
- package/dist/shared/util/LambderTypeUtilities.js +8 -0
- package/dist/shared/wire/LambderAnswerHeaders.d.ts +60 -0
- package/dist/shared/wire/LambderAnswerHeaders.js +94 -0
- package/dist/shared/wire/LambderApiContract.d.ts +129 -0
- package/dist/shared/wire/LambderApiOptionValues.d.ts +39 -0
- package/dist/shared/wire/LambderApiOptionValues.js +11 -0
- package/dist/shared/wire/LambderApiOutcome.d.ts +128 -0
- package/dist/shared/{LambderApiOutcome.js → wire/LambderApiOutcome.js} +16 -9
- package/dist/shared/{LambderApiError.d.ts → wire/LambderApiRefusal.d.ts} +48 -26
- package/dist/shared/{LambderApiError.js → wire/LambderApiRefusal.js} +13 -11
- package/dist/shared/wire/LambderCallOptions.d.ts +171 -0
- package/dist/shared/wire/LambderCallOptions.js +17 -0
- package/dist/shared/{LambderCompressionCodec.d.ts → wire/LambderCompressionCodec.d.ts} +10 -6
- package/dist/shared/{LambderCompressionCodec.js → wire/LambderCompressionCodec.js} +67 -23
- package/dist/shared/{LambderCompressionOption.d.ts → wire/LambderCompressionOption.d.ts} +1 -1
- package/dist/shared/{LambderCompressionOption.js → wire/LambderCompressionOption.js} +3 -4
- package/dist/shared/{LambderCrashDetail.d.ts → wire/LambderCrashDetail.d.ts} +10 -0
- package/dist/shared/{LambderCrashDetail.js → wire/LambderCrashDetail.js} +30 -0
- package/dist/shared/wire/LambderHttpStatus.d.ts +12 -0
- package/dist/shared/wire/LambderHttpStatus.js +1 -0
- package/dist/shared/{LambderRequestPayload.d.ts → wire/LambderRequestPayload.d.ts} +25 -17
- package/dist/shared/{LambderRequestPayload.js → wire/LambderRequestPayload.js} +29 -52
- package/dist/shared/wire/LambderSessionCookieNames.d.ts +9 -0
- package/dist/shared/wire/LambderSessionCookieNames.js +9 -0
- package/dist/stores/LambderDdbCache.d.ts +12 -9
- package/dist/stores/LambderDdbCache.js +56 -47
- package/dist/stores/{LambderDdbIdempotency.d.ts → LambderDdbIdempotencyStore.d.ts} +41 -31
- package/dist/stores/LambderDdbIdempotencyStore.js +319 -0
- package/dist/stores/LambderDdbRateLimiter.d.ts +30 -49
- package/dist/stores/LambderDdbRateLimiter.js +47 -45
- package/dist/stores/LambderDdbSdk.d.ts +83 -6
- package/dist/stores/LambderDdbSdk.js +83 -2
- package/dist/stores/LambderDdbSessionStore.d.ts +65 -0
- package/dist/stores/LambderDdbSessionStore.js +161 -0
- package/dist/stores/LambderHttpFileSource.d.ts +1 -1
- package/dist/stores/LambderHttpFileSource.js +10 -1
- package/dist/stores/LambderLocalFileSource.d.ts +15 -0
- package/dist/stores/LambderLocalFileSource.js +28 -0
- package/dist/stores/LambderMemoryIdempotencyStore.d.ts +63 -0
- package/dist/stores/LambderMemoryIdempotencyStore.js +113 -0
- package/dist/stores/LambderMemoryRateLimiter.d.ts +34 -0
- package/dist/stores/LambderMemoryRateLimiter.js +64 -0
- package/dist/stores/LambderMemorySessionStore.d.ts +48 -0
- package/dist/stores/LambderMemorySessionStore.js +74 -0
- package/dist/stores/LambderS3FileSource.d.ts +1 -1
- package/dist/stores/LambderS3FileSource.js +1 -1
- package/package.json +26 -24
- package/dist/client/LambderMSW.d.ts +0 -69
- package/dist/client/LambderMSW.js +0 -121
- package/dist/policies/LambderApiGuards.d.ts +0 -256
- package/dist/policies/LambderApiGuards.js +0 -94
- package/dist/policies/LambderApiIdempotency.d.ts +0 -58
- package/dist/policies/LambderApiIdempotency.js +0 -219
- package/dist/policies/LambderApiPolicies.d.ts +0 -42
- package/dist/policies/LambderApiPolicies.js +0 -52
- package/dist/policies/LambderApiRateLimits.d.ts +0 -132
- package/dist/policies/LambderApiRateLimits.js +0 -119
- package/dist/shared/LambderApiContract.d.ts +0 -57
- package/dist/shared/LambderApiOutcome.d.ts +0 -69
- package/dist/shared/LambderCallOptions.d.ts +0 -71
- package/dist/shared/LambderCallOptions.js +0 -16
- package/dist/shared/node-polyfills.d.ts +0 -4
- package/dist/shared/node-polyfills.js +0 -58
- package/dist/stores/LambderDdbIdempotency.js +0 -229
- package/dist/testing.d.ts +0 -9
- package/dist/testing.js +0 -8
- /package/dist/shared/{LambderApiContract.js → wire/LambderApiContract.js} +0 -0
- /package/dist/{core → shared/wire}/LambderCookie.d.ts +0 -0
- /package/dist/{core → shared/wire}/LambderCookie.js +0 -0
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
export type
|
|
1
|
+
import type { LambderHttpStatusCode } from "./LambderHttpStatus.js";
|
|
2
|
+
export type LambderApiRefusalOptions = {
|
|
3
3
|
/**
|
|
4
|
-
*
|
|
5
|
-
* `
|
|
6
|
-
*
|
|
7
|
-
*
|
|
4
|
+
* User-facing failure detail placed on the API envelope's `errorMessage`
|
|
5
|
+
* field: a refusal message (`{ type: "warning", content: "..." }`, with
|
|
6
|
+
* an app's own `code`) or a plain string. Defaults to the error message
|
|
7
|
+
* string, so a bare `throw new LambderApiRefusal("...")` is still
|
|
8
8
|
* visible to the client.
|
|
9
9
|
*/
|
|
10
|
-
errorMessage?:
|
|
10
|
+
errorMessage?: LambderAppRefusalMessage | string;
|
|
11
11
|
/** Sets the envelope's `notAuthorized` flag (routed to the caller's notAuthorizedHandler). */
|
|
12
12
|
notAuthorized?: boolean;
|
|
13
13
|
/** Sets the envelope's `sessionExpired` flag (the caller clears session cookies and calls sessionExpiredHandler). */
|
|
@@ -17,7 +17,7 @@ export type LambderApiErrorOptions = {
|
|
|
17
17
|
* semantic channel. Avoid 5xx (LambderCaller treats those as crashes) and
|
|
18
18
|
* 422 (reserved for input validation).
|
|
19
19
|
*/
|
|
20
|
-
statusCode?:
|
|
20
|
+
statusCode?: LambderHttpStatusCode;
|
|
21
21
|
/** Extra response headers on the refusal (e.g. Retry-After on a rate limit). */
|
|
22
22
|
headers?: Record<string, string>;
|
|
23
23
|
/** Underlying cause, preserved on the standard Error `cause` property. */
|
|
@@ -25,8 +25,8 @@ export type LambderApiErrorOptions = {
|
|
|
25
25
|
};
|
|
26
26
|
/**
|
|
27
27
|
* A typed refusal: "this request is denied/invalid" as opposed to "the server
|
|
28
|
-
* crashed". Throw it from anywhere in an API call's call stack
|
|
29
|
-
* hooks, or nested helpers that have no access to the per-request resolver
|
|
28
|
+
* crashed". Throw it from anywhere in an API call's call stack (handlers,
|
|
29
|
+
* hooks, or nested helpers that have no access to the per-request resolver)
|
|
30
30
|
* and the render pipeline maps it onto the structured API envelope
|
|
31
31
|
* (`res.api(null, { errorMessage, notAuthorized, sessionExpired })`) instead
|
|
32
32
|
* of routing it through setGlobalErrorHandler. Refusals therefore never reach
|
|
@@ -39,22 +39,22 @@ export type LambderApiErrorOptions = {
|
|
|
39
39
|
* checks) may import and throw it from packages used by both server and
|
|
40
40
|
* browser builds; in the browser it is just an Error.
|
|
41
41
|
*/
|
|
42
|
-
export declare class
|
|
42
|
+
export declare class LambderApiRefusal extends Error {
|
|
43
43
|
/**
|
|
44
44
|
* Brand for detection across duplicate lambder installs: when two copies
|
|
45
|
-
* of the package coexist in one bundle, `instanceof
|
|
45
|
+
* of the package coexist in one bundle, `instanceof LambderApiRefusal` fails
|
|
46
46
|
* across them while this marker does not. The pipeline checks the brand.
|
|
47
47
|
*/
|
|
48
|
-
readonly
|
|
49
|
-
readonly errorMessage?:
|
|
48
|
+
readonly isLambderApiRefusal = true;
|
|
49
|
+
readonly errorMessage?: LambderAppRefusalMessage | string;
|
|
50
50
|
readonly notAuthorized?: boolean;
|
|
51
51
|
readonly sessionExpired?: boolean;
|
|
52
|
-
readonly statusCode?:
|
|
52
|
+
readonly statusCode?: LambderHttpStatusCode;
|
|
53
53
|
readonly headers?: Record<string, string>;
|
|
54
|
-
constructor(message: string, options?:
|
|
54
|
+
constructor(message: string, options?: LambderApiRefusalOptions);
|
|
55
55
|
}
|
|
56
|
-
/** Brand-based type guard (see
|
|
57
|
-
export declare const
|
|
56
|
+
/** Brand-based type guard (see LambderApiRefusal.isLambderApiRefusal). */
|
|
57
|
+
export declare const isLambderApiRefusal: (err: unknown) => err is LambderApiRefusal;
|
|
58
58
|
/**
|
|
59
59
|
* The standard shape refusals carry on the envelope's errorMessage field.
|
|
60
60
|
* `code` is the refusal's machine-readable identity: clients branch and
|
|
@@ -62,16 +62,36 @@ export declare const isLambderApiError: (err: unknown) => err is LambderApiError
|
|
|
62
62
|
* human-readable fallback for codes a client does not know yet. Apps keep
|
|
63
63
|
* their own typed code vocabulary; the framework's own refusals carry a
|
|
64
64
|
* LambderRefusalCode. The caller's errorMessageHandler receives the object
|
|
65
|
-
* as-is
|
|
66
|
-
*
|
|
65
|
+
* as-is, typed as LambderAppRefusalMessage or a plain string; an app's own
|
|
66
|
+
* vocabulary goes in `code`.
|
|
67
67
|
*/
|
|
68
|
-
export type LambderRefusalMessage = {
|
|
68
|
+
export type LambderRefusalMessage<TAppCode extends string = never> = {
|
|
69
69
|
type: "warning" | "error" | "info";
|
|
70
|
-
/**
|
|
71
|
-
|
|
70
|
+
/**
|
|
71
|
+
* Machine-readable identity of the refusal: a LambderRefusalCode, plus
|
|
72
|
+
* whatever vocabulary the reader names in TAppCode.
|
|
73
|
+
*
|
|
74
|
+
* Parameterized rather than widened with `string & {}`, because a union
|
|
75
|
+
* with `string` in it does not narrow: inside a `switch(message.code)`
|
|
76
|
+
* the case was not assignable and the `default: never` assertion failed,
|
|
77
|
+
* which is exactly the exhaustiveness the codes exist for. A client that
|
|
78
|
+
* reads its own vocabulary declares it
|
|
79
|
+
* (`LambderRefusalMessage<"app/not-verified" | ...>`) and gets a switch
|
|
80
|
+
* that is checked; a value an app WRITES takes LambderAppRefusalMessage,
|
|
81
|
+
* where any code is welcome.
|
|
82
|
+
*/
|
|
83
|
+
code?: LambderRefusalCode | TAppCode;
|
|
72
84
|
title?: string;
|
|
73
85
|
content: string;
|
|
74
86
|
};
|
|
87
|
+
/**
|
|
88
|
+
* The refusal shape an app authors: any code, with the framework's own still
|
|
89
|
+
* autocompleting. What every option that takes a message from an app is
|
|
90
|
+
* typed as (a rate-limit policy's errorMessage, the mock's failure
|
|
91
|
+
* injection); LambderRefusalMessage itself defaults to the framework's codes
|
|
92
|
+
* alone, so a reader's switch over it is exhaustive.
|
|
93
|
+
*/
|
|
94
|
+
export type LambderAppRefusalMessage = LambderRefusalMessage<string & {}>;
|
|
75
95
|
/**
|
|
76
96
|
* Codes the framework stamps on the refusals it authors itself, under the
|
|
77
97
|
* reserved `lambder/` prefix so app codes never collide. Compare against
|
|
@@ -89,6 +109,8 @@ export declare const LAMBDER_REFUSAL_CODES: {
|
|
|
89
109
|
readonly apiNotFound: "lambder/api-not-found";
|
|
90
110
|
/** The request's compressed payload is malformed or over the size limit (400). */
|
|
91
111
|
readonly invalidRequestPayload: "lambder/invalid-request-payload";
|
|
112
|
+
/** Only the mock runtime emits it: the endpoint is registered as not mocked, with a reason. */
|
|
113
|
+
readonly notMocked: "lambder/not-mocked";
|
|
92
114
|
};
|
|
93
115
|
export type LambderRefusalCode = (typeof LAMBDER_REFUSAL_CODES)[keyof typeof LAMBDER_REFUSAL_CODES];
|
|
94
116
|
export type LambderRefuseOptions = {
|
|
@@ -103,7 +125,7 @@ export type LambderRefuseOptions = {
|
|
|
103
125
|
/** Sets the envelope's sessionExpired flag. */
|
|
104
126
|
sessionExpired?: boolean;
|
|
105
127
|
/** HTTP status of the refusal. Default 200; avoid 5xx (caller treats as crash) and 422 (reserved for validation). */
|
|
106
|
-
statusCode?:
|
|
128
|
+
statusCode?: LambderHttpStatusCode;
|
|
107
129
|
/** Extra response headers on the refusal (e.g. Retry-After). */
|
|
108
130
|
headers?: Record<string, string>;
|
|
109
131
|
/** Underlying cause, preserved on the Error cause property. */
|
|
@@ -111,10 +133,10 @@ export type LambderRefuseOptions = {
|
|
|
111
133
|
};
|
|
112
134
|
/**
|
|
113
135
|
* Refuse the current API call: a routine business "no" (not found, invalid
|
|
114
|
-
* input, not allowed) with a user-facing message. Throws a
|
|
136
|
+
* input, not allowed) with a user-facing message. Throws a LambderApiRefusal
|
|
115
137
|
* carrying the standard LambderRefusalMessage shape, so the pipeline maps it
|
|
116
138
|
* onto the structured envelope instead of a 500, and crash logging never
|
|
117
|
-
* sees it. Callable from anywhere in the call stack
|
|
139
|
+
* sees it. Callable from anywhere in the call stack: handlers, hooks,
|
|
118
140
|
* guards, shared helpers with no resolver access.
|
|
119
141
|
*
|
|
120
142
|
* The const carries the annotation so TypeScript applies never-return
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* A typed refusal: "this request is denied/invalid" as opposed to "the server
|
|
3
|
-
* crashed". Throw it from anywhere in an API call's call stack
|
|
4
|
-
* hooks, or nested helpers that have no access to the per-request resolver
|
|
3
|
+
* crashed". Throw it from anywhere in an API call's call stack (handlers,
|
|
4
|
+
* hooks, or nested helpers that have no access to the per-request resolver)
|
|
5
5
|
* and the render pipeline maps it onto the structured API envelope
|
|
6
6
|
* (`res.api(null, { errorMessage, notAuthorized, sessionExpired })`) instead
|
|
7
7
|
* of routing it through setGlobalErrorHandler. Refusals therefore never reach
|
|
@@ -14,13 +14,13 @@
|
|
|
14
14
|
* checks) may import and throw it from packages used by both server and
|
|
15
15
|
* browser builds; in the browser it is just an Error.
|
|
16
16
|
*/
|
|
17
|
-
export class
|
|
17
|
+
export class LambderApiRefusal extends Error {
|
|
18
18
|
/**
|
|
19
19
|
* Brand for detection across duplicate lambder installs: when two copies
|
|
20
|
-
* of the package coexist in one bundle, `instanceof
|
|
20
|
+
* of the package coexist in one bundle, `instanceof LambderApiRefusal` fails
|
|
21
21
|
* across them while this marker does not. The pipeline checks the brand.
|
|
22
22
|
*/
|
|
23
|
-
|
|
23
|
+
isLambderApiRefusal = true;
|
|
24
24
|
errorMessage;
|
|
25
25
|
notAuthorized;
|
|
26
26
|
sessionExpired;
|
|
@@ -28,7 +28,7 @@ export class LambderApiError extends Error {
|
|
|
28
28
|
headers;
|
|
29
29
|
constructor(message, options = {}) {
|
|
30
30
|
super(message, options.cause !== undefined ? { cause: options.cause } : undefined);
|
|
31
|
-
this.name = "
|
|
31
|
+
this.name = "LambderApiRefusal";
|
|
32
32
|
this.errorMessage = options.errorMessage ?? message;
|
|
33
33
|
this.notAuthorized = options.notAuthorized;
|
|
34
34
|
this.sessionExpired = options.sessionExpired;
|
|
@@ -36,8 +36,8 @@ export class LambderApiError extends Error {
|
|
|
36
36
|
this.headers = options.headers;
|
|
37
37
|
}
|
|
38
38
|
}
|
|
39
|
-
/** Brand-based type guard (see
|
|
40
|
-
export const
|
|
39
|
+
/** Brand-based type guard (see LambderApiRefusal.isLambderApiRefusal). */
|
|
40
|
+
export const isLambderApiRefusal = (err) => err instanceof Error && err.isLambderApiRefusal === true;
|
|
41
41
|
/**
|
|
42
42
|
* Codes the framework stamps on the refusals it authors itself, under the
|
|
43
43
|
* reserved `lambder/` prefix so app codes never collide. Compare against
|
|
@@ -55,13 +55,15 @@ export const LAMBDER_REFUSAL_CODES = {
|
|
|
55
55
|
apiNotFound: "lambder/api-not-found",
|
|
56
56
|
/** The request's compressed payload is malformed or over the size limit (400). */
|
|
57
57
|
invalidRequestPayload: "lambder/invalid-request-payload",
|
|
58
|
+
/** Only the mock runtime emits it: the endpoint is registered as not mocked, with a reason. */
|
|
59
|
+
notMocked: "lambder/not-mocked",
|
|
58
60
|
};
|
|
59
61
|
/**
|
|
60
62
|
* Refuse the current API call: a routine business "no" (not found, invalid
|
|
61
|
-
* input, not allowed) with a user-facing message. Throws a
|
|
63
|
+
* input, not allowed) with a user-facing message. Throws a LambderApiRefusal
|
|
62
64
|
* carrying the standard LambderRefusalMessage shape, so the pipeline maps it
|
|
63
65
|
* onto the structured envelope instead of a 500, and crash logging never
|
|
64
|
-
* sees it. Callable from anywhere in the call stack
|
|
66
|
+
* sees it. Callable from anywhere in the call stack: handlers, hooks,
|
|
65
67
|
* guards, shared helpers with no resolver access.
|
|
66
68
|
*
|
|
67
69
|
* The const carries the annotation so TypeScript applies never-return
|
|
@@ -69,7 +71,7 @@ export const LAMBDER_REFUSAL_CODES = {
|
|
|
69
71
|
* `row` is defined afterwards).
|
|
70
72
|
*/
|
|
71
73
|
export const refuse = (content, options = {}) => {
|
|
72
|
-
throw new
|
|
74
|
+
throw new LambderApiRefusal(content, {
|
|
73
75
|
errorMessage: {
|
|
74
76
|
type: options.type ?? "warning",
|
|
75
77
|
...(options.code !== undefined ? { code: options.code } : {}),
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The per-call options both callers take, the contract-driven typing of a
|
|
3
|
+
* call's arguments, and the runtime merge of guard inputs, shared by the
|
|
4
|
+
* browser caller (LambderCaller) and the server-side invoke caller
|
|
5
|
+
* (LambderInvokeCaller). Both speak the same envelope to the same kind of
|
|
6
|
+
* contract, so what an API demands of its caller (a guardInput-mode guard's
|
|
7
|
+
* value, say) is decided here once and the two callers cannot drift on it.
|
|
8
|
+
* Pure types and one dependency-free function, so the browser entry resolves
|
|
9
|
+
* it.
|
|
10
|
+
*/
|
|
11
|
+
import type { LambderContractIdempotencyOf } from "./LambderApiContract.js";
|
|
12
|
+
type IsAny<T> = 0 extends (1 & T) ? true : false;
|
|
13
|
+
/**
|
|
14
|
+
* The options every call takes, whichever caller sends it. Each caller adds
|
|
15
|
+
* its own on top: the browser's per-call handler overrides, the invoke
|
|
16
|
+
* caller's `clientIp` and `session`.
|
|
17
|
+
*/
|
|
18
|
+
export type LambderSharedCallOptions = {
|
|
19
|
+
/** Extra request headers the server sees. */
|
|
20
|
+
headers?: Record<string, string>;
|
|
21
|
+
/** Abort the call after this many ms; overrides the constructor default. */
|
|
22
|
+
timeoutMs?: number;
|
|
23
|
+
/** External abort signal, combined with the timeout when both are set. */
|
|
24
|
+
signal?: AbortSignal;
|
|
25
|
+
/**
|
|
26
|
+
* Overrides the constructor's requestCompression for this call: `false`
|
|
27
|
+
* sends the payload plainly (a hot path where the CPU matters more than
|
|
28
|
+
* the bytes), `true` compresses it regardless of the size threshold.
|
|
29
|
+
* Either way a payload is only sent compressed when that is smaller.
|
|
30
|
+
*/
|
|
31
|
+
compressRequest?: boolean;
|
|
32
|
+
/**
|
|
33
|
+
* Values for the API's guardInput-mode guards, keyed by guard name; sent
|
|
34
|
+
* beside the payload and consumed by the guards before validation. The
|
|
35
|
+
* typed contract makes this REQUIRED for APIs that declare such guards,
|
|
36
|
+
* except the guards a guardInputsProvider covers (these merge on top of
|
|
37
|
+
* the provider's values).
|
|
38
|
+
*/
|
|
39
|
+
guardInputs?: Record<string, unknown>;
|
|
40
|
+
/**
|
|
41
|
+
* Replay-protection key for APIs declared idempotent on the server.
|
|
42
|
+
* Generate once per logical operation with
|
|
43
|
+
* LambderCaller.createIdempotencyKey() and send the same key on retries:
|
|
44
|
+
* duplicates of an in-flight request refuse, and repeats of a completed
|
|
45
|
+
* one replay its stored response instead of re-executing. Must be
|
|
46
|
+
* UNGUESSABLE random (it scopes the replay record for logged-out clients)
|
|
47
|
+
* and at least 16 characters; the server refuses shorter keys with a 400.
|
|
48
|
+
*
|
|
49
|
+
* The typed contract makes this REQUIRED for an API whose entry declares
|
|
50
|
+
* `idempotency`, the way it does for guardInput values: a server
|
|
51
|
+
* declaration that reads as protection and silently provides none (the
|
|
52
|
+
* server runs a keyless call, which dedupes nothing) is exactly what the
|
|
53
|
+
* typed caller is for.
|
|
54
|
+
*/
|
|
55
|
+
idempotencyKey?: string;
|
|
56
|
+
};
|
|
57
|
+
/** The payload type one API of a contract takes; `any` for an untyped caller or a name the contract does not know. */
|
|
58
|
+
type LambderContractInputOf<TContract, TApiName> = IsAny<TContract> extends true ? any : TApiName extends keyof TContract ? TContract[TApiName] extends {
|
|
59
|
+
input: infer TInput;
|
|
60
|
+
} ? TInput : any : any;
|
|
61
|
+
/** The output type one API of a contract declares; `any` for an untyped caller or a name the contract does not know. */
|
|
62
|
+
export type LambderContractOutputOf<TContract, TApiName> = IsAny<TContract> extends true ? any : TApiName extends keyof TContract ? TContract[TApiName] extends {
|
|
63
|
+
output: infer TOutput;
|
|
64
|
+
} ? TOutput : any : any;
|
|
65
|
+
type GuardInputsOf<TEntry> = TEntry extends {
|
|
66
|
+
guardInputs: infer G;
|
|
67
|
+
} ? G : never;
|
|
68
|
+
/** Input type of guard G on one contract entry; never when that API does not declare it. */
|
|
69
|
+
type GuardInputOf<TEntry, G extends string> = GuardInputsOf<TEntry> extends infer I ? (G extends keyof I ? I[G] : never) : never;
|
|
70
|
+
/**
|
|
71
|
+
* What guardInputsProvider returns: for every provided guard name, the value
|
|
72
|
+
* the contract's APIs expect for it (a union across APIs when they differ).
|
|
73
|
+
* Naming a guard no API declares in guardInput mode resolves to never, so a
|
|
74
|
+
* typo fails the provider's return type instead of going missing at runtime.
|
|
75
|
+
*/
|
|
76
|
+
export type LambderProvidedGuardInputs<TContract, TProvided extends string> = IsAny<TContract> extends true ? Record<TProvided, unknown> : {
|
|
77
|
+
[G in TProvided]: {
|
|
78
|
+
[K in keyof TContract]: GuardInputOf<TContract[K], G>;
|
|
79
|
+
}[keyof TContract];
|
|
80
|
+
};
|
|
81
|
+
/**
|
|
82
|
+
* Supplies guardInputs for every call from one place (the organization the
|
|
83
|
+
* UI is on, a device token), keyed by guard name; per-call guardInputs
|
|
84
|
+
* merge on top. Name the guards it covers in the caller's second type
|
|
85
|
+
* parameter, `new LambderCaller<Contract, "orgPermission">`, and calls to
|
|
86
|
+
* APIs whose guardInput guards are all covered do not require the
|
|
87
|
+
* options argument. May be async; a throw fails the call as an unknown
|
|
88
|
+
* error before anything is sent.
|
|
89
|
+
*/
|
|
90
|
+
export type LambderGuardInputsProvider<TContract, TProvided extends string> = (apiName: keyof TContract & string) => LambderProvidedGuardInputs<TContract, TProvided> | Promise<LambderProvidedGuardInputs<TContract, TProvided>>;
|
|
91
|
+
/** Optional until the caller names provided guards: naming them without a provider would send nothing. */
|
|
92
|
+
export type LambderGuardInputsProviderOption<TContract, TProvided extends string> = [
|
|
93
|
+
TProvided
|
|
94
|
+
] extends [never] ? {
|
|
95
|
+
guardInputsProvider?: LambderGuardInputsProvider<TContract, TProvided>;
|
|
96
|
+
} : {
|
|
97
|
+
guardInputsProvider: LambderGuardInputsProvider<TContract, TProvided>;
|
|
98
|
+
};
|
|
99
|
+
/** An API's guardInput guards the provider does not cover: those the call must still pass. */
|
|
100
|
+
type RemainingGuardInputs<TEntry, TProvided extends string> = Omit<GuardInputsOf<TEntry>, TProvided>;
|
|
101
|
+
/**
|
|
102
|
+
* The guardInputs field of one API's options: absent when its guards take no
|
|
103
|
+
* client input, optional when the provider covers every one of them (they may
|
|
104
|
+
* still be overridden per call), and mandatory with the uncovered ones spelled
|
|
105
|
+
* out otherwise.
|
|
106
|
+
*/
|
|
107
|
+
type ContractGuardInputsField<TEntry, TProvided extends string> = [
|
|
108
|
+
GuardInputsOf<TEntry>
|
|
109
|
+
] extends [never] ? {} : [keyof RemainingGuardInputs<TEntry, TProvided>] extends [never] ? {
|
|
110
|
+
guardInputs?: Partial<GuardInputsOf<TEntry>>;
|
|
111
|
+
} : {
|
|
112
|
+
guardInputs: RemainingGuardInputs<TEntry, TProvided> & Partial<GuardInputsOf<TEntry>>;
|
|
113
|
+
};
|
|
114
|
+
/**
|
|
115
|
+
* The idempotencyKey field of one API's options: mandatory once the entry
|
|
116
|
+
* declares `idempotency`, absent otherwise.
|
|
117
|
+
*
|
|
118
|
+
* Read as "required unless it says false" rather than "required only when it
|
|
119
|
+
* says true", so an entry whose option widened to `boolean` (declared through
|
|
120
|
+
* a spread, or built in a helper) keeps the requirement instead of quietly
|
|
121
|
+
* losing its compile-time half.
|
|
122
|
+
*/
|
|
123
|
+
type ContractIdempotencyKeyField<TContract, TApiName> = TApiName extends keyof TContract ? [LambderContractIdempotencyOf<TContract, TApiName>] extends [never] ? {} : [LambderContractIdempotencyOf<TContract, TApiName>] extends [false] ? {} : {
|
|
124
|
+
idempotencyKey: string;
|
|
125
|
+
} : {};
|
|
126
|
+
/**
|
|
127
|
+
* What the contract adds to one call's options: the guardInputs field and the
|
|
128
|
+
* idempotencyKey field, each decided by what the API's entry declares.
|
|
129
|
+
*/
|
|
130
|
+
type ContractCallFields<TContract, TApiName, TProvided extends string> = TApiName extends keyof TContract ? ContractGuardInputsField<TContract[TApiName], TProvided> & ContractIdempotencyKeyField<TContract, TApiName> : {};
|
|
131
|
+
/**
|
|
132
|
+
* The options argument of one call: optional normally, REQUIRED when the
|
|
133
|
+
* contract demands something of it, so forgetting a guard's value or an
|
|
134
|
+
* idempotent API's key is a compile error at the call site rather than a 422
|
|
135
|
+
* from the server or a replay that never happens. TOptions is the caller's own
|
|
136
|
+
* per-call options type; the contract's fields are layered on top of it.
|
|
137
|
+
*
|
|
138
|
+
* `{} extends TFields` is the question "is every field the contract added
|
|
139
|
+
* optional": an empty object is assignable to a type whose properties are all
|
|
140
|
+
* optional and to nothing else.
|
|
141
|
+
*/
|
|
142
|
+
type LambderCallOptionsArg<TContract, TApiName, TProvided extends string, TOptions extends {
|
|
143
|
+
guardInputs?: Record<string, unknown>;
|
|
144
|
+
}> = IsAny<TContract> extends true ? [options?: TOptions] : TApiName extends keyof TContract ? ContractCallFields<TContract, TApiName, TProvided> extends infer TFields ? {} extends TFields ? [options?: TOptions & TFields] : [options: TOptions & TFields] : never : [options?: TOptions];
|
|
145
|
+
/**
|
|
146
|
+
* Everything one call passes after the API name: the payload, then the
|
|
147
|
+
* options, both decided by the contract.
|
|
148
|
+
*
|
|
149
|
+
* The payload is optional only when the API's input accepts undefined, so
|
|
150
|
+
* `caller.api("getUser")` against `input: { id: string }` is a compile error
|
|
151
|
+
* at the call site rather than a 422 from the server. Building it as one rest
|
|
152
|
+
* tuple is what makes that possible: a plain optional parameter cannot be
|
|
153
|
+
* made mandatory by a later type, and TypeScript has no per-argument
|
|
154
|
+
* conditional otherwise.
|
|
155
|
+
*
|
|
156
|
+
* When the options argument is itself mandatory (an uncovered guardInput
|
|
157
|
+
* guard, an idempotent API's key), the payload cannot stay optional in front
|
|
158
|
+
* of it, since a tuple's required element may not follow an optional one.
|
|
159
|
+
* Such a call passes its payload explicitly, `undefined` included.
|
|
160
|
+
*/
|
|
161
|
+
export type LambderCallArgs<TContract, TApiName, TProvided extends string, TOptions extends {
|
|
162
|
+
guardInputs?: Record<string, unknown>;
|
|
163
|
+
}> = LambderCallOptionsArg<TContract, TApiName, TProvided, TOptions> extends [options: infer TRequired] ? [payload: LambderContractInputOf<TContract, TApiName>, options: TRequired] : LambderCallOptionsArg<TContract, TApiName, TProvided, TOptions> extends [options?: infer TOptional] ? undefined extends LambderContractInputOf<TContract, TApiName> ? [payload?: LambderContractInputOf<TContract, TApiName>, options?: TOptional] : [payload: LambderContractInputOf<TContract, TApiName>, options?: TOptional] : never;
|
|
164
|
+
/**
|
|
165
|
+
* Provider values underneath, per-call values on top; undefined when neither
|
|
166
|
+
* side supplied any. Synchronous on purpose: a caller awaits its provider
|
|
167
|
+
* only when it has one, so a call without a provider still issues its
|
|
168
|
+
* request in the same tick it was made.
|
|
169
|
+
*/
|
|
170
|
+
export declare const mergeGuardInputs: (provided: Record<string, unknown> | undefined, perCall: Record<string, unknown> | undefined) => Record<string, unknown> | undefined;
|
|
171
|
+
export {};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The per-call options both callers take, the contract-driven typing of a
|
|
3
|
+
* call's arguments, and the runtime merge of guard inputs, shared by the
|
|
4
|
+
* browser caller (LambderCaller) and the server-side invoke caller
|
|
5
|
+
* (LambderInvokeCaller). Both speak the same envelope to the same kind of
|
|
6
|
+
* contract, so what an API demands of its caller (a guardInput-mode guard's
|
|
7
|
+
* value, say) is decided here once and the two callers cannot drift on it.
|
|
8
|
+
* Pure types and one dependency-free function, so the browser entry resolves
|
|
9
|
+
* it.
|
|
10
|
+
*/
|
|
11
|
+
/**
|
|
12
|
+
* Provider values underneath, per-call values on top; undefined when neither
|
|
13
|
+
* side supplied any. Synchronous on purpose: a caller awaits its provider
|
|
14
|
+
* only when it has one, so a call without a provider still issues its
|
|
15
|
+
* request in the same tick it was made.
|
|
16
|
+
*/
|
|
17
|
+
export const mergeGuardInputs = (provided, perCall) => provided !== undefined || perCall !== undefined ? { ...provided, ...perCall } : undefined;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The one place Lambder compresses and decompresses bytes.
|
|
3
3
|
*
|
|
4
|
-
* Five things compress: sessions, LambderDdbCache and
|
|
4
|
+
* Five things compress: sessions, LambderDdbCache and LambderDdbIdempotencyStore
|
|
5
5
|
* (Brotli at rest in DynamoDB), HTTP responses (Brotli or gzip, negotiated)
|
|
6
6
|
* and request payloads (gzip from a browser, whose CompressionStream offers
|
|
7
7
|
* nothing else; Brotli from a Node caller). They all compress text, so TEXT
|
|
@@ -21,11 +21,15 @@
|
|
|
21
21
|
* implementation: a bug fixed here is fixed for records at rest and for
|
|
22
22
|
* untrusted request bodies alike.
|
|
23
23
|
*
|
|
24
|
-
* zlib is loaded lazily through
|
|
24
|
+
* zlib is loaded lazily through LambderNodeModules, so a module importing this
|
|
25
25
|
* one can still sit in a frontend bundle's import graph via the package
|
|
26
|
-
* root.
|
|
27
|
-
*
|
|
28
|
-
*
|
|
26
|
+
* root. Compressing needs zlib and is server-side; restoring runs anywhere,
|
|
27
|
+
* through zlib where it exists and through the web DecompressionStream
|
|
28
|
+
* otherwise, so the API core can restore a gzipped request payload in a
|
|
29
|
+
* browser (the mock runtime) under the same bound. The option that decides
|
|
30
|
+
* WHETHER to compress, and the encoding vocabulary, live in
|
|
31
|
+
* LambderCompressionOption, which stays free of zlib entirely so the browser
|
|
32
|
+
* entry can resolve it.
|
|
29
33
|
*/
|
|
30
34
|
import type { LambderEncoding } from "./LambderCompressionOption.js";
|
|
31
35
|
/** Why a bounded restore failed, for callers that answer rather than throw. */
|
|
@@ -81,7 +85,7 @@ export type LambderRestoreBound = {
|
|
|
81
85
|
* UTF-8 decode; going through a string would replace every byte that is not
|
|
82
86
|
* valid UTF-8 and hand back a body that is silently not what was sent.
|
|
83
87
|
*/
|
|
84
|
-
export declare const restoreBytes: (compressed: Uint8Array, encoding: LambderEncoding, bound: LambderRestoreBound) => Promise<
|
|
88
|
+
export declare const restoreBytes: (compressed: Uint8Array, encoding: LambderEncoding, bound: LambderRestoreBound) => Promise<Uint8Array>;
|
|
85
89
|
/**
|
|
86
90
|
* Restores text: restoreBytes plus the UTF-8 decode. What every text caller
|
|
87
91
|
* uses (sessions, the DynamoDB stores, request payloads). The declared byte
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The one place Lambder compresses and decompresses bytes.
|
|
3
3
|
*
|
|
4
|
-
* Five things compress: sessions, LambderDdbCache and
|
|
4
|
+
* Five things compress: sessions, LambderDdbCache and LambderDdbIdempotencyStore
|
|
5
5
|
* (Brotli at rest in DynamoDB), HTTP responses (Brotli or gzip, negotiated)
|
|
6
6
|
* and request payloads (gzip from a browser, whose CompressionStream offers
|
|
7
7
|
* nothing else; Brotli from a Node caller). They all compress text, so TEXT
|
|
@@ -21,13 +21,18 @@
|
|
|
21
21
|
* implementation: a bug fixed here is fixed for records at rest and for
|
|
22
22
|
* untrusted request bodies alike.
|
|
23
23
|
*
|
|
24
|
-
* zlib is loaded lazily through
|
|
24
|
+
* zlib is loaded lazily through LambderNodeModules, so a module importing this
|
|
25
25
|
* one can still sit in a frontend bundle's import graph via the package
|
|
26
|
-
* root.
|
|
27
|
-
*
|
|
28
|
-
*
|
|
26
|
+
* root. Compressing needs zlib and is server-side; restoring runs anywhere,
|
|
27
|
+
* through zlib where it exists and through the web DecompressionStream
|
|
28
|
+
* otherwise, so the API core can restore a gzipped request payload in a
|
|
29
|
+
* browser (the mock runtime) under the same bound. The option that decides
|
|
30
|
+
* WHETHER to compress, and the encoding vocabulary, live in
|
|
31
|
+
* LambderCompressionOption, which stays free of zlib entirely so the browser
|
|
32
|
+
* entry can resolve it.
|
|
29
33
|
*/
|
|
30
|
-
import { getZlib } from "
|
|
34
|
+
import { getZlib } from "../util/LambderNodeModules.js";
|
|
35
|
+
import { assertPositiveInteger } from "../util/LambderOptionChecks.js";
|
|
31
36
|
/** Why a bounded restore failed, for callers that answer rather than throw. */
|
|
32
37
|
export const LAMBDER_RESTORE_FAILURES = {
|
|
33
38
|
/** The declared byte length is absent or not a positive integer. */
|
|
@@ -103,27 +108,22 @@ export const compressText = async (input, encoding, quality) => {
|
|
|
103
108
|
export const restoreBytes = async (compressed, encoding, bound) => {
|
|
104
109
|
const verified = "declaredBytes" in bound;
|
|
105
110
|
const limit = verified ? bound.declaredBytes : bound.maxBytes;
|
|
106
|
-
if (
|
|
107
|
-
|
|
111
|
+
if (verified) {
|
|
112
|
+
// A declared length comes off the wire, so a bad one is a bad request
|
|
113
|
+
// with a reason a caller can be told, not a configuration error.
|
|
114
|
+
if (!Number.isSafeInteger(limit) || limit <= 0) {
|
|
108
115
|
throw new LambderCompressionError(LAMBDER_RESTORE_FAILURES.missingLength, "compressed value is missing its byte length");
|
|
109
116
|
}
|
|
110
|
-
throw new Error("restoreBytes: maxBytes must be a positive integer");
|
|
111
117
|
}
|
|
112
|
-
|
|
118
|
+
else {
|
|
119
|
+
assertPositiveInteger(limit, "restoreBytes maxBytes");
|
|
120
|
+
}
|
|
121
|
+
const zlib = await getZlib();
|
|
113
122
|
let output;
|
|
114
123
|
try {
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
reject(error);
|
|
119
|
-
else
|
|
120
|
-
resolve(result); };
|
|
121
|
-
// maxOutputLength is the bound: zlib stops rather than allocating past it.
|
|
122
|
-
if (encoding === "br")
|
|
123
|
-
zlib.brotliDecompress(compressed, { maxOutputLength: limit }, done);
|
|
124
|
-
else
|
|
125
|
-
zlib.gunzip(compressed, { maxOutputLength: limit }, done);
|
|
126
|
-
});
|
|
124
|
+
output = zlib
|
|
125
|
+
? await restoreWithZlib(zlib, compressed, encoding, limit)
|
|
126
|
+
: await restoreWithWebStreams(compressed, encoding, limit);
|
|
127
127
|
}
|
|
128
128
|
catch (err) {
|
|
129
129
|
throw new LambderCompressionError(LAMBDER_RESTORE_FAILURES.undecodable, "compressed value could not be decompressed", { cause: err });
|
|
@@ -140,4 +140,48 @@ export const restoreBytes = async (compressed, encoding, bound) => {
|
|
|
140
140
|
* which is the restored buffer's length, so the verification is the same one
|
|
141
141
|
* either way.
|
|
142
142
|
*/
|
|
143
|
-
export const restoreText = async (compressed, encoding, bound) => (await restoreBytes(compressed, encoding, bound))
|
|
143
|
+
export const restoreText = async (compressed, encoding, bound) => new TextDecoder().decode(await restoreBytes(compressed, encoding, bound));
|
|
144
|
+
/** zlib reads any Uint8Array in place; maxOutputLength is the bound, so it stops rather than allocating past it. */
|
|
145
|
+
const restoreWithZlib = (zlib, compressed, encoding, limit) => new Promise((resolve, reject) => {
|
|
146
|
+
const done = (error, result) => { if (error)
|
|
147
|
+
reject(error);
|
|
148
|
+
else
|
|
149
|
+
resolve(result); };
|
|
150
|
+
if (encoding === "br")
|
|
151
|
+
zlib.brotliDecompress(compressed, { maxOutputLength: limit }, done);
|
|
152
|
+
else
|
|
153
|
+
zlib.gunzip(compressed, { maxOutputLength: limit }, done);
|
|
154
|
+
});
|
|
155
|
+
/**
|
|
156
|
+
* The browser's restore: DecompressionStream, read chunk by chunk and cut
|
|
157
|
+
* off past the bound, which is the one thing the stream API does not do on
|
|
158
|
+
* its own. Brotli arrives here only where the runtime offers it; a runtime
|
|
159
|
+
* without it rejects the format, which reads as undecodable.
|
|
160
|
+
*/
|
|
161
|
+
const restoreWithWebStreams = async (compressed, encoding, limit) => {
|
|
162
|
+
if (typeof DecompressionStream === "undefined")
|
|
163
|
+
throw new Error("Lambder compression requires zlib or DecompressionStream.");
|
|
164
|
+
const format = (encoding === "br" ? "br" : "gzip");
|
|
165
|
+
const bytes = new Uint8Array(compressed);
|
|
166
|
+
const reader = new Blob([bytes]).stream().pipeThrough(new DecompressionStream(format)).getReader();
|
|
167
|
+
const chunks = [];
|
|
168
|
+
let total = 0;
|
|
169
|
+
for (;;) {
|
|
170
|
+
const { done, value } = await reader.read();
|
|
171
|
+
if (done)
|
|
172
|
+
break;
|
|
173
|
+
total += value.length;
|
|
174
|
+
if (total > limit) {
|
|
175
|
+
await reader.cancel();
|
|
176
|
+
throw new Error("decompressed output exceeds the bound");
|
|
177
|
+
}
|
|
178
|
+
chunks.push(value);
|
|
179
|
+
}
|
|
180
|
+
const output = new Uint8Array(total);
|
|
181
|
+
let offset = 0;
|
|
182
|
+
for (const chunk of chunks) {
|
|
183
|
+
output.set(chunk, offset);
|
|
184
|
+
offset += chunk.length;
|
|
185
|
+
}
|
|
186
|
+
return output;
|
|
187
|
+
};
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* that resolves it.
|
|
4
4
|
*
|
|
5
5
|
* Five places compress something: sessions, LambderDdbCache and
|
|
6
|
-
*
|
|
6
|
+
* LambderDdbIdempotencyStore (Brotli at rest in DynamoDB), HTTP responses
|
|
7
7
|
* (Brotli/gzip on the wire) and request payloads (gzip on the wire). They
|
|
8
8
|
* differ in what they can be tuned with, so each declares its own settings
|
|
9
9
|
* type, but they share one vocabulary and one resolution: `true` is on with
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* that resolves it.
|
|
4
4
|
*
|
|
5
5
|
* Five places compress something: sessions, LambderDdbCache and
|
|
6
|
-
*
|
|
6
|
+
* LambderDdbIdempotencyStore (Brotli at rest in DynamoDB), HTTP responses
|
|
7
7
|
* (Brotli/gzip on the wire) and request payloads (gzip on the wire). They
|
|
8
8
|
* differ in what they can be tuned with, so each declares its own settings
|
|
9
9
|
* type, but they share one vocabulary and one resolution: `true` is on with
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
* option without pulling Node built-ins into the bundle; the compression
|
|
17
17
|
* primitives themselves live in LambderCompressionCodec, which does load zlib.
|
|
18
18
|
*/
|
|
19
|
+
import { assertNonNegativeInteger } from "../util/LambderOptionChecks.js";
|
|
19
20
|
/** Algorithms Lambder can produce. Brotli at rest and preferred on responses; gzip everywhere a browser has to do the compressing. */
|
|
20
21
|
export const LAMBDER_ENCODINGS = ["br", "gzip"];
|
|
21
22
|
/**
|
|
@@ -37,9 +38,7 @@ export const resolveCompressionOption = (option, defaults) => {
|
|
|
37
38
|
const config = option === true || option === undefined ? {} : option;
|
|
38
39
|
const overrides = Object.fromEntries(Object.entries(config).filter(([, value]) => value !== undefined));
|
|
39
40
|
const settings = { ...defaults, ...overrides };
|
|
40
|
-
|
|
41
|
-
throw new Error("compression.minBytes must be a non-negative integer");
|
|
42
|
-
}
|
|
41
|
+
assertNonNegativeInteger(settings.minBytes, "compression.minBytes");
|
|
43
42
|
if (settings.quality !== undefined && (!Number.isInteger(settings.quality) || settings.quality < 0 || settings.quality > 11)) {
|
|
44
43
|
throw new Error("compression.quality must be an integer from 0 to 11");
|
|
45
44
|
}
|
|
@@ -46,3 +46,13 @@ export declare const describeCrash: (error: unknown, ctx?: {
|
|
|
46
46
|
* causes see the callee's stack as it was.
|
|
47
47
|
*/
|
|
48
48
|
export declare const errorFromCrashDetail: (crash: LambderCrashDetail) => Error;
|
|
49
|
+
/**
|
|
50
|
+
* A thrown value as an Error, so a reporter or a `cause` chain always holds
|
|
51
|
+
* one. An Error passes through; anything else becomes an Error whose message
|
|
52
|
+
* describes the value the way describeCrash does, JSON where String() cannot
|
|
53
|
+
* (a null-prototype object or a throwing toString must not make the
|
|
54
|
+
* coercion itself throw). One implementation, rather than
|
|
55
|
+
* `err instanceof Error ? err : new Error(...)` spelled at every site with a
|
|
56
|
+
* fallback of its own.
|
|
57
|
+
*/
|
|
58
|
+
export declare const coerceToError: (value: unknown, fallbackMessage?: string) => Error;
|