From 7a8e1efc7d772792fe77aa821d7ed08db6366176 Mon Sep 17 00:00:00 2001 From: Nathan Disidore Date: Fri, 6 Nov 2026 13:52:59 -0600 Subject: [PATCH 01/37] Add PagerDuty gatekeeper Scoped OAuth (US and EU accounts) with five grantable resources: the account, a team, a service, an incident and a schedule, chosen in one HierarchyPicker configurator. Reads report subresources per the new observer contract; incident and override writes go through the approval queue. --- README.md | 1 + docs/observers.md | 1 + packages/gatekeeper-pagerduty/README.md | 130 + .../__tests__/configurator.test.ts | 76 + .../gatekeeper-pagerduty/__tests__/env.d.ts | 18 + .../__tests__/pagerduty-urls.test.ts | 76 + .../__tests__/workerd/account.test.ts | 134 + .../__tests__/workerd/actions.test.ts | 261 + .../__tests__/workerd/fake-pagerduty.ts | 384 + .../__tests__/workerd/observers.test.ts | 127 + .../__tests__/workerd/reads.test.ts | 224 + .../__tests__/workerd/worker.ts | 198 + .../gatekeeper-pagerduty/cloudflare.config.ts | 17 + .../gatekeeper-pagerduty/deploy-inputs.json | 22 + packages/gatekeeper-pagerduty/package.json | 29 + .../pagerduty-configurator-types.d.ts | 70 + .../pagerduty-configurator-ui.tsx | 339 + .../gatekeeper-pagerduty/src/observability.ts | 17 + .../src/pagerduty-actions.ts | 417 + .../gatekeeper-pagerduty/src/pagerduty-api.ts | 159 + .../src/pagerduty-configurator.ts | 246 + .../src/pagerduty-credentials.ts | 158 + .../gatekeeper-pagerduty/src/pagerduty-env.ts | 134 + .../src/pagerduty-gatekeeper.ts | 229 + .../src/pagerduty-logo.svg | 1 + .../src/pagerduty-normalize.ts | 264 + .../src/pagerduty-sessions.ts | 984 + .../src/pagerduty-simulation.ts | 173 + .../src/pagerduty-urls.ts | 79 + .../gatekeeper-pagerduty/src/pagerduty.ts | 656 + .../src/text-modules.d.ts | 9 + packages/gatekeeper-pagerduty/src/types.d.ts | 618 + packages/gatekeeper-pagerduty/src/types.txt | 1 + packages/gatekeeper-pagerduty/tsconfig.json | 19 + packages/gatekeeper-pagerduty/vite.config.ts | 23 + .../gatekeeper-pagerduty/vitest.config.ts | 8 + .../vitest.worker.config.ts | 41 + .../worker-configuration.d.ts | 16265 ++++++++++++++++ packages/gatekeeper-pagerduty/wrangler.jsonc | 41 + pnpm-lock.yaml | 58 +- .../gatekeeper-pagerduty/pagerduty.js | 1 + scripts/release/testdata/golden-manifest.json | 73 + scripts/run-dev-server.ts | 1 + 43 files changed, 22768 insertions(+), 14 deletions(-) create mode 100644 packages/gatekeeper-pagerduty/README.md create mode 100644 packages/gatekeeper-pagerduty/__tests__/configurator.test.ts create mode 100644 packages/gatekeeper-pagerduty/__tests__/env.d.ts create mode 100644 packages/gatekeeper-pagerduty/__tests__/pagerduty-urls.test.ts create mode 100644 packages/gatekeeper-pagerduty/__tests__/workerd/account.test.ts create mode 100644 packages/gatekeeper-pagerduty/__tests__/workerd/actions.test.ts create mode 100644 packages/gatekeeper-pagerduty/__tests__/workerd/fake-pagerduty.ts create mode 100644 packages/gatekeeper-pagerduty/__tests__/workerd/observers.test.ts create mode 100644 packages/gatekeeper-pagerduty/__tests__/workerd/reads.test.ts create mode 100644 packages/gatekeeper-pagerduty/__tests__/workerd/worker.ts create mode 100644 packages/gatekeeper-pagerduty/cloudflare.config.ts create mode 100644 packages/gatekeeper-pagerduty/deploy-inputs.json create mode 100644 packages/gatekeeper-pagerduty/package.json create mode 100644 packages/gatekeeper-pagerduty/src/configurator/pagerduty-configurator-types.d.ts create mode 100644 packages/gatekeeper-pagerduty/src/configurator/pagerduty-configurator-ui.tsx create mode 100644 packages/gatekeeper-pagerduty/src/observability.ts create mode 100644 packages/gatekeeper-pagerduty/src/pagerduty-actions.ts create mode 100644 packages/gatekeeper-pagerduty/src/pagerduty-api.ts create mode 100644 packages/gatekeeper-pagerduty/src/pagerduty-configurator.ts create mode 100644 packages/gatekeeper-pagerduty/src/pagerduty-credentials.ts create mode 100644 packages/gatekeeper-pagerduty/src/pagerduty-env.ts create mode 100644 packages/gatekeeper-pagerduty/src/pagerduty-gatekeeper.ts create mode 100644 packages/gatekeeper-pagerduty/src/pagerduty-logo.svg create mode 100644 packages/gatekeeper-pagerduty/src/pagerduty-normalize.ts create mode 100644 packages/gatekeeper-pagerduty/src/pagerduty-sessions.ts create mode 100644 packages/gatekeeper-pagerduty/src/pagerduty-simulation.ts create mode 100644 packages/gatekeeper-pagerduty/src/pagerduty-urls.ts create mode 100644 packages/gatekeeper-pagerduty/src/pagerduty.ts create mode 100644 packages/gatekeeper-pagerduty/src/text-modules.d.ts create mode 100644 packages/gatekeeper-pagerduty/src/types.d.ts create mode 120000 packages/gatekeeper-pagerduty/src/types.txt create mode 100644 packages/gatekeeper-pagerduty/tsconfig.json create mode 100644 packages/gatekeeper-pagerduty/vite.config.ts create mode 100644 packages/gatekeeper-pagerduty/vitest.config.ts create mode 100644 packages/gatekeeper-pagerduty/vitest.worker.config.ts create mode 100644 packages/gatekeeper-pagerduty/worker-configuration.d.ts create mode 100644 packages/gatekeeper-pagerduty/wrangler.jsonc create mode 100644 scripts/release/testdata/fixture-bundles/gatekeeper-pagerduty/pagerduty.js diff --git a/README.md b/README.md index 3ae5f5137a..6bea65e71d 100644 --- a/README.md +++ b/README.md @@ -216,6 +216,7 @@ Each gatekeeper package contains instructions for how to set it up: * [Slack API](packages/gatekeeper-slack/README.md) * [Spotify](packages/gatekeeper-spotify/README.md) * [Microsoft 365](packages/gatekeeper-microsoft/README.md) +* [PagerDuty API](packages/gatekeeper-pagerduty/README.md) * [X API](packages/gatekeeper-x/README.md) * [ZoomInfo API](packages/gatekeeper-zoominfo/README.md) diff --git a/docs/observers.md b/docs/observers.md index bf288564e5..ca858c31f6 100644 --- a/docs/observers.md +++ b/docs/observers.md @@ -914,6 +914,7 @@ its resource types. | **microsoft** | Outlook Mailbox / Folder / Conversation / Sender | **C**, kernel-checked (§10) | Every read, hook deliveries included, reports the mailbox (`outlook/user/`), folder (`…/folder/`) or messages (`…/message/`) it revealed, by the mailbox owner's object ID and immutable IDs, naming the tenant as for SharePoint (`outlook/tenant//user/…`). `verifyObserverAccess` allows an observer only in that tenant and only if a probe `GET` as them opens each: the whole mailbox needs access to all of it (someone shared one folder gets 404), a folder or message needs that folder or message. Answers are reused as for SharePoint. `addObserver` still checks the units recorded before reads reported subresources. | | **microsoft** | Outlook Calendar / Availability | **C**, kernel-checked (§10) | A calendar binding's reads report its calendar at the projection they showed, `calendar/user///`; an every-calendar binding's reads report `calendar/user/`; free/busy reads report `calendar/availability/
`; each names the tenant as for SharePoint. `verifyObserverAccess` allows an observer only in that tenant: to a calendar projection only when the owner's `calendarPermissions` give them a role that sees it (`calendar-acl.ts`, where a group entry can only lower the organization default), which only the owner can read, so someone else's calendar admits that owner alone (the owner's `UserAccount` reuses a sharing list for 2 minutes); to someone's free/busy only when `getSchedule` as the observer answers (reused as for SharePoint); to a user's whole calendars nobody but that user. `addObserver` still checks the units recorded before reads reported subresources. | | **x** | Profile | As Account, kernel-checked (§10) | As Account: a public profile reports `[]`, and a protected user's posts report `user/`. Accepted over-permissiveness for every X binding: an author who has blocked the observer is still readable through the gadget, since checking each author per observer per read would cost a read each and what leaks is public. | +| **pagerduty** | Account / Team / Service / Incident / Schedule | **C**, kernel-checked (§10) | Every read, each page, an empty one or a cache hit included, reports IDs qualified by region and account (`region//account/[//]`): the account; the bound team, service, incident or schedule; each service, schedule or escalation policy a filter named; and what the answer depends on: each distinct service on an incident page, each service, team or schedule a listing returns, an incident's service (and, in a Team binding, the team), and on-call listings' escalation policies and schedules, the service's or team's included even when empty. Team schedule, service and member listings are covered by the team. `verifyObserverAccess` allows the account only to an observer whose own connection is to the same account in the same region, with a base role that sees the account's objects (not Limited Stakeholder or Restricted Access, read afresh); each object only if PagerDuty opens it for them (`GET` on it, 403/404 deny, anything else fails the check). The observer's `UserAccount` reuses an allowed answer for up to 15 minutes and a denial for up to 15 seconds, partitioned by its credentials. New gatekeeper: `addObserver`/`removeObserver` are no-ops and `getHistoricalSubresources` returns `[]`. | | **mcp-portal** | Upstream server behind the MCP Server Portal (whole server or selected tools) | **A**, kernel-checked (§10) | Admit only an observer whose verifier stands for the binding's own `McpAccount` (`props.accountObjectId`): every read through the binding was made with that account's credentials, so its holder, such as a collaborator who added the connection, sees nothing new. Refuse everyone else, another account of the same person or portal user included; no portal user identity exists to compare (`none`/`token` auth share one credential, OAuth access tokens are opaque). Every read — catalog, lookup, search, read-only call, approved-call result — reports `server/`, the grant's `scope.serverId` (not the portal's own `serverId`), with no endpoint, account or tool in it: requirements are recorded per connection, and each connection's facet is pinned to one endpoint and account. `verifyObserverAccess` applies the same account rule to it and fails the whole call for any other ID. | | **slack** | Workspace | **C**, kernel-checked (§10) | Every read reports what it disclosed: `workspace/` for workspace metadata, the member directory, and a listing or search page naming no conversation; `workspace//conversation/` for each conversation a listing or search page named, and for everything read in a conversation, its threads, messages and members included, since Slack's access control stops at the conversation. `verifyObserverAccess` allows a workspace only to an observer whose own account belongs to it, and a conversation only to one whose account also opens it there (`conversations.info`), answered by their `UserAccount`, which reuses an allowed answer for up to 15 minutes and a denial for up to 15 seconds, until its credentials are next installed. Only Slack answering that the workspace or conversation is out of the observer's reach counts as no access; any other refusal, such as an expired token, a missing scope or a rate limit, fails the check. `addObserver` keeps requiring workspace membership and every conversation reads recorded before they reported subresources, pending or observed. `getHistoricalSubresources` exports those recorded requirements only with a stored workspace and a nonempty legacy tracker; it throws when history is unprovable, without consulting the current account or changing storage. | | **slack** | Conversation / Thread | **B**, kernel-checked (§10) | Every read, including a thread binding's, reports the bound conversation as `workspace//conversation/`, checked as for a workspace binding. `addObserver` keeps checking the observer can open it, for reads made before they reported it. `getHistoricalSubresources` throws for legacy bindings because they did not record whether data was observed; bindings created since reporting began export `[]`. | diff --git a/packages/gatekeeper-pagerduty/README.md b/packages/gatekeeper-pagerduty/README.md new file mode 100644 index 0000000000..c01e7d29f0 --- /dev/null +++ b/packages/gatekeeper-pagerduty/README.md @@ -0,0 +1,130 @@ +# Gatekeeper PagerDuty + +Connects PagerDuty users to Gadgets. A gadget can read incidents, services, teams, schedules and +who is on call, and act on incidents and schedules as the connected user: acknowledge, resolve, +add notes, reassign, open incidents, put people on call. Nothing reaches PagerDuty until a user +approves it. Every pending change is shown to the gadget as if it had already happened, so a gadget +keeps working while it waits. Everything a gadget sees is what the connected user can see in +PagerDuty. + +Five resource types, each its own grant, all picked from one connect card: + +| Resource | Link | What it reaches | +|---|---|---| +| PagerDuty Account | `https://.pagerduty.com` | Every incident, service, team and schedule the user can see. | +| PagerDuty Team | `…/teams/` | The team's services and their incidents, its schedules and members. | +| PagerDuty Service | `…/service-directory/` | One service and its incidents, and opening new incidents on it. | +| PagerDuty Incident | `…/incidents/` | One incident: read it and change it. | +| PagerDuty Schedule | `…/schedules/` | One on-call schedule: its shifts and overrides. | + +EU accounts use `https://.eu.pagerduty.com` in place of the US host. A link to another +account than the connection's is refused. + +## What a gadget can do + +The full API is `src/types.d.ts`, which is also what agents read. In brief: + +- **Account**, **Team** and **Service** list and open incidents (by default, open ones from the + last 30 days), list who is on call, look up users by name or email, and list priorities. Account + and Team also list and open services and schedules; a Team lists its members; a Service opens + new incidents. Opening something from a broader binding returns the same interface a narrower + binding gets, and a narrower binding refuses IDs outside it, such as another team's incident. +- **Incident** reads its details, log, notes and alerts, and acknowledges, snoozes, resolves, + reassigns, escalates, requests responders, sets the title, priority or urgency, adds notes and + posts status updates. +- **Schedule** reads its details, shifts and overrides, and adds or removes overrides. + +Methods that take a user accept an email or a PagerDuty ID. `getMetadata().you` is the connected +user, so "my incidents" is `listIncidents({ assignedToMe: true })`. + +Event hooks are not available yet: `subscribe()` throws on every binding. + +## Actions + +Each kind is a rule a user can enable for auto-approval. Kinds that page or email people outside +the binding, or delete, are never auto-approved. + +| Kind | Methods | Auto-approvable | +|---|---|---| +| Acknowledge and snooze incidents | `acknowledge`, `snooze` | Yes | +| Resolve incidents | `resolve` | Yes | +| Change incidents' title, priority and urgency | `setTitle`, `setPriority`, `setUrgency` | Yes | +| Add notes to incidents | `addNote` | Yes | +| Reassign and escalate incidents | `reassign`, `escalate` | No: notifies the new assignees | +| Ask more people to respond to incidents | `requestResponders` | No: pages each responder | +| Post incident status updates | `postStatusUpdate` | No: emails subscribers and stakeholders | +| Open incidents | `createIncident` | No: pages whoever is on call | +| Add schedule overrides | `createOverride` | Only when it puts the connected user on call | +| Remove schedule overrides | `deleteOverride` | No: removes an override | + +Approved changes can't be reverted automatically: PagerDuty's API has no undo for them. + +## Setting up a PagerDuty app + +The gatekeeper uses a private Scoped OAuth app, registered on the deployment's own PagerDuty +account (for example `acme.pagerduty.com`). Only that account's users can connect, US or EU alike. + +1. Sign in as a Manager, Global Admin or Account Owner, and open **Integrations > Developer Tools + > App Registration**. +2. Select **New App**, and leave its **App Type** as **Private**. Under **Functionality**, add + **OAuth** and choose **Scoped OAuth**. +3. Grant it these scopes, exactly what the gatekeeper's endpoints need (`SCOPES` in + `src/pagerduty-env.ts`): `incidents.read`, `incidents.write`, `services.read`, `teams.read`, + `users.read`, `schedules.read`, `schedules.write`, `oncalls.read`, `priorities.read`, + `escalation_policies.read`. +4. Add the redirect URL `$PUBLIC_BASE_URL/gatekeeper/pagerduty/oauth`, and save. +5. Copy the **Client ID** and **Client Secret** into the deployment's `CLIENT_ID` and + `CLIENT_SECRET`. + +PagerDuty grants a scope the app lacks by silently leaving it out, so a connection missing any of +them is refused, naming the ones missing. + +A deployment behind Cloudflare Access needs nothing more: users sign in on PagerDuty's own page, +which sends them through the account's SSO if it has one. + +### Local development + +Export `PAGERDUTY_CLIENT_ID` and `PAGERDUTY_CLIENT_SECRET` in your shell or the root `.dev.vars`; +`pnpm dev-server` maps them into the worker's `CLIENT_ID` and `CLIENT_SECRET`. Register +`http://localhost:8787/gatekeeper/pagerduty/oauth` as a redirect URL. + +## Sharing a workspace + +Every read reports what it disclosed as subresource IDs, qualified by region and account, such as +`region/us/account/PABC123/service/PXYZ789`: the account, plus the bound team, service, incident or +schedule, plus every service, schedule or escalation policy a filter named or a page contained. +An incident read also reports its service. Empty pages and cached reads report the same. + +A collaborator may observe a read only if their own PagerDuty connection can see every one of +those, checked with their login: the account by being the same account in the same region, with a +base role that sees the account's objects (not Limited Stakeholder or Restricted Access); each +object by PagerDuty opening it for them. A refusal (403 or 404) is a no; any other failure fails the +check. An answer is reused for 15 minutes if it allowed and 15 seconds if it denied, so a lost role +or team membership goes unnoticed for up to 15 minutes. See `docs/observers.md`. + +## Limitations + +- **Reconnect once a year.** PagerDuty caps a Scoped OAuth grant at one year. +- **A connection unused for 30 days dies.** Its refresh token expires, and the user must reconnect. +- **Disconnecting doesn't revoke at PagerDuty**, which documents no revocation endpoint. The grant + lapses there (the access token within a day); a user can revoke it sooner from their PagerDuty + profile. +- **Not covered:** merging incidents, incident workflows, the Events API, maintenance windows, + editing escalation policies, status pages, custom fields and analytics. + +## Troubleshooting + +### "The PagerDuty connection has expired or was revoked. Reconnect the PagerDuty account." + +PagerDuty refused the refresh token: the connection went 30 days unused, reached its one-year cap, +or was revoked. Reconnect from the Workshop's Connections page. + +### "The PagerDuty app is missing permissions" + +The app isn't granted every scope above. Add the ones the message names to the app, then connect +again. + +### "That's a different PagerDuty user" + +A reconnect was authorized by another user than the one the connection was made for. Sign in to +PagerDuty as that user and reconnect, or connect the other user separately. diff --git a/packages/gatekeeper-pagerduty/__tests__/configurator.test.ts b/packages/gatekeeper-pagerduty/__tests__/configurator.test.ts new file mode 100644 index 0000000000..f9f4730aef --- /dev/null +++ b/packages/gatekeeper-pagerduty/__tests__/configurator.test.ts @@ -0,0 +1,76 @@ +// The configurator frame is transpiled alone and can't import `RESOURCES`, so it holds its own copy +// of each resource's URL pattern, which it matches against the patterns the Workshop says it will +// take. This keeps the copies in step: a node of each kind is choosable exactly when its own +// resource's pattern is accepted, and a refusal names the kinds that are. + +import { beforeAll, describe, expect, it, vi } from "vitest"; +import configurator from "../src/configurator/pagerduty-configurator-ui"; +import type { PagerDutyNode, PagerDutyNodeKind } from "../src/configurator/pagerduty-configurator-types"; +import { RESOURCES, type ResourceKind } from "../src/pagerduty-env"; + +vi.mock("@gadgets/configurator-ui", () => ({ + h: (component: unknown, props: unknown, ...children: unknown[]) => ({ component, props: props ?? {}, children }), + Field: "Field", + HierarchyPicker: "HierarchyPicker", + Notice: "Notice", + Section: "Section", +})); + +type Tree = { component: unknown; props: Record; children: unknown[] }; +type Spec = { + isReady(context: { values: Record; acceptedResourceUrlPatterns?: string[] }): boolean; + render(context: unknown): Tree; +}; + +// The frame's callbacks, called here with only the context fields they read. +const spec = configurator as unknown as Spec; + +const KINDS = Object.keys(RESOURCES) as ResourceKind[]; +const patternOf = (kind: ResourceKind) => RESOURCES[kind].urlPattern; + +function find(node: unknown, component: string): Tree | undefined { + if (!node || typeof node !== "object") return undefined; + const tree = node as Tree; + if (tree.component === component) return tree; + for (const child of (tree.children ?? []).flat()) { + const found = find(child, component); + if (found) return found; + } + return undefined; +} + +const ready = Promise.resolve({ state: "ready", account: { ref: "account", host: "acme.pagerduty.com" } }); + +/** The picker as the frame renders it once the connection is ready, under `accepted`. */ +function picker(accepted: string[] | undefined, rows: PagerDutyNode[] = []) { + const ui = { getStatus: () => ready, loadItems: async () => ({ items: rows }) }; + const tree = spec.render({ values: {}, setValues: () => {}, ui, acceptedResourceUrlPatterns: accepted }); + return find(tree, "HierarchyPicker")?.props as { + loadItems(request: { parentId: string; query: string; cursor: string | null }): Promise<{ items: { selectable: boolean; unselectableReason?: string }[] }>; + }; +} + +beforeAll(async () => { + // The frame asks for the connection's status once, on first render, and is ready once it answers. + picker(undefined); + await ready; +}); + +describe("the configurator's copy of the resource URL patterns", () => { + it.each(KINDS)("lets a %s be chosen exactly when its resource's pattern is accepted", kind => { + const values = { ref: kind === "account" ? "account" : `${kind}:PABC123`, kind }; + expect(spec.isReady({ values, acceptedResourceUrlPatterns: [patternOf(kind)] })).toBe(true); + expect(spec.isReady({ values, acceptedResourceUrlPatterns: KINDS.filter(k => k !== kind).map(patternOf) })).toBe(false); + expect(spec.isReady({ values })).toBe(true); + }); + + it.each(KINDS.filter(kind => kind !== "account") as Exclude[])( + "names what is accepted when a %s isn't", async kind => { + const row: PagerDutyNode = { ref: `${kind}:PABC123`, kind, title: "Row" }; + const others = KINDS.filter(k => k !== kind && k !== "account"); + const { items: [refused] } = await picker([patternOf(others[0])], [row]).loadItems({ parentId: "teams", query: "", cursor: null }); + expect(refused).toMatchObject({ selectable: false, unselectableReason: `This connection needs a ${others[0]}.` }); + const { items: [chosen] } = await picker([patternOf(kind)], [row]).loadItems({ parentId: "teams", query: "", cursor: null }); + expect(chosen).toMatchObject({ selectable: true }); + }); +}); diff --git a/packages/gatekeeper-pagerduty/__tests__/env.d.ts b/packages/gatekeeper-pagerduty/__tests__/env.d.ts new file mode 100644 index 0000000000..51a5a7b51d --- /dev/null +++ b/packages/gatekeeper-pagerduty/__tests__/env.d.ts @@ -0,0 +1,18 @@ +/// + +// Types for the workerd suite's environment. The pool types `env` as `Cloudflare.Env`, so the +// test-only bindings, declared in `vitest.worker.config.ts` rather than `wrangler.jsonc`, are added +// by augmenting it. + +import type { TestHooks } from "./workerd/worker.js"; +import type { UserAccount } from "../src/pagerduty.js"; + +declare global { + namespace Cloudflare { + interface Env { + TEST_HOOKS: DurableObjectNamespace; + USER_ACCOUNT: DurableObjectNamespace; + PAGERDUTY_GATEKEEPER: DurableObjectNamespace; + } + } +} diff --git a/packages/gatekeeper-pagerduty/__tests__/pagerduty-urls.test.ts b/packages/gatekeeper-pagerduty/__tests__/pagerduty-urls.test.ts new file mode 100644 index 0000000000..8e134193c5 --- /dev/null +++ b/packages/gatekeeper-pagerduty/__tests__/pagerduty-urls.test.ts @@ -0,0 +1,76 @@ +// PagerDuty's links in both regions: the resource each names, as the server parses it and as the +// Workshop matches it against the resource patterns; the canonical link for each; and the region an +// ID token's claims put an account in. + +import { describe, expect, it } from "vitest"; +import { resolveRequestedResource } from "@gadgets/workshop-shared/gatekeeper"; +import { ALL_RESOURCES, RESOURCES, type ResourceKind } from "../src/pagerduty-env"; +import { parseResourceUrl, regionOf, resourceUrl, type ResourceRef } from "../src/pagerduty-urls"; + +/** The kind of resource the Workshop matches `url` to, by the same patterns. */ +function workshopKind(url: string): ResourceKind | undefined { + const result = resolveRequestedResource(ALL_RESOURCES, url); + if (!result.ok) return undefined; + return (Object.keys(RESOURCES) as ResourceKind[]).find(kind => RESOURCES[kind] === result.resource); +} + +const LINKS: [string, ResourceRef][] = [ + ["https://acme.pagerduty.com", { region: "us", subdomain: "acme", kind: "account" }], + ["https://acme.pagerduty.com/", { region: "us", subdomain: "acme", kind: "account" }], + ["https://acme.eu.pagerduty.com/", { region: "eu", subdomain: "acme", kind: "account" }], + ["https://acme.pagerduty.com/teams/PTEAM01", { region: "us", subdomain: "acme", kind: "team", id: "PTEAM01" }], + ["https://acme.pagerduty.com/teams/PTEAM01/users", { region: "us", subdomain: "acme", kind: "team", id: "PTEAM01" }], + ["https://acme.eu.pagerduty.com/service-directory/PSVC001", { region: "eu", subdomain: "acme", kind: "service", id: "PSVC001" }], + ["https://acme.pagerduty.com/service-directory/PSVC001/activity?x=1", { region: "us", subdomain: "acme", kind: "service", id: "PSVC001" }], + ["https://acme.pagerduty.com/incidents/Q2W3E4R5T6Y7U8", { region: "us", subdomain: "acme", kind: "incident", id: "Q2W3E4R5T6Y7U8" }], + ["https://acme.eu.pagerduty.com/incidents/1234", { region: "eu", subdomain: "acme", kind: "incident", id: "1234" }], + ["https://acme.pagerduty.com/schedules/PSCH001#2026-11-06", { region: "us", subdomain: "acme", kind: "schedule", id: "PSCH001" }], + [" https://ACME.pagerduty.com/schedules/PSCH001 ", { region: "us", subdomain: "acme", kind: "schedule", id: "PSCH001" }], +]; + +describe("parseResourceUrl", () => { + it.each(LINKS)("reads %s as the Workshop matches it", (url, ref) => { + expect(parseResourceUrl(url)).toEqual(ref); + expect(workshopKind(url.trim())).toBe(ref.kind); + }); + + it.each([ + "https://pagerduty.com/teams/PTEAM01", + "https://acme.pagerduty.com.evil.test/teams/PTEAM01", + "http://acme.pagerduty.com/teams/PTEAM01", + "https://acme.pagerduty.com/users/PALICE1", + "https://acme.pagerduty.com/teams", + "https://acme.pagerduty.com/teams/pteam01", + "https://acme.de.pagerduty.com/teams/PTEAM01", + "not a link", + ])("refuses %s", url => { + expect(parseResourceUrl(url)).toBeNull(); + }); +}); + +describe("resourceUrl", () => { + it.each([ + "https://acme.pagerduty.com/", + "https://acme.eu.pagerduty.com/", + "https://acme.pagerduty.com/teams/PTEAM01", + "https://acme.eu.pagerduty.com/service-directory/PSVC001", + "https://acme.pagerduty.com/incidents/Q2W3E4R5T6Y7U8", + "https://acme.eu.pagerduty.com/schedules/PSCH001", + ])("writes %s as the link for what it names", url => { + expect(resourceUrl(parseResourceUrl(url)!)).toBe(url); + }); +}); + +describe("regionOf", () => { + it.each([ + [{ aud: ["https://api.pagerduty.com", "client"] }, "us"], + [{ aud: ["client", "https://api.eu.pagerduty.com"] }, "eu"], + [{ aud: "https://api.eu.pagerduty.com" }, "eu"], + [{ aud: ["client"], region: "UnitedStates" }, "us"], + [{ aud: ["client"], region: "EU" }, "eu"], + [{ aud: ["client"], region: "Mars" }, undefined], + [{}, undefined], + ] satisfies [Parameters[0], string | undefined][])("reads %o as %s", (claims, region) => { + expect(regionOf(claims)).toBe(region); + }); +}); diff --git a/packages/gatekeeper-pagerduty/__tests__/workerd/account.test.ts b/packages/gatekeeper-pagerduty/__tests__/workerd/account.test.ts new file mode 100644 index 0000000000..93a889975b --- /dev/null +++ b/packages/gatekeeper-pagerduty/__tests__/workerd/account.test.ts @@ -0,0 +1,134 @@ +// A connection: which user, account and region PagerDuty's ID token names, the scopes it must +// grant, the user a reconnect must come back as, and which links the connection can bind. + +import { env, runInDurableObject } from "cloudflare:test"; +import { afterEach, describe, expect, it, vi } from "vitest"; +import { generateNonce } from "@gadgets/gatekeeper-kit/connect-nonce"; +import { getRedirectUri, RESOURCES, type Env } from "../../src/pagerduty-env"; +import { ACME, ALICE, BOB, DAVE, EURO, FakePagerDuty, accountStub, failure, hooks, seedAccount, unwrap, type FakeUser } from "./fake-pagerduty"; + +afterEach(() => { + vi.unstubAllGlobals(); +}); + +/** + * Runs a first connect up to PagerDuty's callback, PagerDuty authorizing `who`. Only refusals are + * reached here: a grant that goes live mints the account entrypoint, which the test pool can't. + */ +async function connect(pd: FakePagerDuty, who: FakeUser) { + const userObjectId = env.USER_ACCOUNT.newUniqueId().toString(); + const initiation = generateNonce(); + await hooks().installCallback(userObjectId, initiation); + return { userObjectId, result: await authorize(pd, userObjectId, who, initiation) }; +} + +/** Reconnects `who`'s account as PagerDuty authorizing `as`, committing the grant it stages. */ +async function reconnect(pd: FakePagerDuty, who: FakeUser, as = who) { + const userObjectId = await seedAccount(pd, who); + await hooks().installCallback(userObjectId, generateNonce()); + const { url } = unwrap(await hooks().user(userObjectId, "reconnect")) as { url: string }; + const result = await authorize(pd, userObjectId, as, url.split("/").at(-1)!); + if ("handoff" in result!) unwrap(await hooks().user(userObjectId, "commitReconnect", result.handoff.ticket)); + return { userObjectId, result }; +} + +/** Takes the account through PagerDuty's authorization of `who`, from an armed initiation nonce. */ +async function authorize(pd: FakePagerDuty, userObjectId: string, who: FakeUser, initiation: string) { + return await runInDurableObject(accountStub(userObjectId), async instance => { + const flow = await instance.beginOAuthFlow(initiation, getRedirectUri(env as Env)); + if (!flow) throw new Error("the connect flow did not start"); + const code = `code-${crypto.randomUUID()}`; + pd.codes.set(code, who.id); + try { + return await instance.acceptAuthCode(code, flow.oauthNonce); + } catch (error) { + return { error: (error as Error).message }; + } + }); +} + +describe("connecting", () => { + it("takes the user, account and US region from the ID token, and the user's details from PagerDuty", async () => { + const pd = new FakePagerDuty().install(); + pd.users.get(ALICE.id)!.name = "Alice Liddell"; + const { userObjectId } = await reconnect(pd, ALICE); + expect(await accountStub(userObjectId).getIdentity()).toMatchObject({ + userId: ALICE.id, accountId: ACME.id, subdomain: "acme", region: "us", email: ALICE.email, name: "Alice Liddell", role: "user", + }); + expect(pd.matching("GET", /^\/users\/me$/)[0].url.origin).toBe("https://api.pagerduty.com"); + expect(unwrap(await hooks().user(userObjectId, "describe"))).toMatchObject({ + displayName: "Alice Liddell", uniqueName: "alice@acme.test · acme.pagerduty.com", + }); + }); + + it("reads an EU account's region from the audience and asks PagerDuty's EU host", async () => { + const pd = new FakePagerDuty().install(); + const { userObjectId } = await reconnect(pd, DAVE); + expect(await accountStub(userObjectId).getIdentity()).toMatchObject({ accountId: EURO.id, subdomain: "euro", region: "eu" }); + expect(pd.matching("GET", /^\/users\/me$/)[0].url.origin).toBe("https://api.eu.pagerduty.com"); + }); + + it("falls back to the region claim when the audience names no REST host", async () => { + const pd = new FakePagerDuty().install(); + pd.claims = (_user, claims) => ({ ...claims, aud: ["test-client-id"] }); + const { userObjectId } = await reconnect(pd, DAVE); + expect(await accountStub(userObjectId).getIdentity()).toMatchObject({ region: "eu" }); + expect(pd.matching("GET", /^\/users\/me$/)[0].url.origin).toBe("https://api.eu.pagerduty.com"); + }); + + it("refuses an ID token that doesn't name the account", async () => { + const pd = new FakePagerDuty().install(); + pd.claims = ({ id }) => ({ user_id: id, subdomain: "acme", region: "UnitedStates" }); + const { result } = await connect(pd, ALICE); + expect(result).toEqual({ error: expect.stringMatching(/Could not read which PagerDuty user/) }); + }); + + it("refuses a grant PagerDuty silently narrowed, naming what it dropped", async () => { + const pd = new FakePagerDuty().install(); + pd.grantedScopes = "openid incidents.read"; + const { userObjectId, result } = await connect(pd, ALICE); + expect(result).toMatchObject({ + refused: { title: "The PagerDuty app is missing permissions", detail: expect.stringMatching(/incidents\.write, services\.read/) }, + }); + expect(failure(await hooks().user(userObjectId, "describe"))).toMatch(/Reconnect the PagerDuty account/); + }); + + it("refuses a reconnect that comes back as another user", async () => { + const pd = new FakePagerDuty().install(); + const { userObjectId, result } = await reconnect(pd, ALICE, BOB); + expect(result).toMatchObject({ refused: { title: "That's a different PagerDuty user" } }); + expect(await accountStub(userObjectId).getIdentity()).toMatchObject({ userId: ALICE.id }); + }); +}); + +describe("binding", () => { + it.each([ + ["https://acme.pagerduty.com", "account"], + ["https://acme.pagerduty.com/teams/PTEAM01", "team"], + ["https://acme.pagerduty.com/service-directory/PSVC001/activity", "service"], + ["https://acme.pagerduty.com/incidents/PINC001", "incident"], + ["https://acme.pagerduty.com/schedules/PSCH001", "schedule"], + ] as const)("binds %s as the %s", async (url, kind) => { + const pd = new FakePagerDuty().install(); + const userObjectId = await seedAccount(pd, ALICE); + expect(unwrap(await hooks().user(userObjectId, "getGatekeeperClassFor", url))).toMatchObject({ resource: RESOURCES[kind] }); + }); + + it("binds an EU account's links to an EU connection", async () => { + const pd = new FakePagerDuty().install(); + const userObjectId = await seedAccount(pd, DAVE); + const bound = unwrap(await hooks().user(userObjectId, "getGatekeeperClassFor", "https://euro.eu.pagerduty.com/teams/PTEAM09")); + expect(bound).toMatchObject({ resource: RESOURCES.team }); + }); + + it.each([ + ["https://other.pagerduty.com/teams/PTEAM01", /other\.pagerduty\.com, but this connection is to acme\.pagerduty\.com/], + ["https://acme.eu.pagerduty.com/teams/PTEAM01", /acme\.eu\.pagerduty\.com, but this connection is to acme\.pagerduty\.com/], + ["https://example.com/teams/PTEAM01", /Not a PagerDuty link/], + ["https://acme.pagerduty.com/users/PALICE1", /Not a PagerDuty link/], + ])("refuses %s", async (url, message) => { + const pd = new FakePagerDuty().install(); + const userObjectId = await seedAccount(pd, ALICE); + expect(failure(await hooks().user(userObjectId, "getGatekeeperClassFor", url))).toMatch(message); + }); +}); diff --git a/packages/gatekeeper-pagerduty/__tests__/workerd/actions.test.ts b/packages/gatekeeper-pagerduty/__tests__/workerd/actions.test.ts new file mode 100644 index 0000000000..b97d3cf39f --- /dev/null +++ b/packages/gatekeeper-pagerduty/__tests__/workerd/actions.test.ts @@ -0,0 +1,261 @@ +// Actions, from staging to apply: nothing reaches PagerDuty before approval; each change is the +// REST call PagerDuty documents, sent as the connected user (`From`); each declares its kind and +// auto-approval verdict; and reads show pending changes as if they had been made. + +import { afterEach, describe, expect, it, vi } from "vitest"; +import type { ActionDescription } from "@gadgets/workshop-shared/gatekeeper"; +import type { PagerDutyIncidentSummary } from "../../src/types"; +import { + ALICE, BOB, FakePagerDuty, OTHER_SERVICE, PRIORITY, SCHEDULE, SERVICE, failure, hooks, seedAccount, sub, unwrap, +} from "./fake-pagerduty"; +import type { GatekeeperProps, Step } from "./worker"; + +afterEach(() => { + vi.unstubAllGlobals(); +}); + +type Binding = { resourceKind: "account" } | { resourceKind: "team" | "service" | "incident" | "schedule"; id: string }; + +async function setup(binding: Binding = { resourceKind: "account" }) { + const pd = new FakePagerDuty().install(); + const props = { userObjectId: await seedAccount(pd), region: "us", subdomain: "acme", ...binding } as GatekeeperProps; + const name = crypto.randomUUID(); + return { + pd, + incident: pd.incident(SERVICE), + run: async (steps: Step[], pages?: number) => unwrap(await hooks().run(name, props, steps, pages ? { pages } : {})), + attempt: async (steps: Step[]) => failure(await hooks().run(name, props, steps)), + submitted: async (): Promise> => + (await hooks().queueLog(name)).submitted, + reported: async () => (await hooks().queueLog(name)).observations.map(o => new Set(o.subresources)), + apply: async (actionId: number) => unwrap(await hooks().applyAction(name, props, actionId)), + reject: async (actionId: number) => unwrap(await hooks().rejectAction(name, props, actionId)), + autoApprovable: async () => unwrap(await hooks().autoApprovable(name, props)) as { tag: string }[], + }; +} + +const START = "2026-11-10T09:00:00.000Z"; +const END = "2026-11-10T17:00:00.000Z"; +const incidentRef = { type: "incident_reference" }; + +type Case = { + binding?: Binding; + steps: (incidentId: string) => Step[]; + method: string; + path: (incidentId: string) => string; + body: unknown; + tag: string; + autoApprovable: boolean; +}; + +const onIncident = (...step: Step) => (id: string): Step[] => [["getIncident", id], step]; +const incidentPath = (suffix = "") => (id: string) => `/incidents/${id}${suffix}`; +const schedule = { resourceKind: "schedule", id: SCHEDULE.id } as const; + +const CASES: Record = { + acknowledge: { + steps: onIncident("acknowledge"), method: "PUT", path: incidentPath(), + body: { incident: { ...incidentRef, status: "acknowledged" } }, tag: "acknowledge-incidents", autoApprovable: true, + }, + resolve: { + steps: onIncident("resolve", "Rolled back"), method: "PUT", path: incidentPath(), + body: { incident: { ...incidentRef, status: "resolved", resolution: "Rolled back" } }, tag: "resolve-incidents", autoApprovable: true, + }, + setPriority: { + steps: onIncident("setPriority", "p1"), method: "PUT", path: incidentPath(), + body: { incident: { ...incidentRef, priority: { id: PRIORITY.id, type: "priority_reference" } } }, tag: "edit-incidents", autoApprovable: true, + }, + setTitle: { + steps: onIncident("setTitle", "API down in EU"), method: "PUT", path: incidentPath(), + body: { incident: { ...incidentRef, title: "API down in EU" } }, tag: "edit-incidents", autoApprovable: true, + }, + setUrgency: { + steps: onIncident("setUrgency", "low"), method: "PUT", path: incidentPath(), + body: { incident: { ...incidentRef, urgency: "low" } }, tag: "edit-incidents", autoApprovable: true, + }, + addNote: { + steps: onIncident("addNote", "Looking into it"), method: "POST", path: incidentPath("/notes"), + body: { note: { content: "Looking into it" } }, tag: "add-notes", autoApprovable: true, + }, + reassign: { + steps: onIncident("reassign", { users: ["bob@acme.test"] }), method: "PUT", path: incidentPath(), + body: { incident: { ...incidentRef, assignments: [{ assignee: { id: BOB.id, type: "user_reference" } }] } }, + tag: "reassign-incidents", autoApprovable: false, + }, + escalate: { + steps: onIncident("escalate", 2), method: "PUT", path: incidentPath(), + body: { incident: { ...incidentRef, escalation_level: 2 } }, tag: "reassign-incidents", autoApprovable: false, + }, + requestResponders: { + steps: onIncident("requestResponders", { message: "Need a DBA", users: [BOB.id] }), method: "POST", + path: incidentPath("/responder_requests"), + body: { + requester_id: ALICE.id, message: "Need a DBA", + responder_request_targets: [{ responder_request_target: { id: BOB.id, type: "user_reference" } }], + }, + tag: "request-responders", autoApprovable: false, + }, + postStatusUpdate: { + steps: onIncident("postStatusUpdate", "Mitigated"), method: "POST", path: incidentPath("/status_updates"), + body: { message: "Mitigated" }, tag: "post-status-updates", autoApprovable: false, + }, + createIncident: { + binding: { resourceKind: "service", id: SERVICE.id }, + steps: () => [["createIncident", { title: "Checkout failing", urgency: "low" }]], method: "POST", path: () => "/incidents", + body: { incident: { type: "incident", title: "Checkout failing", service: { id: SERVICE.id, type: "service_reference" }, urgency: "low" } }, + tag: "create-incidents", autoApprovable: false, + }, + "createOverride for oneself": { + binding: schedule, + steps: () => [["createOverride", ALICE.email, START, END]], method: "POST", path: () => `/schedules/${SCHEDULE.id}/overrides`, + body: { overrides: [{ start: START, end: END, user: { id: ALICE.id, type: "user_reference" } }] }, + tag: "manage-overrides", autoApprovable: true, + }, + "createOverride for someone else": { + binding: schedule, + steps: () => [["createOverride", BOB.id, START, END]], method: "POST", path: () => `/schedules/${SCHEDULE.id}/overrides`, + body: { overrides: [{ start: START, end: END, user: { id: BOB.id, type: "user_reference" } }] }, + tag: "manage-overrides", autoApprovable: false, + }, +}; + +describe("each change", () => { + it.each(Object.entries(CASES))("%s: sends nothing until approved, then the documented request as the user", async (_name, c) => { + const t = await setup(c.binding); + await t.run(c.steps(t.incident.id)); + expect(t.pd.writes()).toEqual([]); + const [{ actionId, description }] = await t.submitted(); + expect(description).toMatchObject({ autoApprovable: c.autoApprovable, actionKind: { tag: c.tag } }); + + await t.apply(actionId); + const [write] = t.pd.writes(); + expect({ method: write.method, path: write.url.pathname, origin: write.url.origin, from: write.headers.get("from") }) + .toEqual({ method: c.method, path: c.path(t.incident.id), origin: "https://api.pagerduty.com", from: ALICE.email }); + expect(JSON.parse(write.body!)).toEqual(c.body); + }); + + it("snoozes an incident a pending acknowledgement leaves acknowledged", async () => { + const t = await setup(); + expect(await t.attempt(onIncident("snooze", 3600)(t.incident.id))).toMatch(/Only an acknowledged incident/); + await t.run(onIncident("acknowledge")(t.incident.id)); + await t.run(onIncident("snooze", 3600)(t.incident.id)); + const [acknowledge, snooze] = await t.submitted(); + expect(snooze.description).toMatchObject({ autoApprovable: true, actionKind: { tag: "acknowledge-incidents" } }); + await t.apply(acknowledge.actionId); + await t.apply(snooze.actionId); + expect(t.pd.writes().map(w => [w.method, w.url.pathname, w.body && JSON.parse(w.body)])).toEqual([ + ["PUT", `/incidents/${t.incident.id}`, { incident: { ...incidentRef, status: "acknowledged" } }], + ["POST", `/incidents/${t.incident.id}/snooze`, { duration: 3600 }], + ]); + }); + + it("removes an override, which is not auto-approvable", async () => { + const t = await setup(schedule); + t.pd.overrides.set(SCHEDULE.id, [{ id: "POVR001", start: START, end: END, user: { id: BOB.id, summary: "Bob" } }]); + await t.run([["deleteOverride", "POVR001"]]); + const [{ actionId, description }] = await t.submitted(); + expect(description).toMatchObject({ autoApprovable: false, actionKind: { tag: "delete-overrides" } }); + await t.apply(actionId); + expect(t.pd.writes().map(w => [w.method, w.url.pathname])).toEqual([["DELETE", `/schedules/${SCHEDULE.id}/overrides/POVR001`]]); + expect(t.pd.overrides.get(SCHEDULE.id)).toEqual([]); + }); +}); + +describe("the auto-approvable catalog", () => { + it.each([ + [{ resourceKind: "account" }, ["acknowledge-incidents", "resolve-incidents", "edit-incidents", "add-notes", "manage-overrides"]], + [{ resourceKind: "service", id: SERVICE.id }, ["acknowledge-incidents", "resolve-incidents", "edit-incidents", "add-notes"]], + [{ resourceKind: "incident", id: "PINC001" }, ["acknowledge-incidents", "resolve-incidents", "edit-incidents", "add-notes"]], + [schedule, ["manage-overrides"]], + ] as [Binding, string[]][])("lists for a %o binding what it can submit as auto-approvable", async (binding, tags) => { + const t = await setup(binding); + expect((await t.autoApprovable()).map(kind => kind.tag).toSorted()).toEqual(tags.toSorted()); + }); +}); + +describe("narrow bindings", () => { + it("won't let a team binding add an override to another team's schedule", async () => { + const t = await setup({ resourceKind: "team", id: "PTEAM01" }); + expect(await t.attempt([["getSchedule", "PSCH002"], ["createOverride", ALICE.email, START, END]])).toMatch(/doesn't belong to this team/); + expect(await t.submitted()).toEqual([]); + }); + + it("won't let a service binding act on another service's incident", async () => { + const t = await setup({ resourceKind: "service", id: "PSVC002" }); + expect(await t.attempt(onIncident("acknowledge")(t.incident.id))).toMatch(/belongs to another service/); + expect(await t.submitted()).toEqual([]); + }); +}); + +describe("simulation", () => { + it("shows an incident's pending changes and notes in its reads", async () => { + const t = await setup(); + const steps = (...step: Step) => onIncident(...step)(t.incident.id); + await t.run(steps("acknowledge")); + await t.run(steps("setTitle", "Database failover")); + await t.run(steps("setPriority", "P1")); + await t.run(steps("reassign", { users: [BOB.email] })); + await t.run(steps("addNote", "Failing over")); + expect(await t.run(steps("getDetails"))).toMatchObject({ + status: "acknowledged", title: "Database failover", priority: "P1", assignees: [{ id: BOB.id }], + acknowledgers: [{ user: { id: ALICE.id } }], + }); + expect(await t.run(steps("listNotes"))).toMatchObject([{ content: "Failing over", author: { id: ALICE.id } }]); + expect(t.pd.writes()).toEqual([]); + }); + + it("drops an incident a pending resolve closes from the open incidents", async () => { + const t = await setup(); + await t.run(onIncident("resolve")(t.incident.id)); + expect(await t.run([["listIncidents"]], 5)).toEqual([]); + expect(await t.run(onIncident("getDetails")(t.incident.id))).toMatchObject({ status: "resolved", assignees: [] }); + }); + + it("lists an incident a pending action opens, and binds what builds on it to the incident PagerDuty made", async () => { + const t = await setup({ resourceKind: "service", id: SERVICE.id }); + await t.run([["createIncident", { title: "Checkout failing" }], ["addNote", "Paging payments"]]); + const [[opened, existing]] = await t.run([["listIncidents"]], 5) as PagerDutyIncidentSummary[][]; + expect(opened).toMatchObject({ id: "~1", title: "Checkout failing", status: "triggered", service: { id: SERVICE.id } }); + expect(existing.id).toBe(t.incident.id); + expect(await t.run([["getIncident", "~1"], ["listNotes"]])).toMatchObject([{ content: "Paging payments" }]); + + const [create, note] = await t.submitted(); + await t.apply(create.actionId); + await t.apply(note.actionId); + const made = [...t.pd.incidents.values()].find(i => i.title === "Checkout failing")!; + expect(t.pd.notes.get(made.id)).toMatchObject([{ content: "Paging payments" }]); + expect(await t.run([["getIncident", "~1"], ["getDetails"]])).toMatchObject({ id: made.id }); + }); + + it("reports the service of an incident a pending action opens, whose details staging read from it", async () => { + const t = await setup(); + await t.run([["getService", OTHER_SERVICE.id], ["createIncident", { title: "Refunds failing" }]]); + const [[opened, existing]] = await t.run([["listIncidents"]], 5) as PagerDutyIncidentSummary[][]; + expect([opened.service.name, existing.id]).toEqual(["Billing", t.incident.id]); + expect(await t.reported()).toEqual([new Set([sub(), sub("service", SERVICE.id), sub("service", OTHER_SERVICE.id)])]); + }); + + it("asks the gadget to restart when a rejected incident had work built on it", async () => { + const t = await setup({ resourceKind: "service", id: SERVICE.id }); + await t.run([["createIncident", { title: "Checkout failing" }], ["addNote", "Paging payments"]]); + const [create] = await t.submitted(); + expect(await t.reject(create.actionId)).toEqual({ restart: true }); + expect(await t.run([["listIncidents"]], 5)).toMatchObject([[{ id: t.incident.id }]]); + }); + + it("shows pending overrides in a schedule's overrides and shifts, and hides one pending removal", async () => { + const t = await setup(schedule); + t.pd.overrides.set(SCHEDULE.id, [{ id: "POVR001", start: START, end: END, user: { id: ALICE.id, summary: "Alice" } }]); + t.pd.shifts.set(SCHEDULE.id, [{ start: "2026-11-10T00:00:00.000Z", end: "2026-11-11T00:00:00.000Z", user: { id: ALICE.id, summary: "Alice" } }]); + const ref = await t.run([["createOverride", BOB.email, START, END]]); + expect(ref).toBe("~1"); + await t.run([["deleteOverride", "POVR001"]]); + const window = { since: "2026-11-10T00:00:00.000Z", until: "2026-11-11T00:00:00.000Z" }; + expect(await t.run([["listOverrides", window]])).toEqual([{ id: "~1", user: { id: BOB.id, name: "Bob" }, start: START, end: END }]); + expect(await t.run([["listShifts", window]])).toEqual([ + { user: { id: ALICE.id, name: "Alice" }, start: "2026-11-10T00:00:00.000Z", end: START }, + { user: { id: BOB.id, name: "Bob" }, start: START, end: END }, + { user: { id: ALICE.id, name: "Alice" }, start: END, end: "2026-11-11T00:00:00.000Z" }, + ]); + }); +}); diff --git a/packages/gatekeeper-pagerduty/__tests__/workerd/fake-pagerduty.ts b/packages/gatekeeper-pagerduty/__tests__/workerd/fake-pagerduty.ts new file mode 100644 index 0000000000..b43bfd7c14 --- /dev/null +++ b/packages/gatekeeper-pagerduty/__tests__/workerd/fake-pagerduty.ts @@ -0,0 +1,384 @@ +// PagerDuty, faked at the `fetch` boundary for the workerd suites. Installed with `vi.stubGlobal`, +// which reaches the gatekeeper because the whole suite -- test file, TestHooks, and the Durable +// Objects -- runs in one isolate. It plays the identity service (token endpoint, ID-token signing +// keys) and both regions' REST hosts: a token names a user, and only that user's region answers it. +// The fake keeps state, so a change an approved action makes is visible to the reads that follow, +// and a user can be refused an object (`hide`). A test injects an answer with `on()`, whose routes +// answer before the built-in ones, latest first. Anything unrouted, or sent elsewhere, fails. + +import { env, runInDurableObject } from "cloudflare:test"; +import { SignJWT, importJWK, type JWK } from "jose"; +import { vi } from "vitest"; +import { identityOf } from "../../src/pagerduty-credentials"; +import { ID_TOKEN_ISSUER, SCOPES, apiOrigin, type Region } from "../../src/pagerduty-env"; +import type { + WireIncident, WireNote, WireOnCall, WireOverride, WirePriority, WireRef, WireSchedule, WireService, WireShift, + WireTeam, WireUser, +} from "../../src/pagerduty-normalize"; + +export const IDENTITY = "https://identity.pagerduty.com"; + +// A fixed signing key, so the gatekeeper's cached key set stays valid across every suite. +const SIGNING_KEY: JWK = { + kty: "EC", crv: "P-256", kid: "test", + x: "AcsMwshrsi4kZrZtPB6a2RuG1SoKJf5G7WFAUgJ5oWs", + y: "ZmzG1xvWwGG1T9dx4FIjPQQ2hb8os12EpFbrSWkuhko", + d: "CTIFxWDkJ7t3WMpmK5nB7o365c5ANKIk0iOJkCkXXGE", +}; + +export type Account = { id: string; region: Region; subdomain: string }; +export type FakeUser = WireUser & { account: Account }; + +export const ACME: Account = { id: "PACME01", region: "us", subdomain: "acme" }; +export const EURO: Account = { id: "PEURO01", region: "eu", subdomain: "euro" }; + +const user = (id: string, name: string, account: Account, role = "user"): FakeUser => ({ + id, type: "user", name, email: `${name.toLowerCase()}@${account.subdomain}.test`, role, time_zone: "Etc/UTC", + html_url: `https://${account.subdomain}.pagerduty.com/users/${id}`, account, +}); + +export const ALICE = user("PALICE1", "Alice", ACME); +export const BOB = user("PBOB001", "Bob", ACME); +/** A Limited Stakeholder, who sees no incidents or services. */ +export const CAROL = user("PCAROL1", "Carol", ACME, "read_only_limited_user"); +export const DAVE = user("PDAVE01", "Dave", EURO); + +const ref = (type: string, id: string, name: string): WireRef => ({ id, type: `${type}_reference`, summary: name }); + +export const TEAM = ref("team", "PTEAM01", "Platform"); +export const OTHER_TEAM = ref("team", "PTEAM02", "Payments"); +export const POLICY = ref("escalation_policy", "PEP0001", "Platform on call"); +export const OTHER_POLICY = ref("escalation_policy", "PEP0002", "Payments on call"); +export const SERVICE = ref("service", "PSVC001", "API"); +export const OTHER_SERVICE = ref("service", "PSVC002", "Billing"); +export const SCHEDULE = ref("schedule", "PSCH001", "Platform primary"); +export const OTHER_SCHEDULE = ref("schedule", "PSCH002", "Payments primary"); +export const PRIORITY: WirePriority = { id: "PPRI001", name: "P1" }; + +export type FakeRequest = { method: string; url: URL; headers: Headers; body?: string; userId?: string }; +type Handler = (request: FakeRequest) => Response | undefined | Promise; + +export function json(data: unknown, init: ResponseInit = {}): Response { + return new Response(JSON.stringify(data), { status: 200, ...init, headers: { "content-type": "application/json" } }); +} + +const error = (status: number, message: string) => json({ error: { code: status * 10, message } }, { status }); + +/** A listing page of `rows`, as PagerDuty's classic offset pagination answers. */ +function page(url: URL, key: string, rows: unknown[]): Response { + const offset = Number(url.searchParams.get("offset") ?? 0); + const limit = Number(url.searchParams.get("limit") ?? 25); + return json({ [key]: rows.slice(offset, offset + limit), offset, limit, more: offset + limit < rows.length }); +} + +/** Whether `row` belongs to one of the teams `url` filters by. */ +const inTeams = (url: URL, row: { teams?: WireRef[] }): boolean => { + const teams = url.searchParams.getAll("team_ids[]"); + return teams.length === 0 || (row.teams ?? []).some(({ id }) => teams.includes(id)); +}; + +export class FakePagerDuty { + readonly requests: FakeRequest[] = []; + readonly users = new Map([ALICE, BOB, CAROL, DAVE].map(u => [u.id, { ...u }])); + readonly teams = new Map([TEAM, OTHER_TEAM].map(t => [t.id, { ...t, type: "team", name: t.summary }])); + readonly services = new Map([ + [SERVICE.id, { ...SERVICE, type: "service", name: "API", status: "active", teams: [TEAM], escalation_policy: POLICY }], + [OTHER_SERVICE.id, { ...OTHER_SERVICE, type: "service", name: "Billing", status: "active", teams: [OTHER_TEAM], escalation_policy: OTHER_POLICY }], + ]); + readonly policies: (WireRef & { teams: WireRef[] })[] = [{ ...POLICY, teams: [TEAM] }, { ...OTHER_POLICY, teams: [OTHER_TEAM] }]; + readonly schedules = new Map([ + [SCHEDULE.id, { ...SCHEDULE, type: "schedule", name: "Platform primary", time_zone: "Etc/UTC", teams: [TEAM], escalation_policies: [POLICY] }], + [OTHER_SCHEDULE.id, { ...OTHER_SCHEDULE, type: "schedule", name: "Payments primary", time_zone: "Etc/UTC", teams: [OTHER_TEAM], escalation_policies: [OTHER_POLICY] }], + ]); + readonly incidents = new Map(); + readonly notes = new Map(); + readonly shifts = new Map(); + readonly overrides = new Map(); + readonly priorities: WirePriority[] = [PRIORITY, { id: "PPRI002", name: "P2" }]; + oncalls: WireOnCall[] = []; + /** The scopes the token endpoint grants. */ + grantedScopes = SCOPES.join(" "); + /** Adjusts the ID token's claims, for tests of what the gatekeeper reads from them. */ + claims: (user: FakeUser, claims: Record) => Record = (_user, claims) => claims; + /** Which user each access or refresh token belongs to. */ + readonly tokens = new Map(); + /** Which user each authorization code authorizes. */ + readonly codes = new Map(); + /** `user:object` pairs PagerDuty refuses to open, and with which status. */ + readonly #hidden = new Map(); + #routes: Array<{ method: string; pattern: RegExp; handler: Handler }> = []; + #next = 1; + + install(): this { + vi.stubGlobal("fetch", (input: string | URL | Request, init?: RequestInit) => this.#handle(input, init)); + return this; + } + + /** Answers before the built-in routes; return `undefined` to fall through to them. */ + on(method: string, pattern: RegExp, handler: Handler): this { + this.#routes.unshift({ method, pattern, handler }); + return this; + } + + /** Makes PagerDuty refuse `userId` the object `id`, with 403 or 404. */ + hide(userId: string, id: string, status: 403 | 404 = 403): this { + this.#hidden.set(`${userId}:${id}`, status); + return this; + } + + /** The recorded requests matching, optionally only those made with `userId`'s token. */ + matching(method: string, pattern: RegExp, userId?: string): FakeRequest[] { + return this.requests.filter(r => r.method === method && pattern.test(r.url.pathname + r.url.search) + && (userId === undefined || r.userId === userId)); + } + + /** The requests that would change something at PagerDuty. */ + writes(): FakeRequest[] { + return this.requests.filter(r => r.method !== "GET" && r.url.origin !== IDENTITY); + } + + /** Adds an incident on `service`, open and assigned to Alice unless `extra` says otherwise. */ + incident(service: WireRef, extra: Partial = {}): WireIncident { + const n = this.#next++; + const wire: WireIncident = { + id: `PINC${String(n).padStart(3, "0")}`, type: "incident", incident_number: n, title: `Incident ${n}`, + status: "triggered", urgency: "high", priority: null, service, + teams: [this.services.get(service.id)!.teams![0]], escalation_policy: this.services.get(service.id)!.escalation_policy, + assignments: [{ assignee: ref("user", ALICE.id, ALICE.name!) }], acknowledgements: [], + created_at: new Date(Date.now() - n * 60_000).toISOString(), html_url: `https://acme.pagerduty.com/incidents/${n}`, + alert_counts: { all: 0 }, ...extra, + }; + this.incidents.set(wire.id, wire); + return wire; + } + + /** Mints a token for `userId`, as a code exchange or refresh would. */ + grant(userId: string) { + const n = this.#next++; + this.tokens.set(`access-${n}`, userId); + this.tokens.set(`refresh-${n}`, userId); + return { access_token: `access-${n}`, refresh_token: `refresh-${n}`, expires_in: 86_400, token_type: "bearer", scope: this.grantedScopes }; + } + + async #handle(input: string | URL | Request, init: RequestInit = {}): Promise { + const request = input instanceof Request ? input : new Request(String(input), init); + const url = new URL(request.url); + const recorded: FakeRequest = { method: request.method, url, headers: new Headers(request.headers) }; + if (request.body) recorded.body = await request.text(); + const token = request.headers.get("authorization")?.replace(/^Bearer /, ""); + if (token !== undefined) recorded.userId = this.tokens.get(token); + this.requests.push(recorded); + if (url.origin === IDENTITY) return await this.#identity(recorded); + if (url.origin !== apiOrigin("us") && url.origin !== apiOrigin("eu")) { + throw new Error(`fake PagerDuty: ${request.method} ${url} was not sent to PagerDuty`); + } + if (request.redirect !== "manual") throw new Error(`fake PagerDuty: ${request.method} ${url} would follow redirects`); + for (const route of this.#routes) { + if (route.method === request.method && route.pattern.test(url.pathname + url.search)) { + const answer = await route.handler(recorded); + if (answer) return answer; + } + } + return this.#rest(recorded); + } + + async #identity(request: FakeRequest): Promise { + const { method, url } = request; + if (method === "GET" && url.pathname === "/global/oauth/anonymous/jwks") { + const { d: _private, ...publicKey } = SIGNING_KEY; + return json({ keys: [{ ...publicKey, alg: "ES256", use: "sig" }] }); + } + if (method !== "POST" || url.pathname !== "/oauth/token") throw new Error(`fake PagerDuty: ${method} ${url} is not routed`); + const form = new URLSearchParams(request.body); + if (form.get("client_id") !== "test-client-id" || form.get("client_secret") !== "test-client-secret") { + return json({ error: "invalid_client" }, { status: 401 }); + } + const grantee = form.get("grant_type") === "authorization_code" + ? this.codes.get(form.get("code") ?? "") : this.tokens.get(form.get("refresh_token") ?? ""); + if (grantee === undefined) return json({ error: "invalid_grant" }, { status: 400 }); + const who = this.users.get(grantee)!; + const claims = this.claims(who, { + user_id: who.id, account_id: who.account.id, subdomain: who.account.subdomain, + region: who.account.region === "eu" ? "EU" : "UnitedStates", + }); + const idToken = await new SignJWT(claims) + .setProtectedHeader({ alg: "ES256", kid: SIGNING_KEY.kid }) + .setIssuer(ID_TOKEN_ISSUER) + .setAudience([apiOrigin(who.account.region), "test-client-id"]) + .setIssuedAt() + .setExpirationTime("1h") + .sign(await importJWK(SIGNING_KEY, "ES256")); + return json({ ...this.grant(grantee), id_token: idToken }); + } + + #rest(request: FakeRequest): Response { + const { method, url } = request; + const me = request.userId === undefined ? undefined : this.users.get(request.userId); + // A token works only on its account's regional host. + if (!me || url.origin !== apiOrigin(me.account.region)) return error(401, "Authentication failed"); + if (request.headers.get("accept") !== "application/vnd.pagerduty+json;version=2") return error(406, "Unversioned"); + if (method !== "GET" && request.headers.get("from") !== me.email) return error(400, "From header must name a user"); + const path = url.pathname; + const body = () => JSON.parse(request.body ?? "{}"); + const segments = path.split("/").slice(1); + const id = segments[1]; + const hidden = id ? this.#hidden.get(`${me.id}:${id}`) : undefined; + if (method === "GET" && segments.length === 2 && hidden) return error(hidden, "Not allowed"); + const query = url.searchParams.get("query")?.toLowerCase(); + const named = (row: WireRef) => !query || (row.name ?? row.summary ?? "").toLowerCase().includes(query); + let match: RegExpMatchArray | null; + + if (method === "GET" && path === "/users/me") { + const { account: _account, ...wire } = me; + return json({ user: wire }); + } + if (method === "GET" && (match = path.match(/^\/users\/(\w+)$/))) { + const found = this.users.get(match[1]); + return found ? json({ user: found }) : error(404, "Not found"); + } + if (method === "GET" && path === "/users") { + return page(url, "users", [...this.users.values()].filter(u => u.account === me.account + && (!query || u.email.includes(query) || u.name!.toLowerCase().includes(query)))); + } + if (method === "GET" && path === "/priorities") return page(url, "priorities", this.priorities); + if (method === "GET" && path === "/teams") return page(url, "teams", [...this.teams.values()].filter(named)); + if (method === "GET" && (match = path.match(/^\/teams\/(\w+)$/))) { + const team = this.teams.get(match[1]); + return team ? json({ team }) : error(404, "Not found"); + } + if (method === "GET" && (match = path.match(/^\/teams\/(\w+)\/members$/))) { + return page(url, "members", match[1] === TEAM.id ? [{ user: this.users.get(ALICE.id), role: "manager" }] : []); + } + if (method === "GET" && path === "/services") { + return page(url, "services", [...this.services.values()].filter(s => named(s) && inTeams(url, s))); + } + if (method === "GET" && (match = path.match(/^\/services\/(\w+)$/))) { + const service = this.services.get(match[1]); + return service ? json({ service }) : error(404, "Not found"); + } + if (method === "GET" && path === "/escalation_policies") { + return page(url, "escalation_policies", this.policies.filter(p => inTeams(url, p))); + } + if (method === "GET" && (match = path.match(/^\/escalation_policies\/(\w+)$/))) { + const policy = this.policies.find(p => p.id === match![1]); + return policy ? json({ escalation_policy: policy }) : error(404, "Not found"); + } + if (method === "GET" && path === "/oncalls") { + const policies = url.searchParams.getAll("escalation_policy_ids[]"); + const schedules = url.searchParams.getAll("schedule_ids[]"); + const users = url.searchParams.getAll("user_ids[]"); + return page(url, "oncalls", this.oncalls.filter(o => (!policies.length || policies.includes(o.escalation_policy.id)) + && (!schedules.length || (o.schedule && schedules.includes(o.schedule.id))) + && (!users.length || users.includes(o.user.id)))); + } + if (method === "GET" && path === "/schedules") { + return page(url, "schedules", [...this.schedules.values()].filter(s => named(s) && inTeams(url, s))); + } + if (method === "GET" && (match = path.match(/^\/schedules\/(\w+)$/))) { + const schedule = this.schedules.get(match[1]); + if (!schedule) return error(404, "Not found"); + return json({ schedule: { ...schedule, final_schedule: { rendered_schedule_entries: this.shifts.get(schedule.id) ?? [] } } }); + } + if ((match = path.match(/^\/schedules\/(\w+)\/overrides(?:\/(\w+))?$/))) { + const [, scheduleId, overrideId] = match; + const list = this.overrides.get(scheduleId) ?? []; + if (method === "GET") return json({ overrides: list }); + if (method === "POST") { + const results = (body().overrides as WireShift[]).map(o => { + const made: WireOverride = { id: `POVR${this.#next++}`, start: o.start, end: o.end, user: o.user }; + this.overrides.set(scheduleId, [...this.overrides.get(scheduleId) ?? [], made]); + return { status: 201, override: made }; + }); + return json(results, { status: 201 }); + } + if (method === "DELETE") { + if (!list.some(o => o.id === overrideId)) return error(404, "Not found"); + this.overrides.set(scheduleId, list.filter(o => o.id !== overrideId)); + return new Response(null, { status: 204 }); + } + } + if (method === "GET" && path === "/incidents") { + const statuses = url.searchParams.getAll("statuses[]"); + const services = url.searchParams.getAll("service_ids[]"); + const users = url.searchParams.getAll("user_ids[]"); + const since = url.searchParams.get("since"); + const until = url.searchParams.get("until"); + return page(url, "incidents", [...this.incidents.values()].filter(i => (!statuses.length || statuses.includes(i.status)) + && (!services.length || services.includes(i.service.id)) && inTeams(url, i) + && (!users.length || (i.assignments ?? []).some(({ assignee }) => users.includes(assignee.id))) + && (!since || i.created_at >= since) && (!until || i.created_at < until)) + .toSorted((a, b) => b.created_at.localeCompare(a.created_at))); + } + if (method === "POST" && path === "/incidents") { + const { incident } = body(); + return json({ incident: this.incident(this.services.get(incident.service.id)!, { title: incident.title }) }, { status: 201 }); + } + if ((match = path.match(/^\/incidents\/(\w+)(?:\/(\w+))?$/))) { + const [, key, sub] = match; + const incident = this.incidents.get(key) ?? [...this.incidents.values()].find(i => String(i.incident_number) === key); + if (!incident) return error(404, "Not found"); + if (method === "GET" && !sub) return json({ incident }); + if (method === "GET" && sub === "alerts") return page(url, "alerts", []); + if (method === "GET" && sub === "log_entries") return page(url, "log_entries", []); + if (method === "GET" && sub === "notes") return json({ notes: this.notes.get(incident.id) ?? [] }); + if (method === "PUT" && !sub) { + const { incident: change } = body(); + if (change.status) incident.status = change.status; + if (change.title) incident.title = change.title; + return json({ incident }); + } + if (method === "POST" && sub === "notes") { + const note: WireNote = { id: `PNOTE${this.#next++}`, content: body().note.content, created_at: new Date().toISOString(), user: me }; + this.notes.set(incident.id, [...this.notes.get(incident.id) ?? [], note]); + return json({ note }, { status: 201 }); + } + if (method === "POST" && (sub === "snooze" || sub === "responder_requests" || sub === "status_updates")) { + return json({ incident }, { status: 201 }); + } + } + throw new Error(`fake PagerDuty: ${method} ${url} is not routed`); + } +} + +/** + * Seeds a connected account for `who` whose token PagerDuty will accept and that needs no refresh, + * the way a completed connect leaves one. Returns the account id. + */ +export async function seedAccount(pd: FakePagerDuty, who: FakeUser = ALICE): Promise { + const grant = pd.grant(who.id); + const accountId = env.USER_ACCOUNT.newUniqueId(); + await runInDurableObject(env.USER_ACCOUNT.get(accountId), async (_instance, state) => { + state.storage.kv.put("credentials", { + accessToken: grant.access_token, refreshToken: grant.refresh_token, expiresAt: Date.now() + 3_600_000, scopes: [...SCOPES], + }); + const { account } = who; + state.storage.kv.put("identity", identityOf( + { region: account.region, subdomain: account.subdomain, accountId: account.id, userId: who.id }, who)); + }); + return accountId.toString(); +} + +export function accountStub(userObjectId: string) { + return env.USER_ACCOUNT.get(env.USER_ACCOUNT.idFromString(userObjectId)); +} + +export function hooks() { + return env.TEST_HOOKS.get(env.TEST_HOOKS.idFromName("hooks")); +} + +/** A forwarded call's result, rethrown here when it failed. */ +export function unwrap(outcome: { ok: T } | { error: string }): T { + if ("error" in outcome) throw new Error(outcome.error); + return outcome.ok; +} + +/** A forwarded call's failure message, failing the test when it succeeded. */ +export function failure(outcome: { ok: unknown } | { error: string }): string { + if (!("error" in outcome)) throw new Error(`expected a failure, got ${JSON.stringify(outcome.ok)}`); + return outcome.error; +} + +/** The subresource ID of Acme's account, or of one object in it. */ +export function sub(kind?: string, id?: string): string { + return kind ? `region/us/account/${ACME.id}/${kind}/${id}` : `region/us/account/${ACME.id}`; +} diff --git a/packages/gatekeeper-pagerduty/__tests__/workerd/observers.test.ts b/packages/gatekeeper-pagerduty/__tests__/workerd/observers.test.ts new file mode 100644 index 0000000000..0f78b57d29 --- /dev/null +++ b/packages/gatekeeper-pagerduty/__tests__/workerd/observers.test.ts @@ -0,0 +1,127 @@ +// Who may observe a workspace that read PagerDuty: an observer whose own connection is to the same +// account, with a base role that sees its objects, and which PagerDuty opens each reported object +// for. A 403 or 404 is a definite no; anything else fails the whole answer. Answers are reused per +// observer connection for a while, so a busy workspace doesn't probe PagerDuty on every read. + +import { afterEach, describe, expect, it, vi } from "vitest"; +import { + ALICE, BOB, CAROL, DAVE, FakePagerDuty, POLICY, SCHEDULE, SERVICE, TEAM, failure, hooks, json, seedAccount, sub, unwrap, +} from "./fake-pagerduty"; +import type { GatekeeperProps } from "./worker"; + +afterEach(() => { + vi.unstubAllGlobals(); +}); + +/** An account binding owned by Alice that has read once, so it is pinned to her account. */ +async function setup() { + const pd = new FakePagerDuty().install(); + const props: GatekeeperProps = { userObjectId: await seedAccount(pd), region: "us", subdomain: "acme", resourceKind: "account" }; + const name = crypto.randomUUID(); + unwrap(await hooks().run(name, props, [["getMetadata"]])); + return { + pd, + verify: async (observers: { id: string; userObjectId: string }[], subresources: string[]) => + await hooks().verifyObserverAccess(name, props, observers, subresources), + unread: { name: crypto.randomUUID(), props }, + name, props, + }; +} + +const SERVICE_ID = sub("service", SERVICE.id); + +describe("verifyObserverAccess", () => { + it("allows a colleague PagerDuty opens each object for, and denies one it refuses with 403 or 404", async () => { + const t = await setup(); + t.pd.hide(BOB.id, SERVICE.id, 403); + const observers = [ + { id: "alice", userObjectId: await seedAccount(t.pd, ALICE) }, + { id: "bob", userObjectId: await seedAccount(t.pd, BOB) }, + ]; + expect(unwrap(await t.verify(observers, [sub(), SERVICE_ID]))).toEqual({ allowedObserverIds: ["alice"] }); + expect(unwrap(await t.verify(observers, [sub()]))).toEqual({ allowedObserverIds: ["alice", "bob"] }); + t.pd.hide(ALICE.id, SERVICE.id, 404); + const later = [{ id: "alice", userObjectId: await seedAccount(t.pd, ALICE) }]; + expect(unwrap(await t.verify(later, [sub(), SERVICE_ID]))).toEqual({ allowedObserverIds: [] }); + }); + + it("opens each kind of object where PagerDuty keeps it, denying the whole set for one refusal", async () => { + const t = await setup(); + const incident = t.pd.incident(SERVICE); + const objects = [ + sub("team", TEAM.id), sub("incident", incident.id), sub("schedule", SCHEDULE.id), sub("escalation_policy", POLICY.id), + ]; + const bob = [{ id: "bob", userObjectId: await seedAccount(t.pd, BOB) }]; + expect(unwrap(await t.verify(bob, [sub(), ...objects]))).toEqual({ allowedObserverIds: ["bob"] }); + expect(t.pd.matching("GET", /./, BOB.id).map(r => r.url.pathname).filter(path => path !== "/users/me").toSorted()).toEqual([ + `/escalation_policies/${POLICY.id}`, `/incidents/${incident.id}`, `/schedules/${SCHEDULE.id}`, `/teams/${TEAM.id}`, + ]); + t.pd.hide(ALICE.id, SCHEDULE.id); + const alice = [{ id: "alice", userObjectId: await seedAccount(t.pd, ALICE) }]; + expect(unwrap(await t.verify(alice, [sub(), ...objects]))).toEqual({ allowedObserverIds: [] }); + }); + + it("denies an observer connected to another account, without asking PagerDuty about its objects", async () => { + const t = await setup(); + const dave = await seedAccount(t.pd, DAVE); + expect(unwrap(await t.verify([{ id: "dave", userObjectId: dave }], [sub(), SERVICE_ID]))).toEqual({ allowedObserverIds: [] }); + expect(t.pd.matching("GET", /^\/services\//, DAVE.id)).toHaveLength(0); + }); + + it("denies a Limited Stakeholder the account, whose role sees none of its objects", async () => { + const t = await setup(); + const carol = await seedAccount(t.pd, CAROL); + const bob = await seedAccount(t.pd, BOB); + expect(unwrap(await t.verify([{ id: "carol", userObjectId: carol }, { id: "bob", userObjectId: bob }], [sub()]))) + .toEqual({ allowedObserverIds: ["bob"] }); + }); + + it("reuses an observer connection's answers rather than probing again", async () => { + const t = await setup(); + const bob = await seedAccount(t.pd, BOB); + const carol = await seedAccount(t.pd, CAROL); + for (let i = 0; i < 2; i++) { + expect(unwrap(await t.verify([{ id: "bob", userObjectId: bob }, { id: "carol", userObjectId: carol }], [sub(), SERVICE_ID]))) + .toEqual({ allowedObserverIds: ["bob"] }); + } + expect(t.pd.matching("GET", /^\/services\/PSVC001$/, BOB.id)).toHaveLength(1); + expect(t.pd.matching("GET", /^\/users\/me$/, BOB.id)).toHaveLength(1); + expect(t.pd.matching("GET", /^\/users\/me$/, CAROL.id)).toHaveLength(1); + }); + + it("fails the whole answer when PagerDuty can't say, rather than deny", async () => { + const t = await setup(); + const bob = await seedAccount(t.pd, BOB); + t.pd.on("GET", /^\/services\//, () => json({ error: { message: "Internal error" } }, { status: 500 })); + expect(failure(await t.verify([{ id: "bob", userObjectId: bob }], [sub(), SERVICE_ID]))).toMatch(/HTTP 500/); + }); + + it("refuses IDs of another account, and IDs that aren't PagerDuty's", async () => { + const t = await setup(); + const observers = [{ id: "bob", userObjectId: await seedAccount(t.pd, BOB) }]; + expect(failure(await t.verify(observers, [sub(), "region/eu/account/PACME01"]))).toMatch(/another PagerDuty account/); + expect(failure(await t.verify(observers, [sub(), "region/us/account/POTHER1/service/PSVC001"]))).toMatch(/another PagerDuty account/); + for (const id of [ + "region/us/account/PACME01/user/PALICE1", + "region/us/account/PACME01/service/psvc001", + "region/ca/account/PACME01", + "account/PACME01", + "pagerduty/region/us/account/PACME01", + "region/us/account/PACME01/service/PSVC001/incident/PINC001", + ]) { + expect(failure(await t.verify(observers, [sub(), id])), id).toMatch(/Not a PagerDuty subresource ID/); + } + }); + + it("refuses to answer for a binding that has read nothing yet", async () => { + const t = await setup(); + const observers = [{ id: "bob", userObjectId: await seedAccount(t.pd, BOB) }]; + expect(failure(await hooks().verifyObserverAccess(t.unread.name, t.unread.props, observers, [sub()]))) + .toMatch(/has not read anything yet/); + }); + + it("has no legacy history to export", async () => { + const t = await setup(); + expect(unwrap(await hooks().historicalSubresources(t.name, t.props))).toEqual([]); + }); +}); diff --git a/packages/gatekeeper-pagerduty/__tests__/workerd/reads.test.ts b/packages/gatekeeper-pagerduty/__tests__/workerd/reads.test.ts new file mode 100644 index 0000000000..ac5dd5cb08 --- /dev/null +++ b/packages/gatekeeper-pagerduty/__tests__/workerd/reads.test.ts @@ -0,0 +1,224 @@ +// What each read reports: the account always, the objects its capability is scoped to, those its +// filter names and those its result shows -- on empty pages and cache hits too -- and the narrower +// capabilities a broader one opens, which refuse anything outside them. + +import { afterEach, describe, expect, it, vi } from "vitest"; +import type { PagerDutyIncidentDetails } from "../../src/types"; +import { + ALICE, FakePagerDuty, OTHER_SCHEDULE, OTHER_SERVICE, POLICY, SCHEDULE, SERVICE, TEAM, failure, hooks, seedAccount, sub, unwrap, +} from "./fake-pagerduty"; +import type { GatekeeperProps, Step } from "./worker"; + +afterEach(() => { + vi.unstubAllGlobals(); +}); + +type Binding = { resourceKind: "account" } | { resourceKind: "team" | "service" | "incident" | "schedule"; id: string }; + +async function setup(binding: Binding = { resourceKind: "account" }, pd = new FakePagerDuty().install()) { + const props = { userObjectId: await seedAccount(pd), region: "us", subdomain: "acme", ...binding } as GatekeeperProps; + const name = crypto.randomUUID(); + return { + pd, + run: async (steps: Step[], pages?: number) => unwrap(await hooks().run(name, props, steps, pages ? { pages } : {})), + attempt: async (steps: Step[]) => failure(await hooks().run(name, props, steps)), + /** What each observation so far reported, order aside. */ + reported: async () => (await hooks().queueLog(name)).observations.map(o => new Set(o.subresources)), + }; +} + +const ids = (...list: string[]) => new Set([sub(), ...list]); + +describe("an account binding", () => { + it("reports the account on its own reads, a cached one included", async () => { + const t = await setup(); + expect(await t.run([["getMetadata"]])).toMatchObject({ subdomain: "acme", htmlUrl: "https://acme.pagerduty.com", you: { id: ALICE.id } }); + await t.run([["listPriorities"]]); + await t.run([["listPriorities"]]); + await t.run([["findUsers", "bob"]]); + expect(t.pd.matching("GET", /^\/priorities/)).toHaveLength(1); + expect(await t.reported()).toEqual([ids(), ids(), ids(), ids()]); + }); + + it("reports each incident page's services, open incidents of the last 30 days by default", async () => { + const t = await setup(); + t.pd.incident(SERVICE); + t.pd.incident(SERVICE); + t.pd.incident(OTHER_SERVICE); + const [page] = await t.run([["listIncidents"]], 5) as PagerDutyIncidentDetails[][]; + expect(page).toHaveLength(3); + expect(await t.reported()).toEqual([ids(sub("service", SERVICE.id), sub("service", OTHER_SERVICE.id))]); + const { searchParams } = t.pd.matching("GET", /^\/incidents\?/)[0].url; + expect(searchParams.getAll("statuses[]")).toEqual(["triggered", "acknowledged"]); + expect(Date.parse(searchParams.get("until")!) - Date.parse(searchParams.get("since")!)).toBe(30 * 24 * 60 * 60_000); + }); + + it("reports the services a filter names, though the page is empty", async () => { + const t = await setup(); + t.pd.incident(SERVICE); + expect(await t.run([["listIncidents", { serviceIds: [OTHER_SERVICE.id] }]], 5)).toEqual([]); + expect(await t.reported()).toEqual([ids(sub("service", OTHER_SERVICE.id))]); + }); + + it("reports each page's own services as a listing is walked", async () => { + const t = await setup(); + t.pd.incident(SERVICE); + t.pd.incident(OTHER_SERVICE); + t.pd.incident(SERVICE); + const pages = await t.run([["listIncidents", { pageSize: 1 }]], 5) as PagerDutyIncidentDetails[][]; + expect(pages.map(page => page.map(row => row.service.id))).toEqual([[SERVICE.id], [OTHER_SERVICE.id], [SERVICE.id]]); + expect(await t.reported()).toEqual([ + ids(sub("service", SERVICE.id)), ids(sub("service", OTHER_SERVICE.id)), ids(sub("service", SERVICE.id)), + ]); + }); + + it("keeps an incident search within PagerDuty's six-month window unless it asks for all time", async () => { + const t = await setup(); + expect(await t.attempt([["listIncidents", { since: "2026-01-01T00:00:00Z", until: "2026-08-01T00:00:00Z" }]])) + .toMatch(/at most 6 months apart/); + await t.run([["listIncidents", { allTime: true }]], 1); + const { searchParams } = t.pd.matching("GET", /^\/incidents\?/)[0].url; + expect([searchParams.get("date_range"), searchParams.has("since"), searchParams.has("until")]).toEqual(["all", false, false]); + }); + + it("reports each service, team and schedule it lists", async () => { + const t = await setup(); + await t.run([["listServices"]], 5); + await t.run([["listTeams"]], 5); + await t.run([["listSchedules", { query: "platform" }]], 5); + expect(await t.reported()).toEqual([ + ids(sub("service", SERVICE.id), sub("service", OTHER_SERVICE.id)), + ids(sub("team", TEAM.id), sub("team", "PTEAM02")), + ids(sub("schedule", SCHEDULE.id)), + ]); + }); + + it("reports the schedules an on-call filter names and the policies and schedules its entries show", async () => { + const t = await setup(); + t.pd.oncalls = [{ user: { id: ALICE.id, summary: "Alice" }, escalation_level: 1, escalation_policy: POLICY, schedule: SCHEDULE }]; + expect(await t.run([["listOnCalls", { scheduleIds: [OTHER_SCHEDULE.id] }]])).toEqual([]); + expect(await t.run([["listOnCalls"]])).toHaveLength(1); + expect(await t.reported()).toEqual([ + ids(sub("schedule", OTHER_SCHEDULE.id)), + ids(sub("escalation_policy", POLICY.id), sub("schedule", SCHEDULE.id)), + ]); + }); + + it("opens each object as its narrower capability, which reports it", async () => { + const t = await setup(); + const incident = t.pd.incident(SERVICE); + await t.run([["getTeam", TEAM.id], ["getMetadata"]]); + await t.run([["getService", SERVICE.id], ["getDetails"]]); + await t.run([["getSchedule", SCHEDULE.id], ["getDetails"]]); + await t.run([["getIncident", incident.id], ["getDetails"]]); + await t.run([["getTeam", TEAM.id], ["getService", SERVICE.id], ["getIncident", incident.id], ["listNotes"]]); + expect(await t.reported()).toEqual([ + ids(sub("team", TEAM.id)), + ids(sub("service", SERVICE.id)), + ids(sub("schedule", SCHEDULE.id)), + ids(sub("incident", incident.id), sub("service", SERVICE.id)), + ids(sub("team", TEAM.id), sub("service", SERVICE.id), sub("incident", incident.id)), + ]); + }); +}); + +describe("a team binding", () => { + const team = { resourceKind: "team", id: TEAM.id } as const; + + it("covers what the team owns with the team, and its incidents' services per page", async () => { + const t = await setup(team); + t.pd.incident(SERVICE); + await t.run([["getMetadata"]]); + await t.run([["listMembers"]]); + await t.run([["listServices"]], 5); + await t.run([["listSchedules"]], 5); + await t.run([["listIncidents"]], 5); + const scope = ids(sub("team", TEAM.id)); + expect(await t.reported()).toEqual([scope, scope, scope, scope, ids(sub("team", TEAM.id), sub("service", SERVICE.id))]); + expect(t.pd.matching("GET", /^\/incidents\?/)[0].url.searchParams.getAll("team_ids[]")).toEqual([TEAM.id]); + }); + + it("reports the team's escalation policies for who is on call, though nobody is, a cached answer included", async () => { + const t = await setup(team); + expect(await t.run([["listOnCalls"]])).toEqual([]); + expect(await t.run([["listOnCalls"]])).toEqual([]); + expect(t.pd.matching("GET", /^\/escalation_policies/)).toHaveLength(1); + const expected = ids(sub("team", TEAM.id), sub("escalation_policy", POLICY.id)); + expect(await t.reported()).toEqual([expected, expected]); + }); + + it("asks nothing of a team with no escalation policy, which would answer for the whole account", async () => { + const t = await setup({ resourceKind: "team", id: "PTEAM03" }); + t.pd.teams.set("PTEAM03", { id: "PTEAM03", name: "Empty" }); + expect(await t.run([["listOnCalls"]])).toEqual([]); + expect(t.pd.matching("GET", /^\/oncalls/)).toHaveLength(0); + expect(await t.reported()).toEqual([ids(sub("team", "PTEAM03"))]); + }); + + it("refuses another team's service, schedule or incident", async () => { + const t = await setup(team); + const theirs = t.pd.incident(OTHER_SERVICE); + expect(await t.attempt([["getService", OTHER_SERVICE.id]])).toMatch(/doesn't belong to this team/); + expect(await t.attempt([["getSchedule", OTHER_SCHEDULE.id]])).toMatch(/doesn't belong to this team/); + expect(await t.attempt([["getIncident", theirs.id]])).toMatch(/doesn't belong to this team/); + expect(await t.reported()).toEqual([]); + }); +}); + +describe("a service binding", () => { + const service = { resourceKind: "service", id: SERVICE.id } as const; + + it("reports the service on every read, and its escalation policy for who is on call though nobody is", async () => { + const t = await setup(service); + await t.run([["getDetails"]]); + expect(await t.run([["listIncidents"]], 5)).toEqual([]); + expect(await t.run([["listOnCalls"]])).toEqual([]); + expect(await t.reported()).toEqual([ + ids(sub("service", SERVICE.id)), + ids(sub("service", SERVICE.id)), + ids(sub("service", SERVICE.id), sub("escalation_policy", POLICY.id)), + ]); + }); + + it("refuses another service's incident, and lists none of another service's", async () => { + const t = await setup(service); + const theirs = t.pd.incident(OTHER_SERVICE); + expect(await t.attempt([["getIncident", theirs.id]])).toMatch(/belongs to another service/); + expect(await t.run([["listIncidents", { serviceIds: [OTHER_SERVICE.id] }]], 5)).toEqual([]); + expect(t.pd.matching("GET", /^\/incidents\?/)).toHaveLength(0); + expect(await t.reported()).toEqual([ids(sub("service", SERVICE.id))]); + }); +}); + +describe("an incident binding", () => { + it("reports the incident and its service on every read, though it has no notes, entries or alerts", async () => { + const pd = new FakePagerDuty().install(); + const incident = pd.incident(SERVICE); + const t = await setup({ resourceKind: "incident", id: incident.id }, pd); + await t.run([["getDetails"]]); + expect(await t.run([["listNotes"]])).toEqual([]); + expect(await t.run([["getTimeline"]], 5)).toEqual([]); + expect(await t.run([["listAlerts"]], 5)).toEqual([]); + const expected = ids(sub("incident", incident.id), sub("service", SERVICE.id)); + expect(await t.reported()).toEqual([expected, expected, expected, expected]); + }); + + it("reports an incident bound by number by the ID PagerDuty gave it", async () => { + const pd = new FakePagerDuty().install(); + const incident = pd.incident(SERVICE); + const t = await setup({ resourceKind: "incident", id: String(incident.incident_number) }, pd); + expect(await t.run([["getDetails"]])).toMatchObject({ id: incident.id }); + expect(await t.reported()).toEqual([ids(sub("incident", incident.id), sub("service", SERVICE.id))]); + }); +}); + +describe("a schedule binding", () => { + it("reports the schedule on every read, though it has no shifts or overrides", async () => { + const t = await setup({ resourceKind: "schedule", id: SCHEDULE.id }); + await t.run([["getDetails"]]); + expect(await t.run([["listShifts"]])).toEqual([]); + expect(await t.run([["listOverrides"]])).toEqual([]); + const expected = ids(sub("schedule", SCHEDULE.id)); + expect(await t.reported()).toEqual([expected, expected, expected]); + }); +}); diff --git a/packages/gatekeeper-pagerduty/__tests__/workerd/worker.ts b/packages/gatekeeper-pagerduty/__tests__/workerd/worker.ts new file mode 100644 index 0000000000..26e19c39bc --- /dev/null +++ b/packages/gatekeeper-pagerduty/__tests__/workerd/worker.ts @@ -0,0 +1,198 @@ +// Test worker for the workerd suite. Re-exports the production entrypoints so miniflare can bind +// the Durable Objects, and adds `TestHooks`, which reaches the gatekeeper as the overseer does. +// +// `TestHooks` has to be a Durable Object: a `DurableObjectClass` from `ctx.exports.X({props})` is +// only reachable through `ctx.facets`, which is how the overseer instantiates a gatekeeper. A stub to +// a facet, or to a session it mints, cannot be handed to the test, so TestHooks runs each scenario +// itself -- a chain of method calls on a session -- and returns the result as plain data. + +import { DurableObject, RpcTarget, WorkerEntrypoint } from "cloudflare:workers"; +import type { + ActionDescription, ConnectHandoff, GatekeeperConnectCallback, ObservationDescription, +} from "@gadgets/workshop-shared/gatekeeper"; +import type { PagerDutyGatekeeperImplProps } from "../../src/pagerduty-env.js"; + +export { default } from "../../src/pagerduty.js"; +export * from "../../src/pagerduty.js"; +import { GatekeeperUserImpl, PagerDutyVerifier } from "../../src/pagerduty.js"; + +/** + * The account entrypoint, reachable for tests. Under the capnweb-validate *vite* plugin, which + * applies `@validateRpc()` in memory here, decorated `WorkerEntrypoint` exports are not registered in + * `ctx.exports`; the production build registers them. An undecorated subclass registers, and + * inherits the production behaviour exactly. + */ +export class TestUser extends GatekeeperUserImpl {} + +/** The verifier entrypoint, reachable for tests -- see `TestUser`. */ +export class TestVerifier extends PagerDutyVerifier {} + +export type GatekeeperProps = PagerDutyGatekeeperImplProps; + +type UserProps = { userObjectId: string }; + +type TestExports = { + PagerDutyGatekeeperImpl(options: { props: GatekeeperProps }): DurableObjectClass; + TestUser(options: { props: UserProps }): Record Promise>; + TestVerifier(options: { props: UserProps }): Record Promise>; + TestCallback(options: { props: UserProps }): Fetcher; +}; + +/** A stand-in for the Workshop's connect callback, persistent as the Workshop's own is. */ +export class TestCallback extends WorkerEntrypoint { + async credentialsExpired(): Promise {} + + /** A reconnect's handoff carries its stage id as the ticket, so a test can commit that stage. */ + async reconnectComplete(stageId: string): Promise { + return { targetOrigin: "http://localhost:8787", ticket: stageId }; + } +} + +/** An approval queue that records what it is asked. */ +export class RecordingQueue extends RpcTarget { + readonly submitted: Array<{ actionId: number; description: ActionDescription }> = []; + readonly observations: ObservationDescription[] = []; + + async submitAction(actionId: number, description: ActionDescription): Promise { + this.submitted.push({ actionId, description }); + } + + async authorizeObservation(description: ObservationDescription): Promise { + this.observations.push(description); + } +} + +/** A forwarded call's result as plain data: an expected rejection crossing RPC also reports as unhandled. */ +export type Outcome = { ok: T } | { error: string }; + +async function outcome(fn: () => Promise): Promise> { + try { + return { ok: await fn() }; + } catch (error) { + return { error: error instanceof Error ? error.message : String(error) }; + } +} + +/** One call in a scenario: a method and its arguments. */ +export type Step = [method: string, ...args: unknown[]]; + +type Callable = Record Promise> & Partial; + +type GatekeeperFacet = { + describe(): Promise; + getAutoApprovableActions(): Promise; + startSession(queue: RecordingQueue): Promise; + applyAction(actionId: number, cache: RpcTarget): Promise; + rejectAction(actionId: number): Promise; + verifyObserverAccess(observers: { id: string; verifier: unknown }[], subresources: string[]): + Promise<{ allowedObserverIds: string[] }>; + getHistoricalSubresources(): Promise; +}; + +class NullGitCache extends RpcTarget {} + +export class TestHooks extends DurableObject { + #queues = new Map(); + + get #exports(): TestExports { + return this.ctx.exports as unknown as TestExports; + } + + /** + * The gatekeeper facet named `facetName`, made with `props` on first use. A facet is cached per + * name, storage and all, so each scenario takes a fresh name. + */ + #gatekeeper(facetName: string, props: GatekeeperProps): GatekeeperFacet { + return this.ctx.facets.get(facetName, () => ({ + class: this.#exports.PagerDutyGatekeeperImpl({ props }), + })) as unknown as GatekeeperFacet; + } + + #queue(facetName: string): RecordingQueue { + let queue = this.#queues.get(facetName); + if (!queue) this.#queues.set(facetName, queue = new RecordingQueue()); + return queue; + } + + /** + * Opens a session and makes each call in `steps` on what the call before returned, starting with + * the session. The last call's result comes back; with `pages`, it is a cursor, walked for at most + * that many pages. + */ + async run(facetName: string, props: GatekeeperProps, steps: Step[], options: { pages?: number } = {}): + Promise> { + return await outcome(async () => { + const session = await this.#gatekeeper(facetName, props).startSession(this.#queue(facetName)); + // Every capability a step returned and the scenario walked on from, the session's included. + const owned: Callable[] = [session]; + try { + let target: Callable = session; + for (const [index, [method, ...args]] of steps.entries()) { + const result = await target[method](...args); + if (index === steps.length - 1) { + if (options.pages === undefined) return result; + const cursor = result as Callable & { next(): Promise }; + owned.push(cursor); + const pages: unknown[][] = []; + for (let page; pages.length < options.pages && (page = await cursor.next()) !== null;) pages.push(page); + return pages; + } + target = result as Callable; + owned.push(target); + } + return undefined; + } finally { + for (const capability of owned.toReversed()) capability[Symbol.dispose]?.(); + } + }); + } + + /** What the facet's approval queue has recorded. */ + queueLog(facetName: string): { submitted: RecordingQueue["submitted"]; observations: RecordingQueue["observations"] } { + const queue = this.#queue(facetName); + return { submitted: queue.submitted, observations: queue.observations }; + } + + async describe(facetName: string, props: GatekeeperProps): Promise> { + return await outcome(() => this.#gatekeeper(facetName, props).describe()); + } + + async autoApprovable(facetName: string, props: GatekeeperProps): Promise> { + return await outcome(() => this.#gatekeeper(facetName, props).getAutoApprovableActions()); + } + + async applyAction(facetName: string, props: GatekeeperProps, actionId: number): Promise> { + return await outcome(() => this.#gatekeeper(facetName, props).applyAction(actionId, new NullGitCache())); + } + + async rejectAction(facetName: string, props: GatekeeperProps, actionId: number): Promise> { + return await outcome(() => this.#gatekeeper(facetName, props).rejectAction(actionId)); + } + + /** `verifyObserverAccess` with each observer's verifier minted for its account. */ + async verifyObserverAccess(facetName: string, props: GatekeeperProps, + observers: { id: string; userObjectId: string }[], subresources: string[]): + Promise> { + const minted = observers.map(({ id, userObjectId }) => + ({ id, verifier: this.#exports.TestVerifier({ props: { userObjectId } }) })); + return await outcome(() => this.#gatekeeper(facetName, props).verifyObserverAccess(minted, subresources)); + } + + async historicalSubresources(facetName: string, props: GatekeeperProps): Promise> { + return await outcome(() => this.#gatekeeper(facetName, props).getHistoricalSubresources()); + } + + // -- account ---------------------------------------------------------------------------- + + /** Gives the account a Workshop callback and an initiation nonce, as `connectAccount` does. */ + async installCallback(userObjectId: string, initiationNonce: string): Promise { + const callback = this.#exports.TestCallback({ props: { userObjectId } }); + const account = this.ctx.exports.UserAccount.get(this.ctx.exports.UserAccount.idFromString(userObjectId)); + await account.setCallback(callback, initiationNonce); + } + + /** Calls `method` on the account entrypoint for `userObjectId`. */ + async user(userObjectId: string, method: string, ...args: unknown[]): Promise> { + return await outcome(() => this.#exports.TestUser({ props: { userObjectId } })[method](...args)); + } +} diff --git a/packages/gatekeeper-pagerduty/cloudflare.config.ts b/packages/gatekeeper-pagerduty/cloudflare.config.ts new file mode 100644 index 0000000000..bc7c74672e --- /dev/null +++ b/packages/gatekeeper-pagerduty/cloudflare.config.ts @@ -0,0 +1,17 @@ +import { + DEFAULT_GATEKEEPER_WRANGLER, OBSERVABILITY, defineGadgetsWorker, type DurableObjectMigration, +} from "@gadgets/scripts/worker-config"; + +/** No `vars`: the OAuth client is the `CLIENT_ID` and `CLIENT_SECRET` secrets. */ +export default defineGadgetsWorker({ + name: "gatekeeper-pagerduty", + entrypoint: ".wrangler/validate/src/pagerduty.ts", + compatibilityFlags: ["allow_irrevocable_stub_storage", "nodejs_als"], + observability: OBSERVABILITY, +}); + +export const wrangler = DEFAULT_GATEKEEPER_WRANGLER; + +export const migrations: DurableObjectMigration[] = [ + { tag: "v0", new_sqlite_classes: ["UserAccount", "PagerDutyGatekeeperImpl"] }, +]; diff --git a/packages/gatekeeper-pagerduty/deploy-inputs.json b/packages/gatekeeper-pagerduty/deploy-inputs.json new file mode 100644 index 0000000000..b66ef4d0a5 --- /dev/null +++ b/packages/gatekeeper-pagerduty/deploy-inputs.json @@ -0,0 +1,22 @@ +[ + { + "name": "CLIENT_ID", + "kind": "secret", + "label": "OAuth Client ID", + "help": "A Scoped OAuth app registered on your own PagerDuty account. Only users of that account can connect, and each sees only what PagerDuty shows them.", + "consoleUrl": "https://app.pagerduty.com/developer/apps", + "setupSteps": [ + "Sign in to your PagerDuty account as a Manager, Global Admin or Account Owner, and open Integrations > Developer Tools > App Registration.", + "Select New App, and leave its App Type as Private. Under Functionality, add OAuth and choose \"Scoped OAuth\".", + "Grant it these permissions: Incidents (read and write), Schedules (read and write), and read access to Services, Teams, Users, On-Calls, Priorities and Escalation Policies.", + "Add the Redirect URL below, and save.", + "Copy the Client ID and Client Secret here." + ], + "redirectUriTemplate": "{PUBLIC_BASE_URL}/gatekeeper/pagerduty/oauth" + }, + { + "name": "CLIENT_SECRET", + "kind": "secret", + "label": "OAuth Client Secret" + } +] diff --git a/packages/gatekeeper-pagerduty/package.json b/packages/gatekeeper-pagerduty/package.json new file mode 100644 index 0000000000..fba06a0634 --- /dev/null +++ b/packages/gatekeeper-pagerduty/package.json @@ -0,0 +1,29 @@ +{ + "name": "@gadgets/pagerduty-gatekeeper", + "version": "1.0.0", + "type": "module", + "main": "./src/pagerduty.ts", + "scripts": { + "dev": "echo \"run 'pnpm dev-server' in the root directory instead\" >&2 && exit 1", + "deploy": "vp run --no-cache build:configurator && wrangler deploy", + "clean": "rm -rf dist src/generated", + "test:run": "vp run -F @gadgets/pagerduty-gatekeeper build:configurator && vitest run && vitest run -c vitest.worker.config.ts" + }, + "dependencies": { + "@gadgets/configurator-ui": "workspace:*", + "@gadgets/gatekeeper-kit": "workspace:*", + "@gadgets/observability": "workspace:*", + "@gadgets/workshop-shared": "workspace:*", + "capnweb": "catalog:", + "capnweb-validate": "catalog:", + "jose": "^6.2.13" + }, + "devDependencies": { + "@cloudflare/vitest-plugin": "catalog:", + "@cloudflare/workers-types": "catalog:", + "@gadgets/scripts": "workspace:*", + "typescript": "catalog:", + "vitest": "catalog:", + "wrangler": "catalog:" + } +} diff --git a/packages/gatekeeper-pagerduty/src/configurator/pagerduty-configurator-types.d.ts b/packages/gatekeeper-pagerduty/src/configurator/pagerduty-configurator-types.d.ts new file mode 100644 index 0000000000..289ed9a4c0 --- /dev/null +++ b/packages/gatekeeper-pagerduty/src/configurator/pagerduty-configurator-types.d.ts @@ -0,0 +1,70 @@ +// The contract between the PagerDuty configurator frame and `PagerDutyConfiguratorUI`. The frame is +// transpiled on its own and cannot import runtime code, so the shapes it trades are declared here. +// The frame never builds a resource URL: it trades refs, and the gatekeeper writes the URL a ref +// binds (`resourceUrlFor`), so the frame holds no copy of the URL grammar. + +/** What a node is, which decides its icon and what choosing it binds. */ +export type PagerDutyNodeKind = "account" | "team" | "service" | "incident" | "schedule"; + +/** A place above a node, as a breadcrumb shows it. */ +export type PagerDutyCrumb = { ref: string; kind: PagerDutyNodeKind; title: string }; + +/** One row of the browser. */ +export type PagerDutyNode = { + /** `account`, or `:`, e.g. `service:PABC123`. Only the kind means anything to the frame. */ + ref: string; + kind: PagerDutyNodeKind; + title: string; + /** A team's or service's description, an incident's number and service. */ + subtitle?: string; + /** An incident's status, a service's state. */ + badge?: string; + /** When an incident was opened, "Oct 12". */ + meta?: string; + /** Search results and located nodes: the service an incident is on. */ + path?: PagerDutyCrumb[]; +}; + +/** What the connection allows, read once when the frame opens. */ +export type PagerDutyConfiguratorStatus = + | { state: "ready"; account: { ref: "account"; host: string } } + | { state: "unavailable"; message: string }; + +/** One of the picker's tabs. */ +export type PagerDutySection = "teams" | "services" | "schedules" | "incidents"; + +/** + * What the picker asks for: a team's services and schedules or a service's open incidents + * (`parent`), a search of the account (`query`), or one of its tabs (`section`). Exactly one of the + * three is set. + */ +export type PagerDutyBrowseRequest = { + parent?: string; + query?: string; + section?: PagerDutySection; + cursor?: string; +}; + +export type PagerDutyBrowsePage = { + items: PagerDutyNode[]; + nextCursor?: string; + /** Something the user should know about this listing. */ + notice?: string; +}; + +export type PagerDutyConfiguratorValues = { + /** What is chosen, as a ref. */ + ref?: string | null; + /** The chosen node's kind, which says whether the Workshop takes it. */ + kind?: PagerDutyNodeKind | null; +}; + +export interface PagerDutyConfiguratorRpc { + getStatus(): Promise; + /** One page of rows. Rejections carry a message written for the user. */ + loadItems(request: PagerDutyBrowseRequest): Promise; + /** The node a resource URL, pasted link or ref names, or null when this connection can't open it. */ + locate(urlOrRef: string): Promise; + /** The resource URL a ref binds. */ + resourceUrlFor(ref: string): Promise; +} diff --git a/packages/gatekeeper-pagerduty/src/configurator/pagerduty-configurator-ui.tsx b/packages/gatekeeper-pagerduty/src/configurator/pagerduty-configurator-ui.tsx new file mode 100644 index 0000000000..6ba3c98066 --- /dev/null +++ b/packages/gatekeeper-pagerduty/src/configurator/pagerduty-configurator-ui.tsx @@ -0,0 +1,339 @@ +import { + Field, h, HierarchyPicker, Notice, Section, + type ConfiguratorUIHierarchyAncestor as Ancestor, type ConfiguratorUIHierarchyItem as Item, + type ConfiguratorUIHierarchyPage as Page, type ConfiguratorUIHierarchyRequest as PickerRequest, + type ConfiguratorUIHierarchyTab as Tab, type ConfiguratorUIRenderContext, type ConfiguratorUISpec, +} from "@gadgets/configurator-ui"; +import type { RpcStub } from "capnweb"; +import type { + PagerDutyBrowseRequest as Request, + PagerDutyConfiguratorRpc, + PagerDutyConfiguratorStatus as Status, + PagerDutyConfiguratorValues as Values, + PagerDutyNode as Node, + PagerDutyNodeKind as Kind, + PagerDutySection, +} from "./pagerduty-configurator-types"; + +/** The gatekeeper as the frame holds it: a Cap'n Web stub. */ +type Rpc = RpcStub; + +// Must equal the urlPatterns of RESOURCES in pagerduty-env.ts; __tests__/configurator.test.ts keeps +// the copies in step. +const ACCOUNT_PATTERN = "https://:subdomain{.eu}?.pagerduty.com/"; +const TEAM_PATTERN = "https://:subdomain{.eu}?.pagerduty.com/teams/:teamId([A-Z0-9]+){/*}?"; +const SERVICE_PATTERN = "https://:subdomain{.eu}?.pagerduty.com/service-directory/:serviceId([A-Z0-9]+){/*}?"; +const INCIDENT_PATTERN = "https://:subdomain{.eu}?.pagerduty.com/incidents/:incidentId([A-Z0-9]+){/*}?"; +const SCHEDULE_PATTERN = "https://:subdomain{.eu}?.pagerduty.com/schedules/:scheduleId([A-Z0-9]+){/*}?"; + +const FIELD_LABEL = "What to connect"; +const SEARCH_PLACEHOLDER = "Search names, or paste a PagerDuty link or ID"; +const ROOT_SUBTITLE = "Every team, service, schedule and incident you can see"; + +type Context = ConfiguratorUIRenderContext; +type Connection = { state: "loading" } | { state: "failed"; message: string } | Status; +type ObjectKind = Exclude; + +const errorMessage = (error: unknown, fallback: string) => error instanceof Error && error.message ? error.message : fallback; + +// -- the connection ----------------------------------------------------------------------------- + +let connection: Connection = { state: "loading" }; +let asked = false; +/** A pre-filled resource URL the picker couldn't open: one this connection can't, or whose lookup failed. */ +let missing: { url: string; failed?: string } | null = null; +// Re-renders with what has loaded. Every render installs its own, so a late reply reaches the +// current tree. It changes no values, so it never invalidates a resource URL being collected. +let rerender: () => void = () => {}; + +function loadStatus({ ui }: Context): void { + if (asked) return; + asked = true; + ui.getStatus().then(status => { + connection = status; + rerender(); + }, error => { + connection = { state: "failed", message: errorMessage(error, "Couldn't reach PagerDuty.") }; + rerender(); + }); +} + +// -- what a choice is --------------------------------------------------------------------------- + +const PATTERN_OF: Record = { + account: ACCOUNT_PATTERN, team: TEAM_PATTERN, service: SERVICE_PATTERN, incident: INCIDENT_PATTERN, schedule: SCHEDULE_PATTERN, +}; +const NOUN_OF_PATTERN: Record = { + [ACCOUNT_PATTERN]: "the whole account", [TEAM_PATTERN]: "a team", [SERVICE_PATTERN]: "a service", + [INCIDENT_PATTERN]: "an incident", [SCHEDULE_PATTERN]: "a schedule", +}; + +/** Whether the Workshop takes what `kind` binds. An older Workshop does not say, and takes any. */ +const accepts = (accepted: readonly string[] | undefined, kind: Kind) => accepted === undefined || accepted.includes(PATTERN_OF[kind]); + +/** Why a node of `kind` can't be chosen here, or undefined when it can. */ +function refusal(accepted: readonly string[] | undefined, kind: Kind): string | undefined { + if (accepts(accepted, kind)) return undefined; + const nouns = (accepted ?? []).map(pattern => NOUN_OF_PATTERN[pattern]).filter(Boolean); + return nouns.length > 0 ? `This connection needs ${nouns.join(" or ")}.` : "This can't be connected here."; +} + +/** The second line of an open container's own row: what choosing it covers. */ +const COVERS: Partial> = { + team: "Its services and their incidents, its schedules and members", + service: "This service and its incidents, including new ones", +}; + +// -- icons -------------------------------------------------------------------------------------- + +const ICON_PATHS: Record = { + // The PagerDuty mark, as the Workshop draws the vendor (src/pagerduty-logo.svg). + account: "M5 3h3.4C10.7 3 12 4.3 12 6.4S10.7 9.8 8.4 9.8H7V13H5zm2 1.8v3.2h1.3c1.1 0 1.7-.6 1.7-1.6S9.4 4.8 8.3 4.8z", + team: "M5.5 2a2.5 2.5 0 1 1 0 5 2.5 2.5 0 0 1 0-5m0 1.5a1 1 0 1 0 0 2 1 1 0 0 0 0-2M11 4a2 2 0 1 1 0 4 2 2 0 0 1 0-4m0 1.5a.5.5 0 1 0 0 1 .5.5 0 0 0 0-1M1 13c0-2.2 2-4 4.5-4s4.5 1.8 4.5 4v1H8.5v-1c0-1.3-1.3-2.5-3-2.5s-3 1.2-3 2.5v1H1zm10.5-3.9c2 .2 3.5 1.7 3.5 3.6V14h-1.5v-1.3c0-1-.8-1.9-2-2.1z", + service: "M2 3a1 1 0 0 1 1-1h10a1 1 0 0 1 1 1v3a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1zm1.5.5v2h9v-2zM2 10a1 1 0 0 1 1-1h10a1 1 0 0 1 1 1v3a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1zm1.5.5v2h9v-2zM4.5 4h1v1h-1zm0 7h1v1h-1z", + incident: "M8 1a.75.75 0 0 1 .66.4l6.5 12a.75.75 0 0 1-.66 1.1h-13a.75.75 0 0 1-.66-1.1l6.5-12A.75.75 0 0 1 8 1m0 2.3L2.76 13h10.48zM7.25 6h1.5v3.5h-1.5zm0 4.5h1.5V12h-1.5z", + schedule: "M5 1h1.5v1.5h3V1H11v1.5h2a1 1 0 0 1 1 1V14a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1V3.5a1 1 0 0 1 1-1h2zM3.5 6v7.5h9V6zM5 7.5h2V9.5H5zm4 0h2V9.5H9z", +}; + +const glyph = (kind: keyof typeof ICON_PATHS) => ; + +/** A glyph on its kind's tint, filling the tile the picker draws behind a node's icon. */ +const tinted = (kind: keyof typeof ICON_PATHS) => {glyph(kind)}; + +const ICONS: Record = { + account: tinted("account"), + team: tinted("team"), + service: tinted("service"), + incident: tinted("incident"), + schedule: tinted("schedule"), + teams: glyph("team"), + services: glyph("service"), + schedules: glyph("schedule"), + incidents: glyph("incident"), +}; + +// -- the picker's nodes ------------------------------------------------------------------------- + +const TABS: Tab[] = [ + { id: "teams", title: "Teams", icon: "teams" }, + { id: "services", title: "Services", icon: "services" }, + { id: "schedules", title: "Schedules", icon: "schedules" }, + { id: "incidents", title: "Open incidents", icon: "incidents" }, +]; +const SECTIONS = new Set(TABS.map(tab => tab.id)); +/** What an empty tab says, in place of the picker's "Nothing here." */ +const EMPTY_TAB: Partial> = { incidents: "No incident is open right now." }; +/** The tab each kind of node lives in; an incident lives under its service. */ +const HOME_TAB: Record = { + team: { id: "teams", title: "Teams", selectable: false }, + service: { id: "services", title: "Services", selectable: false }, + incident: { id: "services", title: "Services", selectable: false }, + schedule: { id: "schedules", title: "Schedules", selectable: false }, +}; + +/** Every node listed or looked up, by ref: what a choice's kind is, and where a located one lives. */ +const nodes = new Map(); + +const isContainer = (kind: Kind) => kind === "team" || kind === "service"; + +function toAncestor({ ref, kind, title }: Pick, accepted: readonly string[] | undefined): Ancestor { + const reason = refusal(accepted, kind); + return { id: ref, title, icon: kind, selectable: reason === undefined, unselectableReason: reason, choice: { title: `All of ${title}`, subtitle: COVERS[kind] } }; +} + +/** A node as the picker lists it; a located one (a search result, a pre-fill) says where it lives. */ +function toItem(node: Node, accepted: readonly string[] | undefined, located = false): Item { + nodes.set(node.ref, node); + const reason = refusal(accepted, node.kind); + const container = isContainer(node.kind); + return { + id: node.ref, + title: node.title, + kind: container ? "container" : "leaf", + icon: node.kind, + subtitle: node.subtitle, + badge: node.badge, + meta: node.meta, + selectable: reason === undefined, + unselectableReason: reason, + choice: container ? { title: `All of ${node.title}`, subtitle: COVERS[node.kind] } : undefined, + path: located && node.kind !== "account" + ? [HOME_TAB[node.kind], ...(node.path ?? []).map(crumb => toAncestor(crumb, accepted))] + : undefined, + }; +} + +async function loadPage({ ui, acceptedResourceUrlPatterns: accepted }: Context, { parentId, query, cursor }: PickerRequest): Promise { + const request: Request = query ? { query } : SECTIONS.has(parentId) ? { section: parentId as PagerDutySection } : { parent: parentId }; + if (cursor) request.cursor = cursor; + const page = await ui.loadItems(request); + // A link to the account names the choice above the tabs, which the picker doesn't list again. + if (query && page.items.some(node => node.kind === "account")) { + return { items: [], nextCursor: null, notice: refusal(accepted, "account") ?? "That link is to the whole account. Choose it above the tabs." }; + } + return { + items: page.items.map(node => toItem(node, accepted, Boolean(query))), + nextCursor: page.nextCursor ?? null, + notice: page.notice ?? (page.items.length === 0 && !cursor && request.section ? EMPTY_TAB[request.section] : undefined), + }; +} + +/** The node a value names, with where it lives, from what was searched for or else from PagerDuty. */ +async function loadNode({ ui, acceptedResourceUrlPatterns: accepted }: Context, ref: string): Promise { + const node = await ui.locate(ref); + return node && toItem(node, accepted, true); +} + +const valuesOf = (node: Pick): Values => ({ ref: node.ref, kind: node.kind }); +const NOTHING: Values = { ref: null, kind: null }; + +// -- rendering ---------------------------------------------------------------------------------- + +function picker(context: Context, host: string) { + const { values, setValues, acceptedResourceUrlPatterns: accepted } = context; + return + loadPage(context, request)} + loadItem={ref => loadNode(context, ref)} + onChange={next => { + if (next === null) { + setValues(NOTHING); + return; + } + missing = null; + setValues({ ref: next, kind: next === "account" ? "account" : nodes.get(next)?.kind ?? null }); + }} + /> + ; +} + +/** The picker as it will be, waiting on the connection, so the form doesn't move once it is known. */ +function placeholder({ acceptedResourceUrlPatterns: accepted }: Context) { + return + new Promise(() => {})} + onChange={() => {}} + disabled + /> + ; +} + +/** What became of a pre-filled resource URL the picker isn't showing as a choice. */ +function prefillNotice({ ui, setValues }: Context, attempt: NonNullable) { + const { url, failed } = attempt; + if (failed === undefined) { + return {`Couldn't find ${url} in the PagerDuty account this connection can open. Choose what to connect.`}; + } + return { + ui.locate(url).then(located => { + // A choice made while the lookup ran (which cleared `missing`) stands; the late answer is dropped. + if (missing !== attempt) return; + if (located === null) { + missing = { url }; + rerender(); + return; + } + missing = null; + nodes.set(located.ref, located); + setValues(valuesOf(located)); + }, error => { + if (missing !== attempt) return; + missing = { url, failed: errorMessage(error, "Couldn't reach PagerDuty.") }; + rerender(); + }); + }, + }}>{`Couldn't look up ${url}. ${failed}`}; +} + +function notices(context: Context) { + if (connection.state !== "failed" && connection.state !== "unavailable") return null; + return { + asked = false; + connection = { state: "loading" }; + loadStatus(context); + rerender(); + }, + } : undefined}>{connection.message}; +} + +// Each kind's tint for the icon tiles, on the runtime's tokens, so they follow the host's light and +// dark themes as the other pickers' icons do. +const STYLE = ` +.pd-icon { + --pd-tint: var(--text-color-kumo-subtle); + display: flex; width: 100%; height: 100%; align-items: center; justify-content: center; + color: var(--pd-tint); background: color-mix(in oklab, var(--pd-tint) 11%, var(--color-kumo-base)); +} +.pd-icon svg { width: 16px; height: 16px; } +.pd-account { color: #fff; background: #06ac38; } +.pd-team { --pd-tint: oklch(53% 0.19 300); } +.pd-service { --pd-tint: oklch(54% 0.17 258); } +.pd-incident { --pd-tint: oklch(58% 0.2 28); } +.pd-schedule { --pd-tint: oklch(53% 0.13 152); } +@media (prefers-color-scheme: dark) { + .pd-team { --pd-tint: oklch(74% 0.13 300); } + .pd-service { --pd-tint: oklch(74% 0.13 258); } + .pd-incident { --pd-tint: oklch(74% 0.15 28); } + .pd-schedule { --pd-tint: oklch(74% 0.13 152); } +} +`; + +export default { + initial: {}, + + isReady({ values, acceptedResourceUrlPatterns }) { + return typeof values.ref === "string" && values.kind != null && accepts(acceptedResourceUrlPatterns, values.kind) + && connection.state === "ready"; + }, + + async initialValuesFromResourceUrl({ resourceUrl, ui }) { + let located: Node | null; + try { + located = await ui.locate(resourceUrl); + } catch (error) { + missing = { url: resourceUrl, failed: errorMessage(error, "Couldn't reach PagerDuty.") }; + return {}; + } + if (located === null) { + // Never a fallback to the whole account: that would bind everything with one click, in place of what was asked for. + missing = { url: resourceUrl }; + return {}; + } + nodes.set(located.ref, located); + return valuesOf(located); + }, + + resourceUrl({ values, ui }) { + if (!values.ref) throw new Error("Choose what to connect."); + return ui.resourceUrlFor(values.ref); + }, + + render(context) { + rerender = () => context.setValues({}); + loadStatus(context); + if (connection.state === "unavailable" || connection.state === "failed") return
{notices(context)}
; + return
+ + {missing !== null && !context.values.ref ? prefillNotice(context, missing) : null} + {connection.state === "ready" ? picker(context, connection.account.host) : placeholder(context)} +
; + }, +} satisfies ConfiguratorUISpec; diff --git a/packages/gatekeeper-pagerduty/src/observability.ts b/packages/gatekeeper-pagerduty/src/observability.ts new file mode 100644 index 0000000000..aa2d3a951a --- /dev/null +++ b/packages/gatekeeper-pagerduty/src/observability.ts @@ -0,0 +1,17 @@ +import { createObservabilityContext } from "@gadgets/observability/observability-context"; + +/** Observability fields emitted by the PagerDuty gatekeeper. */ +export type PagerDutyObservabilityFields = { + vendorId: string; + /** The REST endpoint template a log line is about, e.g. `GET /incidents/:id`. */ + endpoint: string; + /** An HTTP status PagerDuty answered with. */ + status: number; + /** PagerDuty's numeric error code, which never carries request content. */ + errorCode: number; + /** An action id, for apply-time events. */ + action: number; +}; + +/** Ambient observability fields for one PagerDuty gatekeeper operation. */ +export const obsContext = createObservabilityContext(); diff --git a/packages/gatekeeper-pagerduty/src/pagerduty-actions.ts b/packages/gatekeeper-pagerduty/src/pagerduty-actions.ts new file mode 100644 index 0000000000..5002dc4fa8 --- /dev/null +++ b/packages/gatekeeper-pagerduty/src/pagerduty-actions.ts @@ -0,0 +1,417 @@ +// What a gadget can change in PagerDuty, as kit actions: each kind's approval text, auto-approval +// verdict and REST call. Sessions validate and stage them (pagerduty-sessions.ts); nothing here runs +// until a user approves. Incidents and overrides a pending action creates have temporary `~N` IDs, +// which later actions can name and which apply resolves to PagerDuty's. + +import { + ActionApplyError, ActionOutcomeUnknownError, defineActions, type ActionPresentation, type TaggedAction, +} from "@gadgets/gatekeeper-kit/actions"; +import { buildDescription, sanitizeTitle, type ActionDescriptionBuilder } from "@gadgets/gatekeeper-kit/action-description"; +import type { ProvisionalIds } from "@gadgets/gatekeeper-kit/simulation"; +import { PagerDutyApiError, type PagerDutyApi } from "./pagerduty-api"; +import type { Identity } from "./pagerduty-credentials"; +import { VENDOR_ID, type Tenant } from "./pagerduty-env"; +import type { WireIncident, WireOverride } from "./pagerduty-normalize"; +import { resourceUrl } from "./pagerduty-urls"; +import type { PagerDutyRef, PagerDutyUrgency } from "./types"; + +/** An incident an action acts on: PagerDuty's ID, or the `~N` of one a pending action creates. */ +export type IncidentTarget = { id: string; number?: number }; + +/** A user named by the agent (email or ID), as the gatekeeper resolved them when staging. */ +export type UserTarget = { id: string; name: string; given: string }; + +/** A priority, resolved by name when staging. */ +export type PriorityTarget = { id: string; name: string }; + +export type NewIncident = { + /** The `~N` the pending incident goes by until PagerDuty assigns its ID. */ + ref: string; + /** The service, and the escalation policy it would notify, as staging read them. */ + service: PagerDutyRef; + team: PagerDutyRef | null; + escalationPolicy: PagerDutyRef; + title: string; + details?: string; + urgency?: PagerDutyUrgency; + priority?: PriorityTarget; + assignTo?: UserTarget[]; + incidentKey?: string; + /** When it was staged, which is when a simulated read says it was created. */ + stagedAt: string; +}; + +export type NewOverride = { + /** The `~N` the pending override goes by until PagerDuty assigns its ID. */ + ref: string; + scheduleId: string; + user: UserTarget; + start: string; + end: string; +}; + +/** Every action's payload, by kind. */ +export type PagerDutyActions = { + acknowledge: { incident: IncidentTarget }; + snooze: { incident: IncidentTarget; seconds: number }; + resolve: { incident: IncidentTarget; resolution?: string }; + setPriority: { incident: IncidentTarget; priority: PriorityTarget | null }; + setTitle: { incident: IncidentTarget; title: string }; + setUrgency: { incident: IncidentTarget; urgency: PagerDutyUrgency }; + addNote: { incident: IncidentTarget; ref: string; content: string; stagedAt: string }; + reassign: { incident: IncidentTarget; to: { users: UserTarget[] } | { escalationPolicyId: string } }; + escalate: { incident: IncidentTarget; level: number }; + requestResponders: { + incident: IncidentTarget; message: string; users: UserTarget[]; escalationPolicyIds: string[]; + }; + postStatusUpdate: { incident: IncidentTarget; message: string }; + createIncident: NewIncident; + createOverride: NewOverride; + /** As `createOverride`, for an override putting someone other than the connected user on call. */ + createOverrideForOthers: NewOverride; + deleteOverride: { scheduleId: string; overrideId: string }; +}; + +export type PagerDutyAction = TaggedAction; + +/** What apply handlers may do. */ +export type PagerDutyActionHost = { + readonly tenant: Tenant; + readonly refs: ProvisionalIds; + /** Runs a write as the connected user, who `From` names. */ + write(op: (api: PagerDutyApi, me: Identity) => Promise): Promise; + /** Drops every cached read, once an action changed what they hold. */ + invalidate(): void; +}; + +/** Whether an ID is a temporary one a pending action created. */ +export const isProvisional = (id: string): boolean => id.startsWith("~"); + +/** Which incident `target` is now: PagerDuty's ID once the action creating it applied. */ +const incidentId = (host: PagerDutyActionHost, target: IncidentTarget): string => + host.refs.requireResolved(target.id, { expectedKind: "incident" }); + +/** The temporary references a payload depends on, for `dependsOn`. */ +const dependsOnIncident = ({ incident }: { incident: IncidentTarget }): string[] => + isProvisional(incident.id) ? [incident.id] : []; + +/** How the approval text names an incident: a link once it exists. */ +function incidentLabel(host: PagerDutyActionHost, target: IncidentTarget): string { + if (isProvisional(target.id)) return "the incident this gadget is opening"; + const url = resourceUrl({ ...host.tenant, kind: "incident", id: target.id }); + return `[incident ${target.number === undefined ? target.id : `#${target.number}`}](${url})`; +} + +function present(title: string, description: ActionDescriptionBuilder): ActionPresentation { + // No PagerDuty action can be undone through the API, so none implements revert. + return { title: sanitizeTitle(title), ...description.finish(), implementsRevert: false }; +} + +/** The users an action names, as the agent gave them. */ +const usersGiven = (users: readonly UserTarget[]): string[] => users.map(({ given, id }) => given === id ? id : `${given} (${id})`); + +/** + * Sends a change PagerDuty applies at most once per call. PagerDuty's refusal, or a rate limit or + * credential rejection that stopped the call, left nothing behind; any other failure may have + * landed after all. + */ +async function once(host: PagerDutyActionHost, op: (api: PagerDutyApi, me: Identity) => Promise): Promise { + try { + return await host.write(op); + } catch (error) { + if (error instanceof PagerDutyApiError && (error.status === 401 || error.status === 429)) throw error; + if (error instanceof PagerDutyApiError && error.status < 500) throw new ActionApplyError(error.message); + throw new ActionOutcomeUnknownError(error instanceof Error ? error.message : "PagerDuty did not answer."); + } +} + +/** Sends a change that repeating leaves the same, so a failure is retried as it came. */ +async function idempotent(host: PagerDutyActionHost, op: (api: PagerDutyApi, me: Identity) => Promise): Promise { + try { + await host.write(op); + } catch (error) { + if (error instanceof PagerDutyApiError && error.status < 500 && error.status !== 401 && error.status !== 429) { + throw new ActionApplyError(error.message); + } + throw error; + } +} + +/** Updates the incident with the `PUT /incidents/{id}` fields in `incident`. */ +function updateIncident(host: PagerDutyActionHost, target: IncidentTarget, incident: Record): Promise { + const id = incidentId(host, target); + return idempotent(host, api => api.write("PUT", `/incidents/${id}`, { incident: { type: "incident_reference", ...incident } })); +} + +const userReference = ({ id }: UserTarget) => ({ id, type: "user_reference" }); +const priorityReference = (priority: PriorityTarget | null | undefined) => + priority ? { id: priority.id, type: "priority_reference" } : null; + +const ACKNOWLEDGE = { tag: "acknowledge-incidents", label: "Acknowledge and snooze incidents" }; +const RESOLVE = { tag: "resolve-incidents", label: "Resolve incidents" }; +const EDIT = { tag: "edit-incidents", label: "Change incidents' title, priority and urgency" }; +const NOTES = { tag: "add-notes", label: "Add notes to incidents" }; +const REASSIGN = { tag: "reassign-incidents", label: "Reassign and escalate incidents" }; +const RESPONDERS = { tag: "request-responders", label: "Ask more people to respond to incidents" }; +const STATUS_UPDATES = { tag: "post-status-updates", label: "Post incident status updates" }; +const CREATE = { tag: "create-incidents", label: "Open incidents" }; +const OVERRIDES = { tag: "manage-overrides", label: "Add schedule overrides" }; +const DELETE_OVERRIDES = { tag: "delete-overrides", label: "Remove schedule overrides" }; + +/** An override's approval text and its REST call, shared by the kinds for oneself and for others. */ +function overrideDefinition(autoApprovable: boolean) { + return { + kind: OVERRIDES, + autoApprovable, + delivery: "continue-with-simulation" as const, + claimBeforeApply: true, + provides: (payload: NewOverride) => [payload.ref], + describe: (payload: NewOverride, host: PagerDutyActionHost) => present("Add a PagerDuty schedule override", + buildDescription(`Puts a user on call on [this schedule](${resourceUrl({ ...host.tenant, kind: "schedule", id: payload.scheduleId })}), ` + + "over whoever it would otherwise have.") + .inline("User", usersGiven([payload.user])[0]) + .inline("From", payload.start) + .inline("Until", payload.end)), + apply: async (payload: NewOverride, host: PagerDutyActionHost) => { + const [result] = await once(host, api => api.write<{ status: number; errors?: string[]; override?: WireOverride }[]>( + "POST", `/schedules/${payload.scheduleId}/overrides`, + { overrides: [{ start: payload.start, end: payload.end, user: userReference(payload.user) }] })); + if (!result?.override?.id) { + throw new ActionApplyError(`PagerDuty refused the override${result?.errors?.length ? `: ${result.errors.join("; ")}` : "."}`); + } + host.refs.bind(payload.ref, result.override.id); + }, + }; +} + +/** The declared action set. */ +export const actions = defineActions({ + acknowledge: { + kind: ACKNOWLEDGE, + autoApprovable: true, + delivery: "continue-with-simulation", + dependsOn: dependsOnIncident, + describe: (payload, host) => present("Acknowledge a PagerDuty incident", + buildDescription(`Acknowledges ${incidentLabel(host, payload.incident)} as you, which stops its escalation.`)), + apply: (payload, host) => updateIncident(host, payload.incident, { status: "acknowledged" }), + }, + snooze: { + kind: ACKNOWLEDGE, + autoApprovable: true, + delivery: "continue-with-simulation", + dependsOn: dependsOnIncident, + describe: (payload, host) => present("Snooze a PagerDuty incident", + buildDescription(`Snoozes ${incidentLabel(host, payload.incident)}: it triggers again after the time below ` + + "unless it is resolved first.").inline("Seconds", String(payload.seconds))), + apply: async (payload, host) => { + const id = incidentId(host, payload.incident); + await idempotent(host, api => api.write("POST", `/incidents/${id}/snooze`, { duration: payload.seconds })); + }, + }, + resolve: { + kind: RESOLVE, + autoApprovable: true, + delivery: "continue-with-simulation", + dependsOn: dependsOnIncident, + describe: (payload, host) => { + const description = buildDescription(`Resolves ${incidentLabel(host, payload.incident)}.`); + if (payload.resolution !== undefined) description.verbatim("Resolution note", payload.resolution); + return present("Resolve a PagerDuty incident", description); + }, + apply: (payload, host) => updateIncident(host, payload.incident, + { status: "resolved", ...(payload.resolution !== undefined ? { resolution: payload.resolution } : {}) }), + }, + setPriority: { + kind: EDIT, + autoApprovable: true, + delivery: "continue-with-simulation", + dependsOn: dependsOnIncident, + describe: (payload, host) => present("Set a PagerDuty incident's priority", + buildDescription(`Sets the priority of ${incidentLabel(host, payload.incident)}.`) + .inline("Priority", payload.priority?.name ?? "(none)")), + apply: (payload, host) => updateIncident(host, payload.incident, { priority: priorityReference(payload.priority) }), + }, + setTitle: { + kind: EDIT, + autoApprovable: true, + delivery: "continue-with-simulation", + dependsOn: dependsOnIncident, + describe: (payload, host) => present("Retitle a PagerDuty incident", + buildDescription(`Replaces the title of ${incidentLabel(host, payload.incident)}.`).inline("Title", payload.title)), + apply: (payload, host) => updateIncident(host, payload.incident, { title: payload.title }), + }, + setUrgency: { + kind: EDIT, + autoApprovable: true, + delivery: "continue-with-simulation", + dependsOn: dependsOnIncident, + describe: (payload, host) => present(`Set a PagerDuty incident's urgency to ${payload.urgency}`, + buildDescription(`Sets the urgency of ${incidentLabel(host, payload.incident)} to ${payload.urgency}` + + (payload.urgency === "high" ? ", which may notify its assignees again." : "."))), + apply: (payload, host) => updateIncident(host, payload.incident, { urgency: payload.urgency }), + }, + addNote: { + kind: NOTES, + autoApprovable: true, + delivery: "continue-with-simulation", + claimBeforeApply: true, + dependsOn: dependsOnIncident, + describe: (payload, host) => present("Add a note to a PagerDuty incident", + buildDescription(`Adds a note to ${incidentLabel(host, payload.incident)}.`).verbatim("Note", payload.content)), + apply: async (payload, host) => { + const id = incidentId(host, payload.incident); + await once(host, api => api.write("POST", `/incidents/${id}/notes`, { note: { content: payload.content } })); + }, + }, + reassign: { + kind: REASSIGN, + // Notifies the new assignees, who may be outside the bound resource (rule 2). + autoApprovable: false, + delivery: "continue-with-simulation", + dependsOn: dependsOnIncident, + describe: (payload, host) => { + const description = buildDescription(`Reassigns ${incidentLabel(host, payload.incident)}, replacing its ` + + "assignees and notifying the new ones."); + if ("users" in payload.to) description.list("Users", usersGiven(payload.to.users)); + else description.inline("Escalation policy", payload.to.escalationPolicyId); + return present("Reassign a PagerDuty incident", description); + }, + apply: (payload, host) => updateIncident(host, payload.incident, "users" in payload.to + ? { assignments: payload.to.users.map(user => ({ assignee: userReference(user) })) } + : { escalation_policy: { id: payload.to.escalationPolicyId, type: "escalation_policy_reference" } }), + }, + escalate: { + kind: REASSIGN, + // Notifies whoever the level names, who may be outside the bound resource (rule 2). + autoApprovable: false, + delivery: "continue-with-simulation", + dependsOn: dependsOnIncident, + describe: (payload, host) => present("Escalate a PagerDuty incident", + buildDescription(`Escalates ${incidentLabel(host, payload.incident)} to this level of its escalation ` + + "policy, notifying whoever is on call there.").inline("Level", String(payload.level))), + apply: (payload, host) => updateIncident(host, payload.incident, { escalation_level: payload.level }), + }, + requestResponders: { + kind: RESPONDERS, + // Pages each responder with the message (rule 2). + autoApprovable: false, + delivery: "continue-with-simulation", + claimBeforeApply: true, + dependsOn: dependsOnIncident, + describe: (payload, host) => { + const description = buildDescription(`Asks more people to help with ${incidentLabel(host, payload.incident)}, ` + + "paging each with the message below.").verbatim("Message", payload.message); + if (payload.users.length) description.list("Users", usersGiven(payload.users)); + if (payload.escalationPolicyIds.length) description.list("Escalation policies", payload.escalationPolicyIds); + return present("Request responders for a PagerDuty incident", description); + }, + apply: async (payload, host) => { + const id = incidentId(host, payload.incident); + await once(host, (api, me) => api.write("POST", `/incidents/${id}/responder_requests`, { + requester_id: me.userId, + message: payload.message, + responder_request_targets: [ + ...payload.users.map(user => ({ responder_request_target: userReference(user) })), + ...payload.escalationPolicyIds.map(policy => + ({ responder_request_target: { id: policy, type: "escalation_policy_reference" } })), + ], + })); + }, + }, + postStatusUpdate: { + kind: STATUS_UPDATES, + // PagerDuty emails it to the incident's subscribers and stakeholders (rule 2). + autoApprovable: false, + delivery: "continue-with-simulation", + claimBeforeApply: true, + dependsOn: dependsOnIncident, + describe: (payload, host) => present("Post a PagerDuty status update", + buildDescription(`Publishes a status update on ${incidentLabel(host, payload.incident)}, which PagerDuty ` + + "sends to its subscribers and stakeholders.").verbatim("Message", payload.message)), + apply: async (payload, host) => { + const id = incidentId(host, payload.incident); + await once(host, api => api.write("POST", `/incidents/${id}/status_updates`, { message: payload.message })); + }, + }, + createIncident: { + kind: CREATE, + // Pages whoever the service's escalation policy or the assignees name (rule 2). + autoApprovable: false, + delivery: "continue-with-simulation", + claimBeforeApply: true, + provides: payload => [payload.ref], + describe: (payload, host) => { + const service = resourceUrl({ ...host.tenant, kind: "service", id: payload.service.id }); + const description = buildDescription(`Opens an incident on [this service](${service}), which notifies ` + + (payload.assignTo?.length ? "the assignees below." : "whoever its escalation policy names.")) + .inline("Title", payload.title); + if (payload.details !== undefined) description.verbatim("Details", payload.details); + if (payload.urgency) description.inline("Urgency", payload.urgency); + if (payload.priority) description.inline("Priority", payload.priority.name); + if (payload.assignTo?.length) description.list("Assign to", usersGiven(payload.assignTo)); + if (payload.incidentKey !== undefined) description.inline("Deduplication key", payload.incidentKey); + return present(`Open a PagerDuty incident: ${payload.title}`, description); + }, + apply: async (payload, host) => { + const { incident } = await once(host, api => api.write<{ incident: WireIncident }>("POST", "/incidents", { + incident: { + type: "incident", + title: payload.title, + service: { id: payload.service.id, type: "service_reference" }, + ...(payload.urgency ? { urgency: payload.urgency } : {}), + ...(payload.priority ? { priority: priorityReference(payload.priority) } : {}), + ...(payload.details !== undefined ? { body: { type: "incident_body", details: payload.details } } : {}), + ...(payload.assignTo?.length + ? { assignments: payload.assignTo.map(user => ({ assignee: userReference(user) })) } : {}), + ...(payload.incidentKey !== undefined ? { incident_key: payload.incidentKey } : {}), + }, + })); + host.refs.bind(payload.ref, incident.id); + }, + }, + createOverride: overrideDefinition(true), + // Puts someone else on call, whom PagerDuty will page (rule 2). + createOverrideForOthers: overrideDefinition(false), + deleteOverride: { + kind: DELETE_OVERRIDES, + // Removes an override (rule 1). + autoApprovable: false, + delivery: "continue-with-simulation", + dependsOn: payload => isProvisional(payload.overrideId) ? [payload.overrideId] : [], + describe: (payload, host) => present("Remove a PagerDuty schedule override", + buildDescription(`Removes an override from [this schedule](${resourceUrl({ ...host.tenant, kind: "schedule", id: payload.scheduleId })}).`) + .inline("Override", payload.overrideId)), + apply: async (payload, host) => { + const id = host.refs.requireResolved(payload.overrideId, { expectedKind: "override" }); + await idempotent(host, async api => { + try { + await api.write("DELETE", `/schedules/${payload.scheduleId}/overrides/${id}`); + } catch (error) { + // Already gone counts as removed. + if (!(error instanceof PagerDutyApiError && error.status === 404)) throw error; + } + }); + }, + }, +}, { + // Fenced on the connected user rather than the connection generation: a reconnect, which must be + // the same user, must not strand what is waiting. + fence: "authority", + afterResolve: host => host.invalidate(), + isResolvedReference: (host, ref) => host.refs.isResolved(ref), + vendorId: VENDOR_ID, +}); + +/** The temporary references an action creates. */ +export function providedRefs(action: PagerDutyAction): string[] { + return action.kind === "createIncident" || action.kind === "createOverride" || action.kind === "createOverrideForOthers" + ? [action.payload.ref] : []; +} + +/** The temporary references an action builds on. */ +export function dependedRefs(action: PagerDutyAction): string[] { + const { payload } = action; + if ("incident" in payload) return dependsOnIncident(payload); + if (action.kind === "deleteOverride" && isProvisional(action.payload.overrideId)) return [action.payload.overrideId]; + return []; +} diff --git a/packages/gatekeeper-pagerduty/src/pagerduty-api.ts b/packages/gatekeeper-pagerduty/src/pagerduty-api.ts new file mode 100644 index 0000000000..52e47499f1 --- /dev/null +++ b/packages/gatekeeper-pagerduty/src/pagerduty-api.ts @@ -0,0 +1,159 @@ +// The only place the gatekeeper talks HTTP to PagerDuty's REST API. One request carries the bearer +// token, PagerDuty's versioned `Accept` header and, on writes, the `From` header naming the +// connected user; it has a timeout and a capped body read, never follows a redirect, and turns a +// non-2xx answer into a `PagerDutyApiError`. + +import { HttpError } from "@gadgets/gatekeeper-kit/http-errors"; +import { readTextCapped } from "@gadgets/gatekeeper-kit/response-body"; +import { apiOrigin, type Region } from "./pagerduty-env"; + +const REQUEST_TIMEOUT_MS = 30_000; +/** A page of 100 incidents with their references is well under this. */ +const MAX_RESPONSE_BYTES = 4 * 1024 * 1024; +/** PagerDuty's largest page. */ +export const MAX_LIMIT = 100; +/** PagerDuty refuses a listing whose `offset + limit` exceeds this. */ +export const MAX_OFFSET = 10_000; + +/** A query value; arrays are sent as PagerDuty's `name[]=a&name[]=b`. */ +export type QueryValue = string | number | boolean | readonly string[] | undefined; +export type Query = Record; + +/** PagerDuty's refusal of a request, with its numeric error code when it gave one. */ +export class PagerDutyApiError extends HttpError { + constructor(status: number, message: string, readonly code?: number) { + super(status, message); + this.name = "PagerDutyApiError"; + } +} + +/** Whether PagerDuty refused the credentials themselves (an expired or revoked token). */ +export function isAuthError(error: unknown): boolean { + return error instanceof PagerDutyApiError && error.status === 401; +} + +/** Whether PagerDuty said the user can't see what was asked for: a definite "no access". */ +export function isDenial(error: unknown): boolean { + return error instanceof PagerDutyApiError && (error.status === 403 || error.status === 404); +} + +/** One page of a classic offset listing. */ +export type Page = { items: T[]; more: boolean }; + +type ErrorBody = { error?: { code?: unknown; message?: unknown; errors?: unknown } }; + +/** A display-safe error for a non-2xx answer. */ +function errorFor(response: Response, text: string): PagerDutyApiError { + if (response.status === 429) { + const reset = Number(response.headers.get("ratelimit-reset")); + const wait = Number.isFinite(reset) && reset > 0 ? `in ${Math.ceil(reset)} seconds` : "shortly"; + return new PagerDutyApiError(429, `PagerDuty is rate limiting this connection; try again ${wait}.`, 2020); + } + let body: ErrorBody = {}; + try { + body = JSON.parse(text) as ErrorBody; + } catch { + // Not JSON: the status alone describes it. + } + const { code, message, errors } = body.error ?? {}; + const details = [ + typeof message === "string" ? message : undefined, + ...(Array.isArray(errors) ? errors.filter(error => typeof error === "string") : []), + ].filter(Boolean).join("; ").slice(0, 500); + return new PagerDutyApiError(response.status, + `PagerDuty answered HTTP ${response.status}${details ? `: ${details}` : "."}`, + typeof code === "number" ? code : undefined); +} + +/** A REST client for one account, acting as one user. */ +export class PagerDutyApi { + readonly #token: string; + readonly #origin: string; + readonly #from?: string; + + /** + * @param token The user's OAuth access token. + * @param region The account's region, which picks the REST host. + * @param from The user's email, which PagerDuty requires on writes. + */ + constructor(token: string, region: Region, from?: string) { + this.#token = token; + this.#origin = apiOrigin(region); + this.#from = from; + } + + get(path: string, query: Query = {}): Promise { + return this.#send("GET", path, query); + } + + /** A change, attributed by `From` to the connected user. */ + write(method: "POST" | "PUT" | "DELETE", path: string, body?: unknown): Promise { + if (!this.#from) throw new Error("A PagerDuty write needs the connected user's email."); + return this.#send(method, path, {}, body); + } + + /** One page of the listing at `path`, whose rows PagerDuty returns under `key`. */ + async page(path: string, key: string, query: Query, offset: number, limit: number): Promise> { + const body = await this.get>(path, { ...query, offset, limit }); + const items = body[key]; + if (!Array.isArray(items)) throw new Error(`PagerDuty's listing of ${key} was malformed.`); + return { items: items as T[], more: body.more === true }; + } + + /** Every row of a listing, up to `max` and PagerDuty's offset cap. */ + async all(path: string, key: string, query: Query = {}, max = MAX_OFFSET): Promise { + const rows: T[] = []; + for (let offset = 0; offset < Math.min(max, MAX_OFFSET);) { + const limit = Math.min(MAX_LIMIT, max - offset, MAX_OFFSET - offset); + const { items, more } = await this.page(path, key, query, offset, limit); + rows.push(...items); + if (!more || items.length === 0) break; + offset += items.length; + } + return rows; + } + + async #send(method: string, path: string, query: Query, body?: unknown): Promise { + const url = new URL(path, this.#origin); + for (const [name, value] of Object.entries(query)) { + if (value === undefined) continue; + if (Array.isArray(value)) for (const item of value) url.searchParams.append(`${name}[]`, item); + else url.searchParams.set(name, String(value)); + } + const headers: Record = { + Authorization: `Bearer ${this.#token}`, + Accept: "application/vnd.pagerduty+json;version=2", + }; + if (method !== "GET") headers.From = this.#from!; + if (body !== undefined) headers["Content-Type"] = "application/json"; + let response: Response; + try { + response = await fetch(url.toString(), { + method, + headers, + body: body === undefined ? undefined : JSON.stringify(body), + redirect: "manual", + signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS), + }); + } catch (error) { + throw new Error("PagerDuty did not answer in time; please try again.", { cause: error }); + } + if (response.status >= 300 && response.status < 400) { + await response.body?.cancel(); + throw new PagerDutyApiError(response.status, "PagerDuty answered with an unexpected redirect."); + } + let text: string; + try { + text = await readTextCapped(response, MAX_RESPONSE_BYTES); + } catch (error) { + throw new Error("PagerDuty's response could not be read; please try again.", { cause: error }); + } + if (!response.ok) throw errorFor(response, text); + if (!text) return undefined as T; + try { + return JSON.parse(text) as T; + } catch (error) { + throw new Error("PagerDuty answered with a response that is not JSON.", { cause: error }); + } + } +} diff --git a/packages/gatekeeper-pagerduty/src/pagerduty-configurator.ts b/packages/gatekeeper-pagerduty/src/pagerduty-configurator.ts new file mode 100644 index 0000000000..a613d7b127 --- /dev/null +++ b/packages/gatekeeper-pagerduty/src/pagerduty-configurator.ts @@ -0,0 +1,246 @@ +// The PagerDuty resource configurator's capability: what the browser frame lists, finds and binds. +// +// One frame chooses among all five resource types, so it trades refs (`team:PABC123`) and +// `resourceUrlFor` writes the URL a ref binds, only for an object the connection can still open. +// Every agent-supplied ID is held to PagerDuty's ID grammar before it reaches a request path. +// Nothing here is an agent observation: these reads serve the owner's own picker. + +import { RpcTarget } from "cloudflare:workers"; +import { validateRpc } from "capnweb-validate"; +import { isCredentialsExpired, type CredentialSource } from "@gadgets/gatekeeper-kit/credentials"; +import { MAX_LIMIT, PagerDutyApi, isDenial, type Page, type Query } from "./pagerduty-api"; +import { RECONNECT_MESSAGE, accountSource, type Identity, type PublicGrant } from "./pagerduty-credentials"; +import { webOrigin } from "./pagerduty-env"; +import type { WireIncident, WireSchedule, WireService, WireTeam } from "./pagerduty-normalize"; +import { INCIDENT_NUMBER, OBJECT_ID, parseResourceUrl, resourceUrl, sameTenant } from "./pagerduty-urls"; +import type { + PagerDutyBrowsePage, PagerDutyBrowseRequest, PagerDutyConfiguratorRpc, PagerDutyConfiguratorStatus, + PagerDutyNode, PagerDutyNodeKind, +} from "./configurator/pagerduty-configurator-types"; + +/** Rows per picker page. */ +const PAGE_SIZE = 25; +const MAX_QUERY_LENGTH = 200; +const MAX_CURSOR_LENGTH = 64; +const GONE = "What you chose no longer exists, or this connection can't open it. Choose again."; + +type ObjectKind = Exclude; +type Ref = { kind: "account" } | { kind: ObjectKind; id: string }; + +/** Where each kind's lookup reads; PagerDuty wraps the object in a key named for its kind. */ +const LOOKUPS: Record = { + team: "/teams/", service: "/services/", incident: "/incidents/", schedule: "/schedules/", +}; + +const REF = /^(team|service|incident|schedule):([A-Z0-9]{1,64})$/; + +/** The ref a frame handed back. */ +function parseRef(ref: string): Ref { + if (ref === "account") return { kind: "account" }; + const match = typeof ref === "string" ? REF.exec(ref) : null; + if (!match) throw new Error("That can't be opened."); + return { kind: match[1] as ObjectKind, id: match[2]! }; +} + +const OPEN_STATUSES = ["triggered", "acknowledged"]; + +const shortDate = (iso: string) => new Date(iso).toLocaleDateString("en-US", { month: "short", day: "numeric" }); + +function teamNode(wire: WireTeam): PagerDutyNode { + return { ref: `team:${wire.id}`, kind: "team", title: wire.name ?? wire.summary ?? wire.id, subtitle: wire.description || undefined }; +} + +function serviceNode(wire: WireService): PagerDutyNode { + return { + ref: `service:${wire.id}`, kind: "service", title: wire.name ?? wire.summary ?? wire.id, + subtitle: wire.description || undefined, badge: wire.status && wire.status !== "active" ? wire.status : undefined, + }; +} + +function scheduleNode(wire: WireSchedule): PagerDutyNode { + return { + ref: `schedule:${wire.id}`, kind: "schedule", title: wire.name ?? wire.summary ?? wire.id, + subtitle: wire.description || wire.time_zone || undefined, + }; +} + +function incidentNode(wire: WireIncident): PagerDutyNode { + const service = wire.service.summary ?? wire.service.id; + return { + ref: `incident:${wire.id}`, kind: "incident", title: wire.title, + subtitle: `#${wire.incident_number} · ${service}`, badge: wire.status, meta: shortDate(wire.created_at), + path: [{ ref: `service:${wire.service.id}`, kind: "service", title: service }], + }; +} + +/** One listing a page can draw on: rows from `offset`, at most `limit`. */ +type Source = (api: PagerDutyApi, offset: number, limit: number) => Promise>; + +const teams = (query: Query): Source => async (api, offset, limit) => { + const page = await api.page("/teams", "teams", query, offset, limit); + return { ...page, items: page.items.map(teamNode) }; +}; +const services = (query: Query): Source => async (api, offset, limit) => { + const page = await api.page("/services", "services", { sort_by: "name", ...query }, offset, limit); + return { ...page, items: page.items.map(serviceNode) }; +}; +const schedules = (query: Query): Source => async (api, offset, limit) => { + const page = await api.page("/schedules", "schedules", query, offset, limit); + return { ...page, items: page.items.map(scheduleNode) }; +}; +const incidents = (query: Query): Source => async (api, offset, limit) => { + const page = await api.page("/incidents", "incidents", + { statuses: OPEN_STATUSES, sort_by: "created_at:desc", ...query }, offset, limit); + return { ...page, items: page.items.map(incidentNode) }; +}; + +/** + * One page drawn from `sources` in turn: a cursor `:` says where the last page + * stopped, and the next source starts once one runs out. + */ +async function chain(api: PagerDutyApi, sources: readonly Source[], cursor: string | undefined): Promise { + let [index, offset] = cursor === undefined ? [0, 0] : (cursor.split(":").map(Number) as [number, number]); + if (!Number.isSafeInteger(index) || !Number.isSafeInteger(offset) || index < 0 || index >= sources.length || offset < 0) { + throw new Error("That page has expired. Try again."); + } + const items: PagerDutyNode[] = []; + while (index < sources.length && items.length < PAGE_SIZE) { + const page = await sources[index]!(api, offset, Math.min(MAX_LIMIT, PAGE_SIZE - items.length)); + items.push(...page.items); + offset += page.items.length; + if (!page.more || page.items.length === 0) [index, offset] = [index + 1, 0]; + } + return { items, ...(index < sources.length ? { nextCursor: `${index}:${offset}` } : {}) }; +} + +/** The PagerDuty configurator frame's capability. */ +@validateRpc() +export class PagerDutyConfiguratorUI extends RpcTarget implements PagerDutyConfiguratorRpc { + readonly #exports: Cloudflare.Exports; + readonly #userObjectId: string; + readonly #creds: CredentialSource; + #identity?: Promise; + + constructor(exports: Cloudflare.Exports, userObjectId: string) { + super(); + this.#exports = exports; + this.#userObjectId = userObjectId; + this.#creds = accountSource(exports, userObjectId); + } + + async getStatus(): Promise { + try { + return { state: "ready", account: { ref: "account", host: new URL(webOrigin(await this.#me())).host } }; + } catch (error) { + if (isCredentialsExpired(error)) return { state: "unavailable", message: RECONNECT_MESSAGE }; + throw error; + } + } + + async loadItems(request: PagerDutyBrowseRequest): Promise { + const { parent, query, section, cursor } = request; + if ([parent, query, section].filter(value => value !== undefined).length !== 1) { + throw new Error("Ask for one listing at a time."); + } + if (cursor !== undefined && (typeof cursor !== "string" || cursor.length > MAX_CURSOR_LENGTH)) { + throw new Error("That page has expired. Try again."); + } + if (query !== undefined) return await this.#search(query, cursor); + if (parent !== undefined) { + const ref = parseRef(parent); + switch (ref.kind) { + case "team": return await this.#chain([services({ team_ids: [ref.id] }), schedules({ team_ids: [ref.id] })], cursor); + case "service": return await this.#chain([incidents({ service_ids: [ref.id] })], cursor); + default: throw new Error("That can't be opened."); + } + } + switch (section) { + case "teams": return await this.#chain([teams({})], cursor); + case "services": return await this.#chain([services({})], cursor); + case "schedules": return await this.#chain([schedules({})], cursor); + case "incidents": return await this.#chain([incidents({})], cursor); + default: throw new Error("That can't be opened."); + } + } + + async locate(urlOrRef: string): Promise { + if (typeof urlOrRef !== "string" || urlOrRef.length > 2048) return null; + let ref: Ref; + if (urlOrRef === "account" || REF.test(urlOrRef)) { + ref = parseRef(urlOrRef); + } else { + const parsed = parseResourceUrl(urlOrRef); + if (!parsed || !sameTenant(parsed, await this.#me())) return null; + ref = parsed; + } + return await this.#node(ref); + } + + async resourceUrlFor(ref: string): Promise { + const parsed = parseRef(ref); + const me = await this.#me(); + if (parsed.kind === "account") return resourceUrl({ ...me, kind: "account" }); + const node = await this.#node(parsed); + if (!node) throw new Error(GONE); + // An incident looked up by number is bound by the ID PagerDuty gave it. + return resourceUrl({ ...me, kind: parsed.kind, id: node.ref.slice(parsed.kind.length + 1) }); + } + + /** The connected user, read once per frame; a failure is forgotten so the next call asks again. */ + #me(): Promise { + const { UserAccount } = this.#exports; + return this.#identity ??= UserAccount.get(UserAccount.idFromString(this.#userObjectId)).getIdentity() + .catch(error => { + this.#identity = undefined; + throw error; + }); + } + + async #read(op: (api: PagerDutyApi) => Promise): Promise { + const { region } = await this.#me(); + return await this.#creds.run(creds => op(new PagerDutyApi(creds.accessToken, region)), { replayable: true }); + } + + #chain(sources: readonly Source[], cursor: string | undefined): Promise { + return this.#read(api => chain(api, sources, cursor)); + } + + /** The object a ref names, or null when this connection can't open it. */ + async #node(ref: Ref): Promise { + if (ref.kind === "account") return { ref: "account", kind: "account", title: new URL(webOrigin(await this.#me())).host }; + try { + const body = await this.#read(api => api.get>(`${LOOKUPS[ref.kind]}${ref.id}`)); + const wire = body[ref.kind]; + if (typeof wire !== "object" || wire === null) throw new Error(`PagerDuty's ${ref.kind} was malformed.`); + switch (ref.kind) { + case "team": return teamNode(wire as WireTeam); + case "service": return serviceNode(wire as WireService); + case "incident": return incidentNode(wire as WireIncident); + case "schedule": return scheduleNode(wire as WireSchedule); + } + } catch (error) { + if (isDenial(error)) return null; + throw error; + } + } + + /** + * Names across teams, services and schedules; a pasted link, or an ID or incident number, opens + * what it names. + */ + async #search(raw: string, cursor: string | undefined): Promise { + const query = typeof raw === "string" ? raw.trim() : ""; + if (query.length === 0 || query.length > MAX_QUERY_LENGTH) throw new Error("Search for a name, or paste a PagerDuty link."); + if (/^https?:\/\//i.test(query)) { + const located = await this.locate(query); + return located ? { items: [located] } : { items: [], notice: "This connection can't open that link." }; + } + if (cursor === undefined && (OBJECT_ID.test(query) || INCIDENT_NUMBER.test(query))) { + // An ID alone doesn't say what it names; a number is only ever an incident's. + const kinds: ObjectKind[] = INCIDENT_NUMBER.test(query) ? ["incident"] : ["incident", "service", "team", "schedule"]; + const found = (await Promise.all(kinds.map(kind => this.#node({ kind, id: query })))).filter(node => node !== null); + if (found.length > 0) return { items: found }; + } + return await this.#chain([teams({ query }), services({ query }), schedules({ query })], cursor); + } +} diff --git a/packages/gatekeeper-pagerduty/src/pagerduty-credentials.ts b/packages/gatekeeper-pagerduty/src/pagerduty-credentials.ts new file mode 100644 index 0000000000..5ac5a5b9eb --- /dev/null +++ b/packages/gatekeeper-pagerduty/src/pagerduty-credentials.ts @@ -0,0 +1,158 @@ +// What the account Durable Object shares with the rest of the Worker: the public view of a grant, +// the connected identity, the subresource IDs every read reports, the verifier's own methods, and +// a credential source over one account. Kept apart from pagerduty.ts so the facet and configurator +// modules can use them without an import cycle. + +import { createRemoteJWKSet, jwtVerify } from "jose"; +import { CredentialSource } from "@gadgets/gatekeeper-kit/credentials"; +import { formatSubresourceId, parseSubresourceId } from "@gadgets/gatekeeper-kit/subresource-ids"; +import type { GatekeeperUserVerifier } from "@gadgets/workshop-shared/gatekeeper"; +import { isAuthError } from "./pagerduty-api"; +import { ID_TOKEN_ISSUER, ID_TOKEN_JWKS_URL, VENDOR_ID, type Region, type Tenant } from "./pagerduty-env"; +import { toUser, type WireUser } from "./pagerduty-normalize"; +import { OBJECT_ID, regionOf } from "./pagerduty-urls"; +import type { PagerDutyUser } from "./types"; + +/** What the gadget is told when the connection no longer works. */ +export const RECONNECT_MESSAGE = + "The PagerDuty connection has expired or was revoked. Reconnect the PagerDuty account."; + +/** What a facet sees of the grant: the refresh token never crosses the account boundary. */ +export type PublicGrant = { accessToken: string; expiresAt?: number }; + +/** The user and account a grant belongs to, as the ID token named them. */ +export type GrantIdentity = Tenant & { userId: string; accountId: string }; + +/** The connected user, as the account last read them. */ +export type Identity = GrantIdentity & { + name: string; + email: string; + /** The user's base role, e.g. `user` or `read_only_limited_user`. */ + role: string; + jobTitle: string | null; + timeZone: string; + htmlUrl: string; + avatarUrl?: string; + fetchedAt: number; +}; + +/** + * Base roles that see no incidents or services whatever teams allow: a Limited Stakeholder sees + * only status pages, and Restricted Access only what a team or object role grants, which no + * account-level read relies on. + */ +const BLIND_ROLES = ["read_only_limited_user", "restricted_access"]; + +/** Whether a user with base role `role` can see the account's public objects. */ +export function seesPublicObjects(role: string): boolean { + return !BLIND_ROLES.includes(role); +} + +/** The connected user as `getMetadata().you` reports them. */ +export function youOf(identity: Identity): PagerDutyUser { + return { + id: identity.userId, + name: identity.name, + email: identity.email, + jobTitle: identity.jobTitle, + timeZone: identity.timeZone, + htmlUrl: identity.htmlUrl, + }; +} + +/** The stored identity for a grant, with what `GET /users/me` said about its user. */ +export function identityOf(grant: GrantIdentity, user: WireUser): Identity { + if (user.id !== grant.userId) throw new Error("PagerDuty described a different user than the grant's."); + const { name, email, jobTitle, timeZone, htmlUrl } = toUser(user); + return { + ...grant, name, email, jobTitle, timeZone, htmlUrl, + role: user.role ?? "", + ...(user.avatar_url ? { avatarUrl: user.avatar_url } : {}), + fetchedAt: Date.now(), + }; +} + +const JWKS = createRemoteJWKSet(new URL(ID_TOKEN_JWKS_URL)); + +/** + * Which user, account and region an ID token, as the token endpoint returned it, names. Its + * signature and issuer are checked against PagerDuty's published keys; its audience is not, since + * the token came straight from the token endpoint and PagerDuty does not document the audience's + * exact form. + * @throws Error for a token that does not verify or lacks a claim. + */ +export async function verifyIdToken(idToken: string): Promise { + const { payload } = await jwtVerify(idToken, JWKS, { issuer: ID_TOKEN_ISSUER }); + const { user_id: userId, account_id: accountId, subdomain } = payload; + const region = regionOf(payload as { aud?: string | string[]; region?: unknown }); + if (typeof userId !== "string" || typeof accountId !== "string" || typeof subdomain !== "string" + || !OBJECT_ID.test(userId) || !OBJECT_ID.test(accountId) || !/^[a-z0-9-]+$/i.test(subdomain) || !region) { + throw new Error("PagerDuty's ID token did not say which user and account authorized the connection."); + } + return { userId, accountId, subdomain: subdomain.toLowerCase(), region }; +} + +/** Credentials for the account behind `userObjectId`, as a facet or entrypoint reads them. */ +export function accountSource(exports: Cloudflare.Exports, userObjectId: string): CredentialSource { + return new CredentialSource({ + // A fresh stub per read: a source can outlive the request that made it. + account: () => exports.UserAccount.get(exports.UserAccount.idFromString(userObjectId)), + isAuthError, + expiredMessage: RECONNECT_MESSAGE, + vendorId: VENDOR_ID, + }); +} + +// --------------------------------------------------------------------------- +// Subresources + +/** The objects a read can depend on beyond the account itself. */ +export type ObjectKind = "team" | "service" | "incident" | "schedule" | "escalation_policy"; + +/** One account, by the region it lives in and PagerDuty's ID for it. */ +export type AccountKey = { region: Region; accountId: string }; + +/** A parsed subresource: the account, or one object in it. */ +export type Subresource = AccountKey & { object?: { kind: ObjectKind; id: string } }; + +const OBJECT_KINDS: readonly ObjectKind[] = ["team", "service", "incident", "schedule", "escalation_policy"]; + +/** + * The subresource ID of an account, `region//account/`, or of one object in it, + * `region//account///`. Every read reports the account, which only + * an observer connected to the same account with a role that sees its public objects may see. + */ +export function subresourceId(account: AccountKey, object?: { kind: ObjectKind; id: string }): string { + return formatSubresourceId({ + fragments: [ + { name: "region", id: account.region }, + { name: "account", id: account.accountId }, + ...(object ? [{ name: object.kind, id: object.id }] : []), + ], + }); +} + +/** + * The account and object a {@link subresourceId} ID names. + * @throws Error for any other ID, or a spelling other than the canonical one. + */ +export function parseSubresource(id: string): Subresource { + const { namespace, fragments: [region, account, object, ...rest] } = parseSubresourceId(id); + if (namespace !== undefined || rest.length > 0 || region?.name !== "region" + || (region.id !== "us" && region.id !== "eu") || account?.name !== "account" || !OBJECT_ID.test(account.id) + || (object && (!OBJECT_KINDS.includes(object.name as ObjectKind) || !OBJECT_ID.test(object.id)))) { + throw new Error("Not a PagerDuty subresource ID."); + } + const key: AccountKey = { region: region.id, accountId: account.id }; + return object ? { ...key, object: { kind: object.name as ObjectKind, id: object.id } } : key; +} + +/** The PagerDuty gatekeeper's own methods on its verifier, which the overseer hands back only to us. */ +export interface PagerDutyVerifierApi extends GatekeeperUserVerifier { + /** + * Whether the observer's own connection can read each subresource, in order (see + * `UserAccount.checkSubresourceAccess`). Throws for an ID it can't check, or when access can't + * be determined. + */ + checkSubresourceAccess(subresources: string[]): Promise; +} diff --git a/packages/gatekeeper-pagerduty/src/pagerduty-env.ts b/packages/gatekeeper-pagerduty/src/pagerduty-env.ts new file mode 100644 index 0000000000..582121c10d --- /dev/null +++ b/packages/gatekeeper-pagerduty/src/pagerduty-env.ts @@ -0,0 +1,134 @@ +// Configuration shared by every part of the PagerDuty gatekeeper: the Worker's environment, +// PagerDuty's endpoints, the connectable resources with their URL patterns, and the OAuth scopes a +// connection needs. + +import { stripTrailingSlashes, type SupportedResource } from "@gadgets/workshop-shared/gatekeeper"; +import type { PreviewOAuthEnv } from "@gadgets/gatekeeper-kit/preview-oauth"; + +/** The vendor id: the router's `/gatekeeper/pagerduty/` path segment, and the log attribution. */ +export const VENDOR_ID = "pagerduty"; + +/** The Worker's environment. The OAuth client is required; everything else has a default. */ +export type Env = Cloudflare.Env & PreviewOAuthEnv & { + /** This Worker's public base URL, ending in `/gatekeeper/pagerduty`. */ + BASE_URL?: string; + /** The Scoped OAuth app's client ID. */ + CLIENT_ID?: string; + /** The Scoped OAuth app's client secret. */ + CLIENT_SECRET?: string; +}; + +/** The browser-facing authorization page, shared by every region. */ +export const AUTHORIZE_URL = "https://identity.pagerduty.com/oauth/authorize"; +/** The token endpoint, for code exchange and refresh. PagerDuty documents no revocation endpoint. */ +export const TOKEN_URL = "https://identity.pagerduty.com/oauth/token"; +/** Who signs the ID tokens that say which user, account and region a grant belongs to. */ +export const ID_TOKEN_ISSUER = "https://identity.pagerduty.com/global/oauth/anonymous"; +/** The issuer's signing keys. */ +export const ID_TOKEN_JWKS_URL = `${ID_TOKEN_ISSUER}/jwks`; + +/** The two regions an account can live in; each has its own REST and web hosts. */ +export type Region = "us" | "eu"; + +/** An account, as its web address names it. */ +export type Tenant = { region: Region; subdomain: string }; + +/** Where REST requests for an account in `region` go. */ +export function apiOrigin(region: Region): string { + return region === "eu" ? "https://api.eu.pagerduty.com" : "https://api.pagerduty.com"; +} + +/** The account's web app, e.g. `https://acme.eu.pagerduty.com`. */ +export function webOrigin({ region, subdomain }: Tenant): string { + return `https://${subdomain}${region === "eu" ? ".eu" : ""}.pagerduty.com`; +} + +/** This Worker's public base URL, without a trailing slash. */ +export function getBaseUrl(env: Env): string { + return stripTrailingSlashes(env.BASE_URL || "http://localhost:8787/gatekeeper/pagerduty"); +} + +export function getBasePath(env: Env): string { + const path = new URL(getBaseUrl(env)).pathname; + return path === "/" ? "" : path; +} + +/** The OAuth callback registered with the PagerDuty app. */ +export function getRedirectUri(env: Env): string { + return `${getBaseUrl(env)}/oauth`; +} + +/** The kinds of resource a binding can be. */ +export type ResourceKind = "account" | "team" | "service" | "incident" | "schedule"; + +/** Props baked into the gatekeeper Durable Object class for one binding. */ +export type PagerDutyGatekeeperImplProps = Tenant & { + /** The `UserAccount` Durable Object holding the connection. */ + userObjectId: string; +} & ( + | { resourceKind: "account" } + | { resourceKind: "team" | "service" | "incident" | "schedule"; id: string } +); + +/** Every account host, US or EU. */ +const HOST = "https://:subdomain{.eu}?.pagerduty.com"; +/** A PagerDuty object ID, e.g. `PABC123`, with any page beneath it. */ +const object = (path: string, group: string) => `${HOST}/${path}/:${group}([A-Z0-9]+){/*}?`; + +const ACCOUNT_PATTERN = `${HOST}/`; + +/** + * The connectable resources. They share the account's connect card, since one configurator picks + * any of them; each is granted with the same scopes, so a connection holds all five or none. + */ +export const RESOURCES: Record = { + account: { + urlPattern: ACCOUNT_PATTERN, + title: "PagerDuty Account", + description: "Every incident, service, team and on-call schedule you can see, and acting on " + + "incidents and schedules as you.", + }, + team: { + urlPattern: object("teams", "teamId"), + title: "PagerDuty Team", + description: "One team's services and their incidents, its schedules and members.", + groupedWith: ACCOUNT_PATTERN, + }, + service: { + urlPattern: object("service-directory", "serviceId"), + title: "PagerDuty Service", + description: "One service and its incidents, and opening new incidents on it.", + groupedWith: ACCOUNT_PATTERN, + }, + incident: { + urlPattern: object("incidents", "incidentId"), + title: "PagerDuty Incident", + description: "One incident: read it, acknowledge, resolve, reassign, and add notes.", + groupedWith: ACCOUNT_PATTERN, + }, + schedule: { + urlPattern: object("schedules", "scheduleId"), + title: "PagerDuty Schedule", + description: "One on-call schedule: who is on call when, and its overrides.", + groupedWith: ACCOUNT_PATTERN, + }, +}; + +/** The resources in the order the picker lists them: whole account first. */ +export const ALL_RESOURCES: SupportedResource[] = Object.values(RESOURCES); + +/** + * The scopes every connection asks for: exactly what the endpoints this gatekeeper calls require. + * PagerDuty drops a scope the app was not granted without saying so, so the granted list is + * checked against this one (see `missingScopes`). + */ +export const SCOPES = [ + "incidents.read", "incidents.write", "services.read", "teams.read", "users.read", + "schedules.read", "schedules.write", "oncalls.read", "priorities.read", "escalation_policies.read", +] as const; + +/** The required scopes a grant lacks. */ +export function missingScopes(granted: readonly string[]): string[] { + const have = new Set(granted); + return SCOPES.filter(scope => !have.has(scope)); +} diff --git a/packages/gatekeeper-pagerduty/src/pagerduty-gatekeeper.ts b/packages/gatekeeper-pagerduty/src/pagerduty-gatekeeper.ts new file mode 100644 index 0000000000..a1c9630ca1 --- /dev/null +++ b/packages/gatekeeper-pagerduty/src/pagerduty-gatekeeper.ts @@ -0,0 +1,229 @@ +// The per-binding gatekeeper Durable Object: one instance per binding of a PagerDuty connection, +// whether to the whole account, a team, a service, an incident or a schedule. It owns the +// binding's action journal, the temporary IDs of incidents and overrides not yet created, and a +// read cache partitioned by connection. It pins the user and account the connection belongs to, +// and mints the capabilities gadgets hold (pagerduty-sessions.ts). Every PagerDuty request runs +// through the account's credential source. + +import { DurableObject, type RpcStub } from "cloudflare:workers"; +import { validateRpc } from "capnweb-validate"; +import type { + ActionKind, ApprovalQueue, Gatekeeper, GatekeeperUserVerifier, GitCache, ResourceDescription, +} from "@gadgets/workshop-shared/gatekeeper"; +import { ActionJournal, type ActionSubmitter } from "@gadgets/gatekeeper-kit/actions"; +import { KvTtlCache } from "@gadgets/gatekeeper-kit/cache"; +import { observerAccessVerifier } from "@gadgets/gatekeeper-kit/observers"; +import { ProvisionalIds } from "@gadgets/gatekeeper-kit/simulation"; +import { + actions, dependedRefs, isProvisional, providedRefs, type PagerDutyAction, type PagerDutyActionHost, + type PagerDutyActions, +} from "./pagerduty-actions"; +import { PagerDutyApi } from "./pagerduty-api"; +import { + accountSource, parseSubresource, type AccountKey, type Identity, type PagerDutyVerifierApi, +} from "./pagerduty-credentials"; +import { webOrigin, type Env, type PagerDutyGatekeeperImplProps, type ResourceKind } from "./pagerduty-env"; +import { + PagerDutyAccountImpl, PagerDutyIncidentImpl, PagerDutyScheduleImpl, PagerDutyServiceImpl, PagerDutyTeamImpl, + SessionContext, type ObjectRef, type SessionHost, +} from "./pagerduty-sessions"; +import { resourceUrl, sameTenant } from "./pagerduty-urls"; +import type { PagerDutyAccount, PagerDutyIncident, PagerDutySchedule, PagerDutyService, PagerDutyTeam } from "./types"; +import TYPES_CODE from "./types.txt"; + +/** How long the connected identity is trusted in memory before the account is asked again. */ +const IDENTITY_MEMO_MS = 60 * 1000; + +// Storage keys of this class's own. The kit modules own theirs: `pagerduty:` (the journal), +// `refs:` and `cache:@pagerduty:`. +const PINNED_KEY = "pinned"; + +/** The user and account a binding is pinned to at its first use. */ +type Pinned = AccountKey & { userId: string }; + +/** How each kind of binding is described, and the interface it implements. */ +const BINDINGS: Record = { + account: { noun: "account", bindingName: "PAGERDUTY", tsType: "PagerDutyAccount" }, + team: { noun: "team", bindingName: "PAGERDUTY_TEAM", tsType: "PagerDutyTeam" }, + service: { noun: "service", bindingName: "PAGERDUTY_SERVICE", tsType: "PagerDutyService" }, + incident: { noun: "incident", bindingName: "PAGERDUTY_INCIDENT", tsType: "PagerDutyIncident" }, + schedule: { noun: "schedule", bindingName: "PAGERDUTY_SCHEDULE", tsType: "PagerDutySchedule" }, +}; + +const SCHEDULE_TAGS = ["manage-overrides", "delete-overrides"]; + +/** Which action tags each kind of binding can submit, for the auto-approval catalog. */ +const BINDING_ACTIONS: Record boolean> = { + account: () => true, + team: () => true, + service: tag => !SCHEDULE_TAGS.includes(tag), + incident: tag => !SCHEDULE_TAGS.includes(tag) && tag !== "create-incidents", + schedule: tag => SCHEDULE_TAGS.includes(tag), +}; + +type PagerDutySession = PagerDutyAccount | PagerDutyTeam | PagerDutyService | PagerDutyIncident | PagerDutySchedule; + +@validateRpc() +export class PagerDutyGatekeeperImpl extends DurableObject + implements Gatekeeper { + readonly #creds = accountSource(this.ctx.exports, this.ctx.props.userObjectId); + readonly #journal = new ActionJournal(this.ctx.storage.kv, { namespace: "pagerduty" }); + readonly #refs = new ProvisionalIds(this.ctx.storage.kv, { namespace: "refs:", isProvisional }); + readonly #cache = KvTtlCache.partitionedBy(this.ctx.storage.kv, this.#creds, { name: "pagerduty" }); + #identity?: { value: Identity; at: number }; + + // Every ID a read reports names the pinned account (see `verifyObserverAccess`), and each + // observer's own connection answers for them. + readonly #verifyAccess = observerAccessVerifier({ + hasCollectionAccess: (verifier, ids) => verifier.checkSubresourceAccess([...ids]), + }); + + // What apply handlers may do. A plain object, not this class: its stub is the overseer's. + readonly #actionHost: PagerDutyActionHost = { + tenant: this.ctx.props, + refs: this.#refs, + write: async op => { + const me = await this.#me(); + return await this.#creds.run(creds => op(new PagerDutyApi(creds.accessToken, me.region, me.email), me)); + }, + invalidate: () => this.#cache.invalidateAll(), + }; + + readonly #actions = actions.bind(this.#journal, this.#actionHost); + + readonly #sessionHost: SessionHost = { + tenant: this.ctx.props, + me: () => this.#me(), + read: op => this.#creds.run(creds => op(new PagerDutyApi(creds.accessToken, this.ctx.props.region)), + { replayable: true }), + cached: (key, ttlMs, load) => this.#cache.cached(key, ttlMs, load), + pending: () => this.#journal.listPending(), + resolve: id => this.#refs.resolve(id), + allocate: kind => this.#refs.allocate(sequence => `~${sequence}`, { kind }), + submit: (queue, kind, payload) => this.#submit(queue, kind, payload), + }; + + async describe(): Promise { + const props = this.ctx.props; + const { noun, bindingName, tsType } = BINDINGS[props.resourceKind]; + const host = new URL(webOrigin(props)).host; + // Descriptions are visible outside observation authorization, so they name objects by ID only. + return props.resourceKind === "account" + ? { + url: resourceUrl({ ...props, kind: "account" }), title: host, snippet: "PagerDuty account", + suggestedBindingName: bindingName, tsType, + } + : { + url: resourceUrl({ ...props, kind: props.resourceKind }), title: `PagerDuty ${noun} ${props.id}`, + snippet: host, suggestedBindingName: bindingName, tsType, + }; + } + + async getTypeScriptTypes(): Promise { + return TYPES_CODE; + } + + async getAutoApprovableActions(): Promise { + const submits = BINDING_ACTIONS[this.ctx.props.resourceKind]; + return this.#actions.autoApprovableKinds().filter(kind => submits(kind.tag)); + } + + async startSession(approvalQueue: RpcStub): Promise { + const props = this.ctx.props; + const ctx = (...scope: ObjectRef[]) => new SessionContext(this.#sessionHost, approvalQueue.dup(), scope); + switch (props.resourceKind) { + case "account": return new PagerDutyAccountImpl(ctx()); + case "team": return new PagerDutyTeamImpl(ctx({ kind: "team", id: props.id }), props.id); + case "service": return new PagerDutyServiceImpl(ctx({ kind: "service", id: props.id }), props.id, {}); + // An incident's reads report the incident by the ID PagerDuty gives it, which a link by + // number doesn't carry. + case "incident": return new PagerDutyIncidentImpl(ctx(), props.id, {}); + case "schedule": return new PagerDutyScheduleImpl(ctx({ kind: "schedule", id: props.id }), props.id, {}); + } + } + + async applyAction(actionId: number, _cache: RpcStub): Promise { + // Actions are fenced on the user the binding is pinned to; see `#submit`. + const { userId } = await this.#me(); + await this.#actions.apply(actionId, { generation: userId }); + } + + async rejectAction(actionId: number): Promise { + // Rejecting an incident or override later actions build on retires those too, and the gadget + // may hold a capability for its temporary ID: restart it rather than leave that dangling. + const record = this.#journal.get(actionId); + const provided = record ? providedRefs(record.action) : []; + const builtOn = provided.length > 0 && this.#journal.listUndecided().some(({ id, action }) => + id !== actionId && dependedRefs(action).some(ref => provided.includes(ref))); + await this.#actions.reject(actionId); + return builtOn ? { restart: true } : undefined; + } + + async revertAction(_actionId: number): Promise<{ message: string; canRetry: boolean }> { + return { message: "PagerDuty changes can't be undone automatically.", canRetry: false }; + } + + /** A new gatekeeper: no read was ever made without reporting its subresources. */ + async getHistoricalSubresources(): Promise { + return []; + } + + /** + * Allows an observer only if their own connection can read every subresource (see + * `UserAccount.checkSubresourceAccess`). Fails the whole call for an ID naming any account but + * the one this binding is pinned to, which no read through it reports. + */ + async verifyObserverAccess( + observers: { id: string; verifier: Fetcher }[], + subresources: string[]): Promise<{ allowedObserverIds: string[] }> { + const pinned = this.ctx.storage.kv.get(PINNED_KEY); + if (pinned === undefined) throw new Error("This PagerDuty binding has not read anything yet."); + for (const id of subresources) { + const { region, accountId } = parseSubresource(id); + if (region !== pinned.region || accountId !== pinned.accountId) { + throw new Error("This subresource ID names another PagerDuty account than this binding's."); + } + } + return await this.#verifyAccess(observers, subresources); + } + + /** Compatibility only: observers are authorized per read, in verifyObserverAccess(). */ + async addObserver(_id: string, _user: Fetcher): Promise {} + + /** Compatibility only: there is no observer state to remove. */ + async removeObserver(_id: string): Promise {} + + /** + * The connected user, pinned with their account at the binding's first use. The account already + * refuses a reconnect as anyone else; the pin keeps a binding from following one regardless. + */ + async #me(): Promise { + const now = Date.now(); + if (this.#identity && now - this.#identity.at < IDENTITY_MEMO_MS) return this.#identity.value; + const { UserAccount } = this.ctx.exports; + const identity = await UserAccount.get(UserAccount.idFromString(this.ctx.props.userObjectId)).getIdentity(); + const kv = this.ctx.storage.kv; + const pinned = kv.get(PINNED_KEY); + if (pinned === undefined) { + if (!sameTenant(identity, this.ctx.props)) { + throw new Error("This binding is for another PagerDuty account than its connection. Bind it again."); + } + kv.put(PINNED_KEY, { region: identity.region, accountId: identity.accountId, userId: identity.userId }); + } else if (pinned.userId !== identity.userId || pinned.accountId !== identity.accountId) { + throw new Error("This binding was made for a different PagerDuty user than its connection now uses. " + + "Remove it and bind the PagerDuty resource again."); + } + this.#identity = { value: identity, at: now }; + return identity; + } + + /** + * Stages an action, fenced on the pinned user rather than the connection generation: a + * reconnect, always as the same user, must not strand what is waiting. + */ + async #submit( + queue: ActionSubmitter, kind: K, payload: PagerDutyActions[K]): Promise { + const { userId } = await this.#me(); + return await this.#actions.submit(queue, kind, payload, { fence: { generation: userId } }); + } +} diff --git a/packages/gatekeeper-pagerduty/src/pagerduty-logo.svg b/packages/gatekeeper-pagerduty/src/pagerduty-logo.svg new file mode 100644 index 0000000000..fbf1be139f --- /dev/null +++ b/packages/gatekeeper-pagerduty/src/pagerduty-logo.svg @@ -0,0 +1 @@ + diff --git a/packages/gatekeeper-pagerduty/src/pagerduty-normalize.ts b/packages/gatekeeper-pagerduty/src/pagerduty-normalize.ts new file mode 100644 index 0000000000..8b868e05ec --- /dev/null +++ b/packages/gatekeeper-pagerduty/src/pagerduty-normalize.ts @@ -0,0 +1,264 @@ +// PagerDuty's REST objects as the gatekeeper reads them, and their conversion into the shapes +// types.d.ts gives the agent. Only the fields a conversion reads are declared; PagerDuty sends +// references (`*_reference`, named by `summary`) and full objects (named by `name`) alike. + +import type { + PagerDutyAlert, PagerDutyIncidentDetails, PagerDutyIncidentStatus, PagerDutyIncidentSummary, + PagerDutyNote, PagerDutyOnCall, PagerDutyOverride, PagerDutyPriority, PagerDutyRef, + PagerDutyScheduleDetails, PagerDutyScheduleSummary, PagerDutyServiceDetails, PagerDutyServiceSummary, + PagerDutyShift, PagerDutyTeamMember, PagerDutyTeamSummary, PagerDutyTimelineEntry, PagerDutyUrgency, + PagerDutyUser, PagerDutyUserRef, +} from "./types"; + +export type WireRef = { + id: string; + type?: string; + summary?: string | null; + name?: string | null; + html_url?: string | null; +}; + +export type WireUser = WireRef & { + email: string; + job_title?: string | null; + time_zone?: string | null; + role?: string; + avatar_url?: string | null; +}; + +export type WireTeam = WireRef & { description?: string | null }; + +export type WireService = WireRef & { + description?: string | null; + status?: PagerDutyServiceSummary["status"]; + teams?: WireRef[]; + escalation_policy?: WireRef; + acknowledgement_timeout?: number | null; + auto_resolve_timeout?: number | null; + created_at?: string; +}; + +export type WireIncident = WireRef & { + incident_number: number; + title: string; + status: PagerDutyIncidentStatus; + urgency: PagerDutyUrgency; + priority?: WireRef | null; + service: WireRef; + teams?: WireRef[]; + escalation_policy?: WireRef; + assignments?: { assignee: WireRef }[]; + acknowledgements?: { acknowledger: WireRef; at: string }[]; + incident_key?: string | null; + created_at: string; + last_status_change_at?: string; + resolved_at?: string | null; + alert_counts?: { all?: number }; + conference_bridge?: { conference_url?: string | null } | null; + body?: { details?: unknown } | null; +}; + +export type WireAlert = WireRef & { + status: PagerDutyAlert["status"]; + severity?: PagerDutyAlert["severity"]; + created_at: string; + body?: { details?: unknown; cef_details?: { source_origin?: string | null } } | null; +}; + +export type WireLogEntry = WireRef & { created_at: string; agent?: WireRef | null }; + +export type WireNote = { id: string; content: string; created_at: string; user?: WireRef | null }; + +export type WireOnCall = { + user: WireRef; + escalation_level: number; + escalation_policy: WireRef; + schedule?: WireRef | null; + start?: string | null; + end?: string | null; +}; + +export type WireSchedule = WireRef & { + description?: string | null; + time_zone: string; + teams?: WireRef[]; + escalation_policies?: WireRef[]; + schedule_layers?: { + name?: string; + users?: { user: WireRef }[]; + rotation_turn_length_seconds?: number; + }[]; + final_schedule?: { rendered_schedule_entries?: WireShift[] }; +}; + +export type WireShift = { user: WireRef; start: string; end: string }; + +export type WireOverride = WireShift & { id: string }; + +export type WirePriority = { id: string; name: string; description?: string | null }; + +export type WireMember = { user: WireUser; role: string }; + +/** What another object names this one by. */ +export function toRef(wire: WireRef): PagerDutyRef { + return { id: wire.id, name: wire.name ?? wire.summary ?? wire.id, htmlUrl: wire.html_url ?? "" }; +} + +export function toUserRef(wire: WireRef): PagerDutyUserRef { + return { id: wire.id, name: wire.name ?? wire.summary ?? wire.id }; +} + +export function toUser(wire: WireUser): PagerDutyUser { + return { + ...toUserRef(wire), + email: wire.email, + jobTitle: wire.job_title || null, + timeZone: wire.time_zone ?? "", + htmlUrl: wire.html_url ?? "", + }; +} + +/** A member's team role; PagerDuty's lowest, `observer`, for one it doesn't name. */ +export function toMember(wire: WireMember): PagerDutyTeamMember { + const teamRole = wire.role === "responder" || wire.role === "manager" ? wire.role : "observer"; + return { ...toUser(wire.user), teamRole }; +} + +export function toTeam(wire: WireTeam): PagerDutyTeamSummary { + return { ...toRef(wire), description: wire.description || null }; +} + +export function toServiceSummary(wire: WireService): PagerDutyServiceSummary { + const team = wire.teams?.[0]; + return { + ...toRef(wire), + description: wire.description || null, + status: wire.status ?? "active", + team: team ? toRef(team) : null, + }; +} + +export function toServiceDetails(wire: WireService, you: PagerDutyUser): PagerDutyServiceDetails { + if (!wire.escalation_policy) throw new Error("PagerDuty described a service without its escalation policy."); + return { + ...toServiceSummary(wire), + escalationPolicy: toRef(wire.escalation_policy), + acknowledgementTimeout: wire.acknowledgement_timeout ?? null, + autoResolveTimeout: wire.auto_resolve_timeout ?? null, + createdAt: wire.created_at ?? "", + you, + }; +} + +export function toIncidentSummary(wire: WireIncident): PagerDutyIncidentSummary { + const team = wire.teams?.[0]; + return { + id: wire.id, + number: wire.incident_number, + title: wire.title, + status: wire.status, + urgency: wire.urgency, + priority: wire.priority ? wire.priority.name ?? wire.priority.summary ?? null : null, + service: toRef(wire.service), + team: team ? toRef(team) : null, + assignees: (wire.assignments ?? []).map(({ assignee }) => toUserRef(assignee)), + createdAt: wire.created_at, + updatedAt: wire.last_status_change_at ?? wire.created_at, + htmlUrl: wire.html_url ?? "", + }; +} + +/** Plain-text details given when the incident was opened by hand, if any. */ +function detailsOf(body: WireIncident["body"]): string | null { + const details = body?.details; + if (details === undefined || details === null || details === "") return null; + return typeof details === "string" ? details : JSON.stringify(details); +} + +export function toIncidentDetails(wire: WireIncident, firstAlert: WireAlert | null): PagerDutyIncidentDetails { + if (!wire.escalation_policy) throw new Error("PagerDuty described an incident without its escalation policy."); + return { + ...toIncidentSummary(wire), + details: detailsOf(wire.body), + escalationPolicy: toRef(wire.escalation_policy), + acknowledgers: (wire.acknowledgements ?? []).map(({ acknowledger, at }) => ({ user: toUserRef(acknowledger), at })), + incidentKey: wire.incident_key || null, + resolvedAt: wire.resolved_at ?? null, + alertCount: wire.alert_counts?.all ?? 0, + firstAlert: firstAlert && toAlert(firstAlert), + conferenceUrl: wire.conference_bridge?.conference_url || null, + }; +} + +export function toAlert(wire: WireAlert): PagerDutyAlert { + const details = wire.body?.details; + return { + id: wire.id, + summary: wire.summary ?? "", + status: wire.status, + severity: wire.severity ?? "info", + createdAt: wire.created_at, + source: wire.body?.cef_details?.source_origin || null, + details: details && typeof details === "object" && !Array.isArray(details) ? details as Record : null, + htmlUrl: wire.html_url ?? "", + }; +} + +export function toTimelineEntry(wire: WireLogEntry): PagerDutyTimelineEntry { + const actor = wire.agent?.type?.startsWith("user") ? toUserRef(wire.agent) : null; + return { + id: wire.id, + at: wire.created_at, + kind: (wire.type ?? "").replace(/_log_entry$/, ""), + summary: wire.summary ?? "", + actor, + }; +} + +export function toNote(wire: WireNote): PagerDutyNote { + return { id: wire.id, content: wire.content, author: wire.user ? toUserRef(wire.user) : null, createdAt: wire.created_at }; +} + +export function toOnCall(wire: WireOnCall): PagerDutyOnCall { + return { + user: toUserRef(wire.user), + level: wire.escalation_level, + escalationPolicy: toRef(wire.escalation_policy), + schedule: wire.schedule ? toRef(wire.schedule) : null, + start: wire.start ?? null, + end: wire.end ?? null, + }; +} + +export function toScheduleSummary(wire: WireSchedule): PagerDutyScheduleSummary { + return { + ...toRef(wire), + description: wire.description || null, + timeZone: wire.time_zone, + teams: (wire.teams ?? []).map(toRef), + }; +} + +export function toScheduleDetails(wire: WireSchedule): PagerDutyScheduleDetails { + return { + ...toScheduleSummary(wire), + escalationPolicies: (wire.escalation_policies ?? []).map(toRef), + layers: (wire.schedule_layers ?? []).map(layer => ({ + name: layer.name ?? "", + users: (layer.users ?? []).map(({ user }) => toUserRef(user)), + turnLength: layer.rotation_turn_length_seconds ?? 0, + })), + }; +} + +export function toShift(wire: WireShift): PagerDutyShift { + return { user: toUserRef(wire.user), start: wire.start, end: wire.end }; +} + +export function toOverride(wire: WireOverride): PagerDutyOverride { + return { id: wire.id, ...toShift(wire) }; +} + +export function toPriority(wire: WirePriority): PagerDutyPriority { + return { id: wire.id, name: wire.name, description: wire.description ?? "" }; +} diff --git a/packages/gatekeeper-pagerduty/src/pagerduty-sessions.ts b/packages/gatekeeper-pagerduty/src/pagerduty-sessions.ts new file mode 100644 index 0000000000..6058ceea38 --- /dev/null +++ b/packages/gatekeeper-pagerduty/src/pagerduty-sessions.ts @@ -0,0 +1,984 @@ +// The capabilities a gadget holds: the account, team, service, incident and schedule sessions, each +// also what a broader one returns when it opens an object. Every read fetches as the connected +// user, replays pending actions over the result, and authorizes the observation before returning, +// reporting the account, the objects the capability is scoped to, those its filter names and those +// its result shows (see `SessionContext.authorize`). Every write validates, then stages an action; +// none reaches PagerDuty before approval. + +import { RpcTarget, type RpcStub } from "cloudflare:workers"; +import { validateRpc } from "capnweb-validate"; +import type { ApprovalQueue } from "@gadgets/workshop-shared/gatekeeper"; +import type { ActionSubmitter } from "@gadgets/gatekeeper-kit/actions"; +import { ArrayCursor, TokenCursor, type CursorInjection } from "@gadgets/gatekeeper-kit/cursors"; +import type { ObservationInput } from "@gadgets/gatekeeper-kit/observers"; +import { MAX_LIMIT, MAX_OFFSET, type Page, type PagerDutyApi, type Query } from "./pagerduty-api"; +import { + isProvisional, type IncidentTarget, type NewIncident, type PagerDutyActions, type PriorityTarget, type UserTarget, +} from "./pagerduty-actions"; +import { subresourceId, youOf, type Identity, type ObjectKind } from "./pagerduty-credentials"; +import { webOrigin, type Tenant } from "./pagerduty-env"; +import { + toAlert, toIncidentDetails, toIncidentSummary, toMember, toNote, toOnCall, toOverride, toPriority, toRef, + toScheduleDetails, toScheduleSummary, toServiceDetails, toServiceSummary, toShift, toTeam, toTimelineEntry, toUser, + type WireAlert, type WireIncident, type WireLogEntry, type WireMember, type WireNote, type WireOnCall, + type WireOverride, type WirePriority, type WireRef, type WireSchedule, type WireService, type WireTeam, type WireUser, +} from "./pagerduty-normalize"; +import { + matchesQuery, newestFirst, overlayIncident, overlayOverrides, overlayShifts, pendingIncident, pendingIncidents, + pendingNotes, type IncidentQuery, type Pending, +} from "./pagerduty-simulation"; +import { OBJECT_ID, objectId } from "./pagerduty-urls"; +import type { + Cursor, PagerDutyAccount, PagerDutyAccountMetadata, PagerDutyAlert, PagerDutyCreateIncidentOptions, PagerDutyIncident, + PagerDutyIncidentDetails, PagerDutyIncidentFilter, PagerDutyIncidentSummary, PagerDutyListOptions, PagerDutyNote, + PagerDutyOnCall, PagerDutyOnCallFilter, PagerDutyOnCallWindow, PagerDutyOverride, PagerDutyPageOptions, + PagerDutyPriority, PagerDutyResponderRequest, PagerDutySchedule, PagerDutyScheduleDetails, PagerDutyScheduleSummary, + PagerDutyService, PagerDutyServiceDetails, PagerDutyServiceSummary, PagerDutyShift, PagerDutyTeam, + PagerDutyTeamMember, PagerDutyTeamMetadata, PagerDutyTeamSummary, PagerDutyTimeWindow, PagerDutyTimelineEntry, + PagerDutyUrgency, PagerDutyUser, +} from "./types"; + +/** How long a service's team and escalation policy, read to check a binding's scope, stay cached. */ +const SERVICE_TTL_MS = 5 * 60_000; +/** How long the account's priorities and a team's escalation policies stay cached. */ +const CATALOG_TTL_MS = 10 * 60_000; +/** Listings' default page size. */ +const DEFAULT_PAGE_SIZE = 25; +/** How far back incident listings reach by default. */ +const DEFAULT_INCIDENT_DAYS = 30; +const DAY_MS = 24 * 60 * 60_000; +/** The most users `findUsers` returns. */ +const MAX_USERS = 50; +/** PagerDuty's longest note. */ +const MAX_NOTE_LENGTH = 25_000; +/** PagerDuty's longest snooze. */ +const MAX_SNOOZE_SECONDS = 604_800; +/** The longest schedule window PagerDuty renders shifts for. */ +const MAX_SHIFT_DAYS = 90; + +const HOOKS_UNAVAILABLE = "PagerDuty hooks are not configured on this deployment."; + +/** What a session may ask of the gatekeeper Durable Object. */ +export type SessionHost = { + readonly tenant: Tenant; + /** The connected user, whose account every subresource names. */ + me(): Promise; + /** Runs a read as the connected user. */ + read(op: (api: PagerDutyApi) => Promise): Promise; + /** A cached value, partitioned by connection. */ + cached(key: string, ttlMs: number, load: () => Promise): Promise; + /** Actions waiting for a decision, for simulation. */ + pending(): Pending; + /** The ID PagerDuty assigned a temporary ID, or the ID itself. */ + resolve(id: string): string; + /** Allocates a temporary ID for something a pending action creates. */ + allocate(kind: "incident" | "note" | "override"): string; + submit(queue: ActionSubmitter, kind: K, payload: PagerDutyActions[K]): Promise; +}; + +/** An object a read depends on. */ +export type ObjectRef = { kind: ObjectKind; id: string }; + +/** What restricts a read beyond its capability's scope: the objects its filter names, and its result shows. */ +export type ReadReport = { filter: readonly ObjectRef[]; result: readonly ObjectRef[] }; + +/** + * What one capability owns: the gatekeeper host, its own approval-queue stub, and its scope -- the + * objects every read through it depends on besides the account. A capability handed out takes a + * duplicate stub, so disposing one never disturbs another. + */ +export class SessionContext implements Disposable { + constructor(readonly host: SessionHost, readonly queue: RpcStub, readonly scope: readonly ObjectRef[]) {} + + dup(): SessionContext { + return new SessionContext(this.host, this.queue.dup(), this.scope); + } + + /** A context for an object opened from this one, which every read through it also depends on. */ + within(object: ObjectRef): SessionContext { + return new SessionContext(this.host, this.queue.dup(), [...this.scope, object]); + } + + /** + * Authorizes a read, reporting the account, this capability's scope, and `report`'s filter and + * result. Every read goes through here, whether it found anything or came from cache, so no read + * can leave out the account or the scope. + */ + async authorize(observation: ObservationInput, report: ReadReport): Promise { + const { region, accountId } = await this.host.me(); + const account = { region, accountId }; + const objects = [...this.scope, ...report.filter, ...report.result].filter(({ id }) => !isProvisional(id)); + await this.queue.authorizeObservation({ + ...observation, + subresources: [...new Set([subresourceId(account), ...objects.map(object => subresourceId(account, object))])], + }); + } + + [Symbol.dispose](): void { + this.queue[Symbol.dispose](); + } +} + +/** The objects a capability may reach beyond itself: those in this team, or on this service. */ +type Within = { teamId?: string; serviceId?: string }; + +const team = (id: string): ObjectRef => ({ kind: "team", id }); +const service = (id: string): ObjectRef => ({ kind: "service", id }); +const incident = (id: string): ObjectRef => ({ kind: "incident", id }); +const schedule = (id: string): ObjectRef => ({ kind: "schedule", id }); +const escalationPolicy = (id: string): ObjectRef => ({ kind: "escalation_policy", id }); + +const countOf = (count: number, noun: string): string => `${count} ${noun}${count === 1 ? "" : "s"}`; + +// --------------------------------------------------------------------------- +// Validation + +function pageSizeOf(options: PagerDutyPageOptions | undefined): number { + const size = options?.pageSize ?? DEFAULT_PAGE_SIZE; + if (!Number.isInteger(size) || size < 1 || size > MAX_LIMIT) { + throw new Error(`pageSize must be a whole number from 1 to ${MAX_LIMIT}.`); + } + return size; +} + +function text(value: unknown, what: string, max = Infinity): string { + if (typeof value !== "string" || value.trim() === "") throw new Error(`${what} must be a non-empty string.`); + if (value.length > max) throw new Error(`${what} can be at most ${max} characters.`); + return value; +} + +function time(value: string | undefined, what: string): string | undefined { + if (value === undefined) return undefined; + if (typeof value !== "string" || Number.isNaN(Date.parse(value))) throw new Error(`${what} must be an ISO 8601 time.`); + return new Date(value).toISOString(); +} + +function urgency(value: unknown): PagerDutyUrgency { + if (value !== "high" && value !== "low") throw new Error('urgency must be "high" or "low".'); + return value; +} + +function ids(values: readonly string[] | undefined, what: string): string[] | undefined { + if (values === undefined) return undefined; + if (!Array.isArray(values)) throw new Error(`${what} must be an array of PagerDuty IDs.`); + return values.map(value => objectId(value, what)); +} + +/** A window between ISO times, defaulting to `days` from now and at most `maxDays` long. */ +function windowOf(window: PagerDutyTimeWindow | undefined, days: number, maxDays = Infinity): { since: string; until: string } { + const since = time(window?.since, "since") ?? new Date().toISOString(); + const until = time(window?.until, "until") ?? new Date(Date.parse(since) + days * DAY_MS).toISOString(); + if (Date.parse(until) <= Date.parse(since)) throw new Error("until must be after since."); + if (Date.parse(until) - Date.parse(since) > maxDays * DAY_MS) throw new Error(`since and until can be at most ${maxDays} days apart.`); + return { since, until }; +} + +// --------------------------------------------------------------------------- +// Shared reads + +/** A service as PagerDuty describes it now. */ +async function readService(ctx: SessionContext, id: string): Promise { + return (await ctx.host.read(api => api.get<{ service: WireService }>(`/services/${id}`))).service; +} + +/** A service's team and escalation policy, cached briefly, for checking a binding's scope. */ +function cachedService(ctx: SessionContext, id: string): Promise { + return ctx.host.cached(`service:${id}`, SERVICE_TTL_MS, () => readService(ctx, id)); +} + +/** @throws Error unless `wire` belongs to team `teamId`. */ +function requireTeam(wire: { teams?: WireRef[] }, teamId: string | undefined, what: string): void { + if (teamId !== undefined && !wire.teams?.some(({ id }) => id === teamId)) { + throw new Error(`That ${what} doesn't belong to this team.`); + } +} + +/** Every user matching `query`, up to `MAX_USERS`. */ +async function findUsers(ctx: SessionContext, query: string | undefined): Promise { + if (query !== undefined) text(query, "query", 200); + const users = await ctx.host.read(api => api.all("/users", "users", { query }, MAX_USERS)); + await ctx.authorize({ title: "Looked up PagerDuty users", description: `Found ${countOf(users.length, "user")}.` }, + { filter: [], result: [] }); + return users.map(toUser); +} + +/** The account's priorities, cached briefly. */ +function priorities(ctx: SessionContext): Promise { + return ctx.host.cached("priorities", CATALOG_TTL_MS, + () => ctx.host.read(api => api.all("/priorities", "priorities"))); +} + +async function listPriorities(ctx: SessionContext): Promise { + const rows = await priorities(ctx); + await ctx.authorize({ title: "Listed PagerDuty priorities", description: `Listed ${countOf(rows.length, "priority")}.` }, + { filter: [], result: [] }); + return rows.map(toPriority); +} + +/** A priority named by the agent, for an action's payload. */ +async function priorityNamed(ctx: SessionContext, name: string): Promise { + text(name, "priority"); + const match = (await priorities(ctx)).find(priority => priority.name.toLowerCase() === name.toLowerCase()); + if (!match) throw new Error(`There is no priority named "${name}"; listPriorities() lists them.`); + return { id: match.id, name: match.name }; +} + +/** Users named by email or ID, for an action's payload or a filter. */ +async function usersNamed(ctx: SessionContext, given: readonly string[], what: string): Promise { + if (!Array.isArray(given) || given.length === 0) throw new Error(`${what} must name at least one user.`); + return await Promise.all(given.map(async value => { + text(value, what); + if (OBJECT_ID.test(value)) { + const { user } = await ctx.host.read(api => api.get<{ user: WireUser }>(`/users/${value}`)); + return { id: user.id, name: user.name ?? user.id, given: value }; + } + const users = await ctx.host.read(api => api.all("/users", "users", { query: value }, MAX_USERS)); + const user = users.find(({ email }) => email.toLowerCase() === value.toLowerCase()); + if (!user) throw new Error(`No PagerDuty user has the email ${value}.`); + return { id: user.id, name: user.name ?? user.id, given: value }; + })); +} + +/** + * A listing walked by offset, authorizing each page as it leaves: the cursor owns a duplicate of + * the capability's queue stub and releases it when the walk is dropped. + */ +function offsetCursor(ctx: SessionContext, options: { + pageSize: number; + fetch(api: PagerDutyApi, offset: number, limit: number): Promise>; + /** Converts a page, applying pending actions; may drop rows they move out of the listing. */ + convert(rows: W[]): T[]; + describe(rows: readonly T[], terminal: boolean): ObservationInput; + filter: readonly ObjectRef[]; + result(rows: readonly T[]): ObjectRef[]; + inject?: CursorInjection; +}): Cursor { + const walk = ctx.dup(); + return new TokenCursor({ + pageSize: options.pageSize, + remotePageSize: Math.max(options.pageSize, DEFAULT_PAGE_SIZE), + dispose: () => walk[Symbol.dispose](), + ...(options.inject ? { inject: options.inject } : {}), + fetchPage: async (token, perPage) => { + const offset = token === undefined ? 0 : Number(token); + const page = await walk.host.read(api => options.fetch(api, offset, Math.min(perPage, MAX_OFFSET - offset))); + const next = offset + page.items.length; + // PagerDuty refuses an offset past its cap, so the walk ends there. + const more = page.more && page.items.length > 0 && next < MAX_OFFSET; + return { items: options.convert(page.items), ...(more ? { nextToken: String(next) } : {}) }; + }, + authorizePage: (rows, { terminal }) => + walk.authorize(options.describe(rows, terminal), { filter: options.filter, result: options.result(rows) }), + }); +} + +/** What a listing page's observation says. */ +function listed(title: string, noun: string) { + return (rows: readonly unknown[], terminal: boolean): ObservationInput => ({ + title, + description: rows.length > 0 ? `Read ${countOf(rows.length, noun)}.` + : terminal ? `Listed ${noun}s; there were none.` : `Scanned ${noun}s; none were in this window.`, + }); +} + +/** + * A name-sorted listing of services, teams or schedules. A team's own listings report nothing per + * row, since the team's scope covers what it owns; the account's report each row. + */ +function namedListing(ctx: SessionContext, options: { + path: string; key: string; kind: ObjectKind; noun: string; teamId?: string; + list?: PagerDutyListOptions; convert(wire: W): T; +}): Cursor { + const query = options.list?.query === undefined ? undefined : text(options.list.query, "query", 200); + const filters: Query = { query, ...(options.teamId ? { team_ids: [options.teamId] } : {}) }; + if (options.kind === "service") filters.sort_by = "name"; + const ids = new Map(); + return offsetCursor(ctx, { + pageSize: pageSizeOf(options.list), + fetch: (api, offset, limit) => api.page(options.path, options.key, filters, offset, limit), + convert: rows => rows.map(row => { + const converted = options.convert(row); + ids.set(converted, row.id); + return converted; + }), + describe: listed(`Listed PagerDuty ${options.noun}s`, options.noun), + filter: [], + result: rows => options.teamId ? [] : rows.map(row => ({ kind: options.kind, id: ids.get(row)! })), + }); +} + +/** The defaults and bounds of an incident filter, as PagerDuty's query and the simulation's. */ +function incidentQuery(filter: PagerDutyIncidentFilter | undefined, within: Within, me: Identity): { query: IncidentQuery; params: Query } { + const statuses = filter?.statuses ?? ["triggered", "acknowledged"]; + if (!Array.isArray(statuses) || statuses.some(status => !["triggered", "acknowledged", "resolved"].includes(status))) { + throw new Error('statuses must list "triggered", "acknowledged" or "resolved".'); + } + const urgencies = filter?.urgencies?.map(urgency); + let serviceIds = ids(filter?.serviceIds, "serviceIds"); + if (within.serviceId) serviceIds = (serviceIds ?? [within.serviceId]).filter(id => id === within.serviceId); + let since: string | undefined; + let until: string | undefined; + if (!filter?.allTime) { + until = time(filter?.until, "until") ?? new Date().toISOString(); + since = time(filter?.since, "since") ?? new Date(Date.parse(until) - DEFAULT_INCIDENT_DAYS * DAY_MS).toISOString(); + const limit = new Date(since); + limit.setUTCMonth(limit.getUTCMonth() + 6); + if (Date.parse(until) <= Date.parse(since)) throw new Error("until must be after since."); + if (Date.parse(until) > limit.getTime()) { + throw new Error("since and until can be at most 6 months apart; set allTime to search every incident."); + } + } + const query: IncidentQuery = { + statuses, urgencies, serviceIds, since, until, assignedToMe: filter?.assignedToMe === true, + }; + const params: Query = { + statuses, urgencies, service_ids: serviceIds, since, until, + user_ids: query.assignedToMe ? [me.userId] : undefined, + team_ids: within.teamId ? [within.teamId] : undefined, + date_range: filter?.allTime ? "all" : undefined, + sort_by: "created_at:desc", + }; + return { query, params }; +} + +/** Incidents, newest first, as pending actions leave them. */ +async function listIncidents(ctx: SessionContext, filter: PagerDutyIncidentFilter | undefined, + within: Within): Promise> { + const { host } = ctx; + const me = await host.me(); + const { query, params } = incidentQuery(filter, within, me); + const you = { id: me.userId, name: me.name }; + const overlay = (row: PagerDutyIncidentSummary) => overlayIncident(row, host.pending(), id => host.resolve(id), you); + const inScope = (row: PagerDutyIncidentSummary) => (!within.serviceId || row.service.id === within.serviceId) + && (!within.teamId || row.team?.id === within.teamId); + const fresh = () => pendingIncidents(host.pending(), id => host.resolve(id), you) + .filter(row => inScope(row) && matchesQuery(row, query, you)); + const injected = fresh().toSorted(newestFirst); + return offsetCursor(ctx, { + pageSize: pageSizeOf(filter), + // A filter naming none of a service binding's services matches nothing, so PagerDuty isn't asked. + fetch: (api, offset, limit) => query.serviceIds?.length === 0 + ? Promise.resolve({ items: [], more: false }) + : api.page("/incidents", "incidents", params, offset, limit), + convert: rows => rows.map(row => overlay(toIncidentSummary(row))).filter(row => matchesQuery(row, query, you)), + describe: listed("Listed PagerDuty incidents", "incident"), + // A pending incident shows what staging read of its service, and the cursor never passes it to + // `result`, so every page of the walk reports that service. + filter: [...query.serviceIds ?? [], ...injected.map(row => row.service.id)].map(service), + // One check per service rather than per incident: an incident is as visible as its service. + result: rows => rows.map(row => service(row.service.id)), + inject: { + items: injected, + compare: newestFirst, + revalidate: row => fresh().find(({ id }) => id === row.id) ?? null, + identity: row => host.resolve(row.id), + }, + }); +} + +/** Who is on call, as `GET /oncalls` answers for `params`, reporting `fixed` even when empty. */ +async function listOnCalls(ctx: SessionContext, window: PagerDutyOnCallWindow | undefined, options: { + params: Query; filter: readonly ObjectRef[]; fixed: readonly ObjectRef[]; none?: boolean; +}): Promise { + const levels = window?.levels; + if (levels !== undefined && (!Array.isArray(levels) || levels.some(level => !Number.isInteger(level) || level < 1))) { + throw new Error("levels must be whole numbers from 1."); + } + const params = { ...options.params, since: time(window?.since, "since"), until: time(window?.until, "until") }; + const rows = options.none ? [] : await ctx.host.read(api => api.all("/oncalls", "oncalls", params)); + const entries = rows.map(toOnCall).filter(entry => !levels || levels.includes(entry.level)); + await ctx.authorize({ title: "Listed who is on call in PagerDuty", description: `Read ${countOf(entries.length, "on-call entry")}.` }, { + filter: options.filter, + result: [...options.fixed, ...entries.flatMap(entry => [ + escalationPolicy(entry.escalationPolicy.id), ...(entry.schedule ? [schedule(entry.schedule.id)] : []), + ])], + }); + return entries; +} + +const unsupportedHooks = (): never => { + throw new Error(HOOKS_UNAVAILABLE); +}; + +// --------------------------------------------------------------------------- +// Account + +@validateRpc() +export class PagerDutyAccountImpl extends RpcTarget implements PagerDutyAccount { + readonly #ctx: SessionContext; + + constructor(ctx: SessionContext) { + super(); + this.#ctx = ctx; + } + + async getMetadata(): Promise { + const me = await this.#ctx.host.me(); + await this.#ctx.authorize({ title: "Read the PagerDuty account", description: "Read the account and the connected user." }, + { filter: [], result: [] }); + return { subdomain: me.subdomain, htmlUrl: webOrigin(me), you: youOf(me) }; + } + + async listIncidents(filter?: PagerDutyIncidentFilter): Promise> { + return await listIncidents(this.#ctx, filter, {}); + } + + async getIncident(idOrNumber: string): Promise { + return await PagerDutyIncidentImpl.open(this.#ctx, idOrNumber, {}); + } + + async listServices(options?: PagerDutyListOptions): Promise> { + return namedListing(this.#ctx, + { path: "/services", key: "services", kind: "service", noun: "service", list: options, convert: toServiceSummary }); + } + + async getService(id: string): Promise { + objectId(id, "id"); + return new PagerDutyServiceImpl(this.#ctx.within(service(id)), id, {}); + } + + async listTeams(options?: PagerDutyListOptions): Promise> { + return namedListing(this.#ctx, + { path: "/teams", key: "teams", kind: "team", noun: "team", list: options, convert: toTeam }); + } + + async getTeam(id: string): Promise { + objectId(id, "id"); + return new PagerDutyTeamImpl(this.#ctx.within(team(id)), id); + } + + async listSchedules(options?: PagerDutyListOptions): Promise> { + return namedListing(this.#ctx, + { path: "/schedules", key: "schedules", kind: "schedule", noun: "schedule", list: options, convert: toScheduleSummary }); + } + + async getSchedule(id: string): Promise { + objectId(id, "id"); + return new PagerDutyScheduleImpl(this.#ctx.within(schedule(id)), id, {}); + } + + async listOnCalls(filter?: PagerDutyOnCallFilter): Promise { + const scheduleIds = ids(filter?.scheduleIds, "scheduleIds"); + const policyIds = ids(filter?.escalationPolicyIds, "escalationPolicyIds"); + const users = filter?.users === undefined ? undefined : await usersNamed(this.#ctx, filter.users, "users"); + return await listOnCalls(this.#ctx, filter, { + params: { user_ids: users?.map(({ id }) => id), schedule_ids: scheduleIds, escalation_policy_ids: policyIds }, + filter: [...(scheduleIds ?? []).map(schedule), ...(policyIds ?? []).map(escalationPolicy)], + fixed: [], + }); + } + + async findUsers(query?: string): Promise { + return await findUsers(this.#ctx, query); + } + + async listPriorities(): Promise { + return await listPriorities(this.#ctx); + } + + async subscribe(): Promise { + unsupportedHooks(); + } + + [Symbol.dispose](): void { + this.#ctx[Symbol.dispose](); + } +} + +// --------------------------------------------------------------------------- +// Team + +@validateRpc() +export class PagerDutyTeamImpl extends RpcTarget implements PagerDutyTeam { + readonly #ctx: SessionContext; + readonly #id: string; + + constructor(ctx: SessionContext, id: string) { + super(); + this.#ctx = ctx; + this.#id = id; + } + + async getMetadata(): Promise { + const { team: wire } = await this.#ctx.host.read(api => api.get<{ team: WireTeam }>(`/teams/${this.#id}`)); + const me = await this.#ctx.host.me(); + await this.#ctx.authorize({ title: "Read a PagerDuty team", description: "Read the team's name and description." }, + { filter: [], result: [] }); + return { ...toTeam(wire), you: youOf(me) }; + } + + async listMembers(): Promise { + const rows = await this.#ctx.host.read(api => + api.all(`/teams/${this.#id}/members`, "members", { include: ["users"] })); + await this.#ctx.authorize({ title: "Listed a PagerDuty team's members", description: `Read ${countOf(rows.length, "member")}.` }, + { filter: [], result: [] }); + return rows.map(toMember); + } + + async listServices(options?: PagerDutyListOptions): Promise> { + return namedListing(this.#ctx, { + path: "/services", key: "services", kind: "service", noun: "service", teamId: this.#id, list: options, + convert: toServiceSummary, + }); + } + + async getService(id: string): Promise { + requireTeam(await cachedService(this.#ctx, objectId(id, "id")), this.#id, "service"); + return new PagerDutyServiceImpl(this.#ctx.within(service(id)), id, { teamId: this.#id }); + } + + async listIncidents(filter?: PagerDutyIncidentFilter): Promise> { + return await listIncidents(this.#ctx, filter, { teamId: this.#id }); + } + + async getIncident(idOrNumber: string): Promise { + return await PagerDutyIncidentImpl.open(this.#ctx, idOrNumber, { teamId: this.#id }); + } + + async listSchedules(options?: PagerDutyListOptions): Promise> { + return namedListing(this.#ctx, { + path: "/schedules", key: "schedules", kind: "schedule", noun: "schedule", teamId: this.#id, list: options, + convert: toScheduleSummary, + }); + } + + async getSchedule(id: string): Promise { + const { schedule: wire } = await this.#ctx.host.read(api => + api.get<{ schedule: WireSchedule }>(`/schedules/${objectId(id, "id")}`)); + requireTeam(wire, this.#id, "schedule"); + return new PagerDutyScheduleImpl(this.#ctx.within(schedule(id)), id, { teamId: this.#id }); + } + + async listOnCalls(window?: PagerDutyOnCallWindow): Promise { + const policies = await this.#ctx.host.cached(`policies:${this.#id}`, CATALOG_TTL_MS, () => this.#ctx.host.read(api => + api.all("/escalation_policies", "escalation_policies", { team_ids: [this.#id] }))); + const policyIds = policies.map(({ id }) => id); + // With no policy to filter by, PagerDuty would answer for the whole account. + return await listOnCalls(this.#ctx, window, { + params: { escalation_policy_ids: policyIds }, filter: [], fixed: policyIds.map(escalationPolicy), + none: policyIds.length === 0, + }); + } + + async findUsers(query?: string): Promise { + return await findUsers(this.#ctx, query); + } + + async listPriorities(): Promise { + return await listPriorities(this.#ctx); + } + + async subscribe(): Promise { + unsupportedHooks(); + } + + [Symbol.dispose](): void { + this.#ctx[Symbol.dispose](); + } +} + +// --------------------------------------------------------------------------- +// Service + +@validateRpc() +export class PagerDutyServiceImpl extends RpcTarget implements PagerDutyService { + readonly #ctx: SessionContext; + readonly #id: string; + readonly #within: Within; + + /** @param within The team the service was opened from, which it must still belong to. */ + constructor(ctx: SessionContext, id: string, within: { teamId?: string }) { + super(); + this.#ctx = ctx; + this.#id = id; + this.#within = { ...within, serviceId: id }; + } + + async getDetails(): Promise { + const wire = await readService(this.#ctx, this.#id); + requireTeam(wire, this.#within.teamId, "service"); + const me = await this.#ctx.host.me(); + await this.#ctx.authorize({ title: "Read a PagerDuty service", description: "Read the service's details." }, + { filter: [], result: [] }); + return toServiceDetails(wire, youOf(me)); + } + + async listIncidents(filter?: PagerDutyIncidentFilter): Promise> { + return await listIncidents(this.#ctx, filter, this.#within); + } + + async getIncident(idOrNumber: string): Promise { + return await PagerDutyIncidentImpl.open(this.#ctx, idOrNumber, this.#within); + } + + async createIncident(options: PagerDutyCreateIncidentOptions): Promise { + const ctx = this.#ctx; + const title = text(options?.title, "title", 1024); + const details = options.details === undefined ? undefined : text(options.details, "details", MAX_NOTE_LENGTH); + const incidentKey = options.incidentKey === undefined ? undefined : text(options.incidentKey, "incidentKey", 255); + const chosenUrgency = options.urgency === undefined ? undefined : urgency(options.urgency); + const priority = options.priority === undefined ? undefined : await priorityNamed(ctx, options.priority); + const assignTo = options.assignTo === undefined ? undefined : await usersNamed(ctx, options.assignTo, "assignTo"); + const wire = await cachedService(ctx, this.#id); + requireTeam(wire, this.#within.teamId, "service"); + if (!wire.escalation_policy) throw new Error("PagerDuty described the service without its escalation policy."); + const summary = toServiceSummary(wire); + const ref = ctx.host.allocate("incident"); + const payload: NewIncident = { + ref, service: { id: summary.id, name: summary.name, htmlUrl: summary.htmlUrl }, + team: summary.team, escalationPolicy: toRef(wire.escalation_policy), + title, stagedAt: new Date().toISOString(), + ...(details !== undefined ? { details } : {}), + ...(chosenUrgency ? { urgency: chosenUrgency } : {}), + ...(priority ? { priority } : {}), + ...(assignTo ? { assignTo } : {}), + ...(incidentKey !== undefined ? { incidentKey } : {}), + }; + await ctx.host.submit(ctx.queue, "createIncident", payload); + return new PagerDutyIncidentImpl(ctx.dup(), ref, this.#within); + } + + async listOnCalls(window?: PagerDutyOnCallWindow): Promise { + const wire = await cachedService(this.#ctx, this.#id); + const policyId = wire.escalation_policy?.id; + if (!policyId) throw new Error("PagerDuty described the service without its escalation policy."); + return await listOnCalls(this.#ctx, window, + { params: { escalation_policy_ids: [policyId] }, filter: [], fixed: [escalationPolicy(policyId)] }); + } + + async findUsers(query?: string): Promise { + return await findUsers(this.#ctx, query); + } + + async listPriorities(): Promise { + return await listPriorities(this.#ctx); + } + + async subscribe(): Promise { + unsupportedHooks(); + } + + [Symbol.dispose](): void { + this.#ctx[Symbol.dispose](); + } +} + +// --------------------------------------------------------------------------- +// Incident + +/** An incident as one read found it: PagerDuty's, or one a pending action opens. */ +type Loaded = { wire: WireIncident; pending?: never } | { wire?: never; pending: NewIncident }; + +const INCIDENT_INCLUDES = ["acknowledgers", "assignees", "conference_bridge", "priorities", "body"]; + +@validateRpc() +export class PagerDutyIncidentImpl extends RpcTarget implements PagerDutyIncident { + readonly #ctx: SessionContext; + readonly #id: string; + readonly #within: Within; + + /** + * @param id PagerDuty's ID, or the `~N` of an incident a pending action opens. + * @param within The team or service the incident was opened from, which it must still belong to. + */ + constructor(ctx: SessionContext, id: string, within: Within) { + super(); + this.#ctx = ctx; + this.#id = id; + this.#within = within; + } + + /** Opens an incident by ID or number, refusing one outside `within`. */ + static async open(ctx: SessionContext, idOrNumber: string, within: Within): Promise { + if (!OBJECT_ID.test(idOrNumber) && !isProvisional(idOrNumber)) { + throw new Error('idOrNumber must be an incident ID, such as "Q2W3E4R5T6Y7U8", or its number.'); + } + const opened = new PagerDutyIncidentImpl(ctx.dup(), idOrNumber, within); + try { + const loaded = await opened.#load(); + return loaded.wire ? new PagerDutyIncidentImpl(opened.#ctx, loaded.wire.id, within) : opened; + } catch (error) { + opened.#ctx[Symbol.dispose](); + throw error; + } + } + + /** The incident now, checked against the capability's scope. */ + async #load(includes: readonly string[] = []): Promise { + const { host } = this.#ctx; + const id = host.resolve(this.#id); + let loaded: Loaded; + if (isProvisional(id)) { + const created = host.pending().find(({ action }) => action.kind === "createIncident" && action.payload.ref === id); + if (created?.action.kind !== "createIncident") { + throw new Error("This incident was never opened: opening it was rejected or failed."); + } + loaded = { pending: created.action.payload }; + } else { + const { incident: wire } = await host.read(api => + api.get<{ incident: WireIncident }>(`/incidents/${id}`, includes.length ? { include: includes } : {})); + loaded = { wire }; + } + const serviceId = loaded.wire ? loaded.wire.service.id : loaded.pending.service.id; + if (this.#within.serviceId && serviceId !== this.#within.serviceId) { + throw new Error("That incident belongs to another service."); + } + if (this.#within.teamId) requireTeam(await cachedService(this.#ctx, serviceId), this.#within.teamId, "incident"); + return loaded; + } + + /** What this incident's reads depend on: itself, once PagerDuty has it, and its service. */ + #result(loaded: Loaded): ObjectRef[] { + return loaded.wire ? [incident(loaded.wire.id), service(loaded.wire.service.id)] : [service(loaded.pending.service.id)]; + } + + async #you() { + const me = await this.#ctx.host.me(); + return { id: me.userId, name: me.name }; + } + + async getDetails(): Promise { + const { host } = this.#ctx; + const loaded = await this.#load(INCIDENT_INCLUDES); + let details: PagerDutyIncidentDetails; + if (loaded.wire) { + const id = loaded.wire.id; + const { alerts } = await host.read(api => api.get<{ alerts: WireAlert[] }>(`/incidents/${id}/alerts`, + { limit: 1, sort_by: "created_at:asc" })); + details = toIncidentDetails(loaded.wire, alerts[0] ?? null); + } else { + details = pendingIncident(loaded.pending); + } + await this.#ctx.authorize({ title: "Read a PagerDuty incident", description: "Read the incident's details." }, + { filter: [], result: this.#result(loaded) }); + return overlayIncident(details, host.pending(), id => host.resolve(id), await this.#you()); + } + + async getTimeline(options?: PagerDutyPageOptions): Promise> { + return await this.#history(options, async (api, id) => { + // PagerDuty doesn't document the order, so the whole log is read and put oldest first. + const rows = await api.all(`/incidents/${id}/log_entries`, "log_entries"); + return rows.map(toTimelineEntry).toSorted((a, b) => Date.parse(a.at) - Date.parse(b.at)); + }, "log entry"); + } + + async listNotes(): Promise { + const { host } = this.#ctx; + const loaded = await this.#load(); + const rows = loaded.wire + ? (await host.read(api => api.get<{ notes: WireNote[] }>(`/incidents/${loaded.wire.id}/notes`))).notes.map(toNote) + : []; + const id = loaded.wire ? loaded.wire.id : loaded.pending.ref; + const notes = [...rows, ...pendingNotes(id, host.pending(), ref => host.resolve(ref), await this.#you())] + .toSorted((a, b) => Date.parse(a.createdAt) - Date.parse(b.createdAt)); + await this.#ctx.authorize({ title: "Read a PagerDuty incident's notes", description: `Read ${countOf(notes.length, "note")}.` }, + { filter: [], result: this.#result(loaded) }); + return notes; + } + + async listAlerts(options?: PagerDutyPageOptions): Promise> { + const pageSize = pageSizeOf(options); + const loaded = await this.#load(); + if (!loaded.wire) return await this.#history(options, async () => [], "alert"); + const { id } = loaded.wire; + return offsetCursor(this.#ctx, { + pageSize, + fetch: (api, offset, limit) => api.page(`/incidents/${id}/alerts`, "alerts", + { sort_by: "created_at:desc" }, offset, limit), + convert: rows => rows.map(toAlert), + describe: listed("Listed a PagerDuty incident's alerts", "alert"), + filter: [], + result: () => this.#result(loaded), + }); + } + + /** A history read whole, then paged, each page authorized with the incident's dependencies. */ + async #history(options: PagerDutyPageOptions | undefined, + read: (api: PagerDutyApi, id: string) => Promise, noun: string): Promise> { + const pageSize = pageSizeOf(options); + const loaded = await this.#load(); + // An incident still waiting to be opened has no history. + if (!loaded.wire) { + await this.#ctx.authorize(listed(`Listed a PagerDuty incident's ${noun}s`, noun)([], true), + { filter: [], result: this.#result(loaded) }); + return new ArrayCursor([], pageSize); + } + const { id } = loaded.wire; + return offsetCursor(this.#ctx, { + pageSize, + fetch: async (api, offset) => offset > 0 ? { items: [], more: false } : { items: await read(api, id), more: false }, + convert: rows => rows, + describe: listed(`Listed a PagerDuty incident's ${noun}s`, noun), + filter: [], + result: () => this.#result(loaded), + }); + } + + /** The incident an action stages against, checked against the capability's scope. */ + async #target(): Promise<{ target: IncidentTarget; status: PagerDutyIncidentSummary["status"] }> { + const { host } = this.#ctx; + const loaded = await this.#load(); + const summary = loaded.wire ? toIncidentSummary(loaded.wire) : pendingIncident(loaded.pending); + const { status } = overlayIncident(summary, host.pending(), id => host.resolve(id), await this.#you()); + return { target: loaded.wire ? { id: loaded.wire.id, number: loaded.wire.incident_number } : { id: this.#id }, status }; + } + + async #stage>( + kind: K, payload: (target: IncidentTarget) => PagerDutyActions[K], + ): Promise { + const { target } = await this.#target(); + await this.#ctx.host.submit(this.#ctx.queue, kind, payload(target)); + } + + async acknowledge(): Promise { + await this.#stage("acknowledge", target => ({ incident: target })); + } + + async resolve(resolution?: string): Promise { + const note = resolution === undefined ? undefined : text(resolution, "resolution", MAX_NOTE_LENGTH); + await this.#stage("resolve", target => ({ incident: target, ...(note !== undefined ? { resolution: note } : {}) })); + } + + async snooze(seconds: number): Promise { + if (!Number.isInteger(seconds) || seconds < 1 || seconds > MAX_SNOOZE_SECONDS) { + throw new Error(`seconds must be a whole number from 1 to ${MAX_SNOOZE_SECONDS}.`); + } + const { target, status } = await this.#target(); + if (status !== "acknowledged") throw new Error("Only an acknowledged incident can be snoozed."); + await this.#ctx.host.submit(this.#ctx.queue, "snooze", { incident: target, seconds }); + } + + async addNote(content: string): Promise { + text(content, "content", MAX_NOTE_LENGTH); + const ref = this.#ctx.host.allocate("note"); + await this.#stage("addNote", target => ({ incident: target, ref, content, stagedAt: new Date().toISOString() })); + } + + async reassign(to: { users: string[] } | { escalationPolicyId: string }): Promise { + const assignee = "users" in to + ? { users: await usersNamed(this.#ctx, to.users, "users") } + : { escalationPolicyId: objectId(to.escalationPolicyId, "escalationPolicyId") }; + await this.#stage("reassign", target => ({ incident: target, to: assignee })); + } + + async escalate(level: number): Promise { + if (!Number.isInteger(level) || level < 1) throw new Error("level must be a whole number from 1."); + await this.#stage("escalate", target => ({ incident: target, level })); + } + + async requestResponders(options: PagerDutyResponderRequest): Promise { + const message = text(options?.message, "message", MAX_NOTE_LENGTH); + const users = options.users?.length ? await usersNamed(this.#ctx, options.users, "users") : []; + const escalationPolicyIds = ids(options.escalationPolicyIds, "escalationPolicyIds") ?? []; + if (users.length + escalationPolicyIds.length === 0) throw new Error("Name at least one user or escalation policy."); + await this.#stage("requestResponders", target => ({ incident: target, message, users, escalationPolicyIds })); + } + + async setPriority(priority: string | null): Promise { + const chosen = priority === null ? null : await priorityNamed(this.#ctx, priority); + await this.#stage("setPriority", target => ({ incident: target, priority: chosen })); + } + + async setUrgency(value: PagerDutyUrgency): Promise { + const chosen = urgency(value); + await this.#stage("setUrgency", target => ({ incident: target, urgency: chosen })); + } + + async setTitle(title: string): Promise { + text(title, "title", 1024); + await this.#stage("setTitle", target => ({ incident: target, title })); + } + + async postStatusUpdate(message: string): Promise { + text(message, "message", MAX_NOTE_LENGTH); + await this.#stage("postStatusUpdate", target => ({ incident: target, message })); + } + + async subscribe(): Promise { + unsupportedHooks(); + } + + [Symbol.dispose](): void { + this.#ctx[Symbol.dispose](); + } +} + +// --------------------------------------------------------------------------- +// Schedule + +@validateRpc() +export class PagerDutyScheduleImpl extends RpcTarget implements PagerDutySchedule { + readonly #ctx: SessionContext; + readonly #id: string; + readonly #teamId?: string; + + /** @param within The team the schedule was opened from, which it must still belong to. */ + constructor(ctx: SessionContext, id: string, within: { teamId?: string }) { + super(); + this.#ctx = ctx; + this.#id = id; + this.#teamId = within.teamId; + } + + async #read(query: Query = {}): Promise { + const { schedule: wire } = await this.#ctx.host.read(api => + api.get<{ schedule: WireSchedule }>(`/schedules/${this.#id}`, query)); + requireTeam(wire, this.#teamId, "schedule"); + return wire; + } + + async getDetails(): Promise { + const wire = await this.#read(); + await this.#ctx.authorize({ title: "Read a PagerDuty schedule", description: "Read the schedule's details." }, + { filter: [], result: [] }); + return toScheduleDetails(wire); + } + + async listShifts(window?: PagerDutyTimeWindow): Promise { + const span = windowOf(window, 7, MAX_SHIFT_DAYS); + const wire = await this.#read(span); + const { host } = this.#ctx; + const shifts = overlayShifts((wire.final_schedule?.rendered_schedule_entries ?? []).map(toShift), this.#id, span, + host.pending(), id => host.resolve(id)); + await this.#ctx.authorize({ title: "Read who is on call on a PagerDuty schedule", description: `Read ${countOf(shifts.length, "shift")}.` }, + { filter: [], result: [] }); + return shifts; + } + + async listOverrides(window?: PagerDutyTimeWindow): Promise { + const span = windowOf(window, 30); + const { host } = this.#ctx; + if (this.#teamId) await this.#read(); + const rows = await host.read(api => + api.get<{ overrides: WireOverride[] }>(`/schedules/${this.#id}/overrides`, span)); + const overrides = overlayOverrides(rows.overrides.map(toOverride), this.#id, span, host.pending(), id => host.resolve(id)); + await this.#ctx.authorize({ title: "Read a PagerDuty schedule's overrides", description: `Read ${countOf(overrides.length, "override")}.` }, + { filter: [], result: [] }); + return overrides; + } + + async createOverride(user: string, start: string, end: string): Promise { + const from = time(text(start, "start"), "start")!; + const until = time(text(end, "end"), "end")!; + if (Date.parse(until) <= Date.parse(from)) throw new Error("end must be after start."); + if (this.#teamId) await this.#read(); + const [target] = await usersNamed(this.#ctx, [user], "user"); + const me = await this.#ctx.host.me(); + const ref = this.#ctx.host.allocate("override"); + await this.#ctx.host.submit(this.#ctx.queue, target.id === me.userId ? "createOverride" : "createOverrideForOthers", + { ref, scheduleId: this.#id, user: target, start: from, end: until }); + return ref; + } + + async deleteOverride(id: string): Promise { + if (typeof id !== "string" || (!OBJECT_ID.test(id) && !isProvisional(id))) { + throw new Error('id must be an override ID, such as "Q3X6MJ1LUKD6QW".'); + } + if (this.#teamId) await this.#read(); + await this.#ctx.host.submit(this.#ctx.queue, "deleteOverride", { scheduleId: this.#id, overrideId: id }); + } + + [Symbol.dispose](): void { + this.#ctx[Symbol.dispose](); + } +} diff --git a/packages/gatekeeper-pagerduty/src/pagerduty-simulation.ts b/packages/gatekeeper-pagerduty/src/pagerduty-simulation.ts new file mode 100644 index 0000000000..896ce4f847 --- /dev/null +++ b/packages/gatekeeper-pagerduty/src/pagerduty-simulation.ts @@ -0,0 +1,173 @@ +// Reads as if every pending action had been applied: an incident shows the status, title, +// priority, urgency and assignees its pending changes give it, incidents and overrides a pending +// action creates are listed, an override pending removal is not, and a note waiting to be added is +// among the incident's notes. Every function is pure over the journal's pending entries. + +import type { JournalEntry } from "@gadgets/gatekeeper-kit/actions"; +import type { NewIncident, PagerDutyAction } from "./pagerduty-actions"; +import type { + PagerDutyIncidentDetails, PagerDutyIncidentFilter, PagerDutyIncidentSummary, PagerDutyNote, + PagerDutyOverride, PagerDutyShift, PagerDutyUserRef, +} from "./types"; + +/** The journal's pending entries, in submission order. */ +export type Pending = readonly JournalEntry[]; + +/** Maps a temporary ID to PagerDuty's once the action creating it applied, else returns it. */ +export type Resolve = (id: string) => string; + +/** The pending actions on the incident `id`, in order. */ +function actionsOn(id: string, pending: Pending, resolve: Resolve): PagerDutyAction[] { + return pending.map(({ action }) => action) + .filter(action => "incident" in action.payload && resolve(action.payload.incident.id) === id); +} + +/** `incident` with its pending changes applied. */ +export function overlayIncident( + incident: T, pending: Pending, resolve: Resolve, me: PagerDutyUserRef, +): T { + let result = incident; + for (const action of actionsOn(incident.id, pending, resolve)) { + switch (action.kind) { + case "acknowledge": + if (result.status === "triggered") { + result = { ...result, status: "acknowledged" }; + if ("acknowledgers" in result) { + const details = result as T & PagerDutyIncidentDetails; + result = { ...details, acknowledgers: [...details.acknowledgers, { user: me, at: details.updatedAt }] }; + } + } + break; + case "resolve": + result = { ...result, status: "resolved", assignees: [] }; + break; + case "setPriority": + result = { ...result, priority: action.payload.priority?.name ?? null }; + break; + case "setTitle": + result = { ...result, title: action.payload.title }; + break; + case "setUrgency": + result = { ...result, urgency: action.payload.urgency }; + break; + case "reassign": + if ("users" in action.payload.to) { + result = { ...result, assignees: action.payload.to.users.map(({ id, name }) => ({ id, name })) }; + } + break; + } + } + return result; +} + +/** An incident a pending action opens, as reads show it until PagerDuty has it. */ +export function pendingIncident(payload: NewIncident): PagerDutyIncidentDetails { + return { + id: payload.ref, + // PagerDuty numbers an incident when it opens it. + number: 0, + title: payload.title, + status: "triggered", + urgency: payload.urgency ?? "high", + priority: payload.priority?.name ?? null, + service: payload.service, + team: payload.team, + assignees: (payload.assignTo ?? []).map(({ id, name }) => ({ id, name })), + createdAt: payload.stagedAt, + updatedAt: payload.stagedAt, + htmlUrl: "", + details: payload.details ?? null, + escalationPolicy: payload.escalationPolicy, + acknowledgers: [], + incidentKey: payload.incidentKey ?? null, + resolvedAt: null, + alertCount: 0, + firstAlert: null, + conferenceUrl: null, + }; +} + +/** The incidents pending actions open that PagerDuty doesn't have yet, with later changes applied. */ +export function pendingIncidents(pending: Pending, resolve: Resolve, me: PagerDutyUserRef): PagerDutyIncidentDetails[] { + return pending.flatMap(({ action }) => action.kind === "createIncident" && resolve(action.payload.ref) === action.payload.ref + ? [overlayIncident(pendingIncident(action.payload), pending, resolve, me)] : []); +} + +/** The filter a listing applies, with its defaults filled in. */ +export type IncidentQuery = Required> + & Pick; + +/** Whether an incident, as simulated, still belongs in a listing made with `query`. */ +export function matchesQuery(incident: PagerDutyIncidentSummary, query: IncidentQuery, me: PagerDutyUserRef): boolean { + return query.statuses.includes(incident.status) + && (!query.urgencies || query.urgencies.includes(incident.urgency)) + && (!query.assignedToMe || incident.assignees.some(({ id }) => id === me.id)) + && (!query.serviceIds || query.serviceIds.includes(incident.service.id)) + && (!query.since || Date.parse(incident.createdAt) >= Date.parse(query.since)) + && (!query.until || Date.parse(incident.createdAt) < Date.parse(query.until)); +} + +/** Newest first, as PagerDuty lists incidents. */ +export const newestFirst = (a: PagerDutyIncidentSummary, b: PagerDutyIncidentSummary): number => + Date.parse(b.createdAt) - Date.parse(a.createdAt); + +/** The notes pending actions add to the incident `id`. */ +export function pendingNotes(id: string, pending: Pending, resolve: Resolve, me: PagerDutyUserRef): PagerDutyNote[] { + return actionsOn(id, pending, resolve).flatMap(action => action.kind === "addNote" + ? [{ id: action.payload.ref, content: action.payload.content, author: me, createdAt: action.payload.stagedAt }] : []); +} + +/** The pending overrides on schedule `scheduleId` PagerDuty doesn't have yet. */ +function pendingOverrides(scheduleId: string, pending: Pending, resolve: Resolve): PagerDutyOverride[] { + return pending.flatMap(({ action }) => + (action.kind === "createOverride" || action.kind === "createOverrideForOthers") + && action.payload.scheduleId === scheduleId && resolve(action.payload.ref) === action.payload.ref + ? [{ id: action.payload.ref, user: { id: action.payload.user.id, name: action.payload.user.name }, + start: action.payload.start, end: action.payload.end }] + : []); +} + +/** Whether two ISO 8601 spans overlap. */ +const overlaps = (a: { start: string; end: string }, b: { start: string; end: string }): boolean => + Date.parse(a.start) < Date.parse(b.end) && Date.parse(b.start) < Date.parse(a.end); + +/** + * A schedule's overrides in `window` as if pending ones were made and removed: those pending + * removal dropped, those pending creation added, ordered by start. + */ +export function overlayOverrides( + overrides: readonly PagerDutyOverride[], scheduleId: string, window: { since: string; until: string }, + pending: Pending, resolve: Resolve, +): PagerDutyOverride[] { + const removed = new Set(pending.flatMap(({ action }) => + action.kind === "deleteOverride" && action.payload.scheduleId === scheduleId ? [resolve(action.payload.overrideId)] : [])); + const span = { start: window.since, end: window.until }; + return [...overrides, ...pendingOverrides(scheduleId, pending, resolve).filter(override => overlaps(override, span))] + .filter(override => !removed.has(override.id)) + .toSorted((a, b) => Date.parse(a.start) - Date.parse(b.start)); +} + +/** + * A schedule's shifts as if the overrides pending creation were made: each covers whatever it + * overlaps, cut at its ends. An override pending removal can't be undone from the shifts alone, + * since they don't say who it displaced. + */ +export function overlayShifts( + shifts: readonly PagerDutyShift[], scheduleId: string, window: { since: string; until: string }, + pending: Pending, resolve: Resolve, +): PagerDutyShift[] { + let result = [...shifts]; + const iso = (ms: number) => new Date(ms).toISOString(); + for (const override of pendingOverrides(scheduleId, pending, resolve)) { + const start = Math.max(Date.parse(override.start), Date.parse(window.since)); + const end = Math.min(Date.parse(override.end), Date.parse(window.until)); + if (!(start < end)) continue; + const span = { start: iso(start), end: iso(end) }; + result = result.flatMap(shift => !overlaps(shift, span) ? [shift] : [ + ...(Date.parse(shift.start) < start ? [{ ...shift, end: span.start }] : []), + ...(Date.parse(shift.end) > end ? [{ ...shift, start: span.end }] : []), + ]); + result.push({ user: override.user, ...span }); + } + return result.toSorted((a, b) => Date.parse(a.start) - Date.parse(b.start)); +} diff --git a/packages/gatekeeper-pagerduty/src/pagerduty-urls.ts b/packages/gatekeeper-pagerduty/src/pagerduty-urls.ts new file mode 100644 index 0000000000..74b9637dbd --- /dev/null +++ b/packages/gatekeeper-pagerduty/src/pagerduty-urls.ts @@ -0,0 +1,79 @@ +// PagerDuty's web addresses: which resource a link names, the link for a resource, and the ID +// grammar every agent-supplied ID is held to before it reaches a request path. + +import { RESOURCES, webOrigin, type Region, type ResourceKind, type Tenant } from "./pagerduty-env"; + +/** The resource a link names: an account, or one object in it. */ +export type ResourceRef = Tenant & ( + | { kind: "account" } + | { kind: Exclude; id: string } +); + +/** An object ID, e.g. `PABC123` or `Q2W3E4R5T6Y7U8`. */ +export const OBJECT_ID = /^[A-Z0-9]{1,64}$/; +/** An account-wide incident number, e.g. `12345`. */ +export const INCIDENT_NUMBER = /^[1-9][0-9]{0,15}$/; + +const PATTERNS = (Object.entries(RESOURCES) as [ResourceKind, { urlPattern: string }][]) + .map(([kind, { urlPattern }]) => [kind, new URLPattern(urlPattern)] as const); + +/** The path segment each object kind's links use. */ +const PATHS: Record, string> = { + team: "teams", + service: "service-directory", + incident: "incidents", + schedule: "schedules", +}; + +/** The resource `url` names, or null for a link that is not a PagerDuty resource. */ +export function parseResourceUrl(url: string): ResourceRef | null { + let parsed: URL; + try { + parsed = new URL(url.trim()); + } catch { + return null; + } + for (const [kind, pattern] of PATTERNS) { + const match = pattern.exec(parsed.href); + if (!match) continue; + const { hostname: { groups: host }, pathname: { groups: path } } = match; + const region: Region = parsed.hostname.endsWith(".eu.pagerduty.com") ? "eu" : "us"; + const tenant: Tenant = { subdomain: host.subdomain!, region }; + if (kind === "account") return { ...tenant, kind }; + const id = Object.entries(path).find(([name]) => name.endsWith("Id"))?.[1]; + return id && OBJECT_ID.test(id) ? { ...tenant, kind, id } : null; + } + return null; +} + +/** The canonical link for a resource. */ +export function resourceUrl(ref: ResourceRef): string { + return ref.kind === "account" ? `${webOrigin(ref)}/` : `${webOrigin(ref)}/${PATHS[ref.kind]}/${ref.id}`; +} + +/** + * An agent-supplied object ID, checked before it reaches a request path. + * @throws Error naming `what` for anything else. + */ +export function objectId(value: string, what: string): string { + if (typeof value !== "string" || !OBJECT_ID.test(value)) { + throw new Error(`${what} must be a PagerDuty ID, such as "PABC123".`); + } + return value; +} + +/** Whether two tenants are the same account address. */ +export function sameTenant(a: Tenant, b: Tenant): boolean { + return a.region === b.region && a.subdomain === b.subdomain; +} + +/** The region an ID token's claims put the account in, or `undefined` when they don't say. */ +export function regionOf(claims: { aud?: string | string[]; region?: unknown }): Region | undefined { + const audiences = typeof claims.aud === "string" ? [claims.aud] : claims.aud ?? []; + if (audiences.some(aud => aud.startsWith("https://api.eu.pagerduty.com"))) return "eu"; + if (audiences.some(aud => aud.startsWith("https://api.pagerduty.com"))) return "us"; + if (typeof claims.region !== "string") return undefined; + if (/^(us|unitedstates)$/i.test(claims.region)) return "us"; + if (/^(eu|europe|europeanunion)$/i.test(claims.region)) return "eu"; + return undefined; +} diff --git a/packages/gatekeeper-pagerduty/src/pagerduty.ts b/packages/gatekeeper-pagerduty/src/pagerduty.ts new file mode 100644 index 0000000000..4ebef07016 --- /dev/null +++ b/packages/gatekeeper-pagerduty/src/pagerduty.ts @@ -0,0 +1,656 @@ +// The PagerDuty gatekeeper's main module: the HTTP entrypoint (connect link and OAuth callback), the +// vendor and per-account entrypoints, the account Durable Object that holds the OAuth grant, and +// the observer verifier. The per-binding gatekeeper Durable Object and its sessions live in +// pagerduty-gatekeeper.ts and pagerduty-sessions.ts, re-exported here so wrangler finds every class +// on the main module. +// +// A connection is a Scoped OAuth user grant: PagerDuty access tokens live a day and refresh tokens +// rotate, so `UserAccount` keeps the grant in the kit's `CredentialCoordinator`. The token +// response's ID token names the user, account and region the grant belongs to; a connection is +// pinned to that user, and a reconnect that comes back as anyone else is refused. + +import { DurableObject, RpcStub, WorkerEntrypoint } from "cloudflare:workers"; +import { skipRpcValidation, validateRpc } from "capnweb-validate"; +import type { + AccountDescription, + ConnectHandoff, + Gatekeeper, + GatekeeperConnectCallback, + GatekeeperConnectOptions, + GatekeeperUser, + GatekeeperUserVerifier, + GatekeeperVendor as GatekeeperVendorIface, + ResourceConfiguratorFrame, + SupportedResource, + VendorDescription, +} from "@gadgets/workshop-shared/gatekeeper"; +import { workerVersionTag } from "@gadgets/workshop-shared/gatekeeper"; +import { KvTtlCache } from "@gadgets/gatekeeper-kit/cache"; +import { + connectHandoffPageHtml, errorPageHtml, htmlResponse, INVALID_LINK_HTML, +} from "@gadgets/gatekeeper-kit/connect-pages"; +import { NONCE_KEY, advanceToOAuth, claimOAuth, putInitiation } from "@gadgets/gatekeeper-kit/connect-handshake"; +import { CONNECT_TIMEOUT_MS, NONCE_BYTES, generateNonce } from "@gadgets/gatekeeper-kit/connect-nonce"; +import { + CredentialCoordinator, + CredentialsExpiredError, + isConnectionSuperseded, + isCredentialsExpired, + type CredentialsWithIdentity, + type RejectionVerdict, +} from "@gadgets/gatekeeper-kit/credentials"; +import { notifyCredentialsExpiredOnce } from "@gadgets/gatekeeper-kit/credential-expiry"; +import { commitStagedCredentials, discardStagedCredentials, stageCredentials } from "@gadgets/gatekeeper-kit/credential-stage"; +import { + OAuthClient, createPkce, mergeOAuthTokens, oauthRefresh, type OAuthTokens, +} from "@gadgets/gatekeeper-kit/oauth-client"; +import { PreviewOAuth, PreviewOAuthConfigurationError } from "@gadgets/gatekeeper-kit/preview-oauth"; +import { PagerDutyApi, isDenial } from "./pagerduty-api"; +import { + RECONNECT_MESSAGE, + identityOf, + parseSubresource, + seesPublicObjects, + subresourceId, + verifyIdToken, + type GrantIdentity, + type Identity, + type ObjectKind, + type PagerDutyVerifierApi, + type PublicGrant, + type Subresource, +} from "./pagerduty-credentials"; +import { + ALL_RESOURCES, + AUTHORIZE_URL, + RESOURCES, + SCOPES, + TOKEN_URL, + VENDOR_ID, + getBasePath, + getBaseUrl, + getRedirectUri, + missingScopes, + webOrigin, + type Env, + type PagerDutyGatekeeperImplProps, + type Tenant, +} from "./pagerduty-env"; +import { PagerDutyConfiguratorUI } from "./pagerduty-configurator"; +import type { WireUser } from "./pagerduty-normalize"; +import { parseResourceUrl, sameTenant } from "./pagerduty-urls"; +import { obsContext } from "./observability"; +import PAGERDUTY_LOGO_SVG from "./pagerduty-logo.svg"; +import TYPES_CODE from "./types.txt"; +import CONFIGURATOR_HTML from "./generated/pagerduty-configurator-ui.txt"; + +export { PagerDutyGatekeeperImpl } from "./pagerduty-gatekeeper"; +export { + PagerDutyAccountImpl, PagerDutyIncidentImpl, PagerDutyScheduleImpl, PagerDutyServiceImpl, PagerDutyTeamImpl, +} from "./pagerduty-sessions"; + +const logger = obsContext.createLogger({ component: "gatekeeper.pagerduty", vendorId: VENDOR_ID }); + +const LOGO_URL = `data:image/svg+xml,${encodeURIComponent(PAGERDUTY_LOGO_SVG)}`; + +/** How long the connected user's details are served before they are read from PagerDuty again. */ +const IDENTITY_TTL_MS = 24 * 60 * 60 * 1000; +/** PagerDuty's documented access-token lifetime, for a response that omits `expires_in`. */ +const ACCESS_TOKEN_SECONDS = 24 * 60 * 60; + +/** + * How long an observer-access answer is reused, by whether it allowed. PagerDuty doesn't say when + * a role or team membership changes, so a removal is noticed once the allowance expires, while a + * denial is kept only briefly so a new membership is noticed soon. + */ +const ACCESS_ALLOWED_TTL_MS = 15 * 60_000; +const ACCESS_DENIED_TTL_MS = 15_000; +const accessTtl = (allowed: boolean): number => allowed ? ACCESS_ALLOWED_TTL_MS : ACCESS_DENIED_TTL_MS; +/** How many objects an access check probes at once. */ +const PROBE_CONCURRENCY = 6; + +/** Where an object's access probe reads it. */ +const PROBE_PATHS: Record = { + team: "/teams/", + service: "/services/", + incident: "/incidents/", + schedule: "/schedules/", + escalation_policy: "/escalation_policies/", +}; + +/** + * Whether PagerDuty opens an object for the user whose token this is: a 403 or 404 is a definite + * no, and any other failure leaves access undetermined, so it throws. + */ +async function probe(accessToken: string, { region, object }: Subresource): Promise { + try { + await new PagerDutyApi(accessToken, region).get(`${PROBE_PATHS[object!.kind]}${object!.id}`); + return true; + } catch (error) { + if (isDenial(error)) return false; + throw error; + } +} + +/** The stored grant. The refresh token rotates and is never sent across the account boundary. */ +type Grant = { accessToken: string; refreshToken?: string; expiresAt?: number; scopes: string[] }; + +/** What a connect attempt carries from the authorize redirect to the callback. */ +type ConnectAttempt = { + codeVerifier: string; + /** The `redirect_uri` the authorize request carried, which the code exchange must repeat. */ + redirectUri: string; + /** The connection generation the attempt began under; a disconnect or reconnect since wins. */ + startedUnder: string; + /** Set when the link was minted to connect a new account, whose grant goes live. */ + firstConnect?: true; +}; + +/** A reconnect's grant, staged until the Workshop confirms the browser that finished it. */ +type ReconnectStage = { grant: Grant; identity: Identity; startedUnder: string }; + +/** How the callback ended: a handoff for the browser, or a refusal to show it. */ +type CallbackResult = { handoff: ConnectHandoff } | { refused: { title: string; detail: string } }; + +const NOT_CONFIGURED_HTML = errorPageHtml( + "PagerDuty gatekeeper not configured", + "Configure a PagerDuty Scoped OAuth client ID and secret for this gatekeeper."); + +/** The PagerDuty app's OAuth client, built from this Worker's secrets. */ +function oauthClient(env: Env): OAuthClient { + if (!env.CLIENT_ID || !env.CLIENT_SECRET) throw new Error("The PagerDuty gatekeeper is not configured."); + return new OAuthClient({ + label: "PagerDuty", + client: { method: "post", id: env.CLIENT_ID, secret: env.CLIENT_SECRET }, + tokenEndpoint: TOKEN_URL, + authorizationEndpoint: AUTHORIZE_URL, + defaultExpiresIn: ACCESS_TOKEN_SECONDS, + }); +} + +function toGrant(tokens: OAuthTokens): Grant { + return { + accessToken: tokens.accessToken, + ...(tokens.refreshToken ? { refreshToken: tokens.refreshToken } : {}), + ...(tokens.expiresAt !== undefined ? { expiresAt: tokens.expiresAt } : {}), + // RFC 6749 lets a response omit `scope` when it granted what was asked. + scopes: tokens.scopes ?? [...SCOPES], + }; +} + +/** Reads the user a grant belongs to. */ +async function fetchUser(accessToken: string, grant: GrantIdentity): Promise { + const { user } = await new PagerDutyApi(accessToken, grant.region).get<{ user: WireUser }>("/users/me"); + return identityOf(grant, user); +} + +/** The OAuth callback policy: direct in production, a relay through the stable Worker on a preview. */ +function previewOAuthFor(env: Env): PreviewOAuth | Response { + try { + return new PreviewOAuth({ callbackUri: getRedirectUri(env), env }); + } catch (error) { + return new Response( + error instanceof Error ? error.message : "The PagerDuty OAuth callback is not configured.", + { status: 503 }); + } +} + +/** The account a connect link or callback state names; null when the id is not one of ours. */ +function accountFor(ctx: ExecutionContext, doId: string): DurableObjectStub | null { + try { + return ctx.exports.UserAccount.get(ctx.exports.UserAccount.idFromString(doId)); + } catch { + return null; + } +} + +/** The `UserAccount` an entrypoint's props name. */ +function accountOf(ctx: ExecutionContext<{ userObjectId: string }>): DurableObjectStub { + return ctx.exports.UserAccount.get(ctx.exports.UserAccount.idFromString(ctx.props.userObjectId)); +} + +/** Where an account's web app lives, for messages. */ +const hostOf = (tenant: Tenant): string => new URL(webOrigin(tenant)).host; + +export default { + async fetch(req: Request, env: Env, ctx: ExecutionContext): Promise { + const url = new URL(req.url); + const basePath = getBasePath(env); + if (!url.pathname.startsWith(`${basePath}/`) && url.pathname !== basePath) { + throw new Error(`Request path ${url.pathname} does not match BASE_URL path ${basePath}`); + } + + const relPath = url.pathname.slice(basePath.length); + const path = relPath.slice(1).split("/"); + + if (path.length === 2 && path[0].length === 64 && path[1].length === NONCE_BYTES * 2) { + if (!env.CLIENT_ID || !env.CLIENT_SECRET) return htmlResponse(NOT_CONFIGURED_HTML); + const previewOAuth = previewOAuthFor(env); + if (previewOAuth instanceof Response) return previewOAuth; + const begun = await accountFor(ctx, path[0])?.beginOAuthFlow(path[1], previewOAuth.redirectUri); + if (!begun) return htmlResponse(INVALID_LINK_HTML); + const authorize = oauthClient(env).authorizationUrl({ + redirectUri: previewOAuth.redirectUri, + state: await previewOAuth.createAuthorizationState({ userObjectId: path[0], oauthNonce: begun.oauthNonce }), + scopes: [...SCOPES], + codeChallenge: begun.codeChallenge, + }); + return Response.redirect(authorize.toString(), 302); + } + + if (relPath === "/oauth") { + const previewOAuth = previewOAuthFor(env); + if (previewOAuth instanceof Response) return previewOAuth; + let doId: string; + let oauthNonce: string; + try { + const result = await previewOAuth.handleCallback(url); + if (result.kind === "relay") return result.response; + ({ userObjectId: doId, oauthNonce } = result.state); + } catch (error) { + if (error instanceof PreviewOAuthConfigurationError) return new Response(error.message, { status: 500 }); + return new Response("Error: malformed state", { status: 400 }); + } + const stub = accountFor(ctx, doId); + if (stub === null) return new Response("Error: malformed state", { status: 400 }); + + if (url.searchParams.get("error")) { + // The refusal ends the attempt: its nonce is consumed so a replayed callback cannot resume it. + if (!await stub.consumeOAuthNonce(oauthNonce)) return htmlResponse(INVALID_LINK_HTML); + return htmlResponse(errorPageHtml( + "PagerDuty authorization was not granted", "Start the connection again from the Workshop."), 400); + } + const code = url.searchParams.get("code"); + if (!code) return new Response("Error: no 'code' provided", { status: 400 }); + + const result = await stub.acceptAuthCode(code, oauthNonce); + if (result === null) return htmlResponse(INVALID_LINK_HTML); + if ("refused" in result) return htmlResponse(errorPageHtml(result.refused.title, result.refused.detail), 409); + return htmlResponse(connectHandoffPageHtml(result.handoff)); + } + + return new Response("Not Found", { status: 404 }); + }, +}; + +@validateRpc() +export class GatekeeperVendor extends WorkerEntrypoint implements GatekeeperVendorIface { + async describe(): Promise { + return { + displayName: "PagerDuty", + url: "https://www.pagerduty.com", + logo: { url: LOGO_URL }, + color: "#e6f7eb", + tagline: "Respond to incidents and see who is on call", + description: + "Connect your PagerDuty account so Cloudflare OS can read incidents, services, teams and " + + "on-call schedules, and acknowledge, resolve, reassign and annotate incidents or add " + + "schedule overrides once you approve.", + }; + } + + async versionTag(): Promise { + return workerVersionTag(this.env); + } + + async connectAccount(callback: Fetcher, + _options?: GatekeeperConnectOptions): Promise<{ url: string }> { + // Every resource type needs the same scopes, so `options.resourceUrlPatterns` narrows nothing. + const userObjectId = this.ctx.exports.UserAccount.newUniqueId(); + const initiationNonce = generateNonce(); + await this.ctx.exports.UserAccount.get(userObjectId).setCallback(callback, initiationNonce); + return { url: `${getBaseUrl(this.env)}/${userObjectId.toString()}/${initiationNonce}` }; + } + + async getSupportedResources(): Promise { + return ALL_RESOURCES; + } + + async getTypeScriptTypes(): Promise { + return TYPES_CODE; + } +} + +export class UserAccount extends DurableObject { + readonly #creds = new CredentialCoordinator(this.ctx.storage.kv, { + expiresAt: grant => grant.expiresAt, + vendorId: VENDOR_ID, + }); + + readonly #refresh = (grant: Grant): Promise => { + logger.info("refreshing PagerDuty access token", { event: "pagerduty.token.refresh" }); + return oauthRefresh(oauthClient(this.env), { + refreshToken: current => current.refreshToken, + merge: mergeOAuthTokens, + expiredMessage: RECONNECT_MESSAGE, + })(grant); + }; + + readonly #notify = (): Promise => notifyCredentialsExpiredOnce(this.ctx.storage.kv, + this.ctx.storage.kv.get>("callback"), VENDOR_ID); + + // This account's answers about its own access to subresources, partitioned by the connection + // that learned them, so none outlives a disconnect or survives a reconnect. Bypassed while the + // account holds no grant. + readonly #access = new KvTtlCache(this.ctx.storage.kv, + () => this.#creds.stored() ? this.#creds.connectionGeneration() : undefined, { name: "access" }); + + async setCallback(callback: Fetcher, initiationNonce: string): Promise { + if (!this.#creds.stored()) await this.ctx.storage.setAlarm(Date.now() + CONNECT_TIMEOUT_MS); + this.ctx.storage.kv.put("callback", callback); + putInitiation>(this.ctx.storage.kv, initiationNonce, Date.now(), + { firstConnect: true }); + } + + /** Arms a reconnect flow. */ + async prepareReconnect(initiationNonce: string): Promise { + putInitiation(this.ctx.storage.kv, initiationNonce, Date.now()); + } + + /** Swaps the initiation nonce for the OAuth-stage nonce and mints this flow's PKCE verifier. */ + async beginOAuthFlow(initiationNonce: string, redirectUri: string): + Promise<{ oauthNonce: string; codeChallenge: string } | null> { + // Reading the generation below writes one, which an old link to a deleted account must not. + if (this.ctx.storage.kv.get(NONCE_KEY) === undefined) return null; + const pkce = await createPkce(); + const oauthNonce = advanceToOAuth(this.ctx.storage.kv, initiationNonce, Date.now(), { + codeVerifier: pkce.codeVerifier, + redirectUri, + startedUnder: this.#creds.connectionGeneration(), + }); + return oauthNonce === null ? null : { oauthNonce, codeChallenge: pkce.codeChallenge }; + } + + /** Ends an attempt PagerDuty refused; returns whether `oauthNonce` named the live one. */ + async consumeOAuthNonce(oauthNonce: string): Promise { + return claimOAuth(this.ctx.storage.kv, oauthNonce, Date.now()) !== null; + } + + /** + * Finishes the code exchange. PagerDuty's authorization code lives 30 seconds, so nothing runs + * between claiming the nonce and the exchange. PagerDuty documents no token revocation, so a + * grant this refuses is dropped rather than revoked. + * @returns The handoff, a refusal to show, or null when the callback's nonce doesn't match. + */ + async acceptAuthCode(code: string, oauthNonce: string): Promise { + const kv = this.ctx.storage.kv; + const attempt = claimOAuth(kv, oauthNonce, Date.now()); + if (attempt === null) return null; + const tokens = await oauthClient(this.env).exchangeCode({ + code, redirectUri: attempt.redirectUri, codeVerifier: attempt.codeVerifier, + }); + const grant = toGrant(tokens); + + const missing = missingScopes(grant.scopes); + if (missing.length > 0) { + return { + refused: { + title: "The PagerDuty app is missing permissions", + detail: `PagerDuty didn't grant ${missing.join(", ")}. Ask whoever registered the app to ` + + "add these permissions, then connect again.", + }, + }; + } + if (!tokens.idToken) throw new Error("PagerDuty did not say which user authorized this connection."); + let identity: Identity; + try { + identity = await fetchUser(grant.accessToken, await verifyIdToken(tokens.idToken)); + } catch (error) { + throw new Error("Could not read which PagerDuty user authorized this connection. Please try again.", + { cause: error }); + } + + const callback = kv.get>("callback"); + if (callback === undefined) { + throw new Error("The PagerDuty account was disconnected while it was being authorized. Connect it again."); + } + + let handoff: ConnectHandoff; + if (!attempt.firstConnect) { + const pinned = kv.get("identity"); + if (pinned !== undefined && (pinned.userId !== identity.userId || pinned.accountId !== identity.accountId)) { + return { + refused: { + title: "That's a different PagerDuty user", + detail: `This connection is for ${pinned.email} on ${hostOf(pinned)}, but PagerDuty authorized ` + + `${identity.email} on ${hostOf(identity)}. Sign in as ${pinned.email} and reconnect, or ` + + "connect the other user as a separate account.", + }, + }; + } + // The reconnect URL is a bearer capability, so the new grant is only staged until the + // Workshop has confirmed the browser that finished the flow is the owner's. + discardStagedCredentials(kv); + const stageId = stageCredentials(kv, + { grant, identity, startedUnder: attempt.startedUnder }, Date.now()); + handoff = await callback.reconnectComplete(stageId); + } else { + try { + this.#creds.connect(grant, { ifGeneration: attempt.startedUnder }); + } catch (error) { + if (!isConnectionSuperseded(error)) throw error; + throw new Error("The PagerDuty account was disconnected while it was being authorized. Connect it again.", + { cause: error }); + } + kv.put("identity", identity); + try { + const props = { userObjectId: this.ctx.id.toString() }; + handoff = await callback.complete(this.ctx.exports.GatekeeperUserImpl({ props })); + } catch (error) { + // A connection the Workshop never took leaves its grant with nobody: drop it. + this.#creds.clear(); + kv.delete("identity"); + throw error; + } + } + await this.ctx.storage.deleteAlarm(); + return { handoff }; + } + + /** + * Makes the grant staged under `stageId` live. One a newer reconnect overtook is discarded, and + * the connection stays pinned to the user it was made with. + */ + async commitReconnect(stageId: string): Promise { + const staged = commitStagedCredentials(this.ctx.storage.kv, Date.now(), stageId); + if (!staged) throw new Error("No reconnect is awaiting confirmation. Please try again."); + try { + this.#creds.connect(staged.grant, { ifGeneration: staged.startedUnder }); + } catch (error) { + if (!isConnectionSuperseded(error)) throw error; + throw new Error("This PagerDuty account was reconnected again while this reconnect was finishing, " + + "so this one was discarded.", { cause: error }); + } + this.ctx.storage.kv.put("identity", staged.identity); + } + + /** The credential triple a facet's `CredentialSource` reads, refresh material projected out. */ + async getCredentials(): Promise> { + let snapshot: CredentialsWithIdentity; + try { + snapshot = await this.#creds.snapshot(this.#refresh, { notify: this.#notify }); + } catch (error) { + // A dead grant, a disconnect, a revoke mid-refresh: to the gadget each means one thing. + if (isCredentialsExpired(error)) throw new CredentialsExpiredError(RECONNECT_MESSAGE, { cause: error }); + throw error; + } + const { creds: { accessToken, expiresAt }, identity, generation } = snapshot; + return { creds: { accessToken, ...(expiresAt !== undefined ? { expiresAt } : {}) }, identity, generation }; + } + + /** Adjudicates PagerDuty's refusal of the credential with `identity`. */ + async reportCredentialsRejected(identity: string): Promise { + return await this.#creds.adjudicateRejection(identity, { refresh: this.#refresh, notify: this.#notify }); + } + + /** + * The connected user, read again from PagerDuty once a day old, or once older than `maxAgeMs` + * for a caller that needs it recent. A failed re-read serves the stored copy, except to a caller + * that asked for a recent one. + */ + async getIdentity(options?: { maxAgeMs?: number }): Promise { + const stored = this.ctx.storage.kv.get("identity"); + if (stored === undefined || !this.#creds.stored()) throw new CredentialsExpiredError(RECONNECT_MESSAGE); + const recent = options?.maxAgeMs !== undefined; + if (Date.now() - stored.fetchedAt < (options?.maxAgeMs ?? IDENTITY_TTL_MS)) return stored; + try { + const { creds } = await this.getCredentials(); + const fresh = await fetchUser(creds.accessToken, stored); + this.ctx.storage.kv.put("identity", fresh); + return fresh; + } catch (error) { + if (recent) throw error; + logger.warn("failed to refresh the PagerDuty identity", { event: "pagerduty.identity.refresh.failed", error }); + return stored; + } + } + + /** + * Whether this connection can read each subresource (see `subresourceId`), in order. The account + * is readable when it is this connection's and the user's base role sees public objects; an + * object, when the account is and PagerDuty opens it for this user, a 403 or 404 being a + * definite no. An allowed answer is reused for up to 15 minutes and a denial for 15 seconds, until + * the next reconnect. Throws for an ID it can't check, or when access can't be determined, which + * is never reused. + */ + async checkSubresourceAccess(subresources: string[]): Promise { + // Parsed before anything is asked, so an ID this account can't check is refused outright. + const parsed = subresources.map(parseSubresource); + const { creds } = await this.getCredentials(); + const accounts = new Map(); + for (const key of parsed) { + const id = subresourceId(key); + if (!accounts.has(id)) accounts.set(id, await this.#access.cached(id, accessTtl, () => this.#ownsAccount(key))); + } + // An object in an account the observer can't read is denied without asking about it. + const objects = [...new Set(parsed.filter(key => key.object && accounts.get(subresourceId(key))) + .map(key => subresourceId(key, key.object)))]; + const allowed = new Map(); + for (let start = 0; start < objects.length; start += PROBE_CONCURRENCY) { + await Promise.all(objects.slice(start, start + PROBE_CONCURRENCY).map(async id => { + allowed.set(id, await this.#access.cached(id, accessTtl, () => probe(creds.accessToken, parseSubresource(id)))); + })); + } + return parsed.map(key => + accounts.get(subresourceId(key))! && (!key.object || allowed.get(subresourceId(key, key.object))!)); + } + + /** Whether `key` is this connection's account and its user's role sees the account's objects. */ + async #ownsAccount(key: Subresource): Promise { + const stored = await this.getIdentity(); + if (stored.region !== key.region || stored.accountId !== key.accountId) return false; + // The role can change, so it is read afresh for every answer this caches. + return seesPublicObjects((await this.getIdentity({ maxAgeMs: 0 })).role); + } + + async alarm(): Promise { + // Drop the account if the connect flow never completed. + if (!this.#creds.stored()) await this.ctx.storage.deleteAll(); + } + + /** + * Disconnects. PagerDuty documents no token revocation, so the grant is forgotten here and lapses + * at PagerDuty: the access token within a day, the refresh token after 30 days unused. A user + * can revoke it sooner from their PagerDuty profile. + */ + async revoke(): Promise { + this.#creds.clear(); + await this.ctx.storage.deleteAlarm(); + await this.ctx.storage.deleteAll(); + } +} + +type GatekeeperUserImplProps = { userObjectId: string }; + +@validateRpc() +export class GatekeeperUserImpl extends WorkerEntrypoint implements GatekeeperUser { + async describe(): Promise { + const identity = await accountOf(this.ctx).getIdentity(); + return { + displayName: identity.name, + uniqueName: `${identity.email} · ${hostOf(identity)}`, + avatar: { url: identity.avatarUrl ?? "" }, + }; + } + + async getAuthenticatedEmail(): Promise { + return null; + } + + async getSupportedResources(): Promise { + return ALL_RESOURCES; + } + + async getGatekeeperClassFor(url: string): Promise<{ + class: DurableObjectClass>; + resource: SupportedResource; + }> { + const parsed = parseResourceUrl(url); + if (!parsed) { + throw new Error("Not a PagerDuty link this gatekeeper can bind: expected an account " + + "(https://.pagerduty.com), or a team, service, incident or schedule in it."); + } + const identity = await accountOf(this.ctx).getIdentity(); + if (!sameTenant(parsed, identity)) { + throw new Error(`That link is for ${hostOf(parsed)}, but this ` + + `connection is to ${hostOf(identity)}.`); + } + const base = { userObjectId: this.ctx.props.userObjectId, region: parsed.region, subdomain: parsed.subdomain }; + const props: PagerDutyGatekeeperImplProps = parsed.kind === "account" + ? { ...base, resourceKind: "account" } + : { ...base, resourceKind: parsed.kind, id: parsed.id }; + return { class: this.ctx.exports.PagerDutyGatekeeperImpl({ props }), resource: RESOURCES[parsed.kind] }; + } + + async startResourceConfigurator(resourceUrlPattern: string): Promise { + // The five types share one card, so each gets the same configurator (see `groupedWith`). + if (!ALL_RESOURCES.some(resource => resource.urlPattern === resourceUrlPattern)) { + throw new Error(`Unsupported PagerDuty resource configurator type: ${resourceUrlPattern}`); + } + return { + iframeHtml: CONFIGURATOR_HTML, + ui: new RpcStub(new PagerDutyConfiguratorUI(this.ctx.exports, this.ctx.props.userObjectId)), + }; + } + + async revoke(): Promise { + await accountOf(this.ctx).revoke(); + } + + async reconnect(): Promise<{ url: string }> { + const initiationNonce = generateNonce(); + await accountOf(this.ctx).prepareReconnect(initiationNonce); + return { url: `${getBaseUrl(this.env)}/${this.ctx.props.userObjectId}/${initiationNonce}` }; + } + + async commitReconnect(stageId: string): Promise { + await accountOf(this.ctx).commitReconnect(stageId); + } + + async ensureResources(_resourceUrlPatterns: string[]): Promise<{ url?: string }> { + // A connection holds every scope or was refused, so it already covers every resource type. + return {}; + } + + /** Mints a verifier for this account; see `PagerDutyGatekeeperImpl.verifyObserverAccess`. */ + @skipRpcValidation() + async getVerifier(): Promise> { + return this.ctx.exports.PagerDutyVerifier({ props: { userObjectId: this.ctx.props.userObjectId } }); + } +} + +// --------------------------------------------------------------------------- +// Verifier +// +// Every read reports the account and the objects whose visibility governs it; an observer may see +// it only if their own connection, to the same account, can read each one (see +// UserAccount.checkSubresourceAccess). + +@validateRpc() +export class PagerDutyVerifier extends WorkerEntrypoint implements PagerDutyVerifierApi { + async checkSubresourceAccess(subresources: string[]): Promise { + return await accountOf(this.ctx).checkSubresourceAccess(subresources); + } +} + +export type { Identity, PagerDutyVerifierApi, PublicGrant }; diff --git a/packages/gatekeeper-pagerduty/src/text-modules.d.ts b/packages/gatekeeper-pagerduty/src/text-modules.d.ts new file mode 100644 index 0000000000..150d94ca21 --- /dev/null +++ b/packages/gatekeeper-pagerduty/src/text-modules.d.ts @@ -0,0 +1,9 @@ +declare module "*.txt" { + const content: string; + export default content; +} + +declare module "*.svg" { + const content: string; + export default content; +} diff --git a/packages/gatekeeper-pagerduty/src/types.d.ts b/packages/gatekeeper-pagerduty/src/types.d.ts new file mode 100644 index 0000000000..e0f5c33e61 --- /dev/null +++ b/packages/gatekeeper-pagerduty/src/types.d.ts @@ -0,0 +1,618 @@ +import type { RpcStub } from "cloudflare:workers"; + +// Session API exposed to Gadgets/agents by the PagerDuty gatekeeper. +// +// PagerDuty routes alerts from services to the people on call for them. The gatekeeper offers +// five grantable resources, each with its own interface: +// +// - PagerDutyAccount — the whole account (e.g. https://acme.pagerduty.com). +// - PagerDutyTeam — one team: its services, their incidents, its schedules and members. +// - PagerDutyService — one service and its incidents. +// - PagerDutyIncident — one incident. +// - PagerDutySchedule — one on-call schedule and its overrides. +// +// `PagerDutyIncident`, `PagerDutyService`, `PagerDutyTeam` and `PagerDutySchedule` are also what +// the broader interfaces return when you open one of those, so the same shape is used everywhere. +// +// Everything you can see is what the connected PagerDuty user can see: services and incidents +// of private teams they don't belong to are absent. + +// --------------------------------------------------------------------------- +// Sessions +// --------------------------------------------------------------------------- + +/** A whole PagerDuty account: every service, team, schedule and incident the user can see. */ +export interface PagerDutyAccount { + /** The account and the connected user. */ + getMetadata(): Promise; + + /** + * Lists incidents, newest first. By default, only open (triggered or acknowledged) incidents + * created in the last 30 days; see {@link PagerDutyIncidentFilter}. + */ + listIncidents(filter?: PagerDutyIncidentFilter): Promise>; + + /** Opens an incident by its id (e.g. `"Q2W3E4R5T6Y7U8"`) or its number (e.g. `"12345"`). */ + getIncident(idOrNumber: string): Promise; + + /** Lists services, by name. `query` filters by a case-insensitive name substring. */ + listServices(options?: PagerDutyListOptions): Promise>; + + /** Opens a service by its id (e.g. `"PABC123"`). */ + getService(id: string): Promise; + + /** Lists teams, by name. `query` filters by a case-insensitive name substring. */ + listTeams(options?: PagerDutyListOptions): Promise>; + + /** Opens a team by its id. */ + getTeam(id: string): Promise; + + /** Lists on-call schedules, by name. `query` filters by a case-insensitive name substring. */ + listSchedules(options?: PagerDutyListOptions): Promise>; + + /** Opens a schedule by its id. */ + getSchedule(id: string): Promise; + + /** + * Who is on call: one entry per user, escalation level and schedule. By default, right now + * across the whole account; see {@link PagerDutyOnCallFilter}. + */ + listOnCalls(filter?: PagerDutyOnCallFilter): Promise; + + /** + * Looks up users (up to 50). With a `query`, matches a case-insensitive name or email + * substring; with none, returns the first users. Use it to find who to reassign an incident to + * or put on call. + */ + findUsers(query?: string): Promise; + + /** The account's incident priorities, highest first. Empty if the account doesn't use them. */ + listPriorities(): Promise; + + /** + * Have `hook.receiveEvent()` called with each incident event of the kinds in `options.events` + * (by default, all of them) on any incident in the account; see {@link PagerDutyEventKind}. + * + * Every event is delivered, whoever caused it, including the changes a hook makes once they + * are approved; `event.actor.isYou` tells those apart. The hook starts disabled, and nothing is + * delivered until the user enables it. Every call creates a distinct hook, so subscribe once + * for each set of events to watch. Throws if this deployment has not configured PagerDuty + * hooks. + * + * `hook` must be a persistent stub that restores through a Gadget's `[restore]()` method: inside + * the Gadget, create it with `this.ctx.restore(params)`; from `executeCode`, through the Gadget's + * binding, as "Persistent Stubs and `ctx.restore()`" in your instructions describes. The + * Gadget's `[restore]()` receives those `params` for every delivery, so they can tell its + * subscriptions apart. The restored target is a separate object; pass it what it needs from + * `[restore]()`, such as `this`, the Gadget. + * + * @example + * // server.js + * import { DurableObject, RpcTarget, restore } from "cloudflare:workers"; + * export class Gadget extends DurableObject { + * async [restore](params) { + * if (params.type === "triage") return new Triage(); + * throw new TypeError(`Unknown restore type: ${params.type}`); + * } + * } + * class Triage extends RpcTarget { + * async receiveEvent(event) { + * if (event.kind === "triggered" && event.info.urgency === "high") { + * await event.incident.addNote("Triage bot is looking at this."); + * } + * } + * } + * + * // executeCode, where `hook` is a persistent stub for the params `{ type: "triage" }` + * await env.PAGERDUTY_ACCOUNT.subscribe(hook, { events: ["triggered"] }); + */ + subscribe(hook: RpcStub, options?: PagerDutySubscribeOptions): Promise; +} + +/** One PagerDuty team: the services it owns, their incidents, its schedules and its members. */ +export interface PagerDutyTeam { + /** The team's name, description and connected user. */ + getMetadata(): Promise; + + /** Lists the team's members and their team roles. */ + listMembers(): Promise; + + /** Lists the services the team owns, by name. */ + listServices(options?: PagerDutyListOptions): Promise>; + + /** Opens one of the team's services by id. Throws if the team doesn't own it. */ + getService(id: string): Promise; + + /** As `PagerDutyAccount.listIncidents()`, for incidents on the team's services. */ + listIncidents(filter?: PagerDutyIncidentFilter): Promise>; + + /** Opens an incident on one of the team's services. Throws if it belongs to another team. */ + getIncident(idOrNumber: string): Promise; + + /** Lists the team's schedules, by name. */ + listSchedules(options?: PagerDutyListOptions): Promise>; + + /** Opens one of the team's schedules by id. Throws if the team doesn't own it. */ + getSchedule(id: string): Promise; + + /** Who is on call for the team's escalation policies; by default, right now. */ + listOnCalls(filter?: PagerDutyOnCallWindow): Promise; + + /** As `PagerDutyAccount.findUsers()`. */ + findUsers(query?: string): Promise; + + /** As `PagerDutyAccount.listPriorities()`. */ + listPriorities(): Promise; + + /** As `PagerDutyAccount.subscribe()`, for incidents on the team's services. */ + subscribe(hook: RpcStub, options?: PagerDutySubscribeOptions): Promise; +} + +/** One PagerDuty service: something that raises incidents, such as an application or a host. */ +export interface PagerDutyService { + /** The service's details, including who it escalates to. */ + getDetails(): Promise; + + /** As `PagerDutyAccount.listIncidents()`, for this service's incidents. */ + listIncidents(filter?: PagerDutyIncidentFilter): Promise>; + + /** Opens one of this service's incidents. Throws if it belongs to another service. */ + getIncident(idOrNumber: string): Promise; + + /** + * Opens a new incident on this service, which notifies whoever its escalation policy names. + * Returns the new incident. + */ + createIncident(options: PagerDutyCreateIncidentOptions): Promise; + + /** Who is on call for this service's escalation policy; by default, right now. */ + listOnCalls(filter?: PagerDutyOnCallWindow): Promise; + + /** As `PagerDutyAccount.findUsers()`. */ + findUsers(query?: string): Promise; + + /** As `PagerDutyAccount.listPriorities()`. */ + listPriorities(): Promise; + + /** As `PagerDutyAccount.subscribe()`, for this service's incidents. */ + subscribe(hook: RpcStub, options?: PagerDutySubscribeOptions): Promise; +} + +/** + * One PagerDuty incident. + * + * `getDetails`, `getTimeline`, `listNotes` and `listAlerts` read the incident; every other method + * changes it. + */ +export interface PagerDutyIncident { + /** The incident's current details, including the triggering alert's payload. */ + getDetails(): Promise; + + /** The incident's log: triggers, notifications, acknowledgements, escalations, notes, oldest first. */ + getTimeline(options?: PagerDutyPageOptions): Promise>; + + /** The incident's notes, oldest first. */ + listNotes(): Promise; + + /** The alerts grouped into the incident, newest first. */ + listAlerts(options?: PagerDutyPageOptions): Promise>; + + /** Acknowledges the incident as the connected user, which stops escalation while it lasts. */ + acknowledge(): Promise; + + /** Resolves the incident. `resolution`, if given, is added to it as a note. */ + resolve(resolution?: string): Promise; + + /** + * Snoozes an acknowledged incident: it re-triggers after `seconds` (1 to 604800) unless resolved + * first. Throws unless the incident is acknowledged. + */ + snooze(seconds: number): Promise; + + /** Adds a note (plain text, up to 25000 characters) to the incident. */ + addNote(content: string): Promise; + + /** + * Reassigns the incident, which notifies the new assignees: either to users, by email or id + * (`PagerDutyAccount.findUsers()` resolves names), or to an escalation policy by id. Replaces + * the current assignees. + */ + reassign(to: { users: string[] } | { escalationPolicyId: string }): Promise; + + /** Escalates to `level` (1 is the first) of the incident's escalation policy, notifying it. */ + escalate(level: number): Promise; + + /** + * Asks more people to help, notifying each with `message`: users by email or id, and escalation + * policies by id. Throws if the account's plan doesn't include responder requests. + */ + requestResponders(options: PagerDutyResponderRequest): Promise; + + /** Sets the priority by name (see `listPriorities()`), or clears it with `null`. */ + setPriority(priority: string | null): Promise; + + /** Sets the urgency. Raising it to `"high"` may notify the assignees again. */ + setUrgency(urgency: PagerDutyUrgency): Promise; + + /** Replaces the incident's title. */ + setTitle(title: string): Promise; + + /** + * Publishes a status update (plain text), which PagerDuty sends to the incident's subscribers + * and stakeholders. + */ + postStatusUpdate(message: string): Promise; + + /** As `PagerDutyAccount.subscribe()`, for this incident alone. */ + subscribe(hook: RpcStub, options?: PagerDutySubscribeOptions): Promise; +} + +/** One on-call schedule. */ +export interface PagerDutySchedule { + /** The schedule's name, time zone, layers and the escalation policies that use it. */ + getDetails(): Promise; + + /** + * Who is on call when, overrides applied, between `since` and `until` (ISO 8601; by default, + * the next 7 days; at most 90 days apart). + */ + listShifts(window?: PagerDutyTimeWindow): Promise; + + /** The schedule's overrides in a window (by default, the next 30 days). */ + listOverrides(window?: PagerDutyTimeWindow): Promise; + + /** + * Puts `user` (an email or id) on call from `start` to `end` (ISO 8601), over whoever the + * schedule would otherwise have. Returns the new override's id. + */ + createOverride(user: string, start: string, end: string): Promise; + + /** Removes an override by id. One already over can't be removed. */ + deleteOverride(id: string): Promise; +} + +// --------------------------------------------------------------------------- +// Hooks +// --------------------------------------------------------------------------- + +/** Implemented by a gadget to receive PagerDuty incident events; see `PagerDutyAccount.subscribe()`. */ +export interface PagerDutyEventHook { + /** + * Called with each event the hook watches for. `event.incident` can read and change the + * incident, and is released when this call returns. An event may arrive more than once or out + * of order, so key any work on `event.id` to keep it idempotent. One this throws for is retried + * with backoff, eight attempts in all, and disabling the hook ends its retries. An event can + * also be missed, so a gadget that must see every incident should also list them now and then. + */ + receiveEvent(event: PagerDutyEvent): Promise; +} + +/** What `subscribe()` watches for. */ +export type PagerDutySubscribeOptions = { + /** The kinds of event to deliver; by default, all of them. */ + events?: PagerDutyEventKind[]; +}; + +/** + * What happened to the incident: + * - `triggered`: it was opened; + * - `acknowledged` / `unacknowledged`: someone acknowledged it, or the acknowledgement timed out; + * - `resolved` / `reopened`; + * - `reassigned`: its assignees changed; + * - `escalated`: it moved to another level of its escalation policy; + * - `delegated`: it moved to another escalation policy; + * - `priorityUpdated`; + * - `noteAdded`: `event.note` holds the note; + * - `responderAdded`: someone was asked to help; + * - `statusUpdatePublished`: `event.statusUpdate` holds the message. + */ +export type PagerDutyEventKind = + | "triggered" + | "acknowledged" + | "unacknowledged" + | "resolved" + | "reopened" + | "reassigned" + | "escalated" + | "delegated" + | "priorityUpdated" + | "noteAdded" + | "responderAdded" + | "statusUpdatePublished"; + +/** One event delivered to a `PagerDutyEventHook`. */ +export type PagerDutyEvent = { + /** The event's id: the same for each delivery of it. */ + id: string; + kind: PagerDutyEventKind; + /** When it happened (ISO 8601). */ + occurredAt: string; + /** Who caused it, or `null` when PagerDuty did (an escalation timeout, an integration). */ + actor: (PagerDutyUserRef & { isYou: boolean }) | null; + /** The incident as of the event. */ + info: PagerDutyIncidentSummary; + /** The incident, to read and change. */ + incident: PagerDutyIncident; + /** For `noteAdded`: the note. */ + note?: string; + /** For `statusUpdatePublished`: the message. */ + statusUpdate?: string; +}; + +// --------------------------------------------------------------------------- +// Pagination and filters +// --------------------------------------------------------------------------- + +/** + * A pagination cursor. This is an RPC object: call `next()` repeatedly on the same cursor to + * fetch subsequent batches. `next()` returns `null` once exhausted. Dispose the cursor when done. + */ +export interface Cursor { + next(): Promise; +} + +/** Paging for cursor-backed lists. */ +export type PagerDutyPageOptions = { + /** How many items each `next()` returns, 1 to 100. Default 25. */ + pageSize?: number; +}; + +/** Paging plus a name filter. */ +export type PagerDutyListOptions = PagerDutyPageOptions & { + /** Case-insensitive substring of the name. */ + query?: string; +}; + +/** A time window; both ends ISO 8601. */ +export type PagerDutyTimeWindow = { + since?: string; + until?: string; +}; + +/** Filters for listing incidents. All given filters must match. */ +export type PagerDutyIncidentFilter = PagerDutyPageOptions & PagerDutyTimeWindow & { + /** By default `["triggered", "acknowledged"]`. */ + statuses?: PagerDutyIncidentStatus[]; + urgencies?: PagerDutyUrgency[]; + /** Only incidents currently assigned to the connected user. */ + assignedToMe?: boolean; + /** Only incidents on these services (ids). Within a team, only the team's services match. */ + serviceIds?: string[]; + /** + * `since`/`until` bound when incidents were created: by default the last 30 days, and at most + * 6 months apart. Set `allTime` to ignore them. + */ + allTime?: boolean; +}; + +/** Which on-call entries to list. */ +export type PagerDutyOnCallWindow = PagerDutyTimeWindow & { + /** + * Only entries for these escalation levels (1 is the first). By default, every level. + */ + levels?: number[]; +}; + +/** Filters for `PagerDutyAccount.listOnCalls()`. */ +export type PagerDutyOnCallFilter = PagerDutyOnCallWindow & { + /** Only these users (emails or ids). */ + users?: string[]; + scheduleIds?: string[]; + escalationPolicyIds?: string[]; +}; + +/** Options for `PagerDutyService.createIncident()`. */ +export type PagerDutyCreateIncidentOptions = { + title: string; + /** Plain-text details. */ + details?: string; + /** Default: the service's own urgency rule. */ + urgency?: PagerDutyUrgency; + /** A priority name (see `listPriorities()`). */ + priority?: string; + /** Assign to these users (emails or ids) instead of the service's escalation policy. */ + assignTo?: string[]; + /** + * De-duplication key: opening a second incident with the same key on the service while the + * first is open throws instead. + */ + incidentKey?: string; +}; + +/** Options for `PagerDutyIncident.requestResponders()`. */ +export type PagerDutyResponderRequest = { + /** Sent to each responder. */ + message: string; + users?: string[]; + escalationPolicyIds?: string[]; +}; + +// --------------------------------------------------------------------------- +// Data shapes +// --------------------------------------------------------------------------- + +export type PagerDutyIncidentStatus = "triggered" | "acknowledged" | "resolved"; +export type PagerDutyUrgency = "high" | "low"; + +/** A user, as other objects refer to one. */ +export type PagerDutyUserRef = { + id: string; + name: string; +}; + +/** A user, with what is needed to contact or assign them. */ +export type PagerDutyUser = PagerDutyUserRef & { + email: string; + /** e.g. `"Site Reliability Engineer"`, if set. */ + jobTitle: string | null; + timeZone: string; + htmlUrl: string; +}; + +/** A team member and their role on the team. */ +export type PagerDutyTeamMember = PagerDutyUser & { + teamRole: "observer" | "responder" | "manager"; +}; + +/** A reference to another PagerDuty object. */ +export type PagerDutyRef = { + id: string; + name: string; + htmlUrl: string; +}; + +export type PagerDutyPriority = { + id: string; + name: string; + description: string; +}; + +/** The account and who is connected to it. */ +export type PagerDutyAccountMetadata = { + /** e.g. `"acme"` for https://acme.pagerduty.com */ + subdomain: string; + htmlUrl: string; + /** The connected user; incidents can be filtered with `assignedToMe`. */ + you: PagerDutyUser; +}; + +export type PagerDutyTeamSummary = PagerDutyRef & { + description: string | null; +}; + +export type PagerDutyTeamMetadata = PagerDutyTeamSummary & { + /** The connected user. */ + you: PagerDutyUser; +}; + +export type PagerDutyServiceSummary = PagerDutyRef & { + description: string | null; + /** `"critical"` while it has a high-urgency open incident, `"warning"` for low urgency. */ + status: "active" | "warning" | "critical" | "maintenance" | "disabled"; + team: PagerDutyRef | null; +}; + +export type PagerDutyServiceDetails = PagerDutyServiceSummary & { + escalationPolicy: PagerDutyRef; + /** Seconds after which an acknowledged incident re-triggers, or `null` for never. */ + acknowledgementTimeout: number | null; + /** Seconds after which an open incident resolves itself, or `null` for never. */ + autoResolveTimeout: number | null; + createdAt: string; + /** You, as this service's connected user. */ + you: PagerDutyUser; +}; + +/** An incident, as lists return it. */ +export type PagerDutyIncidentSummary = { + id: string; + /** The account-wide incident number, e.g. `12345`. */ + number: number; + title: string; + status: PagerDutyIncidentStatus; + urgency: PagerDutyUrgency; + priority: string | null; + service: PagerDutyRef; + team: PagerDutyRef | null; + assignees: PagerDutyUserRef[]; + createdAt: string; + /** When its status last changed. */ + updatedAt: string; + htmlUrl: string; +}; + +/** An incident's full details. */ +export type PagerDutyIncidentDetails = PagerDutyIncidentSummary & { + /** Plain-text details given when it was opened by hand, if any. */ + details: string | null; + escalationPolicy: PagerDutyRef; + acknowledgers: { user: PagerDutyUserRef; at: string }[]; + /** The deduplication key, if any. */ + incidentKey: string | null; + resolvedAt: string | null; + /** The number of alerts grouped into it. */ + alertCount: number; + /** + * The first alert's payload, as the monitoring tool sent it: typically a summary, severity, + * source and tool-specific custom details. + */ + firstAlert: PagerDutyAlert | null; + /** The incident's conference bridge, if one was set. */ + conferenceUrl: string | null; +}; + +/** One alert grouped into an incident. */ +export type PagerDutyAlert = { + id: string; + summary: string; + status: "triggered" | "resolved"; + severity: "critical" | "error" | "warning" | "info"; + createdAt: string; + /** Where it came from, e.g. a hostname. */ + source: string | null; + /** The monitoring tool's own details, as JSON. */ + details: Record | null; + htmlUrl: string; +}; + +/** One line of an incident's log. */ +export type PagerDutyTimelineEntry = { + id: string; + at: string; + /** e.g. `"trigger"`, `"notify"`, `"acknowledge"`, `"escalate"`, `"assign"`, `"annotate"`, `"resolve"`. */ + kind: string; + /** PagerDuty's one-line description, e.g. "Notified Jane Doe via SMS". */ + summary: string; + actor: PagerDutyUserRef | null; +}; + +export type PagerDutyNote = { + id: string; + content: string; + author: PagerDutyUserRef | null; + createdAt: string; +}; + +/** One user's turn on call. */ +export type PagerDutyOnCall = { + user: PagerDutyUserRef; + /** 1 is the first level notified. */ + level: number; + escalationPolicy: PagerDutyRef; + /** `null` when the user is on the escalation policy directly rather than through a schedule. */ + schedule: PagerDutyRef | null; + /** `null` for open-ended (directly on the escalation policy). */ + start: string | null; + end: string | null; +}; + +export type PagerDutyScheduleSummary = PagerDutyRef & { + description: string | null; + timeZone: string; + teams: PagerDutyRef[]; +}; + +export type PagerDutyScheduleDetails = PagerDutyScheduleSummary & { + escalationPolicies: PagerDutyRef[]; + /** The rotations the schedule combines, highest first. */ + layers: { + name: string; + users: PagerDutyUserRef[]; + /** Seconds per turn, e.g. 604800 for a weekly rotation. */ + turnLength: number; + }[]; +}; + +/** A span of a schedule, overrides applied. */ +export type PagerDutyShift = { + user: PagerDutyUserRef; + start: string; + end: string; +}; + +export type PagerDutyOverride = { + id: string; + user: PagerDutyUserRef; + start: string; + end: string; +}; diff --git a/packages/gatekeeper-pagerduty/src/types.txt b/packages/gatekeeper-pagerduty/src/types.txt new file mode 120000 index 0000000000..8ec3d78219 --- /dev/null +++ b/packages/gatekeeper-pagerduty/src/types.txt @@ -0,0 +1 @@ +types.d.ts \ No newline at end of file diff --git a/packages/gatekeeper-pagerduty/tsconfig.json b/packages/gatekeeper-pagerduty/tsconfig.json new file mode 100644 index 0000000000..2852cab1f7 --- /dev/null +++ b/packages/gatekeeper-pagerduty/tsconfig.json @@ -0,0 +1,19 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "target": "ES2022", + "lib": ["ESNext"], + "module": "ESNext", + "moduleResolution": "bundler", + "jsx": "react", + "jsxFactory": "h", + "types": ["./worker-configuration.d.ts"], + "paths": { + "@gadgets/configurator-ui": ["../configurator-ui/src/index.ts"], + "@gadgets/gatekeeper-kit/*": ["../gatekeeper-kit/src/*"], + "@gadgets/workshop-shared/*": ["../workshop-shared/src/*"] + } + }, + "include": ["src", "__tests__/**/*.ts"], + "exclude": ["dist", "node_modules", ".wrangler"] +} diff --git a/packages/gatekeeper-pagerduty/vite.config.ts b/packages/gatekeeper-pagerduty/vite.config.ts new file mode 100644 index 0000000000..cb22ebe805 --- /dev/null +++ b/packages/gatekeeper-pagerduty/vite.config.ts @@ -0,0 +1,23 @@ +import gatekeeperConfiguratorConfig from "@gadgets/scripts/gatekeeper-configurator"; +import { vitestTask } from "@gadgets/scripts/vitest-task"; + +/** + * Vite+ per-package settings: the shared configurator `build`/`build:configurator` tasks plus a + * two-pass `test` task -- pure logic in Node, RpcTarget/Durable Object behaviour in workerd. The + * passes stay separate commands so one can replay from the task cache when only the other's + * inputs moved. The workerd suite imports the generated configurator modules, so `test` generates + * them first rather than rely on an earlier build having left them. + */ +export default { + ...gatekeeperConfiguratorConfig, + run: { + ...gatekeeperConfiguratorConfig.run, + tasks: { + ...gatekeeperConfiguratorConfig.run.tasks, + test: { + ...vitestTask(["vitest run", "vitest run -c vitest.worker.config.ts"]), + dependsOn: ["build:configurator"], + }, + }, + }, +}; diff --git a/packages/gatekeeper-pagerduty/vitest.config.ts b/packages/gatekeeper-pagerduty/vitest.config.ts new file mode 100644 index 0000000000..cb87810179 --- /dev/null +++ b/packages/gatekeeper-pagerduty/vitest.config.ts @@ -0,0 +1,8 @@ +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + include: ["__tests__/*.test.ts"], + environment: "node", + }, +}); diff --git a/packages/gatekeeper-pagerduty/vitest.worker.config.ts b/packages/gatekeeper-pagerduty/vitest.worker.config.ts new file mode 100644 index 0000000000..bb63c7a023 --- /dev/null +++ b/packages/gatekeeper-pagerduty/vitest.worker.config.ts @@ -0,0 +1,41 @@ +import { cloudflareTest } from "@cloudflare/vitest-plugin"; +import capnwebValidate from "capnweb-validate/vite"; +import { defineConfig } from "vitest/config"; + +/** + * The workerd project: the account Durable Object's connect flow and observer checks, and the + * gatekeeper Durable Object reached through a `TestHooks` facet, as the overseer reaches it. The + * `@validateRpc()` decorators are applied in-memory by the capnweb-validate plugin, since + * `.wrangler/validate` is not built for tests. + */ +export default defineConfig({ + plugins: [ + capnwebValidate(), + cloudflareTest({ + main: "./__tests__/workerd/worker.ts", + miniflare: { + // Kept in step with wrangler.jsonc; a drift here tests a runtime we do not deploy. + compatibilityDate: "2026-09-04", + compatibilityFlags: ["allow_irrevocable_stub_storage", "nodejs_als"], + modulesRules: [ + { type: "Text", include: ["**/*.txt", "**/*.svg"] }, + ], + durableObjects: { + USER_ACCOUNT: { className: "UserAccount", useSQLite: true }, + PAGERDUTY_GATEKEEPER: { className: "PagerDutyGatekeeperImpl", useSQLite: true }, + TEST_HOOKS: { className: "TestHooks", useSQLite: true }, + }, + bindings: { + CLIENT_ID: "test-client-id", + CLIENT_SECRET: "test-client-secret", + BASE_URL: "http://localhost:8787/gatekeeper/pagerduty", + }, + }, + }), + ], + test: { + include: ["__tests__/workerd/*.test.ts"], + // Asserts the pool actually started, rather than trusting a green run to mean workerd. + setupFiles: ["../../scripts/assert-workerd.ts"], + }, +}); diff --git a/packages/gatekeeper-pagerduty/worker-configuration.d.ts b/packages/gatekeeper-pagerduty/worker-configuration.d.ts new file mode 100644 index 0000000000..331ad28c1a --- /dev/null +++ b/packages/gatekeeper-pagerduty/worker-configuration.d.ts @@ -0,0 +1,16265 @@ +/* eslint-disable */ +// Generated by Wrangler by running `wrangler types` (hash: 5ecdc82a76950956d159f7f5cbc78cf0) +// Runtime types generated with workerd@1.20261023.1 2026-09-04 allow_irrevocable_stub_storage,nodejs_als +interface __BaseEnv_Env { +} +declare namespace Cloudflare { + interface GlobalProps { + mainModule: typeof import("./src/pagerduty"); + durableNamespaces: "UserAccount" | "PagerDutyGatekeeperImpl"; + } + interface Env extends __BaseEnv_Env {} +} +interface Env extends __BaseEnv_Env {} +declare module "*.txt" { + const value: string; + export default value; +} +declare module "*.svg" { + const value: string; + export default value; +} +declare module "*.bin" { + const value: ArrayBuffer; + export default value; +} +declare module "*.wasm" { + const value: WebAssembly.Module; + export default value; +} +declare module "*.wasm?module" { + const value: WebAssembly.Module; + export default value; +} +// Begin runtime types +/*! ***************************************************************************** +Copyright (c) Cloudflare. All rights reserved. +Copyright (c) Microsoft Corporation. All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); you may not use +this file except in compliance with the License. You may obtain a copy of the +License at http://www.apache.org/licenses/LICENSE-2.0 +THIS CODE IS PROVIDED ON AN *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION ANY IMPLIED +WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A PARTICULAR PURPOSE, +MERCHANTABLITY OR NON-INFRINGEMENT. +See the Apache Version 2.0 License for specific language governing permissions +and limitations under the License. +***************************************************************************** */ +/* eslint-disable */ +// noinspection JSUnusedGlobalSymbols +declare var onmessage: never; +/** + * The **`DOMException`** interface represents an abnormal event (called an exception) that occurs as a result of calling a method or accessing a property of a web API. This is how error conditions are described in web APIs. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/DOMException) + */ +declare class DOMException extends Error { + constructor(message?: string, name?: string); + /** + * The **`message`** read-only property of the DOMException interface returns a string representing a message or description associated with the given error name. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/DOMException/message) + */ + readonly message: string; + /** + * The **`name`** read-only property of the DOMException interface returns a string that contains one of the strings associated with an error name. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/DOMException/name) + */ + readonly name: string; + /** + * The **`code`** read-only property of the DOMException interface returns one of the legacy error code constants, or 0 if none match. + * @deprecated + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/DOMException/code) + */ + readonly code: number; + static readonly INDEX_SIZE_ERR: number; + static readonly DOMSTRING_SIZE_ERR: number; + static readonly HIERARCHY_REQUEST_ERR: number; + static readonly WRONG_DOCUMENT_ERR: number; + static readonly INVALID_CHARACTER_ERR: number; + static readonly NO_DATA_ALLOWED_ERR: number; + static readonly NO_MODIFICATION_ALLOWED_ERR: number; + static readonly NOT_FOUND_ERR: number; + static readonly NOT_SUPPORTED_ERR: number; + static readonly INUSE_ATTRIBUTE_ERR: number; + static readonly INVALID_STATE_ERR: number; + static readonly SYNTAX_ERR: number; + static readonly INVALID_MODIFICATION_ERR: number; + static readonly NAMESPACE_ERR: number; + static readonly INVALID_ACCESS_ERR: number; + static readonly VALIDATION_ERR: number; + static readonly TYPE_MISMATCH_ERR: number; + static readonly SECURITY_ERR: number; + static readonly NETWORK_ERR: number; + static readonly ABORT_ERR: number; + static readonly URL_MISMATCH_ERR: number; + static readonly QUOTA_EXCEEDED_ERR: number; + static readonly TIMEOUT_ERR: number; + static readonly INVALID_NODE_TYPE_ERR: number; + static readonly DATA_CLONE_ERR: number; + get stack(): any; + set stack(value: any); +} +type WorkerGlobalScopeEventMap = { + fetch: FetchEvent; + scheduled: ScheduledEvent; + queue: QueueEvent; + unhandledrejection: PromiseRejectionEvent; + rejectionhandled: PromiseRejectionEvent; +}; +declare abstract class WorkerGlobalScope extends EventTarget { + EventTarget: typeof EventTarget; +} +/* The **`console`** object provides access to the debugging console (e.g., the Web console in Firefox). * + * The **`console`** object provides access to the debugging console (e.g., the Web console in Firefox). + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/console) + */ +interface Console { + "assert"(condition?: boolean, ...data: any[]): void; + /** + * The **`console.clear()`** static method clears the console if possible. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/clear_static) + */ + clear(): void; + /** + * The **`console.count()`** static method logs the number of times that this particular call to count() has been called. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/count_static) + */ + count(label?: string): void; + /** + * The **`console.countReset()`** static method resets counter used with console.count(). + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/countReset_static) + */ + countReset(label?: string): void; + /** + * The **`console.debug()`** static method outputs a message to the console at the "debug" log level. The message is only displayed to the user if the console is configured to display debug output. In most cases, the log level is configured within the console UI. This log level might correspond to the Debug or Verbose log level. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/debug_static) + */ + debug(...data: any[]): void; + /** + * The **`console.dir()`** static method displays a list of the properties of the specified JavaScript object. In browser consoles, the output is presented as a hierarchical listing with disclosure triangles that let you see the contents of child objects. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/dir_static) + */ + dir(item?: any, options?: any): void; + /** + * The **`console.dirxml()`** static method displays an interactive tree of the descendant elements of the specified XML/HTML element. If it is not possible to display as an element the JavaScript Object view is shown instead. The output is presented as a hierarchical listing of expandable nodes that let you see the contents of child nodes. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/dirxml_static) + */ + dirxml(...data: any[]): void; + /** + * The **`console.error()`** static method outputs a message to the console at the "error" log level. The message is only displayed to the user if the console is configured to display error output. In most cases, the log level is configured within the console UI. The message may be formatted as an error, with red colors and call stack information. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/error_static) + */ + error(...data: any[]): void; + /** + * The **`console.group()`** static method creates a new inline group in the Web console log, causing any subsequent console messages to be indented by an additional level, until console.groupEnd() is called. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/group_static) + */ + group(...data: any[]): void; + /** + * The **`console.groupCollapsed()`** static method creates a new inline group in the console. Unlike console.group(), however, the new group is created collapsed. The user will need to use the disclosure button next to it to expand it, revealing the entries created in the group. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/groupCollapsed_static) + */ + groupCollapsed(...data: any[]): void; + /** + * The **`console.groupEnd()`** static method exits the current inline group in the console. See Using groups in the console in the console documentation for details and examples. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/groupEnd_static) + */ + groupEnd(): void; + /** + * The **`console.info()`** static method outputs a message to the console at the "info" log level. The message is only displayed to the user if the console is configured to display info output. In most cases, the log level is configured within the console UI. The message may receive special formatting, such as a small "i" icon next to it. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/info_static) + */ + info(...data: any[]): void; + /** + * The **`console.log()`** static method outputs a message to the console. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/log_static) + */ + log(...data: any[]): void; + /** + * The **`console.table()`** static method displays tabular data as a table. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/table_static) + */ + table(tabularData?: any, properties?: string[]): void; + /** + * The **`console.time()`** static method starts a timer you can use to track how long an operation takes. You give each timer a unique name, and may have up to 10,000 timers running on a given page. When you call console.timeEnd() with the same name, the browser will output the time, in milliseconds, that elapsed since the timer was started. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/time_static) + */ + time(label?: string): void; + /** + * The **`console.timeEnd()`** static method stops a timer that was previously started by calling console.time(). + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/timeEnd_static) + */ + timeEnd(label?: string): void; + /** + * The **`console.timeLog()`** static method logs the current value of a timer that was previously started by calling console.time(). + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/timeLog_static) + */ + timeLog(label?: string, ...data: any[]): void; + /* The **`console.timeStamp()`** static method adds a single marker to the browser's Performance tool (Firefox bug 1387528, Chrome). This lets you correlate a point in your code with the other events recorded in the timeline, such as layout and paint events. */ + timeStamp(label?: string): void; + /** + * The **`console.trace()`** static method outputs a stack trace to the console. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/trace_static) + */ + trace(...data: any[]): void; + /** + * The **`console.warn()`** static method outputs a warning message to the console at the "warning" log level. The message is only displayed to the user if the console is configured to display warning output. In most cases, the log level is configured within the console UI. The message may receive special formatting, such as yellow colors and a warning icon. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/warn_static) + */ + warn(...data: any[]): void; +} +declare const console: Console; +type BufferSource = ArrayBufferView | ArrayBuffer; +type TypedArray = Int8Array | Uint8Array | Uint8ClampedArray | Int16Array | Uint16Array | Int32Array | Uint32Array | Float32Array | Float64Array | BigInt64Array | BigUint64Array; +declare namespace WebAssembly { + class CompileError extends Error { + constructor(message?: string); + } + class RuntimeError extends Error { + constructor(message?: string); + } + type ValueType = "anyfunc" | "externref" | "f32" | "f64" | "i32" | "i64" | "v128"; + interface GlobalDescriptor { + value: ValueType; + mutable?: boolean; + } + class Global { + constructor(descriptor: GlobalDescriptor, value?: any); + value: any; + valueOf(): any; + } + type ImportValue = ExportValue | number; + type ModuleImports = Record; + type Imports = Record; + type ExportValue = Function | Global | Memory | Table; + type Exports = Record; + class Instance { + constructor(module: Module, imports?: Imports); + readonly exports: Exports; + } + interface MemoryDescriptor { + initial: number; + maximum?: number; + shared?: boolean; + } + class Memory { + constructor(descriptor: MemoryDescriptor); + readonly buffer: ArrayBuffer; + grow(delta: number): number; + } + type ImportExportKind = "function" | "global" | "memory" | "table"; + interface ModuleExportDescriptor { + kind: ImportExportKind; + name: string; + } + interface ModuleImportDescriptor { + kind: ImportExportKind; + module: string; + name: string; + } + abstract class Module { + static customSections(module: Module, sectionName: string): ArrayBuffer[]; + static exports(module: Module): ModuleExportDescriptor[]; + static imports(module: Module): ModuleImportDescriptor[]; + } + type TableKind = "anyfunc" | "externref"; + interface TableDescriptor { + element: TableKind; + initial: number; + maximum?: number; + } + class Table { + constructor(descriptor: TableDescriptor, value?: any); + readonly length: number; + get(index: number): any; + grow(delta: number, value?: any): number; + set(index: number, value?: any): void; + } + function instantiate(module: Module, imports?: Imports): Promise; + function validate(bytes: BufferSource): boolean; +} +/** + * The **`ServiceWorkerGlobalScope`** interface of the Service Worker API represents the global execution context of a service worker. + * Available only in secure contexts. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/ServiceWorkerGlobalScope) + */ +interface ServiceWorkerGlobalScope extends WorkerGlobalScope { + DOMException: typeof DOMException; + WorkerGlobalScope: typeof WorkerGlobalScope; + btoa(data: string): string; + atob(data: string): string; + setTimeout(callback: (...args: any[]) => void, msDelay?: number): number; + setTimeout(callback: (...args: Args) => void, msDelay?: number, ...args: Args): number; + clearTimeout(timeoutId: number | null): void; + setInterval(callback: (...args: any[]) => void, msDelay?: number): number; + setInterval(callback: (...args: Args) => void, msDelay?: number, ...args: Args): number; + clearInterval(timeoutId: number | null): void; + queueMicrotask(task: Function): void; + structuredClone(value: T, options?: StructuredSerializeOptions): T; + reportError(error: any): void; + fetch(input: RequestInfo | URL, init?: RequestInit): Promise; + self: ServiceWorkerGlobalScope; + crypto: Crypto; + caches: CacheStorage; + scheduler: Scheduler; + performance: Performance; + Cloudflare: Cloudflare; + readonly origin: string; + Event: typeof Event; + ExtendableEvent: typeof ExtendableEvent; + CustomEvent: typeof CustomEvent; + PromiseRejectionEvent: typeof PromiseRejectionEvent; + FetchEvent: typeof FetchEvent; + TailEvent: typeof TailEvent; + TraceEvent: typeof TailEvent; + ScheduledEvent: typeof ScheduledEvent; + MessageEvent: typeof MessageEvent; + CloseEvent: typeof CloseEvent; + ReadableStreamDefaultReader: typeof ReadableStreamDefaultReader; + ReadableStreamBYOBReader: typeof ReadableStreamBYOBReader; + ReadableStream: typeof ReadableStream; + WritableStream: typeof WritableStream; + WritableStreamDefaultWriter: typeof WritableStreamDefaultWriter; + TransformStream: typeof TransformStream; + ByteLengthQueuingStrategy: typeof ByteLengthQueuingStrategy; + CountQueuingStrategy: typeof CountQueuingStrategy; + ErrorEvent: typeof ErrorEvent; + MessageChannel: typeof MessageChannel; + MessagePort: typeof MessagePort; + EventSource: typeof EventSource; + ReadableStreamBYOBRequest: typeof ReadableStreamBYOBRequest; + ReadableStreamDefaultController: typeof ReadableStreamDefaultController; + ReadableByteStreamController: typeof ReadableByteStreamController; + WritableStreamDefaultController: typeof WritableStreamDefaultController; + TransformStreamDefaultController: typeof TransformStreamDefaultController; + CompressionStream: typeof CompressionStream; + DecompressionStream: typeof DecompressionStream; + TextEncoderStream: typeof TextEncoderStream; + TextDecoderStream: typeof TextDecoderStream; + Headers: typeof Headers; + Body: typeof Body; + Request: typeof Request; + Response: typeof Response; + WebSocket: typeof WebSocket; + WebSocketPair: typeof WebSocketPair; + WebSocketRequestResponsePair: typeof WebSocketRequestResponsePair; + AbortController: typeof AbortController; + AbortSignal: typeof AbortSignal; + TextDecoder: typeof TextDecoder; + TextEncoder: typeof TextEncoder; + navigator: Navigator; + Navigator: typeof Navigator; + URL: typeof URL; + URLSearchParams: typeof URLSearchParams; + URLPattern: typeof URLPattern; + Blob: typeof Blob; + File: typeof File; + FormData: typeof FormData; + Crypto: typeof Crypto; + SubtleCrypto: typeof SubtleCrypto; + CryptoKey: typeof CryptoKey; + CacheStorage: typeof CacheStorage; + Cache: typeof Cache; + FixedLengthStream: typeof FixedLengthStream; + IdentityTransformStream: typeof IdentityTransformStream; + HTMLRewriter: typeof HTMLRewriter; +} +declare function addEventListener(type: Type, handler: EventListenerOrEventListenerObject, options?: EventTargetAddEventListenerOptions | boolean): void; +declare function removeEventListener(type: Type, handler: EventListenerOrEventListenerObject, options?: EventTargetEventListenerOptions | boolean): void; +/** + * The **`dispatchEvent()`** method of the EventTarget sends an Event to the object, (synchronously) invoking the affected event listeners in the appropriate order. The normal event processing rules (including the capturing and optional bubbling phase) also apply to events dispatched manually with dispatchEvent(). + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/EventTarget/dispatchEvent) + */ +declare function dispatchEvent(event: WorkerGlobalScopeEventMap[keyof WorkerGlobalScopeEventMap]): boolean; +/* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Window/btoa) */ +declare function btoa(data: string): string; +/* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Window/atob) */ +declare function atob(data: string): string; +/* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Window/setTimeout) */ +declare function setTimeout(callback: (...args: any[]) => void, msDelay?: number): number; +/* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Window/setTimeout) */ +declare function setTimeout(callback: (...args: Args) => void, msDelay?: number, ...args: Args): number; +/* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Window/clearTimeout) */ +declare function clearTimeout(timeoutId: number | null): void; +/* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Window/setInterval) */ +declare function setInterval(callback: (...args: any[]) => void, msDelay?: number): number; +/* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Window/setInterval) */ +declare function setInterval(callback: (...args: Args) => void, msDelay?: number, ...args: Args): number; +/* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Window/clearInterval) */ +declare function clearInterval(timeoutId: number | null): void; +/* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Window/queueMicrotask) */ +declare function queueMicrotask(task: Function): void; +/* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Window/structuredClone) */ +declare function structuredClone(value: T, options?: StructuredSerializeOptions): T; +/* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Window/reportError) */ +declare function reportError(error: any): void; +/* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Window/fetch) */ +declare function fetch(input: RequestInfo | URL, init?: RequestInit): Promise; +declare const self: ServiceWorkerGlobalScope; +/** +* The Web Crypto API provides a set of low-level functions for common cryptographic tasks. +* The Workers runtime implements the full surface of this API, but with some differences in +* the [supported algorithms](https://developers.cloudflare.com/workers/runtime-apis/web-crypto/#supported-algorithms) +* compared to those implemented in most browsers. +* +* [Cloudflare Docs Reference](https://developers.cloudflare.com/workers/runtime-apis/web-crypto/) +*/ +declare const crypto: Crypto; +/** +* The Cache API allows fine grained control of reading and writing from the Cloudflare global network cache. +* +* [Cloudflare Docs Reference](https://developers.cloudflare.com/workers/runtime-apis/cache/) +*/ +declare const caches: CacheStorage; +/* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Window/scheduler) */ +declare const scheduler: Scheduler; +/** +* The Workers runtime supports a subset of the Performance API, used to measure timing and performance, +* as well as timing of subrequests and other operations. +* +* [Cloudflare Docs Reference](https://developers.cloudflare.com/workers/runtime-apis/performance/) +*/ +declare const performance: Performance; +declare const Cloudflare: Cloudflare; +declare const origin: string; +declare const navigator: Navigator; +interface TestController { +} +interface ExecutionContext { + waitUntil(promise: Promise): void; + passThroughOnException(): void; + readonly exports: Cloudflare.Exports; + readonly props: Props; + restore(params: any): Promise; + cache?: CacheContext; + readonly access?: CloudflareAccessContext; + tracing: Tracing; + abort(reason?: any): void; +} +type ExportedHandlerFetchHandler = (request: Request>, env: Env, ctx: ExecutionContext) => Response | Promise; +type ExportedHandlerConnectHandler = (socket: Socket, env: Env, ctx: ExecutionContext) => void | Promise; +type ExportedHandlerTailHandler = (events: TraceItem[], env: Env, ctx: ExecutionContext) => void | Promise; +type ExportedHandlerTraceHandler = (traces: TraceItem[], env: Env, ctx: ExecutionContext) => void | Promise; +type ExportedHandlerTailStreamHandler = (event: TailStream.TailEvent, env: Env, ctx: ExecutionContext) => TailStream.TailEventHandlerType | undefined | Promise; +type ExportedHandlerScheduledHandler = (controller: ScheduledController, env: Env, ctx: ExecutionContext) => void | Promise; +type ExportedHandlerQueueHandler = (batch: MessageBatch, env: Env, ctx: ExecutionContext) => void | Promise; +type ExportedHandlerTestHandler = (controller: TestController, env: Env, ctx: ExecutionContext) => void | Promise; +interface ExportedHandler { + fetch?: ExportedHandlerFetchHandler; + connect?: ExportedHandlerConnectHandler; + tail?: ExportedHandlerTailHandler; + trace?: ExportedHandlerTraceHandler; + tailStream?: ExportedHandlerTailStreamHandler; + scheduled?: ExportedHandlerScheduledHandler; + test?: ExportedHandlerTestHandler; + email?: EmailExportedHandler; + queue?: ExportedHandlerQueueHandler; +} +interface StructuredSerializeOptions { + transfer?: any[]; +} +declare abstract class Navigator { + sendBeacon(url: string, body?: BodyInit): boolean; + readonly userAgent: string; + readonly hardwareConcurrency: number; + readonly platform: string; + readonly language: string; + readonly languages: string[]; +} +interface AlarmInvocationInfo { + readonly isRetry: boolean; + readonly retryCount: number; + readonly scheduledTime: number; +} +interface Cloudflare { + readonly compatibilityFlags: Record; +} +interface CachePurgeError { + code: number; + message: string; +} +interface CachePurgeResult { + success: boolean; + errors: CachePurgeError[]; +} +interface CachePurgeOptions { + tags?: string[]; + pathPrefixes?: string[]; + purgeEverything?: boolean; +} +interface CacheContext { + purge(options: CachePurgeOptions): Promise; + invalidate(options: CachePurgeOptions): Promise; +} +interface CloudflareAccessContext { + readonly aud: string; + getIdentity(): Promise; +} +declare abstract class ColoLocalActorNamespace { + get(actorId: string): Fetcher; +} +interface DurableObject { + fetch(request: Request): Response | Promise; + connect?(socket: Socket): void | Promise; + alarm?(alarmInfo?: AlarmInvocationInfo): void | Promise; + webSocketMessage?(ws: WebSocket, message: string | ArrayBuffer): void | Promise; + webSocketClose?(ws: WebSocket, code: number, reason: string, wasClean: boolean): void | Promise; + webSocketError?(ws: WebSocket, error: unknown): void | Promise; +} +type DurableObjectStub = Fetcher & { + readonly id: DurableObjectId; + readonly name?: string; +}; +interface DurableObjectId { + toString(): string; + equals(other: DurableObjectId): boolean; + readonly name?: string; + readonly jurisdiction?: string; +} +declare abstract class DurableObjectNamespace { + newUniqueId(options?: DurableObjectNamespaceNewUniqueIdOptions): DurableObjectId; + idFromName(name: string): DurableObjectId; + idFromString(id: string): DurableObjectId; + get(id: DurableObjectId, options?: DurableObjectNamespaceGetDurableObjectOptions): DurableObjectStub; + getByName(name: string, options?: DurableObjectNamespaceGetDurableObjectOptions): DurableObjectStub; + jurisdiction(jurisdiction: DurableObjectJurisdiction): DurableObjectNamespace; +} +type DurableObjectJurisdiction = "eu" | "fedramp" | "fedramp-high" | "us"; +interface DurableObjectNamespaceNewUniqueIdOptions { + jurisdiction?: DurableObjectJurisdiction; +} +type DurableObjectLocationHint = "wnam" | "enam" | "sam" | "weur" | "eeur" | "apac" | "apac-ne" | "apac-se" | "oc" | "afr" | "me"; +type DurableObjectRoutingMode = "primary-only"; +interface DurableObjectNamespaceGetDurableObjectOptions { + locationHint?: DurableObjectLocationHint; + routingMode?: DurableObjectRoutingMode; +} +interface DurableObjectClass<_T extends Rpc.DurableObjectBranded | undefined = undefined> { +} +interface DurableObjectState { + waitUntil(promise: Promise): void; + readonly exports: Cloudflare.Exports; + readonly props: Props; + restore(params: any): Promise; + readonly id: DurableObjectId; + readonly storage: DurableObjectStorage; + container?: Container; + facets: DurableObjectFacets; + blockConcurrencyWhile(callback: () => Promise): Promise; + acceptWebSocket(ws: WebSocket, tags?: string[]): void; + getWebSockets(tag?: string): WebSocket[]; + setWebSocketAutoResponse(maybeReqResp?: WebSocketRequestResponsePair): void; + getWebSocketAutoResponse(): WebSocketRequestResponsePair | null; + getWebSocketAutoResponseTimestamp(ws: WebSocket): Date | null; + setHibernatableWebSocketEventTimeout(timeoutMs?: number): void; + getHibernatableWebSocketEventTimeout(): number | null; + getTags(ws: WebSocket): string[]; + abort(reason?: string, options?: DurableObjectAbortOptions): void; +} +interface DurableObjectTransaction { + get(key: string, options?: DurableObjectGetOptions): Promise; + get(keys: string[], options?: DurableObjectGetOptions): Promise>; + 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 DurableObjectAbortOptions { + retryAlarm?: boolean; +} +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; + clone(src: string, dst: 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. It is set when the event is constructed and is the name commonly used to refer to the specific event, such as click, load, or error. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Event/type) + */ + get type(): string; + /** + * The **`eventPhase`** read-only property of the Event interface indicates which phase of the event flow is currently being evaluated. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Event/eventPhase) + */ + get eventPhase(): number; + /** + * The read-only **`composed`** property of the Event interface returns a boolean value which indicates whether 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 Event interface is a reference to the object onto which the event was dispatched. It is different from Event.currentTarget when the event handler is called during the bubbling or capturing phase of the event. + * + * [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. Use Event.target instead. + * @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 Event interface is a boolean value that is true 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 EventTarget.dispatchEvent(). 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. Use Event.stopPropagation() instead. Setting its value to true before returning from an event handler prevents propagation of the event. In later implementations, setting this to false does nothing. See Browser compatibility for details. + * @deprecated + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Event/cancelBubble) + */ + get cancelBubble(): boolean; + /** + * The **`cancelBubble`** property of the Event interface is deprecated. Use Event.stopPropagation() instead. Setting its value to true before returning from an event handler prevents propagation of the event. In later implementations, setting this to false does nothing. See Browser compatibility for details. + * @deprecated + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Event/cancelBubble) + */ + set cancelBubble(value: boolean); + /** + * The **`stopImmediatePropagation()`** method of the Event interface prevents other listeners of the same event from being called. + * + * [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 the event is being explicitly handled, so its default action, such as page scrolling, link navigation, or pasting text, should not be taken. + * + * [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. It does not, however, prevent any default behaviors from occurring; for instance, clicks on links are still processed. If you want to stop those behaviors, see the preventDefault() method. It also does not prevent propagation to other event-handlers of the current element. If you want to stop those, see stopImmediatePropagation(). + * + * [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. This does not include nodes in shadow trees if the shadow root was created with its ShadowRoot.mode closed. + * + * [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. In other words, any target of events implements the three methods associated with this interface. + * + * [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. The event listener to be removed is identified using a combination of the event type, the event listener function itself, and various optional options that may affect the matching process; see Matching event listeners for removal. + * + * [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. The normal event processing rules (including the capturing and optional bubbling phase) also apply to events dispatched manually with dispatchEvent(). + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/EventTarget/dispatchEvent) + */ + 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. This is able to abort fetch requests, the consumption of any response bodies, or streams. + * + * [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 abort 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. The returned abort signal is aborted when any of the input iterable abort signals are aborted. The abort reason will be set to the reason of the first signal that is aborted. If any of the given abort signals are already aborted then so will be the returned 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 reason if the signal has been aborted; otherwise it does nothing. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/AbortSignal/throwIfAborted) + */ + throwIfAborted(): void; +} +/** + * The **`Scheduler`** interface of the Prioritized Task Scheduling API provides methods for scheduling prioritized tasks. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Scheduler) + */ +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. This ensures that any functional events (like FetchEvent) are not dispatched until it upgrades database schemas and deletes the outdated cache entries. + * + * [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. It can also be used to detect whether that work was successful. In service workers, waitUntil() tells the browser that work is ongoing until the promise settles, and it shouldn't terminate the service worker if it wants that work to complete. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/ExtendableEvent/waitUntil) + */ + waitUntil(promise: Promise): void; +} +/** + * The **`CustomEvent`** interface can be used to attach custom data to an event generated by an application. + * + * [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 Blob interface returns a Promise that resolves with a 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. For security reasons, the path is excluded from this property. + * + * [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). Files without a known last modified date return the current date. + * + * [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 CacheStorage interface returns a Promise that resolves to 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 SubtleCrypto which can then be used to perform low-level 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. The array given as the parameter is filled with random numbers (random in its cryptographic meaning). + * + * [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. It takes as arguments a key to decrypt with, some optional extra parameters, and the data to decrypt (also known as "ciphertext"). It returns a Promise which will be fulfilled with the decrypted data (also known as "plaintext"). + * + * [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. A digest is a short fixed-length value derived from some variable-length input. Cryptographic digests should exhibit collision-resistance, meaning that it's hard to come up with two different inputs that have the same digest value. + * + * [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 SubtleCrypto interface can be used to derive an array of bits from a base 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. This means that it exports the key in an external, portable format, then encrypts the exported key. Wrapping a key helps protect it in untrusted environments, such as inside an otherwise unprotected data store or in transmission over an unprotected network. + * + * [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. This means that it takes as its input a key that has been exported and then encrypted (also called "wrapped"). It decrypts the key and then imports it, returning a CryptoKey object that can be used in the Web Crypto API. + * + * [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 generateKey(), deriveKey(), importKey(), or 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. It can have the following values: + * + * [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; + pub?: string; + priv?: 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; + context?: ArrayBuffer | ArrayBufferView; +} +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, options?: DigestStreamOptions); + readonly digest: Promise; + get bytesWritten(): number | bigint; +} +interface DigestStreamOptions { + toWellFormed?: boolean; +} +/** + * The **`TextDecoder`** interface represents a decoder for a specific text encoding, such as UTF-8, ISO-8859-2, or GBK. A decoder takes an array of bytes as input and returns a JavaScript string. + * + * [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 enables you to encode a JavaScript string using UTF-8. + * + * [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 Uint8Array containing the string encoded using UTF-8. + * + * [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 an object indicating the progress of the encoding. This is potentially more performant than the encode() method — especially when the target buffer is a view into a Wasm heap. + * + * [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 { + bubbles?: boolean; + cancelable?: boolean; + composed?: boolean; + 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 MessageEvent interface represents the data sent by the message emitter. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/MessageEvent/data) + */ + readonly data: any; + /** + * The **`origin`** read-only property of the MessageEvent interface is a string representing 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 MessageEvent interface is a string representing a 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 MessageEvent interface is a MessageEventSource (which can be a WindowProxy, MessagePort, or ServiceWorker object) representing the message emitter. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/MessageEvent/source) + */ + readonly source: MessagePort | null; + /** + * The **`ports`** read-only property of the MessageEvent interface is an array of MessagePort objects 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 { + bubbles?: boolean; + cancelable?: boolean; + composed?: boolean; + data?: any; + origin?: string; + lastEventId?: string; + source?: (MessagePort | null); + ports?: MessagePort[]; +} +/** + * The **`PromiseRejectionEvent`** interface represents events which are sent to the global script context when JavaScript Promises are rejected. These events are particularly useful for telemetry and debugging purposes. + * + * [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 Promise which was rejected. You can examine the event's PromiseRejectionEvent.reason property to learn why the promise was 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(). This in theory provides information about why the promise was rejected. + * + * [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 fetch(), XMLHttpRequest.send() or navigator.sendBeacon() methods. It uses the same format a form would use if the encoding type were set to "multipart/form-data". + * + * [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. If you expect multiple values and want all of them, use the getAll() method instead. + * + * [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; + entries(): IterableIterator<[ + key: string, + value: File | string + ]>; + keys(): IterableIterator; + 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 service worker global scope. It contains information about the fetch, including the request and how the receiver will treat the response. It provides the event.respondWith() method, which allows us to provide a response to this fetch. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/FetchEvent) + */ +declare abstract class FetchEvent extends ExtendableEvent { + /** + * The **`request`** read-only property of the FetchEvent interface returns the Request that triggered the event handler. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/FetchEvent/request) + */ + readonly request: Request; + /** + * The **`respondWith()`** method of FetchEvent prevents the browser's default fetch handling, and 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. These actions include retrieving, setting, adding to, and removing headers from the list of the request's 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. If the requested header doesn't exist in the Headers object, it returns null. + * + * [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. This allows Headers objects to handle having multiple Set-Cookie headers, which wasn't possible prior to its implementation. + * + * [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; + entries(): IterableIterator<[ + key: string, + value: string + ]>; + keys(): IterableIterator; + 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 Response interface contains the Headers object associated 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. The value of the url property will be the final URL obtained after any redirects. + * + * [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. The type determines whether scripts are able to access the response body and headers. + * + * [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 Request interface contains the request's method (GET, POST, etc.) + * + * [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 Request interface contains the Headers object associated 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. It controls how the request will interact with the browser's HTTP cache. + * + * [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. The Fetch API offers a concrete instance of a ReadableStream through the body property of a Response object. + * + * [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. While the stream is locked, no other reader can be acquired until this one is released. + * + * [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. While the stream is locked, no other reader can be acquired until this one is released. + * + * [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 ReadableStream interface tees the current readable stream, returning a 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. The Fetch API offers a concrete instance of a ReadableStream through the body property of a Response object. + * + * [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. It is used for efficient copying from underlying sources where the data is delivered as an "anonymous" sequence of bytes, such as files. + * + * [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. A request for data will be satisfied from the stream's internal queues if there is any data present. If the stream queues are empty, the request may be supplied as a zero-copy transfer from the underlying byte source. + * + * [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. After the lock is released, the reader is no longer active. + * + * [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. Default controllers are for streams that are not byte streams. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/ReadableStreamDefaultController) + */ +declare abstract class ReadableStreamDefaultController { + /** + * The **`desiredSize`** read-only property of the ReadableStreamDefaultController interface returns the desired size 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 ReadableStreamDefaultController interface enqueues a given chunk in the associated stream. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/ReadableStreamDefaultController/enqueue) + */ + enqueue(chunk?: R): void; + /** + * The **`error()`** method of the ReadableStreamDefaultController interface causes any future interactions 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. It allows control of the state and internal queue of a ReadableStream with an underlying byte source, and enables efficient zero-copy transfer of data from the underlying source to a consumer when the stream's internal queue is empty. + * + * [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 transferred 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. When constructing a WritableStream, the underlying sink is given a corresponding WritableStreamDefaultController instance to manipulate. + * + * [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 WritableStreamDefaultController interface causes any future interactions 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. Any further interactions with it will fail with the given error message, and any chunks in the queue will be discarded. + * + * [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. This object comes with built-in backpressure and queuing. + * + * [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. All chunks written before this method is called are sent before the returned promise is fulfilled. + * + * [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. While the stream is locked, no other writer can be acquired until this one is released. + * + * [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 WritableStreamDefaultWriter interface returns a Promise that fulfills if the stream becomes closed, or rejects if 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 WritableStreamDefaultWriter interface returns a Promise 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 WritableStreamDefaultWriter interface returns the desired size required 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 WritableStreamDefaultWriter 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/WritableStreamDefaultWriter/abort) + */ + abort(reason?: any): Promise; + /** + * The **`close()`** method of the WritableStreamDefaultWriter interface closes the associated writable stream. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/WritableStreamDefaultWriter/close) + */ + close(): Promise; + /** + * The **`write()`** method of the WritableStreamDefaultWriter interface writes a passed chunk of data to a WritableStream and its underlying sink, then returns a Promise that resolves to indicate the success or failure of the write operation. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/WritableStreamDefaultWriter/write) + */ + write(chunk?: W): Promise; + /** + * The **`releaseLock()`** method of the WritableStreamDefaultWriter interface releases the writer's lock on the corresponding stream. After the lock is released, the writer is no longer active. If the associated stream is errored when the lock is released, the writer will appear errored in the same way from now on; otherwise, the writer will appear closed. + * + * [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. This stream emits the transformed output data. + * + * [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. This stream accepts input data that will be transformed and emitted to the readable stream. + * + * [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 compresses a stream of data. It implements the same shape as a TransformStream, allowing it to be used in ReadableStream.pipeThrough() and similar methods. + * + * [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 decompresses a stream of data. It implements the same shape as a TransformStream, allowing it to be used in ReadableStream.pipeThrough() and similar methods. + * + * [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. It is the streaming equivalent of TextEncoder. It implements the same shape as a TransformStream, allowing it to be used in ReadableStream.pipeThrough() and similar methods. + * + * [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. It is the streaming equivalent of TextDecoder. It implements the same shape as a TransformStream, allowing it to be used in ReadableStream.pipeThrough() and similar methods. + * + * [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; + readonly errorInfo?: (TraceLogErrorInfo | null)[]; +} +interface TraceLogErrorInfo { + name: string; + message: string; + stack?: string; +} +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 URLs. It works by providing properties which allow you to easily read and modify the components of a 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. If the URL does not have a username, this property contains an empty string, "". + * + * [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. If the URL does not have a username, this property contains an empty string, "". + * + * [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. If the URL does not have a password, this property contains an empty string, "". + * + * [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. If the URL does not have a password, this property contains an empty string, "". + * + * [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 hostname, and then, if the port of the URL is nonempty, a ":", followed by the port of the URL. If the URL does not have a hostname, this property contains an empty string, "". + * + * [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 hostname, and then, if the port of the URL is nonempty, a ":", followed by the port of the URL. If the URL does not have a hostname, this property contains an empty string, "". + * + * [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. If the URL does not have a hostname, this property contains an empty string, "". IPv4 and IPv6 addresses are normalized, such as stripping leading zeros, and domain names are converted to IDN. + * + * [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. If the URL does not have a hostname, this property contains an empty string, "". IPv4 and IPv6 addresses are normalized, such as stripping leading zeros, and domain names are converted to IDN. + * + * [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. If the port is the default for the protocol (80 for ws: and http:, 443 for wss: and https:, and 21 for ftp:), this property contains an empty string, "". + * + * [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. If the port is the default for the protocol (80 for ws: and http:, 443 for wss: and https:, and 21 for ftp:), this property contains an empty string, "". + * + * [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. It is a string constructed from a list of path segments, each of which is prefixed by a / character. + * + * [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. It is a string constructed from a list of path segments, each of which is prefixed by a / character. + * + * [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. If the URL does not have a search query, this property contains an empty string, "". + * + * [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. If the URL does not have a search query, this property contains an empty string, "". + * + * [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. If the URL does not have a fragment identifier, this property contains an empty string, "". + * + * [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. If the URL does not have a fragment identifier, this property contains an empty string, "". + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/URL/hash) + */ + set hash(value: string); + /** + * The **`searchParams`** read-only property of the URL interface returns a URLSearchParams object allowing access to the 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 URL.toString(). + * + * [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 blob URL pointing to 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 URL.createObjectURL(). + * + * [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. If there were several matching values, this method deletes the others. If the search parameter doesn't exist, this method creates it. + * + * [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. Key/value pairs are sorted by the values of the UTF-16 code units of the keys. This method uses a stable sorting algorithm (i.e., the relative order between key/value pairs with equal keys will be preserved). + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/URLSearchParams/sort) + */ + sort(): void; + entries(): IterableIterator<[ + key: string, + value: string + ]>; + keys(): IterableIterator; + 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 + ]>; +} +/** + * The **`URLPattern`** interface of the URL Pattern API matches URLs or parts of URLs against a pattern. The pattern can contain capturing groups that extract parts of the matched URL. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/URLPattern) + */ +declare class URLPattern { + constructor(input?: (string | URLPatternInit), baseURL?: (string | URLPatternOptions), patternOptions?: URLPatternOptions); + /** + * The **`protocol`** read-only property of the URLPattern interface is a string containing the pattern used to match the protocol part of a URL. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/URLPattern/protocol) + */ + get protocol(): string; + /** + * The **`username`** read-only property of the URLPattern interface is a string containing the pattern used to match the username part of a URL. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/URLPattern/username) + */ + get username(): string; + /** + * The **`password`** read-only property of the URLPattern interface is a string containing the pattern used to match the password part of a URL. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/URLPattern/password) + */ + get password(): string; + /** + * The **`hostname`** read-only property of the URLPattern interface is a string containing the pattern used to match the hostname part of a URL. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/URLPattern/hostname) + */ + get hostname(): string; + /** + * The **`port`** read-only property of the URLPattern interface is a string containing the pattern used to match the port part of a URL. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/URLPattern/port) + */ + get port(): string; + /** + * The **`pathname`** read-only property of the URLPattern interface is a string containing the pattern used to match the pathname part of a URL. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/URLPattern/pathname) + */ + get pathname(): string; + /** + * The **`search`** read-only property of the URLPattern interface is a string containing the pattern used to match the search part of a URL. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/URLPattern/search) + */ + get search(): string; + /** + * The **`hash`** read-only property of the URLPattern interface is a string containing the pattern used to match the fragment part of a URL. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/URLPattern/hash) + */ + get hash(): string; + /** + * The **`hasRegExpGroups`** read-only property of the URLPattern interface is a boolean indicating whether or not any of the URLPattern components contain regular expression capturing groups. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/URLPattern/hasRegExpGroups) + */ + get hasRegExpGroups(): boolean; + /** + * The **`test()`** method of the URLPattern interface takes a URL string or object of URL parts, and returns a boolean indicating if the given input matches the current pattern. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/URLPattern/test) + */ + test(input?: (string | URLPatternInit), baseURL?: string): boolean; + /** + * The **`exec()`** method of the URLPattern interface takes a URL or object of URL parts, and returns either an object containing the results of matching the URL to the pattern, or null if the URL does not match the pattern. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/URLPattern/exec) + */ + 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. This is delivered to the listener indicated by the WebSocket object's onclose attribute. + * + * [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 { + bubbles?: boolean; + cancelable?: boolean; + composed?: boolean; + 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. If the data can't be sent (for example, because it needs to be buffered but the buffer is full), the socket is closed automatically. The browser will throw an exception if you call send() when the connection is in the CONNECTING state. If you call send() when the connection is in the CLOSING or CLOSED states, the browser will silently discard 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 WebSocket connection or connection attempt, if any. If the connection is 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. This is currently only the empty string or a list of extensions as negotiated by the connection. + * + * [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(): IteratorResult; + 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 EventSource.readyState attribute to 2 (closed). + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/EventSource/close) + */ + close(): void; + /** + * The **`url`** read-only property of the EventSource interface returns a string representing 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 EventSource interface returns a boolean value indicating whether 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 EventSource interface returns a number representing the state 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 ExecOutput { + readonly stdout: ArrayBuffer; + readonly stderr: ArrayBuffer; + readonly exitCode: number; +} +interface ContainerExecOptions { + cwd?: string; + env?: Record; + user?: string; + signal?: AbortSignal; + pty?: boolean | ContainerExecPtyOptions; + stdin?: ReadableStream | "pipe"; + stdout?: "pipe" | "ignore"; + stderr?: "pipe" | "ignore" | "combined"; +} +interface ContainerExecPtyOptions { + cols?: number; + rows?: number; +} +interface ExecProcess { + readonly stdin: WritableStream | null; + readonly stdout: ReadableStream | null; + readonly stderr: ReadableStream | null; + readonly pid: number; + readonly isPty: boolean; + readonly exitCode: Promise; + output(): Promise; + kill(signal?: number): void; + resize(cols: number, rows: number): void; +} +interface Container { + get running(): boolean; + get images(): Record; + 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; + snapshotContainer(options?: ContainerSnapshotOptions): Promise; + interceptOutboundHttps(addr: string, binding: Fetcher): Promise; + exec(cmd: string[], options?: ContainerExecOptions): Promise; + inspect(): Promise; +} +interface ContainerDirectorySnapshot { + id: string; + size: number; + dir: string; + name?: string; +} +type ContainerDirectorySnapshotRestoreParams = { + snapshot: ContainerDirectorySnapshot; + mountPoint?: string; +} | { + snapshot?: undefined; + mountPoint: string; +}; +interface ContainerSnapshot { + id: string; + size: number; + name?: string; +} +interface ContainerSnapshotRestoreParams { + id: string; +} +interface ContainerSnapshotOptions { + name?: string; +} +type ContainerStartupOptions = { + entrypoint?: string[]; + enableInternet: boolean; + env?: Record; + instance?: "lite" | "basic" | "standard-1" | "standard-2" | "standard-3" | "standard-4" | ContainerStartResources; + labels?: Record; + directorySnapshots?: ContainerDirectorySnapshotRestoreParams[]; +} & ({ + image: string; + containerSnapshot?: never; +} | { + image?: never; + containerSnapshot?: ContainerSnapshotRestoreParams; +}); +interface ContainerInfo { + labels: Record; + image: string; +} +interface ContainerStartResources { + vcpu: number; + memoryMib: number; + diskMb: number; +} +/** + * 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 MessagePort interface sends a message from the port, and optionally, 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. This stops the flow of messages to that port. + * + * [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. This method is only needed when using EventTarget.addEventListener; it is implied when using onmessage. + * + * [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 MessageChannel interface returns the first port of the message channel — 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 MessageChannel interface returns the second port of the message channel — 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 new (...args: any[]) => CloudflareWorkersModule.WorkflowEntrypoint ? Workflow : 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 | ArrayBufferView | WebAssembly.Module; +} +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 serializer; 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; + startActiveSpan(name: string, callback: (span: Span, ...args: A) => T, ...args: A): T; + startSpan(name: string): Span; + getActiveSpan(): Span | undefined; + Span: typeof Span; +} +declare abstract class Span { + get isTraced(): boolean; + spanContext(): TracingSpanContext; + setAttribute(key: string, value: boolean | number | string): this; + setAttributes(attributes: Record): this; + recordException(exception: string | { + code: string | number; + name?: string; + message?: string; + stack?: string; + } | { + code?: string | number; + name: string; + message?: string; + stack?: string; + } | { + code?: string | number; + name?: string; + message: string; + stack?: string; + }): void; + updateName(name: string): this; + setStatus(status: TracingSpanStatus): this; + end(): void; +} +type TracingSpanStatusCode = "unset" | "ok" | "error"; +interface TracingSpanStatus { + code: TracingSpanStatusCode; + message?: string; +} +interface TracingSpanContext { + traceId: string; + spanId: string; + traceFlags: number; +} +/** + * Represents the identity of a user authenticated via Cloudflare Access. + * This matches the result of calling /cdn-cgi/access/get-identity. + * + * The exact structure of the returned object depends on the identity provider + * configuration for the Access application. The fields below represent commonly + * available properties, but additional provider-specific fields may be present. + */ +interface CloudflareAccessIdentity extends Record { + /** The user's email address, if available from the identity provider. */ + email?: string; + /** The user's display name. */ + name?: string; + /** The user's unique identifier. */ + user_uuid?: string; + /** The Cloudflare account ID. */ + account_id?: string; + /** Login timestamp (Unix epoch seconds). */ + iat?: number; + /** The user's IP address at authentication time. */ + ip?: string; + /** Authentication methods used (e.g., "pwd"). */ + amr?: string[]; + /** Identity provider information. */ + idp?: { + id: string; + type: string; + }; + /** Geographic information about where the user authenticated. */ + geo?: { + country: string; + }; + /** Group memberships from the identity provider. */ + groups?: Array<{ + id: string; + name: string; + email?: string; + }>; + /** Device posture check results, keyed by check ID. */ + devicePosture?: Record; + /** True if the user connected via Cloudflare WARP. */ + is_warp?: boolean; + /** True if the user is authenticated via Cloudflare Gateway. */ + is_gateway?: boolean; +} +// ============================================================================ +// 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; + /** Filter items by their unique ID. Returns at most one item. */ + item_id?: string; + /** + * Filter items by their exact key (object key / filename). Keys are unique + * per source, so combine with `source` to disambiguate across data sources. + */ + key?: 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; +}; +// Source of truth for per-model reasoning types: each model's `reasoning_effort` metadata +// in Workers AI ConfigAPI (supported efforts, aliases, defaults, and whether reasoning can +// be turned off). The Workers AI SDK type generator (cloudflare/ai/sdk, +// apps/worker-constellation-entry/scripts/build-types) turns it into the per-model +// `inputs` types below; the developer docs model schemas come from the same metadata. +type ChatTemplateKwargs = { + /** Whether to enable reasoning. Support and defaults depend on the model. */ + 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. Supported levels depend on the model. */ + 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 & { + reasoning?: (Omit & { + /** + * Reasoning effort. Supported levels: low, medium, high. Reasoning cannot be disabled. + * + * @default "medium" + */ + effort?: "low" | "medium" | "high" | null; + }) | null; + }, Omit & { + /** + * Reasoning effort. Supported levels: low, medium, high. Reasoning cannot be disabled. + * + * @default "medium" + */ + reasoning_effort?: "low" | "medium" | "high" | null; + chat_template_kwargs?: Omit & { + /** + * Reasoning is always enabled for this model and cannot be disabled. + * + * @default true + */ + enable_thinking?: true; + }; + }>; + postProcessedOutputs: XOR; +} +declare abstract class Base_Ai_Cf_Openai_Gpt_Oss_20B { + inputs: XOR & { + reasoning?: (Omit & { + /** + * Reasoning effort. Supported levels: low, medium, high. Reasoning cannot be disabled. + * + * @default "medium" + */ + effort?: "low" | "medium" | "high" | null; + }) | null; + }, Omit & { + /** + * Reasoning effort. Supported levels: low, medium, high. Reasoning cannot be disabled. + * + * @default "medium" + */ + reasoning_effort?: "low" | "medium" | "high" | null; + chat_template_kwargs?: Omit & { + /** + * Reasoning is always enabled for this model and cannot be disabled. + * + * @default true + */ + enable_thinking?: true; + }; + }>; + 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: Omit & { + chat_template_kwargs?: Omit & { + /** + * Whether to enable reasoning for this model. This model has no reasoning effort levels. + * + * @default true + */ + enable_thinking?: boolean; + }; + }; + 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: Omit & { + /** + * Reasoning effort. Supported levels: high, none. Compatibility aliases (accepted by the API, not by these types): low maps to high; medium maps to high; max maps to high. + * + * @default "high" + */ + reasoning_effort?: "high" | "none" | null; + chat_template_kwargs?: Omit & { + /** + * Whether to enable reasoning for this model. + * + * @default true + */ + enable_thinking?: boolean; + }; + }; + postProcessedOutputs: ChatCompletionsOutput; +} +type Ai_Cf_Nvidia_Nemotron_3_120B_A12B_Input = Omit & { + /** + * ID of the model to use (for example, '@cf/nvidia/nemotron-3-120b-a12b'). + */ + model?: ChatCompletionsInput["model"]; + /** + * The maximum number of tokens to generate. + */ + max_tokens?: ChatCompletionsInput["max_tokens"]; + /** + * Set of key-value pairs that can be attached to the object. + */ + metadata?: ChatCompletionsInput["metadata"]; + /** + * Output types requested from the model. + */ + modalities?: ChatCompletionsInput["modalities"]; + /** + * Nemotron chat-template controls for normal reasoning, low-effort reasoning, and non-reasoning responses. + */ + chat_template_kwargs?: { + /** + * Whether to enable reasoning. Reasoning is enabled by default. + */ + enable_thinking?: boolean; + /** + * When reasoning is enabled, use Nemotron's low-effort reasoning mode, which uses significantly fewer reasoning tokens. + */ + low_effort?: boolean; + /** + * For coding agent use, Nvidia suggests setting force_nonempty_content=true + */ + force_nonempty_content?: boolean; + }; + /** + * Whether to store the output for model distillation or evaluation. + * + * @default false + */ + store?: ChatCompletionsInput["store"]; +}; +declare abstract class Base_Ai_Cf_Nvidia_Nemotron_3_120B_A12B { + inputs: Ai_Cf_Nvidia_Nemotron_3_120B_A12B_Input; + postProcessedOutputs: ChatCompletionsOutput; +} +type Ai_Cf_Google_Gemma_4_26B_A4B_It_Input = Omit & { + chat_template_kwargs?: Omit & { + /** + * Whether to enable reasoning for this model. This model has no reasoning effort levels. + * + * @default true + */ + enable_thinking?: boolean; + }; +} & { + /** + * @default false + */ + skip_special_tokens?: boolean; +}; +declare abstract class Base_Ai_Cf_Google_Gemma_4_26B_A4B_It { + inputs: Ai_Cf_Google_Gemma_4_26B_A4B_It_Input; + postProcessedOutputs: ChatCompletionsOutput; +} +/** @deprecated Use Base_Ai_Cf_Google_Gemma_4_26B_A4B_It. */ +declare abstract class Base_Ai_Cf_Google_Gemma_4_26B_A4B_IT extends Base_Ai_Cf_Google_Gemma_4_26B_A4B_It { +} +type Ai_Cf_Nvidia_Nemotron_Speech_Streaming_En_0_6B_Input = { + /** + * readable stream with audio data and content-type specified for that data + */ + audio: { + body: object; + contentType: string; + }; +} | { + /** + * base64 encoded audio data + */ + audio: string; + encoding?: "wav" | "flac" | "ogg" | "linear16"; + sample_rate?: number; + channels?: number; +}; +interface Ai_Cf_Nvidia_Nemotron_Speech_Streaming_En_0_6B_Output { + text?: string; + duration?: number; +} +declare abstract class Base_Ai_Cf_Nvidia_Nemotron_Speech_Streaming_En_0_6B { + inputs: Ai_Cf_Nvidia_Nemotron_Speech_Streaming_En_0_6B_Input; + postProcessedOutputs: Ai_Cf_Nvidia_Nemotron_Speech_Streaming_En_0_6B_Output; +} +declare abstract class Base_Ai_Cf_Moonshotai_Kimi_K2_7_Code { + inputs: Omit & { + chat_template_kwargs?: Omit & { + /** + * Reasoning is always enabled for this model and cannot be disabled. This model has no reasoning effort levels. + * + * @default true + */ + enable_thinking?: true; + }; + }; + postProcessedOutputs: ChatCompletionsOutput; +} +declare abstract class Base_Ai_Cf_Zai_Org_Glm_5_2 { + inputs: Omit & { + /** + * Reasoning effort. Supported levels: max, high, none. Compatibility aliases (accepted by the API, not by these types): low maps to high; medium maps to high; xhigh maps to max; minimal maps to none. + * + * @default "max" + */ + reasoning_effort?: "max" | "high" | "none" | null; + chat_template_kwargs?: Omit & { + /** + * Whether to enable reasoning for this model. + * + * @default true + */ + enable_thinking?: boolean; + }; + }; + postProcessedOutputs: ChatCompletionsOutput; +} +interface Ai_Cf_Moondream_Moondream3_1_9B_A2B_Input { + /** + * Which Moondream skill to run. + */ + task?: "query" | "caption" | "point" | "detect"; + /** + * Input image as a public HTTPS URL or base64 data URI. Optional for `query`; required for `caption`, `point`, and `detect`. + */ + image?: string; + /** + * Question for the `query` task. + */ + question?: string; + /** + * Caption length for the `caption` task. + */ + caption_length?: "short" | "normal" | "long"; + /** + * Object phrase to locate for `point` and `detect` tasks (e.g. 'person wearing a red shirt'). + */ + target?: string; + /** + * Enable reasoning trace for the `query` task. + */ + reasoning?: boolean; + /** + * Sampling temperature. + */ + temperature?: number; + /** + * Top-p (nucleus) sampling. + */ + top_p?: number; + /** + * Max tokens to generate for `query` and `caption`. + */ + max_tokens?: number; + /** + * Max objects to return for `point` and `detect`. + */ + max_objects?: number; + /** + * Return incremental tokens for `query` and `caption`. `point` and `detect` do not support streaming. + */ + stream?: boolean; +} +interface Ai_Cf_Moondream_Moondream3_1_9B_A2B_Output { + /** + * Reason the generation finished. + */ + finish_reason: string; + metrics: { + /** + * Number of input tokens consumed. + */ + input_tokens: number; + /** + * Number of output tokens generated. + */ + output_tokens: number; + /** + * Prefill time in milliseconds. + */ + prefill_time_ms: number; + /** + * Decode time in milliseconds. + */ + decode_time_ms: number; + /** + * Time to first token in milliseconds. + */ + ttft_ms: number; + }; + /** + * Answer text for the `query` task. Null for other tasks. + */ + answer?: string; + /** + * Caption text for the `caption` task. Null for other tasks. + */ + caption?: string; + /** + * Located points for the `point` task. Null for other tasks. + */ + points?: { + /** + * X coordinate. + */ + x: number; + /** + * Y coordinate. + */ + y: number; + }[]; + /** + * Detected bounding boxes for the `detect` task. Null for other tasks. + */ + objects?: { + /** + * Minimum X coordinate. + */ + x_min: number; + /** + * Minimum Y coordinate. + */ + y_min: number; + /** + * Maximum X coordinate. + */ + x_max: number; + /** + * Maximum Y coordinate. + */ + y_max: number; + }[]; + /** + * Reasoning trace for the `query` task when reasoning=true. Null otherwise. + */ + reasoning?: { + /** + * Reasoning text. + */ + text: string; + /** + * Grounding information. + */ + grounding?: {}[]; + }; +} +declare abstract class Base_Ai_Cf_Moondream_Moondream3_1_9B_A2B { + inputs: Ai_Cf_Moondream_Moondream3_1_9B_A2B_Input; + postProcessedOutputs: Ai_Cf_Moondream_Moondream3_1_9B_A2B_Output; +} +declare abstract class Base_Ai_Cf_Deepseek_Ai_Deepseek_V4_Flash_0731 { + inputs: Omit & { + /** + * Reasoning effort. Supported levels: max, high, low, none. Compatibility aliases (accepted by the API, not by these types): minimal maps to low; medium maps to high; xhigh maps to high. + * + * @default "high" + */ + reasoning_effort?: "max" | "high" | "low" | "none" | null; + chat_template_kwargs?: Omit & { + /** + * Whether to enable reasoning for this model. + * + * @default true + */ + enable_thinking?: boolean; + }; + }; + postProcessedOutputs: ChatCompletionsOutput; +} +declare abstract class Base_Ai_Cf_Deepseek_Ai_Deepseek_V4_Pro_0813 { + inputs: Omit & { + /** + * Reasoning effort. Supported levels: max, high, low, none. Compatibility aliases (accepted by the API, not by these types): minimal maps to low; medium maps to high; xhigh maps to high. + * + * @default "high" + */ + reasoning_effort?: "max" | "high" | "low" | "none" | null; + chat_template_kwargs?: Omit & { + /** + * Whether to enable reasoning for this model. + * + * @default true + */ + enable_thinking?: boolean; + }; + }; + postProcessedOutputs: ChatCompletionsOutput; +} +declare abstract class Base_Ai_Cf_Qwen_Qwen3_8_27B { + inputs: Omit & { + /** + * Reasoning effort. Supported levels: low, medium, xhigh. + * + * @default "xhigh" + */ + reasoning_effort?: "low" | "medium" | "xhigh" | null; + chat_template_kwargs?: Omit & { + /** + * Whether to enable reasoning for this model. + * + * @default true + */ + enable_thinking?: boolean; + }; + }; + postProcessedOutputs: ChatCompletionsOutput; +} +declare abstract class Base_Ai_Cf_Zai_Org_Glm_5_3 { + inputs: Omit & { + /** + * Reasoning effort. Supported levels: max, high, low. Reasoning cannot be disabled. Compatibility aliases (accepted by the API, not by these types): none maps to max; minimal maps to max; medium maps to max; xhigh maps to max. + * + * @default "max" + */ + reasoning_effort?: "max" | "high" | "low" | null; + chat_template_kwargs?: Omit & { + /** + * Reasoning is always enabled for this model and cannot be disabled. + * + * @default true + */ + enable_thinking?: true; + }; + }; + postProcessedOutputs: ChatCompletionsOutput; +} +declare abstract class Base_Ai_Cf_Zai_Org_Glm_5_3_Flash { + inputs: Omit & { + /** + * Reasoning effort. Supported levels: max, high, low. Reasoning cannot be disabled. Compatibility aliases (accepted by the API, not by these types): none maps to max; minimal maps to max; medium maps to max; xhigh maps to max. + * + * @default "max" + */ + reasoning_effort?: "max" | "high" | "low" | null; + chat_template_kwargs?: Omit & { + /** + * Reasoning is always enabled for this model and cannot be disabled. + * + * @default true + */ + enable_thinking?: true; + }; + }; + 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/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; + "@cf/nvidia/nemotron-speech-streaming-en-0.6b": Base_Ai_Cf_Nvidia_Nemotron_Speech_Streaming_En_0_6B; + "@cf/moonshotai/kimi-k2.7-code": Base_Ai_Cf_Moonshotai_Kimi_K2_7_Code; + "@cf/zai-org/glm-5.2": Base_Ai_Cf_Zai_Org_Glm_5_2; + "@cf/moondream/moondream3.1-9B-A2B": Base_Ai_Cf_Moondream_Moondream3_1_9B_A2B; + "@cf/deepseek-ai/deepseek-v4-flash-0731": Base_Ai_Cf_Deepseek_Ai_Deepseek_V4_Flash_0731; + "@cf/deepseek-ai/deepseek-v4-pro-0813": Base_Ai_Cf_Deepseek_Ai_Deepseek_V4_Pro_0813; + "@cf/qwen/qwen3.8-27b": Base_Ai_Cf_Qwen_Qwen3_8_27B; + "@cf/zai-org/glm-5.3": Base_Ai_Cf_Zai_Org_Glm_5_3; + "@cf/zai-org/glm-5.3-flash": Base_Ai_Cf_Zai_Org_Glm_5_3_Flash; +} +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; +}; +type AiWebSearchRequest = { + /** AI Gateway configuration used for this request. */ + gatewayId: string; + /** Search query. */ + query: string; + /** Maximum number of results. Defaults to 10 and is capped at 20. */ + limit?: number; + /** Optional BYOK web-search provider configured on the gateway. */ + provider?: string; + /** Optional BYOK key alias. Defaults to `default`. */ + byokAlias?: string; +}; +declare abstract class Ai { + aiGatewayLogId: string | null; + gateway(gatewayId: string): AiGateway; + websearch(request: AiWebSearchRequest): Promise; + /** + * @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 +} +/** A parameter accepted by an Analytics SQL query. */ +type AnalyticsSQLParameter = string | number | boolean | null; +/** An Analytics SQL query and its optional positional or named parameters. */ +interface AnalyticsSQLQuery { + query: string; + params?: readonly AnalyticsSQLParameter[] | Readonly>; +} +/** Execution statistics returned by Analytics SQL. */ +interface AnalyticsSQLStatistics { + elapsed_ms: number; + rows_read: number; + bytes_read: number; +} +/** The rows and execution statistics returned by an Analytics SQL query. */ +interface AnalyticsSQLResult = Record> { + data: T[]; + rows: number; + statistics: AnalyticsSQLStatistics; +} +/** An Analytics SQL binding. */ +interface AnalyticsSQLBinding { + query = Record>(request: AnalyticsSQLQuery): Promise>; +} +// 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; +} +/** 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; +} +/** + * Classification of a Git tree entry derived from its mode. + * + * `tree` is a directory, `blob` is a regular file, `symlink` is a symbolic link, + * `gitlink` is a submodule reference, and `exec` is an executable file. + */ +type ArtifactsTreeEntryType = 'tree' | 'blob' | 'symlink' | 'gitlink' | 'exec'; +/** An immediate child of a Git tree returned by {@link ArtifactsRepo.readTree}. */ +interface ArtifactsTreeEntry { + /** Name relative to the tree being read. */ + name: string; + /** Canonical Git mode, such as `100644` for a file or `40000` for a tree. */ + mode: string; + /** Lowercase, 40-character SHA-1 object ID. */ + hash: string; + /** Classification derived from `mode`. */ + type: ArtifactsTreeEntryType; +} +/** Decoded metadata returned by {@link ArtifactsRepo.readCommit} and {@link ArtifactsRepo.log}. */ +interface ArtifactsCommitMetadata { + /** Lowercase, 40-character SHA-1 commit ID. */ + hash: string; + /** Lowercase, 40-character SHA-1 ID of the commit's root tree. */ + treeHash: string; + /** Commit message with one trailing newline removed, if present. */ + message: string; + /** Author identity from the commit. */ + author: { + /** Author name. */ + name: string; + /** Author email address. */ + email: string; + }; + /** Committer identity from the commit. */ + committer: { + /** Committer name. */ + name: string; + /** Committer email address. */ + email: string; + }; + /** Parent commit IDs in Git order; empty for a root commit. */ + parents: string[]; + /** Author timestamp in Unix seconds. */ + authoredAt: number; + /** Committer timestamp in Unix seconds. */ + committedAt: number; +} +/** + * Repository capability returned by {@link Artifacts.get}. + * + * Metadata is available through {@link info}, not as properties on the capability. + * Methods may throw `ArtifactsError` with code `INTERNAL_ERROR` if an unexpected service error occurs. + * + * The capability is an RPC stub. Dispose of it when finished, for example with + * `using repo = await env.ARTIFACTS.get(name);`, to release it before the request ends. + */ +interface ArtifactsRepo extends Disposable { + /** + * 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; + /** + * Retrieve current repository metadata. Each call performs a fresh lookup. + * @returns Current public repository metadata. + * @throws {ArtifactsError} `NOT_FOUND` if the repository was deleted, or `INTERNAL_ERROR` on + * an unexpected lookup failure. + */ + info(): Promise; + /** + * Read the raw bytes of a Git blob by object ID. + * @param hash Lowercase, 40-character SHA-1 object ID. + * @returns An untyped {@link Blob}, or `null` if the object is missing or is not a Git blob. + * @throws {ArtifactsError} `INVALID_INPUT` for a malformed hash, `MEMORY_LIMIT` if the object + * cannot be buffered safely, or `INTERNAL_ERROR` on an unexpected read failure. + * @see {@link ArtifactsRepo.readFile} to resolve a file by ref and path. + */ + readBlob(hash: string): Promise; + /** + * Read the immediate children of a Git tree by object ID. + * @param hash Lowercase, 40-character SHA-1 tree ID. + * @returns Tree entries, or `null` if the object is missing. + * @throws {ArtifactsError} `INVALID_INPUT` for a malformed hash, or `INTERNAL_ERROR` if the + * object is not a valid tree or the read fails unexpectedly. + * @see {@link ArtifactsTreeEntry} + */ + readTree(hash: string): Promise; + /** + * Decode a Git commit by object ID. + * @param hash Lowercase, 40-character SHA-1 commit ID. + * @returns Commit metadata, or `null` if the object is missing. + * @throws {ArtifactsError} `INVALID_INPUT` for a malformed hash, or `INTERNAL_ERROR` if the + * object is not a valid commit or the read fails unexpectedly. + * @see {@link ArtifactsCommitMetadata} + */ + readCommit(hash: string): Promise; + /** + * Resolve a file from a branch, tag, or commit ID and return its bytes with a browser-safe + * content type in {@link Blob.type}. + * @param args File lookup options. + * @param args.ref Branch, tag, or commit ID to resolve. + * @param args.path Non-empty repository-relative path. + * @returns A MIME-typed {@link Blob}, or `null` if the ref or path cannot resolve to a file. + * @throws {ArtifactsError} `INVALID_INPUT` if either argument is empty, `MEMORY_LIMIT` if the + * file cannot be buffered safely, or `INTERNAL_ERROR` on an unexpected read failure. + */ + readFile(args: { + ref: string; + path: string; + }): Promise; + /** + * List commits along the first-parent chain, newest first. + * @param opts History options. All fields are optional. + * @param opts.ref Branch, tag, or commit ID; defaults to `HEAD`. + * @param opts.limit Maximum results; defaults to `50` and is capped at `1000`. + * @param opts.offset Number of matching commits to skip; defaults to `0`. + * @returns Commit metadata, or an empty array if the ref cannot be resolved. + * @throws {ArtifactsError} `INTERNAL_ERROR` on an unexpected traversal failure. + * @see {@link ArtifactsCommitMetadata} + */ + log(opts?: { + ref?: string; + limit?: number; + offset?: number; + }): 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' | 'CREATE_IN_PROGRESS' | '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 `CREATE_IN_PROGRESS` if the repo is still being created. + * @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 `