create-daloy 0.26.0 → 0.34.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.
- package/README.md +18 -5
- package/bin/create-daloy.mjs +130 -37
- package/package.json +5 -3
- package/sbom.cdx.json +56 -0
- package/sbom.spdx.json +42 -0
- package/templates/_ci/deno/SECURITY.md +5 -1
- package/templates/_ci/deno/_github/dependabot.yml +12 -1
- package/templates/_ci/deno/_github/workflows/container-scan.yml +158 -0
- package/templates/_ci/node/SECURITY.md +33 -3
- package/templates/_ci/node/_github/CODEOWNERS +1 -1
- package/templates/_ci/node/_github/dependabot.yml +12 -1
- package/templates/_ci/node/_github/workflows/container-scan.yml +177 -0
- package/templates/_ci/node/_github/workflows/vuln-scan.yml +89 -0
- package/templates/bun-basic/AGENTS.md +10 -0
- package/templates/bun-basic/README.md +10 -0
- package/templates/bun-basic/_Dockerfile +57 -0
- package/templates/bun-basic/_dockerignore +11 -0
- package/templates/bun-basic/package.json +1 -1
- package/templates/cloudflare-worker/_Dockerfile +56 -0
- package/templates/cloudflare-worker/_dockerignore +13 -0
- package/templates/cloudflare-worker/package.json +1 -1
- package/templates/cloudflare-worker/src/index.ts +17 -0
- package/templates/deno-basic/_Dockerfile +66 -0
- package/templates/deno-basic/_dockerignore +10 -0
- package/templates/deno-basic/deno.json +2 -2
- package/templates/node-basic/AGENTS.md +10 -0
- package/templates/node-basic/README.md +10 -0
- package/templates/node-basic/_Dockerfile +23 -10
- package/templates/node-basic/package.json +1 -1
- package/templates/vercel-edge/AGENTS.md +10 -0
- package/templates/vercel-edge/README.md +10 -0
- package/templates/vercel-edge/_Dockerfile +55 -0
- package/templates/vercel-edge/_dockerignore +13 -0
- package/templates/vercel-edge/package.json +1 -1
- package/templates/_ci/node/_github/workflows/release.yml +0 -125
|
@@ -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,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"]
|
|
@@ -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"]
|
|
@@ -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.
|
|
12
|
-
"@daloyjs/core/": "npm:@daloyjs/core@^0.
|
|
11
|
+
"@daloyjs/core": "npm:@daloyjs/core@^0.34.0",
|
|
12
|
+
"@daloyjs/core/": "npm:@daloyjs/core@^0.34.0/",
|
|
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`.
|