diff --git a/codecov.yml b/codecov.yml
index 412a0cbdbc..99e5507877 100644
--- a/codecov.yml
+++ b/codecov.yml
@@ -82,6 +82,11 @@ ignore:
# discovery-index's process entrypoint (#7164) -- same "Docker build+boot, not unit-coverable" reasoning
# as src/server.ts above; app.ts (everything it wires together) is what tests actually import.
- "packages/discovery-index/src/server.ts"
+ # discovery-index's Cloudflare Container/Durable Object entry point (#7167) -- pure infra glue exercised
+ # only by real Cloudflare Containers infrastructure, same reasoning as server.ts above.
+ - "packages/discovery-index/src/worker.ts"
+ - "packages/discovery-index/src/env.d.ts"
+ - "packages/discovery-index/worker-configuration.d.ts"
# NOTE: packages/loopover-miner/bin/loopover-miner.ts and (once #7291 migrates it)
# packages/loopover-mcp/bin/loopover-mcp.ts are plain CLI dispatchers (no exports, terminate via
# process.exit()) exercised only via real subprocess spawn (test/unit/support/{miner,mcp}-cli-harness.ts),
diff --git a/package-lock.json b/package-lock.json
index f26ec6ab53..92c4d2934e 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1584,6 +1584,12 @@
}
}
},
+ "node_modules/@cloudflare/containers": {
+ "version": "0.3.7",
+ "resolved": "https://registry.npmjs.org/@cloudflare/containers/-/containers-0.3.7.tgz",
+ "integrity": "sha512-DM9dm3FnIBSyiSJ1FLavKwl/lk3oAmTaynCzZQ9pZR0ncRPquSxkxd8Nu2MFILxmDDsPkxKsSNEh9mHHMty4Fw==",
+ "license": "MIT OR Apache-2.0"
+ },
"node_modules/@cloudflare/kv-asset-handler": {
"version": "0.5.0",
"resolved": "https://registry.npmjs.org/@cloudflare/kv-asset-handler/-/kv-asset-handler-0.5.0.tgz",
@@ -20824,18 +20830,113 @@
"name": "@loopover/discovery-index",
"version": "0.1.0",
"dependencies": {
+ "@cloudflare/containers": "^0.3.7",
"@hono/node-server": "^1.19.14",
"@loopover/engine": "^3.4.0",
"hono": "^4.12.27"
},
"devDependencies": {
+ "@cloudflare/workers-types": "^5.20260721.1",
"@types/node": "^22.20.0",
- "typescript": "^5.9.3"
+ "typescript": "^5.9.3",
+ "wrangler": "^4.112.0"
},
"engines": {
"node": ">=22.0.0 <23.0.0"
}
},
+ "packages/discovery-index/node_modules/@cloudflare/workerd-darwin-64": {
+ "version": "1.20260714.1",
+ "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-64/-/workerd-darwin-64-1.20260714.1.tgz",
+ "integrity": "sha512-ZWXqAN8G7Cx9hMRQuk+59ziJhR3j1F4iO+Qs8aHdfKZ3Dq5Yi/57xvkJTgCGBnW1YU/L78r8f6HEy51bwbTpNw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=16"
+ }
+ },
+ "packages/discovery-index/node_modules/@cloudflare/workerd-darwin-arm64": {
+ "version": "1.20260714.1",
+ "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-arm64/-/workerd-darwin-arm64-1.20260714.1.tgz",
+ "integrity": "sha512-tueWxWC3wyCbMG6zRAxsMXX0YLgrRWbiAPYFQ2uJ7dUH8G+5E7UTWaQS9B1HdJ0bpKFW1NWxhs1o2noKVFSUYg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=16"
+ }
+ },
+ "packages/discovery-index/node_modules/@cloudflare/workerd-linux-64": {
+ "version": "1.20260714.1",
+ "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-64/-/workerd-linux-64-1.20260714.1.tgz",
+ "integrity": "sha512-1VChTZRb0l0F7R4e1G5RtLKV4oFi6x+rQgxh2+yu887j3l/3TLgatuv1L8/5zhc9gKEhATTxOh0e52Rtd9dDWQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=16"
+ }
+ },
+ "packages/discovery-index/node_modules/@cloudflare/workerd-linux-arm64": {
+ "version": "1.20260714.1",
+ "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-arm64/-/workerd-linux-arm64-1.20260714.1.tgz",
+ "integrity": "sha512-rMm3G+NirG2UdgHIRDdF1asNC6FqgIzZzkRG+VDhhDGcVxAQwvrMT1E38BivEvHr3G04MB4AfhcOczX0+GtRkQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=16"
+ }
+ },
+ "packages/discovery-index/node_modules/@cloudflare/workerd-windows-64": {
+ "version": "1.20260714.1",
+ "resolved": "https://registry.npmjs.org/@cloudflare/workerd-windows-64/-/workerd-windows-64-1.20260714.1.tgz",
+ "integrity": "sha512-cGqnU3Hg2YZS/k3SAqrMp1DjpdsyFde72tWltdl6ZT9+SFz/Zrk/8gyTU1TcxC4YApXeNVH5TyU5cOGPgUJ0pg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=16"
+ }
+ },
+ "packages/discovery-index/node_modules/@cloudflare/workers-types": {
+ "version": "5.20260721.1",
+ "resolved": "https://registry.npmjs.org/@cloudflare/workers-types/-/workers-types-5.20260721.1.tgz",
+ "integrity": "sha512-J6HZRuQOP3gVe9G5rxHQVS8kQRjo7NIaF+Lz4kO2lVmaCkQ1E78APOOthaI7KyCQzp+A2NbXBQI6QLRIswdWbA==",
+ "dev": true,
+ "license": "MIT OR Apache-2.0"
+ },
"packages/discovery-index/node_modules/@hono/node-server": {
"version": "1.19.14",
"resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.14.tgz",
@@ -20858,6 +20959,37 @@
"undici-types": "~6.21.0"
}
},
+ "packages/discovery-index/node_modules/miniflare": {
+ "version": "4.20260714.0",
+ "resolved": "https://registry.npmjs.org/miniflare/-/miniflare-4.20260714.0.tgz",
+ "integrity": "sha512-MYlTCLdWCPqvrYY2uLwOjXwmglXuiHE3TGGkbOW4BwjUPa1r07E0iuHwrNDIs/sxK21r+o90Jx58AV2KeNdJZw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@cspotcode/source-map-support": "0.8.1",
+ "sharp": "0.34.5",
+ "undici": "7.28.0",
+ "workerd": "1.20260714.1",
+ "ws": "8.21.0",
+ "youch": "4.1.0-beta.10"
+ },
+ "bin": {
+ "miniflare": "bootstrap.js"
+ },
+ "engines": {
+ "node": ">=22.0.0"
+ }
+ },
+ "packages/discovery-index/node_modules/undici": {
+ "version": "7.28.0",
+ "resolved": "https://registry.npmjs.org/undici/-/undici-7.28.0.tgz",
+ "integrity": "sha512-cRZYrTDwWznlnRiPjggAGxZXanty6M8RV1ff8Wm4LWXBp7/IG8v5DnOm74DtUBp9OONpK75YlPnIjQqX0dBDtA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=20.18.1"
+ }
+ },
"packages/discovery-index/node_modules/undici-types": {
"version": "6.21.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
@@ -20865,6 +20997,63 @@
"dev": true,
"license": "MIT"
},
+ "packages/discovery-index/node_modules/workerd": {
+ "version": "1.20260714.1",
+ "resolved": "https://registry.npmjs.org/workerd/-/workerd-1.20260714.1.tgz",
+ "integrity": "sha512-oIbQzfdyl9UQUnG6XLegcSq0Mgt/7WKDbFOoqGgOWCS+/fhyGB460uKEgdAQQ9RHCO/ttcNCX/KiMIQzdoeu3Q==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "workerd": "bin/workerd"
+ },
+ "engines": {
+ "node": ">=16"
+ },
+ "optionalDependencies": {
+ "@cloudflare/workerd-darwin-64": "1.20260714.1",
+ "@cloudflare/workerd-darwin-arm64": "1.20260714.1",
+ "@cloudflare/workerd-linux-64": "1.20260714.1",
+ "@cloudflare/workerd-linux-arm64": "1.20260714.1",
+ "@cloudflare/workerd-windows-64": "1.20260714.1"
+ }
+ },
+ "packages/discovery-index/node_modules/wrangler": {
+ "version": "4.112.0",
+ "resolved": "https://registry.npmjs.org/wrangler/-/wrangler-4.112.0.tgz",
+ "integrity": "sha512-5H+XUD0TySCv1LuktFHDIEOkboH2nTfQs+35L+USt3MtntjDTMVIJprLgQcL2WBjulOyjxpd1vyTiSTJVW5MjQ==",
+ "dev": true,
+ "license": "MIT OR Apache-2.0",
+ "dependencies": {
+ "@cloudflare/kv-asset-handler": "0.5.0",
+ "@cloudflare/unenv-preset": "2.16.1",
+ "blake3-wasm": "2.1.5",
+ "esbuild": "0.28.1",
+ "miniflare": "4.20260714.0",
+ "path-to-regexp": "6.3.0",
+ "unenv": "2.0.0-rc.24",
+ "workerd": "1.20260714.1"
+ },
+ "bin": {
+ "cf-wrangler": "bin/cf-wrangler.js",
+ "wrangler": "bin/wrangler.js",
+ "wrangler2": "bin/wrangler.js"
+ },
+ "engines": {
+ "node": ">=22.0.0"
+ },
+ "optionalDependencies": {
+ "fsevents": "2.3.3"
+ },
+ "peerDependencies": {
+ "@cloudflare/workers-types": "^5.20260714.1"
+ },
+ "peerDependenciesMeta": {
+ "@cloudflare/workers-types": {
+ "optional": true
+ }
+ }
+ },
"packages/loopover-engine": {
"name": "@loopover/engine",
"version": "3.4.0",
diff --git a/packages/discovery-index/Dockerfile b/packages/discovery-index/Dockerfile
index c15a9940e2..e5fd5b83b6 100644
--- a/packages/discovery-index/Dockerfile
+++ b/packages/discovery-index/Dockerfile
@@ -1,11 +1,12 @@
-# LoopOver discovery-index service (#4250, #7164). Standalone Hono/Node service, deployment shape mirrors
-# review-enrichment/ (own Dockerfile, own port, no Cloudflare Worker involvement). Build context =
-# the packages/discovery-index/ directory — but it depends on the @loopover/engine workspace package
+# LoopOver discovery-index service (#4250, #7164, #7167). Standalone Hono/Node service (the actual runtime
+# CMD below is unchanged whether run via plain Docker or, per #7167, a Cloudflare Container). Build context
+# is the REPOSITORY ROOT, not this directory — it depends on the @loopover/engine workspace package
# (packages/loopover-engine), so the build stage copies the whole monorepo root and installs via npm
-# workspaces rather than `npm ci` inside this directory alone.
+# workspaces rather than `npm ci` inside this directory alone. See README.md's Deployment section for the
+# exact build invocation (`docker build -f packages/discovery-index/Dockerfile .` from the repo root).
FROM node:22-slim AS build
WORKDIR /app
-COPY package.json package-lock.json ./
+COPY package.json package-lock.json tsconfig.json ./
COPY packages/loopover-engine/package.json packages/loopover-engine/package.json
COPY packages/discovery-index/package.json packages/discovery-index/package.json
RUN npm ci --workspace @loopover/engine --workspace @loopover/discovery-index --include-workspace-root
diff --git a/packages/discovery-index/OPERATIONS.md b/packages/discovery-index/OPERATIONS.md
new file mode 100644
index 0000000000..d862e4b275
--- /dev/null
+++ b/packages/discovery-index/OPERATIONS.md
@@ -0,0 +1,53 @@
+# Discovery-index operating doc (maintainer-only)
+
+This is the server-side operating doc the client guide ([`discovery-plane-operator-guide.md`](../loopover-miner/docs/discovery-plane-operator-guide.md)) refers to. It restates this service's own retention/abuse boundaries for whoever operates it (#4250), and is not published to the public docs website — the public-facing side of this contract is the client guide above.
+
+## What this service is
+
+The hosted half of the discovery plane: a shared, cached GitHub issue/search index that opted-in `loopover-miner` instances query instead of each independently fanning out to GitHub's search/listing APIs, plus fleet-wide soft-claim dedup so two miners don't start duplicate work on the same issue. See [`README.md`](README.md) for the API surface and [#7167](https://github.com/JSONbored/loopover/issues/7167) for the deployment shape (a single Cloudflare Container instance).
+
+**It is optional, shared, owner-operated infrastructure** — self-hosted AMS/ORB deployments work identically whether or not this service exists or is reachable. Nothing here ever gates core self-host functionality.
+
+## What is retained
+
+- Cached GitHub issue/search **metadata only** (titles, labels, counts, timestamps, URLs) — the exact shape `@loopover/engine`'s [`discovery-index-contract.ts`](../loopover-engine/src/discovery-index-contract.ts) defines, for up to `DISCOVERY_INDEX_CACHE_TTL_MS` (default 5 minutes).
+- Soft-claim records: `repoFullName#issueNumber` → claim timestamp, for up to `DISCOVERY_INDEX_SOFT_CLAIM_TTL_MS` (default 30 minutes). No caller identity is stored alongside a claim — the shipped client payload never sends one (see `soft-claim.ts`'s own header comment).
+- Rate-limit buckets: `ip:
` → request count, for a 60-second rolling window (`rate-limiter.ts`).
+
+All of the above are **in-process memory only** — nothing is written to disk or a database. Restarting the container (a redeploy, a sleep/wake cycle) clears all of it. There is no backup, export, or retention beyond the TTLs above by design.
+
+## What is never retained or transmitted
+
+Same boundary the client guide documents, restated from the server's own side:
+
+- Source file contents, patches, diffs, full issue/PR bodies, or review comments.
+- GitHub tokens, PATs, App private keys, or any actor-capable credential belonging to a *caller* (this service's own `DISCOVERY_INDEX_GITHUB_TOKEN` is used only to call GitHub's own API on the caller's behalf; it is never echoed back in a response or log line).
+- Caller identity of any kind (login, IP is used transiently for rate-limiting only — see below — never persisted alongside a cache entry or claim).
+- Reward amounts, wallet addresses, hotkeys, trust scores, or private rankings — this service has no concept of any of these and never will.
+
+## Abuse posture (#4250)
+
+All opted-in miners authenticate with **one shared `DISCOVERY_INDEX_SHARED_SECRET`** — there is no per-caller identity on the wire (`soft-claim.ts`'s client contract deliberately carries none), so per-caller rate-limiting isn't possible without a bigger redesign (individual caller tokens). The mitigation in place instead:
+
+- A blanket, **IP-keyed** rate limit (`rate-limiter.ts`, enforced in `worker.ts` ahead of the Container) — 60 requests/minute per source IP, applied only to `/v1/discovery-index/*` (the real work; `/health`/`/ready`/`/metrics` are exempt for uptime monitors).
+- Fails **open** on a rate-limiter Durable Object error — an unrelated Cloudflare-side hiccup degrades to "unlimited" rather than an outage for every legitimate caller (same fail-open reasoning as the main app's own rate limiter, `src/auth/rate-limit.ts`).
+- This bounds a single misbehaving IP, not a distributed abuser rotating source IPs while holding the shared secret. If that ever becomes a real, observed problem (not just a theoretical one), the real fix is issuing **per-installation** secrets instead of one shared one — a bigger change, tracked as a follow-up if and when it's actually needed, not built speculatively now.
+
+## Incident response
+
+**If `DISCOVERY_INDEX_SHARED_SECRET` leaks or is suspected compromised:**
+1. `npx wrangler secret put DISCOVERY_INDEX_SHARED_SECRET` with a new value — this immediately invalidates the old one for every caller (shared-secret model, no per-caller revocation needed).
+2. Redistribute the new secret to legitimate opted-in operators through whatever channel currently exists for that (this doc doesn't define that channel — it's not built yet as of #7167; today there are no real opted-in operators to notify).
+3. No data-exfiltration concern from the leak itself: per "What is retained" above, there is nothing sensitive to steal even with full API access — the worst case is cache pollution or soft-claim griefing (a malicious "claim" call blocking a real miner from an issue for up to 30 minutes), not data exposure.
+
+**If `DISCOVERY_INDEX_GITHUB_TOKEN` leaks:**
+1. Revoke it directly on GitHub (this service's own token, isolated from every other component's per `README.md`'s Configuration table — revoking it affects only this service).
+2. `npx wrangler secret put DISCOVERY_INDEX_GITHUB_TOKEN` with a freshly generated token.
+
+**If abuse is observed (rate-limit denials spiking, or GitHub API quota exhaustion on `DISCOVERY_INDEX_GITHUB_TOKEN`):**
+1. Check `/metrics` (Prometheus text format) for `discovery_index_query_requests_total`/`discovery_index_soft_claim_requests_total` by status, and Cloudflare's own Worker observability (enabled in `wrangler.jsonc`) for per-IP request volume.
+2. A single abusive IP is already capped by the rate limiter; if the pattern is distributed (many IPs, one shared secret), rotating the shared secret (above) is the only real lever until per-installation secrets exist.
+
+## Monitoring
+
+`/health` (liveness) and `/ready` (readiness — reports whether `DISCOVERY_INDEX_GITHUB_TOKEN` is configured) are the two routes an external uptime check should poll; both are exempt from the rate limit. `/metrics` exposes Prometheus-format counters/histograms for request outcomes and latency. This service is **not** wired into the self-host fleet's own Grafana/Alloy stack — it isn't a self-hosted instance, it's the one hosted plane, deployed and observed separately (Cloudflare's own Worker observability, per `wrangler.jsonc`) rather than folded into infrastructure that assumes a self-hoster's own box.
diff --git a/packages/discovery-index/README.md b/packages/discovery-index/README.md
index df47b603ab..235289b500 100644
--- a/packages/discovery-index/README.md
+++ b/packages/discovery-index/README.md
@@ -1,6 +1,6 @@
# Discovery-index service
-A standalone microservice implementing the hosted half of the [discovery plane](../loopover-miner/docs/discovery-plane-operator-guide.md) (#4250): a shared, cached GitHub issue/search index that opted-in `loopover-miner` instances can query instead of each independently fanning out to GitHub's search/listing APIs. Metadata-only — see [`@loopover/engine`'s discovery-index contract](../loopover-engine/src/discovery-index-contract.ts) for the exact public-safe candidate shape and the forbidden-field boundary this service can never cross.
+A standalone microservice implementing the hosted half of the [discovery plane](../loopover-miner/docs/discovery-plane-operator-guide.md) (#4250): a shared, cached GitHub issue/search index that opted-in `loopover-miner` instances can query instead of each independently fanning out to GitHub's search/listing APIs. Metadata-only — see [`@loopover/engine`'s discovery-index contract](../loopover-engine/src/discovery-index-contract.ts) for the exact public-safe candidate shape and the forbidden-field boundary this service can never cross. See [`OPERATIONS.md`](OPERATIONS.md) for the maintainer-facing retention/abuse/incident-response doc.
This is optional, shared infrastructure to reduce duplicate GitHub API pressure across the miner fleet (the rate-limit incident this mitigates: #1936). Self-hosted AMS/ORB deployments are completely unaffected whether or not this service exists — opting in is a separate, default-off client change (#7168).
@@ -30,11 +30,24 @@ Soft-claim design note: the shipped client payload never carries caller identity
## Deployment
-Build and run via the included `Dockerfile` (build context = the **repository root**, since this service depends on the `@loopover/engine` workspace package):
+The production deployment (#7167) is a **Cloudflare Container**, not a bare VPS/Docker host: `wrangler.jsonc` runs the exact same `Dockerfile` as a Container behind a Durable Object, giving it a public URL (`discovery.loopover.ai`) and TLS with no manual DNS/reverse-proxy setup. This was chosen over a raw VPS/PaaS deploy because it reuses the same platform already chosen for the ORB+AMS hosted control-plane (#7173), and over native (non-Container) Workers because this service's cache (`cache.ts`) and soft-claim dedup store (`soft-claim.ts`) are both in-process memory — a Container is one real, persistent process (so that state behaves correctly, exactly as already tested), where Workers' distributed isolates would not reliably share it. See `src/worker.ts`'s header comment for why the config pins `max_instances: 1` (a correctness requirement for soft-claim dedup, not just a cost choice).
+
+**First-time setup**, from this directory:
+
+```sh
+npx wrangler secret put DISCOVERY_INDEX_SHARED_SECRET # never commit a real value
+npx wrangler secret put DISCOVERY_INDEX_GITHUB_TOKEN # never commit a real value
+npm run cf:typegen # regenerate worker-configuration.d.ts after any wrangler.jsonc change
+ # (wraps `wrangler types` -- raw wrangler output has
+ # trailing whitespace that fails this repo's git diff --check)
+npx wrangler deploy
+```
+
+`npm run cf:dev` runs it locally against Cloudflare's dev runtime; `npm run cf:typecheck` type-checks `src/worker.ts` against the Workers runtime types (kept in a separate `tsconfig.worker.json` from this package's own Node build — see that file's header comment for why).
+
+**Local, non-Cloudflare testing** (no wrangler/Containers involved) still works exactly as before via the plain Docker image:
```sh
docker build -f packages/discovery-index/Dockerfile -t loopover-discovery-index .
docker run -p 8080:8080 -e DISCOVERY_INDEX_SHARED_SECRET=... -e DISCOVERY_INDEX_GITHUB_TOKEN=... loopover-discovery-index
```
-
-Hosting/DNS/TLS/observability wiring for a public deployment is tracked separately (#7167), not part of this service's own code.
diff --git a/packages/discovery-index/package.json b/packages/discovery-index/package.json
index 8d40187096..9ca454b79e 100644
--- a/packages/discovery-index/package.json
+++ b/packages/discovery-index/package.json
@@ -10,15 +10,22 @@
"scripts": {
"build": "tsc -p tsconfig.json",
"start": "node dist/server.js",
- "dev": "node --experimental-strip-types --watch src/server.ts"
+ "dev": "node --experimental-strip-types --watch src/server.ts",
+ "cf:dev": "wrangler dev",
+ "cf:deploy": "wrangler deploy",
+ "cf:typecheck": "tsc -p tsconfig.worker.json --noEmit",
+ "cf:typegen": "node scripts/gen-cf-typegen.mjs"
},
"dependencies": {
+ "@cloudflare/containers": "^0.3.7",
"@hono/node-server": "^1.19.14",
"@loopover/engine": "^3.4.0",
"hono": "^4.12.27"
},
"devDependencies": {
+ "@cloudflare/workers-types": "^5.20260721.1",
"@types/node": "^22.20.0",
- "typescript": "^5.9.3"
+ "typescript": "^5.9.3",
+ "wrangler": "^4.112.0"
}
}
diff --git a/packages/discovery-index/scripts/gen-cf-typegen.mjs b/packages/discovery-index/scripts/gen-cf-typegen.mjs
new file mode 100644
index 0000000000..6dd7c073db
--- /dev/null
+++ b/packages/discovery-index/scripts/gen-cf-typegen.mjs
@@ -0,0 +1,26 @@
+#!/usr/bin/env node
+// Regenerates worker-configuration.d.ts from wrangler.jsonc and strips trailing whitespace -- wrangler's
+// raw `wrangler types` output has trailing whitespace on several lines, which fails this repo's own
+// `git diff --check` whitespace gate the moment it's committed (found the hard way on #7167/#4250's PR).
+// Simpler than the root repo's scripts/gen-cf-typegen.mjs: this package's Env has no `vars`-derived
+// Pick union to reformat (only Durable Object bindings + ambient-declared secrets),
+// so only the whitespace-stripping half of that script's job applies here.
+import { execFileSync } from "node:child_process";
+import { readFileSync, writeFileSync } from "node:fs";
+import { createRequire } from "node:module";
+import { dirname, join } from "node:path";
+
+const require = createRequire(import.meta.url);
+
+function resolveLocalWranglerBin() {
+ const pkgJsonPath = require.resolve("wrangler/package.json");
+ const pkg = JSON.parse(readFileSync(pkgJsonPath, "utf8"));
+ const binRelativePath = typeof pkg.bin === "string" ? pkg.bin : pkg.bin.wrangler;
+ return join(dirname(pkgJsonPath), binRelativePath);
+}
+
+const OUTPUT_PATH = "worker-configuration.d.ts";
+
+execFileSync(process.execPath, [resolveLocalWranglerBin(), "types", OUTPUT_PATH], { stdio: "inherit" });
+const stripped = readFileSync(OUTPUT_PATH, "utf8").replace(/[ \t]+$/gm, "");
+writeFileSync(OUTPUT_PATH, stripped);
diff --git a/packages/discovery-index/src/env.d.ts b/packages/discovery-index/src/env.d.ts
new file mode 100644
index 0000000000..314680da93
--- /dev/null
+++ b/packages/discovery-index/src/env.d.ts
@@ -0,0 +1,27 @@
+// Wrangler secrets don't appear in wrangler.jsonc (never committed) so `wrangler types` can't discover
+// their names -- declared here via ambient global merge with the generated Env interface
+// (worker-configuration.d.ts), mirroring src/env.d.ts's pattern in the main app at the repo root. Set both
+// via `npx wrangler secret put ` before first deploy; never given a real value in this repo.
+// This file has a top-level `export {}` (making it a module), so both augmentations below must live
+// inside `declare global` -- a bare top-level `declare namespace Cloudflare` here would be scoped to this
+// module only and would NOT merge with worker-configuration.d.ts's script-scope `Cloudflare` namespace.
+declare global {
+ interface Env {
+ /** Bearer secret required to call this service's own /v1/discovery-index/* routes. */
+ DISCOVERY_INDEX_SHARED_SECRET: string;
+ /** This service's own GitHub token, isolated from any other component's. */
+ DISCOVERY_INDEX_GITHUB_TOKEN: string;
+ }
+
+ // `import { env } from "cloudflare:workers"` (used in worker.ts's Container class field initializers,
+ // which run outside the fetch handler's own `env` parameter scope) is typed against `Cloudflare.Env`
+ // specifically, not the bare `Env` above -- both need the same augmentation.
+ namespace Cloudflare {
+ interface Env {
+ DISCOVERY_INDEX_SHARED_SECRET: string;
+ DISCOVERY_INDEX_GITHUB_TOKEN: string;
+ }
+ }
+}
+
+export {};
diff --git a/packages/discovery-index/src/rate-limiter.ts b/packages/discovery-index/src/rate-limiter.ts
new file mode 100644
index 0000000000..530431dc00
--- /dev/null
+++ b/packages/discovery-index/src/rate-limiter.ts
@@ -0,0 +1,78 @@
+// Blanket, IP-keyed rate limiter for the discovery-index Worker (#4250's "rate-limiting/abuse posture"
+// deliverable). Mirrors the main app's Durable-Object fixed-window-counter shape (src/auth/rate-limit.ts's
+// RateLimiter) conceptually, but is deliberately self-contained rather than a shared import: Durable Object
+// classes belong to one Worker deployment each, and this service's callers have no per-caller identity to
+// key on in the first place -- every opted-in miner authenticates with the SAME shared
+// DISCOVERY_INDEX_SHARED_SECRET (soft-claim.ts's own header comment: "the shipped client payload never
+// carries caller identity"), so a token- or installation-keyed bucket (the main app's preferred identity
+// when resolvable) isn't available here even in principle. IP-keying, the main app's own fallback for
+// exactly this "no better identity" case, is the right default rather than a fallback.
+import { DurableObject } from "cloudflare:workers";
+
+/** Requests allowed per IP per window. Generous relative to legitimate miner polling cadence (the result
+ * cache TTL is 5 minutes by default, README.md), tight enough to blunt a single misbehaving/abusive
+ * caller sharing the one bearer secret. */
+export const RATE_LIMIT = 60;
+export const RATE_LIMIT_WINDOW_SECONDS = 60;
+
+type Bucket = { count: number; resetAt: number };
+
+export type RateLimitCheckRequest = { key: string; limit: number; windowSeconds: number };
+export type RateLimitDecision = { allowed: boolean; remaining: number; retryAfterSeconds: number };
+
+/** Fixed-window counter, keyed and configured entirely by the caller's request body (limit/windowSeconds
+ * travel on the wire, same shape as the main app's RateLimiter) rather than reading the module-level
+ * RATE_LIMIT/RATE_LIMIT_WINDOW_SECONDS constants directly -- keeps this class's own logic testable with
+ * small, fast numbers independent of the real operational values enforceDiscoveryIndexRateLimit sends. */
+export class DiscoveryIndexRateLimiter extends DurableObject {
+ override async fetch(request: Request): Promise {
+ const body = (await request.json().catch(() => null)) as Partial | null;
+ if (!body?.key || !body.limit || !body.windowSeconds) return Response.json({ error: "invalid_rate_limit_request" }, { status: 400 });
+ const now = Date.now();
+ const storageKey = `bucket:${body.key}`;
+ const existing = await this.ctx.storage.get(storageKey);
+ const bucket: Bucket = existing && existing.resetAt > now ? existing : { count: 0, resetAt: now + body.windowSeconds * 1000 };
+ bucket.count += 1;
+ await this.ctx.storage.put(storageKey, bucket);
+ const allowed = bucket.count <= body.limit;
+ const decision: RateLimitDecision = {
+ allowed,
+ remaining: Math.max(body.limit - bucket.count, 0),
+ retryAfterSeconds: Math.max(1, Math.ceil((bucket.resetAt - now) / 1000)),
+ };
+ return Response.json(decision, { status: allowed ? 200 : 429 });
+ }
+}
+
+/** Cloudflare-populated client IP only -- a client-supplied proxy header is not trusted, same reasoning as
+ * the main app's clientIp() (src/auth/rate-limit.ts). */
+function clientIp(request: Request): string {
+ return request.headers.get("cf-connecting-ip")?.trim() || "unknown-ip";
+}
+
+/** Checks and increments this IP's bucket. Fails OPEN on a Durable Object error (#5000's reasoning, same as
+ * the main app's enforceRateLimit): the limiter exists to protect the service, not to become a second
+ * point of failure that turns a DO hiccup into an outage for every legitimate caller. */
+export async function enforceDiscoveryIndexRateLimit(
+ request: Request,
+ namespace: DurableObjectNamespace,
+): Promise {
+ const key = `ip:${clientIp(request)}`;
+ let decision: RateLimitDecision;
+ try {
+ const id = namespace.idFromName(key);
+ const response = await namespace.get(id).fetch("https://rate-limit/check", {
+ method: "POST",
+ body: JSON.stringify({ key, limit: RATE_LIMIT, windowSeconds: RATE_LIMIT_WINDOW_SECONDS }),
+ });
+ decision = (await response.json()) as RateLimitDecision;
+ } catch (error) {
+ console.error(JSON.stringify({ event: "discovery_index_rate_limit_check_failed", message: error instanceof Error ? error.message : String(error) }));
+ return null;
+ }
+ if (decision.allowed) return null;
+ return Response.json(
+ { error: "rate_limited", retryAfterSeconds: decision.retryAfterSeconds },
+ { status: 429, headers: { "retry-after": String(decision.retryAfterSeconds) } },
+ );
+}
diff --git a/packages/discovery-index/src/worker.ts b/packages/discovery-index/src/worker.ts
new file mode 100644
index 0000000000..eb36dc56db
--- /dev/null
+++ b/packages/discovery-index/src/worker.ts
@@ -0,0 +1,66 @@
+// Cloudflare Worker + Container entry point for the discovery-index service (#7167). Pure infra glue: it
+// adds NO new application logic and does not modify server.ts/app.ts's existing behavior -- it routes
+// incoming requests to a Container instance running the SAME, unmodified Docker image server.ts's own
+// Dockerfile already builds.
+//
+// A SINGLE fixed-name instance, not a load-balanced pool. This is a correctness requirement, not a
+// preference: the service's result cache (cache.ts) and soft-claim dedup store (soft-claim.ts) are both
+// per-process in-memory state. Cloudflare's own getRandom() helper spreads requests across an interchangeable
+// pool of container instances, each with independent memory -- two concurrent "claim" calls for the same
+// repo/issue landing on two different pool instances would each see an empty store and both succeed,
+// silently breaking soft-claim's one-claim-wins guarantee. Routing every request to one fixed instance name
+// keeps the existing in-process TtlCache/SoftClaimStore logic correct exactly as already tested, with zero
+// changes to that logic. If real load ever outgrows a single instance, the fix is moving soft-claim state
+// into the Durable Object's own transactional storage (real cross-instance consistency), not multiplying
+// instances under the current in-memory design.
+//
+// Also applies the blanket IP-keyed rate limit (rate-limiter.ts, #4250's "rate-limiting/abuse posture"
+// deliverable) at the Worker layer, in front of the Container -- an abusive caller gets rejected before it
+// ever reaches (and wakes) the container instance.
+//
+// Not unit-tested: exercised only by real Cloudflare Containers infrastructure, matching server.ts's own
+// existing exclusion (see codecov.yml / vitest.config.ts).
+import { Container } from "@cloudflare/containers";
+import { env } from "cloudflare:workers";
+import { DiscoveryIndexRateLimiter, enforceDiscoveryIndexRateLimit } from "./rate-limiter.js";
+
+export { DiscoveryIndexRateLimiter };
+
+const SINGLETON_INSTANCE_NAME = "discovery-index-singleton";
+
+export class DiscoveryIndexContainer extends Container {
+ override defaultPort = 8080;
+ // Idle timeout before the container sleeps (cost control for an opt-in, low-traffic shared service) --
+ // long enough that normal miner query cadence (cache TTL is 5 minutes, per README.md) doesn't constantly
+ // pay a cold-start penalty between requests.
+ override sleepAfter = "10m";
+ override envVars = {
+ DISCOVERY_INDEX_SHARED_SECRET: env.DISCOVERY_INDEX_SHARED_SECRET,
+ DISCOVERY_INDEX_GITHUB_TOKEN: env.DISCOVERY_INDEX_GITHUB_TOKEN,
+ };
+}
+
+interface WorkerEnv {
+ DISCOVERY_INDEX_CONTAINER: DurableObjectNamespace;
+ DISCOVERY_INDEX_RATE_LIMITER: DurableObjectNamespace;
+ DISCOVERY_INDEX_SHARED_SECRET: string;
+ DISCOVERY_INDEX_GITHUB_TOKEN: string;
+}
+
+// /health, /ready, /metrics are cheap liveness/monitoring routes a legitimate uptime checker may poll
+// frequently -- only the real, potentially-abusable work (query/soft-claim) is rate-limited, mirroring the
+// main app's own isPreAuthRateLimitPath exclusion for equivalent routes (src/auth/rate-limit.ts).
+function isRateLimited(path: string): boolean {
+ return path.startsWith("/v1/discovery-index/");
+}
+
+export default {
+ async fetch(request: Request, workerEnv: WorkerEnv): Promise {
+ if (isRateLimited(new URL(request.url).pathname)) {
+ const limited = await enforceDiscoveryIndexRateLimit(request, workerEnv.DISCOVERY_INDEX_RATE_LIMITER);
+ if (limited) return limited;
+ }
+ const container = workerEnv.DISCOVERY_INDEX_CONTAINER.getByName(SINGLETON_INSTANCE_NAME);
+ return container.fetch(request);
+ },
+};
diff --git a/packages/discovery-index/tsconfig.json b/packages/discovery-index/tsconfig.json
index 7eb007e9b9..dba156cc97 100644
--- a/packages/discovery-index/tsconfig.json
+++ b/packages/discovery-index/tsconfig.json
@@ -19,5 +19,9 @@
// mismatched buildinfo and rechecks from scratch), but real on every local `npm run test:ci`.
"tsBuildInfoFile": "./.tsbuildinfo"
},
- "include": ["src"]
+ "include": ["src"],
+ // worker.ts/rate-limiter.ts target the Cloudflare Workers runtime (Container/Durable Object globals),
+ // incompatible with this config's Node types -- they have their own tsconfig.worker.json, checked via
+ // `npm run cf:typecheck`.
+ "exclude": ["src/worker.ts", "src/rate-limiter.ts"]
}
diff --git a/packages/discovery-index/tsconfig.worker.json b/packages/discovery-index/tsconfig.worker.json
new file mode 100644
index 0000000000..0154198613
--- /dev/null
+++ b/packages/discovery-index/tsconfig.worker.json
@@ -0,0 +1,18 @@
+{
+ "extends": "../../tsconfig.json",
+ "compilerOptions": {
+ "module": "ESNext",
+ "moduleResolution": "Bundler",
+ // worker.ts runs in the Cloudflare Workers runtime only -- Node types (this package's main tsconfig.json)
+ // would incorrectly permit Node-only APIs this file can never actually call, and would conflict with
+ // Workers-only globals (DurableObjectNamespace, `cloudflare:workers`) the Node types don't know about.
+ "types": ["@cloudflare/workers-types"],
+ "declaration": false,
+ "rootDir": "src",
+ "noEmit": true,
+ // Separate buildinfo file, same reasoning as tsconfig.json's own comment on this: two tsc invocations
+ // over the same package must not share one cache file.
+ "tsBuildInfoFile": "./.worker.tsbuildinfo"
+ },
+ "include": ["src/worker.ts", "src/rate-limiter.ts", "src/env.d.ts", "worker-configuration.d.ts"]
+}
diff --git a/packages/discovery-index/worker-configuration.d.ts b/packages/discovery-index/worker-configuration.d.ts
new file mode 100644
index 0000000000..af8640063f
--- /dev/null
+++ b/packages/discovery-index/worker-configuration.d.ts
@@ -0,0 +1,14709 @@
+/* eslint-disable */
+// Generated by Wrangler by running `wrangler types` (hash: d2eeff9c442143d238b1256f3c987930)
+// Runtime types generated with workerd@1.20260714.1 2026-07-21
+interface __BaseEnv_Env {
+ DISCOVERY_INDEX_CONTAINER: DurableObjectNamespace;
+ DISCOVERY_INDEX_RATE_LIMITER: DurableObjectNamespace;
+}
+declare namespace Cloudflare {
+ interface GlobalProps {
+ mainModule: typeof import("./src/worker");
+ durableNamespaces: "DiscoveryIndexContainer" | "DiscoveryIndexRateLimiter";
+ }
+ interface Env extends __BaseEnv_Env {}
+}
+interface Env extends __BaseEnv_Env {}
+
+// Begin runtime types
+/*! *****************************************************************************
+Copyright (c) Cloudflare. All rights reserved.
+Copyright (c) Microsoft Corporation. All rights reserved.
+
+Licensed under the Apache License, Version 2.0 (the "License"); you may not use
+this file except in compliance with the License. You may obtain a copy of the
+License at http://www.apache.org/licenses/LICENSE-2.0
+THIS CODE IS PROVIDED ON AN *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION ANY IMPLIED
+WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A PARTICULAR PURPOSE,
+MERCHANTABLITY OR NON-INFRINGEMENT.
+See the Apache Version 2.0 License for specific language governing permissions
+and limitations under the License.
+***************************************************************************** */
+/* eslint-disable */
+// noinspection JSUnusedGlobalSymbols
+declare var onmessage: never;
+/**
+ * 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.
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/DOMException)
+ */
+declare class DOMException extends Error {
+ constructor(message?: string, name?: string);
+ /**
+ * The **`message`** read-only property of the a message or description associated with the given error name.
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/DOMException/message)
+ */
+ readonly message: string;
+ /**
+ * The **`name`** read-only property of the one of the strings associated with an error name.
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/DOMException/name)
+ */
+ readonly name: string;
+ /**
+ * The **`code`** read-only property of the DOMException interface returns one of the legacy error code constants, or `0` if none match.
+ * @deprecated
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/DOMException/code)
+ */
+ readonly code: number;
+ static readonly INDEX_SIZE_ERR: number;
+ static readonly DOMSTRING_SIZE_ERR: number;
+ static readonly HIERARCHY_REQUEST_ERR: number;
+ static readonly WRONG_DOCUMENT_ERR: number;
+ static readonly INVALID_CHARACTER_ERR: number;
+ static readonly NO_DATA_ALLOWED_ERR: number;
+ static readonly NO_MODIFICATION_ALLOWED_ERR: number;
+ static readonly NOT_FOUND_ERR: number;
+ static readonly NOT_SUPPORTED_ERR: number;
+ static readonly INUSE_ATTRIBUTE_ERR: number;
+ static readonly INVALID_STATE_ERR: number;
+ static readonly SYNTAX_ERR: number;
+ static readonly INVALID_MODIFICATION_ERR: number;
+ static readonly NAMESPACE_ERR: number;
+ static readonly INVALID_ACCESS_ERR: number;
+ static readonly VALIDATION_ERR: number;
+ static readonly TYPE_MISMATCH_ERR: number;
+ static readonly SECURITY_ERR: number;
+ static readonly NETWORK_ERR: number;
+ static readonly ABORT_ERR: number;
+ static readonly URL_MISMATCH_ERR: number;
+ static readonly QUOTA_EXCEEDED_ERR: number;
+ static readonly TIMEOUT_ERR: number;
+ static readonly INVALID_NODE_TYPE_ERR: number;
+ static readonly DATA_CLONE_ERR: number;
+ get stack(): any;
+ set stack(value: any);
+}
+type WorkerGlobalScopeEventMap = {
+ fetch: FetchEvent;
+ scheduled: ScheduledEvent;
+ queue: QueueEvent;
+ unhandledrejection: PromiseRejectionEvent;
+ rejectionhandled: PromiseRejectionEvent;
+};
+declare abstract class WorkerGlobalScope extends EventTarget {
+ EventTarget: typeof EventTarget;
+}
+/* The **`console`** object provides access to the debugging console (e.g., the Web console in Firefox). *
+ * The **`console`** object provides access to the debugging console (e.g., the Web console in Firefox).
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/console)
+ */
+interface Console {
+ "assert"(condition?: boolean, ...data: any[]): void;
+ /**
+ * The **`console.clear()`** static method clears the console if possible.
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/clear_static)
+ */
+ clear(): void;
+ /**
+ * The **`console.count()`** static method logs the number of times that this particular call to `count()` has been called.
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/count_static)
+ */
+ count(label?: string): void;
+ /**
+ * The **`console.countReset()`** static method resets counter used with console/count_static.
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/countReset_static)
+ */
+ countReset(label?: string): void;
+ /**
+ * The **`console.debug()`** static method outputs a message to the console at the 'debug' log level.
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/debug_static)
+ */
+ debug(...data: any[]): void;
+ /**
+ * The **`console.dir()`** static method displays a list of the properties of the specified JavaScript object.
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/dir_static)
+ */
+ dir(item?: any, options?: any): void;
+ /**
+ * The **`console.dirxml()`** static method displays an interactive tree of the descendant elements of the specified XML/HTML element.
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/dirxml_static)
+ */
+ dirxml(...data: any[]): void;
+ /**
+ * The **`console.error()`** static method outputs a message to the console at the 'error' log level.
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/error_static)
+ */
+ error(...data: any[]): void;
+ /**
+ * 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.
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/group_static)
+ */
+ group(...data: any[]): void;
+ /**
+ * The **`console.groupCollapsed()`** static method creates a new inline group in the console.
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/groupCollapsed_static)
+ */
+ groupCollapsed(...data: any[]): void;
+ /**
+ * The **`console.groupEnd()`** static method exits the current inline group in the console.
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/groupEnd_static)
+ */
+ groupEnd(): void;
+ /**
+ * The **`console.info()`** static method outputs a message to the console at the 'info' log level.
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/info_static)
+ */
+ info(...data: any[]): void;
+ /**
+ * The **`console.log()`** static method outputs a message to the console.
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/log_static)
+ */
+ log(...data: any[]): void;
+ /**
+ * The **`console.table()`** static method displays tabular data as a table.
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/table_static)
+ */
+ table(tabularData?: any, properties?: string[]): void;
+ /**
+ * The **`console.time()`** static method starts a timer you can use to track how long an operation takes.
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/time_static)
+ */
+ time(label?: string): void;
+ /**
+ * The **`console.timeEnd()`** static method stops a timer that was previously started by calling console/time_static.
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/timeEnd_static)
+ */
+ timeEnd(label?: string): void;
+ /**
+ * The **`console.timeLog()`** static method logs the current value of a timer that was previously started by calling console/time_static.
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/timeLog_static)
+ */
+ timeLog(label?: string, ...data: any[]): void;
+ timeStamp(label?: string): void;
+ /**
+ * The **`console.trace()`** static method outputs a stack trace to the console.
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/trace_static)
+ */
+ trace(...data: any[]): void;
+ /**
+ * The **`console.warn()`** static method outputs a warning message to the console at the 'warning' log level.
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/warn_static)
+ */
+ warn(...data: any[]): void;
+}
+declare const console: Console;
+type BufferSource = ArrayBufferView | ArrayBuffer;
+type TypedArray = Int8Array | Uint8Array | Uint8ClampedArray | Int16Array | Uint16Array | Int32Array | Uint32Array | Float32Array | Float64Array | BigInt64Array | BigUint64Array;
+declare namespace WebAssembly {
+ class CompileError extends Error {
+ constructor(message?: string);
+ }
+ class RuntimeError extends Error {
+ constructor(message?: string);
+ }
+ type ValueType = "anyfunc" | "externref" | "f32" | "f64" | "i32" | "i64" | "v128";
+ interface GlobalDescriptor {
+ value: ValueType;
+ mutable?: boolean;
+ }
+ class Global {
+ constructor(descriptor: GlobalDescriptor, value?: any);
+ value: any;
+ valueOf(): any;
+ }
+ type ImportValue = ExportValue | number;
+ type ModuleImports = Record;
+ type Imports = Record;
+ type ExportValue = Function | Global | Memory | Table;
+ type Exports = Record;
+ class Instance {
+ constructor(module: Module, imports?: Imports);
+ readonly exports: Exports;
+ }
+ interface MemoryDescriptor {
+ initial: number;
+ maximum?: number;
+ shared?: boolean;
+ }
+ class Memory {
+ constructor(descriptor: MemoryDescriptor);
+ readonly buffer: ArrayBuffer;
+ grow(delta: number): number;
+ }
+ type ImportExportKind = "function" | "global" | "memory" | "table";
+ interface ModuleExportDescriptor {
+ kind: ImportExportKind;
+ name: string;
+ }
+ interface ModuleImportDescriptor {
+ kind: ImportExportKind;
+ module: string;
+ name: string;
+ }
+ abstract class Module {
+ static customSections(module: Module, sectionName: string): ArrayBuffer[];
+ static exports(module: Module): ModuleExportDescriptor[];
+ static imports(module: Module): ModuleImportDescriptor[];
+ }
+ type TableKind = "anyfunc" | "externref";
+ interface TableDescriptor {
+ element: TableKind;
+ initial: number;
+ maximum?: number;
+ }
+ class Table {
+ constructor(descriptor: TableDescriptor, value?: any);
+ readonly length: number;
+ get(index: number): any;
+ grow(delta: number, value?: any): number;
+ set(index: number, value?: any): void;
+ }
+ function instantiate(module: Module, imports?: Imports): Promise;
+ function validate(bytes: BufferSource): boolean;
+}
+/**
+ * The **`ServiceWorkerGlobalScope`** interface of the Service Worker API represents the global execution context of a service worker.
+ * Available only in secure contexts.
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/ServiceWorkerGlobalScope)
+ */
+interface ServiceWorkerGlobalScope extends WorkerGlobalScope {
+ DOMException: typeof DOMException;
+ WorkerGlobalScope: typeof WorkerGlobalScope;
+ btoa(data: string): string;
+ atob(data: string): string;
+ setTimeout(callback: (...args: any[]) => void, msDelay?: number): number;
+ setTimeout(callback: (...args: Args) => void, msDelay?: number, ...args: Args): number;
+ clearTimeout(timeoutId: number | null): void;
+ setInterval(callback: (...args: any[]) => void, msDelay?: number): number;
+ setInterval(callback: (...args: Args) => void, msDelay?: number, ...args: Args): number;
+ clearInterval(timeoutId: number | null): void;
+ queueMicrotask(task: Function): void;
+ structuredClone(value: T, options?: StructuredSerializeOptions): T;
+ reportError(error: any): void;
+ fetch(input: RequestInfo | URL, init?: RequestInit): Promise;
+ self: ServiceWorkerGlobalScope;
+ crypto: Crypto;
+ caches: CacheStorage;
+ scheduler: Scheduler;
+ performance: Performance;
+ Cloudflare: Cloudflare;
+ readonly origin: string;
+ Event: typeof Event;
+ ExtendableEvent: typeof ExtendableEvent;
+ CustomEvent: typeof CustomEvent;
+ PromiseRejectionEvent: typeof PromiseRejectionEvent;
+ FetchEvent: typeof FetchEvent;
+ TailEvent: typeof TailEvent;
+ TraceEvent: typeof TailEvent;
+ ScheduledEvent: typeof ScheduledEvent;
+ MessageEvent: typeof MessageEvent;
+ CloseEvent: typeof CloseEvent;
+ ReadableStreamDefaultReader: typeof ReadableStreamDefaultReader;
+ ReadableStreamBYOBReader: typeof ReadableStreamBYOBReader;
+ ReadableStream: typeof ReadableStream;
+ WritableStream: typeof WritableStream;
+ WritableStreamDefaultWriter: typeof WritableStreamDefaultWriter;
+ TransformStream: typeof TransformStream;
+ ByteLengthQueuingStrategy: typeof ByteLengthQueuingStrategy;
+ CountQueuingStrategy: typeof CountQueuingStrategy;
+ ErrorEvent: typeof ErrorEvent;
+ MessageChannel: typeof MessageChannel;
+ MessagePort: typeof MessagePort;
+ EventSource: typeof EventSource;
+ ReadableStreamBYOBRequest: typeof ReadableStreamBYOBRequest;
+ ReadableStreamDefaultController: typeof ReadableStreamDefaultController;
+ ReadableByteStreamController: typeof ReadableByteStreamController;
+ WritableStreamDefaultController: typeof WritableStreamDefaultController;
+ TransformStreamDefaultController: typeof TransformStreamDefaultController;
+ CompressionStream: typeof CompressionStream;
+ DecompressionStream: typeof DecompressionStream;
+ TextEncoderStream: typeof TextEncoderStream;
+ TextDecoderStream: typeof TextDecoderStream;
+ Headers: typeof Headers;
+ Body: typeof Body;
+ Request: typeof Request;
+ Response: typeof Response;
+ WebSocket: typeof WebSocket;
+ WebSocketPair: typeof WebSocketPair;
+ WebSocketRequestResponsePair: typeof WebSocketRequestResponsePair;
+ AbortController: typeof AbortController;
+ AbortSignal: typeof AbortSignal;
+ TextDecoder: typeof TextDecoder;
+ TextEncoder: typeof TextEncoder;
+ navigator: Navigator;
+ Navigator: typeof Navigator;
+ URL: typeof URL;
+ URLSearchParams: typeof URLSearchParams;
+ URLPattern: typeof URLPattern;
+ Blob: typeof Blob;
+ File: typeof File;
+ FormData: typeof FormData;
+ Crypto: typeof Crypto;
+ SubtleCrypto: typeof SubtleCrypto;
+ CryptoKey: typeof CryptoKey;
+ CacheStorage: typeof CacheStorage;
+ Cache: typeof Cache;
+ FixedLengthStream: typeof FixedLengthStream;
+ IdentityTransformStream: typeof IdentityTransformStream;
+ HTMLRewriter: typeof HTMLRewriter;
+}
+declare function addEventListener(type: Type, handler: EventListenerOrEventListenerObject, options?: EventTargetAddEventListenerOptions | boolean): void;
+declare function removeEventListener(type: Type, handler: EventListenerOrEventListenerObject, options?: EventTargetEventListenerOptions | boolean): void;
+/**
+ * The **`dispatchEvent()`** method of the EventTarget sends an Event to the object, (synchronously) invoking the affected event listeners in the appropriate order.
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/EventTarget/dispatchEvent)
+ */
+declare function dispatchEvent(event: WorkerGlobalScopeEventMap[keyof WorkerGlobalScopeEventMap]): boolean;
+/* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Window/btoa) */
+declare function btoa(data: string): string;
+/* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Window/atob) */
+declare function atob(data: string): string;
+/* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Window/setTimeout) */
+declare function setTimeout(callback: (...args: any[]) => void, msDelay?: number): number;
+/* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Window/setTimeout) */
+declare function setTimeout(callback: (...args: Args) => void, msDelay?: number, ...args: Args): number;
+/* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Window/clearTimeout) */
+declare function clearTimeout(timeoutId: number | null): void;
+/* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Window/setInterval) */
+declare function setInterval(callback: (...args: any[]) => void, msDelay?: number): number;
+/* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Window/setInterval) */
+declare function setInterval(callback: (...args: Args) => void, msDelay?: number, ...args: Args): number;
+/* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Window/clearInterval) */
+declare function clearInterval(timeoutId: number | null): void;
+/* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Window/queueMicrotask) */
+declare function queueMicrotask(task: Function): void;
+/* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Window/structuredClone) */
+declare function structuredClone(value: T, options?: StructuredSerializeOptions): T;
+/* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Window/reportError) */
+declare function reportError(error: any): void;
+/* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Window/fetch) */
+declare function fetch(input: RequestInfo | URL, init?: RequestInit): Promise;
+declare const self: ServiceWorkerGlobalScope;
+/**
+* The Web Crypto API provides a set of low-level functions for common cryptographic tasks.
+* The Workers runtime implements the full surface of this API, but with some differences in
+* the [supported algorithms](https://developers.cloudflare.com/workers/runtime-apis/web-crypto/#supported-algorithms)
+* compared to those implemented in most browsers.
+*
+* [Cloudflare Docs Reference](https://developers.cloudflare.com/workers/runtime-apis/web-crypto/)
+*/
+declare const crypto: Crypto;
+/**
+* The Cache API allows fine grained control of reading and writing from the Cloudflare global network cache.
+*
+* [Cloudflare Docs Reference](https://developers.cloudflare.com/workers/runtime-apis/cache/)
+*/
+declare const caches: CacheStorage;
+declare const scheduler: Scheduler;
+/**
+* The Workers runtime supports a subset of the Performance API, used to measure timing and performance,
+* as well as timing of subrequests and other operations.
+*
+* [Cloudflare Docs Reference](https://developers.cloudflare.com/workers/runtime-apis/performance/)
+*/
+declare const performance: Performance;
+declare const Cloudflare: Cloudflare;
+declare const origin: string;
+declare const navigator: Navigator;
+interface TestController {
+}
+interface ExecutionContext {
+ waitUntil(promise: Promise): void;
+ passThroughOnException(): void;
+ readonly exports: Cloudflare.Exports;
+ readonly props: Props;
+ cache?: CacheContext;
+ readonly access?: CloudflareAccessContext;
+ tracing: Tracing;
+}
+type ExportedHandlerFetchHandler = (request: Request>, env: Env, ctx: ExecutionContext) => Response | Promise;
+type ExportedHandlerConnectHandler = (socket: Socket, env: Env, ctx: ExecutionContext) => void | Promise;
+type ExportedHandlerTailHandler = (events: TraceItem[], env: Env, ctx: ExecutionContext) => void | Promise;
+type ExportedHandlerTraceHandler = (traces: TraceItem[], env: Env, ctx: ExecutionContext) => void | Promise;
+type ExportedHandlerTailStreamHandler = (event: TailStream.TailEvent, env: Env, ctx: ExecutionContext) => TailStream.TailEventHandlerType | Promise;
+type ExportedHandlerScheduledHandler = (controller: ScheduledController, env: Env, ctx: ExecutionContext) => void | Promise;
+type ExportedHandlerQueueHandler = (batch: MessageBatch, env: Env, ctx: ExecutionContext) => void | Promise;
+type ExportedHandlerTestHandler = (controller: TestController, env: Env, ctx: ExecutionContext) => void | Promise;
+interface ExportedHandler {
+ fetch?: ExportedHandlerFetchHandler;
+ connect?: ExportedHandlerConnectHandler;
+ tail?: ExportedHandlerTailHandler;
+ trace?: ExportedHandlerTraceHandler;
+ tailStream?: ExportedHandlerTailStreamHandler;
+ scheduled?: ExportedHandlerScheduledHandler;
+ test?: ExportedHandlerTestHandler;
+ email?: EmailExportedHandler;
+ queue?: ExportedHandlerQueueHandler;
+}
+interface StructuredSerializeOptions {
+ transfer?: any[];
+}
+declare abstract class Navigator {
+ sendBeacon(url: string, body?: BodyInit): boolean;
+ readonly userAgent: string;
+ readonly hardwareConcurrency: number;
+ readonly platform: string;
+ readonly language: string;
+ readonly languages: string[];
+}
+interface AlarmInvocationInfo {
+ readonly isRetry: boolean;
+ readonly retryCount: number;
+ readonly scheduledTime: number;
+}
+interface Cloudflare {
+ readonly compatibilityFlags: Record;
+}
+interface CachePurgeError {
+ code: number;
+ message: string;
+}
+interface CachePurgeResult {
+ success: boolean;
+ errors: CachePurgeError[];
+}
+interface CachePurgeOptions {
+ tags?: string[];
+ pathPrefixes?: string[];
+ purgeEverything?: boolean;
+}
+interface CacheContext {
+ purge(options: CachePurgeOptions): Promise;
+}
+interface CloudflareAccessContext {
+ readonly aud: string;
+ getIdentity(): Promise;
+}
+declare abstract class ColoLocalActorNamespace {
+ get(actorId: string): Fetcher;
+}
+interface DurableObject {
+ fetch(request: Request): Response | Promise;
+ connect?(socket: Socket): void | Promise;
+ alarm?(alarmInfo?: AlarmInvocationInfo): void | Promise;
+ webSocketMessage?(ws: WebSocket, message: string | ArrayBuffer): void | Promise;
+ webSocketClose?(ws: WebSocket, code: number, reason: string, wasClean: boolean): void | Promise;
+ webSocketError?(ws: WebSocket, error: unknown): void | Promise;
+}
+type DurableObjectStub = Fetcher & {
+ readonly id: DurableObjectId;
+ readonly name?: string;
+};
+interface DurableObjectId {
+ toString(): string;
+ equals(other: DurableObjectId): boolean;
+ readonly name?: string;
+ readonly jurisdiction?: string;
+}
+declare abstract class DurableObjectNamespace {
+ newUniqueId(options?: DurableObjectNamespaceNewUniqueIdOptions): DurableObjectId;
+ idFromName(name: string): DurableObjectId;
+ idFromString(id: string): DurableObjectId;
+ get(id: DurableObjectId, options?: DurableObjectNamespaceGetDurableObjectOptions): DurableObjectStub;
+ getByName(name: string, options?: DurableObjectNamespaceGetDurableObjectOptions): DurableObjectStub;
+ jurisdiction(jurisdiction: DurableObjectJurisdiction): DurableObjectNamespace;
+}
+type DurableObjectJurisdiction = "eu" | "fedramp" | "fedramp-high" | "us";
+interface DurableObjectNamespaceNewUniqueIdOptions {
+ jurisdiction?: DurableObjectJurisdiction;
+}
+type DurableObjectLocationHint = "wnam" | "enam" | "sam" | "weur" | "eeur" | "apac" | "apac-ne" | "apac-se" | "oc" | "afr" | "me";
+type DurableObjectRoutingMode = "primary-only";
+interface DurableObjectNamespaceGetDurableObjectOptions {
+ locationHint?: DurableObjectLocationHint;
+ routingMode?: DurableObjectRoutingMode;
+}
+interface DurableObjectClass<_T extends Rpc.DurableObjectBranded | undefined = undefined> {
+}
+interface DurableObjectState {
+ waitUntil(promise: Promise): void;
+ readonly exports: Cloudflare.Exports;
+ readonly props: Props;
+ readonly id: DurableObjectId;
+ readonly storage: DurableObjectStorage;
+ container?: Container;
+ facets: DurableObjectFacets;
+ blockConcurrencyWhile(callback: () => Promise): Promise;
+ acceptWebSocket(ws: WebSocket, tags?: string[]): void;
+ getWebSockets(tag?: string): WebSocket[];
+ setWebSocketAutoResponse(maybeReqResp?: WebSocketRequestResponsePair): void;
+ getWebSocketAutoResponse(): WebSocketRequestResponsePair | null;
+ getWebSocketAutoResponseTimestamp(ws: WebSocket): Date | null;
+ setHibernatableWebSocketEventTimeout(timeoutMs?: number): void;
+ getHibernatableWebSocketEventTimeout(): number | null;
+ getTags(ws: WebSocket): string[];
+ abort(reason?: string): void;
+}
+interface DurableObjectTransaction {
+ get(key: string, options?: DurableObjectGetOptions): Promise;
+ get(keys: string[], options?: DurableObjectGetOptions): Promise