@zerotal/arch 1.7.3 → 1.7.5

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 (127) hide show
  1. package/CHANGELOG.md +34 -2
  2. package/README.md +4 -2
  3. package/api-surface.md +4 -4
  4. package/docs/about.md +16 -16
  5. package/docs/admin/actions.md +9 -9
  6. package/docs/admin/auth.md +2 -2
  7. package/docs/admin/dashboard.md +5 -5
  8. package/docs/admin/extending-ui.md +12 -12
  9. package/docs/admin/extending.md +16 -7
  10. package/docs/admin/forms.md +4 -4
  11. package/docs/admin/index.md +2 -2
  12. package/docs/admin/operations.md +9 -9
  13. package/docs/admin/resources.md +3 -3
  14. package/docs/admin/structure.md +6 -6
  15. package/docs/admin/tables.md +8 -8
  16. package/docs/admin/testing.md +4 -4
  17. package/docs/ai.md +118 -11
  18. package/docs/application.md +12 -12
  19. package/docs/arch.md +25 -3
  20. package/docs/assets.md +11 -11
  21. package/docs/audit.md +13 -13
  22. package/docs/authentication.md +39 -39
  23. package/docs/authorization.md +13 -13
  24. package/docs/broadcasting/channels.md +2 -2
  25. package/docs/broadcasting/client.md +2 -2
  26. package/docs/broadcasting/events.md +9 -10
  27. package/docs/broadcasting/index.md +1 -1
  28. package/docs/broadcasting/testing.md +5 -5
  29. package/docs/cache.md +15 -13
  30. package/docs/carbon.md +35 -35
  31. package/docs/changelog.md +165 -1
  32. package/docs/client/index.md +277 -70
  33. package/docs/commands.md +4 -4
  34. package/docs/components.md +116 -115
  35. package/docs/config-system.md +4 -4
  36. package/docs/container.md +27 -27
  37. package/docs/context.md +27 -27
  38. package/docs/contributing.md +46 -0
  39. package/docs/controllers.md +10 -10
  40. package/docs/conventions.md +23 -15
  41. package/docs/cookies.md +3 -3
  42. package/docs/csrf.md +6 -6
  43. package/docs/database.md +22 -17
  44. package/docs/deployment.md +2 -2
  45. package/docs/devtools.md +16 -16
  46. package/docs/email-verification.md +8 -8
  47. package/docs/encryption.md +7 -7
  48. package/docs/errors.md +6 -6
  49. package/docs/events.md +12 -11
  50. package/docs/flow/components.md +32 -32
  51. package/docs/flow/decorators.md +18 -18
  52. package/docs/flow/events.md +16 -16
  53. package/docs/flow/forms.md +17 -17
  54. package/docs/flow/icons.md +199 -0
  55. package/docs/flow/index.md +20 -20
  56. package/docs/flow/layouts.md +35 -35
  57. package/docs/flow/lifecycle.md +16 -16
  58. package/docs/flow/models.md +9 -9
  59. package/docs/flow/pagination.md +8 -8
  60. package/docs/flow/performance.md +7 -7
  61. package/docs/flow/references.md +3 -3
  62. package/docs/flow/routing.md +26 -26
  63. package/docs/flow/testing.md +20 -20
  64. package/docs/getting-started.md +18 -13
  65. package/docs/health.md +17 -8
  66. package/docs/helpers.md +17 -17
  67. package/docs/i18n.md +10 -10
  68. package/docs/inertia/devtools.md +4 -4
  69. package/docs/inertia/index.md +12 -7
  70. package/docs/inertia/props.md +22 -22
  71. package/docs/inertia/rendering.md +86 -7
  72. package/docs/inertia/ssr.md +2 -2
  73. package/docs/lifecycle.md +2 -2
  74. package/docs/lock.md +11 -11
  75. package/docs/logger.md +12 -12
  76. package/docs/media.md +22 -22
  77. package/docs/middleware.md +17 -17
  78. package/docs/migrations.md +13 -13
  79. package/docs/monitor.md +6 -6
  80. package/docs/notifications.md +24 -24
  81. package/docs/orm/casts.md +14 -14
  82. package/docs/orm/factories.md +8 -8
  83. package/docs/orm/index.md +22 -20
  84. package/docs/orm/lifecycle.md +13 -13
  85. package/docs/orm/queries.md +28 -28
  86. package/docs/orm/relationships.md +16 -16
  87. package/docs/orm/serialization.md +11 -11
  88. package/docs/package-development.md +10 -10
  89. package/docs/pagination.md +14 -14
  90. package/docs/password-reset.md +6 -6
  91. package/docs/providers.md +11 -11
  92. package/docs/query-builder.md +18 -18
  93. package/docs/queue.md +14 -12
  94. package/docs/rate-limiting.md +6 -6
  95. package/docs/responses.md +14 -14
  96. package/docs/roles-and-2fa.md +9 -9
  97. package/docs/routing.md +44 -44
  98. package/docs/scaffolding.md +1 -1
  99. package/docs/scheduler.md +10 -10
  100. package/docs/seeding.md +6 -6
  101. package/docs/session.md +16 -14
  102. package/docs/social.md +14 -14
  103. package/docs/storage.md +25 -13
  104. package/docs/structure.md +2 -2
  105. package/docs/support-policy.md +36 -8
  106. package/docs/telemetry.md +12 -12
  107. package/docs/tenancy.md +34 -40
  108. package/docs/testing/browser.md +6 -6
  109. package/docs/testing/console.md +3 -3
  110. package/docs/testing/database.md +10 -10
  111. package/docs/testing/flow-browser.md +6 -6
  112. package/docs/testing/http.md +12 -12
  113. package/docs/testing/index.md +3 -3
  114. package/docs/testing/mocking.md +8 -8
  115. package/docs/upgrade.md +1 -1
  116. package/docs/validator.md +19 -19
  117. package/docs/view.md +14 -14
  118. package/package.json +4 -4
  119. package/src/index.ts +25 -1
  120. package/src/tools/searchDocs.ts +7 -0
  121. package/docs/client/auth.md +0 -113
  122. package/docs/client/errors.md +0 -139
  123. package/docs/client/files.md +0 -118
  124. package/docs/client/references.md +0 -58
  125. package/docs/client/requests.md +0 -131
  126. package/docs/client/resilience.md +0 -141
  127. package/docs/client/testing.md +0 -146
@@ -42,7 +42,7 @@ app/providers/* → register → boot phases → app/middleware/* →
42
42
 
43
43
  Every `Model` subclass under `app/models/` is registered automatically:
44
44
 
45
- ```typescript
45
+ ```typescript fragment
46
46
  // app/models/User.ts
47
47
  import { Model, column, hasMany } from "@zerotal/orm";
48
48
  import { Post } from "./Post.ts";
@@ -65,7 +65,7 @@ No `@table` needed. The table name is derived by convention — `pluralize(snake
65
65
 
66
66
  Override the name (or set timestamps/soft-deletes) with `@table` whenever you need to:
67
67
 
68
- ```typescript
68
+ ```typescript fragment
69
69
  // app/models/Account.ts
70
70
  @table("legacy_accounts", { softDeletes: true })
71
71
  export class Account extends Model {
@@ -86,7 +86,7 @@ export class Account extends Model {
86
86
  `XObserver` is attached to model `X` automatically (the `Observer` suffix is stripped and matched
87
87
  against the discovered models):
88
88
 
89
- ```typescript
89
+ ```typescript fragment
90
90
  // app/observers/UserObserver.ts
91
91
  import type { ModelObserver } from "@zerotal/orm";
92
92
  import type { User } from "../models/User.ts";
@@ -107,7 +107,7 @@ Override the target with `static model = SomeModel` when the name doesn't match.
107
107
 
108
108
  `XPolicy` is registered with the Gate for model `X`:
109
109
 
110
- ```typescript
110
+ ```typescript fragment
111
111
  // app/policies/PostPolicy.ts
112
112
  import { Policy } from "@zerotal/auth";
113
113
  import type { Post } from "../models/Post.ts";
@@ -130,14 +130,14 @@ model's class automatically. Override the target with `static model = Post`.
130
130
 
131
131
  A listener declares the event(s) it handles via `static listens`:
132
132
 
133
- ```typescript
133
+ ```typescript fragment
134
134
  // app/events/UserRegistered.ts
135
135
  export class UserRegistered {
136
136
  constructor(public user: User) {}
137
137
  }
138
138
  ```
139
139
 
140
- ```typescript
140
+ ```typescript fragment
141
141
  // app/listeners/SendWelcomeEmail.ts
142
142
  import { UserRegistered } from "../events/UserRegistered.ts";
143
143
 
@@ -158,7 +158,7 @@ them and runs any module-level side effects before listeners bind).
158
158
  A model can map its lifecycle events to event classes, which are dispatched on the bus when they
159
159
  fire — listeners then react with no coupling to the model:
160
160
 
161
- ```typescript
161
+ ```typescript fragment
162
162
  // app/models/Order.ts
163
163
  export class Order extends Model {
164
164
  static dispatchesEvents = { created: OrderPlaced, deleted: OrderCancelled };
@@ -181,10 +181,18 @@ Any `ServiceProvider` under `app/providers/` is registered automatically — you
181
181
  (`onRegister` → `onBooting` → `onBooted`), appended **after** the providers you registered
182
182
  explicitly (so framework providers boot first), and de-duplicated if also listed by hand.
183
183
 
184
- ```typescript
184
+ ```typescript fragment
185
185
  // app/providers/AppServiceProvider.ts
186
186
  import { ServiceProvider } from "zerotal";
187
187
 
188
+ // A string token is only valid once it is declared, which is what keeps
189
+ // `make("billing")` typed at the call site. See Container for the full pattern.
190
+ declare module "zerotal" {
191
+ interface ContainerBindings {
192
+ billing: Billing;
193
+ }
194
+ }
195
+
188
196
  export class AppServiceProvider extends ServiceProvider {
189
197
  override onRegister() {
190
198
  this.app.container.singleton("billing", () => new Billing());
@@ -197,7 +205,7 @@ export class AppServiceProvider extends ServiceProvider {
197
205
  Each middleware class under `app/middleware/` is registered as a **named group** under its class
198
206
  name, so routes can reference it by string without importing it:
199
207
 
200
- ```typescript
208
+ ```typescript fragment
201
209
  // app/middleware/EnsureSubscribed.ts
202
210
  import { BaseMiddleware } from "zerotal";
203
211
 
@@ -208,7 +216,7 @@ export class EnsureSubscribed extends BaseMiddleware<{}> {
208
216
  }
209
217
  ```
210
218
 
211
- ```typescript
219
+ ```typescript fragment
212
220
  // routes/web.ts — reference by class name
213
221
  Router.group({ middleware: ["EnsureSubscribed"] }, () => {
214
222
  /* … */
@@ -218,7 +226,7 @@ Router.group({ middleware: ["EnsureSubscribed"] }, () => {
218
226
  Middleware is **not** applied globally by default. Opt a class into the global pipeline with
219
227
  `static global = true`:
220
228
 
221
- ```typescript
229
+ ```typescript fragment
222
230
  // app/middleware/RequestId.ts
223
231
  export class RequestId extends BaseMiddleware<{}> {
224
232
  static global = true; // runs on every request
@@ -281,7 +289,7 @@ export class NotifyFollowersJob extends Job {
281
289
 
282
290
  Dispatch it from anywhere without importing the class into a barrel:
283
291
 
284
- ```typescript
292
+ ```typescript fragment
285
293
  // in a controller
286
294
  await Queue.dispatch(new NotifyFollowersJob(post.id));
287
295
  ```
@@ -296,7 +304,7 @@ registered with the scheduler at boot. Each declares its cadence (a `cron` strin
296
304
  `frequency()` method) and its work (`handle()`); the loader translates the class's declarative
297
305
  settings into a scheduled task.
298
306
 
299
- ```typescript
307
+ ```typescript fragment
300
308
  // app/schedules/SendDailyReports.ts
301
309
  import { Schedule } from "@zerotal/scheduler";
302
310
 
@@ -342,7 +350,7 @@ import only runs because this concern imports it.
342
350
  Once models are registered at boot, Zerotal can sync the schema additively — create missing tables
343
351
  and add missing columns to match your models (TypeORM-style `synchronize`):
344
352
 
345
- ```typescript
353
+ ```typescript fragment
346
354
  // config/database.ts
347
355
  import { DatabaseConfig } from "@zerotal/orm";
348
356
 
@@ -413,7 +421,7 @@ let you relocate any concern. Files starting with `_` and `*.test.ts` / `*.test.
413
421
  Discovery is extensible. A provider can contribute its own concern descriptor via
414
422
  `app.registerConcern(...)`:
415
423
 
416
- ```typescript
424
+ ```typescript fragment
417
425
  // app/providers/AppServiceProvider.ts — inside onRegister/onBooting
418
426
  this.app.registerConcern({
419
427
  name: "validators",
package/docs/cookies.md CHANGED
@@ -28,7 +28,7 @@ When you need to read or set a raw cookie directly, you work with the standard
28
28
 
29
29
  Cookies arrive in the request's `Cookie` header. Read and parse it from the context:
30
30
 
31
- ```ts
31
+ ```ts fragment
32
32
  // in a controller
33
33
  import type { HttpContext } from "zerotal";
34
34
 
@@ -49,7 +49,7 @@ const theme = readCookie(ctx, "theme") ?? "light";
49
49
  Set the response with a `ctx` helper, then append a `Set-Cookie` header to it. Use
50
50
  `append` (not `set`) so multiple cookies can be sent in one response:
51
51
 
52
- ```ts
52
+ ```ts fragment
53
53
  // in a controller
54
54
  ctx.json({ ok: true }); // assigns ctx.response
55
55
 
@@ -75,7 +75,7 @@ ctx.response!.headers.append(
75
75
 
76
76
  To **delete** a cookie, set it again with `Max-Age=0`:
77
77
 
78
- ```ts
78
+ ```ts fragment
79
79
  // in a controller
80
80
  ctx.response!.headers.append("Set-Cookie", "theme=; Path=/; Max-Age=0");
81
81
  ```
package/docs/csrf.md CHANGED
@@ -55,7 +55,7 @@ Register `CsrfMiddleware` **after** `SessionMiddleware` — it reads and writes
55
55
  session. In production over HTTPS, set `secure: true` so the cookie carries the
56
56
  `Secure` flag:
57
57
 
58
- ```typescript
58
+ ```typescript fragment
59
59
  // bootstrap/app.ts
60
60
  import { SessionMiddleware, CsrfMiddleware } from "@zerotal/session";
61
61
 
@@ -89,7 +89,7 @@ on every mutating request.
89
89
  Expose the token server-side and send it in the header (or a hidden field your
90
90
  handler reads). `CsrfMiddleware.token()` returns the current session's token:
91
91
 
92
- ```typescript
92
+ ```typescript fragment
93
93
  // in an Inertia shared-props factory
94
94
  import { CsrfMiddleware } from "@zerotal/session";
95
95
 
@@ -99,7 +99,7 @@ Inertia.share({ csrf_token: () => CsrfMiddleware.token() });
99
99
  // <meta name="csrf-token" content="${CsrfMiddleware.token()}">
100
100
  ```
101
101
 
102
- ```typescript
102
+ ```typescript fragment
103
103
  // client-side fetch
104
104
  await fetch("/posts", {
105
105
  method: "POST",
@@ -140,7 +140,7 @@ is the rare feature whose test is mostly about proving a request **fails**.
140
140
  worth asserting by number — a `403` in this test means your authorization denied
141
141
  the request and CSRF never ran:
142
142
 
143
- ```typescript
143
+ ```typescript fragment
144
144
  // tests/http/csrf.test.ts
145
145
  import { test } from "bun:test";
146
146
  import { createApp } from "../helpers.ts";
@@ -159,7 +159,7 @@ test("an unsafe request without a token is refused", async () => {
159
159
  that is the only request in the test, the middleware could be absent entirely and
160
160
  the suite would stay green:
161
161
 
162
- ```typescript
162
+ ```typescript fragment
163
163
  // tests/http/csrf.test.ts
164
164
  test("GET is never challenged", async () => {
165
165
  (await app.get("/posts")).assertOk(); // passes with or without CSRF — not a CSRF test
@@ -169,7 +169,7 @@ test("GET is never challenged", async () => {
169
169
  To test the **success** path, seed the session with a token you choose and send
170
170
  the same value on the header the middleware reads:
171
171
 
172
- ```typescript
172
+ ```typescript fragment
173
173
  // tests/http/csrf.test.ts
174
174
  const token = "test-csrf-token";
175
175
 
package/docs/database.md CHANGED
@@ -56,7 +56,9 @@ import { env } from "zerotal";
56
56
  export default DatabaseConfig({
57
57
  driver: "postgres",
58
58
  url: env("DATABASE_URL", "./database/db.sqlite"),
59
- replicas: [env("REPLICA_1_URL"), env("REPLICA_2_URL")],
59
+ // `env()` with no fallback is `string | undefined`, and an unset replica should
60
+ // drop out rather than become an empty connection string.
61
+ replicas: [env("REPLICA_1_URL"), env("REPLICA_2_URL")].filter((url) => url !== undefined),
60
62
  });
61
63
  ```
62
64
 
@@ -98,7 +100,7 @@ Three styles, in order of preference:
98
100
 
99
101
  Pass a callback to `DB.transaction()`. Bun commits on resolve and rolls back on throw — you never call commit/rollback manually:
100
102
 
101
- ```typescript
103
+ ```typescript fragment
102
104
  // in a controller or service
103
105
  import { DB } from "@zerotal/orm";
104
106
 
@@ -120,7 +122,7 @@ await DB.transaction(async (trx) => {
120
122
 
121
123
  All `Model` and `DB` queries made inside the callback automatically use the transaction connection via `AsyncLocalStorage` — you don't need to pass `trx` explicitly unless you're mixing raw `DB.table()` calls with model calls:
122
124
 
123
- ```typescript
125
+ ```typescript fragment
124
126
  // in a service
125
127
  await DB.transaction(async () => {
126
128
  // These automatically use the transaction without passing trx:
@@ -136,7 +138,7 @@ await DB.transaction(async () => {
136
138
 
137
139
  Nested `DB.transaction()` calls automatically use SAVEPOINTs. An inner throw rolls back only the inner block, not the entire outer transaction:
138
140
 
139
- ```typescript
141
+ ```typescript fragment
140
142
  // in a service
141
143
  await DB.transaction(async () => {
142
144
  await Order.create({ userId, total });
@@ -159,7 +161,7 @@ await DB.transaction(async () => {
159
161
 
160
162
  Pass the number of attempts as a second argument to retry automatically on deadlock or serialization failures:
161
163
 
162
- ```typescript
164
+ ```typescript fragment
163
165
  // in a service
164
166
  await DB.transaction(async () => {
165
167
  // critical concurrent write
@@ -208,7 +210,7 @@ chain handles all four CRUD operations: call a read terminal like `get()` to fet
208
210
  rows, or a write method like `insert()`, `update()`, or `delete()` to change them.
209
211
  Values are always parameterised for you, so there's no injection risk:
210
212
 
211
- ```typescript
213
+ ```typescript fragment
212
214
  // in a controller or service
213
215
  import { DB } from "@zerotal/orm";
214
216
 
@@ -237,7 +239,7 @@ Use when the query builder doesn't cover what you need.
237
239
 
238
240
  > **Danger** — Always parameterise values; never interpolate them directly into the SQL string. String interpolation opens a SQL injection hole.
239
241
 
240
- ```typescript
242
+ ```typescript fragment
241
243
  // in a service
242
244
  import { DB } from "@zerotal/orm";
243
245
 
@@ -260,7 +262,7 @@ const [{ version }] = await DB.raw<{ version: string }>("SELECT version()");
260
262
 
261
263
  `whereJson` takes the column and a JSON path joined with `->`, then the value to match:
262
264
 
263
- ```typescript
265
+ ```typescript fragment
264
266
  // in a service
265
267
  // Equivalent SQL: WHERE meta->>'notifications.email' = ?
266
268
  await DB.table("settings").whereJson("meta->notifications.email", true).get();
@@ -279,8 +281,10 @@ import { DatabaseConfig } from "@zerotal/orm";
279
281
  import { env } from "zerotal";
280
282
 
281
283
  export default DatabaseConfig({
282
- url: env("DATABASE_URL"),
283
- replicas: [env("REPLICA_1_URL"), env("REPLICA_2_URL")],
284
+ url: env("DATABASE_URL", "./database/db.sqlite"),
285
+ // `env()` with no fallback is `string | undefined`, and an unset replica should
286
+ // drop out rather than become an empty connection string.
287
+ replicas: [env("REPLICA_1_URL"), env("REPLICA_2_URL")].filter((url) => url !== undefined),
284
288
  });
285
289
  ```
286
290
 
@@ -296,7 +300,7 @@ No model code changes needed. Routing is automatic:
296
300
 
297
301
  After a write, the replica may lag. Use `DB.onPrimary()` when you need to read the just-written data immediately:
298
302
 
299
- ```typescript
303
+ ```typescript fragment
300
304
  // in a controller
301
305
  const post = await Post.create({ title: "Hello", userId });
302
306
 
@@ -327,7 +331,7 @@ that did not exist. `NPlusOneError.distinctArgs` carries the count if you want t
327
331
  Tune the detector once at boot. Lower the `threshold` to catch leaks sooner, and set
328
332
  `mode: "throw"` in CI so an N+1 query fails the test suite instead of just logging:
329
333
 
330
- ```typescript
334
+ ```typescript fragment
331
335
  // bootstrap/app.ts — or a service provider
332
336
  DB.preventNPlusOne({
333
337
  threshold: 3, // warn after 3 repetitions instead of 5
@@ -341,7 +345,7 @@ Some repetition is intentional — a polling endpoint, an audit log — and you
341
345
  the detector crying wolf. Call `allowNPlusOne` to silence a specific table, either for
342
346
  good or just for the current request:
343
347
 
344
- ```typescript
348
+ ```typescript fragment
345
349
  // in a service provider or request handler
346
350
  // Suppress permanently for a table/pattern
347
351
  DB.allowNPlusOne("activity_logs");
@@ -373,7 +377,7 @@ With the connection registered, set `static connection` on any model that should
373
377
  there. From then on every query that model makes — reads, writes, pagination — is routed
374
378
  to that connection with no extra arguments:
375
379
 
376
- ```typescript
380
+ ```typescript fragment
377
381
  // app/models/AnalyticsEvent.ts
378
382
  export class AnalyticsEvent extends Model {
379
383
  static connection = "analytics";
@@ -400,7 +404,7 @@ Connection resolution priority (highest to lowest):
400
404
 
401
405
  Use advisory locks for application-level mutual exclusion — e.g. preventing two workers from processing the same job simultaneously:
402
406
 
403
- ```typescript
407
+ ```typescript fragment
404
408
  // in a job or worker
405
409
  import { DB } from "@zerotal/orm";
406
410
 
@@ -435,9 +439,10 @@ from a service provider:
435
439
  For example, subscribe to `QueryExecuted` in a provider to surface slow queries in your
436
440
  logs — the handler receives the SQL, its bindings, and how long it took:
437
441
 
438
- ```typescript
442
+ ```typescript fragment
439
443
  // in a service provider
440
- import { FrameworkEvents, QueryExecuted } from "zerotal";
444
+ import { FrameworkEvents } from "zerotal";
445
+ import { QueryExecuted } from "@zerotal/orm";
441
446
 
442
447
  // Log slow queries
443
448
  FrameworkEvents.on(QueryExecuted, (e) => {
@@ -228,7 +228,7 @@ which behind a proxy is the loopback address it bound to (`http://127.0.0.1:3002
228
228
  the public URL the browser sends. So the public origin has to come from config, and it
229
229
  does: `AppConfig()` fills `app.allowedOrigins` from `url`.
230
230
 
231
- ```ts
231
+ ```ts fragment
232
232
  // config/app.ts
233
233
  export default AppConfig({
234
234
  name: "My App",
@@ -240,7 +240,7 @@ Name additional origins only when a genuinely different host drives the app —
240
240
  `app.example.com` calling `api.example.com`. What you pass is added to the URL's origin
241
241
  rather than replacing it:
242
242
 
243
- ```ts
243
+ ```ts fragment
244
244
  // config/app.ts
245
245
  export default AppConfig({
246
246
  url: env("APP_URL"),
package/docs/devtools.md CHANGED
@@ -57,7 +57,7 @@ lifecycle order, all of them additionally short-circuited in production):
57
57
  > **Note** — Nothing happens at import time. The trace store opens its database
58
58
  > on first use rather than in a constructor, so importing this package in an app
59
59
  > that runs in production writes nothing and starts no timer. N+1 detection
60
- > itself is owned by the [ORM](/docs/orm/index) provider (and env-gated there);
60
+ > itself is owned by the [ORM](/docs/orm) provider (and env-gated there);
61
61
  > DevTools only consumes the `NPlusOneDetected` event and surfaces it.
62
62
 
63
63
  ## Start the client panel
@@ -100,7 +100,7 @@ after works the way you would expect.
100
100
 
101
101
  `DevTools.start()` accepts three optional fields:
102
102
 
103
- ```typescript
103
+ ```typescript fragment
104
104
  // resources/js/app.js
105
105
  DevTools.start({ endpoint: "/__zerotal/devtools", mode: "floating" }); // defaults
106
106
  ```
@@ -356,7 +356,7 @@ log line's, a stack frame. Going from "this query is slow" to the line that ran
356
356
  it is the most frequent move in a debugging session, and without this it is two
357
357
  manual searches.
358
358
 
359
- ```typescript
359
+ ```typescript fragment
360
360
  // config/devtools.ts
361
361
  export default DevtoolsConfig({
362
362
  editor: "cursor", // vscode | vscode-insiders | cursor | windsurf | zed | webstorm
@@ -369,7 +369,7 @@ Set `editor: null` to render locations as plain text instead.
369
369
  the one with your editor on it — a container reports `/app/src/Foo.ts` for a file
370
370
  that lives at `~/project/src/Foo.ts`. Map it home:
371
371
 
372
- ```typescript
372
+ ```typescript fragment
373
373
  export default DevtoolsConfig({
374
374
  editorPathMap: { "/app": "/Users/you/project" },
375
375
  });
@@ -425,7 +425,7 @@ things that look like secrets. `console.log(user)` is masked field by field;
425
425
 
426
426
  Open individual names back up, close extra ones, or turn it off entirely:
427
427
 
428
- ```typescript
428
+ ```typescript fragment
429
429
  // config/devtools.ts
430
430
  export default DevtoolsConfig({
431
431
  redact: {
@@ -458,7 +458,7 @@ directly when you are recording values somewhere else and need the same three
458
458
  problems solved — cycles, a depth bound, and values like `Date` or `File` that
459
459
  read better flat than walked — but want your own markers:
460
460
 
461
- ```typescript
461
+ ```typescript fragment
462
462
  import { redactGraph } from "@zerotal/core/security";
463
463
 
464
464
  const safe = redactGraph(payload, {
@@ -557,7 +557,7 @@ the inspector on a shared staging box was to lie about `APP_ENV`.
557
557
 
558
558
  There is now a supported way:
559
559
 
560
- ```typescript
560
+ ```typescript fragment
561
561
  // config/devtools.ts
562
562
  export default DevtoolsConfig({
563
563
  enabled: true, // explicit; `null` follows the dev-surface gate
@@ -589,7 +589,7 @@ there the way in is the dashboard at `/__zerotal/devtools`.
589
589
 
590
590
  To switch it off entirely without removing the provider:
591
591
 
592
- ```typescript
592
+ ```typescript fragment
593
593
  // config/devtools.ts
594
594
  export default DevtoolsConfig({ enabled: false });
595
595
  ```
@@ -618,7 +618,7 @@ the way — not about the panel itself.
618
618
  injection middleware appends a script tag to every HTML response, and a
619
619
  misconfigured deploy that ships it exposes request traces to your users:
620
620
 
621
- ```typescript
621
+ ```typescript fragment
622
622
  // tests/devtools/injection.test.ts
623
623
  import { test } from "bun:test";
624
624
  import { createApp } from "../helpers.ts";
@@ -637,7 +637,7 @@ test("the devtools script is not injected outside development", async () => {
637
637
  to a JSON body or a file download corrupts it, and the failure shows up as a
638
638
  parse error somewhere unrelated:
639
639
 
640
- ```typescript
640
+ ```typescript fragment
641
641
  // tests/devtools/injection.test.ts
642
642
  test("JSON responses are left alone", async () => {
643
643
  const res = await app.get("/api/posts", { Accept: "application/json" });
@@ -650,7 +650,7 @@ test("JSON responses are left alone", async () => {
650
650
  **The trace store is an ordinary object**, so a panel plugin you write tests
651
651
  without a browser:
652
652
 
653
- ```typescript
653
+ ```typescript fragment
654
654
  // tests/devtools/plugin.test.ts
655
655
  import { traceStore } from "@zerotal/devtools";
656
656
 
@@ -753,7 +753,7 @@ leaves. Each node's own fields become its `treeBadge` chip, its `flags`, and a
753
753
  dim attribute line, so you describe what a node _is_ without the panel knowing
754
754
  what any of it means:
755
755
 
756
- ```typescript
756
+ ```typescript fragment
757
757
  trace.channel({
758
758
  id: "widgets",
759
759
  label: "Widgets",
@@ -779,7 +779,7 @@ every value any package might use.
779
779
 
780
780
  The shape pushed to the store and streamed to the panel:
781
781
 
782
- ```typescript
782
+ ```typescript fragment
783
783
  // from @zerotal/devtools
784
784
  interface RequestTrace {
785
785
  id: string;
@@ -903,7 +903,7 @@ Resolve `devtools.trace` from the container, declare how your entries should
903
903
  read, then record one per event. Guard the lookup: it is absent when devtools is
904
904
  not installed or the app is in production, and your package must not care.
905
905
 
906
- ```typescript
906
+ ```typescript fragment
907
907
  // your-package/src/observability.ts
908
908
  import { FrameworkEvents, RequestContext } from "zerotal";
909
909
  import type { Application } from "zerotal";
@@ -955,7 +955,7 @@ and entries recorded before a channel is declared still appear once it is.
955
955
  From your package's **browser** code, register a panel on the global registry the
956
956
  panel exposes:
957
957
 
958
- ```ts
958
+ ```ts fragment
959
959
  window.__zerotalDevtools?.register({
960
960
  id: "my-panel", // unique — the tab is addressed internally as `plugin:my-panel`
961
961
  title: "My Panel", // tab label
@@ -1010,4 +1010,4 @@ folded follow-up (`child`).
1010
1010
  - [Query builder](/docs/query-builder) — the queries DevTools traces and flags for N+1.
1011
1011
  - [Events](/docs/events) — the `FrameworkEvents` that DevTools subscribes to.
1012
1012
  - [Telemetry](/docs/telemetry) — production-grade metrics once you move past the dev panel.
1013
- - [Testing](/docs/testing/index) — assert on requests without the floating panel.
1013
+ - [Testing](/docs/testing) — assert on requests without the floating panel.
@@ -18,7 +18,7 @@ in `@zerotal/core` signs the link, and `ValidateSignatureMiddleware` in
18
18
  `@zerotal/auth` checks it. There is no extra package and no extra table:
19
19
 
20
20
  ```typescript
21
- import { Url } from "zerotal";
21
+ import { Url } from "zerotal/http";
22
22
  import { ValidateSignatureMiddleware } from "@zerotal/auth";
23
23
  ```
24
24
 
@@ -51,7 +51,7 @@ register ──▶ Url.sign(/auth/verify?id&email, ttl) ──▶ email link
51
51
 
52
52
  Add a nullable `email_verified_at` column to your users table:
53
53
 
54
- ```typescript
54
+ ```typescript fragment
55
55
  // database/migrations/002_add_email_verified_at.ts
56
56
  export default class AddEmailVerifiedAt extends Migration {
57
57
  async up(schema: Schema) {
@@ -140,7 +140,7 @@ The verify route is already guarded by `ValidateSignatureMiddleware`, so the
140
140
  controller can trust that the URL is intact — it only checks that the `id`/`email`
141
141
  pair still resolves to a real user:
142
142
 
143
- ```typescript
143
+ ```typescript fragment
144
144
  // app/controllers/VerificationController.ts
145
145
  import { Auth } from "@zerotal/auth";
146
146
  import { Notify } from "@zerotal/notifications";
@@ -199,7 +199,7 @@ export class VerificationController {
199
199
 
200
200
  ## Routes
201
201
 
202
- ```typescript
202
+ ```typescript fragment
203
203
  // routes/web.ts
204
204
  import { ValidateSignatureMiddleware } from "@zerotal/auth";
205
205
  import { RequireAuthMiddleware } from "#app/middleware/RequireAuth.ts";
@@ -220,7 +220,7 @@ Router.post("/auth/verify/resend", VerificationController, "resend", [RequireAut
220
220
  Call `Notify.queue()` at the end of your registration handler so the HTTP response
221
221
  returns immediately while delivery happens in the background:
222
222
 
223
- ```typescript
223
+ ```typescript fragment
224
224
  // app/controllers/AuthController.ts
225
225
  import { Notify } from "@zerotal/notifications";
226
226
  import { VerifyEmailNotification } from "#app/notifications/VerifyEmailNotification.ts";
@@ -242,7 +242,7 @@ Write a `VerifiedMiddleware` once and apply it to routes that must only be
242
242
  accessible to verified users. It extends `BaseMiddleware` (from `zerotal`)
243
243
  and reads the current user via `Auth.userOrNull()`:
244
244
 
245
- ```typescript
245
+ ```typescript fragment
246
246
  // app/middleware/Verified.ts
247
247
  import { BaseMiddleware } from "zerotal";
248
248
  import type { HttpContext, NextFn } from "zerotal";
@@ -273,7 +273,7 @@ export class VerifiedMiddleware extends BaseMiddleware {
273
273
  Apply it alongside `RequireAuthMiddleware` so unverified users are bounced to the
274
274
  notice page:
275
275
 
276
- ```typescript
276
+ ```typescript fragment
277
277
  // routes/web.ts
278
278
  Router.group({ middleware: [RequireAuthMiddleware, VerifiedMiddleware] }, () => {
279
279
  Router.get("/dashboard", DashboardController, "index");
@@ -284,7 +284,7 @@ Router.group({ middleware: [RequireAuthMiddleware, VerifiedMiddleware] }, () =>
284
284
 
285
285
  ## Testing
286
286
 
287
- ```typescript
287
+ ```typescript fragment
288
288
  // tests/email-verification.test.ts
289
289
  it("unverified user is redirected to notice page", async () => {
290
290
  const user = await UserFactory.create({ emailVerifiedAt: null });
@@ -81,7 +81,7 @@ Crypt.decrypt<{ userId: number }>(blob).userId; // 7
81
81
  algorithm is auto-detected on verify, so changing the default never breaks
82
82
  existing hashes.
83
83
 
84
- ```ts
84
+ ```ts fragment
85
85
  // in a controller or service
86
86
  import { Hash } from "zerotal/security";
87
87
 
@@ -147,7 +147,7 @@ thread a signing key through your code.
147
147
 
148
148
  ### Generating a signed URL
149
149
 
150
- ```ts
150
+ ```ts fragment
151
151
  function sign(
152
152
  base: string,
153
153
  params?: Record<string, string>,
@@ -173,7 +173,7 @@ per-feature invite secret): `Url.sign(base, params, minutes, inviteSecret)`.
173
173
 
174
174
  ### Verifying a signed URL
175
175
 
176
- ```ts
176
+ ```ts fragment
177
177
  // in a controller
178
178
  import { Url } from "zerotal/http";
179
179
 
@@ -209,7 +209,7 @@ Apply `ValidateSignatureMiddleware` to any route that receives signed URLs — i
209
209
  rejects invalid or expired links with a 403 JSON response, so your controller
210
210
  only runs when the signature is good:
211
211
 
212
- ```ts
212
+ ```ts fragment
213
213
  // routes/web.ts
214
214
  import { ValidateSignatureMiddleware } from "@zerotal/auth";
215
215
 
@@ -277,7 +277,7 @@ that a round-trip test alone would never notice.
277
277
  **Tampering must throw**, and proving it is what tells you the payload is
278
278
  authenticated rather than merely scrambled:
279
279
 
280
- ```typescript
280
+ ```typescript fragment
281
281
  // tests/security/Crypt.test.ts
282
282
  import { DecryptionError } from "zerotal/security";
283
283
 
@@ -292,7 +292,7 @@ test("a modified payload is rejected", () => {
292
292
  **`encrypt()` round-trips any JSON value**, so a test covering an object confirms
293
293
  the serialisation as well as the cipher:
294
294
 
295
- ```typescript
295
+ ```typescript fragment
296
296
  // tests/security/Crypt.test.ts
297
297
  const payload = Crypt.encrypt({ userId: 7, scopes: ["read"] });
298
298
 
@@ -303,7 +303,7 @@ Hashing has its own trap: every `Hash` method except `needsRehash` is async, and
303
303
  forgotten `await` yields a `Promise`, which is always truthy — the one mistake in
304
304
  this API that silently passes.
305
305
 
306
- ```typescript
306
+ ```typescript fragment
307
307
  // tests/security/Hash.test.ts
308
308
  import { Hash } from "zerotal/security";
309
309