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.
Files changed (209) hide show
  1. package/CHANGELOG.md +1021 -3
  2. package/README.md +43 -21
  3. package/dist/api/LambderApiAnswer.d.ts +18 -22
  4. package/dist/api/LambderApiAnswer.js +6 -7
  5. package/dist/api/LambderApiCallContext.d.ts +21 -8
  6. package/dist/api/LambderApiCallContext.js +22 -4
  7. package/dist/api/LambderApiDefinition.d.ts +4 -3
  8. package/dist/api/LambderApiEnvelope.d.ts +14 -9
  9. package/dist/api/LambderApiEnvelope.js +33 -34
  10. package/dist/api/LambderApiGuards.d.ts +78 -51
  11. package/dist/api/LambderApiGuards.js +34 -36
  12. package/dist/api/LambderApiIdempotency.d.ts +74 -61
  13. package/dist/api/LambderApiIdempotency.js +226 -151
  14. package/dist/api/LambderApiOutputValidationError.d.ts +32 -0
  15. package/dist/api/LambderApiOutputValidationError.js +50 -0
  16. package/dist/api/LambderApiPipeline.d.ts +77 -39
  17. package/dist/api/LambderApiPipeline.js +135 -62
  18. package/dist/api/LambderApiRateLimits.d.ts +208 -54
  19. package/dist/api/LambderApiRateLimits.js +197 -108
  20. package/dist/api/LambderApiRequest.d.ts +27 -21
  21. package/dist/api/LambderApiRequest.js +26 -19
  22. package/dist/api/LambderApiSignature.d.ts +12 -15
  23. package/dist/api/LambderApiSignature.js +28 -51
  24. package/dist/api/LambderApiValidationRefusal.d.ts +9 -9
  25. package/dist/api/LambderApiValidationRefusal.js +10 -10
  26. package/dist/build/freshProcessVerifier.d.ts +13 -0
  27. package/dist/build/freshProcessVerifier.js +19 -0
  28. package/dist/build/writeApiSignatures.d.ts +109 -0
  29. package/dist/build/writeApiSignatures.js +222 -0
  30. package/dist/build.d.ts +9 -0
  31. package/dist/build.js +8 -0
  32. package/dist/client/LambderCaller.d.ts +13 -44
  33. package/dist/client/LambderCaller.js +77 -84
  34. package/dist/client/LambderReloadLoopBreaker.d.ts +56 -26
  35. package/dist/client/LambderReloadLoopBreaker.js +90 -46
  36. package/dist/client/lambderFetchTransport.d.ts +4 -1
  37. package/dist/client/lambderFetchTransport.js +52 -28
  38. package/dist/client.d.ts +5 -3
  39. package/dist/client.js +2 -1
  40. package/dist/core/Lambder.d.ts +161 -69
  41. package/dist/core/Lambder.js +370 -226
  42. package/dist/core/LambderContext.d.ts +82 -15
  43. package/dist/core/LambderContext.js +107 -20
  44. package/dist/core/LambderCors.d.ts +21 -3
  45. package/dist/core/LambderCors.js +35 -16
  46. package/dist/core/LambderCrashHandling.d.ts +40 -0
  47. package/dist/core/LambderCrashHandling.js +97 -0
  48. package/dist/core/LambderCreateOptions.d.ts +151 -75
  49. package/dist/core/LambderCreateOptions.js +16 -23
  50. package/dist/core/LambderFiles.d.ts +28 -7
  51. package/dist/core/LambderFiles.js +73 -33
  52. package/dist/core/LambderIndexHtml.js +12 -11
  53. package/dist/core/LambderPolicyBuilders.d.ts +17 -5
  54. package/dist/core/LambderPolicyBuilders.js +17 -5
  55. package/dist/core/LambderPublicFiles.d.ts +11 -5
  56. package/dist/core/LambderPublicFiles.js +32 -4
  57. package/dist/core/LambderRequestPath.d.ts +43 -0
  58. package/dist/core/LambderRequestPath.js +63 -0
  59. package/dist/core/LambderResponse.d.ts +26 -5
  60. package/dist/core/LambderResponse.js +157 -70
  61. package/dist/core/LambderResponseBuilder.d.ts +49 -4
  62. package/dist/core/LambderResponseBuilder.js +64 -3
  63. package/dist/core/LambderRouting.d.ts +2 -3
  64. package/dist/core/LambderRouting.js +22 -7
  65. package/dist/core/LambderTemplatingEngine.js +211 -32
  66. package/dist/index.d.ts +15 -8
  67. package/dist/index.js +5 -4
  68. package/dist/invoke/LambderInvokeCaller.d.ts +37 -42
  69. package/dist/invoke/LambderInvokeCaller.js +76 -66
  70. package/dist/invoke/LambderInvokeOutcome.d.ts +27 -26
  71. package/dist/invoke/LambderInvokeOutcome.js +9 -22
  72. package/dist/invoke/LambderLambdaEvent.d.ts +44 -10
  73. package/dist/invoke/LambderLambdaEvent.js +80 -37
  74. package/dist/invoke/lambderHandlerTransport.d.ts +12 -10
  75. package/dist/invoke/lambderHandlerTransport.js +16 -19
  76. package/dist/mock/LambderMockApp.d.ts +67 -83
  77. package/dist/mock/LambderMockApp.js +167 -153
  78. package/dist/mock/LambderMockBrowserCookies.d.ts +24 -28
  79. package/dist/mock/LambderMockBrowserCookies.js +24 -28
  80. package/dist/mock/LambderMockCallRecorder.d.ts +15 -22
  81. package/dist/mock/LambderMockCallRecorder.js +19 -28
  82. package/dist/mock/LambderMockCreateOptions.d.ts +42 -24
  83. package/dist/mock/LambderMockEntryRegistry.d.ts +11 -12
  84. package/dist/mock/LambderMockEntryRegistry.js +24 -29
  85. package/dist/mock/LambderMockFailureInjector.d.ts +3 -6
  86. package/dist/mock/LambderMockFailureInjector.js +3 -6
  87. package/dist/mock/LambderMockTypes.d.ts +78 -108
  88. package/dist/mock/lambderMockInvokeTransport.d.ts +11 -13
  89. package/dist/mock/lambderMockInvokeTransport.js +11 -10
  90. package/dist/mock/lambderMockMswHandler.d.ts +33 -29
  91. package/dist/mock/lambderMockMswHandler.js +50 -39
  92. package/dist/mock.d.ts +3 -1
  93. package/dist/mock.js +5 -3
  94. package/dist/session/LambderSessionController.d.ts +108 -89
  95. package/dist/session/LambderSessionController.js +187 -168
  96. package/dist/session/LambderSessionCrypto.d.ts +16 -7
  97. package/dist/session/LambderSessionCrypto.js +26 -12
  98. package/dist/session/LambderSessionManager.d.ts +136 -47
  99. package/dist/session/LambderSessionManager.js +280 -139
  100. package/dist/shared/LambderHtml.d.ts +42 -3
  101. package/dist/shared/LambderHtml.js +127 -7
  102. package/dist/shared/LambderHtmlPositions.d.ts +173 -0
  103. package/dist/shared/LambderHtmlPositions.js +652 -0
  104. package/dist/shared/LambderI18n.d.ts +10 -11
  105. package/dist/shared/LambderI18n.js +33 -21
  106. package/dist/shared/contracts/LambderCache.d.ts +66 -0
  107. package/dist/shared/contracts/LambderCache.js +11 -0
  108. package/dist/shared/contracts/LambderFileSource.d.ts +6 -6
  109. package/dist/shared/contracts/LambderFileSource.js +5 -8
  110. package/dist/shared/contracts/LambderIdempotencyStore.d.ts +51 -22
  111. package/dist/shared/contracts/LambderIdempotencyStore.js +4 -5
  112. package/dist/shared/contracts/LambderRateLimiter.d.ts +27 -15
  113. package/dist/shared/contracts/LambderRateLimiter.js +4 -5
  114. package/dist/shared/contracts/LambderSessionStore.d.ts +65 -26
  115. package/dist/shared/contracts/LambderSessionStore.js +5 -6
  116. package/dist/shared/transport/LambderApiTransport.d.ts +27 -27
  117. package/dist/shared/transport/LambderApiTransport.js +7 -7
  118. package/dist/shared/transport/LambderCookieJar.d.ts +28 -35
  119. package/dist/shared/transport/LambderCookieJar.js +54 -66
  120. package/dist/shared/transport/lambderCookieJarTransport.d.ts +11 -13
  121. package/dist/shared/transport/lambderCookieJarTransport.js +24 -23
  122. package/dist/shared/util/LambderCallAbort.d.ts +5 -5
  123. package/dist/shared/util/LambderCallAbort.js +5 -5
  124. package/dist/shared/util/LambderClientIp.d.ts +27 -11
  125. package/dist/shared/util/LambderClientIp.js +96 -13
  126. package/dist/shared/util/LambderExpiringMap.d.ts +35 -49
  127. package/dist/shared/util/LambderExpiringMap.js +41 -57
  128. package/dist/shared/util/LambderNodeModules.js +6 -7
  129. package/dist/shared/util/LambderOptionChecks.d.ts +4 -4
  130. package/dist/shared/util/LambderOptionChecks.js +4 -4
  131. package/dist/shared/util/LambderResponseBrand.d.ts +5 -5
  132. package/dist/shared/util/LambderResponseBrand.js +5 -5
  133. package/dist/shared/util/LambderTestingDoors.d.ts +29 -0
  134. package/dist/shared/util/LambderTestingDoors.js +29 -0
  135. package/dist/shared/util/LambderTypeUtilities.d.ts +7 -8
  136. package/dist/shared/util/LambderTypeUtilities.js +3 -3
  137. package/dist/shared/util/boundKeyField.d.ts +20 -0
  138. package/dist/shared/util/boundKeyField.js +34 -0
  139. package/dist/shared/util/canonicalJson.d.ts +11 -0
  140. package/dist/shared/util/canonicalJson.js +28 -0
  141. package/dist/shared/util/joinKeyFields.d.ts +20 -0
  142. package/dist/shared/util/joinKeyFields.js +22 -0
  143. package/dist/shared/wire/LambderAnswerHeaders.d.ts +12 -16
  144. package/dist/shared/wire/LambderAnswerHeaders.js +12 -16
  145. package/dist/shared/wire/LambderApiContract.d.ts +107 -32
  146. package/dist/shared/wire/LambderApiOutcome.d.ts +43 -31
  147. package/dist/shared/wire/LambderApiOutcome.js +48 -23
  148. package/dist/shared/wire/LambderApiRefusal.d.ts +39 -27
  149. package/dist/shared/wire/LambderApiRefusal.js +36 -7
  150. package/dist/shared/wire/LambderApiSignature.d.ts +18 -22
  151. package/dist/shared/wire/LambderApiSignature.js +16 -19
  152. package/dist/shared/wire/LambderCallOptions.d.ts +38 -47
  153. package/dist/shared/wire/LambderCallOptions.js +9 -11
  154. package/dist/shared/wire/LambderCompressionCodec.d.ts +29 -34
  155. package/dist/shared/wire/LambderCompressionCodec.js +31 -36
  156. package/dist/shared/wire/LambderCompressionOption.d.ts +9 -9
  157. package/dist/shared/wire/LambderCompressionOption.js +9 -9
  158. package/dist/shared/wire/LambderCrashDetail.d.ts +12 -15
  159. package/dist/shared/wire/LambderCrashDetail.js +12 -15
  160. package/dist/shared/wire/LambderDefaultApiPath.d.ts +6 -0
  161. package/dist/shared/wire/LambderDefaultApiPath.js +6 -0
  162. package/dist/shared/wire/LambderHttpStatus.d.ts +6 -7
  163. package/dist/shared/wire/LambderIdempotencyKeyScope.d.ts +89 -0
  164. package/dist/shared/wire/LambderIdempotencyKeyScope.js +146 -0
  165. package/dist/shared/wire/LambderInvokeApiId.d.ts +27 -0
  166. package/dist/shared/wire/LambderInvokeApiId.js +27 -0
  167. package/dist/shared/wire/LambderOutcomeAssertions.d.ts +79 -0
  168. package/dist/shared/wire/LambderOutcomeAssertions.js +112 -0
  169. package/dist/shared/wire/LambderRequestPayload.d.ts +18 -20
  170. package/dist/shared/wire/LambderRequestPayload.js +4 -6
  171. package/dist/stores/LambderCacheFiller.d.ts +48 -0
  172. package/dist/stores/LambderCacheFiller.js +119 -0
  173. package/dist/stores/LambderCacheKeys.d.ts +26 -0
  174. package/dist/stores/LambderCacheKeys.js +54 -0
  175. package/dist/stores/LambderCacheValues.d.ts +45 -0
  176. package/dist/stores/LambderCacheValues.js +74 -0
  177. package/dist/stores/LambderDdbCache.d.ts +121 -56
  178. package/dist/stores/LambderDdbCache.js +528 -225
  179. package/dist/stores/LambderDdbIdempotencyStore.d.ts +33 -22
  180. package/dist/stores/LambderDdbIdempotencyStore.js +75 -50
  181. package/dist/stores/LambderDdbRateLimiter.d.ts +76 -20
  182. package/dist/stores/LambderDdbRateLimiter.js +151 -39
  183. package/dist/stores/LambderDdbSdk.d.ts +43 -31
  184. package/dist/stores/LambderDdbSdk.js +79 -33
  185. package/dist/stores/LambderDdbSessionStore.d.ts +27 -14
  186. package/dist/stores/LambderDdbSessionStore.js +119 -47
  187. package/dist/stores/LambderHttpFileSource.d.ts +15 -6
  188. package/dist/stores/LambderHttpFileSource.js +15 -13
  189. package/dist/stores/LambderMemoryCache.d.ts +49 -0
  190. package/dist/stores/LambderMemoryCache.js +113 -0
  191. package/dist/stores/LambderMemoryIdempotencyStore.d.ts +13 -12
  192. package/dist/stores/LambderMemoryIdempotencyStore.js +31 -30
  193. package/dist/stores/LambderMemoryRateLimiter.d.ts +8 -9
  194. package/dist/stores/LambderMemoryRateLimiter.js +14 -13
  195. package/dist/stores/LambderMemorySessionStore.d.ts +14 -11
  196. package/dist/stores/LambderMemorySessionStore.js +38 -19
  197. package/dist/stores/LambderS3FileSource.d.ts +21 -6
  198. package/dist/stores/LambderS3FileSource.js +12 -7
  199. package/dist/testing/LambderTestApp.d.ts +176 -0
  200. package/dist/testing/LambderTestApp.js +204 -0
  201. package/dist/testing/LambderTestVisitor.d.ts +153 -0
  202. package/dist/testing/LambderTestVisitor.js +154 -0
  203. package/dist/testing.d.ts +27 -0
  204. package/dist/testing.js +24 -0
  205. package/package.json +20 -3
  206. package/dist/api/LambderApiPolicyEngine.d.ts +0 -36
  207. package/dist/api/LambderApiPolicyEngine.js +0 -77
  208. package/dist/shared/util/LambderKeyFields.d.ts +0 -32
  209. 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 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.
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 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.
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
- * A named guard, run before the API's own input validation. Three input
46
- * modes:
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 stays the owner of those fields:
51
- * declaring the guard on an API whose schema does not carry them is a
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
- * 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.
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: 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
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, 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.
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 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.
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
- /** Per-guard metadata carried on the Lambder instance: input mode, session requirement, param type, output type. */
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.output<S>;
223
+ apiInput: z.input<S>;
204
224
  } : G extends {
205
225
  guardInput: infer S extends z.ZodType;
206
226
  } ? {
207
- guardInput: z.output<S>;
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. Composed into LambderApiPolicyEngine. Reads the
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 in declared order. Refusals throw; outputs land on
302
- * ctx.guardData. Each guard is recorded on the trace as it returns, so a
303
- * call that a later guard refused still reports the ones that passed.
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 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.
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. Composed into LambderApiPolicyEngine. Reads the
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 that exists to catch
42
- // exactly that typo, and then fail on every request. It also refuses to
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, 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.
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 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.
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 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.
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 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.
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
- // 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.
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
- * Session APIs already scope per session, so this only affects the ones
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
- * 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.
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
- * 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.
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
- * Composed into LambderApiPolicyEngine. Works on plain answers, so it runs
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
- * 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.
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
- private readonly scopeByCall;
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 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.
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
- * Fields are escaped and joined through joinKeyFields, so no two distinct
90
- * scopes can produce one string.
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
- * 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.
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, request: LambderApiRequest, ctx: LambderApiCallContext, trace: LambderApiCallTrace): Promise<LambderApiAnswer | null>;
119
+ findReplay(apiName: string, call: LambderIdempotentCall, trace: LambderApiCallTrace): Promise<LambderApiAnswer | null>;
105
120
  /**
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.
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` 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.
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 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.
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, request: LambderApiRequest, ctx: LambderApiCallContext, config: LambderApiIdempotencyOption, trace: LambderApiCallTrace, exec: () => Promise<LambderApiAnswer>): Promise<LambderApiAnswer>;
134
+ withIdempotency(apiName: string, { scopeKey, fingerprint }: LambderIdempotentCall, config: LambderApiIdempotencyOption, trace: LambderApiCallTrace, exec: () => Promise<LambderApiAnswer>): Promise<LambderApiAnswer>;
122
135
  }