lambder 7.2.5 → 8.0.2
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 +1021 -3
- package/README.md +43 -21
- package/dist/api/LambderApiAnswer.d.ts +18 -22
- package/dist/api/LambderApiAnswer.js +6 -7
- package/dist/api/LambderApiCallContext.d.ts +21 -8
- package/dist/api/LambderApiCallContext.js +22 -4
- package/dist/api/LambderApiDefinition.d.ts +4 -3
- package/dist/api/LambderApiEnvelope.d.ts +14 -9
- package/dist/api/LambderApiEnvelope.js +33 -34
- package/dist/api/LambderApiGuards.d.ts +78 -51
- package/dist/api/LambderApiGuards.js +34 -36
- package/dist/api/LambderApiIdempotency.d.ts +74 -61
- package/dist/api/LambderApiIdempotency.js +226 -151
- package/dist/api/LambderApiOutputValidationError.d.ts +32 -0
- package/dist/api/LambderApiOutputValidationError.js +50 -0
- package/dist/api/LambderApiPipeline.d.ts +77 -39
- package/dist/api/LambderApiPipeline.js +135 -62
- package/dist/api/LambderApiRateLimits.d.ts +208 -54
- package/dist/api/LambderApiRateLimits.js +197 -108
- package/dist/api/LambderApiRequest.d.ts +27 -21
- package/dist/api/LambderApiRequest.js +26 -19
- package/dist/api/LambderApiSignature.d.ts +12 -15
- package/dist/api/LambderApiSignature.js +28 -51
- package/dist/api/LambderApiValidationRefusal.d.ts +9 -9
- package/dist/api/LambderApiValidationRefusal.js +10 -10
- package/dist/build/freshProcessVerifier.d.ts +13 -0
- package/dist/build/freshProcessVerifier.js +19 -0
- package/dist/build/writeApiSignatures.d.ts +109 -0
- package/dist/build/writeApiSignatures.js +222 -0
- package/dist/build.d.ts +9 -0
- package/dist/build.js +8 -0
- package/dist/client/LambderCaller.d.ts +13 -44
- package/dist/client/LambderCaller.js +77 -84
- package/dist/client/LambderReloadLoopBreaker.d.ts +56 -26
- package/dist/client/LambderReloadLoopBreaker.js +90 -46
- package/dist/client/lambderFetchTransport.d.ts +4 -1
- package/dist/client/lambderFetchTransport.js +52 -28
- package/dist/client.d.ts +5 -3
- package/dist/client.js +2 -1
- package/dist/core/Lambder.d.ts +161 -69
- package/dist/core/Lambder.js +370 -226
- package/dist/core/LambderContext.d.ts +82 -15
- package/dist/core/LambderContext.js +107 -20
- package/dist/core/LambderCors.d.ts +21 -3
- package/dist/core/LambderCors.js +35 -16
- package/dist/core/LambderCrashHandling.d.ts +40 -0
- package/dist/core/LambderCrashHandling.js +97 -0
- package/dist/core/LambderCreateOptions.d.ts +151 -75
- package/dist/core/LambderCreateOptions.js +16 -23
- package/dist/core/LambderFiles.d.ts +28 -7
- package/dist/core/LambderFiles.js +73 -33
- package/dist/core/LambderIndexHtml.js +12 -11
- package/dist/core/LambderPolicyBuilders.d.ts +17 -5
- package/dist/core/LambderPolicyBuilders.js +17 -5
- package/dist/core/LambderPublicFiles.d.ts +11 -5
- package/dist/core/LambderPublicFiles.js +32 -4
- package/dist/core/LambderRequestPath.d.ts +43 -0
- package/dist/core/LambderRequestPath.js +63 -0
- package/dist/core/LambderResponse.d.ts +26 -5
- package/dist/core/LambderResponse.js +157 -70
- package/dist/core/LambderResponseBuilder.d.ts +49 -4
- package/dist/core/LambderResponseBuilder.js +64 -3
- package/dist/core/LambderRouting.d.ts +2 -3
- package/dist/core/LambderRouting.js +22 -7
- package/dist/core/LambderTemplatingEngine.js +211 -32
- package/dist/index.d.ts +15 -8
- package/dist/index.js +5 -4
- package/dist/invoke/LambderInvokeCaller.d.ts +37 -42
- package/dist/invoke/LambderInvokeCaller.js +76 -66
- package/dist/invoke/LambderInvokeOutcome.d.ts +27 -26
- package/dist/invoke/LambderInvokeOutcome.js +9 -22
- package/dist/invoke/LambderLambdaEvent.d.ts +44 -10
- package/dist/invoke/LambderLambdaEvent.js +80 -37
- package/dist/invoke/lambderHandlerTransport.d.ts +12 -10
- package/dist/invoke/lambderHandlerTransport.js +16 -19
- package/dist/mock/LambderMockApp.d.ts +67 -83
- package/dist/mock/LambderMockApp.js +167 -153
- package/dist/mock/LambderMockBrowserCookies.d.ts +24 -28
- package/dist/mock/LambderMockBrowserCookies.js +24 -28
- package/dist/mock/LambderMockCallRecorder.d.ts +15 -22
- package/dist/mock/LambderMockCallRecorder.js +19 -28
- package/dist/mock/LambderMockCreateOptions.d.ts +42 -24
- package/dist/mock/LambderMockEntryRegistry.d.ts +11 -12
- package/dist/mock/LambderMockEntryRegistry.js +24 -29
- package/dist/mock/LambderMockFailureInjector.d.ts +3 -6
- package/dist/mock/LambderMockFailureInjector.js +3 -6
- package/dist/mock/LambderMockTypes.d.ts +78 -108
- package/dist/mock/lambderMockInvokeTransport.d.ts +11 -13
- package/dist/mock/lambderMockInvokeTransport.js +11 -10
- package/dist/mock/lambderMockMswHandler.d.ts +33 -29
- package/dist/mock/lambderMockMswHandler.js +50 -39
- package/dist/mock.d.ts +3 -1
- package/dist/mock.js +5 -3
- package/dist/session/LambderSessionController.d.ts +108 -89
- package/dist/session/LambderSessionController.js +187 -168
- package/dist/session/LambderSessionCrypto.d.ts +16 -7
- package/dist/session/LambderSessionCrypto.js +26 -12
- package/dist/session/LambderSessionManager.d.ts +136 -47
- package/dist/session/LambderSessionManager.js +280 -139
- package/dist/shared/LambderHtml.d.ts +42 -3
- package/dist/shared/LambderHtml.js +127 -7
- package/dist/shared/LambderHtmlPositions.d.ts +173 -0
- package/dist/shared/LambderHtmlPositions.js +652 -0
- package/dist/shared/LambderI18n.d.ts +10 -11
- package/dist/shared/LambderI18n.js +33 -21
- package/dist/shared/contracts/LambderCache.d.ts +66 -0
- package/dist/shared/contracts/LambderCache.js +11 -0
- package/dist/shared/contracts/LambderFileSource.d.ts +6 -6
- package/dist/shared/contracts/LambderFileSource.js +5 -8
- package/dist/shared/contracts/LambderIdempotencyStore.d.ts +51 -22
- package/dist/shared/contracts/LambderIdempotencyStore.js +4 -5
- package/dist/shared/contracts/LambderRateLimiter.d.ts +27 -15
- package/dist/shared/contracts/LambderRateLimiter.js +4 -5
- package/dist/shared/contracts/LambderSessionStore.d.ts +65 -26
- package/dist/shared/contracts/LambderSessionStore.js +5 -6
- package/dist/shared/transport/LambderApiTransport.d.ts +27 -27
- package/dist/shared/transport/LambderApiTransport.js +7 -7
- package/dist/shared/transport/LambderCookieJar.d.ts +28 -35
- package/dist/shared/transport/LambderCookieJar.js +54 -66
- package/dist/shared/transport/lambderCookieJarTransport.d.ts +11 -13
- package/dist/shared/transport/lambderCookieJarTransport.js +24 -23
- package/dist/shared/util/LambderCallAbort.d.ts +5 -5
- package/dist/shared/util/LambderCallAbort.js +5 -5
- package/dist/shared/util/LambderClientIp.d.ts +27 -11
- package/dist/shared/util/LambderClientIp.js +96 -13
- package/dist/shared/util/LambderExpiringMap.d.ts +35 -49
- package/dist/shared/util/LambderExpiringMap.js +41 -57
- package/dist/shared/util/LambderNodeModules.js +6 -7
- package/dist/shared/util/LambderOptionChecks.d.ts +4 -4
- package/dist/shared/util/LambderOptionChecks.js +4 -4
- package/dist/shared/util/LambderResponseBrand.d.ts +5 -5
- package/dist/shared/util/LambderResponseBrand.js +5 -5
- package/dist/shared/util/LambderTestingDoors.d.ts +29 -0
- package/dist/shared/util/LambderTestingDoors.js +29 -0
- package/dist/shared/util/LambderTypeUtilities.d.ts +7 -8
- package/dist/shared/util/LambderTypeUtilities.js +3 -3
- package/dist/shared/util/boundKeyField.d.ts +20 -0
- package/dist/shared/util/boundKeyField.js +34 -0
- package/dist/shared/util/canonicalJson.d.ts +11 -0
- package/dist/shared/util/canonicalJson.js +28 -0
- package/dist/shared/util/joinKeyFields.d.ts +20 -0
- package/dist/shared/util/joinKeyFields.js +22 -0
- package/dist/shared/wire/LambderAnswerHeaders.d.ts +12 -16
- package/dist/shared/wire/LambderAnswerHeaders.js +12 -16
- package/dist/shared/wire/LambderApiContract.d.ts +107 -32
- package/dist/shared/wire/LambderApiOutcome.d.ts +43 -31
- package/dist/shared/wire/LambderApiOutcome.js +48 -23
- package/dist/shared/wire/LambderApiRefusal.d.ts +39 -27
- package/dist/shared/wire/LambderApiRefusal.js +36 -7
- package/dist/shared/wire/LambderApiSignature.d.ts +18 -22
- package/dist/shared/wire/LambderApiSignature.js +16 -19
- package/dist/shared/wire/LambderCallOptions.d.ts +38 -47
- package/dist/shared/wire/LambderCallOptions.js +9 -11
- package/dist/shared/wire/LambderCompressionCodec.d.ts +29 -34
- package/dist/shared/wire/LambderCompressionCodec.js +31 -36
- package/dist/shared/wire/LambderCompressionOption.d.ts +9 -9
- package/dist/shared/wire/LambderCompressionOption.js +9 -9
- package/dist/shared/wire/LambderCrashDetail.d.ts +12 -15
- package/dist/shared/wire/LambderCrashDetail.js +12 -15
- package/dist/shared/wire/LambderDefaultApiPath.d.ts +6 -0
- package/dist/shared/wire/LambderDefaultApiPath.js +6 -0
- package/dist/shared/wire/LambderHttpStatus.d.ts +6 -7
- package/dist/shared/wire/LambderIdempotencyKeyScope.d.ts +89 -0
- package/dist/shared/wire/LambderIdempotencyKeyScope.js +146 -0
- package/dist/shared/wire/LambderInvokeApiId.d.ts +27 -0
- package/dist/shared/wire/LambderInvokeApiId.js +27 -0
- package/dist/shared/wire/LambderOutcomeAssertions.d.ts +79 -0
- package/dist/shared/wire/LambderOutcomeAssertions.js +112 -0
- package/dist/shared/wire/LambderRequestPayload.d.ts +18 -20
- package/dist/shared/wire/LambderRequestPayload.js +4 -6
- package/dist/stores/LambderCacheFiller.d.ts +48 -0
- package/dist/stores/LambderCacheFiller.js +119 -0
- package/dist/stores/LambderCacheKeys.d.ts +26 -0
- package/dist/stores/LambderCacheKeys.js +54 -0
- package/dist/stores/LambderCacheValues.d.ts +45 -0
- package/dist/stores/LambderCacheValues.js +74 -0
- package/dist/stores/LambderDdbCache.d.ts +121 -56
- package/dist/stores/LambderDdbCache.js +528 -225
- package/dist/stores/LambderDdbIdempotencyStore.d.ts +33 -22
- package/dist/stores/LambderDdbIdempotencyStore.js +75 -50
- package/dist/stores/LambderDdbRateLimiter.d.ts +76 -20
- package/dist/stores/LambderDdbRateLimiter.js +151 -39
- package/dist/stores/LambderDdbSdk.d.ts +43 -31
- package/dist/stores/LambderDdbSdk.js +79 -33
- package/dist/stores/LambderDdbSessionStore.d.ts +27 -14
- package/dist/stores/LambderDdbSessionStore.js +119 -47
- package/dist/stores/LambderHttpFileSource.d.ts +15 -6
- package/dist/stores/LambderHttpFileSource.js +15 -13
- package/dist/stores/LambderMemoryCache.d.ts +49 -0
- package/dist/stores/LambderMemoryCache.js +113 -0
- package/dist/stores/LambderMemoryIdempotencyStore.d.ts +13 -12
- package/dist/stores/LambderMemoryIdempotencyStore.js +31 -30
- package/dist/stores/LambderMemoryRateLimiter.d.ts +8 -9
- package/dist/stores/LambderMemoryRateLimiter.js +14 -13
- package/dist/stores/LambderMemorySessionStore.d.ts +14 -11
- package/dist/stores/LambderMemorySessionStore.js +38 -19
- package/dist/stores/LambderS3FileSource.d.ts +21 -6
- package/dist/stores/LambderS3FileSource.js +12 -7
- package/dist/testing/LambderTestApp.d.ts +176 -0
- package/dist/testing/LambderTestApp.js +204 -0
- package/dist/testing/LambderTestVisitor.d.ts +153 -0
- package/dist/testing/LambderTestVisitor.js +154 -0
- package/dist/testing.d.ts +27 -0
- package/dist/testing.js +24 -0
- package/package.json +20 -3
- package/dist/api/LambderApiPolicyEngine.d.ts +0 -36
- package/dist/api/LambderApiPolicyEngine.js +0 -77
- package/dist/shared/util/LambderKeyFields.d.ts +0 -32
- package/dist/shared/util/LambderKeyFields.js +0 -34
|
@@ -14,11 +14,10 @@ type LambderGuardMustNotAnswer = {
|
|
|
14
14
|
readonly "lambder: a guard authorizes, it does not answer. Say no with refuse() or by throwing a LambderApiRefusal.": never;
|
|
15
15
|
};
|
|
16
16
|
/**
|
|
17
|
-
* Intersected into the builder's parameter so the mistake is reported
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
* message.
|
|
17
|
+
* Intersected into the builder's parameter so the mistake is reported at the
|
|
18
|
+
* lambderGuard() call, not later where the guard is put into a map. For an
|
|
19
|
+
* answering handler this is a required property no object literal can
|
|
20
|
+
* satisfy, and the property name is the message.
|
|
22
21
|
*/
|
|
23
22
|
type LambderGuardAnswerCheck<TOutput> = [
|
|
24
23
|
TOutput
|
|
@@ -27,34 +26,50 @@ type LambderGuardAnswerCheck<TOutput> = [
|
|
|
27
26
|
}>] extends [never] ? unknown : LambderGuardMustNotAnswer;
|
|
28
27
|
/**
|
|
29
28
|
* A built guard, unless its handler answers instead of authorizing. Applied
|
|
30
|
-
* to the builder's RESULT rather than
|
|
31
|
-
* unannotated arguments keep
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
* Defined in terms of LambderGuardAnswerCheck so the two cannot drift: one
|
|
39
|
-
* rule for what counts as answering, read twice.
|
|
29
|
+
* to the builder's RESULT rather than its parameters, so the handler's
|
|
30
|
+
* unannotated arguments keep their types from the matching overload. A guard
|
|
31
|
+
* that returns a response denies nothing at runtime (the value becomes
|
|
32
|
+
* ctx.guardData[name] and the call carries on), so this makes the ordinary
|
|
33
|
+
* spelling of that mistake a build error; the engine throws on the ones a
|
|
34
|
+
* cast smuggles past. Defined via LambderGuardAnswerCheck so there is one
|
|
35
|
+
* rule for what counts as answering.
|
|
40
36
|
*/
|
|
41
37
|
type LambderGuardOf<TOutput, TGuard> = LambderGuardAnswerCheck<TOutput> extends LambderGuardMustNotAnswer ? LambderGuardMustNotAnswer : TGuard;
|
|
42
38
|
/** One guard handler: the adapter's context, the validated input slice (undefined in the no-input mode), and the per-API parameter. */
|
|
43
39
|
type LambderGuardHandler<TCtx, TPayload, TParam, TOutput> = (ctx: TCtx, payload: TPayload, param: TParam) => TOutput | Promise<TOutput>;
|
|
44
40
|
/**
|
|
45
|
-
*
|
|
46
|
-
*
|
|
41
|
+
* When a guard runs, relative to the API's input validation.
|
|
42
|
+
*
|
|
43
|
+
* - "beforeInputValidation" (default): an unauthorized caller learns nothing
|
|
44
|
+
* about the input, and no async refinement in the schema runs for it.
|
|
45
|
+
* - "afterInputValidation": for a guard that spends something on the
|
|
46
|
+
* request, such as a single-use captcha token, which a request refused for
|
|
47
|
+
* a mistyped field would otherwise waste. The API's input schema then runs
|
|
48
|
+
* for callers this guard would refuse, so keep lookups (an "email is free"
|
|
49
|
+
* refinement) out of it, in the handler.
|
|
50
|
+
*
|
|
51
|
+
* Guards run in their declared order within each, and the limits keyed by
|
|
52
|
+
* caller data are charged after both unless their policy says otherwise
|
|
53
|
+
* (LambderRateLimitChargeAt).
|
|
54
|
+
*/
|
|
55
|
+
export type LambderGuardRunAt = "beforeInputValidation" | "afterInputValidation";
|
|
56
|
+
/** Where in the call a guard runs: see LambderGuardRunAt. */
|
|
57
|
+
export type LambderGuardPlacement = {
|
|
58
|
+
/** Default: "beforeInputValidation". */
|
|
59
|
+
runAt?: LambderGuardRunAt;
|
|
60
|
+
};
|
|
61
|
+
/**
|
|
62
|
+
* A named guard, run before the API's own input validation unless it says
|
|
63
|
+
* otherwise (LambderGuardPlacement). Three input modes:
|
|
47
64
|
*
|
|
48
65
|
* - `apiInput`: the guard checks fields of the API's OWN payload. The slice
|
|
49
66
|
* is validated against the raw payload before `handler` runs and handed to
|
|
50
|
-
* it typed. The API's input schema
|
|
51
|
-
*
|
|
52
|
-
* compile error.
|
|
67
|
+
* it typed. The API's input schema still owns those fields: declaring the
|
|
68
|
+
* guard on an API whose schema lacks them is a compile error.
|
|
53
69
|
* - `guardInput`: the guard has its own value the client sends SEPARATELY,
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
* payload and handler never see the value.
|
|
70
|
+
* via the caller's options.guardInputs[name]. The requirement lands on the
|
|
71
|
+
* API's contract (`guardInputs`), so the typed caller refuses to compile a
|
|
72
|
+
* call that omits it. The API payload and handler never see the value.
|
|
58
73
|
* - neither: the guard reads only the context.
|
|
59
74
|
*
|
|
60
75
|
* Orthogonally, a guard may also:
|
|
@@ -70,22 +85,21 @@ type LambderGuardHandler<TCtx, TPayload, TParam, TOutput> = (ctx: TCtx, payload:
|
|
|
70
85
|
* the API handler's context as `ctx.guardData[guardName]`, fully typed.
|
|
71
86
|
* Guards that return nothing never appear in guardData.
|
|
72
87
|
*
|
|
73
|
-
* A guard says no by throwing
|
|
74
|
-
*
|
|
75
|
-
*
|
|
88
|
+
* A guard says no by throwing refuse() or a LambderApiRefusal, rendered as
|
|
89
|
+
* the structured refusal envelope. A validation failure of its input slice
|
|
90
|
+
* answers like the API's own input validation (the app's
|
|
76
91
|
* setApiInputValidationErrorHandler when set, else the standard 422). Guards
|
|
77
|
-
* build no responses and hold no resolver,
|
|
78
|
-
*
|
|
79
|
-
*
|
|
92
|
+
* build no responses and hold no resolver, so the same engine runs them on
|
|
93
|
+
* the server and in the mock runtime. Build with lambderGuard() so the
|
|
94
|
+
* handler's payload/ctx/param types line up.
|
|
80
95
|
*
|
|
81
96
|
* TCtx and TSessionCtx are the two contexts an adapter runs guards on, and
|
|
82
97
|
* an adapter's guards map pins them (the server's to the render contexts,
|
|
83
|
-
* the mock's to the mock call contexts). Left open, the
|
|
84
|
-
*
|
|
85
|
-
*
|
|
86
|
-
* authorized or refused everything.
|
|
98
|
+
* the mock's to the mock call contexts). Left open, the map would discard
|
|
99
|
+
* the builder's binding: a server guard would compile into a mock guards map
|
|
100
|
+
* and read `ctx.ip` as undefined, authorizing or refusing everything.
|
|
87
101
|
*/
|
|
88
|
-
export type LambderApiGuard<TInput extends z.ZodType = z.ZodType, TParam = any, TOutput = any, TCtx = any, TSessionCtx = TCtx> = {
|
|
102
|
+
export type LambderApiGuard<TInput extends z.ZodType = z.ZodType, TParam = any, TOutput = any, TCtx = any, TSessionCtx = TCtx> = LambderGuardPlacement & ({
|
|
89
103
|
apiInput: TInput;
|
|
90
104
|
guardInput?: undefined;
|
|
91
105
|
session: true;
|
|
@@ -115,7 +129,7 @@ export type LambderApiGuard<TInput extends z.ZodType = z.ZodType, TParam = any,
|
|
|
115
129
|
guardInput?: undefined;
|
|
116
130
|
session?: false;
|
|
117
131
|
handler: LambderGuardHandler<TCtx, undefined, TParam, TOutput>;
|
|
118
|
-
};
|
|
132
|
+
});
|
|
119
133
|
/**
|
|
120
134
|
* The builder's shape, generic over the two context types a guard may
|
|
121
135
|
* receive: the plain one and the session-typed one. Ties the handler's
|
|
@@ -129,7 +143,7 @@ export type LambderGuardBuilder<TCtx, TSessionCtx> = {
|
|
|
129
143
|
apiInput: TInput;
|
|
130
144
|
session: true;
|
|
131
145
|
handler: (ctx: TSessionCtx, payload: z.output<TInput>, param: TParam) => TOutput | Promise<TOutput>;
|
|
132
|
-
} & LambderGuardAnswerCheck<TOutput>): LambderGuardOf<TOutput, {
|
|
146
|
+
} & LambderGuardPlacement & LambderGuardAnswerCheck<TOutput>): LambderGuardOf<TOutput, {
|
|
133
147
|
apiInput: TInput;
|
|
134
148
|
guardInput?: undefined;
|
|
135
149
|
session: true;
|
|
@@ -138,7 +152,7 @@ export type LambderGuardBuilder<TCtx, TSessionCtx> = {
|
|
|
138
152
|
<TInput extends z.ZodType, TParam = undefined, TOutput = void>(guard: {
|
|
139
153
|
apiInput: TInput;
|
|
140
154
|
handler: (ctx: TCtx, payload: z.output<TInput>, param: TParam) => TOutput | Promise<TOutput>;
|
|
141
|
-
} & LambderGuardAnswerCheck<TOutput>): LambderGuardOf<TOutput, {
|
|
155
|
+
} & LambderGuardPlacement & LambderGuardAnswerCheck<TOutput>): LambderGuardOf<TOutput, {
|
|
142
156
|
apiInput: TInput;
|
|
143
157
|
guardInput?: undefined;
|
|
144
158
|
session?: undefined;
|
|
@@ -148,7 +162,7 @@ export type LambderGuardBuilder<TCtx, TSessionCtx> = {
|
|
|
148
162
|
guardInput: TInput;
|
|
149
163
|
session: true;
|
|
150
164
|
handler: (ctx: TSessionCtx, payload: z.output<TInput>, param: TParam) => TOutput | Promise<TOutput>;
|
|
151
|
-
} & LambderGuardAnswerCheck<TOutput>): LambderGuardOf<TOutput, {
|
|
165
|
+
} & LambderGuardPlacement & LambderGuardAnswerCheck<TOutput>): LambderGuardOf<TOutput, {
|
|
152
166
|
guardInput: TInput;
|
|
153
167
|
apiInput?: undefined;
|
|
154
168
|
session: true;
|
|
@@ -157,7 +171,7 @@ export type LambderGuardBuilder<TCtx, TSessionCtx> = {
|
|
|
157
171
|
<TInput extends z.ZodType, TParam = undefined, TOutput = void>(guard: {
|
|
158
172
|
guardInput: TInput;
|
|
159
173
|
handler: (ctx: TCtx, payload: z.output<TInput>, param: TParam) => TOutput | Promise<TOutput>;
|
|
160
|
-
} & LambderGuardAnswerCheck<TOutput>): LambderGuardOf<TOutput, {
|
|
174
|
+
} & LambderGuardPlacement & LambderGuardAnswerCheck<TOutput>): LambderGuardOf<TOutput, {
|
|
161
175
|
guardInput: TInput;
|
|
162
176
|
apiInput?: undefined;
|
|
163
177
|
session?: undefined;
|
|
@@ -166,7 +180,7 @@ export type LambderGuardBuilder<TCtx, TSessionCtx> = {
|
|
|
166
180
|
<TParam = undefined, TOutput = void>(guard: {
|
|
167
181
|
session: true;
|
|
168
182
|
handler: (ctx: TSessionCtx, payload: undefined, param: TParam) => TOutput | Promise<TOutput>;
|
|
169
|
-
} & LambderGuardAnswerCheck<TOutput>): LambderGuardOf<TOutput, {
|
|
183
|
+
} & LambderGuardPlacement & LambderGuardAnswerCheck<TOutput>): LambderGuardOf<TOutput, {
|
|
170
184
|
apiInput?: undefined;
|
|
171
185
|
guardInput?: undefined;
|
|
172
186
|
session: true;
|
|
@@ -174,7 +188,7 @@ export type LambderGuardBuilder<TCtx, TSessionCtx> = {
|
|
|
174
188
|
}>;
|
|
175
189
|
<TParam = undefined, TOutput = void>(guard: {
|
|
176
190
|
handler: (ctx: TCtx, payload: undefined, param: TParam) => TOutput | Promise<TOutput>;
|
|
177
|
-
} & LambderGuardAnswerCheck<TOutput>): LambderGuardOf<TOutput, {
|
|
191
|
+
} & LambderGuardPlacement & LambderGuardAnswerCheck<TOutput>): LambderGuardOf<TOutput, {
|
|
178
192
|
apiInput?: undefined;
|
|
179
193
|
guardInput?: undefined;
|
|
180
194
|
session?: undefined;
|
|
@@ -196,15 +210,21 @@ type LambderGuardParamOf<G> = G extends {
|
|
|
196
210
|
type LambderGuardOutputOf<G> = G extends {
|
|
197
211
|
handler: (...args: any[]) => infer R;
|
|
198
212
|
} ? Awaited<R> : never;
|
|
199
|
-
/**
|
|
213
|
+
/**
|
|
214
|
+
* Per-guard metadata carried on the Lambder instance: input mode, session
|
|
215
|
+
* requirement, param type, output type. The input slices are recorded in
|
|
216
|
+
* their input form (z.input), which is what a client sends and what an API's
|
|
217
|
+
* posted payload is compared with; the guard's handler still receives the
|
|
218
|
+
* parsed form.
|
|
219
|
+
*/
|
|
200
220
|
export type LambderGuardMeta<G> = (G extends {
|
|
201
221
|
apiInput: infer S extends z.ZodType;
|
|
202
222
|
} ? {
|
|
203
|
-
apiInput: z.
|
|
223
|
+
apiInput: z.input<S>;
|
|
204
224
|
} : G extends {
|
|
205
225
|
guardInput: infer S extends z.ZodType;
|
|
206
226
|
} ? {
|
|
207
|
-
guardInput: z.
|
|
227
|
+
guardInput: z.input<S>;
|
|
208
228
|
} : {}) & (G extends {
|
|
209
229
|
session: true;
|
|
210
230
|
} ? {
|
|
@@ -216,9 +236,16 @@ export type LambderGuardMeta<G> = (G extends {
|
|
|
216
236
|
export type LambderGuardMetaMap<TGuards> = {
|
|
217
237
|
[K in keyof TGuards]: LambderGuardMeta<TGuards[K]>;
|
|
218
238
|
};
|
|
239
|
+
/**
|
|
240
|
+
* The guard name when the API's payload carries the guard's apiInput slice.
|
|
241
|
+
* The payload is compared whole, not member by member: a bare `TPayload
|
|
242
|
+
* extends R` would distribute over a union input, so a slice one member
|
|
243
|
+
* carries would be allowed on an API whose other members lack it, and every
|
|
244
|
+
* request of those members would be refused by the slice's parse.
|
|
245
|
+
*/
|
|
219
246
|
type LambderGuardNameIfPayloadOk<TGuards, K extends keyof TGuards, TPayload> = TGuards[K] extends {
|
|
220
247
|
apiInput: infer R;
|
|
221
|
-
} ? (TPayload extends R ? K : never) : K;
|
|
248
|
+
} ? ([TPayload] extends [R] ? K : never) : K;
|
|
222
249
|
/**
|
|
223
250
|
* Guard names an API may declare: apiInput-mode guards only when the API's
|
|
224
251
|
* payload carries their fields, session guards only on session APIs.
|
|
@@ -285,7 +312,7 @@ export declare const toGuardEntries: (value?: LambderGuardsOptionValue) => {
|
|
|
285
312
|
/**
|
|
286
313
|
* Runtime side of the guards subsystem: holds the defined guards, asserts
|
|
287
314
|
* API registrations against them at startup, and executes an API's declared
|
|
288
|
-
* guards during preflight.
|
|
315
|
+
* guards during preflight. Held by LambderApiPipeline. Reads the
|
|
289
316
|
* request and writes the call context, so it runs unchanged under the
|
|
290
317
|
* server and the mock runtime.
|
|
291
318
|
*/
|
|
@@ -298,10 +325,10 @@ export declare class LambderApiGuardsEngine {
|
|
|
298
325
|
/** Startup validation of one API registration's guards option. */
|
|
299
326
|
assertRegistration(apiName: string, mode: LambderApiMode, guardsOption?: LambderGuardsOptionValue): void;
|
|
300
327
|
/**
|
|
301
|
-
* Run the API's guards
|
|
302
|
-
*
|
|
303
|
-
*
|
|
328
|
+
* Run the API's guards that run at `runAt` (see LambderGuardRunAt) in
|
|
329
|
+
* declared order. Refusals throw; outputs land on ctx.guardData; the
|
|
330
|
+
* trace names every guard reached.
|
|
304
331
|
*/
|
|
305
|
-
run(request: LambderApiRequest, ctx: LambderApiCallContext, guardsOption: LambderGuardsOptionValue | undefined, trace: LambderApiCallTrace): Promise<void>;
|
|
332
|
+
run(request: LambderApiRequest, ctx: LambderApiCallContext, guardsOption: LambderGuardsOptionValue | undefined, trace: LambderApiCallTrace, runAt: LambderGuardRunAt): Promise<void>;
|
|
306
333
|
}
|
|
307
334
|
export {};
|
|
@@ -21,42 +21,39 @@ export const toGuardEntries = (value) => {
|
|
|
21
21
|
};
|
|
22
22
|
/**
|
|
23
23
|
* One guard's input out of the map the client posted. The map is client
|
|
24
|
-
* data, so
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
* unrestricted,
|
|
28
|
-
* name's.
|
|
24
|
+
* data, so a guard named for something Object.prototype carries
|
|
25
|
+
* ("toString", "constructor") must read as absent when the client sent
|
|
26
|
+
* nothing, not as the inherited function. Guard names are deliberately
|
|
27
|
+
* unrestricted, so the read has to handle this.
|
|
29
28
|
*/
|
|
30
29
|
const readGuardInput = (guardInputs, name) => guardInputs !== undefined && Object.prototype.hasOwnProperty.call(guardInputs, name) ? guardInputs[name] : undefined;
|
|
31
30
|
/**
|
|
32
31
|
* Runtime side of the guards subsystem: holds the defined guards, asserts
|
|
33
32
|
* API registrations against them at startup, and executes an API's declared
|
|
34
|
-
* guards during preflight.
|
|
33
|
+
* guards during preflight. Held by LambderApiPipeline. Reads the
|
|
35
34
|
* request and writes the call context, so it runs unchanged under the
|
|
36
35
|
* server and the mock runtime.
|
|
37
36
|
*/
|
|
38
37
|
export class LambderApiGuardsEngine {
|
|
39
38
|
// A Map, not an object: a plain object answers for "toString" and
|
|
40
39
|
// "constructor" through its prototype, so an API declaring one of those as
|
|
41
|
-
// a guard name would pass the registration check
|
|
42
|
-
//
|
|
40
|
+
// a guard name would pass the registration check meant to catch exactly
|
|
41
|
+
// that typo and then fail on every request. It would also refuse to
|
|
43
42
|
// register a guard legitimately named one of them.
|
|
44
43
|
guards = new Map();
|
|
45
44
|
/** True once a guards map was configured. */
|
|
46
45
|
get isConfigured() { return this.guards.size > 0; }
|
|
47
46
|
/** Take the guards map given at creation; named like the other two engines' configure(). */
|
|
48
47
|
configure(guards) {
|
|
49
|
-
// One configuration per instance,
|
|
50
|
-
//
|
|
51
|
-
//
|
|
52
|
-
// depend on the order the calls happened to be made in.
|
|
48
|
+
// One configuration per instance, as in the rate-limit and idempotency
|
|
49
|
+
// engines: a second map would silently merge into the first, and which
|
|
50
|
+
// of two same-named guards ran would depend on call order.
|
|
53
51
|
if (this.guards.size > 0)
|
|
54
52
|
throw new Error("Lambder: guards were already configured.");
|
|
55
|
-
// Declaring the option
|
|
56
|
-
//
|
|
57
|
-
//
|
|
58
|
-
//
|
|
59
|
-
// line.
|
|
53
|
+
// Declaring the option always declares a guard, the rule an API's own
|
|
54
|
+
// `guards: {}` is held to. Otherwise the engine stays unconfigured and
|
|
55
|
+
// every API declaring a guard is told no guards option was given,
|
|
56
|
+
// which sends the reader to the wrong line.
|
|
60
57
|
if (Object.keys(guards).length === 0) {
|
|
61
58
|
throw new Error("Lambder: the guards option was declared with no guards in it, which configures nothing. Name the guards APIs will declare, or leave the option off.");
|
|
62
59
|
}
|
|
@@ -67,18 +64,21 @@ export class LambderApiGuardsEngine {
|
|
|
67
64
|
throw new Error(`Lambder: guard "${name}" has no handler function.`);
|
|
68
65
|
if (guardDef.apiInput && guardDef.guardInput)
|
|
69
66
|
throw new Error(`Lambder: guard "${name}" declares both apiInput and guardInput; pick one.`);
|
|
67
|
+
const runAt = guardDef.runAt;
|
|
68
|
+
if (runAt !== undefined && runAt !== "beforeInputValidation" && runAt !== "afterInputValidation") {
|
|
69
|
+
throw new Error(`Lambder: guard "${name}" has runAt "${String(runAt)}"; use "beforeInputValidation" (default) or "afterInputValidation".`);
|
|
70
|
+
}
|
|
70
71
|
this.guards.set(name, guardDef);
|
|
71
72
|
}
|
|
72
73
|
}
|
|
73
74
|
/** Startup validation of one API registration's guards option. */
|
|
74
75
|
assertRegistration(apiName, mode, guardsOption) {
|
|
75
76
|
const entries = toGuardEntries(guardsOption);
|
|
76
|
-
// The runtime half of
|
|
77
|
-
// `guards:
|
|
78
|
-
// field check while running nothing,
|
|
79
|
-
//
|
|
80
|
-
//
|
|
81
|
-
// produce an empty object lands here instead.
|
|
77
|
+
// The runtime half of LambderNonEmptyOptionMap on the guards option.
|
|
78
|
+
// `guards: {}` and `guards: []` would satisfy the require*ApiGuards
|
|
79
|
+
// field check while running nothing, turning a mandatory authorization
|
|
80
|
+
// declaration into an optional one. The type rejects both; a plain-JS
|
|
81
|
+
// caller, a cast, or a spread that produced an empty object lands here.
|
|
82
82
|
if (guardsOption !== undefined && entries.length === 0) {
|
|
83
83
|
throw new Error(`Lambder: API "${apiName}" declares an empty guards option, which authorizes nothing. ` +
|
|
84
84
|
`Name the guard that authorizes it, or omit the option entirely.`);
|
|
@@ -94,29 +94,27 @@ export class LambderApiGuardsEngine {
|
|
|
94
94
|
}
|
|
95
95
|
}
|
|
96
96
|
/**
|
|
97
|
-
* Run the API's guards
|
|
98
|
-
*
|
|
99
|
-
*
|
|
97
|
+
* Run the API's guards that run at `runAt` (see LambderGuardRunAt) in
|
|
98
|
+
* declared order. Refusals throw; outputs land on ctx.guardData; the
|
|
99
|
+
* trace names every guard reached.
|
|
100
100
|
*/
|
|
101
|
-
async run(request, ctx, guardsOption, trace) {
|
|
101
|
+
async run(request, ctx, guardsOption, trace, runAt) {
|
|
102
102
|
for (const { name, param } of toGuardEntries(guardsOption)) {
|
|
103
103
|
const guardDef = this.guards.get(name);
|
|
104
104
|
if (!guardDef)
|
|
105
105
|
throw new Error(`Lambder: guard "${name}" is not configured. Declare it in the guards option at creation.`);
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
//
|
|
109
|
-
//
|
|
110
|
-
//
|
|
111
|
-
// that refuses by rejecting its own input, which answers 422 and
|
|
112
|
-
// is exactly the refusal a reader is trying to place.
|
|
106
|
+
if ((guardDef.runAt ?? "beforeInputValidation") !== runAt)
|
|
107
|
+
continue;
|
|
108
|
+
// Recorded before anything this guard does can refuse, including
|
|
109
|
+
// the slice parse below (a guard that rejects its own input answers
|
|
110
|
+
// 422), so the guard that said no is the last name on the list.
|
|
113
111
|
trace.guardsRun.push(name);
|
|
114
112
|
let payload;
|
|
115
113
|
if (guardDef.apiInput) {
|
|
116
|
-
payload = parsePreflightSlice(guardDef.apiInput, request.payload);
|
|
114
|
+
payload = await parsePreflightSlice(guardDef.apiInput, request.payload);
|
|
117
115
|
}
|
|
118
116
|
else if (guardDef.guardInput) {
|
|
119
|
-
payload = parsePreflightSlice(guardDef.guardInput, readGuardInput(request.guardInputs, name));
|
|
117
|
+
payload = await parsePreflightSlice(guardDef.guardInput, readGuardInput(request.guardInputs, name));
|
|
120
118
|
}
|
|
121
119
|
// A guard's return value becomes the handler's typed
|
|
122
120
|
// ctx.guardData[name]; check-only guards return undefined.
|
|
@@ -3,6 +3,7 @@ import type { LambderApiCallContext, LambderApiCallTrace } from "./LambderApiCal
|
|
|
3
3
|
import type { LambderApiAnswer } from "./LambderApiAnswer.js";
|
|
4
4
|
import type { LambderIdempotencyStore } from "../shared/contracts/LambderIdempotencyStore.js";
|
|
5
5
|
import type { LambderApiIdempotencyOption } from "../shared/wire/LambderApiOptionValues.js";
|
|
6
|
+
import { LAMBDER_BACKEND_SWAP } from "../shared/util/LambderTestingDoors.js";
|
|
6
7
|
export type LambderApiIdempotencyConfig = {
|
|
7
8
|
/** Your idempotency store instance; may share the rate limiter's table (distinct key prefix). */
|
|
8
9
|
store: LambderIdempotencyStore;
|
|
@@ -19,53 +20,56 @@ export type LambderApiIdempotencyConfig = {
|
|
|
19
20
|
/** Skip idempotency (execute normally) when the store errors, instead of failing the request. Default: true. */
|
|
20
21
|
failOpen?: boolean;
|
|
21
22
|
/**
|
|
22
|
-
* Who a request is acting as, for scoping a PUBLIC API's stored answer
|
|
23
|
-
*
|
|
24
|
-
* that do not have a session to scope by.
|
|
23
|
+
* Who a request is acting as, for scoping a PUBLIC API's stored answer
|
|
24
|
+
* (session APIs already scope per user, by the session's sessionKey).
|
|
25
25
|
*
|
|
26
26
|
* Without it, a public API's scope is the posted key alone, which makes
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
* in the scope, so a key only replays to whoever it was issued to.
|
|
27
|
+
* the key a bearer token for its stored answer. The replay is served
|
|
28
|
+
* BEFORE guards run, so an API authorized by a guard would hand its
|
|
29
|
+
* answer to anyone holding the key. An identity here puts the caller in
|
|
30
|
+
* the scope, so a key replays only to whoever it was issued to.
|
|
32
31
|
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
* Read the credential a guard would check, not a value that changes
|
|
41
|
-
* between attempts: a single-use token (a captcha) would give the
|
|
42
|
-
* legitimate retry a different scope and defeat the replay it needs.
|
|
43
|
-
* Return null for requests with no identity to speak of.
|
|
32
|
+
* The context arrives without a session (public APIs read none); the
|
|
33
|
+
* function sees what the request carries: `request.guardInputs`,
|
|
34
|
+
* `request.payload`, `request.headers` and `request.ip`. Read the
|
|
35
|
+
* credential a guard would check, not a value that changes between
|
|
36
|
+
* attempts: a single-use token (a captcha) would give the legitimate
|
|
37
|
+
* retry a different scope and defeat its replay. Return null for
|
|
38
|
+
* requests with no identity.
|
|
44
39
|
*/
|
|
45
40
|
callerIdentity?: (ctx: Omit<LambderApiCallContext, "session">, request: LambderApiRequest) => string | null | Promise<string | null>;
|
|
46
41
|
};
|
|
42
|
+
/**
|
|
43
|
+
* A call under an idempotency key: the scope its record lives under, and the
|
|
44
|
+
* fingerprint of the request as it was posted. Worked out once per call, by
|
|
45
|
+
* resolveKeyedCall, and handed to the replay lookup and the claim.
|
|
46
|
+
*/
|
|
47
|
+
export type LambderIdempotentCall = {
|
|
48
|
+
scopeKey: string;
|
|
49
|
+
fingerprint: string;
|
|
50
|
+
};
|
|
47
51
|
/**
|
|
48
52
|
* Runtime side of the idempotency subsystem: claims a per-operation scope
|
|
49
53
|
* around handler execution, replays stored answers, and settles claims.
|
|
50
|
-
*
|
|
54
|
+
* Held by LambderApiPipeline. Works on plain answers, so it runs
|
|
51
55
|
* unchanged under the server and the mock runtime.
|
|
52
56
|
*/
|
|
53
57
|
export declare class LambderApiIdempotencyEngine {
|
|
58
|
+
/** Stamped on the crash answer the engine records for an answer that broke its output schema. */
|
|
59
|
+
private readonly apiVersion;
|
|
54
60
|
private store;
|
|
55
61
|
private defaultTtlSeconds;
|
|
56
62
|
private defaultPendingTtlSeconds;
|
|
57
63
|
private failOpen;
|
|
58
64
|
private callerIdentity;
|
|
65
|
+
constructor(apiVersion: string | null);
|
|
66
|
+
configure(config: LambderApiIdempotencyConfig): void;
|
|
59
67
|
/**
|
|
60
|
-
*
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
* code that may verify a token or read a store: running it twice per
|
|
64
|
-
* request is a cost the app never asked for, and one it cannot see.
|
|
65
|
-
* Entries go when the call's context does.
|
|
68
|
+
* Puts the engine over another store, for `lambder/testing`; the replay
|
|
69
|
+
* TTLs, failOpen and callerIdentity stay as configured. False when
|
|
70
|
+
* idempotency was never configured.
|
|
66
71
|
*/
|
|
67
|
-
|
|
68
|
-
configure(config: LambderApiIdempotencyConfig): void;
|
|
72
|
+
[LAMBDER_BACKEND_SWAP](store: LambderIdempotencyStore): boolean;
|
|
69
73
|
/** Startup validation of one API registration's idempotency option. */
|
|
70
74
|
assertRegistration(apiName: string, config: LambderApiIdempotencyOption): void;
|
|
71
75
|
/** True once the idempotency option was configured; registration asserts check it. */
|
|
@@ -77,46 +81,55 @@ export declare class LambderApiIdempotencyEngine {
|
|
|
77
81
|
*/
|
|
78
82
|
private readKey;
|
|
79
83
|
/**
|
|
80
|
-
* The record's scope. Session APIs scope per
|
|
81
|
-
*
|
|
82
|
-
*
|
|
83
|
-
*
|
|
84
|
-
*
|
|
85
|
-
* for (a timeout
|
|
86
|
-
* different IP.
|
|
87
|
-
* give callerIdentity, since the replay is
|
|
84
|
+
* The record's scope. Session APIs scope per user (the sessionKey, which
|
|
85
|
+
* every session of one user shares), so even a leaked key cannot cross
|
|
86
|
+
* users. Public APIs scope by the key alone (it must be
|
|
87
|
+
* long and is documented to be random) unless the app supplies
|
|
88
|
+
* callerIdentity. The client IP is deliberately NOT part of the scope:
|
|
89
|
+
* the retry idempotency exists for (a timeout, then a network change)
|
|
90
|
+
* often arrives from a different IP. Apps whose public APIs are
|
|
91
|
+
* authorized by a guard should give callerIdentity, since the replay is
|
|
92
|
+
* served before guards run.
|
|
88
93
|
*
|
|
89
|
-
*
|
|
90
|
-
*
|
|
94
|
+
* joinKeyFields escapes the fields, so no two distinct scopes collide.
|
|
95
|
+
* The identity field is the caller's (a device token in the docs' own
|
|
96
|
+
* example), so it is bounded first (boundKeyField): an over-long one
|
|
97
|
+
* would push the scope past a store's key limit, and the store's refusal
|
|
98
|
+
* is a throw that failOpen turns into no idempotency for that caller.
|
|
91
99
|
*/
|
|
92
100
|
private scopeOf;
|
|
93
|
-
private computeScope;
|
|
94
101
|
/**
|
|
95
|
-
*
|
|
96
|
-
*
|
|
97
|
-
*
|
|
98
|
-
*
|
|
99
|
-
*
|
|
100
|
-
*
|
|
101
|
-
*
|
|
102
|
-
|
|
102
|
+
* The call's key worked out, once per call: its scope and its request's
|
|
103
|
+
* fingerprint, for findReplay and withIdempotency. Null for a call that
|
|
104
|
+
* sends no key, or when idempotency is not configured; a malformed key
|
|
105
|
+
* refuses with a 400. Taken at the replay lookup, before input validation
|
|
106
|
+
* replaces the payload with its parsed form, so the fingerprint is of the
|
|
107
|
+
* request as it was posted, and callerIdentity (app code that may verify
|
|
108
|
+
* a token or read a store) runs once.
|
|
109
|
+
*/
|
|
110
|
+
resolveKeyedCall(apiName: string, request: LambderApiRequest, ctx: LambderApiCallContext): Promise<LambderIdempotentCall | null>;
|
|
111
|
+
/**
|
|
112
|
+
* Replay fast path, run before guards and all rate limits except
|
|
113
|
+
* `per: "ip"`: a completed record returns its stored answer, so a
|
|
114
|
+
* legitimate retry neither burns quota nor re-runs guards (the original
|
|
115
|
+
* passed them, and no handler executes). The IP limits run first because
|
|
116
|
+
* the store read a replay costs is one of the things they bound. Misses
|
|
117
|
+
* fall through to the normal pipeline; store errors follow failOpen.
|
|
103
118
|
*/
|
|
104
|
-
findReplay(apiName: string,
|
|
119
|
+
findReplay(apiName: string, call: LambderIdempotentCall, trace: LambderApiCallTrace): Promise<LambderApiAnswer | null>;
|
|
105
120
|
/**
|
|
106
|
-
* Idempotency wrapper around validation-passed handler execution
|
|
107
|
-
* a
|
|
108
|
-
* (identity + api + key) is claimed atomically: duplicates of an
|
|
109
|
-
*
|
|
110
|
-
*
|
|
111
|
-
* so a retry actually retries.
|
|
121
|
+
* Idempotency wrapper around validation-passed handler execution, for a
|
|
122
|
+
* call resolveKeyedCall found a key on (a keyless call runs its handler
|
|
123
|
+
* without one): the scope (identity + api + key) is claimed atomically: duplicates of an in-flight original
|
|
124
|
+
* refuse with 409, replays of a completed one return the stored answer
|
|
125
|
+
* verbatim, and a crashed original releases its claim so a retry retries.
|
|
112
126
|
*
|
|
113
|
-
* `exec`
|
|
114
|
-
*
|
|
115
|
-
* so a Set-Cookie the handler set is visible to the caching rule below.
|
|
127
|
+
* `exec` returns the handler's own answer with the headers the handler
|
|
128
|
+
* wrote, so a Set-Cookie it set is visible to the caching rule below.
|
|
116
129
|
* Headers written EARLIER in the call (a session read evicting a stale
|
|
117
|
-
* cookie) are deliberately
|
|
118
|
-
*
|
|
119
|
-
*
|
|
130
|
+
* cookie) are deliberately left off: they reach the client either way,
|
|
131
|
+
* and charging them to this answer would make it uncacheable, silently
|
|
132
|
+
* ending the operation's idempotency.
|
|
120
133
|
*/
|
|
121
|
-
withIdempotency(apiName: string,
|
|
134
|
+
withIdempotency(apiName: string, { scopeKey, fingerprint }: LambderIdempotentCall, config: LambderApiIdempotencyOption, trace: LambderApiCallTrace, exec: () => Promise<LambderApiAnswer>): Promise<LambderApiAnswer>;
|
|
122
135
|
}
|