create-daloy 1.0.0-beta.4 → 1.0.0-beta.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.
@@ -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.5",
14
+ "@daloyjs/core/banner": "jsr:@daloyjs/daloy@^1.0.0-beta.5/banner",
15
+ "@daloyjs/core/contract": "jsr:@daloyjs/daloy@^1.0.0-beta.5/contract",
16
+ "@daloyjs/core/deno": "jsr:@daloyjs/daloy@^1.0.0-beta.5/deno",
17
+ "@daloyjs/core/openapi": "jsr:@daloyjs/daloy@^1.0.0-beta.5/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.5",
24
24
  "zod": "^4.4.3"
25
25
  },
26
26
  "devDependencies": {
@@ -1,23 +1,33 @@
1
1
  # AGENTS.md
2
2
 
3
- A [DaloyJS](https://daloyjs.dev) REST API deployed to **Vercel** on the **Node.js runtime** (Vercel's recommended runtime for standalone functions, running on Fluid Compute). **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 deployed to **Vercel** on the **Node.js runtime**. **Contract-first**: routes use validation schemas (Zod here; DaloyJS also supports Standard Schema-compatible validators) and generate OpenAPI 3.1. With `docs: true`, DaloyJS auto-mounts `GET /openapi.json`, `GET /openapi.yaml`, and `GET /docs` (Scalar UI).
4
4
 
5
5
  - Package manager: pnpm (use `pnpm` unless the project's `package.json` was rewritten for npm/yarn/bun).
6
6
  - Runtime: Vercel Node.js Functions on Fluid Compute (Web Standard `Request`/`Response`).
7
7
 
8
+ ## Agent guidance
9
+
10
+ - Treat this file as the short, durable project contract for AI coding agents.
11
+ - Use `.agents/skills/daloyjs-best-practices/SKILL.md` for the detailed DaloyJS workflow; keep this file concise and do not duplicate that skill.
12
+ - If instructions conflict, follow the user's latest prompt first, then the nearest `AGENTS.md`, then the skill.
13
+ - Change route definitions, schemas, metadata, and tests first; regenerate generated files instead of hand-editing OpenAPI output.
14
+
8
15
  ## Commands
9
16
 
10
17
  - `pnpm dev` — local Node dev server (`src/dev.ts`) on http://localhost:3000 (no `vercel dev` / login needed; serves the same app the Vercel Function runs)
11
18
  - `pnpm typecheck` — `tsc --noEmit`
12
19
  - `pnpm test` — run test suite
20
+ - `pnpm contract` — run `daloy inspect --check api/index.ts`
21
+ - `pnpm hooks:install` — enable the optional pre-push contract gate
13
22
  - `pnpm deploy` — deploy to Vercel
14
23
  - `pnpm audit` — supply-chain audit
15
24
 
16
25
  ## Project shape
17
26
 
18
- - `api/index.ts` — the single Vercel Node.js Functions entrypoint. Builds the `App`, registers routes/middleware, and exports `default toFetchHandler(app)` from `@daloyjs/core/vercel` (Node.js Functions expect a default export with a `fetch` method; Node.js is the default runtime, so no `runtime` export is needed). `vercel.json` rewrites every path (`/(.*)` `/api`) to this one function, so DaloyJS owns all routing and the app's routes are served at the site root (`/healthz`, `/docs`, …), not under `/api/*`. If you specifically need the Edge runtime, add `export const runtime = "edge"` and switch to `default toWebHandler(app)`.
19
- - `vercel.json` Vercel config. The `rewrites` rule routing all paths to `/api` is **required** for root routing; do not remove it. (`functions` sets memory / maxDuration.)
20
- - `src/dev.ts` — local Node dev server (`pnpm dev`). Imports the `app` exported from `api/index.ts` and serves it via `@daloyjs/core/node` at the root, so you get fast local iteration without `vercel dev`. Dev-only; it is not under `api/`, so Vercel never deploys it.
27
+ - `api/index.ts` — the single Vercel Node.js Functions entrypoint. Export `default toFetchHandler(app)` from `@daloyjs/core/vercel`; add `runtime = "edge"` and switch to `toWebHandler(app)` only when the user asks for Edge.
28
+ - This template is not a Next.js App Router project. Do not add `app/api` routes, `next.config.*`, or Next-specific file structure unless the user asks to convert or embed the API in a Next.js app.
29
+ - `vercel.json` — routes all paths to `/api` so DaloyJS owns root routing; do not remove this rewrite.
30
+ - `src/dev.ts` — local Node dev server (`pnpm dev`) for fast iteration without `vercel dev`. Dev-only; Vercel does not deploy it.
21
31
  - `tests/` — test files.
22
32
 
23
33
  ## Imports
@@ -28,34 +38,36 @@ This project uses TypeScript with `"allowImportingTsExtensions"`, so relative im
28
38
  import handler from "../api/index.ts";
29
39
  ```
30
40
 
31
- You import the file you see. Vercel bundles the `api/` functions at deploy time and resolves `.ts` directly, and the test runner (tsx) does too. Bare-specifier imports from packages (`@daloyjs/core`, `zod`, ) do not need an extension.
41
+ You import the file you see. Vercel and tsx resolve `.ts` directly. Bare package imports (`@daloyjs/core`, `zod`, ...) need no extension.
32
42
 
33
43
  ## Core rules
34
44
 
35
45
  1. The route definition is the contract. Method, path, request schemas, and response schemas live in one place — `app.route({...})`.
36
- 2. Validate every input with Zod. Use `.strict()` on top-level object schemas to reject unknown keys at the boundary.
46
+ 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.
37
47
  3. Preserve literal types in responses: `status: 200 as const`, `z.literal(...)` on discriminator fields.
38
48
  4. Throw typed errors (`NotFoundError`, `BadRequestError`, etc.) from `@daloyjs/core`.
39
49
  5. Keep `requestId()`, `secureHeaders()`, and `rateLimit()` enabled. For production traffic, back rate-limiting with Vercel KV or another shared store (the in-memory limiter resets per instance).
40
- 6. On the Node.js runtime the full Node API is available (`node:*`, `Buffer`, `fs`), but prefer Web Standards (`Request`/`Response`, `fetch`, Web Crypto) so the same app can also run on the Edge runtime or another adapter unchanged. If you opt into the Edge runtime, drop `node:` modules entirely.
50
+ 6. Prefer Web Standards (`Request`/`Response`, `fetch`, `Web Crypto`) even though Node APIs are available; Edge runtime code must avoid `node:` modules.
41
51
  7. Keep a single `api/index.ts` entry and the `vercel.json` `/(.*)` → `/api` rewrite so DaloyJS handles all routing at the site root.
42
- 8. Every new route ships with a test that covers a happy path and at least one unhappy path.
52
+ 8. Keep operation IDs stable and examples schema-valid; `pnpm contract` must pass after route, metadata, or OpenAPI-facing changes.
53
+ 9. Every new route ships with a test that covers a happy path and at least one unhappy path.
43
54
 
44
55
  ## Secure-by-default (do not let an AI strip these)
45
56
 
46
- 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.**
57
+ 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.**
47
58
 
48
- - Keep `secureHeaders()`, `requestId()`, `rateLimit()` registered, and `bodyLimitBytes` / `requestTimeoutMs` set on `new App({...})`. For production, back the limiter with Vercel KV **in addition to** the in-memory limiter (which resets per instance).
49
- - 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.
50
- - 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.
59
+ - Keep `secureHeaders()`, `requestId()`, `rateLimit()`, `bodyLimitBytes`, and `requestTimeoutMs`. For production, back rate limits with Vercel KV or another shared store.
60
+ - 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.
61
+ - Every protected route attaches auth `beforeHandle` and tests unauthenticated `401` plus wrong-scope `403`.
51
62
  - JWT verifiers keep an explicit `algorithms` allowlist; never trust the token's `alg` header, never allow `none`, always check `exp` / `nbf`.
52
- - Credential / HMAC comparisons use a constant-time comparison (the framework's `timingSafeEqual`), never `===`. Throw typed errors from `@daloyjs/core` so problem+json redacts in prod; never return raw stack traces.
63
+ - Credential / HMAC comparisons use constant-time comparison, never `===`. Throw typed errors so problem+json redacts in prod.
53
64
  - Keep the single `api/index.ts` entry and the `vercel.json` rewrite so DaloyJS owns routing — do not split into per-path files that bypass the middleware chain, and do not remove the rewrite (the root domain would 404).
54
65
  - `.env`, `.env.local`, secrets, private keys: never commit. Use `vercel env` for production secrets.
55
66
 
56
67
  ## Process expectations
57
68
 
58
69
  - Quality gates must pass before declaring work done: `pnpm typecheck` and `pnpm test`.
70
+ - Run the contract gate (`pnpm contract`) whenever route shapes, examples, operation IDs, or OpenAPI metadata change.
59
71
  - Bug fixes include a regression test.
60
72
  - For deploys, ensure the user has run `vercel login`; do not authenticate on their behalf.
61
73
  - Never bypass safety checks without a clear reason.