lambder 7.2.5 → 8.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +1021 -3
- package/README.md +43 -21
- package/dist/api/LambderApiAnswer.d.ts +18 -22
- package/dist/api/LambderApiAnswer.js +6 -7
- package/dist/api/LambderApiCallContext.d.ts +21 -8
- package/dist/api/LambderApiCallContext.js +22 -4
- package/dist/api/LambderApiDefinition.d.ts +4 -3
- package/dist/api/LambderApiEnvelope.d.ts +14 -9
- package/dist/api/LambderApiEnvelope.js +33 -34
- package/dist/api/LambderApiGuards.d.ts +78 -51
- package/dist/api/LambderApiGuards.js +34 -36
- package/dist/api/LambderApiIdempotency.d.ts +74 -61
- package/dist/api/LambderApiIdempotency.js +226 -151
- package/dist/api/LambderApiOutputValidationError.d.ts +32 -0
- package/dist/api/LambderApiOutputValidationError.js +50 -0
- package/dist/api/LambderApiPipeline.d.ts +77 -39
- package/dist/api/LambderApiPipeline.js +135 -62
- package/dist/api/LambderApiRateLimits.d.ts +208 -54
- package/dist/api/LambderApiRateLimits.js +197 -108
- package/dist/api/LambderApiRequest.d.ts +27 -21
- package/dist/api/LambderApiRequest.js +26 -19
- package/dist/api/LambderApiSignature.d.ts +12 -15
- package/dist/api/LambderApiSignature.js +28 -51
- package/dist/api/LambderApiValidationRefusal.d.ts +9 -9
- package/dist/api/LambderApiValidationRefusal.js +10 -10
- package/dist/build/freshProcessVerifier.d.ts +13 -0
- package/dist/build/freshProcessVerifier.js +19 -0
- package/dist/build/writeApiSignatures.d.ts +109 -0
- package/dist/build/writeApiSignatures.js +222 -0
- package/dist/build.d.ts +9 -0
- package/dist/build.js +8 -0
- package/dist/client/LambderCaller.d.ts +13 -44
- package/dist/client/LambderCaller.js +77 -84
- package/dist/client/LambderReloadLoopBreaker.d.ts +56 -26
- package/dist/client/LambderReloadLoopBreaker.js +90 -46
- package/dist/client/lambderFetchTransport.d.ts +4 -1
- package/dist/client/lambderFetchTransport.js +52 -28
- package/dist/client.d.ts +5 -3
- package/dist/client.js +2 -1
- package/dist/core/Lambder.d.ts +161 -69
- package/dist/core/Lambder.js +370 -226
- package/dist/core/LambderContext.d.ts +82 -15
- package/dist/core/LambderContext.js +107 -20
- package/dist/core/LambderCors.d.ts +21 -3
- package/dist/core/LambderCors.js +35 -16
- package/dist/core/LambderCrashHandling.d.ts +40 -0
- package/dist/core/LambderCrashHandling.js +97 -0
- package/dist/core/LambderCreateOptions.d.ts +151 -75
- package/dist/core/LambderCreateOptions.js +16 -23
- package/dist/core/LambderFiles.d.ts +28 -7
- package/dist/core/LambderFiles.js +73 -33
- package/dist/core/LambderIndexHtml.js +12 -11
- package/dist/core/LambderPolicyBuilders.d.ts +17 -5
- package/dist/core/LambderPolicyBuilders.js +17 -5
- package/dist/core/LambderPublicFiles.d.ts +11 -5
- package/dist/core/LambderPublicFiles.js +32 -4
- package/dist/core/LambderRequestPath.d.ts +43 -0
- package/dist/core/LambderRequestPath.js +63 -0
- package/dist/core/LambderResponse.d.ts +26 -5
- package/dist/core/LambderResponse.js +157 -70
- package/dist/core/LambderResponseBuilder.d.ts +49 -4
- package/dist/core/LambderResponseBuilder.js +64 -3
- package/dist/core/LambderRouting.d.ts +2 -3
- package/dist/core/LambderRouting.js +22 -7
- package/dist/core/LambderTemplatingEngine.js +211 -32
- package/dist/index.d.ts +15 -8
- package/dist/index.js +5 -4
- package/dist/invoke/LambderInvokeCaller.d.ts +37 -42
- package/dist/invoke/LambderInvokeCaller.js +76 -66
- package/dist/invoke/LambderInvokeOutcome.d.ts +27 -26
- package/dist/invoke/LambderInvokeOutcome.js +9 -22
- package/dist/invoke/LambderLambdaEvent.d.ts +44 -10
- package/dist/invoke/LambderLambdaEvent.js +80 -37
- package/dist/invoke/lambderHandlerTransport.d.ts +12 -10
- package/dist/invoke/lambderHandlerTransport.js +16 -19
- package/dist/mock/LambderMockApp.d.ts +67 -83
- package/dist/mock/LambderMockApp.js +167 -153
- package/dist/mock/LambderMockBrowserCookies.d.ts +24 -28
- package/dist/mock/LambderMockBrowserCookies.js +24 -28
- package/dist/mock/LambderMockCallRecorder.d.ts +15 -22
- package/dist/mock/LambderMockCallRecorder.js +19 -28
- package/dist/mock/LambderMockCreateOptions.d.ts +42 -24
- package/dist/mock/LambderMockEntryRegistry.d.ts +11 -12
- package/dist/mock/LambderMockEntryRegistry.js +24 -29
- package/dist/mock/LambderMockFailureInjector.d.ts +3 -6
- package/dist/mock/LambderMockFailureInjector.js +3 -6
- package/dist/mock/LambderMockTypes.d.ts +78 -108
- package/dist/mock/lambderMockInvokeTransport.d.ts +11 -13
- package/dist/mock/lambderMockInvokeTransport.js +11 -10
- package/dist/mock/lambderMockMswHandler.d.ts +33 -29
- package/dist/mock/lambderMockMswHandler.js +50 -39
- package/dist/mock.d.ts +3 -1
- package/dist/mock.js +5 -3
- package/dist/session/LambderSessionController.d.ts +108 -89
- package/dist/session/LambderSessionController.js +187 -168
- package/dist/session/LambderSessionCrypto.d.ts +16 -7
- package/dist/session/LambderSessionCrypto.js +26 -12
- package/dist/session/LambderSessionManager.d.ts +136 -47
- package/dist/session/LambderSessionManager.js +280 -139
- package/dist/shared/LambderHtml.d.ts +42 -3
- package/dist/shared/LambderHtml.js +127 -7
- package/dist/shared/LambderHtmlPositions.d.ts +173 -0
- package/dist/shared/LambderHtmlPositions.js +652 -0
- package/dist/shared/LambderI18n.d.ts +10 -11
- package/dist/shared/LambderI18n.js +33 -21
- package/dist/shared/contracts/LambderCache.d.ts +66 -0
- package/dist/shared/contracts/LambderCache.js +11 -0
- package/dist/shared/contracts/LambderFileSource.d.ts +6 -6
- package/dist/shared/contracts/LambderFileSource.js +5 -8
- package/dist/shared/contracts/LambderIdempotencyStore.d.ts +51 -22
- package/dist/shared/contracts/LambderIdempotencyStore.js +4 -5
- package/dist/shared/contracts/LambderRateLimiter.d.ts +27 -15
- package/dist/shared/contracts/LambderRateLimiter.js +4 -5
- package/dist/shared/contracts/LambderSessionStore.d.ts +65 -26
- package/dist/shared/contracts/LambderSessionStore.js +5 -6
- package/dist/shared/transport/LambderApiTransport.d.ts +27 -27
- package/dist/shared/transport/LambderApiTransport.js +7 -7
- package/dist/shared/transport/LambderCookieJar.d.ts +28 -35
- package/dist/shared/transport/LambderCookieJar.js +54 -66
- package/dist/shared/transport/lambderCookieJarTransport.d.ts +11 -13
- package/dist/shared/transport/lambderCookieJarTransport.js +24 -23
- package/dist/shared/util/LambderCallAbort.d.ts +5 -5
- package/dist/shared/util/LambderCallAbort.js +5 -5
- package/dist/shared/util/LambderClientIp.d.ts +27 -11
- package/dist/shared/util/LambderClientIp.js +96 -13
- package/dist/shared/util/LambderExpiringMap.d.ts +35 -49
- package/dist/shared/util/LambderExpiringMap.js +41 -57
- package/dist/shared/util/LambderNodeModules.js +6 -7
- package/dist/shared/util/LambderOptionChecks.d.ts +4 -4
- package/dist/shared/util/LambderOptionChecks.js +4 -4
- package/dist/shared/util/LambderResponseBrand.d.ts +5 -5
- package/dist/shared/util/LambderResponseBrand.js +5 -5
- package/dist/shared/util/LambderTestingDoors.d.ts +29 -0
- package/dist/shared/util/LambderTestingDoors.js +29 -0
- package/dist/shared/util/LambderTypeUtilities.d.ts +7 -8
- package/dist/shared/util/LambderTypeUtilities.js +3 -3
- package/dist/shared/util/boundKeyField.d.ts +20 -0
- package/dist/shared/util/boundKeyField.js +34 -0
- package/dist/shared/util/canonicalJson.d.ts +11 -0
- package/dist/shared/util/canonicalJson.js +28 -0
- package/dist/shared/util/joinKeyFields.d.ts +20 -0
- package/dist/shared/util/joinKeyFields.js +22 -0
- package/dist/shared/wire/LambderAnswerHeaders.d.ts +12 -16
- package/dist/shared/wire/LambderAnswerHeaders.js +12 -16
- package/dist/shared/wire/LambderApiContract.d.ts +107 -32
- package/dist/shared/wire/LambderApiOutcome.d.ts +43 -31
- package/dist/shared/wire/LambderApiOutcome.js +48 -23
- package/dist/shared/wire/LambderApiRefusal.d.ts +39 -27
- package/dist/shared/wire/LambderApiRefusal.js +36 -7
- package/dist/shared/wire/LambderApiSignature.d.ts +18 -22
- package/dist/shared/wire/LambderApiSignature.js +16 -19
- package/dist/shared/wire/LambderCallOptions.d.ts +38 -47
- package/dist/shared/wire/LambderCallOptions.js +9 -11
- package/dist/shared/wire/LambderCompressionCodec.d.ts +29 -34
- package/dist/shared/wire/LambderCompressionCodec.js +31 -36
- package/dist/shared/wire/LambderCompressionOption.d.ts +9 -9
- package/dist/shared/wire/LambderCompressionOption.js +9 -9
- package/dist/shared/wire/LambderCrashDetail.d.ts +12 -15
- package/dist/shared/wire/LambderCrashDetail.js +12 -15
- package/dist/shared/wire/LambderDefaultApiPath.d.ts +6 -0
- package/dist/shared/wire/LambderDefaultApiPath.js +6 -0
- package/dist/shared/wire/LambderHttpStatus.d.ts +6 -7
- package/dist/shared/wire/LambderIdempotencyKeyScope.d.ts +89 -0
- package/dist/shared/wire/LambderIdempotencyKeyScope.js +146 -0
- package/dist/shared/wire/LambderInvokeApiId.d.ts +27 -0
- package/dist/shared/wire/LambderInvokeApiId.js +27 -0
- package/dist/shared/wire/LambderOutcomeAssertions.d.ts +79 -0
- package/dist/shared/wire/LambderOutcomeAssertions.js +112 -0
- package/dist/shared/wire/LambderRequestPayload.d.ts +18 -20
- package/dist/shared/wire/LambderRequestPayload.js +4 -6
- package/dist/stores/LambderCacheFiller.d.ts +48 -0
- package/dist/stores/LambderCacheFiller.js +119 -0
- package/dist/stores/LambderCacheKeys.d.ts +26 -0
- package/dist/stores/LambderCacheKeys.js +54 -0
- package/dist/stores/LambderCacheValues.d.ts +45 -0
- package/dist/stores/LambderCacheValues.js +74 -0
- package/dist/stores/LambderDdbCache.d.ts +121 -56
- package/dist/stores/LambderDdbCache.js +528 -225
- package/dist/stores/LambderDdbIdempotencyStore.d.ts +33 -22
- package/dist/stores/LambderDdbIdempotencyStore.js +75 -50
- package/dist/stores/LambderDdbRateLimiter.d.ts +76 -20
- package/dist/stores/LambderDdbRateLimiter.js +151 -39
- package/dist/stores/LambderDdbSdk.d.ts +43 -31
- package/dist/stores/LambderDdbSdk.js +79 -33
- package/dist/stores/LambderDdbSessionStore.d.ts +27 -14
- package/dist/stores/LambderDdbSessionStore.js +119 -47
- package/dist/stores/LambderHttpFileSource.d.ts +15 -6
- package/dist/stores/LambderHttpFileSource.js +15 -13
- package/dist/stores/LambderMemoryCache.d.ts +49 -0
- package/dist/stores/LambderMemoryCache.js +113 -0
- package/dist/stores/LambderMemoryIdempotencyStore.d.ts +13 -12
- package/dist/stores/LambderMemoryIdempotencyStore.js +31 -30
- package/dist/stores/LambderMemoryRateLimiter.d.ts +8 -9
- package/dist/stores/LambderMemoryRateLimiter.js +14 -13
- package/dist/stores/LambderMemorySessionStore.d.ts +14 -11
- package/dist/stores/LambderMemorySessionStore.js +38 -19
- package/dist/stores/LambderS3FileSource.d.ts +21 -6
- package/dist/stores/LambderS3FileSource.js +12 -7
- package/dist/testing/LambderTestApp.d.ts +176 -0
- package/dist/testing/LambderTestApp.js +204 -0
- package/dist/testing/LambderTestVisitor.d.ts +153 -0
- package/dist/testing/LambderTestVisitor.js +154 -0
- package/dist/testing.d.ts +27 -0
- package/dist/testing.js +24 -0
- package/package.json +20 -3
- package/dist/api/LambderApiPolicyEngine.d.ts +0 -36
- package/dist/api/LambderApiPolicyEngine.js +0 -77
- package/dist/shared/util/LambderKeyFields.d.ts +0 -32
- package/dist/shared/util/LambderKeyFields.js +0 -34
|
@@ -1,14 +1,19 @@
|
|
|
1
1
|
import { resolveCookieDomain, serializeCookie, serializeClearCookie } from "../shared/wire/LambderCookie.js";
|
|
2
2
|
import { isMintedSessionToken } from "./LambderSessionManager.js";
|
|
3
3
|
/**
|
|
4
|
-
* No session for this request: the cookies named none,
|
|
5
|
-
* they named did not pair with the posted CSRF token
|
|
4
|
+
* No session for this request: the cookies named none, the single session
|
|
5
|
+
* they named did not pair with the posted CSRF token, they cannot be
|
|
6
|
+
* resolved to one session (LambderSessionAmbiguousError, the one case with a
|
|
7
|
+
* type of its own), or the session was ended while the request held it (a
|
|
8
|
+
* logout or a password change elsewhere, or the dataRefresh callback).
|
|
6
9
|
*
|
|
7
|
-
* Typed
|
|
8
|
-
* "
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
10
|
+
* Typed so fetchSessionIfExists can tell "no session here" from "something
|
|
11
|
+
* broke", and so can every place that answers a request which needed a
|
|
12
|
+
* session and has none (the API pipeline, a route or a hook on the server):
|
|
13
|
+
* each tests for this class, and the ambiguous case with it. Everything else
|
|
14
|
+
* (a TypeError from a custom store, a bug in a dataRefresh callback)
|
|
15
|
+
* propagates as a crash: answering sessionExpired for a defect makes the
|
|
16
|
+
* client clear its cookies and turns a bug into a logout.
|
|
12
17
|
*/
|
|
13
18
|
export class LambderSessionNotFoundError extends Error {
|
|
14
19
|
constructor(message = "Session not found") {
|
|
@@ -18,39 +23,40 @@ export class LambderSessionNotFoundError extends Error {
|
|
|
18
23
|
}
|
|
19
24
|
/**
|
|
20
25
|
* 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.
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
26
|
+
* them is used and every scope this host can write is cleared. A request with
|
|
27
|
+
* no usable session, so it is a LambderSessionNotFoundError and is answered
|
|
28
|
+
* as one wherever that is (a 401 or the session-expired route answer, the
|
|
29
|
+
* sessionExpired envelope on an API call). The subclass keeps it apart for
|
|
30
|
+
* whoever wants to tell: this case carries the clearing Set-Cookie headers
|
|
31
|
+
* that heal the state, and it is the one worth finding in a log.
|
|
25
32
|
*/
|
|
26
|
-
export class LambderSessionAmbiguousError extends
|
|
33
|
+
export class LambderSessionAmbiguousError extends LambderSessionNotFoundError {
|
|
27
34
|
constructor(message = "Session ambiguous") {
|
|
28
35
|
super(message);
|
|
29
36
|
this.name = "LambderSessionAmbiguousError";
|
|
30
37
|
}
|
|
31
38
|
}
|
|
32
|
-
/** The tokens are hex, so the cookie carries them
|
|
39
|
+
/** The tokens are hex, so the cookie carries them unencoded, in the format browsers already hold. */
|
|
33
40
|
const rawCookieValue = (value) => value;
|
|
34
41
|
/**
|
|
35
42
|
* A `__Host-` cookie is the browser's own answer to a sibling subdomain
|
|
36
43
|
* planting a session cookie at a parent domain: it refuses one that carries a
|
|
37
44
|
* Domain, so no other host can write it. `__Secure-` is the weaker sibling,
|
|
38
|
-
* accepted only on a Secure cookie. Both
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
* creation instead.
|
|
45
|
+
* accepted only on a Secure cookie. Both fail silently: a browser discards a
|
|
46
|
+
* prefixed cookie with an attribute the prefix forbids, and the app looks
|
|
47
|
+
* like it has no sessions rather than like it is misconfigured. So the
|
|
48
|
+
* combinations are rejected at creation.
|
|
43
49
|
*
|
|
44
50
|
* Session policy, so it lives beside the controller that writes the cookies
|
|
45
|
-
* rather than in the pipeline that
|
|
51
|
+
* rather than in the pipeline that calls it.
|
|
46
52
|
*/
|
|
47
53
|
export const assertSessionCookiePrefixes = (sessions) => {
|
|
48
54
|
const keys = [sessions.tokenCookieKey, sessions.csrfCookieKey];
|
|
49
55
|
const hostPrefixed = keys.filter((key) => key.startsWith("__Host-"));
|
|
50
56
|
const securePrefixed = keys.filter((key) => key.startsWith("__Secure-"));
|
|
51
57
|
if (sessions.cookieOptions.secure === false) {
|
|
52
|
-
// Both prefixes require Secure, so
|
|
53
|
-
//
|
|
58
|
+
// Both prefixes require Secure, so one check covers them; the
|
|
59
|
+
// messages stay separate so each names its own prefix.
|
|
54
60
|
if (hostPrefixed.length > 0) {
|
|
55
61
|
throw new Error(`Lambder: session cookie ${hostPrefixed.join(" and ")} uses the __Host- prefix, which a browser accepts only on a Secure cookie. ` +
|
|
56
62
|
"Drop session.cookie.secure: false, or drop the prefix; keeping both means the browser discards the cookie silently and no session is ever read.");
|
|
@@ -74,21 +80,21 @@ export const assertSessionCookiePrefixes = (sessions) => {
|
|
|
74
80
|
/**
|
|
75
81
|
* How many copies of the session cookie one request may carry before the
|
|
76
82
|
* request is treated as ambiguous. A name legitimately arrives at a few
|
|
77
|
-
* scopes
|
|
78
|
-
*
|
|
79
|
-
*
|
|
80
|
-
*
|
|
81
|
-
*
|
|
82
|
-
*
|
|
83
|
+
* scopes (a Domain change mid-migration leaves a host-only twin), and no
|
|
84
|
+
* browser has a reason to send more. Beyond this the request is refused
|
|
85
|
+
* rather than trimmed: dropping extras would let anyone who can plant cookies
|
|
86
|
+
* at a parent domain push the visitor's own copy out of the read and log them
|
|
87
|
+
* out silently, with no eviction emitted, so it would never heal. Nothing is
|
|
88
|
+
* read from the store on that path.
|
|
83
89
|
*/
|
|
84
90
|
const MAX_SESSION_TOKEN_CANDIDATES = 4;
|
|
85
91
|
/**
|
|
86
92
|
* 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
|
-
*
|
|
89
|
-
*
|
|
90
|
-
*
|
|
91
|
-
* request info
|
|
93
|
+
* cookies name onto the context, and writes the cookies a created, rotated
|
|
94
|
+
* or ended session needs into the context's response headers. Server and
|
|
95
|
+
* mock handlers reach it as ctx.sessionController (the server's also through
|
|
96
|
+
* lambder.getSessionController(ctx)); it needs only the call context and
|
|
97
|
+
* request info, so one class serves both.
|
|
92
98
|
*/
|
|
93
99
|
export default class LambderSessionController {
|
|
94
100
|
manager;
|
|
@@ -113,19 +119,23 @@ export default class LambderSessionController {
|
|
|
113
119
|
}
|
|
114
120
|
;
|
|
115
121
|
/**
|
|
116
|
-
* Both cookies at one expiry, with the raw secrets
|
|
117
|
-
* LambderCreatedSession result, in these cookies and in
|
|
118
|
-
*
|
|
119
|
-
* the raw CSRF value
|
|
120
|
-
*
|
|
121
|
-
*
|
|
122
|
+
* Both cookies at one expiry, with the raw secrets, which exist only on
|
|
123
|
+
* the LambderCreatedSession result, in these cookies and in requests that
|
|
124
|
+
* carry them back; the record stores hashes. `csrfToken` is null when this
|
|
125
|
+
* request does not know the raw CSRF value, and then only the session
|
|
126
|
+
* cookie is written: a CSRF cookie that does not pair with the session
|
|
127
|
+
* would break the session it is refreshing.
|
|
122
128
|
*/
|
|
123
129
|
writeSessionCookies(expiresAt, sessionToken, csrfToken) {
|
|
124
130
|
const scope = this.cookieScope();
|
|
125
131
|
const expires = new Date(expiresAt * 1000);
|
|
126
|
-
|
|
132
|
+
// Max-Age beside Expires: a browser that knows Max-Age counts from
|
|
133
|
+
// receipt rather than from a date, so a device whose clock runs ahead
|
|
134
|
+
// does not drop a short-lived session early.
|
|
135
|
+
const maxAge = Math.max(0, expiresAt - Math.floor(Date.now() / 1000));
|
|
136
|
+
this.ctx.responseHeaders.add("Set-Cookie", serializeCookie(this.tokenCookieKey, sessionToken, { ...scope, expires, maxAge, httpOnly: true, encode: rawCookieValue }));
|
|
127
137
|
if (csrfToken !== null)
|
|
128
|
-
this.ctx.responseHeaders.add("Set-Cookie", serializeCookie(this.csrfCookieKey, csrfToken, { ...scope, expires, encode: rawCookieValue }));
|
|
138
|
+
this.ctx.responseHeaders.add("Set-Cookie", serializeCookie(this.csrfCookieKey, csrfToken, { ...scope, expires, maxAge, encode: rawCookieValue }));
|
|
129
139
|
}
|
|
130
140
|
;
|
|
131
141
|
setSessionCookies(created) {
|
|
@@ -133,10 +143,9 @@ export default class LambderSessionController {
|
|
|
133
143
|
}
|
|
134
144
|
;
|
|
135
145
|
/**
|
|
136
|
-
* The deleting pair for one scope.
|
|
137
|
-
*
|
|
138
|
-
*
|
|
139
|
-
* scopes they walk.
|
|
146
|
+
* The deleting pair for one scope. A deletion only reaches a cookie with
|
|
147
|
+
* the same Domain and Path, so the pair is emitted per scope; the two
|
|
148
|
+
* callers differ only in which scopes they walk.
|
|
140
149
|
*/
|
|
141
150
|
addClearCookiePair(scope) {
|
|
142
151
|
this.ctx.responseHeaders.add("Set-Cookie", serializeClearCookie(this.tokenCookieKey, { ...scope, httpOnly: true }));
|
|
@@ -150,10 +159,10 @@ export default class LambderSessionController {
|
|
|
150
159
|
/**
|
|
151
160
|
* Every Domain this host is allowed to write the session cookies at: the
|
|
152
161
|
* host-only scope, the configured one, and each parent domain of the
|
|
153
|
-
* request host. A deletion matches only a cookie
|
|
154
|
-
*
|
|
155
|
-
*
|
|
156
|
-
*
|
|
162
|
+
* request host. A deletion matches only a cookie with the same Domain, so
|
|
163
|
+
* evicting a copy the app never set needs all of them. A browser ignores
|
|
164
|
+
* a Domain it will not accept, so a registry-owned suffix can be offered
|
|
165
|
+
* without checking a public-suffix list.
|
|
157
166
|
*/
|
|
158
167
|
cookieClearDomains() {
|
|
159
168
|
const hostname = (this.request.host.split(":")[0] ?? "").toLowerCase();
|
|
@@ -174,13 +183,12 @@ export default class LambderSessionController {
|
|
|
174
183
|
}
|
|
175
184
|
;
|
|
176
185
|
/**
|
|
177
|
-
* Clears the session cookies at every scope this host can reach,
|
|
178
|
-
*
|
|
179
|
-
*
|
|
180
|
-
*
|
|
181
|
-
*
|
|
182
|
-
*
|
|
183
|
-
* it.
|
|
186
|
+
* Clears the session cookies at every scope this host can reach, not just
|
|
187
|
+
* the configured one. Used when a request carries more than one live
|
|
188
|
+
* session: the copy that has to go may sit at a parent domain where a
|
|
189
|
+
* sibling host planted it, and clearing only the configured scope would
|
|
190
|
+
* evict the visitor's own cookie and leave the planted one as the sole
|
|
191
|
+
* survivor, completing the takeover instead of stopping it.
|
|
184
192
|
*/
|
|
185
193
|
clearSessionCookiesEverywhere() {
|
|
186
194
|
const base = this.cookieScope(true);
|
|
@@ -191,10 +199,8 @@ export default class LambderSessionController {
|
|
|
191
199
|
;
|
|
192
200
|
/**
|
|
193
201
|
* Refuses a request whose session cookies cannot be resolved to one
|
|
194
|
-
* session, clearing every scope this host can write
|
|
195
|
-
*
|
|
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.
|
|
202
|
+
* session, clearing every scope this host can write (see
|
|
203
|
+
* clearSessionCookiesEverywhere for why not just the configured one).
|
|
198
204
|
*
|
|
199
205
|
* Path is the one dimension this cannot sweep: the request info carries
|
|
200
206
|
* no path, and a deletion matches only a cookie at the same Path, so a
|
|
@@ -214,12 +220,11 @@ export default class LambderSessionController {
|
|
|
214
220
|
* Both session cookie names read in one pass: every well-formed value
|
|
215
221
|
* under the token name, and every value under the CSRF name.
|
|
216
222
|
*
|
|
217
|
-
* The CSRF cookie is counted
|
|
218
|
-
* is
|
|
219
|
-
*
|
|
220
|
-
*
|
|
221
|
-
*
|
|
222
|
-
* first. So a sibling host that plants one CSRF cookie at a parent domain
|
|
223
|
+
* The CSRF cookie is counted, not just checked against a token, because
|
|
224
|
+
* it is plantable like the session cookie and the browser picks between
|
|
225
|
+
* copies silently: the client reads it with js-cookie's Cookies.get, which
|
|
226
|
+
* returns the FIRST copy in document.cookie, and browsers order a longer
|
|
227
|
+
* Path first. A sibling host that plants a CSRF cookie at a parent domain
|
|
223
228
|
* with a deeper Path decides which token every call posts, and the count
|
|
224
229
|
* is the only thing that shows it.
|
|
225
230
|
*/
|
|
@@ -227,9 +232,8 @@ export default class LambderSessionController {
|
|
|
227
232
|
const wellFormed = (this.request.cookies[this.tokenCookieKey] ?? []).filter(isMintedSessionToken);
|
|
228
233
|
// Deduplicated: one value arriving twice (a proxy that appends rather
|
|
229
234
|
// than merges Cookie, the same value set at two scopes) is one
|
|
230
|
-
// session,
|
|
231
|
-
//
|
|
232
|
-
// something to trim away (see MAX_SESSION_TOKEN_CANDIDATES).
|
|
235
|
+
// session, not an ambiguity. Not truncated: a count past the cap is
|
|
236
|
+
// itself the answer (see MAX_SESSION_TOKEN_CANDIDATES).
|
|
233
237
|
return {
|
|
234
238
|
sessionTokens: [...new Set(wellFormed)],
|
|
235
239
|
csrfTokens: [...new Set(this.request.cookies[this.csrfCookieKey] ?? [])],
|
|
@@ -250,9 +254,9 @@ export default class LambderSessionController {
|
|
|
250
254
|
}
|
|
251
255
|
;
|
|
252
256
|
/**
|
|
253
|
-
* createSession, handing back the raw tokens beside the session
|
|
254
|
-
* test or
|
|
255
|
-
*
|
|
257
|
+
* createSession, handing back the raw tokens beside the session, for a
|
|
258
|
+
* test or mock runtime that plants the cookies somewhere other than this
|
|
259
|
+
* call's response (a cookie jar).
|
|
256
260
|
*/
|
|
257
261
|
async issueSession(sessionKey, data, ttlInSeconds) {
|
|
258
262
|
const created = await this.manager.createSession(sessionKey, data, ttlInSeconds);
|
|
@@ -266,16 +270,17 @@ export default class LambderSessionController {
|
|
|
266
270
|
}
|
|
267
271
|
;
|
|
268
272
|
/**
|
|
269
|
-
* regenerateSession, handing back the raw tokens beside the session
|
|
270
|
-
*
|
|
271
|
-
*
|
|
272
|
-
*
|
|
273
|
-
* keep calling.
|
|
273
|
+
* regenerateSession, handing back the raw tokens beside the session as
|
|
274
|
+
* issueSession does. Rotation mints a new CSRF token, and a client that
|
|
275
|
+
* holds its token rather than reading document.cookie (a native app, an
|
|
276
|
+
* invoke caller) needs the new one to keep calling.
|
|
274
277
|
*/
|
|
275
278
|
async reissueSession() {
|
|
276
279
|
if (!this.ctx.session)
|
|
277
280
|
throw new LambderSessionNotFoundError();
|
|
278
281
|
const created = await this.manager.regenerateSession(this.ctx.session);
|
|
282
|
+
if (!created)
|
|
283
|
+
this.endWithNoSession();
|
|
279
284
|
this.setSessionCookies(created);
|
|
280
285
|
this.ctx.session = created.session;
|
|
281
286
|
return created;
|
|
@@ -289,34 +294,31 @@ export default class LambderSessionController {
|
|
|
289
294
|
if (candidates.length > MAX_SESSION_TOKEN_CANDIDATES) {
|
|
290
295
|
this.refuseAmbiguousSession(`more than ${MAX_SESSION_TOKEN_CANDIDATES} "${this.tokenCookieKey}" cookies arrived, at different scopes`);
|
|
291
296
|
}
|
|
292
|
-
// After the cap check
|
|
293
|
-
//
|
|
297
|
+
// After the cap check: this says the store is about to be read once
|
|
298
|
+
// per copy, and over the cap nothing is read.
|
|
294
299
|
if (candidates.length > 1) {
|
|
295
300
|
console.warn(`Lambder session: ${candidates.length} "${this.tokenCookieKey}" cookies arrived from ${this.request.host}; the browser holds the cookie at several scopes. Reading each.`);
|
|
296
301
|
}
|
|
297
|
-
// How many sessions the browser
|
|
298
|
-
//
|
|
299
|
-
//
|
|
300
|
-
//
|
|
301
|
-
//
|
|
302
|
-
//
|
|
303
|
-
//
|
|
304
|
-
// would be invisible. The pairing is asked once, below, of whichever
|
|
305
|
-
// single session the cookies resolved to.
|
|
302
|
+
// How many sessions the browser holds is a question about the cookies
|
|
303
|
+
// alone, so it is asked without the CSRF pairing. With the pairing
|
|
304
|
+
// folded in the answer would always be "one": a sibling subdomain
|
|
305
|
+
// plants its own CSRF cookie beside its planted session, only one
|
|
306
|
+
// CSRF token is posted, and no two sessions share a csrfTokenHash, so
|
|
307
|
+
// the ambiguity this check exists to catch would be invisible. The
|
|
308
|
+
// pairing is checked once, below, against the single resolved session.
|
|
306
309
|
//
|
|
307
|
-
// One live session
|
|
308
|
-
//
|
|
309
|
-
//
|
|
310
|
-
//
|
|
311
|
-
//
|
|
312
|
-
//
|
|
313
|
-
//
|
|
310
|
+
// One live session plus stale copies is the ordinary case (a cookie
|
|
311
|
+
// whose Domain or Path changed), and the live one wins. Two LIVE
|
|
312
|
+
// sessions under one name is not: a sibling subdomain can write a
|
|
313
|
+
// cookie at a parent domain that the browser sends alongside the real
|
|
314
|
+
// one, and taking either could sign this visitor into an account that
|
|
315
|
+
// is not theirs. There is no telling which they meant, so neither is
|
|
316
|
+
// used.
|
|
314
317
|
const live = [];
|
|
315
318
|
for (const sessionToken of candidates) {
|
|
316
|
-
// lookupSession finds the record BY the hash of this token's
|
|
317
|
-
// secret and checks
|
|
318
|
-
//
|
|
319
|
-
// against the record would only hash the same secret twice.
|
|
319
|
+
// lookupSession finds the record BY the hash of this token's
|
|
320
|
+
// secret and checks structure and expiry, so possession is proved
|
|
321
|
+
// here; re-checking the token would hash the same secret twice.
|
|
320
322
|
const candidate = await this.manager.lookupSession(sessionToken);
|
|
321
323
|
if (!candidate)
|
|
322
324
|
continue;
|
|
@@ -330,28 +332,24 @@ export default class LambderSessionController {
|
|
|
330
332
|
}
|
|
331
333
|
const found = live[0];
|
|
332
334
|
if (!found)
|
|
333
|
-
|
|
334
|
-
// The pairing check, once, against the
|
|
335
|
-
//
|
|
336
|
-
// has no session here.
|
|
335
|
+
this.endWithNoSession();
|
|
336
|
+
// The pairing check, once, against the resolved session. An API call
|
|
337
|
+
// that did not post the matching CSRF token has no session here.
|
|
337
338
|
if (this.request.csrfToken !== null && !(await this.manager.isSessionCsrfTokenValid(found.session, this.request.csrfToken))) {
|
|
338
|
-
// A session cookie
|
|
339
|
-
//
|
|
340
|
-
//
|
|
341
|
-
//
|
|
342
|
-
//
|
|
343
|
-
//
|
|
344
|
-
//
|
|
345
|
-
//
|
|
346
|
-
// the ones a sibling host planted at. So the everywhere-clear
|
|
347
|
-
// runs instead, and the state heals.
|
|
339
|
+
// A live session cookie with a posted CSRF token that pairs with
|
|
340
|
+
// nothing is the CSRF half of the planted-cookie shape, and it
|
|
341
|
+
// cannot heal on its own: the client posts the FIRST CSRF cookie
|
|
342
|
+
// in document.cookie, a longer Path sorts first, so a planted copy
|
|
343
|
+
// keeps winning through logout, the next sign-in and every call
|
|
344
|
+
// after. A plain no-session answer emits no Set-Cookie, and the
|
|
345
|
+
// client can only clear scopes it knows, not the ones a sibling
|
|
346
|
+
// host planted at. So the everywhere-clear runs instead.
|
|
348
347
|
//
|
|
349
|
-
// Only when the request
|
|
350
|
-
//
|
|
351
|
-
//
|
|
352
|
-
//
|
|
353
|
-
//
|
|
354
|
-
// can clear themselves.
|
|
348
|
+
// Only when the request carried CSRF cookies: an invoke caller
|
|
349
|
+
// posts the value in the envelope with no CSRF cookie, and its
|
|
350
|
+
// wrong token is an ordinary no-session. And only when the cookies
|
|
351
|
+
// are the suspect: a single CSRF cookie equal to the posted token
|
|
352
|
+
// that does not pair is a stale pair the visitor can clear.
|
|
355
353
|
if (csrfTokens.length > 1) {
|
|
356
354
|
this.refuseAmbiguousSession(`more than one "${this.csrfCookieKey}" cookie arrived, at different scopes, and the one posted pairs with no session`);
|
|
357
355
|
}
|
|
@@ -360,33 +358,32 @@ export default class LambderSessionController {
|
|
|
360
358
|
}
|
|
361
359
|
throw new LambderSessionNotFoundError();
|
|
362
360
|
}
|
|
363
|
-
// Renewed only
|
|
364
|
-
//
|
|
361
|
+
// Renewed only once this session is known to be the caller's: a
|
|
362
|
+
// slide or a dataRefresh is a write on their behalf.
|
|
365
363
|
const expiresBefore = found.session.expiresAt;
|
|
366
364
|
const session = await this.manager.renewSession(found.session);
|
|
365
|
+
// Ended while this request read it (a logout, a password change), or
|
|
366
|
+
// by its own dataRefresh: no session either way.
|
|
367
367
|
if (!session)
|
|
368
|
-
|
|
369
|
-
// The other copies are stale. This response can evict the
|
|
370
|
-
//
|
|
371
|
-
//
|
|
368
|
+
this.endWithNoSession();
|
|
369
|
+
// The other copies are stale. This response can evict the host-only
|
|
370
|
+
// twin of a Domain= cookie; a copy at a parent domain this host
|
|
371
|
+
// cannot name is out of reach and expires on its own.
|
|
372
372
|
//
|
|
373
|
-
// Which copy is
|
|
374
|
-
//
|
|
375
|
-
//
|
|
376
|
-
//
|
|
377
|
-
//
|
|
378
|
-
//
|
|
379
|
-
// converge on the first request rather than on the first slide.
|
|
373
|
+
// Which copy is stale is an assumption: the request carries no scope,
|
|
374
|
+
// so the twin being deleted may be the very cookie this read resolved
|
|
375
|
+
// (a visitor who signed in before the app configured a domain). So the
|
|
376
|
+
// eviction always ships with the replacement at the configured scope,
|
|
377
|
+
// and the visitor stays signed in either way. It also lets a domain
|
|
378
|
+
// migration converge on the first request rather than the first slide.
|
|
380
379
|
const evictsHostOnlyTwin = candidates.length > 1 && !!this.cookieScope().domain;
|
|
381
380
|
if (evictsHostOnlyTwin)
|
|
382
381
|
this.clearSessionCookies(true);
|
|
383
|
-
// A sliding write moved the record's expiry, so the cookies
|
|
384
|
-
//
|
|
385
|
-
//
|
|
386
|
-
//
|
|
387
|
-
//
|
|
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.
|
|
382
|
+
// A sliding write moved the record's expiry, so the cookies move with
|
|
383
|
+
// it. Otherwise the browser keeps the creation-time Expires and signs
|
|
384
|
+
// out an active visitor at createdAt + ttl, the very deadline sliding
|
|
385
|
+
// expiration exists to push back. Throttled with the write, so an
|
|
386
|
+
// active session re-issues its cookies at most that often.
|
|
390
387
|
if (evictsHostOnlyTwin || session.expiresAt !== expiresBefore)
|
|
391
388
|
await this.slideSessionCookies(session, found.token, csrfTokens);
|
|
392
389
|
this.ctx.session = session;
|
|
@@ -395,17 +392,16 @@ export default class LambderSessionController {
|
|
|
395
392
|
;
|
|
396
393
|
/**
|
|
397
394
|
* Re-issues both cookies at this session's expiry: after a sliding write
|
|
398
|
-
* moved it, and beside the host-only eviction
|
|
399
|
-
*
|
|
395
|
+
* moved it, and beside the host-only eviction, which would otherwise
|
|
396
|
+
* delete a cookie without replacing it.
|
|
400
397
|
*
|
|
401
|
-
*
|
|
402
|
-
* check
|
|
403
|
-
*
|
|
404
|
-
*
|
|
405
|
-
*
|
|
406
|
-
*
|
|
407
|
-
*
|
|
408
|
-
* session survives.
|
|
398
|
+
* On an API call the raw CSRF value is the posted one, which the pairing
|
|
399
|
+
* check just matched. A route posts none, so the single arriving CSRF
|
|
400
|
+
* cookie stands in, but only once it is known to pair: re-issuing an
|
|
401
|
+
* unpaired value would overwrite the visitor's real CSRF cookie with a
|
|
402
|
+
* planted one at the app's own scope, the takeover the scan exists to
|
|
403
|
+
* prevent. Where neither is available the session cookie slides alone;
|
|
404
|
+
* it is the half that decides whether the session survives.
|
|
409
405
|
*/
|
|
410
406
|
async slideSessionCookies(session, sessionToken, csrfTokens) {
|
|
411
407
|
const posted = this.request.csrfToken;
|
|
@@ -418,49 +414,72 @@ export default class LambderSessionController {
|
|
|
418
414
|
this.writeSessionCookies(session.expiresAt, sessionToken, paired ? only : null);
|
|
419
415
|
}
|
|
420
416
|
;
|
|
417
|
+
/**
|
|
418
|
+
* "No session" for a request whose session cookie names none, or whose
|
|
419
|
+
* session ended while it held it: the context holds none.
|
|
420
|
+
*
|
|
421
|
+
* The cookies are left alone. A deletion matches a cookie by name, not by
|
|
422
|
+
* value, so clearing here would also delete a session another response
|
|
423
|
+
* has just set: a poll sent with the old cookie, answering after a login,
|
|
424
|
+
* a rotation or a password change, would sign the person straight out of
|
|
425
|
+
* the new session. It also keeps the client's own check working, which
|
|
426
|
+
* clears the CSRF cookie only while it still holds the token the call
|
|
427
|
+
* sent. A dead cookie costs a store read per request until it expires.
|
|
428
|
+
*/
|
|
429
|
+
endWithNoSession() {
|
|
430
|
+
this.ctx.session = null;
|
|
431
|
+
throw new LambderSessionNotFoundError();
|
|
432
|
+
}
|
|
433
|
+
;
|
|
421
434
|
async fetchSessionIfExists() {
|
|
422
435
|
try {
|
|
423
436
|
return await this.fetchSession();
|
|
424
437
|
}
|
|
425
438
|
catch (err) {
|
|
426
|
-
// Only
|
|
427
|
-
// dataRefresh
|
|
428
|
-
// unexpected
|
|
429
|
-
//
|
|
430
|
-
//
|
|
431
|
-
// logout and the log would say nothing happened.
|
|
439
|
+
// Only a "no session" exit becomes null, the ambiguous one
|
|
440
|
+
// included. A failing dataRefresh, a store read failure and
|
|
441
|
+
// anything unexpected propagate: answering sessionExpired for a
|
|
442
|
+
// defect makes the client clear its cookies, so a crash would
|
|
443
|
+
// present as a logout with nothing in the log.
|
|
432
444
|
if (err instanceof LambderSessionNotFoundError)
|
|
433
445
|
return null;
|
|
434
|
-
if (err instanceof LambderSessionAmbiguousError)
|
|
435
|
-
return null;
|
|
436
446
|
throw err;
|
|
437
447
|
}
|
|
438
448
|
}
|
|
439
449
|
;
|
|
450
|
+
/**
|
|
451
|
+
* Writes new data onto the current session. Throws
|
|
452
|
+
* LambderSessionNotFoundError when the session was ended while this
|
|
453
|
+
* request held it: the write does not bring it back, and an API call
|
|
454
|
+
* answers sessionExpired.
|
|
455
|
+
*/
|
|
440
456
|
async updateSessionData(newData) {
|
|
441
457
|
if (!this.ctx.session)
|
|
442
458
|
throw new LambderSessionNotFoundError();
|
|
443
|
-
|
|
444
|
-
|
|
459
|
+
const updated = await this.manager.updateSessionData(this.ctx.session, newData);
|
|
460
|
+
if (!updated)
|
|
461
|
+
this.endWithNoSession();
|
|
462
|
+
this.ctx.session = updated;
|
|
463
|
+
return updated;
|
|
445
464
|
}
|
|
446
465
|
;
|
|
447
466
|
/**
|
|
448
|
-
* Force-runs the dataRefresh callback now (see the session option of
|
|
449
|
-
* persists the result onto the current session.
|
|
450
|
-
*
|
|
451
|
-
* the
|
|
467
|
+
* Force-runs the dataRefresh callback now (see the session option of
|
|
468
|
+
* create) and persists the result onto the current session. Throws
|
|
469
|
+
* LambderSessionNotFoundError when the session is over, because the
|
|
470
|
+
* callback ended it (the record is deleted) or because it was ended while
|
|
471
|
+
* this request held it: the same "no session" a read gives for either,
|
|
472
|
+
* with the cookies left alone (see endWithNoSession), and what an API
|
|
473
|
+
* call answers as sessionExpired.
|
|
452
474
|
*/
|
|
453
475
|
async refreshSessionData() {
|
|
454
476
|
if (!this.ctx.session)
|
|
455
477
|
throw new LambderSessionNotFoundError();
|
|
456
478
|
const refreshed = await this.manager.refreshSessionData(this.ctx.session);
|
|
457
|
-
if (!refreshed)
|
|
458
|
-
this.
|
|
459
|
-
this.ctx.session = null;
|
|
460
|
-
return null;
|
|
461
|
-
}
|
|
479
|
+
if (!refreshed)
|
|
480
|
+
this.endWithNoSession();
|
|
462
481
|
this.ctx.session = refreshed;
|
|
463
|
-
return
|
|
482
|
+
return refreshed;
|
|
464
483
|
}
|
|
465
484
|
;
|
|
466
485
|
/**
|
|
@@ -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,12 +23,14 @@ export interface LambderSessionCrypto {
|
|
|
23
23
|
*/
|
|
24
24
|
readonly isCryptographic: boolean;
|
|
25
25
|
sha256Hex(value: string): Promise<string>;
|
|
26
|
+
/** HMAC-SHA256 of `value` under `key` (both UTF-8), as hex: the salted partition hash of a sessionKey. */
|
|
27
|
+
hmacSha256Hex(key: string, value: string): Promise<string>;
|
|
26
28
|
randomHex(bytes: number): Promise<string>;
|
|
27
29
|
constantTimeEqual(a: string, b: string): boolean;
|
|
28
30
|
}
|
|
29
31
|
/** True when this runtime offers WebCrypto's subtle API (secure contexts in browsers; Node 20+). */
|
|
30
32
|
export declare const isWebCryptoAvailable: () => boolean;
|
|
31
|
-
/** sha256 through crypto.subtle and randomness through getRandomValues: the default. */
|
|
33
|
+
/** sha256 and HMAC through crypto.subtle and randomness through getRandomValues: the default. */
|
|
32
34
|
export declare class LambderWebCrypto implements LambderSessionCrypto {
|
|
33
35
|
readonly isCryptographic = true;
|
|
34
36
|
private cryptoPromise;
|
|
@@ -42,14 +44,15 @@ export declare class LambderWebCrypto implements LambderSessionCrypto {
|
|
|
42
44
|
* The runtime's WebCrypto, through the resolver every layer shares, with
|
|
43
45
|
* Node's crypto warmed alongside it.
|
|
44
46
|
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
* LambderPlainSessionCrypto and a memory store, which
|
|
49
|
-
*
|
|
47
|
+
* Availability is checked here, before the shared resolver, so the error
|
|
48
|
+
* can name this layer's own way out: a runtime with neither a global
|
|
49
|
+
* crypto nor Node's webcrypto can still run sessions over
|
|
50
|
+
* LambderPlainSessionCrypto and a memory store, which the shared
|
|
51
|
+
* resolver's message cannot know about.
|
|
50
52
|
*/
|
|
51
53
|
private ready;
|
|
52
54
|
sha256Hex(value: string): Promise<string>;
|
|
55
|
+
hmacSha256Hex(key: string, value: string): Promise<string>;
|
|
53
56
|
randomHex(bytes: number): Promise<string>;
|
|
54
57
|
constantTimeEqual(a: string, b: string): boolean;
|
|
55
58
|
}
|
|
@@ -61,6 +64,12 @@ export declare class LambderWebCrypto implements LambderSessionCrypto {
|
|
|
61
64
|
export declare class LambderPlainSessionCrypto implements LambderSessionCrypto {
|
|
62
65
|
readonly isCryptographic = false;
|
|
63
66
|
sha256Hex(value: string): Promise<string>;
|
|
67
|
+
/**
|
|
68
|
+
* The key and the value hex-encoded as a JSON pair rather than run
|
|
69
|
+
* together, so the pair stays unambiguous the way a keyed hash keeps it:
|
|
70
|
+
* no key and value can pass for another split of the same text.
|
|
71
|
+
*/
|
|
72
|
+
hmacSha256Hex(key: string, value: string): Promise<string>;
|
|
64
73
|
randomHex(bytes: number): Promise<string>;
|
|
65
74
|
constantTimeEqual(a: string, b: string): boolean;
|
|
66
75
|
}
|