create-daloy 1.0.0-rc.3 → 1.0.0-rc.5

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.
@@ -58,8 +58,8 @@ const PACKAGE_MANAGERS = PACKAGE_MANAGER_OPTIONS.map((option) => option.value);
58
58
  // `--template <old>` commands (and published docs/blog posts) keep working.
59
59
  // Each maps to its current canonical template value.
60
60
  const TEMPLATE_ALIASES = new Map([
61
- // `vercel` shipped before Vercel deprecated standalone Edge Functions;
62
- // the template now targets the recommended Node.js runtime as `vercel`.
61
+ // The `vercel` template targets the Node.js runtime, which Vercel
62
+ // recommends over Edge for standalone functions (both run on Fluid compute).
63
63
  ]);
64
64
 
65
65
  const RENAME_ON_COPY = new Map([
@@ -487,6 +487,62 @@ const TOOL_INSTALL_GUIDES = {
487
487
  deno: { label: "Deno", url: "https://docs.deno.com/runtime/getting_started/installation/" },
488
488
  };
489
489
 
490
+ // Version floors the scaffolder and the projects it generates depend on. The
491
+ // CLI itself targets Node >= 24; npm scaffolds require npm >= 12 (see the
492
+ // injected `engines.npm` + engine-strict `.npmrc`); pnpm scaffolds require
493
+ // pnpm >= 11 (see the injected `engines.pnpm`, which pnpm always enforces).
494
+ // The pnpm floor is security-relevant: older pnpm silently ignores the
495
+ // `minimumReleaseAge` guardrail in the generated `pnpm-workspace.yaml`, so
496
+ // the 24h supply-chain cooldown would be off without any warning. We surface
497
+ // these up front so a user on an old runtime gets a clear "here's what to
498
+ // install" message instead of a cryptic syntax error, npm's raw EBADENGINE
499
+ // dump, or a silently weakened install posture.
500
+ const MIN_NODE_MAJOR = 24;
501
+ const MIN_NPM_MAJOR = 12;
502
+ const MIN_PNPM_MAJOR = 11;
503
+
504
+ /**
505
+ * Parse the leading major-version integer out of a version string such as
506
+ * `"v24.3.0"`, `"24.3.0"`, or `"10.8.2\n"`. Returns `NaN` when no leading
507
+ * number can be found so callers can fail open on an unrecognizable value.
508
+ *
509
+ * @param {string} version - a version string, e.g. `process.version` or `npm --version` output.
510
+ * @returns {number} the major version, or `NaN` if it cannot be parsed.
511
+ */
512
+ function parseMajorVersion(version) {
513
+ const match = /(\d+)/.exec(String(version ?? "").trim());
514
+ return match ? Number(match[1]) : NaN;
515
+ }
516
+
517
+ /**
518
+ * Decide whether the running Node.js satisfies the DaloyJS floor. Fails **open**
519
+ * on an unparseable version so a valid-but-unusual runtime is never wrongly
520
+ * blocked.
521
+ *
522
+ * @param {string} [version=process.version] - the Node.js version to check.
523
+ * @returns {{ ok: boolean, major: number }} whether it meets the floor, plus the parsed major.
524
+ */
525
+ function checkNodeVersion(version = process.version) {
526
+ const major = parseMajorVersion(version);
527
+ const ok = !Number.isFinite(major) || major >= MIN_NODE_MAJOR;
528
+ return { ok, major };
529
+ }
530
+
531
+ /**
532
+ * Resolve the installed major version of a CLI tool by running `<tool>
533
+ * --version`. Returns `NaN` when the tool is absent, errors, or prints an
534
+ * unparseable version — callers treat `NaN` as "unknown, don't block".
535
+ *
536
+ * @param {string} tool - bare executable name, e.g. `npm`.
537
+ * @param {string} [cwd] - working directory for the probe.
538
+ * @returns {Promise<number>} the tool's major version, or `NaN`.
539
+ */
540
+ async function detectToolMajor(tool, cwd) {
541
+ const { code, output } = await runQuiet(tool, ["--version"], cwd);
542
+ if (code !== 0) return NaN;
543
+ return parseMajorVersion(output);
544
+ }
545
+
490
546
  /**
491
547
  * Compute the CLIs a freshly-scaffolded project needs to run, given the chosen
492
548
  * template (which fixes the runtime) and package manager. Runtime-only
@@ -621,6 +677,21 @@ async function patchPackageJson(dir, projectName, packageManager) {
621
677
  if (json.scripts && packageManager && packageManager !== "pnpm") {
622
678
  json.scripts = rewriteScriptsForPackageManager(json.scripts, packageManager);
623
679
  }
680
+ // Package-manager engine floors are injected per manager so no project
681
+ // inherits an irrelevant constraint (templates ship without them):
682
+ // - npm scaffolds get `engines.npm >= 12`, paired with the engine-strict
683
+ // `.npmrc` from normalizePackageManagerFiles so the floor is enforced,
684
+ // not just advisory.
685
+ // - pnpm scaffolds get `engines.pnpm >= 11`; pnpm always enforces the
686
+ // project's `engines.pnpm` (no engine-strict needed). The floor matters
687
+ // because pnpm < 11 silently ignores `minimumReleaseAge` in
688
+ // pnpm-workspace.yaml — the 24h supply-chain cooldown would be off.
689
+ if (packageManager === "npm") {
690
+ json.engines = { ...json.engines, npm: ">=12.0.0" };
691
+ }
692
+ if (packageManager === "pnpm") {
693
+ json.engines = { ...json.engines, pnpm: `>=${MIN_PNPM_MAJOR}.0.0` };
694
+ }
624
695
  await writeFile(file, JSON.stringify(json, null, 2) + "\n", "utf8");
625
696
  }
626
697
 
@@ -707,16 +778,36 @@ function rewriteScriptsForPackageManager(scripts, pm) {
707
778
  return out;
708
779
  }
709
780
 
781
+ // npm-native `.npmrc` written for npm scaffolds. The pnpm-specific hardening
782
+ // keys (minimum-release-age, verify-store-integrity, …) mean nothing to npm,
783
+ // but `engine-strict=true` is honored by npm and turns the `engines.npm`
784
+ // floor in package.json into a hard failure instead of a silent warning — so
785
+ // DaloyJS's "npm >= 12" requirement is actually enforced at install time.
786
+ const NPM_STRICT_NPMRC = `# DaloyJS install-time guardrails for the npm CLI.
787
+ #
788
+ # engine-strict makes npm refuse to install when the developer's npm (or
789
+ # Node.js) is older than the "engines" floor in package.json — DaloyJS
790
+ # requires npm >= 12 — instead of only printing a warning. Remove this line
791
+ # only if you deliberately want to install on an unsupported npm version.
792
+ engine-strict=true
793
+ `;
794
+
710
795
  async function normalizePackageManagerFiles(dir, packageManager) {
711
796
  if (packageManager === "pnpm") return;
712
- // The hardened `.npmrc` and `pnpm-workspace.yaml` only make sense for pnpm.
713
- // Removing them keeps npm/yarn/bun scaffolds from inheriting pnpm-specific
714
- // settings the chosen package manager would either ignore or misinterpret.
715
- for (const file of [".npmrc", "pnpm-workspace.yaml"]) {
716
- const target = path.join(dir, file);
717
- if (existsSync(target)) {
718
- await rm(target, { force: true });
719
- }
797
+ // pnpm-workspace.yaml is pnpm-only; drop it for every other manager.
798
+ const workspace = path.join(dir, "pnpm-workspace.yaml");
799
+ if (existsSync(workspace)) {
800
+ await rm(workspace, { force: true });
801
+ }
802
+ // The hardened `.npmrc` is authored for pnpm. For npm we replace it with an
803
+ // npm-native `.npmrc` that keeps the one guardrail npm understands
804
+ // (`engine-strict=true`, enforcing the npm >= 12 engine floor). yarn and bun
805
+ // ignore or misinterpret these keys, so they get no `.npmrc` at all.
806
+ const npmrc = path.join(dir, ".npmrc");
807
+ if (packageManager === "npm") {
808
+ await writeFile(npmrc, NPM_STRICT_NPMRC, "utf8");
809
+ } else if (existsSync(npmrc)) {
810
+ await rm(npmrc, { force: true });
720
811
  }
721
812
  }
722
813
 
@@ -1789,6 +1880,26 @@ async function main() {
1789
1880
  process.exit(0);
1790
1881
  }
1791
1882
 
1883
+ // Preflight: the CLI and every template it emits require a modern Node.js.
1884
+ // `npm create daloy` / npx will happily run this on an old Node (engines are
1885
+ // advisory there), so we check ourselves and stop with a clear, actionable
1886
+ // message rather than scaffolding a project the user's runtime can't run.
1887
+ const nodeCheck = checkNodeVersion();
1888
+ if (!nodeCheck.ok) {
1889
+ printBanner(await readPkgVersion());
1890
+ logError(
1891
+ `DaloyJS needs Node.js ${MIN_NODE_MAJOR} or newer — you're on ${process.version}.`,
1892
+ );
1893
+ console.log(
1894
+ ` ${color(COLORS.yellow, SYMBOLS.pointer)} Install an up-to-date Node.js: ${color(COLORS.cyan + COLORS.underline, TOOL_INSTALL_GUIDES.node.url)}`,
1895
+ );
1896
+ console.log(
1897
+ ` ${color(COLORS.yellow, SYMBOLS.pointer)} ${color(COLORS.dim, "Or switch versions with a manager like nvm, fnm, or Volta.")}`,
1898
+ );
1899
+ console.log("");
1900
+ process.exit(1);
1901
+ }
1902
+
1792
1903
  printBanner(await readPkgVersion());
1793
1904
 
1794
1905
  const detectedPm = detectPackageManager();
@@ -1989,6 +2100,49 @@ async function main() {
1989
2100
  installDeps = false;
1990
2101
  }
1991
2102
 
2103
+ // npm scaffolds require npm >= 12 (enforced by the injected engines.npm +
2104
+ // engine-strict .npmrc). Detect an older npm here so we can explain the fix
2105
+ // clearly instead of leaving the user with npm's raw EBADENGINE failure.
2106
+ if (packageManager === "npm" && !missingTools.some((tool) => tool.tool === "npm")) {
2107
+ const npmMajor = await detectToolMajor("npm", targetDir);
2108
+ if (Number.isFinite(npmMajor) && npmMajor < MIN_NPM_MAJOR) {
2109
+ logWarn(`This project requires npm >= ${MIN_NPM_MAJOR}, but you're on npm ${npmMajor}.x.`);
2110
+ console.log(
2111
+ ` ${color(COLORS.yellow, SYMBOLS.pointer)} Upgrade npm: ${color(COLORS.cyan, "npm install -g npm@latest")} ${color(COLORS.dim, `(or reinstall Node.js: ${TOOL_INSTALL_GUIDES.npm.url})`)}`,
2112
+ );
2113
+ if (installDeps) {
2114
+ console.log(
2115
+ ` ${color(COLORS.yellow, SYMBOLS.pointer)} ${color(COLORS.dim, "Skipping the automatic install until npm is up to date.")}`,
2116
+ );
2117
+ installDeps = false;
2118
+ }
2119
+ }
2120
+ }
2121
+
2122
+ // pnpm scaffolds require pnpm >= 11 (enforced by the injected
2123
+ // engines.pnpm, which pnpm always checks). The floor is security-relevant:
2124
+ // pnpm < 11 silently ignores the pnpm-workspace.yaml `minimumReleaseAge`
2125
+ // guardrail, so the 24h supply-chain cooldown would be off without any
2126
+ // warning. Detect an older pnpm here and explain the fix clearly.
2127
+ if (packageManager === "pnpm" && !missingTools.some((tool) => tool.tool === "pnpm")) {
2128
+ const pnpmMajor = await detectToolMajor("pnpm", targetDir);
2129
+ if (Number.isFinite(pnpmMajor) && pnpmMajor < MIN_PNPM_MAJOR) {
2130
+ logWarn(`This project requires pnpm >= ${MIN_PNPM_MAJOR}, but you're on pnpm ${pnpmMajor}.x.`);
2131
+ console.log(
2132
+ ` ${color(COLORS.yellow, SYMBOLS.pointer)} Upgrade pnpm: ${color(COLORS.cyan, "npm install -g pnpm@latest")} ${color(COLORS.dim, `(or corepack use pnpm@latest — ${TOOL_INSTALL_GUIDES.pnpm.url})`)}`,
2133
+ );
2134
+ console.log(
2135
+ ` ${color(COLORS.yellow, SYMBOLS.pointer)} ${color(COLORS.dim, `pnpm < ${MIN_PNPM_MAJOR} silently ignores minimumReleaseAge in pnpm-workspace.yaml, disabling the 24h supply-chain cooldown.`)}`,
2136
+ );
2137
+ if (installDeps) {
2138
+ console.log(
2139
+ ` ${color(COLORS.yellow, SYMBOLS.pointer)} ${color(COLORS.dim, "Skipping the automatic install until pnpm is up to date.")}`,
2140
+ );
2141
+ installDeps = false;
2142
+ }
2143
+ }
2144
+ }
2145
+
1992
2146
  if (installDeps) {
1993
2147
  const spinner = createSpinner(`Installing dependencies with ${color(COLORS.cyan, packageManager)}\u2026`);
1994
2148
  spinner.start();
@@ -2033,4 +2187,15 @@ if (process.env.DALOY_TEST_IMPORT !== "1") {
2033
2187
  await main();
2034
2188
  }
2035
2189
 
2036
- export { choiceInputMode, requiredTools, missingToolGuides, isToolInstalled, TOOL_INSTALL_GUIDES };
2190
+ export {
2191
+ choiceInputMode,
2192
+ requiredTools,
2193
+ missingToolGuides,
2194
+ isToolInstalled,
2195
+ TOOL_INSTALL_GUIDES,
2196
+ parseMajorVersion,
2197
+ checkNodeVersion,
2198
+ MIN_NODE_MAJOR,
2199
+ MIN_NPM_MAJOR,
2200
+ MIN_PNPM_MAJOR,
2201
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-daloy",
3
- "version": "1.0.0-rc.3",
3
+ "version": "1.0.0-rc.5",
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:837a492d-6f26-5ff8-b1e0-689b969309f7",
4
+ "serialNumber": "urn:uuid:02713320-c651-582f-b203-48552fa27a2e",
5
5
  "version": 1,
6
6
  "metadata": {
7
- "timestamp": "2026-07-09T19:29:37.132Z",
7
+ "timestamp": "2026-07-20T09:04:50.510Z",
8
8
  "tools": [
9
9
  {
10
10
  "vendor": "DaloyJS",
11
11
  "name": "daloy-generate-sbom",
12
- "version": "1.0.0-rc.3"
12
+ "version": "1.0.0-rc.5"
13
13
  }
14
14
  ],
15
15
  "authors": [],
16
16
  "component": {
17
17
  "type": "library",
18
- "bom-ref": "pkg:npm/create-daloy@1.0.0-rc.3",
18
+ "bom-ref": "pkg:npm/create-daloy@1.0.0-rc.5",
19
19
  "name": "create-daloy",
20
- "version": "1.0.0-rc.3",
20
+ "version": "1.0.0-rc.5",
21
21
  "description": "Scaffold a new DaloyJS project. Run with `pnpm create daloy`, `npm create daloy@latest`, `yarn create daloy`, or `bun create daloy`.",
22
- "purl": "pkg:npm/create-daloy@1.0.0-rc.3",
22
+ "purl": "pkg:npm/create-daloy@1.0.0-rc.5",
23
23
  "licenses": [
24
24
  {
25
25
  "license": {
@@ -42,9 +42,9 @@
42
42
  }
43
43
  ],
44
44
  "swid": {
45
- "tagId": "swidtag-create-daloy-1.0.0-rc.3",
45
+ "tagId": "swidtag-create-daloy-1.0.0-rc.5",
46
46
  "name": "create-daloy",
47
- "version": "1.0.0-rc.3",
47
+ "version": "1.0.0-rc.5",
48
48
  "tagVersion": 0,
49
49
  "patch": false
50
50
  }
@@ -53,7 +53,7 @@
53
53
  "components": [],
54
54
  "dependencies": [
55
55
  {
56
- "ref": "pkg:npm/create-daloy@1.0.0-rc.3",
56
+ "ref": "pkg:npm/create-daloy@1.0.0-rc.5",
57
57
  "dependsOn": []
58
58
  }
59
59
  ]
package/sbom.spdx.json CHANGED
@@ -2,10 +2,10 @@
2
2
  "spdxVersion": "SPDX-2.3",
3
3
  "dataLicense": "CC0-1.0",
4
4
  "SPDXID": "SPDXRef-DOCUMENT",
5
- "name": "create-daloy-1.0.0-rc.3",
6
- "documentNamespace": "https://github.com/daloyjs/daloy/sbom/create-daloy-1.0.0-rc.3-837a492d-6f26-5ff8-b1e0-689b969309f7",
5
+ "name": "create-daloy-1.0.0-rc.5",
6
+ "documentNamespace": "https://github.com/daloyjs/daloy/sbom/create-daloy-1.0.0-rc.5-02713320-c651-582f-b203-48552fa27a2e",
7
7
  "creationInfo": {
8
- "created": "2026-07-09T19:29:37.132Z",
8
+ "created": "2026-07-20T09:04:50.510Z",
9
9
  "creators": [
10
10
  "Tool: daloy-generate-sbom",
11
11
  "Organization: DaloyJS"
@@ -16,7 +16,7 @@
16
16
  {
17
17
  "SPDXID": "SPDXRef-Package-create-daloy",
18
18
  "name": "create-daloy",
19
- "versionInfo": "1.0.0-rc.3",
19
+ "versionInfo": "1.0.0-rc.5",
20
20
  "downloadLocation": "https://github.com/daloyjs/daloy",
21
21
  "filesAnalyzed": false,
22
22
  "licenseConcluded": "MIT",
@@ -27,7 +27,7 @@
27
27
  {
28
28
  "referenceCategory": "PACKAGE-MANAGER",
29
29
  "referenceType": "purl",
30
- "referenceLocator": "pkg:npm/create-daloy@1.0.0-rc.3"
30
+ "referenceLocator": "pkg:npm/create-daloy@1.0.0-rc.5"
31
31
  }
32
32
  ]
33
33
  }
@@ -42,7 +42,7 @@ Do not write `.js` here — that's the Node NodeNext convention and will fail to
42
42
 
43
43
  ## Core rules
44
44
 
45
- 1. The route definition is the contract. Method, path, request schemas, and response schemas live in one place — `app.route({...})`.
45
+ 1. The route definition is the contract. Method, path, request schemas, and response schemas live in one place — `app.get(path, contract, handler)` (or `app.route({...})` for reusable `defineRoute()` contracts or metadata-heavy routes).
46
46
  2. Validate every input with Zod or another Standard Schema-compatible validator. For Zod object schemas, use `.strict()` to reject unknown keys at the boundary.
47
47
  3. Preserve literal types in responses: `status: 200 as const`, `z.literal(...)` on discriminator fields. Codegen depends on these.
48
48
  4. Throw typed errors (`NotFoundError`, `BadRequestError`, etc.) from `@daloyjs/core` — never return raw error responses.
@@ -34,9 +34,13 @@ DaloyJS is a **contract-first** framework. Internalize these rules — every
34
34
  recommendation below follows from them:
35
35
 
36
36
  1. **The route definition is the contract.** Method, path, request schemas,
37
- and response schemas live in one place (`app.route({...})`). The OpenAPI
38
- spec, the typed client, and the runtime validation are all derived from
39
- it.
37
+ and response schemas live in one place `app.get(path, contract, handler)`
38
+ (and the matching `app.post`/`put`/`patch`/`delete`/`head` shorthands), or
39
+ `app.route({...})` when you need a reusable `defineRoute()` contract or a
40
+ metadata-heavy route. Both forms produce identical runtime behavior,
41
+ validation, security, and OpenAPI output. The OpenAPI spec, the typed
42
+ client, and the runtime validation are all derived from the route
43
+ definition.
40
44
  2. **Validation schemas protect every boundary.** This template uses Zod,
41
45
  and Daloy accepts any Standard Schema-compatible library. Body, params,
42
46
  query, and headers go through the declared schema.
@@ -118,10 +122,14 @@ when it helps consumers understand or safely automate the route:
118
122
  2. **Design schemas first.** Define request body/params/query/headers and a
119
123
  response body per status code. Prefer `z.object({...}).strict()` for
120
124
  inputs so unknown keys are rejected at the boundary.
121
- 3. **Call `app.route({...})`** with `method`, `path`, `operationId`, `tags`,
122
- `responses`, `handler` (plus `request` when accepting input). Add `meta`
123
- examples / descriptions when the route is user-facing or consumed by
124
- agents.
125
+ 3. **Call the method shorthand: `app.get(path, contract, handler)`** (or
126
+ `app.post`/`put`/`patch`/`delete`/`head` for other methods). The contract
127
+ object's required keys are `operationId`, `tags`, `responses`. Add
128
+ `request` when the route accepts input, and add `meta` examples /
129
+ descriptions when the route is user-facing or consumed by agents. Reach
130
+ for the full `app.route({ method, path, ...contract, handler })` form
131
+ instead when the route is built from a reusable `defineRoute()` contract,
132
+ or when composing many routes at once via `registerRoutes()`.
125
133
  4. **Return `{ status, body, headers? }` from the handler.** Always use
126
134
  `status: 200 as const` so the typed client can narrow.
127
135
  5. **Throw typed errors**, do not return raw error responses. Use
@@ -143,22 +151,23 @@ import { NotFoundError } from "@daloyjs/core";
143
151
  const Book = z.object({ id: z.string(), title: z.string() }).strict();
144
152
  const BookParams = z.object({ id: z.string().min(1) }).strict();
145
153
 
146
- app.route({
147
- method: "GET",
148
- path: "/books/:id",
149
- operationId: "getBookById",
150
- tags: ["Books"],
151
- request: { params: BookParams },
152
- responses: {
153
- 200: { description: "Found", body: Book },
154
- 404: { description: "Not found" },
154
+ app.get(
155
+ "/books/:id",
156
+ {
157
+ operationId: "getBookById",
158
+ tags: ["Books"],
159
+ request: { params: BookParams },
160
+ responses: {
161
+ 200: { description: "Found", body: Book },
162
+ 404: { description: "Not found" },
163
+ },
155
164
  },
156
- handler: async ({ params }) => {
165
+ async ({ params }) => {
157
166
  const book = await store.find(params.id);
158
167
  if (!book) throw new NotFoundError(`Book ${params.id} not found`);
159
168
  return { status: 200 as const, body: book };
160
- },
161
- });
169
+ }
170
+ );
162
171
  ```
163
172
 
164
173
  ## Validation & schema conventions
@@ -222,6 +231,9 @@ Cover both **happy paths and unhappy paths** for every route: valid input,
222
231
  validation failures (400), auth failures (401/403), not-found (404),
223
232
  conflicts (409), rate limiting (429). For external services, inject an
224
233
  in-memory fake via `buildApp({ store })` rather than mocking `fetch`.
234
+ For user-owned or tenant-owned resources, use at least two principals and
235
+ prove that Alice's valid token cannot list, read, update, or delete Bob's
236
+ record.
225
237
  The shipped contract test should fail invalid examples, duplicate/missing
226
238
  `operationId`, or missing responses.
227
239
 
@@ -240,6 +252,17 @@ Aim for complete happy- and unhappy-path test coverage of the routes you add.
240
252
  at boot. Fail fast on missing config.
241
253
  - For auth, verify JWT signatures against an allowlist of keys, never
242
254
  trust the `alg` header, always check `exp` / `nbf`.
255
+ - Authentication and scopes are not resource authorization. For every route
256
+ that accepts a resource id, classify the resource as public, user-owned,
257
+ tenant-owned, shared, or administrator-only.
258
+ - Scope user-owned and tenant-owned database reads and writes with both the
259
+ caller-controlled id and the trusted owner / tenant from the verified
260
+ principal. Do not fetch by id alone and rely on the UI or a later caller to
261
+ remember the ownership check.
262
+ - Never accept `ownerId`, `userId`, `tenantId`, `role`, or another privileged
263
+ ownership field from an ordinary request body. Derive it from the verified
264
+ principal and reject the field with a strict request schema.
265
+ - Use an explicit, permissioned, audited path for administrator bypasses.
243
266
  - Validate redirects against an allowlist.
244
267
  - Set `bodyLimitBytes` and `requestTimeoutMs` on `new App({...})` to
245
268
  mitigate DoS.
@@ -19,12 +19,12 @@
19
19
  "hooks:install": "git config core.hooksPath .githooks"
20
20
  },
21
21
  "dependencies": {
22
- "@daloyjs/core": "^1.0.0-rc.3",
22
+ "@daloyjs/core": "^1.0.0-rc.5",
23
23
  "zod": "^4.4.3"
24
24
  },
25
25
  "devDependencies": {
26
- "@hey-api/openapi-ts": "^0.99.0",
26
+ "@hey-api/openapi-ts": "0.0.0-next-20260711024907",
27
27
  "@types/bun": "^1.1.0",
28
- "typescript": "^6.0.3"
28
+ "typescript": "^7.0.2"
29
29
  }
30
30
  }
@@ -58,22 +58,23 @@ export function buildApp(): App {
58
58
  app.use(secureHeaders());
59
59
  app.use(rateLimit({ windowMs: 60_000, max: 120 }));
60
60
 
61
- app.route({
62
- method: "GET",
63
- path: "/healthz",
64
- operationId: "healthz",
65
- tags: ["Ops"],
66
- responses: {
67
- 200: {
68
- description: "Service is healthy",
69
- body: z.object({ ok: z.literal(true), runtime: z.literal("bun") }),
61
+ app.get(
62
+ "/healthz",
63
+ {
64
+ operationId: "healthz",
65
+ tags: ["Ops"],
66
+ responses: {
67
+ 200: {
68
+ description: "Service is healthy",
69
+ body: z.object({ ok: z.literal(true), runtime: z.literal("bun") }),
70
+ },
70
71
  },
71
72
  },
72
- handler: async () => ({
73
+ async () => ({
73
74
  status: 200 as const,
74
75
  body: { ok: true as const, runtime: "bun" as const },
75
76
  }),
76
- });
77
+ );
77
78
 
78
79
  // daloy-minimal:strip-start books
79
80
  const Book = z.object({ id: z.string(), title: z.string() }).strict();
@@ -82,22 +83,23 @@ export function buildApp(): App {
82
83
  ["2", { id: "2", title: "El Filibusterismo" }],
83
84
  ]);
84
85
 
85
- app.route({
86
- method: "GET",
87
- path: "/books/:id",
88
- operationId: "getBookById",
89
- tags: ["Books"],
90
- request: { params: z.object({ id: z.string().min(1) }).strict() },
91
- responses: {
92
- 200: { description: "Found", body: Book },
93
- 404: { description: "Not found" },
86
+ app.get(
87
+ "/books/:id",
88
+ {
89
+ operationId: "getBookById",
90
+ tags: ["Books"],
91
+ request: { params: z.object({ id: z.string().min(1) }).strict() },
92
+ responses: {
93
+ 200: { description: "Found", body: Book },
94
+ 404: { description: "Not found" },
95
+ },
94
96
  },
95
- handler: async ({ params }) => {
97
+ async ({ params }) => {
96
98
  const book = books.get(params.id);
97
99
  if (!book) throw new NotFoundError(`Book ${params.id} not found`);
98
100
  return { status: 200 as const, body: book };
99
101
  },
100
- });
102
+ );
101
103
  // daloy-minimal:strip-end books
102
104
 
103
105
  return app;
@@ -30,7 +30,7 @@ A [DaloyJS](https://daloyjs.dev) REST API deployed to **Cloudflare Workers**. **
30
30
 
31
31
  ## Core rules
32
32
 
33
- 1. The route definition is the contract. Method, path, request schemas, and response schemas live in one place — `app.route({...})`.
33
+ 1. The route definition is the contract. Method, path, request schemas, and response schemas live in one place — `app.get(path, contract, handler)` (or `app.route({...})` for reusable `defineRoute()` contracts or metadata-heavy routes).
34
34
  2. Validate every input with Zod or another Standard Schema-compatible validator. For Zod object schemas, use `.strict()` to reject unknown keys at the boundary.
35
35
  3. Preserve literal types in responses: `status: 200 as const`, `z.literal(...)` on discriminator fields.
36
36
  4. Throw typed errors (`NotFoundError`, `BadRequestError`, etc.) from `@daloyjs/core`.
@@ -37,7 +37,11 @@ DaloyJS is a **contract-first** framework. On Workers, additionally:
37
37
  Cloudflare-specific bindings. No `node:` modules unless the user
38
38
  explicitly adds `nodejs_compat` to `wrangler.toml` and opts in.
39
39
  2. **The route definition is the contract.** Method, path, request
40
- schemas, and response schemas live in one place (`app.route({...})`).
40
+ schemas, and response schemas live in one place. Use the shorthand
41
+ `app.get(path, contract, handler)` (`app.post`, `app.put`, `app.patch`,
42
+ `app.delete`, `app.head`) for ordinary routes; use `app.route({...})`
43
+ for reusable `defineRoute()` contracts, metadata-heavy routes, or when
44
+ composing many routes via `registerRoutes()`.
41
45
  3. **Validation schemas protect every boundary.** This template uses Zod,
42
46
  and Daloy accepts any Standard Schema-compatible library.
43
47
  4. **Preserve literal types.** Return `status: 200 as const`.
@@ -112,10 +116,14 @@ when it helps consumers understand or safely automate the route:
112
116
 
113
117
  1. **Open `src/index.ts`.**
114
118
  2. **Design schemas first.** Use `z.object({...}).strict()` for inputs.
115
- 3. **Call `app.route({...})`** with `method`, `path`, `operationId`,
116
- `tags`, `responses`, `handler` (plus `request` when accepting input).
117
- Add `meta` examples / descriptions when the route is user-facing or
118
- consumed by agents.
119
+ 3. **Call `app.get(path, contract, handler)`** (or the matching
120
+ `app.post`/`app.put`/`app.patch`/`app.delete`/`app.head` shorthand) with
121
+ a contract object holding `operationId`, `tags`, `responses` (plus
122
+ `request` when accepting input). Add `meta` examples / descriptions when
123
+ the route is user-facing or consumed by agents. Reach for the full
124
+ `app.route({...})` form instead when the contract is a reusable
125
+ `defineRoute()` value, is metadata-heavy, or is being composed with many
126
+ other routes via `registerRoutes()`.
119
127
  4. **Return `{ status, body, headers? }`** with `status: 200 as const`.
120
128
  5. **Throw typed errors** (`NotFoundError`, `BadRequestError`, etc.).
121
129
  6. **Add a test** under `tests/`. Use `app.request(...)` for pure logic;
@@ -144,22 +152,23 @@ function buildApp(env: Env) {
144
152
  app.use(secureHeaders());
145
153
  app.use(rateLimit({ windowMs: 60_000, max: 120 }));
146
154
 
147
- app.route({
148
- method: "GET",
149
- path: "/books/:id",
150
- operationId: "getBookById",
151
- tags: ["Books"],
152
- request: { params: z.object({ id: z.string().min(1) }).strict() },
153
- responses: {
154
- 200: { description: "Found", body: Book },
155
- 404: { description: "Not found" },
155
+ app.get(
156
+ "/books/:id",
157
+ {
158
+ operationId: "getBookById",
159
+ tags: ["Books"],
160
+ request: { params: z.object({ id: z.string().min(1) }).strict() },
161
+ responses: {
162
+ 200: { description: "Found", body: Book },
163
+ 404: { description: "Not found" },
164
+ },
156
165
  },
157
- handler: async ({ params }) => {
166
+ async ({ params }) => {
158
167
  const raw = await env.BOOKS.get(params.id, "json");
159
168
  if (!raw) throw new NotFoundError(`Book ${params.id} not found`);
160
169
  return { status: 200 as const, body: Book.parse(raw) };
161
- },
162
- });
170
+ }
171
+ );
163
172
 
164
173
  return app;
165
174
  }
@@ -221,6 +230,9 @@ Cover **happy paths and unhappy paths** for every route: valid input,
221
230
  validation failures (400), auth failures (401/403), not-found (404),
222
231
  conflict (409), rate limiting (429). For external services, inject an
223
232
  in-memory fake into `buildApp(env)` during tests.
233
+ For user-owned or tenant-owned resources, use at least two principals and
234
+ prove that Alice's valid token cannot list, read, update, or delete Bob's
235
+ record.
224
236
  The shipped contract test should fail invalid examples, duplicate/missing
225
237
  `operationId`, or missing responses.
226
238
 
@@ -240,6 +252,17 @@ Aim for complete happy- and unhappy-path test coverage of the routes you add.
240
252
  `wrangler.toml`.
241
253
  - For auth, verify JWT signatures with the Web Crypto API
242
254
  (`crypto.subtle`). Never trust the `alg` header from the token.
255
+ - Authentication and scopes are not resource authorization. For every route
256
+ that accepts a resource id, classify the resource as public, user-owned,
257
+ tenant-owned, shared, or administrator-only.
258
+ - Scope user-owned and tenant-owned database reads and writes with both the
259
+ caller-controlled id and the trusted owner / tenant from the verified
260
+ principal. Do not fetch by id alone and rely on the UI or a later caller to
261
+ remember the ownership check.
262
+ - Never accept `ownerId`, `userId`, `tenantId`, `role`, or another privileged
263
+ ownership field from an ordinary request body. Derive it from the verified
264
+ principal and reject the field with a strict request schema.
265
+ - Use an explicit, permissioned, audited path for administrator bypasses.
243
266
  - Validate redirects against an allowlist.
244
267
  - Set `bodyLimitBytes` and `requestTimeoutMs` on `new App({...})` to
245
268
  mitigate DoS.
@@ -16,12 +16,12 @@
16
16
  "hooks:install": "git config core.hooksPath .githooks"
17
17
  },
18
18
  "dependencies": {
19
- "@daloyjs/core": "^1.0.0-rc.3",
19
+ "@daloyjs/core": "^1.0.0-rc.5",
20
20
  "zod": "^4.4.3"
21
21
  },
22
22
  "devDependencies": {
23
23
  "@cloudflare/workers-types": "^4.20240909.0",
24
- "typescript": "^6.0.3",
24
+ "typescript": "^7.0.2",
25
25
  "wrangler": "^4.0.0"
26
26
  }
27
27
  }