@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
@@ -0,0 +1,229 @@
1
+ ---
2
+ title: Seeding
3
+ description: Populate your database with repeatable test and demo data through ORM-aware seeder classes.
4
+ ---
5
+
6
+ # Seeding
7
+
8
+ Seeders populate your database with test or demo data. They work through your
9
+ [ORM](/docs/orm) models and pair naturally with [Factories](/docs/orm/factories),
10
+ which generate realistic records.
11
+
12
+ Reach for a seeder whenever you need a known starting state: a fresh developer
13
+ checkout that needs sample content, a demo environment, or the baseline rows a test
14
+ suite expects. Unlike [migrations](/docs/migrations), seeders are **not** tracked —
15
+ nothing records that they've run, so running one twice inserts the data twice. Design
16
+ each seeder to be safe to re-run (wipe first, or check before inserting) and you can
17
+ reset your local database to a clean, populated state any time.
18
+
19
+ Seeding ships as part of [`@zerotal/orm`](/docs/orm) — there is no separate
20
+ package to install or provider to register. Once the ORM is set up, the
21
+ `make:seeder` and `db:seed` commands are available.
22
+
23
+ ## Getting Started
24
+
25
+ Seeding ships with `@zerotal/orm`. If you have the [database](/docs/database)
26
+ set up there is nothing further to install:
27
+
28
+ ```typescript
29
+ import { Seeder } from "@zerotal/orm";
30
+ ```
31
+
32
+ Run `bun zt db:seed` to execute every seeder in `database/seeders/`.
33
+
34
+ ## Creating a seeder
35
+
36
+ ```bash
37
+ # in your project root
38
+ bun zt make:seeder UserSeeder
39
+ ```
40
+
41
+ This creates `database/seeders/UserSeeder.ts`. Every seeder extends `Seeder` and
42
+ implements `run()`:
43
+
44
+ ```ts
45
+ // database/seeders/UserSeeder.ts
46
+ import { Seeder } from "@zerotal/orm";
47
+
48
+ export class UserSeeder extends Seeder {
49
+ async run(): Promise<void> {
50
+ // seed logic here
51
+ }
52
+ }
53
+ ```
54
+
55
+ `run()` is the one method you implement, and it's called once when the seeder runs.
56
+ Put your inserts here — create records through your models or factories exactly as you
57
+ would in application code. Because seeders run through the ORM, casts, hooks, and
58
+ relationships all behave normally:
59
+
60
+ ```ts
61
+ // database/seeders/UserSeeder.ts
62
+ import { Seeder } from "@zerotal/orm";
63
+ import { User } from "../../app/models/User.ts";
64
+
65
+ export class UserSeeder extends Seeder {
66
+ async run(): Promise<void> {
67
+ // A predictable admin account you can always log in with locally
68
+ await User.create({ name: "Admin", email: "admin@example.com", password: "secret" });
69
+ }
70
+ }
71
+ ```
72
+
73
+ ## Root seeder
74
+
75
+ Create a `DatabaseSeeder` that coordinates all other seeders. Use `this.call()`
76
+ to run child seeders — they execute in order inside a single transaction, so if
77
+ any fails, every change rolls back atomically.
78
+
79
+ ```ts
80
+ // database/seeders/DatabaseSeeder.ts
81
+ import { Seeder, DB } from "@zerotal/orm";
82
+ import { UserSeeder } from "./UserSeeder.ts";
83
+ import { PostSeeder } from "./PostSeeder.ts";
84
+ import { TagSeeder } from "./TagSeeder.ts";
85
+
86
+ export class DatabaseSeeder extends Seeder {
87
+ async run(): Promise<void> {
88
+ // Wipe existing data in FK order before re-seeding
89
+ await DB.raw("DELETE FROM comments");
90
+ await DB.raw("DELETE FROM posts");
91
+ await DB.raw("DELETE FROM users");
92
+
93
+ await this.call([UserSeeder, TagSeeder, PostSeeder]);
94
+ }
95
+ }
96
+ ```
97
+
98
+ Order matters: seed dependencies before dependents (users before posts).
99
+
100
+ > **Danger** — `DB.raw("DELETE FROM …")` permanently removes rows. Only wipe data
101
+ > in a seeder you run against development or test databases, never in production.
102
+
103
+ ## Running seeders
104
+
105
+ ```bash
106
+ # in your project root
107
+ bun zt db:seed
108
+ ```
109
+
110
+ The command looks for `database/seeders/DatabaseSeeder.ts`, instantiates it, and
111
+ calls `run()`. `DatabaseSeeder` is the single entry point — `db:seed` always runs that
112
+ one class, so it's the seeder you keep up to date as your app grows.
113
+
114
+ There's no flag to run an individual seeder. When you only want a subset during
115
+ development, comment out the seeders you don't need from the `this.call([...])` array,
116
+ or call one directly from a throwaway script. In tests, instantiate and run a seeder
117
+ yourself (`await new UserSeeder().run()`) to set up just the rows that test needs — see
118
+ [Database Testing](/docs/testing/database).
119
+
120
+ > **Note** — If no `DatabaseSeeder.ts` is found, `db:seed` falls back to a legacy
121
+ > `database/seeders/index.ts` that default-exports an async function. Prefer the
122
+ > class-based `DatabaseSeeder` for new projects.
123
+
124
+ ## Using factories in seeders
125
+
126
+ [Factories](/docs/orm/factories) are the cleanest way to generate seed records.
127
+ Define them once, then call them from a seeder's `run()`:
128
+
129
+ ```ts
130
+ // database/seeders/PostSeeder.ts
131
+ import { Seeder } from "@zerotal/orm";
132
+ import { UserFactory } from "../factories/UserFactory.ts";
133
+ import { PostFactory } from "../factories/PostFactory.ts";
134
+
135
+ export class PostSeeder extends Seeder {
136
+ async run(): Promise<void> {
137
+ const authors = await UserFactory.count(10).create();
138
+
139
+ for (const author of authors) {
140
+ await PostFactory.for(author).count(5).create();
141
+ }
142
+ }
143
+ }
144
+ ```
145
+
146
+ Here `UserFactory.count(10).create()` inserts ten authors, and `PostFactory.for(author)`
147
+ attaches each batch of posts to one of them — a quick way to build realistic related
148
+ data without hand-writing foreign keys. Factories also keep seeders short and readable:
149
+ the seeder says _how much_ data to make, the factory decides _what each record looks
150
+ like_.
151
+
152
+ See [Factories](/docs/orm/factories) for defining factories, the full API
153
+ (`create`, `make`, `count`, `for`, `state`, `afterCreate`, …), and the `fake` data
154
+ helper.
155
+
156
+ ## Testing
157
+
158
+ Set your suite up once as described in [Testing](/docs/testing). A seeder is a
159
+ class with a `run()` method, so testing one is just calling it:
160
+
161
+ ```typescript
162
+ // tests/seeders/RoleSeeder.test.ts
163
+ import { test } from "bun:test";
164
+ import { assertDatabaseCount, assertDatabaseHas } from "@zerotal/testing";
165
+ import { RoleSeeder } from "../../database/seeders/RoleSeeder.ts";
166
+
167
+ test("seeds the three baseline roles", async () => {
168
+ await new RoleSeeder().run();
169
+
170
+ await assertDatabaseCount("roles", 3);
171
+ await assertDatabaseHas("roles", { name: "admin" });
172
+ });
173
+ ```
174
+
175
+ **Test that re-running is safe.** A seeder that a colleague runs twice — or that
176
+ a deploy runs on every release — must not double its rows. This is the failure
177
+ seeders actually have:
178
+
179
+ ```typescript
180
+ // tests/seeders/RoleSeeder.test.ts
181
+ test("running twice does not duplicate rows", async () => {
182
+ await new RoleSeeder().run();
183
+ await new RoleSeeder().run();
184
+
185
+ await assertDatabaseCount("roles", 3);
186
+ });
187
+ ```
188
+
189
+ If that fails, the seeder needs `updateOrCreate` rather than `create`.
190
+
191
+ **`call()` wraps its children in one transaction**, so a test for a composite
192
+ seeder can assert the all-or-nothing behaviour directly — make a late child throw
193
+ and check that the earlier one left nothing behind:
194
+
195
+ ```typescript
196
+ // tests/seeders/DatabaseSeeder.test.ts
197
+ test("a failing child rolls the whole run back", async () => {
198
+ await expect(new DatabaseSeeder().run()).rejects.toThrow();
199
+
200
+ await assertDatabaseCount("users", 0);
201
+ });
202
+ ```
203
+
204
+ > **Note** — Factories used inside a seeder stay silent: model observers and
205
+ > hooks do not fire. That is deliberate, so seeding a thousand users doesn't send
206
+ > a thousand emails. See [Testing](/docs/orm#testing) in the ORM guide if you need
207
+ > the lifecycle to run.
208
+
209
+ ## References
210
+
211
+ The `Seeder` base class, imported from `@zerotal/orm`:
212
+
213
+ | Member | Signature | Description |
214
+ | ------ | ---------------------------------------------------- | -------------------------------------------------------------------------------------------- |
215
+ | `run` | `abstract run(): Promise<void>` | Your seed logic. Implement this on every seeder. |
216
+ | `call` | `call(seeders: (new () => Seeder)[]): Promise<void>` | Run the given seeder classes in order inside one transaction; any throw rolls them all back. |
217
+
218
+ Related commands:
219
+
220
+ | Command | Description |
221
+ | ------------------------- | ------------------------------------------------------- |
222
+ | `bun zt make:seeder Name` | Scaffold `database/seeders/Name.ts` extending `Seeder`. |
223
+ | `bun zt db:seed` | Run `database/seeders/DatabaseSeeder.ts`. |
224
+
225
+ ## Next steps
226
+
227
+ - [Factories](/docs/orm/factories) — generating realistic model records.
228
+ - [Database Testing](/docs/testing/database) — seeding and resetting state in tests.
229
+ - [Migrations](/docs/migrations) — the schema your seeders populate.
@@ -0,0 +1,451 @@
1
+ ---
2
+ title: Session
3
+ description: Give every HTTP request a signed, per-user data store you can read, write, and flash from anywhere in the pipeline.
4
+ ---
5
+
6
+ # Session
7
+
8
+ `@zerotal/session` attaches a signed, per-user data store to every HTTP request.
9
+ Read, write, flash, and regenerate session data from anywhere in the request
10
+ pipeline without managing cookies by hand.
11
+
12
+ ## Getting Started
13
+
14
+ ```bash
15
+ # in your project root
16
+ bun add @zerotal/session
17
+ ```
18
+
19
+ ## Register the provider
20
+
21
+ Add `SessionProvider` to the providers array in `bootstrap/providers.ts`:
22
+
23
+ ```typescript
24
+ // bootstrap/providers.ts
25
+ import { SessionProvider } from "@zerotal/session";
26
+
27
+ export default [
28
+ // …your other providers
29
+ SessionProvider,
30
+ ];
31
+ ```
32
+
33
+ Registering the provider switches on the following (it only runs in the `web` and
34
+ `test` environments):
35
+
36
+ - `onRegister` — binds `session.driver` (the cookie or redis driver) and the
37
+ `session` accessor as lazy singletons in the container.
38
+ - `onBooting` — auto-registers `SessionMiddleware` via `app.useOnce()`, so the
39
+ session is loaded on every request, and warns if `session.secure` is off in
40
+ production.
41
+ - `onBooted` — pre-resolves both singletons so the `Session` facade can read them
42
+ synchronously.
43
+
44
+ > **Note** — `SessionProvider` registers only `SessionMiddleware`. CSRF protection
45
+ > is a separate middleware you opt into — see [CSRF protection](#csrf-protection).
46
+
47
+ ## Configuration
48
+
49
+ Create `config/session.ts`. Use the `SessionConfig()` helper so every field stays
50
+ type-checked while supplying the defaults:
51
+
52
+ ```typescript
53
+ // config/session.ts
54
+ import { SessionConfig } from "@zerotal/session";
55
+ import { env } from "zerotal";
56
+
57
+ export default SessionConfig({
58
+ driver: "cookie", // 'cookie' | 'redis'
59
+ lifetime: 86400, // seconds — 24 hours
60
+ cookie: "zerotal_session",
61
+ httpOnly: true,
62
+ sameSite: "Lax",
63
+ secure: env("APP_ENV") === "production",
64
+ secret: env("APP_KEY", ""), // signs the session cookie
65
+ });
66
+ ```
67
+
68
+ | Field | Required | Default | Description |
69
+ | ---------- | -------- | ----------- | ------------------------------------------------------------ |
70
+ | `driver` | no | `"cookie"` | Storage backend: `"cookie"` or `"redis"`. |
71
+ | `lifetime` | no | `86400` | Session lifetime in seconds (24 hours). |
72
+ | `cookie` | no | `"session"` | Name of the session cookie. |
73
+ | `httpOnly` | no | `true` | Marks the session cookie HTTP-only (hidden from JS). |
74
+ | `sameSite` | no | `"Lax"` | SameSite policy: `"Strict"`, `"Lax"`, or `"None"`. |
75
+ | `secure` | no | `false` | Require HTTPS for the cookie. Turn on in production. |
76
+ | `secret` | no | `""` | HMAC secret used to sign the cookie. Set this in production. |
77
+
78
+ Add a signing secret to `.env`:
79
+
80
+ ```env
81
+ # .env
82
+ APP_KEY=your-random-32-char-secret-here
83
+ ```
84
+
85
+ > **Danger** — Without a strong `secret`, session cookies can be forged. The cookie
86
+ > driver throws `SessionSecretMissingError` if constructed with an empty secret;
87
+ > always set `APP_KEY` (or `session.secret`) before deploying.
88
+
89
+ ## Basic usage
90
+
91
+ `SessionMiddleware` attaches a `SessionManager` to every request. There are two
92
+ ways to reach it:
93
+
94
+ - **`ctx.session`** inside a route handler — the `SessionManager` lives on the
95
+ request `HttpContext` (middleware receives the same context, so it's
96
+ `ctx.session` there too).
97
+ - **The `Session` facade** anywhere — controllers, services, jobs — without
98
+ threading the context through.
99
+
100
+ ```typescript
101
+ // in a controller or service
102
+ import { Session } from "@zerotal/session";
103
+
104
+ Session.set("locale", "fr");
105
+ const locale = Session.get<string>("locale");
106
+ Session.flash("success", "Saved!");
107
+ ```
108
+
109
+ The facade resolves the current request's session via `RequestContext`, so it
110
+ works in any code running during a request. It requires `SessionMiddleware` to be
111
+ registered. Both `ctx.session` and `Session` expose the same methods (see the
112
+ [reference](#session-api) below).
113
+
114
+ ## Reading and writing
115
+
116
+ ```typescript
117
+ // in a controller
118
+ import type { HttpContext } from "zerotal";
119
+
120
+ async action(ctx: HttpContext) {
121
+ // Write
122
+ ctx.session.set("locale", "fr");
123
+ ctx.session.set("cart", [1, 2, 3]);
124
+
125
+ // Read (returns unknown — cast to your type)
126
+ const locale = ctx.session.get("locale") as string;
127
+ const cart = ctx.session.get("cart") as number[];
128
+
129
+ // Check existence
130
+ const hasLocale = ctx.session.has("locale"); // boolean
131
+
132
+ // Read and remove in one call
133
+ const once = ctx.session.pull("cart");
134
+
135
+ // Delete one key
136
+ ctx.session.forget("locale");
137
+
138
+ // Wipe all session data
139
+ ctx.session.flush();
140
+
141
+ // Return the current session ID
142
+ const id = ctx.session.id();
143
+ }
144
+ ```
145
+
146
+ ## Flash data
147
+
148
+ Flash stores a value for **one subsequent request only** — it is automatically
149
+ swept after the next request. The canonical use case is POST-Redirect-GET status
150
+ messages.
151
+
152
+ ```typescript
153
+ // in a controller — survives exactly one redirect
154
+ ctx.session.flash("success", "Post created!");
155
+ ctx.session.flash("errors", { title: ["Required"] });
156
+
157
+ // Or via the ctx shorthand (same thing):
158
+ ctx.flash("success", "Post created!");
159
+ ctx.flash("errors", { title: ["Required"] });
160
+
161
+ // Or via the Session facade (anywhere — no context needed):
162
+ Session.flash("success", "Post created!");
163
+ ```
164
+
165
+ Read on the next request:
166
+
167
+ ```typescript
168
+ // in a controller
169
+ const msg = ctx.flashed<string>("success");
170
+ const errors = ctx.flashed<Record<string, string[]>>("errors");
171
+ ```
172
+
173
+ Full POST-Redirect-GET pattern:
174
+
175
+ ```typescript
176
+ // in a controller
177
+ async store(ctx: HttpContext) {
178
+ await Post.create(await ctx.body());
179
+ ctx.flash("success", "Post published!");
180
+ ctx.redirect("/posts", 303); // 303 ensures the browser GETs the redirect
181
+ }
182
+
183
+ async index(ctx: HttpContext) {
184
+ const success = ctx.flashed<string>("success"); // available here, gone after
185
+ ctx.view(PostsPage, { success });
186
+ }
187
+ ```
188
+
189
+ ## Session fixation protection
190
+
191
+ Always regenerate the session ID after a privilege change (login, logout, password
192
+ change) to prevent session-fixation attacks:
193
+
194
+ ```typescript
195
+ // in a controller
196
+ ctx.session.regenerate(); // issues a new session ID; data is preserved
197
+ // or: Session.regenerate();
198
+ ```
199
+
200
+ > **Danger** — Skipping `regenerate()` on login lets an attacker who planted a
201
+ > known session ID before authentication ride the now-privileged session.
202
+
203
+ ## Session drivers
204
+
205
+ | Driver | Notes |
206
+ | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- |
207
+ | `cookie` | Default. Session data is signed and stored in a browser cookie. No server storage needed. Max ~4 KB per session. |
208
+ | `redis` | Session data stored in Redis by ID. Use when sessions are large, shared across cluster nodes, or need server-side invalidation. Requires a Redis URL. |
209
+
210
+ ### Which driver should I use?
211
+
212
+ - **`cookie`** — the default. Reach for it unless you outgrow it: it needs no
213
+ infrastructure and keeps state with the client.
214
+ - **`redis`** — switch when sessions exceed the ~4 KB cookie budget, must be shared
215
+ across multiple nodes, or need to be invalidated server-side (e.g. force-logout).
216
+
217
+ Switch drivers via the environment without changing application code:
218
+
219
+ ```env
220
+ # .env.production
221
+ SESSION_DRIVER=redis
222
+ SESSION_REDIS=redis://localhost:6379
223
+ ```
224
+
225
+ ```typescript
226
+ // config/session.ts
227
+ import { SessionConfig } from "@zerotal/session";
228
+ import { env } from "zerotal";
229
+
230
+ export default SessionConfig({
231
+ driver: env("SESSION_DRIVER", "cookie"),
232
+ // …
233
+ });
234
+ ```
235
+
236
+ > **Note** — The redis driver reads its connection URL from the `session.redis`
237
+ > config key (defaulting to `redis://localhost:6379`). Wire it from an env var as
238
+ > shown above.
239
+
240
+ ## CSRF protection
241
+
242
+ `CsrfMiddleware` defends state-changing requests against cross-site request forgery.
243
+ Unlike `SessionMiddleware`, it is **not** auto-registered — add it after the session
244
+ middleware, typically in the `web` middleware group:
245
+
246
+ ```typescript
247
+ // bootstrap/app.ts (or wherever you register middleware)
248
+ import { SessionMiddleware, CsrfMiddleware } from "@zerotal/session";
249
+
250
+ app.use([SessionMiddleware, CsrfMiddleware]);
251
+ // In production over HTTPS, mark the XSRF cookie Secure:
252
+ // app.use([SessionMiddleware, CsrfMiddleware.with({ secure: true })]);
253
+ ```
254
+
255
+ On its first request the middleware stores a random token in the session. On any
256
+ unsafe method (anything other than `GET`, `HEAD`, `OPTIONS`) it compares — in
257
+ constant time — the session token against the `x-csrf-token` or `x-xsrf-token`
258
+ request header, returning **`419`** on a mismatch.
259
+
260
+ After every request it also sets a non-HttpOnly `XSRF-TOKEN` cookie, so Axios
261
+ (and therefore Inertia) reads it and replays it as the `X-XSRF-TOKEN` header
262
+ automatically — no manual wiring for those clients.
263
+
264
+ To expose the token elsewhere (HTML meta tag, Inertia shared props), read it with
265
+ `CsrfMiddleware.token()`:
266
+
267
+ ```typescript
268
+ // in a controller or Inertia shared-props factory
269
+ import { CsrfMiddleware } from "@zerotal/session";
270
+
271
+ const token = CsrfMiddleware.token(); // reads the active request's session
272
+ ```
273
+
274
+ For a plain HTML form posting via `fetch`, surface the token in a meta tag and send
275
+ it as a header:
276
+
277
+ ```html
278
+ <!-- in your layout -->
279
+ <meta name="csrf-token" content="${CsrfMiddleware.token()}" />
280
+ ```
281
+
282
+ ```typescript
283
+ // in your client JS
284
+ const token = document.querySelector('meta[name="csrf-token"]')?.getAttribute("content");
285
+
286
+ fetch("/posts", {
287
+ method: "POST",
288
+ headers: {
289
+ "Content-Type": "application/json",
290
+ "X-CSRF-Token": token ?? "",
291
+ },
292
+ body: JSON.stringify({ title: "Hello" }),
293
+ });
294
+ ```
295
+
296
+ > **Warning** — The token is validated from a request **header**, not a `_token`
297
+ > form field. A classic `<form method="POST">` with no JavaScript will not pass
298
+ > the header, so submit through `fetch`/Axios (Inertia does this for you).
299
+
300
+ ## Accessing the session outside a handler
301
+
302
+ Use the **`Session` facade** — it reaches the current request's session through
303
+ `RequestContext`, so service-layer code never has to thread `ctx` through:
304
+
305
+ ```typescript
306
+ // in a service
307
+ import { Session } from "@zerotal/session";
308
+
309
+ function rememberLocale(locale: string) {
310
+ Session.set("locale", locale);
311
+ }
312
+
313
+ function currentLocale(): string | undefined {
314
+ return Session.get<string>("locale");
315
+ }
316
+ ```
317
+
318
+ This is the recommended approach for anything that isn't a route handler. If you
319
+ need the raw `HttpContext` for other reasons, `RequestContext.tryGet()` (or
320
+ `RequestContext.get()` to throw when absent) still gives it to you, but for the
321
+ session itself prefer the facade.
322
+
323
+ ## Authenticated sessions
324
+
325
+ `AuthSessionMiddleware` extends `SessionMiddleware`: after loading the session it
326
+ reads `session.get("user_id")`, looks the user up with a callback you supply, and
327
+ attaches the result to `ctx.user` for the rest of the request.
328
+
329
+ ```typescript
330
+ // bootstrap/app.ts
331
+ import { AuthSessionMiddleware, CookieDriver } from "@zerotal/session";
332
+ import { env } from "zerotal";
333
+ import { User } from "../app/models/User.ts";
334
+
335
+ app.use(
336
+ class extends AuthSessionMiddleware {
337
+ constructor() {
338
+ super(new CookieDriver(env("APP_KEY", "")), (id) => User.find(id));
339
+ }
340
+ },
341
+ );
342
+ ```
343
+
344
+ ## Testing
345
+
346
+ Pre-seed session data with `withSession()`, then assert on the response with
347
+ `assertSessionHas()`:
348
+
349
+ ```typescript
350
+ // in a test
351
+ import { createTestApp } from "@zerotal/testing";
352
+
353
+ const res = await testApp.withSession({ locale: "fr", cart: [1, 2, 3] }).get("/checkout");
354
+
355
+ res.assertOk();
356
+ ```
357
+
358
+ ```typescript
359
+ // in a test
360
+ const res = await testApp.post("/locale", { locale: "fr" });
361
+
362
+ res.assertSessionHas("locale");
363
+ res.assertSessionHas("locale", "fr");
364
+ ```
365
+
366
+ ```typescript
367
+ // in a test — assert a flash key was written before a redirect
368
+ const res = await testApp.actingAs(user).post("/posts", { title: "Hello" });
369
+
370
+ res.assertRedirect("/posts");
371
+ res.assertSessionHas("success"); // flash key was written
372
+ ```
373
+
374
+ ## References
375
+
376
+ ### Session API
377
+
378
+ In a handler the session is `ctx.session`; the `Session` facade exposes the same
379
+ methods anywhere. The facade is generic (`Session.get<T>(key)`), while
380
+ `ctx.session.get` returns `unknown`.
381
+
382
+ | Method | Signature | Description |
383
+ | ------------------------------------------------------ | --------------------------------------- | ---------------------------------------- |
384
+ | `ctx.session.get(key)` / `Session.get<T>(key)` | `(key: string) => unknown` | Read a value (`undefined` if absent). |
385
+ | `ctx.session.set(key, value)` / `Session.set(...)` | `(key: string, value: unknown) => void` | Write a value. |
386
+ | `ctx.session.has(key)` / `Session.has(key)` | `(key: string) => boolean` | Check whether a key exists. |
387
+ | `ctx.session.pull(key)` / `Session.pull<T>(key)` | `(key: string) => unknown` | Read a value and remove it in one call. |
388
+ | `ctx.session.forget(key)` / `Session.forget(key)` | `(key: string) => void` | Delete a key. |
389
+ | `ctx.session.flush()` / `Session.flush()` | `() => void` | Delete all keys. |
390
+ | `ctx.session.flash(key, value)` / `Session.flash(...)` | `(key: string, value: unknown) => void` | Write a one-request-only value. |
391
+ | `ctx.session.regenerate()` / `Session.regenerate()` | `() => void` | Issue a new session ID (data preserved). |
392
+ | `ctx.session.id()` / `Session.id()` | `() => string` | Return the current session ID. |
393
+
394
+ ### HttpContext helpers
395
+
396
+ | Method | Signature | Description |
397
+ | ----------------------- | --------------------------------------- | --------------------------------------------- |
398
+ | `ctx.flash(key, value)` | `(key: string, value: unknown) => void` | Shorthand for `ctx.session.flash()`. |
399
+ | `ctx.flashed<T>(key)` | `(key: string) => T \| undefined` | Read a value flashed in the previous request. |
400
+
401
+ ### CsrfMiddleware
402
+
403
+ | Member | Signature | Description |
404
+ | ---------------------------- | -------------------------------------------- | --------------------------------------------------- |
405
+ | `CsrfMiddleware.token(ctx?)` | `(ctx?: HttpContext) => string \| undefined` | Read the current request's CSRF token from session. |
406
+ | `CsrfMiddleware.with(opts)` | `(opts: { secure?: boolean }) => Middleware` | Configure the middleware (e.g. a `Secure` cookie). |
407
+
408
+ ### Errors
409
+
410
+ Session errors extend `SessionError`, which extends the framework's
411
+ `ZerotalError`. Most surface at boot rather than per request — they are
412
+ configuration faults, and failing loudly at startup beats silently losing
413
+ sessions.
414
+
415
+ | Error | Code | Raised when |
416
+ | ---------------------------- | -------------------------- | ---------------------------------------------------------- |
417
+ | `SessionError` | `E_SESSION` | Base class — catch this to handle any session failure. |
418
+ | `SessionSecretMissingError` | `E_SESSION_SECRET_MISSING` | No `SESSION_SECRET` is configured. Raised at boot. |
419
+ | `SessionDriverMissingError` | `E_SESSION_DRIVER_MISSING` | The configured driver name matches nothing registered. |
420
+ | `SessionCookieOverflowError` | — | The serialised session exceeds the 4 KB a cookie can hold. |
421
+
422
+ `SessionCookieOverflowError` is the one you meet in real use. The `cookie` driver
423
+ stores the whole session in the cookie itself, so putting a user object — or a
424
+ flash message with a stack trace — into the session overflows a hard browser
425
+ limit:
426
+
427
+ ```typescript
428
+ // in a controller
429
+ import { SessionCookieOverflowError } from "@zerotal/session";
430
+
431
+ try {
432
+ http.session.put("report", hugeObject);
433
+ } catch (error) {
434
+ if (error instanceof SessionCookieOverflowError) {
435
+ // Keep a key in the session, the payload somewhere with room.
436
+ const id = await Cache.put(hugeObject);
437
+ http.session.put("reportId", id);
438
+ } else throw error;
439
+ }
440
+ ```
441
+
442
+ The fix is nearly always to store an identifier rather than the object, or to
443
+ move to a server-side driver where the cookie holds only the session id.
444
+
445
+ ## Next steps
446
+
447
+ - [Middleware](/docs/middleware) — where `SessionMiddleware` and `CsrfMiddleware`
448
+ sit in the pipeline.
449
+ - [HttpContext](/docs/context) — the `ctx` object that carries the session.
450
+ - [Authentication](/docs/authentication) — build on regenerated, user-scoped sessions.
451
+ - [Cookies](/docs/cookies) — the lower-level cookie API the cookie driver builds on.