@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/audit.md ADDED
@@ -0,0 +1,367 @@
1
+ ---
2
+ title: Audit
3
+ description: Capture every model write and custom event — with old/new values, actor, and request metadata — into a queryable audit_logs table.
4
+ ---
5
+
6
+ # Audit
7
+
8
+ `@zerotal/audit` provides automatic, zero-boilerplate audit logging for Zerotal
9
+ models and manual events. Every create, update, and delete is captured — with old
10
+ and new values, the authenticated actor, and request metadata — and stored in a
11
+ queryable `audit_logs` table.
12
+
13
+ ## Getting Started
14
+
15
+ `@zerotal/audit` is a workspace package, so there is no separate install step:
16
+
17
+ ```bash
18
+ # in your project root
19
+ bun add @zerotal/audit
20
+ ```
21
+
22
+ ## Register the provider
23
+
24
+ Add `AuditProvider` to the providers array in `bootstrap/providers.ts`, after
25
+ `DatabaseProvider` (the audit table lives in your database):
26
+
27
+ ```ts
28
+ // bootstrap/providers.ts
29
+ import { AuditProvider } from "@zerotal/audit";
30
+
31
+ export default [
32
+ DatabaseProvider,
33
+ SessionProvider,
34
+ AuthProvider,
35
+ AuditProvider, // ← add after DatabaseProvider
36
+ // ...
37
+ ];
38
+ ```
39
+
40
+ Registering the provider switches on the following:
41
+
42
+ - `onRegister` — binds the `Auditor` service as the `"audit"` singleton and
43
+ registers the schema concern that provisions the `audit_logs` table.
44
+ - `onBooted` — drains the pending `Auditable` registry, wiring each composed model
45
+ to the live `Auditor` once the container is ready.
46
+
47
+ > **Note** — No migration is required. A boot-time schema concern creates the
48
+ > `audit_logs` table automatically (idempotently, and skipped for the `null`
49
+ > driver). Register the provider and the table appears on first boot.
50
+
51
+ ## Configuration
52
+
53
+ Create `config/audit.ts` with the `AuditConfig()` helper so every field stays
54
+ type-checked. The file is auto-discovered — its settings are namespaced under
55
+ `audit` in the config tree:
56
+
57
+ ```ts
58
+ // config/audit.ts
59
+ import { AuditConfig } from "@zerotal/audit";
60
+
61
+ export default AuditConfig({
62
+ driver: "database", // 'database' | 'null'
63
+ table: "audit_logs",
64
+ pruneKeep: 0, // 0 = unlimited; cap records kept per model instance
65
+ captureRequest: true, // attach IP, user-agent, URL automatically
66
+ });
67
+ ```
68
+
69
+ | Field | Required | Default | Description |
70
+ | ---------------- | -------- | -------------- | --------------------------------------------------------------------- |
71
+ | `driver` | yes | `"database"` | Storage backend — `"database"` or `"null"`. |
72
+ | `table` | no | `"audit_logs"` | Table name used by the database driver. |
73
+ | `pruneKeep` | no | `0` | Max audit records kept per model instance. `0` = unlimited. |
74
+ | `captureRequest` | no | `true` | Attach IP, user-agent, and URL from the active request automatically. |
75
+
76
+ ## Auditing models
77
+
78
+ ### The Auditable mixin
79
+
80
+ Compose `Auditable` into any `Model` subclass. The audit system hooks
81
+ into the ORM lifecycle and records `created`, `updated`, and `deleted` events
82
+ automatically. Like every Zerotal mixin it takes only `(Base)` — configure it with
83
+ overridable static fields on the model.
84
+
85
+ ```ts
86
+ // app/models/User.ts
87
+ import { Auditable } from "@zerotal/audit";
88
+ import { Model, column, table } from "@zerotal/orm";
89
+
90
+ @table("users")
91
+ export class User extends Model.using(Auditable) {
92
+ protected static auditExcept = ["password", "rememberToken"];
93
+
94
+ @column({ type: "string" }) name: string;
95
+ @column({ type: "string" }) email: string;
96
+ @column({ type: "string" }) password?: string;
97
+ @column({ type: "string" }) rememberToken?: string;
98
+ }
99
+ ```
100
+
101
+ `Auditable` composes with the auth mixins as the outermost wrapper:
102
+
103
+ ```ts
104
+ // app/models/User.ts
105
+ export class User extends Auditable(WithRoles(WithPermissions(AuthUser))) {
106
+ protected static auditExcept = ["password"];
107
+ }
108
+ ```
109
+
110
+ > **Danger** — Never audit columns containing credentials. Exclude `password`,
111
+ > `rememberToken`, API tokens, etc. via `auditExcept` (shown above).
112
+
113
+ Configure auditing with overridable static fields, read per event from the
114
+ concrete model:
115
+
116
+ | Static field | Type | Description |
117
+ | ------------- | ---------- | ----------------------------------------------------------------------- |
118
+ | `auditOnly` | `string[]` | Allowlist — only these columns appear in `old_values` / `new_values`. |
119
+ | `auditExcept` | `string[]` | Denylist — exclude these columns (applied when `auditOnly` is not set). |
120
+ | `auditType` | `string` | Override the `auditable_type` string (defaults to the class name). |
121
+
122
+ The mixin also adds two instance methods, `auditLog()` and `auditLogs()`, covered
123
+ in [Manual audit events](#manual-audit-events) and
124
+ [Querying audit logs](#querying-audit-logs).
125
+
126
+ ### Programmatic registration
127
+
128
+ For a model you'd rather not wrap in the mixin, register it from a provider's
129
+ `onBooted()` (after the container has resolved the `"audit"` binding). Configure it
130
+ with the same static fields:
131
+
132
+ ```ts
133
+ // in a ServiceProvider.onBooted()
134
+ import { registerAudit } from "@zerotal/audit";
135
+
136
+ registerAudit(User);
137
+ ```
138
+
139
+ > **Which should I use?** Reach for the `Auditable` mixin by default — it also
140
+ > gives you the `auditLog()` / `auditLogs()` helpers. Use `registerAudit()` only
141
+ > when you cannot change a model's class hierarchy (e.g. a model from another
142
+ > package).
143
+
144
+ Both routes attach the same `AuditObserver`, which is what actually watches the
145
+ model's lifecycle. An update is recorded in two phases, and the reason is worth
146
+ knowing if you ever hook the same events yourself: the previous values only exist
147
+ until the ORM refreshes its snapshot, so the observer captures them during
148
+ `saving` and pairs them with the current values in `updated`. That is why an audit
149
+ row shows a real before _and_ after rather than the same values twice.
150
+
151
+ `AuditObserver` is exported for the rare case of composing it into an observer of
152
+ your own; using the mixin or `registerAudit()` is the supported path.
153
+
154
+ ## Manual audit events
155
+
156
+ Log any custom event — logins, exports, settings changes — via the `Audit` facade.
157
+ Pass the **model instance** the event concerns; `auditable_type` and `auditable_id`
158
+ are derived from it, so logs are always linked to a record:
159
+
160
+ ```ts
161
+ function log(event: AuditEvent, model: Model, payload?: InstanceAuditPayload): Promise<void>;
162
+ function log(event: AuditEvent, payload: Omit<AuditPayload, "event">): Promise<void>;
163
+ ```
164
+
165
+ ```ts
166
+ // in a controller or service (within a request context)
167
+ import { Audit } from "@zerotal/audit";
168
+
169
+ await Audit.log("login.success", user, {
170
+ tags: { method: "github_oauth" },
171
+ });
172
+
173
+ await Audit.log("report.exported", report, {
174
+ tags: { format: "csv", rows: 5000 },
175
+ });
176
+ ```
177
+
178
+ For an event not tied to a model, pass a raw payload with `auditable_type`. Outside
179
+ a request (queue job, CLI command) supply the actor explicitly:
180
+
181
+ ```ts
182
+ // in a queue job or CLI command
183
+ await Audit.log("subscription.renewed", {
184
+ auditable_type: "Subscription",
185
+ auditable_id: sub.id,
186
+ actor_type: "user",
187
+ actor_id: sub.userId,
188
+ });
189
+ ```
190
+
191
+ When a model is `Auditable`, the same call is available as an instance method:
192
+
193
+ ```ts
194
+ // in a controller or service
195
+ await user.auditLog("login.success", { tags: { method: "github_oauth" } });
196
+ ```
197
+
198
+ The facade reads the authenticated user and request details (IP, user-agent, URL)
199
+ automatically from the active request context. You only need to supply them when
200
+ operating outside a request.
201
+
202
+ > **Warning** — Audit failures never crash the application; a failed write is
203
+ > logged to the console and swallowed. Treat the audit log as best-effort, not as a
204
+ > transactional guarantee.
205
+
206
+ ## Querying audit logs
207
+
208
+ `AuditLog` is a full `Model` with scopes and chainable queries:
209
+
210
+ ```ts
211
+ // in a controller or service
212
+ import { AuditLog } from "@zerotal/audit";
213
+
214
+ // Last 25 events for a specific model instance
215
+ const history = await AuditLog.query()
216
+ .where("auditable_type", "User")
217
+ .where("auditable_id", String(user.id))
218
+ .orderBy("id", "desc")
219
+ .limit(25)
220
+ .get();
221
+
222
+ // Built-in scopes return a chainable query builder
223
+ const userHistory = await AuditLog.forModel("User", user.id).get();
224
+ const actorLog = await AuditLog.byActor(user.id).get();
225
+ const loginEvents = await AuditLog.ofEvent("login.success").get();
226
+
227
+ // Paginate
228
+ const page = await AuditLog.query()
229
+ .where("actor_id", user.id)
230
+ .orderBy("id", "desc")
231
+ .paginate(20, 1);
232
+ ```
233
+
234
+ The `Audit` facade exposes the same queries plus a convenience read:
235
+
236
+ ```ts
237
+ // in a controller or service
238
+ const logs = await Audit.logs(User, user.id).orderBy("id", "desc").limit(25).get();
239
+ const byActor = await Audit.logsByActor(user.id).get();
240
+ const events = await Audit.logsOfEvent("login.success").get();
241
+
242
+ // Eager array of recent records for a model
243
+ const recent = await Audit.historyFor("User", user.id, 25);
244
+ ```
245
+
246
+ An `Auditable` model also offers an instance shortcut:
247
+
248
+ ```ts
249
+ // in a controller or service
250
+ const logs = await user.auditLogs().orderBy("id", "desc").limit(25).get();
251
+ ```
252
+
253
+ ### Inspecting a record
254
+
255
+ ```ts
256
+ // in a controller or service
257
+ const entry = history[0];
258
+
259
+ entry.event; // "updated"
260
+ entry.auditableType; // "User"
261
+ entry.auditableId; // "42"
262
+ entry.actorId; // 7
263
+ entry.oldValues; // { email: "old@example.com" }
264
+ entry.newValues; // { email: "new@example.com" }
265
+ entry.ipAddress; // "203.0.113.1"
266
+ entry.url; // "http://localhost:3000/profile"
267
+
268
+ entry.changedKeys; // ["email"]
269
+ ```
270
+
271
+ ## What gets recorded
272
+
273
+ | Event | `old_values` | `new_values` |
274
+ | --------- | ----------------------- | ---------------------- |
275
+ | `created` | _(empty)_ | full snapshot |
276
+ | `updated` | changed fields (before) | changed fields (after) |
277
+ | `deleted` | full snapshot | _(empty)_ |
278
+ | custom | whatever you pass | whatever you pass |
279
+
280
+ For `updated`, only the diff is stored — unchanged fields are not included.
281
+ Snapshots use the model's `toJSON()`, so columns marked `hidden` are excluded
282
+ automatically.
283
+
284
+ ## Drivers
285
+
286
+ | Driver | Class | Description |
287
+ | ---------- | ---------------- | ----------------------------------------------------- |
288
+ | `database` | `DatabaseDriver` | Stores to `audit_logs` using `@zerotal/orm`. Default. |
289
+ | `null` | `NullDriver` | Discards all entries. Useful in tests. |
290
+
291
+ Both classes are exported, so a custom driver can wrap one rather than
292
+ reimplementing it — decorating `DatabaseDriver` to also ship entries to a SIEM,
293
+ for instance.
294
+
295
+ Switch the driver in config:
296
+
297
+ ```ts
298
+ // config/audit.ts
299
+ import { AuditConfig } from "@zerotal/audit";
300
+
301
+ export default AuditConfig({ driver: "null" }); // suppress auditing
302
+ ```
303
+
304
+ Or swap it entirely in a `ServiceProvider` by binding a custom `AuditDriver`
305
+ implementation to the `"audit"` singleton.
306
+
307
+ ## Testing
308
+
309
+ In tests, swap to the `NullDriver` so no database is needed. Bind a fresh
310
+ `Auditor` over the `"audit"` key:
311
+
312
+ ```ts
313
+ // tests/setup.ts
314
+ import { Application } from "zerotal";
315
+ import { Auditor, NullDriver } from "@zerotal/audit";
316
+
317
+ // Bind a fresh NullDriver so tests don't write to a DB
318
+ app.container.singleton(
319
+ "audit",
320
+ () => new Auditor(new NullDriver(), { driver: "null", captureRequest: false }),
321
+ );
322
+ ```
323
+
324
+ ## Security notes
325
+
326
+ > **Danger** — Never audit columns containing credentials. Exclude `password`,
327
+ > `rememberToken`, API tokens, etc. via `auditExcept`.
328
+
329
+ > **Warning** — `audit_logs` is append-only by design. The `AuditLog` model
330
+ > disables timestamps; do not add `updated_at` or allow row updates.
331
+
332
+ > **Note** — Grant read access to `audit_logs` only to admin roles.
333
+
334
+ ## References
335
+
336
+ `Audit` facade — resolves the `Auditor` bound at `"audit"`:
337
+
338
+ | Method | Signature | Description |
339
+ | ------------- | --------------------------------------------------------------------------------- | ------------------------------------------------------- |
340
+ | `log` | `(event, model, payload?) => Promise<void>` · `(event, payload) => Promise<void>` | Record a manual event from a model instance or payload. |
341
+ | `historyFor` | `(type: string, id: string \| number, limit?: number) => Promise<AuditRecord[]>` | Eager array of recent records for a model. |
342
+ | `logs` | `(model: AuditableRef, id: string \| number) => ModelQueryBuilder<AuditLog>` | Chainable query of one model's audit history. |
343
+ | `logsByActor` | `(actorId: number) => ModelQueryBuilder<AuditLog>` | Chainable query of every log by an actor. |
344
+ | `logsOfEvent` | `(event: AuditEvent) => ModelQueryBuilder<AuditLog>` | Chainable query of every log for an event name. |
345
+
346
+ `AuditLog` model — scopes and helpers:
347
+
348
+ | Member | Signature | Description |
349
+ | ------------- | --------------------------------------------------------------------- | -------------------------------------- |
350
+ | `forModel` | `(type: string, id: string \| number) => ModelQueryBuilder<AuditLog>` | Scope to one model instance's history. |
351
+ | `byActor` | `(actorId: number) => ModelQueryBuilder<AuditLog>` | Scope to a single actor. |
352
+ | `ofEvent` | `(event: AuditEvent) => ModelQueryBuilder<AuditLog>` | Scope to a single event name. |
353
+ | `changedKeys` | `string[]` | Getter — keys present in `newValues`. |
354
+
355
+ `Auditable(Base)` instance methods:
356
+
357
+ | Method | Signature | Description |
358
+ | ----------- | ---------------------------------------------------------------------- | ------------------------------------------- |
359
+ | `auditLog` | `(event: AuditEvent, payload?: InstanceAuditPayload) => Promise<void>` | Log a custom event against this instance. |
360
+ | `auditLogs` | `() => ModelQueryBuilder<AuditLog>` | Chainable query of this instance's history. |
361
+
362
+ ## Next steps
363
+
364
+ - [ORM Lifecycle](/docs/orm/lifecycle) — the model hooks the audit system listens to.
365
+ - [Authentication](/docs/authentication) — how the actor on each entry is resolved.
366
+ - [Authorization](/docs/authorization) — gate read access to audit logs by role.
367
+ - [Testing](/docs/testing/index) — swap in the null driver for isolated tests.