@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 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 | `singleLine()` on every interpolated field. A `string` renders fine and can still carry a newline, which writes a line an operator reads as a genuine message |
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). It is applied at the SIX renderers of that format and never at the
43
- call sites — `UltimateError.format()` here, `SchemaError.format()` in `@ultimat3/schema`,
44
- `renderErrorLines` in `@ultimat3/http`, `renderFrameworkError` in `@ultimat3/mcp`, and
45
- `renderFinding` / `detailLines` in `@ultimat3/cli` because a rule every author must remember is a
46
- rule the next author forgets. It is not a general sanitiser: a cause is prose and keeps its quotes,
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ultimat3/core",
3
- "version": "4.1.0",
3
+ "version": "5.0.0",
4
4
  "description": "Ultimate's foundation: errors, context, env, config, clock, ids, logging, telemetry, lifecycle",
5
5
  "license": "MIT",
6
6
  "type": "module",
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
- readonly driver: JobsDriver;
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(`${init.code}: ${described.title} — ${init.cause}`, { cause: init.cause });
79
- this.code = init.code;
80
- this.title = described.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`, because this format is line-oriented and `cause` may hold a caller's string:
99
- // one newline in it writes a second line an operator reads as a genuine framework message.
100
- const lines = [
101
- `${singleLine(this.code)}: ${singleLine(this.title)}`,
102
- ` cause: ${singleLine(this.cause)}`,
103
- ` fix: ${singleLine(this.fix)}`,
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
 
package/src/index.ts CHANGED
@@ -44,7 +44,6 @@ export type {
44
44
  CacheTier,
45
45
  DatabaseConfig,
46
46
  JobsConfig,
47
- JobsDriver,
48
47
  McpConfig,
49
48
  OfflineStrategy,
50
49
  PwaConfig,