create-daloy 1.4.0 → 1.5.0

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
@@ -95,6 +95,9 @@ const RENAME_ON_COPY = new Map([
95
95
  ["_github", ".github"],
96
96
  ["_Dockerfile", "Dockerfile"],
97
97
  ["_dockerignore", ".dockerignore"],
98
+ // Keeps local secrets out of `vercel deploy` uploads: the Vercel CLI does
99
+ // not apply .gitignore to what it uploads.
100
+ ["_vercelignore", ".vercelignore"],
98
101
  // Directory: holds skill files for AI coding agents under
99
102
  // `.agents/skills/<skill-name>/SKILL.md`. Templates author this as
100
103
  // `_agents/` so npm pack does not drop the dotfolder during publish.
@@ -548,6 +551,10 @@ const TOOL_INSTALL_GUIDES = {
548
551
  const MIN_NODE_MAJOR = 24;
549
552
  const MIN_NPM_MAJOR = 12;
550
553
  const MIN_PNPM_MAJOR = 11;
554
+ // Exact pnpm used by the generated CI (pnpm/action-setup) and, for templates
555
+ // whose platform needs it, by package.json#packageManager. One constant so the
556
+ // two can never drift.
557
+ const PINNED_PNPM_VERSION = "11.1.3";
551
558
 
552
559
  /**
553
560
  * Parse the leading major-version integer out of a version string such as
@@ -719,7 +726,7 @@ async function copyTemplate(src, dest) {
719
726
  }
720
727
  }
721
728
 
722
- async function patchPackageJson(dir, projectName, packageManager) {
729
+ async function patchPackageJson(dir, projectName, packageManager, template) {
723
730
  const file = path.join(dir, "package.json");
724
731
  if (!existsSync(file)) return;
725
732
  const raw = await readFile(file, "utf8");
@@ -742,6 +749,11 @@ async function patchPackageJson(dir, projectName, packageManager) {
742
749
  }
743
750
  if (packageManager === "pnpm") {
744
751
  json.engines = { ...json.engines, pnpm: `>=${MIN_PNPM_MAJOR}.0.0` };
752
+ // Vercel otherwise picks pnpm from the lockfile format (9.x/10.x), which
753
+ // fails the engines.pnpm floor above and aborts every deploy. With this
754
+ // field plus the ENABLE_EXPERIMENTAL_COREPACK=1 project variable (see the
755
+ // template README), Vercel builds with the same pnpm as CI.
756
+ if (template === "vercel") json.packageManager = `pnpm@${PINNED_PNPM_VERSION}`;
745
757
  }
746
758
  await writeFile(file, JSON.stringify(json, null, 2) + "\n", "utf8");
747
759
  }
@@ -767,16 +779,27 @@ async function patchTemplateTextFiles(dir, packageManager) {
767
779
  }
768
780
 
769
781
  function rewritePackageManagerText(raw, packageManager) {
770
- return raw
782
+ // The release-day cooldown note names pnpm's settings file. Bun scaffolds
783
+ // get the same cooldown from bunfig.toml; npm and yarn scaffolds have no
784
+ // cooldown configured, so the note would be wrong there and is dropped.
785
+ const cooldownNote =
786
+ /> \*\*Install refused right after a DaloyJS release\?\*\*[\s\S]*?safeguard off\.\r?\n\r?\n/;
787
+ const withCooldownNote =
788
+ packageManager === "bun"
789
+ ? raw.replace("`pnpm-workspace.yaml`), a supply-chain", "`bunfig.toml`), a supply-chain")
790
+ : raw.replace(cooldownNote, "");
791
+ return withCooldownNote
771
792
  .replace(
772
793
  "Package manager: pnpm (use `pnpm` unless the project's `package.json` was rewritten for npm/yarn/bun).",
773
794
  `Package manager: ${packageManager}.`
774
795
  )
796
+ .replace("Package manager: pnpm.", `Package manager: ${packageManager}.`)
775
797
  .replace(/\bpnpm install\b/g, `${packageManager} install`)
776
798
  .replace(/\bpnpm gen:openapi\b/g, `${packageManager} run gen:openapi`)
777
799
  .replace(/\bpnpm gen:client\b/g, `${packageManager} run gen:client`)
778
800
  .replace(/\bpnpm typecheck\b/g, `${packageManager} run typecheck`)
779
801
  .replace(/\bpnpm build\b/g, `${packageManager} run build`)
802
+ .replace(/\bpnpm run deploy\b/g, `${packageManager} run deploy`)
780
803
  .replace(/\bpnpm deploy\b/g, `${packageManager} run deploy`)
781
804
  .replace(/\bpnpm dev\b/g, `${packageManager} run dev`)
782
805
  .replace(/\bpnpm gen\b/g, `${packageManager} run gen`)
@@ -843,6 +866,20 @@ const NPM_STRICT_NPMRC = `# DaloyJS install-time guardrails for the npm CLI.
843
866
  engine-strict=true
844
867
  `;
845
868
 
869
+ // Bun reads its install settings from bunfig.toml, not from pnpm-workspace.yaml
870
+ // or .npmrc. Without this file a Bun scaffold would have no release-age
871
+ // cooldown at all, silently dropping the 24h supply-chain floor the pnpm path
872
+ // enforces. `minimumReleaseAge` is in seconds and needs Bun >= 1.3; older Bun
873
+ // ignores the key, which is why bun-basic's engines floor is >= 1.3.0.
874
+ const BUN_BUNFIG = `# DaloyJS install-time guardrails for Bun.
875
+ #
876
+ # Wait 24h (86400 seconds) before resolving a freshly published version.
877
+ # Most npm worm campaigns are detected and unpublished within hours. Set to 0
878
+ # only for a real hotfix. Requires Bun >= 1.3.
879
+ [install]
880
+ minimumReleaseAge = 86400
881
+ `;
882
+
846
883
  async function normalizePackageManagerFiles(dir, packageManager) {
847
884
  if (packageManager === "pnpm") return;
848
885
  // pnpm-workspace.yaml is pnpm-only; drop it for every other manager.
@@ -860,6 +897,9 @@ async function normalizePackageManagerFiles(dir, packageManager) {
860
897
  } else if (existsSync(npmrc)) {
861
898
  await rm(npmrc, { force: true });
862
899
  }
900
+ if (packageManager === "bun") {
901
+ await writeFile(path.join(dir, "bunfig.toml"), BUN_BUNFIG, "utf8");
902
+ }
863
903
  }
864
904
 
865
905
  function dockerInstallSnippet(packageManager) {
@@ -902,7 +942,7 @@ async function patchDockerfileForPackageManager(dir, packageManager) {
902
942
  // an `\n`-only pattern would silently fail to match there, leaving the
903
943
  // pnpm-specific COPY/RUN lines in place for npm/yarn/bun scaffolds.
904
944
  let next = raw.replace(
905
- /COPY package\.json pnpm-lock\.yaml\* \.\/\r?\nRUN corepack enable && corepack prepare pnpm@latest --activate && \\\r?\n\s+pnpm install --frozen-lockfile --ignore-scripts/,
945
+ /COPY package\.json pnpm-lock\.yaml\* \.\/\r?\nRUN corepack enable && corepack prepare pnpm@[\w.-]+ --activate && \\\r?\n\s+pnpm install --frozen-lockfile --ignore-scripts/,
906
946
  `${install.copy}\n${install.run}`
907
947
  );
908
948
 
@@ -1031,7 +1071,7 @@ function setupPackageManagerStep(packageManager) {
1031
1071
  return ` - name: Set up pnpm
1032
1072
  uses: pnpm/action-setup@ac6db6d3c1f721f886538a378a2d73e85697340a # v6
1033
1073
  with:
1034
- version: 11.1.3
1074
+ version: ${PINNED_PNPM_VERSION}
1035
1075
  run_install: false`;
1036
1076
  }
1037
1077
  if (packageManager === "yarn") {
@@ -1329,9 +1369,23 @@ async function addDenoSecurityTasks(dir) {
1329
1369
  */
1330
1370
  function nodeVersionStrategy(template) {
1331
1371
  if (template === "bun-basic") {
1332
- return { strategy: "", version: "24" };
1372
+ return {
1373
+ strategy: "",
1374
+ version: "24",
1375
+ checksNote: [
1376
+ "# The verify job runs once (Bun is the runtime, so there is no Node",
1377
+ "# version matrix). Protect `main` with the single `Verify` check.",
1378
+ ].join("\n"),
1379
+ };
1333
1380
  }
1334
1381
  return {
1382
+ checksNote: [
1383
+ "# The verify job runs as a matrix across both ends of the `engines.node`",
1384
+ "# range declared in package.json, so a change that only works on one of",
1385
+ "# them fails here instead of in production. That means the required status",
1386
+ "# checks to protect `main` with are the per-version jobs (`Verify (24)`,",
1387
+ "# `Verify (26)`), not a single `Verify`.",
1388
+ ].join("\n"),
1335
1389
  strategy: [
1336
1390
  " strategy:",
1337
1391
  " fail-fast: false",
@@ -1445,6 +1499,7 @@ function renderCiReplacements({
1445
1499
  ["__BUILD_STEP__", buildStep],
1446
1500
  ["__CONTRACT_STEP__", contractStep],
1447
1501
  ["__CI_NODE_STRATEGY__", nodeVersions.strategy],
1502
+ ["__CI_REQUIRED_CHECKS_NOTE__", nodeVersions.checksNote],
1448
1503
  ["__CI_NODE_VERSION__", nodeVersions.version],
1449
1504
  ["__DAST_LAUNCH_COMMAND__", dast.command],
1450
1505
  ["__DAST_EXTRA_ENV__", dast.extraEnv],
@@ -2339,7 +2394,7 @@ async function main() {
2339
2394
  logStep("Minimal mode applied", `${count} file${count === 1 ? "" : "s"} trimmed`);
2340
2395
  }
2341
2396
  if (!skipPackageManager) {
2342
- await patchPackageJson(targetDir, packageName, packageManager);
2397
+ await patchPackageJson(targetDir, packageName, packageManager, template);
2343
2398
  logStep("Package metadata written", packageName);
2344
2399
  await patchTemplateTextFiles(targetDir, packageManager);
2345
2400
  await patchDockerignoreForPackageManager(targetDir, packageManager);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-daloy",
3
- "version": "1.4.0",
3
+ "version": "1.5.0",
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",
package/sbom.cdx.json CHANGED
@@ -1,25 +1,25 @@
1
1
  {
2
2
  "bomFormat": "CycloneDX",
3
3
  "specVersion": "1.5",
4
- "serialNumber": "urn:uuid:5f0f2cd1-d8e0-57ff-8eed-0d66ff49df85",
4
+ "serialNumber": "urn:uuid:7e070550-e391-54c4-9693-0a1f0c97d9ad",
5
5
  "version": 1,
6
6
  "metadata": {
7
- "timestamp": "2026-09-28T08:58:10.492Z",
7
+ "timestamp": "2026-09-29T22:26:34.115Z",
8
8
  "tools": [
9
9
  {
10
10
  "vendor": "DaloyJS",
11
11
  "name": "daloy-generate-sbom",
12
- "version": "1.4.0"
12
+ "version": "1.5.0"
13
13
  }
14
14
  ],
15
15
  "authors": [],
16
16
  "component": {
17
17
  "type": "library",
18
- "bom-ref": "pkg:npm/create-daloy@1.4.0",
18
+ "bom-ref": "pkg:npm/create-daloy@1.5.0",
19
19
  "name": "create-daloy",
20
- "version": "1.4.0",
20
+ "version": "1.5.0",
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.4.0",
22
+ "purl": "pkg:npm/create-daloy@1.5.0",
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.4.0",
45
+ "tagId": "swidtag-create-daloy-1.5.0",
46
46
  "name": "create-daloy",
47
- "version": "1.4.0",
47
+ "version": "1.5.0",
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.4.0",
56
+ "ref": "pkg:npm/create-daloy@1.5.0",
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.4.0",
6
- "documentNamespace": "https://github.com/daloyjs/daloy/sbom/create-daloy-1.4.0-5f0f2cd1-d8e0-57ff-8eed-0d66ff49df85",
5
+ "name": "create-daloy-1.5.0",
6
+ "documentNamespace": "https://github.com/daloyjs/daloy/sbom/create-daloy-1.5.0-7e070550-e391-54c4-9693-0a1f0c97d9ad",
7
7
  "creationInfo": {
8
- "created": "2026-09-28T08:58:10.492Z",
8
+ "created": "2026-09-29T22:26:34.115Z",
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.4.0",
19
+ "versionInfo": "1.5.0",
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.4.0"
30
+ "referenceLocator": "pkg:npm/create-daloy@1.5.0"
31
31
  }
32
32
  ]
33
33
  }
@@ -116,6 +116,16 @@ jobs:
116
116
  ref="${ref%% as *}"
117
117
  ref="${ref## }"
118
118
  ref="${ref%% }"
119
+ # Resolve `${NAME}` against this Dockerfile's `ARG NAME=default`
120
+ # line, so ARG-driven base images are checked instead of skipped.
121
+ case "$ref" in
122
+ '${'*'}')
123
+ name="${ref#??}"
124
+ name="${name%?}"
125
+ def="$(grep -E "^ARG ${name}=" Dockerfile | head -n 1 | cut -d= -f2-)"
126
+ [ -n "$def" ] && ref="$def"
127
+ ;;
128
+ esac
119
129
  case "$ref" in
120
130
  scratch|"\${"*|*"@sha256:"*) ;;
121
131
  *)
@@ -59,9 +59,18 @@ jobs:
59
59
  with:
60
60
  deno-version: v2.x
61
61
 
62
+ - name: Install dependencies (frozen lockfile)
63
+ # Same integrity gate as ci.yml: fail when any dependency does not
64
+ # match the committed deno.lock, so a deploy never ships something
65
+ # the OSV scan did not see.
66
+ run: deno install --frozen=true
67
+
62
68
  - name: Typecheck
63
69
  run: deno task typecheck
64
70
 
71
+ - name: Contract check
72
+ run: deno task contract
73
+
65
74
  - name: Test
66
75
  run: deno task test
67
76
 
@@ -1,10 +1,6 @@
1
1
  # Hardened CI generated by create-daloy --with-ci.
2
2
  #
3
- # The verify job runs as a matrix across both ends of the `engines.node`
4
- # range declared in package.json, so a change that only works on one of
5
- # them fails here instead of in production. That means the required status
6
- # checks to protect `main` with are the per-version jobs (`Verify (24)`,
7
- # `Verify (26)`), not a single `Verify`.
3
+ __CI_REQUIRED_CHECKS_NOTE__
8
4
  name: CI
9
5
 
10
6
  on:
@@ -134,6 +134,16 @@ jobs:
134
134
  ref="${ref%% as *}"
135
135
  ref="${ref## }"
136
136
  ref="${ref%% }"
137
+ # Resolve `${NAME}` against this Dockerfile's `ARG NAME=default`
138
+ # line, so ARG-driven base images are checked instead of skipped.
139
+ case "$ref" in
140
+ '${'*'}')
141
+ name="${ref#??}"
142
+ name="${name%?}"
143
+ def="$(grep -E "^ARG ${name}=" Dockerfile | head -n 1 | cut -d= -f2-)"
144
+ [ -n "$def" ] && ref="$def"
145
+ ;;
146
+ esac
137
147
  case "$ref" in
138
148
  scratch|"\${"*|*"@sha256:"*) ;;
139
149
  *)
@@ -2,7 +2,7 @@
2
2
 
3
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. With `docs: "auto"` in `new App({...})`, three routes are auto-mounted outside production: `GET /openapi.json`, `GET /openapi.yaml`, and `GET /docs` (Scalar UI).
4
4
 
5
- - Package manager / runtime: Bun.
5
+ - Runtime: Bun. Package manager: pnpm.
6
6
 
7
7
  ## Agent guidance
8
8
 
@@ -5,7 +5,7 @@ A [DaloyJS](https://daloyjs.dev) starter for the [Bun](https://bun.sh) runtime.
5
5
  ## Develop
6
6
 
7
7
  ```bash
8
- bun install
8
+ pnpm install
9
9
  bun run dev # http://localhost:3000
10
10
  ```
11
11
 
@@ -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
@@ -22,8 +22,11 @@
22
22
  # docker build --build-arg \
23
23
  # NODE_IMAGE=node:24-alpine@sha256:<digest> \
24
24
  # BUN_IMAGE=oven/bun:1-alpine@sha256:<digest> .
25
- # Dependabot's `docker` ecosystem (see `.github/dependabot.yml`)
26
- # keeps the digest fresh. The companion `container-scan.yml`
25
+ # The default is a floating tag, and Dependabot's `docker` ecosystem
26
+ # cannot update a `FROM ${ARG}` line. For automatic digest updates,
27
+ # write the digest into the ARG default below (for example
28
+ # `ARG NODE_IMAGE=node:24-alpine@sha256:<digest>`).
29
+ # The companion `container-scan.yml`
27
30
  # workflow lints this file with hadolint and scans the built image
28
31
  # with Trivy on every PR.
29
32
 
@@ -35,7 +38,7 @@ FROM ${NODE_IMAGE} AS builder
35
38
  WORKDIR /app
36
39
  # Install deps in a layer that only invalidates when manifests change.
37
40
  COPY package.json pnpm-lock.yaml* ./
38
- RUN corepack enable && corepack prepare pnpm@latest --activate && \
41
+ RUN corepack enable && corepack prepare pnpm@11.1.3 --activate && \
39
42
  pnpm install --frozen-lockfile --ignore-scripts
40
43
  COPY . .
41
44
 
@@ -201,7 +201,7 @@ app.get(
201
201
  - Throw typed errors from `@daloyjs/core` — they serialize to RFC 9457
202
202
  problem responses.
203
203
  - Add a `responses[code]` entry for every error you throw.
204
- - Do not swallow errors. Log via `ctx.log.error(err, "context")` and
204
+ - Do not swallow errors. Log via `ctx.state.log.error({ err }, "context")` and
205
205
  rethrow if recovery is impossible.
206
206
 
207
207
  ## Middleware
@@ -267,15 +267,15 @@ Tests run with `bun test`. Use **in-process** requests through
267
267
 
268
268
  ```ts
269
269
  import { test, expect } from "bun:test";
270
- import { buildApp } from "../src/build-app";
270
+ import { buildApp } from "../src/build-app.ts";
271
271
 
272
272
  test("GET /healthz returns ok", async () => {
273
273
  const app = buildApp();
274
274
  const res = await app.request("/healthz");
275
275
  expect(res.status).toBe(200);
276
- const body = await res.json();
276
+ const body = (await res.json()) as { ok: boolean; runtime: string };
277
277
  expect(body.ok).toBe(true);
278
- expect(typeof body.uptime).toBe("number");
278
+ expect(body.runtime).toBe("bun");
279
279
  });
280
280
  ```
281
281
 
@@ -324,7 +324,7 @@ Aim for complete happy- and unhappy-path test coverage of the routes you add.
324
324
  of it when URLs can be influenced by users or tenants. SSRF protections
325
325
  should fail closed for private ranges and cloud metadata endpoints.
326
326
  - Bun ships its own audit story; check `bun pm audit` periodically and
327
- pin versions in `bun.lockb`.
327
+ pin versions in the committed lockfile.
328
328
 
329
329
  ## CI and workflows (`--with-ci` scaffolds)
330
330
 
@@ -334,7 +334,7 @@ reference. Skip that file for ordinary route work.
334
334
 
335
335
  ## Logging & observability
336
336
 
337
- - Use the framework logger via `ctx.log` — it carries the request id
337
+ - Use the framework logger via `ctx.state.log` — it carries the request id
338
338
  automatically.
339
339
  - Avoid `console.log` in production code paths; the structured logger
340
340
  emits JSON for log aggregators.
@@ -9,4 +9,7 @@ coverage
9
9
  .env.*
10
10
  !.env.example
11
11
  generated
12
- otherdocs
12
+ # Private keys and certificates never belong in an image.
13
+ *.pem
14
+ *.key
15
+ *.p12
@@ -0,0 +1,12 @@
1
+ # Copy to .env for local use (Bun loads .env automatically). Real secrets
2
+ # belong in your platform's secret store, never in git (.env is git-ignored).
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
@@ -13,3 +13,8 @@ bun.lockb
13
13
  # consumer's lockfile and can pin transitives to unsigned tarballs.
14
14
  # See https://socket.dev/blog/understanding-the-security-concerns-of-npm-shrinkwrap
15
15
  npm-shrinkwrap.json
16
+
17
+ # Private keys and certificates (keep them in a secret manager, not git).
18
+ *.pem
19
+ *.key
20
+ *.p12
@@ -1,9 +1,11 @@
1
1
  # DaloyJS supply-chain hardening defaults.
2
2
  #
3
- # These pnpm-specific defaults are kept here so users who switch this Bun
4
- # starter back to pnpm inherit the same secure-by-default install posture.
3
+ # pnpm is this template's default package manager (Bun is the runtime).
5
4
  # When you scaffold with `--package-manager bun` the CLI removes this file
6
- # so `bun install` does not warn about unsupported keys.
5
+ # and writes bunfig.toml with the same 24h cooldown instead.
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.
7
9
 
8
10
  auto-install-peers=true
9
11
  strict-peer-dependencies=true
@@ -4,7 +4,7 @@
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "engines": {
7
- "bun": ">=1.1.0"
7
+ "bun": ">=1.3.0"
8
8
  },
9
9
  "scripts": {
10
10
  "dev": "daloy dev --runtime bun",
@@ -19,12 +19,12 @@
19
19
  "hooks:install": "git config core.hooksPath .githooks"
20
20
  },
21
21
  "dependencies": {
22
- "@daloyjs/core": "^1.4.0",
22
+ "@daloyjs/core": "^1.5.0",
23
23
  "zod": "^4.4.3"
24
24
  },
25
25
  "devDependencies": {
26
26
  "@hey-api/openapi-ts": "0.0.0-next-20260711024907",
27
- "@types/bun": "^1.1.0",
27
+ "@types/bun": "^1.3.0",
28
28
  "typescript": "^7.0.2"
29
29
  }
30
30
  }
@@ -1,10 +1,8 @@
1
1
  # pnpm workspace-level supply-chain settings.
2
2
  #
3
- # This file is here even though the default package manager for this
4
- # template is Bun, because users can switch to pnpm with
5
- # `pnpm create daloy --package-manager pnpm`. When that happens, the
6
- # scaffolder keeps this file so pnpm 11+ picks up the workspace-level
7
- # supply-chain controls below. If you keep using Bun, you can delete it.
3
+ # pnpm is this template's default package manager (Bun is the runtime).
4
+ # Scaffolding with `--package-manager bun` removes this file and writes a
5
+ # bunfig.toml with the same 24h release-age cooldown instead.
8
6
  #
9
7
  # pnpm 11+ reads `pnpm-workspace.yaml` as a settings file even when no
10
8
  # `packages:` list is declared. It is the only file format that accepts
@@ -23,3 +21,19 @@ minimumReleaseAge: 1440
23
21
  # Transitive deps must resolve from the configured registry, which makes
24
22
  # typosquatted-tarball and compromised-fork attacks much harder to land.
25
23
  blockExoticSubdeps: true
24
+
25
+ # pnpm 11+ ignores these keys in `.npmrc`, so they live here. `.npmrc` keeps
26
+ # the same values for older tooling.
27
+ #
28
+ # Never run dependency lifecycle scripts (postinstall / preinstall / prepare):
29
+ # the main execution channel of the chalk/debug, node-ipc and Shai-Hulud
30
+ # malware. If you later need a build step, allowlist that one package with
31
+ # `allowBuilds` instead of turning this off.
32
+ ignoreScripts: true
33
+ # Fail on unmet or incompatible peer dependencies instead of warning.
34
+ strictPeerDependencies: true
35
+ # Re-verify package integrity hashes against the lockfile on install.
36
+ verifyStoreIntegrity: true
37
+ # Use the lockfile as-is whenever it satisfies package.json.
38
+ preferFrozenLockfile: true
39
+ autoInstallPeers: true
@@ -11,7 +11,9 @@ const handle = serve(app, {
11
11
  idleTimeout: 30,
12
12
  });
13
13
 
14
- const url = handle.url ? String(handle.url) : `http://localhost:${port}`;
14
+ // `handle.url` is a URL whose string form ends in "/"; use the origin so the
15
+ // links below do not become "//docs" (which 404s).
16
+ const url = handle.url ? handle.url.origin : `http://localhost:${port}`;
15
17
  const links: StartupBannerLink[] = [
16
18
  // daloy-minimal:strip-start docs
17
19
  { label: "API docs", url: `${url}/docs` },
@@ -4,7 +4,7 @@
4
4
  "module": "ESNext",
5
5
  "moduleResolution": "Bundler",
6
6
  "lib": ["ES2022"],
7
- "types": ["bun-types"],
7
+ "types": ["bun"],
8
8
  "strict": true,
9
9
  "noUncheckedIndexedAccess": true,
10
10
  "esModuleInterop": true,
@@ -19,7 +19,7 @@ A [DaloyJS](https://daloyjs.dev) REST API deployed to **Cloudflare Workers**. **
19
19
  - `pnpm test` — run test suite
20
20
  - `pnpm contract` — run `daloy inspect --check src/index.ts`
21
21
  - `pnpm hooks:install` — enable the optional pre-push contract gate
22
- - `pnpm deploy` — `wrangler deploy`
22
+ - `pnpm run deploy` — `wrangler deploy`
23
23
  - `pnpm audit` — supply-chain audit
24
24
 
25
25
  ## Project shape
@@ -9,10 +9,15 @@ pnpm install
9
9
  pnpm dev # http://localhost:8787
10
10
  ```
11
11
 
12
+ > **Install refused right after a DaloyJS release?** New installs wait 24 hours
13
+ > before using a freshly published version (`minimumReleaseAge` in
14
+ > `pnpm-workspace.yaml`), a supply-chain safeguard. If `@daloyjs/core` was just
15
+ > released, retry a few hours later rather than turning the safeguard off.
16
+
12
17
  ## Deploy
13
18
 
14
19
  ```bash
15
- pnpm deploy
20
+ pnpm run deploy
16
21
  ```
17
22
 
18
23
  `@daloyjs/core/cloudflare` exposes `toFetchHandler(app)`, so the same `App` you would use on Node also runs on Workers.
@@ -34,7 +34,7 @@ ARG NODE_IMAGE=node:24-alpine
34
34
  FROM ${NODE_IMAGE} AS builder
35
35
  WORKDIR /app
36
36
  COPY package.json pnpm-lock.yaml* ./
37
- RUN corepack enable && corepack prepare pnpm@latest --activate && \
37
+ RUN corepack enable && corepack prepare pnpm@11.1.3 --activate && \
38
38
  pnpm install --frozen-lockfile --ignore-scripts
39
39
  COPY . .
40
40