@adrianhall/cloudflare-toolkit 1.0.0 → 1.0.1

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.
@@ -45,7 +45,7 @@ function createFileSystem() {
45
45
  *
46
46
  * The split is deliberate, not an oversight: this is a Node-only CLI concern (colored, leveled
47
47
  * `stderr` output for a `bin` entry point) rather than the flagship Worker/browser structured
48
- * logging core, and it is a verbatim port from `cloudflare-scripts`. See
48
+ * logging core, ported verbatim from this toolkit's predecessor CLI tooling. See
49
49
  * `docs/specs/SPECv2.md` §12.3 (ARCH-003) for the full rationale.
50
50
  *
51
51
  * All output is written to `stderr`. Color is applied when `process.stderr.isTTY === true`;
@@ -169,7 +169,7 @@ async function run(argv, deps) {
169
169
  const commanderArgv = separatorIndex === -1 ? argv : argv.slice(0, separatorIndex);
170
170
  const extraArgs = separatorIndex === -1 ? [] : argv.slice(separatorIndex + 1);
171
171
  const program = new Command();
172
- program.name("generate-wrangler-types").description("Regenerate worker-configuration.d.ts only when wrangler.jsonc has changed").version("1.0.0", "--version", "Print version and exit").option("-c, --config <file>", "Wrangler config file to watch", "wrangler.jsonc").option("-d, --dir <dir>", "Base directory for resolving relative paths", ".").option("-f, --force", "Force regeneration even if types are already fresh").option("-o, --output <file>", "Output .d.ts file path (relative to --dir)", "worker-configuration.d.ts").option("-q, --quiet", "Quiet logging (min level: warn)").option("-v, --verbose", "Verbose logging (min level: debug)").allowUnknownOption(false);
172
+ program.name("generate-wrangler-types").description("Regenerate worker-configuration.d.ts only when wrangler.jsonc has changed").version("1.0.1", "--version", "Print version and exit").option("-c, --config <file>", "Wrangler config file to watch", "wrangler.jsonc").option("-d, --dir <dir>", "Base directory for resolving relative paths", ".").option("-f, --force", "Force regeneration even if types are already fresh").option("-o, --output <file>", "Output .d.ts file path (relative to --dir)", "worker-configuration.d.ts").option("-q, --quiet", "Quiet logging (min level: warn)").option("-v, --verbose", "Verbose logging (min level: debug)").allowUnknownOption(false);
173
173
  program.exitOverride();
174
174
  try {
175
175
  program.parse(commanderArgv);
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":["process"],"sources":["../../../src/cli/generate-wrangler-types/fs.ts","../../../src/cli/generate-wrangler-types/logger.ts","../../../src/cli/generate-wrangler-types/run.ts","../../../src/cli/generate-wrangler-types/wrangler.ts","../../../src/cli/generate-wrangler-types/index.ts"],"sourcesContent":["/**\n * @file A Node.js filesystem adapter for the `generate-wrangler-types` CLI. Wraps\n * `node:fs/promises` behind the {@link FileSystem} interface so that file I/O can be swapped for\n * an in-memory stub in unit tests without mocking the built-in module directly.\n */\nimport { constants } from \"node:fs\";\nimport { access, stat } from \"node:fs/promises\";\nimport type { FileSystem } from \"./types.js\";\n\n/**\n * Creates a {@link FileSystem} backed by Node's `fs/promises` module.\n *\n * `fileExists` uses `access(F_OK)` and returns `true` for any accessible path.\n * `getModifiedTime` uses `stat().mtimeMs` to retrieve the last-modified time.\n *\n * @returns A {@link FileSystem} implementation that queries real files.\n */\nexport function createFileSystem(): FileSystem {\n return {\n async fileExists(path: string): Promise<boolean> {\n try {\n await access(path, constants.F_OK);\n return true;\n } catch {\n return false;\n }\n },\n\n async getModifiedTime(path: string): Promise<number> {\n const s = await stat(path);\n return s.mtimeMs;\n }\n };\n}\n","/**\n * @file A private stderr logger for the `generate-wrangler-types` CLI. Not exported from any\n * barrel — this logger is an internal implementation detail of this one CLI, not part of the\n * toolkit's public API surface, and is intentionally a separate, simpler abstraction from the\n * `logging` subpath's `Logger`/`Transport` contract (`src/lib/logging/types.ts`).\n *\n * The split is deliberate, not an oversight: this is a Node-only CLI concern (colored, leveled\n * `stderr` output for a `bin` entry point) rather than the flagship Worker/browser structured\n * logging core, and it is a verbatim port from `cloudflare-scripts`. See\n * `docs/specs/SPECv2.md` §12.3 (ARCH-003) for the full rationale.\n *\n * All output is written to `stderr`. Color is applied when `process.stderr.isTTY === true`;\n * otherwise output is plain text. Log line format: `<ISO-UTC-ms> [<level>] <message>`.\n */\nimport chalk from \"chalk\";\n\n/**\n * The set of supported log levels, ordered from least to most severe.\n *\n * - `debug` — verbose diagnostic output (enabled with `-v`)\n * - `info` — normal operational messages (default)\n * - `warn` — non-fatal anomalies\n * - `error` — failures that halt or degrade operation\n */\nexport type LogLevel = \"debug\" | \"info\" | \"warn\" | \"error\";\n\n/**\n * A low-level output function that receives a fully-resolved log record.\n *\n * The default sink writes colored (or plain) timestamped lines to `stderr`. Tests inject an\n * in-memory sink to capture and assert on log output without printing to the console.\n *\n * @param level - The severity level of the message.\n * @param message - The plain-text message body.\n */\nexport type LogSink = (level: LogLevel, message: string) => void;\n\n/**\n * A structured logger with one method per {@link LogLevel}.\n *\n * Messages below the configured minimum level are silently dropped.\n */\nexport interface Logger {\n /** Emit a `debug`-level message. Dropped unless the logger level is `debug`. */\n debug(message: string): void;\n /** Emit an `info`-level message. */\n info(message: string): void;\n /** Emit a `warn`-level message. */\n warn(message: string): void;\n /** Emit an `error`-level message. */\n error(message: string): void;\n}\n\n/**\n * Options accepted by {@link createLogger}.\n */\nexport interface LoggerOptions {\n /** The minimum severity level that will be forwarded to the sink. */\n level: LogLevel;\n /**\n * Optional custom sink. Defaults to a stderr writer that applies chalk colors when\n * `process.stderr.isTTY` is `true`.\n */\n sink?: LogSink;\n}\n\n/** Numeric ordering used to compare {@link LogLevel} values. */\nconst LOG_LEVEL_ORDER: Readonly<Record<LogLevel, number>> = {\n debug: 0,\n info: 1,\n warn: 2,\n error: 3\n};\n\n/**\n * Applies a chalk color to `line` based on the log level.\n *\n * - `debug` → blue\n * - `info` → green\n * - `warn` → yellow\n * - `error` → red\n *\n * @param level - The log level that determines the color.\n * @param line - The fully-formatted log line to colorize.\n * @returns The colorized string.\n */\nfunction colorize(level: LogLevel, line: string): string {\n switch (level) {\n case \"debug\":\n return chalk.blue(line);\n case \"info\":\n return chalk.green(line);\n case \"warn\":\n return chalk.yellow(line);\n case \"error\":\n return chalk.red(line);\n }\n}\n\n/**\n * Creates the default {@link LogSink} that writes to `process.stderr`.\n *\n * Each line is formatted as `<ISO-UTC-ms> [<level>] <message>`. Color is applied when\n * `process.stderr.isTTY === true`.\n *\n * @returns A `LogSink` that writes to `stderr`.\n */\nfunction createDefaultSink(): LogSink {\n const useColor = process.stderr.isTTY === true;\n return (level: LogLevel, message: string): void => {\n const timestamp = new Date().toISOString();\n const line = `${timestamp} [${level}] ${message}`;\n const output = useColor ? colorize(level, line) : line;\n process.stderr.write(`${output}\\n`);\n };\n}\n\n/**\n * Creates a {@link Logger} that forwards messages at or above `options.level` to the configured\n * sink.\n *\n * @param options - Logger configuration including the minimum level and an optional custom sink.\n * @returns A {@link Logger} instance.\n */\nexport function createLogger(options: LoggerOptions): Logger {\n const { level, sink = createDefaultSink() } = options;\n const minOrder = LOG_LEVEL_ORDER[level];\n\n function log(msgLevel: LogLevel, message: string): void {\n if (LOG_LEVEL_ORDER[msgLevel] >= minOrder) {\n sink(msgLevel, message);\n }\n }\n\n return {\n debug: (message) => log(\"debug\", message),\n info: (message) => log(\"info\", message),\n warn: (message) => log(\"warn\", message),\n error: (message) => log(\"error\", message)\n };\n}\n","/**\n * @file Core orchestration logic for the `generate-wrangler-types` CLI.\n *\n * This module owns the full execution pipeline:\n * 1. Argument parsing (Commander, with `--` passthrough for wrangler args)\n * 2. Logger creation\n * 3. Path resolution\n * 4. Wrangler config existence check\n * 5. Freshness check (compare config vs. output modification times)\n * 6. `wrangler types` execution\n *\n * All external I/O is accessed through the injected {@link GenerateWranglerTypesDeps} interfaces,\n * making the function fully testable without touching the real filesystem or spawning processes.\n */\nimport { isAbsolute, resolve } from \"node:path\";\nimport { Command, CommanderError } from \"commander\";\nimport type { LogLevel, LogSink } from \"./logger.js\";\nimport { createLogger } from \"./logger.js\";\nimport type { FileSystem, WranglerRunner } from \"./types.js\";\n\n// CLI_VERSION is replaced at build time by tsdown's `define` option (tsdown.config.ts).\ndeclare const CLI_VERSION: string;\n\n// ---------------------------------------------------------------------------\n// Internal helpers\n// ---------------------------------------------------------------------------\n\n/**\n * Extracts a human-readable message from an unknown `catch` value.\n *\n * Returns `err.message` when the value is an `Error`, otherwise stringifies it with `String()`.\n *\n * @param err - The caught value (may be anything).\n * @returns A string suitable for log messages or user-facing error output.\n */\nfunction getErrorMessage(err: unknown): string {\n return err instanceof Error ? err.message : String(err);\n}\n\n// ---------------------------------------------------------------------------\n// Public types\n// ---------------------------------------------------------------------------\n\n/**\n * External dependencies injected into {@link run} to keep I/O decoupled from business logic and\n * facilitate unit testing.\n */\nexport interface GenerateWranglerTypesDeps {\n /** Wrangler CLI adapter used to execute `wrangler types`. */\n wrangler: WranglerRunner;\n /** Filesystem adapter used for existence and modification time checks. */\n fs: FileSystem;\n /** If provided, used as the logger sink instead of the default stderr writer. */\n logSink?: LogSink;\n}\n\n// ---------------------------------------------------------------------------\n// Core logic (exported for testing)\n// ---------------------------------------------------------------------------\n\n/**\n * Runs the `generate-wrangler-types` CLI pipeline and returns a POSIX exit code.\n *\n * The function is intentionally pure with respect to side effects: all I/O is delegated to\n * `deps` so that every code path can be exercised in tests without spawning child processes or\n * touching real files.\n *\n * Exit codes:\n * | Code | Meaning |\n * |------|---------|\n * | `0` | Types are already fresh (skipped), or `wrangler types` succeeded |\n * | `1` | Wrangler config file not found (run `provision` first) |\n * | `2` | `wrangler` could not be executed (binary not on PATH / ENOENT) |\n * | `3` | `wrangler types` exited with a non-zero code (code is logged) |\n * | `6` | Argument error (`--verbose` + `--quiet`, unknown option) |\n * | `99` | Unexpected internal error |\n *\n * @param argv - The raw `process.argv` array (first two elements are skipped by Commander).\n * @param deps - Injected I/O dependencies.\n * @returns A numeric exit code as listed above.\n */\nexport async function run(argv: string[], deps: GenerateWranglerTypesDeps): Promise<number> {\n const { wrangler, logSink } = deps;\n const fs = deps.fs;\n\n // -------------------------------------------------------------------------\n // Step 1 — Parse arguments\n //\n // Pre-split argv on `--` so that everything after the separator is forwarded verbatim to\n // `wrangler types`, independently of Commander's passthrough handling. Commander only ever\n // sees the first segment.\n // -------------------------------------------------------------------------\n const separatorIndex = argv.indexOf(\"--\");\n const commanderArgv = separatorIndex === -1 ? argv : argv.slice(0, separatorIndex);\n // Everything after `--` is forwarded verbatim to `wrangler types`.\n const extraArgs = separatorIndex === -1 ? [] : argv.slice(separatorIndex + 1);\n\n const program = new Command();\n program\n .name(\"generate-wrangler-types\")\n .description(\"Regenerate worker-configuration.d.ts only when wrangler.jsonc has changed\")\n .version(CLI_VERSION, \"--version\", \"Print version and exit\")\n .option(\"-c, --config <file>\", \"Wrangler config file to watch\", \"wrangler.jsonc\")\n .option(\"-d, --dir <dir>\", \"Base directory for resolving relative paths\", \".\")\n .option(\"-f, --force\", \"Force regeneration even if types are already fresh\")\n .option(\n \"-o, --output <file>\",\n \"Output .d.ts file path (relative to --dir)\",\n \"worker-configuration.d.ts\"\n )\n .option(\"-q, --quiet\", \"Quiet logging (min level: warn)\")\n .option(\"-v, --verbose\", \"Verbose logging (min level: debug)\")\n .allowUnknownOption(false);\n\n program.exitOverride();\n\n try {\n program.parse(commanderArgv);\n } catch (err: unknown) {\n if (err instanceof CommanderError) {\n // --help and --version exit with code 0 after printing.\n if (err.code === \"commander.helpDisplayed\" || err.code === \"commander.version\") {\n return 0;\n }\n // All other parse errors are argument problems → exit 6.\n return 6;\n }\n // Unexpected error during parse.\n process.stderr.write(`Internal error during argument parsing: ${String(err)}\\n`);\n return 99;\n }\n\n const opts = program.opts<{\n config: string;\n dir: string;\n force?: boolean;\n output: string;\n quiet?: boolean;\n verbose?: boolean;\n }>();\n\n // -------------------------------------------------------------------------\n // Step 2 — Check -v / -q conflict\n // -------------------------------------------------------------------------\n if (opts.verbose && opts.quiet) {\n process.stderr.write(\"Error: --verbose (-v) and --quiet (-q) are mutually exclusive\\n\");\n return 6;\n }\n\n // Create the logger now that we know the level.\n const level: LogLevel =\n opts.verbose ? \"debug\"\n : opts.quiet ? \"warn\"\n : \"info\";\n const logger = createLogger({ level, sink: logSink });\n\n // -------------------------------------------------------------------------\n // Step 3 — Resolve paths\n // -------------------------------------------------------------------------\n const baseDir = opts.dir;\n\n const configPath = isAbsolute(opts.config) ? opts.config : resolve(baseDir, opts.config);\n\n const outputPath = isAbsolute(opts.output) ? opts.output : resolve(baseDir, opts.output);\n\n logger.debug(`Config path: ${configPath}`);\n logger.debug(`Output path: ${outputPath}`);\n if (extraArgs.length > 0) {\n logger.debug(`Wrangler args: ${extraArgs.join(\" \")}`);\n }\n\n // -------------------------------------------------------------------------\n // Step 4 — Check config exists\n // -------------------------------------------------------------------------\n const configExists = await fs.fileExists(configPath);\n if (!configExists) {\n logger.error(\n `Wrangler config not found: ${configPath}\\n`\n + \" Run `npm run provision` to provision infrastructure and generate it.\"\n );\n return 1;\n }\n\n // -------------------------------------------------------------------------\n // Step 5 — Freshness check (skip if output is newer than config)\n // -------------------------------------------------------------------------\n if (!opts.force) {\n const outputExists = await fs.fileExists(outputPath);\n if (outputExists) {\n let configMtime: number;\n let outputMtime: number;\n try {\n configMtime = await fs.getModifiedTime(configPath);\n outputMtime = await fs.getModifiedTime(outputPath);\n } catch (err: unknown) {\n process.stderr.write(\n `Internal error reading file modification times: ${getErrorMessage(err)}\\n`\n );\n return 99;\n }\n\n if (outputMtime > configMtime) {\n logger.debug(\n `Types are fresh (output newer than config by ${Math.round(outputMtime - configMtime)}ms) — skipping`\n );\n return 0;\n }\n\n logger.info(`generate-types: ${opts.config} is newer than ${opts.output} — regenerating...`);\n } else {\n logger.info(`generate-types: ${opts.output} not found — generating...`);\n }\n } else {\n logger.debug(\"--force: skipping freshness check\");\n }\n\n // -------------------------------------------------------------------------\n // Step 6 — Run wrangler types\n // -------------------------------------------------------------------------\n logger.debug(\n `Running: npx wrangler types ${outputPath}${extraArgs.length > 0 ? ` ${extraArgs.join(\" \")}` : \"\"}`\n );\n\n let result: Awaited<ReturnType<WranglerRunner[\"runTypes\"]>>;\n try {\n result = await wrangler.runTypes(outputPath, extraArgs, resolve(baseDir));\n } catch (err: unknown) {\n logger.error(`Failed to launch wrangler: ${getErrorMessage(err)}`);\n return 2;\n }\n\n // Log any captured wrangler output at debug level.\n if (result.stdout.trim().length > 0) {\n for (const line of result.stdout.trimEnd().split(\"\\n\")) {\n logger.debug(`wrangler: ${line}`);\n }\n }\n if (result.stderr.trim().length > 0) {\n for (const line of result.stderr.trimEnd().split(\"\\n\")) {\n logger.debug(`wrangler (stderr): ${line}`);\n }\n }\n\n const exitCode = result.exitCode ?? 1;\n\n if (exitCode !== 0) {\n logger.error(`wrangler types failed with exit code ${exitCode}`);\n return 3;\n }\n\n logger.info(`Wrote ${outputPath}`);\n return 0;\n}\n","/**\n * @file A Wrangler CLI adapter for the `generate-wrangler-types` CLI. Wraps the execution of\n * `npx wrangler types` behind the {@link WranglerRunner} interface so that tests can substitute\n * a stub without spawning a real process.\n *\n * The real implementation spawns `npx wrangler types <outputPath> [extraArgs]` in the given\n * working directory and captures stdout/stderr for logging.\n *\n * `outputPath` (attacker-influenceable via `-o/--output`) and `extraArgs` (everything after a\n * `--` separator, forwarded verbatim) are never passed to a shell for interpretation — see\n * SEC-002 (https://github.com/adrianhall/cloudflare-toolkit/issues/47). Process spawning goes\n * through `cross-spawn` instead of `node:child_process.spawn`'s own `shell: true` option: on\n * POSIX it spawns the target binary directly with no shell involved at all, and on Windows it\n * resolves the `npx`/`wrangler` shim's actual `.cmd` file and safely quotes each argument for\n * `cmd.exe` itself, rather than handing it a single, unescaped, attacker-influenceable command\n * line. A value like `--output \"x;rm -rf ~\"` is therefore passed through as one literal argv\n * element, not interpreted as shell syntax.\n */\nimport type { ChildProcessWithoutNullStreams } from \"node:child_process\";\nimport spawn from \"cross-spawn\";\nimport type { WranglerResult, WranglerRunner } from \"./types.js\";\n\n// ---------------------------------------------------------------------------\n// ExecRunner abstraction\n// ---------------------------------------------------------------------------\n\n/**\n * A function that spawns a child process and returns its result. Injectable so tests can stub\n * process execution without real spawning.\n */\nexport type ExecRunner = (\n command: string,\n args: string[],\n options: { cwd: string }\n) => Promise<WranglerResult>;\n\n// ---------------------------------------------------------------------------\n// Default real implementation\n// ---------------------------------------------------------------------------\n\n/**\n * The default {@link ExecRunner} that spawns a real child process via `cross-spawn`.\n *\n * Captures stdout and stderr as strings. `cross-spawn` resolves Windows `.cmd`/`.bat` shims\n * (e.g. `npx.cmd`) and safely quotes arguments for `cmd.exe` when unavoidable, without ever\n * handing `command`/`args` to a shell as an unescaped, concatenated string (SEC-002).\n *\n * Exported for direct testing of the real spawn path.\n *\n * @param command - The executable to spawn (always `\"npx\"` in practice).\n * @param args - Arguments passed to `command`.\n * @param options - Options forwarded to `cross-spawn`.\n * @param options.cwd - Working directory for the spawned process.\n * @returns The process result including exit code and captured output.\n * @throws If the process cannot be spawned (e.g. ENOENT).\n */\nexport async function defaultExecRunner(\n command: string,\n args: string[],\n options: { cwd: string }\n): Promise<WranglerResult> {\n return new Promise((resolve, reject) => {\n // `cross-spawn`'s type declarations expose the generic `child_process.SpawnOptions`\n // signature rather than Node's own stdio-tuple-narrowed overloads, so TypeScript sees\n // `stdout`/`stderr` below as possibly `null`. The `[\"ignore\", \"pipe\", \"pipe\"]` tuple\n // guarantees both are real `Readable` streams at runtime — Node's own `spawn`\n // implementation (which `cross-spawn` delegates to) never omits a stream for a `\"pipe\"`\n // stdio element — so this cast restores that guarantee for the type checker.\n const child = spawn(command, args, {\n cwd: options.cwd,\n stdio: [\"ignore\", \"pipe\", \"pipe\"]\n }) as ChildProcessWithoutNullStreams;\n\n let stdout = \"\";\n let stderr = \"\";\n\n child.stdout.on(\"data\", (chunk: Buffer) => {\n stdout += chunk.toString();\n });\n\n child.stderr.on(\"data\", (chunk: Buffer) => {\n stderr += chunk.toString();\n });\n\n child.on(\"error\", (err: Error) => {\n reject(err);\n });\n\n child.on(\"close\", (code: number | null) => {\n resolve({ exitCode: code, stdout, stderr });\n });\n });\n}\n\n// ---------------------------------------------------------------------------\n// Factory\n// ---------------------------------------------------------------------------\n\n/**\n * Creates a {@link WranglerRunner} that executes `npx wrangler types` as a real child process.\n *\n * @param execRunner - Optional override for the process spawner. When omitted, the default\n * `cross-spawn`-backed wrapper is used. Inject a stub in tests.\n * @returns A {@link WranglerRunner} implementation.\n */\nexport function createWranglerRunner(execRunner?: ExecRunner): WranglerRunner {\n const exec = execRunner ?? defaultExecRunner;\n\n return {\n async runTypes(outputPath: string, extraArgs: string[], cwd: string): Promise<WranglerResult> {\n const args = [\"wrangler\", \"types\", outputPath, ...extraArgs];\n return exec(\"npx\", args, { cwd });\n }\n };\n}\n","#!/usr/bin/env node\n/// <reference types=\"node\" />\n/**\n * @file Entry point for the `generate-wrangler-types` CLI binary.\n *\n * Wires together the real filesystem adapter and Wrangler runner, then delegates to {@link run}\n * which owns all argument parsing and business logic. The process exits with the numeric code\n * returned by `run`.\n *\n * The shebang above is preserved verbatim by tsdown in the built\n * `dist/cli/generate-wrangler-types/index.js` so that `package.json#bin` resolves to a directly\n * executable file once npm links it.\n *\n * This file is a thin wiring shim around `run()`, which is fully covered by\n * `test/node/cli/generate-wrangler-types/run.test.ts`, and is excluded from coverage thresholds.\n */\nimport process from \"node:process\";\nimport { createFileSystem } from \"./fs.js\";\nimport { run } from \"./run.js\";\nimport { createWranglerRunner } from \"./wrangler.js\";\n\nconst exitCode = await run(process.argv, {\n wrangler: createWranglerRunner(),\n fs: createFileSystem()\n});\nprocess.exit(exitCode);\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.js","names":["process"],"sources":["../../../src/cli/generate-wrangler-types/fs.ts","../../../src/cli/generate-wrangler-types/logger.ts","../../../src/cli/generate-wrangler-types/run.ts","../../../src/cli/generate-wrangler-types/wrangler.ts","../../../src/cli/generate-wrangler-types/index.ts"],"sourcesContent":["/**\n * @file A Node.js filesystem adapter for the `generate-wrangler-types` CLI. Wraps\n * `node:fs/promises` behind the {@link FileSystem} interface so that file I/O can be swapped for\n * an in-memory stub in unit tests without mocking the built-in module directly.\n */\nimport { constants } from \"node:fs\";\nimport { access, stat } from \"node:fs/promises\";\nimport type { FileSystem } from \"./types.js\";\n\n/**\n * Creates a {@link FileSystem} backed by Node's `fs/promises` module.\n *\n * `fileExists` uses `access(F_OK)` and returns `true` for any accessible path.\n * `getModifiedTime` uses `stat().mtimeMs` to retrieve the last-modified time.\n *\n * @returns A {@link FileSystem} implementation that queries real files.\n */\nexport function createFileSystem(): FileSystem {\n return {\n async fileExists(path: string): Promise<boolean> {\n try {\n await access(path, constants.F_OK);\n return true;\n } catch {\n return false;\n }\n },\n\n async getModifiedTime(path: string): Promise<number> {\n const s = await stat(path);\n return s.mtimeMs;\n }\n };\n}\n","/**\n * @file A private stderr logger for the `generate-wrangler-types` CLI. Not exported from any\n * barrel — this logger is an internal implementation detail of this one CLI, not part of the\n * toolkit's public API surface, and is intentionally a separate, simpler abstraction from the\n * `logging` subpath's `Logger`/`Transport` contract (`src/lib/logging/types.ts`).\n *\n * The split is deliberate, not an oversight: this is a Node-only CLI concern (colored, leveled\n * `stderr` output for a `bin` entry point) rather than the flagship Worker/browser structured\n * logging core, ported verbatim from this toolkit's predecessor CLI tooling. See\n * `docs/specs/SPECv2.md` §12.3 (ARCH-003) for the full rationale.\n *\n * All output is written to `stderr`. Color is applied when `process.stderr.isTTY === true`;\n * otherwise output is plain text. Log line format: `<ISO-UTC-ms> [<level>] <message>`.\n */\nimport chalk from \"chalk\";\n\n/**\n * The set of supported log levels, ordered from least to most severe.\n *\n * - `debug` — verbose diagnostic output (enabled with `-v`)\n * - `info` — normal operational messages (default)\n * - `warn` — non-fatal anomalies\n * - `error` — failures that halt or degrade operation\n */\nexport type LogLevel = \"debug\" | \"info\" | \"warn\" | \"error\";\n\n/**\n * A low-level output function that receives a fully-resolved log record.\n *\n * The default sink writes colored (or plain) timestamped lines to `stderr`. Tests inject an\n * in-memory sink to capture and assert on log output without printing to the console.\n *\n * @param level - The severity level of the message.\n * @param message - The plain-text message body.\n */\nexport type LogSink = (level: LogLevel, message: string) => void;\n\n/**\n * A structured logger with one method per {@link LogLevel}.\n *\n * Messages below the configured minimum level are silently dropped.\n */\nexport interface Logger {\n /** Emit a `debug`-level message. Dropped unless the logger level is `debug`. */\n debug(message: string): void;\n /** Emit an `info`-level message. */\n info(message: string): void;\n /** Emit a `warn`-level message. */\n warn(message: string): void;\n /** Emit an `error`-level message. */\n error(message: string): void;\n}\n\n/**\n * Options accepted by {@link createLogger}.\n */\nexport interface LoggerOptions {\n /** The minimum severity level that will be forwarded to the sink. */\n level: LogLevel;\n /**\n * Optional custom sink. Defaults to a stderr writer that applies chalk colors when\n * `process.stderr.isTTY` is `true`.\n */\n sink?: LogSink;\n}\n\n/** Numeric ordering used to compare {@link LogLevel} values. */\nconst LOG_LEVEL_ORDER: Readonly<Record<LogLevel, number>> = {\n debug: 0,\n info: 1,\n warn: 2,\n error: 3\n};\n\n/**\n * Applies a chalk color to `line` based on the log level.\n *\n * - `debug` → blue\n * - `info` → green\n * - `warn` → yellow\n * - `error` → red\n *\n * @param level - The log level that determines the color.\n * @param line - The fully-formatted log line to colorize.\n * @returns The colorized string.\n */\nfunction colorize(level: LogLevel, line: string): string {\n switch (level) {\n case \"debug\":\n return chalk.blue(line);\n case \"info\":\n return chalk.green(line);\n case \"warn\":\n return chalk.yellow(line);\n case \"error\":\n return chalk.red(line);\n }\n}\n\n/**\n * Creates the default {@link LogSink} that writes to `process.stderr`.\n *\n * Each line is formatted as `<ISO-UTC-ms> [<level>] <message>`. Color is applied when\n * `process.stderr.isTTY === true`.\n *\n * @returns A `LogSink` that writes to `stderr`.\n */\nfunction createDefaultSink(): LogSink {\n const useColor = process.stderr.isTTY === true;\n return (level: LogLevel, message: string): void => {\n const timestamp = new Date().toISOString();\n const line = `${timestamp} [${level}] ${message}`;\n const output = useColor ? colorize(level, line) : line;\n process.stderr.write(`${output}\\n`);\n };\n}\n\n/**\n * Creates a {@link Logger} that forwards messages at or above `options.level` to the configured\n * sink.\n *\n * @param options - Logger configuration including the minimum level and an optional custom sink.\n * @returns A {@link Logger} instance.\n */\nexport function createLogger(options: LoggerOptions): Logger {\n const { level, sink = createDefaultSink() } = options;\n const minOrder = LOG_LEVEL_ORDER[level];\n\n function log(msgLevel: LogLevel, message: string): void {\n if (LOG_LEVEL_ORDER[msgLevel] >= minOrder) {\n sink(msgLevel, message);\n }\n }\n\n return {\n debug: (message) => log(\"debug\", message),\n info: (message) => log(\"info\", message),\n warn: (message) => log(\"warn\", message),\n error: (message) => log(\"error\", message)\n };\n}\n","/**\n * @file Core orchestration logic for the `generate-wrangler-types` CLI.\n *\n * This module owns the full execution pipeline:\n * 1. Argument parsing (Commander, with `--` passthrough for wrangler args)\n * 2. Logger creation\n * 3. Path resolution\n * 4. Wrangler config existence check\n * 5. Freshness check (compare config vs. output modification times)\n * 6. `wrangler types` execution\n *\n * All external I/O is accessed through the injected {@link GenerateWranglerTypesDeps} interfaces,\n * making the function fully testable without touching the real filesystem or spawning processes.\n */\nimport { isAbsolute, resolve } from \"node:path\";\nimport { Command, CommanderError } from \"commander\";\nimport type { LogLevel, LogSink } from \"./logger.js\";\nimport { createLogger } from \"./logger.js\";\nimport type { FileSystem, WranglerRunner } from \"./types.js\";\n\n// CLI_VERSION is replaced at build time by tsdown's `define` option (tsdown.config.ts).\ndeclare const CLI_VERSION: string;\n\n// ---------------------------------------------------------------------------\n// Internal helpers\n// ---------------------------------------------------------------------------\n\n/**\n * Extracts a human-readable message from an unknown `catch` value.\n *\n * Returns `err.message` when the value is an `Error`, otherwise stringifies it with `String()`.\n *\n * @param err - The caught value (may be anything).\n * @returns A string suitable for log messages or user-facing error output.\n */\nfunction getErrorMessage(err: unknown): string {\n return err instanceof Error ? err.message : String(err);\n}\n\n// ---------------------------------------------------------------------------\n// Public types\n// ---------------------------------------------------------------------------\n\n/**\n * External dependencies injected into {@link run} to keep I/O decoupled from business logic and\n * facilitate unit testing.\n */\nexport interface GenerateWranglerTypesDeps {\n /** Wrangler CLI adapter used to execute `wrangler types`. */\n wrangler: WranglerRunner;\n /** Filesystem adapter used for existence and modification time checks. */\n fs: FileSystem;\n /** If provided, used as the logger sink instead of the default stderr writer. */\n logSink?: LogSink;\n}\n\n// ---------------------------------------------------------------------------\n// Core logic (exported for testing)\n// ---------------------------------------------------------------------------\n\n/**\n * Runs the `generate-wrangler-types` CLI pipeline and returns a POSIX exit code.\n *\n * The function is intentionally pure with respect to side effects: all I/O is delegated to\n * `deps` so that every code path can be exercised in tests without spawning child processes or\n * touching real files.\n *\n * Exit codes:\n * | Code | Meaning |\n * |------|---------|\n * | `0` | Types are already fresh (skipped), or `wrangler types` succeeded |\n * | `1` | Wrangler config file not found (run `provision` first) |\n * | `2` | `wrangler` could not be executed (binary not on PATH / ENOENT) |\n * | `3` | `wrangler types` exited with a non-zero code (code is logged) |\n * | `6` | Argument error (`--verbose` + `--quiet`, unknown option) |\n * | `99` | Unexpected internal error |\n *\n * @param argv - The raw `process.argv` array (first two elements are skipped by Commander).\n * @param deps - Injected I/O dependencies.\n * @returns A numeric exit code as listed above.\n */\nexport async function run(argv: string[], deps: GenerateWranglerTypesDeps): Promise<number> {\n const { wrangler, logSink } = deps;\n const fs = deps.fs;\n\n // -------------------------------------------------------------------------\n // Step 1 — Parse arguments\n //\n // Pre-split argv on `--` so that everything after the separator is forwarded verbatim to\n // `wrangler types`, independently of Commander's passthrough handling. Commander only ever\n // sees the first segment.\n // -------------------------------------------------------------------------\n const separatorIndex = argv.indexOf(\"--\");\n const commanderArgv = separatorIndex === -1 ? argv : argv.slice(0, separatorIndex);\n // Everything after `--` is forwarded verbatim to `wrangler types`.\n const extraArgs = separatorIndex === -1 ? [] : argv.slice(separatorIndex + 1);\n\n const program = new Command();\n program\n .name(\"generate-wrangler-types\")\n .description(\"Regenerate worker-configuration.d.ts only when wrangler.jsonc has changed\")\n .version(CLI_VERSION, \"--version\", \"Print version and exit\")\n .option(\"-c, --config <file>\", \"Wrangler config file to watch\", \"wrangler.jsonc\")\n .option(\"-d, --dir <dir>\", \"Base directory for resolving relative paths\", \".\")\n .option(\"-f, --force\", \"Force regeneration even if types are already fresh\")\n .option(\n \"-o, --output <file>\",\n \"Output .d.ts file path (relative to --dir)\",\n \"worker-configuration.d.ts\"\n )\n .option(\"-q, --quiet\", \"Quiet logging (min level: warn)\")\n .option(\"-v, --verbose\", \"Verbose logging (min level: debug)\")\n .allowUnknownOption(false);\n\n program.exitOverride();\n\n try {\n program.parse(commanderArgv);\n } catch (err: unknown) {\n if (err instanceof CommanderError) {\n // --help and --version exit with code 0 after printing.\n if (err.code === \"commander.helpDisplayed\" || err.code === \"commander.version\") {\n return 0;\n }\n // All other parse errors are argument problems → exit 6.\n return 6;\n }\n // Unexpected error during parse.\n process.stderr.write(`Internal error during argument parsing: ${String(err)}\\n`);\n return 99;\n }\n\n const opts = program.opts<{\n config: string;\n dir: string;\n force?: boolean;\n output: string;\n quiet?: boolean;\n verbose?: boolean;\n }>();\n\n // -------------------------------------------------------------------------\n // Step 2 — Check -v / -q conflict\n // -------------------------------------------------------------------------\n if (opts.verbose && opts.quiet) {\n process.stderr.write(\"Error: --verbose (-v) and --quiet (-q) are mutually exclusive\\n\");\n return 6;\n }\n\n // Create the logger now that we know the level.\n const level: LogLevel =\n opts.verbose ? \"debug\"\n : opts.quiet ? \"warn\"\n : \"info\";\n const logger = createLogger({ level, sink: logSink });\n\n // -------------------------------------------------------------------------\n // Step 3 — Resolve paths\n // -------------------------------------------------------------------------\n const baseDir = opts.dir;\n\n const configPath = isAbsolute(opts.config) ? opts.config : resolve(baseDir, opts.config);\n\n const outputPath = isAbsolute(opts.output) ? opts.output : resolve(baseDir, opts.output);\n\n logger.debug(`Config path: ${configPath}`);\n logger.debug(`Output path: ${outputPath}`);\n if (extraArgs.length > 0) {\n logger.debug(`Wrangler args: ${extraArgs.join(\" \")}`);\n }\n\n // -------------------------------------------------------------------------\n // Step 4 — Check config exists\n // -------------------------------------------------------------------------\n const configExists = await fs.fileExists(configPath);\n if (!configExists) {\n logger.error(\n `Wrangler config not found: ${configPath}\\n`\n + \" Run `npm run provision` to provision infrastructure and generate it.\"\n );\n return 1;\n }\n\n // -------------------------------------------------------------------------\n // Step 5 — Freshness check (skip if output is newer than config)\n // -------------------------------------------------------------------------\n if (!opts.force) {\n const outputExists = await fs.fileExists(outputPath);\n if (outputExists) {\n let configMtime: number;\n let outputMtime: number;\n try {\n configMtime = await fs.getModifiedTime(configPath);\n outputMtime = await fs.getModifiedTime(outputPath);\n } catch (err: unknown) {\n process.stderr.write(\n `Internal error reading file modification times: ${getErrorMessage(err)}\\n`\n );\n return 99;\n }\n\n if (outputMtime > configMtime) {\n logger.debug(\n `Types are fresh (output newer than config by ${Math.round(outputMtime - configMtime)}ms) — skipping`\n );\n return 0;\n }\n\n logger.info(`generate-types: ${opts.config} is newer than ${opts.output} — regenerating...`);\n } else {\n logger.info(`generate-types: ${opts.output} not found — generating...`);\n }\n } else {\n logger.debug(\"--force: skipping freshness check\");\n }\n\n // -------------------------------------------------------------------------\n // Step 6 — Run wrangler types\n // -------------------------------------------------------------------------\n logger.debug(\n `Running: npx wrangler types ${outputPath}${extraArgs.length > 0 ? ` ${extraArgs.join(\" \")}` : \"\"}`\n );\n\n let result: Awaited<ReturnType<WranglerRunner[\"runTypes\"]>>;\n try {\n result = await wrangler.runTypes(outputPath, extraArgs, resolve(baseDir));\n } catch (err: unknown) {\n logger.error(`Failed to launch wrangler: ${getErrorMessage(err)}`);\n return 2;\n }\n\n // Log any captured wrangler output at debug level.\n if (result.stdout.trim().length > 0) {\n for (const line of result.stdout.trimEnd().split(\"\\n\")) {\n logger.debug(`wrangler: ${line}`);\n }\n }\n if (result.stderr.trim().length > 0) {\n for (const line of result.stderr.trimEnd().split(\"\\n\")) {\n logger.debug(`wrangler (stderr): ${line}`);\n }\n }\n\n const exitCode = result.exitCode ?? 1;\n\n if (exitCode !== 0) {\n logger.error(`wrangler types failed with exit code ${exitCode}`);\n return 3;\n }\n\n logger.info(`Wrote ${outputPath}`);\n return 0;\n}\n","/**\n * @file A Wrangler CLI adapter for the `generate-wrangler-types` CLI. Wraps the execution of\n * `npx wrangler types` behind the {@link WranglerRunner} interface so that tests can substitute\n * a stub without spawning a real process.\n *\n * The real implementation spawns `npx wrangler types <outputPath> [extraArgs]` in the given\n * working directory and captures stdout/stderr for logging.\n *\n * `outputPath` (attacker-influenceable via `-o/--output`) and `extraArgs` (everything after a\n * `--` separator, forwarded verbatim) are never passed to a shell for interpretation — see\n * SEC-002 (https://github.com/adrianhall/cloudflare-toolkit/issues/47). Process spawning goes\n * through `cross-spawn` instead of `node:child_process.spawn`'s own `shell: true` option: on\n * POSIX it spawns the target binary directly with no shell involved at all, and on Windows it\n * resolves the `npx`/`wrangler` shim's actual `.cmd` file and safely quotes each argument for\n * `cmd.exe` itself, rather than handing it a single, unescaped, attacker-influenceable command\n * line. A value like `--output \"x;rm -rf ~\"` is therefore passed through as one literal argv\n * element, not interpreted as shell syntax.\n */\nimport type { ChildProcessWithoutNullStreams } from \"node:child_process\";\nimport spawn from \"cross-spawn\";\nimport type { WranglerResult, WranglerRunner } from \"./types.js\";\n\n// ---------------------------------------------------------------------------\n// ExecRunner abstraction\n// ---------------------------------------------------------------------------\n\n/**\n * A function that spawns a child process and returns its result. Injectable so tests can stub\n * process execution without real spawning.\n */\nexport type ExecRunner = (\n command: string,\n args: string[],\n options: { cwd: string }\n) => Promise<WranglerResult>;\n\n// ---------------------------------------------------------------------------\n// Default real implementation\n// ---------------------------------------------------------------------------\n\n/**\n * The default {@link ExecRunner} that spawns a real child process via `cross-spawn`.\n *\n * Captures stdout and stderr as strings. `cross-spawn` resolves Windows `.cmd`/`.bat` shims\n * (e.g. `npx.cmd`) and safely quotes arguments for `cmd.exe` when unavoidable, without ever\n * handing `command`/`args` to a shell as an unescaped, concatenated string (SEC-002).\n *\n * Exported for direct testing of the real spawn path.\n *\n * @param command - The executable to spawn (always `\"npx\"` in practice).\n * @param args - Arguments passed to `command`.\n * @param options - Options forwarded to `cross-spawn`.\n * @param options.cwd - Working directory for the spawned process.\n * @returns The process result including exit code and captured output.\n * @throws If the process cannot be spawned (e.g. ENOENT).\n */\nexport async function defaultExecRunner(\n command: string,\n args: string[],\n options: { cwd: string }\n): Promise<WranglerResult> {\n return new Promise((resolve, reject) => {\n // `cross-spawn`'s type declarations expose the generic `child_process.SpawnOptions`\n // signature rather than Node's own stdio-tuple-narrowed overloads, so TypeScript sees\n // `stdout`/`stderr` below as possibly `null`. The `[\"ignore\", \"pipe\", \"pipe\"]` tuple\n // guarantees both are real `Readable` streams at runtime — Node's own `spawn`\n // implementation (which `cross-spawn` delegates to) never omits a stream for a `\"pipe\"`\n // stdio element — so this cast restores that guarantee for the type checker.\n const child = spawn(command, args, {\n cwd: options.cwd,\n stdio: [\"ignore\", \"pipe\", \"pipe\"]\n }) as ChildProcessWithoutNullStreams;\n\n let stdout = \"\";\n let stderr = \"\";\n\n child.stdout.on(\"data\", (chunk: Buffer) => {\n stdout += chunk.toString();\n });\n\n child.stderr.on(\"data\", (chunk: Buffer) => {\n stderr += chunk.toString();\n });\n\n child.on(\"error\", (err: Error) => {\n reject(err);\n });\n\n child.on(\"close\", (code: number | null) => {\n resolve({ exitCode: code, stdout, stderr });\n });\n });\n}\n\n// ---------------------------------------------------------------------------\n// Factory\n// ---------------------------------------------------------------------------\n\n/**\n * Creates a {@link WranglerRunner} that executes `npx wrangler types` as a real child process.\n *\n * @param execRunner - Optional override for the process spawner. When omitted, the default\n * `cross-spawn`-backed wrapper is used. Inject a stub in tests.\n * @returns A {@link WranglerRunner} implementation.\n */\nexport function createWranglerRunner(execRunner?: ExecRunner): WranglerRunner {\n const exec = execRunner ?? defaultExecRunner;\n\n return {\n async runTypes(outputPath: string, extraArgs: string[], cwd: string): Promise<WranglerResult> {\n const args = [\"wrangler\", \"types\", outputPath, ...extraArgs];\n return exec(\"npx\", args, { cwd });\n }\n };\n}\n","#!/usr/bin/env node\n/// <reference types=\"node\" />\n/**\n * @file Entry point for the `generate-wrangler-types` CLI binary.\n *\n * Wires together the real filesystem adapter and Wrangler runner, then delegates to {@link run}\n * which owns all argument parsing and business logic. The process exits with the numeric code\n * returned by `run`.\n *\n * The shebang above is preserved verbatim by tsdown in the built\n * `dist/cli/generate-wrangler-types/index.js` so that `package.json#bin` resolves to a directly\n * executable file once npm links it.\n *\n * This file is a thin wiring shim around `run()`, which is fully covered by\n * `test/node/cli/generate-wrangler-types/run.test.ts`, and is excluded from coverage thresholds.\n */\nimport process from \"node:process\";\nimport { createFileSystem } from \"./fs.js\";\nimport { run } from \"./run.js\";\nimport { createWranglerRunner } from \"./wrangler.js\";\n\nconst exitCode = await run(process.argv, {\n wrangler: createWranglerRunner(),\n fs: createFileSystem()\n});\nprocess.exit(exitCode);\n"],"mappings":"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"}
@@ -1,6 +1,6 @@
1
1
  import { a as statusToPhrase, i as normalizeProblemDetails, n as ProblemDetailsError, o as statusToSlug, r as buildProblemResponse } from "../factory-BI5gVL_P.js";
2
2
  import { n as resolveLoggerConfig, s as createLogger, t as createSilentTransport } from "../silent-CWpHE65X.js";
3
- import { l as verifyAccessJwt, s as parseCookie, u as verifyDevJwt } from "../jwt-BvuKtvby.js";
3
+ import { l as verifyAccessJwt, s as parseCookie, u as verifyDevJwt } from "../jwt-DJTB3ay0.js";
4
4
  import { t as matchPolicy } from "../policy-CvS6AvvD.js";
5
5
  import { HTTPException } from "hono/http-exception";
6
6
  //#region src/lib/hono/error-handler.ts
package/dist/index.d.ts CHANGED
@@ -1,8 +1,8 @@
1
1
  import { n as throwIfNull, r as valueOrDefault, t as sqlCount } from "./index-434HN8jN.js";
2
2
  import { n as ProblemDetailsInput, t as ProblemDetails } from "./types-Cx6NNILW.js";
3
3
  import { t as ProblemDetailsError } from "./error-CLYcAvBM.js";
4
- import { c as internalServerError, d as notImplemented, f as serviceUnavailable, h as unsupportedMediaType, i as badRequest, l as methodNotAllowed, m as unprocessableContent, n as InvalidShapeError, o as forbidden, p as unauthorized, s as gone, t as NullError, u as notFound } from "./index-DSHomPUi.js";
4
+ import { a as contentTooLarge, c as internalServerError, d as notImplemented, f as serviceUnavailable, h as unsupportedMediaType, i as badRequest, l as methodNotAllowed, m as unprocessableContent, n as InvalidShapeError, o as forbidden, p as unauthorized, s as gone, t as NullError, u as notFound } from "./index-DSHomPUi.js";
5
5
  import { n as statusToSlug, o as createProblemTypeRegistry, s as problemDetails, t as statusToPhrase } from "./index-D5bfk4cR.js";
6
6
  import { a as Environment, c as LogRecord, d as Runtime, f as StructuredTransportOptions, i as CreateLoggerOptions, l as Logger, m as TransportErrorHandler, n as CaptureTransport, o as LogContext, p as Transport, r as ConsoleTransportOptions, s as LogLevel, t as BrowserTransportOptions, u as ResolvedLoggerConfig } from "./types-DCSMb1cp.js";
7
7
  import { a as createCaptureTransport, c as resolveLoggerConfig, i as combineTransports, l as createLogger, n as createSilentTransport, o as createBrowserTransport, r as createConsoleTransport, s as serializeError, t as createStructuredTransport } from "./index-Byl-ZrCy.js";
8
- export { type BrowserTransportOptions, type CaptureTransport, type ConsoleTransportOptions, type CreateLoggerOptions, type Environment, InvalidShapeError, type LogContext, type LogLevel, type LogRecord, type Logger, NullError, type ProblemDetails, ProblemDetailsError, type ProblemDetailsInput, type ResolvedLoggerConfig, type Runtime, type StructuredTransportOptions, type Transport, type TransportErrorHandler, badRequest, combineTransports, createBrowserTransport, createCaptureTransport, createConsoleTransport, createLogger, createProblemTypeRegistry, createSilentTransport, createStructuredTransport, forbidden, gone, internalServerError, methodNotAllowed, notFound, notImplemented, problemDetails, resolveLoggerConfig, serializeError, serviceUnavailable, sqlCount, statusToPhrase, statusToSlug, throwIfNull, unauthorized, unprocessableContent, unsupportedMediaType, valueOrDefault };
8
+ export { type BrowserTransportOptions, type CaptureTransport, type ConsoleTransportOptions, type CreateLoggerOptions, type Environment, InvalidShapeError, type LogContext, type LogLevel, type LogRecord, type Logger, NullError, type ProblemDetails, ProblemDetailsError, type ProblemDetailsInput, type ResolvedLoggerConfig, type Runtime, type StructuredTransportOptions, type Transport, type TransportErrorHandler, badRequest, combineTransports, contentTooLarge, createBrowserTransport, createCaptureTransport, createConsoleTransport, createLogger, createProblemTypeRegistry, createSilentTransport, createStructuredTransport, forbidden, gone, internalServerError, methodNotAllowed, notFound, notImplemented, problemDetails, resolveLoggerConfig, serializeError, serviceUnavailable, sqlCount, statusToPhrase, statusToSlug, throwIfNull, unauthorized, unprocessableContent, unsupportedMediaType, valueOrDefault };
package/dist/index.js CHANGED
@@ -1,8 +1,8 @@
1
1
  import { a as statusToPhrase, n as ProblemDetailsError, o as statusToSlug, t as problemDetails } from "./factory-BI5gVL_P.js";
2
- import { a as internalServerError, c as notImplemented, d as unprocessableContent, f as unsupportedMediaType, i as gone, l as serviceUnavailable, o as methodNotAllowed, r as forbidden, s as notFound, t as badRequest, u as unauthorized } from "./generators-D8WWEHa1.js";
2
+ import { a as internalServerError, c as notImplemented, d as unprocessableContent, f as unsupportedMediaType, i as gone, l as serviceUnavailable, n as contentTooLarge, o as methodNotAllowed, r as forbidden, s as notFound, t as badRequest, u as unauthorized } from "./generators-D8WWEHa1.js";
3
3
  import { n as InvalidShapeError, t as NullError } from "./errors-Ciipq_zr.js";
4
4
  import { n as throwIfNull, r as valueOrDefault, t as sqlCount } from "./guards-6K1CVAr5.js";
5
5
  import { t as createProblemTypeRegistry } from "./problem-details-CuRsLy3Q.js";
6
6
  import { a as createCaptureTransport, c as serializeError, i as createConsoleTransport, n as resolveLoggerConfig, o as createBrowserTransport, r as createStructuredTransport, s as createLogger, t as createSilentTransport } from "./silent-CWpHE65X.js";
7
7
  import { t as combineTransports } from "./logging-CGHjOVLM.js";
8
- export { InvalidShapeError, NullError, ProblemDetailsError, badRequest, combineTransports, createBrowserTransport, createCaptureTransport, createConsoleTransport, createLogger, createProblemTypeRegistry, createSilentTransport, createStructuredTransport, forbidden, gone, internalServerError, methodNotAllowed, notFound, notImplemented, problemDetails, resolveLoggerConfig, serializeError, serviceUnavailable, sqlCount, statusToPhrase, statusToSlug, throwIfNull, unauthorized, unprocessableContent, unsupportedMediaType, valueOrDefault };
8
+ export { InvalidShapeError, NullError, ProblemDetailsError, badRequest, combineTransports, contentTooLarge, createBrowserTransport, createCaptureTransport, createConsoleTransport, createLogger, createProblemTypeRegistry, createSilentTransport, createStructuredTransport, forbidden, gone, internalServerError, methodNotAllowed, notFound, notImplemented, problemDetails, resolveLoggerConfig, serializeError, serviceUnavailable, sqlCount, statusToPhrase, statusToSlug, throwIfNull, unauthorized, unprocessableContent, unsupportedMediaType, valueOrDefault };
@@ -148,7 +148,7 @@ function generateDevSub() {
148
148
  *
149
149
  * @param email - The user's email address (becomes the `email` claim).
150
150
  * @param options - Optional overrides.
151
- * @param options.secret - HMAC signing secret (default {@link DEFAULT_DEV_SECRET}).
151
+ * @param options.secret - HMAC signing secret (default `DEFAULT_DEV_SECRET`).
152
152
  * @param options.lifetime - Token lifetime in seconds (default `86400` / 24 h).
153
153
  * @param options.sub - Subject claim. When provided it is used **verbatim**; when omitted a
154
154
  * random UUID is generated (matching the shape of a real Cloudflare Access `sub`) instead of
@@ -325,4 +325,4 @@ function parseCookie(cookieHeader) {
325
325
  //#endregion
326
326
  export { buildCookieHeader as a, signDevJwt as c, JWT_HEADER as i, verifyAccessJwt as l, DEFAULT_DEV_SECRET as n, clearCookieHeader as o, EMAIL_HEADER as r, parseCookie as s, COOKIE_NAME as t, verifyDevJwt as u };
327
327
 
328
- //# sourceMappingURL=jwt-BvuKtvby.js.map
328
+ //# sourceMappingURL=jwt-DJTB3ay0.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"jwt-DJTB3ay0.js","names":["joseErrors"],"sources":["../src/lib/auth-internal/jwks.ts","../src/lib/auth-internal/jwt.ts"],"sourcesContent":["/**\n * @file Remote JWKS management for Cloudflare Access JWT verification.\n *\n * Only Web-standard APIs (`jose`, `URL`, `Map`) are used, so this module is both Worker-safe\n * (for `hono/`) and Node-safe (for `vite/`). Kept in its own module so tests can mock\n * `getRemoteJwks` and supply a local key set instead of hitting the real Cloudflare Access certs\n * endpoint.\n */\nimport { createRemoteJWKSet } from \"jose\";\n\n/** Remote JWKS cache keyed by team-domain URL. */\nconst jwksCache = new Map<string, ReturnType<typeof createRemoteJWKSet>>();\n\n/**\n * Upper bound on {@link jwksCache}'s size. `teamDomain` is normally static per-deployment\n * config (one, or a handful, of distinct values for the lifetime of a Worker), but nothing\n * prevents a caller from passing a dynamically-sourced value; capping the cache avoids unbounded\n * memory growth in that case. Eviction is FIFO (oldest-inserted entry first, via `Map`'s\n * insertion-order iteration) rather than true LRU — proportionate to how rarely this cache is\n * expected to near its limit at all.\n */\nexport const MAX_JWKS_CACHE_ENTRIES = 20;\n\n/**\n * Pattern a Cloudflare Access team-domain hostname must match: a single DNS label followed by\n * the literal `cloudflareaccess.com` suffix (e.g. `\"my-team.cloudflareaccess.com\"`). Matched\n * against `URL.hostname`, which the WHATWG URL parser always normalizes to ASCII-lowercase and\n * which already resolves away userinfo (`user@host`) and path-based host-spoofing tricks, so\n * this single check is sufficient to reject a `teamDomain` that does not genuinely point at\n * Cloudflare Access's own certs host.\n */\nconst TEAM_DOMAIN_HOST_PATTERN = /^[a-z0-9-]+\\.cloudflareaccess\\.com$/;\n\n/** Matches a leading URI scheme (e.g. `\"http://\"`, `\"ftp://\"`), case-insensitively. */\nconst SCHEME_PATTERN = /^[a-z][a-z0-9+.-]*:\\/\\//i;\n\n/**\n * Normalize and validate a Cloudflare Access team domain to its canonical `https://` origin.\n *\n * Used both to build the JWKS certs URL in {@link getRemoteJwks} and to compute the expected\n * `iss` (Issuer) claim value for `jwtVerify` in `verifyAccessJwt` (SEC-003) — a single shared\n * implementation keeps the two in permanent agreement instead of risking two independent\n * normalizations drifting apart.\n *\n * @param teamDomain - The Cloudflare Access team domain (e.g. `\"my-team.cloudflareaccess.com\"`).\n * A missing `https://` prefix is added automatically, and a trailing slash is stripped, so\n * `\"my-team.cloudflareaccess.com\"`, `\"my-team.cloudflareaccess.com/\"`, and\n * `\"https://my-team.cloudflareaccess.com\"` all normalize to the same origin.\n * @returns The canonical origin, e.g. `\"https://my-team.cloudflareaccess.com\"` — no trailing\n * slash, no path.\n * @throws {Error} If `teamDomain` has an explicit non-`https://` scheme (see {@link ensureHttps}),\n * or if the resulting hostname is not a `*.cloudflareaccess.com` team domain — closing off both\n * the malformed-URL footgun and the SSRF/JWKS-poisoning surface of a dynamically-sourced\n * `teamDomain` pointing somewhere unexpected.\n */\nexport function normalizeTeamDomain(teamDomain: string): string {\n // Normalise: strip trailing slash, ensure https prefix.\n const base = teamDomain.replace(/\\/+$/, \"\");\n const url = ensureHttps(base);\n const parsed = new URL(url);\n\n if (!TEAM_DOMAIN_HOST_PATTERN.test(parsed.hostname)) {\n throw new Error(`Invalid Cloudflare Access team domain: \"${teamDomain}\"`);\n }\n\n return parsed.origin;\n}\n\n/**\n * Return (or create-and-cache) a remote JWKS function for the given Cloudflare Access team\n * domain.\n *\n * @param teamDomain - The Cloudflare Access team domain. See {@link normalizeTeamDomain} for the\n * accepted input forms and normalization rules.\n * @returns The `jose` remote JWK set function for `${teamDomain}/cdn-cgi/access/certs`, cached\n * across calls for the same normalized domain.\n * @throws {Error} See {@link normalizeTeamDomain}.\n */\nexport function getRemoteJwks(teamDomain: string): ReturnType<typeof createRemoteJWKSet> {\n const origin = normalizeTeamDomain(teamDomain);\n const certsUrl = new URL(`${origin}/cdn-cgi/access/certs`);\n\n let jwks = jwksCache.get(certsUrl.href);\n if (!jwks) {\n if (jwksCache.size >= MAX_JWKS_CACHE_ENTRIES) {\n // `Map` iterates keys in insertion order, so the first key yielded here is the oldest\n // entry. `size >= MAX_JWKS_CACHE_ENTRIES` (checked above) with `MAX_JWKS_CACHE_ENTRIES`\n // fixed at a positive constant guarantees the cache is non-empty, so this loop always runs\n // at least once — no \"keys() was empty\" branch to guard against.\n for (const oldestKey of jwksCache.keys()) {\n jwksCache.delete(oldestKey);\n break;\n }\n }\n jwks = createRemoteJWKSet(certsUrl);\n jwksCache.set(certsUrl.href, jwks);\n }\n return jwks;\n}\n\n/**\n * Ensures the base URL for the JWKS endpoint is HTTPS.\n *\n * @param url - The base URL to check.\n * @returns The URL unchanged if it already starts with `https://`; otherwise `https://` is\n * prepended, but only when `url` has no scheme of its own.\n * @throws {Error} If `url` already has an explicit scheme other than `https://` (e.g.\n * `\"http://...\"`, `\"ftp://...\"`) — such input previously became a malformed URL\n * (`\"https://http://...\"`) rather than being upgraded or rejected. Erroring here surfaces the\n * misconfiguration instead of silently constructing a broken (and, for a userinfo-style value,\n * potentially misleading) JWKS URL.\n */\nexport function ensureHttps(url: string): string {\n if (url.startsWith(\"https://\")) {\n return url;\n }\n if (SCHEME_PATTERN.test(url)) {\n throw new Error(`Expected an https:// URL, got: \"${url}\"`);\n }\n return \"https://\" + url;\n}\n","/**\n * @file JWT helpers for the Cloudflare Access authentication internals: signing and verifying\n * developer tokens, verifying real Cloudflare Access tokens, and reading/writing the\n * authorization cookie.\n *\n * Uses `jose` v6 for all cryptographic operations and only Web-standard APIs\n * (`crypto.randomUUID`, `TextEncoder`) otherwise, so this module is both Worker-safe (for\n * `hono/cloudflare-access.ts`) and Node-safe (for `vite/plugin.ts`).\n */\nimport { SignJWT, jwtVerify, errors as joseErrors } from \"jose\";\nimport type { JWTPayload } from \"jose\";\nimport type { AccessJwtPayload } from \"./types.js\";\nimport { getRemoteJwks, normalizeTeamDomain } from \"./jwks.js\";\nimport type { Logger } from \"../logging/types.js\";\n\n// ---------------------------------------------------------------------------\n// Constants\n// ---------------------------------------------------------------------------\n\n/**\n * Well-known HMAC secret used by default for signing and verifying developer-generated JWTs.\n *\n * **This is NOT a real secret.** It only protects the local-dev login flow running on\n * `localhost` and must never be relied on for production security.\n */\nexport const DEFAULT_DEV_SECRET = \"cloudflare-access-dev-secret-do-not-use-in-production\";\n\n/** Algorithm used for developer-signed JWTs. */\nconst DEV_ALG = \"HS256\";\n\n/**\n * Algorithm Cloudflare Access uses to sign real application tokens (confirmed by Cloudflare's\n * own application-token documentation: the JWT header is always `{\"alg\":\"RS256\",...}`, signed\n * with an RSA private key). Pinned explicitly on the JWKS `jwtVerify` call in\n * {@link verifyAccessJwt} (SEC-004/CODE-003) rather than left unconstrained — `jose` already\n * refuses to HMAC-verify against an asymmetric JWK, so this is defense-in-depth, not a fix for a\n * currently-exploitable gap, and it mirrors {@link DEV_ALG} being pinned on the dev-token path.\n * Not exposed as a public override: unlike `audience` (a per-deployment Access Application\n * value), the signing algorithm is a Cloudflare Access platform constant, and widening it would\n * reopen the exact `alg`-confusion class this pin closes.\n */\nconst ACCESS_ALG = \"RS256\";\n\n/** Name of the cookie that stores the JWT. */\nexport const COOKIE_NAME = \"CF_Authorization\";\n\n/** Header containing the JWT (set by Cloudflare Access). */\nexport const JWT_HEADER = \"cf-access-jwt-assertion\";\n\n/** Header containing the authenticated user's email. */\nexport const EMAIL_HEADER = \"cf-access-authenticated-user-email\";\n\n// ---------------------------------------------------------------------------\n// Key helpers\n// ---------------------------------------------------------------------------\n\n/** Encode a string secret into a `CryptoKey`-compatible `Uint8Array`. */\nfunction secretToBytes(secret: string): Uint8Array {\n return new TextEncoder().encode(secret);\n}\n\n// ---------------------------------------------------------------------------\n// Sign (developer mode only)\n// ---------------------------------------------------------------------------\n\n/**\n * Generate a subject identifier for a developer-signed JWT.\n *\n * Returns a random UUID so that the `sub` claim matches the shape of a real Cloudflare Access\n * subject (a UUID) and satisfies strict downstream validators (e.g. `[A-Za-z0-9-]`). Real\n * Access subjects are stable per-user; dev subjects are stable for the life of an issued token\n * (and can be pinned via the `sub` option).\n */\nfunction generateDevSub(): string {\n return crypto.randomUUID();\n}\n\n/**\n * Create a signed JWT that mimics a Cloudflare Access token.\n *\n * The `type` claim is set to `\"dev\"` so that the verification layer can distinguish\n * locally-issued tokens from real Access tokens.\n *\n * @param email - The user's email address (becomes the `email` claim).\n * @param options - Optional overrides.\n * @param options.secret - HMAC signing secret (default `DEFAULT_DEV_SECRET`).\n * @param options.lifetime - Token lifetime in seconds (default `86400` / 24 h).\n * @param options.sub - Subject claim. When provided it is used **verbatim**; when omitted a\n * random UUID is generated (matching the shape of a real Cloudflare Access `sub`) instead of\n * an email-derived value.\n */\nexport async function signDevJwt(\n email: string,\n options: { secret?: string; lifetime?: number; sub?: string } = {}\n): Promise<string> {\n const secret = options.secret ?? DEFAULT_DEV_SECRET;\n const lifetime = options.lifetime ?? 86_400; // 24 h\n\n const now = Math.floor(Date.now() / 1000);\n const sub = options.sub ?? generateDevSub();\n\n return new SignJWT({\n email,\n sub,\n type: \"dev\",\n iss: \"dev-authentication\"\n } satisfies Omit<AccessJwtPayload, \"iat\" | \"exp\">)\n .setProtectedHeader({ alg: DEV_ALG })\n .setIssuedAt(now)\n .setExpirationTime(now + lifetime)\n .sign(secretToBytes(secret));\n}\n\n// ---------------------------------------------------------------------------\n// Verify\n// ---------------------------------------------------------------------------\n\n/** Result of a successful JWT verification. */\nexport interface VerifiedToken {\n /** Authenticated user's email address (from the JWT `email` claim). */\n email: string;\n /** Authenticated user's unique identifier (from the JWT `sub` claim). */\n sub: string;\n}\n\n/**\n * Attempt to verify a JWT as a developer-signed token.\n *\n * Returns `null` if verification fails (wrong key, expired, missing claims, etc.) — the caller\n * can then fall back to Cloudflare JWKS verification.\n */\nexport async function verifyDevJwt(\n token: string,\n secret: string = DEFAULT_DEV_SECRET\n): Promise<VerifiedToken | null> {\n try {\n const { payload } = await jwtVerify(token, secretToBytes(secret), {\n algorithms: [DEV_ALG]\n });\n return extractClaims(payload);\n } catch {\n return null;\n }\n}\n\n/**\n * `jose` error classes that only occur once a JWKS was successfully fetched and a real\n * cryptographic/claims verification attempt against the token was made — i.e. the token itself\n * is what failed, not the JWKS transport.\n *\n * `JWKSNoMatchingKey` is included because `createRemoteJWKSet` already retries a JWKS reload\n * once (subject to a cooldown) before finally throwing it — a final throw means the token's\n * `kid` genuinely doesn't match even after a fresh key fetch, not merely a stale local cache.\n */\nconst TOKEN_VALIDATION_ERRORS = [\n joseErrors.JWSSignatureVerificationFailed,\n joseErrors.JWTExpired,\n joseErrors.JWTClaimValidationFailed,\n joseErrors.JOSEAlgNotAllowed,\n joseErrors.JWTInvalid,\n joseErrors.JWSInvalid,\n joseErrors.JWKSNoMatchingKey\n] as const;\n\n/**\n * Classify a caught `verifyAccessJwt` failure as `\"invalid\"` (the token itself failed\n * cryptographic or claims validation) or `\"network\"` (a JWKS transport/config/infra problem),\n * for the `cause` field attached to the diagnostic log record.\n *\n * Conservatively biased toward `\"network\"`: only the specific {@link TOKEN_VALIDATION_ERRORS}\n * classes are treated as `\"invalid\"`. Everything else — plain fetch/DNS errors (not a\n * `JOSEError` at all, since `getRemoteJwks`'s underlying fetch failures propagate unwrapped),\n * `JWKSTimeout`, and JWKS-structure errors such as `JWKSInvalid`/`JWKSMultipleMatchingKeys` — is\n * classified as `\"network\"`, because misclassifying a real outage as \"just an invalid token\" is\n * the exact debuggability gap this diagnostic exists to close.\n *\n * @param err - The value caught from the `jwtVerify`/`getRemoteJwks` call.\n * @returns `\"invalid\"` when `err` is one of {@link TOKEN_VALIDATION_ERRORS}; `\"network\"`\n * otherwise.\n */\nfunction classifyVerificationFailure(err: unknown): \"network\" | \"invalid\" {\n const isTokenValidationError = TOKEN_VALIDATION_ERRORS.some(\n (ErrorClass) => err instanceof ErrorClass\n );\n return isTokenValidationError ? \"invalid\" : \"network\";\n}\n\n/**\n * Verify a JWT against Cloudflare Access's remote JWKS endpoint.\n *\n * Only {@link ACCESS_ALG} (`\"RS256\"`) is accepted (SEC-004/CODE-003) — this matches what\n * Cloudflare Access actually issues and closes off `alg`-confusion attacks (e.g. a crafted\n * `alg: \"HS256\"` header) as a matter of explicit policy rather than relying solely on `jose`'s\n * own asymmetric-JWK/HMAC mismatch behavior.\n *\n * The `iss` (Issuer) claim is always verified against the normalized `teamDomain` (e.g.\n * `\"https://my-team.cloudflareaccess.com\"`, via {@link normalizeTeamDomain}) — SEC-003. This is\n * defense-in-depth: the JWKS itself is already team-scoped (fetched from\n * `teamDomain/cdn-cgi/access/certs`), so a token signed by a different team's key would already\n * fail signature verification, but binding `iss` explicitly matches Cloudflare's own published\n * Worker+Access reference implementation and requires the presence of the `iss` claim, so a\n * token without one is rejected too.\n *\n * When `audience` is provided the `aud` claim is also verified. **When `audience` is omitted,\n * `aud` is not checked at all** — because every Cloudflare Access application in an account\n * shares the same team JWKS, this means a token minted for *any other Access application in the\n * same team* is accepted here too (cross-application token replay). Callers that expose this\n * through a public option (e.g. `hono/cloudflare-access.ts`'s `cloudflareAccess`) should warn\n * loudly when a caller omits `audience` outside of a clearly-local-development configuration.\n *\n * A transient JWKS network/infra failure (bad team domain, DNS blip, certs endpoint down) is\n * otherwise indistinguishable from a genuinely invalid token — both fall through to the same\n * `catch` and the same `null` return, and without a `logger` nothing is recorded. When `logger`\n * is provided, the caught error is recorded at `warn` (matching the generic\n * `\"JWT verification failed\"` warning callers such as `cloudflareAccess` already emit) together\n * with the raw `err` and a best-effort `cause: \"network\" | \"invalid\"` classification (see\n * {@link classifyVerificationFailure}), so operators can distinguish an outage from a rejected\n * token without changing the fail-closed `null` return contract.\n *\n * @param token - The compact JWS to verify.\n * @param teamDomain - The Cloudflare Access team domain used to fetch the public JWKS and to\n * compute the expected `iss` claim value (see {@link normalizeTeamDomain}).\n * @param audience - Application Audience Tag to verify the `aud` claim against. When omitted,\n * `aud` is not checked at all — see the security remarks above.\n * @param logger - Optional structured logger. When omitted, verification failures are still\n * returned as `null` but nothing is logged (unchanged prior behavior).\n */\nexport async function verifyAccessJwt(\n token: string,\n teamDomain: string,\n audience?: string,\n logger?: Logger\n): Promise<VerifiedToken | null> {\n try {\n const jwks = getRemoteJwks(teamDomain);\n const { payload } = await jwtVerify(token, jwks, {\n algorithms: [ACCESS_ALG],\n audience: audience ?? undefined,\n issuer: normalizeTeamDomain(teamDomain)\n });\n return extractClaims(payload);\n } catch (err) {\n logger?.warn(\"Cloudflare Access JWT verification failed\", {\n err,\n teamDomain,\n cause: classifyVerificationFailure(err)\n });\n return null;\n }\n}\n\n// ---------------------------------------------------------------------------\n// Helpers\n// ---------------------------------------------------------------------------\n\n/** Pull the required claims out of a verified payload. */\nexport function extractClaims(payload: JWTPayload): VerifiedToken | null {\n const email = payload.email;\n const sub = payload.sub;\n\n if (typeof email !== \"string\" || !email) {\n return null;\n }\n if (typeof sub !== \"string\" || !sub) {\n return null;\n }\n\n return { email, sub };\n}\n\n// ---------------------------------------------------------------------------\n// Cookie helpers\n// ---------------------------------------------------------------------------\n\n/**\n * Build a `Set-Cookie` header value for the authorisation cookie.\n *\n * Mirrors the attributes used by Cloudflare Access: `HttpOnly; Secure; SameSite=Lax; Path=/`\n *\n * For local dev over plain HTTP the `Secure` flag is omitted when the request was made to\n * `localhost` or `127.0.0.1`.\n */\nexport function buildCookieHeader(token: string, isSecure: boolean): string {\n const parts = [`${COOKIE_NAME}=${token}`, \"HttpOnly\", \"SameSite=Lax\", \"Path=/\"];\n if (isSecure) {\n parts.push(\"Secure\");\n }\n return parts.join(\"; \");\n}\n\n/**\n * Build a `Set-Cookie` header that clears the `CF_Authorization` cookie by setting it to an\n * empty value with `Max-Age=0`.\n *\n * Use this when a stale or invalid cookie needs to be removed so the user can re-authenticate.\n */\nexport function clearCookieHeader(): string {\n return `${COOKIE_NAME}=; HttpOnly; SameSite=Lax; Path=/; Max-Age=0`;\n}\n\n/**\n * Parse the value of the `CF_Authorization` cookie from a `Cookie` header string. Returns\n * `undefined` when the cookie is absent.\n */\nexport function parseCookie(cookieHeader: string | null | undefined): string | undefined {\n if (!cookieHeader) return undefined;\n\n for (const pair of cookieHeader.split(\";\")) {\n const [name, ...rest] = pair.split(\"=\");\n if (name.trim() === COOKIE_NAME) {\n return rest.join(\"=\").trim();\n }\n }\n return undefined;\n}\n"],"mappings":"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"}
@@ -12,7 +12,7 @@ declare const JWT_HEADER = "cf-access-jwt-assertion";
12
12
  *
13
13
  * @param email - The user's email address (becomes the `email` claim).
14
14
  * @param options - Optional overrides.
15
- * @param options.secret - HMAC signing secret (default {@link DEFAULT_DEV_SECRET}).
15
+ * @param options.secret - HMAC signing secret (default `DEFAULT_DEV_SECRET`).
16
16
  * @param options.lifetime - Token lifetime in seconds (default `86400` / 24 h).
17
17
  * @param options.sub - Subject claim. When provided it is used **verbatim**; when omitted a
18
18
  * random UUID is generated (matching the shape of a real Cloudflare Access `sub`) instead of
@@ -1,2 +1,2 @@
1
- import { a as buildCookieHeader, c as signDevJwt, i as JWT_HEADER, o as clearCookieHeader, t as COOKIE_NAME } from "../jwt-BvuKtvby.js";
1
+ import { a as buildCookieHeader, c as signDevJwt, i as JWT_HEADER, o as clearCookieHeader, t as COOKIE_NAME } from "../jwt-DJTB3ay0.js";
2
2
  export { COOKIE_NAME, JWT_HEADER, buildCookieHeader, clearCookieHeader, signDevJwt };
@@ -1,6 +1,6 @@
1
1
  import { n as ProblemDetailsError } from "../factory-BI5gVL_P.js";
2
2
  import { n as contentTooLarge } from "../generators-D8WWEHa1.js";
3
- import { a as buildCookieHeader, c as signDevJwt, i as JWT_HEADER, o as clearCookieHeader, r as EMAIL_HEADER, s as parseCookie, u as verifyDevJwt } from "../jwt-BvuKtvby.js";
3
+ import { a as buildCookieHeader, c as signDevJwt, i as JWT_HEADER, o as clearCookieHeader, r as EMAIL_HEADER, s as parseCookie, u as verifyDevJwt } from "../jwt-DJTB3ay0.js";
4
4
  import { t as matchPolicy } from "../policy-CvS6AvvD.js";
5
5
  //#region src/lib/vite/login-page.ts
6
6
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adrianhall/cloudflare-toolkit",
3
- "version": "1.0.0",
3
+ "version": "1.0.1",
4
4
  "description": "A toolkit of utilities and skills for developing Workers on the Cloudflare Dev Platform",
5
5
  "keywords": [
6
6
  "cloudflare",
@@ -1 +0,0 @@
1
- {"version":3,"file":"jwt-BvuKtvby.js","names":["joseErrors"],"sources":["../src/lib/auth-internal/jwks.ts","../src/lib/auth-internal/jwt.ts"],"sourcesContent":["/**\n * @file Remote JWKS management for Cloudflare Access JWT verification.\n *\n * Only Web-standard APIs (`jose`, `URL`, `Map`) are used, so this module is both Worker-safe\n * (for `hono/`) and Node-safe (for `vite/`). Kept in its own module so tests can mock\n * `getRemoteJwks` and supply a local key set instead of hitting the real Cloudflare Access certs\n * endpoint.\n */\nimport { createRemoteJWKSet } from \"jose\";\n\n/** Remote JWKS cache keyed by team-domain URL. */\nconst jwksCache = new Map<string, ReturnType<typeof createRemoteJWKSet>>();\n\n/**\n * Upper bound on {@link jwksCache}'s size. `teamDomain` is normally static per-deployment\n * config (one, or a handful, of distinct values for the lifetime of a Worker), but nothing\n * prevents a caller from passing a dynamically-sourced value; capping the cache avoids unbounded\n * memory growth in that case. Eviction is FIFO (oldest-inserted entry first, via `Map`'s\n * insertion-order iteration) rather than true LRU — proportionate to how rarely this cache is\n * expected to near its limit at all.\n */\nexport const MAX_JWKS_CACHE_ENTRIES = 20;\n\n/**\n * Pattern a Cloudflare Access team-domain hostname must match: a single DNS label followed by\n * the literal `cloudflareaccess.com` suffix (e.g. `\"my-team.cloudflareaccess.com\"`). Matched\n * against `URL.hostname`, which the WHATWG URL parser always normalizes to ASCII-lowercase and\n * which already resolves away userinfo (`user@host`) and path-based host-spoofing tricks, so\n * this single check is sufficient to reject a `teamDomain` that does not genuinely point at\n * Cloudflare Access's own certs host.\n */\nconst TEAM_DOMAIN_HOST_PATTERN = /^[a-z0-9-]+\\.cloudflareaccess\\.com$/;\n\n/** Matches a leading URI scheme (e.g. `\"http://\"`, `\"ftp://\"`), case-insensitively. */\nconst SCHEME_PATTERN = /^[a-z][a-z0-9+.-]*:\\/\\//i;\n\n/**\n * Normalize and validate a Cloudflare Access team domain to its canonical `https://` origin.\n *\n * Used both to build the JWKS certs URL in {@link getRemoteJwks} and to compute the expected\n * `iss` (Issuer) claim value for `jwtVerify` in `verifyAccessJwt` (SEC-003) — a single shared\n * implementation keeps the two in permanent agreement instead of risking two independent\n * normalizations drifting apart.\n *\n * @param teamDomain - The Cloudflare Access team domain (e.g. `\"my-team.cloudflareaccess.com\"`).\n * A missing `https://` prefix is added automatically, and a trailing slash is stripped, so\n * `\"my-team.cloudflareaccess.com\"`, `\"my-team.cloudflareaccess.com/\"`, and\n * `\"https://my-team.cloudflareaccess.com\"` all normalize to the same origin.\n * @returns The canonical origin, e.g. `\"https://my-team.cloudflareaccess.com\"` — no trailing\n * slash, no path.\n * @throws {Error} If `teamDomain` has an explicit non-`https://` scheme (see {@link ensureHttps}),\n * or if the resulting hostname is not a `*.cloudflareaccess.com` team domain — closing off both\n * the malformed-URL footgun and the SSRF/JWKS-poisoning surface of a dynamically-sourced\n * `teamDomain` pointing somewhere unexpected.\n */\nexport function normalizeTeamDomain(teamDomain: string): string {\n // Normalise: strip trailing slash, ensure https prefix.\n const base = teamDomain.replace(/\\/+$/, \"\");\n const url = ensureHttps(base);\n const parsed = new URL(url);\n\n if (!TEAM_DOMAIN_HOST_PATTERN.test(parsed.hostname)) {\n throw new Error(`Invalid Cloudflare Access team domain: \"${teamDomain}\"`);\n }\n\n return parsed.origin;\n}\n\n/**\n * Return (or create-and-cache) a remote JWKS function for the given Cloudflare Access team\n * domain.\n *\n * @param teamDomain - The Cloudflare Access team domain. See {@link normalizeTeamDomain} for the\n * accepted input forms and normalization rules.\n * @returns The `jose` remote JWK set function for `${teamDomain}/cdn-cgi/access/certs`, cached\n * across calls for the same normalized domain.\n * @throws {Error} See {@link normalizeTeamDomain}.\n */\nexport function getRemoteJwks(teamDomain: string): ReturnType<typeof createRemoteJWKSet> {\n const origin = normalizeTeamDomain(teamDomain);\n const certsUrl = new URL(`${origin}/cdn-cgi/access/certs`);\n\n let jwks = jwksCache.get(certsUrl.href);\n if (!jwks) {\n if (jwksCache.size >= MAX_JWKS_CACHE_ENTRIES) {\n // `Map` iterates keys in insertion order, so the first key yielded here is the oldest\n // entry. `size >= MAX_JWKS_CACHE_ENTRIES` (checked above) with `MAX_JWKS_CACHE_ENTRIES`\n // fixed at a positive constant guarantees the cache is non-empty, so this loop always runs\n // at least once — no \"keys() was empty\" branch to guard against.\n for (const oldestKey of jwksCache.keys()) {\n jwksCache.delete(oldestKey);\n break;\n }\n }\n jwks = createRemoteJWKSet(certsUrl);\n jwksCache.set(certsUrl.href, jwks);\n }\n return jwks;\n}\n\n/**\n * Ensures the base URL for the JWKS endpoint is HTTPS.\n *\n * @param url - The base URL to check.\n * @returns The URL unchanged if it already starts with `https://`; otherwise `https://` is\n * prepended, but only when `url` has no scheme of its own.\n * @throws {Error} If `url` already has an explicit scheme other than `https://` (e.g.\n * `\"http://...\"`, `\"ftp://...\"`) — such input previously became a malformed URL\n * (`\"https://http://...\"`) rather than being upgraded or rejected. Erroring here surfaces the\n * misconfiguration instead of silently constructing a broken (and, for a userinfo-style value,\n * potentially misleading) JWKS URL.\n */\nexport function ensureHttps(url: string): string {\n if (url.startsWith(\"https://\")) {\n return url;\n }\n if (SCHEME_PATTERN.test(url)) {\n throw new Error(`Expected an https:// URL, got: \"${url}\"`);\n }\n return \"https://\" + url;\n}\n","/**\n * @file JWT helpers for the Cloudflare Access authentication internals: signing and verifying\n * developer tokens, verifying real Cloudflare Access tokens, and reading/writing the\n * authorization cookie.\n *\n * Uses `jose` v6 for all cryptographic operations and only Web-standard APIs\n * (`crypto.randomUUID`, `TextEncoder`) otherwise, so this module is both Worker-safe (for\n * `hono/cloudflare-access.ts`) and Node-safe (for `vite/plugin.ts`).\n */\nimport { SignJWT, jwtVerify, errors as joseErrors } from \"jose\";\nimport type { JWTPayload } from \"jose\";\nimport type { AccessJwtPayload } from \"./types.js\";\nimport { getRemoteJwks, normalizeTeamDomain } from \"./jwks.js\";\nimport type { Logger } from \"../logging/types.js\";\n\n// ---------------------------------------------------------------------------\n// Constants\n// ---------------------------------------------------------------------------\n\n/**\n * Well-known HMAC secret used by default for signing and verifying developer-generated JWTs.\n *\n * **This is NOT a real secret.** It only protects the local-dev login flow running on\n * `localhost` and must never be relied on for production security.\n */\nexport const DEFAULT_DEV_SECRET = \"cloudflare-access-dev-secret-do-not-use-in-production\";\n\n/** Algorithm used for developer-signed JWTs. */\nconst DEV_ALG = \"HS256\";\n\n/**\n * Algorithm Cloudflare Access uses to sign real application tokens (confirmed by Cloudflare's\n * own application-token documentation: the JWT header is always `{\"alg\":\"RS256\",...}`, signed\n * with an RSA private key). Pinned explicitly on the JWKS `jwtVerify` call in\n * {@link verifyAccessJwt} (SEC-004/CODE-003) rather than left unconstrained — `jose` already\n * refuses to HMAC-verify against an asymmetric JWK, so this is defense-in-depth, not a fix for a\n * currently-exploitable gap, and it mirrors {@link DEV_ALG} being pinned on the dev-token path.\n * Not exposed as a public override: unlike `audience` (a per-deployment Access Application\n * value), the signing algorithm is a Cloudflare Access platform constant, and widening it would\n * reopen the exact `alg`-confusion class this pin closes.\n */\nconst ACCESS_ALG = \"RS256\";\n\n/** Name of the cookie that stores the JWT. */\nexport const COOKIE_NAME = \"CF_Authorization\";\n\n/** Header containing the JWT (set by Cloudflare Access). */\nexport const JWT_HEADER = \"cf-access-jwt-assertion\";\n\n/** Header containing the authenticated user's email. */\nexport const EMAIL_HEADER = \"cf-access-authenticated-user-email\";\n\n// ---------------------------------------------------------------------------\n// Key helpers\n// ---------------------------------------------------------------------------\n\n/** Encode a string secret into a `CryptoKey`-compatible `Uint8Array`. */\nfunction secretToBytes(secret: string): Uint8Array {\n return new TextEncoder().encode(secret);\n}\n\n// ---------------------------------------------------------------------------\n// Sign (developer mode only)\n// ---------------------------------------------------------------------------\n\n/**\n * Generate a subject identifier for a developer-signed JWT.\n *\n * Returns a random UUID so that the `sub` claim matches the shape of a real Cloudflare Access\n * subject (a UUID) and satisfies strict downstream validators (e.g. `[A-Za-z0-9-]`). Real\n * Access subjects are stable per-user; dev subjects are stable for the life of an issued token\n * (and can be pinned via the `sub` option).\n */\nfunction generateDevSub(): string {\n return crypto.randomUUID();\n}\n\n/**\n * Create a signed JWT that mimics a Cloudflare Access token.\n *\n * The `type` claim is set to `\"dev\"` so that the verification layer can distinguish\n * locally-issued tokens from real Access tokens.\n *\n * @param email - The user's email address (becomes the `email` claim).\n * @param options - Optional overrides.\n * @param options.secret - HMAC signing secret (default {@link DEFAULT_DEV_SECRET}).\n * @param options.lifetime - Token lifetime in seconds (default `86400` / 24 h).\n * @param options.sub - Subject claim. When provided it is used **verbatim**; when omitted a\n * random UUID is generated (matching the shape of a real Cloudflare Access `sub`) instead of\n * an email-derived value.\n */\nexport async function signDevJwt(\n email: string,\n options: { secret?: string; lifetime?: number; sub?: string } = {}\n): Promise<string> {\n const secret = options.secret ?? DEFAULT_DEV_SECRET;\n const lifetime = options.lifetime ?? 86_400; // 24 h\n\n const now = Math.floor(Date.now() / 1000);\n const sub = options.sub ?? generateDevSub();\n\n return new SignJWT({\n email,\n sub,\n type: \"dev\",\n iss: \"dev-authentication\"\n } satisfies Omit<AccessJwtPayload, \"iat\" | \"exp\">)\n .setProtectedHeader({ alg: DEV_ALG })\n .setIssuedAt(now)\n .setExpirationTime(now + lifetime)\n .sign(secretToBytes(secret));\n}\n\n// ---------------------------------------------------------------------------\n// Verify\n// ---------------------------------------------------------------------------\n\n/** Result of a successful JWT verification. */\nexport interface VerifiedToken {\n /** Authenticated user's email address (from the JWT `email` claim). */\n email: string;\n /** Authenticated user's unique identifier (from the JWT `sub` claim). */\n sub: string;\n}\n\n/**\n * Attempt to verify a JWT as a developer-signed token.\n *\n * Returns `null` if verification fails (wrong key, expired, missing claims, etc.) — the caller\n * can then fall back to Cloudflare JWKS verification.\n */\nexport async function verifyDevJwt(\n token: string,\n secret: string = DEFAULT_DEV_SECRET\n): Promise<VerifiedToken | null> {\n try {\n const { payload } = await jwtVerify(token, secretToBytes(secret), {\n algorithms: [DEV_ALG]\n });\n return extractClaims(payload);\n } catch {\n return null;\n }\n}\n\n/**\n * `jose` error classes that only occur once a JWKS was successfully fetched and a real\n * cryptographic/claims verification attempt against the token was made — i.e. the token itself\n * is what failed, not the JWKS transport.\n *\n * `JWKSNoMatchingKey` is included because `createRemoteJWKSet` already retries a JWKS reload\n * once (subject to a cooldown) before finally throwing it — a final throw means the token's\n * `kid` genuinely doesn't match even after a fresh key fetch, not merely a stale local cache.\n */\nconst TOKEN_VALIDATION_ERRORS = [\n joseErrors.JWSSignatureVerificationFailed,\n joseErrors.JWTExpired,\n joseErrors.JWTClaimValidationFailed,\n joseErrors.JOSEAlgNotAllowed,\n joseErrors.JWTInvalid,\n joseErrors.JWSInvalid,\n joseErrors.JWKSNoMatchingKey\n] as const;\n\n/**\n * Classify a caught `verifyAccessJwt` failure as `\"invalid\"` (the token itself failed\n * cryptographic or claims validation) or `\"network\"` (a JWKS transport/config/infra problem),\n * for the `cause` field attached to the diagnostic log record.\n *\n * Conservatively biased toward `\"network\"`: only the specific {@link TOKEN_VALIDATION_ERRORS}\n * classes are treated as `\"invalid\"`. Everything else — plain fetch/DNS errors (not a\n * `JOSEError` at all, since `getRemoteJwks`'s underlying fetch failures propagate unwrapped),\n * `JWKSTimeout`, and JWKS-structure errors such as `JWKSInvalid`/`JWKSMultipleMatchingKeys` — is\n * classified as `\"network\"`, because misclassifying a real outage as \"just an invalid token\" is\n * the exact debuggability gap this diagnostic exists to close.\n *\n * @param err - The value caught from the `jwtVerify`/`getRemoteJwks` call.\n * @returns `\"invalid\"` when `err` is one of {@link TOKEN_VALIDATION_ERRORS}; `\"network\"`\n * otherwise.\n */\nfunction classifyVerificationFailure(err: unknown): \"network\" | \"invalid\" {\n const isTokenValidationError = TOKEN_VALIDATION_ERRORS.some(\n (ErrorClass) => err instanceof ErrorClass\n );\n return isTokenValidationError ? \"invalid\" : \"network\";\n}\n\n/**\n * Verify a JWT against Cloudflare Access's remote JWKS endpoint.\n *\n * Only {@link ACCESS_ALG} (`\"RS256\"`) is accepted (SEC-004/CODE-003) — this matches what\n * Cloudflare Access actually issues and closes off `alg`-confusion attacks (e.g. a crafted\n * `alg: \"HS256\"` header) as a matter of explicit policy rather than relying solely on `jose`'s\n * own asymmetric-JWK/HMAC mismatch behavior.\n *\n * The `iss` (Issuer) claim is always verified against the normalized `teamDomain` (e.g.\n * `\"https://my-team.cloudflareaccess.com\"`, via {@link normalizeTeamDomain}) — SEC-003. This is\n * defense-in-depth: the JWKS itself is already team-scoped (fetched from\n * `teamDomain/cdn-cgi/access/certs`), so a token signed by a different team's key would already\n * fail signature verification, but binding `iss` explicitly matches Cloudflare's own published\n * Worker+Access reference implementation and requires the presence of the `iss` claim, so a\n * token without one is rejected too.\n *\n * When `audience` is provided the `aud` claim is also verified. **When `audience` is omitted,\n * `aud` is not checked at all** — because every Cloudflare Access application in an account\n * shares the same team JWKS, this means a token minted for *any other Access application in the\n * same team* is accepted here too (cross-application token replay). Callers that expose this\n * through a public option (e.g. `hono/cloudflare-access.ts`'s `cloudflareAccess`) should warn\n * loudly when a caller omits `audience` outside of a clearly-local-development configuration.\n *\n * A transient JWKS network/infra failure (bad team domain, DNS blip, certs endpoint down) is\n * otherwise indistinguishable from a genuinely invalid token — both fall through to the same\n * `catch` and the same `null` return, and without a `logger` nothing is recorded. When `logger`\n * is provided, the caught error is recorded at `warn` (matching the generic\n * `\"JWT verification failed\"` warning callers such as `cloudflareAccess` already emit) together\n * with the raw `err` and a best-effort `cause: \"network\" | \"invalid\"` classification (see\n * {@link classifyVerificationFailure}), so operators can distinguish an outage from a rejected\n * token without changing the fail-closed `null` return contract.\n *\n * @param token - The compact JWS to verify.\n * @param teamDomain - The Cloudflare Access team domain used to fetch the public JWKS and to\n * compute the expected `iss` claim value (see {@link normalizeTeamDomain}).\n * @param audience - Application Audience Tag to verify the `aud` claim against. When omitted,\n * `aud` is not checked at all — see the security remarks above.\n * @param logger - Optional structured logger. When omitted, verification failures are still\n * returned as `null` but nothing is logged (unchanged prior behavior).\n */\nexport async function verifyAccessJwt(\n token: string,\n teamDomain: string,\n audience?: string,\n logger?: Logger\n): Promise<VerifiedToken | null> {\n try {\n const jwks = getRemoteJwks(teamDomain);\n const { payload } = await jwtVerify(token, jwks, {\n algorithms: [ACCESS_ALG],\n audience: audience ?? undefined,\n issuer: normalizeTeamDomain(teamDomain)\n });\n return extractClaims(payload);\n } catch (err) {\n logger?.warn(\"Cloudflare Access JWT verification failed\", {\n err,\n teamDomain,\n cause: classifyVerificationFailure(err)\n });\n return null;\n }\n}\n\n// ---------------------------------------------------------------------------\n// Helpers\n// ---------------------------------------------------------------------------\n\n/** Pull the required claims out of a verified payload. */\nexport function extractClaims(payload: JWTPayload): VerifiedToken | null {\n const email = payload.email;\n const sub = payload.sub;\n\n if (typeof email !== \"string\" || !email) {\n return null;\n }\n if (typeof sub !== \"string\" || !sub) {\n return null;\n }\n\n return { email, sub };\n}\n\n// ---------------------------------------------------------------------------\n// Cookie helpers\n// ---------------------------------------------------------------------------\n\n/**\n * Build a `Set-Cookie` header value for the authorisation cookie.\n *\n * Mirrors the attributes used by Cloudflare Access: `HttpOnly; Secure; SameSite=Lax; Path=/`\n *\n * For local dev over plain HTTP the `Secure` flag is omitted when the request was made to\n * `localhost` or `127.0.0.1`.\n */\nexport function buildCookieHeader(token: string, isSecure: boolean): string {\n const parts = [`${COOKIE_NAME}=${token}`, \"HttpOnly\", \"SameSite=Lax\", \"Path=/\"];\n if (isSecure) {\n parts.push(\"Secure\");\n }\n return parts.join(\"; \");\n}\n\n/**\n * Build a `Set-Cookie` header that clears the `CF_Authorization` cookie by setting it to an\n * empty value with `Max-Age=0`.\n *\n * Use this when a stale or invalid cookie needs to be removed so the user can re-authenticate.\n */\nexport function clearCookieHeader(): string {\n return `${COOKIE_NAME}=; HttpOnly; SameSite=Lax; Path=/; Max-Age=0`;\n}\n\n/**\n * Parse the value of the `CF_Authorization` cookie from a `Cookie` header string. Returns\n * `undefined` when the cookie is absent.\n */\nexport function parseCookie(cookieHeader: string | null | undefined): string | undefined {\n if (!cookieHeader) return undefined;\n\n for (const pair of cookieHeader.split(\";\")) {\n const [name, ...rest] = pair.split(\"=\");\n if (name.trim() === COOKIE_NAME) {\n return rest.join(\"=\").trim();\n }\n }\n return undefined;\n}\n"],"mappings":"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"}