@everystack/server 0.2.19 → 0.2.20
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/package.json +1 -1
- package/src/index.ts +26 -4
- package/src/plugin.ts +35 -4
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -55,9 +55,22 @@ export interface LogSink {
|
|
|
55
55
|
|
|
56
56
|
/** Cache-Control configuration for the Lambda handler's fallback headers. */
|
|
57
57
|
export interface ServerCacheConfig {
|
|
58
|
-
/** Unauthenticated GET/HEAD responses.
|
|
58
|
+
/** Unauthenticated GET/HEAD responses with a non-HTML body (e.g. JSON API).
|
|
59
59
|
* Default: 'public, max-age=60, s-maxage=2592000, stale-while-revalidate=5' */
|
|
60
60
|
public?: string;
|
|
61
|
+
/** Unauthenticated GET/HEAD HTML responses (SSR documents).
|
|
62
|
+
* INVARIANT: SSR HTML is a PUBLIC shell — it must NOT be personalized.
|
|
63
|
+
* Personalization happens on client rehydrate, never in the server-rendered
|
|
64
|
+
* document. This is what makes public edge-caching safe; rendering per-user
|
|
65
|
+
* data into the SSR HTML would let the edge serve one user's page to another.
|
|
66
|
+
*
|
|
67
|
+
* HTML pins content-hashed chunk names, so it is the mutable entry point and
|
|
68
|
+
* must turn over fast — unlike the immutable chunks it references. A short
|
|
69
|
+
* s-maxage bounds edge staleness (incl. the version-key empty-`_v` fallback)
|
|
70
|
+
* so `cache:purge --origin web` / any deploy refreshes HTML within the TTL,
|
|
71
|
+
* with no CloudFront invalidation. No stale-while-revalidate: the cap is hard.
|
|
72
|
+
* Default: 'public, max-age=0, s-maxage=60' */
|
|
73
|
+
html?: string;
|
|
61
74
|
/** Authenticated GET/HEAD responses.
|
|
62
75
|
* Default: 'private, no-store' */
|
|
63
76
|
private?: string;
|
|
@@ -260,9 +273,18 @@ export function createLambdaHandler(
|
|
|
260
273
|
if (!result.headers['cache-control']) {
|
|
261
274
|
if (method === 'GET' || method === 'HEAD') {
|
|
262
275
|
const isAuthenticated = !!httpEvent.headers?.authorization;
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
276
|
+
if (isAuthenticated) {
|
|
277
|
+
result.headers['cache-control'] = options.cache?.private ?? 'private, no-store';
|
|
278
|
+
} else {
|
|
279
|
+
// SSR HTML is a PUBLIC shell (never personalized — personalization
|
|
280
|
+
// happens on client rehydrate), so it caches public but must turn
|
|
281
|
+
// over fast: short s-maxage, no SWR → hard ≤60s edge cap. JSON/API
|
|
282
|
+
// GETs are version-keyed and can cache long.
|
|
283
|
+
const isHtml = String(result.headers['content-type'] || '').includes('text/html');
|
|
284
|
+
result.headers['cache-control'] = isHtml
|
|
285
|
+
? (options.cache?.html ?? 'public, max-age=0, s-maxage=60')
|
|
286
|
+
: (options.cache?.public ?? 'public, max-age=60, s-maxage=2592000, stale-while-revalidate=5');
|
|
287
|
+
}
|
|
266
288
|
} else {
|
|
267
289
|
result.headers['cache-control'] = options.cache?.mutation ?? 'no-store';
|
|
268
290
|
}
|
package/src/plugin.ts
CHANGED
|
@@ -240,9 +240,17 @@ export function createPluginLambdaHandler(
|
|
|
240
240
|
if (!result.headers['cache-control']) {
|
|
241
241
|
if (method === 'GET' || method === 'HEAD') {
|
|
242
242
|
const isAuthenticated = !!httpEvent.headers?.authorization;
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
243
|
+
if (isAuthenticated) {
|
|
244
|
+
result.headers['cache-control'] = options.cache?.private ?? 'private, no-store';
|
|
245
|
+
} else {
|
|
246
|
+
// SSR HTML is the mutable, version-pinning document — it must turn
|
|
247
|
+
// over fast (short s-maxage, no SWR → hard ≤60s edge cap). JSON/API
|
|
248
|
+
// GETs are version-keyed and can cache long at the edge.
|
|
249
|
+
const isHtml = String(result.headers['content-type'] || '').includes('text/html');
|
|
250
|
+
result.headers['cache-control'] = isHtml
|
|
251
|
+
? (options.cache?.html ?? 'public, max-age=0, s-maxage=60')
|
|
252
|
+
: (options.cache?.public ?? 'public, max-age=60, s-maxage=2592000, stale-while-revalidate=5');
|
|
253
|
+
}
|
|
246
254
|
} else {
|
|
247
255
|
result.headers['cache-control'] = options.cache?.mutation ?? 'no-store';
|
|
248
256
|
}
|
|
@@ -363,6 +371,20 @@ interface SsrContext extends PluginContext {
|
|
|
363
371
|
* fallback: ssrPlugin(),
|
|
364
372
|
* fallback: ssrPlugin({ postProcessHtml: injectJsonLd }),
|
|
365
373
|
*/
|
|
374
|
+
/**
|
|
375
|
+
* Re-stamp an SSR response as private/uncacheable, preserving status + body.
|
|
376
|
+
* Used when the SSR ran with a resolved cookie-user (potentially personalized).
|
|
377
|
+
*/
|
|
378
|
+
function withPrivateCache(response: Response): Response {
|
|
379
|
+
const headers = new Headers(response.headers);
|
|
380
|
+
headers.set('cache-control', 'private, no-store');
|
|
381
|
+
return new Response(response.body, {
|
|
382
|
+
status: response.status,
|
|
383
|
+
statusText: response.statusText,
|
|
384
|
+
headers,
|
|
385
|
+
});
|
|
386
|
+
}
|
|
387
|
+
|
|
366
388
|
export function ssrPlugin(options: SsrPluginOptions = {}): (ctx: PluginContext) => Promise<Handler> {
|
|
367
389
|
return async (ctx: PluginContext): Promise<Handler> => {
|
|
368
390
|
const { getWebHandler } = await import('./ssr');
|
|
@@ -405,7 +427,16 @@ export function ssrPlugin(options: SsrPluginOptions = {}): (ctx: PluginContext)
|
|
|
405
427
|
}
|
|
406
428
|
}
|
|
407
429
|
}
|
|
408
|
-
|
|
430
|
+
const response = await runWithAuthContext(user, () => webHandler(request));
|
|
431
|
+
// SECURITY: when the SSR ran with a cookie-resolved user, the response
|
|
432
|
+
// may be personalized. The edge cache-control default keys on the
|
|
433
|
+
// Authorization header (absent for cookie auth), so without this such a
|
|
434
|
+
// response would be cached `public` and could be served to other users
|
|
435
|
+
// (HTML 60s, but loader-data JSON 30 days). Force it private so a
|
|
436
|
+
// personalized SSR response can never be public-cached. Anonymous SSR
|
|
437
|
+
// (no resolved user) stays publicly cacheable — the public shell / SEO
|
|
438
|
+
// path, which by invariant is never personalized.
|
|
439
|
+
return user ? withPrivateCache(response) : response;
|
|
409
440
|
}
|
|
410
441
|
|
|
411
442
|
return webHandler(request);
|