lambder 14.1.1 → 15.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 +429 -0
- package/README.md +14 -17
- package/dist/api/LambderApiDeclarations.d.ts +11 -5
- package/dist/api/LambderApiDefinition.d.ts +2 -1
- package/dist/api/LambderApiEnvelope.d.ts +2 -2
- package/dist/api/LambderApiEnvelope.js +8 -2
- package/dist/api/LambderApiGuards.d.ts +44 -5
- package/dist/api/LambderApiGuards.js +27 -0
- package/dist/api/LambderApiIdempotency.d.ts +14 -7
- package/dist/api/LambderApiIdempotency.js +39 -27
- package/dist/api/LambderApiPipeline.d.ts +25 -11
- package/dist/api/LambderApiPipeline.js +34 -17
- package/dist/api/LambderApiRateLimits.d.ts +28 -21
- package/dist/api/LambderApiRateLimits.js +55 -35
- package/dist/api/LambderApiRefusals.d.ts +10 -4
- package/dist/api/LambderApiRefusals.js +31 -5
- package/dist/api/LambderApiRequest.d.ts +21 -1
- package/dist/api/LambderApiRequest.js +31 -2
- package/dist/build/checkApiRefusals.d.ts +8 -3
- package/dist/build/checkApiRefusals.js +80 -29
- package/dist/build/contractPrinterProcess.d.ts +11 -0
- package/dist/build/contractPrinterProcess.js +17 -0
- package/dist/build/generateApiFiles.d.ts +26 -5
- package/dist/build/generateApiFiles.js +68 -7
- package/dist/build/writeApiSchemas.d.ts +109 -0
- package/dist/build/writeApiSchemas.js +308 -0
- package/dist/build.d.ts +3 -1
- package/dist/build.js +2 -0
- package/dist/client/LambderCaller.d.ts +25 -11
- package/dist/client/LambderCaller.js +57 -49
- package/dist/client/LambderUploadRunner.d.ts +55 -27
- package/dist/client/LambderUploadRunner.js +89 -34
- package/dist/client.d.ts +6 -6
- package/dist/client.js +2 -2
- package/dist/core/Lambder.d.ts +31 -21
- package/dist/core/Lambder.js +79 -23
- package/dist/core/LambderCallSummary.d.ts +1 -1
- package/dist/core/LambderContext.d.ts +59 -7
- package/dist/core/LambderContext.js +31 -15
- package/dist/core/LambderCrashHandling.d.ts +5 -0
- package/dist/core/LambderCrashHandling.js +11 -1
- package/dist/core/LambderCreateOptions.d.ts +28 -17
- package/dist/core/LambderCreateOptions.js +14 -7
- package/dist/core/LambderResolver.d.ts +3 -2
- package/dist/core/LambderResponseBuilder.d.ts +20 -9
- package/dist/core/LambderResponseBuilder.js +39 -8
- package/dist/core/LambderTemplatingEngine.d.ts +24 -6
- package/dist/core/LambderTemplatingEngine.js +23 -3
- package/dist/index.d.ts +15 -12
- package/dist/index.js +7 -5
- package/dist/invoke/LambderInvokeCaller.d.ts +25 -4
- package/dist/invoke/LambderInvokeCaller.js +47 -9
- package/dist/invoke/LambderInvokeOutcome.d.ts +7 -6
- package/dist/mock/LambderMockAdapterDoor.d.ts +40 -0
- package/dist/mock/LambderMockAdapterDoor.js +10 -0
- package/dist/mock/LambderMockApiSchemas.d.ts +28 -0
- package/dist/mock/LambderMockApiSchemas.js +150 -0
- package/dist/mock/LambderMockApp.d.ts +56 -48
- package/dist/mock/LambderMockApp.js +107 -62
- package/dist/mock/LambderMockBrowserCookies.js +18 -5
- package/dist/mock/LambderMockCreateOptions.d.ts +51 -4
- package/dist/mock/LambderMockFailureInjector.d.ts +12 -3
- package/dist/mock/LambderMockFailureInjector.js +12 -8
- package/dist/mock/LambderMockTypes.d.ts +8 -4
- package/dist/mock/lambderMockInvokeTransport.js +2 -9
- package/dist/mock/lambderMockMswHandler.d.ts +19 -13
- package/dist/mock/lambderMockMswHandler.js +25 -22
- package/dist/mock.d.ts +1 -0
- package/dist/secrets/LambderOneShotSecrets.d.ts +1 -1
- package/dist/secrets/LambderOneShotSecrets.js +6 -36
- package/dist/secrets/LambderPasswordHasher.d.ts +56 -0
- package/dist/secrets/LambderPasswordHasher.js +168 -0
- package/dist/session/LambderSessionController.d.ts +37 -4
- package/dist/session/LambderSessionController.js +36 -6
- package/dist/session/LambderSessionManager.d.ts +33 -3
- package/dist/session/LambderSessionManager.js +55 -16
- package/dist/shared/LambderHtml.d.ts +23 -4
- package/dist/shared/LambderHtml.js +17 -5
- package/dist/shared/LambderI18n.d.ts +106 -16
- package/dist/shared/LambderI18n.js +259 -74
- package/dist/shared/contracts/LambderCache.d.ts +14 -1
- package/dist/shared/contracts/LambderRateLimiter.d.ts +5 -6
- package/dist/shared/transport/LambderApiTransport.d.ts +3 -2
- package/dist/shared/util/LambderBackoffTimer.d.ts +24 -13
- package/dist/shared/util/LambderBackoffTimer.js +40 -12
- package/dist/shared/util/LambderCallAbort.d.ts +22 -6
- package/dist/shared/util/LambderCallAbort.js +37 -9
- package/dist/shared/util/LambderErrorReportMark.d.ts +4 -0
- package/dist/shared/util/LambderErrorReportMark.js +22 -0
- package/dist/shared/util/LambderKeyFieldDigest.d.ts +46 -0
- package/dist/shared/util/LambderKeyFieldDigest.js +75 -0
- package/dist/shared/util/LambderSignedClaims.d.ts +19 -0
- package/dist/shared/util/LambderSignedClaims.js +54 -0
- package/dist/shared/util/LambderSwappableInstances.d.ts +29 -0
- package/dist/shared/util/LambderSwappableInstances.js +54 -0
- package/dist/shared/util/LambderTestingDoors.d.ts +6 -2
- package/dist/shared/util/LambderTestingDoors.js +6 -2
- package/dist/shared/util/LambderTextDigest.d.ts +5 -5
- package/dist/shared/util/LambderTextDigest.js +5 -5
- package/dist/shared/wire/LambderApiContract.d.ts +1 -1
- package/dist/shared/wire/LambderApiOptionEntries.d.ts +9 -1
- package/dist/shared/wire/LambderApiOptionEntries.js +15 -0
- package/dist/shared/wire/LambderApiOptionValues.d.ts +3 -2
- package/dist/shared/wire/LambderApiOutcome.d.ts +37 -19
- package/dist/shared/wire/LambderApiOutcome.js +1 -1
- package/dist/shared/wire/LambderApiRefusal.d.ts +39 -12
- package/dist/shared/wire/LambderApiRefusal.js +3 -3
- package/dist/shared/wire/LambderApiSchemaEntries.d.ts +21 -0
- package/dist/shared/wire/LambderApiSchemaEntries.js +18 -0
- package/dist/shared/wire/LambderCallOptions.d.ts +3 -3
- package/dist/shared/wire/LambderCallOutcome.d.ts +2 -1
- package/dist/shared/wire/LambderCallOutcome.js +3 -2
- package/dist/shared/wire/LambderIdempotencyKeyScope.d.ts +5 -3
- package/dist/shared/wire/LambderIdempotencyKeyScope.js +5 -3
- package/dist/shared/wire/LambderInvokeApiId.d.ts +3 -3
- package/dist/shared/wire/LambderInvokeApiId.js +3 -3
- package/dist/shared/wire/LambderOutcomeAssertions.d.ts +7 -6
- package/dist/shared/wire/LambderOutcomeAssertions.js +1 -1
- package/dist/stores/LambderCacheValues.d.ts +2 -5
- package/dist/stores/LambderDdbCache.d.ts +15 -9
- package/dist/stores/LambderDdbCache.js +25 -6
- package/dist/stores/LambderDdbIdempotencyStore.d.ts +6 -4
- package/dist/stores/LambderDdbIdempotencyStore.js +32 -52
- package/dist/stores/LambderDdbOneShotSecretStore.d.ts +11 -0
- package/dist/stores/LambderDdbOneShotSecretStore.js +17 -5
- package/dist/stores/LambderDdbSdk.d.ts +59 -29
- package/dist/stores/LambderDdbSdk.js +116 -44
- package/dist/stores/LambderDdbSessionStore.d.ts +26 -11
- package/dist/stores/LambderDdbSessionStore.js +133 -68
- package/dist/stores/LambderMemoryCache.d.ts +2 -2
- package/dist/stores/LambderS3FileSource.d.ts +3 -2
- package/dist/stores/LambderS3FileSource.js +11 -13
- package/dist/stores/LambderS3UploadBucket.d.ts +12 -0
- package/dist/stores/LambderS3UploadBucket.js +23 -0
- package/dist/stores/LambderStorageBackedCache.d.ts +2 -2
- package/dist/stores/LambderStoredText.d.ts +26 -0
- package/dist/stores/LambderStoredText.js +25 -0
- package/dist/testing/LambderConformanceRunner.d.ts +9 -2
- package/dist/testing/LambderConformanceRunner.js +9 -2
- package/dist/testing/LambderTestApp.d.ts +65 -7
- package/dist/testing/LambderTestApp.js +101 -6
- package/dist/testing/LambderTestVisitor.d.ts +43 -12
- package/dist/testing/LambderTestVisitor.js +35 -11
- package/dist/testing/bootLambdaPackage.d.ts +105 -0
- package/dist/testing/bootLambdaPackage.js +265 -0
- package/dist/testing/lambdaPackageBootProcess.d.ts +39 -0
- package/dist/testing/lambdaPackageBootProcess.js +130 -0
- package/dist/testing/lambderCacheConformance.d.ts +26 -0
- package/dist/testing/lambderCacheConformance.js +287 -0
- package/dist/testing/lambderCacheStorageConformance.d.ts +25 -0
- package/dist/testing/lambderCacheStorageConformance.js +159 -0
- package/dist/testing/lambderSessionStoreConformance.d.ts +3 -3
- package/dist/testing/lambderSessionStoreConformance.js +3 -3
- package/dist/testing.d.ts +7 -1
- package/dist/testing.js +5 -0
- package/package.json +1 -6
- package/dist/shared/util/boundKeyField.d.ts +0 -20
- package/dist/shared/util/boundKeyField.js +0 -34
|
@@ -1,33 +1,61 @@
|
|
|
1
1
|
import { assertNumberAtLeast } from "./LambderOptionChecks.js";
|
|
2
|
+
/**
|
|
3
|
+
* The longest delay setTimeout keeps: a longer one overflows its 32-bit
|
|
4
|
+
* millisecond count and fires at once, in browsers and in Node alike, so a
|
|
5
|
+
* ladder that reached past it would retry with no pause at all.
|
|
6
|
+
*/
|
|
7
|
+
const LONGEST_TIMER_DELAY_MS = 2_147_483_647;
|
|
2
8
|
export class LambderBackoffTimer {
|
|
3
9
|
baseMs;
|
|
4
10
|
maxMs;
|
|
5
11
|
factor;
|
|
6
12
|
jitter;
|
|
7
|
-
|
|
13
|
+
retriesSinceReset = 0;
|
|
8
14
|
timer = null;
|
|
9
15
|
/** Settles the promise of a `wait` that cancel() or a replacement drops, so no `await` is left hanging. */
|
|
10
16
|
dropPending = null;
|
|
11
17
|
constructor(options = {}) {
|
|
12
|
-
|
|
18
|
+
const baseMs = options.baseMs ?? 1_000;
|
|
19
|
+
// Every rung is a multiple of the base, so a base of 0 is a retry
|
|
20
|
+
// loop with no pause.
|
|
21
|
+
if (typeof baseMs !== "number" || !(baseMs > 0) || baseMs > LONGEST_TIMER_DELAY_MS) {
|
|
22
|
+
throw new Error(`Lambder: LambderBackoffTimer baseMs must be a number above 0 and at most ${LONGEST_TIMER_DELAY_MS}, got ${String(baseMs)}: every wait is a multiple of it, so 0 retries with no pause, and setTimeout fires a longer delay at once.`);
|
|
23
|
+
}
|
|
24
|
+
this.baseMs = baseMs;
|
|
13
25
|
this.maxMs = assertNumberAtLeast(options.maxMs ?? Math.max(60_000, this.baseMs), this.baseMs, "maxMs");
|
|
26
|
+
if (this.maxMs > LONGEST_TIMER_DELAY_MS) {
|
|
27
|
+
throw new Error(`Lambder: LambderBackoffTimer maxMs must be at most ${LONGEST_TIMER_DELAY_MS}, got ${this.maxMs}: setTimeout fires a longer delay at once, so the top of the ladder would retry with no pause.`);
|
|
28
|
+
}
|
|
14
29
|
this.factor = assertNumberAtLeast(options.factor ?? 2, 1, "factor");
|
|
15
|
-
|
|
30
|
+
const jitter = options.jitter ?? "full";
|
|
31
|
+
if (jitter !== "full" && jitter !== "none") {
|
|
32
|
+
throw new Error(`Lambder: LambderBackoffTimer jitter must be "full" or "none", got ${JSON.stringify(jitter)}.`);
|
|
33
|
+
}
|
|
34
|
+
this.jitter = jitter;
|
|
16
35
|
}
|
|
17
36
|
/** True while a wait of any kind is pending. False again by the time it runs. */
|
|
18
37
|
get pending() {
|
|
19
38
|
return this.timer !== null;
|
|
20
39
|
}
|
|
21
40
|
/**
|
|
22
|
-
*
|
|
23
|
-
*
|
|
41
|
+
* How many retries `retry` and `wait` have scheduled since the last reset
|
|
42
|
+
* (or since the timer was built), a dropped one included: the rung the
|
|
43
|
+
* next one climbs from. A loop that gives up after so many retries reads
|
|
44
|
+
* it here rather than keeping a counter of its own.
|
|
45
|
+
*/
|
|
46
|
+
get retries() {
|
|
47
|
+
return this.retriesSinceReset;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Runs `run` after the next rung of the ladder, counting one more retry.
|
|
51
|
+
* Replaces whatever was waiting.
|
|
24
52
|
*/
|
|
25
53
|
retry(run) {
|
|
26
54
|
this.schedule(this.nextRung(), run, null);
|
|
27
55
|
}
|
|
28
56
|
/**
|
|
29
|
-
* Resolves after the next rung of the ladder, counting one more
|
|
30
|
-
*
|
|
57
|
+
* Resolves after the next rung of the ladder, counting one more retry:
|
|
58
|
+
* the `retry` for code that awaits rather than calls back.
|
|
31
59
|
* Replaces whatever was waiting. Rejects at once with the signal's reason
|
|
32
60
|
* when `signal` aborts, and with an Error when cancel() or a later wait
|
|
33
61
|
* drops it before it ran, so an await on it always settles.
|
|
@@ -52,9 +80,9 @@ export class LambderBackoffTimer {
|
|
|
52
80
|
after(delayMs, run) {
|
|
53
81
|
this.schedule(delayMs, run, null);
|
|
54
82
|
}
|
|
55
|
-
/** The attempt worked: the next failure waits the shortest time again. A pending wait is left alone. */
|
|
83
|
+
/** The attempt worked: the next failure waits the shortest time again, and `retries` is 0. A pending wait is left alone. */
|
|
56
84
|
reset() {
|
|
57
|
-
this.
|
|
85
|
+
this.retriesSinceReset = 0;
|
|
58
86
|
}
|
|
59
87
|
/** Drops the pending wait (the caller is trying right now, or going away), keeping the count. */
|
|
60
88
|
cancel() {
|
|
@@ -66,10 +94,10 @@ export class LambderBackoffTimer {
|
|
|
66
94
|
this.dropPending = null;
|
|
67
95
|
drop?.();
|
|
68
96
|
}
|
|
69
|
-
/** The next wait on the ladder, in milliseconds, counting one more
|
|
97
|
+
/** The next wait on the ladder, in milliseconds, counting one more retry. */
|
|
70
98
|
nextRung() {
|
|
71
|
-
const ceiling = Math.min(this.baseMs * this.factor ** this.
|
|
72
|
-
this.
|
|
99
|
+
const ceiling = Math.min(this.baseMs * this.factor ** this.retriesSinceReset, this.maxMs - this.baseMs);
|
|
100
|
+
this.retriesSinceReset += 1;
|
|
73
101
|
return this.baseMs + (this.jitter === "full" ? Math.random() : 1) * ceiling;
|
|
74
102
|
}
|
|
75
103
|
schedule(delayMs, run, drop) {
|
|
@@ -9,13 +9,22 @@
|
|
|
9
9
|
* the two from drifting apart: a caller that believed a late answer would
|
|
10
10
|
* report `ok: true` for a call its site had already abandoned.
|
|
11
11
|
*
|
|
12
|
+
* Who gave up matters as much as when. A call's own timeout is a failure the
|
|
13
|
+
* callers report; the site's own signal is the site's choice (a superseded
|
|
14
|
+
* read, a view that closed), which nothing should report as something gone
|
|
15
|
+
* wrong, so the two are told apart here, once. A site signal can carry a
|
|
16
|
+
* deadline of its own, though: AbortSignal.timeout() aborts with a
|
|
17
|
+
* TimeoutError, alone or inside AbortSignal.any(), and a call it ends is
|
|
18
|
+
* timed out like one its own timeoutMs ended. Read as the site's choice, it
|
|
19
|
+
* would fail with nothing reporting it.
|
|
20
|
+
*
|
|
12
21
|
* The listener on an external signal is removed in detach() rather than left
|
|
13
22
|
* to `once`: a site's signal usually outlives the call (one controller per
|
|
14
23
|
* page, per view, per request), so a listener per call would accumulate on it
|
|
15
24
|
* for as long as the signal lives.
|
|
16
25
|
*/
|
|
17
|
-
/**
|
|
18
|
-
type LambderCallAbortReason = "timeout" | "
|
|
26
|
+
/** Who gave up on a call: a deadline ended it (`timeout`: its own timeoutMs, or a site signal that aborted with a TimeoutError), or the site's own signal did (`aborted`). */
|
|
27
|
+
export type LambderCallAbortReason = "timeout" | "aborted";
|
|
19
28
|
/** The reason and the error to report for a call that was given up on. */
|
|
20
29
|
type LambderCallAbortFailure = {
|
|
21
30
|
reason: LambderCallAbortReason;
|
|
@@ -29,8 +38,12 @@ export type LambderCallAbortStage = "beforeSending" | "afterAnswering";
|
|
|
29
38
|
type LambderCallAbort = {
|
|
30
39
|
/** What the transport is handed: the chained signal when a timeout is set, the site's own otherwise, and nothing when there is neither. */
|
|
31
40
|
signal: AbortSignal | undefined;
|
|
32
|
-
/**
|
|
33
|
-
|
|
41
|
+
/**
|
|
42
|
+
* Who aborted the call, or null while nothing has. A transport that
|
|
43
|
+
* rejects once the call is aborted is read by this first, whatever it
|
|
44
|
+
* rejected with: only the caller knows which of the two aborted it.
|
|
45
|
+
*/
|
|
46
|
+
abortReason: () => LambderCallAbortReason | null;
|
|
34
47
|
/**
|
|
35
48
|
* The failure to report, or null while the call still stands. Run before
|
|
36
49
|
* handing the request to the transport (a call already abandoned should
|
|
@@ -55,8 +68,11 @@ export declare const createCallAbort: (options: {
|
|
|
55
68
|
* transports a signal has nothing to cancel, and by the crash reporter's
|
|
56
69
|
* time bound (LambderCrashHandling), where the app's report runs on.
|
|
57
70
|
*
|
|
58
|
-
*
|
|
59
|
-
*
|
|
71
|
+
* A signal that aborted before the wait began rejects it at once with the
|
|
72
|
+
* signal's reason, since it fires no event for a listener added afterwards.
|
|
73
|
+
* `pending` is still attached to either way, so what it later rejects with is
|
|
74
|
+
* settled here rather than left an unhandled rejection. The listener is
|
|
75
|
+
* detached on either outcome, for the reason createCallAbort detaches its own.
|
|
60
76
|
*/
|
|
61
77
|
export declare const stopWaitingWhenAborted: <T>(pending: Promise<T>, signal: AbortSignal | undefined) => Promise<T>;
|
|
62
78
|
export {};
|
|
@@ -9,11 +9,22 @@
|
|
|
9
9
|
* the two from drifting apart: a caller that believed a late answer would
|
|
10
10
|
* report `ok: true` for a call its site had already abandoned.
|
|
11
11
|
*
|
|
12
|
+
* Who gave up matters as much as when. A call's own timeout is a failure the
|
|
13
|
+
* callers report; the site's own signal is the site's choice (a superseded
|
|
14
|
+
* read, a view that closed), which nothing should report as something gone
|
|
15
|
+
* wrong, so the two are told apart here, once. A site signal can carry a
|
|
16
|
+
* deadline of its own, though: AbortSignal.timeout() aborts with a
|
|
17
|
+
* TimeoutError, alone or inside AbortSignal.any(), and a call it ends is
|
|
18
|
+
* timed out like one its own timeoutMs ended. Read as the site's choice, it
|
|
19
|
+
* would fail with nothing reporting it.
|
|
20
|
+
*
|
|
12
21
|
* The listener on an external signal is removed in detach() rather than left
|
|
13
22
|
* to `once`: a site's signal usually outlives the call (one controller per
|
|
14
23
|
* page, per view, per request), so a listener per call would accumulate on it
|
|
15
24
|
* for as long as the signal lives.
|
|
16
25
|
*/
|
|
26
|
+
/** What AbortSignal.timeout() aborts with: a DOMException, which is not an Error everywhere, so it is read by its name. */
|
|
27
|
+
const isTimeoutReason = (reason) => typeof reason === "object" && reason !== null && reason.name === "TimeoutError";
|
|
17
28
|
export const createCallAbort = (options) => {
|
|
18
29
|
const { timeoutMs, signal: external } = options;
|
|
19
30
|
let timedOut = false;
|
|
@@ -34,21 +45,32 @@ export const createCallAbort = (options) => {
|
|
|
34
45
|
detachExternal = () => external.removeEventListener("abort", forwardAbort);
|
|
35
46
|
}
|
|
36
47
|
}
|
|
37
|
-
timeoutId = setTimeout(() => {
|
|
48
|
+
timeoutId = setTimeout(() => {
|
|
49
|
+
// The first abort names the call: a timer that fires after the
|
|
50
|
+
// site's signal already aborted it finds the call given up on by
|
|
51
|
+
// the site, and must not report it as a timeout.
|
|
52
|
+
if (!controller.signal.aborted)
|
|
53
|
+
timedOut = true;
|
|
54
|
+
controller.abort();
|
|
55
|
+
}, timeoutMs);
|
|
38
56
|
signal = controller.signal;
|
|
39
57
|
}
|
|
58
|
+
// The chained controller aborts with the site signal's own reason, so a
|
|
59
|
+
// deadline the site set is still readable here.
|
|
60
|
+
const abortReason = () => !signal?.aborted ? null : timedOut || isTimeoutReason(signal.reason) ? "timeout" : "aborted";
|
|
40
61
|
return {
|
|
41
62
|
signal,
|
|
42
|
-
|
|
63
|
+
abortReason,
|
|
43
64
|
abortFailure: (stage) => {
|
|
44
|
-
|
|
65
|
+
const reason = abortReason();
|
|
66
|
+
if (!reason)
|
|
45
67
|
return null;
|
|
46
68
|
const detail = stage === "beforeSending"
|
|
47
69
|
? "the call was given up on before it was sent"
|
|
48
70
|
: "the answer arrived too late to be used";
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
71
|
+
if (reason === "aborted")
|
|
72
|
+
return { reason, error: new Error(`Request aborted by its signal; ${detail}.`) };
|
|
73
|
+
return { reason, error: new Error(timedOut ? `Request timed out after ${timeoutMs}ms; ${detail}.` : `Request timed out by its signal; ${detail}.`) };
|
|
52
74
|
},
|
|
53
75
|
detach: () => {
|
|
54
76
|
if (timeoutId !== undefined)
|
|
@@ -66,15 +88,21 @@ export const createCallAbort = (options) => {
|
|
|
66
88
|
* transports a signal has nothing to cancel, and by the crash reporter's
|
|
67
89
|
* time bound (LambderCrashHandling), where the app's report runs on.
|
|
68
90
|
*
|
|
69
|
-
*
|
|
70
|
-
*
|
|
91
|
+
* A signal that aborted before the wait began rejects it at once with the
|
|
92
|
+
* signal's reason, since it fires no event for a listener added afterwards.
|
|
93
|
+
* `pending` is still attached to either way, so what it later rejects with is
|
|
94
|
+
* settled here rather than left an unhandled rejection. The listener is
|
|
95
|
+
* detached on either outcome, for the reason createCallAbort detaches its own.
|
|
71
96
|
*/
|
|
72
97
|
export const stopWaitingWhenAborted = (pending, signal) => {
|
|
73
98
|
if (!signal)
|
|
74
99
|
return pending;
|
|
75
100
|
return new Promise((resolve, reject) => {
|
|
76
101
|
const onAbort = () => reject(signal.reason);
|
|
77
|
-
signal.
|
|
102
|
+
if (signal.aborted)
|
|
103
|
+
onAbort();
|
|
104
|
+
else
|
|
105
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
78
106
|
pending.then(resolve, reject).finally(() => signal.removeEventListener("abort", onAbort));
|
|
79
107
|
});
|
|
80
108
|
};
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
/** Marks an error as reported where it arose. */
|
|
2
|
+
export declare const markErrorReported: (error: Error) => void;
|
|
3
|
+
/** Whether an error was marked as reported where it arose. Read by brand, so it holds across realms and package copies. */
|
|
4
|
+
export declare const isErrorReported: (error: unknown) => boolean;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The mark on an error that has already been reported, so the crash
|
|
3
|
+
* reporting it reaches next does not report it a second time.
|
|
4
|
+
*
|
|
5
|
+
* LambderInvokeCaller marks the error of a failure its onFailure handler
|
|
6
|
+
* took: api() then throws that same error, and an app that lets it propagate
|
|
7
|
+
* hands it to the instance's crash reporting, which skips a marked error and
|
|
8
|
+
* still answers it as a crash. The two ends live in invoke/ and core/, which
|
|
9
|
+
* may not import each other, so the mark sits below both.
|
|
10
|
+
*
|
|
11
|
+
* A key from the global symbol registry rather than a WeakSet kept here: a
|
|
12
|
+
* caller and a server from two copies of the package (an app's and a
|
|
13
|
+
* dependency's) agree on it, as they do on every Lambder brand. It is set
|
|
14
|
+
* non-enumerable, so it never shows up in a serialized error or a log line.
|
|
15
|
+
*/
|
|
16
|
+
const LAMBDER_ERROR_REPORTED = Symbol.for("lambder.errorReported");
|
|
17
|
+
/** Marks an error as reported where it arose. */
|
|
18
|
+
export const markErrorReported = (error) => {
|
|
19
|
+
Object.defineProperty(error, LAMBDER_ERROR_REPORTED, { value: true, configurable: true });
|
|
20
|
+
};
|
|
21
|
+
/** Whether an error was marked as reported where it arose. Read by brand, so it holds across realms and package copies. */
|
|
22
|
+
export const isErrorReported = (error) => typeof error === "object" && error !== null && error[LAMBDER_ERROR_REPORTED] === true;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The digest a caller-controlled field of a store key is written as.
|
|
3
|
+
*
|
|
4
|
+
* The rate-limit engine and the idempotency engine each build a store key
|
|
5
|
+
* around fields only the caller controls: an address, a session key, whatever
|
|
6
|
+
* a custom rate-limit key returned, a callerIdentity, a posted idempotency
|
|
7
|
+
* key. Written as they are, a table read shows who called (an email, a device
|
|
8
|
+
* token, an address), and a long one passes a store's own key limit, which
|
|
9
|
+
* the store answers by throwing and both engines' failOpen turns into no
|
|
10
|
+
* limit or no idempotency for that caller. So every such field is written as
|
|
11
|
+
* a digest of a fixed length, whatever its own, in one implementation the two
|
|
12
|
+
* engines share: `<kind>:<64 hex>`, with the kind (ip, session, custom, ...)
|
|
13
|
+
* left readable, as the API and policy names around it are.
|
|
14
|
+
*
|
|
15
|
+
* Keyed when the app has an at-rest secret (the session salt): an HMAC under
|
|
16
|
+
* a subkey derived from it for this one purpose, so a table read alone cannot
|
|
17
|
+
* test a guess. Derived through HKDF rather than used as it is because the
|
|
18
|
+
* session store keys its partition hash with the salt itself, and that hash
|
|
19
|
+
* rides in every session cookie: a sessionKey a user can choose would
|
|
20
|
+
* otherwise let them read off a cookie the digest of any value they like.
|
|
21
|
+
* Without a secret the digest is a plain SHA-256, which keeps the values out
|
|
22
|
+
* of the table but lets a reader test guesses: an email from a list, an IPv4
|
|
23
|
+
* address from all four billion.
|
|
24
|
+
*/
|
|
25
|
+
export declare class LambderKeyFieldDigest {
|
|
26
|
+
/** Whether the digest is keyed by the app's at-rest secret, or a plain SHA-256. */
|
|
27
|
+
readonly isKeyed: boolean;
|
|
28
|
+
private readonly atRestSecret;
|
|
29
|
+
private hmacKeyPromise;
|
|
30
|
+
constructor(atRestSecret: string | null);
|
|
31
|
+
/**
|
|
32
|
+
* `value` written as a field of `kind`: `<kind>:<hex>`. The kind is part
|
|
33
|
+
* of what is digested, so one value under two kinds (a session key
|
|
34
|
+
* counted by a rate limit and scoping an idempotency record) gives two
|
|
35
|
+
* unrelated digests, and a table read cannot join the two.
|
|
36
|
+
*
|
|
37
|
+
* A runtime without WebCrypto (the mock on a page served over plain http,
|
|
38
|
+
* a phone on the LAN) writes `<kind>:<value>` instead: the only stores
|
|
39
|
+
* such a runtime runs over are the mock's memory ones, which nobody reads
|
|
40
|
+
* at rest and which have no key limit, and a key there is only ever
|
|
41
|
+
* compared with keys the same runtime wrote.
|
|
42
|
+
*/
|
|
43
|
+
digestOf(kind: string, value: string): Promise<string>;
|
|
44
|
+
/** The HMAC key, derived once per instance: HKDF-SHA256 over the secret, with no salt and this purpose as its info. */
|
|
45
|
+
private hmacKey;
|
|
46
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The digest a caller-controlled field of a store key is written as.
|
|
3
|
+
*
|
|
4
|
+
* The rate-limit engine and the idempotency engine each build a store key
|
|
5
|
+
* around fields only the caller controls: an address, a session key, whatever
|
|
6
|
+
* a custom rate-limit key returned, a callerIdentity, a posted idempotency
|
|
7
|
+
* key. Written as they are, a table read shows who called (an email, a device
|
|
8
|
+
* token, an address), and a long one passes a store's own key limit, which
|
|
9
|
+
* the store answers by throwing and both engines' failOpen turns into no
|
|
10
|
+
* limit or no idempotency for that caller. So every such field is written as
|
|
11
|
+
* a digest of a fixed length, whatever its own, in one implementation the two
|
|
12
|
+
* engines share: `<kind>:<64 hex>`, with the kind (ip, session, custom, ...)
|
|
13
|
+
* left readable, as the API and policy names around it are.
|
|
14
|
+
*
|
|
15
|
+
* Keyed when the app has an at-rest secret (the session salt): an HMAC under
|
|
16
|
+
* a subkey derived from it for this one purpose, so a table read alone cannot
|
|
17
|
+
* test a guess. Derived through HKDF rather than used as it is because the
|
|
18
|
+
* session store keys its partition hash with the salt itself, and that hash
|
|
19
|
+
* rides in every session cookie: a sessionKey a user can choose would
|
|
20
|
+
* otherwise let them read off a cookie the digest of any value they like.
|
|
21
|
+
* Without a secret the digest is a plain SHA-256, which keeps the values out
|
|
22
|
+
* of the table but lets a reader test guesses: an email from a list, an IPv4
|
|
23
|
+
* address from all four billion.
|
|
24
|
+
*/
|
|
25
|
+
import { joinKeyFields } from "./joinKeyFields.js";
|
|
26
|
+
import { bytesToHexString, resolveWebCrypto } from "./LambderTextDigest.js";
|
|
27
|
+
/** HKDF's `info`: the purpose the subkey is derived for, so no other derivation from the same secret meets it. */
|
|
28
|
+
const KEY_FIELD_DIGEST_PURPOSE = "lambder/key-field-digest";
|
|
29
|
+
export class LambderKeyFieldDigest {
|
|
30
|
+
/** Whether the digest is keyed by the app's at-rest secret, or a plain SHA-256. */
|
|
31
|
+
isKeyed;
|
|
32
|
+
atRestSecret;
|
|
33
|
+
hmacKeyPromise;
|
|
34
|
+
constructor(atRestSecret) {
|
|
35
|
+
if (atRestSecret !== null && (typeof atRestSecret !== "string" || atRestSecret === "")) {
|
|
36
|
+
throw new Error("Lambder: the at-rest secret that keys the digest of rate-limit and idempotency keys is empty. Pass the app's real secret (the session salt), or none.");
|
|
37
|
+
}
|
|
38
|
+
this.atRestSecret = atRestSecret;
|
|
39
|
+
this.isKeyed = atRestSecret !== null;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* `value` written as a field of `kind`: `<kind>:<hex>`. The kind is part
|
|
43
|
+
* of what is digested, so one value under two kinds (a session key
|
|
44
|
+
* counted by a rate limit and scoping an idempotency record) gives two
|
|
45
|
+
* unrelated digests, and a table read cannot join the two.
|
|
46
|
+
*
|
|
47
|
+
* A runtime without WebCrypto (the mock on a page served over plain http,
|
|
48
|
+
* a phone on the LAN) writes `<kind>:<value>` instead: the only stores
|
|
49
|
+
* such a runtime runs over are the mock's memory ones, which nobody reads
|
|
50
|
+
* at rest and which have no key limit, and a key there is only ever
|
|
51
|
+
* compared with keys the same runtime wrote.
|
|
52
|
+
*/
|
|
53
|
+
async digestOf(kind, value) {
|
|
54
|
+
let webCrypto;
|
|
55
|
+
try {
|
|
56
|
+
webCrypto = await resolveWebCrypto();
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
return `${kind}:${value}`;
|
|
60
|
+
}
|
|
61
|
+
const message = new TextEncoder().encode(joinKeyFields(kind, value));
|
|
62
|
+
const digest = this.atRestSecret === null
|
|
63
|
+
? await webCrypto.subtle.digest("SHA-256", message)
|
|
64
|
+
: await webCrypto.subtle.sign("HMAC", await this.hmacKey(webCrypto, this.atRestSecret), message);
|
|
65
|
+
return `${kind}:${bytesToHexString(new Uint8Array(digest))}`;
|
|
66
|
+
}
|
|
67
|
+
/** The HMAC key, derived once per instance: HKDF-SHA256 over the secret, with no salt and this purpose as its info. */
|
|
68
|
+
hmacKey(webCrypto, atRestSecret) {
|
|
69
|
+
this.hmacKeyPromise ??= (async () => {
|
|
70
|
+
const secretKey = await webCrypto.subtle.importKey("raw", new TextEncoder().encode(atRestSecret), "HKDF", false, ["deriveKey"]);
|
|
71
|
+
return await webCrypto.subtle.deriveKey({ name: "HKDF", hash: "SHA-256", salt: new Uint8Array(0), info: new TextEncoder().encode(KEY_FIELD_DIGEST_PURPOSE) }, secretKey, { name: "HMAC", hash: "SHA-256", length: 256 }, false, ["sign"]);
|
|
72
|
+
})();
|
|
73
|
+
return this.hmacKeyPromise;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
@@ -75,4 +75,23 @@ export declare const keyedDigest: (secret: string, value: string) => Promise<str
|
|
|
75
75
|
* without one is told so.
|
|
76
76
|
*/
|
|
77
77
|
export declare const randomSecret: (bytes?: number) => string;
|
|
78
|
+
/**
|
|
79
|
+
* The characters of an alphabet a code is drawn from, as code points, once
|
|
80
|
+
* they are 2 to 256 distinct ones; anything else throws, naming `name`. One
|
|
81
|
+
* character draws nothing random, a repeat weighs its character double, and
|
|
82
|
+
* past 256 the one byte randomCode draws per character cannot reach every
|
|
83
|
+
* character. Returns the characters so the check reads as an assignment.
|
|
84
|
+
*/
|
|
85
|
+
export declare const assertCodeAlphabet: (alphabet: unknown, name: string) => string[];
|
|
86
|
+
/**
|
|
87
|
+
* A code of `length` characters drawn uniformly from `alphabet` with the
|
|
88
|
+
* runtime's cryptographic random source: the digits emailed to an address,
|
|
89
|
+
* the letters of a pairing code somebody types. One byte draws one
|
|
90
|
+
* character, and a byte at or above the largest multiple of the alphabet's
|
|
91
|
+
* size is discarded and drawn again, since `byte % alphabet.length` alone
|
|
92
|
+
* favours the alphabet's first characters. The alphabet is 2 to 256 distinct
|
|
93
|
+
* characters (code points) and `length` a positive integer; anything else
|
|
94
|
+
* throws. Synchronous, as randomSecret is.
|
|
95
|
+
*/
|
|
96
|
+
export declare const randomCode: (alphabet: string, length: number) => string;
|
|
78
97
|
export {};
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { base64UrlToBytes, bytesToBase64Url, isBase64Url } from "./LambderBase64.js";
|
|
2
2
|
import { hmacSha256Of, importHmacKey, resolveWebCrypto } from "./LambderTextDigest.js";
|
|
3
|
+
import { assertPositiveInteger } from "./LambderOptionChecks.js";
|
|
3
4
|
/**
|
|
4
5
|
* One kind of signed token: constructed once with its secret, version and
|
|
5
6
|
* claims schema, it keeps the imported HMAC key and answers `sign` and
|
|
@@ -107,3 +108,56 @@ export const randomSecret = (bytes = 32) => {
|
|
|
107
108
|
}
|
|
108
109
|
return bytesToBase64Url(webCrypto.getRandomValues(new Uint8Array(bytes)));
|
|
109
110
|
};
|
|
111
|
+
/** The most bytes one getRandomValues call fills. */
|
|
112
|
+
const MAX_RANDOM_BYTES_PER_CALL = 65536;
|
|
113
|
+
/**
|
|
114
|
+
* The characters of an alphabet a code is drawn from, as code points, once
|
|
115
|
+
* they are 2 to 256 distinct ones; anything else throws, naming `name`. One
|
|
116
|
+
* character draws nothing random, a repeat weighs its character double, and
|
|
117
|
+
* past 256 the one byte randomCode draws per character cannot reach every
|
|
118
|
+
* character. Returns the characters so the check reads as an assignment.
|
|
119
|
+
*/
|
|
120
|
+
export const assertCodeAlphabet = (alphabet, name) => {
|
|
121
|
+
const characters = typeof alphabet === "string" ? Array.from(alphabet) : [];
|
|
122
|
+
const distinct = new Set(characters).size;
|
|
123
|
+
if (characters.length < 2 || characters.length > 256 || distinct !== characters.length) {
|
|
124
|
+
const found = typeof alphabet === "string" ? `${characters.length} characters, ${distinct} of them distinct` : typeof alphabet;
|
|
125
|
+
throw new Error(`Lambder: ${name} must be 2 to 256 distinct characters, got ${found}.`);
|
|
126
|
+
}
|
|
127
|
+
return characters;
|
|
128
|
+
};
|
|
129
|
+
/**
|
|
130
|
+
* A code of `length` characters drawn uniformly from `alphabet` with the
|
|
131
|
+
* runtime's cryptographic random source: the digits emailed to an address,
|
|
132
|
+
* the letters of a pairing code somebody types. One byte draws one
|
|
133
|
+
* character, and a byte at or above the largest multiple of the alphabet's
|
|
134
|
+
* size is discarded and drawn again, since `byte % alphabet.length` alone
|
|
135
|
+
* favours the alphabet's first characters. The alphabet is 2 to 256 distinct
|
|
136
|
+
* characters (code points) and `length` a positive integer; anything else
|
|
137
|
+
* throws. Synchronous, as randomSecret is.
|
|
138
|
+
*/
|
|
139
|
+
export const randomCode = (alphabet, length) => {
|
|
140
|
+
const characters = assertCodeAlphabet(alphabet, "randomCode alphabet");
|
|
141
|
+
assertPositiveInteger(length, "randomCode length");
|
|
142
|
+
const webCrypto = globalThis.crypto;
|
|
143
|
+
if (typeof webCrypto?.getRandomValues !== "function") {
|
|
144
|
+
throw new Error("Lambder needs crypto.getRandomValues in this runtime to draw a code. Every browser provides it; Node 20+ provides it as globalThis.crypto.");
|
|
145
|
+
}
|
|
146
|
+
const ceiling = Math.floor(256 / characters.length) * characters.length;
|
|
147
|
+
const drawn = [];
|
|
148
|
+
// Twice the length lets one call cover the discarded bytes nearly
|
|
149
|
+
// always; an alphabet that discards close to half of them may need
|
|
150
|
+
// another, which the loop draws.
|
|
151
|
+
const buffer = new Uint8Array(Math.min(length * 2, MAX_RANDOM_BYTES_PER_CALL));
|
|
152
|
+
while (drawn.length < length) {
|
|
153
|
+
webCrypto.getRandomValues(buffer);
|
|
154
|
+
for (const byte of buffer) {
|
|
155
|
+
if (byte >= ceiling)
|
|
156
|
+
continue;
|
|
157
|
+
drawn.push(characters[byte % characters.length]);
|
|
158
|
+
if (drawn.length === length)
|
|
159
|
+
break;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
return drawn.join("");
|
|
163
|
+
};
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/** A registered instance as the test kit reaches it. */
|
|
2
|
+
export type LambderSwappableInstance = {
|
|
3
|
+
readonly instance: object;
|
|
4
|
+
/**
|
|
5
|
+
* Calls the instance's swap door with the twins the test kit offers. The
|
|
6
|
+
* door picks the one its class needs, so this list need not know what
|
|
7
|
+
* each instance is.
|
|
8
|
+
*/
|
|
9
|
+
swapIn(twins: unknown): void;
|
|
10
|
+
};
|
|
11
|
+
/**
|
|
12
|
+
* Registers an instance `lambder/testing` puts a memory twin under, from the
|
|
13
|
+
* end of its constructor: the swap door keyed LAMBDER_BACKEND_SWAP must work
|
|
14
|
+
* by then, since a test app already in charge swaps it on the spot.
|
|
15
|
+
*/
|
|
16
|
+
export declare const registerSwappableInstance: (instance: object) => void;
|
|
17
|
+
/** Every registered instance still alive, in the order they were constructed. */
|
|
18
|
+
export declare const liveSwappableInstances: () => LambderSwappableInstance[];
|
|
19
|
+
/** Hands every instance registered from now on to the watcher, in place of any earlier one. */
|
|
20
|
+
export declare const watchSwappableInstances: (watcher: (instance: LambderSwappableInstance) => void) => void;
|
|
21
|
+
/**
|
|
22
|
+
* Makes every member named answer from the twin, in place: an own property of
|
|
23
|
+
* the instance, bound to the twin, over the class's method. Whatever holds the
|
|
24
|
+
* instance (a module-level constant, a class built over it) reaches the twin
|
|
25
|
+
* on its next call without being told, and a second twin replaces the first.
|
|
26
|
+
* The members are a record over the interface's keys, so an interface that
|
|
27
|
+
* grows a method fails to compile until its stores' doors hand it over too.
|
|
28
|
+
*/
|
|
29
|
+
export declare const delegateToTwin: <TInterface extends object>(instance: TInterface, twin: TInterface, members: { readonly [TMember in keyof TInterface]-?: true; }) => void;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { LAMBDER_BACKEND_SWAP } from "./LambderTestingDoors.js";
|
|
2
|
+
const REGISTRY_KEY = Symbol.for("lambder.swappableInstances");
|
|
3
|
+
const MIN_SWEEP_AT = 64;
|
|
4
|
+
/** Created on first use rather than when the module loads, so importing it does nothing. */
|
|
5
|
+
const registry = () => {
|
|
6
|
+
const holder = globalThis;
|
|
7
|
+
return holder[REGISTRY_KEY] ??= { entries: [], sweepAt: MIN_SWEEP_AT, watcher: null };
|
|
8
|
+
};
|
|
9
|
+
const handleOf = (instance, door) => ({
|
|
10
|
+
instance,
|
|
11
|
+
swapIn: (twins) => instance[door](twins),
|
|
12
|
+
});
|
|
13
|
+
/**
|
|
14
|
+
* Registers an instance `lambder/testing` puts a memory twin under, from the
|
|
15
|
+
* end of its constructor: the swap door keyed LAMBDER_BACKEND_SWAP must work
|
|
16
|
+
* by then, since a test app already in charge swaps it on the spot.
|
|
17
|
+
*/
|
|
18
|
+
export const registerSwappableInstance = (instance) => {
|
|
19
|
+
const state = registry();
|
|
20
|
+
state.entries.push({ ref: new WeakRef(instance), door: LAMBDER_BACKEND_SWAP });
|
|
21
|
+
if (state.entries.length >= state.sweepAt) {
|
|
22
|
+
state.entries = state.entries.filter((entry) => entry.ref.deref() !== undefined);
|
|
23
|
+
state.sweepAt = Math.max(MIN_SWEEP_AT, state.entries.length * 2);
|
|
24
|
+
}
|
|
25
|
+
state.watcher?.(handleOf(instance, LAMBDER_BACKEND_SWAP));
|
|
26
|
+
};
|
|
27
|
+
/** Every registered instance still alive, in the order they were constructed. */
|
|
28
|
+
export const liveSwappableInstances = () => {
|
|
29
|
+
const handles = [];
|
|
30
|
+
for (const entry of registry().entries) {
|
|
31
|
+
const instance = entry.ref.deref();
|
|
32
|
+
if (instance !== undefined)
|
|
33
|
+
handles.push(handleOf(instance, entry.door));
|
|
34
|
+
}
|
|
35
|
+
return handles;
|
|
36
|
+
};
|
|
37
|
+
/** Hands every instance registered from now on to the watcher, in place of any earlier one. */
|
|
38
|
+
export const watchSwappableInstances = (watcher) => {
|
|
39
|
+
registry().watcher = watcher;
|
|
40
|
+
};
|
|
41
|
+
/**
|
|
42
|
+
* Makes every member named answer from the twin, in place: an own property of
|
|
43
|
+
* the instance, bound to the twin, over the class's method. Whatever holds the
|
|
44
|
+
* instance (a module-level constant, a class built over it) reaches the twin
|
|
45
|
+
* on its next call without being told, and a second twin replaces the first.
|
|
46
|
+
* The members are a record over the interface's keys, so an interface that
|
|
47
|
+
* grows a method fails to compile until its stores' doors hand it over too.
|
|
48
|
+
*/
|
|
49
|
+
export const delegateToTwin = (instance, twin, members) => {
|
|
50
|
+
for (const member of Object.keys(members)) {
|
|
51
|
+
const method = twin[member];
|
|
52
|
+
Object.defineProperty(instance, member, { value: method.bind(twin), configurable: true, writable: true });
|
|
53
|
+
}
|
|
54
|
+
};
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The keys of the two doors `lambder/testing` opens on a built instance
|
|
2
|
+
* The keys of the two doors `lambder/testing` opens on a built instance, the
|
|
3
|
+
* first of which it also opens on the classes an app builds beside it.
|
|
3
4
|
*
|
|
4
5
|
* Symbols rather than named methods, and exported by no entry point: the
|
|
5
6
|
* package's exports map is what a consumer can import, so only
|
|
@@ -14,7 +15,10 @@
|
|
|
14
15
|
* other stores: the copy's closures would still reach the original and the
|
|
15
16
|
* production table under it. The stores are therefore replaced in place, and
|
|
16
17
|
* every class that holds one answers to this key with a method that takes the
|
|
17
|
-
* replacement.
|
|
18
|
+
* replacement. A class an app builds itself, beside the instance (a cache, an
|
|
19
|
+
* upload bucket, an invoke caller), registers as it is constructed (see
|
|
20
|
+
* LambderSwappableInstances) and answers to this key with a method that puts
|
|
21
|
+
* a memory twin, or a mock app, under itself in place.
|
|
18
22
|
*/
|
|
19
23
|
export declare const LAMBDER_BACKEND_SWAP: unique symbol;
|
|
20
24
|
/**
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The keys of the two doors `lambder/testing` opens on a built instance
|
|
2
|
+
* The keys of the two doors `lambder/testing` opens on a built instance, the
|
|
3
|
+
* first of which it also opens on the classes an app builds beside it.
|
|
3
4
|
*
|
|
4
5
|
* Symbols rather than named methods, and exported by no entry point: the
|
|
5
6
|
* package's exports map is what a consumer can import, so only
|
|
@@ -14,7 +15,10 @@
|
|
|
14
15
|
* other stores: the copy's closures would still reach the original and the
|
|
15
16
|
* production table under it. The stores are therefore replaced in place, and
|
|
16
17
|
* every class that holds one answers to this key with a method that takes the
|
|
17
|
-
* replacement.
|
|
18
|
+
* replacement. A class an app builds itself, beside the instance (a cache, an
|
|
19
|
+
* upload bucket, an invoke caller), registers as it is constructed (see
|
|
20
|
+
* LambderSwappableInstances) and answers to this key with a method that puts
|
|
21
|
+
* a memory twin, or a mock app, under itself in place.
|
|
18
22
|
*/
|
|
19
23
|
export const LAMBDER_BACKEND_SWAP = Symbol("lambder.backendSwap");
|
|
20
24
|
/**
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* SHA-256 and HMAC-SHA256 through WebCrypto: the digests every layer shares.
|
|
3
3
|
* The session crypto hashes bearer secrets with the first and partitions
|
|
4
|
-
* session keys with the second, the
|
|
5
|
-
*
|
|
6
|
-
* secret, so every key space is built from the same
|
|
7
|
-
* runtime (browsers on a secure context, Node 20+, edge
|
|
8
|
-
* upload's checksum is the same digest over the file's bytes.
|
|
4
|
+
* session keys with the second, the policy engines digest the caller's
|
|
5
|
+
* fields of their store keys, signed claims and keyed digests are HMACs
|
|
6
|
+
* under an app's secret, so every key space is built from the same
|
|
7
|
+
* primitives on every runtime (browsers on a secure context, Node 20+, edge
|
|
8
|
+
* runtimes); an upload's checksum is the same digest over the file's bytes.
|
|
9
9
|
*/
|
|
10
10
|
/** Lowercase hex of a byte array, two characters per byte. */
|
|
11
11
|
export declare const bytesToHexString: (bytes: Uint8Array) => string;
|
|
@@ -3,11 +3,11 @@ import { getCrypto } from "./LambderNodeModules.js";
|
|
|
3
3
|
/**
|
|
4
4
|
* SHA-256 and HMAC-SHA256 through WebCrypto: the digests every layer shares.
|
|
5
5
|
* The session crypto hashes bearer secrets with the first and partitions
|
|
6
|
-
* session keys with the second, the
|
|
7
|
-
*
|
|
8
|
-
* secret, so every key space is built from the same
|
|
9
|
-
* runtime (browsers on a secure context, Node 20+, edge
|
|
10
|
-
* upload's checksum is the same digest over the file's bytes.
|
|
6
|
+
* session keys with the second, the policy engines digest the caller's
|
|
7
|
+
* fields of their store keys, signed claims and keyed digests are HMACs
|
|
8
|
+
* under an app's secret, so every key space is built from the same
|
|
9
|
+
* primitives on every runtime (browsers on a secure context, Node 20+, edge
|
|
10
|
+
* runtimes); an upload's checksum is the same digest over the file's bytes.
|
|
11
11
|
*/
|
|
12
12
|
/** Lowercase hex of a byte array, two characters per byte. */
|
|
13
13
|
export const bytesToHexString = (bytes) => Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("");
|