lambder 6.0.2 → 7.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.
- 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
|
@@ -1,99 +1,421 @@
|
|
|
1
|
-
import { resolveCookieDomain, serializeCookie, serializeClearCookie } from "../
|
|
2
|
-
import {
|
|
1
|
+
import { resolveCookieDomain, serializeCookie, serializeClearCookie } from "../shared/wire/LambderCookie.js";
|
|
2
|
+
import { isMintedSessionToken } from "./LambderSessionManager.js";
|
|
3
|
+
/**
|
|
4
|
+
* No session for this request: the cookies named none, or the single session
|
|
5
|
+
* they named did not pair with the posted CSRF token.
|
|
6
|
+
*
|
|
7
|
+
* Typed rather than a bare Error because fetchSessionIfExists has to tell
|
|
8
|
+
* "there is no session here" apart from "something broke". Everything else,
|
|
9
|
+
* a TypeError from a custom store, a bug in an app's dataRefresh callback,
|
|
10
|
+
* propagates and becomes a crash: answering sessionExpired for a defect makes
|
|
11
|
+
* the client clear its cookies and turns somebody's bug into a logout.
|
|
12
|
+
*/
|
|
13
|
+
export class LambderSessionNotFoundError extends Error {
|
|
14
|
+
constructor(message = "Session not found") {
|
|
15
|
+
super(message);
|
|
16
|
+
this.name = "LambderSessionNotFoundError";
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* The request's session cookies cannot be resolved to one session, so none of
|
|
21
|
+
* them is used and every scope this host can write is cleared. Also a "no
|
|
22
|
+
* session" answer to the caller, and deliberately a different type: this one
|
|
23
|
+
* carries the clearing Set-Cookie headers that heal the state, and it is the
|
|
24
|
+
* one worth finding in a log.
|
|
25
|
+
*/
|
|
26
|
+
export class LambderSessionAmbiguousError extends Error {
|
|
27
|
+
constructor(message = "Session ambiguous") {
|
|
28
|
+
super(message);
|
|
29
|
+
this.name = "LambderSessionAmbiguousError";
|
|
30
|
+
}
|
|
31
|
+
}
|
|
3
32
|
/** The tokens are hex, so the cookie carries them as they are (the format existing browsers hold). */
|
|
4
|
-
const
|
|
33
|
+
const rawCookieValue = (value) => value;
|
|
34
|
+
/**
|
|
35
|
+
* A `__Host-` cookie is the browser's own answer to a sibling subdomain
|
|
36
|
+
* planting a session cookie at a parent domain: it refuses one that carries a
|
|
37
|
+
* Domain, so no other host can write it. `__Secure-` is the weaker sibling,
|
|
38
|
+
* accepted only on a Secure cookie. Both protections fail silently, though: a
|
|
39
|
+
* browser handed a prefixed name with an attribute the prefix forbids simply
|
|
40
|
+
* discards the cookie, and the app looks like it has no sessions at all
|
|
41
|
+
* rather than like it is misconfigured. So the combinations are rejected at
|
|
42
|
+
* creation instead.
|
|
43
|
+
*
|
|
44
|
+
* Session policy, so it lives beside the controller that writes the cookies
|
|
45
|
+
* rather than in the pipeline that happens to call it.
|
|
46
|
+
*/
|
|
47
|
+
export const assertSessionCookiePrefixes = (sessions) => {
|
|
48
|
+
const keys = [sessions.tokenCookieKey, sessions.csrfCookieKey];
|
|
49
|
+
const hostPrefixed = keys.filter((key) => key.startsWith("__Host-"));
|
|
50
|
+
const securePrefixed = keys.filter((key) => key.startsWith("__Secure-"));
|
|
51
|
+
if (sessions.cookieOptions.secure === false) {
|
|
52
|
+
// Both prefixes require Secure, so this one check covers them
|
|
53
|
+
// together; the messages stay separate because the fix differs.
|
|
54
|
+
if (hostPrefixed.length > 0) {
|
|
55
|
+
throw new Error(`Lambder: session cookie ${hostPrefixed.join(" and ")} uses the __Host- prefix, which a browser accepts only on a Secure cookie. ` +
|
|
56
|
+
"Drop session.cookie.secure: false, or drop the prefix; keeping both means the browser discards the cookie silently and no session is ever read.");
|
|
57
|
+
}
|
|
58
|
+
if (securePrefixed.length > 0) {
|
|
59
|
+
throw new Error(`Lambder: session cookie ${securePrefixed.join(" and ")} uses the __Secure- prefix, which a browser accepts only on a Secure cookie. ` +
|
|
60
|
+
"Drop session.cookie.secure: false, or drop the prefix; keeping both means the browser discards the cookie silently and no session is ever read.");
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
if (hostPrefixed.length === 0)
|
|
64
|
+
return;
|
|
65
|
+
if (sessions.cookieOptions.domain !== undefined) {
|
|
66
|
+
throw new Error(`Lambder: session cookie ${hostPrefixed.join(" and ")} uses the __Host- prefix, which a browser accepts only without a Domain. ` +
|
|
67
|
+
"Drop session.cookie.domain, or drop the prefix; keeping both means the browser discards the cookie and no session is ever read.");
|
|
68
|
+
}
|
|
69
|
+
if (sessions.cookieOptions.path !== undefined && sessions.cookieOptions.path !== "/") {
|
|
70
|
+
throw new Error(`Lambder: session cookie ${hostPrefixed.join(" and ")} uses the __Host- prefix, which a browser accepts only at Path=/. ` +
|
|
71
|
+
`Drop session.cookie.path, or drop the prefix.`);
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
/**
|
|
75
|
+
* How many copies of the session cookie one request may carry before the
|
|
76
|
+
* request is treated as ambiguous. A name legitimately arrives at a few
|
|
77
|
+
* scopes at once (a Domain change mid-migration leaves a host-only twin),
|
|
78
|
+
* and no browser has a reason to send more. Beyond this the request is
|
|
79
|
+
* refused rather than trimmed: dropping the extras would let anyone who can
|
|
80
|
+
* plant cookies at a parent domain push the visitor's own copy out of the
|
|
81
|
+
* read and log them out silently, with no eviction emitted, so it would
|
|
82
|
+
* never heal. Nothing is read from the store on that path.
|
|
83
|
+
*/
|
|
84
|
+
const MAX_SESSION_TOKEN_CANDIDATES = 4;
|
|
85
|
+
/**
|
|
86
|
+
* Sessions as one request sees them: reads the session the request's
|
|
87
|
+
* cookies name onto the context, and writes the cookies a created,
|
|
88
|
+
* rotated or ended session needs into the context's response headers.
|
|
89
|
+
* Server handlers reach it through lambder.getSessionController(ctx); mock
|
|
90
|
+
* handlers through ctx.sessions. It works on the call context and the
|
|
91
|
+
* request info alone, so it is one class for both.
|
|
92
|
+
*/
|
|
5
93
|
export default class LambderSessionController {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
94
|
+
manager;
|
|
95
|
+
tokenCookieKey;
|
|
96
|
+
csrfCookieKey;
|
|
9
97
|
cookieOptions;
|
|
10
|
-
ctx;
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
this.
|
|
14
|
-
this.
|
|
98
|
+
ctx;
|
|
99
|
+
request;
|
|
100
|
+
constructor({ manager, tokenCookieKey, csrfCookieKey, cookieOptions, ctx, request }) {
|
|
101
|
+
this.manager = manager;
|
|
102
|
+
this.tokenCookieKey = tokenCookieKey;
|
|
103
|
+
this.csrfCookieKey = csrfCookieKey;
|
|
15
104
|
this.cookieOptions = cookieOptions ?? {};
|
|
16
105
|
this.ctx = ctx;
|
|
106
|
+
this.request = request;
|
|
17
107
|
}
|
|
18
108
|
;
|
|
19
109
|
/** The configured scope with the domain resolved for this request, or the host-only scope. */
|
|
20
110
|
cookieScope(hostOnly = false) {
|
|
21
111
|
const { domain, path, sameSite, secure } = this.cookieOptions;
|
|
22
|
-
return { domain: hostOnly ? undefined : resolveCookieDomain(domain, this.
|
|
112
|
+
return { domain: hostOnly ? undefined : resolveCookieDomain(domain, this.request.host), path, sameSite, secure };
|
|
23
113
|
}
|
|
24
114
|
;
|
|
25
|
-
/**
|
|
26
|
-
|
|
115
|
+
/**
|
|
116
|
+
* Both cookies at one expiry, with the raw secrets. They exist only on the
|
|
117
|
+
* LambderCreatedSession result, in these cookies and in the request that
|
|
118
|
+
* carried them back; the record stores hashes. `csrfToken` is null where
|
|
119
|
+
* the raw CSRF value is not known to this request, in which case only the
|
|
120
|
+
* session cookie is written: writing a CSRF cookie whose value does not
|
|
121
|
+
* pair with the session would break the very session it is refreshing.
|
|
122
|
+
*/
|
|
123
|
+
writeSessionCookies(expiresAt, sessionToken, csrfToken) {
|
|
27
124
|
const scope = this.cookieScope();
|
|
28
|
-
const expires = new Date(
|
|
29
|
-
this.ctx.
|
|
30
|
-
|
|
125
|
+
const expires = new Date(expiresAt * 1000);
|
|
126
|
+
this.ctx.responseHeaders.add("Set-Cookie", serializeCookie(this.tokenCookieKey, sessionToken, { ...scope, expires, httpOnly: true, encode: rawCookieValue }));
|
|
127
|
+
if (csrfToken !== null)
|
|
128
|
+
this.ctx.responseHeaders.add("Set-Cookie", serializeCookie(this.csrfCookieKey, csrfToken, { ...scope, expires, encode: rawCookieValue }));
|
|
31
129
|
}
|
|
32
130
|
;
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
this.ctx._otherInternal.addHeaderFnAccumulator.push({ key: "Set-Cookie", value: serializeClearCookie(this.sessionTokenCookieKey, { ...scope, httpOnly: true }) });
|
|
36
|
-
this.ctx._otherInternal.addHeaderFnAccumulator.push({ key: "Set-Cookie", value: serializeClearCookie(this.sessionCsrfCookieKey, scope) });
|
|
131
|
+
setSessionCookies(created) {
|
|
132
|
+
this.writeSessionCookies(created.session.expiresAt, created.sessionToken, created.csrfToken);
|
|
37
133
|
}
|
|
38
134
|
;
|
|
39
135
|
/**
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
136
|
+
* The deleting pair for one scope. Written once because a deletion only
|
|
137
|
+
* reaches a cookie carrying the same Domain and Path, so the pair is
|
|
138
|
+
* emitted per scope and the two callers differ in nothing but which
|
|
139
|
+
* scopes they walk.
|
|
43
140
|
*/
|
|
44
|
-
|
|
45
|
-
|
|
141
|
+
addClearCookiePair(scope) {
|
|
142
|
+
this.ctx.responseHeaders.add("Set-Cookie", serializeClearCookie(this.tokenCookieKey, { ...scope, httpOnly: true }));
|
|
143
|
+
this.ctx.responseHeaders.add("Set-Cookie", serializeClearCookie(this.csrfCookieKey, scope));
|
|
46
144
|
}
|
|
47
145
|
;
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
146
|
+
clearSessionCookies(hostOnly = false) {
|
|
147
|
+
this.addClearCookiePair(this.cookieScope(hostOnly));
|
|
148
|
+
}
|
|
149
|
+
;
|
|
150
|
+
/**
|
|
151
|
+
* Every Domain this host is allowed to write the session cookies at: the
|
|
152
|
+
* host-only scope, the configured one, and each parent domain of the
|
|
153
|
+
* request host. A deletion matches only a cookie carrying the same
|
|
154
|
+
* Domain, so evicting a copy the app itself never set needs all of them.
|
|
155
|
+
* A browser ignores a Domain it will not accept, which is why a suffix
|
|
156
|
+
* the registry owns can be offered without checking a public-suffix list.
|
|
157
|
+
*/
|
|
158
|
+
cookieClearDomains() {
|
|
159
|
+
const hostname = (this.request.host.split(":")[0] ?? "").toLowerCase();
|
|
160
|
+
const labels = hostname.split(".").filter(Boolean);
|
|
161
|
+
const scopes = [];
|
|
162
|
+
// Two labels is the shortest a browser will take; one is a registry
|
|
163
|
+
// suffix and is rejected wherever it is offered.
|
|
164
|
+
for (let i = 0; i + 2 <= labels.length; i++) {
|
|
165
|
+
scopes.push(labels.slice(i).join("."));
|
|
54
166
|
}
|
|
55
|
-
|
|
56
|
-
|
|
167
|
+
const configured = resolveCookieDomain(this.cookieOptions.domain, this.request.host);
|
|
168
|
+
if (configured)
|
|
169
|
+
scopes.push(configured);
|
|
170
|
+
// A leading dot is ignored when matching, so ".example.com" and
|
|
171
|
+
// "example.com" name one cookie and only one deletion is needed.
|
|
172
|
+
const named = new Set(scopes.map((domain) => domain.replace(/^\./, "")));
|
|
173
|
+
return [undefined, ...named];
|
|
174
|
+
}
|
|
175
|
+
;
|
|
176
|
+
/**
|
|
177
|
+
* Clears the session cookies at every scope this host can reach, rather
|
|
178
|
+
* than at the one the app configured. Used when a request carries more
|
|
179
|
+
* than one live session: the copy that has to go may sit at a parent
|
|
180
|
+
* domain a sibling host planted it at, and clearing the configured scope
|
|
181
|
+
* alone would evict this visitor's own cookie and leave the planted one
|
|
182
|
+
* as the only survivor, which completes the takeover instead of stopping
|
|
183
|
+
* it.
|
|
184
|
+
*/
|
|
185
|
+
clearSessionCookiesEverywhere() {
|
|
186
|
+
const base = this.cookieScope(true);
|
|
187
|
+
for (const domain of this.cookieClearDomains()) {
|
|
188
|
+
this.addClearCookiePair({ ...base, domain });
|
|
57
189
|
}
|
|
58
190
|
}
|
|
59
191
|
;
|
|
192
|
+
/**
|
|
193
|
+
* Refuses a request whose session cookies cannot be resolved to one
|
|
194
|
+
* session, clearing every scope this host can write. Clearing only the
|
|
195
|
+
* configured scope would be worse than picking one: a deletion matches
|
|
196
|
+
* only a cookie carrying the same Domain, so it would evict the visitor's
|
|
197
|
+
* own copy and leave a planted one as the sole survivor.
|
|
198
|
+
*
|
|
199
|
+
* Path is the one dimension this cannot sweep: the request info carries
|
|
200
|
+
* no path, and a deletion matches only a cookie at the same Path, so a
|
|
201
|
+
* copy planted at a deeper path stays out of reach. A `__Host-` cookie
|
|
202
|
+
* name is the structural answer, since the prefix forbids Domain and
|
|
203
|
+
* pins Path to "/"; see docs/sessions.md.
|
|
204
|
+
*/
|
|
205
|
+
refuseAmbiguousSession(what) {
|
|
206
|
+
console.warn(`Lambder session: ${what}, from ${this.request.host}. `
|
|
207
|
+
+ "Refusing all of them and clearing every scope this host can write: only one of them can be this visitor's, and picking one would sign them into the other. "
|
|
208
|
+
+ "A cookie set at a parent domain is the usual cause, whether from a sibling site or an old deployment scope.");
|
|
209
|
+
this.clearSessionCookiesEverywhere();
|
|
210
|
+
throw new LambderSessionAmbiguousError();
|
|
211
|
+
}
|
|
212
|
+
;
|
|
213
|
+
/**
|
|
214
|
+
* Both session cookie names read in one pass: every well-formed value
|
|
215
|
+
* under the token name, and every value under the CSRF name.
|
|
216
|
+
*
|
|
217
|
+
* The CSRF cookie is counted here rather than looked at only when a token
|
|
218
|
+
* is checked against it, because it is plantable exactly like the session
|
|
219
|
+
* cookie and the browser picks between copies without telling anyone: the
|
|
220
|
+
* client reads its CSRF token with js-cookie's Cookies.get, which returns
|
|
221
|
+
* the FIRST copy in document.cookie, and a browser orders a longer Path
|
|
222
|
+
* first. So a sibling host that plants one CSRF cookie at a parent domain
|
|
223
|
+
* with a deeper Path decides which token every call posts, and the count
|
|
224
|
+
* is the only thing that shows it.
|
|
225
|
+
*/
|
|
226
|
+
scanSessionCookies() {
|
|
227
|
+
const wellFormed = (this.request.cookies[this.tokenCookieKey] ?? []).filter(isMintedSessionToken);
|
|
228
|
+
// Deduplicated: one value arriving twice (a proxy that appends rather
|
|
229
|
+
// than merges Cookie, the same value set at two scopes) is one
|
|
230
|
+
// session, and counting it twice would read as an ambiguity. Not
|
|
231
|
+
// truncated: the count past the cap is the caller's answer, not
|
|
232
|
+
// something to trim away (see MAX_SESSION_TOKEN_CANDIDATES).
|
|
233
|
+
return {
|
|
234
|
+
sessionTokens: [...new Set(wellFormed)],
|
|
235
|
+
csrfTokens: [...new Set(this.request.cookies[this.csrfCookieKey] ?? [])],
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
;
|
|
239
|
+
/** An API call must also carry a CSRF token; a route only needs the cookie. */
|
|
240
|
+
areRequestSessionTokensValid(sessionTokens) {
|
|
241
|
+
if (sessionTokens.length === 0)
|
|
242
|
+
return false;
|
|
243
|
+
if (this.request.csrfToken !== null)
|
|
244
|
+
return this.request.csrfToken.length > 0;
|
|
245
|
+
return true;
|
|
246
|
+
}
|
|
247
|
+
;
|
|
60
248
|
async createSession(sessionKey, data, ttlInSeconds) {
|
|
61
|
-
|
|
249
|
+
return (await this.issueSession(sessionKey, data, ttlInSeconds)).session;
|
|
250
|
+
}
|
|
251
|
+
;
|
|
252
|
+
/**
|
|
253
|
+
* createSession, handing back the raw tokens beside the session: what a
|
|
254
|
+
* test or a mock runtime needs to plant the cookies somewhere else (a
|
|
255
|
+
* cookie jar) than this call's response.
|
|
256
|
+
*/
|
|
257
|
+
async issueSession(sessionKey, data, ttlInSeconds) {
|
|
258
|
+
const created = await this.manager.createSession(sessionKey, data, ttlInSeconds);
|
|
62
259
|
this.setSessionCookies(created);
|
|
63
260
|
this.ctx.session = created.session;
|
|
64
|
-
return
|
|
261
|
+
return created;
|
|
65
262
|
}
|
|
66
263
|
;
|
|
67
264
|
async regenerateSession() {
|
|
265
|
+
return (await this.reissueSession()).session;
|
|
266
|
+
}
|
|
267
|
+
;
|
|
268
|
+
/**
|
|
269
|
+
* regenerateSession, handing back the raw tokens beside the session, the
|
|
270
|
+
* way issueSession does for a new one. Rotating the session mints a new
|
|
271
|
+
* CSRF token, and a client that holds its token rather than reading
|
|
272
|
+
* document.cookie (a native app, an invoke caller) needs the new one to
|
|
273
|
+
* keep calling.
|
|
274
|
+
*/
|
|
275
|
+
async reissueSession() {
|
|
68
276
|
if (!this.ctx.session)
|
|
69
|
-
throw new
|
|
70
|
-
const created = await this.
|
|
277
|
+
throw new LambderSessionNotFoundError();
|
|
278
|
+
const created = await this.manager.regenerateSession(this.ctx.session);
|
|
71
279
|
this.setSessionCookies(created);
|
|
72
280
|
this.ctx.session = created.session;
|
|
73
|
-
return
|
|
281
|
+
return created;
|
|
74
282
|
}
|
|
75
283
|
;
|
|
76
284
|
async fetchSession() {
|
|
77
|
-
|
|
78
|
-
|
|
285
|
+
const { sessionTokens: candidates, csrfTokens } = this.scanSessionCookies();
|
|
286
|
+
if (!this.areRequestSessionTokensValid(candidates)) {
|
|
287
|
+
throw new LambderSessionNotFoundError("Session tokens are invalid");
|
|
288
|
+
}
|
|
289
|
+
if (candidates.length > MAX_SESSION_TOKEN_CANDIDATES) {
|
|
290
|
+
this.refuseAmbiguousSession(`more than ${MAX_SESSION_TOKEN_CANDIDATES} "${this.tokenCookieKey}" cookies arrived, at different scopes`);
|
|
79
291
|
}
|
|
80
|
-
|
|
292
|
+
// After the cap check, because this line says the store is about to
|
|
293
|
+
// be read once per copy and over the cap nothing is read at all.
|
|
81
294
|
if (candidates.length > 1) {
|
|
82
|
-
console.warn(`Lambder session: ${candidates.length} "${this.
|
|
295
|
+
console.warn(`Lambder session: ${candidates.length} "${this.tokenCookieKey}" cookies arrived from ${this.request.host}; the browser holds the cookie at several scopes. Reading each.`);
|
|
83
296
|
}
|
|
297
|
+
// How many sessions the browser is holding is a question about the
|
|
298
|
+
// cookies alone, so it is asked without the CSRF pairing. Folding the
|
|
299
|
+
// pairing in here would answer a different question and always answer
|
|
300
|
+
// it "one": a sibling subdomain plants its own CSRF cookie beside the
|
|
301
|
+
// session it planted, only one CSRF token is ever posted, and no two
|
|
302
|
+
// sessions share a csrfTokenHash, so exactly one candidate would
|
|
303
|
+
// survive the pairing and the ambiguity this check exists to catch
|
|
304
|
+
// would be invisible. The pairing is asked once, below, of whichever
|
|
305
|
+
// single session the cookies resolved to.
|
|
306
|
+
//
|
|
307
|
+
// One live session and some stale copies is the ordinary case (a
|
|
308
|
+
// cookie whose Domain or Path changed), and the live one wins. Two
|
|
309
|
+
// LIVE sessions under one name is not ordinary: any sibling subdomain
|
|
310
|
+
// can write a cookie at a parent domain that the browser then sends
|
|
311
|
+
// alongside the real one, and taking either would sign this visitor
|
|
312
|
+
// into an account that may not be theirs. There is no way to tell
|
|
313
|
+
// which copy they meant, so neither is used.
|
|
314
|
+
const live = [];
|
|
84
315
|
for (const sessionToken of candidates) {
|
|
85
|
-
|
|
86
|
-
|
|
316
|
+
// lookupSession finds the record BY the hash of this token's own
|
|
317
|
+
// secret and checks the structure and the expiry on the way, so
|
|
318
|
+
// possession is already proved here and re-checking the token
|
|
319
|
+
// against the record would only hash the same secret twice.
|
|
320
|
+
const candidate = await this.manager.lookupSession(sessionToken);
|
|
321
|
+
if (!candidate)
|
|
87
322
|
continue;
|
|
88
|
-
|
|
89
|
-
//
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
323
|
+
live.push({ token: sessionToken, session: candidate });
|
|
324
|
+
// A second live copy is already the whole answer.
|
|
325
|
+
if (live.length > 1)
|
|
326
|
+
break;
|
|
327
|
+
}
|
|
328
|
+
if (live.length > 1) {
|
|
329
|
+
this.refuseAmbiguousSession(`more than one live "${this.tokenCookieKey}" cookie arrived, at different scopes`);
|
|
330
|
+
}
|
|
331
|
+
const found = live[0];
|
|
332
|
+
if (!found)
|
|
333
|
+
throw new LambderSessionNotFoundError();
|
|
334
|
+
// The pairing check, once, against the one session the cookies
|
|
335
|
+
// resolved to. An API call that did not post the matching CSRF token
|
|
336
|
+
// has no session here.
|
|
337
|
+
if (this.request.csrfToken !== null && !(await this.manager.isSessionCsrfTokenValid(found.session, this.request.csrfToken))) {
|
|
338
|
+
// A session cookie that resolves while the posted CSRF token
|
|
339
|
+
// belongs to nothing is the CSRF half of the planted-cookie
|
|
340
|
+
// shape, and it is unhealable on its own: the client reads the
|
|
341
|
+
// FIRST CSRF cookie in document.cookie, a longer Path sorts
|
|
342
|
+
// first, so the planted copy keeps winning through the logout,
|
|
343
|
+
// through the next sign-in, and through every call after it. The
|
|
344
|
+
// ordinary answer (no session) emits no Set-Cookie at all, and
|
|
345
|
+
// the client can only clear the scopes it knows, which are not
|
|
346
|
+
// the ones a sibling host planted at. So the everywhere-clear
|
|
347
|
+
// runs instead, and the state heals.
|
|
348
|
+
//
|
|
349
|
+
// Only when the request actually carried CSRF cookies: an invoke
|
|
350
|
+
// caller posts the CSRF value in the envelope and sends no CSRF
|
|
351
|
+
// cookie at all, and its wrong token is an ordinary no-session.
|
|
352
|
+
// And only when the cookies are the suspect: one CSRF cookie that
|
|
353
|
+
// is the token posted, not pairing, is a stale pair the visitor
|
|
354
|
+
// can clear themselves.
|
|
355
|
+
if (csrfTokens.length > 1) {
|
|
356
|
+
this.refuseAmbiguousSession(`more than one "${this.csrfCookieKey}" cookie arrived, at different scopes, and the one posted pairs with no session`);
|
|
357
|
+
}
|
|
358
|
+
if (csrfTokens.length === 1 && !csrfTokens.includes(this.request.csrfToken)) {
|
|
359
|
+
this.refuseAmbiguousSession(`the posted CSRF token matches no "${this.csrfCookieKey}" cookie this request carried, while the session cookie named a live session`);
|
|
360
|
+
}
|
|
361
|
+
throw new LambderSessionNotFoundError();
|
|
362
|
+
}
|
|
363
|
+
// Renewed only now that this session is known to be the caller's:
|
|
364
|
+
// a slide or a dataRefresh is a write on their behalf.
|
|
365
|
+
const expiresBefore = found.session.expiresAt;
|
|
366
|
+
const session = await this.manager.renewSession(found.session);
|
|
367
|
+
if (!session)
|
|
368
|
+
throw new LambderSessionNotFoundError();
|
|
369
|
+
// The other copies are stale. This response can evict the
|
|
370
|
+
// host-only twin of a Domain= cookie; a copy at a parent domain
|
|
371
|
+
// this host cannot name is out of reach and expires on its own.
|
|
372
|
+
//
|
|
373
|
+
// Which copy is the stale one is an assumption, not a fact: the
|
|
374
|
+
// request carries no scope, so the twin being deleted may be the live
|
|
375
|
+
// cookie this very read resolved, held by a visitor who signed in
|
|
376
|
+
// before the app configured a domain. So the eviction always ships
|
|
377
|
+
// with the replacement, at the configured scope, and the visitor
|
|
378
|
+
// stays signed in either way. It also lets a domain migration
|
|
379
|
+
// converge on the first request rather than on the first slide.
|
|
380
|
+
const evictsHostOnlyTwin = candidates.length > 1 && !!this.cookieScope().domain;
|
|
381
|
+
if (evictsHostOnlyTwin)
|
|
382
|
+
this.clearSessionCookies(true);
|
|
383
|
+
// A sliding write moved the record's expiry, so the cookies have to
|
|
384
|
+
// move with it. Without this the browser keeps the Expires it was
|
|
385
|
+
// given at creation and drops both cookies at createdAt + ttl, so a
|
|
386
|
+
// visitor who never stops using the app is signed out anyway, on the
|
|
387
|
+
// one deadline sliding expiration exists to push back. Throttled by
|
|
388
|
+
// the same interval as the write, so an active session re-issues its
|
|
389
|
+
// cookies at most that often and not on every request.
|
|
390
|
+
if (evictsHostOnlyTwin || session.expiresAt !== expiresBefore)
|
|
391
|
+
await this.slideSessionCookies(session, found.token, csrfTokens);
|
|
392
|
+
this.ctx.session = session;
|
|
393
|
+
return session;
|
|
394
|
+
}
|
|
395
|
+
;
|
|
396
|
+
/**
|
|
397
|
+
* Re-issues both cookies at this session's expiry: after a sliding write
|
|
398
|
+
* moved it, and beside the host-only eviction above, which would
|
|
399
|
+
* otherwise delete a cookie without replacing it.
|
|
400
|
+
*
|
|
401
|
+
* The raw CSRF value is the posted one on an API call, which the pairing
|
|
402
|
+
* check above has just matched against this session. A route posts none,
|
|
403
|
+
* so the single arriving CSRF cookie stands in, and only once it is known
|
|
404
|
+
* to pair: re-issuing an unpaired value would overwrite this visitor's
|
|
405
|
+
* real CSRF cookie with a planted one, at the app's own scope, which is
|
|
406
|
+
* the takeover the scan exists to prevent. Where neither is available the
|
|
407
|
+
* session cookie slides alone, which is the half that decides whether the
|
|
408
|
+
* session survives.
|
|
409
|
+
*/
|
|
410
|
+
async slideSessionCookies(session, sessionToken, csrfTokens) {
|
|
411
|
+
const posted = this.request.csrfToken;
|
|
412
|
+
if (posted !== null) {
|
|
413
|
+
this.writeSessionCookies(session.expiresAt, sessionToken, posted);
|
|
414
|
+
return;
|
|
95
415
|
}
|
|
96
|
-
|
|
416
|
+
const only = csrfTokens.length === 1 ? csrfTokens[0] : null;
|
|
417
|
+
const paired = only !== null && await this.manager.isSessionCsrfTokenValid(session, only);
|
|
418
|
+
this.writeSessionCookies(session.expiresAt, sessionToken, paired ? only : null);
|
|
97
419
|
}
|
|
98
420
|
;
|
|
99
421
|
async fetchSessionIfExists() {
|
|
@@ -101,32 +423,24 @@ export default class LambderSessionController {
|
|
|
101
423
|
return await this.fetchSession();
|
|
102
424
|
}
|
|
103
425
|
catch (err) {
|
|
104
|
-
//
|
|
105
|
-
// dataRefresh callback
|
|
106
|
-
//
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
/** Checks the record against a presented token (the request's first session cookie by default) and, on API calls, the posted CSRF token. */
|
|
116
|
-
isSessionValid(session, sessionToken = this.ctx.cookie?.[this.sessionTokenCookieKey]) {
|
|
117
|
-
if (this.ctx._otherInternal.isApiCall) {
|
|
118
|
-
const csrfToken = this.ctx.post?.token;
|
|
119
|
-
return this.lambderSessionManager.isSessionValid(session, sessionToken, csrfToken);
|
|
120
|
-
}
|
|
121
|
-
else {
|
|
122
|
-
return this.lambderSessionManager.isSessionValid(session, sessionToken, null, true);
|
|
426
|
+
// Only the two "no session" exits become null. A failing
|
|
427
|
+
// dataRefresh callback, a store read failure, and anything
|
|
428
|
+
// unexpected (a TypeError from a custom store, a bug in this
|
|
429
|
+
// layer) propagate: answering sessionExpired for a defect makes
|
|
430
|
+
// the client clear its cookies, so a crash would present as a
|
|
431
|
+
// logout and the log would say nothing happened.
|
|
432
|
+
if (err instanceof LambderSessionNotFoundError)
|
|
433
|
+
return null;
|
|
434
|
+
if (err instanceof LambderSessionAmbiguousError)
|
|
435
|
+
return null;
|
|
436
|
+
throw err;
|
|
123
437
|
}
|
|
124
438
|
}
|
|
125
439
|
;
|
|
126
440
|
async updateSessionData(newData) {
|
|
127
441
|
if (!this.ctx.session)
|
|
128
|
-
throw new
|
|
129
|
-
this.ctx.session = await this.
|
|
442
|
+
throw new LambderSessionNotFoundError();
|
|
443
|
+
this.ctx.session = await this.manager.updateSessionData(this.ctx.session, newData);
|
|
130
444
|
return this.ctx.session;
|
|
131
445
|
}
|
|
132
446
|
;
|
|
@@ -138,8 +452,8 @@ export default class LambderSessionController {
|
|
|
138
452
|
*/
|
|
139
453
|
async refreshSessionData() {
|
|
140
454
|
if (!this.ctx.session)
|
|
141
|
-
throw new
|
|
142
|
-
const refreshed = await this.
|
|
455
|
+
throw new LambderSessionNotFoundError();
|
|
456
|
+
const refreshed = await this.manager.refreshSessionData(this.ctx.session);
|
|
143
457
|
if (!refreshed) {
|
|
144
458
|
this.clearSessionCookies();
|
|
145
459
|
this.ctx.session = null;
|
|
@@ -155,7 +469,7 @@ export default class LambderSessionController {
|
|
|
155
469
|
* session and touches no cookies, so it works on any subject.
|
|
156
470
|
*/
|
|
157
471
|
async deleteSessionAllByKey(sessionKey) {
|
|
158
|
-
await this.
|
|
472
|
+
await this.manager.deleteSessionAllByKey(sessionKey);
|
|
159
473
|
}
|
|
160
474
|
;
|
|
161
475
|
/**
|
|
@@ -165,21 +479,21 @@ export default class LambderSessionController {
|
|
|
165
479
|
* out. Needs no fetched session; requires dataRefresh.
|
|
166
480
|
*/
|
|
167
481
|
async expireSessionDataAllByKey(sessionKey) {
|
|
168
|
-
await this.
|
|
482
|
+
await this.manager.expireSessionDataAllByKey(sessionKey);
|
|
169
483
|
}
|
|
170
484
|
;
|
|
171
485
|
async endSession() {
|
|
172
486
|
if (!this.ctx.session)
|
|
173
|
-
throw new
|
|
174
|
-
await this.
|
|
487
|
+
throw new LambderSessionNotFoundError();
|
|
488
|
+
await this.manager.deleteSession(this.ctx.session);
|
|
175
489
|
this.clearSessionCookies();
|
|
176
490
|
this.ctx.session = null;
|
|
177
491
|
}
|
|
178
492
|
;
|
|
179
493
|
async endSessionAll() {
|
|
180
494
|
if (!this.ctx.session)
|
|
181
|
-
throw new
|
|
182
|
-
await this.
|
|
495
|
+
throw new LambderSessionNotFoundError();
|
|
496
|
+
await this.manager.deleteSessionAll(this.ctx.session);
|
|
183
497
|
this.clearSessionCookies();
|
|
184
498
|
this.ctx.session = null;
|
|
185
499
|
}
|
|
@@ -0,0 +1,66 @@
|
|
|
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
|
+
/**
|
|
14
|
+
* Hashing, randomness and constant-time comparison, as the session manager
|
|
15
|
+
* asks for them. Implement it to run sessions on a runtime's own primitives.
|
|
16
|
+
*/
|
|
17
|
+
export interface LambderSessionCrypto {
|
|
18
|
+
/**
|
|
19
|
+
* Whether this really hashes and really draws random bytes. The session
|
|
20
|
+
* manager refuses a non-cryptographic implementation over a store that
|
|
21
|
+
* outlives the process, because such a store would then hold usable
|
|
22
|
+
* credentials rather than hashes of them.
|
|
23
|
+
*/
|
|
24
|
+
readonly isCryptographic: boolean;
|
|
25
|
+
sha256Hex(value: string): Promise<string>;
|
|
26
|
+
randomHex(bytes: number): Promise<string>;
|
|
27
|
+
constantTimeEqual(a: string, b: string): boolean;
|
|
28
|
+
}
|
|
29
|
+
/** True when this runtime offers WebCrypto's subtle API (secure contexts in browsers; Node 20+). */
|
|
30
|
+
export declare const isWebCryptoAvailable: () => boolean;
|
|
31
|
+
/** sha256 through crypto.subtle and randomness through getRandomValues: the default. */
|
|
32
|
+
export declare class LambderWebCrypto implements LambderSessionCrypto {
|
|
33
|
+
readonly isCryptographic = true;
|
|
34
|
+
private cryptoPromise;
|
|
35
|
+
/**
|
|
36
|
+
* Node's crypto where the runtime has it, kept for timingSafeEqual.
|
|
37
|
+
* Warmed by ready(), because the comparison itself is synchronous and a
|
|
38
|
+
* session is always hashed before anything is compared against it.
|
|
39
|
+
*/
|
|
40
|
+
private nodeCrypto;
|
|
41
|
+
/**
|
|
42
|
+
* The runtime's WebCrypto, through the resolver every layer shares, with
|
|
43
|
+
* Node's crypto warmed alongside it.
|
|
44
|
+
*
|
|
45
|
+
* The availability question is asked here, before the shared resolver,
|
|
46
|
+
* only because of the answer a session has to it: a runtime with neither
|
|
47
|
+
* a global crypto nor Node's webcrypto can still run sessions over
|
|
48
|
+
* LambderPlainSessionCrypto and a memory store, which is this layer's own
|
|
49
|
+
* way out and not something the shared message can know about.
|
|
50
|
+
*/
|
|
51
|
+
private ready;
|
|
52
|
+
sha256Hex(value: string): Promise<string>;
|
|
53
|
+
randomHex(bytes: number): Promise<string>;
|
|
54
|
+
constantTimeEqual(a: string, b: string): boolean;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* No hashing and no cryptographic randomness: values are hex-encoded as
|
|
58
|
+
* they are and secrets come from Math.random. Only for an in-memory store
|
|
59
|
+
* in a runtime without WebCrypto; never for anything at rest.
|
|
60
|
+
*/
|
|
61
|
+
export declare class LambderPlainSessionCrypto implements LambderSessionCrypto {
|
|
62
|
+
readonly isCryptographic = false;
|
|
63
|
+
sha256Hex(value: string): Promise<string>;
|
|
64
|
+
randomHex(bytes: number): Promise<string>;
|
|
65
|
+
constantTimeEqual(a: string, b: string): boolean;
|
|
66
|
+
}
|