lambder 7.2.5 → 8.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (209) hide show
  1. package/CHANGELOG.md +1021 -3
  2. package/README.md +43 -21
  3. package/dist/api/LambderApiAnswer.d.ts +18 -22
  4. package/dist/api/LambderApiAnswer.js +6 -7
  5. package/dist/api/LambderApiCallContext.d.ts +21 -8
  6. package/dist/api/LambderApiCallContext.js +22 -4
  7. package/dist/api/LambderApiDefinition.d.ts +4 -3
  8. package/dist/api/LambderApiEnvelope.d.ts +14 -9
  9. package/dist/api/LambderApiEnvelope.js +33 -34
  10. package/dist/api/LambderApiGuards.d.ts +78 -51
  11. package/dist/api/LambderApiGuards.js +34 -36
  12. package/dist/api/LambderApiIdempotency.d.ts +74 -61
  13. package/dist/api/LambderApiIdempotency.js +226 -151
  14. package/dist/api/LambderApiOutputValidationError.d.ts +32 -0
  15. package/dist/api/LambderApiOutputValidationError.js +50 -0
  16. package/dist/api/LambderApiPipeline.d.ts +77 -39
  17. package/dist/api/LambderApiPipeline.js +135 -62
  18. package/dist/api/LambderApiRateLimits.d.ts +208 -54
  19. package/dist/api/LambderApiRateLimits.js +197 -108
  20. package/dist/api/LambderApiRequest.d.ts +27 -21
  21. package/dist/api/LambderApiRequest.js +26 -19
  22. package/dist/api/LambderApiSignature.d.ts +12 -15
  23. package/dist/api/LambderApiSignature.js +28 -51
  24. package/dist/api/LambderApiValidationRefusal.d.ts +9 -9
  25. package/dist/api/LambderApiValidationRefusal.js +10 -10
  26. package/dist/build/freshProcessVerifier.d.ts +13 -0
  27. package/dist/build/freshProcessVerifier.js +19 -0
  28. package/dist/build/writeApiSignatures.d.ts +109 -0
  29. package/dist/build/writeApiSignatures.js +222 -0
  30. package/dist/build.d.ts +9 -0
  31. package/dist/build.js +8 -0
  32. package/dist/client/LambderCaller.d.ts +13 -44
  33. package/dist/client/LambderCaller.js +77 -84
  34. package/dist/client/LambderReloadLoopBreaker.d.ts +56 -26
  35. package/dist/client/LambderReloadLoopBreaker.js +90 -46
  36. package/dist/client/lambderFetchTransport.d.ts +4 -1
  37. package/dist/client/lambderFetchTransport.js +52 -28
  38. package/dist/client.d.ts +5 -3
  39. package/dist/client.js +2 -1
  40. package/dist/core/Lambder.d.ts +161 -69
  41. package/dist/core/Lambder.js +370 -226
  42. package/dist/core/LambderContext.d.ts +82 -15
  43. package/dist/core/LambderContext.js +107 -20
  44. package/dist/core/LambderCors.d.ts +21 -3
  45. package/dist/core/LambderCors.js +35 -16
  46. package/dist/core/LambderCrashHandling.d.ts +40 -0
  47. package/dist/core/LambderCrashHandling.js +97 -0
  48. package/dist/core/LambderCreateOptions.d.ts +151 -75
  49. package/dist/core/LambderCreateOptions.js +16 -23
  50. package/dist/core/LambderFiles.d.ts +28 -7
  51. package/dist/core/LambderFiles.js +73 -33
  52. package/dist/core/LambderIndexHtml.js +12 -11
  53. package/dist/core/LambderPolicyBuilders.d.ts +17 -5
  54. package/dist/core/LambderPolicyBuilders.js +17 -5
  55. package/dist/core/LambderPublicFiles.d.ts +11 -5
  56. package/dist/core/LambderPublicFiles.js +32 -4
  57. package/dist/core/LambderRequestPath.d.ts +43 -0
  58. package/dist/core/LambderRequestPath.js +63 -0
  59. package/dist/core/LambderResponse.d.ts +26 -5
  60. package/dist/core/LambderResponse.js +157 -70
  61. package/dist/core/LambderResponseBuilder.d.ts +49 -4
  62. package/dist/core/LambderResponseBuilder.js +64 -3
  63. package/dist/core/LambderRouting.d.ts +2 -3
  64. package/dist/core/LambderRouting.js +22 -7
  65. package/dist/core/LambderTemplatingEngine.js +211 -32
  66. package/dist/index.d.ts +15 -8
  67. package/dist/index.js +5 -4
  68. package/dist/invoke/LambderInvokeCaller.d.ts +37 -42
  69. package/dist/invoke/LambderInvokeCaller.js +76 -66
  70. package/dist/invoke/LambderInvokeOutcome.d.ts +27 -26
  71. package/dist/invoke/LambderInvokeOutcome.js +9 -22
  72. package/dist/invoke/LambderLambdaEvent.d.ts +44 -10
  73. package/dist/invoke/LambderLambdaEvent.js +80 -37
  74. package/dist/invoke/lambderHandlerTransport.d.ts +12 -10
  75. package/dist/invoke/lambderHandlerTransport.js +16 -19
  76. package/dist/mock/LambderMockApp.d.ts +67 -83
  77. package/dist/mock/LambderMockApp.js +167 -153
  78. package/dist/mock/LambderMockBrowserCookies.d.ts +24 -28
  79. package/dist/mock/LambderMockBrowserCookies.js +24 -28
  80. package/dist/mock/LambderMockCallRecorder.d.ts +15 -22
  81. package/dist/mock/LambderMockCallRecorder.js +19 -28
  82. package/dist/mock/LambderMockCreateOptions.d.ts +42 -24
  83. package/dist/mock/LambderMockEntryRegistry.d.ts +11 -12
  84. package/dist/mock/LambderMockEntryRegistry.js +24 -29
  85. package/dist/mock/LambderMockFailureInjector.d.ts +3 -6
  86. package/dist/mock/LambderMockFailureInjector.js +3 -6
  87. package/dist/mock/LambderMockTypes.d.ts +78 -108
  88. package/dist/mock/lambderMockInvokeTransport.d.ts +11 -13
  89. package/dist/mock/lambderMockInvokeTransport.js +11 -10
  90. package/dist/mock/lambderMockMswHandler.d.ts +33 -29
  91. package/dist/mock/lambderMockMswHandler.js +50 -39
  92. package/dist/mock.d.ts +3 -1
  93. package/dist/mock.js +5 -3
  94. package/dist/session/LambderSessionController.d.ts +108 -89
  95. package/dist/session/LambderSessionController.js +187 -168
  96. package/dist/session/LambderSessionCrypto.d.ts +16 -7
  97. package/dist/session/LambderSessionCrypto.js +26 -12
  98. package/dist/session/LambderSessionManager.d.ts +136 -47
  99. package/dist/session/LambderSessionManager.js +280 -139
  100. package/dist/shared/LambderHtml.d.ts +42 -3
  101. package/dist/shared/LambderHtml.js +127 -7
  102. package/dist/shared/LambderHtmlPositions.d.ts +173 -0
  103. package/dist/shared/LambderHtmlPositions.js +652 -0
  104. package/dist/shared/LambderI18n.d.ts +10 -11
  105. package/dist/shared/LambderI18n.js +33 -21
  106. package/dist/shared/contracts/LambderCache.d.ts +66 -0
  107. package/dist/shared/contracts/LambderCache.js +11 -0
  108. package/dist/shared/contracts/LambderFileSource.d.ts +6 -6
  109. package/dist/shared/contracts/LambderFileSource.js +5 -8
  110. package/dist/shared/contracts/LambderIdempotencyStore.d.ts +51 -22
  111. package/dist/shared/contracts/LambderIdempotencyStore.js +4 -5
  112. package/dist/shared/contracts/LambderRateLimiter.d.ts +27 -15
  113. package/dist/shared/contracts/LambderRateLimiter.js +4 -5
  114. package/dist/shared/contracts/LambderSessionStore.d.ts +65 -26
  115. package/dist/shared/contracts/LambderSessionStore.js +5 -6
  116. package/dist/shared/transport/LambderApiTransport.d.ts +27 -27
  117. package/dist/shared/transport/LambderApiTransport.js +7 -7
  118. package/dist/shared/transport/LambderCookieJar.d.ts +28 -35
  119. package/dist/shared/transport/LambderCookieJar.js +54 -66
  120. package/dist/shared/transport/lambderCookieJarTransport.d.ts +11 -13
  121. package/dist/shared/transport/lambderCookieJarTransport.js +24 -23
  122. package/dist/shared/util/LambderCallAbort.d.ts +5 -5
  123. package/dist/shared/util/LambderCallAbort.js +5 -5
  124. package/dist/shared/util/LambderClientIp.d.ts +27 -11
  125. package/dist/shared/util/LambderClientIp.js +96 -13
  126. package/dist/shared/util/LambderExpiringMap.d.ts +35 -49
  127. package/dist/shared/util/LambderExpiringMap.js +41 -57
  128. package/dist/shared/util/LambderNodeModules.js +6 -7
  129. package/dist/shared/util/LambderOptionChecks.d.ts +4 -4
  130. package/dist/shared/util/LambderOptionChecks.js +4 -4
  131. package/dist/shared/util/LambderResponseBrand.d.ts +5 -5
  132. package/dist/shared/util/LambderResponseBrand.js +5 -5
  133. package/dist/shared/util/LambderTestingDoors.d.ts +29 -0
  134. package/dist/shared/util/LambderTestingDoors.js +29 -0
  135. package/dist/shared/util/LambderTypeUtilities.d.ts +7 -8
  136. package/dist/shared/util/LambderTypeUtilities.js +3 -3
  137. package/dist/shared/util/boundKeyField.d.ts +20 -0
  138. package/dist/shared/util/boundKeyField.js +34 -0
  139. package/dist/shared/util/canonicalJson.d.ts +11 -0
  140. package/dist/shared/util/canonicalJson.js +28 -0
  141. package/dist/shared/util/joinKeyFields.d.ts +20 -0
  142. package/dist/shared/util/joinKeyFields.js +22 -0
  143. package/dist/shared/wire/LambderAnswerHeaders.d.ts +12 -16
  144. package/dist/shared/wire/LambderAnswerHeaders.js +12 -16
  145. package/dist/shared/wire/LambderApiContract.d.ts +107 -32
  146. package/dist/shared/wire/LambderApiOutcome.d.ts +43 -31
  147. package/dist/shared/wire/LambderApiOutcome.js +48 -23
  148. package/dist/shared/wire/LambderApiRefusal.d.ts +39 -27
  149. package/dist/shared/wire/LambderApiRefusal.js +36 -7
  150. package/dist/shared/wire/LambderApiSignature.d.ts +18 -22
  151. package/dist/shared/wire/LambderApiSignature.js +16 -19
  152. package/dist/shared/wire/LambderCallOptions.d.ts +38 -47
  153. package/dist/shared/wire/LambderCallOptions.js +9 -11
  154. package/dist/shared/wire/LambderCompressionCodec.d.ts +29 -34
  155. package/dist/shared/wire/LambderCompressionCodec.js +31 -36
  156. package/dist/shared/wire/LambderCompressionOption.d.ts +9 -9
  157. package/dist/shared/wire/LambderCompressionOption.js +9 -9
  158. package/dist/shared/wire/LambderCrashDetail.d.ts +12 -15
  159. package/dist/shared/wire/LambderCrashDetail.js +12 -15
  160. package/dist/shared/wire/LambderDefaultApiPath.d.ts +6 -0
  161. package/dist/shared/wire/LambderDefaultApiPath.js +6 -0
  162. package/dist/shared/wire/LambderHttpStatus.d.ts +6 -7
  163. package/dist/shared/wire/LambderIdempotencyKeyScope.d.ts +89 -0
  164. package/dist/shared/wire/LambderIdempotencyKeyScope.js +146 -0
  165. package/dist/shared/wire/LambderInvokeApiId.d.ts +27 -0
  166. package/dist/shared/wire/LambderInvokeApiId.js +27 -0
  167. package/dist/shared/wire/LambderOutcomeAssertions.d.ts +79 -0
  168. package/dist/shared/wire/LambderOutcomeAssertions.js +112 -0
  169. package/dist/shared/wire/LambderRequestPayload.d.ts +18 -20
  170. package/dist/shared/wire/LambderRequestPayload.js +4 -6
  171. package/dist/stores/LambderCacheFiller.d.ts +48 -0
  172. package/dist/stores/LambderCacheFiller.js +119 -0
  173. package/dist/stores/LambderCacheKeys.d.ts +26 -0
  174. package/dist/stores/LambderCacheKeys.js +54 -0
  175. package/dist/stores/LambderCacheValues.d.ts +45 -0
  176. package/dist/stores/LambderCacheValues.js +74 -0
  177. package/dist/stores/LambderDdbCache.d.ts +121 -56
  178. package/dist/stores/LambderDdbCache.js +528 -225
  179. package/dist/stores/LambderDdbIdempotencyStore.d.ts +33 -22
  180. package/dist/stores/LambderDdbIdempotencyStore.js +75 -50
  181. package/dist/stores/LambderDdbRateLimiter.d.ts +76 -20
  182. package/dist/stores/LambderDdbRateLimiter.js +151 -39
  183. package/dist/stores/LambderDdbSdk.d.ts +43 -31
  184. package/dist/stores/LambderDdbSdk.js +79 -33
  185. package/dist/stores/LambderDdbSessionStore.d.ts +27 -14
  186. package/dist/stores/LambderDdbSessionStore.js +119 -47
  187. package/dist/stores/LambderHttpFileSource.d.ts +15 -6
  188. package/dist/stores/LambderHttpFileSource.js +15 -13
  189. package/dist/stores/LambderMemoryCache.d.ts +49 -0
  190. package/dist/stores/LambderMemoryCache.js +113 -0
  191. package/dist/stores/LambderMemoryIdempotencyStore.d.ts +13 -12
  192. package/dist/stores/LambderMemoryIdempotencyStore.js +31 -30
  193. package/dist/stores/LambderMemoryRateLimiter.d.ts +8 -9
  194. package/dist/stores/LambderMemoryRateLimiter.js +14 -13
  195. package/dist/stores/LambderMemorySessionStore.d.ts +14 -11
  196. package/dist/stores/LambderMemorySessionStore.js +38 -19
  197. package/dist/stores/LambderS3FileSource.d.ts +21 -6
  198. package/dist/stores/LambderS3FileSource.js +12 -7
  199. package/dist/testing/LambderTestApp.d.ts +176 -0
  200. package/dist/testing/LambderTestApp.js +204 -0
  201. package/dist/testing/LambderTestVisitor.d.ts +153 -0
  202. package/dist/testing/LambderTestVisitor.js +154 -0
  203. package/dist/testing.d.ts +27 -0
  204. package/dist/testing.js +24 -0
  205. package/package.json +20 -3
  206. package/dist/api/LambderApiPolicyEngine.d.ts +0 -36
  207. package/dist/api/LambderApiPolicyEngine.js +0 -77
  208. package/dist/shared/util/LambderKeyFields.d.ts +0 -32
  209. package/dist/shared/util/LambderKeyFields.js +0 -34
@@ -106,17 +106,16 @@ export interface LambderI18nInstance<TLanguages extends Record<string, LambderLa
106
106
  [D in TDefault]: Partial<Record<keyof TContract, never>>;
107
107
  } & TExt): LambderI18nInstance<TLanguages, TDefault, TEnforced, TContract & TExt[TDefault]>;
108
108
  /**
109
- * Run the loaders a language has in this instance and in every instance
110
- * sharing its root, and resolve once their dictionaries are merged.
111
- * Defaults to the active language; resolves at once when nothing is left
112
- * to load. Until then `t` falls back per key to the default language, so
113
- * await it before the first render, and before `setLanguage` to switch
114
- * without a flash of the default language. Change listeners fire once
115
- * per load, however many calls share it. A loader that answered never
116
- * runs again; one that rejected rejects this call and runs again on the
117
- * next. Creating an extension loads nothing: one created after its
118
- * language was loaded awaits its own `loadLanguage()`, which runs only
119
- * what is still missing.
109
+ * Run the loaders a language has in this instance and every instance
110
+ * sharing its root, resolving once their dictionaries are merged.
111
+ * Defaults to the active language. Until then `t` falls back per key to
112
+ * the default language, so await it before the first render, and before
113
+ * `setLanguage` to switch without a flash of the default language.
114
+ * Change listeners fire once per load, however many calls share it. A
115
+ * loader that answered never runs again; one that rejected rejects this
116
+ * call and is retried on the next. Creating an extension loads nothing:
117
+ * one created after its language was loaded needs its own
118
+ * `loadLanguage()`, which runs only what is still missing.
120
119
  */
121
120
  loadLanguage(code?: keyof TLanguages & string): Promise<void>;
122
121
  /** Merge additional translations at runtime (e.g. fetched from an API). Notifies change listeners. */
@@ -9,9 +9,14 @@
9
9
  // ---------------------------------------------------------------------------
10
10
  // Implementation
11
11
  // ---------------------------------------------------------------------------
12
- /** Islamery-style browser detection: ordered prefs, full code then primary subtag. */
12
+ /**
13
+ * Browser detection: ordered prefs, full code then primary subtag. Only in a
14
+ * page, where there is a document: Node 21 and later, Deno and Bun define
15
+ * `navigator.languages` too, from the process locale, and a server's locale
16
+ * is not its reader's.
17
+ */
13
18
  const detectBrowserLanguage = (isCode) => {
14
- if (typeof navigator === "undefined")
19
+ if (typeof document === "undefined" || typeof navigator === "undefined")
15
20
  return null;
16
21
  const prefs = navigator.languages?.length ? navigator.languages : [navigator.language];
17
22
  for (const pref of prefs ?? []) {
@@ -30,33 +35,37 @@ class LanguageState {
30
35
  defaultLanguage;
31
36
  customDetect;
32
37
  override = null;
33
- detected = null;
38
+ /** A throwing detector is reported once rather than on every t() call that runs it. */
39
+ detectorFailureReported = false;
34
40
  listeners = new Set();
35
41
  constructor(isCode, defaultLanguage, customDetect) {
36
42
  this.isCode = isCode;
37
43
  this.defaultLanguage = defaultLanguage;
38
44
  this.customDetect = customDetect;
39
45
  }
46
+ /**
47
+ * The active language: the one set, else detected afresh on every read.
48
+ * Detection is cheap, and what it reads lives outside this instance (the
49
+ * path an SPA navigates, the browser's languages), so remembering its
50
+ * first answer would keep a page on /en/ after it moved to /tr/.
51
+ */
40
52
  resolve() {
41
53
  if (this.override)
42
54
  return this.override;
43
- if (this.detected)
44
- return this.detected;
45
55
  // Fail-open: a broken app detector must not take down every t() call.
46
56
  let custom = null;
47
57
  try {
48
58
  custom = this.customDetect?.();
49
59
  }
50
60
  catch (err) {
51
- console.error("LambderI18n: detectLanguage threw; continuing detection chain.", err);
61
+ if (!this.detectorFailureReported) {
62
+ this.detectorFailureReported = true;
63
+ console.error("LambderI18n: detectLanguage threw; continuing detection chain.", err);
64
+ }
52
65
  }
53
- if (custom && this.isCode(custom)) {
54
- this.detected = custom;
66
+ if (custom && this.isCode(custom))
55
67
  return custom;
56
- }
57
- const browser = detectBrowserLanguage(this.isCode);
58
- this.detected = browser ?? this.defaultLanguage;
59
- return this.detected;
68
+ return detectBrowserLanguage(this.isCode) ?? this.defaultLanguage;
60
69
  }
61
70
  set(code) {
62
71
  if (!this.isCode(code))
@@ -68,7 +77,6 @@ class LanguageState {
68
77
  }
69
78
  reset() {
70
79
  this.override = null;
71
- this.detected = null;
72
80
  this.notify(this.resolve());
73
81
  }
74
82
  /** Re-notify listeners without a language change (e.g. dictionaries changed). */
@@ -91,14 +99,18 @@ class LanguageState {
91
99
  }
92
100
  }
93
101
  }
102
+ /**
103
+ * Fills `{name}` tokens in one pass over the template, so a value is inserted
104
+ * as it is: a display name "Eve {org}" stays that, rather than having its own
105
+ * `{org}` filled by the next parameter. A token with no parameter stays.
106
+ * Own properties only, through Object.prototype.hasOwnProperty rather than
107
+ * Object.hasOwn: this runs in the browser bundle, and a browser without
108
+ * ES2022 (Safari before 15.4) would throw on the first parameterised text.
109
+ */
94
110
  const interpolate = (text, params) => {
95
111
  if (!params)
96
112
  return text;
97
- let out = text;
98
- for (const [token, value] of Object.entries(params)) {
99
- out = out.split(`{${token}}`).join(String(value));
100
- }
101
- return out;
113
+ return text.replace(/\{([^{}]+)\}/g, (token, name) => Object.prototype.hasOwnProperty.call(params, name) ? String(params[name]) : token);
102
114
  };
103
115
  const layerLookup = (layer, lang, key) => {
104
116
  for (let node = layer; node; node = node.parent) {
@@ -151,9 +163,9 @@ const startLayerLoad = (core, layer, lang, loader) => {
151
163
  const load = Promise.resolve()
152
164
  .then(loader)
153
165
  .then((loaded) => {
154
- // A loader that answered is spent even when its answer is refused:
155
- // running it again would fetch the same file and fail the same
156
- // way. Only a loader that rejected stays, to be retried.
166
+ // A loader that answered is spent even when its answer is refused,
167
+ // since running it again would fail the same way. Only a loader
168
+ // that rejected stays, to be retried.
157
169
  layer.loaders.delete(lang);
158
170
  if (layer.loaders.size === 0)
159
171
  core.lazyLayers.delete(layer);
@@ -0,0 +1,66 @@
1
+ /**
2
+ * The cache vocabulary every cache in Lambder shares: how an entry is
3
+ * addressed, what writing and listing take, and the methods an app calls.
4
+ *
5
+ * Kept apart from LambderDdbCache for the reason the rate-limit and
6
+ * idempotency contracts are: an app that types its caches against this
7
+ * interface can hold a LambderMemoryCache in a test and a LambderDdbCache in
8
+ * production without the test's import graph ever reaching the DynamoDB
9
+ * store. Pure and dependency-free.
10
+ */
11
+ /**
12
+ * Where a value lives. A plain string addresses one entry. `{ pk, sk }` puts
13
+ * the entry in a partition it can share with others, so a group can be
14
+ * listed or dropped in one call: `{ pk: "store:nyc-01", sk: "1700:1800" }`
15
+ * keeps every cached window of one store together.
16
+ */
17
+ export type LambderCacheKey = string | {
18
+ pk: string;
19
+ sk: string;
20
+ };
21
+ export interface LambderCacheSetOptions {
22
+ /** How long the entry lives. Default: the cache's own defaultTtlSeconds. */
23
+ ttlSeconds?: number;
24
+ }
25
+ export interface LambderCacheListOptions {
26
+ /** Only sort keys starting with this prefix. */
27
+ prefix?: string;
28
+ /** Cap on RESULTS, not on entries read. */
29
+ limit?: number;
30
+ }
31
+ /**
32
+ * What an app asks of a cache. LambderDdbCache and LambderMemoryCache
33
+ * implement it with the same rules (the same key limits, the same JSON round
34
+ * trip, the same expiry), so code written against this interface behaves the
35
+ * same over either.
36
+ *
37
+ * Values are JSON: what `set` is handed is stored as its JSON text and `get`
38
+ * hands back a parse of it, never the object that was stored.
39
+ */
40
+ export interface LambderCache {
41
+ /** The value, or undefined when it is absent or past its TTL. */
42
+ get<T>(key: LambderCacheKey): Promise<T | undefined>;
43
+ has(key: LambderCacheKey): Promise<boolean>;
44
+ /** Stores the value's JSON; throws for a value JSON cannot represent (undefined, a function). */
45
+ set<T>(key: LambderCacheKey, value: T, options?: LambderCacheSetOptions): Promise<void>;
46
+ /** True when there was a live entry to delete: a load in progress for the key, or an entry past its TTL, is none. */
47
+ delete(key: LambderCacheKey): Promise<boolean>;
48
+ /** Drops every entry stored under one `pk` and answers how many live ones there were. */
49
+ deletePartition(partition: string): Promise<number>;
50
+ /** The live sort keys stored under one `pk`, in sort-key order. Plain-string entries have none, so they never appear. */
51
+ listSortKeys(partition: string, options?: LambderCacheListOptions): Promise<string[]>;
52
+ /**
53
+ * The cached value, or the loader's once it has been stored, as the
54
+ * stored JSON either way, so the call that filled the entry answers what
55
+ * every later one does. Concurrent calls for one key in one process share
56
+ * a single load, each handed a parse of its own, so one caller's change to
57
+ * its answer never shows in another's. A loader's undefined is not cached
58
+ * (return null to cache "not found"), and a value the cache fails to store
59
+ * is handed back uncached, as the same JSON. So is the value of a load
60
+ * that a `set`, `delete` or `deletePartition` of the key overtook: the
61
+ * loader may have read its source before that write, so its value never
62
+ * lands over it. An invalid option throws before anything is read or
63
+ * loaded.
64
+ */
65
+ getOrSet<T>(key: LambderCacheKey, loader: () => Promise<T>, options?: LambderCacheSetOptions): Promise<T>;
66
+ }
@@ -0,0 +1,11 @@
1
+ /**
2
+ * The cache vocabulary every cache in Lambder shares: how an entry is
3
+ * addressed, what writing and listing take, and the methods an app calls.
4
+ *
5
+ * Kept apart from LambderDdbCache for the reason the rate-limit and
6
+ * idempotency contracts are: an app that types its caches against this
7
+ * interface can hold a LambderMemoryCache in a test and a LambderDdbCache in
8
+ * production without the test's import graph ever reaching the DynamoDB
9
+ * store. Pure and dependency-free.
10
+ */
11
+ export {};
@@ -5,12 +5,12 @@ export type LambderFile = {
5
5
  };
6
6
  /**
7
7
  * Where an app's files come from: the `files` option at creation, read by
8
- * servePublicFiles, serveIndexHtml, res.file and res.templateFile alike,
9
- * through the instance's one reader (LambderFiles). Implement `read` over
10
- * any backing store: LambderLocalFileSource (a folder), LambderS3FileSource
11
- * (S3, or R2 and other S3-compatible stores), LambderHttpFileSource (any
12
- * origin serving files by path), or your own. The reader does the rest for
13
- * every source: path rule, memory cache, mime fallback from the extension.
8
+ * servePublicFiles, serveIndexHtml, res.file and res.templateFile alike
9
+ * through the instance's one reader (LambderFiles), which adds the path rule,
10
+ * memory cache and mime fallback for every source. Implement `read` over any
11
+ * backing store, or use LambderLocalFileSource (a folder),
12
+ * LambderS3FileSource (S3, R2 and other S3-compatible stores) or
13
+ * LambderHttpFileSource (any origin serving files by path).
14
14
  */
15
15
  export interface LambderFileSource {
16
16
  /**
@@ -2,14 +2,11 @@
2
2
  * Where an app's files come from: the interface, and the one helper its
3
3
  * remote implementations share.
4
4
  *
5
- * Declared in shared/ beside LambderSessionStore, LambderRateLimiter and
6
- * LambderIdempotencyStore, so all four store families read the same way: the
7
- * interface here, the implementations in stores/ (LambderLocalFileSource,
8
- * LambderS3FileSource, LambderHttpFileSource). core/ names the interface and
9
- * nothing else of the family.
10
- *
11
- * What reads through a source is the instance's reader, LambderFiles: it owns
12
- * the path rule, the memory cache and the mime fallback.
5
+ * Laid out like the other store families (LambderSessionStore,
6
+ * LambderRateLimiter, LambderIdempotencyStore): the interface here, the
7
+ * implementations in stores/. core/ names only the interface. The instance's
8
+ * reader, LambderFiles, reads through a source and owns the path rule, the
9
+ * memory cache and the mime fallback.
13
10
  */
14
11
  /**
15
12
  * A file a remote store returned: the store's Content-Type unless it is a
@@ -3,11 +3,10 @@
3
3
  * answer looks like, what claiming a scope reports, and the four methods the
4
4
  * idempotency engine asks of a store.
5
5
  *
6
- * Kept apart from the engine for the same reason LambderRateLimiter is:
7
- * a store implements this and nothing else, and importing it from the engine
8
- * would pull the engine (and through it the refusal machinery) into every
9
- * store's import graph. Pure and dependency-free, so the mock runtime and the
10
- * browser entry can resolve it.
6
+ * Kept apart from the engine, like LambderRateLimiter: a store implements this
7
+ * and nothing else, and importing it from the engine would pull the engine
8
+ * and its refusal machinery into every store's import graph. Dependency-free,
9
+ * so the mock runtime and the browser entry can resolve it.
11
10
  */
12
11
  /** A stored answer: what a completed record replays. */
13
12
  export type LambderIdempotencyDoneRecord = {
@@ -15,12 +14,22 @@ export type LambderIdempotencyDoneRecord = {
15
14
  /** Response headers stored with the record (normalized multi-value map). */
16
15
  headers: Record<string, string[]>;
17
16
  body: string;
17
+ /**
18
+ * The request the answer belongs to, as a digest of its payload: a key
19
+ * reused for a different request is refused rather than handed this
20
+ * answer. The engine's fingerprints are never empty, so a store that
21
+ * holds a record it cannot tie to a request (one another writer left in
22
+ * its table) reports the empty string, which matches no request and has
23
+ * the key refused as reused.
24
+ */
25
+ fingerprint: string;
18
26
  };
19
27
  export type LambderIdempotencyBeginResult = {
20
28
  state: "new";
21
29
  ownerToken: string;
22
30
  } | {
23
31
  state: "pending";
32
+ fingerprint: string;
24
33
  } | ({
25
34
  state: "done";
26
35
  } & LambderIdempotencyDoneRecord);
@@ -29,38 +38,58 @@ export type LambderIdempotencyBeginResult = {
29
38
  * atomically, settled by the claim's owner. LambderDdbIdempotencyStore and
30
39
  * LambderMemoryIdempotencyStore implement it; an app may bring its own.
31
40
  *
32
- * The rules an implementation has to keep are the ones tests/store-conformance
33
- * asserts against every implementation, and the three that are easy to get
34
- * wrong are worth naming here.
41
+ * tests/store-conformance asserts the rules against every implementation.
42
+ * Four are easy to get wrong:
35
43
  *
36
44
  * A read hands back a COPY of the record, never the stored object, because a
37
45
  * caller applies its own headers onto what it gets back.
38
46
  *
39
- * A write takes a copy too: complete() must not keep the caller's record or
40
- * its headers map, because that object goes on being used after the call
41
- * returns (the pipeline writes the call's own headers into it on the way
42
- * out). A store that retained it would let one request's Set-Cookie become
43
- * part of the stored answer and replay to everybody else. A store that goes
44
- * over the wire gets this for free, since serializing IS the copy; one that
45
- * keeps the record in the process has to make it.
47
+ * A write takes a copy too: the pipeline keeps writing the call's own headers
48
+ * into the record after complete() returns, so a store that retained it
49
+ * would let one request's Set-Cookie join the stored answer and replay to
50
+ * everybody else. A store that serializes over the wire copies for free; one
51
+ * that keeps the record in the process has to copy it.
46
52
  *
47
- * Size decides before ownership does, so an answer too big to store reports
53
+ * Size decides before ownership, so an answer too big to store reports
48
54
  * "too-large" even when the claim has meanwhile been lost. The engine
49
- * releases the claim either way, so the order only shows in which reason it
50
- * is told, but an implementation that inverted it would disagree with every
51
- * other one.
55
+ * releases the claim either way and only the reported reason differs, but an
56
+ * implementation with the order inverted would disagree with every other.
57
+ *
58
+ * abandon() releases a PENDING claim and nothing else: a stored record stays,
59
+ * even when the owner that stored it asks. The engine abandons after any
60
+ * complete() that throws, and one whose response was lost may have landed;
61
+ * deleting what it stored would run the operation again on the retry that
62
+ * should have replayed it.
52
63
  */
53
64
  export interface LambderIdempotencyStore {
54
65
  /** The stored answer when a completed, unexpired record exists, null otherwise (absent, pending, or expired). */
55
66
  peek(scopeKey: string): Promise<LambderIdempotencyDoneRecord | null>;
56
- /** Claims the scope: "new" with the ownerToken to settle with, "pending" when another request owns it, "done" with the answer to replay. */
67
+ /**
68
+ * Claims the scope: "new" with the ownerToken to settle with, "pending"
69
+ * when another request owns it, "done" with the answer to replay. The
70
+ * claim keeps `fingerprint`, and "pending" and "done" report the one the
71
+ * scope holds, so the engine can tell a retry from a different request.
72
+ * A store that refuses the claim with no record to show for it (no room
73
+ * to hold one) reports the caller's own: the engine then answers the
74
+ * in-flight 409, which a client retries under the same key, rather than
75
+ * the key-reused one, which would move its key on.
76
+ */
57
77
  begin(scopeKey: string, options: {
58
78
  pendingTtlSeconds: number;
79
+ fingerprint: string;
59
80
  }): Promise<LambderIdempotencyBeginResult>;
60
- /** Stores the answer over the claim: "stored", "too-large" (nothing written; release the claim), or "lost" (the claim expired and a retry took the scope). */
81
+ /**
82
+ * Stores the answer over the claim, with the fingerprint of the request
83
+ * it answers: "stored", "too-large" (nothing written; release the claim),
84
+ * or "lost" (the claim expired and a retry took the scope).
85
+ */
61
86
  complete(scopeKey: string, ownerToken: string, record: LambderIdempotencyDoneRecord & {
62
87
  ttlSeconds: number;
63
88
  }): Promise<"stored" | "too-large" | "lost">;
64
- /** Releases the claim without storing; a lost claim makes this a silent no-op. */
89
+ /**
90
+ * Releases the owner's claim while it is still pending, so a retry can
91
+ * execute. A lost claim, or one already settled by complete(), makes
92
+ * this a silent no-op.
93
+ */
65
94
  abandon(scopeKey: string, ownerToken: string): Promise<void>;
66
95
  }
@@ -3,10 +3,9 @@
3
3
  * answer looks like, what claiming a scope reports, and the four methods the
4
4
  * idempotency engine asks of a store.
5
5
  *
6
- * Kept apart from the engine for the same reason LambderRateLimiter is:
7
- * a store implements this and nothing else, and importing it from the engine
8
- * would pull the engine (and through it the refusal machinery) into every
9
- * store's import graph. Pure and dependency-free, so the mock runtime and the
10
- * browser entry can resolve it.
6
+ * Kept apart from the engine, like LambderRateLimiter: a store implements this
7
+ * and nothing else, and importing it from the engine would pull the engine
8
+ * and its refusal machinery into every store's import graph. Dependency-free,
9
+ * so the mock runtime and the browser entry can resolve it.
11
10
  */
12
11
  export {};
@@ -3,11 +3,10 @@
3
3
  * policy may cap, the policy shape, what an exceeded check reports, and the
4
4
  * one method the rate-limit engine asks of a limiter.
5
5
  *
6
- * Kept apart from the DynamoDB limiter on purpose. The engine needs only this
7
- * table, and importing it from the store would pull the DynamoDB SDK loader
8
- * into the engine's import graph, which is what kept the policy layer from
9
- * running anywhere but inside a Lambda. Pure and dependency-free, so the
10
- * mock runtime and the browser entry can resolve it.
6
+ * Kept apart from the DynamoDB limiter on purpose: importing it from the
7
+ * store would pull the DynamoDB SDK loader into the engine's import graph and
8
+ * keep the policy layer from running anywhere but inside a Lambda. Pure and
9
+ * dependency-free, so the mock runtime and the browser entry can resolve it.
11
10
  */
12
11
  /**
13
12
  * The fixed windows a policy may cap, smallest first (the evaluation order),
@@ -53,19 +52,32 @@ export type LambderRateLimitResult = false | LambderRateLimitExceeded;
53
52
  * DynamoDB limiter and the in-memory one implement it; an app may bring its
54
53
  * own (Redis, a database) by implementing this one method.
55
54
  *
56
- * The engine validates a policy before it ever reaches here, so every capped
57
- * window arrives as a non-negative whole number and a limiter never has to
58
- * invent an answer for a nonsense one. A caller reaching isRateLimited
59
- * directly owes the same precondition: the two shipped implementations
60
- * disagree on a negative cap (one refuses the first attempt, the other allows
61
- * it) because neither was ever meant to be asked.
55
+ * The engine validates a policy before it reaches here, so every capped
56
+ * window arrives as a non-negative whole number. A caller reaching
57
+ * isRateLimited directly owes the same precondition: the two shipped
58
+ * implementations disagree on a negative cap (one refuses the first attempt,
59
+ * the other allows it), since neither is meant to be asked.
62
60
  *
63
61
  * The tracker key is bounded there too: the variable half of it (a custom
64
- * key handler's return, a session key) is replaced by its own sha256 past
65
- * 1024 UTF-8 bytes, so an implementation with a key limit of its own never
66
- * meets a key it has to refuse. That matters because a limiter's refusal is a
67
- * throw, and a throw is what failOpen swallows into no limit at all.
62
+ * key handler's return, a session key) is replaced by its own sha256 once it
63
+ * passes 1024 UTF-8 bytes as written into the key, so an implementation with
64
+ * a key limit of its own never meets a key it has to refuse. That matters
65
+ * because a limiter's refusal is a throw, and a throw is what failOpen
66
+ * swallows into no limit at all.
67
+ *
68
+ * A limiter that answers a run of requests with one continuing failure (the
69
+ * DynamoDB limiter, for a flooded partition it cannot size) may throw the
70
+ * same error object for each of them: the engine logs a failure once, not
71
+ * once per request that meets it.
68
72
  */
69
73
  export interface LambderRateLimiter {
70
74
  isRateLimited(trackerKey: string, policy: LambderRateLimitPolicy): Promise<LambderRateLimitResult>;
75
+ /**
76
+ * The clock the windows are computed against, in epoch milliseconds, for
77
+ * a limiter that keeps one of its own (an injected test clock). A
78
+ * refusal's `resetAt` is a second on this clock, so the engine reads the
79
+ * Retry-After against it. Optional: without it, the engine reads
80
+ * Date.now().
81
+ */
82
+ clockMilliseconds?(): number;
71
83
  }
@@ -3,11 +3,10 @@
3
3
  * policy may cap, the policy shape, what an exceeded check reports, and the
4
4
  * one method the rate-limit engine asks of a limiter.
5
5
  *
6
- * Kept apart from the DynamoDB limiter on purpose. The engine needs only this
7
- * table, and importing it from the store would pull the DynamoDB SDK loader
8
- * into the engine's import graph, which is what kept the policy layer from
9
- * running anywhere but inside a Lambda. Pure and dependency-free, so the
10
- * mock runtime and the browser entry can resolve it.
6
+ * Kept apart from the DynamoDB limiter on purpose: importing it from the
7
+ * store would pull the DynamoDB SDK loader into the engine's import graph and
8
+ * keep the policy layer from running anywhere but inside a Lambda. Pure and
9
+ * dependency-free, so the mock runtime and the browser entry can resolve it.
11
10
  */
12
11
  /**
13
12
  * The fixed windows a policy may cap, smallest first (the evaluation order),
@@ -3,12 +3,11 @@
3
3
  * it rests in.
4
4
  *
5
5
  * The manager owns the model (token format, hashing, expiry, sliding
6
- * writes, dataRefresh, regeneration); a store owns nothing but the five
7
- * operations below, keyed by the two hashes. LambderDdbSessionStore is the
8
- * DynamoDB implementation and LambderMemorySessionStore the in-memory one;
9
- * an app may bring its own (Redis, a database) by implementing this
10
- * interface. Records written by one store read back through another with
11
- * the same shape, because the shape is the manager's, not the store's.
6
+ * writes, dataRefresh, regeneration); a store owns only the operations
7
+ * below, keyed by the two hashes. LambderDdbSessionStore and
8
+ * LambderMemorySessionStore implement it, and an app may bring its own
9
+ * (Redis, a database). The record shape is the manager's, so records written
10
+ * by one store read back through another.
12
11
  */
13
12
  /**
14
13
  * A session record. The two hashes are the record's identity: the partition
@@ -17,7 +16,7 @@
17
16
  * stored, so a read of the store yields no usable credentials.
18
17
  */
19
18
  export type LambderSessionRecord<SessionData = unknown> = {
20
- /** Salted sha256 of the sessionKey: the partition every session of one subject shares. */
19
+ /** HMAC-SHA256 of the sessionKey keyed by the salt: the partition every session of one subject shares. */
21
20
  sessionKeyHash: string;
22
21
  /** sha256 of the sort-key secret the client's session cookie carries. */
23
22
  secretHash: string;
@@ -37,36 +36,76 @@ export type LambderSessionRecord<SessionData = unknown> = {
37
36
  * session's own expiresAt.
38
37
  */
39
38
  dataExpiresAt?: number;
39
+ /**
40
+ * How many writes `data` and `dataExpiresAt` have had: 0 when the record
41
+ * is created, and one more on every update that writes either of them,
42
+ * which the store adds in the same atomic write. A conditioned update
43
+ * names the version it read (see LambderSessionStore.update).
44
+ */
45
+ dataVersion: number;
40
46
  };
47
+ /**
48
+ * The fields of a stored session a write may change: the data and its
49
+ * refresh deadline, and the sliding expiry. A session's identity, its CSRF
50
+ * hash, its subject and its creation are fixed when it is created, and its
51
+ * dataVersion is the store's to advance, never the caller's to set.
52
+ */
53
+ export type LambderSessionChanges<SessionData = unknown> = Partial<Pick<LambderSessionRecord<SessionData>, "data" | "dataExpiresAt" | "lastAccessedAt" | "expiresAt">>;
54
+ /**
55
+ * What an update found: `"updated"` when it applied, `"missing"` when the
56
+ * record is gone (a logout, a password change, an expiry), and `"stale"`
57
+ * when the record exists but its dataVersion is no longer the one the
58
+ * update was conditioned on, so something else wrote its data or its
59
+ * refresh deadline in between.
60
+ */
61
+ export type LambderSessionUpdateResult = "updated" | "missing" | "stale";
41
62
  export interface LambderSessionStore<SessionData = unknown> {
42
63
  /**
43
- * Whether records live only for as long as this process does. The session
44
- * manager reads it to refuse a non-cryptographic LambderSessionCrypto
45
- * over a store that outlives the process: hashing that is not hashing is
46
- * survivable in memory for a development run and never survivable at
47
- * rest, where the records would be usable credentials.
64
+ * Whether records live only as long as this process. The session manager
65
+ * reads it to refuse a non-cryptographic LambderSessionCrypto over a store
66
+ * that outlives the process: fake hashing is tolerable in memory for a
67
+ * development run, but at rest the records would be usable credentials.
48
68
  */
49
69
  readonly isMemoryOnly: boolean;
50
70
  /**
51
71
  * The record under the two hashes, or null. A failing read throws; the
52
72
  * manager wraps it as a LambderSessionReadError.
53
73
  *
54
- * A store MAY hand back a record that is past its expiresAt: a DynamoDB
55
- * TTL deletes within days rather than at the second, and an implementation
56
- * over a plain table has nothing that sweeps at all. Expiry is the
57
- * manager's to enforce, and it does, on every read. A store that drops
58
- * expired records itself is doing housekeeping, not policy.
74
+ * A store MAY hand back a record past its expiresAt: a DynamoDB TTL
75
+ * deletes within days rather than at the second, and a plain table has
76
+ * nothing that sweeps at all. The manager enforces expiry on every read;
77
+ * a store that drops expired records itself is doing housekeeping, not
78
+ * policy.
59
79
  */
60
80
  get(sessionKeyHash: string, secretHash: string): Promise<LambderSessionRecord<SessionData> | null>;
61
- /** Writes the record, replacing any under the same hashes. */
62
- put(record: LambderSessionRecord<SessionData>): Promise<void>;
63
- delete(sessionKeyHash: string, secretHash: string): Promise<void>;
64
- /** Every secretHash stored under the partition: the sessions of one subject. */
65
- listSecretHashes(sessionKeyHash: string): Promise<string[]>;
81
+ /** Writes a new record, and throws when one already exists under the two hashes: a session is minted once and never overwritten. */
82
+ create(record: LambderSessionRecord<SessionData>): Promise<void>;
66
83
  /**
67
- * Stamps dataExpiresAt on one record, only if it still exists: neither
68
- * resurrects a session deleted in between nor overwrites a concurrent
69
- * write. A record that no longer exists is skipped silently.
84
+ * Changes the named fields of an existing record and no others, and only
85
+ * while it exists: an update never brings back a session deleted in
86
+ * between, which is what keeps a logout, "log out everywhere" or a
87
+ * password change from being undone by a write already in flight.
88
+ *
89
+ * An update whose changes carry `data` or `dataExpiresAt` also adds one
90
+ * to the record's dataVersion, in the same atomic write (DynamoDB's
91
+ * `ADD`), even when the value written equals the one stored. With
92
+ * `condition`, the update applies only while the record's dataVersion is
93
+ * still `condition.dataVersion`, and answers "stale" otherwise, so data
94
+ * derived from an earlier read cannot land over a concurrent refresh,
95
+ * data write or expireSessionDataAllByKey. The version is what makes
96
+ * that hold: a deadline compared in whole seconds cannot show a mark
97
+ * that wrote the value already stored, or two marks in one second.
70
98
  */
71
- markDataExpired(sessionKeyHash: string, secretHash: string, at: number): Promise<void>;
99
+ update(sessionKeyHash: string, secretHash: string, changes: LambderSessionChanges<SessionData>, condition?: {
100
+ dataVersion: number;
101
+ }): Promise<LambderSessionUpdateResult>;
102
+ /**
103
+ * Removes the record and hands back what it held at that moment, or null
104
+ * when there was none: rotation carries the stored record over, not the
105
+ * one a request read earlier, and mints nothing when a logout got there
106
+ * first.
107
+ */
108
+ delete(sessionKeyHash: string, secretHash: string): Promise<LambderSessionRecord<SessionData> | null>;
109
+ /** Every secretHash stored under the partition, read consistently: the sessions of one subject, including one created a moment ago. */
110
+ listSecretHashes(sessionKeyHash: string): Promise<string[]>;
72
111
  }
@@ -3,11 +3,10 @@
3
3
  * it rests in.
4
4
  *
5
5
  * The manager owns the model (token format, hashing, expiry, sliding
6
- * writes, dataRefresh, regeneration); a store owns nothing but the five
7
- * operations below, keyed by the two hashes. LambderDdbSessionStore is the
8
- * DynamoDB implementation and LambderMemorySessionStore the in-memory one;
9
- * an app may bring its own (Redis, a database) by implementing this
10
- * interface. Records written by one store read back through another with
11
- * the same shape, because the shape is the manager's, not the store's.
6
+ * writes, dataRefresh, regeneration); a store owns only the operations
7
+ * below, keyed by the two hashes. LambderDdbSessionStore and
8
+ * LambderMemorySessionStore implement it, and an app may bring its own
9
+ * (Redis, a database). The record shape is the manager's, so records written
10
+ * by one store read back through another.
12
11
  */
13
12
  export {};