create-daloy 1.0.0-rc.4 → 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.4",
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:d3388094-26fe-5e1d-980d-c256c3d08aef",
4
+ "serialNumber": "urn:uuid:02713320-c651-582f-b203-48552fa27a2e",
5
5
  "version": 1,
6
6
  "metadata": {
7
- "timestamp": "2026-07-12T20:39:15.641Z",
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.4"
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.4",
18
+ "bom-ref": "pkg:npm/create-daloy@1.0.0-rc.5",
19
19
  "name": "create-daloy",
20
- "version": "1.0.0-rc.4",
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.4",
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.4",
45
+ "tagId": "swidtag-create-daloy-1.0.0-rc.5",
46
46
  "name": "create-daloy",
47
- "version": "1.0.0-rc.4",
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.4",
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.4",
6
- "documentNamespace": "https://github.com/daloyjs/daloy/sbom/create-daloy-1.0.0-rc.4-d3388094-26fe-5e1d-980d-c256c3d08aef",
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-12T20:39:15.641Z",
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.4",
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.4"
30
+ "referenceLocator": "pkg:npm/create-daloy@1.0.0-rc.5"
31
31
  }
32
32
  ]
33
33
  }
@@ -166,7 +166,7 @@ app.get(
166
166
  const book = await store.find(params.id);
167
167
  if (!book) throw new NotFoundError(`Book ${params.id} not found`);
168
168
  return { status: 200 as const, body: book };
169
- },
169
+ }
170
170
  );
171
171
  ```
172
172
 
@@ -231,6 +231,9 @@ Cover both **happy paths and unhappy paths** for every route: valid input,
231
231
  validation failures (400), auth failures (401/403), not-found (404),
232
232
  conflicts (409), rate limiting (429). For external services, inject an
233
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.
234
237
  The shipped contract test should fail invalid examples, duplicate/missing
235
238
  `operationId`, or missing responses.
236
239
 
@@ -249,6 +252,17 @@ Aim for complete happy- and unhappy-path test coverage of the routes you add.
249
252
  at boot. Fail fast on missing config.
250
253
  - For auth, verify JWT signatures against an allowlist of keys, never
251
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.
252
266
  - Validate redirects against an allowlist.
253
267
  - Set `bodyLimitBytes` and `requestTimeoutMs` on `new App({...})` to
254
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.4",
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
  }
@@ -167,7 +167,7 @@ function buildApp(env: Env) {
167
167
  const raw = await env.BOOKS.get(params.id, "json");
168
168
  if (!raw) throw new NotFoundError(`Book ${params.id} not found`);
169
169
  return { status: 200 as const, body: Book.parse(raw) };
170
- },
170
+ }
171
171
  );
172
172
 
173
173
  return app;
@@ -230,6 +230,9 @@ Cover **happy paths and unhappy paths** for every route: valid input,
230
230
  validation failures (400), auth failures (401/403), not-found (404),
231
231
  conflict (409), rate limiting (429). For external services, inject an
232
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.
233
236
  The shipped contract test should fail invalid examples, duplicate/missing
234
237
  `operationId`, or missing responses.
235
238
 
@@ -249,6 +252,17 @@ Aim for complete happy- and unhappy-path test coverage of the routes you add.
249
252
  `wrangler.toml`.
250
253
  - For auth, verify JWT signatures with the Web Crypto API
251
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.
252
266
  - Validate redirects against an allowlist.
253
267
  - Set `bodyLimitBytes` and `requestTimeoutMs` on `new App({...})` to
254
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.4",
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
  }
@@ -167,7 +167,7 @@ app.get(
167
167
  const book = await store.find(params.id);
168
168
  if (!book) throw new NotFoundError(`Book ${params.id} not found`);
169
169
  return { status: 200 as const, body: book };
170
- },
170
+ }
171
171
  );
172
172
  ```
173
173
 
@@ -228,6 +228,9 @@ Cover **happy paths and unhappy paths**: valid input, validation failures
228
228
  (400), auth failures (401/403), not-found (404), conflict (409), rate
229
229
  limiting (429). For external services, inject an in-memory fake via
230
230
  `buildApp({ store })`.
231
+ For user-owned or tenant-owned resources, use at least two principals and
232
+ prove that Alice's valid token cannot list, read, update, or delete Bob's
233
+ record.
231
234
  The shipped contract test should fail invalid examples, duplicate/missing
232
235
  `operationId`, or missing responses.
233
236
 
@@ -249,6 +252,17 @@ Aim for complete happy- and unhappy-path test coverage of the routes you add.
249
252
  missing config.
250
253
  - For auth, verify JWT signatures against an allowlist of keys, never
251
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.
252
266
  - Validate redirects against an allowlist.
253
267
  - Set `bodyLimitBytes` and `requestTimeoutMs` on `new App({...})` to
254
268
  mitigate DoS.
@@ -10,11 +10,11 @@
10
10
  "hooks:install": "git config core.hooksPath .githooks"
11
11
  },
12
12
  "imports": {
13
- "@daloyjs/core": "jsr:@daloyjs/daloy@^1.0.0-rc.4",
14
- "@daloyjs/core/banner": "jsr:@daloyjs/daloy@^1.0.0-rc.4/banner",
15
- "@daloyjs/core/contract": "jsr:@daloyjs/daloy@^1.0.0-rc.4/contract",
16
- "@daloyjs/core/deno": "jsr:@daloyjs/daloy@^1.0.0-rc.4/deno",
17
- "@daloyjs/core/openapi": "jsr:@daloyjs/daloy@^1.0.0-rc.4/openapi",
13
+ "@daloyjs/core": "jsr:@daloyjs/daloy@^1.0.0-rc.5",
14
+ "@daloyjs/core/banner": "jsr:@daloyjs/daloy@^1.0.0-rc.5/banner",
15
+ "@daloyjs/core/contract": "jsr:@daloyjs/daloy@^1.0.0-rc.5/contract",
16
+ "@daloyjs/core/deno": "jsr:@daloyjs/daloy@^1.0.0-rc.5/deno",
17
+ "@daloyjs/core/openapi": "jsr:@daloyjs/daloy@^1.0.0-rc.5/openapi",
18
18
  "zod": "npm:zod@^4.4.3"
19
19
  },
20
20
  "compilerOptions": {
@@ -189,7 +189,7 @@ app.get(
189
189
  const book = await store.find(params.id);
190
190
  if (!book) throw new NotFoundError(`Book ${params.id} not found`);
191
191
  return { status: 200 as const, body: book };
192
- },
192
+ }
193
193
  );
194
194
  ```
195
195
 
@@ -277,6 +277,9 @@ Cover both **happy paths** and **unhappy paths** for every route:
277
277
  - Validation failure: missing/invalid fields → `400` with problem details.
278
278
  - Auth failure (when applicable): unauthenticated → `401`; wrong scope →
279
279
  `403`.
280
+ - Resource authorization (when a route accepts an id): Alice can access
281
+ Alice's record, but the same valid Alice token receives `404` for Bob's
282
+ record. Cover reads, lists, updates, and deletes where applicable.
280
283
  - Not found: unknown id → `404`.
281
284
  - Conflict: duplicate create → `409`.
282
285
  - Rate limit: hammer the route → `429` after the configured threshold.
@@ -307,6 +310,17 @@ honor them.
307
310
  - For auth, prefer a small JWT or session middleware over rolling your
308
311
  own. Verify signatures against an allowlist of keys, never trust the
309
312
  `alg` header from the token, and always check `exp` / `nbf`.
313
+ - Authentication and scopes are not resource authorization. For every route
314
+ that accepts a resource id, classify the resource as public, user-owned,
315
+ tenant-owned, shared, or administrator-only.
316
+ - Scope user-owned and tenant-owned database reads and writes with both the
317
+ caller-controlled id and the trusted owner / tenant from the verified
318
+ principal. Do not fetch by id alone and rely on the UI or a later caller to
319
+ remember the ownership check.
320
+ - Never accept `ownerId`, `userId`, `tenantId`, `role`, or another privileged
321
+ ownership field from an ordinary request body. Derive it from the verified
322
+ principal and reject the field with a strict request schema.
323
+ - Use an explicit, permissioned, audited path for administrator bypasses.
310
324
  - Validate redirects against an allowlist. Open redirects are an OWASP
311
325
  Top-10 issue.
312
326
  - Limit body sizes via `bodyLimitBytes` on `new App({...})` — large
@@ -20,12 +20,12 @@
20
20
  "hooks:install": "git config core.hooksPath .githooks"
21
21
  },
22
22
  "dependencies": {
23
- "@daloyjs/core": "^1.0.0-rc.4",
23
+ "@daloyjs/core": "^1.0.0-rc.5",
24
24
  "zod": "^4.4.3"
25
25
  },
26
26
  "devDependencies": {
27
- "@hey-api/openapi-ts": "^0.99.0",
27
+ "@hey-api/openapi-ts": "0.0.0-next-20260711024907",
28
28
  "@types/node": "^26.1.0",
29
- "typescript": "^6.0.3"
29
+ "typescript": "^7.0.2"
30
30
  }
31
31
  }
@@ -164,7 +164,7 @@ app.get(
164
164
  const book = await store.find(params.id);
165
165
  if (!book) throw new NotFoundError(`Book ${params.id} not found`);
166
166
  return { status: 200 as const, body: book };
167
- },
167
+ }
168
168
  );
169
169
  ```
170
170
 
@@ -217,6 +217,9 @@ Cover **happy paths and unhappy paths** for every route: valid input,
217
217
  validation failures (400), auth failures (401/403), not-found (404),
218
218
  conflict (409), rate limiting (429). For external services, inject an
219
219
  in-memory fake during tests.
220
+ For user-owned or tenant-owned resources, use at least two principals and
221
+ prove that Alice's valid token cannot list, read, update, or delete Bob's
222
+ record.
220
223
  The shipped contract test should fail invalid examples, duplicate/missing
221
224
  `operationId`, or missing responses.
222
225
 
@@ -237,6 +240,17 @@ Aim for complete happy- and unhappy-path test coverage of the routes you add.
237
240
  - For auth, verify JWT signatures with the Web Crypto API
238
241
  (`crypto.subtle`, available on both Node.js and Edge). Never trust the
239
242
  `alg` header from the token.
243
+ - Authentication and scopes are not resource authorization. For every route
244
+ that accepts a resource id, classify the resource as public, user-owned,
245
+ tenant-owned, shared, or administrator-only.
246
+ - Scope user-owned and tenant-owned database reads and writes with both the
247
+ caller-controlled id and the trusted owner / tenant from the verified
248
+ principal. Do not fetch by id alone and rely on the UI or a later caller to
249
+ remember the ownership check.
250
+ - Never accept `ownerId`, `userId`, `tenantId`, `role`, or another privileged
251
+ ownership field from an ordinary request body. Derive it from the verified
252
+ principal and reject the field with a strict request schema.
253
+ - Use an explicit, permissioned, audited path for administrator bypasses.
240
254
  - Validate redirects against an allowlist.
241
255
  - Set `bodyLimitBytes` and `requestTimeoutMs` on `new App({...})` to
242
256
  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.4",
19
+ "@daloyjs/core": "^1.0.0-rc.5",
20
20
  "zod": "^4.4.3"
21
21
  },
22
22
  "devDependencies": {
23
23
  "@types/node": "^26.1.0",
24
- "typescript": "^6.0.3",
24
+ "typescript": "^7.0.2",
25
25
  "vercel": "^48.0.0"
26
26
  }
27
27
  }