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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -17,6 +17,9 @@ pnpm create daloy@latest my-api
17
17
  npm create daloy@latest my-api
18
18
  yarn create daloy my-api
19
19
  bun create daloy my-api
20
+
21
+ # scaffold into the current directory
22
+ pnpm create daloy@latest .
20
23
  ```
21
24
 
22
25
  The CLI is interactive when arguments are missing. It will ask you for:
@@ -363,6 +363,7 @@ ${heading("Usage")}
363
363
  ${color(COLORS.cyan, "npm")} create daloy@latest ${color(COLORS.dim, "[project-name] [options]")}
364
364
  ${color(COLORS.cyan, "yarn")} create daloy ${color(COLORS.dim, "[project-name] [options]")}
365
365
  ${color(COLORS.cyan, "bun")} create daloy ${color(COLORS.dim, "[project-name] [options]")}
366
+ ${color(COLORS.dim, "Use . as the project name to scaffold into the current directory.")}
366
367
 
367
368
  ${heading("Options")}
368
369
  ${color(COLORS.green, "--template <name>")} ${TEMPLATES.join(" | ")} ${color(COLORS.dim, "(default: node-basic)")}
@@ -568,7 +569,8 @@ const VALID_CODE_OWNER =
568
569
 
569
570
  function validateProjectName(name) {
570
571
  if (!name || !name.trim()) return "Project name cannot be empty.";
571
- if (name === "." || name === "..") return "Use a real directory name.";
572
+ if (name === ".") return true;
573
+ if (name === "..") return "Use a real directory name.";
572
574
  if (name.length > 214) return "Project name is too long (max 214 chars).";
573
575
  if (!VALID_NAME.test(name)) {
574
576
  return "Project name must be a valid npm package name (lowercase, no spaces, no leading dot/underscore).";
@@ -576,6 +578,10 @@ function validateProjectName(name) {
576
578
  return true;
577
579
  }
578
580
 
581
+ function projectPackageName(projectName, cwd) {
582
+ return projectName === "." ? path.basename(cwd).toLowerCase() : projectName;
583
+ }
584
+
579
585
  async function isDirEmpty(dir) {
580
586
  try {
581
587
  const entries = await readdir(dir);
@@ -1677,7 +1683,7 @@ function createSpinner(initialMessage) {
1677
1683
  };
1678
1684
  }
1679
1685
 
1680
- function printSummary({ projectName, template, packageManager, installDeps, skipPackageManager, withCi, withDeploy, missingTools = [] }) {
1686
+ function printSummary({ projectName, shouldChangeDirectory = true, template, packageManager, installDeps, skipPackageManager, withCi, withDeploy, missingTools = [] }) {
1681
1687
  const templateMeta = TEMPLATE_OPTIONS.find((option) => option.value === template);
1682
1688
  const templateLabel = templateMeta ? `${templateMeta.title} ${color(COLORS.dim, `(${template})`)}` : template;
1683
1689
  const summaryLines = [
@@ -1703,7 +1709,9 @@ function printSummary({ projectName, template, packageManager, installDeps, skip
1703
1709
 
1704
1710
  const arrow = color(COLORS.cyan, SYMBOLS.arrow);
1705
1711
  console.log(`${color(COLORS.bold, "Next steps")}`);
1706
- console.log(` ${arrow} ${color(COLORS.cyan, `cd ${projectName}`)}`);
1712
+ if (shouldChangeDirectory) {
1713
+ console.log(` ${arrow} ${color(COLORS.cyan, `cd ${projectName}`)}`);
1714
+ }
1707
1715
  if (skipPackageManager) {
1708
1716
  console.log(` ${arrow} ${color(COLORS.cyan, "deno task dev")}`);
1709
1717
  } else {
@@ -1835,7 +1843,14 @@ async function main() {
1835
1843
  process.exit(1);
1836
1844
  }
1837
1845
 
1838
- const targetDir = path.resolve(process.cwd(), projectName);
1846
+ const cwd = process.cwd();
1847
+ const packageName = projectPackageName(projectName, cwd);
1848
+ const packageNameCheck = projectName === "." ? validateProjectName(packageName) : true;
1849
+ if (packageNameCheck !== true) {
1850
+ logError(`Current directory name "${packageName}" cannot be used as a package name. ${packageNameCheck}`);
1851
+ process.exit(1);
1852
+ }
1853
+ const targetDir = projectName === "." ? cwd : path.resolve(cwd, projectName);
1839
1854
  if (existsSync(targetDir)) {
1840
1855
  const empty = await isDirEmpty(targetDir);
1841
1856
  if (!empty && !opts.force) {
@@ -1927,8 +1942,8 @@ async function main() {
1927
1942
  logStep("Minimal mode applied", `${count} file${count === 1 ? "" : "s"} trimmed`);
1928
1943
  }
1929
1944
  if (!skipPackageManager) {
1930
- await patchPackageJson(targetDir, projectName, packageManager);
1931
- logStep("Package metadata written", projectName);
1945
+ await patchPackageJson(targetDir, packageName, packageManager);
1946
+ logStep("Package metadata written", packageName);
1932
1947
  await patchTemplateTextFiles(targetDir, packageManager);
1933
1948
  await patchDockerignoreForPackageManager(targetDir, packageManager);
1934
1949
  if (packageManager !== "pnpm") {
@@ -1985,13 +2000,23 @@ async function main() {
1985
2000
  if (tail.trim().length > 0) {
1986
2001
  console.error(color(COLORS.dim, tail));
1987
2002
  }
1988
- logWarn(`Retry inside ${projectName} with: ${packageManager} install`);
2003
+ logWarn(`Retry inside ${projectName === "." ? "this directory" : projectName} with: ${packageManager} install`);
1989
2004
  } else {
1990
2005
  spinner.stop(`Installed dependencies with ${color(COLORS.cyan, packageManager)}`);
1991
2006
  }
1992
2007
  }
1993
2008
 
1994
- printSummary({ projectName, template, packageManager, installDeps, skipPackageManager, withCi, withDeploy, missingTools });
2009
+ printSummary({
2010
+ projectName: packageName,
2011
+ shouldChangeDirectory: projectName !== ".",
2012
+ template,
2013
+ packageManager,
2014
+ installDeps,
2015
+ skipPackageManager,
2016
+ withCi,
2017
+ withDeploy,
2018
+ missingTools,
2019
+ });
1995
2020
  } catch (err) {
1996
2021
  rl?.close();
1997
2022
  if (err && err.message === "Cancelled") {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-daloy",
3
- "version": "1.0.0-beta.4",
3
+ "version": "1.0.0-beta.6",
4
4
  "description": "Scaffold a new DaloyJS project. Run with `pnpm create daloy`, `npm create daloy@latest`, `yarn create daloy`, or `bun create daloy`.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -27,7 +27,7 @@
27
27
  "openapi"
28
28
  ],
29
29
  "engines": {
30
- "node": ">=24.0.0"
30
+ "node": "^24.0.0 || >=26.0.0"
31
31
  },
32
32
  "bin": {
33
33
  "create-daloy": "bin/create-daloy.mjs"
package/sbom.cdx.json CHANGED
@@ -1,25 +1,25 @@
1
1
  {
2
2
  "bomFormat": "CycloneDX",
3
3
  "specVersion": "1.5",
4
- "serialNumber": "urn:uuid:1f97437c-9665-5c96-94a2-d8d548acda4b",
4
+ "serialNumber": "urn:uuid:f43c9f53-eb2b-530b-a5b0-1406b6ec4e9f",
5
5
  "version": 1,
6
6
  "metadata": {
7
- "timestamp": "2026-06-26T13:41:50.237Z",
7
+ "timestamp": "2026-07-01T22:47:11.159Z",
8
8
  "tools": [
9
9
  {
10
10
  "vendor": "DaloyJS",
11
11
  "name": "daloy-generate-sbom",
12
- "version": "1.0.0-beta.4"
12
+ "version": "1.0.0-beta.6"
13
13
  }
14
14
  ],
15
15
  "authors": [],
16
16
  "component": {
17
17
  "type": "library",
18
- "bom-ref": "pkg:npm/create-daloy@1.0.0-beta.4",
18
+ "bom-ref": "pkg:npm/create-daloy@1.0.0-beta.6",
19
19
  "name": "create-daloy",
20
- "version": "1.0.0-beta.4",
20
+ "version": "1.0.0-beta.6",
21
21
  "description": "Scaffold a new DaloyJS project. Run with `pnpm create daloy`, `npm create daloy@latest`, `yarn create daloy`, or `bun create daloy`.",
22
- "purl": "pkg:npm/create-daloy@1.0.0-beta.4",
22
+ "purl": "pkg:npm/create-daloy@1.0.0-beta.6",
23
23
  "licenses": [
24
24
  {
25
25
  "license": {
@@ -42,9 +42,9 @@
42
42
  }
43
43
  ],
44
44
  "swid": {
45
- "tagId": "swidtag-create-daloy-1.0.0-beta.4",
45
+ "tagId": "swidtag-create-daloy-1.0.0-beta.6",
46
46
  "name": "create-daloy",
47
- "version": "1.0.0-beta.4",
47
+ "version": "1.0.0-beta.6",
48
48
  "tagVersion": 0,
49
49
  "patch": false
50
50
  }
@@ -53,7 +53,7 @@
53
53
  "components": [],
54
54
  "dependencies": [
55
55
  {
56
- "ref": "pkg:npm/create-daloy@1.0.0-beta.4",
56
+ "ref": "pkg:npm/create-daloy@1.0.0-beta.6",
57
57
  "dependsOn": []
58
58
  }
59
59
  ]
package/sbom.spdx.json CHANGED
@@ -2,10 +2,10 @@
2
2
  "spdxVersion": "SPDX-2.3",
3
3
  "dataLicense": "CC0-1.0",
4
4
  "SPDXID": "SPDXRef-DOCUMENT",
5
- "name": "create-daloy-1.0.0-beta.4",
6
- "documentNamespace": "https://github.com/daloyjs/daloy/sbom/create-daloy-1.0.0-beta.4-1f97437c-9665-5c96-94a2-d8d548acda4b",
5
+ "name": "create-daloy-1.0.0-beta.6",
6
+ "documentNamespace": "https://github.com/daloyjs/daloy/sbom/create-daloy-1.0.0-beta.6-f43c9f53-eb2b-530b-a5b0-1406b6ec4e9f",
7
7
  "creationInfo": {
8
- "created": "2026-06-26T13:41:50.237Z",
8
+ "created": "2026-07-01T22:47:11.159Z",
9
9
  "creators": [
10
10
  "Tool: daloy-generate-sbom",
11
11
  "Organization: DaloyJS"
@@ -16,7 +16,7 @@
16
16
  {
17
17
  "SPDXID": "SPDXRef-Package-create-daloy",
18
18
  "name": "create-daloy",
19
- "versionInfo": "1.0.0-beta.4",
19
+ "versionInfo": "1.0.0-beta.6",
20
20
  "downloadLocation": "https://github.com/daloyjs/daloy",
21
21
  "filesAnalyzed": false,
22
22
  "licenseConcluded": "MIT",
@@ -27,7 +27,7 @@
27
27
  {
28
28
  "referenceCategory": "PACKAGE-MANAGER",
29
29
  "referenceType": "purl",
30
- "referenceLocator": "pkg:npm/create-daloy@1.0.0-beta.4"
30
+ "referenceLocator": "pkg:npm/create-daloy@1.0.0-beta.6"
31
31
  }
32
32
  ]
33
33
  }
@@ -1,9 +1,16 @@
1
1
  # AGENTS.md
2
2
 
3
- A [DaloyJS](https://daloyjs.dev) REST API for the [Bun](https://bun.sh) runtime. **Contract-first**: routes are defined with Zod schemas and OpenAPI 3.1 is generated from them. When `docs: true` is set in `new App({...})`, three routes are auto-mounted: `GET /openapi.json`, `GET /openapi.yaml`, and `GET /docs` (Scalar UI).
3
+ A [DaloyJS](https://daloyjs.dev) REST API for the [Bun](https://bun.sh) runtime. **Contract-first**: routes are defined with validation schemas (Zod in this template; DaloyJS also supports Standard Schema-compatible validators) and OpenAPI 3.1 is generated from them. When `docs: true` is set in `new App({...})`, three routes are auto-mounted: `GET /openapi.json`, `GET /openapi.yaml`, and `GET /docs` (Scalar UI).
4
4
 
5
5
  - Package manager / runtime: Bun.
6
6
 
7
+ ## Agent guidance
8
+
9
+ - Treat this file as the short, durable project contract for AI coding agents.
10
+ - Use `.agents/skills/daloyjs-best-practices/SKILL.md` for the detailed DaloyJS workflow; keep this file concise and do not duplicate that skill.
11
+ - If instructions conflict, follow the user's latest prompt first, then the nearest `AGENTS.md`, then the skill.
12
+ - Change route definitions, schemas, metadata, and tests first; regenerate generated files instead of hand-editing OpenAPI or typed-client output.
13
+
7
14
  ## Commands
8
15
 
9
16
  - `bun run dev` — hot-reload server on http://localhost:3000
@@ -11,6 +18,8 @@ A [DaloyJS](https://daloyjs.dev) REST API for the [Bun](https://bun.sh) runtime.
11
18
  - `bun test` — Bun's native test runner
12
19
  - `bun run gen:openapi` — write `generated/openapi.json`
13
20
  - `bun run gen:client` — write the typed Hey API client
21
+ - `bun run contract` — run the focused OpenAPI contract test
22
+ - `bun run hooks:install` — enable the optional pre-push contract gate
14
23
  - `bun run build` — produce `dist/`
15
24
 
16
25
  ## Project shape
@@ -34,19 +43,20 @@ Do not write `.js` here — that's the Node NodeNext convention and will fail to
34
43
  ## Core rules
35
44
 
36
45
  1. The route definition is the contract. Method, path, request schemas, and response schemas live in one place — `app.route({...})`.
37
- 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.
38
47
  3. Preserve literal types in responses: `status: 200 as const`, `z.literal(...)` on discriminator fields. Codegen depends on these.
39
48
  4. Throw typed errors (`NotFoundError`, `BadRequestError`, etc.) from `@daloyjs/core` — never return raw error responses.
40
49
  5. Keep `requestId()`, `secureHeaders()`, and `rateLimit()` enabled. They are the project's secure defaults.
41
- 6. Every new route ships with a test that covers a happy path and at least one unhappy path.
42
- 7. After any route change: `bun run gen:openapi && bun run gen:client && bun run typecheck && bun test`.
50
+ 6. Keep operation IDs stable and examples schema-valid; `bun run contract` must pass after route, metadata, or OpenAPI-facing changes.
51
+ 7. Every new route ships with a test that covers a happy path and at least one unhappy path.
52
+ 8. After any route change: `bun run gen:openapi && bun run gen:client && bun run contract && bun run typecheck && bun test`.
43
53
 
44
54
  ## Secure-by-default (do not let an AI strip these)
45
55
 
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.**
56
+ 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
57
 
48
58
  - Keep `secureHeaders()`, `requestId()`, `rateLimit()` registered, and `bodyLimitBytes` / `requestTimeoutMs` set on `new App({...})`. Tighten per-route; never raise globally to pass a test.
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.
59
+ - 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.
50
60
  - 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.
51
61
  - JWT verifiers keep an explicit `algorithms` allowlist; never trust the token's `alg` header, never allow `none`, always check `exp` / `nbf`.
52
62
  - Credential / HMAC comparisons use `timingSafeEqual`, never `===`. Throw typed errors from `@daloyjs/core` so problem+json redacts in prod; never return raw stack traces.
@@ -56,7 +66,7 @@ Per Supabase + Aikido on [secure-by-default development](https://www.aikido.dev/
56
66
  ## Process expectations
57
67
 
58
68
  - Quality gates must pass before declaring work done: `bun run typecheck` and `bun test`.
59
- - Regenerate the OpenAPI spec and typed client whenever route shapes change.
69
+ - Regenerate the OpenAPI spec and typed client whenever route shapes change, then run `bun run contract`.
60
70
  - Bug fixes include a regression test.
61
71
  - Never bypass safety checks without a clear reason.
62
72
 
@@ -2,10 +2,10 @@
2
2
  name: daloyjs-best-practices
3
3
  description: >-
4
4
  Best practices for building, testing, and hardening this DaloyJS REST API on
5
- the Bun runtime. Use when adding or changing HTTP routes, Zod schemas,
6
- middleware, or error handling; regenerating the OpenAPI spec or the typed
7
- Hey API client; or working on auth, rate limits, secrets, and the project's
8
- quality gates.
5
+ the Bun runtime. 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.
9
9
  license: MIT
10
10
  ---
11
11
 
@@ -37,8 +37,9 @@ recommendation below follows from them:
37
37
  and response schemas live in one place (`app.route({...})`). The OpenAPI
38
38
  spec, the typed client, and the runtime validation are all derived from
39
39
  it.
40
- 2. **Zod schemas validate at every boundary.** Body, params, query, and
41
- headers go through Zod.
40
+ 2. **Validation schemas protect every boundary.** This template uses Zod,
41
+ and Daloy accepts any Standard Schema-compatible library. Body, params,
42
+ query, and headers go through the declared schema.
42
43
  3. **Preserve literal types.** Return `status: 200 as const` and use
43
44
  `z.literal(...)` / `as const` on discriminator fields so the typed
44
45
  client can narrow responses.
@@ -47,6 +48,9 @@ recommendation below follows from them:
47
48
  codegen and tests import `buildApp()` without side effects.
48
49
  5. **Secure by default.** `requestId()`, `secureHeaders()`, and
49
50
  `rateLimit()` are registered before route definitions.
51
+ 6. **Contract gates are part of done.** Keep `operationId` values stable,
52
+ examples schema-valid, declared error responses accurate, and generated
53
+ OpenAPI / client artifacts in sync with the live route table.
50
54
 
51
55
  ## Project shape
52
56
 
@@ -69,12 +73,15 @@ bun run typecheck # tsc --noEmit
69
73
  bun test # Bun's native test runner
70
74
  bun run gen:openapi # write generated/openapi.json
71
75
  bun run gen:client # write generated/client/
76
+ bun run contract # run the focused contract test
72
77
  bun run build # produce dist/
73
78
  ```
74
79
 
75
80
  Always run `bun run typecheck` and `bun test` before declaring a task done.
76
- If a change touches route shapes, also rerun `bun run gen:openapi && bun
77
- run gen:client` so the client stays in sync.
81
+ `bun test` includes the contract gate; if you need a focused contract check,
82
+ run `bun run contract`. If a change touches route shapes, also rerun
83
+ `bun run gen:openapi && bun run gen:client` so the OpenAPI spec and client
84
+ stay in sync.
78
85
 
79
86
  ## OpenAPI & docs routes
80
87
 
@@ -93,6 +100,18 @@ mount only outside production, or `docs: false` to disable all three.
93
100
  For hand-rolled mounting, `openapiToYAML` is exported from
94
101
  `@daloyjs/core/openapi`.
95
102
 
103
+ ## AI-ready contract metadata
104
+
105
+ Daloy can expose route metadata to OpenAPI and agent tooling. Add metadata
106
+ when it helps consumers understand or safely automate the route:
107
+
108
+ - Use `summary`, `description`, and `tags` for concise human-facing docs.
109
+ - Use `meta.examples` for realistic happy-path and unhappy-path examples.
110
+ Examples must match the declared schemas; the contract gate rejects drift.
111
+ - Use `meta.extensions` for stable `x-*` fields consumed by internal tools.
112
+ - Use `deprecated` and `sunset` when changing API lifecycle. Do not remove
113
+ a route or response shape silently if generated clients may depend on it.
114
+
96
115
  ## Workflow: add a new route
97
116
 
98
117
  1. **Open `src/build-app.ts`.**
@@ -100,7 +119,9 @@ For hand-rolled mounting, `openapiToYAML` is exported from
100
119
  response body per status code. Prefer `z.object({...}).strict()` for
101
120
  inputs so unknown keys are rejected at the boundary.
102
121
  3. **Call `app.route({...})`** with `method`, `path`, `operationId`, `tags`,
103
- `responses`, `handler` (plus `request` when accepting input).
122
+ `responses`, `handler` (plus `request` when accepting input). Add `meta`
123
+ examples / descriptions when the route is user-facing or consumed by
124
+ agents.
104
125
  4. **Return `{ status, body, headers? }` from the handler.** Always use
105
126
  `status: 200 as const` so the typed client can narrow.
106
127
  5. **Throw typed errors**, do not return raw error responses. Use
@@ -108,9 +129,10 @@ For hand-rolled mounting, `openapiToYAML` is exported from
108
129
  `ForbiddenError`, `ConflictError`, etc.
109
130
  6. **Add a test in `tests/<route>.test.ts`** using `app.request(...)` for
110
131
  in-process testing — no port needed.
111
- 7. **Regenerate the contract**: `bun run gen:openapi && bun run gen:client`.
132
+ 7. **Run the contract gate**: `bun run contract` or `bun test`.
133
+ 8. **Regenerate the contract artifacts**: `bun run gen:openapi && bun run gen:client`.
112
134
  Inspect `generated/openapi.json` to confirm the operation shows up.
113
- 8. **Run the quality gates**: `bun run typecheck && bun test`.
135
+ 9. **Run the quality gates**: `bun run typecheck && bun test`.
114
136
 
115
137
  ### Example: a typed route
116
138
 
@@ -150,6 +172,8 @@ app.route({
150
172
  `.nullable()` for "explicitly null". They differ in OpenAPI output.
151
173
  - **Pagination**: standardize on `{ items, nextCursor }` cursor pagination.
152
174
  - **Discriminated unions**: use `z.discriminatedUnion("kind", [...])`.
175
+ - Keep response examples close to the route definition and schema-valid.
176
+ The contract test intentionally fails invalid examples.
153
177
  - **Never** call `JSON.parse` or read `req.body` directly. Let the
154
178
  framework validate and pass the typed object to your handler.
155
179
 
@@ -168,8 +192,8 @@ Register middleware **before** route definitions. Order matters.
168
192
  Keep the secure baseline:
169
193
 
170
194
  ```ts
171
- app.use(requestId()); // x-request-id for log correlation
172
- app.use(secureHeaders()); // strict security headers
195
+ app.use(requestId()); // x-request-id for log correlation
196
+ app.use(secureHeaders()); // strict security headers
173
197
  app.use(rateLimit({ windowMs: 60_000, max: 120 }));
174
198
  ```
175
199
 
@@ -198,12 +222,18 @@ Cover both **happy paths and unhappy paths** for every route: valid input,
198
222
  validation failures (400), auth failures (401/403), not-found (404),
199
223
  conflicts (409), rate limiting (429). For external services, inject an
200
224
  in-memory fake via `buildApp({ store })` rather than mocking `fetch`.
225
+ The shipped contract test should fail invalid examples, duplicate/missing
226
+ `operationId`, or missing responses.
201
227
 
202
228
  Aim for **100% line and function coverage** on routes you add.
203
229
 
204
230
  ## Security best practices
205
231
 
206
232
  - Keep `secureHeaders()`, `requestId()`, and `rateLimit()` enabled.
233
+ - Never make a failing test pass by deleting or weakening a security guard.
234
+ If a guard blocks a legitimate route, add the narrowest per-route
235
+ override or configuration knob and cover both the allowed and rejected
236
+ paths in tests.
207
237
  - Never log secrets — filter `authorization`, `cookie`, and any
208
238
  token-bearing fields.
209
239
  - Validate secrets from `process.env` (or `Bun.env`) through a Zod schema
@@ -215,6 +245,9 @@ Aim for **100% line and function coverage** on routes you add.
215
245
  mitigate DoS.
216
246
  - Use parameterized queries for database access — never interpolate user
217
247
  input into SQL.
248
+ - For outbound HTTP, prefer `fetchGuard()` or a transport layered on top
249
+ of it when URLs can be influenced by users or tenants. SSRF protections
250
+ should fail closed for private ranges and cloud metadata endpoints.
218
251
  - Bun ships its own audit story; check `bun pm audit` periodically and
219
252
  pin versions in `bun.lockb`.
220
253
 
@@ -236,6 +269,8 @@ Aim for **100% line and function coverage** on routes you add.
236
269
  - Never import `@daloyjs/core/bun` from `src/build-app.ts` or any script
237
270
  under `scripts/`. That would boot an HTTP listener during codegen.
238
271
  - Do not edit files under `generated/` by hand — they are overwritten.
272
+ - Do not hand-edit OpenAPI paths or client types. Fix the route definition,
273
+ schema, or metadata and regenerate.
239
274
  - Do not weaken response literal types (`as const`); the typed client
240
275
  depends on them.
241
276
  - Do not return errors as `{ status: 4xx, body: {...} }`. Throw a typed
@@ -252,6 +287,8 @@ Aim for **100% line and function coverage** on routes you add.
252
287
  - `bun run typecheck` and `bun test` must pass before completion.
253
288
  - Run `bun run gen:openapi && bun run gen:client` when route shapes
254
289
  change; commit the updated artifacts.
290
+ - When route metadata, examples, lifecycle flags, or operation IDs change,
291
+ run the contract gate and inspect the relevant generated OpenAPI diff.
255
292
  - Keep `README.md`, this `SKILL.md`, and `AGENTS.md` consistent with the
256
293
  code.
257
294
 
@@ -19,7 +19,7 @@
19
19
  "hooks:install": "git config core.hooksPath .githooks"
20
20
  },
21
21
  "dependencies": {
22
- "@daloyjs/core": "^1.0.0-beta.4",
22
+ "@daloyjs/core": "^1.0.0-beta.6",
23
23
  "zod": "^4.4.3"
24
24
  },
25
25
  "devDependencies": {
@@ -1,15 +1,24 @@
1
1
  # AGENTS.md
2
2
 
3
- A [DaloyJS](https://daloyjs.dev) REST API deployed to **Cloudflare Workers**. **Contract-first**: routes are defined with Zod schemas and OpenAPI 3.1 is generated from them. `docs: true` is set in `new App({...})`, so `GET /openapi.json`, `GET /openapi.yaml`, and `GET /docs` (Scalar UI) are auto-mounted. DaloyJS is dependency-free and the Scalar UI loads from a CDN, so this adds negligible Worker bundle size; drop `docs` (and the `openapi` block) if you want the smallest possible bundle.
3
+ A [DaloyJS](https://daloyjs.dev) REST API deployed to **Cloudflare Workers**. **Contract-first**: routes are defined with validation schemas (Zod in this template; DaloyJS also supports Standard Schema-compatible validators) and OpenAPI 3.1 is generated from them. `docs: true` is set in `new App({...})`, so `GET /openapi.json`, `GET /openapi.yaml`, and `GET /docs` (Scalar UI) are auto-mounted. DaloyJS is dependency-free and the Scalar UI loads from a CDN, so this adds negligible Worker bundle size; drop `docs` (and the `openapi` block) if you want the smallest possible bundle.
4
4
 
5
5
  - Package manager: pnpm (use `pnpm` unless the project's `package.json` was rewritten for npm/yarn/bun).
6
6
  - Runtime: Cloudflare Workers (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` — `wrangler dev` on http://localhost:8787
11
18
  - `pnpm typecheck` — `tsc --noEmit`
12
19
  - `pnpm test` — run test suite
20
+ - `pnpm contract` — run `daloy inspect --check src/index.ts`
21
+ - `pnpm hooks:install` — enable the optional pre-push contract gate
13
22
  - `pnpm deploy` — `wrangler deploy`
14
23
  - `pnpm audit` — supply-chain audit
15
24
 
@@ -22,31 +31,33 @@ A [DaloyJS](https://daloyjs.dev) REST API deployed to **Cloudflare Workers**. **
22
31
  ## Core rules
23
32
 
24
33
  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.
34
+ 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.
26
35
  3. Preserve literal types in responses: `status: 200 as const`, `z.literal(...)` on discriminator fields.
27
36
  4. Throw typed errors (`NotFoundError`, `BadRequestError`, etc.) from `@daloyjs/core`.
28
37
  5. Keep `requestId()`, `secureHeaders()`, and `rateLimit()` enabled. For high-traffic routes, attach Cloudflare's native rate-limit binding (the in-memory limiter resets per isolate).
29
38
  6. Stay on the Workers runtime: only Web Standards APIs + Cloudflare bindings. No `node:` modules unless you explicitly add `nodejs_compat` and require it.
30
39
  7. Bindings flow through `env`. Read KV/D1/R2/secrets from the `env` argument; never read them via globals.
31
40
  8. Long-running work belongs in `ctx.waitUntil(...)`, not blocking the response.
32
- 9. Every new route ships with a test that covers a happy path and at least one unhappy path.
41
+ 9. Keep operation IDs stable and examples schema-valid; `pnpm contract` must pass after route, metadata, or OpenAPI-facing changes.
42
+ 10. Every new route ships with a test that covers a happy path and at least one unhappy path.
33
43
 
34
44
  ## Secure-by-default (do not let an AI strip these)
35
45
 
36
- 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.**
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.**
37
47
 
38
48
  - Keep `secureHeaders()`, `requestId()`, `rateLimit()` registered, and `bodyLimitBytes` / `requestTimeoutMs` set on `new App({...})`. For production, add Cloudflare's native rate-limit binding **in addition to** the in-memory limiter, not instead of it.
39
49
  - Read secrets and bindings (KV, D1, R2) from the `env` argument; never hard-code, never log them.
40
- - 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
+ - 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.
41
51
  - 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.
42
52
  - JWT verifiers keep an explicit `algorithms` allowlist; never trust the token's `alg` header, never allow `none`, always check `exp` / `nbf`.
43
- - Credential / HMAC comparisons use `crypto.subtle.timingSafeEqual`, never `===`. Throw typed errors from `@daloyjs/core` so problem+json redacts in prod; never return raw stack traces.
53
+ - Credential / HMAC comparisons use constant-time verification, never `===`. Prefer Web Crypto verification APIs or the framework timing-safe helper where available. Throw typed errors from `@daloyjs/core` so problem+json redacts in prod; never return raw stack traces.
44
54
  - Keep `compatibility_date` pinned; do not enable `nodejs_compat` unless a feature requires it.
45
55
  - `.env`, `.dev.vars`, secrets, private keys: never commit. Use `wrangler secret put` for production secrets.
46
56
 
47
57
  ## Process expectations
48
58
 
49
59
  - Quality gates must pass before declaring work done: `pnpm typecheck` and `pnpm test`.
60
+ - Run the contract gate (`pnpm contract`) whenever route shapes, examples, operation IDs, or OpenAPI metadata change.
50
61
  - Bug fixes include a regression test.
51
62
  - Pin `compatibility_date` in `wrangler.toml`; only bump it deliberately.
52
63
  - For deploys, ensure the user has run `wrangler login`; do not authenticate on their behalf.