create-daloy 0.26.0 → 0.34.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 (36) hide show
  1. package/README.md +18 -5
  2. package/bin/create-daloy.mjs +169 -37
  3. package/package.json +4 -2
  4. package/sbom.cdx.json +56 -0
  5. package/sbom.spdx.json +42 -0
  6. package/templates/_ci/deno/SECURITY.md +5 -1
  7. package/templates/_ci/deno/_github/dependabot.yml +12 -1
  8. package/templates/_ci/deno/_github/workflows/container-scan.yml +158 -0
  9. package/templates/_ci/node/SECURITY.md +33 -3
  10. package/templates/_ci/node/_github/CODEOWNERS +1 -1
  11. package/templates/_ci/node/_github/dependabot.yml +12 -1
  12. package/templates/_ci/node/_github/workflows/container-scan.yml +177 -0
  13. package/templates/_ci/node/_github/workflows/vuln-scan.yml +89 -0
  14. package/templates/bun-basic/AGENTS.md +10 -0
  15. package/templates/bun-basic/README.md +10 -0
  16. package/templates/bun-basic/_Dockerfile +57 -0
  17. package/templates/bun-basic/_dockerignore +12 -0
  18. package/templates/bun-basic/package.json +1 -1
  19. package/templates/cloudflare-worker/_Dockerfile +56 -0
  20. package/templates/cloudflare-worker/_dockerignore +14 -0
  21. package/templates/cloudflare-worker/package.json +1 -1
  22. package/templates/cloudflare-worker/src/index.ts +17 -0
  23. package/templates/deno-basic/_Dockerfile +66 -0
  24. package/templates/deno-basic/_dockerignore +11 -0
  25. package/templates/deno-basic/deno.json +2 -2
  26. package/templates/node-basic/AGENTS.md +10 -0
  27. package/templates/node-basic/README.md +10 -0
  28. package/templates/node-basic/_Dockerfile +23 -10
  29. package/templates/node-basic/_dockerignore +1 -0
  30. package/templates/node-basic/package.json +1 -1
  31. package/templates/vercel-edge/AGENTS.md +10 -0
  32. package/templates/vercel-edge/README.md +10 -0
  33. package/templates/vercel-edge/_Dockerfile +55 -0
  34. package/templates/vercel-edge/_dockerignore +14 -0
  35. package/templates/vercel-edge/package.json +1 -1
  36. package/templates/_ci/node/_github/workflows/release.yml +0 -125
@@ -6,7 +6,7 @@
6
6
  # Workflow / CI / CD.
7
7
  /.github/ __CODE_OWNER__
8
8
  /.github/workflows/ __CODE_OWNER__
9
- /.github/workflows/release.yml __CODE_OWNER__
9
+ /.github/workflows/vuln-scan.yml __CODE_OWNER__
10
10
  /.github/dependabot.yml __CODE_OWNER__
11
11
  /.github/CODEOWNERS __CODE_OWNER__
12
12
 
@@ -25,4 +25,15 @@ updates:
25
25
  dev-dependencies:
26
26
  dependency-type: development
27
27
  commit-message:
28
- prefix: "chore(deps)"
28
+ prefix: "chore(deps)"
29
+
30
+ # Keep the runtime base image (Dockerfile `FROM`) patched. Combined with
31
+ # the `container-scan.yml` workflow, this catches base-image CVEs early.
32
+ - package-ecosystem: docker
33
+ directory: "/"
34
+ schedule:
35
+ interval: weekly
36
+ day: monday
37
+ open-pull-requests-limit: 5
38
+ commit-message:
39
+ prefix: "chore(docker)"
@@ -0,0 +1,177 @@
1
+ # Container security scan generated by create-daloy --with-ci.
2
+ #
3
+ # Inspired by the controls in
4
+ # https://snyk.io/blog/scale-container-security-effortlessly/ — but built
5
+ # only from SHA-pinned, free, open-source GitHub Actions:
6
+ #
7
+ # - hadolint lints the Dockerfile for known anti-patterns and unsafe
8
+ # instructions (CIS Docker Benchmark coverage).
9
+ # - Trivy scans the source tree (config + secrets + vulnerable lockfile
10
+ # entries), then scans the built image for OS + language CVEs.
11
+ # - SARIF results are uploaded to the Code Scanning tab so findings show
12
+ # up in the same place as CodeQL.
13
+ #
14
+ # The job is a no-op if the project has no Dockerfile at the repo root, so
15
+ # it is safe to keep enabled for templates that disable the container.
16
+
17
+ name: Container scan
18
+
19
+ on:
20
+ pull_request:
21
+ paths:
22
+ - "Dockerfile"
23
+ - ".dockerignore"
24
+ - "package.json"
25
+ - "pnpm-lock.yaml"
26
+ - "package-lock.json"
27
+ - "yarn.lock"
28
+ - "bun.lock"
29
+ - ".github/workflows/container-scan.yml"
30
+ push:
31
+ branches: [main]
32
+ paths:
33
+ - "Dockerfile"
34
+ - ".dockerignore"
35
+ - "package.json"
36
+ - "pnpm-lock.yaml"
37
+ - "package-lock.json"
38
+ - "yarn.lock"
39
+ - "bun.lock"
40
+ - ".github/workflows/container-scan.yml"
41
+ schedule:
42
+ # Weekly run catches newly-disclosed base-image CVEs even when the
43
+ # Dockerfile itself hasn't changed.
44
+ - cron: "17 6 * * 1"
45
+
46
+ permissions: {}
47
+
48
+ concurrency:
49
+ group: container-scan-${{ github.workflow }}-${{ github.ref }}
50
+ cancel-in-progress: true
51
+
52
+ jobs:
53
+ scan:
54
+ name: Lint + scan container
55
+ runs-on: ubuntu-latest
56
+ timeout-minutes: 20
57
+ permissions:
58
+ contents: read
59
+ security-events: write
60
+
61
+ steps:
62
+ - name: Harden runner
63
+ uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2
64
+ with:
65
+ egress-policy: audit
66
+ disable-sudo: true
67
+
68
+ - name: Checkout
69
+ uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
70
+ with:
71
+ persist-credentials: false
72
+ show-progress: false
73
+
74
+ - name: Detect Dockerfile
75
+ id: detect
76
+ run: |
77
+ if [ -f Dockerfile ]; then
78
+ echo "present=true" >> "$GITHUB_OUTPUT"
79
+ else
80
+ echo "present=false" >> "$GITHUB_OUTPUT"
81
+ echo "::notice::No Dockerfile found at the repo root; skipping container scan."
82
+ fi
83
+
84
+ - name: Pin check (FROM @sha256 digest)
85
+ # Aikido x Root.io ("Harden your containers without the headaches",
86
+ # 2026) makes the point that a floating tag like `node:24-alpine`
87
+ # silently re-resolves to a new digest on every build, so a
88
+ # base-image CVE that lands tomorrow is invisible to today's
89
+ # scan results. Pinning `FROM <image>@sha256:<digest>` makes the
90
+ # base reproducible; the `docker` Dependabot ecosystem in
91
+ # `.github/dependabot.yml` then opens a PR when a fresher digest
92
+ # is published so the pin stays current instead of stale. This
93
+ # step surfaces unpinned `FROM` lines as PR annotations without
94
+ # failing the build, so a brand-new scaffold still goes green.
95
+ if: steps.detect.outputs.present == 'true'
96
+ run: |
97
+ unpinned=0
98
+ while IFS= read -r line; do
99
+ ref="${line#FROM }"
100
+ ref="${ref#from }"
101
+ ref="${ref%% AS *}"
102
+ ref="${ref%% as *}"
103
+ ref="${ref## }"
104
+ ref="${ref%% }"
105
+ case "$ref" in
106
+ scratch|"\${"*|*"@sha256:"*) ;;
107
+ *)
108
+ echo "::warning file=Dockerfile::FROM '$ref' is not pinned to a @sha256:<digest> — see Aikido x Root.io 'Harden your containers' (Daloy SECURITY.md → Container base-image hardening)."
109
+ unpinned=$((unpinned + 1))
110
+ ;;
111
+ esac
112
+ done < <(grep -E '^(FROM|from) ' Dockerfile || true)
113
+ if [ "$unpinned" -gt 0 ]; then
114
+ echo "::notice::$unpinned unpinned FROM line(s). Override at build time with --build-arg NODE_IMAGE=node:24-alpine@sha256:<digest> or hard-code the digest in the Dockerfile and let Dependabot keep it fresh."
115
+ fi
116
+
117
+ - name: Lint Dockerfile (hadolint)
118
+ if: steps.detect.outputs.present == 'true'
119
+ uses: hadolint/hadolint-action@2332a7b74a6de0dda2e2221d575162eba76ba5e5 # v3.3.0
120
+ with:
121
+ dockerfile: Dockerfile
122
+ format: sarif
123
+ output-file: hadolint.sarif
124
+ no-fail: true
125
+
126
+ - name: Upload hadolint SARIF
127
+ if: steps.detect.outputs.present == 'true'
128
+ uses: github/codeql-action/upload-sarif@52485aec7be33610227643b0fe83936b8b5f061a # v3
129
+ with:
130
+ sarif_file: hadolint.sarif
131
+ category: hadolint
132
+
133
+ - name: Trivy filesystem scan (config + secrets + vulns)
134
+ if: steps.detect.outputs.present == 'true'
135
+ uses: aquasecurity/trivy-action@ed142fd0673e97e23eac54620cfb913e5ce36c25 # v0.36.0
136
+ with:
137
+ scan-type: fs
138
+ scan-ref: .
139
+ severity: HIGH,CRITICAL
140
+ ignore-unfixed: true
141
+ format: sarif
142
+ output: trivy-fs.sarif
143
+ exit-code: "0"
144
+
145
+ - name: Upload Trivy filesystem SARIF
146
+ if: steps.detect.outputs.present == 'true'
147
+ uses: github/codeql-action/upload-sarif@52485aec7be33610227643b0fe83936b8b5f061a # v3
148
+ with:
149
+ sarif_file: trivy-fs.sarif
150
+ category: trivy-fs
151
+
152
+ - name: Build image (no push)
153
+ if: steps.detect.outputs.present == 'true'
154
+ run: |
155
+ # Tag locally so the scan step has something to point at.
156
+ docker build --pull --load -t local/app:scan .
157
+
158
+ - name: Trivy image scan
159
+ if: steps.detect.outputs.present == 'true'
160
+ uses: aquasecurity/trivy-action@ed142fd0673e97e23eac54620cfb913e5ce36c25 # v0.36.0
161
+ with:
162
+ image-ref: local/app:scan
163
+ severity: HIGH,CRITICAL
164
+ ignore-unfixed: true
165
+ format: sarif
166
+ output: trivy-image.sarif
167
+ # Block merges only on CRITICAL by default; relax with `severity`
168
+ # / `exit-code` if it gets noisy in your pipeline.
169
+ exit-code: "1"
170
+ vuln-type: os,library
171
+
172
+ - name: Upload Trivy image SARIF
173
+ if: always() && steps.detect.outputs.present == 'true'
174
+ uses: github/codeql-action/upload-sarif@52485aec7be33610227643b0fe83936b8b5f061a # v3
175
+ with:
176
+ sarif_file: trivy-image.sarif
177
+ category: trivy-image
@@ -0,0 +1,89 @@
1
+ # Daily vulnerability scan generated by create-daloy --with-ci.
2
+ #
3
+ # Why this exists for a REST API service:
4
+ # `ci.yml` already runs an audit on every PR and push to main, which
5
+ # catches CVEs at change time. It does NOT catch CVEs disclosed AFTER
6
+ # the last change. A long-running service may sit on `main` for weeks
7
+ # between deploys; a worm published to a transitive dep yesterday is
8
+ # invisible to that pipeline until the next PR lands.
9
+ #
10
+ # SOC 2 CC7.1 ("the entity uses detection and monitoring procedures to
11
+ # identify ... vulnerabilities") and the Aikido write-up "A Guide to
12
+ # Automating Technical Vulnerability Management for SOC 2" both require
13
+ # a *continuous* cadence: an auditable, scheduled scan that produces
14
+ # evidence on a fixed interval regardless of developer activity.
15
+ #
16
+ # This workflow runs the package manager's audit against the committed
17
+ # lockfile every day at 06:13 UTC. When the package manager exposes
18
+ # separate production and full-tree scopes, the production-tree scan is
19
+ # blocking (it covers what actually ships to your users) and the
20
+ # full-tree scan is advisory so a low-severity dev-tool advisory does
21
+ # not page the on-call.
22
+ #
23
+ # Hardening:
24
+ # * `permissions: {}` at the top level; the single job opts in to
25
+ # `contents: read` only.
26
+ # * Third-party actions are SHA-pinned and managed by Dependabot.
27
+ # * `step-security/harden-runner` runs in audit mode (no need to block
28
+ # egress on a read-only scan that touches no secret).
29
+ # * `actions/checkout` runs with `persist-credentials: false`.
30
+ # * Lifecycle scripts are disabled during install so a malicious dev
31
+ # dep cannot run a postinstall during the scan itself.
32
+ # * No GitHub Actions cache — matches `ci.yml` rationale (shared cache
33
+ # scope between fork PRs and pushes to `main` is a known bridge).
34
+
35
+ name: Vuln scan
36
+
37
+ on:
38
+ schedule:
39
+ # 06:13 UTC daily — offset from CodeQL (Mon 04:37), Scorecard
40
+ # (Tue 05:23), and Zizmor (daily 07:00) so the four scheduled
41
+ # security workflows do not all queue at once.
42
+ - cron: "13 6 * * *"
43
+ workflow_dispatch:
44
+
45
+ permissions: {}
46
+
47
+ concurrency:
48
+ group: vuln-scan-${{ github.workflow }}-${{ github.ref }}
49
+ cancel-in-progress: true
50
+
51
+ jobs:
52
+ audit:
53
+ name: Daily SCA
54
+ runs-on: ubuntu-latest
55
+ timeout-minutes: 15
56
+ permissions:
57
+ contents: read
58
+
59
+ steps:
60
+ - name: Harden runner
61
+ uses: step-security/harden-runner@ab7a9404c0f3da075243ca237b5fac12c98deaa5 # v2
62
+ with:
63
+ egress-policy: audit
64
+ disable-sudo: true
65
+ disable-file-monitoring: false
66
+
67
+ - name: Checkout
68
+ uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
69
+ with:
70
+ persist-credentials: false
71
+ show-progress: false
72
+
73
+ __SETUP_PACKAGE_MANAGER_STEP__
74
+
75
+ - name: Set up Node.js
76
+ uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
77
+ with:
78
+ node-version: 24
79
+
80
+ __SETUP_BUN_RUNTIME_STEP__
81
+
82
+ - name: Install dependencies (no scripts)
83
+ run: __INSTALL_COMMAND__
84
+ env:
85
+ npm_config_ignore_scripts: "true"
86
+
87
+ __AUDIT_PROD_STEP__
88
+
89
+ __AUDIT_FULL_STEP__
@@ -21,6 +21,16 @@ A [DaloyJS](https://daloyjs.dev) REST API for the [Bun](https://bun.sh) runtime.
21
21
  - `generated/` — machine-written. Do not edit by hand.
22
22
  - `tests/` — Bun test files.
23
23
 
24
+ ## Imports
25
+
26
+ This project uses TypeScript with `"moduleResolution": "Bundler"` and `"allowImportingTsExtensions": true`. Relative imports use the **`.ts` extension** directly, since Bun executes TypeScript natively:
27
+
28
+ ```ts
29
+ import { buildApp } from "./build-app.ts";
30
+ ```
31
+
32
+ Do not write `.js` here — that's the Node NodeNext convention and will fail to resolve under Bun's setup. Bare-specifier imports from packages (`@daloyjs/core`, `zod`, …) do not need an extension.
33
+
24
34
  ## Core rules
25
35
 
26
36
  1. The route definition is the contract. Method, path, request schemas, and response schemas live in one place — `app.route({...})`.
@@ -44,6 +44,16 @@ bun run gen:client
44
44
  bun test
45
45
  ```
46
46
 
47
+ ## Imports
48
+
49
+ This project uses TypeScript with `"moduleResolution": "Bundler"` and `"allowImportingTsExtensions": true`. Relative imports use the **`.ts` extension** directly:
50
+
51
+ ```ts
52
+ import { buildApp } from "./build-app.ts";
53
+ ```
54
+
55
+ Do not use `.js` here — that's the Node NodeNext convention and will not resolve under Bun's setup.
56
+
47
57
  ## What's included
48
58
 
49
59
  - `@daloyjs/core` with starter security middleware: `secureHeaders`, `requestId`, and `rateLimit`.
@@ -0,0 +1,57 @@
1
+ # syntax=docker/dockerfile:1.7
2
+ # Container-first defaults for a DaloyJS app running on Bun.
3
+ #
4
+ # Hardening shipped out of the box:
5
+ # - Non-root runtime user (`bun`, uid 1000 — created by the official
6
+ # `oven/bun` image).
7
+ # - Read-only-root-filesystem friendly: no runtime writes outside the
8
+ # ephemeral working dir. Run with `--read-only --tmpfs /tmp` or set
9
+ # `readOnlyRootFilesystem: true` in your orchestrator.
10
+ # - `STOPSIGNAL SIGTERM` so DaloyJS's graceful-shutdown drain fires
11
+ # when the container is stopped.
12
+ # - `HEALTHCHECK` wired to the `/healthz` route registered in
13
+ # `src/build-app.ts`. The healthcheck uses BusyBox `wget` already
14
+ # present in the alpine base — no `curl`, no extra packages, no
15
+ # `exec`-ing shell scripts.
16
+ # - `tini` as PID 1 for proper signal forwarding and zombie reaping
17
+ # in case the app spawns subprocesses.
18
+ # - `pnpm install --frozen-lockfile --ignore-scripts` matches the
19
+ # supply-chain defaults in `.npmrc` (no lifecycle scripts run).
20
+ # - Base images are consumed through ARGs so production builds can
21
+ # pin to immutable digests:
22
+ # docker build --build-arg \
23
+ # NODE_IMAGE=node:24-alpine@sha256:<digest> \
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`
27
+ # workflow lints this file with hadolint and scans the built image
28
+ # with Trivy on every PR.
29
+
30
+ # Override at build time to pin a specific digest.
31
+ ARG NODE_IMAGE=node:24-alpine
32
+ ARG BUN_IMAGE=oven/bun:1-alpine
33
+
34
+ FROM ${NODE_IMAGE} AS builder
35
+ WORKDIR /app
36
+ # Install deps in a layer that only invalidates when manifests change.
37
+ COPY package.json pnpm-lock.yaml* ./
38
+ RUN corepack enable && corepack prepare pnpm@latest --activate && \
39
+ pnpm install --frozen-lockfile --ignore-scripts
40
+ COPY . .
41
+
42
+ FROM ${BUN_IMAGE} AS runner
43
+ WORKDIR /app
44
+ ENV NODE_ENV=production
45
+ # tini only — no curl, no extra packages. BusyBox `wget` (already in
46
+ # alpine) is enough for the HEALTHCHECK below.
47
+ RUN apk add --no-cache tini
48
+ COPY --from=builder --chown=bun:bun /app/node_modules ./node_modules
49
+ COPY --from=builder --chown=bun:bun /app/src ./src
50
+ COPY --from=builder --chown=bun:bun /app/package.json ./package.json
51
+ USER bun
52
+ EXPOSE 3000
53
+ STOPSIGNAL SIGTERM
54
+ HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
55
+ CMD wget -q -O /dev/null --spider http://127.0.0.1:3000/healthz || exit 1
56
+ ENTRYPOINT ["/sbin/tini", "--"]
57
+ CMD ["bun", "run", "src/index.ts"]
@@ -0,0 +1,12 @@
1
+ node_modules
2
+ .pnpm-store/
3
+ coverage
4
+ .git
5
+ .github
6
+ .vscode
7
+ *.log
8
+ .env
9
+ .env.*
10
+ !.env.example
11
+ generated
12
+ otherdocs
@@ -16,7 +16,7 @@
16
16
  "gen": "pnpm gen:openapi && pnpm gen:client"
17
17
  },
18
18
  "dependencies": {
19
- "@daloyjs/core": "^0.32.0",
19
+ "@daloyjs/core": "^0.34.1",
20
20
  "zod": "^4.4.3"
21
21
  },
22
22
  "devDependencies": {
@@ -0,0 +1,56 @@
1
+ # syntax=docker/dockerfile:1.7
2
+ # Containerized local-dev / CI environment for a DaloyJS Cloudflare Worker.
3
+ #
4
+ # Production deploys go through `wrangler deploy` to Cloudflare's edge —
5
+ # **this Dockerfile is not a production runtime**. Ship the Worker to
6
+ # Cloudflare; use this image for:
7
+ # - Reproducible local dev (devcontainers, GitHub Codespaces).
8
+ # - CI smoke tests that exercise the Worker against `wrangler dev`'s
9
+ # local workerd runtime without installing Node/pnpm on the runner.
10
+ # - Air-gapped review apps that need to host `wrangler dev` behind a
11
+ # reverse proxy.
12
+ #
13
+ # Hardening shipped out of the box:
14
+ # - Non-root runtime user (uid 1001).
15
+ # - Read-only-root-filesystem friendly: writes are confined to the
16
+ # working dir; mount `/tmp` as tmpfs (`--read-only --tmpfs /tmp`).
17
+ # - `STOPSIGNAL SIGTERM` so wrangler's child workerd process gets a
18
+ # clean shutdown signal.
19
+ # - Minimal runner surface: no `curl`, no `bash` extras beyond what
20
+ # `node:*-alpine` already ships. BusyBox `wget` powers the
21
+ # HEALTHCHECK.
22
+ # - `tini` as PID 1 for proper signal forwarding and zombie reaping
23
+ # (important because wrangler spawns workerd as a child process).
24
+ # - `pnpm install --frozen-lockfile --ignore-scripts` matches the
25
+ # supply-chain defaults in `.npmrc` (no lifecycle scripts run).
26
+ # - Base image is consumed through the `NODE_IMAGE` ARG so builds
27
+ # can pin to an immutable digest:
28
+ # docker build --build-arg \
29
+ # NODE_IMAGE=node:24-alpine@sha256:<digest> .
30
+
31
+ # Override at build time to pin a specific digest.
32
+ ARG NODE_IMAGE=node:24-alpine
33
+
34
+ FROM ${NODE_IMAGE} AS builder
35
+ WORKDIR /app
36
+ COPY package.json pnpm-lock.yaml* ./
37
+ RUN corepack enable && corepack prepare pnpm@latest --activate && \
38
+ pnpm install --frozen-lockfile --ignore-scripts
39
+ COPY . .
40
+
41
+ FROM ${NODE_IMAGE} AS runner
42
+ WORKDIR /app
43
+ ENV NODE_ENV=development
44
+ # tini only — no curl, no extra packages.
45
+ RUN apk add --no-cache tini && \
46
+ addgroup -S app -g 1001 && \
47
+ adduser -S app -G app -u 1001
48
+ COPY --from=builder --chown=app:app /app /app
49
+ USER app
50
+ EXPOSE 8787
51
+ STOPSIGNAL SIGTERM
52
+ HEALTHCHECK --interval=30s --timeout=5s --start-period=15s --retries=3 \
53
+ CMD wget -q -O /dev/null --spider http://127.0.0.1:8787/healthz || exit 1
54
+ ENTRYPOINT ["/sbin/tini", "--"]
55
+ # Bind to 0.0.0.0 so the container can be reached from the host network.
56
+ CMD ["./node_modules/.bin/wrangler", "dev", "--ip", "0.0.0.0", "--port", "8787"]
@@ -0,0 +1,14 @@
1
+ node_modules
2
+ .pnpm-store/
3
+ .wrangler
4
+ dist
5
+ coverage
6
+ .git
7
+ .github
8
+ .vscode
9
+ *.log
10
+ .env
11
+ .env.*
12
+ !.env.example
13
+ generated
14
+ otherdocs
@@ -10,7 +10,7 @@
10
10
  "test": "node --import tsx/esm --test tests/**/*.test.ts"
11
11
  },
12
12
  "dependencies": {
13
- "@daloyjs/core": "^0.32.0",
13
+ "@daloyjs/core": "^0.34.1",
14
14
  "zod": "^4.4.3"
15
15
  },
16
16
  "devDependencies": {
@@ -11,6 +11,23 @@ const app = new App({
11
11
  app.use(requestId());
12
12
  app.use(secureHeaders());
13
13
 
14
+ app.route({
15
+ method: "GET",
16
+ path: "/healthz",
17
+ operationId: "healthz",
18
+ tags: ["Ops"],
19
+ responses: {
20
+ 200: {
21
+ description: "Service is healthy",
22
+ body: z.object({ ok: z.literal(true), runtime: z.literal("cloudflare-worker") }),
23
+ },
24
+ },
25
+ handler: async () => ({
26
+ status: 200,
27
+ body: { ok: true as const, runtime: "cloudflare-worker" as const },
28
+ }),
29
+ });
30
+
14
31
  // daloy-minimal:strip-start books
15
32
  const Book = z.object({ id: z.string(), title: z.string() });
16
33
  const books = new Map<string, z.infer<typeof Book>>([
@@ -0,0 +1,66 @@
1
+ # syntax=docker/dockerfile:1.7
2
+ # Container-first defaults for a DaloyJS app running on Deno.
3
+ #
4
+ # Hardening shipped out of the box:
5
+ # - Non-root runtime user (`deno`, uid 1993 — created by the official
6
+ # `denoland/deno` image).
7
+ # - **Deno's capability-based permission model is the primary
8
+ # defense.** The runtime CMD passes only `--allow-net`,
9
+ # `--allow-env`, and scoped `--allow-read` — no `--allow-write`, no
10
+ # `--allow-run`, no `--allow-ffi`, no `--allow-sys`, no `--allow-all`.
11
+ # Network and env permissions are constrained to the server port and
12
+ # expected env vars so a compromised dependency cannot silently open
13
+ # arbitrary outbound sockets or read every secret in the environment.
14
+ # - `--cached-only` refuses any module that was not baked into the
15
+ # image at build time. A malicious republish of a transitive dep
16
+ # cannot ride in via a runtime `import("https://…")`.
17
+ # - Read-only-root-filesystem friendly: no runtime writes. Run with
18
+ # `--read-only --tmpfs /tmp` or set `readOnlyRootFilesystem: true`
19
+ # in your orchestrator.
20
+ # - `STOPSIGNAL SIGTERM` so DaloyJS's graceful-shutdown drain fires
21
+ # when the container is stopped.
22
+ # - `HEALTHCHECK` uses BusyBox `wget` already present in the alpine
23
+ # base — no `curl`, no extra packages.
24
+ # - `tini` as PID 1 for proper signal forwarding and zombie reaping.
25
+ # - Base image is consumed through the `DENO_IMAGE` ARG so production
26
+ # builds can pin to an immutable digest:
27
+ # docker build --build-arg \
28
+ # DENO_IMAGE=denoland/deno:alpine@sha256:<digest> .
29
+ # Dependabot's `docker` ecosystem (see `.github/dependabot.yml`)
30
+ # keeps the digest fresh.
31
+
32
+ # Override at build time to pin a specific digest.
33
+ ARG DENO_IMAGE=denoland/deno:alpine
34
+
35
+ FROM ${DENO_IMAGE} AS builder
36
+ WORKDIR /app
37
+ # Cache deps in a layer that only invalidates when imports change.
38
+ COPY deno.json deno.lock* ./
39
+ COPY src ./src
40
+ # `deno cache` resolves and verifies every import against the lockfile
41
+ # and bakes them into Deno's module cache. The resulting image cannot
42
+ # resolve any module that was not present at build time.
43
+ RUN deno cache --lock=deno.lock src/main.ts || deno cache src/main.ts
44
+
45
+ FROM ${DENO_IMAGE} AS runner
46
+ WORKDIR /app
47
+ ENV DENO_DIR=/deno-dir
48
+ ENV DENO_ENV=production
49
+ # tini only — no curl, no extra packages. BusyBox `wget` (already in
50
+ # alpine) is enough for the HEALTHCHECK below.
51
+ USER root
52
+ RUN apk add --no-cache tini
53
+ COPY --from=builder --chown=deno:deno /deno-dir /deno-dir
54
+ COPY --from=builder --chown=deno:deno /app/deno.json /app/deno.json
55
+ COPY --from=builder --chown=deno:deno /app/src /app/src
56
+ USER deno
57
+ EXPOSE 3000
58
+ STOPSIGNAL SIGTERM
59
+ HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
60
+ CMD wget -q -O /dev/null --spider http://127.0.0.1:3000/healthz || exit 1
61
+ ENTRYPOINT ["/sbin/tini", "--"]
62
+ # Minimum permissions for a DaloyJS HTTP server. Add more only if your
63
+ # app genuinely needs them — every flag widens the blast radius of a
64
+ # compromised dependency. If you change `PORT`, update `--allow-net` to
65
+ # match the exposed listen address.
66
+ CMD ["deno", "run", "--cached-only", "--allow-net=0.0.0.0:3000,127.0.0.1:3000,localhost:3000", "--allow-env=PORT,DENO_ENV", "--allow-read=/app/deno.json,/app/src", "src/main.ts"]
@@ -0,0 +1,11 @@
1
+ .deno/
2
+ coverage
3
+ .git
4
+ .github
5
+ .vscode
6
+ *.log
7
+ .env
8
+ .env.*
9
+ !.env.example
10
+ generated
11
+ otherdocs
@@ -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.32.0",
12
- "@daloyjs/core/": "npm:@daloyjs/core@^0.32.0/",
11
+ "@daloyjs/core": "npm:@daloyjs/core@^0.34.1",
12
+ "@daloyjs/core/": "npm:@daloyjs/core@^0.34.1/",
13
13
  "zod": "npm:zod@^4.4.3"
14
14
  },
15
15
  "compilerOptions": {
@@ -25,6 +25,16 @@ When `docs: true` is set in `new App({...})`, three routes are auto-mounted:
25
25
  - `generated/` — machine-written by `pnpm gen`. Do not edit by hand.
26
26
  - `tests/` — `*.test.ts` files run with `node --test` (via `tsx`).
27
27
 
28
+ ## Imports
29
+
30
+ This project uses TypeScript with `"module": "NodeNext"` (ESM). Relative imports **must include a `.js` extension**, even when the source file is `.ts`:
31
+
32
+ ```ts
33
+ import { buildApp } from "./build-app.js"; // resolves to build-app.ts at typecheck, build-app.js at runtime
34
+ ```
35
+
36
+ This is the official Node.js ESM convention — TypeScript rewrites the specifier during typecheck, and the compiled output really is `.js`. Bare-specifier imports from packages (`@daloyjs/core`, `zod`, …) do not need an extension.
37
+
28
38
  ## Core rules
29
39
 
30
40
  1. The route definition is the contract. Method, path, request schemas, and response schemas live in one place — `app.route({...})`.
@@ -46,6 +46,16 @@ pnpm build
46
46
  node dist/index.js
47
47
  ```
48
48
 
49
+ ## Imports
50
+
51
+ This project uses Node.js **ESM** with `"module": "NodeNext"`. Relative imports must include a `.js` extension, even when the source file is `.ts`:
52
+
53
+ ```ts
54
+ import { buildApp } from "./build-app.js"; // ./build-app.ts on disk
55
+ ```
56
+
57
+ TypeScript resolves the `.js` specifier to the matching `.ts` file at typecheck, and the compiled output really is `.js`. This is the official Node ESM convention — not a typo.
58
+
49
59
  ## What's included
50
60
 
51
61
  - `@daloyjs/core` with starter security middleware: `secureHeaders`, `requestId`, and `rateLimit`.