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