lambder 14.1.1 → 15.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 (158) hide show
  1. package/CHANGELOG.md +429 -0
  2. package/README.md +14 -17
  3. package/dist/api/LambderApiDeclarations.d.ts +11 -5
  4. package/dist/api/LambderApiDefinition.d.ts +2 -1
  5. package/dist/api/LambderApiEnvelope.d.ts +2 -2
  6. package/dist/api/LambderApiEnvelope.js +8 -2
  7. package/dist/api/LambderApiGuards.d.ts +44 -5
  8. package/dist/api/LambderApiGuards.js +27 -0
  9. package/dist/api/LambderApiIdempotency.d.ts +14 -7
  10. package/dist/api/LambderApiIdempotency.js +39 -27
  11. package/dist/api/LambderApiPipeline.d.ts +25 -11
  12. package/dist/api/LambderApiPipeline.js +34 -17
  13. package/dist/api/LambderApiRateLimits.d.ts +28 -21
  14. package/dist/api/LambderApiRateLimits.js +55 -35
  15. package/dist/api/LambderApiRefusals.d.ts +10 -4
  16. package/dist/api/LambderApiRefusals.js +31 -5
  17. package/dist/api/LambderApiRequest.d.ts +21 -1
  18. package/dist/api/LambderApiRequest.js +31 -2
  19. package/dist/build/checkApiRefusals.d.ts +8 -3
  20. package/dist/build/checkApiRefusals.js +80 -29
  21. package/dist/build/contractPrinterProcess.d.ts +11 -0
  22. package/dist/build/contractPrinterProcess.js +17 -0
  23. package/dist/build/generateApiFiles.d.ts +26 -5
  24. package/dist/build/generateApiFiles.js +68 -7
  25. package/dist/build/writeApiSchemas.d.ts +109 -0
  26. package/dist/build/writeApiSchemas.js +308 -0
  27. package/dist/build.d.ts +3 -1
  28. package/dist/build.js +2 -0
  29. package/dist/client/LambderCaller.d.ts +25 -11
  30. package/dist/client/LambderCaller.js +57 -49
  31. package/dist/client/LambderUploadRunner.d.ts +55 -27
  32. package/dist/client/LambderUploadRunner.js +89 -34
  33. package/dist/client.d.ts +6 -6
  34. package/dist/client.js +2 -2
  35. package/dist/core/Lambder.d.ts +31 -21
  36. package/dist/core/Lambder.js +79 -23
  37. package/dist/core/LambderCallSummary.d.ts +1 -1
  38. package/dist/core/LambderContext.d.ts +59 -7
  39. package/dist/core/LambderContext.js +31 -15
  40. package/dist/core/LambderCrashHandling.d.ts +5 -0
  41. package/dist/core/LambderCrashHandling.js +11 -1
  42. package/dist/core/LambderCreateOptions.d.ts +28 -17
  43. package/dist/core/LambderCreateOptions.js +14 -7
  44. package/dist/core/LambderResolver.d.ts +3 -2
  45. package/dist/core/LambderResponseBuilder.d.ts +20 -9
  46. package/dist/core/LambderResponseBuilder.js +39 -8
  47. package/dist/core/LambderTemplatingEngine.d.ts +24 -6
  48. package/dist/core/LambderTemplatingEngine.js +23 -3
  49. package/dist/index.d.ts +15 -12
  50. package/dist/index.js +7 -5
  51. package/dist/invoke/LambderInvokeCaller.d.ts +25 -4
  52. package/dist/invoke/LambderInvokeCaller.js +47 -9
  53. package/dist/invoke/LambderInvokeOutcome.d.ts +7 -6
  54. package/dist/mock/LambderMockAdapterDoor.d.ts +40 -0
  55. package/dist/mock/LambderMockAdapterDoor.js +10 -0
  56. package/dist/mock/LambderMockApiSchemas.d.ts +28 -0
  57. package/dist/mock/LambderMockApiSchemas.js +150 -0
  58. package/dist/mock/LambderMockApp.d.ts +56 -48
  59. package/dist/mock/LambderMockApp.js +107 -62
  60. package/dist/mock/LambderMockBrowserCookies.js +18 -5
  61. package/dist/mock/LambderMockCreateOptions.d.ts +51 -4
  62. package/dist/mock/LambderMockFailureInjector.d.ts +12 -3
  63. package/dist/mock/LambderMockFailureInjector.js +12 -8
  64. package/dist/mock/LambderMockTypes.d.ts +8 -4
  65. package/dist/mock/lambderMockInvokeTransport.js +2 -9
  66. package/dist/mock/lambderMockMswHandler.d.ts +19 -13
  67. package/dist/mock/lambderMockMswHandler.js +25 -22
  68. package/dist/mock.d.ts +1 -0
  69. package/dist/secrets/LambderOneShotSecrets.d.ts +1 -1
  70. package/dist/secrets/LambderOneShotSecrets.js +6 -36
  71. package/dist/secrets/LambderPasswordHasher.d.ts +56 -0
  72. package/dist/secrets/LambderPasswordHasher.js +168 -0
  73. package/dist/session/LambderSessionController.d.ts +37 -4
  74. package/dist/session/LambderSessionController.js +36 -6
  75. package/dist/session/LambderSessionManager.d.ts +33 -3
  76. package/dist/session/LambderSessionManager.js +55 -16
  77. package/dist/shared/LambderHtml.d.ts +23 -4
  78. package/dist/shared/LambderHtml.js +17 -5
  79. package/dist/shared/LambderI18n.d.ts +106 -16
  80. package/dist/shared/LambderI18n.js +259 -74
  81. package/dist/shared/contracts/LambderCache.d.ts +14 -1
  82. package/dist/shared/contracts/LambderRateLimiter.d.ts +5 -6
  83. package/dist/shared/transport/LambderApiTransport.d.ts +3 -2
  84. package/dist/shared/util/LambderBackoffTimer.d.ts +24 -13
  85. package/dist/shared/util/LambderBackoffTimer.js +40 -12
  86. package/dist/shared/util/LambderCallAbort.d.ts +22 -6
  87. package/dist/shared/util/LambderCallAbort.js +37 -9
  88. package/dist/shared/util/LambderErrorReportMark.d.ts +4 -0
  89. package/dist/shared/util/LambderErrorReportMark.js +22 -0
  90. package/dist/shared/util/LambderKeyFieldDigest.d.ts +46 -0
  91. package/dist/shared/util/LambderKeyFieldDigest.js +75 -0
  92. package/dist/shared/util/LambderSignedClaims.d.ts +19 -0
  93. package/dist/shared/util/LambderSignedClaims.js +54 -0
  94. package/dist/shared/util/LambderSwappableInstances.d.ts +29 -0
  95. package/dist/shared/util/LambderSwappableInstances.js +54 -0
  96. package/dist/shared/util/LambderTestingDoors.d.ts +6 -2
  97. package/dist/shared/util/LambderTestingDoors.js +6 -2
  98. package/dist/shared/util/LambderTextDigest.d.ts +5 -5
  99. package/dist/shared/util/LambderTextDigest.js +5 -5
  100. package/dist/shared/wire/LambderApiContract.d.ts +1 -1
  101. package/dist/shared/wire/LambderApiOptionEntries.d.ts +9 -1
  102. package/dist/shared/wire/LambderApiOptionEntries.js +15 -0
  103. package/dist/shared/wire/LambderApiOptionValues.d.ts +3 -2
  104. package/dist/shared/wire/LambderApiOutcome.d.ts +37 -19
  105. package/dist/shared/wire/LambderApiOutcome.js +1 -1
  106. package/dist/shared/wire/LambderApiRefusal.d.ts +39 -12
  107. package/dist/shared/wire/LambderApiRefusal.js +3 -3
  108. package/dist/shared/wire/LambderApiSchemaEntries.d.ts +21 -0
  109. package/dist/shared/wire/LambderApiSchemaEntries.js +18 -0
  110. package/dist/shared/wire/LambderCallOptions.d.ts +3 -3
  111. package/dist/shared/wire/LambderCallOutcome.d.ts +2 -1
  112. package/dist/shared/wire/LambderCallOutcome.js +3 -2
  113. package/dist/shared/wire/LambderIdempotencyKeyScope.d.ts +5 -3
  114. package/dist/shared/wire/LambderIdempotencyKeyScope.js +5 -3
  115. package/dist/shared/wire/LambderInvokeApiId.d.ts +3 -3
  116. package/dist/shared/wire/LambderInvokeApiId.js +3 -3
  117. package/dist/shared/wire/LambderOutcomeAssertions.d.ts +7 -6
  118. package/dist/shared/wire/LambderOutcomeAssertions.js +1 -1
  119. package/dist/stores/LambderCacheValues.d.ts +2 -5
  120. package/dist/stores/LambderDdbCache.d.ts +15 -9
  121. package/dist/stores/LambderDdbCache.js +25 -6
  122. package/dist/stores/LambderDdbIdempotencyStore.d.ts +6 -4
  123. package/dist/stores/LambderDdbIdempotencyStore.js +32 -52
  124. package/dist/stores/LambderDdbOneShotSecretStore.d.ts +11 -0
  125. package/dist/stores/LambderDdbOneShotSecretStore.js +17 -5
  126. package/dist/stores/LambderDdbSdk.d.ts +59 -29
  127. package/dist/stores/LambderDdbSdk.js +116 -44
  128. package/dist/stores/LambderDdbSessionStore.d.ts +26 -11
  129. package/dist/stores/LambderDdbSessionStore.js +133 -68
  130. package/dist/stores/LambderMemoryCache.d.ts +2 -2
  131. package/dist/stores/LambderS3FileSource.d.ts +3 -2
  132. package/dist/stores/LambderS3FileSource.js +11 -13
  133. package/dist/stores/LambderS3UploadBucket.d.ts +12 -0
  134. package/dist/stores/LambderS3UploadBucket.js +23 -0
  135. package/dist/stores/LambderStorageBackedCache.d.ts +2 -2
  136. package/dist/stores/LambderStoredText.d.ts +26 -0
  137. package/dist/stores/LambderStoredText.js +25 -0
  138. package/dist/testing/LambderConformanceRunner.d.ts +9 -2
  139. package/dist/testing/LambderConformanceRunner.js +9 -2
  140. package/dist/testing/LambderTestApp.d.ts +65 -7
  141. package/dist/testing/LambderTestApp.js +101 -6
  142. package/dist/testing/LambderTestVisitor.d.ts +43 -12
  143. package/dist/testing/LambderTestVisitor.js +35 -11
  144. package/dist/testing/bootLambdaPackage.d.ts +105 -0
  145. package/dist/testing/bootLambdaPackage.js +265 -0
  146. package/dist/testing/lambdaPackageBootProcess.d.ts +39 -0
  147. package/dist/testing/lambdaPackageBootProcess.js +130 -0
  148. package/dist/testing/lambderCacheConformance.d.ts +26 -0
  149. package/dist/testing/lambderCacheConformance.js +287 -0
  150. package/dist/testing/lambderCacheStorageConformance.d.ts +25 -0
  151. package/dist/testing/lambderCacheStorageConformance.js +159 -0
  152. package/dist/testing/lambderSessionStoreConformance.d.ts +3 -3
  153. package/dist/testing/lambderSessionStoreConformance.js +3 -3
  154. package/dist/testing.d.ts +7 -1
  155. package/dist/testing.js +5 -0
  156. package/package.json +1 -6
  157. package/dist/shared/util/boundKeyField.d.ts +0 -20
  158. package/dist/shared/util/boundKeyField.js +0 -34
@@ -46,6 +46,23 @@ export type LambderGuardPlacement = {
46
46
  /** Default: "beforeInputValidation". */
47
47
  runAt?: LambderGuardRunAt;
48
48
  };
49
+ /** What a `guardInput` guard's value is to an idempotency key's request. */
50
+ export type LambderGuardInputUse = {
51
+ /**
52
+ * The value can differ between attempts of one operation: a single-use
53
+ * proof (a captcha token, a one-time code) or a credential refreshed
54
+ * between attempts (a short-lived token). A genuine retry may carry a new
55
+ * one, so the value is left out of the fingerprint that tells one request
56
+ * under an idempotency key from another; counted, it would turn the retry
57
+ * into a 409 and the person's next attempt into a second run. Default:
58
+ * false, the value counts, so the same key sent with
59
+ * another value (another store, another tenant) is refused as another
60
+ * request rather than replayed the first one's answer. Only a
61
+ * `guardInput` guard takes it: an `apiInput` guard's value is the
62
+ * payload's, which always counts.
63
+ */
64
+ singleUseInput?: boolean;
65
+ };
49
66
  /**
50
67
  * The refusal codes a guard may refuse with, from the app's vocabulary (the
51
68
  * `refusals` option at creation). They join the codes of every API that
@@ -70,6 +87,13 @@ export type LambderGuardRefusals<TRefusals extends readonly string[] = readonly
70
87
  * call that omits it. The API payload and handler never see the value.
71
88
  * - neither: the guard reads only the context.
72
89
  *
90
+ * A `guardInput` is part of what an idempotency key's request is, beside its
91
+ * payload: the same key sent with another value (another store, another
92
+ * tenant) is another request. A guard whose value can differ between
93
+ * attempts of one operation (a captcha token, a one-time code, a short-lived
94
+ * token refreshed in between) declares `singleUseInput: true`, and its value
95
+ * is left out of that, since a genuine retry may carry a new one.
96
+ *
73
97
  * Orthogonally, a guard may also:
74
98
  *
75
99
  * - declare `session: true`: the guard needs ctx.session, so it makes every
@@ -102,31 +126,35 @@ export type LambderGuardRefusals<TRefusals extends readonly string[] = readonly
102
126
  export type LambderApiGuard<TInput extends z.ZodType = z.ZodType, TParam = any, TOutput = any, TCtx = any, TSessionCtx = TCtx> = LambderGuardPlacement & LambderGuardRefusals & ({
103
127
  apiInput: TInput;
104
128
  guardInput?: undefined;
129
+ singleUseInput?: undefined;
105
130
  session: true;
106
131
  handler: LambderGuardHandler<TSessionCtx, z.output<TInput>, TParam, TOutput>;
107
132
  } | {
108
133
  apiInput: TInput;
109
134
  guardInput?: undefined;
135
+ singleUseInput?: undefined;
110
136
  session?: false;
111
137
  handler: LambderGuardHandler<TCtx, z.output<TInput>, TParam, TOutput>;
112
- } | {
138
+ } | ({
113
139
  guardInput: TInput;
114
140
  apiInput?: undefined;
115
141
  session: true;
116
142
  handler: LambderGuardHandler<TSessionCtx, z.output<TInput>, TParam, TOutput>;
117
- } | {
143
+ } & LambderGuardInputUse) | ({
118
144
  guardInput: TInput;
119
145
  apiInput?: undefined;
120
146
  session?: false;
121
147
  handler: LambderGuardHandler<TCtx, z.output<TInput>, TParam, TOutput>;
122
- } | {
148
+ } & LambderGuardInputUse) | {
123
149
  apiInput?: undefined;
124
150
  guardInput?: undefined;
151
+ singleUseInput?: undefined;
125
152
  session: true;
126
153
  handler: LambderGuardHandler<TSessionCtx, undefined, TParam, TOutput>;
127
154
  } | {
128
155
  apiInput?: undefined;
129
156
  guardInput?: undefined;
157
+ singleUseInput?: undefined;
130
158
  session?: false;
131
159
  handler: LambderGuardHandler<TCtx, undefined, TParam, TOutput>;
132
160
  });
@@ -190,20 +218,22 @@ export type LambderGuardBuilder<TCtx, TSessionCtx, TVocabulary = never, TCodesRe
190
218
  guardInput: TInput;
191
219
  session: true;
192
220
  handler: (ctx: TSessionCtx & LambderGuardRefuse<TVocabulary, TRefusals, TCodesRequired>, payload: z.output<TInput>, param: TParam) => TOutput | Promise<TOutput>;
193
- } & LambderGuardPlacement & LambderGuardRefusals<TRefusals> & LambderGuardRefusalsKnownIn<TVocabulary, TRefusals> & LambderGuardAnswerCheck<TOutput>): LambderGuardOf<TOutput, {
221
+ } & LambderGuardInputUse & LambderGuardPlacement & LambderGuardRefusals<TRefusals> & LambderGuardRefusalsKnownIn<TVocabulary, TRefusals> & LambderGuardAnswerCheck<TOutput>): LambderGuardOf<TOutput, {
194
222
  refusals?: NoInfer<TRefusals>;
195
223
  guardInput: TInput;
196
224
  apiInput?: undefined;
225
+ singleUseInput?: boolean;
197
226
  session: true;
198
227
  handler: (ctx: TSessionCtx, payload: z.output<TInput>, param: TParam) => TOutput | Promise<TOutput>;
199
228
  }>;
200
229
  <TInput extends z.ZodType, TParam = undefined, TOutput = void, const TRefusals extends readonly string[] = readonly []>(guard: {
201
230
  guardInput: TInput;
202
231
  handler: (ctx: TCtx & LambderGuardRefuse<TVocabulary, TRefusals, TCodesRequired>, payload: z.output<TInput>, param: TParam) => TOutput | Promise<TOutput>;
203
- } & LambderGuardPlacement & LambderGuardRefusals<TRefusals> & LambderGuardRefusalsKnownIn<TVocabulary, TRefusals> & LambderGuardAnswerCheck<TOutput>): LambderGuardOf<TOutput, {
232
+ } & LambderGuardInputUse & LambderGuardPlacement & LambderGuardRefusals<TRefusals> & LambderGuardRefusalsKnownIn<TVocabulary, TRefusals> & LambderGuardAnswerCheck<TOutput>): LambderGuardOf<TOutput, {
204
233
  refusals?: NoInfer<TRefusals>;
205
234
  guardInput: TInput;
206
235
  apiInput?: undefined;
236
+ singleUseInput?: boolean;
207
237
  session?: undefined;
208
238
  handler: (ctx: TCtx, payload: z.output<TInput>, param: TParam) => TOutput | Promise<TOutput>;
209
239
  }>;
@@ -375,6 +405,15 @@ export declare class LambderApiGuardsEngine {
375
405
  configure(guards: Record<string, LambderApiGuard<any, any, any>>): void;
376
406
  /** Startup validation of one API registration's guards option. */
377
407
  assertRegistration(apiName: string, mode: LambderApiMode, guardsOption?: LambderGuardsOptionValue): void;
408
+ /**
409
+ * The posted guard inputs an idempotency fingerprint holds for an API
410
+ * declaring `guardsOption`: the value of each of its `guardInput` guards
411
+ * by name, except a guard's declared singleUseInput. Read as posted, as
412
+ * the payload is, before any guard parses it; a guard the client sent
413
+ * nothing for is left out. A null-prototype map, so a guard named
414
+ * "__proto__" is a key like any other.
415
+ */
416
+ fingerprintedInputsOf(request: LambderApiRequest, guardsOption: LambderGuardsOptionValue | undefined): Record<string, unknown>;
378
417
  /**
379
418
  * Run the API's guards that run at `runAt` (see LambderGuardRunAt) in
380
419
  * declared order. Refusals throw; outputs land on ctx.guardData; the
@@ -80,6 +80,13 @@ export class LambderApiGuardsEngine {
80
80
  if (runAt !== undefined && runAt !== "beforeInputValidation" && runAt !== "afterInputValidation") {
81
81
  throw new Error(`Lambder: guard "${name}" has runAt "${String(runAt)}"; use "beforeInputValidation" (default) or "afterInputValidation".`);
82
82
  }
83
+ const singleUseInput = guardDef.singleUseInput;
84
+ if (singleUseInput !== undefined && typeof singleUseInput !== "boolean") {
85
+ throw new Error(`Lambder: guard "${name}" has singleUseInput ${String(singleUseInput)}; write true for a value a retry carries fresh, or leave it off.`);
86
+ }
87
+ if (singleUseInput && !guardDef.guardInput) {
88
+ throw new Error(`Lambder: guard "${name}" declares singleUseInput without a guardInput. Only a value the client sends beside the payload can be left out of an idempotent request's fingerprint; a slice of the payload always counts.`);
89
+ }
83
90
  this.guards.set(name, guardDef);
84
91
  }
85
92
  }
@@ -105,6 +112,26 @@ export class LambderApiGuardsEngine {
105
112
  }
106
113
  }
107
114
  }
115
+ /**
116
+ * The posted guard inputs an idempotency fingerprint holds for an API
117
+ * declaring `guardsOption`: the value of each of its `guardInput` guards
118
+ * by name, except a guard's declared singleUseInput. Read as posted, as
119
+ * the payload is, before any guard parses it; a guard the client sent
120
+ * nothing for is left out. A null-prototype map, so a guard named
121
+ * "__proto__" is a key like any other.
122
+ */
123
+ fingerprintedInputsOf(request, guardsOption) {
124
+ const inputs = Object.create(null);
125
+ for (const { name } of toGuardEntries(guardsOption)) {
126
+ const guardDef = this.guards.get(name);
127
+ if (!guardDef?.guardInput || guardDef.singleUseInput)
128
+ continue;
129
+ const input = readGuardInput(request.guardInputs, name);
130
+ if (input !== undefined)
131
+ inputs[name] = input;
132
+ }
133
+ return inputs;
134
+ }
108
135
  /**
109
136
  * Run the API's guards that run at `runAt` (see LambderGuardRunAt) in
110
137
  * declared order. Refusals throw; outputs land on ctx.guardData; the
@@ -3,6 +3,7 @@ import type { LambderApiCallContext, LambderApiCallTrace } from "./LambderApiCal
3
3
  import type { LambderApiAnswer } from "./LambderApiAnswer.js";
4
4
  import type { LambderIdempotencyStore } from "../shared/contracts/LambderIdempotencyStore.js";
5
5
  import type { LambderApiIdempotencyOption } from "../shared/wire/LambderApiOptionValues.js";
6
+ import type { LambderKeyFieldDigest } from "../shared/util/LambderKeyFieldDigest.js";
6
7
  import { LAMBDER_BACKEND_SWAP } from "../shared/util/LambderTestingDoors.js";
7
8
  export type LambderApiIdempotencyConfig = {
8
9
  /** Your idempotency store instance; may share the rate limiter's table (distinct key prefix). */
@@ -57,12 +58,14 @@ export type LambderIdempotentCall = {
57
58
  export declare class LambderApiIdempotencyEngine {
58
59
  /** Stamped on the crash answer the engine records for an answer that broke its output schema. */
59
60
  private readonly apiVersion;
61
+ /** What every caller-controlled field of a scope is written as. */
62
+ private readonly keyFieldDigest;
60
63
  private store;
61
64
  private defaultTtlSeconds;
62
65
  private defaultPendingTtlSeconds;
63
66
  private failOpen;
64
67
  private callerIdentity;
65
- constructor(apiVersion: string | null);
68
+ constructor(apiVersion: string | null, keyFieldDigest: LambderKeyFieldDigest);
66
69
  configure(config: LambderApiIdempotencyConfig): void;
67
70
  /**
68
71
  * Puts the engine over another store, for `lambder/testing`; the replay
@@ -92,10 +95,12 @@ export declare class LambderApiIdempotencyEngine {
92
95
  * served before guards run.
93
96
  *
94
97
  * joinKeyFields escapes the fields, so no two distinct scopes collide.
95
- * The identity field is the caller's (a device token in the docs' own
96
- * example), so it is bounded first (boundKeyField): an over-long one
97
- * would push the scope past a store's key limit, and the store's refusal
98
- * is a throw that failOpen turns into no idempotency for that caller.
98
+ * Every field but the API name is the caller's (a session key, an
99
+ * identity such as the docs' device token, the posted key), so each is
100
+ * written as its digest (LambderKeyFieldDigest): a table read shows which
101
+ * API a record answers and never whose it is, and no field is long
102
+ * enough to push the scope past a store's key limit, whose refusal is a
103
+ * throw that failOpen turns into no idempotency for that caller.
99
104
  */
100
105
  private scopeOf;
101
106
  /**
@@ -105,9 +110,11 @@ export declare class LambderApiIdempotencyEngine {
105
110
  * refuses with a 400. Taken at the replay lookup, before input validation
106
111
  * replaces the payload with its parsed form, so the fingerprint is of the
107
112
  * request as it was posted, and callerIdentity (app code that may verify
108
- * a token or read a store) runs once.
113
+ * a token or read a store) runs once. `guardInputs` are the posted guard
114
+ * inputs the fingerprint holds, as the guards engine picks them
115
+ * (fingerprintedInputsOf): the engine that knows which are single use.
109
116
  */
110
- resolveKeyedCall(apiName: string, request: LambderApiRequest, ctx: LambderApiCallContext): Promise<LambderIdempotentCall | null>;
117
+ resolveKeyedCall(apiName: string, request: LambderApiRequest, ctx: LambderApiCallContext, guardInputs: Readonly<Record<string, unknown>>): Promise<LambderIdempotentCall | null>;
111
118
  /**
112
119
  * Replay fast path, run before guards and all rate limits except
113
120
  * `per: "ip"`: a completed record returns its stored answer, so a
@@ -1,7 +1,6 @@
1
1
  import { getAnswerHeader } from "../shared/wire/LambderAnswerHeaders.js";
2
2
  import { LambderApiRefusal, LAMBDER_REFUSAL_CODES } from "../shared/wire/LambderApiRefusal.js";
3
3
  import { joinKeyFields } from "../shared/util/joinKeyFields.js";
4
- import { boundKeyField } from "../shared/util/boundKeyField.js";
5
4
  import { assertPositiveInteger } from "../shared/util/LambderOptionChecks.js";
6
5
  import { LAMBDER_BACKEND_SWAP } from "../shared/util/LambderTestingDoors.js";
7
6
  import { canonicalJson } from "../shared/util/canonicalJson.js";
@@ -85,22 +84,26 @@ const keyReusedRefusal = (apiName) => new LambderApiRefusal(`Idempotency key reu
85
84
  });
86
85
  /**
87
86
  * A request's fingerprint: what makes two requests under one key the same
88
- * request. The payload as it was posted, as canonical JSON so key order does
89
- * not matter, digested so a record keeps 64 characters rather than the
90
- * payload. Guard inputs stay out: a captcha or proof token is single use, so
91
- * a genuine retry carries a new one, and a fingerprint over it would refuse
92
- * that retry as another request. A runtime without
93
- * WebCrypto (the mock on a page served over plain http, a phone on the LAN)
94
- * keeps the canonical JSON itself: a fingerprint is only compared with ones
95
- * the same runtime took, so its form does not matter, only that one payload
96
- * always gives the same one. Either form is non-empty (canonical JSON is at
97
- * least `null`), which is what lets a store report the empty string for a
98
- * record it cannot tie to any request.
87
+ * request. The payload as it was posted, beside the guard inputs that say
88
+ * what the call is about (see LambderApiGuardsEngine.fingerprintedInputsOf),
89
+ * as canonical JSON so key order does not matter, digested so a record keeps
90
+ * 64 characters rather than the request. A guard input selecting a store or
91
+ * a tenant is part of the request as surely as the payload is: left out, the
92
+ * same key sent for another tenant would replay the first tenant's answer. A
93
+ * guard input declared single use (a captcha token) is left out by the
94
+ * guards engine, since a genuine retry carries a fresh one.
95
+ *
96
+ * A runtime without WebCrypto (the mock on a page served over plain http, a
97
+ * phone on the LAN) keeps the canonical JSON itself: a fingerprint is only
98
+ * compared with ones the same runtime took, so its form does not matter,
99
+ * only that one request always gives the same one. Either form is non-empty,
100
+ * which is what lets a store report the empty string for a record it cannot
101
+ * tie to any request.
99
102
  */
100
- const requestFingerprintOf = async (payload) => {
103
+ const requestFingerprintOf = async (payload, guardInputs) => {
101
104
  let canonical;
102
105
  try {
103
- canonical = canonicalJson(payload ?? null);
106
+ canonical = canonicalJson({ payload: payload ?? null, guardInputs });
104
107
  }
105
108
  catch {
106
109
  // Sorting the keys recurses once per nesting level, so a payload
@@ -125,8 +128,9 @@ const requestFingerprintOf = async (payload) => {
125
128
  * is the right default, but silently it looks exactly like working: permanent
126
129
  * failures (a missing table, a missing IAM action, an SDK that would not
127
130
  * install) could have an app execute every retry twice for months with
128
- * nothing in its logs. The scope key is left out, since it carries the
129
- * caller's identity and posted key.
131
+ * nothing in its logs. The scope key is left out: digested as its caller's
132
+ * fields are, it is still the same for every attempt of one caller, and a
133
+ * log is read by more people than the table.
130
134
  */
131
135
  const reportFailOpen = (apiName, attempted, err) => {
132
136
  console.error(`Lambder idempotency: "${apiName}" could not ${attempted}; the request is being executed as if it carried no idempotency key. ` +
@@ -141,13 +145,16 @@ const reportFailOpen = (apiName, attempted, err) => {
141
145
  export class LambderApiIdempotencyEngine {
142
146
  /** Stamped on the crash answer the engine records for an answer that broke its output schema. */
143
147
  apiVersion;
148
+ /** What every caller-controlled field of a scope is written as. */
149
+ keyFieldDigest;
144
150
  store = null;
145
151
  defaultTtlSeconds = 24 * 3600;
146
152
  defaultPendingTtlSeconds = DEFAULT_IDEMPOTENCY_PENDING_TTL_SECONDS;
147
153
  failOpen = true;
148
154
  callerIdentity = undefined;
149
- constructor(apiVersion) {
155
+ constructor(apiVersion, keyFieldDigest) {
150
156
  this.apiVersion = apiVersion;
157
+ this.keyFieldDigest = keyFieldDigest;
151
158
  }
152
159
  configure(config) {
153
160
  if (this.store)
@@ -210,22 +217,25 @@ export class LambderApiIdempotencyEngine {
210
217
  * served before guards run.
211
218
  *
212
219
  * joinKeyFields escapes the fields, so no two distinct scopes collide.
213
- * The identity field is the caller's (a device token in the docs' own
214
- * example), so it is bounded first (boundKeyField): an over-long one
215
- * would push the scope past a store's key limit, and the store's refusal
216
- * is a throw that failOpen turns into no idempotency for that caller.
220
+ * Every field but the API name is the caller's (a session key, an
221
+ * identity such as the docs' device token, the posted key), so each is
222
+ * written as its digest (LambderKeyFieldDigest): a table read shows which
223
+ * API a record answers and never whose it is, and no field is long
224
+ * enough to push the scope past a store's key limit, whose refusal is a
225
+ * throw that failOpen turns into no idempotency for that caller.
217
226
  */
218
227
  async scopeOf(apiName, ctx, request, key) {
228
+ const postedKey = await this.keyFieldDigest.digestOf("key", key);
219
229
  const sessionKey = ctx.session?.sessionKey;
220
230
  if (sessionKey)
221
- return joinKeyFields(await boundKeyField("s", sessionKey), apiName, key);
231
+ return joinKeyFields(await this.keyFieldDigest.digestOf("s", sessionKey), apiName, postedKey);
222
232
  // No session to scope by. The app may still say who this is, through
223
233
  // callerIdentity; without one the key alone is the scope, which is
224
234
  // what makes it a bearer token for its own answer.
225
235
  const identity = this.callerIdentity ? await this.callerIdentity(ctx, request) : null;
226
236
  return identity
227
- ? joinKeyFields(await boundKeyField("i", identity), apiName, key)
228
- : joinKeyFields("k", apiName, key);
237
+ ? joinKeyFields(await this.keyFieldDigest.digestOf("i", identity), apiName, postedKey)
238
+ : joinKeyFields("k", apiName, postedKey);
229
239
  }
230
240
  /**
231
241
  * The call's key worked out, once per call: its scope and its request's
@@ -234,9 +244,11 @@ export class LambderApiIdempotencyEngine {
234
244
  * refuses with a 400. Taken at the replay lookup, before input validation
235
245
  * replaces the payload with its parsed form, so the fingerprint is of the
236
246
  * request as it was posted, and callerIdentity (app code that may verify
237
- * a token or read a store) runs once.
247
+ * a token or read a store) runs once. `guardInputs` are the posted guard
248
+ * inputs the fingerprint holds, as the guards engine picks them
249
+ * (fingerprintedInputsOf): the engine that knows which are single use.
238
250
  */
239
- async resolveKeyedCall(apiName, request, ctx) {
251
+ async resolveKeyedCall(apiName, request, ctx, guardInputs) {
240
252
  if (!this.store)
241
253
  return null;
242
254
  const key = this.readKey(request);
@@ -244,7 +256,7 @@ export class LambderApiIdempotencyEngine {
244
256
  return null;
245
257
  return {
246
258
  scopeKey: await this.scopeOf(apiName, ctx, request, key),
247
- fingerprint: await requestFingerprintOf(request.payload),
259
+ fingerprint: await requestFingerprintOf(request.payload, guardInputs),
248
260
  };
249
261
  }
250
262
  /**
@@ -39,8 +39,7 @@ export type LambderApiPipelineOptions<TCtx extends LambderApiCallContext<TSessio
39
39
  * The floor under the signature gate: a request naming a `version` below
40
40
  * it answers versionExpired whatever its signature says. Dotted numbers
41
41
  * ("1.2.10"), compared segment by segment. A floor above apiVersion is
42
- * taken as apiVersion, so a mistaken floor cannot refuse the build's own
43
- * clients.
42
+ * refused at construction, since it would refuse the build's own clients.
44
43
  */
45
44
  minApiVersion?: string | null;
46
45
  /**
@@ -60,6 +59,16 @@ export type LambderApiPipelineOptions<TCtx extends LambderApiCallContext<TSessio
60
59
  session: LambderSessionRecord<TSessionData>;
61
60
  }>>;
62
61
  idempotency?: LambderApiIdempotencyConfig;
62
+ /**
63
+ * The app's at-rest secret: the session salt, which both adapters pass
64
+ * when sessions are configured. A subkey derived from it for this one
65
+ * purpose keys the digest every caller-controlled field of a rate-limit
66
+ * tracker key and an idempotency scope is written as
67
+ * (LambderKeyFieldDigest), so a table read can neither show who called
68
+ * nor test a guess. Without one the digest is a plain SHA-256, which
69
+ * keeps the values out of the table but lets a reader test guesses.
70
+ */
71
+ atRestSecret?: string;
63
72
  };
64
73
  /** The stores `lambder/testing` puts under a built pipeline. One the pipeline has no subsystem for is left aside. */
65
74
  export type LambderPipelineBackends = {
@@ -93,11 +102,11 @@ export type LambderApiExec<TCtx> = (ctx: TCtx) => Promise<LambderApiAnswer>;
93
102
  * are adapters over this class; neither reimplements a step of it.
94
103
  *
95
104
  * ```
96
- * version floor → signature gate → restore payload → rate limits keyed per ip
97
- * → session (session mode) → idempotency replay → rate limits keyed per session
98
- * (and custom keys charged beforeGuards) → guards → input validation → guards
99
- * placed after it → rate limits keyed by a custom key → exec, inside the
100
- * idempotency claim → drain response headers → answer
105
+ * envelope check → version floor → signature gate → restore payload → rate
106
+ * limits keyed per ip → session (session mode) → idempotency replay → rate
107
+ * limits keyed per session (and custom keys charged beforeGuards) → guards →
108
+ * input validation → guards placed after it → rate limits keyed by a custom
109
+ * key → exec, inside the idempotency claim → drain response headers → answer
101
110
  * ```
102
111
  *
103
112
  * Each policy subsystem (rate limits, guards, idempotency) is its own
@@ -182,10 +191,15 @@ export declare class LambderApiPipeline<TCtx extends LambderApiCallContext<TSess
182
191
  */
183
192
  answerRefusal(apiName: string, refusals: LambderEndpointRefusals | undefined, thrown: LambderApiRefusal, ctx: TCtx): Promise<LambderApiAnswer>;
184
193
  /**
185
- * The steps that come before anything may read the request: the version
186
- * floor, the signature gate, then the compressed-payload restore that
187
- * every later reader (a rate-limit key slice, a guard, the input schema)
188
- * relies on.
194
+ * The steps that come before anything may read the request: the
195
+ * envelope check, the version floor, the signature gate, then the
196
+ * compressed-payload restore that every later reader (a rate-limit key
197
+ * slice, a guard, the input schema) relies on.
198
+ *
199
+ * A body that is no envelope (readApiEnvelope flagged it invalidEnvelope)
200
+ * carries nothing to call with, no payload, version or signature, and is
201
+ * answered with the invalid-payload refusal, here, so every adapter
202
+ * refuses it alike.
189
203
  *
190
204
  * The floor refuses a request naming a version below minApiVersion,
191
205
  * whatever its signature says: the lever for a change the digest cannot
@@ -11,6 +11,7 @@ import { LambderApiGuardsEngine } from "./LambderApiGuards.js";
11
11
  import { LambderApiRateLimitsEngine, } from "./LambderApiRateLimits.js";
12
12
  import { LambderApiIdempotencyEngine } from "./LambderApiIdempotency.js";
13
13
  import { LAMBDER_BACKEND_SWAP } from "../shared/util/LambderTestingDoors.js";
14
+ import { LambderKeyFieldDigest } from "../shared/util/LambderKeyFieldDigest.js";
14
15
  import LambderSessionController, { assertSessionCookiePrefixes, LambderSessionNotFoundError, } from "../session/LambderSessionController.js";
15
16
  import { DEFAULT_SESSION_CSRF_COOKIE_KEY, DEFAULT_SESSION_TOKEN_COOKIE_KEY } from "../shared/wire/LambderSessionCookieNames.js";
16
17
  /** An API that asks for idempotency: declared, and not the explicit `false` opt-out. */
@@ -21,11 +22,11 @@ const usesIdempotency = (definition) => definition.idempotency !== undefined &&
21
22
  * are adapters over this class; neither reimplements a step of it.
22
23
  *
23
24
  * ```
24
- * version floor → signature gate → restore payload → rate limits keyed per ip
25
- * → session (session mode) → idempotency replay → rate limits keyed per session
26
- * (and custom keys charged beforeGuards) → guards → input validation → guards
27
- * placed after it → rate limits keyed by a custom key → exec, inside the
28
- * idempotency claim → drain response headers → answer
25
+ * envelope check → version floor → signature gate → restore payload → rate
26
+ * limits keyed per ip → session (session mode) → idempotency replay → rate
27
+ * limits keyed per session (and custom keys charged beforeGuards) → guards →
28
+ * input validation → guards placed after it → rate limits keyed by a custom
29
+ * key → exec, inside the idempotency claim → drain response headers → answer
29
30
  * ```
30
31
  *
31
32
  * Each policy subsystem (rate limits, guards, idempotency) is its own
@@ -52,7 +53,7 @@ const usesIdempotency = (definition) => definition.idempotency !== undefined &&
52
53
  export class LambderApiPipeline {
53
54
  apiVersion;
54
55
  minApiVersion;
55
- rateLimits = new LambderApiRateLimitsEngine();
56
+ rateLimits;
56
57
  guards = new LambderApiGuardsEngine();
57
58
  idempotency;
58
59
  maxRequestPayloadBytes;
@@ -75,14 +76,19 @@ export class LambderApiPipeline {
75
76
  }
76
77
  // A floor above the version this server stamps would refuse this
77
78
  // build's own clients, and the first symptom would be every tab
78
- // reloading. The floor is clamped to apiVersion and the mistake
79
- // reported once, at creation.
79
+ // reloading. Refused at creation, as every other misconfiguration
80
+ // is, rather than quietly read as some other floor than the one
81
+ // written: which of the two versions is wrong is the app's call.
80
82
  if (this.apiVersion !== null && compareDottedVersions(this.minApiVersion, this.apiVersion) > 0) {
81
- console.warn(`Lambder: minApiVersion ${this.minApiVersion} is above apiVersion ${this.apiVersion}; the floor is taken as ${this.apiVersion}.`);
82
- this.minApiVersion = this.apiVersion;
83
+ throw new Error(`Lambder: minApiVersion ${this.minApiVersion} is above apiVersion ${this.apiVersion}, so it would refuse this build's own clients. ` +
84
+ `Set minApiVersion to the oldest client version still served, at most ${this.apiVersion}.`);
83
85
  }
84
86
  }
85
- this.idempotency = new LambderApiIdempotencyEngine(this.apiVersion);
87
+ // One digest for both engines, so its subkey is derived once per
88
+ // instance.
89
+ const keyFieldDigest = new LambderKeyFieldDigest(options.atRestSecret ?? null);
90
+ this.rateLimits = new LambderApiRateLimitsEngine(keyFieldDigest);
91
+ this.idempotency = new LambderApiIdempotencyEngine(this.apiVersion, keyFieldDigest);
86
92
  this.apiSignatures = options.apiSignatures ?? null;
87
93
  this.maxRequestPayloadBytes = assertPositiveInteger(options.maxRequestPayloadBytes ?? DEFAULT_MAX_RESTORED_PAYLOAD_BYTES, "maxRequestPayloadBytes");
88
94
  this.onInvalidInput = options.onInvalidInput ?? null;
@@ -211,10 +217,15 @@ export class LambderApiPipeline {
211
217
  return refusalAnswer(checked, this.apiVersion, ctx.logList);
212
218
  }
213
219
  /**
214
- * The steps that come before anything may read the request: the version
215
- * floor, the signature gate, then the compressed-payload restore that
216
- * every later reader (a rate-limit key slice, a guard, the input schema)
217
- * relies on.
220
+ * The steps that come before anything may read the request: the
221
+ * envelope check, the version floor, the signature gate, then the
222
+ * compressed-payload restore that every later reader (a rate-limit key
223
+ * slice, a guard, the input schema) relies on.
224
+ *
225
+ * A body that is no envelope (readApiEnvelope flagged it invalidEnvelope)
226
+ * carries nothing to call with, no payload, version or signature, and is
227
+ * answered with the invalid-payload refusal, here, so every adapter
228
+ * refuses it alike.
218
229
  *
219
230
  * The floor refuses a request naming a version below minApiVersion,
220
231
  * whatever its signature says: the lever for a change the digest cannot
@@ -236,6 +247,8 @@ export class LambderApiPipeline {
236
247
  * ready to dispatch.
237
248
  */
238
249
  async prepare(request) {
250
+ if (request.invalidEnvelope !== undefined)
251
+ return invalidPayloadAnswer(this.apiVersion, request.invalidEnvelope);
239
252
  if (this.minApiVersion !== null && request.version !== null && compareDottedVersions(request.version, this.minApiVersion) < 0) {
240
253
  return versionExpiredAnswer(this.apiVersion);
241
254
  }
@@ -318,8 +331,12 @@ export class LambderApiPipeline {
318
331
  // Replay fast path: a completed idempotent request answers its stored
319
332
  // answer without burning the remaining rate-limit quota or re-running
320
333
  // guards. After the session read, because the replay scope is keyed
321
- // per user, by the session's sessionKey.
322
- const keyedCall = usesIdempotency(definition) ? await this.idempotency.resolveKeyedCall(definition.name, request, ctx) : null;
334
+ // per user, by the session's sessionKey. The guards engine says which
335
+ // posted guard inputs belong to the request's fingerprint, since only
336
+ // it knows which of them are single use.
337
+ const keyedCall = usesIdempotency(definition)
338
+ ? await this.idempotency.resolveKeyedCall(definition.name, request, ctx, this.guards.fingerprintedInputsOf(request, definition.guards))
339
+ : null;
323
340
  const replay = keyedCall ? await this.idempotency.findReplay(definition.name, keyedCall, trace) : null;
324
341
  if (replay)
325
342
  return replay;
@@ -1,31 +1,33 @@
1
1
  import type { LambderApiMode } from "../shared/wire/LambderApiContract.js";
2
2
  import type { LambderRateLimitMessage, LambderRateLimitOptionValue, LambderRateLimitOverride } from "../shared/wire/LambderApiOptionValues.js";
3
+ import type { LambderKeyFieldDigest } from "../shared/util/LambderKeyFieldDigest.js";
3
4
  import type { z } from "zod";
4
5
  import type { LambderApiRequest } from "./LambderApiRequest.js";
5
6
  import type { LambderApiCallContext } from "./LambderApiCallContext.js";
6
7
  import { type LambderRateLimiter, type LambderRateLimitExceeded, type LambderRateLimitPolicy } from "../shared/contracts/LambderRateLimiter.js";
7
8
  import type { LambderSessionRecord } from "../shared/contracts/LambderSessionStore.js";
8
- import { LambderApiRefusal } from "../shared/wire/LambderApiRefusal.js";
9
+ import { LambderApiRefusal, type LambderRateLimitRefusalData } from "../shared/wire/LambderApiRefusal.js";
9
10
  import type { LambderNonEmptyOptionMap } from "../shared/util/LambderTypeUtilities.js";
10
11
  import { LAMBDER_BACKEND_SWAP } from "../shared/util/LambderTestingDoors.js";
11
- /** Refusal a rate-limited request answers unless the policy or the API's override names its own. */
12
+ /** The words a rate-limited call answers with unless the policy or the API's override writes its own (the `refusal` option of each). */
12
13
  export declare const DEFAULT_RATE_LIMIT_REFUSAL: {
13
14
  type: "warning";
14
- code: "lambder/rate-limited";
15
15
  content: string;
16
16
  };
17
17
  /**
18
18
  * The refusal a rate-limited call answers with: a 429 envelope carrying the
19
- * framework code, whatever message a policy or an API wrote, and a
20
- * Retry-After header. The engine throws it; the mock runtime's failure
21
- * injection throws the same one, so an injected rate limit is
22
- * indistinguishable from a real one.
19
+ * framework code, the words a policy or an API wrote, the policy's name and
20
+ * the seconds to wait as its data, and a Retry-After header of those same
21
+ * seconds. The engine throws it; the mock runtime's failure injection throws
22
+ * the same one, so an injected rate limit is indistinguishable from a real
23
+ * one.
23
24
  *
24
- * The code goes on after the message, so a message that carries one anyway
25
- * (a plain-JS caller, a cast) cannot replace it: a rate limit is never a code
26
- * an endpoint declares, and the pipeline would refuse to send one it did not.
25
+ * Only the words are read off the message, so one that carries a code or
26
+ * data anyway (a plain-JS caller, a cast) cannot replace them: a rate limit
27
+ * is never a code an endpoint declares, and the pipeline sends one only with
28
+ * the data a client narrows it by.
27
29
  */
28
- export declare const rateLimitRefusal: (detail: string, retryAfterSeconds: number, message?: LambderRateLimitMessage) => LambderApiRefusal;
30
+ export declare const rateLimitRefusal: (detail: string, data: LambderRateLimitRefusalData, message?: LambderRateLimitMessage) => LambderApiRefusal;
29
31
  /**
30
32
  * A custom rate-limit key. `apiInput` names the fields of the API's OWN
31
33
  * payload the key derives from: that slice is validated against the raw
@@ -110,7 +112,7 @@ export type LambderApiRateLimitPolicyConfig<TCtx = any> = LambderRateLimitPolicy
110
112
  * takes it: `per: "ip"` and `per: "session"` have one place each.
111
113
  */
112
114
  chargeAt?: LambderRateLimitChargeAt;
113
- /** The refusal's message for refused requests: its type, title and content, under the code "lambder/rate-limited". Default: a warning saying too many requests. */
115
+ /** The refusal's words for refused requests: its type, title and content, under the code "lambder/rate-limited" and beside the data that names this policy to the client. Default: a warning saying too many requests. */
114
116
  refusal?: LambderRateLimitMessage;
115
117
  };
116
118
  export type LambderApiRateLimitsConfig<TPolicies extends Record<string, LambderApiRateLimitPolicyConfig<any>>> = {
@@ -276,8 +278,13 @@ type LambderRateLimitPhase = "beforeSession" | LambderRateLimitChargeAt;
276
278
  * slice) and hands the context to a custom key's handler, reading nothing
277
279
  * of the context itself but its session, so it runs unchanged under the
278
280
  * server and the mock runtime.
281
+ *
282
+ * The caller's half of every tracker key (the address, the session key, a
283
+ * custom key) reaches the limiter as a digest (LambderKeyFieldDigest), so a
284
+ * table read names the API and the policy and never who was counted.
279
285
  */
280
286
  export declare class LambderApiRateLimitsEngine {
287
+ private readonly keyFieldDigest;
281
288
  private limiter;
282
289
  private failOpen;
283
290
  private ipv6PrefixLength;
@@ -289,6 +296,7 @@ export declare class LambderApiRateLimitsEngine {
289
296
  * second would otherwise be as many identical log lines.
290
297
  */
291
298
  private readonly loggedFailures;
299
+ constructor(keyFieldDigest: LambderKeyFieldDigest);
292
300
  /** True once rateLimits were configured. */
293
301
  get isConfigured(): boolean;
294
302
  configure(config: LambderApiRateLimitsConfig<Record<string, LambderApiRateLimitPolicyConfig>>): void;
@@ -318,7 +326,7 @@ export declare class LambderApiRateLimitsEngine {
318
326
  /**
319
327
  * One policy charged by code rather than by a declaration, for
320
328
  * `ctx.rateLimit` and `ctx.isRateLimited`. Counters, failOpen, key
321
- * bounding and refusal are those of a declared limit; only who knows the
329
+ * digests and refusal are those of a declared limit; only who knows the
322
330
  * key differs. A policy without `per` takes the key the code passes, a
323
331
  * `per: "ip"` or `per: "session"` one reads it off the request, and one
324
332
  * keyed by an API payload is refused, since only its APIs know the key.
@@ -334,20 +342,19 @@ export declare class LambderApiRateLimitsEngine {
334
342
  /**
335
343
  * Counts one attempt, or lets it through when the limiter itself fails
336
344
  * and failOpen is on. The log line names the policy and its windows,
337
- * never the tracker key: the key carries whatever a custom handler
338
- * returned, which the docs' own example makes an email address. A
339
- * failure the limiter throws again (see loggedFailures) is not logged
340
- * again.
345
+ * never the tracker key: digested as its caller's half is, the key is
346
+ * still the same for every attempt of one caller, and a log is read by
347
+ * more people than the table. A failure the limiter throws again (see
348
+ * loggedFailures) is not logged again.
341
349
  */
342
350
  private countAttempt;
343
351
  private chargeKeyOf;
344
352
  private resolveKey;
345
353
  /**
346
354
  * The key a `per: "ip"` or `per: "session"` policy counts under: what the
347
- * request carries, however the policy is charged. A session key is
348
- * bounded like a custom one (see boundKeyField); an address needs no
349
- * bound, since normalizeClientIp caps it at 45 characters whichever
350
- * header or gateway field named it.
355
+ * request carries, however the policy is charged, digested as a custom
356
+ * key is. The address is the subject rateLimitSubjectOf counts (an IPv6
357
+ * caller's /64), so the digest of one subscriber's every address is one.
351
358
  */
352
359
  private requestKeyOf;
353
360
  }