diff --git a/.claude/skills/wdl-deploy/SKILL.md b/.claude/skills/wdl-deploy/SKILL.md index 0c01a08..10a3626 100644 --- a/.claude/skills/wdl-deploy/SKILL.md +++ b/.claude/skills/wdl-deploy/SKILL.md @@ -32,7 +32,11 @@ Open the relevant doc before answering: - `docs/queues.md` — `[[queues.producers]]` / `[[queues.consumers]]` config, queue handlers, message size and retry limits. - `docs/workflows.md` — `[[workflows]]` config, the WDL Workflows surface, - `wdl workflows` instance management. + `wdl workflows` definition/instance management. Both list commands accept + `--limit`/`--cursor`; follow continuation even after an empty page, and + deduplicate definition entries by worker/name across concurrent changes. On + `workflow_metadata_contention` during definition listing, remove `--cursor` + before retrying. - `docs/ai.md` — `[ai]` config, namespace provider/credential management, Responses/tools/SSE, OpenAI SDK use, and WebSocket inference. - `docs/kv.md` — `[[kv_namespaces]]`, immediately visible writes, batch reads, @@ -74,24 +78,34 @@ Wrangler's temporary bundle config. `[ai]` is standard Wrangler configuration and stays in that config for Wrangler validation. If a selected named environment omits its own `ai`, the CLI warns that the top-level binding is not inherited; WDL independently maps its `binding` into the WDL manifest. Other -fields retain their existing Wrangler passthrough behavior. Specific nested -fields that WDL cannot represent are rejected rather than silently dropped, -including Cloudflare Artifacts `triggers.events` subscriptions and R2 -`local_dev.experimental_s3_credentials`. `[wdl] session_policy` accepts -`preserve` or `restart`. The default `preserve` leaves loaded Durable Object -facets on the version that built them until the host actor restarts or the facet -is deleted, and keeps established WebSockets draining while their backend stays -healthy. `restart` closes the worker's open WebSockets with code `1012` at -promotion and retires stale facets on their next dispatch, preserving SQLite -state. Wrangler's object-shaped declarative `exports` config is unsupported. The -dry-run child hides Wrangler's banner (and its normal update check) and disables -anonymous telemetry. Wrangler may still consult the configured npm registry when -reporting an unknown configuration field; project build hooks retain their -normal network access. For `[[services]]` and `[[exports]]`, read -`docs/deploy.md`: tenant JSRPC may delegate service or Durable Object class -stubs as opaque capabilities, but the receiver cannot rewrite their -host-authored caller properties. Keep delegated stubs in memory; long-term -irrevocable stub storage is unsupported. +fields retain their existing Wrangler passthrough behavior. `[[connect]]` TCP +listeners are unsupported and rejected before bundling. Specific nested fields +that WDL cannot represent are rejected rather than silently dropped, including +Cloudflare Artifacts `triggers.events` subscriptions and R2 +`local_dev.experimental_s3_credentials`. `[[workflows]]` supports only `name`, +`binding`, and `class_name`; `script_name` and all other fields, including +`schedules`, `limits`, `default_retention`, and `concurrency`, are rejected +before bundling. Use per-instance `create()` retention instead of +`[[workflows]].default_retention`. `[wdl] session_policy` accepts `preserve` or +`restart`. The default `preserve` leaves loaded Durable Object facets on the +version that built them until the host actor restarts or the facet is deleted, +and keeps established WebSockets draining while their backend stays healthy. +`restart` closes the worker's open WebSockets with code `1012` at promotion and +retires stale facets on their next dispatch, preserving SQLite state. Wrangler's +object-shaped declarative `exports` config is unsupported. The dry-run child +hides Wrangler's banner (and its normal update check) and disables anonymous +telemetry and automatic agent skills installation, updates, and prompts. +Bundling keeps stdin closed even with `--verbose`, which still forwards +stdout/stderr. Wrangler may still write local metrics state and debug logs, and +consult the configured npm registry when reporting an unknown configuration +field; project build hooks retain their normal network access. For +`[[services]]` and `[[exports]]`, read `docs/deploy.md`: tenant JSRPC may +delegate service or Durable Object class stubs as opaque capabilities, but the +receiver cannot rewrite their host-authored caller properties. Keep delegated +stubs in memory; long-term irrevocable stub storage is unsupported. + +If a command reports `Missing namespace`, supply the intended tenant with +`--ns ` or `WDL_NS` before retrying. `CONTROL_URL` may include a path prefix, but embedded usernames/passwords, query strings, and fragments are rejected. A bare `.local` host defaults to HTTPS diff --git a/CHANGELOG.md b/CHANGELOG.md index 02e3bd8..3b75da4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,29 @@ ## Unreleased +### Changed + +- Upgrade the bundled Wrangler to 4.131.0 and reject unmapped `[[connect]]` TCP + listeners and `[[workflows]]` fields, including `schedules`, `limits`, + `default_retention`, and `concurrency`, instead of silently dropping them. +- Support `--limit` and `--cursor` for Workflow definition listing and display + continuation cursors even for empty definition pages. +- Disable Wrangler skills installation/update prompts during deploy. Bundling + keeps stdin closed even with `--verbose`, which still forwards stdout/stderr. + +### Fixed + +- Distinguish empty intermediate Workflow instance pages from an empty result + set when Control returns a continuation cursor. +- Explain how to restart Workflow definition listing after cursor metadata + contention, even when Control redacts the error message. +- Explain how to set `--ns` or `WDL_NS` when a required namespace is missing. + +### Security + +- Update `smol-toml` to 1.7.1 and Wrangler's transitive `sharp` to 0.35.4, + clearing their dependency advisories without overrides. + ## 1.8.1 ### Changed diff --git a/GUIDE-zh.md b/GUIDE-zh.md index d1cf74b..96a4fa3 100644 --- a/GUIDE-zh.md +++ b/GUIDE-zh.md @@ -87,6 +87,8 @@ ADMIN_TOKEN= CLI 只会从 `.env` 读取 WDL 平台变量:`ADMIN_TOKEN`、`CONTROL_URL`、`CONTROL_CONNECT_HOST`、`WDL_NS`。优先级是 `CLI flag > shell/CI env > [resolved-ns] section > base .env > wdl token store`,都没有提供时命令直接报错——没有内置默认值。namespace 解析顺序是 `--ns`,然后是 shell 或 base `.env` 里的 `WDL_NS`,再然后是 token store 的默认 namespace。section 名可以是 `[acme]` 这类 tenant namespace,也可以是 `[__name__]` 这种运维保留的不透明 section。Tenant Wrangler 配置默认仍使用普通 tenant namespace 语法,除非运维方明确给了这种 namespace token;否则不要把 `__name__` 形态写进 `[[services]].ns`、`allowed_callers` 或命令示例。如果没有解析出 namespace,section 会全部跳过;后续命令如果需要 namespace 或 token,会按正常校验报错。只有临时切换 namespace 时才需要显式传 `--ns`。不带 scheme 的生产 control host(例如 `api.wdl.dev`)默认补 `https://`;loopback 和保留的 `*.test` host 默认补 `http://`。既有的裸 `:8080` 例外对任何 host(包括 `.local`)仍默认使用 HTTP。除此之外,`.local` 是局域网 / mDNS 后缀而不是 loopback,因此裸 host 默认 HTTPS;所有 HTTP `.local` 目标都会显示明文 token 告警。需要强制使用其它协议时,请显式写 scheme。Control URL 可以包含 path prefix,但不能嵌入 username/password,也不能包含 query string 或 fragment,因为命令会在这个 base URL 后追加 endpoint path,并单独发送 admin token。 +如果命令报告 `Missing namespace`,请传入 `--ns ` 或设置 `WDL_NS` 后重试。 + `CONTROL_CONNECT_HOST` 是本地开发 / 调试用的覆盖开关:它改变请求实际连接的 TCP 目标,而 HTTP Host header 和 TLS SNI 仍跟随 `CONTROL_URL`(所以 HTTPS 下控制面证书仍会拒绝被重定向的连接;纯 http 没有这层保护)。只在本地开发用 —— 不要在 CI 或生产 shell 中持久设置,残留值可能把 admin token 路由到非预期目标。覆盖值写成 URL 时,scheme 只决定默认 TCP 端口(`http` 为 80,`https` 为 443);请求使用 HTTP 还是 HTTPS、Host 和 SNI 仍由 `CONTROL_URL` 决定。 推荐的做法是把这些凭证放进托管存储,而不是 shell export 或项目 `.env`:`wdl token set --ns --control-url ` 用隐藏输入读取 token、调 `/whoami` 校验后按 namespace 存入 `~/.config/wdl/credentials`(不进 shell 历史、也不落在项目文件里)。存储是优先级最低的层——命令行标志、shell env、项目 `.env` 仍然胜出——`wdl token list` / `wdl token rm` 管理它。第一个存入的 namespace 成为默认(一行 base `WDL_NS`,和项目 `.env` 一样),命令不带 `--ns` 也能跑;`wdl token use ` 切换默认。CLI 会拒绝 symlink / 非普通文件形式的 credentials 路径;在 POSIX 上,如果 store 文件不属于当前用户、store 目录可被 group/other 写或文件能被 group/other 访问,也会拒绝读取。详见 [token-zh.md](./docs/token-zh.md)。 @@ -150,7 +152,7 @@ APP_NAME = "hello" Control 会拒绝早于 `2026-04-01` 的显式日期、无效或未来日期,以及超出 bundled workerd 支持范围的日期。上游 experimental enable flags、`legacy_error_serialization` 和 `allow_irrevocable_stub_storage` 不受支持。这些校验由 control canonical 持有,CLI 不复制 workerd flag table。 -你可以继续使用 `wrangler dev` 做本地开发;部署到本平台时改用 `wdl deploy`。平台部署命令会调用 `wrangler deploy --dry-run`(Wrangler v4)打包项目,解析顺序是 `WDL_WRANGLER_BIN`、Worker 项目本地 wrangler、CLI 包本地 wrangler、最后是 `PATH`。TypeScript、模块解析、esbuild 打包等流程仍走 wrangler 的标准路径。WDL 会隐藏这个 dry-run 子进程的 Wrangler banner(因此跳过常规 banner 更新检查)并关闭匿名遥测。Wrangler 在报告未知配置字段时仍可能访问已配置的 npm registry;项目 build hook 仍保留正常的网络访问能力。 +你可以继续使用 `wrangler dev` 做本地开发;部署到本平台时改用 `wdl deploy`。平台部署命令会调用 `wrangler deploy --dry-run`(Wrangler v4)打包项目,解析顺序是 `WDL_WRANGLER_BIN`、Worker 项目本地 wrangler、CLI 包本地 wrangler、最后是 `PATH`。TypeScript、模块解析、esbuild 打包等流程仍走 wrangler 的标准路径。WDL 会隐藏这个 dry-run 子进程的 Wrangler banner(因此跳过常规 banner 更新检查),关闭匿名遥测及自动 agent skills 安装、更新和提示。即使使用 `--verbose`,打包时 stdin 也保持关闭,但仍透传 stdout/stderr。Wrangler 仍可能写入本地 metrics 状态和调试日志,并在报告未知配置字段时访问已配置的 npm registry;项目 build hook 仍保留正常的网络访问能力。 如果同时存在多个 Wrangler 配置文件,WDL 跟随 Wrangler 的优先级:`wrangler.json`,然后 `wrangler.jsonc`,最后 `wrangler.toml`。 @@ -268,6 +270,8 @@ Wrangler 能打包、但 WDL 不能运行的形状由 control plane 作为 canon WDL 会自行消费 `[[exports]]`、`[[platform_bindings]]`、`[[triggers.schedules]]`、`[[services]].ns` 和 `[wdl]`,并从传给 Wrangler bundler 的临时配置中移除这些 WDL 扩展。`[ai]` 是 Wrangler 标准配置,会保留在临时配置中供 Wrangler 校验;如果选中的 named environment 没有自己的 `ai`,CLI 会提示顶层 binding 不会继承。WDL 另行只接受其中的 `binding` 字段,并把该声明映射到 WDL manifest。其它字段保持既有的 Wrangler 透传行为。WDL 不支持 Wrangler 对象形态的 declarative `exports` 配置。 +`[[connect]]` TCP listener 没有对应的 WDL runtime 映射,顶层和所选 environment 内的声明都会在打包前被拒绝。 + Cron triggers 和 queue consumers 是运行时 dispatch 能力。除非管理方明确给了 reserved namespace,否则只应声明在 tenant namespace 里的可路由 Worker 上。通过 `[[platform_bindings]]` 选择的 Worker 是冷加载的平台能力,不是公开/runtime dispatch 目标,不能声明 cron triggers 或 queue consumers。 R2 custom metadata key 读取时会按 HTTP header 语义归一成小写。R2 object head 会暴露 HTTP metadata 和 custom metadata,所以鉴权上与读取 object body 同级,不开放给 observer 角色。R2 支持条件请求、range GET 和 `list({ include: [...] })` metadata hydration。 `list({ include })` 会在并发上限内额外发起 HEAD;只有列表结果确实需要 metadata 时再打开。 @@ -541,8 +545,8 @@ class_name = "OrderWorkflow" 用 `wdl workflows` 查看定义和管理实例: ```bash -wdl workflows list -wdl workflows instances api orders +wdl workflows list [--limit ] [--cursor ] +wdl workflows instances api orders [--limit ] [--cursor ] wdl workflows status api orders order-123 --include-steps wdl workflows pause api orders order-123 wdl workflows resume api orders order-123 @@ -554,8 +558,12 @@ wdl workflows terminate api orders order-123 --yes `wdl workflows list` 会把 active Worker version 不再导出的定义标为 `retired=yes`。既有实例仍可查看和 terminate,但 restart 会返回 `workflow_not_exported`;需要先部署一个重新导出该 workflow name 的 active version。 +两个 Workflow list 命令都接受 `--limit` 与 `--cursor`;即使返回空页或短页,也应继续使用返回的 cursor。Definition 只保证页内排序;namespace 并发变化可能让同一个 `worker`/`name` 组合跨页重复,收集完整视图时应按该组合去重或去掉 cursor 重新开始。`workflows list` 返回 `workflow_metadata_contention` 时,如果传了 `--cursor`,CLI 会提示去掉它重新开始;未带 cursor 的首次列表请求可直接重试。 + 这是 WDL Workflows 支持,不是完整 Cloudflare Workflows parity。 `script_name`、跨 worker workflow、跨 worker callback、service-binding callback 和 Cloudflare source-AST visualizer 不支持。same-worker DO progress callback 和 runtime-observed parallel/DAG `step.do` execution 可用。 +WDL 的 `[[workflows]]` 只支持 `name`、`binding` 和 `class_name`。`script_name` 及其它所有字段(包括 `schedules`、`limits`、`default_retention` 和 `concurrency`)都会在打包前被拒绝,所选 environment 内的声明也一样。单个 instance 的 retention 仍可通过 `create()` 配置。 + 重要运行限制和编程规则: - `createBatch()` 每次最多接受 100 个 entry。单个 workflow result 上限是 1 MiB;一次 runtime 到 Workflows backend 的 JSON 请求上限是 2 MiB。 diff --git a/GUIDE.md b/GUIDE.md index 24169ba..1c396e9 100644 --- a/GUIDE.md +++ b/GUIDE.md @@ -123,6 +123,9 @@ send the admin token separately. If no namespace resolves, section values are skipped and the command will fail normally if it needs a namespace or token. Pass `--ns` when you want to override the default for one command. +If a command reports `Missing namespace`, pass `--ns ` or set +`WDL_NS` before retrying. + `CONTROL_CONNECT_HOST` is a local-dev / debug override: it changes the TCP target the request connects to while the HTTP Host header and TLS SNI keep tracking `CONTROL_URL` (so over HTTPS the control plane's certificate still @@ -255,9 +258,12 @@ wrangler, then `PATH`. TypeScript, module resolution, esbuild bundling, and related build behavior still follow Wrangler. WDL hides Wrangler's banner (which skips the normal banner update check) and -disables anonymous telemetry for this dry-run subprocess. Wrangler may still -consult the configured npm registry when reporting an unknown configuration -field. Project build hooks retain their normal network access. +disables anonymous telemetry and automatic agent skills installation, updates, +and prompts for this dry-run subprocess. Bundling keeps stdin closed even with +`--verbose`, which still forwards stdout/stderr. Wrangler may still write local +metrics state and debug logs, and consult the configured npm registry when +reporting an unknown configuration field. Project build hooks retain their +normal network access. When several Wrangler config files exist, WDL follows Wrangler's priority: `wrangler.json`, then `wrangler.jsonc`, then `wrangler.toml`. Both JSON @@ -426,6 +432,9 @@ maps that declaration into the WDL manifest. Other fields retain their existing Wrangler passthrough behavior. Wrangler's object-shaped declarative `exports` configuration is not supported by WDL. +`[[connect]]` TCP listeners have no WDL runtime mapping and are rejected before +bundling, both at the top level and in the selected environment. + Cron triggers and queue consumers are dispatch features. Declare them only on routeable Workers in tenant namespaces unless your operator gives you an explicit reserved namespace. Workers selected through `[[platform_bindings]]` @@ -847,8 +856,8 @@ class_name = "OrderWorkflow" Use `wdl workflows` to inspect definitions and manage instances: ```bash -wdl workflows list -wdl workflows instances api orders +wdl workflows list [--limit ] [--cursor ] +wdl workflows instances api orders [--limit ] [--cursor ] wdl workflows status api orders order-123 --include-steps wdl workflows pause api orders order-123 wdl workflows resume api orders order-123 @@ -865,12 +874,26 @@ rejected locally outside that range. `--step-limit` may be used only with restart returns `workflow_not_exported` until an active version exports that workflow name again. +Both Workflow list commands accept `--limit` and `--cursor`. Follow the reported +cursor even after an empty or short page. Definition ordering is page-local; +concurrent namespace changes can repeat a `worker`/`name` pair across pages, so +deduplicate by that pair or restart without a cursor when collecting a complete +view. When `workflows list` returns `workflow_metadata_contention`, the CLI +advises restarting without `--cursor` when one was supplied; retry an initial +listing without a cursor. + This is WDL Workflows support, not full Cloudflare Workflows parity. `script_name`, cross-worker workflows, cross-worker callbacks, service-binding callbacks, and Cloudflare source-AST visualizer are not supported. Same-worker DO progress callbacks and runtime-observed parallel/DAG `step.do` execution are available. +WDL supports only `name`, `binding`, and `class_name` in `[[workflows]]`. +`script_name` and all other fields, including `schedules`, `limits`, +`default_retention`, and `concurrency`, are rejected before bundling, including +in the selected environment. Per-instance retention remains configurable through +`create()`. + Important runtime limits and programming rules: - `createBatch()` accepts at most 100 entries per call. A single workflow result diff --git a/commands/ai.js b/commands/ai.js index 151ecb2..3fc2bf1 100644 --- a/commands/ai.js +++ b/commands/ai.js @@ -15,6 +15,7 @@ import { formatHelp, isMain, isPathInside, + missingNamespaceError, optionHelp, readJsonOrFailWithHint, redactedArgumentError, @@ -85,7 +86,8 @@ async function runAi({ values, positionals, context }) { } const ns = context.resolveNamespace(); - if (!group || !ns) throw new CliError(usageText()); + if (!group) throw new CliError(usageText()); + if (!ns) throw missingNamespaceError(); if (group === "models") { if (action) throw redactedArgumentError("ai models"); diff --git a/commands/d1.js b/commands/d1.js index d92a155..c608449 100644 --- a/commands/d1.js +++ b/commands/d1.js @@ -22,6 +22,7 @@ import { formatHelp, isMain, isPathInside, + missingNamespaceError, optionHelp, unexpectedArgument, } from "../lib/common.js"; @@ -74,7 +75,8 @@ async function runD1({ values, positionals, context }) { const [subcommand, firstArg] = positionals; const ns = context.resolveNamespace(); - if (!subcommand || !ns) throw new CliError(usageText()); + if (!subcommand) throw new CliError(usageText()); + if (!ns) throw missingNamespaceError(); if (subcommand === "migrations") { const action = firstArg; diff --git a/commands/delete.js b/commands/delete.js index cb9e505..42e4a2f 100644 --- a/commands/delete.js +++ b/commands/delete.js @@ -5,6 +5,7 @@ import { defineHiddenCliOption, formatHelp, isMain, + missingNamespaceError, optionHelp, unexpectedArgument, } from "../lib/common.js"; @@ -41,9 +42,10 @@ async function runDelete({ values, positionals, context }) { const [subcommand, firstArg, secondArg] = positionals; const ns = context.resolveNamespace(); - if (!subcommand || !ns) { + if (!subcommand) { throw new CliError(usageText()); } + if (!ns) throw missingNamespaceError(); if (subcommand !== "version" && subcommand !== "worker") { throw new CliError(`unknown subcommand: ${escapeTerminalText(subcommand)}\n${usageText()}`); diff --git a/commands/deploy.js b/commands/deploy.js index 9af5f83..1b35e49 100644 --- a/commands/deploy.js +++ b/commands/deploy.js @@ -11,6 +11,7 @@ import { formatHttpError, formatHttpErrorBody, isMain, + missingNamespaceError, optionHelp, readJsonOrFail, unexpectedArgument, @@ -385,9 +386,10 @@ async function runDeploy({ values, positionals, context: baseContext }) { const ns = context.resolveNamespace(); const [projectDir] = positionals; const extraArg = positionals[1]; - if (!projectDir || !ns) { + if (!projectDir) { throw new CliError(usageText()); } + if (!ns) throw missingNamespaceError(); if (extraArg) throw unexpectedArgument("deploy", extraArg); const { controlUrl, headers: authHeaders } = context.resolveControl(); diff --git a/commands/r2.js b/commands/r2.js index 2a53d46..d382005 100644 --- a/commands/r2.js +++ b/commands/r2.js @@ -9,6 +9,7 @@ import { defineCliOption, formatHelp, isMain, + missingNamespaceError, normalizePageLimit, optionHelp, unexpectedArgument, @@ -62,7 +63,8 @@ async function runR2({ values, positionals, context: baseContext }) { const [group, action, bucket, key] = positionals; const extraArg = positionals[4]; const ns = context.resolveNamespace(); - if (!group || !action || !ns) throw new CliError(usageText()); + if (!group || !action) throw new CliError(usageText()); + if (!ns) throw missingNamespaceError(); // Object keys can contain "/" and must reject . / .. segments, so they use // encodeR2KeyPath rather than nsUrl's per-segment encodePath. diff --git a/commands/secret.js b/commands/secret.js index c9f8aa5..e42889c 100644 --- a/commands/secret.js +++ b/commands/secret.js @@ -9,6 +9,7 @@ import { formatHelp, isMain, isNonEmptyString, + missingNamespaceError, optionHelp, readJsonOrFailWithHint, redactedArgumentError, @@ -58,9 +59,10 @@ async function runSecret({ values, positionals, context }) { const [subcommand, keyArg] = positionals; const extraArg = positionals[2]; const ns = context.resolveNamespace(); - if (!subcommand || !ns) { + if (!subcommand) { throw new CliError(usageText()); } + if (!ns) throw missingNamespaceError(); // Narrow values.worker to a string once; reuse `worker`/`hasWorker` below so // the worker-presence predicate lives in exactly one place. diff --git a/commands/tail.js b/commands/tail.js index f4efe82..f12da1b 100644 --- a/commands/tail.js +++ b/commands/tail.js @@ -7,7 +7,15 @@ import http from "node:http"; import https from "node:https"; import { defineCommand } from "../lib/command.js"; import { controlRequestError, controlRequestOptions, validateControlHeaders } from "../lib/control-fetch.js"; -import { CliError, defineCliOption, formatHelp, isMain, isNonEmptyString, optionHelp } from "../lib/common.js"; +import { + CliError, + defineCliOption, + formatHelp, + isMain, + isNonEmptyString, + missingNamespaceError, + optionHelp, +} from "../lib/common.js"; import { escapeTerminalLines, escapeTerminalText, formatDiagnosticValue } from "../lib/output.js"; const RECONNECT_INITIAL_MS = 1_000; @@ -157,7 +165,7 @@ async function runTail({ values, positionals, context: baseContext }) { } const ns = context.resolveNamespace(); - if (!ns) throw new CliError(usageText()); + if (!ns) throw missingNamespaceError(); if (positionals.length === 0) { throw new CliError("Specify one or more worker names."); diff --git a/commands/workers.js b/commands/workers.js index acb4d3a..0ce8e12 100644 --- a/commands/workers.js +++ b/commands/workers.js @@ -1,5 +1,5 @@ import { defineCommand } from "../lib/command.js"; -import { CliError, formatHelp, isMain, optionHelp } from "../lib/common.js"; +import { CliError, formatHelp, isMain, missingNamespaceError, optionHelp } from "../lib/common.js"; import { writeResult } from "../lib/output.js"; import { formatWorkersList } from "../lib/workers-format.js"; @@ -23,7 +23,7 @@ export { formatWorkersList }; async function runWorkers({ values, positionals, context }) { const ns = context.resolveNamespace(); if (positionals.length > 0) throw new CliError(usageText()); - if (!ns) throw new CliError(usageText()); + if (!ns) throw missingNamespaceError(); await printWorkersList(context, values.json === true); } diff --git a/commands/workflows.js b/commands/workflows.js index a290eda..def3030 100644 --- a/commands/workflows.js +++ b/commands/workflows.js @@ -4,8 +4,10 @@ import { defineCliOption, formatHelp, isMain, + missingNamespaceError, normalizePageLimit, optionHelp, + readJsonOrFailWithHint, unexpectedArgument, } from "../lib/common.js"; import { confirmAction } from "../lib/stdin.js"; @@ -14,8 +16,18 @@ import { formatInstanceList, formatInstanceStatus, formatWorkflowList } from ".. const LIFECYCLE_ACTIONS = new Set(["pause", "resume", "restart", "terminate"]); const WORKFLOW_OPTIONS = [ - defineCliOption("limit", { type: "string" }, "--limit ", "Instance page size (default 100, max 1000)."), - defineCliOption("cursor", { type: "string" }, "--cursor ", "Opaque instances pagination cursor."), + defineCliOption( + "limit", + { type: "string" }, + "--limit ", + "Definition or instance page size (default 100, max 1000)." + ), + defineCliOption( + "cursor", + { type: "string" }, + "--cursor ", + "Opaque definition or instance pagination cursor." + ), defineCliOption( "include-steps", { type: "boolean" }, @@ -53,15 +65,27 @@ async function runWorkflows({ values, positionals, context }) { const [subcommand] = positionals; const ns = context.resolveNamespace(); - if (!subcommand || !ns) throw new CliError(usageText()); + if (!subcommand) throw new CliError(usageText()); + if (!ns) throw missingNamespaceError(); if (subcommand === "list") { requireNoExtraPositionals(positionals, 1, "workflows list"); + const limit = normalizePageLimit(values.limit, "workflows --limit"); const { headers } = context.resolveControl(); - const body = /** @type {{ workflows?: import("../lib/workflows-format.js").WorkflowSummary[] }} */ ( - await context.fetchJson(context.nsUrl("workflows"), { headers }, "list workflows") - ); - writeResult(Boolean(values.json), body, () => formatWorkflowList(body), stdout); + const url = new URL(context.nsUrl("workflows")); + if (limit) url.searchParams.set("limit", limit); + if (values.cursor) url.searchParams.set("cursor", values.cursor); + const res = await context.controlFetch(url.href, { headers, env: context.env }); + const body = + /** @type {{ workflows?: import("../lib/workflows-format.js").WorkflowSummary[], cursor?: string | null }} */ ( + await readJsonOrFailWithHint(res, "list workflows", (error) => { + if (error !== "workflow_metadata_contention") return ""; + return values.cursor + ? "; workflow metadata changed, restart the listing without --cursor." + : "; workflow metadata changed, retry the command."; + }) + ); + writeResult(Boolean(values.json), body, () => formatWorkflowList(body, Boolean(values.cursor)), stdout); return; } @@ -73,10 +97,10 @@ async function runWorkflows({ values, positionals, context }) { if (limit) url.searchParams.set("limit", limit); if (values.cursor) url.searchParams.set("cursor", values.cursor); const body = - /** @type {{ instances?: import("../lib/workflows-format.js").WorkflowInstance[], cursor?: string }} */ ( + /** @type {{ instances?: import("../lib/workflows-format.js").WorkflowInstance[], cursor?: string | null }} */ ( await context.fetchJson(url.href, { headers }, "list workflow instances") ); - writeResult(Boolean(values.json), body, () => formatInstanceList(body), stdout); + writeResult(Boolean(values.json), body, () => formatInstanceList(body, Boolean(values.cursor)), stdout); return; } @@ -166,7 +190,7 @@ function requireNoExtraPositionals(positionals, expected, label) { function usageText() { return formatHelp({ usage: [ - "wdl workflows list [options]", + "wdl workflows list [--limit ] [--cursor ] [options]", "wdl workflows instances [--limit ] [--cursor ] [options]", "wdl workflows status [--include-steps] [--step-limit ] [options]", "wdl workflows pause [options]", diff --git a/docs/deploy-zh.md b/docs/deploy-zh.md index 6761f1c..270575b 100644 --- a/docs/deploy-zh.md +++ b/docs/deploy-zh.md @@ -6,7 +6,7 @@ wrangler 解析顺序是 `WDL_WRANGLER_BIN`、Worker 项目本地 wrangler、CLI 包本地 wrangler、最后是 `PATH`。默认不会临时 `npx --yes wrangler@^4` 拉包;只有设置 `WDL_ALLOW_NPX_WRANGLER=1` 时才允许这个 fallback。 -WDL 会隐藏这个 dry-run 子进程的 Wrangler banner(因此跳过常规 banner 更新检查)并关闭匿名遥测。Wrangler 在报告未知配置字段时仍可能访问已配置的 npm registry;项目 build hook 仍保留正常的网络访问能力。 +WDL 会隐藏这个 dry-run 子进程的 Wrangler banner(因此跳过常规 banner 更新检查),关闭匿名遥测及自动 agent skills 安装、更新和提示。即使使用 `--verbose`,打包时 stdin 也保持关闭,但仍透传 stdout/stderr。Wrangler 仍可能写入本地 metrics 状态和调试日志,并在报告未知配置字段时访问已配置的 npm registry;项目 build hook 仍保留正常的网络访问能力。 ## CLI 调用形式 @@ -69,7 +69,7 @@ Cloudflare 用 `workers_dev` 控制 Worker 的 `*.workers.dev` route;版本化 | 删除 worker(预览) | `wdl delete worker --dry-run` | | 查看 Workflow 实例 | `wdl workflows instances ` | -只要 `WDL_NS` 通过 env 或 `.env` 设置了,或者 `wdl token` store 有默认 namespace,`--ns` 就是可选的。每个子命令都实现了 `--help` —— 不知道用什么 flag 时直接跑。 +只要 `WDL_NS` 通过 env 或 `.env` 设置了,或者 `wdl token` store 有默认 namespace,`--ns` 就是可选的。如果命令报告 `Missing namespace`,请传入 `--ns ` 或设置 `WDL_NS` 后重试。每个子命令都实现了 `--help` —— 不知道用什么 flag 时直接跑。 ## 标准部署流程 @@ -127,7 +127,7 @@ wdl deploy . --env production WDL 会自行消费 `[[exports]]`、`[[platform_bindings]]`、`[[triggers.schedules]]`、`[[services]].ns` 和 `[wdl]`,并从传给 Wrangler bundler 的临时配置中移除这些 WDL 扩展。`[ai]` 是 Wrangler 标准配置,会保留在临时配置中供 Wrangler 校验;如果选中的 named environment 没有自己的 `ai`,CLI 会提示顶层 binding 不会继承。WDL 另行只接受其中的 `binding` 字段,并把该声明映射到 WDL manifest。其它字段保持既有的 Wrangler 透传行为。WDL 不支持 Wrangler 对象形态的 declarative `exports` 配置。`[wdl] session_policy` 见上面的会话策略一节。 -WDL 还会拒绝 Cloudflare Artifacts `triggers.events` subscription 和 R2 `local_dev.experimental_s3_credentials`;这两个字段都没有对应的 WDL deploy manifest 或 runtime 映射。 +WDL 还会拒绝 `[[connect]]` TCP listener、Cloudflare Artifacts `triggers.events` subscription 和 R2 `local_dev.experimental_s3_credentials`;它们都没有对应的 WDL deploy manifest 或 runtime 映射。 ### Service bindings 与 capability delegation @@ -135,6 +135,8 @@ Tenant JSRPC 可以序列化 `Blob` value,并把 service 或 Durable Object cl **不支持(部署失败):** Analytics Engine。Durable Objects 仅支持同 worker class;`script_name`、rename/delete migration 暂未实现。WDL Workflows 仅支持当前 Worker 内定义的 workflow class,不是完整 Cloudflare Workflows parity;`script_name`、跨 worker workflow、跨 worker callback、service-binding callback 和 Cloudflare source-AST visualizer 暂不支持。`route` / `routes` 仅在运维方启用时支持。Python Workers modules、不支持的 workerd compatibility flags 和 WDL 保留注入模块名会在部署时被拒绝:CLI 会对本地 `.py` module fail-fast,workerd compatibility 与 bundle-shape policy 由 control plane canonical 判断。WDL 会忽略、且无法映射进 manifest 的顶层或所选 env Wrangler runtime/deploy 配置字段和 section 也会由 CLI 直接拒绝,包括 legacy `[site]` Workers Sites、`pages_build_output_dir`、`observability`、`limits`、`placement`,以及错误信息点名的其它 unsupported binding/config field 或 section。`assets.run_worker_first` 会被静默忽略。 +WDL 的 `[[workflows]]` 只支持 `name`、`binding` 和 `class_name`。CLI 会在打包前拒绝 `script_name` 及其它所有字段,包括 `schedules`、`limits`、`default_retention` 和 `concurrency`。保留时间请通过单个 instance 的 `create()` retention 设置,不要使用 Wrangler 的 `default_retention`。 + Cron triggers 和 queue consumers 是 runtime dispatch 能力,只应声明在可路由的 tenant Worker 上。通过 `[[platform_bindings]]` 选择的 Worker 是冷加载的平台能力,不是 public/runtime dispatch 目标,不能声明 cron triggers 或 queue consumers。 ## 破坏性命令 diff --git a/docs/deploy.md b/docs/deploy.md index 4250ec3..ba751ca 100644 --- a/docs/deploy.md +++ b/docs/deploy.md @@ -13,9 +13,12 @@ transient `npx --yes wrangler@^4` fetch; that fallback is allowed only when `WDL_ALLOW_NPX_WRANGLER=1` is set. WDL hides Wrangler's banner (which skips the normal banner update check) and -disables anonymous telemetry for this dry-run subprocess. Wrangler may still -consult the configured npm registry when reporting an unknown configuration -field. Project build hooks retain their normal network access. +disables anonymous telemetry and automatic agent skills installation, updates, +and prompts for this dry-run subprocess. Bundling keeps stdin closed even with +`--verbose`, which still forwards stdout/stderr. Wrangler may still write local +metrics state and debug logs, and consult the configured npm registry when +reporting an unknown configuration field. Project build hooks retain their +normal network access. ## CLI invocation forms @@ -131,8 +134,10 @@ changes only the control socket target and never a printed Worker origin. | Inspect Workflow instances | `wdl workflows instances ` | `--ns` is optional whenever `WDL_NS` is set via env or `.env`, or the -`wdl token` store has a default namespace. Every subcommand implements `--help` -— run it when you don't know which flag to use. +`wdl token` store has a default namespace. If a command reports +`Missing namespace`, pass `--ns ` or set `WDL_NS` before retrying. +Every subcommand implements `--help` — run it when you don't know which flag to +use. ## Standard deploy flow @@ -255,9 +260,9 @@ Wrangler passthrough behavior. Wrangler's object-shaped declarative `exports` configuration is not supported by WDL. `[wdl] session_policy` has its own section above. -WDL also rejects Cloudflare Artifacts `triggers.events` subscriptions and R2 -`local_dev.experimental_s3_credentials`: neither field has a WDL deploy-manifest -or runtime mapping. +WDL also rejects `[[connect]]` TCP listeners, Cloudflare Artifacts +`triggers.events` subscriptions, and R2 `local_dev.experimental_s3_credentials`: +none has a WDL deploy-manifest or runtime mapping. ### Service bindings and delegated capabilities @@ -282,6 +287,11 @@ legacy `[site]` Workers Sites, `pages_build_output_dir`, `observability`, `limits`, `placement`, and other unsupported binding/config fields or sections named in the error. `assets.run_worker_first` is silently ignored. +WDL supports only `name`, `binding`, and `class_name` in `[[workflows]]`. The +CLI rejects `script_name` and all other fields, including `schedules`, `limits`, +`default_retention`, and `concurrency`, before bundling. Use per-instance +`create()` retention instead of Wrangler's `default_retention`. + Cron triggers and queue consumers are runtime dispatch features; declare them only on routeable tenant Workers. Workers selected through `[[platform_bindings]]` are cold-loaded platform capabilities, not diff --git a/docs/workflows-zh.md b/docs/workflows-zh.md index 49cc7f8..38133ef 100644 --- a/docs/workflows-zh.md +++ b/docs/workflows-zh.md @@ -15,6 +15,8 @@ binding = "ORDERS" class_name = "OrderWorkflow" ``` +WDL 的 `[[workflows]]` 只支持 `name`、`binding` 和 `class_name`。`script_name` 及其它所有字段(包括 `schedules`、`limits`、`default_retention` 和 `concurrency`)都会在打包前被拒绝,所选 environment 内的声明也一样。单个 instance 的 retention 可通过下文的 `create()` 设置。 + Worker 代码遵循 Cloudflare Workflows 心智模型:`WorkflowEntrypoint`、 `env..create()`、`createBatch()`、`get()`、`status()`、`pause()`、 `resume()`、`restart()`、`terminate()`、`sendEvent()`、`step.do()`、 `step.sleep()`、`step.sleepUntil()`、`step.waitForEvent()`、retry、 `NonRetryableError`、same-worker DO progress callback 和 runtime-observed parallel/DAG step。 如果一个 `step.do` 永久失败,本次 workflow run 会进入 terminal failure,即使用户代码 catch 了这次抛错也一样。 @@ -33,7 +35,7 @@ Worker 代码遵循 Cloudflare Workflows 心智模型:`WorkflowEntrypoint`、 ## CLI ```bash -wdl workflows list +wdl workflows list [--limit ] [--cursor ] wdl workflows instances [--limit ] [--cursor ] wdl workflows status --include-steps [--step-limit ] wdl workflows pause @@ -48,7 +50,9 @@ wdl workflows terminate --yes `wdl workflows list` 会把 active Worker version 不再导出的定义标为 `retired=yes`。既有实例仍可查看和 terminate,但 restart 会返回 `workflow_not_exported`;需要先部署一个重新导出该 workflow name 的 active version。 -Workflows API 的语义大小限制会返回 `request_too_large`;HTTP body 解析阶段的大小限制可能返回 `request_body_too_large`。Workflows 5xx 表示平台或 backend 故障,响应体会保持通用错误摘要;底层诊断进入平台日志,不作为稳定 CLI 输出。`workflow_metadata_contention` 表示 control 读取期间 active workflow metadata 发生变化,重试命令即可。 +`workflows list` 和 `workflows instances` 都接受 `--limit` 与 `--cursor`。即使返回空页或短页,也要沿 `Next cursor` 继续,直到不再返回 cursor。Definition 只保证页内排序,不保证扫描过程中的跨页全局排序;namespace 并发变化可能让同一个 `worker`/`name` 组合在多个 definition page 中重复,收集完整视图时应按该组合去重或去掉 cursor 重新开始。Metadata 变化使 definition cursor 失效时,也应去掉 cursor 重新开始。 + +Workflows API 的语义大小限制会返回 `request_too_large`;HTTP body 解析阶段的大小限制可能返回 `request_body_too_large`。Workflows 5xx 表示平台或 backend 故障,响应体会保持通用错误摘要;底层诊断进入平台日志,不作为稳定 CLI 输出。`workflow_metadata_contention` 表示 control 读取期间 active workflow metadata 发生变化。对于 `workflows list --cursor`,请去掉 `--cursor` 重新开始;即使 Control 返回 `Internal error`,CLI 也会补充这一提示。首次列表请求或其它 Workflow 命令可直接重试。 ## 端到端示例 diff --git a/docs/workflows.md b/docs/workflows.md index 097b197..2f3f543 100644 --- a/docs/workflows.md +++ b/docs/workflows.md @@ -21,6 +21,12 @@ binding = "ORDERS" class_name = "OrderWorkflow" ``` +WDL supports only `name`, `binding`, and `class_name` in `[[workflows]]`. +`script_name` and all other fields, including `schedules`, `limits`, +`default_retention`, and `concurrency`, are rejected before bundling, including +in the selected environment. Set per-instance retention through `create()` as +described below. + Worker code follows the Cloudflare Workflows mental model: `WorkflowEntrypoint`, `env..create()`, `createBatch()`, `get()`, `status()`, `pause()`, `resume()`, `restart()`, `terminate()`, `sendEvent()`, `step.do()`, @@ -61,7 +67,7 @@ if user code catches the thrown error. ## CLI ```bash -wdl workflows list +wdl workflows list [--limit ] [--cursor ] wdl workflows instances [--limit ] [--cursor ] wdl workflows status --include-steps [--step-limit ] wdl workflows pause @@ -83,12 +89,23 @@ instance id. restart returns `workflow_not_exported` until an active version exports that workflow name again. +Both `workflows list` and `workflows instances` accept `--limit` and `--cursor`. +Follow `Next cursor` until none is returned, including after an empty or short +page. Definition pages are sorted within each page, not globally across the +scan. Concurrent namespace changes can repeat a `worker`/`name` pair across +definition pages; deduplicate by that pair or restart without a cursor when +collecting a complete view. If metadata changes invalidate a definition cursor, +restart without it. + Semantic size limits in the Workflows API return `request_too_large`; size limits hit during HTTP body parsing may return `request_body_too_large`. A Workflows 5xx means a platform or backend failure, and the response body stays a generic error summary; underlying diagnostics go to platform logs and are not stable CLI output. `workflow_metadata_contention` means the active workflow -metadata changed while control was reading it; retry the command. +metadata changed while control was reading it. For `workflows list --cursor`, +remove `--cursor` and restart the listing; the CLI adds that guidance even when +Control returns `Internal error`. For an initial listing or another Workflow +command, retry the command. ## End-to-end example diff --git a/lib/command.js b/lib/command.js index 58d2f7c..127b4cb 100644 --- a/lib/command.js +++ b/lib/command.js @@ -269,8 +269,8 @@ function buildContext(deps, commandDefaults, values, positionals) { // Build a control URL under the resolved namespace, encoding each segment, e.g. // nsUrl("worker", name, "versions", v) -> .../ns//worker//versions/. - // Fail-fast on an unresolved namespace: callers validate --ns and throw their - // own usageText first, so this only fires if a command forgets that check — + // Fail-fast on an unresolved namespace: callers reject a missing namespace + // first, so this only fires if a command forgets that check — // better an internal-invariant error than a silent .../ns/undefined/... fetch. context.nsUrl = (/** @type {string[]} */ ...segments) => { const { controlUrl } = resolveControlFn(); diff --git a/lib/common.js b/lib/common.js index 3ee52cd..852a367 100644 --- a/lib/common.js +++ b/lib/common.js @@ -15,6 +15,11 @@ export class CliError extends Error { } } +/** @returns {CliError} */ +export function missingNamespaceError() { + return new CliError("Missing namespace. Pass --ns or set WDL_NS."); +} + /** * @param {string} label * @param {unknown} arg diff --git a/lib/workflows-format.js b/lib/workflows-format.js index fd49fff..9d5482a 100644 --- a/lib/workflows-format.js +++ b/lib/workflows-format.js @@ -30,27 +30,33 @@ function field(value) { } /** - * @param {{ workflows?: WorkflowSummary[] } | null | undefined} body + * @param {{ workflows?: WorkflowSummary[], cursor?: string | null } | null | undefined} body + * @param {boolean} [continued] * @returns {string[]} */ -export function formatWorkflowList(body) { +export function formatWorkflowList(body, continued = false) { const workflows = Array.isArray(body?.workflows) ? body.workflows : []; - if (workflows.length === 0) return ["(no workflows)"]; - return workflows.map( - (entry) => - `${field(entry.worker)}/${field(entry.name)}\tbinding=${field(entry.binding)}\tclass=${field(entry.className)}\tactive=${field(entry.activeVersion)}\tkey=${field(entry.workflowKey)}\tretired=${entry.retired ? "yes" : "no"}` - ); + const lines = + workflows.length === 0 + ? [body?.cursor || continued ? "(no workflows on this page)" : "(no workflows)"] + : workflows.map( + (entry) => + `${field(entry.worker)}/${field(entry.name)}\tbinding=${field(entry.binding)}\tclass=${field(entry.className)}\tactive=${field(entry.activeVersion)}\tkey=${field(entry.workflowKey)}\tretired=${entry.retired ? "yes" : "no"}` + ); + if (body?.cursor) lines.push(`Next cursor: ${escapeTerminalText(body.cursor)}`); + return lines; } /** - * @param {{ instances?: WorkflowInstance[], cursor?: string } | null | undefined} body + * @param {{ instances?: WorkflowInstance[], cursor?: string | null } | null | undefined} body + * @param {boolean} [continued] * @returns {string[]} */ -export function formatInstanceList(body) { +export function formatInstanceList(body, continued = false) { const instances = Array.isArray(body?.instances) ? body.instances : []; const lines = instances.length === 0 - ? ["(no workflow instances)"] + ? [body?.cursor || continued ? "(no workflow instances on this page)" : "(no workflow instances)"] : instances.map((entry) => `${field(entry.id)}\tstatus=${field(entry.status)}`); if (body?.cursor) lines.push(`Next cursor: ${escapeTerminalText(body.cursor)}`); return lines; diff --git a/lib/wrangler-pack.js b/lib/wrangler-pack.js index f807318..9211da5 100644 --- a/lib/wrangler-pack.js +++ b/lib/wrangler-pack.js @@ -289,7 +289,9 @@ export async function packWranglerProject({ /** @type {import("node:child_process").ExecFileSyncOptions} */ const wranglerOpts = { cwd: absProject, - stdio: verbose ? "inherit" : ["ignore", "pipe", "pipe"], + // Older Wrangler releases offer skills installation on interactive stdin. + // Verbose mode forwards output, but bundling stays non-interactive. + stdio: ["ignore", verbose ? "inherit" : "pipe", verbose ? "inherit" : "pipe"], // Wrangler's arg parser requires CLOUDFLARE_API_TOKEN to be set even // on --dry-run. A dummy value satisfies the local parser without // passing the user's real token to the child process. diff --git a/lib/wrangler/bindings.js b/lib/wrangler/bindings.js index b3ad972..7a6245e 100644 --- a/lib/wrangler/bindings.js +++ b/lib/wrangler/bindings.js @@ -590,6 +590,7 @@ export function parseWorkflowsFromCfg(cfg, configRel = "wrangler config") { if (!Array.isArray(cfg.workflows)) { throw new Error(`${configRel}: [[workflows]] must be an array of tables`); } + const allowedKeys = new Set(["name", "binding", "class_name"]); /** @type {Array<{ name: string, binding: string, className: string }>} */ const out = []; /** @type {Set} */ @@ -611,6 +612,10 @@ export function parseWorkflowsFromCfg(cfg, configRel = "wrangler config") { `${configRel}: [[workflows]] ${formatConfigLabel(entry.name)}: script_name is not supported by WDL Workflows` ); } + const unknownKeys = Object.keys(entry).filter((key) => !allowedKeys.has(key)); + if (unknownKeys.length > 0) { + throw new Error(`${configRel}: [[workflows]] contains unknown field(s): ${formatConfigKeyList(unknownKeys)}`); + } if (RESERVED_OBJECT_KEYS.has(entry.name)) { throw new Error( `${configRel}: [[workflows]] ${formatConfigLabel(entry.name)}: name is a reserved Object.prototype key` diff --git a/lib/wrangler/command.js b/lib/wrangler/command.js index df46a0c..aee2d20 100644 --- a/lib/wrangler/command.js +++ b/lib/wrangler/command.js @@ -151,6 +151,7 @@ export function wranglerChildEnv(env) { // the banner (and its normal update check) and disable anonymous telemetry. WRANGLER_HIDE_BANNER: "true", WRANGLER_SEND_METRICS: "false", + WRANGLER_NO_SKILLS_UPDATE_PROMPTS: "true", }; for (const key of WRANGLER_SCRUB_KEYS) { delete childEnv[key]; diff --git a/lib/wrangler/config.js b/lib/wrangler/config.js index ed421f3..48df6c2 100644 --- a/lib/wrangler/config.js +++ b/lib/wrangler/config.js @@ -37,6 +37,7 @@ const UNSUPPORTED_WRANGLER_KEYS = [ "cache", "cloudchamber", "compliance_region", + "connect", "containers", "data_blobs", "dependencies_instrumentation", diff --git a/package-lock.json b/package-lock.json index fb12a2e..63f84e3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,8 +10,8 @@ "license": "Apache-2.0", "dependencies": { "jsonc-parser": "3.3.1", - "smol-toml": "^1.7.0", - "wrangler": "4.120.0" + "smol-toml": "^1.7.1", + "wrangler": "4.131.0" }, "bin": { "wdl": "bin/wdl.js" @@ -53,9 +53,9 @@ } }, "node_modules/@cloudflare/workerd-darwin-64": { - "version": "1.20260801.1", - "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-64/-/workerd-darwin-64-1.20260801.1.tgz", - "integrity": "sha512-wuJWbXpKvncJi1P0GKS+iYpN5tHdb7JPJJ/+6ZQe8zzovHHVMkLJPNBsgWpqeUhpM3g9qTwEKd2rglNKejuh5A==", + "version": "1.20260910.1", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-64/-/workerd-darwin-64-1.20260910.1.tgz", + "integrity": "sha512-9K72iwX+RdKZLgvEqCgOI0tBgo10lN2lWjrtM8SRFjl7LOzk7NmrepBvAFiUz8H5PhxafQ17Dku2RLmzA/Hnew==", "cpu": [ "x64" ], @@ -69,9 +69,9 @@ } }, "node_modules/@cloudflare/workerd-darwin-arm64": { - "version": "1.20260801.1", - "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-arm64/-/workerd-darwin-arm64-1.20260801.1.tgz", - "integrity": "sha512-kwoZiTpnhNrF3+APx84Q/oAqvJ3sU9yefGagwm/ASaH/2W19x0vghkW/r4qCoHCK0WW7EPugZ+aXjgPMRtlq1Q==", + "version": "1.20260910.1", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-arm64/-/workerd-darwin-arm64-1.20260910.1.tgz", + "integrity": "sha512-OYjzHbTXXxzRDtjnlcy4o6unJ3WiKM9FTtndvy4sitP9ZN+JC2/EDT/lyyJaFw4yZ9uSF9pdeMDp3Kgxixc2NA==", "cpu": [ "arm64" ], @@ -85,9 +85,9 @@ } }, "node_modules/@cloudflare/workerd-linux-64": { - "version": "1.20260801.1", - "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-64/-/workerd-linux-64-1.20260801.1.tgz", - "integrity": "sha512-r0vAxCZH+Jih9Unm1yoyiByPNWNgawcKciOHDm5Q37ZVGOkKLsT9AtLe3yLSaul76WrKqtf+JP2n0WW32VBLJg==", + "version": "1.20260910.1", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-64/-/workerd-linux-64-1.20260910.1.tgz", + "integrity": "sha512-tNlHMrvoAyggdukuPSpN3huxOAwI9xzg2SOHC8sI785/HaAQbA220Ua9MOHWuc6MXj6QNSc3B0YA3DWtFtw37Q==", "cpu": [ "x64" ], @@ -101,9 +101,9 @@ } }, "node_modules/@cloudflare/workerd-linux-arm64": { - "version": "1.20260801.1", - "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-arm64/-/workerd-linux-arm64-1.20260801.1.tgz", - "integrity": "sha512-zWgpdZtSozvIgzQNmQiDSF8yEOQJUkRAWNsDXXzAAoy+fCn8YUoSibj3mpFSbZRvbUldeBEaW6SCdC2VEMkhNQ==", + "version": "1.20260910.1", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-arm64/-/workerd-linux-arm64-1.20260910.1.tgz", + "integrity": "sha512-ScBo7DydT1o/sowCj2HBbN2TU3rQ11H2YEuFG3SC7M5b97PMmakhcIBzFpbKeHbyVf+aEsTlLCJsZtWDuyWAyQ==", "cpu": [ "arm64" ], @@ -117,9 +117,9 @@ } }, "node_modules/@cloudflare/workerd-windows-64": { - "version": "1.20260801.1", - "resolved": "https://registry.npmjs.org/@cloudflare/workerd-windows-64/-/workerd-windows-64-1.20260801.1.tgz", - "integrity": "sha512-2oQz+Ksu4ji6e/+ZoYX+tWQEcxAii2p7l+iR8kx48W1llMalaufAsmVxTlk+3/vrM7D3/2c0iK448e0UQTcIMg==", + "version": "1.20260910.1", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-windows-64/-/workerd-windows-64-1.20260910.1.tgz", + "integrity": "sha512-+iiMuoE9+rbs3NL21gIQYmhMDXs/kfhoiSsehqZparbvZo0MVUZDzm7cSaUgCKM3YCtXNWPnoV6VlfkgBbotvw==", "cpu": [ "x64" ], @@ -774,9 +774,9 @@ } }, "node_modules/@img/sharp-darwin-arm64": { - "version": "0.35.2", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.35.2.tgz", - "integrity": "sha512-eEieHsMksAW4IiO5NzauESRl2D2qz3J/kwUxUrSfV06A93eEaRfMpHXyUb1mAqrR7i8U9A0GRqE9pjn6u1Jjpg==", + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.35.4.tgz", + "integrity": "sha512-Uhfl4V4lhP2nbUVF9+hyH1+luj86f1gUFeo8ALYxFoULoU+G87D43BfeMP8XHsk9boxAnCY/bf2EHwhA7MuGsA==", "cpu": [ "arm64" ], @@ -792,13 +792,13 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-darwin-arm64": "1.3.1" + "@img/sharp-libvips-darwin-arm64": "1.3.3" } }, "node_modules/@img/sharp-darwin-x64": { - "version": "0.35.2", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.35.2.tgz", - "integrity": "sha512-BaktuGPCeHJMARpodR8jK4uKiZrPAy9WrfQW0sdI37clracq8Bp01AYS3SZgi5FS/y5twa9t4+LIuuxQjqRrWw==", + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.35.4.tgz", + "integrity": "sha512-hWniXY3bG5qKpkKrAwPe4y+VTPmf086YQAnkxWh7uA1YrlRouWGa0M0Mxj3ZjnXFkv7/TD1bTy9lGUK26vRvWw==", "cpu": [ "x64" ], @@ -814,20 +814,20 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-darwin-x64": "1.3.1" + "@img/sharp-libvips-darwin-x64": "1.3.3" } }, "node_modules/@img/sharp-freebsd-wasm32": { - "version": "0.35.2", - "resolved": "https://registry.npmjs.org/@img/sharp-freebsd-wasm32/-/sharp-freebsd-wasm32-0.35.2.tgz", - "integrity": "sha512-YoAxdnd8hPUkvLHd3bWY+YA8nw3xM/RyRopYucNsWHVSan8NLVM3X2volsfoRDcXdUJPg6tXahSd7HXPK7lRnw==", + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-freebsd-wasm32/-/sharp-freebsd-wasm32-0.35.4.tgz", + "integrity": "sha512-lIsKw/BU+kjB4eZjxrYrZmwOJYi3Ajrv66iAlBmUPyKc3HpnloevB1g3wxGD9P/5BbQ1brBGl65VRRrCvQDEqA==", "license": "Apache-2.0", "optional": true, "os": [ "freebsd" ], "dependencies": { - "@img/sharp-wasm32": "0.35.2" + "@img/sharp-wasm32": "0.35.4" }, "engines": { "node": ">=20.9.0" @@ -837,9 +837,9 @@ } }, "node_modules/@img/sharp-libvips-darwin-arm64": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.3.1.tgz", - "integrity": "sha512-4V/M3roRMTYjiwZY9IOVQOE8OyeCxFAkYmyZDrZl51uOKjibm3oeEJ4WAmLxutAfzFbC9jqUiPs2gbnGflH+7g==", + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.3.3.tgz", + "integrity": "sha512-suTBPTDGrI9WodccaDdwZItTSaBYASlBk1NSfElSHrUfzu3szG6lvIF58+WiFvnfzuK8ZBFS5zE00PxqxnRiPg==", "cpu": [ "arm64" ], @@ -853,9 +853,9 @@ } }, "node_modules/@img/sharp-libvips-darwin-x64": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.3.1.tgz", - "integrity": "sha512-c0/DxItpJv2+dGhgycJBBgotdqruGYDvA79drdh0MD1dFpy7JzJ/PlXwi1H4rFf0eTy8tgbI91aHDnZIceY3jQ==", + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.3.3.tgz", + "integrity": "sha512-FVJZ5mITMobmXIz/hPDTw0EintTW5H3WfrxwLqEqjiIihlu+hVRyGrFQ60xl0Lxn7Bt3zdpevPaQi0HEzqz9fw==", "cpu": [ "x64" ], @@ -869,9 +869,9 @@ } }, "node_modules/@img/sharp-libvips-linux-arm": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.3.1.tgz", - "integrity": "sha512-aGGy9aWzXgHBG7HNyQPWorZthlp7+x6fDRoPAQbGO3ThcttuTyKIx3NuSHb6zb4gBNq6/yNn9f1cy9nFKS/Vmg==", + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.3.3.tgz", + "integrity": "sha512-3rbU4vqXXc3hY/OiXdl52xZvT0F1yEngWfvqudtPJg/KkyiaQw2DRsFrNzpmLvfavbwOq3qXn36GP8obHRULQA==", "cpu": [ "arm" ], @@ -888,9 +888,9 @@ } }, "node_modules/@img/sharp-libvips-linux-arm64": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.3.1.tgz", - "integrity": "sha512-JznefmcK9j1JKPz8AkQDh89kjojubyfOasWBPKfzMIhPwsgDy9evpE/naJTXXXmghS1iFwR8u/kTwh/I2/+GCw==", + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.3.3.tgz", + "integrity": "sha512-0DaL0A6Xu6sQSQFwe4iVCrKWU2cCTItnRsYsCdxAMm9NF6twAA9BKnoqy4hqz4+azQ0JHuA26qiUKsf1XJ/v5A==", "cpu": [ "arm64" ], @@ -907,9 +907,9 @@ } }, "node_modules/@img/sharp-libvips-linux-ppc64": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.3.1.tgz", - "integrity": "sha512-1EkwGNCZk6iWNCMWqrvdJ+r1j0PT1zIz60CNPhYnJlK/zyeWqlsPZIe+ocBVqPF8k/Ssee/NCk+tE9Ryrko6ng==", + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.3.3.tgz", + "integrity": "sha512-cdn1OvUBwsXhbC0zSzJnNzf5MZ/mTrobawDvNXBTxe8VtqKAm0sRuEY2Evzovb/w9JMk4TvRxqt1mekSuJz64w==", "cpu": [ "ppc64" ], @@ -926,9 +926,9 @@ } }, "node_modules/@img/sharp-libvips-linux-riscv64": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.3.1.tgz", - "integrity": "sha512-Ilays+w2bXdnxzxtQdmXR62u8o8GYa3eL4+Gr+1KiE4xperMZUslRaVPJwwPkzlHEjGfXAfRVAa/7CYCtSqsBw==", + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.3.3.tgz", + "integrity": "sha512-HjPVx7yKz+0lqdhDlTw1tt90wamBoxhiXpvl1XZpJLiHH4RCJ5yDTqH+VlYPv2fwFs89JFw4c1IexYOcQUi4IQ==", "cpu": [ "riscv64" ], @@ -945,9 +945,9 @@ } }, "node_modules/@img/sharp-libvips-linux-s390x": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.3.1.tgz", - "integrity": "sha512-VfBwVHQTbRoj4XlpA/KLZ7ltgMpz+4WSejFzQ+GnoImjo1PtEJ59QB2qR1xQEeRPYIkNrPIm2L4cICMvz4C2ew==", + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.3.3.tgz", + "integrity": "sha512-neWLh+3yCNThxnfy3c4BbVBeGgt9aftno+XbT56iK28RgeDs3UOFWviLWlUu0bArYVYJaFDK+RRohbicUNCm8Q==", "cpu": [ "s390x" ], @@ -964,9 +964,9 @@ } }, "node_modules/@img/sharp-libvips-linux-x64": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.3.1.tgz", - "integrity": "sha512-+c8ukgwU62DS54nCAjw7keOfHUkmr0B5QHEdcOqRnodF/MNXJbVI8Eopoj4B/0H8Asr65I+A4Amrn7a85/md6A==", + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.3.3.tgz", + "integrity": "sha512-4vKmvAst9nrowcqquKFAyZJUDolUaIp8uRiN0mWFguJ1IplC9/pitXtlnnlU4aa/eJw3J7i67V+pwUL+wZGdsA==", "cpu": [ "x64" ], @@ -983,9 +983,9 @@ } }, "node_modules/@img/sharp-libvips-linuxmusl-arm64": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.3.1.tgz", - "integrity": "sha512-qlKb/pwbkAi1WMsJrYHk7CuDrd12s27U2QnRhFYUoJNrRCmkosMTttuRFat/DDB3IlDm5qE1TJgZ4JDnHX8Ldw==", + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.3.3.tgz", + "integrity": "sha512-Y9kQaLMuNoB0bPYOOdcZMaseNrFpPodIWWMrx+CZyydf2xn68j9WYc6sWWRrDwNkzCQjKYfc68L7jKjGlHMibw==", "cpu": [ "arm64" ], @@ -1002,9 +1002,9 @@ } }, "node_modules/@img/sharp-libvips-linuxmusl-x64": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.3.1.tgz", - "integrity": "sha512-yO21HwoUVLN8Qa+/SBjQLMYwBWAVJjeGPNe+hc0OUeMeifEtJqu5a1c4HayE1nNpDih9y3/KkoltfkDodmKAlg==", + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.3.3.tgz", + "integrity": "sha512-fj8Mv0HHfD1Rr+4I68+3agJynxDWtBFgicTbSOb9Bke6pIwzGcJ+RX/yHjmiEGFMCavY/dxvem7MyNaJF+wDiw==", "cpu": [ "x64" ], @@ -1021,9 +1021,9 @@ } }, "node_modules/@img/sharp-linux-arm": { - "version": "0.35.2", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.35.2.tgz", - "integrity": "sha512-SE4kzF2mepn6z+6E7L6lsV8FzuLL6IPQdyX8ZiwROAG/G8td+hP/m7FsFPwidtrF19gvajuC9l6TxAVcsA4S7A==", + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.35.4.tgz", + "integrity": "sha512-7OAS8gI0EReKGVN2HssHlM6umJgxF5VI3xN0p9FA91p/YO+ou5hiNghLdZ5BEHztwaaK5+bLKRf8x/o2L2nk9A==", "cpu": [ "arm" ], @@ -1042,13 +1042,13 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-arm": "1.3.1" + "@img/sharp-libvips-linux-arm": "1.3.3" } }, "node_modules/@img/sharp-linux-arm64": { - "version": "0.35.2", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.35.2.tgz", - "integrity": "sha512-af12Pnd0ZGu2HfP8NayB0kk6eC/lrfbQE6HlR4jD+34wdJ1Vw9TF6TMn6ZvffT+WgqVsl0hRbmNvz2u/23VmwA==", + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.35.4.tgz", + "integrity": "sha512-De4jpEnAU8Hd5oT0j1G3uL4ZvTuipVMn7YC6vPaJhy6/7EwEae0SVAoBrUMYQbkLGDm85taVWwuPc1a44LTzCQ==", "cpu": [ "arm64" ], @@ -1067,13 +1067,13 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-arm64": "1.3.1" + "@img/sharp-libvips-linux-arm64": "1.3.3" } }, "node_modules/@img/sharp-linux-ppc64": { - "version": "0.35.2", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.35.2.tgz", - "integrity": "sha512-hYSBm7zcNtDCozCxQHYZJiu63b/bXsgRZuOxCIBZsStMM9Vap47iFHdbX4kCvQsblPB/k+clhELpdQJHQLSHvg==", + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.35.4.tgz", + "integrity": "sha512-2oYZJeIl4kCcMGk4ouZVjnkCtFrpQFlNEtJ6GbxzhHQchwH0NH/qEb9ykmOl29dqwMq+JhFdZn+1ak2FKhI9fQ==", "cpu": [ "ppc64" ], @@ -1092,13 +1092,13 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-ppc64": "1.3.1" + "@img/sharp-libvips-linux-ppc64": "1.3.3" } }, "node_modules/@img/sharp-linux-riscv64": { - "version": "0.35.2", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.35.2.tgz", - "integrity": "sha512-qQt0Kc13+Hoan/Awq/qMSQw3L+RI1NCRPgD5cUJ/1WSSmIoysLOc72jlRM3E0OHN9Yr313jgeQ2T+zW+F03QFA==", + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.35.4.tgz", + "integrity": "sha512-cPbNChoRURAWdebDIHSenxRpgEdy7JkPydSnUxRm9VvKD7m0/xVaR/8Fzlu81pk5nHEvHH87UZUA7cTtwnbJSA==", "cpu": [ "riscv64" ], @@ -1117,13 +1117,13 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-riscv64": "1.3.1" + "@img/sharp-libvips-linux-riscv64": "1.3.3" } }, "node_modules/@img/sharp-linux-s390x": { - "version": "0.35.2", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.35.2.tgz", - "integrity": "sha512-E4fLLfRPzDLlEeDaTzI98OFLcv++WL5ChLLMwPoVd0CIoZQqupBSNbOisPL5am9XsbQ9T84+iiMpUvbFtkunbA==", + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.35.4.tgz", + "integrity": "sha512-RY0JFY8Fd6RonCBtHz+DvadaPkXDSI1AUn6yWL9TipqkZ1vY8w8evqdgyDFnkm4/K1ve1TvZiaePP5oSd4+WVQ==", "cpu": [ "s390x" ], @@ -1142,13 +1142,13 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-s390x": "1.3.1" + "@img/sharp-libvips-linux-s390x": "1.3.3" } }, "node_modules/@img/sharp-linux-x64": { - "version": "0.35.2", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.35.2.tgz", - "integrity": "sha512-gi0zFJJRLswfCZmHtJdikXPOc5u7qamSOS3NHedLqLd4W8Q0NqjdBr6TTRIgsfFjqfTsHFgdfvJ9LwqSgcHiAA==", + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.35.4.tgz", + "integrity": "sha512-9qvvEAuk8k89TfWUoX2htWjbAMX8p+NxCppjpcg5k6xMsjhBQPTsoIh36h9Qde4WRuGpJeYnOjdosDn/cnv+OA==", "cpu": [ "x64" ], @@ -1167,13 +1167,13 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-x64": "1.3.1" + "@img/sharp-libvips-linux-x64": "1.3.3" } }, "node_modules/@img/sharp-linuxmusl-arm64": { - "version": "0.35.2", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.35.2.tgz", - "integrity": "sha512-siWbOW1u6HFnFLrp0waKyW7VEf7jYvcDWdrXEFa8AkdAQgEvuu5Fz8/Y70w9EeqAdwDtfU012BhEHHaDqvQNzg==", + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.35.4.tgz", + "integrity": "sha512-KB5jxpfWQTr0nc3xdHtWChdbifHrBGsd2SM62Eyxrl8afikm+f5qGBU75SJIZBT/S1MC8XyacdlXBMSWq6OURA==", "cpu": [ "arm64" ], @@ -1192,13 +1192,13 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-arm64": "1.3.1" + "@img/sharp-libvips-linuxmusl-arm64": "1.3.3" } }, "node_modules/@img/sharp-linuxmusl-x64": { - "version": "0.35.2", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.35.2.tgz", - "integrity": "sha512-YBqMMcjDi4QGYiSn4vNOYBhmlC4z5AXqkOUUqI2e0AFA4urNv4ESgOgwNl3K+4etQhha0twXlzeF20bbULm9Yg==", + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.35.4.tgz", + "integrity": "sha512-f+eZJZIQNEEd26RPSW+76chwOf1XtA2Y/O+5ocVyLliHkeih3e+jhLVBdNTd2rS3IbNXK8+ug93Vf5ZXtF5Lxg==", "cpu": [ "x64" ], @@ -1217,17 +1217,17 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-x64": "1.3.1" + "@img/sharp-libvips-linuxmusl-x64": "1.3.3" } }, "node_modules/@img/sharp-wasm32": { - "version": "0.35.2", - "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.35.2.tgz", - "integrity": "sha512-Mrv4JQNYVQ94xH+jzZ9r+gowleN8mv2FTgKT+PI6bx5C0G8TdNYndu161pg2i7uoBwxy2ImPMHrJOM2LZef7Bw==", + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.35.4.tgz", + "integrity": "sha512-zQnl4Kwp7Q6NHsENtU2T/00Zi+w3AQNwz3+UaTyVBy2FpXrzXzGjndpK61onhZjRtRpQXxCTeqw19bVyXOh7jA==", "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", "optional": true, "dependencies": { - "@emnapi/runtime": "^1.11.1" + "@emnapi/runtime": "^1.11.3" }, "engines": { "node": ">=20.9.0" @@ -1237,16 +1237,16 @@ } }, "node_modules/@img/sharp-webcontainers-wasm32": { - "version": "0.35.2", - "resolved": "https://registry.npmjs.org/@img/sharp-webcontainers-wasm32/-/sharp-webcontainers-wasm32-0.35.2.tgz", - "integrity": "sha512-QNV27pxs9wpApEiCfvHM1RDoP1w1+2KrUWWDPEhEwg+latvOrfuhWrHWZKwdSFwU6jh3myjw/yOCRsUIuOft3g==", + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-webcontainers-wasm32/-/sharp-webcontainers-wasm32-0.35.4.tgz", + "integrity": "sha512-ESfNkywmCfPNyaZjxooddJQiQ+l/nTpGEOGthxiLnIHXC/CmcBixnfwUleX9mCz9ovrUUvKMap/pm8RYbzfwaA==", "cpu": [ "wasm32" ], "license": "Apache-2.0", "optional": true, "dependencies": { - "@img/sharp-wasm32": "0.35.2" + "@img/sharp-wasm32": "0.35.4" }, "engines": { "node": ">=20.9.0" @@ -1256,9 +1256,9 @@ } }, "node_modules/@img/sharp-win32-arm64": { - "version": "0.35.2", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.35.2.tgz", - "integrity": "sha512-BiVRYc/t6/Vl3e1hBx0hugG4oN9Pydf4fgMSpxTQJmwGUg/YoXTWHiFeRymHfCZzifxu4F4rpk/I67D0LQ20wQ==", + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.35.4.tgz", + "integrity": "sha512-iNdlBX9gLVvqe2I3uIJSIKTq6wckP/DYxZtcqxm09x5Gi24DnFBmPAWZmr60ZyYMG0xlzo6goG3670ar+RXvRw==", "cpu": [ "arm64" ], @@ -1275,9 +1275,9 @@ } }, "node_modules/@img/sharp-win32-ia32": { - "version": "0.35.2", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.35.2.tgz", - "integrity": "sha512-YYEhx9PImCC7T0tI8JDMi4DB9LwLCXCU5OWNYEXAxh5Q1ShKkyC6byxzoBJ3gEFDnH2lQckWuDe70G7mB2XJog==", + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.35.4.tgz", + "integrity": "sha512-kqRsbaa5CS6KHlpxnN7WhE6vAAugXyZButpRdvDWetlv6Qv4N9WTcrWzF7tXfB9T7MsoadqdI8hmwLq6UlLvtw==", "cpu": [ "ia32" ], @@ -1294,9 +1294,9 @@ } }, "node_modules/@img/sharp-win32-x64": { - "version": "0.35.2", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.35.2.tgz", - "integrity": "sha512-imoOyBcoM/iiUr4J6VPpCNjPnjvP/Gks95898yB8YqoGGYmHYbOyCuNv9FMhFgtaiHFGbHW8bxKqRV6VjtXThQ==", + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.35.4.tgz", + "integrity": "sha512-XtmnYhBcrORsJ4XJngyzr/EWP0hRZLAZRFaApdKuviyqF78+ylxh2y06ZmtULAMOnObJ3ucpN0AcwSWnMowTRg==", "cpu": [ "x64" ], @@ -1322,9 +1322,9 @@ } }, "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.5", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", - "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.6.0.tgz", + "integrity": "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==", "license": "MIT" }, "node_modules/@jridgewell/trace-mapping": { @@ -1376,9 +1376,9 @@ } }, "node_modules/@speed-highlight/core": { - "version": "1.2.23", - "resolved": "https://registry.npmjs.org/@speed-highlight/core/-/core-1.2.23.tgz", - "integrity": "sha512-iRoq6i6JDJP6Mt2A5JaPvzw0pgYHH6k92ij+yXiTrB7T2y9N789aWE3EHWj/5ztlJBokcCBja3iYLVdu5wgnkg==", + "version": "1.2.24", + "resolved": "https://registry.npmjs.org/@speed-highlight/core/-/core-1.2.24.tgz", + "integrity": "sha512-qeW2e1l78afw8VhRPfPQ1Gjj+KU5XFQ/OFV5ti6eTa9bruO7mJyZtA4vw0ofqmA3tKCkROE9xLk3VZoeRc98nw==", "license": "CC0-1.0" }, "node_modules/@types/esrecurse": { @@ -2000,15 +2000,15 @@ } }, "node_modules/miniflare": { - "version": "5.20260801.1-alpha", - "resolved": "https://registry.npmjs.org/miniflare/-/miniflare-5.20260801.1-alpha.tgz", - "integrity": "sha512-BHPVzIDA6mbx7LefxpvkXW7DHx9FKB9GorZatbnrrFTt3CVMU8zuUpbgyCuebwDKcTTOZos43ta8GQ0eMVEpxA==", + "version": "5.20260910.0-alpha", + "resolved": "https://registry.npmjs.org/miniflare/-/miniflare-5.20260910.0-alpha.tgz", + "integrity": "sha512-V41TxLjX1Dq4pys8X5GAl/CrUY0//QcXCIdYwy01Cev+JdsWOWzzN0IP45lojgw+fEVY9QdepzVIziT2IxhbUQ==", "license": "MIT", "dependencies": { "@cspotcode/source-map-support": "0.8.1", - "sharp": "0.35.2", + "sharp": "0.35.4", "undici": "7.29.0", - "workerd": "1.20260801.1", + "workerd": "1.20260910.1", "ws": "8.21.0", "youch": "4.1.0-beta.10" }, @@ -2177,14 +2177,14 @@ } }, "node_modules/sharp": { - "version": "0.35.2", - "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.35.2.tgz", - "integrity": "sha512-FVtFjtBCMiJS6yb5CX7Sop45WFMpeGw6oRKuJnXYgf/f1ms/D7LE/ZUSNxnW7rZ/dbslQWYkoqFHGPaDBtaK4w==", + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.35.4.tgz", + "integrity": "sha512-n++8XWcj+jCOr2IOl7h8LbKnGBDY4aPbmprMONBNFdn0ImXqpGVv5zliDs0V9HbmbCQLpbuo2ej9rAoOQTvMDA==", "license": "Apache-2.0", "dependencies": { "@img/colour": "^1.1.0", "detect-libc": "^2.1.2", - "semver": "^7.8.4" + "semver": "^7.8.5" }, "engines": { "node": ">=20.9.0" @@ -2193,31 +2193,36 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-darwin-arm64": "0.35.2", - "@img/sharp-darwin-x64": "0.35.2", - "@img/sharp-freebsd-wasm32": "0.35.2", - "@img/sharp-libvips-darwin-arm64": "1.3.1", - "@img/sharp-libvips-darwin-x64": "1.3.1", - "@img/sharp-libvips-linux-arm": "1.3.1", - "@img/sharp-libvips-linux-arm64": "1.3.1", - "@img/sharp-libvips-linux-ppc64": "1.3.1", - "@img/sharp-libvips-linux-riscv64": "1.3.1", - "@img/sharp-libvips-linux-s390x": "1.3.1", - "@img/sharp-libvips-linux-x64": "1.3.1", - "@img/sharp-libvips-linuxmusl-arm64": "1.3.1", - "@img/sharp-libvips-linuxmusl-x64": "1.3.1", - "@img/sharp-linux-arm": "0.35.2", - "@img/sharp-linux-arm64": "0.35.2", - "@img/sharp-linux-ppc64": "0.35.2", - "@img/sharp-linux-riscv64": "0.35.2", - "@img/sharp-linux-s390x": "0.35.2", - "@img/sharp-linux-x64": "0.35.2", - "@img/sharp-linuxmusl-arm64": "0.35.2", - "@img/sharp-linuxmusl-x64": "0.35.2", - "@img/sharp-webcontainers-wasm32": "0.35.2", - "@img/sharp-win32-arm64": "0.35.2", - "@img/sharp-win32-ia32": "0.35.2", - "@img/sharp-win32-x64": "0.35.2" + "@img/sharp-darwin-arm64": "0.35.4", + "@img/sharp-darwin-x64": "0.35.4", + "@img/sharp-freebsd-wasm32": "0.35.4", + "@img/sharp-libvips-darwin-arm64": "1.3.3", + "@img/sharp-libvips-darwin-x64": "1.3.3", + "@img/sharp-libvips-linux-arm": "1.3.3", + "@img/sharp-libvips-linux-arm64": "1.3.3", + "@img/sharp-libvips-linux-ppc64": "1.3.3", + "@img/sharp-libvips-linux-riscv64": "1.3.3", + "@img/sharp-libvips-linux-s390x": "1.3.3", + "@img/sharp-libvips-linux-x64": "1.3.3", + "@img/sharp-libvips-linuxmusl-arm64": "1.3.3", + "@img/sharp-libvips-linuxmusl-x64": "1.3.3", + "@img/sharp-linux-arm": "0.35.4", + "@img/sharp-linux-arm64": "0.35.4", + "@img/sharp-linux-ppc64": "0.35.4", + "@img/sharp-linux-riscv64": "0.35.4", + "@img/sharp-linux-s390x": "0.35.4", + "@img/sharp-linux-x64": "0.35.4", + "@img/sharp-linuxmusl-arm64": "0.35.4", + "@img/sharp-linuxmusl-x64": "0.35.4", + "@img/sharp-webcontainers-wasm32": "0.35.4", + "@img/sharp-win32-arm64": "0.35.4", + "@img/sharp-win32-ia32": "0.35.4", + "@img/sharp-win32-x64": "0.35.4" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } } }, "node_modules/shebang-command": { @@ -2244,9 +2249,9 @@ } }, "node_modules/smol-toml": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/smol-toml/-/smol-toml-1.7.0.tgz", - "integrity": "sha512-aqVvWoyO21L23mb+drl4RmMXbf6N7FdHjAhTRA9ZBL7apWBgfWC16KjrASI+1p9GAroljyMHj6fK67i0UiTNvQ==", + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/smol-toml/-/smol-toml-1.7.1.tgz", + "integrity": "sha512-PPlsspAZ4jbMBu5DMFhfUGDQLu/vrL4SyBROVS37x8ynnVmFIs1VPBz1Co8Xks3TvpIaZXmU85y4DrQ+UyVFoQ==", "license": "BSD-3-Clause", "engines": { "node": ">= 18" @@ -2363,9 +2368,9 @@ } }, "node_modules/workerd": { - "version": "1.20260801.1", - "resolved": "https://registry.npmjs.org/workerd/-/workerd-1.20260801.1.tgz", - "integrity": "sha512-/g9JGTyqnHtoIscpBHqKD8swE2V4StBs2i69PmLiOhH45OP95jCFICl4F1hKlgN57rqfni5LiCitttoX5OOkVA==", + "version": "1.20260910.1", + "resolved": "https://registry.npmjs.org/workerd/-/workerd-1.20260910.1.tgz", + "integrity": "sha512-/HF0f8LmgT60Uk+ZPDbMnlUHjecfV8w4VMo61iFxW7ky7EexTdwbzUCANWZQiG0w1cBihjJTkhzmReeZLGarXw==", "hasInstallScript": true, "license": "Apache-2.0", "bin": { @@ -2375,27 +2380,27 @@ "node": ">=16" }, "optionalDependencies": { - "@cloudflare/workerd-darwin-64": "1.20260801.1", - "@cloudflare/workerd-darwin-arm64": "1.20260801.1", - "@cloudflare/workerd-linux-64": "1.20260801.1", - "@cloudflare/workerd-linux-arm64": "1.20260801.1", - "@cloudflare/workerd-windows-64": "1.20260801.1" + "@cloudflare/workerd-darwin-64": "1.20260910.1", + "@cloudflare/workerd-darwin-arm64": "1.20260910.1", + "@cloudflare/workerd-linux-64": "1.20260910.1", + "@cloudflare/workerd-linux-arm64": "1.20260910.1", + "@cloudflare/workerd-windows-64": "1.20260910.1" } }, "node_modules/wrangler": { - "version": "4.120.0", - "resolved": "https://registry.npmjs.org/wrangler/-/wrangler-4.120.0.tgz", - "integrity": "sha512-cBmu/MeaB/fPacC0JpATs4duTOCagBxrZo+vBzuTX06tLzwSyAHE1drlHUZ8rP0VqVz1fy3ReGYTiHdKkoHltg==", + "version": "4.131.0", + "resolved": "https://registry.npmjs.org/wrangler/-/wrangler-4.131.0.tgz", + "integrity": "sha512-ElsBshkzBhpIeaqc7sP5lsAEXYTrHPxWGKExK+HwIRYFleiKkW4ozvQidW36OXcsIUbZ0pPJJUP5RDodKbN0cA==", "license": "MIT OR Apache-2.0", "dependencies": { "@cloudflare/kv-asset-handler": "0.5.0", "@cloudflare/unenv-preset": "2.16.1", "blake3-wasm": "2.1.5", "esbuild": "0.28.1", - "miniflare": "5.20260801.1-alpha", + "miniflare": "5.20260910.0-alpha", "path-to-regexp": "6.3.0", "unenv": "2.0.0-rc.24", - "workerd": "1.20260801.1" + "workerd": "1.20260910.1" }, "bin": { "cf-wrangler": "bin/cf-wrangler.js", @@ -2409,7 +2414,7 @@ "fsevents": "2.3.3" }, "peerDependencies": { - "@cloudflare/workers-types": "^5.20260801.1" + "@cloudflare/workers-types": "^5.20260910.1" }, "peerDependenciesMeta": { "@cloudflare/workers-types": { diff --git a/package.json b/package.json index 9588c35..bf6e555 100644 --- a/package.json +++ b/package.json @@ -45,8 +45,8 @@ ], "dependencies": { "jsonc-parser": "3.3.1", - "smol-toml": "^1.7.0", - "wrangler": "4.120.0" + "smol-toml": "^1.7.1", + "wrangler": "4.131.0" }, "engines": { "node": ">=22" diff --git a/templates/AGENTS.md b/templates/AGENTS.md index 517cb75..8865b84 100644 --- a/templates/AGENTS.md +++ b/templates/AGENTS.md @@ -58,12 +58,16 @@ disabling the default platform-domain URL; it requires at least one `route` / `routes` pattern and is not inferred. The deploy summary prints every active route-pattern URL hint, preserving the trailing `*` on prefix patterns, and includes the platform-domain URL only while it is enabled. Cloudflare's separate -`preview_urls` field is unsupported and rejected by the CLI. Cloudflare -Artifacts `triggers.events` subscriptions and R2 +`preview_urls` field is unsupported and rejected by the CLI. `[[connect]]` TCP +listeners, Cloudflare Artifacts `triggers.events` subscriptions, and R2 `local_dev.experimental_s3_credentials` are also unsupported and rejected. -`[ai] binding = "AI"` declares the AI facade; provider metadata and credentials -are namespace resources managed with `wdl ai`, never ordinary Worker secrets. -Use `wdl ai providers init ` to scaffold a conservative one-model +`[[workflows]]` supports only `name`, `binding`, and `class_name`; `script_name` +and all other fields, including `schedules`, `limits`, `default_retention`, and +`concurrency`, are rejected before bundling. Use per-instance `create()` +retention instead of `[[workflows]].default_retention`. `[ai] binding = "AI"` +declares the AI facade; provider metadata and credentials are namespace +resources managed with `wdl ai`, never ordinary Worker secrets. Use +`wdl ai providers init ` to scaffold a conservative one-model Responses config, then edit model-specific capabilities before `providers put`. Initializer output rejects non-text input, `previous_response_id` continuation, and binary WebSocket frames until their corresponding declarations are enabled; diff --git a/tests/unit/cli-command.test.js b/tests/unit/cli-command.test.js index 69dd81e..81ff78a 100644 --- a/tests/unit/cli-command.test.js +++ b/tests/unit/cli-command.test.js @@ -1,8 +1,17 @@ import { test } from "node:test"; import assert from "node:assert/strict"; +import { runAiCommand } from "../../commands/ai.js"; +import { runD1Command } from "../../commands/d1.js"; +import { runDeleteCommand } from "../../commands/delete.js"; +import { runDeployCommand } from "../../commands/deploy.js"; +import { runR2Command } from "../../commands/r2.js"; +import { runSecretCommand } from "../../commands/secret.js"; +import { runTailCommand } from "../../commands/tail.js"; +import { runWorkersCommand } from "../../commands/workers.js"; +import { runWorkflowsCommand } from "../../commands/workflows.js"; import { defineCommand } from "../../lib/command.js"; import { CliError, defineCliOption } from "../../lib/common.js"; -import { ESC, assertNoRawTerminalControls, response } from "./helpers.js"; +import { ESC, assertNoRawTerminalControls, mockDeps, response } from "./helpers.js"; /** @typedef {Parameters[0]} CommandSpec */ /** @typedef {import("../../lib/command.js").CommandContext} CommandContext */ @@ -12,6 +21,37 @@ import { ESC, assertNoRawTerminalControls, response } from "./helpers.js"; /** @param {Omit & { name?: string, summary?: string }} spec */ const define = (spec) => defineCommand({ name: "t", summary: "t", ...spec }); +test("namespace-required commands explain missing namespaces before contacting Control", async () => { + for (const { name, run, args } of [ + { name: "ai", run: runAiCommand, args: ["models"] }, + { name: "d1", run: runD1Command, args: ["list"] }, + { name: "delete", run: runDeleteCommand, args: ["worker", "api"] }, + { name: "deploy", run: runDeployCommand, args: ["."] }, + { name: "r2", run: runR2Command, args: ["buckets", "list"] }, + { name: "secret", run: runSecretCommand, args: ["list", "--scope", "ns"] }, + { name: "tail", run: runTailCommand, args: ["api"] }, + { name: "workers", run: runWorkersCommand, args: [] }, + { name: "workflows", run: runWorkflowsCommand, args: ["list"] }, + ]) { + const { calls, lines, deps } = mockDeps({}, {}); + for (const flags of [[], ["--ns", ""]]) { + await assert.rejects( + () => run([...args, ...flags], deps), + (err) => { + assert(err instanceof CliError); + assert.match(err.message, /Missing namespace/); + assert.match(err.message, /--ns /); + assert.match(err.message, /WDL_NS/); + return true; + }, + name + ); + } + assert.deepEqual(calls, [], name); + assert.deepEqual(lines, [], name); + } +}); + test("defineCommand assembles flag presets and custom options", async () => { let seen = /** @type {{ values: Record, positionals: string[] }} */ (/** @type {unknown} */ (null)); const cmd = define({ diff --git a/tests/unit/cli-control-fetch.test.js b/tests/unit/cli-control-fetch.test.js index 55b9f4c..d35af56 100644 --- a/tests/unit/cli-control-fetch.test.js +++ b/tests/unit/cli-control-fetch.test.js @@ -1,6 +1,6 @@ import { test } from "node:test"; import assert from "node:assert/strict"; -import { EventEmitter } from "node:events"; +import { EventEmitter, once } from "node:events"; import { PassThrough } from "node:stream"; import { CliError } from "../../lib/common.js"; import { DEFAULT_CONTROL_MAX_BODY_BYTES, controlFetch, readControlResponse } from "../../lib/control-fetch.js"; @@ -227,7 +227,8 @@ test("controlFetch keeps timeout active while streaming the response body", asyn assert.match(requestDestroyError.message, /control request timed out after 20ms/); }); -test("controlFetch streaming timeout is idle-based after headers", async () => { +test("controlFetch streaming timeout is idle-based after headers", async (t) => { + t.mock.timers.enable({ apis: ["setTimeout"] }); const res = Object.assign(new PassThrough(), { statusCode: 200, headers: { "content-type": "application/octet-stream" }, @@ -239,9 +240,6 @@ test("controlFetch streaming timeout is idle-based after headers", async () => { write() {}, end() { onResponse(/** @type {import("node:http").IncomingMessage} */ (/** @type {unknown} */ (res))); - setTimeout(() => res.write("a"), 10); - setTimeout(() => res.write("b"), 25); - setTimeout(() => res.end("c"), 40); }, destroy() {}, }); @@ -257,9 +255,17 @@ test("controlFetch streaming timeout is idle-based after headers", async () => { /** @type {string[]} */ const chunks = []; const body = /** @type {import("node:stream").Readable} */ (response.body); - for await (const chunk of body) { - chunks.push(Buffer.from(chunk).toString("utf8")); + for (const chunk of ["a", "b", "c"]) { + const received = once(body, "data"); + // Total duration exceeds the timeout, but each chunk arrives before idle expiry. + t.mock.timers.tick(15); + res.write(chunk); + const [data] = await received; + chunks.push(Buffer.from(data).toString("utf8")); } + const ended = once(body, "end"); + res.end(); + await ended; assert.equal(chunks.join(""), "abc"); }); diff --git a/tests/unit/cli-deploy.test.js b/tests/unit/cli-deploy.test.js index d8a9a62..d50e96f 100644 --- a/tests/unit/cli-deploy.test.js +++ b/tests/unit/cli-deploy.test.js @@ -20,7 +20,7 @@ import { ESC, assertNoRawTerminalControls, response } from "./helpers.js"; * pipeline sets is present, so reads here are unconditional. * @typedef {object} ExecFileOpts * @property {string} [cwd] - * @property {"inherit" | readonly ("ignore" | "pipe")[]} [stdio] + * @property {readonly ("ignore" | "pipe" | "inherit")[]} [stdio] * @property {string} [encoding] * @property {number} [maxBuffer] * @property {NodeJS.ProcessEnv} env @@ -144,6 +144,45 @@ test("runDeployCommand rejects unsupported event triggers before running Wrangle assert.equal(execCalled, false); }); +test("runDeployCommand rejects unknown Workflow fields before bundling the selected config", async (t) => { + for (const envName of [null, "production"]) { + for (const [key, value] of [ + ["schedules", '"*/5 * * * *"'], + ["limits", "{ steps = 5 }"], + ["default_retention", '{ success_retention = "3 days" }'], + ["concurrency", "{ limit = 10 }"], + ]) { + const dir = createDeployProject( + t, + [ + 'name = "api"', + 'main = "src/index.js"', + `[[${envName ? `env.${envName}.` : ""}workflows]]`, + 'name = "orders"', + 'binding = "ORDERS"', + 'class_name = "OrderWorkflow"', + `${key} = ${value}`, + ].join("\n") + ); + let execCalled = false; + const args = [dir, "--ns", "demo", "--control-url", "http://ctl.test"]; + if (envName) args.push("--env", envName); + await assert.rejects( + () => + runDeployCommand(args, { + env: { ADMIN_TOKEN: "tok" }, + execFile: () => { + execCalled = true; + throw new Error("execFile should not be called"); + }, + }), + new RegExp(`unknown field\\(s\\): ${key}`) + ); + assert.equal(execCalled, false); + } + } +}); + test("runDeployCommand resolves cwd-relative project dir and WDL_NS fallback", async () => { const parent = mkdtempSync(path.join(tmpdir(), "wdl-run-deploy-")); const dir = path.join(parent, "sub"); @@ -245,6 +284,7 @@ test("runDeployCommand resolves cwd-relative project dir and WDL_NS fallback", a assert.deepEqual(execCalls[0].opts.stdio, ["ignore", "pipe", "pipe"]); assert.equal(execCalls[0].opts.encoding, "utf8"); assert.equal(execCalls[0].opts.env.CLOUDFLARE_API_TOKEN, "dry-run-dummy"); + assert.equal(execCalls[0].opts.env.WRANGLER_NO_SKILLS_UPDATE_PROMPTS, "true"); assert.ok(execCalls[1].args.includes("deploy")); assert.ok(execCalls[1].args.includes("--dry-run")); assert.equal(execCalls[1].opts.cwd, dir); @@ -252,6 +292,7 @@ test("runDeployCommand resolves cwd-relative project dir and WDL_NS fallback", a assert.equal(execCalls[1].opts.encoding, "utf8"); assert.equal(execCalls[1].opts.maxBuffer, 10 * 1024 * 1024); assert.equal(execCalls[1].opts.env.CLOUDFLARE_API_TOKEN, "dry-run-dummy"); + assert.equal(execCalls[1].opts.env.WRANGLER_NO_SKILLS_UPDATE_PROMPTS, "true"); assert.equal(fetchCalls.length, 2); assert.equal(fetchCalls[0].url, "http://ctl.test/ns/demo%20space/worker/api/deploy"); @@ -1522,7 +1563,7 @@ test("packWranglerProject escapes missing entry diagnostics", async () => { } }); -test("runDeployCommand passes through wrangler output in verbose mode", async () => { +test("runDeployCommand keeps verbose Wrangler output without interactive stdin", async () => { const dir = mkdtempSync(path.join(tmpdir(), "wdl-run-deploy-verbose-")); try { mkdirSync(path.join(dir, "src"), { recursive: true }); @@ -1553,7 +1594,8 @@ test("runDeployCommand passes through wrangler output in verbose mode", async () assert.equal(execCalls.length, 2); assertWranglerVersionProbe(execCalls[0]); - assert.equal(execCalls[1].opts.stdio, "inherit"); + assert.deepEqual(execCalls[1].opts.stdio, ["ignore", "inherit", "inherit"]); + assert.equal(execCalls[1].opts.env.WRANGLER_NO_SKILLS_UPDATE_PROMPTS, "true"); assert.equal(Object.hasOwn(execCalls[1].opts, "encoding"), false); } finally { rmSync(dir, { recursive: true, force: true }); diff --git a/tests/unit/cli-workflows.test.js b/tests/unit/cli-workflows.test.js index 962f059..2c07a5e 100644 --- a/tests/unit/cli-workflows.test.js +++ b/tests/unit/cli-workflows.test.js @@ -97,6 +97,7 @@ test("workflow formatters escape control fields but preserve their own layout", const hostile = `${ESC}[2J\nFORGED\rBAD\tCOLUMN\u009b`; const lines = [ ...formatWorkflowList({ + cursor: hostile, workflows: [ { worker: hostile, @@ -130,6 +131,88 @@ test("workflow formatters escape control fields but preserve their own layout", assert.equal(out.split("\t").length - 1, 7, "only formatter-owned column separators may remain as raw tabs"); }); +test("workflow definition listing forwards pagination and keeps empty-page continuation visible", async () => { + const { deps, calls, lines } = mockDeps({ namespace: "demo", workflows: [], cursor: "next+page/=" }); + await runWorkflowsCommand( + ["list", "--ns", "demo", "--control-url", "http://ctl.test", "--limit", "2", "--cursor", "a+b/="], + deps + ); + assert.equal(calls[0].url, "http://ctl.test/ns/demo/workflows?limit=2&cursor=a%2Bb%2F%3D"); + assert.deepEqual(lines, ["(no workflows on this page)", "Next cursor: next+page/="]); +}); + +test("workflow definition listing displays continuation after non-empty pages", async () => { + const { deps, lines } = mockDeps({ workflows: [{ worker: "api", name: "orders" }], cursor: "next+page/=" }); + + await runWorkflowsCommand(["list", "--ns", "demo", "--control-url", "http://ctl.test"], deps); + + assert.deepEqual(lines, ["api/orders\tbinding=-\tclass=-\tactive=-\tkey=-\tretired=no", "Next cursor: next+page/="]); +}); + +test("workflow definition listing explains metadata contention without retrying a stale cursor", async () => { + for (const { error, cursor, hint } of [ + { error: "workflow_metadata_contention", cursor: "", hint: /; workflow metadata changed, retry the command/ }, + { + error: "workflow_metadata_contention", + cursor: "stale-cursor", + hint: /; workflow metadata changed, restart the listing without --cursor/, + }, + { error: "workflow_backend_error", cursor: "stale-cursor", hint: null }, + ]) { + const { deps, calls, lines } = mockDeps({}); + const args = ["list", "--ns", "demo", "--control-url", "http://ctl.test"]; + if (cursor) args.push("--cursor", cursor); + await assert.rejects( + () => + runWorkflowsCommand(args, { + ...deps, + controlFetch: async ( + /** @type {string} */ url, + /** @type {import("../../lib/control-fetch.js").ControlFetchInit} */ init = {} + ) => { + calls.push({ url, init }); + return response({ error, message: "Internal error" }, 503); + }, + }), + (err) => { + const message = /** @type {Error} */ (err).message; + assert.match(message, new RegExp(`list workflows failed: 503 ${error}: Internal error`)); + if (hint) assert.match(message, hint); + else assert.doesNotMatch(message, /retry|restart|without --cursor/); + return true; + } + ); + assert.equal(calls.length, 1); + assert.equal(new URL(calls[0].url).searchParams.get("cursor"), cursor || null); + assert.equal(calls[0].init.env, deps.env); + assert.deepEqual(lines, []); + } +}); + +test("workflow instance listing keeps empty-page continuation visible", async () => { + const { deps, lines } = mockDeps({ instances: [], cursor: "7" }); + + await runWorkflowsCommand(["instances", "api", "orders", "--ns", "demo", "--control-url", "http://ctl.test"], deps); + + assert.deepEqual(lines, ["(no workflow instances on this page)", "Next cursor: 7"]); +}); + +test("workflow listings label empty final continuation pages", async () => { + const definitions = mockDeps({ workflows: [], cursor: null }); + await runWorkflowsCommand( + ["list", "--cursor", "final-definitions", "--ns", "demo", "--control-url", "http://ctl.test"], + definitions.deps + ); + assert.deepEqual(definitions.lines, ["(no workflows on this page)"]); + + const instances = mockDeps({ instances: [], cursor: null }); + await runWorkflowsCommand( + ["instances", "api", "orders", "--cursor", "final-instances", "--ns", "demo", "--control-url", "http://ctl.test"], + instances.deps + ); + assert.deepEqual(instances.lines, ["(no workflow instances on this page)"]); +}); + test("workflow lifecycle status lines escape control fields and preserve JSON", async () => { const hostile = `${ESC}[2J\nFORGED\rBAD\tCOLUMN\u009b`; const body = { id: `id-${hostile}`, status: `status-${hostile}` }; @@ -176,6 +259,10 @@ test("workflow page limits must be integers from 1 through 1000", async () => { await runWorkflowsCommand(["status", "api", "orders", "id", "--step-limit", "", "--ns", "demo"], deps); for (const value of INVALID_PAGE_LIMITS) { + await assert.rejects( + () => runWorkflowsCommand(["list", "--limit", value, "--ns", "demo"], deps), + /workflows --limit must be an integer in \[1, 1000\]/ + ); await assert.rejects( () => runWorkflowsCommand(["instances", "api", "orders", "--limit", value, "--ns", "demo"], deps), /workflows --limit must be an integer in \[1, 1000\]/ diff --git a/tests/unit/cli-wrangler-bindings.test.js b/tests/unit/cli-wrangler-bindings.test.js index e624ecb..fd696ad 100644 --- a/tests/unit/cli-wrangler-bindings.test.js +++ b/tests/unit/cli-wrangler-bindings.test.js @@ -571,6 +571,30 @@ test("parseWorkflowsFromCfg: parses local workflow declarations", () => { ); }); +test("parseWorkflowsFromCfg: rejects unknown workflow fields by presence", () => { + for (const key of ["schedules", "limits", "default_retention", "concurrency", "future_policy"]) { + for (const value of [{}, null, false, 0, "", undefined]) { + assert.throws( + () => + parseWorkflowsFromCfg({ + workflows: [{ name: "flow", binding: "WF", class_name: "Flow", [key]: value }], + }), + new RegExp(`unknown field\\(s\\): ${key}`) + ); + } + } + assertThrowsNoRawTerminalControls( + () => + parseWorkflowsFromCfg({ + workflows: [ + { name: "flow", binding: "WF", class_name: "Flow", [`policy${ESC}[2J\nFORGED\rBAD\tCOLUMN\u009b`]: {} }, + ], + }), + /unknown field\(s\):/, + "workflow field diagnostics" + ); +}); + test("parseWorkflowsFromCfg: rejects invalid names and unsupported script_name", () => { assert.throws(() => parseWorkflowsFromCfg({ workflows: {} }), /must be an array/); assert.throws( @@ -626,13 +650,15 @@ test("parseWorkflowsFromCfg: rejects invalid names and unsupported script_name", }), /reserved for runtime-injected entrypoints/ ); - assert.throws( - () => - parseWorkflowsFromCfg({ - workflows: [{ name: "flow", binding: "WF", class_name: "Flow", script_name: "other" }], - }), - /script_name is not supported/ - ); + for (const scriptName of ["other", null, undefined]) { + assert.throws( + () => + parseWorkflowsFromCfg({ + workflows: [{ name: "flow", binding: "WF", class_name: "Flow", script_name: scriptName }], + }), + scriptName == null ? /unknown field\(s\): script_name/ : /script_name is not supported/ + ); + } }); test("parseExportsFromCfg: absent → empty; snake→camel translation", () => { diff --git a/tests/unit/cli-wrangler-command.test.js b/tests/unit/cli-wrangler-command.test.js index 74428c5..10c7e0f 100644 --- a/tests/unit/cli-wrangler-command.test.js +++ b/tests/unit/cli-wrangler-command.test.js @@ -389,26 +389,30 @@ test("resolveWranglerCommand on win32 runs an executable npx shim directly", () } }); -test("wranglerChildEnv scrubs control env, hides Wrangler's banner, and disables telemetry", () => { +test("wranglerChildEnv scrubs control env and disables Wrangler prompts and telemetry", () => { assert.deepEqual( - wranglerChildEnv({ - ADMIN_TOKEN: "secret", - CONTROL_CONNECT_HOST: "ctl.connect.example", - CONTROL_URL: "https://ctl.example", - WDL_NS: "tenant", - // Legacy alias the CLI no longer reads, but must still scrub so a stale - // export does not leak the control endpoint into the bundler. - ADMIN_URL: "https://legacy-admin.example", - CLOUDFLARE_API_TOKEN: "real-cloudflare-token", - WRANGLER_HIDE_BANNER: "false", - WRANGLER_SEND_METRICS: "true", - PATH: "/bin", - KEEP_ME: "ok", - }), + wranglerChildEnv( + Object.freeze({ + ADMIN_TOKEN: "secret", + CONTROL_CONNECT_HOST: "ctl.connect.example", + CONTROL_URL: "https://ctl.example", + WDL_NS: "tenant", + // Legacy alias the CLI no longer reads, but must still scrub so a stale + // export does not leak the control endpoint into the bundler. + ADMIN_URL: "https://legacy-admin.example", + CLOUDFLARE_API_TOKEN: "real-cloudflare-token", + WRANGLER_HIDE_BANNER: "false", + WRANGLER_SEND_METRICS: "true", + WRANGLER_NO_SKILLS_UPDATE_PROMPTS: "false", + PATH: "/bin", + KEEP_ME: "ok", + }) + ), { CLOUDFLARE_API_TOKEN: "dry-run-dummy", WRANGLER_HIDE_BANNER: "true", WRANGLER_SEND_METRICS: "false", + WRANGLER_NO_SKILLS_UPDATE_PROMPTS: "true", PATH: "/bin", KEEP_ME: "ok", } diff --git a/tests/unit/cli-wrangler-config.test.js b/tests/unit/cli-wrangler-config.test.js index 72c12fd..7f49194 100644 --- a/tests/unit/cli-wrangler-config.test.js +++ b/tests/unit/cli-wrangler-config.test.js @@ -704,6 +704,17 @@ test("validateUnsupportedWranglerConfig: rejects unsupported config inside the s ); }); +test("validateUnsupportedWranglerConfig rejects connect listeners at top-level and in the selected environment", () => { + for (const connect of [[{ protocol: "tcp", port: 9000 }], [], null]) { + for (const cfg of [{ connect, env: { staging: {} } }, { env: { staging: { connect } } }]) { + assert.throws( + () => validateUnsupportedWranglerConfig(cfg, "staging", "wrangler.toml"), + /uses unsupported Wrangler field "connect"/ + ); + } + } +}); + test("validateUnsupportedWranglerConfig: top-level allowed_callers is rejected with the [[exports]] migration path", () => { assert.throws( () =>