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
|
@@ -23,6 +23,68 @@ export type LambderBeforeRenderHook = (ctx: LambderRenderContext, resolver: Lamb
|
|
|
23
23
|
export type LambderAfterRenderHook = (ctx: LambderRenderContext, resolver: LambderResolver, response: LambderResponse) => MaybePromise<LambderResponse | Error>;
|
|
24
24
|
export type LambderFallbackHook = (ctx: LambderRenderContext, resolver: LambderResolver) => void | Promise<void>;
|
|
25
25
|
export type LambderGlobalErrorHandler = (err: Error, ctx: LambderRenderContext | null, response: LambderResponseBuilder) => MaybePromise<LambderResponse>;
|
|
26
|
+
/**
|
|
27
|
+
* Where a crash happened.
|
|
28
|
+
*
|
|
29
|
+
* - `api`: an API call, from its hooks, guards or handler.
|
|
30
|
+
* - `route`: any other HTTP request (a route, a served file or page, a
|
|
31
|
+
* fallback). `ctx` is null when the request could not be read at all.
|
|
32
|
+
* - `event`: a non-HTTP invocation (a schedule, SNS, SQS) whose action threw,
|
|
33
|
+
* or that no action matched. The error is rethrown to Lambda after the
|
|
34
|
+
* report, so retries and dead-letter queues keep working.
|
|
35
|
+
* - `startup`: a `created` hook failed before the invocation could start.
|
|
36
|
+
*/
|
|
37
|
+
export type LambderCrashSite = {
|
|
38
|
+
kind: "api";
|
|
39
|
+
ctx: LambderRenderContext;
|
|
40
|
+
lambdaContext: Context;
|
|
41
|
+
} | {
|
|
42
|
+
kind: "route";
|
|
43
|
+
ctx: LambderRenderContext | null;
|
|
44
|
+
lambdaContext: Context;
|
|
45
|
+
} | {
|
|
46
|
+
kind: "event";
|
|
47
|
+
event: unknown;
|
|
48
|
+
lambdaContext: Context;
|
|
49
|
+
} | {
|
|
50
|
+
kind: "startup";
|
|
51
|
+
lambdaContext: Context;
|
|
52
|
+
};
|
|
53
|
+
/**
|
|
54
|
+
* Told every crash, awaited before the answer goes out (a Lambda may be
|
|
55
|
+
* frozen the moment it answers, so a report left running might never land),
|
|
56
|
+
* for up to `reportTimeoutMs`. A refusal is never a crash and never reaches
|
|
57
|
+
* it. Anything it throws is logged and swallowed, so a broken reporter
|
|
58
|
+
* cannot turn one failure into two, and a stalled one cannot turn every
|
|
59
|
+
* crash into a function timeout.
|
|
60
|
+
*
|
|
61
|
+
* `site.ctx` is the request's context, the one a beforeRender hook handed
|
|
62
|
+
* back when one did, secrets included (the session cookie, a login's
|
|
63
|
+
* password in the body, the session record): forward the fields a crash
|
|
64
|
+
* needs rather than the whole context to an error tracker.
|
|
65
|
+
*/
|
|
66
|
+
export type LambderCrashReporter = (error: Error, site: LambderCrashSite) => MaybePromise<void>;
|
|
67
|
+
/** The `crashes` option of create(). */
|
|
68
|
+
export type LambderCrashOptions = {
|
|
69
|
+
/** Told every crash on every path; see LambderCrashReporter. Default: none, and the framework's own 500 logs the crash to the console instead. */
|
|
70
|
+
report?: LambderCrashReporter;
|
|
71
|
+
/**
|
|
72
|
+
* How long a crash's answer waits for the reporter, in milliseconds. A
|
|
73
|
+
* report still running then is logged, with the crash, as unfinished,
|
|
74
|
+
* and the request is answered. Default: 3000.
|
|
75
|
+
*/
|
|
76
|
+
reportTimeoutMs?: number;
|
|
77
|
+
/**
|
|
78
|
+
* Whether the caller behind this request may read the crash: when true,
|
|
79
|
+
* the framework's 500 carries it in full (describeCrash on an API call's
|
|
80
|
+
* `crash` field, beside the call's logList; the stack as text on a
|
|
81
|
+
* route). For a developer's own browser or a trusted invoker. It governs
|
|
82
|
+
* the framework's answer only: an app that sets a global error handler
|
|
83
|
+
* writes its own answer, and describeCrash is there for it. A reveal
|
|
84
|
+
* that throws counts as no. Default: nobody.
|
|
85
|
+
*/
|
|
86
|
+
reveal?: (ctx: LambderRenderContext) => MaybePromise<boolean>;
|
|
87
|
+
};
|
|
26
88
|
export type LambderFallbackHandler = (ctx: LambderRenderContext, resolver: LambderResolver) => MaybePromise<LambderResponse>;
|
|
27
89
|
export type LambderInputValidationHandler = (ctx: LambderRenderContext, resolver: LambderResolver, zodError: z.ZodError) => MaybePromise<LambderResponse>;
|
|
28
90
|
/**
|
|
@@ -52,15 +114,14 @@ export type LambderSessionOptions<TSessionData = any> = {
|
|
|
52
114
|
* Where sessions rest: a LambderDdbSessionStore over your table, a
|
|
53
115
|
* LambderMemorySessionStore in tests, or your own LambderSessionStore.
|
|
54
116
|
*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
* said otherwise would find ctx.session.data typed by its table.
|
|
117
|
+
* Deliberately typed over `any`, not TSessionData. The session data type
|
|
118
|
+
* is the app's declaration (initLambder<SessionData>()), and a store holds
|
|
119
|
+
* whatever the app puts in it; naming TSessionData here would make
|
|
120
|
+
* `new Lambder({ session: { store } })` INFER the session data type from
|
|
121
|
+
* the store, so ctx.session.data would be typed by the table.
|
|
61
122
|
*/
|
|
62
123
|
store: LambderSessionStore<any>;
|
|
63
|
-
/**
|
|
124
|
+
/** The HMAC key that turns a sessionKey into the store's partition key, so a table read does not reveal whose sessions it holds. Treat as a secret. */
|
|
64
125
|
sessionSalt: string;
|
|
65
126
|
enableSlidingExpiration?: boolean;
|
|
66
127
|
/** Min seconds between sliding-expiration writes. Default: max(60, 5% of TTL). */
|
|
@@ -84,11 +145,10 @@ export type LambderSessionOptions<TSessionData = any> = {
|
|
|
84
145
|
};
|
|
85
146
|
/**
|
|
86
147
|
* Everything an instance is configured with, in ONE declaration: base
|
|
87
|
-
* serving options plus the type-affecting policy layer (rate limits,
|
|
88
|
-
*
|
|
89
|
-
*
|
|
90
|
-
*
|
|
91
|
-
* instance type ever needs a name.
|
|
148
|
+
* serving options plus the type-affecting policy layer (rate limits, guards,
|
|
149
|
+
* idempotency) and session/CORS config. The instance is born fully
|
|
150
|
+
* configured and fully typed (via initLambder), so there are no ordering
|
|
151
|
+
* rules and no partially-configured instance type.
|
|
92
152
|
*/
|
|
93
153
|
export type LambderCreateOptions<TSessionData = any> = {
|
|
94
154
|
/**
|
|
@@ -102,11 +162,11 @@ export type LambderCreateOptions<TSessionData = any> = {
|
|
|
102
162
|
apiPath?: string;
|
|
103
163
|
/**
|
|
104
164
|
* Stamped on every API answer's envelope as `apiVersion`, so a client can
|
|
105
|
-
* tell which build answered.
|
|
106
|
-
*
|
|
107
|
-
*
|
|
108
|
-
*
|
|
109
|
-
*
|
|
165
|
+
* tell which build answered. Staleness is decided per endpoint by the
|
|
166
|
+
* signature a client sends (see Lambder.apiSignatures()); only
|
|
167
|
+
* `minApiVersion` reads this string. Dotted numbers ("1.2.10"), as the
|
|
168
|
+
* floor compares them, so a commit sha or a build date is refused rather
|
|
169
|
+
* than read as zero.
|
|
110
170
|
*/
|
|
111
171
|
apiVersion?: string;
|
|
112
172
|
/**
|
|
@@ -114,10 +174,9 @@ export type LambderCreateOptions<TSessionData = any> = {
|
|
|
114
174
|
* it answers `versionExpired` whatever its signature says. The lever for
|
|
115
175
|
* a change the signatures cannot see (a security fix, a field whose
|
|
116
176
|
* meaning changed under the same shape). Dotted numbers ("1.2.10"),
|
|
117
|
-
* compared segment by segment; a call naming no version is not judged.
|
|
118
|
-
*
|
|
119
|
-
*
|
|
120
|
-
* none.
|
|
177
|
+
* compared segment by segment; a call naming no version is not judged. A
|
|
178
|
+
* floor above `apiVersion` is taken as `apiVersion`, with a warning, so a
|
|
179
|
+
* mistaken floor cannot refuse this build's own clients. Default: none.
|
|
121
180
|
*/
|
|
122
181
|
minApiVersion?: string;
|
|
123
182
|
/**
|
|
@@ -130,9 +189,11 @@ export type LambderCreateOptions<TSessionData = any> = {
|
|
|
130
189
|
*/
|
|
131
190
|
apiSignatures?: LambderApiSignatureMap;
|
|
132
191
|
/**
|
|
133
|
-
* Automatic compression for compressible responses. `true`
|
|
134
|
-
*
|
|
135
|
-
*
|
|
192
|
+
* Automatic compression for compressible responses. `true` is
|
|
193
|
+
* `{ minBytes: 860, encodings: ["br", "gzip"], quality: 5 }`, the default
|
|
194
|
+
* behind an HTTP API or a Function URL; behind a REST API it is off unless
|
|
195
|
+
* named here, since a REST API decodes base64 only for its
|
|
196
|
+
* binaryMediaTypes. `false` disables it. `encodings` is a preference order, so `["gzip"]` opts out
|
|
136
197
|
* of Brotli for a client or CDN that mishandles it, and `quality` is the
|
|
137
198
|
* Brotli quality, the same field the at-rest stores take.
|
|
138
199
|
*/
|
|
@@ -155,58 +216,75 @@ export type LambderCreateOptions<TSessionData = any> = {
|
|
|
155
216
|
* behind a proxy that rewrites it. Default: none, so ctx.ip is the address
|
|
156
217
|
* the gateway observed.
|
|
157
218
|
*
|
|
158
|
-
* Only list a header something in front of this app always overwrites
|
|
159
|
-
*
|
|
160
|
-
*
|
|
161
|
-
*
|
|
162
|
-
*
|
|
163
|
-
*
|
|
219
|
+
* Only list a header something in front of this app always overwrites:
|
|
220
|
+
* `per: "ip"` rate limits key off ctx.ip, and an address the caller picks
|
|
221
|
+
* per request is not a limit. API Gateway APPENDS to x-forwarded-for, so
|
|
222
|
+
* behind API Gateway alone the leftmost entry is the client's own claim
|
|
223
|
+
* and the header should be left out. A direct invoke reads none of these:
|
|
224
|
+
* its ctx.ip is the invoker's `clientIp`.
|
|
164
225
|
*/
|
|
165
226
|
trustedClientIpHeaders?: readonly string[];
|
|
227
|
+
/**
|
|
228
|
+
* Headers that may name the host the viewer asked for, in order of
|
|
229
|
+
* preference, e.g. ["x-forwarded-host"] for a Function URL behind
|
|
230
|
+
* CloudFront, which sends the origin its own lambda-url Host. Default:
|
|
231
|
+
* none, so ctx.host is the Host the gateway received.
|
|
232
|
+
*
|
|
233
|
+
* The same rule as trustedClientIpHeaders: ctx.host decides cookie
|
|
234
|
+
* domains and host-matched routes, and a host a client picks is a tenant
|
|
235
|
+
* a client picks. A direct invoke reads none of these: its ctx.host is the
|
|
236
|
+
* invoker's `host`.
|
|
237
|
+
*/
|
|
238
|
+
trustedHostHeaders?: readonly string[];
|
|
166
239
|
/** CORS: true allows any origin; or pass a LambderCorsConfig. Default: off. */
|
|
167
240
|
cors?: boolean | LambderCorsConfig;
|
|
168
241
|
/** Sessions over a store of your choosing; required for addSessionApi/addSessionRoute. */
|
|
169
242
|
session?: LambderSessionOptions<TSessionData>;
|
|
170
243
|
/** Declarative per-API rate limiting: your limiter plus named policies APIs reference (typed) via the `rateLimit` option. */
|
|
171
244
|
rateLimits?: LambderApiRateLimitsConfig<Record<string, LambderApiRateLimitPolicyConfig<LambderRenderContext>>>;
|
|
172
|
-
/**
|
|
173
|
-
|
|
245
|
+
/**
|
|
246
|
+
* Named guards APIs reference (typed) via the `guards` option; build each
|
|
247
|
+
* with `initLambder<SessionData>().guard()`, whose handlers see the app's
|
|
248
|
+
* session type, or lambderGuard(). Pinned to the render contexts, so a
|
|
249
|
+
* guard built for another adapter, or for another session type, is
|
|
250
|
+
* rejected here rather than reading fields that are not on its context.
|
|
251
|
+
*/
|
|
252
|
+
guards?: Record<string, LambderApiGuard<any, any, any, LambderRenderContext<any, Record<string, string>, {}, TSessionData>, LambderSessionRenderContext<any, TSessionData>>>;
|
|
174
253
|
/**
|
|
175
254
|
* Make an authorization declaration part of registering a session API:
|
|
176
|
-
* every addSessionApi must declare `guards`, at the type level (a
|
|
177
|
-
*
|
|
178
|
-
*
|
|
179
|
-
*
|
|
180
|
-
*
|
|
181
|
-
*
|
|
182
|
-
* from. Default: false.
|
|
255
|
+
* every addSessionApi must declare `guards`, at the type level (a missing
|
|
256
|
+
* `guards` is a compile error) and at registration (a plain-JS caller
|
|
257
|
+
* throws). An API whose session is the whole authorization (the
|
|
258
|
+
* signed-in user's own account) declares a named no-op session guard, so
|
|
259
|
+
* every opt-out is explicit and one grep lists them all. Needs a guards
|
|
260
|
+
* map to pick from. Default: false.
|
|
183
261
|
*/
|
|
184
262
|
requireSessionApiGuards?: boolean;
|
|
185
263
|
/**
|
|
186
264
|
* The same for public APIs: every addApi must declare `guards`, at the
|
|
187
265
|
* type level and at registration.
|
|
188
266
|
*
|
|
189
|
-
* Public APIs are open by default
|
|
190
|
-
*
|
|
191
|
-
*
|
|
192
|
-
*
|
|
193
|
-
*
|
|
194
|
-
*
|
|
195
|
-
*
|
|
196
|
-
*
|
|
197
|
-
* surface makes expensive to answer any other way. Needs a guards map to
|
|
198
|
-
* pick from. Default: false.
|
|
267
|
+
* Public APIs are open by default, which is the right default, so this is
|
|
268
|
+
* off unless an app decides otherwise. Turned on, a public endpoint's
|
|
269
|
+
* openness becomes a written decision rather than an omission: the ones
|
|
270
|
+
* anybody may call declare a named no-op guard carrying the reason, and
|
|
271
|
+
* the ones that authorize their caller some other way (a signature, a
|
|
272
|
+
* device secret, a one-shot token) name where that happens. One grep over
|
|
273
|
+
* the guard names then lists every public door and why it is open. Needs
|
|
274
|
+
* a guards map to pick from. Default: false.
|
|
199
275
|
*/
|
|
200
276
|
requirePublicApiGuards?: boolean;
|
|
201
277
|
/** Declarative idempotency: your store plus replay defaults; APIs opt in via `idempotency: true | { ttlSeconds }`. */
|
|
202
278
|
idempotency?: LambderApiIdempotencyConfig;
|
|
279
|
+
/** Crash reporting on every path, and who may read a crash in the answer. See LambderCrashOptions. */
|
|
280
|
+
crashes?: LambderCrashOptions;
|
|
203
281
|
};
|
|
204
282
|
/**
|
|
205
283
|
* What the `guards` field asks for when an API on a require*ApiGuards
|
|
206
|
-
* instance declares none.
|
|
207
|
-
*
|
|
208
|
-
*
|
|
209
|
-
*
|
|
284
|
+
* instance declares none. With nothing to infer from, the inference parameter
|
|
285
|
+
* defaults to `never`, and "Property 'guards' is missing ... but required in
|
|
286
|
+
* type { guards: never }" would read as though nothing could be written
|
|
287
|
+
* there; the property name says what is wanted.
|
|
210
288
|
*/
|
|
211
289
|
type LambderGuardsDeclarationRequired = {
|
|
212
290
|
readonly "lambder: this instance requires every API of this kind to declare guards. Name the guard that authorizes this API, or the named no-op guard that records why anyone may call it.": never;
|
|
@@ -236,19 +314,19 @@ export type LambderSessionEnabledInstance<TSessionsEnabled extends boolean> = TS
|
|
|
236
314
|
* only which flag switches it on differs.
|
|
237
315
|
*/
|
|
238
316
|
export type LambderRequirableGuardsField<TRequired extends boolean, TGuardsOpt> = TRequired extends true ? {
|
|
239
|
-
/** Named guards, run in declared order before input validation: a name, a non-empty list of names, or a non-empty { name: param } map for parameterized guards. Required on this instance: an API that needs no authorization declares a named no-op guard, so every opt-out is explicit and one grep lists them all. Their input requirements merge into this API's contract input; their return values land typed on ctx.guardData. */
|
|
317
|
+
/** Named guards, run in declared order before input validation (after it for a guard declared runAt: "afterInputValidation"): a name, a non-empty list of names, or a non-empty { name: param } map for parameterized guards. Required on this instance: an API that needs no authorization declares a named no-op guard, so every opt-out is explicit and one grep lists them all. Their input requirements merge into this API's contract input; their return values land typed on ctx.guardData. */
|
|
240
318
|
guards: [TGuardsOpt] extends [never] ? LambderGuardsDeclarationRequired : TGuardsOpt;
|
|
241
319
|
} : {
|
|
242
|
-
/** Named guards, run in declared order before input validation: a name, a non-empty list of names, or a non-empty { name: param } map for parameterized guards. Their input requirements merge into this API's contract input; their return values land typed on ctx.guardData. */
|
|
320
|
+
/** Named guards, run in declared order before input validation (after it for a guard declared runAt: "afterInputValidation"): a name, a non-empty list of names, or a non-empty { name: param } map for parameterized guards. Their input requirements merge into this API's contract input; their return values land typed on ctx.guardData. */
|
|
243
321
|
guards?: TGuardsOpt;
|
|
244
322
|
};
|
|
245
323
|
/**
|
|
246
324
|
* Rejects a key the options type does not have, which `const TOptions` would
|
|
247
325
|
* otherwise wave through: inferring a generic from an object literal switches
|
|
248
|
-
* excess-property checking off for the whole literal, so
|
|
249
|
-
* (no trailing "s") or `maxResponseByte` would
|
|
250
|
-
* and leave the app
|
|
251
|
-
* the two flags
|
|
326
|
+
* excess-property checking off for the whole literal, so
|
|
327
|
+
* `requireSessionApiGuard` (no trailing "s") or `maxResponseByte` would
|
|
328
|
+
* compile, be dropped in silence, and leave the app on the default. That is
|
|
329
|
+
* worst for the two require*ApiGuards flags, which exist to make a missing
|
|
252
330
|
* authorization declaration a compile error. Mapping every surplus key to
|
|
253
331
|
* `never` puts the error back on the key itself.
|
|
254
332
|
*/
|
|
@@ -270,22 +348,22 @@ export type LambderGivenOption<TOptions, TKey extends PropertyKey> = TKey extend
|
|
|
270
348
|
type LambderOptionShape<TSessionData, TKey extends keyof LambderCreateOptions<TSessionData>> = NonNullable<LambderCreateOptions<TSessionData>[TKey]>;
|
|
271
349
|
/**
|
|
272
350
|
* The surplus-key rule one level down, over the option objects a typo is
|
|
273
|
-
* worst on.
|
|
274
|
-
*
|
|
275
|
-
*
|
|
276
|
-
*
|
|
277
|
-
*
|
|
278
|
-
*
|
|
279
|
-
*
|
|
280
|
-
*
|
|
281
|
-
*
|
|
282
|
-
* its own option declares, so the error lands on the misspelled key.
|
|
351
|
+
* worst on. Excess-property checking is off for the WHOLE literal under
|
|
352
|
+
* `const TOptions`, nested objects included, and the top-level rule does not
|
|
353
|
+
* reach inside them. Unchecked, `idempotency: { failOpn: false }` would leave
|
|
354
|
+
* the engine failing open, `callerIdentitiy` would leave every public replay
|
|
355
|
+
* key a bearer token, `session: { tokenCookieKe }` would leave the session
|
|
356
|
+
* cookie under its default name, and `guards: { g: { sesion: true } }` would
|
|
357
|
+
* leave a guard reading a context with no session. Each nested object is
|
|
358
|
+
* checked against its own option's shape, so the error lands on the
|
|
359
|
+
* misspelled key.
|
|
283
360
|
*/
|
|
284
361
|
export type LambderNestedOptionChecks<TSessionData, TOptions extends LambderCreateOptions<TSessionData>> = {
|
|
285
362
|
session?: LambderNoExtraKeys<NonNullable<TOptions["session"]>, LambderOptionShape<TSessionData, "session">> & {
|
|
286
363
|
cookie?: LambderNoExtraKeys<NonNullable<NonNullable<TOptions["session"]>["cookie"]>, LambderSessionCookieOptions>;
|
|
287
364
|
};
|
|
288
365
|
idempotency?: LambderNoExtraKeys<NonNullable<TOptions["idempotency"]>, LambderOptionShape<TSessionData, "idempotency">>;
|
|
366
|
+
crashes?: LambderNoExtraKeys<NonNullable<TOptions["crashes"]>, LambderOptionShape<TSessionData, "crashes">>;
|
|
289
367
|
rateLimits?: LambderNoExtraKeys<NonNullable<TOptions["rateLimits"]>, LambderOptionShape<TSessionData, "rateLimits">> & {
|
|
290
368
|
policies?: {
|
|
291
369
|
[TPolicy in keyof NonNullable<TOptions["rateLimits"]>["policies"]]: LambderNoExtraKeys<NonNullable<TOptions["rateLimits"]>["policies"][TPolicy], LambderApiRateLimitPolicyConfig<LambderRenderContext>>;
|
|
@@ -303,12 +381,10 @@ export type LambderNestedOptionChecks<TSessionData, TOptions extends LambderCrea
|
|
|
303
381
|
compression?: TOptions["compression"] extends object ? LambderNoExtraKeys<TOptions["compression"], LambderResponseCompressionSettings> : unknown;
|
|
304
382
|
};
|
|
305
383
|
/**
|
|
306
|
-
* Everything create() refuses before an instance exists
|
|
307
|
-
*
|
|
308
|
-
*
|
|
309
|
-
*
|
|
310
|
-
* every API call (an apiPath with no leading slash) or a 500 on every response
|
|
311
|
-
* (maxResponseBytes: 0) that an app discovers in production.
|
|
384
|
+
* Everything create() refuses before an instance exists, in one place: a
|
|
385
|
+
* value that cannot work is a startup error naming the option, not a 404 on
|
|
386
|
+
* every API call (an apiPath with no leading slash) or a 500 on every
|
|
387
|
+
* response (maxResponseBytes: 0) that an app discovers in production.
|
|
312
388
|
*/
|
|
313
389
|
export declare const assertCreateOptions: (options: LambderCreateOptions<any>) => void;
|
|
314
390
|
export {};
|
|
@@ -1,36 +1,29 @@
|
|
|
1
1
|
import { assertPositiveInteger } from "../shared/util/LambderOptionChecks.js";
|
|
2
2
|
/**
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* would be dropped in silence while the store fell back to its own table
|
|
8
|
-
* defaults. A wrong table key is not something to discover from nobody being
|
|
9
|
-
* able to log in.
|
|
10
|
-
*/
|
|
11
|
-
const MOVED_SESSION_OPTIONS = ["tableName", "tableRegion", "partitionKey", "sortKey", "compression"];
|
|
12
|
-
/**
|
|
13
|
-
* Everything create() refuses before an instance exists.
|
|
14
|
-
*
|
|
15
|
-
* One place rather than five checks spread through the constructor's wiring:
|
|
16
|
-
* a value that cannot work is a startup error naming the option, not a 404 on
|
|
17
|
-
* every API call (an apiPath with no leading slash) or a 500 on every response
|
|
18
|
-
* (maxResponseBytes: 0) that an app discovers in production.
|
|
3
|
+
* Everything create() refuses before an instance exists, in one place: a
|
|
4
|
+
* value that cannot work is a startup error naming the option, not a 404 on
|
|
5
|
+
* every API call (an apiPath with no leading slash) or a 500 on every
|
|
6
|
+
* response (maxResponseBytes: 0) that an app discovers in production.
|
|
19
7
|
*/
|
|
20
8
|
export const assertCreateOptions = (options) => {
|
|
21
9
|
// The path is compared to ctx.path, which always starts with a slash, so
|
|
22
|
-
// apiPath: "api"
|
|
10
|
+
// apiPath: "api" would make every API call a 404 with no reason given.
|
|
23
11
|
if (options.apiPath !== undefined && (options.apiPath === "" || !options.apiPath.startsWith("/"))) {
|
|
24
12
|
throw new Error(`Lambder: apiPath must be a path starting with "/", got ${JSON.stringify(options.apiPath)}.`);
|
|
25
13
|
}
|
|
26
|
-
// 0 or a negative ceiling
|
|
14
|
+
// 0 or a negative ceiling would turn every response into the size guard's own 500.
|
|
27
15
|
if (options.maxResponseBytes !== undefined)
|
|
28
16
|
assertPositiveInteger(options.maxResponseBytes, "maxResponseBytes");
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
17
|
+
// 0 or a negative bound would give up on every report before it started.
|
|
18
|
+
if (options.crashes?.reportTimeoutMs !== undefined)
|
|
19
|
+
assertPositiveInteger(options.crashes.reportTimeoutMs, "crashes.reportTimeoutMs");
|
|
20
|
+
// Credentials with every origin allowed would echo whatever Origin asked,
|
|
21
|
+
// so any website could read a signed-in user's session routes. The usual
|
|
22
|
+
// reason to turn credentials on (SameSite=None cookies) is exactly the
|
|
23
|
+
// setting in which that is reachable.
|
|
24
|
+
const cors = options.cors;
|
|
25
|
+
if (typeof cors === "object" && cors.credentials && (cors.origins === undefined || cors.origins === "*")) {
|
|
26
|
+
throw new Error('Lambder: cors.credentials needs cors.origins to be an allowlist or a predicate. With every origin allowed, any website could make credentialed calls and read the answers.');
|
|
34
27
|
}
|
|
35
28
|
if ((options.requireSessionApiGuards || options.requirePublicApiGuards) && !options.guards) {
|
|
36
29
|
const requireFlag = options.requireSessionApiGuards ? "requireSessionApiGuards" : "requirePublicApiGuards";
|
|
@@ -1,9 +1,20 @@
|
|
|
1
1
|
import { LambderTemplatingEngine } from "./LambderTemplatingEngine.js";
|
|
2
2
|
import type { LambderFileSource } from "../shared/contracts/LambderFileSource.js";
|
|
3
|
-
|
|
3
|
+
import { LAMBDER_BACKEND_SWAP } from "../shared/util/LambderTestingDoors.js";
|
|
4
|
+
/**
|
|
5
|
+
* In-memory cache of files for warm invocations. Default: { maxBytes: 32MB,
|
|
6
|
+
* maxFileBytes: 2MB, missTtlSeconds: 60 }. false disables it, misses
|
|
7
|
+
* included.
|
|
8
|
+
*/
|
|
4
9
|
export type LambderFileMemoryCacheOption = false | {
|
|
5
10
|
maxBytes?: number;
|
|
6
11
|
maxFileBytes?: number;
|
|
12
|
+
/**
|
|
13
|
+
* How long a path the source had no file for is answered as missing
|
|
14
|
+
* without asking the source again; 0 asks every time. A file uploaded
|
|
15
|
+
* under such a path is served once this runs out.
|
|
16
|
+
*/
|
|
17
|
+
missTtlSeconds?: number;
|
|
7
18
|
};
|
|
8
19
|
/** The `files` option at creation: a source, or a source with its memory cache tuned or off. */
|
|
9
20
|
export type LambderFilesOption = LambderFileSource | {
|
|
@@ -24,12 +35,24 @@ export type LambderReadFile = {
|
|
|
24
35
|
*/
|
|
25
36
|
export declare class LambderFiles {
|
|
26
37
|
private source;
|
|
27
|
-
|
|
28
|
-
private
|
|
29
|
-
private
|
|
30
|
-
|
|
38
|
+
/** The files read, least recently served evicted first once the byte budget is spent. */
|
|
39
|
+
private readonly cache;
|
|
40
|
+
private readonly maxFileBytes;
|
|
41
|
+
/**
|
|
42
|
+
* Paths the source had no file for, until their TTL. An SPA asks for a
|
|
43
|
+
* file before it serves the shell for every page route, so without this
|
|
44
|
+
* each navigation would cost a source round trip (an S3 GetObject
|
|
45
|
+
* answering NoSuchKey) in warm containers too.
|
|
46
|
+
*/
|
|
47
|
+
private readonly misses;
|
|
31
48
|
private templates;
|
|
32
49
|
constructor(option: LambderFilesOption);
|
|
50
|
+
/**
|
|
51
|
+
* Puts the reader over another source, for `lambder/testing`. In place,
|
|
52
|
+
* because servePublicFiles holds this reader rather than the instance's
|
|
53
|
+
* field; both caches go with the source they were filled from.
|
|
54
|
+
*/
|
|
55
|
+
[LAMBDER_BACKEND_SWAP](source: LambderFileSource): void;
|
|
33
56
|
/**
|
|
34
57
|
* The file at a request or handler path (leading slash optional), mime
|
|
35
58
|
* type resolved; null when the path is invalid or the source has none.
|
|
@@ -43,6 +66,4 @@ export declare class LambderFiles {
|
|
|
43
66
|
template(path: string, options?: {
|
|
44
67
|
htmlVirtualSlots?: boolean;
|
|
45
68
|
}): Promise<LambderTemplatingEngine>;
|
|
46
|
-
/** Cache small files within the byte budget, evicting the oldest entries first. */
|
|
47
|
-
private remember;
|
|
48
69
|
}
|
|
@@ -1,21 +1,34 @@
|
|
|
1
1
|
import mimeTypeResolver from "mime-types";
|
|
2
|
+
import { LRUCache } from "lru-cache";
|
|
2
3
|
import { LambderTemplatingEngine } from "./LambderTemplatingEngine.js";
|
|
4
|
+
import { LAMBDER_BACKEND_SWAP } from "../shared/util/LambderTestingDoors.js";
|
|
3
5
|
const DEFAULT_MEMORY_CACHE_MAX_BYTES = 32 * 1024 * 1024;
|
|
4
6
|
const DEFAULT_MEMORY_CACHE_MAX_FILE_BYTES = 2 * 1024 * 1024;
|
|
7
|
+
const DEFAULT_MISS_TTL_SECONDS = 60;
|
|
8
|
+
/**
|
|
9
|
+
* The bytes the remembered misses may hold, each counted by its path and the
|
|
10
|
+
* entry overhead: a bot probing random paths churns through this rather than
|
|
11
|
+
* growing the map. Counted in bytes because the paths are the caller's: a
|
|
12
|
+
* count of 10,000 would let 8 KB paths hold 160 MB of a container's memory.
|
|
13
|
+
* About 12,000 misses of ordinary length.
|
|
14
|
+
*/
|
|
15
|
+
const MISS_MEMORY_MAX_BYTES = 4 * 1024 * 1024;
|
|
16
|
+
/** What an entry costs beside its bytes, so the byte budget also bounds how many entries it holds. */
|
|
17
|
+
const FILE_ENTRY_OVERHEAD_BYTES = 256;
|
|
5
18
|
/**
|
|
6
19
|
* The path a source is asked for, or null for one that names no file.
|
|
7
20
|
*
|
|
8
|
-
* This is the whole path rule, and it belongs to the reader:
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
21
|
+
* This is the whole path rule, and it belongs to the reader: a source that
|
|
22
|
+
* resolves the result against a base (a URL, a filesystem root) is safe only
|
|
23
|
+
* if it really is the plain relative path the interface promises. Every
|
|
24
|
+
* leading slash goes, since stripping only one would hand a source
|
|
25
|
+
* "//attacker.example/evil.html" as "/attacker.example/evil.html", which the
|
|
26
|
+
* HTTP source resolves as a protocol-relative reference: the app's origin
|
|
27
|
+
* credentials would go to a host the caller chose, and its bytes would come
|
|
28
|
+
* back under the app's own domain. Every segment is checked, not only ".."
|
|
29
|
+
* ones: an empty inner segment is how a host or a root gets back into the
|
|
30
|
+
* value, and a backslash is a separator to Windows paths and to every
|
|
31
|
+
* browser reading a Location.
|
|
19
32
|
*/
|
|
20
33
|
const toRelativePath = (target) => {
|
|
21
34
|
const relative = target.replace(/^\/+/, "");
|
|
@@ -33,17 +46,49 @@ const toRelativePath = (target) => {
|
|
|
33
46
|
*/
|
|
34
47
|
export class LambderFiles {
|
|
35
48
|
source;
|
|
49
|
+
/** The files read, least recently served evicted first once the byte budget is spent. */
|
|
36
50
|
cache;
|
|
37
|
-
cacheBytes = 0;
|
|
38
|
-
maxBytes;
|
|
39
51
|
maxFileBytes;
|
|
52
|
+
/**
|
|
53
|
+
* Paths the source had no file for, until their TTL. An SPA asks for a
|
|
54
|
+
* file before it serves the shell for every page route, so without this
|
|
55
|
+
* each navigation would cost a source round trip (an S3 GetObject
|
|
56
|
+
* answering NoSuchKey) in warm containers too.
|
|
57
|
+
*/
|
|
58
|
+
misses;
|
|
40
59
|
templates = new Map();
|
|
41
60
|
constructor(option) {
|
|
42
61
|
const { source, memoryCache } = "source" in option ? option : { source: option, memoryCache: undefined };
|
|
43
62
|
this.source = source;
|
|
44
|
-
|
|
45
|
-
this.maxBytes = memoryCache === false ? 0 : (memoryCache?.maxBytes ?? DEFAULT_MEMORY_CACHE_MAX_BYTES);
|
|
63
|
+
const maxBytes = memoryCache === false ? 0 : (memoryCache?.maxBytes ?? DEFAULT_MEMORY_CACHE_MAX_BYTES);
|
|
46
64
|
this.maxFileBytes = memoryCache === false ? 0 : (memoryCache?.maxFileBytes ?? DEFAULT_MEMORY_CACHE_MAX_FILE_BYTES);
|
|
65
|
+
const missTtlMs = memoryCache === false ? 0 : (memoryCache?.missTtlSeconds ?? DEFAULT_MISS_TTL_SECONDS) * 1000;
|
|
66
|
+
this.cache = maxBytes > 0
|
|
67
|
+
? new LRUCache({
|
|
68
|
+
maxSize: maxBytes,
|
|
69
|
+
// The key is a JS string, two bytes a character.
|
|
70
|
+
sizeCalculation: (entry, key) => entry.body.byteLength + key.length * 2 + FILE_ENTRY_OVERHEAD_BYTES,
|
|
71
|
+
})
|
|
72
|
+
: null;
|
|
73
|
+
this.misses = missTtlMs > 0
|
|
74
|
+
? new LRUCache({
|
|
75
|
+
maxSize: MISS_MEMORY_MAX_BYTES,
|
|
76
|
+
// The key is a JS string, two bytes a character.
|
|
77
|
+
sizeCalculation: (_miss, key) => key.length * 2 + FILE_ENTRY_OVERHEAD_BYTES,
|
|
78
|
+
ttl: missTtlMs,
|
|
79
|
+
})
|
|
80
|
+
: null;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Puts the reader over another source, for `lambder/testing`. In place,
|
|
84
|
+
* because servePublicFiles holds this reader rather than the instance's
|
|
85
|
+
* field; both caches go with the source they were filled from.
|
|
86
|
+
*/
|
|
87
|
+
[LAMBDER_BACKEND_SWAP](source) {
|
|
88
|
+
this.source = source;
|
|
89
|
+
this.cache?.clear();
|
|
90
|
+
this.misses?.clear();
|
|
91
|
+
this.templates.clear();
|
|
47
92
|
}
|
|
48
93
|
/**
|
|
49
94
|
* The file at a request or handler path (leading slash optional), mime
|
|
@@ -56,15 +101,25 @@ export class LambderFiles {
|
|
|
56
101
|
const cached = this.cache?.get(relativePath);
|
|
57
102
|
if (cached)
|
|
58
103
|
return cached;
|
|
104
|
+
if (this.misses?.has(relativePath))
|
|
105
|
+
return null;
|
|
59
106
|
const file = await this.source.read(relativePath);
|
|
60
|
-
if (!file)
|
|
107
|
+
if (!file) {
|
|
108
|
+
this.misses?.set(relativePath, true);
|
|
61
109
|
return null;
|
|
110
|
+
}
|
|
62
111
|
const entry = {
|
|
63
112
|
body: file.body,
|
|
64
|
-
|
|
113
|
+
// A source's own type as it gave it: the source knows the bytes'
|
|
114
|
+
// encoding (an S3 object stored as Latin-1 text/plain). Worked out
|
|
115
|
+
// from the extension, a text type carries a UTF-8 charset, so a
|
|
116
|
+
// .txt or an .html with no meta charset is not read in the
|
|
117
|
+
// browser's legacy encoding.
|
|
118
|
+
mimeType: file.mimeType || mimeTypeResolver.contentType(mimeTypeResolver.lookup(relativePath) || "application/octet-stream") || "application/octet-stream",
|
|
65
119
|
relativePath,
|
|
66
120
|
};
|
|
67
|
-
this.
|
|
121
|
+
if (this.cache && entry.body.byteLength <= this.maxFileBytes)
|
|
122
|
+
this.cache.set(relativePath, entry);
|
|
68
123
|
return entry;
|
|
69
124
|
}
|
|
70
125
|
/**
|
|
@@ -84,19 +139,4 @@ export class LambderFiles {
|
|
|
84
139
|
this.templates.set(key, template);
|
|
85
140
|
return template;
|
|
86
141
|
}
|
|
87
|
-
/** Cache small files within the byte budget, evicting the oldest entries first. */
|
|
88
|
-
remember(entry) {
|
|
89
|
-
if (!this.cache || entry.body.length > this.maxFileBytes)
|
|
90
|
-
return;
|
|
91
|
-
for (const [key, value] of this.cache) {
|
|
92
|
-
if (this.cacheBytes + entry.body.length <= this.maxBytes)
|
|
93
|
-
break;
|
|
94
|
-
this.cache.delete(key);
|
|
95
|
-
this.cacheBytes -= value.body.length;
|
|
96
|
-
}
|
|
97
|
-
if (this.cacheBytes + entry.body.length <= this.maxBytes) {
|
|
98
|
-
this.cache.set(entry.relativePath, entry);
|
|
99
|
-
this.cacheBytes += entry.body.length;
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
142
|
}
|