@ultimat3/core 4.1.0 → 5.0.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/CLAUDE.md +16 -6
- package/package.json +1 -1
- package/src/config.ts +11 -3
- package/src/errors.ts +22 -13
- package/src/index.ts +0 -1
package/CLAUDE.md
CHANGED
|
@@ -8,7 +8,7 @@ is a change to every package.
|
|
|
8
8
|
| Deps | none (`bun-types` only) |
|
|
9
9
|
| Errors | subclass `UltimateError`; never `throw new Error` |
|
|
10
10
|
| Values in a message | `renderCauseValue()` / `renderFixLiteral()`; never raw `JSON.stringify`, `String()` or `${…}` on an `unknown` |
|
|
11
|
-
| Rendering the 3-line format | `
|
|
11
|
+
| Rendering the 3-line format | nothing to remember — `UltimateError`'s CONSTRUCTOR escapes `code`, `title`, `cause`, `fix` and `docs` with `singleLine()`. Call it yourself only when you render a shape this class never built, e.g. a `Finding` |
|
|
12
12
|
| A value a CALLER supplied | `describeValue()` — shape, never content. `renderCauseValue` is safe against throwing, not against leaking |
|
|
13
13
|
| Reading a caught value | `renderThrowable()` / `isThrownError()` / `stringField()`; never `error.message`, `error instanceof Error` or `typeof error.code === 'string'` directly — the probe throws before the renderer runs |
|
|
14
14
|
| New code | add to `CORE_CODE_TITLES` in `error-codes.ts`, else the title is auto-humanised |
|
|
@@ -39,11 +39,21 @@ const, then assigns it).
|
|
|
39
39
|
there is nothing for it to object to — while a newline in one adds a line to a format that is
|
|
40
40
|
line-oriented in the terminal, in CI logs and inside the dev overlay's `<pre>`. Three holes shipped
|
|
41
41
|
in `@ultimat3/auth` under a green check, the worst reachable by an unauthenticated stranger with one
|
|
42
|
-
crafted OIDC token (issue #97).
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
42
|
+
crafted OIDC token (issue #97).
|
|
43
|
+
|
|
44
|
+
**It is applied in the CONSTRUCTOR, `As of 2026-08-20` — not at the renderers, which is where it
|
|
45
|
+
went first and could not stay.** Escaping at each renderer was six call sites, and six is a number
|
|
46
|
+
that only goes up: a seventh in this repo, and every renderer an APP writes, would each have had to
|
|
47
|
+
remember. `format()` is also not the only reader — an uncaught throw prints `.message`, a log line
|
|
48
|
+
takes `.cause`, `--json` takes `toJSON()` — so a per-renderer escape left three of four doors open.
|
|
49
|
+
One constructor covers all of them, and `singleLine` is idempotent, so a call site that already
|
|
50
|
+
escaped (`@ultimat3/auth` renders `claims.iss` at its source, quotes and all) is unharmed. `format()`
|
|
51
|
+
therefore interpolates the fields bare: a second pass would be a second place that has to be right.
|
|
52
|
+
The four renderers that still call it — `renderErrorLines` in `@ultimat3/http`,
|
|
53
|
+
`renderFrameworkError` in `@ultimat3/mcp`, `renderFinding` / `detailLines` in `@ultimat3/cli` — take
|
|
54
|
+
shapes this class never built (a `Finding`, a catalog entry), which is the one case left.
|
|
55
|
+
|
|
56
|
+
It is not a general sanitiser: a cause is prose and keeps its quotes,
|
|
47
57
|
its backslashes and its percent signs — only the control range is touched. Line breaks are the
|
|
48
58
|
structural half; the rest of C0 and DEL ride along because a terminal reads a raw `\u001b` as an ANSI
|
|
49
59
|
escape, so a cause could repaint the screen or hide the line above it. `@ultimat3/schema` carries a
|
package/package.json
CHANGED
package/src/config.ts
CHANGED
|
@@ -8,7 +8,6 @@ import { ROLES, type Role } from './roles';
|
|
|
8
8
|
export type ThemeMode = 'light' | 'dark' | 'system';
|
|
9
9
|
export type OfflineStrategy = 'precache' | 'runtime' | 'network-only';
|
|
10
10
|
export type CacheTier = 'memo' | 'lru' | 'shared' | 'isr' | 'cdn';
|
|
11
|
-
export type JobsDriver = 'postgres' | 'redis' | 'nats';
|
|
12
11
|
export type RealtimeTier = 'channels' | 'live-queries' | 'local-first';
|
|
13
12
|
export type RealtimeTransport = 'memory' | 'nats' | 'redis';
|
|
14
13
|
|
|
@@ -83,7 +82,17 @@ export interface CacheConfig {
|
|
|
83
82
|
}
|
|
84
83
|
|
|
85
84
|
export interface JobsConfig {
|
|
86
|
-
|
|
85
|
+
/**
|
|
86
|
+
* No `driver`. It accepted `'postgres' | 'redis' | 'nats'`, was read by NOTHING, and boot always
|
|
87
|
+
* built `createPgDriver` — so `jobs: { driver: 'redis' }` did not throw, did not warn, and
|
|
88
|
+
* silently gave you Postgres. Deleted 2026-08-20, and it is the worse shape of the same defect
|
|
89
|
+
* `realtime.heartbeatMs` was: a knob that fails SILENTLY in the dangerous direction.
|
|
90
|
+
*
|
|
91
|
+
* The seam that works is `setJobDriver(driver)` — `setJobDriver(createPgDriver({ executor }))`,
|
|
92
|
+
* or `setJobDriver(createMemoryDriver())` in a test. Swap the driver, zero job-code change, which
|
|
93
|
+
* is the whole of what the `JobDriver` interface buys. There is no config line, and one that
|
|
94
|
+
* cannot be honoured is worse than none.
|
|
95
|
+
*/
|
|
87
96
|
readonly queues: readonly string[];
|
|
88
97
|
readonly concurrency: number;
|
|
89
98
|
readonly maxAttempts: number;
|
|
@@ -232,7 +241,6 @@ function defaults(name: string): Omit<AppConfig, 'name'> {
|
|
|
232
241
|
database: { driver: 'postgres', ssl: false },
|
|
233
242
|
cache: { driver: 'memory', urlEnv: undefined, defaultTtlMs: 60_000, tiers: ['memo', 'lru'] },
|
|
234
243
|
jobs: {
|
|
235
|
-
driver: 'postgres',
|
|
236
244
|
queues: [`${name}-default`],
|
|
237
245
|
concurrency: 8,
|
|
238
246
|
maxAttempts: 5,
|
package/src/errors.ts
CHANGED
|
@@ -70,16 +70,27 @@ export class UltimateError extends Error {
|
|
|
70
70
|
|
|
71
71
|
constructor(init: UltimateErrorInit) {
|
|
72
72
|
const described = describeErrorCode(init.code);
|
|
73
|
+
// Every line-bearing field is escaped HERE, once, and never again downstream. A `cause` is a
|
|
74
|
+
// single line by contract, and a caller controls one often enough to matter: `claims.iss` off
|
|
75
|
+
// an unverified JWT, an IdP's `error_description`, a forwarded IP. One newline in any of them
|
|
76
|
+
// writes a second line an operator, a CI log or a `<pre>` reads as a genuine framework
|
|
77
|
+
// message. Escaping at each RENDERER was the first fix and it cannot hold — it is six call
|
|
78
|
+
// sites today, a seventh whenever someone writes one, and zero of the renderers an APP writes.
|
|
79
|
+
// Escaping at construction is the one place that covers all of them, and it is what #97 called
|
|
80
|
+
// the real answer. `singleLine` is idempotent, so a call site that already escaped is unharmed.
|
|
81
|
+
const code = singleLine(init.code);
|
|
82
|
+
const title = singleLine(described.title);
|
|
83
|
+
const cause = singleLine(init.cause);
|
|
73
84
|
// `message` carries the cause because it is the ONLY field a runtime prints when an
|
|
74
85
|
// error escapes uncaught — a worker log, a CI transcript, a stack trace. A message of
|
|
75
86
|
// just `code: title` tells an operator which rule fired but not which row, column or
|
|
76
87
|
// value, which is the opposite of "errors are instructions". `format()` still renders
|
|
77
88
|
// the canonical 3 lines from the fields, so the two never disagree.
|
|
78
|
-
super(`${
|
|
79
|
-
this.code =
|
|
80
|
-
this.title =
|
|
81
|
-
this.fix = init.fix;
|
|
82
|
-
this.docs = init.docs ?? described.docs;
|
|
89
|
+
super(`${code}: ${title} — ${cause}`, { cause });
|
|
90
|
+
this.code = code;
|
|
91
|
+
this.title = title;
|
|
92
|
+
this.fix = singleLine(init.fix);
|
|
93
|
+
this.docs = singleLine(init.docs ?? described.docs);
|
|
83
94
|
this.retry = init.retry ?? retryFor(init.code);
|
|
84
95
|
this.meta = init.meta;
|
|
85
96
|
this.sourceError = init.sourceError;
|
|
@@ -95,14 +106,12 @@ export class UltimateError extends Error {
|
|
|
95
106
|
* ```
|
|
96
107
|
*/
|
|
97
108
|
format(options?: FormatErrorOptions): string {
|
|
98
|
-
// `singleLine
|
|
99
|
-
//
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
];
|
|
105
|
-
if (options?.docs === true) lines.push(` docs: ${singleLine(this.docs)}`);
|
|
109
|
+
// No `singleLine` here. The constructor already escaped all five fields, so a second pass
|
|
110
|
+
// would be a second place that has to be right — and the one that gets forgotten. This method
|
|
111
|
+
// is line-oriented and stays exactly 3 lines (4 with `docs`) because the fields cannot carry
|
|
112
|
+
// a line break, not because this joiner removes them.
|
|
113
|
+
const lines = [`${this.code}: ${this.title}`, ` cause: ${this.cause}`, ` fix: ${this.fix}`];
|
|
114
|
+
if (options?.docs === true) lines.push(` docs: ${this.docs}`);
|
|
106
115
|
return lines.join('\n');
|
|
107
116
|
}
|
|
108
117
|
|