create-daloy 1.0.0-beta.3 → 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.
- package/package.json +2 -2
- package/sbom.cdx.json +9 -9
- package/sbom.spdx.json +5 -5
- package/templates/bun-basic/AGENTS.md +17 -7
- package/templates/bun-basic/_agents/skills/daloyjs-best-practices/SKILL.md +50 -13
- package/templates/bun-basic/package.json +1 -1
- package/templates/cloudflare-worker/AGENTS.md +17 -6
- package/templates/cloudflare-worker/_agents/skills/daloyjs-best-practices/SKILL.md +45 -8
- package/templates/cloudflare-worker/package.json +1 -1
- package/templates/deno-basic/AGENTS.md +17 -7
- package/templates/deno-basic/_agents/skills/daloyjs-best-practices/SKILL.md +46 -9
- package/templates/deno-basic/deno.json +5 -5
- package/templates/node-basic/AGENTS.md +20 -8
- package/templates/node-basic/_agents/skills/daloyjs-best-practices/SKILL.md +50 -13
- package/templates/node-basic/package.json +2 -2
- package/templates/vercel/AGENTS.md +25 -13
- package/templates/vercel/_agents/skills/daloyjs-best-practices/SKILL.md +44 -7
- package/templates/vercel/package.json +1 -1
|
@@ -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
|
|
6
|
-
middleware, or error handling;
|
|
7
|
-
|
|
8
|
-
|
|
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. **
|
|
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. **
|
|
113
|
-
8. **
|
|
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
|
-
|
|
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.
|
|
14
|
-
"@daloyjs/core/banner": "jsr:@daloyjs/daloy@^1.0.0-beta.
|
|
15
|
-
"@daloyjs/core/contract": "jsr:@daloyjs/daloy@^1.0.0-beta.
|
|
16
|
-
"@daloyjs/core/deno": "jsr:@daloyjs/daloy@^1.0.0-beta.
|
|
17
|
-
"@daloyjs/core/openapi": "jsr:@daloyjs/daloy@^1.0.0-beta.
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
46
|
-
7.
|
|
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):
|
|
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
|
|
6
|
-
|
|
7
|
-
|
|
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. **
|
|
42
|
-
|
|
43
|
-
|
|
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
|
-
|
|
83
|
-
|
|
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. **
|
|
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
|
-
|
|
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());
|
|
206
|
-
app.use(secureHeaders());
|
|
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": "
|
|
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.
|
|
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**
|
|
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.
|
|
19
|
-
-
|
|
20
|
-
- `
|
|
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
|
|
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
|
|
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.
|
|
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.
|
|
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):
|
|
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()
|
|
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
|
|
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
|
|
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.
|