@warlock.js/core 5.36.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.
- package/CHANGELOG.md +28 -0
- package/esm/cli/commands/migrate.command.mjs +10 -0
- package/esm/cli/commands/migrate.command.mjs.map +1 -1
- package/esm/cli/commands/release/release-command.action.mjs +245 -0
- package/esm/cli/commands/release/release-command.action.mjs.map +1 -0
- package/esm/cli/commands/release.command.mjs +66 -0
- package/esm/cli/commands/release.command.mjs.map +1 -0
- package/esm/cli/framework-cli-commands.mjs +2 -0
- package/esm/cli/framework-cli-commands.mjs.map +1 -1
- package/esm/database/migrate-action.mjs +24 -2
- package/esm/database/migrate-action.mjs.map +1 -1
- package/esm/dev-server/loader/load-hook.d.mts.map +1 -1
- package/esm/dev-server/loader/load-hook.mjs +6 -0
- package/esm/dev-server/loader/load-hook.mjs.map +1 -1
- package/esm/index.d.mts +3 -2
- package/esm/index.mjs +2 -2
- package/esm/production/route-registration-snapshot.mjs +10 -3
- package/esm/production/route-registration-snapshot.mjs.map +1 -1
- package/esm/release/activate.mjs +261 -0
- package/esm/release/activate.mjs.map +1 -0
- package/esm/release/candidate.mjs +107 -0
- package/esm/release/candidate.mjs.map +1 -0
- package/esm/release/cascade-migration-provider.mjs +32 -0
- package/esm/release/cascade-migration-provider.mjs.map +1 -0
- package/esm/release/config.mjs +60 -0
- package/esm/release/config.mjs.map +1 -0
- package/esm/release/gate.mjs +67 -0
- package/esm/release/gate.mjs.map +1 -0
- package/esm/release/migrations-proof.mjs +100 -0
- package/esm/release/migrations-proof.mjs.map +1 -0
- package/esm/release/prune.mjs +42 -0
- package/esm/release/prune.mjs.map +1 -0
- package/esm/release/report.mjs +25 -0
- package/esm/release/report.mjs.map +1 -0
- package/esm/release/runner.mjs +55 -0
- package/esm/release/runner.mjs.map +1 -0
- package/esm/release/semver.mjs +53 -0
- package/esm/release/semver.mjs.map +1 -0
- package/esm/release/stage.mjs +122 -0
- package/esm/release/stage.mjs.map +1 -0
- package/esm/release/store.mjs +131 -0
- package/esm/release/store.mjs.map +1 -0
- package/esm/release/types.d.mts +102 -0
- package/esm/release/types.d.mts.map +1 -0
- package/esm/release/verify.mjs +94 -0
- package/esm/release/verify.mjs.map +1 -0
- package/esm/router/index.d.mts +2 -2
- package/esm/router/index.mjs +2 -2
- package/esm/router/router.d.mts +4 -2
- package/esm/router/router.d.mts.map +1 -1
- package/esm/router/router.mjs +13 -5
- package/esm/router/router.mjs.map +1 -1
- package/esm/router/types.d.mts +26 -0
- package/esm/router/types.d.mts.map +1 -1
- package/esm/router/unguarded-route-diagnostics.d.mts +6 -0
- package/esm/router/unguarded-route-diagnostics.d.mts.map +1 -1
- package/esm/router/unguarded-route-diagnostics.mjs +10 -2
- package/esm/router/unguarded-route-diagnostics.mjs.map +1 -1
- package/esm/tooling/check/security-conventions.mjs +19 -1
- package/esm/tooling/check/security-conventions.mjs.map +1 -1
- package/esm/tooling/inspect/collect-routes.mjs +3 -2
- package/esm/tooling/inspect/collect-routes.mjs.map +1 -1
- package/esm/validation/init.mjs +11 -4
- package/esm/validation/init.mjs.map +1 -1
- package/esm/validation/translate-rule.mjs +63 -0
- package/esm/validation/translate-rule.mjs.map +1 -0
- package/esm/warlock-config/index.d.mts +2 -1
- package/esm/warlock-config/types.d.mts +8 -0
- package/esm/warlock-config/types.d.mts.map +1 -1
- package/llms-full.txt +94 -2
- package/llms.txt +1 -0
- package/package.json +13 -13
- package/skills/deploy-release/SKILL.md +76 -0
- package/skills/index.md +1 -1
- package/skills/register-route/SKILL.md +12 -0
- package/skills/warlock-conventions/SKILL.md +1 -1
|
@@ -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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"}
|
|
@@ -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:
|
|
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":"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"}
|
|
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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"}
|
package/esm/validation/init.mjs
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { config } from "../config/config-getter.mjs";
|
|
2
2
|
import "../config/index.mjs";
|
|
3
3
|
import { t } from "../http/middleware/inject-request-context.mjs";
|
|
4
|
+
import { translateSealRule } from "./translate-rule.mjs";
|
|
4
5
|
import { databasePlugin } from "./plugins/database-plugin.mjs";
|
|
5
6
|
import { filePlugin } from "./plugins/file-plugin.mjs";
|
|
6
7
|
import { localizedPlugin } from "./plugins/localized-plugin.mjs";
|
|
@@ -14,15 +15,21 @@ import { configureSeal, getSealConfig, registerPlugin } from "@warlock.js/seal";
|
|
|
14
15
|
* This file configures Seal to use Warlock's localization system
|
|
15
16
|
*/
|
|
16
17
|
configureSeal({
|
|
17
|
-
translateRule({ rule, attributes }) {
|
|
18
|
+
translateRule({ rule, attributes, context }) {
|
|
18
19
|
const translateRule = config.key("validation.translateRule");
|
|
19
20
|
if (translateRule) return translateRule({
|
|
20
21
|
rule,
|
|
21
|
-
attributes
|
|
22
|
+
attributes,
|
|
23
|
+
context
|
|
22
24
|
});
|
|
23
25
|
const translationKey = `${config.key("validation.translationGroup", "validation")}.${rule.name}`;
|
|
24
|
-
|
|
25
|
-
|
|
26
|
+
return translateSealRule({
|
|
27
|
+
rule,
|
|
28
|
+
attributes,
|
|
29
|
+
context,
|
|
30
|
+
key: translationKey,
|
|
31
|
+
translate: t
|
|
32
|
+
});
|
|
26
33
|
},
|
|
27
34
|
translateAttribute({ attribute, context, rule }) {
|
|
28
35
|
const translateAttribute = config.key("validation.translateAttribute");
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"init.mjs","names":[],"sources":["../../../../../../../../packages/core/src/validation/init.ts"],"sourcesContent":["/**\n * Initialize Seal with Warlock Framework Settings\n *\n * This file configures Seal to use Warlock's localization system\n */\n\n// Auto-register framework plugins\nimport { configureSeal, getSealConfig, registerPlugin } from \"@warlock.js/seal\";\nimport { config } from \"../config\";\nimport { t } from \"../http/middleware/inject-request-context\";\nimport { databasePlugin, filePlugin, localizedPlugin } from \"./plugins\";\n\n// Configure Seal to use Warlock's localization\nconfigureSeal({\n translateRule({ rule, attributes }) {\n const translateRule = config.key(\"validation.translateRule\");\n\n if (translateRule) {\n return translateRule({ rule, attributes });\n }\n\n const translationGroup = config.key(\"validation.translationGroup\", \"validation\");\n\n const translationKey = `${translationGroup}.${rule.name}`;\n
|
|
1
|
+
{"version":3,"file":"init.mjs","names":[],"sources":["../../../../../../../../packages/core/src/validation/init.ts"],"sourcesContent":["/**\n * Initialize Seal with Warlock Framework Settings\n *\n * This file configures Seal to use Warlock's localization system\n */\n\n// Auto-register framework plugins\nimport { configureSeal, getSealConfig, registerPlugin } from \"@warlock.js/seal\";\nimport { config } from \"../config\";\nimport { t } from \"../http/middleware/inject-request-context\";\nimport { translateSealRule } from \"./translate-rule\";\nimport { databasePlugin, filePlugin, localizedPlugin } from \"./plugins\";\n\n// Configure Seal to use Warlock's localization\nconfigureSeal({\n translateRule({ rule, attributes, context }) {\n const translateRule = config.key(\"validation.translateRule\");\n\n if (translateRule) {\n return translateRule({ rule, attributes, context });\n }\n\n const translationGroup = config.key(\"validation.translationGroup\", \"validation\");\n\n const translationKey = `${translationGroup}.${rule.name}`;\n\n return translateSealRule({ rule, attributes, context, key: translationKey, translate: t });\n },\n\n translateAttribute({ attribute, context, rule }) {\n const translateAttribute = config.key(\"validation.translateAttribute\");\n\n if (translateAttribute) {\n return translateAttribute({ attribute, context, rule });\n }\n\n const attributeGroup = config.key(\"validation.attributeGroup\") ?? \"attributes\";\n\n const translationKey = `${attributeGroup ? attributeGroup + \".\" : \"\"}${attribute}`;\n\n const output = t(translationKey, context.allValues);\n return output === translationKey ? attribute : output;\n },\n});\n\n// App config is not loaded yet when this module is imported, so resolve\n// firstErrorOnly lazily on every read instead of capturing it once.\nObject.defineProperty(getSealConfig(), \"firstErrorOnly\", {\n configurable: true,\n enumerable: true,\n get: () => config.key(\"validation.firstErrorOnly\", true),\n});\n\n// Register plugins to inject methods\nregisterPlugin(databasePlugin);\nregisterPlugin(filePlugin);\nregisterPlugin(localizedPlugin);\n"],"mappings":";;;;;;;;;;;;;;;;AAcA,cAAc;CACZ,cAAc,EAAE,MAAM,YAAY,WAAW;EAC3C,MAAM,gBAAgB,OAAO,IAAI,0BAA0B;EAE3D,IAAI,eACF,OAAO,cAAc;GAAE;GAAM;GAAY;EAAQ,CAAC;EAKpD,MAAM,iBAAiB,GAFE,OAAO,IAAI,+BAA+B,YAE1B,EAAE,GAAG,KAAK;EAEnD,OAAO,kBAAkB;GAAE;GAAM;GAAY;GAAS,KAAK;GAAgB,WAAW;EAAE,CAAC;CAC3F;CAEA,mBAAmB,EAAE,WAAW,SAAS,QAAQ;EAC/C,MAAM,qBAAqB,OAAO,IAAI,+BAA+B;EAErE,IAAI,oBACF,OAAO,mBAAmB;GAAE;GAAW;GAAS;EAAK,CAAC;EAGxD,MAAM,iBAAiB,OAAO,IAAI,2BAA2B,KAAK;EAElE,MAAM,iBAAiB,GAAG,iBAAiB,iBAAiB,MAAM,KAAK;EAEvE,MAAM,SAAS,EAAE,gBAAgB,QAAQ,SAAS;EAClD,OAAO,WAAW,iBAAiB,YAAY;CACjD;AACF,CAAC;AAID,OAAO,eAAe,cAAc,GAAG,kBAAkB;CACvD,cAAc;CACd,YAAY;CACZ,WAAW,OAAO,IAAI,6BAA6B,IAAI;AACzD,CAAC;AAGD,eAAe,cAAc;AAC7B,eAAe,UAAU;AACzB,eAAe,eAAe"}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { environment } from "../utils/environment.mjs";
|
|
2
|
+
import { log } from "@warlock.js/logger";
|
|
3
|
+
|
|
4
|
+
//#region ../../packages/core/src/validation/translate-rule.ts
|
|
5
|
+
const PLACEHOLDER = /:([a-zA-Z_]+)/g;
|
|
6
|
+
/**
|
|
7
|
+
* Placeholder names other validation message sets use for the values seal
|
|
8
|
+
* exposes under a different name. Keyed by rule name; each entry maps the
|
|
9
|
+
* alias to the seal attribute it reads.
|
|
10
|
+
*/
|
|
11
|
+
const PLACEHOLDER_ALIASES = {
|
|
12
|
+
minLength: { length: "minLength" },
|
|
13
|
+
maxLength: { length: "maxLength" },
|
|
14
|
+
strongMinLength: { length: "minLength" },
|
|
15
|
+
match: { matchingInput: "field" }
|
|
16
|
+
};
|
|
17
|
+
const warnedKeys = /* @__PURE__ */ new Set();
|
|
18
|
+
/** Seal attributes plus the alias names other message sets use for them. */
|
|
19
|
+
function withPlaceholderAliases(ruleName, attributes) {
|
|
20
|
+
const aliases = PLACEHOLDER_ALIASES[ruleName];
|
|
21
|
+
if (!aliases) return attributes;
|
|
22
|
+
const output = { ...attributes };
|
|
23
|
+
for (const [alias, source] of Object.entries(aliases)) if (!(alias in output) && source in attributes) output[alias] = attributes[source];
|
|
24
|
+
return output;
|
|
25
|
+
}
|
|
26
|
+
/** The `:placeholder` names in a message that `attributes` cannot fill. */
|
|
27
|
+
function unfilledPlaceholders(message, attributes) {
|
|
28
|
+
const names = /* @__PURE__ */ new Set();
|
|
29
|
+
for (const match of message.matchAll(PLACEHOLDER)) {
|
|
30
|
+
const name = match[1];
|
|
31
|
+
if (name !== void 0 && !(name in attributes)) names.add(name);
|
|
32
|
+
}
|
|
33
|
+
return [...names];
|
|
34
|
+
}
|
|
35
|
+
function warnUnfilled(key, message, names) {
|
|
36
|
+
if (environment() !== "development" || warnedKeys.has(key)) return;
|
|
37
|
+
warnedKeys.add(key);
|
|
38
|
+
log.warn("validation", "placeholder", `Validation message for "${key}" still contains ${names.map((name) => `:${name}`).join(", ")} after interpolation: "${message}". Check the translation placeholders against the rule's parameters.`);
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Seal's default rule translation for Warlock. A translation is used only when
|
|
42
|
+
* every `:placeholder` in it can be filled from the rule's parameters (after
|
|
43
|
+
* mapping the alias names other message sets use, such as `:length` for
|
|
44
|
+
* `:maxLength`). Otherwise seal's own message is used, so a message written
|
|
45
|
+
* for a different library (for example the browser form defaults that share the
|
|
46
|
+
* process-wide translation store) never reaches an API response with a raw
|
|
47
|
+
* placeholder in it. In development, the first time a key falls back this way
|
|
48
|
+
* a warning names the key.
|
|
49
|
+
*/
|
|
50
|
+
function translateSealRule({ rule, attributes, key, translate }) {
|
|
51
|
+
const own = rule.errorMessage || rule.defaultErrorMessage || key;
|
|
52
|
+
const aliased = withPlaceholderAliases(rule.name, attributes);
|
|
53
|
+
const translation = translate(key, aliased);
|
|
54
|
+
if (translation === key) return own;
|
|
55
|
+
const leftover = unfilledPlaceholders(translation, aliased);
|
|
56
|
+
if (leftover.length === 0) return translation;
|
|
57
|
+
warnUnfilled(key, translation, leftover);
|
|
58
|
+
return own;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
//#endregion
|
|
62
|
+
export { translateSealRule, withPlaceholderAliases };
|
|
63
|
+
//# sourceMappingURL=translate-rule.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"translate-rule.mjs","names":[],"sources":["../../../../../../../../packages/core/src/validation/translate-rule.ts"],"sourcesContent":["import { log } from \"@warlock.js/logger\";\nimport type { RuleTranslation } from \"@warlock.js/seal\";\nimport { environment } from \"../utils/environment\";\n\nconst PLACEHOLDER = /:([a-zA-Z_]+)/g;\n\n/**\n * Placeholder names other validation message sets use for the values seal\n * exposes under a different name. Keyed by rule name; each entry maps the\n * alias to the seal attribute it reads.\n */\nconst PLACEHOLDER_ALIASES: Record<string, Record<string, string>> = {\n minLength: { length: \"minLength\" },\n maxLength: { length: \"maxLength\" },\n strongMinLength: { length: \"minLength\" },\n match: { matchingInput: \"field\" },\n};\n\nconst warnedKeys = new Set<string>();\n\n/** Test-only: forget which keys already produced a warning. */\nexport function resetUnfilledPlaceholderWarningsForTests(): void {\n warnedKeys.clear();\n}\n\n/** Seal attributes plus the alias names other message sets use for them. */\nexport function withPlaceholderAliases(\n ruleName: string,\n attributes: RuleTranslation[\"attributes\"],\n): RuleTranslation[\"attributes\"] {\n const aliases = PLACEHOLDER_ALIASES[ruleName];\n\n if (!aliases) return attributes;\n\n const output = { ...attributes };\n\n for (const [alias, source] of Object.entries(aliases)) {\n if (!(alias in output) && source in attributes) {\n output[alias] = attributes[source];\n }\n }\n\n return output;\n}\n\n/** The `:placeholder` names in a message that `attributes` cannot fill. */\nfunction unfilledPlaceholders(message: string, attributes: Record<string, unknown>): string[] {\n const names = new Set<string>();\n\n for (const match of message.matchAll(PLACEHOLDER)) {\n const name = match[1];\n\n if (name !== undefined && !(name in attributes)) names.add(name);\n }\n\n return [...names];\n}\n\nfunction warnUnfilled(key: string, message: string, names: string[]): void {\n if (environment() !== \"development\" || warnedKeys.has(key)) return;\n\n warnedKeys.add(key);\n\n log.warn(\n \"validation\",\n \"placeholder\",\n `Validation message for \"${key}\" still contains ${names\n .map((name) => `:${name}`)\n .join(\", \")} after interpolation: \"${message}\". ` +\n \"Check the translation placeholders against the rule's parameters.\",\n );\n}\n\nexport type TranslateSealRuleOptions = RuleTranslation & {\n /** Translation key to look up, e.g. `validation.maxLength`. */\n key: string;\n /** Translates the key with the given placeholders (the request-aware `t`). */\n translate: (key: string, placeholders: Record<string, unknown>) => string;\n};\n\n/**\n * Seal's default rule translation for Warlock. A translation is used only when\n * every `:placeholder` in it can be filled from the rule's parameters (after\n * mapping the alias names other message sets use, such as `:length` for\n * `:maxLength`). Otherwise seal's own message is used, so a message written\n * for a different library (for example the browser form defaults that share the\n * process-wide translation store) never reaches an API response with a raw\n * placeholder in it. In development, the first time a key falls back this way\n * a warning names the key.\n */\nexport function translateSealRule({\n rule,\n attributes,\n key,\n translate,\n}: TranslateSealRuleOptions): string {\n const own = rule.errorMessage || rule.defaultErrorMessage || key;\n const aliased = withPlaceholderAliases(rule.name, attributes);\n const translation = translate(key, aliased);\n\n if (translation === key) return own;\n\n const leftover = unfilledPlaceholders(translation, aliased);\n\n if (leftover.length === 0) return translation;\n\n warnUnfilled(key, translation, leftover);\n\n return own;\n}\n"],"mappings":";;;;AAIA,MAAM,cAAc;;;;;;AAOpB,MAAM,sBAA8D;CAClE,WAAW,EAAE,QAAQ,YAAY;CACjC,WAAW,EAAE,QAAQ,YAAY;CACjC,iBAAiB,EAAE,QAAQ,YAAY;CACvC,OAAO,EAAE,eAAe,QAAQ;AAClC;AAEA,MAAM,6BAAa,IAAI,IAAY;;AAQnC,SAAgB,uBACd,UACA,YAC+B;CAC/B,MAAM,UAAU,oBAAoB;CAEpC,IAAI,CAAC,SAAS,OAAO;CAErB,MAAM,SAAS,EAAE,GAAG,WAAW;CAE/B,KAAK,MAAM,CAAC,OAAO,WAAW,OAAO,QAAQ,OAAO,GAClD,IAAI,EAAE,SAAS,WAAW,UAAU,YAClC,OAAO,SAAS,WAAW;CAI/B,OAAO;AACT;;AAGA,SAAS,qBAAqB,SAAiB,YAA+C;CAC5F,MAAM,wBAAQ,IAAI,IAAY;CAE9B,KAAK,MAAM,SAAS,QAAQ,SAAS,WAAW,GAAG;EACjD,MAAM,OAAO,MAAM;EAEnB,IAAI,SAAS,UAAa,EAAE,QAAQ,aAAa,MAAM,IAAI,IAAI;CACjE;CAEA,OAAO,CAAC,GAAG,KAAK;AAClB;AAEA,SAAS,aAAa,KAAa,SAAiB,OAAuB;CACzE,IAAI,YAAY,MAAM,iBAAiB,WAAW,IAAI,GAAG,GAAG;CAE5D,WAAW,IAAI,GAAG;CAElB,IAAI,KACF,cACA,eACA,2BAA2B,IAAI,mBAAmB,MAC/C,KAAK,SAAS,IAAI,MAAM,CAAC,CACzB,KAAK,IAAI,EAAE,yBAAyB,QAAQ,qEAEjD;AACF;;;;;;;;;;;AAmBA,SAAgB,kBAAkB,EAChC,MACA,YACA,KACA,aACmC;CACnC,MAAM,MAAM,KAAK,gBAAgB,KAAK,uBAAuB;CAC7D,MAAM,UAAU,uBAAuB,KAAK,MAAM,UAAU;CAC5D,MAAM,cAAc,UAAU,KAAK,OAAO;CAE1C,IAAI,gBAAgB,KAAK,OAAO;CAEhC,MAAM,WAAW,qBAAqB,aAAa,OAAO;CAE1D,IAAI,SAAS,WAAW,GAAG,OAAO;CAElC,aAAa,KAAK,aAAa,QAAQ;CAEvC,OAAO;AACT"}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { BudgetPage, BudgetViolation, BuildBudgetReport, BuildBudgets, PageBudget, PayloadBudget } from "../production/build-budgets.mjs";
|
|
2
|
+
import { ReleaseCheck, ReleaseConfig } from "../release/types.mjs";
|
|
2
3
|
import { WarlockConfig } from "./types.mjs";
|
|
3
4
|
import { defineConfig } from "./define-config.mjs";
|
|
4
5
|
import { WarlockConfigManager, isUnknownTsExtensionError, isUnresolvedConfigImportError, isUnsupportedTypeStrippingError, warlockConfigManager } from "./warlock-config.manager.mjs";
|
|
5
|
-
export { type BudgetPage, type BudgetViolation, type BuildBudgetReport, type BuildBudgets, type PageBudget, type PayloadBudget, WarlockConfig, WarlockConfigManager, defineConfig, isUnknownTsExtensionError, isUnresolvedConfigImportError, isUnsupportedTypeStrippingError, warlockConfigManager };
|
|
6
|
+
export { type BudgetPage, type BudgetViolation, type BuildBudgetReport, type BuildBudgets, type PageBudget, type PayloadBudget, type ReleaseCheck, type ReleaseConfig, WarlockConfig, WarlockConfigManager, defineConfig, isUnknownTsExtensionError, isUnresolvedConfigImportError, isUnsupportedTypeStrippingError, warlockConfigManager };
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { BuildBudgets } from "../production/build-budgets.mjs";
|
|
2
|
+
import { ReleaseConfig } from "../release/types.mjs";
|
|
2
3
|
import { BuiltInHealthCheckerOptions, FileHealthCheckerContract } from "../dev-server/health-checker/file-health-checker.contract.mjs";
|
|
3
4
|
import { Connector } from "../connectors/types.mjs";
|
|
4
5
|
import { CLICommand } from "../commands/cli-command.mjs";
|
|
@@ -270,6 +271,13 @@ export type WarlockConfig = {
|
|
|
270
271
|
*/
|
|
271
272
|
migrations?: Array<MigrationConstructor>;
|
|
272
273
|
};
|
|
274
|
+
/**
|
|
275
|
+
* Release pipeline settings for `warlock release`: candidate, gate,
|
|
276
|
+
* migration proof, stage, activate, verify and rollback.
|
|
277
|
+
*
|
|
278
|
+
* @see ReleaseConfig
|
|
279
|
+
*/
|
|
280
|
+
release?: ReleaseConfig;
|
|
273
281
|
/**
|
|
274
282
|
* Testing configuration
|
|
275
283
|
*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.mts","names":[],"sources":["../../../../../../../../packages/core/src/warlock-config/types.ts"],"mappings":"
|
|
1
|
+
{"version":3,"file":"types.d.mts","names":[],"sources":["../../../../../../../../packages/core/src/warlock-config/types.ts"],"mappings":";;;;;;;;;;;;;YAgBY;;;;EAIV;;;;IAIE;;;;IAIA;;;;IAIA;;;;;;;;;;;;;EAcF,QAAQ,KAAK;;;;;;IAMX;;IAEA,UAAU;;;;;;;;;;;IAWV;;;;;;IAMA;;;;;;;IAOA;;;;;;;;;;;;;;;;;IAiBA;;;;;;;;;;;;;;;;IAgBA;;;;;;IAMA;;;;;;IAMA;;;;;;;;;;;;;;;;;;;;;;;;;;EA2BF,aAAa;;;;EAKb;IACE,WAAW;;;;;EAMb;;;;IAIE;;;;;;MAME;;;;;;MAMA;;;;;;IAMF,iBAAiB,8BAA8B;;;;;IAK/C;;;;;;;;;IASA;;;;;;;;;IASA;;;;;;;;;;IAUA;;;;;;;;;;;;IAYA;;;;;;;;;;IAUA;;;;;;;;;IASA;;MAIM;;MAEA;;MAEA;;;;;;EAOR;;;;;;;;;;;;;;;;;;IAkBE,aAAa,MAAM;;;;;;;;EASrB,UAAU;;;;;;;EAQV;;;;;;;;;IASE;;;;;;;;IASA"}
|
package/llms-full.txt
CHANGED
|
@@ -30,7 +30,7 @@ When the framework is wrong or missing a capability, follow the `report-framewor
|
|
|
30
30
|
- **HTTP & routing:** register-route, warlock-routes, create-controller, use-middleware, write-middleware, validate-input, use-request-locals, build-restful, wire-socket
|
|
31
31
|
- **Responses & resources:** send-response, define-resource, use-model-transformers, use-localization
|
|
32
32
|
- **Domain layers:** write-use-case, use-repository, write-seeder
|
|
33
|
-
- **CLI & running the app:** generate-web-ui, run-app, write-cli-command, add-connector, health-checks, warlock-doctor, warlock-inspect, warlock-check, generate-openapi
|
|
33
|
+
- **CLI & running the app:** generate-web-ui, run-app, deploy-release, write-cli-command, add-connector, health-checks, warlock-doctor, warlock-inspect, warlock-check, generate-openapi
|
|
34
34
|
- **Agent tooling:** warlock-mcp, warlock-trace, warlock-agents-md, warlock-board
|
|
35
35
|
- **Storage, files & mail:** store-file, upload-file, process-image, send-mail
|
|
36
36
|
- **Security & utilities:** hash-password, encrypt-data, store-secrets, retry-operation, benchmark-code, request-memo, request-tracing
|
|
@@ -2487,6 +2487,86 @@ export const CommentResource = defineResource({
|
|
|
2487
2487
|
- The `create-controller` topic — calling `new XResource(record).toJSON()` from a controller.
|
|
2488
2488
|
|
|
2489
2489
|
|
|
2490
|
+
## deploy-release `@warlock.js/core/skills/deploy-release/SKILL.md`
|
|
2491
|
+
|
|
2492
|
+
---
|
|
2493
|
+
name: deploy-release
|
|
2494
|
+
description: 'Deploy a Warlock application with `warlock release` — freeze a clean commit as a candidate, run the gate, prove migrations, stage the build, switch `current` by atomic symlink, verify the health URL and roll back, configured by the `release` section of `warlock.config.ts`. Triggers: `warlock release`, `release candidate`, `release gate`, `release migrations --prove`, `release stage`, `release activate`, `release verify`, `release rollback`, `release prune`, `ReleaseConfig`, `release.restart.command`, `release.verify`, `.warlock/releases`; "deploy my app", "zero-downtime deploy", "roll back a deploy", "atomic release", "release gate", "prove migrations before deploy". Skip: the `/health` and `/ready` endpoints themselves — the `health-checks` topic; generating a PM2 ecosystem file — `warlock pm2` (the `run-app` topic); publishing the framework packages — `releasing-warlock-monorepo`; competing tools Capistrano, Deployer, hand-rolled symlink scripts.'
|
|
2495
|
+
---
|
|
2496
|
+
|
|
2497
|
+
# Warlock — deploy with `warlock release`
|
|
2498
|
+
|
|
2499
|
+
A recorded, step-by-step deploy for one host. Each step is a command; each writes into a per-release `manifest.json` and a shared `release.log`.
|
|
2500
|
+
|
|
2501
|
+
```text
|
|
2502
|
+
candidate -> gate -> migrations --prove -> stage -> activate -> verify
|
|
2503
|
+
\-> rollback
|
|
2504
|
+
```
|
|
2505
|
+
|
|
2506
|
+
Warlock never calls PM2, systemd or any process manager. The only restart is the command you set in `release.restart.command`.
|
|
2507
|
+
|
|
2508
|
+
## Configure
|
|
2509
|
+
|
|
2510
|
+
```ts title="warlock.config.ts"
|
|
2511
|
+
import { defineConfig } from "@warlock.js/core";
|
|
2512
|
+
|
|
2513
|
+
export default defineConfig({
|
|
2514
|
+
release: {
|
|
2515
|
+
gate: {
|
|
2516
|
+
checks: [
|
|
2517
|
+
{ name: "typecheck", command: "npx warlock typecheck" },
|
|
2518
|
+
{ name: "test", command: "npx warlock test" },
|
|
2519
|
+
{ name: "build", command: "NODE_ENV=production npx warlock build" },
|
|
2520
|
+
],
|
|
2521
|
+
},
|
|
2522
|
+
restart: { command: "pm2 reload my-app" },
|
|
2523
|
+
verify: { url: "http://127.0.0.1:2030/health" },
|
|
2524
|
+
},
|
|
2525
|
+
});
|
|
2526
|
+
```
|
|
2527
|
+
|
|
2528
|
+
Defaults: `directory` `.warlock`, `distDir` `dist`, gate = typecheck, check, test, build. Add `.warlock` to `.gitignore`.
|
|
2529
|
+
|
|
2530
|
+
## Run the steps
|
|
2531
|
+
|
|
2532
|
+
```bash
|
|
2533
|
+
npx warlock release candidate [--commit <sha>] [--allow-dirty]
|
|
2534
|
+
npx warlock release gate
|
|
2535
|
+
npx warlock release migrations --prove --target staging
|
|
2536
|
+
npx warlock release stage
|
|
2537
|
+
npx warlock release activate --preflight
|
|
2538
|
+
npx warlock release activate [--dry-run] [--allow-same]
|
|
2539
|
+
npx warlock release verify || npx warlock release rollback
|
|
2540
|
+
```
|
|
2541
|
+
|
|
2542
|
+
Every command takes an optional release id (`release gate 1.4.0-a1b2c3d`); without one it uses the newest candidate, gated or staged release. `--json` prints the result as JSON; a failed step exits 1.
|
|
2543
|
+
|
|
2544
|
+
- **candidate** freezes the commit with `git archive` into `.warlock/releases/<version>-<shortsha>/source`. Refuses a dirty tree without `--allow-dirty` and never overwrites a release.
|
|
2545
|
+
- **gate** runs the checks in order in the frozen source and stops at the first failure; results go into the manifest.
|
|
2546
|
+
- **migrations** without `--prove` only lists the pending set. With `--prove` it snapshots the ledger, tables and indexes, runs the pending migrations, snapshots again and compares with the expected set (`release.migrations.expected`, default the pending set; `expectedTables`). The migrations really run: use `--target` (a `release.migrations.targets` entry whose `env` is applied before the database connects) on a database that is safe to migrate. A failed proof blocks activation.
|
|
2547
|
+
- **stage** (needs a passing gate) copies `dist`, `package.json`, the lockfile, `release.include` entries and `release.json` into `staged/`, atomically.
|
|
2548
|
+
- **activate** needs staged artefacts, a passed gate (and proof), and a version strictly greater than the active one (semver; `--allow-same` accepts an equal version, a lower one is refused). It switches `current -> releases/<id>` with an atomic rename, keeps the old target as `previous`, then runs the restart command with `WARLOCK_RELEASE_ID`, `WARLOCK_RELEASE_DIR` and `WARLOCK_RELEASE_CURRENT` set. If that command fails, the pointer stays switched and the exit code is 1.
|
|
2549
|
+
- **verify** GETs `release.verify.url` with retries and a timeout and checks the status plus the `version` and `commit` fields of the JSON body against the active release (`verify.versionPath`, `commitPath` and `match` adjust that). Serve the `release.json` values from your health endpoint.
|
|
2550
|
+
- **rollback** switches `current` back to `previous` (and `previous` to the release just left), reruns the restart command. It does not roll migrations back: a migration may have destroyed data no reverse step restores; use `warlock migrate --rollback` deliberately if needed.
|
|
2551
|
+
- **list** and **status** show releases and the active one; **prune --keep <n>** is the only command that deletes releases and always keeps `current` and `previous`.
|
|
2552
|
+
|
|
2553
|
+
## Layout
|
|
2554
|
+
|
|
2555
|
+
```text
|
|
2556
|
+
.warlock/current -> releases/<id> .warlock/previous -> releases/<id>
|
|
2557
|
+
.warlock/release.log
|
|
2558
|
+
.warlock/releases/<id>/{manifest.json, source/, staged/}
|
|
2559
|
+
```
|
|
2560
|
+
|
|
2561
|
+
Run the app from `.warlock/current/staged` so a restart follows the pointer.
|
|
2562
|
+
|
|
2563
|
+
## Pitfalls
|
|
2564
|
+
|
|
2565
|
+
- Run `activate --dry-run` or `--preflight` first in CI; both change nothing.
|
|
2566
|
+
- Without `restart.command` activation only moves the symlink; the running process keeps serving the old code.
|
|
2567
|
+
- `verify` fails when the response does not report a version; set `verify.match` to `[]` to check only the status.
|
|
2568
|
+
|
|
2569
|
+
|
|
2490
2570
|
## encrypt-data `@warlock.js/core/skills/encrypt-data/SKILL.md`
|
|
2491
2571
|
|
|
2492
2572
|
---
|
|
@@ -4027,6 +4107,18 @@ router.post("/internal/reindex", reindexController, {
|
|
|
4027
4107
|
|
|
4028
4108
|
It is then never embedded in a page, and `warlock check` reports a browser reference to its name as `WL9020` (an error). The server still authorizes the call as for any route.
|
|
4029
4109
|
|
|
4110
|
+
### Offer a route to some sites only — `sites`
|
|
4111
|
+
|
|
4112
|
+
In a multi-site app a route answers on every host, and a page of any site may embed it in its browser table. Declare the sites that may embed it on the route or on a group; a route's own `sites` replaces its group's, and a route with none is shared:
|
|
4113
|
+
|
|
4114
|
+
```ts
|
|
4115
|
+
router.group({ prefix: "/api/admin", sites: ["admin"] }, () => {
|
|
4116
|
+
router.post("/users", createUser, { name: "admin.users.create" });
|
|
4117
|
+
});
|
|
4118
|
+
```
|
|
4119
|
+
|
|
4120
|
+
This limits the browser table only; keep the route's guard. A guard in the route's middleware chain (group middleware included) also keeps it out of a guest's table, and a custom guard must say so with `markAuthGuard(fn)` unless its name contains `auth` or `guard`. `warlock inspect`, `warlock agents-md` and `WL9006` read the same chain, so a guard declared once on a group guards every route inside it.
|
|
4121
|
+
|
|
4030
4122
|
|
|
4031
4123
|
## report-framework-issue `@warlock.js/core/skills/report-framework-issue/SKILL.md`
|
|
4032
4124
|
|
|
@@ -12582,7 +12674,7 @@ router.post("/posts", createPostController, { middleware: [authMiddleware("user"
|
|
|
12582
12674
|
router.post("/contact", contactController, { public: true });
|
|
12583
12675
|
```
|
|
12584
12676
|
|
|
12585
|
-
A route inside `guarded(() => …)` or `router.group({ middleware: [authMiddleware()] }, …)` is guarded. A middleware counts as a guard when its name contains `auth` or `guard`, or when it is a constant initialised with `markAuthGuard()` in the same file. `warlock dev` warns about the same routes at runtime.
|
|
12677
|
+
A route inside `guarded(() => …)` or `router.group({ middleware: [authMiddleware()] }, …)` is guarded. A middleware counts as a guard when its name contains `auth` or `guard`, or when it is a constant initialised with `markAuthGuard()` in the same file, or a local factory that returns `markAuthGuard(fn)` and is called in the `middleware` list. `warlock dev` warns about the same routes at runtime.
|
|
12586
12678
|
|
|
12587
12679
|
### `WL9007` — safe redirects
|
|
12588
12680
|
|