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.
Files changed (209) hide show
  1. package/CHANGELOG.md +1021 -3
  2. package/README.md +43 -21
  3. package/dist/api/LambderApiAnswer.d.ts +18 -22
  4. package/dist/api/LambderApiAnswer.js +6 -7
  5. package/dist/api/LambderApiCallContext.d.ts +21 -8
  6. package/dist/api/LambderApiCallContext.js +22 -4
  7. package/dist/api/LambderApiDefinition.d.ts +4 -3
  8. package/dist/api/LambderApiEnvelope.d.ts +14 -9
  9. package/dist/api/LambderApiEnvelope.js +33 -34
  10. package/dist/api/LambderApiGuards.d.ts +78 -51
  11. package/dist/api/LambderApiGuards.js +34 -36
  12. package/dist/api/LambderApiIdempotency.d.ts +74 -61
  13. package/dist/api/LambderApiIdempotency.js +226 -151
  14. package/dist/api/LambderApiOutputValidationError.d.ts +32 -0
  15. package/dist/api/LambderApiOutputValidationError.js +50 -0
  16. package/dist/api/LambderApiPipeline.d.ts +77 -39
  17. package/dist/api/LambderApiPipeline.js +135 -62
  18. package/dist/api/LambderApiRateLimits.d.ts +208 -54
  19. package/dist/api/LambderApiRateLimits.js +197 -108
  20. package/dist/api/LambderApiRequest.d.ts +27 -21
  21. package/dist/api/LambderApiRequest.js +26 -19
  22. package/dist/api/LambderApiSignature.d.ts +12 -15
  23. package/dist/api/LambderApiSignature.js +28 -51
  24. package/dist/api/LambderApiValidationRefusal.d.ts +9 -9
  25. package/dist/api/LambderApiValidationRefusal.js +10 -10
  26. package/dist/build/freshProcessVerifier.d.ts +13 -0
  27. package/dist/build/freshProcessVerifier.js +19 -0
  28. package/dist/build/writeApiSignatures.d.ts +109 -0
  29. package/dist/build/writeApiSignatures.js +222 -0
  30. package/dist/build.d.ts +9 -0
  31. package/dist/build.js +8 -0
  32. package/dist/client/LambderCaller.d.ts +13 -44
  33. package/dist/client/LambderCaller.js +77 -84
  34. package/dist/client/LambderReloadLoopBreaker.d.ts +56 -26
  35. package/dist/client/LambderReloadLoopBreaker.js +90 -46
  36. package/dist/client/lambderFetchTransport.d.ts +4 -1
  37. package/dist/client/lambderFetchTransport.js +52 -28
  38. package/dist/client.d.ts +5 -3
  39. package/dist/client.js +2 -1
  40. package/dist/core/Lambder.d.ts +161 -69
  41. package/dist/core/Lambder.js +370 -226
  42. package/dist/core/LambderContext.d.ts +82 -15
  43. package/dist/core/LambderContext.js +107 -20
  44. package/dist/core/LambderCors.d.ts +21 -3
  45. package/dist/core/LambderCors.js +35 -16
  46. package/dist/core/LambderCrashHandling.d.ts +40 -0
  47. package/dist/core/LambderCrashHandling.js +97 -0
  48. package/dist/core/LambderCreateOptions.d.ts +151 -75
  49. package/dist/core/LambderCreateOptions.js +16 -23
  50. package/dist/core/LambderFiles.d.ts +28 -7
  51. package/dist/core/LambderFiles.js +73 -33
  52. package/dist/core/LambderIndexHtml.js +12 -11
  53. package/dist/core/LambderPolicyBuilders.d.ts +17 -5
  54. package/dist/core/LambderPolicyBuilders.js +17 -5
  55. package/dist/core/LambderPublicFiles.d.ts +11 -5
  56. package/dist/core/LambderPublicFiles.js +32 -4
  57. package/dist/core/LambderRequestPath.d.ts +43 -0
  58. package/dist/core/LambderRequestPath.js +63 -0
  59. package/dist/core/LambderResponse.d.ts +26 -5
  60. package/dist/core/LambderResponse.js +157 -70
  61. package/dist/core/LambderResponseBuilder.d.ts +49 -4
  62. package/dist/core/LambderResponseBuilder.js +64 -3
  63. package/dist/core/LambderRouting.d.ts +2 -3
  64. package/dist/core/LambderRouting.js +22 -7
  65. package/dist/core/LambderTemplatingEngine.js +211 -32
  66. package/dist/index.d.ts +15 -8
  67. package/dist/index.js +5 -4
  68. package/dist/invoke/LambderInvokeCaller.d.ts +37 -42
  69. package/dist/invoke/LambderInvokeCaller.js +76 -66
  70. package/dist/invoke/LambderInvokeOutcome.d.ts +27 -26
  71. package/dist/invoke/LambderInvokeOutcome.js +9 -22
  72. package/dist/invoke/LambderLambdaEvent.d.ts +44 -10
  73. package/dist/invoke/LambderLambdaEvent.js +80 -37
  74. package/dist/invoke/lambderHandlerTransport.d.ts +12 -10
  75. package/dist/invoke/lambderHandlerTransport.js +16 -19
  76. package/dist/mock/LambderMockApp.d.ts +67 -83
  77. package/dist/mock/LambderMockApp.js +167 -153
  78. package/dist/mock/LambderMockBrowserCookies.d.ts +24 -28
  79. package/dist/mock/LambderMockBrowserCookies.js +24 -28
  80. package/dist/mock/LambderMockCallRecorder.d.ts +15 -22
  81. package/dist/mock/LambderMockCallRecorder.js +19 -28
  82. package/dist/mock/LambderMockCreateOptions.d.ts +42 -24
  83. package/dist/mock/LambderMockEntryRegistry.d.ts +11 -12
  84. package/dist/mock/LambderMockEntryRegistry.js +24 -29
  85. package/dist/mock/LambderMockFailureInjector.d.ts +3 -6
  86. package/dist/mock/LambderMockFailureInjector.js +3 -6
  87. package/dist/mock/LambderMockTypes.d.ts +78 -108
  88. package/dist/mock/lambderMockInvokeTransport.d.ts +11 -13
  89. package/dist/mock/lambderMockInvokeTransport.js +11 -10
  90. package/dist/mock/lambderMockMswHandler.d.ts +33 -29
  91. package/dist/mock/lambderMockMswHandler.js +50 -39
  92. package/dist/mock.d.ts +3 -1
  93. package/dist/mock.js +5 -3
  94. package/dist/session/LambderSessionController.d.ts +108 -89
  95. package/dist/session/LambderSessionController.js +187 -168
  96. package/dist/session/LambderSessionCrypto.d.ts +16 -7
  97. package/dist/session/LambderSessionCrypto.js +26 -12
  98. package/dist/session/LambderSessionManager.d.ts +136 -47
  99. package/dist/session/LambderSessionManager.js +280 -139
  100. package/dist/shared/LambderHtml.d.ts +42 -3
  101. package/dist/shared/LambderHtml.js +127 -7
  102. package/dist/shared/LambderHtmlPositions.d.ts +173 -0
  103. package/dist/shared/LambderHtmlPositions.js +652 -0
  104. package/dist/shared/LambderI18n.d.ts +10 -11
  105. package/dist/shared/LambderI18n.js +33 -21
  106. package/dist/shared/contracts/LambderCache.d.ts +66 -0
  107. package/dist/shared/contracts/LambderCache.js +11 -0
  108. package/dist/shared/contracts/LambderFileSource.d.ts +6 -6
  109. package/dist/shared/contracts/LambderFileSource.js +5 -8
  110. package/dist/shared/contracts/LambderIdempotencyStore.d.ts +51 -22
  111. package/dist/shared/contracts/LambderIdempotencyStore.js +4 -5
  112. package/dist/shared/contracts/LambderRateLimiter.d.ts +27 -15
  113. package/dist/shared/contracts/LambderRateLimiter.js +4 -5
  114. package/dist/shared/contracts/LambderSessionStore.d.ts +65 -26
  115. package/dist/shared/contracts/LambderSessionStore.js +5 -6
  116. package/dist/shared/transport/LambderApiTransport.d.ts +27 -27
  117. package/dist/shared/transport/LambderApiTransport.js +7 -7
  118. package/dist/shared/transport/LambderCookieJar.d.ts +28 -35
  119. package/dist/shared/transport/LambderCookieJar.js +54 -66
  120. package/dist/shared/transport/lambderCookieJarTransport.d.ts +11 -13
  121. package/dist/shared/transport/lambderCookieJarTransport.js +24 -23
  122. package/dist/shared/util/LambderCallAbort.d.ts +5 -5
  123. package/dist/shared/util/LambderCallAbort.js +5 -5
  124. package/dist/shared/util/LambderClientIp.d.ts +27 -11
  125. package/dist/shared/util/LambderClientIp.js +96 -13
  126. package/dist/shared/util/LambderExpiringMap.d.ts +35 -49
  127. package/dist/shared/util/LambderExpiringMap.js +41 -57
  128. package/dist/shared/util/LambderNodeModules.js +6 -7
  129. package/dist/shared/util/LambderOptionChecks.d.ts +4 -4
  130. package/dist/shared/util/LambderOptionChecks.js +4 -4
  131. package/dist/shared/util/LambderResponseBrand.d.ts +5 -5
  132. package/dist/shared/util/LambderResponseBrand.js +5 -5
  133. package/dist/shared/util/LambderTestingDoors.d.ts +29 -0
  134. package/dist/shared/util/LambderTestingDoors.js +29 -0
  135. package/dist/shared/util/LambderTypeUtilities.d.ts +7 -8
  136. package/dist/shared/util/LambderTypeUtilities.js +3 -3
  137. package/dist/shared/util/boundKeyField.d.ts +20 -0
  138. package/dist/shared/util/boundKeyField.js +34 -0
  139. package/dist/shared/util/canonicalJson.d.ts +11 -0
  140. package/dist/shared/util/canonicalJson.js +28 -0
  141. package/dist/shared/util/joinKeyFields.d.ts +20 -0
  142. package/dist/shared/util/joinKeyFields.js +22 -0
  143. package/dist/shared/wire/LambderAnswerHeaders.d.ts +12 -16
  144. package/dist/shared/wire/LambderAnswerHeaders.js +12 -16
  145. package/dist/shared/wire/LambderApiContract.d.ts +107 -32
  146. package/dist/shared/wire/LambderApiOutcome.d.ts +43 -31
  147. package/dist/shared/wire/LambderApiOutcome.js +48 -23
  148. package/dist/shared/wire/LambderApiRefusal.d.ts +39 -27
  149. package/dist/shared/wire/LambderApiRefusal.js +36 -7
  150. package/dist/shared/wire/LambderApiSignature.d.ts +18 -22
  151. package/dist/shared/wire/LambderApiSignature.js +16 -19
  152. package/dist/shared/wire/LambderCallOptions.d.ts +38 -47
  153. package/dist/shared/wire/LambderCallOptions.js +9 -11
  154. package/dist/shared/wire/LambderCompressionCodec.d.ts +29 -34
  155. package/dist/shared/wire/LambderCompressionCodec.js +31 -36
  156. package/dist/shared/wire/LambderCompressionOption.d.ts +9 -9
  157. package/dist/shared/wire/LambderCompressionOption.js +9 -9
  158. package/dist/shared/wire/LambderCrashDetail.d.ts +12 -15
  159. package/dist/shared/wire/LambderCrashDetail.js +12 -15
  160. package/dist/shared/wire/LambderDefaultApiPath.d.ts +6 -0
  161. package/dist/shared/wire/LambderDefaultApiPath.js +6 -0
  162. package/dist/shared/wire/LambderHttpStatus.d.ts +6 -7
  163. package/dist/shared/wire/LambderIdempotencyKeyScope.d.ts +89 -0
  164. package/dist/shared/wire/LambderIdempotencyKeyScope.js +146 -0
  165. package/dist/shared/wire/LambderInvokeApiId.d.ts +27 -0
  166. package/dist/shared/wire/LambderInvokeApiId.js +27 -0
  167. package/dist/shared/wire/LambderOutcomeAssertions.d.ts +79 -0
  168. package/dist/shared/wire/LambderOutcomeAssertions.js +112 -0
  169. package/dist/shared/wire/LambderRequestPayload.d.ts +18 -20
  170. package/dist/shared/wire/LambderRequestPayload.js +4 -6
  171. package/dist/stores/LambderCacheFiller.d.ts +48 -0
  172. package/dist/stores/LambderCacheFiller.js +119 -0
  173. package/dist/stores/LambderCacheKeys.d.ts +26 -0
  174. package/dist/stores/LambderCacheKeys.js +54 -0
  175. package/dist/stores/LambderCacheValues.d.ts +45 -0
  176. package/dist/stores/LambderCacheValues.js +74 -0
  177. package/dist/stores/LambderDdbCache.d.ts +121 -56
  178. package/dist/stores/LambderDdbCache.js +528 -225
  179. package/dist/stores/LambderDdbIdempotencyStore.d.ts +33 -22
  180. package/dist/stores/LambderDdbIdempotencyStore.js +75 -50
  181. package/dist/stores/LambderDdbRateLimiter.d.ts +76 -20
  182. package/dist/stores/LambderDdbRateLimiter.js +151 -39
  183. package/dist/stores/LambderDdbSdk.d.ts +43 -31
  184. package/dist/stores/LambderDdbSdk.js +79 -33
  185. package/dist/stores/LambderDdbSessionStore.d.ts +27 -14
  186. package/dist/stores/LambderDdbSessionStore.js +119 -47
  187. package/dist/stores/LambderHttpFileSource.d.ts +15 -6
  188. package/dist/stores/LambderHttpFileSource.js +15 -13
  189. package/dist/stores/LambderMemoryCache.d.ts +49 -0
  190. package/dist/stores/LambderMemoryCache.js +113 -0
  191. package/dist/stores/LambderMemoryIdempotencyStore.d.ts +13 -12
  192. package/dist/stores/LambderMemoryIdempotencyStore.js +31 -30
  193. package/dist/stores/LambderMemoryRateLimiter.d.ts +8 -9
  194. package/dist/stores/LambderMemoryRateLimiter.js +14 -13
  195. package/dist/stores/LambderMemorySessionStore.d.ts +14 -11
  196. package/dist/stores/LambderMemorySessionStore.js +38 -19
  197. package/dist/stores/LambderS3FileSource.d.ts +21 -6
  198. package/dist/stores/LambderS3FileSource.js +12 -7
  199. package/dist/testing/LambderTestApp.d.ts +176 -0
  200. package/dist/testing/LambderTestApp.js +204 -0
  201. package/dist/testing/LambderTestVisitor.d.ts +153 -0
  202. package/dist/testing/LambderTestVisitor.js +154 -0
  203. package/dist/testing.d.ts +27 -0
  204. package/dist/testing.js +24 -0
  205. package/package.json +20 -3
  206. package/dist/api/LambderApiPolicyEngine.d.ts +0 -36
  207. package/dist/api/LambderApiPolicyEngine.js +0 -77
  208. package/dist/shared/util/LambderKeyFields.d.ts +0 -32
  209. 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
- * Typed over `any` rather than over TSessionData deliberately. The session
56
- * data type is the app's declaration (initLambder<SessionData>()), and a
57
- * store is a storage backend that holds whatever the app puts in it;
58
- * naming TSessionData here would make `new Lambder({ session: { store } })`
59
- * INFER the session data type from the store instead, so an app that never
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
- /** Salts the sessionKey hash that partitions the store. */
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
- * guards, idempotency) and session/CORS config. There are no enable/define
89
- * chain methods; the instance is born fully configured and fully typed
90
- * (via initLambder), so no ordering rules exist and no partially-configured
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. Whether a client is stale is decided per
106
- * endpoint by the signature it sends (see Lambder.apiSignatures()), not
107
- * by this string; `minApiVersion` is the one thing that reads it. Dotted
108
- * numbers ("1.2.10"), since that is how the floor compares it, so a
109
- * commit sha or a build date is refused rather than read as zero.
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
- * A floor above `apiVersion` is taken as `apiVersion`, with a warning,
119
- * so a mistaken floor cannot refuse this build's own clients. Default:
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` (the default)
134
- * is `{ minBytes: 860, encodings: ["br", "gzip"], quality: 5 }`; `false`
135
- * disables it. `encodings` is a preference order, so `["gzip"]` opts out
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. A
159
- * header a client can set is a value a client can choose, and `per: "ip"`
160
- * rate limits key off ctx.ip: one that a caller picks per request is not a
161
- * limit. Note that API Gateway APPENDS to x-forwarded-for rather than
162
- * replacing it, so behind API Gateway alone the leftmost entry is the
163
- * client's own claim and the header should be left out.
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
- /** Named guards APIs reference (typed) via the `guards` option; build each with lambderGuard(). Pinned to the render contexts, so a guard built for another adapter is rejected here rather than reading fields that are not on its context. */
173
- guards?: Record<string, LambderApiGuard<any, any, any, LambderRenderContext, LambderSessionRenderContext<any, any>>>;
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
- * missing `guards` is a compile error) and at registration (a plain-JS
178
- * caller throws). An API that legitimately needs none, because the
179
- * session itself is the whole authorization (the signed-in user's own
180
- * account), declares a named no-op session guard, so every opt-out is
181
- * explicit and one grep lists them all. Needs a guards map to pick
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 and that is the right default, so this
190
- * is off unless an app decides otherwise. What it buys an app that turns
191
- * it on is that a public endpoint's openness becomes a written decision
192
- * rather than an omission: the ones anybody may call declare a named no-op
193
- * guard carrying the reason, and the ones that authorize their caller some
194
- * other way (a signature, a device secret, a one-shot token) name where
195
- * that happens. One grep over the guard names then lists every public
196
- * door and why it is open, which is the review question a growing public
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. The inference parameter defaults to `never` with
207
- * nothing to infer from, and the resulting "Property 'guards' is missing ...
208
- * but required in type { guards: never }" read as though nothing could ever
209
- * be written there; the property name says what is actually wanted.
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 `requireSessionApiGuard`
249
- * (no trailing "s") or `maxResponseByte` would compile, be dropped in silence,
250
- * and leave the app running with the default. That matters most for exactly
251
- * the two flags a typo is worst on, since both exist to make a missing
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
- * Excess-property checking is off for the WHOLE literal under `const
276
- * TOptions`, nested objects included, so the top-level rule caught nothing
277
- * where it mattered most: `idempotency: { failOpn: false }` left the engine
278
- * failing open, `callerIdentitiy` left every public replay key a bearer
279
- * token, `session: { tokenCookieKe }` left the session cookie under its
280
- * default name, and `guards: { g: { sesion: true } }` left a guard reading a
281
- * context with no session. Each nested object is checked against the shape
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
- * One place rather than five checks spread through the constructor's wiring:
309
- * a value that cannot work is a startup error naming the option, not a 404 on
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
- * The session fields that moved onto LambderDdbSessionStore in 7.0.0, refused
4
- * at creation. They are the one break the compiler cannot find: create() is
5
- * generic over `const TOptions`, which switches excess-property checking off
6
- * for the whole options object, so a leftover `partitionKey` compiles and
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" made every API call a 404 and nothing said why.
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 turned every response into the size guard's own 500.
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
- const session = options.session;
30
- const movedOptions = session ? MOVED_SESSION_OPTIONS.filter((key) => key in session) : [];
31
- if (movedOptions.length) {
32
- throw new Error(`Lambder: the session option no longer takes ${movedOptions.join(", ")}. `
33
- + "They belong to the store now: session: { store: new LambderDdbSessionStore({ tableName, region, partitionKey, sortKey, compression }), sessionSalt }.");
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
- /** In-memory cache of files for warm invocations. Default: { maxBytes: 32MB, maxFileBytes: 2MB }. false disables it. */
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
- private cache;
28
- private cacheBytes;
29
- private maxBytes;
30
- private maxFileBytes;
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: every source is
9
- * handed the result, and a source that resolves it against a base (a URL, a
10
- * filesystem root) is safe only if the value really is the plain relative
11
- * path the interface promises. Stripping only ONE leading slash would hand a
12
- * source "//attacker.example/evil.html" as "/attacker.example/evil.html",
13
- * which the HTTP source resolves as a protocol-relative reference: the app's
14
- * origin credentials would go to a host the caller chose and its bytes would
15
- * come back under the app's own domain. So every leading slash goes, and every segment is
16
- * checked rather than only the ".." ones: an empty inner segment is how a
17
- * host or a root gets back into the value, and a backslash is a separator to
18
- * Windows paths and to every browser reading a Location.
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
- this.cache = memoryCache === false ? null : new Map();
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
- mimeType: file.mimeType || mimeTypeResolver.lookup(relativePath) || "application/octet-stream",
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.remember(entry);
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
  }