@ultimat3/http 22.9.1 → 22.10.0
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/error-facts.ts +10 -1
- package/src/errors.ts +7 -6
- package/src/locale-prefix.ts +10 -0
- package/src/stages.ts +5 -4
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ultimat3/http",
|
|
3
|
-
"version": "22.
|
|
3
|
+
"version": "22.10.0",
|
|
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.
|
|
35
|
-
"@ultimat3/i18n": "22.
|
|
36
|
-
"@ultimat3/schema": "22.
|
|
37
|
-
"@ultimat3/time": "22.
|
|
34
|
+
"@ultimat3/core": "22.10.0",
|
|
35
|
+
"@ultimat3/i18n": "22.10.0",
|
|
36
|
+
"@ultimat3/schema": "22.10.0",
|
|
37
|
+
"@ultimat3/time": "22.10.0"
|
|
38
38
|
}
|
|
39
39
|
}
|
package/src/error-facts.ts
CHANGED
|
@@ -21,6 +21,11 @@ export interface ErrorFacts {
|
|
|
21
21
|
readonly title: string;
|
|
22
22
|
readonly cause: string;
|
|
23
23
|
readonly fix: string;
|
|
24
|
+
/**
|
|
25
|
+
* The remote caller's fix (`UltimateErrorInit.callerFix`), branded errors only — `fix`'s trust
|
|
26
|
+
* rule. A production problem document carries it in place of `fix`; the terminal never does.
|
|
27
|
+
*/
|
|
28
|
+
readonly callerFix: string | undefined;
|
|
24
29
|
readonly docs: string;
|
|
25
30
|
readonly status: number;
|
|
26
31
|
/** Present only when the process is in dev mode; never sent to a client in prod. */
|
|
@@ -88,6 +93,7 @@ export const factsOf = (error: unknown): ErrorFacts => {
|
|
|
88
93
|
(FRAMEWORK_CODE.test(code)
|
|
89
94
|
? `x errors explain ${code} --json # then fix the throwing call site`
|
|
90
95
|
: 'x errors list --json # then fix the throwing call site'),
|
|
96
|
+
callerFix: isUltimateError(error) ? str(error, 'callerFix') : undefined,
|
|
91
97
|
// Core's one constant, never a per-code URL: `wiki/` is the only public documentation surface
|
|
92
98
|
// and a code lives there in a table row, which has no anchor. An `UltimateError` already
|
|
93
99
|
// resolved this at construction, so the fallback only fires for a throwable the framework did
|
|
@@ -322,7 +328,10 @@ export const toProblem = (
|
|
|
322
328
|
instance: meta.instance,
|
|
323
329
|
code: facts.code,
|
|
324
330
|
cause: hidden ? INTERNAL_CAUSE : facts.cause,
|
|
325
|
-
fix:
|
|
331
|
+
// The CALLER's fix outside dev: a production client cannot run `x policy explain`, and the
|
|
332
|
+
// developer's line stays in the log this request wrote. Dev keeps the developer's, because the
|
|
333
|
+
// reader of a dev response is the developer.
|
|
334
|
+
fix: meta.dev === true ? facts.fix : (facts.callerFix ?? facts.fix),
|
|
326
335
|
docs: facts.docs,
|
|
327
336
|
requestId: meta.requestId,
|
|
328
337
|
...(issues === undefined ? {} : { issues }),
|
package/src/errors.ts
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
// factory here so a code, a cause and an exact fix always travel together — the
|
|
3
3
|
// terminal, the dev overlay and `--json` all render the same three strings.
|
|
4
4
|
import {
|
|
5
|
+
deniedCallerFix,
|
|
5
6
|
registerErrorCodes,
|
|
6
7
|
registerErrorRetry,
|
|
7
8
|
renderFixShellArg,
|
|
@@ -139,6 +140,8 @@ export class HttpError extends UltimateError {
|
|
|
139
140
|
code: HttpErrorCode;
|
|
140
141
|
cause: string;
|
|
141
142
|
fix: string;
|
|
143
|
+
/** A remote caller's fix, when `fix` needs the app's CLI — `UltimateErrorInit.callerFix`. */
|
|
144
|
+
callerFix?: string;
|
|
142
145
|
/**
|
|
143
146
|
* Facts an operator needs and a CALLER must not be handed. `toProblem` renders code, cause,
|
|
144
147
|
* fix and docs — never this — so the rate limiter's internal key rides here instead of in a
|
|
@@ -146,12 +149,7 @@ export class HttpError extends UltimateError {
|
|
|
146
149
|
*/
|
|
147
150
|
meta?: Readonly<Record<string, unknown>>;
|
|
148
151
|
}) {
|
|
149
|
-
super({
|
|
150
|
-
code: init.code,
|
|
151
|
-
cause: init.cause,
|
|
152
|
-
fix: init.fix,
|
|
153
|
-
...(init.meta === undefined ? {} : { meta: init.meta }),
|
|
154
|
-
});
|
|
152
|
+
super({ ...init }); // an `UltimateErrorInit`: optional fields attached only when present
|
|
155
153
|
}
|
|
156
154
|
}
|
|
157
155
|
|
|
@@ -256,6 +254,9 @@ export const forbidden = (pathname: string, reason: string, policy?: string): Ht
|
|
|
256
254
|
policy !== undefined && POLICY_SUBJECT.test(policy)
|
|
257
255
|
? `x policy explain ${policy} --json # shows which clause denied`
|
|
258
256
|
: `x routes --json # find ${pathname}, then read the policy it declares`,
|
|
257
|
+
callerFix: deniedCallerFix(
|
|
258
|
+
policy !== undefined && POLICY_SUBJECT.test(policy) ? policy : undefined,
|
|
259
|
+
),
|
|
259
260
|
});
|
|
260
261
|
|
|
261
262
|
export const buildSkew = (clientBuildId: string, serverBuildId: string): HttpError =>
|
package/src/locale-prefix.ts
CHANGED
|
@@ -57,6 +57,16 @@ export function requestLocale(ctx: RequestContext, sources: LocaleSources): stri
|
|
|
57
57
|
export const localeFromPath = (ctx: RequestContext): boolean =>
|
|
58
58
|
ctx.pathLocale !== undefined || ctx.route?.meta.localeSource === 'path';
|
|
59
59
|
|
|
60
|
+
/**
|
|
61
|
+
* Whether `Accept-Language` can decide this response's locale — the one condition under which
|
|
62
|
+
* `Vary: accept-language` is true. Not when the URL names the locale (above), and not when the app's
|
|
63
|
+
* locale order leaves the header out (`configureLocales({ order: ['query', 'cookie', 'user'] })`):
|
|
64
|
+
* every renderer adds the dimension by default, and a CDN keying on a header nothing reads stores
|
|
65
|
+
* one copy of the page per browser language for nothing.
|
|
66
|
+
*/
|
|
67
|
+
export const acceptLanguageConsulted = (ctx: RequestContext): boolean =>
|
|
68
|
+
!localeFromPath(ctx) && localeConfig().order.includes('header');
|
|
69
|
+
|
|
60
70
|
/** Removes one dimension from `Vary`, case-insensitively, deleting the header when none is left. */
|
|
61
71
|
export const dropVary = (response: Response, name: string): Response => {
|
|
62
72
|
const existing = response.headers.get('vary');
|
package/src/stages.ts
CHANGED
|
@@ -33,7 +33,7 @@ import {
|
|
|
33
33
|
} from './errors';
|
|
34
34
|
import type { ServerHooks } from './hooks';
|
|
35
35
|
import { acceptsHtml } from './html-render';
|
|
36
|
-
import {
|
|
36
|
+
import { acceptLanguageConsulted, dropVary, routeLocalePrefix } from './locale-prefix';
|
|
37
37
|
import { compose, type Middleware } from './middleware';
|
|
38
38
|
import { navigationGate, redirectForRouter } from './navigation';
|
|
39
39
|
import { overlayResponse } from './overlay';
|
|
@@ -453,9 +453,10 @@ export const stageRunners = (input: StageRunnersInput): Record<StageName, StageR
|
|
|
453
453
|
if (name === 'vary') addVary(response, [value]);
|
|
454
454
|
else response.headers.set(name, value);
|
|
455
455
|
}
|
|
456
|
-
// After every contributor: a locale read off the URL
|
|
457
|
-
//
|
|
458
|
-
|
|
456
|
+
// After every contributor: a locale read off the URL — or an app whose locale order never
|
|
457
|
+
// reads the header — makes `accept-language` a key nothing varies on, and a CDN keying on it
|
|
458
|
+
// stores one copy per browser for nothing.
|
|
459
|
+
if (!acceptLanguageConsulted(ctx)) dropVary(response, 'accept-language');
|
|
459
460
|
for (const [name, value] of Object.entries(
|
|
460
461
|
responseSecurityHeaders(config.security, ctx.https),
|
|
461
462
|
)) {
|