create-daloy 0.34.2 → 0.34.3

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
@@ -57,6 +57,7 @@ pnpm create daloy@latest my-api \
57
57
  | `--git` / `--no-git` | Initialize a git repository. Defaults to interactive. |
58
58
  | `--minimal` | Strip the bookstore demo route and the built-in `/docs` + `/openapi.json` routes so only the framework bootstrap and `/healthz` ship. |
59
59
  | `--with-ci` / `--no-ci` | Add the hardened GitHub Actions, Dependabot, CODEOWNERS, SECURITY.md, and lockfile-source verification bundle. **Defaults to Y** so scaffolded projects are secure by default. |
60
+ | `--with-deploy` / `--no-deploy` | Add the starter `.github/workflows/deploy.yml`. Defaults to the same value as `--with-ci`, so you can keep CI but opt out of deploy scaffolding with `--no-deploy`. |
60
61
  | `--code-owner <owner>` | Replace the CODEOWNERS placeholder when `--with-ci` is used, for example `@acme/security`. |
61
62
  | `--force` | Overwrite an existing non-empty directory. |
62
63
  | `--yes` | Accept all defaults; never prompt. |
@@ -153,6 +154,12 @@ For Node-style templates, the bundle adds:
153
154
  - `.github/workflows/ci.yml` with top-level `permissions: {}`, pinned actions,
154
155
  `harden-runner`, `persist-credentials: false`, no package-manager cache, and
155
156
  install scripts disabled.
157
+ - `.github/workflows/deploy.yml` as a manual-only deployment starter. Container
158
+ templates publish a Docker image to GHCR with the repo-scoped `GITHUB_TOKEN`,
159
+ while Vercel and Cloudflare templates ship concrete CLI deploy steps that
160
+ read their platform credentials from GitHub Actions secrets/variables. The
161
+ deploy job is gated to `main` or a tag by default, and Node-style templates
162
+ re-run `verify:lockfile` before shipping.
156
163
  - `.github/workflows/vuln-scan.yml` — a daily scheduled SCA cron that runs the
157
164
  package manager's audit against the committed lockfile. Catches CVEs disclosed
158
165
  *after* the last PR or push and provides SOC 2 CC7.1
@@ -166,8 +173,14 @@ The bundle deliberately does **not** generate an npm publish workflow.
166
173
  `create-daloy` scaffolds REST API services, not libraries; if you later carve
167
174
  out a reusable package, opt into npm trusted publishing yourself.
168
175
 
169
- For `deno-basic`, `--with-ci` generates a Deno-native CI workflow plus CodeQL,
170
- Scorecard, zizmor, Dependabot for GitHub Actions, CODEOWNERS, and `SECURITY.md`.
176
+ For `deno-basic`, `--with-ci` generates a Deno-native CI workflow, a manual-only
177
+ container publish starter for GHCR that is guarded to `main` or a tag by
178
+ default, plus CodeQL, Scorecard, zizmor, Dependabot for GitHub Actions,
179
+ CODEOWNERS, and `SECURITY.md`.
180
+
181
+ If you want the governance bundle but not the deployment starter, pass
182
+ `--with-ci --no-deploy`. If you only want a deployment starter, pass
183
+ `--with-deploy --no-ci`.
171
184
 
172
185
  If you omit `--code-owner`, the generated CODEOWNERS file uses
173
186
  `@your-org/security-team` as a placeholder. Replace it before relying on branch
@@ -333,6 +333,7 @@ ${heading("Options")}
333
333
  ${color(COLORS.green, "--git / --no-git")} Initialize a git repository.
334
334
  ${color(COLORS.green, "--minimal")} Strip the bookstore + OpenAPI docs demo routes.
335
335
  ${color(COLORS.green, "--with-ci / --no-ci")} Add hardened GitHub Actions + governance files. ${color(COLORS.dim, "(default: Y)")}
336
+ ${color(COLORS.green, "--with-deploy / --no-deploy")} Add starter deploy.yml workflow(s). ${color(COLORS.dim, "(default: inherits --with-ci)")}
336
337
  ${color(COLORS.green, "--code-owner <owner>")} CODEOWNERS owner for --with-ci, e.g. @acme/security.
337
338
  ${color(COLORS.green, "--force")} Overwrite an existing non-empty directory.
338
339
  ${color(COLORS.green, "--yes, -y")} Accept all defaults; never prompt.
@@ -380,6 +381,7 @@ function parseArgs(argv) {
380
381
  listTemplates: false,
381
382
  minimal: false,
382
383
  ci: undefined,
384
+ deploy: undefined,
383
385
  codeOwner: undefined,
384
386
  };
385
387
  const args = [...argv];
@@ -393,6 +395,8 @@ function parseArgs(argv) {
393
395
  else if (a === "--minimal") out.minimal = true;
394
396
  else if (a === "--with-ci") out.ci = true;
395
397
  else if (a === "--no-ci") out.ci = false;
398
+ else if (a === "--with-deploy") out.deploy = true;
399
+ else if (a === "--no-deploy") out.deploy = false;
396
400
  else if (a === "--code-owner") out.codeOwner = args.shift();
397
401
  else if (a?.startsWith("--code-owner=")) out.codeOwner = a.slice("--code-owner=".length);
398
402
  else if (a === "--install") out.install = true;
@@ -686,6 +690,15 @@ function runScriptCommand(packageManager, scriptName) {
686
690
  return `${packageManager} run ${scriptName}`;
687
691
  }
688
692
 
693
+ function execCommand(packageManager, binary, args = "") {
694
+ const suffix = args ? ` ${args}` : "";
695
+ if (packageManager === "pnpm") return `pnpm exec ${binary}${suffix}`;
696
+ if (packageManager === "npm") return `npx ${binary}${suffix}`;
697
+ if (packageManager === "yarn") return `yarn exec ${binary}${suffix}`;
698
+ if (packageManager === "bun") return `bunx ${binary}${suffix}`;
699
+ return `${binary}${suffix}`;
700
+ }
701
+
689
702
  function installCommand(packageManager) {
690
703
  if (packageManager === "pnpm") return "pnpm install --frozen-lockfile --ignore-scripts";
691
704
  if (packageManager === "npm") return "npm ci --ignore-scripts";
@@ -740,6 +753,163 @@ function setupBunStep() {
740
753
  bun-version: latest`;
741
754
  }
742
755
 
756
+ function setupNodeStep() {
757
+ return ` - name: Set up Node.js
758
+ uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
759
+ with:
760
+ node-version: 24
761
+ # Package-manager caching is intentionally disabled. Shared caches
762
+ # can bridge fork PRs into trusted branches when mis-keyed.`;
763
+ }
764
+
765
+ function installDependenciesStep(command) {
766
+ return ` - name: Install dependencies
767
+ run: ${command}
768
+ env:
769
+ npm_config_ignore_scripts: "true"`;
770
+ }
771
+
772
+ function joinWorkflowBlocks(blocks) {
773
+ return blocks.filter(Boolean).join("\n\n");
774
+ }
775
+
776
+ // Deploy job ref guard. `workflow_dispatch` accepts any branch by default, so
777
+ // a junior could trigger a production deploy from an unreviewed feature
778
+ // branch by accident. This `if:` limits the deploy job to `main` or a tag
779
+ // push while still allowing maintainers to flip the guard off if they need
780
+ // to deploy from a release branch.
781
+ function deployRefGuard() {
782
+ return ` # Lock production deploys to main or a tag push. \`workflow_dispatch\`
783
+ # accepts any branch, so this guard stops an accidental dispatch from a
784
+ # feature branch from shipping unreviewed code.
785
+ if: github.ref == 'refs/heads/main' || github.ref_type == 'tag'`;
786
+ }
787
+
788
+ function containerRegistryHeader() {
789
+ return `# Default target: GitHub Container Registry (GHCR).
790
+ # No extra secret is required; the workflow uses the repo-scoped GITHUB_TOKEN.
791
+ # After the image is published, point your platform at GHCR or replace the
792
+ # final push steps with your provider's deploy command.
793
+ #
794
+ # Optional container-host handoff examples:
795
+ # - Fly.io: install flyctl in a later step and run
796
+ # flyctl deploy --image "$IMAGE_REPO:sha-\${GITHUB_SHA}" --remote-only
797
+ # - Render: create an image-backed service that tracks
798
+ # ghcr.io/<owner>/<repo>:sha-\${GITHUB_SHA}`;
799
+ }
800
+
801
+ function containerPublishSteps() {
802
+ return ` - name: Log in to GHCR
803
+ env:
804
+ GH_TOKEN: \${{ github.token }}
805
+ run: |
806
+ set -eu
807
+ echo "$GH_TOKEN" | docker login ghcr.io -u "\${{ github.actor }}" --password-stdin
808
+
809
+ - name: Derive image name
810
+ run: |
811
+ set -eu
812
+ owner="\${GITHUB_REPOSITORY_OWNER,,}"
813
+ repo="\${GITHUB_REPOSITORY#*/}"
814
+ repo="\${repo,,}"
815
+ echo "IMAGE_REPO=ghcr.io/\${owner}/\${repo}" >> "$GITHUB_ENV"
816
+
817
+ - name: Build image
818
+ env:
819
+ DOCKER_BUILDKIT: "1"
820
+ run: |
821
+ set -eu
822
+ docker build \
823
+ --tag "$IMAGE_REPO:sha-\${GITHUB_SHA}" \
824
+ --tag "$IMAGE_REPO:latest" \
825
+ .
826
+
827
+ - name: Push image
828
+ run: |
829
+ set -eu
830
+ docker push "$IMAGE_REPO:sha-\${GITHUB_SHA}"
831
+ docker push "$IMAGE_REPO:latest"`;
832
+ }
833
+
834
+ function vercelDeployHeader() {
835
+ return `# Configure before the first run:
836
+ # - GitHub Actions secret: VERCEL_TOKEN
837
+ # - GitHub Actions variables or environment variables: VERCEL_ORG_ID and VERCEL_PROJECT_ID
838
+ # This workflow stays manual-only until you wire those in and decide whether
839
+ # you want automatic deploys from main.`;
840
+ }
841
+
842
+ function cloudflareDeployHeader() {
843
+ return `# Configure before the first run:
844
+ # - GitHub Actions secret: CLOUDFLARE_API_TOKEN
845
+ # - GitHub Actions variable or environment variable: CLOUDFLARE_ACCOUNT_ID
846
+ # Keep this manual-only until the worker is linked to the right account and
847
+ # the production environment has approval rules.`;
848
+ }
849
+
850
+ function providerDeploySetup({ packageManager, needsBunRuntime }) {
851
+ return joinWorkflowBlocks([
852
+ setupPackageManagerStep(packageManager),
853
+ setupNodeStep(),
854
+ needsBunRuntime ? setupBunStep() : "",
855
+ installDependenciesStep(installCommand(packageManager)),
856
+ ]);
857
+ }
858
+
859
+ function vercelDeploySteps(packageManager) {
860
+ return ` - name: Deploy to Vercel
861
+ env:
862
+ VERCEL_TOKEN: \${{ secrets.VERCEL_TOKEN }}
863
+ VERCEL_ORG_ID: \${{ vars.VERCEL_ORG_ID }}
864
+ VERCEL_PROJECT_ID: \${{ vars.VERCEL_PROJECT_ID }}
865
+ run: |
866
+ set -eu
867
+ : "\${VERCEL_TOKEN:?Set the VERCEL_TOKEN Actions secret before running this workflow.}"
868
+ : "\${VERCEL_ORG_ID:?Set the VERCEL_ORG_ID Actions variable before running this workflow.}"
869
+ : "\${VERCEL_PROJECT_ID:?Set the VERCEL_PROJECT_ID Actions variable before running this workflow.}"
870
+ ${execCommand(packageManager, "vercel", "deploy --prod --yes --token \"$VERCEL_TOKEN\"")}`;
871
+ }
872
+
873
+ function cloudflareDeploySteps(packageManager) {
874
+ return ` - name: Deploy to Cloudflare Workers
875
+ env:
876
+ CLOUDFLARE_API_TOKEN: \${{ secrets.CLOUDFLARE_API_TOKEN }}
877
+ CLOUDFLARE_ACCOUNT_ID: \${{ vars.CLOUDFLARE_ACCOUNT_ID }}
878
+ run: |
879
+ set -eu
880
+ : "\${CLOUDFLARE_API_TOKEN:?Set the CLOUDFLARE_API_TOKEN Actions secret before running this workflow.}"
881
+ : "\${CLOUDFLARE_ACCOUNT_ID:?Set the CLOUDFLARE_ACCOUNT_ID Actions variable before running this workflow.}"
882
+ ${execCommand(packageManager, "wrangler", "deploy")}`;
883
+ }
884
+
885
+ function renderDeployConfig({ template, packageManager, needsBunRuntime }) {
886
+ if (template === "vercel-edge") {
887
+ return {
888
+ header: vercelDeployHeader(),
889
+ jobName: "Deploy to Vercel",
890
+ jobPermissions: "",
891
+ setupSteps: providerDeploySetup({ packageManager, needsBunRuntime }),
892
+ steps: vercelDeploySteps(packageManager),
893
+ };
894
+ }
895
+ if (template === "cloudflare-worker") {
896
+ return {
897
+ header: cloudflareDeployHeader(),
898
+ jobName: "Deploy to Cloudflare Workers",
899
+ jobPermissions: "",
900
+ setupSteps: providerDeploySetup({ packageManager, needsBunRuntime }),
901
+ steps: cloudflareDeploySteps(packageManager),
902
+ };
903
+ }
904
+ return {
905
+ header: containerRegistryHeader(),
906
+ jobName: "Publish container image",
907
+ jobPermissions: " packages: write",
908
+ setupSteps: "",
909
+ steps: containerPublishSteps(),
910
+ };
911
+ }
912
+
743
913
  function workflowStep(name, command) {
744
914
  return ` - name: ${name}
745
915
  run: ${command}`;
@@ -763,13 +933,21 @@ async function addLockfileVerifyScript(dir) {
763
933
  await writePackageJson(dir, packageJson);
764
934
  }
765
935
 
766
- function renderCiReplacements({ packageManager, template, packageJson, codeOwner }) {
936
+ function renderCiReplacements({ packageManager, template, packageJson, codeOwner, includeSecurityBundle = true }) {
767
937
  const setupPm = setupPackageManagerStep(packageManager);
768
938
  const needsBunRuntime = template === "bun-basic" && packageManager !== "bun";
769
939
  const audit = auditCommand(packageManager);
770
940
  const auditFull = auditFullCommand(packageManager);
771
941
  const buildStep = hasPackageScript(packageJson, "build") ? workflowStep("Build", runScriptCommand(packageManager, "build")) : "";
772
942
  const auditStep = audit ? workflowStep(auditStepName(packageManager), audit) : "";
943
+ const deploy = renderDeployConfig({ template, packageManager, needsBunRuntime });
944
+ // The verify:lockfile script is only scaffolded when the security bundle
945
+ // ships. In a deploy-only scaffold the script does not exist on disk, so
946
+ // the deploy workflow must omit the step instead of failing fast on a
947
+ // missing file.
948
+ const deployVerifyLockfileStep = includeSecurityBundle
949
+ ? workflowStep("Verify lockfile sources", runScriptCommand(packageManager, "verify:lockfile"))
950
+ : "";
773
951
  // vuln-scan.yml: production-tree audit is blocking, full-tree audit is
774
952
  // advisory (continue-on-error) so a low-severity dev-tool advisory does
775
953
  // not page the on-call on a daily cron.
@@ -781,6 +959,13 @@ function renderCiReplacements({ packageManager, template, packageJson, codeOwner
781
959
  : "";
782
960
 
783
961
  return new Map([
962
+ ["__DEPLOY_HEADER__", deploy.header],
963
+ ["__DEPLOY_JOB_NAME__", deploy.jobName],
964
+ ["__DEPLOY_JOB_PERMISSIONS__", deploy.jobPermissions],
965
+ ["__DEPLOY_SETUP_STEPS__", deploy.setupSteps],
966
+ ["__DEPLOY_STEPS__", deploy.steps],
967
+ ["__DEPLOY_VERIFY_LOCKFILE_STEP__", deployVerifyLockfileStep],
968
+ ["__DEPLOY_REF_GUARD__", deployRefGuard()],
784
969
  ["__CODE_OWNER__", codeOwner],
785
970
  ["__SETUP_PACKAGE_MANAGER_STEP__", setupPm],
786
971
  ["__SETUP_BUN_RUNTIME_STEP__", needsBunRuntime ? setupBunStep() : ""],
@@ -817,7 +1002,35 @@ async function replacePlaceholdersInTree(dir, replacements) {
817
1002
  }
818
1003
  }
819
1004
 
820
- async function copyCiBundle(targetDir, template, packageManager, skipPackageManager, codeOwner) {
1005
+ async function pruneCiBundle(targetDir, flavor, { includeSecurityBundle, includeDeployWorkflow }) {
1006
+ if (!includeSecurityBundle) {
1007
+ const workflowFiles = flavor === "deno"
1008
+ ? ["ci.yml", "codeql.yml", "container-scan.yml", "dast.yml", "scorecard.yml", "zizmor.yml"]
1009
+ : ["ci.yml", "codeql.yml", "container-scan.yml", "dast.yml", "scorecard.yml", "vuln-scan.yml", "zizmor.yml"];
1010
+ for (const file of workflowFiles) {
1011
+ await rm(path.join(targetDir, ".github", "workflows", file), { force: true });
1012
+ }
1013
+ await rm(path.join(targetDir, ".github", "dependabot.yml"), { force: true });
1014
+ await rm(path.join(targetDir, ".github", "CODEOWNERS"), { force: true });
1015
+ await rm(path.join(targetDir, "SECURITY.md"), { force: true });
1016
+ if (flavor === "node") {
1017
+ await rm(path.join(targetDir, "scripts", "verify-lockfile-sources.mjs"), { force: true });
1018
+ }
1019
+ }
1020
+
1021
+ if (!includeDeployWorkflow) {
1022
+ await rm(path.join(targetDir, ".github", "workflows", "deploy.yml"), { force: true });
1023
+ }
1024
+ }
1025
+
1026
+ async function copyCiBundle(
1027
+ targetDir,
1028
+ template,
1029
+ packageManager,
1030
+ skipPackageManager,
1031
+ codeOwner,
1032
+ { includeSecurityBundle = true, includeDeployWorkflow = true } = {},
1033
+ ) {
821
1034
  const flavor = skipPackageManager ? "deno" : "node";
822
1035
  const sourceDir = path.join(CI_TEMPLATES_DIR, flavor);
823
1036
  if (!existsSync(sourceDir)) {
@@ -825,23 +1038,42 @@ async function copyCiBundle(targetDir, template, packageManager, skipPackageMana
825
1038
  }
826
1039
  await copyTemplate(sourceDir, targetDir);
827
1040
 
828
- const candidate = codeOwner?.trim() ?? "";
829
- if (candidate && !VALID_CODE_OWNER.test(candidate)) {
830
- throw new Error(
831
- `Invalid --code-owner "${candidate}". Use a GitHub handle (@user), a team (@org/team), or an email address.`,
832
- );
1041
+ await pruneCiBundle(targetDir, flavor, { includeSecurityBundle, includeDeployWorkflow });
1042
+
1043
+ if (!includeSecurityBundle && !includeDeployWorkflow) {
1044
+ return;
833
1045
  }
1046
+
1047
+ const candidate = codeOwner?.trim() ?? "";
834
1048
  const owner = candidate || "@your-org/security-team";
835
1049
  if (skipPackageManager) {
1050
+ if (includeSecurityBundle && candidate && !VALID_CODE_OWNER.test(candidate)) {
1051
+ throw new Error(
1052
+ `Invalid --code-owner "${candidate}". Use a GitHub handle (@user), a team (@org/team), or an email address.`,
1053
+ );
1054
+ }
836
1055
  await replacePlaceholdersInTree(targetDir, new Map([["__CODE_OWNER__", owner]]));
837
1056
  return;
838
1057
  }
839
1058
 
840
- await addLockfileVerifyScript(targetDir);
1059
+ if (includeSecurityBundle) {
1060
+ if (candidate && !VALID_CODE_OWNER.test(candidate)) {
1061
+ throw new Error(
1062
+ `Invalid --code-owner "${candidate}". Use a GitHub handle (@user), a team (@org/team), or an email address.`,
1063
+ );
1064
+ }
1065
+ await addLockfileVerifyScript(targetDir);
1066
+ }
841
1067
  const packageJson = await readPackageJsonIfPresent(targetDir);
842
1068
  await replacePlaceholdersInTree(
843
1069
  targetDir,
844
- renderCiReplacements({ packageManager, template, packageJson, codeOwner: owner }),
1070
+ renderCiReplacements({
1071
+ packageManager,
1072
+ template,
1073
+ packageJson,
1074
+ codeOwner: owner,
1075
+ includeSecurityBundle,
1076
+ }),
845
1077
  );
846
1078
  }
847
1079
 
@@ -1213,7 +1445,7 @@ function createSpinner(initialMessage) {
1213
1445
  };
1214
1446
  }
1215
1447
 
1216
- function printSummary({ projectName, template, packageManager, installDeps, skipPackageManager, withCi }) {
1448
+ function printSummary({ projectName, template, packageManager, installDeps, skipPackageManager, withCi, withDeploy }) {
1217
1449
  const templateMeta = TEMPLATE_OPTIONS.find((option) => option.value === template);
1218
1450
  const templateLabel = templateMeta ? `${templateMeta.title} ${color(COLORS.dim, `(${template})`)}` : template;
1219
1451
  const summaryLines = [
@@ -1230,6 +1462,9 @@ function printSummary({ projectName, template, packageManager, installDeps, skip
1230
1462
  if (withCi) {
1231
1463
  summaryLines.push(`${color(COLORS.gray, "Security ")} ${color(COLORS.cyan, "GitHub CI bundle")}`);
1232
1464
  }
1465
+ if (withDeploy) {
1466
+ summaryLines.push(`${color(COLORS.gray, "Deploy ")} ${color(COLORS.cyan, "Starter workflow")}`);
1467
+ }
1233
1468
  console.log("");
1234
1469
  console.log(renderBox(summaryLines, { accent: COLORS.green }));
1235
1470
  console.log("");
@@ -1413,6 +1648,15 @@ async function main() {
1413
1648
  withCi = rl ? await askYesNo(rl, "Add hardened GitHub Actions and security files?", true) : true;
1414
1649
  }
1415
1650
 
1651
+ let withDeploy = opts.deploy;
1652
+ if (withDeploy === undefined) {
1653
+ if (withCi) {
1654
+ withDeploy = true;
1655
+ } else {
1656
+ withDeploy = rl ? await askYesNo(rl, "Add a starter deployment workflow?", false) : false;
1657
+ }
1658
+ }
1659
+
1416
1660
  rl?.close();
1417
1661
 
1418
1662
  if (interactive) {
@@ -1443,9 +1687,18 @@ async function main() {
1443
1687
  }
1444
1688
  }
1445
1689
 
1446
- if (withCi) {
1447
- await copyCiBundle(targetDir, template, packageManager, skipPackageManager, opts.codeOwner);
1448
- logStep("GitHub security bundle added", skipPackageManager ? "deno" : packageManager);
1690
+ if (withCi || withDeploy) {
1691
+ await copyCiBundle(targetDir, template, packageManager, skipPackageManager, opts.codeOwner, {
1692
+ includeSecurityBundle: withCi,
1693
+ includeDeployWorkflow: withDeploy,
1694
+ });
1695
+ if (withCi && withDeploy) {
1696
+ logStep("GitHub automation added", `${skipPackageManager ? "deno" : packageManager} + deploy`);
1697
+ } else if (withCi) {
1698
+ logStep("GitHub security bundle added", skipPackageManager ? "deno" : packageManager);
1699
+ } else if (withDeploy) {
1700
+ logStep("Deploy starter added", template);
1701
+ }
1449
1702
  }
1450
1703
 
1451
1704
  if (initGit) {
@@ -1474,7 +1727,7 @@ async function main() {
1474
1727
  }
1475
1728
  }
1476
1729
 
1477
- printSummary({ projectName, template, packageManager, installDeps, skipPackageManager, withCi });
1730
+ printSummary({ projectName, template, packageManager, installDeps, skipPackageManager, withCi, withDeploy });
1478
1731
  } catch (err) {
1479
1732
  rl?.close();
1480
1733
  if (err && err.message === "Cancelled") {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-daloy",
3
- "version": "0.34.2",
3
+ "version": "0.34.3",
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:6244f261-e99d-5b5f-9ccb-5a10bc0de8b7",
4
+ "serialNumber": "urn:uuid:9fb86079-cf96-5a72-94a8-dd7d3cdc62cc",
5
5
  "version": 1,
6
6
  "metadata": {
7
- "timestamp": "2026-05-23T09:06:22.609Z",
7
+ "timestamp": "2026-05-23T13:34:45.760Z",
8
8
  "tools": [
9
9
  {
10
10
  "vendor": "DaloyJS",
11
11
  "name": "daloy-generate-sbom",
12
- "version": "0.34.2"
12
+ "version": "0.34.3"
13
13
  }
14
14
  ],
15
15
  "authors": [],
16
16
  "component": {
17
17
  "type": "library",
18
- "bom-ref": "pkg:npm/create-daloy@0.34.2",
18
+ "bom-ref": "pkg:npm/create-daloy@0.34.3",
19
19
  "name": "create-daloy",
20
- "version": "0.34.2",
20
+ "version": "0.34.3",
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@0.34.2",
22
+ "purl": "pkg:npm/create-daloy@0.34.3",
23
23
  "licenses": [
24
24
  {
25
25
  "license": {
@@ -38,9 +38,9 @@
38
38
  }
39
39
  ],
40
40
  "swid": {
41
- "tagId": "swidtag-create-daloy-0.34.2",
41
+ "tagId": "swidtag-create-daloy-0.34.3",
42
42
  "name": "create-daloy",
43
- "version": "0.34.2",
43
+ "version": "0.34.3",
44
44
  "tagVersion": 0,
45
45
  "patch": false
46
46
  }
@@ -49,7 +49,7 @@
49
49
  "components": [],
50
50
  "dependencies": [
51
51
  {
52
- "ref": "pkg:npm/create-daloy@0.34.2",
52
+ "ref": "pkg:npm/create-daloy@0.34.3",
53
53
  "dependsOn": []
54
54
  }
55
55
  ]
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-0.34.2",
6
- "documentNamespace": "https://github.com/daloyjs/daloy/sbom/create-daloy-0.34.2-6244f261-e99d-5b5f-9ccb-5a10bc0de8b7",
5
+ "name": "create-daloy-0.34.3",
6
+ "documentNamespace": "https://github.com/daloyjs/daloy/sbom/create-daloy-0.34.3-9fb86079-cf96-5a72-94a8-dd7d3cdc62cc",
7
7
  "creationInfo": {
8
- "created": "2026-05-23T09:06:22.609Z",
8
+ "created": "2026-05-23T13:34:45.760Z",
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": "0.34.2",
19
+ "versionInfo": "0.34.3",
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@0.34.2"
30
+ "referenceLocator": "pkg:npm/create-daloy@0.34.3"
31
31
  }
32
32
  ]
33
33
  }
@@ -0,0 +1,118 @@
1
+ # Starter deployment workflow generated by create-daloy --with-ci.
2
+ #
3
+ # This workflow is intentionally manual-only so a fresh scaffold does not fail
4
+ # on every push before you wire in environment protection. Once you are ready,
5
+ # add your preferred push/tag trigger.
6
+ #
7
+ # Default target: GitHub Container Registry (GHCR).
8
+ # No extra secret is required; the workflow uses the repo-scoped GITHUB_TOKEN.
9
+ # After the image is published, point your platform at GHCR or replace the
10
+ # final push steps with your provider's deploy command.
11
+ #
12
+ # Optional container-host handoff examples:
13
+ # - Fly.io: install flyctl in a later step and run
14
+ # flyctl deploy --image "$IMAGE_REPO:sha-${GITHUB_SHA}" --remote-only
15
+ # - Render: create an image-backed service that tracks
16
+ # ghcr.io/<owner>/<repo>:sha-${GITHUB_SHA}
17
+
18
+ name: Deploy
19
+
20
+ on:
21
+ workflow_dispatch:
22
+
23
+ permissions: {}
24
+
25
+ concurrency:
26
+ group: deploy-${{ github.workflow }}-${{ github.ref }}
27
+ cancel-in-progress: false
28
+
29
+ jobs:
30
+ verify:
31
+ name: Verify before deploy
32
+ runs-on: ubuntu-latest
33
+ timeout-minutes: 20
34
+ permissions:
35
+ contents: read
36
+
37
+ steps:
38
+ - name: Harden runner
39
+ uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2
40
+ with:
41
+ egress-policy: audit
42
+ disable-sudo: true
43
+
44
+ - name: Checkout
45
+ uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
46
+ with:
47
+ persist-credentials: false
48
+ show-progress: false
49
+
50
+ - name: Set up Deno
51
+ uses: denoland/setup-deno@667a34cdef165d8d2b2e98dde39547c9daac7282 # v2.0.4
52
+ with:
53
+ deno-version: v2.x
54
+
55
+ - name: Typecheck
56
+ run: deno task typecheck
57
+
58
+ - name: Test
59
+ run: deno task test
60
+
61
+ deploy:
62
+ name: Publish container image
63
+ needs: verify
64
+ # Lock production deploys to main or a tag push. `workflow_dispatch`
65
+ # accepts any branch, so this guard stops an accidental dispatch from a
66
+ # feature branch from shipping unreviewed code.
67
+ if: github.ref == 'refs/heads/main' || github.ref_type == 'tag'
68
+ runs-on: ubuntu-latest
69
+ timeout-minutes: 20
70
+ environment:
71
+ name: production
72
+ permissions:
73
+ contents: read
74
+ packages: write
75
+
76
+ steps:
77
+ - name: Harden runner
78
+ uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2
79
+ with:
80
+ egress-policy: audit
81
+ disable-sudo: true
82
+
83
+ - name: Checkout
84
+ uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
85
+ with:
86
+ persist-credentials: false
87
+ show-progress: false
88
+
89
+ - name: Log in to GHCR
90
+ env:
91
+ GH_TOKEN: ${{ github.token }}
92
+ run: |
93
+ set -eu
94
+ echo "$GH_TOKEN" | docker login ghcr.io -u "${{ github.actor }}" --password-stdin
95
+
96
+ - name: Derive image name
97
+ run: |
98
+ set -eu
99
+ owner="${GITHUB_REPOSITORY_OWNER,,}"
100
+ repo="${GITHUB_REPOSITORY#*/}"
101
+ repo="${repo,,}"
102
+ echo "IMAGE_REPO=ghcr.io/${owner}/${repo}" >> "$GITHUB_ENV"
103
+
104
+ - name: Build image
105
+ env:
106
+ DOCKER_BUILDKIT: "1"
107
+ run: |
108
+ set -eu
109
+ docker build \
110
+ --tag "$IMAGE_REPO:sha-${GITHUB_SHA}" \
111
+ --tag "$IMAGE_REPO:latest" \
112
+ .
113
+
114
+ - name: Push image
115
+ run: |
116
+ set -eu
117
+ docker push "$IMAGE_REPO:sha-${GITHUB_SHA}"
118
+ docker push "$IMAGE_REPO:latest"
@@ -24,6 +24,7 @@ The `--with-ci` bundle adds these defaults:
24
24
  - Lockfile source verification rejects git dependencies and non-registry tarball URLs.
25
25
  - CodeQL, OpenSSF Scorecard, zizmor, Dependabot, and CODEOWNERS are generated.
26
26
  - A daily scheduled `vuln-scan.yml` runs the package manager's audit against the committed lockfile so newly-disclosed CVEs are surfaced even when no PR or push has run CI (SOC 2 CC7.1 continuous-vulnerability-management evidence).
27
+ - A weekly scheduled `dast.yml` boots the built application and runs an OWASP ZAP baseline scan against it. CodeQL + the `verify:*` family are the SAST half of the posture; this is the DAST half — see Aikido's [SAST vs DAST](https://www.aikido.dev/blog/sast-vs-dast-what-you-need-to-now) write-up for why both matter. Findings are summarized in the workflow log and the job fails on HIGH-risk alerts; MEDIUM / LOW / INFO are surfaced for triage but non-blocking.
27
28
  - No npm publish workflow is generated: this scaffold is a REST API service, not a published package. If you later carve out a reusable library you can opt into npm trusted publishing yourself.
28
29
 
29
30
  ## Container hardening
@@ -7,6 +7,7 @@
7
7
  /.github/ __CODE_OWNER__
8
8
  /.github/workflows/ __CODE_OWNER__
9
9
  /.github/workflows/vuln-scan.yml __CODE_OWNER__
10
+ /.github/workflows/dast.yml __CODE_OWNER__
10
11
  /.github/dependabot.yml __CODE_OWNER__
11
12
  /.github/CODEOWNERS __CODE_OWNER__
12
13
 
@@ -0,0 +1,177 @@
1
+ # Dynamic Application Security Testing (DAST) generated by
2
+ # create-daloy --with-ci.
3
+ #
4
+ # Why this exists:
5
+ # The shipped `ci.yml` + `codeql.yml` + `scorecard.yml` + `zizmor.yml`
6
+ # stack is the SAST half of your security posture — static analysis
7
+ # of source, dependencies, and workflows. Aikido's "SAST vs DAST:
8
+ # what you need to know"
9
+ # (https://www.aikido.dev/blog/sast-vs-dast-what-you-need-to-now)
10
+ # argues that SAST and DAST catch different bug classes and that you
11
+ # want both. This workflow adds the dynamic half: it boots your built
12
+ # application, points OWASP ZAP's baseline scanner at it, and fails
13
+ # the run on any HIGH-risk finding.
14
+ #
15
+ # Because Daloy is JSON-first and the framework already enforces
16
+ # header sanitization, body limits, router path-traversal rejection,
17
+ # and `secureHeaders()` defaults, the baseline scan usually goes
18
+ # green out of the box on a fresh scaffold. The value is the
19
+ # *continuous* probe — newly-disclosed ZAP rules apply to your
20
+ # unchanged code, and runtime regressions (a misconfigured CORS
21
+ # policy, a header you set with CRLF in it, a redirect to an
22
+ # open-redirect target) get caught before they hit production.
23
+ #
24
+ # Trigger surface:
25
+ # * Weekly cron — continuous coverage against newly-disclosed rules.
26
+ # * `workflow_dispatch` — on-demand probe for security review.
27
+ # Deliberately NOT on every PR: a baseline scan takes several
28
+ # minutes and the same surface is exercised by your test suite on
29
+ # every PR via `ci.yml`.
30
+ #
31
+ # Customizing:
32
+ # * The "Start application" step expects `pnpm build` to produce
33
+ # `dist/index.js` and a `PORT` env var to control the listen port,
34
+ # matching the shipped `node-basic` template. If your app starts
35
+ # differently, edit the `nohup` line below.
36
+ # * If your app needs seed data, secrets, or a database, add them
37
+ # before the start step (use `vars` / `secrets` in the GitHub
38
+ # Environment).
39
+ # * To probe a deployed staging URL instead of a local build, drop
40
+ # the "Set up …", "Install", "Build", "Start application", and
41
+ # "Wait for target" steps and point the ZAP `docker run` line at
42
+ # `https://staging.example.com`.
43
+
44
+ name: DAST
45
+
46
+ on:
47
+ schedule:
48
+ # 07:42 UTC weekly (Wed) — offset from the other scheduled scans
49
+ # so they do not all queue at once.
50
+ - cron: "42 7 * * 3"
51
+ workflow_dispatch:
52
+
53
+ permissions: {}
54
+
55
+ concurrency:
56
+ group: dast-${{ github.workflow }}-${{ github.ref }}
57
+ cancel-in-progress: true
58
+
59
+ jobs:
60
+ zap-baseline:
61
+ name: ZAP baseline scan
62
+ runs-on: ubuntu-latest
63
+ timeout-minutes: 25
64
+ permissions:
65
+ contents: read
66
+
67
+ steps:
68
+ - name: Harden runner
69
+ uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2
70
+ with:
71
+ egress-policy: audit
72
+ disable-sudo: true
73
+ disable-file-monitoring: false
74
+
75
+ - name: Checkout
76
+ uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
77
+ with:
78
+ persist-credentials: false
79
+ show-progress: false
80
+
81
+ - name: Set up pnpm
82
+ uses: pnpm/action-setup@ac6db6d3c1f721f886538a378a2d73e85697340a # v6
83
+ with:
84
+ version: 11.1.3
85
+ run_install: false
86
+
87
+ - name: Set up Node.js
88
+ uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
89
+ with:
90
+ node-version: 24
91
+
92
+ - name: Install dependencies (no scripts)
93
+ run: pnpm install --frozen-lockfile --ignore-scripts
94
+ env:
95
+ npm_config_ignore_scripts: "true"
96
+
97
+ - name: Build
98
+ run: pnpm build
99
+
100
+ - name: Start application (background)
101
+ run: |
102
+ mkdir -p dast-results
103
+ chmod 777 dast-results
104
+ nohup node dist/index.js > dast-results/server.log 2>&1 &
105
+ echo $! > dast-results/server.pid
106
+ env:
107
+ PORT: "3000"
108
+ NODE_ENV: production
109
+
110
+ - name: Wait for target to be reachable
111
+ run: |
112
+ for i in $(seq 1 30); do
113
+ if curl -fsS --max-time 2 http://localhost:3000/healthz > /dev/null \
114
+ || curl -fsS --max-time 2 http://localhost:3000/ > /dev/null; then
115
+ echo "Target ready after ${i} attempt(s)."
116
+ exit 0
117
+ fi
118
+ sleep 1
119
+ done
120
+ echo "::error::DAST target did not become reachable in 30s."
121
+ cat dast-results/server.log || true
122
+ exit 1
123
+
124
+ - name: Run OWASP ZAP baseline scan
125
+ run: |
126
+ docker pull ghcr.io/zaproxy/zaproxy:stable
127
+ docker run --rm --network host \
128
+ -v "$PWD/dast-results:/zap/wrk/:rw" \
129
+ -t ghcr.io/zaproxy/zaproxy:stable \
130
+ zap-baseline.py \
131
+ -t http://localhost:3000 \
132
+ -J zap-report.json \
133
+ -w zap-report.md \
134
+ -r zap-report.html \
135
+ -I \
136
+ || true
137
+
138
+ - name: Stop application
139
+ if: always()
140
+ run: |
141
+ if [ -f dast-results/server.pid ]; then
142
+ kill "$(cat dast-results/server.pid)" || true
143
+ fi
144
+
145
+ - name: Summarize ZAP findings and fail on HIGH
146
+ if: always()
147
+ run: |
148
+ report=dast-results/zap-report.json
149
+ if [ ! -f "$report" ]; then
150
+ echo "::warning::No ZAP JSON report produced (scan may have failed to start)."
151
+ exit 0
152
+ fi
153
+ counts=$(node -e "
154
+ const r = JSON.parse(require('node:fs').readFileSync(process.argv[1], 'utf8'));
155
+ const c = { high: 0, medium: 0, low: 0, info: 0 };
156
+ for (const site of r.site ?? []) {
157
+ for (const alert of site.alerts ?? []) {
158
+ const code = String(alert.riskcode);
159
+ if (code === '3') c.high++;
160
+ else if (code === '2') c.medium++;
161
+ else if (code === '1') c.low++;
162
+ else c.info++;
163
+ }
164
+ }
165
+ process.stdout.write(JSON.stringify(c));
166
+ " "$report")
167
+ echo "ZAP findings: ${counts}"
168
+ high=$(node -e "process.stdout.write(String(JSON.parse(process.argv[1]).high))" "$counts")
169
+ if [ -f dast-results/zap-report.md ]; then
170
+ echo "----- ZAP markdown report -----"
171
+ cat dast-results/zap-report.md
172
+ echo "----- end report -----"
173
+ fi
174
+ if [ "${high}" -gt 0 ]; then
175
+ echo "::error::ZAP reported ${high} HIGH-risk finding(s) against your application surface."
176
+ exit 1
177
+ fi
@@ -0,0 +1,94 @@
1
+ # Starter deployment workflow generated by create-daloy --with-ci.
2
+ #
3
+ # This workflow is intentionally manual-only so a fresh scaffold does not fail
4
+ # on every push before you wire in secrets, variables, and environment
5
+ # approvals. Once you are ready, add your preferred push/tag trigger.
6
+ #
7
+ __DEPLOY_HEADER__
8
+
9
+ name: Deploy
10
+
11
+ on:
12
+ workflow_dispatch:
13
+
14
+ permissions: {}
15
+
16
+ concurrency:
17
+ group: deploy-${{ github.workflow }}-${{ github.ref }}
18
+ cancel-in-progress: false
19
+
20
+ jobs:
21
+ verify:
22
+ name: Verify before deploy
23
+ runs-on: ubuntu-latest
24
+ timeout-minutes: 20
25
+ permissions:
26
+ contents: read
27
+
28
+ steps:
29
+ - name: Harden runner
30
+ uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2
31
+ with:
32
+ egress-policy: audit
33
+ disable-sudo: true
34
+
35
+ - name: Checkout
36
+ uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
37
+ with:
38
+ persist-credentials: false
39
+ show-progress: false
40
+
41
+ __SETUP_PACKAGE_MANAGER_STEP__
42
+
43
+ - name: Set up Node.js
44
+ uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
45
+ with:
46
+ node-version: 24
47
+ # Package-manager caching is intentionally disabled. Shared caches
48
+ # can bridge fork PRs into trusted branches when mis-keyed.
49
+
50
+ __SETUP_BUN_RUNTIME_STEP__
51
+
52
+ - name: Install dependencies
53
+ run: __INSTALL_COMMAND__
54
+ env:
55
+ npm_config_ignore_scripts: "true"
56
+
57
+ __DEPLOY_VERIFY_LOCKFILE_STEP__
58
+
59
+ - name: Typecheck
60
+ run: __TYPECHECK_COMMAND__
61
+
62
+ - name: Test
63
+ run: __TEST_COMMAND__
64
+
65
+ __BUILD_STEP__
66
+
67
+ deploy:
68
+ name: __DEPLOY_JOB_NAME__
69
+ needs: verify
70
+ __DEPLOY_REF_GUARD__
71
+ runs-on: ubuntu-latest
72
+ timeout-minutes: 20
73
+ environment:
74
+ name: production
75
+ permissions:
76
+ contents: read
77
+ __DEPLOY_JOB_PERMISSIONS__
78
+
79
+ steps:
80
+ - name: Harden runner
81
+ uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2
82
+ with:
83
+ egress-policy: audit
84
+ disable-sudo: true
85
+
86
+ - name: Checkout
87
+ uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
88
+ with:
89
+ persist-credentials: false
90
+ show-progress: false
91
+
92
+ __DEPLOY_SETUP_STEPS__
93
+
94
+ __DEPLOY_STEPS__
@@ -16,7 +16,7 @@
16
16
  "gen": "pnpm gen:openapi && pnpm gen:client"
17
17
  },
18
18
  "dependencies": {
19
- "@daloyjs/core": "^0.34.2",
19
+ "@daloyjs/core": "^0.34.3",
20
20
  "zod": "^4.4.3"
21
21
  },
22
22
  "devDependencies": {
@@ -10,7 +10,7 @@
10
10
  "test": "node --import tsx/esm --test tests/**/*.test.ts"
11
11
  },
12
12
  "dependencies": {
13
- "@daloyjs/core": "^0.34.2",
13
+ "@daloyjs/core": "^0.34.3",
14
14
  "zod": "^4.4.3"
15
15
  },
16
16
  "devDependencies": {
@@ -8,8 +8,8 @@
8
8
  "gen:openapi": "deno run --allow-net --allow-env --allow-read --allow-write scripts/dump-openapi.ts"
9
9
  },
10
10
  "imports": {
11
- "@daloyjs/core": "npm:@daloyjs/core@^0.34.2",
12
- "@daloyjs/core/": "npm:@daloyjs/core@^0.34.2/",
11
+ "@daloyjs/core": "npm:@daloyjs/core@^0.34.3",
12
+ "@daloyjs/core/": "npm:@daloyjs/core@^0.34.3/",
13
13
  "zod": "npm:zod@^4.4.3"
14
14
  },
15
15
  "compilerOptions": {
@@ -18,7 +18,7 @@
18
18
  "audit": "pnpm audit --prod"
19
19
  },
20
20
  "dependencies": {
21
- "@daloyjs/core": "^0.34.2",
21
+ "@daloyjs/core": "^0.34.3",
22
22
  "zod": "^4.4.3"
23
23
  },
24
24
  "devDependencies": {
@@ -10,7 +10,7 @@
10
10
  "test": "node --import tsx/esm --test tests/**/*.test.ts"
11
11
  },
12
12
  "dependencies": {
13
- "@daloyjs/core": "^0.34.2",
13
+ "@daloyjs/core": "^0.34.3",
14
14
  "zod": "^4.4.3"
15
15
  },
16
16
  "devDependencies": {