create-daloy 1.0.0-beta.4 → 1.0.0-beta.6

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.
@@ -2,10 +2,10 @@
2
2
  name: daloyjs-best-practices
3
3
  description: >-
4
4
  Best practices for building, testing, and hardening this DaloyJS REST API on
5
- Cloudflare Workers. Use when adding or changing HTTP routes, Zod schemas,
6
- middleware, or error handling; wiring Worker bindings (KV, D1, R2, Queues,
7
- env, secrets); or working on auth, rate limits, and the project's quality
8
- gates.
5
+ Cloudflare Workers. Use when adding or changing HTTP routes, Zod/Standard
6
+ Schema validation schemas, middleware, route metadata, or error handling;
7
+ wiring Worker bindings (KV, D1, R2, Queues, env, secrets); running contract
8
+ gates; or working on auth, rate limits, and security defaults.
9
9
  license: MIT
10
10
  ---
11
11
 
@@ -34,11 +34,12 @@ Do **not** use this skill for tasks unrelated to the API itself.
34
34
  DaloyJS is a **contract-first** framework. On Workers, additionally:
35
35
 
36
36
  1. **Stay on the Workers runtime.** Only Web Standards APIs and
37
- Cloudflare-specific bindings. No `node:` modules unless the user
38
- explicitly adds `nodejs_compat` to `wrangler.toml` and opts in.
37
+ Cloudflare-specific bindings. No `node:` modules unless the user
38
+ explicitly adds `nodejs_compat` to `wrangler.toml` and opts in.
39
39
  2. **The route definition is the contract.** Method, path, request
40
40
  schemas, and response schemas live in one place (`app.route({...})`).
41
- 3. **Zod schemas validate at every boundary.**
41
+ 3. **Validation schemas protect every boundary.** This template uses Zod,
42
+ and Daloy accepts any Standard Schema-compatible library.
42
43
  4. **Preserve literal types.** Return `status: 200 as const`.
43
44
  5. **Secure by default.** `requestId()`, `secureHeaders()`, and
44
45
  `rateLimit()` are registered. Note: the in-memory rate limiter resets
@@ -46,6 +47,9 @@ DaloyJS is a **contract-first** framework. On Workers, additionally:
46
47
  rate-limit binding.
47
48
  6. **Bindings flow through `env`.** Read KV/D1/R2/secrets from the
48
49
  `env` argument to `fetch`, never from globals.
50
+ 7. **Contract gates are part of done.** Keep `operationId` values stable,
51
+ examples schema-valid, declared error responses accurate, and the
52
+ generated OpenAPI contract in sync with the live route table.
49
53
 
50
54
  ## Project shape
51
55
 
@@ -64,11 +68,14 @@ DaloyJS is a **contract-first** framework. On Workers, additionally:
64
68
  pnpm dev # wrangler dev on http://localhost:8787
65
69
  pnpm typecheck # tsc --noEmit
66
70
  pnpm test # run test suite
71
+ pnpm contract # daloy inspect --check src/index.ts
67
72
  pnpm deploy # wrangler deploy
68
73
  pnpm audit # supply-chain audit
69
74
  ```
70
75
 
71
76
  Always run `pnpm typecheck` and `pnpm test` before declaring a task done.
77
+ `pnpm test` includes the contract gate; if you need a focused contract
78
+ check, run `pnpm contract`.
72
79
 
73
80
  ## OpenAPI & docs routes
74
81
 
@@ -89,18 +96,33 @@ On Workers the Scalar UI adds the most weight; consider
89
96
  For hand-rolled mounting, `openapiToYAML` is exported from
90
97
  `@daloyjs/core/openapi`.
91
98
 
99
+ ## AI-ready contract metadata
100
+
101
+ Daloy can expose route metadata to OpenAPI and agent tooling. Add metadata
102
+ when it helps consumers understand or safely automate the route:
103
+
104
+ - Use `summary`, `description`, and `tags` for concise human-facing docs.
105
+ - Use `meta.examples` for realistic happy-path and unhappy-path examples.
106
+ Examples must match the declared schemas; the contract gate rejects drift.
107
+ - Use `meta.extensions` for stable `x-*` fields consumed by internal tools.
108
+ - Use `deprecated` and `sunset` when changing API lifecycle. Do not remove
109
+ a route or response shape silently if generated clients may depend on it.
110
+
92
111
  ## Workflow: add a new route
93
112
 
94
113
  1. **Open `src/index.ts`.**
95
114
  2. **Design schemas first.** Use `z.object({...}).strict()` for inputs.
96
115
  3. **Call `app.route({...})`** with `method`, `path`, `operationId`,
97
116
  `tags`, `responses`, `handler` (plus `request` when accepting input).
117
+ Add `meta` examples / descriptions when the route is user-facing or
118
+ consumed by agents.
98
119
  4. **Return `{ status, body, headers? }`** with `status: 200 as const`.
99
120
  5. **Throw typed errors** (`NotFoundError`, `BadRequestError`, etc.).
100
121
  6. **Add a test** under `tests/`. Use `app.request(...)` for pure logic;
101
122
  use `unstable_dev` (Wrangler) or `@cloudflare/vitest-pool-workers`
102
123
  when you need bindings.
103
- 7. **Run the quality gates**: `pnpm typecheck && pnpm test`.
124
+ 7. **Run the contract gate**: `pnpm contract` or `pnpm test`.
125
+ 8. **Run the quality gates**: `pnpm typecheck && pnpm test`.
104
126
 
105
127
  ### Example: a typed route with bindings
106
128
 
@@ -158,6 +180,8 @@ export default {
158
180
  - **Pagination**: standardize on `{ items, nextCursor }` cursor
159
181
  pagination.
160
182
  - **Discriminated unions**: `z.discriminatedUnion("kind", [...])`.
183
+ - Keep response examples close to the route definition and schema-valid.
184
+ The contract test intentionally fails invalid examples.
161
185
 
162
186
  ## Error handling
163
187
 
@@ -197,6 +221,8 @@ Cover **happy paths and unhappy paths** for every route: valid input,
197
221
  validation failures (400), auth failures (401/403), not-found (404),
198
222
  conflict (409), rate limiting (429). For external services, inject an
199
223
  in-memory fake into `buildApp(env)` during tests.
224
+ The shipped contract test should fail invalid examples, duplicate/missing
225
+ `operationId`, or missing responses.
200
226
 
201
227
  Aim for **100% line and function coverage** on the routes you add.
202
228
 
@@ -205,6 +231,10 @@ Aim for **100% line and function coverage** on the routes you add.
205
231
  - Keep `secureHeaders()`, `requestId()`, and `rateLimit()` enabled. For
206
232
  high-traffic routes, attach Cloudflare's native rate-limit binding so
207
233
  limits are shared across isolates.
234
+ - Never make a failing test pass by deleting or weakening a security guard.
235
+ If a guard blocks a legitimate route, add the narrowest per-route
236
+ override or configuration knob and cover both the allowed and rejected
237
+ paths in tests.
208
238
  - Never log secrets — filter `authorization`, `cookie`, etc.
209
239
  - Read secrets via `wrangler secret put`, never via plain `[vars]` in
210
240
  `wrangler.toml`.
@@ -213,6 +243,9 @@ Aim for **100% line and function coverage** on the routes you add.
213
243
  - Validate redirects against an allowlist.
214
244
  - Set `bodyLimitBytes` and `requestTimeoutMs` on `new App({...})` to
215
245
  mitigate DoS.
246
+ - For outbound HTTP, prefer `fetchGuard()` or a transport layered on top
247
+ of it when URLs can be influenced by users or tenants. SSRF protections
248
+ should fail closed for private ranges and cloud metadata endpoints.
216
249
  - Workers have CPU and bundle-size limits; be cautious about adding
217
250
  heavy dependencies. Run `wrangler deploy --dry-run --outdir=dist` to
218
251
  inspect bundle size.
@@ -242,6 +275,8 @@ Aim for **100% line and function coverage** on the routes you add.
242
275
  hand-roll a `fetch(req, env, ctx)` adapter.
243
276
  - Do not import `@daloyjs/core/node`, `@daloyjs/core/bun`, etc. — only
244
277
  `@daloyjs/core` and `@daloyjs/core/cloudflare`.
278
+ - Do not hand-edit OpenAPI paths or client types. Fix the route definition,
279
+ schema, or metadata and regenerate.
245
280
  - Avoid Node-only APIs (`Buffer`, `fs`, `process` beyond
246
281
  `process.env`) unless `nodejs_compat` is enabled and required.
247
282
  - Do not weaken response literal types (`as const`).
@@ -255,6 +290,8 @@ Aim for **100% line and function coverage** on the routes you add.
255
290
  - Every new feature ships with happy-path and unhappy-path tests.
256
291
  - Bug fixes include a regression test.
257
292
  - `pnpm typecheck` and `pnpm test` must pass before completion.
293
+ - When route metadata, examples, lifecycle flags, or operation IDs change,
294
+ run the contract gate and inspect the relevant generated OpenAPI diff.
258
295
  - For deploys, ask the user to run `wrangler login` first if needed —
259
296
  do not attempt to authenticate on their behalf.
260
297
  - Keep `README.md`, this `SKILL.md`, and `AGENTS.md` consistent.
@@ -13,7 +13,7 @@
13
13
  "hooks:install": "git config core.hooksPath .githooks"
14
14
  },
15
15
  "dependencies": {
16
- "@daloyjs/core": "^1.0.0-beta.4",
16
+ "@daloyjs/core": "^1.0.0-beta.6",
17
17
  "zod": "^4.4.3"
18
18
  },
19
19
  "devDependencies": {
@@ -1,15 +1,24 @@
1
1
  # AGENTS.md
2
2
 
3
- A [DaloyJS](https://daloyjs.dev) REST API for the [Deno](https://deno.com) runtime. **Contract-first**: routes are defined with Zod schemas and OpenAPI 3.1 is generated from them. When `docs: true` is set in `new App({...})`, three routes are auto-mounted: `GET /openapi.json`, `GET /openapi.yaml`, and `GET /docs` (Scalar UI).
3
+ A [DaloyJS](https://daloyjs.dev) REST API for the [Deno](https://deno.com) runtime. **Contract-first**: routes are defined with validation schemas (Zod in this template; DaloyJS also supports Standard Schema-compatible validators) and OpenAPI 3.1 is generated from them. When `docs: true` is set in `new App({...})`, three routes are auto-mounted: `GET /openapi.json`, `GET /openapi.yaml`, and `GET /docs` (Scalar UI).
4
4
 
5
5
  - Runtime: Deno (no Node package manager). DaloyJS loads from `jsr:` and third-party packages such as Zod load from `npm:` in `deno.json`.
6
6
 
7
+ ## Agent guidance
8
+
9
+ - Treat this file as the short, durable project contract for AI coding agents.
10
+ - Use `.agents/skills/daloyjs-best-practices/SKILL.md` for the detailed DaloyJS workflow; keep this file concise and do not duplicate that skill.
11
+ - If instructions conflict, follow the user's latest prompt first, then the nearest `AGENTS.md`, then the skill.
12
+ - Change route definitions, schemas, metadata, and tests first; regenerate generated files instead of hand-editing OpenAPI output.
13
+
7
14
  ## Commands
8
15
 
9
16
  - `deno task dev` — watch-mode server on http://localhost:3000
10
17
  - `deno task typecheck`
11
18
  - `deno task test`
12
19
  - `deno task gen:openapi` — write `generated/openapi.json`
20
+ - `deno task contract` — run the focused OpenAPI contract test
21
+ - `deno task hooks:install` — enable the optional pre-push contract gate
13
22
 
14
23
  The typed Hey API SDK is generated outside Deno (Hey API has no Deno entrypoint yet). Run `npx @hey-api/openapi-ts -i generated/openapi.json -o generated/client` if you need the client.
15
24
 
@@ -25,21 +34,22 @@ The typed Hey API SDK is generated outside Deno (Hey API has no Deno entrypoint
25
34
  ## Core rules
26
35
 
27
36
  1. The route definition is the contract. Method, path, request schemas, and response schemas live in one place — `app.route({...})`.
28
- 2. Validate every input with Zod. Use `.strict()` on top-level object schemas to reject unknown keys at the boundary.
37
+ 2. Validate every input with Zod or another Standard Schema-compatible validator. For Zod object schemas, use `.strict()` to reject unknown keys at the boundary.
29
38
  3. Preserve literal types in responses: `status: 200 as const`, `z.literal(...)` on discriminator fields.
30
39
  4. Throw typed errors (`NotFoundError`, `BadRequestError`, etc.) from `@daloyjs/core`.
31
40
  5. Keep `requestId()`, `secureHeaders()`, and `rateLimit()` enabled.
32
41
  6. Deno permissions are part of the contract — keep `--allow-net --allow-env --allow-read` narrow; never use `--allow-all`.
33
- 7. Every new route ships with a test that covers a happy path and at least one unhappy path.
34
- 8. After any route change: `deno task gen:openapi && deno task typecheck && deno task test`.
42
+ 7. Keep operation IDs stable and examples schema-valid; `deno task contract` must pass after route, metadata, or OpenAPI-facing changes.
43
+ 8. Every new route ships with a test that covers a happy path and at least one unhappy path.
44
+ 9. After any route change: `deno task gen:openapi && deno task contract && deno task typecheck && deno task test`.
35
45
 
36
46
  ## Secure-by-default (do not let an AI strip these)
37
47
 
38
- Per Supabase + Aikido on [secure-by-default development](https://www.aikido.dev/blog/supabase-approach-to-secure-by-default-development): *"If you tell an AI to make something work, it might remove the very security checks that protect you."* When a guard rejects a request, **satisfy it, do not delete it.**
48
+ Per Supabase + Aikido on [secure-by-default development](https://www.aikido.dev/blog/supabase-approach-to-secure-by-default-development): _"If you tell an AI to make something work, it might remove the very security checks that protect you."_ When a guard rejects a request, **satisfy it, do not delete it.**
39
49
 
40
50
  - Keep `secureHeaders()`, `requestId()`, `rateLimit()` registered, and `bodyLimitBytes` / `requestTimeoutMs` set on `new App({...})`. Tighten per-route; never raise globally to pass a test.
41
51
  - Keep Deno permissions narrow. Never add `--allow-all`; never broaden `--allow-net` / `--allow-read` / `--allow-env` to silence a prompt — add the specific host / path / var.
42
- - Keep Zod `.strict()` on top-level request objects; do not switch to `.passthrough()`. Keep `responses[N].body` schemas tight; never widen to `z.any()` to let a privileged field escape.
52
+ - Keep Zod `.strict()` on top-level request objects; do not switch to `.passthrough()`. For other validators, use the strict / no-extra-keys equivalent. Keep `responses[N].body` schemas tight; never widen to `z.any()` to let a privileged field escape.
43
53
  - Every protected route attaches an auth `beforeHandle` and ships an unhappy-path test proving an unauthenticated request returns `401` (and wrong scope returns `403`) — the HTTP-boundary equivalent of Supabase's pgTAP policy tests.
44
54
  - JWT verifiers keep an explicit `algorithms` allowlist; never trust the token's `alg` header, never allow `none`, always check `exp` / `nbf`.
45
55
  - Credential / HMAC comparisons use a constant-time comparison, never `===`. Throw typed errors from `@daloyjs/core` so problem+json redacts in prod; never return raw stack traces.
@@ -48,7 +58,7 @@ Per Supabase + Aikido on [secure-by-default development](https://www.aikido.dev/
48
58
  ## Process expectations
49
59
 
50
60
  - Quality gates must pass before declaring work done: `deno task typecheck` and `deno task test`.
51
- - Regenerate the OpenAPI spec whenever route shapes change.
61
+ - Regenerate the OpenAPI spec whenever route shapes change, then run `deno task contract`.
52
62
  - Bug fixes include a regression test.
53
63
  - Use `deno task ...`, not `npm`/`pnpm`. There is no `package.json` here.
54
64
 
@@ -2,10 +2,11 @@
2
2
  name: daloyjs-best-practices
3
3
  description: >-
4
4
  Best practices for building, testing, and hardening this DaloyJS REST API on
5
- the Deno runtime. Use when adding or changing HTTP routes, Zod schemas,
6
- middleware, or error handling; regenerating the OpenAPI spec; managing Deno
7
- permissions and tasks; or working on auth, rate limits, secrets, and the
8
- project's quality gates.
5
+ the Deno runtime. Use when adding or changing HTTP routes, Zod/Standard
6
+ Schema validation schemas, middleware, route metadata, or error handling;
7
+ regenerating the OpenAPI spec; running contract gates; managing Deno
8
+ permissions and tasks; or working on auth, rate limits, secrets, and
9
+ security defaults.
9
10
  license: MIT
10
11
  ---
11
12
 
@@ -35,7 +36,8 @@ DaloyJS is a **contract-first** framework. Internalize these rules:
35
36
 
36
37
  1. **The route definition is the contract.** Method, path, request schemas,
37
38
  and response schemas live in one place (`app.route({...})`).
38
- 2. **Zod schemas validate at every boundary.**
39
+ 2. **Validation schemas protect every boundary.** This template uses Zod,
40
+ and Daloy accepts any Standard Schema-compatible library.
39
41
  3. **Preserve literal types.** Return `status: 200 as const`; use
40
42
  `z.literal(...)` / `as const` on discriminator fields.
41
43
  4. **`buildApp()` is pure.** Construction never opens sockets. The HTTP
@@ -45,6 +47,9 @@ DaloyJS is a **contract-first** framework. Internalize these rules:
45
47
  6. **Deno permissions are part of the contract.** Tasks declare exactly
46
48
  the permissions they need (`--allow-net`, `--allow-env`, `--allow-read`).
47
49
  Do not broaden them casually.
50
+ 7. **Contract gates are part of done.** Keep `operationId` values stable,
51
+ examples schema-valid, declared error responses accurate, and generated
52
+ OpenAPI artifacts in sync with the live route table.
48
53
 
49
54
  ## Project shape
50
55
 
@@ -65,6 +70,7 @@ DaloyJS is a **contract-first** framework. Internalize these rules:
65
70
  deno task dev # watch-mode server on http://localhost:3000
66
71
  deno task typecheck # deno check
67
72
  deno task test # deno test
73
+ deno task contract # run the focused contract test
68
74
  deno task gen:openapi # write generated/openapi.json
69
75
  ```
70
76
 
@@ -76,7 +82,8 @@ npx @hey-api/openapi-ts -i generated/openapi.json -o generated/client
76
82
  ```
77
83
 
78
84
  Always run `deno task typecheck` and `deno task test` before declaring a
79
- task done.
85
+ task done. `deno task test` includes the contract gate; if you need a
86
+ focused contract check, run `deno task contract`.
80
87
 
81
88
  ## OpenAPI & docs routes
82
89
 
@@ -95,6 +102,18 @@ mount only outside production, or `docs: false` to disable all three.
95
102
  For hand-rolled mounting, `openapiToYAML` is exported from
96
103
  `@daloyjs/core/openapi`.
97
104
 
105
+ ## AI-ready contract metadata
106
+
107
+ Daloy can expose route metadata to OpenAPI and agent tooling. Add metadata
108
+ when it helps consumers understand or safely automate the route:
109
+
110
+ - Use `summary`, `description`, and `tags` for concise human-facing docs.
111
+ - Use `meta.examples` for realistic happy-path and unhappy-path examples.
112
+ Examples must match the declared schemas; the contract gate rejects drift.
113
+ - Use `meta.extensions` for stable `x-*` fields consumed by internal tools.
114
+ - Use `deprecated` and `sunset` when changing API lifecycle. Do not remove
115
+ a route or response shape silently if generated clients may depend on it.
116
+
98
117
  ## Workflow: add a new route
99
118
 
100
119
  1. **Open `src/build-app.ts`.**
@@ -103,14 +122,17 @@ For hand-rolled mounting, `openapiToYAML` is exported from
103
122
  inputs.
104
123
  3. **Call `app.route({...})`** with `method`, `path`, `operationId`,
105
124
  `tags`, `responses`, `handler` (plus `request` when accepting input).
125
+ Add `meta` examples / descriptions when the route is user-facing or
126
+ consumed by agents.
106
127
  4. **Return `{ status, body, headers? }` from the handler.** Always
107
128
  `status: 200 as const`.
108
129
  5. **Throw typed errors** (`NotFoundError`, `BadRequestError`, etc.) from
109
130
  `@daloyjs/core`.
110
131
  6. **Add a test in `tests/<route>.test.ts`** using `app.request(...)` for
111
132
  in-process tests.
112
- 7. **Regenerate the contract**: `deno task gen:openapi`.
113
- 8. **Run the quality gates**: `deno task typecheck && deno task test`.
133
+ 7. **Run the contract gate**: `deno task contract` or `deno task test`.
134
+ 8. **Regenerate the contract artifacts**: `deno task gen:openapi`.
135
+ 9. **Run the quality gates**: `deno task typecheck && deno task test`.
114
136
 
115
137
  ### Example: a typed route
116
138
 
@@ -149,6 +171,8 @@ app.route({
149
171
  - **Pagination**: standardize on `{ items, nextCursor }` cursor
150
172
  pagination.
151
173
  - **Discriminated unions**: `z.discriminatedUnion("kind", [...])`.
174
+ - Keep response examples close to the route definition and schema-valid.
175
+ The contract test intentionally fails invalid examples.
152
176
  - **Never** parse `req.body` directly — let the framework validate.
153
177
 
154
178
  ## Error handling
@@ -194,14 +218,20 @@ Cover **happy paths and unhappy paths**: valid input, validation failures
194
218
  (400), auth failures (401/403), not-found (404), conflict (409), rate
195
219
  limiting (429). For external services, inject an in-memory fake via
196
220
  `buildApp({ store })`.
221
+ The shipped contract test should fail invalid examples, duplicate/missing
222
+ `operationId`, or missing responses.
197
223
 
198
224
  Aim for **100% line and function coverage** on routes you add.
199
225
 
200
226
  ## Security best practices
201
227
 
202
228
  - Keep `secureHeaders()`, `requestId()`, and `rateLimit()` enabled.
229
+ - Never make a failing test pass by deleting or weakening a security guard.
230
+ If a guard blocks a legitimate route, add the narrowest per-route
231
+ override or configuration knob and cover both the allowed and rejected
232
+ paths in tests.
203
233
  - Permissions for the `dev` task are intentionally narrow: `--allow-net
204
- --allow-env --allow-read`. If a change requires more permissions, add
234
+ --allow-env --allow-read`. If a change requires more permissions, add
205
235
  them explicitly to the relevant task in `deno.json` and call it out to
206
236
  the user — never `--allow-all`.
207
237
  - Never log secrets — filter `authorization`, `cookie`, etc.
@@ -212,6 +242,9 @@ Aim for **100% line and function coverage** on routes you add.
212
242
  - Validate redirects against an allowlist.
213
243
  - Set `bodyLimitBytes` and `requestTimeoutMs` on `new App({...})` to
214
244
  mitigate DoS.
245
+ - For outbound HTTP, prefer `fetchGuard()` or a transport layered on top
246
+ of it when URLs can be influenced by users or tenants. SSRF protections
247
+ should fail closed for private ranges and cloud metadata endpoints.
215
248
  - Pin `npm:` and `jsr:` specifiers in `deno.json` to exact or
216
249
  caret-locked versions; review changes in `deno.lock` before committing.
217
250
 
@@ -231,6 +264,8 @@ Aim for **100% line and function coverage** on routes you add.
231
264
  - Never import `@daloyjs/core/deno` from `src/build-app.ts` or any
232
265
  script under `scripts/`. That would boot a listener during codegen.
233
266
  - Do not edit files under `generated/` by hand.
267
+ - Do not hand-edit OpenAPI paths or client types. Fix the route definition,
268
+ schema, or metadata and regenerate.
234
269
  - Do not weaken response literal types (`as const`).
235
270
  - Do not return errors as `{ status: 4xx, body }`. Throw a typed error.
236
271
  - Use `deno task ...`, not `npm`/`pnpm`. There is no `package.json`.
@@ -244,6 +279,8 @@ Aim for **100% line and function coverage** on routes you add.
244
279
  - `deno task typecheck` and `deno task test` must pass before completion.
245
280
  - Run `deno task gen:openapi` when route shapes change; commit the
246
281
  updated `generated/openapi.json`.
282
+ - When route metadata, examples, lifecycle flags, or operation IDs change,
283
+ run the contract gate and inspect the relevant generated OpenAPI diff.
247
284
  - Keep `README.md`, this `SKILL.md`, and `AGENTS.md` consistent with the
248
285
  code.
249
286
 
@@ -10,11 +10,11 @@
10
10
  "hooks:install": "git config core.hooksPath .githooks"
11
11
  },
12
12
  "imports": {
13
- "@daloyjs/core": "jsr:@daloyjs/daloy@^1.0.0-beta.4",
14
- "@daloyjs/core/banner": "jsr:@daloyjs/daloy@^1.0.0-beta.4/banner",
15
- "@daloyjs/core/contract": "jsr:@daloyjs/daloy@^1.0.0-beta.4/contract",
16
- "@daloyjs/core/deno": "jsr:@daloyjs/daloy@^1.0.0-beta.4/deno",
17
- "@daloyjs/core/openapi": "jsr:@daloyjs/daloy@^1.0.0-beta.4/openapi",
13
+ "@daloyjs/core": "jsr:@daloyjs/daloy@^1.0.0-beta.6",
14
+ "@daloyjs/core/banner": "jsr:@daloyjs/daloy@^1.0.0-beta.6/banner",
15
+ "@daloyjs/core/contract": "jsr:@daloyjs/daloy@^1.0.0-beta.6/contract",
16
+ "@daloyjs/core/deno": "jsr:@daloyjs/daloy@^1.0.0-beta.6/deno",
17
+ "@daloyjs/core/openapi": "jsr:@daloyjs/daloy@^1.0.0-beta.6/openapi",
18
18
  "zod": "npm:zod@^4.4.3"
19
19
  },
20
20
  "compilerOptions": {
@@ -1,12 +1,21 @@
1
1
  # AGENTS.md
2
2
 
3
3
  A [DaloyJS](https://daloyjs.dev) Node.js REST API. **Contract-first**:
4
- routes are defined with Zod schemas and OpenAPI 3.1 is generated from them.
4
+ routes are defined with validation schemas (Zod in this template; DaloyJS also
5
+ supports Standard Schema-compatible validators) and OpenAPI 3.1 is generated
6
+ from them.
5
7
  When `docs: true` is set in `new App({...})`, three routes are auto-mounted:
6
8
  `GET /openapi.json`, `GET /openapi.yaml`, and `GET /docs` (Scalar UI).
7
9
 
8
10
  - Package manager: pnpm (use `pnpm` unless the project's `package.json` was rewritten for npm/yarn/bun).
9
- - Runtime: Node.js >= 24.0.0 (active LTS).
11
+ - Runtime: Node.js 24 LTS or Node.js 26+ (`^24.0.0 || >=26.0.0`).
12
+
13
+ ## Agent guidance
14
+
15
+ - Treat this file as the short, durable project contract for AI coding agents.
16
+ - Use `.agents/skills/daloyjs-best-practices/SKILL.md` for the detailed DaloyJS workflow; keep this file concise and do not duplicate that skill.
17
+ - If instructions conflict, follow the user's latest prompt first, then the nearest `AGENTS.md`, then the skill.
18
+ - Change route definitions, schemas, metadata, and tests first; regenerate generated files instead of hand-editing OpenAPI or typed-client output.
10
19
 
11
20
  ## Commands
12
21
 
@@ -14,6 +23,8 @@ When `docs: true` is set in `new App({...})`, three routes are auto-mounted:
14
23
  - `pnpm typecheck` — `tsc --noEmit`
15
24
  - `pnpm test` — Node built-in test runner
16
25
  - `pnpm gen` — regenerate `generated/openapi.json` and the typed Hey API client
26
+ - `pnpm contract` — run `daloy inspect --check src/build-app.ts`
27
+ - `pnpm hooks:install` — enable the optional pre-push contract gate
17
28
  - `pnpm build` — emit `dist/`
18
29
  - `pnpm audit` — supply-chain audit (respects the hardened `.npmrc`)
19
30
 
@@ -38,19 +49,20 @@ You import the file you see. On `pnpm build`, TypeScript rewrites the `.ts` spec
38
49
  ## Core rules
39
50
 
40
51
  1. The route definition is the contract. Method, path, request schemas, and response schemas live in one place — `app.route({...})`.
41
- 2. Validate every input with Zod. Use `.strict()` on top-level object schemas to reject unknown keys at the boundary.
52
+ 2. Validate every input with Zod or another Standard Schema-compatible validator. For Zod object schemas, use `.strict()` to reject unknown keys at the boundary.
42
53
  3. Preserve literal types in responses: `status: 200 as const`, `z.literal(...)` on discriminator fields. Codegen depends on these.
43
54
  4. Throw typed errors (`NotFoundError`, `BadRequestError`, etc.) from `@daloyjs/core` — never return raw error responses.
44
55
  5. Keep `requestId()`, `secureHeaders()`, and `rateLimit()` enabled. They are the project's secure defaults.
45
- 6. Every new route ships with a test that covers a happy path and at least one unhappy path.
46
- 7. After any route change: `pnpm gen && pnpm typecheck && pnpm test`.
56
+ 6. Keep operation IDs stable and examples schema-valid; `pnpm contract` must pass after route, metadata, or OpenAPI-facing changes.
57
+ 7. Every new route ships with a test that covers a happy path and at least one unhappy path.
58
+ 8. After any route change: `pnpm gen && pnpm contract && pnpm typecheck && pnpm test`.
47
59
 
48
60
  ## Secure-by-default (do not let an AI strip these)
49
61
 
50
- Per Supabase + Aikido on [secure-by-default development](https://www.aikido.dev/blog/supabase-approach-to-secure-by-default-development): *"If you tell an AI to make something work, it might remove the very security checks that protect you."* When a guard rejects a request, **satisfy it, do not delete it.**
62
+ Per Supabase + Aikido on [secure-by-default development](https://www.aikido.dev/blog/supabase-approach-to-secure-by-default-development): _"If you tell an AI to make something work, it might remove the very security checks that protect you."_ When a guard rejects a request, **satisfy it, do not delete it.**
51
63
 
52
64
  - Keep `secureHeaders()`, `requestId()`, `rateLimit()` registered, and `bodyLimitBytes` / `requestTimeoutMs` set on `new App({...})`. Tighten per-route; never raise globally to pass a test.
53
- - Keep Zod `.strict()` on top-level request objects; do not switch to `.passthrough()`. Keep `responses[N].body` schemas tight; never widen to `z.any()` to let a privileged field escape.
65
+ - Keep Zod `.strict()` on top-level request objects; do not switch to `.passthrough()`. For other validators, use the strict / no-extra-keys equivalent. Keep `responses[N].body` schemas tight; never widen to `z.any()` to let a privileged field escape.
54
66
  - Every protected route attaches an auth `beforeHandle` and ships an unhappy-path test proving an unauthenticated request returns `401` (and wrong scope returns `403`) — the HTTP-boundary equivalent of Supabase's pgTAP policy tests.
55
67
  - JWT verifiers keep an explicit `algorithms` allowlist; never trust the token's `alg` header, never allow `none`, always check `exp` / `nbf`.
56
68
  - Credential / HMAC comparisons use `timingSafeEqual`, never `===`. Throw typed errors from `@daloyjs/core` so problem+json redacts in prod; never return raw stack traces.
@@ -60,7 +72,7 @@ Per Supabase + Aikido on [secure-by-default development](https://www.aikido.dev/
60
72
  ## Process expectations
61
73
 
62
74
  - Quality gates must pass before declaring work done: `pnpm typecheck` and `pnpm test`.
63
- - Update the OpenAPI spec and typed client whenever route shapes change (`pnpm gen`).
75
+ - Update the OpenAPI spec and typed client whenever route shapes change (`pnpm gen`) and run the contract gate (`pnpm contract`).
64
76
  - Bug fixes include a regression test.
65
77
  - Never bypass safety checks (`--no-verify`, `--ignore-scripts=false`) without a clear reason.
66
78
 
@@ -2,9 +2,10 @@
2
2
  name: daloyjs-best-practices
3
3
  description: >-
4
4
  Best practices for building, testing, and hardening this DaloyJS REST API on
5
- Node.js. Use when adding or changing HTTP routes, Zod schemas, middleware, or
6
- error handling; regenerating the OpenAPI spec or the typed Hey API client; or
7
- working on auth, rate limits, secrets, and the project's quality gates.
5
+ Node.js. Use when adding or changing HTTP routes, Zod/Standard Schema
6
+ validation schemas, middleware, route metadata, or error handling;
7
+ regenerating the OpenAPI spec or typed Hey API client; running contract
8
+ gates; or working on auth, rate limits, secrets, and security defaults.
8
9
  license: MIT
9
10
  ---
10
11
 
@@ -38,9 +39,10 @@ recommendation below follows from them:
38
39
  spec, the typed client, and the runtime validation are all derived from
39
40
  it. Never duplicate that information by hand-writing fetch calls, types,
40
41
  or `openapi.json` entries.
41
- 2. **Zod schemas validate at every boundary.** Body, params, query, and
42
- headers go through Zod. If a field is not in the schema, it is not part
43
- of the contract.
42
+ 2. **Validation schemas protect every boundary.** This template uses Zod,
43
+ and Daloy accepts any Standard Schema-compatible library. Body, params,
44
+ query, and headers go through the declared schema. If a field is not in
45
+ the schema, it is not part of the contract.
44
46
  3. **Preserve literal types.** Return `status: 200 as const` and use
45
47
  `z.literal(...)` / `as const` on discriminator fields. The typed client
46
48
  needs narrow types to do useful response narrowing.
@@ -51,6 +53,9 @@ recommendation below follows from them:
51
53
  5. **Secure by default.** `requestId()`, `secureHeaders()`, and
52
54
  `rateLimit()` are registered before route definitions. Do not remove
53
55
  them unless the user explicitly asks.
56
+ 6. **Contract gates are part of done.** Keep `operationId` values stable,
57
+ examples schema-valid, declared error responses accurate, and generated
58
+ OpenAPI / client artifacts in sync with the live route table.
54
59
 
55
60
  ## Project shape
56
61
 
@@ -74,13 +79,15 @@ pnpm test # Node built-in test runner
74
79
  pnpm gen # gen:openapi + gen:client
75
80
  pnpm gen:openapi # write generated/openapi.json
76
81
  pnpm gen:client # write generated/client/
82
+ pnpm contract # daloy inspect --check src/build-app.ts
77
83
  pnpm build # emit dist/
78
84
  pnpm audit # supply-chain audit
79
85
  ```
80
86
 
81
87
  Always run `pnpm typecheck` and `pnpm test` before declaring a task done.
82
- If a change touches route shapes, also run `pnpm gen` so the client stays
83
- in sync.
88
+ `pnpm test` includes the contract gate; if you need a focused contract
89
+ check, run `pnpm contract`. If a change touches route shapes, also run
90
+ `pnpm gen` so the OpenAPI spec and client stay in sync.
84
91
 
85
92
  ## OpenAPI & docs routes
86
93
 
@@ -104,6 +111,18 @@ exported from the openapi subpath:
104
111
  import { generateOpenAPI, openapiToYAML } from "@daloyjs/core/openapi";
105
112
  ```
106
113
 
114
+ ## AI-ready contract metadata
115
+
116
+ Daloy can expose route metadata to OpenAPI and agent tooling. Add metadata
117
+ when it helps consumers understand or safely automate the route:
118
+
119
+ - Use `summary`, `description`, and `tags` for concise human-facing docs.
120
+ - Use `meta.examples` for realistic happy-path and unhappy-path examples.
121
+ Examples must match the declared schemas; the contract gate rejects drift.
122
+ - Use `meta.extensions` for stable `x-*` fields consumed by internal tools.
123
+ - Use `deprecated` and `sunset` when changing API lifecycle. Do not remove
124
+ a route or response shape silently if generated clients may depend on it.
125
+
107
126
  ## Workflow: add a new route
108
127
 
109
128
  Follow these steps in order. Skipping any of them is a common source of
@@ -118,7 +137,8 @@ bugs (drifted client SDK, missing test, broken codegen).
118
137
  keys are rejected at the boundary.
119
138
  3. **Call `app.route({...})`.** Required keys: `method`, `path`,
120
139
  `operationId`, `tags`, `responses`, `handler`. Add `request` when the
121
- route accepts input.
140
+ route accepts input, and add `meta` examples / descriptions when the
141
+ route is user-facing or consumed by agents.
122
142
  4. **Return `{ status, body, headers? }` from the handler.** Always use
123
143
  `status: 200 as const` (or whatever code) so the typed client can
124
144
  narrow. For literal discriminators in `body`, use `as const` or
@@ -129,10 +149,12 @@ bugs (drifted client SDK, missing test, broken codegen).
129
149
  framework maps them to RFC 7807 problem responses.
130
150
  6. **Add a test in `tests/<route>.test.ts`.** Use `app.request(...)` for
131
151
  in-process tests — no port needed (see "Testing best practices").
132
- 7. **Regenerate the contract.** Run `pnpm gen`. Inspect
152
+ 7. **Run the contract gate.** Run `pnpm contract` or `pnpm test` before
153
+ trusting the OpenAPI output.
154
+ 8. **Regenerate the contract artifacts.** Run `pnpm gen`. Inspect
133
155
  `generated/openapi.json` to confirm the operation shows up with the
134
156
  expected schemas and status codes.
135
- 8. **Run the quality gates.** `pnpm typecheck && pnpm test`.
157
+ 9. **Run the quality gates.** `pnpm typecheck && pnpm test`.
136
158
 
137
159
  ### Example: a typed route
138
160
 
@@ -178,6 +200,8 @@ app.route({
178
200
  - **Discriminated unions** (e.g. for response variants): use
179
201
  `z.discriminatedUnion("kind", [...])` and tag each branch with
180
202
  `z.literal("...")` so codegen produces a narrow TypeScript union.
203
+ - Keep response examples close to the route definition and schema-valid.
204
+ The contract test intentionally fails invalid examples.
181
205
  - **Never** call `JSON.parse` or `req.body` directly in a handler. Let the
182
206
  framework validate via the schema and read the typed object passed to
183
207
  the handler.
@@ -202,8 +226,8 @@ Order matters — earlier middleware wraps later middleware and routes.
202
226
  Keep these as the secure baseline:
203
227
 
204
228
  ```ts
205
- app.use(requestId()); // x-request-id for log correlation
206
- app.use(secureHeaders()); // strict security headers
229
+ app.use(requestId()); // x-request-id for log correlation
230
+ app.use(secureHeaders()); // strict security headers
207
231
  app.use(rateLimit({ windowMs: 60_000, max: 120 }));
208
232
  ```
209
233
 
@@ -245,6 +269,8 @@ Cover both **happy paths** and **unhappy paths** for every route:
245
269
  - Not found: unknown id → `404`.
246
270
  - Conflict: duplicate create → `409`.
247
271
  - Rate limit: hammer the route → `429` after the configured threshold.
272
+ - Contract failure: invalid examples, duplicate/missing `operationId`, or
273
+ missing responses should fail through the shipped contract test.
248
274
 
249
275
  For routes that touch external services, write a thin in-memory fake
250
276
  inside the test and inject it via the factory pattern (`buildApp({ store })`).
@@ -259,6 +285,10 @@ honor them.
259
285
 
260
286
  - Keep `secureHeaders()`, `requestId()`, and `rateLimit()` enabled. They
261
287
  ship the OWASP-recommended baseline.
288
+ - Never make a failing test pass by deleting or weakening a security guard.
289
+ If a guard blocks a legitimate route, add the narrowest per-route
290
+ override or configuration knob and cover both the allowed and rejected
291
+ paths in tests.
262
292
  - Never log secrets. Filter `authorization`, `cookie`, and any header /
263
293
  body field that may contain tokens before logging.
264
294
  - Read secrets from `process.env`, validated through a Zod schema at
@@ -273,6 +303,9 @@ honor them.
273
303
  - Set `requestTimeoutMs` so slow clients cannot tie up workers.
274
304
  - For database access, use parameterized queries / a query builder. Never
275
305
  interpolate user input into SQL strings.
306
+ - For outbound HTTP, prefer `fetchGuard()` or a transport layered on top
307
+ of it when URLs can be influenced by users or tenants. SSRF protections
308
+ should fail closed for private ranges and cloud metadata endpoints.
276
309
  - Review `pnpm audit` output before releases. Avoid lowering the
277
310
  install cooldown without reason; new package versions can be malicious.
278
311
 
@@ -303,6 +336,8 @@ honor them.
303
336
  start an HTTP listener as a side effect of codegen.
304
337
  - Do not edit files under `generated/` by hand — they are overwritten by
305
338
  `pnpm gen`.
339
+ - Do not hand-edit OpenAPI paths or client types. Fix the route definition,
340
+ schema, or metadata and regenerate.
306
341
  - Do not weaken response literal types (`as const`); the typed client
307
342
  depends on them.
308
343
  - Do not return errors as `{ status: 4xx, body: {...} }`. Throw a typed
@@ -321,6 +356,8 @@ honor them.
321
356
  declaring the task complete.
322
357
  - When route shapes change, also run `pnpm gen` and commit the updated
323
358
  `generated/openapi.json` + client.
359
+ - When route metadata, examples, lifecycle flags, or operation IDs change,
360
+ run the contract gate and inspect the relevant generated OpenAPI diff.
324
361
  - Keep `README.md`, this `SKILL.md`, and `AGENTS.md` consistent with the
325
362
  code. If you add a workflow, document it here.
326
363
 
@@ -4,7 +4,7 @@
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "engines": {
7
- "node": ">=24.0.0"
7
+ "node": "^24.0.0 || >=26.0.0"
8
8
  },
9
9
  "scripts": {
10
10
  "dev": "daloy dev",
@@ -20,7 +20,7 @@
20
20
  "hooks:install": "git config core.hooksPath .githooks"
21
21
  },
22
22
  "dependencies": {
23
- "@daloyjs/core": "^1.0.0-beta.4",
23
+ "@daloyjs/core": "^1.0.0-beta.6",
24
24
  "zod": "^4.4.3"
25
25
  },
26
26
  "devDependencies": {