lambder 6.0.2 → 7.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 (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,221 @@
1
+ import { restoreCompressedPayload } from "./LambderApiRequest.js";
2
+ import { apiNotFoundAnswer, invalidPayloadAnswer, refusalAnswer, sessionExpiredAnswer, validationAnswer, versionExpiredAnswer, } from "./LambderApiEnvelope.js";
3
+ import { LambderApiValidationRefusal, isLambderApiValidationRefusal } from "./LambderApiValidationRefusal.js";
4
+ import { isLambderApiRefusal } from "../shared/wire/LambderApiRefusal.js";
5
+ import { DEFAULT_MAX_RESTORED_PAYLOAD_BYTES } from "../shared/wire/LambderRequestPayload.js";
6
+ import { assertPositiveInteger } from "../shared/util/LambderOptionChecks.js";
7
+ import { LambderApiPolicyEngine } from "./LambderApiPolicyEngine.js";
8
+ import LambderSessionController, { assertSessionCookiePrefixes, } from "../session/LambderSessionController.js";
9
+ import { DEFAULT_SESSION_CSRF_COOKIE_KEY, DEFAULT_SESSION_TOKEN_COOKIE_KEY } from "../shared/wire/LambderSessionCookieNames.js";
10
+ /**
11
+ * The API pipeline: one API call from a parsed request to a plain answer,
12
+ * in the order the protocol defines. The Lambda server and the mock runtime
13
+ * are adapters over this class; neither reimplements a step of it.
14
+ *
15
+ * ```
16
+ * version gate → restore payload → rate limits that need no session
17
+ * → session (session mode) → idempotency replay → the remaining rate limits
18
+ * → guards → input validation → exec, inside the idempotency claim
19
+ * → drain response headers → answer
20
+ * ```
21
+ *
22
+ * Steps whose subsystem is not configured are skipped. A LambderApiRefusal
23
+ * thrown by any step, guard or handler is rendered here, in one place: a
24
+ * validation error through onInvalidInput, any other refusal as the refusal
25
+ * envelope. Anything else propagates, because only the adapter knows what a
26
+ * crash means (a global error handler, a mock event).
27
+ *
28
+ * `run` never sees a name it has no definition for; resolving a name to a
29
+ * definition is the one thing the adapters legitimately do differently (an
30
+ * action list versus a registry), and answerUnknownApi is what they answer
31
+ * with.
32
+ */
33
+ export class LambderApiPipeline {
34
+ apiVersion;
35
+ policies = new LambderApiPolicyEngine();
36
+ maxRequestPayloadBytes;
37
+ onInvalidInput;
38
+ sessions;
39
+ constructor(options = {}) {
40
+ this.apiVersion = options.apiVersion ?? null;
41
+ this.maxRequestPayloadBytes = assertPositiveInteger(options.maxRequestPayloadBytes ?? DEFAULT_MAX_RESTORED_PAYLOAD_BYTES, "maxRequestPayloadBytes");
42
+ this.onInvalidInput = options.onInvalidInput ?? null;
43
+ this.sessions = options.sessions
44
+ ? {
45
+ manager: options.sessions.manager,
46
+ tokenCookieKey: options.sessions.tokenCookieKey ?? DEFAULT_SESSION_TOKEN_COOKIE_KEY,
47
+ csrfCookieKey: options.sessions.csrfCookieKey ?? DEFAULT_SESSION_CSRF_COOKIE_KEY,
48
+ cookieOptions: options.sessions.cookieOptions ?? {},
49
+ }
50
+ : null;
51
+ if (this.sessions)
52
+ assertSessionCookiePrefixes(this.sessions);
53
+ if (options.rateLimits)
54
+ this.policies.configureRateLimits(options.rateLimits);
55
+ if (options.guards)
56
+ this.policies.configureGuards(options.guards);
57
+ if (options.idempotency)
58
+ this.policies.configureIdempotency(options.idempotency);
59
+ }
60
+ /** True when a session manager was configured. */
61
+ get hasSessions() { return this.sessions !== null; }
62
+ /** The session manager, for adapters that hand it out; throws when sessions are not configured. */
63
+ get sessionManager() {
64
+ if (!this.sessions)
65
+ throw new Error("Session is not enabled. Configure the session option at creation.");
66
+ return this.sessions.manager;
67
+ }
68
+ /**
69
+ * A session controller for one request: what handlers use to create,
70
+ * rotate, refresh and end sessions. The request info is the API request's
71
+ * (its cookies and posted CSRF token) or a route's (cookies and no CSRF).
72
+ */
73
+ sessionController(ctx, request) {
74
+ if (!this.sessions)
75
+ throw new Error("Session is not enabled. Configure the session option at creation.");
76
+ return new LambderSessionController({
77
+ manager: this.sessions.manager,
78
+ tokenCookieKey: this.sessions.tokenCookieKey,
79
+ csrfCookieKey: this.sessions.csrfCookieKey,
80
+ cookieOptions: this.sessions.cookieOptions,
81
+ ctx,
82
+ request,
83
+ });
84
+ }
85
+ /** The session request info of an API request: its cookies, and the CSRF token it posted. */
86
+ static sessionInfoOf(request) {
87
+ return { host: request.host, cookies: request.cookies, csrfToken: request.token };
88
+ }
89
+ /** Registration-time checks of one definition's declarative options; the same messages on the server and in the mock. */
90
+ assertRegistration(definition) {
91
+ this.policies.assertRegistration(definition);
92
+ }
93
+ /** True when the gate is on and the request names a different version. */
94
+ isVersionStale(request) {
95
+ return !!this.apiVersion && !!request.version && request.version !== this.apiVersion;
96
+ }
97
+ /**
98
+ * The answer for a request naming no registered API: the apiNotFound
99
+ * refusal, carrying whatever the call already wrote (a CORS header, a
100
+ * cookie eviction). No version gate here: both adapters run prepare() on
101
+ * the way in, before a name is resolved, so a stale client has already
102
+ * been answered by the time anything asks for an unknown name.
103
+ */
104
+ answerUnknownApi(request, ctx) {
105
+ const answer = apiNotFoundAnswer(this.apiVersion, ctx?.logList);
106
+ ctx?.responseHeaders.applyInto(answer.headers);
107
+ return answer;
108
+ }
109
+ /**
110
+ * The steps that come before anything may read the request: the version
111
+ * gate, then the compressed-payload restore that every later reader (a
112
+ * rate-limit key slice, a guard, the input schema) depends on having
113
+ * happened.
114
+ *
115
+ * Public and named because the server runs them earlier than run() does,
116
+ * on the way in, so that its hooks see a plain payload and a stale client
117
+ * is answered before any of them, whether or not the name it asked for
118
+ * exists. run() calls it too, so an adapter that has no such step still
119
+ * gets the whole protocol. Calling it twice is safe by construction: the
120
+ * gate is a pure comparison and the restore has already removed the wire
121
+ * fields it reads.
122
+ *
123
+ * Returns the answer that ends the call, or null when the request is
124
+ * ready to dispatch.
125
+ */
126
+ async prepare(request) {
127
+ if (this.isVersionStale(request))
128
+ return versionExpiredAnswer(this.apiVersion);
129
+ const restored = await restoreCompressedPayload(request, this.maxRequestPayloadBytes);
130
+ if (!restored.ok)
131
+ return invalidPayloadAnswer(this.apiVersion, restored.message);
132
+ return null;
133
+ }
134
+ /**
135
+ * One call, one answer. Refusals are rendered; crashes propagate.
136
+ *
137
+ * An adapter that wants to report what the call did even when it crashed
138
+ * passes its own trace object: the pipeline writes into that one, so a
139
+ * handler that threw still leaves the guards it ran behind for the
140
+ * adapter's catch. Without it the trace was created here and lost with
141
+ * the throw, and the mock's call log showed no guards on exactly the
142
+ * calls a developer opens the log for.
143
+ */
144
+ async run(request, ctx, definition, exec, trace = { guardsRun: [], replayed: false }) {
145
+ let answer;
146
+ try {
147
+ answer = await this.execute(request, ctx, definition, exec, trace);
148
+ }
149
+ catch (err) {
150
+ if (isLambderApiValidationRefusal(err)) {
151
+ answer = await this.refuseInput(err, ctx, request);
152
+ }
153
+ else if (isLambderApiRefusal(err)) {
154
+ answer = refusalAnswer(err, this.apiVersion, ctx.logList);
155
+ }
156
+ else {
157
+ throw err;
158
+ }
159
+ }
160
+ // Every header written during the call belongs on the answer, whichever
161
+ // way it was produced: a cookie eviction from the session read, a
162
+ // handler's setHeader before it refused, the handler's own headers
163
+ // (already on it, so re-applying them here changes nothing).
164
+ ctx.responseHeaders.applyInto(answer.headers);
165
+ return { answer, ...trace };
166
+ }
167
+ async execute(request, ctx, definition, exec, trace) {
168
+ const unprepared = await this.prepare(request);
169
+ if (unprepared)
170
+ return unprepared;
171
+ // The limits whose key is known from the request alone, before the
172
+ // session store is asked anything: a request carrying bogus session
173
+ // cookies costs up to four store reads, and answering it
174
+ // sessionExpired without the limiter having run let one address spend
175
+ // the session store's read budget freely. A replay costs the same
176
+ // reads, so an ip-limited replay counts against that budget too: the
177
+ // limit protects the stores, not the handler.
178
+ await this.policies.runSessionlessRateLimits(request, ctx, definition);
179
+ if (definition.mode === "session") {
180
+ if (!this.sessions)
181
+ throw new Error(`Lambder: API "${definition.name}" is a session API, but no session store was configured at creation.`);
182
+ const session = await this.sessionController(ctx, LambderApiPipeline.sessionInfoOf(request)).fetchSessionIfExists();
183
+ if (!session)
184
+ return sessionExpiredAnswer(this.apiVersion, ctx.logList);
185
+ }
186
+ // Replay fast path: a completed idempotent request answers its stored
187
+ // answer without burning the remaining rate-limit quota or re-running
188
+ // guards. After the session read, because the replay scope is keyed
189
+ // per session.
190
+ const replay = await this.policies.findReplay(request, ctx, definition, trace);
191
+ if (replay)
192
+ return replay;
193
+ await this.policies.runPreflight(request, ctx, definition, trace);
194
+ if (definition.input) {
195
+ const parsed = definition.input.safeParse(request.payload);
196
+ if (!parsed.success)
197
+ throw new LambderApiValidationRefusal(parsed.error);
198
+ request.payload = parsed.data;
199
+ }
200
+ // The handler's own answer, and only that: what it wrote into
201
+ // responseHeaders during the call is on it before the idempotency
202
+ // engine judges and stores it, while a header written EARLIER in the
203
+ // call is not. That line matters, because the engine refuses to store
204
+ // an answer carrying a Set-Cookie: charge it with the stale-session
205
+ // cookie the session read evicted and an otherwise idempotent
206
+ // operation would silently stop being idempotent and re-execute on
207
+ // every retry. The call's earlier headers still reach the client;
208
+ // run() applies them to the answer on the way out.
209
+ const runHandler = async () => {
210
+ const handlerFirstHeader = ctx.responseHeaders.size;
211
+ const produced = await exec(ctx);
212
+ ctx.responseHeaders.applyInto(produced.headers, handlerFirstHeader);
213
+ return produced;
214
+ };
215
+ return await this.policies.withIdempotency(request, ctx, definition, trace, runHandler);
216
+ }
217
+ async refuseInput(err, ctx, request) {
218
+ const custom = this.onInvalidInput ? await this.onInvalidInput(err.zodError, ctx, request) : null;
219
+ return custom ?? validationAnswer(err.zodError, ctx.logList);
220
+ }
221
+ }
@@ -0,0 +1,36 @@
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 { LambderApiDefinition } from "./LambderApiDefinition.js";
5
+ import { type LambderApiGuard } from "./LambderApiGuards.js";
6
+ import { type LambderApiRateLimitPolicyConfig, type LambderApiRateLimitsConfig } from "./LambderApiRateLimits.js";
7
+ import { type LambderApiIdempotencyConfig } from "./LambderApiIdempotency.js";
8
+ /**
9
+ * Runtime side of the declarative API options: composes the three policy
10
+ * subsystems (rate limits in ./LambderApiRateLimits.ts, guards in
11
+ * ./LambderApiGuards.ts, idempotency in ./LambderApiIdempotency.ts), asserts
12
+ * registrations against them at startup, and executes them around handlers
13
+ * at request time. Owned by LambderApiPipeline; apps interact through the
14
+ * create() options (rateLimits, guards, idempotency) and the per-API
15
+ * options.
16
+ */
17
+ export declare class LambderApiPolicyEngine {
18
+ private rateLimits;
19
+ private guards;
20
+ private idempotency;
21
+ /** True once any of the three subsystems was configured. */
22
+ get isConfigured(): boolean;
23
+ configureRateLimits(config: LambderApiRateLimitsConfig<Record<string, LambderApiRateLimitPolicyConfig>>): void;
24
+ configureGuards(guards: Record<string, LambderApiGuard<any, any, any>>): void;
25
+ configureIdempotency(config: LambderApiIdempotencyConfig): void;
26
+ /** Startup validation of one API's declarative options. */
27
+ assertRegistration(definition: LambderApiDefinition): void;
28
+ /** The rate-limit policies that can be checked before the session is read: see LambderApiRateLimitsEngine.run. */
29
+ runSessionlessRateLimits(request: LambderApiRequest, ctx: LambderApiCallContext, definition: LambderApiDefinition): Promise<void>;
30
+ /** The remaining rate limits, then guards, in declared order. Refusals throw; the trace records each guard as it runs. */
31
+ runPreflight(request: LambderApiRequest, ctx: LambderApiCallContext, definition: LambderApiDefinition, trace: LambderApiCallTrace): Promise<void>;
32
+ /** Idempotency replay fast path, run before the preflight: see LambderApiIdempotencyEngine.findReplay. */
33
+ findReplay(request: LambderApiRequest, ctx: LambderApiCallContext, definition: LambderApiDefinition, trace: LambderApiCallTrace): Promise<LambderApiAnswer | null>;
34
+ /** Idempotency claim/replay wrapper around handler execution: see LambderApiIdempotencyEngine.withIdempotency. */
35
+ withIdempotency(request: LambderApiRequest, ctx: LambderApiCallContext, definition: LambderApiDefinition, trace: LambderApiCallTrace, exec: () => Promise<LambderApiAnswer>): Promise<LambderApiAnswer>;
36
+ }
@@ -0,0 +1,77 @@
1
+ import { LambderApiGuardsEngine } from "./LambderApiGuards.js";
2
+ import { LambderApiRateLimitsEngine } from "./LambderApiRateLimits.js";
3
+ import { LambderApiIdempotencyEngine } from "./LambderApiIdempotency.js";
4
+ /** An API that asks for idempotency: declared, and not the explicit `false` opt-out. */
5
+ const usesIdempotency = (definition) => definition.idempotency !== undefined && definition.idempotency !== false;
6
+ /**
7
+ * Runtime side of the declarative API options: composes the three policy
8
+ * subsystems (rate limits in ./LambderApiRateLimits.ts, guards in
9
+ * ./LambderApiGuards.ts, idempotency in ./LambderApiIdempotency.ts), asserts
10
+ * registrations against them at startup, and executes them around handlers
11
+ * at request time. Owned by LambderApiPipeline; apps interact through the
12
+ * create() options (rateLimits, guards, idempotency) and the per-API
13
+ * options.
14
+ */
15
+ export class LambderApiPolicyEngine {
16
+ rateLimits = new LambderApiRateLimitsEngine();
17
+ guards = new LambderApiGuardsEngine();
18
+ idempotency = new LambderApiIdempotencyEngine();
19
+ /** True once any of the three subsystems was configured. */
20
+ get isConfigured() {
21
+ return this.rateLimits.isConfigured || this.guards.isConfigured || this.idempotency.isConfigured;
22
+ }
23
+ configureRateLimits(config) {
24
+ this.rateLimits.configure(config);
25
+ }
26
+ configureGuards(guards) {
27
+ this.guards.configure(guards);
28
+ }
29
+ configureIdempotency(config) {
30
+ this.idempotency.configure(config);
31
+ }
32
+ /** Startup validation of one API's declarative options. */
33
+ assertRegistration(definition) {
34
+ const { name, mode } = definition;
35
+ // Each subsystem reports its own absence. One combined message would
36
+ // tell an API that declares guards on an instance with no guards map
37
+ // that none of the three was configured, which reads as a question
38
+ // about all three when only one of them is missing.
39
+ if (definition.rateLimit !== undefined && !this.rateLimits.isConfigured) {
40
+ throw new Error(`Lambder: API "${name}" declares rateLimit but no rateLimits option was configured at creation.`);
41
+ }
42
+ if (definition.guards !== undefined && !this.guards.isConfigured) {
43
+ throw new Error(`Lambder: API "${name}" declares guards but no guards option was configured at creation.`);
44
+ }
45
+ this.rateLimits.assertRegistration(name, mode, definition.rateLimit);
46
+ this.guards.assertRegistration(name, mode, definition.guards);
47
+ // `idempotency: false` is an explicit opt-out, not a use: it asks for
48
+ // nothing and so needs no store behind it.
49
+ if (usesIdempotency(definition)) {
50
+ if (!this.idempotency.isConfigured) {
51
+ throw new Error(`Lambder: API "${name}" declares idempotency but no idempotency store was configured at creation.`);
52
+ }
53
+ this.idempotency.assertRegistration(name, definition.idempotency);
54
+ }
55
+ }
56
+ /** The rate-limit policies that can be checked before the session is read: see LambderApiRateLimitsEngine.run. */
57
+ async runSessionlessRateLimits(request, ctx, definition) {
58
+ await this.rateLimits.run(definition.name, request, ctx, definition.rateLimit, "beforeSession");
59
+ }
60
+ /** The remaining rate limits, then guards, in declared order. Refusals throw; the trace records each guard as it runs. */
61
+ async runPreflight(request, ctx, definition, trace) {
62
+ await this.rateLimits.run(definition.name, request, ctx, definition.rateLimit, "afterSession");
63
+ await this.guards.run(request, ctx, definition.guards, trace);
64
+ }
65
+ /** Idempotency replay fast path, run before the preflight: see LambderApiIdempotencyEngine.findReplay. */
66
+ async findReplay(request, ctx, definition, trace) {
67
+ if (!definition.idempotency)
68
+ return null;
69
+ return await this.idempotency.findReplay(definition.name, request, ctx, trace);
70
+ }
71
+ /** Idempotency claim/replay wrapper around handler execution: see LambderApiIdempotencyEngine.withIdempotency. */
72
+ async withIdempotency(request, ctx, definition, trace, exec) {
73
+ if (!definition.idempotency)
74
+ return await exec();
75
+ return await this.idempotency.withIdempotency(definition.name, request, ctx, definition.idempotency, trace, exec);
76
+ }
77
+ }
@@ -0,0 +1,206 @@
1
+ import type { LambderApiMode } from "../shared/wire/LambderApiContract.js";
2
+ import type { LambderRateLimitOptionValue, LambderRateLimitOverride } from "../shared/wire/LambderApiOptionValues.js";
3
+ import type { z } from "zod";
4
+ import type { LambderApiRequest } from "./LambderApiRequest.js";
5
+ import type { LambderApiCallContext } from "./LambderApiCallContext.js";
6
+ import { type LambderRateLimiter, type LambderRateLimitPolicy } from "../shared/contracts/LambderRateLimiter.js";
7
+ import { LambderApiRefusal, type LambderAppRefusalMessage } from "../shared/wire/LambderApiRefusal.js";
8
+ import type { LambderNonEmptyOptionMap } from "../shared/util/LambderTypeUtilities.js";
9
+ /** Refusal a rate-limited request answers unless the policy or the API's override names its own. */
10
+ export declare const DEFAULT_RATE_LIMIT_REFUSAL: {
11
+ type: "warning";
12
+ code: "lambder/rate-limited";
13
+ content: string;
14
+ };
15
+ /**
16
+ * The refusal a rate-limited call answers with: a 429 envelope carrying the
17
+ * framework code (a policy's own message inherits it unless it sets a more
18
+ * specific one) and a Retry-After header. The engine throws it; the mock
19
+ * runtime's failure injection throws the same one, so an injected rate
20
+ * limit is indistinguishable from a real one.
21
+ */
22
+ export declare const rateLimitRefusal: (detail: string, retryAfterSeconds: number, message?: LambderAppRefusalMessage) => LambderApiRefusal;
23
+ /**
24
+ * A custom rate-limit key. `apiInput` names the fields of the API's OWN
25
+ * payload the key derives from: the slice is validated against the raw
26
+ * payload before `handler` runs (failures answer like regular input
27
+ * validation, through setApiInputValidationErrorHandler when set) and the
28
+ * handler receives it typed. Referencing the policy from an API whose input
29
+ * schema does not carry those fields is a compile error, so the API's schema
30
+ * stays the single owner of the field. Build with lambderRateLimitKey() so
31
+ * the handler's payload type follows `apiInput`. The context is the
32
+ * adapter's (the render context on the server); the engine reads nothing
33
+ * from it itself.
34
+ *
35
+ * ONE member, with `apiInput` optional, rather than a union of the two
36
+ * shapes: a union with a function member in each arm defeats contextual
37
+ * typing, so annotating a policies map with LambderRateLimitPer or
38
+ * LambderApiRateLimitPolicyConfig left `ctx` implicitly any and the
39
+ * annotation did not compile at all. The builder's overloads are where the
40
+ * apiInput/payload correlation is kept.
41
+ */
42
+ export type LambderRateLimitKeyFn<TInput extends z.ZodType = z.ZodType, TCtx = any> = {
43
+ apiInput?: TInput;
44
+ handler: (ctx: TCtx, payload: z.output<TInput>) => string | Promise<string>;
45
+ };
46
+ /**
47
+ * Builder that ties the handler's payload type to the `apiInput` schema
48
+ * inside one literal. Returns the exact union member so type extraction can
49
+ * see the schema.
50
+ */
51
+ export type LambderRateLimitKeyBuilder<TCtx> = {
52
+ <TInput extends z.ZodType>(key: {
53
+ apiInput: TInput;
54
+ handler: (ctx: TCtx, payload: z.output<TInput>) => string | Promise<string>;
55
+ }): {
56
+ apiInput: TInput;
57
+ handler: (ctx: TCtx, payload: z.output<TInput>) => string | Promise<string>;
58
+ };
59
+ (key: {
60
+ handler: (ctx: TCtx, payload: undefined) => string | Promise<string>;
61
+ }): {
62
+ apiInput?: undefined;
63
+ handler: (ctx: TCtx, payload: undefined) => string | Promise<string>;
64
+ };
65
+ };
66
+ /**
67
+ * A key builder bound to a context type, the counterpart of
68
+ * lambderGuardBuilder. The server's lambderRateLimitKey() is this bound to
69
+ * the render context; the mock runtime binds it to its own call context and
70
+ * exposes it as `rateLimitKey`.
71
+ *
72
+ * Bound rather than left open because the engine hands the handler whatever
73
+ * context the adapter runs on, and the two adapters run on different ones. A
74
+ * single builder pinned to the server's context type compiled against the
75
+ * mock and then handed the handler a context with no `ip`, `method` or
76
+ * `path`, so every caller collapsed onto one counter and the limit a test was
77
+ * written to prove silently proved nothing.
78
+ */
79
+ export declare const lambderRateLimitKeyBuilder: <TCtx>() => LambderRateLimitKeyBuilder<TCtx>;
80
+ /** What one rate-limit counter tracks: the client IP, the session identity, or a custom payload-derived key. */
81
+ export type LambderRateLimitPer<TCtx = any> = "ip" | "session" | LambderRateLimitKeyFn<any, TCtx>;
82
+ /**
83
+ * What one budget spans:
84
+ *
85
+ * - "perApi" (default): every API referencing the policy gets its own
86
+ * counter, so the windows are a per-API ceiling (three APIs referencing a
87
+ * 60/min policy allow one subject 180/min in total). An API may tune the
88
+ * windows in its declaration: `rateLimit: { name: { perMin: 20 } }`.
89
+ * - "perPolicy": every API referencing the policy shares ONE counter, so the
90
+ * windows are one combined budget (e.g. one per-email allowance across
91
+ * send, register, and reset). The policy IS the group: to give user APIs
92
+ * and report APIs separate shared budgets, declare two policies.
93
+ */
94
+ export type LambderRateLimitBudget = "perApi" | "perPolicy";
95
+ /**
96
+ * A named rate-limit policy: fixed windows, the key one counter tracks, and
97
+ * what one budget spans.
98
+ *
99
+ * Generic over the context a custom key handler receives, so the adapter's
100
+ * policies map pins it: the server's is the render context, the mock's is the
101
+ * mock call context. Left open, a handler written for one adapter compiled
102
+ * against the other and then read fields that were not there.
103
+ */
104
+ export type LambderApiRateLimitPolicyConfig<TCtx = any> = LambderRateLimitPolicy & {
105
+ per: LambderRateLimitPer<TCtx>;
106
+ /** Whether the windows are a per-API ceiling (default) or one budget shared by every referencing API. See LambderRateLimitBudget. */
107
+ budget?: LambderRateLimitBudget;
108
+ /** Envelope errorMessage for refused requests; inherits code "lambder/rate-limited" unless it sets its own. Default: a warning saying too many requests. */
109
+ errorMessage?: LambderAppRefusalMessage;
110
+ };
111
+ export type LambderApiRateLimitsConfig<TPolicies extends Record<string, LambderApiRateLimitPolicyConfig<any>>> = {
112
+ /** Your limiter instance (LambderDdbRateLimiter, LambderMemoryRateLimiter, or your own); its table and keyPrefix apply as configured on it. */
113
+ limiter: LambderRateLimiter;
114
+ /** Named policies referenced (typed) from addApi/addSessionApi. */
115
+ policies: TPolicies;
116
+ /**
117
+ * Let the request through when the limiter itself fails (the table is
118
+ * down, an IAM action is missing), instead of failing the request.
119
+ * Default: true, and the failure is logged either way.
120
+ *
121
+ * It lives here rather than on a limiter implementation because it is a
122
+ * decision about the REQUEST, not about a store: a custom limiter had no
123
+ * fail-open at all, and two limiters could answer the same outage
124
+ * differently. Set it to false on an app where an unmetered request is
125
+ * worse than a refused one.
126
+ */
127
+ failOpen?: boolean;
128
+ };
129
+ /**
130
+ * Policy names an API may reference: session-keyed policies only on session
131
+ * APIs, and apiInput-keyed policies only when the API's payload carries the
132
+ * key's fields.
133
+ */
134
+ export type LambderAllowedPolicyNames<TPolicies, TPayload, TIncludeSession extends boolean> = {
135
+ [K in keyof TPolicies]: TPolicies[K] extends {
136
+ per: "session";
137
+ } ? (TIncludeSession extends true ? K : never) : TPolicies[K] extends {
138
+ per: {
139
+ apiInput: infer S extends z.ZodType;
140
+ };
141
+ } ? (TPayload extends z.output<S> ? K : never) : K;
142
+ }[keyof TPolicies] & string;
143
+ type LambderRateLimitOverrideFor<TPolicy> = TPolicy extends {
144
+ budget: "perPolicy";
145
+ } ? Pick<LambderRateLimitOverride, "errorMessage"> : LambderRateLimitOverride;
146
+ /** The map form's full shape: every referable policy name, each carrying its own override. */
147
+ type LambderRateLimitMap<TPolicies, TPayload, TIncludeSession extends boolean> = {
148
+ readonly [K in LambderAllowedPolicyNames<TPolicies, TPayload, TIncludeSession> & keyof TPolicies]?: true | LambderRateLimitOverrideFor<TPolicies[K]>;
149
+ };
150
+ /**
151
+ * The per-API `rateLimit` option: one policy name, a non-empty ordered list
152
+ * of names, or a non-empty object map that can carry each policy's override
153
+ * (`true` applies the policy as declared). Map entries are checked in
154
+ * insertion order.
155
+ *
156
+ * Every form is non-empty by construction, the same machinery the guards
157
+ * option uses (LambderNonEmptyOptionMap): `rateLimit: {}`, `rateLimit: []`
158
+ * and `rateLimit: { policy: undefined }` announce a limit and enforce none.
159
+ */
160
+ export type LambderRateLimitOption<TPolicies, TPayload, TIncludeSession extends boolean> = LambderAllowedPolicyNames<TPolicies, TPayload, TIncludeSession> | readonly [
161
+ LambderAllowedPolicyNames<TPolicies, TPayload, TIncludeSession>,
162
+ ...LambderAllowedPolicyNames<TPolicies, TPayload, TIncludeSession>[]
163
+ ] | LambderNonEmptyOptionMap<LambderRateLimitMap<TPolicies, TPayload, TIncludeSession>>;
164
+ /**
165
+ * When in a call a policy can be checked. A `per: "ip"` counter is known from
166
+ * the request alone, so it runs before the session read and bounds how often
167
+ * one address may make the session store look a token up. Everything else
168
+ * runs after: `per: "session"` needs the session, and a custom key handler is
169
+ * app code that may read ctx.session too.
170
+ */
171
+ type LambderRateLimitPhase = "beforeSession" | "afterSession";
172
+ /**
173
+ * Runtime side of the rate-limit subsystem: holds the limiter and its named
174
+ * policies, asserts API registrations against them at startup, and checks an
175
+ * API's declared policies during preflight. Composed into
176
+ * LambderApiPolicyEngine. Reads the request's ip and the context's session
177
+ * and nothing else, so it runs unchanged under the server and the mock
178
+ * runtime.
179
+ */
180
+ export declare class LambderApiRateLimitsEngine {
181
+ private limiter;
182
+ private failOpen;
183
+ private policies;
184
+ /** True once rateLimits were configured. */
185
+ get isConfigured(): boolean;
186
+ configure(config: LambderApiRateLimitsConfig<Record<string, LambderApiRateLimitPolicyConfig>>): void;
187
+ /** Startup validation of one API registration's rateLimit option. */
188
+ assertRegistration(apiName: string, mode: LambderApiMode, rateLimitOption?: LambderRateLimitOptionValue): void;
189
+ /**
190
+ * Check the API's policies in declared order; the first exceeded one
191
+ * refuses with a 429 envelope and a Retry-After header. Attempts count,
192
+ * not successes: every counter checked before the refusing one (and every
193
+ * counter, when a later guard or validation refuses) keeps its increment,
194
+ * so list first the policy you want charged on refusals.
195
+ *
196
+ * Run twice per call, once per phase: the policies whose key needs no
197
+ * session are checked BEFORE the session is read, so a flood of requests
198
+ * carrying bogus session cookies is refused without touching the session
199
+ * store; the rest are checked after it, since `per: "session"` and a
200
+ * custom key handler may both read ctx.session. Declared order is kept
201
+ * inside each phase.
202
+ */
203
+ run(apiName: string, request: LambderApiRequest, ctx: LambderApiCallContext, rateLimitOption: LambderRateLimitOptionValue | undefined, phase: LambderRateLimitPhase): Promise<void>;
204
+ private resolveKey;
205
+ }
206
+ export {};