create-daloy 0.1.18 → 0.1.19

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.
@@ -0,0 +1,298 @@
1
+ # SKILL.md — DaloyJS best practices (Node)
2
+
3
+ Operational guidance and best practices for AI coding agents working in this
4
+ DaloyJS Node.js project. This is the project's **single source of truth** for
5
+ how to add routes, write tests, ship secure defaults, and run the quality
6
+ gates. Read this in full before making non-trivial changes.
7
+
8
+ ## When to use this skill
9
+
10
+ Use this skill when you need to:
11
+
12
+ - Add, modify, or remove HTTP routes in this project.
13
+ - Regenerate the OpenAPI spec or the typed Hey API SDK in `generated/`.
14
+ - Wire up new middleware, validation, or error handling.
15
+ - Add or update tests, run typecheck, or build the project.
16
+ - Harden the API (auth, CORS, rate limits, secrets, dependency hygiene).
17
+
18
+ Do **not** use this skill for tasks unrelated to the API itself (infra-only
19
+ changes, unrelated docs sites, etc.).
20
+
21
+ ## Core principles
22
+
23
+ DaloyJS is a **contract-first** framework. Internalize these rules — every
24
+ recommendation below follows from them:
25
+
26
+ 1. **The route definition is the contract.** Method, path, request schemas,
27
+ and response schemas live in one place (`app.route({...})`). The OpenAPI
28
+ spec, the typed client, and the runtime validation are all derived from
29
+ it. Never duplicate that information by hand-writing fetch calls, types,
30
+ or `openapi.json` entries.
31
+ 2. **Zod schemas validate at every boundary.** Body, params, query, and
32
+ headers go through Zod. If a field is not in the schema, it is not part
33
+ of the contract.
34
+ 3. **Preserve literal types.** Return `status: 200 as const` and use
35
+ `z.literal(...)` / `as const` on discriminator fields. The typed client
36
+ needs narrow types to do useful response narrowing.
37
+ 4. **`buildApp()` is pure.** Construction never opens sockets or reads
38
+ stateful resources. The HTTP listener lives in a separate file. This
39
+ lets codegen, tests, and tooling import `buildApp()` without side
40
+ effects.
41
+ 5. **Secure by default.** `requestId()`, `secureHeaders()`, and
42
+ `rateLimit()` are registered before route definitions. Do not remove
43
+ them unless the user explicitly asks.
44
+
45
+ ## Project shape
46
+
47
+ - `src/build-app.ts` — exports `buildApp()`. All routes and middleware are
48
+ registered here. **Pure function, no side effects.**
49
+ - `src/index.ts` — calls `buildApp()` and starts the Node HTTP listener via
50
+ `@daloyjs/core/node`. This is the only file allowed to open a port.
51
+ - `scripts/dump-openapi.ts` — imports `buildApp()` and writes
52
+ `generated/openapi.json`. Imports nothing that boots a server.
53
+ - `openapi-ts.config.ts` — Hey API config; reads `generated/openapi.json`
54
+ and writes `generated/client/`.
55
+ - `tests/` — Node test runner files (`*.test.ts`).
56
+ - `generated/` — **machine-written**. Never edit by hand; rerun `pnpm gen`.
57
+
58
+ ## Commands cheat-sheet
59
+
60
+ ```bash
61
+ pnpm dev # watch-mode dev server on http://localhost:3000
62
+ pnpm typecheck # tsc --noEmit
63
+ pnpm test # Node built-in test runner
64
+ pnpm gen # gen:openapi + gen:client
65
+ pnpm gen:openapi # write generated/openapi.json
66
+ pnpm gen:client # write generated/client/
67
+ pnpm build # emit dist/
68
+ pnpm audit # supply-chain audit
69
+ ```
70
+
71
+ Always run `pnpm typecheck` and `pnpm test` before declaring a task done.
72
+ If a change touches route shapes, also run `pnpm gen` so the client stays
73
+ in sync.
74
+
75
+ ## Workflow: add a new route
76
+
77
+ Follow these steps in order. Skipping any of them is a common source of
78
+ bugs (drifted client SDK, missing test, broken codegen).
79
+
80
+ 1. **Open `src/build-app.ts`.** Routes are registered on the `app`
81
+ instance returned by `new App({...})`.
82
+ 2. **Design the schemas first.** Define request body / params / query /
83
+ headers and a response body per status code. Reuse schemas — co-locate
84
+ them in `src/build-app.ts` or extract a `src/schemas/*.ts` module if
85
+ they grow. Prefer `z.object({...}).strict()` for inputs so unknown
86
+ keys are rejected at the boundary.
87
+ 3. **Call `app.route({...})`.** Required keys: `method`, `path`,
88
+ `operationId`, `tags`, `responses`, `handler`. Add `request` when the
89
+ route accepts input.
90
+ 4. **Return `{ status, body, headers? }` from the handler.** Always use
91
+ `status: 200 as const` (or whatever code) so the typed client can
92
+ narrow. For literal discriminators in `body`, use `as const` or
93
+ `z.literal(...)` in the schema.
94
+ 5. **Throw typed errors, do not return raw error responses.** Use
95
+ `NotFoundError`, `BadRequestError`, `UnauthorizedError`,
96
+ `ForbiddenError`, `ConflictError`, etc. from `@daloyjs/core`. The
97
+ framework maps them to RFC 7807 problem responses.
98
+ 6. **Add a test in `tests/<route>.test.ts`.** Use `app.request(...)` for
99
+ in-process tests — no port needed (see "Testing best practices").
100
+ 7. **Regenerate the contract.** Run `pnpm gen`. Inspect
101
+ `generated/openapi.json` to confirm the operation shows up with the
102
+ expected schemas and status codes.
103
+ 8. **Run the quality gates.** `pnpm typecheck && pnpm test`.
104
+
105
+ ### Example: a typed route
106
+
107
+ ```ts
108
+ import { z } from "zod";
109
+ import { NotFoundError } from "@daloyjs/core";
110
+
111
+ const Book = z.object({ id: z.string(), title: z.string() }).strict();
112
+ const BookParams = z.object({ id: z.string().min(1) }).strict();
113
+
114
+ app.route({
115
+ method: "GET",
116
+ path: "/books/:id",
117
+ operationId: "getBookById",
118
+ tags: ["Books"],
119
+ request: { params: BookParams },
120
+ responses: {
121
+ 200: { description: "Found", body: Book },
122
+ 404: { description: "Not found" },
123
+ },
124
+ handler: async ({ params }) => {
125
+ const book = await store.find(params.id);
126
+ if (!book) throw new NotFoundError(`Book ${params.id} not found`);
127
+ return { status: 200 as const, body: book };
128
+ },
129
+ });
130
+ ```
131
+
132
+ ## Validation & schema conventions
133
+
134
+ - **Inputs**: use `.strict()` on top-level object schemas to reject unknown
135
+ keys at the API boundary. This blocks mass-assignment-style attacks and
136
+ catches typos in clients early.
137
+ - **IDs**: prefer `z.string().min(1)` over `z.string()`. Use
138
+ `z.string().uuid()` / `z.string().regex(...)` when the shape is known.
139
+ - **Numbers from query strings**: use `z.coerce.number().int().min(...)`
140
+ because query params arrive as strings.
141
+ - **Optional vs nullable**: `.optional()` for "may be absent",
142
+ `.nullable()` for "explicitly null". They are not interchangeable in
143
+ OpenAPI.
144
+ - **Pagination**: standardize on `{ items, nextCursor }` cursor pagination
145
+ unless the user asks otherwise. Offset pagination invites large skips.
146
+ - **Discriminated unions** (e.g. for response variants): use
147
+ `z.discriminatedUnion("kind", [...])` and tag each branch with
148
+ `z.literal("...")` so codegen produces a narrow TypeScript union.
149
+ - **Never** call `JSON.parse` or `req.body` directly in a handler. Let the
150
+ framework validate via the schema and read the typed object passed to
151
+ the handler.
152
+
153
+ ## Error handling
154
+
155
+ - Throw typed errors from `@daloyjs/core` — they carry status codes and
156
+ serialize to RFC 7807 problem responses (`application/problem+json`).
157
+ - Add a `responses[code]` entry for every error you throw, so the OpenAPI
158
+ spec and the typed client know it can happen.
159
+ - Do not swallow errors in handlers. If you need to log and rethrow, use
160
+ `ctx.log.error(err, "context")` and rethrow.
161
+ - For unexpected errors, let them bubble. The framework's error middleware
162
+ will convert them into a generic 500 problem response and log them with
163
+ the request ID for correlation.
164
+
165
+ ## Middleware
166
+
167
+ Register middleware **before** route definitions inside `buildApp()`.
168
+ Order matters — earlier middleware wraps later middleware and routes.
169
+
170
+ Keep these as the secure baseline:
171
+
172
+ ```ts
173
+ app.use(requestId()); // x-request-id for log correlation
174
+ app.use(secureHeaders()); // strict security headers
175
+ app.use(rateLimit({ windowMs: 60_000, max: 120 }));
176
+ ```
177
+
178
+ Add CORS only when the API is consumed by browsers from a different
179
+ origin, and always pin `origin` to an allowlist — never `*` for an API
180
+ that accepts credentials.
181
+
182
+ Custom middleware should be small, well-typed, and call `await next()`
183
+ exactly once. Wrap it in `try { await next() } finally { ... }` if it
184
+ needs to run code after the handler.
185
+
186
+ ## Testing best practices
187
+
188
+ Tests live under `tests/` and run with `node --test` (Node's built-in
189
+ runner via `tsx`). Use **in-process** requests through `app.request()` —
190
+ no HTTP server, no port flakiness, no teardown.
191
+
192
+ ```ts
193
+ import { test } from "node:test";
194
+ import assert from "node:assert/strict";
195
+ import { buildApp } from "../src/build-app.ts";
196
+
197
+ test("GET /healthz returns ok", async () => {
198
+ const app = buildApp();
199
+ const res = await app.request("/healthz");
200
+ assert.equal(res.status, 200);
201
+ const body = await res.json();
202
+ assert.equal(body.ok, true);
203
+ assert.ok(typeof body.uptime === "number");
204
+ });
205
+ ```
206
+
207
+ Cover both **happy paths** and **unhappy paths** for every route:
208
+
209
+ - Happy path: valid input → expected `status` and `body`.
210
+ - Validation failure: missing/invalid fields → `400` with problem details.
211
+ - Auth failure (when applicable): unauthenticated → `401`; wrong scope →
212
+ `403`.
213
+ - Not found: unknown id → `404`.
214
+ - Conflict: duplicate create → `409`.
215
+ - Rate limit: hammer the route → `429` after the configured threshold.
216
+
217
+ For routes that touch external services, write a thin in-memory fake
218
+ inside the test and inject it via the factory pattern (`buildApp({ store })`).
219
+ Do not mock global `fetch` unless there is no alternative.
220
+
221
+ Aim for **100% line and function coverage** on routes you add. If a
222
+ branch is impractical to test (e.g. defensive `never` arms), refactor it
223
+ out rather than adding ignore comments — agent coverage tools may not
224
+ honor them.
225
+
226
+ ## Security best practices
227
+
228
+ - Keep `secureHeaders()`, `requestId()`, and `rateLimit()` enabled. They
229
+ ship the OWASP-recommended baseline.
230
+ - Never log secrets. Filter `authorization`, `cookie`, and any header /
231
+ body field that may contain tokens before logging.
232
+ - Read secrets from `process.env`, validated through a Zod schema at
233
+ boot. Fail fast on missing config rather than at request time.
234
+ - For auth, prefer a small JWT or session middleware over rolling your
235
+ own. Verify signatures against an allowlist of keys, never trust the
236
+ `alg` header from the token, and always check `exp` / `nbf`.
237
+ - Validate redirects against an allowlist. Open redirects are an OWASP
238
+ Top-10 issue.
239
+ - Limit body sizes via `bodyLimitBytes` on `new App({...})` — large
240
+ payloads are a cheap denial-of-service vector.
241
+ - Set `requestTimeoutMs` so slow clients cannot tie up workers.
242
+ - For database access, use parameterized queries / a query builder. Never
243
+ interpolate user input into SQL strings.
244
+ - Review `pnpm audit` output before releases. Avoid lowering the
245
+ install cooldown without reason; new package versions can be malicious.
246
+
247
+ ## Logging & observability
248
+
249
+ - The default logger emits structured JSON in production and pretty logs
250
+ in development. Use it via the handler context: `await handler(ctx)` →
251
+ `ctx.log.info({ userId }, "message")`.
252
+ - Always include the request id in log lines automatically emitted by
253
+ the framework. When you add your own logs, the request id is on
254
+ `ctx.requestId` and on the bound child logger.
255
+ - For tracing, the `tracing()` middleware (from `@daloyjs/core`) emits
256
+ OpenTelemetry-compatible spans. Enable it once the user wires up an
257
+ exporter.
258
+
259
+ ## Configuration & secrets
260
+
261
+ - Centralize config parsing in one module (e.g. `src/config.ts`) that
262
+ validates `process.env` via a Zod schema and exports a typed `config`
263
+ object.
264
+ - `.env.example` documents required variables; `.env` is gitignored.
265
+ - Treat config as immutable at runtime — read it once at startup.
266
+
267
+ ## Pitfalls and guardrails
268
+
269
+ - Never import `@daloyjs/core/node` (or any adapter that boots a server)
270
+ from `src/build-app.ts` or any script under `scripts/`. That would
271
+ start an HTTP listener as a side effect of codegen.
272
+ - Do not edit files under `generated/` by hand — they are overwritten by
273
+ `pnpm gen`.
274
+ - Do not weaken response literal types (`as const`); the typed client
275
+ depends on them.
276
+ - Do not return errors as `{ status: 4xx, body: {...} }`. Throw a typed
277
+ error so the framework can format the problem response consistently.
278
+ - Do not add runtime dependencies without checking the hardened `.npmrc` (installs wait 24h after publish by default).
279
+ - Do not bypass safety checks like `--no-verify` on commit or
280
+ `--ignore-scripts=false` on install without a clear reason.
281
+ - Avoid global mutable state in `buildApp()`. If you need shared state,
282
+ pass it in as a parameter (`buildApp({ store })`).
283
+
284
+ ## Process expectations
285
+
286
+ - Every new feature must include happy-path **and** unhappy-path tests.
287
+ - Bug fixes include a regression test.
288
+ - Quality gates (`pnpm typecheck`, `pnpm test`) must pass before
289
+ declaring the task complete.
290
+ - When route shapes change, also run `pnpm gen` and commit the updated
291
+ `generated/openapi.json` + client.
292
+ - Keep `README.md`, this `SKILL.md`, and `AGENTS.md` consistent with the
293
+ code. If you add a workflow, document it here.
294
+
295
+ ## More
296
+
297
+ - Framework docs: <https://daloyjs.dev/docs>
298
+ - Issues: <https://github.com/daloyjs/daloy/issues>
@@ -1,6 +1,6 @@
1
1
  # AGENTS.md
2
2
 
3
- A [DaloyJS](https://daloyjs.dev) REST API deployed to **Vercel Edge**. Contract-first: routes are defined with Zod schemas and OpenAPI 3.1 is generated from them.
3
+ A [DaloyJS](https://daloyjs.dev) REST API deployed to **Vercel Edge**. **Contract-first**: routes are defined with Zod schemas and OpenAPI 3.1 is generated from them.
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 Edge (Web Standard `Request`/`Response`).
@@ -8,13 +8,33 @@ A [DaloyJS](https://daloyjs.dev) REST API deployed to **Vercel Edge**. Contract-
8
8
  ## Commands
9
9
 
10
10
  - `pnpm dev` — local Vercel dev server on http://localhost:3000
11
- - `pnpm typecheck`
12
- - `pnpm test`
11
+ - `pnpm typecheck` — `tsc --noEmit`
12
+ - `pnpm test` — run test suite
13
13
  - `pnpm deploy` — deploy to Vercel
14
+ - `pnpm audit` — supply-chain audit
14
15
 
15
- ## Structure hints
16
+ ## Project shape
16
17
 
17
- - The Edge entrypoint is `api/[...path].ts` — a catch-all that delegates to DaloyJS via `toEdgeHandler(app)` from `@daloyjs/core/vercel`. Keep it as a catch-all so DaloyJS owns routing.
18
- - Build the `App` inside that file (or import a factory it uses); the file must export `default toEdgeHandler(app)` and `export const config = { runtime: "edge" }`.
18
+ - `api/[...path].ts` — Edge entrypoint. Builds the `App`, registers routes/middleware, and exports `default toEdgeHandler(app)` plus `export const config = { runtime: "edge" }`. **Keep it a catch-all** so DaloyJS owns routing.
19
+ - `vercel.json` Vercel build/runtime configuration.
20
+ - `tests/` — test files.
19
21
 
20
- For workflows (adding routes, regenerating the SDK, common pitfalls) see [SKILL.md](SKILL.md).
22
+ ## Core rules
23
+
24
+ 1. The route definition is the contract. Method, path, request schemas, and response schemas live in one place — `app.route({...})`.
25
+ 2. Validate every input with Zod. Use `.strict()` on top-level object schemas to reject unknown keys at the boundary.
26
+ 3. Preserve literal types in responses: `status: 200 as const`, `z.literal(...)` on discriminator fields.
27
+ 4. Throw typed errors (`NotFoundError`, `BadRequestError`, etc.) from `@daloyjs/core`.
28
+ 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).
29
+ 6. Stay on the Edge runtime: only Web Standards APIs. No `node:` modules, no `fs`, no `Buffer`. If a feature requires Node, switch to a Node-runtime template.
30
+ 7. The catch-all `api/[...path].ts` must remain a catch-all so DaloyJS handles routing.
31
+ 8. Every new route ships with a test that covers a happy path and at least one unhappy path.
32
+
33
+ ## Process expectations
34
+
35
+ - Quality gates must pass before declaring work done: `pnpm typecheck` and `pnpm test`.
36
+ - Bug fixes include a regression test.
37
+ - For deploys, ensure the user has run `vercel login`; do not authenticate on their behalf.
38
+ - Never bypass safety checks without a clear reason.
39
+
40
+ For the full workflow — adding routes step-by-step, schema conventions, testing patterns, security guidance, and deployment notes — read [.agents/skills/daloyjs-best-practices/SKILL.md](.agents/skills/daloyjs-best-practices/SKILL.md).
@@ -41,3 +41,14 @@ export default toEdgeHandler(app);
41
41
  ```
42
42
 
43
43
  That catch-all API route lets DaloyJS own routing while Vercel handles the Edge runtime.
44
+
45
+ ## What's included
46
+
47
+ - `@daloyjs/core/vercel` with starter security middleware: `secureHeaders` and `requestId`.
48
+ - Smaller edge-friendly body and timeout limits in the generated app.
49
+ <!-- daloy-minimal:strip-start books -->
50
+ - A health route and a contract-first `/books/:id` route with Zod validation.
51
+ <!-- daloy-minimal:strip-end books -->
52
+ <!-- daloy-minimal:strip-start docs -->
53
+ - Swagger UI at `/docs` and a live OpenAPI 3.1 document at `/openapi.json`.
54
+ <!-- daloy-minimal:strip-end docs -->
@@ -0,0 +1,204 @@
1
+ # SKILL.md — DaloyJS best practices (Vercel Edge)
2
+
3
+ Operational guidance and best practices for AI coding agents working in this
4
+ DaloyJS **Vercel Edge** project. This is the project's **single source of
5
+ truth** for how to add routes, write tests, ship secure defaults, and run
6
+ the quality gates. Read this in full before making non-trivial changes.
7
+
8
+ ## When to use this skill
9
+
10
+ Use this skill when you need to:
11
+
12
+ - Add, modify, or remove HTTP routes in this project.
13
+ - Adjust middleware, validation, or error handling.
14
+ - Run tests or typecheck the project.
15
+ - Deploy or troubleshoot the Edge runtime entrypoint.
16
+ - Harden the API (auth, CORS, rate limits, secrets, dependency hygiene).
17
+
18
+ Do **not** use this skill for tasks unrelated to the API itself.
19
+
20
+ ## Core principles
21
+
22
+ DaloyJS is a **contract-first** framework. On Vercel Edge, additionally:
23
+
24
+ 1. **Stay on the Edge runtime.** Only Web Standards APIs (no `node:`
25
+ modules, no `fs`, no `Buffer`). If a feature requires Node APIs, the
26
+ user must switch to a Node template.
27
+ 2. **The route definition is the contract.** Method, path, request
28
+ schemas, and response schemas live in one place (`app.route({...})`).
29
+ 3. **Zod schemas validate at every boundary.**
30
+ 4. **Preserve literal types.** Return `status: 200 as const`.
31
+ 5. **Secure by default.** `requestId()`, `secureHeaders()`, and
32
+ `rateLimit()` are registered before route definitions. Note the
33
+ in-memory rate limiter resets per instance — for high-traffic
34
+ deployments, prefer Vercel's native rate-limiting (e.g.
35
+ `@vercel/edge` + KV) or an external store.
36
+ 6. **One catch-all entrypoint.** `api/[...path].ts` owns all routing so
37
+ DaloyJS can generate a unified OpenAPI spec.
38
+
39
+ ## Project shape
40
+
41
+ - `api/[...path].ts` — the Edge entrypoint. Builds the `App`, registers
42
+ routes/middleware, and exports `default toEdgeHandler(app)` plus
43
+ `export const config = { runtime: "edge" }`.
44
+ - `vercel.json` — Vercel build/runtime configuration.
45
+ - `tests/` — test files (`*.test.ts`).
46
+
47
+ ## Commands cheat-sheet
48
+
49
+ ```bash
50
+ pnpm dev # local Vercel dev server on http://localhost:3000
51
+ pnpm typecheck # tsc --noEmit
52
+ pnpm test # run test suite
53
+ pnpm deploy # deploy to Vercel
54
+ pnpm audit # supply-chain audit
55
+ ```
56
+
57
+ Always run `pnpm typecheck` and `pnpm test` before declaring a task done.
58
+
59
+ ## Workflow: add a new route
60
+
61
+ 1. **Open `api/[...path].ts`.**
62
+ 2. **Design schemas first.** Use `z.object({...}).strict()` for inputs.
63
+ 3. **Call `app.route({...})`** with `method`, `path`, `operationId`,
64
+ `tags`, `responses`, `handler` (plus `request` when accepting input).
65
+ 4. **Return `{ status, body, headers? }`** with `status: 200 as const`.
66
+ 5. **Throw typed errors** (`NotFoundError`, `BadRequestError`, etc.)
67
+ from `@daloyjs/core`.
68
+ 6. **Add a test** under `tests/` using in-process `app.request(...)`.
69
+ 7. **Run the quality gates**: `pnpm typecheck && pnpm test`.
70
+
71
+ ### Example: a typed route
72
+
73
+ ```ts
74
+ import { z } from "zod";
75
+ import { NotFoundError } from "@daloyjs/core";
76
+
77
+ const Book = z.object({ id: z.string(), title: z.string() }).strict();
78
+ const BookParams = z.object({ id: z.string().min(1) }).strict();
79
+
80
+ app.route({
81
+ method: "GET",
82
+ path: "/books/:id",
83
+ operationId: "getBookById",
84
+ tags: ["Books"],
85
+ request: { params: BookParams },
86
+ responses: {
87
+ 200: { description: "Found", body: Book },
88
+ 404: { description: "Not found" },
89
+ },
90
+ handler: async ({ params }) => {
91
+ const book = await store.find(params.id);
92
+ if (!book) throw new NotFoundError(`Book ${params.id} not found`);
93
+ return { status: 200 as const, body: book };
94
+ },
95
+ });
96
+ ```
97
+
98
+ ## Validation & schema conventions
99
+
100
+ - **Inputs**: use `.strict()` on top-level object schemas.
101
+ - **IDs**: prefer `z.string().min(1)`; use `z.string().uuid()` when
102
+ applicable.
103
+ - **Numbers from query strings**: `z.coerce.number().int().min(...)`.
104
+ - **Optional vs nullable**: differ in OpenAPI output.
105
+ - **Pagination**: standardize on `{ items, nextCursor }` cursor
106
+ pagination.
107
+ - **Discriminated unions**: `z.discriminatedUnion("kind", [...])`.
108
+
109
+ ## Error handling
110
+
111
+ - Throw typed errors from `@daloyjs/core` — they serialize to RFC 7807
112
+ problem responses.
113
+ - Add a `responses[code]` entry for every error you throw.
114
+
115
+ ## Middleware
116
+
117
+ Register middleware **before** route definitions. Order matters.
118
+
119
+ Keep the secure baseline (`requestId`, `secureHeaders`, `rateLimit`).
120
+ Add CORS only when needed, with an explicit `origin` allowlist.
121
+
122
+ ## Testing best practices
123
+
124
+ Tests use in-process `app.request(...)` — no port, no Edge runtime
125
+ needed for unit tests.
126
+
127
+ ```ts
128
+ import { test } from "node:test";
129
+ import assert from "node:assert/strict";
130
+ import handler from "../api/[...path].ts";
131
+
132
+ // Either import the underlying app, or test via the Edge handler's
133
+ // fetch interface by passing a Web Request.
134
+ test("GET /healthz returns ok", async () => {
135
+ const res = await handler(new Request("http://local/healthz"));
136
+ assert.equal(res.status, 200);
137
+ });
138
+ ```
139
+
140
+ Cover **happy paths and unhappy paths** for every route: valid input,
141
+ validation failures (400), auth failures (401/403), not-found (404),
142
+ conflict (409), rate limiting (429). For external services, inject an
143
+ in-memory fake during tests.
144
+
145
+ Aim for **100% line and function coverage** on the routes you add.
146
+
147
+ ## Security best practices
148
+
149
+ - Keep `secureHeaders()`, `requestId()`, and `rateLimit()` enabled. For
150
+ production traffic, back rate-limiting with Vercel KV or another
151
+ shared store so limits apply across instances.
152
+ - Never log secrets — filter `authorization`, `cookie`, etc.
153
+ - Read secrets from `process.env` (available on Edge). Validate via Zod
154
+ at module load.
155
+ - For auth, verify JWT signatures with the Web Crypto API
156
+ (`crypto.subtle`). Never trust the `alg` header from the token.
157
+ - Validate redirects against an allowlist.
158
+ - Set `bodyLimitBytes` and `requestTimeoutMs` on `new App({...})` to
159
+ mitigate DoS.
160
+ - Edge functions have small bundle and CPU limits; be cautious about
161
+ adding heavy dependencies. Inspect bundle size during deploy.
162
+ - Pin Vercel project settings (regions, runtime version) explicitly in
163
+ `vercel.json` rather than relying on dashboard defaults.
164
+
165
+ ## Logging & observability
166
+
167
+ - Use `ctx.log` — it carries the request id.
168
+ - `console.log` on Edge shows up in Vercel's runtime logs; the framework
169
+ logger emits structured JSON for log aggregators.
170
+
171
+ ## Configuration & secrets
172
+
173
+ - Use Vercel project env vars; mirror required names in `.env.example`.
174
+ - Validate `process.env` via a Zod schema at module load.
175
+
176
+ ## Pitfalls and guardrails
177
+
178
+ - The catch-all `api/[...path].ts` must remain a catch-all so DaloyJS
179
+ handles routing. Do not split routes into multiple Vercel API files
180
+ unless the user explicitly asks (it disables shared middleware and a
181
+ unified OpenAPI).
182
+ - Use `toEdgeHandler(app)` from `@daloyjs/core/vercel` — never hand-roll
183
+ a `fetch(req)` adapter.
184
+ - Do not import `@daloyjs/core/node`, `@daloyjs/core/bun`, etc. — only
185
+ `@daloyjs/core` and `@daloyjs/core/vercel`.
186
+ - Avoid Node-only APIs (`Buffer`, `fs`, full `process` API). If a
187
+ feature needs Node, switch to a Node-runtime template.
188
+ - Do not weaken response literal types (`as const`).
189
+ - Do not return errors as `{ status: 4xx, body }`. Throw a typed error.
190
+ - Do not add runtime dependencies without checking the hardened `.npmrc` (installs wait 24h after publish by default).
191
+
192
+ ## Process expectations
193
+
194
+ - Every new feature ships with happy-path and unhappy-path tests.
195
+ - Bug fixes include a regression test.
196
+ - `pnpm typecheck` and `pnpm test` must pass before completion.
197
+ - For deploys, ensure the user is logged in via `vercel login`; do not
198
+ authenticate on their behalf.
199
+ - Keep `README.md`, this `SKILL.md`, and `AGENTS.md` consistent.
200
+
201
+ ## More
202
+
203
+ - Framework docs: <https://daloyjs.dev/docs>
204
+ - Issues: <https://github.com/daloyjs/daloy/issues>
@@ -1,68 +0,0 @@
1
- # SKILL.md
2
-
3
- Operational guidance for AI coding agents working in this DaloyJS Bun project.
4
-
5
- ## When to use this skill
6
-
7
- Use this skill when you need to:
8
-
9
- - Add, modify, or remove HTTP routes in this project.
10
- - Regenerate the OpenAPI spec or the typed Hey API SDK in `generated/`.
11
- - Wire up new middleware, validation, or error handling.
12
- - Run tests or typecheck the project under Bun.
13
-
14
- Do **not** use this skill for tasks unrelated to the API itself.
15
-
16
- ## Project shape
17
-
18
- - `src/build-app.ts` — exports `buildApp()`. All routes and middleware are registered here. **Pure function, no side effects.**
19
- - `src/index.ts` — calls `buildApp()` and starts the Bun HTTP listener via `@daloyjs/core/bun`.
20
- - `scripts/dump-openapi.ts` — imports `buildApp()` and writes `generated/openapi.json`.
21
- - `openapi-ts.config.ts` — Hey API config; reads `generated/openapi.json` and writes `generated/client/`.
22
- - `tests/` — Bun test files (`*.test.ts`).
23
-
24
- ## Core workflows
25
-
26
- ### Add a new route
27
-
28
- 1. Open `src/build-app.ts`.
29
- 2. Define request/response Zod schemas.
30
- 3. Call `app.route({ method, path, request, response, handler })`. Return `{ status, body }`; preserve literal types (`status: 200 as const`, `ok: true as const`) so codegen sees narrow types.
31
- 4. Add a test under `tests/`.
32
- 5. Regenerate the spec + client: `bun run gen:openapi && bun run gen:client`.
33
-
34
- ### Regenerate the OpenAPI spec and SDK
35
-
36
- ```bash
37
- bun run gen:openapi # → generated/openapi.json
38
- bun run gen:client # → generated/client/
39
- ```
40
-
41
- If `gen:openapi` fails with a "server already listening" error, you imported `src/index.ts` (or `@daloyjs/core/bun`) from a place that codegen pulls in. Codegen must only touch `buildApp()`.
42
-
43
- ### Run quality gates
44
-
45
- ```bash
46
- bun run typecheck
47
- bun test
48
- ```
49
-
50
- Both must pass before considering a change done.
51
-
52
- ## Decision rules
53
-
54
- - If the user asks to **change API behavior**, edit `src/build-app.ts`, then regenerate and run `bun test`.
55
- - If the user asks to **consume the API from a client**, import from `generated/client/` — do not handwrite fetch calls.
56
- - If the user asks for **new middleware**, register it on `app` inside `buildApp()` before route definitions so it applies globally.
57
-
58
- ## Pitfalls and guardrails
59
-
60
- - Never import `@daloyjs/core/bun` from `src/build-app.ts` or any script under `scripts/`. That would start an HTTP listener as a side effect of codegen.
61
- - Do not edit files under `generated/` by hand — they are overwritten by codegen.
62
- - Keep `secureHeaders()`, `requestId()`, and `rateLimit()` enabled in `buildApp()` unless the user explicitly asks to remove them.
63
- - Do not weaken response literal types (`as const`); the typed client depends on them.
64
-
65
- ## More
66
-
67
- - Framework docs: <https://daloyjs.dev/docs>
68
- - Issues: <https://github.com/daloyjs/daloy/issues>