lambder 6.0.2 → 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 +21 -19
- 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
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* An answer's headers: the case-insensitive read, replace and append every
|
|
3
|
+
* layer uses on a plain header map, and the accumulator that records what a
|
|
4
|
+
* call wrote so it can be applied onto whichever answer the call ends up
|
|
5
|
+
* with.
|
|
6
|
+
*
|
|
7
|
+
* One implementation, at the bottom of the stack, because every layer above
|
|
8
|
+
* it needs the same one: LambderResponse's own header methods are these three
|
|
9
|
+
* functions, and a second copy had already drifted from them.
|
|
10
|
+
*/
|
|
11
|
+
/** The header's values under a case-insensitive lookup, or undefined. */
|
|
12
|
+
export declare const getAnswerHeader: (headers: Record<string, string[]>, name: string) => string[] | undefined;
|
|
13
|
+
/** Replaces the header under any casing of its name. */
|
|
14
|
+
export declare const setAnswerHeader: (headers: Record<string, string[]>, name: string, value: string | string[]) => void;
|
|
15
|
+
/** Appends a value to the header, under the casing it already has if any. */
|
|
16
|
+
export declare const addAnswerHeader: (headers: Record<string, string[]>, name: string, value: string) => void;
|
|
17
|
+
/** What LambderAnswerHeaders applies onto: a LambderResponse, or a header map. */
|
|
18
|
+
export type LambderHeaderTarget = {
|
|
19
|
+
getHeader(key: string): string[] | undefined;
|
|
20
|
+
setHeader(key: string, value: string | string[]): unknown;
|
|
21
|
+
addHeader(key: string, value: string): unknown;
|
|
22
|
+
};
|
|
23
|
+
/**
|
|
24
|
+
* Response headers written while a call runs (`res.setHeader`, `res.addHeader`,
|
|
25
|
+
* the session controller's Set-Cookie), applied onto the answer once the
|
|
26
|
+
* call has one. Recorded as operations in call order rather than as a map,
|
|
27
|
+
* so `set` replaces what the answer itself carries (a Content-Type, say) and
|
|
28
|
+
* `add` appends to it, exactly as the two would if called on the answer
|
|
29
|
+
* directly.
|
|
30
|
+
*
|
|
31
|
+
* These headers belong to the CALL, not to the response that first carried
|
|
32
|
+
* them: on the server an afterRender hook may answer with a different
|
|
33
|
+
* response than the handler produced, and a session cookie written during the
|
|
34
|
+
* call has to travel across to it. So applying never forgets the operations,
|
|
35
|
+
* and applying the same ones twice is a no-op: `set` writes the same value
|
|
36
|
+
* again, and `add` skips a value the header already carries. The one thing
|
|
37
|
+
* that costs is two `add` calls of the identical value under one name, which
|
|
38
|
+
* collapse to one; duplicate identical header values carry no meaning in
|
|
39
|
+
* HTTP, so nothing observable is lost.
|
|
40
|
+
*/
|
|
41
|
+
export declare class LambderAnswerHeaders {
|
|
42
|
+
private operations;
|
|
43
|
+
set(key: string, value: string | string[]): void;
|
|
44
|
+
add(key: string, value: string): void;
|
|
45
|
+
/**
|
|
46
|
+
* How many operations have been recorded. Also a mark: reading it before
|
|
47
|
+
* a step and passing it back as `fromIndex` applies only what that step
|
|
48
|
+
* wrote.
|
|
49
|
+
*/
|
|
50
|
+
get size(): number;
|
|
51
|
+
/**
|
|
52
|
+
* Applies the recorded operations, in order, onto anything that reads and
|
|
53
|
+
* writes headers: a LambderResponse, or a header map through applyInto.
|
|
54
|
+
* One definition of what an operation does, so the two targets cannot
|
|
55
|
+
* drift apart.
|
|
56
|
+
*/
|
|
57
|
+
applyTo(target: LambderHeaderTarget, fromIndex?: number): void;
|
|
58
|
+
/** The same, onto an answer's plain header map. */
|
|
59
|
+
applyInto(headers: Record<string, string[]>, fromIndex?: number): void;
|
|
60
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* An answer's headers: the case-insensitive read, replace and append every
|
|
3
|
+
* layer uses on a plain header map, and the accumulator that records what a
|
|
4
|
+
* call wrote so it can be applied onto whichever answer the call ends up
|
|
5
|
+
* with.
|
|
6
|
+
*
|
|
7
|
+
* One implementation, at the bottom of the stack, because every layer above
|
|
8
|
+
* it needs the same one: LambderResponse's own header methods are these three
|
|
9
|
+
* functions, and a second copy had already drifted from them.
|
|
10
|
+
*/
|
|
11
|
+
/** The header's values under a case-insensitive lookup, or undefined. */
|
|
12
|
+
export const getAnswerHeader = (headers, name) => {
|
|
13
|
+
const lower = name.toLowerCase();
|
|
14
|
+
for (const [key, values] of Object.entries(headers)) {
|
|
15
|
+
if (key.toLowerCase() === lower)
|
|
16
|
+
return values;
|
|
17
|
+
}
|
|
18
|
+
return undefined;
|
|
19
|
+
};
|
|
20
|
+
/** Replaces the header under any casing of its name. */
|
|
21
|
+
export const setAnswerHeader = (headers, name, value) => {
|
|
22
|
+
const lower = name.toLowerCase();
|
|
23
|
+
for (const key of Object.keys(headers)) {
|
|
24
|
+
if (key.toLowerCase() === lower)
|
|
25
|
+
delete headers[key];
|
|
26
|
+
}
|
|
27
|
+
headers[name] = Array.isArray(value) ? [...value] : [value];
|
|
28
|
+
};
|
|
29
|
+
/** Appends a value to the header, under the casing it already has if any. */
|
|
30
|
+
export const addAnswerHeader = (headers, name, value) => {
|
|
31
|
+
const lower = name.toLowerCase();
|
|
32
|
+
const existing = Object.keys(headers).find((key) => key.toLowerCase() === lower);
|
|
33
|
+
if (existing)
|
|
34
|
+
headers[existing].push(value);
|
|
35
|
+
else
|
|
36
|
+
headers[name] = [value];
|
|
37
|
+
};
|
|
38
|
+
/**
|
|
39
|
+
* Response headers written while a call runs (`res.setHeader`, `res.addHeader`,
|
|
40
|
+
* the session controller's Set-Cookie), applied onto the answer once the
|
|
41
|
+
* call has one. Recorded as operations in call order rather than as a map,
|
|
42
|
+
* so `set` replaces what the answer itself carries (a Content-Type, say) and
|
|
43
|
+
* `add` appends to it, exactly as the two would if called on the answer
|
|
44
|
+
* directly.
|
|
45
|
+
*
|
|
46
|
+
* These headers belong to the CALL, not to the response that first carried
|
|
47
|
+
* them: on the server an afterRender hook may answer with a different
|
|
48
|
+
* response than the handler produced, and a session cookie written during the
|
|
49
|
+
* call has to travel across to it. So applying never forgets the operations,
|
|
50
|
+
* and applying the same ones twice is a no-op: `set` writes the same value
|
|
51
|
+
* again, and `add` skips a value the header already carries. The one thing
|
|
52
|
+
* that costs is two `add` calls of the identical value under one name, which
|
|
53
|
+
* collapse to one; duplicate identical header values carry no meaning in
|
|
54
|
+
* HTTP, so nothing observable is lost.
|
|
55
|
+
*/
|
|
56
|
+
export class LambderAnswerHeaders {
|
|
57
|
+
operations = [];
|
|
58
|
+
set(key, value) {
|
|
59
|
+
this.operations.push({ op: "set", key, value });
|
|
60
|
+
}
|
|
61
|
+
add(key, value) {
|
|
62
|
+
this.operations.push({ op: "add", key, value });
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* How many operations have been recorded. Also a mark: reading it before
|
|
66
|
+
* a step and passing it back as `fromIndex` applies only what that step
|
|
67
|
+
* wrote.
|
|
68
|
+
*/
|
|
69
|
+
get size() { return this.operations.length; }
|
|
70
|
+
/**
|
|
71
|
+
* Applies the recorded operations, in order, onto anything that reads and
|
|
72
|
+
* writes headers: a LambderResponse, or a header map through applyInto.
|
|
73
|
+
* One definition of what an operation does, so the two targets cannot
|
|
74
|
+
* drift apart.
|
|
75
|
+
*/
|
|
76
|
+
applyTo(target, fromIndex = 0) {
|
|
77
|
+
for (const operation of this.operations.slice(fromIndex)) {
|
|
78
|
+
if (operation.op === "set") {
|
|
79
|
+
target.setHeader(operation.key, operation.value);
|
|
80
|
+
}
|
|
81
|
+
else if (!target.getHeader(operation.key)?.includes(operation.value)) {
|
|
82
|
+
target.addHeader(operation.key, operation.value);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
/** The same, onto an answer's plain header map. */
|
|
87
|
+
applyInto(headers, fromIndex = 0) {
|
|
88
|
+
this.applyTo({
|
|
89
|
+
getHeader: (key) => getAnswerHeader(headers, key),
|
|
90
|
+
setHeader: (key, value) => setAnswerHeader(headers, key, value),
|
|
91
|
+
addHeader: (key, value) => addAnswerHeader(headers, key, value),
|
|
92
|
+
}, fromIndex);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lambder API Contract System
|
|
3
|
+
*
|
|
4
|
+
* Contracts are built via method chaining and inferred using typeof lambder.ApiContract
|
|
5
|
+
*/
|
|
6
|
+
import type { LambderCrashDetail } from "./LambderCrashDetail.js";
|
|
7
|
+
import type { LambderNonEmptyOptionMap } from "../util/LambderTypeUtilities.js";
|
|
8
|
+
import type { LambderAppRefusalMessage } from "./LambderApiRefusal.js";
|
|
9
|
+
import type { LambderApiIdempotencyOption, LambderGuardsOptionValue, LambderRateLimitOptionValue } from "./LambderApiOptionValues.js";
|
|
10
|
+
/** Whether an endpoint runs without a session or requires one (addApi versus addSessionApi). */
|
|
11
|
+
export type LambderApiMode = "public" | "session";
|
|
12
|
+
/**
|
|
13
|
+
* Base shape for API contracts: what LambderCaller, LambderInvokeCaller and
|
|
14
|
+
* LambderMockApp accept as a contract type.
|
|
15
|
+
*/
|
|
16
|
+
export type LambderApiContractShape = Record<string, {
|
|
17
|
+
input: any;
|
|
18
|
+
output: any;
|
|
19
|
+
/** "public" (addApi) or "session" (addSessionApi). */
|
|
20
|
+
mode?: LambderApiMode;
|
|
21
|
+
/** Present when the API declares guardInput-mode guards: guard name -> value the client must send via options.guardInputs. */
|
|
22
|
+
guardInputs?: any;
|
|
23
|
+
/**
|
|
24
|
+
* Present when the API declares guards: the `guards` option exactly as
|
|
25
|
+
* written at registration, so a client-side copy of "what does this API
|
|
26
|
+
* need" can be pinned to the server's own declaration with `satisfies`
|
|
27
|
+
* rather than kept honest by a test that reads the source.
|
|
28
|
+
*/
|
|
29
|
+
guards?: LambderGuardsOptionValue;
|
|
30
|
+
/** Present when the API declares a rate limit: the `rateLimit` option exactly as written. */
|
|
31
|
+
rateLimit?: LambderRateLimitOptionValue;
|
|
32
|
+
/** Present when the API declares idempotency: the `idempotency` option exactly as written. */
|
|
33
|
+
idempotency?: LambderApiIdempotencyOption;
|
|
34
|
+
}>;
|
|
35
|
+
/** Envelope flags/channels the server may set beside (or instead of) the payload. */
|
|
36
|
+
export type LambderApiResponseConfig = {
|
|
37
|
+
versionExpired?: boolean;
|
|
38
|
+
sessionExpired?: boolean;
|
|
39
|
+
notAuthorized?: boolean;
|
|
40
|
+
message?: any;
|
|
41
|
+
/** A refusal message, or a plain string: what LambderApiRefusal and res.api(null, { errorMessage }) put here. */
|
|
42
|
+
errorMessage?: LambderAppRefusalMessage | string;
|
|
43
|
+
logList?: any[];
|
|
44
|
+
/**
|
|
45
|
+
* A crash described in full (name, message, stack, cause chain, where it
|
|
46
|
+
* happened), for a caller that is allowed to see it: a global error
|
|
47
|
+
* handler answering a trusted invoker sets it with describeCrash().
|
|
48
|
+
* LambderInvokeCaller reads it back as the cause of the error it throws;
|
|
49
|
+
* the browser caller ignores it.
|
|
50
|
+
*/
|
|
51
|
+
crash?: LambderCrashDetail;
|
|
52
|
+
};
|
|
53
|
+
/**
|
|
54
|
+
* The config a null answer carries: at least one of the reason fields, so
|
|
55
|
+
* `res.api(null, {})` is a compile error. A bare null with no flag and no
|
|
56
|
+
* message reaches the caller as a success whose payload is null, which is
|
|
57
|
+
* indistinguishable from an endpoint that answered nothing on purpose.
|
|
58
|
+
*/
|
|
59
|
+
export type LambderApiNullAnswerConfig = LambderNonEmptyOptionMap<Pick<LambderApiResponseConfig, "versionExpired" | "sessionExpired" | "notAuthorized" | "errorMessage" | "message">> & LambderApiResponseConfig;
|
|
60
|
+
/** The API wire envelope both sides speak: res.api() emits it, LambderCaller parses it. */
|
|
61
|
+
export type LambderApiEnvelopeBody<T> = LambderApiResponseConfig & {
|
|
62
|
+
apiVersion?: string | null;
|
|
63
|
+
payload?: T | null;
|
|
64
|
+
};
|
|
65
|
+
/**
|
|
66
|
+
* One contract entry as addApi/addSessionApi record it: the payload types,
|
|
67
|
+
* the mode, and every declarative option exactly as written. Options that
|
|
68
|
+
* were not written are absent rather than undefined, so `keyof` an entry
|
|
69
|
+
* lists only what the endpoint declared.
|
|
70
|
+
*/
|
|
71
|
+
export type LambderContractEntry<In, Out, Mode extends LambderApiMode, GuardInputs = never, Guards = never, RateLimit = never, Idempotency = never> = {
|
|
72
|
+
input: In;
|
|
73
|
+
output: Out;
|
|
74
|
+
mode: Mode;
|
|
75
|
+
} & ([GuardInputs] extends [never] ? {} : {
|
|
76
|
+
guardInputs: GuardInputs;
|
|
77
|
+
}) & ([Guards] extends [never] ? {} : {
|
|
78
|
+
guards: Guards;
|
|
79
|
+
}) & ([RateLimit] extends [never] ? {} : {
|
|
80
|
+
rateLimit: RateLimit;
|
|
81
|
+
}) & ([Idempotency] extends [never] ? {} : {
|
|
82
|
+
idempotency: Idempotency;
|
|
83
|
+
});
|
|
84
|
+
/** Helper type for merging a new entry into the contract during chaining. */
|
|
85
|
+
export type LambderMergeContract<Old, Name extends string, Entry> = Old & {
|
|
86
|
+
[K in Name]: Entry;
|
|
87
|
+
};
|
|
88
|
+
/** Guard names referenced by a guards option, whichever of its three forms is used. */
|
|
89
|
+
export type LambderGuardNamesIn<TOpt> = TOpt extends string ? TOpt : TOpt extends readonly (infer N extends string)[] ? N : TOpt extends object ? keyof TOpt & string : never;
|
|
90
|
+
/** The endpoint's mode; a contract written without one admits either. */
|
|
91
|
+
export type LambderContractMode<C, K extends keyof C> = C[K] extends {
|
|
92
|
+
mode: infer M extends LambderApiMode;
|
|
93
|
+
} ? M : LambderApiMode;
|
|
94
|
+
/** The endpoint names of one mode. */
|
|
95
|
+
export type LambderContractKeysWithMode<C, M extends LambderApiMode> = {
|
|
96
|
+
[K in keyof C]: LambderContractMode<C, K> extends M ? K : never;
|
|
97
|
+
}[keyof C] & string;
|
|
98
|
+
/** The endpoint's guards option as written, or never when it declared none. */
|
|
99
|
+
export type LambderContractGuardsOf<C, K extends keyof C> = C[K] extends {
|
|
100
|
+
guards: infer G;
|
|
101
|
+
} ? G : never;
|
|
102
|
+
/** Every guard name any endpoint of the contract declares. */
|
|
103
|
+
export type LambderContractGuardNames<C> = {
|
|
104
|
+
[K in keyof C]: LambderGuardNamesIn<LambderContractGuardsOf<C, K>>;
|
|
105
|
+
}[keyof C] & string;
|
|
106
|
+
/** The endpoint's guardInputs requirement, or never when its guards take no client input. */
|
|
107
|
+
export type LambderContractGuardInputsOf<C, K extends keyof C> = C[K] extends {
|
|
108
|
+
guardInputs: infer G;
|
|
109
|
+
} ? G : never;
|
|
110
|
+
/** The value guard N takes from the client, as the endpoints declaring it inferred it (a union across them when they differ). */
|
|
111
|
+
export type LambderContractGuardInput<C, N extends string> = {
|
|
112
|
+
[K in keyof C]: [LambderContractGuardInputsOf<C, K>] extends [never] ? never : (N extends keyof LambderContractGuardInputsOf<C, K> ? LambderContractGuardInputsOf<C, K>[N] : never);
|
|
113
|
+
}[keyof C];
|
|
114
|
+
/**
|
|
115
|
+
* Guard names any endpoint declares in guardInput mode: the ones whose
|
|
116
|
+
* value the client sends. (An endpoint without guardInputs contributes
|
|
117
|
+
* nothing: `keyof never` would be every key, so it is excluded first.)
|
|
118
|
+
*/
|
|
119
|
+
export type LambderContractGuardInputNames<C> = {
|
|
120
|
+
[K in keyof C]: [LambderContractGuardInputsOf<C, K>] extends [never] ? never : keyof LambderContractGuardInputsOf<C, K> & string;
|
|
121
|
+
}[keyof C];
|
|
122
|
+
/** The endpoint's rateLimit option as written, or never. */
|
|
123
|
+
export type LambderContractRateLimitOf<C, K extends keyof C> = C[K] extends {
|
|
124
|
+
rateLimit: infer R;
|
|
125
|
+
} ? R : never;
|
|
126
|
+
/** The endpoint's idempotency option as written, or never. */
|
|
127
|
+
export type LambderContractIdempotencyOf<C, K extends keyof C> = C[K] extends {
|
|
128
|
+
idempotency: infer I;
|
|
129
|
+
} ? I : never;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The runtime shapes of the three per-API policy options (guards, rate limit,
|
|
3
|
+
* idempotency), declared below both the contract that records them and the
|
|
4
|
+
* engines that enforce them so neither has to import the other.
|
|
5
|
+
*
|
|
6
|
+
* A contract type carries these options exactly as an API wrote them, and the
|
|
7
|
+
* engines in `api/` read the same shapes back. Declaring them here is what
|
|
8
|
+
* keeps `shared/` at the bottom of the stack: without it the contract would
|
|
9
|
+
* name an `api/` type and `shared/` would depend on a layer above it.
|
|
10
|
+
*/
|
|
11
|
+
import type { LambderAppRefusalMessage } from "./LambderApiRefusal.js";
|
|
12
|
+
import type { LambderRateLimitPolicy } from "../contracts/LambderRateLimiter.js";
|
|
13
|
+
/** The guards option's runtime shape: a name, ordered names, or a name-to-param map. */
|
|
14
|
+
export type LambderGuardsOptionValue = string | readonly string[] | Readonly<Record<string, unknown>>;
|
|
15
|
+
/**
|
|
16
|
+
* What an API may override on a policy it references, in the map form of the
|
|
17
|
+
* rateLimit option. Windows merge over the policy's own (a tighter burst keeps
|
|
18
|
+
* the policy's daily cap) and are only overridable on "perApi" budgets: a
|
|
19
|
+
* shared counter has one set of numbers. errorMessage is per-API text, so it
|
|
20
|
+
* is overridable on either budget.
|
|
21
|
+
*/
|
|
22
|
+
export type LambderRateLimitOverride = LambderRateLimitPolicy & {
|
|
23
|
+
errorMessage?: LambderAppRefusalMessage;
|
|
24
|
+
};
|
|
25
|
+
/** The rateLimit option's runtime shape: a name, ordered names, or a name-to-override map (LambderRateLimitOption narrows the names and overrides per policy). */
|
|
26
|
+
export type LambderRateLimitOptionValue = string | readonly string[] | Readonly<Record<string, true | LambderRateLimitOverride | undefined>>;
|
|
27
|
+
/** The per-endpoint idempotency declaration: on, or on with its own replay TTL. */
|
|
28
|
+
export type LambderApiIdempotencyOption = boolean | {
|
|
29
|
+
/** Seconds this API's stored answer replays for; overrides defaultTtlSeconds. */
|
|
30
|
+
ttlSeconds?: number;
|
|
31
|
+
/**
|
|
32
|
+
* Seconds this API's claim stays pending before a retry may take the
|
|
33
|
+
* scope; overrides defaultPendingTtlSeconds. Raise it on an API whose
|
|
34
|
+
* handler can run longer than the default, or a retry that arrives after
|
|
35
|
+
* it expires runs the operation a second time while the original is still
|
|
36
|
+
* working.
|
|
37
|
+
*/
|
|
38
|
+
pendingTtlSeconds?: number;
|
|
39
|
+
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The runtime shapes of the three per-API policy options (guards, rate limit,
|
|
3
|
+
* idempotency), declared below both the contract that records them and the
|
|
4
|
+
* engines that enforce them so neither has to import the other.
|
|
5
|
+
*
|
|
6
|
+
* A contract type carries these options exactly as an API wrote them, and the
|
|
7
|
+
* engines in `api/` read the same shapes back. Declaring them here is what
|
|
8
|
+
* keeps `shared/` at the bottom of the stack: without it the contract would
|
|
9
|
+
* name an `api/` type and `shared/` would depend on a layer above it.
|
|
10
|
+
*/
|
|
11
|
+
export {};
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The one mapping from an HTTP answer to an API outcome.
|
|
3
|
+
*
|
|
4
|
+
* LambderCaller (a browser, over fetch) and LambderInvokeCaller (a server,
|
|
5
|
+
* over a direct Lambda invoke) receive the same envelope and must read it
|
|
6
|
+
* the same way: which status is a crash, which is a rejected input, in what
|
|
7
|
+
* order the envelope flags are honoured, what a non-envelope body means.
|
|
8
|
+
* Both hand their answer to resolveApiOutcome and act on the result; the
|
|
9
|
+
* side effects each has (handlers, cookie clearing, error reporting) stay
|
|
10
|
+
* with the caller that owns them. Pure and dependency-free, so the browser
|
|
11
|
+
* entry resolves it.
|
|
12
|
+
*/
|
|
13
|
+
import type { z } from "zod";
|
|
14
|
+
import type { LambderApiEnvelopeBody } from "./LambderApiContract.js";
|
|
15
|
+
import type { LambderAppRefusalMessage } from "./LambderApiRefusal.js";
|
|
16
|
+
/**
|
|
17
|
+
* The 422 body's `zodError` as it survives JSON: a ZodError's name and
|
|
18
|
+
* message, and its issues spelled out. Not a ZodError instance (it has no
|
|
19
|
+
* methods on this side of the wire), which is why it is not typed as one.
|
|
20
|
+
*/
|
|
21
|
+
export type LambderValidationError = {
|
|
22
|
+
name: string;
|
|
23
|
+
message: string;
|
|
24
|
+
issues: z.core.$ZodIssue[];
|
|
25
|
+
};
|
|
26
|
+
export type LambderApiFailureReason = 'network' | 'timeout' | 'server' | 'validation' | 'versionExpired' | 'sessionExpired' | 'notAuthorized' | 'errorMessage' | 'unknown';
|
|
27
|
+
/** A call that produced an answer the server means as a result. */
|
|
28
|
+
export type LambderApiSuccessOutcome<T> = {
|
|
29
|
+
ok: true;
|
|
30
|
+
payload: T | null | undefined;
|
|
31
|
+
response: LambderApiEnvelopeBody<T>;
|
|
32
|
+
/** The answer's logList, when it carried one. See LambderApiFailureFields.logList: the field is on every arm so a caller surfaces logs once. */
|
|
33
|
+
logList?: unknown[];
|
|
34
|
+
};
|
|
35
|
+
/** What every failure carries, whatever went wrong. */
|
|
36
|
+
type LambderApiFailureFields = {
|
|
37
|
+
ok: false;
|
|
38
|
+
/** HTTP status, when a response was received. */
|
|
39
|
+
status?: number;
|
|
40
|
+
/** Envelope errorMessage, when the server provided one. */
|
|
41
|
+
errorMessage?: LambderAppRefusalMessage | string;
|
|
42
|
+
/** Seconds to wait before retrying, from the response's Retry-After header (rate-limit refusals send it). */
|
|
43
|
+
retryAfterSeconds?: number;
|
|
44
|
+
/**
|
|
45
|
+
* The answer's logList, when it carried one: the envelope's on a success
|
|
46
|
+
* or an envelope refusal, the parsed 500 body's on a server failure, and
|
|
47
|
+
* the validation body's on a 422 (the server writes it there too). It is
|
|
48
|
+
* on every arm so that a caller surfaces logs in ONE place, right after
|
|
49
|
+
* reading the answer, instead of once per outcome it happens to handle:
|
|
50
|
+
* the browser caller surfaced them after its early returns and so never
|
|
51
|
+
* printed the logs of the answer whose logs matter most, a 500.
|
|
52
|
+
*/
|
|
53
|
+
logList?: unknown[];
|
|
54
|
+
};
|
|
55
|
+
/**
|
|
56
|
+
* No result came back to read: the request never completed, it was given up
|
|
57
|
+
* on, the server failed, or something inside the caller threw. Always carries
|
|
58
|
+
* the Error, so a reader that narrowed this far never has to check for it.
|
|
59
|
+
* A 5xx also carries `response` when the server answered with Lambder's own
|
|
60
|
+
* envelope, which is how a crash detail and a logList arrive with it.
|
|
61
|
+
*/
|
|
62
|
+
export type LambderApiCallFailure<T> = LambderApiFailureFields & {
|
|
63
|
+
reason: 'network' | 'timeout' | 'server' | 'unknown';
|
|
64
|
+
error: Error;
|
|
65
|
+
response?: LambderApiEnvelopeBody<T>;
|
|
66
|
+
};
|
|
67
|
+
/** HTTP 422: the server rejected the input against the API's schema. Always carries the issues. */
|
|
68
|
+
export type LambderApiValidationFailure = LambderApiFailureFields & {
|
|
69
|
+
reason: 'validation';
|
|
70
|
+
zodError: LambderValidationError;
|
|
71
|
+
};
|
|
72
|
+
/** The server answered, and the envelope itself says the call is refused. Always carries that envelope. */
|
|
73
|
+
export type LambderApiEnvelopeFailure<T> = LambderApiFailureFields & {
|
|
74
|
+
reason: 'versionExpired' | 'sessionExpired' | 'notAuthorized' | 'errorMessage';
|
|
75
|
+
response: LambderApiEnvelopeBody<T>;
|
|
76
|
+
};
|
|
77
|
+
/**
|
|
78
|
+
* Discriminated result of an API call: `ok: true` carries the payload, every
|
|
79
|
+
* failure carries a machine-readable reason, so "the server returned null"
|
|
80
|
+
* and "the request failed" are never conflated.
|
|
81
|
+
*
|
|
82
|
+
* The failure side is discriminated by `reason` rather than being one arm of
|
|
83
|
+
* optional fields, so narrowing to a reason narrows to what that reason
|
|
84
|
+
* actually carries: `zodError` after `reason === 'validation'`, `response`
|
|
85
|
+
* after an envelope reason, `error` after the rest. Read as one wide arm, the
|
|
86
|
+
* framework's own reader needed three non-null assertions to say what the
|
|
87
|
+
* union already knew.
|
|
88
|
+
*/
|
|
89
|
+
export type LambderApiOutcome<T> = LambderApiSuccessOutcome<T> | LambderApiCallFailure<T> | LambderApiValidationFailure | LambderApiEnvelopeFailure<T>;
|
|
90
|
+
/**
|
|
91
|
+
* What reading one HTTP answer can produce. Narrower than LambderApiOutcome
|
|
92
|
+
* by the three reasons no answer can carry: `network` and `timeout` belong to
|
|
93
|
+
* the caller's own abort, and `unknown` to something throwing around the
|
|
94
|
+
* call. So a caller that has handled `server` and `validation` holds a
|
|
95
|
+
* success or an envelope refusal, both of which carry the envelope.
|
|
96
|
+
*/
|
|
97
|
+
export type LambderApiAnswerOutcome<T> = LambderApiSuccessOutcome<T> | (LambderApiCallFailure<T> & {
|
|
98
|
+
reason: 'server';
|
|
99
|
+
}) | LambderApiValidationFailure | LambderApiEnvelopeFailure<T>;
|
|
100
|
+
/**
|
|
101
|
+
* What the mapping needs from an HTTP answer, whichever transport produced it.
|
|
102
|
+
*
|
|
103
|
+
* Exactly one of `json()` and `text()` is read per answer, never both: a
|
|
104
|
+
* transport backed by a real Response body may only be read once, and the
|
|
105
|
+
* mapping is written to that rule (a 5xx reads text and parses it itself, so
|
|
106
|
+
* that a non-envelope body is still reportable).
|
|
107
|
+
*/
|
|
108
|
+
export type LambderApiHttpAnswer = {
|
|
109
|
+
status: number;
|
|
110
|
+
statusText?: string;
|
|
111
|
+
/** Case-insensitive header lookup; null or undefined when absent. */
|
|
112
|
+
header: (name: string) => string | null | undefined;
|
|
113
|
+
/** The body parsed as JSON; rejects when it is not JSON. */
|
|
114
|
+
json: () => Promise<unknown>;
|
|
115
|
+
/** The body as text. */
|
|
116
|
+
text: () => Promise<string>;
|
|
117
|
+
/** The answer's Set-Cookie header values, for a transport that can see them (a cookie jar consumes them); absent in a browser. */
|
|
118
|
+
setCookies?: string[];
|
|
119
|
+
};
|
|
120
|
+
/**
|
|
121
|
+
* Reads one HTTP answer into an outcome. A 5xx is a server failure that keeps
|
|
122
|
+
* the envelope when the server sent one (Lambder's own 500 body carries
|
|
123
|
+
* errorMessage, and a global error handler may add crash and logList); a
|
|
124
|
+
* 422 is a validation failure only with Lambder's validation body; anything
|
|
125
|
+
* else must be a JSON envelope, whose flags are honoured in a fixed order.
|
|
126
|
+
*/
|
|
127
|
+
export declare const resolveApiOutcome: <T>(answer: LambderApiHttpAnswer) => Promise<LambderApiAnswerOutcome<T>>;
|
|
128
|
+
export {};
|
|
@@ -36,6 +36,7 @@ export const resolveApiOutcome = async (answer) => {
|
|
|
36
36
|
return {
|
|
37
37
|
ok: false, reason: 'server', status,
|
|
38
38
|
errorMessage: envelope?.errorMessage,
|
|
39
|
+
logList: envelope?.logList,
|
|
39
40
|
...(envelope ? { response: envelope } : {}),
|
|
40
41
|
error: new Error("Request failed: " + status + " - " + (answer.statusText ?? "")),
|
|
41
42
|
};
|
|
@@ -43,15 +44,18 @@ export const resolveApiOutcome = async (answer) => {
|
|
|
43
44
|
if (status === 422) {
|
|
44
45
|
// A 422 without Lambder's validation body (e.g. a proxy's error page)
|
|
45
46
|
// is a server failure, not a validation result.
|
|
46
|
-
|
|
47
|
+
// The validation body carries the call's logList as every other
|
|
48
|
+
// answer does, so it is read here rather than left on the wire.
|
|
49
|
+
let body;
|
|
47
50
|
try {
|
|
48
|
-
|
|
51
|
+
body = await answer.json();
|
|
49
52
|
}
|
|
50
53
|
catch { /* not JSON */ }
|
|
54
|
+
const zodError = body?.zodError;
|
|
51
55
|
if (zodError === undefined) {
|
|
52
56
|
return { ok: false, reason: 'server', status, error: new Error("Request failed: 422 without a validation body") };
|
|
53
57
|
}
|
|
54
|
-
return { ok: false, reason: 'validation', status, zodError };
|
|
58
|
+
return { ok: false, reason: 'validation', status, zodError, logList: body?.logList };
|
|
55
59
|
}
|
|
56
60
|
// Retry-After (delta-seconds) rides every refusal that knows its reset
|
|
57
61
|
// time, e.g. a rate limit; absent or unreadable is undefined.
|
|
@@ -68,12 +72,15 @@ export const resolveApiOutcome = async (answer) => {
|
|
|
68
72
|
return { ok: false, reason: 'server', status, error: new Error("Request failed: response is not a valid API envelope (status " + status + ")", { cause: err }) };
|
|
69
73
|
}
|
|
70
74
|
if (data.versionExpired)
|
|
71
|
-
return { ok: false, reason: 'versionExpired', status, errorMessage: data.errorMessage, response: data, ...retryAfter };
|
|
75
|
+
return { ok: false, reason: 'versionExpired', status, errorMessage: data.errorMessage, response: data, logList: data.logList, ...retryAfter };
|
|
72
76
|
if (data.sessionExpired)
|
|
73
|
-
return { ok: false, reason: 'sessionExpired', status, errorMessage: data.errorMessage, response: data, ...retryAfter };
|
|
77
|
+
return { ok: false, reason: 'sessionExpired', status, errorMessage: data.errorMessage, response: data, logList: data.logList, ...retryAfter };
|
|
74
78
|
if (data.notAuthorized)
|
|
75
|
-
return { ok: false, reason: 'notAuthorized', status, errorMessage: data.errorMessage, response: data, ...retryAfter };
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
+
return { ok: false, reason: 'notAuthorized', status, errorMessage: data.errorMessage, response: data, logList: data.logList, ...retryAfter };
|
|
80
|
+
// Presence, not truthiness: the writer keeps an errorMessage an app spelled
|
|
81
|
+
// out as the empty string, so a refusal that says nothing is still a
|
|
82
|
+
// refusal. Tested for truth here, it shipped back as a success.
|
|
83
|
+
if (data.errorMessage !== undefined)
|
|
84
|
+
return { ok: false, reason: 'errorMessage', status, errorMessage: data.errorMessage, response: data, logList: data.logList, ...retryAfter };
|
|
85
|
+
return { ok: true, payload: data.payload, response: data, logList: data.logList };
|
|
79
86
|
};
|