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,9 +1,11 @@
1
- import { joinKeyFields } from "../shared/util/LambderKeyFields.js";
2
- import { sha256HexOf } from "../shared/util/LambderTextDigest.js";
1
+ import { joinKeyFields } from "../shared/util/joinKeyFields.js";
2
+ import { boundKeyField } from "../shared/util/boundKeyField.js";
3
3
  import { RATE_LIMIT_WINDOWS, } from "../shared/contracts/LambderRateLimiter.js";
4
4
  import { LambderApiRefusal, LAMBDER_REFUSAL_CODES } from "../shared/wire/LambderApiRefusal.js";
5
5
  import { parsePreflightSlice } from "./LambderApiValidationRefusal.js";
6
6
  import { assertNonNegativeInteger } from "../shared/util/LambderOptionChecks.js";
7
+ import { LAMBDER_BACKEND_SWAP } from "../shared/util/LambderTestingDoors.js";
8
+ import { DEFAULT_IPV6_RATE_LIMIT_PREFIX, rateLimitSubjectOf } from "../shared/util/LambderClientIp.js";
7
9
  const RATE_LIMIT_WINDOW_KEYS = RATE_LIMIT_WINDOWS.map((window) => window.key);
8
10
  /** Refusal a rate-limited request answers unless the policy or the API's override names its own. */
9
11
  export const DEFAULT_RATE_LIMIT_REFUSAL = { type: "warning", code: LAMBDER_REFUSAL_CODES.rateLimited, content: "Too many requests. Please try again later." };
@@ -26,11 +28,10 @@ export const rateLimitRefusal = (detail, retryAfterSeconds, message) => new Lamb
26
28
  * exposes it as `rateLimitKey`.
27
29
  *
28
30
  * Bound rather than left open because the engine hands the handler whatever
29
- * context the adapter runs on, and the two adapters run on different ones. A
30
- * single builder pinned to the server's context type compiled against the
31
- * mock and then handed the handler a context with no `ip`, `method` or
32
- * `path`, so every caller collapsed onto one counter and the limit a test was
33
- * written to prove silently proved nothing.
31
+ * context the adapter runs on, and the two adapters differ. A builder pinned
32
+ * to the server's context type would compile against the mock, then receive
33
+ * a context with no `ip`, `method` or `path`: every caller would share one
34
+ * counter and a test of the limit would silently prove nothing.
34
35
  */
35
36
  export const lambderRateLimitKeyBuilder = () => ((key) => key);
36
37
  /** Normalize the three rateLimit-option forms into ordered entries; an explicit `undefined` map value declares nothing. */
@@ -45,11 +46,10 @@ const toRateLimitEntries = (value) => {
45
46
  };
46
47
  /**
47
48
  * A limiter is handed only limits it can act on, so no implementation has to
48
- * invent an answer for a nonsense one: that is where two limiters drift apart,
49
- * one refusing the first attempt against a negative cap and the other allowing
50
- * it. Zero is legal and leaves the window
51
- * unenforced, which is why this is the non-negative check and not the
52
- * positive one.
49
+ * invent an answer for a nonsense one (where two limiters would drift apart,
50
+ * one refusing the first attempt against a negative cap and one allowing it).
51
+ * Zero is legal and leaves the window unenforced, hence non-negative rather
52
+ * than positive.
53
53
  */
54
54
  const assertWindowLimits = (subject, windows) => {
55
55
  for (const key of RATE_LIMIT_WINDOW_KEYS) {
@@ -60,91 +60,119 @@ const assertWindowLimits = (subject, windows) => {
60
60
  }
61
61
  };
62
62
  const hasWindowOverride = (override) => RATE_LIMIT_WINDOW_KEYS.some((key) => override[key] !== undefined);
63
- const phaseOf = (per) => per === "ip" ? "beforeSession" : "afterSession";
64
- /**
65
- * The ceiling on the variable half of a tracker key, in UTF-8 bytes, past
66
- * which that half is replaced by its own digest. 1024 sits comfortably inside
67
- * every store's key limit (a DynamoDB partition key is 2048 bytes, and the
68
- * limiter's own prefix plus the api and policy names are joined in front of
69
- * this half).
70
- *
71
- * The bound lives in the engine rather than in a limiter because a store that
72
- * REFUSES an over-long key refuses it by throwing, and a throw from a limiter
73
- * is exactly what failOpen swallows: a custom key derived from a payload field
74
- * (the documented shape, an email address) that a caller posts 3,000
75
- * characters long makes every window of every policy fail the same way, and
76
- * the request goes through unmetered with the limit silently off. Folding the
77
- * over-long half into a digest keeps distinct callers on distinct counters,
78
- * and a key that fits stays readable in the table.
79
- *
80
- * `per: "ip"` needs none of this: normalizeClientIp already caps an address at
81
- * 45 characters, whichever header or gateway field named it.
82
- */
83
- const MAX_TRACKER_KEY_PART_BYTES = 1024;
84
- /**
85
- * The variable half of a tracker key, bounded: `<kind>:<value>` while the
86
- * value fits, `<kind>:h:<sha256 hex>` once it does not. The api and policy
87
- * names are joined around it afterwards, so an over-long key still says which
88
- * policy it belongs to.
89
- */
90
- const boundTrackerKeyPart = async (kind, value) => new TextEncoder().encode(value).length > MAX_TRACKER_KEY_PART_BYTES
91
- ? `${kind}:h:${await sha256HexOf(value)}`
92
- : `${kind}:${value}`;
63
+ const phaseOf = (per, chargeAt) => {
64
+ if (per === "ip")
65
+ return "beforeSession";
66
+ if (per === "session")
67
+ return "beforeGuards";
68
+ return chargeAt ?? "afterGuards";
69
+ };
93
70
  /** The windows one check ran against, for a log line that must not carry the tracker key. */
94
71
  const describeWindows = (limits) => RATE_LIMIT_WINDOW_KEYS.filter((key) => limits[key] !== undefined).map((key) => `${key}: ${String(limits[key])}`).join(", ") || "no window";
72
+ /** The windows a check runs against: the policy's own, each replaced by the API's override where it names one. */
73
+ const windowsOf = (policy, override) => {
74
+ const limits = {};
75
+ for (const windowKey of RATE_LIMIT_WINDOW_KEYS) {
76
+ const limit = override?.[windowKey] ?? policy[windowKey];
77
+ if (limit !== undefined)
78
+ limits[windowKey] = limit;
79
+ }
80
+ return limits;
81
+ };
82
+ /**
83
+ * The counter one check charges. "perPolicy" shares one counter across every
84
+ * API referencing the policy; "perApi" keys each API separately, which is
85
+ * also what lets an API override the windows without colliding. A charge
86
+ * made outside an API call (a route's handler) has no API to be counted
87
+ * under, so it counts against the policy's shared counter.
88
+ */
89
+ const trackerKeyOf = (name, policy, apiName, key) => policy.budget === "perPolicy" || apiName === null
90
+ ? joinKeyFields("policy", name, key)
91
+ : joinKeyFields("api", apiName, name, key);
95
92
  /**
96
93
  * Runtime side of the rate-limit subsystem: holds the limiter and its named
97
94
  * policies, asserts API registrations against them at startup, and checks an
98
95
  * API's declared policies during preflight. Composed into
99
- * LambderApiPolicyEngine. Reads the request's ip and the context's session
100
- * and nothing else, so it runs unchanged under the server and the mock
101
- * runtime.
96
+ * LambderApiPipeline. Reads the request (its ip, and a key's payload
97
+ * slice) and hands the context to a custom key's handler, reading nothing
98
+ * of the context itself but its session, so it runs unchanged under the
99
+ * server and the mock runtime.
102
100
  */
103
101
  export class LambderApiRateLimitsEngine {
104
102
  limiter = null;
105
103
  failOpen = true;
106
- // A Map for the same reason the guard registry is one: a plain object
107
- // answers for "toString" and "constructor" through its prototype, so a
108
- // policy named one of those would slip past the registration check and
109
- // fail on every request instead.
104
+ ipv6PrefixLength = DEFAULT_IPV6_RATE_LIMIT_PREFIX;
105
+ // A Map, not a plain object: an object answers for "toString" and
106
+ // "constructor" through its prototype, so a policy named one of those
107
+ // would slip past the registration check and fail on every request.
110
108
  policies = new Map();
109
+ /**
110
+ * The limiter failures already logged. A limiter answering a run of
111
+ * requests with one continuing failure throws the same error for each
112
+ * (see LambderRateLimiter), and a flood of a few thousand requests a
113
+ * second would otherwise be as many identical log lines.
114
+ */
115
+ loggedFailures = new WeakSet();
111
116
  /** True once rateLimits were configured. */
112
117
  get isConfigured() { return this.limiter !== null; }
113
118
  configure(config) {
114
119
  if (this.limiter)
115
120
  throw new Error("Lambder: rateLimits were already configured.");
116
- // The same rule the guards option follows, and the one an API's own
117
- // `rateLimit: {}` is already held to: declaring the option is
118
- // declaring a limit. An empty map configured a limiter with nothing to
119
- // enforce and reported every API that named a policy as if the option
120
- // had never been given, which sends the reader to the wrong line.
121
+ // Declaring the option is declaring a limit, as for the guards option
122
+ // and an API's own `rateLimit: {}`. An empty map would configure a
123
+ // limiter with nothing to enforce, and every API naming a policy would
124
+ // be reported as if the option were missing: the wrong line to fix.
121
125
  if (Object.keys(config.policies).length === 0) {
122
126
  throw new Error("Lambder: the rateLimits option was declared with no policies in it, which configures nothing. Name the policies APIs will declare, or leave the option off.");
123
127
  }
124
128
  for (const [name, policy] of Object.entries(config.policies)) {
125
129
  const per = policy.per;
126
- if (!per || (per !== "ip" && per !== "session" && typeof per.handler !== "function")) {
127
- throw new Error(`Lambder: rate-limit policy "${name}" needs per: "ip", "session", or a { apiInput?, handler } key.`);
130
+ if (per !== undefined && per !== "ip" && per !== "session" && typeof per?.handler !== "function") {
131
+ throw new Error(`Lambder: rate-limit policy "${name}" has a per that is not "ip", "session", or a { apiInput?, handler } key. Leave per out for a policy the handler charges with its own key.`);
128
132
  }
129
133
  if (!RATE_LIMIT_WINDOW_KEYS.some((key) => policy[key])) {
130
134
  throw new Error(`Lambder: rate-limit policy "${name}" declares no window (${RATE_LIMIT_WINDOW_KEYS.join("/")}).`);
131
135
  }
132
136
  assertWindowLimits(`rate-limit policy "${name}"`, policy);
137
+ const chargeAt = policy.chargeAt;
138
+ if (chargeAt !== undefined) {
139
+ if (chargeAt !== "beforeGuards" && chargeAt !== "afterGuards") {
140
+ throw new Error(`Lambder: rate-limit policy "${name}" has chargeAt "${String(chargeAt)}"; use "afterGuards" (default) or "beforeGuards".`);
141
+ }
142
+ if (per === undefined || per === "ip" || per === "session") {
143
+ throw new Error(`Lambder: rate-limit policy "${name}" sets chargeAt, which only a policy keyed by a { apiInput?, handler } key takes: per "ip" and per "session" each run at one fixed place, and a policy without per is charged by the code that names it.`);
144
+ }
145
+ }
133
146
  const budget = policy.budget;
134
147
  if (budget !== undefined && budget !== "perApi" && budget !== "perPolicy") {
135
148
  throw new Error(`Lambder: rate-limit policy "${name}" has budget "${String(budget)}"; use "perApi" (default: each referencing API counts separately) or "perPolicy" (one counter shared by every referencing API).`);
136
149
  }
137
150
  }
151
+ const ipv6PrefixLength = config.ipv6PrefixLength ?? DEFAULT_IPV6_RATE_LIMIT_PREFIX;
152
+ if (!Number.isInteger(ipv6PrefixLength) || ipv6PrefixLength < 1 || ipv6PrefixLength > 128) {
153
+ throw new Error(`Lambder: rateLimits.ipv6PrefixLength must be a whole number from 1 to 128, got ${String(ipv6PrefixLength)}.`);
154
+ }
138
155
  this.limiter = config.limiter;
139
156
  this.failOpen = config.failOpen ?? true;
157
+ this.ipv6PrefixLength = ipv6PrefixLength;
140
158
  this.policies = new Map(Object.entries(config.policies));
141
159
  }
160
+ /**
161
+ * Puts the engine over another limiter, for `lambder/testing`; the named
162
+ * policies and failOpen stay as configured. False when rateLimits were
163
+ * never configured: there is nothing for a limiter to sit under.
164
+ */
165
+ [LAMBDER_BACKEND_SWAP](limiter) {
166
+ if (!this.limiter)
167
+ return false;
168
+ this.limiter = limiter;
169
+ return true;
170
+ }
142
171
  /** Startup validation of one API registration's rateLimit option. */
143
172
  assertRegistration(apiName, mode, rateLimitOption) {
144
173
  const entries = toRateLimitEntries(rateLimitOption);
145
- // The same rule the guards option follows: declaring the option is
146
- // declaring a limit. `{}`, `[]` and `{ name: undefined }` are
147
- // present-but-empty, and would otherwise register an API that
174
+ // Declaring the option is declaring a limit, as for guards: `{}`,
175
+ // `[]` and `{ name: undefined }` would register an API that
148
176
  // announces a rate limit and enforces none.
149
177
  if (rateLimitOption !== undefined && entries.length === 0) {
150
178
  throw new Error(`Lambder: API "${apiName}" declares an empty rateLimit option, which limits nothing. ` +
@@ -155,6 +183,9 @@ export class LambderApiRateLimitsEngine {
155
183
  if (!policy) {
156
184
  throw new Error(`Lambder: API "${apiName}" references unknown rate-limit policy "${name}". Declare it in the rateLimits option at creation.`);
157
185
  }
186
+ if (policy.per === undefined) {
187
+ throw new Error(`Lambder: API "${apiName}" references rate-limit policy "${name}", which declares no per: its key is the one a handler passes to ctx.rateLimit("${name}", key), so the request alone cannot be counted against it.`);
188
+ }
158
189
  if (policy.per === "session" && mode !== "session") {
159
190
  throw new Error(`Lambder: API "${apiName}" uses rate-limit policy "${name}" (per "session"), which requires addSessionApi.`);
160
191
  }
@@ -176,64 +207,122 @@ export class LambderApiRateLimitsEngine {
176
207
  * counter, when a later guard or validation refuses) keeps its increment,
177
208
  * so list first the policy you want charged on refusals.
178
209
  *
179
- * Run twice per call, once per phase: the policies whose key needs no
180
- * session are checked BEFORE the session is read, so a flood of requests
181
- * carrying bogus session cookies is refused without touching the session
182
- * store; the rest are checked after it, since `per: "session"` and a
183
- * custom key handler may both read ctx.session. Declared order is kept
184
- * inside each phase.
210
+ * Runs once per phase (see LambderRateLimitPhase), keeping declared order
211
+ * within each: `per: "ip"` before the session read, so a flood of bogus
212
+ * session cookies never reaches the session store; `per: "session"` after
213
+ * it; a custom key after the guards unless its policy's `chargeAt` says
214
+ * "beforeGuards", so a caller they refuse cannot spend somebody else's
215
+ * budget.
185
216
  */
186
217
  async run(apiName, request, ctx, rateLimitOption, phase) {
187
218
  for (const { name, override } of toRateLimitEntries(rateLimitOption)) {
219
+ // Registration refused an unknown policy and one without per.
188
220
  const policy = this.policies.get(name);
189
- if (!policy || !this.limiter)
190
- throw new Error(`Lambder: rate-limit policy "${name}" is not configured. Declare it in the rateLimits option at creation.`);
191
- if (phaseOf(policy.per) !== phase)
192
- continue;
193
- const key = await this.resolveKey(request, ctx, policy.per);
194
- // "perPolicy" shares one counter across every API referencing the
195
- // policy; "perApi" keys each API separately, which is also what
196
- // lets an API override the windows without colliding.
197
- const trackerKey = policy.budget === "perPolicy"
198
- ? joinKeyFields("policy", name, key)
199
- : joinKeyFields("api", apiName, name, key);
200
- const limits = {};
201
- for (const windowKey of RATE_LIMIT_WINDOW_KEYS) {
202
- const limit = override?.[windowKey] ?? policy[windowKey];
203
- if (limit !== undefined)
204
- limits[windowKey] = limit;
205
- }
206
- let exceeded;
207
- try {
208
- exceeded = await this.limiter.isRateLimited(trackerKey, limits);
209
- }
210
- catch (limiterErr) {
211
- if (!this.failOpen)
212
- throw limiterErr;
213
- // The policy and its windows, never the tracker key: the key
214
- // carries whatever a custom handler returned, which the docs'
215
- // own example makes an email address, and a log line is not
216
- // the place for it.
217
- console.error(`Lambder rate limits: policy "${name}" (${describeWindows(limits)}) could not be checked for API "${apiName}"; ` +
218
- "the request is being allowed through. Set rateLimits.failOpen: false to refuse instead.", limiterErr);
221
+ const per = policy.per;
222
+ if (phaseOf(per, policy.chargeAt) !== phase)
219
223
  continue;
220
- }
224
+ const key = await this.resolveKey(name, request, ctx, per);
225
+ const limits = windowsOf(policy, override);
226
+ const exceeded = await this.countAttempt(name, trackerKeyOf(name, policy, apiName, key), limits, `API "${apiName}"`);
221
227
  if (exceeded) {
222
- const retryAfterSeconds = Math.max(1, exceeded.resetAt - Math.floor(Date.now() / 1000));
223
- throw rateLimitRefusal(`Rate limited: "${apiName}" exceeded policy "${name}" (${exceeded.window}: ${exceeded.limit}).`, retryAfterSeconds, override?.errorMessage ?? policy.errorMessage);
228
+ throw rateLimitRefusal(`Rate limited: "${apiName}" exceeded policy "${name}" (${exceeded.window}: ${exceeded.limit}).`, this.retryAfterOf(exceeded), override?.errorMessage ?? policy.errorMessage);
224
229
  }
225
230
  }
226
231
  }
227
- async resolveKey(request, ctx, per) {
228
- if (per === "ip")
229
- return `ip:${request.ip}`;
230
- if (per === "session") {
231
- const sessionKey = ctx.session?.sessionKey;
232
- if (!sessionKey)
233
- throw new Error('Lambder: rate-limit per "session" evaluated without a session on the context.');
234
- return await boundTrackerKeyPart("session", sessionKey);
232
+ /**
233
+ * One policy charged by code rather than by a declaration, for
234
+ * `ctx.rateLimit` and `ctx.isRateLimited`. Counters, failOpen, key
235
+ * bounding and refusal are those of a declared limit; only who knows the
236
+ * key differs. A policy without `per` takes the key the code passes, a
237
+ * `per: "ip"` or `per: "session"` one reads it off the request, and one
238
+ * keyed by an API payload is refused, since only its APIs know the key.
239
+ */
240
+ async chargePolicy(name, subject) {
241
+ if (!this.limiter)
242
+ throw new Error(`Lambder: charging rate-limit policy "${name}" needs the rateLimits option at creation.`);
243
+ const policy = this.policies.get(name);
244
+ if (!policy)
245
+ throw new Error(`Lambder: unknown rate-limit policy "${name}". Declare it in the rateLimits option at creation.`);
246
+ const key = await this.chargeKeyOf(name, policy, subject);
247
+ const limits = windowsOf(policy);
248
+ const where = subject.apiName === null ? "a route" : `API "${subject.apiName}"`;
249
+ const exceeded = await this.countAttempt(name, trackerKeyOf(name, policy, subject.apiName, key), limits, where);
250
+ if (!exceeded)
251
+ return { checkResult: false, refusal: null };
252
+ const retryAfterSeconds = this.retryAfterOf(exceeded);
253
+ return {
254
+ checkResult: { ...exceeded, retryAfterSeconds },
255
+ refusal: rateLimitRefusal(`Rate limited: ${where} exceeded policy "${name}" (${exceeded.window}: ${exceeded.limit}).`, retryAfterSeconds, policy.errorMessage),
256
+ };
257
+ }
258
+ /**
259
+ * Seconds until the refusing window resets, read against the limiter's
260
+ * own clock when it keeps one: `resetAt` is a second on that clock, and a
261
+ * limiter under a test clock would otherwise be told a Retry-After
262
+ * measured from another time entirely.
263
+ */
264
+ retryAfterOf(exceeded) {
265
+ const nowMilliseconds = this.limiter?.clockMilliseconds?.() ?? Date.now();
266
+ return Math.max(1, exceeded.resetAt - Math.floor(nowMilliseconds / 1000));
267
+ }
268
+ /**
269
+ * Counts one attempt, or lets it through when the limiter itself fails
270
+ * and failOpen is on. The log line names the policy and its windows,
271
+ * never the tracker key: the key carries whatever a custom handler
272
+ * returned, which the docs' own example makes an email address. A
273
+ * failure the limiter throws again (see loggedFailures) is not logged
274
+ * again.
275
+ */
276
+ async countAttempt(name, trackerKey, limits, where) {
277
+ try {
278
+ return await this.limiter.isRateLimited(trackerKey, limits);
235
279
  }
236
- const payload = per.apiInput ? parsePreflightSlice(per.apiInput, request.payload) : undefined;
237
- return await boundTrackerKeyPart("custom", await per.handler(ctx, payload));
280
+ catch (limiterErr) {
281
+ if (!this.failOpen)
282
+ throw limiterErr;
283
+ if (typeof limiterErr === "object" && limiterErr !== null) {
284
+ if (this.loggedFailures.has(limiterErr))
285
+ return false;
286
+ this.loggedFailures.add(limiterErr);
287
+ }
288
+ console.error(`Lambder rate limits: policy "${name}" (${describeWindows(limits)}) could not be checked for ${where}; ` +
289
+ "the request is being allowed through. Set rateLimits.failOpen: false to refuse instead.", limiterErr);
290
+ return false;
291
+ }
292
+ }
293
+ async chargeKeyOf(name, policy, subject) {
294
+ const per = policy.per;
295
+ if (per === undefined) {
296
+ if (subject.key === undefined)
297
+ throw new Error(`Lambder: rate-limit policy "${name}" declares no per, so the code charging it passes the key: ctx.rateLimit("${name}", key).`);
298
+ return await boundKeyField("custom", subject.key);
299
+ }
300
+ if (per !== "ip" && per !== "session") {
301
+ throw new Error(`Lambder: rate-limit policy "${name}" derives its key from an API's payload, so only the APIs declaring it can charge it.`);
302
+ }
303
+ if (subject.key !== undefined)
304
+ throw new Error(`Lambder: rate-limit policy "${name}" is keyed per "${per}", so charging it takes no key.`);
305
+ return await this.requestKeyOf(name, per, subject.ip, subject.session);
306
+ }
307
+ async resolveKey(name, request, ctx, per) {
308
+ if (per === "ip" || per === "session")
309
+ return await this.requestKeyOf(name, per, request.ip, ctx.session);
310
+ const payload = per.apiInput ? await parsePreflightSlice(per.apiInput, request.payload) : undefined;
311
+ return await boundKeyField("custom", await per.handler(ctx, payload));
312
+ }
313
+ /**
314
+ * The key a `per: "ip"` or `per: "session"` policy counts under: what the
315
+ * request carries, however the policy is charged. A session key is
316
+ * bounded like a custom one (see boundKeyField); an address needs no
317
+ * bound, since normalizeClientIp caps it at 45 characters whichever
318
+ * header or gateway field named it.
319
+ */
320
+ async requestKeyOf(name, per, ip, session) {
321
+ if (per === "ip")
322
+ return `ip:${rateLimitSubjectOf(ip, this.ipv6PrefixLength)}`;
323
+ const sessionKey = session?.sessionKey;
324
+ if (!sessionKey)
325
+ throw new Error(`Lambder: rate-limit policy "${name}" is keyed per "session", and the request charging it has no session.`);
326
+ return await boundKeyField("session", sessionKey);
238
327
  }
239
328
  }
@@ -2,11 +2,10 @@
2
2
  * Cookie header pairs (`name=value`) as every value per name, in header
3
3
  * order. Parsed pair by pair, because a whole-header parse keeps only the
4
4
  * first value of a name the browser holds at two scopes, and the session
5
- * controller weighs every copy. The map has no prototype: a cookie is client
6
- * data, and a name such as `__proto__` or `constructor` must land as a key
7
- * of its own rather than resolve to Object.prototype, which is what turned
8
- * one planted cookie into a 500 on every request. Every adapter builds its
9
- * request's cookies through this one function.
5
+ * controller weighs every copy. The map has no prototype: a cookie named
6
+ * `__proto__` or `constructor` must land as its own key rather than resolve
7
+ * to Object.prototype, or one planted cookie would 500 every request. Every
8
+ * adapter builds its request's cookies through this one function.
10
9
  */
11
10
  export declare const cookieValuesByName: (pairs: readonly string[]) => Record<string, string[]>;
12
11
  /** Request headers under lowercased names, so a lookup never depends on how the gateway spelled them. */
@@ -34,10 +33,9 @@ export type LambderApiRequest = {
34
33
  guardInputs: Record<string, unknown> | undefined;
35
34
  /**
36
35
  * The idempotency key exactly as posted, so `unknown`: it is client data,
37
- * and the shape check plus the client-facing 400 belong to the
38
- * idempotency engine. Typed `string | undefined` here, every reader was
39
- * entitled to treat a number or an object as a key, and the only one
40
- * there is had to widen it back before it could check anything.
36
+ * and the shape check and its client-facing 400 belong to the idempotency
37
+ * engine. A string type here would entitle every reader to treat a
38
+ * posted number or object as a key.
41
39
  */
42
40
  idempotencyKey: unknown;
43
41
  /** Request headers, names lowercased. */
@@ -49,10 +47,9 @@ export type LambderApiRequest = {
49
47
  /** The Host the request was made to. */
50
48
  host: string;
51
49
  /**
52
- * The caller's abort signal, carried for adapters that have one (the mock
53
- * runtime aborts its own latency wait with it). The pipeline itself
54
- * neither reads nor honours it: a Lambda invocation has no signal, and an
55
- * adapter that does own one is the layer that knows what abandoning a
50
+ * The caller's abort signal, for adapters that have one (the mock runtime
51
+ * aborts its latency wait with it). The pipeline ignores it: a Lambda
52
+ * invocation has no signal, and only the adapter knows what abandoning a
56
53
  * half-run call means for it.
57
54
  */
58
55
  signal?: AbortSignal;
@@ -71,6 +68,16 @@ export type LambderApiRequestInfo = {
71
68
  host: string;
72
69
  signal?: AbortSignal;
73
70
  };
71
+ /**
72
+ * Whether a POST's Content-Type makes it an API call: application/json,
73
+ * which a browser sends cross-origin only after a CORS preflight. A form
74
+ * (text/plain, urlencoded, multipart) needs none, so any website could post
75
+ * one and, read as an API call, call a public login and plant the
76
+ * attacker's session cookies in a visitor's browser. Every Lambder caller
77
+ * sends application/json; the server and the mock's HTTP-shaped adapters
78
+ * read the same rule here.
79
+ */
80
+ export declare const isApiCallContentType: (lowercasedHeaders: Record<string, string | undefined>) => boolean;
74
81
  /**
75
82
  * Reads the posted envelope into a request. Null when the body carries no
76
83
  * apiName, which is how the server tells an API call from a route with a
@@ -90,14 +97,13 @@ export type LambderRestorePayloadResult = {
90
97
  * Restores a payload the caller sent compressed (`payloadGz` or `payloadBr`,
91
98
  * beside `payloadBytes`) onto request.payload, so every later stage
92
99
  * (rate-limit key slices, guards, input validation, the handler) reads an
93
- * ordinary payload and needs no awareness of the wire format. The field
94
- * names the encoding; a request carrying both is refused. A request that
95
- * sent a plain payload passes through untouched.
100
+ * ordinary payload. The field names the encoding; a request carrying both is
101
+ * refused. A plain payload passes through untouched.
96
102
  *
97
- * Every failure answers with a message instead of throwing: a malformed body
98
- * is a client error, not a crash. The declared byte length both bounds the
99
- * decompression and verifies it, so an over-large or tampered body is
100
- * refused rather than expanded. Runs on Node through zlib and in a browser
101
- * through DecompressionStream, under the same bound.
103
+ * Failures return a message instead of throwing: a malformed body is a
104
+ * client error, not a crash. The declared byte length both bounds and
105
+ * verifies the decompression, so an over-large or tampered body is refused
106
+ * rather than expanded. Runs on Node (zlib) and in a browser
107
+ * (DecompressionStream) under the same bound.
102
108
  */
103
109
  export declare const restoreCompressedPayload: (request: LambderApiRequest, maxPayloadBytes: number) => Promise<LambderRestorePayloadResult>;
@@ -6,11 +6,10 @@ import { restoreText, LambderCompressionError, LAMBDER_RESTORE_FAILURES } from "
6
6
  * Cookie header pairs (`name=value`) as every value per name, in header
7
7
  * order. Parsed pair by pair, because a whole-header parse keeps only the
8
8
  * first value of a name the browser holds at two scopes, and the session
9
- * controller weighs every copy. The map has no prototype: a cookie is client
10
- * data, and a name such as `__proto__` or `constructor` must land as a key
11
- * of its own rather than resolve to Object.prototype, which is what turned
12
- * one planted cookie into a 500 on every request. Every adapter builds its
13
- * request's cookies through this one function.
9
+ * controller weighs every copy. The map has no prototype: a cookie named
10
+ * `__proto__` or `constructor` must land as its own key rather than resolve
11
+ * to Object.prototype, or one planted cookie would 500 every request. Every
12
+ * adapter builds its request's cookies through this one function.
14
13
  */
15
14
  export const cookieValuesByName = (pairs) => {
16
15
  const cookies = Object.create(null);
@@ -24,9 +23,8 @@ export const cookieValuesByName = (pairs) => {
24
23
  };
25
24
  /** Request headers under lowercased names, so a lookup never depends on how the gateway spelled them. */
26
25
  export const lowercaseHeaderNames = (headers) => {
27
- // Prototype-free for the same reason the cookie map is: a header is
28
- // client data, and a name such as `__proto__` must land as a key rather
29
- // than reach Object.prototype's setter and vanish.
26
+ // Prototype-free like the cookie map: a header named `__proto__` must
27
+ // land as a key rather than reach Object.prototype's setter and vanish.
30
28
  const lowered = Object.create(null);
31
29
  for (const [key, value] of Object.entries(headers ?? {})) {
32
30
  if (value !== undefined)
@@ -34,6 +32,16 @@ export const lowercaseHeaderNames = (headers) => {
34
32
  }
35
33
  return lowered;
36
34
  };
35
+ /**
36
+ * Whether a POST's Content-Type makes it an API call: application/json,
37
+ * which a browser sends cross-origin only after a CORS preflight. A form
38
+ * (text/plain, urlencoded, multipart) needs none, so any website could post
39
+ * one and, read as an API call, call a public login and plant the
40
+ * attacker's session cookies in a visitor's browser. Every Lambder caller
41
+ * sends application/json; the server and the mock's HTTP-shaped adapters
42
+ * read the same rule here.
43
+ */
44
+ export const isApiCallContentType = (lowercasedHeaders) => (lowercasedHeaders["content-type"] ?? "").split(";")[0].trim().toLowerCase() === "application/json";
37
45
  /**
38
46
  * Reads the posted envelope into a request. Null when the body carries no
39
47
  * apiName, which is how the server tells an API call from a route with a
@@ -57,9 +65,9 @@ export const readApiEnvelope = (post, info) => {
57
65
  compressedPayload: hasGzip || hasBrotli
58
66
  ? { gzip: post[COMPRESSED_PAYLOAD_GZ_FIELD], brotli: post[COMPRESSED_PAYLOAD_BR_FIELD], declaredBytes: post[COMPRESSED_PAYLOAD_BYTES_FIELD] }
59
67
  : null,
60
- // Arrays are objects, and an array answers for its own properties, so
61
- // a guard named "length" received a number where the client sent it
62
- // nothing. Same reasoning as reading the map with hasOwnProperty.
68
+ // Arrays are refused: an array answers for its own properties, so a
69
+ // guard named "length" would receive a number the client never sent.
70
+ // Same reasoning as reading the map with hasOwnProperty.
63
71
  guardInputs: guardInputs !== null && typeof guardInputs === "object" && !Array.isArray(guardInputs) ? guardInputs : undefined,
64
72
  idempotencyKey: post.idempotencyKey,
65
73
  headers: info.headers,
@@ -73,15 +81,14 @@ export const readApiEnvelope = (post, info) => {
73
81
  * Restores a payload the caller sent compressed (`payloadGz` or `payloadBr`,
74
82
  * beside `payloadBytes`) onto request.payload, so every later stage
75
83
  * (rate-limit key slices, guards, input validation, the handler) reads an
76
- * ordinary payload and needs no awareness of the wire format. The field
77
- * names the encoding; a request carrying both is refused. A request that
78
- * sent a plain payload passes through untouched.
84
+ * ordinary payload. The field names the encoding; a request carrying both is
85
+ * refused. A plain payload passes through untouched.
79
86
  *
80
- * Every failure answers with a message instead of throwing: a malformed body
81
- * is a client error, not a crash. The declared byte length both bounds the
82
- * decompression and verifies it, so an over-large or tampered body is
83
- * refused rather than expanded. Runs on Node through zlib and in a browser
84
- * through DecompressionStream, under the same bound.
87
+ * Failures return a message instead of throwing: a malformed body is a
88
+ * client error, not a crash. The declared byte length both bounds and
89
+ * verifies the decompression, so an over-large or tampered body is refused
90
+ * rather than expanded. Runs on Node (zlib) and in a browser
91
+ * (DecompressionStream) under the same bound.
85
92
  */
86
93
  export const restoreCompressedPayload = async (request, maxPayloadBytes) => {
87
94
  const fields = request.compressedPayload;
@@ -1,10 +1,9 @@
1
1
  import type { LambderApiDefinition } from "./LambderApiDefinition.js";
2
2
  import { type LambderApiGuard } from "./LambderApiGuards.js";
3
3
  /**
4
- * One endpoint as the generator sees it: the key its signature is stored
5
- * under, the signature, and the name both were computed from. The name is
6
- * what the shipped map deliberately does not carry, so this is the build-time
7
- * view of the same data, and the only place a generated map can be diffed by
4
+ * One endpoint as the generator sees it: the map key, the signature, and the
5
+ * name both were computed from. The shipped map deliberately omits the name,
6
+ * so this build-time view is the only place a generated map can be diffed by
8
7
  * endpoint rather than by opaque key.
9
8
  */
10
9
  export type LambderApiSignatureEntry = {
@@ -18,17 +17,15 @@ export type LambderApiSignatureEntry = {
18
17
  * input and output schemas as JSON Schema, every guard it declares with the
19
18
  * schema that guard validates (the guardInput the client sends separately,
20
19
  * or the apiInput slice of the payload), and whether it demands an
21
- * idempotency key. Anything else about the endpoint (its rate limits, a
22
- * guard's parameter, the handler) changes nothing for a client and is left
23
- * out, so changing it never forces a reload.
20
+ * idempotency key. Anything else (rate limits, a guard's parameter, the
21
+ * handler) changes nothing for a client and is left out, so changing it never
22
+ * forces a reload.
24
23
  *
25
- * The description is hashed as built, descriptions and titles included: a
26
- * schema is what the server says it is, and a client built against a
27
- * different one reloads once, with one exception the schema declares itself:
28
- * the values of an extensibleEnum() in an output (see keepShapeOnly). What
29
- * must hold for the digest to mean anything is that a schema is built from
30
- * static values: one that reads the clock, a random source or the environment
31
- * at construction digests differently in the generator's process and on the
32
- * server.
24
+ * Schemas are hashed as built, descriptions and titles included, so a client
25
+ * built against a different one reloads once. The exception is an output
26
+ * extensibleEnum()'s values (see keepShapeOnly). Schemas must be built from
27
+ * static values: one that reads the clock, a random source or the
28
+ * environment at construction digests differently in the generator's process
29
+ * and on the server.
33
30
  */
34
31
  export declare const apiSignatureOf: (definition: LambderApiDefinition, guards: Record<string, LambderApiGuard<any, any, any>> | undefined) => Promise<string>;