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
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
import type { LambderNonEmptyOptionMap } from "../shared/util/LambderTypeUtilities.js";
|
|
2
|
+
import type { z } from "zod";
|
|
3
|
+
import type { LambderApiRequest } from "./LambderApiRequest.js";
|
|
4
|
+
import type { LambderApiCallContext, LambderApiCallTrace } from "./LambderApiCallContext.js";
|
|
5
|
+
import type { LambderApiMode, LambderGuardNamesIn } from "../shared/wire/LambderApiContract.js";
|
|
6
|
+
import type { LambderGuardsOptionValue } from "../shared/wire/LambderApiOptionValues.js";
|
|
7
|
+
import { LAMBDER_RESPONSE_BRAND } from "../shared/util/LambderResponseBrand.js";
|
|
8
|
+
/**
|
|
9
|
+
* What lambderGuard() returns for a handler that answers instead of
|
|
10
|
+
* authorizing. Nothing accepts it, so the guards map is where the mistake
|
|
11
|
+
* surfaces, named.
|
|
12
|
+
*/
|
|
13
|
+
type LambderGuardMustNotAnswer = {
|
|
14
|
+
readonly "lambder: a guard authorizes, it does not answer. Say no with refuse() or by throwing a LambderApiRefusal.": never;
|
|
15
|
+
};
|
|
16
|
+
/**
|
|
17
|
+
* Intersected into the builder's parameter so the mistake is reported where
|
|
18
|
+
* it is written, at the lambderGuard() call, rather than further away where
|
|
19
|
+
* the guard is put into a map. An answering handler makes this a required
|
|
20
|
+
* property no object literal can satisfy, and the property name is the
|
|
21
|
+
* message.
|
|
22
|
+
*/
|
|
23
|
+
type LambderGuardAnswerCheck<TOutput> = [
|
|
24
|
+
TOutput
|
|
25
|
+
] extends [never] ? unknown : 0 extends 1 & TOutput ? unknown : [Extract<TOutput, {
|
|
26
|
+
readonly [LAMBDER_RESPONSE_BRAND]: unknown;
|
|
27
|
+
}>] extends [never] ? unknown : LambderGuardMustNotAnswer;
|
|
28
|
+
/**
|
|
29
|
+
* A built guard, unless its handler answers instead of authorizing. Applied
|
|
30
|
+
* to the builder's RESULT rather than to its parameters, so the handler's
|
|
31
|
+
* unannotated arguments keep taking their types from the overload that
|
|
32
|
+
* matched and only the returned shape changes. A guard that hands back a
|
|
33
|
+
* response denies nothing at runtime (the value would become
|
|
34
|
+
* ctx.guardData[name] and the call would carry on), so it must not reach a
|
|
35
|
+
* guards map: this turns the ordinary spelling of that mistake into a build
|
|
36
|
+
* error, and the engine throws on the ones a cast smuggles past.
|
|
37
|
+
*
|
|
38
|
+
* Defined in terms of LambderGuardAnswerCheck so the two cannot drift: one
|
|
39
|
+
* rule for what counts as answering, read twice.
|
|
40
|
+
*/
|
|
41
|
+
type LambderGuardOf<TOutput, TGuard> = LambderGuardAnswerCheck<TOutput> extends LambderGuardMustNotAnswer ? LambderGuardMustNotAnswer : TGuard;
|
|
42
|
+
/** One guard handler: the adapter's context, the validated input slice (undefined in the no-input mode), and the per-API parameter. */
|
|
43
|
+
type LambderGuardHandler<TCtx, TPayload, TParam, TOutput> = (ctx: TCtx, payload: TPayload, param: TParam) => TOutput | Promise<TOutput>;
|
|
44
|
+
/**
|
|
45
|
+
* A named guard, run before the API's own input validation. Three input
|
|
46
|
+
* modes:
|
|
47
|
+
*
|
|
48
|
+
* - `apiInput`: the guard checks fields of the API's OWN payload. The slice
|
|
49
|
+
* is validated against the raw payload before `handler` runs and handed to
|
|
50
|
+
* it typed. The API's input schema stays the owner of those fields:
|
|
51
|
+
* declaring the guard on an API whose schema does not carry them is a
|
|
52
|
+
* compile error.
|
|
53
|
+
* - `guardInput`: the guard has its own value the client sends SEPARATELY,
|
|
54
|
+
* outside the API payload, via the caller's options.guardInputs[name].
|
|
55
|
+
* The requirement lands on the API's contract (`guardInputs`), so the
|
|
56
|
+
* typed caller refuses to compile a call that does not send it. The API
|
|
57
|
+
* payload and handler never see the value.
|
|
58
|
+
* - neither: the guard reads only the context.
|
|
59
|
+
*
|
|
60
|
+
* Orthogonally, a guard may also:
|
|
61
|
+
*
|
|
62
|
+
* - declare `session: true`: the guard needs ctx.session, so it is only
|
|
63
|
+
* declarable on addSessionApi (compile error and startup assert on public
|
|
64
|
+
* APIs) and its handler receives the session-typed context.
|
|
65
|
+
* - take a PARAMETER: annotate a 3rd handler argument
|
|
66
|
+
* (`(ctx, payload, param: YourType) => ...`) and APIs pass the value in
|
|
67
|
+
* their declaration: `guards: { yourGuard: paramValue }`. The value is
|
|
68
|
+
* trusted registration-time code (never client data), typed per guard.
|
|
69
|
+
* - RETURN a value: whatever the handler returns (awaited) is attached to
|
|
70
|
+
* the API handler's context as `ctx.guardData[guardName]`, fully typed.
|
|
71
|
+
* Guards that return nothing never appear in guardData.
|
|
72
|
+
*
|
|
73
|
+
* A guard says no by throwing: refuse() or a LambderApiRefusal, which the
|
|
74
|
+
* pipeline renders as the structured refusal envelope. A validation failure
|
|
75
|
+
* of its input slice answers like the API's own input validation (the app's
|
|
76
|
+
* setApiInputValidationErrorHandler when set, else the standard 422). Guards
|
|
77
|
+
* build no responses and hold no resolver, which is what lets the same
|
|
78
|
+
* engine run them on the server and in the mock runtime. Build with
|
|
79
|
+
* lambderGuard() so the handler's payload/ctx/param types line up.
|
|
80
|
+
*
|
|
81
|
+
* TCtx and TSessionCtx are the two contexts an adapter runs guards on, and
|
|
82
|
+
* 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 binding the builder
|
|
84
|
+
* establishes was thrown away at the map: a guard written for the server
|
|
85
|
+
* compiled into a mock guards map and then read `ctx.ip` as undefined, so it
|
|
86
|
+
* authorized or refused everything.
|
|
87
|
+
*/
|
|
88
|
+
export type LambderApiGuard<TInput extends z.ZodType = z.ZodType, TParam = any, TOutput = any, TCtx = any, TSessionCtx = TCtx> = {
|
|
89
|
+
apiInput: TInput;
|
|
90
|
+
guardInput?: undefined;
|
|
91
|
+
session: true;
|
|
92
|
+
handler: LambderGuardHandler<TSessionCtx, z.output<TInput>, TParam, TOutput>;
|
|
93
|
+
} | {
|
|
94
|
+
apiInput: TInput;
|
|
95
|
+
guardInput?: undefined;
|
|
96
|
+
session?: false;
|
|
97
|
+
handler: LambderGuardHandler<TCtx, z.output<TInput>, TParam, TOutput>;
|
|
98
|
+
} | {
|
|
99
|
+
guardInput: TInput;
|
|
100
|
+
apiInput?: undefined;
|
|
101
|
+
session: true;
|
|
102
|
+
handler: LambderGuardHandler<TSessionCtx, z.output<TInput>, TParam, TOutput>;
|
|
103
|
+
} | {
|
|
104
|
+
guardInput: TInput;
|
|
105
|
+
apiInput?: undefined;
|
|
106
|
+
session?: false;
|
|
107
|
+
handler: LambderGuardHandler<TCtx, z.output<TInput>, TParam, TOutput>;
|
|
108
|
+
} | {
|
|
109
|
+
apiInput?: undefined;
|
|
110
|
+
guardInput?: undefined;
|
|
111
|
+
session: true;
|
|
112
|
+
handler: LambderGuardHandler<TSessionCtx, undefined, TParam, TOutput>;
|
|
113
|
+
} | {
|
|
114
|
+
apiInput?: undefined;
|
|
115
|
+
guardInput?: undefined;
|
|
116
|
+
session?: false;
|
|
117
|
+
handler: LambderGuardHandler<TCtx, undefined, TParam, TOutput>;
|
|
118
|
+
};
|
|
119
|
+
/**
|
|
120
|
+
* The builder's shape, generic over the two context types a guard may
|
|
121
|
+
* receive: the plain one and the session-typed one. Ties the handler's
|
|
122
|
+
* payload, context, param, and output types together inside one literal
|
|
123
|
+
* and returns the exact shape so type extraction (mode, session, param,
|
|
124
|
+
* output) works downstream. The param type is inferred from the handler's
|
|
125
|
+
* 3rd argument annotation; the output from its return type.
|
|
126
|
+
*/
|
|
127
|
+
export type LambderGuardBuilder<TCtx, TSessionCtx> = {
|
|
128
|
+
<TInput extends z.ZodType, TParam = undefined, TOutput = void>(guard: {
|
|
129
|
+
apiInput: TInput;
|
|
130
|
+
session: true;
|
|
131
|
+
handler: (ctx: TSessionCtx, payload: z.output<TInput>, param: TParam) => TOutput | Promise<TOutput>;
|
|
132
|
+
} & LambderGuardAnswerCheck<TOutput>): LambderGuardOf<TOutput, {
|
|
133
|
+
apiInput: TInput;
|
|
134
|
+
guardInput?: undefined;
|
|
135
|
+
session: true;
|
|
136
|
+
handler: (ctx: TSessionCtx, payload: z.output<TInput>, param: TParam) => TOutput | Promise<TOutput>;
|
|
137
|
+
}>;
|
|
138
|
+
<TInput extends z.ZodType, TParam = undefined, TOutput = void>(guard: {
|
|
139
|
+
apiInput: TInput;
|
|
140
|
+
handler: (ctx: TCtx, payload: z.output<TInput>, param: TParam) => TOutput | Promise<TOutput>;
|
|
141
|
+
} & LambderGuardAnswerCheck<TOutput>): LambderGuardOf<TOutput, {
|
|
142
|
+
apiInput: TInput;
|
|
143
|
+
guardInput?: undefined;
|
|
144
|
+
session?: undefined;
|
|
145
|
+
handler: (ctx: TCtx, payload: z.output<TInput>, param: TParam) => TOutput | Promise<TOutput>;
|
|
146
|
+
}>;
|
|
147
|
+
<TInput extends z.ZodType, TParam = undefined, TOutput = void>(guard: {
|
|
148
|
+
guardInput: TInput;
|
|
149
|
+
session: true;
|
|
150
|
+
handler: (ctx: TSessionCtx, payload: z.output<TInput>, param: TParam) => TOutput | Promise<TOutput>;
|
|
151
|
+
} & LambderGuardAnswerCheck<TOutput>): LambderGuardOf<TOutput, {
|
|
152
|
+
guardInput: TInput;
|
|
153
|
+
apiInput?: undefined;
|
|
154
|
+
session: true;
|
|
155
|
+
handler: (ctx: TSessionCtx, payload: z.output<TInput>, param: TParam) => TOutput | Promise<TOutput>;
|
|
156
|
+
}>;
|
|
157
|
+
<TInput extends z.ZodType, TParam = undefined, TOutput = void>(guard: {
|
|
158
|
+
guardInput: TInput;
|
|
159
|
+
handler: (ctx: TCtx, payload: z.output<TInput>, param: TParam) => TOutput | Promise<TOutput>;
|
|
160
|
+
} & LambderGuardAnswerCheck<TOutput>): LambderGuardOf<TOutput, {
|
|
161
|
+
guardInput: TInput;
|
|
162
|
+
apiInput?: undefined;
|
|
163
|
+
session?: undefined;
|
|
164
|
+
handler: (ctx: TCtx, payload: z.output<TInput>, param: TParam) => TOutput | Promise<TOutput>;
|
|
165
|
+
}>;
|
|
166
|
+
<TParam = undefined, TOutput = void>(guard: {
|
|
167
|
+
session: true;
|
|
168
|
+
handler: (ctx: TSessionCtx, payload: undefined, param: TParam) => TOutput | Promise<TOutput>;
|
|
169
|
+
} & LambderGuardAnswerCheck<TOutput>): LambderGuardOf<TOutput, {
|
|
170
|
+
apiInput?: undefined;
|
|
171
|
+
guardInput?: undefined;
|
|
172
|
+
session: true;
|
|
173
|
+
handler: (ctx: TSessionCtx, payload: undefined, param: TParam) => TOutput | Promise<TOutput>;
|
|
174
|
+
}>;
|
|
175
|
+
<TParam = undefined, TOutput = void>(guard: {
|
|
176
|
+
handler: (ctx: TCtx, payload: undefined, param: TParam) => TOutput | Promise<TOutput>;
|
|
177
|
+
} & LambderGuardAnswerCheck<TOutput>): LambderGuardOf<TOutput, {
|
|
178
|
+
apiInput?: undefined;
|
|
179
|
+
guardInput?: undefined;
|
|
180
|
+
session?: undefined;
|
|
181
|
+
handler: (ctx: TCtx, payload: undefined, param: TParam) => TOutput | Promise<TOutput>;
|
|
182
|
+
}>;
|
|
183
|
+
};
|
|
184
|
+
/**
|
|
185
|
+
* A guard builder bound to a pair of context types. The server's
|
|
186
|
+
* lambderGuard() is this bound to the render contexts; the mock runtime
|
|
187
|
+
* binds it to its own handler contexts, so mock guards are the same shape
|
|
188
|
+
* as server guards and run through the same engine.
|
|
189
|
+
*/
|
|
190
|
+
export declare const lambderGuardBuilder: <TCtx, TSessionCtx>() => LambderGuardBuilder<TCtx, TSessionCtx>;
|
|
191
|
+
/** The param type a guard's handler declares as its 3rd argument; undefined for paramless guards. */
|
|
192
|
+
type LambderGuardParamOf<G> = G extends {
|
|
193
|
+
handler: (...args: infer A) => any;
|
|
194
|
+
} ? (A extends [any, any, infer P, ...any[]] ? P : undefined) : undefined;
|
|
195
|
+
/** What a guard's handler returns (awaited); void for check-only guards. */
|
|
196
|
+
type LambderGuardOutputOf<G> = G extends {
|
|
197
|
+
handler: (...args: any[]) => infer R;
|
|
198
|
+
} ? Awaited<R> : never;
|
|
199
|
+
/** Per-guard metadata carried on the Lambder instance: input mode, session requirement, param type, output type. */
|
|
200
|
+
export type LambderGuardMeta<G> = (G extends {
|
|
201
|
+
apiInput: infer S extends z.ZodType;
|
|
202
|
+
} ? {
|
|
203
|
+
apiInput: z.output<S>;
|
|
204
|
+
} : G extends {
|
|
205
|
+
guardInput: infer S extends z.ZodType;
|
|
206
|
+
} ? {
|
|
207
|
+
guardInput: z.output<S>;
|
|
208
|
+
} : {}) & (G extends {
|
|
209
|
+
session: true;
|
|
210
|
+
} ? {
|
|
211
|
+
session: true;
|
|
212
|
+
} : {}) & {
|
|
213
|
+
param: LambderGuardParamOf<G>;
|
|
214
|
+
output: LambderGuardOutputOf<G>;
|
|
215
|
+
};
|
|
216
|
+
export type LambderGuardMetaMap<TGuards> = {
|
|
217
|
+
[K in keyof TGuards]: LambderGuardMeta<TGuards[K]>;
|
|
218
|
+
};
|
|
219
|
+
type LambderGuardNameIfPayloadOk<TGuards, K extends keyof TGuards, TPayload> = TGuards[K] extends {
|
|
220
|
+
apiInput: infer R;
|
|
221
|
+
} ? (TPayload extends R ? K : never) : K;
|
|
222
|
+
/**
|
|
223
|
+
* Guard names an API may declare: apiInput-mode guards only when the API's
|
|
224
|
+
* payload carries their fields, session guards only on session APIs.
|
|
225
|
+
*/
|
|
226
|
+
export type LambderAllowedGuardNames<TGuards, TPayload, TIncludeSession extends boolean = true> = {
|
|
227
|
+
[K in keyof TGuards]: TGuards[K] extends {
|
|
228
|
+
session: true;
|
|
229
|
+
} ? (TIncludeSession extends true ? LambderGuardNameIfPayloadOk<TGuards, K, TPayload> : never) : LambderGuardNameIfPayloadOk<TGuards, K, TPayload>;
|
|
230
|
+
}[keyof TGuards] & string;
|
|
231
|
+
/** The allowed guard names whose handler takes no param (usable in the string/array forms). */
|
|
232
|
+
export type LambderParamlessGuardNames<TGuards, TPayload, TIncludeSession extends boolean> = {
|
|
233
|
+
[K in LambderAllowedGuardNames<TGuards, TPayload, TIncludeSession> & keyof TGuards]: TGuards[K] extends {
|
|
234
|
+
param: undefined;
|
|
235
|
+
} ? K & string : never;
|
|
236
|
+
}[LambderAllowedGuardNames<TGuards, TPayload, TIncludeSession> & keyof TGuards];
|
|
237
|
+
/** The map form's full shape: every declarable guard name, each carrying its own param type. */
|
|
238
|
+
type LambderGuardsMap<TGuards, TPayload, TIncludeSession extends boolean> = {
|
|
239
|
+
readonly [K in LambderAllowedGuardNames<TGuards, TPayload, TIncludeSession> & keyof TGuards]?: TGuards[K] extends {
|
|
240
|
+
param: undefined;
|
|
241
|
+
} ? true : TGuards[K] extends {
|
|
242
|
+
param: infer P;
|
|
243
|
+
} ? P : true;
|
|
244
|
+
};
|
|
245
|
+
/**
|
|
246
|
+
* The per-API `guards` option: one paramless guard name, a non-empty ordered
|
|
247
|
+
* list of paramless names, or a non-empty object map that can carry each
|
|
248
|
+
* guard's param (`true` enables a paramless guard). Map entries run in
|
|
249
|
+
* insertion order.
|
|
250
|
+
*
|
|
251
|
+
* Every form is non-empty by construction, so declaring the option is always
|
|
252
|
+
* declaring a guard. See LambderNonEmptyOptionMap.
|
|
253
|
+
*/
|
|
254
|
+
export type LambderGuardsOption<TGuards, TPayload, TIncludeSession extends boolean> = LambderParamlessGuardNames<TGuards, TPayload, TIncludeSession> | readonly [
|
|
255
|
+
LambderParamlessGuardNames<TGuards, TPayload, TIncludeSession>,
|
|
256
|
+
...LambderParamlessGuardNames<TGuards, TPayload, TIncludeSession>[]
|
|
257
|
+
] | LambderNonEmptyOptionMap<LambderGuardsMap<TGuards, TPayload, TIncludeSession>>;
|
|
258
|
+
/**
|
|
259
|
+
* The typed ctx.guardData an API's handler sees: declared guards that return
|
|
260
|
+
* a value, keyed by name. Check-only (void) guards never appear.
|
|
261
|
+
*/
|
|
262
|
+
export type LambderGuardDataOf<TGuards, TOpt> = {
|
|
263
|
+
[K in LambderGuardNamesIn<TOpt> & keyof TGuards as [
|
|
264
|
+
TGuards[K] extends {
|
|
265
|
+
output: infer O;
|
|
266
|
+
} ? O : never
|
|
267
|
+
] extends [void] ? never : K & string]: TGuards[K] extends {
|
|
268
|
+
output: infer O;
|
|
269
|
+
} ? O : never;
|
|
270
|
+
};
|
|
271
|
+
type GuardInputsEntries<TGuards, TOpt> = {
|
|
272
|
+
[K in Extract<LambderGuardNamesIn<TOpt>, keyof TGuards> as TGuards[K] extends {
|
|
273
|
+
guardInput: any;
|
|
274
|
+
} ? K : never]: TGuards[K] extends {
|
|
275
|
+
guardInput: infer V;
|
|
276
|
+
} ? V : never;
|
|
277
|
+
};
|
|
278
|
+
/** The guardInputs map an API's contract requires clients to send; never when no declared guard uses guardInput mode. */
|
|
279
|
+
export type LambderGuardInputsOf<TGuards, TOpt> = keyof GuardInputsEntries<TGuards, TOpt> extends never ? never : GuardInputsEntries<TGuards, TOpt>;
|
|
280
|
+
/**
|
|
281
|
+
* Runtime side of the guards subsystem: holds the defined guards, asserts
|
|
282
|
+
* API registrations against them at startup, and executes an API's declared
|
|
283
|
+
* guards during preflight. Composed into LambderApiPolicyEngine. Reads the
|
|
284
|
+
* request and writes the call context, so it runs unchanged under the
|
|
285
|
+
* server and the mock runtime.
|
|
286
|
+
*/
|
|
287
|
+
export declare class LambderApiGuardsEngine {
|
|
288
|
+
private guards;
|
|
289
|
+
/** True once a guards map was configured. */
|
|
290
|
+
get isConfigured(): boolean;
|
|
291
|
+
/** Take the guards map given at creation; named like the other two engines' configure(). */
|
|
292
|
+
configure(guards: Record<string, LambderApiGuard<any, any, any>>): void;
|
|
293
|
+
/** Startup validation of one API registration's guards option. */
|
|
294
|
+
assertRegistration(apiName: string, mode: LambderApiMode, guardsOption?: LambderGuardsOptionValue): void;
|
|
295
|
+
/**
|
|
296
|
+
* Run the API's guards in declared order. Refusals throw; outputs land on
|
|
297
|
+
* ctx.guardData. Each guard is recorded on the trace as it returns, so a
|
|
298
|
+
* call that a later guard refused still reports the ones that passed.
|
|
299
|
+
*/
|
|
300
|
+
run(request: LambderApiRequest, ctx: LambderApiCallContext, guardsOption: LambderGuardsOptionValue | undefined, trace: LambderApiCallTrace): Promise<void>;
|
|
301
|
+
}
|
|
302
|
+
export {};
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { parsePreflightSlice } from "./LambderApiValidationRefusal.js";
|
|
2
|
+
import { LAMBDER_RESPONSE_BRAND, isLambderResponseLike } from "../shared/util/LambderResponseBrand.js";
|
|
3
|
+
/**
|
|
4
|
+
* A guard builder bound to a pair of context types. The server's
|
|
5
|
+
* lambderGuard() is this bound to the render contexts; the mock runtime
|
|
6
|
+
* binds it to its own handler contexts, so mock guards are the same shape
|
|
7
|
+
* as server guards and run through the same engine.
|
|
8
|
+
*/
|
|
9
|
+
export const lambderGuardBuilder = () => ((guard) => guard);
|
|
10
|
+
/** Normalize the three guards-option forms into ordered { name, param } entries. Internal to the engine: nothing outside it reads a guards option. */
|
|
11
|
+
const toGuardEntries = (value) => {
|
|
12
|
+
if (value === undefined)
|
|
13
|
+
return [];
|
|
14
|
+
if (typeof value === "string")
|
|
15
|
+
return [{ name: value, param: undefined }];
|
|
16
|
+
if (Array.isArray(value))
|
|
17
|
+
return value.map((name) => ({ name: String(name), param: undefined }));
|
|
18
|
+
// Object form: insertion order, params passed verbatim (paramless guards
|
|
19
|
+
// are declared with `true` and their handlers take no param argument).
|
|
20
|
+
return Object.entries(value).map(([name, param]) => ({ name, param }));
|
|
21
|
+
};
|
|
22
|
+
/**
|
|
23
|
+
* One guard's input out of the map the client posted. The map is client
|
|
24
|
+
* data, so it is read as data: a guard named for something Object.prototype
|
|
25
|
+
* carries ("toString", "constructor") must come back absent when the client
|
|
26
|
+
* sent nothing, not as the inherited function. Guard names are deliberately
|
|
27
|
+
* unrestricted, which is what makes this the read's problem rather than the
|
|
28
|
+
* name's.
|
|
29
|
+
*/
|
|
30
|
+
const readGuardInput = (guardInputs, name) => guardInputs !== undefined && Object.prototype.hasOwnProperty.call(guardInputs, name) ? guardInputs[name] : undefined;
|
|
31
|
+
/**
|
|
32
|
+
* Runtime side of the guards subsystem: holds the defined guards, asserts
|
|
33
|
+
* API registrations against them at startup, and executes an API's declared
|
|
34
|
+
* guards during preflight. Composed into LambderApiPolicyEngine. Reads the
|
|
35
|
+
* request and writes the call context, so it runs unchanged under the
|
|
36
|
+
* server and the mock runtime.
|
|
37
|
+
*/
|
|
38
|
+
export class LambderApiGuardsEngine {
|
|
39
|
+
// A Map, not an object: a plain object answers for "toString" and
|
|
40
|
+
// "constructor" through its prototype, so an API declaring one of those as
|
|
41
|
+
// a guard name would pass the registration check that exists to catch
|
|
42
|
+
// exactly that typo, and then fail on every request. It also refuses to
|
|
43
|
+
// register a guard legitimately named one of them.
|
|
44
|
+
guards = new Map();
|
|
45
|
+
/** True once a guards map was configured. */
|
|
46
|
+
get isConfigured() { return this.guards.size > 0; }
|
|
47
|
+
/** Take the guards map given at creation; named like the other two engines' configure(). */
|
|
48
|
+
configure(guards) {
|
|
49
|
+
// One configuration per instance, the rule the rate-limit and
|
|
50
|
+
// idempotency engines already hold to: a second map would silently
|
|
51
|
+
// merge into the first, and which of two same-named guards ran would
|
|
52
|
+
// depend on the order the calls happened to be made in.
|
|
53
|
+
if (this.guards.size > 0)
|
|
54
|
+
throw new Error("Lambder: guards were already configured.");
|
|
55
|
+
// Declaring the option is always declaring a guard, the same rule an
|
|
56
|
+
// API's own `guards: {}` is held to. Without this the engine stays
|
|
57
|
+
// unconfigured and every API that declares a guard is told that no
|
|
58
|
+
// guards option was given at all, which sends the reader to the wrong
|
|
59
|
+
// line.
|
|
60
|
+
if (Object.keys(guards).length === 0) {
|
|
61
|
+
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
|
+
}
|
|
63
|
+
for (const [name, guardDef] of Object.entries(guards)) {
|
|
64
|
+
if (this.guards.has(name))
|
|
65
|
+
throw new Error(`Lambder: guard "${name}" is already defined.`);
|
|
66
|
+
if (typeof guardDef?.handler !== "function")
|
|
67
|
+
throw new Error(`Lambder: guard "${name}" has no handler function.`);
|
|
68
|
+
if (guardDef.apiInput && guardDef.guardInput)
|
|
69
|
+
throw new Error(`Lambder: guard "${name}" declares both apiInput and guardInput; pick one.`);
|
|
70
|
+
this.guards.set(name, guardDef);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
/** Startup validation of one API registration's guards option. */
|
|
74
|
+
assertRegistration(apiName, mode, guardsOption) {
|
|
75
|
+
const entries = toGuardEntries(guardsOption);
|
|
76
|
+
// The runtime half of LambderNonEmptyGuardsMap. `guards: {}` and
|
|
77
|
+
// `guards: []` are present-but-empty: they satisfy the require*ApiGuards
|
|
78
|
+
// field check while running nothing, which is the one shape that turns a
|
|
79
|
+
// mandatory authorization declaration back into an optional one. The type
|
|
80
|
+
// rejects both; a plain-JS caller, a cast, or a spread that happened to
|
|
81
|
+
// produce an empty object lands here instead.
|
|
82
|
+
if (guardsOption !== undefined && entries.length === 0) {
|
|
83
|
+
throw new Error(`Lambder: API "${apiName}" declares an empty guards option, which authorizes nothing. ` +
|
|
84
|
+
`Name the guard that authorizes it, or omit the option entirely.`);
|
|
85
|
+
}
|
|
86
|
+
for (const { name } of entries) {
|
|
87
|
+
const guardDef = this.guards.get(name);
|
|
88
|
+
if (!guardDef) {
|
|
89
|
+
throw new Error(`Lambder: API "${apiName}" references unknown guard "${name}". Declare it in the guards option at creation.`);
|
|
90
|
+
}
|
|
91
|
+
if (guardDef.session && mode !== "session") {
|
|
92
|
+
throw new Error(`Lambder: API "${apiName}" uses guard "${name}" (session: true), which requires addSessionApi.`);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Run the API's guards in declared order. Refusals throw; outputs land on
|
|
98
|
+
* ctx.guardData. Each guard is recorded on the trace as it returns, so a
|
|
99
|
+
* call that a later guard refused still reports the ones that passed.
|
|
100
|
+
*/
|
|
101
|
+
async run(request, ctx, guardsOption, trace) {
|
|
102
|
+
for (const { name, param } of toGuardEntries(guardsOption)) {
|
|
103
|
+
const guardDef = this.guards.get(name);
|
|
104
|
+
if (!guardDef)
|
|
105
|
+
throw new Error(`Lambder: guard "${name}" is not configured. Declare it in the guards option at creation.`);
|
|
106
|
+
// Recorded before anything this guard does can refuse, so the list
|
|
107
|
+
// says which guards were reached and the one that said no is the
|
|
108
|
+
// last name on it. Recording after the return named every guard
|
|
109
|
+
// except the one someone reading the trace was looking for; doing
|
|
110
|
+
// it after the slice parse below had the same effect for a guard
|
|
111
|
+
// that refuses by rejecting its own input, which answers 422 and
|
|
112
|
+
// is exactly the refusal a reader is trying to place.
|
|
113
|
+
trace.guardsRun.push(name);
|
|
114
|
+
let payload;
|
|
115
|
+
if (guardDef.apiInput) {
|
|
116
|
+
payload = parsePreflightSlice(guardDef.apiInput, request.payload);
|
|
117
|
+
}
|
|
118
|
+
else if (guardDef.guardInput) {
|
|
119
|
+
payload = parsePreflightSlice(guardDef.guardInput, readGuardInput(request.guardInputs, name));
|
|
120
|
+
}
|
|
121
|
+
// A guard's return value becomes the handler's typed
|
|
122
|
+
// ctx.guardData[name]; check-only guards return undefined.
|
|
123
|
+
const output = await guardDef.handler(ctx, payload, param);
|
|
124
|
+
if (isLambderResponseLike(output)) {
|
|
125
|
+
throw new Error(`Lambder: guard "${name}" returned a LambderResponse. A guard authorizes, it does not answer: ` +
|
|
126
|
+
`say no with refuse() or by throwing a LambderApiRefusal. Returning a response denies nothing, ` +
|
|
127
|
+
`because the value would be attached to ctx.guardData and the call would continue.`);
|
|
128
|
+
}
|
|
129
|
+
if (output !== undefined) {
|
|
130
|
+
ctx.guardData[name] = output;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import type { LambderApiRequest } from "./LambderApiRequest.js";
|
|
2
|
+
import type { LambderApiCallContext, LambderApiCallTrace } from "./LambderApiCallContext.js";
|
|
3
|
+
import type { LambderApiAnswer } from "./LambderApiAnswer.js";
|
|
4
|
+
import type { LambderIdempotencyStore } from "../shared/contracts/LambderIdempotencyStore.js";
|
|
5
|
+
import type { LambderApiIdempotencyOption } from "../shared/wire/LambderApiOptionValues.js";
|
|
6
|
+
export type LambderApiIdempotencyConfig = {
|
|
7
|
+
/** Your idempotency store instance; may share the rate limiter's table (distinct key prefix). */
|
|
8
|
+
store: LambderIdempotencyStore;
|
|
9
|
+
/** Seconds a stored response replays for. Default: 86400 (24h). Per-API override: idempotency: { ttlSeconds }. */
|
|
10
|
+
defaultTtlSeconds?: number;
|
|
11
|
+
/**
|
|
12
|
+
* Seconds a claim stays pending before a retry may take the scope.
|
|
13
|
+
* Default: 300. Raise it past the longest a handler of yours can run, or
|
|
14
|
+
* a retry arriving after it expires executes the operation again while
|
|
15
|
+
* the original is still working. Per-API override:
|
|
16
|
+
* idempotency: { pendingTtlSeconds }.
|
|
17
|
+
*/
|
|
18
|
+
defaultPendingTtlSeconds?: number;
|
|
19
|
+
/** Skip idempotency (execute normally) when the store errors, instead of failing the request. Default: true. */
|
|
20
|
+
failOpen?: boolean;
|
|
21
|
+
/**
|
|
22
|
+
* Who a request is acting as, for scoping a PUBLIC API's stored answer.
|
|
23
|
+
* Session APIs already scope per session, so this only affects the ones
|
|
24
|
+
* that do not have a session to scope by.
|
|
25
|
+
*
|
|
26
|
+
* Without it, a public API's scope is the posted key alone, which makes
|
|
27
|
+
* that key a bearer token for its own stored answer: anyone presenting it
|
|
28
|
+
* gets the response back, and the replay is served BEFORE guards run, so
|
|
29
|
+
* an API whose authorization is a guard hands its answer over without the
|
|
30
|
+
* guard ever being consulted. Returning an identity here puts that caller
|
|
31
|
+
* in the scope, so a key only replays to whoever it was issued to.
|
|
32
|
+
*
|
|
33
|
+
* Consulted only on public APIs, and a public API reads no session, so
|
|
34
|
+
* there is no session here to read: the context arrives without one, and
|
|
35
|
+
* an identity that came from a session would be the session scope the
|
|
36
|
+
* engine already applies. It sees what the request itself carries, and
|
|
37
|
+
* that is the point of it: `request.guardInputs`, `request.payload`,
|
|
38
|
+
* `request.headers` and `request.ip`.
|
|
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.
|
|
44
|
+
*/
|
|
45
|
+
callerIdentity?: (ctx: Omit<LambderApiCallContext, "session">, request: LambderApiRequest) => string | null | Promise<string | null>;
|
|
46
|
+
};
|
|
47
|
+
/**
|
|
48
|
+
* Runtime side of the idempotency subsystem: claims a per-operation scope
|
|
49
|
+
* around handler execution, replays stored answers, and settles claims.
|
|
50
|
+
* Composed into LambderApiPolicyEngine. Works on plain answers, so it runs
|
|
51
|
+
* unchanged under the server and the mock runtime.
|
|
52
|
+
*/
|
|
53
|
+
export declare class LambderApiIdempotencyEngine {
|
|
54
|
+
private store;
|
|
55
|
+
private defaultTtlSeconds;
|
|
56
|
+
private defaultPendingTtlSeconds;
|
|
57
|
+
private failOpen;
|
|
58
|
+
private callerIdentity;
|
|
59
|
+
/**
|
|
60
|
+
* The scope this call resolved to, keyed by its context, which is the one
|
|
61
|
+
* object per call the engine is handed. A keyed request asks for it
|
|
62
|
+
* twice, at the replay lookup and at the claim, and callerIdentity is app
|
|
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.
|
|
66
|
+
*/
|
|
67
|
+
private readonly scopeByCall;
|
|
68
|
+
configure(config: LambderApiIdempotencyConfig): void;
|
|
69
|
+
/** Startup validation of one API registration's idempotency option. */
|
|
70
|
+
assertRegistration(apiName: string, config: LambderApiIdempotencyOption): void;
|
|
71
|
+
/** True once the idempotency option was configured; registration asserts check it. */
|
|
72
|
+
get isConfigured(): boolean;
|
|
73
|
+
/**
|
|
74
|
+
* The request's idempotencyKey: null when absent, the key when valid, a
|
|
75
|
+
* 400 refusal when malformed. The minimum length matters for security:
|
|
76
|
+
* see IDEMPOTENCY_MIN_KEY_LENGTH.
|
|
77
|
+
*/
|
|
78
|
+
private readKey;
|
|
79
|
+
/**
|
|
80
|
+
* The record's scope. Session APIs scope per session, so even a leaked
|
|
81
|
+
* key cannot cross users. Public APIs scope by the key alone unless the
|
|
82
|
+
* app supplies callerIdentity, because the key is required to be long
|
|
83
|
+
* (and documented to be random), and identity proxies like the client IP
|
|
84
|
+
* are deliberately NOT part of the scope: the retry idempotency exists
|
|
85
|
+
* for (a timeout followed by a network change) frequently arrives from a
|
|
86
|
+
* different IP. An app whose public APIs are authorized by a guard should
|
|
87
|
+
* give callerIdentity, since the replay is served before guards run.
|
|
88
|
+
*
|
|
89
|
+
* Fields are escaped and joined through joinKeyFields, so no two distinct
|
|
90
|
+
* scopes can produce one string.
|
|
91
|
+
*/
|
|
92
|
+
private scopeOf;
|
|
93
|
+
private computeScope;
|
|
94
|
+
/**
|
|
95
|
+
* Replay fast path, run before the remaining rate limits and before
|
|
96
|
+
* guards: a completed record answers with its stored answer, so a
|
|
97
|
+
* legitimate retry neither burns rate-limit quota nor re-runs guards (the
|
|
98
|
+
* original already passed them, and no handler executes). The `per: "ip"`
|
|
99
|
+
* limits are the exception and are checked ahead of this, since the store
|
|
100
|
+
* read a replay costs is one of the things they exist to bound. Misses
|
|
101
|
+
* fall through to the normal pipeline; store errors follow the failOpen
|
|
102
|
+
* setting.
|
|
103
|
+
*/
|
|
104
|
+
findReplay(apiName: string, request: LambderApiRequest, ctx: LambderApiCallContext, trace: LambderApiCallTrace): Promise<LambderApiAnswer | null>;
|
|
105
|
+
/**
|
|
106
|
+
* Idempotency wrapper around validation-passed handler execution. Without
|
|
107
|
+
* a client idempotencyKey the handler just runs; with one, the scope
|
|
108
|
+
* (identity + api + key) is claimed atomically: duplicates of an
|
|
109
|
+
* in-flight original refuse with 409, replays of a completed one return
|
|
110
|
+
* the stored answer verbatim, and a crashed original releases its claim
|
|
111
|
+
* so a retry actually retries.
|
|
112
|
+
*
|
|
113
|
+
* `exec` must hand back the handler's own answer, the headers the handler
|
|
114
|
+
* itself wrote included: the pipeline applies those before returning here,
|
|
115
|
+
* so a Set-Cookie the handler set is visible to the caching rule below.
|
|
116
|
+
* Headers written EARLIER in the call (a session read evicting a stale
|
|
117
|
+
* cookie) are deliberately not on it: they are the call's, they reach the
|
|
118
|
+
* client either way, and charging them to this answer would make an
|
|
119
|
+
* idempotent operation silently stop being idempotent.
|
|
120
|
+
*/
|
|
121
|
+
withIdempotency(apiName: string, request: LambderApiRequest, ctx: LambderApiCallContext, config: LambderApiIdempotencyOption, trace: LambderApiCallTrace, exec: () => Promise<LambderApiAnswer>): Promise<LambderApiAnswer>;
|
|
122
|
+
}
|