@ultimat3/core 4.0.0 → 4.1.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,6 +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
12
  | A value a CALLER supplied | `describeValue()` — shape, never content. `renderCauseValue` is safe against throwing, not against leaking |
12
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 |
13
14
  | New code | add to `CORE_CODE_TITLES` in `error-codes.ts`, else the title is auto-humanised |
@@ -32,6 +33,23 @@ parameters typed `unknown` that reach a `cause:` / `fix:`, and it cannot see a v
32
33
  through a local helper first (`packages/ui/src/components/ErrorState.tsx` builds a `message`
33
34
  const, then assigns it).
34
35
 
36
+ `singleLine` is the escape that keeps the 3-line contract to three lines, and it exists because
37
+ `scripts/error-render.ts` **cannot see this class**. That gate refuses a parameter typed
38
+ `unknown`/`any` reaching a `cause:`; a value that is already a `string` renders without throwing, so
39
+ there is nothing for it to object to — while a newline in one adds a line to a format that is
40
+ line-oriented in the terminal, in CI logs and inside the dev overlay's `<pre>`. Three holes shipped
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,
47
+ its backslashes and its percent signs — only the control range is touched. Line breaks are the
48
+ structural half; the rest of C0 and DEL ride along because a terminal reads a raw `\u001b` as an ANSI
49
+ escape, so a cause could repaint the screen or hide the line above it. `@ultimat3/schema` carries a
50
+ deliberate duplicate for the tier-0 reason below, pinned behaviourally by
51
+ `single-line-pin.test.ts` in `@ultimat3/cli`.
52
+
35
53
  `describeValue` in `error-render.ts` is a character-for-character duplicate of `describeValue` in
36
54
  `packages/schema/src/describe-value.ts`, for the same tier-0 reason `SCHEMA_ERROR_CODE_TITLES` is
37
55
  one: schema and core are both tier 0 and `core → schema` is **not** a declared edge in
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ultimat3/core",
3
- "version": "4.0.0",
3
+ "version": "4.1.0",
4
4
  "description": "Ultimate's foundation: errors, context, env, config, clock, ids, logging, telemetry, lifecycle",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -64,6 +64,53 @@ export function renderCauseValue(value: unknown): string {
64
64
  }
65
65
  }
66
66
 
67
+ /**
68
+ * One line, always. Escapes every character that a line-oriented reader would treat as a line
69
+ * break, leaving everything else byte-identical.
70
+ *
71
+ * WHY this exists at all: `renderCauseValue` above is safe against a value that THROWS while
72
+ * rendering, and safe against a value that LEAKS. It is not safe against a value that is already
73
+ * a `string` — a `string` renders fine, so nothing objected to it — and a caller-controlled
74
+ * string can carry a newline. The 3-line contract format (`<code>` / ` cause: …` / ` fix: …`)
75
+ * is line-oriented in the terminal, in CI logs and inside the dev overlay's `<pre>`, so one
76
+ * newline in a `cause` writes a second line an operator reads as a genuine framework message.
77
+ * Reproduced with a forged OIDC `iss` claim: an unauthenticated stranger with one crafted token.
78
+ *
79
+ * At the RENDERER, not at the call site, deliberately. `bun run error-render` cannot see this
80
+ * class — its rule is about `unknown` reaching a `cause:` — and three holes shipped in
81
+ * `@ultimat3/auth` alone under a green check. A rule every call site must remember is a rule the
82
+ * 296th call site forgets; there are six renderers of this format and no more, so escaping there
83
+ * is one place instead of every place (issue #97, option 3).
84
+ *
85
+ * `\u2028` and `\u2029` are included because they terminate a line for a JavaScript parser and
86
+ * for several log viewers, while `String.prototype.split('\n')` never sees them.
87
+ *
88
+ * The set is every C0 control, DEL, and `\u2028`/`\u2029`. Line breaks are the structural half —
89
+ * they add a line to a line-oriented format — and the rest ride along because a terminal reads them
90
+ * as commands of its own: a raw `\u001b` in a cause is an ANSI escape, so a value could repaint the
91
+ * screen, hide the line above it or move the cursor over what a reader had already been shown.
92
+ *
93
+ * NOT a general sanitiser. A `cause` is prose and keeps its quotes, its backslashes, its percent
94
+ * signs and every printable character it arrived with; only the control range is touched.
95
+ */
96
+ export function singleLine(text: string): string {
97
+ // biome-ignore lint/suspicious/noControlCharactersInRegex: escaping them is the point.
98
+ return text.replace(/[\u0000-\u001f\u007f\u2028\u2029]/g, (char) => {
99
+ const known = CONTROL_ESCAPES[char];
100
+ if (known !== undefined) return known;
101
+ return `\\u${char.charCodeAt(0).toString(16).padStart(4, '0')}`;
102
+ });
103
+ }
104
+
105
+ /** The spellings a reader already knows from a JSON string, so the escape reads as an escape. */
106
+ const CONTROL_ESCAPES: Readonly<Record<string, string>> = {
107
+ '\n': String.raw`\n`,
108
+ '\r': String.raw`\r`,
109
+ '\t': String.raw`\t`,
110
+ '\b': String.raw`\b`,
111
+ '\f': String.raw`\f`,
112
+ };
113
+
67
114
  /**
68
115
  * `value instanceof Error`, made total. The test itself can throw: a `Proxy`'s `getPrototypeOf`
69
116
  * trap runs during `instanceof`, and the one place this question is asked is a `catch` block that
package/src/errors.ts CHANGED
@@ -3,7 +3,13 @@
3
3
  // overlay and `--json`. Never throw a bare Error anywhere in the framework.
4
4
 
5
5
  import { describeErrorCode } from './error-codes';
6
- import { isThrownError, renderCauseValue, renderMetaRecord, renderThrowable } from './error-render';
6
+ import {
7
+ isThrownError,
8
+ renderCauseValue,
9
+ renderMetaRecord,
10
+ renderThrowable,
11
+ singleLine,
12
+ } from './error-render';
7
13
  import { DEFAULT_ERROR_RETRY, type ErrorRetry, isErrorRetry, retryFor } from './error-retry';
8
14
 
9
15
  /**
@@ -89,8 +95,14 @@ export class UltimateError extends Error {
89
95
  * ```
90
96
  */
91
97
  format(options?: FormatErrorOptions): string {
92
- const lines = [`${this.code}: ${this.title}`, ` cause: ${this.cause}`, ` fix: ${this.fix}`];
93
- if (options?.docs === true) lines.push(` docs: ${this.docs}`);
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)}`);
94
106
  return lines.join('\n');
95
107
  }
96
108
 
@@ -27,6 +27,7 @@ export {
27
27
  renderCauseValue,
28
28
  renderFixLiteral,
29
29
  renderThrowable,
30
+ singleLine,
30
31
  stringField,
31
32
  } from '../error-render';
32
33
  export type { ErrorRetry } from '../error-retry';
package/src/index.ts CHANGED
@@ -159,6 +159,7 @@ export {
159
159
  resetErrorRetry,
160
160
  retryFor,
161
161
  SCHEMA_ERROR_CODE_TITLES,
162
+ singleLine,
162
163
  stringField,
163
164
  toUltimateError,
164
165
  ULTIMATE_ERROR_BRAND,