@lunora/errors 1.0.0-alpha.1 → 1.0.0-alpha.10

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 CHANGED
@@ -103,3 +103,9 @@ Unless required by applicable law or agreed to in writing, software distributed
103
103
  under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
104
104
  CONDITIONS OF ANY KIND, either express or implied. See the License for the
105
105
  specific language governing permissions and limitations under the License.
106
+
107
+ <!-- DEPENDENCIES -->
108
+ <!-- /DEPENDENCIES -->
109
+
110
+ <!-- TYPE_DEPENDENCIES -->
111
+ <!-- /TYPE_DEPENDENCIES -->
package/dist/index.d.mts CHANGED
@@ -1,18 +1,5 @@
1
- /**
2
- * The central Lunora error catalog — the single source of truth mapping a
3
- * machine-readable `code` to its transport `status`, a short human `title`, and
4
- * (where useful) an actionable Markdown `hint` plus a `docsUrl`.
5
- *
6
- * This table is consumed everywhere an error is surfaced: the runtime/DO wire
7
- * mappers (status), the client SDK (code discrimination), the CLI renderer and
8
- * the Vite overlay (hint), and the Studio UI (title + hint + docs link). It also
9
- * absorbs the former `@lunora/codegen` "solutions" table (see {@link MESSAGE_SOLUTIONS})
10
- * so codegen build-time errors — which are thrown as plain messages into
11
- * generated code and lose their class identity before a consumer sees them —
12
- * keep their message-matched hints.
13
- */
14
1
  /** Markdown hint: a single string or an array of lines. Shape matches `@visulima/error`'s `hint`. */
15
- type ErrorHint = string | string[];
2
+ type ErrorHint = string | ReadonlyArray<string>;
16
3
  /** A catalog entry: the fixed metadata for one error `code`. */
17
4
  interface ErrorCatalogEntry {
18
5
  /** Optional URL to deeper docs for this error. */
@@ -20,11 +7,11 @@ interface ErrorCatalogEntry {
20
7
  /** Optional actionable fix, authored as Markdown (rendered by CLI/overlay/Studio). */
21
8
  hint?: ErrorHint;
22
9
  /**
23
- * When `true`, this code's `message` must NOT cross the wire — an internal
24
- * failure or unhandled invariant may carry SQL fragments, file paths, or
25
- * internal identifiers. The transport mappers emit a generic message for
26
- * these (and log the real one server-side). See {@link isInternalCode}.
27
- */
10
+ * When `true`, this code's `message` must NOT cross the wire — an internal
11
+ * failure or unhandled invariant may carry SQL fragments, file paths, or
12
+ * internal identifiers. The transport mappers emit a generic message for
13
+ * these (and log the real one server-side). See {@link isInternalCode}.
14
+ */
28
15
  internal?: boolean;
29
16
  /** HTTP/RPC status this code maps to on the wire. */
30
17
  status: number;
@@ -32,10 +19,10 @@ interface ErrorCatalogEntry {
32
19
  title: string;
33
20
  }
34
21
  /**
35
- * Every well-known Lunora error code. Domain packages may throw additional
36
- * codes (passing an explicit `status`); those are added here as their package is
37
- * migrated. The keys of this object form the {@link LunoraErrorCode} union.
38
- */
22
+ * Every well-known Lunora error code. Domain packages may throw additional
23
+ * codes (passing an explicit `status`); those are added here as their package is
24
+ * migrated. The keys of this object form the {@link LunoraErrorCode} union.
25
+ */
39
26
  declare const ERROR_CATALOG: {
40
27
  readonly BAD_REQUEST: {
41
28
  readonly status: 400;
@@ -59,9 +46,9 @@ declare const ERROR_CATALOG: {
59
46
  readonly title: "Conflict";
60
47
  };
61
48
  readonly NOT_UNIQUE: {
62
- readonly hint: readonly ["A row with the same value already exists in a `unique` index.", "", "- If you meant to upsert, use `ctx.db.<table>().upsert(...)` (or `.patch(...)` an existing row) instead of `.insert(...)`.", "- Otherwise pick a value that isn't already taken, and consider surfacing a friendly \"already exists\" message to the user."];
49
+ readonly hint: readonly ["`.unique()` matched more than one document — it expects the query to identify at most one row.", "", "- If several matches are legitimate, use `.first()` (take one) or `.collect()` (take all) instead.", "- Otherwise tighten the query (e.g. filter on a unique/indexed field) so it can only match one row."];
63
50
  readonly status: 400;
64
- readonly title: "Unique constraint violation";
51
+ readonly title: "Query matched more than one document";
65
52
  };
66
53
  readonly VALIDATION_ERROR: {
67
54
  readonly status: 400;
@@ -78,7 +65,8 @@ declare const ERROR_CATALOG: {
78
65
  readonly NOT_IMPLEMENTED: {
79
66
  readonly status: 501;
80
67
  readonly title: "Not implemented";
81
- }; /** RPC/REST dispatch codes emitted by the runtime + Durable Object router. */
68
+ };
69
+ /** RPC/REST dispatch codes emitted by the runtime + Durable Object router. */
82
70
  readonly FUNCTION_NOT_FOUND: {
83
71
  readonly status: 404;
84
72
  readonly title: "Function not found";
@@ -90,7 +78,8 @@ declare const ERROR_CATALOG: {
90
78
  readonly PAYLOAD_TOO_LARGE: {
91
79
  readonly status: 413;
92
80
  readonly title: "Payload too large";
93
- }; /** Free-form internal failure — redacted to a generic message on the wire. */
81
+ };
82
+ /** Free-form internal failure — redacted to a generic message on the wire. */
94
83
  readonly INTERNAL: {
95
84
  readonly internal: true;
96
85
  readonly status: 500;
@@ -125,6 +114,28 @@ declare const ERROR_CATALOG: {
125
114
  readonly status: 403;
126
115
  readonly title: "RLS policy required";
127
116
  };
117
+ readonly RUN_DEPTH_EXCEEDED: {
118
+ readonly internal: true;
119
+ readonly status: 500;
120
+ readonly title: "Run depth exceeded";
121
+ };
122
+ readonly TRANSACTION_LIMIT_EXCEEDED: {
123
+ readonly hint: readonly ["A single mutation may only read and write a bounded amount before it is stopped.", "", "Narrow the read with an index (`.withIndex(...)`) instead of scanning the table, or split the write across several mutations — for a large backfill use `defineMigration` + `lunora migrate up`, which batches and checkpoints for you.", "", "The ceilings are deliberately conservative — they exist to stop one request taking down the whole shard. A deployment that genuinely needs bigger transactions can raise them by overriding the `transactionLimits()` seam on its generated shard class."];
124
+ readonly status: 413;
125
+ readonly title: "Transaction limit exceeded";
126
+ };
127
+ readonly MIGRATION_NOT_FOUND: {
128
+ readonly status: 404;
129
+ readonly title: "Data migration not found";
130
+ };
131
+ readonly UNKNOWN_TABLE: {
132
+ readonly status: 404;
133
+ readonly title: "Unknown table";
134
+ };
135
+ readonly GLOBAL_TABLE_NOT_EDITABLE: {
136
+ readonly status: 400;
137
+ readonly title: "Global table is not editable";
138
+ };
128
139
  readonly SHARD_ERROR: {
129
140
  readonly status: 503;
130
141
  readonly title: "Shard error";
@@ -136,29 +147,55 @@ declare const ERROR_CATALOG: {
136
147
  readonly OFFLINE_IDENTITY_CHANGED: {
137
148
  readonly status: 409;
138
149
  readonly title: "Offline identity changed";
139
- }; /** Package-specific codes. Build-time (codegen) codes never cross the RPC wire. */
150
+ };
151
+ /** Package-specific codes. Build-time-only — never cross the RPC wire, so deliberately not `internal`. */
140
152
  readonly CODEGEN_DIAGNOSTIC: {
141
153
  readonly status: 500;
142
154
  readonly title: "Codegen diagnostic";
143
155
  };
156
+ /** Build-time-only — never crosses the RPC wire, so deliberately not `internal`. */
144
157
  readonly SCHEMA_SNAPSHOT_PARSE: {
145
158
  readonly status: 500;
146
159
  readonly title: "Schema snapshot parse error";
147
160
  };
161
+ /** Runtime-reachable (env.ts): message enumerates failing env key names — redact on the wire. */
148
162
  readonly ENV_INVALID: {
163
+ readonly internal: true;
149
164
  readonly status: 500;
150
165
  readonly title: "Invalid environment";
151
166
  };
167
+ /** Runtime-reachable (auth/middleware.ts): message carries auth-wiring guidance — redact on the wire. */
152
168
  readonly AUTH_HEADERS_MISSING: {
169
+ readonly internal: true;
153
170
  readonly status: 500;
154
171
  readonly title: "Auth headers missing";
155
172
  };
156
173
  /**
157
- * Upstream Cloudflare API failures surfaced from an action. The message
158
- * carries the upstream response body (Cloudflare's own error text trusted
159
- * infra, not user input), so it is echoed rather than redacted. `status`
160
- * here is a fallback; each throw passes the actual upstream HTTP status.
161
- */
174
+ * Signup rejected by `@lunora/auth`'s email-domain gate a disposable/throwaway
175
+ * provider (or a caller deny-list hit). Client-safe: the message names only the
176
+ * offending domain class, never a secret, so it is echoed rather than redacted.
177
+ */
178
+ readonly EMAIL_DOMAIN_BLOCKED: {
179
+ readonly hint: readonly ["This address's domain is on the disposable/throwaway blocklist (or your configured deny-list).", "", "Sign up with a permanent mailbox. To tune the policy, pass `blockDisposable` / `allowDomains` / `denyDomains` to `emailGate(...)` (`@lunora/auth/email-guard`)."];
180
+ readonly status: 400;
181
+ readonly title: "Email domain not allowed";
182
+ };
183
+ /**
184
+ * Opt-in MX verification (`@lunora/auth/email-guard`, `mx: true`) found no mail
185
+ * exchanger for the address's domain, so mail to it would never deliver.
186
+ * Client-safe: names only the domain, no secret.
187
+ */
188
+ readonly EMAIL_UNDELIVERABLE: {
189
+ readonly hint: readonly ["The address's domain publishes no MX (or fallback A/AAAA) records, so it can't receive mail.", "", "Check for a typo in the domain. MX verification is opt-in (`mx: true`) and needs DNS — leave it off on the edge path if DNS is unavailable."];
190
+ readonly status: 400;
191
+ readonly title: "Email domain cannot receive mail";
192
+ };
193
+ /**
194
+ * Upstream Cloudflare API failures surfaced from an action. The message
195
+ * carries the upstream response body (Cloudflare's own error text — trusted
196
+ * infra, not user input), so it is echoed rather than redacted. `status`
197
+ * here is a fallback; each throw passes the actual upstream HTTP status.
198
+ */
162
199
  readonly ANALYTICS_SQL_ERROR: {
163
200
  readonly status: 502;
164
201
  readonly title: "Analytics Engine SQL API error";
@@ -175,21 +212,21 @@ declare const ERROR_CATALOG: {
175
212
  /** A well-known Lunora error code (a key of {@link ERROR_CATALOG}). */
176
213
  type LunoraErrorCode = keyof typeof ERROR_CATALOG;
177
214
  /**
178
- * True when `code` is an internal/redacted code — an internal failure or
179
- * unhandled invariant whose `message` must NOT cross the wire (it may carry SQL
180
- * fragments, file paths, or internal identifiers). Derived from the catalog's
181
- * `internal` flag so the redaction posture stays in one place (the table).
182
- * Throwing a `LunoraError` with any non-internal code is the author's vouch that
183
- * its message is client-safe; an unknown/unregistered code is treated as safe.
184
- */
215
+ * True when `code` is an internal/redacted code — an internal failure or
216
+ * unhandled invariant whose `message` must NOT cross the wire (it may carry SQL
217
+ * fragments, file paths, or internal identifiers). Derived from the catalog's
218
+ * `internal` flag so the redaction posture stays in one place (the table).
219
+ * Throwing a `LunoraError` with any non-internal code is the author's vouch that
220
+ * its message is client-safe; an unknown/unregistered code is treated as safe.
221
+ */
185
222
  declare const isInternalCode: (code: string) => boolean;
186
223
  /**
187
- * A message-matched solution for errors that reach a consumer without a `code`
188
- * — chiefly `@lunora/codegen` build errors, which are thrown as plain messages
189
- * into generated code (and flattened to `{ message }` by the Vite overlay), so
190
- * the message text is the only stable join key. Ordered most- to least-specific;
191
- * the first matching rule wins.
192
- */
224
+ * A message-matched solution for errors that reach a consumer without a `code`
225
+ * — chiefly `@lunora/codegen` build errors, which are thrown as plain messages
226
+ * into generated code (and flattened to `{ message }` by the Vite overlay), so
227
+ * the message text is the only stable join key. Ordered most- to least-specific;
228
+ * the first matching rule wins.
229
+ */
193
230
  interface Solution {
194
231
  /** Markdown body shown under the header. */
195
232
  body: string;
@@ -204,27 +241,92 @@ interface SolutionRule extends Solution {
204
241
  test: (message: string) => boolean;
205
242
  }
206
243
  /**
207
- * Message-matched solutions (migrated verbatim from the former
208
- * `@lunora/codegen` solutions table). Re-exported by `@lunora/codegen` as
209
- * `LUNORA_SOLUTION_RULES` for backward compatibility.
210
- */
244
+ * Message-matched solutions (migrated verbatim from the former
245
+ * `@lunora/codegen` solutions table). Re-exported by `@lunora/codegen` as
246
+ * `LUNORA_SOLUTION_RULES` for backward compatibility.
247
+ */
211
248
  declare const MESSAGE_SOLUTIONS: ReadonlyArray<SolutionRule>;
212
249
  /**
213
- * Flatten a Markdown hint to plain text for a terminal / non-Markdown surface:
214
- * drop code-fence markers and strip inline `**bold**` / `` `code` `` emphasis.
215
- * Shared by the CLI renderer and the Studio `ErrorAlert` so the two can't drift.
216
- */
250
+ * One documented Cloudflare **platform** error an edge/origin (5xx) or
251
+ * Cloudflare-service (1xxx) failure surfaced in an error *message* rather than
252
+ * thrown by Lunora as a coded `LunoraError`. These reach a Lunora app as
253
+ * plain text: a Worker that fetches a Cloudflare-fronted origin sees `Error 522`,
254
+ * a deploy that throws surfaces as `Error 1101`, and so on. The fields are the
255
+ * curated facts a grounded explainer elaborates on — never invents beyond.
256
+ */
257
+ interface CloudflarePlatformError {
258
+ /** Documented likely causes (a short, comma-joined clause). */
259
+ causes: string;
260
+ /** The numeric Cloudflare error code, as it appears in the message (e.g. `"522"`, `"1101"`). */
261
+ code: string;
262
+ /** Canonical Cloudflare support-docs URL for this error's family. */
263
+ docsUrl: string;
264
+ /** Which docs family the code belongs to — shown in the "see docs" line. */
265
+ family: "1xxx" | "5xx";
266
+ /** The documented remediation. */
267
+ fix: string;
268
+ /** One-line summary of what the code means. */
269
+ summary: string;
270
+ /** Cloudflare's short name for the code (e.g. `"Connection timed out"`). */
271
+ title: string;
272
+ }
273
+ /**
274
+ * The curated Cloudflare platform-error table. Sourced from Cloudflare's official
275
+ * support docs — the codes surfaced to app authors on Workers/DO deployments (the
276
+ * origin-connection 52x family and the Worker/DNS/security 1xxx family). `1101`
277
+ * (a Worker threw) and `1102` (a Worker exceeded CPU) are the most Lunora-relevant.
278
+ */
279
+ declare const CLOUDFLARE_PLATFORM_ERRORS: ReadonlyArray<CloudflarePlatformError>;
280
+ /**
281
+ * Recognize a Cloudflare platform-error {@link CloudflarePlatformError} in a raw
282
+ * error message, conservatively: the message must carry Cloudflare's own
283
+ * `Error &lt;code>` phrasing, or mention `cloudflare` alongside the standalone
284
+ * code. That keeps a bare number (`expected 520 items`) from false-matching a 5xx
285
+ * code, at the cost of missing a context-free code — the safe trade for a
286
+ * grounded hint. Returns the matched code's {@link Solution}, or `undefined`.
287
+ *
288
+ * Matching runs in two passes, strongest first: Cloudflare's own `Error &lt;code>`
289
+ * phrasing is unambiguous, so it must win over the weaker "mentions cloudflare
290
+ * near some number" heuristic regardless of table order. A single pass let a weak
291
+ * match on an earlier entry beat an explicit match on a later one — `"Cloudflare
292
+ * Error 1102: exceeded after 524 ms"` resolved to 524, and that wrong grounded
293
+ * fix is exactly what the explainer prompt is built from.
294
+ */
295
+ declare const findCloudflarePlatformSolution: (message: string) => Solution | undefined;
296
+ /**
297
+ * Flatten a Markdown hint to plain text for a terminal / non-Markdown surface:
298
+ * drop code-fence markers and strip inline `**bold**` / `` `code` `` emphasis.
299
+ * Shared by the CLI renderer and the Studio `ErrorAlert` so the two can't drift.
300
+ */
217
301
  declare const flattenHint: (hint: ErrorHint) => string;
218
302
  /**
219
- * Find the first message-matched {@link Solution} for `message`, or `undefined`
220
- * if none recognize it. Re-exported by `@lunora/codegen` as `findLunoraSolution`.
221
- */
303
+ * Find the first message-matched {@link Solution} for `message`, or `undefined`
304
+ * if none recognize it. Re-exported by `@lunora/codegen` as `findLunoraSolution`.
305
+ */
222
306
  declare const findSolutionByMessage: (message: string) => Solution | undefined;
223
307
  /**
224
- * Resolve an actionable hint for an error: prefer a hint carried on the error
225
- * (or its `code`'s catalog entry), then fall back to a message match. Returns
226
- * `undefined` when nothing recognizes it.
227
- */
308
+ * Find a solution for `message` across BOTH Lunora's own rules and the curated
309
+ * Cloudflare platform-error table the lookup the Studio Issues panel and the
310
+ * `explainIssue` grounding use.
311
+ *
312
+ * Deliberately separate from {@link findSolutionByMessage} rather than folded into
313
+ * it. That function is on `resolveHint`, and therefore on `toErrorBody` — the
314
+ * envelope builder for every failed request. Most `ERROR_CATALOG` entries
315
+ * carry no `hint`, so folding the platform table in there meant an ordinary
316
+ * `BAD_REQUEST` whose message merely mentioned "cloudflare" near a number shipped
317
+ * zone-configuration guidance ("review the zone's Firewall/WAF and IP Access
318
+ * Rules") to unauthenticated browsers. The same fold put the table on the CLI
319
+ * renderer and the Vite overlay, and on `toErrorBody`'s hot path.
320
+ *
321
+ * Platform errors are operator-facing context for an already-persisted Issue, so
322
+ * the operator-facing surfaces opt in here and the wire path stays Lunora-only.
323
+ */
324
+ declare const findIssueSolution: (message: string) => Solution | undefined;
325
+ /**
326
+ * Resolve an actionable hint for an error: prefer a hint carried on the error
327
+ * (or its `code`'s catalog entry), then fall back to a message match. Returns
328
+ * `undefined` when nothing recognizes it.
329
+ */
228
330
  declare const resolveHint: (input: {
229
331
  code?: string;
230
332
  hint?: ErrorHint;
@@ -256,16 +358,16 @@ interface LunoraErrorOptions {
256
358
  title?: string;
257
359
  }
258
360
  /**
259
- * A code string: a well-known {@link LunoraErrorCode} (with autocomplete) or any
260
- * package-specific code not yet in the catalog.
261
- */
361
+ * A code string: a well-known {@link LunoraErrorCode} (with autocomplete) or any
362
+ * package-specific code not yet in the catalog.
363
+ */
262
364
  type LunoraErrorCodeInput = LunoraErrorCode | (string & {});
263
365
  declare class LunoraError extends Error {
264
366
  /**
265
- * Discriminator recognised by `@visulima/error`'s `renderError`/`isVisulimaError`
266
- * (`error.type === "VisulimaError"`), so a `LunoraError` renders like a native
267
- * `VisulimaError` — hint and all.
268
- */
367
+ * Discriminator recognised by `@visulima/error`'s `renderError`/`isVisulimaError`
368
+ * (`error.type === "VisulimaError"`), so a `LunoraError` renders like a native
369
+ * `VisulimaError` — hint and all.
370
+ */
269
371
  readonly type = "VisulimaError";
270
372
  /** Actionable fix (Markdown), rendered by the CLI/overlay/Studio. */
271
373
  readonly hint: ErrorHint | undefined;
@@ -273,7 +375,7 @@ declare class LunoraError extends Error {
273
375
  readonly title: string | undefined;
274
376
  /** Source location, when known (mirrors `VisulimaError.loc`). */
275
377
  readonly loc: ErrorLocation | undefined;
276
- /** Machine-readable reason, keyed into {@link ERROR_CATALOG}. */
378
+ /** Machine-readable reason, keyed into `ERROR_CATALOG`. */
277
379
  readonly code: string;
278
380
  /** HTTP/RPC status for the transport mappers. */
279
381
  readonly status: number;
@@ -290,8 +392,15 @@ interface LunoraErrorLike extends Error {
290
392
  docsUrl?: string;
291
393
  hint?: ErrorHint;
292
394
  status: number;
395
+ /** Wire brand that distinguishes real `LunoraError`s from foreign errors. */
396
+ type: "VisulimaError";
293
397
  }
294
- /** True when `error` carries the Lunora transport shape (string `code` + numeric `status`). */
398
+ /**
399
+ * True when `error` carries the Lunora transport shape (string `code` + numeric
400
+ * `status` + the `VisulimaError` brand). The `type` brand is what distinguishes
401
+ * a real `LunoraError` (or its wire-decoded twin) from a foreign error that
402
+ * happens to carry `code`/`status` — see plan 119 for the full rationale.
403
+ */
295
404
  declare const isLunoraError: (error: unknown) => error is LunoraErrorLike;
296
405
  /** Throw an `INTERNAL` {@link LunoraError} when `condition` is falsy. */
297
406
  declare const invariant: (condition: unknown, message: string) => asserts condition;
@@ -307,9 +416,9 @@ interface ErrorBody {
307
416
  }
308
417
  interface ToErrorBodyOptions {
309
418
  /**
310
- * Wire-encode a `LunoraError`'s structured `data` for the client (so a
311
- * `bigint`/`bytes` inside it survives). Omit to drop `data` from the body.
312
- */
419
+ * Wire-encode a `LunoraError`'s structured `data` for the client (so a
420
+ * `bigint`/`bytes` inside it survives). Omit to drop `data` from the body.
421
+ */
313
422
  encodeData?: (data: unknown) => unknown;
314
423
  /** Code for an unrecognized (non-`LunoraError`) throw. Default `"INTERNAL"`. */
315
424
  fallbackCode?: string;
@@ -324,12 +433,12 @@ interface ToErrorBodyResult {
324
433
  status: number;
325
434
  }
326
435
  /**
327
- * Turn a thrown value into an {@link ErrorBody} + status, applying the redaction
328
- * invariant. A `LunoraError` with a non-internal code is echoed with its
329
- * `message`, resolved `hint`, `docsUrl`, and (when `encodeData` is given) its
330
- * `data`. An internal-coded `LunoraError` keeps its `code`/`status` but its
331
- * message is replaced with `redactedMessage`. Anything else becomes a generic
332
- * `fallbackCode`/500. When `redacted` is `true`, log the raw error server-side.
333
- */
436
+ * Turn a thrown value into an {@link ErrorBody} + status, applying the redaction
437
+ * invariant. A `LunoraError` with a non-internal code is echoed with its
438
+ * `message`, resolved `hint`, `docsUrl`, and (when `encodeData` is given) its
439
+ * `data`. An internal-coded `LunoraError` keeps its `code`/`status` but its
440
+ * message is replaced with `redactedMessage`. Anything else becomes a generic
441
+ * `fallbackCode`/500. When `redacted` is `true`, log the raw error server-side.
442
+ */
334
443
  declare const toErrorBody: (error: unknown, options?: ToErrorBodyOptions) => ToErrorBodyResult;
335
- export { ERROR_CATALOG, type ErrorBody, type ErrorCatalogEntry, type ErrorHint, type ErrorLocation, LunoraError, type LunoraErrorCode, type LunoraErrorCodeInput, type LunoraErrorLike, type LunoraErrorOptions, MESSAGE_SOLUTIONS, type Solution, type SolutionRule, type ToErrorBodyOptions, type ToErrorBodyResult, findSolutionByMessage, flattenHint, invariant, isInternalCode, isLunoraError, resolveHint, toErrorBody, unreachable };
444
+ export { CLOUDFLARE_PLATFORM_ERRORS, type CloudflarePlatformError, ERROR_CATALOG, type ErrorBody, type ErrorCatalogEntry, type ErrorHint, type ErrorLocation, LunoraError, type LunoraErrorCode, type LunoraErrorCodeInput, type LunoraErrorLike, type LunoraErrorOptions, MESSAGE_SOLUTIONS, type Solution, type SolutionRule, type ToErrorBodyOptions, type ToErrorBodyResult, findCloudflarePlatformSolution, findIssueSolution, findSolutionByMessage, flattenHint, invariant, isInternalCode, isLunoraError, resolveHint, toErrorBody, unreachable };
package/dist/index.d.ts CHANGED
@@ -1,18 +1,5 @@
1
- /**
2
- * The central Lunora error catalog — the single source of truth mapping a
3
- * machine-readable `code` to its transport `status`, a short human `title`, and
4
- * (where useful) an actionable Markdown `hint` plus a `docsUrl`.
5
- *
6
- * This table is consumed everywhere an error is surfaced: the runtime/DO wire
7
- * mappers (status), the client SDK (code discrimination), the CLI renderer and
8
- * the Vite overlay (hint), and the Studio UI (title + hint + docs link). It also
9
- * absorbs the former `@lunora/codegen` "solutions" table (see {@link MESSAGE_SOLUTIONS})
10
- * so codegen build-time errors — which are thrown as plain messages into
11
- * generated code and lose their class identity before a consumer sees them —
12
- * keep their message-matched hints.
13
- */
14
1
  /** Markdown hint: a single string or an array of lines. Shape matches `@visulima/error`'s `hint`. */
15
- type ErrorHint = string | string[];
2
+ type ErrorHint = string | ReadonlyArray<string>;
16
3
  /** A catalog entry: the fixed metadata for one error `code`. */
17
4
  interface ErrorCatalogEntry {
18
5
  /** Optional URL to deeper docs for this error. */
@@ -20,11 +7,11 @@ interface ErrorCatalogEntry {
20
7
  /** Optional actionable fix, authored as Markdown (rendered by CLI/overlay/Studio). */
21
8
  hint?: ErrorHint;
22
9
  /**
23
- * When `true`, this code's `message` must NOT cross the wire — an internal
24
- * failure or unhandled invariant may carry SQL fragments, file paths, or
25
- * internal identifiers. The transport mappers emit a generic message for
26
- * these (and log the real one server-side). See {@link isInternalCode}.
27
- */
10
+ * When `true`, this code's `message` must NOT cross the wire — an internal
11
+ * failure or unhandled invariant may carry SQL fragments, file paths, or
12
+ * internal identifiers. The transport mappers emit a generic message for
13
+ * these (and log the real one server-side). See {@link isInternalCode}.
14
+ */
28
15
  internal?: boolean;
29
16
  /** HTTP/RPC status this code maps to on the wire. */
30
17
  status: number;
@@ -32,10 +19,10 @@ interface ErrorCatalogEntry {
32
19
  title: string;
33
20
  }
34
21
  /**
35
- * Every well-known Lunora error code. Domain packages may throw additional
36
- * codes (passing an explicit `status`); those are added here as their package is
37
- * migrated. The keys of this object form the {@link LunoraErrorCode} union.
38
- */
22
+ * Every well-known Lunora error code. Domain packages may throw additional
23
+ * codes (passing an explicit `status`); those are added here as their package is
24
+ * migrated. The keys of this object form the {@link LunoraErrorCode} union.
25
+ */
39
26
  declare const ERROR_CATALOG: {
40
27
  readonly BAD_REQUEST: {
41
28
  readonly status: 400;
@@ -59,9 +46,9 @@ declare const ERROR_CATALOG: {
59
46
  readonly title: "Conflict";
60
47
  };
61
48
  readonly NOT_UNIQUE: {
62
- readonly hint: readonly ["A row with the same value already exists in a `unique` index.", "", "- If you meant to upsert, use `ctx.db.<table>().upsert(...)` (or `.patch(...)` an existing row) instead of `.insert(...)`.", "- Otherwise pick a value that isn't already taken, and consider surfacing a friendly \"already exists\" message to the user."];
49
+ readonly hint: readonly ["`.unique()` matched more than one document — it expects the query to identify at most one row.", "", "- If several matches are legitimate, use `.first()` (take one) or `.collect()` (take all) instead.", "- Otherwise tighten the query (e.g. filter on a unique/indexed field) so it can only match one row."];
63
50
  readonly status: 400;
64
- readonly title: "Unique constraint violation";
51
+ readonly title: "Query matched more than one document";
65
52
  };
66
53
  readonly VALIDATION_ERROR: {
67
54
  readonly status: 400;
@@ -78,7 +65,8 @@ declare const ERROR_CATALOG: {
78
65
  readonly NOT_IMPLEMENTED: {
79
66
  readonly status: 501;
80
67
  readonly title: "Not implemented";
81
- }; /** RPC/REST dispatch codes emitted by the runtime + Durable Object router. */
68
+ };
69
+ /** RPC/REST dispatch codes emitted by the runtime + Durable Object router. */
82
70
  readonly FUNCTION_NOT_FOUND: {
83
71
  readonly status: 404;
84
72
  readonly title: "Function not found";
@@ -90,7 +78,8 @@ declare const ERROR_CATALOG: {
90
78
  readonly PAYLOAD_TOO_LARGE: {
91
79
  readonly status: 413;
92
80
  readonly title: "Payload too large";
93
- }; /** Free-form internal failure — redacted to a generic message on the wire. */
81
+ };
82
+ /** Free-form internal failure — redacted to a generic message on the wire. */
94
83
  readonly INTERNAL: {
95
84
  readonly internal: true;
96
85
  readonly status: 500;
@@ -125,6 +114,28 @@ declare const ERROR_CATALOG: {
125
114
  readonly status: 403;
126
115
  readonly title: "RLS policy required";
127
116
  };
117
+ readonly RUN_DEPTH_EXCEEDED: {
118
+ readonly internal: true;
119
+ readonly status: 500;
120
+ readonly title: "Run depth exceeded";
121
+ };
122
+ readonly TRANSACTION_LIMIT_EXCEEDED: {
123
+ readonly hint: readonly ["A single mutation may only read and write a bounded amount before it is stopped.", "", "Narrow the read with an index (`.withIndex(...)`) instead of scanning the table, or split the write across several mutations — for a large backfill use `defineMigration` + `lunora migrate up`, which batches and checkpoints for you.", "", "The ceilings are deliberately conservative — they exist to stop one request taking down the whole shard. A deployment that genuinely needs bigger transactions can raise them by overriding the `transactionLimits()` seam on its generated shard class."];
124
+ readonly status: 413;
125
+ readonly title: "Transaction limit exceeded";
126
+ };
127
+ readonly MIGRATION_NOT_FOUND: {
128
+ readonly status: 404;
129
+ readonly title: "Data migration not found";
130
+ };
131
+ readonly UNKNOWN_TABLE: {
132
+ readonly status: 404;
133
+ readonly title: "Unknown table";
134
+ };
135
+ readonly GLOBAL_TABLE_NOT_EDITABLE: {
136
+ readonly status: 400;
137
+ readonly title: "Global table is not editable";
138
+ };
128
139
  readonly SHARD_ERROR: {
129
140
  readonly status: 503;
130
141
  readonly title: "Shard error";
@@ -136,29 +147,55 @@ declare const ERROR_CATALOG: {
136
147
  readonly OFFLINE_IDENTITY_CHANGED: {
137
148
  readonly status: 409;
138
149
  readonly title: "Offline identity changed";
139
- }; /** Package-specific codes. Build-time (codegen) codes never cross the RPC wire. */
150
+ };
151
+ /** Package-specific codes. Build-time-only — never cross the RPC wire, so deliberately not `internal`. */
140
152
  readonly CODEGEN_DIAGNOSTIC: {
141
153
  readonly status: 500;
142
154
  readonly title: "Codegen diagnostic";
143
155
  };
156
+ /** Build-time-only — never crosses the RPC wire, so deliberately not `internal`. */
144
157
  readonly SCHEMA_SNAPSHOT_PARSE: {
145
158
  readonly status: 500;
146
159
  readonly title: "Schema snapshot parse error";
147
160
  };
161
+ /** Runtime-reachable (env.ts): message enumerates failing env key names — redact on the wire. */
148
162
  readonly ENV_INVALID: {
163
+ readonly internal: true;
149
164
  readonly status: 500;
150
165
  readonly title: "Invalid environment";
151
166
  };
167
+ /** Runtime-reachable (auth/middleware.ts): message carries auth-wiring guidance — redact on the wire. */
152
168
  readonly AUTH_HEADERS_MISSING: {
169
+ readonly internal: true;
153
170
  readonly status: 500;
154
171
  readonly title: "Auth headers missing";
155
172
  };
156
173
  /**
157
- * Upstream Cloudflare API failures surfaced from an action. The message
158
- * carries the upstream response body (Cloudflare's own error text trusted
159
- * infra, not user input), so it is echoed rather than redacted. `status`
160
- * here is a fallback; each throw passes the actual upstream HTTP status.
161
- */
174
+ * Signup rejected by `@lunora/auth`'s email-domain gate a disposable/throwaway
175
+ * provider (or a caller deny-list hit). Client-safe: the message names only the
176
+ * offending domain class, never a secret, so it is echoed rather than redacted.
177
+ */
178
+ readonly EMAIL_DOMAIN_BLOCKED: {
179
+ readonly hint: readonly ["This address's domain is on the disposable/throwaway blocklist (or your configured deny-list).", "", "Sign up with a permanent mailbox. To tune the policy, pass `blockDisposable` / `allowDomains` / `denyDomains` to `emailGate(...)` (`@lunora/auth/email-guard`)."];
180
+ readonly status: 400;
181
+ readonly title: "Email domain not allowed";
182
+ };
183
+ /**
184
+ * Opt-in MX verification (`@lunora/auth/email-guard`, `mx: true`) found no mail
185
+ * exchanger for the address's domain, so mail to it would never deliver.
186
+ * Client-safe: names only the domain, no secret.
187
+ */
188
+ readonly EMAIL_UNDELIVERABLE: {
189
+ readonly hint: readonly ["The address's domain publishes no MX (or fallback A/AAAA) records, so it can't receive mail.", "", "Check for a typo in the domain. MX verification is opt-in (`mx: true`) and needs DNS — leave it off on the edge path if DNS is unavailable."];
190
+ readonly status: 400;
191
+ readonly title: "Email domain cannot receive mail";
192
+ };
193
+ /**
194
+ * Upstream Cloudflare API failures surfaced from an action. The message
195
+ * carries the upstream response body (Cloudflare's own error text — trusted
196
+ * infra, not user input), so it is echoed rather than redacted. `status`
197
+ * here is a fallback; each throw passes the actual upstream HTTP status.
198
+ */
162
199
  readonly ANALYTICS_SQL_ERROR: {
163
200
  readonly status: 502;
164
201
  readonly title: "Analytics Engine SQL API error";
@@ -175,21 +212,21 @@ declare const ERROR_CATALOG: {
175
212
  /** A well-known Lunora error code (a key of {@link ERROR_CATALOG}). */
176
213
  type LunoraErrorCode = keyof typeof ERROR_CATALOG;
177
214
  /**
178
- * True when `code` is an internal/redacted code — an internal failure or
179
- * unhandled invariant whose `message` must NOT cross the wire (it may carry SQL
180
- * fragments, file paths, or internal identifiers). Derived from the catalog's
181
- * `internal` flag so the redaction posture stays in one place (the table).
182
- * Throwing a `LunoraError` with any non-internal code is the author's vouch that
183
- * its message is client-safe; an unknown/unregistered code is treated as safe.
184
- */
215
+ * True when `code` is an internal/redacted code — an internal failure or
216
+ * unhandled invariant whose `message` must NOT cross the wire (it may carry SQL
217
+ * fragments, file paths, or internal identifiers). Derived from the catalog's
218
+ * `internal` flag so the redaction posture stays in one place (the table).
219
+ * Throwing a `LunoraError` with any non-internal code is the author's vouch that
220
+ * its message is client-safe; an unknown/unregistered code is treated as safe.
221
+ */
185
222
  declare const isInternalCode: (code: string) => boolean;
186
223
  /**
187
- * A message-matched solution for errors that reach a consumer without a `code`
188
- * — chiefly `@lunora/codegen` build errors, which are thrown as plain messages
189
- * into generated code (and flattened to `{ message }` by the Vite overlay), so
190
- * the message text is the only stable join key. Ordered most- to least-specific;
191
- * the first matching rule wins.
192
- */
224
+ * A message-matched solution for errors that reach a consumer without a `code`
225
+ * — chiefly `@lunora/codegen` build errors, which are thrown as plain messages
226
+ * into generated code (and flattened to `{ message }` by the Vite overlay), so
227
+ * the message text is the only stable join key. Ordered most- to least-specific;
228
+ * the first matching rule wins.
229
+ */
193
230
  interface Solution {
194
231
  /** Markdown body shown under the header. */
195
232
  body: string;
@@ -204,27 +241,92 @@ interface SolutionRule extends Solution {
204
241
  test: (message: string) => boolean;
205
242
  }
206
243
  /**
207
- * Message-matched solutions (migrated verbatim from the former
208
- * `@lunora/codegen` solutions table). Re-exported by `@lunora/codegen` as
209
- * `LUNORA_SOLUTION_RULES` for backward compatibility.
210
- */
244
+ * Message-matched solutions (migrated verbatim from the former
245
+ * `@lunora/codegen` solutions table). Re-exported by `@lunora/codegen` as
246
+ * `LUNORA_SOLUTION_RULES` for backward compatibility.
247
+ */
211
248
  declare const MESSAGE_SOLUTIONS: ReadonlyArray<SolutionRule>;
212
249
  /**
213
- * Flatten a Markdown hint to plain text for a terminal / non-Markdown surface:
214
- * drop code-fence markers and strip inline `**bold**` / `` `code` `` emphasis.
215
- * Shared by the CLI renderer and the Studio `ErrorAlert` so the two can't drift.
216
- */
250
+ * One documented Cloudflare **platform** error an edge/origin (5xx) or
251
+ * Cloudflare-service (1xxx) failure surfaced in an error *message* rather than
252
+ * thrown by Lunora as a coded `LunoraError`. These reach a Lunora app as
253
+ * plain text: a Worker that fetches a Cloudflare-fronted origin sees `Error 522`,
254
+ * a deploy that throws surfaces as `Error 1101`, and so on. The fields are the
255
+ * curated facts a grounded explainer elaborates on — never invents beyond.
256
+ */
257
+ interface CloudflarePlatformError {
258
+ /** Documented likely causes (a short, comma-joined clause). */
259
+ causes: string;
260
+ /** The numeric Cloudflare error code, as it appears in the message (e.g. `"522"`, `"1101"`). */
261
+ code: string;
262
+ /** Canonical Cloudflare support-docs URL for this error's family. */
263
+ docsUrl: string;
264
+ /** Which docs family the code belongs to — shown in the "see docs" line. */
265
+ family: "1xxx" | "5xx";
266
+ /** The documented remediation. */
267
+ fix: string;
268
+ /** One-line summary of what the code means. */
269
+ summary: string;
270
+ /** Cloudflare's short name for the code (e.g. `"Connection timed out"`). */
271
+ title: string;
272
+ }
273
+ /**
274
+ * The curated Cloudflare platform-error table. Sourced from Cloudflare's official
275
+ * support docs — the codes surfaced to app authors on Workers/DO deployments (the
276
+ * origin-connection 52x family and the Worker/DNS/security 1xxx family). `1101`
277
+ * (a Worker threw) and `1102` (a Worker exceeded CPU) are the most Lunora-relevant.
278
+ */
279
+ declare const CLOUDFLARE_PLATFORM_ERRORS: ReadonlyArray<CloudflarePlatformError>;
280
+ /**
281
+ * Recognize a Cloudflare platform-error {@link CloudflarePlatformError} in a raw
282
+ * error message, conservatively: the message must carry Cloudflare's own
283
+ * `Error &lt;code>` phrasing, or mention `cloudflare` alongside the standalone
284
+ * code. That keeps a bare number (`expected 520 items`) from false-matching a 5xx
285
+ * code, at the cost of missing a context-free code — the safe trade for a
286
+ * grounded hint. Returns the matched code's {@link Solution}, or `undefined`.
287
+ *
288
+ * Matching runs in two passes, strongest first: Cloudflare's own `Error &lt;code>`
289
+ * phrasing is unambiguous, so it must win over the weaker "mentions cloudflare
290
+ * near some number" heuristic regardless of table order. A single pass let a weak
291
+ * match on an earlier entry beat an explicit match on a later one — `"Cloudflare
292
+ * Error 1102: exceeded after 524 ms"` resolved to 524, and that wrong grounded
293
+ * fix is exactly what the explainer prompt is built from.
294
+ */
295
+ declare const findCloudflarePlatformSolution: (message: string) => Solution | undefined;
296
+ /**
297
+ * Flatten a Markdown hint to plain text for a terminal / non-Markdown surface:
298
+ * drop code-fence markers and strip inline `**bold**` / `` `code` `` emphasis.
299
+ * Shared by the CLI renderer and the Studio `ErrorAlert` so the two can't drift.
300
+ */
217
301
  declare const flattenHint: (hint: ErrorHint) => string;
218
302
  /**
219
- * Find the first message-matched {@link Solution} for `message`, or `undefined`
220
- * if none recognize it. Re-exported by `@lunora/codegen` as `findLunoraSolution`.
221
- */
303
+ * Find the first message-matched {@link Solution} for `message`, or `undefined`
304
+ * if none recognize it. Re-exported by `@lunora/codegen` as `findLunoraSolution`.
305
+ */
222
306
  declare const findSolutionByMessage: (message: string) => Solution | undefined;
223
307
  /**
224
- * Resolve an actionable hint for an error: prefer a hint carried on the error
225
- * (or its `code`'s catalog entry), then fall back to a message match. Returns
226
- * `undefined` when nothing recognizes it.
227
- */
308
+ * Find a solution for `message` across BOTH Lunora's own rules and the curated
309
+ * Cloudflare platform-error table the lookup the Studio Issues panel and the
310
+ * `explainIssue` grounding use.
311
+ *
312
+ * Deliberately separate from {@link findSolutionByMessage} rather than folded into
313
+ * it. That function is on `resolveHint`, and therefore on `toErrorBody` — the
314
+ * envelope builder for every failed request. Most `ERROR_CATALOG` entries
315
+ * carry no `hint`, so folding the platform table in there meant an ordinary
316
+ * `BAD_REQUEST` whose message merely mentioned "cloudflare" near a number shipped
317
+ * zone-configuration guidance ("review the zone's Firewall/WAF and IP Access
318
+ * Rules") to unauthenticated browsers. The same fold put the table on the CLI
319
+ * renderer and the Vite overlay, and on `toErrorBody`'s hot path.
320
+ *
321
+ * Platform errors are operator-facing context for an already-persisted Issue, so
322
+ * the operator-facing surfaces opt in here and the wire path stays Lunora-only.
323
+ */
324
+ declare const findIssueSolution: (message: string) => Solution | undefined;
325
+ /**
326
+ * Resolve an actionable hint for an error: prefer a hint carried on the error
327
+ * (or its `code`'s catalog entry), then fall back to a message match. Returns
328
+ * `undefined` when nothing recognizes it.
329
+ */
228
330
  declare const resolveHint: (input: {
229
331
  code?: string;
230
332
  hint?: ErrorHint;
@@ -256,16 +358,16 @@ interface LunoraErrorOptions {
256
358
  title?: string;
257
359
  }
258
360
  /**
259
- * A code string: a well-known {@link LunoraErrorCode} (with autocomplete) or any
260
- * package-specific code not yet in the catalog.
261
- */
361
+ * A code string: a well-known {@link LunoraErrorCode} (with autocomplete) or any
362
+ * package-specific code not yet in the catalog.
363
+ */
262
364
  type LunoraErrorCodeInput = LunoraErrorCode | (string & {});
263
365
  declare class LunoraError extends Error {
264
366
  /**
265
- * Discriminator recognised by `@visulima/error`'s `renderError`/`isVisulimaError`
266
- * (`error.type === "VisulimaError"`), so a `LunoraError` renders like a native
267
- * `VisulimaError` — hint and all.
268
- */
367
+ * Discriminator recognised by `@visulima/error`'s `renderError`/`isVisulimaError`
368
+ * (`error.type === "VisulimaError"`), so a `LunoraError` renders like a native
369
+ * `VisulimaError` — hint and all.
370
+ */
269
371
  readonly type = "VisulimaError";
270
372
  /** Actionable fix (Markdown), rendered by the CLI/overlay/Studio. */
271
373
  readonly hint: ErrorHint | undefined;
@@ -273,7 +375,7 @@ declare class LunoraError extends Error {
273
375
  readonly title: string | undefined;
274
376
  /** Source location, when known (mirrors `VisulimaError.loc`). */
275
377
  readonly loc: ErrorLocation | undefined;
276
- /** Machine-readable reason, keyed into {@link ERROR_CATALOG}. */
378
+ /** Machine-readable reason, keyed into `ERROR_CATALOG`. */
277
379
  readonly code: string;
278
380
  /** HTTP/RPC status for the transport mappers. */
279
381
  readonly status: number;
@@ -290,8 +392,15 @@ interface LunoraErrorLike extends Error {
290
392
  docsUrl?: string;
291
393
  hint?: ErrorHint;
292
394
  status: number;
395
+ /** Wire brand that distinguishes real `LunoraError`s from foreign errors. */
396
+ type: "VisulimaError";
293
397
  }
294
- /** True when `error` carries the Lunora transport shape (string `code` + numeric `status`). */
398
+ /**
399
+ * True when `error` carries the Lunora transport shape (string `code` + numeric
400
+ * `status` + the `VisulimaError` brand). The `type` brand is what distinguishes
401
+ * a real `LunoraError` (or its wire-decoded twin) from a foreign error that
402
+ * happens to carry `code`/`status` — see plan 119 for the full rationale.
403
+ */
295
404
  declare const isLunoraError: (error: unknown) => error is LunoraErrorLike;
296
405
  /** Throw an `INTERNAL` {@link LunoraError} when `condition` is falsy. */
297
406
  declare const invariant: (condition: unknown, message: string) => asserts condition;
@@ -307,9 +416,9 @@ interface ErrorBody {
307
416
  }
308
417
  interface ToErrorBodyOptions {
309
418
  /**
310
- * Wire-encode a `LunoraError`'s structured `data` for the client (so a
311
- * `bigint`/`bytes` inside it survives). Omit to drop `data` from the body.
312
- */
419
+ * Wire-encode a `LunoraError`'s structured `data` for the client (so a
420
+ * `bigint`/`bytes` inside it survives). Omit to drop `data` from the body.
421
+ */
313
422
  encodeData?: (data: unknown) => unknown;
314
423
  /** Code for an unrecognized (non-`LunoraError`) throw. Default `"INTERNAL"`. */
315
424
  fallbackCode?: string;
@@ -324,12 +433,12 @@ interface ToErrorBodyResult {
324
433
  status: number;
325
434
  }
326
435
  /**
327
- * Turn a thrown value into an {@link ErrorBody} + status, applying the redaction
328
- * invariant. A `LunoraError` with a non-internal code is echoed with its
329
- * `message`, resolved `hint`, `docsUrl`, and (when `encodeData` is given) its
330
- * `data`. An internal-coded `LunoraError` keeps its `code`/`status` but its
331
- * message is replaced with `redactedMessage`. Anything else becomes a generic
332
- * `fallbackCode`/500. When `redacted` is `true`, log the raw error server-side.
333
- */
436
+ * Turn a thrown value into an {@link ErrorBody} + status, applying the redaction
437
+ * invariant. A `LunoraError` with a non-internal code is echoed with its
438
+ * `message`, resolved `hint`, `docsUrl`, and (when `encodeData` is given) its
439
+ * `data`. An internal-coded `LunoraError` keeps its `code`/`status` but its
440
+ * message is replaced with `redactedMessage`. Anything else becomes a generic
441
+ * `fallbackCode`/500. When `redacted` is `true`, log the raw error server-side.
442
+ */
334
443
  declare const toErrorBody: (error: unknown, options?: ToErrorBodyOptions) => ToErrorBodyResult;
335
- export { ERROR_CATALOG, type ErrorBody, type ErrorCatalogEntry, type ErrorHint, type ErrorLocation, LunoraError, type LunoraErrorCode, type LunoraErrorCodeInput, type LunoraErrorLike, type LunoraErrorOptions, MESSAGE_SOLUTIONS, type Solution, type SolutionRule, type ToErrorBodyOptions, type ToErrorBodyResult, findSolutionByMessage, flattenHint, invariant, isInternalCode, isLunoraError, resolveHint, toErrorBody, unreachable };
444
+ export { CLOUDFLARE_PLATFORM_ERRORS, type CloudflarePlatformError, ERROR_CATALOG, type ErrorBody, type ErrorCatalogEntry, type ErrorHint, type ErrorLocation, LunoraError, type LunoraErrorCode, type LunoraErrorCodeInput, type LunoraErrorLike, type LunoraErrorOptions, MESSAGE_SOLUTIONS, type Solution, type SolutionRule, type ToErrorBodyOptions, type ToErrorBodyResult, findCloudflarePlatformSolution, findIssueSolution, findSolutionByMessage, flattenHint, invariant, isInternalCode, isLunoraError, resolveHint, toErrorBody, unreachable };
package/dist/index.mjs CHANGED
@@ -1,5 +1 @@
1
- export { LunoraError } from './packem_shared/LunoraError-bpS_TPIe.mjs';
2
- export { ERROR_CATALOG, MESSAGE_SOLUTIONS, findSolutionByMessage, flattenHint, isInternalCode, resolveHint } from './packem_shared/ERROR_CATALOG-D3knuUQT.mjs';
3
- export { isLunoraError } from './packem_shared/isLunoraError-BvsoKcWE.mjs';
4
- export { invariant, unreachable } from './packem_shared/invariant-DLXTsHpj.mjs';
5
- export { toErrorBody } from './packem_shared/toErrorBody-DihI5p4Q.mjs';
1
+ import{LunoraError as e}from"./packem_shared/LunoraError-Bi0eJQ64.mjs";import{CLOUDFLARE_PLATFORM_ERRORS as t,ERROR_CATALOG as i,MESSAGE_SOLUTIONS as a,findCloudflarePlatformSolution as f,findIssueSolution as l,findSolutionByMessage as u,flattenHint as E,isInternalCode as R,resolveHint as S}from"./packem_shared/CLOUDFLARE_PLATFORM_ERRORS-AwchC1Yo.mjs";import{isLunoraError as L}from"./packem_shared/isLunoraError-Dvew97xn.mjs";import{invariant as d,unreachable as m}from"./packem_shared/invariant-HTyw4bqA.mjs";import{toErrorBody as x}from"./packem_shared/toErrorBody-Bodp1HTL.mjs";export{t as CLOUDFLARE_PLATFORM_ERRORS,i as ERROR_CATALOG,e as LunoraError,a as MESSAGE_SOLUTIONS,f as findCloudflarePlatformSolution,l as findIssueSolution,u as findSolutionByMessage,E as flattenHint,d as invariant,R as isInternalCode,L as isLunoraError,S as resolveHint,x as toErrorBody,m as unreachable};
@@ -0,0 +1,14 @@
1
+ const o="https://developers.cloudflare.com/support/troubleshooting/http-status-codes/cloudflare-5xx-errors/",a="https://developers.cloudflare.com/support/troubleshooting/http-status-codes/cloudflare-1xxx-errors/",s={BAD_REQUEST:{status:400,title:"Bad request"},UNAUTHORIZED:{status:401,title:"Unauthorized"},FORBIDDEN:{status:403,title:"Forbidden"},NOT_FOUND:{status:404,title:"Not found"},CONFLICT:{hint:["Another write changed this row while your mutation was running (optimistic concurrency conflict).","","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."],status:409,title:"Conflict"},NOT_UNIQUE:{hint:["`.unique()` matched more than one document — it expects the query to identify at most one row.","","- If several matches are legitimate, use `.first()` (take one) or `.collect()` (take all) instead.","- Otherwise tighten the query (e.g. filter on a unique/indexed field) so it can only match one row."],status:400,title:"Query matched more than one document"},VALIDATION_ERROR:{status:400,title:"Validation failed"},TOO_MANY_REQUESTS:{status:429,title:"Too many requests"},UNPROCESSABLE:{status:422,title:"Unprocessable"},NOT_IMPLEMENTED:{status:501,title:"Not implemented"},FUNCTION_NOT_FOUND:{status:404,title:"Function not found"},METHOD_NOT_ALLOWED:{status:405,title:"Method not allowed"},PAYLOAD_TOO_LARGE:{status:413,title:"Payload too large"},INTERNAL:{internal:!0,status:500,title:"Internal error"},INTERNAL_SERVER_ERROR:{internal:!0,status:500,title:"Internal error"},RPC_FAILED:{internal:!0,status:500,title:"Internal error"},COUNT_RLS_UNSUPPORTED:{status:422,title:"count() is unsupported under an RLS policy"},MASK_UNSUPPORTED:{status:422,title:"Aggregation over a masked column is unsupported"},RELATION_PREDICATE_UNSUPPORTED:{status:422,title:"Relation predicate is unsupported in a write policy"},RLS_REQUIRED:{hint:["This table is secure-by-default: it has no `.public()` marker and no RLS policy resolved for the caller, so the read fails closed.","","Add a read policy with `.rls(...)`, or mark the table `.public()` if it is intentionally world-readable."],status:403,title:"RLS policy required"},RUN_DEPTH_EXCEEDED:{internal:!0,status:500,title:"Run depth exceeded"},TRANSACTION_LIMIT_EXCEEDED:{hint:["A single mutation may only read and write a bounded amount before it is stopped.","","Narrow the read with an index (`.withIndex(...)`) instead of scanning the table, or split the write across several mutations — for a large backfill use `defineMigration` + `lunora migrate up`, which batches and checkpoints for you.","","The ceilings are deliberately conservative — they exist to stop one request taking down the whole shard. A deployment that genuinely needs bigger transactions can raise them by overriding the `transactionLimits()` seam on its generated shard class."],status:413,title:"Transaction limit exceeded"},MIGRATION_NOT_FOUND:{status:404,title:"Data migration not found"},UNKNOWN_TABLE:{status:404,title:"Unknown table"},GLOBAL_TABLE_NOT_EDITABLE:{status:400,title:"Global table is not editable"},SHARD_ERROR:{status:503,title:"Shard error"},SHARD_UNAVAILABLE:{status:503,title:"Shard unavailable"},OFFLINE_IDENTITY_CHANGED:{status:409,title:"Offline identity changed"},CODEGEN_DIAGNOSTIC:{status:500,title:"Codegen diagnostic"},SCHEMA_SNAPSHOT_PARSE:{status:500,title:"Schema snapshot parse error"},ENV_INVALID:{internal:!0,status:500,title:"Invalid environment"},AUTH_HEADERS_MISSING:{internal:!0,status:500,title:"Auth headers missing"},EMAIL_DOMAIN_BLOCKED:{hint:["This address's domain is on the disposable/throwaway blocklist (or your configured deny-list).","","Sign up with a permanent mailbox. To tune the policy, pass `blockDisposable` / `allowDomains` / `denyDomains` to `emailGate(...)` (`@lunora/auth/email-guard`)."],status:400,title:"Email domain not allowed"},EMAIL_UNDELIVERABLE:{hint:["The address's domain publishes no MX (or fallback A/AAAA) records, so it can't receive mail.","","Check for a typo in the domain. MX verification is opt-in (`mx: true`) and needs DNS — leave it off on the edge path if DNS is unavailable."],status:400,title:"Email domain cannot receive mail"},ANALYTICS_SQL_ERROR:{status:502,title:"Analytics Engine SQL API error"},R2_SQL_ERROR:{status:502,title:"R2 SQL API error"},WORKFLOWS_REST_ERROR:{status:502,title:"Cloudflare Workflows REST API error"}},h=e=>Object.hasOwn(s,e)?s[e]:void 0,b=e=>h(e)?.internal===!0,f=[{body:["Lunora codegen couldn't find a schema to generate from.","","Create `lunora/schema.ts` exporting a `defineSchema(...)` call:","","```ts",'import { defineSchema, defineTable, v } from "@lunora/server";',"","export default defineSchema({"," messages: defineTable({ body: v.string() }),","});","```","","Or run `lunora init` to scaffold Lunora (a sample `lunora/schema.ts` included) into your app."].join(`
2
+ `),header:"No Lunora schema found",id:"lunora-schema-missing",test:e=>e.includes("defineSchema() not found")||e.includes("schema.ts not found at")},{body:["`defineSchema(...)` must be called with an **inline object literal** mapping table names to `defineTable(...)`:","","```ts","export default defineSchema({"," todos: defineTable({ title: v.string(), done: v.boolean() }),","});","```","","Codegen reads the schema statically, so it can't follow a variable or a spread — pass the object literal directly."].join(`
3
+ `),header:"`defineSchema()` needs an inline object literal",id:"lunora-schema-not-object-literal",test:e=>e.includes("defineSchema() expects an object literal")},{body:["This table name collides with a built-in `ctx.db` member, so the generated client can't expose it.","","Rename the table to anything that isn't a reserved name (the error lists them) — e.g. `userAccounts` instead of `insert`."].join(`
4
+ `),header:"Table name is reserved",id:"lunora-table-reserved",test:e=>e.includes("is reserved")&&e.includes("ctx.db")},{body:["Two tables resolve to the same name — usually a base table and a schema **extension** both defining it.","","Rename one of them, or drop the duplicate from the extension. Each table name must be unique across `defineSchema(...)` and every `.extend(...)`."].join(`
5
+ `),header:"Duplicate table name",id:"lunora-table-duplicate",test:e=>e.includes("already exists")&&e.includes(".extend(")},{body:['`.jurisdiction(...)` accepts only a **string literal** of `"eu"`, `"us"`, or `"fedramp"`:',"","```ts",'defineSchema({ /* … */ }).jurisdiction("eu");',"```"].join(`
6
+ `),header:"Invalid `.jurisdiction(...)` value",id:"lunora-jurisdiction",test:e=>e.includes("unknown jurisdiction")||e.includes("jurisdiction")&&e.includes('"eu", "us", or "fedramp"')},{body:["The `unique` flag on an index must be a **literal** `true` or `false`, not a computed value — codegen needs to read it statically:","","```ts",'defineTable({ email: v.string() }).index("by_email", ["email"], { unique: true });',"```"].join(`
7
+ `),header:"`unique` must be a literal",id:"lunora-unique-literal",test:e=>e.includes("must be a literal")&&e.includes("unique")},{body:["A declared container/workflow class isn't re-exported by your worker entry, so `wrangler deploy` would reject it.","","Add the generated re-export shown in the error to your worker entry (e.g. `src/index.ts`):","","```ts",'export * from "./lunora/_generated/containers";',"```"].join(`
8
+ `),header:"Binding not exported by your worker entry",id:"lunora-worker-entry-export-gap",test:e=>e.includes("not exported by your worker entry")},{body:["A row with the same value already exists in a `unique` index.","","- If you meant to upsert, use `ctx.db.<table>().upsert(...)` (or `.patch(...)` an existing row) instead of `.insert(...)`.",`- Otherwise pick a value that isn't already taken, and consider surfacing a friendly "already exists" message to the user.`].join(`
9
+ `),header:"Unique constraint violation",id:"lunora-runtime-unique",test:e=>e.includes("unique constraint violation on")},{body:s.CONFLICT.hint.join(`
10
+ `),header:"Optimistic concurrency conflict",id:"lunora-runtime-occ",test:e=>e.includes("optimistic concurrency conflict")}],u=[{causes:"the origin returned an empty, unknown, or malformed response Cloudflare couldn't interpret (often an origin crash or an oversized response header)",code:"520",docsUrl:o,family:"5xx",fix:"check your origin's logs for a crash, ensure it returns a valid HTTP response, and keep response headers under the size limit",summary:"Cloudflare got an unknown/empty response from your origin web server.",title:"Web server returns an unknown error"},{causes:"the origin refused the connection — the web server is down, or a firewall is blocking Cloudflare's IP ranges",code:"521",docsUrl:o,family:"5xx",fix:"confirm the origin process is running and allowlist Cloudflare's published IP ranges in your firewall/security groups",summary:"Cloudflare could not connect to your origin because it refused the connection.",title:"Web server is down"},{causes:"Cloudflare could not establish a TCP connection to the origin in time — the origin is overloaded, a firewall is dropping packets, or the origin IP is wrong",code:"522",docsUrl:o,family:"5xx",fix:"verify the origin is reachable and not overloaded, and that the DNS record points at the correct origin IP",summary:"The connection to your origin timed out before it was established.",title:"Connection timed out"},{causes:"Cloudflare cannot route to the origin at all — a bad DNS record, an origin IP that changed, or invalid routing",code:"523",docsUrl:o,family:"5xx",fix:"verify the DNS A/AAAA record points at a reachable origin IP and that no upstream network is blocking Cloudflare",summary:"Cloudflare could not reach your origin server.",title:"Origin is unreachable"},{causes:"Cloudflare made a TCP connection but the origin did not return an HTTP response within 100 seconds — a slow handler or long-running request",code:"524",docsUrl:o,family:"5xx",fix:"speed up the slow origin handler, or move long work off the request path into a background job (a Durable Object, Queue, or Workflow)",summary:"Cloudflare connected to your origin but it did not respond in time.",title:"A timeout occurred"},{causes:"the TLS handshake between Cloudflare and the origin failed — a missing/invalid origin certificate, or a cipher/SNI mismatch under Full (Strict) SSL",code:"525",docsUrl:o,family:"5xx",fix:"install a valid certificate on the origin and align your Cloudflare SSL/TLS mode with the origin's certificate setup",summary:"The SSL handshake with your origin failed.",title:"SSL handshake failed"},{causes:"Cloudflare could not validate the origin's certificate under Full (Strict) SSL — it is expired, self-signed, or issued for the wrong hostname",code:"526",docsUrl:o,family:"5xx",fix:"install a valid, publicly-trusted certificate on the origin (or use a Cloudflare Origin CA cert), matching the request hostname",summary:"Cloudflare could not validate your origin's SSL certificate.",title:"Invalid SSL certificate"},{causes:"a DNS record points at a Cloudflare IP or another prohibited address instead of your real origin",code:"1000",docsUrl:a,family:"1xxx",fix:"point the DNS record at your real origin IP, not a Cloudflare-owned or loopback address",summary:"DNS points to a prohibited IP.",title:"DNS points to prohibited IP"},{causes:"Cloudflare could not resolve the requested hostname — a Worker fetched an unresolvable host, or a DNS record is misconfigured",code:"1001",docsUrl:a,family:"1xxx",fix:"check the hostname you're requesting and the DNS records for the zone resolve to a valid origin",summary:"Cloudflare could not resolve the origin DNS.",title:"DNS resolution error"},{causes:"a Worker or DNS record targets a restricted IP (for example a Cloudflare-owned or loopback address)",code:"1002",docsUrl:a,family:"1xxx",fix:"change the Worker fetch target or DNS record to a valid, non-restricted origin address",summary:"DNS points to a prohibited IP (Worker/restricted).",title:"DNS points to a prohibited IP"},{causes:"the visitor's IP was blocked by an IP Access Rule, WAF rule, or security configuration on the zone",code:"1006",docsUrl:a,family:"1xxx",fix:"review the zone's Firewall/WAF and IP Access Rules to see why the address was banned, and adjust if it was blocked in error",summary:"Access denied: the visitor's IP has been banned.",title:"Access denied: your IP has been banned"},{causes:"your Worker threw an unhandled JavaScript exception during the request",code:"1101",docsUrl:a,family:"1xxx",fix:"reproduce with `wrangler tail` (or the Workers Logs / Studio Logs panel) to get the stack trace, then handle the throwing code path",summary:"A Worker threw a JavaScript exception.",title:"Worker threw a JavaScript exception"},{causes:"the Worker used more CPU time than a single invocation is allowed — usually a hot loop or heavy synchronous work",code:"1102",docsUrl:a,family:"1xxx",fix:"reduce per-request CPU (optimize hot loops, avoid heavy synchronous work) or offload the heavy work to a Durable Object, Queue, or Workflow",summary:"A Worker exceeded its CPU-time resource limit.",title:"Worker exceeded resource limits"}],l=e=>e!==void 0&&e>="0"&&e<="9",m=(e,t)=>{for(let r=e.indexOf(t);r!==-1;r=e.indexOf(t,r+t.length))if(!l(e[r-1])&&!l(e[r+t.length]))return!0;return!1},g=(e,t)=>{for(const r of[`error ${t}`,`error: ${t}`])for(let i=e.indexOf(r);i!==-1;i=e.indexOf(r,i+r.length))if(!l(e[i+r.length]))return!0;return!1},p=/error|cloudflare/iu,c=e=>({body:[e.summary,"",`**Likely cause:** ${e.causes}.`,"",`**Fix:** ${e.fix}.`,"",`See [Cloudflare's ${e.family} error docs](${e.docsUrl}).`].join(`
11
+ `),header:`Cloudflare Error ${e.code}: ${e.title}`,id:`cloudflare-error-${e.code}`}),y=e=>{if(!p.test(e))return;const t=e.toLowerCase(),r=t.includes("error"),i=t.includes("cloudflare");if(r){for(const n of u)if(g(t,n.code))return c(n)}if(i){for(const n of u)if(m(t,n.code))return c(n)}},x=e=>(typeof e=="string"?e:e.join(`
12
+ `)).split(`
13
+ `).filter(t=>!t.startsWith("```")).join(`
14
+ `).replaceAll(/\*\*(.+?)\*\*/gu,"$1").replaceAll(/`([^`]+)`/gu,"$1"),d=e=>{for(const t of f)if(t.test(e))return{body:t.body,header:t.header,id:t.id}},S=e=>d(e)??y(e),w=e=>{if(typeof e=="string")return d(e)?.body;if(e.hint!==void 0)return e.hint;if(e.code!==void 0){const t=h(e.code);if(t?.hint!==void 0)return t.hint}return e.message===void 0?void 0:d(e.message)?.body};export{u as CLOUDFLARE_PLATFORM_ERRORS,s as ERROR_CATALOG,f as MESSAGE_SOLUTIONS,y as findCloudflarePlatformSolution,S as findIssueSolution,d as findSolutionByMessage,x as flattenHint,h as getCatalogEntry,b as isInternalCode,w as resolveHint};
@@ -0,0 +1 @@
1
+ import{getCatalogEntry as i}from"./CLOUDFLARE_PLATFORM_ERRORS-AwchC1Yo.mjs";class c extends Error{type="VisulimaError";hint;title;loc;code;status;docsUrl;data;constructor(o,a,t={}){const s=i(o);super(a??o,t.cause===void 0?void 0:{cause:t.cause}),this.name=t.name??"LunoraError",this.hint=t.hint??s?.hint,this.title=t.title??s?.title,this.loc=t.location,this.code=o,this.status=t.status??s?.status??500,this.docsUrl=t.docsUrl??s?.docsUrl,this.data=t.data}}export{c as LunoraError};
@@ -0,0 +1 @@
1
+ import{LunoraError as n}from"./LunoraError-Bi0eJQ64.mjs";const t=(r,a)=>{if(!r)throw new n("INTERNAL",a,{name:"InvariantError"})},e=r=>{throw new n("INTERNAL",r,{name:"InvariantError"})};export{t as invariant,e as unreachable};
@@ -0,0 +1 @@
1
+ const o=t=>{if(!(t instanceof Error))return!1;const r=t;return r.type==="VisulimaError"&&typeof r.code=="string"&&typeof r.status=="number"};export{o as isLunoraError};
@@ -0,0 +1 @@
1
+ import{isInternalCode as a,resolveHint as r}from"./CLOUDFLARE_PLATFORM_ERRORS-AwchC1Yo.mjs";import{isLunoraError as c}from"./isLunoraError-Dvew97xn.mjs";const u=(o,d={})=>{const s=d.redactedMessage??"Internal error";if(c(o)){if(a(o.code))return{body:{code:o.code,message:s},redacted:!0,status:o.status};const e={code:o.code,message:o.message};o.data!==void 0&&d.encodeData!==void 0&&(e.data=d.encodeData(o.data));const t=r({code:o.code,hint:o.hint,message:o.message});return t!==void 0&&(e.hint=t),o.docsUrl!==void 0&&(e.docsUrl=o.docsUrl),{body:e,redacted:!1,status:o.status}}return{body:{code:d.fallbackCode??"INTERNAL",message:s},redacted:!0,status:500}};export{u as toErrorBody};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lunora/errors",
3
- "version": "1.0.0-alpha.1",
3
+ "version": "1.0.0-alpha.10",
4
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
6
  "cloudflare",
@@ -1,207 +0,0 @@
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 };
@@ -1,38 +0,0 @@
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 };
@@ -1,12 +0,0 @@
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 };
@@ -1,9 +0,0 @@
1
- const isLunoraError = (error) => {
2
- if (!(error instanceof Error)) {
3
- return false;
4
- }
5
- const candidate = error;
6
- return typeof candidate.code === "string" && typeof candidate.status === "number";
7
- };
8
-
9
- export { isLunoraError };
@@ -1,26 +0,0 @@
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 };