@ultimat3/http 22.7.0 → 22.7.1
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 +5 -5
- package/src/cache-policy.ts +13 -0
- package/src/cache-stage.ts +47 -0
- package/src/stages.ts +3 -22
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ultimat3/http",
|
|
3
|
-
"version": "22.7.
|
|
3
|
+
"version": "22.7.1",
|
|
4
4
|
"description": "Owned request lifecycle over Bun.serve: router, ordered pipeline, problem+json errors",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -31,9 +31,9 @@
|
|
|
31
31
|
"test": "bun test"
|
|
32
32
|
},
|
|
33
33
|
"dependencies": {
|
|
34
|
-
"@ultimat3/core": "22.7.
|
|
35
|
-
"@ultimat3/i18n": "22.7.
|
|
36
|
-
"@ultimat3/schema": "22.7.
|
|
37
|
-
"@ultimat3/time": "22.7.
|
|
34
|
+
"@ultimat3/core": "22.7.1",
|
|
35
|
+
"@ultimat3/i18n": "22.7.1",
|
|
36
|
+
"@ultimat3/schema": "22.7.1",
|
|
37
|
+
"@ultimat3/time": "22.7.1"
|
|
38
38
|
}
|
|
39
39
|
}
|
package/src/cache-policy.ts
CHANGED
|
@@ -49,3 +49,16 @@ export const defaultCache = (route: Route | undefined, actor: Actor): CacheHint
|
|
|
49
49
|
*/
|
|
50
50
|
export const reviewedHint = (hint: CacheHint, actor: Actor): CacheHint =>
|
|
51
51
|
hint.mode === 'public' && !isAnonymous(actor) ? PRIVATE_CACHE : hint;
|
|
52
|
+
|
|
53
|
+
const REPLAYABLE_METHODS: ReadonlySet<string> = new Set(['GET', 'HEAD']);
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Whether a shared cache could ever replay this EXCHANGE to someone else. Only a safe method's
|
|
57
|
+
* answer can be (a CDN keys on the URL, and a POST's answer is a function of its body), and never
|
|
58
|
+
* a 4xx/5xx: a `401` + `WWW-Authenticate` is a challenge to THIS caller, an RFC 6749 §5.1 error
|
|
59
|
+
* must be `no-store`, and the next request may well succeed. The route default and a declared
|
|
60
|
+
* shared offer were both decided without the method or the status, so an anonymous `POST /mcp`
|
|
61
|
+
* refused `401` went out `public, s-maxage=60, stale-while-revalidate=600`.
|
|
62
|
+
*/
|
|
63
|
+
export const replayableExchange = (method: string, status: number): boolean =>
|
|
64
|
+
REPLAYABLE_METHODS.has(method.toUpperCase()) && status < 400;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
// Single responsibility: the `cache-headers` stage's final answer — the one owner of what a
|
|
2
|
+
// response may be cached as. Split out of `stages.ts` when the exchange rule (method + status)
|
|
3
|
+
// joined the route default, the declared hint and the review of a handler's own header.
|
|
4
|
+
|
|
5
|
+
import { isAnonymous } from '@ultimat3/core';
|
|
6
|
+
import {
|
|
7
|
+
defaultCache,
|
|
8
|
+
offersSharedCache,
|
|
9
|
+
PRIVATE_CACHE,
|
|
10
|
+
replayableExchange,
|
|
11
|
+
reviewedHint,
|
|
12
|
+
} from './cache-policy';
|
|
13
|
+
import type { RequestContext } from './context';
|
|
14
|
+
import { addVary, applyCacheHeaders, NO_STORE, SHARED_CACHE_VARY } from './response';
|
|
15
|
+
|
|
16
|
+
export const finalizeCacheHeaders = (response: Response, ctx: RequestContext): void => {
|
|
17
|
+
const declared = response.headers.get('cache-control');
|
|
18
|
+
// The EXCHANGE first, before any hint or declaration: a POST's answer or a refusal is never
|
|
19
|
+
// a shared cache's to replay, whatever the route or the handler offered (`replayableExchange`).
|
|
20
|
+
// `pragma: no-cache` beside it is RFC 6749 §5.1's pair for a token endpoint's answer, and
|
|
21
|
+
// costs nothing anywhere else. Only an OFFER is overruled — a handler's own `private` or
|
|
22
|
+
// `no-store` already says less than this would.
|
|
23
|
+
if (!replayableExchange(ctx.method, response.status)) {
|
|
24
|
+
if (declared === null || offersSharedCache(declared)) {
|
|
25
|
+
applyCacheHeaders(response, NO_STORE);
|
|
26
|
+
if (!response.headers.has('pragma')) response.headers.set('pragma', 'no-cache');
|
|
27
|
+
}
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
if (declared === null) {
|
|
31
|
+
const hint = ctx.cache ?? ctx.route?.meta.cache ?? defaultCache(ctx.route, ctx.actor);
|
|
32
|
+
applyCacheHeaders(response, reviewedHint(hint, ctx.actor));
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
// A declaration is the MODE's intent, never the last word: `@ultimat3/render`'s `ssrHeaders`
|
|
36
|
+
// offers any route without a `policy` to a CDN for 30 seconds, and `meta.auth` is
|
|
37
|
+
// `'public' | 'required'` — so the page that greets a signed-in visitor by name is a
|
|
38
|
+
// `'public'` route whose own header says `s-maxage`. This stage is the one owner of the
|
|
39
|
+
// final answer, which is why it REVIEWS what the handler wrote instead of standing down;
|
|
40
|
+
// the rule beside it was otherwise unreachable for every page route in every app.
|
|
41
|
+
if (!offersSharedCache(declared)) return;
|
|
42
|
+
if (!isAnonymous(ctx.actor)) {
|
|
43
|
+
applyCacheHeaders(response, PRIVATE_CACHE);
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
addVary(response, SHARED_CACHE_VARY);
|
|
47
|
+
};
|
package/src/stages.ts
CHANGED
|
@@ -13,7 +13,7 @@ import {
|
|
|
13
13
|
reportError,
|
|
14
14
|
} from '@ultimat3/core';
|
|
15
15
|
import { signInRedirect } from './auth-redirect';
|
|
16
|
-
import {
|
|
16
|
+
import { finalizeCacheHeaders } from './cache-stage';
|
|
17
17
|
import { type HttpConfig, stripBasePath } from './config';
|
|
18
18
|
import { actorView, elapsedMs, type RequestContext } from './context';
|
|
19
19
|
import { corsHeaders, preflight } from './cors';
|
|
@@ -40,7 +40,7 @@ import { resolvePreferences } from './preferences';
|
|
|
40
40
|
import { type RateLimitDecision, type RateLimiter, rateLimitSpends } from './rate-limit';
|
|
41
41
|
import { rateLimited } from './rate-limit-errors';
|
|
42
42
|
import type { UltimateRequest } from './request';
|
|
43
|
-
import { addVary,
|
|
43
|
+
import { addVary, problem, redirect } from './response';
|
|
44
44
|
import { matchRoute, type Route, type RouteHandler, type RouteTable } from './router';
|
|
45
45
|
import { responseSecurityHeaders } from './security-headers';
|
|
46
46
|
import { validate } from './validate';
|
|
@@ -324,26 +324,7 @@ export const stageRunners = (input: StageRunnersInput): Record<StageName, StageR
|
|
|
324
324
|
},
|
|
325
325
|
|
|
326
326
|
'cache-headers': (_request, ctx) => {
|
|
327
|
-
|
|
328
|
-
if (response === undefined) return undefined;
|
|
329
|
-
const declared = response.headers.get('cache-control');
|
|
330
|
-
if (declared === null) {
|
|
331
|
-
const hint = ctx.cache ?? ctx.route?.meta.cache ?? defaultCache(ctx.route, ctx.actor);
|
|
332
|
-
applyCacheHeaders(response, reviewedHint(hint, ctx.actor));
|
|
333
|
-
return undefined;
|
|
334
|
-
}
|
|
335
|
-
// A declaration is the MODE's intent, never the last word: `@ultimat3/render`'s `ssrHeaders`
|
|
336
|
-
// offers any route without a `policy` to a CDN for 30 seconds, and `meta.auth` is
|
|
337
|
-
// `'public' | 'required'` — so the page that greets a signed-in visitor by name is a
|
|
338
|
-
// `'public'` route whose own header says `s-maxage`. This stage is the one owner of the
|
|
339
|
-
// final answer, which is why it REVIEWS what the handler wrote instead of standing down;
|
|
340
|
-
// the rule beside it was otherwise unreachable for every page route in every app.
|
|
341
|
-
if (!offersSharedCache(declared)) return undefined;
|
|
342
|
-
if (!isAnonymous(ctx.actor)) {
|
|
343
|
-
applyCacheHeaders(response, PRIVATE_CACHE);
|
|
344
|
-
return undefined;
|
|
345
|
-
}
|
|
346
|
-
addVary(response, SHARED_CACHE_VARY);
|
|
327
|
+
if (ctx.response !== undefined) finalizeCacheHeaders(ctx.response, ctx);
|
|
347
328
|
return undefined;
|
|
348
329
|
},
|
|
349
330
|
|