create-daloy 1.3.7 → 1.4.1

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.
Files changed (59) hide show
  1. package/bin/create-daloy.mjs +61 -6
  2. package/package.json +1 -1
  3. package/sbom.cdx.json +9 -9
  4. package/sbom.spdx.json +5 -5
  5. package/templates/_ci/deno/_github/workflows/container-scan.yml +10 -0
  6. package/templates/_ci/deno/_github/workflows/deploy.yml +9 -0
  7. package/templates/_ci/node/_github/workflows/ci.yml +1 -5
  8. package/templates/_ci/node/_github/workflows/container-scan.yml +10 -0
  9. package/templates/bun-basic/AGENTS.md +1 -1
  10. package/templates/bun-basic/README.md +6 -1
  11. package/templates/bun-basic/_Dockerfile +6 -3
  12. package/templates/bun-basic/_agents/skills/daloyjs-best-practices/SKILL.md +6 -6
  13. package/templates/bun-basic/_dockerignore +4 -1
  14. package/templates/bun-basic/_env.example +12 -0
  15. package/templates/bun-basic/_gitignore +5 -0
  16. package/templates/bun-basic/_npmrc +5 -3
  17. package/templates/bun-basic/package.json +3 -3
  18. package/templates/bun-basic/pnpm-workspace.yaml +19 -5
  19. package/templates/bun-basic/src/index.ts +3 -1
  20. package/templates/bun-basic/tsconfig.json +1 -1
  21. package/templates/cloudflare-worker/AGENTS.md +1 -1
  22. package/templates/cloudflare-worker/README.md +6 -1
  23. package/templates/cloudflare-worker/_Dockerfile +1 -1
  24. package/templates/cloudflare-worker/_agents/skills/daloyjs-best-practices/SKILL.md +56 -45
  25. package/templates/cloudflare-worker/_dockerignore +6 -1
  26. package/templates/cloudflare-worker/_gitignore +5 -0
  27. package/templates/cloudflare-worker/_npmrc +3 -0
  28. package/templates/cloudflare-worker/package.json +3 -3
  29. package/templates/cloudflare-worker/pnpm-workspace.yaml +16 -0
  30. package/templates/cloudflare-worker/src/index.ts +8 -7
  31. package/templates/deno-basic/AGENTS.md +1 -1
  32. package/templates/deno-basic/README.md +1 -1
  33. package/templates/deno-basic/_Dockerfile +10 -6
  34. package/templates/deno-basic/_agents/skills/daloyjs-best-practices/SKILL.md +4 -4
  35. package/templates/deno-basic/_dockerignore +4 -1
  36. package/templates/deno-basic/_gitignore +5 -0
  37. package/templates/deno-basic/deno.json +8 -8
  38. package/templates/deno-basic/deno.lock +4 -4
  39. package/templates/node-basic/README.md +5 -0
  40. package/templates/node-basic/_Dockerfile +6 -3
  41. package/templates/node-basic/_agents/skills/daloyjs-best-practices/SKILL.md +8 -8
  42. package/templates/node-basic/_dockerignore +4 -1
  43. package/templates/node-basic/_env.example +11 -0
  44. package/templates/node-basic/_gitignore +5 -0
  45. package/templates/node-basic/_npmrc +3 -0
  46. package/templates/node-basic/package.json +2 -2
  47. package/templates/node-basic/pnpm-workspace.yaml +16 -0
  48. package/templates/vercel/AGENTS.md +1 -1
  49. package/templates/vercel/README.md +19 -2
  50. package/templates/vercel/_Dockerfile +10 -9
  51. package/templates/vercel/_agents/skills/daloyjs-best-practices/SKILL.md +2 -2
  52. package/templates/vercel/_dockerignore +4 -1
  53. package/templates/vercel/_env.example +9 -1
  54. package/templates/vercel/_gitignore +5 -0
  55. package/templates/vercel/_npmrc +3 -0
  56. package/templates/vercel/_vercelignore +10 -0
  57. package/templates/vercel/api/index.ts +4 -3
  58. package/templates/vercel/package.json +3 -3
  59. package/templates/vercel/pnpm-workspace.yaml +16 -0
@@ -83,7 +83,7 @@ pnpm dev # wrangler dev on http://localhost:8787
83
83
  pnpm typecheck # tsc --noEmit
84
84
  pnpm test # run test suite
85
85
  pnpm contract # daloy inspect --check src/index.ts
86
- pnpm deploy # wrangler deploy
86
+ pnpm run deploy # wrangler deploy (`pnpm deploy` is pnpm's own command)
87
87
  pnpm audit # supply-chain audit
88
88
  ```
89
89
 
@@ -138,56 +138,63 @@ when it helps consumers understand or safely automate the route:
138
138
  4. **Return `{ status, body, headers? }`** with `status: 200 as const`.
139
139
  5. **Throw typed errors** (`NotFoundError`, `BadRequestError`, etc.).
140
140
  6. **Add a test** under `tests/`. Use `app.request(...)` for pure logic;
141
- use `unstable_dev` (Wrangler) or `@cloudflare/vitest-pool-workers`
142
- when you need bindings.
141
+ use `@cloudflare/vitest-pool-workers` when you need bindings
142
+ (Wrangler's `unstable_dev` is deprecated).
143
143
  7. **Run the contract gate**: `pnpm contract` or `pnpm test`.
144
144
  8. **Run the quality gates**: `pnpm typecheck && pnpm test`.
145
145
 
146
146
  ### Example: a typed route with bindings
147
147
 
148
148
  ```ts
149
+ import { env } from "cloudflare:workers";
149
150
  import { z } from "zod";
150
151
  import { App, NotFoundError, rateLimit, requestId, secureHeaders } from "@daloyjs/core";
151
152
  import { toFetchHandler } from "@daloyjs/core/cloudflare";
152
153
 
153
- interface Env {
154
- BOOKS: KVNamespace;
155
- JWT_SECRET: string;
154
+ // Type your bindings once; `env` from "cloudflare:workers" is typed from this.
155
+ declare global {
156
+ namespace Cloudflare {
157
+ interface Env {
158
+ BOOKS: KVNamespace;
159
+ JWT_SECRET: string;
160
+ }
161
+ }
156
162
  }
157
163
 
158
164
  const Book = z.object({ id: z.string(), title: z.string() }).strict();
159
165
 
160
- function buildApp(env: Env) {
161
- const app = new App({ bodyLimitBytes: 1024 * 1024, requestTimeoutMs: 5_000 });
162
- app.use(requestId());
163
- app.use(secureHeaders());
164
- app.use(rateLimit({ windowMs: 60_000, max: 120 }));
165
-
166
- app.get(
167
- "/books/:id",
168
- {
169
- operationId: "getBookById",
170
- tags: ["Books"],
171
- request: { params: z.object({ id: z.string().min(1) }).strict() },
172
- responses: {
173
- 200: { description: "Found", body: Book },
174
- 404: { description: "Not found" },
175
- },
166
+ // Build the App ONCE at module scope. Creating it per request would give every
167
+ // request a fresh rateLimit() store, so the limit could never trigger.
168
+ const app = new App({
169
+ bodyLimitBytes: 1024 * 1024,
170
+ requestTimeoutMs: 5_000,
171
+ production: true,
172
+ behindProxy: { hops: 1 },
173
+ });
174
+ app.use(requestId());
175
+ app.use(secureHeaders());
176
+ app.use(rateLimit({ windowMs: 60_000, max: 120 }));
177
+
178
+ app.get(
179
+ "/books/:id",
180
+ {
181
+ operationId: "getBookById",
182
+ tags: ["Books"],
183
+ request: { params: z.object({ id: z.string().min(1) }).strict() },
184
+ responses: {
185
+ 200: { description: "Found", body: Book },
186
+ 404: { description: "Not found" },
176
187
  },
177
- async ({ params }) => {
178
- const raw = await env.BOOKS.get(params.id, "json");
179
- if (!raw) throw new NotFoundError(`Book ${params.id} not found`);
180
- return { status: 200 as const, body: Book.parse(raw) };
181
- }
182
- );
183
-
184
- return app;
185
- }
186
-
187
- export default {
188
- fetch: (req: Request, env: Env, ctx: ExecutionContext) =>
189
- toFetchHandler<Env>(buildApp(env)).fetch(req, env, ctx),
190
- };
188
+ },
189
+ async ({ params }) => {
190
+ // Bindings are read per request, inside the handler.
191
+ const raw = await env.BOOKS.get(params.id, "json");
192
+ if (!raw) throw new NotFoundError(`Book ${params.id} not found`);
193
+ return { status: 200 as const, body: Book.parse(raw) };
194
+ }
195
+ );
196
+
197
+ export default toFetchHandler(app);
191
198
  ```
192
199
 
193
200
  ## Validation & schema conventions
@@ -222,9 +229,11 @@ Add CORS only when needed, with an explicit `origin` allowlist.
222
229
 
223
230
  1. Add the binding (`[[kv_namespaces]]`, `[[d1_databases]]`, `[vars]`,
224
231
  etc.) to `wrangler.toml`.
225
- 2. Type the binding in the `Env` interface inside `src/index.ts`.
226
- 3. Pass `env` into `buildApp(env)` so handlers receive bindings via
227
- closure or factory argument. **Never read bindings via globals.**
232
+ 2. Type the binding on the `Cloudflare.Env` interface (see the example
233
+ above).
234
+ 3. Read bindings inside handlers with `import { env } from "cloudflare:workers"`.
235
+ Build the `App` once at module scope; **never create it per request**
236
+ (that resets the rateLimit store and re-compiles every route).
228
237
  4. Store secrets via `wrangler secret put` — they appear on `env` but
229
238
  are not committed to `wrangler.toml`.
230
239
 
@@ -277,13 +286,15 @@ Two patterns:
277
286
 
278
287
  - **In-process** with `app.request(...)` for pure logic that does not
279
288
  need bindings.
280
- - **Workers-aware** runners (`@cloudflare/vitest-pool-workers` or
281
- Wrangler `unstable_dev`) when KV/D1/etc. are involved.
289
+ - **Workers-aware** runner (`@cloudflare/vitest-pool-workers`) when
290
+ KV/D1/etc. are involved.
282
291
 
283
292
  Cover **happy paths and unhappy paths** for every route: valid input,
284
293
  validation failures (400), auth failures (401/403), not-found (404),
285
- conflict (409), rate limiting (429). For external services, inject an
286
- in-memory fake into `buildApp(env)` during tests.
294
+ conflict (409), rate limiting (429). For external services, run under
295
+ `@cloudflare/vitest-pool-workers` with test bindings, or keep the call behind
296
+ a small module that tests replace. Do not rebuild the App per request to
297
+ inject fakes.
287
298
  For user-owned or tenant-owned resources, use at least two principals and
288
299
  prove that Alice's valid token cannot list, read, update, or delete Bob's
289
300
  record.
@@ -339,10 +350,10 @@ reference. Skip that file for ordinary route work.
339
350
 
340
351
  ## Logging & observability
341
352
 
342
- - Use `ctx.log` — it carries the request id.
353
+ - Use `ctx.state.log` — it carries the request id.
343
354
  - `console.log` in Workers shows up in `wrangler tail`. Prefer
344
355
  structured logs through the framework logger.
345
- - For tracing, the `tracing()` middleware emits OpenTelemetry-compatible
356
+ - For tracing, the `otelTracing(opts)` middleware emits OpenTelemetry-compatible
346
357
  spans; wire up a Workers-friendly exporter when needed.
347
358
 
348
359
  ## Configuration & secrets
@@ -10,5 +10,10 @@ coverage
10
10
  .env
11
11
  .env.*
12
12
  !.env.example
13
+ # Wrangler's local secrets file.
14
+ .dev.vars
15
+ .dev.vars.*
16
+ !.dev.vars.example
17
+ *.pem
18
+ *.key
13
19
  generated
14
- otherdocs
@@ -14,3 +14,8 @@ dist/
14
14
  # consumer's lockfile and can pin transitives to unsigned tarballs.
15
15
  # See https://socket.dev/blog/understanding-the-security-concerns-of-npm-shrinkwrap
16
16
  npm-shrinkwrap.json
17
+
18
+ # Private keys and certificates (keep them in a secret manager, not git).
19
+ *.pem
20
+ *.key
21
+ *.p12
@@ -1,4 +1,7 @@
1
1
  # DaloyJS supply-chain hardening defaults — see the "Supply chain" docs.
2
+ #
3
+ # pnpm 11+ reads its settings from pnpm-workspace.yaml, not from this file,
4
+ # so the same values are set there. They stay here for npm and older tooling.
2
5
 
3
6
  auto-install-peers=true
4
7
  strict-peer-dependencies=true
@@ -10,17 +10,17 @@
10
10
  "dev": "wrangler dev",
11
11
  "deploy": "wrangler deploy",
12
12
  "typecheck": "tsc --noEmit",
13
- "test": "node --test tests/**/*.test.ts",
13
+ "test": "node --test \"tests/**/*.test.ts\"",
14
14
  "contract": "daloy inspect --check src/index.ts",
15
15
  "audit": "pnpm audit --prod",
16
16
  "hooks:install": "git config core.hooksPath .githooks"
17
17
  },
18
18
  "dependencies": {
19
- "@daloyjs/core": "^1.3.7",
19
+ "@daloyjs/core": "^1.4.1",
20
20
  "zod": "^4.4.3"
21
21
  },
22
22
  "devDependencies": {
23
- "@cloudflare/workers-types": "^4.20240909.0",
23
+ "@cloudflare/workers-types": "^5.20260926.1",
24
24
  "typescript": "^7.0.2",
25
25
  "wrangler": "^4.0.0"
26
26
  }
@@ -22,3 +22,19 @@ minimumReleaseAge: 1440
22
22
  # Transitive deps must resolve from the configured registry, which makes
23
23
  # typosquatted-tarball and compromised-fork attacks much harder to land.
24
24
  blockExoticSubdeps: true
25
+
26
+ # pnpm 11+ ignores these keys in `.npmrc`, so they live here. `.npmrc` keeps
27
+ # the same values for older tooling.
28
+ #
29
+ # Never run dependency lifecycle scripts (postinstall / preinstall / prepare):
30
+ # the main execution channel of the chalk/debug, node-ipc and Shai-Hulud
31
+ # malware. If you later need a build step, allowlist that one package with
32
+ # `allowBuilds` instead of turning this off.
33
+ ignoreScripts: true
34
+ # Fail on unmet or incompatible peer dependencies instead of warning.
35
+ strictPeerDependencies: true
36
+ # Re-verify package integrity hashes against the lockfile on install.
37
+ verifyStoreIntegrity: true
38
+ # Use the lockfile as-is whenever it satisfies package.json.
39
+ preferFrozenLockfile: true
40
+ autoInstallPeers: true
@@ -6,11 +6,12 @@ const app = new App({
6
6
  bodyLimitBytes: 256 * 1024,
7
7
  requestTimeoutMs: 5_000,
8
8
  production: true,
9
- // Cloudflare Workers always run behind Cloudflare's edge, which sets
10
- // X-Forwarded-For. Declare that single trusted hop so DaloyJS reads the real
11
- // client IP instead of refusing the (otherwise spoofable) header and
12
- // returning 500 in production. Increase the hop count if you put an
13
- // additional proxy in front of the Worker.
9
+ // Cloudflare Workers always run behind Cloudflare's edge, which appends the
10
+ // real client IP as the right-most X-Forwarded-For entry. Declaring that
11
+ // single trusted hop stops production from refusing the header with a 500,
12
+ // and rateLimit() below follows it, so each client gets its own bucket
13
+ // instead of every caller sharing one. Increase the hop count only if you
14
+ // put an additional proxy in front of the Worker.
14
15
  behindProxy: { hops: 1 },
15
16
  // daloy-minimal:strip-start docs
16
17
  // `docs: "auto"` mounts GET /openapi.json, /openapi.yaml and /docs (Scalar
@@ -28,8 +29,8 @@ const app = new App({
28
29
 
29
30
  app.use(requestId());
30
31
  app.use(secureHeaders());
31
- // The in-memory limiter resets per Worker isolate, so treat it as a
32
- // per-isolate abuse brake, not a global quota. For high-traffic routes, attach
32
+ // Keyed per client (via behindProxy above). The in-memory store resets per
33
+ // Worker isolate, so treat it as a per-isolate abuse brake, not a global quota. For high-traffic routes, attach
33
34
  // Cloudflare's native rate-limit binding in addition to — not instead of —
34
35
  // this baseline. Do not remove it to make a test pass; raise `max` per route.
35
36
  app.use(rateLimit({ windowMs: 60_000, max: 120 }));
@@ -38,7 +38,7 @@ The typed Hey API SDK is generated outside Deno (Hey API has no Deno entrypoint
38
38
  3. Preserve literal types in responses: `status: 200 as const`, `z.literal(...)` on discriminator fields.
39
39
  4. Throw typed errors (`NotFoundError`, `BadRequestError`, etc.) from `@daloyjs/core`.
40
40
  5. Keep `requestId()`, `secureHeaders()`, and `rateLimit()` enabled.
41
- 6. Deno permissions are part of the contract — keep `--allow-net --allow-env --allow-read` narrow; never use `--allow-all`.
41
+ 6. Deno permissions are part of the contract — keep `--allow-net` and the `--allow-env` allowlist narrow; never use `--allow-all`.
42
42
  7. Keep operation IDs stable and examples schema-valid; `deno task contract` must pass after route, metadata, or OpenAPI-facing changes.
43
43
  8. Every new route ships with a test that covers a happy path and at least one unhappy path.
44
44
  9. After any route change: `deno task gen:openapi && deno task contract && deno task typecheck && deno task test`.
@@ -72,7 +72,7 @@ deno task hooks:install # points core.hooksPath at .githooks
72
72
 
73
73
  <!-- daloy-minimal:strip-end books -->
74
74
 
75
- - Minimal permissions: `--allow-net --allow-env --allow-read` for `dev`.
75
+ - Minimal permissions: `--allow-net` plus an `--allow-env` allowlist (`PORT`, `DENO_ENV`, `TRUST_PROXY_HOPS`, `PUBLIC_URL`) for `dev` and `start`; add a variable there when you read a new one.
76
76
 
77
77
  ## Authentication (OAuth2 / OpenID Connect)
78
78
 
@@ -30,8 +30,10 @@
30
30
  # builds can pin to an immutable digest:
31
31
  # docker build --build-arg \
32
32
  # DENO_IMAGE=denoland/deno:alpine@sha256:<digest> .
33
- # Dependabot's `docker` ecosystem (see `.github/dependabot.yml`)
34
- # keeps the digest fresh.
33
+ # The default is a floating tag, and Dependabot's `docker` ecosystem
34
+ # cannot update a `FROM ${ARG}` line. For automatic digest updates,
35
+ # write the digest into the ARG default below (for example
36
+ # `ARG DENO_IMAGE=denoland/deno:alpine@sha256:<digest>`).
35
37
 
36
38
  # Override at build time to pin a specific digest.
37
39
  ARG DENO_IMAGE=denoland/deno:alpine
@@ -41,10 +43,12 @@ WORKDIR /app
41
43
  # Cache deps in a layer that only invalidates when imports change.
42
44
  COPY deno.json deno.lock* ./
43
45
  COPY src ./src
44
- # `deno cache` resolves and verifies every import against the lockfile
45
- # and bakes them into Deno's module cache. The resulting image cannot
46
- # resolve any module that was not present at build time.
47
- RUN deno cache --lock=deno.lock src/main.ts || deno cache src/main.ts
46
+ # `deno install --frozen=true` resolves every import from the committed
47
+ # lockfile and bakes it into Deno's module cache. It fails the build when a
48
+ # dependency does not match deno.lock, instead of silently resolving
49
+ # something newer (there is deliberately no unlocked fallback). The resulting
50
+ # image cannot resolve any module that was not present at build time.
51
+ RUN deno install --frozen=true --entrypoint src/main.ts
48
52
 
49
53
  FROM ${DENO_IMAGE} AS runner
50
54
  WORKDIR /app
@@ -74,7 +74,7 @@ DaloyJS is a **contract-first** framework. Internalize these rules:
74
74
  `generated/openapi.json`.
75
75
  - `deno.json` — tasks, import map, and JSR-first dependency specifiers. **There is no
76
76
  `package.json`** in this project — do not add one.
77
- - `tests/` — Deno test files (`*.test.ts`).
77
+ - `tests/` — Deno test files (`*_test.ts`).
78
78
  - `generated/` — **machine-written**. Never edit by hand.
79
79
 
80
80
  ## Commands cheat-sheet
@@ -199,7 +199,7 @@ app.get(
199
199
  - Throw typed errors from `@daloyjs/core` — they serialize to RFC 9457
200
200
  problem responses.
201
201
  - Add a `responses[code]` entry for every error you throw.
202
- - Do not swallow errors. Log via `ctx.log.error(...)` and rethrow.
202
+ - Do not swallow errors. Log via `ctx.state.log.error(...)` and rethrow.
203
203
 
204
204
  ## Middleware
205
205
 
@@ -299,7 +299,7 @@ Aim for complete happy- and unhappy-path test coverage of the routes you add.
299
299
  them explicitly to the relevant task in `deno.json` and call it out to
300
300
  the user — never `--allow-all`.
301
301
  - Never log secrets — filter `authorization`, `cookie`, etc.
302
- - Validate env via Zod at boot (`Deno.env.toObject()`). Fail fast on
302
+ - Validate env via Zod at boot (build the object from explicit `Deno.env.get("NAME")` calls; `Deno.env.toObject()` needs unscoped `--allow-env` and fails under the Dockerfile's allowlist). Fail fast on
303
303
  missing config.
304
304
  - For auth, verify JWT signatures against an allowlist of keys, never
305
305
  trust the `alg` header, always check `exp` / `nbf`.
@@ -331,7 +331,7 @@ Skip that file for ordinary route work.
331
331
 
332
332
  ## Logging & observability
333
333
 
334
- - Use `ctx.log` — it carries the request id.
334
+ - Use `ctx.state.log` — it carries the request id.
335
335
  - Avoid `console.log` in production code paths.
336
336
 
337
337
  ## Configuration & secrets
@@ -8,4 +8,7 @@ coverage
8
8
  .env.*
9
9
  !.env.example
10
10
  generated
11
- otherdocs
11
+ # Private keys and certificates never belong in an image.
12
+ *.pem
13
+ *.key
14
+ *.p12
@@ -10,3 +10,8 @@ generated/
10
10
  # consumer's lockfile and can pin transitives to unsigned tarballs.
11
11
  # See https://socket.dev/blog/understanding-the-security-concerns-of-npm-shrinkwrap
12
12
  npm-shrinkwrap.json
13
+
14
+ # Private keys and certificates (keep them in a secret manager, not git).
15
+ *.pem
16
+ *.key
17
+ *.p12
@@ -1,20 +1,20 @@
1
1
  {
2
2
  "name": "my-daloy-deno-app",
3
3
  "tasks": {
4
- "dev": "deno run --allow-net --allow-env --allow-read --watch src/main.ts",
5
- "start": "deno run --allow-net --allow-env --allow-read src/main.ts",
6
- "typecheck": "deno check src/main.ts",
4
+ "dev": "deno run --allow-net --allow-env=PORT,DENO_ENV,TRUST_PROXY_HOPS,PUBLIC_URL --watch src/main.ts",
5
+ "start": "deno run --allow-net --allow-env=PORT,DENO_ENV,TRUST_PROXY_HOPS,PUBLIC_URL src/main.ts",
6
+ "typecheck": "deno check src/ tests/ scripts/",
7
7
  "test": "deno test --allow-net --allow-env tests/",
8
8
  "contract": "deno test --allow-net --allow-env tests/contract_test.ts",
9
9
  "gen:openapi": "deno run --allow-net --allow-env --allow-read --allow-write scripts/dump-openapi.ts",
10
10
  "hooks:install": "git config core.hooksPath .githooks"
11
11
  },
12
12
  "imports": {
13
- "@daloyjs/core": "jsr:@daloyjs/daloy@^1.3.7",
14
- "@daloyjs/core/banner": "jsr:@daloyjs/daloy@^1.3.7/banner",
15
- "@daloyjs/core/contract": "jsr:@daloyjs/daloy@^1.3.7/contract",
16
- "@daloyjs/core/deno": "jsr:@daloyjs/daloy@^1.3.7/deno",
17
- "@daloyjs/core/openapi": "jsr:@daloyjs/daloy@^1.3.7/openapi",
13
+ "@daloyjs/core": "jsr:@daloyjs/daloy@^1.4.0",
14
+ "@daloyjs/core/banner": "jsr:@daloyjs/daloy@^1.4.0/banner",
15
+ "@daloyjs/core/contract": "jsr:@daloyjs/daloy@^1.4.0/contract",
16
+ "@daloyjs/core/deno": "jsr:@daloyjs/daloy@^1.4.0/deno",
17
+ "@daloyjs/core/openapi": "jsr:@daloyjs/daloy@^1.4.0/openapi",
18
18
  "zod": "npm:zod@^4.4.3"
19
19
  },
20
20
  "compilerOptions": {
@@ -1,14 +1,14 @@
1
1
  {
2
2
  "version": "5",
3
3
  "specifiers": {
4
- "jsr:@daloyjs/daloy@^1.0.0-rc.5": "1.0.0-rc.5",
4
+ "jsr:@daloyjs/daloy@^1.4.0": "1.4.0",
5
5
  "jsr:@std/assert@1": "1.0.19",
6
6
  "jsr:@std/internal@^1.0.12": "1.0.14",
7
7
  "npm:zod@^4.4.3": "4.4.3"
8
8
  },
9
9
  "jsr": {
10
- "@daloyjs/daloy@1.0.0-rc.5": {
11
- "integrity": "30d4d04baae914d7edaaf827d144e28caa41fda6bf77817840b8863d345378e6"
10
+ "@daloyjs/daloy@1.4.0": {
11
+ "integrity": "95c4a18e8e1b134a5c4038134fe0c2c862ce1c48d0b56d5b7ae0f1800945ebed"
12
12
  },
13
13
  "@std/assert@1.0.19": {
14
14
  "integrity": "eaada96ee120cb980bc47e040f82814d786fe8162ecc53c91d8df60b8755991e",
@@ -27,7 +27,7 @@
27
27
  },
28
28
  "workspace": {
29
29
  "dependencies": [
30
- "jsr:@daloyjs/daloy@^1.0.0-rc.5",
30
+ "jsr:@daloyjs/daloy@^1.4.0",
31
31
  "npm:zod@^4.4.3"
32
32
  ]
33
33
  }
@@ -18,6 +18,11 @@ curl http://localhost:3000/books/1
18
18
  <!-- daloy-minimal:strip-end books -->
19
19
  ```
20
20
 
21
+ > **Install refused right after a DaloyJS release?** New installs wait 24 hours
22
+ > before using a freshly published version (`minimumReleaseAge` in
23
+ > `pnpm-workspace.yaml`), a supply-chain safeguard. If `@daloyjs/core` was just
24
+ > released, retry a few hours later rather than turning the safeguard off.
25
+
21
26
  <!-- daloy-minimal:strip-start docs -->
22
27
 
23
28
  ## API documentation
@@ -16,8 +16,11 @@
16
16
  # pin to an immutable digest (recommended for production):
17
17
  # docker build --build-arg \
18
18
  # NODE_IMAGE=node:24-alpine@sha256:<digest> .
19
- # Dependabot's `docker` ecosystem (see `.github/dependabot.yml`)
20
- # keeps the digest fresh. The companion `container-scan.yml`
19
+ # The default is a floating tag, and Dependabot's `docker` ecosystem
20
+ # cannot update a `FROM ${ARG}` line. For automatic digest updates,
21
+ # write the digest into the ARG default below (for example
22
+ # `ARG NODE_IMAGE=node:24-alpine@sha256:<digest>`).
23
+ # The companion `container-scan.yml`
21
24
  # workflow lints this file with hadolint and scans the built
22
25
  # image with Trivy on every PR.
23
26
 
@@ -28,7 +31,7 @@ ARG NODE_IMAGE=node:24-alpine
28
31
  FROM ${NODE_IMAGE} AS builder
29
32
  WORKDIR /app
30
33
  COPY package.json pnpm-lock.yaml* ./
31
- RUN corepack enable && corepack prepare pnpm@latest --activate && \
34
+ RUN corepack enable && corepack prepare pnpm@11.1.3 --activate && \
32
35
  pnpm install --frozen-lockfile --ignore-scripts
33
36
  COPY . .
34
37
  RUN pnpm build
@@ -231,7 +231,7 @@ app.get(
231
231
  - Add a `responses[code]` entry for every error you throw, so the OpenAPI
232
232
  spec and the typed client know it can happen.
233
233
  - Do not swallow errors in handlers. If you need to log and rethrow, use
234
- `ctx.log.error(err, "context")` and rethrow.
234
+ `ctx.state.log.error({ err }, "context")` and rethrow.
235
235
  - For unexpected errors, let them bubble. The framework's error middleware
236
236
  will convert them into a generic 500 problem response and log them with
237
237
  the request ID for correlation.
@@ -315,7 +315,7 @@ test("GET /healthz returns ok", async () => {
315
315
  const app = buildApp();
316
316
  const res = await app.request("/healthz");
317
317
  assert.equal(res.status, 200);
318
- const body = await res.json();
318
+ const body = (await res.json()) as { ok: boolean; uptime: number };
319
319
  assert.equal(body.ok, true);
320
320
  assert.ok(typeof body.uptime === "number");
321
321
  });
@@ -392,14 +392,14 @@ reference. Skip that file for ordinary route work.
392
392
 
393
393
  ## Logging & observability
394
394
 
395
- - The default logger emits structured JSON in production and pretty logs
396
- in development. Use it via the handler context: `await handler(ctx)` →
397
- `ctx.log.info({ userId }, "message")`.
395
+ - The default logger emits structured JSON (it is silent under
396
+ `NODE_ENV=test`). Use it via the handler context:
397
+ `ctx.state.log.info({ userId }, "message")`.
398
398
  - Always include the request id in log lines automatically emitted by
399
399
  the framework. When you add your own logs, the request id is on
400
- `ctx.requestId` and on the bound child logger.
401
- - For tracing, the `tracing()` middleware (from `@daloyjs/core`) emits
402
- OpenTelemetry-compatible spans. Enable it once the user wires up an
400
+ `ctx.state.requestId` and on the bound child logger (`ctx.state.log`).
401
+ - For tracing, the `otelTracing(opts)` middleware (from `@daloyjs/core`)
402
+ emits OpenTelemetry-compatible spans. Enable it once the user wires up an
403
403
  exporter.
404
404
 
405
405
  ## Configuration & secrets
@@ -10,4 +10,7 @@ coverage
10
10
  .env.*
11
11
  !.env.example
12
12
  generated
13
- otherdocs
13
+ # Private keys and certificates never belong in an image.
14
+ *.pem
15
+ *.key
16
+ *.p12
@@ -1 +1,12 @@
1
+ # Copy to .env for local use. Real secrets belong in your platform's secret
2
+ # store, never in git (.env is git-ignored).
1
3
  PORT=3000
4
+
5
+ # Number of reverse-proxy hops in front of the app (load balancer, PaaS edge,
6
+ # CDN). Required in production behind any proxy: without it DaloyJS refuses
7
+ # forwarded headers and answers 500. rateLimit() also uses it to key each
8
+ # client instead of the proxy. Leave unset when clients connect directly.
9
+ # TRUST_PROXY_HOPS=1
10
+
11
+ # Public base URL, used as the OpenAPI `servers` entry (no localhost default).
12
+ # PUBLIC_URL=https://api.example.com
@@ -12,3 +12,8 @@ generated/
12
12
  # consumer's lockfile and can pin transitives to unsigned tarballs.
13
13
  # See https://socket.dev/blog/understanding-the-security-concerns-of-npm-shrinkwrap
14
14
  npm-shrinkwrap.json
15
+
16
+ # Private keys and certificates (keep them in a secret manager, not git).
17
+ *.pem
18
+ *.key
19
+ *.p12
@@ -3,6 +3,9 @@
3
3
  # See the DaloyJS "Supply chain" docs and the 2026-05-11 TanStack incident
4
4
  # postmortem (https://tanstack.com/blog/npm-supply-chain-compromise-postmortem)
5
5
  # for context on why every line below is on by default.
6
+ #
7
+ # pnpm 11+ reads its settings from pnpm-workspace.yaml, not from this file,
8
+ # so the same values are set there. They stay here for npm and older tooling.
6
9
 
7
10
  auto-install-peers=true
8
11
  strict-peer-dependencies=true
@@ -11,7 +11,7 @@
11
11
  "start": "node dist/index.js",
12
12
  "build": "tsc -p tsconfig.build.json",
13
13
  "typecheck": "tsc --noEmit",
14
- "test": "node --test tests/**/*.test.ts",
14
+ "test": "node --test \"tests/**/*.test.ts\"",
15
15
  "contract": "daloy inspect --check src/build-app.ts",
16
16
  "gen:openapi": "node scripts/dump-openapi.ts",
17
17
  "gen:client": "openapi-ts",
@@ -20,7 +20,7 @@
20
20
  "hooks:install": "git config core.hooksPath .githooks"
21
21
  },
22
22
  "dependencies": {
23
- "@daloyjs/core": "^1.3.7",
23
+ "@daloyjs/core": "^1.4.1",
24
24
  "zod": "^4.4.3"
25
25
  },
26
26
  "devDependencies": {
@@ -22,3 +22,19 @@ minimumReleaseAge: 1440
22
22
  # Transitive deps must resolve from the configured registry, which makes
23
23
  # typosquatted-tarball and compromised-fork attacks much harder to land.
24
24
  blockExoticSubdeps: true
25
+
26
+ # pnpm 11+ ignores these keys in `.npmrc`, so they live here. `.npmrc` keeps
27
+ # the same values for older tooling.
28
+ #
29
+ # Never run dependency lifecycle scripts (postinstall / preinstall / prepare):
30
+ # the main execution channel of the chalk/debug, node-ipc and Shai-Hulud
31
+ # malware. If you later need a build step, allowlist that one package with
32
+ # `allowBuilds` instead of turning this off.
33
+ ignoreScripts: true
34
+ # Fail on unmet or incompatible peer dependencies instead of warning.
35
+ strictPeerDependencies: true
36
+ # Re-verify package integrity hashes against the lockfile on install.
37
+ verifyStoreIntegrity: true
38
+ # Use the lockfile as-is whenever it satisfies package.json.
39
+ preferFrozenLockfile: true
40
+ autoInstallPeers: true
@@ -19,7 +19,7 @@ A [DaloyJS](https://daloyjs.dev) REST API deployed to **Vercel** on the **Node.j
19
19
  - `pnpm test` — run test suite
20
20
  - `pnpm contract` — run `daloy inspect --check api/index.ts`
21
21
  - `pnpm hooks:install` — enable the optional pre-push contract gate
22
- - `pnpm deploy` — deploy to Vercel
22
+ - `pnpm run deploy` — deploy to Vercel
23
23
  - `pnpm audit` — supply-chain audit
24
24
 
25
25
  ## Project shape
@@ -18,6 +18,11 @@ curl http://localhost:3000/books/1
18
18
  <!-- daloy-minimal:strip-end books -->
19
19
  ```
20
20
 
21
+ > **Install refused right after a DaloyJS release?** New installs wait 24 hours
22
+ > before using a freshly published version (`minimumReleaseAge` in
23
+ > `pnpm-workspace.yaml`), a supply-chain safeguard. If `@daloyjs/core` was just
24
+ > released, retry a few hours later rather than turning the safeguard off.
25
+
21
26
  <!-- daloy-minimal:strip-start docs -->
22
27
 
23
28
  ## API documentation
@@ -48,9 +53,21 @@ pnpm hooks:install # points core.hooksPath at .githooks
48
53
  ## Deploy
49
54
 
50
55
  ```bash
51
- pnpm deploy
56
+ pnpm run deploy
52
57
  ```
53
58
 
59
+ One-time Vercel project setting: add the environment variable
60
+ `ENABLE_EXPERIMENTAL_COREPACK=1` (Project → Settings → Environment Variables).
61
+ With it, Vercel builds with the pnpm pinned in `package.json#packageManager`
62
+ (the same version as CI). Without it, Vercel guesses pnpm 9 from the lockfile,
63
+ which fails this project's `engines.pnpm >= 11` check and aborts the build.
64
+
65
+ This template uses TypeScript 5.9 on purpose: Vercel's Node builder loads the
66
+ project's `typescript` package through its JavaScript API, which TypeScript 7
67
+ (the native compiler) no longer provides, and the Vercel CLI's own tooling
68
+ declares `typescript ^4 || ^5` as a peer. Move up once Vercel supports newer
69
+ TypeScript.
70
+
54
71
  The API entry lives at `api/index.ts` and uses `@daloyjs/core/vercel`:
55
72
 
56
73
  ```ts
@@ -81,7 +98,7 @@ export default toWebHandler(app);
81
98
 
82
99
  So DaloyJS owns all routing and the app's routes are served at the **site root**
83
100
  (`/healthz`, `/docs`, `/openapi.json`, …) rather than under `/api/*`. Without
84
- this rewrite the function only answers `/api/*` and the root domain returns a
101
+ this rewrite the function only answers `/api` and the root domain returns a
85
102
  Vercel 404. (The demo defines no `/` route, so the bare root returns the app's
86
103
  problem+json 404 — visit `/docs` or `/healthz`.)
87
104