@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.
- package/CHANGELOG.md +34 -2
- package/README.md +4 -2
- package/api-surface.md +4 -4
- package/docs/about.md +16 -16
- package/docs/admin/actions.md +9 -9
- package/docs/admin/auth.md +2 -2
- package/docs/admin/dashboard.md +5 -5
- package/docs/admin/extending-ui.md +12 -12
- package/docs/admin/extending.md +16 -7
- package/docs/admin/forms.md +4 -4
- package/docs/admin/index.md +2 -2
- package/docs/admin/operations.md +9 -9
- package/docs/admin/resources.md +3 -3
- package/docs/admin/structure.md +6 -6
- package/docs/admin/tables.md +8 -8
- package/docs/admin/testing.md +4 -4
- package/docs/ai.md +118 -11
- package/docs/application.md +12 -12
- package/docs/arch.md +25 -3
- package/docs/assets.md +11 -11
- package/docs/audit.md +13 -13
- package/docs/authentication.md +39 -39
- package/docs/authorization.md +13 -13
- package/docs/broadcasting/channels.md +2 -2
- package/docs/broadcasting/client.md +2 -2
- package/docs/broadcasting/events.md +9 -10
- package/docs/broadcasting/index.md +1 -1
- package/docs/broadcasting/testing.md +5 -5
- package/docs/cache.md +15 -13
- package/docs/carbon.md +35 -35
- package/docs/changelog.md +165 -1
- package/docs/client/index.md +277 -70
- package/docs/commands.md +4 -4
- package/docs/components.md +116 -115
- package/docs/config-system.md +4 -4
- package/docs/container.md +27 -27
- package/docs/context.md +27 -27
- package/docs/contributing.md +46 -0
- package/docs/controllers.md +10 -10
- package/docs/conventions.md +23 -15
- package/docs/cookies.md +3 -3
- package/docs/csrf.md +6 -6
- package/docs/database.md +22 -17
- package/docs/deployment.md +2 -2
- package/docs/devtools.md +16 -16
- package/docs/email-verification.md +8 -8
- package/docs/encryption.md +7 -7
- package/docs/errors.md +6 -6
- package/docs/events.md +12 -11
- package/docs/flow/components.md +32 -32
- package/docs/flow/decorators.md +18 -18
- package/docs/flow/events.md +16 -16
- package/docs/flow/forms.md +17 -17
- package/docs/flow/icons.md +199 -0
- package/docs/flow/index.md +20 -20
- package/docs/flow/layouts.md +35 -35
- package/docs/flow/lifecycle.md +16 -16
- package/docs/flow/models.md +9 -9
- package/docs/flow/pagination.md +8 -8
- package/docs/flow/performance.md +7 -7
- package/docs/flow/references.md +3 -3
- package/docs/flow/routing.md +26 -26
- package/docs/flow/testing.md +20 -20
- package/docs/getting-started.md +18 -13
- package/docs/health.md +17 -8
- package/docs/helpers.md +17 -17
- package/docs/i18n.md +10 -10
- package/docs/inertia/devtools.md +4 -4
- package/docs/inertia/index.md +12 -7
- package/docs/inertia/props.md +22 -22
- package/docs/inertia/rendering.md +86 -7
- package/docs/inertia/ssr.md +2 -2
- package/docs/lifecycle.md +2 -2
- package/docs/lock.md +11 -11
- package/docs/logger.md +12 -12
- package/docs/media.md +22 -22
- package/docs/middleware.md +17 -17
- package/docs/migrations.md +13 -13
- package/docs/monitor.md +6 -6
- package/docs/notifications.md +24 -24
- package/docs/orm/casts.md +14 -14
- package/docs/orm/factories.md +8 -8
- package/docs/orm/index.md +22 -20
- package/docs/orm/lifecycle.md +13 -13
- package/docs/orm/queries.md +28 -28
- package/docs/orm/relationships.md +16 -16
- package/docs/orm/serialization.md +11 -11
- package/docs/package-development.md +10 -10
- package/docs/pagination.md +14 -14
- package/docs/password-reset.md +6 -6
- package/docs/providers.md +11 -11
- package/docs/query-builder.md +18 -18
- package/docs/queue.md +14 -12
- package/docs/rate-limiting.md +6 -6
- package/docs/responses.md +14 -14
- package/docs/roles-and-2fa.md +9 -9
- package/docs/routing.md +44 -44
- package/docs/scaffolding.md +1 -1
- package/docs/scheduler.md +10 -10
- package/docs/seeding.md +6 -6
- package/docs/session.md +16 -14
- package/docs/social.md +14 -14
- package/docs/storage.md +25 -13
- package/docs/structure.md +2 -2
- package/docs/support-policy.md +36 -8
- package/docs/telemetry.md +12 -12
- package/docs/tenancy.md +34 -40
- package/docs/testing/browser.md +6 -6
- package/docs/testing/console.md +3 -3
- package/docs/testing/database.md +10 -10
- package/docs/testing/flow-browser.md +6 -6
- package/docs/testing/http.md +12 -12
- package/docs/testing/index.md +3 -3
- package/docs/testing/mocking.md +8 -8
- package/docs/upgrade.md +1 -1
- package/docs/validator.md +19 -19
- package/docs/view.md +14 -14
- package/package.json +4 -4
- package/src/index.ts +25 -1
- package/src/tools/searchDocs.ts +7 -0
- package/docs/client/auth.md +0 -113
- package/docs/client/errors.md +0 -139
- package/docs/client/files.md +0 -118
- package/docs/client/references.md +0 -58
- package/docs/client/requests.md +0 -131
- package/docs/client/resilience.md +0 -141
- package/docs/client/testing.md +0 -146
package/docs/validator.md
CHANGED
|
@@ -89,7 +89,7 @@ export class StorePostRequest extends FormRequest {
|
|
|
89
89
|
Call `validate()` as a static method on the class. It reads `HttpContext` from
|
|
90
90
|
`AsyncLocalStorage` — no arguments needed.
|
|
91
91
|
|
|
92
|
-
```typescript
|
|
92
|
+
```typescript fragment
|
|
93
93
|
// in a controller
|
|
94
94
|
const data = await StorePostRequest.validate();
|
|
95
95
|
|
|
@@ -116,7 +116,7 @@ never need to catch them yourself.
|
|
|
116
116
|
`authorize()` runs **before** validation. Returning `false` throws a
|
|
117
117
|
`ForbiddenError` (403) without touching the request body.
|
|
118
118
|
|
|
119
|
-
```typescript
|
|
119
|
+
```typescript fragment
|
|
120
120
|
// in a FormRequest subclass
|
|
121
121
|
|
|
122
122
|
// Check the current user's role:
|
|
@@ -141,7 +141,7 @@ async authorize(): Promise<boolean> {
|
|
|
141
141
|
Because `rules()` is an instance method, you have full access to `this.context`
|
|
142
142
|
for rules that depend on the current user, route params, or session state:
|
|
143
143
|
|
|
144
|
-
```typescript
|
|
144
|
+
```typescript fragment
|
|
145
145
|
// in a FormRequest subclass
|
|
146
146
|
rules(r: RuleBuilder) {
|
|
147
147
|
const userId = this.context.user?.id;
|
|
@@ -157,7 +157,7 @@ rules(r: RuleBuilder) {
|
|
|
157
157
|
|
|
158
158
|
For simple one-off validation without a dedicated class:
|
|
159
159
|
|
|
160
|
-
```typescript
|
|
160
|
+
```typescript fragment
|
|
161
161
|
// in a controller
|
|
162
162
|
import { validate } from "@zerotal/validator";
|
|
163
163
|
|
|
@@ -180,7 +180,7 @@ For CLI commands, services, or background jobs — where there is no request to
|
|
|
180
180
|
redirect and no response to throw — use the `Validator` facade. It returns a
|
|
181
181
|
`{ success, data, errors }` outcome and never throws.
|
|
182
182
|
|
|
183
|
-
```typescript
|
|
183
|
+
```typescript fragment
|
|
184
184
|
// in a command or service
|
|
185
185
|
import { Validator } from "@zerotal/validator";
|
|
186
186
|
|
|
@@ -200,7 +200,7 @@ if (!result.success) {
|
|
|
200
200
|
|
|
201
201
|
Start every rule chain with a type method on `RuleBuilder`:
|
|
202
202
|
|
|
203
|
-
```typescript
|
|
203
|
+
```typescript fragment
|
|
204
204
|
// in a rules() method or factory
|
|
205
205
|
const r = new RuleBuilder(); // or the `r` param in rules()
|
|
206
206
|
|
|
@@ -247,7 +247,7 @@ These are available on **every** rule type:
|
|
|
247
247
|
|
|
248
248
|
### Custom validator
|
|
249
249
|
|
|
250
|
-
```typescript
|
|
250
|
+
```typescript fragment
|
|
251
251
|
// in a rules() method or factory
|
|
252
252
|
username: r.string().custom(async (value) => {
|
|
253
253
|
const taken = await User.findBy("username", value as string);
|
|
@@ -257,7 +257,7 @@ username: r.string().custom(async (value) => {
|
|
|
257
257
|
|
|
258
258
|
## String rules
|
|
259
259
|
|
|
260
|
-
```typescript
|
|
260
|
+
```typescript fragment
|
|
261
261
|
// in a rules() method or factory
|
|
262
262
|
r.string()
|
|
263
263
|
.min(3) // minimum character length
|
|
@@ -292,7 +292,7 @@ r.string()
|
|
|
292
292
|
|
|
293
293
|
## Number rules
|
|
294
294
|
|
|
295
|
-
```typescript
|
|
295
|
+
```typescript fragment
|
|
296
296
|
// in a rules() method or factory
|
|
297
297
|
r.number()
|
|
298
298
|
.min(0) // minimum value (inclusive)
|
|
@@ -305,7 +305,7 @@ r.number()
|
|
|
305
305
|
|
|
306
306
|
## Boolean rules
|
|
307
307
|
|
|
308
|
-
```typescript
|
|
308
|
+
```typescript fragment
|
|
309
309
|
// in a rules() method or factory
|
|
310
310
|
r.boolean()
|
|
311
311
|
.accepted() // truthy — for "agree to terms" checkboxes
|
|
@@ -314,7 +314,7 @@ r.boolean()
|
|
|
314
314
|
|
|
315
315
|
## Date rules
|
|
316
316
|
|
|
317
|
-
```typescript
|
|
317
|
+
```typescript fragment
|
|
318
318
|
// in a rules() method or factory
|
|
319
319
|
r.date()
|
|
320
320
|
.after("2026-01-01") // strictly after date
|
|
@@ -325,7 +325,7 @@ r.date()
|
|
|
325
325
|
|
|
326
326
|
## Array rules
|
|
327
327
|
|
|
328
|
-
```typescript
|
|
328
|
+
```typescript fragment
|
|
329
329
|
// in a rules() method or factory
|
|
330
330
|
r.array(r.string()) // array of strings
|
|
331
331
|
.min(1) // minimum item count
|
|
@@ -340,7 +340,7 @@ r.array(r.object({ name: r.string(), age: r.number() }));
|
|
|
340
340
|
|
|
341
341
|
## Object rules
|
|
342
342
|
|
|
343
|
-
```typescript
|
|
343
|
+
```typescript fragment
|
|
344
344
|
// in a rules() method or factory
|
|
345
345
|
r.object({
|
|
346
346
|
street: r.string(),
|
|
@@ -357,7 +357,7 @@ r.object({
|
|
|
357
357
|
|
|
358
358
|
Validates `File` objects from `multipart/form-data` uploads:
|
|
359
359
|
|
|
360
|
-
```typescript
|
|
360
|
+
```typescript fragment
|
|
361
361
|
// in a rules() method or factory
|
|
362
362
|
avatar: r.file()
|
|
363
363
|
.mimes(["jpg", "jpeg", "png", "webp"]) // allowed extensions
|
|
@@ -368,7 +368,7 @@ avatar: r.file()
|
|
|
368
368
|
|
|
369
369
|
## Password rules
|
|
370
370
|
|
|
371
|
-
```typescript
|
|
371
|
+
```typescript fragment
|
|
372
372
|
// in a rules() method or factory
|
|
373
373
|
r.password()
|
|
374
374
|
.min(8) // minimum length (default: 8)
|
|
@@ -390,7 +390,7 @@ The third argument ignores a record on update — pass the current record's ID,
|
|
|
390
390
|
a `UniqueOptions` object (`{ ignoreId }`) for clarity. A fourth argument
|
|
391
391
|
overrides the error message.
|
|
392
392
|
|
|
393
|
-
```typescript
|
|
393
|
+
```typescript fragment
|
|
394
394
|
// in a rules() method or factory
|
|
395
395
|
email: r.string().email().unique("users", "email");
|
|
396
396
|
|
|
@@ -403,7 +403,7 @@ email: r.string().email().unique("users", "email", { ignoreId: userId });
|
|
|
403
403
|
|
|
404
404
|
### exists — value must exist in the DB
|
|
405
405
|
|
|
406
|
-
```typescript
|
|
406
|
+
```typescript fragment
|
|
407
407
|
// in a rules() method or factory
|
|
408
408
|
userId: r.number().exists("users", "id");
|
|
409
409
|
roleSlug: r.string().exists("roles", "slug");
|
|
@@ -450,7 +450,7 @@ back to the previous page.
|
|
|
450
450
|
Errors and old input are stored in the [session](/docs/session) under the keys
|
|
451
451
|
`'errors'` and `'old'`. Read them on the next request:
|
|
452
452
|
|
|
453
|
-
```typescript
|
|
453
|
+
```typescript fragment
|
|
454
454
|
// in a controller
|
|
455
455
|
const errors = ctx.flashed<Record<string, string[]>>("errors");
|
|
456
456
|
const old = ctx.flashed<Record<string, unknown>>("old");
|
|
@@ -500,7 +500,7 @@ would not.
|
|
|
500
500
|
**A `FormRequest` fails the HTTP request**, so test it through the route it
|
|
501
501
|
guards. The status tells you which failure mode you got:
|
|
502
502
|
|
|
503
|
-
```typescript
|
|
503
|
+
```typescript fragment
|
|
504
504
|
// tests/http/register.test.ts
|
|
505
505
|
const res = await app.post("/register", { email: "not-an-email" });
|
|
506
506
|
|
package/docs/view.md
CHANGED
|
@@ -67,7 +67,7 @@ use the [`safe()` helper or `Raw` component](#embedding-raw-html).
|
|
|
67
67
|
|
|
68
68
|
The runtime renders attributes from props with a few rules:
|
|
69
69
|
|
|
70
|
-
```tsx
|
|
70
|
+
```tsx fragment
|
|
71
71
|
// in a component
|
|
72
72
|
<input type="text" value={name} disabled={isLocked} required={false} data-id={42} />
|
|
73
73
|
// → <input type="text" value="…" disabled data-id="42">
|
|
@@ -97,7 +97,7 @@ Children are rendered by type, which is the engine's security boundary:
|
|
|
97
97
|
|
|
98
98
|
Two consequences worth remembering:
|
|
99
99
|
|
|
100
|
-
```tsx
|
|
100
|
+
```tsx fragment
|
|
101
101
|
// in a component
|
|
102
102
|
{
|
|
103
103
|
user && <Welcome name={user.name} />;
|
|
@@ -123,7 +123,7 @@ Call `view()` from a controller action to set the response. It accepts any value
|
|
|
123
123
|
whose `.toString()` returns HTML — a `SafeHtml` instance from JSX is the normal
|
|
124
124
|
case:
|
|
125
125
|
|
|
126
|
-
```tsx
|
|
126
|
+
```tsx fragment
|
|
127
127
|
// app/controllers/PostController.tsx
|
|
128
128
|
import { view } from "zerotal";
|
|
129
129
|
import type { HttpContext } from "zerotal";
|
|
@@ -159,7 +159,7 @@ export default function Welcome(ctx: HttpContext, { title }: { title: string })
|
|
|
159
159
|
}
|
|
160
160
|
```
|
|
161
161
|
|
|
162
|
-
```ts
|
|
162
|
+
```ts fragment
|
|
163
163
|
// routes/index.ts — the HttpContext is injected; you only pass the extra props
|
|
164
164
|
import { Router } from "zerotal";
|
|
165
165
|
import Welcome from "../resources/views/Welcome.tsx";
|
|
@@ -213,7 +213,7 @@ export function AppLayout({ title, children }: AppLayoutProps) {
|
|
|
213
213
|
}
|
|
214
214
|
```
|
|
215
215
|
|
|
216
|
-
```tsx
|
|
216
|
+
```tsx fragment
|
|
217
217
|
// resources/views/About.tsx
|
|
218
218
|
import { defineLayout } from "zerotal/view";
|
|
219
219
|
import { AppLayout } from "./layouts/AppLayout.tsx";
|
|
@@ -229,7 +229,7 @@ export const AboutPage = wrap<{ heading: string }>(({ heading }) => (
|
|
|
229
229
|
));
|
|
230
230
|
```
|
|
231
231
|
|
|
232
|
-
```ts
|
|
232
|
+
```ts fragment
|
|
233
233
|
// routes/index.ts
|
|
234
234
|
import { Router } from "zerotal";
|
|
235
235
|
import { AboutPage } from "../resources/views/About.tsx";
|
|
@@ -246,7 +246,7 @@ directly with `Router.view()`. The third argument is the props — a **static
|
|
|
246
246
|
object**, or a **per-request factory** that receives the `HttpContext` and may be
|
|
247
247
|
async:
|
|
248
248
|
|
|
249
|
-
```ts
|
|
249
|
+
```ts fragment
|
|
250
250
|
// routes/index.ts
|
|
251
251
|
// Static props — evaluated once at registration (marketing / info pages):
|
|
252
252
|
Router.view("/about", AboutPage, { title: "About Us" });
|
|
@@ -263,7 +263,7 @@ Router.view("/privacy", PrivacyPage);
|
|
|
263
263
|
|
|
264
264
|
The returned registration is chainable:
|
|
265
265
|
|
|
266
|
-
```ts
|
|
266
|
+
```ts fragment
|
|
267
267
|
// routes/index.ts
|
|
268
268
|
Router.view("/terms", TermsPage)
|
|
269
269
|
.name("terms") // name the route for url() generation
|
|
@@ -276,7 +276,7 @@ Under [file-based routing](/docs/routing), a page file's **default export**
|
|
|
276
276
|
becomes a `GET` route automatically. The page is a function of `(http, params)`
|
|
277
277
|
and may be async:
|
|
278
278
|
|
|
279
|
-
```tsx
|
|
279
|
+
```tsx fragment
|
|
280
280
|
// app/views/posts/[slug].tsx → GET /posts/:slug
|
|
281
281
|
import type { HttpContext } from "zerotal";
|
|
282
282
|
import { Post } from "../../models/Post.ts";
|
|
@@ -311,7 +311,7 @@ export default definePage((http, params) => `<h1>Hello ${params.name}</h1>`);
|
|
|
311
311
|
When you have pre-rendered markup (e.g. from a Markdown renderer) use `safe()` or
|
|
312
312
|
`<Raw>` to bypass escaping:
|
|
313
313
|
|
|
314
|
-
```tsx
|
|
314
|
+
```tsx fragment
|
|
315
315
|
// in a component
|
|
316
316
|
import { safe, Raw } from "zerotal/view";
|
|
317
317
|
|
|
@@ -335,7 +335,7 @@ import { safe, Raw } from "zerotal/view";
|
|
|
335
335
|
`esc()` escapes a value for use in raw string templates, where automatic JSX
|
|
336
336
|
escaping isn't available:
|
|
337
337
|
|
|
338
|
-
```ts
|
|
338
|
+
```ts fragment
|
|
339
339
|
// in a helper
|
|
340
340
|
import { esc } from "zerotal/view";
|
|
341
341
|
|
|
@@ -367,7 +367,7 @@ function returning a node that stringifies, so most of it tests without a server
|
|
|
367
367
|
**Render the component and assert on the string.** No request, no router, no
|
|
368
368
|
browser:
|
|
369
369
|
|
|
370
|
-
```typescript
|
|
370
|
+
```typescript fragment
|
|
371
371
|
// tests/views/PostCard.test.ts
|
|
372
372
|
import { test, expect } from "bun:test";
|
|
373
373
|
import { PostCard } from "../../resources/views/PostCard.tsx";
|
|
@@ -383,7 +383,7 @@ test("renders the title and author", () => {
|
|
|
383
383
|
**Escaping is the test that matters.** Every view that renders user input has one
|
|
384
384
|
job beyond looking right, and it is the job that becomes a security incident:
|
|
385
385
|
|
|
386
|
-
```typescript
|
|
386
|
+
```typescript fragment
|
|
387
387
|
// tests/views/PostCard.test.ts
|
|
388
388
|
test("escapes markup in user-supplied text", () => {
|
|
389
389
|
const html = String(PostCard({ post: { title: "<script>alert(1)</script>" } }));
|
|
@@ -400,7 +400,7 @@ fails if someone later routes user input into it.
|
|
|
400
400
|
**Through a route, assert the rendered text** rather than the markup around it.
|
|
401
401
|
`assertSee` survives a class rename; a full-HTML comparison does not:
|
|
402
402
|
|
|
403
|
-
```typescript
|
|
403
|
+
```typescript fragment
|
|
404
404
|
// tests/http/posts.test.ts
|
|
405
405
|
const res = await app.get("/posts");
|
|
406
406
|
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zerotal/arch",
|
|
3
|
-
"version": "1.7.
|
|
3
|
+
"version": "1.7.5",
|
|
4
4
|
"license": "MIT",
|
|
5
|
-
"maturity": "
|
|
5
|
+
"maturity": "stable",
|
|
6
6
|
"private": false,
|
|
7
7
|
"type": "module",
|
|
8
8
|
"main": "./src/index.ts",
|
|
@@ -35,11 +35,11 @@
|
|
|
35
35
|
"typecheck": "tsc --noEmit"
|
|
36
36
|
},
|
|
37
37
|
"dependencies": {
|
|
38
|
-
"@zerotal/core": "1.7.
|
|
38
|
+
"@zerotal/core": "1.7.5"
|
|
39
39
|
},
|
|
40
40
|
"devDependencies": {
|
|
41
41
|
"typescript": "^5.8.0",
|
|
42
|
-
"@zerotal/orm": "1.7.
|
|
42
|
+
"@zerotal/orm": "1.7.5"
|
|
43
43
|
},
|
|
44
44
|
"description": "The Zerotal agent surface — an MCP server that hands coding agents the framework's machine-readable truth: exact API signatures, live routes and schema, version-matched docs, and `zt doctor`.",
|
|
45
45
|
"keywords": [
|
package/src/index.ts
CHANGED
|
@@ -27,7 +27,15 @@ export * from "./errors.ts";
|
|
|
27
27
|
// serve a subset, or add its own alongside them.
|
|
28
28
|
export { archTools, vendoredDocsDir } from "./tools/index.ts";
|
|
29
29
|
export type { ToolContext } from "./tools/index.ts";
|
|
30
|
+
// Subprocess plumbing, not API. `_probe.ts` carries the underscore this repo uses
|
|
31
|
+
// for a module nobody outside its package should reach for, nothing in the tree
|
|
32
|
+
// imports these from here, and the export block above says why the tools and the
|
|
33
|
+
// probe topics are public without ever saying why these are. Marked before the
|
|
34
|
+
// stable promise attaches, because withdrawing them afterwards is a breaking
|
|
35
|
+
// change made on behalf of a caller that does not exist.
|
|
36
|
+
/** @internal */
|
|
30
37
|
export { findApp, spawnProbe } from "./tools/_probe.ts";
|
|
38
|
+
/** @internal */
|
|
31
39
|
export type { ProbeResult, ProbeRunner, SpawnProbeOptions } from "./tools/_probe.ts";
|
|
32
40
|
|
|
33
41
|
// The in-app reads, for anyone wanting them without the subprocess.
|
|
@@ -45,12 +53,28 @@ export type {
|
|
|
45
53
|
SchemaReport,
|
|
46
54
|
} from "./probe/topics.ts";
|
|
47
55
|
|
|
48
|
-
// The
|
|
56
|
+
// The writers behind `arch:install`. Exported for the command and its tests to
|
|
57
|
+
// reach, not as API.
|
|
58
|
+
//
|
|
59
|
+
// They were public on the reasoning that "a project can generate the same files
|
|
60
|
+
// its own way" — a use nobody has, and a promise about the shape of `.mcp.json`
|
|
61
|
+
// writing, agent detection and marker fencing that would hold for the rest of
|
|
62
|
+
// the 1.x line. `ArchInstallCommand` is the only caller in the tree. Under `beta`
|
|
63
|
+
// that was a cheap bet; the stable label is what makes it expensive, so they are
|
|
64
|
+
// marked now rather than withdrawn later on behalf of a caller who never arrived.
|
|
65
|
+
/** @internal */
|
|
49
66
|
export { detectAgents } from "./install/detect.ts";
|
|
67
|
+
/** @internal */
|
|
50
68
|
export type { Detected, McpTarget } from "./install/detect.ts";
|
|
69
|
+
/** @internal */
|
|
51
70
|
export { applyMcpConfig, serverEntry, SERVER_ENTRY_PATH } from "./install/mcpConfig.ts";
|
|
71
|
+
/** @internal */
|
|
52
72
|
export type { ConfigOutcome } from "./install/mcpConfig.ts";
|
|
73
|
+
/** @internal */
|
|
53
74
|
export { applyBlock, fence, BLOCK_END, BLOCK_START } from "./install/markers.ts";
|
|
75
|
+
/** @internal */
|
|
54
76
|
export type { BlockOutcome } from "./install/markers.ts";
|
|
77
|
+
/** @internal */
|
|
55
78
|
export { agentsPreamble, buildGuidelines, claudeShim } from "./install/guidelines.ts";
|
|
79
|
+
/** @internal */
|
|
56
80
|
export type { GuidelineOptions } from "./install/guidelines.ts";
|
package/src/tools/searchDocs.ts
CHANGED
|
@@ -181,6 +181,13 @@ export function parsePage(path: string, raw: string): DocPage {
|
|
|
181
181
|
};
|
|
182
182
|
|
|
183
183
|
for (const line of body.split("\n")) {
|
|
184
|
+
// A fence delimiter is markup, and its info string is metadata about the
|
|
185
|
+
// block rather than anything the page says. Indexing them put the language
|
|
186
|
+
// name into every page that shows code — a search for "typescript" matched
|
|
187
|
+
// most of the corpus on the strength of its fences — and, once documentation
|
|
188
|
+
// examples grew a `fragment` marker, that word too.
|
|
189
|
+
if (/^\s*`{3,}/.test(line)) continue;
|
|
190
|
+
|
|
184
191
|
const match = /^(#{1,3})\s+(.*)$/.exec(line);
|
|
185
192
|
if (match) {
|
|
186
193
|
flush();
|
package/docs/client/auth.md
DELETED
|
@@ -1,113 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
title: Client Authentication
|
|
3
|
-
description: Bearer tokens, CSRF, and refreshing credentials on a 401.
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
# Authentication & CSRF
|
|
7
|
-
|
|
8
|
-
The client supports the two ways a browser app proves who it is, and the choice is
|
|
9
|
-
usually made for you by where the API lives:
|
|
10
|
-
|
|
11
|
-
- **Bearer tokens** suit APIs on another origin, mobile clients, and anything where
|
|
12
|
-
the caller holds a credential it can attach itself.
|
|
13
|
-
- **Session cookies** suit an API served from your own domain, where the browser
|
|
14
|
-
already carries the session and CSRF protection is the concern instead.
|
|
15
|
-
|
|
16
|
-
## Bearer tokens
|
|
17
|
-
|
|
18
|
-
Attach a bearer token (string or a resolver, sync or async) without writing an interceptor —
|
|
19
|
-
update it at runtime with `setToken()`:
|
|
20
|
-
|
|
21
|
-
```ts
|
|
22
|
-
// app/api/client.ts
|
|
23
|
-
const api = createApiClient<Routes>({
|
|
24
|
-
token: () => authStore.accessToken, // re-read on every request
|
|
25
|
-
});
|
|
26
|
-
api.setToken(freshToken); // or update imperatively
|
|
27
|
-
```
|
|
28
|
-
|
|
29
|
-
Prefer the resolver form. A plain string is captured once at construction, so a
|
|
30
|
-
token refreshed later never reaches the client; a function is consulted on every
|
|
31
|
-
request and always sees the current value.
|
|
32
|
-
|
|
33
|
-
Calling `setToken()` with no argument clears the token, which is what a logout
|
|
34
|
-
should do — otherwise the next request still carries the credential of the user who
|
|
35
|
-
just signed out.
|
|
36
|
-
|
|
37
|
-
> **Note** — The `token` is only applied when no `Authorization` header is already
|
|
38
|
-
> present on the request, so a per-request override always wins.
|
|
39
|
-
|
|
40
|
-
## Session cookies and CSRF
|
|
41
|
-
|
|
42
|
-
For session/cookie (SPA) auth, set `withCredentials` to send cookies, which also turns on CSRF:
|
|
43
|
-
the client reads the `XSRF-TOKEN` cookie and sends it as `X-XSRF-TOKEN` on mutating requests
|
|
44
|
-
(matching the session/CSRF middleware). Customize the names with `csrf`:
|
|
45
|
-
|
|
46
|
-
```ts
|
|
47
|
-
// app/api/client.ts
|
|
48
|
-
createApiClient<Routes>({
|
|
49
|
-
withCredentials: true, // credentials: 'include' + CSRF on
|
|
50
|
-
csrf: { cookie: "XSRF-TOKEN", header: "X-XSRF-TOKEN" }, // defaults shown
|
|
51
|
-
});
|
|
52
|
-
```
|
|
53
|
-
|
|
54
|
-
Enabling `withCredentials` turns CSRF on by default, so the two travel together and
|
|
55
|
-
neither needs configuring in the common case. Set `csrf: false` to opt out, or pass
|
|
56
|
-
an object to rename the cookie and header to match a server that uses different
|
|
57
|
-
ones.
|
|
58
|
-
|
|
59
|
-
The token is attached only to mutating requests — `POST`, `PUT`, `PATCH`, `DELETE`.
|
|
60
|
-
A `GET` is exempt because it should not change state, so it needs no protection
|
|
61
|
-
from being triggered cross-site. If a `GET` in your API does change something, that
|
|
62
|
-
is the thing to fix; adding a CSRF header to it would only hide the problem.
|
|
63
|
-
|
|
64
|
-
The header is skipped when the request already carries one, so a caller that sets
|
|
65
|
-
its own value keeps it.
|
|
66
|
-
|
|
67
|
-
## 401 / token refresh
|
|
68
|
-
|
|
69
|
-
`onUnauthorized` is called when any request receives a 401 response. It receives
|
|
70
|
-
the error and a `retry` function. Call `retry()` — optionally with header overrides
|
|
71
|
-
— to re-execute the failed request. The retry is limited to **one attempt**.
|
|
72
|
-
|
|
73
|
-
```ts
|
|
74
|
-
// app/api/client.ts
|
|
75
|
-
const api = createApiClient<Routes>({
|
|
76
|
-
baseUrl: "https://api.example.com",
|
|
77
|
-
|
|
78
|
-
onUnauthorized: async (err, retry) => {
|
|
79
|
-
const newToken = await authStore.refresh();
|
|
80
|
-
return retry({ Authorization: `Bearer ${newToken}` });
|
|
81
|
-
},
|
|
82
|
-
});
|
|
83
|
-
```
|
|
84
|
-
|
|
85
|
-
If `onUnauthorized` is not provided or does not call `retry`, the 401 error is
|
|
86
|
-
thrown normally.
|
|
87
|
-
|
|
88
|
-
The single-attempt limit is deliberate: a refresh that itself returns 401 would
|
|
89
|
-
otherwise retry forever, turning an expired session into an endless loop of
|
|
90
|
-
requests. When the retry also fails, the error is thrown and the app can send the
|
|
91
|
-
user to the login screen.
|
|
92
|
-
|
|
93
|
-
One case the hook does not solve on its own is a page that fires several requests
|
|
94
|
-
at once. Each 401 calls `onUnauthorized` separately, so a naive handler triggers
|
|
95
|
-
several concurrent refreshes and the losers of that race may invalidate the winner's
|
|
96
|
-
token. Have the refresh itself de-duplicate — cache the in-flight promise in your
|
|
97
|
-
auth store and hand the same one to every caller until it settles:
|
|
98
|
-
|
|
99
|
-
```ts
|
|
100
|
-
// app/api/authStore.ts
|
|
101
|
-
let inflight: Promise<string> | null = null;
|
|
102
|
-
|
|
103
|
-
export function refresh(): Promise<string> {
|
|
104
|
-
inflight ??= requestNewToken().finally(() => (inflight = null));
|
|
105
|
-
return inflight;
|
|
106
|
-
}
|
|
107
|
-
```
|
|
108
|
-
|
|
109
|
-
## Next steps
|
|
110
|
-
|
|
111
|
-
- [Client overview](/docs/client) — the guide's front page and the rest of the sections.
|
|
112
|
-
- [Error handling](/docs/client/errors) — the errors a rejected request throws.
|
|
113
|
-
- [CSRF protection](/docs/csrf) — the server side of the cookie and header pair.
|
package/docs/client/errors.md
DELETED
|
@@ -1,139 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
title: Client Error Handling
|
|
3
|
-
description: What a failed request throws, and how to tell the failure modes apart.
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
# Error handling
|
|
7
|
-
|
|
8
|
-
Non-2xx responses throw `ApiClientError`:
|
|
9
|
-
|
|
10
|
-
```ts
|
|
11
|
-
// in any frontend module
|
|
12
|
-
import { ApiClientError } from "@zerotal/client";
|
|
13
|
-
|
|
14
|
-
try {
|
|
15
|
-
await api.post("/api/users", { name: "", email: "bad" });
|
|
16
|
-
} catch (err) {
|
|
17
|
-
if (err instanceof ApiClientError) {
|
|
18
|
-
console.log(err.status); // 422
|
|
19
|
-
console.log(err.statusText); // 'Unprocessable Entity'
|
|
20
|
-
console.log(err.body); // raw response text (the error message truncates it to 200 chars)
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
```
|
|
24
|
-
|
|
25
|
-
## Telling the failure modes apart
|
|
26
|
-
|
|
27
|
-
Two very different things can go wrong, and only one of them produces an
|
|
28
|
-
`ApiClientError`:
|
|
29
|
-
|
|
30
|
-
| What happened | What is thrown |
|
|
31
|
-
| -------------------------------- | ----------------------------------- |
|
|
32
|
-
| The server answered with non-2xx | `ApiClientError` |
|
|
33
|
-
| A 422 in the validator's shape | `ValidationError` |
|
|
34
|
-
| The circuit breaker is open | `CircuitBreakerOpenError` |
|
|
35
|
-
| No answer at all | The platform's own error, unwrapped |
|
|
36
|
-
|
|
37
|
-
That last row is the one worth internalising. A DNS failure, a dropped connection,
|
|
38
|
-
a CORS rejection, or an aborted request never reaches the point where a status
|
|
39
|
-
exists, so `fetch` rejects with its own error and the client passes it through
|
|
40
|
-
untouched. An `instanceof ApiClientError` check therefore does _not_ catch an
|
|
41
|
-
offline user — and a `catch` block that assumes `err.status` exists throws a second
|
|
42
|
-
error while handling the first.
|
|
43
|
-
|
|
44
|
-
```ts
|
|
45
|
-
try {
|
|
46
|
-
await api.get("/api/users");
|
|
47
|
-
} catch (err) {
|
|
48
|
-
if (err instanceof ValidationError) showFieldErrors(err.errors);
|
|
49
|
-
else if (err instanceof ApiClientError) showStatus(err.status);
|
|
50
|
-
else showOffline(); // no response: network, CORS, timeout, or abort
|
|
51
|
-
}
|
|
52
|
-
```
|
|
53
|
-
|
|
54
|
-
Timeouts and cancellations land in that final branch too, since both abort the
|
|
55
|
-
request rather than producing a response.
|
|
56
|
-
|
|
57
|
-
## Reading response headers
|
|
58
|
-
|
|
59
|
-
`ApiClientError` carries the response headers when there were any, which is where
|
|
60
|
-
rate limiters and throttles put the information you need to react well:
|
|
61
|
-
|
|
62
|
-
```ts
|
|
63
|
-
// in any frontend module
|
|
64
|
-
if (err instanceof ApiClientError && err.status === 429) {
|
|
65
|
-
const waitMs = err.retryAfterMs; // parsed Retry-After, or null
|
|
66
|
-
if (waitMs !== null) scheduleRetry(waitMs);
|
|
67
|
-
console.log(err.headers?.get("X-RateLimit-Remaining"));
|
|
68
|
-
}
|
|
69
|
-
```
|
|
70
|
-
|
|
71
|
-
`retryAfterMs` handles both forms the header takes — a delta in seconds and an
|
|
72
|
-
HTTP-date — and returns milliseconds, or `null` when the header is absent or
|
|
73
|
-
cannot be parsed.
|
|
74
|
-
|
|
75
|
-
## Global handlers
|
|
76
|
-
|
|
77
|
-
The `onError` callback fires for every non-2xx response before the error is thrown.
|
|
78
|
-
Use it for global side-effects (toasts, logging) without needing try/catch at every
|
|
79
|
-
call site:
|
|
80
|
-
|
|
81
|
-
```ts
|
|
82
|
-
// app/api/client.ts
|
|
83
|
-
const api = createApiClient<Routes>({
|
|
84
|
-
baseUrl: "https://api.example.com",
|
|
85
|
-
onError: (err) => {
|
|
86
|
-
toast.error(`${err.status}: ${err.statusText}`);
|
|
87
|
-
logger.error("api_error", { status: err.status, body: err.body });
|
|
88
|
-
},
|
|
89
|
-
});
|
|
90
|
-
```
|
|
91
|
-
|
|
92
|
-
> **Warning** — `onError` fires for every non-2xx error including 401, even when `onUnauthorized` is also configured. To suppress the global error callback for 401 during token refresh, guard by status inside `onError`.
|
|
93
|
-
|
|
94
|
-
Because `onError` only ever sees responses, it does not report the network failures
|
|
95
|
-
described above. Reporting that should also cover "the request never arrived"
|
|
96
|
-
belongs in the caller, or in a wrapper around it.
|
|
97
|
-
|
|
98
|
-
### Typed validation errors
|
|
99
|
-
|
|
100
|
-
A `422` response whose body matches the framework's validation shape (`{ message, errors }`,
|
|
101
|
-
as produced by [`@zerotal/validator`](/docs/validator)) throws a `ValidationError` — an
|
|
102
|
-
`ApiClientError` subclass with the field errors already parsed:
|
|
103
|
-
|
|
104
|
-
```ts
|
|
105
|
-
// in any frontend module
|
|
106
|
-
import { ValidationError } from "@zerotal/client";
|
|
107
|
-
|
|
108
|
-
try {
|
|
109
|
-
await api.post("/api/users", form);
|
|
110
|
-
} catch (err) {
|
|
111
|
-
if (err instanceof ValidationError) {
|
|
112
|
-
setFieldErrors(err.errors); // { email: ["…"], password: ["…"] }
|
|
113
|
-
err.has("email"); // boolean
|
|
114
|
-
err.first("email"); // first message, or undefined
|
|
115
|
-
err.fields(); // ["email", "password"]
|
|
116
|
-
err.validationMessage; // "The given data was invalid."
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
```
|
|
120
|
-
|
|
121
|
-
Check for `ValidationError` before `ApiClientError`. It is a subclass, so the
|
|
122
|
-
broader check also matches it and would swallow the parsed field errors.
|
|
123
|
-
|
|
124
|
-
A 422 whose body does not match that shape stays a plain `ApiClientError`, so an
|
|
125
|
-
endpoint returning its own error format still surfaces as an ordinary failure
|
|
126
|
-
rather than quietly producing an empty `errors` object.
|
|
127
|
-
|
|
128
|
-
`onForbidden` is the 403 counterpart of `onUnauthorized`:
|
|
129
|
-
|
|
130
|
-
```ts
|
|
131
|
-
// app/api/client.ts
|
|
132
|
-
createApiClient<Routes>({ onForbidden: () => router.push("/403") });
|
|
133
|
-
```
|
|
134
|
-
|
|
135
|
-
## Next steps
|
|
136
|
-
|
|
137
|
-
- [Client overview](/docs/client) — the guide's front page and the rest of the sections.
|
|
138
|
-
- [Resilience](/docs/client/resilience) — retries, timeouts, and the circuit breaker.
|
|
139
|
-
- [Authentication](/docs/client/auth) — the 401 refresh hook.
|