@lunora/errors 0.0.1 → 1.0.0-alpha.1
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/LICENSE.md +105 -0
- package/README.md +32 -29
- package/__assets__/package-og.svg +14 -0
- package/dist/index.d.mts +335 -0
- package/dist/index.d.ts +335 -0
- package/dist/index.mjs +5 -0
- package/dist/packem_shared/ERROR_CATALOG-D3knuUQT.mjs +207 -0
- package/dist/packem_shared/LunoraError-bpS_TPIe.mjs +38 -0
- package/dist/packem_shared/invariant-DLXTsHpj.mjs +12 -0
- package/dist/packem_shared/isLunoraError-BvsoKcWE.mjs +9 -0
- package/dist/packem_shared/toErrorBody-DihI5p4Q.mjs +26 -0
- package/package.json +47 -7
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
const ERROR_CATALOG = {
|
|
2
|
+
BAD_REQUEST: { status: 400, title: "Bad request" },
|
|
3
|
+
UNAUTHORIZED: { status: 401, title: "Unauthorized" },
|
|
4
|
+
FORBIDDEN: { status: 403, title: "Forbidden" },
|
|
5
|
+
NOT_FOUND: { status: 404, title: "Not found" },
|
|
6
|
+
CONFLICT: {
|
|
7
|
+
hint: [
|
|
8
|
+
"Another write changed this row while your mutation was running (optimistic concurrency conflict).",
|
|
9
|
+
"",
|
|
10
|
+
"Re-read the row and retry the mutation with the fresh value. Lunora serializes a DO's mutations, so a persistent conflict usually means the handler conflicts **with itself** (e.g. a trigger or cascade touching the same row) — split that work rather than adding a retry loop."
|
|
11
|
+
],
|
|
12
|
+
status: 409,
|
|
13
|
+
title: "Conflict"
|
|
14
|
+
},
|
|
15
|
+
NOT_UNIQUE: {
|
|
16
|
+
hint: [
|
|
17
|
+
"A row with the same value already exists in a `unique` index.",
|
|
18
|
+
"",
|
|
19
|
+
"- If you meant to upsert, use `ctx.db.<table>().upsert(...)` (or `.patch(...)` an existing row) instead of `.insert(...)`.",
|
|
20
|
+
`- Otherwise pick a value that isn't already taken, and consider surfacing a friendly "already exists" message to the user.`
|
|
21
|
+
],
|
|
22
|
+
status: 400,
|
|
23
|
+
title: "Unique constraint violation"
|
|
24
|
+
},
|
|
25
|
+
VALIDATION_ERROR: { status: 400, title: "Validation failed" },
|
|
26
|
+
TOO_MANY_REQUESTS: { status: 429, title: "Too many requests" },
|
|
27
|
+
UNPROCESSABLE: { status: 422, title: "Unprocessable" },
|
|
28
|
+
NOT_IMPLEMENTED: { status: 501, title: "Not implemented" },
|
|
29
|
+
/** RPC/REST dispatch codes emitted by the runtime + Durable Object router. */
|
|
30
|
+
FUNCTION_NOT_FOUND: { status: 404, title: "Function not found" },
|
|
31
|
+
METHOD_NOT_ALLOWED: { status: 405, title: "Method not allowed" },
|
|
32
|
+
PAYLOAD_TOO_LARGE: { status: 413, title: "Payload too large" },
|
|
33
|
+
/** Free-form internal failure — redacted to a generic message on the wire. */
|
|
34
|
+
INTERNAL: { internal: true, status: 500, title: "Internal error" },
|
|
35
|
+
/** Alias of {@link ERROR_CATALOG.INTERNAL} kept for `@lunora/server`'s historical code name. */
|
|
36
|
+
INTERNAL_SERVER_ERROR: { internal: true, status: 500, title: "Internal error" },
|
|
37
|
+
/** Non-mappable throw crossed the RPC boundary. */
|
|
38
|
+
RPC_FAILED: { internal: true, status: 500, title: "Internal error" },
|
|
39
|
+
COUNT_RLS_UNSUPPORTED: { status: 422, title: "count() is unsupported under an RLS policy" },
|
|
40
|
+
MASK_UNSUPPORTED: { status: 422, title: "Aggregation over a masked column is unsupported" },
|
|
41
|
+
RELATION_PREDICATE_UNSUPPORTED: { status: 422, title: "Relation predicate is unsupported in a write policy" },
|
|
42
|
+
RLS_REQUIRED: {
|
|
43
|
+
hint: [
|
|
44
|
+
"This table is secure-by-default: it has no `.public()` marker and no RLS policy resolved for the caller, so the read fails closed.",
|
|
45
|
+
"",
|
|
46
|
+
"Add a read policy with `.rls(...)`, or mark the table `.public()` if it is intentionally world-readable."
|
|
47
|
+
],
|
|
48
|
+
status: 403,
|
|
49
|
+
title: "RLS policy required"
|
|
50
|
+
},
|
|
51
|
+
SHARD_ERROR: { status: 503, title: "Shard error" },
|
|
52
|
+
SHARD_UNAVAILABLE: { status: 503, title: "Shard unavailable" },
|
|
53
|
+
OFFLINE_IDENTITY_CHANGED: { status: 409, title: "Offline identity changed" },
|
|
54
|
+
/** Package-specific codes. Build-time (codegen) codes never cross the RPC wire. */
|
|
55
|
+
CODEGEN_DIAGNOSTIC: { status: 500, title: "Codegen diagnostic" },
|
|
56
|
+
SCHEMA_SNAPSHOT_PARSE: { status: 500, title: "Schema snapshot parse error" },
|
|
57
|
+
ENV_INVALID: { status: 500, title: "Invalid environment" },
|
|
58
|
+
AUTH_HEADERS_MISSING: { status: 500, title: "Auth headers missing" },
|
|
59
|
+
/**
|
|
60
|
+
* Upstream Cloudflare API failures surfaced from an action. The message
|
|
61
|
+
* carries the upstream response body (Cloudflare's own error text — trusted
|
|
62
|
+
* infra, not user input), so it is echoed rather than redacted. `status`
|
|
63
|
+
* here is a fallback; each throw passes the actual upstream HTTP status.
|
|
64
|
+
*/
|
|
65
|
+
ANALYTICS_SQL_ERROR: { status: 502, title: "Analytics Engine SQL API error" },
|
|
66
|
+
R2_SQL_ERROR: { status: 502, title: "R2 SQL API error" },
|
|
67
|
+
WORKFLOWS_REST_ERROR: { status: 502, title: "Cloudflare Workflows REST API error" }
|
|
68
|
+
};
|
|
69
|
+
const isInternalCode = (code) => ERROR_CATALOG[code]?.internal === true;
|
|
70
|
+
const MESSAGE_SOLUTIONS = [
|
|
71
|
+
{
|
|
72
|
+
body: [
|
|
73
|
+
"Lunora codegen couldn't find a schema to generate from.",
|
|
74
|
+
"",
|
|
75
|
+
"Create `lunora/schema.ts` exporting a `defineSchema(...)` call:",
|
|
76
|
+
"",
|
|
77
|
+
"```ts",
|
|
78
|
+
'import { defineSchema, defineTable, v } from "@lunora/server";',
|
|
79
|
+
"",
|
|
80
|
+
"export default defineSchema({",
|
|
81
|
+
" messages: defineTable({ body: v.string() }),",
|
|
82
|
+
"});",
|
|
83
|
+
"```",
|
|
84
|
+
"",
|
|
85
|
+
"Or run `lunora init` to scaffold Lunora (a sample `lunora/schema.ts` included) into your app."
|
|
86
|
+
].join("\n"),
|
|
87
|
+
header: "No Lunora schema found",
|
|
88
|
+
id: "lunora-schema-missing",
|
|
89
|
+
test: (message) => message.includes("defineSchema() not found") || message.includes("schema.ts not found at")
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
body: [
|
|
93
|
+
"`defineSchema(...)` must be called with an **inline object literal** mapping table names to `defineTable(...)`:",
|
|
94
|
+
"",
|
|
95
|
+
"```ts",
|
|
96
|
+
"export default defineSchema({",
|
|
97
|
+
" todos: defineTable({ title: v.string(), done: v.boolean() }),",
|
|
98
|
+
"});",
|
|
99
|
+
"```",
|
|
100
|
+
"",
|
|
101
|
+
"Codegen reads the schema statically, so it can't follow a variable or a spread — pass the object literal directly."
|
|
102
|
+
].join("\n"),
|
|
103
|
+
header: "`defineSchema()` needs an inline object literal",
|
|
104
|
+
id: "lunora-schema-not-object-literal",
|
|
105
|
+
test: (message) => message.includes("defineSchema() expects an object literal")
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
body: [
|
|
109
|
+
"This table name collides with a built-in `ctx.db` member, so the generated client can't expose it.",
|
|
110
|
+
"",
|
|
111
|
+
"Rename the table to anything that isn't a reserved name (the error lists them) — e.g. `userAccounts` instead of `insert`."
|
|
112
|
+
].join("\n"),
|
|
113
|
+
header: "Table name is reserved",
|
|
114
|
+
id: "lunora-table-reserved",
|
|
115
|
+
test: (message) => message.includes("is reserved") && message.includes("ctx.db")
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
body: [
|
|
119
|
+
"Two tables resolve to the same name — usually a base table and a schema **extension** both defining it.",
|
|
120
|
+
"",
|
|
121
|
+
"Rename one of them, or drop the duplicate from the extension. Each table name must be unique across `defineSchema(...)` and every `.extend(...)`."
|
|
122
|
+
].join("\n"),
|
|
123
|
+
header: "Duplicate table name",
|
|
124
|
+
id: "lunora-table-duplicate",
|
|
125
|
+
// Anchor on `.extend(` — the only Lunora throw for this is
|
|
126
|
+
// `defineSchema(...).extend(...): table "x" already exists …`. Matching a
|
|
127
|
+
// bare "already exists"/"extension" pair would false-positive on
|
|
128
|
+
// unrelated forwarded errors (e.g. a "file already exists" + "extension").
|
|
129
|
+
test: (message) => message.includes("already exists") && message.includes(".extend(")
|
|
130
|
+
},
|
|
131
|
+
{
|
|
132
|
+
body: [
|
|
133
|
+
'`.jurisdiction(...)` accepts only a **string literal** of `"eu"`, `"us"`, or `"fedramp"`:',
|
|
134
|
+
"",
|
|
135
|
+
"```ts",
|
|
136
|
+
'defineSchema({ /* … */ }).jurisdiction("eu");',
|
|
137
|
+
"```"
|
|
138
|
+
].join("\n"),
|
|
139
|
+
header: "Invalid `.jurisdiction(...)` value",
|
|
140
|
+
id: "lunora-jurisdiction",
|
|
141
|
+
test: (message) => message.includes("unknown jurisdiction") || message.includes("jurisdiction") && message.includes('"eu", "us", or "fedramp"')
|
|
142
|
+
},
|
|
143
|
+
{
|
|
144
|
+
body: [
|
|
145
|
+
"The `unique` flag on an index must be a **literal** `true` or `false`, not a computed value — codegen needs to read it statically:",
|
|
146
|
+
"",
|
|
147
|
+
"```ts",
|
|
148
|
+
'defineTable({ email: v.string() }).index("by_email", ["email"], { unique: true });',
|
|
149
|
+
"```"
|
|
150
|
+
].join("\n"),
|
|
151
|
+
header: "`unique` must be a literal",
|
|
152
|
+
id: "lunora-unique-literal",
|
|
153
|
+
test: (message) => message.includes("must be a literal") && message.includes("unique")
|
|
154
|
+
},
|
|
155
|
+
{
|
|
156
|
+
body: [
|
|
157
|
+
"A declared container/workflow class isn't re-exported by your worker entry, so `wrangler deploy` would reject it.",
|
|
158
|
+
"",
|
|
159
|
+
"Add the generated re-export shown in the error to your worker entry (e.g. `src/index.ts`):",
|
|
160
|
+
"",
|
|
161
|
+
"```ts",
|
|
162
|
+
'export * from "./lunora/_generated/containers";',
|
|
163
|
+
"```"
|
|
164
|
+
].join("\n"),
|
|
165
|
+
header: "Binding not exported by your worker entry",
|
|
166
|
+
id: "lunora-worker-entry-export-gap",
|
|
167
|
+
test: (message) => message.includes("not exported by your worker entry")
|
|
168
|
+
},
|
|
169
|
+
{
|
|
170
|
+
body: ERROR_CATALOG.NOT_UNIQUE.hint.join("\n"),
|
|
171
|
+
header: "Unique constraint violation",
|
|
172
|
+
id: "lunora-runtime-unique",
|
|
173
|
+
test: (message) => message.includes("unique constraint violation on")
|
|
174
|
+
},
|
|
175
|
+
{
|
|
176
|
+
body: ERROR_CATALOG.CONFLICT.hint.join("\n"),
|
|
177
|
+
header: "Optimistic concurrency conflict",
|
|
178
|
+
id: "lunora-runtime-occ",
|
|
179
|
+
test: (message) => message.includes("optimistic concurrency conflict")
|
|
180
|
+
}
|
|
181
|
+
];
|
|
182
|
+
const flattenHint = (hint) => (Array.isArray(hint) ? hint.join("\n") : hint).split("\n").filter((line) => !line.startsWith("```")).join("\n").replaceAll(/\*\*(.+?)\*\*/gu, "$1").replaceAll(/`([^`]+)`/gu, "$1");
|
|
183
|
+
const findSolutionByMessage = (message) => {
|
|
184
|
+
for (const rule of MESSAGE_SOLUTIONS) {
|
|
185
|
+
if (rule.test(message)) {
|
|
186
|
+
return { body: rule.body, header: rule.header, id: rule.id };
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
return void 0;
|
|
190
|
+
};
|
|
191
|
+
const resolveHint = (input) => {
|
|
192
|
+
if (typeof input === "string") {
|
|
193
|
+
return findSolutionByMessage(input)?.body;
|
|
194
|
+
}
|
|
195
|
+
if (input.hint !== void 0) {
|
|
196
|
+
return input.hint;
|
|
197
|
+
}
|
|
198
|
+
if (input.code !== void 0) {
|
|
199
|
+
const entry = ERROR_CATALOG[input.code];
|
|
200
|
+
if (entry?.hint !== void 0) {
|
|
201
|
+
return entry.hint;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
return input.message === void 0 ? void 0 : findSolutionByMessage(input.message)?.body;
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
export { ERROR_CATALOG, MESSAGE_SOLUTIONS, findSolutionByMessage, flattenHint, isInternalCode, resolveHint };
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { ERROR_CATALOG } from './ERROR_CATALOG-D3knuUQT.mjs';
|
|
2
|
+
|
|
3
|
+
class LunoraError extends Error {
|
|
4
|
+
/**
|
|
5
|
+
* Discriminator recognised by `@visulima/error`'s `renderError`/`isVisulimaError`
|
|
6
|
+
* (`error.type === "VisulimaError"`), so a `LunoraError` renders like a native
|
|
7
|
+
* `VisulimaError` — hint and all.
|
|
8
|
+
*/
|
|
9
|
+
type = "VisulimaError";
|
|
10
|
+
/** Actionable fix (Markdown), rendered by the CLI/overlay/Studio. */
|
|
11
|
+
hint;
|
|
12
|
+
/** Short, human-readable summary (separate from `message`). */
|
|
13
|
+
title;
|
|
14
|
+
/** Source location, when known (mirrors `VisulimaError.loc`). */
|
|
15
|
+
loc;
|
|
16
|
+
/** Machine-readable reason, keyed into {@link ERROR_CATALOG}. */
|
|
17
|
+
code;
|
|
18
|
+
/** HTTP/RPC status for the transport mappers. */
|
|
19
|
+
status;
|
|
20
|
+
/** Optional link to deeper docs. */
|
|
21
|
+
docsUrl;
|
|
22
|
+
/** Optional structured payload propagated verbatim to the client. */
|
|
23
|
+
data;
|
|
24
|
+
constructor(code, message, options = {}) {
|
|
25
|
+
const entry = ERROR_CATALOG[code];
|
|
26
|
+
super(message ?? code, { cause: options.cause });
|
|
27
|
+
this.name = options.name ?? "LunoraError";
|
|
28
|
+
this.hint = options.hint ?? entry?.hint;
|
|
29
|
+
this.title = options.title ?? entry?.title;
|
|
30
|
+
this.loc = options.location;
|
|
31
|
+
this.code = code;
|
|
32
|
+
this.status = options.status ?? entry?.status ?? 500;
|
|
33
|
+
this.docsUrl = options.docsUrl ?? entry?.docsUrl;
|
|
34
|
+
this.data = options.data;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export { LunoraError };
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { LunoraError } from './LunoraError-bpS_TPIe.mjs';
|
|
2
|
+
|
|
3
|
+
const invariant = (condition, message) => {
|
|
4
|
+
if (!condition) {
|
|
5
|
+
throw new LunoraError("INTERNAL", message, { name: "InvariantError" });
|
|
6
|
+
}
|
|
7
|
+
};
|
|
8
|
+
const unreachable = (message) => {
|
|
9
|
+
throw new LunoraError("INTERNAL", message, { name: "InvariantError" });
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export { invariant, unreachable };
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { isInternalCode, resolveHint } from './ERROR_CATALOG-D3knuUQT.mjs';
|
|
2
|
+
import { isLunoraError } from './isLunoraError-BvsoKcWE.mjs';
|
|
3
|
+
|
|
4
|
+
const toErrorBody = (error, options = {}) => {
|
|
5
|
+
const redactedMessage = options.redactedMessage ?? "Internal error";
|
|
6
|
+
if (isLunoraError(error)) {
|
|
7
|
+
if (isInternalCode(error.code)) {
|
|
8
|
+
return { body: { code: error.code, message: redactedMessage }, redacted: true, status: error.status };
|
|
9
|
+
}
|
|
10
|
+
const body = { code: error.code, message: error.message };
|
|
11
|
+
if (error.data !== void 0 && options.encodeData !== void 0) {
|
|
12
|
+
body.data = options.encodeData(error.data);
|
|
13
|
+
}
|
|
14
|
+
const hint = resolveHint({ code: error.code, hint: error.hint, message: error.message });
|
|
15
|
+
if (hint !== void 0) {
|
|
16
|
+
body.hint = hint;
|
|
17
|
+
}
|
|
18
|
+
if (error.docsUrl !== void 0) {
|
|
19
|
+
body.docsUrl = error.docsUrl;
|
|
20
|
+
}
|
|
21
|
+
return { body, redacted: false, status: error.status };
|
|
22
|
+
}
|
|
23
|
+
return { body: { code: options.fallbackCode ?? "INTERNAL", message: redactedMessage }, redacted: true, status: 500 };
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export { toErrorBody };
|
package/package.json
CHANGED
|
@@ -1,10 +1,50 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lunora/errors",
|
|
3
|
-
"version": "0.0.1",
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "1.0.0-alpha.1",
|
|
4
|
+
"description": "Unified error layer for Lunora: one LunoraError base + a central catalog of codes, statuses, and actionable hints, rendered across CLI, overlay, Studio, and the client",
|
|
5
5
|
"keywords": [
|
|
6
|
-
"
|
|
7
|
-
"
|
|
8
|
-
"
|
|
9
|
-
|
|
10
|
-
|
|
6
|
+
"cloudflare",
|
|
7
|
+
"durable-objects",
|
|
8
|
+
"error",
|
|
9
|
+
"error-handling",
|
|
10
|
+
"hints",
|
|
11
|
+
"lunora",
|
|
12
|
+
"workers"
|
|
13
|
+
],
|
|
14
|
+
"homepage": "https://lunora.sh",
|
|
15
|
+
"bugs": "https://github.com/anolilab/lunora/issues",
|
|
16
|
+
"license": "FSL-1.1-Apache-2.0",
|
|
17
|
+
"author": {
|
|
18
|
+
"name": "Daniel Bannert",
|
|
19
|
+
"email": "d.bannert@anolilab.de"
|
|
20
|
+
},
|
|
21
|
+
"repository": {
|
|
22
|
+
"type": "git",
|
|
23
|
+
"url": "git+https://github.com/anolilab/lunora.git",
|
|
24
|
+
"directory": "packages/errors"
|
|
25
|
+
},
|
|
26
|
+
"files": [
|
|
27
|
+
"./dist",
|
|
28
|
+
"README.md",
|
|
29
|
+
"LICENSE.md",
|
|
30
|
+
"__assets__"
|
|
31
|
+
],
|
|
32
|
+
"type": "module",
|
|
33
|
+
"sideEffects": false,
|
|
34
|
+
"main": "./dist/index.mjs",
|
|
35
|
+
"module": "./dist/index.mjs",
|
|
36
|
+
"types": "./dist/index.d.ts",
|
|
37
|
+
"exports": {
|
|
38
|
+
".": {
|
|
39
|
+
"types": "./dist/index.d.ts",
|
|
40
|
+
"import": "./dist/index.mjs"
|
|
41
|
+
},
|
|
42
|
+
"./package.json": "./package.json"
|
|
43
|
+
},
|
|
44
|
+
"publishConfig": {
|
|
45
|
+
"access": "public"
|
|
46
|
+
},
|
|
47
|
+
"engines": {
|
|
48
|
+
"node": "^22.15.0 || >=24.11.0"
|
|
49
|
+
}
|
|
50
|
+
}
|