lambder 8.1.1 → 8.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. package/CHANGELOG.md +164 -0
  2. package/README.md +8 -7
  3. package/dist/api/LambderApiGuards.d.ts +2 -17
  4. package/dist/api/LambderApiRateLimits.d.ts +2 -29
  5. package/dist/build/ContractTypePrinter.d.ts +39 -9
  6. package/dist/build/ContractTypePrinter.js +89 -31
  7. package/dist/build/generatedTables.d.ts +72 -0
  8. package/dist/build/generatedTables.js +99 -0
  9. package/dist/build/writeApiContract.d.ts +2 -1
  10. package/dist/build/writeApiContract.js +2 -1
  11. package/dist/build/writeApiGuardParams.d.ts +60 -0
  12. package/dist/build/writeApiGuardParams.js +85 -0
  13. package/dist/build/writeApiOptions.d.ts +68 -0
  14. package/dist/build/writeApiOptions.js +102 -0
  15. package/dist/build.d.ts +10 -4
  16. package/dist/build.js +7 -4
  17. package/dist/client/LambderUploadRunner.d.ts +7 -7
  18. package/dist/client/LambderUploadRunner.js +23 -32
  19. package/dist/client.d.ts +7 -0
  20. package/dist/client.js +11 -0
  21. package/dist/core/Lambder.d.ts +21 -0
  22. package/dist/core/Lambder.js +69 -0
  23. package/dist/index.d.ts +13 -0
  24. package/dist/index.js +13 -0
  25. package/dist/mock/LambderMockApp.d.ts +34 -17
  26. package/dist/mock/LambderMockApp.js +67 -21
  27. package/dist/mock/LambderMockCreateOptions.d.ts +68 -5
  28. package/dist/mock/LambderMockTypes.d.ts +29 -10
  29. package/dist/mock/lambderMockPoliciesFrom.d.ts +51 -0
  30. package/dist/mock/lambderMockPoliciesFrom.js +46 -0
  31. package/dist/mock.d.ts +3 -0
  32. package/dist/mock.js +3 -0
  33. package/dist/secrets/LambderOneShotSecrets.d.ts +166 -0
  34. package/dist/secrets/LambderOneShotSecrets.js +217 -0
  35. package/dist/session/LambderSessionCrypto.js +6 -16
  36. package/dist/shared/contracts/LambderIdempotencyStore.d.ts +3 -2
  37. package/dist/shared/contracts/LambderOneShotSecretStore.d.ts +122 -0
  38. package/dist/shared/contracts/LambderOneShotSecretStore.js +38 -0
  39. package/dist/shared/util/LambderBackoffTimer.d.ts +82 -0
  40. package/dist/shared/util/LambderBackoffTimer.js +86 -0
  41. package/dist/shared/util/LambderBase64.d.ts +14 -0
  42. package/dist/shared/util/LambderBase64.js +17 -0
  43. package/dist/shared/util/LambderSignedClaims.d.ts +78 -0
  44. package/dist/shared/util/LambderSignedClaims.js +109 -0
  45. package/dist/shared/util/LambderTextDigest.d.ts +19 -5
  46. package/dist/shared/util/LambderTextDigest.js +30 -5
  47. package/dist/shared/util/assertPlainData.d.ts +9 -0
  48. package/dist/shared/util/assertPlainData.js +41 -0
  49. package/dist/shared/util/escapeXmlText.d.ts +8 -0
  50. package/dist/shared/util/escapeXmlText.js +8 -0
  51. package/dist/shared/wire/LambderApiOptionEntries.d.ts +148 -0
  52. package/dist/shared/wire/LambderApiOptionEntries.js +35 -0
  53. package/dist/shared/wire/LambderUploadObjectFields.js +2 -2
  54. package/dist/stores/LambderDdbOneShotSecretStore.d.ts +64 -0
  55. package/dist/stores/LambderDdbOneShotSecretStore.js +266 -0
  56. package/dist/stores/LambderMemoryIdempotencyStore.d.ts +3 -2
  57. package/dist/stores/LambderMemoryIdempotencyStore.js +3 -2
  58. package/dist/stores/LambderMemoryOneShotSecretStore.d.ts +36 -0
  59. package/dist/stores/LambderMemoryOneShotSecretStore.js +93 -0
  60. package/dist/stores/LambderMemoryUploadBucket.js +2 -2
  61. package/dist/testing/LambderConformanceRunner.d.ts +46 -0
  62. package/dist/testing/LambderConformanceRunner.js +21 -0
  63. package/dist/testing/lambderIdempotencyStoreConformance.d.ts +33 -0
  64. package/dist/testing/lambderIdempotencyStoreConformance.js +237 -0
  65. package/dist/testing/lambderOneShotSecretStoreConformance.d.ts +43 -0
  66. package/dist/testing/lambderOneShotSecretStoreConformance.js +224 -0
  67. package/dist/testing/lambderRateLimiterConformance.d.ts +20 -0
  68. package/dist/testing/lambderRateLimiterConformance.js +72 -0
  69. package/dist/testing/lambderSessionStoreConformance.d.ts +27 -0
  70. package/dist/testing/lambderSessionStoreConformance.js +165 -0
  71. package/dist/testing.d.ts +14 -0
  72. package/dist/testing.js +12 -0
  73. package/package.json +1 -1
package/dist/client.d.ts CHANGED
@@ -26,6 +26,8 @@ export type { LambderIdempotencyKeyScope } from "./shared/wire/LambderIdempotenc
26
26
  export { LambderApiRefusal, isLambderApiRefusal, refuse, refusalMessageOf, LAMBDER_REFUSAL_CODES } from "./shared/wire/LambderApiRefusal.js";
27
27
  export type { LambderApiRefusalOptions, LambderRefusalMessage, LambderAppRefusalMessage, LambderRefusalCode, LambderRefuseOptions } from "./shared/wire/LambderApiRefusal.js";
28
28
  export type { LambderApiContractShape, LambderApiMode, LambderApiEnvelopeBody, LambderApiResponseConfig, LambderGuardNamesIn, LambderContractMode, LambderContractKeysWithMode, LambderContractKeysWithGuard, LambderJsonOf, LambderJsonOutputOf, LambderContractGuardsOf, LambderContractGuardNames, LambderContractGuardInputsOf, LambderContractGuardInput, LambderContractGuardInputNames, LambderContractRateLimitOf, LambderContractRateLimitNames, LambderContractIdempotencyOf, } from "./shared/wire/LambderApiContract.js";
29
+ export { apiGuardParam } from "./shared/wire/LambderApiOptionEntries.js";
30
+ export type { LambderApiOptionEntries, LambderApiOptionEntry, LambderRateLimitPolicyEntry, LambderGuardDeclarationEntry, LambderApisWithGuard, LambderApisGuardedBy, LambderApisWithMode, LambderGuardParamOf, } from "./shared/wire/LambderApiOptionEntries.js";
29
31
  export { describeCrash, errorFromCrashDetail } from "./shared/wire/LambderCrashDetail.js";
30
32
  export type { LambderCrashDetail, LambderCrashCause } from "./shared/wire/LambderCrashDetail.js";
31
33
  export { compressPayloadGzip, isRequestCompressionAvailable, COMPRESSED_PAYLOAD_GZ_FIELD, COMPRESSED_PAYLOAD_BR_FIELD, COMPRESSED_PAYLOAD_BYTES_FIELD, DEFAULT_REQUEST_COMPRESSION_SETTINGS, } from "./shared/wire/LambderRequestPayload.js";
@@ -36,6 +38,11 @@ export { html, xml, raw, jsonScript, escapeHtml, renderHtmlValue, LambderSafeHtm
36
38
  export { createLambderI18n } from "./shared/LambderI18n.js";
37
39
  export type { LambderLanguageMeta, LambderI18nConfig, LambderI18nInstance, LambderI18nTranslator, LambderI18nExtractParams, LambderI18nDictionaryLoader, LambderI18nCodes, LambderI18nKeys, LambderI18nTranslatorFor, } from "./shared/LambderI18n.js";
38
40
  export type { LambderHttpStatusCode } from "./shared/wire/LambderHttpStatus.js";
41
+ export { LambderBackoffTimer } from "./shared/util/LambderBackoffTimer.js";
42
+ export type { LambderBackoffTimerOptions } from "./shared/util/LambderBackoffTimer.js";
43
+ export { LambderSignedClaims, keyedDigest, randomSecret } from "./shared/util/LambderSignedClaims.js";
44
+ export type { LambderSignedClaimsOptions } from "./shared/util/LambderSignedClaims.js";
45
+ export { constantTimeEquals } from "./shared/util/LambderTextDigest.js";
39
46
  export { LambderUploadRunner, LambderUploadError } from "./client/LambderUploadRunner.js";
40
47
  export type { LambderUploadRunnerOptions, LambderUploadProgress, LambderUploadPhase, LambderUploadFailureReason } from "./client/LambderUploadRunner.js";
41
48
  export { checkUploadRule } from "./shared/contracts/LambderUploadBucket.js";
package/dist/client.js CHANGED
@@ -23,6 +23,9 @@ export { createIdempotencyKey, createIdempotencyKeyScope } from "./shared/wire/L
23
23
  // Typed API refusals (isomorphic: shared code may throw them from anywhere;
24
24
  // in the browser they are plain Errors).
25
25
  export { LambderApiRefusal, isLambderApiRefusal, refuse, refusalMessageOf, LAMBDER_REFUSAL_CODES } from "./shared/wire/LambderApiRefusal.js";
26
+ // The server's declared options as plain data (the generated options module's
27
+ // entry types) and the readers a client derives its own facts from.
28
+ export { apiGuardParam } from "./shared/wire/LambderApiOptionEntries.js";
26
29
  // A crash described for a caller allowed to see it (the envelope's `crash` field; pure, no Node built-ins).
27
30
  export { describeCrash, errorFromCrashDetail } from "./shared/wire/LambderCrashDetail.js";
28
31
  // Request payload compression (browser-safe: gzip via CompressionStream, no Node built-ins).
@@ -33,6 +36,14 @@ export { resolveCompressionOption } from "./shared/wire/LambderCompressionOption
33
36
  export { html, xml, raw, jsonScript, escapeHtml, renderHtmlValue, LambderSafeHtml } from "./shared/LambderHtml.js";
34
37
  // Typed translations (standalone, isomorphic)
35
38
  export { createLambderI18n } from "./shared/LambderI18n.js";
39
+ // Waiting longer after each failure, once: for a reconnecting client or a
40
+ // screen that has to come back by itself.
41
+ export { LambderBackoffTimer } from "./shared/util/LambderBackoffTimer.js";
42
+ // Signed claims tokens, for the isomorphic code that verifies them where a
43
+ // server's secret is at hand (an edge Worker, a shared backend package);
44
+ // never in a page, which holds no secret to verify with.
45
+ export { LambderSignedClaims, keyedDigest, randomSecret } from "./shared/util/LambderSignedClaims.js";
46
+ export { constantTimeEquals } from "./shared/util/LambderTextDigest.js";
36
47
  // Direct uploads: the runner that takes a file from the browser straight to
37
48
  // storage, and the vocabulary it shares with the server's bucket.
38
49
  export { LambderUploadRunner, LambderUploadError } from "./client/LambderUploadRunner.js";
@@ -14,6 +14,7 @@ import type { LambderFileSource } from "../shared/contracts/LambderFileSource.js
14
14
  import { LAMBDER_BACKEND_SWAP, LAMBDER_CRASH_WATCH } from "../shared/util/LambderTestingDoors.js";
15
15
  import { type LambderApiSignatureEntry } from "../api/LambderApiSignature.js";
16
16
  import { type LambderApiSignatureMap } from "../shared/wire/LambderApiSignature.js";
17
+ import type { LambderApiOptionEntries } from "../shared/wire/LambderApiOptionEntries.js";
17
18
  import type { LambderApiIdempotencyOption } from "../shared/wire/LambderApiOptionValues.js";
18
19
  import type { LambderApiGuard, LambderGuardMetaMap, LambderGuardsOption, LambderGuardDataOf, LambderGuardInputsOf } from "../api/LambderApiGuards.js";
19
20
  import type { LambderApiRateLimitPolicyConfig, LambderRateLimitOption } from "../api/LambderApiRateLimits.js";
@@ -91,6 +92,8 @@ export default class Lambder<TSessionData = any, _TContract extends Record<strin
91
92
  private readonly apiDefinitions;
92
93
  /** The guards map given at creation, kept for apiSignatures(): a guard's schema is part of the signature of every endpoint declaring it. */
93
94
  private readonly guards;
95
+ /** The rate-limit policies given at creation, kept for apiOptionEntries(), which records each one less its key handler. */
96
+ private readonly rateLimitPolicies;
94
97
  private hookList;
95
98
  private createdHooks;
96
99
  private initPromise;
@@ -246,6 +249,24 @@ export default class Lambder<TSessionData = any, _TContract extends Record<strin
246
249
  * unless the generator writes it there.
247
250
  */
248
251
  apiSignatureEntries(): Promise<LambderApiSignatureEntry[]>;
252
+ /**
253
+ * Every registered API's mode and declared options as plain data, with
254
+ * the rate-limit policies and guards they name reduced to what is not
255
+ * code: what writeApiOptions (lambder/build) writes to a module a client,
256
+ * a mock or a test imports instead of the server. The contract carries
257
+ * the same options as types; this is the same fact as a value, for code
258
+ * that decides something at runtime with it.
259
+ *
260
+ * Nothing here is a secret or a handler by construction. A guard's
261
+ * parameter is written as it was declared, so it has to be plain data
262
+ * (a permission string, a list, a reason); one that is not fails by API
263
+ * and guard name. A policy's key handler is never written: its `per`
264
+ * says "custom" and no more. A guard's input schema is never written
265
+ * either; its declaration says only which of the three input modes it
266
+ * has. Every table is sorted by name, so the module diffs by endpoint
267
+ * and never moves when registrations are reordered.
268
+ */
269
+ apiOptionEntries(): LambderApiOptionEntries;
249
270
  getResponseBuilder(ctx?: LambderRenderContext): LambderResponseBuilder<any>;
250
271
  private getResolver;
251
272
  getHandler(): LambderHandler;
@@ -15,6 +15,7 @@ import { LambderApiPipeline } from "../api/LambderApiPipeline.js";
15
15
  import { LAMBDER_BACKEND_SWAP, LAMBDER_CRASH_WATCH } from "../shared/util/LambderTestingDoors.js";
16
16
  import { apiSignatureOf } from "../api/LambderApiSignature.js";
17
17
  import { apiNameKeyOf } from "../shared/wire/LambderApiSignature.js";
18
+ import { assertPlainData } from "../shared/util/assertPlainData.js";
18
19
  import { apiNotFoundAnswer, refusalAnswer, sessionExpiredAnswer, } from "../api/LambderApiEnvelope.js";
19
20
  import { bindContextTools, createContext, isV2HttpEvent, } from "./LambderContext.js";
20
21
  import { COMPRESSED_PAYLOAD_GZ_FIELD, COMPRESSED_PAYLOAD_BR_FIELD, COMPRESSED_PAYLOAD_BYTES_FIELD } from "../shared/wire/LambderRequestPayload.js";
@@ -80,6 +81,8 @@ export default class Lambder {
80
81
  apiDefinitions = new Map();
81
82
  /** The guards map given at creation, kept for apiSignatures(): a guard's schema is part of the signature of every endpoint declaring it. */
82
83
  guards;
84
+ /** The rate-limit policies given at creation, kept for apiOptionEntries(), which records each one less its key handler. */
85
+ rateLimitPolicies;
83
86
  hookList = { "beforeRender": [], "afterRender": [], "fallback": [] };
84
87
  createdHooks = [];
85
88
  initPromise = null;
@@ -122,6 +125,7 @@ export default class Lambder {
122
125
  }
123
126
  const session = options.session;
124
127
  this.guards = options.guards;
128
+ this.rateLimitPolicies = options.rateLimits?.policies;
125
129
  this.pipeline = new LambderApiPipeline({
126
130
  apiVersion: this.apiVersion,
127
131
  minApiVersion: options.minApiVersion,
@@ -413,6 +417,71 @@ export default class Lambder {
413
417
  entries.sort((a, b) => (a.key < b.key ? -1 : a.key > b.key ? 1 : 0));
414
418
  return entries;
415
419
  }
420
+ /**
421
+ * Every registered API's mode and declared options as plain data, with
422
+ * the rate-limit policies and guards they name reduced to what is not
423
+ * code: what writeApiOptions (lambder/build) writes to a module a client,
424
+ * a mock or a test imports instead of the server. The contract carries
425
+ * the same options as types; this is the same fact as a value, for code
426
+ * that decides something at runtime with it.
427
+ *
428
+ * Nothing here is a secret or a handler by construction. A guard's
429
+ * parameter is written as it was declared, so it has to be plain data
430
+ * (a permission string, a list, a reason); one that is not fails by API
431
+ * and guard name. A policy's key handler is never written: its `per`
432
+ * says "custom" and no more. A guard's input schema is never written
433
+ * either; its declaration says only which of the three input modes it
434
+ * has. Every table is sorted by name, so the module diffs by endpoint
435
+ * and never moves when registrations are reordered.
436
+ */
437
+ apiOptionEntries() {
438
+ const apis = {};
439
+ for (const name of [...this.apiDefinitions.keys()].sort()) {
440
+ const { mode, guards, rateLimit, idempotency } = this.apiDefinitions.get(name);
441
+ const entry = { mode };
442
+ if (guards !== undefined) {
443
+ assertPlainData(guards, `the guards option of API "${name}"`);
444
+ entry.guards = guards;
445
+ }
446
+ if (rateLimit !== undefined) {
447
+ assertPlainData(rateLimit, `the rateLimit option of API "${name}"`);
448
+ entry.rateLimit = rateLimit;
449
+ }
450
+ if (idempotency !== undefined)
451
+ entry.idempotency = idempotency;
452
+ apis[name] = entry;
453
+ }
454
+ const rateLimitPolicies = {};
455
+ for (const name of Object.keys(this.rateLimitPolicies ?? {}).sort()) {
456
+ const { per, budget, chargeAt, errorMessage, ...windows } = this.rateLimitPolicies[name];
457
+ const entry = {};
458
+ for (const [window, limit] of Object.entries(windows)) {
459
+ if (limit !== undefined)
460
+ entry[window] = limit;
461
+ }
462
+ if (per !== undefined)
463
+ entry.per = per === "ip" || per === "session" ? per : "custom";
464
+ if (budget !== undefined)
465
+ entry.budget = budget;
466
+ if (chargeAt !== undefined)
467
+ entry.chargeAt = chargeAt;
468
+ if (errorMessage !== undefined) {
469
+ assertPlainData(errorMessage, `the errorMessage of rate-limit policy "${name}"`);
470
+ entry.errorMessage = errorMessage;
471
+ }
472
+ rateLimitPolicies[name] = entry;
473
+ }
474
+ const guards = {};
475
+ for (const name of Object.keys(this.guards ?? {}).sort()) {
476
+ const guard = this.guards[name];
477
+ guards[name] = {
478
+ input: guard.apiInput ? "apiInput" : guard.guardInput ? "guardInput" : "none",
479
+ session: guard.session === true,
480
+ runAt: guard.runAt ?? "beforeInputValidation",
481
+ };
482
+ }
483
+ return { apis, rateLimitPolicies, guards };
484
+ }
416
485
  getResponseBuilder(ctx) {
417
486
  return new LambderResponseBuilder({
418
487
  files: this.files,
package/dist/index.d.ts CHANGED
@@ -12,6 +12,11 @@ export { lambderCookieJarTransport } from "./shared/transport/lambderCookieJarTr
12
12
  export type { LambderApiTransport, LambderApiTransportRequest, LambderTransportFailureReason } from "./shared/transport/LambderApiTransport.js";
13
13
  export { LambderCookieJar, parseSetCookie } from "./shared/transport/LambderCookieJar.js";
14
14
  export { LambderExpiringMap, LambderExpiringMapFullError } from "./shared/util/LambderExpiringMap.js";
15
+ export { LambderBackoffTimer } from "./shared/util/LambderBackoffTimer.js";
16
+ export type { LambderBackoffTimerOptions } from "./shared/util/LambderBackoffTimer.js";
17
+ export { LambderSignedClaims, keyedDigest, randomSecret } from "./shared/util/LambderSignedClaims.js";
18
+ export type { LambderSignedClaimsOptions } from "./shared/util/LambderSignedClaims.js";
19
+ export { constantTimeEquals } from "./shared/util/LambderTextDigest.js";
15
20
  export type { LambderStoredCookie } from "./shared/transport/LambderCookieJar.js";
16
21
  export { lambderHandlerTransport } from "./invoke/lambderHandlerTransport.js";
17
22
  export type { LambderHandlerTransportOptions } from "./invoke/lambderHandlerTransport.js";
@@ -106,6 +111,12 @@ export type { LambderRateLimiter, LambderRateLimitWindow, LambderRateLimitPolicy
106
111
  export { LambderDdbRateLimiter } from "./stores/LambderDdbRateLimiter.js";
107
112
  export type { LambderDdbRateLimiterOptions } from "./stores/LambderDdbRateLimiter.js";
108
113
  export { LambderMemoryRateLimiter } from "./stores/LambderMemoryRateLimiter.js";
114
+ export { LambderOneShotSecrets } from "./secrets/LambderOneShotSecrets.js";
115
+ export type { LambderOneShotSecretKind, LambderOneShotSecretsOptions, LambderOneShotIssueResult, LambderOneShotRedeemResult, LambderOneShotCodeKindNames, LambderOneShotTokenKindNames, } from "./secrets/LambderOneShotSecrets.js";
116
+ export type { LambderOneShotSecretStore, LambderOneShotSecretRecord, LambderOneShotSecretDraft, LambderOneShotSecretShape, LambderOneShotIssueOutcome, } from "./shared/contracts/LambderOneShotSecretStore.js";
117
+ export { LambderDdbOneShotSecretStore } from "./stores/LambderDdbOneShotSecretStore.js";
118
+ export type { LambderDdbOneShotSecretStoreOptions } from "./stores/LambderDdbOneShotSecretStore.js";
119
+ export { LambderMemoryOneShotSecretStore } from "./stores/LambderMemoryOneShotSecretStore.js";
109
120
  export type { LambderIdempotencyStore, LambderIdempotencyBeginResult, LambderIdempotencyDoneRecord, } from "./shared/contracts/LambderIdempotencyStore.js";
110
121
  export { LambderDdbIdempotencyStore } from "./stores/LambderDdbIdempotencyStore.js";
111
122
  export type { LambderDdbIdempotencyStoreOptions } from "./stores/LambderDdbIdempotencyStore.js";
@@ -118,6 +129,8 @@ export type { LambderRateLimitKeyBuilder } from "./api/LambderApiRateLimits.js";
118
129
  export type { LambderContextRateLimit, LambderContextRateLimitCheck, LambderRateLimitCheckResult, LambderChargeablePolicyNames, LambderChargeKeyArgs, LambderRateLimitKeyFn, LambderRateLimitPer, LambderRateLimitBudget, LambderRateLimitChargeAt, LambderApiRateLimitPolicyConfig, LambderApiRateLimitsConfig, LambderAllowedPolicyNames, LambderRateLimitOption, } from "./api/LambderApiRateLimits.js";
119
130
  export type { LambderApiIdempotencyConfig } from "./api/LambderApiIdempotency.js";
120
131
  export type { LambderGuardsOptionValue, LambderRateLimitOverride, LambderRateLimitOptionValue, LambderApiIdempotencyOption, } from "./shared/wire/LambderApiOptionValues.js";
132
+ export { apiGuardParam } from "./shared/wire/LambderApiOptionEntries.js";
133
+ export type { LambderApiOptionEntries, LambderApiOptionEntry, LambderRateLimitPolicyEntry, LambderGuardDeclarationEntry, LambderApisWithGuard, LambderApisGuardedBy, LambderApisWithMode, LambderGuardParamOf, } from "./shared/wire/LambderApiOptionEntries.js";
121
134
  export { createLambderI18n } from "./shared/LambderI18n.js";
122
135
  export type { LambderLanguageMeta, LambderI18nConfig, LambderI18nInstance, LambderI18nTranslator, LambderI18nExtractParams, LambderI18nDictionaryLoader, LambderI18nCodes, LambderI18nKeys, LambderI18nTranslatorFor, } from "./shared/LambderI18n.js";
123
136
  export type { LambderApiContractShape, LambderApiMode, LambderApiEnvelopeBody, LambderApiResponseConfig, LambderApiNullAnswerConfig, LambderContractEntry, LambderMergeContract, LambderGuardNamesIn, LambderContractMode, LambderContractKeysWithMode, LambderContractKeysWithGuard, LambderJsonOf, LambderJsonOutputOf, LambderContractGuardsOf, LambderContractGuardNames, LambderContractGuardInputsOf, LambderContractGuardInput, LambderContractGuardInputNames, LambderContractRateLimitOf, LambderContractRateLimitNames, LambderContractIdempotencyOf, } from "./shared/wire/LambderApiContract.js";
package/dist/index.js CHANGED
@@ -9,6 +9,11 @@ export { buildTransportEnvelope, LambderTransportFailure, isLambderTransportFail
9
9
  export { lambderCookieJarTransport } from "./shared/transport/lambderCookieJarTransport.js";
10
10
  export { LambderCookieJar, parseSetCookie } from "./shared/transport/LambderCookieJar.js";
11
11
  export { LambderExpiringMap, LambderExpiringMapFullError } from "./shared/util/LambderExpiringMap.js";
12
+ // Waiting longer after each failure, once: what the upload runner and an app's own retrying code climb.
13
+ export { LambderBackoffTimer } from "./shared/util/LambderBackoffTimer.js";
14
+ // Signed claims tokens, and the keyed digest and random secret for the secrets an app stores.
15
+ export { LambderSignedClaims, keyedDigest, randomSecret } from "./shared/util/LambderSignedClaims.js";
16
+ export { constantTimeEquals } from "./shared/util/LambderTextDigest.js";
12
17
  export { lambderHandlerTransport } from "./invoke/lambderHandlerTransport.js";
13
18
  export { synthesizeLambdaHttpEvent, decodeLambdaHttpResult, localLambdaContext, LAMBDER_INVOKE_HEADER, LAMBDER_INVOKED_BY_HEADER, LAMBDER_INVOKE_PROTOCOL, } from "./invoke/LambderLambdaEvent.js";
14
19
  // The API core: the request, answer, envelope and pipeline both the server and the mock runtime run
@@ -71,6 +76,11 @@ export { LambderMemoryCache } from "./stores/LambderMemoryCache.js";
71
76
  export { RATE_LIMIT_WINDOWS } from "./shared/contracts/LambderRateLimiter.js";
72
77
  export { LambderDdbRateLimiter } from "./stores/LambderDdbRateLimiter.js";
73
78
  export { LambderMemoryRateLimiter } from "./stores/LambderMemoryRateLimiter.js";
79
+ // One-shot secrets: codes and tokens handed out once and taken back once, the
80
+ // store interface they live in, and the DynamoDB and in-memory stores
81
+ export { LambderOneShotSecrets } from "./secrets/LambderOneShotSecrets.js";
82
+ export { LambderDdbOneShotSecretStore } from "./stores/LambderDdbOneShotSecretStore.js";
83
+ export { LambderMemoryOneShotSecretStore } from "./stores/LambderMemoryOneShotSecretStore.js";
74
84
  export { LambderDdbIdempotencyStore } from "./stores/LambderDdbIdempotencyStore.js";
75
85
  export { LambderMemoryIdempotencyStore } from "./stores/LambderMemoryIdempotencyStore.js";
76
86
  // Declarative per-API policies: guards
@@ -78,6 +88,9 @@ export { lambderGuard, lambderRateLimitKey } from "./core/LambderPolicyBuilders.
78
88
  export { lambderGuardBuilder } from "./api/LambderApiGuards.js";
79
89
  // Declarative per-API policies: rate limits
80
90
  export { lambderRateLimitKeyBuilder, rateLimitRefusal, DEFAULT_RATE_LIMIT_REFUSAL } from "./api/LambderApiRateLimits.js";
91
+ // The declared options as plain data: what apiOptionEntries() reports and
92
+ // writeApiOptions writes, and the readers over the generated tables.
93
+ export { apiGuardParam } from "./shared/wire/LambderApiOptionEntries.js";
81
94
  // Typed translations (standalone, isomorphic)
82
95
  export { createLambderI18n } from "./shared/LambderI18n.js";
83
96
  export { resolveApiOutcome } from "./shared/wire/LambderApiOutcome.js";
@@ -10,6 +10,7 @@ import { LambderMemoryIdempotencyStore } from "../stores/LambderMemoryIdempotenc
10
10
  import { LambderMemorySessionStore } from "../stores/LambderMemorySessionStore.js";
11
11
  import LambderSessionManager, { type LambderCreatedSession } from "../session/LambderSessionManager.js";
12
12
  import type { LambderMockAppOptions, LambderMockIdempotencyOptions, LambderMockTransport, LambderMockTransportOptions } from "./LambderMockCreateOptions.js";
13
+ import type { LambderApiOptionEntry, LambderGuardDeclarationEntry } from "../shared/wire/LambderApiOptionEntries.js";
13
14
  import type { LambderMockCallContext, LambderMockCallRecord, LambderMockEntry, LambderMockEntryInput, LambderMockFailure, LambderMockFailureReason, LambderMockHandler, LambderMockLatency, LambderMockListener, LambderMockPublicNames, LambderMockRateLimitPolicies, LambderMockRegistryCheck, LambderMockRestEntry, LambderMockSessionCallContext, LambderMockSessionNames, LambderMockSlice, LambderMockOverride } from "./LambderMockTypes.js";
14
15
  /**
15
16
  * The mock runtime: the API core (LambderApiPipeline, the same class the
@@ -23,9 +24,10 @@ import type { LambderMockCallContext, LambderMockCallRecord, LambderMockEntry, L
23
24
  *
24
25
  * Create one with initLambderMock<Contract, SessionData>().create(...),
25
26
  * which fixes the contract and session types first so everything else is
26
- * inferred from the options.
27
+ * inferred from the options. `TDerived` is true for a mock created with the
28
+ * generated `apiOptions` table, whose entries are their handlers alone.
27
29
  */
28
- export declare class LambderMockApp<C extends LambderApiContractShape, S = any, G extends Record<string, LambderApiGuard<any, any, any>> = {}> {
30
+ export declare class LambderMockApp<C extends LambderApiContractShape, S = any, G extends Record<string, LambderApiGuard<any, any, any>> = {}, TDerived extends boolean = false> {
29
31
  readonly apiVersion: string | null;
30
32
  /** The memory stores, for assertions and reset; null for a subsystem that is off or backed by a store of yours. */
31
33
  readonly sessionStore: LambderMemorySessionStore<S> | null;
@@ -61,9 +63,11 @@ export declare class LambderMockApp<C extends LambderApiContractShape, S = any,
61
63
  private readonly recorder;
62
64
  /** Registered entries and the overrides over them (see LambderMockEntryRegistry). */
63
65
  private readonly registry;
66
+ /** The server's declared options per API, when create() was given the generated table; the entries' declarations come from here. */
67
+ private readonly apiOptions;
64
68
  /** The jars the runtime owns and what it planted in document.cookie (see LambderMockBrowserCookies). */
65
69
  private readonly browserCookies;
66
- constructor(options: LambderMockAppOptions<C, S, G>);
70
+ constructor(options: LambderMockAppOptions<C, S, G, LambderMockRateLimitPolicies<S>, boolean | LambderMockIdempotencyOptions<S>, {}, Record<string, LambderApiOptionEntry> | undefined>);
67
71
  /**
68
72
  * The registration-time checks every entry goes through, mocked or not:
69
73
  * the ones the server runs on a definition, and the mock's own "a session
@@ -75,11 +79,21 @@ export declare class LambderMockApp<C extends LambderApiContractShape, S = any,
75
79
  * option, for a mistake fixed by one option at create().
76
80
  */
77
81
  private assertEntryRegistration;
82
+ /**
83
+ * The table's entry for an endpoint, when create() was given one: null
84
+ * without a table, and a throw for a name the table does not hold or an
85
+ * entry registered under the other mode. The compiler already refuses
86
+ * both against the contract; this is where a stale table meets a caller
87
+ * the compiler did not see.
88
+ */
89
+ private declaredOptionsOf;
90
+ /** The mode the apiOptions table gives a name, or null without a table or for a name it does not hold. */
91
+ private declaredModeOf;
78
92
  private buildEntry;
79
93
  /** A mock for a public endpoint: a handler, or the handler with the endpoint's declarations restated. */
80
- publicApi<K extends LambderMockPublicNames<C>, TInputSchema extends z.ZodType = z.ZodType>(name: K, entry: LambderMockEntryInput<C, K, S, G, TInputSchema>): LambderMockEntry<C, K>;
94
+ publicApi<K extends LambderMockPublicNames<C>, TInputSchema extends z.ZodType = z.ZodType>(name: K, entry: LambderMockEntryInput<C, K, S, G, TInputSchema, TDerived>): LambderMockEntry<C, K>;
81
95
  /** A mock for a session endpoint: the pipeline fetches the session before the handler runs, and refuses without one. */
82
- sessionApi<K extends LambderMockSessionNames<C>, TInputSchema extends z.ZodType = z.ZodType>(name: K, entry: LambderMockEntryInput<C, K, S, G, TInputSchema>): LambderMockEntry<C, K>;
96
+ sessionApi<K extends LambderMockSessionNames<C>, TInputSchema extends z.ZodType = z.ZodType>(name: K, entry: LambderMockEntryInput<C, K, S, G, TInputSchema, TDerived>): LambderMockEntry<C, K>;
83
97
  /**
84
98
  * A public endpoint deliberately left without a mock; a call answers the
85
99
  * notMocked refusal carrying the reason.
@@ -111,13 +125,15 @@ export declare class LambderMockApp<C extends LambderApiContractShape, S = any,
111
125
  * still refused, and an entry registered later (registerPartial, or a
112
126
  * second register) takes its endpoint back from the rest.
113
127
  *
114
- * What it cannot do is the session read. The mode of an unregistered name
115
- * is not knowable at runtime (the contract is a type), so a call it
116
- * answers is processed as public: the protocol's pre-pass still runs, so
117
- * a stale client still hears versionExpired, but a signed-out call to an
118
- * unmocked session endpoint answers "not mocked" where the server answers
119
- * sessionExpired. Declare an endpoint whose signed-out path a test cares
120
- * about with sessionNotMocked instead.
128
+ * The mode of an unregistered name comes from the apiOptions table, when
129
+ * create() was given one, so an unmocked session endpoint still reads the
130
+ * session and a signed-out call answers sessionExpired as on the server.
131
+ * Without the table the mode is not knowable at runtime (the contract is
132
+ * a type), and a call it answers is processed as public: the protocol's
133
+ * pre-pass still runs, so a stale client still hears versionExpired, but
134
+ * a signed-out call to an unmocked session endpoint answers "not mocked"
135
+ * where the server answers sessionExpired. Declare an endpoint whose
136
+ * signed-out path a test cares about with sessionNotMocked there.
121
137
  */
122
138
  restNotMocked(reason: string): LambderMockRestEntry;
123
139
  private buildNotMockedEntry;
@@ -165,10 +181,11 @@ export declare class LambderMockApp<C extends LambderApiContractShape, S = any,
165
181
  /**
166
182
  * The entry that answers a name nothing registered, when register() was
167
183
  * given a rest entry: the notMocked refusal carrying its reason, run
168
- * through the pipeline as a public endpoint, since the mode of an
169
- * unregistered name cannot be recovered at runtime. Everything before
170
- * dispatch still runs (the signature gate, the payload restore); the
171
- * missing session read is the fidelity limit restNotMocked documents.
184
+ * through the pipeline under the mode the apiOptions table gives the
185
+ * name, and as a public endpoint where there is no table to say (the
186
+ * mode of an unregistered name cannot otherwise be recovered at runtime).
187
+ * Everything before dispatch still runs (the signature gate, the payload
188
+ * restore, and for a session endpoint the session read).
172
189
  */
173
190
  private restNotMockedEntry;
174
191
  /** The next call to the endpoint fails this way; several calls queue in order. */
@@ -332,5 +349,5 @@ export declare const initLambderMock: <C extends LambderApiContractShape, S = an
332
349
  * dropped. `I` exists for the same reason `P` does, and
333
350
  * LambderMockSurplusKeys puts the error back on the key.
334
351
  */
335
- create<const G extends Record<string, LambderApiGuard<any, any, any, LambderMockCallContext<S>, LambderMockSessionCallContext<S>>> = {}, const P extends LambderMockRateLimitPolicies<S> = {}, I extends boolean | LambderMockIdempotencyOptions<S> = boolean | LambderMockIdempotencyOptions<S>>(options: LambderMockAppOptions<C, S, G, P, I>): LambderMockApp<C, S, G>;
352
+ create<const G extends Record<string, LambderApiGuard<any, any, any, LambderMockCallContext<S>, LambderMockSessionCallContext<S>>> = {}, const P extends LambderMockRateLimitPolicies<S> = {}, I extends boolean | LambderMockIdempotencyOptions<S> = boolean | LambderMockIdempotencyOptions<S>, const D extends Record<string, LambderGuardDeclarationEntry> = {}, const A extends Record<string, LambderApiOptionEntry> | undefined = undefined>(options: LambderMockAppOptions<C, S, G, P, I, D, A>): LambderMockApp<C, S, G, [A] extends [undefined] ? false : true>;
336
353
  };
@@ -47,7 +47,8 @@ const defaultCookieHost = () => globalThis.location?.host || "localhost";
47
47
  *
48
48
  * Create one with initLambderMock<Contract, SessionData>().create(...),
49
49
  * which fixes the contract and session types first so everything else is
50
- * inferred from the options.
50
+ * inferred from the options. `TDerived` is true for a mock created with the
51
+ * generated `apiOptions` table, whose entries are their handlers alone.
51
52
  */
52
53
  export class LambderMockApp {
53
54
  apiVersion;
@@ -85,10 +86,13 @@ export class LambderMockApp {
85
86
  recorder;
86
87
  /** Registered entries and the overrides over them (see LambderMockEntryRegistry). */
87
88
  registry = new LambderMockEntryRegistry();
89
+ /** The server's declared options per API, when create() was given the generated table; the entries' declarations come from here. */
90
+ apiOptions;
88
91
  /** The jars the runtime owns and what it planted in document.cookie (see LambderMockBrowserCookies). */
89
92
  browserCookies = new LambderMockBrowserCookies();
90
93
  constructor(options) {
91
94
  this.apiVersion = options.apiVersion ?? null;
95
+ this.apiOptions = options.apiOptions ?? null;
92
96
  this.failures = new LambderMockFailureInjector({ apiVersion: this.apiVersion, latency: options.latency ?? 0 });
93
97
  this.recorder = new LambderMockCallRecorder({ callLogSize: options.callLogSize ?? DEFAULT_CALL_LOG_SIZE });
94
98
  // The loopback address when nothing names a client, as for a request
@@ -196,13 +200,49 @@ export class LambderMockApp {
196
200
  throw new Error(`LambderMockApp: session endpoint "${definition.name}" needs the sessions option at creation.`);
197
201
  }
198
202
  }
203
+ /**
204
+ * The table's entry for an endpoint, when create() was given one: null
205
+ * without a table, and a throw for a name the table does not hold or an
206
+ * entry registered under the other mode. The compiler already refuses
207
+ * both against the contract; this is where a stale table meets a caller
208
+ * the compiler did not see.
209
+ */
210
+ declaredOptionsOf(name, mode) {
211
+ if (!this.apiOptions)
212
+ return null;
213
+ const declared = Object.prototype.hasOwnProperty.call(this.apiOptions, name) ? this.apiOptions[name] : undefined;
214
+ if (!declared) {
215
+ throw new Error(`LambderMockApp: "${name}" has no entry in the apiOptions table given to create(). The table predates this endpoint: regenerate it with writeApiOptions.`);
216
+ }
217
+ if (declared.mode !== mode) {
218
+ throw new Error(`LambderMockApp: "${name}" is a ${declared.mode} endpoint in the apiOptions table, registered here as a ${mode} one.`);
219
+ }
220
+ return declared;
221
+ }
222
+ /** The mode the apiOptions table gives a name, or null without a table or for a name it does not hold. */
223
+ declaredModeOf(name) {
224
+ if (!this.apiOptions || !Object.prototype.hasOwnProperty.call(this.apiOptions, name))
225
+ return null;
226
+ return this.apiOptions[name].mode;
227
+ }
199
228
  buildEntry(name, mode, input) {
200
229
  const options = (typeof input === "function" ? { handler: input } : input);
230
+ const declared = this.declaredOptionsOf(name, mode);
231
+ if (declared) {
232
+ // A caller the compiler did not see (a JavaScript slice, a cast)
233
+ // would otherwise have its restatement silently lose to the table.
234
+ for (const field of ["guards", "rateLimit", "idempotency"]) {
235
+ if (options[field] !== undefined) {
236
+ throw new Error(`LambderMockApp: "${name}" restates its ${field} option, which the apiOptions table given to create() already declares. Leave it out of the entry.`);
237
+ }
238
+ }
239
+ }
240
+ const declarations = declared ?? options;
201
241
  const definition = {
202
242
  name, mode,
203
- guards: options.guards,
204
- rateLimit: options.rateLimit,
205
- idempotency: options.idempotency,
243
+ guards: declarations.guards,
244
+ rateLimit: declarations.rateLimit,
245
+ idempotency: declarations.idempotency,
206
246
  // No cast: the entry's schema is a z.ZodType, the same type the
207
247
  // definition holds. A structural { safeParse } here would let a
208
248
  // validator that is not a zod schema reach the 422 body as
@@ -257,18 +297,21 @@ export class LambderMockApp {
257
297
  * still refused, and an entry registered later (registerPartial, or a
258
298
  * second register) takes its endpoint back from the rest.
259
299
  *
260
- * What it cannot do is the session read. The mode of an unregistered name
261
- * is not knowable at runtime (the contract is a type), so a call it
262
- * answers is processed as public: the protocol's pre-pass still runs, so
263
- * a stale client still hears versionExpired, but a signed-out call to an
264
- * unmocked session endpoint answers "not mocked" where the server answers
265
- * sessionExpired. Declare an endpoint whose signed-out path a test cares
266
- * about with sessionNotMocked instead.
300
+ * The mode of an unregistered name comes from the apiOptions table, when
301
+ * create() was given one, so an unmocked session endpoint still reads the
302
+ * session and a signed-out call answers sessionExpired as on the server.
303
+ * Without the table the mode is not knowable at runtime (the contract is
304
+ * a type), and a call it answers is processed as public: the protocol's
305
+ * pre-pass still runs, so a stale client still hears versionExpired, but
306
+ * a signed-out call to an unmocked session endpoint answers "not mocked"
307
+ * where the server answers sessionExpired. Declare an endpoint whose
308
+ * signed-out path a test cares about with sessionNotMocked there.
267
309
  */
268
310
  restNotMocked(reason) {
269
311
  return { restNotMockedReason: reason };
270
312
  }
271
313
  buildNotMockedEntry(name, mode, reason) {
314
+ this.declaredOptionsOf(name, mode);
272
315
  const definition = { name, mode };
273
316
  this.assertEntryRegistration(definition);
274
317
  return { name, mode, definition, handler: null, notMockedReason: reason };
@@ -352,16 +395,18 @@ export class LambderMockApp {
352
395
  /**
353
396
  * The entry that answers a name nothing registered, when register() was
354
397
  * given a rest entry: the notMocked refusal carrying its reason, run
355
- * through the pipeline as a public endpoint, since the mode of an
356
- * unregistered name cannot be recovered at runtime. Everything before
357
- * dispatch still runs (the signature gate, the payload restore); the
358
- * missing session read is the fidelity limit restNotMocked documents.
398
+ * through the pipeline under the mode the apiOptions table gives the
399
+ * name, and as a public endpoint where there is no table to say (the
400
+ * mode of an unregistered name cannot otherwise be recovered at runtime).
401
+ * Everything before dispatch still runs (the signature gate, the payload
402
+ * restore, and for a session endpoint the session read).
359
403
  */
360
404
  restNotMockedEntry(apiName) {
361
405
  const reason = this.registry.restNotMockedReason;
362
406
  if (reason === null)
363
407
  return null;
364
- return { name: apiName, mode: "public", definition: { name: apiName, mode: "public" }, handler: null, notMockedReason: reason };
408
+ const mode = this.declaredModeOf(apiName) ?? "public";
409
+ return { name: apiName, mode, definition: { name: apiName, mode }, handler: null, notMockedReason: reason };
365
410
  }
366
411
  // -----------------------------------------------------------------------
367
412
  // Control surface
@@ -581,12 +626,13 @@ export class LambderMockApp {
581
626
  const id = this.recorder.nextCallId();
582
627
  const registered = this.entryFor(request.apiName);
583
628
  // The rest entry answers whatever nothing registered, when register()
584
- // was given one. The mode reported stays the registered entry's, so it
585
- // is null here exactly as it is for a name the registry does not know:
586
- // the rest answer is processed as public, which is a property of the
587
- // answer rather than a claim about the endpoint.
629
+ // was given one. The mode reported is the registered entry's, or the
630
+ // apiOptions table's for a name it holds, and null otherwise, exactly
631
+ // as for a name nothing knows: without the table a rest answer is
632
+ // processed as public, which is a property of the answer rather than
633
+ // a claim about the endpoint.
588
634
  const entry = registered ?? this.restNotMockedEntry(request.apiName);
589
- const mode = registered?.mode ?? null;
635
+ const mode = registered?.mode ?? this.declaredModeOf(request.apiName);
590
636
  const facts = this.callFacts(id, request, mode);
591
637
  const startedAt = facts.startedAt;
592
638
  const ctx = this.createContext(request);
@@ -3,8 +3,9 @@ import type { LambderApiSignatureMap } from "../shared/wire/LambderApiSignature.
3
3
  import type { LambderApiResponseConfig } from "../shared/wire/LambderApiContract.js";
4
4
  import type { LambderHttpStatusCode } from "../shared/wire/LambderHttpStatus.js";
5
5
  import type { MaybePromise } from "../shared/util/LambderTypeUtilities.js";
6
- import type { LambderContractGuardNames, LambderContractIdempotencyOf, LambderContractKeysWithMode, LambderContractRateLimitNames, LambderContractRateLimitOf } from "../shared/wire/LambderApiContract.js";
6
+ import type { LambderContractGuardNames, LambderContractIdempotencyOf, LambderContractKeysWithMode, LambderContractMode, LambderContractRateLimitNames, LambderContractRateLimitOf } from "../shared/wire/LambderApiContract.js";
7
7
  import type { LambderApiGuard } from "../api/LambderApiGuards.js";
8
+ import type { LambderApiOptionEntry, LambderGuardDeclarationEntry } from "../shared/wire/LambderApiOptionEntries.js";
8
9
  import type { LambderApiRateLimitPolicyConfig } from "../api/LambderApiRateLimits.js";
9
10
  import type { LambderApiRequest } from "../api/LambderApiRequest.js";
10
11
  import type { LambderApiTransport } from "../shared/transport/LambderApiTransport.js";
@@ -114,6 +115,37 @@ type LambderMockRateLimitsOptions<C, S, P extends LambderMockRateLimitPolicies<S
114
115
  /** Let a call through when the limiter throws, instead of refusing it. Default: true. */
115
116
  failOpen?: boolean;
116
117
  };
118
+ /**
119
+ * The shape a mock guard has to have to stand in for a server guard the
120
+ * generated `guardDeclarations` table describes: the same input mode and the
121
+ * same session requirement. The contract cannot say these for every guard
122
+ * (it names a guardInput's shape, and nothing about a guard fed from the
123
+ * payload or from nothing), so a mock guard that reads a payload slice the
124
+ * server's guard never sees, or that requires a session where the server's
125
+ * does not, would run and decide differently without this.
126
+ */
127
+ export type LambderMockGuardShapeOf<D> = (D extends {
128
+ input: "apiInput";
129
+ } ? {
130
+ apiInput: z.ZodType;
131
+ } : D extends {
132
+ input: "guardInput";
133
+ } ? {
134
+ guardInput: z.ZodType;
135
+ } : {
136
+ apiInput?: undefined;
137
+ guardInput?: undefined;
138
+ }) & (D extends {
139
+ session: true;
140
+ } ? {
141
+ session: true;
142
+ } : {
143
+ session?: false | undefined;
144
+ });
145
+ /** Each mock guard the declarations know held to its declared shape; a guard the table does not have is free. */
146
+ type LambderMockGuardsAgree<G, D> = {
147
+ [N in keyof G & keyof D]: G[N] extends LambderMockGuardShapeOf<D[N]> ? unknown : LambderMockGuardShapeOf<D[N]>;
148
+ };
117
149
  /**
118
150
  * The guards option: required whenever the contract declares any guard name,
119
151
  * omittable only for a contract that declares none.
@@ -123,12 +155,12 @@ type LambderMockRateLimitsOptions<C, S, P extends LambderMockRateLimitPolicies<S
123
155
  * would answer 200 here. Optional, it would be the droppable half of exactly
124
156
  * the check it exists for.
125
157
  */
126
- type LambderMockGuardsOption<C, S, G> = [
158
+ type LambderMockGuardsOption<C, S, G, D> = [
127
159
  LambderContractGuardNames<C>
128
160
  ] extends [never] ? {
129
- guards?: G & LambderMockGuards<C, S> & LambderMockGuardShapes<S, G>;
161
+ guards?: G & LambderMockGuards<C, S> & LambderMockGuardShapes<S, G> & LambderMockGuardsAgree<G, NoInfer<D>>;
130
162
  } : {
131
- guards: G & LambderMockGuards<C, S> & LambderMockGuardShapes<S, G>;
163
+ guards: G & LambderMockGuards<C, S> & LambderMockGuardShapes<S, G> & LambderMockGuardsAgree<G, NoInfer<D>>;
132
164
  };
133
165
  /**
134
166
  * The sessions, idempotency and rateLimits options: each required whenever
@@ -168,7 +200,38 @@ type LambderMockRateLimitsOption<C, S, P extends LambderMockRateLimitPolicies<S>
168
200
  type LambderMockGuardShapes<S, G> = {
169
201
  [N in keyof G]: LambderMockSurplusKeys<G[N], LambderApiGuard<any, any, any, LambderMockCallContext<S>, LambderMockSessionCallContext<S>>>;
170
202
  };
171
- export type LambderMockAppOptions<C, S, G, P extends LambderMockRateLimitPolicies<S> = LambderMockRateLimitPolicies<S>, I extends boolean | LambderMockIdempotencyOptions<S> = boolean | LambderMockIdempotencyOptions<S>> = LambderMockGuardsOption<C, S, G> & LambderMockSessionsOption<C, S> & LambderMockIdempotencyOption<C, S, I> & LambderMockRateLimitsOption<C, S, P> & {
203
+ /**
204
+ * What the generated `apiOptions` table has to hold to stand in for the
205
+ * restated declarations of a contract's entries: an entry for every endpoint
206
+ * the contract declares, of the endpoint's mode. A table generated before an
207
+ * endpoint was added, or before one changed mode, is a compile error at the
208
+ * option rather than a throw when that endpoint's entry registers.
209
+ */
210
+ export type LambderMockApiOptionsCover<C> = {
211
+ [K in keyof C & string]: {
212
+ mode: LambderContractMode<C, K>;
213
+ };
214
+ };
215
+ export type LambderMockAppOptions<C, S, G, P extends LambderMockRateLimitPolicies<S> = LambderMockRateLimitPolicies<S>, I extends boolean | LambderMockIdempotencyOptions<S> = boolean | LambderMockIdempotencyOptions<S>, D extends Record<string, LambderGuardDeclarationEntry> = {}, A extends Record<string, LambderApiOptionEntry> | undefined = undefined> = LambderMockGuardsOption<C, S, G, D> & LambderMockSessionsOption<C, S> & LambderMockIdempotencyOption<C, S, I> & LambderMockRateLimitsOption<C, S, P> & {
216
+ /**
217
+ * The server's guard declarations, as the generated options module
218
+ * exports them (`guardDeclarations`). Given, every mock guard of a name
219
+ * the table has is held to its input mode and session requirement at
220
+ * the `guards` option (see LambderMockGuardShapeOf). Nothing runs on it.
221
+ */
222
+ guardDeclarations?: D;
223
+ /**
224
+ * The server's declared options per API, as the generated options module
225
+ * exports them (`apiOptions`). Given, every entry's guards, rateLimit and
226
+ * idempotency are read off the table rather than restated: an entry is
227
+ * its handler (and an input schema, if it has one), a restated option is
228
+ * a compile error, and an endpoint whose mode the table and the builder
229
+ * disagree on is refused at registration. A restNotMocked answer reads
230
+ * the endpoint's mode off the table too, so a session endpoint nothing
231
+ * mocks still reads the session first. The table has to cover the
232
+ * contract (see LambderMockApiOptionsCover).
233
+ */
234
+ apiOptions?: A & LambderMockApiOptionsCover<C>;
172
235
  /** Stamped on every answer's envelope as apiVersion, as the server's option is. */
173
236
  apiVersion?: string;
174
237
  /** The version floor, as on the server: a call naming a lower `version` answers versionExpired whatever its signature says. */