diff --git a/.agents/skills/hyperbug-development/SKILL.md b/.agents/skills/hyperbug-development/SKILL.md
new file mode 100644
index 0000000..31d6f96
--- /dev/null
+++ b/.agents/skills/hyperbug-development/SKILL.md
@@ -0,0 +1,35 @@
+---
+name: hyperbug-development
+description: "Implement or fix HyperBug product features, backend contracts and adapters, or frontend workflows according to the repository plan. Use for feature development and application behavior changes; use hyperbug-maintenance for dependency upgrades, migrations, releases, and operational recovery."
+---
+
+# HyperBug Development
+
+Develop the requested HyperBug capability within its accepted architecture and delivery gates. This is repository-specific guidance; it does not authorize unrelated features or deployment.
+
+## Begin with the current state
+
+Read [root guidance](../../../AGENTS.md), [master progress](../../../docs/plan/PROGRESS.md), and [session handoffs](references/session-handoffs.md). Identify the owning module and read its plan and latest progress entry before editing. Source policies and their refinements are indexed in [SOURCES](../../../docs/plan/SOURCES.md).
+
+G0 must pass before application work. Complete the backend acceptance gate in module 10 before starting the product SPA; each post-MVP feature also accepts its backend before its UI. Backend-owned authorization pages and minimal protocol test fixtures are distinct from product SPA work.
+
+Use the selected checklist IDs to bound the change. Inspect existing package scripts and implementation rather than treating planned paths, command names, or source version snapshots as installed facts.
+
+## Read only the relevant references
+
+| Work | Reference |
+| --- | --- |
+| Endpoints, domain/application services, persistence adapters, queues, or plugins | [Backend development](references/backend.md) |
+| Static SPA, browser auth, forms, components, or backend-owned HTML | [Frontend development](references/frontend.md) |
+| Authentication, permissions, credentials, content, uploads, trust boundaries, or review/ADR decisions | [Security and review boundaries](references/security.md) |
+| Selecting verification or deciding acceptance | [Testing and evidence](references/testing.md) |
+| Any session start, checkpoint, or finish | [Session handoffs](references/session-handoffs.md) |
+| A feature needs a schema/data migration | [Database changes](../hyperbug-maintenance/references/database-migrations.md) |
+
+## Implement and close the batch
+
+Keep domain/public contracts independent of runtime infrastructure. Share product, authorization, and consistency semantics across both production profiles; allow optimized adapter implementations. Backend writes need server validation, object permissions, required audit/timeline events, and bounded work from their first implementation.
+
+Fetch current library documentation and relevant web guidance using the rules in root guidance. Record unresolved assumptions instead of silently weakening a baseline or changing the chosen stack.
+
+Verify the changed behavior and affected contracts using the testing reference. Update relevant specifications and guidance in English. Complete only evidence-backed checklist items, then record progress, changed files, verification, next actions, and next-session cautions in every affected progress record. Follow the session reference even when the batch only investigated a failure.
diff --git a/.agents/skills/hyperbug-development/agents/openai.yaml b/.agents/skills/hyperbug-development/agents/openai.yaml
new file mode 100644
index 0000000..fa9b9e9
--- /dev/null
+++ b/.agents/skills/hyperbug-development/agents/openai.yaml
@@ -0,0 +1,4 @@
+interface:
+ display_name: "HyperBug Development"
+ short_description: "Develop HyperBug within its architecture and plan"
+ default_prompt: "Use $hyperbug-development to implement the next eligible HyperBug checklist item and record the session handoff."
diff --git a/.agents/skills/hyperbug-development/references/backend.md b/.agents/skills/hyperbug-development/references/backend.md
new file mode 100644
index 0000000..9fdf0e3
--- /dev/null
+++ b/.agents/skills/hyperbug-development/references/backend.md
@@ -0,0 +1,32 @@
+# Backend development
+
+Read for backend features, API contracts, repositories, queues, and plugins. Use [SOURCES](../../../../docs/plan/SOURCES.md) to locate TECH-STACK §§3–30, 38–54, the relevant PRODUCT scope, and the owning [module](../../../../docs/plan/README.md#module-index).
+
+## Boundaries that determine implementation
+
+- Domain entities and public schemas have no infrastructure dependencies. Application services use repository/security/blob/queue/workflow ports. Adapters implement the ports; runtime composition roots supply them.
+- Share Elysia routes and middleware in the server package. Runtime startup belongs in the Workers/Node entry points; D1 bindings, PostgreSQL pools, R2 buckets, S3 clients, and workflow objects must not leak into the domain.
+- Publish versioned REST/OpenAPI and framework-independent DTOs. Clients do not import server implementation types or require Elysia to consume the contract.
+- Keep Cloudflare and self-host behavior equivalent while allowing separate SQL, FTS, indexes, transactions, and infrastructure integrations. Resolve package/API details through current documentation; do not assume Node tooling capabilities exist in Workers.
+- Preserve User/Staff separation, project-local Issue numbers, simple Open/Closed state, and optional provider integrations. Core is usable without an email, SSO, or CAPTCHA plugin.
+
+## Complete a backend capability
+
+Choose the owning checklist and define the request/response/error contract, object permissions, invariant, transaction intent, event/audit requirements, and resource bounds before exposing the route.
+
+Implement domain/application behavior, each required repository adapter, route validation/error mapping, and contract evidence as one coherent feature. Where a mutation needs timeline/audit/outbox records, prove the intended atomic outcome. A zero-row conditional update is not automatically an exception or rollback.
+
+Use [database changes](../../hyperbug-maintenance/references/database-migrations.md) for schema work and [security](security.md) for credentials, permissions, content, or trust boundaries.
+
+## Query and side-effect decisions
+
+Use bounded keyset/cursor pagination with stable unique tie-breakers and indexes matched to filter/sort/join patterns. Batch hydration rather than querying relations per result. Keep the structured search AST independent of FTS5 and PostgreSQL syntax and enforce current authorization even with stale indexes.
+
+Record non-critical effects through the outbox/queue path. Assume at-least-once execution; bound fan-out/retries/concurrency and retain recoverable failures. Workflow steps need bounded checkpoints and replay-safe writes. See modules 08–09 for index dispatch and workflow handoffs.
+
+Use upload intents, direct object-store upload, and verified finalization; do not buffer ordinary uploads through the API. Check object identity/immutability as well as declared metadata. Native plugins are trusted application code; declared permissions are not a sandbox.
+
+## Acceptance
+
+Use [testing](testing.md) to select both-runtime contract, authorization, concurrency, migration, and resource-bound checks. Record real versus emulated results and unresolved profile differences. Update public specifications/client contracts where affected and finish the [session handoff](session-handoffs.md).
+
diff --git a/.agents/skills/hyperbug-development/references/frontend.md b/.agents/skills/hyperbug-development/references/frontend.md
new file mode 100644
index 0000000..6b8b937
--- /dev/null
+++ b/.agents/skills/hyperbug-development/references/frontend.md
@@ -0,0 +1,36 @@
+# Frontend development
+
+Read for product SPA work and backend-owned HTML/auth fixtures. First inspect G1 in [master progress](../../../../docs/plan/PROGRESS.md). Before G1 passes, work on the relevant backend contract/test fixture; do not scaffold the product SPA. A post-MVP feature needs its own backend acceptance before UI work.
+
+## Research and compatibility
+
+Follow root Context7 rules for the selected React/TanStack/Vite/UI APIs. Use the available modern-web-guidance skill before relevant HTML/CSS/client-side work: action-oriented search, relevant guide retrieval, then implementation/fallback review. Use its current instructions rather than hard-coding an old skill-version command here.
+
+Record guide IDs, lookup date, feature status, and fallback in the owning progress entry or linked design record. If guidance is unavailable, record the gap; do not claim compliance based on a remembered guide.
+
+The project baseline is Baseline Widely Available and WCAG 2.2 AA. Newly Available features require detection and usable fallback; Limited Availability cannot become a core dependency without the specified decision/review. Avoid blanket polyfills. Browser support does not replace keyboard, screen-reader, touch, zoom, contrast, and reduced-motion checks.
+
+## Static application and state
+
+The build must run on generic static hosting and Cloudflare Pages without SSR, Pages Functions, or a mandatory proxy. Runtime configuration contains public values only and can change the API origin without recompilation. Align deployment CSP with configured API/auth/media origins.
+
+Use Router for URL/navigation state, Query for remote state, Form for form state, and React for local UI. Preserve shareable filters/sort/search, back/forward behavior, request cancellation, and bounded cache/page accumulation. Invalidate or partition user-specific state on identity changes.
+
+Browser access tokens remain in memory; no localStorage, sessionStorage, IndexedDB, or persistent refresh token. Use top-level PKCE recovery and business API requests with omitted credentials. Short-lived one-shot redirect state/verifiers are distinct from access tokens and follow module 11's policy. Cookie-backed authorization pages retain their own CSRF defenses.
+
+## Forms, content, and interaction
+
+Use semantic controls, visible labels, associated help/errors, server-error mapping, and deliberate focus management. Coordinate visual/ARIA validation after interaction, handle IME composition, and keep drafts recoverable without leaking them across principals. Browser validation does not replace backend validation.
+
+Render Markdown through the shared versioned sanitizer after raw HTML parsing; never introduce a separate feature allowlist. Upload directly through the accepted intent/finalize protocol and show safe retry/expiry/quota outcomes.
+
+Native dialogs or established accessible primitives must provide focus placement/restoration, Escape where appropriate, and explicit close/cancel. Additional platform dismissal gestures are progressive enhancements.
+
+Profile long timelines before optimizing. If applying content-visibility to confirmed offscreen content, reserve intrinsic size, verify sequential keyboard/find-in-page behavior, and retain ordinary rendering without support. Keep optional provider/admin resources out of the initial bundle when practical.
+
+## Adapt guidance to HyperBug
+
+Do not copy guide examples that add SSR fallback, cookie-based business authentication, inline script handlers, relaxed CSP, or a second sanitizer. Reviewed provider SDK origins differ from arbitrary remote plugin JavaScript. Native plugin UI is build-time composition.
+
+Sources: ARCHITECTURE §§7–21, SECURITY §§7–20, 40–61, and PERFORMANCE §§25–34 via [SOURCES](../../../../docs/plan/SOURCES.md). Apply [testing](testing.md) and [session handoffs](session-handoffs.md) to close the work.
+
diff --git a/.agents/skills/hyperbug-development/references/security.md b/.agents/skills/hyperbug-development/references/security.md
new file mode 100644
index 0000000..40e4baa
--- /dev/null
+++ b/.agents/skills/hyperbug-development/references/security.md
@@ -0,0 +1,29 @@
+# Security and review boundaries
+
+Read when work changes security behavior or when deciding review/ADR scope. Authoritative requirements are in SECURITY and PERFORMANCE through [SOURCES](../../../../docs/plan/SOURCES.md); this reference routes decisions rather than replacing those baselines.
+
+## Core policy checks
+
+| Change surface | Invariants and source sections |
+| --- | --- |
+| Identity/auth | User and Staff are distinct; email matching grants no Staff privilege. PKCE S256, exact redirects, one-use codes, short-lived opaque tokens, immediate revocation, and first-party authorization sessions. SECURITY §§7–44. |
+| Authorization | Check object/project permissions on the server, including searches and delayed jobs. Fail closed on verification errors; require assurance/recent authentication where specified. SECURITY §§35–39, 141, 158–159. |
+| Credentials/keys | Hash non-recoverable credentials; use mature crypto and AES-256-GCM envelope encryption for recoverable secrets, contextual AAD, unique nonces, key IDs/rotation. Do not lower memory-hard password parameters to fit a runtime. SECURITY §§28–34, 67–91, 148–150. |
+| Content/uploads | Final tree sanitization after raw HTML parse; centralized versioned policy. Reauthorize uploads/finalization, verify actual objects, isolate dangerous media, and prevent post-validation overwrite. SECURITY §§53–66, 99–105. |
+| Cross-origin/cache | Explicit origins; no ambient cookies for business APIs; CSRF protection for cookie flows. Never reuse private responses through shared caches or weaken CSP for an ordinary plugin. SECURITY §§40–52, 132–136. |
+| External/plugin | Central SSRF/redirect/size/timeout limits; scoped secret/capability access. Native code is trusted, not sandboxed; plugins cannot disable Core policy. SECURITY §§92–98, 106–123. |
+| Audit/retention | Redact logs/errors; required security actions get append-only audit records. Bound and document retention without silently deleting audit history. SECURITY §§110–116, 139–149. |
+
+CORS is not authentication, CAPTCHA is not authorization, and a cache hit is not a permission check. Provider failure cannot mean successful security verification. Do not claim uploads were scanned without a scanner or full PQ authentication from hosting alone.
+
+## Review and decision routing
+
+- Apply focused security review to authentication/authorization, crypto/password/token storage, sanitization, CSP/CORS, uploads, plugin runtime, outbound fetch, SSO/CAPTCHA, and KMS changes (SECURITY §154). Ordinary feature work does not automatically require a separate exhaustive repository audit.
+- Write a security ADR for a new auth flow, persistent browser credential, cipher/password algorithm/key provider, plugin execution model, weaker CSP, raw HTML capability, cross-origin trust, or public-key signature algorithm (SECURITY §155).
+- Deviations from SECURITY MUST/MUST NOT require both an ADR and security review (§2). Resolve uncertainty explicitly before embedding a weaker behavior.
+- Review hot queries/indexes, list/search/pagination, bulk/cache/fan-out, plugin hooks, uploads/import/export, auth cost, and large dependencies for performance (PERFORMANCE §75).
+- Record an ADR for pagination/cache/search/replication/denormalization architecture, sync-to-async shifts, coordination objects, changed queue delivery semantics, or a large persistent frontend dependency (PERFORMANCE §76); core framework/database/service/browser-baseline changes also follow ARCHITECTURE §49.
+- Record review evidence and rationale in the owning module's progress entry or linked decision file. A review requirement is not automatically a new user-approval requirement for routine authorized implementation.
+
+Use [testing](testing.md) for adversarial acceptance evidence and the maintenance [deployment reference](../../hyperbug-maintenance/references/deployment.md) for operational key/incident work.
+
diff --git a/.agents/skills/hyperbug-development/references/session-handoffs.md b/.agents/skills/hyperbug-development/references/session-handoffs.md
new file mode 100644
index 0000000..ed2f050
--- /dev/null
+++ b/.agents/skills/hyperbug-development/references/session-handoffs.md
@@ -0,0 +1,27 @@
+# Session handoffs
+
+This is the shared session reference for both project skills. The canonical checklist, status definitions, and entry template are in [EXECUTION](../../../../docs/plan/EXECUTION.md); read it instead of creating a competing log format.
+
+## Start
+
+Read [master progress](../../../../docs/plan/PROGRESS.md), the owning [module plan](../../../../docs/plan/README.md#module-index), its same-named file under `docs/plan/progress/`, and affected prerequisite records. Inspect actual files/versions/uncommitted changes. Record the intended stable checklist IDs and verify gates before dependent work.
+
+For maintenance, use the module that owns the changed artifact: foundation/dependencies 01, persistence 02, security 03–04, content 07, async 09, deployment/recovery 13, or the relevant feature/integration module. A multi-module change updates every affected record. Use [COVERAGE](../../../../docs/plan/COVERAGE.md) for ownership; do not execute unrelated future modules just to obtain a log owner.
+
+## Checkpoint
+
+Update the current status and append or update the current session entry after a meaningful batch, a decision/failure/blocker, or before handing off context. Record partial completion honestly and leave unfinished checkboxes unchecked. Preserve earlier session history.
+
+## Finish every session
+
+Append a concise English entry with the [required template](../../../../docs/plan/EXECUTION.md#required-session-entry-template):
+
+- Scope/checklist IDs, actual progress, and a summary of what changed.
+- Changed paths/artifacts and commands/checks with environments, results, and evidence; state explicitly when a check was not run.
+- Decisions/deviations and blockers with concrete resolution steps.
+- Ordered next actions and next-session cautions: unfinished migrations, version uncertainty, uncommitted work, missing environment access, or invariants that must not be lost.
+
+This applies even to planning, investigation, review, failed attempts, and no-code work. A session that only answers a user question and changes no repository state is exempt unless the user asks for a record. Mark plan items complete only when their stated outcome and verification are complete. Update master/gate status when it changes; an initialized log or proposed procedure is not implementation evidence.
+
+Never put secrets, private payloads, live credentials, or signed capabilities into handoffs. Link long reports instead of copying them. Keep records usable without chat history; do not claim commits, tests, deployments, or compatibility that did not happen.
+
diff --git a/.agents/skills/hyperbug-development/references/testing.md b/.agents/skills/hyperbug-development/references/testing.md
new file mode 100644
index 0000000..b5fbc6b
--- /dev/null
+++ b/.agents/skills/hyperbug-development/references/testing.md
@@ -0,0 +1,29 @@
+# Testing and acceptance evidence
+
+Read when choosing checks or deciding whether a checklist/gate is complete. Module-specific acceptance cases remain authoritative; use [COVERAGE](../../../../docs/plan/COVERAGE.md) to find integration owners.
+
+## Choose checks from the changed behavior
+
+| Change | Meaningful evidence |
+| --- | --- |
+| Domain rule/API | Unit and HTTP/contract cases for valid, invalid, forbidden, missing, and conflicting operations; public DTO/OpenAPI compatibility. |
+| Persistence | Shared repository suite on D1/workerd and PostgreSQL, fresh/upgrade migrations, rollback/conditional-write behavior, concurrent operations, and stable pagination. |
+| Credentials/trust/content | Relevant security regressions: permission loss/BOLA, PKCE/state/replay, expiry/revocation, key rotation/tamper, CORS/CSRF, sanitizer corpus, upload/SSRF boundaries. |
+| Queue/workflow/plugin | Duplicate/out-of-order/invalid messages, crash after a side effect, retry/DLQ/lease recovery, timeouts, enable/disable/version mismatch, and bounded fan-out. |
+| Blob adapter | Declared PUT/GET/HEAD/DELETE/presign/multipart/abort/metadata/streaming behavior on the actual claimed provider/version. |
+| Browser flow | E2E against both profiles, cross-site auth with third-party cookies blocked, keyboard/focus/screen-reader/touch/zoom checks, cache isolation, CSP and relevant failure states. |
+| Performance | Representative fixtures with auth/security active; query plans/counts/rows, latency/error rate/CPU/memory where measurable, queue lag, and bundle/CWV regression. |
+| Guidance/docs only | Metadata, relative links, reachable references, actual workflow walkthroughs, and checklist/status consistency. Application tests are unnecessary when behavior did not change. |
+
+Run existing relevant checks first. Inspect actual package scripts/tool configuration before invoking a command; do not fabricate a planned script. Add tests for new invariants and meaningful failure modes, not assertions that merely restate implementation.
+
+## Report what the evidence proves
+
+Record the command/check, software versions, fixture, runtime/provider, result, and evidence location. Separate passed, failed, not run, mock, emulator, real local service, and staging results. A mock proves neither live S3 compatibility nor Workers behavior. Source research proves neither installation nor runtime support.
+
+Do not substitute a narrow test for a gate requiring both full deployment profiles. Module 10 opens G1 only after its defined backend matrix; module 13 owns MVP deployment/recovery acceptance. If required access is unavailable, keep the affected gate incomplete and name the missing check.
+
+Benchmark realistic data with bounded authorized loads. Establish regression thresholds from measured baselines; do not invent SLA promises or hide regressions by raising thresholds indefinitely. Security parameters, authorization, and accessibility stay active.
+
+Use [session handoffs](session-handoffs.md) for results, unresolved failures, and next steps.
+
diff --git a/.agents/skills/hyperbug-maintenance/SKILL.md b/.agents/skills/hyperbug-maintenance/SKILL.md
new file mode 100644
index 0000000..78457b1
--- /dev/null
+++ b/.agents/skills/hyperbug-maintenance/SKILL.md
@@ -0,0 +1,36 @@
+---
+name: hyperbug-maintenance
+description: "Maintain HyperBug dependencies, database migrations, deployment and release procedures, secrets, backups, and incident recovery. Use for repository upkeep and operational changes; use hyperbug-development for new product behavior or feature implementation."
+---
+
+# HyperBug Maintenance
+
+Make the requested maintenance change incrementally, preserving the documented product behavior and both supported deployment profiles. Do not treat this skill as permission to publish, delete production data, rotate live credentials, or contact external parties.
+
+## Establish the change boundary
+
+Read [root guidance](../../../AGENTS.md), [master progress](../../../docs/plan/PROGRESS.md), the owning module's plan/progress, and [session handoffs](../hyperbug-development/references/session-handoffs.md). Inspect the actual versions, scripts, deployment state, and uncommitted work before choosing commands.
+
+Define the affected artifact, behavior/data that must survive, verification environment, and recovery path. Keep a dependency update or operational repair separate from unrelated feature expansion. If the task crosses into product behavior, also use [development guidance](../hyperbug-development/SKILL.md).
+
+## Select the maintenance procedure
+
+| Work | Read |
+| --- | --- |
+| Package/toolchain upgrades or compatibility drift | [Dependency updates](references/dependencies.md) |
+| Schema changes, migration tooling, backfills, or data repair | [Database changes](references/database-migrations.md) |
+| Deployments, releases, rollback, backup/restore, key rotation, or incident recovery | [Deployment and recovery](references/deployment.md) |
+| Sensitive changes or deciding whether review/ADR is required | [Shared security and review boundaries](../hyperbug-development/references/security.md) |
+| Verification and support claims | [Shared testing and evidence](../hyperbug-development/references/testing.md) |
+
+The shared references are intentionally owned by the adjacent development skill; keep the two repository skills together. Authoritative policy remains in [SOURCES](../../../docs/plan/SOURCES.md) and its linked baselines.
+
+## Execute incrementally
+
+1. Record the selected checklist IDs and reproduce the problem or establish the current compatibility/recovery baseline.
+2. Prepare the smallest coherent update, including affected migrations/configuration/docs. Verify exact dependency resolutions and current provider/CLI behavior through root documentation rules.
+3. Exercise the relevant runtime, migration, security, performance, and recovery checks before any authorized rollout. Distinguish local fixtures from live compatibility evidence.
+4. For retrying mutations, inspect current external state after an ambiguous result before retrying. Stop repeating a failing rollout at its documented failure threshold; recover or leave a concrete blocker instead of broadening privileges or deleting data to force success.
+5. Refresh actual commands, supported-version records, and operator guidance. Log the outcome and next-session cautions in each affected progress record, and change master/gate status only when supported by evidence.
+
+Prepare a concrete reviewable artifact before asking for any genuinely missing authorization. Routine reversible work already within scope does not require an extra approval ritual. Never report a prepared release as published or a proposed recovery procedure as exercised.
diff --git a/.agents/skills/hyperbug-maintenance/agents/openai.yaml b/.agents/skills/hyperbug-maintenance/agents/openai.yaml
new file mode 100644
index 0000000..e6a1979
--- /dev/null
+++ b/.agents/skills/hyperbug-maintenance/agents/openai.yaml
@@ -0,0 +1,4 @@
+interface:
+ display_name: "HyperBug Maintenance"
+ short_description: "Maintain HyperBug dependencies, data, and releases"
+ default_prompt: "Use $hyperbug-maintenance to perform the requested HyperBug maintenance change, verify its impact, and update progress."
diff --git a/.agents/skills/hyperbug-maintenance/references/database-migrations.md b/.agents/skills/hyperbug-maintenance/references/database-migrations.md
new file mode 100644
index 0000000..5aa35cf
--- /dev/null
+++ b/.agents/skills/hyperbug-maintenance/references/database-migrations.md
@@ -0,0 +1,30 @@
+# Database migrations and repair
+
+Read for schema, index, repository, backfill, migration-tool, or data-repair changes. Locate TECH-STACK §§9–13, 48, 54 and PERFORMANCE §§7–19, 54–57 through [SOURCES](../../../../docs/plan/SOURCES.md). Read the actual DATA-MODEL when it exists; it is a planned artifact until module 02 creates it.
+
+## Define the data invariant first
+
+State the business invariant, old/new representation, compatibility window, data/retention impact, and transaction intent. Public IDs, project Issue numbers, actor identity, timestamps, uniqueness, and event/audit/outbox semantics must remain explicit.
+
+Keep separate D1/SQLite and PostgreSQL schemas/migration histories. Share repository behavior rather than SQL text or a presumed universal Drizzle transaction API. Check current driver/runtime documentation before choosing transaction operations.
+
+D1 batches and PostgreSQL interactive transactions have different capabilities. Prove atomic business outcomes, including conditional zero-row/conflict cases, instead of assuming every failed precondition throws and rolls back. Do not perform read-check-write integrity enforcement without addressing concurrency.
+
+## Prepare and inspect
+
+Generate using installed reviewed tooling, then inspect actual SQL: destructive changes, renames, defaults/nullability, foreign keys, uniqueness, indexes/FTS, conversion/backfill cost, and table locking/rebuild behavior.
+
+For deployed data, choose an explicit expand/backfill/contract or justified alternative, with bounded resumable batches, checkpoints, concurrency control, and old/new application compatibility. Estimate work from representative data. Do not rewrite migrations already applied to shared environments.
+
+Keep migration credentials scoped and separate from normal application capabilities where practical. External plugins do not gain arbitrary Core SQL; trusted plugin migrations need namespaces and review.
+
+## Verify before an authorized rollout
+
+Run empty-database and prior-schema upgrade cases on both adapters, shared repository contracts, concurrency/idempotency/cursor checks, and relevant query plans/counts/rows. Restore a representative backup when validating an operational migration/recovery claim.
+
+Define failure detection, stop/retry conditions, and recovery before mutation. Application rollback does not undo an irreversible schema change. Document a safe forward fix or coordinated restore when reversal would lose data; retain the key versions required to read restored encrypted records.
+
+For production repair, identify the exact environment and selected records, prepare a dry-run/result preview where possible, and use only current session authorization. Do not repeat ambiguous mutations without inspecting actual state.
+
+Update model/API/operator documentation and the owning module progress records with migration IDs, tested starting/ending versions, evidence, remaining backfill work, and next-session cautions.
+
diff --git a/.agents/skills/hyperbug-maintenance/references/dependencies.md b/.agents/skills/hyperbug-maintenance/references/dependencies.md
new file mode 100644
index 0000000..28d6a1a
--- /dev/null
+++ b/.agents/skills/hyperbug-maintenance/references/dependencies.md
@@ -0,0 +1,26 @@
+# Dependency updates
+
+Read for toolchain, framework, ORM, driver, build/test, or provider SDK updates. Source policy: TECH-STACK §§2, 8, 12, 31–40, 52–53 and SECURITY §§124–128 through [SOURCES](../../../../docs/plan/SOURCES.md).
+
+## Establish the actual update
+
+Inspect manifests, the lockfile, package scripts, runtime configuration, and dependency callers. If the workspace has not been scaffolded, module 01 version verification is the next action; do not fabricate an upgrade from a version that was never installed.
+
+Fetch current documentation through root Context7 rules, including migration notes and exact package/export names. Verify package availability in official sources/registries. Record old/new exact resolutions, dependency purpose, compatibility constraints, and the update rationale.
+
+Preserve declared major policies: Node 24, PostgreSQL 18.x, TypeScript 7, pnpm 11, and React 19; Vite/Vitest follow reviewed latest stable updates. These are project choices, not assertions that dated source patch versions remain current. Unavailable or incompatible required versions need an explicit blocker/decision rather than a silent downgrade.
+
+## Review risk and apply a coherent change
+
+Consider maintenance/security history, transitive dependencies, license, install scripts, ESM/type quality, runtime compatibility, native/WASM code, and browser bundle impact.
+
+Prereleases are allowed when justified: tooling/framework/ORM risk differs from drivers, auth, sanitizers, crypto, and persistent storage. Follow the source policy; do not interpret allowed as preferred. Keep crypto mature/audited and sanitizer stable by default. Review ORM/migration-tool upgrades together and inspect generated migration changes.
+
+Use the repository's package manager and a single lockfile. Record exact resolutions; CI/release installation is frozen, not a floating latest/rc lookup. Retain the approved lifecycle-script controls. Do not bundle unrelated major upgrades into an incident fix.
+
+## Validate and record
+
+Use the actual targeted commands from the workspace: type/build/test/contract/runtime checks plus the affected [test matrix](../../hyperbug-development/references/testing.md). Elysia changes need routing/validation/OpenAPI/encapsulation and Workers/Node verification; database changes also use [migration guidance](database-migrations.md).
+
+Inspect API/schema drift, bundle/query/performance changes, and review-triggered decisions. Keep an experimental next-version lane distinct from required production checks. Document the accepted resolution and recovery path, update commands/runbooks when needed, and leave a [session handoff](../../hyperbug-development/references/session-handoffs.md).
+
diff --git a/.agents/skills/hyperbug-maintenance/references/deployment.md b/.agents/skills/hyperbug-maintenance/references/deployment.md
new file mode 100644
index 0000000..3e1be41
--- /dev/null
+++ b/.agents/skills/hyperbug-maintenance/references/deployment.md
@@ -0,0 +1,36 @@
+# Deployment, release, and recovery
+
+Read for packaging, rollout, rollback, backup/restore, secret rotation, and incidents. Module [13](../../../../docs/plan/modules/13-mvp-release-and-operations.md) owns release acceptance. Existing authorization determines whether this task prepares artifacts, uses staging, or changes production.
+
+## Prepare the concrete operation
+
+Identify environment, current/target version, artifacts, migrations, configuration, credentials source, expected checks, and stop/recovery criteria. Inspect live state when the task concerns a running deployment. Refresh provider/CLI documentation through root rules; do not turn a planned runbook command into an assumed installed tool.
+
+Support both official profiles: Cloudflare-native and Node/PostgreSQL/S3 with Graphile Worker. Frontend and backend releases are independent. Keep runtime config public, static SPA fallbacks/cache rules valid, and CSP/CORS aligned with exact frontend/API/auth/media origins. Preview defaults to staging rather than wildcard access to production.
+
+Only list a provider/version as tested when its compatibility suite has actually run. Workers + PostgreSQL/Hyperdrive and generic S3-compatible branding do not establish support by themselves.
+
+## Release and rollback
+
+Build from reviewed exact dependencies/frozen lockfile; retain artifact hashes and reproducible metadata, with provenance/signing where supported. Run relevant [acceptance checks](../../hyperbug-development/references/testing.md), migration/upgrade evidence, configuration validation, and secret/license/dependency checks.
+
+Sequence database compatibility, backend/worker deployment, then independently configured frontend as the specific release requires. Define smoke checks and thresholds before rollout. On failure, stop the rollout and apply its recovery path; inspect ambiguous deployment state before retrying.
+
+Roll back application artifacts only when compatible with the current schema. Use the [migration procedure](database-migrations.md) for forward fixes or coordinated restores. Do not claim publication from a prepared artifact, nor successful recovery from an unexercised runbook.
+
+## Backup and restore
+
+Back up database records, referenced blobs, non-secret configuration, and access to required encryption-key versions through appropriate secret management. Define a consistency point and retention policy; do not expose keys in ordinary backups/logs.
+
+Rehearse restoration in an isolated authorized environment. Verify schema/application versions, counts and relationships, attachment readability, encrypted data, outbox/jobs, and search rebuild. Avoid replaying notifications/webhooks to real recipients during a restore drill. Record measured recovery duration/data loss rather than promising unmeasured recovery objectives.
+
+## Secrets and incidents
+
+For planned rotation, inventory key consumers and stored key IDs, introduce the new version, retain current/previous overlap as policy permits, migrate or rewrap retained data, and verify reads/writes before retirement. Include HMAC/token/blind-index/signing keys where affected. Never retire a key still needed by retained data or recoverable backups.
+
+For confirmed compromise, follow explicit emergency revocation and containment; planned overlap must not keep a compromised credential trusted. Record affected scope and recovery evidence without logging secrets. Preserve relevant diagnostic/audit evidence under the retention policy.
+
+During an outage, distinguish critical verification failures from non-critical provider effects. Keep authentication/authorization fail closed; recover queued effects with bounded retries, idempotency, and audited replay. Inspect DLQ growth, queue lag, expired leases, search drift, and attachment cleanup as relevant.
+
+Update the maintenance reference/runbook only with verified commands and lessons, then complete the shared [session handoff](../../hyperbug-development/references/session-handoffs.md). Prepare reviewable output before seeking any actually missing authorization; a skill does not grant production access or permission to message others.
+
diff --git a/.claude/skills b/.claude/skills
new file mode 120000
index 0000000..2b7a412
--- /dev/null
+++ b/.claude/skills
@@ -0,0 +1 @@
+../.agents/skills
\ No newline at end of file
diff --git a/.env.example b/.env.example
new file mode 100644
index 0000000..78336fc
--- /dev/null
+++ b/.env.example
@@ -0,0 +1,6 @@
+HYPERBUG_ENV=local
+ALLOWED_ORIGINS=http://localhost:5173
+MAX_BODY_BYTES=65536
+REQUEST_TIMEOUT_MS=10000
+HOST=127.0.0.1
+PORT=3000
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
new file mode 100644
index 0000000..77bcd9e
--- /dev/null
+++ b/.github/dependabot.yml
@@ -0,0 +1,16 @@
+version: 2
+updates:
+ - package-ecosystem: npm
+ directory: /
+ schedule:
+ interval: weekly
+ open-pull-requests-limit: 5
+ ignore:
+ - dependency-name: pnpm
+ update-types: [version-update:semver-major]
+ - dependency-name: typescript
+ update-types: [version-update:semver-major]
+ - package-ecosystem: github-actions
+ directory: /
+ schedule:
+ interval: weekly
diff --git a/.github/workflows/backend.yml b/.github/workflows/backend.yml
new file mode 100644
index 0000000..bb9e71f
--- /dev/null
+++ b/.github/workflows/backend.yml
@@ -0,0 +1,70 @@
+name: Backend Tests
+on:
+ push:
+ pull_request:
+permissions:
+ contents: read
+jobs:
+ quality:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v7
+ - uses: actions/setup-node@v7
+ with:
+ node-version: 24
+ - run: corepack enable
+ - run: pnpm install --frozen-lockfile
+ - run: pnpm db:check
+ - run: pnpm typecheck
+ - run: pnpm lint
+ - run: pnpm format:check
+ - run: pnpm test:unit
+ - run: pnpm test:contract
+ - run: pnpm scan:secrets
+ - run: pnpm scan:licenses
+ - run: pnpm audit --audit-level high
+ - run: pnpm build
+ runtime:
+ strategy:
+ fail-fast: false
+ matrix:
+ profile: [node, workerd]
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v7
+ - uses: actions/setup-node@v7
+ with:
+ node-version: 24
+ - run: corepack enable
+ - run: pnpm install --frozen-lockfile
+ - run: pnpm test:${{ matrix.profile }}
+
+ postgres:
+ runs-on: ubuntu-latest
+ services:
+ database:
+ image: postgres:18.6
+ env:
+ POSTGRES_HOST_AUTH_METHOD: trust
+ POSTGRES_DB: hyperbug_test
+ ports:
+ - 5432:5432
+ options: >-
+ --health-cmd "pg_isready -U postgres -d hyperbug_test"
+ --health-interval 5s
+ --health-timeout 5s
+ --health-retries 10
+ env:
+ PGHOST: 127.0.0.1
+ PGPORT: 5432
+ PGUSER: postgres
+ PGDATABASE: hyperbug_test
+ HYPERBUG_TEST_POSTGRES: '1'
+ steps:
+ - uses: actions/checkout@v7
+ - uses: actions/setup-node@v7
+ with:
+ node-version: 24
+ - run: corepack enable
+ - run: pnpm install --frozen-lockfile
+ - run: pnpm exec vitest run --project postgres
diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml
new file mode 100644
index 0000000..83f7995
--- /dev/null
+++ b/.github/workflows/codeql.yml
@@ -0,0 +1,109 @@
+# For most projects, this workflow file will not need changing; you simply need
+# to commit it to your repository.
+#
+# You may wish to alter this file to override the set of languages analyzed,
+# or to provide custom queries or build logic.
+#
+# ******** NOTE ********
+# We have attempted to detect the languages in your repository. Please check
+# the `language` matrix defined below to confirm you have the correct set of
+# supported CodeQL languages.
+#
+name: 'CodeQL'
+
+on:
+ push:
+ branches: ['main']
+ # Scan every pull request targeting main. These are the default activity
+ # types, listed explicitly so that PR submission and later head-branch
+ # updates are auditable here: `opened` covers submission, `synchronize`
+ # covers new commits pushed to the PR, and `reopened` covers a PR brought
+ # back after being closed. A PR with a merge conflict does not trigger
+ # the workflow until the conflict is resolved.
+ pull_request:
+ branches: ['main']
+ types: [opened, synchronize, reopened]
+ # No `schedule` trigger by choice: analysis runs only when a commit
+ # arrives. CodeQL ships new and updated queries over time, so a dormant
+ # branch or a newly disclosed vulnerability class will not surface until
+ # the next push or pull request. Re-add a cron here to restore periodic
+ # scanning.
+
+jobs:
+ analyze:
+ name: Analyze (${{ matrix.language }})
+ # Runner size impacts CodeQL analysis time. To learn more, please see:
+ # - https://gh.io/recommended-hardware-resources-for-running-codeql
+ # - https://gh.io/supported-runners-and-hardware-resources
+ # - https://gh.io/using-larger-runners (GitHub.com only)
+ # Consider using larger runners or machines with greater resources for possible analysis time improvements.
+ runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }}
+ permissions:
+ # required for all workflows
+ security-events: write
+
+ # required to fetch internal or private CodeQL packs
+ packages: read
+
+ # only required for workflows in private repositories
+ actions: read
+ contents: read
+
+ strategy:
+ fail-fast: false
+ matrix:
+ include:
+ # CodeQL supports the following values keywords for 'language': 'actions', 'c-cpp', 'csharp', 'go', 'java-kotlin', 'javascript-typescript', 'python', 'ruby', 'rust', 'swift'
+ # Use `c-cpp` to analyze code written in C, C++ or both
+ # Use 'java-kotlin' to analyze code written in Java, Kotlin or both
+ # Use 'javascript-typescript' to analyze code written in JavaScript, TypeScript or both
+ # To learn more about changing the languages that are analyzed or customizing the build mode for your analysis,
+ # see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/customizing-your-advanced-setup-for-code-scanning.
+ # If you are analyzing a compiled language, you can modify the 'build-mode' for that language to customize how
+ # your codebase is analyzed, see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages
+ - language: javascript-typescript
+ build-mode: none
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v7
+
+ # Add any setup steps before running the `github/codeql-action/init` action.
+ # This includes steps like installing compilers or runtimes (`actions/setup-node`
+ # or others). This is typically only required for manual builds.
+ # - name: Setup runtime (example)
+ # uses: actions/setup-example@v1
+
+ # Initializes the CodeQL tools for scanning.
+ - name: Initialize CodeQL
+ uses: github/codeql-action/init@v4
+ with:
+ languages: ${{ matrix.language }}
+ build-mode: ${{ matrix.build-mode }}
+ # If you wish to specify custom queries, you can do so here or in a config file.
+ # By default, queries listed here will override any specified in a config file.
+ # Prefix the list here with "+" to use these queries and those in the config file.
+
+ # For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs
+ # queries: security-extended,security-and-quality
+
+ # If the analyze step fails for one of the languages you are analyzing with
+ # "We were unable to automatically build your code", modify the matrix above
+ # to set the build mode to "manual" for that language. Then modify this step
+ # to build your code.
+ # ℹ️ Command-line programs to run using the OS shell.
+ # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
+ - name: Run manual build steps
+ if: matrix.build-mode == 'manual'
+ shell: bash
+ run: |
+ echo 'If you are using a "manual" build mode for one or more of the' \
+ 'languages you are analyzing, replace this with the commands to build' \
+ 'your code, for example:'
+ echo ' make bootstrap'
+ echo ' make release'
+ exit 1
+
+ - name: Perform CodeQL Analysis
+ uses: github/codeql-action/analyze@v4
+ with:
+ category: '/language:${{matrix.language}}'
diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml
new file mode 100644
index 0000000..1722db8
--- /dev/null
+++ b/.github/workflows/docs.yml
@@ -0,0 +1,61 @@
+name: Documentation
+
+on:
+ push:
+ branches: [main]
+ paths:
+ - 'docs/**'
+ - 'package.json'
+ - 'pnpm-lock.yaml'
+ - 'pnpm-workspace.yaml'
+ - '.github/workflows/docs.yml'
+ pull_request:
+ paths:
+ - 'docs/**'
+ - 'package.json'
+ - 'pnpm-lock.yaml'
+ - 'pnpm-workspace.yaml'
+ - '.github/workflows/docs.yml'
+ workflow_dispatch:
+
+permissions:
+ contents: read
+
+jobs:
+ build:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v7
+ - uses: actions/setup-node@v7
+ with:
+ node-version: 24
+ - run: corepack enable
+ - run: pnpm install --frozen-lockfile
+ - name: Build documentation
+ env:
+ DOCS_BASE: /${{ github.event.repository.name }}/
+ run: pnpm docs:build
+ - name: Upload Pages artifact
+ if: github.event_name != 'pull_request' && github.ref == 'refs/heads/main'
+ uses: actions/upload-pages-artifact@v5
+ with:
+ path: docs/.vitepress/dist
+
+ deploy:
+ if: github.event_name != 'pull_request' && github.ref == 'refs/heads/main'
+ needs: build
+ runs-on: ubuntu-latest
+ permissions:
+ pages: write
+ id-token: write
+ concurrency:
+ group: github-pages
+ cancel-in-progress: false
+ environment:
+ name: github-pages
+ url: ${{ steps.deployment.outputs.page_url }}
+ steps:
+ - uses: actions/configure-pages@v6
+ - name: Deploy to GitHub Pages
+ id: deployment
+ uses: actions/deploy-pages@v5
diff --git a/.gitignore b/.gitignore
index 872d5f6..789c52a 100644
--- a/.gitignore
+++ b/.gitignore
@@ -141,3 +141,25 @@ dist
vite.config.js.timestamp-*
vite.config.ts.timestamp-*
.vite/
+
+# Vim files
+*.swp
+*.swo
+*~
+*.un~
+
+# Local temporary files
+tmp/
+
+# macOS system
+.DS_Store
+_MACOSX
+
+# Windows system
+desktop.ini
+
+# HyperBug local runtime state
+.wrangler/
+.dev.vars
+.dev.vars.*
+.local/
diff --git a/.node-version b/.node-version
new file mode 100644
index 0000000..a45fd52
--- /dev/null
+++ b/.node-version
@@ -0,0 +1 @@
+24
diff --git a/.oxfmtrc.json b/.oxfmtrc.json
new file mode 100644
index 0000000..51a64cc
--- /dev/null
+++ b/.oxfmtrc.json
@@ -0,0 +1,21 @@
+{
+ "$schema": "./node_modules/oxfmt/configuration_schema.json",
+ "printWidth": 80,
+ "proseWrap": "preserve",
+ "singleQuote": true,
+ "trailingComma": "all",
+ "indentStyle": "space",
+ "indentWidth": 2,
+ "sortPackageJson": false,
+ "sortImports": false,
+ "ignorePatterns": [
+ "node_modules/**",
+ "dist/**",
+ "pnpm-lock.yaml",
+ "docs/**",
+ ".local/**",
+ ".agents/**",
+ "**/migrations/**",
+ "**/worker-configuration.d.ts"
+ ]
+}
diff --git a/.oxlintrc.json b/.oxlintrc.json
new file mode 100644
index 0000000..c234f94
--- /dev/null
+++ b/.oxlintrc.json
@@ -0,0 +1,301 @@
+{
+ "$schema": "./node_modules/oxlint/configuration_schema.json",
+ "plugins": ["import"],
+ "categories": {
+ "correctness": "error",
+ "suspicious": "warn",
+ "perf": "warn"
+ },
+ "rules": {
+ "import/extensions": [
+ "error",
+ "ignorePackages",
+ {
+ "ts": "always",
+ "tsx": "always",
+ "js": "always"
+ }
+ ],
+ "no-await-in-loop": "warn"
+ },
+ "overrides": [
+ {
+ "files": ["packages/domain/**/*.ts"],
+ "rules": {
+ "no-restricted-imports": [
+ "error",
+ {
+ "patterns": [
+ {
+ "group": ["*", "!./**", "../**"],
+ "message": "packages/domain is a leaf package: no imports outside itself and no external dependencies."
+ }
+ ]
+ }
+ ]
+ }
+ },
+ {
+ "files": ["packages/contracts/**/*.ts"],
+ "rules": {
+ "no-restricted-imports": [
+ "error",
+ {
+ "patterns": [
+ {
+ "group": ["*", "!./**", "../**", "!@sinclair/typebox"],
+ "message": "packages/contracts may depend only on @sinclair/typebox, so a client can read the contract without Elysia."
+ }
+ ]
+ }
+ ]
+ }
+ },
+ {
+ "files": ["packages/security/**/*.ts"],
+ "rules": {
+ "no-restricted-imports": [
+ "error",
+ {
+ "patterns": [
+ {
+ "group": [
+ "*",
+ "!./**",
+ "../**",
+ "!unified",
+ "!remark-parse",
+ "!remark-gfm",
+ "!remark-rehype",
+ "!rehype-raw",
+ "!rehype-sanitize",
+ "!hast-util-to-html",
+ "!hast",
+ "!yaml"
+ ],
+ "message": "packages/security may import only reviewed platform-neutral content libraries; no infrastructure dependencies."
+ }
+ ]
+ }
+ ]
+ }
+ },
+ {
+ "files": ["packages/config/**/*.ts"],
+ "rules": {
+ "no-restricted-imports": [
+ "error",
+ {
+ "patterns": [
+ {
+ "group": ["*", "!./**", "../**"],
+ "message": "packages/config is a leaf package: no imports outside itself and no external dependencies."
+ }
+ ]
+ }
+ ]
+ }
+ },
+ {
+ "files": ["packages/plugin-api/**/*.ts"],
+ "rules": {
+ "no-restricted-imports": [
+ "error",
+ {
+ "patterns": [
+ {
+ "group": [
+ "*",
+ "!./**",
+ "../**",
+ "!@sinclair/typebox",
+ "!@sinclair/typebox/**"
+ ],
+ "message": "packages/plugin-api is the public contract: only @sinclair/typebox may be imported, so one contract serves both profiles."
+ }
+ ]
+ }
+ ]
+ }
+ },
+ {
+ "files": ["packages/plugin-sdk/**/*.ts"],
+ "rules": {
+ "no-restricted-imports": [
+ "error",
+ {
+ "patterns": [
+ {
+ "group": ["*", "!./**", "../**", "!@hyperbug/plugin-api"],
+ "message": "packages/plugin-sdk may import only @hyperbug/plugin-api; the SDK re-exports the contract without host dependencies."
+ }
+ ]
+ }
+ ]
+ }
+ },
+ {
+ "files": ["packages/plugin-runtime/**/*.ts"],
+ "rules": {
+ "no-restricted-imports": [
+ "error",
+ {
+ "patterns": [
+ {
+ "group": ["*", "!./**", "../**", "!@hyperbug/plugin-api"],
+ "message": "packages/plugin-runtime currently may import only @hyperbug/plugin-api; Core integration dependencies arrive with module 05.2."
+ }
+ ]
+ }
+ ]
+ }
+ },
+ {
+ "files": ["packages/observability/**/*.ts"],
+ "rules": {
+ "no-restricted-imports": [
+ "error",
+ {
+ "patterns": [
+ {
+ "group": ["*", "!./**", "../**"],
+ "message": "packages/observability is a leaf package: no imports outside itself and no external dependencies."
+ }
+ ]
+ }
+ ]
+ }
+ },
+ {
+ "files": ["packages/application/**/*.ts"],
+ "rules": {
+ "no-restricted-imports": [
+ "error",
+ {
+ "patterns": [
+ {
+ "group": [
+ "*",
+ "!./**",
+ "../**",
+ "!@hyperbug/domain",
+ "!@hyperbug/contracts",
+ "!@hyperbug/plugin-api"
+ ],
+ "message": "packages/application may import only @hyperbug/domain, @hyperbug/contracts and @hyperbug/plugin-api."
+ }
+ ]
+ }
+ ]
+ }
+ },
+ {
+ "files": ["packages/testing/**/*.ts"],
+ "rules": {
+ "no-restricted-imports": [
+ "error",
+ {
+ "patterns": [
+ {
+ "group": [
+ "*",
+ "!./**",
+ "../**",
+ "!@hyperbug/contracts",
+ "!@hyperbug/domain",
+ "!@hyperbug/application"
+ ],
+ "message": "packages/testing may import only @hyperbug/contracts, @hyperbug/domain and @hyperbug/application."
+ }
+ ]
+ }
+ ]
+ }
+ },
+ {
+ "files": [
+ "packages/database/d1/**/*.ts",
+ "packages/database/postgres/**/*.ts"
+ ],
+ "rules": {
+ "no-restricted-imports": [
+ "error",
+ {
+ "patterns": [
+ {
+ "group": [
+ "*",
+ "!./**",
+ "../**",
+ "!@hyperbug/domain",
+ "!@hyperbug/application",
+ "!@hyperbug/security",
+ "!@hyperbug/plugin-api",
+ "!drizzle-orm",
+ "!drizzle-orm/**",
+ "!drizzle-kit",
+ "!pg",
+ "!@cloudflare/workers-types",
+ "!@hyperbug/security/**"
+ ],
+ "message": "database adapters may import only @hyperbug/domain, @hyperbug/application, @hyperbug/security, @hyperbug/plugin-api and their own persistence libraries; persistence types stay out of the domain."
+ }
+ ]
+ }
+ ]
+ }
+ },
+ {
+ "files": ["packages/server/**/*.ts"],
+ "rules": {
+ "no-restricted-imports": [
+ "error",
+ {
+ "patterns": [
+ {
+ "group": [
+ "*",
+ "!./**",
+ "../**",
+ "!@hyperbug/application",
+ "!@hyperbug/domain",
+ "!@hyperbug/contracts",
+ "!@hyperbug/security",
+ "!@hyperbug/config",
+ "!@hyperbug/observability",
+ "!elysia",
+ "!elysia/**",
+ "!@simplewebauthn/server",
+ "!@simplewebauthn/server/**",
+ "!@hyperbug/plugin-api",
+ "!@hyperbug/security/**"
+ ],
+ "message": "packages/server may import only the shared server-side packages, Elysia and the pinned WebAuthn verifier and @hyperbug/plugin-api."
+ }
+ ]
+ }
+ ]
+ }
+ },
+ {
+ "files": [
+ "tests/**/*.ts",
+ "tooling/**/*.ts",
+ "tooling/**/*.mjs",
+ "packages/database/**/migrations/**/*.ts"
+ ],
+ "rules": {
+ "no-await-in-loop": "off"
+ }
+ },
+ {
+ "files": ["apps/web/**/*.ts", "apps/web/**/*.tsx"],
+ "plugins": ["react", "jsx-a11y"],
+ "rules": {
+ "react/rules-of-hooks": "error",
+ "react/exhaustive-deps": "error",
+ "react/react-in-jsx-scope": "off"
+ }
+ }
+ ]
+}
diff --git a/.vscode/settings.json b/.vscode/settings.json
new file mode 100644
index 0000000..512a318
--- /dev/null
+++ b/.vscode/settings.json
@@ -0,0 +1,3 @@
+{
+ "markdown.validate.enabled": true
+}
diff --git a/AGENTS.md b/AGENTS.md
new file mode 100644
index 0000000..9b57c0f
--- /dev/null
+++ b/AGENTS.md
@@ -0,0 +1,53 @@
+# HyperBug repository guidance
+
+HyperBug is a public-first issue tracker with a static frontend and two first-class backend profiles. Start from the current repository state and the requested task scope.
+
+## Project skills
+
+- [hyperbug-development](.agents/skills/hyperbug-development/SKILL.md): use for product features, backend/API/adapter changes, and frontend workflows.
+- [hyperbug-maintenance](.agents/skills/hyperbug-maintenance/SKILL.md): use for dependency updates, database migrations, releases, deployment, and operational recovery.
+
+Read the applicable skill and only the references relevant to the work. For feature changes that also require migrations, combine the development workflow with the maintenance migration reference.
+
+## Planning and session continuity
+
+Read [the plan](docs/plan/README.md), [master progress](docs/plan/PROGRESS.md), the owning module's checklist, and its latest progress record before implementation. Follow [EXECUTION](docs/plan/EXECUTION.md) at session start, meaningful checkpoints, and session end.
+
+Every session must append a concise entry to each affected module's progress document: progress, change summary, files/artifacts, verification results, decisions/blockers, next actions, and next-session cautions. This includes investigation, failed attempts, reviews, and documentation-only sessions. Mark checklist items complete only when their actual outcomes are verified. Exception: a session that only answers a user question and changes no repository state (a read-only check, comparison, measurement, or explanation) needs no progress entry unless the user asks for one; the entry requirement still applies whenever the session implements, reviews, migrates, deploys, or otherwise changes repository state.
+
+Create and validate guidance skills before application implementation (G0). Backend acceptance in module 10 (G1) precedes product SPA development. Each post-MVP increment also completes backend acceptance before its UI. Follow the current plan gates; a request for one module does not authorize implementing the rest of the roadmap.
+
+Maintain human-facing reader documentation under `docs/site/` in both English and Simplified Chinese, with matching page paths and synchronized content. Reader-facing repository entry points may also be bilingual. Write engineering documentation and substantive updates in English, including skills, plans, progress logs, specifications, ADRs, and internal runbooks. Existing initial-architecture documents remain historical inputs unless translation is separately requested. Product localization is a separate concern.
+
+## Architecture and policy sources
+
+[SOURCES](docs/plan/SOURCES.md) links the initial specifications and records their refinements. PRODUCT owns scope; SECURITY owns minimum security policy; PERFORMANCE prioritizes correctness/security over optimization; TECH-STACK refines the older runtime/profile/version choices.
+
+Both Workers/D1/R2/Queues/Workflows and Node 24/PostgreSQL 18.x/S3-compatible storage/Graphile Worker are required. Share domain semantics, public contracts, application services, and Elysia routes; keep infrastructure types in adapters/composition roots. The frontend remains independently hosted and uses the public API. Plugins provide mechanisms without weakening Core policy.
+
+Use the skills' review and testing references for changes to security, persistent data, public contracts, or resource-intensive paths. Do not silently relax a normative baseline to accommodate a dependency or provider. Routine implementation choices do not imply a new user-approval step.
+
+## Current documentation lookup
+
+
+
+Use Context7 MCP whenever the task asks about a library, framework, SDK, API, CLI tool, or cloud service, including syntax, configuration, migration, library-specific debugging, setup, and CLI usage. Do this even for familiar tools. Prefer it over general web search for library documentation.
+
+Do not trigger this solely for refactoring, scripts from scratch, business-logic debugging, code review, or general programming concepts. If those tasks introduce an actual library/API question, look up that question. Record lookup dates, references, and remaining gaps. If the tool or coverage is unavailable, document the limitation and consult official sources or a focused runtime experiment; do not invent documentation or claim an unperformed lookup.
+
+
+For HTML/CSS/client-side JavaScript work, use the available modern-web-guidance skill: search by the intended action, retrieve relevant guides, then check implementation against them. Record guide IDs, current compatibility status, and chosen fallbacks. Preserve Baseline Widely Available, WCAG 2.2 AA, the static SPA boundary, and memory-only browser access tokens. Treat newer capabilities as progressive enhancements under the project's policy.
+
+## Commands and evidence
+
+Use commands actually defined by the repository and installed tooling. A command proposed in a plan is not proof that it exists. Exact source version examples are snapshots; verify availability before installation. Record unavailable checks and keep dependent gates incomplete rather than replacing required evidence with mock-only results.
+
+## Commit messages
+
+Write every commit message in English using Conventional Commits: `type(scope): description`, with the scope optional and the description in the imperative mood, lower case, and without a trailing period.
+
+Choose the type that matches the change: `feat`, `fix`, `docs`, `test`, `ci`, `refactor`, `perf`, `build`, `chore`, or `revert`. Keep each commit to one coherent change, and use the message body to explain rationale and verification when the subject alone does not.
+
+Record a breaking change with `!` after the type or scope and a `BREAKING CHANGE:` footer. Reference an issue in the footer as `Refs: #123`.
+
+No commit-msg hook enforces this convention yet, so it depends on author discipline during review.
diff --git a/CLAUDE.md b/CLAUDE.md
new file mode 120000
index 0000000..47dc3e3
--- /dev/null
+++ b/CLAUDE.md
@@ -0,0 +1 @@
+AGENTS.md
\ No newline at end of file
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
new file mode 100644
index 0000000..e69de29
diff --git a/PRIVACY.md b/PRIVACY.md
new file mode 100644
index 0000000..e69de29
diff --git a/PRIVACY_zh-CN.md b/PRIVACY_zh-CN.md
new file mode 100644
index 0000000..e69de29
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..0fbda41
--- /dev/null
+++ b/README.md
@@ -0,0 +1,88 @@
+**English** | [**简体中文**](README_zh-CN.md)
+
+# HyperBug
+
+[](LICENSE)
+[](https://github.com/EIHRTeam/HyperBug/releases/latest)
+[](https://github.com/EIHRTeam/HyperBug/actions/workflows/codeql.yml)
+[](https://github.com/EIHRTeam/HyperBug/actions/workflows/backend.yml)
+
+[](https://www.typescriptlang.org/)
+[](https://elysiajs.com/)
+[](.node-version)
+[](https://workers.cloudflare.com/)
+[](https://www.postgresql.org/)
+
+> [!IMPORTANT]
+> This project is still in early development stage and may contain major defects or breaking changes. DO NOT use in production.
+
+A modern, lightweight, cloud-native, serverless-first, GitHub Issues-style issue-tracking platform.
+
+## Documentation
+
+Read the [English documentation](docs/site/index.md) or [简体中文文档](docs/site/zh-CN/index.md). The guides are still outlines. The [VitePress site workflow](docs/development/DOCUMENTATION.md) covers local preview and GitHub Pages deployment. For the current backend workspace, see [backend development](docs/development/README.md).
+
+## Highlights
+
+## Quick Start
+
+### Deployment
+
+#### Backend
+
+| Profile | Runtime | Database | Object storage | Queue and workflow |
+| --------------- | ------------------ | ------------- | -------------- | ------------------ |
+| A — Cloudflare | Cloudflare Workers | D1 | R2 | Queues, Workflows |
+| B — Self-hosted | Node.js 24 | PostgreSQL 18 | S3-compatible | Graphile Worker |
+
+#### Frontend
+
+### Development
+
+```sh
+git clone git@github.com:EIHRTeam/HyperBug.git
+cd HyperBug
+pnpm install --frozen-lockfile
+pnpm dev # Run the API and static frontend locally
+pnpm test # Unit, contract, and integration tests
+pnpm lint # Lint and formatting checks
+pnpm typecheck # Strict type checking across the workspace
+pnpm build # Build API and static frontend artifacts
+```
+
+Run `pnpm run` to view the complete script list, including formatting, migration, and profile-specific integration test commands. Migrations are applied through the workspace migration command for the selected profile, delegating to the D1 migration adapter or PostgreSQL migration set. All dependencies are pinned by the committed lockfile, and lifecycle install scripts are restricted by policy.
+
+```text
+repo/
+├── apps/
+│ ├── web/ # Static SPA, independently deployed
+│ ├── api-cloudflare/ # Workers entry point
+│ └── api-node/ # Node 24 entry point
+├── packages/
+│ ├── contracts/ # Public DTOs, REST/OpenAPI docs, generated API client
+│ ├── domain/ # Entities, invariants, policies
+│ ├── application/ # Use cases and application services
+│ ├── server/ # Shared Elysia routes and middleware
+│ ├── security/ # Cryptography, tokens, sanitization, audit, abuse controls
+│ ├── database/ # Contracts plus D1 and PostgreSQL adapters
+│ ├── blob/ # Contracts plus R2 and S3 adapters
+│ ├── queue/ # Contracts plus Cloudflare and Graphile adapters
+│ ├── workflow/, search/, plugin-api/, observability/
+└── tooling/
+```
+
+See [CONTRIBUTING.md](CONTRIBUTING.md) for details.
+
+## Legal
+
+### Copyright
+
+Copyright © 2026 Endfield Industries Human Resource Team, licensed under the [Apache License Version 2.0](LICENSE).
+
+### Disclaimer
+
+Neither this project nor the Endfield Industries Human Resource Team (EIHRTeam) has any affiliation or relationship, financial or otherwise, with Shanghai Hypergryph Network Technology Co., Ltd., Gryph Frontier Pte. Ltd., or any of their respective affiliates or related entities.
+
+### Other
+
+#### Privacy
diff --git a/README_zh-CN.md b/README_zh-CN.md
new file mode 100644
index 0000000..3a8e6dd
--- /dev/null
+++ b/README_zh-CN.md
@@ -0,0 +1,88 @@
+[**English**](README.md) | **简体中文**
+
+# HyperBug
+
+[](LICENSE)
+[](https://github.com/EIHRTeam/HyperBug/releases/latest)
+[](https://github.com/EIHRTeam/HyperBug/actions/workflows/codeql.yml)
+[](https://github.com/EIHRTeam/HyperBug/actions/workflows/backend.yml)
+
+[](https://www.typescriptlang.org/)
+[](https://elysiajs.com/)
+[](.node-version)
+[](https://workers.cloudflare.com/)
+[](https://www.postgresql.org/)
+
+> [!IMPORTANT]
+> 本项目尚处早期开发阶段,可能包含重大缺陷或破坏性更改,请勿用于生产环境
+
+现代化、轻量化、云原生、Serverless 优先的类 GitHub Issue 问题跟踪平台解决方案
+
+## 文档
+
+阅读[简体中文文档](docs/site/zh-CN/index.md)或 [English documentation](docs/site/index.md)。指南目前仍为提纲;[VitePress 站点维护说明](docs/development/DOCUMENTATION.md)介绍本地预览与 GitHub Pages 部署。现有后端工作区请参阅[后端开发指南](docs/development/README.md)。
+
+## 亮点
+
+## 快速开始
+
+### 部署
+
+#### 后端
+
+| Profile | 运行时 | 数据库 | 对象存储 | 队列与工作流 |
+| -------------- | ------------------ | --------------- | -------- | ----------------- |
+| A — Cloudflare | Cloudflare Workers | D1 | R2 | Queues、Workflows |
+| B — 自托管 | Node.js 24 | PostgreSQL 18.x | S3 兼容 | Graphile Worker |
+
+#### 前端
+
+### 开发
+
+```sh
+git clone git@github.com:EIHRTeam/HyperBug.git
+cd HyperBug
+pnpm install --frozen-lockfile
+pnpm dev # 本地运行 API 与静态前端
+pnpm test # 单元、契约与集成测试
+pnpm lint # Lint 与格式检查
+pnpm typecheck # 全工作区严格类型检查
+pnpm build # 构建 API 产物与静态前端产物
+```
+
+运行 `pnpm run` 可查看完整脚本列表,其中包含格式化、迁移与按 Profile 划分的集成测试命令;迁移通过所选 Profile 对应的工作区迁移命令执行,分别委托给 D1 迁移适配器或 PostgreSQL 迁移集。所有依赖都由已提交的 lockfile 固定,生命周期安装脚本受策略限制。
+
+```text
+repo/
+├── apps/
+│ ├── web/ # 静态 SPA,独立部署
+│ ├── api-cloudflare/ # Workers 入口
+│ └── api-node/ # Node 24 入口
+├── packages/
+│ ├── contracts/ # 公开 DTO、REST/OpenAPI 文档、生成的 API 客户端
+│ ├── domain/ # 实体、不变量、策略
+│ ├── application/ # 用例与应用服务
+│ ├── server/ # 共享 Elysia 路由与中间件
+│ ├── security/ # 密码学、令牌、清洗、审计、滥用防护
+│ ├── database/ # contracts + d1 与 postgres 适配器
+│ ├── blob/ # contracts + r2 与 s3 适配器
+│ ├── queue/ # contracts + cloudflare 与 graphile 适配器
+│ ├── workflow/, search/, plugin-api/, observability/
+└── tooling/
+```
+
+详请参见 [CONTRIBUTING.md](CONTRIBUTING.md)
+
+## 法律
+
+### 版权
+
+Copyright © 2026 Endfield Industries Human Resource Team, licensed under [Apache License, Version 2.0.](LICENSE)
+
+### 免责声明
+
+本项目及终末地工业人事部与上海市鹰角网络科技有限公司、Gryph Frontier Pte. Ltd.,及其各自的关联实体之间没有任何组织或资金上的联系。
+
+### 其他
+
+#### 隐私
diff --git a/apps/api-cloudflare/ingress-configuration.d.ts b/apps/api-cloudflare/ingress-configuration.d.ts
new file mode 100644
index 0000000..52ffde4
--- /dev/null
+++ b/apps/api-cloudflare/ingress-configuration.d.ts
@@ -0,0 +1,34 @@
+/* eslint-disable */
+// Generated by Wrangler by running `wrangler types --config=apps/api-cloudflare/wrangler-ingress.jsonc --include-runtime=false --env-interface=GatewayEnv apps/api-cloudflare/ingress-configuration.d.ts` (hash: 63acac08f74c7d9d690b3b0d30f3d1d5)
+interface __BaseEnv_GatewayEnv {
+ HYPERBUG_INGRESS_KEY: string;
+ API:
+ | Fetcher /* hyperbug-api-staging */
+ | Fetcher /* hyperbug-api */
+ | Fetcher /* hyperbug-api-local */;
+}
+declare namespace Cloudflare {
+ interface GlobalProps {
+ mainModule: typeof import('./src/ingress');
+ }
+ interface StagingEnv {
+ HYPERBUG_INGRESS_KEY: string;
+ API: Fetcher /* hyperbug-api-staging */;
+ }
+ interface ProductionEnv {
+ HYPERBUG_INGRESS_KEY: string;
+ API: Fetcher /* hyperbug-api */;
+ }
+ interface Env extends __BaseEnv_GatewayEnv {}
+}
+interface GatewayEnv extends __BaseEnv_GatewayEnv {}
+type StringifyValues> = {
+ [Binding in keyof EnvType]: EnvType[Binding] extends string
+ ? EnvType[Binding]
+ : string;
+};
+declare namespace NodeJS {
+ interface ProcessEnv extends StringifyValues<
+ Pick
+ > {}
+}
diff --git a/apps/api-cloudflare/package.json b/apps/api-cloudflare/package.json
new file mode 100644
index 0000000..730f1b1
--- /dev/null
+++ b/apps/api-cloudflare/package.json
@@ -0,0 +1,17 @@
+{
+ "name": "@hyperbug/api-cloudflare",
+ "version": "0.0.0",
+ "private": true,
+ "type": "module",
+ "dependencies": {
+ "@hyperbug/config": "workspace:*",
+ "@hyperbug/database-d1": "workspace:*",
+ "@hyperbug/observability": "workspace:*",
+ "@hyperbug/security": "workspace:*",
+ "@hyperbug/server": "workspace:*",
+ "elysia": "1.4.30",
+ "libsodium-sumo": "0.8.4",
+ "@hyperbug/application": "workspace:*",
+ "@hyperbug/blob-r2": "workspace:*"
+ }
+}
diff --git a/apps/api-cloudflare/src/abuse-keys.ts b/apps/api-cloudflare/src/abuse-keys.ts
new file mode 100644
index 0000000..e849af6
--- /dev/null
+++ b/apps/api-cloudflare/src/abuse-keys.ts
@@ -0,0 +1,19 @@
+import { RateLimitFailure, SecretAbuseKeyProvider } from '@hyperbug/security';
+
+/** A dedicated Worker Secret binding, separate from ordinary vars and keys. */
+export function createWorkerAbuseKeyProvider(
+ secretBinding: string,
+): SecretAbuseKeyProvider {
+ return new SecretAbuseKeyProvider({
+ async read(signal) {
+ if (
+ signal.aborted ||
+ typeof secretBinding !== 'string' ||
+ secretBinding.length < 1 ||
+ secretBinding.length > 16384
+ )
+ throw new RateLimitFailure();
+ return secretBinding;
+ },
+ });
+}
diff --git a/apps/api-cloudflare/src/async-budget.ts b/apps/api-cloudflare/src/async-budget.ts
new file mode 100644
index 0000000..c694e61
--- /dev/null
+++ b/apps/api-cloudflare/src/async-budget.ts
@@ -0,0 +1,118 @@
+import type {
+ D1Database,
+ D1PreparedStatement,
+ R2Bucket,
+} from '@cloudflare/workers-types';
+import { MINIMUM_ASYNC_LIMITS, AsyncError } from '@hyperbug/application/async';
+
+/** Count before I/O, including every SQL statement in an atomic batch. */
+export function minimumInvocationBudget() {
+ let queries = 0,
+ subrequests = 0,
+ reads = 0,
+ writes = 0,
+ reporting = false;
+ const take = (sql = 0, external = 0) => {
+ if (
+ queries + sql >
+ (reporting
+ ? MINIMUM_ASYNC_LIMITS.queries
+ : MINIMUM_ASYNC_LIMITS.queries - 5) ||
+ subrequests + sql + external >
+ (reporting
+ ? MINIMUM_ASYNC_LIMITS.subrequests
+ : MINIMUM_ASYNC_LIMITS.subrequests - 5)
+ )
+ throw new AsyncError('transient');
+ queries += sql;
+ subrequests += sql + external;
+ };
+ const result = (value: unknown) => {
+ for (const item of Array.isArray(value) ? value : [value]) {
+ const meta = (
+ item as { meta?: { rows_read?: number; rows_written?: number } }
+ )?.meta;
+ reads += meta?.rows_read ?? 0;
+ writes += meta?.rows_written ?? 0;
+ }
+ if (
+ reads > MINIMUM_ASYNC_LIMITS.invocationReads ||
+ writes > MINIMUM_ASYNC_LIMITS.invocationWrites
+ )
+ throw new AsyncError('transient');
+ return value;
+ };
+ const originals = new WeakMap();
+ const statement = (raw: D1PreparedStatement): D1PreparedStatement => {
+ const wrapped = {
+ bind: (...values: unknown[]) => statement(raw.bind(...values)),
+ async first(column?: string) {
+ take(1);
+ const value = await raw.all>();
+ result(value);
+ const row = value.results[0];
+ return (
+ row ? (column === undefined ? row : row[column]) : null
+ ) as T | null;
+ },
+ async run() {
+ take(1);
+ return result(await raw.run());
+ },
+ async all() {
+ take(1);
+ return result(await raw.all());
+ },
+ raw() {
+ throw new AsyncError('unsupported');
+ },
+ } as D1PreparedStatement;
+ originals.set(wrapped, raw);
+ return wrapped;
+ };
+ return {
+ db(raw: D1Database): D1Database {
+ return {
+ prepare: (sql: string) => statement(raw.prepare(sql)),
+ async batch(items: D1PreparedStatement[]) {
+ take(items.length);
+ return result(
+ await raw.batch(
+ items.map((item) => originals.get(item) ?? item),
+ ),
+ );
+ },
+ exec() {
+ throw new AsyncError('unsupported');
+ },
+ withSession() {
+ throw new AsyncError('unsupported');
+ },
+ dump: () => raw.dump(),
+ } as D1Database;
+ },
+ bucket(raw: R2Bucket): R2Bucket {
+ return new Proxy(raw, {
+ get(target, key) {
+ const member = Reflect.get(target, key);
+ return typeof member === 'function'
+ ? (...args: unknown[]) => {
+ take(0, 1);
+ return member.apply(target, args);
+ }
+ : member;
+ },
+ });
+ },
+ fetch(request: Request) {
+ take(0, 1);
+ return fetch(request);
+ },
+ diagnostics() {
+ reporting = true;
+ },
+ snapshot() {
+ return { queries, subrequests, reads, writes };
+ },
+ };
+}
diff --git a/apps/api-cloudflare/src/async-minimum.ts b/apps/api-cloudflare/src/async-minimum.ts
new file mode 100644
index 0000000..76e282c
--- /dev/null
+++ b/apps/api-cloudflare/src/async-minimum.ts
@@ -0,0 +1,144 @@
+import type { D1Database } from '@cloudflare/workers-types';
+import {
+ createD1AsyncStore,
+ createD1MinimumAsyncStore,
+ createD1MinimumJobStore,
+ createD1PluginRegistryStore,
+ createD1PluginSettingsStore,
+ createD1ExpiredCleanupStore,
+} from '@hyperbug/database-d1/async';
+import {
+ createAsyncProcessor,
+ createAsyncEventHandler,
+ runConformanceStep,
+ runScheduledUploadCleanup,
+ MINIMUM_ASYNC_LIMITS,
+} from '@hyperbug/application/async';
+import { loadAsyncMaintenancePolicy } from '@hyperbug/config';
+import { minimumInvocationBudget } from './async-budget.ts';
+import type { WorkerUploadBindings } from './uploads.ts';
+
+/** A single phase per existing Cron tick. No queue or Workflow binding is used. */
+export async function runMinimumAsyncTick(input: {
+ db: D1Database;
+ scheduledTime: number;
+ now?: () => number;
+ bindings: WorkerUploadBindings & {
+ ASYNC_ORPHAN_RETENTION_SECONDS?: unknown;
+ ASYNC_TERMINAL_RETENTION_SECONDS?: unknown;
+ };
+ sessionRetentionMs: number;
+}) {
+ const clock = input.now ?? Date.now,
+ budget = minimumInvocationBudget(),
+ db = budget.db(input.db);
+ const minimum = createD1MinimumAsyncStore(db);
+ const phase = Math.floor(input.scheduledTime / 300000) % 5;
+ let outcome = 'done';
+ let jobState: { checkpoint: number; terminal: boolean } | null = null;
+ try {
+ if (phase === 0) {
+ const token = crypto.randomUUID(),
+ admission = await minimum.admitAndClaim(clock(), token),
+ refs = admission.references;
+ if (!admission.admitted) outcome = 'quota-exhausted';
+ if (refs[0]) {
+ const process = createAsyncProcessor({
+ store: minimum.executionStore(),
+ clock,
+ token: () => token,
+ handle: createAsyncEventHandler({
+ search: minimum.searchStore(),
+ registry: createD1PluginRegistryStore(db),
+ settings: createD1PluginSettingsStore(db),
+ tier: 'cloudflare-minimum',
+ }),
+ });
+ await process(refs[0]);
+ }
+ } else if (phase === 4) {
+ const minimumJobs = createD1MinimumJobStore(db),
+ selection = await minimumJobs.admit(clock()),
+ jobs = minimumJobs.store;
+ if (!selection.admitted) outcome = 'quota-exhausted';
+ if (selection.job) {
+ if (selection.job.maxSteps > MINIMUM_ASYNC_LIMITS.steps)
+ await jobs.cancel(selection.job.id, clock());
+ else
+ jobState = await runConformanceStep(
+ jobs,
+ { version: 1, jobId: selection.job.id },
+ clock,
+ );
+ }
+ } else if (!(await minimum.reserve(clock()))) outcome = 'quota-exhausted';
+ else if (phase === 1) {
+ await createD1ExpiredCleanupStore(db).purgeExpired(
+ clock(),
+ input.sessionRetentionMs,
+ 1,
+ );
+ } else if (phase === 2) {
+ const store = createD1AsyncStore(db);
+ const { configureWorkerUploads } = await import('./uploads.ts');
+ const uploads = configureWorkerUploads(
+ db,
+ {
+ ...input.bindings,
+ ...(input.bindings.HYPERBUG_UPLOAD_BLOB
+ ? {
+ HYPERBUG_UPLOAD_BLOB: budget.bucket(
+ input.bindings.HYPERBUG_UPLOAD_BLOB,
+ ),
+ }
+ : {}),
+ },
+ budget.fetch,
+ );
+ if (uploads)
+ await runScheduledUploadCleanup(
+ store,
+ uploads,
+ loadAsyncMaintenancePolicy(input.bindings).orphanRetentionMs,
+ clock,
+ );
+ } else if (phase === 3) {
+ const store = createD1AsyncStore(db);
+ await store.purgeTerminal(
+ Math.max(
+ 0,
+ clock() -
+ loadAsyncMaintenancePolicy(input.bindings).terminalRetentionMs,
+ ),
+ 1,
+ );
+ await store.purgeJobs(
+ Math.max(
+ 0,
+ clock() -
+ loadAsyncMaintenancePolicy(input.bindings).terminalRetentionMs,
+ ),
+ 1,
+ );
+ }
+ } catch {
+ // Leases/source/checkpoints survive budget limits and interrupted I/O.
+ outcome = 'bounded-error';
+ }
+ budget.diagnostics();
+ const backlog = await minimum.backlog().catch(() => null);
+ const report = {
+ component: 'async-minimum',
+ phase,
+ outcome,
+ job: jobState,
+ ...budget.snapshot(),
+ backlog,
+ ageMs:
+ backlog?.oldestAt === null || !backlog
+ ? null
+ : Math.max(0, clock() - backlog.oldestAt),
+ };
+ console.log(JSON.stringify(report));
+ return report;
+}
diff --git a/apps/api-cloudflare/src/async-processing.ts b/apps/api-cloudflare/src/async-processing.ts
new file mode 100644
index 0000000..614e912
--- /dev/null
+++ b/apps/api-cloudflare/src/async-processing.ts
@@ -0,0 +1,57 @@
+import type {
+ D1Database,
+ Queue,
+ MessageBatch,
+} from '@cloudflare/workers-types';
+import {
+ createD1AsyncStore,
+ createD1SearchIndexStore,
+ createD1PluginRegistryStore,
+ createD1PluginSettingsStore,
+} from '@hyperbug/database-d1';
+import {
+ createAsyncEventHandler,
+ createAsyncProcessor,
+ createOutboxDispatcher,
+ type AsyncPluginBinding,
+ type TaskReference,
+} from '@hyperbug/application';
+import {
+ createCloudflareTaskQueue,
+ consumeCloudflareBatch,
+} from './task-queue.ts';
+export function configureCloudflareAsync(
+ db: D1Database,
+ queue: Queue | null,
+ bindings: readonly AsyncPluginBinding[] = [],
+) {
+ const store = createD1AsyncStore(db);
+ const report = (metric: unknown) =>
+ console.log(JSON.stringify({ component: 'async', metric }));
+ const process = createAsyncProcessor({
+ store,
+ handle: createAsyncEventHandler({
+ search: createD1SearchIndexStore(db),
+ registry: createD1PluginRegistryStore(db),
+ settings: createD1PluginSettingsStore(db),
+ bindings,
+ }),
+ telemetry: report,
+ });
+ const dispatch = queue
+ ? createOutboxDispatcher({
+ store,
+ queue: createCloudflareTaskQueue(queue),
+ telemetry: report,
+ })
+ : null;
+ return {
+ store,
+ async dispatch() {
+ if (!dispatch) throw new Error('Async queue unavailable');
+ await dispatch();
+ },
+ queue: (batch: MessageBatch) =>
+ consumeCloudflareBatch(batch, process),
+ };
+}
diff --git a/apps/api-cloudflare/src/attachment-scanner.ts b/apps/api-cloudflare/src/attachment-scanner.ts
new file mode 100644
index 0000000..9ca7c0a
--- /dev/null
+++ b/apps/api-cloudflare/src/attachment-scanner.ts
@@ -0,0 +1,96 @@
+import type {
+ Fetcher,
+ AbortController as PlatformAbortController,
+} from '@cloudflare/workers-types';
+import { createR2BlobDigest } from '@hyperbug/blob-r2';
+import {
+ createScannerServiceClient,
+ scannerServiceUrl,
+ type AttachmentScanner,
+} from '@hyperbug/application';
+declare const AbortController: typeof PlatformAbortController;
+
+/** Explicit private service capability; no ambient Internet fetch, endpoint selection or retry. */
+export function configureWorkerAttachmentScanner(
+ binding: Pick | undefined,
+ secret: string | undefined,
+): AttachmentScanner | null {
+ if (binding === undefined && secret === undefined) return null;
+ try {
+ if (!binding || typeof binding.fetch !== 'function' || secret === undefined)
+ throw new Error();
+ return createScannerServiceClient(
+ {
+ async send(input) {
+ const controller = new AbortController();
+ const abort = () => controller.abort();
+ const cleanup = () =>
+ input.signal.removeEventListener('abort', abort);
+ input.signal.addEventListener('abort', abort, { once: true });
+ if (input.signal.aborted) abort();
+ try {
+ const response = await binding.fetch(scannerServiceUrl, {
+ method: 'POST',
+ headers: input.headers,
+ body: input.body,
+ signal: controller.signal,
+ redirect: 'manual',
+ });
+ const source = response.body?.getReader();
+ if (!source) cleanup();
+ const body = source
+ ? new ReadableStream(
+ {
+ async pull(sink) {
+ try {
+ const next = await source.read();
+ if (next.done) {
+ sink.close();
+ source.releaseLock();
+ cleanup();
+ return;
+ }
+ if (!(next.value instanceof Uint8Array))
+ throw new Error();
+ sink.enqueue(next.value);
+ } catch {
+ cleanup();
+ void source
+ .cancel()
+ .catch(() => {})
+ .finally(() => source.releaseLock());
+ sink.error(
+ new Error('Scanner service response failed'),
+ );
+ }
+ },
+ async cancel() {
+ try {
+ await source.cancel();
+ } finally {
+ source.releaseLock();
+ cleanup();
+ }
+ },
+ },
+ { highWaterMark: 0 },
+ )
+ : null;
+ return {
+ status: response.status,
+ contentType: response.headers.get('content-type'),
+ body,
+ };
+ } catch (error) {
+ cleanup();
+ throw error;
+ }
+ },
+ },
+ createR2BlobDigest,
+ secret,
+ );
+ } catch {
+ throw new Error('Invalid attachment scanner service configuration');
+ }
+}
diff --git a/apps/api-cloudflare/src/composition.ts b/apps/api-cloudflare/src/composition.ts
new file mode 100644
index 0000000..7e90102
--- /dev/null
+++ b/apps/api-cloudflare/src/composition.ts
@@ -0,0 +1,478 @@
+import { runMinimumAsyncTick } from './async-minimum.ts';
+import { loadAsyncMaintenancePolicy } from '@hyperbug/config';
+import { createD1AsyncStore } from '@hyperbug/database-d1';
+import { runScheduledUploadCleanup } from '@hyperbug/application';
+import { createCloudflareWorkflowAdapter, d1JobStore } from './workflow.ts';
+import type { AsyncPluginBinding } from '@hyperbug/application';
+import { configureCloudflareAsync } from './async-processing.ts';
+import { expiredCleanupBatchSize } from '@hyperbug/application';
+import { configureWorkerUploads } from './uploads.ts';
+import { CloudflareAdapter } from 'elysia/adapter/cloudflare-worker';
+import {
+ configureOptionalTurnstile,
+ createApp,
+ parseBootstrapEnrollmentCode,
+ parseOAuthClients,
+ parsePasskeyConfiguration,
+ createBoundMinimumLoginAdmission,
+} from '@hyperbug/server';
+import { loadConfig, type DeploymentConfig } from '@hyperbug/config';
+import { jsonTelemetry } from '@hyperbug/observability';
+import { createCloudflareTurnstileVerifier } from './turnstile.ts';
+import {
+ adaptMinimumPasswordService,
+ auditEvent,
+ createMinimumPasswordService,
+ withMinimumPasswordUpgrade,
+ minimumTierAccountLockoutPolicy,
+ type AccountPasswordService,
+ minimumTierPasswordPolicy,
+ type MinimumPasswordService,
+} from '@hyperbug/security';
+import { createWorkerAbuseKeyProvider } from './abuse-keys.ts';
+import {
+ createD1AccountAdministration,
+ createD1AccountLockoutStore,
+ createD1AccountRegistrationStore,
+ createD1AccountRecoveryStore,
+ createD1AccountSessionStore,
+ createD1AuditRepository,
+ createD1KeyRegistry,
+ createD1OAuthStores,
+ createD1PasskeyStores,
+ createD1PluginRegistryStore,
+ createD1PluginEventOutbox,
+ createD1PluginSettingsStore,
+ createD1ProjectRoleStore,
+ createD1ProjectStore,
+ createD1Repository,
+ createD1SearchStore,
+ createD1SearchIndexStore,
+ createD1CommentStore,
+ createD1ReactionStore,
+ createD1TimelineStore,
+ createD1RateCounterStore,
+ createD1ExpiredCleanupStore,
+ createD1StaffEnrollmentStore,
+ createD1TaxonomyStore,
+ createD1ContentDefinitionStore,
+ createD1AttachmentStore,
+} from '@hyperbug/database-d1';
+import { createCloudflareVolumetricLimiter } from './rate-limit.ts';
+import { createWorkerKeyProvider } from './key-provider.ts';
+import { createIngressAttestation } from './ingress-attestation.ts';
+
+export function createCloudflareApi(
+ env: Env,
+ standardFactory?: (deployment: DeploymentConfig) => AccountPasswordService,
+ asyncPlugins: readonly AsyncPluginBinding[] = [],
+) {
+ // Workers permits AOT compilation during module initialization, before requests.
+ const config = loadConfig(env, 'cloudflare');
+ const captcha = configureOptionalTurnstile(
+ {
+ secret: env.TURNSTILE_SECRET,
+ siteKey: env.TURNSTILE_SITE_KEY,
+ hostname: env.TURNSTILE_HOSTNAME,
+ environment: config.environment,
+ },
+ createCloudflareTurnstileVerifier,
+ );
+ const auditRepository = env.DB ? createD1AuditRepository(env.DB) : null;
+ const abuse = env.DB
+ ? {
+ provider: createWorkerAbuseKeyProvider(env.HYPERBUG_ABUSE_KEY_RING),
+ store: createD1RateCounterStore(env.DB),
+ limiter: createCloudflareVolumetricLimiter(env.ABUSE_VOLUMETRIC),
+ auditAppend: auditRepository?.append ?? null,
+ }
+ : null;
+ const keyProvider = env.DB
+ ? createWorkerKeyProvider(
+ env.HYPERBUG_KEY_RING,
+ createD1KeyRegistry(env.DB),
+ config.security.authorization.timeoutMs,
+ )
+ : null;
+ const ingress = env.HYPERBUG_INGRESS_KEY
+ ? createIngressAttestation(env.HYPERBUG_INGRESS_KEY)
+ : null;
+ const accountStore = env.DB ? createD1AccountRegistrationStore(env.DB) : null;
+ // Optional operator-channel enrollment code: absent disarms the bootstrap
+ // route; a present but malformed value must refuse startup instead of
+ // silently disabling enrollment. Not declared required anywhere, so the
+ // binding is read through a narrowed view of the generated environment.
+ const bootstrapSecret = (
+ env as {
+ HYPERBUG_BOOTSTRAP_ENROLLMENT?: string;
+ }
+ ).HYPERBUG_BOOTSTRAP_ENROLLMENT;
+ const bootstrapCode =
+ bootstrapSecret === undefined
+ ? null
+ : parseBootstrapEnrollmentCode(bootstrapSecret);
+ if (bootstrapSecret !== undefined && bootstrapCode === null)
+ throw new Error('Invalid bootstrap enrollment configuration');
+ const staffEnrollmentStore = env.DB
+ ? createD1StaffEnrollmentStore(env.DB)
+ : null;
+ // Optional registered public clients: absent disarms the code flow; a present
+ // but malformed registry refuses startup instead of running a broken flow.
+ // Not declared required anywhere, so the binding is read through a narrowed
+ // view of the generated environment.
+ const oauthSecret = (
+ env as {
+ HYPERBUG_OAUTH_CLIENTS?: string;
+ }
+ ).HYPERBUG_OAUTH_CLIENTS;
+ const oauthClients = parseOAuthClients(oauthSecret);
+ const oauthCodeStore = env.DB ? createD1OAuthStores(env.DB) : null;
+ const projectRoleStore = env.DB ? createD1ProjectRoleStore(env.DB) : null;
+ const projectStore = env.DB ? createD1ProjectStore(env.DB) : null;
+ const issueRepository = env.DB ? createD1Repository(env.DB) : null;
+ const commentStore = env.DB ? createD1CommentStore(env.DB) : null;
+ const reactionStore = env.DB ? createD1ReactionStore(env.DB) : null;
+ const timelineStore = env.DB ? createD1TimelineStore(env.DB) : null;
+ const taxonomyStore = env.DB ? createD1TaxonomyStore(env.DB) : null;
+ const pluginRegistry = env.DB ? createD1PluginRegistryStore(env.DB) : null;
+ const pluginSettings = env.DB ? createD1PluginSettingsStore(env.DB) : null;
+ const asyncRuntime =
+ env.DB && config.deployment.tier === 'standard'
+ ? configureCloudflareAsync(
+ env.DB,
+ (
+ env as {
+ ASYNC_TASKS?: import('@cloudflare/workers-types').Queue<
+ import('@hyperbug/application').TaskReference
+ >;
+ }
+ ).ASYNC_TASKS ?? null,
+ asyncPlugins,
+ )
+ : null;
+ const pluginEventOutbox = env.DB ? createD1PluginEventOutbox(env.DB) : null;
+ const accountAdministration = env.DB
+ ? createD1AccountAdministration(env.DB)
+ : null;
+ // A complete relying-party selection without the session/key prerequisites
+ // refuses startup instead of silently disabling passkey authentication.
+ // Not declared required anywhere, so the bindings are read through a
+ // narrowed view of the generated environment.
+ const passkeyVars = env as {
+ PASSKEY_RP_ID?: string;
+ PASSKEY_RP_NAME?: string;
+ PASSKEY_ORIGIN?: string;
+ };
+ const passkeyConfig = parsePasskeyConfiguration({
+ rpId: passkeyVars.PASSKEY_RP_ID,
+ rpName: passkeyVars.PASSKEY_RP_NAME,
+ origin: passkeyVars.PASSKEY_ORIGIN,
+ });
+ const passkey =
+ passkeyConfig && env.DB && keyProvider
+ ? {
+ ...passkeyConfig,
+ store: createD1PasskeyStores(env.DB),
+ passwordStore: createD1AccountRegistrationStore(env.DB),
+ sessionStore: createD1AccountSessionStore(env.DB),
+ keyProvider,
+ }
+ : null;
+ // A correctly acknowledged minimum-tier selection is activated through the
+ // audited enablement contract: the startup warning, one persisted
+ // deployment.enablement event (a deterministic id makes cold starts
+ // idempotent — only the first request of the first isolate writes it) and
+ // the peppered password service preflight through the loader-based adapter.
+ const minimumTierEnabled = config.deployment.tier === 'cloudflare-minimum';
+ const enablementEventId = '80fad34e-66c5-4c63-a0c1-1a2e7a35b3f2';
+ if (minimumTierEnabled)
+ console.warn(
+ `HyperBug minimum tier enabled (acknowledged): degradations ${config.deployment.degradationIds.join(',')} active; PBKDF2 passwords use the disclosed Minimum exception; the tier runs without independent 13.G6 acceptance.`,
+ );
+ const recordTierEnablement = async (): Promise => {
+ const recorded = await env
+ .DB!.prepare('SELECT id FROM audit_events WHERE id = ?')
+ .bind(enablementEventId)
+ .first();
+ if (recorded !== null) return;
+ try {
+ await auditRepository!.append(
+ auditEvent({
+ id: enablementEventId,
+ projectId: null,
+ actorId: null,
+ systemActor: 'core.deployment',
+ action: 'deployment.enablement',
+ targetId: 'cloudflare-minimum',
+ result: 'success',
+ requestId: enablementEventId,
+ createdAt: Date.now(),
+ metadata: {
+ v: 1,
+ acknowledgement: 'minimum-v2',
+ outcome: 'enabled',
+ },
+ }),
+ new AbortController().signal,
+ );
+ } catch {
+ // A racing cold start may have written the same deterministic id.
+ const raced = await env
+ .DB!.prepare('SELECT id FROM audit_events WHERE id = ?')
+ .bind(enablementEventId)
+ .first();
+ if (raced === null)
+ throw new Error('The minimum tier enablement audit write failed');
+ }
+ };
+ let minimumService: Promise | null = null;
+ const loadMinimumPasswordService = (): Promise =>
+ (minimumService ??= (async () => {
+ // Runtime prerequisite check: the audited activation needs the database,
+ // its audit repository and the key provider. (Cloudflare's upload-time
+ // module validation runs without live bindings, so a module-scope check
+ // here would falsely refuse valid uploads; this runs per isolate.)
+ if (!env.DB || !auditRepository || !keyProvider)
+ throw new Error(
+ 'The minimum tier requires its audited enablement trail',
+ );
+ await recordTierEnablement();
+ return createMinimumPasswordService(
+ keyProvider!,
+ minimumTierPasswordPolicy,
+ // B15's ten-login burst exposed single-slot saturation; bound native
+ // PBKDF2 work separately from durable account/IP admission.
+ 10,
+ );
+ })().catch((error: unknown) => {
+ minimumService = null;
+ throw error;
+ }));
+ const minimumPassword = minimumTierEnabled
+ ? adaptMinimumPasswordService(loadMinimumPasswordService)
+ : null;
+
+ const standardPassword =
+ config.deployment.tier === 'standard'
+ ? (() => {
+ if (!standardFactory)
+ throw new Error(
+ 'The Minimum entry requires cloudflare-minimum configuration',
+ );
+ const standard = standardFactory(config.deployment);
+ return keyProvider
+ ? withMinimumPasswordUpgrade(
+ standard,
+ adaptMinimumPasswordService(() =>
+ createMinimumPasswordService(
+ keyProvider,
+ minimumTierPasswordPolicy,
+ ),
+ ),
+ )
+ : standard;
+ })()
+ : null;
+ const minimumLoginAdmission = minimumTierEnabled
+ ? createBoundMinimumLoginAdmission(
+ captcha.gate,
+ {
+ lockout: minimumTierAccountLockoutPolicy,
+ rateTimeoutMs: config.security.authorization.timeoutMs,
+ lockoutTimeoutMs: config.security.authorization.timeoutMs,
+ },
+ abuse && env.DB
+ ? {
+ provider: abuse.provider,
+ rateStore: abuse.store,
+ lockoutStore: createD1AccountLockoutStore(env.DB),
+ ...(ingress ? { clientAddress: ingress.verify } : {}),
+ }
+ : null,
+ )
+ : null;
+ const uploads = configureWorkerUploads(
+ env.DB ?? null,
+ env as unknown as Parameters[1],
+ );
+ const app = createApp({
+ adapter: CloudflareAdapter,
+ config,
+ telemetry: jsonTelemetry((line) => console.log(line)),
+ ready: async (signal) => {
+ if (minimumTierEnabled && !activationComplete) return false;
+ if (
+ !abuse ||
+ !env.DB ||
+ typeof env.ABUSE_VOLUMETRIC?.limit !== 'function' ||
+ signal.aborted
+ )
+ return false;
+ try {
+ await abuse.provider.active(signal);
+ if (signal.aborted) return false;
+ if (!keyProvider) return false;
+ await keyProvider.current('token-hmac');
+ if (signal.aborted) return false;
+ const result = await env.DB.prepare(
+ 'SELECT 1 FROM rate_limit_counters, password_credentials, authorization_sessions LIMIT 0',
+ ).all();
+ return !signal.aborted && result.success === true;
+ } catch {
+ return false;
+ }
+ },
+ captcha: captcha.gate,
+ captchaSiteKey: captcha.siteKey,
+ abuse: abuse
+ ? {
+ ...abuse,
+ ...(ingress ? { clientAddress: ingress.verify } : {}),
+ }
+ : null,
+ keyProvider,
+ standardPassword: standardPassword ?? null,
+ minimumPassword,
+ minimumLoginAdmission,
+ registrationStore: accountStore,
+ passwordStore: accountStore,
+ sessionStore: env.DB ? createD1AccountSessionStore(env.DB) : null,
+ recoveryStore: env.DB ? createD1AccountRecoveryStore(env.DB) : null,
+ passkey,
+ // The shared server requires a complete OAuth dependency set. During
+ // upload validation the parsed registry stays in this root, whose fetch
+ // guard refuses all requests until native resources are present.
+ oauthClients: env.DB && keyProvider ? oauthClients : [],
+ oauthCodeStore,
+ projectRoleStore,
+ projectStore,
+ issueRepository,
+ searchStore: env.DB ? createD1SearchStore(env.DB) : null,
+ searchIndexStore: env.DB ? createD1SearchIndexStore(env.DB) : null,
+ commentStore,
+ reactionStore,
+ timelineStore,
+ taxonomyStore,
+ contentDefinitionStore: env.DB
+ ? createD1ContentDefinitionStore(env.DB)
+ : null,
+ attachmentStore: env.DB ? createD1AttachmentStore(env.DB) : null,
+ mediaOrigin:
+ (env as { HYPERBUG_MEDIA_ORIGIN?: string }).HYPERBUG_MEDIA_ORIGIN ?? null,
+ uploads,
+ pluginRegistry,
+ pluginSettings,
+ pluginEventOutbox,
+ accountAdministration,
+ auditAppend: auditRepository?.append ?? null,
+ auditRepository,
+ bootstrapCode,
+ staffEnrollmentStore,
+ bootstrapState: staffEnrollmentStore
+ ? async () => (await staffEnrollmentStore.countActiveStaff()) === 0
+ : null,
+ }).compile();
+
+ // Request-local I/O starts lazily; only sensitive routes wait for activation.
+ let activationComplete = false;
+ let tierActivation: Promise | null = null;
+ const ensureTierActivated = (): Promise =>
+ (tierActivation ??= loadMinimumPasswordService()
+ .then(() => {
+ activationComplete = true;
+ })
+ .catch((error: unknown) => {
+ tierActivation = null;
+ throw error;
+ }));
+
+ return {
+ async fetch(request: Request) {
+ // Cloudflare validates modules at upload without live native bindings.
+ // Validate configuration above; require binding-dependent auth resources
+ // before the first runtime request can reach any route.
+ if (oauthClients.length > 0 && (!env.DB || !keyProvider))
+ throw new Error('Invalid OAuth client configuration');
+ if (passkeyConfig && (!env.DB || !keyProvider))
+ throw new Error('Invalid passkey configuration');
+ void uploads?.intents;
+ if (minimumTierEnabled) {
+ const path = new URL(request.url).pathname;
+ const publicRead =
+ (request.method === 'GET' || request.method === 'HEAD') &&
+ !request.headers.has('authorization') &&
+ !request.headers.has('cookie') &&
+ path.startsWith('/api/v1/') &&
+ !path.startsWith('/api/v1/account') &&
+ !path.startsWith('/api/v1/admin');
+ const activationExempt =
+ path === '/health/live' ||
+ path === '/health/ready' ||
+ path === '/api/v1/instance' ||
+ publicRead ||
+ request.method === 'OPTIONS';
+ if (!activationExempt) await ensureTierActivated();
+ }
+ return app.fetch(request);
+ },
+ async queue(batch: MessageBatch) {
+ if (!asyncRuntime) throw new Error('Async consumer unavailable');
+ await asyncRuntime.queue(batch);
+ },
+ async scheduled(controller: ScheduledController, bindings: Env) {
+ if (!bindings.DB) throw new Error('Expired cleanup unavailable');
+ const cleanupConfig = loadConfig(bindings, 'cloudflare');
+ if (cleanupConfig.deployment.tier === 'cloudflare-minimum') {
+ await runMinimumAsyncTick({
+ db: bindings.DB,
+ scheduledTime: controller.scheduledTime,
+ bindings: bindings as Env &
+ import('./uploads.ts').WorkerUploadBindings,
+ sessionRetentionMs:
+ cleanupConfig.security.retentionSeconds.expiredSessions * 1000,
+ });
+ return;
+ }
+ await createD1ExpiredCleanupStore(bindings.DB).purgeExpired(
+ Date.now(),
+ cleanupConfig.security.retentionSeconds.expiredSessions * 1000,
+ expiredCleanupBatchSize,
+ );
+ const maintenance = createD1AsyncStore(bindings.DB);
+ const policy = loadAsyncMaintenancePolicy(
+ bindings as {
+ ASYNC_ORPHAN_RETENTION_SECONDS?: string;
+ ASYNC_TERMINAL_RETENTION_SECONDS?: string;
+ },
+ );
+ await maintenance.purgeTerminal(
+ Math.max(0, Date.now() - policy.terminalRetentionMs),
+ 10,
+ );
+ await maintenance.purgeJobs(
+ Math.max(0, Date.now() - policy.terminalRetentionMs),
+ 10,
+ );
+ if (uploads)
+ await runScheduledUploadCleanup(
+ maintenance,
+ uploads,
+ policy.orphanRetentionMs,
+ );
+ const workflow = bindings.ASYNC_WORKFLOW;
+ if (workflow) {
+ const adapter = createCloudflareWorkflowAdapter(
+ d1JobStore(bindings.DB),
+ workflow,
+ );
+ await maintenance.recoverableJobs(Date.now(), 1).then((ids) =>
+ ids.reduce(async (previous, id) => {
+ await previous;
+ await adapter.resume(id);
+ }, Promise.resolve()),
+ );
+ }
+ await asyncRuntime?.dispatch();
+ },
+ } satisfies ExportedHandler;
+}
diff --git a/apps/api-cloudflare/src/index.ts b/apps/api-cloudflare/src/index.ts
new file mode 100644
index 0000000..c73a5c6
--- /dev/null
+++ b/apps/api-cloudflare/src/index.ts
@@ -0,0 +1,14 @@
+import { env } from 'cloudflare:workers';
+import { createCloudflareApi } from './composition.ts';
+import { createCloudflareStandardPasswordService } from './standard-password.ts';
+import { initialStandardPasswordPolicy } from '@hyperbug/security';
+
+export default createCloudflareApi(env, (deployment) =>
+ createCloudflareStandardPasswordService(
+ deployment,
+ initialStandardPasswordPolicy,
+ initialStandardPasswordPolicy.maximum.memoryKiB,
+ ),
+);
+
+export { HyperBugWorkflow } from './workflow.ts';
diff --git a/apps/api-cloudflare/src/ingress-attestation.ts b/apps/api-cloudflare/src/ingress-attestation.ts
new file mode 100644
index 0000000..b8986b8
--- /dev/null
+++ b/apps/api-cloudflare/src/ingress-attestation.ts
@@ -0,0 +1,106 @@
+import { canonicalIpAddress, RateLimitFailure } from '@hyperbug/security';
+
+const addressHeader = 'x-hyperbug-ingress-ip';
+const timeHeader = 'x-hyperbug-ingress-time';
+const signatureHeader = 'x-hyperbug-ingress-signature';
+const maxAgeMs = 5000;
+
+function decode(value: string, length: number): Uint8Array {
+ if (
+ typeof value !== 'string' ||
+ !new RegExp(`^[A-Za-z0-9_-]{${length}}$`).test(value)
+ )
+ throw new RateLimitFailure();
+ try {
+ const bytes = Uint8Array.from(
+ atob(value.replaceAll('-', '+').replaceAll('_', '/') + '='),
+ (character) => character.charCodeAt(0),
+ );
+ if (bytes.length !== 32 || encode(buffer(bytes)) !== value)
+ throw new RateLimitFailure();
+ return bytes;
+ } catch {
+ throw new RateLimitFailure();
+ }
+}
+
+function encode(bytes: ArrayBuffer): string {
+ return btoa(String.fromCharCode(...new Uint8Array(bytes)))
+ .replaceAll('+', '-')
+ .replaceAll('/', '_')
+ .replaceAll('=', '');
+}
+
+function buffer(bytes: Uint8Array): ArrayBuffer {
+ const copy = new Uint8Array(bytes.byteLength);
+ copy.set(bytes);
+ return copy.buffer;
+}
+
+function payload(request: Request, ip: string, issuedAt: string): Uint8Array {
+ const url = new URL(request.url);
+ return new TextEncoder().encode(
+ `hyperbug-ingress-v1\n${request.method}\n${url.pathname}${url.search}\n${ip}\n${issuedAt}`,
+ );
+}
+
+/** Share only with a controlled public ingress Worker and a private API Worker. */
+export function createIngressAttestation(secret: string) {
+ const material = decode(secret, 43);
+ const key = crypto.subtle.importKey(
+ 'raw',
+ buffer(material),
+ { name: 'HMAC', hash: 'SHA-256' },
+ false,
+ ['sign', 'verify'],
+ );
+ return Object.freeze({
+ async sign(request: Request, rawIp: string): Promise {
+ const ip = canonicalIpAddress(rawIp);
+ const issuedAt = String(Date.now());
+ const headers = new Headers(request.headers);
+ headers.delete(addressHeader);
+ headers.delete(timeHeader);
+ headers.delete(signatureHeader);
+ for (const name of [
+ 'cf-connecting-ip',
+ 'x-real-ip',
+ 'true-client-ip',
+ 'x-forwarded-for',
+ 'forwarded',
+ 'cf-worker',
+ ])
+ headers.delete(name);
+ const signature = await crypto.subtle.sign(
+ 'HMAC',
+ await key,
+ buffer(payload(request, ip, issuedAt)),
+ );
+ headers.set(addressHeader, ip);
+ headers.set(timeHeader, issuedAt);
+ headers.set(signatureHeader, encode(signature));
+ return headers;
+ },
+ async verify(request: Request): Promise {
+ const ip = canonicalIpAddress(request.headers.get(addressHeader));
+ const issuedAt = request.headers.get(timeHeader);
+ const signature = request.headers.get(signatureHeader);
+ if (
+ request.signal.aborted ||
+ typeof issuedAt !== 'string' ||
+ !/^\d{13}$/.test(issuedAt) ||
+ Math.abs(Date.now() - Number(issuedAt)) > maxAgeMs ||
+ typeof signature !== 'string'
+ )
+ throw new RateLimitFailure();
+ const valid = await crypto.subtle.verify(
+ 'HMAC',
+ await key,
+ buffer(decode(signature, 43)),
+ buffer(payload(request, ip, issuedAt)),
+ );
+ if (!valid || request.signal.aborted) throw new RateLimitFailure();
+ return ip;
+ },
+ });
+}
diff --git a/apps/api-cloudflare/src/ingress.ts b/apps/api-cloudflare/src/ingress.ts
new file mode 100644
index 0000000..6ca0c92
--- /dev/null
+++ b/apps/api-cloudflare/src/ingress.ts
@@ -0,0 +1,37 @@
+import { env } from 'cloudflare:workers';
+import { canonicalIpAddress } from '@hyperbug/security';
+import { createIngressAttestation } from './ingress-attestation.ts';
+
+/** Public edge entry; API is reachable only through its service binding. */
+const attestation = createIngressAttestation(env.HYPERBUG_INGRESS_KEY);
+const unavailable = () =>
+ new Response(null, {
+ status: 503,
+ headers: {
+ 'cache-control': 'no-store',
+ 'x-content-type-options': 'nosniff',
+ },
+ });
+export default {
+ async fetch(request, gatewayEnv) {
+ try {
+ if (request.headers.has('cf-worker')) return unavailable();
+ const ip = canonicalIpAddress(request.headers.get('cf-connecting-ip'));
+ // Cloudflare substitutes this address for cross-zone Worker subrequests.
+ if (ip === '2a06:98c0:3600::103') return unavailable();
+ // The edge force-sets x-real-ip to the connecting address on this
+ // platform (observed deployed on workers.dev and custom domains;
+ // client-supplied values are overwritten), so a direct client cannot
+ // arrive without it. A same-zone Worker subrequest can still set it
+ // (Cloudflare's header reference), so keep it as a divergence tripwire:
+ // provenance itself is always the canonical cf-connecting-ip.
+ const claimedRealIp = request.headers.get('x-real-ip');
+ if (claimedRealIp !== null && canonicalIpAddress(claimedRealIp) !== ip)
+ return unavailable();
+ const headers = await attestation.sign(request, ip);
+ return await gatewayEnv.API.fetch(new Request(request, { headers }));
+ } catch {
+ return unavailable();
+ }
+ },
+} satisfies ExportedHandler;
diff --git a/apps/api-cloudflare/src/key-provider.ts b/apps/api-cloudflare/src/key-provider.ts
new file mode 100644
index 0000000..93a1382
--- /dev/null
+++ b/apps/api-cloudflare/src/key-provider.ts
@@ -0,0 +1,29 @@
+import {
+ CryptoFailure,
+ SecretKeyProvider,
+ type KeyLifecycle,
+} from '@hyperbug/security';
+
+/** Pass the generated Env's Worker Secret binding, never a vars/config default. */
+export function createWorkerKeyProvider(
+ secretBinding: string,
+ lifecycle: KeyLifecycle,
+ timeoutMs = 1000,
+): SecretKeyProvider {
+ return new SecretKeyProvider(
+ {
+ async read(signal) {
+ if (
+ signal.aborted ||
+ typeof secretBinding !== 'string' ||
+ secretBinding.length === 0 ||
+ secretBinding.length > 16384
+ )
+ throw new CryptoFailure();
+ return secretBinding;
+ },
+ },
+ lifecycle,
+ timeoutMs,
+ );
+}
diff --git a/apps/api-cloudflare/src/minimum-password.ts b/apps/api-cloudflare/src/minimum-password.ts
new file mode 100644
index 0000000..97e457a
--- /dev/null
+++ b/apps/api-cloudflare/src/minimum-password.ts
@@ -0,0 +1,26 @@
+import type { DeploymentConfig } from '@hyperbug/config';
+import {
+ createMinimumPasswordService,
+ CryptoFailure,
+ minimumTierPasswordPolicy,
+ type KeyProvider,
+ type MinimumPasswordPolicy,
+ type MinimumPasswordService,
+} from '@hyperbug/security';
+
+/** Separate opt-in tier; standard paid Workers retain Argon2id. */
+export async function createCloudflareMinimumPasswordService(
+ deployment: DeploymentConfig,
+ provider: KeyProvider,
+ policy: MinimumPasswordPolicy,
+): Promise {
+ if (deployment?.tier !== 'cloudflare-minimum') throw new CryptoFailure();
+ // The measured plan fit is a hard ceiling: a configured policy above it
+ // would exceed the Free per-invocation CPU budget on every verification.
+ if (
+ policy.maximumIterations > minimumTierPasswordPolicy.maximumIterations ||
+ policy.currentIterations > minimumTierPasswordPolicy.maximumIterations
+ )
+ throw new CryptoFailure();
+ return createMinimumPasswordService(provider, policy);
+}
diff --git a/apps/api-cloudflare/src/minimum.ts b/apps/api-cloudflare/src/minimum.ts
new file mode 100644
index 0000000..305bbec
--- /dev/null
+++ b/apps/api-cloudflare/src/minimum.ts
@@ -0,0 +1,5 @@
+import { env } from 'cloudflare:workers';
+import { createCloudflareApi } from './composition.ts';
+
+// No Argon2id runtime or Wasm import enters this profile's dependency graph.
+export default createCloudflareApi(env);
diff --git a/apps/api-cloudflare/src/outbound.ts b/apps/api-cloudflare/src/outbound.ts
new file mode 100644
index 0000000..d25fea9
--- /dev/null
+++ b/apps/api-cloudflare/src/outbound.ts
@@ -0,0 +1,6 @@
+import { createOutboundFetcher, type OutboundLimits } from '@hyperbug/security';
+
+/** Workers egress uses platform fetch under the shared fixed-destination policy. */
+export function createCloudflareOutboundFetcher(limits: OutboundLimits) {
+ return createOutboundFetcher(limits, fetch);
+}
diff --git a/apps/api-cloudflare/src/rate-limit.ts b/apps/api-cloudflare/src/rate-limit.ts
new file mode 100644
index 0000000..10350b1
--- /dev/null
+++ b/apps/api-cloudflare/src/rate-limit.ts
@@ -0,0 +1,26 @@
+import { validVolumetricKey, type VolumetricLimiter } from '@hyperbug/security';
+
+export interface WorkersRateLimitBinding {
+ limit(options: { key: string }): Promise<{ success: boolean }>;
+}
+
+/** Cloudflare's binding is location scoped and eventually consistent. */
+export function createCloudflareVolumetricLimiter(
+ binding: WorkersRateLimitBinding,
+): VolumetricLimiter {
+ return {
+ async consume(digest, nowMs) {
+ if (!validVolumetricKey(digest, nowMs))
+ return { allowed: false, reason: 'unavailable' };
+ try {
+ const outcome = await binding.limit({ key: digest });
+ if (outcome.success === true) return { allowed: true };
+ if (outcome.success === false)
+ return { allowed: false, reason: 'limited' };
+ } catch {
+ // An unavailable limiter never grants the dependent request.
+ }
+ return { allowed: false, reason: 'unavailable' };
+ },
+ };
+}
diff --git a/apps/api-cloudflare/src/standard-password.ts b/apps/api-cloudflare/src/standard-password.ts
new file mode 100644
index 0000000..2c268c4
--- /dev/null
+++ b/apps/api-cloudflare/src/standard-password.ts
@@ -0,0 +1,196 @@
+///
+/* oxlint-disable no-underscore-dangle -- libsodium's raw Wasm exports use leading underscores. */
+import sodiumFactory from 'libsodium-sumo';
+import sodiumWasm from './vendor/libsodium-sumo-0.8.4.wasm';
+import type { DeploymentConfig } from '@hyperbug/config';
+import {
+ createStandardPasswordService,
+ CryptoFailure,
+ snapshotStandardPasswordPolicy,
+ validArgon2idParameters,
+ type Argon2idParameters,
+ type Argon2idProvider,
+ type StandardPasswordPolicy,
+ type StandardPasswordService,
+} from '@hyperbug/security';
+
+const saltBytes = 16;
+const verifierBytes = 32;
+
+type Sodium = Awaited>;
+let sodium: Promise | undefined;
+
+function loadSodium(): Promise {
+ // The factory uses CSPRNG during initialization; Workers forbids this in
+ // global scope, so instantiate on the first request and reuse the instance.
+ return (sodium ??= sodiumFactory({
+ getRandomValue: () => crypto.getRandomValues(new Uint32Array(1))[0]!,
+ instantiateWasm(imports, receive) {
+ const instance = new WebAssembly.Instance(sodiumWasm, imports);
+ receive(instance, sodiumWasm);
+ return instance.exports;
+ },
+ }).then((loaded) => {
+ if (loaded._sodium_init() < 0) throw new CryptoFailure();
+ return loaded;
+ }));
+}
+
+function alloc(loaded: Sodium, bytes: number): number {
+ const pointer = loaded._malloc(bytes);
+ if (!Number.isSafeInteger(pointer) || pointer <= 0) throw new CryptoFailure();
+ return pointer;
+}
+
+function wipeAndFree(loaded: Sodium, pointer: number, bytes: number): void {
+ if (pointer <= 0) return;
+ loaded.HEAPU8.fill(0, pointer, pointer + bytes);
+ loaded._free(pointer);
+}
+
+/** Candidate for paid-capable standard Workers only; no account route is wired. */
+export function createCloudflareArgon2idProvider(
+ maximumMemoryKiB: number,
+): Argon2idProvider {
+ if (
+ !Number.isSafeInteger(maximumMemoryKiB) ||
+ maximumMemoryKiB < 19 * 1024 ||
+ maximumMemoryKiB > 1_048_576
+ )
+ throw new CryptoFailure();
+ let active = false;
+ const derive = async (
+ password: Uint8Array,
+ salt: Uint8Array,
+ parameters: Argon2idParameters,
+ signal?: AbortSignal,
+ ): Promise => {
+ let cost: Argon2idParameters;
+ try {
+ cost = Object.freeze({
+ memoryKiB: parameters.memoryKiB,
+ passes: parameters.passes,
+ parallelism: parameters.parallelism,
+ });
+ } catch {
+ throw new CryptoFailure();
+ }
+ // libsodium's pwhash ABI does not expose a lane-count argument.
+ if (
+ !(password instanceof Uint8Array) ||
+ password.byteLength < 1 ||
+ password.byteLength > 1024 ||
+ !(salt instanceof Uint8Array) ||
+ salt.byteLength !== saltBytes ||
+ !validArgon2idParameters(cost) ||
+ cost.parallelism !== 1 ||
+ cost.memoryKiB > maximumMemoryKiB ||
+ signal?.aborted ||
+ active
+ )
+ throw new CryptoFailure();
+ active = true;
+ const message = Uint8Array.from(password);
+ const nonce = Uint8Array.from(salt);
+ let loaded: Sodium | undefined;
+ let messagePtr = 0;
+ let noncePtr = 0;
+ let outputPtr = 0;
+ try {
+ loaded = await loadSodium();
+ if (signal?.aborted) throw new CryptoFailure();
+ messagePtr = alloc(loaded, message.byteLength);
+ noncePtr = alloc(loaded, saltBytes);
+ outputPtr = alloc(loaded, verifierBytes);
+ loaded.HEAPU8.set(message, messagePtr);
+ loaded.HEAPU8.set(nonce, noncePtr);
+ const result = loaded._crypto_pwhash(
+ outputPtr,
+ verifierBytes,
+ 0,
+ messagePtr,
+ message.byteLength,
+ 0,
+ noncePtr,
+ cost.passes,
+ 0,
+ cost.memoryKiB * 1024,
+ 2,
+ );
+ if (result !== 0 || signal?.aborted) throw new CryptoFailure();
+ return Uint8Array.from(
+ loaded.HEAPU8.subarray(outputPtr, outputPtr + verifierBytes),
+ );
+ } catch {
+ throw new CryptoFailure();
+ } finally {
+ if (loaded) {
+ wipeAndFree(loaded, outputPtr, verifierBytes);
+ wipeAndFree(loaded, noncePtr, saltBytes);
+ wipeAndFree(loaded, messagePtr, message.byteLength);
+ }
+ message.fill(0);
+ nonce.fill(0);
+ active = false;
+ }
+ };
+ return Object.freeze({
+ derive,
+ async matches(
+ password: Uint8Array,
+ salt: Uint8Array,
+ verifier: Uint8Array,
+ parameters: Argon2idParameters,
+ signal?: AbortSignal,
+ ): Promise {
+ if (
+ !(verifier instanceof Uint8Array) ||
+ verifier.byteLength !== verifierBytes
+ )
+ throw new CryptoFailure();
+ const expected = Uint8Array.from(verifier);
+ let derived: Uint8Array | undefined;
+ let loaded: Sodium | undefined;
+ let left = 0;
+ let right = 0;
+ try {
+ derived = await derive(password, salt, parameters, signal);
+ loaded = await loadSodium();
+ if (signal?.aborted) throw new CryptoFailure();
+ left = alloc(loaded, verifierBytes);
+ right = alloc(loaded, verifierBytes);
+ loaded.HEAPU8.set(derived, left);
+ loaded.HEAPU8.set(expected, right);
+ return loaded._crypto_verify_32(left, right) === 0;
+ } catch {
+ throw new CryptoFailure();
+ } finally {
+ if (loaded) {
+ wipeAndFree(loaded, right, verifierBytes);
+ wipeAndFree(loaded, left, verifierBytes);
+ }
+ derived?.fill(0);
+ expected.fill(0);
+ }
+ },
+ });
+}
+
+/** Paid-capable standard Workers only; reject unsupported stored-cost policy. */
+export function createCloudflareStandardPasswordService(
+ deployment: DeploymentConfig,
+ policy: StandardPasswordPolicy,
+ maximumMemoryKiB: number,
+): StandardPasswordService {
+ if (deployment?.tier !== 'standard') throw new CryptoFailure();
+ const selected = snapshotStandardPasswordPolicy(policy);
+ if (
+ selected.maximum.parallelism !== 1 ||
+ selected.maximum.memoryKiB > maximumMemoryKiB
+ )
+ throw new CryptoFailure();
+ return createStandardPasswordService(
+ createCloudflareArgon2idProvider(maximumMemoryKiB),
+ selected,
+ );
+}
diff --git a/apps/api-cloudflare/src/task-queue.ts b/apps/api-cloudflare/src/task-queue.ts
new file mode 100644
index 0000000..f474364
--- /dev/null
+++ b/apps/api-cloudflare/src/task-queue.ts
@@ -0,0 +1,46 @@
+import {
+ ASYNC_LIMITS,
+ AsyncError,
+ validateTaskReference,
+ type TaskQueue,
+ type TaskReference,
+} from '@hyperbug/application';
+import type { Queue, MessageBatch } from '@cloudflare/workers-types';
+
+export function createCloudflareTaskQueue(
+ queue: Queue,
+): TaskQueue {
+ const schedule = async (reference: TaskReference, delayMs: number) => {
+ validateTaskReference(reference);
+ if (
+ !Number.isInteger(delayMs) ||
+ delayMs < 0 ||
+ delayMs > ASYNC_LIMITS.maxDelayMs
+ )
+ throw new AsyncError('invalid');
+ await queue.send(reference, {
+ contentType: 'json',
+ delaySeconds: Math.ceil(delayMs / 1000),
+ });
+ };
+ return { enqueue: (reference) => schedule(reference, 0), schedule };
+}
+export async function consumeCloudflareBatch(
+ batch: MessageBatch,
+ process: (value: unknown) => Promise<'ack' | 'retry'>,
+): Promise {
+ // Sequential within a batch, platform max_concurrency=2; excess is never silently accepted.
+ await batch.messages.reduce(async (previous, message, index) => {
+ await previous;
+ if (index >= ASYNC_LIMITS.batch) {
+ message.retry({ delaySeconds: 30 });
+ return;
+ }
+ try {
+ if ((await process(message.body)) === 'ack') message.ack();
+ else message.retry({ delaySeconds: 30 });
+ } catch {
+ message.retry({ delaySeconds: 30 });
+ }
+ }, Promise.resolve());
+}
diff --git a/apps/api-cloudflare/src/turnstile.ts b/apps/api-cloudflare/src/turnstile.ts
new file mode 100644
index 0000000..e418e1a
--- /dev/null
+++ b/apps/api-cloudflare/src/turnstile.ts
@@ -0,0 +1,13 @@
+import {
+ createTurnstileVerifier,
+ turnstileOutboundLimits,
+} from '@hyperbug/server';
+import { createCloudflareOutboundFetcher } from './outbound.ts';
+
+/** Optional Workers provider using platform fetch under the fixed catalog. */
+export function createCloudflareTurnstileVerifier(secret: string) {
+ return createTurnstileVerifier(
+ secret,
+ createCloudflareOutboundFetcher(turnstileOutboundLimits),
+ );
+}
diff --git a/apps/api-cloudflare/src/uploads.ts b/apps/api-cloudflare/src/uploads.ts
new file mode 100644
index 0000000..666a873
--- /dev/null
+++ b/apps/api-cloudflare/src/uploads.ts
@@ -0,0 +1,75 @@
+import type { D1Database, R2Bucket } from '@cloudflare/workers-types';
+import {
+ createR2BlobStore,
+ createR2BlobAuthorization,
+ createR2BlobDigest,
+ createR2MultipartReconciler,
+ type R2SigningConfig,
+} from '@hyperbug/blob-r2';
+import { createD1UploadIntentStore } from '@hyperbug/database-d1';
+import {
+ defaultUploadQuota,
+ defaultUploadMultipart,
+ validateMultipartPolicy,
+ type BlobMultipartPolicy,
+ type UploadDependencies,
+} from '@hyperbug/application';
+
+export interface WorkerUploadBindings {
+ HYPERBUG_UPLOAD_BLOB?: R2Bucket;
+ HYPERBUG_UPLOAD_STORAGE?: string;
+}
+/** Native CRUD binding plus a private signing secret for the same operator-selected bucket. */
+export function configureWorkerUploads(
+ db: D1Database | null,
+ bindings: WorkerUploadBindings,
+ transport?: (outbound: Request) => Promise,
+): UploadDependencies | null {
+ if (
+ bindings.HYPERBUG_UPLOAD_BLOB === undefined &&
+ bindings.HYPERBUG_UPLOAD_STORAGE === undefined
+ )
+ return null;
+ let phase = 'bindings';
+ try {
+ if (!bindings.HYPERBUG_UPLOAD_STORAGE) throw new Error();
+ phase = 'configuration';
+ const config = JSON.parse(
+ bindings.HYPERBUG_UPLOAD_STORAGE,
+ ) as R2SigningConfig & { multipart?: BlobMultipartPolicy };
+ // Validate static policy now; native bindings may be absent during
+ // upload-time module validation. Acquire them on the first runtime request.
+ phase = 'blob-policy';
+ const multipart = validateMultipartPolicy(
+ config.multipart ?? defaultUploadMultipart,
+ );
+ phase = 'signing';
+ const capabilities = createR2BlobAuthorization(config);
+ phase = 'reconciliation';
+ const multipartReconciler = createR2MultipartReconciler(config, transport);
+ let native: Pick | null = null;
+ const live = () => {
+ if (native) return native;
+ if (!db || !bindings.HYPERBUG_UPLOAD_BLOB)
+ throw new Error('Invalid upload storage configuration (bindings)');
+ native = {
+ intents: createD1UploadIntentStore(db, defaultUploadQuota),
+ blobs: createR2BlobStore(bindings.HYPERBUG_UPLOAD_BLOB, multipart),
+ };
+ return native;
+ };
+ return {
+ get intents() {
+ return live().intents;
+ },
+ get blobs() {
+ return live().blobs;
+ },
+ capabilities,
+ createDigest: createR2BlobDigest,
+ multipartReconciler,
+ };
+ } catch {
+ throw new Error(`Invalid upload storage configuration (${phase})`);
+ }
+}
diff --git a/apps/api-cloudflare/src/vendor.d.ts b/apps/api-cloudflare/src/vendor.d.ts
new file mode 100644
index 0000000..321a3e6
--- /dev/null
+++ b/apps/api-cloudflare/src/vendor.d.ts
@@ -0,0 +1,45 @@
+declare module '*.wasm' {
+ const module: WebAssembly.Module;
+ export default module;
+}
+
+declare namespace Cloudflare {
+ interface Env {
+ TURNSTILE_SECRET?: string;
+ TURNSTILE_SITE_KEY?: string;
+ TURNSTILE_HOSTNAME?: string;
+ }
+}
+
+declare module 'libsodium-sumo' {
+ interface RawSodium {
+ HEAPU8: Uint8Array;
+ _malloc(bytes: number): number;
+ _free(pointer: number): void;
+ _sodium_init(): number;
+ _crypto_pwhash(
+ output: number,
+ outputLength: number,
+ outputLengthHigh: number,
+ password: number,
+ passwordLength: number,
+ passwordLengthHigh: number,
+ salt: number,
+ passes: number,
+ passesHigh: number,
+ memoryBytes: number,
+ algorithm: number,
+ ): number;
+ _crypto_verify_32(left: number, right: number): number;
+ }
+ export default function sodiumFactory(options: {
+ getRandomValue(): number;
+ instantiateWasm(
+ imports: WebAssembly.Imports,
+ receive: (
+ instance: WebAssembly.Instance,
+ module: WebAssembly.Module,
+ ) => void,
+ ): WebAssembly.Exports;
+ }): Promise;
+}
diff --git a/apps/api-cloudflare/src/vendor/libsodium-sumo-0.8.4.wasm b/apps/api-cloudflare/src/vendor/libsodium-sumo-0.8.4.wasm
new file mode 100644
index 0000000..1e7c6e1
Binary files /dev/null and b/apps/api-cloudflare/src/vendor/libsodium-sumo-0.8.4.wasm differ
diff --git a/apps/api-cloudflare/src/vendor/libsodium-sumo-LICENSE.txt b/apps/api-cloudflare/src/vendor/libsodium-sumo-LICENSE.txt
new file mode 100644
index 0000000..1322d77
--- /dev/null
+++ b/apps/api-cloudflare/src/vendor/libsodium-sumo-LICENSE.txt
@@ -0,0 +1,16 @@
+Copyright (c) 2015-2026
+Ahmad Ben Mrad
+Frank Denis
+Ryan Lester
+
+Permission to use, copy, modify, and/or distribute this software for any
+purpose with or without fee is hereby granted, provided that the above
+copyright notice and this permission notice appear in all copies.
+
+THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
diff --git a/apps/api-cloudflare/src/workflow.ts b/apps/api-cloudflare/src/workflow.ts
new file mode 100644
index 0000000..1e17942
--- /dev/null
+++ b/apps/api-cloudflare/src/workflow.ts
@@ -0,0 +1,90 @@
+import {
+ WorkflowEntrypoint,
+ type WorkflowEvent,
+ type WorkflowStep,
+} from 'cloudflare:workers';
+import { createD1AsyncStore } from '@hyperbug/database-d1';
+import {
+ runConformanceStep,
+ validateWorkflowReference,
+ type WorkflowReference,
+ type WorkflowAdapter,
+ type JobStore,
+} from '@hyperbug/application';
+import type { D1Database } from '@cloudflare/workers-types';
+/** Only methods used by the adapter; runtime-generated and package bindings differ in optional newer APIs. */
+export interface WorkflowBinding {
+ create(options: { id: string; params: WorkflowReference }): Promise;
+ get(id: string): Promise<{
+ status(): Promise<{ status: string }>;
+ restart(): Promise;
+ resume(): Promise;
+ terminate(): Promise;
+ }>;
+}
+
+export function d1JobStore(db: D1Database): JobStore {
+ const store = createD1AsyncStore(db);
+ return { ...store, claim: store.claimJob };
+}
+export class HyperBugWorkflow extends WorkflowEntrypoint<
+ { DB: D1Database },
+ WorkflowReference
+> {
+ async run(event: WorkflowEvent, step: WorkflowStep) {
+ const reference = validateWorkflowReference(event.payload),
+ store = d1JobStore(this.env.DB);
+ // Named retry-safe steps; large results belong behind artifact references, never step state.
+ const execute = async (
+ index: number,
+ ): Promise<{ checkpoint: number; terminal: boolean }> => {
+ if (index >= 16) throw new Error('Workflow step ceiling exhausted');
+ const result = await step.do(
+ `checkpoint-${index}`,
+ {
+ retries: { limit: 4, delay: '1 second', backoff: 'exponential' },
+ timeout: '1 second',
+ },
+ () => runConformanceStep(store, reference, Date.now, index),
+ );
+ return result.terminal ? result : execute(index + 1);
+ };
+ return execute(0);
+ }
+}
+export function createCloudflareWorkflowAdapter(
+ store: JobStore,
+ binding: WorkflowBinding,
+): WorkflowAdapter {
+ const start = async (jobId: string, maxSteps: number) => {
+ await store.create(jobId, maxSteps, Date.now());
+ await binding.create({ id: jobId, params: { version: 1, jobId } });
+ };
+ return {
+ start,
+ async resume(jobId) {
+ validateWorkflowReference({ version: 1, jobId });
+ const job = await store.get(jobId);
+ if (!job) throw new Error('Workflow missing');
+ if (job.status === 'completed' || job.status === 'cancelled') return;
+ let instance;
+ try {
+ instance = await binding.get(jobId);
+ } catch {
+ // A fixed ID makes creation safe to retry after an ambiguous get failure.
+ // Creation errors propagate; status/restart errors never trigger creation.
+ await binding.create({ id: jobId, params: { version: 1, jobId } });
+ return;
+ }
+ const status = await instance.status();
+ if (status.status === 'errored' || status.status === 'terminated')
+ await instance.restart();
+ else if (status.status === 'paused') await instance.resume();
+ },
+ async cancel(jobId) {
+ validateWorkflowReference({ version: 1, jobId });
+ await store.cancel(jobId, Date.now());
+ await (await binding.get(jobId)).terminate();
+ },
+ };
+}
diff --git a/apps/api-cloudflare/tsconfig.ingress.json b/apps/api-cloudflare/tsconfig.ingress.json
new file mode 100644
index 0000000..fef7d2e
--- /dev/null
+++ b/apps/api-cloudflare/tsconfig.ingress.json
@@ -0,0 +1,13 @@
+{
+ "extends": "../../tsconfig.json",
+ "compilerOptions": {
+ "lib": ["ES2023"],
+ "types": ["@cloudflare/workers-types"]
+ },
+ "include": [
+ "src/ingress.ts",
+ "src/ingress-attestation.ts",
+ "ingress-configuration.d.ts"
+ ],
+ "exclude": ["worker-configuration.d.ts"]
+}
diff --git a/apps/api-cloudflare/tsconfig.json b/apps/api-cloudflare/tsconfig.json
new file mode 100644
index 0000000..205dec1
--- /dev/null
+++ b/apps/api-cloudflare/tsconfig.json
@@ -0,0 +1,13 @@
+{
+ "extends": "../../tsconfig.json",
+ "compilerOptions": {
+ "lib": ["ES2023"],
+ "types": []
+ },
+ "include": [
+ "src/**/*.ts",
+ "worker-configuration.d.ts",
+ "../../tests/fixtures/async-workflow-worker.ts"
+ ],
+ "exclude": ["src/ingress.ts", "ingress-configuration.d.ts"]
+}
diff --git a/apps/api-cloudflare/worker-configuration.d.ts b/apps/api-cloudflare/worker-configuration.d.ts
new file mode 100644
index 0000000..892afce
--- /dev/null
+++ b/apps/api-cloudflare/worker-configuration.d.ts
@@ -0,0 +1,16114 @@
+/* eslint-disable */
+// Generated by Wrangler by running `wrangler types --config=apps/api-cloudflare/wrangler.jsonc apps/api-cloudflare/worker-configuration.d.ts` (hash: e2165a0d43455aba23ab07c1dec2440c)
+// Runtime types generated with workerd@1.20260926.1 2026-09-16 cache_option_enabled,enable_request_signal,nodejs_compat
+interface __BaseEnv_Env {
+ DB?: D1Database;
+ ABUSE_VOLUMETRIC: RateLimit;
+ HYPERBUG_ENV: "local" | "staging" | "production";
+ ALLOWED_ORIGINS?: "http://localhost:5173";
+ HYPERBUG_DEPLOYMENT_TIER?: "cloudflare-minimum";
+ HYPERBUG_DEGRADATION_ACK?: "minimum-v2";
+ HYPERBUG_KEY_RING: string;
+ HYPERBUG_ABUSE_KEY_RING: string;
+ ASYNC_TASKS?: Queue;
+ TURNSTILE_SECRET?: string;
+ TURNSTILE_SITE_KEY?: string;
+ TURNSTILE_HOSTNAME?: string;
+ ASYNC_WORKFLOW?: Workflow[0]['payload']>;
+ HYPERBUG_INGRESS_KEY?: string;
+}
+declare namespace Cloudflare {
+ interface GlobalProps {
+ mainModule: typeof import("./src/index");
+ }
+ interface MinimumLocalEnv {
+ DB: D1Database;
+ ABUSE_VOLUMETRIC: RateLimit;
+ HYPERBUG_ENV: "local";
+ ALLOWED_ORIGINS: "http://localhost:5173";
+ HYPERBUG_DEPLOYMENT_TIER: "cloudflare-minimum";
+ HYPERBUG_DEGRADATION_ACK: "minimum-v2";
+ HYPERBUG_KEY_RING: string;
+ HYPERBUG_ABUSE_KEY_RING: string;
+ }
+ interface LocalTurnstileEnv {
+ DB: D1Database;
+ ASYNC_TASKS: Queue;
+ ABUSE_VOLUMETRIC: RateLimit;
+ HYPERBUG_ENV: "local";
+ ALLOWED_ORIGINS: "http://localhost:5173";
+ HYPERBUG_KEY_RING: string;
+ HYPERBUG_ABUSE_KEY_RING: string;
+ TURNSTILE_SECRET: string;
+ TURNSTILE_SITE_KEY: string;
+ TURNSTILE_HOSTNAME: string;
+ ASYNC_WORKFLOW: Workflow[0]['payload']>;
+ }
+ interface StagingEnv {
+ ASYNC_TASKS: Queue;
+ ABUSE_VOLUMETRIC: RateLimit;
+ HYPERBUG_ENV: "staging";
+ HYPERBUG_KEY_RING: string;
+ HYPERBUG_ABUSE_KEY_RING: string;
+ HYPERBUG_INGRESS_KEY: string;
+ ASYNC_WORKFLOW: Workflow[0]['payload']>;
+ }
+ interface ProductionEnv {
+ ASYNC_TASKS: Queue;
+ ABUSE_VOLUMETRIC: RateLimit;
+ HYPERBUG_ENV: "production";
+ HYPERBUG_KEY_RING: string;
+ HYPERBUG_ABUSE_KEY_RING: string;
+ HYPERBUG_INGRESS_KEY: string;
+ ASYNC_WORKFLOW: Workflow[0]['payload']>;
+ }
+ interface Env extends __BaseEnv_Env {}
+}
+interface Env extends __BaseEnv_Env {}
+type StringifyValues> = {
+ [Binding in keyof EnvType]: EnvType[Binding] extends string ? EnvType[Binding] : string;
+};
+declare namespace NodeJS {
+ interface ProcessEnv extends StringifyValues> {}
+}
+
+// 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. This is how error conditions are described in web APIs.
+ *
+ * [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 DOMException interface returns a string representing 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 DOMException interface returns a string that contains 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().
+ *
+ * [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. The message is only displayed to the user if the console is configured to display debug output. In most cases, the log level is configured within the console UI. This log level might correspond to the Debug or Verbose 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. In browser consoles, the output is presented as a hierarchical listing with disclosure triangles that let you see the contents of child objects.
+ *
+ * [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. If it is not possible to display as an element the JavaScript Object view is shown instead. The output is presented as a hierarchical listing of expandable nodes that let you see the contents of child nodes.
+ *
+ * [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. The message is only displayed to the user if the console is configured to display error output. In most cases, the log level is configured within the console UI. The message may be formatted as an error, with red colors and call stack information.
+ *
+ * [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() 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. Unlike console.group(), however, the new group is created collapsed. The user will need to use the disclosure button next to it to expand it, revealing the entries created in the group.
+ *
+ * [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. See Using groups in the console in the console documentation for details and examples.
+ *
+ * [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. The message is only displayed to the user if the console is configured to display info output. In most cases, the log level is configured within the console UI. The message may receive special formatting, such as a small "i" icon next to it.
+ *
+ * [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. You give each timer a unique name, and may have up to 10,000 timers running on a given page. When you call console.timeEnd() with the same name, the browser will output the time, in milliseconds, that elapsed since the timer was started.
+ *
+ * [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().
+ *
+ * [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().
+ *
+ * [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/timeLog_static)
+ */
+ timeLog(label?: string, ...data: any[]): void;
+ /* The **`console.timeStamp()`** static method adds a single marker to the browser's Performance tool (Firefox bug 1387528, Chrome). This lets you correlate a point in your code with the other events recorded in the timeline, such as layout and paint events. */
+ 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. The message is only displayed to the user if the console is configured to display warning output. In most cases, the log level is configured within the console UI. The message may receive special formatting, such as yellow colors and a warning icon.
+ *
+ * [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. The normal event processing rules (including the capturing and optional bubbling phase) also apply to events dispatched manually with dispatchEvent().
+ *
+ * [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;
+/* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Window/scheduler) */
+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;
+ abort(reason?: any): void;
+}
+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 | undefined | 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, options?: DurableObjectAbortOptions): void;
+}
+interface DurableObjectTransaction {
+ get(key: string, options?: DurableObjectGetOptions): Promise;
+ get(keys: string[], options?: DurableObjectGetOptions): Promise