diff --git a/CHANGELOG.md b/CHANGELOG.md index ea639e1..a7282b0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +- Accept strictly validated, identifier-free update outcomes in a separate, explicitly configured dataset; add a side-effect-free capability check while keeping production collection unbound. Thanks @roboclaw-bot, @fuller-stack-dev, and @vincentkoc. +- Refresh Wrangler, Cloudflare Worker types, and Vitest with the matching workerd runtime and npm lockfile. - Discard malformed UTF-8 feature-statistics uploads instead of repairing and recording them, while preserving update responses. - Keep offline exports outside their executing source checkout, linked worktrees, and input archives, including differently cased paths on case-insensitive filesystems. - Remove the public statistics dashboard and `/api/stats` endpoint while preserving the privacy page, update checks, analytics recording, and data retention. diff --git a/README.md b/README.md index f71e3a1..894f906 100644 --- a/README.md +++ b/README.md @@ -12,8 +12,34 @@ written from a validated request. | Route | Purpose | | --- | --- | | `GET \| POST /api/latest-version` | Returns `{ version, note? }`. `version` is the latest published OpenClaw release (looked up from the npm registry and cached at the edge for 5 minutes). `note` is an optional short message shown in the operator's terminal, used only when a release is worth acting on immediately. | +| `HEAD /api/latest-version` | Outcome capability only: empty 204 with `OpenClaw-Update-Results: 2` when its separate binding is present; otherwise empty 503 without that header. Always `Cache-Control: no-store`; no analytics, quota consumption or version lookup. | | `GET /` | Human-readable page: what is collected, how to turn it off, without a public statistics dashboard. | +## Identifier-free update outcomes + +The same POST endpoint also accepts a strict schema-2 `update_result` event from +a companion client implementation. These outcome reports use a **separate dataset** +and never include geography or daily feature/identity rows. All fields are required +public labels; unknown keys, invalid labels, malformed UTF-8 and bodies over 4096 +bytes are rejected. This receiver change does not enable a client or deploy collection. +See [the wire contract, storage columns, retention and private aggregate SQL](docs/update-results.md). +The companion client reports outcomes **on by default**, like the existing update +ping, under update-request policy rather than optional feature-statistics consent. +`update.checkOnStart: false`, `OPENCLAW_NO_AUTO_UPDATE=1`, and Nix mode suppress +outcomes. A truthy `CI` always suppresses outcomes, including when a replacement +`OPENCLAW_TELEMETRY_ENDPOINT` is explicitly configured. `DO_NOT_TRACK` and +`openclaw telemetry off` control feature statistics, not update outcomes. Feature +statistics remain off by default. Deploy and verify this receiver and its separate +dataset **before releasing the default-on client**, under separately authorized +rollout. The default production configuration intentionally omits `UPDATE_RESULTS`, +so the existing main-push deployment cannot implicitly activate outcome collection. +Outcome attempts HEAD the same full configured endpoint and POST only after exact +204 plus `OpenClaw-Update-Results: 2`; old receivers (405) and unconfigured receivers +(503) never receive the outcome payload. Binding presence is capability, not proof +of production delivery. The daily GET and opt-in schema-1 POST remain single requests. +See the contract for the shared timeout, no-redirect and opt-out recheck requirements. +The daily-check behavior described below is unchanged. + ## What an install sends With automatic update checks enabled, OpenClaw reuses a successful version check for 24 hours. @@ -72,7 +98,7 @@ each field; missing or invalid values are left empty without discarding valid fi ## What is stored -Each recorded request contributes one Analytics Engine data point with these columns and no others: +Each recorded daily update check contributes one Analytics Engine data point with these columns and no others (schema-2 outcome storage is documented separately above): | Column | Value | | --- | --- | @@ -99,7 +125,7 @@ enablement. The session count depends on creation events still retained in a bou Missing or unreadable state produces zero; this is not active sessions, messages, or all sessions that existed that day. -Unknown keys in a request body are dropped rather than stored, so a future client cannot silently +Unknown keys in a schema-1 request body are dropped rather than stored, so a future client cannot silently widen what this service keeps. User-Agents longer than 512 characters become an unknown identity before parsing. Identity fields remain length-bounded and character-filtered. Feature IDs must be complete identifiers of at most 64 characters; malformed or overlength IDs are dropped, never @@ -169,12 +195,13 @@ processing is outside those settings. | Command or setting | Effect | | --- | --- | -| `openclaw telemetry off` | Stops anonymous feature statistics. Update checks continue. | +| `openclaw telemetry off` | Stops anonymous feature statistics. Update checks and default-on outcomes continue. | | `DO_NOT_TRACK=1` | Same, enforced from the environment. | -| `update.checkOnStart: false` | Stops both tiers of automatic update requests. Explicit update commands and other configured services are separate. | +| `update.checkOnStart: false` | Stops automatic update requests and outcome reporting. Explicit update commands and other configured services are separate. | -`OPENCLAW_NO_AUTO_UPDATE=1` also prevents automatic update requests. A truthy `CI` suppresses both -tiers unless a replacement `OPENCLAW_TELEMETRY_ENDPOINT` is explicitly configured. +`OPENCLAW_NO_AUTO_UPDATE=1` also prevents automatic update requests. A truthy `CI` suppresses +daily checks and schema-1 feature reports unless a replacement `OPENCLAW_TELEMETRY_ENDPOINT` +is explicitly configured. Outcome reports remain suppressed in CI even with a replacement endpoint. Disabling requests stops future automatic reports; it does not erase previously recorded rows. The same three-month Analytics Engine retention applies. This receiver adds no backup or export job. diff --git a/docs/update-results.md b/docs/update-results.md new file mode 100644 index 0000000..55d0587 --- /dev/null +++ b/docs/update-results.md @@ -0,0 +1,173 @@ +# Identifier-free update outcomes (schema 2) + +This receiver accepts a terminal update outcome on the existing +`POST /api/latest-version` endpoint. It is separate from daily update checks and +schema-1 feature reports. This change does not enable a client, deploy the Worker, +provision a production dataset, or authorize production collection. The companion +client reports outcomes **on by default**, like the existing update ping, governed +by update-request policy rather than optional schema-1 feature statistics. +`update.checkOnStart: false`, `OPENCLAW_NO_AUTO_UPDATE=1`, and Nix mode suppress +outcome reports. A truthy `CI` always suppresses them, including when a replacement +`OPENCLAW_TELEMETRY_ENDPOINT` is explicitly configured. The daily-check custom-endpoint +exception does not apply to outcomes. `DO_NOT_TRACK=1`, +`openclaw telemetry off`, and `telemetry.enabled` control feature statistics, not +default-on update outcomes; feature statistics remain off by default. + +The receiver and separate outcome dataset must be deployed and verified **before +releasing the default-on client**. That rollout needs separate authorization; +local tests do not establish production readiness. The default production +template deliberately **omits `UPDATE_RESULTS`**: the existing push-to-main +workflow may deploy receiver code, but must not implicitly activate collection. +A separately authorized rollout must explicitly add the `UPDATE_RESULTS` +binding for `openclaw_update_results`, preserving the existing `TELEMETRY` +binding, and verify retention and delivery before enabling production collection. +No deployment or provisioning is authorized by this PR. + +## Capability handshake + +An outcome attempt uses **two requests**, first `HEAD`, then (only when supported) +`POST`, to the same full configured `/api/latest-version` URL, including its +query. There is no new endpoint setting or fallback. Both requests use the fixed +`openclaw-update-result/1` User-Agent. Only exact status **204** with header +`OpenClaw-Update-Results: 2` permits the client to POST. Header names are +case-insensitive. Older receivers return 405; a receiver without the outcome +binding returns **503 without the capability header**. Neither gets outcome data. + +HEAD has an empty body and `Cache-Control: no-store` for both 204 and 503. It +checks binding presence synchronously; it never reads an upload or geography, +uses the recording limiter, writes analytics (including sample points), or looks +up a version. It is a protocol capability check, **not production readiness or +successful storage proof**. The client shares a three-second timeout across both +requests, disallows redirects, and rechecks update-request opt-outs after the +HEAD await before sending. No retries or delayed queue are added. Daily GET and +opt-in schema-1 feature POST remain single requests and need no handshake. + +## Wire contract + +Send JSON with fixed User-Agent `openclaw-update-result/1` (not stored). All 18 +fields in `test/fixtures/update-result.json` are mandatory; additional keys are +rejected, not ignored. The only numeric field is `schema: 2`; `event` is exactly +`update_result`. Other values must exactly match these case-sensitive labels: + +| Field | Accepted values | +| --- | --- | +| outcome | succeeded, failed, rolled-back | +| fromVersion, targetVersion, resultingVersion, runningVersion | public release syntax below, or unknown | +| platform | linux, darwin, win32, freebsd, openbsd, unknown | +| arch | x64, arm64, arm, ia32, unknown | +| installMethod | git-checkout, npm-global, pnpm-global, bun-global, managed-service, unknown | +| channel | stable, beta, dev, extended-stable, unknown | +| duration | under-10s, under-1m, under-5m, under-30m, over-30m, unknown | +| postCheck | passed, failed, unknown | +| failedStage | requested, staging, validating, repairing, activating, restarting, verifying, unknown, none | +| errorCategory | permission, network, timeout, storage, other, none | +| errorCode | EACCES, EPERM, ENOSPC, ETIMEDOUT, ECONNRESET, ECONNREFUSED, ENOTFOUND, unknown, none | +| rollback | not-needed, not-attempted, succeeded, failed, unknown | +| recovery | safe, unsafe, unknown | + +Public version syntax is +`/^202[0-9]\.(?:[1-9]|1[0-2])\.(?:0|[1-9][0-9]{0,5})(?:-[1-9][0-9]{0,2})?(?:-beta\.[1-9][0-9]{0,2})?$/`, +matching the entire string (including rejecting trailing line terminators). +The patch component accepts zero or a non-zero-leading integer up to six digits +(0–999999), including extended-stable versions such as `2026.8.33` and +`2026.8.123`. Year, month, revision and beta-suffix restrictions are unchanged. +It excludes build metadata, commit SHAs and private prerelease labels. This is +syntax validation, not a claim that a label was actually published. `succeeded` +requires `failedStage`, `errorCategory` and `errorCode` all to be `none`. + +Uploads must be valid UTF-8 and at most **4096 bytes**, including whitespace and +any BOM. The fixed UA selects the 4096-byte streaming cap before JSON decoding, +so malformed, absent or oversized outcome bodies never reach the legacy recorder. +A parsed `schema: 2` or `event: "update_result"` also selects strict validation +without that UA, using the exact byte count from the legacy bounded reader. +Without the fixed UA, undecodable bodies cannot be classified as outcomes and +retain legacy invalid-feature behavior; clients must always send the fixed UA. + +Invalid outcomes return `400 {"error":"invalid_update_result"}` with no recording. +A missing outcome binding or synchronous analytics write failure returns +`503 {"error":"update_results_unavailable"}`; there is **no fallback** to the daily +dataset. Neither response echoes input or diagnostics. Accepted requests receive +the existing version response (or its existing `503 version_unavailable`). A +version failure can occur after recording; clients must not infer exactly-once +storage or retry to recover an acknowledgement. The existing per-IP recording +limiter runs exactly once before reading a GET/POST upload: exhausted callers +receive their version answer without reading or validating the body, even if +it never finishes, and without recording. Thus invalid/unavailable outcome +responses above apply only while recording quota is available. IP is only a transient limiter key, never an analytics column. + +## Storage and privacy + +Binding `UPDATE_RESULTS` writes exclusively to `openclaw_update_results`. The daily +`TELEMETRY` binding and `openclaw_telemetry` columns remain unchanged. Outcome +processing does not read `request.cf`, parse legacy identity, load the feature +vocabulary, or write any daily row. No IDs, raw User-Agent, geography, hostname, +path, command, arbitrary error text, logs or free-form strings are stored. +Worker observability and invocation logs stay disabled. Cloudflare still +processes connection metadata independently of these Worker storage rules. + +The positional contract in `src/update-result.ts` is: + +| Column | Value | +| --- | --- | +| index1 | targetVersion (sampling key, not an identifier) | +| blob1 | event | +| blob2 | outcome | +| blob3 | fromVersion | +| blob4 | targetVersion | +| blob5 | resultingVersion | +| blob6 | runningVersion | +| blob7 | platform | +| blob8 | arch | +| blob9 | installMethod | +| blob10 | channel | +| blob11 | duration | +| blob12 | postCheck | +| blob13 | failedStage | +| blob14 | errorCategory | +| blob15 | errorCode | +| blob16 | rollback | +| blob17 | recovery | +| double1 | schema (2) | + +Analytics Engine adds its own receipt timestamp and sampling weight. Its published +retention is **three months**; this change adds no archive, backup or export job. +Seventeen blobs, one double and one bounded version index fit the published +limits. Operators must confirm retention and the separate binding before a +separately authorized rollout. No production binding was provisioned or verified +by local tests. References: Cloudflare Analytics Engine +[limits](https://developers.cloudflare.com/analytics/analytics-engine/limits/) and +[SQL API](https://developers.cloudflare.com/analytics/analytics-engine/sql-api/), +checked September 19, 2026. + +## Aggregate-only analysis + +No public statistics or individual report route is added. Authorized operators +can use the private Analytics Engine SQL API for bounded aggregates, for example: + +```sql +SELECT blob4 AS target_version, blob2 AS outcome, + SUM(_sample_interval) AS reports +FROM openclaw_update_results +WHERE timestamp > NOW() - INTERVAL '7' DAY AND double1 = 2 +GROUP BY blob4, blob2 +ORDER BY reports DESC +``` + +```sql +SELECT blob13 AS failed_stage, blob14 AS error_category, + SUM(_sample_interval) AS reports +FROM openclaw_update_results +WHERE timestamp > NOW() - INTERVAL '7' DAY + AND double1 = 2 AND blob2 != 'succeeded' +GROUP BY blob13, blob14 +ORDER BY reports DESC +``` + +These are report counts, not unique installs, people or attempts. There are no +identifiers for deduplication or longitudinal joins. Missing reports, update-policy +opt-outs, Nix/CI suppression, NAT rate limits, unauthenticated spoofing and sampling +bias the counts. +Do not treat them as fleet-wide success rates, billing or security evidence. +Avoid individual-row exports or joins to daily geography; review any aggregate +publication separately for small groups. These SQL examples were not run against +production data. diff --git a/package-lock.json b/package-lock.json index ba1157c..ce237f3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,10 +9,10 @@ "version": "1.0.0", "license": "MIT", "devDependencies": { - "@cloudflare/workers-types": "^5.20260923.1", + "@cloudflare/workers-types": "^5.20260930.1", "typescript": "^7.0.2", - "vitest": "^5.0.1", - "wrangler": "^4.136.3" + "vitest": "^5.0.2", + "wrangler": "^4.144.0" } }, "node_modules/@cloudflare/kv-asset-handler": { @@ -42,9 +42,9 @@ } }, "node_modules/@cloudflare/workerd-darwin-64": { - "version": "1.20260921.1", - "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-64/-/workerd-darwin-64-1.20260921.1.tgz", - "integrity": "sha512-3iB2WnYOlZ29T+1zhCwbHFExCBp6E9bgmDUMryATYwrIGEQ1YbvR78m4ydm56XKN/d/yF3803ivMGfZMYDtiMg==", + "version": "1.20260926.1", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-64/-/workerd-darwin-64-1.20260926.1.tgz", + "integrity": "sha512-VCIpwgJu5Dm1o+VHLclNOKIdpttEqss7oDHS5DYxgVoonM9Dxxxr1xCQvJbh6IXFuQ1DDrbm5krjte5NsTgX3Q==", "cpu": [ "x64" ], @@ -59,9 +59,9 @@ } }, "node_modules/@cloudflare/workerd-darwin-arm64": { - "version": "1.20260921.1", - "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-arm64/-/workerd-darwin-arm64-1.20260921.1.tgz", - "integrity": "sha512-FpqVR7IQXVBmGtajyonEmhmb5UAsmV7dTaIkpemmHZXHEw7uYpkhkzKPjc4BOPhNQy8iwt2p+RZBPMY3Y7/bvQ==", + "version": "1.20260926.1", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-arm64/-/workerd-darwin-arm64-1.20260926.1.tgz", + "integrity": "sha512-oVuLXfCnr1Xu9sBMNYrEPcFZQNMcviNcacz9l+oqQiLHc5UrVp++lpKbsuWstCfh+/c39Pp37TVeaNBLA8+ReA==", "cpu": [ "arm64" ], @@ -76,9 +76,9 @@ } }, "node_modules/@cloudflare/workerd-linux-64": { - "version": "1.20260921.1", - "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-64/-/workerd-linux-64-1.20260921.1.tgz", - "integrity": "sha512-riAJIohaVp5A8Sqy4yKlzHOaLPOICMf5oey+jC2rm45RVT+wK8+7UU0d31Dy/02Nc8YUkobAFwNVjX06P8WQ5g==", + "version": "1.20260926.1", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-64/-/workerd-linux-64-1.20260926.1.tgz", + "integrity": "sha512-BjmiDvYBVBG5248gCHSZS3yONbcm3/3qKTu9RaQA8o7jQHJfXx9JnWbhkld2KsbXnRGEGHFwpLLidIpIGtQYUA==", "cpu": [ "x64" ], @@ -93,9 +93,9 @@ } }, "node_modules/@cloudflare/workerd-linux-arm64": { - "version": "1.20260921.1", - "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-arm64/-/workerd-linux-arm64-1.20260921.1.tgz", - "integrity": "sha512-tnJu08tT7s0XWDqp3O0H/vCp0voy9OqVAzspb89biMo1dh8IiEpnyXnoPmdJ7H4qBnXCmXgy0kuEphuvpDPj9w==", + "version": "1.20260926.1", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-arm64/-/workerd-linux-arm64-1.20260926.1.tgz", + "integrity": "sha512-28bdfQDIFcpaKXMPisADy8BchpIsX+tEAhD+bhfEBXukFGFr8F3ouyiG3HzN3CqAh/OYvifJizzFZtptUPCy7w==", "cpu": [ "arm64" ], @@ -110,9 +110,9 @@ } }, "node_modules/@cloudflare/workerd-windows-64": { - "version": "1.20260921.1", - "resolved": "https://registry.npmjs.org/@cloudflare/workerd-windows-64/-/workerd-windows-64-1.20260921.1.tgz", - "integrity": "sha512-VgNcRPstoZMb1G94JTrx+jU24GtkkazNfox0gnF/2fkuXpcfW/M0e0xvdMovYfwt8ZxG5AB2ZNvanD6ufBwiuQ==", + "version": "1.20260926.1", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-windows-64/-/workerd-windows-64-1.20260926.1.tgz", + "integrity": "sha512-ehdL3ataKdEmlnW9oom6OZUR8n3XsTL/zl8+KGREpLi5sMSW0mek0PrF4wK9SX51f3Alnlu8tF8eKBg+mc3xFw==", "cpu": [ "x64" ], @@ -127,9 +127,9 @@ } }, "node_modules/@cloudflare/workers-types": { - "version": "5.20260923.1", - "resolved": "https://registry.npmjs.org/@cloudflare/workers-types/-/workers-types-5.20260923.1.tgz", - "integrity": "sha512-zuOqyxfhzhN2LlxxcpnAS79aGVm9ku3ZcWKyvVLHyjPV6oHsuHSFidzSHhHE9ho9UTcUxU1IdtpdVuFXluKB+g==", + "version": "5.20260930.1", + "resolved": "https://registry.npmjs.org/@cloudflare/workers-types/-/workers-types-5.20260930.1.tgz", + "integrity": "sha512-ZUHwK/kG4GD6owwHY+Le+QYq/LO+R0f8b51cE3+IXe4bcKefKWIVYYqsUrKwSTo9su1LSIxKEfqui4r7LrxU1Q==", "dev": true, "license": "MIT OR Apache-2.0" }, @@ -1214,9 +1214,9 @@ } }, "node_modules/@oxc-project/types": { - "version": "0.150.0", - "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.150.0.tgz", - "integrity": "sha512-rDS5/31E9HfPl/CIzGrn0DOlvBbXFseQ5URJ9sYMfstbKLD/c6Gm9vmRzRGDdAXyOIL4zmO37lc9RIwYqVruZw==", + "version": "0.151.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.151.0.tgz", + "integrity": "sha512-J1yXrIlNDZVzE3ada310xeAw7nH8yCAyLPuUIsjKatFPmfn5bS1oW+cM+QsGOtVWd5nhSpbwZWx/rue+r5Z+PA==", "dev": true, "license": "MIT", "peer": true, @@ -1254,9 +1254,9 @@ "license": "MIT" }, "node_modules/@rolldown/binding-android-arm-eabi": { - "version": "1.2.9", - "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm-eabi/-/binding-android-arm-eabi-1.2.9.tgz", - "integrity": "sha512-tNISae1QEf/vkb3xkRcjV5SEdzPE97We5IVaa2Z8jSszQPZ8U60B/YCYpw4QI7VidYsBtKavczXf+DyDs9WGxw==", + "version": "1.2.11", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm-eabi/-/binding-android-arm-eabi-1.2.11.tgz", + "integrity": "sha512-A5kXfGKvKWWZE0TtPrfsvT+q4Y5d1QG8gGUzpYjGydM+fARM9MuX90PrXYXe0XbsDVgyxxNzHo6giCj90bsFNw==", "cpu": [ "arm" ], @@ -1272,9 +1272,9 @@ } }, "node_modules/@rolldown/binding-android-arm64": { - "version": "1.2.9", - "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.9.tgz", - "integrity": "sha512-YC8YsI30o606GTZi0VyzYlsDKFP8W61i/QzayHDkLbNEz/IShqAmTa+hsJRj13xTHA0H+6fk4b2UmGn+Q/cMlg==", + "version": "1.2.11", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.11.tgz", + "integrity": "sha512-z6cTycz+iJ4PVkuL4HHW4DfTfoeU/2nqYYuSOrTmH7yHK5Y0LCOnA03V4ZNxavyVaU1oOqUgIg2klN/s+USGOA==", "cpu": [ "arm64" ], @@ -1290,9 +1290,9 @@ } }, "node_modules/@rolldown/binding-darwin-arm64": { - "version": "1.2.9", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.9.tgz", - "integrity": "sha512-IwhlH3qK5urrY8hZiEgGkHKEFN901p/p2bjxCxJlr4GyNnF7wYpUvK+Y43uaRYuC4hpfjzbR3SJC3arX1jGvmw==", + "version": "1.2.11", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.11.tgz", + "integrity": "sha512-jShvqNtP6vDC6/A5JOAzbVV+DkgHqhl/ScVCJEbt+TUY6QYz7YnXcrg3sLtFBniro0f/Ld50ZwCWA6f7KYD1nQ==", "cpu": [ "arm64" ], @@ -1308,9 +1308,9 @@ } }, "node_modules/@rolldown/binding-darwin-x64": { - "version": "1.2.9", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.9.tgz", - "integrity": "sha512-XxpJfVzFh+jilRxIXUqcfYAYcunIc/XEzIizsOL1fcJee5Sf7H3mH8WlLmfHfluz5amqR88QQo9izKtmMlavAw==", + "version": "1.2.11", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.11.tgz", + "integrity": "sha512-f2i2xiNWq1Z1l2++q2fuhZRdLAT3aqxD6vRNm1RAxpUoBcdqNB3C0s1Bt+K+PbEx2F5F4gQp6hqKkphCY/xF9w==", "cpu": [ "x64" ], @@ -1326,9 +1326,9 @@ } }, "node_modules/@rolldown/binding-freebsd-x64": { - "version": "1.2.9", - "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.9.tgz", - "integrity": "sha512-kSfvhmgeWyfkbT3p/1s5vSgboogoah2zkm9fX2zjg2hHxSV7T4KhMWRUUaRk4OXNqoD3QAUeRqLcs1aZOK4U1g==", + "version": "1.2.11", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.11.tgz", + "integrity": "sha512-4Ir5FSOKIAMr4r0kExpt1s3bMgzJU3rA45AYOHtQpls0oNeqcYBKrWMlckrYH4KCfGLfkfn1tN1dmZPMVsdXow==", "cpu": [ "x64" ], @@ -1344,9 +1344,9 @@ } }, "node_modules/@rolldown/binding-linux-arm-gnueabihf": { - "version": "1.2.9", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.9.tgz", - "integrity": "sha512-1RVzG17pxqbTfYLC352JlLt6kKLG+6Hr30n8DlIJqsnV5luUDd2Qdx9Ayw1Cabfyb1K9k0jXEZ7evxkRoT+uiw==", + "version": "1.2.11", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.11.tgz", + "integrity": "sha512-/gnRDM+39BROzAN/k1OZjDPnDMcZxB/0EUxKjONO5yVkNEvlsoMDrxGNKgZi/ttFriS2gwlDNzB65pvNbFOXIQ==", "cpu": [ "arm" ], @@ -1362,9 +1362,9 @@ } }, "node_modules/@rolldown/binding-linux-arm64-gnu": { - "version": "1.2.9", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.9.tgz", - "integrity": "sha512-BXqPvZ2drqVD+/Z8UpKwcs4Mp7grM+eGFku4CAEKrEtcbAsUpzREphK1sogCRZGreVPiMkiiBtw0n3TPteuqvw==", + "version": "1.2.11", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.11.tgz", + "integrity": "sha512-PFaK8HwvAHbaKbBcDNQihjMKYvFnA5hiENx/l5tphTDz1E0WFp32l0A7aq7lyUwGsRw/xSrNIy/gIK4thrSCrw==", "cpu": [ "arm64" ], @@ -1383,9 +1383,9 @@ } }, "node_modules/@rolldown/binding-linux-arm64-musl": { - "version": "1.2.9", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.9.tgz", - "integrity": "sha512-11vWvo8YDwLzukt27J3aYDWU+gg2P7J+ZOmiJ0hkF5BXZDW7pVya7r40MXDy6ya0i9KamoENSVKIugvJNgFXIA==", + "version": "1.2.11", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.11.tgz", + "integrity": "sha512-AskzJUIKRLPxkruR1wLKewGbOw+EYfU/9lOrBFj4AFrEA8hPpKFnODWNu2WLaNs0QNkEb9QIJufmVZZIL/bJlg==", "cpu": [ "arm64" ], @@ -1404,9 +1404,9 @@ } }, "node_modules/@rolldown/binding-linux-ppc64-gnu": { - "version": "1.2.9", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.9.tgz", - "integrity": "sha512-a1tijMkdwsIARtc0F39ApURROkf3NwqinI6TOiSSWCTR7dT96dffNvMUtDHnq64wKNTIZOIlzKrFvvFUznJiyw==", + "version": "1.2.11", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.11.tgz", + "integrity": "sha512-qlUGAheh2yh8afH7QBgx0PrRHN85hKnNd78x8MeMhXivuevgd8vgf6/CstOzmNKY/lLTHvNTrPy98cLnAugzJw==", "cpu": [ "ppc64" ], @@ -1425,9 +1425,9 @@ } }, "node_modules/@rolldown/binding-linux-s390x-gnu": { - "version": "1.2.9", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.9.tgz", - "integrity": "sha512-x6SQNdAvv4c3hWqTMaWuawzMX9myaCs/yEmlGsxJzkdClnHW7FbrjQuSiRDhuSYzEYoEMhsaJy9qHG/XNemJPQ==", + "version": "1.2.11", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.11.tgz", + "integrity": "sha512-secpEad+0vCbSfn8upFySkDskv+bGPk3THSDS9Y89yc4rb4kzqHp8Dmyd9BkQW4SnhNXBZCl/6CrO//hZahNJQ==", "cpu": [ "s390x" ], @@ -1446,9 +1446,9 @@ } }, "node_modules/@rolldown/binding-linux-x64-gnu": { - "version": "1.2.9", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.9.tgz", - "integrity": "sha512-9s0AZ8BFK5/n7B/TBoa2yJE3gI3KURrbXcPBlsAsvjU4VeJKgE90y1YtNxyEUIcHPQkg6/yfF3qihUrcM/Kf0Q==", + "version": "1.2.11", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.11.tgz", + "integrity": "sha512-mOVBT3dPpkWm8XBWPmU4bf+U6dYDLeMo/9ojUmis4N0L5uu10qra5vOyngZ7/PSdoE4G9KvRt4bloRxNjLas7A==", "cpu": [ "x64" ], @@ -1467,9 +1467,9 @@ } }, "node_modules/@rolldown/binding-linux-x64-musl": { - "version": "1.2.9", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.9.tgz", - "integrity": "sha512-P7VWAmV+WdJluH7ovnRGoiv2i8To7GAZ+kGzfGup635cyL7SyYl3lSUaA3Gp5THf0n/Co5EyEqb2zbqq+nMOHQ==", + "version": "1.2.11", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.11.tgz", + "integrity": "sha512-Is78i9A8Ui4SqcxUwFJ9uMmjDn58IbVTjFWYdQestFEgeuEmHMLGNriXnVJKkwG2YiZjw8cP0zCTyDMdDGtOOg==", "cpu": [ "x64" ], @@ -1488,9 +1488,9 @@ } }, "node_modules/@rolldown/binding-openharmony-arm64": { - "version": "1.2.9", - "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.9.tgz", - "integrity": "sha512-1qixtsE4BK8h+yS3BfmZ09UhA7O/N4IACva6YBr7EBvCJraByTuRcgOTaiA62Tm0vey3UcKXLOaoGHtYmNGEVg==", + "version": "1.2.11", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.11.tgz", + "integrity": "sha512-dUCXneZ87INUMyQ0D+C0HrEBNUPNXHaPmU5GTjyKTJEiussw9Kaj5Ln8UztPe4epV/ffvgNBEadksdYhmW6xJA==", "cpu": [ "arm64" ], @@ -1506,9 +1506,9 @@ } }, "node_modules/@rolldown/binding-win32-arm64-msvc": { - "version": "1.2.9", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.9.tgz", - "integrity": "sha512-ok8IQjcEPs1AKZfuEUznVBrJw+gK4soq+bx8b1X2XoMqVClarc1q5JDmVtWXY1xfr6ZuHTAsPXHTgTrqKTZeww==", + "version": "1.2.11", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.11.tgz", + "integrity": "sha512-jByxb6qfd+bH1xUd0qnfFnb17i9sWBPY2tOavJ0l3tdr3OTu+Kvtm8cd/JV5nFt657b1VqGltxg9olOEfofXWw==", "cpu": [ "arm64" ], @@ -1524,9 +1524,9 @@ } }, "node_modules/@rolldown/binding-win32-x64-msvc": { - "version": "1.2.9", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.9.tgz", - "integrity": "sha512-Ip2mXoU0hM0boq3Rf+ekuT653OROSo6aSYcPT1VHE4q52KvyxgFkQgrgb/IEsxOuvQ2fZZbs8khJAyCEPM24/g==", + "version": "1.2.11", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.11.tgz", + "integrity": "sha512-/PzKqzAJ03i19oy2ItPvyvaVjOjBCNnfaJs8yvUdGBKmiESgnrJSQ2awd81QzFbbnAmu7YO9ZnJrDCb9VSJPRA==", "cpu": [ "x64" ], @@ -1935,14 +1935,14 @@ } }, "node_modules/@vitest/mocker": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-5.0.1.tgz", - "integrity": "sha512-6K1DoBNAPGvuOcSsGA4D6x+5zEEff/KmOOP3uetT2TrGpVfI+HRHRnJJfKi5ib/g1vx8IYHQD8s0pbJz8WQI7Q==", + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-5.0.2.tgz", + "integrity": "sha512-Z5FS00Q1SJHkB35xATsmWGdQ5WA1/0MV3CDjqyv7GavHv1OfOj145MNfHOlHk7QLes21dKFDHr8EO2zvL+9WGA==", "dev": true, "license": "MIT", "dependencies": { "@jridgewell/trace-mapping": "0.3.31", - "@vitest/spy": "5.0.1", + "@vitest/spy": "5.0.2", "estree-walker": "^3.0.3", "magic-string": "^1.2.3" }, @@ -1963,9 +1963,9 @@ } }, "node_modules/@vitest/spy": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-5.0.1.tgz", - "integrity": "sha512-rbto/mF/SGERxEgYOek7Xm6B9b+y+mVoo+f4b2LymYO8zM1b7uB5nHuhVMTP2hxdzgxvGiZYGxGIaMvL5y180Q==", + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-5.0.2.tgz", + "integrity": "sha512-Ijc7T1nT9efNb5LxvjaBrEqw3f/QwUv5EE0nKqZxgqsaV/FxAAZ8baGylA8X/Z2oS4Lp+K74Jr6dTJsDKxJDeg==", "dev": true, "license": "MIT", "funding": { @@ -2431,9 +2431,9 @@ } }, "node_modules/magic-string": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-1.4.1.tgz", - "integrity": "sha512-8lyCu36ErXR0J9uaGKlKQoiLZKmtI63YGLE8G2o9jyRPdr4X47LusSOwgOJOzcVtp81fTAAjxR7BwKz682Jhow==", + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-1.4.2.tgz", + "integrity": "sha512-vG+rjFRj1PqdIBozIxAGMjPlOhaVe+GXpbttY/iSK7rGcJRMlwNJO7dcUwmUqkymsFLJiNGI06t4D7Fr7yRC9g==", "dev": true, "license": "MIT", "dependencies": { @@ -2441,16 +2441,16 @@ } }, "node_modules/miniflare": { - "version": "5.20260921.0-alpha", - "resolved": "https://registry.npmjs.org/miniflare/-/miniflare-5.20260921.0-alpha.tgz", - "integrity": "sha512-vHH/unOYvV2jA1Q9SdkmzrQhhMoksdwg5jegu6ZeKaaRzgxZhVbt1NdTpQjHF2VTgiBjgP8SiUlUMfruB3N3SQ==", + "version": "5.20260926.1-alpha", + "resolved": "https://registry.npmjs.org/miniflare/-/miniflare-5.20260926.1-alpha.tgz", + "integrity": "sha512-vc4lgl8YVxR2aundPnVPTIcL46UQKK+ebW7yinXNE1Y9joFS0JoakMazr7r3M8DXRvaemKS8EEmszkv/b05qUw==", "dev": true, "license": "MIT", "dependencies": { "@cspotcode/source-map-support": "0.8.1", "sharp": "0.35.4", - "undici": "7.29.0", - "workerd": "1.20260921.1", + "undici": "7.29.1", + "workerd": "1.20260926.1", "ws": "8.21.0", "youch": "4.1.0-beta.10" }, @@ -2558,14 +2558,14 @@ } }, "node_modules/rolldown": { - "version": "1.2.9", - "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.9.tgz", - "integrity": "sha512-hx/Pv0N1haXRb11qkfnK5MXB/iqr7i0yjWQqmO9uHqZpBgQSqzc8UsSnEpalsh+j1I8qQ2CkXAkJC8Br3dKSlg==", + "version": "1.2.11", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.11.tgz", + "integrity": "sha512-qpSwIyz0jHQq5qXBTNxFmE6664rJ7O+4TvPFOiOaBSrz8IOHc1koKKSqTM2H6u1UG1+TveuC6vaDHKXFOvb1Kw==", "dev": true, "license": "MIT", "peer": true, "dependencies": { - "@oxc-project/types": "=0.150.0", + "@oxc-project/types": "=0.151.0", "@rolldown/pluginutils": "^1.0.0" }, "bin": { @@ -2575,21 +2575,21 @@ "node": "^20.19.0 || >=22.12.0" }, "optionalDependencies": { - "@rolldown/binding-android-arm-eabi": "1.2.9", - "@rolldown/binding-android-arm64": "1.2.9", - "@rolldown/binding-darwin-arm64": "1.2.9", - "@rolldown/binding-darwin-x64": "1.2.9", - "@rolldown/binding-freebsd-x64": "1.2.9", - "@rolldown/binding-linux-arm-gnueabihf": "1.2.9", - "@rolldown/binding-linux-arm64-gnu": "1.2.9", - "@rolldown/binding-linux-arm64-musl": "1.2.9", - "@rolldown/binding-linux-ppc64-gnu": "1.2.9", - "@rolldown/binding-linux-s390x-gnu": "1.2.9", - "@rolldown/binding-linux-x64-gnu": "1.2.9", - "@rolldown/binding-linux-x64-musl": "1.2.9", - "@rolldown/binding-openharmony-arm64": "1.2.9", - "@rolldown/binding-win32-arm64-msvc": "1.2.9", - "@rolldown/binding-win32-x64-msvc": "1.2.9" + "@rolldown/binding-android-arm-eabi": "1.2.11", + "@rolldown/binding-android-arm64": "1.2.11", + "@rolldown/binding-darwin-arm64": "1.2.11", + "@rolldown/binding-darwin-x64": "1.2.11", + "@rolldown/binding-freebsd-x64": "1.2.11", + "@rolldown/binding-linux-arm-gnueabihf": "1.2.11", + "@rolldown/binding-linux-arm64-gnu": "1.2.11", + "@rolldown/binding-linux-arm64-musl": "1.2.11", + "@rolldown/binding-linux-ppc64-gnu": "1.2.11", + "@rolldown/binding-linux-s390x-gnu": "1.2.11", + "@rolldown/binding-linux-x64-gnu": "1.2.11", + "@rolldown/binding-linux-x64-musl": "1.2.11", + "@rolldown/binding-openharmony-arm64": "1.2.11", + "@rolldown/binding-win32-arm64-msvc": "1.2.11", + "@rolldown/binding-win32-x64-msvc": "1.2.11" } }, "node_modules/semver": { @@ -2655,13 +2655,6 @@ } } }, - "node_modules/siginfo": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", - "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", - "dev": true, - "license": "ISC" - }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", @@ -2673,13 +2666,6 @@ "node": ">=0.10.0" } }, - "node_modules/stackback": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", - "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", - "dev": true, - "license": "MIT" - }, "node_modules/std-env": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.2.0.tgz", @@ -2781,9 +2767,9 @@ } }, "node_modules/undici": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/undici/-/undici-7.29.0.tgz", - "integrity": "sha512-IDxfleLmmbSskfWSUATiN1nfn2rDuvnMOqb5CWR92iIfojA0Ud+ulOAAEQ57LPr9rWmsreUyf5lwyao+7GNNVw==", + "version": "7.29.1", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.29.1.tgz", + "integrity": "sha512-RYONW2MeafgYlkVOKYKkA/Ag7BmXqgIWCa8t1m0JcxrQg9pI9lEqRhAOruOBCbAohOa/gkCF+iPi9hrgvTzu6Q==", "dev": true, "license": "MIT", "engines": { @@ -2801,9 +2787,9 @@ } }, "node_modules/vite": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/vite/-/vite-8.3.0.tgz", - "integrity": "sha512-lhZBVvEHefgE+HQZC9O7EBJgCU/nVzFNl7vkS4RE0APtWLP02/8QVIkQtzBxPquh7lq5/78NHipTj7ODQ6XuyQ==", + "version": "8.3.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.3.1.tgz", + "integrity": "sha512-/bvH9E9tmCXRGp2uXY3WbOldqpTwFkbha/8ANaEQ6VkxhH60KyqLwgZq6lG2y+4uT55x9+9eUHMpQ7uGnOCKjA==", "dev": true, "license": "MIT", "peer": true, @@ -2811,7 +2797,7 @@ "lightningcss": "^1.33.0", "picomatch": "^4.0.7", "postcss": "^8.5.28", - "rolldown": "~1.2.6", + "rolldown": "~1.2.9", "tinyglobby": "^0.2.17" }, "bin": { @@ -2880,14 +2866,14 @@ } }, "node_modules/vitest": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-5.0.1.tgz", - "integrity": "sha512-iA95lQbKEkvrtTkdAgnWbXfbipWiiWe/hDl2P5tMi6WFwD76G0NxXAGp/M9EOcYupeGJRr6wppMc7CoA41TQjg==", + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-5.0.2.tgz", + "integrity": "sha512-7MQrx9pDv5aHiUcovIb/70Ys3tgtkUVgCtledvKdCmEO+/1Dicq5ZqoSxOW034m03oqC+oHOKui2dM6qtMLoJg==", "dev": true, "license": "MIT", "dependencies": { "@types/chai": "^5.2.2", - "@vitest/mocker": "5.0.1", + "@vitest/mocker": "5.0.2", "chai": "^6.2.2", "es-module-lexer": "^2.3.2", "expect-type": "^1.4.0", @@ -2895,10 +2881,10 @@ "obug": "^2.1.4", "picomatch": "^4.0.7", "std-env": "^4.2.0", - "tinybench": "6.1.4", - "tinyexec": "1.3.0", + "tinybench": "^6.1.4", + "tinyexec": "^1.3.0", "tinyglobby": "^0.2.17", - "why-is-node-running": "^2.3.0" + "why-is-node-running": "^3.2.1" }, "bin": { "vitest": "vitest.mjs" @@ -2913,12 +2899,12 @@ "@edge-runtime/vm": "*", "@opentelemetry/api": "^1.9.0", "@types/node": "^22.0.0 || >=24.0.0", - "@vitest/browser-playwright": "5.0.1", - "@vitest/browser-preview": "5.0.1", + "@vitest/browser-playwright": "5.0.2", + "@vitest/browser-preview": "5.0.2", "@vitest/browser-webdriverio": "^5.0.0-beta.5 || >=5.0.0", - "@vitest/coverage-istanbul": "5.0.1", - "@vitest/coverage-v8": "5.0.1", - "@vitest/ui": "5.0.1", + "@vitest/coverage-istanbul": "5.0.2", + "@vitest/coverage-v8": "5.0.2", + "@vitest/ui": "5.0.2", "happy-dom": "*", "jsdom": "*", "vite": "^6.4.0 || ^7.0.0 || ^8.0.0" @@ -2963,26 +2949,22 @@ } }, "node_modules/why-is-node-running": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", - "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-3.2.2.tgz", + "integrity": "sha512-NKUzAelcoCXhXL4dJzKIwXeR8iEVqsA0Lq6Vnd0UXvgaKbzVo4ZTHROF2Jidrv+SgxOQ03fMinnNhzZATxOD3A==", "dev": true, "license": "MIT", - "dependencies": { - "siginfo": "^2.0.0", - "stackback": "0.0.2" - }, "bin": { "why-is-node-running": "cli.js" }, "engines": { - "node": ">=8" + "node": ">=20.11" } }, "node_modules/workerd": { - "version": "1.20260921.1", - "resolved": "https://registry.npmjs.org/workerd/-/workerd-1.20260921.1.tgz", - "integrity": "sha512-4HyG7G1W4ksa6tUZ8bV2jxDRWuL5PXnHm9+Z1sjFPb9OZNoYtXz4y7QQRh4ibi0BF/lOmlAVjbhkUqsAVZuUKA==", + "version": "1.20260926.1", + "resolved": "https://registry.npmjs.org/workerd/-/workerd-1.20260926.1.tgz", + "integrity": "sha512-YojhsWBuZwk3Jk9OC0QfTgVbLeQOD1tK1olC9ZXAUcmeip8z4NE/xtttpxCsuTvQBRqppvy7Tkac0kxPYMjIXA==", "dev": true, "hasInstallScript": true, "license": "Apache-2.0", @@ -2993,17 +2975,17 @@ "node": ">=16" }, "optionalDependencies": { - "@cloudflare/workerd-darwin-64": "1.20260921.1", - "@cloudflare/workerd-darwin-arm64": "1.20260921.1", - "@cloudflare/workerd-linux-64": "1.20260921.1", - "@cloudflare/workerd-linux-arm64": "1.20260921.1", - "@cloudflare/workerd-windows-64": "1.20260921.1" + "@cloudflare/workerd-darwin-64": "1.20260926.1", + "@cloudflare/workerd-darwin-arm64": "1.20260926.1", + "@cloudflare/workerd-linux-64": "1.20260926.1", + "@cloudflare/workerd-linux-arm64": "1.20260926.1", + "@cloudflare/workerd-windows-64": "1.20260926.1" } }, "node_modules/wrangler": { - "version": "4.136.3", - "resolved": "https://registry.npmjs.org/wrangler/-/wrangler-4.136.3.tgz", - "integrity": "sha512-L1bS8BI9xoEk3RRr5XoZn5q5k1jCRo8+37ZEEab/jsUFzE1Ea2sIBAbmP1CdwSeD6mZYmnUtR6xpi9esHIl6GA==", + "version": "4.144.0", + "resolved": "https://registry.npmjs.org/wrangler/-/wrangler-4.144.0.tgz", + "integrity": "sha512-4xIASysrsGBLkTuyoYUTJuhqjlnhwX90Ui8HVLElLJ9Unfqn0Hq51YuMP6GMmijbKFK/7Jmlgk6WkMZDCAklLQ==", "dev": true, "license": "MIT OR Apache-2.0", "dependencies": { @@ -3011,10 +2993,10 @@ "@cloudflare/unenv-preset": "2.16.2", "blake3-wasm": "2.1.5", "esbuild": "0.28.1", - "miniflare": "5.20260921.0-alpha", + "miniflare": "5.20260926.1-alpha", "path-to-regexp": "6.3.0", "unenv": "2.0.0-rc.24", - "workerd": "1.20260921.1" + "workerd": "1.20260926.1" }, "bin": { "cf-wrangler": "bin/cf-wrangler.js", @@ -3028,7 +3010,7 @@ "fsevents": "2.3.3" }, "peerDependencies": { - "@cloudflare/workers-types": "^5.20260921.1" + "@cloudflare/workers-types": "^5.20260926.1" }, "peerDependenciesMeta": { "@cloudflare/workers-types": { diff --git a/package.json b/package.json index 4ef347e..0aa6ee1 100644 --- a/package.json +++ b/package.json @@ -18,13 +18,13 @@ "vocabulary:check": "node scripts/public-vocabulary.mjs --check" }, "devDependencies": { - "@cloudflare/workers-types": "^5.20260923.1", + "@cloudflare/workers-types": "^5.20260930.1", "typescript": "^7.0.2", - "vitest": "^5.0.1", - "wrangler": "^4.136.3" + "vitest": "^5.0.2", + "wrangler": "^4.144.0" }, "allowScripts": { "esbuild@0.28.1": true, - "workerd@1.20260921.1": true + "workerd@1.20260926.1": true } } diff --git a/src/env.ts b/src/env.ts index d17361d..c96b5f9 100644 --- a/src/env.ts +++ b/src/env.ts @@ -4,6 +4,8 @@ export type RateLimiter = { export type Env = { TELEMETRY: AnalyticsEngineDataset; + /** Separate, identifier-free outcome dataset; never fall back to TELEMETRY. */ + UPDATE_RESULTS?: AnalyticsEngineDataset; /** Per-IP limit on recorded update checks. */ RATE_LIMIT?: RateLimiter; }; diff --git a/src/feature-stats.ts b/src/feature-stats.ts index 20a901b..518d165 100644 --- a/src/feature-stats.ts +++ b/src/feature-stats.ts @@ -3,17 +3,17 @@ import { parseFeatureStats } from "./payload.js"; /** Body cap: the documented payload is well under 1 KB. */ export const MAX_BODY_BYTES = 16_384; -function rejectDeclaredLength(request: Request): boolean { +function rejectDeclaredLength(request: Request, maxBytes: number): boolean { const header = request.headers.get("content-length"); if (header === null) return false; if (!/^[0-9]+$/.test(header)) return true; const declared = Number(header); - return !Number.isSafeInteger(declared) || declared > MAX_BODY_BYTES; + return !Number.isSafeInteger(declared) || declared > maxBytes; } /** Count stream bytes so a missing Content-Length cannot allocate the whole body. */ -async function readCappedText(request: Request): Promise { - if (rejectDeclaredLength(request)) return undefined; +export async function readCappedBody(request: Request, maxBytes = MAX_BODY_BYTES): Promise<{ text: string; byteLength: number } | undefined> { + if (rejectDeclaredLength(request, maxBytes)) return undefined; const body = request.body; if (!body) return undefined; @@ -27,7 +27,7 @@ async function readCappedText(request: Request): Promise { if (done) break; if (!value?.byteLength) continue; total += value.byteLength; - if (total > MAX_BODY_BYTES) { + if (total > maxBytes) { await reader.cancel().catch(() => undefined); return undefined; } @@ -45,7 +45,7 @@ async function readCappedText(request: Request): Promise { offset += chunk.byteLength; } try { - return new TextDecoder("utf-8", { fatal: true, ignoreBOM: false }).decode(bytes); + return { text: new TextDecoder("utf-8", { fatal: true, ignoreBOM: false }).decode(bytes), byteLength: total }; } catch { return undefined; } @@ -53,7 +53,7 @@ async function readCappedText(request: Request): Promise { export async function readFeatureStats(request: Request) { if (request.method !== "POST") return undefined; - const raw = await readCappedText(request); + const raw = (await readCappedBody(request))?.text; if (!raw) return undefined; const parsed = ((): unknown => { try { diff --git a/src/index.ts b/src/index.ts index ace71a4..42fbfe4 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,9 +1,11 @@ import { keepKnownNames, normalizeVersion } from "./allowlist.js"; import { buildDataPoint } from "./analytics.js"; import type { Env } from "./env.js"; -import { readFeatureStats } from "./feature-stats.js"; +import { readCappedBody } from "./feature-stats.js"; import { parseRequestGeography } from "./geography.js"; -import { parseClientIdentity } from "./payload.js"; +import { parseClientIdentity, parseFeatureStats } from "./payload.js"; +import type { FeatureStats } from "./payload.js"; +import { buildUpdateResultPoint, isUpdateResultCandidate, MAX_UPDATE_RESULT_BYTES, parseUpdateResult, UPDATE_RESULT_USER_AGENT } from "./update-result.js"; import { renderHomePage } from "./page.js"; const UPSTREAM_VERSION_URL = "https://registry.npmjs.org/openclaw/latest"; @@ -90,12 +92,8 @@ async function mayRecord(request: Request, env: Env): Promise { return outcome?.success !== false; } -async function recordRequest(request: Request, env: Env): Promise { - // Over-limit callers still get their answer below; they just stop counting. - if (!(await mayRecord(request, env))) return; - +function recordRequest(request: Request, env: Env, features: FeatureStats | undefined): void { const identity = parseClientIdentity(request.headers.get("user-agent")); - const features = await readFeatureStats(request); const validated = features ? { ...features, @@ -119,8 +117,33 @@ async function recordRequest(request: Request, env: Env): Promise { } async function handleLatestVersion(request: Request, env: Env): Promise { - await recordRequest(request, env); + // Check recording quota exactly once, before reading any upload. Exhausted + // callers must still get a version answer even if their body never finishes. + if (!(await mayRecord(request, env))) return latestVersionResponse(); + + let parsed: unknown; + if (request.method === "POST") { + // The fixed UA keeps even malformed/oversized outcome uploads off the + // legacy identity/geography path. JSON discriminators also work without it. + const outcomeAgent = request.headers.get("user-agent") === UPDATE_RESULT_USER_AGENT; + const body = await readCappedBody(request, outcomeAgent ? MAX_UPDATE_RESULT_BYTES : undefined); + const raw = body?.text; + try { parsed = raw ? JSON.parse(raw) : undefined; } catch { /* Never log request bodies. */ } + if (outcomeAgent || isUpdateResultCandidate(parsed)) { + const result = body && body.byteLength <= MAX_UPDATE_RESULT_BYTES + ? parseUpdateResult(parsed) : undefined; + if (!result) return jsonResponse({ error: "invalid_update_result" }, 400); + if (!env.UPDATE_RESULTS) return jsonResponse({ error: "update_results_unavailable" }, 503); + try { env.UPDATE_RESULTS.writeDataPoint(buildUpdateResultPoint(result)); } + catch { return jsonResponse({ error: "update_results_unavailable" }, 503); } + return latestVersionResponse(); + } + } + recordRequest(request, env, parseFeatureStats(parsed)); + return latestVersionResponse(); +} +async function latestVersionResponse(): Promise { const latest = await fetchLatestVersion(); if (!latest) return jsonResponse({ error: "version_unavailable" }, 503); return jsonResponse(RELEASE_NOTE ? { ...latest, note: RELEASE_NOTE } : latest, 200, VERSION_CACHE_SECONDS); @@ -131,6 +154,16 @@ export default { const url = new URL(request.url); if (url.pathname === "/api/latest-version") { + // Capability only: no body, geography, recording quota, analytics or npm. + // Presence is not a delivery/readiness probe; never write a sample point. + if (request.method === "HEAD") { + return new Response(null, { + status: env.UPDATE_RESULTS ? 204 : 503, + headers: env.UPDATE_RESULTS + ? { "OpenClaw-Update-Results": "2", "Cache-Control": "no-store" } + : { "Cache-Control": "no-store" }, + }); + } if (request.method !== "GET" && request.method !== "POST") { return jsonResponse({ error: "method_not_allowed" }, 405); } diff --git a/src/page.ts b/src/page.ts index a184566..d51217f 100644 --- a/src/page.ts +++ b/src/page.ts @@ -54,6 +54,9 @@ footer { margin-top: 3rem; padding-top: 1.5rem; border-top: 1px solid var(--line

Interactive setup defaults to No thanks; guided Quick Start skips the question. Scripted installs do not opt in automatically. The enabled setting controls inclusion, not whether a prompt was answered.

Channels and providers describe configuration; plugins describe enabled inventory, not invocations. sessionsLast24h counts retained session-creation events timestamped in the preceding 24 hours, not active sessions or messages. Missing or unreadable local state produces zero.

+

Update outcomes

+

The receiver also supports identifier-free terminal update outcomes from a companion client implementation. These use a separate dataset, strict public version labels and bounded outcome categories, with no geography, install IDs, raw errors or logs. Uploads are limited to 4096 bytes. Reports are retained for three months; no public individual-report route is provided. The companion client reports outcomes on by default, like the existing update ping, under update-request policy rather than optional feature-statistics consent. update.checkOnStart: false, OPENCLAW_NO_AUTO_UPDATE=1, and Nix mode suppress outcomes; a truthy CI always suppresses outcomes, even when a replacement OPENCLAW_TELEMETRY_ENDPOINT is configured. DO_NOT_TRACK controls feature statistics, not update outcomes. Receiver support does not itself enable client reporting: deploy and verify the receiver and separate dataset before releasing the default-on client, under separately authorized rollout. See the outcome contract and collection boundaries.

+

Approximate location

Cloudflare provides approximate location: country, region code, city, and timezone. We store no raw IP addresses or precise coordinates in analytics.

Recorded update checks include these fields even when anonymous feature statistics are off or DO_NOT_TRACK is set. Missing or invalid fields stay empty. Records are retained for three months.

@@ -71,11 +74,11 @@ footer { margin-top: 3rem; padding-top: 1.5rem; border-top: 1px solid var(--line

How to turn it off

- + - +
Command or settingEffect
openclaw telemetry offStops anonymous feature statistics. Update checks continue.
openclaw telemetry offStops anonymous feature statistics. Update checks and default-on outcomes continue.
DO_NOT_TRACK=1Same, enforced from the environment.
update.checkOnStart: falseStops both tiers of automatic update requests. Explicit updates and other configured services are separate.
update.checkOnStart: falseStops automatic update requests and outcome reporting. Explicit updates and other configured services are separate.
-

OPENCLAW_NO_AUTO_UPDATE=1 also prevents automatic update requests. A truthy CI suppresses both tiers unless a replacement OPENCLAW_TELEMETRY_ENDPOINT is explicitly configured.

+

OPENCLAW_NO_AUTO_UPDATE=1 also prevents automatic update requests. A truthy CI suppresses daily update checks and optional feature statistics unless a replacement OPENCLAW_TELEMETRY_ENDPOINT is explicitly configured. The replacement-endpoint exception applies only to daily update checks and optional feature statistics, not update outcomes.

openclaw telemetry show displays policy and a CLI-built payload preview, not the exact next Gateway payload or server-derived location information. Registry state and collection time can differ. If policy disables requests, it shows Request: none. Disabling requests does not erase previously recorded rows.