lambder 6.0.1 → 7.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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 +26 -24
  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
@@ -76,3 +76,33 @@ export const errorFromCrashDetail = (crash) => {
76
76
  cause = build(entry, cause);
77
77
  return build(crash, cause);
78
78
  };
79
+ /**
80
+ * A thrown value as an Error, so a reporter or a `cause` chain always holds
81
+ * one. An Error passes through; anything else becomes an Error whose message
82
+ * describes the value the way describeCrash does, JSON where String() cannot
83
+ * (a null-prototype object or a throwing toString must not make the
84
+ * coercion itself throw). One implementation, rather than
85
+ * `err instanceof Error ? err : new Error(...)` spelled at every site with a
86
+ * fallback of its own.
87
+ */
88
+ export const coerceToError = (value, fallbackMessage = "Unknown error") => {
89
+ if (value instanceof Error)
90
+ return value;
91
+ let message;
92
+ if (typeof value === "string")
93
+ message = value;
94
+ else {
95
+ try {
96
+ message = JSON.stringify(value);
97
+ }
98
+ catch {
99
+ try {
100
+ message = String(value);
101
+ }
102
+ catch {
103
+ message = undefined;
104
+ }
105
+ }
106
+ }
107
+ return new Error(message || fallbackMessage);
108
+ };
@@ -0,0 +1,12 @@
1
+ /**
2
+ * The HTTP status codes a Lambder response may carry.
3
+ *
4
+ * In `shared/` rather than beside LambderResponse because it is HTTP
5
+ * vocabulary with no relationship to the server's response class, and the
6
+ * places that need it include ones a browser bundle reaches
7
+ * (LambderApiRefusal, the mock's contract types). Importing it from `core/`
8
+ * pulled `core/LambderResponse.ts` into the type graph of `lambder/client`,
9
+ * and with it `aws-lambda`, so a browser-only consumer needed
10
+ * `@types/aws-lambda` resolvable to typecheck a status union.
11
+ */
12
+ export type LambderHttpStatusCode = 100 | 101 | 200 | 201 | 202 | 203 | 204 | 206 | 300 | 301 | 302 | 303 | 304 | 307 | 308 | 400 | 401 | 402 | 403 | 404 | 405 | 406 | 408 | 409 | 410 | 412 | 413 | 415 | 416 | 418 | 422 | 428 | 429 | 431 | 451 | 500 | 501 | 502 | 503 | 504;
@@ -0,0 +1 @@
1
+ export {};
@@ -25,7 +25,7 @@
25
25
  * LambderCompressionCodec gives stored records, so a malicious or truncated
26
26
  * body fails instead of expanding without limit.
27
27
  */
28
- import type { LambderCompressionOption } from "./LambderCompressionOption.js";
28
+ import type { LambderCompressionOption, LambderCompressionSettings } from "./LambderCompressionOption.js";
29
29
  /** Envelope field carrying the base64 gzip of the payload's JSON. */
30
30
  export declare const COMPRESSED_PAYLOAD_GZ_FIELD = "payloadGz";
31
31
  /**
@@ -71,18 +71,18 @@ export declare const DEFAULT_REQUEST_COMPRESSION_SETTINGS: LambderRequestCompres
71
71
  */
72
72
  export declare const DEFAULT_MAX_RESTORED_PAYLOAD_BYTES = 20000000;
73
73
  /**
74
- * The two rules every compressed payload follows, whichever algorithm made
75
- * the bytes: nothing below the threshold is compressed, and the compressed
76
- * form is only ever sent when its base64 is smaller than the JSON it
77
- * replaces. The threshold is measured on real UTF-8 bytes, not string
78
- * length, so a payload of multi-byte text is judged by what actually goes on
79
- * the wire. `compress` is the algorithm: the browser's CompressionStream for
80
- * gzip, zlib for Brotli (LambderInvokeCaller); both go through here so the
81
- * rules cannot drift between them.
74
+ * The threshold one call is judged against, or null when its payload goes
75
+ * plainly: the caller's configured setting unless the call overrode it, and
76
+ * `compressRequest: true` means "whatever the size", which is a threshold of
77
+ * zero rather than a separate path.
78
+ *
79
+ * Both callers decide this, and the three-line ternary they each wrote is the
80
+ * one place a caller can get the override backwards, so it is written once
81
+ * beside the compressors it feeds.
82
82
  */
83
- export declare const compressPayloadWith: <TField extends string>(json: string, minBytes: number, field: TField, compress: (bytes: Uint8Array<ArrayBuffer>) => Promise<Uint8Array>) => Promise<({ [K in TField]: string; } & {
84
- [COMPRESSED_PAYLOAD_BYTES_FIELD]: number;
85
- }) | null>;
83
+ export declare const resolveRequestCompressionMinBytes: (compressRequest: boolean | undefined, settings: {
84
+ minBytes: number;
85
+ } | null | undefined) => number | null;
86
86
  /** True when this runtime can compress request payloads (browsers, and Node 18+). */
87
87
  export declare const isRequestCompressionAvailable: () => boolean;
88
88
  /**
@@ -94,10 +94,18 @@ export declare const isRequestCompressionAvailable: () => boolean;
94
94
  * bigger, so the compressed form is only ever sent when it is smaller.
95
95
  */
96
96
  export declare const compressPayloadGzip: (json: string, minBytes: number) => Promise<LambderCompressedGzipPayload | null>;
97
+ /** Request Brotli when `requestCompression: true` on LambderInvokeCaller: the HTTP request threshold, at the quality every other Lambder site uses. */
98
+ export declare const DEFAULT_INVOKE_REQUEST_COMPRESSION_SETTINGS: LambderCompressionSettings;
97
99
  /**
98
- * Restores a payload the caller compressed, for request mocking
99
- * (LambderMSW), so a mock handler receives the same payload the server
100
- * would. The server does NOT use this: it decompresses through zlib, whose
101
- * bounded output is what makes an untrusted body safe to expand.
100
+ * Brotli one payload's JSON for sending, or null when the plain JSON should
101
+ * go instead: compressPayloadGzip with Brotli, for a caller where both ends
102
+ * are Node (LambderInvokeCaller). The threshold and the only-when-smaller
103
+ * rule are compressPayloadWith's, shared with the gzip side.
104
+ *
105
+ * Beside its gzip twin rather than inside the invoke caller, because the two
106
+ * are one wire format with one set of rules and a reader comparing them
107
+ * should not have to open two files. It needs Buffer and zlib, which the
108
+ * codec loads lazily, so this module stays resolvable from the browser entry
109
+ * and a bundle that never calls this drops it with the codec behind it.
102
110
  */
103
- export declare const decompressPayloadGzip: (payloadGz: string) => Promise<unknown>;
111
+ export declare const compressPayloadBrotli: (json: string, minBytes: number, quality: number) => Promise<LambderCompressedBrotliPayload | null>;
@@ -1,30 +1,5 @@
1
- /**
2
- * Request payload compression: the wire format both sides speak.
3
- *
4
- * When a LambderCaller call's payload clears the configured size, the caller
5
- * sends the payload's JSON as `payloadGz` (gzip bytes, base64) beside
6
- * `payloadBytes` (its UTF-8 byte length) in place of `payload`, and the
7
- * server restores it before anything reads the payload. A Node caller
8
- * (LambderInvokeCaller) sends `payloadBr` instead, Brotli under the same
9
- * rules; the server accepts either. Everything else in the envelope
10
- * (apiName, version, token, siteHost, guardInputs, idempotencyKey) stays
11
- * plain text, so routing, logging and request mocking are unaffected.
12
- *
13
- * Base64 inside the JSON envelope, rather than a binary body with
14
- * Content-Encoding: API Gateway hands a binary request body to Lambda
15
- * base64-encoded anyway, so binary saves nothing against Lambda's ~6MB
16
- * invoke payload cap while adding a content-type negotiation that gateways,
17
- * CDNs and mock servers each treat differently. Base64's 4/3 overhead
18
- * applies to bytes that already shrank several times over.
19
- *
20
- * gzip rather than Brotli because the browser's CompressionStream offers
21
- * gzip and deflate only; responses, compressed by Node, do prefer Brotli.
22
- *
23
- * `payloadBytes` is not bookkeeping: it bounds the server's decompression
24
- * and the restored length must match it exactly, the same guarantee
25
- * LambderCompressionCodec gives stored records, so a malicious or truncated
26
- * body fails instead of expanding without limit.
27
- */
1
+ import { bytesToBase64 } from "../util/LambderBase64.js";
2
+ import { compressText } from "./LambderCompressionCodec.js";
28
3
  /** Envelope field carrying the base64 gzip of the payload's JSON. */
29
4
  export const COMPRESSED_PAYLOAD_GZ_FIELD = "payloadGz";
30
5
  /**
@@ -51,17 +26,6 @@ export const DEFAULT_REQUEST_COMPRESSION_SETTINGS = { minBytes: 4096 };
51
26
  * compressible body cannot exhaust the function's memory.
52
27
  */
53
28
  export const DEFAULT_MAX_RESTORED_PAYLOAD_BYTES = 20_000_000;
54
- /** Buffer where there is one (Node); otherwise chunked so a large payload cannot overflow the argument list of String.fromCharCode. */
55
- const bytesToBase64 = (bytes) => {
56
- if (typeof Buffer !== "undefined")
57
- return Buffer.from(bytes.buffer, bytes.byteOffset, bytes.byteLength).toString("base64");
58
- const chunkSize = 0x8000;
59
- let binary = "";
60
- for (let i = 0; i < bytes.length; i += chunkSize) {
61
- binary += String.fromCharCode(...bytes.subarray(i, i + chunkSize));
62
- }
63
- return btoa(binary);
64
- };
65
29
  /**
66
30
  * The two rules every compressed payload follows, whichever algorithm made
67
31
  * the bytes: nothing below the threshold is compressed, and the compressed
@@ -72,7 +36,7 @@ const bytesToBase64 = (bytes) => {
72
36
  * gzip, zlib for Brotli (LambderInvokeCaller); both go through here so the
73
37
  * rules cannot drift between them.
74
38
  */
75
- export const compressPayloadWith = async (json, minBytes, field, compress) => {
39
+ const compressPayloadWith = async (json, minBytes, field, compress) => {
76
40
  const encoded = new TextEncoder().encode(json);
77
41
  if (encoded.length < minBytes)
78
42
  return null;
@@ -81,6 +45,19 @@ export const compressPayloadWith = async (json, minBytes, field, compress) => {
81
45
  return null;
82
46
  return { [field]: base64, [COMPRESSED_PAYLOAD_BYTES_FIELD]: encoded.length };
83
47
  };
48
+ /**
49
+ * The threshold one call is judged against, or null when its payload goes
50
+ * plainly: the caller's configured setting unless the call overrode it, and
51
+ * `compressRequest: true` means "whatever the size", which is a threshold of
52
+ * zero rather than a separate path.
53
+ *
54
+ * Both callers decide this, and the three-line ternary they each wrote is the
55
+ * one place a caller can get the override backwards, so it is written once
56
+ * beside the compressors it feeds.
57
+ */
58
+ export const resolveRequestCompressionMinBytes = (compressRequest, settings) => compressRequest === true ? 0
59
+ : compressRequest === false ? null
60
+ : settings?.minBytes ?? null;
84
61
  /** True when this runtime can compress request payloads (browsers, and Node 18+). */
85
62
  export const isRequestCompressionAvailable = () => typeof CompressionStream !== "undefined" && typeof btoa !== "undefined";
86
63
  /**
@@ -95,18 +72,18 @@ export const compressPayloadGzip = (json, minBytes) => compressPayloadWith(json,
95
72
  const stream = new Blob([bytes]).stream().pipeThrough(new CompressionStream("gzip"));
96
73
  return new Uint8Array(await new Response(stream).arrayBuffer());
97
74
  });
75
+ /** Request Brotli when `requestCompression: true` on LambderInvokeCaller: the HTTP request threshold, at the quality every other Lambder site uses. */
76
+ export const DEFAULT_INVOKE_REQUEST_COMPRESSION_SETTINGS = { minBytes: 4096, quality: 5 };
98
77
  /**
99
- * Restores a payload the caller compressed, for request mocking
100
- * (LambderMSW), so a mock handler receives the same payload the server
101
- * would. The server does NOT use this: it decompresses through zlib, whose
102
- * bounded output is what makes an untrusted body safe to expand.
78
+ * Brotli one payload's JSON for sending, or null when the plain JSON should
79
+ * go instead: compressPayloadGzip with Brotli, for a caller where both ends
80
+ * are Node (LambderInvokeCaller). The threshold and the only-when-smaller
81
+ * rule are compressPayloadWith's, shared with the gzip side.
82
+ *
83
+ * Beside its gzip twin rather than inside the invoke caller, because the two
84
+ * are one wire format with one set of rules and a reader comparing them
85
+ * should not have to open two files. It needs Buffer and zlib, which the
86
+ * codec loads lazily, so this module stays resolvable from the browser entry
87
+ * and a bundle that never calls this drops it with the codec behind it.
103
88
  */
104
- export const decompressPayloadGzip = async (payloadGz) => {
105
- const binary = atob(payloadGz);
106
- const bytes = new Uint8Array(binary.length);
107
- for (let i = 0; i < binary.length; i += 1) {
108
- bytes[i] = binary.charCodeAt(i);
109
- }
110
- const stream = new Blob([bytes]).stream().pipeThrough(new DecompressionStream("gzip"));
111
- return JSON.parse(await new Response(stream).text());
112
- };
89
+ export const compressPayloadBrotli = (json, minBytes, quality) => compressPayloadWith(json, minBytes, COMPRESSED_PAYLOAD_BR_FIELD, (bytes) => compressText(Buffer.from(bytes.buffer, bytes.byteOffset, bytes.byteLength), "br", quality));
@@ -0,0 +1,9 @@
1
+ /**
2
+ * The session cookie names an app uses unless it configures its own. One
3
+ * definition, in shared, because the server writes these names and the
4
+ * browser caller, the invoke caller and the mock runtime all read them: the
5
+ * two sides must agree byte for byte, and a second copy is a place for them
6
+ * to drift apart with every test still green.
7
+ */
8
+ export declare const DEFAULT_SESSION_TOKEN_COOKIE_KEY = "LMDRSESSIONTKID";
9
+ export declare const DEFAULT_SESSION_CSRF_COOKIE_KEY = "LMDRSESSIONCSTK";
@@ -0,0 +1,9 @@
1
+ /**
2
+ * The session cookie names an app uses unless it configures its own. One
3
+ * definition, in shared, because the server writes these names and the
4
+ * browser caller, the invoke caller and the mock runtime all read them: the
5
+ * two sides must agree byte for byte, and a second copy is a place for them
6
+ * to drift apart with every test still green.
7
+ */
8
+ export const DEFAULT_SESSION_TOKEN_COOKIE_KEY = "LMDRSESSIONTKID";
9
+ export const DEFAULT_SESSION_CSRF_COOKIE_KEY = "LMDRSESSIONCSTK";
@@ -1,7 +1,8 @@
1
1
  import type { DynamoDBClient } from "@aws-sdk/client-dynamodb";
2
- import { type LambderCompressionOption } from "../shared/LambderCompressionOption.js";
2
+ import { type LambderCompressionOption } from "../shared/wire/LambderCompressionOption.js";
3
3
  export interface LambderDdbCacheOptions {
4
4
  tableName: string;
5
+ /** Region the client is created for on first use; the SDK's default chain otherwise. */
5
6
  region?: string;
6
7
  /** Partition key prefix, keeps cache items separated from other systems in a shared table. Default: "CACHE". */
7
8
  keyPrefix?: string;
@@ -18,6 +19,12 @@ export interface LambderDdbCacheOptions {
18
19
  maxValueBytes?: number;
19
20
  memoryMaxBytes?: number;
20
21
  client?: DynamoDBClient;
22
+ /**
23
+ * The clock entries are expired against, injectable the way the rate
24
+ * limiter's and the idempotency store's are, so a test can cross a TTL
25
+ * boundary without moving the world's clock.
26
+ */
27
+ now?: () => number;
21
28
  }
22
29
  /**
23
30
  * Where a value lives. A plain string addresses one entry, as it always has.
@@ -59,7 +66,7 @@ export interface LambderDdbCacheListOptions {
59
66
  * Table shape: string hash key `pk`, string range key `sk`, TTL on
60
67
  * `expiresAt`. Items are prefixed `CACHE#<namespace>#` by default, so the
61
68
  * table can be shared with LambderDdbRateLimiter (`RL#`) and
62
- * LambderDdbIdempotency (`IDEM#`) without key collisions.
69
+ * LambderDdbIdempotencyStore (`IDEM#`) without key collisions.
63
70
  *
64
71
  * A key may also be a `{ pk, sk }` pair, which groups entries under one
65
72
  * partition so `deletePartition` and `listSortKeys` can work on the group
@@ -72,19 +79,16 @@ export declare class LambderDdbCache {
72
79
  readonly tableName: string;
73
80
  readonly keyPrefix: string;
74
81
  readonly namespace: string;
75
- /** The client given at creation, or one created from `region` on first use; the SDK arrives with it. */
76
- private readonly providedClient;
77
- private readonly region;
78
- private readyPromise;
82
+ /** The SDK and the client, loaded and created the first time the table is touched (see LambderDdbSdk). */
83
+ private readonly ready;
79
84
  private readonly defaultTtlSeconds;
80
85
  private readonly chunkBytes;
81
86
  private readonly compression;
82
87
  private readonly maxValueBytes;
83
88
  private readonly memory;
84
89
  private readonly inFlight;
90
+ private readonly now;
85
91
  constructor(options: LambderDdbCacheOptions);
86
- /** The SDK and the client, loaded and created the first time the table is touched (see LambderDdbSdk). */
87
- private ready;
88
92
  get<T>(key: LambderCacheKey): Promise<T | undefined>;
89
93
  private getByAddress;
90
94
  has(key: LambderCacheKey): Promise<boolean>;
@@ -145,5 +149,4 @@ export declare class LambderDdbCache {
145
149
  /** Drop every in-memory copy belonging to one partition. */
146
150
  private forgetPartition;
147
151
  private nowSeconds;
148
- private isConditionalFailure;
149
152
  }
@@ -1,7 +1,8 @@
1
- import { loadDynamoClientSdk } from "./LambderDdbSdk.js";
2
- import { getCrypto } from "../shared/node-polyfills.js";
3
- import { compressText, restoreText } from "../shared/LambderCompressionCodec.js";
4
- import { resolveCompressionOption, } from "../shared/LambderCompressionOption.js";
1
+ import { createDynamoClientLoader, isConditionalCheckFailure } from "./LambderDdbSdk.js";
2
+ import { getCrypto } from "../shared/util/LambderNodeModules.js";
3
+ import { assertPositiveInteger } from "../shared/util/LambderOptionChecks.js";
4
+ import { compressText, restoreText, LambderCompressionError } from "../shared/wire/LambderCompressionCodec.js";
5
+ import { resolveCompressionOption, } from "../shared/wire/LambderCompressionOption.js";
5
6
  import { LRUCache } from "lru-cache";
6
7
  const DEFAULT_TTL_SECONDS = 365 * 24 * 60 * 60;
7
8
  const DEFAULT_CHUNK_BYTES = 350 * 1024;
@@ -19,6 +20,25 @@ const BATCH_WRITE_LIMIT = 25;
19
20
  const MAX_BATCH_RETRIES = 8;
20
21
  /** Every value compressed by default; see the `compression` option. */
21
22
  const COMPRESSION_DEFAULTS = { minBytes: 0, quality: 5 };
23
+ /**
24
+ * A stored entry that cannot be trusted: chunks that do not add up to what
25
+ * the manifest describes, bytes that fail its checksum, or a restore the
26
+ * codec would not vouch for (a LambderCompressionError, which is the same
27
+ * answer in the compression layer's own words).
28
+ *
29
+ * A type rather than "anything thrown while reading the entry", because the
30
+ * two are treated in opposite ways: a corrupt entry is dropped so the next
31
+ * reader refills it, while a throttled or failed Query is the table being
32
+ * busy. Deleting a healthy 2MB entry over one throttle orphans its chunks
33
+ * until their TTL and sends every later reader to the origin, which is the
34
+ * load the cache exists to absorb.
35
+ */
36
+ class LambderCacheIntegrityError extends Error {
37
+ constructor(message) {
38
+ super(message);
39
+ this.name = "LambderCacheIntegrityError";
40
+ }
41
+ }
22
42
  /**
23
43
  * `#` separates the store's own item-key segments, so a caller's `#` is
24
44
  * escaped rather than refused: `~` becomes `~0` and `#` becomes `~1`. An
@@ -30,11 +50,11 @@ const COMPRESSION_DEFAULTS = { minBytes: 0, quality: 5 };
30
50
  */
31
51
  const encodeSortKey = (value) => value.replace(/~/g, "~0").replace(/#/g, "~1");
32
52
  const decodeSortKey = (value) => value.replace(/~([01])/g, (_match, code) => code === "0" ? "~" : "#");
33
- // Node builtins are loaded lazily through node-polyfills so this module can
53
+ // Node builtins are loaded lazily through LambderNodeModules so this module can
34
54
  // sit in a frontend bundle's import graph (via the package root) without
35
55
  // breaking; using the cache at runtime still requires Node. Brotli helpers
36
- // are shared with LambderDdbIdempotency and LambderSessionManager via
37
- // ../shared/LambderCompressionCodec.js.
56
+ // are shared with LambderDdbIdempotencyStore and LambderDdbSessionStore via
57
+ // ../shared/wire/LambderCompressionCodec.js.
38
58
  const requireCrypto = async () => {
39
59
  const crypto = await getCrypto();
40
60
  if (!crypto)
@@ -49,12 +69,6 @@ const randomUUID = async () => {
49
69
  const crypto = await requireCrypto();
50
70
  return crypto.randomUUID();
51
71
  };
52
- const positiveInteger = (value, name) => {
53
- if (!Number.isSafeInteger(value) || value <= 0) {
54
- throw new Error(`${name} must be a positive safe integer`);
55
- }
56
- return value;
57
- };
58
72
  const sleep = (milliseconds) => new Promise((resolve) => setTimeout(resolve, milliseconds));
59
73
  /**
60
74
  * Persistent JSON cache backed by DynamoDB.
@@ -71,7 +85,7 @@ const sleep = (milliseconds) => new Promise((resolve) => setTimeout(resolve, mil
71
85
  * Table shape: string hash key `pk`, string range key `sk`, TTL on
72
86
  * `expiresAt`. Items are prefixed `CACHE#<namespace>#` by default, so the
73
87
  * table can be shared with LambderDdbRateLimiter (`RL#`) and
74
- * LambderDdbIdempotency (`IDEM#`) without key collisions.
88
+ * LambderDdbIdempotencyStore (`IDEM#`) without key collisions.
75
89
  *
76
90
  * A key may also be a `{ pk, sk }` pair, which groups entries under one
77
91
  * partition so `deletePartition` and `listSortKeys` can work on the group
@@ -84,16 +98,15 @@ export class LambderDdbCache {
84
98
  tableName;
85
99
  keyPrefix;
86
100
  namespace;
87
- /** The client given at creation, or one created from `region` on first use; the SDK arrives with it. */
88
- providedClient;
89
- region;
90
- readyPromise;
101
+ /** The SDK and the client, loaded and created the first time the table is touched (see LambderDdbSdk). */
102
+ ready;
91
103
  defaultTtlSeconds;
92
104
  chunkBytes;
93
105
  compression;
94
106
  maxValueBytes;
95
107
  memory;
96
108
  inFlight = new Map();
109
+ now;
97
110
  constructor(options) {
98
111
  if (!options.tableName.trim())
99
112
  throw new Error("tableName is required");
@@ -103,29 +116,22 @@ export class LambderDdbCache {
103
116
  if (Buffer.byteLength(this.namespace, "utf8") > 128) {
104
117
  throw new Error("namespace must be at most 128 UTF-8 bytes");
105
118
  }
106
- this.defaultTtlSeconds = positiveInteger(options.defaultTtlSeconds ?? DEFAULT_TTL_SECONDS, "defaultTtlSeconds");
107
- this.chunkBytes = positiveInteger(options.chunkBytes ?? DEFAULT_CHUNK_BYTES, "chunkBytes");
119
+ this.defaultTtlSeconds = assertPositiveInteger(options.defaultTtlSeconds ?? DEFAULT_TTL_SECONDS, "defaultTtlSeconds");
120
+ this.chunkBytes = assertPositiveInteger(options.chunkBytes ?? DEFAULT_CHUNK_BYTES, "chunkBytes");
108
121
  if (this.chunkBytes > MAX_SAFE_CHUNK_BYTES) {
109
122
  throw new Error(`chunkBytes must not exceed ${MAX_SAFE_CHUNK_BYTES}`);
110
123
  }
111
124
  this.compression = resolveCompressionOption(options.compression, COMPRESSION_DEFAULTS);
112
- this.maxValueBytes = positiveInteger(options.maxValueBytes ?? DEFAULT_MAX_VALUE_BYTES, "maxValueBytes");
125
+ this.maxValueBytes = assertPositiveInteger(options.maxValueBytes ?? DEFAULT_MAX_VALUE_BYTES, "maxValueBytes");
113
126
  const memoryMaxBytes = options.memoryMaxBytes ?? DEFAULT_MEMORY_BYTES;
114
127
  this.memory = memoryMaxBytes === 0
115
128
  ? null
116
129
  : new LRUCache({
117
- maxSize: positiveInteger(memoryMaxBytes, "memoryMaxBytes"),
130
+ maxSize: assertPositiveInteger(memoryMaxBytes, "memoryMaxBytes"),
118
131
  sizeCalculation: (entry) => entry.stored.length,
119
132
  });
120
- this.providedClient = options.client;
121
- this.region = options.region ?? "us-east-1";
122
- }
123
- /** The SDK and the client, loaded and created the first time the table is touched (see LambderDdbSdk). */
124
- ready() {
125
- this.readyPromise ??= loadDynamoClientSdk("LambderDdbCache")
126
- .then((sdk) => ({ sdk, client: this.providedClient ?? new sdk.DynamoDBClient({ region: this.region }) }))
127
- .catch((error) => { this.readyPromise = undefined; throw error; });
128
- return this.readyPromise;
133
+ this.now = options.now ?? (() => Date.now());
134
+ this.ready = createDynamoClientLoader({ user: "LambderDdbCache", region: options.region, client: options.client });
129
135
  }
130
136
  async get(key) {
131
137
  return await this.getByAddress(this.normalizeKey(key));
@@ -150,10 +156,10 @@ export class LambderDdbCache {
150
156
  try {
151
157
  const stored = manifest.inlineData ?? await this.readChunks(pk, address, manifest);
152
158
  if (stored.length !== manifest.storedBytes) {
153
- throw new Error("stored byte length does not match manifest");
159
+ throw new LambderCacheIntegrityError("stored byte length does not match manifest");
154
160
  }
155
161
  if (await sha256(stored) !== manifest.checksum) {
156
- throw new Error("stored checksum does not match manifest");
162
+ throw new LambderCacheIntegrityError("stored checksum does not match manifest");
157
163
  }
158
164
  const json = await this.decode(stored, manifest.encoding, manifest.uncompressedBytes);
159
165
  const parsed = JSON.parse(json);
@@ -161,6 +167,12 @@ export class LambderDdbCache {
161
167
  return parsed;
162
168
  }
163
169
  catch (error) {
170
+ // Only an entry this read can prove wrong is dropped. Everything
171
+ // else is the table answering badly, which is the manifest read's
172
+ // own behaviour one line up: it propagates, and getOrSet's
173
+ // fail-open handles it as the infrastructure failure it is.
174
+ if (!(error instanceof LambderCacheIntegrityError || error instanceof LambderCompressionError))
175
+ throw error;
164
176
  await this.invalidateManifest(pk, address, manifest.version);
165
177
  console.warn(`Ignoring corrupt DynamoDB cache entry in ${this.namespace}`, error);
166
178
  return undefined;
@@ -181,7 +193,7 @@ export class LambderDdbCache {
181
193
  return await this.setByAddress(this.normalizeKey(key), value, options);
182
194
  }
183
195
  async setByAddress(address, value, options) {
184
- const ttlSeconds = positiveInteger(options.ttlSeconds ?? this.defaultTtlSeconds, "ttlSeconds");
196
+ const ttlSeconds = assertPositiveInteger(options.ttlSeconds ?? this.defaultTtlSeconds, "ttlSeconds");
185
197
  const json = JSON.stringify(value);
186
198
  if (json === undefined)
187
199
  throw new Error("Cache value must be JSON-serializable");
@@ -272,7 +284,7 @@ export class LambderDdbCache {
272
284
  async listSortKeys(partition, options = {}) {
273
285
  const pk = await this.partitionKey(this.normalizePartition(partition));
274
286
  const prefix = `${SORT_KEY_MARKER}${encodeSortKey(options.prefix ?? "")}`;
275
- const limit = options.limit === undefined ? undefined : positiveInteger(options.limit, "limit");
287
+ const limit = options.limit === undefined ? undefined : assertPositiveInteger(options.limit, "limit");
276
288
  const nowSeconds = this.nowSeconds();
277
289
  const items = await this.queryItems(pk, { prefix, projection: "#sk, #expiresAt", extraNames: { "#expiresAt": "expiresAt" } });
278
290
  const sortKeys = [];
@@ -330,8 +342,8 @@ export class LambderDdbCache {
330
342
  }
331
343
  }
332
344
  async fill(address, factory, options) {
333
- const leaseSeconds = positiveInteger(options.leaseSeconds ?? 15, "leaseSeconds");
334
- const waitForFillMs = positiveInteger(options.waitForFillMs ?? 5_000, "waitForFillMs");
345
+ const leaseSeconds = assertPositiveInteger(options.leaseSeconds ?? 15, "leaseSeconds");
346
+ const waitForFillMs = assertPositiveInteger(options.waitForFillMs ?? 5_000, "waitForFillMs");
335
347
  const pk = await this.partitionKey(address.partition);
336
348
  const owner = await randomUUID();
337
349
  if (await this.acquireLease(pk, address, owner, leaseSeconds)) {
@@ -384,7 +396,7 @@ export class LambderDdbCache {
384
396
  return true;
385
397
  }
386
398
  catch (error) {
387
- if (this.isConditionalFailure(error))
399
+ if (isConditionalCheckFailure(error))
388
400
  return false;
389
401
  throw error;
390
402
  }
@@ -401,7 +413,7 @@ export class LambderDdbCache {
401
413
  }));
402
414
  }
403
415
  catch (error) {
404
- if (!this.isConditionalFailure(error)) {
416
+ if (!isConditionalCheckFailure(error)) {
405
417
  console.warn(`Failed to release DynamoDB cache lease in ${this.namespace}`, error);
406
418
  }
407
419
  }
@@ -470,11 +482,11 @@ export class LambderDdbCache {
470
482
  .map((item) => ({ sk: item.sk.S, data: Buffer.from(item.data.B) }));
471
483
  chunks.sort((left, right) => left.sk.localeCompare(right.sk));
472
484
  if (chunks.length !== manifest.chunkCount) {
473
- throw new Error(`DynamoDB cache entry is missing chunks (${chunks.length}/${manifest.chunkCount})`);
485
+ throw new LambderCacheIntegrityError(`DynamoDB cache entry is missing chunks (${chunks.length}/${manifest.chunkCount})`);
474
486
  }
475
487
  for (let index = 0; index < chunks.length; index += 1) {
476
488
  if (chunks[index]?.sk !== this.chunkSortKey(address, manifest.version, index)) {
477
- throw new Error(`DynamoDB cache entry has an invalid chunk index at ${index}`);
489
+ throw new LambderCacheIntegrityError(`DynamoDB cache entry has an invalid chunk index at ${index}`);
478
490
  }
479
491
  }
480
492
  return Buffer.concat(chunks.map((chunk) => chunk.data), manifest.storedBytes);
@@ -520,7 +532,7 @@ export class LambderDdbCache {
520
532
  }));
521
533
  }
522
534
  catch (error) {
523
- if (!this.isConditionalFailure(error)) {
535
+ if (!isConditionalCheckFailure(error)) {
524
536
  console.warn(`Failed to invalidate corrupt DynamoDB cache manifest in ${this.namespace}`, error);
525
537
  }
526
538
  }
@@ -549,7 +561,7 @@ export class LambderDdbCache {
549
561
  remember(key, stored, encoding, uncompressedBytes, expiresAt) {
550
562
  if (!this.memory)
551
563
  return;
552
- const ttl = expiresAt * 1000 - Date.now();
564
+ const ttl = expiresAt * 1000 - this.now();
553
565
  if (ttl <= 0)
554
566
  return;
555
567
  this.memory.set(key, { stored, encoding, uncompressedBytes, expiresAt }, { ttl });
@@ -616,9 +628,6 @@ export class LambderDdbCache {
616
628
  }
617
629
  }
618
630
  nowSeconds() {
619
- return Math.floor(Date.now() / 1000);
620
- }
621
- isConditionalFailure(error) {
622
- return !!error && typeof error === "object" && "name" in error && error.name === "ConditionalCheckFailedException";
631
+ return Math.floor(this.now() / 1000);
623
632
  }
624
633
  }