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,60 @@
1
+ /**
2
+ * An answer's headers: the case-insensitive read, replace and append every
3
+ * layer uses on a plain header map, and the accumulator that records what a
4
+ * call wrote so it can be applied onto whichever answer the call ends up
5
+ * with.
6
+ *
7
+ * One implementation, at the bottom of the stack, because every layer above
8
+ * it needs the same one: LambderResponse's own header methods are these three
9
+ * functions, and a second copy had already drifted from them.
10
+ */
11
+ /** The header's values under a case-insensitive lookup, or undefined. */
12
+ export declare const getAnswerHeader: (headers: Record<string, string[]>, name: string) => string[] | undefined;
13
+ /** Replaces the header under any casing of its name. */
14
+ export declare const setAnswerHeader: (headers: Record<string, string[]>, name: string, value: string | string[]) => void;
15
+ /** Appends a value to the header, under the casing it already has if any. */
16
+ export declare const addAnswerHeader: (headers: Record<string, string[]>, name: string, value: string) => void;
17
+ /** What LambderAnswerHeaders applies onto: a LambderResponse, or a header map. */
18
+ export type LambderHeaderTarget = {
19
+ getHeader(key: string): string[] | undefined;
20
+ setHeader(key: string, value: string | string[]): unknown;
21
+ addHeader(key: string, value: string): unknown;
22
+ };
23
+ /**
24
+ * Response headers written while a call runs (`res.setHeader`, `res.addHeader`,
25
+ * the session controller's Set-Cookie), applied onto the answer once the
26
+ * call has one. Recorded as operations in call order rather than as a map,
27
+ * so `set` replaces what the answer itself carries (a Content-Type, say) and
28
+ * `add` appends to it, exactly as the two would if called on the answer
29
+ * directly.
30
+ *
31
+ * These headers belong to the CALL, not to the response that first carried
32
+ * them: on the server an afterRender hook may answer with a different
33
+ * response than the handler produced, and a session cookie written during the
34
+ * call has to travel across to it. So applying never forgets the operations,
35
+ * and applying the same ones twice is a no-op: `set` writes the same value
36
+ * again, and `add` skips a value the header already carries. The one thing
37
+ * that costs is two `add` calls of the identical value under one name, which
38
+ * collapse to one; duplicate identical header values carry no meaning in
39
+ * HTTP, so nothing observable is lost.
40
+ */
41
+ export declare class LambderAnswerHeaders {
42
+ private operations;
43
+ set(key: string, value: string | string[]): void;
44
+ add(key: string, value: string): void;
45
+ /**
46
+ * How many operations have been recorded. Also a mark: reading it before
47
+ * a step and passing it back as `fromIndex` applies only what that step
48
+ * wrote.
49
+ */
50
+ get size(): number;
51
+ /**
52
+ * Applies the recorded operations, in order, onto anything that reads and
53
+ * writes headers: a LambderResponse, or a header map through applyInto.
54
+ * One definition of what an operation does, so the two targets cannot
55
+ * drift apart.
56
+ */
57
+ applyTo(target: LambderHeaderTarget, fromIndex?: number): void;
58
+ /** The same, onto an answer's plain header map. */
59
+ applyInto(headers: Record<string, string[]>, fromIndex?: number): void;
60
+ }
@@ -0,0 +1,94 @@
1
+ /**
2
+ * An answer's headers: the case-insensitive read, replace and append every
3
+ * layer uses on a plain header map, and the accumulator that records what a
4
+ * call wrote so it can be applied onto whichever answer the call ends up
5
+ * with.
6
+ *
7
+ * One implementation, at the bottom of the stack, because every layer above
8
+ * it needs the same one: LambderResponse's own header methods are these three
9
+ * functions, and a second copy had already drifted from them.
10
+ */
11
+ /** The header's values under a case-insensitive lookup, or undefined. */
12
+ export const getAnswerHeader = (headers, name) => {
13
+ const lower = name.toLowerCase();
14
+ for (const [key, values] of Object.entries(headers)) {
15
+ if (key.toLowerCase() === lower)
16
+ return values;
17
+ }
18
+ return undefined;
19
+ };
20
+ /** Replaces the header under any casing of its name. */
21
+ export const setAnswerHeader = (headers, name, value) => {
22
+ const lower = name.toLowerCase();
23
+ for (const key of Object.keys(headers)) {
24
+ if (key.toLowerCase() === lower)
25
+ delete headers[key];
26
+ }
27
+ headers[name] = Array.isArray(value) ? [...value] : [value];
28
+ };
29
+ /** Appends a value to the header, under the casing it already has if any. */
30
+ export const addAnswerHeader = (headers, name, value) => {
31
+ const lower = name.toLowerCase();
32
+ const existing = Object.keys(headers).find((key) => key.toLowerCase() === lower);
33
+ if (existing)
34
+ headers[existing].push(value);
35
+ else
36
+ headers[name] = [value];
37
+ };
38
+ /**
39
+ * Response headers written while a call runs (`res.setHeader`, `res.addHeader`,
40
+ * the session controller's Set-Cookie), applied onto the answer once the
41
+ * call has one. Recorded as operations in call order rather than as a map,
42
+ * so `set` replaces what the answer itself carries (a Content-Type, say) and
43
+ * `add` appends to it, exactly as the two would if called on the answer
44
+ * directly.
45
+ *
46
+ * These headers belong to the CALL, not to the response that first carried
47
+ * them: on the server an afterRender hook may answer with a different
48
+ * response than the handler produced, and a session cookie written during the
49
+ * call has to travel across to it. So applying never forgets the operations,
50
+ * and applying the same ones twice is a no-op: `set` writes the same value
51
+ * again, and `add` skips a value the header already carries. The one thing
52
+ * that costs is two `add` calls of the identical value under one name, which
53
+ * collapse to one; duplicate identical header values carry no meaning in
54
+ * HTTP, so nothing observable is lost.
55
+ */
56
+ export class LambderAnswerHeaders {
57
+ operations = [];
58
+ set(key, value) {
59
+ this.operations.push({ op: "set", key, value });
60
+ }
61
+ add(key, value) {
62
+ this.operations.push({ op: "add", key, value });
63
+ }
64
+ /**
65
+ * How many operations have been recorded. Also a mark: reading it before
66
+ * a step and passing it back as `fromIndex` applies only what that step
67
+ * wrote.
68
+ */
69
+ get size() { return this.operations.length; }
70
+ /**
71
+ * Applies the recorded operations, in order, onto anything that reads and
72
+ * writes headers: a LambderResponse, or a header map through applyInto.
73
+ * One definition of what an operation does, so the two targets cannot
74
+ * drift apart.
75
+ */
76
+ applyTo(target, fromIndex = 0) {
77
+ for (const operation of this.operations.slice(fromIndex)) {
78
+ if (operation.op === "set") {
79
+ target.setHeader(operation.key, operation.value);
80
+ }
81
+ else if (!target.getHeader(operation.key)?.includes(operation.value)) {
82
+ target.addHeader(operation.key, operation.value);
83
+ }
84
+ }
85
+ }
86
+ /** The same, onto an answer's plain header map. */
87
+ applyInto(headers, fromIndex = 0) {
88
+ this.applyTo({
89
+ getHeader: (key) => getAnswerHeader(headers, key),
90
+ setHeader: (key, value) => setAnswerHeader(headers, key, value),
91
+ addHeader: (key, value) => addAnswerHeader(headers, key, value),
92
+ }, fromIndex);
93
+ }
94
+ }
@@ -0,0 +1,129 @@
1
+ /**
2
+ * Lambder API Contract System
3
+ *
4
+ * Contracts are built via method chaining and inferred using typeof lambder.ApiContract
5
+ */
6
+ import type { LambderCrashDetail } from "./LambderCrashDetail.js";
7
+ import type { LambderNonEmptyOptionMap } from "../util/LambderTypeUtilities.js";
8
+ import type { LambderAppRefusalMessage } from "./LambderApiRefusal.js";
9
+ import type { LambderApiIdempotencyOption, LambderGuardsOptionValue, LambderRateLimitOptionValue } from "./LambderApiOptionValues.js";
10
+ /** Whether an endpoint runs without a session or requires one (addApi versus addSessionApi). */
11
+ export type LambderApiMode = "public" | "session";
12
+ /**
13
+ * Base shape for API contracts: what LambderCaller, LambderInvokeCaller and
14
+ * LambderMockApp accept as a contract type.
15
+ */
16
+ export type LambderApiContractShape = Record<string, {
17
+ input: any;
18
+ output: any;
19
+ /** "public" (addApi) or "session" (addSessionApi). */
20
+ mode?: LambderApiMode;
21
+ /** Present when the API declares guardInput-mode guards: guard name -> value the client must send via options.guardInputs. */
22
+ guardInputs?: any;
23
+ /**
24
+ * Present when the API declares guards: the `guards` option exactly as
25
+ * written at registration, so a client-side copy of "what does this API
26
+ * need" can be pinned to the server's own declaration with `satisfies`
27
+ * rather than kept honest by a test that reads the source.
28
+ */
29
+ guards?: LambderGuardsOptionValue;
30
+ /** Present when the API declares a rate limit: the `rateLimit` option exactly as written. */
31
+ rateLimit?: LambderRateLimitOptionValue;
32
+ /** Present when the API declares idempotency: the `idempotency` option exactly as written. */
33
+ idempotency?: LambderApiIdempotencyOption;
34
+ }>;
35
+ /** Envelope flags/channels the server may set beside (or instead of) the payload. */
36
+ export type LambderApiResponseConfig = {
37
+ versionExpired?: boolean;
38
+ sessionExpired?: boolean;
39
+ notAuthorized?: boolean;
40
+ message?: any;
41
+ /** A refusal message, or a plain string: what LambderApiRefusal and res.api(null, { errorMessage }) put here. */
42
+ errorMessage?: LambderAppRefusalMessage | string;
43
+ logList?: any[];
44
+ /**
45
+ * A crash described in full (name, message, stack, cause chain, where it
46
+ * happened), for a caller that is allowed to see it: a global error
47
+ * handler answering a trusted invoker sets it with describeCrash().
48
+ * LambderInvokeCaller reads it back as the cause of the error it throws;
49
+ * the browser caller ignores it.
50
+ */
51
+ crash?: LambderCrashDetail;
52
+ };
53
+ /**
54
+ * The config a null answer carries: at least one of the reason fields, so
55
+ * `res.api(null, {})` is a compile error. A bare null with no flag and no
56
+ * message reaches the caller as a success whose payload is null, which is
57
+ * indistinguishable from an endpoint that answered nothing on purpose.
58
+ */
59
+ export type LambderApiNullAnswerConfig = LambderNonEmptyOptionMap<Pick<LambderApiResponseConfig, "versionExpired" | "sessionExpired" | "notAuthorized" | "errorMessage" | "message">> & LambderApiResponseConfig;
60
+ /** The API wire envelope both sides speak: res.api() emits it, LambderCaller parses it. */
61
+ export type LambderApiEnvelopeBody<T> = LambderApiResponseConfig & {
62
+ apiVersion?: string | null;
63
+ payload?: T | null;
64
+ };
65
+ /**
66
+ * One contract entry as addApi/addSessionApi record it: the payload types,
67
+ * the mode, and every declarative option exactly as written. Options that
68
+ * were not written are absent rather than undefined, so `keyof` an entry
69
+ * lists only what the endpoint declared.
70
+ */
71
+ export type LambderContractEntry<In, Out, Mode extends LambderApiMode, GuardInputs = never, Guards = never, RateLimit = never, Idempotency = never> = {
72
+ input: In;
73
+ output: Out;
74
+ mode: Mode;
75
+ } & ([GuardInputs] extends [never] ? {} : {
76
+ guardInputs: GuardInputs;
77
+ }) & ([Guards] extends [never] ? {} : {
78
+ guards: Guards;
79
+ }) & ([RateLimit] extends [never] ? {} : {
80
+ rateLimit: RateLimit;
81
+ }) & ([Idempotency] extends [never] ? {} : {
82
+ idempotency: Idempotency;
83
+ });
84
+ /** Helper type for merging a new entry into the contract during chaining. */
85
+ export type LambderMergeContract<Old, Name extends string, Entry> = Old & {
86
+ [K in Name]: Entry;
87
+ };
88
+ /** Guard names referenced by a guards option, whichever of its three forms is used. */
89
+ export type LambderGuardNamesIn<TOpt> = TOpt extends string ? TOpt : TOpt extends readonly (infer N extends string)[] ? N : TOpt extends object ? keyof TOpt & string : never;
90
+ /** The endpoint's mode; a contract written without one admits either. */
91
+ export type LambderContractMode<C, K extends keyof C> = C[K] extends {
92
+ mode: infer M extends LambderApiMode;
93
+ } ? M : LambderApiMode;
94
+ /** The endpoint names of one mode. */
95
+ export type LambderContractKeysWithMode<C, M extends LambderApiMode> = {
96
+ [K in keyof C]: LambderContractMode<C, K> extends M ? K : never;
97
+ }[keyof C] & string;
98
+ /** The endpoint's guards option as written, or never when it declared none. */
99
+ export type LambderContractGuardsOf<C, K extends keyof C> = C[K] extends {
100
+ guards: infer G;
101
+ } ? G : never;
102
+ /** Every guard name any endpoint of the contract declares. */
103
+ export type LambderContractGuardNames<C> = {
104
+ [K in keyof C]: LambderGuardNamesIn<LambderContractGuardsOf<C, K>>;
105
+ }[keyof C] & string;
106
+ /** The endpoint's guardInputs requirement, or never when its guards take no client input. */
107
+ export type LambderContractGuardInputsOf<C, K extends keyof C> = C[K] extends {
108
+ guardInputs: infer G;
109
+ } ? G : never;
110
+ /** The value guard N takes from the client, as the endpoints declaring it inferred it (a union across them when they differ). */
111
+ export type LambderContractGuardInput<C, N extends string> = {
112
+ [K in keyof C]: [LambderContractGuardInputsOf<C, K>] extends [never] ? never : (N extends keyof LambderContractGuardInputsOf<C, K> ? LambderContractGuardInputsOf<C, K>[N] : never);
113
+ }[keyof C];
114
+ /**
115
+ * Guard names any endpoint declares in guardInput mode: the ones whose
116
+ * value the client sends. (An endpoint without guardInputs contributes
117
+ * nothing: `keyof never` would be every key, so it is excluded first.)
118
+ */
119
+ export type LambderContractGuardInputNames<C> = {
120
+ [K in keyof C]: [LambderContractGuardInputsOf<C, K>] extends [never] ? never : keyof LambderContractGuardInputsOf<C, K> & string;
121
+ }[keyof C];
122
+ /** The endpoint's rateLimit option as written, or never. */
123
+ export type LambderContractRateLimitOf<C, K extends keyof C> = C[K] extends {
124
+ rateLimit: infer R;
125
+ } ? R : never;
126
+ /** The endpoint's idempotency option as written, or never. */
127
+ export type LambderContractIdempotencyOf<C, K extends keyof C> = C[K] extends {
128
+ idempotency: infer I;
129
+ } ? I : never;
@@ -0,0 +1,39 @@
1
+ /**
2
+ * The runtime shapes of the three per-API policy options (guards, rate limit,
3
+ * idempotency), declared below both the contract that records them and the
4
+ * engines that enforce them so neither has to import the other.
5
+ *
6
+ * A contract type carries these options exactly as an API wrote them, and the
7
+ * engines in `api/` read the same shapes back. Declaring them here is what
8
+ * keeps `shared/` at the bottom of the stack: without it the contract would
9
+ * name an `api/` type and `shared/` would depend on a layer above it.
10
+ */
11
+ import type { LambderAppRefusalMessage } from "./LambderApiRefusal.js";
12
+ import type { LambderRateLimitPolicy } from "../contracts/LambderRateLimiter.js";
13
+ /** The guards option's runtime shape: a name, ordered names, or a name-to-param map. */
14
+ export type LambderGuardsOptionValue = string | readonly string[] | Readonly<Record<string, unknown>>;
15
+ /**
16
+ * What an API may override on a policy it references, in the map form of the
17
+ * rateLimit option. Windows merge over the policy's own (a tighter burst keeps
18
+ * the policy's daily cap) and are only overridable on "perApi" budgets: a
19
+ * shared counter has one set of numbers. errorMessage is per-API text, so it
20
+ * is overridable on either budget.
21
+ */
22
+ export type LambderRateLimitOverride = LambderRateLimitPolicy & {
23
+ errorMessage?: LambderAppRefusalMessage;
24
+ };
25
+ /** The rateLimit option's runtime shape: a name, ordered names, or a name-to-override map (LambderRateLimitOption narrows the names and overrides per policy). */
26
+ export type LambderRateLimitOptionValue = string | readonly string[] | Readonly<Record<string, true | LambderRateLimitOverride | undefined>>;
27
+ /** The per-endpoint idempotency declaration: on, or on with its own replay TTL. */
28
+ export type LambderApiIdempotencyOption = boolean | {
29
+ /** Seconds this API's stored answer replays for; overrides defaultTtlSeconds. */
30
+ ttlSeconds?: number;
31
+ /**
32
+ * Seconds this API's claim stays pending before a retry may take the
33
+ * scope; overrides defaultPendingTtlSeconds. Raise it on an API whose
34
+ * handler can run longer than the default, or a retry that arrives after
35
+ * it expires runs the operation a second time while the original is still
36
+ * working.
37
+ */
38
+ pendingTtlSeconds?: number;
39
+ };
@@ -0,0 +1,11 @@
1
+ /**
2
+ * The runtime shapes of the three per-API policy options (guards, rate limit,
3
+ * idempotency), declared below both the contract that records them and the
4
+ * engines that enforce them so neither has to import the other.
5
+ *
6
+ * A contract type carries these options exactly as an API wrote them, and the
7
+ * engines in `api/` read the same shapes back. Declaring them here is what
8
+ * keeps `shared/` at the bottom of the stack: without it the contract would
9
+ * name an `api/` type and `shared/` would depend on a layer above it.
10
+ */
11
+ export {};
@@ -0,0 +1,128 @@
1
+ /**
2
+ * The one mapping from an HTTP answer to an API outcome.
3
+ *
4
+ * LambderCaller (a browser, over fetch) and LambderInvokeCaller (a server,
5
+ * over a direct Lambda invoke) receive the same envelope and must read it
6
+ * the same way: which status is a crash, which is a rejected input, in what
7
+ * order the envelope flags are honoured, what a non-envelope body means.
8
+ * Both hand their answer to resolveApiOutcome and act on the result; the
9
+ * side effects each has (handlers, cookie clearing, error reporting) stay
10
+ * with the caller that owns them. Pure and dependency-free, so the browser
11
+ * entry resolves it.
12
+ */
13
+ import type { z } from "zod";
14
+ import type { LambderApiEnvelopeBody } from "./LambderApiContract.js";
15
+ import type { LambderAppRefusalMessage } from "./LambderApiRefusal.js";
16
+ /**
17
+ * The 422 body's `zodError` as it survives JSON: a ZodError's name and
18
+ * message, and its issues spelled out. Not a ZodError instance (it has no
19
+ * methods on this side of the wire), which is why it is not typed as one.
20
+ */
21
+ export type LambderValidationError = {
22
+ name: string;
23
+ message: string;
24
+ issues: z.core.$ZodIssue[];
25
+ };
26
+ export type LambderApiFailureReason = 'network' | 'timeout' | 'server' | 'validation' | 'versionExpired' | 'sessionExpired' | 'notAuthorized' | 'errorMessage' | 'unknown';
27
+ /** A call that produced an answer the server means as a result. */
28
+ export type LambderApiSuccessOutcome<T> = {
29
+ ok: true;
30
+ payload: T | null | undefined;
31
+ response: LambderApiEnvelopeBody<T>;
32
+ /** The answer's logList, when it carried one. See LambderApiFailureFields.logList: the field is on every arm so a caller surfaces logs once. */
33
+ logList?: unknown[];
34
+ };
35
+ /** What every failure carries, whatever went wrong. */
36
+ type LambderApiFailureFields = {
37
+ ok: false;
38
+ /** HTTP status, when a response was received. */
39
+ status?: number;
40
+ /** Envelope errorMessage, when the server provided one. */
41
+ errorMessage?: LambderAppRefusalMessage | string;
42
+ /** Seconds to wait before retrying, from the response's Retry-After header (rate-limit refusals send it). */
43
+ retryAfterSeconds?: number;
44
+ /**
45
+ * The answer's logList, when it carried one: the envelope's on a success
46
+ * or an envelope refusal, the parsed 500 body's on a server failure, and
47
+ * the validation body's on a 422 (the server writes it there too). It is
48
+ * on every arm so that a caller surfaces logs in ONE place, right after
49
+ * reading the answer, instead of once per outcome it happens to handle:
50
+ * the browser caller surfaced them after its early returns and so never
51
+ * printed the logs of the answer whose logs matter most, a 500.
52
+ */
53
+ logList?: unknown[];
54
+ };
55
+ /**
56
+ * No result came back to read: the request never completed, it was given up
57
+ * on, the server failed, or something inside the caller threw. Always carries
58
+ * the Error, so a reader that narrowed this far never has to check for it.
59
+ * A 5xx also carries `response` when the server answered with Lambder's own
60
+ * envelope, which is how a crash detail and a logList arrive with it.
61
+ */
62
+ export type LambderApiCallFailure<T> = LambderApiFailureFields & {
63
+ reason: 'network' | 'timeout' | 'server' | 'unknown';
64
+ error: Error;
65
+ response?: LambderApiEnvelopeBody<T>;
66
+ };
67
+ /** HTTP 422: the server rejected the input against the API's schema. Always carries the issues. */
68
+ export type LambderApiValidationFailure = LambderApiFailureFields & {
69
+ reason: 'validation';
70
+ zodError: LambderValidationError;
71
+ };
72
+ /** The server answered, and the envelope itself says the call is refused. Always carries that envelope. */
73
+ export type LambderApiEnvelopeFailure<T> = LambderApiFailureFields & {
74
+ reason: 'versionExpired' | 'sessionExpired' | 'notAuthorized' | 'errorMessage';
75
+ response: LambderApiEnvelopeBody<T>;
76
+ };
77
+ /**
78
+ * Discriminated result of an API call: `ok: true` carries the payload, every
79
+ * failure carries a machine-readable reason, so "the server returned null"
80
+ * and "the request failed" are never conflated.
81
+ *
82
+ * The failure side is discriminated by `reason` rather than being one arm of
83
+ * optional fields, so narrowing to a reason narrows to what that reason
84
+ * actually carries: `zodError` after `reason === 'validation'`, `response`
85
+ * after an envelope reason, `error` after the rest. Read as one wide arm, the
86
+ * framework's own reader needed three non-null assertions to say what the
87
+ * union already knew.
88
+ */
89
+ export type LambderApiOutcome<T> = LambderApiSuccessOutcome<T> | LambderApiCallFailure<T> | LambderApiValidationFailure | LambderApiEnvelopeFailure<T>;
90
+ /**
91
+ * What reading one HTTP answer can produce. Narrower than LambderApiOutcome
92
+ * by the three reasons no answer can carry: `network` and `timeout` belong to
93
+ * the caller's own abort, and `unknown` to something throwing around the
94
+ * call. So a caller that has handled `server` and `validation` holds a
95
+ * success or an envelope refusal, both of which carry the envelope.
96
+ */
97
+ export type LambderApiAnswerOutcome<T> = LambderApiSuccessOutcome<T> | (LambderApiCallFailure<T> & {
98
+ reason: 'server';
99
+ }) | LambderApiValidationFailure | LambderApiEnvelopeFailure<T>;
100
+ /**
101
+ * What the mapping needs from an HTTP answer, whichever transport produced it.
102
+ *
103
+ * Exactly one of `json()` and `text()` is read per answer, never both: a
104
+ * transport backed by a real Response body may only be read once, and the
105
+ * mapping is written to that rule (a 5xx reads text and parses it itself, so
106
+ * that a non-envelope body is still reportable).
107
+ */
108
+ export type LambderApiHttpAnswer = {
109
+ status: number;
110
+ statusText?: string;
111
+ /** Case-insensitive header lookup; null or undefined when absent. */
112
+ header: (name: string) => string | null | undefined;
113
+ /** The body parsed as JSON; rejects when it is not JSON. */
114
+ json: () => Promise<unknown>;
115
+ /** The body as text. */
116
+ text: () => Promise<string>;
117
+ /** The answer's Set-Cookie header values, for a transport that can see them (a cookie jar consumes them); absent in a browser. */
118
+ setCookies?: string[];
119
+ };
120
+ /**
121
+ * Reads one HTTP answer into an outcome. A 5xx is a server failure that keeps
122
+ * the envelope when the server sent one (Lambder's own 500 body carries
123
+ * errorMessage, and a global error handler may add crash and logList); a
124
+ * 422 is a validation failure only with Lambder's validation body; anything
125
+ * else must be a JSON envelope, whose flags are honoured in a fixed order.
126
+ */
127
+ export declare const resolveApiOutcome: <T>(answer: LambderApiHttpAnswer) => Promise<LambderApiAnswerOutcome<T>>;
128
+ export {};
@@ -36,6 +36,7 @@ export const resolveApiOutcome = async (answer) => {
36
36
  return {
37
37
  ok: false, reason: 'server', status,
38
38
  errorMessage: envelope?.errorMessage,
39
+ logList: envelope?.logList,
39
40
  ...(envelope ? { response: envelope } : {}),
40
41
  error: new Error("Request failed: " + status + " - " + (answer.statusText ?? "")),
41
42
  };
@@ -43,15 +44,18 @@ export const resolveApiOutcome = async (answer) => {
43
44
  if (status === 422) {
44
45
  // A 422 without Lambder's validation body (e.g. a proxy's error page)
45
46
  // is a server failure, not a validation result.
46
- let zodError;
47
+ // The validation body carries the call's logList as every other
48
+ // answer does, so it is read here rather than left on the wire.
49
+ let body;
47
50
  try {
48
- zodError = (await answer.json())?.zodError;
51
+ body = await answer.json();
49
52
  }
50
53
  catch { /* not JSON */ }
54
+ const zodError = body?.zodError;
51
55
  if (zodError === undefined) {
52
56
  return { ok: false, reason: 'server', status, error: new Error("Request failed: 422 without a validation body") };
53
57
  }
54
- return { ok: false, reason: 'validation', status, zodError };
58
+ return { ok: false, reason: 'validation', status, zodError, logList: body?.logList };
55
59
  }
56
60
  // Retry-After (delta-seconds) rides every refusal that knows its reset
57
61
  // time, e.g. a rate limit; absent or unreadable is undefined.
@@ -68,12 +72,15 @@ export const resolveApiOutcome = async (answer) => {
68
72
  return { ok: false, reason: 'server', status, error: new Error("Request failed: response is not a valid API envelope (status " + status + ")", { cause: err }) };
69
73
  }
70
74
  if (data.versionExpired)
71
- return { ok: false, reason: 'versionExpired', status, errorMessage: data.errorMessage, response: data, ...retryAfter };
75
+ return { ok: false, reason: 'versionExpired', status, errorMessage: data.errorMessage, response: data, logList: data.logList, ...retryAfter };
72
76
  if (data.sessionExpired)
73
- return { ok: false, reason: 'sessionExpired', status, errorMessage: data.errorMessage, response: data, ...retryAfter };
77
+ return { ok: false, reason: 'sessionExpired', status, errorMessage: data.errorMessage, response: data, logList: data.logList, ...retryAfter };
74
78
  if (data.notAuthorized)
75
- return { ok: false, reason: 'notAuthorized', status, errorMessage: data.errorMessage, response: data, ...retryAfter };
76
- if (data.errorMessage)
77
- return { ok: false, reason: 'errorMessage', status, errorMessage: data.errorMessage, response: data, ...retryAfter };
78
- return { ok: true, payload: data.payload, response: data };
79
+ return { ok: false, reason: 'notAuthorized', status, errorMessage: data.errorMessage, response: data, logList: data.logList, ...retryAfter };
80
+ // Presence, not truthiness: the writer keeps an errorMessage an app spelled
81
+ // out as the empty string, so a refusal that says nothing is still a
82
+ // refusal. Tested for truth here, it shipped back as a success.
83
+ if (data.errorMessage !== undefined)
84
+ return { ok: false, reason: 'errorMessage', status, errorMessage: data.errorMessage, response: data, logList: data.logList, ...retryAfter };
85
+ return { ok: true, payload: data.payload, response: data, logList: data.logList };
79
86
  };