lambder 6.0.2 → 7.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (195) hide show
  1. package/CHANGELOG.md +2316 -0
  2. package/README.md +60 -33
  3. package/dist/api/LambderApiAnswer.d.ts +40 -0
  4. package/dist/api/LambderApiAnswer.js +19 -0
  5. package/dist/api/LambderApiCallContext.d.ts +38 -0
  6. package/dist/api/LambderApiCallContext.js +13 -0
  7. package/dist/api/LambderApiDefinition.d.ts +18 -0
  8. package/dist/api/LambderApiDefinition.js +1 -0
  9. package/dist/api/LambderApiEnvelope.d.ts +67 -0
  10. package/dist/api/LambderApiEnvelope.js +180 -0
  11. package/dist/api/LambderApiGuards.d.ts +302 -0
  12. package/dist/api/LambderApiGuards.js +134 -0
  13. package/dist/api/LambderApiIdempotency.d.ts +122 -0
  14. package/dist/api/LambderApiIdempotency.js +330 -0
  15. package/dist/api/LambderApiPipeline.d.ts +134 -0
  16. package/dist/api/LambderApiPipeline.js +221 -0
  17. package/dist/api/LambderApiPolicyEngine.d.ts +36 -0
  18. package/dist/api/LambderApiPolicyEngine.js +77 -0
  19. package/dist/api/LambderApiRateLimits.d.ts +206 -0
  20. package/dist/api/LambderApiRateLimits.js +239 -0
  21. package/dist/api/LambderApiRequest.d.ts +101 -0
  22. package/dist/api/LambderApiRequest.js +129 -0
  23. package/dist/api/LambderApiValidationRefusal.d.ts +32 -0
  24. package/dist/api/LambderApiValidationRefusal.js +40 -0
  25. package/dist/client/LambderCaller.d.ts +62 -55
  26. package/dist/client/LambderCaller.js +147 -90
  27. package/dist/client/lambderFetchTransport.d.ts +9 -0
  28. package/dist/client/lambderFetchTransport.js +71 -0
  29. package/dist/client.d.ts +20 -10
  30. package/dist/client.js +11 -5
  31. package/dist/core/Lambder.d.ts +117 -253
  32. package/dist/core/Lambder.js +374 -341
  33. package/dist/core/LambderContext.d.ts +54 -44
  34. package/dist/core/LambderContext.js +41 -110
  35. package/dist/core/LambderCreateOptions.d.ts +285 -0
  36. package/dist/core/LambderCreateOptions.js +44 -0
  37. package/dist/core/LambderFiles.d.ts +1 -45
  38. package/dist/core/LambderFiles.js +18 -38
  39. package/dist/core/LambderIndexHtml.d.ts +37 -0
  40. package/dist/core/LambderIndexHtml.js +87 -0
  41. package/dist/core/LambderPolicyBuilders.d.ts +17 -0
  42. package/dist/core/LambderPolicyBuilders.js +16 -0
  43. package/dist/core/LambderPublicFiles.d.ts +5 -2
  44. package/dist/core/LambderPublicFiles.js +7 -2
  45. package/dist/core/LambderResolver.d.ts +8 -6
  46. package/dist/core/LambderResponse.d.ts +29 -11
  47. package/dist/core/LambderResponse.js +96 -49
  48. package/dist/core/LambderResponseBuilder.d.ts +18 -14
  49. package/dist/core/LambderResponseBuilder.js +19 -25
  50. package/dist/core/LambderRouting.d.ts +18 -7
  51. package/dist/core/LambderRouting.js +17 -7
  52. package/dist/core/LambderTemplatingEngine.d.ts +0 -62
  53. package/dist/core/LambderTemplatingEngine.js +7 -3
  54. package/dist/index.d.ts +85 -32
  55. package/dist/index.js +44 -16
  56. package/dist/invoke/LambderInvokeCaller.d.ts +46 -139
  57. package/dist/invoke/LambderInvokeCaller.js +140 -335
  58. package/dist/invoke/LambderInvokeOutcome.d.ts +165 -0
  59. package/dist/invoke/LambderInvokeOutcome.js +129 -0
  60. package/dist/invoke/LambderLambdaEvent.d.ts +81 -0
  61. package/dist/invoke/LambderLambdaEvent.js +187 -0
  62. package/dist/invoke/lambderHandlerTransport.d.ts +36 -0
  63. package/dist/invoke/lambderHandlerTransport.js +89 -0
  64. package/dist/mock/LambderMockApp.d.ts +352 -0
  65. package/dist/mock/LambderMockApp.js +815 -0
  66. package/dist/mock/LambderMockBrowserCookies.d.ts +55 -0
  67. package/dist/mock/LambderMockBrowserCookies.js +76 -0
  68. package/dist/mock/LambderMockCallRecorder.d.ts +85 -0
  69. package/dist/mock/LambderMockCallRecorder.js +183 -0
  70. package/dist/mock/LambderMockCreateOptions.d.ts +161 -0
  71. package/dist/mock/LambderMockCreateOptions.js +9 -0
  72. package/dist/mock/LambderMockEntryRegistry.d.ts +52 -0
  73. package/dist/mock/LambderMockEntryRegistry.js +126 -0
  74. package/dist/mock/LambderMockFailureInjector.d.ts +60 -0
  75. package/dist/mock/LambderMockFailureInjector.js +138 -0
  76. package/dist/mock/LambderMockTypes.d.ts +421 -0
  77. package/dist/mock/LambderMockTypes.js +8 -0
  78. package/dist/mock/lambderMockConsoleLogger.d.ts +16 -0
  79. package/dist/mock/lambderMockConsoleLogger.js +35 -0
  80. package/dist/mock/lambderMockInvokeTransport.d.ts +50 -0
  81. package/dist/mock/lambderMockInvokeTransport.js +52 -0
  82. package/dist/mock/lambderMockMswHandler.d.ts +99 -0
  83. package/dist/mock/lambderMockMswHandler.js +126 -0
  84. package/dist/mock.d.ts +34 -0
  85. package/dist/mock.js +27 -0
  86. package/dist/session/LambderSessionController.d.ts +199 -30
  87. package/dist/session/LambderSessionController.js +396 -82
  88. package/dist/session/LambderSessionCrypto.d.ts +66 -0
  89. package/dist/session/LambderSessionCrypto.js +101 -0
  90. package/dist/session/LambderSessionManager.d.ts +118 -80
  91. package/dist/session/LambderSessionManager.js +212 -184
  92. package/dist/shared/LambderI18n.d.ts +6 -6
  93. package/dist/shared/LambderI18n.js +1 -1
  94. package/dist/shared/contracts/LambderFileSource.d.ts +33 -0
  95. package/dist/shared/contracts/LambderFileSource.js +19 -0
  96. package/dist/shared/contracts/LambderIdempotencyStore.d.ts +66 -0
  97. package/dist/shared/contracts/LambderIdempotencyStore.js +12 -0
  98. package/dist/shared/contracts/LambderRateLimiter.d.ts +71 -0
  99. package/dist/shared/contracts/LambderRateLimiter.js +24 -0
  100. package/dist/shared/contracts/LambderSessionStore.d.ts +72 -0
  101. package/dist/shared/contracts/LambderSessionStore.js +13 -0
  102. package/dist/shared/transport/LambderApiTransport.d.ts +139 -0
  103. package/dist/shared/transport/LambderApiTransport.js +65 -0
  104. package/dist/shared/transport/LambderCookieJar.d.ts +121 -0
  105. package/dist/shared/transport/LambderCookieJar.js +246 -0
  106. package/dist/shared/transport/lambderCookieJarTransport.d.ts +30 -0
  107. package/dist/shared/transport/lambderCookieJarTransport.js +60 -0
  108. package/dist/shared/util/LambderBase64.d.ts +10 -0
  109. package/dist/shared/util/LambderBase64.js +27 -0
  110. package/dist/shared/util/LambderCallAbort.d.ts +62 -0
  111. package/dist/shared/util/LambderCallAbort.js +80 -0
  112. package/dist/shared/util/LambderClientIp.d.ts +32 -0
  113. package/dist/shared/util/LambderClientIp.js +56 -0
  114. package/dist/shared/util/LambderExpiringMap.d.ts +119 -0
  115. package/dist/shared/util/LambderExpiringMap.js +217 -0
  116. package/dist/shared/util/LambderKeyFields.d.ts +32 -0
  117. package/dist/shared/util/LambderKeyFields.js +34 -0
  118. package/dist/shared/util/LambderNodeModules.d.ts +9 -0
  119. package/dist/shared/util/LambderNodeModules.js +39 -0
  120. package/dist/shared/util/LambderOptionChecks.d.ts +17 -0
  121. package/dist/shared/util/LambderOptionChecks.js +33 -0
  122. package/dist/shared/util/LambderResponseBrand.d.ts +20 -0
  123. package/dist/shared/util/LambderResponseBrand.js +18 -0
  124. package/dist/shared/util/LambderTextDigest.d.ts +17 -0
  125. package/dist/shared/util/LambderTextDigest.js +34 -0
  126. package/dist/shared/util/LambderTypeUtilities.d.ts +33 -0
  127. package/dist/shared/util/LambderTypeUtilities.js +8 -0
  128. package/dist/shared/wire/LambderAnswerHeaders.d.ts +60 -0
  129. package/dist/shared/wire/LambderAnswerHeaders.js +94 -0
  130. package/dist/shared/wire/LambderApiContract.d.ts +129 -0
  131. package/dist/shared/wire/LambderApiOptionValues.d.ts +39 -0
  132. package/dist/shared/wire/LambderApiOptionValues.js +11 -0
  133. package/dist/shared/wire/LambderApiOutcome.d.ts +128 -0
  134. package/dist/shared/{LambderApiOutcome.js → wire/LambderApiOutcome.js} +16 -9
  135. package/dist/shared/{LambderApiError.d.ts → wire/LambderApiRefusal.d.ts} +48 -26
  136. package/dist/shared/{LambderApiError.js → wire/LambderApiRefusal.js} +13 -11
  137. package/dist/shared/wire/LambderCallOptions.d.ts +171 -0
  138. package/dist/shared/wire/LambderCallOptions.js +17 -0
  139. package/dist/shared/{LambderCompressionCodec.d.ts → wire/LambderCompressionCodec.d.ts} +10 -6
  140. package/dist/shared/{LambderCompressionCodec.js → wire/LambderCompressionCodec.js} +67 -23
  141. package/dist/shared/{LambderCompressionOption.d.ts → wire/LambderCompressionOption.d.ts} +1 -1
  142. package/dist/shared/{LambderCompressionOption.js → wire/LambderCompressionOption.js} +3 -4
  143. package/dist/shared/{LambderCrashDetail.d.ts → wire/LambderCrashDetail.d.ts} +10 -0
  144. package/dist/shared/{LambderCrashDetail.js → wire/LambderCrashDetail.js} +30 -0
  145. package/dist/shared/wire/LambderHttpStatus.d.ts +12 -0
  146. package/dist/shared/wire/LambderHttpStatus.js +1 -0
  147. package/dist/shared/{LambderRequestPayload.d.ts → wire/LambderRequestPayload.d.ts} +25 -17
  148. package/dist/shared/{LambderRequestPayload.js → wire/LambderRequestPayload.js} +29 -52
  149. package/dist/shared/wire/LambderSessionCookieNames.d.ts +9 -0
  150. package/dist/shared/wire/LambderSessionCookieNames.js +9 -0
  151. package/dist/stores/LambderDdbCache.d.ts +12 -9
  152. package/dist/stores/LambderDdbCache.js +56 -47
  153. package/dist/stores/{LambderDdbIdempotency.d.ts → LambderDdbIdempotencyStore.d.ts} +41 -31
  154. package/dist/stores/LambderDdbIdempotencyStore.js +319 -0
  155. package/dist/stores/LambderDdbRateLimiter.d.ts +30 -49
  156. package/dist/stores/LambderDdbRateLimiter.js +47 -45
  157. package/dist/stores/LambderDdbSdk.d.ts +83 -6
  158. package/dist/stores/LambderDdbSdk.js +83 -2
  159. package/dist/stores/LambderDdbSessionStore.d.ts +65 -0
  160. package/dist/stores/LambderDdbSessionStore.js +161 -0
  161. package/dist/stores/LambderHttpFileSource.d.ts +1 -1
  162. package/dist/stores/LambderHttpFileSource.js +10 -1
  163. package/dist/stores/LambderLocalFileSource.d.ts +15 -0
  164. package/dist/stores/LambderLocalFileSource.js +28 -0
  165. package/dist/stores/LambderMemoryIdempotencyStore.d.ts +63 -0
  166. package/dist/stores/LambderMemoryIdempotencyStore.js +113 -0
  167. package/dist/stores/LambderMemoryRateLimiter.d.ts +34 -0
  168. package/dist/stores/LambderMemoryRateLimiter.js +64 -0
  169. package/dist/stores/LambderMemorySessionStore.d.ts +48 -0
  170. package/dist/stores/LambderMemorySessionStore.js +74 -0
  171. package/dist/stores/LambderS3FileSource.d.ts +1 -1
  172. package/dist/stores/LambderS3FileSource.js +1 -1
  173. package/package.json +21 -19
  174. package/dist/client/LambderMSW.d.ts +0 -69
  175. package/dist/client/LambderMSW.js +0 -121
  176. package/dist/policies/LambderApiGuards.d.ts +0 -256
  177. package/dist/policies/LambderApiGuards.js +0 -94
  178. package/dist/policies/LambderApiIdempotency.d.ts +0 -58
  179. package/dist/policies/LambderApiIdempotency.js +0 -219
  180. package/dist/policies/LambderApiPolicies.d.ts +0 -42
  181. package/dist/policies/LambderApiPolicies.js +0 -52
  182. package/dist/policies/LambderApiRateLimits.d.ts +0 -132
  183. package/dist/policies/LambderApiRateLimits.js +0 -119
  184. package/dist/shared/LambderApiContract.d.ts +0 -57
  185. package/dist/shared/LambderApiOutcome.d.ts +0 -69
  186. package/dist/shared/LambderCallOptions.d.ts +0 -71
  187. package/dist/shared/LambderCallOptions.js +0 -16
  188. package/dist/shared/node-polyfills.d.ts +0 -4
  189. package/dist/shared/node-polyfills.js +0 -58
  190. package/dist/stores/LambderDdbIdempotency.js +0 -229
  191. package/dist/testing.d.ts +0 -9
  192. package/dist/testing.js +0 -8
  193. /package/dist/shared/{LambderApiContract.js → wire/LambderApiContract.js} +0 -0
  194. /package/dist/{core → shared/wire}/LambderCookie.d.ts +0 -0
  195. /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
+ }