lambder 7.3.1 → 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 (207) hide show
  1. package/CHANGELOG.md +933 -3
  2. package/README.md +41 -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 +68 -62
  13. package/dist/api/LambderApiIdempotency.js +214 -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 +47 -38
  17. package/dist/api/LambderApiPipeline.js +122 -63
  18. package/dist/api/LambderApiRateLimits.d.ts +201 -54
  19. package/dist/api/LambderApiRateLimits.js +185 -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 +140 -75
  41. package/dist/core/Lambder.js +347 -227
  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 +21 -7
  51. package/dist/core/LambderFiles.js +62 -34
  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 +29 -9
  73. package/dist/invoke/LambderLambdaEvent.js +40 -22
  74. package/dist/invoke/lambderHandlerTransport.d.ts +9 -10
  75. package/dist/invoke/lambderHandlerTransport.js +15 -18
  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 +1 -1
  93. package/dist/mock.js +2 -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 +124 -46
  99. package/dist/session/LambderSessionManager.js +262 -137
  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/LambderTypeUtilities.d.ts +7 -8
  134. package/dist/shared/util/LambderTypeUtilities.js +3 -3
  135. package/dist/shared/util/boundKeyField.d.ts +20 -0
  136. package/dist/shared/util/boundKeyField.js +34 -0
  137. package/dist/shared/util/canonicalJson.d.ts +11 -0
  138. package/dist/shared/util/canonicalJson.js +28 -0
  139. package/dist/shared/util/joinKeyFields.d.ts +20 -0
  140. package/dist/shared/util/joinKeyFields.js +22 -0
  141. package/dist/shared/wire/LambderAnswerHeaders.d.ts +12 -16
  142. package/dist/shared/wire/LambderAnswerHeaders.js +12 -16
  143. package/dist/shared/wire/LambderApiContract.d.ts +107 -32
  144. package/dist/shared/wire/LambderApiOutcome.d.ts +43 -31
  145. package/dist/shared/wire/LambderApiOutcome.js +48 -23
  146. package/dist/shared/wire/LambderApiRefusal.d.ts +39 -27
  147. package/dist/shared/wire/LambderApiRefusal.js +36 -7
  148. package/dist/shared/wire/LambderApiSignature.d.ts +18 -22
  149. package/dist/shared/wire/LambderApiSignature.js +16 -19
  150. package/dist/shared/wire/LambderCallOptions.d.ts +38 -47
  151. package/dist/shared/wire/LambderCallOptions.js +9 -11
  152. package/dist/shared/wire/LambderCompressionCodec.d.ts +29 -34
  153. package/dist/shared/wire/LambderCompressionCodec.js +31 -36
  154. package/dist/shared/wire/LambderCompressionOption.d.ts +9 -9
  155. package/dist/shared/wire/LambderCompressionOption.js +9 -9
  156. package/dist/shared/wire/LambderCrashDetail.d.ts +12 -15
  157. package/dist/shared/wire/LambderCrashDetail.js +12 -15
  158. package/dist/shared/wire/LambderDefaultApiPath.d.ts +6 -0
  159. package/dist/shared/wire/LambderDefaultApiPath.js +6 -0
  160. package/dist/shared/wire/LambderHttpStatus.d.ts +6 -7
  161. package/dist/shared/wire/LambderIdempotencyKeyScope.d.ts +89 -0
  162. package/dist/shared/wire/LambderIdempotencyKeyScope.js +146 -0
  163. package/dist/shared/wire/LambderInvokeApiId.d.ts +27 -0
  164. package/dist/shared/wire/LambderInvokeApiId.js +27 -0
  165. package/dist/shared/wire/LambderOutcomeAssertions.d.ts +6 -7
  166. package/dist/shared/wire/LambderOutcomeAssertions.js +6 -7
  167. package/dist/shared/wire/LambderRequestPayload.d.ts +18 -20
  168. package/dist/shared/wire/LambderRequestPayload.js +4 -6
  169. package/dist/stores/LambderCacheFiller.d.ts +48 -0
  170. package/dist/stores/LambderCacheFiller.js +119 -0
  171. package/dist/stores/LambderCacheKeys.d.ts +26 -0
  172. package/dist/stores/LambderCacheKeys.js +54 -0
  173. package/dist/stores/LambderCacheValues.d.ts +45 -0
  174. package/dist/stores/LambderCacheValues.js +74 -0
  175. package/dist/stores/LambderDdbCache.d.ts +121 -56
  176. package/dist/stores/LambderDdbCache.js +528 -225
  177. package/dist/stores/LambderDdbIdempotencyStore.d.ts +33 -22
  178. package/dist/stores/LambderDdbIdempotencyStore.js +75 -50
  179. package/dist/stores/LambderDdbRateLimiter.d.ts +76 -20
  180. package/dist/stores/LambderDdbRateLimiter.js +151 -39
  181. package/dist/stores/LambderDdbSdk.d.ts +43 -31
  182. package/dist/stores/LambderDdbSdk.js +79 -33
  183. package/dist/stores/LambderDdbSessionStore.d.ts +27 -14
  184. package/dist/stores/LambderDdbSessionStore.js +119 -47
  185. package/dist/stores/LambderHttpFileSource.d.ts +15 -6
  186. package/dist/stores/LambderHttpFileSource.js +15 -13
  187. package/dist/stores/LambderMemoryCache.d.ts +49 -0
  188. package/dist/stores/LambderMemoryCache.js +113 -0
  189. package/dist/stores/LambderMemoryIdempotencyStore.d.ts +13 -12
  190. package/dist/stores/LambderMemoryIdempotencyStore.js +31 -30
  191. package/dist/stores/LambderMemoryRateLimiter.d.ts +8 -9
  192. package/dist/stores/LambderMemoryRateLimiter.js +14 -13
  193. package/dist/stores/LambderMemorySessionStore.d.ts +14 -11
  194. package/dist/stores/LambderMemorySessionStore.js +38 -19
  195. package/dist/stores/LambderS3FileSource.d.ts +21 -6
  196. package/dist/stores/LambderS3FileSource.js +12 -7
  197. package/dist/testing/LambderTestApp.d.ts +21 -23
  198. package/dist/testing/LambderTestApp.js +22 -24
  199. package/dist/testing/LambderTestVisitor.d.ts +10 -12
  200. package/dist/testing/LambderTestVisitor.js +15 -15
  201. package/dist/testing.d.ts +1 -0
  202. package/dist/testing.js +1 -0
  203. package/package.json +12 -3
  204. package/dist/api/LambderApiPolicyEngine.d.ts +0 -47
  205. package/dist/api/LambderApiPolicyEngine.js +0 -85
  206. package/dist/shared/util/LambderKeyFields.d.ts +0 -32
  207. package/dist/shared/util/LambderKeyFields.js +0 -34
@@ -2,7 +2,7 @@
2
2
  * The cryptography the session model runs on, behind an interface so the
3
3
  * manager itself has no Node dependency: the bearer secrets are hashed at
4
4
  * rest, compared in constant time, and minted from a cryptographic random
5
- * source.
5
+ * source, and the sessionKey is hashed under the salt as its key.
6
6
  *
7
7
  * LambderWebCrypto is the default and runs on Node 20+, every browser on a
8
8
  * secure context, and edge runtimes. LambderPlainSessionCrypto is the
@@ -23,7 +23,7 @@ const constantTimeEqual = (a, b) => {
23
23
  };
24
24
  /** True when this runtime offers WebCrypto's subtle API (secure contexts in browsers; Node 20+). */
25
25
  export const isWebCryptoAvailable = () => typeof globalThis.crypto?.subtle?.digest === "function" && typeof globalThis.crypto.getRandomValues === "function";
26
- /** sha256 through crypto.subtle and randomness through getRandomValues: the default. */
26
+ /** sha256 and HMAC through crypto.subtle and randomness through getRandomValues: the default. */
27
27
  export class LambderWebCrypto {
28
28
  isCryptographic = true;
29
29
  cryptoPromise;
@@ -37,11 +37,11 @@ export class LambderWebCrypto {
37
37
  * The runtime's WebCrypto, through the resolver every layer shares, with
38
38
  * Node's crypto warmed alongside it.
39
39
  *
40
- * The availability question is asked here, before the shared resolver,
41
- * only because of the answer a session has to it: a runtime with neither
42
- * a global crypto nor Node's webcrypto can still run sessions over
43
- * LambderPlainSessionCrypto and a memory store, which is this layer's own
44
- * way out and not something the shared message can know about.
40
+ * Availability is checked here, before the shared resolver, so the error
41
+ * can name this layer's own way out: a runtime with neither a global
42
+ * crypto nor Node's webcrypto can still run sessions over
43
+ * LambderPlainSessionCrypto and a memory store, which the shared
44
+ * resolver's message cannot know about.
45
45
  */
46
46
  ready() {
47
47
  this.cryptoPromise ??= (async () => {
@@ -56,19 +56,25 @@ export class LambderWebCrypto {
56
56
  return this.cryptoPromise;
57
57
  }
58
58
  async sha256Hex(value) {
59
- // ready() first, for the message above and for the warmed Node
60
- // crypto; the digest itself is the one every layer shares.
59
+ // ready() first, for its error message and the warmed Node crypto;
60
+ // the digest itself is the one every layer shares.
61
61
  await this.ready();
62
62
  return await sha256HexOf(value);
63
63
  }
64
+ async hmacSha256Hex(key, value) {
65
+ const webCrypto = await this.ready();
66
+ const encoder = new TextEncoder();
67
+ const hmacKey = await webCrypto.subtle.importKey("raw", encoder.encode(key), { name: "HMAC", hash: "SHA-256" }, false, ["sign"]);
68
+ return bytesToHexString(new Uint8Array(await webCrypto.subtle.sign("HMAC", hmacKey, encoder.encode(value))));
69
+ }
64
70
  async randomHex(bytes) {
65
71
  const webCrypto = await this.ready();
66
72
  return bytesToHexString(webCrypto.getRandomValues(new Uint8Array(bytes)));
67
73
  }
68
74
  constantTimeEqual(a, b) {
69
- // node's timingSafeEqual where the runtime has it: a primitive built
70
- // for this beats a JS loop the engine is free to optimize. The loop is
71
- // the fallback everywhere else, which is every browser.
75
+ // Node's timingSafeEqual where the runtime has it: a primitive built
76
+ // for this beats a JS loop the engine is free to optimize. Browsers
77
+ // fall back to the loop.
72
78
  const nodeCrypto = this.nodeCrypto;
73
79
  if (nodeCrypto && a.length === b.length) {
74
80
  const left = Buffer.from(a, "utf8");
@@ -89,6 +95,14 @@ export class LambderPlainSessionCrypto {
89
95
  async sha256Hex(value) {
90
96
  return bytesToHexString(new TextEncoder().encode(value));
91
97
  }
98
+ /**
99
+ * The key and the value hex-encoded as a JSON pair rather than run
100
+ * together, so the pair stays unambiguous the way a keyed hash keeps it:
101
+ * no key and value can pass for another split of the same text.
102
+ */
103
+ async hmacSha256Hex(key, value) {
104
+ return bytesToHexString(new TextEncoder().encode(JSON.stringify([key, value])));
105
+ }
92
106
  async randomHex(bytes) {
93
107
  const random = new Uint8Array(bytes);
94
108
  for (let i = 0; i < bytes; i += 1)
@@ -20,13 +20,20 @@ export type LambderCreatedSession<SessionData = any> = {
20
20
  * onto the same session record: same tokens, same cookies, the session
21
21
  * itself is untouched. The refresh write and the sliding-expiration write
22
22
  * share a single store write when both are due.
23
+ *
24
+ * A refresh result is written only over the data it was computed from: if
25
+ * the record's dataVersion moved in between (another request refreshed or
26
+ * wrote the data, or expireSessionDataAllByKey marked it stale), the result
27
+ * still serves this request but the newer write stands, and a marked record
28
+ * renews again on its next read.
23
29
  */
24
30
  export type LambderSessionDataRefreshConfig<SessionData = any> = {
25
31
  /** Seconds session.data stays valid before refresh() runs on read. */
26
32
  ttlSeconds: number;
27
33
  /**
28
34
  * Rebuild session.data from its source of truth. Must be a pure
29
- * derivation (concurrent reads may run it in parallel; last write wins).
35
+ * derivation (concurrent reads may run it in parallel; the first result
36
+ * written stands, since each is written only over the data it read).
30
37
  * Return null to end the session: the record is deleted and the read
31
38
  * reports no session. Thrown errors fail the read as a
32
39
  * LambderSessionDataRefreshError and leave the session untouched; catch
@@ -56,7 +63,7 @@ export declare class LambderSessionReadError extends Error {
56
63
  export type LambderSessionManagerOptions<SessionData = any> = {
57
64
  /** Where sessions rest: LambderDdbSessionStore, LambderMemorySessionStore, or your own. */
58
65
  store: LambderSessionStore<SessionData>;
59
- /** Salts the sessionKey hash that partitions the store, so a table read does not reveal which subject a session belongs to. */
66
+ /** The HMAC key that turns a sessionKey into the store's partition key, so a table read does not reveal which subject a session belongs to. */
60
67
  sessionSalt: string;
61
68
  enableSlidingExpiration?: boolean;
62
69
  /** Min seconds between sliding-expiration writes. Default: max(60, 5% of TTL). */
@@ -69,13 +76,12 @@ export type LambderSessionManagerOptions<SessionData = any> = {
69
76
  * Whether a string has the shape a minted session token has:
70
77
  * `sessionKeyHash:secret`, both hex, neither longer than 1024 characters.
71
78
  *
72
- * It lives beside the code that mints and splits that format rather than
73
- * beside the cookie reader, so the ceiling is a property of the model and
74
- * anyone handing lookupSession a token they did not mint can ask the same
75
- * question. The session controller asks it of every candidate cookie before
76
- * any store read, so a malformed candidate is "no session" and never a read
77
- * error. Nothing a browser legitimately holds fails it, because the only
78
- * writer of these cookies is the code that mints them.
79
+ * It lives beside the code that mints and splits the format, not the cookie
80
+ * reader, so the ceiling belongs to the model and anyone handing
81
+ * lookupSession a token they did not mint can ask it too. The session
82
+ * controller checks every candidate cookie with it before any store read, so
83
+ * a malformed one is "no session", never a read error. No legitimate cookie
84
+ * fails it, since only the minting code writes them.
79
85
  */
80
86
  export declare const isMintedSessionToken: (token: string) => boolean;
81
87
  /**
@@ -111,21 +117,16 @@ export default class LambderSessionManager<SessionData = any> {
111
117
  */
112
118
  [LAMBDER_BACKEND_SWAP](store: LambderSessionStore<SessionData>): void;
113
119
  /**
114
- * The salted partition hash of a sessionKey: sha256 of the key followed
115
- * by the salt, with NO separator between them.
120
+ * The salted partition hash of a sessionKey: HMAC-SHA256 with the salt
121
+ * as the key and the sessionKey as the message.
116
122
  *
117
- * The missing separator is frozen by the wire guarantee, not chosen
118
- * again here: every live session in every deployed table was partitioned
119
- * under this exact string, so inserting a separator would relocate every
120
- * partition key at once and read to everyone signed in as being logged
121
- * out. What it costs is worth naming so nobody reintroduces it by
122
- * accident: without a separator the split between key and salt is not
123
- * recoverable from the string, so two deployments SHARING one table
124
- * collide when the difference between their salts can be absorbed into a
125
- * sessionKey ("ab" + "cd" and "a" + "bcd" hash alike). Two deployments
126
- * over one table must therefore not stand in a prefix relationship over
127
- * their salts. Separate tables, or salts that are independent random
128
- * strings, both rule it out.
123
+ * Keyed rather than hashed over the two run together, because a plain
124
+ * concatenation cannot tell where the sessionKey ends and the salt
125
+ * begins: "ab" + "cd" and "a" + "bcd" would hash alike, so two
126
+ * deployments sharing one table could collide whenever the difference
127
+ * between their salts fits into a sessionKey. With the salt as the HMAC
128
+ * key the two inputs never meet in one string, so no choice of salts
129
+ * lets one deployment's subject land in another's partition.
129
130
  */
130
131
  private sessionKeyHashOf;
131
132
  /**
@@ -136,68 +137,145 @@ export default class LambderSessionManager<SessionData = any> {
136
137
  */
137
138
  private hashToken;
138
139
  createSession(sessionKey: string, data?: SessionData, ttlInSeconds?: number, options?: {
139
- /** Carries an existing data freshness stamp over (used by regenerateSession). */
140
+ /** The data refresh deadline to start with (regenerateSession starts its data due). */
140
141
  dataExpiresAt?: number;
141
142
  }): Promise<LambderCreatedSession<SessionData>>;
142
- updateSessionData(session: LambderSessionRecord<SessionData>, newData: SessionData): Promise<LambderSessionRecord<SessionData>>;
143
+ /**
144
+ * Writes new session data onto the record, and nothing else: the expiry
145
+ * slides only in renewSession, which also re-issues the cookies, so a
146
+ * data write never moves the record's expiry away from the browser's.
147
+ * Returns the updated record, or null when the session was ended in
148
+ * between (the write does not bring it back).
149
+ *
150
+ * With dataRefresh, the write leaves dataExpiresAt where it is: only the
151
+ * dataRefresh callback's output is stamped fresh. The data an app writes
152
+ * is almost always the session's own with a field changed, still carrying
153
+ * whatever the callback derived last, so a write that pushed the deadline
154
+ * would let an app writing more often than ttlSeconds never refresh at
155
+ * all, and would cancel the due start regenerateSession gives a rotated
156
+ * session. The write is still conditioned on the dataVersion this record
157
+ * was read with. If that moved in between (expireSessionDataAllByKey
158
+ * marked the data stale, or another request refreshed it, possibly
159
+ * applying a revocation already), the data is written and marked due, so
160
+ * a revocation that landed during this request is not undone by data
161
+ * derived before it.
162
+ *
163
+ * The returned record carries the dataVersion this write produced only
164
+ * when a conditioned write applied, which is then exactly one past the
165
+ * version it named. Otherwise it keeps the version it was read with,
166
+ * which the store has already passed, so a later conditioned write from
167
+ * the same request answers "stale" and lands marked due: the safe side.
168
+ */
169
+ updateSessionData(session: LambderSessionRecord<SessionData>, newData: SessionData): Promise<LambderSessionRecord<SessionData> | null>;
143
170
  /**
144
171
  * The record a token names, read and structurally checked, with nothing
145
- * renewed. Split out of getSession so a caller weighing several candidate
146
- * cookies can decide which one is this visitor's BEFORE anything is
172
+ * renewed. Kept apart from renewSession so a caller weighing several
173
+ * candidate cookies can decide which is this visitor's BEFORE anything is
147
174
  * written on their behalf: renewing slides an expiry and may run the
148
175
  * app's dataRefresh callback, and a cookie a sibling host planted must
149
- * not get either from the victim's traffic.
176
+ * get neither from the victim's traffic.
150
177
  */
151
178
  lookupSession(sessionToken: string): Promise<LambderSessionRecord<SessionData> | null>;
152
179
  /**
153
180
  * The renewal half of a session read: the dataRefresh callback once its
154
181
  * shelf life has passed, and the sliding-expiration write. Returns null
155
- * when a refresh says the session is over (a deleted or disabled login),
156
- * which ends it the same way a missing record does.
182
+ * when the session is over: a refresh said so (a deleted or disabled
183
+ * login), or the record was deleted while this request read it (a
184
+ * logout, a password change), which a renewal must not undo.
157
185
  */
158
186
  renewSession(session: LambderSessionRecord<SessionData>): Promise<LambderSessionRecord<SessionData> | null>;
159
187
  /**
160
- * Runs the dataRefresh callback now, regardless of dataExpiresAt, and
161
- * persists the result onto the same record. Returns the updated session,
162
- * or null when the callback ended it (the record is deleted). Requires
163
- * dataRefresh to be configured.
188
+ * Runs the dataRefresh callback immediately, regardless of
189
+ * dataExpiresAt, and persists the result onto the same record, over the
190
+ * data it was computed from only (see LambderSessionDataRefreshConfig).
191
+ * Returns the refreshed session, or null when the callback ended it (the
192
+ * record is deleted) or the session is gone. The expiry does not slide
193
+ * here; renewSession slides it. Requires dataRefresh to be configured.
164
194
  */
165
195
  refreshSessionData(session: LambderSessionRecord<SessionData>): Promise<LambderSessionRecord<SessionData> | null>;
166
196
  /**
167
197
  * Checks a record against the session token presented with it: the
168
198
  * partition hash and the bearer secret the cookie carries, plus the
169
- * structural checks and the expiry. This is the half a route needs, and
170
- * the half lookupSession has already proved for a record it just found by
171
- * that token's own hash, so the read path does not ask it again.
199
+ * structural checks and the expiry. This is the half a route needs; a
200
+ * record lookupSession just found by that token's own hash has already
201
+ * passed it, so the read path does not ask again.
172
202
  */
173
203
  isSessionTokenValid(session: LambderSessionRecord<SessionData> | null, sessionToken: string | null): Promise<boolean>;
174
204
  /**
175
205
  * Checks a record against the CSRF token the request posted: the other
176
206
  * half, asked of an API call and not of a route. Separate methods rather
177
- * than one with a skip flag, because a boolean at the call site says
178
- * nothing about which half it turns off, and the two are asked in
179
- * different places for different reasons.
207
+ * than one with a skip flag, because a boolean at the call site does not
208
+ * say which half it turns off.
180
209
  */
181
210
  isSessionCsrfTokenValid(session: LambderSessionRecord<SessionData> | null, csrfToken: string | null): Promise<boolean>;
211
+ /** Deletes the session; false when there was none left to delete. */
182
212
  deleteSession(session: LambderSessionRecord<SessionData>): Promise<boolean>;
183
- /** Deletes every session that shares the record's subject: "log this subject out everywhere". */
213
+ /**
214
+ * Deletes every session that shares the record's subject: "log this
215
+ * subject out everywhere". False when a rotation racing it may have left
216
+ * a session behind (see deleteAllUnder).
217
+ */
184
218
  deleteSessionAll(session: LambderSessionRecord<SessionData>): Promise<boolean>;
185
219
  /**
186
220
  * Deletes every session created for the given sessionKey (e.g. a user
187
221
  * id): "log this subject out everywhere", without needing a fetched
188
- * session record.
222
+ * session record. False when a rotation racing it may have left a
223
+ * session behind (see deleteAllUnder).
189
224
  */
190
225
  deleteSessionAllByKey(sessionKey: string): Promise<boolean>;
226
+ /**
227
+ * Deletes every session of a subject, and answers whether it is sure
228
+ * none is left. A pass that finds a record it listed already gone lists
229
+ * again: a rotation writes its new record before deleting the old one
230
+ * (see regenerateSession), so the old one vanishing between this listing
231
+ * and this delete means a new record may have appeared after the
232
+ * listing. When the last pass the bound allows still finds one gone,
233
+ * such a record may be standing, so the call is logged and answers
234
+ * false: a caller ending a subject's sessions after a password change
235
+ * can run it again.
236
+ */
191
237
  private deleteAllUnder;
238
+ /**
239
+ * One write per session of a subject, a bounded number at a time: a
240
+ * subject with hundreds of sessions is not hundreds of round trips in a
241
+ * row, and not hundreds at once either.
242
+ */
243
+ private forEachSessionOf;
192
244
  /**
193
245
  * Marks the data of every session of the given sessionKey stale, so each
194
246
  * renews via dataRefresh on its next read: "this subject's roles or
195
- * permissions changed, apply it now", without logging the subject out
196
- * (deleteSessionAllByKey) and without waiting for the data TTL. Stamps
247
+ * permissions changed, apply that right away", without logging them out
248
+ * (deleteSessionAllByKey) and without waiting for the data TTL. Writes
197
249
  * dataExpiresAt only, on records that still exist, so it neither
198
250
  * resurrects a session deleted in between nor overwrites a concurrent
199
- * write. Requires dataRefresh to be configured.
251
+ * write. The write moves each record's dataVersion, even when the
252
+ * deadline already reads this second, so a refresh or data write in
253
+ * flight, conditioned on the version it read, answers "stale" rather
254
+ * than landing data derived before the change. Requires dataRefresh to
255
+ * be configured.
200
256
  */
201
257
  expireSessionDataAllByKey(sessionKey: string): Promise<boolean>;
202
- regenerateSession(session: LambderSessionRecord<SessionData>): Promise<LambderCreatedSession<SessionData>>;
258
+ /**
259
+ * Replaces the session with a new one under new tokens, carrying over
260
+ * the data as the delete removed it rather than as this request read
261
+ * it, so data another request wrote meanwhile stays. Returns null, and
262
+ * leaves no new session behind, when the record was already gone or
263
+ * over: a logout, "log out everywhere" or a password change that landed
264
+ * during this request stays in force.
265
+ *
266
+ * The new record is written before the old one is deleted. The other
267
+ * way round, a subject-wide delete that listed the subject's sessions
268
+ * between the two would find neither, and the new session would outlive
269
+ * the password change it was racing. Written first, the new record is in
270
+ * that listing, or the old one still is: then either the subject-wide
271
+ * delete removes the old one before this delete does, and this delete
272
+ * takes the new one back out, or this delete removes it first, and the
273
+ * subject-wide delete, finding it gone, lists again (deleteAllUnder).
274
+ *
275
+ * With dataRefresh, the new session's data is due at once: a revocation
276
+ * marked by expireSessionDataAllByKey while the new record was being
277
+ * written may have passed it by, and due, its next read renews the data
278
+ * from the source of truth either way.
279
+ */
280
+ regenerateSession(session: LambderSessionRecord<SessionData>): Promise<LambderCreatedSession<SessionData> | null>;
203
281
  }