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.
- package/bin/create-daloy.mjs +176 -11
- package/package.json +1 -1
- package/sbom.cdx.json +9 -9
- package/sbom.spdx.json +5 -5
- package/templates/bun-basic/_agents/skills/daloyjs-best-practices/SKILL.md +15 -1
- package/templates/bun-basic/package.json +3 -3
- package/templates/cloudflare-worker/_agents/skills/daloyjs-best-practices/SKILL.md +15 -1
- package/templates/cloudflare-worker/package.json +2 -2
- package/templates/deno-basic/_agents/skills/daloyjs-best-practices/SKILL.md +15 -1
- package/templates/deno-basic/deno.json +5 -5
- package/templates/node-basic/_agents/skills/daloyjs-best-practices/SKILL.md +15 -1
- package/templates/node-basic/package.json +3 -3
- package/templates/vercel/_agents/skills/daloyjs-best-practices/SKILL.md +15 -1
- package/templates/vercel/package.json +2 -2
package/bin/create-daloy.mjs
CHANGED
|
@@ -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`
|
|
62
|
-
//
|
|
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
|
-
//
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
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 {
|
|
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
|
+
"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:
|
|
4
|
+
"serialNumber": "urn:uuid:02713320-c651-582f-b203-48552fa27a2e",
|
|
5
5
|
"version": 1,
|
|
6
6
|
"metadata": {
|
|
7
|
-
"timestamp": "2026-07-
|
|
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.
|
|
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.
|
|
18
|
+
"bom-ref": "pkg:npm/create-daloy@1.0.0-rc.5",
|
|
19
19
|
"name": "create-daloy",
|
|
20
|
-
"version": "1.0.0-rc.
|
|
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.
|
|
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.
|
|
45
|
+
"tagId": "swidtag-create-daloy-1.0.0-rc.5",
|
|
46
46
|
"name": "create-daloy",
|
|
47
|
-
"version": "1.0.0-rc.
|
|
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.
|
|
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.
|
|
6
|
-
"documentNamespace": "https://github.com/daloyjs/daloy/sbom/create-daloy-1.0.0-rc.
|
|
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-
|
|
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.
|
|
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.
|
|
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.
|
|
22
|
+
"@daloyjs/core": "^1.0.0-rc.5",
|
|
23
23
|
"zod": "^4.4.3"
|
|
24
24
|
},
|
|
25
25
|
"devDependencies": {
|
|
26
|
-
"@hey-api/openapi-ts": "
|
|
26
|
+
"@hey-api/openapi-ts": "0.0.0-next-20260711024907",
|
|
27
27
|
"@types/bun": "^1.1.0",
|
|
28
|
-
"typescript": "^
|
|
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.
|
|
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": "^
|
|
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.
|
|
14
|
-
"@daloyjs/core/banner": "jsr:@daloyjs/daloy@^1.0.0-rc.
|
|
15
|
-
"@daloyjs/core/contract": "jsr:@daloyjs/daloy@^1.0.0-rc.
|
|
16
|
-
"@daloyjs/core/deno": "jsr:@daloyjs/daloy@^1.0.0-rc.
|
|
17
|
-
"@daloyjs/core/openapi": "jsr:@daloyjs/daloy@^1.0.0-rc.
|
|
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.
|
|
23
|
+
"@daloyjs/core": "^1.0.0-rc.5",
|
|
24
24
|
"zod": "^4.4.3"
|
|
25
25
|
},
|
|
26
26
|
"devDependencies": {
|
|
27
|
-
"@hey-api/openapi-ts": "
|
|
27
|
+
"@hey-api/openapi-ts": "0.0.0-next-20260711024907",
|
|
28
28
|
"@types/node": "^26.1.0",
|
|
29
|
-
"typescript": "^
|
|
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.
|
|
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": "^
|
|
24
|
+
"typescript": "^7.0.2",
|
|
25
25
|
"vercel": "^48.0.0"
|
|
26
26
|
}
|
|
27
27
|
}
|