lambder 7.2.5 → 8.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (209) hide show
  1. package/CHANGELOG.md +1021 -3
  2. package/README.md +43 -21
  3. package/dist/api/LambderApiAnswer.d.ts +18 -22
  4. package/dist/api/LambderApiAnswer.js +6 -7
  5. package/dist/api/LambderApiCallContext.d.ts +21 -8
  6. package/dist/api/LambderApiCallContext.js +22 -4
  7. package/dist/api/LambderApiDefinition.d.ts +4 -3
  8. package/dist/api/LambderApiEnvelope.d.ts +14 -9
  9. package/dist/api/LambderApiEnvelope.js +33 -34
  10. package/dist/api/LambderApiGuards.d.ts +78 -51
  11. package/dist/api/LambderApiGuards.js +34 -36
  12. package/dist/api/LambderApiIdempotency.d.ts +74 -61
  13. package/dist/api/LambderApiIdempotency.js +226 -151
  14. package/dist/api/LambderApiOutputValidationError.d.ts +32 -0
  15. package/dist/api/LambderApiOutputValidationError.js +50 -0
  16. package/dist/api/LambderApiPipeline.d.ts +77 -39
  17. package/dist/api/LambderApiPipeline.js +135 -62
  18. package/dist/api/LambderApiRateLimits.d.ts +208 -54
  19. package/dist/api/LambderApiRateLimits.js +197 -108
  20. package/dist/api/LambderApiRequest.d.ts +27 -21
  21. package/dist/api/LambderApiRequest.js +26 -19
  22. package/dist/api/LambderApiSignature.d.ts +12 -15
  23. package/dist/api/LambderApiSignature.js +28 -51
  24. package/dist/api/LambderApiValidationRefusal.d.ts +9 -9
  25. package/dist/api/LambderApiValidationRefusal.js +10 -10
  26. package/dist/build/freshProcessVerifier.d.ts +13 -0
  27. package/dist/build/freshProcessVerifier.js +19 -0
  28. package/dist/build/writeApiSignatures.d.ts +109 -0
  29. package/dist/build/writeApiSignatures.js +222 -0
  30. package/dist/build.d.ts +9 -0
  31. package/dist/build.js +8 -0
  32. package/dist/client/LambderCaller.d.ts +13 -44
  33. package/dist/client/LambderCaller.js +77 -84
  34. package/dist/client/LambderReloadLoopBreaker.d.ts +56 -26
  35. package/dist/client/LambderReloadLoopBreaker.js +90 -46
  36. package/dist/client/lambderFetchTransport.d.ts +4 -1
  37. package/dist/client/lambderFetchTransport.js +52 -28
  38. package/dist/client.d.ts +5 -3
  39. package/dist/client.js +2 -1
  40. package/dist/core/Lambder.d.ts +161 -69
  41. package/dist/core/Lambder.js +370 -226
  42. package/dist/core/LambderContext.d.ts +82 -15
  43. package/dist/core/LambderContext.js +107 -20
  44. package/dist/core/LambderCors.d.ts +21 -3
  45. package/dist/core/LambderCors.js +35 -16
  46. package/dist/core/LambderCrashHandling.d.ts +40 -0
  47. package/dist/core/LambderCrashHandling.js +97 -0
  48. package/dist/core/LambderCreateOptions.d.ts +151 -75
  49. package/dist/core/LambderCreateOptions.js +16 -23
  50. package/dist/core/LambderFiles.d.ts +28 -7
  51. package/dist/core/LambderFiles.js +73 -33
  52. package/dist/core/LambderIndexHtml.js +12 -11
  53. package/dist/core/LambderPolicyBuilders.d.ts +17 -5
  54. package/dist/core/LambderPolicyBuilders.js +17 -5
  55. package/dist/core/LambderPublicFiles.d.ts +11 -5
  56. package/dist/core/LambderPublicFiles.js +32 -4
  57. package/dist/core/LambderRequestPath.d.ts +43 -0
  58. package/dist/core/LambderRequestPath.js +63 -0
  59. package/dist/core/LambderResponse.d.ts +26 -5
  60. package/dist/core/LambderResponse.js +157 -70
  61. package/dist/core/LambderResponseBuilder.d.ts +49 -4
  62. package/dist/core/LambderResponseBuilder.js +64 -3
  63. package/dist/core/LambderRouting.d.ts +2 -3
  64. package/dist/core/LambderRouting.js +22 -7
  65. package/dist/core/LambderTemplatingEngine.js +211 -32
  66. package/dist/index.d.ts +15 -8
  67. package/dist/index.js +5 -4
  68. package/dist/invoke/LambderInvokeCaller.d.ts +37 -42
  69. package/dist/invoke/LambderInvokeCaller.js +76 -66
  70. package/dist/invoke/LambderInvokeOutcome.d.ts +27 -26
  71. package/dist/invoke/LambderInvokeOutcome.js +9 -22
  72. package/dist/invoke/LambderLambdaEvent.d.ts +44 -10
  73. package/dist/invoke/LambderLambdaEvent.js +80 -37
  74. package/dist/invoke/lambderHandlerTransport.d.ts +12 -10
  75. package/dist/invoke/lambderHandlerTransport.js +16 -19
  76. package/dist/mock/LambderMockApp.d.ts +67 -83
  77. package/dist/mock/LambderMockApp.js +167 -153
  78. package/dist/mock/LambderMockBrowserCookies.d.ts +24 -28
  79. package/dist/mock/LambderMockBrowserCookies.js +24 -28
  80. package/dist/mock/LambderMockCallRecorder.d.ts +15 -22
  81. package/dist/mock/LambderMockCallRecorder.js +19 -28
  82. package/dist/mock/LambderMockCreateOptions.d.ts +42 -24
  83. package/dist/mock/LambderMockEntryRegistry.d.ts +11 -12
  84. package/dist/mock/LambderMockEntryRegistry.js +24 -29
  85. package/dist/mock/LambderMockFailureInjector.d.ts +3 -6
  86. package/dist/mock/LambderMockFailureInjector.js +3 -6
  87. package/dist/mock/LambderMockTypes.d.ts +78 -108
  88. package/dist/mock/lambderMockInvokeTransport.d.ts +11 -13
  89. package/dist/mock/lambderMockInvokeTransport.js +11 -10
  90. package/dist/mock/lambderMockMswHandler.d.ts +33 -29
  91. package/dist/mock/lambderMockMswHandler.js +50 -39
  92. package/dist/mock.d.ts +3 -1
  93. package/dist/mock.js +5 -3
  94. package/dist/session/LambderSessionController.d.ts +108 -89
  95. package/dist/session/LambderSessionController.js +187 -168
  96. package/dist/session/LambderSessionCrypto.d.ts +16 -7
  97. package/dist/session/LambderSessionCrypto.js +26 -12
  98. package/dist/session/LambderSessionManager.d.ts +136 -47
  99. package/dist/session/LambderSessionManager.js +280 -139
  100. package/dist/shared/LambderHtml.d.ts +42 -3
  101. package/dist/shared/LambderHtml.js +127 -7
  102. package/dist/shared/LambderHtmlPositions.d.ts +173 -0
  103. package/dist/shared/LambderHtmlPositions.js +652 -0
  104. package/dist/shared/LambderI18n.d.ts +10 -11
  105. package/dist/shared/LambderI18n.js +33 -21
  106. package/dist/shared/contracts/LambderCache.d.ts +66 -0
  107. package/dist/shared/contracts/LambderCache.js +11 -0
  108. package/dist/shared/contracts/LambderFileSource.d.ts +6 -6
  109. package/dist/shared/contracts/LambderFileSource.js +5 -8
  110. package/dist/shared/contracts/LambderIdempotencyStore.d.ts +51 -22
  111. package/dist/shared/contracts/LambderIdempotencyStore.js +4 -5
  112. package/dist/shared/contracts/LambderRateLimiter.d.ts +27 -15
  113. package/dist/shared/contracts/LambderRateLimiter.js +4 -5
  114. package/dist/shared/contracts/LambderSessionStore.d.ts +65 -26
  115. package/dist/shared/contracts/LambderSessionStore.js +5 -6
  116. package/dist/shared/transport/LambderApiTransport.d.ts +27 -27
  117. package/dist/shared/transport/LambderApiTransport.js +7 -7
  118. package/dist/shared/transport/LambderCookieJar.d.ts +28 -35
  119. package/dist/shared/transport/LambderCookieJar.js +54 -66
  120. package/dist/shared/transport/lambderCookieJarTransport.d.ts +11 -13
  121. package/dist/shared/transport/lambderCookieJarTransport.js +24 -23
  122. package/dist/shared/util/LambderCallAbort.d.ts +5 -5
  123. package/dist/shared/util/LambderCallAbort.js +5 -5
  124. package/dist/shared/util/LambderClientIp.d.ts +27 -11
  125. package/dist/shared/util/LambderClientIp.js +96 -13
  126. package/dist/shared/util/LambderExpiringMap.d.ts +35 -49
  127. package/dist/shared/util/LambderExpiringMap.js +41 -57
  128. package/dist/shared/util/LambderNodeModules.js +6 -7
  129. package/dist/shared/util/LambderOptionChecks.d.ts +4 -4
  130. package/dist/shared/util/LambderOptionChecks.js +4 -4
  131. package/dist/shared/util/LambderResponseBrand.d.ts +5 -5
  132. package/dist/shared/util/LambderResponseBrand.js +5 -5
  133. package/dist/shared/util/LambderTestingDoors.d.ts +29 -0
  134. package/dist/shared/util/LambderTestingDoors.js +29 -0
  135. package/dist/shared/util/LambderTypeUtilities.d.ts +7 -8
  136. package/dist/shared/util/LambderTypeUtilities.js +3 -3
  137. package/dist/shared/util/boundKeyField.d.ts +20 -0
  138. package/dist/shared/util/boundKeyField.js +34 -0
  139. package/dist/shared/util/canonicalJson.d.ts +11 -0
  140. package/dist/shared/util/canonicalJson.js +28 -0
  141. package/dist/shared/util/joinKeyFields.d.ts +20 -0
  142. package/dist/shared/util/joinKeyFields.js +22 -0
  143. package/dist/shared/wire/LambderAnswerHeaders.d.ts +12 -16
  144. package/dist/shared/wire/LambderAnswerHeaders.js +12 -16
  145. package/dist/shared/wire/LambderApiContract.d.ts +107 -32
  146. package/dist/shared/wire/LambderApiOutcome.d.ts +43 -31
  147. package/dist/shared/wire/LambderApiOutcome.js +48 -23
  148. package/dist/shared/wire/LambderApiRefusal.d.ts +39 -27
  149. package/dist/shared/wire/LambderApiRefusal.js +36 -7
  150. package/dist/shared/wire/LambderApiSignature.d.ts +18 -22
  151. package/dist/shared/wire/LambderApiSignature.js +16 -19
  152. package/dist/shared/wire/LambderCallOptions.d.ts +38 -47
  153. package/dist/shared/wire/LambderCallOptions.js +9 -11
  154. package/dist/shared/wire/LambderCompressionCodec.d.ts +29 -34
  155. package/dist/shared/wire/LambderCompressionCodec.js +31 -36
  156. package/dist/shared/wire/LambderCompressionOption.d.ts +9 -9
  157. package/dist/shared/wire/LambderCompressionOption.js +9 -9
  158. package/dist/shared/wire/LambderCrashDetail.d.ts +12 -15
  159. package/dist/shared/wire/LambderCrashDetail.js +12 -15
  160. package/dist/shared/wire/LambderDefaultApiPath.d.ts +6 -0
  161. package/dist/shared/wire/LambderDefaultApiPath.js +6 -0
  162. package/dist/shared/wire/LambderHttpStatus.d.ts +6 -7
  163. package/dist/shared/wire/LambderIdempotencyKeyScope.d.ts +89 -0
  164. package/dist/shared/wire/LambderIdempotencyKeyScope.js +146 -0
  165. package/dist/shared/wire/LambderInvokeApiId.d.ts +27 -0
  166. package/dist/shared/wire/LambderInvokeApiId.js +27 -0
  167. package/dist/shared/wire/LambderOutcomeAssertions.d.ts +79 -0
  168. package/dist/shared/wire/LambderOutcomeAssertions.js +112 -0
  169. package/dist/shared/wire/LambderRequestPayload.d.ts +18 -20
  170. package/dist/shared/wire/LambderRequestPayload.js +4 -6
  171. package/dist/stores/LambderCacheFiller.d.ts +48 -0
  172. package/dist/stores/LambderCacheFiller.js +119 -0
  173. package/dist/stores/LambderCacheKeys.d.ts +26 -0
  174. package/dist/stores/LambderCacheKeys.js +54 -0
  175. package/dist/stores/LambderCacheValues.d.ts +45 -0
  176. package/dist/stores/LambderCacheValues.js +74 -0
  177. package/dist/stores/LambderDdbCache.d.ts +121 -56
  178. package/dist/stores/LambderDdbCache.js +528 -225
  179. package/dist/stores/LambderDdbIdempotencyStore.d.ts +33 -22
  180. package/dist/stores/LambderDdbIdempotencyStore.js +75 -50
  181. package/dist/stores/LambderDdbRateLimiter.d.ts +76 -20
  182. package/dist/stores/LambderDdbRateLimiter.js +151 -39
  183. package/dist/stores/LambderDdbSdk.d.ts +43 -31
  184. package/dist/stores/LambderDdbSdk.js +79 -33
  185. package/dist/stores/LambderDdbSessionStore.d.ts +27 -14
  186. package/dist/stores/LambderDdbSessionStore.js +119 -47
  187. package/dist/stores/LambderHttpFileSource.d.ts +15 -6
  188. package/dist/stores/LambderHttpFileSource.js +15 -13
  189. package/dist/stores/LambderMemoryCache.d.ts +49 -0
  190. package/dist/stores/LambderMemoryCache.js +113 -0
  191. package/dist/stores/LambderMemoryIdempotencyStore.d.ts +13 -12
  192. package/dist/stores/LambderMemoryIdempotencyStore.js +31 -30
  193. package/dist/stores/LambderMemoryRateLimiter.d.ts +8 -9
  194. package/dist/stores/LambderMemoryRateLimiter.js +14 -13
  195. package/dist/stores/LambderMemorySessionStore.d.ts +14 -11
  196. package/dist/stores/LambderMemorySessionStore.js +38 -19
  197. package/dist/stores/LambderS3FileSource.d.ts +21 -6
  198. package/dist/stores/LambderS3FileSource.js +12 -7
  199. package/dist/testing/LambderTestApp.d.ts +176 -0
  200. package/dist/testing/LambderTestApp.js +204 -0
  201. package/dist/testing/LambderTestVisitor.d.ts +153 -0
  202. package/dist/testing/LambderTestVisitor.js +154 -0
  203. package/dist/testing.d.ts +27 -0
  204. package/dist/testing.js +24 -0
  205. package/package.json +20 -3
  206. package/dist/api/LambderApiPolicyEngine.d.ts +0 -36
  207. package/dist/api/LambderApiPolicyEngine.js +0 -77
  208. package/dist/shared/util/LambderKeyFields.d.ts +0 -32
  209. package/dist/shared/util/LambderKeyFields.js +0 -34
@@ -1,8 +1,8 @@
1
1
  /**
2
2
  * The small type utilities more than one module needs.
3
3
  *
4
- * Nothing here is Lambder's own vocabulary: these are the shapes TypeScript
5
- * does not ship, written once because the alternative is the same three
6
- * lines in every module that wants them, drifting in name and in meaning.
4
+ * Nothing here is Lambder's own vocabulary: these are shapes TypeScript does
5
+ * not ship, written once so they cannot drift in name or meaning across the
6
+ * modules that use them.
7
7
  */
8
8
  export {};
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Bounding the caller-supplied field of a tracker or scope key.
3
+ *
4
+ * The rate-limit engine and the idempotency engine each build a store key
5
+ * around a field only the caller controls: a custom rate-limit key or a
6
+ * session key, a callerIdentity or a session key. A store has a key limit of
7
+ * its own (a DynamoDB partition key stops at 2048 bytes) and refuses a key
8
+ * past it by throwing, and both engines fail open on a store throw by
9
+ * default: a long enough field (a 3,000-character email, a device token)
10
+ * would turn the rate limit or the idempotency off for that caller, in
11
+ * silence, while the table held the field in plain text. So the field is
12
+ * bounded before any store sees it, in one implementation the two engines
13
+ * share, so they cannot drift apart.
14
+ */
15
+ /**
16
+ * The field bounded: `<kind>:<value>` while the value fits, `<kind>:h:<sha256
17
+ * hex>` once it does not. The digest keeps distinct callers on distinct
18
+ * counters and scopes, and a value that fits stays readable in the table.
19
+ */
20
+ export declare const boundKeyField: (kind: string, value: string) => Promise<string>;
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Bounding the caller-supplied field of a tracker or scope key.
3
+ *
4
+ * The rate-limit engine and the idempotency engine each build a store key
5
+ * around a field only the caller controls: a custom rate-limit key or a
6
+ * session key, a callerIdentity or a session key. A store has a key limit of
7
+ * its own (a DynamoDB partition key stops at 2048 bytes) and refuses a key
8
+ * past it by throwing, and both engines fail open on a store throw by
9
+ * default: a long enough field (a 3,000-character email, a device token)
10
+ * would turn the rate limit or the idempotency off for that caller, in
11
+ * silence, while the table held the field in plain text. So the field is
12
+ * bounded before any store sees it, in one implementation the two engines
13
+ * share, so they cannot drift apart.
14
+ */
15
+ import { joinKeyFields } from "./joinKeyFields.js";
16
+ import { sha256HexOf } from "./LambderTextDigest.js";
17
+ /**
18
+ * The ceiling, in UTF-8 bytes, on the field as it is written into the key;
19
+ * past it, the field is replaced by its digest. Measured escaped, since
20
+ * joinKeyFields doubles every separator and escape character: a field of
21
+ * 1,000 separators is 2,000 bytes in the key. 1024 sits well inside every
22
+ * store's key limit, with the store's prefix and the engine's other fields
23
+ * (API and policy names, a posted idempotency key of at most 200 characters)
24
+ * joined around it.
25
+ */
26
+ const MAX_KEY_FIELD_BYTES = 1024;
27
+ /**
28
+ * The field bounded: `<kind>:<value>` while the value fits, `<kind>:h:<sha256
29
+ * hex>` once it does not. The digest keeps distinct callers on distinct
30
+ * counters and scopes, and a value that fits stays readable in the table.
31
+ */
32
+ export const boundKeyField = async (kind, value) => new TextEncoder().encode(joinKeyFields(value)).length > MAX_KEY_FIELD_BYTES
33
+ ? `${kind}:h:${await sha256HexOf(value)}`
34
+ : `${kind}:${value}`;
@@ -0,0 +1,11 @@
1
+ /**
2
+ * JSON with object keys sorted at every level, so two values that are the
3
+ * same data hash the same whatever order their keys were built in. Arrays
4
+ * keep their order: a tuple's positions and an enum's values are part of the
5
+ * data. Undefined entries are dropped, as JSON.stringify would drop them.
6
+ *
7
+ * What the API signature digests a schema's description with, and what the
8
+ * idempotency engine fingerprints a request with: two hashes that must not
9
+ * depend on the order a client or a builder happened to write keys in.
10
+ */
11
+ export declare const canonicalJson: (value: unknown) => string;
@@ -0,0 +1,28 @@
1
+ /**
2
+ * JSON with object keys sorted at every level, so two values that are the
3
+ * same data hash the same whatever order their keys were built in. Arrays
4
+ * keep their order: a tuple's positions and an enum's values are part of the
5
+ * data. Undefined entries are dropped, as JSON.stringify would drop them.
6
+ *
7
+ * What the API signature digests a schema's description with, and what the
8
+ * idempotency engine fingerprints a request with: two hashes that must not
9
+ * depend on the order a client or a builder happened to write keys in.
10
+ */
11
+ export const canonicalJson = (value) => JSON.stringify(sortKeys(value));
12
+ const sortKeys = (value) => {
13
+ if (Array.isArray(value))
14
+ return value.map(sortKeys);
15
+ if (value === null || typeof value !== "object")
16
+ return value;
17
+ const source = value;
18
+ // No prototype, so a "__proto__" key (JSON.parse makes it an own key) is
19
+ // kept as data. On a plain object the assignment would set the prototype
20
+ // instead, and two payloads differing only under "__proto__" would share
21
+ // one fingerprint.
22
+ const sorted = Object.create(null);
23
+ for (const key of Object.keys(source).sort()) {
24
+ if (source[key] !== undefined)
25
+ sorted[key] = sortKeys(source[key]);
26
+ }
27
+ return sorted;
28
+ };
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Joining the fields of a tracker or scope key.
3
+ *
4
+ * The rate limiter and the idempotency engine each join fields with a
5
+ * separator, and at least one field is caller data (a policy's rate-limit
6
+ * key, a posted idempotency key). A plain join lets two field lists produce
7
+ * one string, so two callers would share a counter or one would read
8
+ * another's stored answer. The caller's separator is escaped, not refused: a
9
+ * limit that rejects a legal key is a bug of its own.
10
+ *
11
+ * The join is one-way (a key is looked up or compared, never taken apart),
12
+ * unlike LambderDdbCache's reversible sort-key escape, which listSortKeys
13
+ * must decode to exactly what was written; neither stands in for the other.
14
+ * Both engines share this one implementation so they cannot drift apart.
15
+ */
16
+ /**
17
+ * The fields joined into one key, each escaped, so no two distinct field
18
+ * lists can produce the same string.
19
+ */
20
+ export declare const joinKeyFields: (...fields: string[]) => string;
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Joining the fields of a tracker or scope key.
3
+ *
4
+ * The rate limiter and the idempotency engine each join fields with a
5
+ * separator, and at least one field is caller data (a policy's rate-limit
6
+ * key, a posted idempotency key). A plain join lets two field lists produce
7
+ * one string, so two callers would share a counter or one would read
8
+ * another's stored answer. The caller's separator is escaped, not refused: a
9
+ * limit that rejects a legal key is a bug of its own.
10
+ *
11
+ * The join is one-way (a key is looked up or compared, never taken apart),
12
+ * unlike LambderDdbCache's reversible sort-key escape, which listSortKeys
13
+ * must decode to exactly what was written; neither stands in for the other.
14
+ * Both engines share this one implementation so they cannot drift apart.
15
+ */
16
+ /** One field, with the separator and its own escape character escaped. */
17
+ const escapeKeyField = (value) => value.replace(/\\/g, "\\\\").replace(/\|/g, "\\|");
18
+ /**
19
+ * The fields joined into one key, each escaped, so no two distinct field
20
+ * lists can produce the same string.
21
+ */
22
+ export const joinKeyFields = (...fields) => fields.map(escapeKeyField).join("|");
@@ -4,9 +4,9 @@
4
4
  * call wrote so it can be applied onto whichever answer the call ends up
5
5
  * with.
6
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.
7
+ * One implementation at the bottom of the stack, because every layer above
8
+ * needs the same behavior: LambderResponse's own header methods are these
9
+ * three functions, so a second copy cannot drift from them.
10
10
  */
11
11
  /** The header's values under a case-insensitive lookup, or undefined. */
12
12
  export declare const getAnswerHeader: (headers: Record<string, string[]>, name: string) => string[] | undefined;
@@ -25,18 +25,15 @@ export type LambderHeaderTarget = {
25
25
  * the session controller's Set-Cookie), applied onto the answer once the
26
26
  * call has one. Recorded as operations in call order rather than as a map,
27
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.
28
+ * `add` appends to it, exactly as if called on the answer directly.
30
29
  *
31
30
  * 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.
31
+ * them: an afterRender hook may answer with a different response than the
32
+ * handler produced, and a session cookie written during the call must travel
33
+ * to it. So applying never forgets the operations, and applying them twice
34
+ * is a no-op (`add` skips a value the header already carries). The cost is
35
+ * that two identical `add` values under one name collapse to one, which HTTP
36
+ * gives no meaning to anyway.
40
37
  */
41
38
  export declare class LambderAnswerHeaders {
42
39
  private operations;
@@ -50,9 +47,8 @@ export declare class LambderAnswerHeaders {
50
47
  get size(): number;
51
48
  /**
52
49
  * 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.
50
+ * writes headers: a LambderResponse, or a header map through applyInto,
51
+ * so both targets share one definition of what an operation does.
56
52
  */
57
53
  applyTo(target: LambderHeaderTarget, fromIndex?: number): void;
58
54
  /** The same, onto an answer's plain header map. */
@@ -4,9 +4,9 @@
4
4
  * call wrote so it can be applied onto whichever answer the call ends up
5
5
  * with.
6
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.
7
+ * One implementation at the bottom of the stack, because every layer above
8
+ * needs the same behavior: LambderResponse's own header methods are these
9
+ * three functions, so a second copy cannot drift from them.
10
10
  */
11
11
  /** The header's values under a case-insensitive lookup, or undefined. */
12
12
  export const getAnswerHeader = (headers, name) => {
@@ -40,18 +40,15 @@ export const addAnswerHeader = (headers, name, value) => {
40
40
  * the session controller's Set-Cookie), applied onto the answer once the
41
41
  * call has one. Recorded as operations in call order rather than as a map,
42
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.
43
+ * `add` appends to it, exactly as if called on the answer directly.
45
44
  *
46
45
  * 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.
46
+ * them: an afterRender hook may answer with a different response than the
47
+ * handler produced, and a session cookie written during the call must travel
48
+ * to it. So applying never forgets the operations, and applying them twice
49
+ * is a no-op (`add` skips a value the header already carries). The cost is
50
+ * that two identical `add` values under one name collapse to one, which HTTP
51
+ * gives no meaning to anyway.
55
52
  */
56
53
  export class LambderAnswerHeaders {
57
54
  operations = [];
@@ -69,9 +66,8 @@ export class LambderAnswerHeaders {
69
66
  get size() { return this.operations.length; }
70
67
  /**
71
68
  * 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.
69
+ * writes headers: a LambderResponse, or a header map through applyInto,
70
+ * so both targets share one definition of what an operation does.
75
71
  */
76
72
  applyTo(target, fromIndex = 0) {
77
73
  for (const operation of this.operations.slice(fromIndex)) {
@@ -38,7 +38,14 @@ export type LambderApiResponseConfig = {
38
38
  sessionExpired?: boolean;
39
39
  notAuthorized?: boolean;
40
40
  message?: any;
41
- /** A refusal message, or a plain string: what LambderApiRefusal and res.api(null, { errorMessage }) put here. */
41
+ /**
42
+ * A refusal message, or a plain string when writing
43
+ * (`res.api(null, { errorMessage: "..." })`): the envelope goes out with
44
+ * the message object either way. Read off the wire it can still be a
45
+ * string, or no message at all, wherever a Lambder server did not write
46
+ * the body (a hand-built mock answer, a proxy); refusalMessageOf reads
47
+ * whatever arrives as a message.
48
+ */
42
49
  errorMessage?: LambderAppRefusalMessage | string;
43
50
  logList?: any[];
44
51
  /**
@@ -57,16 +64,76 @@ export type LambderApiResponseConfig = {
57
64
  * indistinguishable from an endpoint that answered nothing on purpose.
58
65
  */
59
66
  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. */
67
+ /**
68
+ * The API wire envelope both sides speak: res.api() emits it, LambderCaller
69
+ * parses it. `apiVersion` is always there (null when the server set none):
70
+ * it is how a reader tells a Lambder envelope from another JSON answer, such
71
+ * as API Gateway's own `{ "message": ... }` errors, so an answer without it
72
+ * reads as a server failure.
73
+ */
61
74
  export type LambderApiEnvelopeBody<T> = LambderApiResponseConfig & {
62
- apiVersion?: string | null;
75
+ apiVersion: string | null;
63
76
  payload?: T | null;
64
77
  };
78
+ /** A value that is already JSON, recursive structures such as z.json() included. */
79
+ type LambderJsonValue = string | number | boolean | null | LambderJsonValue[] | {
80
+ [key: string]: LambderJsonValue;
81
+ };
82
+ /** An array item once it has been through JSON: what an object would drop, an array writes as null. */
83
+ type LambderJsonArrayItemOf<T> = T extends undefined | symbol | ((...args: any[]) => unknown) ? null : LambderJsonOf<T>;
84
+ /**
85
+ * The keys of object T that JSON may leave out: those whose value may be
86
+ * undefined, since JSON.stringify omits such a key. Distributed over K, the
87
+ * keys of T, one at a time. An index signature is never one of them: a
88
+ * record's undefined entries are left out, which its value type already
89
+ * says once undefined is dropped from it.
90
+ */
91
+ type LambderJsonOmissibleKeys<T, K extends keyof T = keyof T> = K extends keyof T ? (string extends K ? never : number extends K ? never : undefined extends T[K] ? K : never) : never;
92
+ /**
93
+ * The type a value has once it has been through JSON: what an API's output
94
+ * reaches a client as. A Date becomes its string (through toJSON), a
95
+ * function, a symbol or an undefined member is dropped (written as null in
96
+ * an array), and a bigint, which JSON.stringify refuses, is never. A key
97
+ * whose value may be undefined is optional, since JSON leaves it out then,
98
+ * and a Map or a Set, whose entries are not properties, is written as an
99
+ * empty object. `unknown` stays unknown, and a type that is already JSON
100
+ * maps to itself, which is also what lets a recursive one such as z.json()
101
+ * resolve.
102
+ *
103
+ * An object is mapped in two steps. The omissible keys are made optional
104
+ * first, through the key types alone, and the mapping over the result then
105
+ * keeps each key's modifiers. Deciding optionality inside the mapping, per
106
+ * key, would need the mapped value of every key before the object's own
107
+ * keys were known, which a recursive type (a tree of its own nodes) cannot
108
+ * give without recursing for ever.
109
+ */
110
+ export type LambderJsonOf<T> = unknown extends T ? T : T extends LambderJsonValue ? T : T extends {
111
+ toJSON(): infer TJson;
112
+ } ? LambderJsonOf<TJson> : T extends undefined | bigint | symbol | ((...args: any[]) => unknown) ? never : T extends readonly unknown[] ? {
113
+ [K in keyof T]: LambderJsonArrayItemOf<T[K]>;
114
+ } : T extends ReadonlyMap<unknown, unknown> | ReadonlySet<unknown> ? {} : T extends object ? (Partial<Pick<T, LambderJsonOmissibleKeys<T>>> & Omit<T, LambderJsonOmissibleKeys<T>>) extends infer TKeyed ? {
115
+ [K in keyof TKeyed as K extends string | number ? (string extends K ? K : number extends K ? K : [LambderJsonOf<TKeyed[K]>] extends [never] ? never : K) : never]: LambderJsonOf<TKeyed[K]>;
116
+ } : never : never;
117
+ /**
118
+ * What an API's output reaches the client as: LambderJsonOf of the schema's
119
+ * output, except at the top, where an envelope carries no payload at all
120
+ * rather than a JSON `undefined`. A void or undefined output keeps its type,
121
+ * so a handler and a mock handler answer nothing, and an output that may be
122
+ * undefined keeps that member, which LambderJsonOf drops as it would a
123
+ * member of an object.
124
+ */
125
+ export type LambderJsonOutputOf<T> = [T] extends [void] ? T : (undefined extends T ? undefined : never) | LambderJsonOf<T>;
65
126
  /**
66
127
  * One contract entry as addApi/addSessionApi record it: the payload types,
67
128
  * the mode, and every declarative option exactly as written. Options that
68
129
  * were not written are absent rather than undefined, so `keyof` an entry
69
130
  * lists only what the endpoint declared.
131
+ *
132
+ * `In` is what a client sends (the input schema's z.input: a field with a
133
+ * default is optional, a transform's source type is what is posted) and `Out`
134
+ * what it receives (the output schema's z.output as JSON, see
135
+ * LambderJsonOf). The handler's own types are the other side of each, and
136
+ * are not recorded here.
70
137
  */
71
138
  export type LambderContractEntry<In, Out, Mode extends LambderApiMode, GuardInputs = never, Guards = never, RateLimit = never, Idempotency = never> = {
72
139
  input: In;
@@ -94,43 +161,33 @@ export type LambderMergeContract<Old, Name extends string, Entry> = Old & {
94
161
  * ```
95
162
  *
96
163
  * Chaining leaves the contract an intersection one member deep per endpoint
97
- * (LambderMergeContract above), and every `C[K]` written against a type
98
- * parameter then resolves the property across all of them. That lookup is
99
- * the atom the reading helpers below are built from, so its cost is paid
100
- * again by each of them, per endpoint, in every app that registers a mock,
101
- * declares a needs map, or otherwise reads the contract generically: in a
102
- * 182-endpoint app one indexed access measured ~3,000 type instantiations
103
- * and one mock registration ~18,000.
164
+ * (LambderMergeContract above), and every `C[K]` against a type parameter
165
+ * resolves the property across all of them. The reading helpers below are
166
+ * built on that lookup, so each pays it again per endpoint: in a 182-endpoint
167
+ * app one indexed access costs ~3,000 type instantiations and one mock
168
+ * registration ~18,000.
104
169
  *
105
- * Extending an interface is what collapses it. An interface's members are
106
- * declared, so they are resolved once for the whole declaration rather than
107
- * per lookup, and the same access measured ~6 instantiations after the
108
- * change: a 182-endpoint app's frontend type check went from 27.8M
109
- * instantiations to 7.0M and from 20.2s to 10.6s of check time. The alias
110
- * form (`type C = LambderFlattenContract<...>`) does NOT do this: a mapped
111
- * type stays deferred and each lookup pays the full cost again, so the
112
- * `interface ... extends` spelling is the point.
113
- *
114
- * Diagnostics are the same ones, and they read better: a message naming the
115
- * contract prints the interface by name, where the intersection is printed
116
- * as a truncated spill of entries.
170
+ * An interface's members are declared, so they resolve once for the whole
171
+ * declaration: the same access costs ~6 instantiations instead, roughly
172
+ * halving such an app's frontend type check time. The alias form
173
+ * (`type C = LambderFlattenContract<...>`) does NOT do this: a mapped type
174
+ * stays deferred and each lookup pays in full, so the `interface ... extends`
175
+ * spelling is the point. Diagnostics also print the interface by name rather
176
+ * than a truncated spill of entries.
117
177
  *
118
178
  * Every endpoint name must be a string literal for an interface to extend
119
179
  * the result, which registration through addApi/addSessionApi guarantees.
120
180
  *
121
- * Two things quietly undo it, both of which look like tidying:
181
+ * Two things that look like tidying undo it:
122
182
  *
123
183
  * - `@typescript-eslint/no-empty-object-type` reports the empty body as
124
- * "equivalent to its supertype" and its fix is a type alias, which is the
125
- * one spelling that does not collapse anything. Disable the rule on the
126
- * line rather than taking the fix.
184
+ * "equivalent to its supertype" and its fix is a type alias, the one
185
+ * spelling that collapses nothing. Disable the rule on the line instead.
127
186
  * - Extending anything but a mapped type loses the inferable index signature.
128
- * An interface has none of its own, so a hand-written `interface C { ... }`
129
- * is not assignable to LambderApiContractShape and is rejected by
130
- * initLambderMock<C>, LambderCaller<C> and LambderInvokeCaller<C>;
131
- * extending this mapped type is what keeps it. api-contract.test.ts pins
132
- * that, along with the flattened contract being the same type member for
133
- * member.
187
+ * A hand-written `interface C { ... }` has none, so it is not assignable to
188
+ * LambderApiContractShape, and initLambderMock<C>, LambderCaller<C> and
189
+ * LambderInvokeCaller<C> reject it. api-contract.test.ts pins this, and
190
+ * that the flattened contract is the same type member for member.
134
191
  */
135
192
  export type LambderFlattenContract<C> = {
136
193
  [K in keyof C]: C[K];
@@ -149,6 +206,23 @@ export type LambderContractKeysWithMode<C, M extends LambderApiMode> = {
149
206
  export type LambderContractGuardsOf<C, K extends keyof C> = C[K] extends {
150
207
  guards: infer G;
151
208
  } ? G : never;
209
+ /**
210
+ * The endpoint names whose guards option names guard N, in any of its three
211
+ * forms and whatever else it declares beside it. What a test that calls
212
+ * every endpoint behind one guard loops over, and what a list meant to hold
213
+ * exactly those endpoints is checked against. `satisfies` refuses a name the
214
+ * guard does not cover; a missing name needs a check of its own:
215
+ *
216
+ * ```ts
217
+ * type AdminApi = LambderContractKeysWithGuard<Contract, "platformAdmin">;
218
+ * const ADMIN_APIS = ["admin.listUsers", "admin.deleteUser"] as const satisfies readonly AdminApi[];
219
+ * // Fails to compile while an endpoint behind the guard is left off the list.
220
+ * const adminApisComplete: [Exclude<AdminApi, (typeof ADMIN_APIS)[number]>] extends [never] ? true : false = true;
221
+ * ```
222
+ */
223
+ export type LambderContractKeysWithGuard<C, N extends string> = {
224
+ [K in keyof C]: N extends LambderGuardNamesIn<LambderContractGuardsOf<C, K>> ? K : never;
225
+ }[keyof C] & string;
152
226
  /** Every guard name any endpoint of the contract declares, or only the endpoints of mode M. */
153
227
  export type LambderContractGuardNames<C, M extends LambderApiMode = LambderApiMode> = {
154
228
  [K in LambderContractKeysWithMode<C, M>]: LambderGuardNamesIn<LambderContractGuardsOf<C, K>>;
@@ -181,3 +255,4 @@ export type LambderContractRateLimitNames<C, M extends LambderApiMode = LambderA
181
255
  export type LambderContractIdempotencyOf<C, K extends keyof C> = C[K] extends {
182
256
  idempotency: infer I;
183
257
  } ? I : never;
258
+ export {};
@@ -5,14 +5,13 @@
5
5
  * over a direct Lambda invoke) receive the same envelope and must read it
6
6
  * the same way: which status is a crash, which is a rejected input, in what
7
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.
8
+ * Both hand their answer to resolveApiOutcome and act on the result; their
9
+ * side effects (handlers, cookie clearing, error reporting) stay with them.
10
+ * Pure and dependency-free, so the browser entry can include it.
12
11
  */
13
12
  import type { z } from "zod";
14
13
  import type { LambderApiEnvelopeBody } from "./LambderApiContract.js";
15
- import type { LambderAppRefusalMessage } from "./LambderApiRefusal.js";
14
+ import { type LambderAppRefusalMessage } from "./LambderApiRefusal.js";
16
15
  /**
17
16
  * The 422 body's `zodError` as it survives JSON: a ZodError's name and
18
17
  * message, and its issues spelled out. Not a ZodError instance (it has no
@@ -37,18 +36,17 @@ type LambderApiFailureFields = {
37
36
  ok: false;
38
37
  /** HTTP status, when a response was received. */
39
38
  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). */
39
+ /** Envelope errorMessage, when the server provided one: always the message object, a plain string having been read as one (refusalMessageOf). */
40
+ errorMessage?: LambderAppRefusalMessage;
41
+ /** Seconds to wait before retrying, from the response's Retry-After header (rate-limit refusals send it, and so may a 503). */
43
42
  retryAfterSeconds?: number;
44
43
  /**
45
44
  * The answer's logList, when it carried one: the envelope's on a success
46
45
  * 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.
46
+ * the validation body's on a 422. It is on every arm so a caller surfaces
47
+ * logs in ONE place, right after reading the answer, rather than per
48
+ * outcome, where early returns would skip the logs that matter most: a
49
+ * 500's.
52
50
  */
53
51
  logList?: unknown[];
54
52
  };
@@ -69,30 +67,32 @@ export type LambderApiValidationFailure = LambderApiFailureFields & {
69
67
  reason: 'validation';
70
68
  zodError: LambderValidationError;
71
69
  };
72
- /** The server answered, and the envelope itself says the call is refused. Always carries that envelope. */
70
+ /** The server answered, and the envelope itself says the call is refused. Always carries that envelope, and an `errorMessage` refusal always carries its message. */
73
71
  export type LambderApiEnvelopeFailure<T> = LambderApiFailureFields & {
74
- reason: 'versionExpired' | 'sessionExpired' | 'notAuthorized' | 'errorMessage';
75
72
  response: LambderApiEnvelopeBody<T>;
76
- };
73
+ } & ({
74
+ reason: 'versionExpired' | 'sessionExpired' | 'notAuthorized';
75
+ } | {
76
+ reason: 'errorMessage';
77
+ errorMessage: LambderAppRefusalMessage;
78
+ });
77
79
  /**
78
80
  * Discriminated result of an API call: `ok: true` carries the payload, every
79
81
  * failure carries a machine-readable reason, so "the server returned null"
80
82
  * and "the request failed" are never conflated.
81
83
  *
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.
84
+ * The failure side is discriminated by `reason`, so narrowing to a reason
85
+ * narrows to what it carries: `zodError` after `reason === 'validation'`,
86
+ * `response` after an envelope reason, `error` after the rest, with no
87
+ * non-null assertion needed.
88
88
  */
89
89
  export type LambderApiOutcome<T> = LambderApiSuccessOutcome<T> | LambderApiCallFailure<T> | LambderApiValidationFailure | LambderApiEnvelopeFailure<T>;
90
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.
91
+ * What reading one HTTP answer can produce: LambderApiOutcome minus the
92
+ * three reasons no answer carries (`network` and `timeout` are the caller's
93
+ * own abort, `unknown` is something throwing around the call). A caller that
94
+ * has handled `server` and `validation` holds a success or an envelope
95
+ * refusal, both of which carry the envelope.
96
96
  */
97
97
  export type LambderApiAnswerOutcome<T> = LambderApiSuccessOutcome<T> | (LambderApiCallFailure<T> & {
98
98
  reason: 'server';
@@ -100,10 +100,9 @@ export type LambderApiAnswerOutcome<T> = LambderApiSuccessOutcome<T> | (LambderA
100
100
  /**
101
101
  * What the mapping needs from an HTTP answer, whichever transport produced it.
102
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).
103
+ * Exactly one of `json()` and `text()` is read per answer, since a real
104
+ * Response body may only be read once (a 5xx reads text and parses it
105
+ * itself, so a non-envelope body is still reportable).
107
106
  */
108
107
  export type LambderApiHttpAnswer = {
109
108
  status: number;
@@ -116,6 +115,17 @@ export type LambderApiHttpAnswer = {
116
115
  text: () => Promise<string>;
117
116
  /** The answer's Set-Cookie header values, for a transport that can see them (a cookie jar consumes them); absent in a browser. */
118
117
  setCookies?: string[];
118
+ /**
119
+ * The CSRF tokens of a transport that keeps the session's cookies itself
120
+ * (a cookie jar), where document.cookie is not where they live: the one
121
+ * it posted, and a read of the one it holds now. LambderCaller judges
122
+ * whether a sessionExpired is about the session the page still holds by
123
+ * these; absent, it compares document.cookie before and after the call.
124
+ */
125
+ csrfTokens?: {
126
+ posted: string;
127
+ held: () => string;
128
+ };
119
129
  };
120
130
  /**
121
131
  * Reads one HTTP answer into an outcome. A 5xx is a server failure that keeps
@@ -123,6 +133,8 @@ export type LambderApiHttpAnswer = {
123
133
  * errorMessage, and a global error handler may add crash and logList); a
124
134
  * 422 is a validation failure only with Lambder's validation body; anything
125
135
  * else must be a JSON envelope, whose flags are honoured in a fixed order.
136
+ * A failure read off any answer but a 422 carries the answer's Retry-After
137
+ * as retryAfterSeconds.
126
138
  */
127
139
  export declare const resolveApiOutcome: <T>(answer: LambderApiHttpAnswer) => Promise<LambderApiAnswerOutcome<T>>;
128
140
  export {};