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,71 +1,115 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Stops a stale client from reloading forever.
|
|
3
3
|
*
|
|
4
|
-
* A versionExpired answer means
|
|
5
|
-
* not the one the server holds
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* cached bundle, a server deploy that failed behind
|
|
9
|
-
* reload brings back the same
|
|
10
|
-
* and the page reloads again, indefinitely.
|
|
4
|
+
* A versionExpired answer means this client's signature for the endpoint is
|
|
5
|
+
* not the one the server holds (or its version is below the server's floor),
|
|
6
|
+
* and the ordinary response is to reload for the current bundle. When the
|
|
7
|
+
* served bundle is itself stale (a frontend deployed with a signature map the
|
|
8
|
+
* server does not match, a cached bundle, a server deploy that failed behind
|
|
9
|
+
* a fresh frontend), the reload brings back the same bundle, the calls fail
|
|
10
|
+
* the same way, and the page reloads again, indefinitely.
|
|
11
11
|
*
|
|
12
|
-
* The evidence
|
|
13
|
-
* the same
|
|
14
|
-
* changed the endpoint would carry a different signature
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
12
|
+
* The evidence is a versionExpired for a call an earlier load refused within
|
|
13
|
+
* the window: the same endpoint with the same signature and version. A bundle
|
|
14
|
+
* that had changed the endpoint would carry a different signature, and a
|
|
15
|
+
* rebuilt one a different version. Each call is kept with the time it was
|
|
16
|
+
* refused, and only one refused before this document loaded counts: a call
|
|
17
|
+
* this page refused itself (a retry, another caller's) has seen no reload
|
|
18
|
+
* since. Every call refused within the window is kept, not only the latest,
|
|
19
|
+
* because a stale bundle is usually stale for several endpoints and they
|
|
20
|
+
* answer in no fixed order: whichever of them answers first on the next load
|
|
21
|
+
* has to find itself in the record.
|
|
18
22
|
*
|
|
19
|
-
*
|
|
20
|
-
* the
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
23
|
+
* A page asks one versionExpiredHandler at a time. While it runs, the rest of
|
|
24
|
+
* what the page hears (the other stale endpoints it boots with, a retry,
|
|
25
|
+
* another caller's) is recorded and answered quietly, since the page is being
|
|
26
|
+
* asked already. A handler that has returned while the page is still here may
|
|
27
|
+
* not have reloaded it (a per-call handler that does something else, a reload
|
|
28
|
+
* cancelled at a beforeunload prompt), so the next versionExpired asks again;
|
|
29
|
+
* where it did, asking again before the page unloads only repeats the reload
|
|
30
|
+
* under way. An instance is one page: LambderCaller keeps one at module
|
|
31
|
+
* scope, shared by every caller the page builds (in a runtime with no page,
|
|
32
|
+
* every caller of the process).
|
|
33
|
+
*
|
|
34
|
+
* Once a repeat is confirmed, every versionExpired within the window counts
|
|
35
|
+
* as one, whichever endpoint it names, so an endpoint the stale bundle calls
|
|
36
|
+
* only later does not earn a reload of its own. The window runs from the
|
|
37
|
+
* first versionExpired recorded, not from the latest. After it a reload is
|
|
38
|
+
* allowed again, so a client stuck on a stale bundle retries a few times an
|
|
39
|
+
* hour and recovers once the deploy is fixed.
|
|
40
|
+
*
|
|
41
|
+
* The record has to outlive the reload it watches for, so it lives in
|
|
42
|
+
* sessionStorage, per tab and per origin. Where that does not work (storage
|
|
43
|
+
* blocked, a runtime with no page) nothing outlives the page: it still asks
|
|
44
|
+
* one handler at a time, and a stale bundle there reloads as it would without
|
|
45
|
+
* this class.
|
|
25
46
|
*/
|
|
26
47
|
/** How long after the first versionExpired a repeat counts as the same loop. */
|
|
27
48
|
export const RELOAD_LOOP_WINDOW_MS = 5 * 60 * 1000;
|
|
28
49
|
const STORAGE_KEY = "lambder:version-expired";
|
|
29
|
-
const
|
|
30
|
-
&& typeof value
|
|
31
|
-
&& typeof value
|
|
50
|
+
const isRefusedCall = (value) => Array.isArray(value) && value.length === 4
|
|
51
|
+
&& typeof value[0] === "string" && typeof value[1] === "string" && typeof value[2] === "string"
|
|
52
|
+
&& typeof value[3] === "number";
|
|
53
|
+
const isVersionExpiredRecord = (value) => typeof value === "object" && value !== null
|
|
32
54
|
&& typeof value.at === "number"
|
|
33
|
-
&& typeof value.confirmed === "boolean"
|
|
55
|
+
&& typeof value.confirmed === "boolean"
|
|
56
|
+
&& Array.isArray(value.calls)
|
|
57
|
+
&& value.calls.every(isRefusedCall);
|
|
58
|
+
/** A record from the future (a clock set back) is outside the window rather than inside it forever. */
|
|
59
|
+
const isWithinWindow = (at, now) => now >= at && now - at < RELOAD_LOOP_WINDOW_MS;
|
|
34
60
|
export class LambderReloadLoopBreaker {
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
61
|
+
loadedAt;
|
|
62
|
+
/** Whether the handler this page asked is still running: what the page hears meanwhile stays quiet. */
|
|
63
|
+
reloadAskPending = false;
|
|
64
|
+
/** loadedAt: when this document loaded. A call refused before it was refused by an earlier load, with a reload in between. */
|
|
65
|
+
constructor(loadedAt = performance.timeOrigin) {
|
|
66
|
+
this.loadedAt = loadedAt;
|
|
67
|
+
}
|
|
68
|
+
/** Records this versionExpired and decides what the caller does about it. */
|
|
69
|
+
recordVersionExpired(apiName, signature, version, now = Date.now()) {
|
|
70
|
+
const stored = this.read();
|
|
71
|
+
const record = stored && isWithinWindow(stored.at, now) ? stored : { at: now, confirmed: false, calls: [] };
|
|
72
|
+
const earlier = record.calls.find(([name, sig, ver]) => name === apiName && sig === signature && ver === version);
|
|
73
|
+
if (!earlier)
|
|
74
|
+
record.calls.push([apiName, signature, version, now]);
|
|
75
|
+
let decision;
|
|
76
|
+
if (this.reloadAskPending)
|
|
77
|
+
decision = "alreadyAsked";
|
|
78
|
+
else if (record.confirmed || (earlier !== undefined && earlier[3] < this.loadedAt))
|
|
79
|
+
decision = "loopConfirmed";
|
|
80
|
+
else
|
|
81
|
+
decision = "askForReload";
|
|
82
|
+
if (decision === "loopConfirmed")
|
|
83
|
+
record.confirmed = true;
|
|
84
|
+
this.write(record);
|
|
85
|
+
return decision;
|
|
86
|
+
}
|
|
87
|
+
/** Runs the ask an askForReload decision calls for; until it settles, every versionExpired is alreadyAsked. */
|
|
88
|
+
async runReloadAsk(ask) {
|
|
89
|
+
this.reloadAskPending = true;
|
|
90
|
+
try {
|
|
91
|
+
await ask();
|
|
92
|
+
}
|
|
93
|
+
finally {
|
|
94
|
+
this.reloadAskPending = false;
|
|
48
95
|
}
|
|
49
|
-
this.write({ apiName, signature, at: now, confirmed: false });
|
|
50
|
-
return false;
|
|
51
96
|
}
|
|
52
97
|
read() {
|
|
53
98
|
try {
|
|
54
|
-
const
|
|
55
|
-
if (
|
|
56
|
-
const parsed = JSON.parse(
|
|
57
|
-
if (
|
|
99
|
+
const stored = globalThis.sessionStorage?.getItem(STORAGE_KEY);
|
|
100
|
+
if (stored) {
|
|
101
|
+
const parsed = JSON.parse(stored);
|
|
102
|
+
if (isVersionExpiredRecord(parsed))
|
|
58
103
|
return parsed;
|
|
59
104
|
}
|
|
60
105
|
}
|
|
61
|
-
catch { /* a
|
|
62
|
-
return
|
|
106
|
+
catch { /* blocked storage, or a record that is not JSON: read as no record */ }
|
|
107
|
+
return null;
|
|
63
108
|
}
|
|
64
109
|
write(record) {
|
|
65
|
-
this.memory = record;
|
|
66
110
|
try {
|
|
67
111
|
globalThis.sessionStorage?.setItem(STORAGE_KEY, JSON.stringify(record));
|
|
68
112
|
}
|
|
69
|
-
catch { /*
|
|
113
|
+
catch { /* blocked or full: nothing outlives this page */ }
|
|
70
114
|
}
|
|
71
115
|
}
|
|
@@ -2,7 +2,10 @@ import type { LambderApiTransport } from "../shared/transport/LambderApiTranspor
|
|
|
2
2
|
/**
|
|
3
3
|
* The production transport: one POST of the envelope to the API path over
|
|
4
4
|
* fetch, with the cookie and CORS behaviour a browser call needs. The
|
|
5
|
-
* caller's default, built from its isCorsEnabled option.
|
|
5
|
+
* caller's default, built from its isCorsEnabled option. `cors` defaults to
|
|
6
|
+
* whether the call's apiPath is on another origin than the page's: a browser
|
|
7
|
+
* refuses a same-origin mode request to another origin outright, and a
|
|
8
|
+
* credentialed cross-origin one is what a separate API host needs.
|
|
6
9
|
*/
|
|
7
10
|
export declare const lambderFetchTransport: (options?: {
|
|
8
11
|
cors?: boolean;
|
|
@@ -1,20 +1,19 @@
|
|
|
1
1
|
import { buildTransportEnvelope, LambderTransportFailure } from "../shared/transport/LambderApiTransport.js";
|
|
2
2
|
/**
|
|
3
|
-
* fetch, with
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* that resolves relative URLs some other way is left alone.
|
|
3
|
+
* fetch, with a clearer error for one failure. Outside a page a relative
|
|
4
|
+
* apiPath has nothing to resolve against, so fetch rejects with a URL parse
|
|
5
|
+
* error that reaches the caller looking like the network is down. The
|
|
6
|
+
* condition is read from the actual failure rather than guessed beforehand,
|
|
7
|
+
* so a runtime that resolves relative URLs some other way is left alone.
|
|
9
8
|
*/
|
|
10
9
|
const fetchOrExplain = async (url, init) => {
|
|
11
10
|
try {
|
|
12
11
|
return await fetch(url, init);
|
|
13
12
|
}
|
|
14
13
|
catch (err) {
|
|
15
|
-
// Narrow on purpose: only
|
|
16
|
-
//
|
|
17
|
-
//
|
|
14
|
+
// Narrow on purpose: only a URL parse failure is rewritten. A fetch
|
|
15
|
+
// that rejected for any other reason, a stubbed one included, keeps
|
|
16
|
+
// its own error.
|
|
18
17
|
const failedToParseUrl = err instanceof TypeError && /failed to parse url|invalid url/i.test(String(err.message));
|
|
19
18
|
if (failedToParseUrl && url.startsWith("/") && typeof globalThis.location?.href !== "string") {
|
|
20
19
|
throw new LambderTransportFailure("protocol", `lambderFetchTransport could not resolve the relative apiPath "${url}": there is no page to resolve it against outside a browser. `
|
|
@@ -26,10 +25,32 @@ const fetchOrExplain = async (url, init) => {
|
|
|
26
25
|
/**
|
|
27
26
|
* The production transport: one POST of the envelope to the API path over
|
|
28
27
|
* fetch, with the cookie and CORS behaviour a browser call needs. The
|
|
29
|
-
* caller's default, built from its isCorsEnabled option.
|
|
28
|
+
* caller's default, built from its isCorsEnabled option. `cors` defaults to
|
|
29
|
+
* whether the call's apiPath is on another origin than the page's: a browser
|
|
30
|
+
* refuses a same-origin mode request to another origin outright, and a
|
|
31
|
+
* credentialed cross-origin one is what a separate API host needs.
|
|
30
32
|
*/
|
|
31
33
|
export const lambderFetchTransport = (options = {}) => async (request) => {
|
|
32
|
-
|
|
34
|
+
let cors = options.cors;
|
|
35
|
+
if (cors === undefined) {
|
|
36
|
+
// Resolved against the page, so a protocol-relative `//api.example.com`
|
|
37
|
+
// counts as another origin too. Outside a page a relative path
|
|
38
|
+
// resolves against nothing and an absolute one has no page origin to
|
|
39
|
+
// match, and neither matters there: a fetch outside a browser does
|
|
40
|
+
// not enforce the mode.
|
|
41
|
+
try {
|
|
42
|
+
cors = new URL(request.apiPath, globalThis.location?.href).origin !== globalThis.location?.origin;
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
cors = false;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
// The headers this transport sets itself leave the caller's set in any
|
|
49
|
+
// spelling: fetch merges header names case-insensitively, so a
|
|
50
|
+
// `content-type` beside the transport's `Content-Type` would join it
|
|
51
|
+
// ("text/plain, application/json") rather than give way to it.
|
|
52
|
+
const owned = new Set(['content-type', ...(request.cookies?.length ? ['cookie'] : [])]);
|
|
53
|
+
const callerHeaders = Object.fromEntries(Object.entries(request.headers ?? {}).filter(([name]) => !owned.has(name.toLowerCase())));
|
|
33
54
|
const response = await fetchOrExplain(request.apiPath, {
|
|
34
55
|
method: 'POST', cache: 'no-cache',
|
|
35
56
|
// Cross-origin API hosts need CORS mode and included credentials.
|
|
@@ -37,33 +58,36 @@ export const lambderFetchTransport = (options = {}) => async (request) => {
|
|
|
37
58
|
credentials: cors ? 'include' : 'same-origin',
|
|
38
59
|
redirect: 'follow', referrerPolicy: 'origin',
|
|
39
60
|
headers: {
|
|
40
|
-
// The caller's own headers go on first
|
|
41
|
-
//
|
|
42
|
-
//
|
|
43
|
-
//
|
|
44
|
-
//
|
|
45
|
-
|
|
46
|
-
...(request.headers ?? {}),
|
|
61
|
+
// The caller's own headers go on first so they cannot displace the
|
|
62
|
+
// ones this transport owns (the synthesized invoke event does the
|
|
63
|
+
// same). With the spread last, a per-call `Cookie` header would
|
|
64
|
+
// replace the whole Cookie header a jar had just built, and the
|
|
65
|
+
// session would go missing with nothing pointing at the cause.
|
|
66
|
+
...callerHeaders,
|
|
47
67
|
'Content-Type': 'application/json',
|
|
48
|
-
// The cookies the request
|
|
49
|
-
//
|
|
50
|
-
//
|
|
51
|
-
//
|
|
52
|
-
//
|
|
53
|
-
//
|
|
54
|
-
//
|
|
55
|
-
// on every session call.
|
|
68
|
+
// The cookies the request carries: this is how a cookie jar works
|
|
69
|
+
// over fetch outside a browser, where undici sends the header. A
|
|
70
|
+
// page cannot (Cookie is a forbidden header name, so a browser
|
|
71
|
+
// drops it and uses its own cookie store, which is right there).
|
|
72
|
+
// Without it a jar would collect every Set-Cookie and send none
|
|
73
|
+
// back, and a Node script against a deployed app would get
|
|
74
|
+
// sessionExpired on every session call.
|
|
56
75
|
...(request.cookies?.length ? { Cookie: request.cookies.join('; ') } : {}),
|
|
57
76
|
},
|
|
58
77
|
body: JSON.stringify(buildTransportEnvelope(request)),
|
|
59
78
|
...(request.signal ? { signal: request.signal } : {}),
|
|
60
79
|
});
|
|
80
|
+
// Read here, inside the call, so the caller's timeout and abort cover the
|
|
81
|
+
// whole answer: fetch resolves on the headers, and a body abandoned while
|
|
82
|
+
// it downloads would otherwise read as a malformed answer (a server
|
|
83
|
+
// error) rather than the timeout or abort it was.
|
|
84
|
+
const body = await response.text();
|
|
61
85
|
return {
|
|
62
86
|
status: response.status,
|
|
63
87
|
statusText: response.statusText,
|
|
64
88
|
header: (name) => response.headers?.get?.(name) ?? null,
|
|
65
|
-
json: () =>
|
|
66
|
-
text: () =>
|
|
89
|
+
json: async () => JSON.parse(body),
|
|
90
|
+
text: async () => body,
|
|
67
91
|
// Set-Cookie is unreadable from a page's script; where the runtime
|
|
68
92
|
// exposes it (Node's fetch), a cookie jar can still consume it.
|
|
69
93
|
...(typeof response.headers?.getSetCookie === "function" ? { setCookies: response.headers.getSetCookie() } : {}),
|
package/dist/client.d.ts
CHANGED
|
@@ -20,10 +20,12 @@ export type { LambderApiSignatureMap } from "./shared/wire/LambderApiSignature.j
|
|
|
20
20
|
export { RELOAD_LOOP_WINDOW_MS } from "./client/LambderReloadLoopBreaker.js";
|
|
21
21
|
export { compareDottedVersions, isDottedVersion } from "./shared/wire/LambderVersionOrder.js";
|
|
22
22
|
export type { LambderApiAnswerOutcome, LambderApiSuccessOutcome, LambderApiCallFailure, LambderApiValidationFailure, LambderApiEnvelopeFailure, LambderApiHttpAnswer, } from "./shared/wire/LambderApiOutcome.js";
|
|
23
|
-
export type { LambderApiOutcome, LambderApiFailureReason, LambderValidationError, LambderCallOptions, LambderCallerOptions, LambderGuardInputsProvider, LambderProvidedGuardInputs,
|
|
24
|
-
export {
|
|
23
|
+
export type { LambderApiOutcome, LambderApiFailureReason, LambderValidationError, LambderCallOptions, LambderCallerOptions, LambderGuardInputsProvider, LambderProvidedGuardInputs, LambderLogListHandler, } from "./client/LambderCaller.js";
|
|
24
|
+
export { createIdempotencyKey, createIdempotencyKeyScope } from "./shared/wire/LambderIdempotencyKeyScope.js";
|
|
25
|
+
export type { LambderIdempotencyKeyScope } from "./shared/wire/LambderIdempotencyKeyScope.js";
|
|
26
|
+
export { LambderApiRefusal, isLambderApiRefusal, refuse, refusalMessageOf, LAMBDER_REFUSAL_CODES } from "./shared/wire/LambderApiRefusal.js";
|
|
25
27
|
export type { LambderApiRefusalOptions, LambderRefusalMessage, LambderAppRefusalMessage, LambderRefusalCode, LambderRefuseOptions } from "./shared/wire/LambderApiRefusal.js";
|
|
26
|
-
export type { LambderApiContractShape, LambderApiMode, LambderApiEnvelopeBody, LambderApiResponseConfig, LambderGuardNamesIn, LambderContractMode, LambderContractKeysWithMode, LambderContractGuardsOf, LambderContractGuardNames, LambderContractGuardInputsOf, LambderContractGuardInput, LambderContractGuardInputNames, LambderContractRateLimitOf, LambderContractRateLimitNames, LambderContractIdempotencyOf, } from "./shared/wire/LambderApiContract.js";
|
|
28
|
+
export type { LambderApiContractShape, LambderApiMode, LambderApiEnvelopeBody, LambderApiResponseConfig, LambderGuardNamesIn, LambderContractMode, LambderContractKeysWithMode, LambderContractKeysWithGuard, LambderJsonOf, LambderJsonOutputOf, LambderContractGuardsOf, LambderContractGuardNames, LambderContractGuardInputsOf, LambderContractGuardInput, LambderContractGuardInputNames, LambderContractRateLimitOf, LambderContractRateLimitNames, LambderContractIdempotencyOf, } from "./shared/wire/LambderApiContract.js";
|
|
27
29
|
export { describeCrash, errorFromCrashDetail } from "./shared/wire/LambderCrashDetail.js";
|
|
28
30
|
export type { LambderCrashDetail, LambderCrashCause } from "./shared/wire/LambderCrashDetail.js";
|
|
29
31
|
export { compressPayloadGzip, isRequestCompressionAvailable, COMPRESSED_PAYLOAD_GZ_FIELD, COMPRESSED_PAYLOAD_BR_FIELD, COMPRESSED_PAYLOAD_BYTES_FIELD, DEFAULT_REQUEST_COMPRESSION_SETTINGS, } from "./shared/wire/LambderRequestPayload.js";
|
package/dist/client.js
CHANGED
|
@@ -19,9 +19,10 @@ export { resolveApiOutcome } from "./shared/wire/LambderApiOutcome.js";
|
|
|
19
19
|
export { apiNameKeyOf, lookupApiSignature, readApiSignature, API_SIGNATURE_HEX_LENGTH, extensibleEnum } from "./shared/wire/LambderApiSignature.js";
|
|
20
20
|
export { RELOAD_LOOP_WINDOW_MS } from "./client/LambderReloadLoopBreaker.js";
|
|
21
21
|
export { compareDottedVersions, isDottedVersion } from "./shared/wire/LambderVersionOrder.js";
|
|
22
|
+
export { createIdempotencyKey, createIdempotencyKeyScope } from "./shared/wire/LambderIdempotencyKeyScope.js";
|
|
22
23
|
// Typed API refusals (isomorphic: shared code may throw them from anywhere;
|
|
23
24
|
// in the browser they are plain Errors).
|
|
24
|
-
export { LambderApiRefusal, isLambderApiRefusal, refuse, LAMBDER_REFUSAL_CODES } from "./shared/wire/LambderApiRefusal.js";
|
|
25
|
+
export { LambderApiRefusal, isLambderApiRefusal, refuse, refusalMessageOf, LAMBDER_REFUSAL_CODES } from "./shared/wire/LambderApiRefusal.js";
|
|
25
26
|
// A crash described for a caller allowed to see it (the envelope's `crash` field; pure, no Node built-ins).
|
|
26
27
|
export { describeCrash, errorFromCrashDetail } from "./shared/wire/LambderCrashDetail.js";
|
|
27
28
|
// Request payload compression (browser-safe: gzip via CompressionStream, no Node built-ins).
|