diff --git a/.changeset/bright-otters-sleep.md b/.changeset/bright-otters-sleep.md new file mode 100644 index 0000000000..e0854fc583 --- /dev/null +++ b/.changeset/bright-otters-sleep.md @@ -0,0 +1,5 @@ +--- +"@emdash-cms/registry-lexicons": minor +--- + +Adds experimental package profile policy and release provenance lexicon contracts. diff --git a/.changeset/calm-access-rules.md b/.changeset/calm-access-rules.md new file mode 100644 index 0000000000..551af698c4 --- /dev/null +++ b/.changeset/calm-access-rules.md @@ -0,0 +1,5 @@ +--- +"@emdash-cms/plugin-types": minor +--- + +Adds canonical declared-access comparison, structured escalation diffs, and stable digest input generation. diff --git a/.changeset/canonical-bundle-verification.md b/.changeset/canonical-bundle-verification.md new file mode 100644 index 0000000000..9127fc44ea --- /dev/null +++ b/.changeset/canonical-bundle-verification.md @@ -0,0 +1,5 @@ +--- +"@emdash-cms/registry-verification": minor +--- + +Adds canonical, runtime-neutral validation for plugin bundle archives and exports their security limits. diff --git a/.changeset/cli-canonical-bundle-validation.md b/.changeset/cli-canonical-bundle-validation.md new file mode 100644 index 0000000000..3e6f3eaacc --- /dev/null +++ b/.changeset/cli-canonical-bundle-validation.md @@ -0,0 +1,5 @@ +--- +"@emdash-cms/plugin-cli": patch +--- + +Rejects unsafe or malformed plugin bundles before publishing with consistent archive and manifest validation. diff --git a/.changeset/core-canonical-bundle-validation.md b/.changeset/core-canonical-bundle-validation.md new file mode 100644 index 0000000000..66a5ba149e --- /dev/null +++ b/.changeset/core-canonical-bundle-validation.md @@ -0,0 +1,5 @@ +--- +"emdash": patch +--- + +Rejects unsafe, mismatched, or non-canonical marketplace plugin bundles during installation, including archives with links or extended headers that older parsing may have ignored. diff --git a/.changeset/create-only-delegated-releases.md b/.changeset/create-only-delegated-releases.md new file mode 100644 index 0000000000..8cdf3cf241 --- /dev/null +++ b/.changeset/create-only-delegated-releases.md @@ -0,0 +1,6 @@ +--- +"@emdash-cms/registry-client": minor +"@emdash-cms/registry-lexicons": minor +--- + +Adds the exact create-only delegated release scope and a narrow API for publishing immutable package releases. diff --git a/.changeset/direct-pds-record-verification.md b/.changeset/direct-pds-record-verification.md new file mode 100644 index 0000000000..3f46ee8cdb --- /dev/null +++ b/.changeset/direct-pds-record-verification.md @@ -0,0 +1,6 @@ +--- +"@emdash-cms/registry-client": minor +"@emdash-cms/registry-verification": minor +--- + +Adds authoritative direct-PDS package reads and shared signed record, policy, and provenance verification reports. diff --git a/.changeset/lucky-verifiers-float.md b/.changeset/lucky-verifiers-float.md new file mode 100644 index 0000000000..efff56821b --- /dev/null +++ b/.changeset/lucky-verifiers-float.md @@ -0,0 +1,5 @@ +--- +"@emdash-cms/registry-verification": minor +--- + +Adds shared registry checksum verification and bounded HTTPS resource fetching primitives. diff --git a/.changeset/passkey-uv-context.md b/.changeset/passkey-uv-context.md new file mode 100644 index 0000000000..89d303b1f8 --- /dev/null +++ b/.changeset/passkey-uv-context.md @@ -0,0 +1,5 @@ +--- +"@emdash-cms/auth": minor +--- + +Adds configurable passkey user verification and typed, versioned WebAuthn challenge context while preserving preferred verification by default. diff --git a/.changeset/preserve-profile-extensions.md b/.changeset/preserve-profile-extensions.md new file mode 100644 index 0000000000..e978e5184b --- /dev/null +++ b/.changeset/preserve-profile-extensions.md @@ -0,0 +1,5 @@ +--- +"@emdash-cms/plugin-cli": patch +--- + +Fixes subsequent `emdash-plugin publish` commands removing existing package-profile extensions. diff --git a/.changeset/profile-policy-editor.md b/.changeset/profile-policy-editor.md new file mode 100644 index 0000000000..40ec086ce9 --- /dev/null +++ b/.changeset/profile-policy-editor.md @@ -0,0 +1,5 @@ +--- +"@emdash-cms/plugin-cli": minor +--- + +Adds `emdash-plugin policy set` for editing an existing package's signed release policy. diff --git a/.changeset/provenance-verification.md b/.changeset/provenance-verification.md new file mode 100644 index 0000000000..0133c46c22 --- /dev/null +++ b/.changeset/provenance-verification.md @@ -0,0 +1,5 @@ +--- +"@emdash-cms/registry-verification": minor +--- + +Adds offline GitHub Actions Sigstore SLSA v1 provenance verification for Node and Cloudflare Workers. diff --git a/.changeset/shared-plugin-manifest-schema.md b/.changeset/shared-plugin-manifest-schema.md new file mode 100644 index 0000000000..7c443d40ab --- /dev/null +++ b/.changeset/shared-plugin-manifest-schema.md @@ -0,0 +1,5 @@ +--- +"@emdash-cms/plugin-types": minor +--- + +Adds the authoritative plugin manifest schema and access reconciliation helpers to the shared manifest contract. diff --git a/.changeset/verifier-fetch-entry.md b/.changeset/verifier-fetch-entry.md new file mode 100644 index 0000000000..a1d36bd7e4 --- /dev/null +++ b/.changeset/verifier-fetch-entry.md @@ -0,0 +1,5 @@ +--- +"@emdash-cms/registry-verification": minor +--- + +Adds a Worker-safe safe-fetch entry point for isolated resource verification. diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3bc2cb39ca..a42ae5f3bc 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -124,6 +124,8 @@ jobs: - run: pnpm test:unit env: EMDASH_TEST_PG: postgres://postgres:test@localhost:5432/emdash_test + - run: pnpm --filter @emdash-cms/release-service typecheck + - run: pnpm --filter @emdash-cms/release-service test # Render tests use the Astro Vite plugin (vitest.repro.config.ts); # they can't run under the plain-node config in test:unit. - run: pnpm --filter emdash exec vitest run --config vitest.repro.config.ts diff --git a/.gitignore b/.gitignore index 5e4e771b8b..d755b20bf2 100644 --- a/.gitignore +++ b/.gitignore @@ -75,6 +75,10 @@ examples/wp-theme-unit-test/ # pattern so we can re-include the agents subdirectory. .opencode/* !.opencode/agents/ +!.opencode/plans/ +.opencode/plans/* +!.opencode/plans/delegated-release-service/ +!.opencode/plans/delegated-release-service/** # .claude is local-only EXCEPT for the symlinks pointing at AGENTS.md and skills/ .claude/* diff --git a/.opencode/plans/delegated-release-service/implementation-plan.md b/.opencode/plans/delegated-release-service/implementation-plan.md new file mode 100644 index 0000000000..1286b1f822 --- /dev/null +++ b/.opencode/plans/delegated-release-service/implementation-plan.md @@ -0,0 +1,1134 @@ +# Delegated Release Service Implementation Plan + +Companion: [Implementation spec](./spec.md) + +Status: implementation in progress; deployed-PDS validation is deferred to `W12.7`, and history feasibility validation remains open + +This plan turns the delegated release service spec into independently deliverable workstreams. It defines ownership boundaries, dependencies, integration gates, and completion criteria. It intentionally contains no time estimates. + +## Stage Deliverables + +| Stage | Deliverable | Repository change allowed | +| ------- | ---------------------------------------------------------------------------- | ----------------------------------------------------------------- | +| Gate 0A | Complete: confidential OAuth custody feasibility | Spec and plan updates only | +| Gate 0B | History feasibility: event-specific, verifiable aggregator input | Spec and plan updates only | +| Gate 1 | Experimental lexicons, generated types, and one shared verification contract | Production contract code and tests | +| Gate 2 | Secure delegated release service vertical slice | Service, client, and installer code and tests | +| Gate 3 | Independent install and minimum discovery enforcement | Installer and aggregator code and tests | +| Gate 4 | Hosted beta product and operational readiness | Console, notifications, tooling, operations, and conformance code | +| Gate 5 | Accurate historical policy enforcement and production launch evidence | Aggregator history implementation and production verification | + +Gates 0A and 0B are deliberately not implementation stages. RFC #1870 is the product and protocol decision; these gates record only implementation clarifications and external validation. Gate 0A is complete. Deployed-PDS compatibility remains required by conformance and production smoke but does not block implementation. Gate 0B blocks historical aggregator enforcement and production launch, but does not block shared verification, installer work, or the service. Do not reopen RFC decisions unless the text is ambiguous, contradicts an existing constraint, or an external result makes it impossible. Do not add repository test harnesses, prototype services, package dependencies, root scripts, or CI wiring for these gates. Commit concise conclusions directly to the integration branch. + +## Implementation Baseline + +The integration branch includes these completed merge units: + +| Work items | Evidence | Result | +| -------------------------- | -------- | ---------------------------------------------------------------------- | +| `W0.1`, `W0.2`, `W0.7` | #1915 | RFC acceptance criteria and `emdash-plugin` command decision recorded. | +| `W1.1` through `W1.3` | #1918 | Experimental profile and release contracts and generated types landed. | +| `W1.4` | #1920 | Existing profile writers preserve extensions. | +| `W1.5` | #1925 | Local profile-policy command landed. | +| `W2.1`, `W2.2` foundations | #1929 | Shared verification package, checksums, and safe fetching landed. | +| `W2.3` | #1932 | Canonical plugin bundle validation landed. | +| `W2.4` | #1937 | Declared-access canonicalization and escalation landed. | +| `W0.5` | #1943 | Public Sigstore verification feasibility in workerd proved. | +| `W2.5` | #1951 | Production public Sigstore provenance verification landed. | +| `W0.4` | Direct | Confidential OAuth custody and refresh are compatible with workerd. | + +`W0.6` remains external-validation work. Deployed-PDS compatibility from `W0.3` moves to conformance and production smoke. The next shared-verification merge unit is combined `W2.6` and `W2.7`; `W1.6` and `W1.7` land together to establish the exact supported scope contract. + +## Outcomes + +The implementation is complete when: + +1. A publisher can establish an atproto OAuth grant restricted to create-only release records. +2. GitHub Actions can submit an attested release without storing an atproto credential. +3. The service validates the artifact, manifest access, provenance, source, workflow, and signed profile policy. +4. Releases requiring confirmation are held until a currently authorized approver uses a previously enrolled, UV-capable passkey. +5. The service publishes exactly one immutable release record and reconciles ambiguous PDS responses. +6. An EmDash installer independently repeats integrity, provenance, and policy verification. +7. The aggregator records policy history, verifies provenance, and excludes policy-violating releases from default discovery. +8. Publishers can manage delegation, workflow policies, approvers, notification endpoints, and audit history through the web console. +9. The hosted service and a fresh Workers/D1 self-host pass the same conformance suite. + +## Execution Rules + +- Protocol and security semantics live in shared packages, not independently in the service, installer, and aggregator. +- Every workstream lands tests with its implementation. Tests are not a cleanup phase. +- New public record fields remain optional until the stable namespace migration. +- No component accepts `transition:generic` as a fallback for delegated publishing. +- No implementation path may overwrite a release record. +- Every asynchronous consumer is idempotent before it is connected to a real queue. +- User-facing console work uses Kumo, Lingui, and RTL-safe layout from its first PR. +- Public beta may begin after Gate 4. Production launch remains blocked on accurate aggregator policy history in Gate 5. +- Work behind an incomplete integration must be unreachable by default, not merely undocumented. + +## Dependency Model + +### Workstream IDs + +| ID | Workstream | Primary output | +| ----- | ------------------------------------- | ----------------------------------------------------------- | +| `W0` | Clarification and external validation | RFC-derived acceptance criteria and platform assumptions | +| `W1` | Protocol and lexicons | Profile policy and release provenance records | +| `W2` | Shared verification | One verification implementation for all consumers | +| `W3` | OAuth, crypto, and passkeys | Secure identity, grant custody, and approval primitives | +| `W4` | Workload identity | GitHub OIDC verification and typed workflow policies | +| `W5` | Service persistence and orchestration | D1 state machine, queues, publication, reconciliation | +| `W6` | Publisher and approver console | Full management and approval UI | +| `W7` | Notifications | Email, signed webhooks, outbox delivery | +| `W8` | CLI and GitHub Action | Author and CI clients | +| `W9` | Installer enforcement | Independent install-time verification | +| `W10` | Aggregator enforcement | Historical policy and discovery filtering | +| `W11` | Operations and self-hosting | Production config, observability, abuse controls, runbooks | +| `W12` | Conformance and security | Cross-component, browser, adversarial, and production tests | + +### High-Level Graph + +```mermaid +flowchart TD + W0A[W0A Service feasibility] + W0B[W0B History feasibility] + W1[W1 Protocol and lexicons] + W2[W2 Shared verification] + W3[W3 OAuth, crypto, passkeys] + W4[W4 GitHub workload identity] + W5[W5 Service orchestration] + W6[W6 Web console] + W7[W7 Notifications] + W8[W8 CLI and GitHub Action] + W9[W9 Installer enforcement] + W10M[W10 Minimum aggregator enforcement] + W10H[W10 Historical aggregator enforcement] + W11[W11 Operations and self-hosting] + W12[W12 Conformance and security] + + W0A --> W1 + W0A --> W3 + W0B --> W10H + W1 --> W2 + W1 --> W5 + W1 --> W8 + W1 --> W9 + W1 --> W10M + W2 --> W5 + W2 --> W9 + W2 --> W10M + W3 --> W5 + W3 --> W6 + W4 --> W5 + W4 --> W6 + W5 --> W6 + W5 --> W7 + W5 --> W8 + W5 --> W11 + W7 --> W6 + W8 --> W12 + W9 --> W12 + W10M --> W10H + W10M --> W12 + W10H --> W12 + W11 --> W12 +``` + +### Critical Path + +```text +Service path: W1 -> W2 -> W5 -> W9 -> W12 +Launch-history path: W0B -> W10 -> W12 +``` + +Historical aggregator work depends on Gate 0B. Service, installer, and most API/UI work proceed independently once their code dependencies are satisfied. + +## Integration Gates + +### Gate 0A: OAuth Custody Feasibility + +Complete: + +- Confidential OAuth sessions can be restored and refreshed safely in workerd. +- Persistence, refresh locking, nonce handling, and client-key rotation constraints are recorded in `W0.4`. + +Gate owner: `W0`. + +Deployed-PDS create-only compatibility is validated by the conformance suite and production smoke before support is claimed. Failure removes that PDS from the support matrix or changes the RFC; it never adds a broad-scope fallback. + +### Gate 0B: Historical Ingest Feasibility + +Required before `W10.1` and production historical-policy claims: + +- An aggregator event source can recover intermediate profile values with verifiable ordering. +- The source supplies event-specific record values, CIDs, revisions, ordering keys, and proof material sufficient for the selected trust model. + +Gate owner: `W0`. + +Gate 0B evidence is a source-selection decision and explicit `W10.1` constraints. Failure changes the RFC's historical-policy and cooldown guarantees before `W10.1` through `W10.3` and `W10.5` through `W10.7` proceed; it does not block minimum current-policy filtering in `W10.4` or invalidate the service or installer architecture. + +### Gate 1: Protocol and Verification Foundation + +- New profile and release extension fixtures round-trip through generated lexicon types. +- Existing profile writers preserve unknown and policy extensions. +- Shared checksum, fetch, bundle, access-diff, and provenance tests pass in Node and workerd. +- The active release collection and exact create-only scope are exposed by one typed contract. +- One narrow registry-client helper can create, but cannot update or delete, a delegated release. +- Direct PDS reads and record/policy verification produce one structured report and stable error codes for every consumer. +- Installer, service, and aggregator can consume the same fixture corpus and report contract. + +Gate owners: `W1`, `W2`. + +### Gate 2: Secure Service Core + +- A publisher can establish and revoke an exact create-only delegation. +- GitHub OIDC submission creates one D1 intent and survives Queue redelivery. +- Automatic and passkey-approved paths publish through the same final verification function. +- Ambiguous PDS writes reconcile exact match, confirmed absence, and conflict. +- No profile write or release overwrite operation exists in the retained-session path. + +Gate owners: `W3`, `W4`, `W5`. + +### Gate 3: Independent Consumer Enforcement + +- A clean EmDash site independently blocks absent-required, failed, and unverifiable provenance. +- The installer does not trust release-service or aggregator verification status. +- The aggregator marks unsupported or unverified policy state and excludes it from default discovery. + +Gate owners: `W9`, minimum `W10`. + +### Gate 4: Hosted Beta Readiness + +- Full console, multiple passkeys, email, webhooks, audit, recovery, and delegation health work without operator database edits. +- Official CLI and GitHub Action pass the service conformance suite. +- Abuse limits, encryption rotation, alerts, backup/export, and self-hosting docs are complete. +- External security review has no unresolved critical or high findings. + +Gate owners: `W6`, `W7`, `W8`, `W11`, `W12`. + +### Gate 5: Production Registry Launch + +- Aggregator policy-at-publication history is accurate under rapid profile changes, event reordering, queue delay, and replay. +- Downgrade cooldown and notification behavior pass conformance tests. +- Default discovery cannot recommend a release whose required provenance is pending, invalid, or unverifiable. +- End-to-end production smoke succeeds from real GitHub OIDC through PDS, aggregator, and clean-site installation. + +Gate owners: `W10`, `W12`. + +## Workstream W0: Decisions and Feasibility + +This workstream closes the implementation blockers in the spec. Its outputs are RFC-derived acceptance criteria and concise research conclusions. It does not land product code, test fixtures, package dependencies, or internal prototypes. + +### `W0.1` Record the profile contract acceptance criteria + +Extract from RFC #1870: + +- Exact profile extension NSID. +- Exact location and canonical form of the signed repository URL. +- Release-policy object shape and defaults. +- Provenance reference shape. +- Stable treatment of unknown provenance predicates. +- Experimental-to-stable NSID migration consequences for OAuth grants. + +Output: an implementation acceptance table and matching JSON examples. Do not reopen the RFC decision unless its text is contradictory or ambiguous. + +Dependencies: none. + +### `W0.2` Record escalation acceptance criteria + +Extract from RFC #1870: + +- Highest-semver current release as the baseline. +- First release uses empty access. +- Out-of-order publication behavior. +- `allowedHosts` containment rules. +- Unknown constraint changes are conservatively escalating. +- Which baseline changes invalidate an approval. + +Output: an implementation acceptance table consumed directly by `W2.4` tests. Do not reopen the RFC decision unless its text is contradictory or ambiguous. + +Dependencies: none. + +### `W0.3` Validate deployed-PDS create-only support + +Validate externally, against the candidate PDS implementations: + +- `atproto repo:?action=create` authorization. +- Successful release create. +- Rejected update, delete, profile create/update, and unrelated collection writes. +- Revocation endpoint behavior. +- Key-removal behavior before and after access-token expiry. + +Targets: Bluesky-hosted PDS and at least one alternative implementation intended for support. + +Output: a supported-PDS compatibility matrix and any required RFC/spec correction. This validation now runs with `W12.7` conformance and production smoke rather than blocking implementation. Keep disposable clients and accounts outside this repository. + +Dependencies: `W0.1` draft NSID. + +### `W0.4` Prove confidential OAuth custody in workerd + +Research the real `@atcute/oauth-node-client` behavior needed by the future service: + +- Private-key JWT and published JWKS. +- Separate client assertion and DPoP keys. +- D1-backed session restore. +- Nonce retry. +- Concurrent refresh attempts under a D1 lease. +- Rotating refresh tokens and client assertion keys. + +Output: exact persisted session requirements, lock expectations, key-rotation constraints, and any incompatible upstream behavior. Commit only a concise spec/plan update when the result changes the design. + +Result: complete against `@atcute/oauth-node-client@2.0.1`. Private-key JWT, public JWKS derivation, separate assertion and DPoP keys, and nonce retry run in workerd. Persist `StoredSession` (`tokenSet`, `authMethod`, and `dpopKey`) and short-lived authorization state in D1. DPoP nonce and metadata caches may remain isolate-local; atcute's replayable token requests retry one recognized nonce challenge after a cache miss. + +Supply a D1-lease-backed `requestLock` because the package's in-memory coalescing is isolate-local. Before network I/O, atomically move the delegation from `active` to `refreshing` with a unique lease owner. Hold or renew that lease through load, refresh, and persistence. A stale `refreshing` row, lost lease, or post-refresh persistence failure transitions only to `reauthorization_required`; it never retries or leases the old token generation again. + +Retain every client assertion private key referenced by an active session or unexpired authorization transaction. The package records the `kid`, but a missing key raises a generic error without deleting a session. Publish the new public key and wait for cache propagation before selecting it for new grants. The service checks the recorded key ID before callback, restore, and refresh, and transitions unavailable-key state to reauthorization before removing an old key. + +Dependencies: none. + +### `W0.5` Prove Sigstore verification in workerd + +Inspect a real `actions/attest-build-provenance` bundle and validate, outside this repository: + +- Sigstore signature and transparency evidence. +- Artifact subject digest. +- Repository ID and URL. +- Commit SHA and ref. +- Workflow identity and SLSA builder fields. +- RFC `sourceRepository` and `builderId` mapping. + +Output: a Workers-compatible verifier choice and exact field-mapping contract. Commit only the resulting spec/plan decision; fixture acquisition and experimentation remain external until `W2.5` implements the verifier. + +Dependencies: `W0.1` provenance draft. + +### `W0.6` Prove historical aggregator input + +Determine whether an event source can recover profile states `strict -> relaxed -> strict`, with a release between transitions, after queue delay. The selected source must provide event-specific record values, ordering keys, CIDs, revisions, and verifiable commit proof material. + +Output: a source-selection decision and explicit W10.1 constraints. If no source can provide this, return to the RFC before implementing cooldown semantics. Do not add an aggregator prototype to this repository. + +Dependencies: none. + +### `W0.7` Decide public CLI shape + +Use `emdash-plugin` as the v1 public command. A future `emdash plugin` alias is additive work, not a Gate 0 blocker. + +Output: update the RFC examples and implementation documentation to use `emdash-plugin`. + +Dependencies: none. + +### W0 Completion + +The RFC-derived work (`W0.1`, `W0.2`, `W0.5`, and `W0.7`) and OAuth custody validation (`W0.4`) are complete, so Gate 0A is complete. `W0.3` is deferred to `W12.7`. Gate 0B passes independently when `W0.6` selects a viable historical event source. An incompatible deployed-PDS result changes the support matrix or affected RFC guarantee before support is claimed or production launches. + +## Workstream W1: Protocol and Lexicons + +### `W1.1` Add package profile extension + +Files: + +- `packages/registry-lexicons/lexicons/com/emdashcms/experimental/package/profile.json` +- New `profileExtension.json` +- Generated exports and types. + +Deliver: + +- Optional `extensions` container. +- Signed repository anchor. +- `requireProvenance`, `confirmation`, and `approvers`. +- Semantic validation for DID uniqueness, policy values, and URL canonicalization. + +Dependencies: `W0.1`. + +### `W1.2` Add release provenance + +Extend `releaseExtension.json` with the ratified provenance reference and add semantic validation for predicate, URL, checksum, source, and builder. + +Dependencies: `W0.1`, `W0.5` field mapping. + +### `W1.3` Regenerate and publish typed contracts + +- Regenerate atcute types. +- Export value and type symbols from the package barrel. +- Add valid, absent-policy, unknown-predicate, and invalid fixture tests. +- Document experimental/stable NSID lookup through exported constants. + +Dependencies: `W1.1`, `W1.2`. + +### `W1.4` Preserve extensions in all profile writers + +Update interactive publish and profile update paths, especially: + +- `ProfileInput`. +- `buildProfileRecord`. +- `stampLastUpdated`. +- Profile update validation and CLI serialization. + +Regression test: write strict policy, run a later ordinary interactive publish, and verify the extension survives unchanged. + +Dependencies: `W1.1`. + +### `W1.5` Add profile-policy editing to the local CLI core + +Provide a profile-scoped, interactive-only operation that: + +- Fetches and validates the current profile. +- Preserves unrelated extension data. +- Applies one policy edit. +- Uses `swapRecord` with the fetched CID. +- Never shares its OAuth session with the release service. + +Dependencies: `W1.3`, `W1.4`. + +### `W1.6` Update permission-set and namespace migration contracts + +- Publish or update the experimental release permission set if applicable. +- Document that stable NSID migration requires reauthorization. +- Add a typed helper that returns the active release collection and scope string. + +Dependencies: `W1.3`. + +Merge boundary: land with `W1.7` so the exact scope contract is proved by the only API allowed to exercise it. + +### `W1.7` Add create-only release publishing helper + +Extend `registry-client` with a narrow delegated-release helper that: + +- Constructs and validates the deterministic `:` rkey. +- Serializes the canonical release record from verified inputs. +- Performs exactly one create through `createRecord` or a single-create `applyWrites` call. +- Exposes no update, delete, profile write, `putRecord`, or overwrite option. +- Returns the AT URI and CID needed for reconciliation. + +Dependencies: `W1.3`, `W1.6`. + +### W1 Completion + +The protocol can represent every signed fact required by the service and installer, and existing tools cannot accidentally strip policy. + +## Workstream W2: Shared Verification + +Create `packages/registry-verification` and make its APIs usable in Workers and Node. + +### `W2.1` Package scaffold and fixture corpus + +- Add package build, exports, tests, and workerd compatibility job. +- Establish canonical fixture directories for records, tarballs, checksums, and Sigstore bundles. +- Define stable verification error codes shared by all consumers. + +Dependencies: none; complete. + +### `W2.2` Checksums and safe resource fetching + +Extract and reconcile existing checksum behavior. Add manual redirects, byte/time limits, URL validation, DNS defense-in-depth, and injectable test transport. The dedicated verifier-Worker deployment boundary belongs to `W5.1a`; this package provides the transport-neutral safe-fetch primitive it invokes. + +Dependencies: `W2.1`. + +### `W2.3` Canonical plugin bundle validation + +Unify CLI and core tar readers. Reject traversal, duplicate normalized paths, links, devices, duplicate manifests, gzip bombs, and limit violations. Return the validated manifest and canonical access. + +Dependencies: `W2.1`. + +### `W2.4` Declared-access canonicalization and escalation + +Implement in `packages/plugin-types/src/declared-access.ts`: + +- Canonical form. +- Equality. +- Structured diff. +- Escalation predicate. +- Stable digest input. + +Drive implementation from the ratified `W0.2` table. + +Dependencies: `W0.2`, `W1.3`. + +### `W2.5` Provenance verification + +Implement the `ProvenanceVerifier` interface and GitHub/SLSA v1 adapter proved in `W0.5`. Include trust-root update strategy and no partial-success state. + +Dependencies: `W0.5`, `W1.2`, `W2.2`. + +### `W2.6` Record and policy verification + +Add helpers that: + +- Validate profile and release lexicons. +- Normalize absent policy defaults. +- Match release package/rkey/version. +- Resolve the signed repository anchor. +- Verify required/optional/failed provenance semantics. +- Produce a structured verification report suitable for console, installer, and aggregator. + +Dependencies: `W1.3`, `W2.2`, `W2.4`, `W2.5`. + +Merge boundary: land with `W2.7`; the report contract is incomplete without authoritative direct-PDS inputs. + +### `W2.7` Direct PDS read helpers + +Extend `registry-client` with unauthenticated direct-PDS profile/release reads, bounded rkey enumeration, lexicon validation, and semver baseline selection. Do not route these helpers through the aggregator. + +Dependencies: `W1.3`, `W0.2`. + +### W2 Completion + +Given the same profile, release, artifact, and provenance fixtures, service, installer, and aggregator receive the same verification result and error code. + +## Workstream W3: OAuth, Crypto, and Passkeys + +### `W3.1` Service encryption + +Implement versioned AES-GCM envelope encryption with HKDF-derived purpose keys and associated row identity. Cover OAuth session blobs, DPoP keys, emails, webhook destinations, and webhook secrets. + +Dependencies: none. + +### `W3.2` Confidential client metadata and JWKS + +Serve stable metadata and JWKS routes, support overlapping assertion keys, and validate deployment-derived client ID, redirects, scope declaration, and public origin. + +Dependencies: `W0.4`, `W1.6` for final scope. + +### `W3.3` D1 OAuth stores + +Implement separate logical stores for: + +- Console identity. +- Approver identity proof. +- Durable release delegation. + +Persist only the release delegation after callback. Encrypt all sensitive state. + +Dependencies: `W3.1`, `W3.2`. + +### `W3.4` Session refresh coordination + +Implement `PublisherCoordinator` with D1 leases, rotated-session CAS persistence, proactive refresh, jitter, reauthorization state, revocation, and ambiguous refresh recovery. + +Dependencies: `W0.4`, `W3.3`, `W5.2a`. + +### `W3.5` Console and approver identity sessions + +- Convert successful `atproto` OAuth into short-lived, hashed, same-origin service sessions. +- Delete unneeded OAuth session material. +- Bind approver identity proof to invitation or requested DID. +- Add CSRF and session rotation. + +Dependencies: `W3.3`, `W5.1` app scaffold. + +### `W3.6` Required-UV passkey primitives + +Extend `packages/auth` additively with configurable user verification and typed challenge context. Preserve existing CMS behavior by default. + +Dependencies: none. + +### `W3.7` Service passkey repository and ceremonies + +- Multiple named credentials per DID. +- OAuth-before-registration. +- Required UV. +- Bound approval/rejection challenges. +- Atomic challenge consumption and counter update. +- Individual revocation requires fresh atproto identity proof and, when another active credential exists, an assertion from another credential. +- Last-credential recovery may proceed from fresh atproto proof alone, but emits a high-severity audit event and notifications to every affected publisher. +- Define minimal versioned credential-security events for enrolment, credential addition/removal, and recovery; write audit and outbox rows transactionally with each ceremony. + +Dependencies: `W3.5`, `W3.6`. + +Merge boundary: land with `W5.2c`; the repository is part of this service vertical, not a prerequisite merge. + +### W3 Completion + +The service can prove publisher and approver DIDs, hold only the exact writer grant, serialize refresh, and verify replay-resistant UV passkey ceremonies. + +## Workstream W4: GitHub Workload Identity + +### `W4.1` Issuer-neutral interfaces + +Define `WorkloadIssuer`, `VerifiedWorkload`, policy matcher, and stable failure codes without GitHub-specific types leaking into intent orchestration. + +Dependencies: `W5.1`. + +Merge boundary: land with `W4.2` so the abstraction is proved by its first production adapter. + +### `W4.2` GitHub JWT verification + +Verify discovery, remote JWKS, issuer, audience, token times, immutable repository/owner IDs, workflow identity, ref, SHA, run ID, run attempt, and optional environment. + +Dependencies: `W4.1`. + +### `W4.3` Typed workload-policy model + +Implement D1 repository and semantic matcher for repository, workflow, refs, and environments. No arbitrary expressions. + +Dependencies: `W4.1`, `W5.1`. + +Merge boundary: land with `W5.2b`; the repository is part of this workload-policy vertical. + +### `W4.4` Submission evidence and cancellation identity + +- Bind submission evidence to policy ID/version. +- Require matching repository, workflow, run ID, and run attempt for workload cancellation. +- Allow separately audited publisher-console cancellation. + +Dependencies: `W4.2`, `W4.3`, `W5.3`. + +### W4 Completion + +A verified token maps to exactly one normalized workload identity and either one authorized package policy or a stable rejection. + +## Workstream W5: Service Persistence and Orchestration + +### `W5.1` Worker application scaffold + +Create `apps/release-service` using the aggregator's Cloudflare Vite and workers-vitest patterns. Add D1, Queues, DLQs, cron, static assets, generated Worker types, health route, and fail-closed configuration validation. + +Dependencies: none. + +### `W5.1a` Dedicated verifier Worker + +Implement the isolated Worker used for artifact, provenance, and webhook egress. It exposes a narrow typed service binding, applies `W2.2` safe-fetch limits, and has no D1, Queue, service-secret, VPC, or private-origin bindings. Add workerd tests for redirects, size/time limits, malformed responses, and service-binding failure behavior. + +Dependencies: `W2.2`, `W5.1`. + +### `W5.2` D1 repository slices + +Do not land the entire service schema as one horizontal merge. Each slice includes only the migrations, repository methods, real-D1 tests, ownership constraints, and indexes required by its first service operation: + +- `W5.2a`: publisher accounts, OAuth transactions, console sessions, and delegations. Lands with `W3.2` and `W3.3`. +- `W5.2b`: workload policies. Lands with `W4.3`. +- `W5.2c`: approver identities, credentials, invitations, challenges, and the shared audit/outbox foundation needed for credential-security events. Lands with `W3.7`. +- `W5.2d`: workload JWT replay reservations, release targets, intents, submission outbox, and audit. Lands with `W5.3` and `W5.7a`. +- `W5.2e`: notification endpoints and delivery attempts. Lands with `W7.2`. +- `W5.2f`: append-only approvals and approval-lifecycle outbox. Lands with `W5.5`. + +Every applicable slice includes owner columns, unique constraints, indexes, and CAS/lease fields. `W5.2d` introduces a cryptographically random `public_intent_id` distinct from internal row identifiers; no external surface exposes the internal ID. + +Dependencies: `W5.1` and the contracts consumed by each slice. + +### `W5.3` Intent submission + +- Verify OIDC before remote fetch. +- Reject publishers excluded by the deployment's allowed-publisher policy before creating state or fetching user-controlled URLs. +- Hash and reserve the raw JWT until expiry, then reserve the idempotency key and release target atomically. +- Create intent, audit event, and validation outbox row in one D1 batch. +- Return stable `202`, duplicate, and conflict responses. +- Keep the public submission route unregistered until `W5.6` lands the publication consumer. + +Dependencies: `W1.3`, `W4.2`, `W4.3`. + +Merge boundary: land with `W5.2d` and `W5.7a`. This merge proves transactional outbox draining but exposes no route that can accept an intent before a validation consumer exists. + +### `W5.4` Validation worker + +- Resolve DID and PDS. +- Fetch signed profile and baseline releases. +- Validate artifacts, manifest access, and provenance. +- Derive signed policy, escalation, approval requirement, approval digest inputs, and expiry. +- Transition by CAS and write outbox events. + +Dependencies: `W2.3`, `W2.6`, `W2.7`, `W5.1a`, `W5.3`, `W5.7a`. + +### `W5.5` Approval and rejection lifecycle + +- Revalidate profile and baseline before challenge creation. +- Before returning substantive approval details, issuing a challenge, or accepting approval/rejection, fetch the current profile policy and require the authenticated approver DID to remain listed. An unauthenticated approval URL reveals only minimal package/status data and the login action. +- Recompute approval digest before challenge verification. +- Store append-only approval/rejection evidence. +- Invalidate approval when any bound fact changes. +- Transition approved intent to publish queue. +- Keep approval and rejection mutation routes unregistered until `W5.6` lands the publication consumer. + +Dependencies: `W3.7`, `W5.4`. + +Merge boundary: include `W5.2f` approval persistence in this lifecycle vertical. + +### `W5.6` Final verification and PDS publication + +- Add the idempotent publication Queue consumer. +- Re-run full verification. +- Acquire publisher publication/session lease. +- Create one deterministic release record. +- Reconcile timeout or transport ambiguity by direct read and canonical comparison. +- Distinguish published, confirmed absent/retryable, immutable conflict, and reauthorization-required. +- Register submission, approval, and rejection routes only after validation and publication consumers are active. + +Dependencies: `W1.7`, `W3.4`, `W5.4`, `W5.5`. + +### `W5.7` Outbox, Queues, cron, and recovery + +This work lands as two merge units rather than one horizontal infrastructure change. + +#### `W5.7a` Outbox and Queue infrastructure + +Land the transactional outbox drainer, Queue dispatch plumbing, DLQ forensics, and bounded retry primitives before connecting lifecycle operations to real queues. + +Dependencies: `W5.1`. + +Merge boundary: land with `W5.2d` and `W5.3`; lifecycle-specific Queue consumers still land with their operations. + +#### `W5.7b` Lifecycle recovery and maintenance + +Add stage expiry, lease reclamation, publishing reconciliation, proactive OAuth refresh, and bounded pruning after publication semantics are stable. + +Dependencies: `W3.4`, `W5.6`, `W5.7a`. + +### `W5.8` Versioned JSON API foundation + +Land only shared response envelopes, stable error-code serialization, request IDs, authentication/CSRF primitives, owner checks, pagination conventions, and API-schema generation here. CI, publisher, and approver endpoints land vertically with the operation they expose. Enforce the deployment's allowed-publisher policy on publisher login/delegation and every CI submission. Expose only `public_intent_id` for intent addressing; internal row IDs never cross the API boundary. + +Dependencies: `W5.1`. Each endpoint depends on its own service operation and repository slice. + +### W5 Completion + +Gate 2 passes with real D1, Queue redelivery, and fake-PDS integration tests. + +## Workstream W6: Publisher and Approver Console + +### `W6.1` Console foundation + +- React SPA through Worker static assets. +- Kumo component setup. +- Lingui extraction and locale loading. +- `LocaleDirectionProvider` and logical classes. +- Authenticated router, API client, error boundaries, and session expiry behavior. + +Dependencies: `W3.5`, initial `W5.8` session endpoints. + +### `W6.2` Publisher overview and delegation + +Show delegation scope, PDS, status, refresh health, assertion key, revoke, and reauthorize flows. Never imply key removal revokes current access tokens immediately. + +Dependencies: `W3.4`, delegation endpoints in `W5.8`. + +### `W6.3` Packages and workload policies + +- Read-only signed profile policy. +- Listed-versus-enrolled approver matrix. +- Typed GitHub repository/workflow/ref/environment editor. +- Generate local CLI command for signed profile-policy changes. + +Dependencies: `W4.3`, package/policy endpoints, `W8.3` command contract. + +### `W6.4` Intent and approval views + +- Lifecycle timeline. +- Workload evidence. +- Artifact and provenance checks. +- Baseline and structured access diff. +- Approval, rejection, blocked, expired, conflict, and revalidation states. + +Dependencies: `W5.4`, `W5.5`, approval endpoints. + +### `W6.5` Passkey and enrolment UI + +- Invitation acceptance. +- atproto identity callback. +- Multiple credential registration, naming, listing, and revocation. +- High-severity recovery warnings. +- No enrol-and-approve combined action. + +Dependencies: `W3.7`, passkey endpoints. + +### `W6.6` Notifications and audit UI + +- Verified email configuration. +- Webhook creation, one-time secret display, event selection, test, rotation, and failure state. +- Paginated audit filters and export. + +Dependencies: `W7`, audit endpoints. + +### `W6.7` Localization, accessibility, and RTL completion + +- No hard-coded user-facing strings. +- Keyboard and screen-reader pass. +- Arabic end-to-end pass. +- WebAuthn status and error announcements. +- Mobile approval review remains usable without hiding security details. + +Dependencies: all `W6` screens. + +### W6 Completion + +Every service-local publisher and approver operation is available through the console without direct D1 access. + +## Workstream W7: Notifications + +### `W7.1` Notification event contract + +Extend the credential-security event contract from `W3.7` with versioned release-lifecycle event types and safe payloads. Separate internal event data from the intentionally minimal email/webhook payload. + +Dependencies: `W3.7` and lifecycle contract from `W5.4` through `W5.7`. + +### `W7.2` Endpoint ownership and verification + +- Explicit publisher owner on every endpoint. +- Optional approver recipient. +- Email verification. +- Webhook URL validation and test delivery through the shared verifier/egress boundary. +- Event allowlists and disabled state. + +Dependencies: `W2.2`, `W3.1`, `W5.1`, `W5.1a`. + +Merge boundary: land with `W5.2e`; the repository is part of the notification-endpoint vertical. + +### `W7.3` Email adapter + +Implement Cloudflare Email Service behind `Mailer`, with text and HTML templates, localized copy where recipient locale is known, and no sensitive intent detail in mail. + +Dependencies: `W7.1`, `W7.2`. + +### `W7.4` Signed webhook adapter + +- Stable delivery ID and event schema version. +- Timestamped HMAC over raw body. +- Dedicated untrusted-egress boundary. +- Retry, jitter, DLQ, disable threshold, and secret rotation overlap. + +Dependencies: `W2.2`, `W7.1`, `W7.2`. + +### `W7.5` Delivery dispatcher + +Consume outbox events, materialize per-endpoint deliveries, deduplicate, record attempts, and surface failures to console and audit. + +Dependencies: `W5.7`, `W7.3`, `W7.4`. + +### W7 Completion + +Every lifecycle and credential-security event reaches configured destinations with at-least-once, auditable delivery. + +## Workstream W8: CLI and GitHub Action + +### `W8.1` Shared delegated-service API client + +Add `packages/registry-client/src/delegated` with typed requests, envelopes, polling, idempotency, stable errors, and no browser-specific dependency. + +Dependencies: API schemas from `W5.8`. + +### `W8.2` Delegation command + +Open the service authorization flow, wait for completion, display exact granted scope and service origin, and report reauthorization requirements. + +Dependencies: `W3.2` through `W3.5`, `W8.1`. + +### `W8.3` Profile policy command (complete) + +Delivered by `W1.5` in #1925, including add/remove approver, confirmation, provenance requirement, conflict handling, and JSON output. This is not a future merge unit. + +### `W8.4` Enrol and approve commands + +Open service-hosted browser ceremonies and wait for a terminal result. Do not attempt WebAuthn in the terminal. + +Dependencies: `W3.7`, approval API, `W8.1`. + +### `W8.5` Release submit, status, and cancel commands + +- Build request from local manifest/artifact/provenance inputs. +- Acquire workload token only in CI-supported mode. +- Poll until published, awaiting approval, or terminal failure. +- Emit stable JSON for automation. + +Dependencies: `W5.8`, `W8.1`. + +### `W8.6` Official GitHub Action + +- Request an audience-scoped OIDC token. +- Submit with deterministic idempotency input. +- Poll status. +- Output intent ID, status, approval URL, and release URI. +- Support cancellation from the same run identity. +- Accept no atproto secret. + +Dependencies: `W4`, `W5.8`, `W8.1`. + +### W8 Completion + +First-time delegation and steady-state automated release work through documented commands and the official Action. + +## Workstream W9: Installer Enforcement + +### `W9.1` Replace duplicate integrity helpers + +Move core artifact checksum, tar, and manifest consistency checks to `@emdash-cms/registry-verification` without behavior regression for existing releases. + +Dependencies: `W2.2`, `W2.3`, `W2.4`. + +### `W9.2` Fetch signed profile policy and provenance + +Extend direct-PDS install resolution to validate profile and release extensions and retain the profile CID used for the decision. + +Dependencies: `W1.3`, `W2.6`, `W2.7`. + +### `W9.3` Enforce provenance semantics + +- Optional and absent: install with explicit unattested status. +- Required and absent: block. +- Present and valid: continue. +- Present and failed/unverifiable: block, regardless of policy default. + +Dependencies: `W9.1`, `W9.2`. + +### `W9.4` Admin consent and provenance UI + +Show source, builder, workflow identity, verification status, and precise errors without presenting provenance as a safety guarantee. Localize and test RTL. + +Dependencies: `W9.3`. + +### `W9.5` Historical-policy limitation handling + +Apply current signed policy as a conservative direct-install floor and expose when historical policy-at-publication is unavailable. Never trust the aggregator to relax a current signed requirement. + +Dependencies: `W9.2`, RFC clarification. + +### W9 Completion + +Gate 3 installer criteria pass against valid, absent, tampered, foreign-source, and unknown-predicate fixtures. + +## Workstream W10: Aggregator Enforcement + +### `W10.1` Event-specific ingest source + +Replace or extend current Jetstream/current-record ingestion with the `W0.6` proved source. Carry ordering key, event CID, repo revision, record value, and proof blocks through Queue jobs. + +Dependencies: `W0.6`. + +### `W10.2` Policy history schema and ingest + +- Persist every package policy event. +- Classify tightening, weakening, and approver changes. +- Associate each release with the last preceding profile event. +- Handle profile/release arrival reordering through pending state and retry. + +Dependencies: `W1.3`, `W10.1`. + +### `W10.3` Historical policy reclassification + +- Associate each release with its event-specific policy state. +- Combine the existing provenance verification result with the historical policy in force at publication. +- Reclassify policy compliance by CAS without repeating cryptographic verification when inputs are unchanged. +- Record stable reasons and the associated policy-event ID. + +Dependencies: `W10.2`, `W10.4`. + +### `W10.4` Minimum default filtering + +Before hosted beta, persist the release provenance reference and current signed policy digest, queue shared verification, and CAS provenance status from `pending` to `valid`, `invalid`, or `unverifiable`. Expose status and exclude releases that are pending, invalid, or unverifiable when current signed policy requires provenance. This is a conservative current-policy floor and does not claim historical policy-at-publication accuracy. + +Dependencies: `W1.3`, `W2.6`; does not wait for `W10.1`. + +### `W10.5` Accurate policy-at-publication views + +Update latest-release, search, package, and audit views to use the associated historical policy event. Explicit audit reads may include violating releases with reasons. + +Dependencies: `W10.2`, `W10.3`. + +### `W10.6` Downgrade cooldown and notification + +- Detect `requireProvenance: true -> false`. +- Continue enforcing the strict prior floor through configured cooldown. +- Notify signed security contacts through the approved channel. +- Audit legitimate expiry and repeated downgrade transitions. + +Dependencies: `W10.2`, `W7` notification adapter or an explicitly separate aggregator notifier. + +### `W10.7` Reconciliation and backfill + +Backfill current records and available historical events, retry pending policy associations, reverify stale provenance, and preserve immutable duplicate-release behavior. + +Dependencies: `W10.2`, `W10.3`, `W10.5`. + +### W10 Completion + +Gate 5 aggregator criteria pass under event reordering, rapid profile transitions, queue delays, retries, and backfill. + +## Workstream W11: Operations and Self-Hosting + +### `W11.1` Deployment configuration + +Define fail-closed Worker bindings and variables for D1, Queues, DLQs, static assets, verifier Worker, email, public origin, OAuth metadata, GitHub audience, encryption keys, and allowed publisher policy. + +Dependencies: `W5.1`, `W5.1a`, `W7` binding choices. + +### `W11.2` Key and session operations + +Runbooks and tooling for: + +- Client assertion key overlap and removal. +- Application encryption-key migration. +- Webhook secret rotation. +- Publisher revocation and reauthorization. +- Compromised deployment emergency response. + +Dependencies: `W3.1` through `W3.4`, `W7.4`. + +### `W11.3` Observability and alerts + +Implement metrics and structured logs for lifecycle latency, validation failures, OAuth refresh, lease contention, PDS ambiguity, approval expiry, Queue age, DLQs, and delivery failures. Add security alerts listed in the spec. + +Dependencies: service lifecycle stabilized in `W5`, notification lifecycle in `W7`. + +### `W11.4` Abuse controls + +- Rate limits by publisher, policy, package, and source. +- Active-intent and remote-byte quotas. +- OIDC replay and policy-mismatch detection. +- Strict CSP, cookies, origin, and framing policy. +- Log/Sentry redaction tests. + +Dependencies: `W4`, `W5`, `W6`. + +### `W11.5` Backup, restore, and audit export + +Document and test D1 backup/export, encrypted-row restore, intent/audit export, and recovery after Queue loss. Restoration must not duplicate PDS releases. + +Dependencies: `W5.7`, `W3.1`. + +### `W11.6` Workers self-hosting path + +Provide a deployment template and guide that creates D1, Queues, DLQs, cron, verifier Worker, email adapter, OAuth keys, and secrets in another Cloudflare account. Include a post-deploy conformance command. + +Dependencies: `W11.1` through `W11.5`. + +### W11 Completion + +An operator can deploy, monitor, rotate, back up, restore, revoke, and incident-respond without undocumented database edits. + +## Workstream W12: Conformance and Security + +This workstream owns cross-component testing. Unit and workstream integration tests remain with their implementation workstreams. + +### `W12.1` Shared conformance fixtures + +Version fixtures for profiles, releases, access diffs, GitHub tokens, Sigstore bundles, service API responses, aggregator events, and installer outcomes. Every component imports fixtures rather than copying JSON. + +Dependencies: `W1`, `W2`, `W4` contracts. + +### `W12.2` Real workerd integration suite + +Cover D1 migrations, Queues, DLQs, cron, OAuth stores, refresh contention, state CAS, outbox recovery, expiry, encryption, and verifier Worker calls. + +Dependencies: `W3`, `W5`, `W7`. + +### `W12.3` Browser/WebAuthn suite + +Using the existing virtual authenticator pattern, cover multiple credentials, UV rejection, challenge replay, separate enrolment/approval, removal authorized by another active credential, last-credential OAuth recovery, affected-publisher notifications, rejection, Arabic RTL, accessibility, and mobile approval review. + +Dependencies: `W3.7`, `W6`. + +### `W12.4` End-to-end protocol suite + +Run fake GitHub issuer -> release service -> test PDS -> aggregator -> clean EmDash installer. Include automatic, approval-required, tampered, changed-profile, changed-baseline, ambiguous-write, and downgrade cases. + +Dependencies: `W5`, `W8`, `W9`, `W10`. + +### `W12.5` Adversarial and fuzz testing + +- Tar and Sigstore parser fuzzing. +- URL, redirect, DNS, and webhook SSRF cases. +- OIDC claim confusion and replay. +- OAuth refresh race and key rotation. +- Cross-tenant endpoint and intent authorization. +- Allowed-publisher rejection at console login/delegation and CI submission. +- Guessing and internal-row-ID attempts against public intent routes. +- Approval digest mutation and cross-action replay. +- Queue duplicate, reorder, poison, and DLQ behavior. + +Dependencies: feature-complete implementations. + +### `W12.6` External security review + +Review scope includes service compromise blast radius, OAuth custody, DPoP storage, encryption/key rotation, WebAuthn ceremonies, OIDC policy, PDS reconciliation, parser boundaries, notification egress, and tenant isolation. + +Dependencies: Gates 2 and 3. + +### `W12.7` Self-host and production smoke + +First provision a fresh Workers/D1 self-host from `W11.6` and run the same delegated-release conformance suite used for the hosted service. Complete the `W0.3` compatibility matrix against every PDS implementation for which support will be claimed, with a Bluesky-hosted PDS and at least one supported alternative as the minimum set: authorize the exact create-only scope, create a release, reject release update/delete, reject profile create/update and unrelated collection writes, and verify revocation and client-key removal before and after access-token expiry. Then use a controlled GitHub repository, real OIDC, the hosted service, production aggregator, and a disposable EmDash site. Verify rollback disables new submissions without invalidating published records or losing staged audit data. + +Dependencies: `W11.6` and Gates 1 through 4; completes `W0.3`; final hosted production run after Gate 0B and the Gate 5 `W10` implementation prerequisites. + +### W12 Completion + +No unresolved critical/high security findings, the `W0.3` compatibility matrix passes for every claimed-supported PDS with at least the required two-PDS minimum, all conformance suites pass, and the production smoke completes Gate 5. + +## Recommended Merge Sequence + +Completed work is recorded in the implementation baseline instead of remaining in the future queue. The next merge units are: + +| Sequence | Merge unit | Depends on | +| -------- | ------------------------------------------------------------ | ----------------------------------------- | +| 1 | `W2.6` + `W2.7` record verification and direct-PDS reads | Completed W1/W2 contracts | +| 2 | `W1.6` + `W1.7` exact scope and create-only publishing | Completed W1 contracts | +| 3 | `W3.6` required-UV and typed challenge primitives | None | +| 4 | `W5.1` service scaffold and `W5.8` API foundation | None; sensitive routes remain unreachable | +| 5 | `W4.1` + `W4.2` issuer contract and GitHub verifier | `W5.1` | +| 6 | `W5.1a` dedicated verifier Worker | `W2.2`, `W5.1` | +| 7 | `W3.1` encryption | None | +| 8 | `W3.2` + `W3.3` confidential OAuth and `W5.2a` custody slice | `W1.6`, `W3.1`, `W5.1` | +| 9 | `W4.3` workload policy and `W5.2b` repository slice | `W4.1`, `W5.1` | +| 10 | `W5.2d` + `W5.3` + `W5.7a` unreachable submission pipeline | `W4.2`, `W4.3`, API foundation | +| 11 | `W5.4` validation consumer, routes remain unreachable | `W2.6`, `W2.7`, `W5.1a`, `W5.3`, `W5.7a` | + +Later work continues as vertical merge units: passkey storage with ceremonies, approval storage with lifecycle, publication with reconciliation, notification storage with adapters, and API endpoints with their operations. `W10.1` begins only after Gate 0B; minimum current-policy filtering in `W10.4` may proceed earlier. + +## Parallelization Map + +Current parallel work: + +- `W2.6` + `W2.7`, `W3.1`, `W3.6`, and `W5.1` may proceed independently. `W4.1` + `W4.2` and `W5.1a` begin after `W5.1` establishes their app and binding boundaries. +- `W1.6` + `W1.7` and `W3.2` + `W3.3` close the exact delegation boundary. +- `W4.3`, then `W5.2d` + `W5.3` + `W5.7a`, establish the unreachable submission pipeline. `W5.4` adds validation while routes remain unreachable until publication lands in `W5.6`. +- `W0.6` external research may proceed concurrently and commit only conclusions to this branch. + +After Gate 0B: + +- `W10.1` and historical policy association may proceed independently of service orchestration. + +After Gate 1: + +- `W5` validation and publication become the main integration path. +- `W9` installer work can proceed independently against shared fixtures. +- `W10.3` provenance processing can proceed once event-specific policy association exists. + +After Gate 2: + +- `W6`, `W7`, and `W8` can proceed in parallel against the stable API. +- `W11` can add production bindings, observability, and runbooks. +- `W12` can begin full cross-component suites. + +## Dependency Risks + +| Risk | Impacted work | Required response | +| ---------------------------------------------------------- | ------------------------ | -------------------------------------------------------------------------------------------------- | +| Supported PDS rejects create-only scope | `W3`, `W5`, `W8` | Change RFC/support matrix; never add broad fallback. | +| Patched Sigstore dependency regresses or cannot be updated | `W2`, `W5`, `W9`, `W10` | Retain packed-output workerd tests; replace the pinned patch only with reviewed upstream behavior. | +| Historical profile values cannot be recovered | `W10`, production launch | Redesign history source or revise RFC downgrade guarantees before Gate 5. | +| Profile extension shape changes after implementation | `W1`, all consumers | Block downstream schema work until ratification; regenerate fixtures together. | +| D1 refresh lease proves unsafe under real atcute behavior | `W3`, `W5` | Introduce per-publisher DO coordinator behind `PublisherCoordinator`, keeping D1 canonical. | +| GitHub changes attestation identity or predicate fields | `W2`, `W4` | Fail closed, add a real fixture, and ratify the mapping before accepting the new shape. | +| Verifier Worker cannot enforce required egress policy | `W2`, `W7`, self-hosting | Require controlled egress proxy for deployments with private connectivity. | + +## Definition of Done for Every Work Item + +- Intended behavior and failure behavior are both tested. +- New persisted state has a forward-only migration and real D1 test. +- New async work is idempotent under duplicate and reordered delivery. +- New API behavior has a stable error code and API-client coverage. +- Security-sensitive comparisons use canonical forms and constant-time comparison where applicable. +- No secrets or private notification data appear in logs, errors, fixtures, or snapshots. +- User-facing strings are localized and new layouts pass RTL review. +- Package changes include an appropriate changeset. +- `pnpm build`, targeted tests, `pnpm lint:quick`, and relevant typechecks pass. +- The workstream's integration gate documentation is updated with actual verification evidence. + +## Current Execution Set + +Start these independently, with at most three implementation branches active at once: + +1. `W2.6` + `W2.7`: structured record/policy verification over authoritative direct-PDS reads. +2. `W1.6` + `W1.7`: implement the typed exact scope and narrow create-only publishing helper. +3. `W0.6`: select historical aggregator input and record Gate 0B constraints independently. +4. `W3.6`: required-UV passkey primitives. +5. `W5.1` + `W5.8`: unreachable service and API foundations. +6. After `W5.1`, `W4.1` + `W4.2` and `W5.1a`: workload verification and the isolated verifier Worker. + +Do not implement historical policy association or cooldown claims until Gate 0B passes. Deployed-PDS compatibility remains mandatory in `W12.7` before claiming support or launching production. diff --git a/.opencode/plans/delegated-release-service/sigstore-workerd-spike.md b/.opencode/plans/delegated-release-service/sigstore-workerd-spike.md new file mode 100644 index 0000000000..ceab2d9ea9 --- /dev/null +++ b/.opencode/plans/delegated-release-service/sigstore-workerd-spike.md @@ -0,0 +1,49 @@ +# W0.5 Sigstore verification in workerd + +This is the completed W0.5 decision record. W2.5 production implementation landed in #1951. + +## Proven facts + +The external spike used `@sigstore/verify@4.1.0`, `@sigstore/bundle@5.0.0`, and `@sigstore/protobuf-specs@0.5.1` with workerd, compatibility date `2026-07-11`, and `nodejs_compat`. It exercised `bundleFromJSON`, `TrustedRoot.fromJSON`, `toTrustMaterial`, `toSignedEntity`, and `Verifier.verify` offline against a vendored trust root. + +The fixture was the public Sigstore SLSA v1 provenance for `@sigstore/core@4.0.1`, generated by `sigstore/sigstore-js` `.github/workflows/release.yml` through `actions/attest-build-provenance`. Exact sources: + +- Archive: `https://registry.npmjs.org/@sigstore/core/-/core-4.0.1.tgz` +- Attestation API: `https://registry.npmjs.org/-/npm/v1/attestations/@sigstore%2fcore@4.0.1` +- Source commit: `https://github.com/sigstore/sigstore-js/tree/d406ea60b342ca37cdeecd7afedb992cd189db92` +- Workflow: `https://github.com/sigstore/sigstore-js/blob/d406ea60b342ca37cdeecd7afedb992cd189db92/.github/workflows/release.yml` +- Archive SHA-256: `8cc190e4385ee18399950148723aecd8db6e835668c361e4b6014435ba202643` +- Archive SHA-512 and DSSE subject digest: `f6fe61463ba39f9357abca3b5c511480bc80b5daf9222b1be29cccd39bb72bad484b9ab784fde5b96027764d1190f3cb4d41684db83b55bf38510d5941e6a359` +- Bundle SHA-256: `f925318b749b898f2cdfb7bdf59542c34e970c016488ecbcfa67ec5b7ffb1452` +- `gh attestation trusted-root` output from 2026-07-11 SHA-256: `65ca537f6ed8a47fd0e560c421baa1f6c1efb8b25fc200d8c5c02c0e92eb2b9c` + +With `tlogThreshold: 1`, `ctlogThreshold: 1`, `timestampThreshold: 1`, the exact certificate SAN, and issuer `https://token.actions.githubusercontent.com`, the Sigstore library verified the fixture's DSSE signature, certificate chain, Rekor SET and inclusion proof, signed checkpoint, CT evidence, and identity. The surrounding spike separately hashed the archive and matched it to the signed statement's subject digest; `Verifier.verify` does not perform that artifact binding. The timestamp threshold was satisfied by the transparency-log integrated time; this fixture contained no RFC3161 timestamp and did not exercise a TSA. + +The selected packages do not verify this fixture unmodified in workerd. `@sigstore/core` omits the `node:crypto.verify` algorithm for the primary DSSE envelope signature, Rekor SET, and signed checkpoint. Node accepts that omitted argument for these P-256 keys; workerd throws. The external spike proved that explicitly supplying SHA-256 for each fixture P-256 verification makes the complete public Sigstore/Fulcio/Rekor path pass while tampered signatures, payloads, Merkle proofs, identities, issuers, and artifact digests still fail. + +Node documents `crypto.verify` algorithm selection at `https://nodejs.org/api/crypto.html#cryptoverifyalgorithm-data-key-signature-callback`: `null` or `undefined` is key-type-dependent, notably for Ed25519/Ed448. Sigstore `PublicKeyDetails` separately binds supported ECDSA curves to digest algorithms. A general `algorithm ?? "sha256"` substitution is therefore incorrect. + +## Identity mapping + +| Field | Fixture value | Authoritative binding and required agreement | +| ---------------------- | --------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------- | +| Repository ID | `495574555` | Fulcio OID `.15`; predicate `internalParameters.github.repository_id` agrees | +| Workflow path | `.github/workflows/release.yml` | Predicate `externalParameters.workflow.path`; certificate workflow identity supplies the authoritative full URI | +| Workflow ref | `refs/heads/main` | Predicate workflow ref; exact SAN, build-signer URI OID `.9`, and build-config URI OID `.18` agree | +| Commit SHA | `d406ea60b342ca37cdeecd7afedb992cd189db92` | Fulcio digest OIDs `.10`, `.13`, and `.19`; resolved dependency `gitCommit` agrees | +| Source ref | `refs/heads/main` | Fulcio OID `.14`; predicate workflow ref agrees | +| Certificate identity | `https://github.com/sigstore/sigstore-js/.github/workflows/release.yml@refs/heads/main` | Exact SAN; OIDs `.9` and `.18` agree | +| SLSA `builder.id` | `https://github.com/actions/runner/github-hosted` | Signed predicate runner class, not the RFC workflow identity | +| Invocation | `https://github.com/sigstore/sigstore-js/actions/runs/28204693054/attempts/1` | Fulcio OID `.21`; predicate invocation ID agrees | +| RFC `sourceRepository` | `https://github.com/sigstore/sigstore-js` | Authoritative SourceRepositoryURI OID `.12`; predicate workflow repository must agree | +| RFC `builderId` | `https://github.com/sigstore/sigstore-js/.github/workflows/release.yml@refs/heads/main` | Authoritative exact SAN/build-config URI OID `.18`; predicate repository, path, and ref must compose to the same URI | + +Predicate fields are signed but workflow-controlled. Production policy must safely decode the modern DER UTF8String Fulcio extensions and require predicate agreement. RFC `builderId` is not SLSA `builder.id`. + +## W2.5 resolution + +- #1951 resolves the algorithm-selection gap with a narrowly scoped, lockfile-pinned `@sigstore/core@4.0.1` patch. The published package bundles the patched behavior; packed-output tests run in workerd so consumers do not depend on workspace patch application. +- Verification derives algorithms from validated key details and rejects unsupported or contradictory keys. Node and workerd exercise the same contract, including DSSE, SET, checkpoint, identity, predicate, and artifact bindings. +- The package vendors a reviewed public trust root and performs no runtime trust-API fetch. Trust-root refresh remains an explicit reviewed maintenance operation. +- `ProvenanceVerifier` implements all-or-nothing bundle checksum, trust, transparency, identity, predicate, artifact, repository, and builder agreement. Failed supplied provenance is never treated as absent. +- Support remains limited to public Sigstore/Fulcio/Rekor. GitHub-private Fulcio/TSA requires a separate real fixture and trust-domain test before support can be claimed. diff --git a/.opencode/plans/delegated-release-service/spec.md b/.opencode/plans/delegated-release-service/spec.md new file mode 100644 index 0000000000..f1b2c53307 --- /dev/null +++ b/.opencode/plans/delegated-release-service/spec.md @@ -0,0 +1,1160 @@ +# Delegated Release Service Implementation Spec + +Status: implementation in progress; deployed-PDS validation is deferred to production conformance, and aggregator-history validation remains open + +Source: [RFC PR #1870](https://github.com/emdash-cms/emdash/pull/1870), Attested Automated Publishing + +This spec covers the complete feature: protocol records, shared verification, the hosted delegated release service, publisher and approver UI, CLI integration, install-time verification, and aggregator handling. The service is the center of the design, but it is not useful or safe unless the protocol and install-time work land with it. + +## Decisions + +| Area | Decision | +| -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Reference runtime | A standalone Cloudflare Workers app at `apps/release-service`, separate from the aggregator. | +| Canonical state | D1. Use CAS state transitions, unique constraints, leases, and a transactional outbox. | +| Durable Objects | Do not use one in v1. A per-publisher DO is a later optimization for OAuth refresh and PDS write serialization if D1 lease contention becomes material. | +| Workload identity | GitHub Actions OIDC in v1 behind an issuer-neutral verifier interface. | +| Management surface | A full web console for service-local configuration and audit. Signed package policy remains read-only in the service. | +| Approver credentials | Multiple named passkeys per approver DID, individually revocable. | +| Notifications | Email and signed webhooks, both behind adapters. | +| API style | Versioned JSON HTTP API under `/v1`, not XRPC. Protocol records remain atproto lexicons. | +| Provenance predicate | SLSA provenance v1 is the only understood predicate in v1. Other predicates are present-but-unverifiable and fail when provenance is supplied. | +| Approval threshold | One valid approval from any currently listed, enrolled approver. Quorum is out of scope. | +| Stage TTL | 24 hours by default, operator-configurable with a bounded range of 15 minutes to 7 days. | +| Service tenancy | Multi-tenant by default. A v1 self-host deploys the same Worker app in its own Cloudflare account and can restrict allowed publisher DIDs. | + +## Implementation Baseline + +The integration branch has implemented the RFC-derived profile and release contracts, extension-preserving profile writes, the local profile-policy command, and the shared verification foundations through production public Sigstore provenance verification. See implementation PRs #1915, #1918, #1920, #1925, #1929, #1932, #1937, #1943, and #1951. + +The remaining external validation is split by impact: + +- Service feasibility: confidential OAuth custody is compatible with workerd. Deployed-PDS compatibility is a conformance and production-smoke requirement, not a pre-implementation gate; the service still has no broad-scope fallback. +- History feasibility: select an event source that preserves verifiable intermediate profile values. This blocks historical aggregator enforcement and production launch, not the service or installer. + +The next protocol/verification closure is two coherent merge units: exact scope plus create-only publishing (`W1.6` + `W1.7`), and direct-PDS reads plus structured record/policy verification (`W2.6` + `W2.7`). + +## Non-Negotiable Security Invariants + +1. CI never receives or stores an atproto account credential, refresh token, DPoP private key, or delegated release session. +2. The durable writer grant is exactly `atproto repo:?action=create`. There is no `transition:generic` fallback. +3. The service never durably acquires profile write scope. The console reads signed package policy from the PDS but cannot change it. +4. A release version is create-only and immutable. The delegated path has no overwrite option. +5. Every decision is based on records fetched directly from the publisher PDS, not aggregator data. +6. Every supplied artifact and provenance document is fetched through the shared SSRF-safe fetcher and checked against its signed checksum. +7. Artifact, provenance, profile policy, and escalation checks run at submission and again immediately before the PDS write. +8. Human approval is bound to an approval digest covering the release, workload evidence, policy, profile CID, baseline release CID, approver DID, and action. +9. Passkey approval requires WebAuthn user verification, not only user presence. +10. A listed but unenrolled DID cannot approve. Enrolment and approval are separate ceremonies. +11. D1 state and a PDS write are never treated as one transaction. Ambiguous PDS results are reconciled by reading the deterministic record key. +12. Secrets at rest are application-encrypted. Database access alone must not expose OAuth sessions, DPoP keys, emails, or webhook secrets. + +## Scope + +### Included + +- Profile policy and release provenance lexicon additions. +- A shared, Workers-compatible registry verification package. +- A standalone hosted release service with D1, Queues, cron, static assets, and a React console. +- Confidential atproto OAuth for durable release delegation. +- atproto login for publisher console sessions and approver DID proof. +- GitHub Actions OIDC policy registration and verification. +- Staged release lifecycle, passkey enrolment, approval, rejection, cancellation, and expiry. +- Multiple passkeys per approver DID. +- Email and webhook notifications. +- CLI commands and a reusable service API client. +- Install-time provenance and signed policy enforcement. +- Aggregator policy status, provenance verification, and downgrade history. +- Local workerd tests, browser WebAuthn tests, and an end-to-end test PDS flow. +- Cloudflare Workers self-hosting documentation and replaceable notification adapters. + +### Excluded + +- Native plugins. +- Non-GitHub workload issuers in the first release. +- Quorum approval. +- Signed approval receipts verifiable by installers. +- The future approver-addition cooldown. +- A profile-policy editor inside the release service. +- Artifact hosting or upload. CI supplies stable URLs. +- Generic configurable OIDC claim expressions in v1. +- A protocol-level recovery identity for lost approver passkeys. + +## Required RFC Clarifications + +These should be corrected in PR #1870 or recorded in a follow-up before implementation is called conforming. + +### OAuth revocation is not immediate + +Removing a confidential-client key from JWKS prevents sessions bound to that key from refreshing after the authorization server observes the removal. Existing access tokens remain usable until expiry. Emergency revocation must also call the authorization server's revocation endpoint for each known session. The service stores the client assertion key ID used to create each session so routine rotation can retain old public keys while those sessions exist. + +### The service uses public PDS reads + +Create-only repo scope grants writes, not reads. Package profiles and prior releases are public records and are read without the delegated session. Reads resolve the current PDS from the publisher DID and never use the aggregator as authority. + +### Automated submission is asynchronous + +Artifact and Sigstore verification can outlive a normal request and must survive retries. `POST /v1/release-intents` returns an intent resource. The GitHub Action and CLI poll or stream status until the result is `published` or `awaiting_approval`. A fast path may include the AT URI in the initial response, but callers cannot require it. + +### "Previous release" needs a deterministic definition + +For escalation evaluation, the baseline is the highest-semver existing release for the package at the time of verification, excluding the proposed `(package, version)` key. If no release exists, the baseline access is `{}`. This is deliberately conservative for out-of-order publication. The baseline is recomputed before publishing; a change invalidates an existing approval. + +### Experimental and stable NSIDs + +During the registry experiment the grant targets `com.emdashcms.experimental.package.release?action=create`. The service derives this from `@emdash-cms/registry-lexicons`; it must not hard-code the eventual `pm.fair.*` collection. Stable namespace migration requires every publisher to establish a new grant because OAuth scope is collection-specific. + +### CLI binary name + +The repository currently ships `emdash-plugin`, while the RFC examples say `emdash plugin`. Implementation and docs must settle on one public spelling. This spec uses the existing `emdash-plugin` binary. + +## Architecture + +```text +GitHub Actions Publisher / approver browser + | OIDC | atproto OAuth + WebAuthn + v v ++----------------------------------------------------------------+ +| apps/release-service | +| Worker API, React console, OAuth, OIDC, policy, orchestration | ++-----------------------------+----------------------------------+ + | + D1 canonical state + | + transactional outbox rows + | + +------------+-------------+ + | | + validation/publish queue notification queue + | | + artifact host, Sigstore Email Service, webhooks + | + publisher PDS createRecord +``` + +The service is not part of `apps/aggregator`. The aggregator is public, read-oriented, and has no publisher capability. The release service is a security-sensitive control plane with encrypted credentials and private user data. They may share published libraries, not bindings, databases, or deployments. + +### D1 Versus Durable Objects + +#### D1-only + +Benefits: + +- One relational source for console queries, package policy, stages, credentials, audit, and deliveries. +- Unique constraints naturally reserve package versions and deduplicate OIDC tokens. +- D1 migrations, local workerd testing, and Queue patterns already exist in `apps/aggregator`. +- The relational model does not prevent a later SQLite or Postgres port, but v1 self-hosting targets Workers and D1. + +Costs: + +- OAuth refresh and publication need explicit leases because multiple Worker isolates may race. +- PDS writes remain external side effects and require reconciliation. + +#### Durable Object authority + +A per-publisher SQLite Durable Object would serialize token refreshes and PDS writes. It does not solve the PDS transaction boundary, and it fragments state needed by the global console, audit queries, approver views, and operator tooling. Adding a D1 projection would introduce a dual-write problem more dangerous than the race it removes. + +#### Hybrid + +A hybrid keeps D1 canonical and routes only publisher-session operations through a per-publisher DO. This is viable later if metrics show meaningful lease contention or repeated refresh races. The DO must remain an execution coordinator with rebuildable state, never the only copy of a grant or release lifecycle. + +#### Decision + +Ship D1-only. Implement `PublisherCoordinator` behind an interface so a later DO adapter can replace D1 leases without changing intent or API code. + +```ts +interface PublisherCoordinator { + withSessionLease(publisherDid: string, operation: () => Promise): Promise; + withPublishLease(publisherDid: string, operation: () => Promise): Promise; +} +``` + +## Repository Layout + +```text +apps/release-service/ + migrations/ + public/ + src/ + api/ + audit/ + console/ + crypto/ + db/ + intents/ + notifications/ + oauth/ + oidc/ + passkeys/ + publishing/ + queues/ + routes/ + index.ts + test/ + package.json + vite.config.ts + vitest.config.ts + wrangler.jsonc + +apps/release-verifier/ + src/ + index.ts + test/ + package.json + vitest.config.ts + wrangler.jsonc + +packages/registry-verification/ + src/ + access.ts + bundle.ts + checksum.ts + fetch.ts + policy.ts + provenance.ts + records.ts + index.ts +``` + +Changes also touch: + +- `packages/registry-lexicons`: profile policy and provenance schemas. +- `packages/registry-client`: direct PDS read/create helpers and release-service API client. +- `packages/plugin-types`: canonical declared-access comparison and diff. +- `packages/auth`: optional required-UV WebAuthn verification and challenge context. +- `packages/plugin-cli`: delegate, policy, enrol, approve, and automated submit commands. +- `packages/core`: install-time provenance and profile policy enforcement. +- `apps/aggregator`: policy history, provenance verification, and discovery filtering. + +## Protocol Changes + +### Package profile extension + +Add `com.emdashcms.experimental.package.profileExtension` and an `extensions` map on the package profile. The extension has this shape: + +```ts +interface PackageProfileExtension { + $type: "com.emdashcms.experimental.package.profileExtension"; + repository: string; + releasePolicy?: { + requireProvenance?: boolean; + confirmation?: "escalation-only" | "always"; + approvers?: string[]; + }; +} +``` + +Constraints: + +- `repository` is a canonical HTTPS source repository URL, maximum 1024 bytes. +- `approvers` contains atproto DIDs only, has no duplicates, and is capped at 32. +- Absence normalizes to `requireProvenance: false`, `confirmation: "escalation-only"`, `approvers: []`. +- Unknown `confirmation` values fail lexicon or semantic validation. +- Policy fields stay optional for backwards compatibility. + +The repository field belongs in the signed profile extension because provenance layer 4 needs a package-level source anchor. The existing per-release `repo` field may remain for FAIR compatibility but cannot replace the profile anchor. + +### Release provenance + +Extend the release extension: + +```ts +interface ReleaseProvenance { + predicateType: "https://slsa.dev/provenance/v1" | string; + url: string; + checksum: string; + sourceRepository: string; + builderId: string; +} + +interface PackageReleaseExtension { + $type: "com.emdashcms.experimental.package.releaseExtension"; + declaredAccess: DeclaredAccess; + provenance?: ReleaseProvenance; +} +``` + +The provenance document is a Sigstore bundle. `checksum` covers the exact fetched document bytes. The attestation subject digest covers the package artifact bytes. + +### Lexicon rollout + +1. Add JSON lexicons and semantic validators. +2. Regenerate types with the existing registry-lexicons generation command. +3. Add round-trip fixtures before publishing schemas. +4. Update the experimental permission set, if one is published, to include the exact create-only collection. +5. Test scope support against Bluesky's PDS and at least one alternative PDS before service beta. + +Update every existing profile writer at the same time. In particular, `packages/plugin-cli/src/publish/api.ts` currently reconstructs profiles from a whitelist when updating `lastUpdated`; `ProfileInput`, `buildProfileRecord`, `stampLastUpdated`, and related validation must preserve `extensions` byte-for-byte unless `emdash-plugin policy` intentionally changes it. Add a regression test that sets strict policy, performs a later interactive publish, and proves the profile extension and CID-derived policy survive. + +## Shared Verification Package + +`@emdash-cms/registry-verification` is runtime-neutral ESM and must run in Workers and Node. It is the only implementation used by the service, installer, and eventually the CLI and aggregator. + +### Safe fetching + +`fetchVerifiedResource()` enforces: + +- HTTPS only. +- No URL credentials. +- Manual redirects, maximum 3. +- Reject IP literals and local/internal hostname forms. Pre-resolve DNS and reject loopback, private, link-local, multicast, unspecified, and metadata ranges before every hop as defense in depth. +- A response header timeout, total timeout, and byte limit. +- Streaming reads that abort on the first byte over the limit. +- No forwarding of authorization, cookies, or caller headers across origins. +- Content length is an early rejection only; the stream limit remains authoritative. +- Injected `fetch` and resolver for deterministic tests. + +Artifact and provenance hosts are untrusted. The same restrictions apply to webhook registration probes and redirects. + +Workers `fetch()` does not expose or pin the IP used for the actual connection, so DNS pre-resolution cannot eliminate rebinding TOCTOU. The hosted deployment performs untrusted fetches in `apps/release-verifier`, a dedicated Worker reached through a narrow release-service binding, with no public route, outbound service bindings, D1, Queues, service secrets, VPC connectivity, credentials, or private origin access. If a deployment exposes private network connectivity, it must route these requests through an egress proxy that resolves, validates, and pins the destination. The threat model and self-hosting docs state this residual explicitly. + +### Checksum handling + +Exports: + +```ts +computeMultihash(bytes, algorithm); +decodeMultihash(value); +verifyMultihash(bytes, expected); +compareDigestBytes(left, right); +``` + +The delegated path accepts multibase multihashes only. Legacy bare hexadecimal checksums may remain install-compatible for old records but are rejected for new delegated releases. + +### Bundle verification + +Consolidate the currently different readers in `packages/plugin-cli/src/commands/publish.ts` and `packages/core/src/plugins/marketplace.ts`. + +The canonical reader: + +- Rejects absolute paths, `..`, normalized-path collisions, duplicate entries, links, devices, and unsupported entry types. +- Requires exactly one root `manifest.json` and one root `backend.js`. +- Enforces compressed, decompressed, file-count, and per-file limits from one exported constants module. +- Parses `manifest.json` with the shared plugin manifest schema. +- Requires manifest slug and version to match the intent. +- Returns canonical `declaredAccess`, never raw manifest capability sugar. + +### Declared access escalation + +Add these exports to `packages/plugin-types/src/declared-access.ts` and re-export from `@emdash-cms/plugin-types`: + +```ts +canonicalizeDeclaredAccess(value): CanonicalDeclaredAccess +declaredAccessEqual(previous, next): boolean +diffDeclaredAccess(previous, next): AccessDiff +isDeclaredAccessEscalation(previous, next): boolean +``` + +Rules: + +- Materialize implied operations, such as write implying read. +- Sort object keys and sort/deduplicate host lists. +- Adding a category or operation is escalation. +- Removing a category or operation is narrowing. +- Missing `network.request.allowedHosts` means unrestricted access. +- Restricted to unrestricted network access is escalation. +- A new host is escalation unless an old exact or wildcard pattern already covers it. +- `*.example.com` covers subdomains, not `example.com` itself. +- Removing a known restricting constraint is escalation. +- Any changed unknown constraint is conservatively escalation because narrowing cannot be proved. +- The first release compares against empty access. +- The result includes a structured, localized-UI-friendly diff, not preformatted English. + +### Provenance verification + +```ts +interface ProvenanceVerifier { + verify(input: { + document: Uint8Array; + reference: ReleaseProvenance; + artifactDigest: Uint8Array; + profileRepository: string; + }): Promise; +} +``` + +The GitHub/Sigstore implementation verifies: + +1. The reference checksum matches the fetched Sigstore bundle. +2. The Sigstore signature, certificate chain, identity, and transparency evidence are valid under current Sigstore trust roots. +3. The attestation predicate is SLSA provenance v1. +4. A subject digest exactly matches the package artifact digest. +5. The attested source repository canonicalizes to `sourceRepository` in the release reference. +6. `sourceRepository` canonicalizes to the signed profile `repository`. +7. The verified GitHub workflow identity exactly matches `builderId` using the authoritative identity fields in the Sigstore certificate and SLSA predicate. + +The implementation must not do partial verification. Unknown predicates and unsupported bundle formats produce `PROVENANCE_UNVERIFIABLE`, never "absent". + +W0.5 proved public Sigstore verification in workerd with a real `actions/attest-build-provenance` bundle and documented the exact repository, workflow, commit, certificate, SLSA builder, and RFC `builderId` mapping. W2.5 then landed production verification with a vendored trust root, real fixture, packed-output workerd coverage, and a version-pinned `@sigstore/core` key-aware algorithm patch. The implementation fails closed and does not delegate cryptographic verification to an external trust API. + +## Service Data Model + +All IDs are ULIDs. Timestamps are UTC ISO strings. JSON columns contain canonical JSON where hashes or comparisons depend on them. + +### Identity and delegation + +`publisher_accounts` + +| Column | Notes | +| -------------------------- | -------------------------------------------- | +| `did` | Primary key. | +| `handle` | Display cache only. | +| `pds_url` | Resolved cache, never a permanent authority. | +| `pds_resolved_at` | Cache timestamp. | +| `created_at`, `updated_at` | Audit metadata. | + +`delegations` + +| Column | Notes | +| ---------------------------------------- | -------------------------------------------------------------------------- | +| `id` | Primary key. | +| `publisher_did` | Unique active delegation per DID and release NSID. | +| `release_nsid` | Scope-bound collection. | +| `encrypted_session` | atcute token set, authentication method, and per-session DPoP private key. | +| `encryption_key_version` | Enables envelope-key rotation. | +| `client_key_id` | Confidential client assertion key that created the session. | +| `scope` | Must equal the expected exact scope. | +| `status` | `active`, `refreshing`, `reauthorization_required`, `revoked`. | +| `lease_owner`, `lease_expires_at` | Distributed refresh lock. | +| `last_refreshed_at`, `refresh_before` | Proactive refresh schedule. | +| `created_at`, `updated_at`, `revoked_at` | Lifecycle. | + +`console_sessions` stores an opaque hashed browser session token, publisher DID, expiry, and CSRF secret. The atproto identity OAuth session is discarded after login; console authentication does not need durable PDS access. + +`oauth_transactions` stores hashed state, PKCE material, purpose, expected DID, client assertion key ID, encrypted temporary state, redirect target, and expiry. Purposes are `console_login`, `release_delegation`, and `approver_identity`. + +### Workload policy + +`workload_policies` + +| Column | Notes | +| -------------------------------------------- | -------------------------------------------------------- | +| `id` | Primary key. | +| `publisher_did`, `package_slug` | Unique package mapping. | +| `provider` | `github-actions` in v1. | +| `issuer` | Exact issuer URL. | +| `audience` | Exact service audience. | +| `repository_id`, `repository_owner_id` | Immutable GitHub identifiers, required. | +| `repository` | Human-readable `owner/name`, checked in addition to IDs. | +| `workflow_ref` | Exact reusable workflow or workflow file identity. | +| `allowed_refs` | JSON list of exact refs or validated prefix patterns. | +| `allowed_environments` | Optional JSON allowlist. | +| `enabled` | Boolean. | +| `version` | Increments on every edit and is captured by an intent. | +| `created_by_did`, `created_at`, `updated_at` | Audit. | + +Policies do not accept arbitrary JavaScript or expression languages. The console presents typed fields. A package may have multiple policies, for example release and prerelease workflows. + +`workload_token_uses` has a primary key over `(issuer, token_hash)`, expiry, intent ID, and first-seen timestamp. Hash the complete raw JWT so replay protection does not depend on an optional `jti` claim. + +### Approvers and passkeys + +`approver_identities` + +| Column | Notes | +| -------------------------- | ------------------------------------ | +| `did` | Primary key. | +| `handle` | Display cache. | +| `encrypted_email` | Optional private notification route. | +| `email_verified_at` | Null until verified. | +| `created_at`, `updated_at` | Lifecycle. | + +`approver_credentials` + +| Column | Notes | +| -------------------------------------------------- | ------------------------------------- | +| `credential_id` | Primary key, base64url credential ID. | +| `approver_did` | Owner. | +| `name` | User-provided authenticator label. | +| `public_key`, `algorithm`, `counter`, `transports` | WebAuthn verification data. | +| `device_type`, `backed_up` | Display and risk metadata. | +| `created_at`, `last_used_at`, `revoked_at` | Lifecycle. | + +`enrolment_invitations` stores a random-token hash, target DID, optional publisher/package context, expiry, consumed time, and inviter DID. Possession does not authorize enrolment; OAuth must return the same target DID. + +`webauthn_challenges` stores only the challenge hash plus purpose, approver DID, credential restrictions, intent ID, approval digest, profile CID, baseline release CID, action, expiry, and consumed timestamp. + +### Release lifecycle + +`release_targets` reserves immutable versions. + +| Column | Notes | +| ------------------------------------------ | --------------------------------------- | +| `publisher_did`, `package_slug`, `version` | Composite primary key. | +| `record_digest` | Canonical proposed release record hash. | +| `intent_id` | Owning intent. | +| `at_uri`, `cid` | Populated on publication. | + +`release_intents` + +| Column | Notes | +| ---------------------------------------------------------------------- | ------------------------------------------------------------------ | +| `id` | Primary key. | +| `publisher_did`, `package_slug`, `version`, `rkey` | Release identity. | +| `idempotency_key` | Unique per workload policy. | +| `record_json`, `record_digest` | Immutable canonical release draft. | +| `approval_digest` | Hash of every fact displayed and authorized by a passkey approval. | +| `artifact_digest` | Raw digest representation for provenance comparison. | +| `workload_policy_id`, `workload_policy_version` | Policy used at submission. | +| `workload_evidence` | Canonical verified claims, never the raw bearer token. | +| `profile_cid`, `profile_policy_json`, `profile_policy_digest` | Submission snapshot. | +| `baseline_release_uri`, `baseline_release_cid`, `baseline_access_json` | Escalation baseline. | +| `access_diff_json`, `is_escalation` | Approval presentation and decision. | +| `requires_approval` | Derived from signed profile policy plus escalation. | +| `status`, `state_version` | CAS state machine. | +| `expires_at` | Stage TTL. | +| `lease_owner`, `lease_expires_at` | Queue consumer lease. | +| `at_uri`, `release_cid` | Publication result. | +| `failure_code`, `failure_detail` | Stable machine code plus safe detail. | +| `created_at`, `updated_at`, `published_at` | Lifecycle. | + +The approval digest is not the release record digest. It is a domain-separated hash over canonical release record JSON, artifact and provenance references, derived declared access, normalized verified workload evidence, workload policy ID and version, profile CID and policy digest, baseline release CID and access, approver DID, and action. Recompute it when issuing and consuming a challenge. Any mismatch invalidates the challenge. + +`intent_approvals` stores intent ID, approver DID, credential ID, approval digest, profile CID, baseline release CID, approved/rejected action, and timestamp. It is append-only. + +### Delivery and audit + +`notification_endpoints` has an explicit `publisher_did` owner, endpoint type, optional `approver_did` recipient, encrypted destination, verification state, event allowlist, enabled state, and last failure. Webhook rows also store an encrypted signing secret. Every management and delivery query includes the owner key; ownership is never inferred from an event payload. + +`outbox_events` stores event ID, type, canonical payload, destination class, creation time, enqueue time, and completion time. + +`delivery_attempts` has a unique key `(event_id, endpoint_id, attempt)`, HTTP status or mail result, retry time, and redacted error. + +`audit_events` is append-only and contains actor type, actor identifier, action, target type/ID, request ID, IP prefix or privacy-preserving hash, structured safe metadata, and timestamp. Never log JWTs, OAuth state, session blobs, passkey challenges, email addresses, or webhook secrets. + +## Release Intent State Machine + +```text +received + -> validating + -> awaiting_approval + -> publish_queued + -> failed + +awaiting_approval + -> publish_queued + -> rejected + -> cancelled + -> expired + -> validating profile/baseline changed and must be re-evaluated + +publish_queued + -> publishing + -> published + -> publish_queued transient failure after confirmed absence + -> conflict different record exists at deterministic rkey + -> failed permanent verification or auth failure +``` + +Every transition uses a conditional update on `(id, status, state_version)`. Queue messages carry only intent ID and expected state version. A consumer that loses the CAS does no external work. + +Terminal statuses are `published`, `rejected`, `cancelled`, `expired`, `conflict`, and `failed`. + +### Submission transaction + +1. Verify the GitHub JWT and workload policy before fetching user-controlled URLs. +2. Validate and canonicalize the release draft. +3. Hash the raw JWT and canonical record. +4. Use one D1 batch to insert the token-use row, reserve the release target, insert the intent, append an audit event, and add a validation outbox row. +5. If the same idempotency key and record digest already exist, return the existing intent. +6. If an idempotency key or release target exists with different content, return `409`. + +Queue send is not atomic with D1. The request attempts to enqueue after commit, and a cron-driven outbox drainer sends any remaining rows. Duplicate sends are expected. + +### Validation + +The worker claims a bounded lease, then: + +1. Re-resolves the publisher DID and fetches the package profile from the current PDS. +2. Validates the profile lexicon and extension semantics. +3. Requires the package's signed profile repository to match the workload policy's repository. +4. Fetches and verifies all supplied artifact checksums. +5. Validates the package bundle and derives its canonical `declaredAccess`. +6. Requires bundle access to equal release-record access. +7. Fetches and verifies provenance when present. +8. Rejects absent provenance when `requireProvenance` is true. +9. Reads package releases directly from the PDS and selects the baseline. +10. Computes the structured access diff and escalation flag. +11. Derives `requiresApproval = confirmation === "always" || isEscalation`. +12. Stores the immutable snapshots and transitions to `awaiting_approval` or `publish_queued`. + +An intent requiring approval remains stageable when no listed approver is enrolled. The console shows it as blocked and notifications explain that enrolment is required. It is never auto-approved. + +### Approval + +The approval page can be viewed only after atproto login proves a DID currently listed in the fetched package policy. It shows: + +- Package, version, publisher DID, and signed source repository. +- GitHub repository, workflow, ref, commit SHA, run ID, run attempt, and environment. +- Artifact URL and checksum. +- Provenance URL, checksum, source, and builder. +- Previous release version and CID. +- Full declared-access diff, with escalation highlighted. +- Intent creation and expiry time. + +`POST .../approval/options` re-fetches profile and baseline state before issuing a challenge. If either CID changed, the intent returns to `validating`. The challenge binds the approval digest, current profile CID, current baseline CID, approver DID, and action. + +Approval verification: + +1. Find the credential by response ID and require it to belong to the OAuth-proven approver DID. +2. Verify origin and RP ID. +3. Require user presence and user verification. +4. Consume the exact single-use challenge. +5. Verify signature and counter. +6. In one D1 batch, update the credential counter, append approval and audit rows, transition the intent, and create outbox rows. + +Rejection uses a passkey assertion bound to action `reject`; it is not a weaker button click. + +### Final verification and publication + +Before PDS write, repeat all validation steps and require unchanged: + +- Canonical release record digest. +- Workload policy version and enabled state. +- Package profile CID and policy digest. +- Baseline release CID. +- Artifact bytes and checksum. +- Provenance document and all four verification layers. + +If the profile or baseline changed, invalidate the pending publication and return to validation. Human approval must be repeated if the recomputed intent still requires it. + +Publication uses `com.atproto.repo.createRecord` or an `applyWrites` batch containing one create. The deterministic rkey is `:`. It never updates the profile. + +If the PDS call times out or returns an ambiguous transport error: + +1. Fetch the deterministic AT URI from the PDS. +2. If it exists and canonical content equals the intended record, mark the intent published. +3. If absent, return to `publish_queued` with bounded exponential backoff. +4. If different content exists, mark `conflict` and alert the publisher. + +Authentication or scope errors mark the delegation `reauthorization_required`; affected intents remain staged until expiry and the publisher is notified. + +## Atproto OAuth + +### Client metadata + +Serve public metadata and JWKS from stable HTTPS URLs. The confidential client declares: + +```json +{ + "application_type": "web", + "grant_types": ["authorization_code", "refresh_token"], + "response_types": ["code"], + "dpop_bound_access_tokens": true, + "token_endpoint_auth_method": "private_key_jwt", + "token_endpoint_auth_signing_alg": "ES256" +} +``` + +The exact `client_id`, redirects, scope declaration, and `jwks_uri` are deployment-derived. Client assertion keys and per-session DPoP keys are distinct keypairs. + +### OAuth purposes + +| Purpose | Requested scope | Persistence | +| ----------------------- | ------------------------------------------- | ------------------------------------------------------- | +| Console login | `atproto` | Delete OAuth session after creating the service cookie. | +| Approver identity proof | `atproto` | Delete immediately after matching the expected DID. | +| Release delegation | `atproto repo:?action=create` | Store encrypted until revoked. | + +Use separate logical atcute stores per purpose so sessions for the same DID cannot overwrite each other. + +### OAuth client compatibility + +`@atcute/oauth-node-client@2.0.1` is compatible with confidential-client custody in workerd. Its persisted `StoredSession` contains the token set, negotiated authentication method including the client assertion key ID, and per-session DPoP private key. Authorization state is a separate short-lived store. DPoP nonces are cached separately by origin; this cache may remain isolate-local because atcute's replayable token requests retry one recognized nonce challenge. Metadata caches may also remain isolate-local. + +The package's in-memory request coalescing does not coordinate Worker isolates. Supply `requestLock` backed by the delegation's D1 lease, and hold that lease across session load, refresh, and persistence. + +The package restores authorization state and sessions with their recorded client assertion key IDs. If a private key is absent, it throws a generic error and does not invalidate a session itself. Before callback, restore, or refresh, the service checks the recorded key ID against the configured private keyset and rejects unavailable-key state as requiring reauthorization. + +### Session refresh + +Refresh tokens are rotating and replay-sensitive. Before any restore that may refresh a writer session, atomically change `active` to `refreshing` and record a unique D1 lease owner. This durable marker poisons the persisted refresh-token generation before network I/O. Hold or renew the lease across session load, refresh, and persistence, and require the unexpired lease owner to match when atomically storing the rotated session and returning to `active`. + +Never steal an expired lease from a `refreshing` row to retry its old session. A stale `refreshing` row, lost lease, or failed post-refresh persistence has an ambiguous single-use token outcome and transitions only to `reauthorization_required`. The package attempts to revoke a newly issued token after a store failure, but this is best effort and does not make the old token safe to retry. + +Refresh proactively well before expiry and add jitter. A scheduled sweep refreshes idle sessions so a three-month refresh-token lifetime cannot lapse unnoticed. + +Store the confidential client key ID used at initial authorization. Routine rotation publishes old and new public keys together, waits for JWKS cache propagation before ordering the new key first for new grants, and retains each old private key until its active sessions are reauthorized or revoked and every authorization transaction that may reference it has expired. + +## GitHub Actions OIDC + +```ts +interface WorkloadIssuer { + verify(token: string, expectedAudience: string): Promise; +} + +interface VerifiedWorkload { + issuer: string; + subject: string; + repository: string; + repositoryId: string; + repositoryOwnerId: string; + workflowRef: string; + jobWorkflowRef?: string; + ref: string; + sha: string; + runId: string; + runAttempt: string; + environment?: string; + expiresAt: number; +} +``` + +The GitHub implementation verifies discovery metadata, remote JWKS signature, exact issuer, exact audience, `exp`, `nbf`, and reasonable `iat`. Authorization matches immutable repository and owner IDs, repository name, exact workflow identity, ref rules, and optional environment. + +Never authorize from request-body repository fields, actor names, branch display names, or a GitHub organization name alone. Store normalized verified claims, not the raw JWT. + +Workload cancellation requires a fresh OIDC token whose repository ID, workflow identity, run ID, and run attempt match the submitting workload evidence. The original bearer token and optional `jti` are not retained. A publisher may separately cancel any pre-publication intent through an authenticated console action; the audit log distinguishes workload and publisher cancellation. + +## WebAuthn and Enrolment + +Extend `@emdash-cms/auth/passkey` additively: + +- `PasskeyConfig.userVerification?: "preferred" | "required"`. +- Verification checks `authenticatorData.userVerified` when required. +- Challenge data accepts an opaque typed context and returns it after atomic consumption. +- Existing CMS callers retain `preferred` as their default. + +Service enrolment flow: + +1. Approver opens an invitation or starts enrolment for their own DID. +2. Service performs atproto OAuth with scope `atproto`. +3. OAuth `sub` must equal the invitation target DID, when present. +4. Service issues WebAuthn registration with UV required and excludes existing credential IDs. +5. Verified credential is inserted with a user-provided name. +6. Enrolment is audited and publisher/approver notifications are queued. +7. The user returns to the intent page but no approval is created automatically. + +An approver can add multiple credentials. Removing a credential requires a fresh atproto login and a passkey assertion from another active credential when one exists. If no credential remains, OAuth proof alone can start recovery, but the event is high-severity audited and notifies all publishers whose profiles currently list the DID. + +Package authorization is always re-evaluated from the current signed profile. Retaining a credential after a DID is removed from one package is safe because the same DID may approve other packages. + +## JSON API + +All responses use: + +```ts +type ApiResponse = + | { data: T; requestId: string } + | { error: { code: string; message: string; details?: unknown }; requestId: string }; +``` + +Errors have stable screaming-snake codes. Secrets and upstream raw errors never appear in `details`. + +### CI endpoints + +#### `POST /v1/release-intents` + +Headers: + +- `Authorization: Bearer ` +- `Idempotency-Key: ` +- `Content-Type: application/json` + +Body: + +```json +{ + "publisherDid": "did:plc:...", + "package": "gallery-plugin", + "version": "1.2.3", + "artifacts": { + "package": { + "url": "https://github.com/.../gallery-plugin.tgz", + "checksum": "b...", + "contentType": "application/gzip" + } + }, + "requires": { "env:emdash": ">=1.0.0" }, + "provenance": { + "predicateType": "https://slsa.dev/provenance/v1", + "url": "https://github.com/.../attestation.sigstore.json", + "checksum": "b...", + "sourceRepository": "https://github.com/example/gallery-plugin", + "builderId": "https://github.com/example/gallery-plugin/.github/workflows/release.yml@refs/heads/main" + } +} +``` + +The service derives `declaredAccess` from the fetched bundle. It may accept a caller-provided copy only to fail fast on disagreement; the service-derived value is authoritative for the record draft. + +Response: `202` with intent ID, status URL, expiry, and approval URL when known. + +#### `GET /v1/release-intents/:id` + +CI authenticates with a fresh matching OIDC token. Browser console sessions may also read authorized intents. Returns status, safe evidence, approval requirement, failure code, and final AT URI/CID. + +#### `POST /v1/release-intents/:id/cancel` + +Requires either a fresh OIDC token matching the original repository, workflow, run ID, and run attempt, or the publisher's authenticated console session. Valid only before `publishing`. + +### Publisher console endpoints + +- `GET /v1/me` +- `GET /v1/delegations` +- `POST /v1/delegations/start` +- `DELETE /v1/delegations/:id` +- `GET /v1/packages` +- `GET /v1/packages/:slug/profile-policy` +- `GET|POST|PATCH|DELETE /v1/workload-policies` +- `GET /v1/release-intents` +- `GET /v1/release-intents/:id` +- `POST /v1/approver-invitations` +- `GET|POST|PATCH|DELETE /v1/notification-endpoints` +- `GET /v1/audit-events` + +Mutations require the console CSRF token and same-origin request headers. A console session may manage only rows whose publisher DID matches its authenticated DID. + +### Approver endpoints + +- `GET /v1/enrolment-invitations/:token` +- `POST /v1/approver/oauth/start` +- `POST /v1/passkeys/registration/options` +- `POST /v1/passkeys/registration/verify` +- `GET /v1/passkeys` +- `PATCH|DELETE /v1/passkeys/:credentialId` +- `GET /v1/release-intents/:id/approval` +- `POST /v1/release-intents/:id/approval/options` +- `POST /v1/release-intents/:id/approve` +- `POST /v1/release-intents/:id/reject` + +Approval details require an atproto-authenticated browser session whose DID is currently listed by the package profile. The untrusted approval URL alone exposes only package name, status, and a login prompt. + +## Web Console + +Build a React SPA served by the Worker static-assets binding. Use Kumo components and Lingui from the beginning. All layouts use logical RTL-safe classes. + +Publisher pages: + +- Overview: delegation health, packages, held intents, failed deliveries. +- Delegation: exact scope, PDS, key/session health, revoke and reauthorize actions. +- Packages: current signed policy, enrolled/listed approver matrix, workload policies. +- Workload policy editor: typed GitHub repository, workflow, ref, and environment controls. +- Intents: searchable lifecycle history and exact verification results. +- Notifications: verified email routes, webhooks, event selection, test delivery, secret rotation. +- Audit: actor, action, target, outcome, and timestamp with filters and export. + +Approver pages: + +- Enrolment invitation and atproto identity proof. +- Passkey list with names, creation, last use, and individual revocation. +- Approval detail with workload, provenance, checksums, and access diff. +- Success, rejection, cancellation, and expiry states. + +The console displays signed profile policy read-only. To change `requireProvenance`, `confirmation`, or `approvers`, it generates the corresponding `emdash-plugin policy` command and refreshes until the new profile CID appears. This preserves the RFC's profile-scope separation. A future browser-only direct-to-PDS editor may be added only if profile tokens never reach the service Worker. + +## Notifications + +Events: + +- `intent.awaiting_approval` +- `intent.approved` +- `intent.rejected` +- `intent.cancelled` +- `intent.expired` +- `intent.published` +- `intent.failed` +- `delegation.reauthorization_required` +- `approver.enrolled` +- `approver.credential_added` +- `approver.credential_removed` +- `approver.recovery` +- `delivery.disabled` + +Email contains the package, version, intent ID, and service-origin URL. It does not include full workload claims, access details, or secrets. Those require authenticated console access. + +Webhook delivery: + +- Stable event ID and schema version. +- `X-EmDash-Event`, `X-EmDash-Delivery`, `X-EmDash-Timestamp`, and HMAC-SHA256 signature headers. +- Signature input is `.`. +- At-least-once semantics; receivers deduplicate by delivery ID. +- Manual redirect handling and SSRF checks on every attempt. +- Exponential retries with jitter, then DLQ and endpoint disable threshold. +- Secret shown once and rotatable with an overlap window. + +Use interfaces for `Mailer` and `WebhookDispatcher`. The hosted adapter uses Cloudflare Email Service and Workers fetch; a Workers self-host may select another HTTP mail adapter. A future Node port can add SMTP without changing the webhook contract. + +## CLI and GitHub Action + +Add a shared client under `packages/registry-client/src/delegated`. + +Commands: + +```text +emdash-plugin delegate --service +emdash-plugin policy set --require-provenance --confirmation always --approver +emdash-plugin enrol --service [--invite ] +emdash-plugin approve --service +emdash-plugin release submit --service --url --provenance +emdash-plugin release status --service [--wait] +emdash-plugin release cancel --service +``` + +`delegate` opens the service OAuth flow and waits for completion. `enrol` and `approve` open the service-hosted WebAuthn page because terminal programs cannot directly perform a platform passkey ceremony. + +Provide an official GitHub Action wrapping OIDC acquisition and intent submission. It outputs: + +- `intent-id` +- `status` +- `approval-url` when held +- `release-uri` when published + +The action waits by default until `published`, `awaiting_approval`, or terminal failure. It never accepts an atproto secret input. + +## Installer Changes + +`packages/core` must enforce the signed policy independently of the service and aggregator. + +Install flow additions: + +1. Fetch the current release and package profile directly from the publisher PDS with record proof as already required by RFC 0001. +2. Validate profile and release extensions. +3. Verify artifact checksum and manifest access as today, using the shared package. +4. If provenance is present, run all provenance checks. +5. If `requireProvenance` was in force for the release and provenance is absent or failed, block installation. +6. If provenance is optional and absent, continue with an explicit "not attested" status. +7. If provenance is present but failed or unverifiable, block. Never downgrade it to absent. +8. Surface source repository, builder identity, workflow, and verified/unverified state without presenting provenance as malware safety. + +Historical policy-at-publication cannot be reconstructed from only the current profile after a legitimate policy change. Until the protocol carries a policy snapshot/receipt, direct installers use the current signed policy as a conservative floor. The aggregator tracks event history for accurate discovery-time status. This limitation must be explicit in UI and RFC text. + +## Aggregator Changes + +The aggregator needs event-history work before it can implement the RFC's "policy in force at publication time" rule. + +### Ingest ordering + +Current Jetstream jobs discard `time_us`, event CID, and repo revision, then fetch the current record by rkey. That loses intermediate profile versions. Merely adding those fields to the job is insufficient because a later PDS read still returns only the current value. Phase 0 must choose and prove an event source that provides the event-specific record value plus a verifiable commit/CAR path, or explicitly document a weaker trust model. Carry its ordering key, event CID, repo revision, and record blocks through the queue and persist every profile-policy version. + +Do not claim downgrade cooldown support until intermediate versions are provably retained. + +### Schema + +Add: + +- Raw profile extension and current policy digest on `packages`. +- `package_policy_events` with DID, slug, event ordering key, profile CID, policy, repository, and transition classification. +- Provenance reference, policy event ID, policy status, reasons, and verification time on `releases`. +- A provenance verification queue/outbox. +- Downgrade notification state and cooldown expiry. + +Release policy status is `pending`, `valid`, `invalid`, or `unverifiable`. Default search, latest-release selection, and update discovery exclude `pending`, `invalid`, and `unverifiable` releases when policy requires provenance. Explicit audit endpoints may return them with reasons. + +Policy weakening transitions include `requireProvenance: true -> false`, `confirmation: always -> escalation-only`, and removal of approvers. Only `requireProvenance` affects install validity; the other transitions are audit signals because confirmation is not installer-verifiable. + +Expensive provenance work runs asynchronously. Structural ingest writes `pending`, queues verification, and updates status with CAS. Any exposed pending/invalid release carries explicit status; it is never silently treated as valid. + +## Encryption and Key Management + +Use AES-256-GCM with a versioned master key supplied by a Worker secret or Secrets Store. Derive purpose-specific data-encryption keys with HKDF. Associated data includes table name, row primary key, publisher DID, and key version. + +Encrypt: + +- atcute OAuth session blobs and DPoP private keys. +- Confidential client private keys if not directly represented as deployment secrets. +- Email addresses. +- Webhook URLs when they contain private paths or tokens. +- Webhook secrets. + +Never encrypt fields that need indexed equality; store a separate keyed hash when lookup is required. Key rotation rewrites rows in bounded queue jobs. Old keys remain available until migration completes and verification reports zero old-version rows. + +## Abuse and Platform Security + +- Rate-limit intent submission by workload policy, publisher, package, and source network. +- Cap active intents per package and publisher. +- Reserve storage before remote fetches and expire abandoned intents. +- Reject URLs before enqueueing when syntax is unsafe; repeat DNS/IP checks during fetch. +- Apply strict CSP, frame ancestors, referrer policy, secure cookies, and origin checks to console and approval pages. +- Use random opaque public intent IDs; do not expose D1 row IDs or sequential identifiers. +- Redact all bearer tokens and OAuth materials from logs and Sentry. +- Treat provenance parser and tar parser input as hostile and fuzz both. +- Keep approval and enrolment pages on one configured RP origin; preview deployments use separate RP IDs and databases. +- Require explicit operator configuration for trusted origins, client ID, audience, email sender, and public base URL. Fail closed when absent. + +## Background Work + +Queues: + +- `release-validation`: validation and publication jobs. +- `release-notifications`: email and webhook jobs. +- A DLQ for each queue. + +Cron tasks: + +- Every minute: drain unsent outbox rows and expire staged intents. +- Every 5 minutes: reclaim expired worker leases and reconcile `publishing` intents. +- Daily: refresh delegations approaching refresh-token expiry, re-resolve stale DIDs, prune consumed OAuth state and challenges, and verify notification endpoint health. + +Every consumer is idempotent and begins with a state/version CAS. DLQ consumers persist a forensics row or audit event before acknowledging. + +## Observability + +Structured logs and metrics include: + +- Intent counts and latency by lifecycle state. +- Validation failure code and layer, without private payloads. +- OIDC failures by reason. +- OAuth refresh success, contention, and reauthorization count. +- PDS write latency, ambiguous outcomes, and reconciliation result. +- Approval wait duration and expiry rate. +- Queue age, retries, and DLQ count. +- Email/webhook delivery success and endpoint disable count. +- D1 query and write latency. +- Active leases and lease-steal count. + +Security alerts: + +- Delegation scope mismatch. +- OAuth client key removal or refresh replay error. +- Re-enrolment or recovery. +- Different record at a reserved release rkey. +- Repeated provenance or artifact mutation between stage and approval. +- Repeated OIDC replay or policy mismatch. +- Webhook SSRF attempt. + +## Testing + +### Protocol and shared package + +- Lexicon valid/invalid fixtures and generated-type tests. +- Checksum vectors for supported and unsupported algorithms. +- URL redirect, DNS rebinding, private IPv4/IPv6, metadata endpoint, timeout, and oversize tests. +- Tar traversal, duplicate normalized path, duplicate manifest, links, devices, gzip bomb, and cap tests. +- Every declared-access widening and narrowing rule. +- Valid Sigstore bundle, bad signature, wrong subject, substituted bundle, wrong repository, wrong builder, unknown predicate, and expired trust material. + +### Service unit and workerd integration + +- Real D1 migrations with `@cloudflare/vitest-pool-workers`. +- OIDC signature, issuer, audience, time, immutable claim, policy, and replay failures. +- Duplicate idempotency and version races. +- First release with non-empty access requires approval. +- `confirmation: always` requires approval for unchanged access. +- Listed but unenrolled approver cannot approve. +- Removed approver cannot approve with a retained credential. +- Required-UV rejects a user-present-only assertion. +- Challenge replay and cross-intent/action replay fail. +- Profile, baseline, artifact, provenance, or workload-policy mutation invalidates approval. +- Concurrent OAuth refresh obtains one lease and preserves the rotated session. +- Ambiguous PDS write reconciles exact match, absence, and conflicting content. +- Queue redelivery, outbox recovery, lease expiry, stage expiry, rejection, and cancellation. +- Email/webhook deduplication, signature, redirect, retry, and disable behavior. +- Encryption round trip and key rotation. + +### Browser E2E + +Use the existing virtual authenticator fixture pattern from `e2e/fixtures/virtual-authenticator.ts`. + +- Publisher signs in and creates a GitHub policy. +- Publisher establishes the create-only delegation. +- Approver accepts invitation through atproto OAuth and registers two passkeys. +- Enrolment does not approve an existing intent. +- Approver reviews and approves an escalating intent. +- A second credential can approve after the first is revoked. +- Arabic locale and RTL layout for every new console flow. + +### End-to-end protocol + +Against a test PDS and fake GitHub issuer: + +1. Publish a signed profile with strict policy. +2. Submit an attested artifact from CI. +3. Approve with a virtual passkey. +4. Create the release record through the scoped session. +5. Ingest it in the aggregator. +6. Install it from a clean EmDash site with independent provenance verification. + +Run a second suite against real GitHub OIDC, a Bluesky-hosted PDS, and at least one supported alternative PDS in a controlled repository before production launch. For each PDS, verify successful release creation and rejection of release update/delete, profile create/update, and unrelated collection writes, plus revocation and client-key removal behavior. + +## Delivery Plan + +### Phase 0: RFC clarification and external validation + +- Complete: record implementation acceptance criteria for the profile extension, repository anchor, release provenance, and escalation contracts already decided by RFC #1870. +- Deferred to conformance and production smoke: validate create-only permission support on every supported deployed PDS without broad fallback. +- Complete: confirmed `@atcute/oauth-node-client@2.0.1` confidential-client persistence, DPoP nonce retry, D1 lock requirements, and client-key rotation behavior in workerd. +- Complete: inspect a real GitHub provenance bundle and land the Workers-compatible Sigstore verifier plus exact field mapping. +- Pending Gate 0B: select an aggregator history source that can retain event-specific signed profile values and document `W10.1` constraints. +- Complete: use `emdash-plugin` as the v1 public command. + +OAuth custody feasibility is complete. Gate 0B passes when historical ingest has a viable source and trust model. External history validation adds no repository harnesses, production code, test scripts, package dependencies, or CI wiring; commit conclusions directly to the integration branch. + +### Phase 1: Protocol and verification foundation + +- Complete: land profile and release lexicon additions. +- In progress: complete `@emdash-cms/registry-verification` with direct-PDS record/policy verification. +- Complete: land declared-access canonical diff. +- Extend passkey primitives with required UV and bound challenge context. +- Extract create-only release record construction into `registry-client`. +- Switch existing installer integrity checks to shared verification where behavior is equivalent. + +Exit criterion: records can represent the RFC, and service/installer use identical verification fixtures. + +### Phase 2: Secure automated vertical slice + +- Scaffold `apps/release-service` with D1, OAuth, GitHub OIDC, intent state machine, queues, and publication reconciliation. +- Implement delegation and minimal publisher console. +- Implement approver OAuth, multiple passkeys, approval pages, and audit. +- Implement CLI API client and official GitHub Action. +- Implement install-time provenance and policy enforcement. +- Add minimum aggregator policy status and default filtering. Until historical ordering lands, apply the current signed profile as a conservative floor and mark the result accordingly. + +Exit criterion: GitHub Actions can publish an attested release without a stored atproto credential, including an escalation requiring passkey approval, and a clean site independently verifies and installs it. + +### Phase 3: Hosted product completeness + +- Complete the publisher console and localization. +- Add email, webhook management, retries, and delivery UI. +- Add credential recovery, delegation health, key rotation tooling, and audit export. +- Add operator abuse controls and production observability. +- Publish Workers/D1 self-hosting docs and deployment templates. + +Exit criterion: the default hosted instance and a fresh Workers/D1 self-host can complete all service lifecycle operations without database/operator hand edits. Public production launch remains blocked on Phase 4's accurate aggregator history. + +### Phase 4: Aggregator policy enforcement + +- Preserve event-specific policy history. +- Add asynchronous provenance verification. +- Add policy status to read views and default filtering. +- Add downgrade cooldown and security-contact notification. + +Exit criterion: discovery accurately applies policy in force at publication time and survives out-of-order/replayed ingest. + +Phase 4 is a production launch gate for the default public service and registry, not optional post-launch hardening. + +### Phase 5: Hardening and expansion + +- External security review and parser fuzzing. +- Load and failure-injection tests. +- Additional OIDC issuers through the existing interface. +- Evaluate a per-publisher DO coordinator using measured refresh/publish contention. +- Consider signed approval receipts and quorum in a new RFC. + +## Acceptance Criteria + +- No atproto account credential exists in CI configuration or logs. +- The retained grant is demonstrably create-only for the active release NSID. +- The service cannot write profile records with its retained grant. +- Every escalation and every `confirmation: always` release requires a current listed approver's UV passkey assertion. +- Changing any approval-bound input invalidates the approval. +- A PDS timeout cannot create a duplicate or cause a successful release to be reported permanently failed. +- An installer rejects missing required provenance and every failed provenance layer without trusting service or aggregator status. +- The hosted service supports multiple passkeys, email, webhooks, full publisher audit, and self-service delegation. +- The implementation runs under Workers with real D1/Queue tests and has no DO correctness dependency. +- A self-host can deploy the same Worker with its own D1, Queues, cron triggers, Email Service or mail adapter, and webhook configuration without changing protocol or API semantics. + +## Known Residual Risks + +- A compromised release service can use active grants to publish unwanted releases that still satisfy signed profile and provenance constraints. +- Compromise of the service encryption key exposes all retained publisher sessions for that deployment. +- Revoking a confidential client key does not invalidate already-issued access tokens immediately. +- Hosted CI and Sigstore are part of provenance trust; a compromised authorized workflow can produce genuinely attested malicious output. +- Confirmation remains unverifiable at install time until the protocol carries signed approval receipts. +- Current-profile enforcement by a direct installer cannot perfectly reconstruct policy at historical publication time. +- Unknown declared-access constraints must be treated conservatively, which can require approval for a change that is actually narrowing. +- D1 and PDS cannot commit atomically; deterministic keys and reconciliation reduce, but do not remove, operational complexity. + +## Implementation Blockers to Resolve First + +1. Decide the authoritative historical event source and trust model for aggregator policy ordering. diff --git a/apps/release-service/migrations/.gitkeep b/apps/release-service/migrations/.gitkeep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/apps/release-service/package.json b/apps/release-service/package.json new file mode 100644 index 0000000000..e27b5b632c --- /dev/null +++ b/apps/release-service/package.json @@ -0,0 +1,28 @@ +{ + "name": "@emdash-cms/release-service", + "version": "0.0.0", + "private": true, + "description": "Cloudflare Worker for delegated EmDash registry releases.", + "type": "module", + "scripts": { + "dev": "vite dev", + "build": "vite build", + "preview": "vite preview", + "deploy": "vite build && wrangler deploy", + "typecheck": "tsgo --noEmit", + "test": "vitest run && vitest run --config vitest.node.config.ts", + "types": "wrangler types -c wrangler.jsonc -c ../release-verifier/wrangler.jsonc" + }, + "devDependencies": { + "@cloudflare/vite-plugin": "catalog:", + "@cloudflare/vitest-pool-workers": "catalog:", + "@types/node": "catalog:", + "typescript": "catalog:", + "vite": "catalog:", + "vitest": "catalog:", + "wrangler": "catalog:" + }, + "dependencies": { + "jose": "^6.1.3" + } +} diff --git a/apps/release-service/public/.gitkeep b/apps/release-service/public/.gitkeep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/apps/release-service/src/api/auth.ts b/apps/release-service/src/api/auth.ts new file mode 100644 index 0000000000..a1da0f0811 --- /dev/null +++ b/apps/release-service/src/api/auth.ts @@ -0,0 +1,23 @@ +import { ApiError } from "./errors.js"; + +const BEARER_TOKEN_PATTERN = /^Bearer ([A-Za-z0-9._~+/-]+=*)$/i; + +export interface AuthenticatedActor { + subjectDid: string; +} + +export function requireBearerToken(request: Request): string { + const authorization = request.headers.get("authorization"); + const match = authorization?.match(BEARER_TOKEN_PATTERN); + if (!match?.[1]) { + throw new ApiError("UNAUTHENTICATED", 401, "Authentication required"); + } + return match[1]; +} + +export function requireAuthenticated(actor: AuthenticatedActor | null): AuthenticatedActor { + if (!actor) { + throw new ApiError("UNAUTHENTICATED", 401, "Authentication required"); + } + return actor; +} diff --git a/apps/release-service/src/api/errors.ts b/apps/release-service/src/api/errors.ts new file mode 100644 index 0000000000..0e0db03057 --- /dev/null +++ b/apps/release-service/src/api/errors.ts @@ -0,0 +1,45 @@ +export const API_ERROR_CODES = [ + "CONFIGURATION_ERROR", + "INTERNAL_ERROR", + "NOT_FOUND", + "METHOD_NOT_ALLOWED", + "UNAUTHENTICATED", + "FORBIDDEN", + "ORIGIN_NOT_ALLOWED", + "CSRF_INVALID", + "UNSUPPORTED_MEDIA_TYPE", + "PAYLOAD_TOO_LARGE", + "INVALID_JSON", + "INVALID_CURSOR", +] as const; + +export type ApiErrorCode = (typeof API_ERROR_CODES)[number]; + +export interface SerializedApiError { + code: ApiErrorCode; + message: string; + details?: unknown; +} + +export class ApiError extends Error { + readonly code: ApiErrorCode; + readonly status: number; + readonly details?: unknown; + + constructor(code: ApiErrorCode, status: number, message: string, details?: unknown) { + super(message); + this.name = "ApiError"; + this.code = code; + this.status = status; + this.details = details; + } +} + +export function serializeApiError(error: unknown): SerializedApiError { + if (error instanceof ApiError) { + return error.details === undefined + ? { code: error.code, message: error.message } + : { code: error.code, message: error.message, details: error.details }; + } + return { code: "INTERNAL_ERROR", message: "Internal server error" }; +} diff --git a/apps/release-service/src/api/owner.ts b/apps/release-service/src/api/owner.ts new file mode 100644 index 0000000000..3e878515cb --- /dev/null +++ b/apps/release-service/src/api/owner.ts @@ -0,0 +1,20 @@ +import type { AuthenticatedActor } from "./auth.js"; +import { ApiError } from "./errors.js"; + +export function requireOwner(actor: AuthenticatedActor, ownerDid: string): AuthenticatedActor { + if (actor.subjectDid !== ownerDid) { + throw new ApiError("FORBIDDEN", 403, "Not authorized for this resource"); + } + return actor; +} + +export function requireOwnerOr( + actor: AuthenticatedActor, + ownerDid: string, + isAuthorized: (actor: AuthenticatedActor) => boolean, +): AuthenticatedActor { + if (actor.subjectDid !== ownerDid && !isAuthorized(actor)) { + throw new ApiError("FORBIDDEN", 403, "Not authorized for this resource"); + } + return actor; +} diff --git a/apps/release-service/src/api/pagination.ts b/apps/release-service/src/api/pagination.ts new file mode 100644 index 0000000000..091d6a908d --- /dev/null +++ b/apps/release-service/src/api/pagination.ts @@ -0,0 +1,87 @@ +import { ApiError } from "./errors.js"; + +const DEFAULT_LIMIT = 50; +const MAX_LIMIT = 100; +const MAX_CURSOR_LENGTH = 2048; +const MAX_CURSOR_PART_LENGTH = 512; +const BASE64_PADDING_PATTERN = /=+$/; +const BASE64_URL_PATTERN = /^[A-Za-z0-9_-]+$/; + +export interface PaginationCursor { + version: 1; + orderValue: string; + id: string; +} + +export interface Pagination { + limit: number; + cursor?: PaginationCursor; +} + +function toBase64Url(value: string): string { + const bytes = new TextEncoder().encode(value); + let binary = ""; + for (const byte of bytes) binary += String.fromCharCode(byte); + return btoa(binary).replaceAll("+", "-").replaceAll("/", "_").replace(BASE64_PADDING_PATTERN, ""); +} + +function fromBase64Url(value: string): string { + if (!BASE64_URL_PATTERN.test(value) || value.length > MAX_CURSOR_LENGTH) { + throw new ApiError("INVALID_CURSOR", 400, "Invalid pagination cursor"); + } + const padded = value + .replaceAll("-", "+") + .replaceAll("_", "/") + .padEnd(Math.ceil(value.length / 4) * 4, "="); + const binary = atob(padded); + const bytes = Uint8Array.from(binary, (character) => character.charCodeAt(0)); + return new TextDecoder("utf-8", { fatal: true, ignoreBOM: false }).decode(bytes); +} + +export function encodeCursor(orderValue: string, id: string): string { + if ( + orderValue.length === 0 || + orderValue.length > MAX_CURSOR_PART_LENGTH || + id.length === 0 || + id.length > MAX_CURSOR_PART_LENGTH + ) { + throw new ApiError("INVALID_CURSOR", 400, "Invalid pagination cursor"); + } + const cursor = toBase64Url(JSON.stringify([1, orderValue, id])); + if (cursor.length > MAX_CURSOR_LENGTH) { + throw new ApiError("INVALID_CURSOR", 400, "Invalid pagination cursor"); + } + return cursor; +} + +export function decodeCursor(value: string): PaginationCursor { + try { + const parsed: unknown = JSON.parse(fromBase64Url(value)); + if ( + !Array.isArray(parsed) || + parsed.length !== 3 || + parsed[0] !== 1 || + typeof parsed[1] !== "string" || + parsed[1].length === 0 || + parsed[1].length > MAX_CURSOR_PART_LENGTH || + typeof parsed[2] !== "string" || + parsed[2].length === 0 || + parsed[2].length > MAX_CURSOR_PART_LENGTH + ) { + throw new Error("invalid cursor shape"); + } + return { version: 1, orderValue: parsed[1], id: parsed[2] }; + } catch { + throw new ApiError("INVALID_CURSOR", 400, "Invalid pagination cursor"); + } +} + +export function parsePagination(searchParams: URLSearchParams): Pagination { + const rawLimit = searchParams.get("limit"); + const parsedLimit = rawLimit === null ? DEFAULT_LIMIT : Number.parseInt(rawLimit, 10); + const limit = Number.isFinite(parsedLimit) + ? Math.min(MAX_LIMIT, Math.max(1, parsedLimit)) + : DEFAULT_LIMIT; + const rawCursor = searchParams.get("cursor"); + return rawCursor ? { limit, cursor: decodeCursor(rawCursor) } : { limit }; +} diff --git a/apps/release-service/src/api/request-id.ts b/apps/release-service/src/api/request-id.ts new file mode 100644 index 0000000000..f3360f5127 --- /dev/null +++ b/apps/release-service/src/api/request-id.ts @@ -0,0 +1,6 @@ +const REQUEST_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:-]{7,127}$/; + +export function getRequestId(request: Request): string { + const supplied = request.headers.get("x-request-id"); + return supplied && REQUEST_ID_PATTERN.test(supplied) ? supplied : crypto.randomUUID(); +} diff --git a/apps/release-service/src/api/response.ts b/apps/release-service/src/api/response.ts new file mode 100644 index 0000000000..f0c545e803 --- /dev/null +++ b/apps/release-service/src/api/response.ts @@ -0,0 +1,26 @@ +import { ApiError, serializeApiError } from "./errors.js"; + +export type ApiResponse = + | { data: T; requestId: string } + | { error: ReturnType; requestId: string }; + +const JSON_HEADERS = { + "cache-control": "no-store", + "content-type": "application/json; charset=utf-8", + "x-content-type-options": "nosniff", +} as const; + +export function apiSuccess(data: T, requestId: string, status = 200): Response { + return Response.json({ data, requestId } satisfies ApiResponse, { + status, + headers: { ...JSON_HEADERS, "x-request-id": requestId }, + }); +} + +export function apiFailure(error: unknown, requestId: string, fallbackStatus = 500): Response { + const status = error instanceof ApiError ? error.status : fallbackStatus; + return Response.json( + { error: serializeApiError(error), requestId } satisfies ApiResponse, + { status, headers: { ...JSON_HEADERS, "x-request-id": requestId } }, + ); +} diff --git a/apps/release-service/src/api/schema.ts b/apps/release-service/src/api/schema.ts new file mode 100644 index 0000000000..9d3ac3731b --- /dev/null +++ b/apps/release-service/src/api/schema.ts @@ -0,0 +1,65 @@ +import { ROUTES } from "../routes.js"; +import type { RouteDefinition } from "../routes.js"; +import { API_ERROR_CODES } from "./errors.js"; + +const requestIdSchema = { + type: "string", + pattern: "^[A-Za-z0-9][A-Za-z0-9._:-]{7,127}$", +} as const; + +const errorEnvelopeSchema = { + type: "object", + required: ["error", "requestId"], + additionalProperties: false, + properties: { + error: { + type: "object", + required: ["code", "message"], + additionalProperties: false, + properties: { + code: { type: "string", enum: API_ERROR_CODES }, + message: { type: "string" }, + details: {}, + }, + }, + requestId: requestIdSchema, + }, +} as const; + +export function generateApiSchema(routes: readonly RouteDefinition[] = ROUTES) { + const paths: Record> = {}; + for (const route of routes) { + const path = (paths[route.path] ??= {}); + path[route.method.toLowerCase()] = { + operationId: route.operationId, + summary: route.summary, + responses: { + [route.successStatus]: { + description: "Successful response", + content: { + "application/json": { + schema: { + type: "object", + required: ["data", "requestId"], + additionalProperties: false, + properties: { + data: route.successDataSchema, + requestId: requestIdSchema, + }, + }, + }, + }, + }, + default: { + description: "Error response", + content: { "application/json": { schema: errorEnvelopeSchema } }, + }, + }, + }; + } + return { + openapi: "3.1.0", + info: { title: "EmDash Delegated Release Service API", version: "1.0.0" }, + paths, + }; +} diff --git a/apps/release-service/src/api/security.ts b/apps/release-service/src/api/security.ts new file mode 100644 index 0000000000..e10dbe798c --- /dev/null +++ b/apps/release-service/src/api/security.ts @@ -0,0 +1,109 @@ +import type { ServiceConfiguration } from "../config.js"; +import { ApiError } from "./errors.js"; + +export const MAX_JSON_BODY_BYTES = 256 * 1024; + +const encoder = new TextEncoder(); + +async function secretsEqual(left: string, right: string): Promise { + const [leftHash, rightHash] = await Promise.all([ + crypto.subtle.digest("SHA-256", encoder.encode(left)), + crypto.subtle.digest("SHA-256", encoder.encode(right)), + ]); + return crypto.subtle.timingSafeEqual(leftHash, rightHash); +} + +function requireAllowedOrigin(request: Request, config: ServiceConfiguration): string { + const origin = request.headers.get("origin"); + if (!origin || !config.allowedOrigins.has(origin)) { + throw new ApiError("ORIGIN_NOT_ALLOWED", 403, "Request origin is not allowed"); + } + return origin; +} + +export function getCorsHeaders( + request: Request, + config: ServiceConfiguration, +): Record { + const origin = requireAllowedOrigin(request, config); + return { + "access-control-allow-credentials": "true", + "access-control-allow-origin": origin, + vary: "Origin", + }; +} + +export function getCorsPreflightHeaders( + request: Request, + config: ServiceConfiguration, + allowedMethods: readonly string[], +): Record { + return { + ...getCorsHeaders(request, config), + "access-control-allow-headers": + "Authorization, Content-Type, Idempotency-Key, X-EmDash-CSRF, X-Request-ID", + "access-control-allow-methods": allowedMethods.join(", "), + "access-control-max-age": "600", + }; +} + +async function readBoundedBody(request: Request, maxBytes: number): Promise { + const contentLength = request.headers.get("content-length"); + if (contentLength !== null) { + const declaredLength = Number(contentLength); + if (!Number.isSafeInteger(declaredLength) || declaredLength < 0 || declaredLength > maxBytes) { + throw new ApiError("PAYLOAD_TOO_LARGE", 413, "Request body is too large"); + } + } + if (!request.body) return new Uint8Array(); + + const reader = request.body.getReader(); + const chunks: Uint8Array[] = []; + let total = 0; + while (true) { + const { done, value } = await reader.read(); + if (done) break; + total += value.byteLength; + if (total > maxBytes) { + await reader.cancel(); + throw new ApiError("PAYLOAD_TOO_LARGE", 413, "Request body is too large"); + } + chunks.push(value); + } + const body = new Uint8Array(total); + let offset = 0; + for (const chunk of chunks) { + body.set(chunk, offset); + offset += chunk.byteLength; + } + return body; +} + +export async function parseJsonMutation( + request: Request, + config: ServiceConfiguration, + expectedCsrfToken: string | null, + maxBytes = MAX_JSON_BODY_BYTES, +): Promise { + if (!new Set(["POST", "PUT", "PATCH", "DELETE"]).has(request.method)) { + throw new ApiError("METHOD_NOT_ALLOWED", 405, "Mutation requires a state-changing HTTP method"); + } + requireAllowedOrigin(request, config); + const contentType = request.headers.get("content-type")?.split(";", 1)[0]?.trim().toLowerCase(); + if (contentType !== "application/json") { + throw new ApiError("UNSUPPORTED_MEDIA_TYPE", 415, "Content-Type must be application/json"); + } + if (!expectedCsrfToken) { + throw new ApiError("UNAUTHENTICATED", 401, "Authentication required"); + } + const suppliedCsrfToken = request.headers.get("x-emdash-csrf"); + if (!suppliedCsrfToken || !(await secretsEqual(suppliedCsrfToken, expectedCsrfToken))) { + throw new ApiError("CSRF_INVALID", 403, "CSRF validation failed"); + } + const bytes = await readBoundedBody(request, maxBytes); + try { + return JSON.parse(new TextDecoder("utf-8", { fatal: true, ignoreBOM: false }).decode(bytes)); + } catch { + throw new ApiError("INVALID_JSON", 400, "Request body must be valid JSON"); + } +} diff --git a/apps/release-service/src/config.ts b/apps/release-service/src/config.ts new file mode 100644 index 0000000000..f3f867e048 --- /dev/null +++ b/apps/release-service/src/config.ts @@ -0,0 +1,140 @@ +import { createEnvelopeEncryption, type EnvelopeEncryption } from "./crypto/encryption.js"; + +export type ConfigurationBindings = Record< + keyof Pick< + Env, + | "PUBLIC_ORIGIN" + | "ALLOWED_ORIGINS" + | "ALLOWED_PUBLISHERS" + | "DEPLOYMENT_POLICY" + | "ENCRYPTION_KEYRING" + >, + string +>; + +export type DeploymentPolicy = "hosted" | "self-hosted"; + +const DID_PATTERN = /^did:[a-z0-9]+:[A-Za-z0-9._:%-]+$/; + +interface AllowAllPublishers { + mode: "all"; +} + +interface AllowlistedPublishers { + mode: "allowlist"; + dids: ReadonlySet; +} + +type AllowedPublisherPolicy = AllowAllPublishers | AllowlistedPublishers; + +export interface ServiceConfiguration { + publicOrigin: string; + allowedOrigins: ReadonlySet; + deploymentPolicy: DeploymentPolicy; + encryption: EnvelopeEncryption; + isPublisherAllowed(did: string): boolean; +} + +export class ConfigurationError extends Error { + readonly issues: readonly string[]; + + constructor(issues: readonly string[]) { + super("Invalid release-service configuration"); + this.name = "ConfigurationError"; + this.issues = issues; + } +} + +function parseOrigin(value: unknown): string | null { + if (typeof value !== "string" || value.length === 0) return null; + try { + const url = new URL(value); + if (url.protocol !== "https:" || url.origin !== value) return null; + return url.origin; + } catch { + return null; + } +} + +function isRecord(value: unknown): value is Record { + return value !== null && typeof value === "object" && !Array.isArray(value); +} + +function parseAllowedOrigins(value: string): ReadonlySet | null { + try { + const parsed: unknown = JSON.parse(value); + if (!Array.isArray(parsed) || parsed.length === 0) return null; + const origins = parsed.map(parseOrigin); + if (origins.some((origin) => origin === null)) return null; + const validOrigins = new Set(); + for (const origin of origins) { + if (origin) validOrigins.add(origin); + } + return validOrigins; + } catch { + return null; + } +} + +function parseAllowedPublishers(value: string): AllowedPublisherPolicy | null { + try { + const parsed: unknown = JSON.parse(value); + if (!isRecord(parsed)) return null; + const record = parsed; + if (record["mode"] === "all" && Object.keys(record).length === 1) return { mode: "all" }; + const dids = record["dids"]; + if ( + record["mode"] !== "allowlist" || + Object.keys(record).some((key) => key !== "mode" && key !== "dids") || + !Array.isArray(dids) || + !dids.every((did) => typeof did === "string" && DID_PATTERN.test(did)) + ) { + return null; + } + return { mode: "allowlist", dids: new Set(dids) }; + } catch { + return null; + } +} + +export function loadConfiguration(bindings: ConfigurationBindings): ServiceConfiguration { + const issues: string[] = []; + const publicOrigin = parseOrigin(bindings.PUBLIC_ORIGIN); + if (!publicOrigin) issues.push("PUBLIC_ORIGIN_INVALID"); + const allowedOrigins = parseAllowedOrigins(bindings.ALLOWED_ORIGINS); + if (!allowedOrigins) issues.push("ALLOWED_ORIGINS_INVALID"); + else if (publicOrigin && !allowedOrigins.has(publicOrigin)) + issues.push("PUBLIC_ORIGIN_NOT_ALLOWED"); + const publisherPolicy = parseAllowedPublishers(bindings.ALLOWED_PUBLISHERS); + if (!publisherPolicy) issues.push("ALLOWED_PUBLISHERS_INVALID"); + const deploymentPolicy: DeploymentPolicy | null = + bindings.DEPLOYMENT_POLICY === "hosted" || bindings.DEPLOYMENT_POLICY === "self-hosted" + ? bindings.DEPLOYMENT_POLICY + : null; + if (!deploymentPolicy) { + issues.push("DEPLOYMENT_POLICY_INVALID"); + } + let encryption: EnvelopeEncryption | null = null; + try { + encryption = createEnvelopeEncryption(bindings.ENCRYPTION_KEYRING); + } catch { + issues.push("ENCRYPTION_KEYRING_INVALID"); + } + if ( + !publicOrigin || + !allowedOrigins || + !publisherPolicy || + !deploymentPolicy || + !encryption || + issues.length > 0 + ) { + throw new ConfigurationError(issues); + } + return { + publicOrigin, + allowedOrigins, + deploymentPolicy, + encryption, + isPublisherAllowed: (did) => publisherPolicy.mode === "all" || publisherPolicy.dids.has(did), + }; +} diff --git a/apps/release-service/src/crypto/encryption.ts b/apps/release-service/src/crypto/encryption.ts new file mode 100644 index 0000000000..bd4740e27e --- /dev/null +++ b/apps/release-service/src/crypto/encryption.ts @@ -0,0 +1,407 @@ +const ENVELOPE_VERSION = 1; +const NONCE_BYTES = 12; +const TAG_BYTES = 16; +const MAX_PLAINTEXT_BYTES = 1024 * 1024; +const MAX_ENVELOPE_CHARS = 1_500_000; +const MAX_KEYRING_CHARS = 64 * 1024; +const MAX_KEY_VERSION = 2_147_483_647; +const HKDF_SALT = new TextEncoder().encode( + JSON.stringify(["emdash-release-service", "hkdf-salt", ENVELOPE_VERSION]), +); +const DID_PATTERN = /^did:[a-z0-9]+:[A-Za-z0-9._:%-]+$/; +const TABLE_PATTERN = /^[a-z][a-z0-9_]{0,127}$/; +const BASE64URL_PATTERN = /^[A-Za-z0-9_-]+$/; +const BASE64_PADDING_PATTERN = /=+$/; +const OWNED_PURPOSES: ReadonlySet = new Set([ + "oauth-session", + "dpop-private-key", + "email-address", + "webhook-destination", + "webhook-secret", +]); +const UNOWNED_PURPOSES: ReadonlySet = new Set([ + "confidential-client-private-key", +]); +const OPTIONAL_OWNER_PURPOSES: ReadonlySet = new Set([ + "oauth-transaction", +]); + +export type OwnedEncryptionPurpose = + | "oauth-session" + | "dpop-private-key" + | "email-address" + | "webhook-destination" + | "webhook-secret"; + +export type OptionalOwnerEncryptionPurpose = "oauth-transaction"; + +export type UnownedEncryptionPurpose = "confidential-client-private-key"; + +export type EncryptionPurpose = + | OwnedEncryptionPurpose + | OptionalOwnerEncryptionPurpose + | UnownedEncryptionPurpose; + +interface EncryptionContextBase { + table: string; + primaryKey: string; +} + +export type EncryptionContext = EncryptionContextBase & + ( + | { purpose: OwnedEncryptionPurpose; ownerDid: string } + | { purpose: OptionalOwnerEncryptionPurpose; ownerDid: string | null } + | { purpose: UnownedEncryptionPurpose; ownerDid: null } + ); + +export interface EncryptedValue { + envelope: string; + keyVersion: number; +} + +export type EncryptionErrorCode = + | "ENCRYPTION_CONFIGURATION_INVALID" + | "ENCRYPTION_CONTEXT_INVALID" + | "ENCRYPTED_VALUE_INVALID" + | "ENCRYPTED_VALUE_UNSUPPORTED" + | "ENCRYPTION_KEY_UNAVAILABLE" + | "ENCRYPTION_FAILED" + | "DECRYPTION_FAILED"; + +const ERROR_MESSAGES: Record = { + ENCRYPTION_CONFIGURATION_INVALID: "Invalid encryption configuration", + ENCRYPTION_CONTEXT_INVALID: "Invalid encryption context", + ENCRYPTED_VALUE_INVALID: "Invalid encrypted value", + ENCRYPTED_VALUE_UNSUPPORTED: "Unsupported encrypted value", + ENCRYPTION_KEY_UNAVAILABLE: "Encryption key is unavailable", + ENCRYPTION_FAILED: "Encryption failed", + DECRYPTION_FAILED: "Decryption failed", +}; + +export class EncryptionError extends Error { + readonly code: EncryptionErrorCode; + + constructor(code: EncryptionErrorCode) { + super(ERROR_MESSAGES[code]); + this.name = "EncryptionError"; + this.code = code; + } +} + +interface EncryptionKeyring { + currentVersion: number; + keys: ReadonlyMap>; +} + +interface SerializedEnvelope { + v: number; + k: number; + n: string; + c: string; +} + +export interface EnvelopeEncryption { + readonly currentKeyVersion: number; + encrypt(plaintext: Uint8Array, context: EncryptionContext): Promise; + decrypt(envelope: string, context: EncryptionContext): Promise>; + needsRotation(envelope: string): boolean; + rotate(envelope: string, context: EncryptionContext): Promise; +} + +function isRecord(value: unknown): value is Record { + return value !== null && typeof value === "object" && !Array.isArray(value); +} + +function hasExactKeys(record: Record, expected: readonly string[]): boolean { + const keys = Object.keys(record); + return keys.length === expected.length && keys.every((key) => expected.includes(key)); +} + +function isKeyVersion(value: unknown): value is number { + return Number.isInteger(value) && Number(value) >= 1 && Number(value) <= MAX_KEY_VERSION; +} + +function encodeBase64Url(bytes: Uint8Array): string { + let binary = ""; + const chunkSize = 32 * 1024; + for (let offset = 0; offset < bytes.length; offset += chunkSize) { + binary += String.fromCharCode(...bytes.subarray(offset, offset + chunkSize)); + } + return btoa(binary).replaceAll("+", "-").replaceAll("/", "_").replace(BASE64_PADDING_PATTERN, ""); +} + +function decodeBase64Url(value: unknown): Uint8Array | null { + if ( + typeof value !== "string" || + value.length === 0 || + !BASE64URL_PATTERN.test(value) || + value.length % 4 === 1 + ) { + return null; + } + try { + const padded = value + .replaceAll("-", "+") + .replaceAll("_", "/") + .padEnd(value.length + ((4 - (value.length % 4)) % 4), "="); + const binary = atob(padded); + const bytes = new Uint8Array(binary.length); + for (let index = 0; index < binary.length; index += 1) { + bytes[index] = binary.charCodeAt(index); + } + return encodeBase64Url(bytes) === value ? bytes : null; + } catch { + return null; + } +} + +function invalidConfiguration(): never { + throw new EncryptionError("ENCRYPTION_CONFIGURATION_INVALID"); +} + +function parseKeyring(value: string): EncryptionKeyring { + if (typeof value !== "string" || value.length === 0 || value.length > MAX_KEYRING_CHARS) { + invalidConfiguration(); + } + let parsed: unknown; + try { + parsed = JSON.parse(value); + } catch { + invalidConfiguration(); + } + if (!isRecord(parsed) || !hasExactKeys(parsed, ["current", "keys"])) { + invalidConfiguration(); + } + const current = parsed["current"]; + const entries = parsed["keys"]; + if (!isKeyVersion(current) || !Array.isArray(entries) || entries.length === 0) { + invalidConfiguration(); + } + const keys = new Map>(); + for (const entry of entries) { + if (!isRecord(entry) || !hasExactKeys(entry, ["version", "key"])) { + invalidConfiguration(); + } + const version = entry["version"]; + const key = decodeBase64Url(entry["key"]); + if (!isKeyVersion(version) || !key || key.length !== 32 || keys.has(version)) { + invalidConfiguration(); + } + keys.set(version, key); + } + if (!keys.has(current)) invalidConfiguration(); + return { currentVersion: current, keys }; +} + +function parseEnvelope(value: string): SerializedEnvelope { + if (typeof value !== "string" || value.length === 0 || value.length > MAX_ENVELOPE_CHARS) { + throw new EncryptionError("ENCRYPTED_VALUE_INVALID"); + } + let parsed: unknown; + try { + parsed = JSON.parse(value); + } catch { + throw new EncryptionError("ENCRYPTED_VALUE_INVALID"); + } + if (!isRecord(parsed) || !hasExactKeys(parsed, ["v", "k", "n", "c"])) { + throw new EncryptionError("ENCRYPTED_VALUE_INVALID"); + } + if (parsed["v"] !== ENVELOPE_VERSION) { + throw new EncryptionError("ENCRYPTED_VALUE_UNSUPPORTED"); + } + const keyVersion = parsed["k"]; + const encodedNonce = parsed["n"]; + const encodedCiphertext = parsed["c"]; + const nonce = decodeBase64Url(encodedNonce); + const ciphertext = decodeBase64Url(encodedCiphertext); + if ( + !isKeyVersion(keyVersion) || + typeof encodedNonce !== "string" || + typeof encodedCiphertext !== "string" || + !nonce || + nonce.length !== NONCE_BYTES || + !ciphertext || + ciphertext.length < TAG_BYTES || + ciphertext.length > MAX_PLAINTEXT_BYTES + TAG_BYTES + ) { + throw new EncryptionError("ENCRYPTED_VALUE_INVALID"); + } + const envelope = { + v: ENVELOPE_VERSION, + k: keyVersion, + n: encodedNonce, + c: encodedCiphertext, + }; + if (JSON.stringify(envelope) !== value) { + throw new EncryptionError("ENCRYPTED_VALUE_INVALID"); + } + return envelope; +} + +function snapshotContext(context: EncryptionContext): EncryptionContext { + if (!isRecord(context)) { + throw new EncryptionError("ENCRYPTION_CONTEXT_INVALID"); + } + const purpose = context["purpose"]; + const hasOwnedPurpose = OWNED_PURPOSES.has(purpose); + const hasUnownedPurpose = UNOWNED_PURPOSES.has(purpose); + const hasOptionalOwnerPurpose = OPTIONAL_OWNER_PURPOSES.has(purpose); + const hasValidDid = + typeof context.ownerDid === "string" && + context.ownerDid.length <= 2048 && + DID_PATTERN.test(context.ownerDid); + if ( + (!hasOwnedPurpose && !hasUnownedPurpose && !hasOptionalOwnerPurpose) || + typeof context.table !== "string" || + !TABLE_PATTERN.test(context.table) || + typeof context.primaryKey !== "string" || + context.primaryKey.length === 0 || + context.primaryKey.length > 512 || + (hasOwnedPurpose && !hasValidDid) || + (hasUnownedPurpose && context.ownerDid !== null) || + (hasOptionalOwnerPurpose && context.ownerDid !== null && !hasValidDid) + ) { + throw new EncryptionError("ENCRYPTION_CONTEXT_INVALID"); + } + return { ...context }; +} + +function encodeInfo(keyVersion: number, purpose: EncryptionPurpose): Uint8Array { + return Uint8Array.from( + new TextEncoder().encode( + JSON.stringify(["emdash-release-service", "data-key", ENVELOPE_VERSION, keyVersion, purpose]), + ), + ); +} + +function encodeAdditionalData( + keyVersion: number, + context: EncryptionContext, +): Uint8Array { + return Uint8Array.from( + new TextEncoder().encode( + JSON.stringify([ + "emdash-release-service", + "ciphertext", + ENVELOPE_VERSION, + keyVersion, + context.purpose, + context.table, + context.primaryKey, + context.ownerDid, + ]), + ), + ); +} + +class WebCryptoEnvelopeEncryption implements EnvelopeEncryption { + readonly currentKeyVersion: number; + readonly #keys: ReadonlyMap>; + readonly #derivedKeys = new Map>(); + + constructor(keyring: EncryptionKeyring) { + this.currentKeyVersion = keyring.currentVersion; + this.#keys = keyring.keys; + } + + async #deriveKey(keyVersion: number, purpose: EncryptionPurpose): Promise { + const cacheKey = `${keyVersion}:${purpose}`; + let derived = this.#derivedKeys.get(cacheKey); + if (!derived) { + const masterKey = this.#keys.get(keyVersion); + if (!masterKey) throw new EncryptionError("ENCRYPTION_KEY_UNAVAILABLE"); + derived = crypto.subtle + .importKey("raw", masterKey, "HKDF", false, ["deriveKey"]) + .then((key) => + crypto.subtle.deriveKey( + { + name: "HKDF", + hash: "SHA-256", + salt: HKDF_SALT, + info: encodeInfo(keyVersion, purpose), + }, + key, + { name: "AES-GCM", length: 256 }, + false, + ["encrypt", "decrypt"], + ), + ); + this.#derivedKeys.set(cacheKey, derived); + } + return derived; + } + + async encrypt(plaintext: Uint8Array, context: EncryptionContext): Promise { + const contextSnapshot = snapshotContext(context); + if (!(plaintext instanceof Uint8Array) || plaintext.length > MAX_PLAINTEXT_BYTES) { + throw new EncryptionError("ENCRYPTION_FAILED"); + } + const keyVersion = this.currentKeyVersion; + const plaintextCopy = Uint8Array.from(plaintext); + try { + const nonce = crypto.getRandomValues(new Uint8Array(NONCE_BYTES)); + const key = await this.#deriveKey(keyVersion, contextSnapshot.purpose); + const encrypted = await crypto.subtle.encrypt( + { + name: "AES-GCM", + iv: nonce, + additionalData: encodeAdditionalData(keyVersion, contextSnapshot), + tagLength: 128, + }, + key, + plaintextCopy, + ); + const envelope = JSON.stringify({ + v: ENVELOPE_VERSION, + k: keyVersion, + n: encodeBase64Url(nonce), + c: encodeBase64Url(new Uint8Array(encrypted)), + }); + return { envelope, keyVersion }; + } catch { + throw new EncryptionError("ENCRYPTION_FAILED"); + } + } + + async decrypt(envelope: string, context: EncryptionContext): Promise> { + const contextSnapshot = snapshotContext(context); + const parsed = parseEnvelope(envelope); + if (!this.#keys.has(parsed.k)) { + throw new EncryptionError("ENCRYPTION_KEY_UNAVAILABLE"); + } + try { + const key = await this.#deriveKey(parsed.k, contextSnapshot.purpose); + const decrypted = await crypto.subtle.decrypt( + { + name: "AES-GCM", + iv: decodeBase64Url(parsed.n)!, + additionalData: encodeAdditionalData(parsed.k, contextSnapshot), + tagLength: 128, + }, + key, + decodeBase64Url(parsed.c)!, + ); + return new Uint8Array(decrypted); + } catch { + throw new EncryptionError("DECRYPTION_FAILED"); + } + } + + needsRotation(envelope: string): boolean { + return parseEnvelope(envelope).k !== this.currentKeyVersion; + } + + async rotate(envelope: string, context: EncryptionContext): Promise { + const contextSnapshot = snapshotContext(context); + const parsed = parseEnvelope(envelope); + const plaintext = await this.decrypt(envelope, contextSnapshot); + if (parsed.k === this.currentKeyVersion) { + return { envelope, keyVersion: parsed.k }; + } + return this.encrypt(plaintext, contextSnapshot); + } +} + +export function createEnvelopeEncryption(keyring: string): EnvelopeEncryption { + return new WebCryptoEnvelopeEncryption(parseKeyring(keyring)); +} diff --git a/apps/release-service/src/index.ts b/apps/release-service/src/index.ts new file mode 100644 index 0000000000..859d1b1071 --- /dev/null +++ b/apps/release-service/src/index.ts @@ -0,0 +1,78 @@ +import { ApiError } from "./api/errors.js"; +import { getRequestId } from "./api/request-id.js"; +import { apiFailure } from "./api/response.js"; +import { ConfigurationError, loadConfiguration, type ConfigurationBindings } from "./config.js"; +import { ROUTES } from "./routes.js"; +import type { RouteDefinition } from "./routes.js"; + +const UNSUPPORTED_QUEUE_RETRY_SECONDS = 300; + +export interface RetryableQueueBatch { + queue: string; + messages: readonly unknown[]; + retryAll(options?: QueueRetryOptions): void; +} + +export async function handleRequest( + request: Request, + bindings: ConfigurationBindings, + routes: readonly RouteDefinition[] = ROUTES, +): Promise { + const requestId = getRequestId(request); + try { + const configuration = loadConfiguration(bindings); + const url = new URL(request.url); + const route = routes.find( + (candidate) => candidate.path === url.pathname && candidate.method === request.method, + ); + if (route) return await route.handler(request, requestId, configuration); + if (routes.some((candidate) => candidate.path === url.pathname)) { + return apiFailure(new ApiError("METHOD_NOT_ALLOWED", 405, "Method not allowed"), requestId); + } + return apiFailure(new ApiError("NOT_FOUND", 404, "Not found"), requestId); + } catch (error) { + if (error instanceof ConfigurationError) { + console.error(JSON.stringify({ event: "configuration_error", issues: error.issues })); + return apiFailure( + new ApiError("CONFIGURATION_ERROR", 503, "Service is not configured"), + requestId, + ); + } + console.error( + JSON.stringify({ + event: "request_error", + requestId, + error: error instanceof Error ? (error.stack ?? error.message) : String(error), + }), + ); + return apiFailure(error, requestId); + } +} + +export function retryUnsupportedQueue(batch: RetryableQueueBatch): void { + console.error( + JSON.stringify({ + event: "unsupported_queue_message", + queue: batch.queue, + messageCount: batch.messages.length, + }), + ); + batch.retryAll({ delaySeconds: UNSUPPORTED_QUEUE_RETRY_SECONDS }); +} + +export function failInactiveSchedule(scheduledTime: number): never { + console.error(JSON.stringify({ event: "scheduled_lifecycle_not_active", scheduledTime })); + throw new Error("Scheduled lifecycle is not active"); +} + +export default { + fetch(request: Request, env: Env): Promise { + return handleRequest(request, env); + }, + queue(batch: MessageBatch): void { + retryUnsupportedQueue(batch); + }, + scheduled(event: ScheduledController): void { + failInactiveSchedule(event.scheduledTime); + }, +} satisfies ExportedHandler; diff --git a/apps/release-service/src/routes.ts b/apps/release-service/src/routes.ts new file mode 100644 index 0000000000..138951bc3d --- /dev/null +++ b/apps/release-service/src/routes.ts @@ -0,0 +1,35 @@ +import { apiSuccess } from "./api/response.js"; +import type { ServiceConfiguration } from "./config.js"; + +export type RouteMethod = "GET" | "POST" | "PUT" | "PATCH" | "DELETE"; + +export interface RouteDefinition { + method: RouteMethod; + path: string; + operationId: string; + summary: string; + successStatus: number; + successDataSchema: Readonly>; + handler( + request: Request, + requestId: string, + configuration: ServiceConfiguration, + ): Response | Promise; +} + +export const ROUTES = Object.freeze([ + { + method: "GET", + path: "/health", + operationId: "getHealth", + summary: "Check release-service health", + successStatus: 200, + successDataSchema: { + type: "object", + required: ["status"], + additionalProperties: false, + properties: { status: { const: "ok" } }, + }, + handler: (_request, requestId) => apiSuccess({ status: "ok" }, requestId), + }, +] as const satisfies readonly RouteDefinition[]); diff --git a/apps/release-service/src/verifier.ts b/apps/release-service/src/verifier.ts new file mode 100644 index 0000000000..d0cb1d6d66 --- /dev/null +++ b/apps/release-service/src/verifier.ts @@ -0,0 +1,66 @@ +type VerifierMethod = "fetchArtifact" | "fetchProvenance"; + +export type ReleaseVerifierResult = + | { success: true; value: Uint8Array } + | { success: false; error: { code: string; message: string } }; + +export interface ReleaseVerifierBinding { + fetchArtifact(url: string): Promise; + fetchProvenance(url: string): Promise; +} + +export class VerifierUnavailableError extends Error { + readonly retryable = true; + + constructor(options?: ErrorOptions) { + super("Release verifier is unavailable", options); + this.name = "VerifierUnavailableError"; + } +} + +export async function fetchArtifact( + binding: ReleaseVerifierBinding, + url: string, +): Promise { + return callVerifier(binding, "fetchArtifact", url); +} + +export async function fetchProvenance( + binding: ReleaseVerifierBinding, + url: string, +): Promise { + return callVerifier(binding, "fetchProvenance", url); +} + +async function callVerifier( + binding: ReleaseVerifierBinding, + method: VerifierMethod, + url: string, +): Promise { + try { + const result = await binding[method](url); + if (!isVerificationResult(result)) { + throw new TypeError("Release verifier returned an invalid response"); + } + return result; + } catch (error) { + if (error instanceof VerifierUnavailableError) throw error; + throw new VerifierUnavailableError({ cause: error }); + } +} + +function isVerificationResult(value: unknown): value is ReleaseVerifierResult { + if (!isRecord(value) || typeof value["success"] !== "boolean") return false; + if (value["success"] === true) return value["value"] instanceof Uint8Array; + return isVerificationError(value["error"]); +} + +function isVerificationError(value: unknown): value is { code: string; message: string } { + return ( + isRecord(value) && typeof value["code"] === "string" && typeof value["message"] === "string" + ); +} + +function isRecord(value: unknown): value is Record { + return value !== null && typeof value === "object" && !Array.isArray(value); +} diff --git a/apps/release-service/src/workload/github-actions.ts b/apps/release-service/src/workload/github-actions.ts new file mode 100644 index 0000000000..f67020ec0c --- /dev/null +++ b/apps/release-service/src/workload/github-actions.ts @@ -0,0 +1,560 @@ +import { + base64url, + createLocalJWKSet, + decodeProtectedHeader, + errors, + importJWK, + jwtVerify, + type JSONWebKeySet, + type JWTPayload, +} from "jose"; + +import type { + VerifiedWorkload, + WorkloadIssuer, + WorkloadVerificationErrorCode, + WorkloadVerificationOptions, + WorkloadVerificationResult, +} from "./types.js"; + +const ALGORITHM = "RS256"; +const DEFAULT_TIMEOUT_MS = 10_000; +const DEFAULT_MAX_RESPONSE_BYTES = 100_000; +const DEFAULT_MAX_TOKEN_BYTES = 16_384; +const DEFAULT_MAX_TOKEN_AGE_SECONDS = 600; +const ERROR_MESSAGE = "Workload identity verification failed" as const; +const BASE64URL_PATTERN = /^[A-Za-z0-9_-]+$/; +const DECIMAL_ID_PATTERN = /^[1-9]\d*$/; +const REPOSITORY_PATTERN = + /^[A-Za-z0-9](?:[A-Za-z0-9._-]{0,99})\/[A-Za-z0-9](?:[A-Za-z0-9._-]{0,99})$/; +const SHA_PATTERN = /^[0-9a-f]{40}$/; +const REF_PREFIX_PATTERN = /^refs\/(?:heads|tags|pull)\//; +const WORKFLOW_PATH_PATTERN = /^[A-Za-z0-9._-]+(?:\/[A-Za-z0-9._-]+)*\.ya?ml$/; + +interface DiscoveryMetadata { + issuer: string; + jwks_uri: string; + id_token_signing_alg_values_supported: string[]; +} + +export interface GitHubActionsIssuerOptions { + issuer: string; + fetch?: typeof fetch; + now?: () => number; + timeoutMs?: number; + maxResponseBytes?: number; + maxTokenBytes?: number; + maxTokenAgeSeconds?: number; +} + +class VerificationFailure extends Error { + constructor(readonly code: WorkloadVerificationErrorCode) { + super(ERROR_MESSAGE); + } +} + +function failure(code: WorkloadVerificationErrorCode): WorkloadVerificationResult { + return { success: false, error: { code, message: ERROR_MESSAGE } }; +} + +function isRecord(value: unknown): value is Record { + return value !== null && typeof value === "object" && !Array.isArray(value); +} + +function isSafeText(value: string): boolean { + for (const character of value) { + const code = character.charCodeAt(0); + if (code <= 31 || code === 127) return false; + } + return true; +} + +function requirePositiveInteger(value: number, name: string): number { + if (!Number.isSafeInteger(value) || value <= 0) throw new TypeError(`${name} must be positive`); + return value; +} + +function parseTrustedIssuer(value: string): URL { + const url = new URL(value); + if ( + url.protocol !== "https:" || + url.username || + url.password || + url.search || + url.hash || + url.pathname !== "/" || + value !== url.origin + ) { + throw new TypeError("issuer must be a canonical HTTPS origin"); + } + return url; +} + +function composeAbortSignal( + timeoutMs: number, + externalSignal?: AbortSignal, +): { signal: AbortSignal; cleanup(): void } { + const controller = new AbortController(); + const abortFromExternal = () => controller.abort(externalSignal?.reason); + if (externalSignal?.aborted) abortFromExternal(); + else externalSignal?.addEventListener("abort", abortFromExternal, { once: true }); + const timeout = setTimeout(() => controller.abort(new Error("timeout")), timeoutMs); + return { + signal: controller.signal, + cleanup() { + clearTimeout(timeout); + externalSignal?.removeEventListener("abort", abortFromExternal); + }, + }; +} + +async function readBoundedJson(response: Response, maxBytes: number): Promise { + const declaredLength = response.headers.get("content-length"); + if (declaredLength !== null) { + const parsedLength = Number(declaredLength); + if (!Number.isSafeInteger(parsedLength) || parsedLength < 0 || parsedLength > maxBytes) { + await response.body?.cancel(); + throw new Error("response size invalid"); + } + } + if (!response.body) throw new Error("response body missing"); + const reader = response.body.getReader(); + const chunks: Uint8Array[] = []; + let total = 0; + try { + while (true) { + const { done, value } = await reader.read(); + if (done) break; + total += value.byteLength; + if (total > maxBytes) { + await reader.cancel(); + throw new Error("response too large"); + } + chunks.push(value); + } + } finally { + reader.releaseLock(); + } + const body = new Uint8Array(total); + let offset = 0; + for (const chunk of chunks) { + body.set(chunk, offset); + offset += chunk.byteLength; + } + return JSON.parse(new TextDecoder().decode(body)) as unknown; +} + +function parseDiscovery(value: unknown, issuer: URL): DiscoveryMetadata { + if (!isRecord(value)) throw new VerificationFailure("WORKLOAD_DISCOVERY_INVALID"); + const metadataIssuer = value["issuer"]; + const jwksUri = value["jwks_uri"]; + const algorithms = value["id_token_signing_alg_values_supported"]; + if ( + metadataIssuer !== issuer.origin || + typeof jwksUri !== "string" || + !Array.isArray(algorithms) || + !algorithms.every((algorithm) => typeof algorithm === "string") || + !algorithms.includes(ALGORITHM) + ) { + throw new VerificationFailure("WORKLOAD_DISCOVERY_INVALID"); + } + let jwksUrl: URL; + try { + jwksUrl = new URL(jwksUri); + } catch { + throw new VerificationFailure("WORKLOAD_DISCOVERY_INVALID"); + } + if ( + jwksUrl.protocol !== "https:" || + jwksUrl.origin !== issuer.origin || + jwksUrl.username || + jwksUrl.password || + jwksUrl.hash + ) { + throw new VerificationFailure("WORKLOAD_DISCOVERY_INVALID"); + } + return { + issuer: metadataIssuer, + jwks_uri: jwksUrl.href, + id_token_signing_alg_values_supported: algorithms, + }; +} + +function decodeRsaInteger(value: string): Uint8Array { + if (!BASE64URL_PATTERN.test(value)) { + throw new VerificationFailure("WORKLOAD_JWKS_INVALID"); + } + let decoded: Uint8Array; + try { + decoded = base64url.decode(value); + } catch { + throw new VerificationFailure("WORKLOAD_JWKS_INVALID"); + } + if (decoded.length === 0 || decoded[0] === 0 || base64url.encode(decoded) !== value) { + throw new VerificationFailure("WORKLOAD_JWKS_INVALID"); + } + return decoded; +} + +function validateRsaMaterial(modulus: string, exponent: string): void { + const modulusBytes = decodeRsaInteger(modulus); + if ( + modulusBytes.byteLength < 256 || + (modulusBytes.byteLength === 256 && (modulusBytes[0] ?? 0) < 0x80) + ) { + throw new VerificationFailure("WORKLOAD_JWKS_INVALID"); + } + const exponentBytes = decodeRsaInteger(exponent); + if (exponentBytes.byteLength > 4) { + throw new VerificationFailure("WORKLOAD_JWKS_INVALID"); + } + let exponentValue = 0; + for (const byte of exponentBytes) exponentValue = exponentValue * 256 + byte; + if (exponentValue < 3 || exponentValue % 2 === 0) { + throw new VerificationFailure("WORKLOAD_JWKS_INVALID"); + } +} + +async function parseJwks(value: unknown): Promise { + if (!isRecord(value) || !Array.isArray(value["keys"]) || value["keys"].length === 0) { + throw new VerificationFailure("WORKLOAD_JWKS_INVALID"); + } + const keyIds = new Set(); + const keys: JSONWebKeySet["keys"] = []; + for (const key of value["keys"]) { + if ( + !isRecord(key) || + key["kty"] !== "RSA" || + typeof key["kid"] !== "string" || + key["kid"].length === 0 || + typeof key["n"] !== "string" || + typeof key["e"] !== "string" || + ("alg" in key && key["alg"] !== ALGORITHM) || + ("use" in key && key["use"] !== "sig") || + "d" in key + ) { + throw new VerificationFailure("WORKLOAD_JWKS_INVALID"); + } + const keyOperations = key["key_ops"]; + if ( + keyOperations !== undefined && + (!Array.isArray(keyOperations) || keyOperations.length !== 1 || keyOperations[0] !== "verify") + ) { + throw new VerificationFailure("WORKLOAD_JWKS_INVALID"); + } + if (keyIds.has(key["kid"])) throw new VerificationFailure("WORKLOAD_JWKS_INVALID"); + keyIds.add(key["kid"]); + validateRsaMaterial(key["n"], key["e"]); + const validatedKey = { + kty: "RSA", + kid: key["kid"], + n: key["n"], + e: key["e"], + alg: ALGORITHM, + use: "sig", + ...(keyOperations === undefined ? {} : { key_ops: ["verify"] }), + }; + try { + await importJWK(validatedKey, ALGORITHM); + } catch { + throw new VerificationFailure("WORKLOAD_JWKS_INVALID"); + } + keys.push(validatedKey); + } + return { keys }; +} + +function isSignatureFailure(error: unknown): boolean { + return ( + error instanceof errors.JWKSNoMatchingKey || + error instanceof errors.JWSSignatureVerificationFailed + ); +} + +function classifyJwtError(error: unknown): WorkloadVerificationErrorCode { + if (error instanceof errors.JWTExpired) return "WORKLOAD_TOKEN_EXPIRED"; + if (error instanceof errors.JWTClaimValidationFailed && error.claim === "nbf") { + return "WORKLOAD_TOKEN_NOT_ACTIVE"; + } + if (isSignatureFailure(error)) return "WORKLOAD_TOKEN_SIGNATURE_INVALID"; + return "WORKLOAD_TOKEN_MALFORMED"; +} + +function requiredString(payload: JWTPayload, claim: string, maxLength = 1024): string { + const value = payload[claim]; + if ( + typeof value !== "string" || + value.length === 0 || + value.length > maxLength || + !isSafeText(value) + ) { + throw new VerificationFailure("WORKLOAD_CLAIMS_INVALID"); + } + return value; +} + +function optionalString(payload: JWTPayload, claim: string, maxLength = 255): string | undefined { + if (payload[claim] === undefined) return undefined; + return requiredString(payload, claim, maxLength); +} + +function canonicalId(payload: JWTPayload, claim: string): string { + const value = requiredString(payload, claim, 32); + if (!DECIMAL_ID_PATTERN.test(value)) { + throw new VerificationFailure("WORKLOAD_CLAIMS_INVALID"); + } + return value; +} + +function validRef(value: string): boolean { + if ( + value.length > 1024 || + !isSafeText(value) || + !REF_PREFIX_PATTERN.test(value) || + value.includes("..") || + value.includes("@{") || + value.includes("//") || + value.endsWith("/") || + value.endsWith(".") + ) { + return false; + } + for (const character of value) { + if (" ~^:?*[\\".includes(character)) return false; + } + return value.split("/").every((part) => !part.startsWith(".") && !part.endsWith(".lock")); +} + +function validWorkflowRef(value: string, expectedRepository?: string): boolean { + const marker = "/.github/workflows/"; + const markerIndex = value.indexOf(marker); + const separatorIndex = value.indexOf("@", markerIndex + marker.length); + if (markerIndex <= 0 || separatorIndex <= markerIndex + marker.length) return false; + const repository = value.slice(0, markerIndex); + const workflowPath = value.slice(markerIndex + marker.length, separatorIndex); + const workflowVersion = value.slice(separatorIndex + 1); + return ( + REPOSITORY_PATTERN.test(repository) && + (!expectedRepository || repository === expectedRepository) && + WORKFLOW_PATH_PATTERN.test(workflowPath) && + !workflowPath.split("/").includes("..") && + (validRef(workflowVersion) || SHA_PATTERN.test(workflowVersion)) + ); +} + +function normalizePayload( + payload: JWTPayload, + issuer: string, + expectedAudience: string, + now: number, + maxTokenAgeSeconds: number, +): VerifiedWorkload { + if (payload.iss !== issuer) throw new VerificationFailure("WORKLOAD_TOKEN_ISSUER_INVALID"); + if (payload.aud !== expectedAudience) { + throw new VerificationFailure("WORKLOAD_TOKEN_AUDIENCE_INVALID"); + } + const expiresAt = payload.exp; + if (!Number.isSafeInteger(expiresAt)) { + throw new VerificationFailure("WORKLOAD_CLAIMS_INVALID"); + } + if (expiresAt === undefined || expiresAt <= now) { + throw new VerificationFailure("WORKLOAD_TOKEN_EXPIRED"); + } + if (payload.nbf !== undefined && !Number.isSafeInteger(payload.nbf)) { + throw new VerificationFailure("WORKLOAD_CLAIMS_INVALID"); + } + if (payload.nbf !== undefined && payload.nbf > now) { + throw new VerificationFailure("WORKLOAD_TOKEN_NOT_ACTIVE"); + } + const issuedAt = payload.iat; + if ( + !Number.isSafeInteger(issuedAt) || + issuedAt === undefined || + issuedAt > now || + issuedAt < now - maxTokenAgeSeconds + ) { + throw new VerificationFailure("WORKLOAD_TOKEN_IAT_INVALID"); + } + const repository = requiredString(payload, "repository", 201); + if (!REPOSITORY_PATTERN.test(repository)) { + throw new VerificationFailure("WORKLOAD_CLAIMS_INVALID"); + } + const workflowRef = requiredString(payload, "workflow_ref", 1024); + const jobWorkflowRef = optionalString(payload, "job_workflow_ref", 1024); + const environment = optionalString(payload, "environment"); + const ref = requiredString(payload, "ref", 1024); + const sha = requiredString(payload, "sha", 40); + if ( + !validWorkflowRef(workflowRef, repository) || + (jobWorkflowRef !== undefined && !validWorkflowRef(jobWorkflowRef)) || + !validRef(ref) || + !SHA_PATTERN.test(sha) + ) { + throw new VerificationFailure("WORKLOAD_CLAIMS_INVALID"); + } + return { + issuer, + subject: requiredString(payload, "sub"), + repository, + repositoryId: canonicalId(payload, "repository_id"), + repositoryOwnerId: canonicalId(payload, "repository_owner_id"), + workflowRef, + ...(jobWorkflowRef === undefined ? {} : { jobWorkflowRef }), + ref, + sha, + runId: canonicalId(payload, "run_id"), + runAttempt: canonicalId(payload, "run_attempt"), + ...(environment === undefined ? {} : { environment }), + expiresAt, + }; +} + +export class GitHubActionsIssuer implements WorkloadIssuer { + readonly #issuer: URL; + readonly #fetch: typeof fetch; + readonly #now: () => number; + readonly #timeoutMs: number; + readonly #maxResponseBytes: number; + readonly #maxTokenBytes: number; + readonly #maxTokenAgeSeconds: number; + + constructor(options: GitHubActionsIssuerOptions) { + this.#issuer = parseTrustedIssuer(options.issuer); + const transport = options.fetch ?? fetch; + this.#fetch = (input, init) => transport(input, init); + this.#now = options.now ?? (() => Math.floor(Date.now() / 1000)); + this.#timeoutMs = requirePositiveInteger(options.timeoutMs ?? DEFAULT_TIMEOUT_MS, "timeoutMs"); + this.#maxResponseBytes = requirePositiveInteger( + options.maxResponseBytes ?? DEFAULT_MAX_RESPONSE_BYTES, + "maxResponseBytes", + ); + this.#maxTokenBytes = requirePositiveInteger( + options.maxTokenBytes ?? DEFAULT_MAX_TOKEN_BYTES, + "maxTokenBytes", + ); + this.#maxTokenAgeSeconds = requirePositiveInteger( + options.maxTokenAgeSeconds ?? DEFAULT_MAX_TOKEN_AGE_SECONDS, + "maxTokenAgeSeconds", + ); + } + + async verify( + token: string, + expectedAudience: string, + options: WorkloadVerificationOptions = {}, + ): Promise { + if ( + typeof token !== "string" || + token.length === 0 || + token.length > this.#maxTokenBytes || + typeof expectedAudience !== "string" + ) { + return failure("WORKLOAD_TOKEN_MALFORMED"); + } + if (expectedAudience.length === 0 || !isSafeText(expectedAudience)) { + return failure("WORKLOAD_TOKEN_AUDIENCE_INVALID"); + } + try { + const header = decodeProtectedHeader(token); + if (header.alg !== ALGORITHM) return failure("WORKLOAD_TOKEN_UNSUPPORTED_ALGORITHM"); + if (typeof header.kid !== "string" || header.kid.length === 0) { + return failure("WORKLOAD_TOKEN_MALFORMED"); + } + } catch { + return failure("WORKLOAD_TOKEN_MALFORMED"); + } + + const abort = composeAbortSignal(this.#timeoutMs, options.signal); + try { + if (abort.signal.aborted) throw new VerificationFailure("WORKLOAD_ISSUER_UNAVAILABLE"); + const metadata = await this.#fetchJson( + new URL("/.well-known/openid-configuration", this.#issuer), + abort.signal, + "WORKLOAD_DISCOVERY_INVALID", + ); + const discovery = parseDiscovery(metadata, this.#issuer); + let jwks = await this.#fetchJwks(discovery.jwks_uri, abort.signal); + let payload: JWTPayload; + try { + payload = await this.#verifySignature(token, jwks); + } catch (error) { + if (!isSignatureFailure(error)) throw error; + jwks = await this.#fetchJwks(discovery.jwks_uri, abort.signal); + try { + payload = await this.#verifySignature(token, jwks); + } catch (retryError) { + if (isSignatureFailure(retryError)) { + throw new VerificationFailure("WORKLOAD_TOKEN_SIGNATURE_INVALID"); + } + throw retryError; + } + } + if (abort.signal.aborted) { + throw new VerificationFailure("WORKLOAD_ISSUER_UNAVAILABLE"); + } + const workload = normalizePayload( + payload, + this.#issuer.origin, + expectedAudience, + this.#now(), + this.#maxTokenAgeSeconds, + ); + if (abort.signal.aborted) { + throw new VerificationFailure("WORKLOAD_ISSUER_UNAVAILABLE"); + } + return { + success: true, + workload, + }; + } catch (error) { + if (error instanceof VerificationFailure) return failure(error.code); + if (abort.signal.aborted) return failure("WORKLOAD_ISSUER_UNAVAILABLE"); + return failure(classifyJwtError(error)); + } finally { + abort.cleanup(); + } + } + + async #verifySignature(token: string, jwks: JSONWebKeySet): Promise { + const result = await jwtVerify(token, createLocalJWKSet(jwks), { + algorithms: [ALGORITHM], + currentDate: new Date(this.#now() * 1000), + clockTolerance: 0, + }); + return result.payload; + } + + async #fetchJwks(url: string, signal: AbortSignal): Promise { + const value = await this.#fetchJson(new URL(url), signal, "WORKLOAD_JWKS_INVALID"); + return await parseJwks(value); + } + + async #fetchJson( + url: URL, + signal: AbortSignal, + invalidCode: "WORKLOAD_DISCOVERY_INVALID" | "WORKLOAD_JWKS_INVALID", + ): Promise { + let response: Response; + try { + response = await this.#fetch(url.href, { + method: "GET", + headers: { accept: "application/json" }, + redirect: "manual", + signal, + }); + } catch { + throw new VerificationFailure("WORKLOAD_ISSUER_UNAVAILABLE"); + } + try { + if (!response.ok) { + await response.body?.cancel(); + throw new VerificationFailure(invalidCode); + } + return await readBoundedJson(response, this.#maxResponseBytes); + } catch (error) { + if (signal.aborted) throw new VerificationFailure("WORKLOAD_ISSUER_UNAVAILABLE"); + if (error instanceof VerificationFailure) throw error; + throw new VerificationFailure(invalidCode); + } + } +} diff --git a/apps/release-service/src/workload/types.ts b/apps/release-service/src/workload/types.ts new file mode 100644 index 0000000000..464819e24a --- /dev/null +++ b/apps/release-service/src/workload/types.ts @@ -0,0 +1,57 @@ +export const WORKLOAD_VERIFICATION_ERROR_CODES = [ + "WORKLOAD_TOKEN_MALFORMED", + "WORKLOAD_TOKEN_UNSUPPORTED_ALGORITHM", + "WORKLOAD_TOKEN_SIGNATURE_INVALID", + "WORKLOAD_TOKEN_ISSUER_INVALID", + "WORKLOAD_TOKEN_AUDIENCE_INVALID", + "WORKLOAD_TOKEN_EXPIRED", + "WORKLOAD_TOKEN_NOT_ACTIVE", + "WORKLOAD_TOKEN_IAT_INVALID", + "WORKLOAD_CLAIMS_INVALID", + "WORKLOAD_DISCOVERY_INVALID", + "WORKLOAD_JWKS_INVALID", + "WORKLOAD_ISSUER_UNAVAILABLE", +] as const; + +export type WorkloadVerificationErrorCode = (typeof WORKLOAD_VERIFICATION_ERROR_CODES)[number]; + +export interface VerifiedWorkload { + issuer: string; + subject: string; + repository: string; + repositoryId: string; + repositoryOwnerId: string; + workflowRef: string; + jobWorkflowRef?: string; + ref: string; + sha: string; + runId: string; + runAttempt: string; + environment?: string; + expiresAt: number; +} + +export interface WorkloadVerificationError { + code: WorkloadVerificationErrorCode; + message: "Workload identity verification failed"; +} + +export type WorkloadVerificationResult = + | { success: true; workload: VerifiedWorkload } + | { success: false; error: WorkloadVerificationError }; + +export interface WorkloadVerificationOptions { + signal?: AbortSignal; +} + +export interface WorkloadIssuer { + verify( + token: string, + expectedAudience: string, + options?: WorkloadVerificationOptions, + ): Promise; +} + +export interface WorkloadMatcher { + matches(workload: VerifiedWorkload, policy: Policy): boolean; +} diff --git a/apps/release-service/test/api-foundation.test.ts b/apps/release-service/test/api-foundation.test.ts new file mode 100644 index 0000000000..52d55bb2a7 --- /dev/null +++ b/apps/release-service/test/api-foundation.test.ts @@ -0,0 +1,264 @@ +import { describe, expect, it } from "vitest"; + +import { requireAuthenticated, requireBearerToken } from "../src/api/auth.js"; +import { ApiError, serializeApiError } from "../src/api/errors.js"; +import { requireOwner, requireOwnerOr } from "../src/api/owner.js"; +import { decodeCursor, encodeCursor, parsePagination } from "../src/api/pagination.js"; +import { getRequestId } from "../src/api/request-id.js"; +import { generateApiSchema } from "../src/api/schema.js"; +import { + MAX_JSON_BODY_BYTES, + getCorsHeaders, + getCorsPreflightHeaders, + parseJsonMutation, +} from "../src/api/security.js"; +import { loadConfiguration, type ConfigurationBindings } from "../src/config.js"; +import { ROUTES } from "../src/routes.js"; + +const BINDINGS = { + PUBLIC_ORIGIN: "https://release.example.com", + ALLOWED_ORIGINS: '["https://release.example.com","https://console.example.com"]', + ALLOWED_PUBLISHERS: '{"mode":"allowlist","dids":["did:plc:publisher"]}', + DEPLOYMENT_POLICY: "hosted", + ENCRYPTION_KEYRING: + '{"current":1,"keys":[{"version":1,"key":"AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8"}]}', +} satisfies ConfigurationBindings; + +const config = loadConfiguration(BINDINGS); + +describe("request IDs and errors", () => { + it("accepts only bounded, header-safe inbound request IDs", () => { + expect( + getRequestId( + new Request("https://release.example.com", { headers: { "x-request-id": "run:abc-123" } }), + ), + ).toBe("run:abc-123"); + const generated = getRequestId( + new Request("https://release.example.com", { headers: { "x-request-id": "bad id value" } }), + ); + expect(generated).toMatch(/^[0-9a-f-]{36}$/); + }); + + it("serializes stable errors without leaking exception messages", () => { + expect(serializeApiError(new ApiError("FORBIDDEN", 403, "Not allowed"))).toEqual({ + code: "FORBIDDEN", + message: "Not allowed", + }); + expect(serializeApiError(new Error("database password leaked"))).toEqual({ + code: "INTERNAL_ERROR", + message: "Internal server error", + }); + }); +}); + +describe("configuration", () => { + it("loads explicit origins, deployment policy, and publisher policy", () => { + expect(config.publicOrigin).toBe("https://release.example.com"); + expect(config.allowedOrigins.has("https://console.example.com")).toBe(true); + expect(config.isPublisherAllowed("did:plc:publisher")).toBe(true); + expect(config.isPublisherAllowed("did:plc:other")).toBe(false); + expect(config.encryption.currentKeyVersion).toBe(1); + }); + + it.each([ + { ...BINDINGS, PUBLIC_ORIGIN: "" }, + { ...BINDINGS, PUBLIC_ORIGIN: "http://release.example.com" }, + { ...BINDINGS, ALLOWED_ORIGINS: "[]" }, + { ...BINDINGS, ALLOWED_ORIGINS: '["https://other.example.com"]' }, + { ...BINDINGS, ALLOWED_PUBLISHERS: '{"mode":"allowlist","dids":["not-a-did"]}' }, + { ...BINDINGS, DEPLOYMENT_POLICY: "preview" }, + { ...BINDINGS, ENCRYPTION_KEYRING: "not-json" }, + ])("fails closed for invalid deployment configuration", (bindings) => { + expect(() => loadConfiguration(bindings)).toThrowError("Invalid release-service configuration"); + }); +}); + +describe("mutation security", () => { + it("parses an allowed JSON mutation with the matching CSRF token", async () => { + const request = new Request("https://release.example.com/v1/example", { + method: "POST", + headers: { + origin: "https://console.example.com", + "content-type": "application/json; charset=utf-8", + "x-emdash-csrf": "session-secret", + }, + body: '{"ok":true}', + }); + await expect(parseJsonMutation(request, config, "session-secret")).resolves.toEqual({ + ok: true, + }); + expect(getCorsHeaders(request, config)).toEqual({ + "access-control-allow-credentials": "true", + "access-control-allow-origin": "https://console.example.com", + vary: "Origin", + }); + expect(getCorsPreflightHeaders(request, config, ["POST", "DELETE"])).toMatchObject({ + "access-control-allow-methods": "POST, DELETE", + "access-control-allow-headers": expect.stringContaining("X-EmDash-CSRF"), + }); + }); + + it.each([ + { + name: "GET mutation", + request: new Request("https://release.example.com/v1/example", { + headers: { + origin: BINDINGS.PUBLIC_ORIGIN, + "content-type": "application/json", + "x-emdash-csrf": "x", + }, + }), + code: "METHOD_NOT_ALLOWED", + }, + { + name: "missing origin", + request: new Request("https://release.example.com/v1/example", { + method: "POST", + headers: { "content-type": "application/json", "x-emdash-csrf": "x" }, + body: "{}", + }), + code: "ORIGIN_NOT_ALLOWED", + }, + { + name: "foreign origin", + request: new Request("https://release.example.com/v1/example", { + method: "POST", + headers: { + origin: "https://evil.example", + "content-type": "application/json", + "x-emdash-csrf": "x", + }, + body: "{}", + }), + code: "ORIGIN_NOT_ALLOWED", + }, + { + name: "wrong content type", + request: new Request("https://release.example.com/v1/example", { + method: "POST", + headers: { + origin: BINDINGS.PUBLIC_ORIGIN, + "content-type": "text/plain", + "x-emdash-csrf": "x", + }, + body: "{}", + }), + code: "UNSUPPORTED_MEDIA_TYPE", + }, + { + name: "wrong CSRF token", + request: new Request("https://release.example.com/v1/example", { + method: "POST", + headers: { + origin: BINDINGS.PUBLIC_ORIGIN, + "content-type": "application/json", + "x-emdash-csrf": "wrong", + }, + body: "{}", + }), + code: "CSRF_INVALID", + }, + ])("rejects $name", async ({ request, code }) => { + await expect(parseJsonMutation(request, config, "x")).rejects.toMatchObject({ code }); + }); + + it("rejects bodies whose declared or streamed size exceeds the bound", async () => { + const headers = { + origin: BINDINGS.PUBLIC_ORIGIN, + "content-type": "application/json", + "x-emdash-csrf": "x", + }; + const declared = new Request("https://release.example.com/v1/example", { + method: "POST", + headers: { ...headers, "content-length": String(MAX_JSON_BODY_BYTES + 1) }, + body: "{}", + }); + await expect(parseJsonMutation(declared, config, "x")).rejects.toMatchObject({ + code: "PAYLOAD_TOO_LARGE", + }); + + const streamed = new Request("https://release.example.com/v1/example", { + method: "POST", + headers, + body: JSON.stringify({ value: "x".repeat(MAX_JSON_BODY_BYTES) }), + }); + await expect(parseJsonMutation(streamed, config, "x")).rejects.toMatchObject({ + code: "PAYLOAD_TOO_LARGE", + }); + }); +}); + +describe("authentication and ownership", () => { + it("parses strict bearer authentication", () => { + const request = new Request("https://release.example.com", { + headers: { authorization: "Bearer token-value" }, + }); + expect(requireBearerToken(request)).toBe("token-value"); + expect( + requireBearerToken( + new Request("https://release.example.com", { + headers: { authorization: "bearer another-token" }, + }), + ), + ).toBe("another-token"); + expect(() => requireBearerToken(new Request("https://release.example.com"))).toThrowError( + ApiError, + ); + }); + + it("requires identity ownership or an explicit authorization predicate", () => { + const actor = requireAuthenticated({ subjectDid: "did:plc:publisher" }); + expect(requireOwner(actor, "did:plc:publisher")).toBe(actor); + expect(() => requireOwner(actor, "did:plc:other")).toThrowError(ApiError); + expect(requireOwnerOr(actor, "did:plc:other", () => true)).toBe(actor); + }); +}); + +describe("pagination", () => { + it("clamps limits and round-trips opaque versioned cursors", () => { + expect( + parsePagination(new URL("https://release.example.com?limit=500").searchParams).limit, + ).toBe(100); + expect(parsePagination(new URL("https://release.example.com?limit=2").searchParams).limit).toBe( + 2, + ); + const cursor = encodeCursor("2026-07-11T12:00:00.000Z", "01J123"); + expect(cursor).not.toContain("2026-07-11"); + expect(decodeCursor(cursor)).toEqual({ + version: 1, + orderValue: "2026-07-11T12:00:00.000Z", + id: "01J123", + }); + }); + + it("rejects malformed and unsupported cursors", () => { + expect(() => decodeCursor("not-base64-json")).toThrowError(ApiError); + const unsupported = btoa(JSON.stringify([2, "order", "id"])).replaceAll("=", ""); + expect(() => decodeCursor(unsupported)).toThrowError(ApiError); + expect(() => encodeCursor("😀".repeat(512), "id")).toThrowError(ApiError); + }); +}); + +describe("API schema", () => { + it("generates OpenAPI 3.1 from only the reachable route registry", () => { + const schema = generateApiSchema(); + expect(schema.openapi).toBe("3.1.0"); + expect(Object.keys(schema.paths)).toEqual(["/health"]); + expect(schema.paths["/health"]).toHaveProperty("get"); + expect(JSON.stringify(schema)).toContain("requestId"); + expect(JSON.stringify(schema)).toContain("INTERNAL_ERROR"); + }); + + it("preserves multiple methods registered for the same path", () => { + const schema = generateApiSchema([ + ...ROUTES, + { + ...ROUTES[0], + method: "POST", + operationId: "postHealthTest", + }, + ]); + expect(schema.paths["/health"]).toHaveProperty("get"); + expect(schema.paths["/health"]).toHaveProperty("post"); + }); +}); diff --git a/apps/release-service/test/encryption.test.ts b/apps/release-service/test/encryption.test.ts new file mode 100644 index 0000000000..8b766a626f --- /dev/null +++ b/apps/release-service/test/encryption.test.ts @@ -0,0 +1,318 @@ +import { describe, expect, it } from "vitest"; + +import { + EncryptionError, + createEnvelopeEncryption, + type EncryptionContext, +} from "../src/crypto/encryption.js"; + +const KEY_1 = "AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8"; +const KEY_2 = "ICEiIyQlJicoKSorLC0uLzAxMjM0NTY3ODk6Ozw9Pj8"; +const KEYRING = JSON.stringify({ + current: 2, + keys: [ + { version: 1, key: KEY_1 }, + { version: 2, key: KEY_2 }, + ], +}); +const CONTEXT = { + purpose: "oauth-session", + table: "publisher_delegations", + primaryKey: "01JABCDEFGHJKMNPQRSTVWXYZ", + ownerDid: "did:plc:publisher", +} satisfies EncryptionContext; +const encoder = new TextEncoder(); +const decoder = new TextDecoder(); + +function expectEncryptionError(code: string) { + return (error: unknown) => { + expect(error).toBeInstanceOf(EncryptionError); + expect(error).toMatchObject({ code }); + return true; + }; +} + +function mutateBase64Url(value: string): string { + return `${value.startsWith("A") ? "B" : "A"}${value.slice(1)}`; +} + +describe("envelope encryption", () => { + it.each(["", "plain text", "こんにちは世界", JSON.stringify({ token: "secret" })])( + "round trips UTF-8 plaintext", + async (plaintext) => { + const encryption = createEnvelopeEncryption(KEYRING); + const encrypted = await encryption.encrypt(encoder.encode(plaintext), CONTEXT); + + expect(encrypted.keyVersion).toBe(2); + expect(encrypted.envelope).not.toContain(plaintext || "secret"); + expect(decoder.decode(await encryption.decrypt(encrypted.envelope, CONTEXT))).toBe(plaintext); + }, + ); + + it("decrypts the persisted version 1 compatibility vector", async () => { + const encryption = createEnvelopeEncryption(KEYRING); + const envelope = + '{"v":1,"k":1,"n":"AAECAwQFBgcICQoL","c":"HdFw4nM5jJCXNGp4JXOQJEGstFPca4NiixkzLzDR2RA"}'; + + expect(decoder.decode(await encryption.decrypt(envelope, CONTEXT))).toBe("persisted secret"); + }); + + it("uses a fresh nonce for every encryption", async () => { + const encryption = createEnvelopeEncryption(KEYRING); + const plaintext = encoder.encode("same secret"); + const first = await encryption.encrypt(plaintext, CONTEXT); + const second = await encryption.encrypt(plaintext, CONTEXT); + + expect(first.envelope).not.toBe(second.envelope); + }); + + it("round trips arbitrary binary data", async () => { + const encryption = createEnvelopeEncryption(KEYRING); + const plaintext = Uint8Array.from([0, 255, 128, 1, 127]); + const encrypted = await encryption.encrypt(plaintext, CONTEXT); + + expect(await encryption.decrypt(encrypted.envelope, CONTEXT)).toEqual(plaintext); + }); + + it("copies mutable plaintext before asynchronous key derivation", async () => { + const encryption = createEnvelopeEncryption(KEYRING); + const plaintext = encoder.encode("original"); + const pending = encryption.encrypt(plaintext, CONTEXT); + plaintext.fill(0); + const encrypted = await pending; + + expect(decoder.decode(await encryption.decrypt(encrypted.envelope, CONTEXT))).toBe("original"); + }); + + it("supports pre-identity transactions with an explicit unowned context", async () => { + const encryption = createEnvelopeEncryption(KEYRING); + const context = { + purpose: "oauth-transaction", + table: "oauth_transactions", + primaryKey: "transaction-id", + ownerDid: null, + } as const satisfies EncryptionContext; + const encrypted = await encryption.encrypt(encoder.encode("oauth state"), context); + + expect(decoder.decode(await encryption.decrypt(encrypted.envelope, context))).toBe( + "oauth state", + ); + }); + + it("binds known-identity OAuth transactions to their expected DID", async () => { + const encryption = createEnvelopeEncryption(KEYRING); + const context = { + purpose: "oauth-transaction", + table: "oauth_transactions", + primaryKey: "transaction-id", + ownerDid: "did:plc:expected", + } as const satisfies EncryptionContext; + const encrypted = await encryption.encrypt(encoder.encode("oauth state"), context); + + await expect( + encryption.decrypt(encrypted.envelope, { ...context, ownerDid: "did:plc:other" }), + ).rejects.toSatisfy(expectEncryptionError("DECRYPTION_FAILED")); + }); + + it("rejects owner semantics that do not match the purpose", async () => { + const encryption = createEnvelopeEncryption(KEYRING); + const ownedContext: EncryptionContext = { ...CONTEXT }; + Object.assign(ownedContext, { ownerDid: null }); + const unownedContext: EncryptionContext = { + purpose: "confidential-client-private-key", + table: "service_keys", + primaryKey: "current-client-key", + ownerDid: null, + }; + Object.assign(unownedContext, { ownerDid: "did:plc:publisher" }); + + for (const context of [ownedContext, unownedContext]) { + await expect(encryption.encrypt(encoder.encode("secret"), context)).rejects.toSatisfy( + expectEncryptionError("ENCRYPTION_CONTEXT_INVALID"), + ); + } + }); + + it("snapshots mutable context before asynchronous key derivation", async () => { + const encryption = createEnvelopeEncryption(KEYRING); + const context: EncryptionContext = { ...CONTEXT }; + const pending = encryption.encrypt(encoder.encode("secret"), context); + context.primaryKey = "mutated-row"; + context.ownerDid = "did:plc:mutated"; + const encrypted = await pending; + + expect(decoder.decode(await encryption.decrypt(encrypted.envelope, CONTEXT))).toBe("secret"); + }); + + it.each([ + ["purpose", { ...CONTEXT, purpose: "dpop-private-key" }], + ["table", { ...CONTEXT, table: "oauth_transactions" }], + ["primary key", { ...CONTEXT, primaryKey: "other-row" }], + ["owner DID", { ...CONTEXT, ownerDid: "did:plc:other" }], + ] satisfies ReadonlyArray)( + "binds ciphertext to its %s", + async (_name, changedContext) => { + const encryption = createEnvelopeEncryption(KEYRING); + const encrypted = await encryption.encrypt(encoder.encode("secret"), CONTEXT); + + await expect(encryption.decrypt(encrypted.envelope, changedContext)).rejects.toSatisfy( + expectEncryptionError("DECRYPTION_FAILED"), + ); + }, + ); + + it.each([ + ["malformed JSON", "not-json", "ENCRYPTED_VALUE_INVALID"], + [ + "additional property", + '{"v":1,"k":2,"n":"AAAAAAAAAAAAAAAA","c":"AAAAAAAAAAAAAAAAAAAAAA","x":1}', + "ENCRYPTED_VALUE_INVALID", + ], + [ + "unsupported envelope version", + '{"v":2,"k":2,"n":"AAAAAAAAAAAAAAAA","c":"AAAAAAAAAAAAAAAAAAAAAA"}', + "ENCRYPTED_VALUE_UNSUPPORTED", + ], + [ + "padded nonce", + '{"v":1,"k":2,"n":"AAAAAAAAAAAAAAAA=","c":"AAAAAAAAAAAAAAAAAAAAAA"}', + "ENCRYPTED_VALUE_INVALID", + ], + [ + "short nonce", + '{"v":1,"k":2,"n":"AAAA","c":"AAAAAAAAAAAAAAAAAAAAAA"}', + "ENCRYPTED_VALUE_INVALID", + ], + [ + "short ciphertext", + '{"v":1,"k":2,"n":"AAAAAAAAAAAAAAAA","c":"AAAA"}', + "ENCRYPTED_VALUE_INVALID", + ], + [ + "non-canonical serialization", + '{ "v":1,"k":2,"n":"AAAAAAAAAAAAAAAA","c":"AAAAAAAAAAAAAAAAAAAAAA"}', + "ENCRYPTED_VALUE_INVALID", + ], + [ + "duplicate property", + '{"v":1,"v":1,"k":2,"n":"AAAAAAAAAAAAAAAA","c":"AAAAAAAAAAAAAAAAAAAAAA"}', + "ENCRYPTED_VALUE_INVALID", + ], + ])("rejects a %s", async (_name, envelope, code) => { + const encryption = createEnvelopeEncryption(KEYRING); + + await expect(encryption.decrypt(envelope, CONTEXT)).rejects.toSatisfy( + expectEncryptionError(code), + ); + }); + + it("rejects ciphertext modification without leaking the crypto exception", async () => { + const encryption = createEnvelopeEncryption(KEYRING); + const encrypted = await encryption.encrypt(encoder.encode("secret marker"), CONTEXT); + const parsed = JSON.parse(encrypted.envelope) as { c: string }; + parsed.c = mutateBase64Url(parsed.c); + + await expect(encryption.decrypt(JSON.stringify(parsed), CONTEXT)).rejects.toSatisfy( + expectEncryptionError("DECRYPTION_FAILED"), + ); + }); + + it("rejects nonce modification", async () => { + const encryption = createEnvelopeEncryption(KEYRING); + const encrypted = await encryption.encrypt(encoder.encode("secret"), CONTEXT); + const parsed = JSON.parse(encrypted.envelope) as { n: string }; + parsed.n = mutateBase64Url(parsed.n); + + await expect(encryption.decrypt(JSON.stringify(parsed), CONTEXT)).rejects.toSatisfy( + expectEncryptionError("DECRYPTION_FAILED"), + ); + }); + + it("reads old keys, writes the current key, and rotates idempotently", async () => { + const oldEncryption = createEnvelopeEncryption( + JSON.stringify({ current: 1, keys: [{ version: 1, key: KEY_1 }] }), + ); + const oldValue = await oldEncryption.encrypt(encoder.encode("rotate me"), CONTEXT); + const encryption = createEnvelopeEncryption(KEYRING); + + expect(encryption.needsRotation(oldValue.envelope)).toBe(true); + expect(decoder.decode(await encryption.decrypt(oldValue.envelope, CONTEXT))).toBe("rotate me"); + + const rotated = await encryption.rotate(oldValue.envelope, CONTEXT); + expect(rotated.keyVersion).toBe(2); + expect(rotated.envelope).not.toBe(oldValue.envelope); + expect(decoder.decode(await encryption.decrypt(rotated.envelope, CONTEXT))).toBe("rotate me"); + expect(encryption.needsRotation(rotated.envelope)).toBe(false); + expect(await encryption.rotate(rotated.envelope, CONTEXT)).toEqual(rotated); + }); + + it("authenticates a current-version envelope before treating rotation as complete", async () => { + const encryption = createEnvelopeEncryption(KEYRING); + const encrypted = await encryption.encrypt(encoder.encode("secret"), CONTEXT); + const parsed = JSON.parse(encrypted.envelope) as { c: string }; + parsed.c = mutateBase64Url(parsed.c); + + await expect(encryption.rotate(JSON.stringify(parsed), CONTEXT)).rejects.toSatisfy( + expectEncryptionError("DECRYPTION_FAILED"), + ); + }); + + it("rejects oversized plaintext before encryption", async () => { + const encryption = createEnvelopeEncryption(KEYRING); + + await expect(encryption.encrypt(new Uint8Array(1024 * 1024 + 1), CONTEXT)).rejects.toSatisfy( + expectEncryptionError("ENCRYPTION_FAILED"), + ); + }); + + it("fails closed when an old key is unavailable", async () => { + const oldEncryption = createEnvelopeEncryption( + JSON.stringify({ current: 1, keys: [{ version: 1, key: KEY_1 }] }), + ); + const oldValue = await oldEncryption.encrypt(encoder.encode("old secret"), CONTEXT); + const currentEncryption = createEnvelopeEncryption( + JSON.stringify({ current: 2, keys: [{ version: 2, key: KEY_2 }] }), + ); + + await expect(currentEncryption.decrypt(oldValue.envelope, CONTEXT)).rejects.toSatisfy( + expectEncryptionError("ENCRYPTION_KEY_UNAVAILABLE"), + ); + }); +}); + +describe("encryption configuration", () => { + it.each([ + ["malformed JSON", "{"], + ["additional property", JSON.stringify({ current: 1, keys: [], extra: true })], + ["no keys", JSON.stringify({ current: 1, keys: [] })], + ["missing current key", JSON.stringify({ current: 2, keys: [{ version: 1, key: KEY_1 }] })], + [ + "duplicate versions", + JSON.stringify({ + current: 1, + keys: [ + { version: 1, key: KEY_1 }, + { version: 1, key: KEY_2 }, + ], + }), + ], + ["short key", JSON.stringify({ current: 1, keys: [{ version: 1, key: "AAAA" }] })], + ["padded key", JSON.stringify({ current: 1, keys: [{ version: 1, key: `${KEY_1}=` }] })], + ])("rejects %s", (_name, keyring) => { + expect(() => createEnvelopeEncryption(keyring)).toThrowError( + expect.objectContaining({ code: "ENCRYPTION_CONFIGURATION_INVALID" }), + ); + }); + + it("does not expose key material in configuration errors", () => { + const marker = "private-key-marker"; + + try { + createEnvelopeEncryption(JSON.stringify({ current: 1, keys: [{ version: 1, key: marker }] })); + expect.unreachable(); + } catch (error) { + expect(String(error)).not.toContain(marker); + expect(error).not.toHaveProperty("cause"); + } + }); +}); diff --git a/apps/release-service/test/fixtures/release-verifier.js b/apps/release-service/test/fixtures/release-verifier.js new file mode 100644 index 0000000000..cac8e89b57 --- /dev/null +++ b/apps/release-service/test/fixtures/release-verifier.js @@ -0,0 +1,12 @@ +import { WorkerEntrypoint } from "cloudflare:workers"; + +export default class TestReleaseVerifier extends WorkerEntrypoint { + fetchArtifact(url) { + if (url.endsWith("/unavailable")) throw new Error("internal service address"); + return { success: true, value: new Uint8Array([7]) }; + } + + fetchProvenance() { + return { success: true, value: "not bytes" }; + } +} diff --git a/apps/release-service/test/github-workload.test.ts b/apps/release-service/test/github-workload.test.ts new file mode 100644 index 0000000000..d8fb7fdef6 --- /dev/null +++ b/apps/release-service/test/github-workload.test.ts @@ -0,0 +1,634 @@ +import { base64url, exportJWK, generateKeyPair, SignJWT } from "jose"; +import { beforeAll, describe, expect, it, vi } from "vitest"; + +import { + GitHubActionsIssuer, + type GitHubActionsIssuerOptions, +} from "../src/workload/github-actions.js"; +import type { + VerifiedWorkload, + WorkloadMatcher, + WorkloadVerificationErrorCode, +} from "../src/workload/types.js"; + +const ISSUER = "https://token.actions.example.invalid"; +const AUDIENCE = "https://release.example.invalid"; +const NOW = 2_000_000_000; +const GENERIC_MESSAGE = "Workload identity verification failed"; + +let signingKey: CryptoKey; +let rotatedSigningKey: CryptoKey; +let jwk: Record; +let rotatedJwk: Record; + +const BASE_CLAIMS = { + iss: ISSUER, + aud: AUDIENCE, + sub: "repo:emdash-cms/emdash:ref:refs/heads/main", + exp: NOW + 300, + nbf: NOW - 10, + iat: NOW - 10, + repository: "emdash-cms/emdash", + repository_id: "123456789", + repository_owner_id: "987654321", + workflow_ref: "emdash-cms/emdash/.github/workflows/release.yml@refs/heads/main", + ref: "refs/heads/main", + sha: "0123456789abcdef0123456789abcdef01234567", + run_id: "1122334455", + run_attempt: "2", +} as const; + +beforeAll(async () => { + const current = await generateKeyPair("RS256"); + const rotated = await generateKeyPair("RS256"); + signingKey = current.privateKey; + rotatedSigningKey = rotated.privateKey; + jwk = { ...(await exportJWK(current.publicKey)), kid: "current", use: "sig", alg: "RS256" }; + rotatedJwk = { + ...(await exportJWK(rotated.publicKey)), + kid: "rotated", + use: "sig", + alg: "RS256", + }; +}); + +function jsonResponse(value: unknown, init?: ResponseInit): Response { + const headers = new Headers(init?.headers); + headers.set("content-type", "application/json"); + return new Response(JSON.stringify(value), { + ...init, + headers, + }); +} + +function discovery(overrides: Record = {}): Record { + return { + issuer: ISSUER, + jwks_uri: `${ISSUER}/.well-known/jwks`, + id_token_signing_alg_values_supported: ["RS256"], + ...overrides, + }; +} + +function createFetch( + options: { + metadata?: unknown; + jwks?: unknown; + metadataResponse?: Response; + jwksResponse?: Response; + } = {}, +): { fetch: typeof fetch; calls: Request[] } { + const calls: Request[] = []; + let jwksCall = 0; + const transport = vi.fn(async (input: RequestInfo | URL, init?: RequestInit) => { + const request = new Request(input, init); + calls.push(request); + if (request.url.endsWith("/.well-known/openid-configuration")) { + return options.metadataResponse ?? jsonResponse(options.metadata ?? discovery()); + } + const configured = options.jwks ?? { keys: [jwk] }; + const value = Array.isArray(configured) + ? configured[Math.min(jwksCall++, configured.length - 1)] + : configured; + return options.jwksResponse ?? jsonResponse(value); + }); + return { fetch: transport as typeof fetch, calls }; +} + +function createIssuer( + transport = createFetch().fetch, + overrides: Partial = {}, +): GitHubActionsIssuer { + return new GitHubActionsIssuer({ + issuer: ISSUER, + fetch: transport, + now: () => NOW, + timeoutMs: 100, + ...overrides, + }); +} + +async function sign( + claims: Record = {}, + options: { key?: CryptoKey; kid?: string; alg?: string } = {}, +): Promise { + return new SignJWT({ ...BASE_CLAIMS, ...claims }) + .setProtectedHeader({ alg: options.alg ?? "RS256", kid: options.kid ?? "current", typ: "JWT" }) + .sign(options.key ?? signingKey); +} + +async function expectFailure( + promise: Promise>>, + code: WorkloadVerificationErrorCode, +): Promise { + const result = await promise; + expect(result).toEqual({ success: false, error: { code, message: GENERIC_MESSAGE } }); + expect(JSON.stringify(result)).not.toContain(BASE_CLAIMS.repository); +} + +describe("issuer-neutral workload contract", () => { + it("supports a non-persistent matcher without issuer-specific policy types", () => { + const matcher: WorkloadMatcher<{ repositoryId: string }> = { + matches(workload, policy) { + return workload.repositoryId === policy.repositoryId; + }, + }; + const workload = { + issuer: ISSUER, + subject: BASE_CLAIMS.sub, + repository: BASE_CLAIMS.repository, + repositoryId: BASE_CLAIMS.repository_id, + repositoryOwnerId: BASE_CLAIMS.repository_owner_id, + workflowRef: BASE_CLAIMS.workflow_ref, + ref: BASE_CLAIMS.ref, + sha: BASE_CLAIMS.sha, + runId: BASE_CLAIMS.run_id, + runAttempt: BASE_CLAIMS.run_attempt, + expiresAt: BASE_CLAIMS.exp, + } satisfies VerifiedWorkload; + expect(matcher.matches(workload, { repositoryId: "123456789" })).toBe(true); + }); +}); + +describe("GitHub Actions workload verification", () => { + it("returns only normalized verified claims", async () => { + const token = await sign({ + job_workflow_ref: + "emdash-cms/automation/.github/workflows/publish.yml@0123456789abcdef0123456789abcdef01234567", + environment: "production", + actor: "untrusted-display-name", + jti: "never-persist-this", + }); + const result = await createIssuer().verify(token, AUDIENCE); + expect(result).toEqual({ + success: true, + workload: { + issuer: ISSUER, + subject: BASE_CLAIMS.sub, + repository: BASE_CLAIMS.repository, + repositoryId: BASE_CLAIMS.repository_id, + repositoryOwnerId: BASE_CLAIMS.repository_owner_id, + workflowRef: BASE_CLAIMS.workflow_ref, + jobWorkflowRef: + "emdash-cms/automation/.github/workflows/publish.yml@0123456789abcdef0123456789abcdef01234567", + ref: BASE_CLAIMS.ref, + sha: BASE_CLAIMS.sha, + runId: BASE_CLAIMS.run_id, + runAttempt: BASE_CLAIMS.run_attempt, + environment: "production", + expiresAt: BASE_CLAIMS.exp, + }, + }); + expect(JSON.stringify(result)).not.toContain(token); + expect(JSON.stringify(result)).not.toContain("untrusted-display-name"); + }); + + it("accepts absent optional claims", async () => { + const result = await createIssuer().verify(await sign(), AUDIENCE); + expect(result.success).toBe(true); + if (result.success) { + expect(result.workload).not.toHaveProperty("jobWorkflowRef"); + expect(result.workload).not.toHaveProperty("environment"); + } + }); + + it("accepts canonical Git refs with valid punctuation", async () => { + const ref = "refs/heads/release+candidate@v2"; + const result = await createIssuer().verify( + await sign({ + ref, + workflow_ref: `emdash-cms/emdash/.github/workflows/release.yml@${ref}`, + }), + AUDIENCE, + ); + expect(result.success).toBe(true); + }); + + it.each(["not-a-jwt", "a.b", "a.b.c.d"])("rejects malformed token %s", async (token) => { + await expectFailure(createIssuer().verify(token, AUDIENCE), "WORKLOAD_TOKEN_MALFORMED"); + }); + + it("rejects an oversized token before making network requests", async () => { + const { fetch, calls } = createFetch(); + await expectFailure( + createIssuer(fetch, { maxTokenBytes: 100 }).verify("a".repeat(101), AUDIENCE), + "WORKLOAD_TOKEN_MALFORMED", + ); + expect(calls).toHaveLength(0); + }); + + it("rejects an empty expected audience before making network requests", async () => { + const { fetch, calls } = createFetch(); + await expectFailure( + createIssuer(fetch).verify(await sign(), ""), + "WORKLOAD_TOKEN_AUDIENCE_INVALID", + ); + expect(calls).toHaveLength(0); + }); + + it.each([ + ["none", "eyJhbGciOiJub25lIiwia2lkIjoiY3VycmVudCJ9.e30."], + ["HS256", null], + ])("rejects unsupported %s tokens", async (algorithm, staticToken) => { + const token = + staticToken ?? + (await new SignJWT(BASE_CLAIMS) + .setProtectedHeader({ alg: "HS256", kid: "current" }) + .sign(new TextEncoder().encode("not-a-public-key"))); + await expectFailure( + createIssuer().verify(token, AUDIENCE), + "WORKLOAD_TOKEN_UNSUPPORTED_ALGORITHM", + ); + }); + + it("rejects malformed protected headers", async () => { + const token = await new SignJWT(BASE_CLAIMS) + .setProtectedHeader({ alg: "RS256" }) + .sign(signingKey); + await expectFailure(createIssuer().verify(token, AUDIENCE), "WORKLOAD_TOKEN_MALFORMED"); + }); + + it("rejects a bad signature", async () => { + const token = await sign({}, { key: rotatedSigningKey }); + await expectFailure(createIssuer().verify(token, AUDIENCE), "WORKLOAD_TOKEN_SIGNATURE_INVALID"); + }); + + it("rejects the wrong issuer", async () => { + await expectFailure( + createIssuer().verify(await sign({ iss: `${ISSUER}/lookalike` }), AUDIENCE), + "WORKLOAD_TOKEN_ISSUER_INVALID", + ); + }); + + it.each([ + "https://other.example.invalid", + [AUDIENCE], + [AUDIENCE, "https://other.example.invalid"], + ])("rejects a wrong or ambiguous audience", async (aud) => { + await expectFailure( + createIssuer().verify(await sign({ aud }), AUDIENCE), + "WORKLOAD_TOKEN_AUDIENCE_INVALID", + ); + }); + + it.each([ + ["expired", { exp: NOW - 1 }, "WORKLOAD_TOKEN_EXPIRED"], + ["early", { nbf: NOW + 1 }, "WORKLOAD_TOKEN_NOT_ACTIVE"], + ["stale", { iat: NOW - 601 }, "WORKLOAD_TOKEN_IAT_INVALID"], + ["future", { iat: NOW + 1 }, "WORKLOAD_TOKEN_IAT_INVALID"], + ["missing exp", { exp: undefined }, "WORKLOAD_CLAIMS_INVALID"], + ["fractional exp", { exp: NOW + 0.5 }, "WORKLOAD_CLAIMS_INVALID"], + ] as const)("rejects %s token times", async (_name, claims, code) => { + await expectFailure(createIssuer().verify(await sign(claims), AUDIENCE), code); + }); + + it.each(["repository_id", "repository_owner_id", "run_id", "run_attempt"])( + "requires canonical decimal string %s", + async (claim) => { + for (const value of [123, "01", "+1", "1.0", "0", "9007199254740993.0"]) { + await expectFailure( + createIssuer().verify(await sign({ [claim]: value }), AUDIENCE), + "WORKLOAD_CLAIMS_INVALID", + ); + } + }, + ); + + it.each([ + ["repository", "owner only"], + ["repository", "owner/repo/extra"], + ["ref", "main"], + ["ref", "refs/heads/../main"], + ["sha", "ABCDEF6789abcdef0123456789abcdef01234567"], + ["sha", "deadbeef"], + ["workflow_ref", "emdash-cms/emdash/.github/workflows/release.yml"], + ["workflow_ref", "other/repo/.github/workflows/release.yml@refs/heads/main"], + ["workflow_ref", "emdash-cms/emdash/.github/workflows/../release.yml@refs/heads/main"], + ["job_workflow_ref", "not-a-workflow"], + ["environment", ""], + ["environment", "production\nforged"], + ])("rejects malformed %s", async (claim, value) => { + await expectFailure( + createIssuer().verify(await sign({ [claim]: value }), AUDIENCE), + "WORKLOAD_CLAIMS_INVALID", + ); + }); + + it("rejects missing required claims", async () => { + for (const claim of [ + "sub", + "repository", + "repository_id", + "repository_owner_id", + "workflow_ref", + "ref", + "sha", + "run_id", + "run_attempt", + ]) { + await expectFailure( + createIssuer().verify(await sign({ [claim]: undefined }), AUDIENCE), + "WORKLOAD_CLAIMS_INVALID", + ); + } + }); +}); + +describe("GitHub issuer metadata and transport", () => { + it("treats the configured issuer as the discovery trust anchor", async () => { + const { fetch, calls } = createFetch(); + const result = await createIssuer(fetch).verify(await sign(), AUDIENCE); + expect(calls[0]?.url).toBe(`${ISSUER}/.well-known/openid-configuration`); + expect(calls.every((request) => request.redirect === "manual")).toBe(true); + expect(result.success).toBe(true); + }); + + it.each([ + ["issuer mismatch", discovery({ issuer: `${ISSUER}/other` })], + ["missing signing algorithm", discovery({ id_token_signing_alg_values_supported: ["ES256"] })], + ["HTTP JWKS", discovery({ jwks_uri: "http://token.actions.example.invalid/keys" })], + ["cross-origin JWKS", discovery({ jwks_uri: "https://attacker.invalid/keys" })], + [ + "credentialed JWKS", + discovery({ jwks_uri: "https://user@token.actions.example.invalid/keys" }), + ], + ])("rejects unsafe discovery: %s", async (_name, metadata) => { + await expectFailure( + createIssuer(createFetch({ metadata }).fetch).verify(await sign(), AUDIENCE), + "WORKLOAD_DISCOVERY_INVALID", + ); + }); + + it.each([ + ["malformed metadata", jsonResponse("not-an-object")], + ["invalid metadata JSON", new Response("{")], + ["redirect", new Response(null, { status: 302, headers: { location: `${ISSUER}/other` } })], + ["server error", new Response(null, { status: 503 })], + ])("rejects %s without following it", async (_name, metadataResponse) => { + await expectFailure( + createIssuer(createFetch({ metadataResponse }).fetch).verify(await sign(), AUDIENCE), + "WORKLOAD_DISCOVERY_INVALID", + ); + }); + + it("rejects a JWKS redirect without following it", async () => { + const redirect = new Response(null, { + status: 302, + headers: { location: "https://attacker.invalid/keys" }, + }); + const { fetch, calls } = createFetch({ jwksResponse: redirect }); + await expectFailure( + createIssuer(fetch).verify(await sign(), AUDIENCE), + "WORKLOAD_JWKS_INVALID", + ); + expect(calls).toHaveLength(2); + }); + + it.each([ + ["missing keys", {}], + ["non-array keys", { keys: "invalid" }], + ["symmetric key", { keys: [{ kty: "oct", k: "c2VjcmV0", kid: "current" }] }], + ])("rejects malformed JWKS: %s", async (_name, keys) => { + await expectFailure( + createIssuer(createFetch({ jwks: keys }).fetch).verify(await sign(), AUDIENCE), + "WORKLOAD_JWKS_INVALID", + ); + }); + + it.each([ + ["non-verification", ["encrypt"]], + ["mixed", ["verify", "sign"]], + ["duplicate", ["verify", "verify"]], + ["malformed", ["verify", 1]], + ])("rejects %s key operations", async (_name, keyOperations) => { + const keys = { keys: [{ ...jwk, key_ops: keyOperations }] }; + await expectFailure( + createIssuer(createFetch({ jwks: keys }).fetch).verify(await sign(), AUDIENCE), + "WORKLOAD_JWKS_INVALID", + ); + }); + + it("accepts an explicit verify-only key operation", async () => { + const result = await createIssuer( + createFetch({ jwks: { keys: [{ ...jwk, key_ops: ["verify"] }] } }).fetch, + ).verify(await sign(), AUDIENCE); + expect(result.success).toBe(true); + }); + + it.each([ + ["invalid modulus encoding", { n: "not+base64url" }], + ["invalid exponent encoding", { e: "not+base64url" }], + ["unusable exponent", { e: "AQ" }], + ["unacceptable key material", { n: "AQ" }], + ])("classifies %s as invalid JWKS", async (_name, overrides) => { + const invalidKey = { ...jwk, ...overrides }; + await expectFailure( + createIssuer(createFetch({ jwks: { keys: [invalidKey] } }).fetch).verify( + await sign(), + AUDIENCE, + ), + "WORKLOAD_JWKS_INVALID", + ); + }); + + it("rejects an RSA modulus shorter than 2048 significant bits", async () => { + const modulus = base64url.decode(jwk["n"] as string); + modulus[0] = 1; + const invalidKey = { ...jwk, n: base64url.encode(modulus) }; + await expectFailure( + createIssuer(createFetch({ jwks: { keys: [invalidKey] } }).fetch).verify( + await sign(), + AUDIENCE, + ), + "WORKLOAD_JWKS_INVALID", + ); + }); + + it("rejects duplicate JWKS key IDs", async () => { + const keys = { keys: [jwk, { ...rotatedJwk, kid: "current" }] }; + await expectFailure( + createIssuer(createFetch({ jwks: keys }).fetch).verify(await sign(), AUDIENCE), + "WORKLOAD_JWKS_INVALID", + ); + }); + + it.each(["metadata", "JWKS"])("rejects oversized %s by declared length", async (resource) => { + let cancelled = false; + const body = new ReadableStream({ + start(controller) { + controller.enqueue(new TextEncoder().encode("{}")); + }, + cancel() { + cancelled = true; + }, + }); + const oversized = new Response(body, { + headers: { "content-length": "100001", "content-type": "application/json" }, + }); + const transport = createFetch( + resource === "metadata" ? { metadataResponse: oversized } : { jwksResponse: oversized }, + ).fetch; + await expectFailure( + createIssuer(transport, { maxResponseBytes: 100_000 }).verify(await sign(), AUDIENCE), + resource === "metadata" ? "WORKLOAD_DISCOVERY_INVALID" : "WORKLOAD_JWKS_INVALID", + ); + expect(cancelled).toBe(true); + }); + + it("rejects oversized streamed metadata and cancels the body", async () => { + let cancelled = false; + const body = new ReadableStream({ + start(controller) { + controller.enqueue(new Uint8Array(60)); + controller.enqueue(new Uint8Array(60)); + }, + cancel() { + cancelled = true; + }, + }); + await expectFailure( + createIssuer(createFetch({ metadataResponse: new Response(body) }).fetch, { + maxResponseBytes: 100, + }).verify(await sign(), AUDIENCE), + "WORKLOAD_DISCOVERY_INVALID", + ); + expect(cancelled).toBe(true); + }); + + it("rejects oversized streamed JWKS and cancels the body", async () => { + let cancelled = false; + const body = new ReadableStream({ + start(controller) { + controller.enqueue(new Uint8Array(200)); + controller.enqueue(new Uint8Array(200)); + }, + cancel() { + cancelled = true; + }, + }); + await expectFailure( + createIssuer(createFetch({ jwksResponse: new Response(body) }).fetch, { + maxResponseBytes: 300, + }).verify(await sign(), AUDIENCE), + "WORKLOAD_JWKS_INVALID", + ); + expect(cancelled).toBe(true); + }); + + it("times out stalled transport and aborts the request", async () => { + let observedAbort = false; + const stalled = ((_input: RequestInfo | URL, init?: RequestInit) => + new Promise((_resolve, reject) => { + init?.signal?.addEventListener( + "abort", + () => { + observedAbort = true; + reject(init.signal?.reason); + }, + { once: true }, + ); + })) as typeof fetch; + await expectFailure( + createIssuer(stalled, { timeoutMs: 10 }).verify(await sign(), AUDIENCE), + "WORKLOAD_ISSUER_UNAVAILABLE", + ); + expect(observedAbort).toBe(true); + }); + + it("applies the same total timeout while reading a stalled body", async () => { + let observedAbort = false; + const transport = ((_input: RequestInfo | URL, init?: RequestInit) => { + const body = new ReadableStream({ + start(controller) { + init?.signal?.addEventListener( + "abort", + () => { + observedAbort = true; + controller.error(init.signal?.reason); + }, + { once: true }, + ); + }, + }); + return Promise.resolve(new Response(body)); + }) as typeof fetch; + await expectFailure( + createIssuer(transport, { timeoutMs: 10 }).verify(await sign(), AUDIENCE), + "WORKLOAD_ISSUER_UNAVAILABLE", + ); + expect(observedAbort).toBe(true); + }); + + it("maps transport failures to a stable unavailable error", async () => { + const transport = (() => Promise.reject(new Error("DNS resolver detail"))) as typeof fetch; + await expectFailure( + createIssuer(transport).verify(await sign(), AUDIENCE), + "WORKLOAD_ISSUER_UNAVAILABLE", + ); + }); + + it("composes caller abort and removes listeners after success", async () => { + const controller = new AbortController(); + const addListener = vi.spyOn(controller.signal, "addEventListener"); + const removeListener = vi.spyOn(controller.signal, "removeEventListener"); + const { fetch } = createFetch(); + const issuer = createIssuer(fetch); + const result = await issuer.verify(await sign(), AUDIENCE, { signal: controller.signal }); + expect(result.success).toBe(true); + expect(addListener).toHaveBeenCalledTimes(1); + expect(removeListener).toHaveBeenCalledTimes(1); + controller.abort(new Error("late abort must be detached")); + expect(result.success).toBe(true); + }); + + it("returns a generic unavailable error for caller abort", async () => { + const controller = new AbortController(); + controller.abort(new Error("sensitive caller reason")); + const result = await createIssuer().verify(await sign(), AUDIENCE, { + signal: controller.signal, + }); + expect(result).toEqual({ + success: false, + error: { code: "WORKLOAD_ISSUER_UNAVAILABLE", message: GENERIC_MESSAGE }, + }); + expect(JSON.stringify(result)).not.toContain("sensitive caller reason"); + }); + + it("does not return success when aborted after signature verification", async () => { + const controller = new AbortController(); + let clockReads = 0; + const issuer = createIssuer(createFetch().fetch, { + now() { + clockReads += 1; + if (clockReads === 2) controller.abort(new Error("post-signature abort")); + return NOW; + }, + }); + await expectFailure( + issuer.verify(await sign(), AUDIENCE, { signal: controller.signal }), + "WORKLOAD_ISSUER_UNAVAILABLE", + ); + expect(clockReads).toBe(2); + }); + + it("refetches JWKS once when a rotated key is first unknown", async () => { + const { fetch, calls } = createFetch({ + jwks: [{ keys: [jwk] }, { keys: [rotatedJwk] }], + }); + const token = await sign({}, { key: rotatedSigningKey, kid: "rotated" }); + const result = await createIssuer(fetch).verify(token, AUDIENCE); + expect(result.success).toBe(true); + expect(calls.filter((request) => request.url.endsWith("/.well-known/jwks"))).toHaveLength(2); + expect(calls.filter((request) => request.url.includes("openid-configuration"))).toHaveLength(1); + }); + + it("does not repeatedly refetch JWKS after one failed rotation retry", async () => { + const { fetch, calls } = createFetch({ jwks: { keys: [jwk] } }); + const token = await sign({}, { key: rotatedSigningKey, kid: "rotated" }); + await expectFailure( + createIssuer(fetch).verify(token, AUDIENCE), + "WORKLOAD_TOKEN_SIGNATURE_INVALID", + ); + expect(calls.filter((request) => request.url.endsWith("/.well-known/jwks"))).toHaveLength(2); + }); +}); diff --git a/apps/release-service/test/verifier.test.ts b/apps/release-service/test/verifier.test.ts new file mode 100644 index 0000000000..ba2579b393 --- /dev/null +++ b/apps/release-service/test/verifier.test.ts @@ -0,0 +1,108 @@ +import { env } from "cloudflare:workers"; +import { describe, expect, it, vi } from "vitest"; + +import { + fetchArtifact, + fetchProvenance, + type ReleaseVerifierBinding, + VerifierUnavailableError, +} from "../src/verifier.js"; + +function verifierBinding(overrides: Partial = {}): ReleaseVerifierBinding { + return { + fetchArtifact: async () => ({ success: true, value: new Uint8Array([1]) }), + fetchProvenance: async () => ({ success: true, value: new Uint8Array([2]) }), + ...overrides, + }; +} + +describe("release verifier binding adapter", () => { + it("validates results across the configured RPC service binding", async () => { + await expect( + fetchArtifact(env.RELEASE_VERIFIER, "https://example.test/plugin.tgz"), + ).resolves.toEqual({ success: true, value: new Uint8Array([7]) }); + await expect( + fetchProvenance(env.RELEASE_VERIFIER, "https://example.test/provenance.json"), + ).rejects.toBeInstanceOf(VerifierUnavailableError); + }); + + it("normalizes configured service-binding failures", async () => { + await expect( + fetchArtifact(env.RELEASE_VERIFIER, "https://example.test/unavailable"), + ).rejects.toMatchObject({ + message: "Release verifier is unavailable", + retryable: true, + }); + }); + + it("returns successful bytes and stable verification failures", async () => { + await expect( + fetchArtifact(verifierBinding(), "https://example.test/plugin.tgz"), + ).resolves.toEqual({ + success: true, + value: new Uint8Array([1]), + }); + const failure = { + success: false, + error: { code: "RESOURCE_STATUS_ERROR", message: "The resource returned HTTP 404." }, + }; + await expect( + fetchProvenance( + verifierBinding({ fetchProvenance: async () => failure }), + "https://example.test/provenance.json", + ), + ).resolves.toEqual(failure); + }); + + it("preserves definitive failures from a newer verifier", async () => { + const failure = { + success: false as const, + error: { code: "NEW_VERSION_CODE", message: "new definitive failure" }, + }; + + await expect( + fetchArtifact( + verifierBinding({ fetchArtifact: async () => failure }), + "https://example.test/plugin.tgz", + ), + ).resolves.toEqual(failure); + }); + + it("fails closed with a retryable generic error when the binding rejects", async () => { + const cause = new Error("internal service address"); + const binding = verifierBinding({ + fetchArtifact: vi.fn().mockRejectedValue(cause), + }); + const result = fetchArtifact(binding, "https://example.test/plugin.tgz"); + await expect(result).rejects.toBeInstanceOf(VerifierUnavailableError); + await expect(result).rejects.toMatchObject({ + message: "Release verifier is unavailable", + retryable: true, + cause, + }); + }); + + it("preserves malformed response details as a diagnostic cause", async () => { + const binding = verifierBinding({ fetchArtifact: async () => null }); + + await expect(fetchArtifact(binding, "https://example.test/plugin.tgz")).rejects.toMatchObject({ + name: "VerifierUnavailableError", + cause: expect.objectContaining({ + name: "TypeError", + message: "Release verifier returned an invalid response", + }), + }); + }); + + it.each([ + null, + { success: true, value: "not bytes" }, + { success: false, error: { code: "FETCH_FAILED" } }, + ])("rejects malformed RPC result %#", async (rpcResult) => { + const binding = verifierBinding({ fetchArtifact: async () => rpcResult }); + await expect(fetchArtifact(binding, "https://example.test/plugin.tgz")).rejects.toMatchObject({ + name: "VerifierUnavailableError", + retryable: true, + }); + }); +}); diff --git a/apps/release-service/test/worker.test.ts b/apps/release-service/test/worker.test.ts new file mode 100644 index 0000000000..a275e0b792 --- /dev/null +++ b/apps/release-service/test/worker.test.ts @@ -0,0 +1,137 @@ +import { SELF, env } from "cloudflare:test"; +import { describe, expect, it, vi } from "vitest"; + +import type { ConfigurationBindings } from "../src/config.js"; +import { failInactiveSchedule, handleRequest, retryUnsupportedQueue } from "../src/index.js"; +import { ROUTES, type RouteDefinition } from "../src/routes.js"; + +const BLOCKED_PATHS = [ + "/v1/release-intents", + "/v1/release-intents/public-id", + "/v1/release-intents/public-id/cancel", + "/v1/release-intents/public-id/approval", + "/v1/release-intents/public-id/approval/options", + "/v1/release-intents/public-id/approve", + "/v1/release-intents/public-id/reject", + "/v1/me", + "/v1/delegations", + "/v1/delegations/start", + "/v1/workload-policies", + "/v1/approver/oauth/start", + "/v1/passkeys", + "/v1/notification-endpoints", + "/v1/audit-events", +]; + +describe("release-service worker", () => { + it("serves a healthy versioned JSON envelope with a request ID", async () => { + const response = await SELF.fetch("https://release.example.invalid/health", { + headers: { "x-request-id": "health-check-1" }, + }); + expect(response.status).toBe(200); + expect(response.headers.get("x-request-id")).toBe("health-check-1"); + expect(response.headers.get("content-type")).toBe("application/json; charset=utf-8"); + expect(response.headers.get("cache-control")).toBe("no-store"); + expect(await response.json()).toEqual({ + data: { status: "ok" }, + requestId: "health-check-1", + }); + }); + + it("fails health closed without exposing configuration details", async () => { + const bindings = { + PUBLIC_ORIGIN: "", + ALLOWED_ORIGINS: "[]", + ALLOWED_PUBLISHERS: '{"mode":"all"}', + DEPLOYMENT_POLICY: "hosted", + ENCRYPTION_KEYRING: + '{"current":1,"keys":[{"version":1,"key":"AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8"}]}', + } satisfies ConfigurationBindings; + const response = await handleRequest(new Request("https://test/health"), bindings); + expect(response.status).toBe(503); + expect(await response.json()).toMatchObject({ + error: { + code: "CONFIGURATION_ERROR", + message: "Service is not configured", + }, + }); + const secondResponse = await handleRequest(new Request("https://test/health"), bindings); + expect(await secondResponse.text()).not.toContain("PUBLIC_ORIGIN"); + }); + + it("provides the configured D1 binding", async () => { + const result = await env.DB.prepare("SELECT 1 AS healthy").first<{ healthy: number }>(); + expect(result).toEqual({ healthy: 1 }); + }); + + it("registers only the health route", () => { + expect(ROUTES.map(({ method, path }) => `${method} ${path}`)).toEqual(["GET /health"]); + }); + + it("catches async route failures without leaking them to clients", async () => { + const internalMessage = "database password leaked"; + const failingRoute: RouteDefinition = { + method: "GET", + path: "/__test/async-rejection", + operationId: "testAsyncRejection", + summary: "Test async rejection", + successStatus: 200, + successDataSchema: { type: "object" }, + async handler() { + await Promise.resolve(); + throw new Error(internalMessage); + }, + }; + const errorLog = vi.spyOn(console, "error").mockImplementation(() => {}); + try { + const response = await handleRequest( + new Request("https://release.example.invalid/__test/async-rejection", { + headers: { "x-request-id": "async-failure-1" }, + }), + env, + [failingRoute], + ); + expect(response.status).toBe(500); + expect(response.headers.get("x-request-id")).toBe("async-failure-1"); + const body = await response.json(); + expect(body).toEqual({ + error: { code: "INTERNAL_ERROR", message: "Internal server error" }, + requestId: "async-failure-1", + }); + expect(JSON.stringify(body)).not.toContain(internalMessage); + expect(errorLog).toHaveBeenCalledWith(expect.stringContaining(internalMessage)); + } finally { + errorLog.mockRestore(); + } + }); + + it.each(BLOCKED_PATHS)("does not expose %s", async (path) => { + for (const method of ["GET", "POST", "PATCH", "DELETE"]) { + const response = await SELF.fetch(`https://release.example.invalid${path}`, { method }); + expect(response.status).toBe(404); + const body = await response.json<{ error: { code: string } }>(); + expect(body.error.code).toBe("NOT_FOUND"); + } + }); + + it("does not expose static assets through the Worker catch-all", async () => { + const response = await SELF.fetch("https://release.example.invalid/.gitkeep"); + expect(response.status).toBe(404); + }); + + it("retries messages while lifecycle consumers are unavailable", () => { + let retryOptions: QueueRetryOptions | undefined; + retryUnsupportedQueue({ + queue: "emdash-release-service-releases", + messages: [{ id: "message-1" }], + retryAll(options) { + retryOptions = options; + }, + }); + expect(retryOptions).toEqual({ delaySeconds: 300 }); + }); + + it("fails scheduled invocations while lifecycle recovery is unavailable", () => { + expect(() => failInactiveSchedule(1234)).toThrowError("Scheduled lifecycle is not active"); + }); +}); diff --git a/apps/release-service/tsconfig.json b/apps/release-service/tsconfig.json new file mode 100644 index 0000000000..34ce731d08 --- /dev/null +++ b/apps/release-service/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "types": ["@cloudflare/vitest-pool-workers/types"], + "verbatimModuleSyntax": true, + "noEmit": true + }, + "include": ["src/**/*", "test/**/*", "worker-configuration.d.ts"] +} diff --git a/apps/release-service/vite.config.ts b/apps/release-service/vite.config.ts new file mode 100644 index 0000000000..f6268f8e95 --- /dev/null +++ b/apps/release-service/vite.config.ts @@ -0,0 +1,6 @@ +import { cloudflare } from "@cloudflare/vite-plugin"; +import { defineConfig } from "vite"; + +export default defineConfig({ + plugins: [cloudflare()], +}); diff --git a/apps/release-service/vitest.config.ts b/apps/release-service/vitest.config.ts new file mode 100644 index 0000000000..49f7781b9b --- /dev/null +++ b/apps/release-service/vitest.config.ts @@ -0,0 +1,35 @@ +import { fileURLToPath } from "node:url"; + +import { cloudflareTest } from "@cloudflare/vitest-pool-workers"; +import { defineConfig } from "vitest/config"; + +const TEST_ENCRYPTION_KEYRING = + '{"current":1,"keys":[{"version":1,"key":"AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8"}]}'; +process.env["ENCRYPTION_KEYRING"] ??= TEST_ENCRYPTION_KEYRING; +const verifierScriptPath = fileURLToPath( + new URL("./test/fixtures/release-verifier.js", import.meta.url), +); + +export default defineConfig({ + plugins: [ + cloudflareTest({ + wrangler: { configPath: "./wrangler.jsonc" }, + miniflare: { + workers: [ + { + name: "emdash-release-verifier", + modules: true, + scriptPath: verifierScriptPath, + }, + ], + bindings: { + PUBLIC_ORIGIN: "https://release.example.invalid", + ALLOWED_ORIGINS: '["https://release.example.invalid"]', + ALLOWED_PUBLISHERS: '{"mode":"all"}', + DEPLOYMENT_POLICY: "hosted", + ENCRYPTION_KEYRING: TEST_ENCRYPTION_KEYRING, + }, + }, + }), + ], +}); diff --git a/apps/release-service/vitest.node.config.ts b/apps/release-service/vitest.node.config.ts new file mode 100644 index 0000000000..ba77ab2aba --- /dev/null +++ b/apps/release-service/vitest.node.config.ts @@ -0,0 +1,7 @@ +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + include: ["test/encryption.test.ts"], + }, +}); diff --git a/apps/release-service/worker-configuration.d.ts b/apps/release-service/worker-configuration.d.ts new file mode 100644 index 0000000000..9bd40a64a0 --- /dev/null +++ b/apps/release-service/worker-configuration.d.ts @@ -0,0 +1,14497 @@ +/* eslint-disable */ +// Generated by Wrangler by running `wrangler types --config=wrangler.jsonc --config=../release-verifier/wrangler.jsonc` (hash: f16c80582f5d22ad180e2539412d622e) +// Runtime types generated with workerd@1.20260611.1 2026-05-14 nodejs_compat +interface __BaseEnv_Env { + DB: D1Database; + RELEASE_QUEUE: Queue; + NOTIFICATION_QUEUE: Queue; + ASSETS: Fetcher; + PUBLIC_ORIGIN: ""; + ALLOWED_ORIGINS: "[]"; + ALLOWED_PUBLISHERS: "{\"mode\":\"allowlist\",\"dids\":[]}"; + DEPLOYMENT_POLICY: ""; + ENCRYPTION_KEYRING: string; + RELEASE_VERIFIER: Service; +} +declare namespace Cloudflare { + interface GlobalProps { + mainModule: typeof import("./src/index"); + } + 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. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/DOMException) + */ +declare class DOMException extends Error { + constructor(message?: string, name?: string); + /** + * The **`message`** read-only property of the a message or description associated with the given error name. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/DOMException/message) + */ + readonly message: string; + /** + * The **`name`** read-only property of the one of the strings associated with an error name. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/DOMException/name) + */ + readonly name: string; + /** + * The **`code`** read-only property of the DOMException interface returns one of the legacy error code constants, or `0` if none match. + * @deprecated + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/DOMException/code) + */ + readonly code: number; + static readonly INDEX_SIZE_ERR: number; + static readonly DOMSTRING_SIZE_ERR: number; + static readonly HIERARCHY_REQUEST_ERR: number; + static readonly WRONG_DOCUMENT_ERR: number; + static readonly INVALID_CHARACTER_ERR: number; + static readonly NO_DATA_ALLOWED_ERR: number; + static readonly NO_MODIFICATION_ALLOWED_ERR: number; + static readonly NOT_FOUND_ERR: number; + static readonly NOT_SUPPORTED_ERR: number; + static readonly INUSE_ATTRIBUTE_ERR: number; + static readonly INVALID_STATE_ERR: number; + static readonly SYNTAX_ERR: number; + static readonly INVALID_MODIFICATION_ERR: number; + static readonly NAMESPACE_ERR: number; + static readonly INVALID_ACCESS_ERR: number; + static readonly VALIDATION_ERR: number; + static readonly TYPE_MISMATCH_ERR: number; + static readonly SECURITY_ERR: number; + static readonly NETWORK_ERR: number; + static readonly ABORT_ERR: number; + static readonly URL_MISMATCH_ERR: number; + static readonly QUOTA_EXCEEDED_ERR: number; + static readonly TIMEOUT_ERR: number; + static readonly INVALID_NODE_TYPE_ERR: number; + static readonly DATA_CLONE_ERR: number; + get stack(): any; + set stack(value: any); +} +type WorkerGlobalScopeEventMap = { + fetch: FetchEvent; + scheduled: ScheduledEvent; + queue: QueueEvent; + unhandledrejection: PromiseRejectionEvent; + rejectionhandled: PromiseRejectionEvent; +}; +declare abstract class WorkerGlobalScope extends EventTarget { + EventTarget: typeof EventTarget; +} +/* The **`console`** object provides access to the debugging console (e.g., the Web console in Firefox). * + * The **`console`** object provides access to the debugging console (e.g., the Web console in Firefox). + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/console) + */ +interface Console { + "assert"(condition?: boolean, ...data: any[]): void; + /** + * The **`console.clear()`** static method clears the console if possible. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/clear_static) + */ + clear(): void; + /** + * The **`console.count()`** static method logs the number of times that this particular call to `count()` has been called. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/count_static) + */ + count(label?: string): void; + /** + * The **`console.countReset()`** static method resets counter used with console/count_static. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/countReset_static) + */ + countReset(label?: string): void; + /** + * The **`console.debug()`** static method outputs a message to the console at the 'debug' log level. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/debug_static) + */ + debug(...data: any[]): void; + /** + * The **`console.dir()`** static method displays a list of the properties of the specified JavaScript object. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/dir_static) + */ + dir(item?: any, options?: any): void; + /** + * The **`console.dirxml()`** static method displays an interactive tree of the descendant elements of the specified XML/HTML element. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/dirxml_static) + */ + dirxml(...data: any[]): void; + /** + * The **`console.error()`** static method outputs a message to the console at the 'error' log level. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/error_static) + */ + error(...data: any[]): void; + /** + * The **`console.group()`** static method creates a new inline group in the Web console log, causing any subsequent console messages to be indented by an additional level, until console/groupEnd_static is called. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/group_static) + */ + group(...data: any[]): void; + /** + * The **`console.groupCollapsed()`** static method creates a new inline group in the console. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/groupCollapsed_static) + */ + groupCollapsed(...data: any[]): void; + /** + * The **`console.groupEnd()`** static method exits the current inline group in the console. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/groupEnd_static) + */ + groupEnd(): void; + /** + * The **`console.info()`** static method outputs a message to the console at the 'info' log level. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/info_static) + */ + info(...data: any[]): void; + /** + * The **`console.log()`** static method outputs a message to the console. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/log_static) + */ + log(...data: any[]): void; + /** + * The **`console.table()`** static method displays tabular data as a table. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/table_static) + */ + table(tabularData?: any, properties?: string[]): void; + /** + * The **`console.time()`** static method starts a timer you can use to track how long an operation takes. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/time_static) + */ + time(label?: string): void; + /** + * The **`console.timeEnd()`** static method stops a timer that was previously started by calling console/time_static. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/timeEnd_static) + */ + timeEnd(label?: string): void; + /** + * The **`console.timeLog()`** static method logs the current value of a timer that was previously started by calling console/time_static. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/timeLog_static) + */ + timeLog(label?: string, ...data: any[]): void; + timeStamp(label?: string): void; + /** + * The **`console.trace()`** static method outputs a stack trace to the console. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/trace_static) + */ + trace(...data: any[]): void; + /** + * The **`console.warn()`** static method outputs a warning message to the console at the 'warning' log level. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/warn_static) + */ + warn(...data: any[]): void; +} +declare const console: Console; +type BufferSource = ArrayBufferView | ArrayBuffer; +type TypedArray = Int8Array | Uint8Array | Uint8ClampedArray | Int16Array | Uint16Array | Int32Array | Uint32Array | Float32Array | Float64Array | BigInt64Array | BigUint64Array; +declare namespace WebAssembly { + class CompileError extends Error { + constructor(message?: string); + } + class RuntimeError extends Error { + constructor(message?: string); + } + type ValueType = "anyfunc" | "externref" | "f32" | "f64" | "i32" | "i64" | "v128"; + interface GlobalDescriptor { + value: ValueType; + mutable?: boolean; + } + class Global { + constructor(descriptor: GlobalDescriptor, value?: any); + value: any; + valueOf(): any; + } + type ImportValue = ExportValue | number; + type ModuleImports = Record; + type Imports = Record; + type ExportValue = Function | Global | Memory | Table; + type Exports = Record; + class Instance { + constructor(module: Module, imports?: Imports); + readonly exports: Exports; + } + interface MemoryDescriptor { + initial: number; + maximum?: number; + shared?: boolean; + } + class Memory { + constructor(descriptor: MemoryDescriptor); + readonly buffer: ArrayBuffer; + grow(delta: number): number; + } + type ImportExportKind = "function" | "global" | "memory" | "table"; + interface ModuleExportDescriptor { + kind: ImportExportKind; + name: string; + } + interface ModuleImportDescriptor { + kind: ImportExportKind; + module: string; + name: string; + } + abstract class Module { + static customSections(module: Module, sectionName: string): ArrayBuffer[]; + static exports(module: Module): ModuleExportDescriptor[]; + static imports(module: Module): ModuleImportDescriptor[]; + } + type TableKind = "anyfunc" | "externref"; + interface TableDescriptor { + element: TableKind; + initial: number; + maximum?: number; + } + class Table { + constructor(descriptor: TableDescriptor, value?: any); + readonly length: number; + get(index: number): any; + grow(delta: number, value?: any): number; + set(index: number, value?: any): void; + } + function instantiate(module: Module, imports?: Imports): Promise; + function validate(bytes: BufferSource): boolean; +} +/** + * The **`ServiceWorkerGlobalScope`** interface of the Service Worker API represents the global execution context of a service worker. + * Available only in secure contexts. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/ServiceWorkerGlobalScope) + */ +interface ServiceWorkerGlobalScope extends WorkerGlobalScope { + DOMException: typeof DOMException; + WorkerGlobalScope: typeof WorkerGlobalScope; + btoa(data: string): string; + atob(data: string): string; + setTimeout(callback: (...args: any[]) => void, msDelay?: number): number; + setTimeout(callback: (...args: Args) => void, msDelay?: number, ...args: Args): number; + clearTimeout(timeoutId: number | null): void; + setInterval(callback: (...args: any[]) => void, msDelay?: number): number; + setInterval(callback: (...args: Args) => void, msDelay?: number, ...args: Args): number; + clearInterval(timeoutId: number | null): void; + queueMicrotask(task: Function): void; + structuredClone(value: T, options?: StructuredSerializeOptions): T; + reportError(error: any): void; + fetch(input: RequestInfo | URL, init?: RequestInit): Promise; + self: ServiceWorkerGlobalScope; + crypto: Crypto; + caches: CacheStorage; + scheduler: Scheduler; + performance: Performance; + Cloudflare: Cloudflare; + readonly origin: string; + Event: typeof Event; + ExtendableEvent: typeof ExtendableEvent; + CustomEvent: typeof CustomEvent; + PromiseRejectionEvent: typeof PromiseRejectionEvent; + FetchEvent: typeof FetchEvent; + TailEvent: typeof TailEvent; + TraceEvent: typeof TailEvent; + ScheduledEvent: typeof ScheduledEvent; + MessageEvent: typeof MessageEvent; + CloseEvent: typeof CloseEvent; + ReadableStreamDefaultReader: typeof ReadableStreamDefaultReader; + ReadableStreamBYOBReader: typeof ReadableStreamBYOBReader; + ReadableStream: typeof ReadableStream; + WritableStream: typeof WritableStream; + WritableStreamDefaultWriter: typeof WritableStreamDefaultWriter; + TransformStream: typeof TransformStream; + ByteLengthQueuingStrategy: typeof ByteLengthQueuingStrategy; + CountQueuingStrategy: typeof CountQueuingStrategy; + ErrorEvent: typeof ErrorEvent; + MessageChannel: typeof MessageChannel; + MessagePort: typeof MessagePort; + EventSource: typeof EventSource; + ReadableStreamBYOBRequest: typeof ReadableStreamBYOBRequest; + ReadableStreamDefaultController: typeof ReadableStreamDefaultController; + ReadableByteStreamController: typeof ReadableByteStreamController; + WritableStreamDefaultController: typeof WritableStreamDefaultController; + TransformStreamDefaultController: typeof TransformStreamDefaultController; + CompressionStream: typeof CompressionStream; + DecompressionStream: typeof DecompressionStream; + TextEncoderStream: typeof TextEncoderStream; + TextDecoderStream: typeof TextDecoderStream; + Headers: typeof Headers; + Body: typeof Body; + Request: typeof Request; + Response: typeof Response; + WebSocket: typeof WebSocket; + WebSocketPair: typeof WebSocketPair; + WebSocketRequestResponsePair: typeof WebSocketRequestResponsePair; + AbortController: typeof AbortController; + AbortSignal: typeof AbortSignal; + TextDecoder: typeof TextDecoder; + TextEncoder: typeof TextEncoder; + navigator: Navigator; + Navigator: typeof Navigator; + URL: typeof URL; + URLSearchParams: typeof URLSearchParams; + URLPattern: typeof URLPattern; + Blob: typeof Blob; + File: typeof File; + FormData: typeof FormData; + Crypto: typeof Crypto; + SubtleCrypto: typeof SubtleCrypto; + CryptoKey: typeof CryptoKey; + CacheStorage: typeof CacheStorage; + Cache: typeof Cache; + FixedLengthStream: typeof FixedLengthStream; + IdentityTransformStream: typeof IdentityTransformStream; + HTMLRewriter: typeof HTMLRewriter; +} +declare function addEventListener(type: Type, handler: EventListenerOrEventListenerObject, options?: EventTargetAddEventListenerOptions | boolean): void; +declare function removeEventListener(type: Type, handler: EventListenerOrEventListenerObject, options?: EventTargetEventListenerOptions | boolean): void; +/** + * The **`dispatchEvent()`** method of the EventTarget sends an Event to the object, (synchronously) invoking the affected event listeners in the appropriate order. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/EventTarget/dispatchEvent) + */ +declare function dispatchEvent(event: WorkerGlobalScopeEventMap[keyof WorkerGlobalScopeEventMap]): boolean; +/* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Window/btoa) */ +declare function btoa(data: string): string; +/* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Window/atob) */ +declare function atob(data: string): string; +/* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Window/setTimeout) */ +declare function setTimeout(callback: (...args: any[]) => void, msDelay?: number): number; +/* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Window/setTimeout) */ +declare function setTimeout(callback: (...args: Args) => void, msDelay?: number, ...args: Args): number; +/* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Window/clearTimeout) */ +declare function clearTimeout(timeoutId: number | null): void; +/* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Window/setInterval) */ +declare function setInterval(callback: (...args: any[]) => void, msDelay?: number): number; +/* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Window/setInterval) */ +declare function setInterval(callback: (...args: Args) => void, msDelay?: number, ...args: Args): number; +/* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Window/clearInterval) */ +declare function clearInterval(timeoutId: number | null): void; +/* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Window/queueMicrotask) */ +declare function queueMicrotask(task: Function): void; +/* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Window/structuredClone) */ +declare function structuredClone(value: T, options?: StructuredSerializeOptions): T; +/* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Window/reportError) */ +declare function reportError(error: any): void; +/* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Window/fetch) */ +declare function fetch(input: RequestInfo | URL, init?: RequestInit): Promise; +declare const self: ServiceWorkerGlobalScope; +/** +* The Web Crypto API provides a set of low-level functions for common cryptographic tasks. +* The Workers runtime implements the full surface of this API, but with some differences in +* the [supported algorithms](https://developers.cloudflare.com/workers/runtime-apis/web-crypto/#supported-algorithms) +* compared to those implemented in most browsers. +* +* [Cloudflare Docs Reference](https://developers.cloudflare.com/workers/runtime-apis/web-crypto/) +*/ +declare const crypto: Crypto; +/** +* The Cache API allows fine grained control of reading and writing from the Cloudflare global network cache. +* +* [Cloudflare Docs Reference](https://developers.cloudflare.com/workers/runtime-apis/cache/) +*/ +declare const caches: CacheStorage; +declare const scheduler: Scheduler; +/** +* The Workers runtime supports a subset of the Performance API, used to measure timing and performance, +* as well as timing of subrequests and other operations. +* +* [Cloudflare Docs Reference](https://developers.cloudflare.com/workers/runtime-apis/performance/) +*/ +declare const performance: Performance; +declare const Cloudflare: Cloudflare; +declare const origin: string; +declare const navigator: Navigator; +interface TestController { +} +interface ExecutionContext { + waitUntil(promise: Promise): void; + passThroughOnException(): void; + readonly exports: Cloudflare.Exports; + readonly props: Props; + cache?: CacheContext; + tracing?: Tracing; +} +type ExportedHandlerFetchHandler = (request: Request>, env: Env, ctx: ExecutionContext) => Response | Promise; +type ExportedHandlerConnectHandler = (socket: Socket, env: Env, ctx: ExecutionContext) => void | Promise; +type ExportedHandlerTailHandler = (events: TraceItem[], env: Env, ctx: ExecutionContext) => void | Promise; +type ExportedHandlerTraceHandler = (traces: TraceItem[], env: Env, ctx: ExecutionContext) => void | Promise; +type ExportedHandlerTailStreamHandler = (event: TailStream.TailEvent, env: Env, ctx: ExecutionContext) => TailStream.TailEventHandlerType | Promise; +type ExportedHandlerScheduledHandler = (controller: ScheduledController, env: Env, ctx: ExecutionContext) => void | Promise; +type ExportedHandlerQueueHandler = (batch: MessageBatch, env: Env, ctx: ExecutionContext) => void | Promise; +type ExportedHandlerTestHandler = (controller: TestController, env: Env, ctx: ExecutionContext) => void | Promise; +interface ExportedHandler { + fetch?: ExportedHandlerFetchHandler; + connect?: ExportedHandlerConnectHandler; + tail?: ExportedHandlerTailHandler; + trace?: ExportedHandlerTraceHandler; + tailStream?: ExportedHandlerTailStreamHandler; + scheduled?: ExportedHandlerScheduledHandler; + test?: ExportedHandlerTestHandler; + email?: EmailExportedHandler; + queue?: ExportedHandlerQueueHandler; +} +interface StructuredSerializeOptions { + transfer?: any[]; +} +declare abstract class Navigator { + sendBeacon(url: string, body?: BodyInit): boolean; + readonly userAgent: string; + readonly hardwareConcurrency: number; + readonly platform: string; + readonly language: string; + readonly languages: string[]; +} +interface AlarmInvocationInfo { + readonly isRetry: boolean; + readonly retryCount: number; + readonly scheduledTime: number; +} +interface Cloudflare { + readonly compatibilityFlags: Record; +} +interface CachePurgeError { + code: number; + message: string; +} +interface CachePurgeResult { + success: boolean; + errors: CachePurgeError[]; +} +interface CachePurgeOptions { + tags?: string[]; + pathPrefixes?: string[]; + purgeEverything?: boolean; +} +interface CacheContext { + purge(options: CachePurgeOptions): Promise; +} +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"; +interface DurableObjectNamespaceNewUniqueIdOptions { + jurisdiction?: DurableObjectJurisdiction; +} +type DurableObjectLocationHint = "wnam" | "enam" | "sam" | "weur" | "eeur" | "apac" | "oc" | "afr" | "me"; +type DurableObjectRoutingMode = "primary-only"; +interface DurableObjectNamespaceGetDurableObjectOptions { + locationHint?: DurableObjectLocationHint; + routingMode?: DurableObjectRoutingMode; +} +interface DurableObjectClass<_T extends Rpc.DurableObjectBranded | undefined = undefined> { +} +interface DurableObjectState { + waitUntil(promise: Promise): void; + readonly exports: Cloudflare.Exports; + readonly props: Props; + readonly id: DurableObjectId; + readonly storage: DurableObjectStorage; + container?: Container; + facets: DurableObjectFacets; + blockConcurrencyWhile(callback: () => Promise): Promise; + acceptWebSocket(ws: WebSocket, tags?: string[]): void; + getWebSockets(tag?: string): WebSocket[]; + setWebSocketAutoResponse(maybeReqResp?: WebSocketRequestResponsePair): void; + getWebSocketAutoResponse(): WebSocketRequestResponsePair | null; + getWebSocketAutoResponseTimestamp(ws: WebSocket): Date | null; + setHibernatableWebSocketEventTimeout(timeoutMs?: number): void; + getHibernatableWebSocketEventTimeout(): number | null; + getTags(ws: WebSocket): string[]; + abort(reason?: string): void; +} +interface DurableObjectTransaction { + get(key: string, options?: DurableObjectGetOptions): Promise; + get(keys: string[], options?: DurableObjectGetOptions): Promise>; + list(options?: DurableObjectListOptions): Promise>; + put(key: string, value: T, options?: DurableObjectPutOptions): Promise; + put(entries: Record, options?: DurableObjectPutOptions): Promise; + delete(key: string, options?: DurableObjectPutOptions): Promise; + delete(keys: string[], options?: DurableObjectPutOptions): Promise; + rollback(): void; + getAlarm(options?: DurableObjectGetAlarmOptions): Promise; + setAlarm(scheduledTime: number | Date, options?: DurableObjectSetAlarmOptions): Promise; + deleteAlarm(options?: DurableObjectSetAlarmOptions): Promise; +} +interface DurableObjectStorage { + get(key: string, options?: DurableObjectGetOptions): Promise; + get(keys: string[], options?: DurableObjectGetOptions): Promise>; + list(options?: DurableObjectListOptions): Promise>; + put(key: string, value: T, options?: DurableObjectPutOptions): Promise; + put(entries: Record, options?: DurableObjectPutOptions): Promise; + delete(key: string, options?: DurableObjectPutOptions): Promise; + delete(keys: string[], options?: DurableObjectPutOptions): Promise; + deleteAll(options?: DurableObjectPutOptions): Promise; + transaction(closure: (txn: DurableObjectTransaction) => Promise): Promise; + getAlarm(options?: DurableObjectGetAlarmOptions): Promise; + setAlarm(scheduledTime: number | Date, options?: DurableObjectSetAlarmOptions): Promise; + deleteAlarm(options?: DurableObjectSetAlarmOptions): Promise; + sync(): Promise; + sql: SqlStorage; + kv: SyncKvStorage; + transactionSync(closure: () => T): T; + getCurrentBookmark(): Promise; + getBookmarkForTime(timestamp: number | Date): Promise; + onNextSessionRestoreBookmark(bookmark: string): Promise; +} +interface DurableObjectListOptions { + start?: string; + startAfter?: string; + end?: string; + prefix?: string; + reverse?: boolean; + limit?: number; + allowConcurrency?: boolean; + noCache?: boolean; +} +interface DurableObjectGetOptions { + allowConcurrency?: boolean; + noCache?: boolean; +} +interface DurableObjectGetAlarmOptions { + allowConcurrency?: boolean; +} +interface DurableObjectPutOptions { + allowConcurrency?: boolean; + allowUnconfirmed?: boolean; + noCache?: boolean; +} +interface DurableObjectSetAlarmOptions { + allowConcurrency?: boolean; + allowUnconfirmed?: boolean; +} +declare class WebSocketRequestResponsePair { + constructor(request: string, response: string); + get request(): string; + get response(): string; +} +interface DurableObjectFacets { + get(name: string, getStartupOptions: () => FacetStartupOptions | Promise>): Fetcher; + abort(name: string, reason: any): void; + delete(name: string): void; +} +interface FacetStartupOptions { + id?: DurableObjectId | string; + class: DurableObjectClass; +} +interface AnalyticsEngineDataset { + writeDataPoint(event?: AnalyticsEngineDataPoint): void; +} +interface AnalyticsEngineDataPoint { + indexes?: ((ArrayBuffer | string) | null)[]; + doubles?: number[]; + blobs?: ((ArrayBuffer | string) | null)[]; +} +/** + * The **`Event`** interface represents an event which takes place on an `EventTarget`. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Event) + */ +declare class Event { + constructor(type: string, init?: EventInit); + /** + * The **`type`** read-only property of the Event interface returns a string containing the event's type. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Event/type) + */ + get type(): string; + /** + * The **`eventPhase`** read-only property of the being evaluated. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Event/eventPhase) + */ + get eventPhase(): number; + /** + * The read-only **`composed`** property of the or not the event will propagate across the shadow DOM boundary into the standard DOM. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Event/composed) + */ + get composed(): boolean; + /** + * The **`bubbles`** read-only property of the Event interface indicates whether the event bubbles up through the DOM tree or not. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Event/bubbles) + */ + get bubbles(): boolean; + /** + * The **`cancelable`** read-only property of the Event interface indicates whether the event can be canceled, and therefore prevented as if the event never happened. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Event/cancelable) + */ + get cancelable(): boolean; + /** + * The **`defaultPrevented`** read-only property of the Event interface returns a boolean value indicating whether or not the call to Event.preventDefault() canceled the event. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Event/defaultPrevented) + */ + get defaultPrevented(): boolean; + /** + * The Event property **`returnValue`** indicates whether the default action for this event has been prevented or not. + * @deprecated + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Event/returnValue) + */ + get returnValue(): boolean; + /** + * The **`currentTarget`** read-only property of the Event interface identifies the element to which the event handler has been attached. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Event/currentTarget) + */ + get currentTarget(): EventTarget | undefined; + /** + * The read-only **`target`** property of the dispatched. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Event/target) + */ + get target(): EventTarget | undefined; + /** + * The deprecated **`Event.srcElement`** is an alias for the Event.target property. + * @deprecated + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Event/srcElement) + */ + get srcElement(): EventTarget | undefined; + /** + * The **`timeStamp`** read-only property of the Event interface returns the time (in milliseconds) at which the event was created. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Event/timeStamp) + */ + get timeStamp(): number; + /** + * The **`isTrusted`** read-only property of the when the event was generated by the user agent (including via user actions and programmatic methods such as HTMLElement.focus()), and `false` when the event was dispatched via The only exception is the `click` event, which initializes the `isTrusted` property to `false` in user agents. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Event/isTrusted) + */ + get isTrusted(): boolean; + /** + * The **`cancelBubble`** property of the Event interface is deprecated. + * @deprecated + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Event/cancelBubble) + */ + get cancelBubble(): boolean; + /** + * The **`cancelBubble`** property of the Event interface is deprecated. + * @deprecated + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Event/cancelBubble) + */ + set cancelBubble(value: boolean); + /** + * The **`stopImmediatePropagation()`** method of the If several listeners are attached to the same element for the same event type, they are called in the order in which they were added. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Event/stopImmediatePropagation) + */ + stopImmediatePropagation(): void; + /** + * The **`preventDefault()`** method of the Event interface tells the user agent that if the event does not get explicitly handled, its default action should not be taken as it normally would be. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Event/preventDefault) + */ + preventDefault(): void; + /** + * The **`stopPropagation()`** method of the Event interface prevents further propagation of the current event in the capturing and bubbling phases. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Event/stopPropagation) + */ + stopPropagation(): void; + /** + * The **`composedPath()`** method of the Event interface returns the event's path which is an array of the objects on which listeners will be invoked. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Event/composedPath) + */ + composedPath(): EventTarget[]; + static readonly NONE: number; + static readonly CAPTURING_PHASE: number; + static readonly AT_TARGET: number; + static readonly BUBBLING_PHASE: number; +} +interface EventInit { + bubbles?: boolean; + cancelable?: boolean; + composed?: boolean; +} +type EventListener = (event: EventType) => void; +interface EventListenerObject { + handleEvent(event: EventType): void; +} +type EventListenerOrEventListenerObject = EventListener | EventListenerObject; +/** + * The **`EventTarget`** interface is implemented by objects that can receive events and may have listeners for them. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/EventTarget) + */ +declare class EventTarget = Record> { + constructor(); + /** + * The **`addEventListener()`** method of the EventTarget interface sets up a function that will be called whenever the specified event is delivered to the target. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/EventTarget/addEventListener) + */ + addEventListener(type: Type, handler: EventListenerOrEventListenerObject, options?: EventTargetAddEventListenerOptions | boolean): void; + /** + * The **`removeEventListener()`** method of the EventTarget interface removes an event listener previously registered with EventTarget.addEventListener() from the target. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/EventTarget/removeEventListener) + */ + removeEventListener(type: Type, handler: EventListenerOrEventListenerObject, options?: EventTargetEventListenerOptions | boolean): void; + /** + * The **`dispatchEvent()`** method of the EventTarget sends an Event to the object, (synchronously) invoking the affected event listeners in the appropriate order. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/EventTarget/dispatchEvent) + */ + dispatchEvent(event: EventMap[keyof EventMap]): boolean; +} +interface EventTargetEventListenerOptions { + capture?: boolean; +} +interface EventTargetAddEventListenerOptions { + capture?: boolean; + passive?: boolean; + once?: boolean; + signal?: AbortSignal; +} +interface EventTargetHandlerObject { + handleEvent: (event: Event) => any | undefined; +} +/** + * The **`AbortController`** interface represents a controller object that allows you to abort one or more Web requests as and when desired. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/AbortController) + */ +declare class AbortController { + constructor(); + /** + * The **`signal`** read-only property of the AbortController interface returns an AbortSignal object instance, which can be used to communicate with/abort an asynchronous operation as desired. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/AbortController/signal) + */ + get signal(): AbortSignal; + /** + * The **`abort()`** method of the AbortController interface aborts an asynchronous operation before it has completed. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/AbortController/abort) + */ + abort(reason?: any): void; +} +/** + * The **`AbortSignal`** interface represents a signal object that allows you to communicate with an asynchronous operation (such as a fetch request) and abort it if required via an AbortController object. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/AbortSignal) + */ +declare abstract class AbortSignal extends EventTarget { + /** + * The **`AbortSignal.abort()`** static method returns an AbortSignal that is already set as aborted (and which does not trigger an AbortSignal/abort_event event). + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/AbortSignal/abort_static) + */ + static abort(reason?: any): AbortSignal; + /** + * The **`AbortSignal.timeout()`** static method returns an AbortSignal that will automatically abort after a specified time. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/AbortSignal/timeout_static) + */ + static timeout(delay: number): AbortSignal; + /** + * The **`AbortSignal.any()`** static method takes an iterable of abort signals and returns an AbortSignal. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/AbortSignal/any_static) + */ + static any(signals: AbortSignal[]): AbortSignal; + /** + * The **`aborted`** read-only property returns a value that indicates whether the asynchronous operations the signal is communicating with are aborted (`true`) or not (`false`). + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/AbortSignal/aborted) + */ + get aborted(): boolean; + /** + * The **`reason`** read-only property returns a JavaScript value that indicates the abort reason. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/AbortSignal/reason) + */ + get reason(): any; + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/AbortSignal/abort_event) */ + get onabort(): any | null; + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/AbortSignal/abort_event) */ + set onabort(value: any | null); + /** + * The **`throwIfAborted()`** method throws the signal's abort AbortSignal.reason if the signal has been aborted; otherwise it does nothing. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/AbortSignal/throwIfAborted) + */ + throwIfAborted(): void; +} +interface Scheduler { + wait(delay: number, maybeOptions?: SchedulerWaitOptions): Promise; +} +interface SchedulerWaitOptions { + signal?: AbortSignal; +} +/** + * The **`ExtendableEvent`** interface extends the lifetime of the `install` and `activate` events dispatched on the global scope as part of the service worker lifecycle. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/ExtendableEvent) + */ +declare abstract class ExtendableEvent extends Event { + /** + * The **`ExtendableEvent.waitUntil()`** method tells the event dispatcher that work is ongoing. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/ExtendableEvent/waitUntil) + */ + waitUntil(promise: Promise): void; +} +/** + * The **`CustomEvent`** interface represents events initialized by an application for any purpose. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/CustomEvent) + */ +declare class CustomEvent extends Event { + constructor(type: string, init?: CustomEventCustomEventInit); + /** + * The read-only **`detail`** property of the CustomEvent interface returns any data passed when initializing the event. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/CustomEvent/detail) + */ + get detail(): T; +} +interface CustomEventCustomEventInit { + bubbles?: boolean; + cancelable?: boolean; + composed?: boolean; + detail?: any; +} +/** + * The **`Blob`** interface represents a blob, which is a file-like object of immutable, raw data; they can be read as text or binary data, or converted into a ReadableStream so its methods can be used for processing the data. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Blob) + */ +declare class Blob { + constructor(bits?: ((ArrayBuffer | ArrayBufferView) | string | Blob)[], options?: BlobOptions); + /** + * The **`size`** read-only property of the Blob interface returns the size of the Blob or File in bytes. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Blob/size) + */ + get size(): number; + /** + * The **`type`** read-only property of the Blob interface returns the MIME type of the file. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Blob/type) + */ + get type(): string; + /** + * The **`slice()`** method of the Blob interface creates and returns a new `Blob` object which contains data from a subset of the blob on which it's called. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Blob/slice) + */ + slice(start?: number, end?: number, type?: string): Blob; + /** + * The **`arrayBuffer()`** method of the Blob interface returns a Promise that resolves with the contents of the blob as binary data contained in an ArrayBuffer. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Blob/arrayBuffer) + */ + arrayBuffer(): Promise; + /** + * The **`bytes()`** method of the Blob interface returns a Promise that resolves with a Uint8Array containing the contents of the blob as an array of bytes. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Blob/bytes) + */ + bytes(): Promise; + /** + * The **`text()`** method of the string containing the contents of the blob, interpreted as UTF-8. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Blob/text) + */ + text(): Promise; + /** + * The **`stream()`** method of the Blob interface returns a ReadableStream which upon reading returns the data contained within the `Blob`. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Blob/stream) + */ + stream(): ReadableStream; +} +interface BlobOptions { + type?: string; +} +/** + * The **`File`** interface provides information about files and allows JavaScript in a web page to access their content. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/File) + */ +declare class File extends Blob { + constructor(bits: ((ArrayBuffer | ArrayBufferView) | string | Blob)[] | undefined, name: string, options?: FileOptions); + /** + * The **`name`** read-only property of the File interface returns the name of the file represented by a File object. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/File/name) + */ + get name(): string; + /** + * The **`lastModified`** read-only property of the File interface provides the last modified date of the file as the number of milliseconds since the Unix epoch (January 1, 1970 at midnight). + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/File/lastModified) + */ + get lastModified(): number; +} +interface FileOptions { + type?: string; + lastModified?: number; +} +/** +* 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 abstract class CacheStorage { + /** + * The **`open()`** method of the the Cache object matching the `cacheName`. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/CacheStorage/open) + */ + open(cacheName: string): Promise; + readonly default: Cache; +} +/** +* 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 abstract class Cache { + /* [Cloudflare Docs Reference](https://developers.cloudflare.com/workers/runtime-apis/cache/#delete) */ + delete(request: RequestInfo | URL, options?: CacheQueryOptions): Promise; + /* [Cloudflare Docs Reference](https://developers.cloudflare.com/workers/runtime-apis/cache/#match) */ + match(request: RequestInfo | URL, options?: CacheQueryOptions): Promise; + /* [Cloudflare Docs Reference](https://developers.cloudflare.com/workers/runtime-apis/cache/#put) */ + put(request: RequestInfo | URL, response: Response): Promise; +} +interface CacheQueryOptions { + ignoreMethod?: boolean; +} +/** +* 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 abstract class Crypto { + /** + * The **`Crypto.subtle`** read-only property returns a cryptographic operations. + * Available only in secure contexts. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Crypto/subtle) + */ + get subtle(): SubtleCrypto; + /** + * The **`Crypto.getRandomValues()`** method lets you get cryptographically strong random values. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Crypto/getRandomValues) + */ + getRandomValues(buffer: T): T; + /** + * The **`randomUUID()`** method of the Crypto interface is used to generate a v4 UUID using a cryptographically secure random number generator. + * Available only in secure contexts. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Crypto/randomUUID) + */ + randomUUID(): string; + DigestStream: typeof DigestStream; +} +/** + * The **`SubtleCrypto`** interface of the Web Crypto API provides a number of low-level cryptographic functions. + * Available only in secure contexts. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/SubtleCrypto) + */ +declare abstract class SubtleCrypto { + /** + * The **`encrypt()`** method of the SubtleCrypto interface encrypts data. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/SubtleCrypto/encrypt) + */ + encrypt(algorithm: string | SubtleCryptoEncryptAlgorithm, key: CryptoKey, plainText: ArrayBuffer | ArrayBufferView): Promise; + /** + * The **`decrypt()`** method of the SubtleCrypto interface decrypts some encrypted data. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/SubtleCrypto/decrypt) + */ + decrypt(algorithm: string | SubtleCryptoEncryptAlgorithm, key: CryptoKey, cipherText: ArrayBuffer | ArrayBufferView): Promise; + /** + * The **`sign()`** method of the SubtleCrypto interface generates a digital signature. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/SubtleCrypto/sign) + */ + sign(algorithm: string | SubtleCryptoSignAlgorithm, key: CryptoKey, data: ArrayBuffer | ArrayBufferView): Promise; + /** + * The **`verify()`** method of the SubtleCrypto interface verifies a digital signature. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/SubtleCrypto/verify) + */ + verify(algorithm: string | SubtleCryptoSignAlgorithm, key: CryptoKey, signature: ArrayBuffer | ArrayBufferView, data: ArrayBuffer | ArrayBufferView): Promise; + /** + * The **`digest()`** method of the SubtleCrypto interface generates a _digest_ of the given data, using the specified hash function. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/SubtleCrypto/digest) + */ + digest(algorithm: string | SubtleCryptoHashAlgorithm, data: ArrayBuffer | ArrayBufferView): Promise; + /** + * The **`generateKey()`** method of the SubtleCrypto interface is used to generate a new key (for symmetric algorithms) or key pair (for public-key algorithms). + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/SubtleCrypto/generateKey) + */ + generateKey(algorithm: string | SubtleCryptoGenerateKeyAlgorithm, extractable: boolean, keyUsages: string[]): Promise; + /** + * The **`deriveKey()`** method of the SubtleCrypto interface can be used to derive a secret key from a master key. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/SubtleCrypto/deriveKey) + */ + deriveKey(algorithm: string | SubtleCryptoDeriveKeyAlgorithm, baseKey: CryptoKey, derivedKeyAlgorithm: string | SubtleCryptoImportKeyAlgorithm, extractable: boolean, keyUsages: string[]): Promise; + /** + * The **`deriveBits()`** method of the key. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/SubtleCrypto/deriveBits) + */ + deriveBits(algorithm: string | SubtleCryptoDeriveKeyAlgorithm, baseKey: CryptoKey, length?: number | null): Promise; + /** + * The **`importKey()`** method of the SubtleCrypto interface imports a key: that is, it takes as input a key in an external, portable format and gives you a CryptoKey object that you can use in the Web Crypto API. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/SubtleCrypto/importKey) + */ + importKey(format: string, keyData: (ArrayBuffer | ArrayBufferView) | JsonWebKey, algorithm: string | SubtleCryptoImportKeyAlgorithm, extractable: boolean, keyUsages: string[]): Promise; + /** + * The **`exportKey()`** method of the SubtleCrypto interface exports a key: that is, it takes as input a CryptoKey object and gives you the key in an external, portable format. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/SubtleCrypto/exportKey) + */ + exportKey(format: string, key: CryptoKey): Promise; + /** + * The **`wrapKey()`** method of the SubtleCrypto interface 'wraps' a key. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/SubtleCrypto/wrapKey) + */ + wrapKey(format: string, key: CryptoKey, wrappingKey: CryptoKey, wrapAlgorithm: string | SubtleCryptoEncryptAlgorithm): Promise; + /** + * The **`unwrapKey()`** method of the SubtleCrypto interface 'unwraps' a key. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/SubtleCrypto/unwrapKey) + */ + unwrapKey(format: string, wrappedKey: ArrayBuffer | ArrayBufferView, unwrappingKey: CryptoKey, unwrapAlgorithm: string | SubtleCryptoEncryptAlgorithm, unwrappedKeyAlgorithm: string | SubtleCryptoImportKeyAlgorithm, extractable: boolean, keyUsages: string[]): Promise; + timingSafeEqual(a: ArrayBuffer | ArrayBufferView, b: ArrayBuffer | ArrayBufferView): boolean; +} +/** + * The **`CryptoKey`** interface of the Web Crypto API represents a cryptographic key obtained from one of the SubtleCrypto methods SubtleCrypto.generateKey, SubtleCrypto.deriveKey, SubtleCrypto.importKey, or SubtleCrypto.unwrapKey. + * Available only in secure contexts. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/CryptoKey) + */ +declare abstract class CryptoKey { + /** + * The read-only **`type`** property of the CryptoKey interface indicates which kind of key is represented by the object. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/CryptoKey/type) + */ + readonly type: string; + /** + * The read-only **`extractable`** property of the CryptoKey interface indicates whether or not the key may be extracted using `SubtleCrypto.exportKey()` or `SubtleCrypto.wrapKey()`. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/CryptoKey/extractable) + */ + readonly extractable: boolean; + /** + * The read-only **`algorithm`** property of the CryptoKey interface returns an object describing the algorithm for which this key can be used, and any associated extra parameters. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/CryptoKey/algorithm) + */ + readonly algorithm: CryptoKeyKeyAlgorithm | CryptoKeyAesKeyAlgorithm | CryptoKeyHmacKeyAlgorithm | CryptoKeyRsaKeyAlgorithm | CryptoKeyEllipticKeyAlgorithm | CryptoKeyArbitraryKeyAlgorithm; + /** + * The read-only **`usages`** property of the CryptoKey interface indicates what can be done with the key. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/CryptoKey/usages) + */ + readonly usages: string[]; +} +interface CryptoKeyPair { + publicKey: CryptoKey; + privateKey: CryptoKey; +} +interface JsonWebKey { + kty: string; + use?: string; + key_ops?: string[]; + alg?: string; + ext?: boolean; + crv?: string; + x?: string; + y?: string; + d?: string; + n?: string; + e?: string; + p?: string; + q?: string; + dp?: string; + dq?: string; + qi?: string; + oth?: RsaOtherPrimesInfo[]; + k?: string; +} +interface RsaOtherPrimesInfo { + r?: string; + d?: string; + t?: string; +} +interface SubtleCryptoDeriveKeyAlgorithm { + name: string; + salt?: (ArrayBuffer | ArrayBufferView); + iterations?: number; + hash?: (string | SubtleCryptoHashAlgorithm); + $public?: CryptoKey; + info?: (ArrayBuffer | ArrayBufferView); +} +interface SubtleCryptoEncryptAlgorithm { + name: string; + iv?: (ArrayBuffer | ArrayBufferView); + additionalData?: (ArrayBuffer | ArrayBufferView); + tagLength?: number; + counter?: (ArrayBuffer | ArrayBufferView); + length?: number; + label?: (ArrayBuffer | ArrayBufferView); +} +interface SubtleCryptoGenerateKeyAlgorithm { + name: string; + hash?: (string | SubtleCryptoHashAlgorithm); + modulusLength?: number; + publicExponent?: (ArrayBuffer | ArrayBufferView); + length?: number; + namedCurve?: string; +} +interface SubtleCryptoHashAlgorithm { + name: string; +} +interface SubtleCryptoImportKeyAlgorithm { + name: string; + hash?: (string | SubtleCryptoHashAlgorithm); + length?: number; + namedCurve?: string; + compressed?: boolean; +} +interface SubtleCryptoSignAlgorithm { + name: string; + hash?: (string | SubtleCryptoHashAlgorithm); + dataLength?: number; + saltLength?: number; +} +interface CryptoKeyKeyAlgorithm { + name: string; +} +interface CryptoKeyAesKeyAlgorithm { + name: string; + length: number; +} +interface CryptoKeyHmacKeyAlgorithm { + name: string; + hash: CryptoKeyKeyAlgorithm; + length: number; +} +interface CryptoKeyRsaKeyAlgorithm { + name: string; + modulusLength: number; + publicExponent: ArrayBuffer | ArrayBufferView; + hash?: CryptoKeyKeyAlgorithm; +} +interface CryptoKeyEllipticKeyAlgorithm { + name: string; + namedCurve: string; +} +interface CryptoKeyArbitraryKeyAlgorithm { + name: string; + hash?: CryptoKeyKeyAlgorithm; + namedCurve?: string; + length?: number; +} +declare class DigestStream extends WritableStream { + constructor(algorithm: string | SubtleCryptoHashAlgorithm); + readonly digest: Promise; + get bytesWritten(): number | bigint; +} +/** + * The **`TextDecoder`** interface represents a decoder for a specific text encoding, such as `UTF-8`, `ISO-8859-2`, `KOI8-R`, `GBK`, etc. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/TextDecoder) + */ +declare class TextDecoder { + constructor(label?: string, options?: TextDecoderConstructorOptions); + /** + * The **`TextDecoder.decode()`** method returns a string containing text decoded from the buffer passed as a parameter. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/TextDecoder/decode) + */ + decode(input?: (ArrayBuffer | ArrayBufferView), options?: TextDecoderDecodeOptions): string; + get encoding(): string; + get fatal(): boolean; + get ignoreBOM(): boolean; +} +/** + * The **`TextEncoder`** interface takes a stream of code points as input and emits a stream of UTF-8 bytes. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/TextEncoder) + */ +declare class TextEncoder { + constructor(); + /** + * The **`TextEncoder.encode()`** method takes a string as input, and returns a Global_Objects/Uint8Array containing the text given in parameters encoded with the specific method for that TextEncoder object. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/TextEncoder/encode) + */ + encode(input?: string): Uint8Array; + /** + * The **`TextEncoder.encodeInto()`** method takes a string to encode and a destination Uint8Array to put resulting UTF-8 encoded text into, and returns a dictionary object indicating the progress of the encoding. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/TextEncoder/encodeInto) + */ + encodeInto(input: string, buffer: Uint8Array): TextEncoderEncodeIntoResult; + get encoding(): string; +} +interface TextDecoderConstructorOptions { + fatal: boolean; + ignoreBOM: boolean; +} +interface TextDecoderDecodeOptions { + stream: boolean; +} +interface TextEncoderEncodeIntoResult { + read: number; + written: number; +} +/** + * The **`ErrorEvent`** interface represents events providing information related to errors in scripts or in files. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/ErrorEvent) + */ +declare class ErrorEvent extends Event { + constructor(type: string, init?: ErrorEventErrorEventInit); + /** + * The **`filename`** read-only property of the ErrorEvent interface returns a string containing the name of the script file in which the error occurred. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/ErrorEvent/filename) + */ + get filename(): string; + /** + * The **`message`** read-only property of the ErrorEvent interface returns a string containing a human-readable error message describing the problem. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/ErrorEvent/message) + */ + get message(): string; + /** + * The **`lineno`** read-only property of the ErrorEvent interface returns an integer containing the line number of the script file on which the error occurred. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/ErrorEvent/lineno) + */ + get lineno(): number; + /** + * The **`colno`** read-only property of the ErrorEvent interface returns an integer containing the column number of the script file on which the error occurred. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/ErrorEvent/colno) + */ + get colno(): number; + /** + * The **`error`** read-only property of the ErrorEvent interface returns a JavaScript value, such as an Error or DOMException, representing the error associated with this event. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/ErrorEvent/error) + */ + get error(): any; +} +interface ErrorEventErrorEventInit { + message?: string; + filename?: string; + lineno?: number; + colno?: number; + error?: any; +} +/** + * The **`MessageEvent`** interface represents a message received by a target object. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/MessageEvent) + */ +declare class MessageEvent extends Event { + constructor(type: string, initializer: MessageEventInit); + /** + * The **`data`** read-only property of the The data sent by the message emitter; this can be any data type, depending on what originated this event. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/MessageEvent/data) + */ + readonly data: any; + /** + * The **`origin`** read-only property of the origin of the message emitter. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/MessageEvent/origin) + */ + readonly origin: string | null; + /** + * The **`lastEventId`** read-only property of the unique ID for the event. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/MessageEvent/lastEventId) + */ + readonly lastEventId: string; + /** + * The **`source`** read-only property of the a WindowProxy, MessagePort, or a `MessageEventSource` (which can be a WindowProxy, message emitter. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/MessageEvent/source) + */ + readonly source: MessagePort | null; + /** + * The **`ports`** read-only property of the containing all MessagePort objects sent with the message, in order. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/MessageEvent/ports) + */ + readonly ports: MessagePort[]; +} +interface MessageEventInit { + data: ArrayBuffer | string; +} +/** + * The **`PromiseRejectionEvent`** interface represents events which are sent to the global script context when JavaScript Promises are rejected. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/PromiseRejectionEvent) + */ +declare abstract class PromiseRejectionEvent extends Event { + /** + * The PromiseRejectionEvent interface's **`promise`** read-only property indicates the JavaScript rejected. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/PromiseRejectionEvent/promise) + */ + readonly promise: Promise; + /** + * The PromiseRejectionEvent **`reason`** read-only property is any JavaScript value or Object which provides the reason passed into Promise.reject(). + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/PromiseRejectionEvent/reason) + */ + readonly reason: any; +} +/** + * The **`FormData`** interface provides a way to construct a set of key/value pairs representing form fields and their values, which can be sent using the Window/fetch, XMLHttpRequest.send() or navigator.sendBeacon() methods. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/FormData) + */ +declare class FormData { + constructor(); + /** + * The **`append()`** method of the FormData interface appends a new value onto an existing key inside a `FormData` object, or adds the key if it does not already exist. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/FormData/append) + */ + append(name: string, value: string | Blob): void; + /** + * The **`append()`** method of the FormData interface appends a new value onto an existing key inside a `FormData` object, or adds the key if it does not already exist. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/FormData/append) + */ + append(name: string, value: string): void; + /** + * The **`append()`** method of the FormData interface appends a new value onto an existing key inside a `FormData` object, or adds the key if it does not already exist. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/FormData/append) + */ + append(name: string, value: Blob, filename?: string): void; + /** + * The **`delete()`** method of the FormData interface deletes a key and its value(s) from a `FormData` object. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/FormData/delete) + */ + delete(name: string): void; + /** + * The **`get()`** method of the FormData interface returns the first value associated with a given key from within a `FormData` object. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/FormData/get) + */ + get(name: string): (File | string) | null; + /** + * The **`getAll()`** method of the FormData interface returns all the values associated with a given key from within a `FormData` object. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/FormData/getAll) + */ + getAll(name: string): (File | string)[]; + /** + * The **`has()`** method of the FormData interface returns whether a `FormData` object contains a certain key. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/FormData/has) + */ + has(name: string): boolean; + /** + * The **`set()`** method of the FormData interface sets a new value for an existing key inside a `FormData` object, or adds the key/value if it does not already exist. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/FormData/set) + */ + set(name: string, value: string | Blob): void; + /** + * The **`set()`** method of the FormData interface sets a new value for an existing key inside a `FormData` object, or adds the key/value if it does not already exist. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/FormData/set) + */ + set(name: string, value: string): void; + /** + * The **`set()`** method of the FormData interface sets a new value for an existing key inside a `FormData` object, or adds the key/value if it does not already exist. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/FormData/set) + */ + set(name: string, value: Blob, filename?: string): void; + /* Returns an array of key, value pairs for every entry in the list. */ + entries(): IterableIterator<[ + key: string, + value: File | string + ]>; + /* Returns a list of keys in the list. */ + keys(): IterableIterator; + /* Returns a list of values in the list. */ + values(): IterableIterator<(File | string)>; + forEach(callback: (this: This, value: File | string, key: string, parent: FormData) => void, thisArg?: This): void; + [Symbol.iterator](): IterableIterator<[ + key: string, + value: File | string + ]>; +} +interface ContentOptions { + html?: boolean; +} +declare class HTMLRewriter { + constructor(); + on(selector: string, handlers: HTMLRewriterElementContentHandlers): HTMLRewriter; + onDocument(handlers: HTMLRewriterDocumentContentHandlers): HTMLRewriter; + transform(response: Response): Response; +} +interface HTMLRewriterElementContentHandlers { + element?(element: Element): void | Promise; + comments?(comment: Comment): void | Promise; + text?(element: Text): void | Promise; +} +interface HTMLRewriterDocumentContentHandlers { + doctype?(doctype: Doctype): void | Promise; + comments?(comment: Comment): void | Promise; + text?(text: Text): void | Promise; + end?(end: DocumentEnd): void | Promise; +} +interface Doctype { + readonly name: string | null; + readonly publicId: string | null; + readonly systemId: string | null; +} +interface Element { + tagName: string; + readonly attributes: IterableIterator; + readonly removed: boolean; + readonly namespaceURI: string; + getAttribute(name: string): string | null; + hasAttribute(name: string): boolean; + setAttribute(name: string, value: string): Element; + removeAttribute(name: string): Element; + before(content: string | ReadableStream | Response, options?: ContentOptions): Element; + after(content: string | ReadableStream | Response, options?: ContentOptions): Element; + prepend(content: string | ReadableStream | Response, options?: ContentOptions): Element; + append(content: string | ReadableStream | Response, options?: ContentOptions): Element; + replace(content: string | ReadableStream | Response, options?: ContentOptions): Element; + remove(): Element; + removeAndKeepContent(): Element; + setInnerContent(content: string | ReadableStream | Response, options?: ContentOptions): Element; + onEndTag(handler: (tag: EndTag) => void | Promise): void; +} +interface EndTag { + name: string; + before(content: string | ReadableStream | Response, options?: ContentOptions): EndTag; + after(content: string | ReadableStream | Response, options?: ContentOptions): EndTag; + remove(): EndTag; +} +interface Comment { + text: string; + readonly removed: boolean; + before(content: string, options?: ContentOptions): Comment; + after(content: string, options?: ContentOptions): Comment; + replace(content: string, options?: ContentOptions): Comment; + remove(): Comment; +} +interface Text { + readonly text: string; + readonly lastInTextNode: boolean; + readonly removed: boolean; + before(content: string | ReadableStream | Response, options?: ContentOptions): Text; + after(content: string | ReadableStream | Response, options?: ContentOptions): Text; + replace(content: string | ReadableStream | Response, options?: ContentOptions): Text; + remove(): Text; +} +interface DocumentEnd { + append(content: string, options?: ContentOptions): DocumentEnd; +} +/** + * This is the event type for `fetch` events dispatched on the ServiceWorkerGlobalScope. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/FetchEvent) + */ +declare abstract class FetchEvent extends ExtendableEvent { + /** + * The **`request`** read-only property of the the event handler. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/FetchEvent/request) + */ + readonly request: Request; + /** + * The **`respondWith()`** method of allows you to provide a promise for a Response yourself. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/FetchEvent/respondWith) + */ + respondWith(promise: Response | Promise): void; + passThroughOnException(): void; +} +type HeadersInit = Headers | Iterable> | Record; +/** + * The **`Headers`** interface of the Fetch API allows you to perform various actions on HTTP request and response headers. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Headers) + */ +declare class Headers { + constructor(init?: HeadersInit); + /** + * The **`get()`** method of the Headers interface returns a byte string of all the values of a header within a `Headers` object with a given name. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Headers/get) + */ + get(name: string): string | null; + getAll(name: string): string[]; + /** + * The **`getSetCookie()`** method of the Headers interface returns an array containing the values of all Set-Cookie headers associated with a response. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Headers/getSetCookie) + */ + getSetCookie(): string[]; + /** + * The **`has()`** method of the Headers interface returns a boolean stating whether a `Headers` object contains a certain header. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Headers/has) + */ + has(name: string): boolean; + /** + * The **`set()`** method of the Headers interface sets a new value for an existing header inside a `Headers` object, or adds the header if it does not already exist. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Headers/set) + */ + set(name: string, value: string): void; + /** + * The **`append()`** method of the Headers interface appends a new value onto an existing header inside a `Headers` object, or adds the header if it does not already exist. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Headers/append) + */ + append(name: string, value: string): void; + /** + * The **`delete()`** method of the Headers interface deletes a header from the current `Headers` object. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Headers/delete) + */ + delete(name: string): void; + forEach(callback: (this: This, value: string, key: string, parent: Headers) => void, thisArg?: This): void; + /* Returns an iterator allowing to go through all key/value pairs contained in this object. */ + entries(): IterableIterator<[ + key: string, + value: string + ]>; + /* Returns an iterator allowing to go through all keys of the key/value pairs contained in this object. */ + keys(): IterableIterator; + /* Returns an iterator allowing to go through all values of the key/value pairs contained in this object. */ + values(): IterableIterator; + [Symbol.iterator](): IterableIterator<[ + key: string, + value: string + ]>; +} +type BodyInit = ReadableStream | string | ArrayBuffer | ArrayBufferView | Blob | URLSearchParams | FormData | Iterable | AsyncIterable; +declare abstract class Body { + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Request/body) */ + get body(): ReadableStream | null; + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Request/bodyUsed) */ + get bodyUsed(): boolean; + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Request/arrayBuffer) */ + arrayBuffer(): Promise; + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Request/bytes) */ + bytes(): Promise; + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Request/text) */ + text(): Promise; + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Request/json) */ + json(): Promise; + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Request/formData) */ + formData(): Promise; + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Request/blob) */ + blob(): Promise; +} +/** + * The **`Response`** interface of the Fetch API represents the response to a request. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Response) + */ +declare var Response: { + prototype: Response; + new (body?: BodyInit | null, init?: ResponseInit): Response; + error(): Response; + redirect(url: string, status?: number): Response; + json(any: any, maybeInit?: (ResponseInit | Response)): Response; +}; +/** + * The **`Response`** interface of the Fetch API represents the response to a request. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Response) + */ +interface Response extends Body { + /** + * The **`clone()`** method of the Response interface creates a clone of a response object, identical in every way, but stored in a different variable. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Response/clone) + */ + clone(): Response; + /** + * The **`status`** read-only property of the Response interface contains the HTTP status codes of the response. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Response/status) + */ + status: number; + /** + * The **`statusText`** read-only property of the Response interface contains the status message corresponding to the HTTP status code in Response.status. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Response/statusText) + */ + statusText: string; + /** + * The **`headers`** read-only property of the with the response. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Response/headers) + */ + headers: Headers; + /** + * The **`ok`** read-only property of the Response interface contains a Boolean stating whether the response was successful (status in the range 200-299) or not. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Response/ok) + */ + ok: boolean; + /** + * The **`redirected`** read-only property of the Response interface indicates whether or not the response is the result of a request you made which was redirected. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Response/redirected) + */ + redirected: boolean; + /** + * The **`url`** read-only property of the Response interface contains the URL of the response. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Response/url) + */ + url: string; + webSocket: WebSocket | null; + cf: any | undefined; + /** + * The **`type`** read-only property of the Response interface contains the type of the response. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Response/type) + */ + type: "default" | "error"; +} +interface ResponseInit { + status?: number; + statusText?: string; + headers?: HeadersInit; + cf?: any; + webSocket?: (WebSocket | null); + encodeBody?: "automatic" | "manual"; +} +type RequestInfo> = Request | string; +/** + * The **`Request`** interface of the Fetch API represents a resource request. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Request) + */ +declare var Request: { + prototype: Request; + new >(input: RequestInfo | URL, init?: RequestInit): Request; +}; +/** + * The **`Request`** interface of the Fetch API represents a resource request. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Request) + */ +interface Request> extends Body { + /** + * The **`clone()`** method of the Request interface creates a copy of the current `Request` object. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Request/clone) + */ + clone(): Request; + /** + * The **`method`** read-only property of the `POST`, etc.) A String indicating the method of the request. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Request/method) + */ + method: string; + /** + * The **`url`** read-only property of the Request interface contains the URL of the request. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Request/url) + */ + url: string; + /** + * The **`headers`** read-only property of the with the request. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Request/headers) + */ + headers: Headers; + /** + * The **`redirect`** read-only property of the Request interface contains the mode for how redirects are handled. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Request/redirect) + */ + redirect: string; + fetcher: Fetcher | null; + /** + * The read-only **`signal`** property of the Request interface returns the AbortSignal associated with the request. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Request/signal) + */ + signal: AbortSignal; + cf?: Cf; + /** + * The **`integrity`** read-only property of the Request interface contains the subresource integrity value of the request. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Request/integrity) + */ + integrity: string; + /** + * The **`keepalive`** read-only property of the Request interface contains the request's `keepalive` setting (`true` or `false`), which indicates whether the browser will keep the associated request alive if the page that initiated it is unloaded before the request is complete. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Request/keepalive) + */ + keepalive: boolean; + /** + * The **`cache`** read-only property of the Request interface contains the cache mode of the request. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Request/cache) + */ + cache?: "no-store" | "no-cache"; +} +interface RequestInit { + /* A string to set request's method. */ + method?: string; + /* A Headers object, an object literal, or an array of two-item arrays to set request's headers. */ + headers?: HeadersInit; + /* A BodyInit object or null to set request's body. */ + body?: BodyInit | null; + /* A string indicating whether request follows redirects, results in an error upon encountering a redirect, or returns the redirect (in an opaque fashion). Sets request's redirect. */ + redirect?: string; + fetcher?: (Fetcher | null); + cf?: Cf; + /* A string indicating how the request will interact with the browser's cache to set request's cache. */ + cache?: "no-store" | "no-cache"; + /* A cryptographic hash of the resource to be fetched by request. Sets request's integrity. */ + integrity?: string; + /* An AbortSignal to set request's signal. */ + signal?: (AbortSignal | null); + encodeResponseBody?: "automatic" | "manual"; +} +type Service Rpc.WorkerEntrypointBranded) | Rpc.WorkerEntrypointBranded | ExportedHandler | undefined = undefined> = T extends new (...args: any[]) => Rpc.WorkerEntrypointBranded ? Fetcher> : T extends Rpc.WorkerEntrypointBranded ? Fetcher : T extends Exclude ? never : Fetcher; +type Fetcher = (T extends Rpc.EntrypointBranded ? Rpc.Provider : unknown) & { + fetch(input: RequestInfo | URL, init?: RequestInit): Promise; + connect(address: SocketAddress | string, options?: SocketOptions): Socket; +}; +interface KVNamespaceListKey { + name: Key; + expiration?: number; + metadata?: Metadata; +} +type KVNamespaceListResult = { + list_complete: false; + keys: KVNamespaceListKey[]; + cursor: string; + cacheStatus: string | null; +} | { + list_complete: true; + keys: KVNamespaceListKey[]; + cacheStatus: string | null; +}; +interface KVNamespace { + get(key: Key, options?: Partial>): Promise; + get(key: Key, type: "text"): Promise; + get(key: Key, type: "json"): Promise; + get(key: Key, type: "arrayBuffer"): Promise; + get(key: Key, type: "stream"): Promise; + get(key: Key, options?: KVNamespaceGetOptions<"text">): Promise; + get(key: Key, options?: KVNamespaceGetOptions<"json">): Promise; + get(key: Key, options?: KVNamespaceGetOptions<"arrayBuffer">): Promise; + get(key: Key, options?: KVNamespaceGetOptions<"stream">): Promise; + get(key: Array, type: "text"): Promise>; + get(key: Array, type: "json"): Promise>; + get(key: Array, options?: Partial>): Promise>; + get(key: Array, options?: KVNamespaceGetOptions<"text">): Promise>; + get(key: Array, options?: KVNamespaceGetOptions<"json">): Promise>; + list(options?: KVNamespaceListOptions): Promise>; + put(key: Key, value: string | ArrayBuffer | ArrayBufferView | ReadableStream, options?: KVNamespacePutOptions): Promise; + getWithMetadata(key: Key, options?: Partial>): Promise>; + getWithMetadata(key: Key, type: "text"): Promise>; + getWithMetadata(key: Key, type: "json"): Promise>; + getWithMetadata(key: Key, type: "arrayBuffer"): Promise>; + getWithMetadata(key: Key, type: "stream"): Promise>; + getWithMetadata(key: Key, options: KVNamespaceGetOptions<"text">): Promise>; + getWithMetadata(key: Key, options: KVNamespaceGetOptions<"json">): Promise>; + getWithMetadata(key: Key, options: KVNamespaceGetOptions<"arrayBuffer">): Promise>; + getWithMetadata(key: Key, options: KVNamespaceGetOptions<"stream">): Promise>; + getWithMetadata(key: Array, type: "text"): Promise>>; + getWithMetadata(key: Array, type: "json"): Promise>>; + getWithMetadata(key: Array, options?: Partial>): Promise>>; + getWithMetadata(key: Array, options?: KVNamespaceGetOptions<"text">): Promise>>; + getWithMetadata(key: Array, options?: KVNamespaceGetOptions<"json">): Promise>>; + delete(key: Key): Promise; +} +interface KVNamespaceListOptions { + limit?: number; + prefix?: (string | null); + cursor?: (string | null); +} +interface KVNamespaceGetOptions { + type: Type; + cacheTtl?: number; +} +interface KVNamespacePutOptions { + expiration?: number; + expirationTtl?: number; + metadata?: (any | null); +} +interface KVNamespaceGetWithMetadataResult { + value: Value | null; + metadata: Metadata | null; + cacheStatus: string | null; +} +type QueueContentType = "text" | "bytes" | "json" | "v8"; +interface Queue { + metrics(): Promise; + send(message: Body, options?: QueueSendOptions): Promise; + sendBatch(messages: Iterable>, options?: QueueSendBatchOptions): Promise; +} +interface QueueSendMetrics { + backlogCount: number; + backlogBytes: number; + oldestMessageTimestamp?: Date; +} +interface QueueSendMetadata { + metrics: QueueSendMetrics; +} +interface QueueSendResponse { + metadata: QueueSendMetadata; +} +interface QueueSendBatchMetrics { + backlogCount: number; + backlogBytes: number; + oldestMessageTimestamp?: Date; +} +interface QueueSendBatchMetadata { + metrics: QueueSendBatchMetrics; +} +interface QueueSendBatchResponse { + metadata: QueueSendBatchMetadata; +} +interface QueueSendOptions { + contentType?: QueueContentType; + delaySeconds?: number; +} +interface QueueSendBatchOptions { + delaySeconds?: number; +} +interface MessageSendRequest { + body: Body; + contentType?: QueueContentType; + delaySeconds?: number; +} +interface QueueMetrics { + backlogCount: number; + backlogBytes: number; + oldestMessageTimestamp?: Date; +} +interface MessageBatchMetrics { + backlogCount: number; + backlogBytes: number; + oldestMessageTimestamp?: Date; +} +interface MessageBatchMetadata { + metrics: MessageBatchMetrics; +} +interface QueueRetryOptions { + delaySeconds?: number; +} +interface Message { + readonly id: string; + readonly timestamp: Date; + readonly body: Body; + readonly attempts: number; + retry(options?: QueueRetryOptions): void; + ack(): void; +} +interface QueueEvent extends ExtendableEvent { + readonly messages: readonly Message[]; + readonly queue: string; + readonly metadata: MessageBatchMetadata; + retryAll(options?: QueueRetryOptions): void; + ackAll(): void; +} +interface MessageBatch { + readonly messages: readonly Message[]; + readonly queue: string; + readonly metadata: MessageBatchMetadata; + retryAll(options?: QueueRetryOptions): void; + ackAll(): void; +} +interface R2Error extends Error { + readonly name: string; + readonly code: number; + readonly message: string; + readonly action: string; + readonly stack: any; +} +interface R2ListOptions { + limit?: number; + prefix?: string; + cursor?: string; + delimiter?: string; + startAfter?: string; + include?: ("httpMetadata" | "customMetadata")[]; +} +interface R2Bucket { + head(key: string): Promise; + get(key: string, options: R2GetOptions & { + onlyIf: R2Conditional | Headers; + }): Promise; + get(key: string, options?: R2GetOptions): Promise; + put(key: string, value: ReadableStream | ArrayBuffer | ArrayBufferView | string | null | Blob, options?: R2PutOptions & { + onlyIf: R2Conditional | Headers; + }): Promise; + put(key: string, value: ReadableStream | ArrayBuffer | ArrayBufferView | string | null | Blob, options?: R2PutOptions): Promise; + createMultipartUpload(key: string, options?: R2MultipartOptions): Promise; + resumeMultipartUpload(key: string, uploadId: string): R2MultipartUpload; + delete(keys: string | string[]): Promise; + list(options?: R2ListOptions): Promise; +} +interface R2MultipartUpload { + readonly key: string; + readonly uploadId: string; + uploadPart(partNumber: number, value: ReadableStream | (ArrayBuffer | ArrayBufferView) | string | Blob, options?: R2UploadPartOptions): Promise; + abort(): Promise; + complete(uploadedParts: R2UploadedPart[]): Promise; +} +interface R2UploadedPart { + partNumber: number; + etag: string; +} +declare abstract class R2Object { + readonly key: string; + readonly version: string; + readonly size: number; + readonly etag: string; + readonly httpEtag: string; + readonly checksums: R2Checksums; + readonly uploaded: Date; + readonly httpMetadata?: R2HTTPMetadata; + readonly customMetadata?: Record; + readonly range?: R2Range; + readonly storageClass: string; + readonly ssecKeyMd5?: string; + writeHttpMetadata(headers: Headers): void; +} +interface R2ObjectBody extends R2Object { + get body(): ReadableStream; + get bodyUsed(): boolean; + arrayBuffer(): Promise; + bytes(): Promise; + text(): Promise; + json(): Promise; + blob(): Promise; +} +type R2Range = { + offset: number; + length?: number; +} | { + offset?: number; + length: number; +} | { + suffix: number; +}; +interface R2Conditional { + etagMatches?: string; + etagDoesNotMatch?: string; + uploadedBefore?: Date; + uploadedAfter?: Date; + secondsGranularity?: boolean; +} +interface R2GetOptions { + onlyIf?: (R2Conditional | Headers); + range?: (R2Range | Headers); + ssecKey?: (ArrayBuffer | string); +} +interface R2PutOptions { + onlyIf?: (R2Conditional | Headers); + httpMetadata?: (R2HTTPMetadata | Headers); + customMetadata?: Record; + md5?: ((ArrayBuffer | ArrayBufferView) | string); + sha1?: ((ArrayBuffer | ArrayBufferView) | string); + sha256?: ((ArrayBuffer | ArrayBufferView) | string); + sha384?: ((ArrayBuffer | ArrayBufferView) | string); + sha512?: ((ArrayBuffer | ArrayBufferView) | string); + storageClass?: string; + ssecKey?: (ArrayBuffer | string); +} +interface R2MultipartOptions { + httpMetadata?: (R2HTTPMetadata | Headers); + customMetadata?: Record; + storageClass?: string; + ssecKey?: (ArrayBuffer | string); +} +interface R2Checksums { + readonly md5?: ArrayBuffer; + readonly sha1?: ArrayBuffer; + readonly sha256?: ArrayBuffer; + readonly sha384?: ArrayBuffer; + readonly sha512?: ArrayBuffer; + toJSON(): R2StringChecksums; +} +interface R2StringChecksums { + md5?: string; + sha1?: string; + sha256?: string; + sha384?: string; + sha512?: string; +} +interface R2HTTPMetadata { + contentType?: string; + contentLanguage?: string; + contentDisposition?: string; + contentEncoding?: string; + cacheControl?: string; + cacheExpiry?: Date; +} +type R2Objects = { + objects: R2Object[]; + delimitedPrefixes: string[]; +} & ({ + truncated: true; + cursor: string; +} | { + truncated: false; +}); +interface R2UploadPartOptions { + ssecKey?: (ArrayBuffer | string); +} +declare abstract class ScheduledEvent extends ExtendableEvent { + readonly scheduledTime: number; + readonly cron: string; + noRetry(): void; +} +interface ScheduledController { + readonly scheduledTime: number; + readonly cron: string; + noRetry(): void; +} +interface QueuingStrategy { + highWaterMark?: (number | bigint); + size?: (chunk: T) => number | bigint; +} +interface UnderlyingSink { + type?: string; + start?: (controller: WritableStreamDefaultController) => void | Promise; + write?: (chunk: W, controller: WritableStreamDefaultController) => void | Promise; + abort?: (reason: any) => void | Promise; + close?: () => void | Promise; +} +interface UnderlyingByteSource { + type: "bytes"; + autoAllocateChunkSize?: number; + start?: (controller: ReadableByteStreamController) => void | Promise; + pull?: (controller: ReadableByteStreamController) => void | Promise; + cancel?: (reason: any) => void | Promise; +} +interface UnderlyingSource { + type?: "" | undefined; + start?: (controller: ReadableStreamDefaultController) => void | Promise; + pull?: (controller: ReadableStreamDefaultController) => void | Promise; + cancel?: (reason: any) => void | Promise; + expectedLength?: (number | bigint); +} +interface Transformer { + readableType?: string; + writableType?: string; + start?: (controller: TransformStreamDefaultController) => void | Promise; + transform?: (chunk: I, controller: TransformStreamDefaultController) => void | Promise; + flush?: (controller: TransformStreamDefaultController) => void | Promise; + cancel?: (reason: any) => void | Promise; + expectedLength?: number; +} +interface StreamPipeOptions { + preventAbort?: boolean; + preventCancel?: boolean; + /** + * Pipes this readable stream to a given writable stream destination. The way in which the piping process behaves under various error conditions can be customized with a number of passed options. It returns a promise that fulfills when the piping process completes successfully, or rejects if any errors were encountered. + * + * Piping a stream will lock it for the duration of the pipe, preventing any other consumer from acquiring a reader. + * + * Errors and closures of the source and destination streams propagate as follows: + * + * An error in this source readable stream will abort destination, unless preventAbort is truthy. The returned promise will be rejected with the source's error, or with any error that occurs during aborting the destination. + * + * An error in destination will cancel this source readable stream, unless preventCancel is truthy. The returned promise will be rejected with the destination's error, or with any error that occurs during canceling the source. + * + * When this source readable stream closes, destination will be closed, unless preventClose is truthy. The returned promise will be fulfilled once this process completes, unless an error is encountered while closing the destination, in which case it will be rejected with that error. + * + * If destination starts out closed or closing, this source readable stream will be canceled, unless preventCancel is true. The returned promise will be rejected with an error indicating piping to a closed stream failed, or with any error that occurs during canceling the source. + * + * The signal option can be set to an AbortSignal to allow aborting an ongoing pipe operation via the corresponding AbortController. In this case, this source readable stream will be canceled, and destination aborted, unless the respective options preventCancel or preventAbort are set. + */ + preventClose?: boolean; + signal?: AbortSignal; +} +type ReadableStreamReadResult = { + done: false; + value: R; +} | { + done: true; + value?: undefined; +}; +/** + * The `ReadableStream` interface of the Streams API represents a readable stream of byte data. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/ReadableStream) + */ +interface ReadableStream { + /** + * The **`locked`** read-only property of the ReadableStream interface returns whether or not the readable stream is locked to a reader. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/ReadableStream/locked) + */ + get locked(): boolean; + /** + * The **`cancel()`** method of the ReadableStream interface returns a Promise that resolves when the stream is canceled. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/ReadableStream/cancel) + */ + cancel(reason?: any): Promise; + /** + * The **`getReader()`** method of the ReadableStream interface creates a reader and locks the stream to it. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/ReadableStream/getReader) + */ + getReader(): ReadableStreamDefaultReader; + /** + * The **`getReader()`** method of the ReadableStream interface creates a reader and locks the stream to it. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/ReadableStream/getReader) + */ + getReader(options: ReadableStreamGetReaderOptions): ReadableStreamBYOBReader; + /** + * The **`pipeThrough()`** method of the ReadableStream interface provides a chainable way of piping the current stream through a transform stream or any other writable/readable pair. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/ReadableStream/pipeThrough) + */ + pipeThrough(transform: ReadableWritablePair, options?: StreamPipeOptions): ReadableStream; + /** + * The **`pipeTo()`** method of the ReadableStream interface pipes the current `ReadableStream` to a given WritableStream and returns a Promise that fulfills when the piping process completes successfully, or rejects if any errors were encountered. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/ReadableStream/pipeTo) + */ + pipeTo(destination: WritableStream, options?: StreamPipeOptions): Promise; + /** + * The **`tee()`** method of the two-element array containing the two resulting branches as new ReadableStream instances. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/ReadableStream/tee) + */ + tee(): [ + ReadableStream, + ReadableStream + ]; + values(options?: ReadableStreamValuesOptions): AsyncIterableIterator; + [Symbol.asyncIterator](options?: ReadableStreamValuesOptions): AsyncIterableIterator; +} +/** + * The `ReadableStream` interface of the Streams API represents a readable stream of byte data. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/ReadableStream) + */ +declare const ReadableStream: { + prototype: ReadableStream; + new (underlyingSource: UnderlyingByteSource, strategy?: QueuingStrategy): ReadableStream; + new (underlyingSource?: UnderlyingSource, strategy?: QueuingStrategy): ReadableStream; +}; +/** + * The **`ReadableStreamDefaultReader`** interface of the Streams API represents a default reader that can be used to read stream data supplied from a network (such as a fetch request). + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/ReadableStreamDefaultReader) + */ +declare class ReadableStreamDefaultReader { + constructor(stream: ReadableStream); + get closed(): Promise; + cancel(reason?: any): Promise; + /** + * The **`read()`** method of the ReadableStreamDefaultReader interface returns a Promise providing access to the next chunk in the stream's internal queue. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/ReadableStreamDefaultReader/read) + */ + read(): Promise>; + /** + * The **`releaseLock()`** method of the ReadableStreamDefaultReader interface releases the reader's lock on the stream. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/ReadableStreamDefaultReader/releaseLock) + */ + releaseLock(): void; +} +/** + * The `ReadableStreamBYOBReader` interface of the Streams API defines a reader for a ReadableStream that supports zero-copy reading from an underlying byte source. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/ReadableStreamBYOBReader) + */ +declare class ReadableStreamBYOBReader { + constructor(stream: ReadableStream); + get closed(): Promise; + cancel(reason?: any): Promise; + /** + * The **`read()`** method of the ReadableStreamBYOBReader interface is used to read data into a view on a user-supplied buffer from an associated readable byte stream. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/ReadableStreamBYOBReader/read) + */ + read(view: T): Promise>; + /** + * The **`releaseLock()`** method of the ReadableStreamBYOBReader interface releases the reader's lock on the stream. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/ReadableStreamBYOBReader/releaseLock) + */ + releaseLock(): void; + readAtLeast(minElements: number, view: T): Promise>; +} +interface ReadableStreamBYOBReaderReadableStreamBYOBReaderReadOptions { + min?: number; +} +interface ReadableStreamGetReaderOptions { + /** + * Creates a ReadableStreamBYOBReader and locks the stream to the new reader. + * + * This call behaves the same way as the no-argument variant, except that it only works on readable byte streams, i.e. streams which were constructed specifically with the ability to handle "bring your own buffer" reading. The returned BYOB reader provides the ability to directly read individual chunks from the stream via its read() method, into developer-supplied buffers, allowing more precise control over allocation. + */ + mode: "byob"; +} +/** + * The **`ReadableStreamBYOBRequest`** interface of the Streams API represents a 'pull request' for data from an underlying source that will made as a zero-copy transfer to a consumer (bypassing the stream's internal queues). + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/ReadableStreamBYOBRequest) + */ +declare abstract class ReadableStreamBYOBRequest { + /** + * The **`view`** getter property of the ReadableStreamBYOBRequest interface returns the current view. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/ReadableStreamBYOBRequest/view) + */ + get view(): Uint8Array | null; + /** + * The **`respond()`** method of the ReadableStreamBYOBRequest interface is used to signal to the associated readable byte stream that the specified number of bytes were written into the ReadableStreamBYOBRequest.view. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/ReadableStreamBYOBRequest/respond) + */ + respond(bytesWritten: number): void; + /** + * The **`respondWithNewView()`** method of the ReadableStreamBYOBRequest interface specifies a new view that the consumer of the associated readable byte stream should write to instead of ReadableStreamBYOBRequest.view. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/ReadableStreamBYOBRequest/respondWithNewView) + */ + respondWithNewView(view: ArrayBuffer | ArrayBufferView): void; + get atLeast(): number | null; +} +/** + * The **`ReadableStreamDefaultController`** interface of the Streams API represents a controller allowing control of a ReadableStream's state and internal queue. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/ReadableStreamDefaultController) + */ +declare abstract class ReadableStreamDefaultController { + /** + * The **`desiredSize`** read-only property of the required to fill the stream's internal queue. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/ReadableStreamDefaultController/desiredSize) + */ + get desiredSize(): number | null; + /** + * The **`close()`** method of the ReadableStreamDefaultController interface closes the associated stream. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/ReadableStreamDefaultController/close) + */ + close(): void; + /** + * The **`enqueue()`** method of the ```js-nolint enqueue(chunk) ``` - `chunk` - : The chunk to enqueue. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/ReadableStreamDefaultController/enqueue) + */ + enqueue(chunk?: R): void; + /** + * The **`error()`** method of the with the associated stream to error. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/ReadableStreamDefaultController/error) + */ + error(reason: any): void; +} +/** + * The **`ReadableByteStreamController`** interface of the Streams API represents a controller for a readable byte stream. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/ReadableByteStreamController) + */ +declare abstract class ReadableByteStreamController { + /** + * The **`byobRequest`** read-only property of the ReadableByteStreamController interface returns the current BYOB request, or `null` if there are no pending requests. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/ReadableByteStreamController/byobRequest) + */ + get byobRequest(): ReadableStreamBYOBRequest | null; + /** + * The **`desiredSize`** read-only property of the ReadableByteStreamController interface returns the number of bytes required to fill the stream's internal queue to its 'desired size'. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/ReadableByteStreamController/desiredSize) + */ + get desiredSize(): number | null; + /** + * The **`close()`** method of the ReadableByteStreamController interface closes the associated stream. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/ReadableByteStreamController/close) + */ + close(): void; + /** + * The **`enqueue()`** method of the ReadableByteStreamController interface enqueues a given chunk on the associated readable byte stream (the chunk is copied into the stream's internal queues). + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/ReadableByteStreamController/enqueue) + */ + enqueue(chunk: ArrayBuffer | ArrayBufferView): void; + /** + * The **`error()`** method of the ReadableByteStreamController interface causes any future interactions with the associated stream to error with the specified reason. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/ReadableByteStreamController/error) + */ + error(reason: any): void; +} +/** + * The **`WritableStreamDefaultController`** interface of the Streams API represents a controller allowing control of a WritableStream's state. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/WritableStreamDefaultController) + */ +declare abstract class WritableStreamDefaultController { + /** + * The read-only **`signal`** property of the WritableStreamDefaultController interface returns the AbortSignal associated with the controller. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/WritableStreamDefaultController/signal) + */ + get signal(): AbortSignal; + /** + * The **`error()`** method of the with the associated stream to error. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/WritableStreamDefaultController/error) + */ + error(reason?: any): void; +} +/** + * The **`TransformStreamDefaultController`** interface of the Streams API provides methods to manipulate the associated ReadableStream and WritableStream. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/TransformStreamDefaultController) + */ +declare abstract class TransformStreamDefaultController { + /** + * The **`desiredSize`** read-only property of the TransformStreamDefaultController interface returns the desired size to fill the queue of the associated ReadableStream. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/TransformStreamDefaultController/desiredSize) + */ + get desiredSize(): number | null; + /** + * The **`enqueue()`** method of the TransformStreamDefaultController interface enqueues the given chunk in the readable side of the stream. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/TransformStreamDefaultController/enqueue) + */ + enqueue(chunk?: O): void; + /** + * The **`error()`** method of the TransformStreamDefaultController interface errors both sides of the stream. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/TransformStreamDefaultController/error) + */ + error(reason: any): void; + /** + * The **`terminate()`** method of the TransformStreamDefaultController interface closes the readable side and errors the writable side of the stream. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/TransformStreamDefaultController/terminate) + */ + terminate(): void; +} +interface ReadableWritablePair { + readable: ReadableStream; + /** + * Provides a convenient, chainable way of piping this readable stream through a transform stream (or any other { writable, readable } pair). It simply pipes the stream into the writable side of the supplied pair, and returns the readable side for further use. + * + * Piping a stream will lock it for the duration of the pipe, preventing any other consumer from acquiring a reader. + */ + writable: WritableStream; +} +/** + * The **`WritableStream`** interface of the Streams API provides a standard abstraction for writing streaming data to a destination, known as a sink. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/WritableStream) + */ +declare class WritableStream { + constructor(underlyingSink?: UnderlyingSink, queuingStrategy?: QueuingStrategy); + /** + * The **`locked`** read-only property of the WritableStream interface returns a boolean indicating whether the `WritableStream` is locked to a writer. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/WritableStream/locked) + */ + get locked(): boolean; + /** + * The **`abort()`** method of the WritableStream interface aborts the stream, signaling that the producer can no longer successfully write to the stream and it is to be immediately moved to an error state, with any queued writes discarded. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/WritableStream/abort) + */ + abort(reason?: any): Promise; + /** + * The **`close()`** method of the WritableStream interface closes the associated stream. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/WritableStream/close) + */ + close(): Promise; + /** + * The **`getWriter()`** method of the WritableStream interface returns a new instance of WritableStreamDefaultWriter and locks the stream to that instance. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/WritableStream/getWriter) + */ + getWriter(): WritableStreamDefaultWriter; +} +/** + * The **`WritableStreamDefaultWriter`** interface of the Streams API is the object returned by WritableStream.getWriter() and once created locks the writer to the `WritableStream` ensuring that no other streams can write to the underlying sink. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/WritableStreamDefaultWriter) + */ +declare class WritableStreamDefaultWriter { + constructor(stream: WritableStream); + /** + * The **`closed`** read-only property of the the stream errors or the writer's lock is released. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/WritableStreamDefaultWriter/closed) + */ + get closed(): Promise; + /** + * The **`ready`** read-only property of the that resolves when the desired size of the stream's internal queue transitions from non-positive to positive, signaling that it is no longer applying backpressure. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/WritableStreamDefaultWriter/ready) + */ + get ready(): Promise; + /** + * The **`desiredSize`** read-only property of the to fill the stream's internal queue. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/WritableStreamDefaultWriter/desiredSize) + */ + get desiredSize(): number | null; + /** + * The **`abort()`** method of the the producer can no longer successfully write to the stream and it is to be immediately moved to an error state, with any queued writes discarded. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/WritableStreamDefaultWriter/abort) + */ + abort(reason?: any): Promise; + /** + * The **`close()`** method of the stream. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/WritableStreamDefaultWriter/close) + */ + close(): Promise; + /** + * The **`write()`** method of the operation. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/WritableStreamDefaultWriter/write) + */ + write(chunk?: W): Promise; + /** + * The **`releaseLock()`** method of the corresponding stream. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/WritableStreamDefaultWriter/releaseLock) + */ + releaseLock(): void; +} +/** + * The **`TransformStream`** interface of the Streams API represents a concrete implementation of the pipe chain _transform stream_ concept. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/TransformStream) + */ +declare class TransformStream { + constructor(transformer?: Transformer, writableStrategy?: QueuingStrategy, readableStrategy?: QueuingStrategy); + /** + * The **`readable`** read-only property of the TransformStream interface returns the ReadableStream instance controlled by this `TransformStream`. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/TransformStream/readable) + */ + get readable(): ReadableStream; + /** + * The **`writable`** read-only property of the TransformStream interface returns the WritableStream instance controlled by this `TransformStream`. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/TransformStream/writable) + */ + get writable(): WritableStream; +} +declare class FixedLengthStream extends IdentityTransformStream { + constructor(expectedLength: number | bigint, queuingStrategy?: IdentityTransformStreamQueuingStrategy); +} +declare class IdentityTransformStream extends TransformStream { + constructor(queuingStrategy?: IdentityTransformStreamQueuingStrategy); +} +interface IdentityTransformStreamQueuingStrategy { + highWaterMark?: (number | bigint); +} +interface ReadableStreamValuesOptions { + preventCancel?: boolean; +} +/** + * The **`CompressionStream`** interface of the Compression Streams API is an API for compressing a stream of data. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/CompressionStream) + */ +declare class CompressionStream extends TransformStream { + constructor(format: "gzip" | "deflate" | "deflate-raw"); +} +/** + * The **`DecompressionStream`** interface of the Compression Streams API is an API for decompressing a stream of data. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/DecompressionStream) + */ +declare class DecompressionStream extends TransformStream { + constructor(format: "gzip" | "deflate" | "deflate-raw"); +} +/** + * The **`TextEncoderStream`** interface of the Encoding API converts a stream of strings into bytes in the UTF-8 encoding. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/TextEncoderStream) + */ +declare class TextEncoderStream extends TransformStream { + constructor(); + get encoding(): string; +} +/** + * The **`TextDecoderStream`** interface of the Encoding API converts a stream of text in a binary encoding, such as UTF-8 etc., to a stream of strings. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/TextDecoderStream) + */ +declare class TextDecoderStream extends TransformStream { + constructor(label?: string, options?: TextDecoderStreamTextDecoderStreamInit); + get encoding(): string; + get fatal(): boolean; + get ignoreBOM(): boolean; +} +interface TextDecoderStreamTextDecoderStreamInit { + fatal?: boolean; + ignoreBOM?: boolean; +} +/** + * The **`ByteLengthQueuingStrategy`** interface of the Streams API provides a built-in byte length queuing strategy that can be used when constructing streams. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/ByteLengthQueuingStrategy) + */ +declare class ByteLengthQueuingStrategy implements QueuingStrategy { + constructor(init: QueuingStrategyInit); + /** + * The read-only **`ByteLengthQueuingStrategy.highWaterMark`** property returns the total number of bytes that can be contained in the internal queue before backpressure is applied. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/ByteLengthQueuingStrategy/highWaterMark) + */ + get highWaterMark(): number; + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/ByteLengthQueuingStrategy/size) */ + get size(): (chunk?: any) => number; +} +/** + * The **`CountQueuingStrategy`** interface of the Streams API provides a built-in chunk counting queuing strategy that can be used when constructing streams. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/CountQueuingStrategy) + */ +declare class CountQueuingStrategy implements QueuingStrategy { + constructor(init: QueuingStrategyInit); + /** + * The read-only **`CountQueuingStrategy.highWaterMark`** property returns the total number of chunks that can be contained in the internal queue before backpressure is applied. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/CountQueuingStrategy/highWaterMark) + */ + get highWaterMark(): number; + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/CountQueuingStrategy/size) */ + get size(): (chunk?: any) => number; +} +interface QueuingStrategyInit { + /** + * Creates a new ByteLengthQueuingStrategy with the provided high water mark. + * + * Note that the provided high water mark will not be validated ahead of time. Instead, if it is negative, NaN, or not a number, the resulting ByteLengthQueuingStrategy will cause the corresponding stream constructor to throw. + */ + highWaterMark: number; +} +interface TracePreviewInfo { + id: string; + slug: string; + name: string; +} +interface ScriptVersion { + id?: string; + tag?: string; + message?: string; +} +declare abstract class TailEvent extends ExtendableEvent { + readonly events: TraceItem[]; + readonly traces: TraceItem[]; +} +interface TraceItem { + readonly event: (TraceItemFetchEventInfo | TraceItemJsRpcEventInfo | TraceItemConnectEventInfo | TraceItemScheduledEventInfo | TraceItemAlarmEventInfo | TraceItemQueueEventInfo | TraceItemEmailEventInfo | TraceItemTailEventInfo | TraceItemCustomEventInfo | TraceItemHibernatableWebSocketEventInfo) | null; + readonly eventTimestamp: number | null; + readonly logs: TraceLog[]; + readonly exceptions: TraceException[]; + readonly diagnosticsChannelEvents: TraceDiagnosticChannelEvent[]; + readonly scriptName: string | null; + readonly entrypoint?: string; + readonly scriptVersion?: ScriptVersion; + readonly dispatchNamespace?: string; + readonly scriptTags?: string[]; + readonly tailAttributes?: Record; + readonly preview?: TracePreviewInfo; + readonly durableObjectId?: string; + readonly outcome: string; + readonly executionModel: string; + readonly truncated: boolean; + readonly cpuTime: number; + readonly wallTime: number; +} +interface TraceItemAlarmEventInfo { + readonly scheduledTime: Date; +} +interface TraceItemConnectEventInfo { +} +interface TraceItemCustomEventInfo { +} +interface TraceItemScheduledEventInfo { + readonly scheduledTime: number; + readonly cron: string; +} +interface TraceItemQueueEventInfo { + readonly queue: string; + readonly batchSize: number; +} +interface TraceItemEmailEventInfo { + readonly mailFrom: string; + readonly rcptTo: string; + readonly rawSize: number; +} +interface TraceItemTailEventInfo { + readonly consumedEvents: TraceItemTailEventInfoTailItem[]; +} +interface TraceItemTailEventInfoTailItem { + readonly scriptName: string | null; +} +interface TraceItemFetchEventInfo { + readonly response?: TraceItemFetchEventInfoResponse; + readonly request: TraceItemFetchEventInfoRequest; +} +interface TraceItemFetchEventInfoRequest { + readonly cf?: any; + readonly headers: Record; + readonly method: string; + readonly url: string; + getUnredacted(): TraceItemFetchEventInfoRequest; +} +interface TraceItemFetchEventInfoResponse { + readonly status: number; +} +interface TraceItemJsRpcEventInfo { + readonly rpcMethod: string; +} +interface TraceItemHibernatableWebSocketEventInfo { + readonly getWebSocketEvent: TraceItemHibernatableWebSocketEventInfoMessage | TraceItemHibernatableWebSocketEventInfoClose | TraceItemHibernatableWebSocketEventInfoError; +} +interface TraceItemHibernatableWebSocketEventInfoMessage { + readonly webSocketEventType: string; +} +interface TraceItemHibernatableWebSocketEventInfoClose { + readonly webSocketEventType: string; + readonly code: number; + readonly wasClean: boolean; +} +interface TraceItemHibernatableWebSocketEventInfoError { + readonly webSocketEventType: string; +} +interface TraceLog { + readonly timestamp: number; + readonly level: string; + readonly message: any; +} +interface TraceException { + readonly timestamp: number; + readonly message: string; + readonly name: string; + readonly stack?: string; +} +interface TraceDiagnosticChannelEvent { + readonly timestamp: number; + readonly channel: string; + readonly message: any; +} +interface TraceMetrics { + readonly cpuTime: number; + readonly wallTime: number; +} +interface UnsafeTraceMetrics { + fromTrace(item: TraceItem): TraceMetrics; +} +/** + * The **`URL`** interface is used to parse, construct, normalize, and encode URL. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/URL) + */ +declare class URL { + constructor(url: string | URL, base?: string | URL); + /** + * The **`origin`** read-only property of the URL interface returns a string containing the Unicode serialization of the origin of the represented URL. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/URL/origin) + */ + get origin(): string; + /** + * The **`href`** property of the URL interface is a string containing the whole URL. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/URL/href) + */ + get href(): string; + /** + * The **`href`** property of the URL interface is a string containing the whole URL. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/URL/href) + */ + set href(value: string); + /** + * The **`protocol`** property of the URL interface is a string containing the protocol or scheme of the URL, including the final `':'`. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/URL/protocol) + */ + get protocol(): string; + /** + * The **`protocol`** property of the URL interface is a string containing the protocol or scheme of the URL, including the final `':'`. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/URL/protocol) + */ + set protocol(value: string); + /** + * The **`username`** property of the URL interface is a string containing the username component of the URL. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/URL/username) + */ + get username(): string; + /** + * The **`username`** property of the URL interface is a string containing the username component of the URL. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/URL/username) + */ + set username(value: string); + /** + * The **`password`** property of the URL interface is a string containing the password component of the URL. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/URL/password) + */ + get password(): string; + /** + * The **`password`** property of the URL interface is a string containing the password component of the URL. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/URL/password) + */ + set password(value: string); + /** + * The **`host`** property of the URL interface is a string containing the host, which is the URL.hostname, and then, if the port of the URL is nonempty, a `':'`, followed by the URL.port of the URL. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/URL/host) + */ + get host(): string; + /** + * The **`host`** property of the URL interface is a string containing the host, which is the URL.hostname, and then, if the port of the URL is nonempty, a `':'`, followed by the URL.port of the URL. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/URL/host) + */ + set host(value: string); + /** + * The **`hostname`** property of the URL interface is a string containing either the domain name or IP address of the URL. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/URL/hostname) + */ + get hostname(): string; + /** + * The **`hostname`** property of the URL interface is a string containing either the domain name or IP address of the URL. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/URL/hostname) + */ + set hostname(value: string); + /** + * The **`port`** property of the URL interface is a string containing the port number of the URL. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/URL/port) + */ + get port(): string; + /** + * The **`port`** property of the URL interface is a string containing the port number of the URL. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/URL/port) + */ + set port(value: string); + /** + * The **`pathname`** property of the URL interface represents a location in a hierarchical structure. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/URL/pathname) + */ + get pathname(): string; + /** + * The **`pathname`** property of the URL interface represents a location in a hierarchical structure. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/URL/pathname) + */ + set pathname(value: string); + /** + * The **`search`** property of the URL interface is a search string, also called a _query string_, that is a string containing a `'?'` followed by the parameters of the URL. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/URL/search) + */ + get search(): string; + /** + * The **`search`** property of the URL interface is a search string, also called a _query string_, that is a string containing a `'?'` followed by the parameters of the URL. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/URL/search) + */ + set search(value: string); + /** + * The **`hash`** property of the URL interface is a string containing a `'#'` followed by the fragment identifier of the URL. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/URL/hash) + */ + get hash(): string; + /** + * The **`hash`** property of the URL interface is a string containing a `'#'` followed by the fragment identifier of the URL. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/URL/hash) + */ + set hash(value: string); + /** + * The **`searchParams`** read-only property of the access to the [MISSING: httpmethod('GET')] decoded query arguments contained in the URL. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/URL/searchParams) + */ + get searchParams(): URLSearchParams; + /** + * The **`toJSON()`** method of the URL interface returns a string containing a serialized version of the URL, although in practice it seems to have the same effect as ```js-nolint toJSON() ``` None. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/URL/toJSON) + */ + toJSON(): string; + /*function toString() { [native code] }*/ + toString(): string; + /** + * The **`URL.canParse()`** static method of the URL interface returns a boolean indicating whether or not an absolute URL, or a relative URL combined with a base URL, are parsable and valid. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/URL/canParse_static) + */ + static canParse(url: string, base?: string): boolean; + /** + * The **`URL.parse()`** static method of the URL interface returns a newly created URL object representing the URL defined by the parameters. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/URL/parse_static) + */ + static parse(url: string, base?: string): URL | null; + /** + * The **`createObjectURL()`** static method of the URL interface creates a string containing a URL representing the object given in the parameter. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/URL/createObjectURL_static) + */ + static createObjectURL(object: File | Blob): string; + /** + * The **`revokeObjectURL()`** static method of the URL interface releases an existing object URL which was previously created by calling Call this method when you've finished using an object URL to let the browser know not to keep the reference to the file any longer. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/URL/revokeObjectURL_static) + */ + static revokeObjectURL(object_url: string): void; +} +/** + * The **`URLSearchParams`** interface defines utility methods to work with the query string of a URL. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/URLSearchParams) + */ +declare class URLSearchParams { + constructor(init?: (Iterable> | Record | string)); + /** + * The **`size`** read-only property of the URLSearchParams interface indicates the total number of search parameter entries. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/URLSearchParams/size) + */ + get size(): number; + /** + * The **`append()`** method of the URLSearchParams interface appends a specified key/value pair as a new search parameter. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/URLSearchParams/append) + */ + append(name: string, value: string): void; + /** + * The **`delete()`** method of the URLSearchParams interface deletes specified parameters and their associated value(s) from the list of all search parameters. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/URLSearchParams/delete) + */ + delete(name: string, value?: string): void; + /** + * The **`get()`** method of the URLSearchParams interface returns the first value associated to the given search parameter. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/URLSearchParams/get) + */ + get(name: string): string | null; + /** + * The **`getAll()`** method of the URLSearchParams interface returns all the values associated with a given search parameter as an array. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/URLSearchParams/getAll) + */ + getAll(name: string): string[]; + /** + * The **`has()`** method of the URLSearchParams interface returns a boolean value that indicates whether the specified parameter is in the search parameters. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/URLSearchParams/has) + */ + has(name: string, value?: string): boolean; + /** + * The **`set()`** method of the URLSearchParams interface sets the value associated with a given search parameter to the given value. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/URLSearchParams/set) + */ + set(name: string, value: string): void; + /** + * The **`URLSearchParams.sort()`** method sorts all key/value pairs contained in this object in place and returns `undefined`. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/URLSearchParams/sort) + */ + sort(): void; + /* Returns an array of key, value pairs for every entry in the search params. */ + entries(): IterableIterator<[ + key: string, + value: string + ]>; + /* Returns a list of keys in the search params. */ + keys(): IterableIterator; + /* Returns a list of values in the search params. */ + values(): IterableIterator; + forEach(callback: (this: This, value: string, key: string, parent: URLSearchParams) => void, thisArg?: This): void; + /*function toString() { [native code] }*/ + toString(): string; + [Symbol.iterator](): IterableIterator<[ + key: string, + value: string + ]>; +} +declare class URLPattern { + constructor(input?: (string | URLPatternInit), baseURL?: (string | URLPatternOptions), patternOptions?: URLPatternOptions); + get protocol(): string; + get username(): string; + get password(): string; + get hostname(): string; + get port(): string; + get pathname(): string; + get search(): string; + get hash(): string; + get hasRegExpGroups(): boolean; + test(input?: (string | URLPatternInit), baseURL?: string): boolean; + exec(input?: (string | URLPatternInit), baseURL?: string): URLPatternResult | null; +} +interface URLPatternInit { + protocol?: string; + username?: string; + password?: string; + hostname?: string; + port?: string; + pathname?: string; + search?: string; + hash?: string; + baseURL?: string; +} +interface URLPatternComponentResult { + input: string; + groups: Record; +} +interface URLPatternResult { + inputs: (string | URLPatternInit)[]; + protocol: URLPatternComponentResult; + username: URLPatternComponentResult; + password: URLPatternComponentResult; + hostname: URLPatternComponentResult; + port: URLPatternComponentResult; + pathname: URLPatternComponentResult; + search: URLPatternComponentResult; + hash: URLPatternComponentResult; +} +interface URLPatternOptions { + ignoreCase?: boolean; +} +/** + * A `CloseEvent` is sent to clients using WebSockets when the connection is closed. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/CloseEvent) + */ +declare class CloseEvent extends Event { + constructor(type: string, initializer?: CloseEventInit); + /** + * The **`code`** read-only property of the CloseEvent interface returns a WebSocket connection close code indicating the reason the connection was closed. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/CloseEvent/code) + */ + readonly code: number; + /** + * The **`reason`** read-only property of the CloseEvent interface returns the WebSocket connection close reason the server gave for closing the connection; that is, a concise human-readable prose explanation for the closure. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/CloseEvent/reason) + */ + readonly reason: string; + /** + * The **`wasClean`** read-only property of the CloseEvent interface returns `true` if the connection closed cleanly. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/CloseEvent/wasClean) + */ + readonly wasClean: boolean; +} +interface CloseEventInit { + code?: number; + reason?: string; + wasClean?: boolean; +} +type WebSocketEventMap = { + close: CloseEvent; + message: MessageEvent; + open: Event; + error: ErrorEvent; +}; +/** + * The `WebSocket` object provides the API for creating and managing a WebSocket connection to a server, as well as for sending and receiving data on the connection. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/WebSocket) + */ +declare var WebSocket: { + prototype: WebSocket; + new (url: string, protocols?: (string[] | string)): WebSocket; + readonly READY_STATE_CONNECTING: number; + readonly CONNECTING: number; + readonly READY_STATE_OPEN: number; + readonly OPEN: number; + readonly READY_STATE_CLOSING: number; + readonly CLOSING: number; + readonly READY_STATE_CLOSED: number; + readonly CLOSED: number; +}; +/** + * The `WebSocket` object provides the API for creating and managing a WebSocket connection to a server, as well as for sending and receiving data on the connection. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/WebSocket) + */ +interface WebSocket extends EventTarget { + accept(options?: WebSocketAcceptOptions): void; + /** + * The **`WebSocket.send()`** method enqueues the specified data to be transmitted to the server over the WebSocket connection, increasing the value of `bufferedAmount` by the number of bytes needed to contain the data. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/WebSocket/send) + */ + send(message: (ArrayBuffer | ArrayBufferView) | string): void; + /** + * The **`WebSocket.close()`** method closes the already `CLOSED`, this method does nothing. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/WebSocket/close) + */ + close(code?: number, reason?: string): void; + serializeAttachment(attachment: any): void; + deserializeAttachment(): any | null; + /** + * The **`WebSocket.readyState`** read-only property returns the current state of the WebSocket connection. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/WebSocket/readyState) + */ + readyState: number; + /** + * The **`WebSocket.url`** read-only property returns the absolute URL of the WebSocket as resolved by the constructor. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/WebSocket/url) + */ + url: string | null; + /** + * The **`WebSocket.protocol`** read-only property returns the name of the sub-protocol the server selected; this will be one of the strings specified in the `protocols` parameter when creating the WebSocket object, or the empty string if no connection is established. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/WebSocket/protocol) + */ + protocol: string | null; + /** + * The **`WebSocket.extensions`** read-only property returns the extensions selected by the server. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/WebSocket/extensions) + */ + extensions: string | null; + /** + * The **`WebSocket.binaryType`** property controls the type of binary data being received over the WebSocket connection. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/WebSocket/binaryType) + */ + binaryType: "blob" | "arraybuffer"; +} +interface WebSocketAcceptOptions { + /** + * When set to `true`, receiving a server-initiated WebSocket Close frame will not + * automatically send a reciprocal Close frame, leaving the connection in a half-open + * state. This is useful for proxying scenarios where you need to coordinate closing + * both sides independently. Defaults to `false` when the + * `no_web_socket_half_open_by_default` compatibility flag is enabled. + */ + allowHalfOpen?: boolean; +} +declare const WebSocketPair: { + new (): { + 0: WebSocket; + 1: WebSocket; + }; +}; +interface SqlStorage { + exec>(query: string, ...bindings: any[]): SqlStorageCursor; + get databaseSize(): number; + Cursor: typeof SqlStorageCursor; + Statement: typeof SqlStorageStatement; +} +declare abstract class SqlStorageStatement { +} +type SqlStorageValue = ArrayBuffer | string | number | null; +declare abstract class SqlStorageCursor> { + next(): { + done?: false; + value: T; + } | { + done: true; + value?: never; + }; + toArray(): T[]; + one(): T; + raw(): IterableIterator; + columnNames: string[]; + get rowsRead(): number; + get rowsWritten(): number; + [Symbol.iterator](): IterableIterator; +} +interface Socket { + get readable(): ReadableStream; + get writable(): WritableStream; + get closed(): Promise; + get opened(): Promise; + get upgraded(): boolean; + get secureTransport(): "on" | "off" | "starttls"; + close(): Promise; + startTls(options?: TlsOptions): Socket; +} +interface SocketOptions { + secureTransport?: string; + allowHalfOpen: boolean; + highWaterMark?: (number | bigint); +} +interface SocketAddress { + hostname: string; + port: number; +} +interface TlsOptions { + expectedServerHostname?: string; +} +interface SocketInfo { + remoteAddress?: string; + localAddress?: string; +} +/** + * The **`EventSource`** interface is web content's interface to server-sent events. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/EventSource) + */ +declare class EventSource extends EventTarget { + constructor(url: string, init?: EventSourceEventSourceInit); + /** + * The **`close()`** method of the EventSource interface closes the connection, if one is made, and sets the ```js-nolint close() ``` None. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/EventSource/close) + */ + close(): void; + /** + * The **`url`** read-only property of the URL of the source. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/EventSource/url) + */ + get url(): string; + /** + * The **`withCredentials`** read-only property of the the `EventSource` object was instantiated with CORS credentials set. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/EventSource/withCredentials) + */ + get withCredentials(): boolean; + /** + * The **`readyState`** read-only property of the connection. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/EventSource/readyState) + */ + get readyState(): number; + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/EventSource/open_event) */ + get onopen(): any | null; + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/EventSource/open_event) */ + set onopen(value: any | null); + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/EventSource/message_event) */ + get onmessage(): any | null; + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/EventSource/message_event) */ + set onmessage(value: any | null); + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/EventSource/error_event) */ + get onerror(): any | null; + /* [MDN Reference](https://developer.mozilla.org/docs/Web/API/EventSource/error_event) */ + set onerror(value: any | null); + static readonly CONNECTING: number; + static readonly OPEN: number; + static readonly CLOSED: number; + static from(stream: ReadableStream): EventSource; +} +interface EventSourceEventSourceInit { + withCredentials?: boolean; + fetcher?: Fetcher; +} +interface Container { + get running(): boolean; + start(options?: ContainerStartupOptions): void; + monitor(): Promise; + destroy(error?: any): Promise; + signal(signo: number): void; + getTcpPort(port: number): Fetcher; + setInactivityTimeout(durationMs: number | bigint): Promise; + interceptOutboundHttp(addr: string, binding: Fetcher): Promise; + interceptAllOutboundHttp(binding: Fetcher): Promise; + snapshotDirectory(options: ContainerDirectorySnapshotOptions): Promise; + snapshotContainer(options: ContainerSnapshotOptions): Promise; + interceptOutboundHttps(addr: string, binding: Fetcher): Promise; +} +interface ContainerDirectorySnapshot { + id: string; + size: number; + dir: string; + name?: string; +} +interface ContainerDirectorySnapshotOptions { + dir: string; + name?: string; +} +interface ContainerDirectorySnapshotRestoreParams { + snapshot: ContainerDirectorySnapshot; + mountPoint?: string; +} +interface ContainerSnapshot { + id: string; + size: number; + name?: string; +} +interface ContainerSnapshotOptions { + name?: string; +} +interface ContainerStartupOptions { + entrypoint?: string[]; + enableInternet: boolean; + env?: Record; + labels?: Record; + directorySnapshots?: ContainerDirectorySnapshotRestoreParams[]; + containerSnapshot?: ContainerSnapshot; +} +/** + * The **`MessagePort`** interface of the Channel Messaging API represents one of the two ports of a MessageChannel, allowing messages to be sent from one port and listening out for them arriving at the other. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/MessagePort) + */ +declare abstract class MessagePort extends EventTarget { + /** + * The **`postMessage()`** method of the transfers ownership of objects to other browsing contexts. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/MessagePort/postMessage) + */ + postMessage(data?: any, options?: (any[] | MessagePortPostMessageOptions)): void; + /** + * The **`close()`** method of the MessagePort interface disconnects the port, so it is no longer active. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/MessagePort/close) + */ + close(): void; + /** + * The **`start()`** method of the MessagePort interface starts the sending of messages queued on the port. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/MessagePort/start) + */ + start(): void; + get onmessage(): any | null; + set onmessage(value: any | null); +} +/** + * The **`MessageChannel`** interface of the Channel Messaging API allows us to create a new message channel and send data through it via its two MessagePort properties. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/MessageChannel) + */ +declare class MessageChannel { + constructor(); + /** + * The **`port1`** read-only property of the the port attached to the context that originated the channel. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/MessageChannel/port1) + */ + readonly port1: MessagePort; + /** + * The **`port2`** read-only property of the the port attached to the context at the other end of the channel, which the message is initially sent to. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/MessageChannel/port2) + */ + readonly port2: MessagePort; +} +interface MessagePortPostMessageOptions { + transfer?: any[]; +} +type LoopbackForExport Rpc.EntrypointBranded) | ExportedHandler | undefined = undefined> = T extends new (...args: any[]) => Rpc.WorkerEntrypointBranded ? LoopbackServiceStub> : T extends new (...args: any[]) => Rpc.DurableObjectBranded ? LoopbackDurableObjectClass> : T extends ExportedHandler ? LoopbackServiceStub : undefined; +type LoopbackServiceStub = Fetcher & (T extends CloudflareWorkersModule.WorkerEntrypoint ? (opts: { + props?: Props; +}) => Fetcher : (opts: { + props?: any; +}) => Fetcher); +type LoopbackDurableObjectClass = DurableObjectClass & (T extends CloudflareWorkersModule.DurableObject ? (opts: { + props?: Props; +}) => DurableObjectClass : (opts: { + props?: any; +}) => DurableObjectClass); +interface LoopbackDurableObjectNamespace extends DurableObjectNamespace { +} +interface LoopbackColoLocalActorNamespace extends ColoLocalActorNamespace { +} +interface SyncKvStorage { + get(key: string): T | undefined; + list(options?: SyncKvListOptions): Iterable<[ + string, + T + ]>; + put(key: string, value: T): void; + delete(key: string): boolean; +} +interface SyncKvListOptions { + start?: string; + startAfter?: string; + end?: string; + prefix?: string; + reverse?: boolean; + limit?: number; +} +interface WorkerStub { + getEntrypoint(name?: string, options?: WorkerStubEntrypointOptions): Fetcher; + getDurableObjectClass(name?: string, options?: WorkerStubEntrypointOptions): DurableObjectClass; +} +interface WorkerStubEntrypointOptions { + props?: any; + limits?: workerdResourceLimits; +} +interface WorkerLoader { + get(name: string | null, getCode: () => WorkerLoaderWorkerCode | Promise): WorkerStub; + load(code: WorkerLoaderWorkerCode): WorkerStub; +} +interface WorkerLoaderModule { + js?: string; + cjs?: string; + text?: string; + data?: ArrayBuffer; + json?: any; + py?: string; + wasm?: ArrayBuffer; +} +interface WorkerLoaderWorkerCode { + compatibilityDate: string; + compatibilityFlags?: string[]; + allowExperimental?: boolean; + limits?: workerdResourceLimits; + mainModule: string; + modules: Record; + env?: any; + globalOutbound?: (Fetcher | null); + tails?: Fetcher[]; + streamingTails?: Fetcher[]; +} +interface workerdResourceLimits { + cpuMs?: number; + subRequests?: number; +} +/** +* 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 abstract class Performance { + /* [Cloudflare Docs Reference](https://developers.cloudflare.com/workers/runtime-apis/performance/#performancetimeorigin) */ + get timeOrigin(): number; + /* [Cloudflare Docs Reference](https://developers.cloudflare.com/workers/runtime-apis/performance/#performancenow) */ + now(): number; + /** + * The **`toJSON()`** method of the Performance interface is a Serialization; it returns a JSON representation of the Performance object. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Performance/toJSON) + */ + toJSON(): object; +} +interface Tracing { + enterSpan(name: string, callback: (span: Span, ...args: A) => T, ...args: A): T; + Span: typeof Span; +} +declare abstract class Span { + get isTraced(): boolean; + setAttribute(key: string, value?: (boolean | number | string)): void; +} +// ============================================================================ +// Agent Memory +// +// Public type surface for user Workers binding to an Agent Memory namespace. +// ============================================================================ +/** Memory type — every memory is classified into exactly one. */ +type AgentMemoryMemoryType = "fact" | "event" | "instruction" | "task"; +/** Search intensity for recall. */ +type AgentMemoryThinkingLevel = "low" | "medium" | "high"; +/** Response verbosity for recall. */ +type AgentMemoryResponseLength = "short" | "medium" | "long"; +/** A conversation message passed to ingest(). */ +interface AgentMemoryMessage { + role: "system" | "user" | "assistant"; + content: string; + /** Optional message timestamp. */ + timestamp?: Date; +} +/** Raw memory content passed to remember(). */ +interface AgentMemoryIncomingMemory { + /** Raw memory content. The service classifies and summarizes automatically. */ + content: string; + /** Optional session identifier to associate with this memory. */ + sessionId?: string | null | undefined; +} +/** A stored memory returned from remember(), get(), and delete(). */ +interface AgentMemoryMemory { + /** Memory ID. */ + id: string; + /** Memory type. */ + type: AgentMemoryMemoryType; + /** Text summary. */ + summary: string; + /** Memory text. */ + content: string; + /** Session that created this memory. */ + sessionId: string | null; + /** Memory creation time. */ + createdAt: Date; + /** Memory last-update time. */ + updatedAt: Date; +} +/** Single entry in a list() response. Same shape as Memory minus full content. */ +type AgentMemoryMemoryListEntry = Omit; +/** A scored memory candidate in a recall result. */ +interface AgentMemoryScoredCandidate { + /** Candidate ID. */ + id: string; + /** Text summary. */ + summary: string; + /** Session that created this candidate, when known. */ + sessionId: string | null; + /** Relevance score (higher is better). Comparable only within a single query. */ + score: number; +} +/** Options for the ingest() method. */ +interface AgentMemoryIngestOptions { + /** Session identifier to associate with memories created during ingestion. */ + sessionId?: string | null | undefined; +} +/** Options for the getSummary() method. */ +interface AgentMemoryGetSummaryOptions { + /** Session identifier to retrieve session summary for. */ + sessionId?: string | null | undefined; +} +/** Response from the getSummary() method. */ +interface AgentMemoryGetSummaryResponse { + /** Markdown summary. */ + summary: string; +} +/** + * Options for the recall() method. + * + * `referenceDate` accepts a Date object, an ISO-8601 date string + * (YYYY-MM-DD), or a full ISO-8601 datetime string. When provided, this + * date is used as "today" for resolving relative time references + * ("how many days ago", "last week") instead of the server's wall-clock time. + */ +interface AgentMemoryRecallOptions { + /** Recall intensity: "low" (default), "medium", or "high". */ + thinkingLevel?: AgentMemoryThinkingLevel; + /** Response verbosity: "short", "medium" (default), or "long". */ + responseLength?: AgentMemoryResponseLength; + /** Temporal anchor for date arithmetic. */ + referenceDate?: Date | string; +} +/** Response from the recall() method. */ +interface AgentMemoryRecallResult { + /** Number of memories retrieved. */ + count: number; + /** LLM-generated answer synthesizing the matching memories. */ + answer: string; + /** Matching memories ranked by relevance. */ + candidates: AgentMemoryScoredCandidate[]; +} +/** + * Options for the list() method. + * + * `cursor` is the opaque continuation token returned by the previous page; + * pass it back unchanged to fetch the next page. `sessionId` and `type` + * are exact-match filters; combining them is allowed. + */ +interface AgentMemoryListMemoriesOptions { + /** Maximum number of memories to return. Default 20, max 500. */ + limit?: number; + /** Opaque cursor from a previous page. */ + cursor?: string; + /** Exact-match session filter. */ + sessionId?: string; + /** Exact-match memory-type filter. */ + type?: AgentMemoryMemoryType; +} +/** Response from the list() method. */ +interface AgentMemoryListMemoriesResult { + memories: AgentMemoryMemoryListEntry[]; + /** Continuation cursor; absent when this page exhausted the result set. */ + cursor?: string; +} +/** + * A single Agent Memory profile, scoped to a profile name. + * + * Returned by {@link AgentMemoryNamespace.getProfile}. + */ +declare abstract class AgentMemoryProfile { + /** + * Retrieve a memory by ID. + * + * @param memoryId - ULID of the memory to retrieve. + * @throws if the memory does not exist. + */ + get(memoryId: string): Promise; + /** + * Delete a memory by ID. + * + * Removes the memory and any source messages linked by the memory's + * source message IDs. + * + * @param memoryId - ULID of the memory to delete. + * @throws if the memory does not exist. + */ + delete(memoryId: string): Promise; + /** + * Store a memory in this profile. The content is automatically classified, + * summarized, and indexed. + * + * @param memory - Raw memory content to persist. + */ + remember(memory: AgentMemoryIncomingMemory): Promise; + /** + * Extract memories from a conversation. + * + * @param messages - Conversation messages to extract memories from. + * @param options - Optional ingest options. + */ + ingest(messages: Iterable, options?: AgentMemoryIngestOptions): Promise; + /** + * Get a profile summary. + * + * @param options - Optional getSummary options. + */ + getSummary(options?: AgentMemoryGetSummaryOptions): Promise; + /** + * Recall memories in this profile. + * + * @param query - Recall query matched against memory content and keywords. + * @param options - Optional recall parameters. + * @returns Matching memories with relevance scores and a synthesized answer. + */ + recall(query: string, options?: AgentMemoryRecallOptions): Promise; + /** + * List active memories in this profile. + * + * Returns a paginated, filterable view of stored memories. Superseded + * versions are excluded. Use the returned `cursor` (when present) to + * fetch the next page. + * + * @param options - Optional pagination and filter options. + */ + list(options?: AgentMemoryListMemoriesOptions): Promise; + /** + * Soft-delete every memory and message in this profile that is tagged + * with `sessionId`. + * + * Idempotent: deleting a sessionId that has no rows is a no-op. + * + * @param sessionId - Session to delete. + */ + deleteSession(sessionId: string): Promise; +} +/** + * Namespace-level Agent Memory binding. + * + * Used as the type of an `env.MEMORY`-style binding backed by the Agent + * Memory product. + * + * @example + * ```ts + * export default { + * async fetch(_request: Request, env: Env): Promise { + * const profile = await env.MEMORY.getProfile("wrangler-e2e"); + * const summary = await profile.getSummary(); + * return Response.json(summary); + * }, + * }; + * ``` + */ +declare abstract class AgentMemoryNamespace { + /** + * Get a memory profile by name. Profiles are isolated by namespace and + * addressed by a compound key (namespaceId:profileName). + * + * @param profileName - Profile name (validated against naming rules). + * @returns RPC target for interacting with the profile. + */ + getProfile(profileName: string): Promise; + /** + * Soft-delete a profile and schedule deferred purge. Marks all + * memories and messages as deleted. + * + * @param profileName - Name of the profile to delete. + */ + deleteProfile(profileName: string): Promise; +} +// ============ AI Search Error Interfaces ============ +interface AiSearchInternalError extends Error { +} +interface AiSearchNotFoundError extends Error { +} +// ============ AI Search Common Types ============ +/** A single message in a conversation-style search or chat request. */ +type AiSearchMessage = { + role: 'system' | 'developer' | 'user' | 'assistant' | 'tool'; + content: string | null; +}; +/** + * Common shape for `ai_search_options` used by both single-instance and multi-instance requests. + * Contains retrieval, query rewrite, reranking, and cache sub-options. + */ +type AiSearchOptions = { + retrieval?: { + /** Which retrieval backend to use. Defaults to the instance's configured index_method. */ + retrieval_type?: 'vector' | 'keyword' | 'hybrid'; + /** Fusion method for combining vector + keyword results. */ + fusion_method?: 'max' | 'rrf'; + /** How keyword terms are combined: "and" = all terms must match, "or" = any term matches. */ + keyword_match_mode?: 'and' | 'or'; + /** Minimum similarity score (0-1) for a result to be included. Default 0.4. */ + match_threshold?: number; + /** Maximum number of results to return (1-50). Default 10. */ + max_num_results?: number; + /** Vectorize metadata filters applied to the search. */ + filters?: VectorizeVectorMetadataFilter; + /** Number of surrounding chunks to include for context (0-3). Default 0. */ + context_expansion?: number; + /** If true, return only item metadata without chunk text. */ + metadata_only?: boolean; + /** If true (default), return empty results on retrieval failure instead of throwing. */ + return_on_failure?: boolean; + /** Boost results by metadata field values. Max 3 entries. */ + boost_by?: Array<{ + field: string; + direction?: 'asc' | 'desc' | 'exists' | 'not_exists'; + }>; + [key: string]: unknown; + }; + query_rewrite?: { + enabled?: boolean; + model?: string; + rewrite_prompt?: string; + [key: string]: unknown; + }; + reranking?: { + enabled?: boolean; + model?: string; + /** Match threshold (0-1, default 0.4) */ + match_threshold?: number; + [key: string]: unknown; + }; + cache?: { + enabled?: boolean; + cache_threshold?: 'super_strict_match' | 'close_enough' | 'flexible_friend' | 'anything_goes'; + }; + [key: string]: unknown; +}; +// ============ AI Search Request Types ============ +/** + * Request body for single-instance search. + * Exactly one of `query` or `messages` must be provided. + */ +type AiSearchSearchRequest = { + /** Simple query string. */ + query: string; + messages?: never; + ai_search_options?: AiSearchOptions; +} | { + query?: never; + /** Conversation-style input. At least one user message with non-empty content is required. */ + messages: AiSearchMessage[]; + ai_search_options?: AiSearchOptions; +}; +type AiSearchChatCompletionsRequest = { + messages: AiSearchMessage[]; + model?: string; + stream?: boolean; + ai_search_options?: AiSearchOptions; + [key: string]: unknown; +}; +// ============ AI Search Multi-Instance Types (Namespace-Scoped) ============ +/** `ai_search_options` shape for multi-instance requests — requires `instance_ids`. */ +type AiSearchMultiSearchOptions = AiSearchOptions & { + /** Instance IDs to search across (1-10). */ + instance_ids: string[]; +}; +/** + * Request for searching across multiple instances within a namespace. + * `ai_search_options` is required and must include `instance_ids`. + * Exactly one of `query` or `messages` must be provided. + */ +type AiSearchMultiSearchRequest = { + /** Simple query string. */ + query: string; + messages?: never; + ai_search_options: AiSearchMultiSearchOptions; +} | { + query?: never; + /** Conversation-style input. */ + messages: AiSearchMessage[]; + ai_search_options: AiSearchMultiSearchOptions; +}; +/** A search result chunk tagged with the instance it originated from. */ +type AiSearchMultiSearchChunk = AiSearchSearchResponse['chunks'][number] & { + instance_id: string; +}; +/** Describes a per-instance error during a multi-instance operation. */ +type AiSearchMultiSearchError = { + instance_id: string; + message: string; +}; +/** Response from a multi-instance search, with chunks tagged by instance and optional partial-failure errors. */ +type AiSearchMultiSearchResponse = { + search_query: string; + chunks: AiSearchMultiSearchChunk[]; + errors?: AiSearchMultiSearchError[]; +}; +/** Request for chat completions across multiple instances within a namespace. `ai_search_options` is required and must include `instance_ids`. */ +type AiSearchMultiChatCompletionsRequest = Omit & { + ai_search_options: AiSearchMultiSearchOptions; +}; +/** Response from multi-instance chat completions, with chunks tagged by instance and optional partial-failure errors. */ +type AiSearchMultiChatCompletionsResponse = Omit & { + chunks: AiSearchMultiSearchChunk[]; + errors?: AiSearchMultiSearchError[]; +}; +// ============ AI Search Response Types ============ +type AiSearchSearchResponse = { + search_query: string; + chunks: Array<{ + id: string; + type: string; + /** Match score (0-1) */ + score: number; + text: string; + item: { + timestamp?: number; + key: string; + metadata?: Record; + }; + scoring_details?: { + /** Keyword match score (0-1) */ + keyword_score?: number; + /** Vector similarity score (0-1) */ + vector_score?: number; + /** Keyword rank position */ + keyword_rank?: number; + /** Vector rank position */ + vector_rank?: number; + /** Reranking model score */ + reranking_score?: number; + /** Fusion method used to combine results */ + fusion_method?: 'rrf' | 'max'; + [key: string]: unknown; + }; + }>; +}; +type AiSearchChatCompletionsResponse = { + id?: string; + object?: string; + model?: string; + choices: Array<{ + index?: number; + message: { + role: 'system' | 'developer' | 'user' | 'assistant' | 'tool'; + content: string | null; + [key: string]: unknown; + }; + [key: string]: unknown; + }>; + chunks: AiSearchSearchResponse['chunks']; + [key: string]: unknown; +}; +type AiSearchStatsResponse = { + queued?: number; + running?: number; + completed?: number; + error?: number; + skipped?: number; + outdated?: number; + last_activity?: string; + /** Storage engine statistics. */ + engine?: { + vectorize?: { + vectorsCount: number; + dimensions: number; + }; + r2?: { + payloadSizeBytes: number; + metadataSizeBytes: number; + objectCount: number; + }; + }; +}; +// ============ AI Search Instance Info Types ============ +type AiSearchInstanceInfo = { + id: string; + type?: 'r2' | 'web-crawler' | string; + source?: string; + source_params?: unknown; + paused?: boolean; + status?: string; + namespace?: string; + created_at?: string; + modified_at?: string; + token_id?: string; + ai_gateway_id?: string; + rewrite_query?: boolean; + reranking?: boolean; + embedding_model?: string; + ai_search_model?: string; + rewrite_model?: string; + reranking_model?: string; + /** @deprecated Use index_method instead. */ + hybrid_search_enabled?: boolean; + /** Controls which storage backends are active. */ + index_method?: { + vector?: boolean; + keyword?: boolean; + }; + /** Fusion method for combining vector and keyword results. */ + fusion_method?: 'max' | 'rrf'; + indexing_options?: { + keyword_tokenizer?: 'porter' | 'trigram'; + } | null; + retrieval_options?: { + keyword_match_mode?: 'and' | 'or'; + boost_by?: Array<{ + field: string; + direction?: 'asc' | 'desc' | 'exists' | 'not_exists'; + }>; + } | null; + chunk?: boolean; + chunk_size?: number; + chunk_overlap?: number; + score_threshold?: number; + max_num_results?: number; + cache?: boolean; + cache_threshold?: 'super_strict_match' | 'close_enough' | 'flexible_friend' | 'anything_goes'; + custom_metadata?: Array<{ + field_name: string; + data_type: 'text' | 'number' | 'boolean' | 'datetime'; + }>; + /** Sync interval in seconds. */ + sync_interval?: 3600 | 7200 | 14400 | 21600 | 43200 | 86400; + metadata?: Record; + [key: string]: unknown; +}; +/** Pagination, search, and ordering parameters for listing instances within a namespace. */ +type AiSearchListInstancesParams = { + page?: number; + per_page?: number; + /** Search instances by ID. */ + search?: string; + /** Field to sort by. */ + order_by?: 'created_at'; + /** Sort direction. */ + order_by_direction?: 'asc' | 'desc'; +}; +type AiSearchListResponse = { + result: AiSearchInstanceInfo[]; + result_info?: { + count: number; + page: number; + per_page: number; + total_count: number; + }; +}; +// ============ AI Search Config Types ============ +type AiSearchConfig = { + /** Instance ID (1-32 chars, pattern: ^[a-z0-9_]+(?:-[a-z0-9_]+)*$) */ + id: string; + /** Instance type. Omit to create with built-in storage. */ + type?: 'r2' | 'web-crawler' | string; + /** Source URL (required for web-crawler type). */ + source?: string; + source_params?: unknown; + /** Token ID (UUID format) */ + token_id?: string; + ai_gateway_id?: string; + /** Enable query rewriting (default false) */ + rewrite_query?: boolean; + /** Enable reranking (default false) */ + reranking?: boolean; + embedding_model?: string; + ai_search_model?: string; + rewrite_model?: string; + reranking_model?: string; + /** @deprecated Use index_method instead. */ + hybrid_search_enabled?: boolean; + /** Controls which storage backends are used during indexing. Defaults to vector-only. */ + index_method?: { + vector?: boolean; + keyword?: boolean; + }; + /** Fusion method for combining vector and keyword results. "rrf" = reciprocal rank fusion (default), "max" = maximum score. */ + fusion_method?: 'max' | 'rrf'; + indexing_options?: { + keyword_tokenizer?: 'porter' | 'trigram'; + } | null; + retrieval_options?: { + keyword_match_mode?: 'and' | 'or'; + boost_by?: Array<{ + field: string; + direction?: 'asc' | 'desc' | 'exists' | 'not_exists'; + }>; + } | null; + chunk?: boolean; + chunk_size?: number; + chunk_overlap?: number; + /** Minimum similarity score (0-1) for a result to be included. */ + score_threshold?: number; + max_num_results?: number; + cache?: boolean; + /** Similarity threshold for cache hits. Stricter = fewer cache hits but higher relevance. */ + cache_threshold?: 'super_strict_match' | 'close_enough' | 'flexible_friend' | 'anything_goes'; + custom_metadata?: Array<{ + field_name: string; + data_type: 'text' | 'number' | 'boolean' | 'datetime'; + }>; + namespace?: string; + /** Sync interval in seconds. 3600=1h, 7200=2h, 14400=4h, 21600=6h, 43200=12h, 86400=24h. */ + sync_interval?: 3600 | 7200 | 14400 | 21600 | 43200 | 86400; + metadata?: Record; + [key: string]: unknown; +}; +// ============ AI Search Item Types ============ +type AiSearchItemInfo = { + id: string; + key: string; + status: 'completed' | 'error' | 'skipped' | 'queued' | 'running' | 'outdated'; + next_action?: 'INDEX' | 'DELETE' | null; + error?: string; + checksum?: string; + namespace?: string; + chunks_count?: number | null; + file_size?: number | null; + source_id?: string | null; + last_seen_at?: string; + created_at?: string; + metadata?: Record; + [key: string]: unknown; +}; +type AiSearchItemContentResult = { + body: ReadableStream; + contentType: string; + filename: string; + size: number; +}; +type AiSearchUploadItemOptions = { + metadata?: Record; +}; +type AiSearchListItemsParams = { + page?: number; + per_page?: number; + /** Search items by key name. */ + search?: string; + /** Sort order for results. */ + sort_by?: 'status' | 'modified_at'; + /** Filter items by processing status. */ + status?: 'queued' | 'running' | 'completed' | 'error' | 'skipped' | 'outdated'; + /** Filter items by source (e.g. "builtin" or "web-crawler:https://example.com"). */ + source?: string; + /** JSON-encoded Vectorize filter for metadata filtering. */ + metadata_filter?: string; +}; +type AiSearchListItemsResponse = { + result: AiSearchItemInfo[]; + result_info?: { + count: number; + page: number; + per_page: number; + total_count: number; + }; +}; +// ============ AI Search Item Logs Types ============ +type AiSearchItemLogsParams = { + /** Maximum number of log entries to return (1-100, default 50). */ + limit?: number; + /** Opaque cursor for pagination. Pass the `cursor` value from a previous response. */ + cursor?: string; +}; +type AiSearchItemLog = { + timestamp: string; + action: string; + message: string; + fileKey?: string; + chunkCount?: number; + processingTimeMs?: number; + errorType?: string; +}; +/** Paginated response for item processing logs (cursor-based). */ +type AiSearchItemLogsResponse = { + result: AiSearchItemLog[]; + result_info: { + count: number; + per_page: number; + cursor: string | null; + truncated: boolean; + }; +}; +// ============ AI Search Item Chunks Types ============ +type AiSearchItemChunksParams = { + /** Maximum number of chunks to return (1-100, default 20). */ + limit?: number; + /** Offset into the chunks list (default 0). */ + offset?: number; +}; +/** A single indexed chunk belonging to an item, including its text content and byte range. */ +type AiSearchItemChunk = { + id: string; + text: string; + start_byte: number; + end_byte: number; + item?: { + timestamp?: number; + key: string; + metadata?: Record; + }; +}; +/** Paginated response for item chunks (offset-based). */ +type AiSearchItemChunksResponse = { + result: AiSearchItemChunk[]; + result_info: { + count: number; + total: number; + limit: number; + offset: number; + }; +}; +// ============ AI Search Job Types ============ +type AiSearchJobInfo = { + id: string; + source: 'user' | 'schedule'; + description?: string; + last_seen_at?: string; + started_at?: string; + ended_at?: string; + end_reason?: string; +}; +type AiSearchJobLog = { + id: number; + message: string; + message_type: number; + created_at: number; +}; +type AiSearchCreateJobParams = { + description?: string; +}; +type AiSearchListJobsParams = { + page?: number; + per_page?: number; +}; +type AiSearchListJobsResponse = { + result: AiSearchJobInfo[]; + result_info?: { + count: number; + page: number; + per_page: number; + total_count: number; + }; +}; +type AiSearchJobLogsParams = { + page?: number; + per_page?: number; +}; +type AiSearchJobLogsResponse = { + result: AiSearchJobLog[]; + result_info?: { + count: number; + page: number; + per_page: number; + total_count: number; + }; +}; +// ============ AI Search Sub-Service Classes ============ +/** + * Single item service for an AI Search instance. + * Provides info, download, sync, logs, and chunks operations on a specific item. + */ +declare abstract class AiSearchItem { + /** Get metadata about this item. */ + info(): Promise; + /** + * Download the item's content. + * @returns Object with body stream, content type, filename, and size. + */ + download(): Promise; + /** + * Trigger re-indexing of this item. + * @returns The updated item info. + */ + sync(): Promise; + /** + * Retrieve processing logs for this item (cursor-based pagination). + * @param params Optional pagination parameters (limit, cursor). + * @returns Paginated log entries for this item. + */ + logs(params?: AiSearchItemLogsParams): Promise; + /** + * List indexed chunks for this item (offset-based pagination). + * @param params Optional pagination parameters (limit, offset). + * @returns Paginated chunk entries for this item. + */ + chunks(params?: AiSearchItemChunksParams): Promise; +} +/** + * Items collection service for an AI Search instance. + * Provides list, upload, and access to individual items. + */ +declare abstract class AiSearchItems { + /** List items in this instance. */ + list(params?: AiSearchListItemsParams): Promise; + /** + * Upload a file as an item. Behaves as an upsert: if an item with the same + * filename already exists, it is overwritten and re-indexed. + * @param name Filename for the uploaded item. + * @param content File content as a ReadableStream, Blob, or string. + * @param options Optional metadata to attach to the item. + * @returns The created item info. + */ + upload(name: string, content: ReadableStream | Blob | string, options?: AiSearchUploadItemOptions): Promise; + /** + * Upload a file and poll until processing completes. + * Behaves as an upsert: if an item with the same filename already exists, + * it is overwritten and re-indexed. + * @param name Filename for the uploaded item. + * @param content File content as a ReadableStream, Blob, or string. + * @param options Optional metadata and polling configuration. + * @returns The item info after processing completes (or timeout). + */ + uploadAndPoll(name: string, content: ReadableStream | Blob | string, options?: AiSearchUploadItemOptions & { + /** Polling interval in milliseconds (default 1000). */ + pollIntervalMs?: number; + /** Maximum time to wait in milliseconds (default 30000). */ + timeoutMs?: number; + }): Promise; + /** + * Get an item by ID. + * @param itemId The item identifier. + * @returns Item service for info, download, sync, logs, and chunks operations. + */ + get(itemId: string): AiSearchItem; + /** + * Delete an item from the instance. + * @param itemId The item identifier. + */ + delete(itemId: string): Promise; +} +/** + * Single job service for an AI Search instance. + * Provides info, logs, and cancel operations for a specific job. + */ +declare abstract class AiSearchJob { + /** Get metadata about this job. */ + info(): Promise; + /** Get logs for this job. */ + logs(params?: AiSearchJobLogsParams): Promise; + /** + * Cancel a running job. + * @returns The updated job info. + * @throws AiSearchNotFoundError if the job does not exist. + */ + cancel(): Promise; +} +/** + * Jobs collection service for an AI Search instance. + * Provides list, create, and access to individual jobs. + */ +declare abstract class AiSearchJobs { + /** List jobs for this instance. */ + list(params?: AiSearchListJobsParams): Promise; + /** + * Create a new indexing job. + * @param params Optional job parameters. + * @returns The created job info. + */ + create(params?: AiSearchCreateJobParams): Promise; + /** + * Get a job by ID. + * @param jobId The job identifier. + * @returns Job service for info, logs, and cancel operations. + */ + get(jobId: string): AiSearchJob; +} +// ============ AI Search Binding Classes ============ +/** + * Instance-level AI Search service. + * + * Used as: + * - The return type of `AiSearchNamespace.get(name)` (namespace binding) + * - The type of `env.BLOG_SEARCH` (single instance binding via `ai_search`) + * + * Provides search, chat, update, stats, items, and jobs operations. + * + * @example + * ```ts + * // Via namespace binding + * const instance = env.AI_SEARCH.get("blog"); + * const results = await instance.search({ + * query: "How does caching work?", + * }); + * + * // Via single instance binding + * const results = await env.BLOG_SEARCH.search({ + * messages: [{ role: "user", content: "How does caching work?" }], + * }); + * ``` + */ +declare abstract class AiSearchInstance { + /** + * Search the AI Search instance for relevant chunks. + * @param params Search request with query or messages and optional AI search options. + * @returns Search response with matching chunks and search query. + */ + search(params: AiSearchSearchRequest): Promise; + /** + * Generate chat completions with AI Search context (streaming). + * @param params Chat completions request with stream: true. + * @returns ReadableStream of server-sent events. + */ + chatCompletions(params: AiSearchChatCompletionsRequest & { + stream: true; + }): Promise; + /** + * Generate chat completions with AI Search context. + * @param params Chat completions request. + * @returns Chat completion response with choices and RAG chunks. + */ + chatCompletions(params: AiSearchChatCompletionsRequest): Promise; + /** + * Update the instance configuration. + * @param config Partial configuration to update. + * @returns Updated instance info. + */ + update(config: Partial): Promise; + /** Get metadata about this instance. */ + info(): Promise; + /** + * Get instance statistics (item count, indexing status, etc.). + * @returns Statistics with counts per status, last activity time, and engine details. + */ + stats(): Promise; + /** Items collection — list, upload, and manage items in this instance. */ + get items(): AiSearchItems; + /** Jobs collection — list, create, and inspect indexing jobs. */ + get jobs(): AiSearchJobs; +} +/** + * Namespace-level AI Search service. + * + * Used as the type of `env.AI_SEARCH` (namespace binding via `ai_search_namespaces`). + * Scoped to a single namespace. Provides dynamic instance access, creation, deletion, + * and multi-instance search/chat operations. + * + * @example + * ```ts + * // Access an instance within the namespace + * const blog = env.AI_SEARCH.get("blog"); + * const results = await blog.search({ query: "How does caching work?" }); + * + * // List all instances in the namespace + * const instances = await env.AI_SEARCH.list(); + * + * // Create a new instance with built-in storage + * const tenant = await env.AI_SEARCH.create({ id: "tenant-123" }); + * + * // Upload items into the instance + * await tenant.items.upload("doc.pdf", fileContent); + * + * // Search across multiple instances + * const multi = await env.AI_SEARCH.search({ + * query: "caching", + * ai_search_options: { instance_ids: ["blog", "docs"] }, + * }); + * + * // Delete an instance + * await env.AI_SEARCH.delete("tenant-123"); + * ``` + */ +declare abstract class AiSearchNamespace { + /** + * Get an instance by name within the bound namespace. + * @param name Instance name. + * @returns Instance service for search, chat, update, stats, items, and jobs. + */ + get(name: string): AiSearchInstance; + /** + * List instances in the bound namespace. + * @param params Optional pagination, search, and ordering parameters. + * @returns Array of instance metadata with pagination info. + */ + list(params?: AiSearchListInstancesParams): Promise; + /** + * Create a new instance within the bound namespace. + * @param config Instance configuration. Only `id` is required — omit `type` and `source` to create with built-in storage. + * @returns Instance service for the newly created instance. + * + * @example + * ```ts + * // Create with built-in storage (upload items manually) + * const instance = await env.AI_SEARCH.create({ id: "my-search" }); + * + * // Create with web crawler source + * const instance = await env.AI_SEARCH.create({ + * id: "docs-search", + * type: "web-crawler", + * source: "https://developers.cloudflare.com", + * }); + * ``` + */ + create(config: AiSearchConfig): Promise; + /** + * Delete an instance from the bound namespace. + * @param name Instance name to delete. + */ + delete(name: string): Promise; + /** + * Search across multiple instances within the bound namespace. + * Fans out to the specified instance_ids and merges results. + * @param params Search request with required `ai_search_options.instance_ids`. + * @returns Search response with chunks tagged by instance_id and optional partial-failure errors. + */ + search(params: AiSearchMultiSearchRequest): Promise; + /** + * Generate chat completions across multiple instances within the bound namespace (streaming). + * Fans out to the specified instance_ids, merges context, and generates a response. + * @param params Chat completions request with stream: true and required `ai_search_options.instance_ids`. + * @returns ReadableStream of server-sent events. + */ + chatCompletions(params: AiSearchMultiChatCompletionsRequest & { + stream: true; + }): Promise; + /** + * Generate chat completions across multiple instances within the bound namespace. + * Fans out to the specified instance_ids, merges context, and generates a response. + * @param params Chat completions request with required `ai_search_options.instance_ids`. + * @returns Chat completion response with choices, chunks tagged by instance_id, and optional partial-failure errors. + */ + chatCompletions(params: AiSearchMultiChatCompletionsRequest): Promise; +} +type AiImageClassificationInput = { + image: number[]; +}; +type AiImageClassificationOutput = { + score?: number; + label?: string; +}[]; +declare abstract class BaseAiImageClassification { + inputs: AiImageClassificationInput; + postProcessedOutputs: AiImageClassificationOutput; +} +type AiImageToTextInput = { + image: number[]; + prompt?: string; + max_tokens?: number; + temperature?: number; + top_p?: number; + top_k?: number; + seed?: number; + repetition_penalty?: number; + frequency_penalty?: number; + presence_penalty?: number; + raw?: boolean; + messages?: RoleScopedChatInput[]; +}; +type AiImageToTextOutput = { + description: string; +}; +declare abstract class BaseAiImageToText { + inputs: AiImageToTextInput; + postProcessedOutputs: AiImageToTextOutput; +} +type AiImageTextToTextInput = { + image: string; + prompt?: string; + max_tokens?: number; + temperature?: number; + ignore_eos?: boolean; + top_p?: number; + top_k?: number; + seed?: number; + repetition_penalty?: number; + frequency_penalty?: number; + presence_penalty?: number; + raw?: boolean; + messages?: RoleScopedChatInput[]; +}; +type AiImageTextToTextOutput = { + description: string; +}; +declare abstract class BaseAiImageTextToText { + inputs: AiImageTextToTextInput; + postProcessedOutputs: AiImageTextToTextOutput; +} +type AiMultimodalEmbeddingsInput = { + image: string; + text: string[]; +}; +type AiIMultimodalEmbeddingsOutput = { + data: number[][]; + shape: number[]; +}; +declare abstract class BaseAiMultimodalEmbeddings { + inputs: AiImageTextToTextInput; + postProcessedOutputs: AiImageTextToTextOutput; +} +type AiObjectDetectionInput = { + image: number[]; +}; +type AiObjectDetectionOutput = { + score?: number; + label?: string; +}[]; +declare abstract class BaseAiObjectDetection { + inputs: AiObjectDetectionInput; + postProcessedOutputs: AiObjectDetectionOutput; +} +type AiSentenceSimilarityInput = { + source: string; + sentences: string[]; +}; +type AiSentenceSimilarityOutput = number[]; +declare abstract class BaseAiSentenceSimilarity { + inputs: AiSentenceSimilarityInput; + postProcessedOutputs: AiSentenceSimilarityOutput; +} +type AiAutomaticSpeechRecognitionInput = { + audio: number[]; +}; +type AiAutomaticSpeechRecognitionOutput = { + text?: string; + words?: { + word: string; + start: number; + end: number; + }[]; + vtt?: string; +}; +declare abstract class BaseAiAutomaticSpeechRecognition { + inputs: AiAutomaticSpeechRecognitionInput; + postProcessedOutputs: AiAutomaticSpeechRecognitionOutput; +} +type AiSummarizationInput = { + input_text: string; + max_length?: number; +}; +type AiSummarizationOutput = { + summary: string; +}; +declare abstract class BaseAiSummarization { + inputs: AiSummarizationInput; + postProcessedOutputs: AiSummarizationOutput; +} +type AiTextClassificationInput = { + text: string; +}; +type AiTextClassificationOutput = { + score?: number; + label?: string; +}[]; +declare abstract class BaseAiTextClassification { + inputs: AiTextClassificationInput; + postProcessedOutputs: AiTextClassificationOutput; +} +type AiTextEmbeddingsInput = { + text: string | string[]; +}; +type AiTextEmbeddingsOutput = { + shape: number[]; + data: number[][]; +}; +declare abstract class BaseAiTextEmbeddings { + inputs: AiTextEmbeddingsInput; + postProcessedOutputs: AiTextEmbeddingsOutput; +} +type RoleScopedChatInput = { + role: "user" | "assistant" | "system" | "tool" | (string & NonNullable); + content: string; + name?: string; +}; +type AiTextGenerationToolLegacyInput = { + name: string; + description: string; + parameters?: { + type: "object" | (string & NonNullable); + properties: { + [key: string]: { + type: string; + description?: string; + }; + }; + required: string[]; + }; +}; +type AiTextGenerationToolInput = { + type: "function" | (string & NonNullable); + function: { + name: string; + description: string; + parameters?: { + type: "object" | (string & NonNullable); + properties: { + [key: string]: { + type: string; + description?: string; + }; + }; + required: string[]; + }; + }; +}; +type AiTextGenerationFunctionsInput = { + name: string; + code: string; +}; +type AiTextGenerationResponseFormat = { + type: string; + json_schema?: any; +}; +type AiTextGenerationInput = { + prompt?: string; + raw?: boolean; + stream?: boolean; + max_tokens?: number; + temperature?: number; + top_p?: number; + top_k?: number; + seed?: number; + repetition_penalty?: number; + frequency_penalty?: number; + presence_penalty?: number; + messages?: RoleScopedChatInput[]; + response_format?: AiTextGenerationResponseFormat; + tools?: AiTextGenerationToolInput[] | AiTextGenerationToolLegacyInput[] | (object & NonNullable); + functions?: AiTextGenerationFunctionsInput[]; +}; +type AiTextGenerationToolLegacyOutput = { + name: string; + arguments: unknown; +}; +type AiTextGenerationToolOutput = { + id: string; + type: "function"; + function: { + name: string; + arguments: string; + }; +}; +type UsageTags = { + prompt_tokens: number; + completion_tokens: number; + total_tokens: number; +}; +type AiTextGenerationOutput = { + response?: string; + tool_calls?: AiTextGenerationToolLegacyOutput[] & AiTextGenerationToolOutput[]; + usage?: UsageTags; +}; +declare abstract class BaseAiTextGeneration { + inputs: AiTextGenerationInput; + postProcessedOutputs: AiTextGenerationOutput; +} +type AiTextToSpeechInput = { + prompt: string; + lang?: string; +}; +type AiTextToSpeechOutput = Uint8Array | { + audio: string; +}; +declare abstract class BaseAiTextToSpeech { + inputs: AiTextToSpeechInput; + postProcessedOutputs: AiTextToSpeechOutput; +} +type AiTextToImageInput = { + prompt: string; + negative_prompt?: string; + height?: number; + width?: number; + image?: number[]; + image_b64?: string; + mask?: number[]; + num_steps?: number; + strength?: number; + guidance?: number; + seed?: number; +}; +type AiTextToImageOutput = ReadableStream; +declare abstract class BaseAiTextToImage { + inputs: AiTextToImageInput; + postProcessedOutputs: AiTextToImageOutput; +} +type AiTranslationInput = { + text: string; + target_lang: string; + source_lang?: string; +}; +type AiTranslationOutput = { + translated_text?: string; +}; +declare abstract class BaseAiTranslation { + inputs: AiTranslationInput; + postProcessedOutputs: AiTranslationOutput; +} +/** + * Workers AI support for OpenAI's Chat Completions API + */ +type ChatCompletionContentPartText = { + type: "text"; + text: string; +}; +type ChatCompletionContentPartImage = { + type: "image_url"; + image_url: { + url: string; + detail?: "auto" | "low" | "high"; + }; +}; +type ChatCompletionContentPartInputAudio = { + type: "input_audio"; + input_audio: { + /** Base64 encoded audio data. */ + data: string; + format: "wav" | "mp3"; + }; +}; +type ChatCompletionContentPartFile = { + type: "file"; + file: { + /** Base64 encoded file data. */ + file_data?: string; + /** The ID of an uploaded file. */ + file_id?: string; + filename?: string; + }; +}; +type ChatCompletionContentPartRefusal = { + type: "refusal"; + refusal: string; +}; +type ChatCompletionContentPart = ChatCompletionContentPartText | ChatCompletionContentPartImage | ChatCompletionContentPartInputAudio | ChatCompletionContentPartFile; +type FunctionDefinition = { + name: string; + description?: string; + parameters?: Record; + strict?: boolean | null; +}; +type ChatCompletionFunctionTool = { + type: "function"; + function: FunctionDefinition; +}; +type ChatCompletionCustomToolGrammarFormat = { + type: "grammar"; + grammar: { + definition: string; + syntax: "lark" | "regex"; + }; +}; +type ChatCompletionCustomToolTextFormat = { + type: "text"; +}; +type ChatCompletionCustomToolFormat = ChatCompletionCustomToolTextFormat | ChatCompletionCustomToolGrammarFormat; +type ChatCompletionCustomTool = { + type: "custom"; + custom: { + name: string; + description?: string; + format?: ChatCompletionCustomToolFormat; + }; +}; +type ChatCompletionTool = ChatCompletionFunctionTool | ChatCompletionCustomTool; +type ChatCompletionMessageFunctionToolCall = { + id: string; + type: "function"; + function: { + name: string; + /** JSON-encoded arguments string. */ + arguments: string; + }; +}; +type ChatCompletionMessageCustomToolCall = { + id: string; + type: "custom"; + custom: { + name: string; + input: string; + }; +}; +type ChatCompletionMessageToolCall = ChatCompletionMessageFunctionToolCall | ChatCompletionMessageCustomToolCall; +type ChatCompletionToolChoiceFunction = { + type: "function"; + function: { + name: string; + }; +}; +type ChatCompletionToolChoiceCustom = { + type: "custom"; + custom: { + name: string; + }; +}; +type ChatCompletionToolChoiceAllowedTools = { + type: "allowed_tools"; + allowed_tools: { + mode: "auto" | "required"; + tools: Array>; + }; +}; +type ChatCompletionToolChoiceOption = "none" | "auto" | "required" | ChatCompletionToolChoiceFunction | ChatCompletionToolChoiceCustom | ChatCompletionToolChoiceAllowedTools; +type DeveloperMessage = { + role: "developer"; + content: string | Array<{ + type: "text"; + text: string; + }>; + name?: string; +}; +type SystemMessage = { + role: "system"; + content: string | Array<{ + type: "text"; + text: string; + }>; + name?: string; +}; +/** + * Permissive merged content part used inside UserMessage arrays. + * + * Cabidela has a limitation where anyOf/oneOf with enum-based discrimination + * inside nested array items does not correctly match different branches for + * different array elements, so the schema uses a single merged object. + */ +type UserMessageContentPart = { + type: "text" | "image_url" | "input_audio" | "file"; + text?: string; + image_url?: { + url?: string; + detail?: "auto" | "low" | "high"; + }; + input_audio?: { + data?: string; + format?: "wav" | "mp3"; + }; + file?: { + file_data?: string; + file_id?: string; + filename?: string; + }; +}; +type UserMessage = { + role: "user"; + content: string | Array; + name?: string; +}; +type AssistantMessageContentPart = { + type: "text" | "refusal"; + text?: string; + refusal?: string; +}; +type AssistantMessage = { + role: "assistant"; + content?: string | null | Array; + refusal?: string | null; + name?: string; + audio?: { + id: string; + }; + tool_calls?: Array; + function_call?: { + name: string; + arguments: string; + }; +}; +type ToolMessage = { + role: "tool"; + content: string | Array<{ + type: "text"; + text: string; + }>; + tool_call_id: string; +}; +type FunctionMessage = { + role: "function"; + content: string; + name: string; +}; +type ChatCompletionMessageParam = DeveloperMessage | SystemMessage | UserMessage | AssistantMessage | ToolMessage | FunctionMessage; +type ChatCompletionsResponseFormatText = { + type: "text"; +}; +type ChatCompletionsResponseFormatJSONObject = { + type: "json_object"; +}; +type ResponseFormatJSONSchema = { + type: "json_schema"; + json_schema: { + name: string; + description?: string; + schema?: Record; + strict?: boolean | null; + }; +}; +type ResponseFormat = ChatCompletionsResponseFormatText | ChatCompletionsResponseFormatJSONObject | ResponseFormatJSONSchema; +type ChatCompletionsStreamOptions = { + include_usage?: boolean; + include_obfuscation?: boolean; +}; +type PredictionContent = { + type: "content"; + content: string | Array<{ + type: "text"; + text: string; + }>; +}; +type AudioParams = { + voice: string | { + id: string; + }; + format: "wav" | "aac" | "mp3" | "flac" | "opus" | "pcm16"; +}; +type WebSearchUserLocation = { + type: "approximate"; + approximate: { + city?: string; + country?: string; + region?: string; + timezone?: string; + }; +}; +type WebSearchOptions = { + search_context_size?: "low" | "medium" | "high"; + user_location?: WebSearchUserLocation; +}; +type ChatTemplateKwargs = { + /** Whether to enable reasoning, enabled by default. */ + enable_thinking?: boolean; + /** If false, preserves reasoning context between turns. */ + clear_thinking?: boolean; +}; +/** Shared optional properties used by both Prompt and Messages input branches. */ +type ChatCompletionsCommonOptions = { + model?: string; + audio?: AudioParams; + frequency_penalty?: number | null; + logit_bias?: Record | null; + logprobs?: boolean | null; + top_logprobs?: number | null; + max_tokens?: number | null; + max_completion_tokens?: number | null; + metadata?: Record | null; + modalities?: Array<"text" | "audio"> | null; + n?: number | null; + parallel_tool_calls?: boolean; + prediction?: PredictionContent; + presence_penalty?: number | null; + reasoning_effort?: "low" | "medium" | "high" | null; + chat_template_kwargs?: ChatTemplateKwargs; + response_format?: ResponseFormat; + seed?: number | null; + service_tier?: "auto" | "default" | "flex" | "scale" | "priority" | null; + stop?: string | Array | null; + store?: boolean | null; + stream?: boolean | null; + stream_options?: ChatCompletionsStreamOptions; + temperature?: number | null; + tool_choice?: ChatCompletionToolChoiceOption; + tools?: Array; + top_p?: number | null; + user?: string; + web_search_options?: WebSearchOptions; + function_call?: "none" | "auto" | { + name: string; + }; + functions?: Array; +}; +type PromptTokensDetails = { + cached_tokens?: number; + audio_tokens?: number; +}; +type CompletionTokensDetails = { + reasoning_tokens?: number; + audio_tokens?: number; + accepted_prediction_tokens?: number; + rejected_prediction_tokens?: number; +}; +type CompletionUsage = { + prompt_tokens: number; + completion_tokens: number; + total_tokens: number; + prompt_tokens_details?: PromptTokensDetails; + completion_tokens_details?: CompletionTokensDetails; +}; +type ChatCompletionTopLogprob = { + token: string; + logprob: number; + bytes: Array | null; +}; +type ChatCompletionTokenLogprob = { + token: string; + logprob: number; + bytes: Array | null; + top_logprobs: Array; +}; +type ChatCompletionAudio = { + id: string; + /** Base64 encoded audio bytes. */ + data: string; + expires_at: number; + transcript: string; +}; +type ChatCompletionUrlCitation = { + type: "url_citation"; + url_citation: { + url: string; + title: string; + start_index: number; + end_index: number; + }; +}; +type ChatCompletionResponseMessage = { + role: "assistant"; + content: string | null; + refusal: string | null; + annotations?: Array; + audio?: ChatCompletionAudio; + tool_calls?: Array; + function_call?: { + name: string; + arguments: string; + } | null; +}; +type ChatCompletionLogprobs = { + content: Array | null; + refusal?: Array | null; +}; +type ChatCompletionChoice = { + index: number; + message: ChatCompletionResponseMessage; + finish_reason: "stop" | "length" | "tool_calls" | "content_filter" | "function_call"; + logprobs: ChatCompletionLogprobs | null; +}; +type ChatCompletionsMessagesInput = { + messages: Array; +} & ChatCompletionsCommonOptions; +type ChatCompletionsOutput = { + id: string; + object: string; + created: number; + model: string; + choices: Array; + usage?: CompletionUsage; + system_fingerprint?: string | null; + service_tier?: "auto" | "default" | "flex" | "scale" | "priority" | null; +}; +/** + * Workers AI support for OpenAI's Responses API + * Reference: https://github.com/openai/openai-node/blob/master/src/resources/responses/responses.ts + * + * It's a stripped down version from its source. + * It currently supports basic function calling, json mode and accepts images as input. + * + * It does not include types for WebSearch, CodeInterpreter, FileInputs, MCP, CustomTools. + * We plan to add those incrementally as model + platform capabilities evolve. + */ +type ResponsesInput = { + background?: boolean | null; + conversation?: string | ResponseConversationParam | null; + include?: Array | null; + input?: string | ResponseInput; + instructions?: string | null; + max_output_tokens?: number | null; + parallel_tool_calls?: boolean | null; + previous_response_id?: string | null; + prompt_cache_key?: string; + reasoning?: Reasoning | null; + safety_identifier?: string; + service_tier?: "auto" | "default" | "flex" | "scale" | "priority" | null; + stream?: boolean | null; + stream_options?: StreamOptions | null; + temperature?: number | null; + text?: ResponseTextConfig; + tool_choice?: ToolChoiceOptions | ToolChoiceFunction; + tools?: Array; + top_p?: number | null; + truncation?: "auto" | "disabled" | null; +}; +type ResponsesOutput = { + id?: string; + created_at?: number; + output_text?: string; + error?: ResponseError | null; + incomplete_details?: ResponseIncompleteDetails | null; + instructions?: string | Array | null; + object?: "response"; + output?: Array; + parallel_tool_calls?: boolean; + temperature?: number | null; + tool_choice?: ToolChoiceOptions | ToolChoiceFunction; + tools?: Array; + top_p?: number | null; + max_output_tokens?: number | null; + previous_response_id?: string | null; + prompt?: ResponsePrompt | null; + reasoning?: Reasoning | null; + safety_identifier?: string; + service_tier?: "auto" | "default" | "flex" | "scale" | "priority" | null; + status?: ResponseStatus; + text?: ResponseTextConfig; + truncation?: "auto" | "disabled" | null; + usage?: ResponseUsage; +}; +type EasyInputMessage = { + content: string | ResponseInputMessageContentList; + role: "user" | "assistant" | "system" | "developer"; + type?: "message"; +}; +type ResponsesFunctionTool = { + name: string; + parameters: { + [key: string]: unknown; + } | null; + strict: boolean | null; + type: "function"; + description?: string | null; +}; +type ResponseIncompleteDetails = { + reason?: "max_output_tokens" | "content_filter"; +}; +type ResponsePrompt = { + id: string; + variables?: { + [key: string]: string | ResponseInputText | ResponseInputImage; + } | null; + version?: string | null; +}; +type Reasoning = { + effort?: ReasoningEffort | null; + generate_summary?: "auto" | "concise" | "detailed" | null; + summary?: "auto" | "concise" | "detailed" | null; +}; +type ResponseContent = ResponseInputText | ResponseInputImage | ResponseOutputText | ResponseOutputRefusal | ResponseContentReasoningText; +type ResponseContentReasoningText = { + text: string; + type: "reasoning_text"; +}; +type ResponseConversationParam = { + id: string; +}; +type ResponseCreatedEvent = { + response: Response; + sequence_number: number; + type: "response.created"; +}; +type ResponseCustomToolCallOutput = { + call_id: string; + output: string | Array; + type: "custom_tool_call_output"; + id?: string; +}; +type ResponseError = { + code: "server_error" | "rate_limit_exceeded" | "invalid_prompt" | "vector_store_timeout" | "invalid_image" | "invalid_image_format" | "invalid_base64_image" | "invalid_image_url" | "image_too_large" | "image_too_small" | "image_parse_error" | "image_content_policy_violation" | "invalid_image_mode" | "image_file_too_large" | "unsupported_image_media_type" | "empty_image_file" | "failed_to_download_image" | "image_file_not_found"; + message: string; +}; +type ResponseErrorEvent = { + code: string | null; + message: string; + param: string | null; + sequence_number: number; + type: "error"; +}; +type ResponseFailedEvent = { + response: Response; + sequence_number: number; + type: "response.failed"; +}; +type ResponseFormatText = { + type: "text"; +}; +type ResponseFormatJSONObject = { + type: "json_object"; +}; +type ResponseFormatTextConfig = ResponseFormatText | ResponseFormatTextJSONSchemaConfig | ResponseFormatJSONObject; +type ResponseFormatTextJSONSchemaConfig = { + name: string; + schema: { + [key: string]: unknown; + }; + type: "json_schema"; + description?: string; + strict?: boolean | null; +}; +type ResponseFunctionCallArgumentsDeltaEvent = { + delta: string; + item_id: string; + output_index: number; + sequence_number: number; + type: "response.function_call_arguments.delta"; +}; +type ResponseFunctionCallArgumentsDoneEvent = { + arguments: string; + item_id: string; + name: string; + output_index: number; + sequence_number: number; + type: "response.function_call_arguments.done"; +}; +type ResponseFunctionCallOutputItem = ResponseInputTextContent | ResponseInputImageContent; +type ResponseFunctionCallOutputItemList = Array; +type ResponseFunctionToolCall = { + arguments: string; + call_id: string; + name: string; + type: "function_call"; + id?: string; + status?: "in_progress" | "completed" | "incomplete"; +}; +interface ResponseFunctionToolCallItem extends ResponseFunctionToolCall { + id: string; +} +type ResponseFunctionToolCallOutputItem = { + id: string; + call_id: string; + output: string | Array; + type: "function_call_output"; + status?: "in_progress" | "completed" | "incomplete"; +}; +type ResponseIncludable = "message.input_image.image_url" | "message.output_text.logprobs"; +type ResponseIncompleteEvent = { + response: Response; + sequence_number: number; + type: "response.incomplete"; +}; +type ResponseInput = Array; +type ResponseInputContent = ResponseInputText | ResponseInputImage; +type ResponseInputImage = { + detail: "low" | "high" | "auto"; + type: "input_image"; + /** + * Base64 encoded image + */ + image_url?: string | null; +}; +type ResponseInputImageContent = { + type: "input_image"; + detail?: "low" | "high" | "auto" | null; + /** + * Base64 encoded image + */ + image_url?: string | null; +}; +type ResponseInputItem = EasyInputMessage | ResponseInputItemMessage | ResponseOutputMessage | ResponseFunctionToolCall | ResponseInputItemFunctionCallOutput | ResponseReasoningItem; +type ResponseInputItemFunctionCallOutput = { + call_id: string; + output: string | ResponseFunctionCallOutputItemList; + type: "function_call_output"; + id?: string | null; + status?: "in_progress" | "completed" | "incomplete" | null; +}; +type ResponseInputItemMessage = { + content: ResponseInputMessageContentList; + role: "user" | "system" | "developer"; + status?: "in_progress" | "completed" | "incomplete"; + type?: "message"; +}; +type ResponseInputMessageContentList = Array; +type ResponseInputMessageItem = { + id: string; + content: ResponseInputMessageContentList; + role: "user" | "system" | "developer"; + status?: "in_progress" | "completed" | "incomplete"; + type?: "message"; +}; +type ResponseInputText = { + text: string; + type: "input_text"; +}; +type ResponseInputTextContent = { + text: string; + type: "input_text"; +}; +type ResponseItem = ResponseInputMessageItem | ResponseOutputMessage | ResponseFunctionToolCallItem | ResponseFunctionToolCallOutputItem; +type ResponseOutputItem = ResponseOutputMessage | ResponseFunctionToolCall | ResponseReasoningItem; +type ResponseOutputItemAddedEvent = { + item: ResponseOutputItem; + output_index: number; + sequence_number: number; + type: "response.output_item.added"; +}; +type ResponseOutputItemDoneEvent = { + item: ResponseOutputItem; + output_index: number; + sequence_number: number; + type: "response.output_item.done"; +}; +type ResponseOutputMessage = { + id: string; + content: Array; + role: "assistant"; + status: "in_progress" | "completed" | "incomplete"; + type: "message"; +}; +type ResponseOutputRefusal = { + refusal: string; + type: "refusal"; +}; +type ResponseOutputText = { + text: string; + type: "output_text"; + logprobs?: Array; +}; +type ResponseReasoningItem = { + id: string; + summary: Array; + type: "reasoning"; + content?: Array; + encrypted_content?: string | null; + status?: "in_progress" | "completed" | "incomplete"; +}; +type ResponseReasoningSummaryItem = { + text: string; + type: "summary_text"; +}; +type ResponseReasoningContentItem = { + text: string; + type: "reasoning_text"; +}; +type ResponseReasoningTextDeltaEvent = { + content_index: number; + delta: string; + item_id: string; + output_index: number; + sequence_number: number; + type: "response.reasoning_text.delta"; +}; +type ResponseReasoningTextDoneEvent = { + content_index: number; + item_id: string; + output_index: number; + sequence_number: number; + text: string; + type: "response.reasoning_text.done"; +}; +type ResponseRefusalDeltaEvent = { + content_index: number; + delta: string; + item_id: string; + output_index: number; + sequence_number: number; + type: "response.refusal.delta"; +}; +type ResponseRefusalDoneEvent = { + content_index: number; + item_id: string; + output_index: number; + refusal: string; + sequence_number: number; + type: "response.refusal.done"; +}; +type ResponseStatus = "completed" | "failed" | "in_progress" | "cancelled" | "queued" | "incomplete"; +type ResponseStreamEvent = ResponseCompletedEvent | ResponseCreatedEvent | ResponseErrorEvent | ResponseFunctionCallArgumentsDeltaEvent | ResponseFunctionCallArgumentsDoneEvent | ResponseFailedEvent | ResponseIncompleteEvent | ResponseOutputItemAddedEvent | ResponseOutputItemDoneEvent | ResponseReasoningTextDeltaEvent | ResponseReasoningTextDoneEvent | ResponseRefusalDeltaEvent | ResponseRefusalDoneEvent | ResponseTextDeltaEvent | ResponseTextDoneEvent; +type ResponseCompletedEvent = { + response: Response; + sequence_number: number; + type: "response.completed"; +}; +type ResponseTextConfig = { + format?: ResponseFormatTextConfig; + verbosity?: "low" | "medium" | "high" | null; +}; +type ResponseTextDeltaEvent = { + content_index: number; + delta: string; + item_id: string; + logprobs: Array; + output_index: number; + sequence_number: number; + type: "response.output_text.delta"; +}; +type ResponseTextDoneEvent = { + content_index: number; + item_id: string; + logprobs: Array; + output_index: number; + sequence_number: number; + text: string; + type: "response.output_text.done"; +}; +type Logprob = { + token: string; + logprob: number; + top_logprobs?: Array; +}; +type TopLogprob = { + token?: string; + logprob?: number; +}; +type ResponseUsage = { + input_tokens: number; + output_tokens: number; + total_tokens: number; +}; +type Tool = ResponsesFunctionTool; +type ToolChoiceFunction = { + name: string; + type: "function"; +}; +type ToolChoiceOptions = "none"; +type ReasoningEffort = "minimal" | "low" | "medium" | "high" | null; +type StreamOptions = { + include_obfuscation?: boolean; +}; +/** Marks keys from T that aren't in U as optional never */ +type Without = { + [P in Exclude]?: never; +}; +/** Either T or U, but not both (mutually exclusive) */ +type XOR = (T & Without) | (U & Without); +type Ai_Cf_Baai_Bge_Base_En_V1_5_Input = { + text: string | string[]; + /** + * The pooling method used in the embedding process. `cls` pooling will generate more accurate embeddings on larger inputs - however, embeddings created with cls pooling are not compatible with embeddings generated with mean pooling. The default pooling method is `mean` in order for this to not be a breaking change, but we highly suggest using the new `cls` pooling for better accuracy. + */ + pooling?: "mean" | "cls"; +} | { + /** + * Batch of the embeddings requests to run using async-queue + */ + requests: { + text: string | string[]; + /** + * The pooling method used in the embedding process. `cls` pooling will generate more accurate embeddings on larger inputs - however, embeddings created with cls pooling are not compatible with embeddings generated with mean pooling. The default pooling method is `mean` in order for this to not be a breaking change, but we highly suggest using the new `cls` pooling for better accuracy. + */ + pooling?: "mean" | "cls"; + }[]; +}; +type Ai_Cf_Baai_Bge_Base_En_V1_5_Output = { + shape?: number[]; + /** + * Embeddings of the requested text values + */ + data?: number[][]; + /** + * The pooling method used in the embedding process. + */ + pooling?: "mean" | "cls"; +} | Ai_Cf_Baai_Bge_Base_En_V1_5_AsyncResponse; +interface Ai_Cf_Baai_Bge_Base_En_V1_5_AsyncResponse { + /** + * The async request id that can be used to obtain the results. + */ + request_id?: string; +} +declare abstract class Base_Ai_Cf_Baai_Bge_Base_En_V1_5 { + inputs: Ai_Cf_Baai_Bge_Base_En_V1_5_Input; + postProcessedOutputs: Ai_Cf_Baai_Bge_Base_En_V1_5_Output; +} +type Ai_Cf_Openai_Whisper_Input = string | { + /** + * An array of integers that represent the audio data constrained to 8-bit unsigned integer values + */ + audio: number[]; +}; +interface Ai_Cf_Openai_Whisper_Output { + /** + * The transcription + */ + text: string; + word_count?: number; + words?: { + word?: string; + /** + * The second this word begins in the recording + */ + start?: number; + /** + * The ending second when the word completes + */ + end?: number; + }[]; + vtt?: string; +} +declare abstract class Base_Ai_Cf_Openai_Whisper { + inputs: Ai_Cf_Openai_Whisper_Input; + postProcessedOutputs: Ai_Cf_Openai_Whisper_Output; +} +type Ai_Cf_Meta_M2M100_1_2B_Input = { + /** + * The text to be translated + */ + text: string; + /** + * The language code of the source text (e.g., 'en' for English). Defaults to 'en' if not specified + */ + source_lang?: string; + /** + * The language code to translate the text into (e.g., 'es' for Spanish) + */ + target_lang: string; +} | { + /** + * Batch of the embeddings requests to run using async-queue + */ + requests: { + /** + * The text to be translated + */ + text: string; + /** + * The language code of the source text (e.g., 'en' for English). Defaults to 'en' if not specified + */ + source_lang?: string; + /** + * The language code to translate the text into (e.g., 'es' for Spanish) + */ + target_lang: string; + }[]; +}; +type Ai_Cf_Meta_M2M100_1_2B_Output = { + /** + * The translated text in the target language + */ + translated_text?: string; +} | Ai_Cf_Meta_M2M100_1_2B_AsyncResponse; +interface Ai_Cf_Meta_M2M100_1_2B_AsyncResponse { + /** + * The async request id that can be used to obtain the results. + */ + request_id?: string; +} +declare abstract class Base_Ai_Cf_Meta_M2M100_1_2B { + inputs: Ai_Cf_Meta_M2M100_1_2B_Input; + postProcessedOutputs: Ai_Cf_Meta_M2M100_1_2B_Output; +} +type Ai_Cf_Baai_Bge_Small_En_V1_5_Input = { + text: string | string[]; + /** + * The pooling method used in the embedding process. `cls` pooling will generate more accurate embeddings on larger inputs - however, embeddings created with cls pooling are not compatible with embeddings generated with mean pooling. The default pooling method is `mean` in order for this to not be a breaking change, but we highly suggest using the new `cls` pooling for better accuracy. + */ + pooling?: "mean" | "cls"; +} | { + /** + * Batch of the embeddings requests to run using async-queue + */ + requests: { + text: string | string[]; + /** + * The pooling method used in the embedding process. `cls` pooling will generate more accurate embeddings on larger inputs - however, embeddings created with cls pooling are not compatible with embeddings generated with mean pooling. The default pooling method is `mean` in order for this to not be a breaking change, but we highly suggest using the new `cls` pooling for better accuracy. + */ + pooling?: "mean" | "cls"; + }[]; +}; +type Ai_Cf_Baai_Bge_Small_En_V1_5_Output = { + shape?: number[]; + /** + * Embeddings of the requested text values + */ + data?: number[][]; + /** + * The pooling method used in the embedding process. + */ + pooling?: "mean" | "cls"; +} | Ai_Cf_Baai_Bge_Small_En_V1_5_AsyncResponse; +interface Ai_Cf_Baai_Bge_Small_En_V1_5_AsyncResponse { + /** + * The async request id that can be used to obtain the results. + */ + request_id?: string; +} +declare abstract class Base_Ai_Cf_Baai_Bge_Small_En_V1_5 { + inputs: Ai_Cf_Baai_Bge_Small_En_V1_5_Input; + postProcessedOutputs: Ai_Cf_Baai_Bge_Small_En_V1_5_Output; +} +type Ai_Cf_Baai_Bge_Large_En_V1_5_Input = { + text: string | string[]; + /** + * The pooling method used in the embedding process. `cls` pooling will generate more accurate embeddings on larger inputs - however, embeddings created with cls pooling are not compatible with embeddings generated with mean pooling. The default pooling method is `mean` in order for this to not be a breaking change, but we highly suggest using the new `cls` pooling for better accuracy. + */ + pooling?: "mean" | "cls"; +} | { + /** + * Batch of the embeddings requests to run using async-queue + */ + requests: { + text: string | string[]; + /** + * The pooling method used in the embedding process. `cls` pooling will generate more accurate embeddings on larger inputs - however, embeddings created with cls pooling are not compatible with embeddings generated with mean pooling. The default pooling method is `mean` in order for this to not be a breaking change, but we highly suggest using the new `cls` pooling for better accuracy. + */ + pooling?: "mean" | "cls"; + }[]; +}; +type Ai_Cf_Baai_Bge_Large_En_V1_5_Output = { + shape?: number[]; + /** + * Embeddings of the requested text values + */ + data?: number[][]; + /** + * The pooling method used in the embedding process. + */ + pooling?: "mean" | "cls"; +} | Ai_Cf_Baai_Bge_Large_En_V1_5_AsyncResponse; +interface Ai_Cf_Baai_Bge_Large_En_V1_5_AsyncResponse { + /** + * The async request id that can be used to obtain the results. + */ + request_id?: string; +} +declare abstract class Base_Ai_Cf_Baai_Bge_Large_En_V1_5 { + inputs: Ai_Cf_Baai_Bge_Large_En_V1_5_Input; + postProcessedOutputs: Ai_Cf_Baai_Bge_Large_En_V1_5_Output; +} +type Ai_Cf_Unum_Uform_Gen2_Qwen_500M_Input = string | { + /** + * The input text prompt for the model to generate a response. + */ + prompt?: string; + /** + * If true, a chat template is not applied and you must adhere to the specific model's expected formatting. + */ + raw?: boolean; + /** + * Controls the creativity of the AI's responses by adjusting how many possible words it considers. Lower values make outputs more predictable; higher values allow for more varied and creative responses. + */ + top_p?: number; + /** + * Limits the AI to choose from the top 'k' most probable words. Lower values make responses more focused; higher values introduce more variety and potential surprises. + */ + top_k?: number; + /** + * Random seed for reproducibility of the generation. + */ + seed?: number; + /** + * Penalty for repeated tokens; higher values discourage repetition. + */ + repetition_penalty?: number; + /** + * Decreases the likelihood of the model repeating the same lines verbatim. + */ + frequency_penalty?: number; + /** + * Increases the likelihood of the model introducing new topics. + */ + presence_penalty?: number; + image: number[] | (string & NonNullable); + /** + * The maximum number of tokens to generate in the response. + */ + max_tokens?: number; +}; +interface Ai_Cf_Unum_Uform_Gen2_Qwen_500M_Output { + description?: string; +} +declare abstract class Base_Ai_Cf_Unum_Uform_Gen2_Qwen_500M { + inputs: Ai_Cf_Unum_Uform_Gen2_Qwen_500M_Input; + postProcessedOutputs: Ai_Cf_Unum_Uform_Gen2_Qwen_500M_Output; +} +type Ai_Cf_Openai_Whisper_Tiny_En_Input = string | { + /** + * An array of integers that represent the audio data constrained to 8-bit unsigned integer values + */ + audio: number[]; +}; +interface Ai_Cf_Openai_Whisper_Tiny_En_Output { + /** + * The transcription + */ + text: string; + word_count?: number; + words?: { + word?: string; + /** + * The second this word begins in the recording + */ + start?: number; + /** + * The ending second when the word completes + */ + end?: number; + }[]; + vtt?: string; +} +declare abstract class Base_Ai_Cf_Openai_Whisper_Tiny_En { + inputs: Ai_Cf_Openai_Whisper_Tiny_En_Input; + postProcessedOutputs: Ai_Cf_Openai_Whisper_Tiny_En_Output; +} +interface Ai_Cf_Openai_Whisper_Large_V3_Turbo_Input { + audio: string | { + body?: object; + contentType?: string; + }; + /** + * Supported tasks are 'translate' or 'transcribe'. + */ + task?: string; + /** + * The language of the audio being transcribed or translated. + */ + language?: string; + /** + * Preprocess the audio with a voice activity detection model. + */ + vad_filter?: boolean; + /** + * A text prompt to help provide context to the model on the contents of the audio. + */ + initial_prompt?: string; + /** + * The prefix appended to the beginning of the output of the transcription and can guide the transcription result. + */ + prefix?: string; + /** + * The number of beams to use in beam search decoding. Higher values may improve accuracy at the cost of speed. + */ + beam_size?: number; + /** + * Whether to condition on previous text during transcription. Setting to false may help prevent hallucination loops. + */ + condition_on_previous_text?: boolean; + /** + * Threshold for detecting no-speech segments. Segments with no-speech probability above this value are skipped. + */ + no_speech_threshold?: number; + /** + * Threshold for filtering out segments with high compression ratio, which often indicate repetitive or hallucinated text. + */ + compression_ratio_threshold?: number; + /** + * Threshold for filtering out segments with low average log probability, indicating low confidence. + */ + log_prob_threshold?: number; + /** + * Optional threshold (in seconds) to skip silent periods that may cause hallucinations. + */ + hallucination_silence_threshold?: number; +} +interface Ai_Cf_Openai_Whisper_Large_V3_Turbo_Output { + transcription_info?: { + /** + * The language of the audio being transcribed or translated. + */ + language?: string; + /** + * The confidence level or probability of the detected language being accurate, represented as a decimal between 0 and 1. + */ + language_probability?: number; + /** + * The total duration of the original audio file, in seconds. + */ + duration?: number; + /** + * The duration of the audio after applying Voice Activity Detection (VAD) to remove silent or irrelevant sections, in seconds. + */ + duration_after_vad?: number; + }; + /** + * The complete transcription of the audio. + */ + text: string; + /** + * The total number of words in the transcription. + */ + word_count?: number; + segments?: { + /** + * The starting time of the segment within the audio, in seconds. + */ + start?: number; + /** + * The ending time of the segment within the audio, in seconds. + */ + end?: number; + /** + * The transcription of the segment. + */ + text?: string; + /** + * The temperature used in the decoding process, controlling randomness in predictions. Lower values result in more deterministic outputs. + */ + temperature?: number; + /** + * The average log probability of the predictions for the words in this segment, indicating overall confidence. + */ + avg_logprob?: number; + /** + * The compression ratio of the input to the output, measuring how much the text was compressed during the transcription process. + */ + compression_ratio?: number; + /** + * The probability that the segment contains no speech, represented as a decimal between 0 and 1. + */ + no_speech_prob?: number; + words?: { + /** + * The individual word transcribed from the audio. + */ + word?: string; + /** + * The starting time of the word within the audio, in seconds. + */ + start?: number; + /** + * The ending time of the word within the audio, in seconds. + */ + end?: number; + }[]; + }[]; + /** + * The transcription in WebVTT format, which includes timing and text information for use in subtitles. + */ + vtt?: string; +} +declare abstract class Base_Ai_Cf_Openai_Whisper_Large_V3_Turbo { + inputs: Ai_Cf_Openai_Whisper_Large_V3_Turbo_Input; + postProcessedOutputs: Ai_Cf_Openai_Whisper_Large_V3_Turbo_Output; +} +type Ai_Cf_Baai_Bge_M3_Input = Ai_Cf_Baai_Bge_M3_Input_QueryAnd_Contexts | Ai_Cf_Baai_Bge_M3_Input_Embedding | { + /** + * Batch of the embeddings requests to run using async-queue + */ + requests: (Ai_Cf_Baai_Bge_M3_Input_QueryAnd_Contexts_1 | Ai_Cf_Baai_Bge_M3_Input_Embedding_1)[]; +}; +interface Ai_Cf_Baai_Bge_M3_Input_QueryAnd_Contexts { + /** + * A query you wish to perform against the provided contexts. If no query is provided the model with respond with embeddings for contexts + */ + query?: string; + /** + * List of provided contexts. Note that the index in this array is important, as the response will refer to it. + */ + contexts: { + /** + * One of the provided context content + */ + text?: string; + }[]; + /** + * When provided with too long context should the model error out or truncate the context to fit? + */ + truncate_inputs?: boolean; +} +interface Ai_Cf_Baai_Bge_M3_Input_Embedding { + text: string | string[]; + /** + * When provided with too long context should the model error out or truncate the context to fit? + */ + truncate_inputs?: boolean; +} +interface Ai_Cf_Baai_Bge_M3_Input_QueryAnd_Contexts_1 { + /** + * A query you wish to perform against the provided contexts. If no query is provided the model with respond with embeddings for contexts + */ + query?: string; + /** + * List of provided contexts. Note that the index in this array is important, as the response will refer to it. + */ + contexts: { + /** + * One of the provided context content + */ + text?: string; + }[]; + /** + * When provided with too long context should the model error out or truncate the context to fit? + */ + truncate_inputs?: boolean; +} +interface Ai_Cf_Baai_Bge_M3_Input_Embedding_1 { + text: string | string[]; + /** + * When provided with too long context should the model error out or truncate the context to fit? + */ + truncate_inputs?: boolean; +} +type Ai_Cf_Baai_Bge_M3_Output = Ai_Cf_Baai_Bge_M3_Output_Query | Ai_Cf_Baai_Bge_M3_Output_EmbeddingFor_Contexts | Ai_Cf_Baai_Bge_M3_Output_Embedding | Ai_Cf_Baai_Bge_M3_AsyncResponse; +interface Ai_Cf_Baai_Bge_M3_Output_Query { + response?: { + /** + * Index of the context in the request + */ + id?: number; + /** + * Score of the context under the index. + */ + score?: number; + }[]; +} +interface Ai_Cf_Baai_Bge_M3_Output_EmbeddingFor_Contexts { + response?: number[][]; + shape?: number[]; + /** + * The pooling method used in the embedding process. + */ + pooling?: "mean" | "cls"; +} +interface Ai_Cf_Baai_Bge_M3_Output_Embedding { + shape?: number[]; + /** + * Embeddings of the requested text values + */ + data?: number[][]; + /** + * The pooling method used in the embedding process. + */ + pooling?: "mean" | "cls"; +} +interface Ai_Cf_Baai_Bge_M3_AsyncResponse { + /** + * The async request id that can be used to obtain the results. + */ + request_id?: string; +} +declare abstract class Base_Ai_Cf_Baai_Bge_M3 { + inputs: Ai_Cf_Baai_Bge_M3_Input; + postProcessedOutputs: Ai_Cf_Baai_Bge_M3_Output; +} +interface Ai_Cf_Black_Forest_Labs_Flux_1_Schnell_Input { + /** + * A text description of the image you want to generate. + */ + prompt: string; + /** + * The number of diffusion steps; higher values can improve quality but take longer. + */ + steps?: number; +} +interface Ai_Cf_Black_Forest_Labs_Flux_1_Schnell_Output { + /** + * The generated image in Base64 format. + */ + image?: string; +} +declare abstract class Base_Ai_Cf_Black_Forest_Labs_Flux_1_Schnell { + inputs: Ai_Cf_Black_Forest_Labs_Flux_1_Schnell_Input; + postProcessedOutputs: Ai_Cf_Black_Forest_Labs_Flux_1_Schnell_Output; +} +type Ai_Cf_Meta_Llama_3_2_11B_Vision_Instruct_Input = Ai_Cf_Meta_Llama_3_2_11B_Vision_Instruct_Prompt | Ai_Cf_Meta_Llama_3_2_11B_Vision_Instruct_Messages; +interface Ai_Cf_Meta_Llama_3_2_11B_Vision_Instruct_Prompt { + /** + * The input text prompt for the model to generate a response. + */ + prompt: string; + image?: number[] | (string & NonNullable); + /** + * If true, a chat template is not applied and you must adhere to the specific model's expected formatting. + */ + raw?: boolean; + /** + * If true, the response will be streamed back incrementally using SSE, Server Sent Events. + */ + stream?: boolean; + /** + * The maximum number of tokens to generate in the response. + */ + max_tokens?: number; + /** + * Controls the randomness of the output; higher values produce more random results. + */ + temperature?: number; + /** + * Adjusts the creativity of the AI's responses by controlling how many possible words it considers. Lower values make outputs more predictable; higher values allow for more varied and creative responses. + */ + top_p?: number; + /** + * Limits the AI to choose from the top 'k' most probable words. Lower values make responses more focused; higher values introduce more variety and potential surprises. + */ + top_k?: number; + /** + * Random seed for reproducibility of the generation. + */ + seed?: number; + /** + * Penalty for repeated tokens; higher values discourage repetition. + */ + repetition_penalty?: number; + /** + * Decreases the likelihood of the model repeating the same lines verbatim. + */ + frequency_penalty?: number; + /** + * Increases the likelihood of the model introducing new topics. + */ + presence_penalty?: number; + /** + * Name of the LoRA (Low-Rank Adaptation) model to fine-tune the base model. + */ + lora?: string; +} +interface Ai_Cf_Meta_Llama_3_2_11B_Vision_Instruct_Messages { + /** + * An array of message objects representing the conversation history. + */ + messages: { + /** + * The role of the message sender (e.g., 'user', 'assistant', 'system', 'tool'). + */ + role?: string; + /** + * The tool call id. If you don't know what to put here you can fall back to 000000001 + */ + tool_call_id?: string; + content?: string | { + /** + * Type of the content provided + */ + type?: string; + text?: string; + image_url?: { + /** + * image uri with data (e.g. data:image/jpeg;base64,/9j/...). HTTP URL will not be accepted + */ + url?: string; + }; + }[] | { + /** + * Type of the content provided + */ + type?: string; + text?: string; + image_url?: { + /** + * image uri with data (e.g. data:image/jpeg;base64,/9j/...). HTTP URL will not be accepted + */ + url?: string; + }; + }; + }[]; + image?: number[] | (string & NonNullable); + functions?: { + name: string; + code: string; + }[]; + /** + * A list of tools available for the assistant to use. + */ + tools?: ({ + /** + * The name of the tool. More descriptive the better. + */ + name: string; + /** + * A brief description of what the tool does. + */ + description: string; + /** + * Schema defining the parameters accepted by the tool. + */ + parameters: { + /** + * The type of the parameters object (usually 'object'). + */ + type: string; + /** + * List of required parameter names. + */ + required?: string[]; + /** + * Definitions of each parameter. + */ + properties: { + [k: string]: { + /** + * The data type of the parameter. + */ + type: string; + /** + * A description of the expected parameter. + */ + description: string; + }; + }; + }; + } | { + /** + * Specifies the type of tool (e.g., 'function'). + */ + type: string; + /** + * Details of the function tool. + */ + function: { + /** + * The name of the function. + */ + name: string; + /** + * A brief description of what the function does. + */ + description: string; + /** + * Schema defining the parameters accepted by the function. + */ + parameters: { + /** + * The type of the parameters object (usually 'object'). + */ + type: string; + /** + * List of required parameter names. + */ + required?: string[]; + /** + * Definitions of each parameter. + */ + properties: { + [k: string]: { + /** + * The data type of the parameter. + */ + type: string; + /** + * A description of the expected parameter. + */ + description: string; + }; + }; + }; + }; + })[]; + /** + * If true, the response will be streamed back incrementally. + */ + stream?: boolean; + /** + * The maximum number of tokens to generate in the response. + */ + max_tokens?: number; + /** + * Controls the randomness of the output; higher values produce more random results. + */ + temperature?: number; + /** + * Controls the creativity of the AI's responses by adjusting how many possible words it considers. Lower values make outputs more predictable; higher values allow for more varied and creative responses. + */ + top_p?: number; + /** + * Limits the AI to choose from the top 'k' most probable words. Lower values make responses more focused; higher values introduce more variety and potential surprises. + */ + top_k?: number; + /** + * Random seed for reproducibility of the generation. + */ + seed?: number; + /** + * Penalty for repeated tokens; higher values discourage repetition. + */ + repetition_penalty?: number; + /** + * Decreases the likelihood of the model repeating the same lines verbatim. + */ + frequency_penalty?: number; + /** + * Increases the likelihood of the model introducing new topics. + */ + presence_penalty?: number; +} +type Ai_Cf_Meta_Llama_3_2_11B_Vision_Instruct_Output = { + /** + * The generated text response from the model + */ + response?: string; + /** + * An array of tool calls requests made during the response generation + */ + tool_calls?: { + /** + * The arguments passed to be passed to the tool call request + */ + arguments?: object; + /** + * The name of the tool to be called + */ + name?: string; + }[]; +}; +declare abstract class Base_Ai_Cf_Meta_Llama_3_2_11B_Vision_Instruct { + inputs: Ai_Cf_Meta_Llama_3_2_11B_Vision_Instruct_Input; + postProcessedOutputs: Ai_Cf_Meta_Llama_3_2_11B_Vision_Instruct_Output; +} +type Ai_Cf_Meta_Llama_3_3_70B_Instruct_Fp8_Fast_Input = Ai_Cf_Meta_Llama_3_3_70B_Instruct_Fp8_Fast_Prompt | Ai_Cf_Meta_Llama_3_3_70B_Instruct_Fp8_Fast_Messages | Ai_Cf_Meta_Llama_3_3_70B_Instruct_Fp8_Fast_Async_Batch; +interface Ai_Cf_Meta_Llama_3_3_70B_Instruct_Fp8_Fast_Prompt { + /** + * The input text prompt for the model to generate a response. + */ + prompt: string; + /** + * Name of the LoRA (Low-Rank Adaptation) model to fine-tune the base model. + */ + lora?: string; + response_format?: Ai_Cf_Meta_Llama_3_3_70B_Instruct_Fp8_Fast_JSON_Mode; + /** + * If true, a chat template is not applied and you must adhere to the specific model's expected formatting. + */ + raw?: boolean; + /** + * If true, the response will be streamed back incrementally using SSE, Server Sent Events. + */ + stream?: boolean; + /** + * The maximum number of tokens to generate in the response. + */ + max_tokens?: number; + /** + * Controls the randomness of the output; higher values produce more random results. + */ + temperature?: number; + /** + * Adjusts the creativity of the AI's responses by controlling how many possible words it considers. Lower values make outputs more predictable; higher values allow for more varied and creative responses. + */ + top_p?: number; + /** + * Limits the AI to choose from the top 'k' most probable words. Lower values make responses more focused; higher values introduce more variety and potential surprises. + */ + top_k?: number; + /** + * Random seed for reproducibility of the generation. + */ + seed?: number; + /** + * Penalty for repeated tokens; higher values discourage repetition. + */ + repetition_penalty?: number; + /** + * Decreases the likelihood of the model repeating the same lines verbatim. + */ + frequency_penalty?: number; + /** + * Increases the likelihood of the model introducing new topics. + */ + presence_penalty?: number; +} +interface Ai_Cf_Meta_Llama_3_3_70B_Instruct_Fp8_Fast_JSON_Mode { + type?: "json_object" | "json_schema"; + json_schema?: unknown; +} +interface Ai_Cf_Meta_Llama_3_3_70B_Instruct_Fp8_Fast_Messages { + /** + * An array of message objects representing the conversation history. + */ + messages: { + /** + * The role of the message sender (e.g., 'user', 'assistant', 'system', 'tool'). + */ + role: string; + content: string | { + /** + * Type of the content (text) + */ + type?: string; + /** + * Text content + */ + text?: string; + }[]; + }[]; + functions?: { + name: string; + code: string; + }[]; + /** + * A list of tools available for the assistant to use. + */ + tools?: ({ + /** + * The name of the tool. More descriptive the better. + */ + name: string; + /** + * A brief description of what the tool does. + */ + description: string; + /** + * Schema defining the parameters accepted by the tool. + */ + parameters: { + /** + * The type of the parameters object (usually 'object'). + */ + type: string; + /** + * List of required parameter names. + */ + required?: string[]; + /** + * Definitions of each parameter. + */ + properties: { + [k: string]: { + /** + * The data type of the parameter. + */ + type: string; + /** + * A description of the expected parameter. + */ + description: string; + }; + }; + }; + } | { + /** + * Specifies the type of tool (e.g., 'function'). + */ + type: string; + /** + * Details of the function tool. + */ + function: { + /** + * The name of the function. + */ + name: string; + /** + * A brief description of what the function does. + */ + description: string; + /** + * Schema defining the parameters accepted by the function. + */ + parameters: { + /** + * The type of the parameters object (usually 'object'). + */ + type: string; + /** + * List of required parameter names. + */ + required?: string[]; + /** + * Definitions of each parameter. + */ + properties: { + [k: string]: { + /** + * The data type of the parameter. + */ + type: string; + /** + * A description of the expected parameter. + */ + description: string; + }; + }; + }; + }; + })[]; + response_format?: Ai_Cf_Meta_Llama_3_3_70B_Instruct_Fp8_Fast_JSON_Mode_1; + /** + * If true, a chat template is not applied and you must adhere to the specific model's expected formatting. + */ + raw?: boolean; + /** + * If true, the response will be streamed back incrementally using SSE, Server Sent Events. + */ + stream?: boolean; + /** + * The maximum number of tokens to generate in the response. + */ + max_tokens?: number; + /** + * Controls the randomness of the output; higher values produce more random results. + */ + temperature?: number; + /** + * Adjusts the creativity of the AI's responses by controlling how many possible words it considers. Lower values make outputs more predictable; higher values allow for more varied and creative responses. + */ + top_p?: number; + /** + * Limits the AI to choose from the top 'k' most probable words. Lower values make responses more focused; higher values introduce more variety and potential surprises. + */ + top_k?: number; + /** + * Random seed for reproducibility of the generation. + */ + seed?: number; + /** + * Penalty for repeated tokens; higher values discourage repetition. + */ + repetition_penalty?: number; + /** + * Decreases the likelihood of the model repeating the same lines verbatim. + */ + frequency_penalty?: number; + /** + * Increases the likelihood of the model introducing new topics. + */ + presence_penalty?: number; +} +interface Ai_Cf_Meta_Llama_3_3_70B_Instruct_Fp8_Fast_JSON_Mode_1 { + type?: "json_object" | "json_schema"; + json_schema?: unknown; +} +interface Ai_Cf_Meta_Llama_3_3_70B_Instruct_Fp8_Fast_Async_Batch { + requests?: { + /** + * User-supplied reference. This field will be present in the response as well it can be used to reference the request and response. It's NOT validated to be unique. + */ + external_reference?: string; + /** + * Prompt for the text generation model + */ + prompt?: string; + /** + * If true, the response will be streamed back incrementally using SSE, Server Sent Events. + */ + stream?: boolean; + /** + * The maximum number of tokens to generate in the response. + */ + max_tokens?: number; + /** + * Controls the randomness of the output; higher values produce more random results. + */ + temperature?: number; + /** + * Adjusts the creativity of the AI's responses by controlling how many possible words it considers. Lower values make outputs more predictable; higher values allow for more varied and creative responses. + */ + top_p?: number; + /** + * Random seed for reproducibility of the generation. + */ + seed?: number; + /** + * Penalty for repeated tokens; higher values discourage repetition. + */ + repetition_penalty?: number; + /** + * Decreases the likelihood of the model repeating the same lines verbatim. + */ + frequency_penalty?: number; + /** + * Increases the likelihood of the model introducing new topics. + */ + presence_penalty?: number; + response_format?: Ai_Cf_Meta_Llama_3_3_70B_Instruct_Fp8_Fast_JSON_Mode_2; + }[]; +} +interface Ai_Cf_Meta_Llama_3_3_70B_Instruct_Fp8_Fast_JSON_Mode_2 { + type?: "json_object" | "json_schema"; + json_schema?: unknown; +} +type Ai_Cf_Meta_Llama_3_3_70B_Instruct_Fp8_Fast_Output = { + /** + * The generated text response from the model + */ + response: string; + /** + * Usage statistics for the inference request + */ + usage?: { + /** + * Total number of tokens in input + */ + prompt_tokens?: number; + /** + * Total number of tokens in output + */ + completion_tokens?: number; + /** + * Total number of input and output tokens + */ + total_tokens?: number; + }; + /** + * An array of tool calls requests made during the response generation + */ + tool_calls?: { + /** + * The arguments passed to be passed to the tool call request + */ + arguments?: object; + /** + * The name of the tool to be called + */ + name?: string; + }[]; +} | string | Ai_Cf_Meta_Llama_3_3_70B_Instruct_Fp8_Fast_AsyncResponse; +interface Ai_Cf_Meta_Llama_3_3_70B_Instruct_Fp8_Fast_AsyncResponse { + /** + * The async request id that can be used to obtain the results. + */ + request_id?: string; +} +declare abstract class Base_Ai_Cf_Meta_Llama_3_3_70B_Instruct_Fp8_Fast { + inputs: Ai_Cf_Meta_Llama_3_3_70B_Instruct_Fp8_Fast_Input; + postProcessedOutputs: Ai_Cf_Meta_Llama_3_3_70B_Instruct_Fp8_Fast_Output; +} +interface Ai_Cf_Meta_Llama_Guard_3_8B_Input { + /** + * An array of message objects representing the conversation history. + */ + messages: { + /** + * The role of the message sender must alternate between 'user' and 'assistant'. + */ + role: "user" | "assistant"; + /** + * The content of the message as a string. + */ + content: string; + }[]; + /** + * The maximum number of tokens to generate in the response. + */ + max_tokens?: number; + /** + * Controls the randomness of the output; higher values produce more random results. + */ + temperature?: number; + /** + * Dictate the output format of the generated response. + */ + response_format?: { + /** + * Set to json_object to process and output generated text as JSON. + */ + type?: string; + }; +} +interface Ai_Cf_Meta_Llama_Guard_3_8B_Output { + response?: string | { + /** + * Whether the conversation is safe or not. + */ + safe?: boolean; + /** + * A list of what hazard categories predicted for the conversation, if the conversation is deemed unsafe. + */ + categories?: string[]; + }; + /** + * Usage statistics for the inference request + */ + usage?: { + /** + * Total number of tokens in input + */ + prompt_tokens?: number; + /** + * Total number of tokens in output + */ + completion_tokens?: number; + /** + * Total number of input and output tokens + */ + total_tokens?: number; + }; +} +declare abstract class Base_Ai_Cf_Meta_Llama_Guard_3_8B { + inputs: Ai_Cf_Meta_Llama_Guard_3_8B_Input; + postProcessedOutputs: Ai_Cf_Meta_Llama_Guard_3_8B_Output; +} +interface Ai_Cf_Baai_Bge_Reranker_Base_Input { + /** + * A query you wish to perform against the provided contexts. + */ + /** + * Number of returned results starting with the best score. + */ + top_k?: number; + /** + * List of provided contexts. Note that the index in this array is important, as the response will refer to it. + */ + contexts: { + /** + * One of the provided context content + */ + text?: string; + }[]; +} +interface Ai_Cf_Baai_Bge_Reranker_Base_Output { + response?: { + /** + * Index of the context in the request + */ + id?: number; + /** + * Score of the context under the index. + */ + score?: number; + }[]; +} +declare abstract class Base_Ai_Cf_Baai_Bge_Reranker_Base { + inputs: Ai_Cf_Baai_Bge_Reranker_Base_Input; + postProcessedOutputs: Ai_Cf_Baai_Bge_Reranker_Base_Output; +} +type Ai_Cf_Qwen_Qwen2_5_Coder_32B_Instruct_Input = Ai_Cf_Qwen_Qwen2_5_Coder_32B_Instruct_Prompt | Ai_Cf_Qwen_Qwen2_5_Coder_32B_Instruct_Messages; +interface Ai_Cf_Qwen_Qwen2_5_Coder_32B_Instruct_Prompt { + /** + * The input text prompt for the model to generate a response. + */ + prompt: string; + /** + * Name of the LoRA (Low-Rank Adaptation) model to fine-tune the base model. + */ + lora?: string; + response_format?: Ai_Cf_Qwen_Qwen2_5_Coder_32B_Instruct_JSON_Mode; + /** + * If true, a chat template is not applied and you must adhere to the specific model's expected formatting. + */ + raw?: boolean; + /** + * If true, the response will be streamed back incrementally using SSE, Server Sent Events. + */ + stream?: boolean; + /** + * The maximum number of tokens to generate in the response. + */ + max_tokens?: number; + /** + * Controls the randomness of the output; higher values produce more random results. + */ + temperature?: number; + /** + * Adjusts the creativity of the AI's responses by controlling how many possible words it considers. Lower values make outputs more predictable; higher values allow for more varied and creative responses. + */ + top_p?: number; + /** + * Limits the AI to choose from the top 'k' most probable words. Lower values make responses more focused; higher values introduce more variety and potential surprises. + */ + top_k?: number; + /** + * Random seed for reproducibility of the generation. + */ + seed?: number; + /** + * Penalty for repeated tokens; higher values discourage repetition. + */ + repetition_penalty?: number; + /** + * Decreases the likelihood of the model repeating the same lines verbatim. + */ + frequency_penalty?: number; + /** + * Increases the likelihood of the model introducing new topics. + */ + presence_penalty?: number; +} +interface Ai_Cf_Qwen_Qwen2_5_Coder_32B_Instruct_JSON_Mode { + type?: "json_object" | "json_schema"; + json_schema?: unknown; +} +interface Ai_Cf_Qwen_Qwen2_5_Coder_32B_Instruct_Messages { + /** + * An array of message objects representing the conversation history. + */ + messages: { + /** + * The role of the message sender (e.g., 'user', 'assistant', 'system', 'tool'). + */ + role: string; + /** + * The content of the message as a string. + */ + content: string; + }[]; + functions?: { + name: string; + code: string; + }[]; + /** + * A list of tools available for the assistant to use. + */ + tools?: ({ + /** + * The name of the tool. More descriptive the better. + */ + name: string; + /** + * A brief description of what the tool does. + */ + description: string; + /** + * Schema defining the parameters accepted by the tool. + */ + parameters: { + /** + * The type of the parameters object (usually 'object'). + */ + type: string; + /** + * List of required parameter names. + */ + required?: string[]; + /** + * Definitions of each parameter. + */ + properties: { + [k: string]: { + /** + * The data type of the parameter. + */ + type: string; + /** + * A description of the expected parameter. + */ + description: string; + }; + }; + }; + } | { + /** + * Specifies the type of tool (e.g., 'function'). + */ + type: string; + /** + * Details of the function tool. + */ + function: { + /** + * The name of the function. + */ + name: string; + /** + * A brief description of what the function does. + */ + description: string; + /** + * Schema defining the parameters accepted by the function. + */ + parameters: { + /** + * The type of the parameters object (usually 'object'). + */ + type: string; + /** + * List of required parameter names. + */ + required?: string[]; + /** + * Definitions of each parameter. + */ + properties: { + [k: string]: { + /** + * The data type of the parameter. + */ + type: string; + /** + * A description of the expected parameter. + */ + description: string; + }; + }; + }; + }; + })[]; + response_format?: Ai_Cf_Qwen_Qwen2_5_Coder_32B_Instruct_JSON_Mode_1; + /** + * If true, a chat template is not applied and you must adhere to the specific model's expected formatting. + */ + raw?: boolean; + /** + * If true, the response will be streamed back incrementally using SSE, Server Sent Events. + */ + stream?: boolean; + /** + * The maximum number of tokens to generate in the response. + */ + max_tokens?: number; + /** + * Controls the randomness of the output; higher values produce more random results. + */ + temperature?: number; + /** + * Adjusts the creativity of the AI's responses by controlling how many possible words it considers. Lower values make outputs more predictable; higher values allow for more varied and creative responses. + */ + top_p?: number; + /** + * Limits the AI to choose from the top 'k' most probable words. Lower values make responses more focused; higher values introduce more variety and potential surprises. + */ + top_k?: number; + /** + * Random seed for reproducibility of the generation. + */ + seed?: number; + /** + * Penalty for repeated tokens; higher values discourage repetition. + */ + repetition_penalty?: number; + /** + * Decreases the likelihood of the model repeating the same lines verbatim. + */ + frequency_penalty?: number; + /** + * Increases the likelihood of the model introducing new topics. + */ + presence_penalty?: number; +} +interface Ai_Cf_Qwen_Qwen2_5_Coder_32B_Instruct_JSON_Mode_1 { + type?: "json_object" | "json_schema"; + json_schema?: unknown; +} +type Ai_Cf_Qwen_Qwen2_5_Coder_32B_Instruct_Output = { + /** + * The generated text response from the model + */ + response: string; + /** + * Usage statistics for the inference request + */ + usage?: { + /** + * Total number of tokens in input + */ + prompt_tokens?: number; + /** + * Total number of tokens in output + */ + completion_tokens?: number; + /** + * Total number of input and output tokens + */ + total_tokens?: number; + }; + /** + * An array of tool calls requests made during the response generation + */ + tool_calls?: { + /** + * The arguments passed to be passed to the tool call request + */ + arguments?: object; + /** + * The name of the tool to be called + */ + name?: string; + }[]; +}; +declare abstract class Base_Ai_Cf_Qwen_Qwen2_5_Coder_32B_Instruct { + inputs: Ai_Cf_Qwen_Qwen2_5_Coder_32B_Instruct_Input; + postProcessedOutputs: Ai_Cf_Qwen_Qwen2_5_Coder_32B_Instruct_Output; +} +type Ai_Cf_Qwen_Qwq_32B_Input = Ai_Cf_Qwen_Qwq_32B_Prompt | Ai_Cf_Qwen_Qwq_32B_Messages; +interface Ai_Cf_Qwen_Qwq_32B_Prompt { + /** + * The input text prompt for the model to generate a response. + */ + prompt: string; + /** + * JSON schema that should be fulfilled for the response. + */ + guided_json?: object; + /** + * If true, a chat template is not applied and you must adhere to the specific model's expected formatting. + */ + raw?: boolean; + /** + * If true, the response will be streamed back incrementally using SSE, Server Sent Events. + */ + stream?: boolean; + /** + * The maximum number of tokens to generate in the response. + */ + max_tokens?: number; + /** + * Controls the randomness of the output; higher values produce more random results. + */ + temperature?: number; + /** + * Adjusts the creativity of the AI's responses by controlling how many possible words it considers. Lower values make outputs more predictable; higher values allow for more varied and creative responses. + */ + top_p?: number; + /** + * Limits the AI to choose from the top 'k' most probable words. Lower values make responses more focused; higher values introduce more variety and potential surprises. + */ + top_k?: number; + /** + * Random seed for reproducibility of the generation. + */ + seed?: number; + /** + * Penalty for repeated tokens; higher values discourage repetition. + */ + repetition_penalty?: number; + /** + * Decreases the likelihood of the model repeating the same lines verbatim. + */ + frequency_penalty?: number; + /** + * Increases the likelihood of the model introducing new topics. + */ + presence_penalty?: number; +} +interface Ai_Cf_Qwen_Qwq_32B_Messages { + /** + * An array of message objects representing the conversation history. + */ + messages: { + /** + * The role of the message sender (e.g., 'user', 'assistant', 'system', 'tool'). + */ + role?: string; + /** + * The tool call id. If you don't know what to put here you can fall back to 000000001 + */ + tool_call_id?: string; + content?: string | { + /** + * Type of the content provided + */ + type?: string; + text?: string; + image_url?: { + /** + * image uri with data (e.g. data:image/jpeg;base64,/9j/...). HTTP URL will not be accepted + */ + url?: string; + }; + }[] | { + /** + * Type of the content provided + */ + type?: string; + text?: string; + image_url?: { + /** + * image uri with data (e.g. data:image/jpeg;base64,/9j/...). HTTP URL will not be accepted + */ + url?: string; + }; + }; + }[]; + functions?: { + name: string; + code: string; + }[]; + /** + * A list of tools available for the assistant to use. + */ + tools?: ({ + /** + * The name of the tool. More descriptive the better. + */ + name: string; + /** + * A brief description of what the tool does. + */ + description: string; + /** + * Schema defining the parameters accepted by the tool. + */ + parameters: { + /** + * The type of the parameters object (usually 'object'). + */ + type: string; + /** + * List of required parameter names. + */ + required?: string[]; + /** + * Definitions of each parameter. + */ + properties: { + [k: string]: { + /** + * The data type of the parameter. + */ + type: string; + /** + * A description of the expected parameter. + */ + description: string; + }; + }; + }; + } | { + /** + * Specifies the type of tool (e.g., 'function'). + */ + type: string; + /** + * Details of the function tool. + */ + function: { + /** + * The name of the function. + */ + name: string; + /** + * A brief description of what the function does. + */ + description: string; + /** + * Schema defining the parameters accepted by the function. + */ + parameters: { + /** + * The type of the parameters object (usually 'object'). + */ + type: string; + /** + * List of required parameter names. + */ + required?: string[]; + /** + * Definitions of each parameter. + */ + properties: { + [k: string]: { + /** + * The data type of the parameter. + */ + type: string; + /** + * A description of the expected parameter. + */ + description: string; + }; + }; + }; + }; + })[]; + /** + * JSON schema that should be fufilled for the response. + */ + guided_json?: object; + /** + * If true, a chat template is not applied and you must adhere to the specific model's expected formatting. + */ + raw?: boolean; + /** + * If true, the response will be streamed back incrementally using SSE, Server Sent Events. + */ + stream?: boolean; + /** + * The maximum number of tokens to generate in the response. + */ + max_tokens?: number; + /** + * Controls the randomness of the output; higher values produce more random results. + */ + temperature?: number; + /** + * Adjusts the creativity of the AI's responses by controlling how many possible words it considers. Lower values make outputs more predictable; higher values allow for more varied and creative responses. + */ + top_p?: number; + /** + * Limits the AI to choose from the top 'k' most probable words. Lower values make responses more focused; higher values introduce more variety and potential surprises. + */ + top_k?: number; + /** + * Random seed for reproducibility of the generation. + */ + seed?: number; + /** + * Penalty for repeated tokens; higher values discourage repetition. + */ + repetition_penalty?: number; + /** + * Decreases the likelihood of the model repeating the same lines verbatim. + */ + frequency_penalty?: number; + /** + * Increases the likelihood of the model introducing new topics. + */ + presence_penalty?: number; +} +type Ai_Cf_Qwen_Qwq_32B_Output = { + /** + * The generated text response from the model + */ + response: string; + /** + * Usage statistics for the inference request + */ + usage?: { + /** + * Total number of tokens in input + */ + prompt_tokens?: number; + /** + * Total number of tokens in output + */ + completion_tokens?: number; + /** + * Total number of input and output tokens + */ + total_tokens?: number; + }; + /** + * An array of tool calls requests made during the response generation + */ + tool_calls?: { + /** + * The arguments passed to be passed to the tool call request + */ + arguments?: object; + /** + * The name of the tool to be called + */ + name?: string; + }[]; +}; +declare abstract class Base_Ai_Cf_Qwen_Qwq_32B { + inputs: Ai_Cf_Qwen_Qwq_32B_Input; + postProcessedOutputs: Ai_Cf_Qwen_Qwq_32B_Output; +} +type Ai_Cf_Mistralai_Mistral_Small_3_1_24B_Instruct_Input = Ai_Cf_Mistralai_Mistral_Small_3_1_24B_Instruct_Prompt | Ai_Cf_Mistralai_Mistral_Small_3_1_24B_Instruct_Messages; +interface Ai_Cf_Mistralai_Mistral_Small_3_1_24B_Instruct_Prompt { + /** + * The input text prompt for the model to generate a response. + */ + prompt: string; + /** + * JSON schema that should be fulfilled for the response. + */ + guided_json?: object; + /** + * If true, a chat template is not applied and you must adhere to the specific model's expected formatting. + */ + raw?: boolean; + /** + * If true, the response will be streamed back incrementally using SSE, Server Sent Events. + */ + stream?: boolean; + /** + * The maximum number of tokens to generate in the response. + */ + max_tokens?: number; + /** + * Controls the randomness of the output; higher values produce more random results. + */ + temperature?: number; + /** + * Adjusts the creativity of the AI's responses by controlling how many possible words it considers. Lower values make outputs more predictable; higher values allow for more varied and creative responses. + */ + top_p?: number; + /** + * Limits the AI to choose from the top 'k' most probable words. Lower values make responses more focused; higher values introduce more variety and potential surprises. + */ + top_k?: number; + /** + * Random seed for reproducibility of the generation. + */ + seed?: number; + /** + * Penalty for repeated tokens; higher values discourage repetition. + */ + repetition_penalty?: number; + /** + * Decreases the likelihood of the model repeating the same lines verbatim. + */ + frequency_penalty?: number; + /** + * Increases the likelihood of the model introducing new topics. + */ + presence_penalty?: number; +} +interface Ai_Cf_Mistralai_Mistral_Small_3_1_24B_Instruct_Messages { + /** + * An array of message objects representing the conversation history. + */ + messages: { + /** + * The role of the message sender (e.g., 'user', 'assistant', 'system', 'tool'). + */ + role?: string; + /** + * The tool call id. Must be supplied for tool calls for Mistral-3. If you don't know what to put here you can fall back to 000000001 + */ + tool_call_id?: string; + content?: string | { + /** + * Type of the content provided + */ + type?: string; + text?: string; + image_url?: { + /** + * image uri with data (e.g. data:image/jpeg;base64,/9j/...). HTTP URL will not be accepted + */ + url?: string; + }; + }[] | { + /** + * Type of the content provided + */ + type?: string; + text?: string; + image_url?: { + /** + * image uri with data (e.g. data:image/jpeg;base64,/9j/...). HTTP URL will not be accepted + */ + url?: string; + }; + }; + }[]; + functions?: { + name: string; + code: string; + }[]; + /** + * A list of tools available for the assistant to use. + */ + tools?: ({ + /** + * The name of the tool. More descriptive the better. + */ + name: string; + /** + * A brief description of what the tool does. + */ + description: string; + /** + * Schema defining the parameters accepted by the tool. + */ + parameters: { + /** + * The type of the parameters object (usually 'object'). + */ + type: string; + /** + * List of required parameter names. + */ + required?: string[]; + /** + * Definitions of each parameter. + */ + properties: { + [k: string]: { + /** + * The data type of the parameter. + */ + type: string; + /** + * A description of the expected parameter. + */ + description: string; + }; + }; + }; + } | { + /** + * Specifies the type of tool (e.g., 'function'). + */ + type: string; + /** + * Details of the function tool. + */ + function: { + /** + * The name of the function. + */ + name: string; + /** + * A brief description of what the function does. + */ + description: string; + /** + * Schema defining the parameters accepted by the function. + */ + parameters: { + /** + * The type of the parameters object (usually 'object'). + */ + type: string; + /** + * List of required parameter names. + */ + required?: string[]; + /** + * Definitions of each parameter. + */ + properties: { + [k: string]: { + /** + * The data type of the parameter. + */ + type: string; + /** + * A description of the expected parameter. + */ + description: string; + }; + }; + }; + }; + })[]; + /** + * JSON schema that should be fufilled for the response. + */ + guided_json?: object; + /** + * If true, a chat template is not applied and you must adhere to the specific model's expected formatting. + */ + raw?: boolean; + /** + * If true, the response will be streamed back incrementally using SSE, Server Sent Events. + */ + stream?: boolean; + /** + * The maximum number of tokens to generate in the response. + */ + max_tokens?: number; + /** + * Controls the randomness of the output; higher values produce more random results. + */ + temperature?: number; + /** + * Adjusts the creativity of the AI's responses by controlling how many possible words it considers. Lower values make outputs more predictable; higher values allow for more varied and creative responses. + */ + top_p?: number; + /** + * Limits the AI to choose from the top 'k' most probable words. Lower values make responses more focused; higher values introduce more variety and potential surprises. + */ + top_k?: number; + /** + * Random seed for reproducibility of the generation. + */ + seed?: number; + /** + * Penalty for repeated tokens; higher values discourage repetition. + */ + repetition_penalty?: number; + /** + * Decreases the likelihood of the model repeating the same lines verbatim. + */ + frequency_penalty?: number; + /** + * Increases the likelihood of the model introducing new topics. + */ + presence_penalty?: number; +} +type Ai_Cf_Mistralai_Mistral_Small_3_1_24B_Instruct_Output = { + /** + * The generated text response from the model + */ + response: string; + /** + * Usage statistics for the inference request + */ + usage?: { + /** + * Total number of tokens in input + */ + prompt_tokens?: number; + /** + * Total number of tokens in output + */ + completion_tokens?: number; + /** + * Total number of input and output tokens + */ + total_tokens?: number; + }; + /** + * An array of tool calls requests made during the response generation + */ + tool_calls?: { + /** + * The arguments passed to be passed to the tool call request + */ + arguments?: object; + /** + * The name of the tool to be called + */ + name?: string; + }[]; +}; +declare abstract class Base_Ai_Cf_Mistralai_Mistral_Small_3_1_24B_Instruct { + inputs: Ai_Cf_Mistralai_Mistral_Small_3_1_24B_Instruct_Input; + postProcessedOutputs: Ai_Cf_Mistralai_Mistral_Small_3_1_24B_Instruct_Output; +} +type Ai_Cf_Google_Gemma_3_12B_It_Input = Ai_Cf_Google_Gemma_3_12B_It_Prompt | Ai_Cf_Google_Gemma_3_12B_It_Messages; +interface Ai_Cf_Google_Gemma_3_12B_It_Prompt { + /** + * The input text prompt for the model to generate a response. + */ + prompt: string; + /** + * JSON schema that should be fufilled for the response. + */ + guided_json?: object; + /** + * If true, a chat template is not applied and you must adhere to the specific model's expected formatting. + */ + raw?: boolean; + /** + * If true, the response will be streamed back incrementally using SSE, Server Sent Events. + */ + stream?: boolean; + /** + * The maximum number of tokens to generate in the response. + */ + max_tokens?: number; + /** + * Controls the randomness of the output; higher values produce more random results. + */ + temperature?: number; + /** + * Adjusts the creativity of the AI's responses by controlling how many possible words it considers. Lower values make outputs more predictable; higher values allow for more varied and creative responses. + */ + top_p?: number; + /** + * Limits the AI to choose from the top 'k' most probable words. Lower values make responses more focused; higher values introduce more variety and potential surprises. + */ + top_k?: number; + /** + * Random seed for reproducibility of the generation. + */ + seed?: number; + /** + * Penalty for repeated tokens; higher values discourage repetition. + */ + repetition_penalty?: number; + /** + * Decreases the likelihood of the model repeating the same lines verbatim. + */ + frequency_penalty?: number; + /** + * Increases the likelihood of the model introducing new topics. + */ + presence_penalty?: number; +} +interface Ai_Cf_Google_Gemma_3_12B_It_Messages { + /** + * An array of message objects representing the conversation history. + */ + messages: { + /** + * The role of the message sender (e.g., 'user', 'assistant', 'system', 'tool'). + */ + role?: string; + content?: string | { + /** + * Type of the content provided + */ + type?: string; + text?: string; + image_url?: { + /** + * image uri with data (e.g. data:image/jpeg;base64,/9j/...). HTTP URL will not be accepted + */ + url?: string; + }; + }[]; + }[]; + functions?: { + name: string; + code: string; + }[]; + /** + * A list of tools available for the assistant to use. + */ + tools?: ({ + /** + * The name of the tool. More descriptive the better. + */ + name: string; + /** + * A brief description of what the tool does. + */ + description: string; + /** + * Schema defining the parameters accepted by the tool. + */ + parameters: { + /** + * The type of the parameters object (usually 'object'). + */ + type: string; + /** + * List of required parameter names. + */ + required?: string[]; + /** + * Definitions of each parameter. + */ + properties: { + [k: string]: { + /** + * The data type of the parameter. + */ + type: string; + /** + * A description of the expected parameter. + */ + description: string; + }; + }; + }; + } | { + /** + * Specifies the type of tool (e.g., 'function'). + */ + type: string; + /** + * Details of the function tool. + */ + function: { + /** + * The name of the function. + */ + name: string; + /** + * A brief description of what the function does. + */ + description: string; + /** + * Schema defining the parameters accepted by the function. + */ + parameters: { + /** + * The type of the parameters object (usually 'object'). + */ + type: string; + /** + * List of required parameter names. + */ + required?: string[]; + /** + * Definitions of each parameter. + */ + properties: { + [k: string]: { + /** + * The data type of the parameter. + */ + type: string; + /** + * A description of the expected parameter. + */ + description: string; + }; + }; + }; + }; + })[]; + /** + * JSON schema that should be fufilled for the response. + */ + guided_json?: object; + /** + * If true, a chat template is not applied and you must adhere to the specific model's expected formatting. + */ + raw?: boolean; + /** + * If true, the response will be streamed back incrementally using SSE, Server Sent Events. + */ + stream?: boolean; + /** + * The maximum number of tokens to generate in the response. + */ + max_tokens?: number; + /** + * Controls the randomness of the output; higher values produce more random results. + */ + temperature?: number; + /** + * Adjusts the creativity of the AI's responses by controlling how many possible words it considers. Lower values make outputs more predictable; higher values allow for more varied and creative responses. + */ + top_p?: number; + /** + * Limits the AI to choose from the top 'k' most probable words. Lower values make responses more focused; higher values introduce more variety and potential surprises. + */ + top_k?: number; + /** + * Random seed for reproducibility of the generation. + */ + seed?: number; + /** + * Penalty for repeated tokens; higher values discourage repetition. + */ + repetition_penalty?: number; + /** + * Decreases the likelihood of the model repeating the same lines verbatim. + */ + frequency_penalty?: number; + /** + * Increases the likelihood of the model introducing new topics. + */ + presence_penalty?: number; +} +type Ai_Cf_Google_Gemma_3_12B_It_Output = { + /** + * The generated text response from the model + */ + response: string; + /** + * Usage statistics for the inference request + */ + usage?: { + /** + * Total number of tokens in input + */ + prompt_tokens?: number; + /** + * Total number of tokens in output + */ + completion_tokens?: number; + /** + * Total number of input and output tokens + */ + total_tokens?: number; + }; + /** + * An array of tool calls requests made during the response generation + */ + tool_calls?: { + /** + * The arguments passed to be passed to the tool call request + */ + arguments?: object; + /** + * The name of the tool to be called + */ + name?: string; + }[]; +}; +declare abstract class Base_Ai_Cf_Google_Gemma_3_12B_It { + inputs: Ai_Cf_Google_Gemma_3_12B_It_Input; + postProcessedOutputs: Ai_Cf_Google_Gemma_3_12B_It_Output; +} +type Ai_Cf_Meta_Llama_4_Scout_17B_16E_Instruct_Input = Ai_Cf_Meta_Llama_4_Scout_17B_16E_Instruct_Prompt | Ai_Cf_Meta_Llama_4_Scout_17B_16E_Instruct_Messages | Ai_Cf_Meta_Llama_4_Scout_17B_16E_Instruct_Async_Batch; +interface Ai_Cf_Meta_Llama_4_Scout_17B_16E_Instruct_Prompt { + /** + * The input text prompt for the model to generate a response. + */ + prompt: string; + /** + * JSON schema that should be fulfilled for the response. + */ + guided_json?: object; + response_format?: Ai_Cf_Meta_Llama_4_Scout_17B_16E_Instruct_JSON_Mode; + /** + * If true, a chat template is not applied and you must adhere to the specific model's expected formatting. + */ + raw?: boolean; + /** + * If true, the response will be streamed back incrementally using SSE, Server Sent Events. + */ + stream?: boolean; + /** + * The maximum number of tokens to generate in the response. + */ + max_tokens?: number; + /** + * Controls the randomness of the output; higher values produce more random results. + */ + temperature?: number; + /** + * Adjusts the creativity of the AI's responses by controlling how many possible words it considers. Lower values make outputs more predictable; higher values allow for more varied and creative responses. + */ + top_p?: number; + /** + * Limits the AI to choose from the top 'k' most probable words. Lower values make responses more focused; higher values introduce more variety and potential surprises. + */ + top_k?: number; + /** + * Random seed for reproducibility of the generation. + */ + seed?: number; + /** + * Penalty for repeated tokens; higher values discourage repetition. + */ + repetition_penalty?: number; + /** + * Decreases the likelihood of the model repeating the same lines verbatim. + */ + frequency_penalty?: number; + /** + * Increases the likelihood of the model introducing new topics. + */ + presence_penalty?: number; +} +interface Ai_Cf_Meta_Llama_4_Scout_17B_16E_Instruct_JSON_Mode { + type?: "json_object" | "json_schema"; + json_schema?: unknown; +} +interface Ai_Cf_Meta_Llama_4_Scout_17B_16E_Instruct_Messages { + /** + * An array of message objects representing the conversation history. + */ + messages: { + /** + * The role of the message sender (e.g., 'user', 'assistant', 'system', 'tool'). + */ + role?: string; + /** + * The tool call id. If you don't know what to put here you can fall back to 000000001 + */ + tool_call_id?: string; + content?: string | { + /** + * Type of the content provided + */ + type?: string; + text?: string; + image_url?: { + /** + * image uri with data (e.g. data:image/jpeg;base64,/9j/...). HTTP URL will not be accepted + */ + url?: string; + }; + }[] | { + /** + * Type of the content provided + */ + type?: string; + text?: string; + image_url?: { + /** + * image uri with data (e.g. data:image/jpeg;base64,/9j/...). HTTP URL will not be accepted + */ + url?: string; + }; + }; + }[]; + functions?: { + name: string; + code: string; + }[]; + /** + * A list of tools available for the assistant to use. + */ + tools?: ({ + /** + * The name of the tool. More descriptive the better. + */ + name: string; + /** + * A brief description of what the tool does. + */ + description: string; + /** + * Schema defining the parameters accepted by the tool. + */ + parameters: { + /** + * The type of the parameters object (usually 'object'). + */ + type: string; + /** + * List of required parameter names. + */ + required?: string[]; + /** + * Definitions of each parameter. + */ + properties: { + [k: string]: { + /** + * The data type of the parameter. + */ + type: string; + /** + * A description of the expected parameter. + */ + description: string; + }; + }; + }; + } | { + /** + * Specifies the type of tool (e.g., 'function'). + */ + type: string; + /** + * Details of the function tool. + */ + function: { + /** + * The name of the function. + */ + name: string; + /** + * A brief description of what the function does. + */ + description: string; + /** + * Schema defining the parameters accepted by the function. + */ + parameters: { + /** + * The type of the parameters object (usually 'object'). + */ + type: string; + /** + * List of required parameter names. + */ + required?: string[]; + /** + * Definitions of each parameter. + */ + properties: { + [k: string]: { + /** + * The data type of the parameter. + */ + type: string; + /** + * A description of the expected parameter. + */ + description: string; + }; + }; + }; + }; + })[]; + response_format?: Ai_Cf_Meta_Llama_4_Scout_17B_16E_Instruct_JSON_Mode; + /** + * JSON schema that should be fufilled for the response. + */ + guided_json?: object; + /** + * If true, a chat template is not applied and you must adhere to the specific model's expected formatting. + */ + raw?: boolean; + /** + * If true, the response will be streamed back incrementally using SSE, Server Sent Events. + */ + stream?: boolean; + /** + * The maximum number of tokens to generate in the response. + */ + max_tokens?: number; + /** + * Controls the randomness of the output; higher values produce more random results. + */ + temperature?: number; + /** + * Adjusts the creativity of the AI's responses by controlling how many possible words it considers. Lower values make outputs more predictable; higher values allow for more varied and creative responses. + */ + top_p?: number; + /** + * Limits the AI to choose from the top 'k' most probable words. Lower values make responses more focused; higher values introduce more variety and potential surprises. + */ + top_k?: number; + /** + * Random seed for reproducibility of the generation. + */ + seed?: number; + /** + * Penalty for repeated tokens; higher values discourage repetition. + */ + repetition_penalty?: number; + /** + * Decreases the likelihood of the model repeating the same lines verbatim. + */ + frequency_penalty?: number; + /** + * Increases the likelihood of the model introducing new topics. + */ + presence_penalty?: number; +} +interface Ai_Cf_Meta_Llama_4_Scout_17B_16E_Instruct_Async_Batch { + requests: (Ai_Cf_Meta_Llama_4_Scout_17B_16E_Instruct_Prompt_Inner | Ai_Cf_Meta_Llama_4_Scout_17B_16E_Instruct_Messages_Inner)[]; +} +interface Ai_Cf_Meta_Llama_4_Scout_17B_16E_Instruct_Prompt_Inner { + /** + * The input text prompt for the model to generate a response. + */ + prompt: string; + /** + * JSON schema that should be fulfilled for the response. + */ + guided_json?: object; + response_format?: Ai_Cf_Meta_Llama_4_Scout_17B_16E_Instruct_JSON_Mode; + /** + * If true, a chat template is not applied and you must adhere to the specific model's expected formatting. + */ + raw?: boolean; + /** + * If true, the response will be streamed back incrementally using SSE, Server Sent Events. + */ + stream?: boolean; + /** + * The maximum number of tokens to generate in the response. + */ + max_tokens?: number; + /** + * Controls the randomness of the output; higher values produce more random results. + */ + temperature?: number; + /** + * Adjusts the creativity of the AI's responses by controlling how many possible words it considers. Lower values make outputs more predictable; higher values allow for more varied and creative responses. + */ + top_p?: number; + /** + * Limits the AI to choose from the top 'k' most probable words. Lower values make responses more focused; higher values introduce more variety and potential surprises. + */ + top_k?: number; + /** + * Random seed for reproducibility of the generation. + */ + seed?: number; + /** + * Penalty for repeated tokens; higher values discourage repetition. + */ + repetition_penalty?: number; + /** + * Decreases the likelihood of the model repeating the same lines verbatim. + */ + frequency_penalty?: number; + /** + * Increases the likelihood of the model introducing new topics. + */ + presence_penalty?: number; +} +interface Ai_Cf_Meta_Llama_4_Scout_17B_16E_Instruct_Messages_Inner { + /** + * An array of message objects representing the conversation history. + */ + messages: { + /** + * The role of the message sender (e.g., 'user', 'assistant', 'system', 'tool'). + */ + role?: string; + /** + * The tool call id. If you don't know what to put here you can fall back to 000000001 + */ + tool_call_id?: string; + content?: string | { + /** + * Type of the content provided + */ + type?: string; + text?: string; + image_url?: { + /** + * image uri with data (e.g. data:image/jpeg;base64,/9j/...). HTTP URL will not be accepted + */ + url?: string; + }; + }[] | { + /** + * Type of the content provided + */ + type?: string; + text?: string; + image_url?: { + /** + * image uri with data (e.g. data:image/jpeg;base64,/9j/...). HTTP URL will not be accepted + */ + url?: string; + }; + }; + }[]; + functions?: { + name: string; + code: string; + }[]; + /** + * A list of tools available for the assistant to use. + */ + tools?: ({ + /** + * The name of the tool. More descriptive the better. + */ + name: string; + /** + * A brief description of what the tool does. + */ + description: string; + /** + * Schema defining the parameters accepted by the tool. + */ + parameters: { + /** + * The type of the parameters object (usually 'object'). + */ + type: string; + /** + * List of required parameter names. + */ + required?: string[]; + /** + * Definitions of each parameter. + */ + properties: { + [k: string]: { + /** + * The data type of the parameter. + */ + type: string; + /** + * A description of the expected parameter. + */ + description: string; + }; + }; + }; + } | { + /** + * Specifies the type of tool (e.g., 'function'). + */ + type: string; + /** + * Details of the function tool. + */ + function: { + /** + * The name of the function. + */ + name: string; + /** + * A brief description of what the function does. + */ + description: string; + /** + * Schema defining the parameters accepted by the function. + */ + parameters: { + /** + * The type of the parameters object (usually 'object'). + */ + type: string; + /** + * List of required parameter names. + */ + required?: string[]; + /** + * Definitions of each parameter. + */ + properties: { + [k: string]: { + /** + * The data type of the parameter. + */ + type: string; + /** + * A description of the expected parameter. + */ + description: string; + }; + }; + }; + }; + })[]; + response_format?: Ai_Cf_Meta_Llama_4_Scout_17B_16E_Instruct_JSON_Mode; + /** + * JSON schema that should be fufilled for the response. + */ + guided_json?: object; + /** + * If true, a chat template is not applied and you must adhere to the specific model's expected formatting. + */ + raw?: boolean; + /** + * If true, the response will be streamed back incrementally using SSE, Server Sent Events. + */ + stream?: boolean; + /** + * The maximum number of tokens to generate in the response. + */ + max_tokens?: number; + /** + * Controls the randomness of the output; higher values produce more random results. + */ + temperature?: number; + /** + * Adjusts the creativity of the AI's responses by controlling how many possible words it considers. Lower values make outputs more predictable; higher values allow for more varied and creative responses. + */ + top_p?: number; + /** + * Limits the AI to choose from the top 'k' most probable words. Lower values make responses more focused; higher values introduce more variety and potential surprises. + */ + top_k?: number; + /** + * Random seed for reproducibility of the generation. + */ + seed?: number; + /** + * Penalty for repeated tokens; higher values discourage repetition. + */ + repetition_penalty?: number; + /** + * Decreases the likelihood of the model repeating the same lines verbatim. + */ + frequency_penalty?: number; + /** + * Increases the likelihood of the model introducing new topics. + */ + presence_penalty?: number; +} +type Ai_Cf_Meta_Llama_4_Scout_17B_16E_Instruct_Output = { + /** + * The generated text response from the model + */ + response: string; + /** + * Usage statistics for the inference request + */ + usage?: { + /** + * Total number of tokens in input + */ + prompt_tokens?: number; + /** + * Total number of tokens in output + */ + completion_tokens?: number; + /** + * Total number of input and output tokens + */ + total_tokens?: number; + }; + /** + * An array of tool calls requests made during the response generation + */ + tool_calls?: { + /** + * The tool call id. + */ + id?: string; + /** + * Specifies the type of tool (e.g., 'function'). + */ + type?: string; + /** + * Details of the function tool. + */ + function?: { + /** + * The name of the tool to be called + */ + name?: string; + /** + * The arguments passed to be passed to the tool call request + */ + arguments?: object; + }; + }[]; +}; +declare abstract class Base_Ai_Cf_Meta_Llama_4_Scout_17B_16E_Instruct { + inputs: Ai_Cf_Meta_Llama_4_Scout_17B_16E_Instruct_Input; + postProcessedOutputs: Ai_Cf_Meta_Llama_4_Scout_17B_16E_Instruct_Output; +} +type Ai_Cf_Qwen_Qwen3_30B_A3B_Fp8_Input = Ai_Cf_Qwen_Qwen3_30B_A3B_Fp8_Prompt | Ai_Cf_Qwen_Qwen3_30B_A3B_Fp8_Messages | Ai_Cf_Qwen_Qwen3_30B_A3B_Fp8_Async_Batch; +interface Ai_Cf_Qwen_Qwen3_30B_A3B_Fp8_Prompt { + /** + * The input text prompt for the model to generate a response. + */ + prompt: string; + /** + * Name of the LoRA (Low-Rank Adaptation) model to fine-tune the base model. + */ + lora?: string; + response_format?: Ai_Cf_Qwen_Qwen3_30B_A3B_Fp8_JSON_Mode; + /** + * If true, a chat template is not applied and you must adhere to the specific model's expected formatting. + */ + raw?: boolean; + /** + * If true, the response will be streamed back incrementally using SSE, Server Sent Events. + */ + stream?: boolean; + /** + * The maximum number of tokens to generate in the response. + */ + max_tokens?: number; + /** + * Controls the randomness of the output; higher values produce more random results. + */ + temperature?: number; + /** + * Adjusts the creativity of the AI's responses by controlling how many possible words it considers. Lower values make outputs more predictable; higher values allow for more varied and creative responses. + */ + top_p?: number; + /** + * Limits the AI to choose from the top 'k' most probable words. Lower values make responses more focused; higher values introduce more variety and potential surprises. + */ + top_k?: number; + /** + * Random seed for reproducibility of the generation. + */ + seed?: number; + /** + * Penalty for repeated tokens; higher values discourage repetition. + */ + repetition_penalty?: number; + /** + * Decreases the likelihood of the model repeating the same lines verbatim. + */ + frequency_penalty?: number; + /** + * Increases the likelihood of the model introducing new topics. + */ + presence_penalty?: number; +} +interface Ai_Cf_Qwen_Qwen3_30B_A3B_Fp8_JSON_Mode { + type?: "json_object" | "json_schema"; + json_schema?: unknown; +} +interface Ai_Cf_Qwen_Qwen3_30B_A3B_Fp8_Messages { + /** + * An array of message objects representing the conversation history. + */ + messages: { + /** + * The role of the message sender (e.g., 'user', 'assistant', 'system', 'tool'). + */ + role: string; + content: string | { + /** + * Type of the content (text) + */ + type?: string; + /** + * Text content + */ + text?: string; + }[]; + }[]; + functions?: { + name: string; + code: string; + }[]; + /** + * A list of tools available for the assistant to use. + */ + tools?: ({ + /** + * The name of the tool. More descriptive the better. + */ + name: string; + /** + * A brief description of what the tool does. + */ + description: string; + /** + * Schema defining the parameters accepted by the tool. + */ + parameters: { + /** + * The type of the parameters object (usually 'object'). + */ + type: string; + /** + * List of required parameter names. + */ + required?: string[]; + /** + * Definitions of each parameter. + */ + properties: { + [k: string]: { + /** + * The data type of the parameter. + */ + type: string; + /** + * A description of the expected parameter. + */ + description: string; + }; + }; + }; + } | { + /** + * Specifies the type of tool (e.g., 'function'). + */ + type: string; + /** + * Details of the function tool. + */ + function: { + /** + * The name of the function. + */ + name: string; + /** + * A brief description of what the function does. + */ + description: string; + /** + * Schema defining the parameters accepted by the function. + */ + parameters: { + /** + * The type of the parameters object (usually 'object'). + */ + type: string; + /** + * List of required parameter names. + */ + required?: string[]; + /** + * Definitions of each parameter. + */ + properties: { + [k: string]: { + /** + * The data type of the parameter. + */ + type: string; + /** + * A description of the expected parameter. + */ + description: string; + }; + }; + }; + }; + })[]; + response_format?: Ai_Cf_Qwen_Qwen3_30B_A3B_Fp8_JSON_Mode_1; + /** + * If true, a chat template is not applied and you must adhere to the specific model's expected formatting. + */ + raw?: boolean; + /** + * If true, the response will be streamed back incrementally using SSE, Server Sent Events. + */ + stream?: boolean; + /** + * The maximum number of tokens to generate in the response. + */ + max_tokens?: number; + /** + * Controls the randomness of the output; higher values produce more random results. + */ + temperature?: number; + /** + * Adjusts the creativity of the AI's responses by controlling how many possible words it considers. Lower values make outputs more predictable; higher values allow for more varied and creative responses. + */ + top_p?: number; + /** + * Limits the AI to choose from the top 'k' most probable words. Lower values make responses more focused; higher values introduce more variety and potential surprises. + */ + top_k?: number; + /** + * Random seed for reproducibility of the generation. + */ + seed?: number; + /** + * Penalty for repeated tokens; higher values discourage repetition. + */ + repetition_penalty?: number; + /** + * Decreases the likelihood of the model repeating the same lines verbatim. + */ + frequency_penalty?: number; + /** + * Increases the likelihood of the model introducing new topics. + */ + presence_penalty?: number; +} +interface Ai_Cf_Qwen_Qwen3_30B_A3B_Fp8_JSON_Mode_1 { + type?: "json_object" | "json_schema"; + json_schema?: unknown; +} +interface Ai_Cf_Qwen_Qwen3_30B_A3B_Fp8_Async_Batch { + requests: (Ai_Cf_Qwen_Qwen3_30B_A3B_Fp8_Prompt_1 | Ai_Cf_Qwen_Qwen3_30B_A3B_Fp8_Messages_1)[]; +} +interface Ai_Cf_Qwen_Qwen3_30B_A3B_Fp8_Prompt_1 { + /** + * The input text prompt for the model to generate a response. + */ + prompt: string; + /** + * Name of the LoRA (Low-Rank Adaptation) model to fine-tune the base model. + */ + lora?: string; + response_format?: Ai_Cf_Qwen_Qwen3_30B_A3B_Fp8_JSON_Mode_2; + /** + * If true, a chat template is not applied and you must adhere to the specific model's expected formatting. + */ + raw?: boolean; + /** + * If true, the response will be streamed back incrementally using SSE, Server Sent Events. + */ + stream?: boolean; + /** + * The maximum number of tokens to generate in the response. + */ + max_tokens?: number; + /** + * Controls the randomness of the output; higher values produce more random results. + */ + temperature?: number; + /** + * Adjusts the creativity of the AI's responses by controlling how many possible words it considers. Lower values make outputs more predictable; higher values allow for more varied and creative responses. + */ + top_p?: number; + /** + * Limits the AI to choose from the top 'k' most probable words. Lower values make responses more focused; higher values introduce more variety and potential surprises. + */ + top_k?: number; + /** + * Random seed for reproducibility of the generation. + */ + seed?: number; + /** + * Penalty for repeated tokens; higher values discourage repetition. + */ + repetition_penalty?: number; + /** + * Decreases the likelihood of the model repeating the same lines verbatim. + */ + frequency_penalty?: number; + /** + * Increases the likelihood of the model introducing new topics. + */ + presence_penalty?: number; +} +interface Ai_Cf_Qwen_Qwen3_30B_A3B_Fp8_JSON_Mode_2 { + type?: "json_object" | "json_schema"; + json_schema?: unknown; +} +interface Ai_Cf_Qwen_Qwen3_30B_A3B_Fp8_Messages_1 { + /** + * An array of message objects representing the conversation history. + */ + messages: { + /** + * The role of the message sender (e.g., 'user', 'assistant', 'system', 'tool'). + */ + role: string; + content: string | { + /** + * Type of the content (text) + */ + type?: string; + /** + * Text content + */ + text?: string; + }[]; + }[]; + functions?: { + name: string; + code: string; + }[]; + /** + * A list of tools available for the assistant to use. + */ + tools?: ({ + /** + * The name of the tool. More descriptive the better. + */ + name: string; + /** + * A brief description of what the tool does. + */ + description: string; + /** + * Schema defining the parameters accepted by the tool. + */ + parameters: { + /** + * The type of the parameters object (usually 'object'). + */ + type: string; + /** + * List of required parameter names. + */ + required?: string[]; + /** + * Definitions of each parameter. + */ + properties: { + [k: string]: { + /** + * The data type of the parameter. + */ + type: string; + /** + * A description of the expected parameter. + */ + description: string; + }; + }; + }; + } | { + /** + * Specifies the type of tool (e.g., 'function'). + */ + type: string; + /** + * Details of the function tool. + */ + function: { + /** + * The name of the function. + */ + name: string; + /** + * A brief description of what the function does. + */ + description: string; + /** + * Schema defining the parameters accepted by the function. + */ + parameters: { + /** + * The type of the parameters object (usually 'object'). + */ + type: string; + /** + * List of required parameter names. + */ + required?: string[]; + /** + * Definitions of each parameter. + */ + properties: { + [k: string]: { + /** + * The data type of the parameter. + */ + type: string; + /** + * A description of the expected parameter. + */ + description: string; + }; + }; + }; + }; + })[]; + response_format?: Ai_Cf_Qwen_Qwen3_30B_A3B_Fp8_JSON_Mode_3; + /** + * If true, a chat template is not applied and you must adhere to the specific model's expected formatting. + */ + raw?: boolean; + /** + * If true, the response will be streamed back incrementally using SSE, Server Sent Events. + */ + stream?: boolean; + /** + * The maximum number of tokens to generate in the response. + */ + max_tokens?: number; + /** + * Controls the randomness of the output; higher values produce more random results. + */ + temperature?: number; + /** + * Adjusts the creativity of the AI's responses by controlling how many possible words it considers. Lower values make outputs more predictable; higher values allow for more varied and creative responses. + */ + top_p?: number; + /** + * Limits the AI to choose from the top 'k' most probable words. Lower values make responses more focused; higher values introduce more variety and potential surprises. + */ + top_k?: number; + /** + * Random seed for reproducibility of the generation. + */ + seed?: number; + /** + * Penalty for repeated tokens; higher values discourage repetition. + */ + repetition_penalty?: number; + /** + * Decreases the likelihood of the model repeating the same lines verbatim. + */ + frequency_penalty?: number; + /** + * Increases the likelihood of the model introducing new topics. + */ + presence_penalty?: number; +} +interface Ai_Cf_Qwen_Qwen3_30B_A3B_Fp8_JSON_Mode_3 { + type?: "json_object" | "json_schema"; + json_schema?: unknown; +} +type Ai_Cf_Qwen_Qwen3_30B_A3B_Fp8_Output = Ai_Cf_Qwen_Qwen3_30B_A3B_Fp8_Chat_Completion_Response | Ai_Cf_Qwen_Qwen3_30B_A3B_Fp8_Text_Completion_Response | string | Ai_Cf_Qwen_Qwen3_30B_A3B_Fp8_AsyncResponse; +interface Ai_Cf_Qwen_Qwen3_30B_A3B_Fp8_Chat_Completion_Response { + /** + * Unique identifier for the completion + */ + id?: string; + /** + * Object type identifier + */ + object?: "chat.completion"; + /** + * Unix timestamp of when the completion was created + */ + created?: number; + /** + * Model used for the completion + */ + model?: string; + /** + * List of completion choices + */ + choices?: { + /** + * Index of the choice in the list + */ + index?: number; + /** + * The message generated by the model + */ + message?: { + /** + * Role of the message author + */ + role: string; + /** + * The content of the message + */ + content: string; + /** + * Internal reasoning content (if available) + */ + reasoning_content?: string; + /** + * Tool calls made by the assistant + */ + tool_calls?: { + /** + * Unique identifier for the tool call + */ + id: string; + /** + * Type of tool call + */ + type: "function"; + function: { + /** + * Name of the function to call + */ + name: string; + /** + * JSON string of arguments for the function + */ + arguments: string; + }; + }[]; + }; + /** + * Reason why the model stopped generating + */ + finish_reason?: string; + /** + * Stop reason (may be null) + */ + stop_reason?: string | null; + /** + * Log probabilities (if requested) + */ + logprobs?: {} | null; + }[]; + /** + * Usage statistics for the inference request + */ + usage?: { + /** + * Total number of tokens in input + */ + prompt_tokens?: number; + /** + * Total number of tokens in output + */ + completion_tokens?: number; + /** + * Total number of input and output tokens + */ + total_tokens?: number; + }; + /** + * Log probabilities for the prompt (if requested) + */ + prompt_logprobs?: {} | null; +} +interface Ai_Cf_Qwen_Qwen3_30B_A3B_Fp8_Text_Completion_Response { + /** + * Unique identifier for the completion + */ + id?: string; + /** + * Object type identifier + */ + object?: "text_completion"; + /** + * Unix timestamp of when the completion was created + */ + created?: number; + /** + * Model used for the completion + */ + model?: string; + /** + * List of completion choices + */ + choices?: { + /** + * Index of the choice in the list + */ + index: number; + /** + * The generated text completion + */ + text: string; + /** + * Reason why the model stopped generating + */ + finish_reason: string; + /** + * Stop reason (may be null) + */ + stop_reason?: string | null; + /** + * Log probabilities (if requested) + */ + logprobs?: {} | null; + /** + * Log probabilities for the prompt (if requested) + */ + prompt_logprobs?: {} | null; + }[]; + /** + * Usage statistics for the inference request + */ + usage?: { + /** + * Total number of tokens in input + */ + prompt_tokens?: number; + /** + * Total number of tokens in output + */ + completion_tokens?: number; + /** + * Total number of input and output tokens + */ + total_tokens?: number; + }; +} +interface Ai_Cf_Qwen_Qwen3_30B_A3B_Fp8_AsyncResponse { + /** + * The async request id that can be used to obtain the results. + */ + request_id?: string; +} +declare abstract class Base_Ai_Cf_Qwen_Qwen3_30B_A3B_Fp8 { + inputs: Ai_Cf_Qwen_Qwen3_30B_A3B_Fp8_Input; + postProcessedOutputs: Ai_Cf_Qwen_Qwen3_30B_A3B_Fp8_Output; +} +interface Ai_Cf_Deepgram_Nova_3_Input { + audio: { + body: object; + contentType: string; + }; + /** + * Sets how the model will interpret strings submitted to the custom_topic param. When strict, the model will only return topics submitted using the custom_topic param. When extended, the model will return its own detected topics in addition to those submitted using the custom_topic param. + */ + custom_topic_mode?: "extended" | "strict"; + /** + * Custom topics you want the model to detect within your input audio or text if present Submit up to 100 + */ + custom_topic?: string; + /** + * Sets how the model will interpret intents submitted to the custom_intent param. When strict, the model will only return intents submitted using the custom_intent param. When extended, the model will return its own detected intents in addition those submitted using the custom_intents param + */ + custom_intent_mode?: "extended" | "strict"; + /** + * Custom intents you want the model to detect within your input audio if present + */ + custom_intent?: string; + /** + * Identifies and extracts key entities from content in submitted audio + */ + detect_entities?: boolean; + /** + * Identifies the dominant language spoken in submitted audio + */ + detect_language?: boolean; + /** + * Recognize speaker changes. Each word in the transcript will be assigned a speaker number starting at 0 + */ + diarize?: boolean; + /** + * Identify and extract key entities from content in submitted audio + */ + dictation?: boolean; + /** + * Specify the expected encoding of your submitted audio + */ + encoding?: "linear16" | "flac" | "mulaw" | "amr-nb" | "amr-wb" | "opus" | "speex" | "g729"; + /** + * Arbitrary key-value pairs that are attached to the API response for usage in downstream processing + */ + extra?: string; + /** + * Filler Words can help transcribe interruptions in your audio, like 'uh' and 'um' + */ + filler_words?: boolean; + /** + * Key term prompting can boost or suppress specialized terminology and brands. + */ + keyterm?: string; + /** + * Keywords can boost or suppress specialized terminology and brands. + */ + keywords?: string; + /** + * The BCP-47 language tag that hints at the primary spoken language. Depending on the Model and API endpoint you choose only certain languages are available. + */ + language?: string; + /** + * Spoken measurements will be converted to their corresponding abbreviations. + */ + measurements?: boolean; + /** + * Opts out requests from the Deepgram Model Improvement Program. Refer to our Docs for pricing impacts before setting this to true. https://dpgr.am/deepgram-mip. + */ + mip_opt_out?: boolean; + /** + * Mode of operation for the model representing broad area of topic that will be talked about in the supplied audio + */ + mode?: "general" | "medical" | "finance"; + /** + * Transcribe each audio channel independently. + */ + multichannel?: boolean; + /** + * Numerals converts numbers from written format to numerical format. + */ + numerals?: boolean; + /** + * Splits audio into paragraphs to improve transcript readability. + */ + paragraphs?: boolean; + /** + * Profanity Filter looks for recognized profanity and converts it to the nearest recognized non-profane word or removes it from the transcript completely. + */ + profanity_filter?: boolean; + /** + * Add punctuation and capitalization to the transcript. + */ + punctuate?: boolean; + /** + * Redaction removes sensitive information from your transcripts. + */ + redact?: string; + /** + * Search for terms or phrases in submitted audio and replaces them. + */ + replace?: string; + /** + * Search for terms or phrases in submitted audio. + */ + search?: string; + /** + * Recognizes the sentiment throughout a transcript or text. + */ + sentiment?: boolean; + /** + * Apply formatting to transcript output. When set to true, additional formatting will be applied to transcripts to improve readability. + */ + smart_format?: boolean; + /** + * Detect topics throughout a transcript or text. + */ + topics?: boolean; + /** + * Segments speech into meaningful semantic units. + */ + utterances?: boolean; + /** + * Seconds to wait before detecting a pause between words in submitted audio. + */ + utt_split?: number; + /** + * The number of channels in the submitted audio + */ + channels?: number; + /** + * Specifies whether the streaming endpoint should provide ongoing transcription updates as more audio is received. When set to true, the endpoint sends continuous updates, meaning transcription results may evolve over time. Note: Supported only for webosockets. + */ + interim_results?: boolean; + /** + * Indicates how long model will wait to detect whether a speaker has finished speaking or pauses for a significant period of time. When set to a value, the streaming endpoint immediately finalizes the transcription for the processed time range and returns the transcript with a speech_final parameter set to true. Can also be set to false to disable endpointing + */ + endpointing?: string; + /** + * Indicates that speech has started. You'll begin receiving Speech Started messages upon speech starting. Note: Supported only for webosockets. + */ + vad_events?: boolean; + /** + * Indicates how long model will wait to send an UtteranceEnd message after a word has been transcribed. Use with interim_results. Note: Supported only for webosockets. + */ + utterance_end_ms?: boolean; +} +interface Ai_Cf_Deepgram_Nova_3_Output { + results?: { + channels?: { + alternatives?: { + confidence?: number; + transcript?: string; + words?: { + confidence?: number; + end?: number; + start?: number; + word?: string; + }[]; + }[]; + }[]; + summary?: { + result?: string; + short?: string; + }; + sentiments?: { + segments?: { + text?: string; + start_word?: number; + end_word?: number; + sentiment?: string; + sentiment_score?: number; + }[]; + average?: { + sentiment?: string; + sentiment_score?: number; + }; + }; + }; +} +declare abstract class Base_Ai_Cf_Deepgram_Nova_3 { + inputs: Ai_Cf_Deepgram_Nova_3_Input; + postProcessedOutputs: Ai_Cf_Deepgram_Nova_3_Output; +} +interface Ai_Cf_Qwen_Qwen3_Embedding_0_6B_Input { + queries?: string | string[]; + /** + * Optional instruction for the task + */ + instruction?: string; + documents?: string | string[]; + text?: string | string[]; +} +interface Ai_Cf_Qwen_Qwen3_Embedding_0_6B_Output { + data?: number[][]; + shape?: number[]; +} +declare abstract class Base_Ai_Cf_Qwen_Qwen3_Embedding_0_6B { + inputs: Ai_Cf_Qwen_Qwen3_Embedding_0_6B_Input; + postProcessedOutputs: Ai_Cf_Qwen_Qwen3_Embedding_0_6B_Output; +} +type Ai_Cf_Pipecat_Ai_Smart_Turn_V2_Input = { + /** + * readable stream with audio data and content-type specified for that data + */ + audio: { + body: object; + contentType: string; + }; + /** + * type of data PCM data that's sent to the inference server as raw array + */ + dtype?: "uint8" | "float32" | "float64"; +} | { + /** + * base64 encoded audio data + */ + audio: string; + /** + * type of data PCM data that's sent to the inference server as raw array + */ + dtype?: "uint8" | "float32" | "float64"; +}; +interface Ai_Cf_Pipecat_Ai_Smart_Turn_V2_Output { + /** + * if true, end-of-turn was detected + */ + is_complete?: boolean; + /** + * probability of the end-of-turn detection + */ + probability?: number; +} +declare abstract class Base_Ai_Cf_Pipecat_Ai_Smart_Turn_V2 { + inputs: Ai_Cf_Pipecat_Ai_Smart_Turn_V2_Input; + postProcessedOutputs: Ai_Cf_Pipecat_Ai_Smart_Turn_V2_Output; +} +declare abstract class Base_Ai_Cf_Openai_Gpt_Oss_120B { + inputs: XOR; + postProcessedOutputs: XOR; +} +declare abstract class Base_Ai_Cf_Openai_Gpt_Oss_20B { + inputs: XOR; + postProcessedOutputs: XOR; +} +interface Ai_Cf_Leonardo_Phoenix_1_0_Input { + /** + * A text description of the image you want to generate. + */ + prompt: string; + /** + * Controls how closely the generated image should adhere to the prompt; higher values make the image more aligned with the prompt + */ + guidance?: number; + /** + * Random seed for reproducibility of the image generation + */ + seed?: number; + /** + * The height of the generated image in pixels + */ + height?: number; + /** + * The width of the generated image in pixels + */ + width?: number; + /** + * The number of diffusion steps; higher values can improve quality but take longer + */ + num_steps?: number; + /** + * Specify what to exclude from the generated images + */ + negative_prompt?: string; +} +/** + * The generated image in JPEG format + */ +type Ai_Cf_Leonardo_Phoenix_1_0_Output = string; +declare abstract class Base_Ai_Cf_Leonardo_Phoenix_1_0 { + inputs: Ai_Cf_Leonardo_Phoenix_1_0_Input; + postProcessedOutputs: Ai_Cf_Leonardo_Phoenix_1_0_Output; +} +interface Ai_Cf_Leonardo_Lucid_Origin_Input { + /** + * A text description of the image you want to generate. + */ + prompt: string; + /** + * Controls how closely the generated image should adhere to the prompt; higher values make the image more aligned with the prompt + */ + guidance?: number; + /** + * Random seed for reproducibility of the image generation + */ + seed?: number; + /** + * The height of the generated image in pixels + */ + height?: number; + /** + * The width of the generated image in pixels + */ + width?: number; + /** + * The number of diffusion steps; higher values can improve quality but take longer + */ + num_steps?: number; + /** + * The number of diffusion steps; higher values can improve quality but take longer + */ + steps?: number; +} +interface Ai_Cf_Leonardo_Lucid_Origin_Output { + /** + * The generated image in Base64 format. + */ + image?: string; +} +declare abstract class Base_Ai_Cf_Leonardo_Lucid_Origin { + inputs: Ai_Cf_Leonardo_Lucid_Origin_Input; + postProcessedOutputs: Ai_Cf_Leonardo_Lucid_Origin_Output; +} +interface Ai_Cf_Deepgram_Aura_1_Input { + /** + * Speaker used to produce the audio. + */ + speaker?: "angus" | "asteria" | "arcas" | "orion" | "orpheus" | "athena" | "luna" | "zeus" | "perseus" | "helios" | "hera" | "stella"; + /** + * Encoding of the output audio. + */ + encoding?: "linear16" | "flac" | "mulaw" | "alaw" | "mp3" | "opus" | "aac"; + /** + * Container specifies the file format wrapper for the output audio. The available options depend on the encoding type.. + */ + container?: "none" | "wav" | "ogg"; + /** + * The text content to be converted to speech + */ + text: string; + /** + * Sample Rate specifies the sample rate for the output audio. Based on the encoding, different sample rates are supported. For some encodings, the sample rate is not configurable + */ + sample_rate?: number; + /** + * The bitrate of the audio in bits per second. Choose from predefined ranges or specific values based on the encoding type. + */ + bit_rate?: number; +} +/** + * The generated audio in MP3 format + */ +type Ai_Cf_Deepgram_Aura_1_Output = string; +declare abstract class Base_Ai_Cf_Deepgram_Aura_1 { + inputs: Ai_Cf_Deepgram_Aura_1_Input; + postProcessedOutputs: Ai_Cf_Deepgram_Aura_1_Output; +} +interface Ai_Cf_Ai4Bharat_Indictrans2_En_Indic_1B_Input { + /** + * Input text to translate. Can be a single string or a list of strings. + */ + text: string | string[]; + /** + * Target langauge to translate to + */ + target_language: "asm_Beng" | "awa_Deva" | "ben_Beng" | "bho_Deva" | "brx_Deva" | "doi_Deva" | "eng_Latn" | "gom_Deva" | "gon_Deva" | "guj_Gujr" | "hin_Deva" | "hne_Deva" | "kan_Knda" | "kas_Arab" | "kas_Deva" | "kha_Latn" | "lus_Latn" | "mag_Deva" | "mai_Deva" | "mal_Mlym" | "mar_Deva" | "mni_Beng" | "mni_Mtei" | "npi_Deva" | "ory_Orya" | "pan_Guru" | "san_Deva" | "sat_Olck" | "snd_Arab" | "snd_Deva" | "tam_Taml" | "tel_Telu" | "urd_Arab" | "unr_Deva"; +} +interface Ai_Cf_Ai4Bharat_Indictrans2_En_Indic_1B_Output { + /** + * Translated texts + */ + translations: string[]; +} +declare abstract class Base_Ai_Cf_Ai4Bharat_Indictrans2_En_Indic_1B { + inputs: Ai_Cf_Ai4Bharat_Indictrans2_En_Indic_1B_Input; + postProcessedOutputs: Ai_Cf_Ai4Bharat_Indictrans2_En_Indic_1B_Output; +} +type Ai_Cf_Aisingapore_Gemma_Sea_Lion_V4_27B_It_Input = Ai_Cf_Aisingapore_Gemma_Sea_Lion_V4_27B_It_Prompt | Ai_Cf_Aisingapore_Gemma_Sea_Lion_V4_27B_It_Messages | Ai_Cf_Aisingapore_Gemma_Sea_Lion_V4_27B_It_Async_Batch; +interface Ai_Cf_Aisingapore_Gemma_Sea_Lion_V4_27B_It_Prompt { + /** + * The input text prompt for the model to generate a response. + */ + prompt: string; + /** + * Name of the LoRA (Low-Rank Adaptation) model to fine-tune the base model. + */ + lora?: string; + response_format?: Ai_Cf_Aisingapore_Gemma_Sea_Lion_V4_27B_It_JSON_Mode; + /** + * If true, a chat template is not applied and you must adhere to the specific model's expected formatting. + */ + raw?: boolean; + /** + * If true, the response will be streamed back incrementally using SSE, Server Sent Events. + */ + stream?: boolean; + /** + * The maximum number of tokens to generate in the response. + */ + max_tokens?: number; + /** + * Controls the randomness of the output; higher values produce more random results. + */ + temperature?: number; + /** + * Adjusts the creativity of the AI's responses by controlling how many possible words it considers. Lower values make outputs more predictable; higher values allow for more varied and creative responses. + */ + top_p?: number; + /** + * Limits the AI to choose from the top 'k' most probable words. Lower values make responses more focused; higher values introduce more variety and potential surprises. + */ + top_k?: number; + /** + * Random seed for reproducibility of the generation. + */ + seed?: number; + /** + * Penalty for repeated tokens; higher values discourage repetition. + */ + repetition_penalty?: number; + /** + * Decreases the likelihood of the model repeating the same lines verbatim. + */ + frequency_penalty?: number; + /** + * Increases the likelihood of the model introducing new topics. + */ + presence_penalty?: number; +} +interface Ai_Cf_Aisingapore_Gemma_Sea_Lion_V4_27B_It_JSON_Mode { + type?: "json_object" | "json_schema"; + json_schema?: unknown; +} +interface Ai_Cf_Aisingapore_Gemma_Sea_Lion_V4_27B_It_Messages { + /** + * An array of message objects representing the conversation history. + */ + messages: { + /** + * The role of the message sender (e.g., 'user', 'assistant', 'system', 'tool'). + */ + role: string; + content: string | { + /** + * Type of the content (text) + */ + type?: string; + /** + * Text content + */ + text?: string; + }[]; + }[]; + functions?: { + name: string; + code: string; + }[]; + /** + * A list of tools available for the assistant to use. + */ + tools?: ({ + /** + * The name of the tool. More descriptive the better. + */ + name: string; + /** + * A brief description of what the tool does. + */ + description: string; + /** + * Schema defining the parameters accepted by the tool. + */ + parameters: { + /** + * The type of the parameters object (usually 'object'). + */ + type: string; + /** + * List of required parameter names. + */ + required?: string[]; + /** + * Definitions of each parameter. + */ + properties: { + [k: string]: { + /** + * The data type of the parameter. + */ + type: string; + /** + * A description of the expected parameter. + */ + description: string; + }; + }; + }; + } | { + /** + * Specifies the type of tool (e.g., 'function'). + */ + type: string; + /** + * Details of the function tool. + */ + function: { + /** + * The name of the function. + */ + name: string; + /** + * A brief description of what the function does. + */ + description: string; + /** + * Schema defining the parameters accepted by the function. + */ + parameters: { + /** + * The type of the parameters object (usually 'object'). + */ + type: string; + /** + * List of required parameter names. + */ + required?: string[]; + /** + * Definitions of each parameter. + */ + properties: { + [k: string]: { + /** + * The data type of the parameter. + */ + type: string; + /** + * A description of the expected parameter. + */ + description: string; + }; + }; + }; + }; + })[]; + response_format?: Ai_Cf_Aisingapore_Gemma_Sea_Lion_V4_27B_It_JSON_Mode_1; + /** + * If true, a chat template is not applied and you must adhere to the specific model's expected formatting. + */ + raw?: boolean; + /** + * If true, the response will be streamed back incrementally using SSE, Server Sent Events. + */ + stream?: boolean; + /** + * The maximum number of tokens to generate in the response. + */ + max_tokens?: number; + /** + * Controls the randomness of the output; higher values produce more random results. + */ + temperature?: number; + /** + * Adjusts the creativity of the AI's responses by controlling how many possible words it considers. Lower values make outputs more predictable; higher values allow for more varied and creative responses. + */ + top_p?: number; + /** + * Limits the AI to choose from the top 'k' most probable words. Lower values make responses more focused; higher values introduce more variety and potential surprises. + */ + top_k?: number; + /** + * Random seed for reproducibility of the generation. + */ + seed?: number; + /** + * Penalty for repeated tokens; higher values discourage repetition. + */ + repetition_penalty?: number; + /** + * Decreases the likelihood of the model repeating the same lines verbatim. + */ + frequency_penalty?: number; + /** + * Increases the likelihood of the model introducing new topics. + */ + presence_penalty?: number; +} +interface Ai_Cf_Aisingapore_Gemma_Sea_Lion_V4_27B_It_JSON_Mode_1 { + type?: "json_object" | "json_schema"; + json_schema?: unknown; +} +interface Ai_Cf_Aisingapore_Gemma_Sea_Lion_V4_27B_It_Async_Batch { + requests: (Ai_Cf_Aisingapore_Gemma_Sea_Lion_V4_27B_It_Prompt_1 | Ai_Cf_Aisingapore_Gemma_Sea_Lion_V4_27B_It_Messages_1)[]; +} +interface Ai_Cf_Aisingapore_Gemma_Sea_Lion_V4_27B_It_Prompt_1 { + /** + * The input text prompt for the model to generate a response. + */ + prompt: string; + /** + * Name of the LoRA (Low-Rank Adaptation) model to fine-tune the base model. + */ + lora?: string; + response_format?: Ai_Cf_Aisingapore_Gemma_Sea_Lion_V4_27B_It_JSON_Mode_2; + /** + * If true, a chat template is not applied and you must adhere to the specific model's expected formatting. + */ + raw?: boolean; + /** + * If true, the response will be streamed back incrementally using SSE, Server Sent Events. + */ + stream?: boolean; + /** + * The maximum number of tokens to generate in the response. + */ + max_tokens?: number; + /** + * Controls the randomness of the output; higher values produce more random results. + */ + temperature?: number; + /** + * Adjusts the creativity of the AI's responses by controlling how many possible words it considers. Lower values make outputs more predictable; higher values allow for more varied and creative responses. + */ + top_p?: number; + /** + * Limits the AI to choose from the top 'k' most probable words. Lower values make responses more focused; higher values introduce more variety and potential surprises. + */ + top_k?: number; + /** + * Random seed for reproducibility of the generation. + */ + seed?: number; + /** + * Penalty for repeated tokens; higher values discourage repetition. + */ + repetition_penalty?: number; + /** + * Decreases the likelihood of the model repeating the same lines verbatim. + */ + frequency_penalty?: number; + /** + * Increases the likelihood of the model introducing new topics. + */ + presence_penalty?: number; +} +interface Ai_Cf_Aisingapore_Gemma_Sea_Lion_V4_27B_It_JSON_Mode_2 { + type?: "json_object" | "json_schema"; + json_schema?: unknown; +} +interface Ai_Cf_Aisingapore_Gemma_Sea_Lion_V4_27B_It_Messages_1 { + /** + * An array of message objects representing the conversation history. + */ + messages: { + /** + * The role of the message sender (e.g., 'user', 'assistant', 'system', 'tool'). + */ + role: string; + content: string | { + /** + * Type of the content (text) + */ + type?: string; + /** + * Text content + */ + text?: string; + }[]; + }[]; + functions?: { + name: string; + code: string; + }[]; + /** + * A list of tools available for the assistant to use. + */ + tools?: ({ + /** + * The name of the tool. More descriptive the better. + */ + name: string; + /** + * A brief description of what the tool does. + */ + description: string; + /** + * Schema defining the parameters accepted by the tool. + */ + parameters: { + /** + * The type of the parameters object (usually 'object'). + */ + type: string; + /** + * List of required parameter names. + */ + required?: string[]; + /** + * Definitions of each parameter. + */ + properties: { + [k: string]: { + /** + * The data type of the parameter. + */ + type: string; + /** + * A description of the expected parameter. + */ + description: string; + }; + }; + }; + } | { + /** + * Specifies the type of tool (e.g., 'function'). + */ + type: string; + /** + * Details of the function tool. + */ + function: { + /** + * The name of the function. + */ + name: string; + /** + * A brief description of what the function does. + */ + description: string; + /** + * Schema defining the parameters accepted by the function. + */ + parameters: { + /** + * The type of the parameters object (usually 'object'). + */ + type: string; + /** + * List of required parameter names. + */ + required?: string[]; + /** + * Definitions of each parameter. + */ + properties: { + [k: string]: { + /** + * The data type of the parameter. + */ + type: string; + /** + * A description of the expected parameter. + */ + description: string; + }; + }; + }; + }; + })[]; + response_format?: Ai_Cf_Aisingapore_Gemma_Sea_Lion_V4_27B_It_JSON_Mode_3; + /** + * If true, a chat template is not applied and you must adhere to the specific model's expected formatting. + */ + raw?: boolean; + /** + * If true, the response will be streamed back incrementally using SSE, Server Sent Events. + */ + stream?: boolean; + /** + * The maximum number of tokens to generate in the response. + */ + max_tokens?: number; + /** + * Controls the randomness of the output; higher values produce more random results. + */ + temperature?: number; + /** + * Adjusts the creativity of the AI's responses by controlling how many possible words it considers. Lower values make outputs more predictable; higher values allow for more varied and creative responses. + */ + top_p?: number; + /** + * Limits the AI to choose from the top 'k' most probable words. Lower values make responses more focused; higher values introduce more variety and potential surprises. + */ + top_k?: number; + /** + * Random seed for reproducibility of the generation. + */ + seed?: number; + /** + * Penalty for repeated tokens; higher values discourage repetition. + */ + repetition_penalty?: number; + /** + * Decreases the likelihood of the model repeating the same lines verbatim. + */ + frequency_penalty?: number; + /** + * Increases the likelihood of the model introducing new topics. + */ + presence_penalty?: number; +} +interface Ai_Cf_Aisingapore_Gemma_Sea_Lion_V4_27B_It_JSON_Mode_3 { + type?: "json_object" | "json_schema"; + json_schema?: unknown; +} +type Ai_Cf_Aisingapore_Gemma_Sea_Lion_V4_27B_It_Output = Ai_Cf_Aisingapore_Gemma_Sea_Lion_V4_27B_It_Chat_Completion_Response | Ai_Cf_Aisingapore_Gemma_Sea_Lion_V4_27B_It_Text_Completion_Response | string | Ai_Cf_Aisingapore_Gemma_Sea_Lion_V4_27B_It_AsyncResponse; +interface Ai_Cf_Aisingapore_Gemma_Sea_Lion_V4_27B_It_Chat_Completion_Response { + /** + * Unique identifier for the completion + */ + id?: string; + /** + * Object type identifier + */ + object?: "chat.completion"; + /** + * Unix timestamp of when the completion was created + */ + created?: number; + /** + * Model used for the completion + */ + model?: string; + /** + * List of completion choices + */ + choices?: { + /** + * Index of the choice in the list + */ + index?: number; + /** + * The message generated by the model + */ + message?: { + /** + * Role of the message author + */ + role: string; + /** + * The content of the message + */ + content: string; + /** + * Internal reasoning content (if available) + */ + reasoning_content?: string; + /** + * Tool calls made by the assistant + */ + tool_calls?: { + /** + * Unique identifier for the tool call + */ + id: string; + /** + * Type of tool call + */ + type: "function"; + function: { + /** + * Name of the function to call + */ + name: string; + /** + * JSON string of arguments for the function + */ + arguments: string; + }; + }[]; + }; + /** + * Reason why the model stopped generating + */ + finish_reason?: string; + /** + * Stop reason (may be null) + */ + stop_reason?: string | null; + /** + * Log probabilities (if requested) + */ + logprobs?: {} | null; + }[]; + /** + * Usage statistics for the inference request + */ + usage?: { + /** + * Total number of tokens in input + */ + prompt_tokens?: number; + /** + * Total number of tokens in output + */ + completion_tokens?: number; + /** + * Total number of input and output tokens + */ + total_tokens?: number; + }; + /** + * Log probabilities for the prompt (if requested) + */ + prompt_logprobs?: {} | null; +} +interface Ai_Cf_Aisingapore_Gemma_Sea_Lion_V4_27B_It_Text_Completion_Response { + /** + * Unique identifier for the completion + */ + id?: string; + /** + * Object type identifier + */ + object?: "text_completion"; + /** + * Unix timestamp of when the completion was created + */ + created?: number; + /** + * Model used for the completion + */ + model?: string; + /** + * List of completion choices + */ + choices?: { + /** + * Index of the choice in the list + */ + index: number; + /** + * The generated text completion + */ + text: string; + /** + * Reason why the model stopped generating + */ + finish_reason: string; + /** + * Stop reason (may be null) + */ + stop_reason?: string | null; + /** + * Log probabilities (if requested) + */ + logprobs?: {} | null; + /** + * Log probabilities for the prompt (if requested) + */ + prompt_logprobs?: {} | null; + }[]; + /** + * Usage statistics for the inference request + */ + usage?: { + /** + * Total number of tokens in input + */ + prompt_tokens?: number; + /** + * Total number of tokens in output + */ + completion_tokens?: number; + /** + * Total number of input and output tokens + */ + total_tokens?: number; + }; +} +interface Ai_Cf_Aisingapore_Gemma_Sea_Lion_V4_27B_It_AsyncResponse { + /** + * The async request id that can be used to obtain the results. + */ + request_id?: string; +} +declare abstract class Base_Ai_Cf_Aisingapore_Gemma_Sea_Lion_V4_27B_It { + inputs: Ai_Cf_Aisingapore_Gemma_Sea_Lion_V4_27B_It_Input; + postProcessedOutputs: Ai_Cf_Aisingapore_Gemma_Sea_Lion_V4_27B_It_Output; +} +interface Ai_Cf_Pfnet_Plamo_Embedding_1B_Input { + /** + * Input text to embed. Can be a single string or a list of strings. + */ + text: string | string[]; +} +interface Ai_Cf_Pfnet_Plamo_Embedding_1B_Output { + /** + * Embedding vectors, where each vector is a list of floats. + */ + data: number[][]; + /** + * Shape of the embedding data as [number_of_embeddings, embedding_dimension]. + * + * @minItems 2 + * @maxItems 2 + */ + shape: [ + number, + number + ]; +} +declare abstract class Base_Ai_Cf_Pfnet_Plamo_Embedding_1B { + inputs: Ai_Cf_Pfnet_Plamo_Embedding_1B_Input; + postProcessedOutputs: Ai_Cf_Pfnet_Plamo_Embedding_1B_Output; +} +interface Ai_Cf_Deepgram_Flux_Input { + /** + * Encoding of the audio stream. Currently only supports raw signed little-endian 16-bit PCM. + */ + encoding: "linear16"; + /** + * Sample rate of the audio stream in Hz. + */ + sample_rate: string; + /** + * End-of-turn confidence required to fire an eager end-of-turn event. When set, enables EagerEndOfTurn and TurnResumed events. Valid Values 0.3 - 0.9. + */ + eager_eot_threshold?: string; + /** + * End-of-turn confidence required to finish a turn. Valid Values 0.5 - 0.9. + */ + eot_threshold?: string; + /** + * A turn will be finished when this much time has passed after speech, regardless of EOT confidence. + */ + eot_timeout_ms?: string; + /** + * Keyterm prompting can improve recognition of specialized terminology. Pass multiple keyterm query parameters to boost multiple keyterms. + */ + keyterm?: string; + /** + * Opts out requests from the Deepgram Model Improvement Program. Refer to Deepgram Docs for pricing impacts before setting this to true. https://dpgr.am/deepgram-mip + */ + mip_opt_out?: "true" | "false"; + /** + * Label your requests for the purpose of identification during usage reporting + */ + tag?: string; +} +/** + * Output will be returned as websocket messages. + */ +interface Ai_Cf_Deepgram_Flux_Output { + /** + * The unique identifier of the request (uuid) + */ + request_id?: string; + /** + * Starts at 0 and increments for each message the server sends to the client. + */ + sequence_id?: number; + /** + * The type of event being reported. + */ + event?: "Update" | "StartOfTurn" | "EagerEndOfTurn" | "TurnResumed" | "EndOfTurn"; + /** + * The index of the current turn + */ + turn_index?: number; + /** + * Start time in seconds of the audio range that was transcribed + */ + audio_window_start?: number; + /** + * End time in seconds of the audio range that was transcribed + */ + audio_window_end?: number; + /** + * Text that was said over the course of the current turn + */ + transcript?: string; + /** + * The words in the transcript + */ + words?: { + /** + * The individual punctuated, properly-cased word from the transcript + */ + word: string; + /** + * Confidence that this word was transcribed correctly + */ + confidence: number; + }[]; + /** + * Confidence that no more speech is coming in this turn + */ + end_of_turn_confidence?: number; +} +declare abstract class Base_Ai_Cf_Deepgram_Flux { + inputs: Ai_Cf_Deepgram_Flux_Input; + postProcessedOutputs: Ai_Cf_Deepgram_Flux_Output; +} +interface Ai_Cf_Deepgram_Aura_2_En_Input { + /** + * Speaker used to produce the audio. + */ + speaker?: "amalthea" | "andromeda" | "apollo" | "arcas" | "aries" | "asteria" | "athena" | "atlas" | "aurora" | "callista" | "cora" | "cordelia" | "delia" | "draco" | "electra" | "harmonia" | "helena" | "hera" | "hermes" | "hyperion" | "iris" | "janus" | "juno" | "jupiter" | "luna" | "mars" | "minerva" | "neptune" | "odysseus" | "ophelia" | "orion" | "orpheus" | "pandora" | "phoebe" | "pluto" | "saturn" | "thalia" | "theia" | "vesta" | "zeus"; + /** + * Encoding of the output audio. + */ + encoding?: "linear16" | "flac" | "mulaw" | "alaw" | "mp3" | "opus" | "aac"; + /** + * Container specifies the file format wrapper for the output audio. The available options depend on the encoding type.. + */ + container?: "none" | "wav" | "ogg"; + /** + * The text content to be converted to speech + */ + text: string; + /** + * Sample Rate specifies the sample rate for the output audio. Based on the encoding, different sample rates are supported. For some encodings, the sample rate is not configurable + */ + sample_rate?: number; + /** + * The bitrate of the audio in bits per second. Choose from predefined ranges or specific values based on the encoding type. + */ + bit_rate?: number; +} +/** + * The generated audio in MP3 format + */ +type Ai_Cf_Deepgram_Aura_2_En_Output = string; +declare abstract class Base_Ai_Cf_Deepgram_Aura_2_En { + inputs: Ai_Cf_Deepgram_Aura_2_En_Input; + postProcessedOutputs: Ai_Cf_Deepgram_Aura_2_En_Output; +} +interface Ai_Cf_Deepgram_Aura_2_Es_Input { + /** + * Speaker used to produce the audio. + */ + speaker?: "sirio" | "nestor" | "carina" | "celeste" | "alvaro" | "diana" | "aquila" | "selena" | "estrella" | "javier"; + /** + * Encoding of the output audio. + */ + encoding?: "linear16" | "flac" | "mulaw" | "alaw" | "mp3" | "opus" | "aac"; + /** + * Container specifies the file format wrapper for the output audio. The available options depend on the encoding type.. + */ + container?: "none" | "wav" | "ogg"; + /** + * The text content to be converted to speech + */ + text: string; + /** + * Sample Rate specifies the sample rate for the output audio. Based on the encoding, different sample rates are supported. For some encodings, the sample rate is not configurable + */ + sample_rate?: number; + /** + * The bitrate of the audio in bits per second. Choose from predefined ranges or specific values based on the encoding type. + */ + bit_rate?: number; +} +/** + * The generated audio in MP3 format + */ +type Ai_Cf_Deepgram_Aura_2_Es_Output = string; +declare abstract class Base_Ai_Cf_Deepgram_Aura_2_Es { + inputs: Ai_Cf_Deepgram_Aura_2_Es_Input; + postProcessedOutputs: Ai_Cf_Deepgram_Aura_2_Es_Output; +} +interface Ai_Cf_Black_Forest_Labs_Flux_2_Dev_Input { + multipart: { + body?: object; + contentType?: string; + }; +} +interface Ai_Cf_Black_Forest_Labs_Flux_2_Dev_Output { + /** + * Generated image as Base64 string. + */ + image?: string; +} +declare abstract class Base_Ai_Cf_Black_Forest_Labs_Flux_2_Dev { + inputs: Ai_Cf_Black_Forest_Labs_Flux_2_Dev_Input; + postProcessedOutputs: Ai_Cf_Black_Forest_Labs_Flux_2_Dev_Output; +} +interface Ai_Cf_Black_Forest_Labs_Flux_2_Klein_4B_Input { + multipart: { + body?: object; + contentType?: string; + }; +} +interface Ai_Cf_Black_Forest_Labs_Flux_2_Klein_4B_Output { + /** + * Generated image as Base64 string. + */ + image?: string; +} +declare abstract class Base_Ai_Cf_Black_Forest_Labs_Flux_2_Klein_4B { + inputs: Ai_Cf_Black_Forest_Labs_Flux_2_Klein_4B_Input; + postProcessedOutputs: Ai_Cf_Black_Forest_Labs_Flux_2_Klein_4B_Output; +} +interface Ai_Cf_Black_Forest_Labs_Flux_2_Klein_9B_Input { + multipart: { + body?: object; + contentType?: string; + }; +} +interface Ai_Cf_Black_Forest_Labs_Flux_2_Klein_9B_Output { + /** + * Generated image as Base64 string. + */ + image?: string; +} +declare abstract class Base_Ai_Cf_Black_Forest_Labs_Flux_2_Klein_9B { + inputs: Ai_Cf_Black_Forest_Labs_Flux_2_Klein_9B_Input; + postProcessedOutputs: Ai_Cf_Black_Forest_Labs_Flux_2_Klein_9B_Output; +} +declare abstract class Base_Ai_Cf_Zai_Org_Glm_4_7_Flash { + inputs: ChatCompletionsInput; + postProcessedOutputs: ChatCompletionsOutput; +} +declare abstract class Base_Ai_Cf_Moonshotai_Kimi_K2_5 { + inputs: ChatCompletionsInput; + postProcessedOutputs: ChatCompletionsOutput; +} +declare abstract class Base_Ai_Cf_Moonshotai_Kimi_K2_6 { + inputs: ChatCompletionsInput; + postProcessedOutputs: ChatCompletionsOutput; +} +declare abstract class Base_Ai_Cf_Nvidia_Nemotron_3_120B_A12B { + inputs: ChatCompletionsInput; + postProcessedOutputs: ChatCompletionsOutput; +} +declare abstract class Base_Ai_Cf_Google_Gemma_4_26B_A4B_IT { + inputs: ChatCompletionsInput; + postProcessedOutputs: ChatCompletionsOutput; +} +interface AiModels { + "@cf/huggingface/distilbert-sst-2-int8": BaseAiTextClassification; + "@cf/stabilityai/stable-diffusion-xl-base-1.0": BaseAiTextToImage; + "@cf/runwayml/stable-diffusion-v1-5-inpainting": BaseAiTextToImage; + "@cf/runwayml/stable-diffusion-v1-5-img2img": BaseAiTextToImage; + "@cf/lykon/dreamshaper-8-lcm": BaseAiTextToImage; + "@cf/bytedance/stable-diffusion-xl-lightning": BaseAiTextToImage; + "@cf/myshell-ai/melotts": BaseAiTextToSpeech; + "@cf/google/embeddinggemma-300m": BaseAiTextEmbeddings; + "@cf/microsoft/resnet-50": BaseAiImageClassification; + "@cf/meta/llama-2-7b-chat-int8": BaseAiTextGeneration; + "@cf/mistral/mistral-7b-instruct-v0.1": BaseAiTextGeneration; + "@cf/meta/llama-2-7b-chat-fp16": BaseAiTextGeneration; + "@hf/thebloke/llama-2-13b-chat-awq": BaseAiTextGeneration; + "@hf/thebloke/mistral-7b-instruct-v0.1-awq": BaseAiTextGeneration; + "@hf/thebloke/zephyr-7b-beta-awq": BaseAiTextGeneration; + "@hf/thebloke/openhermes-2.5-mistral-7b-awq": BaseAiTextGeneration; + "@hf/thebloke/neural-chat-7b-v3-1-awq": BaseAiTextGeneration; + "@hf/thebloke/deepseek-coder-6.7b-base-awq": BaseAiTextGeneration; + "@hf/thebloke/deepseek-coder-6.7b-instruct-awq": BaseAiTextGeneration; + "@cf/deepseek-ai/deepseek-math-7b-instruct": BaseAiTextGeneration; + "@cf/defog/sqlcoder-7b-2": BaseAiTextGeneration; + "@cf/openchat/openchat-3.5-0106": BaseAiTextGeneration; + "@cf/tiiuae/falcon-7b-instruct": BaseAiTextGeneration; + "@cf/thebloke/discolm-german-7b-v1-awq": BaseAiTextGeneration; + "@cf/qwen/qwen1.5-0.5b-chat": BaseAiTextGeneration; + "@cf/qwen/qwen1.5-7b-chat-awq": BaseAiTextGeneration; + "@cf/qwen/qwen1.5-14b-chat-awq": BaseAiTextGeneration; + "@cf/tinyllama/tinyllama-1.1b-chat-v1.0": BaseAiTextGeneration; + "@cf/microsoft/phi-2": BaseAiTextGeneration; + "@cf/qwen/qwen1.5-1.8b-chat": BaseAiTextGeneration; + "@cf/mistral/mistral-7b-instruct-v0.2-lora": BaseAiTextGeneration; + "@hf/nousresearch/hermes-2-pro-mistral-7b": BaseAiTextGeneration; + "@hf/nexusflow/starling-lm-7b-beta": BaseAiTextGeneration; + "@hf/google/gemma-7b-it": BaseAiTextGeneration; + "@cf/meta-llama/llama-2-7b-chat-hf-lora": BaseAiTextGeneration; + "@cf/google/gemma-2b-it-lora": BaseAiTextGeneration; + "@cf/google/gemma-7b-it-lora": BaseAiTextGeneration; + "@hf/mistral/mistral-7b-instruct-v0.2": BaseAiTextGeneration; + "@cf/meta/llama-3-8b-instruct": BaseAiTextGeneration; + "@cf/fblgit/una-cybertron-7b-v2-bf16": BaseAiTextGeneration; + "@cf/meta/llama-3-8b-instruct-awq": BaseAiTextGeneration; + "@cf/meta/llama-3.1-8b-instruct-fp8": BaseAiTextGeneration; + "@cf/meta/llama-3.1-8b-instruct-awq": BaseAiTextGeneration; + "@cf/meta/llama-3.2-3b-instruct": BaseAiTextGeneration; + "@cf/meta/llama-3.2-1b-instruct": BaseAiTextGeneration; + "@cf/deepseek-ai/deepseek-r1-distill-qwen-32b": BaseAiTextGeneration; + "@cf/ibm-granite/granite-4.0-h-micro": BaseAiTextGeneration; + "@cf/facebook/bart-large-cnn": BaseAiSummarization; + "@cf/llava-hf/llava-1.5-7b-hf": BaseAiImageToText; + "@cf/baai/bge-base-en-v1.5": Base_Ai_Cf_Baai_Bge_Base_En_V1_5; + "@cf/openai/whisper": Base_Ai_Cf_Openai_Whisper; + "@cf/meta/m2m100-1.2b": Base_Ai_Cf_Meta_M2M100_1_2B; + "@cf/baai/bge-small-en-v1.5": Base_Ai_Cf_Baai_Bge_Small_En_V1_5; + "@cf/baai/bge-large-en-v1.5": Base_Ai_Cf_Baai_Bge_Large_En_V1_5; + "@cf/unum/uform-gen2-qwen-500m": Base_Ai_Cf_Unum_Uform_Gen2_Qwen_500M; + "@cf/openai/whisper-tiny-en": Base_Ai_Cf_Openai_Whisper_Tiny_En; + "@cf/openai/whisper-large-v3-turbo": Base_Ai_Cf_Openai_Whisper_Large_V3_Turbo; + "@cf/baai/bge-m3": Base_Ai_Cf_Baai_Bge_M3; + "@cf/black-forest-labs/flux-1-schnell": Base_Ai_Cf_Black_Forest_Labs_Flux_1_Schnell; + "@cf/meta/llama-3.2-11b-vision-instruct": Base_Ai_Cf_Meta_Llama_3_2_11B_Vision_Instruct; + "@cf/meta/llama-3.3-70b-instruct-fp8-fast": Base_Ai_Cf_Meta_Llama_3_3_70B_Instruct_Fp8_Fast; + "@cf/meta/llama-guard-3-8b": Base_Ai_Cf_Meta_Llama_Guard_3_8B; + "@cf/baai/bge-reranker-base": Base_Ai_Cf_Baai_Bge_Reranker_Base; + "@cf/qwen/qwen2.5-coder-32b-instruct": Base_Ai_Cf_Qwen_Qwen2_5_Coder_32B_Instruct; + "@cf/qwen/qwq-32b": Base_Ai_Cf_Qwen_Qwq_32B; + "@cf/mistralai/mistral-small-3.1-24b-instruct": Base_Ai_Cf_Mistralai_Mistral_Small_3_1_24B_Instruct; + "@cf/google/gemma-3-12b-it": Base_Ai_Cf_Google_Gemma_3_12B_It; + "@cf/meta/llama-4-scout-17b-16e-instruct": Base_Ai_Cf_Meta_Llama_4_Scout_17B_16E_Instruct; + "@cf/qwen/qwen3-30b-a3b-fp8": Base_Ai_Cf_Qwen_Qwen3_30B_A3B_Fp8; + "@cf/deepgram/nova-3": Base_Ai_Cf_Deepgram_Nova_3; + "@cf/qwen/qwen3-embedding-0.6b": Base_Ai_Cf_Qwen_Qwen3_Embedding_0_6B; + "@cf/pipecat-ai/smart-turn-v2": Base_Ai_Cf_Pipecat_Ai_Smart_Turn_V2; + "@cf/openai/gpt-oss-120b": Base_Ai_Cf_Openai_Gpt_Oss_120B; + "@cf/openai/gpt-oss-20b": Base_Ai_Cf_Openai_Gpt_Oss_20B; + "@cf/leonardo/phoenix-1.0": Base_Ai_Cf_Leonardo_Phoenix_1_0; + "@cf/leonardo/lucid-origin": Base_Ai_Cf_Leonardo_Lucid_Origin; + "@cf/deepgram/aura-1": Base_Ai_Cf_Deepgram_Aura_1; + "@cf/ai4bharat/indictrans2-en-indic-1B": Base_Ai_Cf_Ai4Bharat_Indictrans2_En_Indic_1B; + "@cf/aisingapore/gemma-sea-lion-v4-27b-it": Base_Ai_Cf_Aisingapore_Gemma_Sea_Lion_V4_27B_It; + "@cf/pfnet/plamo-embedding-1b": Base_Ai_Cf_Pfnet_Plamo_Embedding_1B; + "@cf/deepgram/flux": Base_Ai_Cf_Deepgram_Flux; + "@cf/deepgram/aura-2-en": Base_Ai_Cf_Deepgram_Aura_2_En; + "@cf/deepgram/aura-2-es": Base_Ai_Cf_Deepgram_Aura_2_Es; + "@cf/black-forest-labs/flux-2-dev": Base_Ai_Cf_Black_Forest_Labs_Flux_2_Dev; + "@cf/black-forest-labs/flux-2-klein-4b": Base_Ai_Cf_Black_Forest_Labs_Flux_2_Klein_4B; + "@cf/black-forest-labs/flux-2-klein-9b": Base_Ai_Cf_Black_Forest_Labs_Flux_2_Klein_9B; + "@cf/zai-org/glm-4.7-flash": Base_Ai_Cf_Zai_Org_Glm_4_7_Flash; + "@cf/moonshotai/kimi-k2.5": Base_Ai_Cf_Moonshotai_Kimi_K2_5; + "@cf/moonshotai/kimi-k2.6": Base_Ai_Cf_Moonshotai_Kimi_K2_6; + "@cf/nvidia/nemotron-3-120b-a12b": Base_Ai_Cf_Nvidia_Nemotron_3_120B_A12B; + "@cf/google/gemma-4-26b-a4b-it": Base_Ai_Cf_Google_Gemma_4_26B_A4B_IT; +} +type AiOptions = { + /** + * Send requests as an asynchronous batch job, only works for supported models + * https://developers.cloudflare.com/workers-ai/features/batch-api + */ + queueRequest?: boolean; + /** + * Establish websocket connections, only works for supported models + */ + websocket?: boolean; + /** + * Tag your requests to group and view them in Cloudflare dashboard. + * + * Rules: + * Tags must only contain letters, numbers, and the symbols: : - . / @ + * Each tag can have maximum 50 characters. + * Maximum 5 tags are allowed each request. + * Duplicate tags will removed. + */ + tags?: string[]; + gateway?: GatewayOptions; + returnRawResponse?: boolean; + prefix?: string; + extraHeaders?: object; + signal?: AbortSignal; +}; +type AiModelsSearchParams = { + author?: string; + hide_experimental?: boolean; + page?: number; + per_page?: number; + search?: string; + source?: number; + task?: string; +}; +type AiModelsSearchObject = { + id: string; + source: number; + name: string; + description: string; + task: { + id: string; + name: string; + description: string; + }; + tags: string[]; + properties: { + property_id: string; + value: string; + }[]; +}; +type ChatCompletionsBase = ChatCompletionsMessagesInput; +type ChatCompletionsInput = ChatCompletionsMessagesInput; +interface InferenceUpstreamError extends Error { +} +interface AiInternalError extends Error { +} +type AiModelListType = Record; +type AiAsyncBatchResponse = { + request_id: string; +}; +declare abstract class Ai { + aiGatewayLogId: string | null; + gateway(gatewayId: string): AiGateway; + /** + * @deprecated Use the standalone `ai_search_namespaces` or `ai_search` Workers bindings instead. + * See https://developers.cloudflare.com/ai-search/usage/workers-binding/ + */ + aiSearch(): AiSearchNamespace; + /** + * @deprecated AutoRAG has been replaced by AI Search. + * Use the standalone `ai_search_namespaces` or `ai_search` Workers bindings instead. + * See https://developers.cloudflare.com/ai-search/usage/workers-binding/ + * + * @param autoragId Instance ID + */ + autorag(autoragId: string): AutoRAG; + // Batch request + run(model: Name, inputs: { + requests: AiModelList[Name]['inputs'][]; + }, options: AiOptions & { + queueRequest: true; + }): Promise; + // Raw response + run(model: Name, inputs: AiModelList[Name]['inputs'], options: AiOptions & { + returnRawResponse: true; + }): Promise; + // WebSocket + run(model: Name, inputs: AiModelList[Name]['inputs'], options: AiOptions & { + websocket: true; + }): Promise; + // Streaming + run(model: Name, inputs: AiModelList[Name]['inputs'] & { + stream: true; + }, options?: AiOptions): Promise; + // Normal (default) - known model + run(model: Name, inputs: AiModelList[Name]['inputs'], options?: AiOptions): Promise; + // Unknown model (fallback). + // + // The `Exclude<..., keyof AiModelList>` constraint forces TypeScript to + // route any model name that is a literal key of `AiModelList` to one of + // the known-model overloads above (so input/output mismatches surface as + // type errors rather than silently falling back to `Record`). + // Names that aren't in `AiModelList` — e.g. third-party gateway models + // like `"google/nano-banana"` — still hit this overload. + run(model: Model extends keyof AiModelList ? never : Model, inputs: Record, options?: AiOptions): Promise>; + models(params?: AiModelsSearchParams): Promise; + toMarkdown(): ToMarkdownService; + toMarkdown(files: MarkdownDocument[], options?: ConversionRequestOptions): Promise; + toMarkdown(files: MarkdownDocument, options?: ConversionRequestOptions): Promise; +} +type GatewayRetries = { + maxAttempts?: 1 | 2 | 3 | 4 | 5; + retryDelayMs?: number; + backoff?: 'constant' | 'linear' | 'exponential'; +}; +type GatewayOptions = { + id: string; + cacheKey?: string; + cacheTtl?: number; + skipCache?: boolean; + metadata?: Record; + collectLog?: boolean; + eventId?: string; + requestTimeoutMs?: number; + retries?: GatewayRetries; +}; +type UniversalGatewayOptions = Exclude & { + /** + ** @deprecated + */ + id?: string; +}; +type AiGatewayPatchLog = { + score?: number | null; + feedback?: -1 | 1 | null; + metadata?: Record | null; +}; +type AiGatewayLog = { + id: string; + provider: string; + model: string; + model_type?: string; + path: string; + duration: number; + request_type?: string; + request_content_type?: string; + status_code: number; + response_content_type?: string; + success: boolean; + cached: boolean; + tokens_in?: number; + tokens_out?: number; + metadata?: Record; + step?: number; + cost?: number; + custom_cost?: boolean; + request_size: number; + request_head?: string; + request_head_complete: boolean; + response_size: number; + response_head?: string; + response_head_complete: boolean; + created_at: Date; +}; +type AIGatewayProviders = 'workers-ai' | 'anthropic' | 'aws-bedrock' | 'azure-openai' | 'google-vertex-ai' | 'huggingface' | 'openai' | 'perplexity-ai' | 'replicate' | 'groq' | 'cohere' | 'google-ai-studio' | 'mistral' | 'grok' | 'openrouter' | 'deepseek' | 'cerebras' | 'cartesia' | 'elevenlabs' | 'adobe-firefly'; +type AIGatewayHeaders = { + 'cf-aig-metadata': Record | string; + 'cf-aig-custom-cost': { + per_token_in?: number; + per_token_out?: number; + } | { + total_cost?: number; + } | string; + 'cf-aig-cache-ttl': number | string; + 'cf-aig-skip-cache': boolean | string; + 'cf-aig-cache-key': string; + 'cf-aig-event-id': string; + 'cf-aig-request-timeout': number | string; + 'cf-aig-max-attempts': number | string; + 'cf-aig-retry-delay': number | string; + 'cf-aig-backoff': string; + 'cf-aig-collect-log': boolean | string; + Authorization: string; + 'Content-Type': string; + [key: string]: string | number | boolean | object; +}; +type AIGatewayUniversalRequest = { + provider: AIGatewayProviders | string; // eslint-disable-line + endpoint: string; + headers: Partial; + query: unknown; +}; +interface AiGatewayInternalError extends Error { +} +interface AiGatewayLogNotFound extends Error { +} +declare abstract class AiGateway { + patchLog(logId: string, data: AiGatewayPatchLog): Promise; + getLog(logId: string): Promise; + run(data: AIGatewayUniversalRequest | AIGatewayUniversalRequest[], options?: { + gateway?: UniversalGatewayOptions; + extraHeaders?: object; + signal?: AbortSignal; + }): Promise; + getUrl(provider?: AIGatewayProviders | string): Promise; // eslint-disable-line +} +// Copyright (c) 2022-2025 Cloudflare, Inc. +// Licensed under the Apache 2.0 license found in the LICENSE file or at: +// https://opensource.org/licenses/Apache-2.0 +/** + * Artifacts — Git-compatible file storage on Cloudflare Workers. + * + * Provides programmatic access to create, manage, and fork repositories, + * and to issue and revoke scoped access tokens. + */ +/** Information about a repository. */ +interface ArtifactsRepoInfo { + /** Unique repository ID. */ + id: string; + /** Repository name. */ + name: string; + /** Repository description, or null if not set. */ + description: string | null; + /** Default branch name (e.g. "main"). */ + defaultBranch: string; + /** ISO 8601 creation timestamp. */ + createdAt: string; + /** ISO 8601 last-updated timestamp. */ + updatedAt: string; + /** ISO 8601 timestamp of the last push, or null if never pushed. */ + lastPushAt: string | null; + /** Fork source (e.g. "github:owner/repo", "artifacts:namespace/repo"), or null if not a fork. */ + source: string | null; + /** Whether the repository is read-only. */ + readOnly: boolean; + /** HTTPS git remote URL. */ + remote: string; +} +/** Result of creating a repository — includes the initial access token. */ +interface ArtifactsCreateRepoResult { + /** Unique repository ID. */ + id: string; + /** Repository name. */ + name: string; + /** Repository description, or null if not set. */ + description: string | null; + /** Default branch name. */ + defaultBranch: string; + /** HTTPS git remote URL. */ + remote: string; + /** Plaintext access token (only returned at creation time). */ + token: string; + /** ISO 8601 token expiry timestamp. */ + tokenExpiresAt: string; +} +/** Paginated list of repositories. */ +interface ArtifactsRepoListResult { + /** Repositories in this page (without the `remote` field). */ + repos: Omit[]; + /** Total number of repositories in the namespace. */ + total: number; + /** Cursor for the next page, if there are more results. */ + cursor?: string; +} +/** Result of creating an access token. */ +interface ArtifactsCreateTokenResult { + /** Unique token ID. */ + id: string; + /** Plaintext token (only returned at creation time). */ + plaintext: string; + /** Token scope: "read" or "write". */ + scope: 'read' | 'write'; + /** ISO 8601 token expiry timestamp. */ + expiresAt: string; +} +/** Token metadata (no plaintext). */ +interface ArtifactsTokenInfo { + /** Unique token ID. */ + id: string; + /** Token scope: "read" or "write". */ + scope: 'read' | 'write'; + /** Token state: "active", "expired", or "revoked". */ + state: 'active' | 'expired' | 'revoked'; + /** ISO 8601 creation timestamp. */ + createdAt: string; + /** ISO 8601 expiry timestamp. */ + expiresAt: string; +} +/** Paginated list of tokens for a repository. */ +interface ArtifactsTokenListResult { + /** Tokens in this page. */ + tokens: ArtifactsTokenInfo[]; + /** Total number of tokens for the repository. */ + total: number; +} +/** + * Handle for a single repository. Returned by Artifacts.get(). + * + * Methods may throw `ArtifactsError` with code `INTERNAL_ERROR` if an unexpected service error occurs. + */ +interface ArtifactsRepo extends ArtifactsRepoInfo { + /** + * Create an access token for this repo. + * @param scope Token scope: "write" (default) or "read". + * @param ttl Time-to-live in seconds (default 86400, min 60, max 31536000). + * @throws {ArtifactsError} with code `INVALID_TTL` if ttl is out of range. + */ + createToken(scope?: 'write' | 'read', ttl?: number): Promise; + /** List tokens for this repo (metadata only, no plaintext). */ + listTokens(): Promise; + /** + * Revoke a token by plaintext or ID. + * @param tokenOrId Plaintext token or token ID. + * @returns true if revoked, false if not found. + * @throws {ArtifactsError} with code `INVALID_INPUT` if tokenOrId is empty. + */ + revokeToken(tokenOrId: string): Promise; + // ── Fork ── + /** + * Fork this repo to a new repo. + * @param name Target repository name. + * @param opts Optional: description, readOnly flag, defaultBranchOnly (default true). + * @throws {ArtifactsError} with code `INVALID_REPO_NAME` if name is invalid. + * @throws {ArtifactsError} with code `ALREADY_EXISTS` if the target repo already exists. + * @throws {ArtifactsError} with code `FORK_IN_PROGRESS` if a fork is already running. + */ + fork(name: string, opts?: { + description?: string; + readOnly?: boolean; + defaultBranchOnly?: boolean; + }): Promise; +} +// ── Error types ────────────────────────────────────────────────────────────── +/** + * Error codes returned by Artifacts binding operations. + * + * Each code maps to a numeric code available on `ArtifactsError.numericCode`. + */ +type ArtifactsErrorCode = 'ALREADY_EXISTS' | 'NOT_FOUND' | 'IMPORT_IN_PROGRESS' | 'FORK_IN_PROGRESS' | 'INVALID_INPUT' | 'INVALID_REPO_NAME' | 'INVALID_TTL' | 'INVALID_URL' | 'REMOTE_AUTH_REQUIRED' | 'UPSTREAM_UNAVAILABLE' | 'MEMORY_LIMIT' | 'INTERNAL_ERROR'; +/** + * Error thrown by Artifacts binding operations. + * + * Uses a string `.code` discriminator following the Cloudflare platform + * convention (StreamError, ImagesError, etc.). The `.numericCode` matches + * the REST API `errors[].code` values. + */ +interface ArtifactsError extends Error { + readonly name: 'ArtifactsError'; + /** String error code for programmatic matching. */ + readonly code: ArtifactsErrorCode; + /** Numeric error code matching the REST API. */ + readonly numericCode: number; +} +// ── Binding ────────────────────────────────────────────────────────────────── +/** + * Artifacts binding — namespace-level operations. + * + * Methods may throw `ArtifactsError` with code `INTERNAL_ERROR` if an unexpected service error occurs. + */ +interface Artifacts { + /** + * Create a new repository with an initial access token. + * @param name Repository name (alphanumeric, dots, hyphens, underscores). + * @param opts Optional: readOnly flag, description, default branch name. + * @returns Repo metadata with initial token. + * @throws {ArtifactsError} with code `INVALID_REPO_NAME` if name is invalid. + * @throws {ArtifactsError} with code `ALREADY_EXISTS` if the repo already exists. + */ + create(name: string, opts?: { + readOnly?: boolean; + description?: string; + setDefaultBranch?: string; + }): Promise; + /** + * Get a handle to an existing repository. + * @param name Repository name. + * @returns Repo handle. + * @throws {ArtifactsError} with code `NOT_FOUND` if the repo does not exist. + * @throws {ArtifactsError} with code `IMPORT_IN_PROGRESS` if the repo is still importing. + * @throws {ArtifactsError} with code `FORK_IN_PROGRESS` if the repo is still forking. + */ + get(name: string): Promise; + /** + * Import a repository from an external git remote. + * @param params Source URL and optional branch/depth, plus target name and options. + * @returns Repo metadata with initial token. + * @throws {ArtifactsError} with code `INVALID_REPO_NAME` if the target name is invalid. + * @throws {ArtifactsError} with code `INVALID_INPUT` if the source URL is not valid HTTPS. + * @throws {ArtifactsError} with code `INVALID_URL` if the source URL does not point to a git repository. + * @throws {ArtifactsError} with code `REMOTE_AUTH_REQUIRED` if the remote requires authentication. + * @throws {ArtifactsError} with code `NOT_FOUND` if the remote repository does not exist. + * @throws {ArtifactsError} with code `UPSTREAM_UNAVAILABLE` if the remote cannot be reached. + * @throws {ArtifactsError} with code `MEMORY_LIMIT` if the import exceeds service memory limits. + * @throws {ArtifactsError} with code `ALREADY_EXISTS` if the target repo already exists. + */ + import(params: { + source: { + url: string; + branch?: string; + depth?: number; + }; + target: { + name: string; + opts?: { + description?: string; + readOnly?: boolean; + }; + }; + }): Promise; + /** + * List repositories with cursor-based pagination. + * @param opts Optional: limit (1–200, default 50), cursor for next page. + */ + list(opts?: { + limit?: number; + cursor?: string; + }): Promise; + /** + * Delete a repository and all associated tokens. + * @param name Repository name. + * @returns true if deleted, false if not found. + * @throws {ArtifactsError} with code `INVALID_REPO_NAME` if name is invalid. + */ + delete(name: string): Promise; +} +/** + * @deprecated Use the standalone AI Search Workers binding instead. + * See https://developers.cloudflare.com/ai-search/usage/workers-binding/ + */ +interface AutoRAGInternalError extends Error { +} +/** + * @deprecated Use the standalone AI Search Workers binding instead. + * See https://developers.cloudflare.com/ai-search/usage/workers-binding/ + */ +interface AutoRAGNotFoundError extends Error { +} +/** + * @deprecated Use the standalone AI Search Workers binding instead. + * See https://developers.cloudflare.com/ai-search/usage/workers-binding/ + */ +interface AutoRAGUnauthorizedError extends Error { +} +/** + * @deprecated Use the standalone AI Search Workers binding instead. + * See https://developers.cloudflare.com/ai-search/usage/workers-binding/ + */ +interface AutoRAGNameNotSetError extends Error { +} +type ComparisonFilter = { + key: string; + type: 'eq' | 'ne' | 'gt' | 'gte' | 'lt' | 'lte'; + value: string | number | boolean; +}; +type CompoundFilter = { + type: 'and' | 'or'; + filters: ComparisonFilter[]; +}; +/** + * @deprecated Use the standalone AI Search Workers binding instead. + * See https://developers.cloudflare.com/ai-search/usage/workers-binding/ + */ +type AutoRagSearchRequest = { + query: string; + filters?: CompoundFilter | ComparisonFilter; + max_num_results?: number; + ranking_options?: { + ranker?: string; + score_threshold?: number; + }; + reranking?: { + enabled?: boolean; + model?: string; + }; + rewrite_query?: boolean; +}; +/** + * @deprecated Use the standalone AI Search Workers binding instead. + * See https://developers.cloudflare.com/ai-search/usage/workers-binding/ + */ +type AutoRagAiSearchRequest = AutoRagSearchRequest & { + stream?: boolean; + system_prompt?: string; +}; +/** + * @deprecated Use the standalone AI Search Workers binding instead. + * See https://developers.cloudflare.com/ai-search/usage/workers-binding/ + */ +type AutoRagAiSearchRequestStreaming = Omit & { + stream: true; +}; +/** + * @deprecated Use the standalone AI Search Workers binding instead. + * See https://developers.cloudflare.com/ai-search/usage/workers-binding/ + */ +type AutoRagSearchResponse = { + object: 'vector_store.search_results.page'; + search_query: string; + data: { + file_id: string; + filename: string; + score: number; + attributes: Record; + content: { + type: 'text'; + text: string; + }[]; + }[]; + has_more: boolean; + next_page: string | null; +}; +/** + * @deprecated Use the standalone AI Search Workers binding instead. + * See https://developers.cloudflare.com/ai-search/usage/workers-binding/ + */ +type AutoRagListResponse = { + id: string; + enable: boolean; + type: string; + source: string; + vectorize_name: string; + paused: boolean; + status: string; +}[]; +/** + * @deprecated Use the standalone AI Search Workers binding instead. + * See https://developers.cloudflare.com/ai-search/usage/workers-binding/ + */ +type AutoRagAiSearchResponse = AutoRagSearchResponse & { + response: string; +}; +/** + * @deprecated Use the standalone AI Search Workers binding instead. + * See https://developers.cloudflare.com/ai-search/usage/workers-binding/ + */ +declare abstract class AutoRAG { + /** + * @deprecated Use the standalone AI Search Workers binding instead. + * See https://developers.cloudflare.com/ai-search/usage/workers-binding/ + */ + list(): Promise; + /** + * @deprecated Use the standalone AI Search Workers binding instead. + * See https://developers.cloudflare.com/ai-search/usage/workers-binding/ + */ + search(params: AutoRagSearchRequest): Promise; + /** + * @deprecated Use the standalone AI Search Workers binding instead. + * See https://developers.cloudflare.com/ai-search/usage/workers-binding/ + */ + aiSearch(params: AutoRagAiSearchRequestStreaming): Promise; + /** + * @deprecated Use the standalone AI Search Workers binding instead. + * See https://developers.cloudflare.com/ai-search/usage/workers-binding/ + */ + aiSearch(params: AutoRagAiSearchRequest): Promise; + /** + * @deprecated Use the standalone AI Search Workers binding instead. + * See https://developers.cloudflare.com/ai-search/usage/workers-binding/ + */ + aiSearch(params: AutoRagAiSearchRequest): Promise; +} +type BrowserRunLifecycleEvent = 'load' | 'domcontentloaded' | 'networkidle0' | 'networkidle2'; +type BrowserRunResourceType = 'document' | 'stylesheet' | 'image' | 'media' | 'font' | 'script' | 'texttrack' | 'xhr' | 'fetch' | 'prefetch' | 'eventsource' | 'websocket' | 'manifest' | 'signedexchange' | 'ping' | 'cspviolationreport' | 'preflight' | 'other'; +/** Options fields shared by all quick actions. */ +interface BrowserRunBaseOptions { + /** Adds `