Add bounded enumeration persistence, durable alert retries, Worker input limits, workerd integration coverage, focused API/security modules, generated bindings, release gates, and collapsible operations documentation.
- commit
- 12e6976a64a833638247dc85bf0f7d5738f1e3c9
- parent
- dd91f998905dc81075395a001b38e8370f0ffe1f
- author
- Dami <dami.etoile@gmail.com>
@@ -16,8 +16,30 @@ jobs:
1616 - uses: denoland/setup-deno@v21717 with:1818 deno-version: v2.x19- # Publishes the whole workspace (@nzip/shared then @nzip/cli) at the20- # versions in their deno.json. No token needed — OIDC handles auth and21- # attaches build provenance.19+ - uses: actions/setup-node@v620+ with:21+ node-version: 2422+ cache: npm23+ cache-dependency-path: |24+ package-lock.json25+ worker/package-lock.json26+ - name: Install root tooling27+ run: npm ci28+ - name: Install Worker tooling29+ working-directory: worker30+ run: npm ci31+ - name: Check, lint, and test Deno workspace32+ run: |33+ deno task fmt:check34+ deno task lint35+ deno task check36+ deno task test37+ deno publish --dry-run38+ - name: Check and test Worker39+ working-directory: worker40+ run: |41+ npm run check42+ npm test43+ npx wrangler deploy --dry-run2244 - name: Publish workspace2345 run: deno publish@@ -136,7 +136,9 @@ flowchart LR
136136 GATE -- "no" --> PUBLIC["public response"]137137 ENUM -. "HMAC scanner identity" .-> PROBES[("D1 probe windows")]138138 PROBES --> EVAL["5-minute incident evaluator"]139- EVAL --> EMAIL["verified-destination email"]139+ EVAL --> OUTBOX[("D1 notification outbox")]140+ OUTBOX --> EMAIL["verified-destination email"]141+ EMAIL -. "retry on later cron" .-> OUTBOX140142 EMAIL --> INBOX(["operator inbox"])141143 ```142144 @@ -158,12 +160,15 @@ flowchart LR
158160 PROBES --> METRICS["D1 row metrics"]159161 ```160162 163+<details>164+<summary>Protocol, caching, and storage details</summary>165+161166 A push is a stateless three-step protocol, and the manifest itself is the state:162167 163168 1. `POST /api/push/prepare`: send the manifest, get back which blob hashes the164169 server is missing165170 2. `PUT /api/blob/{sha256}`: upload only those, one per request, 6 at a time;166- the server re-hashes and rejects mismatches171+ the server re-hashes and rejects mismatches or blobs above 50 MiB167172 3. `POST /api/push/commit`: the server re-verifies every blob exists, then168173 commits atomically, resolving or allocating the address, applying169174 TTL/password policy, repointing the site, and appending history@@ -185,24 +190,30 @@ Content metadata lives in three tables:
185190 186191 Security alert state is deliberately separate:187192 188-| table | purpose |189-| -------------------- | ------------------------------------------------------------------------ |190-| `security_probes` | deduplicated five-minute scanner/address observations; retained 7 days |191-| `security_signals` | rate-limit confirmations, deduplicated by scanner and window |192-| `security_incidents` | open/closed state, severity, suppression timestamps, and incident totals |193+| table | purpose |194+| ------------------------ | ------------------------------------------------------------------------ |195+| `security_probes` | deduplicated five-minute scanner/address observations; retained 7 days |196+| `security_signals` | rate-limit confirmations, deduplicated by scanner and window |197+| `security_incidents` | open/closed state, severity, suppression timestamps, and incident totals |198+| `security_notifications` | durable email payloads, delivery attempts, and retry state |193199 194200 **GC safety rule:** an R2 object is deleted only if no live site or retained195201 history entry references it _and_ it's older than 24 hours, so an in-flight push196202 can never lose objects to a concurrent sweep.197203 204+</details>205+198206 ## Security and observability199207 200208 The public address space is intentionally small and human-friendly, so nzip201209 treats enumeration as something to observe and rate-limit rather than pretending202210 four hex characters are a secret. The two telemetry paths serve different jobs:203211 204-| path | signal and retention |205-| ---------------------------- | ------------------------------------------------------------------------------------ |212+<details>213+<summary>Signals, alert policy, and operator workflow</summary>214+215+| path | signal and retention |216+| ---------------------------- | ----------------------------------------------------------------------------------- |206217 | Workers Logs | structured `security.request` events for a deterministic 1% scanner-identity sample |207218 | D1 five-minute probe windows | bounded, unsampled bare-address observations used for automatic incident decisions |208219 @@ -217,25 +228,30 @@ second time to control log volume.
217228 The five-minute evaluator opens or escalates an enumeration incident on these218229 signals:219230 220-| severity | signal |221-| --------- | -------------------------------------------------------------------------------------- |231+| severity | signal |232+| --------- | ------------------------------------------------------------------------------------- |222233 | warning | one scanner tries 20 distinct addresses in 5 minutes |223-| warning | 128 addresses from 10 scanners in 5 minutes, with at least 90% misses |234+| warning | 128 addresses from 10 scanners in 5 minutes, with at least 90% misses |224235 | confirmed | an enumeration request reaches the 429 limiter |225236 | confirmed | a live hit after 8-address/sequence evidence, or any live hit during an open incident |226237 227238 Duplicate email is suppressed unless severity increases, volume doubles, a live228239 site is hit for the first time, or a new vault is targeted after 30 minutes.229240 Active incidents summarize at most hourly and resolve after three quiet windows230-(15 minutes). A daily activity digest is sent only when probes occurred. Alerts231-use a Worker email binding restricted to one verified destination; the232-owner-authenticated `/api/security/test-alert` endpoint validates delivery.241+(15 minutes). Alert state and the exact email payload are committed to a D1242+outbox before delivery; transient failures retry on later cron runs with a243+stable notification ID. A daily activity digest is sent only when probes244+occurred. Alerts use a Worker email binding restricted to one verified245+destination; the owner-authenticated `/api/security/test-alert` endpoint246+validates delivery.233247 234-Inspect sampled events under **Workers & Pages → nzip → Observability**, filtering235-on `event = "security.request"`. Each event's `sample_rate` records its sampling236-factor for aggregate estimates. Watch D1 row metrics as the direct free-tier237-budget signal for alert storage, and Worker request/log counts for the overall238-service budget.248+Inspect sampled events under **Workers & Pages → nzip → Observability**,249+filtering on `event = "security.request"`. Each event's `sample_rate` records250+its sampling factor for aggregate estimates. Watch D1 row metrics as the direct251+free-tier budget signal for alert storage, and Worker request/log counts for the252+overall service budget.253+254+</details>239255 240256 ## Free-tier design target241257 @@ -245,14 +261,17 @@ design target, not a promise: account-wide traffic and storage count toward the
245261 same quotas, Cloudflare can change its limits, and a sufficiently distributed246262 attack can exhaust a daily quota.247263 248-| resource | how nzip keeps usage bounded | current included usage to watch |249-| -------- | ---------------------------- | ------------------------------- |250-| Workers | short request path; cache checked before invocation | [100,000 requests/day; 10 ms CPU/invocation](https://developers.cloudflare.com/workers/platform/limits/) |251-| Workers Cache | public content only, 60-second TTL, tag purge on mutation | [no separate request allowance](https://developers.cloudflare.com/workers/platform/pricing/); cached Worker requests still count toward Workers usage |252-| Workers Logs | invocation logs off; deterministic 1% identity sample; successful assets omitted | [200,000 log events/day with 3-day retention](https://developers.cloudflare.com/workers/observability/logs/workers-logs/#pricing) |253-| D1 | probe rows deduplicated by scanner/address/window, capped per scanner/location, pruned after 7 days | [5M rows read/day, 100K written/day, 5 GB](https://developers.cloudflare.com/d1/platform/pricing/) |254-| R2 Standard | content-addressed deduplication, retained-history cap, daily GC | [10 GB-month, 1M Class A and 10M Class B operations/month; free egress](https://developers.cloudflare.com/r2/pricing/#free-tier) |255-| Alert email | restricted to a verified Email Routing destination; no arbitrary-recipient sending | [verified-destination sends are free on all plans](https://developers.cloudflare.com/email-service/platform/pricing/) |264+<details>265+<summary>Included quotas, safeguards, and failure behavior</summary>266+267+| resource | how nzip keeps usage bounded | current included usage to watch |268+| ------------- | --------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- |269+| Workers | short request path; cache checked before invocation | [100,000 requests/day; 10 ms CPU/invocation](https://developers.cloudflare.com/workers/platform/limits/) |270+| Workers Cache | public content only, 60-second TTL, tag purge on mutation | [no separate request allowance](https://developers.cloudflare.com/workers/platform/pricing/); cached Worker requests still count toward Workers usage |271+| Workers Logs | invocation logs off; deterministic 1% identity sample; successful assets omitted | [200,000 log events/day with 3-day retention](https://developers.cloudflare.com/workers/observability/logs/workers-logs/#pricing) |272+| D1 | probe rows deduplicated by scanner/address/window, capped per scanner/location, pruned after 7 days | [5M rows read/day, 100K written/day, 5 GB](https://developers.cloudflare.com/d1/platform/pricing/) |273+| R2 Standard | content-addressed deduplication, retained-history cap, daily GC | [10 GB-month, 1M Class A and 10M Class B operations/month; free egress](https://developers.cloudflare.com/r2/pricing/#free-tier) |274+| Alert email | restricted to a verified Email Routing destination; no arbitrary-recipient sending | [verified-destination sends are free on all plans](https://developers.cloudflare.com/email-service/platform/pricing/) |256275 257276 On the Workers Free plan, exceeding included daily usage does not create an258277 overage bill. D1 queries fail until the quota resets; Worker request-limit@@ -261,10 +280,12 @@ persistence runs after the response and catches its own errors, so an exhausted
261280 alert-storage budget does not stop normal site serving; it does reduce detection262281 coverage until the quota resets. Cache hits reduce execution and storage reads,263282 but they do not remove the request from the Workers request quota. The telemetry264-rate limiter is best-effort and local to a Cloudflare location; deduplication and265-seven-day pruning provide the durable bounds, while a distributed attack can283+rate limiter is best-effort and local to a Cloudflare location; deduplication284+and seven-day pruning provide the durable bounds, while a distributed attack can266285 still consume the daily D1 allowance.267286 287+</details>288+268289 ## Repo layout269290 270291 ```text@@ -311,6 +332,9 @@ Wrangler config. Standing up that server is the one-time setup below.
311332 312333 See [`worker/setup.md`](worker/setup.md) for the full checklist. In short:313334 335+<details>336+<summary>Cloudflare provisioning commands and deployment gotchas</summary>337+314338 ```sh315339 cd worker316340 npx wrangler login@@ -344,8 +368,13 @@ Two gotchas learned the hard way:
344368 needs the zone on your account; it provisions DNS and the cert automatically345369 on deploy.346370 371+</details>372+347373 ## Development348374 375+<details>376+<summary>Local checks, Worker development, and scheduled-handler testing</summary>377+349378 ```sh350379 deno task check # typecheck CLI + shared351380 cd worker && npx tsc --noEmit # typecheck Worker@@ -362,3 +391,5 @@ either cron locally:
362391 curl "http://localhost:8787/__scheduled?cron=*/5+*+*+*+*" # alert evaluation363392 curl "http://localhost:8787/__scheduled?cron=0+4+*+*+*" # GC + digest364393 ```394+395+</details>@@ -3,7 +3,9 @@ import { configPath, loadConfig, saveConfig } from "../lib/config.ts";
33 import { emit, fail, green } from "../lib/fmt.ts";44 55 function prompt2(question: string, fallback?: string): string {6- const answer = prompt(fallback ? `${question} [${fallback}]:` : `${question}:`);6+ const answer = prompt(7+ fallback ? `${question} [${fallback}]:` : `${question}:`,8+ );79 const value = (answer ?? "").trim() || fallback || "";810 if (!value) fail(`${question} is required`);911 return value;@@ -13,7 +15,10 @@ function envDefaultServer(): string | undefined {
1315 try {1416 const server = Deno.env.get("NZIP_SERVER")?.trim();1517 if (server) return server;16- const domain = Deno.env.get("NZIP_DOMAIN")?.trim().replace(/^https?:\/\//, "").replace(18+ const domain = Deno.env.get("NZIP_DOMAIN")?.trim().replace(19+ /^https?:\/\//,20+ "",21+ ).replace(1722 /\/$/,1823 "",1924 );@@ -38,7 +43,9 @@ export async function cmdAuth(
3843 try {3944 status = await api.status();4045 } catch (e) {41- fail(`could not verify token against ${server}/api/status — ${(e as Error).message}`);46+ fail(47+ `could not verify token against ${server}/api/status — ${(e as Error).message}`,48+ );4249 }4350 4451 if (!config.defaultVault && status.vaults.length > 0) {@@ -46,8 +53,12 @@ export async function cmdAuth(
4653 }4754 await saveConfig(config);4855 emit(() => {49- console.log(`${green("✓")} authenticated against ${server} — saved to ${configPath()}`);50- if (config.defaultVault) console.log(` default vault: ${config.defaultVault}`);56+ console.log(57+ `${green("✓")} authenticated against ${server} — saved to ${configPath()}`,58+ );59+ if (config.defaultVault) {60+ console.log(` default vault: ${config.defaultVault}`);61+ }5162 }, {5263 ok: true,5364 server,@@ -11,7 +11,10 @@ const DOWNLOAD_CONCURRENCY = 6;
1111 1212 interface SourceApi {1313 source(target: string): ReturnType<ApiClient["source"]>;14- downloadSourceBlob(target: string, hash: string): ReturnType<ApiClient["downloadSourceBlob"]>;14+ downloadSourceBlob(15+ target: string,16+ hash: string,17+ ): ReturnType<ApiClient["downloadSourceBlob"]>;1518 }1619 1720 export interface CopyResult {@@ -31,12 +34,21 @@ function targetOrFail(raw: string | undefined, config: Config): string {
3134 }3235 }3336 34-async function prepareDestination(path: string, overwrite: boolean): Promise<void> {37+async function prepareDestination(38+ path: string,39+ overwrite: boolean,40+): Promise<void> {3541 try {3642 const stat = await Deno.lstat(path);37- if (!stat.isDirectory || stat.isSymlink) fail(`destination is not a directory: ${path}`);43+ if (!stat.isDirectory || stat.isSymlink) {44+ fail(`destination is not a directory: ${path}`);45+ }3846 for await (const _entry of Deno.readDir(path)) {39- if (!overwrite) fail(`destination is not empty: ${path} (pass --overwrite to replace files)`);47+ if (!overwrite) {48+ fail(49+ `destination is not empty: ${path} (pass --overwrite to replace files)`,50+ );51+ }4052 break;4153 }4254 } catch (e) {@@ -52,10 +64,16 @@ async function makeParent(path: string): Promise<void> {
5264 await Deno.mkdir(dirname(path), { recursive: true });5365 }5466 55-async function writeFile(path: string, bytes: Uint8Array, overwrite: boolean): Promise<void> {67+async function writeFile(68+ path: string,69+ bytes: Uint8Array,70+ overwrite: boolean,71+): Promise<void> {5672 try {5773 const existing = await Deno.lstat(path);58- if (existing.isDirectory || existing.isSymlink) fail(`refusing to replace non-file: ${path}`);74+ if (existing.isDirectory || existing.isSymlink) {75+ fail(`refusing to replace non-file: ${path}`);76+ }5977 if (!overwrite) fail(`destination file already exists: ${path}`);6078 } catch (e) {6179 if (!(e instanceof Deno.errors.NotFound)) throw e;@@ -105,12 +123,18 @@ export async function downloadSource(
105123 const totalBytes = files.reduce((total, [, entry]) => total + entry.s, 0);106124 const queue = [...files];107125 let downloaded = 0;108- const workers = Array.from({ length: Math.min(DOWNLOAD_CONCURRENCY, queue.length) }, async () => {126+ const workers = Array.from({127+ length: Math.min(DOWNLOAD_CONCURRENCY, queue.length),128+ }, async () => {109129 for (let next = queue.shift(); next !== undefined; next = queue.shift()) {110130 const [relativePath, entry] = next;111- if (!validatePath(relativePath)) fail(`invalid path in source manifest: ${relativePath}`);131+ if (!validatePath(relativePath)) {132+ fail(`invalid path in source manifest: ${relativePath}`);133+ }112134 const bytes = await api.downloadSourceBlob(target, entry.h);113- if (bytes.length !== entry.s) fail(`size mismatch while downloading ${relativePath}`);135+ if (bytes.length !== entry.s) {136+ fail(`size mismatch while downloading ${relativePath}`);137+ }114138 if (await sha256hex(bytes) !== entry.h) {115139 fail(`hash mismatch while downloading ${relativePath}`);116140 }@@ -3,7 +3,9 @@ import { downloadSource } from "./cp.ts";
33 44 function assertEquals(actual: unknown, expected: unknown): void {55 if (JSON.stringify(actual) !== JSON.stringify(expected)) {6- throw new Error(`expected ${JSON.stringify(expected)}, got ${JSON.stringify(actual)}`);6+ throw new Error(7+ `expected ${JSON.stringify(expected)}, got ${JSON.stringify(actual)}`,8+ );79 }810 }911 @@ -14,19 +16,32 @@ Deno.test("cp reconstructs each manifest file and reports the total", async () =
1416 ]);1517 const manifest: Manifest = { v: 1, files: {} };1618 for (const [path, bytes] of files) {17- manifest.files[path] = { h: await sha256hex(bytes), s: bytes.length, ct: "text/plain" };19+ manifest.files[path] = {20+ h: await sha256hex(bytes),21+ s: bytes.length,22+ ct: "text/plain",23+ };1824 }19- const response: SourceResponse = { address: "2a3f", manifestHash: "a".repeat(64), manifest };20- const byHash = new Map(Object.values(manifest.files).map((entry) => [entry.h, entry]));25+ const response: SourceResponse = {26+ address: "2a3f",27+ manifestHash: "a".repeat(64),28+ manifest,29+ };30+ const byHash = new Map(31+ Object.values(manifest.files).map((entry) => [entry.h, entry]),32+ );2133 const output = await Deno.makeTempDir();2234 2335 try {2436 const result = await downloadSource(2537 {26- source: async () => response,27- downloadSourceBlob: async (_target, hash) => {38+ source: () => Promise.resolve(response),39+ downloadSourceBlob: (_target, hash) => {2840 const entry = byHash.get(hash)!;29- return files.get(Object.entries(manifest.files).find(([, f]) => f === entry)![0])!;41+ const bytes = files.get(42+ Object.entries(manifest.files).find(([, f]) => f === entry)![0],43+ )!;44+ return Promise.resolve(bytes);3045 },3146 },3247 "work:demo",@@ -34,8 +49,14 @@ Deno.test("cp reconstructs each manifest file and reports the total", async () =
3449 false,3550 );3651 37- assertEquals(await Deno.readTextFile(`${output}/index.html`), "<h1>recovered</h1>\n");38- assertEquals(await Deno.readTextFile(`${output}/assets/app.js`), "console.log('ok')\n");52+ assertEquals(53+ await Deno.readTextFile(`${output}/index.html`),54+ "<h1>recovered</h1>\n",55+ );56+ assertEquals(57+ await Deno.readTextFile(`${output}/assets/app.js`),58+ "console.log('ok')\n",59+ );3960 assertEquals(result, {4061 address: "2a3f",4162 manifestHash: "a".repeat(64),@@ -37,13 +37,20 @@ export async function cmdPush(
3737 const bundle = await buildBundle(path).catch((e) => fail((e as Error).message));3838 const fileCount = Object.keys(bundle.manifest.files).length;3939 emit(() => {40- console.log(dim(` bundling ${path} … ${fileCount} files, ${formatBytes(bundle.totalBytes)}`));40+ console.log(41+ dim(42+ ` bundling ${path} … ${fileCount} files, ${formatBytes(bundle.totalBytes)}`,43+ ),44+ );4145 for (const w of bundle.warnings) console.log(` ${amber("!")} ${w}`);4246 }, undefined);4347 4448 const prep = await api.prepare(bundle.manifest);4549 const dedupCount = bundle.blobs.size - prep.missing.length;46- const missingBytes = prep.missing.reduce((n, h) => n + (bundle.blobs.get(h)?.length ?? 0), 0);50+ const missingBytes = prep.missing.reduce(51+ (n, h) => n + (bundle.blobs.get(h)?.length ?? 0),52+ 0,53+ );4754 emit(() =>4855 console.log(4956 dim(@@ -17,7 +17,11 @@ import {
1717 ttlLeft,1818 } from "../lib/fmt.ts";1919 20-function targetOrFail(raw: string | undefined, config: Config, usage: string): string {20+function targetOrFail(21+ raw: string | undefined,22+ config: Config,23+ usage: string,24+): string {2125 if (!raw) fail(usage);2226 try {2327 return resolveCliTarget(raw, config);@@ -61,7 +65,10 @@ export async function cmdSite(
6165 );6266 }6367 64-export async function cmdLs(config: Config, vault: string | undefined): Promise<void> {68+export async function cmdLs(69+ config: Config,70+ vault: string | undefined,71+): Promise<void> {6572 const api = new ApiClient(config);6673 const sites = await api.listSites(vault);6774 // The server is authoritative for what still exists — drop breadcrumbs for@@ -86,7 +93,11 @@ export async function cmdLs(config: Config, vault: string | undefined): Promise<
8693 }, { ok: true, sites });8794 }8895 89-export async function cmdRm(config: Config, raw: string | undefined, yes: boolean): Promise<void> {96+export async function cmdRm(97+ config: Config,98+ raw: string | undefined,99+ yes: boolean,100+): Promise<void> {90101 const target = targetOrFail(raw, config, "usage: nzip rm <target> [--yes]");91102 const api = new ApiClient(config);92103 const site = await api.siteDetail(target);@@ -102,11 +113,18 @@ export async function cmdRm(config: Config, raw: string | undefined, yes: boolea
102113 }103114 }104115 await api.deleteSite(target);105- await forget({ address: site.address, vault: site.vault, alias: site.alias ?? undefined }).catch(116+ await forget({117+ address: site.address,118+ vault: site.vault,119+ alias: site.alias ?? undefined,120+ }).catch(106121 () => {},107122 );108123 emit(109- () => console.log(`${green("✓")} removed ${bold(name)} — content becomes eligible for GC`),124+ () =>125+ console.log(126+ `${green("✓")} removed ${bold(name)} — content becomes eligible for GC`,127+ ),110128 { ok: true, removed: site.address, alias: site.alias, vault: site.vault },111129 );112130 }@@ -124,7 +142,9 @@ export async function cmdStatus(config: Config): Promise<void> {
124142 if (status.vaults.length > 0) {125143 console.log(table(126144 ["SLOT", "VAULT", "SITES"],127- status.vaults.map((v) => [`0x${v.slot.toString(16)}`, v.name, String(v.siteCount)]),145+ status.vaults.map((146+ v,147+ ) => [`0x${v.slot.toString(16)}`, v.name, String(v.siteCount)]),128148 ));129149 }130150 }, { ...status, server: config.server });@@ -136,7 +156,11 @@ export async function cmdRevert(
136156 toSeq: number | undefined,137157 list: boolean,138158 ): Promise<void> {139- const target = targetOrFail(raw, config, "usage: nzip revert <target> [--to N] [--list]");159+ const target = targetOrFail(160+ raw,161+ config,162+ "usage: nzip revert <target> [--to N] [--list]",163+ );140164 const api = new ApiClient(config);141165 142166 if (list) {@@ -45,7 +45,11 @@ export async function cmdVault(
4545 `${green("✓")} vault ${bold(v.name)} registered at slot 0x${v.slot.toString(16)}`,4646 );4747 if (madeDefault) console.log(dim(` set as default vault`));48- }, { ok: true, ...v, default: madeDefault || config.defaultVault === v.name });48+ }, {49+ ok: true,50+ ...v,51+ default: madeDefault || config.defaultVault === v.name,52+ });4953 return;5054 }5155 @@ -7,7 +7,10 @@ import type { Config } from "../lib/config.ts";
77 import { lookup } from "../lib/paths.ts";88 import { amber, dim, emit, fail } from "../lib/fmt.ts";99 10-export async function cmdWhere(config: Config, raw: string | undefined): Promise<void> {10+export async function cmdWhere(11+ config: Config,12+ raw: string | undefined,13+): Promise<void> {1114 if (!raw) fail("usage: nzip where <target>");1215 1316 const q = /^[0-9a-f]{4}$/.test(raw) ? { address: raw } : (() => {@@ -41,7 +44,9 @@ export async function cmdWhere(config: Config, raw: string | undefined): Promise
4144 () => {4245 console.log(entry.path); // bare path on stdout for `cd "$(nzip where …)"`4346 if (!exists) {44- console.error(dim(` ${amber("↳")} directory no longer exists (moved or deleted)`));47+ console.error(48+ dim(` ${amber("↳")} directory no longer exists (moved or deleted)`),49+ );4550 }4651 },4752 {@@ -1,9 +1,10 @@
11 {22 "name": "@nzip/cli",3- "version": "0.2.0",3+ "version": "0.3.0",44 "license": "MIT",55 "description": "Push a directory of HTML from the terminal and get a four-character URL back. The Deno CLI for nzip, a self-hosted Cloudflare-backed share tool.",66 "exports": "./main.ts",7+ "publish": { "exclude": ["**/*.test.ts", "**/*.test.mts", "**/*_test.ts"] },78 "imports": {89 "@std/cli": "jsr:@std/cli@^1.0.0",910 "@std/path": "jsr:@std/path@^1.0.0",@@ -18,14 +18,24 @@ import { assertVaultAllowed, type Config } from "./config.ts";
1818 export class ApiClient {1919 constructor(private config: Config) {}2020 21- private async request<T>(method: string, path: string, body?: BodyInit): Promise<T> {21+ private async request<T>(22+ method: string,23+ path: string,24+ body?: BodyInit | Uint8Array,25+ ): Promise<T> {26+ const fetchBody: BodyInit | undefined = body instanceof Uint8Array27+ ? body.buffer instanceof ArrayBuffer28+ ? body.buffer.slice(body.byteOffset, body.byteOffset + body.byteLength)29+ : body.slice().buffer30+ : body;2231 const res = await fetch(`${this.config.server}${path}`, {2332 method,2433 headers: {2534 authorization: `Bearer ${this.config.token}`,2635 ...(typeof body === "string" ? { "content-type": "application/json" } : {}),36+ ...(body instanceof Uint8Array ? { "content-length": String(body.byteLength) } : {}),2737 },28- body,38+ body: fetchBody,2939 });3040 const text = await res.text();3141 if (!res.ok) {@@ -38,7 +48,10 @@ export class ApiClient {
3848 return JSON.parse(text) as T;3949 }4050 41- private async requestBytes(method: string, path: string): Promise<Uint8Array> {51+ private async requestBytes(52+ method: string,53+ path: string,54+ ): Promise<Uint8Array> {4255 const res = await fetch(`${this.config.server}${path}`, {4356 method,4457 headers: { authorization: `Bearer ${this.config.token}` },@@ -59,11 +72,19 @@ export class ApiClient {
5972 }6073 6174 prepare(manifest: Manifest): Promise<PrepareResponse> {62- return this.request("POST", "/api/push/prepare", JSON.stringify({ manifest }));75+ return this.request(76+ "POST",77+ "/api/push/prepare",78+ JSON.stringify({ manifest }),79+ );6380 }6481 6582 uploadBlob(hash: string, bytes: Uint8Array): Promise<{ ok: true }> {66- return this.request("PUT", `/api/blob/${hash}`, bytes as unknown as BodyInit);83+ return this.request(84+ "PUT",85+ `/api/blob/${hash}`,86+ bytes,87+ );6788 }6889 6990 commit(req: CommitRequest): Promise<CommitResponse> {@@ -80,7 +101,10 @@ export class ApiClient {
80101 }81102 82103 source(target: string): Promise<SourceResponse> {83- return this.request("GET", `/api/sites/${encodeURIComponent(target)}/source`);104+ return this.request(105+ "GET",106+ `/api/sites/${encodeURIComponent(target)}/source`,107+ );84108 }85109 86110 downloadSourceBlob(target: string, hash: string): Promise<Uint8Array> {@@ -94,14 +118,21 @@ export class ApiClient {
94118 target: string,95119 patch: { ttl?: number | "forever"; password?: string | null },96120 ): Promise<SiteDetail> {97- return this.request("PATCH", `/api/sites/${encodeURIComponent(target)}`, JSON.stringify(patch));121+ return this.request(122+ "PATCH",123+ `/api/sites/${encodeURIComponent(target)}`,124+ JSON.stringify(patch),125+ );98126 }99127 100128 deleteSite(target: string): Promise<{ ok: true; address: string }> {101129 return this.request("DELETE", `/api/sites/${encodeURIComponent(target)}`);102130 }103131 104- revert(target: string, toSeq?: number): Promise<CommitResponse & { revertedTo: number }> {132+ revert(133+ target: string,134+ toSeq?: number,135+ ): Promise<CommitResponse & { revertedTo: number }> {105136 return this.request(106137 "POST",107138 `/api/sites/${encodeURIComponent(target)}/revert`,@@ -139,7 +170,10 @@ export function resolveCliTarget(raw: string, config: Config): string {
139170 return `${vault}:${raw}`;140171 }141172 142-export function commitTargetFor(raw: string | undefined, config: Config): Target {173+export function commitTargetFor(174+ raw: string | undefined,175+ config: Config,176+): Target {143177 if (raw === undefined) {144178 const vault = config.defaultVault;145179 if (!vault) throw new Error("no target given and no defaultVault set");@@ -0,0 +1,51 @@
1+import { MAX_BLOB_BYTES, MAX_UNIQUE_BLOBS } from "@nzip/shared";2+import { buildBundle } from "./bundle.ts";3+4+Deno.test("bundle rejects an oversized file before reading it", async () => {5+ const dir = await Deno.makeTempDir();6+ const path = `${dir}/oversized.bin`;7+ try {8+ await Deno.writeFile(path, new Uint8Array());9+ await Deno.truncate(path, MAX_BLOB_BYTES + 1);10+ try {11+ await buildBundle(path);12+ throw new Error("expected oversized bundle to be rejected");13+ } catch (error) {14+ if (15+ !(error instanceof Error) ||16+ !error.message.includes("file too large (max 50 MiB)")17+ ) {18+ throw error;19+ }20+ }21+ } finally {22+ await Deno.remove(dir, { recursive: true });23+ }24+});25+26+Deno.test("bundle rejects the first unique blob above the Worker ceiling", async () => {27+ const dir = await Deno.makeTempDir();28+ try {29+ await Promise.all(30+ Array.from(31+ { length: MAX_UNIQUE_BLOBS + 1 },32+ (_, i) => Deno.writeTextFile(`${dir}/file-${i}.txt`, `unique-${i}`),33+ ),34+ );35+ try {36+ await buildBundle(dir);37+ throw new Error("expected excessive unique blobs to be rejected");38+ } catch (error) {39+ if (40+ !(error instanceof Error) ||41+ !error.message.includes(42+ `too many unique blobs (max ${MAX_UNIQUE_BLOBS})`,43+ )44+ ) {45+ throw error;46+ }47+ }48+ } finally {49+ await Deno.remove(dir, { recursive: true });50+ }51+});@@ -1,7 +1,14 @@
11 // Directory walking, hashing, and manifest building for `nzip push`.22 33 import { globToRegExp, join, relative } from "@std/path";4-import { contentTypeFor, sha256hex, validatePath } from "@nzip/shared";4+import {5+ contentTypeFor,6+ MAX_BLOB_BYTES,7+ MAX_UNIQUE_BLOBS,8+ sha256hex,9+ validateManifest,10+ validatePath,11+} from "@nzip/shared";512 import type { Manifest } from "@nzip/shared";613 714 const SKIP_DIRS = new Set(["node_modules", ".git"]);@@ -26,7 +33,11 @@ async function loadIgnore(dir: string): Promise<RegExp[]> {
2633 }2734 }2835 29-async function* walk(root: string, dir: string, ignore: RegExp[]): AsyncGenerator<string> {36+async function* walk(37+ root: string,38+ dir: string,39+ ignore: RegExp[],40+): AsyncGenerator<string> {3041 for await (const entry of Deno.readDir(dir)) {3142 if (entry.name.startsWith(".")) continue;3243 const full = join(dir, entry.name);@@ -49,10 +60,26 @@ export async function buildBundle(path: string): Promise<Bundle> {
4960 let totalBytes = 0;5061 5162 const addFile = async (storedPath: string, filePath: string) => {63+ const fileStat = await Deno.stat(filePath);64+ if (fileStat.size > MAX_BLOB_BYTES) {65+ throw new Error(`file too large (max 50 MiB): ${storedPath}`);66+ }5267 const bytes = await Deno.readFile(filePath);68+ if (bytes.length > MAX_BLOB_BYTES) {69+ throw new Error(`file too large (max 50 MiB): ${storedPath}`);70+ }5371 const hash = await sha256hex(bytes);54- manifest.files[storedPath] = { h: hash, s: bytes.length, ct: contentTypeFor(storedPath) };55- if (!blobs.has(hash)) blobs.set(hash, bytes);72+ manifest.files[storedPath] = {73+ h: hash,74+ s: bytes.length,75+ ct: contentTypeFor(storedPath),76+ };77+ if (!blobs.has(hash)) {78+ if (blobs.size >= MAX_UNIQUE_BLOBS) {79+ throw new Error(`too many unique blobs (max ${MAX_UNIQUE_BLOBS})`);80+ }81+ blobs.set(hash, bytes);82+ }5683 totalBytes += bytes.length;5784 };5885 @@ -61,6 +88,7 @@ export async function buildBundle(path: string): Promise<Bundle> {
6188 const name = path.split("/").pop()!;6289 const stored = /\.html?$/i.test(name) ? "index.html" : name;6390 await addFile(stored, path);91+ validateManifest(manifest);6492 return { manifest, blobs, totalBytes, warnings };6593 }6694 @@ -78,8 +106,11 @@ export async function buildBundle(path: string): Promise<Bundle> {
78106 throw new Error(`no files to push in ${path}`);79107 }80108 if (!manifest.files["index.html"]) {81- warnings.push("no root index.html — the site URL will 404 until one exists");109+ warnings.push(110+ "no root index.html — the site URL will 404 until one exists",111+ );82112 }113+ validateManifest(manifest);83114 return { manifest, blobs, totalBytes, warnings };84115 }85116 @@ -20,7 +20,9 @@ export function assertVaultAllowed(vault: string, config: Config): void {
2020 if (!config.allowVaults) return; // unset → unrestricted2121 if (!config.allowVaults.includes(vault)) {2222 const allowed = config.allowVaults.length ? config.allowVaults.join(", ") : "none";23- throw new Error(`vault "${vault}" is not allowed by this config (allowed: ${allowed})`);23+ throw new Error(24+ `vault "${vault}" is not allowed by this config (allowed: ${allowed})`,25+ );2426 }2527 }2628 @@ -100,7 +100,9 @@ export function hintFor(message: string): string | undefined {
100100 101101 export function fail(message: string, hint = hintFor(message)): never {102102 if (jsonMode) {103- console.error(JSON.stringify({ ok: false, error: message, ...(hint ? { hint } : {}) }));103+ console.error(104+ JSON.stringify({ ok: false, error: message, ...(hint ? { hint } : {}) }),105+ );104106 } else {105107 console.error(`${red("✗")} ${message}`);106108 if (hint) console.error(` ${dim(`↳ ${hint}`)}`);@@ -67,7 +67,10 @@ export async function recordPush(entry: {
6767 // entry that still claims this vault:alias under a different address.6868 if (entry.vault && entry.alias) {6969 for (const [addr, e] of Object.entries(reg)) {70- if (addr !== entry.address && e.vault === entry.vault && e.alias === entry.alias) {70+ if (71+ addr !== entry.address && e.vault === entry.vault &&72+ e.alias === entry.alias73+ ) {7174 delete reg[addr];7275 }7376 }@@ -126,7 +129,10 @@ export async function forget(
126129 * addresses returned by the API; when `scopeVault` is set only entries in127130 * that vault are eligible for removal (the listing was vault-scoped).128131 */129-export async function reconcile(live: Set<string>, scopeVault?: string): Promise<void> {132+export async function reconcile(133+ live: Set<string>,134+ scopeVault?: string,135+): Promise<void> {130136 const reg = pruneExpired(await load());131137 let changed = false;132138 for (const [addr, e] of Object.entries(reg)) {@@ -89,7 +89,13 @@ async function main(): Promise<void> {
8989 return await cmdCp(config, rest[0], rest[1], args.overwrite);9090 case "site":9191 case "share": // compatibility alias for the former site-management command92- return await cmdSite(config, rest[0], args.ttl, args.password, args["no-password"]);92+ return await cmdSite(93+ config,94+ rest[0],95+ args.ttl,96+ args.password,97+ args["no-password"],98+ );9399 case "ls":94100 return await cmdLs(config, rest[0]);95101 case "where":@@ -5,7 +5,7 @@
55 "packages": {66 "": {77 "devDependencies": {8- "wrangler": "^4.109.0"8+ "wrangler": "^4.110.0"99 }1010 },1111 "node_modules/@cloudflare/kv-asset-handler": {@@ -683,6 +683,9 @@
683683 "arm"684684 ],685685 "dev": true,686+ "libc": [687+ "glibc"688+ ],686689 "license": "LGPL-3.0-or-later",687690 "optional": true,688691 "os": [@@ -700,6 +703,9 @@
700703 "arm64"701704 ],702705 "dev": true,706+ "libc": [707+ "glibc"708+ ],703709 "license": "LGPL-3.0-or-later",704710 "optional": true,705711 "os": [@@ -717,6 +723,9 @@
717723 "ppc64"718724 ],719725 "dev": true,726+ "libc": [727+ "glibc"728+ ],720729 "license": "LGPL-3.0-or-later",721730 "optional": true,722731 "os": [@@ -734,6 +743,9 @@
734743 "riscv64"735744 ],736745 "dev": true,746+ "libc": [747+ "glibc"748+ ],737749 "license": "LGPL-3.0-or-later",738750 "optional": true,739751 "os": [@@ -751,6 +763,9 @@
751763 "s390x"752764 ],753765 "dev": true,766+ "libc": [767+ "glibc"768+ ],754769 "license": "LGPL-3.0-or-later",755770 "optional": true,756771 "os": [@@ -768,6 +783,9 @@
768783 "x64"769784 ],770785 "dev": true,786+ "libc": [787+ "glibc"788+ ],771789 "license": "LGPL-3.0-or-later",772790 "optional": true,773791 "os": [@@ -785,6 +803,9 @@
785803 "arm64"786804 ],787805 "dev": true,806+ "libc": [807+ "musl"808+ ],788809 "license": "LGPL-3.0-or-later",789810 "optional": true,790811 "os": [@@ -802,6 +823,9 @@
802823 "x64"803824 ],804825 "dev": true,826+ "libc": [827+ "musl"828+ ],805829 "license": "LGPL-3.0-or-later",806830 "optional": true,807831 "os": [@@ -819,6 +843,9 @@
819843 "arm"820844 ],821845 "dev": true,846+ "libc": [847+ "glibc"848+ ],822849 "license": "Apache-2.0",823850 "optional": true,824851 "os": [@@ -842,6 +869,9 @@
842869 "arm64"843870 ],844871 "dev": true,872+ "libc": [873+ "glibc"874+ ],845875 "license": "Apache-2.0",846876 "optional": true,847877 "os": [@@ -865,6 +895,9 @@
865895 "ppc64"866896 ],867897 "dev": true,898+ "libc": [899+ "glibc"900+ ],868901 "license": "Apache-2.0",869902 "optional": true,870903 "os": [@@ -888,6 +921,9 @@
888921 "riscv64"889922 ],890923 "dev": true,924+ "libc": [925+ "glibc"926+ ],891927 "license": "Apache-2.0",892928 "optional": true,893929 "os": [@@ -911,6 +947,9 @@
911947 "s390x"912948 ],913949 "dev": true,950+ "libc": [951+ "glibc"952+ ],914953 "license": "Apache-2.0",915954 "optional": true,916955 "os": [@@ -934,6 +973,9 @@
934973 "x64"935974 ],936975 "dev": true,976+ "libc": [977+ "glibc"978+ ],937979 "license": "Apache-2.0",938980 "optional": true,939981 "os": [@@ -957,6 +999,9 @@
957999 "arm64"9581000 ],9591001 "dev": true,1002+ "libc": [1003+ "musl"1004+ ],9601005 "license": "Apache-2.0",9611006 "optional": true,9621007 "os": [@@ -980,6 +1025,9 @@
9801025 "x64"9811026 ],9821027 "dev": true,1028+ "libc": [1029+ "musl"1030+ ],9831031 "license": "Apache-2.0",9841032 "optional": true,9851033 "os": [@@ -1261,9 +1309,9 @@
12611309 }12621310 },12631311 "node_modules/miniflare": {1264- "version": "4.20260708.0",1265- "resolved": "https://registry.npmjs.org/miniflare/-/miniflare-4.20260708.0.tgz",1266- "integrity": "sha512-ut32kKKTyZIoXhbtteDAMOwTVx9aCeehMeKb1xmJs4RLdBrF5flSZD1MOpWqmJycIV7YBHjcaZcVHLMr/g5RZA==",1312+ "version": "4.20260708.1",1313+ "resolved": "https://registry.npmjs.org/miniflare/-/miniflare-4.20260708.1.tgz",1314+ "integrity": "sha512-c94O9zRDISdqO18EHt6l0iF/fWgWt8p18PJvRsA/L/NJZ9Cfke3s/F5Blg1XXF7WDutVRzWVWy8Vy4LaT5ifsA==",12671315 "dev": true,12681316 "license": "MIT",12691317 "dependencies": {@@ -1416,9 +1464,9 @@
14161464 }14171465 },14181466 "node_modules/wrangler": {1419- "version": "4.109.0",1420- "resolved": "https://registry.npmjs.org/wrangler/-/wrangler-4.109.0.tgz",1421- "integrity": "sha512-Ev6iIBNnRwvYeJv8B7377YsnY+PxEznHmz3r33cWgP+6A5HNqqqJhChBaVUWxxs0uQPKuYBy8tqrfyd7o8WmBg==",1467+ "version": "4.110.0",1468+ "resolved": "https://registry.npmjs.org/wrangler/-/wrangler-4.110.0.tgz",1469+ "integrity": "sha512-xZeXKYi7hxQRF5anL+v77RkufJNpF9f3Eqeyqq2QBsETpLZgh0Agj0jJ6JPtkbgn6ukZdh8OK5egsGPWIditgg==",14221470 "dev": true,14231471 "license": "MIT OR Apache-2.0",14241472 "dependencies": {@@ -1426,7 +1474,7 @@
14261474 "@cloudflare/unenv-preset": "2.16.1",14271475 "blake3-wasm": "2.1.5",14281476 "esbuild": "0.28.1",1429- "miniflare": "4.20260708.0",1477+ "miniflare": "4.20260708.1",14301478 "path-to-regexp": "6.3.0",14311479 "unenv": "2.0.0-rc.24",14321480 "workerd": "1.20260708.1"@@ -24,7 +24,11 @@ import { canonicalManifestBytes, type Manifest, manifestHash, parseTarget } from
2424 const manifest: Manifest = {2525 v: 1,2626 files: {27- "index.html": { h: "…64-hex-sha256…", s: 128, ct: "text/html; charset=utf-8" },27+ "index.html": {28+ h: "…64-hex-sha256…",29+ s: 128,30+ ct: "text/html; charset=utf-8",31+ },2832 },2933 };3034 @@ -16,13 +16,17 @@ export function isAddressString(s: string): boolean {
1616 1717 /** "2a3f" → 0x2a3f. Throws on malformed input. */1818 export function parseAddress(s: string): number {19- if (!ADDR_RE.test(s)) throw new Error(`invalid address: ${JSON.stringify(s)}`);19+ if (!ADDR_RE.test(s)) {20+ throw new Error(`invalid address: ${JSON.stringify(s)}`);21+ }2022 return parseInt(s, 16);2123 }2224 2325 /** 0x2a3f → "2a3f" */2426 export function formatAddress(addr: number): string {25- if (!Number.isInteger(addr) || addr < 0 || addr >= VAULT_SLOTS * SITES_PER_VAULT) {27+ if (28+ !Number.isInteger(addr) || addr < 0 || addr >= VAULT_SLOTS * SITES_PER_VAULT29+ ) {2630 throw new Error(`address out of range: ${addr}`);2731 }2832 return addr.toString(16).padStart(4, "0");@@ -1,7 +1,8 @@
11 {22 "name": "@nzip/shared",3- "version": "0.2.0",3+ "version": "0.3.0",44 "license": "MIT",55 "description": "Runtime-agnostic contract for nzip: manifest canonicalization, content hashing, and address parsing shared by the nzip CLI and Worker.",6- "exports": "./mod.ts"6+ "exports": "./mod.ts",7+ "publish": { "exclude": ["**/*.test.ts", "**/*.test.mts", "**/*_test.ts"] }78 }@@ -1,5 +1,6 @@
11 /** sha256 of bytes as lowercase hex. Web Crypto only — works in Deno and Workers. */22 export async function sha256hex(bytes: Uint8Array): Promise<string> {33 const digest = await crypto.subtle.digest("SHA-256", bytes as BufferSource);4- return [...new Uint8Array(digest)].map((b) => b.toString(16).padStart(2, "0")).join("");4+ return [...new Uint8Array(digest)].map((b) => b.toString(16).padStart(2, "0"))5+ .join("");56 }@@ -0,0 +1,51 @@
1+import { MAX_BLOB_BYTES, MAX_UNIQUE_BLOBS } from "./limits.ts";2+import { validateManifest } from "./manifest.ts";3+import type { Manifest } from "./types.ts";4+5+function manifestWithUniqueBlobs(count: number): Manifest {6+ const files: Manifest["files"] = {};7+ for (let i = 0; i < count; i++) {8+ files[`file-${i}.txt`] = {9+ h: i.toString(16).padStart(64, "0"),10+ s: 1,11+ ct: "text/plain",12+ };13+ }14+ return { v: 1, files };15+}16+17+function assertThrows(fn: () => void, message: string): void {18+ try {19+ fn();20+ } catch (error) {21+ if (error instanceof Error && error.message.includes(message)) return;22+ throw error;23+ }24+ throw new Error(`expected error containing: ${message}`);25+}26+27+Deno.test("manifest accepts the Workers Free unique-blob ceiling", () => {28+ validateManifest(manifestWithUniqueBlobs(MAX_UNIQUE_BLOBS));29+});30+31+Deno.test("manifest rejects one unique blob above the Workers Free ceiling", () => {32+ assertThrows(33+ () => validateManifest(manifestWithUniqueBlobs(MAX_UNIQUE_BLOBS + 1)),34+ `too many unique blobs (max ${MAX_UNIQUE_BLOBS})`,35+ );36+});37+38+Deno.test("duplicate hashes do not consume additional unique-blob capacity", () => {39+ const manifest = manifestWithUniqueBlobs(MAX_UNIQUE_BLOBS);40+ manifest.files["duplicate.txt"] = { ...manifest.files["file-0.txt"] };41+ validateManifest(manifest);42+ if (Object.keys(manifest.files).length !== MAX_UNIQUE_BLOBS + 1) {43+ throw new Error("duplicate manifest entry was not retained");44+ }45+});46+47+Deno.test("manifest rejects blobs larger than the upload limit", () => {48+ const manifest = manifestWithUniqueBlobs(1);49+ manifest.files["file-0.txt"].s = MAX_BLOB_BYTES + 1;50+ assertThrows(() => validateManifest(manifest), "blob too large");51+});Binary file.
@@ -25,3 +25,5 @@ export * from "./hash.ts";
2525 export * from "./manifest.ts";2626 export * from "./address.ts";2727 export * from "./mediatypes.ts";28+export * from "./version.ts";29+export * from "./limits.ts";@@ -0,0 +1,11 @@
1+import { VERSION } from "./version.ts";2+3+Deno.test("workspace package versions match the runtime version", async () => {4+ const cli = JSON.parse(await Deno.readTextFile("cli/deno.json"));5+ const shared = JSON.parse(await Deno.readTextFile("shared/deno.json"));6+ if (cli.version !== VERSION || shared.version !== VERSION) {7+ throw new Error(8+ `version mismatch: runtime=${VERSION}, cli=${cli.version}, shared=${shared.version}`,9+ );10+ }11+});@@ -0,0 +1,17 @@
1+CREATE TABLE IF NOT EXISTS security_notifications (2+ id TEXT PRIMARY KEY,3+ incident_name TEXT NOT NULL,4+ action TEXT NOT NULL,5+ window_bucket INTEGER NOT NULL,6+ subject TEXT NOT NULL,7+ text TEXT NOT NULL,8+ html TEXT,9+ created_at INTEGER NOT NULL,10+ sent_at INTEGER,11+ attempts INTEGER NOT NULL DEFAULT 0,12+ last_error TEXT13+);14+15+CREATE INDEX IF NOT EXISTS idx_security_notifications_pending16+ ON security_notifications(created_at)17+ WHERE sent_at IS NULL;@@ -6,9 +6,11 @@
66 "": {77 "name": "nzip-worker",88 "devDependencies": {9- "@cloudflare/workers-types": "^5.20260708.1",10- "typescript": "^5.8.0",11- "wrangler": "^4.0.0"9+ "@cloudflare/vitest-pool-workers": "^0.18.4",10+ "@types/node": "^26.1.1",11+ "typescript": "^5.9.3",12+ "vitest": "^4.1.10",13+ "wrangler": "^4.110.0"1214 }1315 },1416 "node_modules/@cloudflare/kv-asset-handler": {@@ -37,6 +39,25 @@
3739 }3840 }3941 },42+ "node_modules/@cloudflare/vitest-pool-workers": {43+ "version": "0.18.4",44+ "resolved": "https://registry.npmjs.org/@cloudflare/vitest-pool-workers/-/vitest-pool-workers-0.18.4.tgz",45+ "integrity": "sha512-jOXvyLoR2b8jFvo3tX+mWxXXwcZ+PbId3d7vGFtZsuKakmDjKSeIq4o/sQjkqNv6q3XacIKSCAvfM8TfkPyrEw==",46+ "dev": true,47+ "license": "MIT",48+ "dependencies": {49+ "cjs-module-lexer": "1.2.3",50+ "esbuild": "0.28.1",51+ "miniflare": "4.20260708.1",52+ "wrangler": "4.110.0",53+ "zod": "3.25.76"54+ },55+ "peerDependencies": {56+ "@vitest/runner": "^4.1.0",57+ "@vitest/snapshot": "^4.1.0",58+ "vitest": "^4.1.0"59+ }60+ },4061 "node_modules/@cloudflare/workerd-darwin-64": {4162 "version": "1.20260708.1",4263 "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-64/-/workerd-darwin-64-1.20260708.1.tgz",@@ -123,11 +144,13 @@
123144 }124145 },125146 "node_modules/@cloudflare/workers-types": {126- "version": "5.20260708.1",127- "resolved": "https://registry.npmjs.org/@cloudflare/workers-types/-/workers-types-5.20260708.1.tgz",128- "integrity": "sha512-FSRyCsxALKmmNnk/2HMkTiX9Iz9yqTiTWX/BJZdffGznMSunXmQgb3mKy9wGBX2BCTLOuRYWL36uh7/3Gm05Eg==",147+ "version": "5.20260712.1",148+ "resolved": "https://registry.npmjs.org/@cloudflare/workers-types/-/workers-types-5.20260712.1.tgz",149+ "integrity": "sha512-tkQ18Lz41EPXLzh4cSuIGQzztDVueuGfTcjlP4M7Qa0rfHpVBNItf3GRkd9O0JgiXs9csAwz/kVv7sjYXSoF2w==",129150 "dev": true,130- "license": "MIT OR Apache-2.0"151+ "license": "MIT OR Apache-2.0",152+ "optional": true,153+ "peer": true131154 },132155 "node_modules/@cspotcode/source-map-support": {133156 "version": "0.8.1",@@ -142,6 +165,18 @@
142165 "node": ">=12"143166 }144167 },168+ "node_modules/@emnapi/core": {169+ "version": "1.11.1",170+ "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.1.tgz",171+ "integrity": "sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==",172+ "dev": true,173+ "license": "MIT",174+ "optional": true,175+ "dependencies": {176+ "@emnapi/wasi-threads": "1.2.2",177+ "tslib": "^2.4.0"178+ }179+ },145180 "node_modules/@emnapi/runtime": {146181 "version": "1.11.2",147182 "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.2.tgz",@@ -153,6 +188,17 @@
153188 "tslib": "^2.4.0"154189 }155190 },191+ "node_modules/@emnapi/wasi-threads": {192+ "version": "1.2.2",193+ "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.2.tgz",194+ "integrity": "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==",195+ "dev": true,196+ "license": "MIT",197+ "optional": true,198+ "dependencies": {199+ "tslib": "^2.4.0"200+ }201+ },156202 "node_modules/@esbuild/aix-ppc64": {157203 "version": "0.28.1",158204 "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.1.tgz",@@ -1113,6 +1159,35 @@
11131159 "@jridgewell/sourcemap-codec": "^1.4.10"11141160 }11151161 },1162+ "node_modules/@napi-rs/wasm-runtime": {1163+ "version": "1.1.6",1164+ "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.6.tgz",1165+ "integrity": "sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg==",1166+ "dev": true,1167+ "license": "MIT",1168+ "optional": true,1169+ "dependencies": {1170+ "@tybys/wasm-util": "^0.10.3"1171+ },1172+ "funding": {1173+ "type": "github",1174+ "url": "https://github.com/sponsors/Brooooooklyn"1175+ },1176+ "peerDependencies": {1177+ "@emnapi/core": "^1.7.1",1178+ "@emnapi/runtime": "^1.7.1"1179+ }1180+ },1181+ "node_modules/@oxc-project/types": {1182+ "version": "0.139.0",1183+ "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.139.0.tgz",1184+ "integrity": "sha512-r9gHphtCs+1M7J0pw6Sn/hh/Wpa/iQrOOkrNAlVLF/gHq+/CJmHIWKKUUhdWjcD6CIa8idarspCsASiXCXvFUw==",1185+ "dev": true,1186+ "license": "MIT",1187+ "funding": {1188+ "url": "https://github.com/sponsors/Boshen"1189+ }1190+ },11161191 "node_modules/@poppinss/colors": {11171192 "version": "4.1.6",11181193 "resolved": "https://registry.npmjs.org/@poppinss/colors/-/colors-4.1.6.tgz",@@ -1142,184 +1217,1091 @@
11421217 "dev": true,11431218 "license": "MIT"11441219 },1145- "node_modules/@sindresorhus/is": {1146- "version": "7.2.0",1147- "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-7.2.0.tgz",1148- "integrity": "sha512-P1Cz1dWaFfR4IR+U13mqqiGsLFf1KbayybWwdd2vfctdV6hDpUkgCY0nKOLLTMSoRd/jJNjtbqzf13K8DCCXQw==",1220+ "node_modules/@rolldown/binding-android-arm64": {1221+ "version": "1.1.5",1222+ "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.1.5.tgz",1223+ "integrity": "sha512-lZg8fqIv2v7FF237bwMgzGZEJvGL79/s5knJ/i6FmsGF4XXlzccZ4jb+TrFIxtSSxFtIpdsgrPZeMk1I9AFcyQ==",1224+ "cpu": [1225+ "arm64"1226+ ],11491227 "dev": true,11501228 "license": "MIT",1229+ "optional": true,1230+ "os": [1231+ "android"1232+ ],11511233 "engines": {1152- "node": ">=18"1153- },1154- "funding": {1155- "url": "https://github.com/sindresorhus/is?sponsor=1"1234+ "node": "^20.19.0 || >=22.12.0"11561235 }11571236 },1158- "node_modules/@speed-highlight/core": {1159- "version": "1.2.17",1160- "resolved": "https://registry.npmjs.org/@speed-highlight/core/-/core-1.2.17.tgz",1161- "integrity": "sha512-Z92FwKpCtfaW1V0jTU/fh3QzYEZN8wDwrzRIBoADCJfn4mJCNcJN/XegifX7BDrQ8/h9Xh/JnbyMchL0FqXrkg==",1162- "dev": true,1163- "license": "CC0-1.0"1164- },1165- "node_modules/blake3-wasm": {1166- "version": "2.1.5",1167- "resolved": "https://registry.npmjs.org/blake3-wasm/-/blake3-wasm-2.1.5.tgz",1168- "integrity": "sha512-F1+K8EbfOZE49dtoPtmxUQrpXaBIl3ICvasLh+nJta0xkz+9kF/7uet9fLnwKqhDrmj6g+6K3Tw9yQPUg2ka5g==",1237+ "node_modules/@rolldown/binding-darwin-arm64": {1238+ "version": "1.1.5",1239+ "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.1.5.tgz",1240+ "integrity": "sha512-51Bnx9pNiMRKSUNtBfySkNJ9vMU9Hh3I1ozDd6gyPPYzaXCfnptUcEZxXGYFn+ul2dtcMUiqGR1Yai2K10uoTw==",1241+ "cpu": [1242+ "arm64"1243+ ],11691244 "dev": true,1170- "license": "MIT"1245+ "license": "MIT",1246+ "optional": true,1247+ "os": [1248+ "darwin"1249+ ],1250+ "engines": {1251+ "node": "^20.19.0 || >=22.12.0"1252+ }11711253 },1172- "node_modules/cookie": {1173- "version": "1.1.1",1174- "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz",1175- "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==",1254+ "node_modules/@rolldown/binding-darwin-x64": {1255+ "version": "1.1.5",1256+ "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.1.5.tgz",1257+ "integrity": "sha512-Tm+gbfC0aHu1tBA/JvKQh32S0K6YgCHkiAF4/W6xX0K0RmNuc94VeK419dJoE65R5aRxmo+noZQSWrAMF6yb6g==",1258+ "cpu": [1259+ "x64"1260+ ],11761261 "dev": true,11771262 "license": "MIT",1263+ "optional": true,1264+ "os": [1265+ "darwin"1266+ ],11781267 "engines": {1179- "node": ">=18"1180- },1181- "funding": {1182- "type": "opencollective",1183- "url": "https://opencollective.com/express"1268+ "node": "^20.19.0 || >=22.12.0"11841269 }11851270 },1186- "node_modules/detect-libc": {1187- "version": "2.1.2",1188- "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",1189- "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",1271+ "node_modules/@rolldown/binding-freebsd-x64": {1272+ "version": "1.1.5",1273+ "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.1.5.tgz",1274+ "integrity": "sha512-JMzDKCCXq93YccG5gz3hvOs1oXRKAf0XYpfOS88e+wZrC8Iugj6j68867vrYZkvpDDpKn/KoKORThmchMpF6TA==",1275+ "cpu": [1276+ "x64"1277+ ],11901278 "dev": true,1191- "license": "Apache-2.0",1279+ "license": "MIT",1280+ "optional": true,1281+ "os": [1282+ "freebsd"1283+ ],11921284 "engines": {1193- "node": ">=8"1285+ "node": "^20.19.0 || >=22.12.0"11941286 }11951287 },1196- "node_modules/error-stack-parser-es": {1197- "version": "1.0.5",1198- "resolved": "https://registry.npmjs.org/error-stack-parser-es/-/error-stack-parser-es-1.0.5.tgz",1199- "integrity": "sha512-5qucVt2XcuGMcEGgWI7i+yZpmpByQ8J1lHhcL7PwqCwu9FPP3VUXzT4ltHe5i2z9dePwEHcDVOAfSnHsOlCXRA==",1288+ "node_modules/@rolldown/binding-linux-arm-gnueabihf": {1289+ "version": "1.1.5",1290+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.1.5.tgz",1291+ "integrity": "sha512-uML21j2K5TfPGutKxub+M+nLjZIrWjXQ5Grx4lCe/nimTj9B4L63zHpjXLl4y0L3mcm2htEQIb06oCG/szerNw==",1292+ "cpu": [1293+ "arm"1294+ ],12001295 "dev": true,12011296 "license": "MIT",1202- "funding": {1203- "url": "https://github.com/sponsors/antfu"1297+ "optional": true,1298+ "os": [1299+ "linux"1300+ ],1301+ "engines": {1302+ "node": "^20.19.0 || >=22.12.0"12041303 }12051304 },1206- "node_modules/esbuild": {1207- "version": "0.28.1",1208- "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.1.tgz",1209- "integrity": "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==",1305+ "node_modules/@rolldown/binding-linux-arm64-gnu": {1306+ "version": "1.1.5",1307+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.1.5.tgz",1308+ "integrity": "sha512-navSiuTMogvnQoZoM/v+l3ZWo50/NTwSHSzheABx/RCnmUPaKwq9qSo4Br2OYRs21+Fz8uFqITZM3H4opOB0/Q==",1309+ "cpu": [1310+ "arm64"1311+ ],12101312 "dev": true,1211- "hasInstallScript": true,12121313 "license": "MIT",1213- "bin": {1214- "esbuild": "bin/esbuild"1215- },1314+ "optional": true,1315+ "os": [1316+ "linux"1317+ ],12161318 "engines": {1217- "node": ">=18"1218- },1219- "optionalDependencies": {1220- "@esbuild/aix-ppc64": "0.28.1",1221- "@esbuild/android-arm": "0.28.1",1222- "@esbuild/android-arm64": "0.28.1",1223- "@esbuild/android-x64": "0.28.1",1224- "@esbuild/darwin-arm64": "0.28.1",1225- "@esbuild/darwin-x64": "0.28.1",1226- "@esbuild/freebsd-arm64": "0.28.1",1227- "@esbuild/freebsd-x64": "0.28.1",1228- "@esbuild/linux-arm": "0.28.1",1229- "@esbuild/linux-arm64": "0.28.1",1230- "@esbuild/linux-ia32": "0.28.1",1231- "@esbuild/linux-loong64": "0.28.1",1232- "@esbuild/linux-mips64el": "0.28.1",1233- "@esbuild/linux-ppc64": "0.28.1",1234- "@esbuild/linux-riscv64": "0.28.1",1235- "@esbuild/linux-s390x": "0.28.1",1236- "@esbuild/linux-x64": "0.28.1",1237- "@esbuild/netbsd-arm64": "0.28.1",1238- "@esbuild/netbsd-x64": "0.28.1",1239- "@esbuild/openbsd-arm64": "0.28.1",1240- "@esbuild/openbsd-x64": "0.28.1",1241- "@esbuild/openharmony-arm64": "0.28.1",1242- "@esbuild/sunos-x64": "0.28.1",1243- "@esbuild/win32-arm64": "0.28.1",1244- "@esbuild/win32-ia32": "0.28.1",1245- "@esbuild/win32-x64": "0.28.1"1319+ "node": "^20.19.0 || >=22.12.0"12461320 }12471321 },1248- "node_modules/fsevents": {1249- "version": "2.3.3",1250- "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",1251- "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",1322+ "node_modules/@rolldown/binding-linux-arm64-musl": {1323+ "version": "1.1.5",1324+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.1.5.tgz",1325+ "integrity": "sha512-lAryqH7IteztmCXQXk0etKj4wBQ7Gx5S6LjKhsgp9zb8I5bsuvU/2llH1hDQcjsFeqIsovMVN339/8pUDDBXxA==",1326+ "cpu": [1327+ "arm64"1328+ ],12521329 "dev": true,1253- "hasInstallScript": true,12541330 "license": "MIT",12551331 "optional": true,12561332 "os": [1257- "darwin"1333+ "linux"12581334 ],12591335 "engines": {1260- "node": "^8.16.0 || ^10.6.0 || >=11.0.0"1336+ "node": "^20.19.0 || >=22.12.0"12611337 }12621338 },1263- "node_modules/kleur": {1264- "version": "4.1.5",1265- "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz",1266- "integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==",1339+ "node_modules/@rolldown/binding-linux-ppc64-gnu": {1340+ "version": "1.1.5",1341+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.1.5.tgz",1342+ "integrity": "sha512-fsK/sNBnxzBlL4O1JNrZakVQxPspqpED5dLtNsZS9oOKmtSpdNIzxH2kkol5HYTWJN47sE20ztMJPxfZ89qGOg==",1343+ "cpu": [1344+ "ppc64"1345+ ],12671346 "dev": true,12681347 "license": "MIT",1348+ "optional": true,1349+ "os": [1350+ "linux"1351+ ],12691352 "engines": {1270- "node": ">=6"1353+ "node": "^20.19.0 || >=22.12.0"12711354 }12721355 },1273- "node_modules/miniflare": {1274- "version": "4.20260708.0",1275- "resolved": "https://registry.npmjs.org/miniflare/-/miniflare-4.20260708.0.tgz",1276- "integrity": "sha512-ut32kKKTyZIoXhbtteDAMOwTVx9aCeehMeKb1xmJs4RLdBrF5flSZD1MOpWqmJycIV7YBHjcaZcVHLMr/g5RZA==",1356+ "node_modules/@rolldown/binding-linux-s390x-gnu": {1357+ "version": "1.1.5",1358+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.1.5.tgz",1359+ "integrity": "sha512-gLYb4BIadlfTOYT5gO503n8zQjXflgzpD0FcyKh0Mzx3rqCZKnHoJWV9xe1KXUJ5lx2JfcSHr/mhzS0PC/McAA==",1360+ "cpu": [1361+ "s390x"1362+ ],12771363 "dev": true,12781364 "license": "MIT",1279- "dependencies": {1280- "@cspotcode/source-map-support": "0.8.1",1281- "sharp": "0.34.5",1282- "undici": "7.28.0",1283- "workerd": "1.20260708.1",1284- "ws": "8.21.0",1285- "youch": "4.1.0-beta.10"1286- },1287- "bin": {1288- "miniflare": "bootstrap.js"1289- },1365+ "optional": true,1366+ "os": [1367+ "linux"1368+ ],12901369 "engines": {1291- "node": ">=22.0.0"1370+ "node": "^20.19.0 || >=22.12.0"12921371 }12931372 },1294- "node_modules/path-to-regexp": {1295- "version": "6.3.0",1296- "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.3.0.tgz",1297- "integrity": "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==",1373+ "node_modules/@rolldown/binding-linux-x64-gnu": {1374+ "version": "1.1.5",1375+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.1.5.tgz",1376+ "integrity": "sha512-FjcpEKUyJygHgs1o50VYNvkt5+7Le/VEdYt0AkRpkL33MnyQfwr8l5mXwMmfmTbyMPr5vJLC+8/Gd9gXnwU1QQ==",1377+ "cpu": [1378+ "x64"1379+ ],12981380 "dev": true,1299- "license": "MIT"1381+ "license": "MIT",1382+ "optional": true,1383+ "os": [1384+ "linux"1385+ ],1386+ "engines": {1387+ "node": "^20.19.0 || >=22.12.0"1388+ }13001389 },1301- "node_modules/pathe": {1302- "version": "2.0.3",1303- "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz",1304- "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==",1390+ "node_modules/@rolldown/binding-linux-x64-musl": {1391+ "version": "1.1.5",1392+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.1.5.tgz",1393+ "integrity": "sha512-Me+PfPI2TMeOQk0gYWfLQZtTktrmzbr8cDboqX83XKc7UrgAi55gF+2dUkWdxd19n55Essp2yeca+O9N5rBxHg==",1394+ "cpu": [1395+ "x64"1396+ ],13051397 "dev": true,1306- "license": "MIT"1398+ "license": "MIT",1399+ "optional": true,1400+ "os": [1401+ "linux"1402+ ],1403+ "engines": {1404+ "node": "^20.19.0 || >=22.12.0"1405+ }13071406 },1308- "node_modules/semver": {1309- "version": "7.8.5",1310- "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz",1311- "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==",1407+ "node_modules/@rolldown/binding-openharmony-arm64": {1408+ "version": "1.1.5",1409+ "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.1.5.tgz",1410+ "integrity": "sha512-yc5WrLzXks6zCQfn9Oxr8pORKyl/pF+QjHmW/Qx3qu0oyrrNC+y2JLTU1E2rcWYAmzlnqngWXHQjy51VzW70Vw==",1411+ "cpu": [1412+ "arm64"1413+ ],13121414 "dev": true,1313- "license": "ISC",1314- "bin": {1315- "semver": "bin/semver.js"1316- },1415+ "license": "MIT",1416+ "optional": true,1417+ "os": [1418+ "openharmony"1419+ ],13171420 "engines": {1318- "node": ">=10"1421+ "node": "^20.19.0 || >=22.12.0"13191422 }13201423 },1321- "node_modules/sharp": {1322- "version": "0.34.5",1424+ "node_modules/@rolldown/binding-wasm32-wasi": {1425+ "version": "1.1.5",1426+ "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.1.5.tgz",1427+ "integrity": "sha512-VbQGPX2b4r48TAMIM2cjgluIM1HYutm4pcTEJsle7iEP7sB1dFqtPLBVbdLAZCxy1txCcPxf4QFf4v8uvltPqA==",1428+ "cpu": [1429+ "wasm32"1430+ ],1431+ "dev": true,1432+ "license": "MIT",1433+ "optional": true,1434+ "dependencies": {1435+ "@emnapi/core": "1.11.1",1436+ "@emnapi/runtime": "1.11.1",1437+ "@napi-rs/wasm-runtime": "^1.1.6"1438+ },1439+ "engines": {1440+ "node": "^20.19.0 || >=22.12.0"1441+ }1442+ },1443+ "node_modules/@rolldown/binding-wasm32-wasi/node_modules/@emnapi/runtime": {1444+ "version": "1.11.1",1445+ "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.1.tgz",1446+ "integrity": "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==",1447+ "dev": true,1448+ "license": "MIT",1449+ "optional": true,1450+ "dependencies": {1451+ "tslib": "^2.4.0"1452+ }1453+ },1454+ "node_modules/@rolldown/binding-win32-arm64-msvc": {1455+ "version": "1.1.5",1456+ "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.1.5.tgz",1457+ "integrity": "sha512-gHv82k63z4qpV5+Q1y/12KrK0ltWBukVDI8nZcbT7Tt/ZlOIVwppazneq0F93oDxTo3IgAMEDIoQh3E2n6mVsw==",1458+ "cpu": [1459+ "arm64"1460+ ],1461+ "dev": true,1462+ "license": "MIT",1463+ "optional": true,1464+ "os": [1465+ "win32"1466+ ],1467+ "engines": {1468+ "node": "^20.19.0 || >=22.12.0"1469+ }1470+ },1471+ "node_modules/@rolldown/binding-win32-x64-msvc": {1472+ "version": "1.1.5",1473+ "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.1.5.tgz",1474+ "integrity": "sha512-tTZuDBPw85tEN5PQi1pnEBzDy0Z49HtScLAbD5t6hyeU92A95pRWaSMw1GZZi/RwgSgUIl0xrSlXIT/9QzvYSA==",1475+ "cpu": [1476+ "x64"1477+ ],1478+ "dev": true,1479+ "license": "MIT",1480+ "optional": true,1481+ "os": [1482+ "win32"1483+ ],1484+ "engines": {1485+ "node": "^20.19.0 || >=22.12.0"1486+ }1487+ },1488+ "node_modules/@rolldown/pluginutils": {1489+ "version": "1.0.1",1490+ "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz",1491+ "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==",1492+ "dev": true,1493+ "license": "MIT"1494+ },1495+ "node_modules/@sindresorhus/is": {1496+ "version": "7.2.0",1497+ "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-7.2.0.tgz",1498+ "integrity": "sha512-P1Cz1dWaFfR4IR+U13mqqiGsLFf1KbayybWwdd2vfctdV6hDpUkgCY0nKOLLTMSoRd/jJNjtbqzf13K8DCCXQw==",1499+ "dev": true,1500+ "license": "MIT",1501+ "engines": {1502+ "node": ">=18"1503+ },1504+ "funding": {1505+ "url": "https://github.com/sindresorhus/is?sponsor=1"1506+ }1507+ },1508+ "node_modules/@speed-highlight/core": {1509+ "version": "1.2.17",1510+ "resolved": "https://registry.npmjs.org/@speed-highlight/core/-/core-1.2.17.tgz",1511+ "integrity": "sha512-Z92FwKpCtfaW1V0jTU/fh3QzYEZN8wDwrzRIBoADCJfn4mJCNcJN/XegifX7BDrQ8/h9Xh/JnbyMchL0FqXrkg==",1512+ "dev": true,1513+ "license": "CC0-1.0"1514+ },1515+ "node_modules/@standard-schema/spec": {1516+ "version": "1.1.0",1517+ "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz",1518+ "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==",1519+ "dev": true,1520+ "license": "MIT"1521+ },1522+ "node_modules/@tybys/wasm-util": {1523+ "version": "0.10.3",1524+ "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.3.tgz",1525+ "integrity": "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==",1526+ "dev": true,1527+ "license": "MIT",1528+ "optional": true,1529+ "dependencies": {1530+ "tslib": "^2.4.0"1531+ }1532+ },1533+ "node_modules/@types/chai": {1534+ "version": "5.2.3",1535+ "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz",1536+ "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==",1537+ "dev": true,1538+ "license": "MIT",1539+ "dependencies": {1540+ "@types/deep-eql": "*",1541+ "assertion-error": "^2.0.1"1542+ }1543+ },1544+ "node_modules/@types/deep-eql": {1545+ "version": "4.0.2",1546+ "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz",1547+ "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==",1548+ "dev": true,1549+ "license": "MIT"1550+ },1551+ "node_modules/@types/estree": {1552+ "version": "1.0.9",1553+ "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz",1554+ "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==",1555+ "dev": true,1556+ "license": "MIT"1557+ },1558+ "node_modules/@types/node": {1559+ "version": "26.1.1",1560+ "resolved": "https://registry.npmjs.org/@types/node/-/node-26.1.1.tgz",1561+ "integrity": "sha512-nxAkRSVkN1Y0JC1W8ky/fTfkGsMmcrRsbx+3XoZE+rMOX71kLYTV7fLXpqud1GpbpP5TuffXFqfX7fH2GgZREw==",1562+ "dev": true,1563+ "license": "MIT",1564+ "dependencies": {1565+ "undici-types": "~8.3.0"1566+ }1567+ },1568+ "node_modules/@vitest/expect": {1569+ "version": "4.1.10",1570+ "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.10.tgz",1571+ "integrity": "sha512-YsCn+qAk1GWjQOWFEsEcL2gNQ0zmVmQu3T03qP6UyjhtmdtwtbuI+DASn/7iQB3HGTXkdBwGddzxPlmiql5vlA==",1572+ "dev": true,1573+ "license": "MIT",1574+ "dependencies": {1575+ "@standard-schema/spec": "^1.1.0",1576+ "@types/chai": "^5.2.2",1577+ "@vitest/spy": "4.1.10",1578+ "@vitest/utils": "4.1.10",1579+ "chai": "^6.2.2",1580+ "tinyrainbow": "^3.1.0"1581+ },1582+ "funding": {1583+ "url": "https://opencollective.com/vitest"1584+ }1585+ },1586+ "node_modules/@vitest/mocker": {1587+ "version": "4.1.10",1588+ "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.10.tgz",1589+ "integrity": "sha512-v0xaezt+DKEmKfaxg133ldzADrwLGd7Ze1MfQQTYfvs8OqZIwbxyxaYURivwV7sWy5fqn3rH5uOrSp07bp44Ow==",1590+ "dev": true,1591+ "license": "MIT",1592+ "dependencies": {1593+ "@vitest/spy": "4.1.10",1594+ "estree-walker": "^3.0.3",1595+ "magic-string": "^0.30.21"1596+ },1597+ "funding": {1598+ "url": "https://opencollective.com/vitest"1599+ },1600+ "peerDependencies": {1601+ "msw": "^2.4.9",1602+ "vite": "^6.0.0 || ^7.0.0 || ^8.0.0"1603+ },1604+ "peerDependenciesMeta": {1605+ "msw": {1606+ "optional": true1607+ },1608+ "vite": {1609+ "optional": true1610+ }1611+ }1612+ },1613+ "node_modules/@vitest/pretty-format": {1614+ "version": "4.1.10",1615+ "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.10.tgz",1616+ "integrity": "sha512-W1HsjSH4MXQ9YfmmhLAoIYf1HRfekQCGngeIgcei6MP5QQGWUe0gkopdZQaVCFO+JDJMrAJGwa5pRpNpvy4P8Q==",1617+ "dev": true,1618+ "license": "MIT",1619+ "dependencies": {1620+ "tinyrainbow": "^3.1.0"1621+ },1622+ "funding": {1623+ "url": "https://opencollective.com/vitest"1624+ }1625+ },1626+ "node_modules/@vitest/runner": {1627+ "version": "4.1.10",1628+ "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.10.tgz",1629+ "integrity": "sha512-IKI6kpIH+LmpROplyLwBBaCfMgOZOMsygVa6BARD6ahA04VRuJSa6OaVG7kRvSEMD870Vd91rSSw0eegtWyLGg==",1630+ "dev": true,1631+ "license": "MIT",1632+ "dependencies": {1633+ "@vitest/utils": "4.1.10",1634+ "pathe": "^2.0.3"1635+ },1636+ "funding": {1637+ "url": "https://opencollective.com/vitest"1638+ }1639+ },1640+ "node_modules/@vitest/snapshot": {1641+ "version": "4.1.10",1642+ "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.10.tgz",1643+ "integrity": "sha512-xRkfOT1qpTAi/Ti4Y1LtfRc3kEuqxGw59eN2jN9pRWMtS/XDevekhcFSqvQqjUNGksfjMJu3Y+oJ+4Ypn2OaJw==",1644+ "dev": true,1645+ "license": "MIT",1646+ "dependencies": {1647+ "@vitest/pretty-format": "4.1.10",1648+ "@vitest/utils": "4.1.10",1649+ "magic-string": "^0.30.21",1650+ "pathe": "^2.0.3"1651+ },1652+ "funding": {1653+ "url": "https://opencollective.com/vitest"1654+ }1655+ },1656+ "node_modules/@vitest/spy": {1657+ "version": "4.1.10",1658+ "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.10.tgz",1659+ "integrity": "sha512-PLf/Ugvoq5wO/b4rwYCR1h2PSIdXz7wnkQFMiUpLdtM7l6pqVFcQIBEHyT1+l+cj7mNwAfZHzqXqDyjvOuwbDw==",1660+ "dev": true,1661+ "license": "MIT",1662+ "funding": {1663+ "url": "https://opencollective.com/vitest"1664+ }1665+ },1666+ "node_modules/@vitest/utils": {1667+ "version": "4.1.10",1668+ "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.10.tgz",1669+ "integrity": "sha512-fy9am/HWxbaGt/Sawrp90vt6Y6jQwf1RX77cz3uwoJwJVMli/e1IEwRPnMNJ7vKfPTwo0diXifkpPvwH9v7nGA==",1670+ "dev": true,1671+ "license": "MIT",1672+ "dependencies": {1673+ "@vitest/pretty-format": "4.1.10",1674+ "convert-source-map": "^2.0.0",1675+ "tinyrainbow": "^3.1.0"1676+ },1677+ "funding": {1678+ "url": "https://opencollective.com/vitest"1679+ }1680+ },1681+ "node_modules/assertion-error": {1682+ "version": "2.0.1",1683+ "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz",1684+ "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==",1685+ "dev": true,1686+ "license": "MIT",1687+ "engines": {1688+ "node": ">=12"1689+ }1690+ },1691+ "node_modules/blake3-wasm": {1692+ "version": "2.1.5",1693+ "resolved": "https://registry.npmjs.org/blake3-wasm/-/blake3-wasm-2.1.5.tgz",1694+ "integrity": "sha512-F1+K8EbfOZE49dtoPtmxUQrpXaBIl3ICvasLh+nJta0xkz+9kF/7uet9fLnwKqhDrmj6g+6K3Tw9yQPUg2ka5g==",1695+ "dev": true,1696+ "license": "MIT"1697+ },1698+ "node_modules/chai": {1699+ "version": "6.2.2",1700+ "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz",1701+ "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==",1702+ "dev": true,1703+ "license": "MIT",1704+ "engines": {1705+ "node": ">=18"1706+ }1707+ },1708+ "node_modules/cjs-module-lexer": {1709+ "version": "1.2.3",1710+ "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.2.3.tgz",1711+ "integrity": "sha512-0TNiGstbQmCFwt4akjjBg5pLRTSyj/PkWQ1ZoO2zntmg9yLqSRxwEa4iCfQLGjqhiqBfOJa7W/E8wfGrTDmlZQ==",1712+ "dev": true,1713+ "license": "MIT"1714+ },1715+ "node_modules/convert-source-map": {1716+ "version": "2.0.0",1717+ "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",1718+ "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",1719+ "dev": true,1720+ "license": "MIT"1721+ },1722+ "node_modules/cookie": {1723+ "version": "1.1.1",1724+ "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz",1725+ "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==",1726+ "dev": true,1727+ "license": "MIT",1728+ "engines": {1729+ "node": ">=18"1730+ },1731+ "funding": {1732+ "type": "opencollective",1733+ "url": "https://opencollective.com/express"1734+ }1735+ },1736+ "node_modules/detect-libc": {1737+ "version": "2.1.2",1738+ "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",1739+ "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",1740+ "dev": true,1741+ "license": "Apache-2.0",1742+ "engines": {1743+ "node": ">=8"1744+ }1745+ },1746+ "node_modules/error-stack-parser-es": {1747+ "version": "1.0.5",1748+ "resolved": "https://registry.npmjs.org/error-stack-parser-es/-/error-stack-parser-es-1.0.5.tgz",1749+ "integrity": "sha512-5qucVt2XcuGMcEGgWI7i+yZpmpByQ8J1lHhcL7PwqCwu9FPP3VUXzT4ltHe5i2z9dePwEHcDVOAfSnHsOlCXRA==",1750+ "dev": true,1751+ "license": "MIT",1752+ "funding": {1753+ "url": "https://github.com/sponsors/antfu"1754+ }1755+ },1756+ "node_modules/es-module-lexer": {1757+ "version": "2.3.0",1758+ "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.0.tgz",1759+ "integrity": "sha512-KLdwQm2NvGLDkQDCGvmiQrhkd0JbMzXthwQAUgWjQuQdBLFa3eiBP5arXZyA+f8x+x7OXgud6bq2rxjGtHV2tw==",1760+ "dev": true,1761+ "license": "MIT"1762+ },1763+ "node_modules/esbuild": {1764+ "version": "0.28.1",1765+ "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.1.tgz",1766+ "integrity": "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==",1767+ "dev": true,1768+ "hasInstallScript": true,1769+ "license": "MIT",1770+ "bin": {1771+ "esbuild": "bin/esbuild"1772+ },1773+ "engines": {1774+ "node": ">=18"1775+ },1776+ "optionalDependencies": {1777+ "@esbuild/aix-ppc64": "0.28.1",1778+ "@esbuild/android-arm": "0.28.1",1779+ "@esbuild/android-arm64": "0.28.1",1780+ "@esbuild/android-x64": "0.28.1",1781+ "@esbuild/darwin-arm64": "0.28.1",1782+ "@esbuild/darwin-x64": "0.28.1",1783+ "@esbuild/freebsd-arm64": "0.28.1",1784+ "@esbuild/freebsd-x64": "0.28.1",1785+ "@esbuild/linux-arm": "0.28.1",1786+ "@esbuild/linux-arm64": "0.28.1",1787+ "@esbuild/linux-ia32": "0.28.1",1788+ "@esbuild/linux-loong64": "0.28.1",1789+ "@esbuild/linux-mips64el": "0.28.1",1790+ "@esbuild/linux-ppc64": "0.28.1",1791+ "@esbuild/linux-riscv64": "0.28.1",1792+ "@esbuild/linux-s390x": "0.28.1",1793+ "@esbuild/linux-x64": "0.28.1",1794+ "@esbuild/netbsd-arm64": "0.28.1",1795+ "@esbuild/netbsd-x64": "0.28.1",1796+ "@esbuild/openbsd-arm64": "0.28.1",1797+ "@esbuild/openbsd-x64": "0.28.1",1798+ "@esbuild/openharmony-arm64": "0.28.1",1799+ "@esbuild/sunos-x64": "0.28.1",1800+ "@esbuild/win32-arm64": "0.28.1",1801+ "@esbuild/win32-ia32": "0.28.1",1802+ "@esbuild/win32-x64": "0.28.1"1803+ }1804+ },1805+ "node_modules/estree-walker": {1806+ "version": "3.0.3",1807+ "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz",1808+ "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==",1809+ "dev": true,1810+ "license": "MIT",1811+ "dependencies": {1812+ "@types/estree": "^1.0.0"1813+ }1814+ },1815+ "node_modules/expect-type": {1816+ "version": "1.4.0",1817+ "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz",1818+ "integrity": "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==",1819+ "dev": true,1820+ "license": "Apache-2.0",1821+ "engines": {1822+ "node": ">=12.0.0"1823+ }1824+ },1825+ "node_modules/fdir": {1826+ "version": "6.5.0",1827+ "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",1828+ "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",1829+ "dev": true,1830+ "license": "MIT",1831+ "engines": {1832+ "node": ">=12.0.0"1833+ },1834+ "peerDependencies": {1835+ "picomatch": "^3 || ^4"1836+ },1837+ "peerDependenciesMeta": {1838+ "picomatch": {1839+ "optional": true1840+ }1841+ }1842+ },1843+ "node_modules/fsevents": {1844+ "version": "2.3.3",1845+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",1846+ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",1847+ "dev": true,1848+ "hasInstallScript": true,1849+ "license": "MIT",1850+ "optional": true,1851+ "os": [1852+ "darwin"1853+ ],1854+ "engines": {1855+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"1856+ }1857+ },1858+ "node_modules/kleur": {1859+ "version": "4.1.5",1860+ "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz",1861+ "integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==",1862+ "dev": true,1863+ "license": "MIT",1864+ "engines": {1865+ "node": ">=6"1866+ }1867+ },1868+ "node_modules/lightningcss": {1869+ "version": "1.32.0",1870+ "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz",1871+ "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==",1872+ "dev": true,1873+ "license": "MPL-2.0",1874+ "dependencies": {1875+ "detect-libc": "^2.0.3"1876+ },1877+ "engines": {1878+ "node": ">= 12.0.0"1879+ },1880+ "funding": {1881+ "type": "opencollective",1882+ "url": "https://opencollective.com/parcel"1883+ },1884+ "optionalDependencies": {1885+ "lightningcss-android-arm64": "1.32.0",1886+ "lightningcss-darwin-arm64": "1.32.0",1887+ "lightningcss-darwin-x64": "1.32.0",1888+ "lightningcss-freebsd-x64": "1.32.0",1889+ "lightningcss-linux-arm-gnueabihf": "1.32.0",1890+ "lightningcss-linux-arm64-gnu": "1.32.0",1891+ "lightningcss-linux-arm64-musl": "1.32.0",1892+ "lightningcss-linux-x64-gnu": "1.32.0",1893+ "lightningcss-linux-x64-musl": "1.32.0",1894+ "lightningcss-win32-arm64-msvc": "1.32.0",1895+ "lightningcss-win32-x64-msvc": "1.32.0"1896+ }1897+ },1898+ "node_modules/lightningcss-android-arm64": {1899+ "version": "1.32.0",1900+ "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz",1901+ "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==",1902+ "cpu": [1903+ "arm64"1904+ ],1905+ "dev": true,1906+ "license": "MPL-2.0",1907+ "optional": true,1908+ "os": [1909+ "android"1910+ ],1911+ "engines": {1912+ "node": ">= 12.0.0"1913+ },1914+ "funding": {1915+ "type": "opencollective",1916+ "url": "https://opencollective.com/parcel"1917+ }1918+ },1919+ "node_modules/lightningcss-darwin-arm64": {1920+ "version": "1.32.0",1921+ "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz",1922+ "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==",1923+ "cpu": [1924+ "arm64"1925+ ],1926+ "dev": true,1927+ "license": "MPL-2.0",1928+ "optional": true,1929+ "os": [1930+ "darwin"1931+ ],1932+ "engines": {1933+ "node": ">= 12.0.0"1934+ },1935+ "funding": {1936+ "type": "opencollective",1937+ "url": "https://opencollective.com/parcel"1938+ }1939+ },1940+ "node_modules/lightningcss-darwin-x64": {1941+ "version": "1.32.0",1942+ "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz",1943+ "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==",1944+ "cpu": [1945+ "x64"1946+ ],1947+ "dev": true,1948+ "license": "MPL-2.0",1949+ "optional": true,1950+ "os": [1951+ "darwin"1952+ ],1953+ "engines": {1954+ "node": ">= 12.0.0"1955+ },1956+ "funding": {1957+ "type": "opencollective",1958+ "url": "https://opencollective.com/parcel"1959+ }1960+ },1961+ "node_modules/lightningcss-freebsd-x64": {1962+ "version": "1.32.0",1963+ "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz",1964+ "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==",1965+ "cpu": [1966+ "x64"1967+ ],1968+ "dev": true,1969+ "license": "MPL-2.0",1970+ "optional": true,1971+ "os": [1972+ "freebsd"1973+ ],1974+ "engines": {1975+ "node": ">= 12.0.0"1976+ },1977+ "funding": {1978+ "type": "opencollective",1979+ "url": "https://opencollective.com/parcel"1980+ }1981+ },1982+ "node_modules/lightningcss-linux-arm-gnueabihf": {1983+ "version": "1.32.0",1984+ "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz",1985+ "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==",1986+ "cpu": [1987+ "arm"1988+ ],1989+ "dev": true,1990+ "license": "MPL-2.0",1991+ "optional": true,1992+ "os": [1993+ "linux"1994+ ],1995+ "engines": {1996+ "node": ">= 12.0.0"1997+ },1998+ "funding": {1999+ "type": "opencollective",2000+ "url": "https://opencollective.com/parcel"2001+ }2002+ },2003+ "node_modules/lightningcss-linux-arm64-gnu": {2004+ "version": "1.32.0",2005+ "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz",2006+ "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==",2007+ "cpu": [2008+ "arm64"2009+ ],2010+ "dev": true,2011+ "license": "MPL-2.0",2012+ "optional": true,2013+ "os": [2014+ "linux"2015+ ],2016+ "engines": {2017+ "node": ">= 12.0.0"2018+ },2019+ "funding": {2020+ "type": "opencollective",2021+ "url": "https://opencollective.com/parcel"2022+ }2023+ },2024+ "node_modules/lightningcss-linux-arm64-musl": {2025+ "version": "1.32.0",2026+ "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz",2027+ "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==",2028+ "cpu": [2029+ "arm64"2030+ ],2031+ "dev": true,2032+ "license": "MPL-2.0",2033+ "optional": true,2034+ "os": [2035+ "linux"2036+ ],2037+ "engines": {2038+ "node": ">= 12.0.0"2039+ },2040+ "funding": {2041+ "type": "opencollective",2042+ "url": "https://opencollective.com/parcel"2043+ }2044+ },2045+ "node_modules/lightningcss-linux-x64-gnu": {2046+ "version": "1.32.0",2047+ "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz",2048+ "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==",2049+ "cpu": [2050+ "x64"2051+ ],2052+ "dev": true,2053+ "license": "MPL-2.0",2054+ "optional": true,2055+ "os": [2056+ "linux"2057+ ],2058+ "engines": {2059+ "node": ">= 12.0.0"2060+ },2061+ "funding": {2062+ "type": "opencollective",2063+ "url": "https://opencollective.com/parcel"2064+ }2065+ },2066+ "node_modules/lightningcss-linux-x64-musl": {2067+ "version": "1.32.0",2068+ "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz",2069+ "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==",2070+ "cpu": [2071+ "x64"2072+ ],2073+ "dev": true,2074+ "license": "MPL-2.0",2075+ "optional": true,2076+ "os": [2077+ "linux"2078+ ],2079+ "engines": {2080+ "node": ">= 12.0.0"2081+ },2082+ "funding": {2083+ "type": "opencollective",2084+ "url": "https://opencollective.com/parcel"2085+ }2086+ },2087+ "node_modules/lightningcss-win32-arm64-msvc": {2088+ "version": "1.32.0",2089+ "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz",2090+ "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==",2091+ "cpu": [2092+ "arm64"2093+ ],2094+ "dev": true,2095+ "license": "MPL-2.0",2096+ "optional": true,2097+ "os": [2098+ "win32"2099+ ],2100+ "engines": {2101+ "node": ">= 12.0.0"2102+ },2103+ "funding": {2104+ "type": "opencollective",2105+ "url": "https://opencollective.com/parcel"2106+ }2107+ },2108+ "node_modules/lightningcss-win32-x64-msvc": {2109+ "version": "1.32.0",2110+ "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz",2111+ "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==",2112+ "cpu": [2113+ "x64"2114+ ],2115+ "dev": true,2116+ "license": "MPL-2.0",2117+ "optional": true,2118+ "os": [2119+ "win32"2120+ ],2121+ "engines": {2122+ "node": ">= 12.0.0"2123+ },2124+ "funding": {2125+ "type": "opencollective",2126+ "url": "https://opencollective.com/parcel"2127+ }2128+ },2129+ "node_modules/magic-string": {2130+ "version": "0.30.21",2131+ "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",2132+ "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==",2133+ "dev": true,2134+ "license": "MIT",2135+ "dependencies": {2136+ "@jridgewell/sourcemap-codec": "^1.5.5"2137+ }2138+ },2139+ "node_modules/miniflare": {2140+ "version": "4.20260708.1",2141+ "resolved": "https://registry.npmjs.org/miniflare/-/miniflare-4.20260708.1.tgz",2142+ "integrity": "sha512-c94O9zRDISdqO18EHt6l0iF/fWgWt8p18PJvRsA/L/NJZ9Cfke3s/F5Blg1XXF7WDutVRzWVWy8Vy4LaT5ifsA==",2143+ "dev": true,2144+ "license": "MIT",2145+ "dependencies": {2146+ "@cspotcode/source-map-support": "0.8.1",2147+ "sharp": "0.34.5",2148+ "undici": "7.28.0",2149+ "workerd": "1.20260708.1",2150+ "ws": "8.21.0",2151+ "youch": "4.1.0-beta.10"2152+ },2153+ "bin": {2154+ "miniflare": "bootstrap.js"2155+ },2156+ "engines": {2157+ "node": ">=22.0.0"2158+ }2159+ },2160+ "node_modules/nanoid": {2161+ "version": "3.3.15",2162+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.15.tgz",2163+ "integrity": "sha512-y7Wygv/7mEOvxTuEQDB8StXdMRBWf1kR/tlhAzBRUFkB2jfcLOAxO/SHmOO2zgz1pVgK29/kyupn059/bCHdjA==",2164+ "dev": true,2165+ "funding": [2166+ {2167+ "type": "github",2168+ "url": "https://github.com/sponsors/ai"2169+ }2170+ ],2171+ "license": "MIT",2172+ "bin": {2173+ "nanoid": "bin/nanoid.cjs"2174+ },2175+ "engines": {2176+ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"2177+ }2178+ },2179+ "node_modules/obug": {2180+ "version": "2.1.3",2181+ "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.3.tgz",2182+ "integrity": "sha512-9miFgM2OFba7hB+pRgvtV84pYTBaoTHohvmIgiRt6dRIzbwEOIaNaP+dIlGs2fNFoB0SeISs0Jz5WFVRid6Xyg==",2183+ "dev": true,2184+ "funding": [2185+ "https://github.com/sponsors/sxzz",2186+ "https://opencollective.com/debug"2187+ ],2188+ "license": "MIT",2189+ "engines": {2190+ "node": ">=12.20.0"2191+ }2192+ },2193+ "node_modules/path-to-regexp": {2194+ "version": "6.3.0",2195+ "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.3.0.tgz",2196+ "integrity": "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==",2197+ "dev": true,2198+ "license": "MIT"2199+ },2200+ "node_modules/pathe": {2201+ "version": "2.0.3",2202+ "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz",2203+ "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==",2204+ "dev": true,2205+ "license": "MIT"2206+ },2207+ "node_modules/picocolors": {2208+ "version": "1.1.1",2209+ "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",2210+ "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",2211+ "dev": true,2212+ "license": "ISC"2213+ },2214+ "node_modules/picomatch": {2215+ "version": "4.0.5",2216+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz",2217+ "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==",2218+ "dev": true,2219+ "license": "MIT",2220+ "engines": {2221+ "node": ">=12"2222+ },2223+ "funding": {2224+ "url": "https://github.com/sponsors/jonschlinkert"2225+ }2226+ },2227+ "node_modules/postcss": {2228+ "version": "8.5.17",2229+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.17.tgz",2230+ "integrity": "sha512-J7EF+8X+CzRPaJPOv9Ck2wNWJvGnnl3PcNPAdGg6GTLjyVpyQ0yATMSXRFRV01BviT/9Gwuc3rjEyJbDJG9a4w==",2231+ "dev": true,2232+ "funding": [2233+ {2234+ "type": "opencollective",2235+ "url": "https://opencollective.com/postcss/"2236+ },2237+ {2238+ "type": "tidelift",2239+ "url": "https://tidelift.com/funding/github/npm/postcss"2240+ },2241+ {2242+ "type": "github",2243+ "url": "https://github.com/sponsors/ai"2244+ }2245+ ],2246+ "license": "MIT",2247+ "dependencies": {2248+ "nanoid": "^3.3.12",2249+ "picocolors": "^1.1.1",2250+ "source-map-js": "^1.2.1"2251+ },2252+ "engines": {2253+ "node": "^10 || ^12 || >=14"2254+ }2255+ },2256+ "node_modules/rolldown": {2257+ "version": "1.1.5",2258+ "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.1.5.tgz",2259+ "integrity": "sha512-t9z29cJjXf/vxQ8dyhCSpt6H6aSwHTk8cT5I3iy6SMXuFpk5mB6PL6XfC8PCwrPTx93udwKUm9HRteAlTGBLiA==",2260+ "dev": true,2261+ "license": "MIT",2262+ "dependencies": {2263+ "@oxc-project/types": "=0.139.0",2264+ "@rolldown/pluginutils": "^1.0.0"2265+ },2266+ "bin": {2267+ "rolldown": "bin/cli.mjs"2268+ },2269+ "engines": {2270+ "node": "^20.19.0 || >=22.12.0"2271+ },2272+ "optionalDependencies": {2273+ "@rolldown/binding-android-arm64": "1.1.5",2274+ "@rolldown/binding-darwin-arm64": "1.1.5",2275+ "@rolldown/binding-darwin-x64": "1.1.5",2276+ "@rolldown/binding-freebsd-x64": "1.1.5",2277+ "@rolldown/binding-linux-arm-gnueabihf": "1.1.5",2278+ "@rolldown/binding-linux-arm64-gnu": "1.1.5",2279+ "@rolldown/binding-linux-arm64-musl": "1.1.5",2280+ "@rolldown/binding-linux-ppc64-gnu": "1.1.5",2281+ "@rolldown/binding-linux-s390x-gnu": "1.1.5",2282+ "@rolldown/binding-linux-x64-gnu": "1.1.5",2283+ "@rolldown/binding-linux-x64-musl": "1.1.5",2284+ "@rolldown/binding-openharmony-arm64": "1.1.5",2285+ "@rolldown/binding-wasm32-wasi": "1.1.5",2286+ "@rolldown/binding-win32-arm64-msvc": "1.1.5",2287+ "@rolldown/binding-win32-x64-msvc": "1.1.5"2288+ }2289+ },2290+ "node_modules/semver": {2291+ "version": "7.8.5",2292+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz",2293+ "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==",2294+ "dev": true,2295+ "license": "ISC",2296+ "bin": {2297+ "semver": "bin/semver.js"2298+ },2299+ "engines": {2300+ "node": ">=10"2301+ }2302+ },2303+ "node_modules/sharp": {2304+ "version": "0.34.5",13232305 "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz",13242306 "integrity": "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==",13252307 "dev": true,@@ -1363,6 +2345,37 @@
13632345 "@img/sharp-win32-x64": "0.34.5"13642346 }13652347 },2348+ "node_modules/siginfo": {2349+ "version": "2.0.0",2350+ "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz",2351+ "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==",2352+ "dev": true,2353+ "license": "ISC"2354+ },2355+ "node_modules/source-map-js": {2356+ "version": "1.2.1",2357+ "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",2358+ "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",2359+ "dev": true,2360+ "license": "BSD-3-Clause",2361+ "engines": {2362+ "node": ">=0.10.0"2363+ }2364+ },2365+ "node_modules/stackback": {2366+ "version": "0.0.2",2367+ "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz",2368+ "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==",2369+ "dev": true,2370+ "license": "MIT"2371+ },2372+ "node_modules/std-env": {2373+ "version": "4.2.0",2374+ "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.2.0.tgz",2375+ "integrity": "sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw==",2376+ "dev": true,2377+ "license": "MIT"2378+ },13662379 "node_modules/supports-color": {13672380 "version": "10.2.2",13682381 "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-10.2.2.tgz",@@ -1376,6 +2389,50 @@
13762389 "url": "https://github.com/chalk/supports-color?sponsor=1"13772390 }13782391 },2392+ "node_modules/tinybench": {2393+ "version": "2.9.0",2394+ "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz",2395+ "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==",2396+ "dev": true,2397+ "license": "MIT"2398+ },2399+ "node_modules/tinyexec": {2400+ "version": "1.2.4",2401+ "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.2.4.tgz",2402+ "integrity": "sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg==",2403+ "dev": true,2404+ "license": "MIT",2405+ "engines": {2406+ "node": ">=18"2407+ }2408+ },2409+ "node_modules/tinyglobby": {2410+ "version": "0.2.17",2411+ "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz",2412+ "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==",2413+ "dev": true,2414+ "license": "MIT",2415+ "dependencies": {2416+ "fdir": "^6.5.0",2417+ "picomatch": "^4.0.4"2418+ },2419+ "engines": {2420+ "node": ">=12.0.0"2421+ },2422+ "funding": {2423+ "url": "https://github.com/sponsors/SuperchupuDev"2424+ }2425+ },2426+ "node_modules/tinyrainbow": {2427+ "version": "3.1.0",2428+ "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.0.tgz",2429+ "integrity": "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==",2430+ "dev": true,2431+ "license": "MIT",2432+ "engines": {2433+ "node": ">=14.0.0"2434+ }2435+ },13792436 "node_modules/tslib": {13802437 "version": "2.8.1",13812438 "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",@@ -1408,6 +2465,13 @@
14082465 "node": ">=20.18.1"14092466 }14102467 },2468+ "node_modules/undici-types": {2469+ "version": "8.3.0",2470+ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.3.0.tgz",2471+ "integrity": "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==",2472+ "dev": true,2473+ "license": "MIT"2474+ },14112475 "node_modules/unenv": {14122476 "version": "2.0.0-rc.24",14132477 "resolved": "https://registry.npmjs.org/unenv/-/unenv-2.0.0-rc.24.tgz",@@ -1418,6 +2482,191 @@
14182482 "pathe": "^2.0.3"14192483 }14202484 },2485+ "node_modules/vite": {2486+ "version": "8.1.4",2487+ "resolved": "https://registry.npmjs.org/vite/-/vite-8.1.4.tgz",2488+ "integrity": "sha512-bTT9PsdWO+MQMNG9ZXIP/qM9wGh37DFxTV/sPq9cFpHr3w4jkgef032PkAL9jAqhk3Nz8NQw3O8n6/xFkqO4QQ==",2489+ "dev": true,2490+ "license": "MIT",2491+ "dependencies": {2492+ "lightningcss": "^1.32.0",2493+ "picomatch": "^4.0.5",2494+ "postcss": "^8.5.16",2495+ "rolldown": "~1.1.4",2496+ "tinyglobby": "^0.2.17"2497+ },2498+ "bin": {2499+ "vite": "bin/vite.js"2500+ },2501+ "engines": {2502+ "node": "^20.19.0 || >=22.12.0"2503+ },2504+ "funding": {2505+ "url": "https://github.com/vitejs/vite?sponsor=1"2506+ },2507+ "optionalDependencies": {2508+ "fsevents": "~2.3.3"2509+ },2510+ "peerDependencies": {2511+ "@types/node": "^20.19.0 || >=22.12.0",2512+ "@vitejs/devtools": "^0.3.0",2513+ "esbuild": "^0.27.0 || ^0.28.0",2514+ "jiti": ">=1.21.0",2515+ "less": "^4.0.0",2516+ "sass": "^1.70.0",2517+ "sass-embedded": "^1.70.0",2518+ "stylus": ">=0.54.8",2519+ "sugarss": "^5.0.0",2520+ "terser": "^5.16.0",2521+ "tsx": "^4.8.1",2522+ "yaml": "^2.4.2"2523+ },2524+ "peerDependenciesMeta": {2525+ "@types/node": {2526+ "optional": true2527+ },2528+ "@vitejs/devtools": {2529+ "optional": true2530+ },2531+ "esbuild": {2532+ "optional": true2533+ },2534+ "jiti": {2535+ "optional": true2536+ },2537+ "less": {2538+ "optional": true2539+ },2540+ "sass": {2541+ "optional": true2542+ },2543+ "sass-embedded": {2544+ "optional": true2545+ },2546+ "stylus": {2547+ "optional": true2548+ },2549+ "sugarss": {2550+ "optional": true2551+ },2552+ "terser": {2553+ "optional": true2554+ },2555+ "tsx": {2556+ "optional": true2557+ },2558+ "yaml": {2559+ "optional": true2560+ }2561+ }2562+ },2563+ "node_modules/vitest": {2564+ "version": "4.1.10",2565+ "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.10.tgz",2566+ "integrity": "sha512-R9jUTe5S4Qb0HCd4TNqpC7oGcrMssMRGXLW80ubjWsW9VH5GF8y1Y0SFLY9AbqSk6nt0PnOx4H4WNJYZ13GUPw==",2567+ "dev": true,2568+ "license": "MIT",2569+ "dependencies": {2570+ "@vitest/expect": "4.1.10",2571+ "@vitest/mocker": "4.1.10",2572+ "@vitest/pretty-format": "4.1.10",2573+ "@vitest/runner": "4.1.10",2574+ "@vitest/snapshot": "4.1.10",2575+ "@vitest/spy": "4.1.10",2576+ "@vitest/utils": "4.1.10",2577+ "es-module-lexer": "^2.0.0",2578+ "expect-type": "^1.3.0",2579+ "magic-string": "^0.30.21",2580+ "obug": "^2.1.1",2581+ "pathe": "^2.0.3",2582+ "picomatch": "^4.0.3",2583+ "std-env": "^4.0.0-rc.1",2584+ "tinybench": "^2.9.0",2585+ "tinyexec": "^1.0.2",2586+ "tinyglobby": "^0.2.15",2587+ "tinyrainbow": "^3.1.0",2588+ "vite": "^6.0.0 || ^7.0.0 || ^8.0.0",2589+ "why-is-node-running": "^2.3.0"2590+ },2591+ "bin": {2592+ "vitest": "vitest.mjs"2593+ },2594+ "engines": {2595+ "node": "^20.0.0 || ^22.0.0 || >=24.0.0"2596+ },2597+ "funding": {2598+ "url": "https://opencollective.com/vitest"2599+ },2600+ "peerDependencies": {2601+ "@edge-runtime/vm": "*",2602+ "@opentelemetry/api": "^1.9.0",2603+ "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0",2604+ "@vitest/browser-playwright": "4.1.10",2605+ "@vitest/browser-preview": "4.1.10",2606+ "@vitest/browser-webdriverio": "4.1.10",2607+ "@vitest/coverage-istanbul": "4.1.10",2608+ "@vitest/coverage-v8": "4.1.10",2609+ "@vitest/ui": "4.1.10",2610+ "happy-dom": "*",2611+ "jsdom": "*",2612+ "vite": "^6.0.0 || ^7.0.0 || ^8.0.0"2613+ },2614+ "peerDependenciesMeta": {2615+ "@edge-runtime/vm": {2616+ "optional": true2617+ },2618+ "@opentelemetry/api": {2619+ "optional": true2620+ },2621+ "@types/node": {2622+ "optional": true2623+ },2624+ "@vitest/browser-playwright": {2625+ "optional": true2626+ },2627+ "@vitest/browser-preview": {2628+ "optional": true2629+ },2630+ "@vitest/browser-webdriverio": {2631+ "optional": true2632+ },2633+ "@vitest/coverage-istanbul": {2634+ "optional": true2635+ },2636+ "@vitest/coverage-v8": {2637+ "optional": true2638+ },2639+ "@vitest/ui": {2640+ "optional": true2641+ },2642+ "happy-dom": {2643+ "optional": true2644+ },2645+ "jsdom": {2646+ "optional": true2647+ },2648+ "vite": {2649+ "optional": false2650+ }2651+ }2652+ },2653+ "node_modules/why-is-node-running": {2654+ "version": "2.3.0",2655+ "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz",2656+ "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==",2657+ "dev": true,2658+ "license": "MIT",2659+ "dependencies": {2660+ "siginfo": "^2.0.0",2661+ "stackback": "0.0.2"2662+ },2663+ "bin": {2664+ "why-is-node-running": "cli.js"2665+ },2666+ "engines": {2667+ "node": ">=8"2668+ }2669+ },14212670 "node_modules/workerd": {14222671 "version": "1.20260708.1",14232672 "resolved": "https://registry.npmjs.org/workerd/-/workerd-1.20260708.1.tgz",@@ -1440,9 +2689,9 @@
14402689 }14412690 },14422691 "node_modules/wrangler": {1443- "version": "4.109.0",1444- "resolved": "https://registry.npmjs.org/wrangler/-/wrangler-4.109.0.tgz",1445- "integrity": "sha512-Ev6iIBNnRwvYeJv8B7377YsnY+PxEznHmz3r33cWgP+6A5HNqqqJhChBaVUWxxs0uQPKuYBy8tqrfyd7o8WmBg==",2692+ "version": "4.110.0",2693+ "resolved": "https://registry.npmjs.org/wrangler/-/wrangler-4.110.0.tgz",2694+ "integrity": "sha512-xZeXKYi7hxQRF5anL+v77RkufJNpF9f3Eqeyqq2QBsETpLZgh0Agj0jJ6JPtkbgn6ukZdh8OK5egsGPWIditgg==",14462695 "dev": true,14472696 "license": "MIT OR Apache-2.0",14482697 "dependencies": {@@ -1450,7 +2699,7 @@
14502699 "@cloudflare/unenv-preset": "2.16.1",14512700 "blake3-wasm": "2.1.5",14522701 "esbuild": "0.28.1",1453- "miniflare": "4.20260708.0",2702+ "miniflare": "4.20260708.1",14542703 "path-to-regexp": "6.3.0",14552704 "unenv": "2.0.0-rc.24",14562705 "workerd": "1.20260708.1"@@ -1521,6 +2770,16 @@
15212770 "@poppinss/exception": "^1.2.2",15222771 "error-stack-parser-es": "^1.0.5"15232772 }2773+ },2774+ "node_modules/zod": {2775+ "version": "3.25.76",2776+ "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz",2777+ "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==",2778+ "dev": true,2779+ "license": "MIT",2780+ "funding": {2781+ "url": "https://github.com/sponsors/colinhacks"2782+ }15242783 }15252784 }15262785 }@@ -7,12 +7,17 @@
77 "dev:local": "wrangler dev --config wrangler.local.jsonc",88 "deploy": "wrangler deploy",99 "deploy:local": "wrangler deploy --config wrangler.local.jsonc",10- "check": "tsc --noEmit",11- "test": "node --experimental-strip-types --test test/*.test.mjs"10+ "check": "tsc --noEmit && wrangler types --check worker-configuration.d.ts",11+ "test": "npm run test:unit && npm run test:runtime",12+ "test:unit": "node --experimental-strip-types --test test/*.test.mjs",13+ "test:runtime": "vitest run",14+ "types": "wrangler types worker-configuration.d.ts"1215 },1316 "devDependencies": {14- "@cloudflare/workers-types": "^5.20260708.1",15- "typescript": "^5.8.0",16- "wrangler": "^4.0.0"17+ "@cloudflare/vitest-pool-workers": "^0.18.4",18+ "@types/node": "^26.1.1",19+ "typescript": "^5.9.3",20+ "vitest": "^4.1.10",21+ "wrangler": "^4.110.0"1722 }1823 }@@ -15,7 +15,7 @@ CREATE TABLE IF NOT EXISTS sites (
1515 created_at INTEGER NOT NULL,1616 updated_at INTEGER NOT NULL,1717 expires_at INTEGER, -- unix seconds; NULL = permanent18- password_hash TEXT, -- reserved for password shares; NULL = public18+ password_hash TEXT, -- PBKDF2 password verifier; NULL = public1919 auth_version INTEGER NOT NULL DEFAULT 1, -- increment to revoke unlock cookies2020 UNIQUE (vault_slot, alias)2121 );@@ -62,3 +62,21 @@ CREATE TABLE IF NOT EXISTS security_incidents (
6262 total_rate_limited INTEGER NOT NULL,6363 vault_mask INTEGER NOT NULL6464 );65+66+CREATE TABLE IF NOT EXISTS security_notifications (67+ id TEXT PRIMARY KEY,68+ incident_name TEXT NOT NULL,69+ action TEXT NOT NULL,70+ window_bucket INTEGER NOT NULL,71+ subject TEXT NOT NULL,72+ text TEXT NOT NULL,73+ html TEXT,74+ created_at INTEGER NOT NULL,75+ sent_at INTEGER,76+ attempts INTEGER NOT NULL DEFAULT 0,77+ last_error TEXT78+);79+80+CREATE INDEX IF NOT EXISTS idx_security_notifications_pending81+ ON security_notifications(created_at)82+ WHERE sent_at IS NULL;@@ -64,7 +64,9 @@ Click the verification link Cloudflare sends, then configure:
6464 }6565 ```6666 67-Apply `migrations/0002_security_alerts.sql` before deploying an upgraded Worker.67+Apply `migrations/0002_security_alerts.sql` and68+`migrations/0003_security_notification_outbox.sql` before deploying an upgraded69+Worker.6870 After deployment, send a delivery test through the owner-authenticated endpoint:6971 7072 ```sh@@ -78,9 +80,11 @@ at least 90% misses. A rate-limit hit or a suspicious live-address hit confirms
7880 the incident. Duplicate email is suppressed unless severity increases or volume7981 doubles, active incidents summarize hourly, and three quiet windows (15 minutes)8082 resolve the incident. Probe rows are deduplicated by scanner/address, capped at81-30 per scanner per minute in each Cloudflare location, contain no raw IP, and are82-pruned after seven days. A daily activity digest is sent only when probes83-occurred in the preceding 24 hours.83+30 per scanner per minute in each Cloudflare location; 429 confirmations have a84+separate one-per-minute persistence cap. Alert payloads are written to a D185+outbox before delivery and retried by later cron runs with a stable notification86+ID. Telemetry contains no raw IP and is pruned after seven days. A daily activity87+digest is sent only when probes occurred in the preceding 24 hours.8488 8589 ### Operational checks after deployment8690 @@ -104,6 +108,7 @@ apply its migration before deploying the new Worker:
104108 cd worker105109 npx wrangler d1 execute nzip --remote --file migrations/0001_auth_version.sql106110 npx wrangler d1 execute nzip --remote --file migrations/0002_security_alerts.sql111+npx wrangler d1 execute nzip --remote --file migrations/0003_security_notification_outbox.sql107112 ```108113 109114 > **Cron gotcha:** deploying the `triggers` block fails with a 403 (API error@@ -1,378 +1,55 @@
1-import {2- canonicalManifestBytes,3- formatAddress,4- isValidName,5- manifestHash,6- parseManifest,7- parseTarget,8- sha256hex,9- VAULT_SLOTS,10- vaultSlotOf,11-} from "../../shared/mod.ts";12-import type {13- CommitRequest,14- CommitResponse,15- Manifest,16- PatchSiteRequest,17- PrepareRequest,18- PrepareResponse,19- RevertRequest,20- SourceResponse,21- StatusResponse,22- Target,23-} from "../../shared/mod.ts";24-import {25- allocateAddress,26- commitSite,27- createVault,28- getSiteByAddress,29- getVaultByName,30- getVaultBySlot,31- listSites,32- listVaults,33- resolveTarget,34- siteHistory,35- siteRowToDetail,36- siteRowToInfo,37- type SiteRow,38-} from "./db.ts";39-import { type Env, err, json, siteUrl } from "./env.ts";40-import { purgeSiteCache } from "./cache.ts";41-import { hashPassword } from "./password.ts";1+import type { Env } from "./env.ts";2+import { json } from "./env.ts";3+import { ApiError, apiErrorResponse } from "./api/errors.ts";4+import { handleBlobPut, handleCommit, handlePrepare } from "./api/push.ts";5+import { handleSite, handleSiteList } from "./api/sites.ts";6+import { handleStatus } from "./api/status.ts";7+import { handleVaults } from "./api/vaults.ts";428 import { sendAlertTest } from "./security_alerts.ts";439 44-const HEX64 = /^[0-9a-f]{64}$/;45-const DEFAULT_TTL_DAYS = 14;46-const MAX_BLOB_BYTES = 50 * 1024 * 1024;47-const HEAD_CHUNK = 40; // stay under Worker subrequest limits on big bundles48-49-function ttlToExpiry(ttl: number | "forever" | undefined): number | null {50- if (ttl === "forever") return null;51- const days = ttl ?? DEFAULT_TTL_DAYS;52- if (!Number.isFinite(days) || days <= 0 || days > 3650) {53- throw new Error("ttl must be 1-3650 days or \"forever\"");54- }55- return Math.floor(Date.now() / 1000) + Math.round(days * 86400);56-}57-58-async function passwordHashFor(password: unknown): Promise<string | null | undefined> {59- if (password === undefined || password === null) return password;60- if (typeof password !== "string" || password.length < 4) {61- throw new Error("password must be at least 4 characters");62- }63- return await hashPassword(password);64-}65-66-/** Which blob hashes referenced by the manifest are absent from R2? */67-async function missingBlobs(env: Env, manifest: Manifest): Promise<string[]> {68- const hashes = [...new Set(Object.values(manifest.files).map((f) => f.h))];69- const missing: string[] = [];70- for (let i = 0; i < hashes.length; i += HEAD_CHUNK) {71- const chunk = hashes.slice(i, i + HEAD_CHUNK);72- const heads = await Promise.all(chunk.map((h) => env.CONTENT.head(`blob/${h}`)));73- heads.forEach((head, j) => {74- if (!head) missing.push(chunk[j]);75- });76- }77- return missing;78-}79-80-async function handlePrepare(req: Request, env: Env): Promise<Response> {81- const body = await req.json<PrepareRequest>();82- const hash = await manifestHash(body.manifest); // validates too83- return json<PrepareResponse>({ manifestHash: hash, missing: await missingBlobs(env, body.manifest) });84-}85-86-async function handleBlobPut(req: Request, env: Env, hash: string): Promise<Response> {87- if (!HEX64.test(hash)) return err("invalid blob hash", 400);88- const bytes = new Uint8Array(await req.arrayBuffer());89- if (bytes.length > MAX_BLOB_BYTES) return err("blob too large", 413);90- const actual = await sha256hex(bytes);91- if (actual !== hash) return err(`hash mismatch: body is ${actual}`, 400);92- await env.CONTENT.put(`blob/${hash}`, bytes);93- return json({ ok: true, hash });94-}95-96-async function handleCommit(req: Request, env: Env, ctx: ExecutionContext): Promise<Response> {97- const body = await req.json<CommitRequest>();98- const passwordHash = await passwordHashFor(body.password);99- const bytes = canonicalManifestBytes(body.manifest);100- const hash = await sha256hex(bytes);101-102- // Guard against crashed uploads: every referenced blob must exist before commit.103- const missing = await missingBlobs(env, body.manifest);104- if (missing.length > 0) {105- return err(`cannot commit: ${missing.length} blobs missing (run prepare again)`, 409);106- }107-108- let expiresAt: number | null;109- try {110- expiresAt = ttlToExpiry(body.ttl);111- } catch (e) {112- return err((e as Error).message, 400);113- }114-115- // Resolve target → (address, vaultSlot, alias, isNew)116- const t = body.target as Target;117- let address: number;118- let alias: string | null = null;119- let isNew: boolean;120- if ("address" in t) {121- const existing = await getSiteByAddress(env, t.address);122- const slot = vaultSlotOf(t.address);123- if (!(await getVaultBySlot(env, slot))) return err(`vault slot 0x${slot.toString(16)} not registered`, 404);124- address = t.address;125- alias = existing?.alias ?? null;126- isNew = !existing;127- } else {128- const vault = await getVaultByName(env, t.vault);129- if (!vault) return err(`unknown vault: ${t.vault}`, 404);130- alias = t.alias ?? null;131- if (alias !== null && !isValidName(alias)) return err(`invalid alias: ${alias}`, 400);132- const existing = alias === null ? null : await resolveTarget(env, { vault: t.vault, alias });133- if (existing) {134- address = existing.address;135- isNew = false;136- } else {137- address = await allocateAddress(env, vault.slot);138- isNew = true;139- }140- }141-142- if (!(await env.CONTENT.head(`manifest/${hash}`))) {143- await env.CONTENT.put(`manifest/${hash}`, bytes);144- }145- const seq = await commitSite(env, {146- address,147- vaultSlot: vaultSlotOf(address),148- alias,149- manifestHash: hash,150- expiresAt,151- passwordHash,152- isNew,153- });154-155- const addressStr = formatAddress(address);156- await purgeSiteCache(ctx, addressStr);157- return json<CommitResponse>({158- address: addressStr,159- url: siteUrl(env, addressStr),160- alias,161- manifestHash: hash,162- expiresAt,163- seq,164- });165-}166-167-/** Resolve a URL path target segment ("2a3f" | "work:demo") to a site row. */168-async function resolvePathTarget(env: Env, segment: string): Promise<SiteRow | null> {169- const parsed = parseTarget(decodeURIComponent(segment));170- if (parsed.kind === "address") return await resolveTarget(env, { address: parsed.address });171- if (parsed.kind === "vaultAlias") {172- return await resolveTarget(env, { vault: parsed.vault, alias: parsed.alias });173- }174- return null; // bare alias needs a default vault — the CLI resolves that client-side175-}176-177-async function handleStatus(env: Env): Promise<Response> {178- const vaults = await listVaults(env);179- const sites = await listSites(env);180- const soon = Math.floor(Date.now() / 1000) + 48 * 3600;181- return json<StatusResponse>({182- ok: true,183- version: "0.2.0",184- vaults,185- siteCount: sites.length,186- expiringSoon: sites.filter((s) => s.expires_at !== null && s.expires_at < soon).length,187- });188-}189-190-/** Load the current source manifest, refusing content that is no longer hosted. */191-async function sourceManifest(env: Env, site: SiteRow): Promise<Manifest | Response> {192- const now = Math.floor(Date.now() / 1000);193- if (site.expires_at !== null && site.expires_at < now) return err("site expired", 410);194- const object = await env.CONTENT.get(`manifest/${site.current_manifest}`);195- if (!object) return err("source manifest not found", 404);196- return parseManifest(new Uint8Array(await object.arrayBuffer()));197-}198-10+/** Authenticated management API router. Route handlers own validation and storage concerns. */19911 export async function api(200- req: Request,12+ request: Request,20113 env: Env,20214 url: URL,20315 ctx: ExecutionContext,20416 ): Promise<Response> {205- const parts = url.pathname.split("/").filter(Boolean); // ["api", ...]206- const method = req.method;17+ const parts = url.pathname.split("/").filter(Boolean);18+ const method = request.method;20719 20820 try {209- // push protocol21021 if (parts[1] === "push" && parts[2] === "prepare" && method === "POST") {211- return await handlePrepare(req, env);22+ return await handlePrepare(request, env);21223 }21324 if (parts[1] === "push" && parts[2] === "commit" && method === "POST") {214- return await handleCommit(req, env, ctx);25+ return await handleCommit(request, env, ctx);21526 }21627 if (parts[1] === "blob" && parts.length === 3 && method === "PUT") {217- return await handleBlobPut(req, env, parts[2]);28+ return await handleBlobPut(request, env, parts[2]);21829 }219-220- // status221- if (parts[1] === "status" && method === "GET") return await handleStatus(env);222- if (parts[1] === "security" && parts[2] === "test-alert" && method === "POST") {30+ if (parts[1] === "status" && parts.length === 2 && method === "GET") {31+ return await handleStatus(env);32+ }33+ if (34+ parts[1] === "security" && parts[2] === "test-alert" && method === "POST"35+ ) {22336 await sendAlertTest(env);22437 return json({ ok: true });22538 }226-227- // vaults228- if (parts[1] === "vaults" && parts.length === 2) {229- if (method === "GET") return json(await listVaults(env));230- if (method === "POST") {231- const body = await req.json<{ name: string; slot?: number }>();232- if (!isValidName(body.name)) return err("invalid vault name", 400);233- if (body.slot !== undefined && (body.slot < 0 || body.slot >= VAULT_SLOTS)) {234- return err("slot must be 0-15", 400);235- }236- try {237- const v = await createVault(env, body.name, body.slot);238- return json({ slot: v.slot, name: v.name, createdAt: v.created_at }, 201);239- } catch (e) {240- return err((e as Error).message, 409);241- }242- }39+ if (40+ parts[1] === "vaults" && parts.length === 2 &&41+ (method === "GET" || method === "POST")42+ ) {43+ return await handleVaults(request, env);24344 }244-245- // sites collection24645 if (parts[1] === "sites" && parts.length === 2 && method === "GET") {247- const vaultName = url.searchParams.get("vault");248- let slot: number | undefined;249- if (vaultName) {250- const vault = await getVaultByName(env, vaultName);251- if (!vault) return err(`unknown vault: ${vaultName}`, 404);252- slot = vault.slot;253- }254- const rows = await listSites(env, slot);255- return json(rows.map((r) => siteRowToInfo(env, r, r.vault_name)));46+ return await handleSiteList(url, env);25647 }257-258- // sites/{target}[/source[/blob-hash]|revert]25948 if (parts[1] === "sites" && parts.length >= 3) {260- const site = await resolvePathTarget(env, parts[2]);261- if (!site) return err("site not found", 404);262-263- if (parts.length === 4 && parts[3] === "source" && method === "GET") {264- const manifest = await sourceManifest(env, site);265- if (manifest instanceof Response) return manifest;266- return json<SourceResponse>({267- address: formatAddress(site.address),268- manifestHash: site.current_manifest,269- manifest,270- });271- }272- if (parts.length === 5 && parts[3] === "source" && method === "GET") {273- const hash = parts[4];274- if (!HEX64.test(hash)) return err("invalid blob hash", 400);275- const manifest = await sourceManifest(env, site);276- if (manifest instanceof Response) return manifest;277- const entry = Object.values(manifest.files).find((file) => file.h === hash);278- if (!entry) return err("source blob not found", 404);279- const blob = await env.CONTENT.get(`blob/${hash}`);280- if (!blob) return err("source blob not found", 404);281- return new Response(blob.body, {282- headers: {283- "content-type": "application/octet-stream",284- "content-length": String(entry.s),285- "cache-control": "no-store",286- },287- });288- }289-290- if (parts.length === 3 && method === "GET") {291- return json(await siteRowToDetail(env, site));292- }293- if (parts.length === 3 && method === "DELETE") {294- await env.DB.prepare("DELETE FROM sites WHERE address = ?").bind(site.address).run();295- const address = formatAddress(site.address);296- await purgeSiteCache(ctx, address);297- return json({ ok: true, address });298- }299- if (parts.length === 3 && method === "PATCH") {300- const body = await req.json<PatchSiteRequest>();301- const now = Math.floor(Date.now() / 1000);302- const expiresAt = body.ttl === undefined ? undefined : ttlToExpiry(body.ttl);303- const passwordHash = await passwordHashFor(body.password);304- const updates: string[] = [];305- const values: (string | number | null)[] = [];306- if (expiresAt !== undefined) {307- updates.push("expires_at = ?");308- values.push(expiresAt);309- }310- if (passwordHash !== undefined) {311- updates.push("password_hash = ?");312- values.push(passwordHash);313- updates.push("auth_version = auth_version + 1");314- }315- if (updates.length > 0) {316- updates.push("updated_at = ?");317- values.push(now, site.address);318- await env.DB.prepare(`UPDATE sites SET ${updates.join(", ")} WHERE address = ?`)319- .bind(...values).run();320- }321- const updated = await getSiteByAddress(env, site.address);322- await purgeSiteCache(ctx, formatAddress(site.address));323- return json(await siteRowToDetail(env, updated!));324- }325- if (parts.length === 4 && parts[3] === "revert" && method === "POST") {326- return await handleRevert(req, env, site, ctx);327- }49+ return await handleSite(request, env, parts, ctx);32850 }329-330- return err("not found", 404);331- } catch (e) {332- const msg = (e as Error).message ?? "internal error";333- // Validation errors from shared/ read as client mistakes.334- const status = /invalid|unsupported|too many|no files|must be/.test(msg) ? 400 : 500;335- return err(msg, status);336- }337-}338-339-async function handleRevert(340- req: Request,341- env: Env,342- site: SiteRow,343- ctx: ExecutionContext,344-): Promise<Response> {345- const body = await req.json<RevertRequest>().catch(() => ({} as RevertRequest));346- const history = await siteHistory(env, site.address); // newest first347-348- let target;349- if (body.toSeq !== undefined) {350- target = history.find((h) => h.seq === body.toSeq);351- if (!target) return err(`no push #${body.toSeq} in history`, 404);352- } else {353- // Default: most recent push whose manifest differs from what's live now.354- target = history.find((h) => h.manifestHash !== site.current_manifest);355- if (!target) return err("nothing to revert to", 409);51+ throw new ApiError(404, "not found");52+ } catch (error) {53+ return apiErrorResponse(error, request, url);35654 }357-358- const seq = await commitSite(env, {359- address: site.address,360- vaultSlot: site.vault_slot,361- alias: site.alias,362- manifestHash: target.manifestHash,363- expiresAt: site.expires_at,364- isNew: false,365- note: `revert to #${target.seq}`,366- });367-368- const addressStr = formatAddress(site.address);369- await purgeSiteCache(ctx, addressStr);370- return json({371- address: addressStr,372- url: siteUrl(env, addressStr),373- alias: site.alias,374- manifestHash: target.manifestHash,375- revertedTo: target.seq,376- seq,377- });37855 }@@ -0,0 +1,64 @@
1+import { parseManifest, parseTarget } from "../../../shared/mod.ts";2+import type { Manifest } from "../../../shared/mod.ts";3+import { resolveTarget, type SiteRow } from "../db.ts";4+import type { Env } from "../env.ts";5+import { hashPassword } from "../password.ts";6+import { ApiError, clientInput } from "./errors.ts";7+8+export const HEX64 = /^[0-9a-f]{64}$/;9+10+export function ttlToExpiry(11+ ttl: number | "forever" | undefined,12+): number | null {13+ if (ttl === "forever") return null;14+ const days = ttl ?? 14;15+ if (!Number.isFinite(days) || days <= 0 || days > 3650) {16+ throw new ApiError(400, 'ttl must be 1-3650 days or "forever"');17+ }18+ return Math.floor(Date.now() / 1000) + Math.round(days * 86400);19+}20+21+export async function passwordHashFor(22+ password: unknown,23+): Promise<string | null | undefined> {24+ if (password === undefined || password === null) return password;25+ if (26+ typeof password !== "string" || password.length < 4 || password.length > 25627+ ) {28+ throw new ApiError(400, "password must be 4-256 characters");29+ }30+ return await hashPassword(password);31+}32+33+export async function resolvePathTarget(34+ env: Env,35+ segment: string,36+): Promise<SiteRow | null> {37+ const parsed = await clientInput(() =>38+ parseTarget(decodeURIComponent(segment))39+ );40+ if (parsed.kind === "address") {41+ return await resolveTarget(env, { address: parsed.address });42+ }43+ if (parsed.kind === "vaultAlias") {44+ return await resolveTarget(env, {45+ vault: parsed.vault,46+ alias: parsed.alias,47+ });48+ }49+ return null;50+}51+52+export async function sourceManifest(53+ env: Env,54+ site: SiteRow,55+): Promise<Manifest> {56+ if (57+ site.expires_at !== null && site.expires_at < Math.floor(Date.now() / 1000)58+ ) {59+ throw new ApiError(410, "site expired");60+ }61+ const object = await env.CONTENT.get(`manifest/${site.current_manifest}`);62+ if (!object) throw new ApiError(404, "source manifest not found");63+ return parseManifest(new Uint8Array(await object.arrayBuffer()));64+}@@ -0,0 +1,45 @@
1+import { err } from "../env.ts";2+3+export class ApiError extends Error {4+ readonly status: number;5+6+ constructor(7+ status: number,8+ message: string,9+ options?: ErrorOptions,10+ ) {11+ super(message, options);12+ this.name = "ApiError";13+ this.status = status;14+ }15+}16+17+export async function readJson<T>(request: Request): Promise<T> {18+ try {19+ return await request.json<T>();20+ } catch (cause) {21+ throw new ApiError(400, "invalid JSON body", { cause });22+ }23+}24+25+export async function clientInput<T>(26+ operation: () => T | Promise<T>,27+): Promise<T> {28+ try {29+ return await operation();30+ } catch (cause) {31+ const message = cause instanceof Error ? cause.message : "invalid request";32+ throw new ApiError(400, message, { cause });33+ }34+}35+36+export function apiErrorResponse(error: unknown, request: Request, url: URL): Response {37+ if (error instanceof ApiError) return err(error.message, error.status);38+ console.error({39+ event: "api.unhandled_error",40+ method: request.method,41+ path: url.pathname,42+ error: error instanceof Error ? error.message : String(error),43+ });44+ return err("internal error", 500);45+}@@ -0,0 +1,165 @@
1+import {2+ canonicalManifestBytes,3+ formatAddress,4+ isValidName,5+ manifestHash,6+ MAX_BLOB_BYTES,7+ sha256hex,8+ vaultSlotOf,9+} from "../../../shared/mod.ts";10+import type {11+ CommitRequest,12+ CommitResponse,13+ Manifest,14+ PrepareRequest,15+ PrepareResponse,16+ Target,17+} from "../../../shared/mod.ts";18+import { purgeSiteCache } from "../cache.ts";19+import {20+ allocateAddress,21+ commitSite,22+ getSiteByAddress,23+ getVaultByName,24+ getVaultBySlot,25+ resolveTarget,26+} from "../db.ts";27+import { type Env, json, siteUrl } from "../env.ts";28+import { ApiError, clientInput, readJson } from "./errors.ts";29+import { HEX64, passwordHashFor, ttlToExpiry } from "./common.ts";30+31+const HEAD_BATCH_SIZE = 6;32+33+async function missingBlobs(env: Env, manifest: Manifest): Promise<string[]> {34+ const hashes = [35+ ...new Set(Object.values(manifest.files).map((file) => file.h)),36+ ];37+ const missing: string[] = [];38+ for (let i = 0; i < hashes.length; i += HEAD_BATCH_SIZE) {39+ const batch = hashes.slice(i, i + HEAD_BATCH_SIZE);40+ const heads = await Promise.all(41+ batch.map((hash) => env.CONTENT.head(`blob/${hash}`)),42+ );43+ heads.forEach((head, index) => {44+ if (!head) missing.push(batch[index]);45+ });46+ }47+ return missing;48+}49+50+export async function handlePrepare(51+ request: Request,52+ env: Env,53+): Promise<Response> {54+ const body = await readJson<PrepareRequest>(request);55+ const hash = await clientInput(() => manifestHash(body.manifest));56+ return json<PrepareResponse>({57+ manifestHash: hash,58+ missing: await missingBlobs(env, body.manifest),59+ });60+}61+62+export async function handleBlobPut(63+ request: Request,64+ env: Env,65+ hash: string,66+): Promise<Response> {67+ if (!HEX64.test(hash)) throw new ApiError(400, "invalid blob hash");68+ const contentLength = request.headers.get("content-length");69+ if (contentLength === null) {70+ throw new ApiError(411, "content-length required");71+ }72+ const declaredBytes = Number(contentLength);73+ if (!Number.isSafeInteger(declaredBytes) || declaredBytes < 0) {74+ throw new ApiError(400, "invalid content-length");75+ }76+ if (declaredBytes > MAX_BLOB_BYTES) throw new ApiError(413, "blob too large");77+ const bytes = new Uint8Array(await request.arrayBuffer());78+ if (bytes.length > MAX_BLOB_BYTES) throw new ApiError(413, "blob too large");79+ if (bytes.length !== declaredBytes) {80+ throw new ApiError(400, "content-length mismatch");81+ }82+ const actual = await sha256hex(bytes);83+ if (actual !== hash) {84+ throw new ApiError(400, `hash mismatch: body is ${actual}`);85+ }86+ await env.CONTENT.put(`blob/${hash}`, bytes);87+ return json({ ok: true, hash });88+}89+90+export async function handleCommit(91+ request: Request,92+ env: Env,93+ ctx: ExecutionContext,94+): Promise<Response> {95+ const body = await readJson<CommitRequest>(request);96+ const passwordHash = await passwordHashFor(body.password);97+ const bytes = await clientInput(() => canonicalManifestBytes(body.manifest));98+ const hash = await sha256hex(bytes);99+ const missing = await missingBlobs(env, body.manifest);100+ if (missing.length > 0) {101+ throw new ApiError(102+ 409,103+ `cannot commit: ${missing.length} blobs missing (run prepare again)`,104+ );105+ }106+107+ const expiresAt = ttlToExpiry(body.ttl);108+ const target = body.target as Target;109+ let address: number;110+ let alias: string | null = null;111+ let isNew: boolean;112+ if ("address" in target) {113+ const existing = await getSiteByAddress(env, target.address);114+ const slot = vaultSlotOf(target.address);115+ if (!(await getVaultBySlot(env, slot))) {116+ throw new ApiError(117+ 404,118+ `vault slot 0x${slot.toString(16)} not registered`,119+ );120+ }121+ address = target.address;122+ alias = existing?.alias ?? null;123+ isNew = !existing;124+ } else {125+ const vault = await getVaultByName(env, target.vault);126+ if (!vault) throw new ApiError(404, `unknown vault: ${target.vault}`);127+ alias = target.alias ?? null;128+ if (alias !== null && !isValidName(alias)) {129+ throw new ApiError(400, `invalid alias: ${alias}`);130+ }131+ const existing = alias === null132+ ? null133+ : await resolveTarget(env, { vault: target.vault, alias });134+ if (existing) {135+ address = existing.address;136+ isNew = false;137+ } else {138+ address = await allocateAddress(env, vault.slot);139+ isNew = true;140+ }141+ }142+143+ if (!(await env.CONTENT.head(`manifest/${hash}`))) {144+ await env.CONTENT.put(`manifest/${hash}`, bytes);145+ }146+ const seq = await commitSite(env, {147+ address,148+ vaultSlot: vaultSlotOf(address),149+ alias,150+ manifestHash: hash,151+ expiresAt,152+ passwordHash,153+ isNew,154+ });155+ const addressString = formatAddress(address);156+ await purgeSiteCache(ctx, addressString);157+ return json<CommitResponse>({158+ address: addressString,159+ url: siteUrl(env, addressString),160+ alias,161+ manifestHash: hash,162+ expiresAt,163+ seq,164+ });165+}@@ -0,0 +1,176 @@
1+import { formatAddress } from "../../../shared/mod.ts";2+import type {3+ PatchSiteRequest,4+ RevertRequest,5+ SourceResponse,6+} from "../../../shared/mod.ts";7+import { purgeSiteCache } from "../cache.ts";8+import {9+ commitSite,10+ getSiteByAddress,11+ getVaultByName,12+ listSites,13+ siteHistory,14+ type SiteRow,15+ siteRowToDetail,16+ siteRowToInfo,17+} from "../db.ts";18+import { type Env, json, siteUrl } from "../env.ts";19+import {20+ HEX64,21+ passwordHashFor,22+ resolvePathTarget,23+ sourceManifest,24+ ttlToExpiry,25+} from "./common.ts";26+import { ApiError, readJson } from "./errors.ts";27+28+export async function handleSiteList(url: URL, env: Env): Promise<Response> {29+ const vaultName = url.searchParams.get("vault");30+ let slot: number | undefined;31+ if (vaultName) {32+ const vault = await getVaultByName(env, vaultName);33+ if (!vault) throw new ApiError(404, `unknown vault: ${vaultName}`);34+ slot = vault.slot;35+ }36+ const rows = await listSites(env, slot);37+ return json(rows.map((row) => siteRowToInfo(env, row, row.vault_name)));38+}39+40+export async function handleSite(41+ request: Request,42+ env: Env,43+ parts: string[],44+ ctx: ExecutionContext,45+): Promise<Response> {46+ const site = await resolvePathTarget(env, parts[2]);47+ if (!site) throw new ApiError(404, "site not found");48+49+ if (parts.length === 4 && parts[3] === "source" && request.method === "GET") {50+ const manifest = await sourceManifest(env, site);51+ return json<SourceResponse>({52+ address: formatAddress(site.address),53+ manifestHash: site.current_manifest,54+ manifest,55+ });56+ }57+ if (parts.length === 5 && parts[3] === "source" && request.method === "GET") {58+ return await handleSourceBlob(env, site, parts[4]);59+ }60+ if (parts.length === 3 && request.method === "GET") {61+ return json(await siteRowToDetail(env, site));62+ }63+ if (parts.length === 3 && request.method === "DELETE") {64+ await env.DB.prepare("DELETE FROM sites WHERE address = ?").bind(65+ site.address,66+ ).run();67+ const address = formatAddress(site.address);68+ await purgeSiteCache(ctx, address);69+ return json({ ok: true, address });70+ }71+ if (parts.length === 3 && request.method === "PATCH") {72+ return await handlePatch(request, env, site, ctx);73+ }74+ if (75+ parts.length === 4 && parts[3] === "revert" && request.method === "POST"76+ ) {77+ return await handleRevert(request, env, site, ctx);78+ }79+ throw new ApiError(404, "not found");80+}81+82+async function handleSourceBlob(83+ env: Env,84+ site: SiteRow,85+ hash: string,86+): Promise<Response> {87+ if (!HEX64.test(hash)) throw new ApiError(400, "invalid blob hash");88+ const manifest = await sourceManifest(env, site);89+ const entry = Object.values(manifest.files).find((file) => file.h === hash);90+ if (!entry) throw new ApiError(404, "source blob not found");91+ const blob = await env.CONTENT.get(`blob/${hash}`);92+ if (!blob) throw new ApiError(404, "source blob not found");93+ return new Response(blob.body, {94+ headers: {95+ "content-type": "application/octet-stream",96+ "content-length": String(entry.s),97+ "cache-control": "no-store",98+ },99+ });100+}101+102+async function handlePatch(103+ request: Request,104+ env: Env,105+ site: SiteRow,106+ ctx: ExecutionContext,107+): Promise<Response> {108+ const body = await readJson<PatchSiteRequest>(request);109+ const expiresAt = body.ttl === undefined ? undefined : ttlToExpiry(body.ttl);110+ const passwordHash = await passwordHashFor(body.password);111+ const updates: string[] = [];112+ const values: (string | number | null)[] = [];113+ if (expiresAt !== undefined) {114+ updates.push("expires_at = ?");115+ values.push(expiresAt);116+ }117+ if (passwordHash !== undefined) {118+ updates.push("password_hash = ?", "auth_version = auth_version + 1");119+ values.push(passwordHash);120+ }121+ if (updates.length > 0) {122+ updates.push("updated_at = ?");123+ values.push(Math.floor(Date.now() / 1000), site.address);124+ await env.DB.prepare(125+ `UPDATE sites SET ${updates.join(", ")} WHERE address = ?`,126+ )127+ .bind(...values).run();128+ }129+ const updated = await getSiteByAddress(env, site.address);130+ if (!updated) throw new ApiError(404, "site not found");131+ await purgeSiteCache(ctx, formatAddress(site.address));132+ return json(await siteRowToDetail(env, updated));133+}134+135+async function handleRevert(136+ request: Request,137+ env: Env,138+ site: SiteRow,139+ ctx: ExecutionContext,140+): Promise<Response> {141+ const body = await readJson<RevertRequest>(request).catch((error) => {142+ if ((request.headers.get("content-length") ?? "0") === "0") {143+ return {} as RevertRequest;144+ }145+ throw error;146+ });147+ const history = await siteHistory(env, site.address);148+ const target = body.toSeq !== undefined149+ ? history.find((entry) => entry.seq === body.toSeq)150+ : history.find((entry) => entry.manifestHash !== site.current_manifest);151+ if (!target) {152+ if (body.toSeq !== undefined) {153+ throw new ApiError(404, `no push #${body.toSeq} in history`);154+ }155+ throw new ApiError(409, "nothing to revert to");156+ }157+ const seq = await commitSite(env, {158+ address: site.address,159+ vaultSlot: site.vault_slot,160+ alias: site.alias,161+ manifestHash: target.manifestHash,162+ expiresAt: site.expires_at,163+ isNew: false,164+ note: `revert to #${target.seq}`,165+ });166+ const address = formatAddress(site.address);167+ await purgeSiteCache(ctx, address);168+ return json({169+ address,170+ url: siteUrl(env, address),171+ alias: site.alias,172+ manifestHash: target.manifestHash,173+ revertedTo: target.seq,174+ seq,175+ });176+}@@ -0,0 +1,19 @@
1+import { VERSION } from "../../../shared/mod.ts";2+import type { StatusResponse } from "../../../shared/mod.ts";3+import { listSites, listVaults } from "../db.ts";4+import { type Env, json } from "../env.ts";5+6+export async function handleStatus(env: Env): Promise<Response> {7+ const vaults = await listVaults(env);8+ const sites = await listSites(env);9+ const soon = Math.floor(Date.now() / 1000) + 48 * 3600;10+ return json<StatusResponse>({11+ ok: true,12+ version: VERSION,13+ vaults,14+ siteCount: sites.length,15+ expiringSoon: sites.filter((site) =>16+ site.expires_at !== null && site.expires_at < soon17+ ).length,18+ });19+}@@ -0,0 +1,27 @@
1+import { isValidName, VAULT_SLOTS } from "../../../shared/mod.ts";2+import { createVault, listVaults } from "../db.ts";3+import { type Env, json } from "../env.ts";4+import { ApiError, readJson } from "./errors.ts";5+6+export async function handleVaults(7+ request: Request,8+ env: Env,9+): Promise<Response> {10+ if (request.method === "GET") return json(await listVaults(env));11+ const body = await readJson<{ name: string; slot?: number }>(request);12+ if (!isValidName(body.name)) throw new ApiError(400, "invalid vault name");13+ if (body.slot !== undefined && (body.slot < 0 || body.slot >= VAULT_SLOTS)) {14+ throw new ApiError(400, "slot must be 0-15");15+ }16+ try {17+ const vault = await createVault(env, body.name, body.slot);18+ return json({19+ slot: vault.slot,20+ name: vault.name,21+ createdAt: vault.created_at,22+ }, 201);23+ } catch (cause) {24+ const message = cause instanceof Error ? cause.message : "vault conflict";25+ throw new ApiError(409, message, { cause });26+ }27+}@@ -5,7 +5,12 @@ import {
55 VAULT_SLOTS,66 vaultSlotOf,77 } from "../../shared/mod.ts";8-import type { PushInfo, SiteDetail, SiteInfo, VaultInfo } from "../../shared/mod.ts";8+import type {9+ PushInfo,10+ SiteDetail,11+ SiteInfo,12+ VaultInfo,13+} from "../../shared/mod.ts";914 import { type Env, siteUrl } from "./env.ts";1015 1116 export const HISTORY_CAP = 10;@@ -28,25 +33,41 @@ export interface VaultRow {
2833 created_at: number;2934 }3035 31-export async function getSiteByAddress(env: Env, address: number): Promise<SiteRow | null> {36+export async function getSiteByAddress(37+ env: Env,38+ address: number,39+): Promise<SiteRow | null> {3240 return await env.DB.prepare("SELECT * FROM sites WHERE address = ?")3341 .bind(address).first<SiteRow>();3442 }3543 36-export async function getVaultByName(env: Env, name: string): Promise<VaultRow | null> {37- return await env.DB.prepare("SELECT * FROM vaults WHERE name = ?").bind(name).first<VaultRow>();44+export async function getVaultByName(45+ env: Env,46+ name: string,47+): Promise<VaultRow | null> {48+ return await env.DB.prepare("SELECT * FROM vaults WHERE name = ?").bind(name)49+ .first<VaultRow>();3850 }3951 40-export async function getVaultBySlot(env: Env, slot: number): Promise<VaultRow | null> {41- return await env.DB.prepare("SELECT * FROM vaults WHERE slot = ?").bind(slot).first<VaultRow>();52+export async function getVaultBySlot(53+ env: Env,54+ slot: number,55+): Promise<VaultRow | null> {56+ return await env.DB.prepare("SELECT * FROM vaults WHERE slot = ?").bind(slot)57+ .first<VaultRow>();4258 }4359 4460 /** Resolve an API target to an existing site row, or null if it doesn't exist. */45-export async function resolveTarget(env: Env, target: Target): Promise<SiteRow | null> {61+export async function resolveTarget(62+ env: Env,63+ target: Target,64+): Promise<SiteRow | null> {4665 if ("address" in target) return await getSiteByAddress(env, target.address);4766 const vault = await getVaultByName(env, target.vault);4867 if (!vault) return null;49- return await env.DB.prepare("SELECT * FROM sites WHERE vault_slot = ? AND alias = ?")68+ return await env.DB.prepare(69+ "SELECT * FROM sites WHERE vault_slot = ? AND alias = ?",70+ )5071 .bind(vault.slot, target.alias).first<SiteRow>();5172 }5273 @@ -55,12 +76,15 @@ export async function resolveTarget(env: Env, target: Target): Promise<SiteRow |
5576 * don't leak site count or enumerate in order.5677 */5778 export async function allocateAddress(env: Env, slot: number): Promise<number> {58- const taken = await env.DB.prepare("SELECT address FROM sites WHERE vault_slot = ?")79+ const taken = await env.DB.prepare(80+ "SELECT address FROM sites WHERE vault_slot = ?",81+ )5982 .bind(slot).all<{ address: number }>();6083 const used = new Set(taken.results.map((r) => r.address));6184 if (used.size >= SITES_PER_VAULT) throw new Error("vault is full");6285 for (let i = 0; i < 64; i++) {63- const candidate = (slot << 12) | (crypto.getRandomValues(new Uint32Array(1))[0] & 0xfff);86+ const candidate = (slot << 12) |87+ (crypto.getRandomValues(new Uint32Array(1))[0] & 0xfff);6488 if (!used.has(candidate)) return candidate;6589 }6690 // Vault nearly full and randomness unlucky: fall back to first free slot.@@ -86,7 +110,9 @@ export interface CommitParams {
86110 /** Atomic commit: upsert site, append history, prune beyond HISTORY_CAP. Returns new seq. */87111 export async function commitSite(env: Env, p: CommitParams): Promise<number> {88112 const now = Math.floor(Date.now() / 1000);89- const seqRow = await env.DB.prepare("SELECT COALESCE(MAX(seq), 0) AS s FROM pushes WHERE address = ?")113+ const seqRow = await env.DB.prepare(114+ "SELECT COALESCE(MAX(seq), 0) AS s FROM pushes WHERE address = ?",115+ )90116 .bind(p.address).first<{ s: number }>();91117 const seq = (seqRow?.s ?? 0) + 1;92118 @@ -114,7 +140,14 @@ export async function commitSite(env: Env, p: CommitParams): Promise<number> {
114140 `UPDATE sites SET current_manifest = ?, updated_at = ?, expires_at = ?,115141 alias = COALESCE(?, alias), password_hash = ?, auth_version = auth_version + 1116142 WHERE address = ?`,117- ).bind(p.manifestHash, now, p.expiresAt, p.alias, p.passwordHash, p.address);143+ ).bind(144+ p.manifestHash,145+ now,146+ p.expiresAt,147+ p.alias,148+ p.passwordHash,149+ p.address,150+ );118151 119152 const statements = [120153 siteCommit,@@ -129,20 +162,35 @@ export async function commitSite(env: Env, p: CommitParams): Promise<number> {
129162 return seq;130163 }131164 132-export async function listSites(env: Env, vaultSlot?: number): Promise<(SiteRow & { vault_name: string })[]> {165+export async function listSites(166+ env: Env,167+ vaultSlot?: number,168+): Promise<(SiteRow & { vault_name: string })[]> {133169 const base =134170 `SELECT s.*, v.name AS vault_name FROM sites s JOIN vaults v ON v.slot = s.vault_slot`;135171 const stmt = vaultSlot === undefined136172 ? env.DB.prepare(`${base} ORDER BY s.updated_at DESC`)137- : env.DB.prepare(`${base} WHERE s.vault_slot = ? ORDER BY s.updated_at DESC`).bind(vaultSlot);173+ : env.DB.prepare(174+ `${base} WHERE s.vault_slot = ? ORDER BY s.updated_at DESC`,175+ ).bind(vaultSlot);138176 const res = await stmt.all<SiteRow & { vault_name: string }>();139177 return res.results;140178 }141179 142-export async function siteHistory(env: Env, address: number): Promise<PushInfo[]> {180+export async function siteHistory(181+ env: Env,182+ address: number,183+): Promise<PushInfo[]> {143184 const res = await env.DB.prepare(144185 "SELECT seq, manifest_hash, pushed_at, note FROM pushes WHERE address = ? ORDER BY seq DESC",145- ).bind(address).all<{ seq: number; manifest_hash: string; pushed_at: number; note: string | null }>();186+ ).bind(address).all<187+ {188+ seq: number;189+ manifest_hash: string;190+ pushed_at: number;191+ note: string | null;192+ }193+ >();146194 return res.results.map((r) => ({147195 seq: r.seq,148196 manifestHash: r.manifest_hash,@@ -156,7 +204,9 @@ export async function listVaults(env: Env): Promise<VaultInfo[]> {
156204 `SELECT v.slot, v.name, v.created_at, COUNT(s.address) AS site_count157205 FROM vaults v LEFT JOIN sites s ON s.vault_slot = v.slot158206 GROUP BY v.slot ORDER BY v.slot`,159- ).all<{ slot: number; name: string; created_at: number; site_count: number }>();207+ ).all<208+ { slot: number; name: string; created_at: number; site_count: number }209+ >();160210 return res.results.map((r) => ({161211 slot: r.slot,162212 name: r.name,@@ -165,11 +215,17 @@ export async function listVaults(env: Env): Promise<VaultInfo[]> {
165215 }));166216 }167217 168-export async function createVault(env: Env, name: string, slot?: number): Promise<VaultRow> {218+export async function createVault(219+ env: Env,220+ name: string,221+ slot?: number,222+): Promise<VaultRow> {169223 const now = Math.floor(Date.now() / 1000);170224 let chosen = slot;171225 if (chosen === undefined) {172- const takenRes = await env.DB.prepare("SELECT slot FROM vaults").all<{ slot: number }>();226+ const takenRes = await env.DB.prepare("SELECT slot FROM vaults").all<227+ { slot: number }228+ >();173229 const taken = new Set(takenRes.results.map((r) => r.slot));174230 for (let s = 0; s < VAULT_SLOTS; s++) {175231 if (!taken.has(s)) {@@ -179,12 +235,18 @@ export async function createVault(env: Env, name: string, slot?: number): Promis
179235 }180236 if (chosen === undefined) throw new Error("all 16 vault slots are taken");181237 }182- await env.DB.prepare("INSERT INTO vaults (slot, name, created_at) VALUES (?, ?, ?)")238+ await env.DB.prepare(239+ "INSERT INTO vaults (slot, name, created_at) VALUES (?, ?, ?)",240+ )183241 .bind(chosen, name, now).run();184242 return { slot: chosen, name, created_at: now };185243 }186244 187-export function siteRowToInfo(env: Env, row: SiteRow, vaultName: string): SiteInfo {245+export function siteRowToInfo(246+ env: Env,247+ row: SiteRow,248+ vaultName: string,249+): SiteInfo {188250 const address = formatAddress(row.address);189251 return {190252 address,@@ -199,7 +261,10 @@ export function siteRowToInfo(env: Env, row: SiteRow, vaultName: string): SiteIn
199261 };200262 }201263 202-export async function siteRowToDetail(env: Env, row: SiteRow): Promise<SiteDetail> {264+export async function siteRowToDetail(265+ env: Env,266+ row: SiteRow,267+): Promise<SiteDetail> {203268 const vault = await getVaultBySlot(env, vaultSlotOf(row.address));204269 return {205270 ...siteRowToInfo(env, row, vault?.name ?? `slot-${row.vault_slot}`),@@ -1,18 +1,5 @@
1-export interface Env {2- CONTENT: R2Bucket;3- DB: D1Database;4- NZIP_TOKEN: string;5- PUBLIC_BASE: string;6- // Per-IP limiter for bare-address hits — throttles address enumeration.7- RL_ENUM: RateLimit;8- // Per-IP+address limiter for the unlock endpoint — throttles password guessing.9- RL_UNLOCK: RateLimit;10- // Caps persisted enumeration observations per scanner to bound D1 usage.11- RL_OBSERVE: RateLimit;12- EMAIL: SendEmail;13- ALERT_EMAIL_TO: string;14- ALERT_EMAIL_FROM: string;15-}1+// Generated from wrangler.jsonc by `npm run types`.2+export type Env = Cloudflare.Env;163 174 export function json<T = unknown>(185 body: T,@@ -7,7 +7,9 @@ import type { Env } from "./env.ts";
77 // concurrent sweep.88 const MIN_AGE_MS = 24 * 3600 * 1000;99 10-export async function runGc(env: Env): Promise<{ expiredSites: number; deletedObjects: number }> {10+export async function runGc(11+ env: Env,12+): Promise<{ expiredSites: number; deletedObjects: number }> {1113 const now = Math.floor(Date.now() / 1000);1214 1315 // 1. Sweep expired sites (cascades to pushes history).@@ -20,7 +22,9 @@ export async function runGc(env: Env): Promise<{ expiredSites: number; deletedOb
2022 const current = await env.DB.prepare("SELECT current_manifest FROM sites")2123 .all<{ current_manifest: string }>();2224 for (const r of current.results) liveManifests.add(r.current_manifest);23- const historic = await env.DB.prepare("SELECT DISTINCT manifest_hash FROM pushes")25+ const historic = await env.DB.prepare(26+ "SELECT DISTINCT manifest_hash FROM pushes",27+ )2428 .all<{ manifest_hash: string }>();2529 for (const r of historic.results) liveManifests.add(r.manifest_hash);2630 @@ -39,11 +43,17 @@ export async function runGc(env: Env): Promise<{ expiredSites: number; deletedOb
3943 for (const prefix of ["manifest/", "blob/"] as const) {4044 let cursor: string | undefined;4145 do {42- const page: R2Objects = await env.CONTENT.list({ prefix, cursor, limit: 1000 });46+ const page: R2Objects = await env.CONTENT.list({47+ prefix,48+ cursor,49+ limit: 1000,50+ });4351 const doomed = page.objects.filter((o) => {4452 if (o.uploaded.getTime() > cutoff) return false;4553 const hash = o.key.slice(prefix.length);46- return prefix === "manifest/" ? !liveManifests.has(hash) : !liveBlobs.has(hash);54+ return prefix === "manifest/"55+ ? !liveManifests.has(hash)56+ : !liveBlobs.has(hash);4757 }).map((o) => o.key);4858 if (doomed.length > 0) {4959 await env.CONTENT.delete(doomed);@@ -125,8 +125,15 @@ export async function recordEnumerationProbe(
125125 const ip = req.headers.get("cf-connecting-ip") ?? "local";126126 const digest = await scannerDigest(ip, env.NZIP_TOKEN);127127 const scannerId = hex(digest.slice(0, 8));128- const { success } = await env.RL_OBSERVE.limit({ key: scannerId });129- if (!success && response.status !== 429) return;128+ // A rejected enumeration request still carries a useful confirmation that129+ // the edge limiter fired, but it must not bypass the persistence budget.130+ // Keep confirmations on a separate quota so a scanner cannot turn an131+ // unlimited stream of 429 responses into unlimited D1 writes.132+ const persistenceLimiter = response.status === 429133+ ? env.RL_SIGNAL134+ : env.RL_OBSERVE;135+ const { success } = await persistenceLimiter.limit({ key: scannerId });136+ if (!success) return;130137 131138 const now = Math.floor(Date.now() / 1000);132139 const bucket = now - (now % 300);@@ -8,15 +8,23 @@ interface UnlockSecrets {
88 }99 1010 const hex = (bytes: ArrayBuffer | Uint8Array) =>11- [...new Uint8Array(bytes as ArrayBuffer)].map((b) => b.toString(16).padStart(2, "0")).join("");11+ [...new Uint8Array(bytes as ArrayBuffer)].map((b) =>12+ b.toString(16).padStart(2, "0")13+ ).join("");1214 1315 function hexToBytes(s: string): Uint8Array {1416 const out = new Uint8Array(s.length / 2);15- for (let i = 0; i < out.length; i++) out[i] = parseInt(s.slice(i * 2, i * 2 + 2), 16);17+ for (let i = 0; i < out.length; i++) {18+ out[i] = parseInt(s.slice(i * 2, i * 2 + 2), 16);19+ }1620 return out;1721 }1822 19-async function pbkdf2(password: string, salt: Uint8Array, iterations: number): Promise<string> {23+async function pbkdf2(24+ password: string,25+ salt: Uint8Array,26+ iterations: number,27+): Promise<string> {2028 const key = await crypto.subtle.importKey(2129 "raw",2230 new TextEncoder().encode(password),@@ -35,18 +43,31 @@ async function pbkdf2(password: string, salt: Uint8Array, iterations: number): P
3543 /** → "pbkdf2$<iterations>$<saltHex>$<hashHex>" */3644 export async function hashPassword(password: string): Promise<string> {3745 const salt = crypto.getRandomValues(new Uint8Array(16));38- return `pbkdf2$${PBKDF2_ITERATIONS}$${hex(salt)}$${await pbkdf2(password, salt, PBKDF2_ITERATIONS)}`;46+ return `pbkdf2$${PBKDF2_ITERATIONS}$${hex(salt)}$${await pbkdf2(47+ password,48+ salt,49+ PBKDF2_ITERATIONS,50+ )}`;3951 }4052 41-export async function verifyPassword(password: string, stored: string): Promise<boolean> {53+export async function verifyPassword(54+ password: string,55+ stored: string,56+): Promise<boolean> {4257 const [scheme, iterStr, saltHex, hashHex] = stored.split("$");4358 if (scheme !== "pbkdf2") return false;44- const actual = await pbkdf2(password, hexToBytes(saltHex), parseInt(iterStr, 10));59+ const actual = await pbkdf2(60+ password,61+ hexToBytes(saltHex),62+ parseInt(iterStr, 10),63+ );4564 // Both sides are fixed-length hex of a fresh derivation; timing is not a concern here,4665 // but compare in constant time anyway.4766 if (actual.length !== hashHex.length) return false;4867 let diff = 0;49- for (let i = 0; i < actual.length; i++) diff |= actual.charCodeAt(i) ^ hashHex.charCodeAt(i);68+ for (let i = 0; i < actual.length; i++) {69+ diff |= actual.charCodeAt(i) ^ hashHex.charCodeAt(i);70+ }5071 return diff === 0;5172 }5273 @@ -79,7 +100,9 @@ export async function makeUnlockCookie(
79100 ),80101 );81102 // Path without trailing slash so the cookie also matches the bare /{address} URL.82- return `${cookieName(address)}=${authVersion}.${exp}.${sig}; Max-Age=${COOKIE_TTL_S}; Path=/${address}; HttpOnly; Secure; SameSite=Lax`;103+ return `${104+ cookieName(address)105+ }=${authVersion}.${exp}.${sig}; Max-Age=${COOKIE_TTL_S}; Path=/${address}; HttpOnly; Secure; SameSite=Lax`;83106 }84107 85108 export async function hasValidUnlockCookie(@@ -89,7 +112,9 @@ export async function hasValidUnlockCookie(
89112 authVersion: number,90113 ): Promise<boolean> {91114 const cookies = req.headers.get("cookie") ?? "";92- const match = cookies.match(new RegExp(`(?:^|;\\s*)${cookieName(address)}=([^;]+)`));115+ const match = cookies.match(116+ new RegExp(`(?:^|;\\s*)${cookieName(address)}=([^;]+)`),117+ );93118 if (!match) return false;94119 const parts = match[1].split(".");95120 let expStr: string;@@ -107,7 +132,9 @@ export async function hasValidUnlockCookie(
107132 return false;108133 }109134 const exp = parseInt(expStr, 10);110- if (!Number.isFinite(exp) || exp < Math.floor(Date.now() / 1000)) return false;135+ if (!Number.isFinite(exp) || exp < Math.floor(Date.now() / 1000)) {136+ return false;137+ }111138 try {112139 return await crypto.subtle.verify(113140 "HMAC",@@ -0,0 +1,137 @@
1+import type { Env } from "../env.ts";2+import type { AlertAction, IncidentRow, WindowStats } from "./policy.ts";3+4+interface NotificationRow {5+ id: string;6+ subject: string;7+ text: string;8+ html: string | null;9+ attempts: number;10+}11+12+function subject(action: Exclude<AlertAction, null>, severity: number): string {13+ if (action === "resolve") return "[nzip] Enumeration incident resolved";14+ if (action === "summary") return "[nzip] Enumeration incident summary";15+ return `[nzip] ${16+ severity >= 2 ? "Confirmed" : "Possible"17+ } enumeration ${action}`;18+}19+20+function emailText(21+ action: Exclude<AlertAction, null>,22+ stats: WindowStats,23+ incident: IncidentRow,24+): string {25+ return [26+ `Enumeration incident: ${action}`,27+ `Severity: ${incident.severity >= 2 ? "confirmed" : "warning"}`,28+ `Opened: ${new Date(incident.opened_at * 1000).toISOString()}`,29+ `This window: ${stats.addresses} distinct scanner/address pairs, ${stats.scanners} scanners`,30+ `Top scanner: ${stats.topScannerAddresses} distinct addresses`,31+ `Live hits: ${stats.liveHits}; rate-limited scanners: ${stats.rateLimited}`,32+ `Longest adjacent run: ${stats.longestSequence}`,33+ `Incident totals: ${incident.total_addresses} observations, ${incident.total_live_hits} live hits, ${incident.total_rate_limited} rate-limit confirmations`,34+ `Countries: ${stats.countries.join(", ") || "unknown"}`,35+ `ASNs: ${stats.asns.join(", ") || "unknown"}`,36+ "Re-alert policy: immediate on escalation; otherwise at most hourly; resolves after 15 quiet minutes.",37+ ].join("\n");38+}39+40+export function notificationId(41+ incidentName: string,42+ action: string,43+ bucket: number,44+): string {45+ return `${incidentName}:${action}:${bucket}`;46+}47+48+export function enqueueNotification(49+ env: Env,50+ id: string,51+ incidentName: string,52+ action: Exclude<AlertAction, null>,53+ bucket: number,54+ stats: WindowStats,55+ incident: IncidentRow,56+ now: number,57+): D1PreparedStatement {58+ const text = `${emailText(action, stats, incident)}\nNotification ID: ${id}`;59+ const html =60+ `<pre style="font:14px/1.5 ui-monospace,monospace;white-space:pre-wrap">${61+ text.replaceAll("&", "&").replaceAll("<", "<")62+ }</pre>`;63+ return env.DB.prepare(64+ `INSERT OR IGNORE INTO security_notifications65+ (id, incident_name, action, window_bucket, subject, text, html, created_at)66+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,67+ ).bind(68+ id,69+ incidentName,70+ action,71+ bucket,72+ subject(action, incident.severity),73+ text,74+ html,75+ now,76+ );77+}78+79+function errorMessage(error: unknown): string {80+ return (error instanceof Error ? error.message : String(error)).slice(81+ 0,82+ 1000,83+ );84+}85+86+export async function drainSecurityNotifications(87+ env: Env,88+ now = Math.floor(Date.now() / 1000),89+): Promise<void> {90+ const pending = await env.DB.prepare(91+ `SELECT id, subject, text, html, attempts FROM security_notifications92+ WHERE sent_at IS NULL ORDER BY created_at, id LIMIT 10`,93+ ).all<NotificationRow>();94+95+ for (const notification of pending.results) {96+ const attempt = notification.attempts + 1;97+ try {98+ await env.EMAIL.send({99+ from: { email: env.ALERT_EMAIL_FROM, name: "nzip security" },100+ to: env.ALERT_EMAIL_TO,101+ subject: notification.subject,102+ text: notification.text,103+ ...(notification.html ? { html: notification.html } : {}),104+ });105+ await env.DB.prepare(106+ `UPDATE security_notifications SET sent_at = ?, attempts = attempts + 1,107+ last_error = NULL WHERE id = ? AND sent_at IS NULL`,108+ ).bind(now, notification.id).run();109+ console.log({110+ event: "security.notification_sent",111+ notificationId: notification.id,112+ attempt,113+ });114+ } catch (error) {115+ const message = errorMessage(error);116+ await env.DB.prepare(117+ `UPDATE security_notifications SET attempts = attempts + 1, last_error = ?118+ WHERE id = ? AND sent_at IS NULL`,119+ ).bind(message, notification.id).run();120+ console.error({121+ event: "security.notification_failed",122+ notificationId: notification.id,123+ attempt,124+ error: message,125+ });126+ }127+ }128+}129+130+export async function sendAlertTest(env: Env): Promise<void> {131+ await env.EMAIL.send({132+ from: { email: env.ALERT_EMAIL_FROM, name: "nzip security" },133+ to: env.ALERT_EMAIL_TO,134+ subject: "[nzip] Security alert delivery test",135+ text: "nzip security alert delivery is configured correctly.",136+ });137+}@@ -0,0 +1,79 @@
1+export interface WindowStats {2+ bucket: number;3+ addresses: number;4+ scanners: number;5+ misses: number;6+ liveHits: number;7+ rateLimited: number;8+ topScannerAddresses: number;9+ longestSequence: number;10+ vaultMask: number;11+ countries: string[];12+ asns: number[];13+}14+15+export interface IncidentRow {16+ status: "open" | "closed";17+ severity: number;18+ opened_at: number;19+ last_seen_at: number;20+ last_alerted_at: number;21+ quiet_windows: number;22+ peak_addresses: number;23+ total_addresses: number;24+ total_live_hits: number;25+ total_rate_limited: number;26+ vault_mask: number;27+}28+29+export type AlertAction = "open" | "escalate" | "summary" | "resolve" | null;30+31+export function signalSeverity(32+ stats: WindowStats,33+ incidentOpen = false,34+): number {35+ const distributed = stats.addresses >= 128 && stats.scanners >= 10 &&36+ (stats.addresses === 0 || stats.misses / stats.addresses >= 0.9);37+ const scannerSweep = stats.topScannerAddresses >= 20;38+ const suspiciousLiveHit = stats.liveHits > 0 &&39+ (incidentOpen || stats.topScannerAddresses >= 8 ||40+ stats.longestSequence >= 8);41+ if (stats.rateLimited > 0 || suspiciousLiveHit) return 2;42+ if (scannerSweep || distributed) return 1;43+ return 0;44+}45+46+export function chooseAlertAction(47+ incident: IncidentRow | null,48+ stats: WindowStats,49+ now: number,50+): AlertAction {51+ const severity = signalSeverity(stats, incident?.status === "open");52+ if (!incident || incident.status === "closed") {53+ return severity > 0 ? "open" : null;54+ }55+ if (severity === 0) return incident.quiet_windows >= 2 ? "resolve" : null;56+ if (57+ severity > incident.severity ||58+ stats.addresses >= Math.max(20, incident.peak_addresses * 2) ||59+ (stats.liveHits > 0 && incident.total_live_hits === 0)60+ ) return "escalate";61+ const reachedNewVault = (stats.vaultMask & ~incident.vault_mask) !== 0;62+ if (reachedNewVault && now - incident.last_alerted_at >= 1800) {63+ return "escalate";64+ }65+ if (now - incident.last_alerted_at >= 3600) return "summary";66+ return null;67+}68+69+export function longestAdjacentRun(addresses: number[]): number {70+ if (addresses.length === 0) return 0;71+ const sorted = [...new Set(addresses)].sort((a, b) => a - b);72+ let longest = 1;73+ let current = 1;74+ for (let i = 1; i < sorted.length; i++) {75+ current = sorted[i] === sorted[i - 1] + 1 ? current + 1 : 1;76+ longest = Math.max(longest, current);77+ }78+ return longest;79+}@@ -1,84 +1,23 @@
11 import type { Env } from "./env.ts";2-3-export interface WindowStats {4- bucket: number;5- addresses: number;6- scanners: number;7- misses: number;8- liveHits: number;9- rateLimited: number;10- topScannerAddresses: number;11- longestSequence: number;12- vaultMask: number;13- countries: string[];14- asns: number[];15-}16-17-interface IncidentRow {18- status: "open" | "closed";19- severity: number;20- opened_at: number;21- last_seen_at: number;22- last_alerted_at: number;23- quiet_windows: number;24- peak_addresses: number;25- total_addresses: number;26- total_live_hits: number;27- total_rate_limited: number;28- vault_mask: number;29-}30-31-export type AlertAction = "open" | "escalate" | "summary" | "resolve" | null;32-33-export function signalSeverity(34- stats: WindowStats,35- incidentOpen = false,36-): number {37- const distributed = stats.addresses >= 128 && stats.scanners >= 10 &&38- (stats.addresses === 0 || stats.misses / stats.addresses >= 0.9);39- const scannerSweep = stats.topScannerAddresses >= 20;40- const suspiciousLiveHit = stats.liveHits > 0 &&41- (incidentOpen || stats.topScannerAddresses >= 8 ||42- stats.longestSequence >= 8);43- if (stats.rateLimited > 0 || suspiciousLiveHit) return 2;44- if (scannerSweep || distributed) return 1;45- return 0;46-}47-48-export function chooseAlertAction(49- incident: IncidentRow | null,50- stats: WindowStats,51- now: number,52-): AlertAction {53- const severity = signalSeverity(stats, incident?.status === "open");54- if (!incident || incident.status === "closed") {55- return severity > 0 ? "open" : null;56- }57- if (severity === 0) return incident.quiet_windows >= 2 ? "resolve" : null;58- if (59- severity > incident.severity ||60- stats.addresses >= Math.max(20, incident.peak_addresses * 2) ||61- (stats.liveHits > 0 && incident.total_live_hits === 0)62- ) return "escalate";63- const reachedNewVault = (stats.vaultMask & ~incident.vault_mask) !== 0;64- if (reachedNewVault && now - incident.last_alerted_at >= 1800) {65- return "escalate";66- }67- if (now - incident.last_alerted_at >= 3600) return "summary";68- return null;69-}70-71-function longestAdjacentRun(addresses: number[]): number {72- if (addresses.length === 0) return 0;73- const sorted = [...new Set(addresses)].sort((a, b) => a - b);74- let longest = 1;75- let current = 1;76- for (let i = 1; i < sorted.length; i++) {77- current = sorted[i] === sorted[i - 1] + 1 ? current + 1 : 1;78- longest = Math.max(longest, current);79- }80- return longest;81-}2+import {3+ drainSecurityNotifications,4+ enqueueNotification,5+ notificationId,6+} from "./security/notifications.ts";7+import {8+ chooseAlertAction,9+ type IncidentRow,10+ longestAdjacentRun,11+ signalSeverity,12+ type WindowStats,13+} from "./security/policy.ts";14+15+export {16+ drainSecurityNotifications,17+ sendAlertTest,18+} from "./security/notifications.ts";19+export { chooseAlertAction, signalSeverity } from "./security/policy.ts";20+export type { AlertAction, WindowStats } from "./security/policy.ts";8221 8322 async function loadWindow(env: Env, bucket: number): Promise<WindowStats> {8423 const probes = await env.DB.prepare(@@ -130,54 +69,6 @@ async function loadWindow(env: Env, bucket: number): Promise<WindowStats> {
13069 };13170 }13271 133-function subject(action: Exclude<AlertAction, null>, severity: number): string {134- if (action === "resolve") return "[nzip] Enumeration incident resolved";135- if (action === "summary") return "[nzip] Enumeration incident summary";136- return `[nzip] ${137- severity >= 2 ? "Confirmed" : "Possible"138- } enumeration ${action}`;139-}140-141-function emailText(142- action: Exclude<AlertAction, null>,143- stats: WindowStats,144- incident: IncidentRow,145-): string {146- const start = new Date(incident.opened_at * 1000).toISOString();147- return [148- `Enumeration incident: ${action}`,149- `Severity: ${incident.severity >= 2 ? "confirmed" : "warning"}`,150- `Opened: ${start}`,151- `This window: ${stats.addresses} distinct scanner/address pairs, ${stats.scanners} scanners`,152- `Top scanner: ${stats.topScannerAddresses} distinct addresses`,153- `Live hits: ${stats.liveHits}; rate-limited scanners: ${stats.rateLimited}`,154- `Longest adjacent run: ${stats.longestSequence}`,155- `Incident totals: ${incident.total_addresses} observations, ${incident.total_live_hits} live hits, ${incident.total_rate_limited} rate-limit confirmations`,156- `Countries: ${stats.countries.join(", ") || "unknown"}`,157- `ASNs: ${stats.asns.join(", ") || "unknown"}`,158- "Re-alert policy: immediate on escalation; otherwise at most hourly; resolves after 15 quiet minutes.",159- ].join("\n");160-}161-162-async function sendAlert(163- env: Env,164- action: Exclude<AlertAction, null>,165- stats: WindowStats,166- incident: IncidentRow,167-): Promise<void> {168- const text = emailText(action, stats, incident);169- await env.EMAIL.send({170- from: { email: env.ALERT_EMAIL_FROM, name: "nzip security" },171- to: env.ALERT_EMAIL_TO,172- subject: subject(action, incident.severity),173- text,174- html:175- `<pre style="font:14px/1.5 ui-monospace,monospace;white-space:pre-wrap">${176- text.replaceAll("&", "&").replaceAll("<", "<")177- }</pre>`,178- });179-}180-18172 export async function evaluateEnumerationWindow(18273 env: Env,18374 now = Math.floor(Date.now() / 1000),@@ -221,11 +112,7 @@ export async function evaluateEnumerationWindow(
221112 };222113 }223114 224- // Deliver before advancing last_alerted_at so transient failures retry on225- // the next scheduled window instead of being silently suppressed.226- if (action) await sendAlert(env, action, stats, incident);227-228- await env.DB.prepare(115+ const upsertIncident = env.DB.prepare(229116 `INSERT INTO security_incidents230117 (name, status, severity, opened_at, last_seen_at, last_alerted_at, quiet_windows,231118 peak_addresses, total_addresses, total_live_hits, total_rate_limited, vault_mask)@@ -248,7 +135,28 @@ export async function evaluateEnumerationWindow(
248135 incident.total_live_hits,249136 incident.total_rate_limited,250137 incident.vault_mask,251- ).run();138+ );139+140+ if (action) {141+ const id = notificationId("enumeration", action, bucket);142+ await env.DB.batch([143+ upsertIncident,144+ enqueueNotification(145+ env,146+ id,147+ "enumeration",148+ action,149+ bucket,150+ stats,151+ incident,152+ now,153+ ),154+ ]);155+ } else {156+ await upsertIncident.run();157+ }158+159+ await drainSecurityNotifications(env, now);252160 253161 console.log({ event: "security.enumeration_window", action, ...stats });254162 }@@ -265,11 +173,16 @@ export async function sendDailySecurityDigest(
265173 ).bind(since).first<266174 { addresses: number; scanners: number; live_hits: number | null }267175 >();268- if (!probes || probes.addresses === 0) return;176+ if (!probes || probes.addresses === 0) {177+ await drainSecurityNotifications(env, now);178+ return;179+ }269180 const signals = await env.DB.prepare(270181 `SELECT COUNT(*) AS count FROM security_signals271182 WHERE bucket >= ? AND kind = 'rate_limited'`,272183 ).bind(since).first<{ count: number }>();184+ const day = now - (now % 86400);185+ const id = notificationId("security", "daily-digest", day);273186 const text = [274187 "nzip security activity for the last 24 hours",275188 `Distinct scanner/address pairs: ${probes.addresses}`,@@ -277,22 +190,20 @@ export async function sendDailySecurityDigest(
277190 `Live-address hits: ${probes.live_hits ?? 0}`,278191 `Rate-limit confirmations: ${signals?.count ?? 0}`,279192 "No digest is sent on days without bare-address probe activity.",193+ `Notification ID: ${id}`,280194 ].join("\n");281- await env.EMAIL.send({282- from: { email: env.ALERT_EMAIL_FROM, name: "nzip security" },283- to: env.ALERT_EMAIL_TO,284- subject: "[nzip] Daily security activity summary",195+ await env.DB.prepare(196+ `INSERT OR IGNORE INTO security_notifications197+ (id, incident_name, action, window_bucket, subject, text, html, created_at)198+ VALUES (?, 'security', 'daily-digest', ?, ?, ?, NULL, ?)`,199+ ).bind(200+ id,201+ day,202+ "[nzip] Daily security activity summary",285203 text,286- });287-}288-289-export async function sendAlertTest(env: Env): Promise<void> {290- await env.EMAIL.send({291- from: { email: env.ALERT_EMAIL_FROM, name: "nzip security" },292- to: env.ALERT_EMAIL_TO,293- subject: "[nzip] Security alert delivery test",294- text: "nzip security alert delivery is configured correctly.",295- });204+ now,205+ ).run();206+ await drainSecurityNotifications(env, now);296207 }297208 298209 export async function pruneSecurityTelemetry(@@ -300,10 +211,14 @@ export async function pruneSecurityTelemetry(
300211 now = Math.floor(Date.now() / 1000),301212 ): Promise<void> {302213 const cutoff = now - 7 * 86400;214+ const notificationCutoff = now - 30 * 86400;303215 await env.DB.batch([304216 env.DB.prepare("DELETE FROM security_probes WHERE bucket < ?").bind(cutoff),305217 env.DB.prepare("DELETE FROM security_signals WHERE bucket < ?").bind(306218 cutoff,307219 ),220+ env.DB.prepare(221+ "DELETE FROM security_notifications WHERE sent_at IS NOT NULL AND sent_at < ?",222+ ).bind(notificationCutoff),308223 ]);309224 }@@ -3,16 +3,25 @@ import { getSiteByAddress, type SiteRow } from "./db.ts";
33 import { siteCacheTag } from "./cache.ts";44 import type { Env } from "./env.ts";55 import { FAVICON_PNG } from "./favicon.ts";6-import { hasValidUnlockCookie, makeUnlockCookie, verifyPassword } from "./password.ts";6+import {7+ hasValidUnlockCookie,8+ makeUnlockCookie,9+ verifyPassword,10+} from "./password.ts";711 8-const GONE_PAGE = `<!doctype html><meta charset="utf-8"><title>expired — nzip</title>12+const GONE_PAGE =13+ `<!doctype html><meta charset="utf-8"><title>expired — nzip</title>914 <style>body{background:#121110;color:#8a8172;font-family:ui-monospace,monospace;display:grid;place-items:center;height:100vh;margin:0}div{text-align:center}b{color:#ffb347;font-size:32px;display:block;margin-bottom:8px}</style>1015 <div><b>410</b>this share expired</div>`;1116 12-const NOT_FOUND_PAGE = `<!doctype html><meta charset="utf-8"><title>not found — nzip</title>17+const NOT_FOUND_PAGE =18+ `<!doctype html><meta charset="utf-8"><title>not found — nzip</title>1319 <style>body{background:#121110;color:#8a8172;font-family:ui-monospace,monospace;display:grid;place-items:center;height:100vh;margin:0}div{text-align:center}b{color:#ffb347;font-size:32px;display:block;margin-bottom:8px}</style>1420 <div><b>404</b>nothing here</div>`;1521 22+const MAX_UNLOCK_BODY_BYTES = 4096;23+const MAX_PASSWORD_LENGTH = 256;24+1625 const escapeHtml = (s: string): string =>1726 s.replace(/[&<>]/g, (c) => ({ "&": "&", "<": "<", ">": ">" }[c]!));1827 @@ -21,15 +30,25 @@ const escapeHtml = (s: string): string =>
2130 // hostname with the nzip wordmark; the coral z is the product's small visual mark.2231 function landingPage(env: Env): string {2332 const host = new URL(env.PUBLIC_BASE).host;24- return `<!doctype html><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>${escapeHtml(host)}</title>33+ return `<!doctype html><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>${34+ escapeHtml(host)35+ }</title>2536 <style>body{background:#1e1810;color:#e7dbc5;font-family:ui-monospace,monospace;margin:0;min-height:100dvh;display:flex;flex-direction:column}main{flex:1;display:grid;place-items:center;padding:24px;text-align:center}.landing{display:grid;gap:24px;justify-items:center}.wordmark{color:#f2e8d4;font-size:clamp(40px,10vw,72px);font-weight:700;letter-spacing:-.18em;line-height:.8}.wordmark .z{color:#ff6b4a}.host{font-size:clamp(14px,2vw,18px);letter-spacing:-.06em}.host b{color:#d99a5b}footer{text-align:center;padding:14px 16px calc(14px + env(safe-area-inset-bottom))}a{color:#6b6355;font-size:12px;text-decoration:underline}a:hover{color:#d99a5b}</style>2637 <main><div class="landing"><div class="wordmark" aria-label="nzip">n<span class="z">z</span>ip</div></div></main><footer><a href="https://args.io/cat/nzip">args</a></footer>`;2738 }2839 29-function htmlResponse(body: string, status: number, headers: HeadersInit = {}): Response {40+function htmlResponse(41+ body: string,42+ status: number,43+ headers: HeadersInit = {},44+): Response {3045 return new Response(body, {3146 status,32- headers: { "content-type": "text/html; charset=utf-8", "cache-control": "no-store", ...headers },47+ headers: {48+ "content-type": "text/html; charset=utf-8",49+ "cache-control": "no-store",50+ ...headers,51+ },3352 });3453 }3554 @@ -50,16 +69,50 @@ function publicSiteRedirect(url: string, address: string): Response {
5069 function unlockForm(address: string, error?: string): string {5170 return `<!doctype html><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover"><meta name="robots" content="noindex"><title>unlock — nzip</title>5271 <style>*{box-sizing:border-box}body{background:#121110;color:#d6cfc2;font-family:ui-monospace,monospace;display:grid;place-items:center;min-height:100vh;min-height:100dvh;margin:0;padding:max(24px,env(safe-area-inset-top)) max(16px,env(safe-area-inset-right)) max(24px,env(safe-area-inset-bottom)) max(16px,env(safe-area-inset-left))}form{width:min(100%,420px);text-align:center}b{color:#ffb347;font-size:24px;display:block;margin-bottom:20px}.field{display:flex;width:100%}input{background:#1a1815;border:1px solid #2e2a24;color:#d6cfc2;min-width:0;flex:1;padding:12px 14px;font:inherit;font-size:16px;line-height:20px;outline:none;border-radius:0}input:focus{border-color:#ffb347}button{background:#ffb347;border:0;color:#121110;min-height:46px;padding:12px 18px;font:inherit;font-weight:700;cursor:pointer;border-radius:0}.e{color:#f7768e;margin-top:12px;font-size:13px}@media(max-width:360px){.field{display:grid;gap:10px}button{width:100%}}</style>53-<form method="post" action="/${address}/__unlock"><b>🔒 ${address}</b><div class="field"><input type="password" name="password" aria-label="password" placeholder="password" autofocus autocomplete="current-password"><button>unlock</button></div>${72+<form method="post" action="/${address}/__unlock"><b>🔒 ${address}</b><div class="field"><input type="password" name="password" aria-label="password" placeholder="password" maxlength="256" autofocus autocomplete="current-password"><button>unlock</button></div>${5473 error ? `<div class="e">${error}</div>` : ""5574 }</form>`;5675 }5776 58-async function handleUnlock(req: Request, env: Env, site: SiteRow, address: string): Promise<Response> {59- const form = await req.formData().catch(() => null);60- const password = form?.get("password");61- if (typeof password !== "string" || !site.password_hash ||62- !(await verifyPassword(password, site.password_hash))) {77+async function handleUnlock(78+ req: Request,79+ env: Env,80+ site: SiteRow,81+ address: string,82+): Promise<Response> {83+ const contentLength = req.headers.get("content-length");84+ if (contentLength === null) {85+ return htmlResponse(unlockForm(address, "request size required"), 411);86+ }87+ const declaredBytes = Number(contentLength);88+ if (!Number.isSafeInteger(declaredBytes) || declaredBytes < 0) {89+ return htmlResponse(unlockForm(address, "invalid request size"), 400);90+ }91+ if (declaredBytes > MAX_UNLOCK_BODY_BYTES) {92+ return htmlResponse(unlockForm(address, "request too large"), 413);93+ }94+ const bytes = new Uint8Array(await req.arrayBuffer());95+ if (bytes.length > MAX_UNLOCK_BODY_BYTES) {96+ return htmlResponse(unlockForm(address, "request too large"), 413);97+ }98+ if (bytes.length !== declaredBytes) {99+ return htmlResponse(unlockForm(address, "request size mismatch"), 400);100+ }101+ const contentType = req.headers.get("content-type")?.split(";", 1)[0].trim()102+ .toLowerCase();103+ if (contentType !== "application/x-www-form-urlencoded") {104+ return htmlResponse(unlockForm(address, "unsupported form"), 415);105+ }106+ const password = new URLSearchParams(new TextDecoder().decode(bytes)).get(107+ "password",108+ );109+ if (password !== null && password.length > MAX_PASSWORD_LENGTH) {110+ return htmlResponse(unlockForm(address, "password too long"), 400);111+ }112+ if (113+ typeof password !== "string" || !site.password_hash ||114+ !(await verifyPassword(password, site.password_hash))115+ ) {63116 return htmlResponse(unlockForm(address, "wrong password"), 401);64117 }65118 return new Response(null, {@@ -73,23 +126,35 @@ async function handleUnlock(req: Request, env: Env, site: SiteRow, address: stri
73126 });74127 }75128 76-export async function serve(req: Request, env: Env, url: URL): Promise<Response> {129+export async function serve(130+ req: Request,131+ env: Env,132+ url: URL,133+): Promise<Response> {77134 const path = url.pathname;78135 79136 if (path === "/") {80- return htmlResponse(landingPage(env), 200, { "cache-control": "public, max-age=3600" });137+ return htmlResponse(landingPage(env), 200, {138+ "cache-control": "public, max-age=3600",139+ });81140 }82141 // Served for the browsers' default probe; pushed sites under /{4hex}/ still83142 // control their own icons through their HTML. PNG bytes at the .ico path are84143 // accepted everywhere.85144 if (path === "/favicon.ico") {86145 return new Response(FAVICON_PNG, {87- headers: { "content-type": "image/png", "cache-control": "public, max-age=86400" },146+ headers: {147+ "content-type": "image/png",148+ "cache-control": "public, max-age=86400",149+ },88150 });89151 }90152 if (path === "/robots.txt") {91153 return new Response("User-agent: *\nAllow: /\n", {92- headers: { "content-type": "text/plain", "cache-control": "public, max-age=86400" },154+ headers: {155+ "content-type": "text/plain",156+ "cache-control": "public, max-age=86400",157+ },93158 });94159 }95160 @@ -101,7 +166,9 @@ export async function serve(req: Request, env: Env, url: URL): Promise<Response>
101166 const site = await getSiteByAddress(env, parseAddress(addressStr));102167 if (!site) return htmlResponse(NOT_FOUND_PAGE, 404);103168 const now = Math.floor(Date.now() / 1000);104- if (site.expires_at !== null && site.expires_at < now) return htmlResponse(GONE_PAGE, 410);169+ if (site.expires_at !== null && site.expires_at < now) {170+ return htmlResponse(GONE_PAGE, 410);171+ }105172 106173 // Password gate.107174 if (m[2] === "/__unlock" && req.method === "POST") {@@ -114,9 +181,13 @@ export async function serve(req: Request, env: Env, url: URL): Promise<Response>
114181 return htmlResponse(unlockForm(addressStr), 401);115182 }116183 117- const manifestObj = await env.CONTENT.get(`manifest/${site.current_manifest}`);184+ const manifestObj = await env.CONTENT.get(185+ `manifest/${site.current_manifest}`,186+ );118187 if (!manifestObj) return htmlResponse(NOT_FOUND_PAGE, 404);119- const manifest = parseManifest(new Uint8Array(await manifestObj.arrayBuffer()));188+ const manifest = parseManifest(189+ new Uint8Array(await manifestObj.arrayBuffer()),190+ );120191 const filePaths = Object.keys(manifest.files);121192 122193 // Bare address: single-file sites serve directly (URL stays /{4hex} — nothing@@ -126,7 +197,10 @@ export async function serve(req: Request, env: Env, url: URL): Promise<Response>
126197 let explicitIndexPath = false;127198 if (m[2] === undefined) {128199 if (filePaths.length !== 1) {129- return publicSiteRedirect(new URL(`/${addressStr}/`, url).toString(), addressStr);200+ return publicSiteRedirect(201+ new URL(`/${addressStr}/`, url).toString(),202+ addressStr,203+ );130204 }131205 assetPath = filePaths[0];132206 } else {@@ -148,7 +222,7 @@ export async function serve(req: Request, env: Env, url: URL): Promise<Response>
148222 return publicSiteRedirect(canonicalUrl.toString(), addressStr);149223 }150224 151- let entry = manifest.files[assetPath];225+ const entry = manifest.files[assetPath];152226 if (!entry && manifest.files[`${assetPath}/index.html`]) {153227 // Directory hit without trailing slash: redirect so relative URLs resolve.154228 return publicSiteRedirect(new URL(`${path}/`, url).toString(), addressStr);@@ -157,7 +231,9 @@ export async function serve(req: Request, env: Env, url: URL): Promise<Response>
157231 158232 const etag = `"${entry.h}"`;159233 const protectedSite = site.password_hash !== null;160- const cacheControl = protectedSite ? "private, no-store" : "public, max-age=60";234+ const cacheControl = protectedSite235+ ? "private, no-store"236+ : "public, max-age=60";161237 // Never validate a previously cached protected response with 304: doing so162238 // would let the browser reuse its stored body after the site was locked.163239 if (!protectedSite && req.headers.get("if-none-match") === etag) {@@ -0,0 +1,164 @@
1+import assert from "node:assert/strict";2+import test from "node:test";3+4+import { sha256hex } from "../../shared/hash.ts";5+import { MAX_BLOB_BYTES } from "../../shared/limits.ts";6+import { api } from "../src/api.ts";7+8+function envForBlob() {9+ const puts = [];10+ return {11+ puts,12+ env: {13+ CONTENT: {14+ put: async (key, bytes) => puts.push([key, bytes]),15+ },16+ },17+ };18+}19+20+async function put(request, env, hash = "a".repeat(64)) {21+ const url = new URL(`https://n.zip/api/blob/${hash}`);22+ return await api(request, env, url, {});23+}24+25+test("blob upload requires content-length before reading the body", async () => {26+ const { env, puts } = envForBlob();27+ const response = await put(28+ new Request(`https://n.zip/api/blob/${"a".repeat(64)}`, {29+ method: "PUT",30+ body: new Uint8Array([1]),31+ }),32+ env,33+ );34+35+ assert.equal(response.status, 411);36+ assert.equal(puts.length, 0);37+});38+39+test("blob upload rejects an oversized declared body before reading it", async () => {40+ const { env, puts } = envForBlob();41+ const response = await put(42+ new Request(`https://n.zip/api/blob/${"a".repeat(64)}`, {43+ method: "PUT",44+ headers: { "content-length": String(MAX_BLOB_BYTES + 1) },45+ body: new Uint8Array([1]),46+ }),47+ env,48+ );49+50+ assert.equal(response.status, 413);51+ assert.equal(puts.length, 0);52+});53+54+test("blob upload rejects a body that disagrees with its declared length", async () => {55+ const { env, puts } = envForBlob();56+ const response = await put(57+ new Request(`https://n.zip/api/blob/${"a".repeat(64)}`, {58+ method: "PUT",59+ headers: { "content-length": "1" },60+ body: new Uint8Array([1, 2]),61+ }),62+ env,63+ );64+65+ assert.equal(response.status, 400);66+ assert.equal(puts.length, 0);67+});68+69+test("blob upload accepts a bounded body with a matching hash", async () => {70+ const { env, puts } = envForBlob();71+ const bytes = new Uint8Array([1, 2, 3]);72+ const hash = await sha256hex(bytes);73+ const response = await put(74+ new Request(`https://n.zip/api/blob/${hash}`, {75+ method: "PUT",76+ headers: { "content-length": String(bytes.length) },77+ body: bytes,78+ }),79+ env,80+ hash,81+ );82+83+ assert.equal(response.status, 200);84+ assert.equal(puts.length, 1);85+ assert.equal(puts[0][0], `blob/${hash}`);86+});87+88+test("prepare performs no more than six concurrent R2 HEAD requests", async () => {89+ const files = {};90+ for (let i = 0; i < 13; i++) {91+ files[`file-${i}.txt`] = {92+ h: i.toString(16).padStart(64, "0"),93+ s: 1,94+ ct: "text/plain",95+ };96+ }97+ let active = 0;98+ let maximumActive = 0;99+ const env = {100+ CONTENT: {101+ head: async () => {102+ active++;103+ maximumActive = Math.max(maximumActive, active);104+ await new Promise((resolve) => setImmediate(resolve));105+ active--;106+ return null;107+ },108+ },109+ };110+ const url = new URL("https://n.zip/api/push/prepare");111+ const response = await api(112+ new Request(url, {113+ method: "POST",114+ headers: { "content-type": "application/json" },115+ body: JSON.stringify({ manifest: { v: 1, files } }),116+ }),117+ env,118+ url,119+ {},120+ );121+122+ assert.equal(response.status, 200);123+ assert.equal(maximumActive, 6);124+ assert.equal((await response.json()).missing.length, 13);125+});126+127+test("shared validation errors are typed as bad requests", async () => {128+ const url = new URL("https://n.zip/api/push/prepare");129+ const response = await api(130+ new Request(url, {131+ method: "POST",132+ headers: { "content-type": "application/json" },133+ body: JSON.stringify({ manifest: { v: 2, files: {} } }),134+ }),135+ {},136+ url,137+ {},138+ );139+140+ assert.equal(response.status, 400);141+ assert.deepEqual(await response.json(), { error: "unsupported manifest version" });142+});143+144+test("unexpected storage errors are logged but not exposed", async () => {145+ const url = new URL("https://n.zip/api/push/prepare");146+ const response = await api(147+ new Request(url, {148+ method: "POST",149+ headers: { "content-type": "application/json" },150+ body: JSON.stringify({151+ manifest: {152+ v: 1,153+ files: { "index.html": { h: "a".repeat(64), s: 1, ct: "text/html" } },154+ },155+ }),156+ }),157+ { CONTENT: { head: async () => Promise.reject(new Error("private storage detail")) } },158+ url,159+ {},160+ );161+162+ assert.equal(response.status, 500);163+ assert.deepEqual(await response.json(), { error: "internal error" });164+});@@ -0,0 +1,26 @@
1+CREATE TABLE vaults (2+ slot INTEGER PRIMARY KEY CHECK (slot BETWEEN 0 AND 15),3+ name TEXT NOT NULL UNIQUE,4+ created_at INTEGER NOT NULL5+);6+7+CREATE TABLE sites (8+ address INTEGER PRIMARY KEY CHECK (address BETWEEN 0 AND 65535),9+ vault_slot INTEGER NOT NULL REFERENCES vaults(slot),10+ alias TEXT,11+ current_manifest TEXT NOT NULL,12+ created_at INTEGER NOT NULL,13+ updated_at INTEGER NOT NULL,14+ expires_at INTEGER,15+ password_hash TEXT,16+ UNIQUE (vault_slot, alias)17+);18+19+CREATE TABLE pushes (20+ address INTEGER NOT NULL REFERENCES sites(address) ON DELETE CASCADE,21+ seq INTEGER NOT NULL,22+ manifest_hash TEXT NOT NULL,23+ pushed_at INTEGER NOT NULL,24+ note TEXT,25+ PRIMARY KEY (address, seq)26+);@@ -0,0 +1,84 @@
1+import {2+ createExecutionContext,3+ env,4+ SELF,5+ waitOnExecutionContext,6+} from "cloudflare:test";7+import { describe, expect, it } from "vitest";8+import worker from "../src/index.ts";9+import { hashPassword, verifyPassword } from "../src/password.ts";10+11+describe("Worker runtime", () => {12+ it("serves the landing page with public cache headers", async () => {13+ const response = await SELF.fetch("https://share.example.com/");14+15+ expect(response.status).toBe(200);16+ expect(response.headers.get("cache-control")).toBe("public, max-age=3600");17+ expect(await response.text()).toContain("nzip");18+ });19+20+ it("rejects unauthenticated API requests before touching storage", async () => {21+ const response = await SELF.fetch("https://share.example.com/api/status");22+23+ expect(response.status).toBe(401);24+ expect(await response.json()).toEqual({ error: "unauthorized" });25+ });26+27+ it("applies the complete D1 migration set", async () => {28+ const table = await env.DB.prepare(29+ "SELECT name FROM sqlite_master WHERE type = 'table' AND name = 'security_incidents'",30+ ).first<{ name: string }>();31+32+ expect(table?.name).toBe("security_incidents");33+ });34+35+ it("routes authenticated status requests with the shared release version", async () => {36+ const response = await SELF.fetch("https://share.example.com/api/status", {37+ headers: { authorization: "Bearer runtime-test-token" },38+ });39+40+ expect(response.status).toBe(200);41+ expect((await response.json<{ version: string }>()).version).toBe("0.3.0");42+ });43+44+ it("evaluates a real D1 probe window through the scheduled handler", async () => {45+ const now = Math.floor(Date.now() / 300_000) * 300;46+ const bucket = now - 300;47+ await env.DB.batch(48+ Array.from({ length: 20 }, (_, address) =>49+ env.DB.prepare(50+ `INSERT INTO security_probes51+ (bucket, scanner_id, address, vault_slot, is_live, country, asn)52+ VALUES (?, 'runtime-scanner', ?, 0, 0, 'US', 64500)`,53+ ).bind(bucket, address)),54+ );55+ const ctx = createExecutionContext();56+57+ await worker.scheduled(58+ { cron: "*/5 * * * *", scheduledTime: Date.now(), noRetry() {} },59+ env,60+ ctx,61+ );62+ await waitOnExecutionContext(ctx);63+64+ const incident = await env.DB.prepare(65+ "SELECT status, severity FROM security_incidents WHERE name = 'enumeration'",66+ ).first<{ status: string; severity: number }>();67+ const notification = await env.DB.prepare(68+ "SELECT attempts, sent_at FROM security_notifications WHERE action = 'open'",69+ ).first<{ attempts: number; sent_at: number | null }>();70+ expect(incident).toMatchObject({ status: "open", severity: 1 });71+ expect(notification?.attempts).toBe(1);72+ expect(notification?.sent_at).not.toBeNull();73+ });74+75+ it("runs password hashing and verification inside workerd", async () => {76+ const started = performance.now();77+ const encoded = await hashPassword("runtime-password");78+ const elapsedMs = performance.now() - started;79+80+ expect(await verifyPassword("runtime-password", encoded)).toBe(true);81+ expect(elapsedMs).toBeLessThan(1_000);82+ console.log({ event: "test.pbkdf2_runtime", elapsedMs });83+ });84+});@@ -1,7 +1,10 @@
11 import assert from "node:assert/strict";22 import test from "node:test";33 4-import { buildSecurityRequestEvent } from "../src/observability.ts";4+import {5+ buildSecurityRequestEvent,6+ recordEnumerationProbe,7+} from "../src/observability.ts";58 69 function eventFor(path, status, method = "GET") {710 const url = new URL(path, "https://n.zip");@@ -50,3 +53,130 @@ test("missing assets and unlock attempts retain their request class", () => {
5053 assert.equal(eventFor("/0123/__unlock", 429, "POST")?.path_class, "unlock");5154 assert.equal(eventFor("/0123/__unlock", 429, "POST")?.result, "rate_limited");5255 });56+57+class FakeRateLimit {58+ calls = [];59+60+ constructor(allow) {61+ this.allow = allow;62+ }63+64+ async limit(options) {65+ this.calls.push(options);66+ return { success: this.allow(this.calls.length) };67+ }68+}69+70+class FakeStatement {71+ constructor(db, sql) {72+ this.db = db;73+ this.sql = sql;74+ this.params = [];75+ }76+77+ bind(...params) {78+ this.params = params;79+ return this;80+ }81+82+ async run() {83+ this.db.writes.push({ sql: this.sql, params: this.params });84+ return { success: true };85+ }86+}87+88+class FakeD1 {89+ writes = [];90+91+ prepare(sql) {92+ return new FakeStatement(this, sql);93+ }94+}95+96+function recordingEnv({ observe = () => true, signal = () => true } = {}) {97+ const DB = new FakeD1();98+ const RL_OBSERVE = new FakeRateLimit(observe);99+ const RL_SIGNAL = new FakeRateLimit(signal);100+ return {101+ DB,102+ RL_OBSERVE,103+ RL_SIGNAL,104+ NZIP_TOKEN: "test-scanner-digest-secret",105+ };106+}107+108+function record(path, status, env, ip = "192.0.2.10") {109+ const url = new URL(path, "https://n.zip");110+ return recordEnumerationProbe(111+ new Request(url, { headers: { "cf-connecting-ip": ip } }),112+ env,113+ url,114+ new Response(null, { status }),115+ );116+}117+118+test("ordinary probes use the observation persistence quota", async () => {119+ const env = recordingEnv();120+121+ await record("/0123", 404, env);122+123+ assert.equal(env.RL_OBSERVE.calls.length, 1);124+ assert.equal(env.RL_SIGNAL.calls.length, 0);125+ assert.equal(env.DB.writes.length, 1);126+ assert.match(env.DB.writes[0].sql, /security_probes/);127+});128+129+test("denied observation quota performs no D1 writes", async () => {130+ const env = recordingEnv({ observe: () => false });131+132+ await record("/0123", 404, env);133+134+ assert.equal(env.RL_OBSERVE.calls.length, 1);135+ assert.equal(env.RL_SIGNAL.calls.length, 0);136+ assert.equal(env.DB.writes.length, 0);137+});138+139+test("429 confirmations use the signal quota instead of bypassing persistence limits", async () => {140+ const env = recordingEnv();141+142+ await record("/0123", 429, env);143+144+ assert.equal(env.RL_OBSERVE.calls.length, 0);145+ assert.equal(env.RL_SIGNAL.calls.length, 1);146+ assert.equal(env.DB.writes.length, 2);147+ assert.match(env.DB.writes[0].sql, /security_probes/);148+ assert.match(env.DB.writes[1].sql, /security_signals/);149+ assert.match(env.DB.writes[1].sql, /'rate_limited'/);150+ assert.equal(env.RL_SIGNAL.calls[0].key.length, 16);151+});152+153+test("denied signal quota performs no D1 writes for 429 responses", async () => {154+ const env = recordingEnv({ signal: () => false });155+156+ await record("/0123", 429, env);157+158+ assert.equal(env.RL_OBSERVE.calls.length, 0);159+ assert.equal(env.RL_SIGNAL.calls.length, 1);160+ assert.equal(env.DB.writes.length, 0);161+});162+163+test("a flood of 429 responses is bounded to one permitted persistence pair", async () => {164+ const env = recordingEnv({ signal: (callNumber) => callNumber === 1 });165+166+ await Promise.all(167+ Array.from({ length: 250 }, () => record("/0123", 429, env)),168+ );169+170+ assert.equal(env.RL_OBSERVE.calls.length, 0);171+ assert.equal(env.RL_SIGNAL.calls.length, 250);172+ assert.equal(env.DB.writes.length, 2);173+ assert.equal(174+ env.DB.writes.filter(({ sql }) => sql.includes("security_probes")).length,175+ 1,176+ );177+ assert.equal(178+ env.DB.writes.filter(({ sql }) => sql.includes("security_signals")).length,179+ 1,180+ );181+ assert.equal(new Set(env.RL_SIGNAL.calls.map(({ key }) => key)).size, 1);182+});@@ -0,0 +1,8 @@
1+import { applyD1Migrations, type D1Migration, env } from "cloudflare:test";2+3+const testEnv = env as Cloudflare.Env & {4+ TEST_BASE_MIGRATIONS: D1Migration[];5+ TEST_MIGRATIONS: D1Migration[];6+};7+await applyD1Migrations(testEnv.DB, testEnv.TEST_BASE_MIGRATIONS);8+await applyD1Migrations(testEnv.DB, testEnv.TEST_MIGRATIONS);@@ -1,7 +1,13 @@
11 import assert from "node:assert/strict";22 import test from "node:test";33 4-import { chooseAlertAction, signalSeverity } from "../src/security_alerts.ts";4+import {5+ chooseAlertAction,6+ drainSecurityNotifications,7+ evaluateEnumerationWindow,8+ sendDailySecurityDigest,9+ signalSeverity,10+} from "../src/security_alerts.ts";511 612 function stats(overrides = {}) {713 return {@@ -124,3 +130,262 @@ test("an incident resolves on its third quiet window", () => {
124130 "resolve",125131 );126132 });133+134+class FakeStatement {135+ constructor(db, sql, args = []) {136+ this.db = db;137+ this.sql = sql;138+ this.args = args;139+ }140+141+ bind(...args) {142+ return new FakeStatement(this.db, this.sql, args);143+ }144+145+ async all() {146+ if (this.sql.includes("FROM security_probes")) {147+ return { results: this.db.probes };148+ }149+ if (this.sql.includes("FROM security_notifications")) {150+ return {151+ results: [...this.db.notifications.values()]152+ .filter((row) => row.sent_at === null)153+ .sort((a, b) => a.created_at - b.created_at),154+ };155+ }156+ throw new Error(`unexpected all(): ${this.sql}`);157+ }158+159+ async first() {160+ if (161+ this.sql.includes("COUNT(*) AS addresses") &&162+ this.sql.includes("FROM security_probes")163+ ) {164+ return {165+ addresses: this.db.probes.length,166+ scanners: new Set(this.db.probes.map((probe) => probe.scanner_id)).size,167+ live_hits: this.db.probes.reduce(168+ (sum, probe) => sum + probe.is_live,169+ 0,170+ ),171+ };172+ }173+ if (this.sql.includes("FROM security_signals")) {174+ return { count: this.db.rateLimited };175+ }176+ if (this.sql.includes("FROM security_incidents")) {177+ return this.db.incident;178+ }179+ throw new Error(`unexpected first(): ${this.sql}`);180+ }181+182+ async run() {183+ if (this.sql.includes("INSERT INTO security_incidents")) {184+ const [185+ status,186+ severity,187+ opened_at,188+ last_seen_at,189+ last_alerted_at,190+ quiet_windows,191+ peak_addresses,192+ total_addresses,193+ total_live_hits,194+ total_rate_limited,195+ vault_mask,196+ ] = this.args;197+ this.db.incident = {198+ status,199+ severity,200+ opened_at,201+ last_seen_at,202+ last_alerted_at,203+ quiet_windows,204+ peak_addresses,205+ total_addresses,206+ total_live_hits,207+ total_rate_limited,208+ vault_mask,209+ };210+ return { success: true };211+ }212+ if (213+ this.sql.includes("INSERT OR IGNORE INTO security_notifications") &&214+ this.sql.includes("'daily-digest'")215+ ) {216+ const [id, window_bucket, subject, text, created_at] = this.args;217+ if (!this.db.notifications.has(id)) {218+ this.db.notifications.set(id, {219+ id,220+ incident_name: "security",221+ action: "daily-digest",222+ window_bucket,223+ subject,224+ text,225+ html: null,226+ created_at,227+ sent_at: null,228+ attempts: 0,229+ last_error: null,230+ });231+ }232+ return { success: true };233+ }234+ if (this.sql.includes("INSERT OR IGNORE INTO security_notifications")) {235+ const [236+ id,237+ incident_name,238+ action,239+ window_bucket,240+ subject,241+ text,242+ html,243+ created_at,244+ ] = this.args;245+ if (!this.db.notifications.has(id)) {246+ this.db.notifications.set(id, {247+ id,248+ incident_name,249+ action,250+ window_bucket,251+ subject,252+ text,253+ html,254+ created_at,255+ sent_at: null,256+ attempts: 0,257+ last_error: null,258+ });259+ }260+ return { success: true };261+ }262+ if (this.sql.includes("SET sent_at = ?")) {263+ const [sentAt, id] = this.args;264+ const row = this.db.notifications.get(id);265+ if (row?.sent_at === null) {266+ row.sent_at = sentAt;267+ row.attempts += 1;268+ row.last_error = null;269+ }270+ return { success: true };271+ }272+ if (this.sql.includes("SET attempts = attempts + 1")) {273+ const [lastError, id] = this.args;274+ const row = this.db.notifications.get(id);275+ if (row?.sent_at === null) {276+ row.attempts += 1;277+ row.last_error = lastError;278+ }279+ return { success: true };280+ }281+ throw new Error(`unexpected run(): ${this.sql}`);282+ }283+}284+285+class FakeDb {286+ probes = [];287+ rateLimited = 0;288+ incident = null;289+ notifications = new Map();290+ batches = 0;291+292+ prepare(sql) {293+ return new FakeStatement(this, sql);294+ }295+296+ async batch(statements) {297+ this.batches += 1;298+ const results = [];299+ for (const statement of statements) results.push(await statement.run());300+ return results;301+ }302+}303+304+test("a transient email failure preserves and retries the original alert", async () => {305+ const db = new FakeDb();306+ db.probes = Array.from({ length: 20 }, (_, address) => ({307+ scanner_id: "scanner-a",308+ address,309+ is_live: 0,310+ vault_slot: 0,311+ country: "US",312+ asn: 64500,313+ }));314+ const deliveries = [];315+ let failNext = true;316+ const env = {317+ DB: db,318+ ALERT_EMAIL_FROM: "security@example.com",319+ ALERT_EMAIL_TO: "operator@example.com",320+ EMAIL: {321+ async send(message) {322+ // Both durable records must exist before delivery is attempted.323+ assert.equal(db.incident?.status, "open");324+ assert.equal(db.notifications.size, 1);325+ deliveries.push(message);326+ if (failNext) {327+ failNext = false;328+ throw new Error("temporary email outage");329+ }330+ },331+ },332+ };333+334+ await evaluateEnumerationWindow(env, 600);335+336+ const [notification] = db.notifications.values();337+ assert.equal(db.batches, 1);338+ assert.equal(notification.id, "enumeration:open:300");339+ assert.equal(notification.sent_at, null);340+ assert.equal(notification.attempts, 1);341+ assert.equal(notification.last_error, "temporary email outage");342+ assert.equal(db.incident.last_alerted_at, 600);343+344+ // A later cron drain retries the exact stored payload, not a new window.345+ await drainSecurityNotifications(env, 900);346+347+ assert.equal(deliveries.length, 2);348+ assert.deepEqual(deliveries[1], deliveries[0]);349+ assert.match(deliveries[1].text, /Notification ID: enumeration:open:300/);350+ assert.equal(notification.sent_at, 900);351+ assert.equal(notification.attempts, 2);352+ assert.equal(notification.last_error, null);353+});354+355+test("the daily digest is durably deduplicated by UTC day", async () => {356+ const db = new FakeDb();357+ db.probes = [{358+ scanner_id: "scanner-a",359+ address: 1,360+ is_live: 0,361+ vault_slot: 0,362+ country: null,363+ asn: null,364+ }];365+ let sends = 0;366+ const env = {367+ DB: db,368+ ALERT_EMAIL_FROM: "security@example.com",369+ ALERT_EMAIL_TO: "operator@example.com",370+ EMAIL: {371+ async send() {372+ sends += 1;373+ if (sends === 1) throw new Error("temporary email outage");374+ },375+ },376+ };377+ const now = 2 * 86400 + 123;378+379+ await sendDailySecurityDigest(env, now);380+ await sendDailySecurityDigest(env, now + 60);381+382+ assert.equal(db.notifications.size, 1);383+ const [notification] = db.notifications.values();384+ assert.equal(notification.id, `security:daily-digest:${2 * 86400}`);385+ assert.match(386+ notification.text,387+ new RegExp(`Notification ID: ${notification.id}`),388+ );389+ assert.equal(notification.sent_at, now + 60);390+ assert.equal(notification.attempts, 2);391+});@@ -5,7 +5,7 @@ import { serve } from "../src/serve.ts";
55 66 const HASH = "a".repeat(64);77 8-function envFor(files) {8+function envFor(files, siteOverrides = {}) {99 const manifest = new TextEncoder().encode(JSON.stringify({ v: 1, files }));1010 const site = {1111 address: 0x2f9b,@@ -17,6 +17,7 @@ function envFor(files) {
1717 expires_at: null,1818 password_hash: null,1919 auth_version: 1,20+ ...siteOverrides,2021 };2122 2223 return {@@ -121,3 +122,55 @@ test("favicon serves the wordmark PNG with a cacheable response", async () => {
121122 assert.deepEqual([...bytes.slice(0, 8)], [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);122123 assert.ok(bytes.length > 500);123124 });125+126+test("unlock requires a declared request size", async () => {127+ const url = new URL("https://n.zip/2f9b/__unlock");128+ const response = await serve(129+ new Request(url, {130+ method: "POST",131+ headers: { "content-type": "application/x-www-form-urlencoded" },132+ body: "password=test",133+ }),134+ envFor({ "index.html": html }, { password_hash: "unused" }),135+ url,136+ );137+138+ assert.equal(response.status, 411);139+});140+141+test("unlock rejects an oversized declared body before password verification", async () => {142+ const url = new URL("https://n.zip/2f9b/__unlock");143+ const response = await serve(144+ new Request(url, {145+ method: "POST",146+ headers: {147+ "content-type": "application/x-www-form-urlencoded",148+ "content-length": "4097",149+ },150+ body: "password=test",151+ }),152+ envFor({ "index.html": html }, { password_hash: "unused" }),153+ url,154+ );155+156+ assert.equal(response.status, 413);157+});158+159+test("unlock rejects passwords above 256 characters before hashing", async () => {160+ const url = new URL("https://n.zip/2f9b/__unlock");161+ const body = new URLSearchParams({ password: "x".repeat(257) }).toString();162+ const response = await serve(163+ new Request(url, {164+ method: "POST",165+ headers: {166+ "content-type": "application/x-www-form-urlencoded",167+ "content-length": String(new TextEncoder().encode(body).length),168+ },169+ body,170+ }),171+ envFor({ "index.html": html }, { password_hash: "unused" }),172+ url,173+ );174+175+ assert.equal(response.status, 400);176+});@@ -4,11 +4,13 @@
44 "module": "ESNext",55 "moduleResolution": "bundler",66 "lib": ["ES2023"],7- "types": ["@cloudflare/workers-types"],7+ "types": ["@types/node", "@cloudflare/vitest-pool-workers/types"],88 "strict": true,9+ "noUnusedLocals": true,10+ "noUnusedParameters": true,911 "noEmit": true,1012 "allowImportingTsExtensions": true,1113 "skipLibCheck": true1214 },13- "include": ["src/**/*.ts", "../shared/**/*.ts"]15+ "include": ["src/**/*.ts", "test/**/*.ts", "vitest.config.ts", "worker-configuration.d.ts", "../shared/*.ts"]1416 }@@ -0,0 +1,26 @@
1+import {2+ cloudflareTest,3+ readD1Migrations,4+} from "@cloudflare/vitest-pool-workers";5+import { defineConfig } from "vitest/config";6+7+export default defineConfig({8+ plugins: [9+ cloudflareTest(async () => ({10+ wrangler: { configPath: "./wrangler.jsonc" },11+ miniflare: {12+ bindings: {13+ NZIP_TOKEN: "runtime-test-token",14+ TEST_BASE_MIGRATIONS: await readD1Migrations(15+ "./test/base-migrations",16+ ),17+ TEST_MIGRATIONS: await readD1Migrations("./migrations"),18+ },19+ },20+ })),21+ ],22+ test: {23+ setupFiles: ["./test/runtime.setup.ts"],24+ include: ["./test/**/*.runtime.test.ts"],25+ },26+});@@ -0,0 +1,14723 @@
1+/* eslint-disable */2+// Generated by Wrangler by running `wrangler types` (hash: 990c104e67727cf0d631159dadc2a132)3+// Runtime types generated with workerd@1.20260708.1 2026-07-11 nodejs_compat4+interface __BaseEnv_Env {5+ CONTENT: R2Bucket;6+ DB: D1Database;7+ EMAIL: SendEmail;8+ RL_ENUM: RateLimit;9+ RL_UNLOCK: RateLimit;10+ RL_OBSERVE: RateLimit;11+ RL_SIGNAL: RateLimit;12+ ALERT_EMAIL_TO: "operator@example.com";13+ ALERT_EMAIL_FROM: "alerts@share.example.com";14+ NZIP_TOKEN: string;15+ PUBLIC_BASE: string;16+}17+declare namespace Cloudflare {18+ interface GlobalProps {19+ mainModule: typeof import("./src/index");20+ }21+ interface Env extends __BaseEnv_Env {}22+}23+interface Env extends __BaseEnv_Env {}24+type StringifyValues<EnvType extends Record<string, unknown>> = {25+ [Binding in keyof EnvType]: EnvType[Binding] extends string ? EnvType[Binding] : string;26+};27+declare namespace NodeJS {28+ interface ProcessEnv extends StringifyValues<Pick<Cloudflare.Env, "ALERT_EMAIL_TO" | "ALERT_EMAIL_FROM" | "NZIP_TOKEN" | "PUBLIC_BASE">> {}29+}30+31+// Begin runtime types32+/*! *****************************************************************************33+Copyright (c) Cloudflare. All rights reserved.34+Copyright (c) Microsoft Corporation. All rights reserved.35+36+Licensed under the Apache License, Version 2.0 (the "License"); you may not use37+this file except in compliance with the License. You may obtain a copy of the38+License at http://www.apache.org/licenses/LICENSE-2.039+THIS CODE IS PROVIDED ON AN *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY40+KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION ANY IMPLIED41+WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A PARTICULAR PURPOSE,42+MERCHANTABLITY OR NON-INFRINGEMENT.43+See the Apache Version 2.0 License for specific language governing permissions44+and limitations under the License.45+***************************************************************************** */46+/* eslint-disable */47+// noinspection JSUnusedGlobalSymbols48+declare var onmessage: never;49+/**50+ * The **`DOMException`** interface represents an abnormal event (called an **exception**) that occurs as a result of calling a method or accessing a property of a web API.51+ *52+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/DOMException)53+ */54+declare class DOMException extends Error {55+ constructor(message?: string, name?: string);56+ /**57+ * The **`message`** read-only property of the a message or description associated with the given error name.58+ *59+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/DOMException/message)60+ */61+ readonly message: string;62+ /**63+ * The **`name`** read-only property of the one of the strings associated with an error name.64+ *65+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/DOMException/name)66+ */67+ readonly name: string;68+ /**69+ * The **`code`** read-only property of the DOMException interface returns one of the legacy error code constants, or `0` if none match.70+ * @deprecated71+ *72+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/DOMException/code)73+ */74+ readonly code: number;75+ static readonly INDEX_SIZE_ERR: number;76+ static readonly DOMSTRING_SIZE_ERR: number;77+ static readonly HIERARCHY_REQUEST_ERR: number;78+ static readonly WRONG_DOCUMENT_ERR: number;79+ static readonly INVALID_CHARACTER_ERR: number;80+ static readonly NO_DATA_ALLOWED_ERR: number;81+ static readonly NO_MODIFICATION_ALLOWED_ERR: number;82+ static readonly NOT_FOUND_ERR: number;83+ static readonly NOT_SUPPORTED_ERR: number;84+ static readonly INUSE_ATTRIBUTE_ERR: number;85+ static readonly INVALID_STATE_ERR: number;86+ static readonly SYNTAX_ERR: number;87+ static readonly INVALID_MODIFICATION_ERR: number;88+ static readonly NAMESPACE_ERR: number;89+ static readonly INVALID_ACCESS_ERR: number;90+ static readonly VALIDATION_ERR: number;91+ static readonly TYPE_MISMATCH_ERR: number;92+ static readonly SECURITY_ERR: number;93+ static readonly NETWORK_ERR: number;94+ static readonly ABORT_ERR: number;95+ static readonly URL_MISMATCH_ERR: number;96+ static readonly QUOTA_EXCEEDED_ERR: number;97+ static readonly TIMEOUT_ERR: number;98+ static readonly INVALID_NODE_TYPE_ERR: number;99+ static readonly DATA_CLONE_ERR: number;100+ get stack(): any;101+ set stack(value: any);102+}103+type WorkerGlobalScopeEventMap = {104+ fetch: FetchEvent;105+ scheduled: ScheduledEvent;106+ queue: QueueEvent;107+ unhandledrejection: PromiseRejectionEvent;108+ rejectionhandled: PromiseRejectionEvent;109+};110+declare abstract class WorkerGlobalScope extends EventTarget<WorkerGlobalScopeEventMap> {111+ EventTarget: typeof EventTarget;112+}113+/* The **`console`** object provides access to the debugging console (e.g., the Web console in Firefox). *114+ * The **`console`** object provides access to the debugging console (e.g., the Web console in Firefox).115+ *116+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/console)117+ */118+interface Console {119+ "assert"(condition?: boolean, ...data: any[]): void;120+ /**121+ * The **`console.clear()`** static method clears the console if possible.122+ *123+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/clear_static)124+ */125+ clear(): void;126+ /**127+ * The **`console.count()`** static method logs the number of times that this particular call to `count()` has been called.128+ *129+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/count_static)130+ */131+ count(label?: string): void;132+ /**133+ * The **`console.countReset()`** static method resets counter used with console/count_static.134+ *135+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/countReset_static)136+ */137+ countReset(label?: string): void;138+ /**139+ * The **`console.debug()`** static method outputs a message to the console at the 'debug' log level.140+ *141+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/debug_static)142+ */143+ debug(...data: any[]): void;144+ /**145+ * The **`console.dir()`** static method displays a list of the properties of the specified JavaScript object.146+ *147+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/dir_static)148+ */149+ dir(item?: any, options?: any): void;150+ /**151+ * The **`console.dirxml()`** static method displays an interactive tree of the descendant elements of the specified XML/HTML element.152+ *153+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/dirxml_static)154+ */155+ dirxml(...data: any[]): void;156+ /**157+ * The **`console.error()`** static method outputs a message to the console at the 'error' log level.158+ *159+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/error_static)160+ */161+ error(...data: any[]): void;162+ /**163+ * The **`console.group()`** static method creates a new inline group in the Web console log, causing any subsequent console messages to be indented by an additional level, until console/groupEnd_static is called.164+ *165+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/group_static)166+ */167+ group(...data: any[]): void;168+ /**169+ * The **`console.groupCollapsed()`** static method creates a new inline group in the console.170+ *171+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/groupCollapsed_static)172+ */173+ groupCollapsed(...data: any[]): void;174+ /**175+ * The **`console.groupEnd()`** static method exits the current inline group in the console.176+ *177+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/groupEnd_static)178+ */179+ groupEnd(): void;180+ /**181+ * The **`console.info()`** static method outputs a message to the console at the 'info' log level.182+ *183+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/info_static)184+ */185+ info(...data: any[]): void;186+ /**187+ * The **`console.log()`** static method outputs a message to the console.188+ *189+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/log_static)190+ */191+ log(...data: any[]): void;192+ /**193+ * The **`console.table()`** static method displays tabular data as a table.194+ *195+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/table_static)196+ */197+ table(tabularData?: any, properties?: string[]): void;198+ /**199+ * The **`console.time()`** static method starts a timer you can use to track how long an operation takes.200+ *201+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/time_static)202+ */203+ time(label?: string): void;204+ /**205+ * The **`console.timeEnd()`** static method stops a timer that was previously started by calling console/time_static.206+ *207+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/timeEnd_static)208+ */209+ timeEnd(label?: string): void;210+ /**211+ * The **`console.timeLog()`** static method logs the current value of a timer that was previously started by calling console/time_static.212+ *213+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/timeLog_static)214+ */215+ timeLog(label?: string, ...data: any[]): void;216+ timeStamp(label?: string): void;217+ /**218+ * The **`console.trace()`** static method outputs a stack trace to the console.219+ *220+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/trace_static)221+ */222+ trace(...data: any[]): void;223+ /**224+ * The **`console.warn()`** static method outputs a warning message to the console at the 'warning' log level.225+ *226+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/warn_static)227+ */228+ warn(...data: any[]): void;229+}230+declare const console: Console;231+type BufferSource = ArrayBufferView | ArrayBuffer;232+type TypedArray = Int8Array | Uint8Array | Uint8ClampedArray | Int16Array | Uint16Array | Int32Array | Uint32Array | Float32Array | Float64Array | BigInt64Array | BigUint64Array;233+declare namespace WebAssembly {234+ class CompileError extends Error {235+ constructor(message?: string);236+ }237+ class RuntimeError extends Error {238+ constructor(message?: string);239+ }240+ type ValueType = "anyfunc" | "externref" | "f32" | "f64" | "i32" | "i64" | "v128";241+ interface GlobalDescriptor {242+ value: ValueType;243+ mutable?: boolean;244+ }245+ class Global {246+ constructor(descriptor: GlobalDescriptor, value?: any);247+ value: any;248+ valueOf(): any;249+ }250+ type ImportValue = ExportValue | number;251+ type ModuleImports = Record<string, ImportValue>;252+ type Imports = Record<string, ModuleImports>;253+ type ExportValue = Function | Global | Memory | Table;254+ type Exports = Record<string, ExportValue>;255+ class Instance {256+ constructor(module: Module, imports?: Imports);257+ readonly exports: Exports;258+ }259+ interface MemoryDescriptor {260+ initial: number;261+ maximum?: number;262+ shared?: boolean;263+ }264+ class Memory {265+ constructor(descriptor: MemoryDescriptor);266+ readonly buffer: ArrayBuffer;267+ grow(delta: number): number;268+ }269+ type ImportExportKind = "function" | "global" | "memory" | "table";270+ interface ModuleExportDescriptor {271+ kind: ImportExportKind;272+ name: string;273+ }274+ interface ModuleImportDescriptor {275+ kind: ImportExportKind;276+ module: string;277+ name: string;278+ }279+ abstract class Module {280+ static customSections(module: Module, sectionName: string): ArrayBuffer[];281+ static exports(module: Module): ModuleExportDescriptor[];282+ static imports(module: Module): ModuleImportDescriptor[];283+ }284+ type TableKind = "anyfunc" | "externref";285+ interface TableDescriptor {286+ element: TableKind;287+ initial: number;288+ maximum?: number;289+ }290+ class Table {291+ constructor(descriptor: TableDescriptor, value?: any);292+ readonly length: number;293+ get(index: number): any;294+ grow(delta: number, value?: any): number;295+ set(index: number, value?: any): void;296+ }297+ function instantiate(module: Module, imports?: Imports): Promise<Instance>;298+ function validate(bytes: BufferSource): boolean;299+}300+/**301+ * The **`ServiceWorkerGlobalScope`** interface of the Service Worker API represents the global execution context of a service worker.302+ * Available only in secure contexts.303+ *304+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/ServiceWorkerGlobalScope)305+ */306+interface ServiceWorkerGlobalScope extends WorkerGlobalScope {307+ DOMException: typeof DOMException;308+ WorkerGlobalScope: typeof WorkerGlobalScope;309+ btoa(data: string): string;310+ atob(data: string): string;311+ setTimeout(callback: (...args: any[]) => void, msDelay?: number): number;312+ setTimeout<Args extends any[]>(callback: (...args: Args) => void, msDelay?: number, ...args: Args): number;313+ clearTimeout(timeoutId: number | null): void;314+ setInterval(callback: (...args: any[]) => void, msDelay?: number): number;315+ setInterval<Args extends any[]>(callback: (...args: Args) => void, msDelay?: number, ...args: Args): number;316+ clearInterval(timeoutId: number | null): void;317+ queueMicrotask(task: Function): void;318+ structuredClone<T>(value: T, options?: StructuredSerializeOptions): T;319+ reportError(error: any): void;320+ fetch(input: RequestInfo | URL, init?: RequestInit<RequestInitCfProperties>): Promise<Response>;321+ self: ServiceWorkerGlobalScope;322+ crypto: Crypto;323+ caches: CacheStorage;324+ scheduler: Scheduler;325+ performance: Performance;326+ Cloudflare: Cloudflare;327+ readonly origin: string;328+ Event: typeof Event;329+ ExtendableEvent: typeof ExtendableEvent;330+ CustomEvent: typeof CustomEvent;331+ PromiseRejectionEvent: typeof PromiseRejectionEvent;332+ FetchEvent: typeof FetchEvent;333+ TailEvent: typeof TailEvent;334+ TraceEvent: typeof TailEvent;335+ ScheduledEvent: typeof ScheduledEvent;336+ MessageEvent: typeof MessageEvent;337+ CloseEvent: typeof CloseEvent;338+ ReadableStreamDefaultReader: typeof ReadableStreamDefaultReader;339+ ReadableStreamBYOBReader: typeof ReadableStreamBYOBReader;340+ ReadableStream: typeof ReadableStream;341+ WritableStream: typeof WritableStream;342+ WritableStreamDefaultWriter: typeof WritableStreamDefaultWriter;343+ TransformStream: typeof TransformStream;344+ ByteLengthQueuingStrategy: typeof ByteLengthQueuingStrategy;345+ CountQueuingStrategy: typeof CountQueuingStrategy;346+ ErrorEvent: typeof ErrorEvent;347+ MessageChannel: typeof MessageChannel;348+ MessagePort: typeof MessagePort;349+ EventSource: typeof EventSource;350+ ReadableStreamBYOBRequest: typeof ReadableStreamBYOBRequest;351+ ReadableStreamDefaultController: typeof ReadableStreamDefaultController;352+ ReadableByteStreamController: typeof ReadableByteStreamController;353+ WritableStreamDefaultController: typeof WritableStreamDefaultController;354+ TransformStreamDefaultController: typeof TransformStreamDefaultController;355+ CompressionStream: typeof CompressionStream;356+ DecompressionStream: typeof DecompressionStream;357+ TextEncoderStream: typeof TextEncoderStream;358+ TextDecoderStream: typeof TextDecoderStream;359+ Headers: typeof Headers;360+ Body: typeof Body;361+ Request: typeof Request;362+ Response: typeof Response;363+ WebSocket: typeof WebSocket;364+ WebSocketPair: typeof WebSocketPair;365+ WebSocketRequestResponsePair: typeof WebSocketRequestResponsePair;366+ AbortController: typeof AbortController;367+ AbortSignal: typeof AbortSignal;368+ TextDecoder: typeof TextDecoder;369+ TextEncoder: typeof TextEncoder;370+ navigator: Navigator;371+ Navigator: typeof Navigator;372+ URL: typeof URL;373+ URLSearchParams: typeof URLSearchParams;374+ URLPattern: typeof URLPattern;375+ Blob: typeof Blob;376+ File: typeof File;377+ FormData: typeof FormData;378+ Crypto: typeof Crypto;379+ SubtleCrypto: typeof SubtleCrypto;380+ CryptoKey: typeof CryptoKey;381+ CacheStorage: typeof CacheStorage;382+ Cache: typeof Cache;383+ FixedLengthStream: typeof FixedLengthStream;384+ IdentityTransformStream: typeof IdentityTransformStream;385+ HTMLRewriter: typeof HTMLRewriter;386+}387+declare function addEventListener<Type extends keyof WorkerGlobalScopeEventMap>(type: Type, handler: EventListenerOrEventListenerObject<WorkerGlobalScopeEventMap[Type]>, options?: EventTargetAddEventListenerOptions | boolean): void;388+declare function removeEventListener<Type extends keyof WorkerGlobalScopeEventMap>(type: Type, handler: EventListenerOrEventListenerObject<WorkerGlobalScopeEventMap[Type]>, options?: EventTargetEventListenerOptions | boolean): void;389+/**390+ * The **`dispatchEvent()`** method of the EventTarget sends an Event to the object, (synchronously) invoking the affected event listeners in the appropriate order.391+ *392+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/EventTarget/dispatchEvent)393+ */394+declare function dispatchEvent(event: WorkerGlobalScopeEventMap[keyof WorkerGlobalScopeEventMap]): boolean;395+/* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Window/btoa) */396+declare function btoa(data: string): string;397+/* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Window/atob) */398+declare function atob(data: string): string;399+/* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Window/setTimeout) */400+declare function setTimeout(callback: (...args: any[]) => void, msDelay?: number): number;401+/* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Window/setTimeout) */402+declare function setTimeout<Args extends any[]>(callback: (...args: Args) => void, msDelay?: number, ...args: Args): number;403+/* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Window/clearTimeout) */404+declare function clearTimeout(timeoutId: number | null): void;405+/* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Window/setInterval) */406+declare function setInterval(callback: (...args: any[]) => void, msDelay?: number): number;407+/* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Window/setInterval) */408+declare function setInterval<Args extends any[]>(callback: (...args: Args) => void, msDelay?: number, ...args: Args): number;409+/* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Window/clearInterval) */410+declare function clearInterval(timeoutId: number | null): void;411+/* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Window/queueMicrotask) */412+declare function queueMicrotask(task: Function): void;413+/* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Window/structuredClone) */414+declare function structuredClone<T>(value: T, options?: StructuredSerializeOptions): T;415+/* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Window/reportError) */416+declare function reportError(error: any): void;417+/* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Window/fetch) */418+declare function fetch(input: RequestInfo | URL, init?: RequestInit<RequestInitCfProperties>): Promise<Response>;419+declare const self: ServiceWorkerGlobalScope;420+/**421+* The Web Crypto API provides a set of low-level functions for common cryptographic tasks.422+* The Workers runtime implements the full surface of this API, but with some differences in423+* the [supported algorithms](https://developers.cloudflare.com/workers/runtime-apis/web-crypto/#supported-algorithms)424+* compared to those implemented in most browsers.425+*426+* [Cloudflare Docs Reference](https://developers.cloudflare.com/workers/runtime-apis/web-crypto/)427+*/428+declare const crypto: Crypto;429+/**430+* The Cache API allows fine grained control of reading and writing from the Cloudflare global network cache.431+*432+* [Cloudflare Docs Reference](https://developers.cloudflare.com/workers/runtime-apis/cache/)433+*/434+declare const caches: CacheStorage;435+declare const scheduler: Scheduler;436+/**437+* The Workers runtime supports a subset of the Performance API, used to measure timing and performance,438+* as well as timing of subrequests and other operations.439+*440+* [Cloudflare Docs Reference](https://developers.cloudflare.com/workers/runtime-apis/performance/)441+*/442+declare const performance: Performance;443+declare const Cloudflare: Cloudflare;444+declare const origin: string;445+declare const navigator: Navigator;446+interface TestController {447+}448+interface ExecutionContext<Props = unknown> {449+ waitUntil(promise: Promise<any>): void;450+ passThroughOnException(): void;451+ readonly exports: Cloudflare.Exports;452+ readonly props: Props;453+ cache?: CacheContext;454+ readonly access?: CloudflareAccessContext;455+ tracing: Tracing;456+}457+type ExportedHandlerFetchHandler<Env = unknown, CfHostMetadata = unknown, Props = unknown> = (request: Request<CfHostMetadata, IncomingRequestCfProperties<CfHostMetadata>>, env: Env, ctx: ExecutionContext<Props>) => Response | Promise<Response>;458+type ExportedHandlerConnectHandler<Env = unknown, Props = unknown> = (socket: Socket, env: Env, ctx: ExecutionContext<Props>) => void | Promise<void>;459+type ExportedHandlerTailHandler<Env = unknown, Props = unknown> = (events: TraceItem[], env: Env, ctx: ExecutionContext<Props>) => void | Promise<void>;460+type ExportedHandlerTraceHandler<Env = unknown, Props = unknown> = (traces: TraceItem[], env: Env, ctx: ExecutionContext<Props>) => void | Promise<void>;461+type ExportedHandlerTailStreamHandler<Env = unknown, Props = unknown> = (event: TailStream.TailEvent<TailStream.Onset>, env: Env, ctx: ExecutionContext<Props>) => TailStream.TailEventHandlerType | Promise<TailStream.TailEventHandlerType>;462+type ExportedHandlerScheduledHandler<Env = unknown, Props = unknown> = (controller: ScheduledController, env: Env, ctx: ExecutionContext<Props>) => void | Promise<void>;463+type ExportedHandlerQueueHandler<Env = unknown, Message = unknown, Props = unknown> = (batch: MessageBatch<Message>, env: Env, ctx: ExecutionContext<Props>) => void | Promise<void>;464+type ExportedHandlerTestHandler<Env = unknown, Props = unknown> = (controller: TestController, env: Env, ctx: ExecutionContext<Props>) => void | Promise<void>;465+interface ExportedHandler<Env = unknown, QueueHandlerMessage = unknown, CfHostMetadata = unknown, Props = unknown> {466+ fetch?: ExportedHandlerFetchHandler<Env, CfHostMetadata, Props>;467+ connect?: ExportedHandlerConnectHandler<Env, Props>;468+ tail?: ExportedHandlerTailHandler<Env, Props>;469+ trace?: ExportedHandlerTraceHandler<Env, Props>;470+ tailStream?: ExportedHandlerTailStreamHandler<Env, Props>;471+ scheduled?: ExportedHandlerScheduledHandler<Env, Props>;472+ test?: ExportedHandlerTestHandler<Env, Props>;473+ email?: EmailExportedHandler<Env, Props>;474+ queue?: ExportedHandlerQueueHandler<Env, QueueHandlerMessage, Props>;475+}476+interface StructuredSerializeOptions {477+ transfer?: any[];478+}479+declare abstract class Navigator {480+ sendBeacon(url: string, body?: BodyInit): boolean;481+ readonly userAgent: string;482+ readonly hardwareConcurrency: number;483+ readonly platform: string;484+ readonly language: string;485+ readonly languages: string[];486+}487+interface AlarmInvocationInfo {488+ readonly isRetry: boolean;489+ readonly retryCount: number;490+ readonly scheduledTime: number;491+}492+interface Cloudflare {493+ readonly compatibilityFlags: Record<string, boolean>;494+}495+interface CachePurgeError {496+ code: number;497+ message: string;498+}499+interface CachePurgeResult {500+ success: boolean;501+ errors: CachePurgeError[];502+}503+interface CachePurgeOptions {504+ tags?: string[];505+ pathPrefixes?: string[];506+ purgeEverything?: boolean;507+}508+interface CacheContext {509+ purge(options: CachePurgeOptions): Promise<CachePurgeResult>;510+}511+interface CloudflareAccessContext {512+ readonly aud: string;513+ getIdentity(): Promise<CloudflareAccessIdentity | undefined>;514+}515+declare abstract class ColoLocalActorNamespace {516+ get(actorId: string): Fetcher;517+}518+interface DurableObject {519+ fetch(request: Request): Response | Promise<Response>;520+ connect?(socket: Socket): void | Promise<void>;521+ alarm?(alarmInfo?: AlarmInvocationInfo): void | Promise<void>;522+ webSocketMessage?(ws: WebSocket, message: string | ArrayBuffer): void | Promise<void>;523+ webSocketClose?(ws: WebSocket, code: number, reason: string, wasClean: boolean): void | Promise<void>;524+ webSocketError?(ws: WebSocket, error: unknown): void | Promise<void>;525+}526+type DurableObjectStub<T extends Rpc.DurableObjectBranded | undefined = undefined> = Fetcher<T, "alarm" | "connect" | "webSocketMessage" | "webSocketClose" | "webSocketError"> & {527+ readonly id: DurableObjectId;528+ readonly name?: string;529+};530+interface DurableObjectId {531+ toString(): string;532+ equals(other: DurableObjectId): boolean;533+ readonly name?: string;534+ readonly jurisdiction?: string;535+}536+declare abstract class DurableObjectNamespace<T extends Rpc.DurableObjectBranded | undefined = undefined> {537+ newUniqueId(options?: DurableObjectNamespaceNewUniqueIdOptions): DurableObjectId;538+ idFromName(name: string): DurableObjectId;539+ idFromString(id: string): DurableObjectId;540+ get(id: DurableObjectId, options?: DurableObjectNamespaceGetDurableObjectOptions): DurableObjectStub<T>;541+ getByName(name: string, options?: DurableObjectNamespaceGetDurableObjectOptions): DurableObjectStub<T>;542+ jurisdiction(jurisdiction: DurableObjectJurisdiction): DurableObjectNamespace<T>;543+}544+type DurableObjectJurisdiction = "eu" | "fedramp" | "fedramp-high" | "us";545+interface DurableObjectNamespaceNewUniqueIdOptions {546+ jurisdiction?: DurableObjectJurisdiction;547+}548+type DurableObjectLocationHint = "wnam" | "enam" | "sam" | "weur" | "eeur" | "apac" | "apac-ne" | "apac-se" | "oc" | "afr" | "me";549+type DurableObjectRoutingMode = "primary-only";550+interface DurableObjectNamespaceGetDurableObjectOptions {551+ locationHint?: DurableObjectLocationHint;552+ routingMode?: DurableObjectRoutingMode;553+}554+interface DurableObjectClass<_T extends Rpc.DurableObjectBranded | undefined = undefined> {555+}556+interface DurableObjectState<Props = unknown> {557+ waitUntil(promise: Promise<any>): void;558+ readonly exports: Cloudflare.Exports;559+ readonly props: Props;560+ readonly id: DurableObjectId;561+ readonly storage: DurableObjectStorage;562+ container?: Container;563+ facets: DurableObjectFacets;564+ blockConcurrencyWhile<T>(callback: () => Promise<T>): Promise<T>;565+ acceptWebSocket(ws: WebSocket, tags?: string[]): void;566+ getWebSockets(tag?: string): WebSocket[];567+ setWebSocketAutoResponse(maybeReqResp?: WebSocketRequestResponsePair): void;568+ getWebSocketAutoResponse(): WebSocketRequestResponsePair | null;569+ getWebSocketAutoResponseTimestamp(ws: WebSocket): Date | null;570+ setHibernatableWebSocketEventTimeout(timeoutMs?: number): void;571+ getHibernatableWebSocketEventTimeout(): number | null;572+ getTags(ws: WebSocket): string[];573+ abort(reason?: string): void;574+}575+interface DurableObjectTransaction {576+ get<T = unknown>(key: string, options?: DurableObjectGetOptions): Promise<T | undefined>;577+ get<T = unknown>(keys: string[], options?: DurableObjectGetOptions): Promise<Map<string, T>>;578+ list<T = unknown>(options?: DurableObjectListOptions): Promise<Map<string, T>>;579+ put<T>(key: string, value: T, options?: DurableObjectPutOptions): Promise<void>;580+ put<T>(entries: Record<string, T>, options?: DurableObjectPutOptions): Promise<void>;581+ delete(key: string, options?: DurableObjectPutOptions): Promise<boolean>;582+ delete(keys: string[], options?: DurableObjectPutOptions): Promise<number>;583+ rollback(): void;584+ getAlarm(options?: DurableObjectGetAlarmOptions): Promise<number | null>;585+ setAlarm(scheduledTime: number | Date, options?: DurableObjectSetAlarmOptions): Promise<void>;586+ deleteAlarm(options?: DurableObjectSetAlarmOptions): Promise<void>;587+}588+interface DurableObjectStorage {589+ get<T = unknown>(key: string, options?: DurableObjectGetOptions): Promise<T | undefined>;590+ get<T = unknown>(keys: string[], options?: DurableObjectGetOptions): Promise<Map<string, T>>;591+ list<T = unknown>(options?: DurableObjectListOptions): Promise<Map<string, T>>;592+ put<T>(key: string, value: T, options?: DurableObjectPutOptions): Promise<void>;593+ put<T>(entries: Record<string, T>, options?: DurableObjectPutOptions): Promise<void>;594+ delete(key: string, options?: DurableObjectPutOptions): Promise<boolean>;595+ delete(keys: string[], options?: DurableObjectPutOptions): Promise<number>;596+ deleteAll(options?: DurableObjectPutOptions): Promise<void>;597+ transaction<T>(closure: (txn: DurableObjectTransaction) => Promise<T>): Promise<T>;598+ getAlarm(options?: DurableObjectGetAlarmOptions): Promise<number | null>;599+ setAlarm(scheduledTime: number | Date, options?: DurableObjectSetAlarmOptions): Promise<void>;600+ deleteAlarm(options?: DurableObjectSetAlarmOptions): Promise<void>;601+ sync(): Promise<void>;602+ sql: SqlStorage;603+ kv: SyncKvStorage;604+ transactionSync<T>(closure: () => T): T;605+ getCurrentBookmark(): Promise<string>;606+ getBookmarkForTime(timestamp: number | Date): Promise<string>;607+ onNextSessionRestoreBookmark(bookmark: string): Promise<string>;608+}609+interface DurableObjectListOptions {610+ start?: string;611+ startAfter?: string;612+ end?: string;613+ prefix?: string;614+ reverse?: boolean;615+ limit?: number;616+ allowConcurrency?: boolean;617+ noCache?: boolean;618+}619+interface DurableObjectGetOptions {620+ allowConcurrency?: boolean;621+ noCache?: boolean;622+}623+interface DurableObjectGetAlarmOptions {624+ allowConcurrency?: boolean;625+}626+interface DurableObjectPutOptions {627+ allowConcurrency?: boolean;628+ allowUnconfirmed?: boolean;629+ noCache?: boolean;630+}631+interface DurableObjectSetAlarmOptions {632+ allowConcurrency?: boolean;633+ allowUnconfirmed?: boolean;634+}635+declare class WebSocketRequestResponsePair {636+ constructor(request: string, response: string);637+ get request(): string;638+ get response(): string;639+}640+interface DurableObjectFacets {641+ get<T extends Rpc.DurableObjectBranded | undefined = undefined>(name: string, getStartupOptions: () => FacetStartupOptions<T> | Promise<FacetStartupOptions<T>>): Fetcher<T>;642+ abort(name: string, reason: any): void;643+ delete(name: string): void;644+ clone(src: string, dst: string): void;645+}646+interface FacetStartupOptions<T extends Rpc.DurableObjectBranded | undefined = undefined> {647+ id?: DurableObjectId | string;648+ class: DurableObjectClass<T>;649+}650+interface AnalyticsEngineDataset {651+ writeDataPoint(event?: AnalyticsEngineDataPoint): void;652+}653+interface AnalyticsEngineDataPoint {654+ indexes?: ((ArrayBuffer | string) | null)[];655+ doubles?: number[];656+ blobs?: ((ArrayBuffer | string) | null)[];657+}658+/**659+ * The **`Event`** interface represents an event which takes place on an `EventTarget`.660+ *661+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Event)662+ */663+declare class Event {664+ constructor(type: string, init?: EventInit);665+ /**666+ * The **`type`** read-only property of the Event interface returns a string containing the event's type.667+ *668+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Event/type)669+ */670+ get type(): string;671+ /**672+ * The **`eventPhase`** read-only property of the being evaluated.673+ *674+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Event/eventPhase)675+ */676+ get eventPhase(): number;677+ /**678+ * The read-only **`composed`** property of the or not the event will propagate across the shadow DOM boundary into the standard DOM.679+ *680+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Event/composed)681+ */682+ get composed(): boolean;683+ /**684+ * The **`bubbles`** read-only property of the Event interface indicates whether the event bubbles up through the DOM tree or not.685+ *686+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Event/bubbles)687+ */688+ get bubbles(): boolean;689+ /**690+ * The **`cancelable`** read-only property of the Event interface indicates whether the event can be canceled, and therefore prevented as if the event never happened.691+ *692+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Event/cancelable)693+ */694+ get cancelable(): boolean;695+ /**696+ * The **`defaultPrevented`** read-only property of the Event interface returns a boolean value indicating whether or not the call to Event.preventDefault() canceled the event.697+ *698+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Event/defaultPrevented)699+ */700+ get defaultPrevented(): boolean;701+ /**702+ * The Event property **`returnValue`** indicates whether the default action for this event has been prevented or not.703+ * @deprecated704+ *705+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Event/returnValue)706+ */707+ get returnValue(): boolean;708+ /**709+ * The **`currentTarget`** read-only property of the Event interface identifies the element to which the event handler has been attached.710+ *711+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Event/currentTarget)712+ */713+ get currentTarget(): EventTarget | undefined;714+ /**715+ * The read-only **`target`** property of the dispatched.716+ *717+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Event/target)718+ */719+ get target(): EventTarget | undefined;720+ /**721+ * The deprecated **`Event.srcElement`** is an alias for the Event.target property.722+ * @deprecated723+ *724+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Event/srcElement)725+ */726+ get srcElement(): EventTarget | undefined;727+ /**728+ * The **`timeStamp`** read-only property of the Event interface returns the time (in milliseconds) at which the event was created.729+ *730+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Event/timeStamp)731+ */732+ get timeStamp(): number;733+ /**734+ * The **`isTrusted`** read-only property of the when the event was generated by the user agent (including via user actions and programmatic methods such as HTMLElement.focus()), and `false` when the event was dispatched via The only exception is the `click` event, which initializes the `isTrusted` property to `false` in user agents.735+ *736+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Event/isTrusted)737+ */738+ get isTrusted(): boolean;739+ /**740+ * The **`cancelBubble`** property of the Event interface is deprecated.741+ * @deprecated742+ *743+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Event/cancelBubble)744+ */745+ get cancelBubble(): boolean;746+ /**747+ * The **`cancelBubble`** property of the Event interface is deprecated.748+ * @deprecated749+ *750+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Event/cancelBubble)751+ */752+ set cancelBubble(value: boolean);753+ /**754+ * The **`stopImmediatePropagation()`** method of the If several listeners are attached to the same element for the same event type, they are called in the order in which they were added.755+ *756+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Event/stopImmediatePropagation)757+ */758+ stopImmediatePropagation(): void;759+ /**760+ * The **`preventDefault()`** method of the Event interface tells the user agent that if the event does not get explicitly handled, its default action should not be taken as it normally would be.761+ *762+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Event/preventDefault)763+ */764+ preventDefault(): void;765+ /**766+ * The **`stopPropagation()`** method of the Event interface prevents further propagation of the current event in the capturing and bubbling phases.767+ *768+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Event/stopPropagation)769+ */770+ stopPropagation(): void;771+ /**772+ * The **`composedPath()`** method of the Event interface returns the event's path which is an array of the objects on which listeners will be invoked.773+ *774+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Event/composedPath)775+ */776+ composedPath(): EventTarget[];777+ static readonly NONE: number;778+ static readonly CAPTURING_PHASE: number;779+ static readonly AT_TARGET: number;780+ static readonly BUBBLING_PHASE: number;781+}782+interface EventInit {783+ bubbles?: boolean;784+ cancelable?: boolean;785+ composed?: boolean;786+}787+type EventListener<EventType extends Event = Event> = (event: EventType) => void;788+interface EventListenerObject<EventType extends Event = Event> {789+ handleEvent(event: EventType): void;790+}791+type EventListenerOrEventListenerObject<EventType extends Event = Event> = EventListener<EventType> | EventListenerObject<EventType>;792+/**793+ * The **`EventTarget`** interface is implemented by objects that can receive events and may have listeners for them.794+ *795+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/EventTarget)796+ */797+declare class EventTarget<EventMap extends Record<string, Event> = Record<string, Event>> {798+ constructor();799+ /**800+ * The **`addEventListener()`** method of the EventTarget interface sets up a function that will be called whenever the specified event is delivered to the target.801+ *802+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/EventTarget/addEventListener)803+ */804+ addEventListener<Type extends keyof EventMap>(type: Type, handler: EventListenerOrEventListenerObject<EventMap[Type]>, options?: EventTargetAddEventListenerOptions | boolean): void;805+ /**806+ * The **`removeEventListener()`** method of the EventTarget interface removes an event listener previously registered with EventTarget.addEventListener() from the target.807+ *808+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/EventTarget/removeEventListener)809+ */810+ removeEventListener<Type extends keyof EventMap>(type: Type, handler: EventListenerOrEventListenerObject<EventMap[Type]>, options?: EventTargetEventListenerOptions | boolean): void;811+ /**812+ * The **`dispatchEvent()`** method of the EventTarget sends an Event to the object, (synchronously) invoking the affected event listeners in the appropriate order.813+ *814+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/EventTarget/dispatchEvent)815+ */816+ dispatchEvent(event: EventMap[keyof EventMap]): boolean;817+}818+interface EventTargetEventListenerOptions {819+ capture?: boolean;820+}821+interface EventTargetAddEventListenerOptions {822+ capture?: boolean;823+ passive?: boolean;824+ once?: boolean;825+ signal?: AbortSignal;826+}827+interface EventTargetHandlerObject {828+ handleEvent: (event: Event) => any | undefined;829+}830+/**831+ * The **`AbortController`** interface represents a controller object that allows you to abort one or more Web requests as and when desired.832+ *833+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/AbortController)834+ */835+declare class AbortController {836+ constructor();837+ /**838+ * The **`signal`** read-only property of the AbortController interface returns an AbortSignal object instance, which can be used to communicate with/abort an asynchronous operation as desired.839+ *840+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/AbortController/signal)841+ */842+ get signal(): AbortSignal;843+ /**844+ * The **`abort()`** method of the AbortController interface aborts an asynchronous operation before it has completed.845+ *846+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/AbortController/abort)847+ */848+ abort(reason?: any): void;849+}850+/**851+ * The **`AbortSignal`** interface represents a signal object that allows you to communicate with an asynchronous operation (such as a fetch request) and abort it if required via an AbortController object.852+ *853+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/AbortSignal)854+ */855+declare abstract class AbortSignal extends EventTarget {856+ /**857+ * The **`AbortSignal.abort()`** static method returns an AbortSignal that is already set as aborted (and which does not trigger an AbortSignal/abort_event event).858+ *859+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/AbortSignal/abort_static)860+ */861+ static abort(reason?: any): AbortSignal;862+ /**863+ * The **`AbortSignal.timeout()`** static method returns an AbortSignal that will automatically abort after a specified time.864+ *865+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/AbortSignal/timeout_static)866+ */867+ static timeout(delay: number): AbortSignal;868+ /**869+ * The **`AbortSignal.any()`** static method takes an iterable of abort signals and returns an AbortSignal.870+ *871+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/AbortSignal/any_static)872+ */873+ static any(signals: AbortSignal[]): AbortSignal;874+ /**875+ * The **`aborted`** read-only property returns a value that indicates whether the asynchronous operations the signal is communicating with are aborted (`true`) or not (`false`).876+ *877+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/AbortSignal/aborted)878+ */879+ get aborted(): boolean;880+ /**881+ * The **`reason`** read-only property returns a JavaScript value that indicates the abort reason.882+ *883+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/AbortSignal/reason)884+ */885+ get reason(): any;886+ /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/AbortSignal/abort_event) */887+ get onabort(): any | null;888+ /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/AbortSignal/abort_event) */889+ set onabort(value: any | null);890+ /**891+ * The **`throwIfAborted()`** method throws the signal's abort AbortSignal.reason if the signal has been aborted; otherwise it does nothing.892+ *893+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/AbortSignal/throwIfAborted)894+ */895+ throwIfAborted(): void;896+}897+interface Scheduler {898+ wait(delay: number, maybeOptions?: SchedulerWaitOptions): Promise<void>;899+}900+interface SchedulerWaitOptions {901+ signal?: AbortSignal;902+}903+/**904+ * The **`ExtendableEvent`** interface extends the lifetime of the `install` and `activate` events dispatched on the global scope as part of the service worker lifecycle.905+ *906+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/ExtendableEvent)907+ */908+declare abstract class ExtendableEvent extends Event {909+ /**910+ * The **`ExtendableEvent.waitUntil()`** method tells the event dispatcher that work is ongoing.911+ *912+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/ExtendableEvent/waitUntil)913+ */914+ waitUntil(promise: Promise<any>): void;915+}916+/**917+ * The **`CustomEvent`** interface represents events initialized by an application for any purpose.918+ *919+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/CustomEvent)920+ */921+declare class CustomEvent<T = any> extends Event {922+ constructor(type: string, init?: CustomEventCustomEventInit);923+ /**924+ * The read-only **`detail`** property of the CustomEvent interface returns any data passed when initializing the event.925+ *926+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/CustomEvent/detail)927+ */928+ get detail(): T;929+}930+interface CustomEventCustomEventInit {931+ bubbles?: boolean;932+ cancelable?: boolean;933+ composed?: boolean;934+ detail?: any;935+}936+/**937+ * The **`Blob`** interface represents a blob, which is a file-like object of immutable, raw data; they can be read as text or binary data, or converted into a ReadableStream so its methods can be used for processing the data.938+ *939+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Blob)940+ */941+declare class Blob {942+ constructor(bits?: ((ArrayBuffer | ArrayBufferView) | string | Blob)[], options?: BlobOptions);943+ /**944+ * The **`size`** read-only property of the Blob interface returns the size of the Blob or File in bytes.945+ *946+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Blob/size)947+ */948+ get size(): number;949+ /**950+ * The **`type`** read-only property of the Blob interface returns the MIME type of the file.951+ *952+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Blob/type)953+ */954+ get type(): string;955+ /**956+ * The **`slice()`** method of the Blob interface creates and returns a new `Blob` object which contains data from a subset of the blob on which it's called.957+ *958+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Blob/slice)959+ */960+ slice(start?: number, end?: number, type?: string): Blob;961+ /**962+ * The **`arrayBuffer()`** method of the Blob interface returns a Promise that resolves with the contents of the blob as binary data contained in an ArrayBuffer.963+ *964+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Blob/arrayBuffer)965+ */966+ arrayBuffer(): Promise<ArrayBuffer>;967+ /**968+ * The **`bytes()`** method of the Blob interface returns a Promise that resolves with a Uint8Array containing the contents of the blob as an array of bytes.969+ *970+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Blob/bytes)971+ */972+ bytes(): Promise<Uint8Array>;973+ /**974+ * The **`text()`** method of the string containing the contents of the blob, interpreted as UTF-8.975+ *976+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Blob/text)977+ */978+ text(): Promise<string>;979+ /**980+ * The **`stream()`** method of the Blob interface returns a ReadableStream which upon reading returns the data contained within the `Blob`.981+ *982+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Blob/stream)983+ */984+ stream(): ReadableStream;985+}986+interface BlobOptions {987+ type?: string;988+}989+/**990+ * The **`File`** interface provides information about files and allows JavaScript in a web page to access their content.991+ *992+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/File)993+ */994+declare class File extends Blob {995+ constructor(bits: ((ArrayBuffer | ArrayBufferView) | string | Blob)[] | undefined, name: string, options?: FileOptions);996+ /**997+ * The **`name`** read-only property of the File interface returns the name of the file represented by a File object.998+ *999+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/File/name)1000+ */1001+ get name(): string;1002+ /**1003+ * The **`lastModified`** read-only property of the File interface provides the last modified date of the file as the number of milliseconds since the Unix epoch (January 1, 1970 at midnight).1004+ *1005+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/File/lastModified)1006+ */1007+ get lastModified(): number;1008+}1009+interface FileOptions {1010+ type?: string;1011+ lastModified?: number;1012+}1013+/**1014+* The Cache API allows fine grained control of reading and writing from the Cloudflare global network cache.1015+*1016+* [Cloudflare Docs Reference](https://developers.cloudflare.com/workers/runtime-apis/cache/)1017+*/1018+declare abstract class CacheStorage {1019+ /**1020+ * The **`open()`** method of the the Cache object matching the `cacheName`.1021+ *1022+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/CacheStorage/open)1023+ */1024+ open(cacheName: string): Promise<Cache>;1025+ readonly default: Cache;1026+}1027+/**1028+* The Cache API allows fine grained control of reading and writing from the Cloudflare global network cache.1029+*1030+* [Cloudflare Docs Reference](https://developers.cloudflare.com/workers/runtime-apis/cache/)1031+*/1032+declare abstract class Cache {1033+ /* [Cloudflare Docs Reference](https://developers.cloudflare.com/workers/runtime-apis/cache/#delete) */1034+ delete(request: RequestInfo | URL, options?: CacheQueryOptions): Promise<boolean>;1035+ /* [Cloudflare Docs Reference](https://developers.cloudflare.com/workers/runtime-apis/cache/#match) */1036+ match(request: RequestInfo | URL, options?: CacheQueryOptions): Promise<Response | undefined>;1037+ /* [Cloudflare Docs Reference](https://developers.cloudflare.com/workers/runtime-apis/cache/#put) */1038+ put(request: RequestInfo | URL, response: Response): Promise<void>;1039+}1040+interface CacheQueryOptions {1041+ ignoreMethod?: boolean;1042+}1043+/**1044+* The Web Crypto API provides a set of low-level functions for common cryptographic tasks.1045+* The Workers runtime implements the full surface of this API, but with some differences in1046+* the [supported algorithms](https://developers.cloudflare.com/workers/runtime-apis/web-crypto/#supported-algorithms)1047+* compared to those implemented in most browsers.1048+*1049+* [Cloudflare Docs Reference](https://developers.cloudflare.com/workers/runtime-apis/web-crypto/)1050+*/1051+declare abstract class Crypto {1052+ /**1053+ * The **`Crypto.subtle`** read-only property returns a cryptographic operations.1054+ * Available only in secure contexts.1055+ *1056+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Crypto/subtle)1057+ */1058+ get subtle(): SubtleCrypto;1059+ /**1060+ * The **`Crypto.getRandomValues()`** method lets you get cryptographically strong random values.1061+ *1062+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Crypto/getRandomValues)1063+ */1064+ getRandomValues<T extends Int8Array | Uint8Array | Int16Array | Uint16Array | Int32Array | Uint32Array | BigInt64Array | BigUint64Array>(buffer: T): T;1065+ /**1066+ * The **`randomUUID()`** method of the Crypto interface is used to generate a v4 UUID using a cryptographically secure random number generator.1067+ * Available only in secure contexts.1068+ *1069+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Crypto/randomUUID)1070+ */1071+ randomUUID(): string;1072+ DigestStream: typeof DigestStream;1073+}1074+/**1075+ * The **`SubtleCrypto`** interface of the Web Crypto API provides a number of low-level cryptographic functions.1076+ * Available only in secure contexts.1077+ *1078+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/SubtleCrypto)1079+ */1080+declare abstract class SubtleCrypto {1081+ /**1082+ * The **`encrypt()`** method of the SubtleCrypto interface encrypts data.1083+ *1084+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/SubtleCrypto/encrypt)1085+ */1086+ encrypt(algorithm: string | SubtleCryptoEncryptAlgorithm, key: CryptoKey, plainText: ArrayBuffer | ArrayBufferView): Promise<ArrayBuffer>;1087+ /**1088+ * The **`decrypt()`** method of the SubtleCrypto interface decrypts some encrypted data.1089+ *1090+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/SubtleCrypto/decrypt)1091+ */1092+ decrypt(algorithm: string | SubtleCryptoEncryptAlgorithm, key: CryptoKey, cipherText: ArrayBuffer | ArrayBufferView): Promise<ArrayBuffer>;1093+ /**1094+ * The **`sign()`** method of the SubtleCrypto interface generates a digital signature.1095+ *1096+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/SubtleCrypto/sign)1097+ */1098+ sign(algorithm: string | SubtleCryptoSignAlgorithm, key: CryptoKey, data: ArrayBuffer | ArrayBufferView): Promise<ArrayBuffer>;1099+ /**1100+ * The **`verify()`** method of the SubtleCrypto interface verifies a digital signature.1101+ *1102+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/SubtleCrypto/verify)1103+ */1104+ verify(algorithm: string | SubtleCryptoSignAlgorithm, key: CryptoKey, signature: ArrayBuffer | ArrayBufferView, data: ArrayBuffer | ArrayBufferView): Promise<boolean>;1105+ /**1106+ * The **`digest()`** method of the SubtleCrypto interface generates a _digest_ of the given data, using the specified hash function.1107+ *1108+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/SubtleCrypto/digest)1109+ */1110+ digest(algorithm: string | SubtleCryptoHashAlgorithm, data: ArrayBuffer | ArrayBufferView): Promise<ArrayBuffer>;1111+ /**1112+ * The **`generateKey()`** method of the SubtleCrypto interface is used to generate a new key (for symmetric algorithms) or key pair (for public-key algorithms).1113+ *1114+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/SubtleCrypto/generateKey)1115+ */1116+ generateKey(algorithm: string | SubtleCryptoGenerateKeyAlgorithm, extractable: boolean, keyUsages: string[]): Promise<CryptoKey | CryptoKeyPair>;1117+ /**1118+ * The **`deriveKey()`** method of the SubtleCrypto interface can be used to derive a secret key from a master key.1119+ *1120+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/SubtleCrypto/deriveKey)1121+ */1122+ deriveKey(algorithm: string | SubtleCryptoDeriveKeyAlgorithm, baseKey: CryptoKey, derivedKeyAlgorithm: string | SubtleCryptoImportKeyAlgorithm, extractable: boolean, keyUsages: string[]): Promise<CryptoKey>;1123+ /**1124+ * The **`deriveBits()`** method of the key.1125+ *1126+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/SubtleCrypto/deriveBits)1127+ */1128+ deriveBits(algorithm: string | SubtleCryptoDeriveKeyAlgorithm, baseKey: CryptoKey, length?: number | null): Promise<ArrayBuffer>;1129+ /**1130+ * The **`importKey()`** method of the SubtleCrypto interface imports a key: that is, it takes as input a key in an external, portable format and gives you a CryptoKey object that you can use in the Web Crypto API.1131+ *1132+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/SubtleCrypto/importKey)1133+ */1134+ importKey(format: string, keyData: (ArrayBuffer | ArrayBufferView) | JsonWebKey, algorithm: string | SubtleCryptoImportKeyAlgorithm, extractable: boolean, keyUsages: string[]): Promise<CryptoKey>;1135+ /**1136+ * The **`exportKey()`** method of the SubtleCrypto interface exports a key: that is, it takes as input a CryptoKey object and gives you the key in an external, portable format.1137+ *1138+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/SubtleCrypto/exportKey)1139+ */1140+ exportKey(format: string, key: CryptoKey): Promise<ArrayBuffer | JsonWebKey>;1141+ /**1142+ * The **`wrapKey()`** method of the SubtleCrypto interface 'wraps' a key.1143+ *1144+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/SubtleCrypto/wrapKey)1145+ */1146+ wrapKey(format: string, key: CryptoKey, wrappingKey: CryptoKey, wrapAlgorithm: string | SubtleCryptoEncryptAlgorithm): Promise<ArrayBuffer>;1147+ /**1148+ * The **`unwrapKey()`** method of the SubtleCrypto interface 'unwraps' a key.1149+ *1150+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/SubtleCrypto/unwrapKey)1151+ */1152+ unwrapKey(format: string, wrappedKey: ArrayBuffer | ArrayBufferView, unwrappingKey: CryptoKey, unwrapAlgorithm: string | SubtleCryptoEncryptAlgorithm, unwrappedKeyAlgorithm: string | SubtleCryptoImportKeyAlgorithm, extractable: boolean, keyUsages: string[]): Promise<CryptoKey>;1153+ timingSafeEqual(a: ArrayBuffer | ArrayBufferView, b: ArrayBuffer | ArrayBufferView): boolean;1154+}1155+/**1156+ * The **`CryptoKey`** interface of the Web Crypto API represents a cryptographic key obtained from one of the SubtleCrypto methods SubtleCrypto.generateKey, SubtleCrypto.deriveKey, SubtleCrypto.importKey, or SubtleCrypto.unwrapKey.1157+ * Available only in secure contexts.1158+ *1159+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/CryptoKey)1160+ */1161+declare abstract class CryptoKey {1162+ /**1163+ * The read-only **`type`** property of the CryptoKey interface indicates which kind of key is represented by the object.1164+ *1165+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/CryptoKey/type)1166+ */1167+ readonly type: string;1168+ /**1169+ * The read-only **`extractable`** property of the CryptoKey interface indicates whether or not the key may be extracted using `SubtleCrypto.exportKey()` or `SubtleCrypto.wrapKey()`.1170+ *1171+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/CryptoKey/extractable)1172+ */1173+ readonly extractable: boolean;1174+ /**1175+ * The read-only **`algorithm`** property of the CryptoKey interface returns an object describing the algorithm for which this key can be used, and any associated extra parameters.1176+ *1177+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/CryptoKey/algorithm)1178+ */1179+ readonly algorithm: CryptoKeyKeyAlgorithm | CryptoKeyAesKeyAlgorithm | CryptoKeyHmacKeyAlgorithm | CryptoKeyRsaKeyAlgorithm | CryptoKeyEllipticKeyAlgorithm | CryptoKeyArbitraryKeyAlgorithm;1180+ /**1181+ * The read-only **`usages`** property of the CryptoKey interface indicates what can be done with the key.1182+ *1183+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/CryptoKey/usages)1184+ */1185+ readonly usages: string[];1186+}1187+interface CryptoKeyPair {1188+ publicKey: CryptoKey;1189+ privateKey: CryptoKey;1190+}1191+interface JsonWebKey {1192+ kty: string;1193+ use?: string;1194+ key_ops?: string[];1195+ alg?: string;1196+ ext?: boolean;1197+ crv?: string;1198+ x?: string;1199+ y?: string;1200+ d?: string;1201+ n?: string;1202+ e?: string;1203+ p?: string;1204+ q?: string;1205+ dp?: string;1206+ dq?: string;1207+ qi?: string;1208+ oth?: RsaOtherPrimesInfo[];1209+ k?: string;1210+}1211+interface RsaOtherPrimesInfo {1212+ r?: string;1213+ d?: string;1214+ t?: string;1215+}1216+interface SubtleCryptoDeriveKeyAlgorithm {1217+ name: string;1218+ salt?: (ArrayBuffer | ArrayBufferView);1219+ iterations?: number;1220+ hash?: (string | SubtleCryptoHashAlgorithm);1221+ $public?: CryptoKey;1222+ info?: (ArrayBuffer | ArrayBufferView);1223+}1224+interface SubtleCryptoEncryptAlgorithm {1225+ name: string;1226+ iv?: (ArrayBuffer | ArrayBufferView);1227+ additionalData?: (ArrayBuffer | ArrayBufferView);1228+ tagLength?: number;1229+ counter?: (ArrayBuffer | ArrayBufferView);1230+ length?: number;1231+ label?: (ArrayBuffer | ArrayBufferView);1232+}1233+interface SubtleCryptoGenerateKeyAlgorithm {1234+ name: string;1235+ hash?: (string | SubtleCryptoHashAlgorithm);1236+ modulusLength?: number;1237+ publicExponent?: (ArrayBuffer | ArrayBufferView);1238+ length?: number;1239+ namedCurve?: string;1240+}1241+interface SubtleCryptoHashAlgorithm {1242+ name: string;1243+}1244+interface SubtleCryptoImportKeyAlgorithm {1245+ name: string;1246+ hash?: (string | SubtleCryptoHashAlgorithm);1247+ length?: number;1248+ namedCurve?: string;1249+ compressed?: boolean;1250+}1251+interface SubtleCryptoSignAlgorithm {1252+ name: string;1253+ hash?: (string | SubtleCryptoHashAlgorithm);1254+ dataLength?: number;1255+ saltLength?: number;1256+}1257+interface CryptoKeyKeyAlgorithm {1258+ name: string;1259+}1260+interface CryptoKeyAesKeyAlgorithm {1261+ name: string;1262+ length: number;1263+}1264+interface CryptoKeyHmacKeyAlgorithm {1265+ name: string;1266+ hash: CryptoKeyKeyAlgorithm;1267+ length: number;1268+}1269+interface CryptoKeyRsaKeyAlgorithm {1270+ name: string;1271+ modulusLength: number;1272+ publicExponent: ArrayBuffer | ArrayBufferView;1273+ hash?: CryptoKeyKeyAlgorithm;1274+}1275+interface CryptoKeyEllipticKeyAlgorithm {1276+ name: string;1277+ namedCurve: string;1278+}1279+interface CryptoKeyArbitraryKeyAlgorithm {1280+ name: string;1281+ hash?: CryptoKeyKeyAlgorithm;1282+ namedCurve?: string;1283+ length?: number;1284+}1285+declare class DigestStream extends WritableStream<ArrayBuffer | ArrayBufferView> {1286+ constructor(algorithm: string | SubtleCryptoHashAlgorithm);1287+ readonly digest: Promise<ArrayBuffer>;1288+ get bytesWritten(): number | bigint;1289+}1290+/**1291+ * The **`TextDecoder`** interface represents a decoder for a specific text encoding, such as `UTF-8`, `ISO-8859-2`, `KOI8-R`, `GBK`, etc.1292+ *1293+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/TextDecoder)1294+ */1295+declare class TextDecoder {1296+ constructor(label?: string, options?: TextDecoderConstructorOptions);1297+ /**1298+ * The **`TextDecoder.decode()`** method returns a string containing text decoded from the buffer passed as a parameter.1299+ *1300+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/TextDecoder/decode)1301+ */1302+ decode(input?: (ArrayBuffer | ArrayBufferView), options?: TextDecoderDecodeOptions): string;1303+ get encoding(): string;1304+ get fatal(): boolean;1305+ get ignoreBOM(): boolean;1306+}1307+/**1308+ * The **`TextEncoder`** interface takes a stream of code points as input and emits a stream of UTF-8 bytes.1309+ *1310+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/TextEncoder)1311+ */1312+declare class TextEncoder {1313+ constructor();1314+ /**1315+ * The **`TextEncoder.encode()`** method takes a string as input, and returns a Global_Objects/Uint8Array containing the text given in parameters encoded with the specific method for that TextEncoder object.1316+ *1317+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/TextEncoder/encode)1318+ */1319+ encode(input?: string): Uint8Array;1320+ /**1321+ * The **`TextEncoder.encodeInto()`** method takes a string to encode and a destination Uint8Array to put resulting UTF-8 encoded text into, and returns a dictionary object indicating the progress of the encoding.1322+ *1323+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/TextEncoder/encodeInto)1324+ */1325+ encodeInto(input: string, buffer: Uint8Array): TextEncoderEncodeIntoResult;1326+ get encoding(): string;1327+}1328+interface TextDecoderConstructorOptions {1329+ fatal: boolean;1330+ ignoreBOM: boolean;1331+}1332+interface TextDecoderDecodeOptions {1333+ stream: boolean;1334+}1335+interface TextEncoderEncodeIntoResult {1336+ read: number;1337+ written: number;1338+}1339+/**1340+ * The **`ErrorEvent`** interface represents events providing information related to errors in scripts or in files.1341+ *1342+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/ErrorEvent)1343+ */1344+declare class ErrorEvent extends Event {1345+ constructor(type: string, init?: ErrorEventErrorEventInit);1346+ /**1347+ * The **`filename`** read-only property of the ErrorEvent interface returns a string containing the name of the script file in which the error occurred.1348+ *1349+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/ErrorEvent/filename)1350+ */1351+ get filename(): string;1352+ /**1353+ * The **`message`** read-only property of the ErrorEvent interface returns a string containing a human-readable error message describing the problem.1354+ *1355+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/ErrorEvent/message)1356+ */1357+ get message(): string;1358+ /**1359+ * The **`lineno`** read-only property of the ErrorEvent interface returns an integer containing the line number of the script file on which the error occurred.1360+ *1361+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/ErrorEvent/lineno)1362+ */1363+ get lineno(): number;1364+ /**1365+ * The **`colno`** read-only property of the ErrorEvent interface returns an integer containing the column number of the script file on which the error occurred.1366+ *1367+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/ErrorEvent/colno)1368+ */1369+ get colno(): number;1370+ /**1371+ * The **`error`** read-only property of the ErrorEvent interface returns a JavaScript value, such as an Error or DOMException, representing the error associated with this event.1372+ *1373+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/ErrorEvent/error)1374+ */1375+ get error(): any;1376+}1377+interface ErrorEventErrorEventInit {1378+ message?: string;1379+ filename?: string;1380+ lineno?: number;1381+ colno?: number;1382+ error?: any;1383+}1384+/**1385+ * The **`MessageEvent`** interface represents a message received by a target object.1386+ *1387+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/MessageEvent)1388+ */1389+declare class MessageEvent extends Event {1390+ constructor(type: string, initializer: MessageEventInit);1391+ /**1392+ * The **`data`** read-only property of the The data sent by the message emitter; this can be any data type, depending on what originated this event.1393+ *1394+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/MessageEvent/data)1395+ */1396+ readonly data: any;1397+ /**1398+ * The **`origin`** read-only property of the origin of the message emitter.1399+ *1400+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/MessageEvent/origin)1401+ */1402+ readonly origin: string | null;1403+ /**1404+ * The **`lastEventId`** read-only property of the unique ID for the event.1405+ *1406+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/MessageEvent/lastEventId)1407+ */1408+ readonly lastEventId: string;1409+ /**1410+ * The **`source`** read-only property of the a WindowProxy, MessagePort, or a `MessageEventSource` (which can be a WindowProxy, message emitter.1411+ *1412+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/MessageEvent/source)1413+ */1414+ readonly source: MessagePort | null;1415+ /**1416+ * The **`ports`** read-only property of the containing all MessagePort objects sent with the message, in order.1417+ *1418+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/MessageEvent/ports)1419+ */1420+ readonly ports: MessagePort[];1421+}1422+interface MessageEventInit {1423+ data: ArrayBuffer | string;1424+}1425+/**1426+ * The **`PromiseRejectionEvent`** interface represents events which are sent to the global script context when JavaScript Promises are rejected.1427+ *1428+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/PromiseRejectionEvent)1429+ */1430+declare abstract class PromiseRejectionEvent extends Event {1431+ /**1432+ * The PromiseRejectionEvent interface's **`promise`** read-only property indicates the JavaScript rejected.1433+ *1434+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/PromiseRejectionEvent/promise)1435+ */1436+ readonly promise: Promise<any>;1437+ /**1438+ * The PromiseRejectionEvent **`reason`** read-only property is any JavaScript value or Object which provides the reason passed into Promise.reject().1439+ *1440+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/PromiseRejectionEvent/reason)1441+ */1442+ readonly reason: any;1443+}1444+/**1445+ * The **`FormData`** interface provides a way to construct a set of key/value pairs representing form fields and their values, which can be sent using the Window/fetch, XMLHttpRequest.send() or navigator.sendBeacon() methods.1446+ *1447+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/FormData)1448+ */1449+declare class FormData {1450+ constructor();1451+ /**1452+ * The **`append()`** method of the FormData interface appends a new value onto an existing key inside a `FormData` object, or adds the key if it does not already exist.1453+ *1454+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/FormData/append)1455+ */1456+ append(name: string, value: string | Blob): void;1457+ /**1458+ * The **`append()`** method of the FormData interface appends a new value onto an existing key inside a `FormData` object, or adds the key if it does not already exist.1459+ *1460+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/FormData/append)1461+ */1462+ append(name: string, value: string): void;1463+ /**1464+ * The **`append()`** method of the FormData interface appends a new value onto an existing key inside a `FormData` object, or adds the key if it does not already exist.1465+ *1466+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/FormData/append)1467+ */1468+ append(name: string, value: Blob, filename?: string): void;1469+ /**1470+ * The **`delete()`** method of the FormData interface deletes a key and its value(s) from a `FormData` object.1471+ *1472+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/FormData/delete)1473+ */1474+ delete(name: string): void;1475+ /**1476+ * The **`get()`** method of the FormData interface returns the first value associated with a given key from within a `FormData` object.1477+ *1478+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/FormData/get)1479+ */1480+ get(name: string): (File | string) | null;1481+ /**1482+ * The **`getAll()`** method of the FormData interface returns all the values associated with a given key from within a `FormData` object.1483+ *1484+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/FormData/getAll)1485+ */1486+ getAll(name: string): (File | string)[];1487+ /**1488+ * The **`has()`** method of the FormData interface returns whether a `FormData` object contains a certain key.1489+ *1490+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/FormData/has)1491+ */1492+ has(name: string): boolean;1493+ /**1494+ * The **`set()`** method of the FormData interface sets a new value for an existing key inside a `FormData` object, or adds the key/value if it does not already exist.1495+ *1496+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/FormData/set)1497+ */1498+ set(name: string, value: string | Blob): void;1499+ /**1500+ * The **`set()`** method of the FormData interface sets a new value for an existing key inside a `FormData` object, or adds the key/value if it does not already exist.1501+ *1502+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/FormData/set)1503+ */1504+ set(name: string, value: string): void;1505+ /**1506+ * The **`set()`** method of the FormData interface sets a new value for an existing key inside a `FormData` object, or adds the key/value if it does not already exist.1507+ *1508+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/FormData/set)1509+ */1510+ set(name: string, value: Blob, filename?: string): void;1511+ /* Returns an array of key, value pairs for every entry in the list. */1512+ entries(): IterableIterator<[1513+ key: string,1514+ value: File | string1515+ ]>;1516+ /* Returns a list of keys in the list. */1517+ keys(): IterableIterator<string>;1518+ /* Returns a list of values in the list. */1519+ values(): IterableIterator<(File | string)>;1520+ forEach<This = unknown>(callback: (this: This, value: File | string, key: string, parent: FormData) => void, thisArg?: This): void;1521+ [Symbol.iterator](): IterableIterator<[1522+ key: string,1523+ value: File | string1524+ ]>;1525+}1526+interface ContentOptions {1527+ html?: boolean;1528+}1529+declare class HTMLRewriter {1530+ constructor();1531+ on(selector: string, handlers: HTMLRewriterElementContentHandlers): HTMLRewriter;1532+ onDocument(handlers: HTMLRewriterDocumentContentHandlers): HTMLRewriter;1533+ transform(response: Response): Response;1534+}1535+interface HTMLRewriterElementContentHandlers {1536+ element?(element: Element): void | Promise<void>;1537+ comments?(comment: Comment): void | Promise<void>;1538+ text?(element: Text): void | Promise<void>;1539+}1540+interface HTMLRewriterDocumentContentHandlers {1541+ doctype?(doctype: Doctype): void | Promise<void>;1542+ comments?(comment: Comment): void | Promise<void>;1543+ text?(text: Text): void | Promise<void>;1544+ end?(end: DocumentEnd): void | Promise<void>;1545+}1546+interface Doctype {1547+ readonly name: string | null;1548+ readonly publicId: string | null;1549+ readonly systemId: string | null;1550+}1551+interface Element {1552+ tagName: string;1553+ readonly attributes: IterableIterator<string[]>;1554+ readonly removed: boolean;1555+ readonly namespaceURI: string;1556+ getAttribute(name: string): string | null;1557+ hasAttribute(name: string): boolean;1558+ setAttribute(name: string, value: string): Element;1559+ removeAttribute(name: string): Element;1560+ before(content: string | ReadableStream | Response, options?: ContentOptions): Element;1561+ after(content: string | ReadableStream | Response, options?: ContentOptions): Element;1562+ prepend(content: string | ReadableStream | Response, options?: ContentOptions): Element;1563+ append(content: string | ReadableStream | Response, options?: ContentOptions): Element;1564+ replace(content: string | ReadableStream | Response, options?: ContentOptions): Element;1565+ remove(): Element;1566+ removeAndKeepContent(): Element;1567+ setInnerContent(content: string | ReadableStream | Response, options?: ContentOptions): Element;1568+ onEndTag(handler: (tag: EndTag) => void | Promise<void>): void;1569+}1570+interface EndTag {1571+ name: string;1572+ before(content: string | ReadableStream | Response, options?: ContentOptions): EndTag;1573+ after(content: string | ReadableStream | Response, options?: ContentOptions): EndTag;1574+ remove(): EndTag;1575+}1576+interface Comment {1577+ text: string;1578+ readonly removed: boolean;1579+ before(content: string, options?: ContentOptions): Comment;1580+ after(content: string, options?: ContentOptions): Comment;1581+ replace(content: string, options?: ContentOptions): Comment;1582+ remove(): Comment;1583+}1584+interface Text {1585+ readonly text: string;1586+ readonly lastInTextNode: boolean;1587+ readonly removed: boolean;1588+ before(content: string | ReadableStream | Response, options?: ContentOptions): Text;1589+ after(content: string | ReadableStream | Response, options?: ContentOptions): Text;1590+ replace(content: string | ReadableStream | Response, options?: ContentOptions): Text;1591+ remove(): Text;1592+}1593+interface DocumentEnd {1594+ append(content: string, options?: ContentOptions): DocumentEnd;1595+}1596+/**1597+ * This is the event type for `fetch` events dispatched on the ServiceWorkerGlobalScope.1598+ *1599+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/FetchEvent)1600+ */1601+declare abstract class FetchEvent extends ExtendableEvent {1602+ /**1603+ * The **`request`** read-only property of the the event handler.1604+ *1605+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/FetchEvent/request)1606+ */1607+ readonly request: Request;1608+ /**1609+ * The **`respondWith()`** method of allows you to provide a promise for a Response yourself.1610+ *1611+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/FetchEvent/respondWith)1612+ */1613+ respondWith(promise: Response | Promise<Response>): void;1614+ passThroughOnException(): void;1615+}1616+type HeadersInit = Headers | Iterable<Iterable<string>> | Record<string, string>;1617+/**1618+ * The **`Headers`** interface of the Fetch API allows you to perform various actions on HTTP request and response headers.1619+ *1620+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Headers)1621+ */1622+declare class Headers {1623+ constructor(init?: HeadersInit);1624+ /**1625+ * The **`get()`** method of the Headers interface returns a byte string of all the values of a header within a `Headers` object with a given name.1626+ *1627+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Headers/get)1628+ */1629+ get(name: string): string | null;1630+ getAll(name: string): string[];1631+ /**1632+ * The **`getSetCookie()`** method of the Headers interface returns an array containing the values of all Set-Cookie headers associated with a response.1633+ *1634+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Headers/getSetCookie)1635+ */1636+ getSetCookie(): string[];1637+ /**1638+ * The **`has()`** method of the Headers interface returns a boolean stating whether a `Headers` object contains a certain header.1639+ *1640+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Headers/has)1641+ */1642+ has(name: string): boolean;1643+ /**1644+ * The **`set()`** method of the Headers interface sets a new value for an existing header inside a `Headers` object, or adds the header if it does not already exist.1645+ *1646+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Headers/set)1647+ */1648+ set(name: string, value: string): void;1649+ /**1650+ * The **`append()`** method of the Headers interface appends a new value onto an existing header inside a `Headers` object, or adds the header if it does not already exist.1651+ *1652+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Headers/append)1653+ */1654+ append(name: string, value: string): void;1655+ /**1656+ * The **`delete()`** method of the Headers interface deletes a header from the current `Headers` object.1657+ *1658+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Headers/delete)1659+ */1660+ delete(name: string): void;1661+ forEach<This = unknown>(callback: (this: This, value: string, key: string, parent: Headers) => void, thisArg?: This): void;1662+ /* Returns an iterator allowing to go through all key/value pairs contained in this object. */1663+ entries(): IterableIterator<[1664+ key: string,1665+ value: string1666+ ]>;1667+ /* Returns an iterator allowing to go through all keys of the key/value pairs contained in this object. */1668+ keys(): IterableIterator<string>;1669+ /* Returns an iterator allowing to go through all values of the key/value pairs contained in this object. */1670+ values(): IterableIterator<string>;1671+ [Symbol.iterator](): IterableIterator<[1672+ key: string,1673+ value: string1674+ ]>;1675+}1676+type BodyInit = ReadableStream<Uint8Array> | string | ArrayBuffer | ArrayBufferView | Blob | URLSearchParams | FormData | Iterable<ArrayBuffer | ArrayBufferView> | AsyncIterable<ArrayBuffer | ArrayBufferView>;1677+declare abstract class Body {1678+ /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Request/body) */1679+ get body(): ReadableStream | null;1680+ /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Request/bodyUsed) */1681+ get bodyUsed(): boolean;1682+ /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Request/arrayBuffer) */1683+ arrayBuffer(): Promise<ArrayBuffer>;1684+ /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Request/bytes) */1685+ bytes(): Promise<Uint8Array>;1686+ /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Request/text) */1687+ text(): Promise<string>;1688+ /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Request/json) */1689+ json<T>(): Promise<T>;1690+ /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Request/formData) */1691+ formData(): Promise<FormData>;1692+ /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Request/blob) */1693+ blob(): Promise<Blob>;1694+}1695+/**1696+ * The **`Response`** interface of the Fetch API represents the response to a request.1697+ *1698+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Response)1699+ */1700+declare var Response: {1701+ prototype: Response;1702+ new (body?: BodyInit | null, init?: ResponseInit): Response;1703+ error(): Response;1704+ redirect(url: string, status?: number): Response;1705+ json(any: any, maybeInit?: (ResponseInit | Response)): Response;1706+};1707+/**1708+ * The **`Response`** interface of the Fetch API represents the response to a request.1709+ *1710+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Response)1711+ */1712+interface Response extends Body {1713+ /**1714+ * The **`clone()`** method of the Response interface creates a clone of a response object, identical in every way, but stored in a different variable.1715+ *1716+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Response/clone)1717+ */1718+ clone(): Response;1719+ /**1720+ * The **`status`** read-only property of the Response interface contains the HTTP status codes of the response.1721+ *1722+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Response/status)1723+ */1724+ status: number;1725+ /**1726+ * The **`statusText`** read-only property of the Response interface contains the status message corresponding to the HTTP status code in Response.status.1727+ *1728+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Response/statusText)1729+ */1730+ statusText: string;1731+ /**1732+ * The **`headers`** read-only property of the with the response.1733+ *1734+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Response/headers)1735+ */1736+ headers: Headers;1737+ /**1738+ * The **`ok`** read-only property of the Response interface contains a Boolean stating whether the response was successful (status in the range 200-299) or not.1739+ *1740+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Response/ok)1741+ */1742+ ok: boolean;1743+ /**1744+ * The **`redirected`** read-only property of the Response interface indicates whether or not the response is the result of a request you made which was redirected.1745+ *1746+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Response/redirected)1747+ */1748+ redirected: boolean;1749+ /**1750+ * The **`url`** read-only property of the Response interface contains the URL of the response.1751+ *1752+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Response/url)1753+ */1754+ url: string;1755+ webSocket: WebSocket | null;1756+ cf: any | undefined;1757+ /**1758+ * The **`type`** read-only property of the Response interface contains the type of the response.1759+ *1760+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Response/type)1761+ */1762+ type: "default" | "error";1763+}1764+interface ResponseInit {1765+ status?: number;1766+ statusText?: string;1767+ headers?: HeadersInit;1768+ cf?: any;1769+ webSocket?: (WebSocket | null);1770+ encodeBody?: "automatic" | "manual";1771+}1772+type RequestInfo<CfHostMetadata = unknown, Cf = CfProperties<CfHostMetadata>> = Request<CfHostMetadata, Cf> | string;1773+/**1774+ * The **`Request`** interface of the Fetch API represents a resource request.1775+ *1776+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Request)1777+ */1778+declare var Request: {1779+ prototype: Request;1780+ new <CfHostMetadata = unknown, Cf = CfProperties<CfHostMetadata>>(input: RequestInfo<CfProperties> | URL, init?: RequestInit<Cf>): Request<CfHostMetadata, Cf>;1781+};1782+/**1783+ * The **`Request`** interface of the Fetch API represents a resource request.1784+ *1785+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Request)1786+ */1787+interface Request<CfHostMetadata = unknown, Cf = CfProperties<CfHostMetadata>> extends Body {1788+ /**1789+ * The **`clone()`** method of the Request interface creates a copy of the current `Request` object.1790+ *1791+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Request/clone)1792+ */1793+ clone(): Request<CfHostMetadata, Cf>;1794+ /**1795+ * The **`method`** read-only property of the `POST`, etc.) A String indicating the method of the request.1796+ *1797+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Request/method)1798+ */1799+ method: string;1800+ /**1801+ * The **`url`** read-only property of the Request interface contains the URL of the request.1802+ *1803+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Request/url)1804+ */1805+ url: string;1806+ /**1807+ * The **`headers`** read-only property of the with the request.1808+ *1809+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Request/headers)1810+ */1811+ headers: Headers;1812+ /**1813+ * The **`redirect`** read-only property of the Request interface contains the mode for how redirects are handled.1814+ *1815+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Request/redirect)1816+ */1817+ redirect: string;1818+ fetcher: Fetcher | null;1819+ /**1820+ * The read-only **`signal`** property of the Request interface returns the AbortSignal associated with the request.1821+ *1822+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Request/signal)1823+ */1824+ signal: AbortSignal;1825+ cf?: Cf;1826+ /**1827+ * The **`integrity`** read-only property of the Request interface contains the subresource integrity value of the request.1828+ *1829+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Request/integrity)1830+ */1831+ integrity: string;1832+ /**1833+ * The **`keepalive`** read-only property of the Request interface contains the request's `keepalive` setting (`true` or `false`), which indicates whether the browser will keep the associated request alive if the page that initiated it is unloaded before the request is complete.1834+ *1835+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Request/keepalive)1836+ */1837+ keepalive: boolean;1838+ /**1839+ * The **`cache`** read-only property of the Request interface contains the cache mode of the request.1840+ *1841+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Request/cache)1842+ */1843+ cache?: "no-store" | "no-cache";1844+}1845+interface RequestInit<Cf = CfProperties> {1846+ /* A string to set request's method. */1847+ method?: string;1848+ /* A Headers object, an object literal, or an array of two-item arrays to set request's headers. */1849+ headers?: HeadersInit;1850+ /* A BodyInit object or null to set request's body. */1851+ body?: BodyInit | null;1852+ /* A string indicating whether request follows redirects, results in an error upon encountering a redirect, or returns the redirect (in an opaque fashion). Sets request's redirect. */1853+ redirect?: string;1854+ fetcher?: (Fetcher | null);1855+ cf?: Cf;1856+ /* A string indicating how the request will interact with the browser's cache to set request's cache. */1857+ cache?: "no-store" | "no-cache";1858+ /* A cryptographic hash of the resource to be fetched by request. Sets request's integrity. */1859+ integrity?: string;1860+ /* An AbortSignal to set request's signal. */1861+ signal?: (AbortSignal | null);1862+ encodeResponseBody?: "automatic" | "manual";1863+}1864+type Service<T extends (new (...args: any[]) => Rpc.WorkerEntrypointBranded) | Rpc.WorkerEntrypointBranded | ExportedHandler<any, any, any> | undefined = undefined> = T extends new (...args: any[]) => Rpc.WorkerEntrypointBranded ? Fetcher<InstanceType<T>> : T extends Rpc.WorkerEntrypointBranded ? Fetcher<T> : T extends Exclude<Rpc.EntrypointBranded, Rpc.WorkerEntrypointBranded> ? never : Fetcher<undefined>;1865+type Fetcher<T extends Rpc.EntrypointBranded | undefined = undefined, Reserved extends string = never> = (T extends Rpc.EntrypointBranded ? Rpc.Provider<T, Reserved | "fetch" | "connect"> : unknown) & {1866+ fetch(input: RequestInfo | URL, init?: RequestInit): Promise<Response>;1867+ connect(address: SocketAddress | string, options?: SocketOptions): Socket;1868+};1869+interface KVNamespaceListKey<Metadata, Key extends string = string> {1870+ name: Key;1871+ expiration?: number;1872+ metadata?: Metadata;1873+}1874+type KVNamespaceListResult<Metadata, Key extends string = string> = {1875+ list_complete: false;1876+ keys: KVNamespaceListKey<Metadata, Key>[];1877+ cursor: string;1878+ cacheStatus: string | null;1879+} | {1880+ list_complete: true;1881+ keys: KVNamespaceListKey<Metadata, Key>[];1882+ cacheStatus: string | null;1883+};1884+interface KVNamespace<Key extends string = string> {1885+ get(key: Key, options?: Partial<KVNamespaceGetOptions<undefined>>): Promise<string | null>;1886+ get(key: Key, type: "text"): Promise<string | null>;1887+ get<ExpectedValue = unknown>(key: Key, type: "json"): Promise<ExpectedValue | null>;1888+ get(key: Key, type: "arrayBuffer"): Promise<ArrayBuffer | null>;1889+ get(key: Key, type: "stream"): Promise<ReadableStream | null>;1890+ get(key: Key, options?: KVNamespaceGetOptions<"text">): Promise<string | null>;1891+ get<ExpectedValue = unknown>(key: Key, options?: KVNamespaceGetOptions<"json">): Promise<ExpectedValue | null>;1892+ get(key: Key, options?: KVNamespaceGetOptions<"arrayBuffer">): Promise<ArrayBuffer | null>;1893+ get(key: Key, options?: KVNamespaceGetOptions<"stream">): Promise<ReadableStream | null>;1894+ get(key: Array<Key>, type: "text"): Promise<Map<string, string | null>>;1895+ get<ExpectedValue = unknown>(key: Array<Key>, type: "json"): Promise<Map<string, ExpectedValue | null>>;1896+ get(key: Array<Key>, options?: Partial<KVNamespaceGetOptions<undefined>>): Promise<Map<string, string | null>>;1897+ get(key: Array<Key>, options?: KVNamespaceGetOptions<"text">): Promise<Map<string, string | null>>;1898+ get<ExpectedValue = unknown>(key: Array<Key>, options?: KVNamespaceGetOptions<"json">): Promise<Map<string, ExpectedValue | null>>;1899+ list<Metadata = unknown>(options?: KVNamespaceListOptions): Promise<KVNamespaceListResult<Metadata, Key>>;1900+ put(key: Key, value: string | ArrayBuffer | ArrayBufferView | ReadableStream, options?: KVNamespacePutOptions): Promise<void>;1901+ getWithMetadata<Metadata = unknown>(key: Key, options?: Partial<KVNamespaceGetOptions<undefined>>): Promise<KVNamespaceGetWithMetadataResult<string, Metadata>>;1902+ getWithMetadata<Metadata = unknown>(key: Key, type: "text"): Promise<KVNamespaceGetWithMetadataResult<string, Metadata>>;1903+ getWithMetadata<ExpectedValue = unknown, Metadata = unknown>(key: Key, type: "json"): Promise<KVNamespaceGetWithMetadataResult<ExpectedValue, Metadata>>;1904+ getWithMetadata<Metadata = unknown>(key: Key, type: "arrayBuffer"): Promise<KVNamespaceGetWithMetadataResult<ArrayBuffer, Metadata>>;1905+ getWithMetadata<Metadata = unknown>(key: Key, type: "stream"): Promise<KVNamespaceGetWithMetadataResult<ReadableStream, Metadata>>;1906+ getWithMetadata<Metadata = unknown>(key: Key, options: KVNamespaceGetOptions<"text">): Promise<KVNamespaceGetWithMetadataResult<string, Metadata>>;1907+ getWithMetadata<ExpectedValue = unknown, Metadata = unknown>(key: Key, options: KVNamespaceGetOptions<"json">): Promise<KVNamespaceGetWithMetadataResult<ExpectedValue, Metadata>>;1908+ getWithMetadata<Metadata = unknown>(key: Key, options: KVNamespaceGetOptions<"arrayBuffer">): Promise<KVNamespaceGetWithMetadataResult<ArrayBuffer, Metadata>>;1909+ getWithMetadata<Metadata = unknown>(key: Key, options: KVNamespaceGetOptions<"stream">): Promise<KVNamespaceGetWithMetadataResult<ReadableStream, Metadata>>;1910+ getWithMetadata<Metadata = unknown>(key: Array<Key>, type: "text"): Promise<Map<string, KVNamespaceGetWithMetadataResult<string, Metadata>>>;1911+ getWithMetadata<ExpectedValue = unknown, Metadata = unknown>(key: Array<Key>, type: "json"): Promise<Map<string, KVNamespaceGetWithMetadataResult<ExpectedValue, Metadata>>>;1912+ getWithMetadata<Metadata = unknown>(key: Array<Key>, options?: Partial<KVNamespaceGetOptions<undefined>>): Promise<Map<string, KVNamespaceGetWithMetadataResult<string, Metadata>>>;1913+ getWithMetadata<Metadata = unknown>(key: Array<Key>, options?: KVNamespaceGetOptions<"text">): Promise<Map<string, KVNamespaceGetWithMetadataResult<string, Metadata>>>;1914+ getWithMetadata<ExpectedValue = unknown, Metadata = unknown>(key: Array<Key>, options?: KVNamespaceGetOptions<"json">): Promise<Map<string, KVNamespaceGetWithMetadataResult<ExpectedValue, Metadata>>>;1915+ delete(key: Key): Promise<void>;1916+}1917+interface KVNamespaceListOptions {1918+ limit?: number;1919+ prefix?: (string | null);1920+ cursor?: (string | null);1921+}1922+interface KVNamespaceGetOptions<Type> {1923+ type: Type;1924+ cacheTtl?: number;1925+}1926+interface KVNamespacePutOptions {1927+ expiration?: number;1928+ expirationTtl?: number;1929+ metadata?: (any | null);1930+}1931+interface KVNamespaceGetWithMetadataResult<Value, Metadata> {1932+ value: Value | null;1933+ metadata: Metadata | null;1934+ cacheStatus: string | null;1935+}1936+type QueueContentType = "text" | "bytes" | "json" | "v8";1937+interface Queue<Body = unknown> {1938+ metrics(): Promise<QueueMetrics>;1939+ send(message: Body, options?: QueueSendOptions): Promise<QueueSendResponse>;1940+ sendBatch(messages: Iterable<MessageSendRequest<Body>>, options?: QueueSendBatchOptions): Promise<QueueSendBatchResponse>;1941+}1942+interface QueueSendMetrics {1943+ backlogCount: number;1944+ backlogBytes: number;1945+ oldestMessageTimestamp?: Date;1946+}1947+interface QueueSendMetadata {1948+ metrics: QueueSendMetrics;1949+}1950+interface QueueSendResponse {1951+ metadata: QueueSendMetadata;1952+}1953+interface QueueSendBatchMetrics {1954+ backlogCount: number;1955+ backlogBytes: number;1956+ oldestMessageTimestamp?: Date;1957+}1958+interface QueueSendBatchMetadata {1959+ metrics: QueueSendBatchMetrics;1960+}1961+interface QueueSendBatchResponse {1962+ metadata: QueueSendBatchMetadata;1963+}1964+interface QueueSendOptions {1965+ contentType?: QueueContentType;1966+ delaySeconds?: number;1967+}1968+interface QueueSendBatchOptions {1969+ delaySeconds?: number;1970+}1971+interface MessageSendRequest<Body = unknown> {1972+ body: Body;1973+ contentType?: QueueContentType;1974+ delaySeconds?: number;1975+}1976+interface QueueMetrics {1977+ backlogCount: number;1978+ backlogBytes: number;1979+ oldestMessageTimestamp?: Date;1980+}1981+interface MessageBatchMetrics {1982+ backlogCount: number;1983+ backlogBytes: number;1984+ oldestMessageTimestamp?: Date;1985+}1986+interface MessageBatchMetadata {1987+ metrics: MessageBatchMetrics;1988+}1989+interface QueueRetryOptions {1990+ delaySeconds?: number;1991+}1992+interface Message<Body = unknown> {1993+ readonly id: string;1994+ readonly timestamp: Date;1995+ readonly body: Body;1996+ readonly attempts: number;1997+ retry(options?: QueueRetryOptions): void;1998+ ack(): void;1999+}2000+interface QueueEvent<Body = unknown> extends ExtendableEvent {2001+ readonly messages: readonly Message<Body>[];2002+ readonly queue: string;2003+ readonly metadata: MessageBatchMetadata;2004+ retryAll(options?: QueueRetryOptions): void;2005+ ackAll(): void;2006+}2007+interface MessageBatch<Body = unknown> {2008+ readonly messages: readonly Message<Body>[];2009+ readonly queue: string;2010+ readonly metadata: MessageBatchMetadata;2011+ retryAll(options?: QueueRetryOptions): void;2012+ ackAll(): void;2013+}2014+interface R2Error extends Error {2015+ readonly name: string;2016+ readonly code: number;2017+ readonly message: string;2018+ readonly action: string;2019+ readonly stack: any;2020+}2021+interface R2ListOptions {2022+ limit?: number;2023+ prefix?: string;2024+ cursor?: string;2025+ delimiter?: string;2026+ startAfter?: string;2027+ include?: ("httpMetadata" | "customMetadata")[];2028+}2029+interface R2Bucket {2030+ head(key: string): Promise<R2Object | null>;2031+ get(key: string, options: R2GetOptions & {2032+ onlyIf: R2Conditional | Headers;2033+ }): Promise<R2ObjectBody | R2Object | null>;2034+ get(key: string, options?: R2GetOptions): Promise<R2ObjectBody | null>;2035+ put(key: string, value: ReadableStream | ArrayBuffer | ArrayBufferView | string | null | Blob, options?: R2PutOptions & {2036+ onlyIf: R2Conditional | Headers;2037+ }): Promise<R2Object | null>;2038+ put(key: string, value: ReadableStream | ArrayBuffer | ArrayBufferView | string | null | Blob, options?: R2PutOptions): Promise<R2Object>;2039+ createMultipartUpload(key: string, options?: R2MultipartOptions): Promise<R2MultipartUpload>;2040+ resumeMultipartUpload(key: string, uploadId: string): R2MultipartUpload;2041+ delete(keys: string | string[]): Promise<void>;2042+ list(options?: R2ListOptions): Promise<R2Objects>;2043+}2044+interface R2MultipartUpload {2045+ readonly key: string;2046+ readonly uploadId: string;2047+ uploadPart(partNumber: number, value: ReadableStream | (ArrayBuffer | ArrayBufferView) | string | Blob, options?: R2UploadPartOptions): Promise<R2UploadedPart>;2048+ abort(): Promise<void>;2049+ complete(uploadedParts: R2UploadedPart[]): Promise<R2Object>;2050+}2051+interface R2UploadedPart {2052+ partNumber: number;2053+ etag: string;2054+}2055+declare abstract class R2Object {2056+ readonly key: string;2057+ readonly version: string;2058+ readonly size: number;2059+ readonly etag: string;2060+ readonly httpEtag: string;2061+ readonly checksums: R2Checksums;2062+ readonly uploaded: Date;2063+ readonly httpMetadata?: R2HTTPMetadata;2064+ readonly customMetadata?: Record<string, string>;2065+ readonly range?: R2Range;2066+ readonly storageClass: string;2067+ readonly ssecKeyMd5?: string;2068+ writeHttpMetadata(headers: Headers): void;2069+}2070+interface R2ObjectBody extends R2Object {2071+ get body(): ReadableStream;2072+ get bodyUsed(): boolean;2073+ arrayBuffer(): Promise<ArrayBuffer>;2074+ bytes(): Promise<Uint8Array>;2075+ text(): Promise<string>;2076+ json<T>(): Promise<T>;2077+ blob(): Promise<Blob>;2078+}2079+type R2Range = {2080+ offset: number;2081+ length?: number;2082+} | {2083+ offset?: number;2084+ length: number;2085+} | {2086+ suffix: number;2087+};2088+interface R2Conditional {2089+ etagMatches?: string;2090+ etagDoesNotMatch?: string;2091+ uploadedBefore?: Date;2092+ uploadedAfter?: Date;2093+ secondsGranularity?: boolean;2094+}2095+interface R2GetOptions {2096+ onlyIf?: (R2Conditional | Headers);2097+ range?: (R2Range | Headers);2098+ ssecKey?: (ArrayBuffer | string);2099+}2100+interface R2PutOptions {2101+ onlyIf?: (R2Conditional | Headers);2102+ httpMetadata?: (R2HTTPMetadata | Headers);2103+ customMetadata?: Record<string, string>;2104+ md5?: ((ArrayBuffer | ArrayBufferView) | string);2105+ sha1?: ((ArrayBuffer | ArrayBufferView) | string);2106+ sha256?: ((ArrayBuffer | ArrayBufferView) | string);2107+ sha384?: ((ArrayBuffer | ArrayBufferView) | string);2108+ sha512?: ((ArrayBuffer | ArrayBufferView) | string);2109+ storageClass?: string;2110+ ssecKey?: (ArrayBuffer | string);2111+}2112+interface R2MultipartOptions {2113+ httpMetadata?: (R2HTTPMetadata | Headers);2114+ customMetadata?: Record<string, string>;2115+ storageClass?: string;2116+ ssecKey?: (ArrayBuffer | string);2117+}2118+interface R2Checksums {2119+ readonly md5?: ArrayBuffer;2120+ readonly sha1?: ArrayBuffer;2121+ readonly sha256?: ArrayBuffer;2122+ readonly sha384?: ArrayBuffer;2123+ readonly sha512?: ArrayBuffer;2124+ toJSON(): R2StringChecksums;2125+}2126+interface R2StringChecksums {2127+ md5?: string;2128+ sha1?: string;2129+ sha256?: string;2130+ sha384?: string;2131+ sha512?: string;2132+}2133+interface R2HTTPMetadata {2134+ contentType?: string;2135+ contentLanguage?: string;2136+ contentDisposition?: string;2137+ contentEncoding?: string;2138+ cacheControl?: string;2139+ cacheExpiry?: Date;2140+}2141+type R2Objects = {2142+ objects: R2Object[];2143+ delimitedPrefixes: string[];2144+} & ({2145+ truncated: true;2146+ cursor: string;2147+} | {2148+ truncated: false;2149+});2150+interface R2UploadPartOptions {2151+ ssecKey?: (ArrayBuffer | string);2152+}2153+declare abstract class ScheduledEvent extends ExtendableEvent {2154+ readonly scheduledTime: number;2155+ readonly cron: string;2156+ noRetry(): void;2157+}2158+interface ScheduledController {2159+ readonly scheduledTime: number;2160+ readonly cron: string;2161+ noRetry(): void;2162+}2163+interface QueuingStrategy<T = any> {2164+ highWaterMark?: (number | bigint);2165+ size?: (chunk: T) => number | bigint;2166+}2167+interface UnderlyingSink<W = any> {2168+ type?: string;2169+ start?: (controller: WritableStreamDefaultController) => void | Promise<void>;2170+ write?: (chunk: W, controller: WritableStreamDefaultController) => void | Promise<void>;2171+ abort?: (reason: any) => void | Promise<void>;2172+ close?: () => void | Promise<void>;2173+}2174+interface UnderlyingByteSource {2175+ type: "bytes";2176+ autoAllocateChunkSize?: number;2177+ start?: (controller: ReadableByteStreamController) => void | Promise<void>;2178+ pull?: (controller: ReadableByteStreamController) => void | Promise<void>;2179+ cancel?: (reason: any) => void | Promise<void>;2180+}2181+interface UnderlyingSource<R = any> {2182+ type?: "" | undefined;2183+ start?: (controller: ReadableStreamDefaultController<R>) => void | Promise<void>;2184+ pull?: (controller: ReadableStreamDefaultController<R>) => void | Promise<void>;2185+ cancel?: (reason: any) => void | Promise<void>;2186+ expectedLength?: (number | bigint);2187+}2188+interface Transformer<I = any, O = any> {2189+ readableType?: string;2190+ writableType?: string;2191+ start?: (controller: TransformStreamDefaultController<O>) => void | Promise<void>;2192+ transform?: (chunk: I, controller: TransformStreamDefaultController<O>) => void | Promise<void>;2193+ flush?: (controller: TransformStreamDefaultController<O>) => void | Promise<void>;2194+ cancel?: (reason: any) => void | Promise<void>;2195+ expectedLength?: number;2196+}2197+interface StreamPipeOptions {2198+ preventAbort?: boolean;2199+ preventCancel?: boolean;2200+ /**2201+ * Pipes this readable stream to a given writable stream destination. The way in which the piping process behaves under various error conditions can be customized with a number of passed options. It returns a promise that fulfills when the piping process completes successfully, or rejects if any errors were encountered.2202+ *2203+ * Piping a stream will lock it for the duration of the pipe, preventing any other consumer from acquiring a reader.2204+ *2205+ * Errors and closures of the source and destination streams propagate as follows:2206+ *2207+ * An error in this source readable stream will abort destination, unless preventAbort is truthy. The returned promise will be rejected with the source's error, or with any error that occurs during aborting the destination.2208+ *2209+ * An error in destination will cancel this source readable stream, unless preventCancel is truthy. The returned promise will be rejected with the destination's error, or with any error that occurs during canceling the source.2210+ *2211+ * When this source readable stream closes, destination will be closed, unless preventClose is truthy. The returned promise will be fulfilled once this process completes, unless an error is encountered while closing the destination, in which case it will be rejected with that error.2212+ *2213+ * If destination starts out closed or closing, this source readable stream will be canceled, unless preventCancel is true. The returned promise will be rejected with an error indicating piping to a closed stream failed, or with any error that occurs during canceling the source.2214+ *2215+ * The signal option can be set to an AbortSignal to allow aborting an ongoing pipe operation via the corresponding AbortController. In this case, this source readable stream will be canceled, and destination aborted, unless the respective options preventCancel or preventAbort are set.2216+ */2217+ preventClose?: boolean;2218+ signal?: AbortSignal;2219+}2220+type ReadableStreamReadResult<R = any> = {2221+ done: false;2222+ value: R;2223+} | {2224+ done: true;2225+ value?: undefined;2226+};2227+/**2228+ * The `ReadableStream` interface of the Streams API represents a readable stream of byte data.2229+ *2230+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/ReadableStream)2231+ */2232+interface ReadableStream<R = any> {2233+ /**2234+ * The **`locked`** read-only property of the ReadableStream interface returns whether or not the readable stream is locked to a reader.2235+ *2236+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/ReadableStream/locked)2237+ */2238+ get locked(): boolean;2239+ /**2240+ * The **`cancel()`** method of the ReadableStream interface returns a Promise that resolves when the stream is canceled.2241+ *2242+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/ReadableStream/cancel)2243+ */2244+ cancel(reason?: any): Promise<void>;2245+ /**2246+ * The **`getReader()`** method of the ReadableStream interface creates a reader and locks the stream to it.2247+ *2248+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/ReadableStream/getReader)2249+ */2250+ getReader(): ReadableStreamDefaultReader<R>;2251+ /**2252+ * The **`getReader()`** method of the ReadableStream interface creates a reader and locks the stream to it.2253+ *2254+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/ReadableStream/getReader)2255+ */2256+ getReader(options: ReadableStreamGetReaderOptions): ReadableStreamBYOBReader;2257+ /**2258+ * The **`pipeThrough()`** method of the ReadableStream interface provides a chainable way of piping the current stream through a transform stream or any other writable/readable pair.2259+ *2260+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/ReadableStream/pipeThrough)2261+ */2262+ pipeThrough<T>(transform: ReadableWritablePair<T, R>, options?: StreamPipeOptions): ReadableStream<T>;2263+ /**2264+ * The **`pipeTo()`** method of the ReadableStream interface pipes the current `ReadableStream` to a given WritableStream and returns a Promise that fulfills when the piping process completes successfully, or rejects if any errors were encountered.2265+ *2266+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/ReadableStream/pipeTo)2267+ */2268+ pipeTo(destination: WritableStream<R>, options?: StreamPipeOptions): Promise<void>;2269+ /**2270+ * The **`tee()`** method of the two-element array containing the two resulting branches as new ReadableStream instances.2271+ *2272+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/ReadableStream/tee)2273+ */2274+ tee(): [2275+ ReadableStream<R>,2276+ ReadableStream<R>2277+ ];2278+ values(options?: ReadableStreamValuesOptions): AsyncIterableIterator<R>;2279+ [Symbol.asyncIterator](options?: ReadableStreamValuesOptions): AsyncIterableIterator<R>;2280+}2281+/**2282+ * The `ReadableStream` interface of the Streams API represents a readable stream of byte data.2283+ *2284+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/ReadableStream)2285+ */2286+declare const ReadableStream: {2287+ prototype: ReadableStream;2288+ new (underlyingSource: UnderlyingByteSource, strategy?: QueuingStrategy<Uint8Array>): ReadableStream<Uint8Array>;2289+ new <R = any>(underlyingSource?: UnderlyingSource<R>, strategy?: QueuingStrategy<R>): ReadableStream<R>;2290+};2291+/**2292+ * The **`ReadableStreamDefaultReader`** interface of the Streams API represents a default reader that can be used to read stream data supplied from a network (such as a fetch request).2293+ *2294+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/ReadableStreamDefaultReader)2295+ */2296+declare class ReadableStreamDefaultReader<R = any> {2297+ constructor(stream: ReadableStream);2298+ get closed(): Promise<void>;2299+ cancel(reason?: any): Promise<void>;2300+ /**2301+ * The **`read()`** method of the ReadableStreamDefaultReader interface returns a Promise providing access to the next chunk in the stream's internal queue.2302+ *2303+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/ReadableStreamDefaultReader/read)2304+ */2305+ read(): Promise<ReadableStreamReadResult<R>>;2306+ /**2307+ * The **`releaseLock()`** method of the ReadableStreamDefaultReader interface releases the reader's lock on the stream.2308+ *2309+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/ReadableStreamDefaultReader/releaseLock)2310+ */2311+ releaseLock(): void;2312+}2313+/**2314+ * The `ReadableStreamBYOBReader` interface of the Streams API defines a reader for a ReadableStream that supports zero-copy reading from an underlying byte source.2315+ *2316+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/ReadableStreamBYOBReader)2317+ */2318+declare class ReadableStreamBYOBReader {2319+ constructor(stream: ReadableStream);2320+ get closed(): Promise<void>;2321+ cancel(reason?: any): Promise<void>;2322+ /**2323+ * The **`read()`** method of the ReadableStreamBYOBReader interface is used to read data into a view on a user-supplied buffer from an associated readable byte stream.2324+ *2325+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/ReadableStreamBYOBReader/read)2326+ */2327+ read<T extends ArrayBufferView>(view: T): Promise<ReadableStreamReadResult<T>>;2328+ /**2329+ * The **`releaseLock()`** method of the ReadableStreamBYOBReader interface releases the reader's lock on the stream.2330+ *2331+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/ReadableStreamBYOBReader/releaseLock)2332+ */2333+ releaseLock(): void;2334+ readAtLeast<T extends ArrayBufferView>(minElements: number, view: T): Promise<ReadableStreamReadResult<T>>;2335+}2336+interface ReadableStreamBYOBReaderReadableStreamBYOBReaderReadOptions {2337+ min?: number;2338+}2339+interface ReadableStreamGetReaderOptions {2340+ /**2341+ * Creates a ReadableStreamBYOBReader and locks the stream to the new reader.2342+ *2343+ * This call behaves the same way as the no-argument variant, except that it only works on readable byte streams, i.e. streams which were constructed specifically with the ability to handle "bring your own buffer" reading. The returned BYOB reader provides the ability to directly read individual chunks from the stream via its read() method, into developer-supplied buffers, allowing more precise control over allocation.2344+ */2345+ mode: "byob";2346+}2347+/**2348+ * The **`ReadableStreamBYOBRequest`** interface of the Streams API represents a 'pull request' for data from an underlying source that will made as a zero-copy transfer to a consumer (bypassing the stream's internal queues).2349+ *2350+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/ReadableStreamBYOBRequest)2351+ */2352+declare abstract class ReadableStreamBYOBRequest {2353+ /**2354+ * The **`view`** getter property of the ReadableStreamBYOBRequest interface returns the current view.2355+ *2356+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/ReadableStreamBYOBRequest/view)2357+ */2358+ get view(): Uint8Array | null;2359+ /**2360+ * The **`respond()`** method of the ReadableStreamBYOBRequest interface is used to signal to the associated readable byte stream that the specified number of bytes were written into the ReadableStreamBYOBRequest.view.2361+ *2362+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/ReadableStreamBYOBRequest/respond)2363+ */2364+ respond(bytesWritten: number): void;2365+ /**2366+ * The **`respondWithNewView()`** method of the ReadableStreamBYOBRequest interface specifies a new view that the consumer of the associated readable byte stream should write to instead of ReadableStreamBYOBRequest.view.2367+ *2368+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/ReadableStreamBYOBRequest/respondWithNewView)2369+ */2370+ respondWithNewView(view: ArrayBuffer | ArrayBufferView): void;2371+ get atLeast(): number | null;2372+}2373+/**2374+ * The **`ReadableStreamDefaultController`** interface of the Streams API represents a controller allowing control of a ReadableStream's state and internal queue.2375+ *2376+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/ReadableStreamDefaultController)2377+ */2378+declare abstract class ReadableStreamDefaultController<R = any> {2379+ /**2380+ * The **`desiredSize`** read-only property of the required to fill the stream's internal queue.2381+ *2382+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/ReadableStreamDefaultController/desiredSize)2383+ */2384+ get desiredSize(): number | null;2385+ /**2386+ * The **`close()`** method of the ReadableStreamDefaultController interface closes the associated stream.2387+ *2388+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/ReadableStreamDefaultController/close)2389+ */2390+ close(): void;2391+ /**2392+ * The **`enqueue()`** method of the ```js-nolint enqueue(chunk) ``` - `chunk` - : The chunk to enqueue.2393+ *2394+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/ReadableStreamDefaultController/enqueue)2395+ */2396+ enqueue(chunk?: R): void;2397+ /**2398+ * The **`error()`** method of the with the associated stream to error.2399+ *2400+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/ReadableStreamDefaultController/error)2401+ */2402+ error(reason: any): void;2403+}2404+/**2405+ * The **`ReadableByteStreamController`** interface of the Streams API represents a controller for a readable byte stream.2406+ *2407+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/ReadableByteStreamController)2408+ */2409+declare abstract class ReadableByteStreamController {2410+ /**2411+ * The **`byobRequest`** read-only property of the ReadableByteStreamController interface returns the current BYOB request, or `null` if there are no pending requests.2412+ *2413+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/ReadableByteStreamController/byobRequest)2414+ */2415+ get byobRequest(): ReadableStreamBYOBRequest | null;2416+ /**2417+ * The **`desiredSize`** read-only property of the ReadableByteStreamController interface returns the number of bytes required to fill the stream's internal queue to its 'desired size'.2418+ *2419+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/ReadableByteStreamController/desiredSize)2420+ */2421+ get desiredSize(): number | null;2422+ /**2423+ * The **`close()`** method of the ReadableByteStreamController interface closes the associated stream.2424+ *2425+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/ReadableByteStreamController/close)2426+ */2427+ close(): void;2428+ /**2429+ * The **`enqueue()`** method of the ReadableByteStreamController interface enqueues a given chunk on the associated readable byte stream (the chunk is copied into the stream's internal queues).2430+ *2431+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/ReadableByteStreamController/enqueue)2432+ */2433+ enqueue(chunk: ArrayBuffer | ArrayBufferView): void;2434+ /**2435+ * The **`error()`** method of the ReadableByteStreamController interface causes any future interactions with the associated stream to error with the specified reason.2436+ *2437+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/ReadableByteStreamController/error)2438+ */2439+ error(reason: any): void;2440+}2441+/**2442+ * The **`WritableStreamDefaultController`** interface of the Streams API represents a controller allowing control of a WritableStream's state.2443+ *2444+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/WritableStreamDefaultController)2445+ */2446+declare abstract class WritableStreamDefaultController {2447+ /**2448+ * The read-only **`signal`** property of the WritableStreamDefaultController interface returns the AbortSignal associated with the controller.2449+ *2450+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/WritableStreamDefaultController/signal)2451+ */2452+ get signal(): AbortSignal;2453+ /**2454+ * The **`error()`** method of the with the associated stream to error.2455+ *2456+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/WritableStreamDefaultController/error)2457+ */2458+ error(reason?: any): void;2459+}2460+/**2461+ * The **`TransformStreamDefaultController`** interface of the Streams API provides methods to manipulate the associated ReadableStream and WritableStream.2462+ *2463+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/TransformStreamDefaultController)2464+ */2465+declare abstract class TransformStreamDefaultController<O = any> {2466+ /**2467+ * The **`desiredSize`** read-only property of the TransformStreamDefaultController interface returns the desired size to fill the queue of the associated ReadableStream.2468+ *2469+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/TransformStreamDefaultController/desiredSize)2470+ */2471+ get desiredSize(): number | null;2472+ /**2473+ * The **`enqueue()`** method of the TransformStreamDefaultController interface enqueues the given chunk in the readable side of the stream.2474+ *2475+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/TransformStreamDefaultController/enqueue)2476+ */2477+ enqueue(chunk?: O): void;2478+ /**2479+ * The **`error()`** method of the TransformStreamDefaultController interface errors both sides of the stream.2480+ *2481+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/TransformStreamDefaultController/error)2482+ */2483+ error(reason: any): void;2484+ /**2485+ * The **`terminate()`** method of the TransformStreamDefaultController interface closes the readable side and errors the writable side of the stream.2486+ *2487+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/TransformStreamDefaultController/terminate)2488+ */2489+ terminate(): void;2490+}2491+interface ReadableWritablePair<R = any, W = any> {2492+ readable: ReadableStream<R>;2493+ /**2494+ * Provides a convenient, chainable way of piping this readable stream through a transform stream (or any other { writable, readable } pair). It simply pipes the stream into the writable side of the supplied pair, and returns the readable side for further use.2495+ *2496+ * Piping a stream will lock it for the duration of the pipe, preventing any other consumer from acquiring a reader.2497+ */2498+ writable: WritableStream<W>;2499+}2500+/**2501+ * The **`WritableStream`** interface of the Streams API provides a standard abstraction for writing streaming data to a destination, known as a sink.2502+ *2503+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/WritableStream)2504+ */2505+declare class WritableStream<W = any> {2506+ constructor(underlyingSink?: UnderlyingSink, queuingStrategy?: QueuingStrategy);2507+ /**2508+ * The **`locked`** read-only property of the WritableStream interface returns a boolean indicating whether the `WritableStream` is locked to a writer.2509+ *2510+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/WritableStream/locked)2511+ */2512+ get locked(): boolean;2513+ /**2514+ * The **`abort()`** method of the WritableStream interface aborts the stream, signaling that the producer can no longer successfully write to the stream and it is to be immediately moved to an error state, with any queued writes discarded.2515+ *2516+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/WritableStream/abort)2517+ */2518+ abort(reason?: any): Promise<void>;2519+ /**2520+ * The **`close()`** method of the WritableStream interface closes the associated stream.2521+ *2522+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/WritableStream/close)2523+ */2524+ close(): Promise<void>;2525+ /**2526+ * The **`getWriter()`** method of the WritableStream interface returns a new instance of WritableStreamDefaultWriter and locks the stream to that instance.2527+ *2528+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/WritableStream/getWriter)2529+ */2530+ getWriter(): WritableStreamDefaultWriter<W>;2531+}2532+/**2533+ * The **`WritableStreamDefaultWriter`** interface of the Streams API is the object returned by WritableStream.getWriter() and once created locks the writer to the `WritableStream` ensuring that no other streams can write to the underlying sink.2534+ *2535+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/WritableStreamDefaultWriter)2536+ */2537+declare class WritableStreamDefaultWriter<W = any> {2538+ constructor(stream: WritableStream);2539+ /**2540+ * The **`closed`** read-only property of the the stream errors or the writer's lock is released.2541+ *2542+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/WritableStreamDefaultWriter/closed)2543+ */2544+ get closed(): Promise<void>;2545+ /**2546+ * The **`ready`** read-only property of the that resolves when the desired size of the stream's internal queue transitions from non-positive to positive, signaling that it is no longer applying backpressure.2547+ *2548+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/WritableStreamDefaultWriter/ready)2549+ */2550+ get ready(): Promise<void>;2551+ /**2552+ * The **`desiredSize`** read-only property of the to fill the stream's internal queue.2553+ *2554+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/WritableStreamDefaultWriter/desiredSize)2555+ */2556+ get desiredSize(): number | null;2557+ /**2558+ * The **`abort()`** method of the the producer can no longer successfully write to the stream and it is to be immediately moved to an error state, with any queued writes discarded.2559+ *2560+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/WritableStreamDefaultWriter/abort)2561+ */2562+ abort(reason?: any): Promise<void>;2563+ /**2564+ * The **`close()`** method of the stream.2565+ *2566+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/WritableStreamDefaultWriter/close)2567+ */2568+ close(): Promise<void>;2569+ /**2570+ * The **`write()`** method of the operation.2571+ *2572+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/WritableStreamDefaultWriter/write)2573+ */2574+ write(chunk?: W): Promise<void>;2575+ /**2576+ * The **`releaseLock()`** method of the corresponding stream.2577+ *2578+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/WritableStreamDefaultWriter/releaseLock)2579+ */2580+ releaseLock(): void;2581+}2582+/**2583+ * The **`TransformStream`** interface of the Streams API represents a concrete implementation of the pipe chain _transform stream_ concept.2584+ *2585+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/TransformStream)2586+ */2587+declare class TransformStream<I = any, O = any> {2588+ constructor(transformer?: Transformer<I, O>, writableStrategy?: QueuingStrategy<I>, readableStrategy?: QueuingStrategy<O>);2589+ /**2590+ * The **`readable`** read-only property of the TransformStream interface returns the ReadableStream instance controlled by this `TransformStream`.2591+ *2592+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/TransformStream/readable)2593+ */2594+ get readable(): ReadableStream<O>;2595+ /**2596+ * The **`writable`** read-only property of the TransformStream interface returns the WritableStream instance controlled by this `TransformStream`.2597+ *2598+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/TransformStream/writable)2599+ */2600+ get writable(): WritableStream<I>;2601+}2602+declare class FixedLengthStream extends IdentityTransformStream {2603+ constructor(expectedLength: number | bigint, queuingStrategy?: IdentityTransformStreamQueuingStrategy);2604+}2605+declare class IdentityTransformStream extends TransformStream<ArrayBuffer | ArrayBufferView, Uint8Array> {2606+ constructor(queuingStrategy?: IdentityTransformStreamQueuingStrategy);2607+}2608+interface IdentityTransformStreamQueuingStrategy {2609+ highWaterMark?: (number | bigint);2610+}2611+interface ReadableStreamValuesOptions {2612+ preventCancel?: boolean;2613+}2614+/**2615+ * The **`CompressionStream`** interface of the Compression Streams API is an API for compressing a stream of data.2616+ *2617+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/CompressionStream)2618+ */2619+declare class CompressionStream extends TransformStream<ArrayBuffer | ArrayBufferView, Uint8Array> {2620+ constructor(format: "gzip" | "deflate" | "deflate-raw");2621+}2622+/**2623+ * The **`DecompressionStream`** interface of the Compression Streams API is an API for decompressing a stream of data.2624+ *2625+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/DecompressionStream)2626+ */2627+declare class DecompressionStream extends TransformStream<ArrayBuffer | ArrayBufferView, Uint8Array> {2628+ constructor(format: "gzip" | "deflate" | "deflate-raw");2629+}2630+/**2631+ * The **`TextEncoderStream`** interface of the Encoding API converts a stream of strings into bytes in the UTF-8 encoding.2632+ *2633+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/TextEncoderStream)2634+ */2635+declare class TextEncoderStream extends TransformStream<string, Uint8Array> {2636+ constructor();2637+ get encoding(): string;2638+}2639+/**2640+ * The **`TextDecoderStream`** interface of the Encoding API converts a stream of text in a binary encoding, such as UTF-8 etc., to a stream of strings.2641+ *2642+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/TextDecoderStream)2643+ */2644+declare class TextDecoderStream extends TransformStream<ArrayBuffer | ArrayBufferView, string> {2645+ constructor(label?: string, options?: TextDecoderStreamTextDecoderStreamInit);2646+ get encoding(): string;2647+ get fatal(): boolean;2648+ get ignoreBOM(): boolean;2649+}2650+interface TextDecoderStreamTextDecoderStreamInit {2651+ fatal?: boolean;2652+ ignoreBOM?: boolean;2653+}2654+/**2655+ * The **`ByteLengthQueuingStrategy`** interface of the Streams API provides a built-in byte length queuing strategy that can be used when constructing streams.2656+ *2657+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/ByteLengthQueuingStrategy)2658+ */2659+declare class ByteLengthQueuingStrategy implements QueuingStrategy<ArrayBufferView> {2660+ constructor(init: QueuingStrategyInit);2661+ /**2662+ * The read-only **`ByteLengthQueuingStrategy.highWaterMark`** property returns the total number of bytes that can be contained in the internal queue before backpressure is applied.2663+ *2664+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/ByteLengthQueuingStrategy/highWaterMark)2665+ */2666+ get highWaterMark(): number;2667+ /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/ByteLengthQueuingStrategy/size) */2668+ get size(): (chunk?: any) => number;2669+}2670+/**2671+ * The **`CountQueuingStrategy`** interface of the Streams API provides a built-in chunk counting queuing strategy that can be used when constructing streams.2672+ *2673+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/CountQueuingStrategy)2674+ */2675+declare class CountQueuingStrategy implements QueuingStrategy {2676+ constructor(init: QueuingStrategyInit);2677+ /**2678+ * The read-only **`CountQueuingStrategy.highWaterMark`** property returns the total number of chunks that can be contained in the internal queue before backpressure is applied.2679+ *2680+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/CountQueuingStrategy/highWaterMark)2681+ */2682+ get highWaterMark(): number;2683+ /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/CountQueuingStrategy/size) */2684+ get size(): (chunk?: any) => number;2685+}2686+interface QueuingStrategyInit {2687+ /**2688+ * Creates a new ByteLengthQueuingStrategy with the provided high water mark.2689+ *2690+ * Note that the provided high water mark will not be validated ahead of time. Instead, if it is negative, NaN, or not a number, the resulting ByteLengthQueuingStrategy will cause the corresponding stream constructor to throw.2691+ */2692+ highWaterMark: number;2693+}2694+interface TracePreviewInfo {2695+ id: string;2696+ slug: string;2697+ name: string;2698+}2699+interface ScriptVersion {2700+ id?: string;2701+ tag?: string;2702+ message?: string;2703+}2704+declare abstract class TailEvent extends ExtendableEvent {2705+ readonly events: TraceItem[];2706+ readonly traces: TraceItem[];2707+}2708+interface TraceItem {2709+ readonly event: (TraceItemFetchEventInfo | TraceItemJsRpcEventInfo | TraceItemConnectEventInfo | TraceItemScheduledEventInfo | TraceItemAlarmEventInfo | TraceItemQueueEventInfo | TraceItemEmailEventInfo | TraceItemTailEventInfo | TraceItemCustomEventInfo | TraceItemHibernatableWebSocketEventInfo) | null;2710+ readonly eventTimestamp: number | null;2711+ readonly logs: TraceLog[];2712+ readonly exceptions: TraceException[];2713+ readonly diagnosticsChannelEvents: TraceDiagnosticChannelEvent[];2714+ readonly scriptName: string | null;2715+ readonly entrypoint?: string;2716+ readonly scriptVersion?: ScriptVersion;2717+ readonly dispatchNamespace?: string;2718+ readonly scriptTags?: string[];2719+ readonly tailAttributes?: Record<string, boolean | number | string>;2720+ readonly preview?: TracePreviewInfo;2721+ readonly durableObjectId?: string;2722+ readonly outcome: string;2723+ readonly executionModel: string;2724+ readonly truncated: boolean;2725+ readonly cpuTime: number;2726+ readonly wallTime: number;2727+}2728+interface TraceItemAlarmEventInfo {2729+ readonly scheduledTime: Date;2730+}2731+interface TraceItemConnectEventInfo {2732+}2733+interface TraceItemCustomEventInfo {2734+}2735+interface TraceItemScheduledEventInfo {2736+ readonly scheduledTime: number;2737+ readonly cron: string;2738+}2739+interface TraceItemQueueEventInfo {2740+ readonly queue: string;2741+ readonly batchSize: number;2742+}2743+interface TraceItemEmailEventInfo {2744+ readonly mailFrom: string;2745+ readonly rcptTo: string;2746+ readonly rawSize: number;2747+}2748+interface TraceItemTailEventInfo {2749+ readonly consumedEvents: TraceItemTailEventInfoTailItem[];2750+}2751+interface TraceItemTailEventInfoTailItem {2752+ readonly scriptName: string | null;2753+}2754+interface TraceItemFetchEventInfo {2755+ readonly response?: TraceItemFetchEventInfoResponse;2756+ readonly request: TraceItemFetchEventInfoRequest;2757+}2758+interface TraceItemFetchEventInfoRequest {2759+ readonly cf?: any;2760+ readonly headers: Record<string, string>;2761+ readonly method: string;2762+ readonly url: string;2763+ getUnredacted(): TraceItemFetchEventInfoRequest;2764+}2765+interface TraceItemFetchEventInfoResponse {2766+ readonly status: number;2767+}2768+interface TraceItemJsRpcEventInfo {2769+ readonly rpcMethod: string;2770+}2771+interface TraceItemHibernatableWebSocketEventInfo {2772+ readonly getWebSocketEvent: TraceItemHibernatableWebSocketEventInfoMessage | TraceItemHibernatableWebSocketEventInfoClose | TraceItemHibernatableWebSocketEventInfoError;2773+}2774+interface TraceItemHibernatableWebSocketEventInfoMessage {2775+ readonly webSocketEventType: string;2776+}2777+interface TraceItemHibernatableWebSocketEventInfoClose {2778+ readonly webSocketEventType: string;2779+ readonly code: number;2780+ readonly wasClean: boolean;2781+}2782+interface TraceItemHibernatableWebSocketEventInfoError {2783+ readonly webSocketEventType: string;2784+}2785+interface TraceLog {2786+ readonly timestamp: number;2787+ readonly level: string;2788+ readonly message: any;2789+}2790+interface TraceException {2791+ readonly timestamp: number;2792+ readonly message: string;2793+ readonly name: string;2794+ readonly stack?: string;2795+}2796+interface TraceDiagnosticChannelEvent {2797+ readonly timestamp: number;2798+ readonly channel: string;2799+ readonly message: any;2800+}2801+interface TraceMetrics {2802+ readonly cpuTime: number;2803+ readonly wallTime: number;2804+}2805+interface UnsafeTraceMetrics {2806+ fromTrace(item: TraceItem): TraceMetrics;2807+}2808+/**2809+ * The **`URL`** interface is used to parse, construct, normalize, and encode URL.2810+ *2811+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/URL)2812+ */2813+declare class URL {2814+ constructor(url: string | URL, base?: string | URL);2815+ /**2816+ * The **`origin`** read-only property of the URL interface returns a string containing the Unicode serialization of the origin of the represented URL.2817+ *2818+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/URL/origin)2819+ */2820+ get origin(): string;2821+ /**2822+ * The **`href`** property of the URL interface is a string containing the whole URL.2823+ *2824+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/URL/href)2825+ */2826+ get href(): string;2827+ /**2828+ * The **`href`** property of the URL interface is a string containing the whole URL.2829+ *2830+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/URL/href)2831+ */2832+ set href(value: string);2833+ /**2834+ * The **`protocol`** property of the URL interface is a string containing the protocol or scheme of the URL, including the final `':'`.2835+ *2836+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/URL/protocol)2837+ */2838+ get protocol(): string;2839+ /**2840+ * The **`protocol`** property of the URL interface is a string containing the protocol or scheme of the URL, including the final `':'`.2841+ *2842+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/URL/protocol)2843+ */2844+ set protocol(value: string);2845+ /**2846+ * The **`username`** property of the URL interface is a string containing the username component of the URL.2847+ *2848+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/URL/username)2849+ */2850+ get username(): string;2851+ /**2852+ * The **`username`** property of the URL interface is a string containing the username component of the URL.2853+ *2854+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/URL/username)2855+ */2856+ set username(value: string);2857+ /**2858+ * The **`password`** property of the URL interface is a string containing the password component of the URL.2859+ *2860+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/URL/password)2861+ */2862+ get password(): string;2863+ /**2864+ * The **`password`** property of the URL interface is a string containing the password component of the URL.2865+ *2866+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/URL/password)2867+ */2868+ set password(value: string);2869+ /**2870+ * The **`host`** property of the URL interface is a string containing the host, which is the URL.hostname, and then, if the port of the URL is nonempty, a `':'`, followed by the URL.port of the URL.2871+ *2872+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/URL/host)2873+ */2874+ get host(): string;2875+ /**2876+ * The **`host`** property of the URL interface is a string containing the host, which is the URL.hostname, and then, if the port of the URL is nonempty, a `':'`, followed by the URL.port of the URL.2877+ *2878+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/URL/host)2879+ */2880+ set host(value: string);2881+ /**2882+ * The **`hostname`** property of the URL interface is a string containing either the domain name or IP address of the URL.2883+ *2884+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/URL/hostname)2885+ */2886+ get hostname(): string;2887+ /**2888+ * The **`hostname`** property of the URL interface is a string containing either the domain name or IP address of the URL.2889+ *2890+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/URL/hostname)2891+ */2892+ set hostname(value: string);2893+ /**2894+ * The **`port`** property of the URL interface is a string containing the port number of the URL.2895+ *2896+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/URL/port)2897+ */2898+ get port(): string;2899+ /**2900+ * The **`port`** property of the URL interface is a string containing the port number of the URL.2901+ *2902+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/URL/port)2903+ */2904+ set port(value: string);2905+ /**2906+ * The **`pathname`** property of the URL interface represents a location in a hierarchical structure.2907+ *2908+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/URL/pathname)2909+ */2910+ get pathname(): string;2911+ /**2912+ * The **`pathname`** property of the URL interface represents a location in a hierarchical structure.2913+ *2914+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/URL/pathname)2915+ */2916+ set pathname(value: string);2917+ /**2918+ * The **`search`** property of the URL interface is a search string, also called a _query string_, that is a string containing a `'?'` followed by the parameters of the URL.2919+ *2920+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/URL/search)2921+ */2922+ get search(): string;2923+ /**2924+ * The **`search`** property of the URL interface is a search string, also called a _query string_, that is a string containing a `'?'` followed by the parameters of the URL.2925+ *2926+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/URL/search)2927+ */2928+ set search(value: string);2929+ /**2930+ * The **`hash`** property of the URL interface is a string containing a `'#'` followed by the fragment identifier of the URL.2931+ *2932+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/URL/hash)2933+ */2934+ get hash(): string;2935+ /**2936+ * The **`hash`** property of the URL interface is a string containing a `'#'` followed by the fragment identifier of the URL.2937+ *2938+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/URL/hash)2939+ */2940+ set hash(value: string);2941+ /**2942+ * The **`searchParams`** read-only property of the access to the [MISSING: httpmethod('GET')] decoded query arguments contained in the URL.2943+ *2944+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/URL/searchParams)2945+ */2946+ get searchParams(): URLSearchParams;2947+ /**2948+ * The **`toJSON()`** method of the URL interface returns a string containing a serialized version of the URL, although in practice it seems to have the same effect as ```js-nolint toJSON() ``` None.2949+ *2950+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/URL/toJSON)2951+ */2952+ toJSON(): string;2953+ /*function toString() { [native code] }*/2954+ toString(): string;2955+ /**2956+ * The **`URL.canParse()`** static method of the URL interface returns a boolean indicating whether or not an absolute URL, or a relative URL combined with a base URL, are parsable and valid.2957+ *2958+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/URL/canParse_static)2959+ */2960+ static canParse(url: string, base?: string): boolean;2961+ /**2962+ * The **`URL.parse()`** static method of the URL interface returns a newly created URL object representing the URL defined by the parameters.2963+ *2964+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/URL/parse_static)2965+ */2966+ static parse(url: string, base?: string): URL | null;2967+ /**2968+ * The **`createObjectURL()`** static method of the URL interface creates a string containing a URL representing the object given in the parameter.2969+ *2970+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/URL/createObjectURL_static)2971+ */2972+ static createObjectURL(object: File | Blob): string;2973+ /**2974+ * The **`revokeObjectURL()`** static method of the URL interface releases an existing object URL which was previously created by calling Call this method when you've finished using an object URL to let the browser know not to keep the reference to the file any longer.2975+ *2976+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/URL/revokeObjectURL_static)2977+ */2978+ static revokeObjectURL(object_url: string): void;2979+}2980+/**2981+ * The **`URLSearchParams`** interface defines utility methods to work with the query string of a URL.2982+ *2983+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/URLSearchParams)2984+ */2985+declare class URLSearchParams {2986+ constructor(init?: (Iterable<Iterable<string>> | Record<string, string> | string));2987+ /**2988+ * The **`size`** read-only property of the URLSearchParams interface indicates the total number of search parameter entries.2989+ *2990+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/URLSearchParams/size)2991+ */2992+ get size(): number;2993+ /**2994+ * The **`append()`** method of the URLSearchParams interface appends a specified key/value pair as a new search parameter.2995+ *2996+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/URLSearchParams/append)2997+ */2998+ append(name: string, value: string): void;2999+ /**3000+ * The **`delete()`** method of the URLSearchParams interface deletes specified parameters and their associated value(s) from the list of all search parameters.diff truncated
Not shown: this page already shows 6000 lines of diff.