lambder 6.0.2 → 7.0.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.
- package/CHANGELOG.md +2316 -0
- package/README.md +60 -33
- package/dist/api/LambderApiAnswer.d.ts +40 -0
- package/dist/api/LambderApiAnswer.js +19 -0
- package/dist/api/LambderApiCallContext.d.ts +38 -0
- package/dist/api/LambderApiCallContext.js +13 -0
- package/dist/api/LambderApiDefinition.d.ts +18 -0
- package/dist/api/LambderApiDefinition.js +1 -0
- package/dist/api/LambderApiEnvelope.d.ts +67 -0
- package/dist/api/LambderApiEnvelope.js +180 -0
- package/dist/api/LambderApiGuards.d.ts +302 -0
- package/dist/api/LambderApiGuards.js +134 -0
- package/dist/api/LambderApiIdempotency.d.ts +122 -0
- package/dist/api/LambderApiIdempotency.js +330 -0
- package/dist/api/LambderApiPipeline.d.ts +134 -0
- package/dist/api/LambderApiPipeline.js +221 -0
- package/dist/api/LambderApiPolicyEngine.d.ts +36 -0
- package/dist/api/LambderApiPolicyEngine.js +77 -0
- package/dist/api/LambderApiRateLimits.d.ts +206 -0
- package/dist/api/LambderApiRateLimits.js +239 -0
- package/dist/api/LambderApiRequest.d.ts +101 -0
- package/dist/api/LambderApiRequest.js +129 -0
- package/dist/api/LambderApiValidationRefusal.d.ts +32 -0
- package/dist/api/LambderApiValidationRefusal.js +40 -0
- package/dist/client/LambderCaller.d.ts +62 -55
- package/dist/client/LambderCaller.js +147 -90
- package/dist/client/lambderFetchTransport.d.ts +9 -0
- package/dist/client/lambderFetchTransport.js +71 -0
- package/dist/client.d.ts +20 -10
- package/dist/client.js +11 -5
- package/dist/core/Lambder.d.ts +117 -253
- package/dist/core/Lambder.js +374 -341
- package/dist/core/LambderContext.d.ts +54 -44
- package/dist/core/LambderContext.js +41 -110
- package/dist/core/LambderCreateOptions.d.ts +285 -0
- package/dist/core/LambderCreateOptions.js +44 -0
- package/dist/core/LambderFiles.d.ts +1 -45
- package/dist/core/LambderFiles.js +18 -38
- package/dist/core/LambderIndexHtml.d.ts +37 -0
- package/dist/core/LambderIndexHtml.js +87 -0
- package/dist/core/LambderPolicyBuilders.d.ts +17 -0
- package/dist/core/LambderPolicyBuilders.js +16 -0
- package/dist/core/LambderPublicFiles.d.ts +5 -2
- package/dist/core/LambderPublicFiles.js +7 -2
- package/dist/core/LambderResolver.d.ts +8 -6
- package/dist/core/LambderResponse.d.ts +29 -11
- package/dist/core/LambderResponse.js +96 -49
- package/dist/core/LambderResponseBuilder.d.ts +18 -14
- package/dist/core/LambderResponseBuilder.js +19 -25
- package/dist/core/LambderRouting.d.ts +18 -7
- package/dist/core/LambderRouting.js +17 -7
- package/dist/core/LambderTemplatingEngine.d.ts +0 -62
- package/dist/core/LambderTemplatingEngine.js +7 -3
- package/dist/index.d.ts +85 -32
- package/dist/index.js +44 -16
- package/dist/invoke/LambderInvokeCaller.d.ts +46 -139
- package/dist/invoke/LambderInvokeCaller.js +140 -335
- package/dist/invoke/LambderInvokeOutcome.d.ts +165 -0
- package/dist/invoke/LambderInvokeOutcome.js +129 -0
- package/dist/invoke/LambderLambdaEvent.d.ts +81 -0
- package/dist/invoke/LambderLambdaEvent.js +187 -0
- package/dist/invoke/lambderHandlerTransport.d.ts +36 -0
- package/dist/invoke/lambderHandlerTransport.js +89 -0
- package/dist/mock/LambderMockApp.d.ts +352 -0
- package/dist/mock/LambderMockApp.js +815 -0
- package/dist/mock/LambderMockBrowserCookies.d.ts +55 -0
- package/dist/mock/LambderMockBrowserCookies.js +76 -0
- package/dist/mock/LambderMockCallRecorder.d.ts +85 -0
- package/dist/mock/LambderMockCallRecorder.js +183 -0
- package/dist/mock/LambderMockCreateOptions.d.ts +161 -0
- package/dist/mock/LambderMockCreateOptions.js +9 -0
- package/dist/mock/LambderMockEntryRegistry.d.ts +52 -0
- package/dist/mock/LambderMockEntryRegistry.js +126 -0
- package/dist/mock/LambderMockFailureInjector.d.ts +60 -0
- package/dist/mock/LambderMockFailureInjector.js +138 -0
- package/dist/mock/LambderMockTypes.d.ts +421 -0
- package/dist/mock/LambderMockTypes.js +8 -0
- package/dist/mock/lambderMockConsoleLogger.d.ts +16 -0
- package/dist/mock/lambderMockConsoleLogger.js +35 -0
- package/dist/mock/lambderMockInvokeTransport.d.ts +50 -0
- package/dist/mock/lambderMockInvokeTransport.js +52 -0
- package/dist/mock/lambderMockMswHandler.d.ts +99 -0
- package/dist/mock/lambderMockMswHandler.js +126 -0
- package/dist/mock.d.ts +34 -0
- package/dist/mock.js +27 -0
- package/dist/session/LambderSessionController.d.ts +199 -30
- package/dist/session/LambderSessionController.js +396 -82
- package/dist/session/LambderSessionCrypto.d.ts +66 -0
- package/dist/session/LambderSessionCrypto.js +101 -0
- package/dist/session/LambderSessionManager.d.ts +118 -80
- package/dist/session/LambderSessionManager.js +212 -184
- package/dist/shared/LambderI18n.d.ts +6 -6
- package/dist/shared/LambderI18n.js +1 -1
- package/dist/shared/contracts/LambderFileSource.d.ts +33 -0
- package/dist/shared/contracts/LambderFileSource.js +19 -0
- package/dist/shared/contracts/LambderIdempotencyStore.d.ts +66 -0
- package/dist/shared/contracts/LambderIdempotencyStore.js +12 -0
- package/dist/shared/contracts/LambderRateLimiter.d.ts +71 -0
- package/dist/shared/contracts/LambderRateLimiter.js +24 -0
- package/dist/shared/contracts/LambderSessionStore.d.ts +72 -0
- package/dist/shared/contracts/LambderSessionStore.js +13 -0
- package/dist/shared/transport/LambderApiTransport.d.ts +139 -0
- package/dist/shared/transport/LambderApiTransport.js +65 -0
- package/dist/shared/transport/LambderCookieJar.d.ts +121 -0
- package/dist/shared/transport/LambderCookieJar.js +246 -0
- package/dist/shared/transport/lambderCookieJarTransport.d.ts +30 -0
- package/dist/shared/transport/lambderCookieJarTransport.js +60 -0
- package/dist/shared/util/LambderBase64.d.ts +10 -0
- package/dist/shared/util/LambderBase64.js +27 -0
- package/dist/shared/util/LambderCallAbort.d.ts +62 -0
- package/dist/shared/util/LambderCallAbort.js +80 -0
- package/dist/shared/util/LambderClientIp.d.ts +32 -0
- package/dist/shared/util/LambderClientIp.js +56 -0
- package/dist/shared/util/LambderExpiringMap.d.ts +119 -0
- package/dist/shared/util/LambderExpiringMap.js +217 -0
- package/dist/shared/util/LambderKeyFields.d.ts +32 -0
- package/dist/shared/util/LambderKeyFields.js +34 -0
- package/dist/shared/util/LambderNodeModules.d.ts +9 -0
- package/dist/shared/util/LambderNodeModules.js +39 -0
- package/dist/shared/util/LambderOptionChecks.d.ts +17 -0
- package/dist/shared/util/LambderOptionChecks.js +33 -0
- package/dist/shared/util/LambderResponseBrand.d.ts +20 -0
- package/dist/shared/util/LambderResponseBrand.js +18 -0
- package/dist/shared/util/LambderTextDigest.d.ts +17 -0
- package/dist/shared/util/LambderTextDigest.js +34 -0
- package/dist/shared/util/LambderTypeUtilities.d.ts +33 -0
- package/dist/shared/util/LambderTypeUtilities.js +8 -0
- package/dist/shared/wire/LambderAnswerHeaders.d.ts +60 -0
- package/dist/shared/wire/LambderAnswerHeaders.js +94 -0
- package/dist/shared/wire/LambderApiContract.d.ts +129 -0
- package/dist/shared/wire/LambderApiOptionValues.d.ts +39 -0
- package/dist/shared/wire/LambderApiOptionValues.js +11 -0
- package/dist/shared/wire/LambderApiOutcome.d.ts +128 -0
- package/dist/shared/{LambderApiOutcome.js → wire/LambderApiOutcome.js} +16 -9
- package/dist/shared/{LambderApiError.d.ts → wire/LambderApiRefusal.d.ts} +48 -26
- package/dist/shared/{LambderApiError.js → wire/LambderApiRefusal.js} +13 -11
- package/dist/shared/wire/LambderCallOptions.d.ts +171 -0
- package/dist/shared/wire/LambderCallOptions.js +17 -0
- package/dist/shared/{LambderCompressionCodec.d.ts → wire/LambderCompressionCodec.d.ts} +10 -6
- package/dist/shared/{LambderCompressionCodec.js → wire/LambderCompressionCodec.js} +67 -23
- package/dist/shared/{LambderCompressionOption.d.ts → wire/LambderCompressionOption.d.ts} +1 -1
- package/dist/shared/{LambderCompressionOption.js → wire/LambderCompressionOption.js} +3 -4
- package/dist/shared/{LambderCrashDetail.d.ts → wire/LambderCrashDetail.d.ts} +10 -0
- package/dist/shared/{LambderCrashDetail.js → wire/LambderCrashDetail.js} +30 -0
- package/dist/shared/wire/LambderHttpStatus.d.ts +12 -0
- package/dist/shared/wire/LambderHttpStatus.js +1 -0
- package/dist/shared/{LambderRequestPayload.d.ts → wire/LambderRequestPayload.d.ts} +25 -17
- package/dist/shared/{LambderRequestPayload.js → wire/LambderRequestPayload.js} +29 -52
- package/dist/shared/wire/LambderSessionCookieNames.d.ts +9 -0
- package/dist/shared/wire/LambderSessionCookieNames.js +9 -0
- package/dist/stores/LambderDdbCache.d.ts +12 -9
- package/dist/stores/LambderDdbCache.js +56 -47
- package/dist/stores/{LambderDdbIdempotency.d.ts → LambderDdbIdempotencyStore.d.ts} +41 -31
- package/dist/stores/LambderDdbIdempotencyStore.js +319 -0
- package/dist/stores/LambderDdbRateLimiter.d.ts +30 -49
- package/dist/stores/LambderDdbRateLimiter.js +47 -45
- package/dist/stores/LambderDdbSdk.d.ts +83 -6
- package/dist/stores/LambderDdbSdk.js +83 -2
- package/dist/stores/LambderDdbSessionStore.d.ts +65 -0
- package/dist/stores/LambderDdbSessionStore.js +161 -0
- package/dist/stores/LambderHttpFileSource.d.ts +1 -1
- package/dist/stores/LambderHttpFileSource.js +10 -1
- package/dist/stores/LambderLocalFileSource.d.ts +15 -0
- package/dist/stores/LambderLocalFileSource.js +28 -0
- package/dist/stores/LambderMemoryIdempotencyStore.d.ts +63 -0
- package/dist/stores/LambderMemoryIdempotencyStore.js +113 -0
- package/dist/stores/LambderMemoryRateLimiter.d.ts +34 -0
- package/dist/stores/LambderMemoryRateLimiter.js +64 -0
- package/dist/stores/LambderMemorySessionStore.d.ts +48 -0
- package/dist/stores/LambderMemorySessionStore.js +74 -0
- package/dist/stores/LambderS3FileSource.d.ts +1 -1
- package/dist/stores/LambderS3FileSource.js +1 -1
- package/package.json +21 -19
- package/dist/client/LambderMSW.d.ts +0 -69
- package/dist/client/LambderMSW.js +0 -121
- package/dist/policies/LambderApiGuards.d.ts +0 -256
- package/dist/policies/LambderApiGuards.js +0 -94
- package/dist/policies/LambderApiIdempotency.d.ts +0 -58
- package/dist/policies/LambderApiIdempotency.js +0 -219
- package/dist/policies/LambderApiPolicies.d.ts +0 -42
- package/dist/policies/LambderApiPolicies.js +0 -52
- package/dist/policies/LambderApiRateLimits.d.ts +0 -132
- package/dist/policies/LambderApiRateLimits.js +0 -119
- package/dist/shared/LambderApiContract.d.ts +0 -57
- package/dist/shared/LambderApiOutcome.d.ts +0 -69
- package/dist/shared/LambderCallOptions.d.ts +0 -71
- package/dist/shared/LambderCallOptions.js +0 -16
- package/dist/shared/node-polyfills.d.ts +0 -4
- package/dist/shared/node-polyfills.js +0 -58
- package/dist/stores/LambderDdbIdempotency.js +0 -229
- package/dist/testing.d.ts +0 -9
- package/dist/testing.js +0 -8
- /package/dist/shared/{LambderApiContract.js → wire/LambderApiContract.js} +0 -0
- /package/dist/{core → shared/wire}/LambderCookie.d.ts +0 -0
- /package/dist/{core → shared/wire}/LambderCookie.js +0 -0
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The cryptography the session model runs on, behind an interface so the
|
|
3
|
+
* manager itself has no Node dependency: the bearer secrets are hashed at
|
|
4
|
+
* rest, compared in constant time, and minted from a cryptographic random
|
|
5
|
+
* source.
|
|
6
|
+
*
|
|
7
|
+
* LambderWebCrypto is the default and runs on Node 20+, every browser on a
|
|
8
|
+
* secure context, and edge runtimes. LambderPlainSessionCrypto is the
|
|
9
|
+
* stand-in the mock runtime picks on its own where crypto.subtle is missing
|
|
10
|
+
* (a plain-http page during device testing on a LAN): its memory store is
|
|
11
|
+
* not a table anybody can leak, so hashing there protects nothing.
|
|
12
|
+
*/
|
|
13
|
+
import { getCrypto } from "../shared/util/LambderNodeModules.js";
|
|
14
|
+
import { bytesToHexString, resolveWebCrypto, sha256HexOf } from "../shared/util/LambderTextDigest.js";
|
|
15
|
+
/** Length-aware, timing-neutral string comparison: no early exit on the first differing character. */
|
|
16
|
+
const constantTimeEqual = (a, b) => {
|
|
17
|
+
if (a.length !== b.length)
|
|
18
|
+
return false;
|
|
19
|
+
let difference = 0;
|
|
20
|
+
for (let i = 0; i < a.length; i += 1)
|
|
21
|
+
difference |= a.charCodeAt(i) ^ b.charCodeAt(i);
|
|
22
|
+
return difference === 0;
|
|
23
|
+
};
|
|
24
|
+
/** True when this runtime offers WebCrypto's subtle API (secure contexts in browsers; Node 20+). */
|
|
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. */
|
|
27
|
+
export class LambderWebCrypto {
|
|
28
|
+
isCryptographic = true;
|
|
29
|
+
cryptoPromise;
|
|
30
|
+
/**
|
|
31
|
+
* Node's crypto where the runtime has it, kept for timingSafeEqual.
|
|
32
|
+
* Warmed by ready(), because the comparison itself is synchronous and a
|
|
33
|
+
* session is always hashed before anything is compared against it.
|
|
34
|
+
*/
|
|
35
|
+
nodeCrypto = null;
|
|
36
|
+
/**
|
|
37
|
+
* The runtime's WebCrypto, through the resolver every layer shares, with
|
|
38
|
+
* Node's crypto warmed alongside it.
|
|
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.
|
|
45
|
+
*/
|
|
46
|
+
ready() {
|
|
47
|
+
this.cryptoPromise ??= (async () => {
|
|
48
|
+
const nodeCrypto = await getCrypto();
|
|
49
|
+
this.nodeCrypto = nodeCrypto;
|
|
50
|
+
const nodeWebCrypto = nodeCrypto?.webcrypto;
|
|
51
|
+
if (!isWebCryptoAvailable() && !nodeWebCrypto?.subtle) {
|
|
52
|
+
throw new Error("Lambder sessions need WebCrypto (crypto.subtle). In a browser that means a secure context (https or localhost); pass `crypto: new LambderPlainSessionCrypto()` where none is available and the store holds nothing worth hashing.");
|
|
53
|
+
}
|
|
54
|
+
return await resolveWebCrypto();
|
|
55
|
+
})();
|
|
56
|
+
return this.cryptoPromise;
|
|
57
|
+
}
|
|
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.
|
|
61
|
+
await this.ready();
|
|
62
|
+
return await sha256HexOf(value);
|
|
63
|
+
}
|
|
64
|
+
async randomHex(bytes) {
|
|
65
|
+
const webCrypto = await this.ready();
|
|
66
|
+
return bytesToHexString(webCrypto.getRandomValues(new Uint8Array(bytes)));
|
|
67
|
+
}
|
|
68
|
+
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.
|
|
72
|
+
const nodeCrypto = this.nodeCrypto;
|
|
73
|
+
if (nodeCrypto && a.length === b.length) {
|
|
74
|
+
const left = Buffer.from(a, "utf8");
|
|
75
|
+
const right = Buffer.from(b, "utf8");
|
|
76
|
+
if (left.length === right.length)
|
|
77
|
+
return nodeCrypto.timingSafeEqual(left, right);
|
|
78
|
+
}
|
|
79
|
+
return constantTimeEqual(a, b);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* No hashing and no cryptographic randomness: values are hex-encoded as
|
|
84
|
+
* they are and secrets come from Math.random. Only for an in-memory store
|
|
85
|
+
* in a runtime without WebCrypto; never for anything at rest.
|
|
86
|
+
*/
|
|
87
|
+
export class LambderPlainSessionCrypto {
|
|
88
|
+
isCryptographic = false;
|
|
89
|
+
async sha256Hex(value) {
|
|
90
|
+
return bytesToHexString(new TextEncoder().encode(value));
|
|
91
|
+
}
|
|
92
|
+
async randomHex(bytes) {
|
|
93
|
+
const random = new Uint8Array(bytes);
|
|
94
|
+
for (let i = 0; i < bytes; i += 1)
|
|
95
|
+
random[i] = Math.floor(Math.random() * 256);
|
|
96
|
+
return bytesToHexString(random);
|
|
97
|
+
}
|
|
98
|
+
constantTimeEqual(a, b) {
|
|
99
|
+
return constantTimeEqual(a, b);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
@@ -1,34 +1,13 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
[x: string]: any;
|
|
4
|
-
/**
|
|
5
|
-
* sha256 of the raw CSRF token. The raw bearer secrets are never stored:
|
|
6
|
-
* they exist only in the client's cookies (and, at creation, on the
|
|
7
|
-
* LambderCreatedSession result), so a read of the session table yields
|
|
8
|
-
* no usable credentials.
|
|
9
|
-
*/
|
|
10
|
-
csrfTokenHash: string;
|
|
11
|
-
sessionKey: string;
|
|
12
|
-
data: SessionData;
|
|
13
|
-
createdAt: number;
|
|
14
|
-
expiresAt: number;
|
|
15
|
-
lastAccessedAt: number;
|
|
16
|
-
ttlInSeconds: number;
|
|
17
|
-
/**
|
|
18
|
-
* When `data` must be renewed via the dataRefresh callback (epoch seconds).
|
|
19
|
-
* Only present when dataRefresh is configured; independent of the
|
|
20
|
-
* session's own expiresAt.
|
|
21
|
-
*/
|
|
22
|
-
dataExpiresAt?: number;
|
|
23
|
-
};
|
|
1
|
+
import type { LambderSessionCrypto } from "./LambderSessionCrypto.js";
|
|
2
|
+
import type { LambderSessionRecord, LambderSessionStore } from "../shared/contracts/LambderSessionStore.js";
|
|
24
3
|
/**
|
|
25
4
|
* A freshly created (or regenerated) session: the persisted record plus the
|
|
26
5
|
* RAW cookie secrets, which exist only here and in the cookies the caller
|
|
27
6
|
* sets. At rest the record carries hashes of both.
|
|
28
7
|
*/
|
|
29
8
|
export type LambderCreatedSession<SessionData = any> = {
|
|
30
|
-
session:
|
|
31
|
-
/** Raw bearer token for the session cookie (`
|
|
9
|
+
session: LambderSessionRecord<SessionData>;
|
|
10
|
+
/** Raw bearer token for the session cookie (`sessionKeyHash:secret`). */
|
|
32
11
|
sessionToken: string;
|
|
33
12
|
/** Raw CSRF token for the client-readable csrf cookie. */
|
|
34
13
|
csrfToken: string;
|
|
@@ -39,7 +18,7 @@ export type LambderCreatedSession<SessionData = any> = {
|
|
|
39
18
|
* checks dataExpiresAt and calls `refresh` past it, persisting the result
|
|
40
19
|
* onto the same session record: same tokens, same cookies, the session
|
|
41
20
|
* itself is untouched. The refresh write and the sliding-expiration write
|
|
42
|
-
* share a single
|
|
21
|
+
* share a single store write when both are due.
|
|
43
22
|
*/
|
|
44
23
|
export type LambderSessionDataRefreshConfig<SessionData = any> = {
|
|
45
24
|
/** Seconds session.data stays valid before refresh() runs on read. */
|
|
@@ -52,7 +31,7 @@ export type LambderSessionDataRefreshConfig<SessionData = any> = {
|
|
|
52
31
|
* LambderSessionDataRefreshError and leave the session untouched; catch
|
|
53
32
|
* inside and return session.data to explicitly serve stale instead.
|
|
54
33
|
*/
|
|
55
|
-
refresh: (session:
|
|
34
|
+
refresh: (session: LambderSessionRecord<SessionData>) => Promise<SessionData | null>;
|
|
56
35
|
};
|
|
57
36
|
/**
|
|
58
37
|
* Wraps errors thrown by the dataRefresh callback so they stay
|
|
@@ -64,91 +43,150 @@ export declare class LambderSessionDataRefreshError extends Error {
|
|
|
64
43
|
constructor(cause: unknown);
|
|
65
44
|
}
|
|
66
45
|
/**
|
|
67
|
-
* Wraps
|
|
46
|
+
* Wraps store failures during a session read so they stay distinguishable
|
|
68
47
|
* from "no session": fetchSessionIfExists() swallows missing or invalid
|
|
69
|
-
* sessions but rethrows this. Without the distinction a transient
|
|
48
|
+
* sessions but rethrows this. Without the distinction a transient store
|
|
70
49
|
* error would answer sessionExpired, and the caller would then clear the
|
|
71
50
|
* client's session cookies: an infra blip forcing a real logout.
|
|
72
51
|
*/
|
|
73
52
|
export declare class LambderSessionReadError extends Error {
|
|
74
53
|
constructor(cause: unknown);
|
|
75
54
|
}
|
|
76
|
-
export
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
/**
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
55
|
+
export type LambderSessionManagerOptions<SessionData = any> = {
|
|
56
|
+
/** Where sessions rest: LambderDdbSessionStore, LambderMemorySessionStore, or your own. */
|
|
57
|
+
store: LambderSessionStore<SessionData>;
|
|
58
|
+
/** Salts the sessionKey hash that partitions the store, so a table read does not reveal which subject a session belongs to. */
|
|
59
|
+
sessionSalt: string;
|
|
60
|
+
enableSlidingExpiration?: boolean;
|
|
61
|
+
/** Min seconds between sliding-expiration writes. Default: max(60, 5% of TTL). */
|
|
62
|
+
slidingWriteIntervalSeconds?: number;
|
|
63
|
+
dataRefresh?: LambderSessionDataRefreshConfig<SessionData>;
|
|
64
|
+
/** Hashing and randomness. Default: WebCrypto. */
|
|
65
|
+
crypto?: LambderSessionCrypto;
|
|
66
|
+
};
|
|
67
|
+
/**
|
|
68
|
+
* Whether a string has the shape a minted session token has:
|
|
69
|
+
* `sessionKeyHash:secret`, both hex, neither longer than 1024 characters.
|
|
70
|
+
*
|
|
71
|
+
* It lives beside the code that mints and splits that format rather than
|
|
72
|
+
* beside the cookie reader, so the ceiling is a property of the model and
|
|
73
|
+
* anyone handing lookupSession a token they did not mint can ask the same
|
|
74
|
+
* question. The session controller asks it of every candidate cookie before
|
|
75
|
+
* any store read, so a malformed candidate is "no session" and never a read
|
|
76
|
+
* error. Nothing a browser legitimately holds fails it, because the only
|
|
77
|
+
* writer of these cookies is the code that mints them.
|
|
78
|
+
*/
|
|
79
|
+
export declare const isMintedSessionToken: (token: string) => boolean;
|
|
80
|
+
/**
|
|
81
|
+
* The session model: how a session is minted, what its tokens look like,
|
|
82
|
+
* how it is found from a presented token, when it expires, when its data is
|
|
83
|
+
* renewed, and how it is rotated or ended. Storage and cryptography are
|
|
84
|
+
* injected, so the same class runs on Lambda over DynamoDB, in a test over
|
|
85
|
+
* a Map, and in a browser under the mock runtime.
|
|
86
|
+
*
|
|
87
|
+
* Token format: the session cookie carries `sessionKeyHash:secret`, where
|
|
88
|
+
* the hash locates the partition and only the secret's hash is stored as
|
|
89
|
+
* the sort key, so the lookup itself proves possession of the raw secret
|
|
90
|
+
* and a store read yields no usable token. The CSRF token is a second
|
|
91
|
+
* random value the client sends back in the request body; its hash is
|
|
92
|
+
* stored too.
|
|
93
|
+
*/
|
|
94
|
+
export default class LambderSessionManager<SessionData = any> {
|
|
95
|
+
private readonly store;
|
|
96
|
+
private readonly sessionSalt;
|
|
97
|
+
private readonly enableSlidingExpiration;
|
|
98
|
+
private readonly slidingWriteIntervalSeconds;
|
|
99
|
+
private readonly dataRefresh;
|
|
100
|
+
private readonly crypto;
|
|
101
|
+
constructor({ store, sessionSalt, enableSlidingExpiration, slidingWriteIntervalSeconds, dataRefresh, crypto, }: LambderSessionManagerOptions<SessionData>);
|
|
102
|
+
/**
|
|
103
|
+
* The salted partition hash of a sessionKey: sha256 of the key followed
|
|
104
|
+
* by the salt, with NO separator between them.
|
|
105
|
+
*
|
|
106
|
+
* The missing separator is frozen by the wire guarantee, not chosen
|
|
107
|
+
* again here: every live session in every deployed table was partitioned
|
|
108
|
+
* under this exact string, so inserting a separator would relocate every
|
|
109
|
+
* partition key at once and read to everyone signed in as being logged
|
|
110
|
+
* out. What it costs is worth naming so nobody reintroduces it by
|
|
111
|
+
* accident: without a separator the split between key and salt is not
|
|
112
|
+
* recoverable from the string, so two deployments SHARING one table
|
|
113
|
+
* collide when the difference between their salts can be absorbed into a
|
|
114
|
+
* sessionKey ("ab" + "cd" and "a" + "bcd" hash alike). Two deployments
|
|
115
|
+
* over one table must therefore not stand in a prefix relationship over
|
|
116
|
+
* their salts. Separate tables, or salts that are independent random
|
|
117
|
+
* strings, both rule it out.
|
|
118
|
+
*/
|
|
119
|
+
private sessionKeyHashOf;
|
|
102
120
|
/**
|
|
103
121
|
* At-rest hash for the bearer secrets (session sort-key secret, CSRF
|
|
104
122
|
* token). Fast unsalted sha256 is the right construction here: the
|
|
105
123
|
* inputs are 256-bit random values, so there is nothing to brute-force;
|
|
106
|
-
* hashing just ensures a leaked
|
|
124
|
+
* hashing just ensures a leaked store read yields no usable cookies.
|
|
107
125
|
*/
|
|
108
126
|
private hashToken;
|
|
109
|
-
|
|
110
|
-
private ddbGetItem;
|
|
111
|
-
/**
|
|
112
|
-
* Persists a session record. With compression on, `data` is stored as
|
|
113
|
-
* Brotli bytes (`dataBr`) beside its JSON byte length (`dataBytes`)
|
|
114
|
-
* once the JSON reaches minBytes; otherwise it stays a plain attribute.
|
|
115
|
-
*/
|
|
116
|
-
private ddbPutItem;
|
|
117
|
-
private ddbDeleteItem;
|
|
118
|
-
/** Sort keys of every session under a partition (the callers only need the keys). */
|
|
119
|
-
private ddbQueryAllByPartitionKey;
|
|
120
|
-
private ddbDeleteAllByPartitionKey;
|
|
121
|
-
createSession(sessionKey: string, data?: any, ttlInSeconds?: number, options?: {
|
|
127
|
+
createSession(sessionKey: string, data?: SessionData, ttlInSeconds?: number, options?: {
|
|
122
128
|
/** Carries an existing data freshness stamp over (used by regenerateSession). */
|
|
123
129
|
dataExpiresAt?: number;
|
|
124
|
-
}): Promise<LambderCreatedSession
|
|
125
|
-
updateSessionData(session:
|
|
126
|
-
|
|
130
|
+
}): Promise<LambderCreatedSession<SessionData>>;
|
|
131
|
+
updateSessionData(session: LambderSessionRecord<SessionData>, newData: SessionData): Promise<LambderSessionRecord<SessionData>>;
|
|
132
|
+
/**
|
|
133
|
+
* The record a token names, read and structurally checked, with nothing
|
|
134
|
+
* renewed. Split out of getSession so a caller weighing several candidate
|
|
135
|
+
* cookies can decide which one is this visitor's BEFORE anything is
|
|
136
|
+
* written on their behalf: renewing slides an expiry and may run the
|
|
137
|
+
* app's dataRefresh callback, and a cookie a sibling host planted must
|
|
138
|
+
* not get either from the victim's traffic.
|
|
139
|
+
*/
|
|
140
|
+
lookupSession(sessionToken: string): Promise<LambderSessionRecord<SessionData> | null>;
|
|
141
|
+
/**
|
|
142
|
+
* The renewal half of a session read: the dataRefresh callback once its
|
|
143
|
+
* shelf life has passed, and the sliding-expiration write. Returns null
|
|
144
|
+
* when a refresh says the session is over (a deleted or disabled login),
|
|
145
|
+
* which ends it the same way a missing record does.
|
|
146
|
+
*/
|
|
147
|
+
renewSession(session: LambderSessionRecord<SessionData>): Promise<LambderSessionRecord<SessionData> | null>;
|
|
127
148
|
/**
|
|
128
149
|
* Runs the dataRefresh callback now, regardless of dataExpiresAt, and
|
|
129
150
|
* persists the result onto the same record. Returns the updated session,
|
|
130
151
|
* or null when the callback ended it (the record is deleted). Requires
|
|
131
152
|
* dataRefresh to be configured.
|
|
132
153
|
*/
|
|
133
|
-
refreshSessionData(session:
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
154
|
+
refreshSessionData(session: LambderSessionRecord<SessionData>): Promise<LambderSessionRecord<SessionData> | null>;
|
|
155
|
+
/**
|
|
156
|
+
* Checks a record against the session token presented with it: the
|
|
157
|
+
* partition hash and the bearer secret the cookie carries, plus the
|
|
158
|
+
* structural checks and the expiry. This is the half a route needs, and
|
|
159
|
+
* the half lookupSession has already proved for a record it just found by
|
|
160
|
+
* that token's own hash, so the read path does not ask it again.
|
|
161
|
+
*/
|
|
162
|
+
isSessionTokenValid(session: LambderSessionRecord<SessionData> | null, sessionToken: string | null): Promise<boolean>;
|
|
163
|
+
/**
|
|
164
|
+
* Checks a record against the CSRF token the request posted: the other
|
|
165
|
+
* half, asked of an API call and not of a route. Separate methods rather
|
|
166
|
+
* than one with a skip flag, because a boolean at the call site says
|
|
167
|
+
* nothing about which half it turns off, and the two are asked in
|
|
168
|
+
* different places for different reasons.
|
|
169
|
+
*/
|
|
170
|
+
isSessionCsrfTokenValid(session: LambderSessionRecord<SessionData> | null, csrfToken: string | null): Promise<boolean>;
|
|
171
|
+
deleteSession(session: LambderSessionRecord<SessionData>): Promise<boolean>;
|
|
172
|
+
/** Deletes every session that shares the record's subject: "log this subject out everywhere". */
|
|
173
|
+
deleteSessionAll(session: LambderSessionRecord<SessionData>): Promise<boolean>;
|
|
137
174
|
/**
|
|
138
175
|
* Deletes every session created for the given sessionKey (e.g. a user
|
|
139
176
|
* id): "log this subject out everywhere", without needing a fetched
|
|
140
177
|
* session record.
|
|
141
178
|
*/
|
|
142
179
|
deleteSessionAllByKey(sessionKey: string): Promise<boolean>;
|
|
180
|
+
private deleteAllUnder;
|
|
143
181
|
/**
|
|
144
182
|
* Marks the data of every session of the given sessionKey stale, so each
|
|
145
183
|
* renews via dataRefresh on its next read: "this subject's roles or
|
|
146
184
|
* permissions changed, apply it now", without logging the subject out
|
|
147
185
|
* (deleteSessionAllByKey) and without waiting for the data TTL. Stamps
|
|
148
|
-
* dataExpiresAt only,
|
|
149
|
-
*
|
|
150
|
-
*
|
|
186
|
+
* dataExpiresAt only, on records that still exist, so it neither
|
|
187
|
+
* resurrects a session deleted in between nor overwrites a concurrent
|
|
188
|
+
* write. Requires dataRefresh to be configured.
|
|
151
189
|
*/
|
|
152
190
|
expireSessionDataAllByKey(sessionKey: string): Promise<boolean>;
|
|
153
|
-
regenerateSession(session:
|
|
191
|
+
regenerateSession(session: LambderSessionRecord<SessionData>): Promise<LambderCreatedSession<SessionData>>;
|
|
154
192
|
}
|