@warlock.js/core 5.35.0 → 5.37.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (133) hide show
  1. package/CHANGELOG.md +53 -0
  2. package/esm/cli/commands/migrate.command.mjs +10 -0
  3. package/esm/cli/commands/migrate.command.mjs.map +1 -1
  4. package/esm/cli/commands/release/release-command.action.mjs +245 -0
  5. package/esm/cli/commands/release/release-command.action.mjs.map +1 -0
  6. package/esm/cli/commands/release.command.mjs +66 -0
  7. package/esm/cli/commands/release.command.mjs.map +1 -0
  8. package/esm/cli/framework-cli-commands.mjs +2 -0
  9. package/esm/cli/framework-cli-commands.mjs.map +1 -1
  10. package/esm/database/migrate-action.mjs +24 -2
  11. package/esm/database/migrate-action.mjs.map +1 -1
  12. package/esm/dev-server/loader/load-hook.d.mts.map +1 -1
  13. package/esm/dev-server/loader/load-hook.mjs +6 -0
  14. package/esm/dev-server/loader/load-hook.mjs.map +1 -1
  15. package/esm/dev-server/module-loader.mjs +1 -1
  16. package/esm/dev-server/parse-imports.mjs +1 -1
  17. package/esm/dev-server/type-generator.mjs +1 -1
  18. package/esm/e2e/cdp.d.mts.map +1 -1
  19. package/esm/e2e/cdp.mjs +9 -2
  20. package/esm/e2e/cdp.mjs.map +1 -1
  21. package/esm/e2e/page.d.mts +9 -1
  22. package/esm/e2e/page.d.mts.map +1 -1
  23. package/esm/e2e/page.mjs +45 -7
  24. package/esm/e2e/page.mjs.map +1 -1
  25. package/esm/e2e/types.d.mts +18 -0
  26. package/esm/e2e/types.d.mts.map +1 -1
  27. package/esm/e2e/types.mjs.map +1 -1
  28. package/esm/errors/error-codes.d.mts +24 -0
  29. package/esm/errors/error-codes.d.mts.map +1 -1
  30. package/esm/errors/error-codes.mjs +24 -0
  31. package/esm/errors/error-codes.mjs.map +1 -1
  32. package/esm/generations/features/test.feature.mjs +7 -7
  33. package/esm/generations/features/test.feature.mjs.map +1 -1
  34. package/esm/generations/features/web.feature.mjs +3 -3
  35. package/esm/generations/features/web.feature.mjs.map +1 -1
  36. package/esm/index.d.mts +6 -3
  37. package/esm/index.mjs +4 -2
  38. package/esm/localization/index.mjs +3 -1
  39. package/esm/localization/translation-usage-graph.d.mts +49 -0
  40. package/esm/localization/translation-usage-graph.d.mts.map +1 -0
  41. package/esm/localization/translation-usage-graph.mjs +128 -0
  42. package/esm/localization/translation-usage-graph.mjs.map +1 -0
  43. package/esm/localization/translation-usage.d.mts +85 -0
  44. package/esm/localization/translation-usage.d.mts.map +1 -0
  45. package/esm/localization/translation-usage.mjs +307 -0
  46. package/esm/localization/translation-usage.mjs.map +1 -0
  47. package/esm/production/build-budgets.d.mts +18 -0
  48. package/esm/production/build-budgets.d.mts.map +1 -1
  49. package/esm/production/build-budgets.mjs.map +1 -1
  50. package/esm/production/route-registration-snapshot.mjs +12 -3
  51. package/esm/production/route-registration-snapshot.mjs.map +1 -1
  52. package/esm/release/activate.mjs +261 -0
  53. package/esm/release/activate.mjs.map +1 -0
  54. package/esm/release/candidate.mjs +107 -0
  55. package/esm/release/candidate.mjs.map +1 -0
  56. package/esm/release/cascade-migration-provider.mjs +32 -0
  57. package/esm/release/cascade-migration-provider.mjs.map +1 -0
  58. package/esm/release/config.mjs +60 -0
  59. package/esm/release/config.mjs.map +1 -0
  60. package/esm/release/gate.mjs +67 -0
  61. package/esm/release/gate.mjs.map +1 -0
  62. package/esm/release/migrations-proof.mjs +100 -0
  63. package/esm/release/migrations-proof.mjs.map +1 -0
  64. package/esm/release/prune.mjs +42 -0
  65. package/esm/release/prune.mjs.map +1 -0
  66. package/esm/release/report.mjs +25 -0
  67. package/esm/release/report.mjs.map +1 -0
  68. package/esm/release/runner.mjs +55 -0
  69. package/esm/release/runner.mjs.map +1 -0
  70. package/esm/release/semver.mjs +53 -0
  71. package/esm/release/semver.mjs.map +1 -0
  72. package/esm/release/stage.mjs +122 -0
  73. package/esm/release/stage.mjs.map +1 -0
  74. package/esm/release/store.mjs +131 -0
  75. package/esm/release/store.mjs.map +1 -0
  76. package/esm/release/types.d.mts +102 -0
  77. package/esm/release/types.d.mts.map +1 -0
  78. package/esm/release/verify.mjs +94 -0
  79. package/esm/release/verify.mjs.map +1 -0
  80. package/esm/router/index.d.mts +2 -2
  81. package/esm/router/index.mjs +2 -2
  82. package/esm/router/router.d.mts +4 -0
  83. package/esm/router/router.d.mts.map +1 -1
  84. package/esm/router/router.mjs +14 -3
  85. package/esm/router/router.mjs.map +1 -1
  86. package/esm/router/types.d.mts +45 -0
  87. package/esm/router/types.d.mts.map +1 -1
  88. package/esm/router/unguarded-route-diagnostics.d.mts +6 -0
  89. package/esm/router/unguarded-route-diagnostics.d.mts.map +1 -1
  90. package/esm/router/unguarded-route-diagnostics.mjs +10 -2
  91. package/esm/router/unguarded-route-diagnostics.mjs.map +1 -1
  92. package/esm/tooling/check/api-route-references.d.mts +16 -0
  93. package/esm/tooling/check/api-route-references.d.mts.map +1 -0
  94. package/esm/tooling/check/api-route-references.mjs +146 -0
  95. package/esm/tooling/check/api-route-references.mjs.map +1 -0
  96. package/esm/tooling/check/conventions-step.d.mts +6 -3
  97. package/esm/tooling/check/conventions-step.d.mts.map +1 -1
  98. package/esm/tooling/check/conventions-step.mjs +19 -10
  99. package/esm/tooling/check/conventions-step.mjs.map +1 -1
  100. package/esm/tooling/check/run-check.d.mts.map +1 -1
  101. package/esm/tooling/check/run-check.mjs +4 -2
  102. package/esm/tooling/check/run-check.mjs.map +1 -1
  103. package/esm/tooling/check/security-conventions.mjs +19 -1
  104. package/esm/tooling/check/security-conventions.mjs.map +1 -1
  105. package/esm/tooling/check/translation-conventions.mjs +32 -0
  106. package/esm/tooling/check/translation-conventions.mjs.map +1 -0
  107. package/esm/tooling/inspect/collect-files.mjs +1 -1
  108. package/esm/tooling/inspect/collect-routes.mjs +3 -2
  109. package/esm/tooling/inspect/collect-routes.mjs.map +1 -1
  110. package/esm/upgrade/codemods/index.mjs +2 -0
  111. package/esm/upgrade/codemods/index.mjs.map +1 -1
  112. package/esm/upgrade/codemods/vite-vitest.mjs +170 -0
  113. package/esm/upgrade/codemods/vite-vitest.mjs.map +1 -0
  114. package/esm/upgrade/notes.mjs +4 -0
  115. package/esm/upgrade/notes.mjs.map +1 -1
  116. package/esm/validation/init.mjs +11 -4
  117. package/esm/validation/init.mjs.map +1 -1
  118. package/esm/validation/translate-rule.mjs +63 -0
  119. package/esm/validation/translate-rule.mjs.map +1 -0
  120. package/esm/warlock-config/index.d.mts +3 -2
  121. package/esm/warlock-config/types.d.mts +8 -0
  122. package/esm/warlock-config/types.d.mts.map +1 -1
  123. package/llms-full.txt +221 -18
  124. package/llms.txt +1 -0
  125. package/package.json +13 -13
  126. package/skills/deploy-release/SKILL.md +76 -0
  127. package/skills/index.md +1 -1
  128. package/skills/register-route/SKILL.md +25 -0
  129. package/skills/test-service/SKILL.md +8 -8
  130. package/skills/upgrade-from-5-29/SKILL.md +91 -0
  131. package/skills/use-localization/SKILL.md +4 -0
  132. package/skills/warlock-check/SKILL.md +11 -8
  133. package/skills/warlock-conventions/SKILL.md +1 -1
@@ -1 +1 @@
1
- {"version":3,"file":"run-check.mjs","names":[],"sources":["../../../../../../../../../packages/core/src/tooling/check/run-check.ts"],"sourcesContent":["import { affectedFiles } from \"./affected-files\";\nimport { applyConventionFixes } from \"./fixes\";\nimport path from \"node:path\";\nimport { createProblem, type ToolingProblem } from \"../problem\";\nimport {\n runTypecheck,\n type TypecheckOptions,\n type TypecheckResult,\n} from \"../typecheck/run-typecheck\";\nimport { type GitRunner, listChangedFiles } from \"./changed-files\";\nimport {\n CHECK_STEPS,\n type CheckReport,\n type CheckStep,\n type CheckStepName,\n type CheckStepStatus,\n checkExitCode,\n} from \"./check-report.types\";\nimport { runConventions } from \"./conventions-step\";\nimport { checkNodeVersion } from \"./environment-step\";\nimport { runLint } from \"./lint-step\";\nimport { listSourceFiles, listTestFiles } from \"./source-files\";\nimport {\n findVitest,\n parseVitestReport,\n runVitest,\n type VitestRunner,\n vitestArguments,\n vitestRunFailure,\n} from \"./tests-step\";\n\n/** Runs the typecheck step. The CLI swaps in a child process so a heap overflow cannot end `check`. */\nexport type TypecheckRunner = (\n options: TypecheckOptions,\n) => Promise<TypecheckResult> | TypecheckResult;\n\n/** Inputs of {@link runCheck}. */\nexport type CheckOptions = {\n /** Project root. Default: `process.cwd()`. */\n cwd?: string;\n /** Oxlint is the only supported linter. */\n lint?: \"oxlint\";\n /** Limit every step to the files changed against `base` (default HEAD) plus untracked files. */\n changed?: boolean;\n /** Apply safe convention fixes before checking. */\n fix?: boolean;\n /** Compare with `git merge-base <base> HEAD` instead of HEAD. Implies `changed`. */\n base?: string;\n /** Steps not to run. */\n skip?: readonly CheckStepName[];\n /** tsconfig path, relative to `cwd`. Default: `tsconfig.json`. */\n project?: string;\n /** How git is run. */\n git?: GitRunner;\n /** How the typecheck runs. Default: in this process. */\n typecheck?: TypecheckRunner;\n /** How vitest runs. */\n vitest?: VitestRunner;\n /** Where the app's vitest CLI is. Default: resolved from the app root. */\n vitestBinary?: string | null;\n /** The Node.js version to check. Default: the running one. */\n nodeVersion?: string;\n};\n\nfunction errorMessage(error: unknown): string {\n return error instanceof Error ? error.message : String(error);\n}\n\nfunction statusOf(problems: readonly ToolingProblem[]): CheckStepStatus {\n const errors = problems.filter((problem) => problem.severity === \"error\");\n\n if (errors.some((problem) => problem.category === \"environment\")) return \"blocked\";\n\n return errors.length > 0 ? \"failed\" : \"passed\";\n}\n\n/** Bundles one step's timing, status and problems. */\ntype StepOutcome = { status?: CheckStepStatus; detail: string | null; problems: ToolingProblem[] };\n\n/**\n * The single verify command: typecheck, lint, the tests affected by the\n * change, and the framework's convention checks, as one report with one entry\n * per problem and an exit code that tells \"your code\" (1) from \"this machine\"\n * (2).\n *\n * Every step runs even when an earlier one failed, so one run lists every\n * problem. The only exception: with `changed`, a git failure leaves no file\n * list, and every step after `environment` is `blocked`.\n */\nexport async function runCheck(options: CheckOptions = {}): Promise<CheckReport> {\n const selection: unknown = options.lint;\n if (selection !== undefined && selection !== \"oxlint\")\n throw new Error(\n \"ESLint support was removed in 5.34.0. Oxlint/Oxfmt is the default; run npx warlock upgrade --from 5.33 --to 5.34.0.\",\n );\n const cwd = path.resolve(options.cwd ?? process.cwd());\n const changed = options.changed === true || options.base !== undefined;\n const skip = new Set(options.skip ?? []);\n const steps: CheckStep[] = [];\n const problems: ToolingProblem[] = [];\n let files: string[] | null = null;\n let filesBlocked = false;\n\n const run = async (name: CheckStepName, body: () => Promise<StepOutcome> | StepOutcome) => {\n const startedAt = Date.now();\n\n if (skip.has(name)) {\n steps.push({ name, status: \"skipped\", durationMs: 0, detail: \"skipped with --skip\" });\n return;\n }\n\n if (filesBlocked && name !== \"environment\") {\n steps.push({\n name,\n status: \"blocked\",\n durationMs: 0,\n detail: \"the changed files could not be listed\",\n });\n return;\n }\n\n let outcome: StepOutcome;\n\n try {\n outcome = await body();\n } catch (error) {\n outcome = {\n detail: null,\n problems: [\n createProblem({\n code: \"WL8007\",\n source: \"environment\",\n message: `The ${name} step crashed: ${errorMessage(error)}`,\n }),\n ],\n };\n }\n\n problems.push(...outcome.problems);\n steps.push({\n name,\n status: outcome.status ?? statusOf(outcome.problems),\n durationMs: Date.now() - startedAt,\n detail: outcome.detail,\n });\n };\n\n await run(\"environment\", async () => {\n const found = checkNodeVersion(cwd, options.nodeVersion);\n\n if (changed) {\n const listed = await listChangedFiles({\n cwd,\n base: options.base,\n git: options.git,\n includeDeleted: true,\n });\n\n if (\"problem\" in listed) {\n filesBlocked = true;\n found.push(listed.problem);\n } else {\n filesBlocked = true;\n files = await affectedFiles(cwd, listed.files, options.project);\n filesBlocked = false;\n }\n }\n\n return {\n detail: `Node ${options.nodeVersion ?? process.versions.node}${files ? `, ${files.length} changed file(s)` : \"\"}`,\n problems: found,\n };\n });\n\n const targets = async (): Promise<string[]> => files ?? (await listSourceFiles(cwd));\n\n if (options.fix && !filesBlocked && !skip.has(\"conventions\")) {\n try {\n applyConventionFixes(cwd, runConventions(cwd, await targets(), options.project));\n } catch (error) {\n problems.push(\n createProblem({\n code: \"WL8007\",\n source: \"environment\",\n message: `Could not apply fixes: ${errorMessage(error)}`,\n }),\n );\n }\n }\n\n await run(\"typecheck\", async () => {\n if (files !== null && files.length === 0) {\n return { status: \"skipped\", detail: \"no changed files\", problems: [] };\n }\n\n const typecheck = options.typecheck ?? runTypecheck;\n const result = await typecheck({\n cwd,\n project: options.project,\n ...(files !== null ? { files } : {}),\n });\n\n return {\n detail: `${result.checkedFiles} file(s) checked in ${result.durationMs}ms`,\n problems: result.problems,\n };\n });\n\n await run(\"lint\", async () => {\n const lintFiles = await targets();\n\n if (lintFiles.length === 0) {\n return { status: \"skipped\", detail: \"no files to lint\", problems: [] };\n }\n\n const { linters, problems: found } = await runLint(cwd, lintFiles);\n\n if (linters.length === 0) {\n return {\n status: \"skipped\",\n detail: \"no Oxlint config\",\n problems: [],\n };\n }\n\n return { detail: `${linters.join(\" + \")} over ${lintFiles.length} file(s)`, problems: found };\n });\n\n await run(\"tests\", async () => {\n if (files !== null && files.length === 0) {\n return { status: \"skipped\", detail: \"no changed files\", problems: [] };\n }\n\n const binary = options.vitestBinary === undefined ? findVitest(cwd) : options.vitestBinary;\n\n if (!binary) {\n if ((await listTestFiles(cwd)).length === 0) {\n return { status: \"skipped\", detail: \"no test files\", problems: [] };\n }\n\n return {\n detail: null,\n problems: [\n createProblem({\n code: \"WL8002\",\n source: \"environment\",\n message: \"The project has test files but vitest is not installed in the app.\",\n }),\n ],\n };\n }\n\n const vitest = options.vitest ?? runVitest;\n const outcome = await vitest(binary, vitestArguments(files), cwd);\n const found =\n outcome.report === undefined\n ? vitestRunFailure(outcome)\n : parseVitestReport(outcome.report, cwd);\n const total =\n typeof outcome.report === \"object\" && outcome.report !== null\n ? Reflect.get(outcome.report, \"numTotalTests\")\n : undefined;\n\n return {\n detail: typeof total === \"number\" ? `${total} test(s) ran` : null,\n problems: found,\n };\n });\n\n await run(\"conventions\", async () => {\n const conventionFiles = await targets();\n\n return {\n detail: `${conventionFiles.length} file(s) checked`,\n problems: runConventions(cwd, conventionFiles, options.project),\n };\n });\n\n // Keep the declared order even if a future step is added out of sequence.\n steps.sort((first, second) => CHECK_STEPS.indexOf(first.name) - CHECK_STEPS.indexOf(second.name));\n\n const exitCode = checkExitCode(problems);\n const errors = problems.filter((problem) => problem.severity === \"error\");\n\n return {\n schemaVersion: 1,\n ok: exitCode === 0,\n exitCode,\n changed,\n files,\n steps,\n problems,\n summary: {\n errors: errors.length,\n warnings: problems.length - errors.length,\n code: errors.filter((problem) => problem.category === \"code\").length,\n environment: errors.filter((problem) => problem.category === \"environment\").length,\n },\n };\n}\n"],"mappings":";;;;;;;;;;;;;;AAgEA,SAAS,aAAa,OAAwB;CAC5C,OAAO,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;AAC9D;AAEA,SAAS,SAAS,UAAsD;CACtE,MAAM,SAAS,SAAS,QAAQ,YAAY,QAAQ,aAAa,OAAO;CAExE,IAAI,OAAO,MAAM,YAAY,QAAQ,aAAa,aAAa,GAAG,OAAO;CAEzE,OAAO,OAAO,SAAS,IAAI,WAAW;AACxC;;;;;;;;;;;AAeA,eAAsB,SAAS,UAAwB,CAAC,GAAyB;CAC/E,MAAM,YAAqB,QAAQ;CACnC,IAAI,cAAc,UAAa,cAAc,UAC3C,MAAM,IAAI,MACR,qHACF;CACF,MAAM,MAAM,KAAK,QAAQ,QAAQ,OAAO,QAAQ,IAAI,CAAC;CACrD,MAAM,UAAU,QAAQ,YAAY,QAAQ,QAAQ,SAAS;CAC7D,MAAM,OAAO,IAAI,IAAI,QAAQ,QAAQ,CAAC,CAAC;CACvC,MAAM,QAAqB,CAAC;CAC5B,MAAM,WAA6B,CAAC;CACpC,IAAI,QAAyB;CAC7B,IAAI,eAAe;CAEnB,MAAM,MAAM,OAAO,MAAqB,SAAmD;EACzF,MAAM,YAAY,KAAK,IAAI;EAE3B,IAAI,KAAK,IAAI,IAAI,GAAG;GAClB,MAAM,KAAK;IAAE;IAAM,QAAQ;IAAW,YAAY;IAAG,QAAQ;GAAsB,CAAC;GACpF;EACF;EAEA,IAAI,gBAAgB,SAAS,eAAe;GAC1C,MAAM,KAAK;IACT;IACA,QAAQ;IACR,YAAY;IACZ,QAAQ;GACV,CAAC;GACD;EACF;EAEA,IAAI;EAEJ,IAAI;GACF,UAAU,MAAM,KAAK;EACvB,SAAS,OAAO;GACd,UAAU;IACR,QAAQ;IACR,UAAU,CACR,cAAc;KACZ,MAAM;KACN,QAAQ;KACR,SAAS,OAAO,KAAK,iBAAiB,aAAa,KAAK;IAC1D,CAAC,CACH;GACF;EACF;EAEA,SAAS,KAAK,GAAG,QAAQ,QAAQ;EACjC,MAAM,KAAK;GACT;GACA,QAAQ,QAAQ,UAAU,SAAS,QAAQ,QAAQ;GACnD,YAAY,KAAK,IAAI,IAAI;GACzB,QAAQ,QAAQ;EAClB,CAAC;CACH;CAEA,MAAM,IAAI,eAAe,YAAY;EACnC,MAAM,QAAQ,iBAAiB,KAAK,QAAQ,WAAW;EAEvD,IAAI,SAAS;GACX,MAAM,SAAS,MAAM,iBAAiB;IACpC;IACA,MAAM,QAAQ;IACd,KAAK,QAAQ;IACb,gBAAgB;GAClB,CAAC;GAED,IAAI,aAAa,QAAQ;IACvB,eAAe;IACf,MAAM,KAAK,OAAO,OAAO;GAC3B,OAAO;IACL,eAAe;IACf,QAAQ,MAAM,cAAc,KAAK,OAAO,OAAO,QAAQ,OAAO;IAC9D,eAAe;GACjB;EACF;EAEA,OAAO;GACL,QAAQ,QAAQ,QAAQ,eAAe,QAAQ,SAAS,OAAO,QAAQ,KAAK,MAAM,OAAO,oBAAoB;GAC7G,UAAU;EACZ;CACF,CAAC;CAED,MAAM,UAAU,YAA+B,SAAU,MAAM,gBAAgB,GAAG;CAElF,IAAI,QAAQ,OAAO,CAAC,gBAAgB,CAAC,KAAK,IAAI,aAAa,GACzD,IAAI;EACF,qBAAqB,KAAK,eAAe,KAAK,MAAM,QAAQ,GAAG,QAAQ,OAAO,CAAC;CACjF,SAAS,OAAO;EACd,SAAS,KACP,cAAc;GACZ,MAAM;GACN,QAAQ;GACR,SAAS,0BAA0B,aAAa,KAAK;EACvD,CAAC,CACH;CACF;CAGF,MAAM,IAAI,aAAa,YAAY;EACjC,IAAI,UAAU,QAAQ,MAAM,WAAW,GACrC,OAAO;GAAE,QAAQ;GAAW,QAAQ;GAAoB,UAAU,CAAC;EAAE;EAIvE,MAAM,SAAS,OADG,QAAQ,aAAa,aACT,CAAC;GAC7B;GACA,SAAS,QAAQ;GACjB,GAAI,UAAU,OAAO,EAAE,MAAM,IAAI,CAAC;EACpC,CAAC;EAED,OAAO;GACL,QAAQ,GAAG,OAAO,aAAa,sBAAsB,OAAO,WAAW;GACvE,UAAU,OAAO;EACnB;CACF,CAAC;CAED,MAAM,IAAI,QAAQ,YAAY;EAC5B,MAAM,YAAY,MAAM,QAAQ;EAEhC,IAAI,UAAU,WAAW,GACvB,OAAO;GAAE,QAAQ;GAAW,QAAQ;GAAoB,UAAU,CAAC;EAAE;EAGvE,MAAM,EAAE,SAAS,UAAU,UAAU,MAAM,QAAQ,KAAK,SAAS;EAEjE,IAAI,QAAQ,WAAW,GACrB,OAAO;GACL,QAAQ;GACR,QAAQ;GACR,UAAU,CAAC;EACb;EAGF,OAAO;GAAE,QAAQ,GAAG,QAAQ,KAAK,KAAK,EAAE,QAAQ,UAAU,OAAO;GAAW,UAAU;EAAM;CAC9F,CAAC;CAED,MAAM,IAAI,SAAS,YAAY;EAC7B,IAAI,UAAU,QAAQ,MAAM,WAAW,GACrC,OAAO;GAAE,QAAQ;GAAW,QAAQ;GAAoB,UAAU,CAAC;EAAE;EAGvE,MAAM,SAAS,QAAQ,iBAAiB,SAAY,WAAW,GAAG,IAAI,QAAQ;EAE9E,IAAI,CAAC,QAAQ;GACX,KAAK,MAAM,cAAc,GAAG,EAAC,CAAE,WAAW,GACxC,OAAO;IAAE,QAAQ;IAAW,QAAQ;IAAiB,UAAU,CAAC;GAAE;GAGpE,OAAO;IACL,QAAQ;IACR,UAAU,CACR,cAAc;KACZ,MAAM;KACN,QAAQ;KACR,SAAS;IACX,CAAC,CACH;GACF;EACF;EAGA,MAAM,UAAU,OADD,QAAQ,UAAU,UACL,CAAC,QAAQ,gBAAgB,KAAK,GAAG,GAAG;EAChE,MAAM,QACJ,QAAQ,WAAW,SACf,iBAAiB,OAAO,IACxB,kBAAkB,QAAQ,QAAQ,GAAG;EAC3C,MAAM,QACJ,OAAO,QAAQ,WAAW,YAAY,QAAQ,WAAW,OACrD,QAAQ,IAAI,QAAQ,QAAQ,eAAe,IAC3C;EAEN,OAAO;GACL,QAAQ,OAAO,UAAU,WAAW,GAAG,MAAM,gBAAgB;GAC7D,UAAU;EACZ;CACF,CAAC;CAED,MAAM,IAAI,eAAe,YAAY;EACnC,MAAM,kBAAkB,MAAM,QAAQ;EAEtC,OAAO;GACL,QAAQ,GAAG,gBAAgB,OAAO;GAClC,UAAU,eAAe,KAAK,iBAAiB,QAAQ,OAAO;EAChE;CACF,CAAC;CAGD,MAAM,MAAM,OAAO,WAAW,YAAY,QAAQ,MAAM,IAAI,IAAI,YAAY,QAAQ,OAAO,IAAI,CAAC;CAEhG,MAAM,WAAW,cAAc,QAAQ;CACvC,MAAM,SAAS,SAAS,QAAQ,YAAY,QAAQ,aAAa,OAAO;CAExE,OAAO;EACL,eAAe;EACf,IAAI,aAAa;EACjB;EACA;EACA;EACA;EACA;EACA,SAAS;GACP,QAAQ,OAAO;GACf,UAAU,SAAS,SAAS,OAAO;GACnC,MAAM,OAAO,QAAQ,YAAY,QAAQ,aAAa,MAAM,CAAC,CAAC;GAC9D,aAAa,OAAO,QAAQ,YAAY,QAAQ,aAAa,aAAa,CAAC,CAAC;EAC9E;CACF;AACF"}
1
+ {"version":3,"file":"run-check.mjs","names":[],"sources":["../../../../../../../../../packages/core/src/tooling/check/run-check.ts"],"sourcesContent":["import { affectedFiles } from \"./affected-files\";\nimport { applyConventionFixes } from \"./fixes\";\nimport path from \"node:path\";\nimport { createProblem, type ToolingProblem } from \"../problem\";\nimport {\n runTypecheck,\n type TypecheckOptions,\n type TypecheckResult,\n} from \"../typecheck/run-typecheck\";\nimport { type GitRunner, listChangedFiles } from \"./changed-files\";\nimport {\n CHECK_STEPS,\n type CheckReport,\n type CheckStep,\n type CheckStepName,\n type CheckStepStatus,\n checkExitCode,\n} from \"./check-report.types\";\nimport { runConventions } from \"./conventions-step\";\nimport { collectApiRouteFacts } from \"./api-route-references\";\nimport { checkNodeVersion } from \"./environment-step\";\nimport { runLint } from \"./lint-step\";\nimport { listSourceFiles, listTestFiles } from \"./source-files\";\nimport {\n findVitest,\n parseVitestReport,\n runVitest,\n type VitestRunner,\n vitestArguments,\n vitestRunFailure,\n} from \"./tests-step\";\n\n/** Runs the typecheck step. The CLI swaps in a child process so a heap overflow cannot end `check`. */\nexport type TypecheckRunner = (\n options: TypecheckOptions,\n) => Promise<TypecheckResult> | TypecheckResult;\n\n/** Inputs of {@link runCheck}. */\nexport type CheckOptions = {\n /** Project root. Default: `process.cwd()`. */\n cwd?: string;\n /** Oxlint is the only supported linter. */\n lint?: \"oxlint\";\n /** Limit every step to the files changed against `base` (default HEAD) plus untracked files. */\n changed?: boolean;\n /** Apply safe convention fixes before checking. */\n fix?: boolean;\n /** Compare with `git merge-base <base> HEAD` instead of HEAD. Implies `changed`. */\n base?: string;\n /** Steps not to run. */\n skip?: readonly CheckStepName[];\n /** tsconfig path, relative to `cwd`. Default: `tsconfig.json`. */\n project?: string;\n /** How git is run. */\n git?: GitRunner;\n /** How the typecheck runs. Default: in this process. */\n typecheck?: TypecheckRunner;\n /** How vitest runs. */\n vitest?: VitestRunner;\n /** Where the app's vitest CLI is. Default: resolved from the app root. */\n vitestBinary?: string | null;\n /** The Node.js version to check. Default: the running one. */\n nodeVersion?: string;\n};\n\nfunction errorMessage(error: unknown): string {\n return error instanceof Error ? error.message : String(error);\n}\n\nfunction statusOf(problems: readonly ToolingProblem[]): CheckStepStatus {\n const errors = problems.filter((problem) => problem.severity === \"error\");\n\n if (errors.some((problem) => problem.category === \"environment\")) return \"blocked\";\n\n return errors.length > 0 ? \"failed\" : \"passed\";\n}\n\n/** Bundles one step's timing, status and problems. */\ntype StepOutcome = { status?: CheckStepStatus; detail: string | null; problems: ToolingProblem[] };\n\n/**\n * The single verify command: typecheck, lint, the tests affected by the\n * change, and the framework's convention checks, as one report with one entry\n * per problem and an exit code that tells \"your code\" (1) from \"this machine\"\n * (2).\n *\n * Every step runs even when an earlier one failed, so one run lists every\n * problem. The only exception: with `changed`, a git failure leaves no file\n * list, and every step after `environment` is `blocked`.\n */\nexport async function runCheck(options: CheckOptions = {}): Promise<CheckReport> {\n const selection: unknown = options.lint;\n if (selection !== undefined && selection !== \"oxlint\")\n throw new Error(\n \"ESLint support was removed in 5.34.0. Oxlint/Oxfmt is the default; run npx warlock upgrade --from 5.33 --to 5.34.0.\",\n );\n const cwd = path.resolve(options.cwd ?? process.cwd());\n const changed = options.changed === true || options.base !== undefined;\n const skip = new Set(options.skip ?? []);\n const steps: CheckStep[] = [];\n const problems: ToolingProblem[] = [];\n let files: string[] | null = null;\n let filesBlocked = false;\n\n const run = async (name: CheckStepName, body: () => Promise<StepOutcome> | StepOutcome) => {\n const startedAt = Date.now();\n\n if (skip.has(name)) {\n steps.push({ name, status: \"skipped\", durationMs: 0, detail: \"skipped with --skip\" });\n return;\n }\n\n if (filesBlocked && name !== \"environment\") {\n steps.push({\n name,\n status: \"blocked\",\n durationMs: 0,\n detail: \"the changed files could not be listed\",\n });\n return;\n }\n\n let outcome: StepOutcome;\n\n try {\n outcome = await body();\n } catch (error) {\n outcome = {\n detail: null,\n problems: [\n createProblem({\n code: \"WL8007\",\n source: \"environment\",\n message: `The ${name} step crashed: ${errorMessage(error)}`,\n }),\n ],\n };\n }\n\n problems.push(...outcome.problems);\n steps.push({\n name,\n status: outcome.status ?? statusOf(outcome.problems),\n durationMs: Date.now() - startedAt,\n detail: outcome.detail,\n });\n };\n\n await run(\"environment\", async () => {\n const found = checkNodeVersion(cwd, options.nodeVersion);\n\n if (changed) {\n const listed = await listChangedFiles({\n cwd,\n base: options.base,\n git: options.git,\n includeDeleted: true,\n });\n\n if (\"problem\" in listed) {\n filesBlocked = true;\n found.push(listed.problem);\n } else {\n filesBlocked = true;\n files = await affectedFiles(cwd, listed.files, options.project);\n filesBlocked = false;\n }\n }\n\n return {\n detail: `Node ${options.nodeVersion ?? process.versions.node}${files ? `, ${files.length} changed file(s)` : \"\"}`,\n problems: found,\n };\n });\n\n const targets = async (): Promise<string[]> => files ?? (await listSourceFiles(cwd));\n\n if (options.fix && !filesBlocked && !skip.has(\"conventions\")) {\n try {\n const fixTargets = await targets();\n applyConventionFixes(\n cwd,\n runConventions(\n cwd,\n fixTargets,\n options.project,\n collectApiRouteFacts(cwd, await listSourceFiles(cwd)),\n ),\n );\n } catch (error) {\n problems.push(\n createProblem({\n code: \"WL8007\",\n source: \"environment\",\n message: `Could not apply fixes: ${errorMessage(error)}`,\n }),\n );\n }\n }\n\n await run(\"typecheck\", async () => {\n if (files !== null && files.length === 0) {\n return { status: \"skipped\", detail: \"no changed files\", problems: [] };\n }\n\n const typecheck = options.typecheck ?? runTypecheck;\n const result = await typecheck({\n cwd,\n project: options.project,\n ...(files !== null ? { files } : {}),\n });\n\n return {\n detail: `${result.checkedFiles} file(s) checked in ${result.durationMs}ms`,\n problems: result.problems,\n };\n });\n\n await run(\"lint\", async () => {\n const lintFiles = await targets();\n\n if (lintFiles.length === 0) {\n return { status: \"skipped\", detail: \"no files to lint\", problems: [] };\n }\n\n const { linters, problems: found } = await runLint(cwd, lintFiles);\n\n if (linters.length === 0) {\n return {\n status: \"skipped\",\n detail: \"no Oxlint config\",\n problems: [],\n };\n }\n\n return { detail: `${linters.join(\" + \")} over ${lintFiles.length} file(s)`, problems: found };\n });\n\n await run(\"tests\", async () => {\n if (files !== null && files.length === 0) {\n return { status: \"skipped\", detail: \"no changed files\", problems: [] };\n }\n\n const binary = options.vitestBinary === undefined ? findVitest(cwd) : options.vitestBinary;\n\n if (!binary) {\n if ((await listTestFiles(cwd)).length === 0) {\n return { status: \"skipped\", detail: \"no test files\", problems: [] };\n }\n\n return {\n detail: null,\n problems: [\n createProblem({\n code: \"WL8002\",\n source: \"environment\",\n message: \"The project has test files but vitest is not installed in the app.\",\n }),\n ],\n };\n }\n\n const vitest = options.vitest ?? runVitest;\n const outcome = await vitest(binary, vitestArguments(files), cwd);\n const found =\n outcome.report === undefined\n ? vitestRunFailure(outcome)\n : parseVitestReport(outcome.report, cwd);\n const total =\n typeof outcome.report === \"object\" && outcome.report !== null\n ? Reflect.get(outcome.report, \"numTotalTests\")\n : undefined;\n\n return {\n detail: typeof total === \"number\" ? `${total} test(s) ran` : null,\n problems: found,\n };\n });\n\n await run(\"conventions\", async () => {\n const conventionFiles = await targets();\n\n return {\n detail: `${conventionFiles.length} file(s) checked`,\n // Facts come from the whole project even when only changed files are checked:\n // a route registered in one file may be referenced from another.\n problems: runConventions(\n cwd,\n conventionFiles,\n options.project,\n collectApiRouteFacts(cwd, await listSourceFiles(cwd)),\n ),\n };\n });\n\n // Keep the declared order even if a future step is added out of sequence.\n steps.sort((first, second) => CHECK_STEPS.indexOf(first.name) - CHECK_STEPS.indexOf(second.name));\n\n const exitCode = checkExitCode(problems);\n const errors = problems.filter((problem) => problem.severity === \"error\");\n\n return {\n schemaVersion: 1,\n ok: exitCode === 0,\n exitCode,\n changed,\n files,\n steps,\n problems,\n summary: {\n errors: errors.length,\n warnings: problems.length - errors.length,\n code: errors.filter((problem) => problem.category === \"code\").length,\n environment: errors.filter((problem) => problem.category === \"environment\").length,\n },\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;AAiEA,SAAS,aAAa,OAAwB;CAC5C,OAAO,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;AAC9D;AAEA,SAAS,SAAS,UAAsD;CACtE,MAAM,SAAS,SAAS,QAAQ,YAAY,QAAQ,aAAa,OAAO;CAExE,IAAI,OAAO,MAAM,YAAY,QAAQ,aAAa,aAAa,GAAG,OAAO;CAEzE,OAAO,OAAO,SAAS,IAAI,WAAW;AACxC;;;;;;;;;;;AAeA,eAAsB,SAAS,UAAwB,CAAC,GAAyB;CAC/E,MAAM,YAAqB,QAAQ;CACnC,IAAI,cAAc,UAAa,cAAc,UAC3C,MAAM,IAAI,MACR,qHACF;CACF,MAAM,MAAM,KAAK,QAAQ,QAAQ,OAAO,QAAQ,IAAI,CAAC;CACrD,MAAM,UAAU,QAAQ,YAAY,QAAQ,QAAQ,SAAS;CAC7D,MAAM,OAAO,IAAI,IAAI,QAAQ,QAAQ,CAAC,CAAC;CACvC,MAAM,QAAqB,CAAC;CAC5B,MAAM,WAA6B,CAAC;CACpC,IAAI,QAAyB;CAC7B,IAAI,eAAe;CAEnB,MAAM,MAAM,OAAO,MAAqB,SAAmD;EACzF,MAAM,YAAY,KAAK,IAAI;EAE3B,IAAI,KAAK,IAAI,IAAI,GAAG;GAClB,MAAM,KAAK;IAAE;IAAM,QAAQ;IAAW,YAAY;IAAG,QAAQ;GAAsB,CAAC;GACpF;EACF;EAEA,IAAI,gBAAgB,SAAS,eAAe;GAC1C,MAAM,KAAK;IACT;IACA,QAAQ;IACR,YAAY;IACZ,QAAQ;GACV,CAAC;GACD;EACF;EAEA,IAAI;EAEJ,IAAI;GACF,UAAU,MAAM,KAAK;EACvB,SAAS,OAAO;GACd,UAAU;IACR,QAAQ;IACR,UAAU,CACR,cAAc;KACZ,MAAM;KACN,QAAQ;KACR,SAAS,OAAO,KAAK,iBAAiB,aAAa,KAAK;IAC1D,CAAC,CACH;GACF;EACF;EAEA,SAAS,KAAK,GAAG,QAAQ,QAAQ;EACjC,MAAM,KAAK;GACT;GACA,QAAQ,QAAQ,UAAU,SAAS,QAAQ,QAAQ;GACnD,YAAY,KAAK,IAAI,IAAI;GACzB,QAAQ,QAAQ;EAClB,CAAC;CACH;CAEA,MAAM,IAAI,eAAe,YAAY;EACnC,MAAM,QAAQ,iBAAiB,KAAK,QAAQ,WAAW;EAEvD,IAAI,SAAS;GACX,MAAM,SAAS,MAAM,iBAAiB;IACpC;IACA,MAAM,QAAQ;IACd,KAAK,QAAQ;IACb,gBAAgB;GAClB,CAAC;GAED,IAAI,aAAa,QAAQ;IACvB,eAAe;IACf,MAAM,KAAK,OAAO,OAAO;GAC3B,OAAO;IACL,eAAe;IACf,QAAQ,MAAM,cAAc,KAAK,OAAO,OAAO,QAAQ,OAAO;IAC9D,eAAe;GACjB;EACF;EAEA,OAAO;GACL,QAAQ,QAAQ,QAAQ,eAAe,QAAQ,SAAS,OAAO,QAAQ,KAAK,MAAM,OAAO,oBAAoB;GAC7G,UAAU;EACZ;CACF,CAAC;CAED,MAAM,UAAU,YAA+B,SAAU,MAAM,gBAAgB,GAAG;CAElF,IAAI,QAAQ,OAAO,CAAC,gBAAgB,CAAC,KAAK,IAAI,aAAa,GACzD,IAAI;EACF,MAAM,aAAa,MAAM,QAAQ;EACjC,qBACE,KACA,eACE,KACA,YACA,QAAQ,SACR,qBAAqB,KAAK,MAAM,gBAAgB,GAAG,CAAC,CACtD,CACF;CACF,SAAS,OAAO;EACd,SAAS,KACP,cAAc;GACZ,MAAM;GACN,QAAQ;GACR,SAAS,0BAA0B,aAAa,KAAK;EACvD,CAAC,CACH;CACF;CAGF,MAAM,IAAI,aAAa,YAAY;EACjC,IAAI,UAAU,QAAQ,MAAM,WAAW,GACrC,OAAO;GAAE,QAAQ;GAAW,QAAQ;GAAoB,UAAU,CAAC;EAAE;EAIvE,MAAM,SAAS,OADG,QAAQ,aAAa,aACT,CAAC;GAC7B;GACA,SAAS,QAAQ;GACjB,GAAI,UAAU,OAAO,EAAE,MAAM,IAAI,CAAC;EACpC,CAAC;EAED,OAAO;GACL,QAAQ,GAAG,OAAO,aAAa,sBAAsB,OAAO,WAAW;GACvE,UAAU,OAAO;EACnB;CACF,CAAC;CAED,MAAM,IAAI,QAAQ,YAAY;EAC5B,MAAM,YAAY,MAAM,QAAQ;EAEhC,IAAI,UAAU,WAAW,GACvB,OAAO;GAAE,QAAQ;GAAW,QAAQ;GAAoB,UAAU,CAAC;EAAE;EAGvE,MAAM,EAAE,SAAS,UAAU,UAAU,MAAM,QAAQ,KAAK,SAAS;EAEjE,IAAI,QAAQ,WAAW,GACrB,OAAO;GACL,QAAQ;GACR,QAAQ;GACR,UAAU,CAAC;EACb;EAGF,OAAO;GAAE,QAAQ,GAAG,QAAQ,KAAK,KAAK,EAAE,QAAQ,UAAU,OAAO;GAAW,UAAU;EAAM;CAC9F,CAAC;CAED,MAAM,IAAI,SAAS,YAAY;EAC7B,IAAI,UAAU,QAAQ,MAAM,WAAW,GACrC,OAAO;GAAE,QAAQ;GAAW,QAAQ;GAAoB,UAAU,CAAC;EAAE;EAGvE,MAAM,SAAS,QAAQ,iBAAiB,SAAY,WAAW,GAAG,IAAI,QAAQ;EAE9E,IAAI,CAAC,QAAQ;GACX,KAAK,MAAM,cAAc,GAAG,EAAC,CAAE,WAAW,GACxC,OAAO;IAAE,QAAQ;IAAW,QAAQ;IAAiB,UAAU,CAAC;GAAE;GAGpE,OAAO;IACL,QAAQ;IACR,UAAU,CACR,cAAc;KACZ,MAAM;KACN,QAAQ;KACR,SAAS;IACX,CAAC,CACH;GACF;EACF;EAGA,MAAM,UAAU,OADD,QAAQ,UAAU,UACL,CAAC,QAAQ,gBAAgB,KAAK,GAAG,GAAG;EAChE,MAAM,QACJ,QAAQ,WAAW,SACf,iBAAiB,OAAO,IACxB,kBAAkB,QAAQ,QAAQ,GAAG;EAC3C,MAAM,QACJ,OAAO,QAAQ,WAAW,YAAY,QAAQ,WAAW,OACrD,QAAQ,IAAI,QAAQ,QAAQ,eAAe,IAC3C;EAEN,OAAO;GACL,QAAQ,OAAO,UAAU,WAAW,GAAG,MAAM,gBAAgB;GAC7D,UAAU;EACZ;CACF,CAAC;CAED,MAAM,IAAI,eAAe,YAAY;EACnC,MAAM,kBAAkB,MAAM,QAAQ;EAEtC,OAAO;GACL,QAAQ,GAAG,gBAAgB,OAAO;GAGlC,UAAU,eACR,KACA,iBACA,QAAQ,SACR,qBAAqB,KAAK,MAAM,gBAAgB,GAAG,CAAC,CACtD;EACF;CACF,CAAC;CAGD,MAAM,MAAM,OAAO,WAAW,YAAY,QAAQ,MAAM,IAAI,IAAI,YAAY,QAAQ,OAAO,IAAI,CAAC;CAEhG,MAAM,WAAW,cAAc,QAAQ;CACvC,MAAM,SAAS,SAAS,QAAQ,YAAY,QAAQ,aAAa,OAAO;CAExE,OAAO;EACL,eAAe;EACf,IAAI,aAAa;EACjB;EACA;EACA;EACA;EACA;EACA,SAAS;GACP,QAAQ,OAAO;GACf,UAAU,SAAS,SAAS,OAAO;GACnC,MAAM,OAAO,QAAQ,YAAY,QAAQ,aAAa,MAAM,CAAC,CAAC;GAC9D,aAAa,OAAO,QAAQ,YAAY,QAAQ,aAAa,aAAa,CAAC,CAAC;EAC9E;CACF;AACF"}
@@ -131,6 +131,24 @@ function isMarkedGuard(identifier) {
131
131
  const value = initializer ? unwrap(initializer) : void 0;
132
132
  return value !== void 0 && isCallExpression(value) && calleeName(value) === "markAuthGuard" && !(value.arguments[1]?.type === "Literal" && value.arguments[1].value === false);
133
133
  }
134
+ /**
135
+ * Whether a call such as `requireAdmin()` invokes a local factory
136
+ * (`const requireAdmin = () => markAuthGuard(fn)`) whose body declares the
137
+ * middleware it returns an auth guard. A name that says nothing about guards
138
+ * (`requireAdmin`) still counts once the factory marks itself.
139
+ */
140
+ function isMarkedGuardFactory(call) {
141
+ const callee = unwrap(expressionOf(call));
142
+ if (!isIdentifier(callee)) return false;
143
+ const initializer = resolveInitializer(callee);
144
+ if (!initializer || !isFunctionLike(unwrap(initializer))) return false;
145
+ let marked = false;
146
+ walk(initializer, (node) => {
147
+ if (isCallExpression(node) && calleeName(node) === "markAuthGuard" && !(node.arguments[1]?.type === "Literal" && node.arguments[1].value === false)) marked = true;
148
+ return marked ? false : void 0;
149
+ }, true);
150
+ return marked;
151
+ }
134
152
  function middlewareHasGuard(node) {
135
153
  const value = unwrap(node);
136
154
  if (!isArrayLiteralExpression(value)) return true;
@@ -139,7 +157,7 @@ function middlewareHasGuard(node) {
139
157
  if (isSpreadElement(element)) return true;
140
158
  const item = unwrap(element);
141
159
  if (isIdentifier(item)) return isGuardName(valueText(item)) || isMarkedGuard(item);
142
- if (isCallExpression(item)) return isGuardName(calleeName(item));
160
+ if (isCallExpression(item)) return !(calleeName(item) === "markAuthGuard" && item.arguments[1]?.type === "Literal" && item.arguments[1].value === false) && (isGuardName(calleeName(item)) || isMarkedGuardFactory(item));
143
161
  if (isMemberAccess(item)) return isGuardName(memberName(item));
144
162
  return false;
145
163
  });
@@ -1 +1 @@
1
- {"version":3,"file":"security-conventions.mjs","names":["ast.isIdentifier","ast.valueText","ast.expressionOf","ast.parentOf","ast.isCallExpression","ast.isNewExpression","ast.isSpreadElement","ast.isSpreadAssignment","ast.isObjectLiteralExpression","ast.isArrayLiteralExpression","ast.isPropertyAssignment","ast.isShorthandPropertyAssignment","ast.isAsExpression","ast.isNonNullExpression","ast.isSatisfiesExpression","ast.isTypeAssertionExpression","ast.isParenthesizedExpression","ast.isAwaitExpression","ast.isVariableDeclaration","ast.isArrowFunction","ast.isFunctionExpression","ast.isIfStatement","ast.isConditionalExpression","ast.isJsxAttribute","ast.nameNode","ast.isJsxOpeningElement","ast.isJsxSelfClosingElement","ast.isJsxExpression","ast.isElementAccessExpression","ast.isTemplateExpression","ast.isBinaryExpression","ast.isOmittedExpression","ast.isRegularExpressionLiteral"],"sources":["../../../../../../../../../packages/core/src/tooling/check/security-conventions.ts"],"sourcesContent":["import { DEFAULT_REDACT_KEY_SET, normalizeRedactKey } from \"@warlock.js/logger\";\nimport * as ast from \"../syntax/ast\";\nimport { isAuthGuardName, isMutatingHttpMethod } from \"../../router/route-guard-names\";\nimport {\n containsRequestInput,\n REQUEST_BODY_MEMBERS,\n REQUEST_NAMES,\n requestMemberAccess,\n} from \"./request-input\";\nimport {\n ancestors,\n calleeName,\n isFunctionLike,\n isMemberAccess,\n keyName,\n literalString,\n memberName,\n referencesOf,\n resolveInitializer,\n rootIdentifier,\n type SourceFinding,\n unwrap,\n walk,\n} from \"./source-ast\";\n\n// ── WL9005: request.all() into a query, a service or inApp() ────────────────\n\n/** Request methods that return every input the client sent, validated or not. */\nconst UNFILTERED_METHODS: ReadonlySet<string> = new Set([\n \"all\",\n \"allExceptParams\",\n \"heavy\",\n \"heavyExceptParams\",\n]);\n\n/**\n * Responses and loggers are not the data sinks `WL9005` guards: echoing input\n * back is not mass assignment, and logging it is `WL9008`'s concern.\n */\nconst IGNORED_RECEIVERS: ReadonlySet<string> = new Set([\n \"response\",\n \"reply\",\n \"res\",\n \"console\",\n \"log\",\n \"logger\",\n]);\n\nfunction displayCallee(callee: ast.Node): string {\n const target = unwrap(callee);\n\n if (ast.isIdentifier(target)) return `${ast.valueText(target)}()`;\n\n if (isMemberAccess(target)) {\n const name = memberName(target);\n const object = unwrap(ast.expressionOf(target));\n const owner = ast.isIdentifier(object) ? `${ast.valueText(object)}.` : \"\";\n\n return name ? `${owner}${name}()` : \"a call\";\n }\n\n return \"a call\";\n}\n\nfunction isIgnoredReceiver(callee: ast.Node): boolean {\n const target = unwrap(callee);\n\n if (!isMemberAccess(target)) return false;\n\n const object = unwrap(ast.expressionOf(target));\n\n return ast.isIdentifier(object) && IGNORED_RECEIVERS.has(ast.valueText(object));\n}\n\n/** When `node` flows into a call's arguments, the callee's display name. */\nfunction callReceiving(node: ast.Node): string | undefined {\n let current = node;\n\n for (;;) {\n const parent = ast.parentOf(current);\n\n if (!parent) return undefined;\n\n if (ast.isCallExpression(parent) || ast.isNewExpression(parent)) {\n if (!(parent.arguments ?? []).some((argument) => argument === current)) return undefined;\n if (isIgnoredReceiver(ast.expressionOf(parent))) return undefined;\n\n return displayCallee(ast.expressionOf(parent));\n }\n\n if (\n ast.isSpreadElement(parent) ||\n ast.isSpreadAssignment(parent) ||\n ast.isObjectLiteralExpression(parent) ||\n ast.isArrayLiteralExpression(parent) ||\n (ast.isPropertyAssignment(parent) && parent.value === current) ||\n ast.isShorthandPropertyAssignment(parent) ||\n ast.isAsExpression(parent) ||\n ast.isNonNullExpression(parent) ||\n ast.isSatisfiesExpression(parent) ||\n ast.isTypeAssertionExpression(parent) ||\n ast.isParenthesizedExpression(parent) ||\n ast.isAwaitExpression(parent)\n ) {\n current = parent;\n continue;\n }\n\n return undefined;\n }\n}\n\n/**\n * The call that ends up receiving this value: directly as an argument, spread\n * into an object/array argument, or through a local constant later passed to\n * a call.\n */\nfunction receivingCall(node: ast.Node): string | undefined {\n const direct = callReceiving(node);\n\n if (direct) return direct;\n\n // `const data = request.all(); await Model.create(data);`\n const parent = ast.parentOf(node);\n\n if (\n parent &&\n ast.isVariableDeclaration(parent) &&\n parent.init === node &&\n ast.isIdentifier(parent.id)\n ) {\n for (const reference of referencesOf(parent)) {\n const name = callReceiving(reference);\n if (name) return name;\n }\n }\n\n return undefined;\n}\n\nfunction checkRequestAll(node: ast.Node, findings: SourceFinding[]): void {\n if (!ast.isCallExpression(node)) return;\n\n const callee = unwrap(ast.expressionOf(node));\n\n if (!isMemberAccess(callee)) return;\n\n const object = unwrap(ast.expressionOf(callee));\n\n if (!ast.isIdentifier(object) || !REQUEST_NAMES.has(ast.valueText(object))) return;\n\n const method = memberName(callee);\n\n if (!method || !UNFILTERED_METHODS.has(method)) return;\n\n const target = receivingCall(node);\n\n if (!target) return;\n\n findings.push({\n code: \"WL9005\",\n node,\n message: `\\`${ast.valueText(object)}.${method}()\\` passes every client-sent field to \\`${target}\\`. Use \\`${ast.valueText(object)}.validated()\\` so only the fields the validation schema accepted reach it.`,\n });\n}\n\n// ── WL9006: a mutating route with no auth guard and no public: true ─────────\n\nconst ROUTER_NAMES: ReadonlySet<string> = new Set([\"router\"]);\n\n/** Wrappers whose callback routes are guarded, e.g. `guarded(() => …)`. */\nconst GUARD_FUNCTIONS: ReadonlySet<string> = new Set([\"guarded\"]);\n\n/** `router.post(…)` and friends. */\nconst MUTATING_ROUTER_METHODS: ReadonlySet<string> = new Set([\"post\", \"put\", \"patch\", \"delete\"]);\n\n/** Route builder methods (`router.route(path).create(…)`) that register a mutating route. */\nconst MUTATING_BUILDER_METHODS: ReadonlySet<string> = new Set([\n \"post\",\n \"postOne\",\n \"put\",\n \"updateOne\",\n \"patch\",\n \"patchOne\",\n \"delete\",\n \"deleteOne\",\n \"create\",\n \"update\",\n \"destroy\",\n]);\n\nconst READ_ONLY_RESOURCE_METHODS: ReadonlySet<string> = new Set([\"list\", \"get\", \"show\"]);\n\nfunction isRouter(node: ast.Node): boolean {\n const target = unwrap(node);\n\n return ast.isIdentifier(target) && ROUTER_NAMES.has(ast.valueText(target));\n}\n\nfunction isGuardName(name: string | undefined): boolean {\n return name !== undefined && isAuthGuardName(name);\n}\n\n/** Whether a constant is initialised with `markAuthGuard(fn)` (not `markAuthGuard(fn, false)`). */\nfunction isMarkedGuard(identifier: ast.Identifier): boolean {\n const initializer = resolveInitializer(identifier);\n const value = initializer ? unwrap(initializer) : undefined;\n\n return (\n value !== undefined &&\n ast.isCallExpression(value) &&\n calleeName(value) === \"markAuthGuard\" &&\n !(value.arguments[1]?.type === \"Literal\" && value.arguments[1].value === false)\n );\n}\n\nfunction middlewareHasGuard(node: ast.Node): boolean {\n const value = unwrap(node);\n\n // A variable or a call: the check cannot see into it, so it trusts it.\n if (!ast.isArrayLiteralExpression(value)) return true;\n\n return value.elements.some((element) => {\n if (!element) return false;\n if (ast.isSpreadElement(element)) return true;\n\n const item = unwrap(element);\n\n if (ast.isIdentifier(item)) return isGuardName(ast.valueText(item)) || isMarkedGuard(item);\n if (ast.isCallExpression(item)) return isGuardName(calleeName(item));\n if (isMemberAccess(item)) return isGuardName(memberName(item));\n\n return false;\n });\n}\n\n/** `public: true`, a guard in `middleware`, or an options value the check cannot see into. */\nfunction optionsGuard(node: ast.Node | undefined): \"guarded\" | \"unguarded\" | \"none\" {\n if (!node) return \"none\";\n\n const value = unwrap(node);\n\n if (!ast.isObjectLiteralExpression(value)) return \"guarded\";\n\n let verdict: \"guarded\" | \"unguarded\" = \"unguarded\";\n\n for (const property of value.properties) {\n if (ast.isSpreadAssignment(property)) {\n verdict = \"guarded\";\n continue;\n }\n\n let key: string | undefined;\n let propertyValue: ast.Node | undefined;\n\n if (ast.isPropertyAssignment(property)) {\n key = keyName(property.key);\n propertyValue = unwrap(property.value);\n } else if (ast.isShorthandPropertyAssignment(property)) {\n key = ast.valueText(property.key);\n propertyValue = property.key;\n }\n\n if (!propertyValue) continue;\n if (key === \"public\" && propertyValue.type === \"Literal\" && propertyValue.value === true)\n return \"guarded\";\n if (key === \"middleware\" && middlewareHasGuard(propertyValue)) return \"guarded\";\n }\n\n return verdict;\n}\n\n/** Whether an enclosing function is a guarded (or public) group callback. */\nfunction insideGuardedGroup(node: ast.Node): boolean {\n for (const ancestor of ancestors(node)) {\n if (!ast.isArrowFunction(ancestor) && !ast.isFunctionExpression(ancestor)) continue;\n\n const parent = ast.parentOf(ancestor);\n\n if (!parent || !ast.isCallExpression(parent)) continue;\n if (!parent.arguments.some((argument) => argument === ancestor)) continue;\n\n const name = calleeName(parent);\n\n if (name !== undefined && GUARD_FUNCTIONS.has(name)) return true;\n\n const callee = unwrap(ast.expressionOf(parent));\n\n if (\n isMemberAccess(callee) &&\n memberName(callee) === \"group\" &&\n isRouter(ast.expressionOf(callee)) &&\n optionsGuard(parent.arguments[0]) === \"guarded\"\n ) {\n return true;\n }\n }\n\n return false;\n}\n\nfunction pathText(node: ast.Node | undefined): string {\n return literalString(node) ?? \"(dynamic path)\";\n}\n\n/**\n * Walk a builder chain back to `router.route(path, options)`, collecting every\n * options object passed along the way (`route()`, `nest()`).\n */\nfunction builderChain(node: ast.Node): { path: string; optionNodes: ast.Node[] } | undefined {\n const optionNodes: ast.Node[] = [];\n let current = unwrap(node);\n\n while (ast.isCallExpression(current)) {\n const callee = unwrap(ast.expressionOf(current));\n\n if (!isMemberAccess(callee)) return undefined;\n\n const name = memberName(callee);\n\n if (name === \"route\" && isRouter(ast.expressionOf(callee))) {\n const options = current.arguments[1];\n if (options) optionNodes.push(options);\n\n return { path: pathText(current.arguments[0]), optionNodes };\n }\n\n if (name === \"nest\") {\n const options = current.arguments[1];\n if (options) optionNodes.push(options);\n }\n\n current = unwrap(ast.expressionOf(callee));\n }\n\n return undefined;\n}\n\nfunction resourceIsReadOnly(node: ast.Node | undefined): boolean {\n if (!node) return false;\n\n const value = unwrap(node);\n\n if (!ast.isObjectLiteralExpression(value)) return false;\n\n for (const property of value.properties) {\n if (!ast.isPropertyAssignment(property) || keyName(property.key) !== \"only\") continue;\n\n const only = unwrap(property.value);\n\n if (!ast.isArrayLiteralExpression(only)) return false;\n\n return only.elements.every((element) => {\n if (!element) return false;\n const method = literalString(element);\n return method !== undefined && READ_ONLY_RESOURCE_METHODS.has(method);\n });\n }\n\n return false;\n}\n\nfunction unguardedRoute(node: ast.Node, route: string): SourceFinding {\n return {\n code: \"WL9006\",\n node,\n message: `Mutating route \\`${route}\\` has no auth middleware. Add an auth guard (\\`middleware: [authMiddleware()]\\`, or register it inside a guarded group) or mark it intentionally open with \\`public: true\\`.`,\n };\n}\n\nfunction checkRouteGuard(node: ast.Node, findings: SourceFinding[]): void {\n if (!ast.isCallExpression(node)) return;\n\n const callee = unwrap(ast.expressionOf(node));\n\n if (!isMemberAccess(callee)) return;\n\n const method = memberName(callee);\n\n if (!method) return;\n\n const guarded = (options: ast.Node | undefined) =>\n optionsGuard(options) === \"guarded\" || insideGuardedGroup(node);\n\n if (isRouter(ast.expressionOf(callee))) {\n // router.post(path, handler, options)\n if (MUTATING_ROUTER_METHODS.has(method)) {\n if (!guarded(node.arguments[2])) {\n findings.push(\n unguardedRoute(node, `${method.toUpperCase()} ${pathText(node.arguments[0])}`),\n );\n }\n\n return;\n }\n\n // router.add(\"POST\", path, handler, options)\n if (method === \"add\") {\n const httpMethod = literalString(node.arguments[0])?.toUpperCase();\n\n if (!httpMethod || !isMutatingHttpMethod(httpMethod)) return;\n\n if (!guarded(node.arguments[3])) {\n findings.push(unguardedRoute(node, `${httpMethod} ${pathText(node.arguments[1])}`));\n }\n\n return;\n }\n\n // router.restfulResource(path, resource, options)\n if (method === \"restfulResource\") {\n if (resourceIsReadOnly(node.arguments[2])) return;\n\n if (!guarded(node.arguments[2])) {\n findings.push(unguardedRoute(node, `resource ${pathText(node.arguments[0])}`));\n }\n }\n\n return;\n }\n\n // router.route(path, options).create(handler, options)\n if (!MUTATING_BUILDER_METHODS.has(method)) return;\n\n const chain = builderChain(ast.expressionOf(callee));\n\n if (!chain) return;\n if (optionsGuard(node.arguments[1]) === \"guarded\") return;\n if (chain.optionNodes.some((options) => optionsGuard(options) === \"guarded\")) return;\n if (insideGuardedGroup(node)) return;\n\n findings.push(unguardedRoute(node, `${method} ${chain.path}`));\n}\n\n// ── WL9007: a redirect or link target from request input ────────────────────\n\n/** Calls that turn their first argument into a redirect or a navigation. */\nconst REDIRECT_CALLS: ReadonlySet<string> = new Set([\n \"redirect\",\n \"permanentRedirect\",\n \"navigateTo\",\n]);\n\n/** Components whose `to`/`href` prop navigates. */\nconst LINK_COMPONENTS: ReadonlySet<string> = new Set([\"Link\", \"NavLink\"]);\n\n/** Helpers that return an allowed target (or a fallback) for any input. */\nconst REDIRECT_SANITIZERS: ReadonlySet<string> = new Set([\"safeRedirect\", \"safeRedirectTarget\"]);\n\n/** Predicates that, used in an enclosing `if` or `? :`, prove the target was checked. */\nconst REDIRECT_VALIDATORS: ReadonlySet<string> = new Set([\"isSafeRedirect\", \"isSafeRedirectUrl\"]);\n\n/** Client-side input: query-string hooks. */\nconst CLIENT_INPUT_CALLS: ReadonlySet<string> = new Set([\n \"useQueryString\",\n \"useSearchParams\",\n \"useParams\",\n]);\n\n/** Identifiers whose members are client-controlled (a loader's `query`, URLSearchParams). */\nconst CLIENT_INPUT_NAMES: ReadonlySet<string> = new Set([\"query\", \"searchParams\"]);\n\nfunction isClientSource(node: ast.Node): boolean {\n if (ast.isCallExpression(node)) {\n const name = calleeName(node);\n if (name !== undefined && CLIENT_INPUT_CALLS.has(name)) return true;\n }\n\n if (isMemberAccess(node)) {\n const root = rootIdentifier(node);\n\n if (root && CLIENT_INPUT_NAMES.has(ast.valueText(root))) return true;\n\n // location.search / window.location.hash\n const name = memberName(node);\n\n if (name === \"search\" || name === \"hash\") {\n const object = unwrap(ast.expressionOf(node));\n\n if (ast.isIdentifier(object) && ast.valueText(object) === \"location\") return true;\n if (isMemberAccess(object) && memberName(object) === \"location\") return true;\n }\n }\n\n return false;\n}\n\nfunction isRedirectTainted(node: ast.Node): boolean {\n return containsRequestInput(node, { sanitizers: REDIRECT_SANITIZERS, isSource: isClientSource });\n}\n\n/** An enclosing `if (isSafeRedirect(x))` or `isSafeRedirect(x) ? … : …` validated the target. */\nfunction checkedByValidator(node: ast.Node): boolean {\n for (const ancestor of ancestors(node)) {\n let test: ast.Node | undefined;\n\n if (ast.isIfStatement(ancestor)) test = ast.expressionOf(ancestor);\n else if (ast.isConditionalExpression(ancestor)) test = ancestor.test;\n else continue;\n\n let validated = false;\n\n walk(test, (child) => {\n if (ast.isCallExpression(child)) {\n const name = calleeName(child);\n if (name !== undefined && REDIRECT_VALIDATORS.has(name)) validated = true;\n }\n\n return undefined;\n });\n\n if (validated) return true;\n }\n\n return false;\n}\n\nfunction unsafeRedirect(node: ast.Node, what: string, helper: string, from: string): SourceFinding {\n return {\n code: \"WL9007\",\n node,\n message: `This ${what} target comes from request input. Pass it through \\`${helper}()\\` from \\`${from}\\` so only same-origin paths and allow-listed origins are followed (open redirects and \\`javascript:\\` URLs otherwise).`,\n };\n}\n\nfunction checkRedirect(node: ast.Node, findings: SourceFinding[]): void {\n if (ast.isCallExpression(node)) {\n const name = calleeName(node);\n\n if (name === undefined || !REDIRECT_CALLS.has(name)) return;\n\n // `router.redirect(from, to)` registers a static redirect, not a request-time one.\n const callee = unwrap(ast.expressionOf(node));\n\n if (isMemberAccess(callee)) {\n const owner = unwrap(ast.expressionOf(callee));\n if (ast.isIdentifier(owner) && ast.valueText(owner) === \"router\") return;\n }\n\n const target = node.arguments[0];\n\n if (!target || ast.isSpreadElement(target)) return;\n if (!isRedirectTainted(target) || checkedByValidator(node)) return;\n\n findings.push(\n name === \"navigateTo\"\n ? unsafeRedirect(target, \"navigation\", \"safeRedirectTarget\", \"@warlock.js/web\")\n : unsafeRedirect(target, \"redirect\", \"safeRedirect\", \"@warlock.js/core\"),\n );\n\n return;\n }\n\n if (!ast.isJsxAttribute(node) || ast.nameNode(node).type !== \"JSXIdentifier\") return;\n if (ast.valueText(ast.nameNode(node)) !== \"to\" && ast.valueText(ast.nameNode(node)) !== \"href\")\n return;\n\n const element = ast.parentOf(node);\n\n if (!element || (!ast.isJsxOpeningElement(element) && !ast.isJsxSelfClosingElement(element)))\n return;\n if (element.name.type !== \"JSXIdentifier\" || !LINK_COMPONENTS.has(element.name.name)) return;\n\n const initializer = node.value;\n\n if (!initializer || !ast.isJsxExpression(initializer) || !initializer.expression) return;\n\n const expression = initializer.expression;\n\n if (!isRedirectTainted(expression) || checkedByValidator(expression)) return;\n\n findings.push(unsafeRedirect(expression, \"link\", \"safeRedirectTarget\", \"@warlock.js/web\"));\n}\n\n// ── WL9008: raw emails, credentials and request bodies in logs ──────────────\n\nconst LOGGER_NAMES: ReadonlySet<string> = new Set([\"log\", \"logger\", \"console\"]);\n\nconst LOG_METHODS: ReadonlySet<string> = new Set([\n \"info\",\n \"warn\",\n \"error\",\n \"debug\",\n \"success\",\n \"fatal\",\n \"log\",\n \"assert\",\n \"trace\",\n]);\n\n/** Helpers that make a value safe to log; their arguments are not inspected. */\nconst REDACTORS: ReadonlySet<string> = new Set([\n \"maskEmail\",\n \"maskEmailsInText\",\n \"redact\",\n \"redactHeaders\",\n \"redactError\",\n \"redactUrlCredentials\",\n \"redactQueryCredentials\",\n \"escapeLogValue\",\n \"toLogSnapshot\",\n]);\n\n/** Name endings that mark a value as a credential. */\nconst SECRET_SUFFIXES = [\n \"token\",\n \"password\",\n \"passwd\",\n \"secret\",\n \"apikey\",\n \"authorization\",\n \"cookie\",\n \"cookies\",\n \"otp\",\n \"jwt\",\n \"privatekey\",\n];\n\n/**\n * Whether the logger censors this key by default (`DEFAULT_REDACT_KEYS` from\n * `@warlock.js/logger`). A secret logged under one of them never reaches a\n * channel. Emails are not on that list: they are not censored by default.\n */\nfunction isCensoredKey(key: string): boolean {\n return DEFAULT_REDACT_KEY_SET.has(normalizeRedactKey(key));\n}\n\n/** `email` for addresses, `secret` for credentials, otherwise `undefined`. */\nfunction sensitiveCategory(name: string): \"email\" | \"secret\" | undefined {\n const normalized = normalizeRedactKey(name);\n\n if (normalized.endsWith(\"email\") || normalized.endsWith(\"emailaddress\")) return \"email\";\n if (DEFAULT_REDACT_KEY_SET.has(normalized)) return \"secret\";\n if (SECRET_SUFFIXES.some((suffix) => normalized.endsWith(suffix))) return \"secret\";\n\n return undefined;\n}\n\nfunction describeCategory(category: \"email\" | \"secret\", name: string | undefined): string {\n if (category === \"email\") return \"a raw email address\";\n\n return name ? `the credential \\`${name}\\`` : \"a credential\";\n}\n\nfunction describeMember(member: string): string {\n if (member === \"headers\" || member === \"cookies\") return member;\n\n return `body (\\`${member}\\`)`;\n}\n\n/** `request.all()` / `request.body` itself, not `request.body.title`. */\nfunction isWholeAccess(node: ast.Node, member: string): boolean {\n if (ast.isCallExpression(node)) {\n const callee = unwrap(ast.expressionOf(node));\n return isMemberAccess(callee) && memberName(callee) === member;\n }\n\n return isMemberAccess(node) && memberName(node) === member;\n}\n\ntype LogFinding = { node: ast.Node; what: string };\n\nfunction inspectLogged(node: ast.Node, censoredKey: boolean, found: LogFinding[]): void {\n const value = unwrap(node);\n\n if (isFunctionLike(value)) return;\n\n if (ast.isCallExpression(value)) {\n const name = calleeName(value);\n if (name !== undefined && REDACTORS.has(name)) return;\n }\n\n // request.all() / request.body / request.headers / request.input(\"email\")\n if (ast.isCallExpression(value) || isMemberAccess(value)) {\n const member = requestMemberAccess(value);\n\n if (member !== undefined && isWholeAccess(value, member)) {\n if (REQUEST_BODY_MEMBERS.has(member)) {\n found.push({ node: value, what: `the request ${describeMember(member)}` });\n return;\n }\n\n if (member === \"email\") {\n found.push({ node: value, what: \"a raw email address\" });\n return;\n }\n }\n\n if (ast.isCallExpression(value) && member !== undefined) {\n const key = literalString(value.arguments[0]);\n const category = key ? sensitiveCategory(key) : undefined;\n\n if (category && !(censoredKey && category === \"secret\")) {\n found.push({ node: value, what: describeCategory(category, key) });\n return;\n }\n }\n }\n\n if (ast.isIdentifier(value) || isMemberAccess(value)) {\n const name = ast.isIdentifier(value) ? ast.valueText(value) : memberName(value);\n const category = name ? sensitiveCategory(name) : undefined;\n\n if (category && !(censoredKey && category === \"secret\")) {\n found.push({ node: value, what: describeCategory(category, name) });\n return;\n }\n\n // `request.body.title` reads one field; only the whole body is reported.\n if (isMemberAccess(value) && requestMemberAccess(value) === undefined) {\n inspectLogged(ast.expressionOf(value), false, found);\n\n if (ast.isElementAccessExpression(value)) inspectLogged(value.property, false, found);\n }\n\n return;\n }\n\n if (ast.isObjectLiteralExpression(value)) {\n for (const property of value.properties) {\n if (ast.isSpreadAssignment(property)) {\n inspectLogged(ast.expressionOf(property), false, found);\n } else if (ast.isPropertyAssignment(property)) {\n const key = keyName(property.key);\n inspectLogged(property.value, key !== undefined && isCensoredKey(key), found);\n } else if (ast.isShorthandPropertyAssignment(property)) {\n inspectLogged(property.key, isCensoredKey(ast.valueText(property.key)), found);\n }\n }\n\n return;\n }\n\n if (ast.isTemplateExpression(value)) {\n for (const expression of value.expressions) inspectLogged(expression, false, found);\n return;\n }\n\n if (ast.isBinaryExpression(value)) {\n inspectLogged(value.left, false, found);\n inspectLogged(value.right, false, found);\n return;\n }\n\n if (ast.isConditionalExpression(value)) {\n inspectLogged(value.consequent, censoredKey, found);\n inspectLogged(value.alternate, censoredKey, found);\n return;\n }\n\n if (ast.isArrayLiteralExpression(value)) {\n for (const element of value.elements) {\n if (ast.isOmittedExpression(element)) continue;\n inspectLogged(\n ast.isSpreadElement(element) ? ast.expressionOf(element) : element,\n false,\n found,\n );\n }\n\n return;\n }\n\n if (ast.isCallExpression(value)) {\n // JSON.stringify(request.all()), String(user.email), user.email.trim()\n const callee = unwrap(ast.expressionOf(value));\n\n if (isMemberAccess(callee)) inspectLogged(ast.expressionOf(callee), false, found);\n\n for (const argument of value.arguments) {\n inspectLogged(\n ast.isSpreadElement(argument) ? ast.expressionOf(argument) : argument,\n false,\n found,\n );\n }\n\n return;\n }\n\n if (ast.isAwaitExpression(value)) inspectLogged(ast.expressionOf(value), censoredKey, found);\n}\n\nfunction checkSensitiveLog(node: ast.Node, findings: SourceFinding[]): void {\n if (!ast.isCallExpression(node)) return;\n\n const callee = unwrap(ast.expressionOf(node));\n\n if (!isMemberAccess(callee)) return;\n\n const method = memberName(callee);\n\n if (!method || !LOG_METHODS.has(method)) return;\n\n const owner = unwrap(ast.expressionOf(callee));\n\n if (!ast.isIdentifier(owner)) return;\n\n // log.info(…), console.log(…), and request.log(…)\n const isLogger =\n LOGGER_NAMES.has(ast.valueText(owner)) ||\n (REQUEST_NAMES.has(ast.valueText(owner)) && method === \"log\");\n\n if (!isLogger) return;\n\n const found: LogFinding[] = [];\n\n for (const argument of node.arguments) {\n inspectLogged(\n ast.isSpreadElement(argument) ? ast.expressionOf(argument) : argument,\n false,\n found,\n );\n }\n\n for (const finding of found) {\n findings.push({\n code: \"WL9008\",\n node: finding.node,\n message: `Do not log ${finding.what} as-is. Pass it through a redaction helper (\\`maskEmail()\\`, \\`redact()\\`, \\`request.toLogSnapshot()\\`), or log it under a key the logger censors.`,\n });\n }\n}\n\n// ── WL9009: unbounded regex quantifiers on request input ────────────────────\n\n/** `regex.test(input)` / `regex.exec(input)`. */\nconst REGEX_METHODS: ReadonlySet<string> = new Set([\"test\", \"exec\"]);\n\n/** `input.match(regex)` and friends: the regex is the first argument. */\nconst STRING_METHODS: ReadonlySet<string> = new Set([\n \"match\",\n \"matchAll\",\n \"replace\",\n \"replaceAll\",\n \"search\",\n \"split\",\n]);\n\n/**\n * The first unbounded quantifier in a regex source (`*`, `+`, `{n,}`), skipping\n * escapes and character classes, or `undefined` when every repetition is\n * bounded.\n */\nexport function findUnboundedQuantifier(source: string): string | undefined {\n let inClass = false;\n\n for (let index = 0; index < source.length; index++) {\n const char = source[index];\n\n if (char === \"\\\\\") {\n index++;\n continue;\n }\n\n if (inClass) {\n if (char === \"]\") inClass = false;\n continue;\n }\n\n if (char === \"[\") {\n inClass = true;\n continue;\n }\n\n if (char === \"*\" || char === \"+\") {\n return `${char}${source[index + 1] === \"?\" ? \"?\" : \"\"}`;\n }\n\n if (char === \"{\") {\n const match = /^\\{(\\d+),\\}/.exec(source.slice(index));\n if (match) return match[0];\n }\n }\n\n return undefined;\n}\n\n/** The pattern of a regex literal, `new RegExp(\"…\")`, or a constant holding either. */\nfunction regexSource(node: ast.Node | undefined, depth = 0): string | undefined {\n if (!node || depth > 8) return undefined;\n\n const value = unwrap(node);\n\n if (ast.isRegularExpressionLiteral(value)) {\n return ast.valueText(value).slice(1, ast.valueText(value).lastIndexOf(\"/\"));\n }\n\n if (\n (ast.isNewExpression(value) || ast.isCallExpression(value)) &&\n ast.isIdentifier(ast.expressionOf(value)) &&\n ast.valueText(ast.expressionOf(value)) === \"RegExp\"\n ) {\n return literalString(value.arguments?.[0]);\n }\n\n if (ast.isIdentifier(value)) {\n const initializer = resolveInitializer(value);\n return initializer ? regexSource(initializer, depth + 1) : undefined;\n }\n\n return undefined;\n}\n\nfunction checkUnboundedRegex(node: ast.Node, findings: SourceFinding[]): void {\n if (!ast.isCallExpression(node)) return;\n\n const callee = unwrap(ast.expressionOf(node));\n\n if (!isMemberAccess(callee)) return;\n\n const method = memberName(callee);\n\n if (!method) return;\n\n const first = node.arguments[0];\n const firstArgument = first && !ast.isSpreadElement(first) ? first : undefined;\n let regex: ast.Node | undefined;\n let input: ast.Node | undefined;\n\n if (REGEX_METHODS.has(method)) {\n regex = ast.expressionOf(callee);\n input = firstArgument;\n } else if (STRING_METHODS.has(method)) {\n regex = firstArgument;\n input = ast.expressionOf(callee);\n } else {\n return;\n }\n\n if (!input) return;\n\n const source = regexSource(regex);\n\n if (source === undefined) return;\n\n const quantifier = findUnboundedQuantifier(source);\n\n if (!quantifier || !containsRequestInput(input)) return;\n\n findings.push({\n code: \"WL9009\",\n node,\n message: `This regex has an unbounded quantifier (\\`${quantifier}\\`) and runs on request input, so a crafted input can make it backtrack for a long time (ReDoS). Bound the repetition, e.g. \\`{1,256}\\`, or cap the input length before matching.`,\n });\n}\n\n/**\n * The security conventions in one parsed file:\n *\n * - `WL9005` `request.all()` (or `heavy()`, `allExceptParams()`) passed to a\n * query, a service or `inApp()`;\n * - `WL9006` a POST/PUT/PATCH/DELETE route with no auth middleware and no\n * `public: true`;\n * - `WL9007` a `redirect()` / `navigateTo()` / `<Link to|href>` target taken\n * from request input without `safeRedirect()` / `safeRedirectTarget()` or an\n * `isSafeRedirect()` check;\n * - `WL9008` a raw email, credential or request body passed to a logger;\n * - `WL9009` a regex with an unbounded quantifier run on request input.\n */\nexport function securityFindings(sourceFile: ast.SourceFile): SourceFinding[] {\n const findings: SourceFinding[] = [];\n\n walk(\n sourceFile,\n (node) => {\n checkRequestAll(node, findings);\n checkRouteGuard(node, findings);\n checkRedirect(node, findings);\n checkSensitiveLog(node, findings);\n checkUnboundedRegex(node, findings);\n\n return undefined;\n },\n true,\n );\n\n return findings;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"security-conventions.mjs","names":["ast.isIdentifier","ast.valueText","ast.expressionOf","ast.parentOf","ast.isCallExpression","ast.isNewExpression","ast.isSpreadElement","ast.isSpreadAssignment","ast.isObjectLiteralExpression","ast.isArrayLiteralExpression","ast.isPropertyAssignment","ast.isShorthandPropertyAssignment","ast.isAsExpression","ast.isNonNullExpression","ast.isSatisfiesExpression","ast.isTypeAssertionExpression","ast.isParenthesizedExpression","ast.isAwaitExpression","ast.isVariableDeclaration","ast.isArrowFunction","ast.isFunctionExpression","ast.isIfStatement","ast.isConditionalExpression","ast.isJsxAttribute","ast.nameNode","ast.isJsxOpeningElement","ast.isJsxSelfClosingElement","ast.isJsxExpression","ast.isElementAccessExpression","ast.isTemplateExpression","ast.isBinaryExpression","ast.isOmittedExpression","ast.isRegularExpressionLiteral"],"sources":["../../../../../../../../../packages/core/src/tooling/check/security-conventions.ts"],"sourcesContent":["import { DEFAULT_REDACT_KEY_SET, normalizeRedactKey } from \"@warlock.js/logger\";\nimport * as ast from \"../syntax/ast\";\nimport { isAuthGuardName, isMutatingHttpMethod } from \"../../router/route-guard-names\";\nimport {\n containsRequestInput,\n REQUEST_BODY_MEMBERS,\n REQUEST_NAMES,\n requestMemberAccess,\n} from \"./request-input\";\nimport {\n ancestors,\n calleeName,\n isFunctionLike,\n isMemberAccess,\n keyName,\n literalString,\n memberName,\n referencesOf,\n resolveInitializer,\n rootIdentifier,\n type SourceFinding,\n unwrap,\n walk,\n} from \"./source-ast\";\n\n// ── WL9005: request.all() into a query, a service or inApp() ────────────────\n\n/** Request methods that return every input the client sent, validated or not. */\nconst UNFILTERED_METHODS: ReadonlySet<string> = new Set([\n \"all\",\n \"allExceptParams\",\n \"heavy\",\n \"heavyExceptParams\",\n]);\n\n/**\n * Responses and loggers are not the data sinks `WL9005` guards: echoing input\n * back is not mass assignment, and logging it is `WL9008`'s concern.\n */\nconst IGNORED_RECEIVERS: ReadonlySet<string> = new Set([\n \"response\",\n \"reply\",\n \"res\",\n \"console\",\n \"log\",\n \"logger\",\n]);\n\nfunction displayCallee(callee: ast.Node): string {\n const target = unwrap(callee);\n\n if (ast.isIdentifier(target)) return `${ast.valueText(target)}()`;\n\n if (isMemberAccess(target)) {\n const name = memberName(target);\n const object = unwrap(ast.expressionOf(target));\n const owner = ast.isIdentifier(object) ? `${ast.valueText(object)}.` : \"\";\n\n return name ? `${owner}${name}()` : \"a call\";\n }\n\n return \"a call\";\n}\n\nfunction isIgnoredReceiver(callee: ast.Node): boolean {\n const target = unwrap(callee);\n\n if (!isMemberAccess(target)) return false;\n\n const object = unwrap(ast.expressionOf(target));\n\n return ast.isIdentifier(object) && IGNORED_RECEIVERS.has(ast.valueText(object));\n}\n\n/** When `node` flows into a call's arguments, the callee's display name. */\nfunction callReceiving(node: ast.Node): string | undefined {\n let current = node;\n\n for (;;) {\n const parent = ast.parentOf(current);\n\n if (!parent) return undefined;\n\n if (ast.isCallExpression(parent) || ast.isNewExpression(parent)) {\n if (!(parent.arguments ?? []).some((argument) => argument === current)) return undefined;\n if (isIgnoredReceiver(ast.expressionOf(parent))) return undefined;\n\n return displayCallee(ast.expressionOf(parent));\n }\n\n if (\n ast.isSpreadElement(parent) ||\n ast.isSpreadAssignment(parent) ||\n ast.isObjectLiteralExpression(parent) ||\n ast.isArrayLiteralExpression(parent) ||\n (ast.isPropertyAssignment(parent) && parent.value === current) ||\n ast.isShorthandPropertyAssignment(parent) ||\n ast.isAsExpression(parent) ||\n ast.isNonNullExpression(parent) ||\n ast.isSatisfiesExpression(parent) ||\n ast.isTypeAssertionExpression(parent) ||\n ast.isParenthesizedExpression(parent) ||\n ast.isAwaitExpression(parent)\n ) {\n current = parent;\n continue;\n }\n\n return undefined;\n }\n}\n\n/**\n * The call that ends up receiving this value: directly as an argument, spread\n * into an object/array argument, or through a local constant later passed to\n * a call.\n */\nfunction receivingCall(node: ast.Node): string | undefined {\n const direct = callReceiving(node);\n\n if (direct) return direct;\n\n // `const data = request.all(); await Model.create(data);`\n const parent = ast.parentOf(node);\n\n if (\n parent &&\n ast.isVariableDeclaration(parent) &&\n parent.init === node &&\n ast.isIdentifier(parent.id)\n ) {\n for (const reference of referencesOf(parent)) {\n const name = callReceiving(reference);\n if (name) return name;\n }\n }\n\n return undefined;\n}\n\nfunction checkRequestAll(node: ast.Node, findings: SourceFinding[]): void {\n if (!ast.isCallExpression(node)) return;\n\n const callee = unwrap(ast.expressionOf(node));\n\n if (!isMemberAccess(callee)) return;\n\n const object = unwrap(ast.expressionOf(callee));\n\n if (!ast.isIdentifier(object) || !REQUEST_NAMES.has(ast.valueText(object))) return;\n\n const method = memberName(callee);\n\n if (!method || !UNFILTERED_METHODS.has(method)) return;\n\n const target = receivingCall(node);\n\n if (!target) return;\n\n findings.push({\n code: \"WL9005\",\n node,\n message: `\\`${ast.valueText(object)}.${method}()\\` passes every client-sent field to \\`${target}\\`. Use \\`${ast.valueText(object)}.validated()\\` so only the fields the validation schema accepted reach it.`,\n });\n}\n\n// ── WL9006: a mutating route with no auth guard and no public: true ─────────\n\nconst ROUTER_NAMES: ReadonlySet<string> = new Set([\"router\"]);\n\n/** Wrappers whose callback routes are guarded, e.g. `guarded(() => …)`. */\nconst GUARD_FUNCTIONS: ReadonlySet<string> = new Set([\"guarded\"]);\n\n/** `router.post(…)` and friends. */\nconst MUTATING_ROUTER_METHODS: ReadonlySet<string> = new Set([\"post\", \"put\", \"patch\", \"delete\"]);\n\n/** Route builder methods (`router.route(path).create(…)`) that register a mutating route. */\nconst MUTATING_BUILDER_METHODS: ReadonlySet<string> = new Set([\n \"post\",\n \"postOne\",\n \"put\",\n \"updateOne\",\n \"patch\",\n \"patchOne\",\n \"delete\",\n \"deleteOne\",\n \"create\",\n \"update\",\n \"destroy\",\n]);\n\nconst READ_ONLY_RESOURCE_METHODS: ReadonlySet<string> = new Set([\"list\", \"get\", \"show\"]);\n\nfunction isRouter(node: ast.Node): boolean {\n const target = unwrap(node);\n\n return ast.isIdentifier(target) && ROUTER_NAMES.has(ast.valueText(target));\n}\n\nfunction isGuardName(name: string | undefined): boolean {\n return name !== undefined && isAuthGuardName(name);\n}\n\n/** Whether a constant is initialised with `markAuthGuard(fn)` (not `markAuthGuard(fn, false)`). */\nfunction isMarkedGuard(identifier: ast.Identifier): boolean {\n const initializer = resolveInitializer(identifier);\n const value = initializer ? unwrap(initializer) : undefined;\n\n return (\n value !== undefined &&\n ast.isCallExpression(value) &&\n calleeName(value) === \"markAuthGuard\" &&\n !(value.arguments[1]?.type === \"Literal\" && value.arguments[1].value === false)\n );\n}\n\n/**\n * Whether a call such as `requireAdmin()` invokes a local factory\n * (`const requireAdmin = () => markAuthGuard(fn)`) whose body declares the\n * middleware it returns an auth guard. A name that says nothing about guards\n * (`requireAdmin`) still counts once the factory marks itself.\n */\nfunction isMarkedGuardFactory(call: ast.CallExpression): boolean {\n const callee = unwrap(ast.expressionOf(call));\n\n if (!ast.isIdentifier(callee)) return false;\n\n const initializer = resolveInitializer(callee);\n\n if (!initializer || !isFunctionLike(unwrap(initializer))) return false;\n\n let marked = false;\n\n walk(\n initializer,\n (node) => {\n if (\n ast.isCallExpression(node) &&\n calleeName(node) === \"markAuthGuard\" &&\n !(node.arguments[1]?.type === \"Literal\" && node.arguments[1].value === false)\n ) {\n marked = true;\n }\n\n return marked ? false : undefined;\n },\n true,\n );\n\n return marked;\n}\n\nfunction middlewareHasGuard(node: ast.Node): boolean {\n const value = unwrap(node);\n\n // A variable or a call: the check cannot see into it, so it trusts it.\n if (!ast.isArrayLiteralExpression(value)) return true;\n\n return value.elements.some((element) => {\n if (!element) return false;\n if (ast.isSpreadElement(element)) return true;\n\n const item = unwrap(element);\n\n if (ast.isIdentifier(item)) return isGuardName(ast.valueText(item)) || isMarkedGuard(item);\n if (ast.isCallExpression(item)) {\n // `markAuthGuard(fn, false)` declares the opposite: not a guard.\n const optedOut =\n calleeName(item) === \"markAuthGuard\" &&\n item.arguments[1]?.type === \"Literal\" &&\n item.arguments[1].value === false;\n\n return !optedOut && (isGuardName(calleeName(item)) || isMarkedGuardFactory(item));\n }\n if (isMemberAccess(item)) return isGuardName(memberName(item));\n\n return false;\n });\n}\n\n/** `public: true`, a guard in `middleware`, or an options value the check cannot see into. */\nfunction optionsGuard(node: ast.Node | undefined): \"guarded\" | \"unguarded\" | \"none\" {\n if (!node) return \"none\";\n\n const value = unwrap(node);\n\n if (!ast.isObjectLiteralExpression(value)) return \"guarded\";\n\n let verdict: \"guarded\" | \"unguarded\" = \"unguarded\";\n\n for (const property of value.properties) {\n if (ast.isSpreadAssignment(property)) {\n verdict = \"guarded\";\n continue;\n }\n\n let key: string | undefined;\n let propertyValue: ast.Node | undefined;\n\n if (ast.isPropertyAssignment(property)) {\n key = keyName(property.key);\n propertyValue = unwrap(property.value);\n } else if (ast.isShorthandPropertyAssignment(property)) {\n key = ast.valueText(property.key);\n propertyValue = property.key;\n }\n\n if (!propertyValue) continue;\n if (key === \"public\" && propertyValue.type === \"Literal\" && propertyValue.value === true)\n return \"guarded\";\n if (key === \"middleware\" && middlewareHasGuard(propertyValue)) return \"guarded\";\n }\n\n return verdict;\n}\n\n/** Whether an enclosing function is a guarded (or public) group callback. */\nfunction insideGuardedGroup(node: ast.Node): boolean {\n for (const ancestor of ancestors(node)) {\n if (!ast.isArrowFunction(ancestor) && !ast.isFunctionExpression(ancestor)) continue;\n\n const parent = ast.parentOf(ancestor);\n\n if (!parent || !ast.isCallExpression(parent)) continue;\n if (!parent.arguments.some((argument) => argument === ancestor)) continue;\n\n const name = calleeName(parent);\n\n if (name !== undefined && GUARD_FUNCTIONS.has(name)) return true;\n\n const callee = unwrap(ast.expressionOf(parent));\n\n if (\n isMemberAccess(callee) &&\n memberName(callee) === \"group\" &&\n isRouter(ast.expressionOf(callee)) &&\n optionsGuard(parent.arguments[0]) === \"guarded\"\n ) {\n return true;\n }\n }\n\n return false;\n}\n\nfunction pathText(node: ast.Node | undefined): string {\n return literalString(node) ?? \"(dynamic path)\";\n}\n\n/**\n * Walk a builder chain back to `router.route(path, options)`, collecting every\n * options object passed along the way (`route()`, `nest()`).\n */\nfunction builderChain(node: ast.Node): { path: string; optionNodes: ast.Node[] } | undefined {\n const optionNodes: ast.Node[] = [];\n let current = unwrap(node);\n\n while (ast.isCallExpression(current)) {\n const callee = unwrap(ast.expressionOf(current));\n\n if (!isMemberAccess(callee)) return undefined;\n\n const name = memberName(callee);\n\n if (name === \"route\" && isRouter(ast.expressionOf(callee))) {\n const options = current.arguments[1];\n if (options) optionNodes.push(options);\n\n return { path: pathText(current.arguments[0]), optionNodes };\n }\n\n if (name === \"nest\") {\n const options = current.arguments[1];\n if (options) optionNodes.push(options);\n }\n\n current = unwrap(ast.expressionOf(callee));\n }\n\n return undefined;\n}\n\nfunction resourceIsReadOnly(node: ast.Node | undefined): boolean {\n if (!node) return false;\n\n const value = unwrap(node);\n\n if (!ast.isObjectLiteralExpression(value)) return false;\n\n for (const property of value.properties) {\n if (!ast.isPropertyAssignment(property) || keyName(property.key) !== \"only\") continue;\n\n const only = unwrap(property.value);\n\n if (!ast.isArrayLiteralExpression(only)) return false;\n\n return only.elements.every((element) => {\n if (!element) return false;\n const method = literalString(element);\n return method !== undefined && READ_ONLY_RESOURCE_METHODS.has(method);\n });\n }\n\n return false;\n}\n\nfunction unguardedRoute(node: ast.Node, route: string): SourceFinding {\n return {\n code: \"WL9006\",\n node,\n message: `Mutating route \\`${route}\\` has no auth middleware. Add an auth guard (\\`middleware: [authMiddleware()]\\`, or register it inside a guarded group) or mark it intentionally open with \\`public: true\\`.`,\n };\n}\n\nfunction checkRouteGuard(node: ast.Node, findings: SourceFinding[]): void {\n if (!ast.isCallExpression(node)) return;\n\n const callee = unwrap(ast.expressionOf(node));\n\n if (!isMemberAccess(callee)) return;\n\n const method = memberName(callee);\n\n if (!method) return;\n\n const guarded = (options: ast.Node | undefined) =>\n optionsGuard(options) === \"guarded\" || insideGuardedGroup(node);\n\n if (isRouter(ast.expressionOf(callee))) {\n // router.post(path, handler, options)\n if (MUTATING_ROUTER_METHODS.has(method)) {\n if (!guarded(node.arguments[2])) {\n findings.push(\n unguardedRoute(node, `${method.toUpperCase()} ${pathText(node.arguments[0])}`),\n );\n }\n\n return;\n }\n\n // router.add(\"POST\", path, handler, options)\n if (method === \"add\") {\n const httpMethod = literalString(node.arguments[0])?.toUpperCase();\n\n if (!httpMethod || !isMutatingHttpMethod(httpMethod)) return;\n\n if (!guarded(node.arguments[3])) {\n findings.push(unguardedRoute(node, `${httpMethod} ${pathText(node.arguments[1])}`));\n }\n\n return;\n }\n\n // router.restfulResource(path, resource, options)\n if (method === \"restfulResource\") {\n if (resourceIsReadOnly(node.arguments[2])) return;\n\n if (!guarded(node.arguments[2])) {\n findings.push(unguardedRoute(node, `resource ${pathText(node.arguments[0])}`));\n }\n }\n\n return;\n }\n\n // router.route(path, options).create(handler, options)\n if (!MUTATING_BUILDER_METHODS.has(method)) return;\n\n const chain = builderChain(ast.expressionOf(callee));\n\n if (!chain) return;\n if (optionsGuard(node.arguments[1]) === \"guarded\") return;\n if (chain.optionNodes.some((options) => optionsGuard(options) === \"guarded\")) return;\n if (insideGuardedGroup(node)) return;\n\n findings.push(unguardedRoute(node, `${method} ${chain.path}`));\n}\n\n// ── WL9007: a redirect or link target from request input ────────────────────\n\n/** Calls that turn their first argument into a redirect or a navigation. */\nconst REDIRECT_CALLS: ReadonlySet<string> = new Set([\n \"redirect\",\n \"permanentRedirect\",\n \"navigateTo\",\n]);\n\n/** Components whose `to`/`href` prop navigates. */\nconst LINK_COMPONENTS: ReadonlySet<string> = new Set([\"Link\", \"NavLink\"]);\n\n/** Helpers that return an allowed target (or a fallback) for any input. */\nconst REDIRECT_SANITIZERS: ReadonlySet<string> = new Set([\"safeRedirect\", \"safeRedirectTarget\"]);\n\n/** Predicates that, used in an enclosing `if` or `? :`, prove the target was checked. */\nconst REDIRECT_VALIDATORS: ReadonlySet<string> = new Set([\"isSafeRedirect\", \"isSafeRedirectUrl\"]);\n\n/** Client-side input: query-string hooks. */\nconst CLIENT_INPUT_CALLS: ReadonlySet<string> = new Set([\n \"useQueryString\",\n \"useSearchParams\",\n \"useParams\",\n]);\n\n/** Identifiers whose members are client-controlled (a loader's `query`, URLSearchParams). */\nconst CLIENT_INPUT_NAMES: ReadonlySet<string> = new Set([\"query\", \"searchParams\"]);\n\nfunction isClientSource(node: ast.Node): boolean {\n if (ast.isCallExpression(node)) {\n const name = calleeName(node);\n if (name !== undefined && CLIENT_INPUT_CALLS.has(name)) return true;\n }\n\n if (isMemberAccess(node)) {\n const root = rootIdentifier(node);\n\n if (root && CLIENT_INPUT_NAMES.has(ast.valueText(root))) return true;\n\n // location.search / window.location.hash\n const name = memberName(node);\n\n if (name === \"search\" || name === \"hash\") {\n const object = unwrap(ast.expressionOf(node));\n\n if (ast.isIdentifier(object) && ast.valueText(object) === \"location\") return true;\n if (isMemberAccess(object) && memberName(object) === \"location\") return true;\n }\n }\n\n return false;\n}\n\nfunction isRedirectTainted(node: ast.Node): boolean {\n return containsRequestInput(node, { sanitizers: REDIRECT_SANITIZERS, isSource: isClientSource });\n}\n\n/** An enclosing `if (isSafeRedirect(x))` or `isSafeRedirect(x) ? … : …` validated the target. */\nfunction checkedByValidator(node: ast.Node): boolean {\n for (const ancestor of ancestors(node)) {\n let test: ast.Node | undefined;\n\n if (ast.isIfStatement(ancestor)) test = ast.expressionOf(ancestor);\n else if (ast.isConditionalExpression(ancestor)) test = ancestor.test;\n else continue;\n\n let validated = false;\n\n walk(test, (child) => {\n if (ast.isCallExpression(child)) {\n const name = calleeName(child);\n if (name !== undefined && REDIRECT_VALIDATORS.has(name)) validated = true;\n }\n\n return undefined;\n });\n\n if (validated) return true;\n }\n\n return false;\n}\n\nfunction unsafeRedirect(node: ast.Node, what: string, helper: string, from: string): SourceFinding {\n return {\n code: \"WL9007\",\n node,\n message: `This ${what} target comes from request input. Pass it through \\`${helper}()\\` from \\`${from}\\` so only same-origin paths and allow-listed origins are followed (open redirects and \\`javascript:\\` URLs otherwise).`,\n };\n}\n\nfunction checkRedirect(node: ast.Node, findings: SourceFinding[]): void {\n if (ast.isCallExpression(node)) {\n const name = calleeName(node);\n\n if (name === undefined || !REDIRECT_CALLS.has(name)) return;\n\n // `router.redirect(from, to)` registers a static redirect, not a request-time one.\n const callee = unwrap(ast.expressionOf(node));\n\n if (isMemberAccess(callee)) {\n const owner = unwrap(ast.expressionOf(callee));\n if (ast.isIdentifier(owner) && ast.valueText(owner) === \"router\") return;\n }\n\n const target = node.arguments[0];\n\n if (!target || ast.isSpreadElement(target)) return;\n if (!isRedirectTainted(target) || checkedByValidator(node)) return;\n\n findings.push(\n name === \"navigateTo\"\n ? unsafeRedirect(target, \"navigation\", \"safeRedirectTarget\", \"@warlock.js/web\")\n : unsafeRedirect(target, \"redirect\", \"safeRedirect\", \"@warlock.js/core\"),\n );\n\n return;\n }\n\n if (!ast.isJsxAttribute(node) || ast.nameNode(node).type !== \"JSXIdentifier\") return;\n if (ast.valueText(ast.nameNode(node)) !== \"to\" && ast.valueText(ast.nameNode(node)) !== \"href\")\n return;\n\n const element = ast.parentOf(node);\n\n if (!element || (!ast.isJsxOpeningElement(element) && !ast.isJsxSelfClosingElement(element)))\n return;\n if (element.name.type !== \"JSXIdentifier\" || !LINK_COMPONENTS.has(element.name.name)) return;\n\n const initializer = node.value;\n\n if (!initializer || !ast.isJsxExpression(initializer) || !initializer.expression) return;\n\n const expression = initializer.expression;\n\n if (!isRedirectTainted(expression) || checkedByValidator(expression)) return;\n\n findings.push(unsafeRedirect(expression, \"link\", \"safeRedirectTarget\", \"@warlock.js/web\"));\n}\n\n// ── WL9008: raw emails, credentials and request bodies in logs ──────────────\n\nconst LOGGER_NAMES: ReadonlySet<string> = new Set([\"log\", \"logger\", \"console\"]);\n\nconst LOG_METHODS: ReadonlySet<string> = new Set([\n \"info\",\n \"warn\",\n \"error\",\n \"debug\",\n \"success\",\n \"fatal\",\n \"log\",\n \"assert\",\n \"trace\",\n]);\n\n/** Helpers that make a value safe to log; their arguments are not inspected. */\nconst REDACTORS: ReadonlySet<string> = new Set([\n \"maskEmail\",\n \"maskEmailsInText\",\n \"redact\",\n \"redactHeaders\",\n \"redactError\",\n \"redactUrlCredentials\",\n \"redactQueryCredentials\",\n \"escapeLogValue\",\n \"toLogSnapshot\",\n]);\n\n/** Name endings that mark a value as a credential. */\nconst SECRET_SUFFIXES = [\n \"token\",\n \"password\",\n \"passwd\",\n \"secret\",\n \"apikey\",\n \"authorization\",\n \"cookie\",\n \"cookies\",\n \"otp\",\n \"jwt\",\n \"privatekey\",\n];\n\n/**\n * Whether the logger censors this key by default (`DEFAULT_REDACT_KEYS` from\n * `@warlock.js/logger`). A secret logged under one of them never reaches a\n * channel. Emails are not on that list: they are not censored by default.\n */\nfunction isCensoredKey(key: string): boolean {\n return DEFAULT_REDACT_KEY_SET.has(normalizeRedactKey(key));\n}\n\n/** `email` for addresses, `secret` for credentials, otherwise `undefined`. */\nfunction sensitiveCategory(name: string): \"email\" | \"secret\" | undefined {\n const normalized = normalizeRedactKey(name);\n\n if (normalized.endsWith(\"email\") || normalized.endsWith(\"emailaddress\")) return \"email\";\n if (DEFAULT_REDACT_KEY_SET.has(normalized)) return \"secret\";\n if (SECRET_SUFFIXES.some((suffix) => normalized.endsWith(suffix))) return \"secret\";\n\n return undefined;\n}\n\nfunction describeCategory(category: \"email\" | \"secret\", name: string | undefined): string {\n if (category === \"email\") return \"a raw email address\";\n\n return name ? `the credential \\`${name}\\`` : \"a credential\";\n}\n\nfunction describeMember(member: string): string {\n if (member === \"headers\" || member === \"cookies\") return member;\n\n return `body (\\`${member}\\`)`;\n}\n\n/** `request.all()` / `request.body` itself, not `request.body.title`. */\nfunction isWholeAccess(node: ast.Node, member: string): boolean {\n if (ast.isCallExpression(node)) {\n const callee = unwrap(ast.expressionOf(node));\n return isMemberAccess(callee) && memberName(callee) === member;\n }\n\n return isMemberAccess(node) && memberName(node) === member;\n}\n\ntype LogFinding = { node: ast.Node; what: string };\n\nfunction inspectLogged(node: ast.Node, censoredKey: boolean, found: LogFinding[]): void {\n const value = unwrap(node);\n\n if (isFunctionLike(value)) return;\n\n if (ast.isCallExpression(value)) {\n const name = calleeName(value);\n if (name !== undefined && REDACTORS.has(name)) return;\n }\n\n // request.all() / request.body / request.headers / request.input(\"email\")\n if (ast.isCallExpression(value) || isMemberAccess(value)) {\n const member = requestMemberAccess(value);\n\n if (member !== undefined && isWholeAccess(value, member)) {\n if (REQUEST_BODY_MEMBERS.has(member)) {\n found.push({ node: value, what: `the request ${describeMember(member)}` });\n return;\n }\n\n if (member === \"email\") {\n found.push({ node: value, what: \"a raw email address\" });\n return;\n }\n }\n\n if (ast.isCallExpression(value) && member !== undefined) {\n const key = literalString(value.arguments[0]);\n const category = key ? sensitiveCategory(key) : undefined;\n\n if (category && !(censoredKey && category === \"secret\")) {\n found.push({ node: value, what: describeCategory(category, key) });\n return;\n }\n }\n }\n\n if (ast.isIdentifier(value) || isMemberAccess(value)) {\n const name = ast.isIdentifier(value) ? ast.valueText(value) : memberName(value);\n const category = name ? sensitiveCategory(name) : undefined;\n\n if (category && !(censoredKey && category === \"secret\")) {\n found.push({ node: value, what: describeCategory(category, name) });\n return;\n }\n\n // `request.body.title` reads one field; only the whole body is reported.\n if (isMemberAccess(value) && requestMemberAccess(value) === undefined) {\n inspectLogged(ast.expressionOf(value), false, found);\n\n if (ast.isElementAccessExpression(value)) inspectLogged(value.property, false, found);\n }\n\n return;\n }\n\n if (ast.isObjectLiteralExpression(value)) {\n for (const property of value.properties) {\n if (ast.isSpreadAssignment(property)) {\n inspectLogged(ast.expressionOf(property), false, found);\n } else if (ast.isPropertyAssignment(property)) {\n const key = keyName(property.key);\n inspectLogged(property.value, key !== undefined && isCensoredKey(key), found);\n } else if (ast.isShorthandPropertyAssignment(property)) {\n inspectLogged(property.key, isCensoredKey(ast.valueText(property.key)), found);\n }\n }\n\n return;\n }\n\n if (ast.isTemplateExpression(value)) {\n for (const expression of value.expressions) inspectLogged(expression, false, found);\n return;\n }\n\n if (ast.isBinaryExpression(value)) {\n inspectLogged(value.left, false, found);\n inspectLogged(value.right, false, found);\n return;\n }\n\n if (ast.isConditionalExpression(value)) {\n inspectLogged(value.consequent, censoredKey, found);\n inspectLogged(value.alternate, censoredKey, found);\n return;\n }\n\n if (ast.isArrayLiteralExpression(value)) {\n for (const element of value.elements) {\n if (ast.isOmittedExpression(element)) continue;\n inspectLogged(\n ast.isSpreadElement(element) ? ast.expressionOf(element) : element,\n false,\n found,\n );\n }\n\n return;\n }\n\n if (ast.isCallExpression(value)) {\n // JSON.stringify(request.all()), String(user.email), user.email.trim()\n const callee = unwrap(ast.expressionOf(value));\n\n if (isMemberAccess(callee)) inspectLogged(ast.expressionOf(callee), false, found);\n\n for (const argument of value.arguments) {\n inspectLogged(\n ast.isSpreadElement(argument) ? ast.expressionOf(argument) : argument,\n false,\n found,\n );\n }\n\n return;\n }\n\n if (ast.isAwaitExpression(value)) inspectLogged(ast.expressionOf(value), censoredKey, found);\n}\n\nfunction checkSensitiveLog(node: ast.Node, findings: SourceFinding[]): void {\n if (!ast.isCallExpression(node)) return;\n\n const callee = unwrap(ast.expressionOf(node));\n\n if (!isMemberAccess(callee)) return;\n\n const method = memberName(callee);\n\n if (!method || !LOG_METHODS.has(method)) return;\n\n const owner = unwrap(ast.expressionOf(callee));\n\n if (!ast.isIdentifier(owner)) return;\n\n // log.info(…), console.log(…), and request.log(…)\n const isLogger =\n LOGGER_NAMES.has(ast.valueText(owner)) ||\n (REQUEST_NAMES.has(ast.valueText(owner)) && method === \"log\");\n\n if (!isLogger) return;\n\n const found: LogFinding[] = [];\n\n for (const argument of node.arguments) {\n inspectLogged(\n ast.isSpreadElement(argument) ? ast.expressionOf(argument) : argument,\n false,\n found,\n );\n }\n\n for (const finding of found) {\n findings.push({\n code: \"WL9008\",\n node: finding.node,\n message: `Do not log ${finding.what} as-is. Pass it through a redaction helper (\\`maskEmail()\\`, \\`redact()\\`, \\`request.toLogSnapshot()\\`), or log it under a key the logger censors.`,\n });\n }\n}\n\n// ── WL9009: unbounded regex quantifiers on request input ────────────────────\n\n/** `regex.test(input)` / `regex.exec(input)`. */\nconst REGEX_METHODS: ReadonlySet<string> = new Set([\"test\", \"exec\"]);\n\n/** `input.match(regex)` and friends: the regex is the first argument. */\nconst STRING_METHODS: ReadonlySet<string> = new Set([\n \"match\",\n \"matchAll\",\n \"replace\",\n \"replaceAll\",\n \"search\",\n \"split\",\n]);\n\n/**\n * The first unbounded quantifier in a regex source (`*`, `+`, `{n,}`), skipping\n * escapes and character classes, or `undefined` when every repetition is\n * bounded.\n */\nexport function findUnboundedQuantifier(source: string): string | undefined {\n let inClass = false;\n\n for (let index = 0; index < source.length; index++) {\n const char = source[index];\n\n if (char === \"\\\\\") {\n index++;\n continue;\n }\n\n if (inClass) {\n if (char === \"]\") inClass = false;\n continue;\n }\n\n if (char === \"[\") {\n inClass = true;\n continue;\n }\n\n if (char === \"*\" || char === \"+\") {\n return `${char}${source[index + 1] === \"?\" ? \"?\" : \"\"}`;\n }\n\n if (char === \"{\") {\n const match = /^\\{(\\d+),\\}/.exec(source.slice(index));\n if (match) return match[0];\n }\n }\n\n return undefined;\n}\n\n/** The pattern of a regex literal, `new RegExp(\"…\")`, or a constant holding either. */\nfunction regexSource(node: ast.Node | undefined, depth = 0): string | undefined {\n if (!node || depth > 8) return undefined;\n\n const value = unwrap(node);\n\n if (ast.isRegularExpressionLiteral(value)) {\n return ast.valueText(value).slice(1, ast.valueText(value).lastIndexOf(\"/\"));\n }\n\n if (\n (ast.isNewExpression(value) || ast.isCallExpression(value)) &&\n ast.isIdentifier(ast.expressionOf(value)) &&\n ast.valueText(ast.expressionOf(value)) === \"RegExp\"\n ) {\n return literalString(value.arguments?.[0]);\n }\n\n if (ast.isIdentifier(value)) {\n const initializer = resolveInitializer(value);\n return initializer ? regexSource(initializer, depth + 1) : undefined;\n }\n\n return undefined;\n}\n\nfunction checkUnboundedRegex(node: ast.Node, findings: SourceFinding[]): void {\n if (!ast.isCallExpression(node)) return;\n\n const callee = unwrap(ast.expressionOf(node));\n\n if (!isMemberAccess(callee)) return;\n\n const method = memberName(callee);\n\n if (!method) return;\n\n const first = node.arguments[0];\n const firstArgument = first && !ast.isSpreadElement(first) ? first : undefined;\n let regex: ast.Node | undefined;\n let input: ast.Node | undefined;\n\n if (REGEX_METHODS.has(method)) {\n regex = ast.expressionOf(callee);\n input = firstArgument;\n } else if (STRING_METHODS.has(method)) {\n regex = firstArgument;\n input = ast.expressionOf(callee);\n } else {\n return;\n }\n\n if (!input) return;\n\n const source = regexSource(regex);\n\n if (source === undefined) return;\n\n const quantifier = findUnboundedQuantifier(source);\n\n if (!quantifier || !containsRequestInput(input)) return;\n\n findings.push({\n code: \"WL9009\",\n node,\n message: `This regex has an unbounded quantifier (\\`${quantifier}\\`) and runs on request input, so a crafted input can make it backtrack for a long time (ReDoS). Bound the repetition, e.g. \\`{1,256}\\`, or cap the input length before matching.`,\n });\n}\n\n/**\n * The security conventions in one parsed file:\n *\n * - `WL9005` `request.all()` (or `heavy()`, `allExceptParams()`) passed to a\n * query, a service or `inApp()`;\n * - `WL9006` a POST/PUT/PATCH/DELETE route with no auth middleware and no\n * `public: true`;\n * - `WL9007` a `redirect()` / `navigateTo()` / `<Link to|href>` target taken\n * from request input without `safeRedirect()` / `safeRedirectTarget()` or an\n * `isSafeRedirect()` check;\n * - `WL9008` a raw email, credential or request body passed to a logger;\n * - `WL9009` a regex with an unbounded quantifier run on request input.\n */\nexport function securityFindings(sourceFile: ast.SourceFile): SourceFinding[] {\n const findings: SourceFinding[] = [];\n\n walk(\n sourceFile,\n (node) => {\n checkRequestAll(node, findings);\n checkRouteGuard(node, findings);\n checkRedirect(node, findings);\n checkSensitiveLog(node, findings);\n checkUnboundedRegex(node, findings);\n\n return undefined;\n },\n true,\n );\n\n return findings;\n}\n"],"mappings":"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"}
@@ -0,0 +1,32 @@
1
+ import { scanTranslationSource } from "../../localization/translation-usage.mjs";
2
+ import { createProblem } from "../problem.mjs";
3
+
4
+ //#region ../../packages/core/src/tooling/check/translation-conventions.ts
5
+ /** Application code that renders on the client: everything under `src/web` except setup and test files. */
6
+ const WEB_CLIENT_FILE = /^src\/web\/.+\.[cm]?[jt]sx?$/;
7
+ const SETUP_OR_TEST_FILE = /\.(?:setup|test|spec)\.[cm]?[jt]sx?$/i;
8
+ /**
9
+ * `WL9104`: a translation lookup whose key is only known at runtime.
10
+ *
11
+ * A page ships the translation keys its client code can reach, found by
12
+ * reading the code. A key computed at runtime (`trans(key)`, a translator
13
+ * passed on as a value) defeats that reading, so the page falls back to every
14
+ * group. The fix is a `config.translations` declaration in the page's setup
15
+ * file, which this rule points at.
16
+ */
17
+ function translationProblems(relativePath, source) {
18
+ if (!WEB_CLIENT_FILE.test(relativePath) || SETUP_OR_TEST_FILE.test(relativePath)) return [];
19
+ return scanTranslationSource(relativePath, source).usage.dynamicSites.map((site) => createProblem({
20
+ code: "WL9104",
21
+ source: "convention",
22
+ severity: "warning",
23
+ message: `Translation key \`${site.text}\` is computed at runtime, so a page that renders it ships every translation group. Declare the keys it uses with \`config.translations\` in the page's setup file.`,
24
+ file: relativePath,
25
+ line: site.line,
26
+ column: site.column
27
+ }));
28
+ }
29
+
30
+ //#endregion
31
+ export { translationProblems };
32
+ //# sourceMappingURL=translation-conventions.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"translation-conventions.mjs","names":[],"sources":["../../../../../../../../../packages/core/src/tooling/check/translation-conventions.ts"],"sourcesContent":["import { scanTranslationSource } from \"../../localization/translation-usage\";\nimport { createProblem, type ToolingProblem } from \"../problem\";\n\n/** Application code that renders on the client: everything under `src/web` except setup and test files. */\nconst WEB_CLIENT_FILE = /^src\\/web\\/.+\\.[cm]?[jt]sx?$/;\nconst SETUP_OR_TEST_FILE = /\\.(?:setup|test|spec)\\.[cm]?[jt]sx?$/i;\n\n/**\n * `WL9104`: a translation lookup whose key is only known at runtime.\n *\n * A page ships the translation keys its client code can reach, found by\n * reading the code. A key computed at runtime (`trans(key)`, a translator\n * passed on as a value) defeats that reading, so the page falls back to every\n * group. The fix is a `config.translations` declaration in the page's setup\n * file, which this rule points at.\n */\nexport function translationProblems(relativePath: string, source: string): ToolingProblem[] {\n if (!WEB_CLIENT_FILE.test(relativePath) || SETUP_OR_TEST_FILE.test(relativePath)) return [];\n\n return scanTranslationSource(relativePath, source).usage.dynamicSites.map((site) =>\n createProblem({\n code: \"WL9104\",\n source: \"convention\",\n severity: \"warning\",\n message:\n `Translation key \\`${site.text}\\` is computed at runtime, so a page that renders it ships every ` +\n \"translation group. Declare the keys it uses with `config.translations` in the page's setup file.\",\n file: relativePath,\n line: site.line,\n column: site.column,\n }),\n );\n}\n"],"mappings":";;;;;AAIA,MAAM,kBAAkB;AACxB,MAAM,qBAAqB;;;;;;;;;;AAW3B,SAAgB,oBAAoB,cAAsB,QAAkC;CAC1F,IAAI,CAAC,gBAAgB,KAAK,YAAY,KAAK,mBAAmB,KAAK,YAAY,GAAG,OAAO,CAAC;CAE1F,OAAO,sBAAsB,cAAc,MAAM,CAAC,CAAC,MAAM,aAAa,KAAK,SACzE,cAAc;EACZ,MAAM;EACN,QAAQ;EACR,UAAU;EACV,SACE,qBAAqB,KAAK,KAAK;EAEjC,MAAM;EACN,MAAM,KAAK;EACX,QAAQ,KAAK;CACf,CAAC,CACH;AACF"}
@@ -1,5 +1,5 @@
1
- import { isConfigFile, isEventFile, isLocaleFile, isMainFile, isRouteFile, isWorkerFile } from "../../dev-server/special-file-patterns.mjs";
2
1
  import { forEachChild } from "../syntax/ast.mjs";
2
+ import { isConfigFile, isEventFile, isLocaleFile, isMainFile, isRouteFile, isWorkerFile } from "../../dev-server/special-file-patterns.mjs";
3
3
  import path from "node:path";
4
4
  import { existsSync, readFileSync, readdirSync } from "node:fs";
5
5
  import { readTsconfig, resolvePathAlias } from "get-tsconfig";
@@ -1,4 +1,5 @@
1
1
  import { config } from "../../config/config-getter.mjs";
2
+ import { isRequiredGuardRoute } from "../../router/unguarded-route-diagnostics.mjs";
2
3
  import { buildRateLimitOptions } from "../../http/build-rate-limit-options.mjs";
3
4
  import { buildDeclaredResponses } from "../../openapi/response-schemas.mjs";
4
5
  import { expandMethods } from "../../openapi/paths.mjs";
@@ -134,11 +135,11 @@ function collectRoutes(routes, registry, warnings) {
134
135
  isPage: route.isPage === true,
135
136
  middleware: (route.middleware ?? []).map(functionName),
136
137
  auth: auth ? {
137
- required: auth.required !== false,
138
+ required: auth.required !== false || isRequiredGuardRoute(route),
138
139
  sources: auth.sources,
139
140
  userTypes: auth.userTypes
140
141
  } : {
141
- required: false,
142
+ required: isRequiredGuardRoute(route),
142
143
  sources: [],
143
144
  userTypes: []
144
145
  },
@@ -1 +1 @@
1
- {"version":3,"file":"collect-routes.mjs","names":[],"sources":["../../../../../../../../../packages/core/src/tooling/inspect/collect-routes.ts"],"sourcesContent":["import { config } from \"../../config/config-getter\";\nimport { buildRateLimitOptions } from \"../../http/build-rate-limit-options\";\nimport type { HttpLoggingConfig } from \"../../http/request-log-snapshot\";\nimport { expandMethods } from \"../../openapi/paths\";\nimport { buildRequestParts } from \"../../openapi/request-schemas\";\nimport { buildDeclaredResponses } from \"../../openapi/response-schemas\";\nimport type { SchemaRegistry } from \"../../openapi/schema-registry\";\nimport { readRouteAuth } from \"../../openapi/security\";\nimport type { Route } from \"../../router/types\";\nimport type {\n InspectRoute,\n InspectRouteLogging,\n InspectRouteRateLimit,\n InspectWarning,\n JsonSchema,\n} from \"./inspect-report.types\";\n\n/** Display order of methods within one path, matching `warlock routes`. */\nconst METHOD_ORDER = [\"GET\", \"POST\", \"PUT\", \"PATCH\", \"DELETE\", \"OPTIONS\", \"HEAD\", \"ALL\"];\n\nfunction methodRank(method: string): number {\n const index = METHOD_ORDER.indexOf(method);\n\n return index === -1 ? METHOD_ORDER.length : index;\n}\n\n/** The name of a function, without the `bound ` prefix `bind()` adds. */\nexport function functionName(value: unknown): string {\n if (typeof value !== \"function\") return \"anonymous\";\n\n const name = value.name.replace(/^bound /, \"\").trim();\n\n return name.length > 0 ? name : \"anonymous\";\n}\n\nfunction limitValue(value: unknown): number | \"dynamic\" | null {\n if (typeof value === \"number\") return value;\n\n return value === undefined || value === null ? null : \"dynamic\";\n}\n\nfunction windowValue(value: unknown): number | string | null {\n if (typeof value === \"number\" || typeof value === \"string\") return value;\n\n return value === undefined || value === null ? null : \"dynamic\";\n}\n\n/**\n * The limiter a route runs under: its own `rateLimit` options, else the global\n * `http.rateLimit` limiter (built with the same defaults the HTTP connector\n * uses), else none. `rateLimit: false` exempts the route from the global\n * limiter; `rateLimit: true` keeps it under the global policy.\n */\nfunction readRateLimit(\n route: Route,\n global: Record<string, unknown> | null,\n): InspectRouteRateLimit {\n if (typeof route.rateLimit === \"object\" && route.rateLimit !== null) {\n return { source: \"route\", max: route.rateLimit.max, timeWindow: route.rateLimit.timeWindow };\n }\n\n if (!global || route.rateLimit === false) {\n return { source: \"none\", max: null, timeWindow: null };\n }\n\n return {\n source: \"global\",\n max: limitValue(global.max),\n timeWindow: windowValue(global.timeWindow),\n };\n}\n\n/**\n * What the route's request logs keep: the route's `logging` option over\n * `http.logging`, the same precedence `resolveRequestLoggingOptions` applies.\n * Private URL rules can only remove more, per URL, so they are flagged rather\n * than evaluated.\n */\nfunction readLogging(route: Route, global: HttpLoggingConfig): InspectRouteLogging {\n return {\n body: route.logging?.body ?? global.body ?? true,\n query: route.logging?.query ?? global.query ?? true,\n privateRules: (global.private?.length ?? 0) > 0,\n };\n}\n\nfunction globalRateLimit(warnings: InspectWarning[]): Record<string, unknown> | null {\n try {\n const options = buildRateLimitOptions(config.get(\"http.rateLimit\"));\n\n return options ? { max: options.max, timeWindow: options.timeWindow } : null;\n } catch (error) {\n warnings.push({\n code: null,\n message: `http.rateLimit could not be read: ${error instanceof Error ? error.message : String(error)}`,\n source: \"routes\",\n });\n\n return null;\n }\n}\n\n/**\n * Describe every route: identity, middleware chain, guard, limits, logging and\n * the request/response schemas the OpenAPI generator derives (the same\n * `buildRequestParts` / `buildDeclaredResponses` code, so `inspect` and\n * `generate.openapi` never disagree). Resource `$ref`s land in `registry`.\n */\nexport function collectRoutes(\n routes: readonly Route[],\n registry: SchemaRegistry,\n warnings: InspectWarning[],\n): InspectRoute[] {\n const rateLimit = globalRateLimit(warnings);\n const logging = (config.get(\"http.logging\") ?? {}) as HttpLoggingConfig;\n\n const collected = routes.map((route): InspectRoute => {\n const method = String(route.method).toUpperCase();\n const label = `${method} ${route.path}`;\n const warn = (message: string) => {\n warnings.push({\n code: message.includes(\"JSON Schema\") ? \"WL4001\" : null,\n message: `${label}: ${message}`,\n source: route.sourceFile || null,\n });\n };\n const handler = route.handler as Route[\"handler\"] | undefined;\n const validation = handler?.validation;\n const auth = readRouteAuth(route.middleware, warn);\n const request = buildRequestParts({\n method: expandMethods(route.method)[0] ?? \"get\",\n path: route.path,\n validation,\n warn,\n });\n const body = request.requestBody?.content[\"application/json\"]?.schema;\n const responses: InspectRoute[\"responses\"] = {};\n\n for (const [status, response] of Object.entries(\n buildDeclaredResponses(handler?.responseSchema, registry, warn),\n )) {\n const schema: JsonSchema | undefined = response.content?.[\"application/json\"]?.schema;\n\n responses[status] = { description: response.description, schema: schema ?? null };\n }\n\n return {\n method,\n path: route.path,\n name: route.name ? route.name : null,\n sourceFile: route.sourceFile ? route.sourceFile : null,\n handler: functionName(handler),\n description: route.description ?? handler?.description ?? null,\n isPage: route.isPage === true,\n middleware: (route.middleware ?? []).map(functionName),\n auth: auth\n ? { required: auth.required !== false, sources: auth.sources, userTypes: auth.userTypes }\n : { required: false, sources: [], userTypes: [] },\n csrf: route.csrf !== false,\n rateLimit: readRateLimit(route, rateLimit),\n logging: readLogging(route, logging),\n request: {\n parameters: request.parameters.map((parameter) => ({\n name: parameter.name,\n in: parameter.in,\n required: parameter.required === true,\n schema: parameter.schema,\n })),\n body: body ?? null,\n customValidation: typeof validation?.validate === \"function\",\n },\n responses,\n };\n });\n\n return collected.sort((first, second) => {\n if (first.path !== second.path) return first.path < second.path ? -1 : 1;\n\n return methodRank(first.method) - methodRank(second.method);\n });\n}\n"],"mappings":";;;;;;;;;AAkBA,MAAM,eAAe;CAAC;CAAO;CAAQ;CAAO;CAAS;CAAU;CAAW;CAAQ;AAAK;AAEvF,SAAS,WAAW,QAAwB;CAC1C,MAAM,QAAQ,aAAa,QAAQ,MAAM;CAEzC,OAAO,UAAU,KAAK,aAAa,SAAS;AAC9C;;AAGA,SAAgB,aAAa,OAAwB;CACnD,IAAI,OAAO,UAAU,YAAY,OAAO;CAExC,MAAM,OAAO,MAAM,KAAK,QAAQ,WAAW,EAAE,CAAC,CAAC,KAAK;CAEpD,OAAO,KAAK,SAAS,IAAI,OAAO;AAClC;AAEA,SAAS,WAAW,OAA2C;CAC7D,IAAI,OAAO,UAAU,UAAU,OAAO;CAEtC,OAAO,UAAU,UAAa,UAAU,OAAO,OAAO;AACxD;AAEA,SAAS,YAAY,OAAwC;CAC3D,IAAI,OAAO,UAAU,YAAY,OAAO,UAAU,UAAU,OAAO;CAEnE,OAAO,UAAU,UAAa,UAAU,OAAO,OAAO;AACxD;;;;;;;AAQA,SAAS,cACP,OACA,QACuB;CACvB,IAAI,OAAO,MAAM,cAAc,YAAY,MAAM,cAAc,MAC7D,OAAO;EAAE,QAAQ;EAAS,KAAK,MAAM,UAAU;EAAK,YAAY,MAAM,UAAU;CAAW;CAG7F,IAAI,CAAC,UAAU,MAAM,cAAc,OACjC,OAAO;EAAE,QAAQ;EAAQ,KAAK;EAAM,YAAY;CAAK;CAGvD,OAAO;EACL,QAAQ;EACR,KAAK,WAAW,OAAO,GAAG;EAC1B,YAAY,YAAY,OAAO,UAAU;CAC3C;AACF;;;;;;;AAQA,SAAS,YAAY,OAAc,QAAgD;CACjF,OAAO;EACL,MAAM,MAAM,SAAS,QAAQ,OAAO,QAAQ;EAC5C,OAAO,MAAM,SAAS,SAAS,OAAO,SAAS;EAC/C,eAAe,OAAO,SAAS,UAAU,KAAK;CAChD;AACF;AAEA,SAAS,gBAAgB,UAA4D;CACnF,IAAI;EACF,MAAM,UAAU,sBAAsB,OAAO,IAAI,gBAAgB,CAAC;EAElE,OAAO,UAAU;GAAE,KAAK,QAAQ;GAAK,YAAY,QAAQ;EAAW,IAAI;CAC1E,SAAS,OAAO;EACd,SAAS,KAAK;GACZ,MAAM;GACN,SAAS,qCAAqC,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;GACnG,QAAQ;EACV,CAAC;EAED,OAAO;CACT;AACF;;;;;;;AAQA,SAAgB,cACd,QACA,UACA,UACgB;CAChB,MAAM,YAAY,gBAAgB,QAAQ;CAC1C,MAAM,UAAW,OAAO,IAAI,cAAc,KAAK,CAAC;CA6DhD,OA3DkB,OAAO,KAAK,UAAwB;EACpD,MAAM,SAAS,OAAO,MAAM,MAAM,CAAC,CAAC,YAAY;EAChD,MAAM,QAAQ,GAAG,OAAO,GAAG,MAAM;EACjC,MAAM,QAAQ,YAAoB;GAChC,SAAS,KAAK;IACZ,MAAM,QAAQ,SAAS,aAAa,IAAI,WAAW;IACnD,SAAS,GAAG,MAAM,IAAI;IACtB,QAAQ,MAAM,cAAc;GAC9B,CAAC;EACH;EACA,MAAM,UAAU,MAAM;EACtB,MAAM,aAAa,SAAS;EAC5B,MAAM,OAAO,cAAc,MAAM,YAAY,IAAI;EACjD,MAAM,UAAU,kBAAkB;GAChC,QAAQ,cAAc,MAAM,MAAM,CAAC,CAAC,MAAM;GAC1C,MAAM,MAAM;GACZ;GACA;EACF,CAAC;EACD,MAAM,OAAO,QAAQ,aAAa,QAAQ,mBAAmB,EAAE;EAC/D,MAAM,YAAuC,CAAC;EAE9C,KAAK,MAAM,CAAC,QAAQ,aAAa,OAAO,QACtC,uBAAuB,SAAS,gBAAgB,UAAU,IAAI,CAChE,GAAG;GACD,MAAM,SAAiC,SAAS,UAAU,mBAAmB,EAAE;GAE/E,UAAU,UAAU;IAAE,aAAa,SAAS;IAAa,QAAQ,UAAU;GAAK;EAClF;EAEA,OAAO;GACL;GACA,MAAM,MAAM;GACZ,MAAM,MAAM,OAAO,MAAM,OAAO;GAChC,YAAY,MAAM,aAAa,MAAM,aAAa;GAClD,SAAS,aAAa,OAAO;GAC7B,aAAa,MAAM,eAAe,SAAS,eAAe;GAC1D,QAAQ,MAAM,WAAW;GACzB,aAAa,MAAM,cAAc,CAAC,EAAC,CAAE,IAAI,YAAY;GACrD,MAAM,OACF;IAAE,UAAU,KAAK,aAAa;IAAO,SAAS,KAAK;IAAS,WAAW,KAAK;GAAU,IACtF;IAAE,UAAU;IAAO,SAAS,CAAC;IAAG,WAAW,CAAC;GAAE;GAClD,MAAM,MAAM,SAAS;GACrB,WAAW,cAAc,OAAO,SAAS;GACzC,SAAS,YAAY,OAAO,OAAO;GACnC,SAAS;IACP,YAAY,QAAQ,WAAW,KAAK,eAAe;KACjD,MAAM,UAAU;KAChB,IAAI,UAAU;KACd,UAAU,UAAU,aAAa;KACjC,QAAQ,UAAU;IACpB,EAAE;IACF,MAAM,QAAQ;IACd,kBAAkB,OAAO,YAAY,aAAa;GACpD;GACA;EACF;CACF,CAEe,CAAC,CAAC,MAAM,OAAO,WAAW;EACvC,IAAI,MAAM,SAAS,OAAO,MAAM,OAAO,MAAM,OAAO,OAAO,OAAO,KAAK;EAEvE,OAAO,WAAW,MAAM,MAAM,IAAI,WAAW,OAAO,MAAM;CAC5D,CAAC;AACH"}
1
+ {"version":3,"file":"collect-routes.mjs","names":[],"sources":["../../../../../../../../../packages/core/src/tooling/inspect/collect-routes.ts"],"sourcesContent":["import { config } from \"../../config/config-getter\";\nimport { buildRateLimitOptions } from \"../../http/build-rate-limit-options\";\nimport type { HttpLoggingConfig } from \"../../http/request-log-snapshot\";\nimport { expandMethods } from \"../../openapi/paths\";\nimport { buildRequestParts } from \"../../openapi/request-schemas\";\nimport { buildDeclaredResponses } from \"../../openapi/response-schemas\";\nimport type { SchemaRegistry } from \"../../openapi/schema-registry\";\nimport { readRouteAuth } from \"../../openapi/security\";\nimport { isRequiredGuardRoute } from \"../../router/unguarded-route-diagnostics\";\nimport type { Route } from \"../../router/types\";\nimport type {\n InspectRoute,\n InspectRouteLogging,\n InspectRouteRateLimit,\n InspectWarning,\n JsonSchema,\n} from \"./inspect-report.types\";\n\n/** Display order of methods within one path, matching `warlock routes`. */\nconst METHOD_ORDER = [\"GET\", \"POST\", \"PUT\", \"PATCH\", \"DELETE\", \"OPTIONS\", \"HEAD\", \"ALL\"];\n\nfunction methodRank(method: string): number {\n const index = METHOD_ORDER.indexOf(method);\n\n return index === -1 ? METHOD_ORDER.length : index;\n}\n\n/** The name of a function, without the `bound ` prefix `bind()` adds. */\nexport function functionName(value: unknown): string {\n if (typeof value !== \"function\") return \"anonymous\";\n\n const name = value.name.replace(/^bound /, \"\").trim();\n\n return name.length > 0 ? name : \"anonymous\";\n}\n\nfunction limitValue(value: unknown): number | \"dynamic\" | null {\n if (typeof value === \"number\") return value;\n\n return value === undefined || value === null ? null : \"dynamic\";\n}\n\nfunction windowValue(value: unknown): number | string | null {\n if (typeof value === \"number\" || typeof value === \"string\") return value;\n\n return value === undefined || value === null ? null : \"dynamic\";\n}\n\n/**\n * The limiter a route runs under: its own `rateLimit` options, else the global\n * `http.rateLimit` limiter (built with the same defaults the HTTP connector\n * uses), else none. `rateLimit: false` exempts the route from the global\n * limiter; `rateLimit: true` keeps it under the global policy.\n */\nfunction readRateLimit(\n route: Route,\n global: Record<string, unknown> | null,\n): InspectRouteRateLimit {\n if (typeof route.rateLimit === \"object\" && route.rateLimit !== null) {\n return { source: \"route\", max: route.rateLimit.max, timeWindow: route.rateLimit.timeWindow };\n }\n\n if (!global || route.rateLimit === false) {\n return { source: \"none\", max: null, timeWindow: null };\n }\n\n return {\n source: \"global\",\n max: limitValue(global.max),\n timeWindow: windowValue(global.timeWindow),\n };\n}\n\n/**\n * What the route's request logs keep: the route's `logging` option over\n * `http.logging`, the same precedence `resolveRequestLoggingOptions` applies.\n * Private URL rules can only remove more, per URL, so they are flagged rather\n * than evaluated.\n */\nfunction readLogging(route: Route, global: HttpLoggingConfig): InspectRouteLogging {\n return {\n body: route.logging?.body ?? global.body ?? true,\n query: route.logging?.query ?? global.query ?? true,\n privateRules: (global.private?.length ?? 0) > 0,\n };\n}\n\nfunction globalRateLimit(warnings: InspectWarning[]): Record<string, unknown> | null {\n try {\n const options = buildRateLimitOptions(config.get(\"http.rateLimit\"));\n\n return options ? { max: options.max, timeWindow: options.timeWindow } : null;\n } catch (error) {\n warnings.push({\n code: null,\n message: `http.rateLimit could not be read: ${error instanceof Error ? error.message : String(error)}`,\n source: \"routes\",\n });\n\n return null;\n }\n}\n\n/**\n * Describe every route: identity, middleware chain, guard, limits, logging and\n * the request/response schemas the OpenAPI generator derives (the same\n * `buildRequestParts` / `buildDeclaredResponses` code, so `inspect` and\n * `generate.openapi` never disagree). Resource `$ref`s land in `registry`.\n */\nexport function collectRoutes(\n routes: readonly Route[],\n registry: SchemaRegistry,\n warnings: InspectWarning[],\n): InspectRoute[] {\n const rateLimit = globalRateLimit(warnings);\n const logging = (config.get(\"http.logging\") ?? {}) as HttpLoggingConfig;\n\n const collected = routes.map((route): InspectRoute => {\n const method = String(route.method).toUpperCase();\n const label = `${method} ${route.path}`;\n const warn = (message: string) => {\n warnings.push({\n code: message.includes(\"JSON Schema\") ? \"WL4001\" : null,\n message: `${label}: ${message}`,\n source: route.sourceFile || null,\n });\n };\n const handler = route.handler as Route[\"handler\"] | undefined;\n const validation = handler?.validation;\n const auth = readRouteAuth(route.middleware, warn);\n const request = buildRequestParts({\n method: expandMethods(route.method)[0] ?? \"get\",\n path: route.path,\n validation,\n warn,\n });\n const body = request.requestBody?.content[\"application/json\"]?.schema;\n const responses: InspectRoute[\"responses\"] = {};\n\n for (const [status, response] of Object.entries(\n buildDeclaredResponses(handler?.responseSchema, registry, warn),\n )) {\n const schema: JsonSchema | undefined = response.content?.[\"application/json\"]?.schema;\n\n responses[status] = { description: response.description, schema: schema ?? null };\n }\n\n return {\n method,\n path: route.path,\n name: route.name ? route.name : null,\n sourceFile: route.sourceFile ? route.sourceFile : null,\n handler: functionName(handler),\n description: route.description ?? handler?.description ?? null,\n isPage: route.isPage === true,\n middleware: (route.middleware ?? []).map(functionName),\n // A guard that declared itself (`markAuthGuard`) counts even without a\n // `warlock.auth` descriptor; `route.middleware` already carries group middleware.\n auth: auth\n ? {\n required: auth.required !== false || isRequiredGuardRoute(route),\n sources: auth.sources,\n userTypes: auth.userTypes,\n }\n : { required: isRequiredGuardRoute(route), sources: [], userTypes: [] },\n csrf: route.csrf !== false,\n rateLimit: readRateLimit(route, rateLimit),\n logging: readLogging(route, logging),\n request: {\n parameters: request.parameters.map((parameter) => ({\n name: parameter.name,\n in: parameter.in,\n required: parameter.required === true,\n schema: parameter.schema,\n })),\n body: body ?? null,\n customValidation: typeof validation?.validate === \"function\",\n },\n responses,\n };\n });\n\n return collected.sort((first, second) => {\n if (first.path !== second.path) return first.path < second.path ? -1 : 1;\n\n return methodRank(first.method) - methodRank(second.method);\n });\n}\n"],"mappings":"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"}
@@ -4,6 +4,7 @@ import { loggerSyncCalls } from "./logger-sync-calls.mjs";
4
4
  import { node2215, warlockVersions } from "./package-json.mjs";
5
5
  import { oxcToolchain, oxcToolchainRemoval } from "./oxc-toolchain.mjs";
6
6
  import { requestLogSnapshot } from "./request-log-snapshot.mjs";
7
+ import { viteVitest } from "./vite-vitest.mjs";
7
8
  import { aiAnthropicBody, aiCompactionOptIn, aiFinishReason, aiToolDispatch, rateLimitDefaults, sealContainerCatch, sealRefineOutput, sealUnionOutput, sealValidateResult } from "./review-sites.mjs";
8
9
 
9
10
  //#region ../../packages/core/src/upgrade/codemods/index.ts
@@ -29,6 +30,7 @@ const CODEMODS = [
29
30
  aiCompactionOptIn,
30
31
  oxcToolchain,
31
32
  oxcToolchainRemoval,
33
+ viteVitest,
32
34
  warlockVersions
33
35
  ];
34
36
 
@@ -1 +1 @@
1
- {"version":3,"file":"index.mjs","names":[],"sources":["../../../../../../../../../packages/core/src/upgrade/codemods/index.ts"],"sourcesContent":["import type { Codemod } from \"../types\";\nimport { headViewport } from \"./head-viewport\";\nimport { loggerSyncCalls } from \"./logger-sync-calls\";\nimport { node2215, warlockVersions } from \"./package-json\";\nimport { oxcToolchain, oxcToolchainRemoval } from \"./oxc-toolchain\";\nimport { requestLogSnapshot } from \"./request-log-snapshot\";\nimport {\n aiAnthropicBody,\n aiCompactionOptIn,\n aiFinishReason,\n aiToolDispatch,\n rateLimitDefaults,\n sealContainerCatch,\n sealRefineOutput,\n sealUnionOutput,\n sealValidateResult,\n} from \"./review-sites\";\n\n/**\n * Every codemod, oldest release first. A codemod runs when the app upgrades\n * across its `version` (`from < version <= to`); `\"target\"` ones run on every\n * upgrade. Add one here for each breaking change, and name its id next to the\n * `**BREAKING**` entry in the package changelog.\n */\nexport const CODEMODS: readonly Codemod[] = [\n loggerSyncCalls,\n requestLogSnapshot,\n headViewport,\n sealValidateResult,\n aiToolDispatch,\n aiAnthropicBody,\n aiFinishReason,\n node2215,\n rateLimitDefaults,\n sealRefineOutput,\n sealContainerCatch,\n sealUnionOutput,\n aiCompactionOptIn,\n oxcToolchain,\n oxcToolchainRemoval,\n warlockVersions,\n];\n\nexport * from \"./helpers\";\n"],"mappings":";;;;;;;;;;;;;;;AAwBA,MAAa,WAA+B;CAC1C;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;AACF"}
1
+ {"version":3,"file":"index.mjs","names":[],"sources":["../../../../../../../../../packages/core/src/upgrade/codemods/index.ts"],"sourcesContent":["import type { Codemod } from \"../types\";\nimport { headViewport } from \"./head-viewport\";\nimport { loggerSyncCalls } from \"./logger-sync-calls\";\nimport { node2215, warlockVersions } from \"./package-json\";\nimport { oxcToolchain, oxcToolchainRemoval } from \"./oxc-toolchain\";\nimport { requestLogSnapshot } from \"./request-log-snapshot\";\nimport { viteVitest } from \"./vite-vitest\";\nimport {\n aiAnthropicBody,\n aiCompactionOptIn,\n aiFinishReason,\n aiToolDispatch,\n rateLimitDefaults,\n sealContainerCatch,\n sealRefineOutput,\n sealUnionOutput,\n sealValidateResult,\n} from \"./review-sites\";\n\n/**\n * Every codemod, oldest release first. A codemod runs when the app upgrades\n * across its `version` (`from < version <= to`); `\"target\"` ones run on every\n * upgrade. Add one here for each breaking change, and name its id next to the\n * `**BREAKING**` entry in the package changelog.\n */\nexport const CODEMODS: readonly Codemod[] = [\n loggerSyncCalls,\n requestLogSnapshot,\n headViewport,\n sealValidateResult,\n aiToolDispatch,\n aiAnthropicBody,\n aiFinishReason,\n node2215,\n rateLimitDefaults,\n sealRefineOutput,\n sealContainerCatch,\n sealUnionOutput,\n aiCompactionOptIn,\n oxcToolchain,\n oxcToolchainRemoval,\n viteVitest,\n warlockVersions,\n];\n\nexport * from \"./helpers\";\n"],"mappings":";;;;;;;;;;;;;;;;AAyBA,MAAa,WAA+B;CAC1C;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;AACF"}