@zerotal/arch 1.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (160) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/LICENSE +21 -0
  3. package/README.md +67 -0
  4. package/api-surface.md +283 -0
  5. package/docs/about.md +676 -0
  6. package/docs/admin/actions.md +183 -0
  7. package/docs/admin/auth.md +89 -0
  8. package/docs/admin/dashboard.md +155 -0
  9. package/docs/admin/extending-ui.md +229 -0
  10. package/docs/admin/extending.md +205 -0
  11. package/docs/admin/forms.md +155 -0
  12. package/docs/admin/index.md +144 -0
  13. package/docs/admin/operations.md +224 -0
  14. package/docs/admin/references.md +174 -0
  15. package/docs/admin/resources.md +132 -0
  16. package/docs/admin/structure.md +158 -0
  17. package/docs/admin/tables.md +185 -0
  18. package/docs/admin/testing.md +138 -0
  19. package/docs/ai.md +373 -0
  20. package/docs/application.md +435 -0
  21. package/docs/arch.md +263 -0
  22. package/docs/assets.md +755 -0
  23. package/docs/audit.md +367 -0
  24. package/docs/authentication.md +1000 -0
  25. package/docs/authorization.md +529 -0
  26. package/docs/broadcasting/channels.md +170 -0
  27. package/docs/broadcasting/client.md +108 -0
  28. package/docs/broadcasting/events.md +190 -0
  29. package/docs/broadcasting/index.md +163 -0
  30. package/docs/broadcasting/references.md +84 -0
  31. package/docs/broadcasting/testing.md +132 -0
  32. package/docs/cache.md +387 -0
  33. package/docs/carbon.md +830 -0
  34. package/docs/changelog.md +465 -0
  35. package/docs/client/auth.md +113 -0
  36. package/docs/client/errors.md +139 -0
  37. package/docs/client/files.md +118 -0
  38. package/docs/client/index.md +128 -0
  39. package/docs/client/references.md +58 -0
  40. package/docs/client/requests.md +131 -0
  41. package/docs/client/resilience.md +141 -0
  42. package/docs/client/testing.md +146 -0
  43. package/docs/commands.md +450 -0
  44. package/docs/components.md +2427 -0
  45. package/docs/config-system.md +258 -0
  46. package/docs/container.md +596 -0
  47. package/docs/context.md +579 -0
  48. package/docs/contributing.md +164 -0
  49. package/docs/controllers.md +354 -0
  50. package/docs/conventions.md +466 -0
  51. package/docs/cookies.md +101 -0
  52. package/docs/csrf.md +206 -0
  53. package/docs/database.md +476 -0
  54. package/docs/deployment.md +434 -0
  55. package/docs/devtools.md +1013 -0
  56. package/docs/email-verification.md +320 -0
  57. package/docs/encryption.md +380 -0
  58. package/docs/errors.md +317 -0
  59. package/docs/events.md +420 -0
  60. package/docs/flow/components.md +846 -0
  61. package/docs/flow/decorators.md +458 -0
  62. package/docs/flow/events.md +420 -0
  63. package/docs/flow/forms.md +551 -0
  64. package/docs/flow/index.md +568 -0
  65. package/docs/flow/layouts.md +793 -0
  66. package/docs/flow/lifecycle.md +336 -0
  67. package/docs/flow/pagination.md +402 -0
  68. package/docs/flow/performance.md +262 -0
  69. package/docs/flow/references.md +317 -0
  70. package/docs/flow/routing.md +550 -0
  71. package/docs/flow/testing.md +447 -0
  72. package/docs/getting-started.md +286 -0
  73. package/docs/health.md +269 -0
  74. package/docs/helpers.md +423 -0
  75. package/docs/i18n.md +414 -0
  76. package/docs/index.md +65 -0
  77. package/docs/inertia/build.md +95 -0
  78. package/docs/inertia/devtools.md +197 -0
  79. package/docs/inertia/index.md +267 -0
  80. package/docs/inertia/middleware.md +81 -0
  81. package/docs/inertia/props.md +507 -0
  82. package/docs/inertia/references.md +144 -0
  83. package/docs/inertia/rendering.md +189 -0
  84. package/docs/inertia/ssr.md +102 -0
  85. package/docs/inspirations.md +257 -0
  86. package/docs/lifecycle.md +331 -0
  87. package/docs/lock.md +454 -0
  88. package/docs/logger.md +517 -0
  89. package/docs/media.md +582 -0
  90. package/docs/middleware.md +509 -0
  91. package/docs/migrations.md +529 -0
  92. package/docs/monitor.md +377 -0
  93. package/docs/notifications.md +848 -0
  94. package/docs/orm/casts.md +365 -0
  95. package/docs/orm/factories.md +231 -0
  96. package/docs/orm/index.md +663 -0
  97. package/docs/orm/lifecycle.md +421 -0
  98. package/docs/orm/queries.md +692 -0
  99. package/docs/orm/relationships.md +440 -0
  100. package/docs/orm/serialization.md +251 -0
  101. package/docs/package-development.md +536 -0
  102. package/docs/pagination.md +340 -0
  103. package/docs/password-reset.md +335 -0
  104. package/docs/providers.md +447 -0
  105. package/docs/query-builder.md +465 -0
  106. package/docs/queue.md +549 -0
  107. package/docs/rate-limiting.md +253 -0
  108. package/docs/responses.md +336 -0
  109. package/docs/roles-and-2fa.md +418 -0
  110. package/docs/routing.md +1008 -0
  111. package/docs/scaffolding.md +335 -0
  112. package/docs/scheduler.md +533 -0
  113. package/docs/seeding.md +229 -0
  114. package/docs/session.md +451 -0
  115. package/docs/social.md +524 -0
  116. package/docs/storage.md +622 -0
  117. package/docs/structure.md +230 -0
  118. package/docs/support-policy.md +114 -0
  119. package/docs/telemetry.md +505 -0
  120. package/docs/tenancy.md +608 -0
  121. package/docs/testing/browser.md +153 -0
  122. package/docs/testing/console.md +130 -0
  123. package/docs/testing/database.md +238 -0
  124. package/docs/testing/flow-browser.md +216 -0
  125. package/docs/testing/http.md +441 -0
  126. package/docs/testing/index.md +240 -0
  127. package/docs/testing/mocking.md +410 -0
  128. package/docs/upgrade.md +192 -0
  129. package/docs/validator.md +549 -0
  130. package/docs/view.md +453 -0
  131. package/package.json +60 -0
  132. package/src/bin/mcp.ts +73 -0
  133. package/src/config.ts +63 -0
  134. package/src/errors.ts +27 -0
  135. package/src/index.ts +56 -0
  136. package/src/install/ArchInstallCommand.ts +189 -0
  137. package/src/install/detect.ts +98 -0
  138. package/src/install/guidelines.ts +290 -0
  139. package/src/install/markers.ts +77 -0
  140. package/src/install/mcpConfig.ts +81 -0
  141. package/src/mcp/index.ts +27 -0
  142. package/src/mcp/jsonrpc.ts +133 -0
  143. package/src/mcp/server.ts +307 -0
  144. package/src/mcp/stdio.ts +111 -0
  145. package/src/mcp/types.ts +146 -0
  146. package/src/probe/ArchProbeCommand.ts +48 -0
  147. package/src/probe/sentinel.ts +13 -0
  148. package/src/probe/topics.ts +291 -0
  149. package/src/provider/ArchProvider.ts +101 -0
  150. package/src/tools/_probe.ts +164 -0
  151. package/src/tools/apiSurface.ts +241 -0
  152. package/src/tools/appInfo.ts +88 -0
  153. package/src/tools/baselines.ts +210 -0
  154. package/src/tools/context.ts +37 -0
  155. package/src/tools/doctor.ts +93 -0
  156. package/src/tools/index.ts +50 -0
  157. package/src/tools/logs.ts +265 -0
  158. package/src/tools/routes.ts +133 -0
  159. package/src/tools/schema.ts +119 -0
  160. package/src/tools/searchDocs.ts +345 -0
package/docs/errors.md ADDED
@@ -0,0 +1,317 @@
1
+ ---
2
+ title: Error Handling
3
+ description: Turn any unhandled exception into the right HTTP response for the client and environment.
4
+ ---
5
+
6
+ # Error Handling
7
+
8
+ Zerotal centralises error handling through an `ExceptionHandler` class: every
9
+ unhandled exception flows through it, and the framework picks the response format
10
+ automatically based on the client type (browser vs. API) and the environment
11
+ (dev vs. production).
12
+
13
+ ## Default behaviour
14
+
15
+ In development, an unhandled error shows a styled stack-trace page in the browser
16
+ and a JSON body for API clients. In production, the browser sees a plain
17
+ "Internal Server Error" page and API clients get `{ message: 'Internal Server Error' }`
18
+ — no internals are leaked.
19
+
20
+ HTTP errors (`NotFoundError`, `ForbiddenError`, etc.) always produce a
21
+ status-code-appropriate response regardless of environment.
22
+
23
+ > **Note** — The client type is decided by the `Accept` header. A request whose
24
+ > `Accept` starts with `application/json` gets JSON; anything that includes
25
+ > `text/html` (regular browsers and Inertia XHR) gets the HTML page.
26
+
27
+ ## Built-in HTTP errors
28
+
29
+ Throw any of these from a controller, middleware, or model and the framework
30
+ turns it into the correct HTTP response automatically. Each carries a default
31
+ message and a stable `code`:
32
+
33
+ ```typescript
34
+ // in a controller
35
+ import { NotFoundError, ForbiddenError, UnauthorizedError } from "zerotal";
36
+
37
+ // 404
38
+ throw new NotFoundError();
39
+ throw new NotFoundError("Article not found");
40
+
41
+ // 403
42
+ throw new ForbiddenError();
43
+ throw new ForbiddenError("You cannot edit this post");
44
+
45
+ // 401
46
+ throw new UnauthorizedError();
47
+ ```
48
+
49
+ For any other status, throw `HttpError` directly:
50
+
51
+ ```typescript
52
+ // in a controller
53
+ import { HttpError } from "zerotal";
54
+
55
+ throw new HttpError("Payment required", 402, "E_PAYMENT_REQUIRED");
56
+ ```
57
+
58
+ The constructor is `new HttpError(message, status, code?, headers?)` — when you
59
+ omit `code` it defaults to `E_HTTP_<status>`.
60
+
61
+ The full set exported from `zerotal`:
62
+
63
+ | Class | Status | Default `code` |
64
+ | -------------------------- | ------ | ------------------------ |
65
+ | `BadRequestError` | 400 | `E_BAD_REQUEST` |
66
+ | `UnauthorizedError` | 401 | `E_UNAUTHORIZED` |
67
+ | `ForbiddenError` | 403 | `E_FORBIDDEN` |
68
+ | `NotFoundError` | 404 | `E_NOT_FOUND` |
69
+ | `MethodNotAllowedError` | 405 | `E_METHOD_NOT_ALLOWED` |
70
+ | `ConflictError` | 409 | `E_CONFLICT` |
71
+ | `GoneError` | 410 | `E_GONE` |
72
+ | `UnprocessableEntityError` | 422 | `E_UNPROCESSABLE_ENTITY` |
73
+ | `TooManyRequestsError` | 429 | `E_TOO_MANY_REQUESTS` |
74
+ | `ServiceUnavailableError` | 503 | `E_SERVICE_UNAVAILABLE` |
75
+
76
+ > **Tip** — `MethodNotAllowedError(allowed)`, `TooManyRequestsError(retryAfter)`,
77
+ > and `ServiceUnavailableError(reason, retryAfter)` populate the `Allow` and
78
+ > `Retry-After` response headers for you when you pass those arguments.
79
+
80
+ ## Custom exception classes
81
+
82
+ Extend `ZerotalError` for domain errors, or `HttpError` for HTTP errors. The
83
+ `ZerotalError` constructor is `(message, code, status = 500, context?)`:
84
+
85
+ ```typescript
86
+ // app/exceptions/PaymentFailedError.ts
87
+ import { ZerotalError, HttpError } from "zerotal";
88
+
89
+ // Domain error — pass status 402 as the third argument
90
+ export class PaymentFailedError extends ZerotalError {
91
+ constructor(public readonly chargeId: string) {
92
+ super(`Payment failed for charge ${chargeId}`, "E_PAYMENT_FAILED", 402);
93
+ }
94
+ }
95
+
96
+ // HTTP shortcut — HttpError takes (message, status, code?)
97
+ export class QuotaExceededError extends HttpError {
98
+ constructor() {
99
+ super("Quota exceeded", 429, "E_QUOTA_EXCEEDED");
100
+ }
101
+ }
102
+ ```
103
+
104
+ ```typescript
105
+ // in a controller
106
+ throw new PaymentFailedError(charge.id);
107
+ ```
108
+
109
+ ## Custom exception handler
110
+
111
+ Create a handler in `app/exceptions/Handler.ts` extending `ExceptionHandler`,
112
+ then register it in `bootstrap/app.ts` with `app.withExceptionHandler(Handler)`.
113
+ The framework calls `report()` first, then `render()`, for every unhandled
114
+ exception:
115
+
116
+ ```typescript
117
+ // app/exceptions/Handler.ts
118
+ import { ExceptionHandler, NotFoundError, ForbiddenError } from "zerotal";
119
+ import type { HttpContext } from "zerotal";
120
+ import { PaymentFailedError } from "./PaymentFailedError.ts";
121
+
122
+ export class Handler extends ExceptionHandler {
123
+ // report() runs first — use it to log to Sentry, Datadog, etc.
124
+ override async report(err: unknown, ctx?: HttpContext): Promise<void> {
125
+ if (err instanceof NotFoundError) return; // skip expected errors
126
+ if (err instanceof ForbiddenError) return;
127
+
128
+ await Sentry.captureException(err, { extra: { path: ctx?.path() } });
129
+
130
+ // Call super to keep the default console logging.
131
+ await super.report(err, ctx);
132
+ }
133
+
134
+ // render() turns an error into a Response.
135
+ override async render(err: unknown, ctx: HttpContext): Promise<Response> {
136
+ if (err instanceof PaymentFailedError) {
137
+ if (ctx.wantsJson()) {
138
+ return Response.json({ message: err.message, chargeId: err.chargeId }, { status: 402 });
139
+ }
140
+ ctx.flash("error", "Your payment could not be processed.");
141
+ return new Response(null, { status: 303, headers: { Location: "/billing" } });
142
+ }
143
+
144
+ // Fall back to the framework default for everything you don't handle.
145
+ return super.render(err, ctx);
146
+ }
147
+ }
148
+ ```
149
+
150
+ ```typescript
151
+ // bootstrap/app.ts
152
+ import { Application } from "zerotal";
153
+ import providers from "./providers.ts";
154
+ import { Handler } from "../app/exceptions/Handler.ts";
155
+
156
+ export default Application.create({ providers }).withExceptionHandler(Handler);
157
+ ```
158
+
159
+ > **Warning** — Always call `super.render(err, ctx)` as the final fallback. If you
160
+ > forget it, any error your `render()` doesn't explicitly handle falls through
161
+ > without a response.
162
+
163
+ ## Silencing expected errors
164
+
165
+ `report()` is for errors you want tracked externally. Errors that are normal user
166
+ behaviour (404, 403, validation) should not page anyone. The base handler already
167
+ silences a few by name — `ValidationRedirectError`, `ValidationJsonError`,
168
+ `ValidationError`, `PrecognitionResponse`, and `NotFoundError` — so they never
169
+ reach `console.error`.
170
+
171
+ To silence your own classes, list them on the `dontReport` array instead of
172
+ filtering by hand:
173
+
174
+ ```typescript
175
+ // app/exceptions/Handler.ts
176
+ import { ExceptionHandler, ForbiddenError } from "zerotal";
177
+ import { PaymentFailedError } from "./PaymentFailedError.ts";
178
+
179
+ export class Handler extends ExceptionHandler {
180
+ protected override dontReport = [ForbiddenError, PaymentFailedError];
181
+ }
182
+ ```
183
+
184
+ > **Tip** — Prefer `dontReport` over a manual `instanceof` chain in `report()`.
185
+ > The base `report()` checks it for you, so any error whose class (or class name)
186
+ > is in the silent list is skipped before logging.
187
+
188
+ ## Response format by client type
189
+
190
+ The base handler negotiates the response from the `Accept` header and the
191
+ environment:
192
+
193
+ | Situation | Browser | API client (`Accept: application/json`) |
194
+ | ---------------------- | ----------------------------- | ----------------------------------------------------- |
195
+ | `NotFoundError` | Styled 404 HTML page | `{ message: 'Not Found', code: 'E_NOT_FOUND' }` |
196
+ | `ForbiddenError` | Styled 403 HTML page | `{ message: 'Forbidden', code: 'E_FORBIDDEN' }` |
197
+ | Unhandled error (dev) | Full stack-trace page | `{ message: '...', code: 'E_INTERNAL' }` |
198
+ | Unhandled error (prod) | "Internal Server Error" page | `{ message: 'Internal Server Error' }` |
199
+ | `ValidationError` | 303 redirect + flashed errors | `{ message: 'Validation failed', errors: {...} }` 422 |
200
+
201
+ ## Validation errors
202
+
203
+ The validation system throws automatically — you don't catch anything. It throws
204
+ `ValidationRedirectError` (a 303 redirect for browser clients) or
205
+ `ValidationJsonError` (a 422 JSON body for API clients), and both flow straight
206
+ through the handler:
207
+
208
+ ```typescript
209
+ // in a controller
210
+ // Browser → 303 redirect back with flashed errors
211
+ // API → 422 JSON: { message: 'Validation failed', errors: {...} }
212
+ const data = await StorePostRequest.validate();
213
+ ```
214
+
215
+ See the [Validator](/docs/validator) guide for details.
216
+
217
+ ## Error codes
218
+
219
+ Every `ZerotalError` carries a machine-readable `code` string alongside the HTTP
220
+ `status`. Use it on the client to distinguish errors that share a status:
221
+
222
+ ```typescript
223
+ // the API client receives:
224
+ // { "message": "Payment failed for charge ch_xxx", "code": "E_PAYMENT_FAILED" }
225
+
226
+ if (error.code === "E_PAYMENT_FAILED") {
227
+ showPaymentFailedUI(error.chargeId);
228
+ }
229
+ ```
230
+
231
+ ## afterResponse and errors
232
+
233
+ Errors thrown inside `ctx.afterResponse()` callbacks are logged but never sent to
234
+ the client — the response has already been delivered by the time these fire, so a
235
+ failing callback can't crash the request.
236
+
237
+ ## The development error page, and diagnoses
238
+
239
+ Outside production, an unhandled error renders a full-page overlay: the message, the stack split into application and framework frames, source context around the failing line, the request, and a **Copy for AI** button that puts the whole thing on the clipboard as Markdown.
240
+
241
+ Some errors are exact about _what_ happened and useless about _what to do_. `no such table: assets` is the canonical one — the message is precise, and every frame in the stack is inside the SQL driver, because that is where the failure surfaced rather than where it came from. The answer is somewhere else entirely: you have migrations you have not run.
242
+
243
+ So a package that owns an error class can contribute a **diagnosis**, rendered above the stack.
244
+
245
+ ### Missing tables and columns
246
+
247
+ `@zerotal/orm` registers one. When a query fails because a table or column does not exist, the overlay checks the migration state and answers one of two ways:
248
+
249
+ - **Migrations are pending** — it names them and offers a **Run migrations** button. Pressing it runs exactly what `bun zt migrate` would, then reloads the page.
250
+ - **Nothing is pending** — it offers **no button**, because running every pending migration when there are none changes nothing and leaves you back where you started. Instead it says whether any migration on disk so much as mentions the missing name: if none does, the migration that would create it was probably never written.
251
+
252
+ It works on SQLite, PostgreSQL and MySQL — matched on the driver's error code where there is one (`42P01`, `42703`, `1146`, `1054`) and on the message otherwise.
253
+
254
+ > **The button exists only in development.** The endpoint behind it refuses unless [`devSurfacesEnabled()`](/docs/deployment) is true, and that gate **fails closed**: an unset `APP_ENV` does not qualify. The route is not even registered otherwise.
255
+ >
256
+ > It also requires a single-use token minted into the page, and passes the same origin check the WebSocket endpoints use. A dev server on `localhost:3000` is reachable by any site you have open in another tab, and "run every pending migration" is not something a random page should be able to trigger.
257
+
258
+ ### Writing your own
259
+
260
+ `registerErrorDiagnoser` takes a function that either recognises an error or returns `null`. Register it from a provider's `onRegister()`:
261
+
262
+ ```typescript
263
+ import { registerErrorDiagnoser } from "zerotal";
264
+
265
+ registerErrorDiagnoser((error) => {
266
+ if (!/ECONNREFUSED .*:6379/.test(error.message)) return null;
267
+ return {
268
+ title: "Redis refused the connection.",
269
+ detail:
270
+ "The cache and queue drivers are both configured to use it. Start it with `docker compose up redis`, or switch `cache.driver` to `memory` while you work.",
271
+ items: ["cache.driver = redis", "queue.driver = redis"],
272
+ };
273
+ });
274
+ ```
275
+
276
+ The function is an `ErrorDiagnoser`, and what it returns is an `ErrorDiagnosis`.
277
+
278
+ Diagnosers run in registration order and the **first** one to return a result wins, so recognise only errors you genuinely own — a diagnoser that claims an error it cannot explain replaces a real stack trace with a wrong answer. One that throws is skipped rather than taking the error page down with it.
279
+
280
+ | Field | Meaning |
281
+ | -------- | ------------------------------------------------------------------- |
282
+ | `title` | One line: what is actually wrong. |
283
+ | `detail` | A short paragraph: why, and what to do. |
284
+ | `items` | Supporting specifics — file names, config keys. Rendered as a list. |
285
+ | `action` | A button. See below. |
286
+
287
+ An `action` is a `DiagnosisAction`: a `label`, a same-origin `url` to `POST` to, a `token`, and an optional `pendingLabel`.
288
+
289
+ > **An `action` changes server state from a page rendered by a GET.** Whoever registers the endpoint owns its safety, and the type only carries the values to the page. The endpoint must refuse outside development _on its own terms_ rather than trusting that the overlay is dev-only, require the token, and check the origin. If you cannot do all three, ship the diagnosis without a button — a `title` and `detail` that name the fix are most of the value.
290
+
291
+ Offer a button only when you are confident it helps. A button that runs and changes nothing is worse than no button, because it teaches people not to trust the panel.
292
+
293
+ ## References
294
+
295
+ `ExceptionHandler` is the class you subclass; the named errors are exported from
296
+ `zerotal`.
297
+
298
+ | Member | Signature | Description |
299
+ | -------------- | ----------------------------------------------------------- | ---------------------------------------------------------------------------------- |
300
+ | `report` | `report(err: unknown, ctx?: HttpContext): Promise<void>` | Called first; log or forward the error. Override to add tracking. |
301
+ | `render` | `render(err: unknown, ctx: HttpContext): Promise<Response>` | Turns an error into a `Response`. Override per error class, call `super`. |
302
+ | `dontReport` | `protected dontReport: Array<new (...args) => unknown>` | Error classes to skip in `report()`. |
303
+ | `ZerotalError` | `new ZerotalError(message, code, status = 500, context?)` | Base framework error with a `code`, `status`, and optional `context`. |
304
+ | `HttpError` | `new HttpError(message, status, code?, headers?)` | Error mapped directly onto an HTTP response; `code` defaults to `E_HTTP_<status>`. |
305
+
306
+ | Method (on `HttpContext`) | Signature | Description |
307
+ | ------------------------- | ------------------------- | --------------------------------------------------------- |
308
+ | `wantsJson` | `wantsJson(): boolean` | `true` when the client expects a JSON response. |
309
+ | `path` | `path(): string` | The request path, handy for error context. |
310
+ | `flash` | `flash(key, value): void` | Stash data for the next request (e.g. before a redirect). |
311
+
312
+ ## Next steps
313
+
314
+ - [Validator](/docs/validator) — the validation errors that flow through the handler.
315
+ - [Logger](/docs/logger) — where `report()` writes by default.
316
+ - [HTTP context](/docs/context) — the `ctx` passed to `report()` and `render()`.
317
+ - [Telemetry](/docs/telemetry) — forward reported errors to an external tracker.