@velajs/errors 1.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/CHANGELOG.md ADDED
@@ -0,0 +1,19 @@
1
+ # Changelog
2
+
3
+ All notable changes to `@velajs/errors` are documented here. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
4
+
5
+ ## 1.0.0
6
+
7
+ Initial release — the unified, zero-dependency error layer for Vela.
8
+
9
+ ### Added
10
+
11
+ - **`VelaError`** — one framework error class carrying `code`, `status`, `hint`, `docsUrl`, and `data` as own-enumerable properties, so an error rides any wire codec / `structuredClone` / Durable-Object RPC boundary with no special serialization path. Branded with an own-enumerable `type: "VelaError"` discriminator.
12
+ - **Composable catalogs** — `defineErrorCatalog` (typed, keys derive the code union), `composeCatalogs` (merges catalogs and throws on a duplicate code at compose time), the built-in `CORE_CATALOG` (the HTTP status family), and `STATUS_TO_CODE`. Catalog lookups use `Object.hasOwn`, so prototype keys (`toString`, `constructor`, …) are never matched.
13
+ - **`isVelaError`** — a structural, realm-safe, **branded** type guard: `instanceof Error` plus a string `code`, a numeric `status`, and the `VelaError` brand. Survives serialization and the DO↔worker boundary where `instanceof` is unreliable, and a foreign error that merely carries `code`+`status` cannot pass.
14
+ - **`toErrorBody`** — the single wire-redaction seam. Unbranded and internal-coded errors are redacted to a generic message; branded, non-internal errors echo their `message`/`hint`/`details`. Returns a `redacted` flag so callers log the raw error server-side. Pluggable `redactedMessage` and `encodeData` hooks.
15
+ - **`invariant` / `unreachable`** — assertion helpers that throw an internal-coded `VelaError` (rich in server logs, redacted on the wire).
16
+
17
+ ### Notes
18
+
19
+ - Zero runtime dependencies; ESM; `sideEffects: false`; edge-runtime safe (no `node:*`, `Buffer`, or `process`).
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Kauan Guesser
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,83 @@
1
+ # @velajs/errors
2
+
3
+ Unified error layer for Vela: a single branded `VelaError` whose every field is an own-enumerable property (so it rides any wire codec, `structuredClone`, or Durable Object RPC prop-copy unchanged), composable error catalogs that default status/hint/docs from a code, and the one `toErrorBody` seam that redacts internal errors before they reach the wire. Zero runtime dependencies, edge-runtime safe.
4
+
5
+ ## Why
6
+
7
+ Across HTTP, WebSocket, live queries, and queue reporting, Vela needs one answer to two questions: *"is this error safe to show a client?"* and *"what exact JSON goes on the wire?"* This package is that answer. Every transport edge funnels through `toErrorBody`, so the redaction invariant holds identically everywhere and the wire shape is pinned by a golden fixture.
8
+
9
+ ## The three redaction rules
10
+
11
+ `toErrorBody(error, options?)` returns `{ body, status, redacted }`. Whether the original message/hint/details reach the client is decided by exactly these rules:
12
+
13
+ | # | Input error | Result | `redacted` |
14
+ | - | ----------- | ------ | ---------- |
15
+ | 1 | Anything not a branded `VelaError` (plain `Error`, a foreign driver error that merely has `code`+`status`, `null`, …) | Generic title for the fallback status; original message dropped | `true` |
16
+ | 2 | A branded `VelaError` whose code is the literal `internal`, **or** whose catalog entry has `internal: true` | `code` + `status` kept; message/hint/details dropped, replaced with the generic title for that status | `true` |
17
+ | 3 | Any other branded `VelaError` — catalogued or open (unknown) code | `code`, `message`, and any `hint`/`docsUrl`/`details` echoed verbatim | `false` |
18
+
19
+ Status alone never triggers redaction: an open code with `status: 500` still echoes (rule 3) — only the literal `internal` code or an `internal: true` catalog flag redacts (rule 2). `redacted: true` is the caller's signal to log the raw error server-side; `toErrorBody` itself never logs.
20
+
21
+ The canonical wire shape (pinned by `wire-fixture.test.ts`) is:
22
+
23
+ ```json
24
+ { "error": { "code": "not_found", "message": "no such route", "hint": "Run `vela route list`." } }
25
+ ```
26
+
27
+ and, redacted:
28
+
29
+ ```json
30
+ { "error": { "code": "internal", "message": "Internal Server Error" } }
31
+ ```
32
+
33
+ ## The brand contract
34
+
35
+ `isVelaError` is a **branded structural** guard: it requires `error instanceof Error` plus `typeof code === 'string'`, `typeof status === 'number'`, and `type === 'VelaError'`. The `type` brand is an own-enumerable property, so it survives JSON round-trips, `structuredClone`, and DO↔worker RPC prop-copy — a wire-decoded twin (`Object.assign(new Error(msg), {...decoded})`) still passes. `instanceof VelaError` is deliberately **not** load-bearing (it breaks across realms and on decoded twins), so nothing in the redaction path uses it.
36
+
37
+ The consequence for new transport edges: **construct real `VelaError`s, not shape-alikes.** A foreign error that merely carries `code` and `status` is intentionally rejected by the guard and redacted by rule 1 — that is the mechanism that stops a database driver's `internal driver detail: host=10.0.0.5` from riding the client-echo path.
38
+
39
+ ## Catalog composition
40
+
41
+ A catalog maps codes to defaults (`status`, `title`, optional `hint`/`docsUrl`, and the `internal` redaction posture). `CORE_CATALOG` ships the standard HTTP-shaped codes. Compose your app's catalog onto it; duplicate codes throw at composition time.
42
+
43
+ ```ts
44
+ import { CORE_CATALOG, composeCatalogs, defineErrorCatalog } from '@velajs/errors';
45
+
46
+ const appCatalog = composeCatalogs(
47
+ CORE_CATALOG,
48
+ defineErrorCatalog({
49
+ order_expired: { status: 410, title: 'Order expired', hint: 'Create a new order.' },
50
+ db_corruption: { status: 500, title: 'Storage failure', internal: true },
51
+ }),
52
+ );
53
+ ```
54
+
55
+ > **Gotcha:** a custom catalog's `title` is **not** used as the thrown error's default message. Only core codes default their message to the catalog title; a throw for a custom-catalog code defaults its message to the code string unless you pass `message`. Pass `message` explicitly when you want human-readable text (and remember rule 2 will redact it anyway for `internal: true` entries). Use `catalog.error(code, options)` to inherit the entry's `status`/`hint`/`docsUrl`.
56
+
57
+ ## Usage
58
+
59
+ ```ts
60
+ import { invariant, toErrorBody, VelaError } from '@velajs/errors';
61
+
62
+ // Throw a catalogued error; status/hint default from the core catalog.
63
+ throw new VelaError('not_found', { message: 'no such route', hint: 'Run `vela route list`.' });
64
+
65
+ // Internal-coded errors are rich in logs, redacted on the wire.
66
+ invariant(subscription !== undefined, 'subscription registry out of sync', { subId });
67
+
68
+ // At every transport edge, funnel through the one seam:
69
+ const { body, status, redacted } = toErrorBody(caughtError, { catalog: appCatalog });
70
+ if (redacted) logger.error(caughtError); // safe details stay server-side
71
+ return Response.json(body, { status });
72
+ ```
73
+
74
+ `invariant(condition, message, data?)` narrows types (`asserts condition`) and, on failure, throws an `internal`-coded `VelaError` — always redacted by rule 2. `unreachable(value: never)` is its exhaustiveness-check companion.
75
+
76
+ ## API
77
+
78
+ - `VelaError`, `VelaErrorOptions` — the one error and its constructor options.
79
+ - `isVelaError`, `VelaErrorLike` — the branded structural guard and its type.
80
+ - `toErrorBody`, `WireErrorObject`, `ErrorBodyResult`, `ToErrorBodyOptions` — the single wire-redaction seam.
81
+ - `defineErrorCatalog`, `composeCatalogs`, `Catalog`, `ErrorCatalogEntry` — catalog authoring.
82
+ - `CORE_CATALOG`, `CORE_ENTRIES`, `CoreErrorCode`, `STATUS_TO_CODE` — the core catalog and its lookups.
83
+ - `invariant`, `unreachable` — internal-coded assertion helpers.
@@ -0,0 +1,76 @@
1
+ export interface ErrorCatalogEntry {
2
+ status: number;
3
+ title: string;
4
+ hint?: string;
5
+ docsUrl?: string;
6
+ /** Redaction posture: true → message/hint/data are never echoed to clients. */
7
+ internal?: boolean;
8
+ }
9
+ export declare const CORE_ENTRIES: {
10
+ readonly bad_request: {
11
+ readonly status: 400;
12
+ readonly title: "Bad Request";
13
+ };
14
+ readonly unauthorized: {
15
+ readonly status: 401;
16
+ readonly title: "Unauthorized";
17
+ };
18
+ readonly forbidden: {
19
+ readonly status: 403;
20
+ readonly title: "Forbidden";
21
+ };
22
+ readonly not_found: {
23
+ readonly status: 404;
24
+ readonly title: "Not Found";
25
+ };
26
+ readonly method_not_allowed: {
27
+ readonly status: 405;
28
+ readonly title: "Method Not Allowed";
29
+ };
30
+ readonly conflict: {
31
+ readonly status: 409;
32
+ readonly title: "Conflict";
33
+ };
34
+ readonly gone: {
35
+ readonly status: 410;
36
+ readonly title: "Gone";
37
+ };
38
+ readonly payload_too_large: {
39
+ readonly status: 413;
40
+ readonly title: "Payload Too Large";
41
+ };
42
+ readonly unsupported_media_type: {
43
+ readonly status: 415;
44
+ readonly title: "Unsupported Media Type";
45
+ };
46
+ readonly unprocessable: {
47
+ readonly status: 422;
48
+ readonly title: "Unprocessable Entity";
49
+ };
50
+ readonly too_many_requests: {
51
+ readonly status: 429;
52
+ readonly title: "Too Many Requests";
53
+ };
54
+ readonly internal: {
55
+ readonly status: 500;
56
+ readonly title: "Internal Server Error";
57
+ readonly internal: true;
58
+ };
59
+ readonly not_implemented: {
60
+ readonly status: 501;
61
+ readonly title: "Not Implemented";
62
+ };
63
+ readonly bad_gateway: {
64
+ readonly status: 502;
65
+ readonly title: "Bad Gateway";
66
+ };
67
+ readonly service_unavailable: {
68
+ readonly status: 503;
69
+ readonly title: "Service Unavailable";
70
+ };
71
+ readonly gateway_timeout: {
72
+ readonly status: 504;
73
+ readonly title: "Gateway Timeout";
74
+ };
75
+ };
76
+ export type CoreErrorCode = keyof typeof CORE_ENTRIES;
@@ -0,0 +1,67 @@
1
+ export const CORE_ENTRIES = {
2
+ bad_request: {
3
+ status: 400,
4
+ title: 'Bad Request'
5
+ },
6
+ unauthorized: {
7
+ status: 401,
8
+ title: 'Unauthorized'
9
+ },
10
+ forbidden: {
11
+ status: 403,
12
+ title: 'Forbidden'
13
+ },
14
+ not_found: {
15
+ status: 404,
16
+ title: 'Not Found'
17
+ },
18
+ method_not_allowed: {
19
+ status: 405,
20
+ title: 'Method Not Allowed'
21
+ },
22
+ conflict: {
23
+ status: 409,
24
+ title: 'Conflict'
25
+ },
26
+ gone: {
27
+ status: 410,
28
+ title: 'Gone'
29
+ },
30
+ payload_too_large: {
31
+ status: 413,
32
+ title: 'Payload Too Large'
33
+ },
34
+ unsupported_media_type: {
35
+ status: 415,
36
+ title: 'Unsupported Media Type'
37
+ },
38
+ unprocessable: {
39
+ status: 422,
40
+ title: 'Unprocessable Entity'
41
+ },
42
+ too_many_requests: {
43
+ status: 429,
44
+ title: 'Too Many Requests'
45
+ },
46
+ internal: {
47
+ status: 500,
48
+ title: 'Internal Server Error',
49
+ internal: true
50
+ },
51
+ not_implemented: {
52
+ status: 501,
53
+ title: 'Not Implemented'
54
+ },
55
+ bad_gateway: {
56
+ status: 502,
57
+ title: 'Bad Gateway'
58
+ },
59
+ service_unavailable: {
60
+ status: 503,
61
+ title: 'Service Unavailable'
62
+ },
63
+ gateway_timeout: {
64
+ status: 504,
65
+ title: 'Gateway Timeout'
66
+ }
67
+ };
@@ -0,0 +1,15 @@
1
+ import { type CoreErrorCode, type ErrorCatalogEntry } from './catalog-data';
2
+ import { VelaError, type VelaErrorOptions } from './error';
3
+ export type { CoreErrorCode, ErrorCatalogEntry } from './catalog-data';
4
+ export { CORE_ENTRIES } from './catalog-data';
5
+ export interface Catalog<C extends string = string> {
6
+ readonly entries: Readonly<Record<C, ErrorCatalogEntry>>;
7
+ /** Typed thrower bound to this catalog's defaults. */
8
+ error(code: C | (string & {}), options?: VelaErrorOptions): VelaError;
9
+ has(code: string): boolean;
10
+ get(code: string): ErrorCatalogEntry | undefined;
11
+ }
12
+ export declare const defineErrorCatalog: <const T extends Record<string, ErrorCatalogEntry>>(entries: T) => Catalog<Extract<keyof T, string>>;
13
+ export declare const composeCatalogs: (...catalogs: Array<Catalog<string>>) => Catalog<string>;
14
+ export declare const CORE_CATALOG: Catalog<CoreErrorCode>;
15
+ export declare const STATUS_TO_CODE: Readonly<Record<number, CoreErrorCode>>;
@@ -0,0 +1,37 @@
1
+ import { CORE_ENTRIES } from "./catalog-data.js";
2
+ import { VelaError } from "./error.js";
3
+ export { CORE_ENTRIES } from "./catalog-data.js";
4
+ const makeCatalog = (entries)=>({
5
+ entries,
6
+ error (code, options = {}) {
7
+ const entry = entries[code];
8
+ return new VelaError(code, {
9
+ ...options,
10
+ status: options.status ?? entry?.status ?? 500,
11
+ hint: options.hint ?? entry?.hint,
12
+ docsUrl: options.docsUrl ?? entry?.docsUrl
13
+ });
14
+ },
15
+ has: (code)=>Object.hasOwn(entries, code),
16
+ get: (code)=>Object.hasOwn(entries, code) ? entries[code] : undefined
17
+ });
18
+ export const defineErrorCatalog = (entries)=>makeCatalog(entries);
19
+ export const composeCatalogs = (...catalogs)=>{
20
+ const merged = {};
21
+ for (const catalog of catalogs){
22
+ for (const [code, entry] of Object.entries(catalog.entries)){
23
+ if (Object.hasOwn(merged, code)) {
24
+ throw new VelaError('internal', {
25
+ message: `duplicate error code '${code}' while composing catalogs`
26
+ });
27
+ }
28
+ merged[code] = entry;
29
+ }
30
+ }
31
+ return makeCatalog(merged);
32
+ };
33
+ export const CORE_CATALOG = makeCatalog(CORE_ENTRIES);
34
+ export const STATUS_TO_CODE = Object.fromEntries(Object.entries(CORE_ENTRIES).map(([code, e])=>[
35
+ e.status,
36
+ code
37
+ ]));
@@ -0,0 +1,27 @@
1
+ import { type CoreErrorCode } from './catalog-data';
2
+ export interface VelaErrorOptions {
3
+ message?: string;
4
+ status?: number;
5
+ hint?: string;
6
+ docsUrl?: string;
7
+ data?: unknown;
8
+ cause?: unknown;
9
+ }
10
+ /**
11
+ * The one Vela error. Every field is an OWN ENUMERABLE property so the error
12
+ * rides any wire codec / structuredClone / DO-RPC prop-copy with no special
13
+ * serialization path. `type` is the brand `isVelaError` checks — it must
14
+ * survive serialization, which own+enumerable guarantees.
15
+ */
16
+ export declare class VelaError extends Error {
17
+ readonly type = "VelaError";
18
+ readonly code: string;
19
+ readonly status: number;
20
+ readonly hint?: string;
21
+ readonly docsUrl?: string;
22
+ readonly data?: unknown;
23
+ constructor(code: CoreErrorCode, options?: VelaErrorOptions);
24
+ constructor(code: string, options: VelaErrorOptions & {
25
+ status: number;
26
+ });
27
+ }
package/dist/error.js ADDED
@@ -0,0 +1,27 @@
1
+ import { CORE_ENTRIES } from "./catalog-data.js";
2
+ /**
3
+ * The one Vela error. Every field is an OWN ENUMERABLE property so the error
4
+ * rides any wire codec / structuredClone / DO-RPC prop-copy with no special
5
+ * serialization path. `type` is the brand `isVelaError` checks — it must
6
+ * survive serialization, which own+enumerable guarantees.
7
+ */ export class VelaError extends Error {
8
+ type = 'VelaError';
9
+ code;
10
+ status;
11
+ hint;
12
+ docsUrl;
13
+ data;
14
+ constructor(code, options = {}){
15
+ const entry = CORE_ENTRIES[code];
16
+ super(options.message ?? entry?.title ?? code, options.cause !== undefined ? {
17
+ cause: options.cause
18
+ } : undefined);
19
+ this.name = 'VelaError';
20
+ this.code = code;
21
+ this.status = options.status ?? entry?.status ?? 500;
22
+ if (options.hint ?? entry?.hint) this.hint = options.hint ?? entry?.hint;
23
+ if (options.docsUrl ?? entry?.docsUrl) this.docsUrl = options.docsUrl ?? entry?.docsUrl;
24
+ if (options.data !== undefined) this.data = options.data;
25
+ Object.setPrototypeOf(this, new.target.prototype);
26
+ }
27
+ }
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Structural, realm-safe, BRANDED guard. `instanceof VelaError` is unreliable
3
+ * across DO↔worker RPC and for wire-decoded twins; a bare code+status shape
4
+ * check lets foreign driver errors ride the client-echo path. The brand
5
+ * (`type === 'VelaError'`, an own enumerable prop that survives serialization)
6
+ * closes both failure modes. Nothing load-bearing may use `instanceof`.
7
+ */
8
+ export interface VelaErrorLike extends Error {
9
+ type: 'VelaError';
10
+ code: string;
11
+ status: number;
12
+ hint?: string;
13
+ docsUrl?: string;
14
+ data?: unknown;
15
+ }
16
+ export declare const isVelaError: (error: unknown) => error is VelaErrorLike;
package/dist/guard.js ADDED
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Structural, realm-safe, BRANDED guard. `instanceof VelaError` is unreliable
3
+ * across DO↔worker RPC and for wire-decoded twins; a bare code+status shape
4
+ * check lets foreign driver errors ride the client-echo path. The brand
5
+ * (`type === 'VelaError'`, an own enumerable prop that survives serialization)
6
+ * closes both failure modes. Nothing load-bearing may use `instanceof`.
7
+ */ export const isVelaError = (error)=>{
8
+ if (!(error instanceof Error)) return false;
9
+ const candidate = error;
10
+ return typeof candidate.code === 'string' && typeof candidate.status === 'number' && candidate.type === 'VelaError';
11
+ };
@@ -0,0 +1,11 @@
1
+ export { VelaError } from './error';
2
+ export type { VelaErrorOptions } from './error';
3
+ export { CORE_ENTRIES } from './catalog-data';
4
+ export type { CoreErrorCode, ErrorCatalogEntry } from './catalog-data';
5
+ export { CORE_CATALOG, STATUS_TO_CODE, composeCatalogs, defineErrorCatalog } from './catalog';
6
+ export type { Catalog } from './catalog';
7
+ export { isVelaError } from './guard';
8
+ export type { VelaErrorLike } from './guard';
9
+ export { toErrorBody } from './to-error-body';
10
+ export type { ErrorBodyResult, ToErrorBodyOptions, WireErrorObject } from './to-error-body';
11
+ export { invariant, unreachable } from './invariant';
package/dist/index.js ADDED
@@ -0,0 +1,6 @@
1
+ export { VelaError } from "./error.js";
2
+ export { CORE_ENTRIES } from "./catalog-data.js";
3
+ export { CORE_CATALOG, STATUS_TO_CODE, composeCatalogs, defineErrorCatalog } from "./catalog.js";
4
+ export { isVelaError } from "./guard.js";
5
+ export { toErrorBody } from "./to-error-body.js";
6
+ export { invariant, unreachable } from "./invariant.js";
@@ -0,0 +1,3 @@
1
+ /** Throws an internal-coded VelaError — rich in server logs, redacted on the wire. */
2
+ export declare function invariant(condition: unknown, message: string, data?: unknown): asserts condition;
3
+ export declare function unreachable(value: never, message?: string): never;
@@ -0,0 +1,17 @@
1
+ import { VelaError } from "./error.js";
2
+ /** Throws an internal-coded VelaError — rich in server logs, redacted on the wire. */ export function invariant(condition, message, data) {
3
+ if (!condition) {
4
+ throw new VelaError('internal', {
5
+ message: `Invariant violation: ${message}`,
6
+ data
7
+ });
8
+ }
9
+ }
10
+ export function unreachable(value, message = 'unreachable code reached') {
11
+ throw new VelaError('internal', {
12
+ message,
13
+ data: {
14
+ value
15
+ }
16
+ });
17
+ }
@@ -0,0 +1,34 @@
1
+ import { type Catalog } from './catalog';
2
+ export interface WireErrorObject {
3
+ code: string;
4
+ message: string;
5
+ hint?: string;
6
+ docsUrl?: string;
7
+ details?: unknown;
8
+ }
9
+ export interface ErrorBodyResult {
10
+ body: {
11
+ error: WireErrorObject;
12
+ };
13
+ status: number;
14
+ redacted: boolean;
15
+ }
16
+ export interface ToErrorBodyOptions {
17
+ /** Composed catalog; defaults to the core catalog. */
18
+ catalog?: Catalog<string>;
19
+ /** Status used for unbranded errors. Default 500. */
20
+ fallbackStatus?: number;
21
+ redactedMessage?: (status: number) => string;
22
+ /** Injectable wire codec for `data` → `details` (bigint/bytes etc.). */
23
+ encodeData?: (data: unknown) => unknown;
24
+ /** Default true. */
25
+ includeHint?: boolean;
26
+ }
27
+ /**
28
+ * THE single wire-redaction seam. Every transport edge (HTTP, WS, live, queue
29
+ * reporting) builds its client-bound error content here, so the invariant
30
+ * "unbranded or internal-coded errors never echo their message" holds
31
+ * identically everywhere. `redacted: true` is the caller's signal to log the
32
+ * raw error server-side — this function never logs (zero-dep purity).
33
+ */
34
+ export declare const toErrorBody: (error: unknown, options?: ToErrorBodyOptions) => ErrorBodyResult;
@@ -0,0 +1,50 @@
1
+ import { CORE_CATALOG, STATUS_TO_CODE } from "./catalog.js";
2
+ import { isVelaError } from "./guard.js";
3
+ const defaultRedactedMessage = (status, catalog)=>{
4
+ const code = STATUS_TO_CODE[status];
5
+ return code && catalog.get(code)?.title || 'Internal Server Error';
6
+ };
7
+ /**
8
+ * THE single wire-redaction seam. Every transport edge (HTTP, WS, live, queue
9
+ * reporting) builds its client-bound error content here, so the invariant
10
+ * "unbranded or internal-coded errors never echo their message" holds
11
+ * identically everywhere. `redacted: true` is the caller's signal to log the
12
+ * raw error server-side — this function never logs (zero-dep purity).
13
+ */ export const toErrorBody = (error, options = {})=>{
14
+ const catalog = options.catalog ?? CORE_CATALOG;
15
+ const message = options.redactedMessage ?? ((s)=>defaultRedactedMessage(s, catalog));
16
+ const redact = (status, code)=>({
17
+ body: {
18
+ error: {
19
+ code,
20
+ message: message(status)
21
+ }
22
+ },
23
+ status,
24
+ redacted: true
25
+ });
26
+ if (!isVelaError(error)) {
27
+ const status = options.fallbackStatus ?? 500;
28
+ return redact(status, STATUS_TO_CODE[status] ?? 'internal');
29
+ }
30
+ const entry = catalog.get(error.code);
31
+ if (error.code === 'internal' || entry?.internal === true) {
32
+ return redact(error.status, error.code);
33
+ }
34
+ const wire = {
35
+ code: error.code,
36
+ message: error.message
37
+ };
38
+ const hint = error.hint ?? entry?.hint;
39
+ if (options.includeHint !== false && hint !== undefined) wire.hint = hint;
40
+ const docsUrl = error.docsUrl ?? entry?.docsUrl;
41
+ if (docsUrl !== undefined) wire.docsUrl = docsUrl;
42
+ if (error.data !== undefined) wire.details = options.encodeData ? options.encodeData(error.data) : error.data;
43
+ return {
44
+ body: {
45
+ error: wire
46
+ },
47
+ status: error.status,
48
+ redacted: false
49
+ };
50
+ };
package/package.json ADDED
@@ -0,0 +1,53 @@
1
+ {
2
+ "name": "@velajs/errors",
3
+ "version": "1.0.0",
4
+ "description": "Unified error layer for Vela: branded VelaError, composable error catalogs, and the single toErrorBody wire-redaction seam",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.js"
12
+ }
13
+ },
14
+ "files": [
15
+ "dist",
16
+ "README.md",
17
+ "LICENSE",
18
+ "CHANGELOG.md"
19
+ ],
20
+ "sideEffects": false,
21
+ "keywords": [
22
+ "vela",
23
+ "errors",
24
+ "error-catalog",
25
+ "redaction",
26
+ "framework"
27
+ ],
28
+ "author": "ksh",
29
+ "license": "MIT",
30
+ "repository": {
31
+ "type": "git",
32
+ "url": "git+https://github.com/velajs/errors.git"
33
+ },
34
+ "homepage": "https://github.com/velajs/errors#readme",
35
+ "bugs": {
36
+ "url": "https://github.com/velajs/errors/issues"
37
+ },
38
+ "engines": {
39
+ "node": ">=20"
40
+ },
41
+ "devDependencies": {
42
+ "@swc/cli": "^0.8.1",
43
+ "@swc/core": "^1.15.43",
44
+ "typescript": "^6.0.3",
45
+ "unplugin-swc": "^1.5.9",
46
+ "vitest": "^4.1.9"
47
+ },
48
+ "scripts": {
49
+ "build": "rm -rf dist && swc src -d dist --strip-leading-paths && tsc --emitDeclarationOnly",
50
+ "test": "vitest run",
51
+ "typecheck": "tsc --noEmit -p tsconfig.test.json"
52
+ }
53
+ }