diff --git a/.github/workflows/buf-push.yml b/.github/workflows/buf-push.yml index c2ac37676..1de72cb8c 100644 --- a/.github/workflows/buf-push.yml +++ b/.github/workflows/buf-push.yml @@ -32,7 +32,7 @@ jobs: - name: ⎔ Setup node uses: actions/setup-node@v6 with: - node-version: 24.12.0 + node-version: 26.8.1 cache: "pnpm" - name: 📥 Download deps diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index af1718949..46d92188f 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -24,7 +24,7 @@ jobs: - name: ⎔ Setup node uses: actions/setup-node@v6 with: - node-version: 24.12.0 + node-version: 26.8.1 cache: "pnpm" - name: 🦕 Setup Deno diff --git a/.github/workflows/dx.yml b/.github/workflows/dx.yml index 79973afe6..1d5b0bcec 100644 --- a/.github/workflows/dx.yml +++ b/.github/workflows/dx.yml @@ -35,7 +35,7 @@ jobs: - name: ⎔ Setup node uses: actions/setup-node@v6 with: - node-version: 24.12.0 + node-version: 26.8.1 cache: "pnpm" - name: 📥 Download deps diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index b04500ae0..39212d85d 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -25,7 +25,7 @@ jobs: - name: ⎔ Setup node uses: actions/setup-node@v6 with: - node-version: 24.12.0 + node-version: 26.8.1 cache: "pnpm" - name: 📥 Download deps diff --git a/.github/workflows/migrate.yml b/.github/workflows/migrate.yml index 9944f7a7c..e2dcffc93 100644 --- a/.github/workflows/migrate.yml +++ b/.github/workflows/migrate.yml @@ -23,7 +23,7 @@ jobs: - name: ⎔ Setup node uses: actions/setup-node@v6 with: - node-version: 24.12.0 + node-version: 26.8.1 cache: "pnpm" - name: 🦕 Install deno diff --git a/.github/workflows/proto-check.yml b/.github/workflows/proto-check.yml index 1b2f41934..39e5072ac 100644 --- a/.github/workflows/proto-check.yml +++ b/.github/workflows/proto-check.yml @@ -32,7 +32,7 @@ jobs: - name: ⎔ Setup node uses: actions/setup-node@v6 with: - node-version: 24.12.0 + node-version: 26.8.1 cache: "pnpm" - name: 🦕 Setup Deno diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 5dd8e0c51..3bf2506b8 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -66,7 +66,7 @@ jobs: - name: ⎔ Setup node uses: actions/setup-node@v6 with: - node-version: 24.12.0 + node-version: 26.8.1 cache: "pnpm" - name: 🦕 Setup Deno diff --git a/AGENTS.md b/AGENTS.md index feae67c92..52b0e68d9 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -7,7 +7,7 @@ this file never restates them. ## Verify your change ```sh -pnpm verify # oxfmt + oxlint + doc refs + deno check. No database, seconds. +pnpm verify # oxfmt + oxlint + doc refs + tsc (deno check in test-utils). No database, seconds. pnpm verify:test # tests for packages affected by your diff. Needs a database. ``` @@ -19,7 +19,7 @@ are in `apps/dashboard/README.md`. `devbox.json` pins exact node, deno, bun, turso-cli and sqld versions; the CI workflows pin the same node and deno. Never float one back to `@latest` or -`v2.x` — `deno check` results differ by deno version, so a drifting pin makes +`v2.x` — `tsc` and `deno test` results differ by version, so a drifting pin makes `pnpm verify` disagree with CI for reasons unrelated to your change. ## Architecture @@ -83,7 +83,7 @@ boundaries with external SDKs or at registry-style dispatch. When you need one: - `apps/dashboard/AGENTS.md` — Next.js runtimes, client boundary, UI verification - `apps/server/AGENTS.md` — Hono API, API-key scopes - `apps/status-page/AGENTS.md` — public surfaces and gated content -- `apps/workflows/AGENTS.md` — Deno runtime constraints +- `apps/workflows/AGENTS.md` — bundling constraints, native side-car - `apps/checker/AGENTS.md` — Go probing tier - `apps/web/AGENTS.md` — marketing site, `.well-known`, search, content pages diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 6b5a84ca1..9146fe34c 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -33,9 +33,9 @@ To contribute code changes, follow these steps: no database and takes seconds. `pnpm verify:test` runs the tests for the packages your diff touches; those do need a local libSQL server. - Note: the type checks (`check` task) in most packages run through - [Deno](https://deno.com/), which must be installed locally. Without it, - `pnpm verify` fails with `deno: command not found` in every package. + Note: tests still run on `deno test` and `packages/test-utils` still + type-checks with [Deno](https://deno.com/), so both `pnpm verify` and + `pnpm verify:test` need it installed locally. See the [Requirements](./README.md#requirements) list in the README for the full toolchain (Node.js, pnpm, Bun, Deno, Turso CLI). 4. Make commits with clear and descriptive messages. Each commit should have a single logical purpose. diff --git a/DOCKER.md b/DOCKER.md index c29c46a45..88b6de3d6 100644 --- a/DOCKER.md +++ b/DOCKER.md @@ -22,7 +22,7 @@ docker compose ps # port — seed refuses non-localhost databases by design) docker run --rm --network host \ -e DATABASE_URL=http://localhost:8080 \ - --entrypoint deno openstatus/db-migrate:latest task seed + --entrypoint npm openstatus/db-migrate:latest run seed # 6. (Optional) Deploy Tinybird local - requires tb CLI cd packages/tinybird @@ -122,7 +122,7 @@ After migrations complete, seed the database with sample data: # databases by design, so it cannot run over the compose-internal network docker run --rm --network host \ -e DATABASE_URL=http://localhost:8080 \ - --entrypoint deno openstatus/db-migrate:latest task seed + --entrypoint npm openstatus/db-migrate:latest run seed ``` This creates: diff --git a/apps/dashboard/AGENTS.md b/apps/dashboard/AGENTS.md index c3c6b78ff..033ab604e 100644 --- a/apps/dashboard/AGENTS.md +++ b/apps/dashboard/AGENTS.md @@ -26,7 +26,7 @@ The whole tRPC surface is served from `apps/dashboard/src/app/api/trpc/lambda/[trpc]/route.ts` on the **Node.js** runtime — several routers pull Node-only SDKs (`@slack/web-api`, email and notification clients). Node-only dependencies inside `@openstatus/services` are -still forbidden, because `apps/workflows` runs the same code on Deno. +still forbidden, because that package stays Edge-safe. ## Client boundary diff --git a/apps/dashboard/README.md b/apps/dashboard/README.md index 15c7e40e5..73c8ea9a2 100644 --- a/apps/dashboard/README.md +++ b/apps/dashboard/README.md @@ -6,7 +6,6 @@ - [Node.js](https://nodejs.org/en/) >= 20.0.0 — runtime - [pnpm](https://pnpm.io/) >= 8 — monorepo package manager (see root `package.json` for pinned version) -- [Deno](https://deno.com/) — runs the db env/migrate/seed scripts in `packages/db` - [Bun](https://bun.sh/) — optional, used by the utility scripts in `src/scripts` - [Turso CLI](https://docs.turso.tech/quickstart) — starts the local libSQL server via `pnpm --filter '@openstatus/db' dev` diff --git a/apps/dashboard/env.ts b/apps/dashboard/env.ts index dab63f5f4..3e27fee79 100644 --- a/apps/dashboard/env.ts +++ b/apps/dashboard/env.ts @@ -1 +1,3 @@ -await Deno.copyFile("./.env.example", "./.env"); +import { copyFile } from "node:fs/promises"; + +await copyFile("./.env.example", "./.env"); diff --git a/apps/screenshot-service/package.json b/apps/screenshot-service/package.json index 4bd7feb41..4220ef23b 100644 --- a/apps/screenshot-service/package.json +++ b/apps/screenshot-service/package.json @@ -2,7 +2,7 @@ "name": "@openstatus/screenshot-service", "version": "0.0.1", "scripts": { - "check": "deno check --sloppy-imports .", + "check": "tsc --noEmit", "dev": "bun run --hot src/index.ts", "start": "NODE_ENV=production bun run src/index.ts" }, @@ -19,6 +19,7 @@ }, "devDependencies": { "@openstatus/tsconfig": "workspace:*", + "@types/node": "catalog:", "typescript": "catalog:" } } diff --git a/apps/screenshot-service/tsconfig.json b/apps/screenshot-service/tsconfig.json index 0770bb760..d1ea02542 100644 --- a/apps/screenshot-service/tsconfig.json +++ b/apps/screenshot-service/tsconfig.json @@ -1,5 +1,14 @@ { "extends": "@openstatus/tsconfig/base.json", "include": ["src", "*.ts", "**/*.ts"], - "compilerOptions": {} + "compilerOptions": {}, + "exclude": [ + "node_modules", + "**/*.test.ts", + "**/*.test.tsx", + "**/__tests__/**", + "**/test/**", + "**/*.mock.ts", + "**/test-preload.ts" + ] } diff --git a/apps/server/AGENTS.md b/apps/server/AGENTS.md index 40c03a340..0a296c44f 100644 --- a/apps/server/AGENTS.md +++ b/apps/server/AGENTS.md @@ -1,6 +1,6 @@ # AGENTS.md — apps/server -Hono, running on Deno. Four surfaces under `apps/server/src/routes/`: `v1` +Hono, running on Node. Four surfaces under `apps/server/src/routes/`: `v1` (public REST), `rpc` (ConnectRPC), `mcp`, and `slack`. ## API-key scopes diff --git a/apps/server/Dockerfile b/apps/server/Dockerfile index 0198630f2..62da251ac 100644 --- a/apps/server/Dockerfile +++ b/apps/server/Dockerfile @@ -3,10 +3,10 @@ # See https://github.com/lenra-io/dofigen # install -FROM node@sha256:0e0ff40c39bc087845bfb27465a0df4ea419520094bc35842ff83dd8cbe6f9b6 AS install +FROM node@sha256:c0753125a3789977aefe869cbebccf70e3cfd7ea84ca48547458f02e4f1d7146 AS install LABEL \ - org.opencontainers.image.base.digest="sha256:0e0ff40c39bc087845bfb27465a0df4ea419520094bc35842ff83dd8cbe6f9b6" \ - org.opencontainers.image.base.name="docker.io/node:24-slim" \ + org.opencontainers.image.base.digest="sha256:c0753125a3789977aefe869cbebccf70e3cfd7ea84ca48547458f02e4f1d7146" \ + org.opencontainers.image.base.name="docker.io/node:26.8.1-slim" \ org.opencontainers.image.stage="install" WORKDIR /app/ RUN \ @@ -39,6 +39,7 @@ RUN \ --mount=type=bind,target=packages/upstash/package.json,source=packages/upstash/package.json \ --mount=type=bind,target=packages/utils/package.json,source=packages/utils/package.json \ --mount=type=bind,target=packages/tsconfig/package.json,source=packages/tsconfig/package.json \ + --mount=type=bind,target=packages/test-utils/package.json,source=packages/test-utils/package.json \ --mount=type=bind,target=packages/subscriptions/package.json,source=packages/subscriptions/package.json \ --mount=type=bind,target=packages/assertions/package.json,source=packages/assertions/package.json \ --mount=type=bind,target=packages/theme-store/package.json,source=packages/theme-store/package.json \ @@ -47,15 +48,16 @@ RUN \ --mount=type=bind,target=packages/importers/package.json,source=packages/importers/package.json \ --mount=type=cache,target=/root/.local/share/pnpm/store,sharing=locked \ </slack/oauth/callback to apps/server/.env", ); - Deno.exit(1); + process.exit(1); } const redirectUrl = new URL(redirectUri); if (redirectUrl.pathname !== "/slack/oauth/callback") { console.error( `SLACK_REDIRECT_URI must have the exact path /slack/oauth/callback — got ${redirectUri}`, ); - Deno.exit(1); + process.exit(1); } const devOrigin = redirectUrl.origin; @@ -32,8 +34,5 @@ dev.features.bot_user.display_name = DEV_NAME; dev.oauth_config.redirect_urls = [redirectUri]; const output = `${JSON.stringify(dev, null, 2).replaceAll(PROD_ORIGIN, devOrigin)}\n`; -await Deno.writeTextFile( - new URL("../slack-manifest.dev.json", import.meta.url), - output, -); +await writeFile(new URL("../slack-manifest.dev.json", import.meta.url), output); console.log(output); diff --git a/apps/server/src/index.ts b/apps/server/src/index.ts index a432808bb..c4af9d0ea 100644 --- a/apps/server/src/index.ts +++ b/apps/server/src/index.ts @@ -58,9 +58,9 @@ const otelLogger = getLogger("api-server-otel"); * This allows tests to import `app` immediately. */ -// Build the LoggerProvider with a static OTLP exporter import so `deno bundle` -// includes it. Letting @logtape/otel create the exporter triggers a dynamic -// import of a bare specifier, which the compiled --node-modules-dir=none binary +// Build the LoggerProvider with a static OTLP exporter import so tsdown +// bundles it. Letting @logtape/otel create the exporter triggers a dynamic +// import of a bare specifier, which the image (shipped without node_modules) // can't resolve. const loggerProvider = new LoggerProvider({ resource: resourceFromAttributes({ diff --git a/apps/server/src/routes/mcp/evals/run.ts b/apps/server/src/routes/mcp/evals/run.ts index 0a96425e6..d49903800 100644 --- a/apps/server/src/routes/mcp/evals/run.ts +++ b/apps/server/src/routes/mcp/evals/run.ts @@ -1,5 +1,5 @@ /** - * MCP tool-selection eval. Standalone bun script — `pnpm eval:mcp`. + * MCP tool-selection eval. Standalone Node script — `pnpm eval:mcp`. * * Runs each case in `cases.ts` against Claude Opus 5 (via the AI * Gateway), asserting the model picks the expected tool and includes diff --git a/apps/server/src/routes/openapi.ts b/apps/server/src/routes/openapi.ts index 2b8e8bb25..5fee6da07 100644 --- a/apps/server/src/routes/openapi.ts +++ b/apps/server/src/routes/openapi.ts @@ -2,7 +2,7 @@ import { Hono } from "hono"; import openapiV1Json from "../../static/openapi-v1.json" with { type: "json" }; // Generated alongside openapi.json; embedded rather than read from disk because -// `deno bundle` flattens src/ and a runtime path would resolve from the bundle. +// tsdown flattens src/ and a runtime path would resolve from the bundle. import openapiYaml from "../../static/openapi-yaml"; import openapiJson from "../../static/openapi.json" with { type: "json" }; diff --git a/apps/server/src/routes/v1/monitors/run/post.ts b/apps/server/src/routes/v1/monitors/run/post.ts index 0c34dcb8d..86b7242f0 100644 --- a/apps/server/src/routes/v1/monitors/run/post.ts +++ b/apps/server/src/routes/v1/monitors/run/post.ts @@ -159,7 +159,7 @@ export function registerRunMonitor(api: typeof monitorsApi) { if (noWait) { // The promises keep running after the response; an uncaught rejection - // here would crash the Deno process, so swallow and log failures. + // here would crash the process, so swallow and log failures. for (const result of allResult) { result.catch((error: unknown) => { logger.warn("Checker request failed", { diff --git a/apps/server/src/serve.ts b/apps/server/src/serve.ts index 9080cb04f..7ee67e50d 100644 --- a/apps/server/src/serve.ts +++ b/apps/server/src/serve.ts @@ -1,3 +1,4 @@ +import { serve } from "@hono/node-server"; import { getLogger } from "@logtape/logtape"; import { env } from "./env"; @@ -5,15 +6,13 @@ import { app } from "./index"; const port = 3000; -// Deno terminates the process on unhandled rejections; log instead so one +// Node terminates the process on unhandled rejections; log instead so one // stray background promise (e.g. a fire-and-forget fetch) can't kill the instance. -globalThis.addEventListener("unhandledrejection", (event) => { - const reason = event.reason; +process.on("unhandledRejection", (reason) => { getLogger("api-server").error("Unhandled promise rejection", { reason: reason instanceof Error ? reason.message : String(reason), stack: reason instanceof Error ? reason.stack : undefined, }); - event.preventDefault(); }); getLogger("api-server").info("Starting server", { @@ -21,4 +20,4 @@ getLogger("api-server").info("Starting server", { environment: env.NODE_ENV, }); -Deno.serve({ port }, app.fetch); +serve({ fetch: app.fetch, port }); diff --git a/apps/server/tsconfig.json b/apps/server/tsconfig.json index 964d6a3cf..004eda196 100644 --- a/apps/server/tsconfig.json +++ b/apps/server/tsconfig.json @@ -8,15 +8,17 @@ "moduleResolution": "bundler", "jsxImportSource": "react", "allowJs": true, - "types": [], "paths": { - "@/*": ["./src/*"], - "@modelcontextprotocol/sdk/server/mcp.js": [ - "./node_modules/@modelcontextprotocol/sdk/dist/esm/server/mcp.d.ts" - ], - "@modelcontextprotocol/sdk/types.js": [ - "./node_modules/@modelcontextprotocol/sdk/dist/esm/types.d.ts" - ] + "@/*": ["./src/*"] } - } + }, + "exclude": [ + "node_modules", + "**/*.test.ts", + "**/*.test.tsx", + "**/__tests__/**", + "**/test/**", + "**/*.mock.ts", + "**/test-preload.ts" + ] } diff --git a/apps/server/tsdown.config.ts b/apps/server/tsdown.config.ts new file mode 100644 index 000000000..fe2598351 --- /dev/null +++ b/apps/server/tsdown.config.ts @@ -0,0 +1,12 @@ +import { defineConfig } from "tsdown"; + +// The image ships dist/ without node_modules, so every dependency is inlined. +export default defineConfig({ + entry: ["src/serve.ts"], + format: "esm", + platform: "node", + target: "node26", + fixedExtension: true, + dts: false, + deps: { alwaysBundle: [/.*/], onlyBundle: false }, +}); diff --git a/apps/status-page/env.ts b/apps/status-page/env.ts index dab63f5f4..3e27fee79 100644 --- a/apps/status-page/env.ts +++ b/apps/status-page/env.ts @@ -1 +1,3 @@ -await Deno.copyFile("./.env.example", "./.env"); +import { copyFile } from "node:fs/promises"; + +await copyFile("./.env.example", "./.env"); diff --git a/apps/status-page/package.json b/apps/status-page/package.json index eefbf6057..17eeb0ad8 100644 --- a/apps/status-page/package.json +++ b/apps/status-page/package.json @@ -3,7 +3,7 @@ "version": "1.0.0", "private": true, "scripts": { - "env": "deno run -A env.ts", + "env": "node env.ts", "dev": "next dev --turbopack", "build": "next build", "start": "next start", diff --git a/apps/web/env.ts b/apps/web/env.ts index dab63f5f4..3e27fee79 100644 --- a/apps/web/env.ts +++ b/apps/web/env.ts @@ -1 +1,3 @@ -await Deno.copyFile("./.env.example", "./.env"); +import { copyFile } from "node:fs/promises"; + +await copyFile("./.env.example", "./.env"); diff --git a/apps/web/package.json b/apps/web/package.json index 5e81cb080..06c713201 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -3,7 +3,7 @@ "version": "1.0.0", "private": true, "scripts": { - "env": "deno run -A env.ts", + "env": "node env.ts", "dev": "next dev", "build:registry": "turbo run registry:build --filter=@openstatus/ui", "build": "pnpm build:registry && next build", diff --git a/apps/workflows/AGENTS.md b/apps/workflows/AGENTS.md index b0d6b0910..84da5983f 100644 --- a/apps/workflows/AGENTS.md +++ b/apps/workflows/AGENTS.md @@ -1,15 +1,16 @@ # AGENTS.md — apps/workflows -Runs on **Deno**, not Node. `pnpm check` here is -`deno check --sloppy-imports src/serve.ts`, and it must pass — it is the only -type gate this app has (`pnpm test` runs with `--no-check`). +Runs on **Node**, through oxc-node in dev and as a tsdown bundle in production. +`pnpm check` here is `tsc --noEmit`, and it must pass — it is the only type gate +this app has (`pnpm test` runs with `--no-check`). -Constraints that follow from the runtime: +Constraints that follow from the build: -- No `node:*` built-ins, and no dependency that reaches for one. This app - imports `@openstatus/services`, so a `node:*` import added there breaks the - build here — run `pnpm check` in both packages after touching services. -- Sentry comes from `@sentry/deno`, not `@sentry/node`. +- `@libsql/client`, `libsql` and `@google-cloud/tasks` stay external in + `tsdown.config.ts` (native addon, CJS `__dirname` reads) and ship as a + side-car `node_modules` in the image. A new dependency with either trait + joins that list and the `externals` in `apps/workflows/scripts/sidecar-package.mjs`, which + writes the side-car's pinned `package.json`. - Tests run `deno test --parallel`, so test files share one process environment. Never drive a branch by assigning to `process.env` mid-test — the assignment leaks into whatever else is running. Resolve config once at diff --git a/apps/workflows/Dockerfile b/apps/workflows/Dockerfile index 59cc905e7..9a2f5c410 100644 --- a/apps/workflows/Dockerfile +++ b/apps/workflows/Dockerfile @@ -3,10 +3,10 @@ # See https://github.com/lenra-io/dofigen # install -FROM node@sha256:0e0ff40c39bc087845bfb27465a0df4ea419520094bc35842ff83dd8cbe6f9b6 AS install +FROM node@sha256:c0753125a3789977aefe869cbebccf70e3cfd7ea84ca48547458f02e4f1d7146 AS install LABEL \ - org.opencontainers.image.base.digest="sha256:0e0ff40c39bc087845bfb27465a0df4ea419520094bc35842ff83dd8cbe6f9b6" \ - org.opencontainers.image.base.name="docker.io/node:24-slim" \ + org.opencontainers.image.base.digest="sha256:c0753125a3789977aefe869cbebccf70e3cfd7ea84ca48547458f02e4f1d7146" \ + org.opencontainers.image.base.name="docker.io/node:26.8.1-slim" \ org.opencontainers.image.stage="install" WORKDIR /app/ RUN \ @@ -37,6 +37,7 @@ RUN \ --mount=type=bind,target=packages/subscriptions/package.json,source=packages/subscriptions/package.json \ --mount=type=bind,target=packages/utils/package.json,source=packages/utils/package.json \ --mount=type=bind,target=packages/tsconfig/package.json,source=packages/tsconfig/package.json \ + --mount=type=bind,target=packages/test-utils/package.json,source=packages/test-utils/package.json \ --mount=type=bind,target=packages/tinybird/package.json,source=packages/tinybird/package.json \ --mount=type=bind,target=packages/upstash/package.json,source=packages/upstash/package.json \ --mount=type=bind,target=packages/theme-store/package.json,source=packages/theme-store/package.json \ @@ -44,15 +45,16 @@ RUN \ --mount=type=bind,target=packages/status-fetcher/package.json,source=packages/status-fetcher/package.json \ --mount=type=cache,target=/root/.local/share/pnpm/store,sharing=locked \ <{for(const e of Deno.readDirSync(p))if(e.name.startsWith(x+"@"))return e.name.slice(x.length+1).split("_")[0]};Deno.writeTextFileSync("/tmp/sc/package.json",JSON.stringify({dependencies:{"@libsql/client":v("@libsql+client"),libsql:v("libsql"),"@google-cloud/tasks":v("@google-cloud+tasks")}}))' -cd /tmp/sc && deno install --node-modules-dir=auto --allow-scripts -cd /app/apps/workflows && deno bundle --platform=deno --minify --sloppy-imports --node-modules-dir=manual --external '@libsql/client' --external 'libsql' --external '@google-cloud/tasks' --output /tmp/sc/_serve.bundle.mjs src/serve.ts -cd /tmp/sc && deno compile --no-check -A --node-modules-dir=manual --output /app/apps/workflows/app /tmp/sc/_serve.bundle.mjs -mkdir -p /app/data +./node_modules/.bin/tsdown +mkdir -p /tmp/sc /app/data +node scripts/sidecar-package.mjs /tmp/sc/package.json +npm install --prefix /tmp/sc --omit=dev --no-package-lock --no-audit --no-fund EOF # runtime -FROM registry.access.redhat.com/hi/curl@sha256:848b81ab5d5e55371d7193fd4f1ea7b605d14dbb039344ffad34a4c1f0d880f4 AS runtime +FROM node@sha256:c0753125a3789977aefe869cbebccf70e3cfd7ea84ca48547458f02e4f1d7146 AS runtime LABEL \ io.dofigen.version="2.8.0" \ org.opencontainers.image.authors="OpenStatus Team" \ - org.opencontainers.image.base.digest="sha256:848b81ab5d5e55371d7193fd4f1ea7b605d14dbb039344ffad34a4c1f0d880f4" \ + org.opencontainers.image.base.digest="sha256:c0753125a3789977aefe869cbebccf70e3cfd7ea84ca48547458f02e4f1d7146" \ + org.opencontainers.image.base.name="docker.io/node:26.8.1-slim" \ org.opencontainers.image.description="Background job processing and probe scheduling for OpenStatus" \ org.opencontainers.image.source="https://github.com/openstatusHQ/openstatus" \ org.opencontainers.image.title="OpenStatus Workflows" \ org.opencontainers.image.vendor="OpenStatus" +ENV NODE_ENV="production" WORKDIR /app/ COPY \ --from=build \ --chown=1000:1000 \ --chmod=555 \ --link \ - "/app/apps/workflows/app" "/app/apps/workflows/" + "/app/apps/workflows/dist" "/app/apps/workflows/dist" +COPY \ + --from=build \ + --chown=1000:1000 \ + --chmod=555 \ + --link \ + "/tmp/sc/node_modules" "/app/apps/workflows/node_modules" COPY \ --from=build \ --chown=1000:1000 \ --link \ "/app/data" "/app/data" +USER 0:0 +RUN apt-get update && apt-get install -y --no-install-recommends curl ca-certificates && rm -rf /var/lib/apt/lists/* USER 1000:1000 EXPOSE 3000 HEALTHCHECK \ @@ -110,4 +120,4 @@ HEALTHCHECK \ --start-period=30s \ --retries=3 \ CMD ["curl", "-f", "http://localhost:3000/ping"] -ENTRYPOINT ["/app/apps/workflows/app"] +ENTRYPOINT ["node", "/app/apps/workflows/dist/serve.mjs"] diff --git a/apps/workflows/dofigen.lock b/apps/workflows/dofigen.lock index d1d8bd203..d67ba53ea 100644 --- a/apps/workflows/dofigen.lock +++ b/apps/workflows/dofigen.lock @@ -15,15 +15,16 @@ effective: | install: fromImage: path: node - digest: sha256:0e0ff40c39bc087845bfb27465a0df4ea419520094bc35842ff83dd8cbe6f9b6 + digest: sha256:c0753125a3789977aefe869cbebccf70e3cfd7ea84ca48547458f02e4f1d7146 label: - org.opencontainers.image.base.name: docker.io/node:24-slim + org.opencontainers.image.base.digest: sha256:c0753125a3789977aefe869cbebccf70e3cfd7ea84ca48547458f02e4f1d7146 org.opencontainers.image.stage: install - org.opencontainers.image.base.digest: sha256:0e0ff40c39bc087845bfb27465a0df4ea419520094bc35842ff83dd8cbe6f9b6 + org.opencontainers.image.base.name: docker.io/node:26.8.1-slim workdir: /app/ run: + - npm install -g corepack@0.36.0 - corepack enable pnpm - - pnpm install --prod --frozen-lockfile --filter=@openstatus/workflows... --loglevel debug + - pnpm install --frozen-lockfile --filter=@openstatus/workflows... --loglevel debug cache: - target: /root/.local/share/pnpm/store bind: @@ -81,6 +82,8 @@ effective: | source: packages/utils/package.json - target: packages/tsconfig/package.json source: packages/tsconfig/package.json + - target: packages/test-utils/package.json + source: packages/test-utils/package.json - target: packages/tinybird/package.json source: packages/tinybird/package.json - target: packages/upstash/package.json @@ -93,12 +96,12 @@ effective: | source: packages/status-fetcher/package.json build: fromImage: - path: denoland/deno - digest: sha256:fa335acdf6b72106eda2cb6a8cb5f4187e7630e357467489db4b2e7352d5e432 + path: node + digest: sha256:c0753125a3789977aefe869cbebccf70e3cfd7ea84ca48547458f02e4f1d7146 label: - org.opencontainers.image.base.name: docker.io/denoland/deno:2.9.7 + org.opencontainers.image.base.digest: sha256:c0753125a3789977aefe869cbebccf70e3cfd7ea84ca48547458f02e4f1d7146 org.opencontainers.image.stage: build - org.opencontainers.image.base.digest: sha256:fa335acdf6b72106eda2cb6a8cb5f4187e7630e357467489db4b2e7352d5e432 + org.opencontainers.image.base.name: docker.io/node:26.8.1-slim workdir: /app/apps/workflows env: NODE_ENV: production @@ -119,41 +122,49 @@ effective: | - /app/packages target: /app/packages run: - - mkdir -p /tmp/sc - - deno eval 'const p="/app/node_modules/.pnpm";const v=(x)=>{for(const e of Deno.readDirSync(p))if(e.name.startsWith(x+"@"))return e.name.slice(x.length+1).split("_")[0]};Deno.writeTextFileSync("/tmp/sc/package.json",JSON.stringify({dependencies:{"@libsql/client":v("@libsql+client"),libsql:v("libsql"),"@google-cloud/tasks":v("@google-cloud+tasks")}}))' - - cd /tmp/sc && deno install --node-modules-dir=auto --allow-scripts - - cd /app/apps/workflows && deno bundle --platform=deno --minify --sloppy-imports --node-modules-dir=manual --external '@libsql/client' --external 'libsql' --external '@google-cloud/tasks' --output /tmp/sc/_serve.bundle.mjs src/serve.ts - - cd /tmp/sc && deno compile --no-check -A --node-modules-dir=manual --output /app/apps/workflows/app /tmp/sc/_serve.bundle.mjs - - mkdir -p /app/data + - ./node_modules/.bin/tsdown + - mkdir -p /tmp/sc /app/data + - node scripts/sidecar-package.mjs /tmp/sc/package.json + - npm install --prefix /tmp/sc --omit=dev --no-package-lock --no-audit --no-fund fromImage: - host: registry.access.redhat.com - port: 443 - path: hi/curl - digest: sha256:848b81ab5d5e55371d7193fd4f1ea7b605d14dbb039344ffad34a4c1f0d880f4 + path: node + digest: sha256:c0753125a3789977aefe869cbebccf70e3cfd7ea84ca48547458f02e4f1d7146 label: org.opencontainers.image.source: https://github.com/openstatusHQ/openstatus - org.opencontainers.image.base.digest: sha256:848b81ab5d5e55371d7193fd4f1ea7b605d14dbb039344ffad34a4c1f0d880f4 - io.dofigen.version: 2.8.0 - org.opencontainers.image.description: Background job processing and probe scheduling for OpenStatus - org.opencontainers.image.authors: OpenStatus Team org.opencontainers.image.title: OpenStatus Workflows org.opencontainers.image.vendor: OpenStatus + io.dofigen.version: 2.8.0 + org.opencontainers.image.authors: OpenStatus Team + org.opencontainers.image.description: Background job processing and probe scheduling for OpenStatus + org.opencontainers.image.base.digest: sha256:c0753125a3789977aefe869cbebccf70e3cfd7ea84ca48547458f02e4f1d7146 + org.opencontainers.image.base.name: docker.io/node:26.8.1-slim user: user: '1000' group: '1000' workdir: /app/ + env: + NODE_ENV: production copy: - fromBuilder: build paths: - - /app/apps/workflows/app - target: /app/apps/workflows/ + - /app/apps/workflows/dist + target: /app/apps/workflows/dist + chmod: '555' + - fromBuilder: build + paths: + - /tmp/sc/node_modules + target: /app/apps/workflows/node_modules chmod: '555' - fromBuilder: build paths: - /app/data target: /app/data + root: + run: + - apt-get update && apt-get install -y --no-install-recommends curl ca-certificates && rm -rf /var/lib/apt/lists/* entrypoint: - - /app/apps/workflows/app + - node + - /app/apps/workflows/dist/serve.mjs expose: - port: 3000 healthcheck: @@ -166,15 +177,11 @@ images: docker.io: library: node: - 24-slim: - digest: sha256:0e0ff40c39bc087845bfb27465a0df4ea419520094bc35842ff83dd8cbe6f9b6 - denoland: - deno: - 2.9.7: - digest: sha256:fa335acdf6b72106eda2cb6a8cb5f4187e7630e357467489db4b2e7352d5e432 + 26.8.1-slim: + digest: sha256:c0753125a3789977aefe869cbebccf70e3cfd7ea84ca48547458f02e4f1d7146 resources: dofigen.yml: - hash: bb33793c4b91222bc9d01de4911a4150c33290039ddf20f78f594ac96d3677e1 + hash: 31d39f4cb1231466bc802222e029bfad8f7447f8e35013f5a222c3cf5499c10b content: | ignore: - node_modules @@ -190,7 +197,7 @@ resources: - /packages/tracker builders: install: - fromImage: node:24-slim + fromImage: node:26.8.1-slim workdir: /app/ labels: org.opencontainers.image.stage: install @@ -222,22 +229,25 @@ resources: - packages/subscriptions/package.json - packages/utils/package.json - packages/tsconfig/package.json + - packages/test-utils/package.json - packages/tinybird/package.json - packages/upstash/package.json - packages/theme-store/package.json - packages/locales/package.json - packages/status-fetcher/package.json - # Install dependencies (scoped to the workflows subgraph so node_modules — and the binary that embeds it — stays small) + # Install dependencies scoped to the workflows subgraph (dev included: the build stage needs tsdown) run: + # Node 25+ no longer bundles corepack + - npm install -g corepack@0.36.0 - corepack enable pnpm - - pnpm install --prod --frozen-lockfile --filter=@openstatus/workflows... --loglevel debug + - pnpm install --frozen-lockfile --filter=@openstatus/workflows... --loglevel debug cache: - /root/.local/share/pnpm/store - # Stage 4: Build application (compile to binary with Deno) + # Stage 4: Bundle the app with tsdown into dist/ build: - fromImage: denoland/deno:2.9.7 + fromImage: node:26.8.1-slim workdir: /app/apps/workflows labels: org.opencontainers.image.stage: build @@ -254,21 +264,22 @@ resources: target: /app/packages env: NODE_ENV: production - # Bundle the app to self-contained JS, EXTERNALISING the 3 packages that can't be inlined: - # @libsql/client + libsql (native .node loader) and @google-cloud/tasks (google-proto-files reads - # files via CJS __dirname). They ship as a minimal side-car node_modules (deno install, ~46MB, - # platform-aware so Fly amd64 works) instead of embedding all node_modules. Binary ~146MB vs ~360MB. + # Everything is inlined except the 3 packages that can't be (see tsdown.config.ts). They ship as + # a minimal side-car node_modules, installed by npm on the build platform so Fly amd64 gets the + # right libsql native binary, at the exact versions pnpm resolved. run: - - mkdir -p /tmp/sc - - deno eval 'const p="/app/node_modules/.pnpm";const v=(x)=>{for(const e of Deno.readDirSync(p))if(e.name.startsWith(x+"@"))return e.name.slice(x.length+1).split("_")[0]};Deno.writeTextFileSync("/tmp/sc/package.json",JSON.stringify({dependencies:{"@libsql/client":v("@libsql+client"),libsql:v("libsql"),"@google-cloud/tasks":v("@google-cloud+tasks")}}))' - - cd /tmp/sc && deno install --node-modules-dir=auto --allow-scripts - - cd /app/apps/workflows && deno bundle --platform=deno --minify --sloppy-imports --node-modules-dir=manual --external '@libsql/client' --external 'libsql' --external '@google-cloud/tasks' --output /tmp/sc/_serve.bundle.mjs src/serve.ts - - cd /tmp/sc && deno compile --no-check -A --node-modules-dir=manual --output /app/apps/workflows/app /tmp/sc/_serve.bundle.mjs - - mkdir -p /app/data + - ./node_modules/.bin/tsdown + - mkdir -p /tmp/sc /app/data + - node scripts/sidecar-package.mjs /tmp/sc/package.json + - npm install --prefix /tmp/sc --omit=dev --no-package-lock --no-audit --no-fund - # Runtime: distroless glibc base that already ships curl + CA certs (Deno's HTTPS works on it; no apt needed). - # Pinned to the multi-arch index digest of hi/curl:latest — dofigen mis-resolves this registry's tag to an unpullable digest. - fromImage: registry.access.redhat.com/hi/curl@sha256:848b81ab5d5e55371d7193fd4f1ea7b605d14dbb039344ffad34a4c1f0d880f4 + # Runtime stage: Node plus curl (healthcheck) and the system CA store, which libsql's native + # TLS reads — Node's own fetch bundles its CAs, the addon does not. + fromImage: node:26.8.1-slim + root: + run: + # One chained command: dofigen emits a heredoc RUN, which doesn't stop on a failed line + - apt-get update && apt-get install -y --no-install-recommends curl ca-certificates && rm -rf /var/lib/apt/lists/* workdir: /app/ # Metadata labels @@ -279,11 +290,18 @@ resources: org.opencontainers.image.vendor: OpenStatus org.opencontainers.image.authors: OpenStatus Team - # Copy the self-contained compiled binary (native addon embedded by deno compile) + env: + NODE_ENV: production + + # The bundle resolves its externals from the side-car node_modules one level up from dist/ copy: - fromBuilder: build - source: /app/apps/workflows/app - target: /app/apps/workflows/ + source: /app/apps/workflows/dist + target: /app/apps/workflows/dist + chmod: "555" + - fromBuilder: build + source: /tmp/sc/node_modules + target: /app/apps/workflows/node_modules chmod: "555" - fromBuilder: build source: /app/data @@ -292,7 +310,6 @@ resources: user: "1000:1000" expose: "3000" - # Health check — exec form (JSON array) because the base has no shell for CMD-SHELL healthcheck: interval: 1m timeout: 10s @@ -300,4 +317,4 @@ resources: retries: 3 cmd: '["curl", "-f", "http://localhost:3000/ping"]' - entrypoint: /app/apps/workflows/app + entrypoint: ["node", "/app/apps/workflows/dist/serve.mjs"] diff --git a/apps/workflows/dofigen.yml b/apps/workflows/dofigen.yml index e67937e84..39426b38f 100644 --- a/apps/workflows/dofigen.yml +++ b/apps/workflows/dofigen.yml @@ -12,7 +12,7 @@ ignore: - /packages/tracker builders: install: - fromImage: node:24-slim + fromImage: node:26.8.1-slim workdir: /app/ labels: org.opencontainers.image.stage: install @@ -44,22 +44,25 @@ builders: - packages/subscriptions/package.json - packages/utils/package.json - packages/tsconfig/package.json + - packages/test-utils/package.json - packages/tinybird/package.json - packages/upstash/package.json - packages/theme-store/package.json - packages/locales/package.json - packages/status-fetcher/package.json - # Install dependencies (scoped to the workflows subgraph so node_modules — and the binary that embeds it — stays small) + # Install dependencies scoped to the workflows subgraph (dev included: the build stage needs tsdown) run: + # Node 25+ no longer bundles corepack + - npm install -g corepack@0.36.0 - corepack enable pnpm - - pnpm install --prod --frozen-lockfile --filter=@openstatus/workflows... --loglevel debug + - pnpm install --frozen-lockfile --filter=@openstatus/workflows... --loglevel debug cache: - /root/.local/share/pnpm/store - # Stage 4: Build application (compile to binary with Deno) + # Stage 4: Bundle the app with tsdown into dist/ build: - fromImage: denoland/deno:2.9.7 + fromImage: node:26.8.1-slim workdir: /app/apps/workflows labels: org.opencontainers.image.stage: build @@ -76,21 +79,22 @@ builders: target: /app/packages env: NODE_ENV: production - # Bundle the app to self-contained JS, EXTERNALISING the 3 packages that can't be inlined: - # @libsql/client + libsql (native .node loader) and @google-cloud/tasks (google-proto-files reads - # files via CJS __dirname). They ship as a minimal side-car node_modules (deno install, ~46MB, - # platform-aware so Fly amd64 works) instead of embedding all node_modules. Binary ~146MB vs ~360MB. + # Everything is inlined except the 3 packages that can't be (see tsdown.config.ts). They ship as + # a minimal side-car node_modules, installed by npm on the build platform so Fly amd64 gets the + # right libsql native binary, at the exact versions pnpm resolved. run: - - mkdir -p /tmp/sc - - deno eval 'const p="/app/node_modules/.pnpm";const v=(x)=>{for(const e of Deno.readDirSync(p))if(e.name.startsWith(x+"@"))return e.name.slice(x.length+1).split("_")[0]};Deno.writeTextFileSync("/tmp/sc/package.json",JSON.stringify({dependencies:{"@libsql/client":v("@libsql+client"),libsql:v("libsql"),"@google-cloud/tasks":v("@google-cloud+tasks")}}))' - - cd /tmp/sc && deno install --node-modules-dir=auto --allow-scripts - - cd /app/apps/workflows && deno bundle --platform=deno --minify --sloppy-imports --node-modules-dir=manual --external '@libsql/client' --external 'libsql' --external '@google-cloud/tasks' --output /tmp/sc/_serve.bundle.mjs src/serve.ts - - cd /tmp/sc && deno compile --no-check -A --node-modules-dir=manual --output /app/apps/workflows/app /tmp/sc/_serve.bundle.mjs - - mkdir -p /app/data + - ./node_modules/.bin/tsdown + - mkdir -p /tmp/sc /app/data + - node scripts/sidecar-package.mjs /tmp/sc/package.json + - npm install --prefix /tmp/sc --omit=dev --no-package-lock --no-audit --no-fund -# Runtime: distroless glibc base that already ships curl + CA certs (Deno's HTTPS works on it; no apt needed). -# Pinned to the multi-arch index digest of hi/curl:latest — dofigen mis-resolves this registry's tag to an unpullable digest. -fromImage: registry.access.redhat.com/hi/curl@sha256:848b81ab5d5e55371d7193fd4f1ea7b605d14dbb039344ffad34a4c1f0d880f4 +# Runtime stage: Node plus curl (healthcheck) and the system CA store, which libsql's native +# TLS reads — Node's own fetch bundles its CAs, the addon does not. +fromImage: node:26.8.1-slim +root: + run: + # One chained command: dofigen emits a heredoc RUN, which doesn't stop on a failed line + - apt-get update && apt-get install -y --no-install-recommends curl ca-certificates && rm -rf /var/lib/apt/lists/* workdir: /app/ # Metadata labels @@ -101,11 +105,18 @@ labels: org.opencontainers.image.vendor: OpenStatus org.opencontainers.image.authors: OpenStatus Team -# Copy the self-contained compiled binary (native addon embedded by deno compile) +env: + NODE_ENV: production + +# The bundle resolves its externals from the side-car node_modules one level up from dist/ copy: - fromBuilder: build - source: /app/apps/workflows/app - target: /app/apps/workflows/ + source: /app/apps/workflows/dist + target: /app/apps/workflows/dist + chmod: "555" + - fromBuilder: build + source: /tmp/sc/node_modules + target: /app/apps/workflows/node_modules chmod: "555" - fromBuilder: build source: /app/data @@ -114,7 +125,6 @@ copy: user: "1000:1000" expose: "3000" -# Health check — exec form (JSON array) because the base has no shell for CMD-SHELL healthcheck: interval: 1m timeout: 10s @@ -122,4 +132,4 @@ healthcheck: retries: 3 cmd: '["curl", "-f", "http://localhost:3000/ping"]' -entrypoint: /app/apps/workflows/app +entrypoint: ["node", "/app/apps/workflows/dist/serve.mjs"] diff --git a/apps/workflows/package.json b/apps/workflows/package.json index 46624fca6..2c52a4e10 100644 --- a/apps/workflows/package.json +++ b/apps/workflows/package.json @@ -1,13 +1,15 @@ { "name": "@openstatus/workflows", "scripts": { - "check": "deno check --sloppy-imports src/serve.ts", - "dev": "NODE_ENV=development deno run -A --watch src/serve.ts", - "start": "NODE_ENV=production deno run -A src/serve.ts", - "test": "NODE_ENV=test RESEND_API_KEY=test-key CRON_SECRET=test-secret deno test --parallel -A --no-check --sloppy-imports" + "check": "tsc --noEmit", + "dev": "NODE_ENV=development node --import @oxc-node/core/register --watch src/serve.ts", + "start": "NODE_ENV=production node dist/serve.mjs", + "test": "NODE_ENV=test RESEND_API_KEY=test-key CRON_SECRET=test-secret deno test --parallel -A --no-check --sloppy-imports", + "build": "tsdown" }, "dependencies": { "@google-cloud/tasks": "catalog:", + "@hono/node-server": "catalog:", "@libsql/client": "catalog:", "@logtape/logtape": "catalog:", "@logtape/otel": "catalog:", @@ -43,7 +45,7 @@ "@opentelemetry/resources": "catalog:", "@opentelemetry/sdk-logs": "catalog:", "@opentelemetry/semantic-conventions": "catalog:", - "@sentry/deno": "catalog:", + "@sentry/node": "catalog:", "@slack/web-api": "catalog:", "@upstash/qstash": "catalog:", "drizzle-orm": "catalog:", @@ -55,6 +57,9 @@ "devDependencies": { "@openstatus/test-utils": "workspace:*", "@openstatus/tsconfig": "workspace:*", + "@oxc-node/core": "catalog:", + "@types/node": "catalog:", + "tsdown": "catalog:", "typescript": "catalog:" } } diff --git a/apps/workflows/scripts/sidecar-package.mjs b/apps/workflows/scripts/sidecar-package.mjs new file mode 100644 index 000000000..343735501 --- /dev/null +++ b/apps/workflows/scripts/sidecar-package.mjs @@ -0,0 +1,61 @@ +// Writes the package.json for the side-car node_modules that ships next to the +// bundle: the packages tsdown leaves external, pinned to the versions pnpm +// installed so the image runs what was tested. Their transitive dependencies +// are pinned too, through npm `overrides`, by walking pnpm's install. +import { readFileSync, realpathSync, writeFileSync } from "node:fs"; +import { findPackageJSON } from "node:module"; + +const [outFile] = process.argv.slice(2); +if (!outFile) throw new Error("usage: sidecar-package.mjs "); + +const read = (path) => JSON.parse(readFileSync(path, "utf8")); +const workflows = new URL("../package.json", import.meta.url); +// Resolve from real paths: pnpm only links a package's deps inside its store dir. +const resolve = (name, from) => realpathSync(findPackageJSON(name, from)); + +const externals = ["@libsql/client", "libsql", "@google-cloud/tasks"]; +const versions = new Map(); +const seen = new Set(); + +function walk(path) { + if (seen.has(path)) return; + seen.add(path); + const pkg = read(path); + const pinned = versions.get(pkg.name); + if (pinned && pinned !== pkg.version) { + throw new Error( + `${pkg.name} resolves to both ${pinned} and ${pkg.version}; a flat override can't pin both`, + ); + } + versions.set(pkg.name, pkg.version); + for (const name of Object.keys(pkg.dependencies ?? {})) { + walk(resolve(name, path)); + } + // Platform binaries: pnpm installs only the build platform's, the rest don't resolve. + for (const name of Object.keys(pkg.optionalDependencies ?? {})) { + let dep; + try { + dep = resolve(name, path); + } catch { + continue; + } + walk(dep); + } +} + +const libsqlClient = resolve("@libsql/client", workflows); +walk(libsqlClient); +walk(resolve("libsql", libsqlClient)); +walk(resolve("@google-cloud/tasks", workflows)); + +const dependencies = {}; +const overrides = {}; +for (const [name, version] of [...versions].sort(([a], [b]) => + a.localeCompare(b), +)) { + (externals.includes(name) ? dependencies : overrides)[name] = version; +} +writeFileSync( + outFile, + `${JSON.stringify({ private: true, dependencies, overrides }, null, 2)}\n`, +); diff --git a/apps/workflows/src/checker/outbox.ts b/apps/workflows/src/checker/outbox.ts index d16f337ac..64bcef2c6 100644 --- a/apps/workflows/src/checker/outbox.ts +++ b/apps/workflows/src/checker/outbox.ts @@ -16,7 +16,7 @@ import { selectNotificationSchema, } from "@openstatus/db/src/schema"; import { withBusyRetry } from "@openstatus/services"; -import * as Sentry from "@sentry/deno"; +import * as Sentry from "@sentry/node"; import { Effect, Exit, Queue } from "effect"; import { env } from "../env"; diff --git a/apps/workflows/src/cron/index.ts b/apps/workflows/src/cron/index.ts index c00dbb5c2..d09fc114a 100644 --- a/apps/workflows/src/cron/index.ts +++ b/apps/workflows/src/cron/index.ts @@ -1,5 +1,5 @@ import { monitorPeriodicitySchema } from "@openstatus/db/src/schema/constants"; -import * as Sentry from "@sentry/deno"; +import * as Sentry from "@sentry/node"; import { Effect, Schedule } from "effect"; import { Hono } from "hono"; diff --git a/apps/workflows/src/cron/outbox.ts b/apps/workflows/src/cron/outbox.ts index 8e0d2b5fb..f5999e617 100644 --- a/apps/workflows/src/cron/outbox.ts +++ b/apps/workflows/src/cron/outbox.ts @@ -6,7 +6,7 @@ import { notificationTrigger, } from "@openstatus/db/src/schema"; import { withBusyRetry } from "@openstatus/services"; -import * as Sentry from "@sentry/deno"; +import * as Sentry from "@sentry/node"; import { drainUntilEmpty, sweepExpiredOutbox } from "../checker/outbox"; diff --git a/apps/workflows/src/cron/scheduler.ts b/apps/workflows/src/cron/scheduler.ts index ccda55f92..fbc326d65 100644 --- a/apps/workflows/src/cron/scheduler.ts +++ b/apps/workflows/src/cron/scheduler.ts @@ -1,5 +1,5 @@ import { getLogger } from "@logtape/logtape"; -import * as Sentry from "@sentry/deno"; +import * as Sentry from "@sentry/node"; import { Effect, Fiber, Schedule } from "effect"; import { runIncidentRemindersTick } from "./incident-reminders"; diff --git a/apps/workflows/src/cron/status-drift.ts b/apps/workflows/src/cron/status-drift.ts index d936a55ee..1dccaac44 100644 --- a/apps/workflows/src/cron/status-drift.ts +++ b/apps/workflows/src/cron/status-drift.ts @@ -3,7 +3,7 @@ import { db, sql } from "@openstatus/db"; import type { MonitorStatus } from "@openstatus/db/src/schema"; import { monitor, monitorStatusTable } from "@openstatus/db/src/schema"; import { withBusyRetry } from "@openstatus/services"; -import * as Sentry from "@sentry/deno"; +import * as Sentry from "@sentry/node"; import { triggerNotifications } from "../checker/alerting"; import { enqueueOutbox } from "../checker/outbox"; diff --git a/apps/workflows/src/index.ts b/apps/workflows/src/index.ts index 1597165cf..55b5085bd 100644 --- a/apps/workflows/src/index.ts +++ b/apps/workflows/src/index.ts @@ -16,7 +16,7 @@ import { } from "@opentelemetry/sdk-logs"; import { ATTR_SERVICE_NAME } from "@opentelemetry/semantic-conventions"; import { ATTR_DEPLOYMENT_ENVIRONMENT_NAME } from "@opentelemetry/semantic-conventions/incubating"; -import * as Sentry from "@sentry/deno"; +import * as Sentry from "@sentry/node"; import { Hono } from "hono"; import { showRoutes } from "hono/dev"; import { requestId } from "hono/request-id"; @@ -66,10 +66,10 @@ function shouldSample(event: Record): boolean { return Math.random() < 0.2; } -// Build the LoggerProvider with a static OTLP exporter import so `deno bundle` -// includes it. Letting @logtape/otel create the exporter triggers a dynamic -// import of a bare specifier, which the compiled --node-modules-dir=none binary -// can't resolve. +// Build the LoggerProvider with a static OTLP exporter import so tsdown +// bundles it. Letting @logtape/otel create the exporter triggers a dynamic +// import of a bare specifier, which the image can't resolve: its side-car +// node_modules only carries the tsdown externals. const loggerProvider = new LoggerProvider({ resource: resourceFromAttributes({ [ATTR_SERVICE_NAME]: "openstatus-workflows", diff --git a/apps/workflows/src/lib/sentry.ts b/apps/workflows/src/lib/sentry.ts index 7afeb72c3..a404cf3a0 100644 --- a/apps/workflows/src/lib/sentry.ts +++ b/apps/workflows/src/lib/sentry.ts @@ -1,6 +1,6 @@ import { FetchError } from "@openstatus/status-fetcher"; -import * as Sentry from "@sentry/deno"; -import type { SeverityLevel } from "@sentry/deno"; +import * as Sentry from "@sentry/node"; +import type { SeverityLevel } from "@sentry/node"; import { env } from "../env"; @@ -8,6 +8,9 @@ Sentry.init({ dsn: env().SENTRY_DSN, environment: env().NODE_ENV, tracesSampleRate: 0, + // Tracing is off; the ESM hooks only feed auto-instrumentation and use the + // deprecated module.register() (DEP0205). + registerEsmLoaderHooks: false, }); export function runSentryCron(monitorSlug: string): { diff --git a/apps/workflows/src/serve.ts b/apps/workflows/src/serve.ts index 12c5df1ea..5a2b8408d 100644 --- a/apps/workflows/src/serve.ts +++ b/apps/workflows/src/serve.ts @@ -1,5 +1,4 @@ -/// - +import { serve } from "@hono/node-server"; import { getLogger } from "@logtape/logtape"; import { shutdownOutbox, startOutboxConsumer } from "./checker/outbox"; @@ -19,13 +18,13 @@ logger.info("Starting server", { startOutboxConsumer(); startScheduler(); -const server = Deno.serve({ port: PORT }, app.fetch); +const server = serve({ fetch: app.fetch, port: PORT }); -Deno.addSignalListener("SIGTERM", () => { +process.on("SIGTERM", () => { void (async () => { logger.info("SIGTERM received, releasing outbox claims"); await stopScheduler(); await shutdownOutbox(); - await server.shutdown(); + server.close(); })(); }); diff --git a/apps/workflows/start.sh b/apps/workflows/start.sh index e24344a23..6739a1bb1 100644 --- a/apps/workflows/start.sh +++ b/apps/workflows/start.sh @@ -2,8 +2,8 @@ set -e echo "Running database migrations..." -cd /app/packages/db && deno run -A src/migrate.mts +cd /app/packages/db && node src/migrate.mts echo "Starting workflows service..." cd /app/apps/workflows -exec /app/apps/workflows/app +exec node /app/apps/workflows/dist/serve.mjs diff --git a/apps/workflows/tsconfig.json b/apps/workflows/tsconfig.json index a627827e3..455f0cda3 100644 --- a/apps/workflows/tsconfig.json +++ b/apps/workflows/tsconfig.json @@ -1,10 +1,17 @@ { "extends": "@openstatus/tsconfig/base.json", "compilerOptions": { - "jsx": "react-jsx", - "jsxImportSource": "hono/jsx", "module": "ESNext", "target": "ESNext", "moduleResolution": "bundler" - } + }, + "exclude": [ + "node_modules", + "**/*.test.ts", + "**/*.test.tsx", + "**/__tests__/**", + "**/test/**", + "**/*.mock.ts", + "**/test-preload.ts" + ] } diff --git a/apps/workflows/tsdown.config.ts b/apps/workflows/tsdown.config.ts new file mode 100644 index 000000000..c4aebd513 --- /dev/null +++ b/apps/workflows/tsdown.config.ts @@ -0,0 +1,21 @@ +import { defineConfig } from "tsdown"; + +// Inline everything except the packages that can't be bundled: libsql loads a +// native .node addon, and google-proto-files (under @google-cloud/tasks) reads +// files via CJS __dirname. Those three ship as a side-car node_modules. +const external = ["@libsql/client", "libsql", "@google-cloud/tasks"]; + +export default defineConfig({ + entry: ["src/serve.ts"], + format: "esm", + platform: "node", + target: "node26", + fixedExtension: true, + dts: false, + deps: { + neverBundle: external, + alwaysBundle: (id) => + !external.some((e) => id === e || id.startsWith(`${e}/`)), + onlyBundle: false, + }, +}); diff --git a/deno.lock b/deno.lock index 5cb6c59e7..a0899fd5e 100644 --- a/deno.lock +++ b/deno.lock @@ -72,7 +72,8 @@ "packageJson": { "dependencies": [ "npm:@jsr/std__expect@^1.0.19", - "npm:@jsr/std__testing@^1.0.19" + "npm:@jsr/std__testing@^1.0.19", + "npm:@jsr/std__yaml@^1.0.9" ] } }, diff --git a/devbox.json b/devbox.json index a8f057b3a..78ea1f227 100644 --- a/devbox.json +++ b/devbox.json @@ -2,7 +2,7 @@ "$schema": "https://raw.githubusercontent.com/jetify-com/devbox/0.10.6/.schema/devbox.schema.json", "packages": [ "turso-cli@1.0.15", - "nodejs@24.12.0", + "nodejs@26.8.1", "deno@2.9.5", "bun@1.3.13", "sqld@0.24.32" diff --git a/devbox.lock b/devbox.lock index d8c146e44..3c276e53d 100644 --- a/devbox.lock +++ b/devbox.lock @@ -115,84 +115,42 @@ "last_modified": "2025-11-05T16:44:39Z", "resolved": "github:NixOS/nixpkgs/ffcdcf99d65c61956d882df249a9be53e5902ea5?lastModified=1762361079&narHash=sha256-lz718rr1BDpZBYk7%2BG8cE6wee3PiBUpn8aomG%2FvLLiY%3D" }, - "nodejs@24.12.0": { - "last_modified": "2025-12-27T12:56:01Z", + "nodejs@26.8.1": { + "last_modified": "2026-08-27T23:47:58Z", "plugin_version": "0.0.2", - "resolved": "github:NixOS/nixpkgs/3edc4a30ed3903fdf6f90c837f961fa6b49582d1#nodejs_24", + "resolved": "github:NixOS/nixpkgs/7d1a7c21a4c00ea653fc7ed5c083d261340f185f#nodejs_26", "source": "devbox-search", - "version": "24.12.0", + "version": "26.8.1", "systems": { "aarch64-darwin": { "outputs": [ { "name": "out", - "path": "/nix/store/b1h0af3yb3z9jz048phclcqw6ihiww67-nodejs-24.12.0", + "path": "/nix/store/szf4xdpd4znb3hzprlyivq6nd507hyas-nodejs-26.8.1", "default": true - }, - { - "name": "dev", - "path": "/nix/store/ksn4nj0nbp0ikmh4scy9x4biqqdswwhy-nodejs-24.12.0-dev" - }, - { - "name": "libv8", - "path": "/nix/store/3x72bm7l30ib7l7cximrp0sklcq8jqwy-nodejs-24.12.0-libv8" } ], - "store_path": "/nix/store/b1h0af3yb3z9jz048phclcqw6ihiww67-nodejs-24.12.0" + "store_path": "/nix/store/szf4xdpd4znb3hzprlyivq6nd507hyas-nodejs-26.8.1" }, "aarch64-linux": { "outputs": [ { "name": "out", - "path": "/nix/store/9jfsyhcr9c6rpm94lrkibv75jkggvwss-nodejs-24.12.0", - "default": true - }, - { - "name": "dev", - "path": "/nix/store/7izhv9x1yvsmyrhmmyps6bxjjclajm80-nodejs-24.12.0-dev" - }, - { - "name": "libv8", - "path": "/nix/store/8hl74pnc1qyz68rg10ixw7fz32hcxqkk-nodejs-24.12.0-libv8" - } - ], - "store_path": "/nix/store/9jfsyhcr9c6rpm94lrkibv75jkggvwss-nodejs-24.12.0" - }, - "x86_64-darwin": { - "outputs": [ - { - "name": "out", - "path": "/nix/store/cj1243xswxvnwgifyiyg8axhn0r2vl48-nodejs-24.12.0", + "path": "/nix/store/s3azam32y3bmwd6b2zynqqg9yv99lnam-nodejs-26.8.1", "default": true - }, - { - "name": "dev", - "path": "/nix/store/kzia3rkaf9vwpn3py23q9v49jpjm6zx7-nodejs-24.12.0-dev" - }, - { - "name": "libv8", - "path": "/nix/store/xszji81xj7ghjhz7jk0jkhb84nfwyd8f-nodejs-24.12.0-libv8" } ], - "store_path": "/nix/store/cj1243xswxvnwgifyiyg8axhn0r2vl48-nodejs-24.12.0" + "store_path": "/nix/store/s3azam32y3bmwd6b2zynqqg9yv99lnam-nodejs-26.8.1" }, "x86_64-linux": { "outputs": [ { "name": "out", - "path": "/nix/store/9z1v3wyrxp6fpyzw21lcakd5w7aknzyc-nodejs-24.12.0", + "path": "/nix/store/ymlp5q9l64224b9jqpqwp298bgbjv7p9-nodejs-26.8.1", "default": true - }, - { - "name": "libv8", - "path": "/nix/store/5yvx909wlis13qd2y1ahwl7ij21ma69a-nodejs-24.12.0-libv8" - }, - { - "name": "dev", - "path": "/nix/store/7ll99p08gmpg0n048669p80zjibl5akr-nodejs-24.12.0-dev" } ], - "store_path": "/nix/store/9z1v3wyrxp6fpyzw21lcakd5w7aknzyc-nodejs-24.12.0" + "store_path": "/nix/store/ymlp5q9l64224b9jqpqwp298bgbjv7p9-nodejs-26.8.1" } } }, diff --git a/packages/ai/package.json b/packages/ai/package.json index ae9f8d21f..dea32ab20 100644 --- a/packages/ai/package.json +++ b/packages/ai/package.json @@ -12,7 +12,7 @@ } }, "scripts": { - "check": "deno check --sloppy-imports .", + "check": "tsc --noEmit", "test": "deno test --parallel -A --no-check --sloppy-imports", "lint": "oxlint", "tsc": "tsc --noEmit" diff --git a/packages/ai/tsconfig.json b/packages/ai/tsconfig.json index 230005c38..cbf5122bf 100644 --- a/packages/ai/tsconfig.json +++ b/packages/ai/tsconfig.json @@ -3,5 +3,14 @@ "include": ["src", "*.ts"], "compilerOptions": { "noEmit": true - } + }, + "exclude": [ + "node_modules", + "**/*.test.ts", + "**/*.test.tsx", + "**/__tests__/**", + "**/test/**", + "**/*.mock.ts", + "**/test-preload.ts" + ] } diff --git a/packages/analytics/package.json b/packages/analytics/package.json index 4c43a19cc..eddae2306 100644 --- a/packages/analytics/package.json +++ b/packages/analytics/package.json @@ -7,7 +7,7 @@ "author": "", "main": "src/index.ts", "scripts": { - "check": "deno check --sloppy-imports ." + "check": "tsc --noEmit" }, "dependencies": { "@openpanel/sdk": "catalog:", diff --git a/packages/analytics/tsconfig.json b/packages/analytics/tsconfig.json index 4b64f988a..7c76dbe70 100644 --- a/packages/analytics/tsconfig.json +++ b/packages/analytics/tsconfig.json @@ -1,7 +1,16 @@ { "$schema": "https://json.schemastore.org/tsconfig", "extends": "@openstatus/tsconfig/base.json", - "exclude": ["dist", "node_modules"], + "exclude": [ + "dist", + "node_modules", + "**/*.test.ts", + "**/*.test.tsx", + "**/__tests__/**", + "**/test/**", + "**/*.mock.ts", + "**/test-preload.ts" + ], "compilerOptions": { "outDir": "dist", "target": "es2021", diff --git a/packages/api/env.ts b/packages/api/env.ts index 4796312cc..091584e3b 100644 --- a/packages/api/env.ts +++ b/packages/api/env.ts @@ -1 +1,3 @@ -await Deno.copyFile("./.env.test", "./.env"); +import { copyFile } from "node:fs/promises"; + +await copyFile("./.env.test", "./.env"); diff --git a/packages/api/package.json b/packages/api/package.json index 4393ecb9d..a94373caa 100644 --- a/packages/api/package.json +++ b/packages/api/package.json @@ -11,9 +11,9 @@ "./src/*": "./src/*.ts" }, "scripts": { - "check": "deno check --sloppy-imports .", + "check": "tsc --noEmit", "test": "deno test --parallel -A --no-check --sloppy-imports --env-file=.env.test --import-map=test.importmap.json", - "env": "deno run -A env.ts" + "env": "node env.ts" }, "dependencies": { "@libsql/client": "catalog:", @@ -69,6 +69,7 @@ "@openstatus/tsconfig": "workspace:*", "@std/expect": "jsr:^1.0.19", "@std/testing": "jsr:^1.0.19", + "@types/node": "catalog:", "@types/react": "catalog:", "@types/react-dom": "catalog:", "typescript": "catalog:" diff --git a/packages/api/tsconfig.json b/packages/api/tsconfig.json index 48baa5cfe..314dbdf90 100644 --- a/packages/api/tsconfig.json +++ b/packages/api/tsconfig.json @@ -2,6 +2,17 @@ "extends": "@openstatus/tsconfig/base.json", "include": ["src", "*.ts"], "compilerOptions": { + "declaration": false, + "declarationMap": false, "resolveJsonModule": true - } + }, + "exclude": [ + "node_modules", + "**/*.test.ts", + "**/*.test.tsx", + "**/__tests__/**", + "**/test/**", + "**/*.mock.ts", + "**/test-preload.ts" + ] } diff --git a/packages/assertions/package.json b/packages/assertions/package.json index 55d553bca..e28403cb2 100644 --- a/packages/assertions/package.json +++ b/packages/assertions/package.json @@ -7,7 +7,7 @@ "author": "", "main": "src/index.ts", "scripts": { - "check": "deno check --sloppy-imports .", + "check": "tsc --noEmit", "test": "deno test --parallel -A --no-check --sloppy-imports" }, "dependencies": { diff --git a/packages/assertions/tsconfig.json b/packages/assertions/tsconfig.json index aed15c74a..9f7ee457b 100644 --- a/packages/assertions/tsconfig.json +++ b/packages/assertions/tsconfig.json @@ -1,4 +1,13 @@ { "extends": "@openstatus/tsconfig/base.json", - "include": ["src", "*.ts"] + "include": ["src", "*.ts"], + "exclude": [ + "node_modules", + "**/*.test.ts", + "**/*.test.tsx", + "**/__tests__/**", + "**/test/**", + "**/*.mock.ts", + "**/test-preload.ts" + ] } diff --git a/packages/db/Dockerfile b/packages/db/Dockerfile index 47fc955fc..8335b712f 100644 --- a/packages/db/Dockerfile +++ b/packages/db/Dockerfile @@ -3,10 +3,10 @@ # See https://github.com/lenra-io/dofigen # install -FROM node@sha256:0e0ff40c39bc087845bfb27465a0df4ea419520094bc35842ff83dd8cbe6f9b6 AS install +FROM node@sha256:c0753125a3789977aefe869cbebccf70e3cfd7ea84ca48547458f02e4f1d7146 AS install LABEL \ - org.opencontainers.image.base.digest="sha256:0e0ff40c39bc087845bfb27465a0df4ea419520094bc35842ff83dd8cbe6f9b6" \ - org.opencontainers.image.base.name="docker.io/node:24-slim" \ + org.opencontainers.image.base.digest="sha256:c0753125a3789977aefe869cbebccf70e3cfd7ea84ca48547458f02e4f1d7146" \ + org.opencontainers.image.base.name="docker.io/node:26.8.1-slim" \ org.opencontainers.image.stage="install" WORKDIR /app/ RUN \ @@ -21,22 +21,22 @@ RUN \ --mount=type=bind,target=packages/utils/package.json,source=packages/utils/package.json \ --mount=type=cache,target=/root/.local/share/pnpm/store,sharing=locked \ <=20'} peerDependencies: - hono: ^4 + hono: ^4 || ^5.0.0-0 '@hono/sentry@1.2.2': resolution: {integrity: sha512-027grZBrRGDPor8mRd+QOBcSpUlF07YrTp/WFDXZhbvWZ+1LrZdERUqcdg1gBGDUTanHhd9ucblpNNN6+V1bxg==} @@ -5251,6 +5311,9 @@ packages: '@jsr/std__testing@1.0.20': resolution: {integrity: sha512-DRCuU1u4OXD/AXBJ2q9IvMHthMSti1V1604b2+xa5WaAcm6Ghazk0ac1AsMwiAOXEpK0nfIXd1MD3ttT8LQ5Rw==, tarball: https://npm.jsr.io/~/11/@jsr/std__testing/1.0.20.tgz} + '@jsr/std__yaml@1.3.0': + resolution: {integrity: sha512-TuFbXlSy3nRuQuR8co2t7yzhv53ObdYZZidn8M958Glj6vBp5b7nxg7wTN8VTyCwC3vpbr6Mfdx9auNe/BMAVA==, tarball: https://npm.jsr.io/~/11/@jsr/std__yaml/1.3.0.tgz} + '@kobalte/core@0.13.14': resolution: {integrity: sha512-SBVB1lnvYbYt2x3t7fG0lo5fGLEbHQ6PII3NDPva8vD5e2v53On84ZVQiuIk0XI/55R4GQhden7Yem+R/XR8uQ==} peerDependencies: @@ -5682,6 +5745,95 @@ packages: resolution: {integrity: sha512-eSYWTm620tTk45EKSedaUL8MFYI8hW164hIXsgIHyxu3VobUB3fFCu5t0hQby6OoWRPsG1KkKUG2M5UadiLiVg==} engines: {node: '>=14'} + '@oxc-node/core-android-arm-eabi@0.1.5': + resolution: {integrity: sha512-68iEMQ/68tqrylwDHUrcaQF94npiochGUY/DXjapdPBUHNQbSITyIdQzIKG7h1Zdl64kFJLQzGvOS3+VRVvK+A==} + cpu: [arm] + os: [android] + + '@oxc-node/core-android-arm64@0.1.5': + resolution: {integrity: sha512-BtOm4fxiqCXr+NqA61q87/RXf+r3+N5G9V5SdvUth9ITg+Bbvc7hO+zuLTt9dN2CgfJpJLiJUhtywfOzxUumlw==} + cpu: [arm64] + os: [android] + + '@oxc-node/core-darwin-arm64@0.1.5': + resolution: {integrity: sha512-yxtk5TrauFJgDFskIyydpdUZPLJDgYobaOo2Mi1wTvMl5XjXu1VVMDJ9/H0ilKQ33oc+D4HWggY4f0fktZJJmg==} + cpu: [arm64] + os: [darwin] + + '@oxc-node/core-darwin-x64@0.1.5': + resolution: {integrity: sha512-r85OJm4Q/SlNx2G4zdojOTVKDa9GmxuM2MSephb6Xr4gZNgMrYk8SNmos/PSPr9pcZiLtJ1EXKK7PQhmrdk37A==} + cpu: [x64] + os: [darwin] + + '@oxc-node/core-freebsd-x64@0.1.5': + resolution: {integrity: sha512-IRCLepFhP1nR0bbQDR4UuaOnACbJbCfyu/l7Em3JBpuAyCy0+IcBCsbwgjabsXW72k4dbbtz0NE5NrJIOcxb2w==} + cpu: [x64] + os: [freebsd] + + '@oxc-node/core-linux-arm-gnueabihf@0.1.5': + resolution: {integrity: sha512-uCbqPCpjQF2sPah8jfMjw+wnV7HB0EYyeF/4JNAFDBQVCYqkYiTlZeKhvsThlKsYewdO5Cz8FGbF1HhQJC5XOQ==} + cpu: [arm] + os: [linux] + + '@oxc-node/core-linux-arm64-gnu@0.1.5': + resolution: {integrity: sha512-epy8YgOxhIiIEF2E/KKr5WhWmN7AONPGIYNqzbJpyXqH/fVKs2oZYfgoYGOjM794oT3AE4VGNAqwd6lMmmGO8g==} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@oxc-node/core-linux-arm64-musl@0.1.5': + resolution: {integrity: sha512-TCpCE9HJFh8czVjltJFxoAKT6rkDBuSsInODoUKrq7NKw3p6nuI6O8stMTxPnLm8658ZR0DDZiPxzp3pkjbYag==} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@oxc-node/core-linux-ppc64-gnu@0.1.5': + resolution: {integrity: sha512-u8EFVWAv5HydemPV0EZvHWjyL0ymRganYhwGYWpWBq6/qA5CoSyBZ2YcmCizhhvsjW5GjAcUHoTF0Y1naB7Zow==} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@oxc-node/core-linux-s390x-gnu@0.1.5': + resolution: {integrity: sha512-gdpOiodpzvRhSTntsSYcfcaADJPkMGPCY+RIABiQUU/inNLflUNNv5d2J0rXQEKswEJqC4SNQ2+qLdrcoOfzYw==} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@oxc-node/core-linux-x64-gnu@0.1.5': + resolution: {integrity: sha512-gggyg6BLiED3hsCtl1ShVjVNlPkHQ/p/SQna1ytsII24O29aoQH0iqSPzTSAw18Ew0luyHRYQZdarOX0bP/Vug==} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@oxc-node/core-linux-x64-musl@0.1.5': + resolution: {integrity: sha512-xXIByxOf/RTQJqRyHEz/aq5yRcuZjo4CNhrXaTzxPHEPL8fY/HKxi6F3p7joF6hvGDZP9XsRyGTUZDnedNPFvw==} + cpu: [x64] + os: [linux] + libc: [musl] + + '@oxc-node/core-openharmony-arm64@0.1.5': + resolution: {integrity: sha512-0JFa8khKtaAOODEtZEM/lW8HeZNE9m5v+wXrabTn2vpPmL2FPmqz9eT4tYFNNHRjw515gcsY5P7Id0dblwheaA==} + cpu: [arm64] + os: [openharmony] + + '@oxc-node/core-win32-arm64-msvc@0.1.5': + resolution: {integrity: sha512-Ev2gMd7SWPJ4Qwn3lb5Lcckq1qsB5++NAF2nOzDWlARNZN8QyMwt9uoLX3Sk2UunExP0Ux9V5VnOpdNEBziQEA==} + cpu: [arm64] + os: [win32] + + '@oxc-node/core-win32-ia32-msvc@0.1.5': + resolution: {integrity: sha512-2Di21C19W94ryPWYh7WYMwGfJH4hV4cgkJqH8U16CDMTN+QBVwzXRr8xwSxz3BiKOaPRsLMIfO2g4R7R2674WQ==} + cpu: [ia32] + os: [win32] + + '@oxc-node/core-win32-x64-msvc@0.1.5': + resolution: {integrity: sha512-ctl1jv+uLXDZKSJgkMDwDoVQulKQuLh6SIm6mcJK3XCo1+i3aou6ycOWNjgXX4lRfhFRmzC3Oo2Akf2NB06KLg==} + cpu: [x64] + os: [win32] + + '@oxc-node/core@0.1.5': + resolution: {integrity: sha512-DAL8opn0FPSH/wtsvOP1jZ8nPP0zOKdXP/8K3IziacsCMhluhhT8hTcSVVIKdAumpmcZ4xPei/0YJS6n91Ya/Q==} + '@oxc-parser/binding-android-arm-eabi@0.150.0': resolution: {integrity: sha512-oQef2Zu4Prz1KLKznz3HqZzU9uVoA5PMoDZuuLmqms7hKmKSAPzlaMnLllJq3t+rgKmfJJ2siPrpZfFrW06btw==} engines: {node: ^20.19.0 || >=22.12.0} @@ -5807,6 +5959,9 @@ packages: '@oxc-project/types@0.150.0': resolution: {integrity: sha512-rDS5/31E9HfPl/CIzGrn0DOlvBbXFseQ5URJ9sYMfstbKLD/c6Gm9vmRzRGDdAXyOIL4zmO37lc9RIwYqVruZw==} + '@oxc-project/types@0.153.0': + resolution: {integrity: sha512-jSNrRIyvPVLL5E1z/tS5zLAprosGz/FuI1PWwEOz59j3E4TTTzSpqKb/RDGqJhhW0QHYISGXxq582wGx6T11Tw==} + '@oxc-resolver/binding-android-arm-eabi@11.24.2': resolution: {integrity: sha512-y09e0L0SRI2OA2tUIrjBgoV3eH5hvUKXNkJqXmNo5V2WxIjyC7I7aJfRLMEVpA8yi95f90gFDvO0VMgrDw+vwA==} cpu: [arm] @@ -6384,6 +6539,9 @@ packages: '@protobufjs/utf8@1.1.2': resolution: {integrity: sha512-b1UQwcEZ4yCnMCD8DAL1VlbvBJE9/IX4FTIp7BG1xYpf29SLazLSrqUkj4w7Y5y7cCVP6E5tcqqcI0xemPkHug==} + '@quansync/fs@1.1.0': + resolution: {integrity: sha512-qAPG/t3HqML1TlN7sY/pTbEjzFVAKsMjNNMGheyDosro+kT4iw2KCUoHcVdmliwWjorm4elZbgNQyU2eD97eDg==} + '@radix-ui/number@1.1.3': resolution: {integrity: sha512-Road2bidD0uu/1BGDOWNdPI06g0lIRy6IF9GZcIrDK2KGItfor8IQwQa+yM2ERgHM1MmHxaxpTzk0/Jp42lNfA==} @@ -7084,6 +7242,105 @@ packages: '@react-email/ui@6.9.5': resolution: {integrity: sha512-UvE0yeSQCvdBm9BoEgvZ5vBi8abk02GbRE+4RQjjtxozy66D/O1GbUgK9RdB26+7EqQdOxdwkGLq5CMCiRO96A==} + '@rolldown/binding-android-arm-eabi@1.2.13': + resolution: {integrity: sha512-Vu/NICrgdRvUOeaJZIyB2FP63s6fO/4ud5l8dXmxDx9yDyn+mN0y+gsvsVviSli+T361Aqo0h46EuMoobrwawA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [android] + + '@rolldown/binding-android-arm64@1.2.13': + resolution: {integrity: sha512-6LEoIrGy5vvCd2boNnv2soPSUZj/PRvhp8R/t9KlX+mIZxThIE/ZqxrGXxmuNX0FuvQFvLdZr7S4HrYpYBVhEQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [android] + + '@rolldown/binding-darwin-arm64@1.2.13': + resolution: {integrity: sha512-CaJSRzlQ6Q+zkDMVA9OkVxBjVoPRCDEE3v0FSJ9kIQBgRjFApA3Si4ijhbNVUji7ZJt+PfO0e46qy4nPInHUPA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [darwin] + + '@rolldown/binding-darwin-x64@1.2.13': + resolution: {integrity: sha512-e4r340I5hxSzJkD3YJBxgzq/rJ3wwxwdLLb2FPt1Zpyem20nrQPT11DDEEkazwxaHumH8wpglsp/Ozgj/wT6mw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [darwin] + + '@rolldown/binding-freebsd-x64@1.2.13': + resolution: {integrity: sha512-hoBWDZ4/K/mJKVMbsgUd1jfpA2DiOrJ54mpegObU9TBHBBrtX4FLIXJJQRDPZ/2Dn91GvPob+Z2hwKJzf4E5SQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [freebsd] + + '@rolldown/binding-linux-arm-gnueabihf@1.2.13': + resolution: {integrity: sha512-oF9fZu3vTWnTiVAkDFxU9PJLt1nh20K9RRpuHsR7ZnE2L7XookBbDBVLojCVSLGbvtSlxeUNnjAj6DbNsKyzbg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@rolldown/binding-linux-arm64-gnu@1.2.13': + resolution: {integrity: sha512-DTpb/+jMiqWMPgphn4ksW1zW+XLJ6dJJ9FMvnIk9I291LhCctMPhMYJrP5t9p6C9qa3KXadDdUEZHBaPh3kNxQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-arm64-musl@1.2.13': + resolution: {integrity: sha512-xU+Kvv9N2A3SY35zFh1FWWNjxbadE/BKeqnkXe7vScU4GZsw757BlVmDLITZEq8DPpk6m7uoDYkK7P+x/uU5Ow==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@rolldown/binding-linux-ppc64-gnu@1.2.13': + resolution: {integrity: sha512-XMk/hnhh25oQ756Lclb1Q11h4ll0xC0Esa5w4Vzp+mAE2S3AGJgeM0SonrIgY8fHhjarb6hcoTuwoFG0VVP2ig==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-s390x-gnu@1.2.13': + resolution: {integrity: sha512-U1BoTHYmlywE2n3HUWjzZ+8EQsKr+OwpxYuDWaubx42tUL8QSL7nOqRz/NVHanibbFP3M1Rxuprz/wNcbMKjqw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-x64-gnu@1.2.13': + resolution: {integrity: sha512-J4cm80Pc6wEwV5AZ42o62sIQgDk+Y7R2s646jIwuvO+4V46QB58N4vVuMPWCBaNgursXLpmUWJV//WQAkPvHBQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-x64-musl@1.2.13': + resolution: {integrity: sha512-W0euZS40U4V6JkmEnVCIX9mcsL6cbXQ4X8c2w+MMOIjFg68TXUW+u3RT+2e1t1pv9mg/EP3pDtK3POXdQ7Rxyw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [musl] + + '@rolldown/binding-openharmony-arm64@1.2.13': + resolution: {integrity: sha512-do/AF/4n9MRshia3a5aXoy8xM2c6mgfT6D7H/dY6my5z+hfCIR4ab7mk4IRj++nsEQpKy39kd0DQnDq+r692tw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [openharmony] + + '@rolldown/binding-win32-arm64-msvc@1.2.13': + resolution: {integrity: sha512-6FtUf2cJZobutJdxnGYX8z0vtbgqU0b2v8vfBhkXb9wp2MppMFtkunE+IaOntyi01rkIQoPW2GHABtfzsC0BRQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [win32] + + '@rolldown/binding-win32-x64-msvc@1.2.13': + resolution: {integrity: sha512-UKhFaZTeTcbKrb3iU82m17jVcPEAoqDkCN5MBqgPyZSwp/cQmib21crTS0DdH7T1B7wZRWEI89YQAOJMet5yhg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [win32] + + '@rolldown/pluginutils@1.0.1': + resolution: {integrity: sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==} + '@rollup/plugin-commonjs@28.0.1': resolution: {integrity: sha512-+tNWdlWKbpB3WgBN7ijjYkq9X5uhjmcvyjEght4NmH5fAU++zfQzAJ6wumLS+dNcvwEZhKx2Z+skY8m7v0wGSA==} engines: {node: '>=16.0.0 || 14 >= 14.17'} @@ -7365,9 +7622,6 @@ packages: resolution: {integrity: sha512-ixm8NISFlPlEo3FjSaqmq4nnd13BRHoafwJ5MG+okCz6BKGZ1SexEggP42/QpGvDprUUHnfncG6WUMgcarr1zA==} engines: {node: '>=14.18'} - '@sentry/deno@10.75.1': - resolution: {integrity: sha512-fBWh7Tdx4B2SsCOeJ2QupO3LlgM99nnNDxI5pKlruJQijfragw2YidLQwcU0RGb/KE0IMvEyMgkpKxCF5cinng==} - '@sentry/feedback@10.75.1': resolution: {integrity: sha512-2P5G160I0bHW4jXTU5gJB4I6h/mamAdkK2oezyuE6+bwoXZ2JSh7+hug/FLhzIQMu92ZoyrP6ar/1v6TC33w+w==} engines: {node: '>=18'} @@ -8293,6 +8547,165 @@ packages: '@xtuc/long@4.2.2': resolution: {integrity: sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==} + '@yuku-codegen/binding-android-arm64@0.10.2': + resolution: {integrity: sha512-Oc2KInVkPfUjEB4PeV6X0NvIoyYTzDe/WmqFrTwBqKs6YKnz4A7XwWQvJTb7M0rLz9a6WgpUUiiS5QKaCi4O/g==} + cpu: [arm64] + os: [android] + deprecated: yuku-codegen is pure JavaScript since 0.14 + + '@yuku-codegen/binding-darwin-arm64@0.10.2': + resolution: {integrity: sha512-3H7eNPIHJndUJXZ4QUGBPq1zC6RGcUZfm7bymJes+/N7wTVWUn1bxZ9JcozYHpkWQNm9f23G8YWbaHD9aYH72A==} + cpu: [arm64] + os: [darwin] + deprecated: yuku-codegen is pure JavaScript since 0.14 + + '@yuku-codegen/binding-darwin-x64@0.10.2': + resolution: {integrity: sha512-AJOUpR2s3LF9AWiiub/Uyc/UoXNTWcq8hK3cVI6fUSXtwh34dG21J6hRuNlO6JVJFTo4o3ScVvZOrh/YFfUEAA==} + cpu: [x64] + os: [darwin] + deprecated: yuku-codegen is pure JavaScript since 0.14 + + '@yuku-codegen/binding-freebsd-x64@0.10.2': + resolution: {integrity: sha512-ms7DcZu87u5CiK/wMwvpKAvAmtaqKjCQIXNweMLypG6qbuiaOMQf1jpbB5+j46xBcgCovo8TQMcv0bCQLKIL9w==} + cpu: [x64] + os: [freebsd] + deprecated: yuku-codegen is pure JavaScript since 0.14 + + '@yuku-codegen/binding-linux-arm-gnu@0.10.2': + resolution: {integrity: sha512-xJDpYAsKV5+eaiBhTYl05fvT6sst4PsCeuIFMRu9b0/WCSrNQPfCDtAQ5/MS6xNpsdujdZEPR+gmfWk3ZG5i3A==} + cpu: [arm] + os: [linux] + libc: [glibc] + deprecated: yuku-codegen is pure JavaScript since 0.14 + + '@yuku-codegen/binding-linux-arm-musl@0.10.2': + resolution: {integrity: sha512-qfTkgd7AEx61l4K9VtXWCGTxc/fmXJ4ZheDz3i+/AAJUtg4sa0bPrc0VBxPggB/rayR8Z3+JfrBItuyanBJJ9Q==} + cpu: [arm] + os: [linux] + libc: [musl] + deprecated: yuku-codegen is pure JavaScript since 0.14 + + '@yuku-codegen/binding-linux-arm64-gnu@0.10.2': + resolution: {integrity: sha512-4e6Mifm/4UdjtU3D8mATIrvgP+xEiH8xtQOjd0zpd72XKWT7ug3sdyhq2utkkZFP6BvYp7SgZrI+aR4VeIOHdw==} + cpu: [arm64] + os: [linux] + libc: [glibc] + deprecated: yuku-codegen is pure JavaScript since 0.14 + + '@yuku-codegen/binding-linux-arm64-musl@0.10.2': + resolution: {integrity: sha512-RdsJrUfDYFVV3JOmWFUWwSu31fa1APn12xDeIKxgl/YWxrabwtxsDBNjF2561c7tmcbiewdCUvngqRs8AyGVLA==} + cpu: [arm64] + os: [linux] + libc: [musl] + deprecated: yuku-codegen is pure JavaScript since 0.14 + + '@yuku-codegen/binding-linux-x64-gnu@0.10.2': + resolution: {integrity: sha512-mVzWimEPPreaPyXIUvxsHoJzvO7ckZ5j0LgQFHz04zQIRwt9A9T2hA7K5/jYjJKkMxWG/U2VjhGNwVhtyU+uqg==} + cpu: [x64] + os: [linux] + libc: [glibc] + deprecated: yuku-codegen is pure JavaScript since 0.14 + + '@yuku-codegen/binding-linux-x64-musl@0.10.2': + resolution: {integrity: sha512-Y77fyISurmr2mvO1yeq3u+x/WJbACgPR4w+lzEVx30ViCxZzIGrZPRN1yEdZ9xUNPNsIO5thBHIdRNG+UGUG+Q==} + cpu: [x64] + os: [linux] + libc: [musl] + deprecated: yuku-codegen is pure JavaScript since 0.14 + + '@yuku-codegen/binding-win32-arm64@0.10.2': + resolution: {integrity: sha512-1+tGLyG0u5YYy0lev4ck7/0sd8xJ9SZde0dLut7qLDPZV5WVTV8x6OxNXUne/PGuHZyO1vK+txPQzzHOyXHGSw==} + cpu: [arm64] + os: [win32] + deprecated: yuku-codegen is pure JavaScript since 0.14 + + '@yuku-codegen/binding-win32-x64@0.10.2': + resolution: {integrity: sha512-8a2SExRohRbwC0MY4VpOF0/RSckrJ2ASZqAor5/RYSJJaeadR93n10gZzcKT6pY9ukiSOIZCRKVRD2tH73T0qA==} + cpu: [x64] + os: [win32] + deprecated: yuku-codegen is pure JavaScript since 0.14 + + '@yuku-parser/binding-android-arm64@0.10.2': + resolution: {integrity: sha512-2VPBU9fRGRAQ2xPAvghnec3oou5Nrxm2Bezhf+13UMspAxQSkF/32r+ySKXSwIPA5/RivumDJDxAxc301Sgicw==} + cpu: [arm64] + os: [android] + deprecated: yuku-parser runs on yuku-core since 0.14 + + '@yuku-parser/binding-darwin-arm64@0.10.2': + resolution: {integrity: sha512-LD+PMZE51tCYTOss5HBkm3/AE39MvcMDBfWJx7A4yDcjfNbAQDnHZKtzSOuqrswx+TQHY0ws5xn6+fWOwtmfBA==} + cpu: [arm64] + os: [darwin] + deprecated: yuku-parser runs on yuku-core since 0.14 + + '@yuku-parser/binding-darwin-x64@0.10.2': + resolution: {integrity: sha512-mmZ8cND+AoIIMRERyMinlg5ApHxP23B9jH2B5wT7T+dliPa9rubLxneB/SUjFwyUjGaFnB5G7t4YvpfbO5zbkQ==} + cpu: [x64] + os: [darwin] + deprecated: yuku-parser runs on yuku-core since 0.14 + + '@yuku-parser/binding-freebsd-x64@0.10.2': + resolution: {integrity: sha512-gVIjaaIddbRfAhHlC8N809wQWml7mxfSVnzaLtzGXObTeFTPAg/YVnQXt1UOhPM1ah5eG/8Y0RUlNpB4GVe7eQ==} + cpu: [x64] + os: [freebsd] + deprecated: yuku-parser runs on yuku-core since 0.14 + + '@yuku-parser/binding-linux-arm-gnu@0.10.2': + resolution: {integrity: sha512-q/XPPQQAPdlw05aPj30ygBhekmQryGOwxVraBgApjKK8yY1kNQgqq6XCYLF1WHSee+lhxclrTO7W0/bt7dR1GA==} + cpu: [arm] + os: [linux] + libc: [glibc] + deprecated: yuku-parser runs on yuku-core since 0.14 + + '@yuku-parser/binding-linux-arm-musl@0.10.2': + resolution: {integrity: sha512-A+Cb0I1hFF4wilTQpWs79k1aNnj4B2tkrHx3zsuUNF9BtVY2zPZ4yeQEM/zjXrS/qJlNMZVK9NrWJjwdpnTrfg==} + cpu: [arm] + os: [linux] + libc: [musl] + deprecated: yuku-parser runs on yuku-core since 0.14 + + '@yuku-parser/binding-linux-arm64-gnu@0.10.2': + resolution: {integrity: sha512-aGNSzqIqqphFwAdIFwVvKIyXD1Iy3CxrEJVIZAT87Ecyi4vCmmDD2v2l9h+Gd3/wSy3JZlOI792LJbKAjyy9rQ==} + cpu: [arm64] + os: [linux] + libc: [glibc] + deprecated: yuku-parser runs on yuku-core since 0.14 + + '@yuku-parser/binding-linux-arm64-musl@0.10.2': + resolution: {integrity: sha512-Ddl1sF0rtuCXyHGSOV6dcmdx+ETv1iD+IVFqQuOjzkJZWclxJxdBWX6ZJMRtTiGqGUTbqLEKiTXxpR/Bvqk+2A==} + cpu: [arm64] + os: [linux] + libc: [musl] + deprecated: yuku-parser runs on yuku-core since 0.14 + + '@yuku-parser/binding-linux-x64-gnu@0.10.2': + resolution: {integrity: sha512-/nlcpR6IF5U+0m5L9wvAIXeQa2DT+BXuvqKDsTcOTlcc0B4dHNyqE5hTXsS4hAyimpy2ZK8A1zMNF5hTrjg2cg==} + cpu: [x64] + os: [linux] + libc: [glibc] + deprecated: yuku-parser runs on yuku-core since 0.14 + + '@yuku-parser/binding-linux-x64-musl@0.10.2': + resolution: {integrity: sha512-GX80dxTQD/M/OryyuxJcFzFENzry3cDFPvCFTyWogNWQH3fSHfMrNfpwpl1YzMos1eV9NygK5GSDG7VArQlb4w==} + cpu: [x64] + os: [linux] + libc: [musl] + deprecated: yuku-parser runs on yuku-core since 0.14 + + '@yuku-parser/binding-win32-arm64@0.10.2': + resolution: {integrity: sha512-agePQBV4VHewiGU0ACSjscZ/hJd283f9JfAF19Gf1rI5+wy5tTgx4GS36i0b3xim15AQ4RCpDRosEvhLZ4zAOw==} + cpu: [arm64] + os: [win32] + deprecated: yuku-parser runs on yuku-core since 0.14 + + '@yuku-parser/binding-win32-x64@0.10.2': + resolution: {integrity: sha512-s8//CMpgL5+y1lvDCdyh1rwGI5+ytDJywFJRe1vnhI7n0j+caxshNucurikYLLVeQ2SYFex6aPrzgQxkabBQRA==} + cpu: [x64] + os: [win32] + deprecated: yuku-parser runs on yuku-core since 0.14 + + '@yuku-toolchain/types@0.10.2': + resolution: {integrity: sha512-sSeo4SSSToiS+sSD+bwn/s94EEcaLJ7tG5LCp8gFYC1G5VzxzX7fqB6m9RU1yMD8KTpu6zdU/I1NlQf8hVBJ9Q==} + accepts@1.3.8: resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==} engines: {node: '>= 0.6'} @@ -8487,6 +8900,10 @@ packages: resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} engines: {node: '>=8'} + cac@7.0.1: + resolution: {integrity: sha512-86diuUyeeWM8CrqhpGCaDafrzjS1s++7sWy2AbAPoFNz/xKv7ID57CR4pZsMuGCh60PtsnEx4z68b+Vgt07VPw==} + engines: {node: '>=20.19.0'} + call-bind-apply-helpers@1.0.2: resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} engines: {node: '>= 0.4'} @@ -8825,6 +9242,9 @@ packages: resolution: {integrity: sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==} engines: {node: '>=12'} + defu@6.1.7: + resolution: {integrity: sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ==} + depd@2.0.0: resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} engines: {node: '>= 0.8'} @@ -9017,6 +9437,15 @@ packages: drizzle-orm: '>=0.36.0' zod: ^3.25.0 || ^4.0.0 + dts-resolver@3.0.0: + resolution: {integrity: sha512-1T1f+z+4tl9XD+m+0HBgWoL/nm0bOIffyWaUuUSBlFg/86IWvfx+wjNaO/ybU0AJzG9/Mi5hBUgGV6zCmWEN7Q==} + engines: {node: ^22.18.0 || >=24.0.0} + peerDependencies: + oxc-resolver: '>=11.0.0' + peerDependenciesMeta: + oxc-resolver: + optional: true + dunder-proto@1.0.1: resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} engines: {node: '>= 0.4'} @@ -9042,6 +9471,10 @@ packages: emoji-regex@8.0.0: resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + empathic@2.1.0: + resolution: {integrity: sha512-AnfC1ATldl49/cvZdLPDjBfrRNwbDO05aibiOtzQu3qtlbJtomNLhF30HEtn/7iBz50dlMECqATo3fG0LrdEgw==} + engines: {node: '>=14'} + encodeurl@2.0.0: resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==} engines: {node: '>= 0.8'} @@ -9385,6 +9818,10 @@ packages: get-tsconfig@4.14.3: resolution: {integrity: sha512-++QEw4DIY7WGoukz+/+A/8dGYPT9l9yIadnmSgZ8Rjr3YVSVDipQSO9CdnJo9ePqFqUUqh+wk9uIaoiAwsiPkA==} + get-tsconfig@5.0.0-beta.6: + resolution: {integrity: sha512-X6fBC0pmImC70gvX2zm56go9hx0MyoGVdG0tUCkg/D+Xnh5TJsOZ7iDbOdI3PvmtrDxnu1YdDufpK2QJX1Meqw==} + engines: {node: '>=20.20.0'} + github-slugger@2.0.0: resolution: {integrity: sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw==} @@ -9486,6 +9923,9 @@ packages: resolution: {integrity: sha512-/Gng7NfoykZl2pjukW5Z6+8Yxm3BPRf86GTbQnt0SbySkvax4fyL4H3HhY1cCpBGmiW9XDRFzRV+CXK2W8QudQ==} engines: {node: '>=16.9.0'} + hookable@6.1.2: + resolution: {integrity: sha512-+abwxtiEA52GCVIsQqut3S/uKTbUwYIp4Pe/vv+6py5XiXBCqMZHg6pA6Y5qhgLSEys0/cYuPbO0z1QFj5ZCmg==} + html-encoding-sniffer@7.0.0: resolution: {integrity: sha512-UikN5yr7xsCDAq87Or5or0PAlD3HJJOKVzM05az588WnpDJ4Ux7a2A53Qi6gofGg2/EtvF/H4hCi/TXfCW4Y6w==} engines: {node: ^22.13.0 || >=24.0.0} @@ -9553,6 +9993,10 @@ packages: resolution: {integrity: sha512-mPKuL8bPQzecui2KK6Gb+M8JvJoHnhS1FeYGa22QopBmlevF5F0FE6ued/B5EgHDeIoMTONIpDWWFKUPOG0DBQ==} engines: {node: '>=18'} + import-without-cache@0.4.1: + resolution: {integrity: sha512-vXoV9PjKHEednCUu01e98TkImxy67e3BJXbTIOmIq4Hyzw3IAwYRcuPkfeTzJzwyyts4kjuA73x+pWJLc4f86A==} + engines: {node: ^22.18.0 || >=24.0.0} + inherits@2.0.4: resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} @@ -10508,6 +10952,14 @@ packages: resolution: {integrity: sha512-EFVjAYfzWqWsBMRHPMAXLCDIJnpMhdWAqR7xG6M6a2cs6PMFpl/+Z20w9zDW4vkxOFfddegBKq9Rehd0bxWE7A==} engines: {node: '>= 10'} + obug@2.2.1: + resolution: {integrity: sha512-XrsrhT5sybtKI6wakr2SPOlGZWWYbUXZ7a0jT8/QOeAPau+1X/bSegNe5YR75oJmEZQbKningirmGOEJCIk61Q==} + engines: {node: '>=12.20.0'} + + obug@3.0.0: + resolution: {integrity: sha512-5vvB5+W7ePv+p3uqxi+RcW1XAzLW0/hxt3/4X4Lc4qHudzOhmBiBwOY6DRob4WnanAEGvNcLjF+KNOufrUoEQw==} + engines: {node: '>=12.20.0'} + on-finished@2.4.1: resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} engines: {node: '>= 0.8'} @@ -10886,6 +11338,9 @@ packages: resolution: {integrity: sha512-h6fhOIaRrID2CbEY2fqs+7t+UXZo+MLAnU5gRIq85uFtdiUPCdsApMlHhXogKVM4HM2DVbIjGNTTYH2OcmP1vA==} engines: {node: '>=0.6'} + quansync@1.0.0: + resolution: {integrity: sha512-5xZacEEufv3HSTPQuchrvV6soaiACMFnq1H8wkVioctoH3TRha9Sz66lOxRwPK/qZj7HPiSveih9yAyh98gvqA==} + queue-microtask@1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} @@ -11140,6 +11595,33 @@ packages: resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + rolldown-plugin-dts@0.28.6: + resolution: {integrity: sha512-qKrFtBfRfR2hP233m7Ic9zf3wv6MSYdZghWKMdEO6dRYZGlNfINJAa1P5ZVvyHh3mrcd7IJQvIY8L9vnm+v5rQ==} + engines: {node: ^22.18.0 || ^24.11.0 || >=26.0.0} + peerDependencies: + '@typescript/native-preview': '*' + '@volar/typescript': ~2.4.0 + '@vue/language-core': ~3.2.0 || ~3.3.0 + rolldown: ^1.2.0 + typescript: ^5.0.0 || ^6.0.0 || ~7.0.0 + vue-tsc: ~3.2.0 || ~3.3.0 + peerDependenciesMeta: + '@typescript/native-preview': + optional: true + '@volar/typescript': + optional: true + '@vue/language-core': + optional: true + typescript: + optional: true + vue-tsc: + optional: true + + rolldown@1.2.13: + resolution: {integrity: sha512-/5apOkwWxtACcQtZ5WlrAsKS4JwXVThQPJZ3RqIruRY9UG9M8e0VlXN068xum/JRKA4aIUoJ13ghe/kGCvidiQ==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + rollup@4.63.4: resolution: {integrity: sha512-4U0liVayNIoLp3GFl1FcI8561WepLnZ1rqfraGh7S9B3Ur5F9S283y8Futii7RUU2C/97tOBmBy7nYvhoiOpbQ==} engines: {node: '>=18.0.0', npm: '>=8.0.0'} @@ -11606,6 +12088,40 @@ packages: resolution: {integrity: sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg==} engines: {node: '>=6'} + tsdown@0.23.0: + resolution: {integrity: sha512-BaT+ep1xnj5hdyICLd5r5SYYE+TXnI1ATePLykv9vcKpHpFTWUWRH+x1AF/E2qcu3YB6+vI8IdyxIIIffEqw5Q==} + engines: {node: ^22.18.0 || ^24.11.0 || >=26.0.0} + hasBin: true + peerDependencies: + '@arethetypeswrong/core': ^0.18.1 + '@tsdown/css': 0.23.0 + '@tsdown/exe': 0.23.0 + '@vitejs/devtools': '*' + publint: ^0.3.8 + tsx: '*' + typescript: ^5.0.0 || ^6.0.0 || ^7.0.0 + unplugin-unused: '>=0.5.0' + unrun: '*' + peerDependenciesMeta: + '@arethetypeswrong/core': + optional: true + '@tsdown/css': + optional: true + '@tsdown/exe': + optional: true + '@vitejs/devtools': + optional: true + publint: + optional: true + tsx: + optional: true + typescript: + optional: true + unplugin-unused: + optional: true + unrun: + optional: true + tslib@2.8.1: resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} @@ -11673,6 +12189,9 @@ packages: resolution: {integrity: sha512-FoSOKV7NEofQSkAefMVHam4ZPKYMxjAydxiV72UFEDNV/YofxjGfiZ2A9pZjdL/lRJzTjcu4PABo1JYJX8N5iQ==} engines: {node: '>=14'} + unconfig-core@7.5.0: + resolution: {integrity: sha512-Su3FauozOGP44ZmKdHy2oE6LPjk51M/TRRjHv2HNCWiDvfvCoxC2lno6jevMA91MYAdCdwP05QnWdWpSbncX/w==} + uncrypto@0.1.3: resolution: {integrity: sha512-Ql87qFHB3s/De2ClA9e0gsnS6zXG27SkTiSJwjCc9MebbfapQfuPzumMIUMi38ezPZVNFcHI9sUIepeQfw8J8Q==} @@ -11813,6 +12332,10 @@ packages: react: ^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc + verkit@0.4.1: + resolution: {integrity: sha512-mMVzj0TXExtVdlDEq+Mzp0eyOuyznNpFobNM3uAqe3HVm/Ps2c+u17BligMkZjZCA6EiXpoon8iRTer3iu40SQ==} + engines: {node: '>=18.12.0'} + vfile-matter@5.0.1: resolution: {integrity: sha512-o6roP82AiX0XfkyTHyRCMXgHfltUNlXSEqCIS80f+mbAyiQBE2fxtDVMtseyytGx75sihiJFo/zR6r/4LTs2Cw==} @@ -11986,6 +12509,15 @@ packages: resolution: {integrity: sha512-xYqdZFUK/VYazNl/oCDYN+3WloWQwMfZxBoiNt6qNyk+xfOdi598muWE42rNZFp1kNOiqW936q5RhUdnpqElSg==} engines: {node: '>=18'} + yuku-ast@0.10.2: + resolution: {integrity: sha512-UnG9mA6giglCvSErft2/40TVFX750Sj1xgwddPLpG7J7rlr/P1wPADg9G2RK+d/1tNLtRVoLdMMOMVBB9561TQ==} + + yuku-codegen@0.10.2: + resolution: {integrity: sha512-hentl2dtrF6cPjiAirtkfxfFZBA6Hdm61UqRJ7cA0qrlDNMk9PZGOjw5w1mx3E0hu/6pHcJF4dJW+D0SXHPZOA==} + + yuku-parser@0.10.2: + resolution: {integrity: sha512-CgaU0/PPjCAIEZ3WQroosOxTY3eeKldAN3h+vk8pMNz4+jl1CZzBR4pW+K/rRPF112VooCL5FdjJoiMNjDrL2A==} + zod-to-json-schema@3.25.2: resolution: {integrity: sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA==} peerDependencies: @@ -13218,7 +13750,7 @@ snapshots: pkce-challenge: 5.0.1 zod: 4.6.5 - '@hono/node-server@2.1.1(hono@4.13.8)': + '@hono/node-server@2.1.4(hono@4.13.8)': dependencies: hono: 4.13.8 @@ -13606,6 +14138,8 @@ snapshots: '@jsr/std__internal': 1.0.14 '@jsr/std__path': 1.1.6 + '@jsr/std__yaml@1.3.0': {} + '@kobalte/core@0.13.14(solid-js@1.9.15)': dependencies: '@floating-ui/dom': 1.8.0 @@ -13744,7 +14278,7 @@ snapshots: '@modelcontextprotocol/sdk@1.30.0(supports-color@8.1.1)(zod@3.25.76)': dependencies: - '@hono/node-server': 2.1.1(hono@4.13.8) + '@hono/node-server': 2.1.4(hono@4.13.8) ajv: 8.20.0 ajv-formats: 3.0.1(ajv@8.20.0) content-type: 1.0.5 @@ -13766,7 +14300,7 @@ snapshots: '@modelcontextprotocol/sdk@1.30.0(supports-color@8.1.1)(zod@4.6.5)': dependencies: - '@hono/node-server': 2.1.1(hono@4.13.8) + '@hono/node-server': 2.1.4(hono@4.13.8) ajv: 8.20.0 ajv-formats: 3.0.1(ajv@8.20.0) content-type: 1.0.5 @@ -14063,6 +14597,75 @@ snapshots: '@opentelemetry/semantic-conventions@1.43.0': {} + '@oxc-node/core-android-arm-eabi@0.1.5': + optional: true + + '@oxc-node/core-android-arm64@0.1.5': + optional: true + + '@oxc-node/core-darwin-arm64@0.1.5': + optional: true + + '@oxc-node/core-darwin-x64@0.1.5': + optional: true + + '@oxc-node/core-freebsd-x64@0.1.5': + optional: true + + '@oxc-node/core-linux-arm-gnueabihf@0.1.5': + optional: true + + '@oxc-node/core-linux-arm64-gnu@0.1.5': + optional: true + + '@oxc-node/core-linux-arm64-musl@0.1.5': + optional: true + + '@oxc-node/core-linux-ppc64-gnu@0.1.5': + optional: true + + '@oxc-node/core-linux-s390x-gnu@0.1.5': + optional: true + + '@oxc-node/core-linux-x64-gnu@0.1.5': + optional: true + + '@oxc-node/core-linux-x64-musl@0.1.5': + optional: true + + '@oxc-node/core-openharmony-arm64@0.1.5': + optional: true + + '@oxc-node/core-win32-arm64-msvc@0.1.5': + optional: true + + '@oxc-node/core-win32-ia32-msvc@0.1.5': + optional: true + + '@oxc-node/core-win32-x64-msvc@0.1.5': + optional: true + + '@oxc-node/core@0.1.5': + dependencies: + pirates: 4.0.7 + optionalDependencies: + '@oxc-node/core-android-arm-eabi': 0.1.5 + '@oxc-node/core-android-arm64': 0.1.5 + '@oxc-node/core-darwin-arm64': 0.1.5 + '@oxc-node/core-darwin-x64': 0.1.5 + '@oxc-node/core-freebsd-x64': 0.1.5 + '@oxc-node/core-linux-arm-gnueabihf': 0.1.5 + '@oxc-node/core-linux-arm64-gnu': 0.1.5 + '@oxc-node/core-linux-arm64-musl': 0.1.5 + '@oxc-node/core-linux-ppc64-gnu': 0.1.5 + '@oxc-node/core-linux-s390x-gnu': 0.1.5 + '@oxc-node/core-linux-x64-gnu': 0.1.5 + '@oxc-node/core-linux-x64-musl': 0.1.5 + '@oxc-node/core-openharmony-arm64': 0.1.5 + '@oxc-node/core-win32-arm64-msvc': 0.1.5 + '@oxc-node/core-win32-ia32-msvc': 0.1.5 + '@oxc-node/core-win32-x64-msvc': 0.1.5 + '@oxc-parser/binding-android-arm-eabi@0.150.0': optional: true @@ -14122,6 +14725,8 @@ snapshots: '@oxc-project/types@0.150.0': {} + '@oxc-project/types@0.153.0': {} + '@oxc-resolver/binding-android-arm-eabi@11.24.2': optional: true @@ -14453,6 +15058,10 @@ snapshots: '@protobufjs/utf8@1.1.2': {} + '@quansync/fs@1.1.0': + dependencies: + quansync: 1.0.0 + '@radix-ui/number@1.1.3': {} '@radix-ui/primitive@1.1.7': {} @@ -15224,6 +15833,53 @@ snapshots: - react-dom - sass + '@rolldown/binding-android-arm-eabi@1.2.13': + optional: true + + '@rolldown/binding-android-arm64@1.2.13': + optional: true + + '@rolldown/binding-darwin-arm64@1.2.13': + optional: true + + '@rolldown/binding-darwin-x64@1.2.13': + optional: true + + '@rolldown/binding-freebsd-x64@1.2.13': + optional: true + + '@rolldown/binding-linux-arm-gnueabihf@1.2.13': + optional: true + + '@rolldown/binding-linux-arm64-gnu@1.2.13': + optional: true + + '@rolldown/binding-linux-arm64-musl@1.2.13': + optional: true + + '@rolldown/binding-linux-ppc64-gnu@1.2.13': + optional: true + + '@rolldown/binding-linux-s390x-gnu@1.2.13': + optional: true + + '@rolldown/binding-linux-x64-gnu@1.2.13': + optional: true + + '@rolldown/binding-linux-x64-musl@1.2.13': + optional: true + + '@rolldown/binding-openharmony-arm64@1.2.13': + optional: true + + '@rolldown/binding-win32-arm64-msvc@1.2.13': + optional: true + + '@rolldown/binding-win32-x64-msvc@1.2.13': + optional: true + + '@rolldown/pluginutils@1.0.1': {} + '@rollup/plugin-commonjs@28.0.1(rollup@4.63.4)': dependencies: '@rollup/pluginutils': 5.4.0(rollup@4.63.4) @@ -15467,12 +16123,6 @@ snapshots: '@sentry/types': 8.9.2 '@sentry/utils': 8.9.2 - '@sentry/deno@10.75.1': - dependencies: - '@opentelemetry/api': 1.9.1 - '@sentry/core': 10.75.1 - '@sentry/server-utils': 10.75.1 - '@sentry/feedback@10.75.1': dependencies: '@sentry/core': 10.75.1 @@ -16383,6 +17033,80 @@ snapshots: '@xtuc/long@4.2.2': {} + '@yuku-codegen/binding-android-arm64@0.10.2': + optional: true + + '@yuku-codegen/binding-darwin-arm64@0.10.2': + optional: true + + '@yuku-codegen/binding-darwin-x64@0.10.2': + optional: true + + '@yuku-codegen/binding-freebsd-x64@0.10.2': + optional: true + + '@yuku-codegen/binding-linux-arm-gnu@0.10.2': + optional: true + + '@yuku-codegen/binding-linux-arm-musl@0.10.2': + optional: true + + '@yuku-codegen/binding-linux-arm64-gnu@0.10.2': + optional: true + + '@yuku-codegen/binding-linux-arm64-musl@0.10.2': + optional: true + + '@yuku-codegen/binding-linux-x64-gnu@0.10.2': + optional: true + + '@yuku-codegen/binding-linux-x64-musl@0.10.2': + optional: true + + '@yuku-codegen/binding-win32-arm64@0.10.2': + optional: true + + '@yuku-codegen/binding-win32-x64@0.10.2': + optional: true + + '@yuku-parser/binding-android-arm64@0.10.2': + optional: true + + '@yuku-parser/binding-darwin-arm64@0.10.2': + optional: true + + '@yuku-parser/binding-darwin-x64@0.10.2': + optional: true + + '@yuku-parser/binding-freebsd-x64@0.10.2': + optional: true + + '@yuku-parser/binding-linux-arm-gnu@0.10.2': + optional: true + + '@yuku-parser/binding-linux-arm-musl@0.10.2': + optional: true + + '@yuku-parser/binding-linux-arm64-gnu@0.10.2': + optional: true + + '@yuku-parser/binding-linux-arm64-musl@0.10.2': + optional: true + + '@yuku-parser/binding-linux-x64-gnu@0.10.2': + optional: true + + '@yuku-parser/binding-linux-x64-musl@0.10.2': + optional: true + + '@yuku-parser/binding-win32-arm64@0.10.2': + optional: true + + '@yuku-parser/binding-win32-x64@0.10.2': + optional: true + + '@yuku-toolchain/types@0.10.2': {} + accepts@1.3.8: dependencies: mime-types: 2.1.35 @@ -16557,6 +17281,8 @@ snapshots: cac@6.7.14: {} + cac@7.0.1: {} + call-bind-apply-helpers@1.0.2: dependencies: es-errors: 1.3.0 @@ -16849,6 +17575,8 @@ snapshots: define-lazy-prop@3.0.0: {} + defu@6.1.7: {} + depd@2.0.0: {} dequal@2.0.3: {} @@ -16946,6 +17674,10 @@ snapshots: drizzle-orm: 0.45.3(@libsql/client@0.17.4)(@opentelemetry/api@1.9.1)(@types/pg@8.15.6)(@upstash/redis@1.38.0)(bun-types@1.4.2) zod: 4.6.5 + dts-resolver@3.0.0(oxc-resolver@11.24.2): + optionalDependencies: + oxc-resolver: 11.24.2 + dunder-proto@1.0.1: dependencies: call-bind-apply-helpers: 1.0.2 @@ -16973,6 +17705,8 @@ snapshots: emoji-regex@8.0.0: {} + empathic@2.1.0: {} + encodeurl@2.0.0: {} end-of-stream@1.4.5: @@ -17487,6 +18221,10 @@ snapshots: dependencies: resolve-pkg-maps: 1.0.0 + get-tsconfig@5.0.0-beta.6: + dependencies: + resolve-pkg-maps: 1.0.0 + github-slugger@2.0.0: {} glob-parent@5.1.2: @@ -17647,6 +18385,8 @@ snapshots: hono@4.13.8: {} + hookable@6.1.2: {} + html-encoding-sniffer@7.0.0: dependencies: '@exodus/bytes': 1.15.2 @@ -17740,6 +18480,8 @@ snapshots: es-module-lexer: 3.0.2 module-details-from-path: 1.0.4 + import-without-cache@0.4.1: {} + inherits@2.0.4: {} inline-style-parser@0.2.7: {} @@ -18836,6 +19578,10 @@ snapshots: object-treeify@1.1.33: {} + obug@2.2.1: {} + + obug@3.0.0: {} + on-finished@2.4.1: dependencies: ee-first: 1.1.1 @@ -19265,6 +20011,8 @@ snapshots: es-define-property: 1.0.1 side-channel: 1.1.1 + quansync@1.0.0: {} + queue-microtask@1.2.3: {} radix-ui@1.6.7(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0): @@ -19650,6 +20398,41 @@ snapshots: reusify@1.1.0: {} + rolldown-plugin-dts@0.28.6(oxc-resolver@11.24.2)(rolldown@1.2.13)(typescript@7.0.2): + dependencies: + dts-resolver: 3.0.0(oxc-resolver@11.24.2) + get-tsconfig: 5.0.0-beta.6 + obug: 3.0.0 + rolldown: 1.2.13 + yuku-ast: 0.10.2 + yuku-codegen: 0.10.2 + yuku-parser: 0.10.2 + optionalDependencies: + typescript: 7.0.2 + transitivePeerDependencies: + - oxc-resolver + + rolldown@1.2.13: + dependencies: + '@oxc-project/types': 0.153.0 + '@rolldown/pluginutils': 1.0.1 + optionalDependencies: + '@rolldown/binding-android-arm-eabi': 1.2.13 + '@rolldown/binding-android-arm64': 1.2.13 + '@rolldown/binding-darwin-arm64': 1.2.13 + '@rolldown/binding-darwin-x64': 1.2.13 + '@rolldown/binding-freebsd-x64': 1.2.13 + '@rolldown/binding-linux-arm-gnueabihf': 1.2.13 + '@rolldown/binding-linux-arm64-gnu': 1.2.13 + '@rolldown/binding-linux-arm64-musl': 1.2.13 + '@rolldown/binding-linux-ppc64-gnu': 1.2.13 + '@rolldown/binding-linux-s390x-gnu': 1.2.13 + '@rolldown/binding-linux-x64-gnu': 1.2.13 + '@rolldown/binding-linux-x64-musl': 1.2.13 + '@rolldown/binding-openharmony-arm64': 1.2.13 + '@rolldown/binding-win32-arm64-msvc': 1.2.13 + '@rolldown/binding-win32-x64-msvc': 1.2.13 + rollup@4.63.4: dependencies: '@types/estree': 1.0.9 @@ -20231,6 +21014,32 @@ snapshots: minimist: 1.2.8 strip-bom: 3.0.0 + tsdown@0.23.0(oxc-resolver@11.24.2)(tsx@4.23.15)(typescript@7.0.2): + dependencies: + cac: 7.0.1 + defu: 6.1.7 + empathic: 2.1.0 + hookable: 6.1.2 + import-without-cache: 0.4.1 + obug: 2.2.1 + picomatch: 4.0.7 + rolldown: 1.2.13 + rolldown-plugin-dts: 0.28.6(oxc-resolver@11.24.2)(rolldown@1.2.13)(typescript@7.0.2) + tinyexec: 1.3.1 + tinyglobby: 0.2.17 + tree-kill: 1.2.2 + unconfig-core: 7.5.0 + verkit: 0.4.1 + optionalDependencies: + tsx: 4.23.15 + typescript: 7.0.2 + transitivePeerDependencies: + - '@typescript/native-preview' + - '@volar/typescript' + - '@vue/language-core' + - oxc-resolver + - vue-tsc + tslib@2.8.1: {} tsup@8.5.1(@swc/core@1.16.2)(jiti@2.7.0)(postcss@8.5.28)(supports-color@8.1.1)(tsx@4.23.15)(typescript@7.0.2)(yaml@2.9.1): @@ -20322,6 +21131,11 @@ snapshots: unbash@4.0.11: {} + unconfig-core@7.5.0: + dependencies: + '@quansync/fs': 1.1.0 + quansync: 1.0.0 + uncrypto@0.1.3: {} undici-types@8.9.0: {} @@ -20470,6 +21284,8 @@ snapshots: - '@types/react' - '@types/react-dom' + verkit@0.4.1: {} + vfile-matter@5.0.1: dependencies: vfile: 6.0.3 @@ -20710,6 +21526,45 @@ snapshots: yoctocolors@2.2.0: {} + yuku-ast@0.10.2: + dependencies: + '@yuku-toolchain/types': 0.10.2 + + yuku-codegen@0.10.2: + dependencies: + '@yuku-toolchain/types': 0.10.2 + optionalDependencies: + '@yuku-codegen/binding-android-arm64': 0.10.2 + '@yuku-codegen/binding-darwin-arm64': 0.10.2 + '@yuku-codegen/binding-darwin-x64': 0.10.2 + '@yuku-codegen/binding-freebsd-x64': 0.10.2 + '@yuku-codegen/binding-linux-arm-gnu': 0.10.2 + '@yuku-codegen/binding-linux-arm-musl': 0.10.2 + '@yuku-codegen/binding-linux-arm64-gnu': 0.10.2 + '@yuku-codegen/binding-linux-arm64-musl': 0.10.2 + '@yuku-codegen/binding-linux-x64-gnu': 0.10.2 + '@yuku-codegen/binding-linux-x64-musl': 0.10.2 + '@yuku-codegen/binding-win32-arm64': 0.10.2 + '@yuku-codegen/binding-win32-x64': 0.10.2 + + yuku-parser@0.10.2: + dependencies: + '@yuku-toolchain/types': 0.10.2 + yuku-ast: 0.10.2 + optionalDependencies: + '@yuku-parser/binding-android-arm64': 0.10.2 + '@yuku-parser/binding-darwin-arm64': 0.10.2 + '@yuku-parser/binding-darwin-x64': 0.10.2 + '@yuku-parser/binding-freebsd-x64': 0.10.2 + '@yuku-parser/binding-linux-arm-gnu': 0.10.2 + '@yuku-parser/binding-linux-arm-musl': 0.10.2 + '@yuku-parser/binding-linux-arm64-gnu': 0.10.2 + '@yuku-parser/binding-linux-arm64-musl': 0.10.2 + '@yuku-parser/binding-linux-x64-gnu': 0.10.2 + '@yuku-parser/binding-linux-x64-musl': 0.10.2 + '@yuku-parser/binding-win32-arm64': 0.10.2 + '@yuku-parser/binding-win32-x64': 0.10.2 + zod-to-json-schema@3.25.2(zod@3.25.76): dependencies: zod: 3.25.76 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 1c2ef2eae..d51813724 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -58,6 +58,7 @@ catalog: "@dnd-kit/utilities": 3.2.2 "@google-cloud/tasks": 7.1.1 "@hono/mcp": 0.2.5 + "@hono/node-server": 2.1.4 "@hono/sentry": 1.2.2 "@hono/zod-openapi": 1.6.3 "@hono/zod-validator": 0.8.0 @@ -82,6 +83,7 @@ catalog: "@opentelemetry/resources": 2.11.0 "@opentelemetry/sdk-logs": 0.208.0 "@opentelemetry/semantic-conventions": 1.43.0 + "@oxc-node/core": 0.1.5 "@radix-ui/react-accordion": 1.2.20 "@radix-ui/react-alert-dialog": 1.1.23 "@radix-ui/react-avatar": 1.2.6 @@ -109,7 +111,7 @@ catalog: "@radix-ui/react-tooltip": 1.2.16 "@react-email/ui": 6.9.5 "@scalar/hono-api-reference": 0.8.10 - "@sentry/deno": 10.75.1 + "@sentry/node": 10.75.1 "@sentry/nextjs": 10.75.1 "@slack/web-api": 8.1.1 "@t3-oss/env-core": 0.13.11 @@ -211,6 +213,7 @@ catalog: tailwind-merge: 3.7.0 tailwindcss: 4.3.3 tailwindcss-animate: 1.0.7 + tsdown: 0.23.0 tsup: 8.5.1 tw-animate-css: 1.4.0 typescript: 7.0.2 -- 2.54.0 From dd537deab653659398fb4a44ed7a2a6d343475e0 Mon Sep 17 00:00:00 2001 From: Thibault Le Ouay Date: Tue, 20 Oct 2026 10:25:00 +0200 Subject: [PATCH 2/6] test: move the test suites from deno test to vitest (#2891) * feat: run server and workflows on node 26 Replace the Deno runtime and build for apps/server, apps/workflows and the db migrate image with Node 26 and the Oxc toolchain: - dev and scripts run through oxc-node (node --import @oxc-node/core/register) - prod bundles with tsdown; @hono/node-server replaces Deno.serve - images move to node:26.8.1-slim with curl and ca-certificates (libsql's native TLS reads the system CA store); workflows ships its tsdown externals as a side-car node_modules - workflows switches @sentry/deno to @sentry/node - type checks move from deno check to tsc --noEmit; test files are excluded until they move to Vitest - pin node 26.8.1 in devbox and CI Tests still run on deno test; that moves in the next PR. Co-Authored-By: Claude Opus 5.5 * test: move the test suites from deno test to vitest Run all 33 test packages on Vitest 5 instead of deno test with @std/expect and @std/testing: - @openstatus/test-utils becomes a shared vitest config (defineTestConfig) with a plugin that applies the existing test.importmap.json doubles with Deno import-map semantics, so no double changed - a codemod moves @std/testing/bdd + @std/expect to vitest, stub/spy to vi.spyOn/vi.fn, assertSpyCalls to toHaveBeenCalledTimes, .calls[i].args to .mock.calls[i]; FakeTime, returnsNext, Deno.serve/Command hand-ported - test files are typechecked again (tsc --noEmit), with the type errors that surfaced fixed - 10 unawaited expect().rejects assertions now await, so they actually run - the Bun-era compat shims (runner, mock(), matchers) and the dead server preload are removed; openapi:v1 runs on node Parity with the deno baseline was checked file by file: every test that ran under deno runs under vitest (5163 passed, 15 skipped). --- AGENTS.md | 4 +- CONTRIBUTING.md | 3 - apps/dashboard/package.json | 8 +- .../chat/tool-renderers/details.test.tsx | 5 +- .../chat/tool-renderers/get-audit-log.test.ts | 3 +- .../chat/tool-renderers/incidents.test.tsx | 3 +- .../chat/tool-renderers/list-tables.test.tsx | 3 +- .../maintenance-changes.test.ts | 3 +- .../chat/tool-renderers/registry.test.tsx | 22 +- .../status-report-changes.test.ts | 3 +- .../chat/tool-renderers/tables.test.tsx | 3 +- .../command-menu/groups/entities.test.ts | 3 +- .../command-menu/groups/scope.test.ts | 3 +- .../command-menu/groups/static.test.ts | 3 +- .../src/components/command-menu/types.test.ts | 3 +- .../components/common/changes-table.test.tsx | 3 +- .../components/common/status-badge.test.tsx | 3 +- .../data-table/table-cell-metadata.test.tsx | 3 +- .../incidents/incident-actions.test.ts | 3 +- .../onboarding/checks-table-adapter.test.ts | 3 +- .../src/components/ui/checkbox-tree.test.tsx | 3 +- .../src/data/entity-refs.client.test.ts | 3 +- .../src/data/entity-refs.coverage.test.ts | 3 +- .../dashboard/src/data/metrics.client.test.ts | 3 +- .../src/data/monitors.client.test.ts | 3 +- .../src/data/overview-events.client.test.ts | 3 +- .../src/data/page-components.client.test.ts | 3 +- apps/dashboard/src/data/status-codes.test.ts | 3 +- .../src/data/status-page-history.test.ts | 8 +- .../data/status-report-updates.client.test.ts | 3 +- .../src/data/status-reports.client.test.ts | 3 +- apps/dashboard/src/lib/auth/providers.test.ts | 41 +- apps/dashboard/src/lib/composition.test.ts | 3 +- apps/dashboard/src/lib/domains.test.ts | 20 +- apps/dashboard/src/lib/formatter.test.ts | 12 +- apps/dashboard/src/lib/keyboard.test.ts | 3 +- .../src/lib/onboarding/email-domain.test.ts | 3 +- .../src/lib/rate-limit/magic-link.test.ts | 41 +- apps/dashboard/src/test/next-link.mock.tsx | 4 +- apps/dashboard/vitest.config.ts | 6 + apps/server/AGENTS.md | 5 +- apps/server/dofigen.lock | 70 +- apps/server/package.json | 9 +- apps/server/src/libs/checker/utils.test.ts | 2 +- apps/server/src/libs/cli-telemetry.test.ts | 3 +- .../src/libs/middlewares/concurrency.test.ts | 3 +- .../src/libs/middlewares/credentials.test.ts | 3 +- .../src/libs/middlewares/rate-limit.test.ts | 14 +- .../libs/middlewares/require-scope.test.ts | 3 +- .../server/src/libs/middlewares/track.test.ts | 26 +- .../src/libs/middlewares/validate-key.test.ts | 3 +- apps/server/src/libs/sampling.test.ts | 8 +- .../src/libs/test/doubles/analytics.mock.ts | 10 +- apps/server/src/libs/test/doubles/state.ts | 18 +- apps/server/src/libs/test/preload.ts | 196 ---- apps/server/src/overload.test.ts | 5 +- apps/server/src/routes/mcp/adapter.test.ts | 3 +- apps/server/src/routes/mcp/handler.test.ts | 12 +- apps/server/src/routes/mcp/resources.test.ts | 23 +- .../routes/mcp/tools/register-scoped.test.ts | 3 +- .../server/src/routes/mcp/tools/tools.test.ts | 3 +- apps/server/src/routes/oauth/oauth.test.ts | 3 +- apps/server/src/routes/openapi.test.ts | 13 +- apps/server/src/routes/public/status.test.ts | 4 +- apps/server/src/routes/rpc/adapter.test.ts | 3 +- .../incident/__tests__/incident.test.ts | 4 +- .../maintenance/__tests__/maintenance.test.ts | 16 +- .../monitor/__tests__/monitor.test.ts | 8 +- .../monitor/converters/assertions.test.ts | 3 +- .../monitor/converters/headers.test.ts | 3 +- .../monitor/converters/regions.test.ts | 3 +- .../handlers/monitor/response-logs.test.ts | 3 +- .../rpc/handlers/monitor/validators.test.ts | 3 +- .../__tests__/notification.test.ts | 3 +- .../__tests__/private-location.test.ts | 3 +- .../status-page/__tests__/status-page.test.ts | 14 +- .../__tests__/status-report.test.ts | 16 +- .../rpc/interceptors/__tests__/error.test.ts | 2 +- .../interceptors/__tests__/tracking.test.ts | 35 +- apps/server/src/routes/rpc/parse.test.ts | 3 +- apps/server/src/routes/slack/blocks.test.ts | 3 +- .../src/routes/slack/channel-context.test.ts | 7 +- apps/server/src/routes/slack/commands.test.ts | 2 +- apps/server/src/routes/slack/config.ts | 5 +- .../routes/slack/confirmation-store.test.ts | 3 +- apps/server/src/routes/slack/handler.test.ts | 8 +- apps/server/src/routes/slack/home.test.ts | 3 +- .../routes/slack/incident-commands.test.ts | 3 +- apps/server/src/routes/slack/index.test.ts | 3 +- .../src/routes/slack/interactions.test.ts | 50 +- apps/server/src/routes/slack/mrkdwn.test.ts | 2 +- .../src/routes/slack/next-steps.test.ts | 2 +- apps/server/src/routes/slack/oauth.test.ts | 3 +- .../slack/presenters/presenters.test.ts | 2 +- .../src/routes/slack/registry-runner.test.ts | 7 +- .../src/routes/slack/report-draft.test.ts | 2 +- .../routes/slack/resolve-slack-user.test.ts | 3 +- .../server/src/routes/slack/rich-text.test.ts | 2 +- .../src/routes/slack/slack-links.test.ts | 2 +- .../src/routes/slack/system-prompt.test.ts | 3 +- .../src/routes/slack/thread-memory.test.ts | 2 +- .../src/routes/slack/thread-messages.test.ts | 2 +- .../src/routes/slack/thread-title.test.ts | 2 +- apps/server/src/routes/slack/verify.test.ts | 3 +- .../src/routes/v1/check/http/post.test.ts | 4 +- .../src/routes/v1/incidents/get.test.ts | 3 +- .../src/routes/v1/incidents/get_all.test.ts | 3 +- .../src/routes/v1/incidents/put.test.ts | 3 +- .../v1/maintenances/attribution.test.ts | 3 +- .../src/routes/v1/maintenances/get.test.ts | 3 +- .../routes/v1/maintenances/get_all.test.ts | 3 +- .../src/routes/v1/maintenances/post.test.ts | 3 +- .../src/routes/v1/maintenances/put.test.ts | 46 +- .../src/routes/v1/monitors/delete.test.ts | 3 +- .../server/src/routes/v1/monitors/get.test.ts | 3 +- .../src/routes/v1/monitors/get_all.test.ts | 3 +- .../src/routes/v1/monitors/post.test.ts | 3 +- .../src/routes/v1/monitors/post_dns.test.ts | 3 +- .../src/routes/v1/monitors/post_http.test.ts | 3 +- .../src/routes/v1/monitors/post_tcp.test.ts | 3 +- .../server/src/routes/v1/monitors/put.test.ts | 3 +- .../src/routes/v1/monitors/put_dns.test.ts | 3 +- .../src/routes/v1/monitors/put_http.test.ts | 3 +- .../src/routes/v1/monitors/put_tcp.test.ts | 3 +- .../routes/v1/monitors/results/get.test.ts | 36 +- .../src/routes/v1/monitors/run/post.test.ts | 89 +- .../routes/v1/monitors/summary/get.test.ts | 5 +- .../routes/v1/monitors/trigger/post.test.ts | 4 +- .../src/routes/v1/notifications/get.test.ts | 3 +- .../routes/v1/notifications/get_all.test.ts | 3 +- .../src/routes/v1/notifications/post.test.ts | 3 +- .../routes/v1/pageSubscribers/post.test.ts | 29 +- apps/server/src/routes/v1/pages/get.test.ts | 3 +- .../src/routes/v1/pages/get_all.test.ts | 3 +- apps/server/src/routes/v1/pages/post.test.ts | 3 +- apps/server/src/routes/v1/pages/put.test.ts | 3 +- .../routes/v1/statusReportUpdates/get.test.ts | 3 +- .../v1/statusReportUpdates/post.test.ts | 3 +- .../v1/statusReports/attribution.test.ts | 3 +- .../routes/v1/statusReports/delete.test.ts | 3 +- .../src/routes/v1/statusReports/get.test.ts | 3 +- .../routes/v1/statusReports/get_all.test.ts | 3 +- .../src/routes/v1/statusReports/post.test.ts | 3 +- .../subscriber-filtering.integration.test.ts | 3 +- .../v1/statusReports/update/post.test.ts | 3 +- apps/server/src/routes/v1/whoami/get.test.ts | 3 +- apps/server/tsconfig.json | 19 +- apps/server/vitest.config.ts | 6 + apps/status-page/package.json | 8 +- .../src/components/button/buttons.test.tsx | 3 +- .../src/components/common/link.test.tsx | 3 +- .../content/process-message.test.tsx | 5 +- .../src/components/date-picker.test.tsx | 3 +- .../forms/form-manage-subscription.test.tsx | 3 +- .../src/components/forms/forms.test.tsx | 3 +- .../src/components/locale-switcher.test.tsx | 3 +- .../status-component-group.test.tsx | 3 +- .../status-page/status-feed.test.tsx | 3 +- .../src/components/status-page/utils.test.ts | 3 +- .../themes/theme-builder-provider.test.tsx | 3 +- .../data-table/data-table-pagination.test.tsx | 3 +- .../src/content/markdown/generators.test.ts | 3 +- .../src/content/markdown/helpers.test.ts | 3 +- .../src/content/markdown/match-route.test.ts | 3 +- .../src/content/status-json.test.ts | 3 +- .../src/content/status-vocab.test.ts | 3 +- .../src/data/actions.client.test.ts | 3 +- .../src/data/metrics.client.test.ts | 3 +- apps/status-page/src/i18n/messages.test.ts | 3 +- apps/status-page/src/lib/alternates.test.ts | 3 +- apps/status-page/src/lib/base-url.test.ts | 3 +- apps/status-page/src/lib/chart.test.ts | 3 +- apps/status-page/src/lib/composition.test.ts | 3 +- apps/status-page/src/lib/domain.test.ts | 3 +- apps/status-page/src/lib/embed-params.test.ts | 3 +- apps/status-page/src/lib/formatter.test.ts | 3 +- apps/status-page/src/lib/http/etag.test.ts | 3 +- .../src/lib/http/markdown-response.test.ts | 3 +- .../proxy/apply-page-locale-override.test.ts | 3 +- .../lib/proxy/apply-page-slug-prefix.test.ts | 3 +- .../src/lib/proxy/build-external-path.test.ts | 3 +- .../src/lib/proxy/compose-page-action.test.ts | 3 +- .../src/lib/proxy/detect-markdown.test.ts | 3 +- .../lib/proxy/markdown-cache-control.test.ts | 3 +- .../src/lib/proxy/proxy-chain.test.ts | 3 +- .../lib/proxy/resolve-default-rewrite.test.ts | 3 +- .../proxy/resolve-email-domain-action.test.ts | 3 +- .../resolve-ip-restriction-action.test.ts | 3 +- .../lib/proxy/resolve-locale-action.test.ts | 3 +- .../lib/proxy/resolve-password-action.test.ts | 3 +- .../resolve-unresolved-host-action.test.ts | 3 +- .../lib/proxy/sanitize-redirect-param.test.ts | 3 +- .../proxy/strip-prefix-for-external.test.ts | 3 +- .../src/lib/report-impacts.test.ts | 3 +- .../src/lib/resolve-pathname-prefix.test.ts | 3 +- .../status-page/src/lib/resolve-route.test.ts | 3 +- .../src/lib/status-blocks-labels.test.ts | 3 +- .../src/lib/status-event-timeline.test.ts | 3 +- apps/status-page/src/lib/theme-draft.test.ts | 22 +- .../src/lib/theme-explorer-host.test.ts | 3 +- apps/status-page/src/lib/theme-prompt.test.ts | 3 +- apps/status-page/src/test/next-link.mock.tsx | 4 +- apps/status-page/vitest.config.ts | 5 + apps/web/package.json | 8 +- apps/web/src/app/(ai)/mcp/route.test.ts | 3 +- .../app/(landing)/play/checker/parse.test.ts | 3 +- .../(landing)/play/mcp-health/utils.test.ts | 3 +- .../(landing)/status/incident-events.test.ts | 3 +- .../src/app/(landing)/status/utils.test.ts | 3 +- apps/web/src/content/agent-recovery.test.ts | 3 +- apps/web/src/content/convert.test.ts | 3 +- .../mdx-components/content-lint.test.ts | 3 +- .../content/mdx-components/details.test.ts | 3 +- .../src/content/utils/search-match.test.ts | 3 +- apps/web/src/lib/cdn-checker/probe.test.ts | 3 +- apps/web/src/lib/cdn-checker/schema.test.ts | 3 +- apps/web/src/lib/cdn-checker/summary.test.ts | 3 +- apps/web/src/lib/checker/utils.test.ts | 3 +- apps/web/src/lib/mcp/health-check.test.ts | 3 +- .../src/lib/metadata/shared-metadata.test.ts | 3 +- .../src/lib/metadata/structured-data.test.ts | 3 +- apps/web/src/lib/monitor/utils.test.ts | 8 +- apps/web/src/lib/ratelimit-config.test.ts | 3 +- apps/web/src/lib/utils.test.ts | 7 +- apps/web/vitest.config.ts | 3 + apps/workflows/AGENTS.md | 12 +- apps/workflows/package.json | 5 +- apps/workflows/src/checker/alerting.test.ts | 46 +- apps/workflows/src/checker/outbox.test.ts | 70 +- .../src/checker/private-location.test.ts | 57 +- apps/workflows/src/checker/transition.test.ts | 8 +- .../src/checker/update-status.test.ts | 25 +- .../src/cron/external-status-detect.test.ts | 2 +- .../src/cron/private-location-health.test.ts | 37 +- apps/workflows/src/cron/scheduler.test.ts | 2 +- apps/workflows/src/cron/status-drift.test.ts | 26 +- apps/workflows/src/test/stub-audit-log.ts | 21 + apps/workflows/vitest.config.ts | 3 + packages/ai/package.json | 8 +- packages/ai/src/resolve-model.test.ts | 3 +- packages/ai/vitest.config.ts | 3 + packages/api/package.json | 8 +- packages/api/src/router/blob.test.ts | 3 +- packages/api/src/router/checker.test.ts | 3 +- packages/api/src/router/domain.test.ts | 3 +- .../api/src/router/effective-status.test.ts | 3 +- packages/api/src/router/feedback.test.ts | 3 +- packages/api/src/router/import.test.ts | 3 +- packages/api/src/router/incident.test.ts | 3 +- packages/api/src/router/maintenance.test.ts | 3 +- packages/api/src/router/monitor.test.ts | 3 +- packages/api/src/router/notification.test.ts | 3 +- packages/api/src/router/oauth.test.ts | 3 +- .../api/src/router/page.custom-domain.test.ts | 3 +- packages/api/src/router/pageComponent.test.ts | 3 +- .../api/src/router/privateLocation.test.ts | 3 +- packages/api/src/router/slackUser.test.ts | 3 +- .../api/src/router/statusPage.access.test.ts | 3 +- .../api/src/router/statusPage.e2e.test.ts | 3 +- .../src/router/statusPage.subscribe.test.ts | 3 +- .../src/router/statusPage.unsubscribe.test.ts | 3 +- .../api/src/router/statusPage.utils.test.ts | 7 +- packages/api/src/router/statusReport.test.ts | 3 +- packages/api/src/router/stripe/shared.test.ts | 3 +- .../src/router/stripe/trial-lifecycle.test.ts | 322 ++++--- packages/api/src/router/stripe/trial.test.ts | 100 ++- packages/api/src/router/stripe/utils.test.ts | 3 +- .../api/src/router/stripe/webhook.test.ts | 186 ++-- .../api/src/router/tinybird/index.test.ts | 3 +- packages/api/src/router/workspace.test.ts | 3 +- packages/api/src/test/upstash.mock.ts | 4 +- packages/api/src/trpc-errors.test.ts | 3 +- packages/api/src/trpc.test.ts | 3 +- packages/api/vitest.config.ts | 6 + packages/assertions/package.json | 8 +- packages/assertions/src/serializing.test.ts | 3 +- packages/assertions/src/v1.test.ts | 9 +- packages/assertions/vitest.config.ts | 3 + packages/db/package.json | 9 +- packages/db/script/region-migration.test.ts | 3 +- packages/db/src/schema/plan/utils.test.ts | 3 +- packages/db/src/utils/api-key.test.ts | 3 +- packages/db/vitest.config.ts | 3 + packages/emails/package.json | 8 +- packages/emails/src/billing.test.tsx | 131 +-- .../emails/src/client.integration.test.tsx | 3 +- packages/emails/src/client.test.ts | 100 ++- packages/emails/src/format.test.ts | 3 +- packages/emails/src/templates.test.tsx | 3 +- packages/emails/src/test-preload.ts | 3 +- packages/emails/vitest.config.ts | 3 + packages/header-analysis/package.json | 10 +- .../src/cdn/detect-cdn.test.ts | 3 +- .../header-analysis/src/cdn/edge-pop.test.ts | 3 +- .../src/cdn/infer-topology.test.ts | 3 +- .../src/cdn/normalize-cache-status.test.ts | 3 +- .../src/parser/cache-control.test.ts | 3 +- .../src/parser/cf-cache-status.test.ts | 3 +- .../header-analysis/src/parser/cf-ray.test.ts | 3 +- .../src/parser/fly-request-id.test.ts | 3 +- .../src/parser/x-vercel-cache.test.ts | 3 +- .../src/parser/x-vercel-id.test.ts | 3 +- packages/header-analysis/vitest.config.ts | 3 + packages/importers/package.json | 10 +- .../src/providers/betterstack/client.test.ts | 24 +- .../src/providers/betterstack/mapper.test.ts | 3 +- .../providers/betterstack/provider.test.ts | 8 +- .../src/providers/checkly/client.test.ts | 32 +- .../src/providers/checkly/mapper.test.ts | 3 +- .../src/providers/checkly/provider.test.ts | 6 +- .../src/providers/instatus/client.test.ts | 26 +- .../src/providers/instatus/mapper.test.ts | 3 +- .../src/providers/instatus/provider.test.ts | 3 +- .../src/providers/statuspage/client.test.ts | 26 +- .../src/providers/statuspage/mapper.test.ts | 3 +- .../src/providers/statuspage/provider.test.ts | 3 +- packages/importers/src/schemas.test.ts | 3 +- packages/importers/vitest.config.ts | 3 + packages/notifications/base/package.json | 8 +- .../base/src/utils/duration.test.ts | 3 +- .../base/src/utils/incident.test.ts | 3 +- .../base/src/utils/timestamp.test.ts | 3 +- packages/notifications/base/tsconfig.json | 10 +- packages/notifications/base/vitest.config.ts | 3 + .../notifications/bird-whatsapp/package.json | 8 +- .../bird-whatsapp/src/index.test.ts | 40 +- .../bird-whatsapp/vitest.config.ts | 3 + packages/notifications/discord/package.json | 8 +- .../notifications/discord/src/index.test.ts | 48 +- .../notifications/discord/vitest.config.ts | 3 + packages/notifications/email/package.json | 5 +- .../notifications/email/src/index.test.ts | 39 +- .../notifications/email/src/test-preload.ts | 3 +- packages/notifications/email/vitest.config.ts | 3 + .../notifications/google-chat/package.json | 8 +- .../google-chat/src/index.test.ts | 52 +- .../google-chat/vitest.config.ts | 3 + .../notifications/grafana-oncall/package.json | 8 +- .../grafana-oncall/src/index.test.ts | 82 +- .../grafana-oncall/vitest.config.ts | 3 + packages/notifications/ms-teams/package.json | 8 +- .../notifications/ms-teams/src/index.test.ts | 43 +- .../notifications/ms-teams/vitest.config.ts | 3 + packages/notifications/ntfy/package.json | 8 +- packages/notifications/ntfy/src/index.test.ts | 84 +- packages/notifications/ntfy/vitest.config.ts | 3 + packages/notifications/opsgenie/package.json | 8 +- .../notifications/opsgenie/src/index.test.ts | 92 +- .../notifications/opsgenie/vitest.config.ts | 3 + packages/notifications/pagerduty/package.json | 8 +- .../notifications/pagerduty/src/index.test.ts | 68 +- .../notifications/pagerduty/vitest.config.ts | 3 + packages/notifications/slack/package.json | 8 +- .../notifications/slack/src/index.test.ts | 66 +- packages/notifications/slack/vitest.config.ts | 3 + packages/notifications/telegram/package.json | 8 +- .../notifications/telegram/src/index.test.ts | 70 +- .../notifications/telegram/vitest.config.ts | 3 + .../notifications/twillio-sms/package.json | 8 +- .../twillio-sms/src/index.test.ts | 52 +- .../twillio-sms/vitest.config.ts | 3 + packages/notifications/webhook/package.json | 8 +- .../notifications/webhook/src/index.test.ts | 116 ++- packages/notifications/webhook/tsconfig.json | 11 +- .../notifications/webhook/vitest.config.ts | 3 + packages/proto/tsconfig.json | 10 +- packages/services/package.json | 8 +- .../src/__tests__/attribution.test.ts | 3 +- .../services/src/__tests__/context.test.ts | 3 +- .../services/src/__tests__/features.test.ts | 3 +- .../services/src/__tests__/limits.test.ts | 3 +- packages/services/src/__tests__/retry.test.ts | 3 +- .../__tests__/with-test-transaction.test.ts | 3 +- .../src/__tests__/with-transaction.test.ts | 3 +- .../src/agent-tools/__tests__/content.test.ts | 16 +- .../src/agent-tools/__tests__/docs.test.ts | 8 +- .../agent-tools/__tests__/incident.test.ts | 3 +- .../__tests__/maintenance-updates.test.ts | 3 +- .../src/agent-tools/__tests__/prompt.test.ts | 3 +- .../__tests__/shape-equivalence.test.ts | 3 +- .../__tests__/status-report-impacts.test.ts | 3 +- .../__tests__/summary-refs.test.ts | 3 +- .../src/api-key/__tests__/api-key.test.ts | 3 +- .../src/audit/__tests__/audit.test.ts | 3 +- .../src/auth/__tests__/matches-scope.test.ts | 3 +- .../src/auth/__tests__/require-role.test.ts | 3 +- .../src/auth/__tests__/require-scope.test.ts | 3 +- .../external-service-component.test.ts | 3 +- .../__tests__/internal.test.ts | 3 +- .../external-service-incident.test.ts | 3 +- .../__tests__/record.test.ts | 3 +- .../__tests__/external-service.test.ts | 3 +- .../__tests__/update-provider.test.ts | 3 +- .../frozen-uptime/__tests__/compute.test.ts | 3 +- .../frozen-uptime/__tests__/freeze.test.ts | 3 +- .../__tests__/get-history.test.ts | 3 +- .../src/frozen-uptime/__tests__/run.test.ts | 3 +- .../frozen-uptime/__tests__/schemas.test.ts | 3 +- .../src/import/__tests__/attribution.test.ts | 3 +- .../__tests__/dedupe-and-atomicity.test.ts | 3 +- .../import/__tests__/incidents-phase.test.ts | 3 +- .../src/import/__tests__/unit.test.ts | 3 +- .../src/incident/__tests__/effects.test.ts | 3 +- .../incident/__tests__/event-source.test.ts | 3 +- .../src/incident/__tests__/hooks.test.ts | 3 +- .../src/incident/__tests__/incident.test.ts | 3 +- .../incident/__tests__/legacy-source.test.ts | 3 +- .../__tests__/postmortem-draft.test.ts | 3 +- .../src/incident/__tests__/postmortem.test.ts | 3 +- .../src/incident/__tests__/reminders.test.ts | 3 +- .../src/incident/__tests__/slack-flow.test.ts | 3 +- .../__tests__/install-slack-agent.test.ts | 3 +- .../__tests__/slack-scopes.test.ts | 3 +- .../__tests__/uninstall-slack-agent.test.ts | 3 +- .../invitation/__tests__/invitation.test.ts | 3 +- .../maintenance/__tests__/attribution.test.ts | 3 +- .../maintenance/__tests__/maintenance.test.ts | 4 +- .../src/member/__tests__/display-name.test.ts | 3 +- .../member/__tests__/find-by-email.test.ts | 3 +- .../src/member/__tests__/member.test.ts | 3 +- .../__tests__/transfer-ownership.test.ts | 3 +- .../__tests__/monitor-incident.test.ts | 3 +- .../monitor-tag/__tests__/monitor-tag.test.ts | 3 +- .../src/monitor/__tests__/monitor.test.ts | 3 +- .../src/monitor/__tests__/reads.test.ts | 3 +- .../__tests__/response-logs-cursor.test.ts | 3 +- .../__tests__/response-logs-internal.test.ts | 3 +- .../__tests__/stream-monitor-preview.test.ts | 3 +- .../src/monitor/__tests__/url-safety.test.ts | 3 +- .../__tests__/notification.test.ts | 3 +- .../services/src/oauth/__tests__/cimd.test.ts | 3 +- .../src/oauth/__tests__/crypto.test.ts | 3 +- .../src/oauth/__tests__/metadata.test.ts | 3 +- .../src/oauth/__tests__/oauth.test.ts | 3 +- .../__tests__/redirect-allowlist.test.ts | 3 +- .../src/oauth/__tests__/schemas.test.ts | 3 +- .../src/page-access/access-predicates.test.ts | 3 +- .../src/page-access/client-ip.test.ts | 3 +- .../evaluate-markdown-gate.test.ts | 3 +- .../src/page-access/is-ip-allowed.test.ts | 3 +- .../__tests__/page-component-group.test.ts | 3 +- .../__tests__/get-daily-summary.test.ts | 3 +- .../__tests__/page-component.test.ts | 3 +- .../__tests__/page-subscriber.test.ts | 34 +- .../page-subscriber/__tests__/slack.test.ts | 3 +- .../services/src/page/__tests__/page.test.ts | 3 +- .../__tests__/status-update-types.test.ts | 3 +- .../__tests__/private-location.test.ts | 3 +- .../slack-user/__tests__/slack-user.test.ts | 3 +- .../services/src/sso/__tests__/sso.test.ts | 3 +- .../__tests__/attribution.test.ts | 3 +- .../__tests__/derive-status.test.ts | 3 +- .../__tests__/incident-id.test.ts | 3 +- .../__tests__/status-report.test.ts | 4 +- .../__tests__/downtime.test.ts | 3 +- .../status-timeline/__tests__/events.test.ts | 12 +- .../status-timeline/__tests__/uptime.test.ts | 3 +- .../src/user/__tests__/providers.test.ts | 3 +- .../src/workspace/__tests__/billing.test.ts | 3 +- .../src/workspace/__tests__/downgrade.test.ts | 3 +- .../src/workspace/__tests__/trial.test.ts | 3 +- .../src/workspace/__tests__/workspace.test.ts | 3 +- packages/services/test/helpers.ts | 2 +- packages/services/vitest.config.ts | 3 + .../status-fetcher/__tests__/detect.test.ts | 13 +- .../status-fetcher/__tests__/fetch.test.ts | 16 +- .../__tests__/fetchers/atlassian.test.ts | 21 +- .../__tests__/fetchers/betterstack.test.ts | 15 +- .../__tests__/fetchers/components.test.ts | 11 +- .../__tests__/fetchers/custom.test.ts | 3 +- .../__tests__/fetchers/edge-cases.test.ts | 3 +- .../__tests__/fetchers/html.test.ts | 9 +- .../__tests__/fetchers/incidentio.test.ts | 23 +- .../__tests__/fetchers/instatus.test.ts | 15 +- .../__tests__/fetchers/uptimerobot.test.ts | 14 +- packages/status-fetcher/__tests__/helpers.ts | 4 +- .../__tests__/integration.test.ts | 3 +- .../status-fetcher/__tests__/utils.test.ts | 3 +- packages/status-fetcher/package.json | 10 +- packages/status-fetcher/vitest.config.ts | 3 + packages/subscriptions/package.json | 8 +- .../subscriptions/src/channels/email.test.ts | 62 +- .../subscriptions/src/channels/index.test.ts | 3 +- .../src/channels/slack-blocks.test.ts | 3 +- .../subscriptions/src/channels/slack.test.ts | 3 +- .../src/channels/webhook.test.ts | 78 +- packages/subscriptions/src/dispatcher.test.ts | 51 +- packages/subscriptions/src/test-preload.ts | 3 +- packages/subscriptions/vitest.config.ts | 3 + packages/test-utils/package.json | 9 +- packages/test-utils/src/index.ts | 27 - packages/test-utils/src/matchers.ts | 95 -- packages/test-utils/src/mock-fn.ts | 112 --- packages/test-utils/src/runner.ts | 58 -- packages/test-utils/src/vitest.ts | 91 ++ packages/test-utils/tsconfig.json | 4 + packages/theme-store/package.json | 8 +- .../src/__tests__/custom-theme.test.ts | 3 +- .../src/__tests__/theme-io.test.ts | 3 +- packages/theme-store/vitest.config.ts | 3 + packages/tinybird/package.json | 8 +- packages/tinybird/src/client.test.ts | 3 +- packages/tinybird/src/schema.test.ts | 3 +- packages/tinybird/vitest.config.ts | 3 + packages/tracker/package.json | 8 +- packages/tracker/src/tracker.test.ts | 3 +- packages/tracker/src/utils.test.ts | 3 +- packages/tracker/vitest.config.ts | 3 + packages/upstash/package.json | 8 +- .../upstash/src/redis/incr-with-ttl.test.ts | 3 +- packages/upstash/vitest.config.ts | 3 + packages/utils/package.json | 8 +- packages/utils/src/curl.test.ts | 69 +- packages/utils/src/headers.test.ts | 3 +- packages/utils/src/payloads.test.ts | 3 +- packages/utils/src/person-name.test.ts | 3 +- packages/utils/src/ssrf.test.ts | 3 +- packages/utils/src/status.test.ts | 3 +- packages/utils/src/stream.test.ts | 3 +- packages/utils/vitest.config.ts | 3 + pnpm-lock.yaml | 841 ++++++++++++------ pnpm-workspace.yaml | 2 + 522 files changed, 3161 insertions(+), 3124 deletions(-) create mode 100644 apps/dashboard/vitest.config.ts delete mode 100644 apps/server/src/libs/test/preload.ts create mode 100644 apps/server/vitest.config.ts create mode 100644 apps/status-page/vitest.config.ts create mode 100644 apps/web/vitest.config.ts create mode 100644 apps/workflows/src/test/stub-audit-log.ts create mode 100644 apps/workflows/vitest.config.ts create mode 100644 packages/ai/vitest.config.ts create mode 100644 packages/api/vitest.config.ts create mode 100644 packages/assertions/vitest.config.ts create mode 100644 packages/db/vitest.config.ts create mode 100644 packages/emails/vitest.config.ts create mode 100644 packages/header-analysis/vitest.config.ts create mode 100644 packages/importers/vitest.config.ts create mode 100644 packages/notifications/base/vitest.config.ts create mode 100644 packages/notifications/bird-whatsapp/vitest.config.ts create mode 100644 packages/notifications/discord/vitest.config.ts create mode 100644 packages/notifications/email/vitest.config.ts create mode 100644 packages/notifications/google-chat/vitest.config.ts create mode 100644 packages/notifications/grafana-oncall/vitest.config.ts create mode 100644 packages/notifications/ms-teams/vitest.config.ts create mode 100644 packages/notifications/ntfy/vitest.config.ts create mode 100644 packages/notifications/opsgenie/vitest.config.ts create mode 100644 packages/notifications/pagerduty/vitest.config.ts create mode 100644 packages/notifications/slack/vitest.config.ts create mode 100644 packages/notifications/telegram/vitest.config.ts create mode 100644 packages/notifications/twillio-sms/vitest.config.ts create mode 100644 packages/notifications/webhook/vitest.config.ts create mode 100644 packages/services/vitest.config.ts create mode 100644 packages/status-fetcher/vitest.config.ts create mode 100644 packages/subscriptions/vitest.config.ts delete mode 100644 packages/test-utils/src/index.ts delete mode 100644 packages/test-utils/src/matchers.ts delete mode 100644 packages/test-utils/src/mock-fn.ts delete mode 100644 packages/test-utils/src/runner.ts create mode 100644 packages/test-utils/src/vitest.ts create mode 100644 packages/test-utils/tsconfig.json create mode 100644 packages/theme-store/vitest.config.ts create mode 100644 packages/tinybird/vitest.config.ts create mode 100644 packages/tracker/vitest.config.ts create mode 100644 packages/upstash/vitest.config.ts create mode 100644 packages/utils/vitest.config.ts diff --git a/AGENTS.md b/AGENTS.md index 52b0e68d9..85176e90a 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -7,7 +7,7 @@ this file never restates them. ## Verify your change ```sh -pnpm verify # oxfmt + oxlint + doc refs + tsc (deno check in test-utils). No database, seconds. +pnpm verify # oxfmt + oxlint + doc refs + tsc. No database, seconds. pnpm verify:test # tests for packages affected by your diff. Needs a database. ``` @@ -19,7 +19,7 @@ are in `apps/dashboard/README.md`. `devbox.json` pins exact node, deno, bun, turso-cli and sqld versions; the CI workflows pin the same node and deno. Never float one back to `@latest` or -`v2.x` — `tsc` and `deno test` results differ by version, so a drifting pin makes +`v2.x` — `tsc` and test results differ by version, so a drifting pin makes `pnpm verify` disagree with CI for reasons unrelated to your change. ## Architecture diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 9146fe34c..b59f41ec8 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -33,9 +33,6 @@ To contribute code changes, follow these steps: no database and takes seconds. `pnpm verify:test` runs the tests for the packages your diff touches; those do need a local libSQL server. - Note: tests still run on `deno test` and `packages/test-utils` still - type-checks with [Deno](https://deno.com/), so both `pnpm verify` and - `pnpm verify:test` need it installed locally. See the [Requirements](./README.md#requirements) list in the README for the full toolchain (Node.js, pnpm, Bun, Deno, Turso CLI). 4. Make commits with clear and descriptive messages. Each commit should have a single logical purpose. diff --git a/apps/dashboard/package.json b/apps/dashboard/package.json index 030f4355a..f392c67cb 100644 --- a/apps/dashboard/package.json +++ b/apps/dashboard/package.json @@ -9,7 +9,7 @@ "lint": "next lint", "check": "tsc --noEmit", "tsc": "tsc --noEmit", - "test": "deno test --parallel -A --no-check --sloppy-imports --import-map=test.importmap.json src" + "test": "vitest run" }, "dependencies": { "@ai-sdk/react": "catalog:", @@ -107,8 +107,7 @@ "zod": "catalog:" }, "devDependencies": { - "@std/expect": "jsr:^1.0.19", - "@std/testing": "jsr:^1.0.19", + "@openstatus/test-utils": "workspace:*", "@tailwindcss/postcss": "catalog:", "@tailwindcss/typography": "catalog:", "@types/dom-speech-recognition": "catalog:", @@ -118,6 +117,7 @@ "shadcn": "catalog:", "tailwindcss": "catalog:", "tw-animate-css": "catalog:", - "typescript": "catalog:" + "typescript": "catalog:", + "vitest": "catalog:" } } diff --git a/apps/dashboard/src/components/chat/tool-renderers/details.test.tsx b/apps/dashboard/src/components/chat/tool-renderers/details.test.tsx index 0605463ff..acf996b73 100644 --- a/apps/dashboard/src/components/chat/tool-renderers/details.test.tsx +++ b/apps/dashboard/src/components/chat/tool-renderers/details.test.tsx @@ -1,7 +1,6 @@ -import { expect } from "@std/expect"; -import { describe, it } from "@std/testing/bdd"; import type { ReactNode } from "react"; import { renderToStaticMarkup } from "react-dom/server"; +import { describe, expect, it } from "vitest"; import type { DetailsTableData } from "./details-table"; import { getMonitorDetails } from "./get-monitor"; @@ -198,7 +197,7 @@ describe("getResponseLogDetails", () => { }); // statusCode is nullable, but TableCellNumber renders Number(null) as 0. - it.ignore("does not show 0 for a missing status code", () => { + it.skip("does not show 0 for a missing status code", () => { const data = getResponseLogDetails( input, asFixture({ ...log, statusCode: null }), diff --git a/apps/dashboard/src/components/chat/tool-renderers/get-audit-log.test.ts b/apps/dashboard/src/components/chat/tool-renderers/get-audit-log.test.ts index a7ed3b83a..47c58b113 100644 --- a/apps/dashboard/src/components/chat/tool-renderers/get-audit-log.test.ts +++ b/apps/dashboard/src/components/chat/tool-renderers/get-audit-log.test.ts @@ -1,5 +1,4 @@ -import { expect } from "@std/expect"; -import { describe, it } from "@std/testing/bdd"; +import { describe, expect, it } from "vitest"; import { getAuditLogChanges } from "./get-audit-log"; diff --git a/apps/dashboard/src/components/chat/tool-renderers/incidents.test.tsx b/apps/dashboard/src/components/chat/tool-renderers/incidents.test.tsx index 42bcb56fc..c4eeb8073 100644 --- a/apps/dashboard/src/components/chat/tool-renderers/incidents.test.tsx +++ b/apps/dashboard/src/components/chat/tool-renderers/incidents.test.tsx @@ -1,7 +1,6 @@ -import { expect } from "@std/expect"; -import { describe, it } from "@std/testing/bdd"; import type { ReactNode } from "react"; import { renderToStaticMarkup } from "react-dom/server"; +import { describe, expect, it } from "vitest"; import { declareIncidentChanges, diff --git a/apps/dashboard/src/components/chat/tool-renderers/list-tables.test.tsx b/apps/dashboard/src/components/chat/tool-renderers/list-tables.test.tsx index 2d8a97e17..ee73fa873 100644 --- a/apps/dashboard/src/components/chat/tool-renderers/list-tables.test.tsx +++ b/apps/dashboard/src/components/chat/tool-renderers/list-tables.test.tsx @@ -1,7 +1,6 @@ -import { expect } from "@std/expect"; -import { describe, it } from "@std/testing/bdd"; import type { ReactNode } from "react"; import { renderToStaticMarkup } from "react-dom/server"; +import { describe, expect, it } from "vitest"; import { listMonitorsTable } from "./list-monitors"; import { listNotificationsTable } from "./list-notifications"; diff --git a/apps/dashboard/src/components/chat/tool-renderers/maintenance-changes.test.ts b/apps/dashboard/src/components/chat/tool-renderers/maintenance-changes.test.ts index d3f5c954f..106b865af 100644 --- a/apps/dashboard/src/components/chat/tool-renderers/maintenance-changes.test.ts +++ b/apps/dashboard/src/components/chat/tool-renderers/maintenance-changes.test.ts @@ -1,5 +1,4 @@ -import { expect } from "@std/expect"; -import { describe, it } from "@std/testing/bdd"; +import { describe, expect, it } from "vitest"; import { createMaintenanceChanges } from "./create-maintenance"; import { diff --git a/apps/dashboard/src/components/chat/tool-renderers/registry.test.tsx b/apps/dashboard/src/components/chat/tool-renderers/registry.test.tsx index 40e7a274a..0685c0baa 100644 --- a/apps/dashboard/src/components/chat/tool-renderers/registry.test.tsx +++ b/apps/dashboard/src/components/chat/tool-renderers/registry.test.tsx @@ -1,7 +1,13 @@ -import { expect } from "@std/expect"; -import { afterEach, beforeEach, describe, it } from "@std/testing/bdd"; -import { type Stub, stub } from "@std/testing/mock"; import { renderToStaticMarkup } from "react-dom/server"; +import { + afterEach, + beforeEach, + describe, + expect, + it, + vi, + type MockInstance, +} from "vitest"; import { renderToolDraft, @@ -10,11 +16,13 @@ import { toolRenderers, } from "./index"; -let warnStub: Stub | undefined; +let warnStub: MockInstance | undefined; beforeEach(() => { - warnStub = stub(console, "warn"); + warnStub = vi + .spyOn(console, "warn") + .mockImplementation(() => undefined as never); }); -afterEach(() => warnStub?.restore()); +afterEach(() => warnStub?.mockRestore()); describe("summarizeToolOutput", () => { it("pluralises item counts", () => { @@ -129,7 +137,7 @@ describe("unknown tools", () => { expect(summarizeToolOutput("not_a_tool", {})).toBeUndefined(); expect(renderToolDraft("not_a_tool", {})).toBeUndefined(); expect(renderToolResult("not_a_tool", {}, {})).toBeUndefined(); - expect(warnStub?.calls).toHaveLength(1); + expect(warnStub?.mock.calls).toHaveLength(1); }); }); diff --git a/apps/dashboard/src/components/chat/tool-renderers/status-report-changes.test.ts b/apps/dashboard/src/components/chat/tool-renderers/status-report-changes.test.ts index bf3d1f6cf..83f3c5187 100644 --- a/apps/dashboard/src/components/chat/tool-renderers/status-report-changes.test.ts +++ b/apps/dashboard/src/components/chat/tool-renderers/status-report-changes.test.ts @@ -1,5 +1,4 @@ -import { expect } from "@std/expect"; -import { describe, it } from "@std/testing/bdd"; +import { describe, expect, it } from "vitest"; import { addStatusReportUpdateChanges } from "./add-status-report-update"; import { createStatusReportChanges } from "./create-status-report"; diff --git a/apps/dashboard/src/components/chat/tool-renderers/tables.test.tsx b/apps/dashboard/src/components/chat/tool-renderers/tables.test.tsx index 7ebe952aa..d282439da 100644 --- a/apps/dashboard/src/components/chat/tool-renderers/tables.test.tsx +++ b/apps/dashboard/src/components/chat/tool-renderers/tables.test.tsx @@ -1,7 +1,6 @@ -import { expect } from "@std/expect"; -import { describe, it } from "@std/testing/bdd"; import type { ReactNode } from "react"; import { renderToStaticMarkup } from "react-dom/server"; +import { describe, expect, it } from "vitest"; import { DetailsTable } from "./details-table"; import { getMonitorStatusTable } from "./get-monitor-status"; diff --git a/apps/dashboard/src/components/command-menu/groups/entities.test.ts b/apps/dashboard/src/components/command-menu/groups/entities.test.ts index 213981dde..41ac4b4cb 100644 --- a/apps/dashboard/src/components/command-menu/groups/entities.test.ts +++ b/apps/dashboard/src/components/command-menu/groups/entities.test.ts @@ -1,5 +1,4 @@ -import { expect } from "@std/expect"; -import { describe, it } from "@std/testing/bdd"; +import { describe, expect, it } from "vitest"; import { incidentsGroup, diff --git a/apps/dashboard/src/components/command-menu/groups/scope.test.ts b/apps/dashboard/src/components/command-menu/groups/scope.test.ts index 31ce56dd8..20e7ae915 100644 --- a/apps/dashboard/src/components/command-menu/groups/scope.test.ts +++ b/apps/dashboard/src/components/command-menu/groups/scope.test.ts @@ -1,5 +1,4 @@ -import { expect } from "@std/expect"; -import { describe, it } from "@std/testing/bdd"; +import { describe, expect, it } from "vitest"; import { MONITOR_TABS } from "@/app/(dashboard)/monitors/[id]/constants"; import { STATUS_PAGE_TABS } from "@/app/(dashboard)/status-pages/[id]/constants"; diff --git a/apps/dashboard/src/components/command-menu/groups/static.test.ts b/apps/dashboard/src/components/command-menu/groups/static.test.ts index 4df096607..70d751382 100644 --- a/apps/dashboard/src/components/command-menu/groups/static.test.ts +++ b/apps/dashboard/src/components/command-menu/groups/static.test.ts @@ -1,5 +1,4 @@ -import { expect } from "@std/expect"; -import { describe, it } from "@std/testing/bdd"; +import { describe, expect, it } from "vitest"; import { HELP_LINKS } from "@/config/help"; import { SETTINGS_TABS } from "@/config/settings"; diff --git a/apps/dashboard/src/components/command-menu/types.test.ts b/apps/dashboard/src/components/command-menu/types.test.ts index 7f3ae5965..4a7aacc39 100644 --- a/apps/dashboard/src/components/command-menu/types.test.ts +++ b/apps/dashboard/src/components/command-menu/types.test.ts @@ -1,5 +1,4 @@ -import { expect } from "@std/expect"; -import { describe, it } from "@std/testing/bdd"; +import { describe, expect, it } from "vitest"; import { pageLabel } from "./types"; diff --git a/apps/dashboard/src/components/common/changes-table.test.tsx b/apps/dashboard/src/components/common/changes-table.test.tsx index 036df330a..97506c597 100644 --- a/apps/dashboard/src/components/common/changes-table.test.tsx +++ b/apps/dashboard/src/components/common/changes-table.test.tsx @@ -1,6 +1,5 @@ -import { expect } from "@std/expect"; -import { describe, it } from "@std/testing/bdd"; import { renderToStaticMarkup } from "react-dom/server"; +import { describe, expect, it } from "vitest"; import { ChangesTable, diff --git a/apps/dashboard/src/components/common/status-badge.test.tsx b/apps/dashboard/src/components/common/status-badge.test.tsx index e504b0430..802465db0 100644 --- a/apps/dashboard/src/components/common/status-badge.test.tsx +++ b/apps/dashboard/src/components/common/status-badge.test.tsx @@ -1,6 +1,5 @@ -import { expect } from "@std/expect"; -import { describe, it } from "@std/testing/bdd"; import { renderToStaticMarkup } from "react-dom/server"; +import { describe, expect, it } from "vitest"; import { StatusBadge } from "./status-badge"; diff --git a/apps/dashboard/src/components/data-table/table-cell-metadata.test.tsx b/apps/dashboard/src/components/data-table/table-cell-metadata.test.tsx index 87eb14ab9..9b7c128a6 100644 --- a/apps/dashboard/src/components/data-table/table-cell-metadata.test.tsx +++ b/apps/dashboard/src/components/data-table/table-cell-metadata.test.tsx @@ -1,6 +1,5 @@ -import { expect } from "@std/expect"; -import { describe, it } from "@std/testing/bdd"; import { renderToStaticMarkup } from "react-dom/server"; +import { describe, expect, it } from "vitest"; import { TableCellMetadata, getMetadataEntries } from "./table-cell-metadata"; diff --git a/apps/dashboard/src/components/incidents/incident-actions.test.ts b/apps/dashboard/src/components/incidents/incident-actions.test.ts index 6c3ea1d96..3ac8c6f3d 100644 --- a/apps/dashboard/src/components/incidents/incident-actions.test.ts +++ b/apps/dashboard/src/components/incidents/incident-actions.test.ts @@ -1,5 +1,4 @@ -import { expect } from "@std/expect"; -import { describe, it } from "@std/testing/bdd"; +import { describe, expect, it } from "vitest"; import { hasIncidentActions } from "./incident-actions"; diff --git a/apps/dashboard/src/components/onboarding/checks-table-adapter.test.ts b/apps/dashboard/src/components/onboarding/checks-table-adapter.test.ts index f47183b5d..fd61bb6b8 100644 --- a/apps/dashboard/src/components/onboarding/checks-table-adapter.test.ts +++ b/apps/dashboard/src/components/onboarding/checks-table-adapter.test.ts @@ -1,6 +1,5 @@ import type { CheckResult } from "@openstatus/services/monitor"; -import { expect } from "@std/expect"; -import { describe, it } from "@std/testing/bdd"; +import { describe, expect, it } from "vitest"; import { checkResultToResponseLog } from "./checks-table-adapter"; diff --git a/apps/dashboard/src/components/ui/checkbox-tree.test.tsx b/apps/dashboard/src/components/ui/checkbox-tree.test.tsx index 642cd27c4..9c1600e04 100644 --- a/apps/dashboard/src/components/ui/checkbox-tree.test.tsx +++ b/apps/dashboard/src/components/ui/checkbox-tree.test.tsx @@ -1,6 +1,5 @@ -import { expect } from "@std/expect"; -import { describe, it } from "@std/testing/bdd"; import { renderToStaticMarkup } from "react-dom/server"; +import { describe, expect, it } from "vitest"; import { CheckboxTree, toCheckboxTreeItems } from "./checkbox-tree"; diff --git a/apps/dashboard/src/data/entity-refs.client.test.ts b/apps/dashboard/src/data/entity-refs.client.test.ts index 1238723cf..bf8d2a07e 100644 --- a/apps/dashboard/src/data/entity-refs.client.test.ts +++ b/apps/dashboard/src/data/entity-refs.client.test.ts @@ -1,5 +1,4 @@ -import { expect } from "@std/expect"; -import { describe, it } from "@std/testing/bdd"; +import { describe, expect, it } from "vitest"; import { auditEntityKind, diff --git a/apps/dashboard/src/data/entity-refs.coverage.test.ts b/apps/dashboard/src/data/entity-refs.coverage.test.ts index 3c9a91b5d..8c2b01983 100644 --- a/apps/dashboard/src/data/entity-refs.coverage.test.ts +++ b/apps/dashboard/src/data/entity-refs.coverage.test.ts @@ -1,5 +1,4 @@ -import { expect } from "@std/expect"; -import { describe, it } from "@std/testing/bdd"; +import { describe, expect, it } from "vitest"; import { entityKindList, diff --git a/apps/dashboard/src/data/metrics.client.test.ts b/apps/dashboard/src/data/metrics.client.test.ts index 95202dc46..b67291207 100644 --- a/apps/dashboard/src/data/metrics.client.test.ts +++ b/apps/dashboard/src/data/metrics.client.test.ts @@ -1,5 +1,4 @@ -import { expect } from "@std/expect"; -import { describe, it } from "@std/testing/bdd"; +import { describe, expect, it } from "vitest"; import { getMonitorListMetrics, diff --git a/apps/dashboard/src/data/monitors.client.test.ts b/apps/dashboard/src/data/monitors.client.test.ts index 7b1865c39..eca7c54cc 100644 --- a/apps/dashboard/src/data/monitors.client.test.ts +++ b/apps/dashboard/src/data/monitors.client.test.ts @@ -1,5 +1,4 @@ -import { expect } from "@std/expect"; -import { describe, it } from "@std/testing/bdd"; +import { describe, expect, it } from "vitest"; import { getMonitorHealth } from "./monitors.client"; diff --git a/apps/dashboard/src/data/overview-events.client.test.ts b/apps/dashboard/src/data/overview-events.client.test.ts index 6137ca3fc..0d743d58b 100644 --- a/apps/dashboard/src/data/overview-events.client.test.ts +++ b/apps/dashboard/src/data/overview-events.client.test.ts @@ -1,5 +1,4 @@ -import { expect } from "@std/expect"; -import { describe, it } from "@std/testing/bdd"; +import { describe, expect, it } from "vitest"; import { buildOverviewData, diff --git a/apps/dashboard/src/data/page-components.client.test.ts b/apps/dashboard/src/data/page-components.client.test.ts index 742ecc9f2..0724d31cf 100644 --- a/apps/dashboard/src/data/page-components.client.test.ts +++ b/apps/dashboard/src/data/page-components.client.test.ts @@ -1,5 +1,4 @@ -import { expect } from "@std/expect"; -import { describe, it } from "@std/testing/bdd"; +import { describe, expect, it } from "vitest"; import { getActions, diff --git a/apps/dashboard/src/data/status-codes.test.ts b/apps/dashboard/src/data/status-codes.test.ts index 6b0e5d101..54e387e0b 100644 --- a/apps/dashboard/src/data/status-codes.test.ts +++ b/apps/dashboard/src/data/status-codes.test.ts @@ -1,5 +1,4 @@ -import { expect } from "@std/expect"; -import { describe, it } from "@std/testing/bdd"; +import { describe, expect, it } from "vitest"; import { getStatusCodeVariant } from "./status-codes"; diff --git a/apps/dashboard/src/data/status-page-history.test.ts b/apps/dashboard/src/data/status-page-history.test.ts index 9cb61f0b1..00556a1cc 100644 --- a/apps/dashboard/src/data/status-page-history.test.ts +++ b/apps/dashboard/src/data/status-page-history.test.ts @@ -1,6 +1,4 @@ -import { expect } from "@std/expect"; -import { describe, it } from "@std/testing/bdd"; -import { FakeTime } from "@std/testing/time"; +import { describe, expect, it, vi } from "vitest"; import { type HistoryEvent, @@ -70,13 +68,13 @@ describe("eventsForMonth", () => { }); it("treats open-ended events as lasting until now", () => { - const time = new FakeTime(new Date("2025-05-15T00:00:00Z")); + vi.useFakeTimers({ now: new Date("2025-05-15T00:00:00Z") }); try { const open = event("2025-03-20T00:00:00Z", null); expect(eventsForMonth([open], "2025-04")).toEqual([open]); expect(eventsForMonth([open], "2025-06")).toEqual([]); } finally { - time.restore(); + vi.useRealTimers(); } }); }); diff --git a/apps/dashboard/src/data/status-report-updates.client.test.ts b/apps/dashboard/src/data/status-report-updates.client.test.ts index b9aa9b4a3..dd1538a46 100644 --- a/apps/dashboard/src/data/status-report-updates.client.test.ts +++ b/apps/dashboard/src/data/status-report-updates.client.test.ts @@ -1,5 +1,4 @@ -import { expect } from "@std/expect"; -import { describe, it } from "@std/testing/bdd"; +import { describe, expect, it } from "vitest"; import { isComponentImpactList, diff --git a/apps/dashboard/src/data/status-reports.client.test.ts b/apps/dashboard/src/data/status-reports.client.test.ts index 85dca282c..3e85c8896 100644 --- a/apps/dashboard/src/data/status-reports.client.test.ts +++ b/apps/dashboard/src/data/status-reports.client.test.ts @@ -1,5 +1,4 @@ -import { expect } from "@std/expect"; -import { describe, it } from "@std/testing/bdd"; +import { describe, expect, it } from "vitest"; import { incidentEndedAt } from "./managed-incidents.client"; import { diff --git a/apps/dashboard/src/lib/auth/providers.test.ts b/apps/dashboard/src/lib/auth/providers.test.ts index 734f82427..370a941f4 100644 --- a/apps/dashboard/src/lib/auth/providers.test.ts +++ b/apps/dashboard/src/lib/auth/providers.test.ts @@ -1,18 +1,23 @@ import "@/test-preload"; import { AuthError } from "@auth/core/errors"; import { redis } from "@openstatus/upstash"; -import { expect } from "@std/expect"; -import { afterEach, describe, test } from "@std/testing/bdd"; -import { type Stub, stub } from "@std/testing/mock"; +import { + afterEach, + describe, + expect, + test, + vi, + type MockInstance, +} from "vitest"; import { ResendProvider } from "./providers"; -let evalStub: Stub | undefined; -let logStub: Stub | undefined; +let evalStub: MockInstance | undefined; +let logStub: MockInstance | undefined; afterEach(() => { - evalStub?.restore(); - logStub?.restore(); + evalStub?.mockRestore(); + logStub?.mockRestore(); }); function sendTo(identifier: string, ip = "1.2.3.4") { @@ -34,17 +39,21 @@ function sendTo(identifier: string, ip = "1.2.3.4") { describe("ResendProvider.sendVerificationRequest", () => { test("refuses disposable domains before touching the rate limit", async () => { - evalStub = stub(redis, "eval"); + evalStub = vi + .spyOn(redis, "eval") + .mockImplementation(() => undefined as never); await expect(sendTo("a@mailinator.com")).rejects.toThrow(AuthError); await expect(sendTo("a@mailinator.com")).rejects.toThrow( "disposable domain", ); - expect(evalStub.calls).toHaveLength(0); + expect(evalStub.mock.calls).toHaveLength(0); }); test("refuses throttled addresses", async () => { - evalStub = stub(redis, "eval", () => Promise.resolve([1, 4])); + evalStub = vi + .spyOn(redis, "eval") + .mockImplementation(() => Promise.resolve([1, 4])); await expect(sendTo("gilfoyle@piedpiper.dev")).rejects.toThrow( "rate limited", @@ -52,16 +61,20 @@ describe("ResendProvider.sendVerificationRequest", () => { }); test("sends when screening passes, keyed by client ip and address", async () => { - evalStub = stub(redis, "eval", () => Promise.resolve([1, 1])); - logStub = stub(console, "log"); + evalStub = vi + .spyOn(redis, "eval") + .mockImplementation(() => Promise.resolve([1, 1])); + logStub = vi + .spyOn(console, "log") + .mockImplementation(() => undefined as never); await sendTo("gilfoyle@piedpiper.dev", "9.9.9.9"); - expect(evalStub.calls[0]?.args[1]).toEqual([ + expect(evalStub.mock.calls[0]?.[1]).toEqual([ "ratelimit:magic-link:ip:9.9.9.9", "ratelimit:magic-link:email:gilfoyle@piedpiper.dev", ]); - const printed = logStub.calls.map((c) => String(c.args[0])).join("\n"); + const printed = logStub.mock.calls.map((c) => String(c[0])).join("\n"); expect(printed).toContain( "https://app.openstatus.dev/api/auth/callback/resend?token=t", ); diff --git a/apps/dashboard/src/lib/composition.test.ts b/apps/dashboard/src/lib/composition.test.ts index 1f21141e1..2e7cf8146 100644 --- a/apps/dashboard/src/lib/composition.test.ts +++ b/apps/dashboard/src/lib/composition.test.ts @@ -1,5 +1,4 @@ -import { expect } from "@std/expect"; -import { describe, it } from "@std/testing/bdd"; +import { describe, expect, it } from "vitest"; import { composeEventHandlers, composeRefs } from "./composition"; diff --git a/apps/dashboard/src/lib/domains.test.ts b/apps/dashboard/src/lib/domains.test.ts index e24e0d3a9..3023180b4 100644 --- a/apps/dashboard/src/lib/domains.test.ts +++ b/apps/dashboard/src/lib/domains.test.ts @@ -1,14 +1,22 @@ -import { expect } from "@std/expect"; -import { afterEach, beforeEach, describe, it } from "@std/testing/bdd"; -import { type Stub, stub } from "@std/testing/mock"; +import { + afterEach, + beforeEach, + describe, + expect, + it, + vi, + type MockInstance, +} from "vitest"; import { extractDomain, getApexDomain, getSubdomain } from "./domains"; -let errorStub: Stub | undefined; +let errorStub: MockInstance | undefined; beforeEach(() => { - errorStub = stub(console, "error"); + errorStub = vi + .spyOn(console, "error") + .mockImplementation(() => undefined as never); }); -afterEach(() => errorStub?.restore()); +afterEach(() => errorStub?.mockRestore()); describe("getSubdomain", () => { it("returns null for the apex itself", () => { diff --git a/apps/dashboard/src/lib/formatter.test.ts b/apps/dashboard/src/lib/formatter.test.ts index 9612f3d97..4255fd71c 100644 --- a/apps/dashboard/src/lib/formatter.test.ts +++ b/apps/dashboard/src/lib/formatter.test.ts @@ -1,6 +1,4 @@ -import { expect } from "@std/expect"; -import { describe, it } from "@std/testing/bdd"; -import { FakeTime } from "@std/testing/time"; +import { describe, expect, it, vi } from "vitest"; import { formatDateForInput, @@ -95,24 +93,24 @@ describe("formatDistanceToNowShort", () => { [10 * 24 * 60 * 60, "10d ago"], ] as const) { it(`${secondsAgo}s -> ${expected}`, () => { - const time = new FakeTime(now); + vi.useFakeTimers({ now }); try { const date = new Date(now.getTime() - secondsAgo * 1000); expect(formatDistanceToNowShort(date)).toBe(expected); } finally { - time.restore(); + vi.useRealTimers(); } }); } it("clamps future dates to 0s", () => { - const time = new FakeTime(now); + vi.useFakeTimers({ now }); try { expect(formatDistanceToNowShort(new Date(now.getTime() + 60_000))).toBe( "0s ago", ); } finally { - time.restore(); + vi.useRealTimers(); } }); }); diff --git a/apps/dashboard/src/lib/keyboard.test.ts b/apps/dashboard/src/lib/keyboard.test.ts index 3ed769787..a9025849c 100644 --- a/apps/dashboard/src/lib/keyboard.test.ts +++ b/apps/dashboard/src/lib/keyboard.test.ts @@ -1,5 +1,4 @@ -import { expect } from "@std/expect"; -import { afterAll, beforeAll, describe, it } from "@std/testing/bdd"; +import { afterAll, beforeAll, describe, expect, it } from "vitest"; import { getModifierKey, isKeyboardOwned, isResetShortcut } from "./keyboard"; diff --git a/apps/dashboard/src/lib/onboarding/email-domain.test.ts b/apps/dashboard/src/lib/onboarding/email-domain.test.ts index 34112fc3d..e3f700146 100644 --- a/apps/dashboard/src/lib/onboarding/email-domain.test.ts +++ b/apps/dashboard/src/lib/onboarding/email-domain.test.ts @@ -1,5 +1,4 @@ -import { expect } from "@std/expect"; -import { describe, it } from "@std/testing/bdd"; +import { describe, expect, it } from "vitest"; import { getCompanyDomainFromEmail } from "./email-domain"; diff --git a/apps/dashboard/src/lib/rate-limit/magic-link.test.ts b/apps/dashboard/src/lib/rate-limit/magic-link.test.ts index 966e59599..383f519ec 100644 --- a/apps/dashboard/src/lib/rate-limit/magic-link.test.ts +++ b/apps/dashboard/src/lib/rate-limit/magic-link.test.ts @@ -1,23 +1,32 @@ import "@/test-preload"; import { redis } from "@openstatus/upstash"; -import { expect } from "@std/expect"; -import { afterEach, describe, test } from "@std/testing/bdd"; -import { type Stub, stub } from "@std/testing/mock"; +import { + afterEach, + describe, + expect, + test, + vi, + type MockInstance, +} from "vitest"; import { magicLinkRateLimit } from "./magic-link"; -let evalStub: Stub | undefined; -let warnStub: Stub | undefined; +let evalStub: MockInstance | undefined; +let warnStub: MockInstance | undefined; function stubCounts(counts: number[] | Error) { - evalStub = stub(redis, "eval", () => - counts instanceof Error ? Promise.reject(counts) : Promise.resolve(counts), - ); + evalStub = vi + .spyOn(redis, "eval") + .mockImplementation(() => + counts instanceof Error + ? Promise.reject(counts) + : Promise.resolve(counts), + ); } afterEach(() => { - evalStub?.restore(); - warnStub?.restore(); + evalStub?.mockRestore(); + warnStub?.mockRestore(); }); describe("magicLinkRateLimit", () => { @@ -26,11 +35,11 @@ describe("magicLinkRateLimit", () => { await magicLinkRateLimit({ ip: "1.2.3.4", email: "a@b.c" }); - expect(evalStub?.calls[0]?.args[1]).toEqual([ + expect(evalStub?.mock.calls[0]?.[1]).toEqual([ "ratelimit:magic-link:ip:1.2.3.4", "ratelimit:magic-link:email:a@b.c", ]); - expect(evalStub?.calls[0]?.args[2]).toEqual([600]); + expect(evalStub?.mock.calls[0]?.[2]).toEqual([600]); }); test("lowercases the email key", async () => { @@ -38,7 +47,7 @@ describe("magicLinkRateLimit", () => { await magicLinkRateLimit({ ip: "ip", email: " Gilfoyle@PiedPiper.dev " }); - expect(evalStub?.calls[0]?.args[1]).toEqual([ + expect(evalStub?.mock.calls[0]?.[1]).toEqual([ "ratelimit:magic-link:ip:ip", "ratelimit:magic-link:email:gilfoyle@piedpiper.dev", ]); @@ -61,9 +70,11 @@ describe("magicLinkRateLimit", () => { test("fails open with a warning when redis is down", async () => { stubCounts(new Error("redis down")); - warnStub = stub(console, "warn"); + warnStub = vi + .spyOn(console, "warn") + .mockImplementation(() => undefined as never); expect(await magicLinkRateLimit({ ip: "ip", email: "e" })).toBe(true); - expect(warnStub.calls).toHaveLength(1); + expect(warnStub.mock.calls).toHaveLength(1); }); }); diff --git a/apps/dashboard/src/test/next-link.mock.tsx b/apps/dashboard/src/test/next-link.mock.tsx index 03dcc1836..95afbc9b2 100644 --- a/apps/dashboard/src/test/next-link.mock.tsx +++ b/apps/dashboard/src/test/next-link.mock.tsx @@ -1,5 +1,5 @@ -// Test double for next/link, swapped in via --import-map: Deno cannot resolve -// Next's extensionless CJS entrypoints. Renders a plain anchor. +// Test double for next/link, swapped in via test.importmap.json. Renders a +// plain anchor. import type * as React from "react"; type LinkProps = Omit, "href"> & { diff --git a/apps/dashboard/vitest.config.ts b/apps/dashboard/vitest.config.ts new file mode 100644 index 000000000..2debbded5 --- /dev/null +++ b/apps/dashboard/vitest.config.ts @@ -0,0 +1,6 @@ +import { defineTestConfig } from "@openstatus/test-utils/vitest"; + +export default defineTestConfig(import.meta.dirname, { + importMap: "test.importmap.json", + include: ["src/**/*.test.{ts,tsx}"], +}); diff --git a/apps/server/AGENTS.md b/apps/server/AGENTS.md index 0a296c44f..3b671cad4 100644 --- a/apps/server/AGENTS.md +++ b/apps/server/AGENTS.md @@ -25,9 +25,8 @@ the handlers that have migrated, and that list grows one domain per PR. Resolve config once and pass it in; `createSlackRoute(config)` in `apps/server/src/routes/slack/index.ts` is the pattern — production calls `slackConfigFromEnv()` at module scope, tests build a -route with explicit config. Reading `env` at request time is what made the slack -route untestable under `deno test --parallel`, since the workers share one -process environment. +route with explicit config. Reading `env` at request time would force tests to +mutate the shared `process.env`, which leaks between cases. ## MCP diff --git a/apps/server/dofigen.lock b/apps/server/dofigen.lock index a9b3206b7..8bc98d877 100644 --- a/apps/server/dofigen.lock +++ b/apps/server/dofigen.lock @@ -10,42 +10,13 @@ effective: | - /packages/api - /packages/integrations/vercel builders: - build: - fromImage: - path: node - digest: sha256:c0753125a3789977aefe869cbebccf70e3cfd7ea84ca48547458f02e4f1d7146 - label: - org.opencontainers.image.base.name: docker.io/node:26.8.1-slim - org.opencontainers.image.base.digest: sha256:c0753125a3789977aefe869cbebccf70e3cfd7ea84ca48547458f02e4f1d7146 - org.opencontainers.image.stage: build - workdir: /app/apps/server - env: - NODE_ENV: production - copy: - - paths: - - . - target: /app/ - - fromBuilder: install - paths: - - /app/node_modules - target: /app/node_modules - - fromBuilder: install - paths: - - /app/apps/server/node_modules - target: /app/apps/server/node_modules - - fromBuilder: install - paths: - - /app/packages - target: /app/packages - run: - - ./node_modules/.bin/tsdown install: fromImage: path: node digest: sha256:c0753125a3789977aefe869cbebccf70e3cfd7ea84ca48547458f02e4f1d7146 label: - org.opencontainers.image.stage: install org.opencontainers.image.base.digest: sha256:c0753125a3789977aefe869cbebccf70e3cfd7ea84ca48547458f02e4f1d7146 + org.opencontainers.image.stage: install org.opencontainers.image.base.name: docker.io/node:26.8.1-slim workdir: /app/ run: @@ -127,18 +98,47 @@ effective: | source: packages/services/package.json - target: packages/importers/package.json source: packages/importers/package.json + build: + fromImage: + path: node + digest: sha256:c0753125a3789977aefe869cbebccf70e3cfd7ea84ca48547458f02e4f1d7146 + label: + org.opencontainers.image.base.digest: sha256:c0753125a3789977aefe869cbebccf70e3cfd7ea84ca48547458f02e4f1d7146 + org.opencontainers.image.stage: build + org.opencontainers.image.base.name: docker.io/node:26.8.1-slim + workdir: /app/apps/server + env: + NODE_ENV: production + copy: + - paths: + - . + target: /app/ + - fromBuilder: install + paths: + - /app/node_modules + target: /app/node_modules + - fromBuilder: install + paths: + - /app/apps/server/node_modules + target: /app/apps/server/node_modules + - fromBuilder: install + paths: + - /app/packages + target: /app/packages + run: + - ./node_modules/.bin/tsdown fromImage: path: node digest: sha256:c0753125a3789977aefe869cbebccf70e3cfd7ea84ca48547458f02e4f1d7146 label: - org.opencontainers.image.base.name: docker.io/node:26.8.1-slim - io.dofigen.version: 2.8.0 - org.opencontainers.image.source: https://github.com/openstatusHQ/openstatus - org.opencontainers.image.authors: OpenStatus Team org.opencontainers.image.title: OpenStatus Server org.opencontainers.image.vendor: OpenStatus - org.opencontainers.image.description: REST API server with Hono framework for OpenStatus org.opencontainers.image.base.digest: sha256:c0753125a3789977aefe869cbebccf70e3cfd7ea84ca48547458f02e4f1d7146 + org.opencontainers.image.description: REST API server with Hono framework for OpenStatus + org.opencontainers.image.source: https://github.com/openstatusHQ/openstatus + io.dofigen.version: 2.8.0 + org.opencontainers.image.base.name: docker.io/node:26.8.1-slim + org.opencontainers.image.authors: OpenStatus Team user: user: '1000' group: '1000' diff --git a/apps/server/package.json b/apps/server/package.json index 68be92d3b..d0f8b2348 100644 --- a/apps/server/package.json +++ b/apps/server/package.json @@ -8,13 +8,13 @@ "env": "node env.ts", "dev": "node --import @oxc-node/core/register --watch --env-file=.env src/serve.ts", "start": "NODE_ENV=production node dist/serve.mjs", - "test": "deno test --parallel -A --no-check --sloppy-imports --env-file=.env.test --import-map=test.importmap.json", + "test": "vitest run", "check": "tsc --noEmit", "slack:manifest:prod": "cat slack-manifest.json", "slack:manifest:dev": "node --import @oxc-node/core/register --env-file=.env scripts/slack-manifest-dev.ts", "eval:mcp": "node --import @oxc-node/core/register src/routes/mcp/evals/run.ts", "openapi:json": "node --import @oxc-node/core/register scripts/build-openapi-json.ts && pnpm exec oxfmt static/openapi.json static/openapi-yaml.ts", - "openapi:v1": "node --env-file=.env.test --import @oxc-node/core/register scripts/build-openapi-v1-json.ts && pnpm exec oxfmt static/openapi-v1.json", + "openapi:v1": "node --import @oxc-node/core/register --env-file=.env.test scripts/build-openapi-v1-json.ts && pnpm exec oxfmt static/openapi-v1.json", "build": "tsdown" }, "dependencies": { @@ -87,13 +87,12 @@ "@openstatus/test-utils": "workspace:*", "@openstatus/tsconfig": "workspace:*", "@oxc-node/core": "catalog:", - "@std/expect": "jsr:^1.0.19", - "@std/testing": "jsr:^1.0.19", "@std/yaml": "jsr:1.3.0", "@types/node": "catalog:", "@types/validator": "catalog:", "dotenv": "catalog:", "tsdown": "catalog:", - "typescript": "catalog:" + "typescript": "catalog:", + "vitest": "catalog:" } } diff --git a/apps/server/src/libs/checker/utils.test.ts b/apps/server/src/libs/checker/utils.test.ts index c1417407d..683b66191 100644 --- a/apps/server/src/libs/checker/utils.test.ts +++ b/apps/server/src/libs/checker/utils.test.ts @@ -1,6 +1,6 @@ import type { z } from "@hono/zod-openapi"; import type { selectMonitorSchema } from "@openstatus/db/src/schema"; -import { expect, test } from "@openstatus/test-utils"; +import { expect, test } from "vitest"; import { OpenStatusApiError } from "@/libs/errors"; diff --git a/apps/server/src/libs/cli-telemetry.test.ts b/apps/server/src/libs/cli-telemetry.test.ts index e072dedb2..913ba365d 100644 --- a/apps/server/src/libs/cli-telemetry.test.ts +++ b/apps/server/src/libs/cli-telemetry.test.ts @@ -1,6 +1,5 @@ import { Events } from "@openstatus/analytics"; -import { expect } from "@std/expect"; -import { describe, test } from "@std/testing/bdd"; +import { describe, expect, test } from "vitest"; import { claimCliCommandEvent, diff --git a/apps/server/src/libs/middlewares/concurrency.test.ts b/apps/server/src/libs/middlewares/concurrency.test.ts index 32b837bfc..4db594f9d 100644 --- a/apps/server/src/libs/middlewares/concurrency.test.ts +++ b/apps/server/src/libs/middlewares/concurrency.test.ts @@ -1,7 +1,6 @@ -import { expect } from "@std/expect"; -import { describe, test } from "@std/testing/bdd"; import { Hono } from "hono"; import { requestId } from "hono/request-id"; +import { describe, expect, test } from "vitest"; import { createConcurrencyGuard } from "./concurrency"; diff --git a/apps/server/src/libs/middlewares/credentials.test.ts b/apps/server/src/libs/middlewares/credentials.test.ts index 4acffc5f1..738114d79 100644 --- a/apps/server/src/libs/middlewares/credentials.test.ts +++ b/apps/server/src/libs/middlewares/credentials.test.ts @@ -1,5 +1,4 @@ -import { expect } from "@std/expect"; -import { describe, test } from "@std/testing/bdd"; +import { describe, expect, test } from "vitest"; import { extractCredential } from "./credentials"; diff --git a/apps/server/src/libs/middlewares/rate-limit.test.ts b/apps/server/src/libs/middlewares/rate-limit.test.ts index 860efe3b1..d7ed0d232 100644 --- a/apps/server/src/libs/middlewares/rate-limit.test.ts +++ b/apps/server/src/libs/middlewares/rate-limit.test.ts @@ -1,8 +1,6 @@ -import { expect } from "@std/expect"; -import { describe, test } from "@std/testing/bdd"; -import { FakeTime } from "@std/testing/time"; import { type Context, Hono } from "hono"; import { requestId } from "hono/request-id"; +import { describe, expect, test, vi } from "vitest"; import { createRateLimit, credentialKey } from "./rate-limit"; @@ -227,23 +225,23 @@ describe("rate limit", () => { test("the 10 s bucket recovers after its window while the minute window keeps counting", async () => { const { app } = recovery; - const time = new FakeTime(); + vi.useFakeTimers(); try { const init = { headers: keyHeaders("a") }; expect((await app.request("/v1/monitor", init)).status).toBe(200); expect((await app.request("/v1/monitor", init)).status).toBe(429); - time.tick(10_001); + vi.advanceTimersByTime(10_001); expect((await app.request("/v1/monitor", init)).status).toBe(200); // minute window (limit 2) is now full even though the burst bucket reset - time.tick(10_001); + vi.advanceTimersByTime(10_001); expect((await app.request("/v1/monitor", init)).status).toBe(429); - time.tick(60_001); + vi.advanceTimersByTime(60_001); expect((await app.request("/v1/monitor", init)).status).toBe(200); } finally { - time.restore(); + vi.useRealTimers(); } }); diff --git a/apps/server/src/libs/middlewares/require-scope.test.ts b/apps/server/src/libs/middlewares/require-scope.test.ts index f1f0962a6..2de3e8731 100644 --- a/apps/server/src/libs/middlewares/require-scope.test.ts +++ b/apps/server/src/libs/middlewares/require-scope.test.ts @@ -1,7 +1,6 @@ import type { Scope } from "@openstatus/db/src/schema"; -import { expect } from "@std/expect"; -import { describe, test } from "@std/testing/bdd"; import { Hono } from "hono"; +import { describe, expect, test } from "vitest"; import type { Variables } from "@/types"; diff --git a/apps/server/src/libs/middlewares/track.test.ts b/apps/server/src/libs/middlewares/track.test.ts index 6c735b8dc..7e7851b1f 100644 --- a/apps/server/src/libs/middlewares/track.test.ts +++ b/apps/server/src/libs/middlewares/track.test.ts @@ -1,16 +1,16 @@ import { Events } from "@openstatus/analytics"; import type { Workspace } from "@openstatus/db/src/schema"; +import { Hono } from "hono"; import { afterEach, beforeEach, describe, expect, - type mock, - type Stub, - stub, + type Mock, + type MockInstance, test, -} from "@openstatus/test-utils"; -import { Hono } from "hono"; + vi, +} from "vitest"; import type { Variables } from "@/types"; @@ -21,8 +21,8 @@ import { apiTrackMiddleware } from "./track"; const { track: mockTrack, setupAnalytics: mockSetupAnalytics } = ( globalThis as Record ).__analyticsSpies as { - track: ReturnType; - setupAnalytics: ReturnType; + track: Mock; + setupAnalytics: Mock; }; /** Let the fire-and-forget analytics chain settle. */ @@ -66,17 +66,17 @@ function cliCommandCalls() { } describe("apiTrackMiddleware", () => { - let random: Stub; + let random: MockInstance<() => number>; beforeEach(() => { mockSetupAnalytics.mockClear(); mockTrack.mockClear(); // Keep every sampled request unless a test says otherwise. - random = stub(Math, "random", () => 0); + random = vi.spyOn(Math, "random").mockReturnValue(0); }); afterEach(() => { - random.restore(); + random.mockRestore(); }); test("fires api_request for GET requests with the route pattern", async () => { @@ -97,8 +97,7 @@ describe("apiTrackMiddleware", () => { }); test("drops non-CLI requests outside the sample", async () => { - random.restore(); - random = stub(Math, "random", () => 0.5); + random.mockReturnValue(0.5); const res = await makeApp().request("/whoami", { headers: { "user-agent": "curl/8" }, @@ -111,8 +110,7 @@ describe("apiTrackMiddleware", () => { }); test("always tracks CLI requests", async () => { - random.restore(); - random = stub(Math, "random", () => 0.99); + random.mockReturnValue(0.99); await makeApp().request("/whoami", { headers: cliHeaders(crypto.randomUUID()), diff --git a/apps/server/src/libs/middlewares/validate-key.test.ts b/apps/server/src/libs/middlewares/validate-key.test.ts index f4ecabba5..524facecd 100644 --- a/apps/server/src/libs/middlewares/validate-key.test.ts +++ b/apps/server/src/libs/middlewares/validate-key.test.ts @@ -1,5 +1,4 @@ -import { expect } from "@std/expect"; -import { describe, test } from "@std/testing/bdd"; +import { describe, expect, test } from "vitest"; import { validateKey } from "./auth"; diff --git a/apps/server/src/libs/sampling.test.ts b/apps/server/src/libs/sampling.test.ts index 0983df614..986f8739d 100644 --- a/apps/server/src/libs/sampling.test.ts +++ b/apps/server/src/libs/sampling.test.ts @@ -1,15 +1,13 @@ -import { expect } from "@std/expect"; -import { describe, test } from "@std/testing/bdd"; -import { stub } from "@std/testing/mock"; +import { describe, expect, test, vi } from "vitest"; import { shouldSample } from "./sampling"; function withRandom(value: number, fn: () => T): T { - const random = stub(Math, "random", () => value); + const random = vi.spyOn(Math, "random").mockImplementation(() => value); try { return fn(); } finally { - random.restore(); + random.mockRestore(); } } diff --git a/apps/server/src/libs/test/doubles/analytics.mock.ts b/apps/server/src/libs/test/doubles/analytics.mock.ts index 3ac496513..ec830b0a6 100644 --- a/apps/server/src/libs/test/doubles/analytics.mock.ts +++ b/apps/server/src/libs/test/doubles/analytics.mock.ts @@ -9,20 +9,20 @@ export { parseInputToProps, } from "@openstatus/analytics-real"; -import { mock } from "@openstatus/test-utils"; +import { vi, type Mock } from "vitest"; const g = globalThis as Record; if (!g.__analyticsSpies) { - const track = mock(() => Promise.resolve()); + const track = vi.fn(() => Promise.resolve()); g.__analyticsSpies = { track, - setupAnalytics: mock((_props: unknown) => Promise.resolve({ track })), + setupAnalytics: vi.fn((_props: unknown) => Promise.resolve({ track })), }; } const spies = g.__analyticsSpies as { - track: ReturnType; - setupAnalytics: ReturnType; + track: Mock; + setupAnalytics: Mock; }; export const setupAnalytics = (props: unknown) => spies.setupAnalytics(props); diff --git a/apps/server/src/libs/test/doubles/state.ts b/apps/server/src/libs/test/doubles/state.ts index ec2239076..6b2279010 100644 --- a/apps/server/src/libs/test/doubles/state.ts +++ b/apps/server/src/libs/test/doubles/state.ts @@ -1,22 +1,22 @@ // Shared test-double state, set on globalThis so tests can assert on spies and // configure mock data — same interface the old bun `mock.module` preload // exposed. Imported (for side effect) by every double module below. -import { mock } from "@openstatus/test-utils"; +import { vi, type Mock } from "vitest"; type Mut = Record; const g = globalThis as Mut; if (!g.__subscriptionSpies) { - const sendVerification = mock(() => Promise.resolve()); + const sendVerification = vi.fn(() => Promise.resolve()); g.__subscriptionSpies = { - dispatchStatusReportUpdate: mock(() => Promise.resolve()), - dispatchMaintenanceUpdate: mock(() => Promise.resolve()), + dispatchStatusReportUpdate: vi.fn(() => Promise.resolve()), + dispatchMaintenanceUpdate: vi.fn(() => Promise.resolve()), sendVerification, getChannel: { id: "email", sendVerification, - sendNotifications: mock(() => Promise.resolve()), - validateConfig: mock(() => Promise.resolve({ valid: true })), + sendNotifications: vi.fn(() => Promise.resolve()), + validateConfig: vi.fn(() => Promise.resolve({ valid: true })), }, }; } @@ -33,9 +33,9 @@ if (!g.__tinybirdMockCalls) { } export const subscriptionSpies = g.__subscriptionSpies as { - dispatchStatusReportUpdate: ReturnType; - dispatchMaintenanceUpdate: ReturnType; - sendVerification: ReturnType; + dispatchStatusReportUpdate: Mock; + dispatchMaintenanceUpdate: Mock; + sendVerification: Mock; getChannel: Record; }; export const testRedisStore = g.__testRedisStore as Map; diff --git a/apps/server/src/libs/test/preload.ts b/apps/server/src/libs/test/preload.ts deleted file mode 100644 index 232bf7f9f..000000000 --- a/apps/server/src/libs/test/preload.ts +++ /dev/null @@ -1,196 +0,0 @@ -// Import the real module BEFORE mock.module runs so we can spread real -// service functions (createSubscription, updateChannel, etc.) into the -// replacement while still stubbing the dispatch-side spies. -import * as realSubscriptions from "@openstatus/subscriptions"; -import { mock } from "@openstatus/test-utils"; - -// Subscription dispatch spies — accessible in tests via globalThis.__subscriptionSpies -const dispatchStatusReportUpdateSpy = mock((_id: number) => Promise.resolve()); -const dispatchMaintenanceUpdateSpy = mock((_id: number) => Promise.resolve()); - -const sendVerificationSpy = mock( - (_subscription: realSubscriptions.Subscription, _verifyUrl: string) => - Promise.resolve(), -); - -const getChannelMock = { - id: "email", - sendVerification: sendVerificationSpy, - sendNotifications: mock(() => Promise.resolve()), - validateConfig: mock(() => Promise.resolve({ valid: true })), -}; - -(globalThis as Record).__subscriptionSpies = { - dispatchStatusReportUpdate: dispatchStatusReportUpdateSpy, - dispatchMaintenanceUpdate: dispatchMaintenanceUpdateSpy, - sendVerification: sendVerificationSpy, - getChannel: getChannelMock, -}; - -mock.module("@openstatus/subscriptions", () => ({ - ...realSubscriptions, - dispatchStatusReportUpdate: dispatchStatusReportUpdateSpy, - dispatchMaintenanceUpdate: dispatchMaintenanceUpdateSpy, - getChannel: () => getChannelMock, -})); - -const testRedisStore = new Map(); -(globalThis as Record).__testRedisStore = testRedisStore; - -mock.module("@openstatus/upstash", () => ({ - Redis: { - fromEnv() { - return { - get: (key: string) => Promise.resolve(testRedisStore.get(key) ?? null), - set: (key: string, value: string) => { - testRedisStore.set(key, value); - return Promise.resolve("OK"); - }, - del: (key: string) => { - const existed = testRedisStore.has(key) ? 1 : 0; - testRedisStore.delete(key); - return Promise.resolve(existed); - }, - getdel: (key: string) => { - const value = testRedisStore.get(key) ?? null; - testRedisStore.delete(key); - return Promise.resolve(value); - }, - expire: (_key: string, _seconds: number) => { - return Promise.resolve(1); - }, - }; - }, - }, -})); - -const tinybirdMockData = { - httpListBiweekly: [] as unknown[], - httpGetBiweekly: [] as unknown[], -}; -const tinybirdMockCalls = { - httpListBiweekly: [] as unknown[], - httpGetBiweekly: [] as unknown[], -}; - -(globalThis as Record).__tinybirdMockData = tinybirdMockData; -(globalThis as Record).__tinybirdMockCalls = tinybirdMockCalls; - -const emptyTinybirdResponse = () => Promise.resolve({ data: [] }); - -const tbMock = { - get legacy_httpStatus45d() { - return emptyTinybirdResponse; - }, - get legacy_tcpStatus45d() { - return emptyTinybirdResponse; - }, - get httpListBiweekly() { - return (params: unknown) => { - tinybirdMockCalls.httpListBiweekly.push(params); - return Promise.resolve({ data: tinybirdMockData.httpListBiweekly }); - }; - }, - get httpGetBiweekly() { - return (params: unknown) => { - tinybirdMockCalls.httpGetBiweekly.push(params); - return Promise.resolve({ data: tinybirdMockData.httpGetBiweekly }); - }; - }, - get httpMetricsDaily() { - return emptyTinybirdResponse; - }, - get httpMetricsWeekly() { - return emptyTinybirdResponse; - }, - get httpMetricsBiweekly() { - return emptyTinybirdResponse; - }, - get tcpMetricsDaily() { - return emptyTinybirdResponse; - }, - get tcpMetricsWeekly() { - return emptyTinybirdResponse; - }, - get tcpMetricsBiweekly() { - return emptyTinybirdResponse; - }, - get dnsMetricsDaily() { - return emptyTinybirdResponse; - }, - get dnsMetricsWeekly() { - return emptyTinybirdResponse; - }, - get dnsMetricsBiweekly() { - return emptyTinybirdResponse; - }, -}; - -mock.module("@/libs/clients", () => ({ - tb: tbMock, - redis: { - get: (key: string) => Promise.resolve(testRedisStore.get(key) ?? null), - set: (key: string, value: string) => { - testRedisStore.set(key, value); - return Promise.resolve("OK"); - }, - del: (key: string) => { - const existed = testRedisStore.has(key) ? 1 : 0; - testRedisStore.delete(key); - return Promise.resolve(existed); - }, - getdel: (key: string) => { - const value = testRedisStore.get(key) ?? null; - testRedisStore.delete(key); - return Promise.resolve(value); - }, - expire: (_key: string, _seconds: number) => Promise.resolve(1), - }, -})); - -mock.module("@openstatus/tinybird", () => ({ - OSTinybird: class { - get legacy_httpStatus45d() { - return tbMock.legacy_httpStatus45d; - } - get legacy_tcpStatus45d() { - return tbMock.legacy_tcpStatus45d; - } - get httpListBiweekly() { - return tbMock.httpListBiweekly; - } - get httpGetBiweekly() { - return tbMock.httpGetBiweekly; - } - // HTTP metrics for GetMonitorSummary - get httpMetricsDaily() { - return tbMock.httpMetricsDaily; - } - get httpMetricsWeekly() { - return tbMock.httpMetricsWeekly; - } - get httpMetricsBiweekly() { - return tbMock.httpMetricsBiweekly; - } - // TCP metrics for GetMonitorSummary - get tcpMetricsDaily() { - return tbMock.tcpMetricsDaily; - } - get tcpMetricsWeekly() { - return tbMock.tcpMetricsWeekly; - } - get tcpMetricsBiweekly() { - return tbMock.tcpMetricsBiweekly; - } - // DNS metrics for GetMonitorSummary - get dnsMetricsDaily() { - return tbMock.dnsMetricsDaily; - } - get dnsMetricsWeekly() { - return tbMock.dnsMetricsWeekly; - } - get dnsMetricsBiweekly() { - return tbMock.dnsMetricsBiweekly; - } - }, -})); diff --git a/apps/server/src/overload.test.ts b/apps/server/src/overload.test.ts index e0609f27d..ba6d1375a 100644 --- a/apps/server/src/overload.test.ts +++ b/apps/server/src/overload.test.ts @@ -1,5 +1,4 @@ -import { expect } from "@std/expect"; -import { afterAll, beforeAll, describe, test } from "@std/testing/bdd"; +import { afterAll, beforeAll, describe, expect, test } from "vitest"; import { app } from "@/index"; import { concurrencyGuard } from "@/libs/middlewares/concurrency"; @@ -8,7 +7,7 @@ import { limits } from "@/libs/middlewares/limits"; /** * Wiring test: the guards must be mounted in the real app, after the wide * event and before every route. Limits are lowered on the in-isolate - * `limits` object, never via `Deno.env`, which is shared by parallel workers. + * `limits` object, never via `process.env`, which every test in the file shares. */ const original = { ...limits }; const tick = () => new Promise((r) => setTimeout(r, 0)); diff --git a/apps/server/src/routes/mcp/adapter.test.ts b/apps/server/src/routes/mcp/adapter.test.ts index b1a00d1a1..ca4af4473 100644 --- a/apps/server/src/routes/mcp/adapter.test.ts +++ b/apps/server/src/routes/mcp/adapter.test.ts @@ -1,7 +1,6 @@ import type { Workspace } from "@openstatus/db/src/schema"; import { ForbiddenError, NotFoundError } from "@openstatus/services"; -import { expect } from "@std/expect"; -import { describe, test } from "@std/testing/bdd"; +import { describe, expect, test } from "vitest"; import { mapError, toServiceCtx } from "./adapter"; diff --git a/apps/server/src/routes/mcp/handler.test.ts b/apps/server/src/routes/mcp/handler.test.ts index c4a86c77a..a271967de 100644 --- a/apps/server/src/routes/mcp/handler.test.ts +++ b/apps/server/src/routes/mcp/handler.test.ts @@ -4,17 +4,17 @@ import { db, desc, eq } from "@openstatus/db"; import { auditLog, page, statusReport } from "@openstatus/db/src/schema"; import { resourceMetadataUrl } from "@openstatus/services/oauth"; import { SEEDED_WORKSPACE_TEAM_ID } from "@openstatus/services/test/fixtures"; -import type { MockFn } from "@openstatus/test-utils"; -import { expect } from "@std/expect"; +import { Hono } from "hono"; +import { requestId } from "hono/request-id"; import { afterAll, beforeAll, beforeEach, describe, + expect, test, -} from "@std/testing/bdd"; -import { Hono } from "hono"; -import { requestId } from "hono/request-id"; + type Mock, +} from "vitest"; import { handleError } from "../../libs/errors"; import { oauthConfigFromEnv } from "../oauth/config"; @@ -256,7 +256,7 @@ describe("MCP transport", () => { * spies live on `globalThis.__analyticsSpies`. */ const analyticsSpies = (globalThis as Record) - .__analyticsSpies as { track: MockFn; setupAnalytics: MockFn }; + .__analyticsSpies as { track: Mock; setupAnalytics: Mock }; /** * Tracking is fire-and-forget — `track` runs in a microtask chained off diff --git a/apps/server/src/routes/mcp/resources.test.ts b/apps/server/src/routes/mcp/resources.test.ts index 0ab60331b..3e2ebdcd3 100644 --- a/apps/server/src/routes/mcp/resources.test.ts +++ b/apps/server/src/routes/mcp/resources.test.ts @@ -1,8 +1,7 @@ import { Client } from "@modelcontextprotocol/sdk/client/index.js"; import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js"; import type { Workspace } from "@openstatus/db/src/schema"; -import { expect } from "@std/expect"; -import { afterEach, describe, test } from "@std/testing/bdd"; +import { afterEach, describe, expect, test } from "vitest"; import { toServiceCtx } from "./adapter"; import { PUBLIC_RESOURCES, readPublicResource } from "./resources"; @@ -18,6 +17,14 @@ function stubFetch(impl: () => Promise) { globalThis.fetch = impl as unknown as typeof fetch; } +// Fails on a missing or non-string `text` instead of coercing it to "undefined". +function textOf(result: { contents: object[] }): string { + const first = result.contents[0]; + const text = first && "text" in first ? first.text : undefined; + expect(text).toBeTypeOf("string"); + return text as string; +} + describe("public MCP resources", () => { test("every entry is addressable and typed", () => { expect(PUBLIC_RESOURCES.length).toBeGreaterThan(0); @@ -45,7 +52,7 @@ describe("public MCP resources", () => { stubFetch(() => Promise.reject(new Error("network down"))); for (const resource of PUBLIC_RESOURCES) { const result = await readPublicResource(resource); - expect(String(result.contents[0].text).length).toBeGreaterThan(0); + expect(textOf(result).length).toBeGreaterThan(0); } }); @@ -55,13 +62,13 @@ describe("public MCP resources", () => { stubFetch(() => Promise.resolve(new Response(" "))); const blank = await readPublicResource(remote); - expect(String(blank.contents[0].text).trim().length).toBeGreaterThan(0); + expect(textOf(blank).trim().length).toBeGreaterThan(0); stubFetch(() => Promise.resolve(new Response("not found", { status: 404 })), ); const missing = await readPublicResource(remote); - expect(String(missing.contents[0].text).trim().length).toBeGreaterThan(0); + expect(textOf(missing).trim().length).toBeGreaterThan(0); }); test("serves the fetched document when it resolves", async () => { @@ -72,7 +79,7 @@ describe("public MCP resources", () => { stubFetch(() => Promise.resolve(new Response("# MCP server\n\nlive copy"))); const result = await readPublicResource(remote); - expect(result.contents[0].text).toContain("live copy"); + expect(textOf(result)).toContain("live copy"); }); test("ships the OpenAPI document without a network call", async () => { @@ -83,7 +90,7 @@ describe("public MCP resources", () => { if (!spec) throw new Error("openapi resource missing"); const result = await readPublicResource(spec); - const parsed = JSON.parse(String(result.contents[0].text)); + const parsed = JSON.parse(textOf(result)); expect(parsed.openapi).toBe("3.1.0"); expect(parsed.servers[0].url).toBe("https://api.openstatus.dev"); }); @@ -155,7 +162,7 @@ describe("public MCP documents over the transport", () => { try { for (const resource of PUBLIC_RESOURCES) { const result = await client.readResource({ uri: resource.uri }); - expect(String(result.contents[0].text).length).toBeGreaterThan(0); + expect(textOf(result).length).toBeGreaterThan(0); } } finally { await close(); diff --git a/apps/server/src/routes/mcp/tools/register-scoped.test.ts b/apps/server/src/routes/mcp/tools/register-scoped.test.ts index 3b5eae6f7..141772f1a 100644 --- a/apps/server/src/routes/mcp/tools/register-scoped.test.ts +++ b/apps/server/src/routes/mcp/tools/register-scoped.test.ts @@ -1,8 +1,7 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import type { Workspace } from "@openstatus/db/src/schema"; import type { ServiceContext } from "@openstatus/services"; -import { expect } from "@std/expect"; -import { describe, test } from "@std/testing/bdd"; +import { describe, expect, test } from "vitest"; import { registerScopedTool } from "./register-scoped"; diff --git a/apps/server/src/routes/mcp/tools/tools.test.ts b/apps/server/src/routes/mcp/tools/tools.test.ts index deb3d239f..0c321ccd4 100644 --- a/apps/server/src/routes/mcp/tools/tools.test.ts +++ b/apps/server/src/routes/mcp/tools/tools.test.ts @@ -17,8 +17,7 @@ import { readAuditLog, withTestTransaction, } from "@openstatus/services/test/helpers"; -import { expect } from "@std/expect"; -import { afterAll, beforeAll, describe, test } from "@std/testing/bdd"; +import { afterAll, beforeAll, describe, expect, test } from "vitest"; import { toServiceCtx } from "../adapter"; import { registerMaintenanceTools } from "./maintenance"; diff --git a/apps/server/src/routes/oauth/oauth.test.ts b/apps/server/src/routes/oauth/oauth.test.ts index c86b5ef83..aaf90e611 100644 --- a/apps/server/src/routes/oauth/oauth.test.ts +++ b/apps/server/src/routes/oauth/oauth.test.ts @@ -14,9 +14,8 @@ import { pkceChallenge, } from "@openstatus/services/oauth"; import { clearAuditLogFor } from "@openstatus/services/test/helpers"; -import { expect } from "@std/expect"; -import { afterAll, beforeAll, describe, test } from "@std/testing/bdd"; import { Hono } from "hono"; +import { afterAll, beforeAll, describe, expect, test } from "vitest"; import { app } from "../../index"; import { oauthConfigFromEnv } from "./config"; diff --git a/apps/server/src/routes/openapi.test.ts b/apps/server/src/routes/openapi.test.ts index 666884f2f..2c007f68f 100644 --- a/apps/server/src/routes/openapi.test.ts +++ b/apps/server/src/routes/openapi.test.ts @@ -1,7 +1,8 @@ -import { expect } from "@std/expect"; -import { describe, test } from "@std/testing/bdd"; +import { readFile } from "node:fs/promises"; + +import { parse as parseYaml } from "@std/yaml"; import { Hono } from "hono"; -import { parse as parseYaml } from "jsr:@std/yaml@^1.0.9"; +import { describe, expect, test } from "vitest"; import openapiV1Json from "../../static/openapi-v1.json" with { type: "json" }; import openapiYaml from "../../static/openapi-yaml"; @@ -32,8 +33,9 @@ function withoutDateDefaults(value: unknown): unknown { describe("OpenAPI documents", () => { test("openapi.json is a faithful rendering of openapi.yaml", async () => { const yaml = parseYaml( - await Deno.readTextFile( + await readFile( new URL("../../static/openapi.yaml", import.meta.url), + "utf8", ), ); expect(JSON.parse(JSON.stringify(openapiJson))).toEqual(yaml); @@ -41,8 +43,9 @@ describe("OpenAPI documents", () => { test("the embedded YAML module matches openapi.yaml on disk", async () => { expect(openapiYaml).toBe( - await Deno.readTextFile( + await readFile( new URL("../../static/openapi.yaml", import.meta.url), + "utf8", ), ); }); diff --git a/apps/server/src/routes/public/status.test.ts b/apps/server/src/routes/public/status.test.ts index 0945504b0..1e91b9f2e 100644 --- a/apps/server/src/routes/public/status.test.ts +++ b/apps/server/src/routes/public/status.test.ts @@ -1,12 +1,12 @@ import { db, eq } from "@openstatus/db"; -import { expect } from "@std/expect"; import { afterAll, beforeAll, beforeEach, describe, + expect, test, -} from "@std/testing/bdd"; +} from "vitest"; import { app } from "../../index"; diff --git a/apps/server/src/routes/rpc/adapter.test.ts b/apps/server/src/routes/rpc/adapter.test.ts index 09e988efa..919ea1ad8 100644 --- a/apps/server/src/routes/rpc/adapter.test.ts +++ b/apps/server/src/routes/rpc/adapter.test.ts @@ -13,8 +13,7 @@ import { createWorkspaceFixture, withTestTransaction, } from "@openstatus/services/test/helpers"; -import { expect } from "@std/expect"; -import { beforeAll, describe, test } from "@std/testing/bdd"; +import { beforeAll, describe, expect, test } from "vitest"; import { toConnectError, toServiceCtx } from "./adapter"; diff --git a/apps/server/src/routes/rpc/handlers/incident/__tests__/incident.test.ts b/apps/server/src/routes/rpc/handlers/incident/__tests__/incident.test.ts index 99b084104..5854a41b9 100644 --- a/apps/server/src/routes/rpc/handlers/incident/__tests__/incident.test.ts +++ b/apps/server/src/routes/rpc/handlers/incident/__tests__/incident.test.ts @@ -46,14 +46,14 @@ import { ListStatusReportsResponseSchema, } from "@openstatus/proto/status_report/v1"; import { SLACK_BOT_SCOPES } from "@openstatus/services/integration"; -import { expect } from "@std/expect"; import { afterAll, beforeAll, beforeEach, describe, + expect, test, -} from "@std/testing/bdd"; +} from "vitest"; import { settleBackgroundTasks } from "@/libs/background"; import { incidentCommanderEmails } from "@/libs/test/doubles/emails.mock"; diff --git a/apps/server/src/routes/rpc/handlers/maintenance/__tests__/maintenance.test.ts b/apps/server/src/routes/rpc/handlers/maintenance/__tests__/maintenance.test.ts index ad72c9a60..86b2f1051 100644 --- a/apps/server/src/routes/rpc/handlers/maintenance/__tests__/maintenance.test.ts +++ b/apps/server/src/routes/rpc/handlers/maintenance/__tests__/maintenance.test.ts @@ -9,17 +9,23 @@ import { workspace, } from "@openstatus/db/src/schema"; import { createTestWorkspace } from "@openstatus/db/src/test/factories"; -import { mock } from "@openstatus/test-utils"; -import { expect } from "@std/expect"; -import { afterAll, beforeAll, describe, test } from "@std/testing/bdd"; import { nanoid } from "nanoid"; +import { + afterAll, + beforeAll, + describe, + expect, + test, + vi, + type Mock, +} from "vitest"; import { app } from "../../../../../index"; const subscriptionSpies = (globalThis as Record) .__subscriptionSpies as { - dispatchStatusReportUpdate: ReturnType; - dispatchMaintenanceUpdate: ReturnType; + dispatchStatusReportUpdate: Mock; + dispatchMaintenanceUpdate: Mock; }; /** diff --git a/apps/server/src/routes/rpc/handlers/monitor/__tests__/monitor.test.ts b/apps/server/src/routes/rpc/handlers/monitor/__tests__/monitor.test.ts index 9bf3933b3..2a44dd445 100644 --- a/apps/server/src/routes/rpc/handlers/monitor/__tests__/monitor.test.ts +++ b/apps/server/src/routes/rpc/handlers/monitor/__tests__/monitor.test.ts @@ -14,14 +14,14 @@ import { createMonitor, createWorkspace, } from "@openstatus/db/src/test/factories"; -import { expect } from "@std/expect"; import { afterAll, beforeAll, beforeEach, describe, + expect, test, -} from "@std/testing/bdd"; +} from "vitest"; import { app } from "../../../../../index"; @@ -3274,16 +3274,16 @@ describe("MonitorService.GetMonitor", () => { // Insert a monitor with invalid data that will fail selectMonitorSchema parsing const corruptedMon = await db .insert(monitor) - // @ts-expect-error - intentionally invalid periodicity to test parse failure .values({ workspaceId: 1, name: `${TEST_PREFIX}-corrupted-data`, url: "https://corrupted.example.com", + // intentionally invalid periodicity to test parse failure periodicity: "invalid-periodicity", active: true, regions: "ams", jobType: "http", - }) + } as unknown as typeof monitor.$inferInsert) .returning() .get(); diff --git a/apps/server/src/routes/rpc/handlers/monitor/converters/assertions.test.ts b/apps/server/src/routes/rpc/handlers/monitor/converters/assertions.test.ts index 30001928e..8e2339680 100644 --- a/apps/server/src/routes/rpc/handlers/monitor/converters/assertions.test.ts +++ b/apps/server/src/routes/rpc/handlers/monitor/converters/assertions.test.ts @@ -8,8 +8,7 @@ import { type StatusCodeAssertion, StringComparator, } from "@openstatus/proto/monitor/v1"; -import { expect } from "@std/expect"; -import { describe, test } from "@std/testing/bdd"; +import { describe, expect, test } from "vitest"; import { z } from "zod"; import { diff --git a/apps/server/src/routes/rpc/handlers/monitor/converters/headers.test.ts b/apps/server/src/routes/rpc/handlers/monitor/converters/headers.test.ts index a0eebfb88..a329fbba6 100644 --- a/apps/server/src/routes/rpc/handlers/monitor/converters/headers.test.ts +++ b/apps/server/src/routes/rpc/handlers/monitor/converters/headers.test.ts @@ -2,8 +2,7 @@ import type { Headers, OpenTelemetryConfig, } from "@openstatus/proto/monitor/v1"; -import { expect } from "@std/expect"; -import { describe, test } from "@std/testing/bdd"; +import { describe, expect, test } from "vitest"; import { parseOpenTelemetry, diff --git a/apps/server/src/routes/rpc/handlers/monitor/converters/regions.test.ts b/apps/server/src/routes/rpc/handlers/monitor/converters/regions.test.ts index c617b766b..0fdb6807c 100644 --- a/apps/server/src/routes/rpc/handlers/monitor/converters/regions.test.ts +++ b/apps/server/src/routes/rpc/handlers/monitor/converters/regions.test.ts @@ -1,7 +1,6 @@ import { Region } from "@openstatus/proto/monitor/v1"; import { AVAILABLE_REGIONS, RAILWAY_REGIONS } from "@openstatus/regions"; -import { expect } from "@std/expect"; -import { describe, test } from "@std/testing/bdd"; +import { describe, expect, test } from "vitest"; import { regionToString, diff --git a/apps/server/src/routes/rpc/handlers/monitor/response-logs.test.ts b/apps/server/src/routes/rpc/handlers/monitor/response-logs.test.ts index 315eb4dfc..9720ac79c 100644 --- a/apps/server/src/routes/rpc/handlers/monitor/response-logs.test.ts +++ b/apps/server/src/routes/rpc/handlers/monitor/response-logs.test.ts @@ -1,6 +1,5 @@ import { redactSensitiveHeaders } from "@openstatus/services/monitor"; -import { expect } from "@std/expect"; -import { test } from "@std/testing/bdd"; +import { expect, test } from "vitest"; test("redactSensitiveHeaders returns an empty object when headers are missing", () => { expect(redactSensitiveHeaders(null)).toEqual({}); diff --git a/apps/server/src/routes/rpc/handlers/monitor/validators.test.ts b/apps/server/src/routes/rpc/handlers/monitor/validators.test.ts index 1d3546a0e..a25af51ba 100644 --- a/apps/server/src/routes/rpc/handlers/monitor/validators.test.ts +++ b/apps/server/src/routes/rpc/handlers/monitor/validators.test.ts @@ -1,7 +1,6 @@ import { ConnectError } from "@connectrpc/connect"; import { Periodicity, Region } from "@openstatus/proto/monitor/v1"; -import { expect } from "@std/expect"; -import { describe, test } from "@std/testing/bdd"; +import { describe, expect, test } from "vitest"; import { MONITOR_DEFAULTS } from "./converters"; import { diff --git a/apps/server/src/routes/rpc/handlers/notification/__tests__/notification.test.ts b/apps/server/src/routes/rpc/handlers/notification/__tests__/notification.test.ts index 14796f5b5..0b905f328 100644 --- a/apps/server/src/routes/rpc/handlers/notification/__tests__/notification.test.ts +++ b/apps/server/src/routes/rpc/handlers/notification/__tests__/notification.test.ts @@ -4,8 +4,7 @@ import { notificationsToMonitors, } from "@openstatus/db/src/schema"; import { createTestWorkspace } from "@openstatus/db/src/test/factories"; -import { expect } from "@std/expect"; -import { afterAll, beforeAll, describe, test } from "@std/testing/bdd"; +import { afterAll, beforeAll, describe, expect, test } from "vitest"; import { app } from "../../../../../index"; diff --git a/apps/server/src/routes/rpc/handlers/private-location/__tests__/private-location.test.ts b/apps/server/src/routes/rpc/handlers/private-location/__tests__/private-location.test.ts index 93c60ca83..6e4321587 100644 --- a/apps/server/src/routes/rpc/handlers/private-location/__tests__/private-location.test.ts +++ b/apps/server/src/routes/rpc/handlers/private-location/__tests__/private-location.test.ts @@ -6,9 +6,8 @@ import { workspace, } from "@openstatus/db/src/schema"; import { getLimits } from "@openstatus/db/src/schema/plan/utils"; -import { expect } from "@std/expect"; -import { afterAll, beforeAll, describe, test } from "@std/testing/bdd"; import { nanoid } from "nanoid"; +import { afterAll, beforeAll, describe, expect, test } from "vitest"; import { app } from "../../../../../index"; diff --git a/apps/server/src/routes/rpc/handlers/status-page/__tests__/status-page.test.ts b/apps/server/src/routes/rpc/handlers/status-page/__tests__/status-page.test.ts index 21d7ba335..bc495398a 100644 --- a/apps/server/src/routes/rpc/handlers/status-page/__tests__/status-page.test.ts +++ b/apps/server/src/routes/rpc/handlers/status-page/__tests__/status-page.test.ts @@ -10,9 +10,15 @@ import { statusReportsToPageComponents, } from "@openstatus/db/src/schema"; import { createTestWorkspace } from "@openstatus/db/src/test/factories"; -import { mock } from "@openstatus/test-utils"; -import { expect } from "@std/expect"; -import { afterAll, beforeAll, describe, test } from "@std/testing/bdd"; +import { + afterAll, + beforeAll, + describe, + expect, + test, + vi, + type Mock, +} from "vitest"; import { app } from "../../../../../index"; @@ -2256,7 +2262,7 @@ describe("StatusPageService.UpdateComponentGroup", () => { const subscriptionSpies = (globalThis as Record) .__subscriptionSpies as { - sendVerification: ReturnType; + sendVerification: Mock; }; describe("StatusPageService.SubscribeToPage", () => { diff --git a/apps/server/src/routes/rpc/handlers/status-report/__tests__/status-report.test.ts b/apps/server/src/routes/rpc/handlers/status-report/__tests__/status-report.test.ts index 5d87a419d..9c0bf4d5e 100644 --- a/apps/server/src/routes/rpc/handlers/status-report/__tests__/status-report.test.ts +++ b/apps/server/src/routes/rpc/handlers/status-report/__tests__/status-report.test.ts @@ -15,9 +15,15 @@ import { PageComponentImpact, StatusReportStatus, } from "@openstatus/proto/status_report/v1"; -import { mock } from "@openstatus/test-utils"; -import { expect } from "@std/expect"; -import { afterAll, beforeAll, describe, test } from "@std/testing/bdd"; +import { + afterAll, + beforeAll, + describe, + expect, + test, + vi, + type Mock, +} from "vitest"; import { app } from "../../../../../index"; import { @@ -28,8 +34,8 @@ import { const subscriptionSpies = (globalThis as Record) .__subscriptionSpies as { - dispatchStatusReportUpdate: ReturnType; - dispatchMaintenanceUpdate: ReturnType; + dispatchStatusReportUpdate: Mock; + dispatchMaintenanceUpdate: Mock; }; /** diff --git a/apps/server/src/routes/rpc/interceptors/__tests__/error.test.ts b/apps/server/src/routes/rpc/interceptors/__tests__/error.test.ts index 8997a44f0..1302fbf38 100644 --- a/apps/server/src/routes/rpc/interceptors/__tests__/error.test.ts +++ b/apps/server/src/routes/rpc/interceptors/__tests__/error.test.ts @@ -1,5 +1,5 @@ import { Code, ConnectError, type Interceptor } from "@connectrpc/connect"; -import { describe, expect, test } from "@openstatus/test-utils"; +import { describe, expect, test } from "vitest"; import { RPC_CONTEXT_KEY } from "../auth"; import { errorInterceptor } from "../error"; diff --git a/apps/server/src/routes/rpc/interceptors/__tests__/tracking.test.ts b/apps/server/src/routes/rpc/interceptors/__tests__/tracking.test.ts index 4f61679b5..2846db187 100644 --- a/apps/server/src/routes/rpc/interceptors/__tests__/tracking.test.ts +++ b/apps/server/src/routes/rpc/interceptors/__tests__/tracking.test.ts @@ -1,3 +1,5 @@ +// @ts-nocheck — ConnectRPC's deep generic types (AnyFn, UnaryResponse, etc.) +// are incompatible with the test mocks. All runtime behavior is correct. import { create } from "@bufbuild/protobuf"; import type { Interceptor } from "@connectrpc/connect"; import { Events } from "@openstatus/analytics"; @@ -9,18 +11,16 @@ import { SetIncidentStatusRequestSchema, } from "@openstatus/proto/incident/v1"; import { MonitorService } from "@openstatus/proto/monitor/v1"; -// @ts-nocheck — ConnectRPC's deep generic types (AnyFn, UnaryResponse, etc.) -// are incompatible with the test mocks. All runtime behavior is correct. import { afterEach, beforeEach, describe, expect, - mock, - type Stub, - stub, + type Mock, + type MockInstance, test, -} from "@openstatus/test-utils"; + vi, +} from "vitest"; import { RPC_CONTEXT_KEY } from "../auth"; import { RPC_EVENT_MAP, trackingInterceptor } from "../tracking"; @@ -30,8 +30,8 @@ import { RPC_EVENT_MAP, trackingInterceptor } from "../tracking"; const { track: mockTrack, setupAnalytics: mockSetupAnalytics } = ( globalThis as Record ).__analyticsSpies as { - track: ReturnType; - setupAnalytics: ReturnType; + track: Mock; + setupAnalytics: Mock; }; type NextFn = Parameters>[0]; @@ -41,12 +41,12 @@ const flush = () => new Promise((resolve) => setTimeout(resolve, 0)); /** Create a mock `next` that resolves with the given value. */ function mockNext(response: unknown): NextFn { - return mock(() => Promise.resolve(response)) as unknown as NextFn; + return vi.fn(() => Promise.resolve(response)) as unknown as NextFn; } /** Create a mock `next` that rejects with the given error. */ function mockNextReject(error: Error): NextFn { - return mock(() => Promise.reject(error)) as unknown as NextFn; + return vi.fn(() => Promise.reject(error)) as unknown as NextFn; } const TEST_WORKSPACE = { @@ -95,17 +95,17 @@ function createMockRequest( } describe("trackingInterceptor", () => { - let random: Stub; + let random: MockInstance<() => number>; beforeEach(() => { mockSetupAnalytics.mockClear(); mockTrack.mockClear(); // Keep every sampled request unless a test says otherwise. - random = stub(Math, "random", () => 0); + random = vi.spyOn(Math, "random").mockReturnValue(0); }); afterEach(() => { - random.restore(); + random.mockRestore(); }); test("tracks mapped event on success", async () => { @@ -274,8 +274,7 @@ describe("trackingInterceptor", () => { }); test("drops non-CLI api_request outside the sample", async () => { - random.restore(); - random = stub(Math, "random", () => 0.5); + random.mockReturnValue(0.5); const interceptor = trackingInterceptor(); const req = createMockRequest( @@ -291,8 +290,7 @@ describe("trackingInterceptor", () => { }); test("keeps the domain event when api_request is sampled out", async () => { - random.restore(); - random = stub(Math, "random", () => 0.5); + random.mockReturnValue(0.5); const interceptor = trackingInterceptor(); const req = createMockRequest( @@ -308,8 +306,7 @@ describe("trackingInterceptor", () => { }); test("always tracks CLI api_request", async () => { - random.restore(); - random = stub(Math, "random", () => 0.99); + random.mockReturnValue(0.99); const interceptor = trackingInterceptor(); const req = createMockRequest( diff --git a/apps/server/src/routes/rpc/parse.test.ts b/apps/server/src/routes/rpc/parse.test.ts index 487aa0d0b..83c892fc5 100644 --- a/apps/server/src/routes/rpc/parse.test.ts +++ b/apps/server/src/routes/rpc/parse.test.ts @@ -1,6 +1,5 @@ import { Code, ConnectError } from "@connectrpc/connect"; -import { expect } from "@std/expect"; -import { describe, test } from "@std/testing/bdd"; +import { describe, expect, test } from "vitest"; import { parseDate, parseId } from "./parse"; diff --git a/apps/server/src/routes/slack/blocks.test.ts b/apps/server/src/routes/slack/blocks.test.ts index b5a7170d1..7b7c1a434 100644 --- a/apps/server/src/routes/slack/blocks.test.ts +++ b/apps/server/src/routes/slack/blocks.test.ts @@ -1,6 +1,5 @@ import { agentTools } from "@openstatus/services/agent-tools"; -import { expect } from "@std/expect"; -import { describe, test } from "@std/testing/bdd"; +import { describe, expect, test } from "vitest"; import { buildConfirmationBlocks, diff --git a/apps/server/src/routes/slack/channel-context.test.ts b/apps/server/src/routes/slack/channel-context.test.ts index 0e44ce318..939363592 100644 --- a/apps/server/src/routes/slack/channel-context.test.ts +++ b/apps/server/src/routes/slack/channel-context.test.ts @@ -1,6 +1,6 @@ -import { beforeEach, describe, expect, test } from "@openstatus/test-utils"; // The double stands in for @slack/web-api via the test import map. import { WebClient } from "@slack/web-api"; +import { beforeEach, describe, expect, test } from "vitest"; import { slackTestState } from "@/libs/test/doubles/slack-test-state"; @@ -179,7 +179,10 @@ describe("fetchChannelHistory", () => { }); const execute = tools.read_slack_channel.execute; if (!execute) throw new Error("read_slack_channel has no execute"); - await execute({ before: "10.0" }, { toolCallId: "t1", messages: [] }); + await execute( + { before: "10.0" }, + { toolCallId: "t1", messages: [], context: {} }, + ); expect(slackTestState.calls[0].args).toMatchObject({ latest: "10.0" }); }); }); diff --git a/apps/server/src/routes/slack/commands.test.ts b/apps/server/src/routes/slack/commands.test.ts index 6e3bd8676..ddfeee27b 100644 --- a/apps/server/src/routes/slack/commands.test.ts +++ b/apps/server/src/routes/slack/commands.test.ts @@ -10,8 +10,8 @@ import { workspace as workspaceTable, } from "@openstatus/db/src/schema"; import { declareIncident } from "@openstatus/services/incident"; -import { beforeEach, describe, expect, test } from "@openstatus/test-utils"; import { Hono } from "hono"; +import { beforeEach, describe, expect, test } from "vitest"; import { slackTestState } from "@/libs/test/doubles/slack-test-state"; import { diff --git a/apps/server/src/routes/slack/config.ts b/apps/server/src/routes/slack/config.ts index 4ab39e55c..2376d6e0d 100644 --- a/apps/server/src/routes/slack/config.ts +++ b/apps/server/src/routes/slack/config.ts @@ -7,9 +7,8 @@ import { env } from "@/env"; * built and carried on the request context. * * Handlers read this instead of `env` directly so tests can construct a route - * with explicit config. Mutating `process.env` per-test is not viable: Deno's - * `--parallel` runs test files in workers that share one process environment, - * so concurrent files clobber each other's settings. + * with explicit config. Mutating `process.env` per-test is not viable: every + * test in a file shares it, so one case's settings leak into the next. */ export type SlackConfig = { signingSecret?: string; diff --git a/apps/server/src/routes/slack/confirmation-store.test.ts b/apps/server/src/routes/slack/confirmation-store.test.ts index 40a9e84c6..55ccdbf1a 100644 --- a/apps/server/src/routes/slack/confirmation-store.test.ts +++ b/apps/server/src/routes/slack/confirmation-store.test.ts @@ -1,5 +1,4 @@ -import { expect } from "@std/expect"; -import { beforeEach, describe, test } from "@std/testing/bdd"; +import { beforeEach, describe, expect, test } from "vitest"; import { consume, diff --git a/apps/server/src/routes/slack/handler.test.ts b/apps/server/src/routes/slack/handler.test.ts index e9f4b8de7..7c3d812c0 100644 --- a/apps/server/src/routes/slack/handler.test.ts +++ b/apps/server/src/routes/slack/handler.test.ts @@ -15,14 +15,8 @@ import { createTestWorkspace, createUser, } from "@openstatus/db/src/test/factories"; -import { - afterEach, - beforeEach, - describe, - expect, - test, -} from "@openstatus/test-utils"; import { Hono } from "hono"; +import { afterEach, beforeEach, describe, expect, test } from "vitest"; import { settleBackgroundTasks } from "@/libs/background"; // workspace-resolver / @slack/web-api / agent are swapped for doubles via the diff --git a/apps/server/src/routes/slack/home.test.ts b/apps/server/src/routes/slack/home.test.ts index acd3b1117..11d335484 100644 --- a/apps/server/src/routes/slack/home.test.ts +++ b/apps/server/src/routes/slack/home.test.ts @@ -1,5 +1,4 @@ -import { expect } from "@std/expect"; -import { describe, test } from "@std/testing/bdd"; +import { describe, expect, test } from "vitest"; import { buildHomeBlocks, OPEN_DECLARE_INCIDENT_ACTION } from "./home"; diff --git a/apps/server/src/routes/slack/incident-commands.test.ts b/apps/server/src/routes/slack/incident-commands.test.ts index 30faf8708..4788926dd 100644 --- a/apps/server/src/routes/slack/incident-commands.test.ts +++ b/apps/server/src/routes/slack/incident-commands.test.ts @@ -1,5 +1,4 @@ -import { expect } from "@std/expect"; -import { describe, test } from "@std/testing/bdd"; +import { describe, expect, test } from "vitest"; import { parseDeclare, parseTarget } from "./incident-commands"; diff --git a/apps/server/src/routes/slack/index.test.ts b/apps/server/src/routes/slack/index.test.ts index 6cb69fe98..ba4fcf257 100644 --- a/apps/server/src/routes/slack/index.test.ts +++ b/apps/server/src/routes/slack/index.test.ts @@ -1,8 +1,7 @@ import crypto from "node:crypto"; -import { expect } from "@std/expect"; -import { describe, test } from "@std/testing/bdd"; import { Hono } from "hono"; +import { describe, expect, test } from "vitest"; import type { SlackConfig } from "./config"; import { createSlackRoute } from "./index"; diff --git a/apps/server/src/routes/slack/interactions.test.ts b/apps/server/src/routes/slack/interactions.test.ts index 0014943de..da94bc2b3 100644 --- a/apps/server/src/routes/slack/interactions.test.ts +++ b/apps/server/src/routes/slack/interactions.test.ts @@ -16,6 +16,7 @@ import { import { addUserToWorkspace, createPage, + createPageComponent, createTestWorkspace, createUser, } from "@openstatus/db/src/test/factories"; @@ -23,15 +24,16 @@ import { bindIncidentSlackChannel, declareIncident, } from "@openstatus/services/incident"; +import { WebClient } from "@slack/web-api"; +import { Hono } from "hono"; import { afterEach, + beforeAll, beforeEach, describe, expect, test, -} from "@openstatus/test-utils"; -import { WebClient } from "@slack/web-api"; -import { Hono } from "hono"; +} from "vitest"; import { settleBackgroundTasks } from "@/libs/background"; // workspace-resolver / @slack/web-api are swapped for doubles via the test @@ -1763,17 +1765,42 @@ describe("publish status update button", () => { describe("status report follow-up after a declare", () => { const app = createTestApp(); let workspace: ReturnType; + let ownerEmail: string; + let pageId: number; + let componentId: number; let incidentId: number; const startedAt = Math.floor(Date.now() / 1000) - 11 * 3600; + // Its own workspace: other suites rename and empty the seeded page 1. + beforeAll(async () => { + const fixture = await createTestWorkspace(); + workspace = selectWorkspaceSchema.parse(fixture.workspace); + ownerEmail = fixture.user.email as string; + const page = await createPage(workspace.id); + pageId = page.id; + const component = await createPageComponent(workspace.id, page.id, { + name: "Google Monitor", + }); + componentId = component.id; + }); + beforeEach(async () => { configureSlackDoubles(); + slackTestState.usersInfoImpl = () => + Promise.resolve({ ok: true, user: { profile: { email: ownerEmail } } }); + slackTestState.resolveWorkspace = (teamId: string) => + teamId === "T_KNOWN" + ? Promise.resolve({ + botToken: "xoxb-fallback", + workspace: { id: workspace.id, limits: { "slack-agent": true } }, + }) + : Promise.resolve(null); redisStore.clear(); reportDraftState.calls = []; reportDraftState.impl = () => Promise.resolve({ - pageId: 1, - componentIds: [2], + pageId, + componentIds: [componentId], impact: "major_outage", message: "The dashboard API is returning errors. We are investigating.", startedAt: new Date(startedAt * 1000).toISOString(), @@ -1781,9 +1808,6 @@ describe("status report follow-up after a declare", () => { }); slackTestState.historyImpl = () => Promise.resolve({ messages: busyChannelHistory }); - workspace = selectWorkspaceSchema.parse( - await db.query.workspace.findFirst({ where: eq(workspaceTable.id, 1) }), - ); const created = await declareIncident({ ctx: { workspace, actor: { type: "system", job: "test" } }, input: { title: `Follow-up ${crypto.randomUUID()}`, severity: "major" }, @@ -1913,8 +1937,8 @@ describe("status report follow-up after a declare", () => { expect(block("message")?.initial_value).toBe( "The dashboard API is returning errors. We are investigating.", ); - expect(block("components_1")?.initial_options).toEqual([ - expect.objectContaining({ value: "2" }), + expect(block(`components_${pageId}`)?.initial_options).toEqual([ + expect.objectContaining({ value: String(componentId) }), ]); expect(block("impact")?.initial_option?.value).toBe("major_outage"); expect(block("started_at")?.initial_date_time).toBe(startedAt); @@ -1934,7 +1958,11 @@ describe("status report follow-up after a declare", () => { }), state: { values: { - page: { incident_card_page: { selected_option: { value: "1" } } }, + page: { + incident_card_page: { + selected_option: { value: String(pageId) }, + }, + }, title: { value: { value: "Dashboard errors" } }, status: { value: { selected_option: { value: "investigating" } } }, started_at: { value: { selected_date_time: startedAt } }, diff --git a/apps/server/src/routes/slack/mrkdwn.test.ts b/apps/server/src/routes/slack/mrkdwn.test.ts index 3e00a7723..1bf374018 100644 --- a/apps/server/src/routes/slack/mrkdwn.test.ts +++ b/apps/server/src/routes/slack/mrkdwn.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, test } from "@openstatus/test-utils"; +import { describe, expect, test } from "vitest"; import { toMrkdwn } from "./mrkdwn"; diff --git a/apps/server/src/routes/slack/next-steps.test.ts b/apps/server/src/routes/slack/next-steps.test.ts index d2c606b97..9c8a8f591 100644 --- a/apps/server/src/routes/slack/next-steps.test.ts +++ b/apps/server/src/routes/slack/next-steps.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, test } from "@openstatus/test-utils"; +import { describe, expect, test } from "vitest"; import { nextStepBlocks } from "./next-steps"; diff --git a/apps/server/src/routes/slack/oauth.test.ts b/apps/server/src/routes/slack/oauth.test.ts index 8f718f5d8..a7f48229b 100644 --- a/apps/server/src/routes/slack/oauth.test.ts +++ b/apps/server/src/routes/slack/oauth.test.ts @@ -1,9 +1,8 @@ import crypto from "node:crypto"; import { SLACK_BOT_SCOPES } from "@openstatus/services/integration"; -import { expect } from "@std/expect"; -import { describe, test } from "@std/testing/bdd"; import { Hono } from "hono"; +import { describe, expect, test } from "vitest"; import { TEST_SIGNING_SECRET as SIGNING_SECRET, diff --git a/apps/server/src/routes/slack/presenters/presenters.test.ts b/apps/server/src/routes/slack/presenters/presenters.test.ts index 5598db039..97661bfb4 100644 --- a/apps/server/src/routes/slack/presenters/presenters.test.ts +++ b/apps/server/src/routes/slack/presenters/presenters.test.ts @@ -1,7 +1,7 @@ import type { ServiceContext } from "@openstatus/services"; import { agentTools } from "@openstatus/services/agent-tools"; import type { AnyAgentTool } from "@openstatus/services/agent-tools"; -import { describe, expect, test } from "@openstatus/test-utils"; +import { describe, expect, test } from "vitest"; import { z } from "zod"; // page-urls.ts hits the db; it's swapped for a deterministic double via the diff --git a/apps/server/src/routes/slack/registry-runner.test.ts b/apps/server/src/routes/slack/registry-runner.test.ts index d0854fad9..f345e6cc3 100644 --- a/apps/server/src/routes/slack/registry-runner.test.ts +++ b/apps/server/src/routes/slack/registry-runner.test.ts @@ -1,8 +1,7 @@ import type { ServiceContext } from "@openstatus/services"; import { agentTools } from "@openstatus/services/agent-tools"; import type { AnyAgentTool } from "@openstatus/services/agent-tools"; -import { expect } from "@std/expect"; -import { describe, test } from "@std/testing/bdd"; +import { describe, expect, test } from "vitest"; import { z } from "zod"; import { @@ -249,7 +248,7 @@ describe("executeRegistryAction", () => { summarize: () => ({ title: "fake", lines: [] }), }, }); - expect( + await expect( executeRegistryAction({ tool: t, ctx: fakeCtx, @@ -265,7 +264,7 @@ describe("executeRegistryAction", () => { throw new Error("kaboom"); }, }); - expect( + await expect( executeRegistryAction({ tool: t, ctx: fakeCtx, diff --git a/apps/server/src/routes/slack/report-draft.test.ts b/apps/server/src/routes/slack/report-draft.test.ts index e3dfd9606..0af1fa953 100644 --- a/apps/server/src/routes/slack/report-draft.test.ts +++ b/apps/server/src/routes/slack/report-draft.test.ts @@ -1,4 +1,4 @@ -import { beforeEach, describe, expect, test } from "@openstatus/test-utils"; +import { beforeEach, describe, expect, test } from "vitest"; import { reportDraftState } from "@/libs/test/doubles/report-draft-model.mock"; import { busyChannelHistory } from "@/libs/test/slack-incident-fixtures"; diff --git a/apps/server/src/routes/slack/resolve-slack-user.test.ts b/apps/server/src/routes/slack/resolve-slack-user.test.ts index 08855ee37..3502033b8 100644 --- a/apps/server/src/routes/slack/resolve-slack-user.test.ts +++ b/apps/server/src/routes/slack/resolve-slack-user.test.ts @@ -10,8 +10,7 @@ import { createUser, } from "@openstatus/db/src/test/factories"; import { WebClient } from "@slack/web-api"; -import { expect } from "@std/expect"; -import { beforeAll, beforeEach, describe, test } from "@std/testing/bdd"; +import { beforeAll, beforeEach, describe, expect, test } from "vitest"; // @slack/web-api is the import-mapped double; `users.info` is driven by // slackTestState.usersInfoImpl. diff --git a/apps/server/src/routes/slack/rich-text.test.ts b/apps/server/src/routes/slack/rich-text.test.ts index 6aa5d6128..240a39010 100644 --- a/apps/server/src/routes/slack/rich-text.test.ts +++ b/apps/server/src/routes/slack/rich-text.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, test } from "@openstatus/test-utils"; +import { describe, expect, test } from "vitest"; import { collectMentions, diff --git a/apps/server/src/routes/slack/slack-links.test.ts b/apps/server/src/routes/slack/slack-links.test.ts index 87588e1f2..dfaf5820a 100644 --- a/apps/server/src/routes/slack/slack-links.test.ts +++ b/apps/server/src/routes/slack/slack-links.test.ts @@ -1,6 +1,6 @@ -import { beforeEach, describe, expect, test } from "@openstatus/test-utils"; // The double stands in for @slack/web-api via the test import map. import { WebClient } from "@slack/web-api"; +import { beforeEach, describe, expect, test } from "vitest"; import { slackTestState } from "@/libs/test/doubles/slack-test-state"; import { diff --git a/apps/server/src/routes/slack/system-prompt.test.ts b/apps/server/src/routes/slack/system-prompt.test.ts index fb8dfe75c..8f22878f1 100644 --- a/apps/server/src/routes/slack/system-prompt.test.ts +++ b/apps/server/src/routes/slack/system-prompt.test.ts @@ -1,5 +1,4 @@ -import { expect } from "@std/expect"; -import { describe, test } from "@std/testing/bdd"; +import { describe, expect, test } from "vitest"; import { buildSystemPrompt } from "./system-prompt"; diff --git a/apps/server/src/routes/slack/thread-memory.test.ts b/apps/server/src/routes/slack/thread-memory.test.ts index 855d9c7bb..df2392edc 100644 --- a/apps/server/src/routes/slack/thread-memory.test.ts +++ b/apps/server/src/routes/slack/thread-memory.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, test } from "@openstatus/test-utils"; +import { describe, expect, test } from "vitest"; import { createdNote, createdRecordFrom } from "./thread-memory"; diff --git a/apps/server/src/routes/slack/thread-messages.test.ts b/apps/server/src/routes/slack/thread-messages.test.ts index 5c3334de0..b802b3fae 100644 --- a/apps/server/src/routes/slack/thread-messages.test.ts +++ b/apps/server/src/routes/slack/thread-messages.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, test } from "@openstatus/test-utils"; +import { describe, expect, test } from "vitest"; import { convertThreadToMessages, diff --git a/apps/server/src/routes/slack/thread-title.test.ts b/apps/server/src/routes/slack/thread-title.test.ts index ea9fac037..af30cd43d 100644 --- a/apps/server/src/routes/slack/thread-title.test.ts +++ b/apps/server/src/routes/slack/thread-title.test.ts @@ -1,5 +1,5 @@ -import { beforeEach, describe, expect, test } from "@openstatus/test-utils"; import type { WebClient } from "@slack/web-api"; +import { beforeEach, describe, expect, test } from "vitest"; import { buildThreadTitle, diff --git a/apps/server/src/routes/slack/verify.test.ts b/apps/server/src/routes/slack/verify.test.ts index 14240ee68..22ea2de7c 100644 --- a/apps/server/src/routes/slack/verify.test.ts +++ b/apps/server/src/routes/slack/verify.test.ts @@ -1,8 +1,7 @@ import crypto from "node:crypto"; -import { expect } from "@std/expect"; -import { describe, test } from "@std/testing/bdd"; import { Hono } from "hono"; +import { describe, expect, test } from "vitest"; import { TEST_SIGNING_SECRET as SIGNING_SECRET, diff --git a/apps/server/src/routes/v1/check/http/post.test.ts b/apps/server/src/routes/v1/check/http/post.test.ts index f94500dcb..c8f64e0b4 100644 --- a/apps/server/src/routes/v1/check/http/post.test.ts +++ b/apps/server/src/routes/v1/check/http/post.test.ts @@ -1,8 +1,8 @@ -import { afterEach, expect, mock, test } from "@openstatus/test-utils"; +import { afterEach, expect, test, vi } from "vitest"; import { app } from "@/index"; -const mockFetch = mock(); +const mockFetch = vi.fn(); global.fetch = mockFetch as unknown as typeof fetch; diff --git a/apps/server/src/routes/v1/incidents/get.test.ts b/apps/server/src/routes/v1/incidents/get.test.ts index 9de6b293d..2b5b33e4c 100644 --- a/apps/server/src/routes/v1/incidents/get.test.ts +++ b/apps/server/src/routes/v1/incidents/get.test.ts @@ -1,5 +1,4 @@ -import { expect } from "@std/expect"; -import { test } from "@std/testing/bdd"; +import { expect, test } from "vitest"; import { app } from "@/index"; diff --git a/apps/server/src/routes/v1/incidents/get_all.test.ts b/apps/server/src/routes/v1/incidents/get_all.test.ts index c0c5cf082..cf9405cb5 100644 --- a/apps/server/src/routes/v1/incidents/get_all.test.ts +++ b/apps/server/src/routes/v1/incidents/get_all.test.ts @@ -1,7 +1,6 @@ import { db, eq } from "@openstatus/db"; import { monitorIncidentTable, monitor } from "@openstatus/db/src/schema"; -import { expect } from "@std/expect"; -import { afterAll, beforeAll, test } from "@std/testing/bdd"; +import { afterAll, beforeAll, expect, test } from "vitest"; import { app } from "@/index"; diff --git a/apps/server/src/routes/v1/incidents/put.test.ts b/apps/server/src/routes/v1/incidents/put.test.ts index 8bf5f059e..c2aa59f45 100644 --- a/apps/server/src/routes/v1/incidents/put.test.ts +++ b/apps/server/src/routes/v1/incidents/put.test.ts @@ -1,5 +1,4 @@ -import { expect } from "@std/expect"; -import { test } from "@std/testing/bdd"; +import { expect, test } from "vitest"; import { app } from "@/index"; diff --git a/apps/server/src/routes/v1/maintenances/attribution.test.ts b/apps/server/src/routes/v1/maintenances/attribution.test.ts index f7e55962c..9fba86554 100644 --- a/apps/server/src/routes/v1/maintenances/attribution.test.ts +++ b/apps/server/src/routes/v1/maintenances/attribution.test.ts @@ -1,8 +1,7 @@ import { OpenAPIHono } from "@hono/zod-openapi"; import { db, eq } from "@openstatus/db"; import { maintenance } from "@openstatus/db/src/schema"; -import { expect } from "@std/expect"; -import { test } from "@std/testing/bdd"; +import { expect, test } from "vitest"; import { lookupWorkspace } from "@/libs/middlewares/auth"; diff --git a/apps/server/src/routes/v1/maintenances/get.test.ts b/apps/server/src/routes/v1/maintenances/get.test.ts index 69a4a2483..db267b89b 100644 --- a/apps/server/src/routes/v1/maintenances/get.test.ts +++ b/apps/server/src/routes/v1/maintenances/get.test.ts @@ -1,5 +1,4 @@ -import { expect } from "@std/expect"; -import { test } from "@std/testing/bdd"; +import { expect, test } from "vitest"; import { app } from "@/index"; diff --git a/apps/server/src/routes/v1/maintenances/get_all.test.ts b/apps/server/src/routes/v1/maintenances/get_all.test.ts index 2eb761920..07d893845 100644 --- a/apps/server/src/routes/v1/maintenances/get_all.test.ts +++ b/apps/server/src/routes/v1/maintenances/get_all.test.ts @@ -5,8 +5,7 @@ import { monitor, pageComponent, } from "@openstatus/db/src/schema"; -import { expect } from "@std/expect"; -import { afterAll, beforeAll, test } from "@std/testing/bdd"; +import { afterAll, beforeAll, expect, test } from "vitest"; import { app } from "@/index"; diff --git a/apps/server/src/routes/v1/maintenances/post.test.ts b/apps/server/src/routes/v1/maintenances/post.test.ts index e6312a1d7..97bff6e92 100644 --- a/apps/server/src/routes/v1/maintenances/post.test.ts +++ b/apps/server/src/routes/v1/maintenances/post.test.ts @@ -1,7 +1,6 @@ import { db, eq } from "@openstatus/db"; import { maintenance } from "@openstatus/db/src/schema"; -import { expect } from "@std/expect"; -import { beforeEach, test } from "@std/testing/bdd"; +import { beforeEach, expect, test } from "vitest"; import { app } from "@/index"; diff --git a/apps/server/src/routes/v1/maintenances/put.test.ts b/apps/server/src/routes/v1/maintenances/put.test.ts index 1400f196a..6ddaff617 100644 --- a/apps/server/src/routes/v1/maintenances/put.test.ts +++ b/apps/server/src/routes/v1/maintenances/put.test.ts @@ -1,5 +1,4 @@ -import { expect } from "@std/expect"; -import { test } from "@std/testing/bdd"; +import { expect, test } from "vitest"; import { app } from "@/index"; @@ -97,7 +96,7 @@ test("update only the message", async () => { expect(result.data?.message).toBe("Only Message Updated"); }); -test.ignore("update only the dates", async () => { +test.skip("update only the dates", async () => { const from = new Date(); const to = new Date(from.getTime() + 7200000); // 2 hours later @@ -121,27 +120,24 @@ test.ignore("update only the dates", async () => { expect(result.data?.to).toEqual(to); }); -test.ignore( - "update maintenance with `from` date after `to` date should return 400", - async () => { - const to = new Date(); - const from = new Date(to.getTime() + 3600000); // from is 1 hour after to - - const res = await app.request("/v1/maintenance/1", { - method: "PUT", - headers: { - "x-openstatus-key": "1", - "Content-Type": "application/json", - }, - body: JSON.stringify({ - from: from.toISOString(), - to: to.toISOString(), - }), - }); - - expect(res.status).toBe(400); - }, -); +test.skip("update maintenance with `from` date after `to` date should return 400", async () => { + const to = new Date(); + const from = new Date(to.getTime() + 3600000); // from is 1 hour after to + + const res = await app.request("/v1/maintenance/1", { + method: "PUT", + headers: { + "x-openstatus-key": "1", + "Content-Type": "application/json", + }, + body: JSON.stringify({ + from: from.toISOString(), + to: to.toISOString(), + }), + }); + + expect(res.status).toBe(400); +}); test("remove all maintenance monitors", async () => { const res = await app.request("/v1/maintenance/1", { @@ -162,7 +158,7 @@ test("remove all maintenance monitors", async () => { expect(result.data?.monitorIds?.length).toBe(0); }); -test.ignore("empty body should return 400", async () => { +test.skip("empty body should return 400", async () => { const res = await app.request("/v1/maintenance/1", { method: "PUT", headers: { diff --git a/apps/server/src/routes/v1/monitors/delete.test.ts b/apps/server/src/routes/v1/monitors/delete.test.ts index 9e9adec89..3b5d27d6a 100644 --- a/apps/server/src/routes/v1/monitors/delete.test.ts +++ b/apps/server/src/routes/v1/monitors/delete.test.ts @@ -1,5 +1,4 @@ -import { expect } from "@std/expect"; -import { test } from "@std/testing/bdd"; +import { expect, test } from "vitest"; import { app } from "@/index"; diff --git a/apps/server/src/routes/v1/monitors/get.test.ts b/apps/server/src/routes/v1/monitors/get.test.ts index 15209292c..64fe515b0 100644 --- a/apps/server/src/routes/v1/monitors/get.test.ts +++ b/apps/server/src/routes/v1/monitors/get.test.ts @@ -1,5 +1,4 @@ -import { expect } from "@std/expect"; -import { test } from "@std/testing/bdd"; +import { expect, test } from "vitest"; import { app } from "@/index"; diff --git a/apps/server/src/routes/v1/monitors/get_all.test.ts b/apps/server/src/routes/v1/monitors/get_all.test.ts index 97c7dbedc..dbb7482ac 100644 --- a/apps/server/src/routes/v1/monitors/get_all.test.ts +++ b/apps/server/src/routes/v1/monitors/get_all.test.ts @@ -1,5 +1,4 @@ -import { expect } from "@std/expect"; -import { test } from "@std/testing/bdd"; +import { expect, test } from "vitest"; import { app } from "@/index"; diff --git a/apps/server/src/routes/v1/monitors/post.test.ts b/apps/server/src/routes/v1/monitors/post.test.ts index 84ba06e50..711de0df3 100644 --- a/apps/server/src/routes/v1/monitors/post.test.ts +++ b/apps/server/src/routes/v1/monitors/post.test.ts @@ -1,5 +1,4 @@ -import { expect } from "@std/expect"; -import { test } from "@std/testing/bdd"; +import { expect, test } from "vitest"; import { app } from "@/index"; import { createErrorSchema } from "@/libs/errors"; diff --git a/apps/server/src/routes/v1/monitors/post_dns.test.ts b/apps/server/src/routes/v1/monitors/post_dns.test.ts index 5a5b3d81f..d97fd72d8 100644 --- a/apps/server/src/routes/v1/monitors/post_dns.test.ts +++ b/apps/server/src/routes/v1/monitors/post_dns.test.ts @@ -1,5 +1,4 @@ -import { expect } from "@std/expect"; -import { test } from "@std/testing/bdd"; +import { expect, test } from "vitest"; import { app } from "@/index"; diff --git a/apps/server/src/routes/v1/monitors/post_http.test.ts b/apps/server/src/routes/v1/monitors/post_http.test.ts index 7ac84591a..f96c19f4c 100644 --- a/apps/server/src/routes/v1/monitors/post_http.test.ts +++ b/apps/server/src/routes/v1/monitors/post_http.test.ts @@ -1,5 +1,4 @@ -import { expect } from "@std/expect"; -import { test } from "@std/testing/bdd"; +import { expect, test } from "vitest"; import { app } from "@/index"; diff --git a/apps/server/src/routes/v1/monitors/post_tcp.test.ts b/apps/server/src/routes/v1/monitors/post_tcp.test.ts index 650a79dca..ddd9a4094 100644 --- a/apps/server/src/routes/v1/monitors/post_tcp.test.ts +++ b/apps/server/src/routes/v1/monitors/post_tcp.test.ts @@ -1,5 +1,4 @@ -import { expect } from "@std/expect"; -import { test } from "@std/testing/bdd"; +import { expect, test } from "vitest"; import { app } from "@/index"; diff --git a/apps/server/src/routes/v1/monitors/put.test.ts b/apps/server/src/routes/v1/monitors/put.test.ts index f9714651f..aaeba27a9 100644 --- a/apps/server/src/routes/v1/monitors/put.test.ts +++ b/apps/server/src/routes/v1/monitors/put.test.ts @@ -1,5 +1,4 @@ -import { expect } from "@std/expect"; -import { test } from "@std/testing/bdd"; +import { expect, test } from "vitest"; import { app } from "@/index"; diff --git a/apps/server/src/routes/v1/monitors/put_dns.test.ts b/apps/server/src/routes/v1/monitors/put_dns.test.ts index a55026b81..2b96d65c9 100644 --- a/apps/server/src/routes/v1/monitors/put_dns.test.ts +++ b/apps/server/src/routes/v1/monitors/put_dns.test.ts @@ -1,5 +1,4 @@ -import { expect } from "@std/expect"; -import { test } from "@std/testing/bdd"; +import { expect, test } from "vitest"; import { app } from "@/index"; diff --git a/apps/server/src/routes/v1/monitors/put_http.test.ts b/apps/server/src/routes/v1/monitors/put_http.test.ts index 12dd5ff79..1ae7b46fa 100644 --- a/apps/server/src/routes/v1/monitors/put_http.test.ts +++ b/apps/server/src/routes/v1/monitors/put_http.test.ts @@ -1,5 +1,4 @@ -import { expect } from "@std/expect"; -import { test } from "@std/testing/bdd"; +import { expect, test } from "vitest"; import { app } from "@/index"; diff --git a/apps/server/src/routes/v1/monitors/put_tcp.test.ts b/apps/server/src/routes/v1/monitors/put_tcp.test.ts index db7db81a7..b10ca8e3c 100644 --- a/apps/server/src/routes/v1/monitors/put_tcp.test.ts +++ b/apps/server/src/routes/v1/monitors/put_tcp.test.ts @@ -1,5 +1,4 @@ -import { expect } from "@std/expect"; -import { test } from "@std/testing/bdd"; +import { expect, test } from "vitest"; import { app } from "@/index"; diff --git a/apps/server/src/routes/v1/monitors/results/get.test.ts b/apps/server/src/routes/v1/monitors/results/get.test.ts index 511ec1bff..7bdd94d7e 100644 --- a/apps/server/src/routes/v1/monitors/results/get.test.ts +++ b/apps/server/src/routes/v1/monitors/results/get.test.ts @@ -1,11 +1,10 @@ -import { expect } from "@std/expect"; -import { test } from "@std/testing/bdd"; +import { expect, test } from "vitest"; import { app } from "@/index"; import { ResultRun } from "../schema"; -test.ignore("get monitor result with valid id should return 200", async () => { +test.skip("get monitor result with valid id should return 200", async () => { const res = await app.request("/v1/monitor/1/result/1", { method: "GET", headers: { @@ -61,23 +60,20 @@ test("get monitor result from different workspace should return 404", async () = expect(res.status).toBe(404); }); -test.ignore( - "get monitor result with valid TCP monitor should return 200", - async () => { - const res = await app.request("/v1/monitor/4/result/2", { - method: "GET", - headers: { - "x-openstatus-key": "1", - }, - }); - - expect(res.status).toBe(200); - - const json = await res.json(); - const result = ResultRun.array().safeParse(json); - expect(result.success).toBe(true); - }, -); +test.skip("get monitor result with valid TCP monitor should return 200", async () => { + const res = await app.request("/v1/monitor/4/result/2", { + method: "GET", + headers: { + "x-openstatus-key": "1", + }, + }); + + expect(res.status).toBe(200); + + const json = await res.json(); + const result = ResultRun.array().safeParse(json); + expect(result.success).toBe(true); +}); test("get monitor result with non-matching result id should return 404", async () => { const res = await app.request("/v1/monitor/1/result/999998", { diff --git a/apps/server/src/routes/v1/monitors/run/post.test.ts b/apps/server/src/routes/v1/monitors/run/post.test.ts index b0a1f7565..29e2b846d 100644 --- a/apps/server/src/routes/v1/monitors/run/post.test.ts +++ b/apps/server/src/routes/v1/monitors/run/post.test.ts @@ -1,10 +1,10 @@ -import { afterEach, expect, mock, test } from "@openstatus/test-utils"; +import { afterEach, expect, test, vi } from "vitest"; import { app } from "@/index"; import { TriggerResult } from "../schema"; -const mockFetch = mock(); +const mockFetch = vi.fn(); global.fetch = mockFetch as unknown as typeof fetch; @@ -56,6 +56,8 @@ test("run monitor with valid id should return 200", async () => { }); test("run monitor with no-wait parameter should return empty array", async () => { + // The handler attaches .catch to each checker request without awaiting it. + mockFetch.mockResolvedValue(new Response("{}")); const res = await app.request("/v1/monitor/1/run?no-wait=true", { method: "POST", headers: { @@ -142,47 +144,44 @@ test("run TCP monitor with valid id should return 200", async () => { } }); -test.ignore( - "run monitor with multiple regions should return array of results", - async () => { - mockFetch.mockReturnValue( - Promise.resolve( - new Response( - JSON.stringify({ - jobType: "http", - status: 200, - latency: 100, - region: "ams", - timestamp: 1234567890, - timing: { - dnsStart: 1, - dnsDone: 2, - connectStart: 3, - connectDone: 4, - tlsHandshakeStart: 5, - tlsHandshakeDone: 6, - firstByteStart: 7, - firstByteDone: 8, - transferStart: 9, - transferDone: 10, - }, - }), - { status: 200, headers: { "content-type": "application/json" } }, - ), +test.skip("run monitor with multiple regions should return array of results", async () => { + mockFetch.mockReturnValue( + Promise.resolve( + new Response( + JSON.stringify({ + jobType: "http", + status: 200, + latency: 100, + region: "ams", + timestamp: 1234567890, + timing: { + dnsStart: 1, + dnsDone: 2, + connectStart: 3, + connectDone: 4, + tlsHandshakeStart: 5, + tlsHandshakeDone: 6, + firstByteStart: 7, + firstByteDone: 8, + transferStart: 9, + transferDone: 10, + }, + }), + { status: 200, headers: { "content-type": "application/json" } }, ), - ); - - const res = await app.request("/v1/monitor/5/run", { - method: "POST", - headers: { - "x-openstatus-key": "1", - "content-type": "application/json", - }, - }); - - expect(res.status).toBe(200); - - const json = await res.json(); - expect(Array.isArray(json)).toBe(true); - }, -); + ), + ); + + const res = await app.request("/v1/monitor/5/run", { + method: "POST", + headers: { + "x-openstatus-key": "1", + "content-type": "application/json", + }, + }); + + expect(res.status).toBe(200); + + const json = await res.json(); + expect(Array.isArray(json)).toBe(true); +}); diff --git a/apps/server/src/routes/v1/monitors/summary/get.test.ts b/apps/server/src/routes/v1/monitors/summary/get.test.ts index 300271c17..bc21298fa 100644 --- a/apps/server/src/routes/v1/monitors/summary/get.test.ts +++ b/apps/server/src/routes/v1/monitors/summary/get.test.ts @@ -1,12 +1,11 @@ import { z } from "@hono/zod-openapi"; -import { expect } from "@std/expect"; -import { test } from "@std/testing/bdd"; +import { expect, test } from "vitest"; import { app } from "@/index"; import { SummarySchema } from "./schema"; -test.ignore("return the summary of the monitor", async () => { +test.skip("return the summary of the monitor", async () => { const res = await app.request("/v1/monitor/1/summary", { headers: { "x-openstatus-key": "1", diff --git a/apps/server/src/routes/v1/monitors/trigger/post.test.ts b/apps/server/src/routes/v1/monitors/trigger/post.test.ts index f0b322c27..190a74ed8 100644 --- a/apps/server/src/routes/v1/monitors/trigger/post.test.ts +++ b/apps/server/src/routes/v1/monitors/trigger/post.test.ts @@ -1,11 +1,11 @@ import { createMonitor } from "@openstatus/db/src/test/factories"; -import { afterEach, expect, mock, test } from "@openstatus/test-utils"; +import { afterEach, expect, test, vi } from "vitest"; import { app } from "@/index"; import { TriggerSchema } from "./schema"; -const mockFetch = mock(); +const mockFetch = vi.fn(); global.fetch = mockFetch as unknown as typeof fetch; diff --git a/apps/server/src/routes/v1/notifications/get.test.ts b/apps/server/src/routes/v1/notifications/get.test.ts index 8759de392..bf9c410e7 100644 --- a/apps/server/src/routes/v1/notifications/get.test.ts +++ b/apps/server/src/routes/v1/notifications/get.test.ts @@ -1,5 +1,4 @@ -import { expect } from "@std/expect"; -import { test } from "@std/testing/bdd"; +import { expect, test } from "vitest"; import { app } from "@/index"; diff --git a/apps/server/src/routes/v1/notifications/get_all.test.ts b/apps/server/src/routes/v1/notifications/get_all.test.ts index 5f2f580eb..624dc46c5 100644 --- a/apps/server/src/routes/v1/notifications/get_all.test.ts +++ b/apps/server/src/routes/v1/notifications/get_all.test.ts @@ -1,7 +1,6 @@ import { db, eq } from "@openstatus/db"; import { notification } from "@openstatus/db/src/schema"; -import { expect } from "@std/expect"; -import { afterAll, beforeAll, test } from "@std/testing/bdd"; +import { afterAll, beforeAll, expect, test } from "vitest"; import { app } from "@/index"; diff --git a/apps/server/src/routes/v1/notifications/post.test.ts b/apps/server/src/routes/v1/notifications/post.test.ts index d65ce7dee..c1c130f53 100644 --- a/apps/server/src/routes/v1/notifications/post.test.ts +++ b/apps/server/src/routes/v1/notifications/post.test.ts @@ -1,7 +1,6 @@ import { db, eq } from "@openstatus/db"; import { notification } from "@openstatus/db/src/schema"; -import { expect } from "@std/expect"; -import { test } from "@std/testing/bdd"; +import { expect, test } from "vitest"; import { app } from "@/index"; diff --git a/apps/server/src/routes/v1/pageSubscribers/post.test.ts b/apps/server/src/routes/v1/pageSubscribers/post.test.ts index 1aad898fc..5a5973b28 100644 --- a/apps/server/src/routes/v1/pageSubscribers/post.test.ts +++ b/apps/server/src/routes/v1/pageSubscribers/post.test.ts @@ -1,9 +1,7 @@ import { and, db, eq, isNull } from "@openstatus/db"; import { pageSubscriber } from "@openstatus/db/src/schema"; import { delivery, resend } from "@openstatus/emails/src/send"; -import { expect } from "@std/expect"; -import { test } from "@std/testing/bdd"; -import { assertSpyCalls, stub } from "@std/testing/mock"; +import { expect, test, vi } from "vitest"; import { app } from "@/index"; @@ -33,10 +31,12 @@ test("create a page subscription", async () => { }); test("sends the same redesigned verification email as the status page form", async () => { - const enabled = stub(delivery, "enabled", () => true); - const send = stub(resend.emails, "send", () => - Promise.resolve({ data: { id: "email_1" }, error: null } as never), - ); + const enabled = vi.spyOn(delivery, "enabled").mockImplementation(() => true); + const send = vi + .spyOn(resend.emails, "send") + .mockImplementation(() => + Promise.resolve({ data: { id: "email_1" }, error: null } as never), + ); const email = "verify-mail@openstatus.dev"; const cleanup = () => @@ -54,20 +54,23 @@ test("sends the same redesigned verification email as the status page form", asy }); expect(res.status).toBe(200); - assertSpyCalls(send, 1); - const payload = send.calls[0].args[0]; + expect(send).toHaveBeenCalledTimes(1); + const payload = send.mock.calls[0][0]; expect(payload.to).toEqual([email]); expect(payload.from).toBe( "Status Page ", ); expect(payload.subject).toMatch(/^Confirm your subscription to /); // the route calls the template as a function, so this is its - expect(payload.react.props.brand.name).toMatch(/Status$/); - expect(payload.react.props.pill.label).toBe("Confirm"); + const layout = payload.react as unknown as { + props: { brand: { name: string }; pill: { label: string } }; + }; + expect(layout.props.brand.name).toMatch(/Status$/); + expect(layout.props.pill.label).toBe("Confirm"); } finally { await cleanup(); - send.restore(); - enabled.restore(); + send.mockRestore(); + enabled.mockRestore(); } }); diff --git a/apps/server/src/routes/v1/pages/get.test.ts b/apps/server/src/routes/v1/pages/get.test.ts index 4335e569d..d6012e05f 100644 --- a/apps/server/src/routes/v1/pages/get.test.ts +++ b/apps/server/src/routes/v1/pages/get.test.ts @@ -1,5 +1,4 @@ -import { expect } from "@std/expect"; -import { test } from "@std/testing/bdd"; +import { expect, test } from "vitest"; import { app } from "@/index"; diff --git a/apps/server/src/routes/v1/pages/get_all.test.ts b/apps/server/src/routes/v1/pages/get_all.test.ts index 6c9485527..26753c7c5 100644 --- a/apps/server/src/routes/v1/pages/get_all.test.ts +++ b/apps/server/src/routes/v1/pages/get_all.test.ts @@ -1,7 +1,6 @@ import { db, eq } from "@openstatus/db"; import { monitor, page, pageComponent } from "@openstatus/db/src/schema"; -import { expect } from "@std/expect"; -import { afterAll, beforeAll, test } from "@std/testing/bdd"; +import { afterAll, beforeAll, expect, test } from "vitest"; import { app } from "@/index"; diff --git a/apps/server/src/routes/v1/pages/post.test.ts b/apps/server/src/routes/v1/pages/post.test.ts index ae17a0a51..603a77769 100644 --- a/apps/server/src/routes/v1/pages/post.test.ts +++ b/apps/server/src/routes/v1/pages/post.test.ts @@ -1,7 +1,6 @@ import { db, eq } from "@openstatus/db"; import { monitor, page, pageComponent } from "@openstatus/db/src/schema"; -import { expect } from "@std/expect"; -import { test } from "@std/testing/bdd"; +import { expect, test } from "vitest"; import { app } from "@/index"; diff --git a/apps/server/src/routes/v1/pages/put.test.ts b/apps/server/src/routes/v1/pages/put.test.ts index d34c68145..cf88ff197 100644 --- a/apps/server/src/routes/v1/pages/put.test.ts +++ b/apps/server/src/routes/v1/pages/put.test.ts @@ -1,5 +1,4 @@ -import { expect } from "@std/expect"; -import { test } from "@std/testing/bdd"; +import { expect, test } from "vitest"; import { app } from "@/index"; diff --git a/apps/server/src/routes/v1/statusReportUpdates/get.test.ts b/apps/server/src/routes/v1/statusReportUpdates/get.test.ts index 510c03b86..21a4b2942 100644 --- a/apps/server/src/routes/v1/statusReportUpdates/get.test.ts +++ b/apps/server/src/routes/v1/statusReportUpdates/get.test.ts @@ -1,5 +1,4 @@ -import { expect } from "@std/expect"; -import { test } from "@std/testing/bdd"; +import { expect, test } from "vitest"; import { app } from "@/index"; diff --git a/apps/server/src/routes/v1/statusReportUpdates/post.test.ts b/apps/server/src/routes/v1/statusReportUpdates/post.test.ts index d07c2f315..eedb2217d 100644 --- a/apps/server/src/routes/v1/statusReportUpdates/post.test.ts +++ b/apps/server/src/routes/v1/statusReportUpdates/post.test.ts @@ -1,5 +1,4 @@ -import { expect } from "@std/expect"; -import { beforeEach, test } from "@std/testing/bdd"; +import { beforeEach, expect, test } from "vitest"; import { app } from "@/index"; diff --git a/apps/server/src/routes/v1/statusReports/attribution.test.ts b/apps/server/src/routes/v1/statusReports/attribution.test.ts index 99f97c113..68c4ffdbe 100644 --- a/apps/server/src/routes/v1/statusReports/attribution.test.ts +++ b/apps/server/src/routes/v1/statusReports/attribution.test.ts @@ -1,8 +1,7 @@ import { OpenAPIHono } from "@hono/zod-openapi"; import { db, eq } from "@openstatus/db"; import { statusReport, statusReportUpdate } from "@openstatus/db/src/schema"; -import { expect } from "@std/expect"; -import { test } from "@std/testing/bdd"; +import { expect, test } from "vitest"; import { lookupWorkspace } from "@/libs/middlewares/auth"; diff --git a/apps/server/src/routes/v1/statusReports/delete.test.ts b/apps/server/src/routes/v1/statusReports/delete.test.ts index b0760725b..6ad5cb2b3 100644 --- a/apps/server/src/routes/v1/statusReports/delete.test.ts +++ b/apps/server/src/routes/v1/statusReports/delete.test.ts @@ -1,5 +1,4 @@ -import { expect } from "@std/expect"; -import { test } from "@std/testing/bdd"; +import { expect, test } from "vitest"; import { app } from "@/index"; diff --git a/apps/server/src/routes/v1/statusReports/get.test.ts b/apps/server/src/routes/v1/statusReports/get.test.ts index 61980dfde..e7e467dce 100644 --- a/apps/server/src/routes/v1/statusReports/get.test.ts +++ b/apps/server/src/routes/v1/statusReports/get.test.ts @@ -1,5 +1,4 @@ -import { expect } from "@std/expect"; -import { test } from "@std/testing/bdd"; +import { expect, test } from "vitest"; import { app } from "@/index"; diff --git a/apps/server/src/routes/v1/statusReports/get_all.test.ts b/apps/server/src/routes/v1/statusReports/get_all.test.ts index 93e057df5..4fb1414ec 100644 --- a/apps/server/src/routes/v1/statusReports/get_all.test.ts +++ b/apps/server/src/routes/v1/statusReports/get_all.test.ts @@ -6,8 +6,7 @@ import { statusReportUpdate, statusReportsToPageComponents, } from "@openstatus/db/src/schema"; -import { expect } from "@std/expect"; -import { afterAll, beforeAll, test } from "@std/testing/bdd"; +import { afterAll, beforeAll, expect, test } from "vitest"; import { app } from "@/index"; diff --git a/apps/server/src/routes/v1/statusReports/post.test.ts b/apps/server/src/routes/v1/statusReports/post.test.ts index 94c8d9058..6310181f9 100644 --- a/apps/server/src/routes/v1/statusReports/post.test.ts +++ b/apps/server/src/routes/v1/statusReports/post.test.ts @@ -4,8 +4,7 @@ import { statusReport, statusReportsToPageComponents, } from "@openstatus/db/src/schema"; -import { expect } from "@std/expect"; -import { beforeEach, test } from "@std/testing/bdd"; +import { beforeEach, expect, test } from "vitest"; import { app } from "@/index"; diff --git a/apps/server/src/routes/v1/statusReports/subscriber-filtering.integration.test.ts b/apps/server/src/routes/v1/statusReports/subscriber-filtering.integration.test.ts index 95d14722d..352787ab3 100644 --- a/apps/server/src/routes/v1/statusReports/subscriber-filtering.integration.test.ts +++ b/apps/server/src/routes/v1/statusReports/subscriber-filtering.integration.test.ts @@ -1,7 +1,6 @@ import { and, db, eq, isNotNull, isNull } from "@openstatus/db"; import { page, pageSubscriber } from "@openstatus/db/src/schema"; -import { expect } from "@std/expect"; -import { afterAll, beforeAll, describe, test } from "@std/testing/bdd"; +import { afterAll, beforeAll, describe, expect, test } from "vitest"; /** * Integration tests for subscriber filtering in status report email queries. diff --git a/apps/server/src/routes/v1/statusReports/update/post.test.ts b/apps/server/src/routes/v1/statusReports/update/post.test.ts index cf13fc176..83604a96f 100644 --- a/apps/server/src/routes/v1/statusReports/update/post.test.ts +++ b/apps/server/src/routes/v1/statusReports/update/post.test.ts @@ -1,5 +1,4 @@ -import { expect } from "@std/expect"; -import { test } from "@std/testing/bdd"; +import { expect, test } from "vitest"; import { app } from "@/index"; diff --git a/apps/server/src/routes/v1/whoami/get.test.ts b/apps/server/src/routes/v1/whoami/get.test.ts index a0a7aa1b2..34839bbe4 100644 --- a/apps/server/src/routes/v1/whoami/get.test.ts +++ b/apps/server/src/routes/v1/whoami/get.test.ts @@ -1,5 +1,4 @@ -import { expect } from "@std/expect"; -import { test } from "@std/testing/bdd"; +import { expect, test } from "vitest"; import { app } from "@/index"; diff --git a/apps/server/tsconfig.json b/apps/server/tsconfig.json index 004eda196..39fd093f5 100644 --- a/apps/server/tsconfig.json +++ b/apps/server/tsconfig.json @@ -9,16 +9,13 @@ "jsxImportSource": "react", "allowJs": true, "paths": { - "@/*": ["./src/*"] + "@/*": ["./src/*"], + "@openstatus/analytics-real": ["../../packages/analytics/src/index.ts"], + "@openstatus/emails-real": ["../../packages/emails/src/index.ts"], + "@openstatus/subscriptions-real": [ + "../../packages/subscriptions/src/index.ts" + ], + "@openstatus/tinybird-real": ["../../packages/tinybird/src/index.ts"] } - }, - "exclude": [ - "node_modules", - "**/*.test.ts", - "**/*.test.tsx", - "**/__tests__/**", - "**/test/**", - "**/*.mock.ts", - "**/test-preload.ts" - ] + } } diff --git a/apps/server/vitest.config.ts b/apps/server/vitest.config.ts new file mode 100644 index 000000000..8b57687a1 --- /dev/null +++ b/apps/server/vitest.config.ts @@ -0,0 +1,6 @@ +import { defineTestConfig } from "@openstatus/test-utils/vitest"; + +export default defineTestConfig(import.meta.dirname, { + importMap: "test.importmap.json", + envFile: ".env.test", +}); diff --git a/apps/status-page/package.json b/apps/status-page/package.json index 17eeb0ad8..9ec316bfd 100644 --- a/apps/status-page/package.json +++ b/apps/status-page/package.json @@ -8,7 +8,7 @@ "build": "next build", "start": "next start", "lint": "next lint", - "test": "deno test --parallel -A --no-check --sloppy-imports --import-map=test.importmap.json", + "test": "vitest run", "tsc": "tsc --noEmit" }, "dependencies": { @@ -82,9 +82,8 @@ "zod": "catalog:" }, "devDependencies": { + "@openstatus/test-utils": "workspace:*", "@openstatus/tsconfig": "workspace:*", - "@std/expect": "jsr:^1.0.19", - "@std/testing": "jsr:^1.0.19", "@tailwindcss/postcss": "catalog:", "@tailwindcss/typography": "catalog:", "@types/dom-speech-recognition": "catalog:", @@ -94,6 +93,7 @@ "shadcn": "catalog:", "tailwindcss": "catalog:", "tw-animate-css": "catalog:", - "typescript": "catalog:" + "typescript": "catalog:", + "vitest": "catalog:" } } diff --git a/apps/status-page/src/components/button/buttons.test.tsx b/apps/status-page/src/components/button/buttons.test.tsx index ea891d4c2..4a3d0abe3 100644 --- a/apps/status-page/src/components/button/buttons.test.tsx +++ b/apps/status-page/src/components/button/buttons.test.tsx @@ -1,6 +1,5 @@ -import { expect } from "@std/expect"; -import { describe, it } from "@std/testing/bdd"; import { renderToStaticMarkup } from "react-dom/server"; +import { describe, expect, it } from "vitest"; import { ChartTooltipNumber } from "../chart/chart-tooltip-number"; import { PopoverQuantile } from "../popover/popover-quantile"; diff --git a/apps/status-page/src/components/common/link.test.tsx b/apps/status-page/src/components/common/link.test.tsx index 2ad2294d0..d1975e0d6 100644 --- a/apps/status-page/src/components/common/link.test.tsx +++ b/apps/status-page/src/components/common/link.test.tsx @@ -1,7 +1,6 @@ -import { expect } from "@std/expect"; -import { describe, it } from "@std/testing/bdd"; import { NuqsTestingAdapter } from "nuqs/adapters/testing"; import { renderToStaticMarkup } from "react-dom/server"; +import { describe, expect, it } from "vitest"; import { Link } from "./link"; diff --git a/apps/status-page/src/components/content/process-message.test.tsx b/apps/status-page/src/components/content/process-message.test.tsx index 75db588b1..1bf33fa51 100644 --- a/apps/status-page/src/components/content/process-message.test.tsx +++ b/apps/status-page/src/components/content/process-message.test.tsx @@ -1,6 +1,5 @@ -import { expect } from "@std/expect"; -import { describe, it } from "@std/testing/bdd"; import { renderToStaticMarkup } from "react-dom/server"; +import { describe, expect, it } from "vitest"; import { ProcessMessage } from "./process-message"; @@ -43,7 +42,7 @@ describe("ProcessMessage", () => { }); // the `ol` override drops its props, so ordered list items vanish - it.ignore("keeps the items of ordered lists", () => { + it.skip("keeps the items of ordered lists", () => { const html = render("1. first\n2. second"); expect(html).toContain("list-decimal"); expect(html).toContain("
  • first
  • "); diff --git a/apps/status-page/src/components/date-picker.test.tsx b/apps/status-page/src/components/date-picker.test.tsx index eb47b7471..7d2a9cee2 100644 --- a/apps/status-page/src/components/date-picker.test.tsx +++ b/apps/status-page/src/components/date-picker.test.tsx @@ -1,6 +1,5 @@ -import { expect } from "@std/expect"; -import { describe, it } from "@std/testing/bdd"; import { renderToStaticMarkup } from "react-dom/server"; +import { describe, expect, it } from "vitest"; import { DatePicker } from "./date-picker"; diff --git a/apps/status-page/src/components/forms/form-manage-subscription.test.tsx b/apps/status-page/src/components/forms/form-manage-subscription.test.tsx index 63af75e18..48d2e4a7d 100644 --- a/apps/status-page/src/components/forms/form-manage-subscription.test.tsx +++ b/apps/status-page/src/components/forms/form-manage-subscription.test.tsx @@ -1,7 +1,6 @@ import type { RouterOutputs } from "@openstatus/api"; -import { expect } from "@std/expect"; -import { describe, it } from "@std/testing/bdd"; import { renderToStaticMarkup } from "react-dom/server"; +import { describe, expect, it } from "vitest"; import { FormManageSubscription } from "./form-manage-subscription"; diff --git a/apps/status-page/src/components/forms/forms.test.tsx b/apps/status-page/src/components/forms/forms.test.tsx index 979a1cd42..66cdb3e45 100644 --- a/apps/status-page/src/components/forms/forms.test.tsx +++ b/apps/status-page/src/components/forms/forms.test.tsx @@ -1,6 +1,5 @@ -import { expect } from "@std/expect"; -import { describe, it } from "@std/testing/bdd"; import { renderToStaticMarkup } from "react-dom/server"; +import { describe, expect, it } from "vitest"; import { FormCard, diff --git a/apps/status-page/src/components/locale-switcher.test.tsx b/apps/status-page/src/components/locale-switcher.test.tsx index bedb8d2b2..e3d0a5c97 100644 --- a/apps/status-page/src/components/locale-switcher.test.tsx +++ b/apps/status-page/src/components/locale-switcher.test.tsx @@ -1,6 +1,5 @@ -import { expect } from "@std/expect"; -import { describe, it } from "@std/testing/bdd"; import { renderToStaticMarkup } from "react-dom/server"; +import { describe, expect, it } from "vitest"; import { LocaleSwitcher } from "./locale-switcher"; diff --git a/apps/status-page/src/components/status-page/status-component-group.test.tsx b/apps/status-page/src/components/status-page/status-component-group.test.tsx index 7a8a10bb9..e7f223ff8 100644 --- a/apps/status-page/src/components/status-page/status-component-group.test.tsx +++ b/apps/status-page/src/components/status-page/status-component-group.test.tsx @@ -1,6 +1,5 @@ -import { expect } from "@std/expect"; -import { describe, it } from "@std/testing/bdd"; import { renderToStaticMarkup } from "react-dom/server"; +import { describe, expect, it } from "vitest"; import { StatusComponentGroup } from "./status-component-group"; diff --git a/apps/status-page/src/components/status-page/status-feed.test.tsx b/apps/status-page/src/components/status-page/status-feed.test.tsx index a154a186f..66f915a8f 100644 --- a/apps/status-page/src/components/status-page/status-feed.test.tsx +++ b/apps/status-page/src/components/status-page/status-feed.test.tsx @@ -1,7 +1,6 @@ -import { expect } from "@std/expect"; -import { afterEach, describe, it } from "@std/testing/bdd"; import { NuqsTestingAdapter } from "nuqs/adapters/testing"; import { renderToStaticMarkup } from "react-dom/server"; +import { afterEach, describe, expect, it } from "vitest"; import { pathnamePrefix } from "@/test/use-pathname-prefix.mock"; diff --git a/apps/status-page/src/components/status-page/utils.test.ts b/apps/status-page/src/components/status-page/utils.test.ts index ea5fa252d..1e776f375 100644 --- a/apps/status-page/src/components/status-page/utils.test.ts +++ b/apps/status-page/src/components/status-page/utils.test.ts @@ -1,5 +1,4 @@ -import { expect } from "@std/expect"; -import { describe, it } from "@std/testing/bdd"; +import { describe, expect, it } from "vitest"; import { type ChartData, diff --git a/apps/status-page/src/components/themes/theme-builder-provider.test.tsx b/apps/status-page/src/components/themes/theme-builder-provider.test.tsx index ebb5dfd6b..458120330 100644 --- a/apps/status-page/src/components/themes/theme-builder-provider.test.tsx +++ b/apps/status-page/src/components/themes/theme-builder-provider.test.tsx @@ -1,8 +1,7 @@ import { THEMES } from "@openstatus/theme-store"; -import { expect } from "@std/expect"; -import { describe, it } from "@std/testing/bdd"; import { NuqsTestingAdapter } from "nuqs/adapters/testing"; import { renderToStaticMarkup } from "react-dom/server"; +import { describe, expect, it } from "vitest"; import { ThemePromptButton } from "./theme-agent-actions"; import { diff --git a/apps/status-page/src/components/ui/data-table/data-table-pagination.test.tsx b/apps/status-page/src/components/ui/data-table/data-table-pagination.test.tsx index 3c227689e..427d24dd8 100644 --- a/apps/status-page/src/components/ui/data-table/data-table-pagination.test.tsx +++ b/apps/status-page/src/components/ui/data-table/data-table-pagination.test.tsx @@ -1,5 +1,3 @@ -import { expect } from "@std/expect"; -import { describe, it } from "@std/testing/bdd"; import { type ColumnDef, type SortingState, @@ -9,6 +7,7 @@ import { useReactTable, } from "@tanstack/react-table"; import { renderToStaticMarkup } from "react-dom/server"; +import { describe, expect, it } from "vitest"; import { DataTableColumnHeader } from "./data-table-column-header"; import { diff --git a/apps/status-page/src/content/markdown/generators.test.ts b/apps/status-page/src/content/markdown/generators.test.ts index bef405019..e15c546d0 100644 --- a/apps/status-page/src/content/markdown/generators.test.ts +++ b/apps/status-page/src/content/markdown/generators.test.ts @@ -1,5 +1,4 @@ -import { expect } from "@std/expect"; -import { describe, test } from "@std/testing/bdd"; +import { describe, expect, test } from "vitest"; import { escapeCell, diff --git a/apps/status-page/src/content/markdown/helpers.test.ts b/apps/status-page/src/content/markdown/helpers.test.ts index 9b5495769..b27be5785 100644 --- a/apps/status-page/src/content/markdown/helpers.test.ts +++ b/apps/status-page/src/content/markdown/helpers.test.ts @@ -1,5 +1,4 @@ -import { expect } from "@std/expect"; -import { describe, test } from "@std/testing/bdd"; +import { describe, expect, test } from "vitest"; import { canonicalUrl, diff --git a/apps/status-page/src/content/markdown/match-route.test.ts b/apps/status-page/src/content/markdown/match-route.test.ts index 4a4175fd9..06bf19942 100644 --- a/apps/status-page/src/content/markdown/match-route.test.ts +++ b/apps/status-page/src/content/markdown/match-route.test.ts @@ -1,5 +1,4 @@ -import { expect } from "@std/expect"; -import { describe, test } from "@std/testing/bdd"; +import { describe, expect, test } from "vitest"; import { matchMarkdownRoute, parseMarkdownPath } from "./match-route"; diff --git a/apps/status-page/src/content/status-json.test.ts b/apps/status-page/src/content/status-json.test.ts index 37931027d..6a49c74d2 100644 --- a/apps/status-page/src/content/status-json.test.ts +++ b/apps/status-page/src/content/status-json.test.ts @@ -1,6 +1,5 @@ import type { RouterOutputs } from "@openstatus/api"; -import { expect } from "@std/expect"; -import { describe, test } from "@std/testing/bdd"; +import { describe, expect, test } from "vitest"; import { matchEndpoint, diff --git a/apps/status-page/src/content/status-vocab.test.ts b/apps/status-page/src/content/status-vocab.test.ts index da2f5bb64..9cea57f4e 100644 --- a/apps/status-page/src/content/status-vocab.test.ts +++ b/apps/status-page/src/content/status-vocab.test.ts @@ -1,5 +1,4 @@ -import { expect } from "@std/expect"; -import { describe, test } from "@std/testing/bdd"; +import { describe, expect, test } from "vitest"; import { componentStatus, diff --git a/apps/status-page/src/data/actions.client.test.ts b/apps/status-page/src/data/actions.client.test.ts index 5a04c56a8..34e3d23c4 100644 --- a/apps/status-page/src/data/actions.client.test.ts +++ b/apps/status-page/src/data/actions.client.test.ts @@ -1,5 +1,4 @@ -import { expect } from "@std/expect"; -import { describe, it } from "@std/testing/bdd"; +import { describe, expect, it } from "vitest"; import * as incidents from "./incidents.client"; import * as maintenances from "./maintenances.client"; diff --git a/apps/status-page/src/data/metrics.client.test.ts b/apps/status-page/src/data/metrics.client.test.ts index b94027964..03d5aafa1 100644 --- a/apps/status-page/src/data/metrics.client.test.ts +++ b/apps/status-page/src/data/metrics.client.test.ts @@ -1,6 +1,5 @@ import type { RouterOutputs } from "@openstatus/api"; -import { expect } from "@std/expect"; -import { describe, test } from "@std/testing/bdd"; +import { describe, expect, test } from "vitest"; import { getMonitorListMetrics, diff --git a/apps/status-page/src/i18n/messages.test.ts b/apps/status-page/src/i18n/messages.test.ts index eea5fb455..825f6748b 100644 --- a/apps/status-page/src/i18n/messages.test.ts +++ b/apps/status-page/src/i18n/messages.test.ts @@ -2,8 +2,7 @@ import { Buffer } from "node:buffer"; import { readFile } from "node:fs/promises"; import { defaultLocale, locales } from "@openstatus/locales"; -import { expect } from "@std/expect"; -import { describe, test } from "@std/testing/bdd"; +import { describe, expect, test } from "vitest"; // next-intl's extractor derives ids from the source text; catalogs drift // silently when that format changes (4.14 moved base64 -> base64url). diff --git a/apps/status-page/src/lib/alternates.test.ts b/apps/status-page/src/lib/alternates.test.ts index 320be3b7d..53091e3bd 100644 --- a/apps/status-page/src/lib/alternates.test.ts +++ b/apps/status-page/src/lib/alternates.test.ts @@ -1,5 +1,4 @@ -import { expect } from "@std/expect"; -import { describe, test } from "@std/testing/bdd"; +import { describe, expect, test } from "vitest"; import { statusPageAlternates } from "./alternates"; diff --git a/apps/status-page/src/lib/base-url.test.ts b/apps/status-page/src/lib/base-url.test.ts index 157d28dd6..8ca787862 100644 --- a/apps/status-page/src/lib/base-url.test.ts +++ b/apps/status-page/src/lib/base-url.test.ts @@ -1,5 +1,4 @@ -import { expect } from "@std/expect"; -import { afterEach, beforeEach, describe, it } from "@std/testing/bdd"; +import { afterEach, beforeEach, describe, expect, it } from "vitest"; import { getBaseUrl } from "./base-url"; diff --git a/apps/status-page/src/lib/chart.test.ts b/apps/status-page/src/lib/chart.test.ts index 28ec02780..1d7366979 100644 --- a/apps/status-page/src/lib/chart.test.ts +++ b/apps/status-page/src/lib/chart.test.ts @@ -1,5 +1,4 @@ -import { expect } from "@std/expect"; -import { describe, test } from "@std/testing/bdd"; +import { describe, expect, test } from "vitest"; import { getPayloadConfigFromPayload } from "./chart"; diff --git a/apps/status-page/src/lib/composition.test.ts b/apps/status-page/src/lib/composition.test.ts index a65777454..f4bfea304 100644 --- a/apps/status-page/src/lib/composition.test.ts +++ b/apps/status-page/src/lib/composition.test.ts @@ -1,5 +1,4 @@ -import { expect } from "@std/expect"; -import { describe, test } from "@std/testing/bdd"; +import { describe, expect, test } from "vitest"; import { composeEventHandlers, composeRefs } from "./composition"; diff --git a/apps/status-page/src/lib/domain.test.ts b/apps/status-page/src/lib/domain.test.ts index 7483aca24..15f66518d 100644 --- a/apps/status-page/src/lib/domain.test.ts +++ b/apps/status-page/src/lib/domain.test.ts @@ -1,5 +1,4 @@ -import { expect } from "@std/expect"; -import { describe, test } from "@std/testing/bdd"; +import { describe, expect, test } from "vitest"; import { getValidSubdomain, stripHostPort } from "./domain"; diff --git a/apps/status-page/src/lib/embed-params.test.ts b/apps/status-page/src/lib/embed-params.test.ts index 560a9ee18..ab109476f 100644 --- a/apps/status-page/src/lib/embed-params.test.ts +++ b/apps/status-page/src/lib/embed-params.test.ts @@ -1,5 +1,4 @@ -import { expect } from "@std/expect"; -import { describe, test } from "@std/testing/bdd"; +import { describe, expect, test } from "vitest"; import { ALL_EMBED_SECTIONS, diff --git a/apps/status-page/src/lib/formatter.test.ts b/apps/status-page/src/lib/formatter.test.ts index 1e832ebc6..9ca20f914 100644 --- a/apps/status-page/src/lib/formatter.test.ts +++ b/apps/status-page/src/lib/formatter.test.ts @@ -1,5 +1,4 @@ -import { expect } from "@std/expect"; -import { describe, test } from "@std/testing/bdd"; +import { describe, expect, test } from "vitest"; import { formatDate, diff --git a/apps/status-page/src/lib/http/etag.test.ts b/apps/status-page/src/lib/http/etag.test.ts index 66d584e39..78fad545b 100644 --- a/apps/status-page/src/lib/http/etag.test.ts +++ b/apps/status-page/src/lib/http/etag.test.ts @@ -1,5 +1,4 @@ -import { expect } from "@std/expect"; -import { describe, test } from "@std/testing/bdd"; +import { describe, expect, test } from "vitest"; import { computeETag, isNotModified } from "./etag"; diff --git a/apps/status-page/src/lib/http/markdown-response.test.ts b/apps/status-page/src/lib/http/markdown-response.test.ts index 00976f941..3749c6b5a 100644 --- a/apps/status-page/src/lib/http/markdown-response.test.ts +++ b/apps/status-page/src/lib/http/markdown-response.test.ts @@ -1,5 +1,4 @@ -import { expect } from "@std/expect"; -import { describe, test } from "@std/testing/bdd"; +import { describe, expect, test } from "vitest"; import { withPoweredBy } from "../../content/markdown"; import { computeETag } from "./etag"; diff --git a/apps/status-page/src/lib/proxy/apply-page-locale-override.test.ts b/apps/status-page/src/lib/proxy/apply-page-locale-override.test.ts index c04f0fe62..bc346bd27 100644 --- a/apps/status-page/src/lib/proxy/apply-page-locale-override.test.ts +++ b/apps/status-page/src/lib/proxy/apply-page-locale-override.test.ts @@ -1,5 +1,4 @@ -import { expect } from "@std/expect"; -import { describe, test } from "@std/testing/bdd"; +import { describe, expect, test } from "vitest"; import type { ResolvedRoute } from "../resolve-route"; import { applyPageLocaleOverride } from "./apply-page-locale-override"; diff --git a/apps/status-page/src/lib/proxy/apply-page-slug-prefix.test.ts b/apps/status-page/src/lib/proxy/apply-page-slug-prefix.test.ts index f4083a0de..e8dd10220 100644 --- a/apps/status-page/src/lib/proxy/apply-page-slug-prefix.test.ts +++ b/apps/status-page/src/lib/proxy/apply-page-slug-prefix.test.ts @@ -1,5 +1,4 @@ -import { expect } from "@std/expect"; -import { describe, test } from "@std/testing/bdd"; +import { describe, expect, test } from "vitest"; import type { ResolvedRoute } from "../resolve-route"; import { applyPageLocaleOverride } from "./apply-page-locale-override"; diff --git a/apps/status-page/src/lib/proxy/build-external-path.test.ts b/apps/status-page/src/lib/proxy/build-external-path.test.ts index df5444bb4..39ff5e891 100644 --- a/apps/status-page/src/lib/proxy/build-external-path.test.ts +++ b/apps/status-page/src/lib/proxy/build-external-path.test.ts @@ -1,5 +1,4 @@ -import { expect } from "@std/expect"; -import { describe, test } from "@std/testing/bdd"; +import { describe, expect, test } from "vitest"; import { buildExternalPath } from "./build-external-path"; diff --git a/apps/status-page/src/lib/proxy/compose-page-action.test.ts b/apps/status-page/src/lib/proxy/compose-page-action.test.ts index 8b6d4e678..6a90a6572 100644 --- a/apps/status-page/src/lib/proxy/compose-page-action.test.ts +++ b/apps/status-page/src/lib/proxy/compose-page-action.test.ts @@ -1,6 +1,5 @@ import type { Page } from "@openstatus/db/src/schema"; -import { expect } from "@std/expect"; -import { describe, test } from "@std/testing/bdd"; +import { describe, expect, test } from "vitest"; import type { ResolvedRoute } from "../resolve-route"; import { type ComposeInput, composePageAction } from "./compose-page-action"; diff --git a/apps/status-page/src/lib/proxy/detect-markdown.test.ts b/apps/status-page/src/lib/proxy/detect-markdown.test.ts index 75d0c6135..8b4d134a8 100644 --- a/apps/status-page/src/lib/proxy/detect-markdown.test.ts +++ b/apps/status-page/src/lib/proxy/detect-markdown.test.ts @@ -1,5 +1,4 @@ -import { expect } from "@std/expect"; -import { describe, test } from "@std/testing/bdd"; +import { describe, expect, test } from "vitest"; import { detectMarkdown } from "./detect-markdown"; diff --git a/apps/status-page/src/lib/proxy/markdown-cache-control.test.ts b/apps/status-page/src/lib/proxy/markdown-cache-control.test.ts index 0f8cd54f2..321bdc7ee 100644 --- a/apps/status-page/src/lib/proxy/markdown-cache-control.test.ts +++ b/apps/status-page/src/lib/proxy/markdown-cache-control.test.ts @@ -1,5 +1,4 @@ -import { expect } from "@std/expect"; -import { describe, test } from "@std/testing/bdd"; +import { describe, expect, test } from "vitest"; import { markdownCacheControl } from "./markdown-cache-control"; diff --git a/apps/status-page/src/lib/proxy/proxy-chain.test.ts b/apps/status-page/src/lib/proxy/proxy-chain.test.ts index 8090223c1..c1e826cca 100644 --- a/apps/status-page/src/lib/proxy/proxy-chain.test.ts +++ b/apps/status-page/src/lib/proxy/proxy-chain.test.ts @@ -1,6 +1,5 @@ import type { Page } from "@openstatus/db/src/schema"; -import { expect } from "@std/expect"; -import { describe, test } from "@std/testing/bdd"; +import { describe, expect, test } from "vitest"; import { resolveRoute } from "../resolve-route"; import { applyPageLocaleOverride } from "./apply-page-locale-override"; diff --git a/apps/status-page/src/lib/proxy/resolve-default-rewrite.test.ts b/apps/status-page/src/lib/proxy/resolve-default-rewrite.test.ts index 5aff1bca9..d234e1e55 100644 --- a/apps/status-page/src/lib/proxy/resolve-default-rewrite.test.ts +++ b/apps/status-page/src/lib/proxy/resolve-default-rewrite.test.ts @@ -1,5 +1,4 @@ -import { expect } from "@std/expect"; -import { describe, test } from "@std/testing/bdd"; +import { describe, expect, test } from "vitest"; import type { ResolvedRoute } from "../resolve-route"; import { resolveDefaultRewrite } from "./resolve-default-rewrite"; diff --git a/apps/status-page/src/lib/proxy/resolve-email-domain-action.test.ts b/apps/status-page/src/lib/proxy/resolve-email-domain-action.test.ts index 0a47a7efe..7f068b5d5 100644 --- a/apps/status-page/src/lib/proxy/resolve-email-domain-action.test.ts +++ b/apps/status-page/src/lib/proxy/resolve-email-domain-action.test.ts @@ -1,5 +1,4 @@ -import { expect } from "@std/expect"; -import { describe, test } from "@std/testing/bdd"; +import { describe, expect, test } from "vitest"; import type { ResolvedRoute } from "../resolve-route"; import { resolveEmailDomainAction } from "./resolve-email-domain-action"; diff --git a/apps/status-page/src/lib/proxy/resolve-ip-restriction-action.test.ts b/apps/status-page/src/lib/proxy/resolve-ip-restriction-action.test.ts index 58987fb9f..d22660c42 100644 --- a/apps/status-page/src/lib/proxy/resolve-ip-restriction-action.test.ts +++ b/apps/status-page/src/lib/proxy/resolve-ip-restriction-action.test.ts @@ -1,5 +1,4 @@ -import { expect } from "@std/expect"; -import { describe, test } from "@std/testing/bdd"; +import { describe, expect, test } from "vitest"; import type { ResolvedRoute } from "../resolve-route"; import { resolveIpRestrictionAction } from "./resolve-ip-restriction-action"; diff --git a/apps/status-page/src/lib/proxy/resolve-locale-action.test.ts b/apps/status-page/src/lib/proxy/resolve-locale-action.test.ts index ef78a82cc..4b98d550e 100644 --- a/apps/status-page/src/lib/proxy/resolve-locale-action.test.ts +++ b/apps/status-page/src/lib/proxy/resolve-locale-action.test.ts @@ -1,5 +1,4 @@ -import { expect } from "@std/expect"; -import { describe, test } from "@std/testing/bdd"; +import { describe, expect, test } from "vitest"; import type { ResolvedRoute } from "../resolve-route"; import { resolveLocaleAction } from "./resolve-locale-action"; diff --git a/apps/status-page/src/lib/proxy/resolve-password-action.test.ts b/apps/status-page/src/lib/proxy/resolve-password-action.test.ts index 382f8137b..44cc0d770 100644 --- a/apps/status-page/src/lib/proxy/resolve-password-action.test.ts +++ b/apps/status-page/src/lib/proxy/resolve-password-action.test.ts @@ -1,5 +1,4 @@ -import { expect } from "@std/expect"; -import { describe, test } from "@std/testing/bdd"; +import { describe, expect, test } from "vitest"; import type { ResolvedRoute } from "../resolve-route"; import { resolvePasswordAction } from "./resolve-password-action"; diff --git a/apps/status-page/src/lib/proxy/resolve-unresolved-host-action.test.ts b/apps/status-page/src/lib/proxy/resolve-unresolved-host-action.test.ts index 67d7a8b9e..4d267e54d 100644 --- a/apps/status-page/src/lib/proxy/resolve-unresolved-host-action.test.ts +++ b/apps/status-page/src/lib/proxy/resolve-unresolved-host-action.test.ts @@ -1,5 +1,4 @@ -import { expect } from "@std/expect"; -import { describe, test } from "@std/testing/bdd"; +import { describe, expect, test } from "vitest"; import { NOT_FOUND_PATH, diff --git a/apps/status-page/src/lib/proxy/sanitize-redirect-param.test.ts b/apps/status-page/src/lib/proxy/sanitize-redirect-param.test.ts index 5a3c69897..1c9842fee 100644 --- a/apps/status-page/src/lib/proxy/sanitize-redirect-param.test.ts +++ b/apps/status-page/src/lib/proxy/sanitize-redirect-param.test.ts @@ -1,5 +1,4 @@ -import { expect } from "@std/expect"; -import { describe, test } from "@std/testing/bdd"; +import { describe, expect, test } from "vitest"; import { sanitizeRedirectParam } from "./sanitize-redirect-param"; diff --git a/apps/status-page/src/lib/proxy/strip-prefix-for-external.test.ts b/apps/status-page/src/lib/proxy/strip-prefix-for-external.test.ts index 041ad0a4c..5ac5e12ca 100644 --- a/apps/status-page/src/lib/proxy/strip-prefix-for-external.test.ts +++ b/apps/status-page/src/lib/proxy/strip-prefix-for-external.test.ts @@ -1,5 +1,4 @@ -import { expect } from "@std/expect"; -import { describe, test } from "@std/testing/bdd"; +import { describe, expect, test } from "vitest"; import { stripPrefixForExternal } from "./strip-prefix-for-external"; diff --git a/apps/status-page/src/lib/report-impacts.test.ts b/apps/status-page/src/lib/report-impacts.test.ts index 12d736683..12886533f 100644 --- a/apps/status-page/src/lib/report-impacts.test.ts +++ b/apps/status-page/src/lib/report-impacts.test.ts @@ -1,5 +1,4 @@ -import { expect } from "@std/expect"; -import { describe, test } from "@std/testing/bdd"; +import { describe, expect, test } from "vitest"; import { updatesWithImpactChanges } from "./report-impacts"; diff --git a/apps/status-page/src/lib/resolve-pathname-prefix.test.ts b/apps/status-page/src/lib/resolve-pathname-prefix.test.ts index e76cf185f..4dba85fc3 100644 --- a/apps/status-page/src/lib/resolve-pathname-prefix.test.ts +++ b/apps/status-page/src/lib/resolve-pathname-prefix.test.ts @@ -1,5 +1,4 @@ -import { expect } from "@std/expect"; -import { describe, test } from "@std/testing/bdd"; +import { describe, expect, test } from "vitest"; import { resolvePathnamePrefix } from "./resolve-pathname-prefix"; diff --git a/apps/status-page/src/lib/resolve-route.test.ts b/apps/status-page/src/lib/resolve-route.test.ts index 78dea75e8..de92f9e2b 100644 --- a/apps/status-page/src/lib/resolve-route.test.ts +++ b/apps/status-page/src/lib/resolve-route.test.ts @@ -1,5 +1,4 @@ -import { expect } from "@std/expect"; -import { describe, test } from "@std/testing/bdd"; +import { describe, expect, test } from "vitest"; import { resolveRoute } from "./resolve-route"; diff --git a/apps/status-page/src/lib/status-blocks-labels.test.ts b/apps/status-page/src/lib/status-blocks-labels.test.ts index f6878d61d..67a53d76d 100644 --- a/apps/status-page/src/lib/status-blocks-labels.test.ts +++ b/apps/status-page/src/lib/status-blocks-labels.test.ts @@ -1,6 +1,5 @@ import { defaultStatusBlocksLabels as labels } from "@openstatus/ui/components/blocks/status.utils"; -import { expect } from "@std/expect"; -import { describe, test } from "@std/testing/bdd"; +import { describe, expect, test } from "vitest"; // Status-page timestamps render in UTC, so the user-facing labels carry a // trailing "(UTC)" to tell viewers which zone they are reading (see #2302). diff --git a/apps/status-page/src/lib/status-event-timeline.test.ts b/apps/status-page/src/lib/status-event-timeline.test.ts index 22e79667a..5a313fac3 100644 --- a/apps/status-page/src/lib/status-event-timeline.test.ts +++ b/apps/status-page/src/lib/status-event-timeline.test.ts @@ -1,8 +1,7 @@ import { StatusEventTimelineReport } from "@openstatus/ui/components/blocks/status-events"; -import { expect } from "@std/expect"; -import { describe, test } from "@std/testing/bdd"; import { createElement } from "react"; import { renderToStaticMarkup } from "react-dom/server"; +import { describe, expect, test } from "vitest"; describe("StatusEventTimelineReport", () => { const updates = [ diff --git a/apps/status-page/src/lib/theme-draft.test.ts b/apps/status-page/src/lib/theme-draft.test.ts index f9e37ab1e..36d4eb450 100644 --- a/apps/status-page/src/lib/theme-draft.test.ts +++ b/apps/status-page/src/lib/theme-draft.test.ts @@ -1,6 +1,5 @@ import { THEMES } from "@openstatus/theme-store"; -import { expect } from "@std/expect"; -import { afterEach, describe, it } from "@std/testing/bdd"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { THEME_DRAFT_STORAGE_KEY, @@ -9,7 +8,24 @@ import { saveThemeDraft, } from "./theme-draft"; -afterEach(() => sessionStorage.clear()); +// Node only exposes Web Storage by default from v25; a Map-backed stand-in +// keeps the suite independent of the runtime version. +function memoryStorage(): Storage { + const items = new Map(); + return { + get length() { + return items.size; + }, + clear: () => items.clear(), + getItem: (key) => items.get(key) ?? null, + key: (index) => [...items.keys()][index] ?? null, + removeItem: (key) => void items.delete(key), + setItem: (key, value) => void items.set(key, String(value)), + }; +} + +beforeEach(() => vi.stubGlobal("sessionStorage", memoryStorage())); +afterEach(() => vi.unstubAllGlobals()); const theme = Object.values(THEMES)[0]; diff --git a/apps/status-page/src/lib/theme-explorer-host.test.ts b/apps/status-page/src/lib/theme-explorer-host.test.ts index 02d6f5cff..d83335a24 100644 --- a/apps/status-page/src/lib/theme-explorer-host.test.ts +++ b/apps/status-page/src/lib/theme-explorer-host.test.ts @@ -1,5 +1,4 @@ -import { expect } from "@std/expect"; -import { describe, test } from "@std/testing/bdd"; +import { describe, expect, test } from "vitest"; import { isCanonicalThemeExplorerHost, diff --git a/apps/status-page/src/lib/theme-prompt.test.ts b/apps/status-page/src/lib/theme-prompt.test.ts index 014a891a9..8b198c11c 100644 --- a/apps/status-page/src/lib/theme-prompt.test.ts +++ b/apps/status-page/src/lib/theme-prompt.test.ts @@ -1,6 +1,5 @@ import { THEME_VAR_NAMES, THEMES } from "@openstatus/theme-store"; -import { expect } from "@std/expect"; -import { describe, test } from "@std/testing/bdd"; +import { describe, expect, test } from "vitest"; import { generateThemePrompt, diff --git a/apps/status-page/src/test/next-link.mock.tsx b/apps/status-page/src/test/next-link.mock.tsx index 03dcc1836..95afbc9b2 100644 --- a/apps/status-page/src/test/next-link.mock.tsx +++ b/apps/status-page/src/test/next-link.mock.tsx @@ -1,5 +1,5 @@ -// Test double for next/link, swapped in via --import-map: Deno cannot resolve -// Next's extensionless CJS entrypoints. Renders a plain anchor. +// Test double for next/link, swapped in via test.importmap.json. Renders a +// plain anchor. import type * as React from "react"; type LinkProps = Omit, "href"> & { diff --git a/apps/status-page/vitest.config.ts b/apps/status-page/vitest.config.ts new file mode 100644 index 000000000..ed88cbc5c --- /dev/null +++ b/apps/status-page/vitest.config.ts @@ -0,0 +1,5 @@ +import { defineTestConfig } from "@openstatus/test-utils/vitest"; + +export default defineTestConfig(import.meta.dirname, { + importMap: "test.importmap.json", +}); diff --git a/apps/web/package.json b/apps/web/package.json index 06c713201..8154683de 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -9,7 +9,7 @@ "build": "pnpm build:registry && next build", "start": "next start", "lint": "next lint", - "test": "NODE_ENV=test deno test --parallel -A --no-check --sloppy-imports", + "test": "NODE_ENV=test vitest run", "tsc": "tsc --noEmit" }, "dependencies": { @@ -96,9 +96,8 @@ "zod": "catalog:" }, "devDependencies": { + "@openstatus/test-utils": "workspace:*", "@openstatus/tsconfig": "workspace:*", - "@std/expect": "jsr:^1.0.19", - "@std/testing": "jsr:^1.0.19", "@tailwindcss/postcss": "catalog:", "@types/node": "catalog:", "@types/react": "catalog:", @@ -109,6 +108,7 @@ "rehype-slug": "catalog:", "tailwindcss": "catalog:", "typescript": "catalog:", - "unified": "catalog:" + "unified": "catalog:", + "vitest": "catalog:" } } diff --git a/apps/web/src/app/(ai)/mcp/route.test.ts b/apps/web/src/app/(ai)/mcp/route.test.ts index 38f323524..910b740fa 100644 --- a/apps/web/src/app/(ai)/mcp/route.test.ts +++ b/apps/web/src/app/(ai)/mcp/route.test.ts @@ -1,5 +1,4 @@ -import { expect } from "@std/expect"; -import { describe, test } from "@std/testing/bdd"; +import { describe, expect, test } from "vitest"; import { GET, MCP_TRANSPORT_URL, POST } from "./route"; diff --git a/apps/web/src/app/(landing)/play/checker/parse.test.ts b/apps/web/src/app/(landing)/play/checker/parse.test.ts index 3cd1edc04..2de000329 100644 --- a/apps/web/src/app/(landing)/play/checker/parse.test.ts +++ b/apps/web/src/app/(landing)/play/checker/parse.test.ts @@ -1,5 +1,4 @@ -import { expect } from "@std/expect"; -import { describe, test } from "@std/testing/bdd"; +import { describe, expect, test } from "vitest"; import { parseCheckerLine, splitStreamLines } from "./parse"; diff --git a/apps/web/src/app/(landing)/play/mcp-health/utils.test.ts b/apps/web/src/app/(landing)/play/mcp-health/utils.test.ts index ba080c634..8745b734c 100644 --- a/apps/web/src/app/(landing)/play/mcp-health/utils.test.ts +++ b/apps/web/src/app/(landing)/play/mcp-health/utils.test.ts @@ -1,5 +1,4 @@ -import { expect } from "@std/expect"; -import { describe, test } from "@std/testing/bdd"; +import { describe, expect, test } from "vitest"; import { formatResponseBody, diff --git a/apps/web/src/app/(landing)/status/incident-events.test.ts b/apps/web/src/app/(landing)/status/incident-events.test.ts index b2fcfc7e7..2dd91a1d8 100644 --- a/apps/web/src/app/(landing)/status/incident-events.test.ts +++ b/apps/web/src/app/(landing)/status/incident-events.test.ts @@ -1,5 +1,4 @@ -import { expect } from "@std/expect"; -import { describe, test } from "@std/testing/bdd"; +import { describe, expect, test } from "vitest"; import { type OverlayIncident, diff --git a/apps/web/src/app/(landing)/status/utils.test.ts b/apps/web/src/app/(landing)/status/utils.test.ts index 7339b146e..140810686 100644 --- a/apps/web/src/app/(landing)/status/utils.test.ts +++ b/apps/web/src/app/(landing)/status/utils.test.ts @@ -1,5 +1,4 @@ -import { expect } from "@std/expect"; -import { describe, test } from "@std/testing/bdd"; +import { describe, expect, test } from "vitest"; import { getComponentFullName } from "./utils"; diff --git a/apps/web/src/content/agent-recovery.test.ts b/apps/web/src/content/agent-recovery.test.ts index 603132800..8e7051b75 100644 --- a/apps/web/src/content/agent-recovery.test.ts +++ b/apps/web/src/content/agent-recovery.test.ts @@ -1,5 +1,4 @@ -import { expect } from "@std/expect"; -import { describe, test } from "@std/testing/bdd"; +import { describe, expect, test } from "vitest"; import { AGENT_RECOVERY_LINKS, notFoundMarkdown } from "./agent-recovery"; diff --git a/apps/web/src/content/convert.test.ts b/apps/web/src/content/convert.test.ts index 741e52a3d..20970f357 100644 --- a/apps/web/src/content/convert.test.ts +++ b/apps/web/src/content/convert.test.ts @@ -1,5 +1,4 @@ -import { expect } from "@std/expect"; -import { describe, test } from "@std/testing/bdd"; +import { describe, expect, test } from "vitest"; import { convertMdxToMarkdown } from "./convert"; import { getHomePage, getProductPages } from "./utils"; diff --git a/apps/web/src/content/mdx-components/content-lint.test.ts b/apps/web/src/content/mdx-components/content-lint.test.ts index 3be402978..3dcf15b1f 100644 --- a/apps/web/src/content/mdx-components/content-lint.test.ts +++ b/apps/web/src/content/mdx-components/content-lint.test.ts @@ -2,8 +2,7 @@ import { readFileSync, readdirSync } from "node:fs"; import { dirname, join } from "node:path"; import { fileURLToPath } from "node:url"; -import { expect } from "@std/expect"; -import { describe, test } from "@std/testing/bdd"; +import { describe, expect, test } from "vitest"; // Pages built from the section pattern (heading, text cell, `Demo` cell). Blog // posts and docs are prose and stay out of scope. diff --git a/apps/web/src/content/mdx-components/details.test.ts b/apps/web/src/content/mdx-components/details.test.ts index 6db076937..3b6156981 100644 --- a/apps/web/src/content/mdx-components/details.test.ts +++ b/apps/web/src/content/mdx-components/details.test.ts @@ -1,6 +1,5 @@ -import { expect } from "@std/expect"; -import { describe, test } from "@std/testing/bdd"; import { renderToStaticMarkup } from "react-dom/server"; +import { describe, expect, test } from "vitest"; import { Details } from "./details"; diff --git a/apps/web/src/content/utils/search-match.test.ts b/apps/web/src/content/utils/search-match.test.ts index 1642ae881..31f2d35cc 100644 --- a/apps/web/src/content/utils/search-match.test.ts +++ b/apps/web/src/content/utils/search-match.test.ts @@ -1,5 +1,4 @@ -import { expect } from "@std/expect"; -import { describe, test } from "@std/testing/bdd"; +import { describe, expect, test } from "vitest"; import { WEIGHTS, diff --git a/apps/web/src/lib/cdn-checker/probe.test.ts b/apps/web/src/lib/cdn-checker/probe.test.ts index 1871d007e..69bb95e2c 100644 --- a/apps/web/src/lib/cdn-checker/probe.test.ts +++ b/apps/web/src/lib/cdn-checker/probe.test.ts @@ -1,5 +1,4 @@ -import { expect } from "@std/expect"; -import { afterEach, describe, test } from "@std/testing/bdd"; +import { afterEach, describe, expect, test } from "vitest"; import type { RegionCheckerResponse } from "@/lib/checker/utils"; diff --git a/apps/web/src/lib/cdn-checker/schema.test.ts b/apps/web/src/lib/cdn-checker/schema.test.ts index ab8d72b7e..c7f55ba31 100644 --- a/apps/web/src/lib/cdn-checker/schema.test.ts +++ b/apps/web/src/lib/cdn-checker/schema.test.ts @@ -1,5 +1,4 @@ -import { expect } from "@std/expect"; -import { describe, test } from "@std/testing/bdd"; +import { describe, expect, test } from "vitest"; import { cdnRegionResponseSchema, cdnSummarySchema } from "./schema"; diff --git a/apps/web/src/lib/cdn-checker/summary.test.ts b/apps/web/src/lib/cdn-checker/summary.test.ts index 51401c76f..532ab53c1 100644 --- a/apps/web/src/lib/cdn-checker/summary.test.ts +++ b/apps/web/src/lib/cdn-checker/summary.test.ts @@ -1,5 +1,4 @@ -import { expect } from "@std/expect"; -import { describe, test } from "@std/testing/bdd"; +import { describe, expect, test } from "vitest"; import type { CdnRegionResponse, CdnRegionResult } from "./schema"; import { computeCdnSummary } from "./summary"; diff --git a/apps/web/src/lib/checker/utils.test.ts b/apps/web/src/lib/checker/utils.test.ts index 8cc03eb01..420b76030 100644 --- a/apps/web/src/lib/checker/utils.test.ts +++ b/apps/web/src/lib/checker/utils.test.ts @@ -1,5 +1,4 @@ -import { expect } from "@std/expect"; -import { describe, test } from "@std/testing/bdd"; +import { describe, expect, test } from "vitest"; import { type Timing, diff --git a/apps/web/src/lib/mcp/health-check.test.ts b/apps/web/src/lib/mcp/health-check.test.ts index 760265b64..108507bae 100644 --- a/apps/web/src/lib/mcp/health-check.test.ts +++ b/apps/web/src/lib/mcp/health-check.test.ts @@ -1,5 +1,4 @@ -import { expect } from "@std/expect"; -import { afterEach, describe, test } from "@std/testing/bdd"; +import { afterEach, describe, expect, test } from "vitest"; import { normalizeUrlForStorage, diff --git a/apps/web/src/lib/metadata/shared-metadata.test.ts b/apps/web/src/lib/metadata/shared-metadata.test.ts index 137956748..6e5573fe5 100644 --- a/apps/web/src/lib/metadata/shared-metadata.test.ts +++ b/apps/web/src/lib/metadata/shared-metadata.test.ts @@ -1,5 +1,4 @@ -import { expect } from "@std/expect"; -import { describe, test } from "@std/testing/bdd"; +import { describe, expect, test } from "vitest"; import type { MDXData } from "../../content/utils"; import { diff --git a/apps/web/src/lib/metadata/structured-data.test.ts b/apps/web/src/lib/metadata/structured-data.test.ts index 012a82d43..286fd7136 100644 --- a/apps/web/src/lib/metadata/structured-data.test.ts +++ b/apps/web/src/lib/metadata/structured-data.test.ts @@ -1,5 +1,4 @@ -import { expect } from "@std/expect"; -import { describe, test } from "@std/testing/bdd"; +import { describe, expect, test } from "vitest"; import { getJsonLDOrganization } from "./structured-data"; diff --git a/apps/web/src/lib/monitor/utils.test.ts b/apps/web/src/lib/monitor/utils.test.ts index 63f24b517..d8b3aa37c 100644 --- a/apps/web/src/lib/monitor/utils.test.ts +++ b/apps/web/src/lib/monitor/utils.test.ts @@ -1,6 +1,4 @@ -import { expect } from "@std/expect"; -import { describe, test } from "@std/testing/bdd"; -import { FakeTime } from "@std/testing/time"; +import { describe, expect, test, vi } from "vitest"; import { getDateByPeriod, @@ -31,11 +29,11 @@ describe("getDateByPeriod", () => { const now = new Date(2024, 5, 15, 13, 27, 45, 123); function at(fn: () => T) { - const time = new FakeTime(now); + vi.useFakeTimers({ now }); try { return fn(); } finally { - time.restore(); + vi.useRealTimers(); } } diff --git a/apps/web/src/lib/ratelimit-config.test.ts b/apps/web/src/lib/ratelimit-config.test.ts index d5ce55db4..d5fd3d13b 100644 --- a/apps/web/src/lib/ratelimit-config.test.ts +++ b/apps/web/src/lib/ratelimit-config.test.ts @@ -1,5 +1,4 @@ -import { expect } from "@std/expect"; -import { describe, test } from "@std/testing/bdd"; +import { describe, expect, test } from "vitest"; import { PLAY_RATE_LIMIT_TIERS, diff --git a/apps/web/src/lib/utils.test.ts b/apps/web/src/lib/utils.test.ts index afc5c5ba6..495a0a8f3 100644 --- a/apps/web/src/lib/utils.test.ts +++ b/apps/web/src/lib/utils.test.ts @@ -1,5 +1,4 @@ -import { expect } from "@std/expect"; -import { describe, test } from "@std/testing/bdd"; +import { describe, expect, test } from "vitest"; import { cn, @@ -97,7 +96,7 @@ describe("manipulateDate", () => { // midnight is detected by the timestamp ending in "00000", which also // matches any time on a 100-second boundary, e.g. 10:00:00 - test.ignore("keeps a whole-hour end date as is", () => { + test.skip("keeps a whole-hour end date as is", () => { const to = new Date(2024, 0, 2, 10); expect(manipulateDate({ from: undefined, to }).toDate).toBe(to.getTime()); }); @@ -138,7 +137,7 @@ describe("hashIP", () => { ); // the `?? ""` fallback feeds a zero-length key to importKey, which throws - test.ignore( + test.skip( "hashes without a configured secret", withSalt(undefined, async () => { const prev = process.env.CRON_SECRET; diff --git a/apps/web/vitest.config.ts b/apps/web/vitest.config.ts new file mode 100644 index 000000000..26bdb37da --- /dev/null +++ b/apps/web/vitest.config.ts @@ -0,0 +1,3 @@ +import { defineTestConfig } from "@openstatus/test-utils/vitest"; + +export default defineTestConfig(import.meta.dirname); diff --git a/apps/workflows/AGENTS.md b/apps/workflows/AGENTS.md index 84da5983f..5789fdc4d 100644 --- a/apps/workflows/AGENTS.md +++ b/apps/workflows/AGENTS.md @@ -2,7 +2,7 @@ Runs on **Node**, through oxc-node in dev and as a tsdown bundle in production. `pnpm check` here is `tsc --noEmit`, and it must pass — it is the only type gate -this app has (`pnpm test` runs with `--no-check`). +this app has (Vitest strips types without checking them). Constraints that follow from the build: @@ -11,8 +11,8 @@ Constraints that follow from the build: side-car `node_modules` in the image. A new dependency with either trait joins that list and the `externals` in `apps/workflows/scripts/sidecar-package.mjs`, which writes the side-car's pinned `package.json`. -- Tests run `deno test --parallel`, so test files share one process - environment. Never drive a branch by assigning to `process.env` mid-test — - the assignment leaks into whatever else is running. Resolve config once at - module scope and pass it in, so "credential missing" is a value a test hands - you rather than a global it mutates. +- Tests run on Vitest: each test file gets its own process, but the tests in a + file share `process.env`. Never drive a branch by assigning to `process.env` + mid-test — the assignment leaks into every later test in the file. Resolve + config once at module scope and pass it in, so "credential missing" is a + value a test hands you rather than a global it mutates. diff --git a/apps/workflows/package.json b/apps/workflows/package.json index 2c52a4e10..4aebcccf3 100644 --- a/apps/workflows/package.json +++ b/apps/workflows/package.json @@ -4,7 +4,7 @@ "check": "tsc --noEmit", "dev": "NODE_ENV=development node --import @oxc-node/core/register --watch src/serve.ts", "start": "NODE_ENV=production node dist/serve.mjs", - "test": "NODE_ENV=test RESEND_API_KEY=test-key CRON_SECRET=test-secret deno test --parallel -A --no-check --sloppy-imports", + "test": "NODE_ENV=test RESEND_API_KEY=test-key CRON_SECRET=test-secret vitest run", "build": "tsdown" }, "dependencies": { @@ -60,6 +60,7 @@ "@oxc-node/core": "catalog:", "@types/node": "catalog:", "tsdown": "catalog:", - "typescript": "catalog:" + "typescript": "catalog:", + "vitest": "catalog:" } } diff --git a/apps/workflows/src/checker/alerting.test.ts b/apps/workflows/src/checker/alerting.test.ts index d07bb5e55..c6a735f9f 100644 --- a/apps/workflows/src/checker/alerting.test.ts +++ b/apps/workflows/src/checker/alerting.test.ts @@ -13,32 +13,32 @@ import { import { afterAll, afterEach, - assertSpyCalls, beforeAll, beforeEach, describe, expect, - type Stub, - stub, test, -} from "@openstatus/test-utils"; + vi, + type MockInstance, +} from "vitest"; -import { checkerAudit } from "../utils/audit-log"; +import { stubPublishAuditLog } from "../test/stub-audit-log"; import { triggerNotifications } from "./alerting"; import { providerToFunction } from "./utils"; -// Deno has no module mocking, so we stub the methods on the real singletons -// that alerting.ts reads at call time. Stubbing per-test resolves them to no-op +// Stub the methods on the real singletons that alerting.ts reads at call time. Stubbing per-test resolves them to no-op // to avoid real HTTP / Tinybird calls and gives a fresh call count each test. // biome-ignore lint/suspicious/noExplicitAny: heterogeneous provider stubs -type AnyStub = Stub; +type AnyStub = MockInstance; let stubs: AnyStub[] = []; const stubSend = ( provider: "email" | "slack" | "discord", verb: "sendAlert" | "sendRecovery" | "sendDegraded", ): AnyStub => { - const s = stub(providerToFunction[provider], verb, () => Promise.resolve()); + const s = vi + .spyOn(providerToFunction[provider], verb) + .mockImplementation(() => Promise.resolve()); stubs.push(s); return s; }; @@ -64,9 +64,7 @@ let noNotifMonitorId: number; beforeEach(() => { stubs = []; // biome-ignore lint/suspicious/noExplicitAny: avoid Tinybird call - stubs.push( - stub(checkerAudit, "publishAuditLog", () => Promise.resolve()) as AnyStub, - ); + stubs.push(stubPublishAuditLog()); mockEmailSendAlert = stubSend("email", "sendAlert"); mockEmailSendRecovery = stubSend("email", "sendRecovery"); mockEmailSendDegraded = stubSend("email", "sendDegraded"); @@ -79,7 +77,7 @@ beforeEach(() => { }); afterEach(() => { - for (const s of stubs) s.restore(); + for (const s of stubs) s.mockRestore(); stubs = []; }); @@ -113,7 +111,7 @@ await describe("triggerNotifications", async () => { latency: 1500, }); - assertSpyCalls(mockEmailSendAlert, 1); + expect(mockEmailSendAlert).toHaveBeenCalledTimes(1); expect(result).toHaveLength(1); expect(result[0]).toEqual({ notificationId: emailNotificationId, @@ -132,7 +130,7 @@ await describe("triggerNotifications", async () => { regions: ["ams"], }); - assertSpyCalls(mockEmailSendRecovery, 1); + expect(mockEmailSendRecovery).toHaveBeenCalledTimes(1); expect(result).toHaveLength(1); expect(result[0]).toEqual({ notificationId: emailNotificationId, @@ -152,7 +150,7 @@ await describe("triggerNotifications", async () => { regions: ["ams"], }); - assertSpyCalls(mockEmailSendDegraded, 1); + expect(mockEmailSendDegraded).toHaveBeenCalledTimes(1); expect(result).toHaveLength(1); expect(result[0]).toEqual({ notificationId: emailNotificationId, @@ -171,7 +169,7 @@ await describe("triggerNotifications", async () => { cronTimestamp, }); - assertSpyCalls(mockEmailSendAlert, 0); + expect(mockEmailSendAlert).toHaveBeenCalledTimes(0); expect(result).toHaveLength(0); }); @@ -186,7 +184,7 @@ await describe("triggerNotifications", async () => { }); expect(first).toHaveLength(1); - assertSpyCalls(mockEmailSendAlert, 1); + expect(mockEmailSendAlert).toHaveBeenCalledTimes(1); // Same cronTimestamp should be skipped due to unique constraint const second = await triggerNotifications({ @@ -197,7 +195,7 @@ await describe("triggerNotifications", async () => { }); // still only the first call — the duplicate was skipped - assertSpyCalls(mockEmailSendAlert, 1); + expect(mockEmailSendAlert).toHaveBeenCalledTimes(1); expect(second).toHaveLength(0); }); }); @@ -278,9 +276,9 @@ describe("triggerNotifications with multiple providers", () => { regions: ["ams"], }); - assertSpyCalls(mockSlackSendAlert, 1); - assertSpyCalls(mockDiscordSendAlert, 1); - assertSpyCalls(mockEmailSendAlert, 0); + expect(mockSlackSendAlert).toHaveBeenCalledTimes(1); + expect(mockDiscordSendAlert).toHaveBeenCalledTimes(1); + expect(mockEmailSendAlert).toHaveBeenCalledTimes(0); expect(result).toHaveLength(2); expect(result).toContainEqual({ @@ -304,8 +302,8 @@ describe("triggerNotifications with multiple providers", () => { regions: ["ams"], }); - assertSpyCalls(mockSlackSendRecovery, 1); - assertSpyCalls(mockDiscordSendRecovery, 1); + expect(mockSlackSendRecovery).toHaveBeenCalledTimes(1); + expect(mockDiscordSendRecovery).toHaveBeenCalledTimes(1); expect(result).toHaveLength(2); const providers = result.map((r) => r.provider).sort(); diff --git a/apps/workflows/src/checker/outbox.test.ts b/apps/workflows/src/checker/outbox.test.ts index b9cce995d..f61c3f9d9 100644 --- a/apps/workflows/src/checker/outbox.test.ts +++ b/apps/workflows/src/checker/outbox.test.ts @@ -19,19 +19,19 @@ import { beforeEach, describe, expect, - type Stub, - stub, test, -} from "@openstatus/test-utils"; + vi, + type MockInstance, +} from "vitest"; -import { checkerAudit } from "../utils/audit-log"; +import { stubPublishAuditLog } from "../test/stub-audit-log"; import { drainOutbox, shutdownOutbox, sweepExpiredOutbox } from "./outbox"; import { providerToFunction } from "./utils"; -// Deno has no module mocking; stub the singleton the drainer resolves at call -// time, as alerting.test.ts does. +// Stub the singleton the drainer resolves at call time, as alerting.test.ts +// does. // biome-ignore lint/suspicious/noExplicitAny: heterogeneous provider stubs -type AnyStub = Stub; +type AnyStub = MockInstance; let stubs: AnyStub[] = []; let workspaceId: number; @@ -56,13 +56,11 @@ afterAll(async () => { beforeEach(() => { stubs = []; - stubs.push( - stub(checkerAudit, "publishAuditLog", () => Promise.resolve()) as AnyStub, - ); + stubs.push(stubPublishAuditLog()); }); afterEach(async () => { - for (const s of stubs) s.restore(); + for (const s of stubs) s.mockRestore(); stubs = []; await db .delete(notificationOutbox) @@ -119,7 +117,9 @@ async function insertOutboxRow(overrides: { describe("drainOutbox", () => { test("delivers a row, marks it done and records the send", async () => { stubs.push( - stub(providerToFunction.email, "sendAlert", () => Promise.resolve()), + vi + .spyOn(providerToFunction.email, "sendAlert") + .mockImplementation(() => Promise.resolve()), ); await insertOutboxRow({ cronTimestamp: Date.now() }); @@ -151,9 +151,9 @@ describe("drainOutbox", () => { test("dead-letters a row past its deadline and frees the channel", async () => { stubs.push( - stub(providerToFunction.email, "sendAlert", () => - Promise.reject(new Error("provider down")), - ), + vi + .spyOn(providerToFunction.email, "sendAlert") + .mockImplementation(() => Promise.reject(new Error("provider down"))), ); // `attempts` caps the backoff (>=15s), so the failed send's retry lands // past the deadline below without the claim racing a 1s deadline under @@ -190,11 +190,9 @@ describe("drainOutbox", () => { test("a hanging provider is bounded by the timeout", async () => { stubs.push( - stub( - providerToFunction.email, - "sendAlert", - () => new Promise(() => {}), - ), + vi + .spyOn(providerToFunction.email, "sendAlert") + .mockImplementation(() => new Promise(() => {})), ); await insertOutboxRow({ cronTimestamp: Date.now(), @@ -215,10 +213,14 @@ describe("drainOutbox", () => { test("claims only the oldest pending row per channel", async () => { stubs.push( - stub(providerToFunction.email, "sendAlert", () => Promise.resolve()), + vi + .spyOn(providerToFunction.email, "sendAlert") + .mockImplementation(() => Promise.resolve()), ); stubs.push( - stub(providerToFunction.email, "sendRecovery", () => Promise.resolve()), + vi + .spyOn(providerToFunction.email, "sendRecovery") + .mockImplementation(() => Promise.resolve()), ); const base = Date.now(); @@ -261,7 +263,9 @@ describe("drainOutbox", () => { test("an sms row over quota is recorded, not delivered", async () => { stubs.push( - stub(providerToFunction.sms, "sendAlert", () => Promise.resolve()), + vi + .spyOn(providerToFunction.sms, "sendAlert") + .mockImplementation(() => Promise.resolve()), ); const { workspace: smsWorkspace } = await createTestWorkspace({ @@ -329,9 +333,9 @@ describe("drainOutbox", () => { describe("retry", () => { test("a failed send is handed back with its backoff recorded", async () => { stubs.push( - stub(providerToFunction.email, "sendAlert", () => - Promise.reject(new Error("provider down")), - ), + vi + .spyOn(providerToFunction.email, "sendAlert") + .mockImplementation(() => Promise.reject(new Error("provider down"))), ); await insertOutboxRow({ cronTimestamp: Date.now() }); @@ -369,7 +373,9 @@ describe("retry", () => { test("a lapsed lease lets the other machine take the row over", async () => { stubs.push( - stub(providerToFunction.email, "sendAlert", () => Promise.resolve()), + vi + .spyOn(providerToFunction.email, "sendAlert") + .mockImplementation(() => Promise.resolve()), ); // A worker that died mid-attempt: still claimed, lease already expired. await insertOutboxRow({ @@ -398,7 +404,9 @@ describe("retry", () => { test("a live lease is left alone", async () => { stubs.push( - stub(providerToFunction.email, "sendAlert", () => Promise.resolve()), + vi + .spyOn(providerToFunction.email, "sendAlert") + .mockImplementation(() => Promise.resolve()), ); await insertOutboxRow({ cronTimestamp: Date.now(), @@ -433,7 +441,7 @@ describe("shutdownOutbox", () => { release = resolve; }); stubs.push( - stub(providerToFunction.email, "sendAlert", () => { + vi.spyOn(providerToFunction.email, "sendAlert").mockImplementation(() => { dispatched = true; return pending; }), @@ -476,7 +484,9 @@ describe("shutdownOutbox", () => { test("hands claimed work back instead of delivering it", async () => { stubs.push( - stub(providerToFunction.email, "sendAlert", () => Promise.resolve()), + vi + .spyOn(providerToFunction.email, "sendAlert") + .mockImplementation(() => Promise.resolve()), ); await insertOutboxRow({ cronTimestamp: Date.now() }); diff --git a/apps/workflows/src/checker/private-location.test.ts b/apps/workflows/src/checker/private-location.test.ts index 8da20e5fb..cd9efd607 100644 --- a/apps/workflows/src/checker/private-location.test.ts +++ b/apps/workflows/src/checker/private-location.test.ts @@ -12,23 +12,22 @@ import { createTestWorkspace } from "@openstatus/db/src/test/factories"; import { afterAll, afterEach, - assertSpyCalls, beforeAll, beforeEach, describe, expect, - type Stub, - stub, test, -} from "@openstatus/test-utils"; + vi, + type MockInstance, +} from "vitest"; import { env } from "../env"; -import { checkerAudit } from "../utils/audit-log"; +import { stubPublishAuditLog } from "../test/stub-audit-log"; import { checkerRoute } from "./index"; import { providerToFunction } from "./utils"; // biome-ignore lint/suspicious/noExplicitAny: heterogeneous provider stubs -type AnyStub = Stub; +type AnyStub = MockInstance; // Dedicated fixtures, not seed monitor 1: other suites (api/server maintenance // tests) put seed monitor 1 under active maintenance on the shared CI database, @@ -292,19 +291,19 @@ describe("updateStatusPrivate", () => { beforeEach(() => { stubs = []; stubs.push( - stub(checkerAudit, "publishAuditLog", () => + stubPublishAuditLog(() => Promise.resolve({ successful_rows: 1, quarantined_rows: 0 }), - ) as AnyStub, + ), ); - mockEmailSendAlert = stub(providerToFunction.email, "sendAlert", () => - Promise.resolve(), - ) as AnyStub; - mockEmailSendRecovery = stub(providerToFunction.email, "sendRecovery", () => - Promise.resolve(), - ) as AnyStub; - mockEmailSendDegraded = stub(providerToFunction.email, "sendDegraded", () => - Promise.resolve(), - ) as AnyStub; + mockEmailSendAlert = vi + .spyOn(providerToFunction.email, "sendAlert") + .mockImplementation(() => Promise.resolve()) as AnyStub; + mockEmailSendRecovery = vi + .spyOn(providerToFunction.email, "sendRecovery") + .mockImplementation(() => Promise.resolve()) as AnyStub; + mockEmailSendDegraded = vi + .spyOn(providerToFunction.email, "sendDegraded") + .mockImplementation(() => Promise.resolve()) as AnyStub; stubs.push( mockEmailSendAlert, mockEmailSendRecovery, @@ -313,7 +312,7 @@ describe("updateStatusPrivate", () => { }); afterEach(async () => { - for (const s of stubs) s.restore(); + for (const s of stubs) s.mockRestore(); stubs = []; await db .delete(privateLocationMonitorStatus) @@ -346,7 +345,7 @@ describe("updateStatusPrivate", () => { "Basic wrong-secret", ); expect(res.status).toBe(401); - assertSpyCalls(mockEmailSendAlert, 0); + expect(mockEmailSendAlert).toHaveBeenCalledTimes(0); }); test("rejects an invalid payload with 422", async () => { @@ -371,7 +370,7 @@ describe("updateStatusPrivate", () => { message: "down", }); expect(res.status).toBe(200); - assertSpyCalls(mockEmailSendAlert, 1); + expect(mockEmailSendAlert).toHaveBeenCalledTimes(1); const row = await readRow(TEST_MONITOR_ID, TEST_LOCATION_ID); expect(row?.status).toBe("error"); @@ -385,7 +384,7 @@ describe("updateStatusPrivate", () => { status: "error", cronTimestamp: 9300020, }); - assertSpyCalls(mockEmailSendAlert, 1); + expect(mockEmailSendAlert).toHaveBeenCalledTimes(1); const res = await post({ monitorId: String(TEST_MONITOR_ID), @@ -394,7 +393,7 @@ describe("updateStatusPrivate", () => { cronTimestamp: 9300021, }); expect(res.status).toBe(200); - assertSpyCalls(mockEmailSendAlert, 1); + expect(mockEmailSendAlert).toHaveBeenCalledTimes(1); const row = await readRow(TEST_MONITOR_ID, TEST_LOCATION_ID); expect(row?.cronTimestamp).toBe(9300021); @@ -407,7 +406,7 @@ describe("updateStatusPrivate", () => { status: "error", cronTimestamp: 9300030, }); - assertSpyCalls(mockEmailSendAlert, 1); + expect(mockEmailSendAlert).toHaveBeenCalledTimes(1); const res = await post({ monitorId: String(TEST_MONITOR_ID), @@ -416,7 +415,7 @@ describe("updateStatusPrivate", () => { cronTimestamp: 9300031, }); expect(res.status).toBe(200); - assertSpyCalls(mockEmailSendRecovery, 1); + expect(mockEmailSendRecovery).toHaveBeenCalledTimes(1); const row = await readRow(TEST_MONITOR_ID, TEST_LOCATION_ID); expect(row?.status).toBe("active"); @@ -431,7 +430,7 @@ describe("updateStatusPrivate", () => { latency: 5000, }); expect(res.status).toBe(200); - assertSpyCalls(mockEmailSendDegraded, 1); + expect(mockEmailSendDegraded).toHaveBeenCalledTimes(1); const row = await readRow(TEST_MONITOR_ID, TEST_LOCATION_ID); expect(row?.status).toBe("degraded"); @@ -452,7 +451,7 @@ describe("updateStatusPrivate", () => { cronTimestamp: 9300049, }); expect(res.status).toBe(200); - assertSpyCalls(mockEmailSendAlert, 0); + expect(mockEmailSendAlert).toHaveBeenCalledTimes(0); const row = await readRow(TEST_MONITOR_ID, TEST_LOCATION_ID); expect(row?.status).toBe("active"); @@ -467,7 +466,7 @@ describe("updateStatusPrivate", () => { cronTimestamp: 9300060, }); expect(res.status).toBe(200); - assertSpyCalls(mockEmailSendAlert, 0); + expect(mockEmailSendAlert).toHaveBeenCalledTimes(0); const row = await readRow(TEST_MONITOR_ID, UNATTACHED_LOCATION_ID); expect(row).toBeUndefined(); @@ -481,7 +480,7 @@ describe("updateStatusPrivate", () => { cronTimestamp: 9300070, }); expect(res.status).toBe(200); - assertSpyCalls(mockEmailSendAlert, 0); + expect(mockEmailSendAlert).toHaveBeenCalledTimes(0); }); test("private-only monitor: updates status when threshold met (2/3 locations agree)", async () => { @@ -520,7 +519,7 @@ describe("updateStatusPrivate", () => { .where(eq(monitor.id, PRIVATE_ONLY_MONITOR_ID)) .get(); expect(monitorAfter2?.status).toBe("error"); - assertSpyCalls(mockEmailSendAlert, 1); + expect(mockEmailSendAlert).toHaveBeenCalledTimes(1); }); test("does not change monitor.status from error when 1/3 locations report degraded (threshold not met)", async () => { diff --git a/apps/workflows/src/checker/transition.test.ts b/apps/workflows/src/checker/transition.test.ts index ec616a4fd..9b6459e5d 100644 --- a/apps/workflows/src/checker/transition.test.ts +++ b/apps/workflows/src/checker/transition.test.ts @@ -12,13 +12,7 @@ import { createTestWorkspace, linkNotificationToMonitor, } from "@openstatus/db/src/test/factories"; -import { - afterAll, - beforeAll, - describe, - expect, - test, -} from "@openstatus/test-utils"; +import { afterAll, beforeAll, describe, expect, test } from "vitest"; import { drainOutbox } from "./outbox"; import { applyStatusTransition, isStaleCheck } from "./transition"; diff --git a/apps/workflows/src/checker/update-status.test.ts b/apps/workflows/src/checker/update-status.test.ts index 9ab676ccc..c845a8dd3 100644 --- a/apps/workflows/src/checker/update-status.test.ts +++ b/apps/workflows/src/checker/update-status.test.ts @@ -14,23 +14,22 @@ import { import { afterAll, afterEach, - assertSpyCalls, beforeAll, beforeEach, describe, expect, - type Stub, - stub, test, -} from "@openstatus/test-utils"; + vi, + type MockInstance, +} from "vitest"; import { env } from "../env"; -import { checkerAudit } from "../utils/audit-log"; +import { stubPublishAuditLog } from "../test/stub-audit-log"; import { checkerRoute } from "./index"; import { providerToFunction } from "./utils"; // biome-ignore lint/suspicious/noExplicitAny: heterogeneous provider stubs -type AnyStub = Stub; +type AnyStub = MockInstance; let stubs: AnyStub[] = []; let workspaceId: number; @@ -61,7 +60,7 @@ beforeEach(() => { }); afterEach(async () => { - for (const s of stubs) s.restore(); + for (const s of stubs) s.mockRestore(); stubs = []; await db .delete(notificationTrigger) @@ -98,13 +97,13 @@ describe("updateStatus audit failures", () => { // Tinybird is down: every audit publish rejects, including the status one // that runs after the transition batch has committed. stubs.push( - stub(checkerAudit, "publishAuditLog", () => + stubPublishAuditLog(() => Promise.reject(new Error("tinybird unavailable")), - ) as AnyStub, - ); - const sendAlert = stub(providerToFunction.email, "sendAlert", () => - Promise.resolve(), + ), ); + const sendAlert = vi + .spyOn(providerToFunction.email, "sendAlert") + .mockImplementation(() => Promise.resolve()); stubs.push(sendAlert as AnyStub); const res = await post({ @@ -118,7 +117,7 @@ describe("updateStatus audit failures", () => { // Retrying this transition is useless — the region status is already // written, so the retry takes the fast path and sends nothing. expect(res.status).toBe(200); - assertSpyCalls(sendAlert, 1); + expect(sendAlert).toHaveBeenCalledTimes(1); const rows = await db .select() diff --git a/apps/workflows/src/cron/external-status-detect.test.ts b/apps/workflows/src/cron/external-status-detect.test.ts index bdb63cf00..00325a975 100644 --- a/apps/workflows/src/cron/external-status-detect.test.ts +++ b/apps/workflows/src/cron/external-status-detect.test.ts @@ -1,6 +1,6 @@ import { FetchError } from "@openstatus/status-fetcher"; import type { DetectionResult } from "@openstatus/status-fetcher"; -import { describe, expect, test } from "@openstatus/test-utils"; +import { describe, expect, test } from "vitest"; import { PROBE_TTL_MS, diff --git a/apps/workflows/src/cron/private-location-health.test.ts b/apps/workflows/src/cron/private-location-health.test.ts index d717b51b5..4c219e3d9 100644 --- a/apps/workflows/src/cron/private-location-health.test.ts +++ b/apps/workflows/src/cron/private-location-health.test.ts @@ -11,15 +11,14 @@ import { } from "@openstatus/db/src/test/factories"; import { afterEach, - assertSpyCalls, beforeAll, beforeEach, describe, expect, - type Stub, - stub, test, -} from "@openstatus/test-utils"; + vi, + type MockInstance, +} from "vitest"; import { emailClient, @@ -34,7 +33,7 @@ const FRESH = new Date(NOW.getTime() - 60 * 1000); const STALE = new Date(NOW.getTime() - STALE_THRESHOLD_MS - 60 * 1000); // biome-ignore lint/suspicious/noExplicitAny: email stub -type AnyStub = Stub; +type AnyStub = MockInstance; async function seedLocation( status: "active" | "error", @@ -73,13 +72,13 @@ describe("runPrivateLocationHealth", () => { }); beforeEach(() => { - mockEmail = stub(emailClient, "sendPrivateLocationAlert", () => - Promise.resolve(), - ) as AnyStub; + mockEmail = vi + .spyOn(emailClient, "sendPrivateLocationAlert") + .mockImplementation(() => Promise.resolve()) as AnyStub; }); afterEach(async () => { - mockEmail.restore(); + mockEmail.mockRestore(); await db .delete(auditLog) .where(eq(auditLog.entityId, String(TEST_LOCATION_ID))); @@ -94,12 +93,12 @@ describe("runPrivateLocationHealth", () => { await runPrivateLocationHealth(NOW); expect((await readStatus())?.status).toBe("error"); - assertSpyCalls(mockEmail, 1); - expect(mockEmail.calls[0].args[0].status).toBe("error"); - expect(mockEmail.calls[0].args[0].locationName).toBe( + expect(mockEmail).toHaveBeenCalledTimes(1); + expect(mockEmail.mock.calls[0][0].status).toBe("error"); + expect(mockEmail.mock.calls[0][0].locationName).toBe( "health-test-location", ); - expect(mockEmail.calls[0].args[0].monitorCount).toBe(0); + expect(mockEmail.mock.calls[0][0].monitorCount).toBe(0); }); test("passes the number of monitors scheduled on the location", async () => { @@ -122,7 +121,7 @@ describe("runPrivateLocationHealth", () => { try { await runPrivateLocationHealth(NOW); - expect(mockEmail.calls[0].args[0].monitorCount).toBe(1); + expect(mockEmail.mock.calls[0][0].monitorCount).toBe(1); } finally { await db.delete(monitor).where(eq(monitor.id, linked.id)); await db.delete(monitor).where(eq(monitor.id, unlinked.id)); @@ -136,8 +135,8 @@ describe("runPrivateLocationHealth", () => { await runPrivateLocationHealth(NOW); expect((await readStatus())?.status).toBe("active"); - assertSpyCalls(mockEmail, 1); - expect(mockEmail.calls[0].args[0].status).toBe("recovered"); + expect(mockEmail).toHaveBeenCalledTimes(1); + expect(mockEmail.mock.calls[0][0].status).toBe("recovered"); }); test("active + fresh → untouched, no email", async () => { @@ -146,7 +145,7 @@ describe("runPrivateLocationHealth", () => { await runPrivateLocationHealth(NOW); expect((await readStatus())?.status).toBe("active"); - assertSpyCalls(mockEmail, 0); + expect(mockEmail).toHaveBeenCalledTimes(0); }); test("error + stale → untouched, no email", async () => { @@ -155,7 +154,7 @@ describe("runPrivateLocationHealth", () => { await runPrivateLocationHealth(NOW); expect((await readStatus())?.status).toBe("error"); - assertSpyCalls(mockEmail, 0); + expect(mockEmail).toHaveBeenCalledTimes(0); }); test("lastSeenAt NULL → skipped regardless of status", async () => { @@ -164,6 +163,6 @@ describe("runPrivateLocationHealth", () => { await runPrivateLocationHealth(NOW); expect((await readStatus())?.status).toBe("error"); - assertSpyCalls(mockEmail, 0); + expect(mockEmail).toHaveBeenCalledTimes(0); }); }); diff --git a/apps/workflows/src/cron/scheduler.test.ts b/apps/workflows/src/cron/scheduler.test.ts index 517e7c367..356213103 100644 --- a/apps/workflows/src/cron/scheduler.test.ts +++ b/apps/workflows/src/cron/scheduler.test.ts @@ -1,5 +1,5 @@ -import { describe, expect, test } from "@openstatus/test-utils"; import { Cron, Result } from "effect"; +import { describe, expect, test } from "vitest"; import { SCHEDULED_TASKS } from "./scheduler"; diff --git a/apps/workflows/src/cron/status-drift.test.ts b/apps/workflows/src/cron/status-drift.test.ts index 4b72744c3..5cdea3f90 100644 --- a/apps/workflows/src/cron/status-drift.test.ts +++ b/apps/workflows/src/cron/status-drift.test.ts @@ -18,18 +18,18 @@ import { beforeEach, describe, expect, - type Stub, - stub, test, -} from "@openstatus/test-utils"; + vi, + type MockInstance, +} from "vitest"; import { applyStatusTransition } from "../checker/transition"; import { providerToFunction } from "../checker/utils"; -import { checkerAudit } from "../utils/audit-log"; +import { stubPublishAuditLog } from "../test/stub-audit-log"; import { handleStatusDriftCron } from "./status-drift"; // biome-ignore lint/suspicious/noExplicitAny: heterogeneous provider stubs -type AnyStub = Stub; +type AnyStub = MockInstance; let stubs: AnyStub[] = []; let workspaceId: number; @@ -40,16 +40,16 @@ beforeAll(async () => { beforeEach(() => { stubs = []; + stubs.push(stubPublishAuditLog()); stubs.push( - stub(checkerAudit, "publishAuditLog", () => Promise.resolve()) as AnyStub, - ); - stubs.push( - stub(providerToFunction.email, "sendAlert", () => Promise.resolve()), + vi + .spyOn(providerToFunction.email, "sendAlert") + .mockImplementation(() => Promise.resolve()), ); }); afterEach(() => { - for (const s of stubs) s.restore(); + for (const s of stubs) s.mockRestore(); stubs = []; }); @@ -157,10 +157,10 @@ describe("handleStatusDriftCron", () => { describe("drift repair delivery", () => { test("sends the notification the repair is recovering", async () => { const sent: string[] = []; - for (const s of stubs) s.restore(); + for (const s of stubs) s.mockRestore(); stubs = [ - stub(checkerAudit, "publishAuditLog", () => Promise.resolve()) as AnyStub, - stub(providerToFunction.email, "sendAlert", () => { + stubPublishAuditLog(), + vi.spyOn(providerToFunction.email, "sendAlert").mockImplementation(() => { sent.push("alert"); return Promise.resolve(); }), diff --git a/apps/workflows/src/test/stub-audit-log.ts b/apps/workflows/src/test/stub-audit-log.ts new file mode 100644 index 000000000..d986d1ce4 --- /dev/null +++ b/apps/workflows/src/test/stub-audit-log.ts @@ -0,0 +1,21 @@ +import { type MockInstance, vi } from "vitest"; + +import { checkerAudit } from "../utils/audit-log"; + +type Publish = typeof checkerAudit.publishAuditLog; + +/** + * `publishAuditLog` is a getter that builds the ingest function on each read, + * so it has to be spied as an accessor. vitest only types the `"get"` form for + * non-function properties; the key is widened here, safe because the getter + * really exists on the instance's prototype. + */ +export function stubPublishAuditLog( + impl: Publish = () => + Promise.resolve({ successful_rows: 0, quarantined_rows: 0 }), +): MockInstance { + const target = checkerAudit as unknown as { publishAuditLog: unknown }; + return vi + .spyOn(target, "publishAuditLog", "get") + .mockReturnValue(vi.fn(impl)); +} diff --git a/apps/workflows/vitest.config.ts b/apps/workflows/vitest.config.ts new file mode 100644 index 000000000..26bdb37da --- /dev/null +++ b/apps/workflows/vitest.config.ts @@ -0,0 +1,3 @@ +import { defineTestConfig } from "@openstatus/test-utils/vitest"; + +export default defineTestConfig(import.meta.dirname); diff --git a/packages/ai/package.json b/packages/ai/package.json index dea32ab20..9a7997ff2 100644 --- a/packages/ai/package.json +++ b/packages/ai/package.json @@ -13,7 +13,7 @@ }, "scripts": { "check": "tsc --noEmit", - "test": "deno test --parallel -A --no-check --sloppy-imports", + "test": "vitest run", "lint": "oxlint", "tsc": "tsc --noEmit" }, @@ -23,10 +23,10 @@ }, "devDependencies": { "@openstatus/db": "workspace:*", + "@openstatus/test-utils": "workspace:*", "@openstatus/tsconfig": "workspace:*", - "@std/expect": "jsr:^1.0.19", - "@std/testing": "jsr:^1.0.19", "@types/node": "catalog:", - "typescript": "catalog:" + "typescript": "catalog:", + "vitest": "catalog:" } } diff --git a/packages/ai/src/resolve-model.test.ts b/packages/ai/src/resolve-model.test.ts index c9d7c6921..be75961eb 100644 --- a/packages/ai/src/resolve-model.test.ts +++ b/packages/ai/src/resolve-model.test.ts @@ -1,5 +1,4 @@ -import { expect } from "@std/expect"; -import { afterEach, beforeEach, describe, test } from "@std/testing/bdd"; +import { afterEach, beforeEach, describe, expect, test } from "vitest"; import { resolveChatModel } from "./resolve-model"; diff --git a/packages/ai/vitest.config.ts b/packages/ai/vitest.config.ts new file mode 100644 index 000000000..26bdb37da --- /dev/null +++ b/packages/ai/vitest.config.ts @@ -0,0 +1,3 @@ +import { defineTestConfig } from "@openstatus/test-utils/vitest"; + +export default defineTestConfig(import.meta.dirname); diff --git a/packages/api/package.json b/packages/api/package.json index a94373caa..0d411e271 100644 --- a/packages/api/package.json +++ b/packages/api/package.json @@ -12,7 +12,7 @@ }, "scripts": { "check": "tsc --noEmit", - "test": "deno test --parallel -A --no-check --sloppy-imports --env-file=.env.test --import-map=test.importmap.json", + "test": "vitest run", "env": "node env.ts" }, "dependencies": { @@ -66,12 +66,12 @@ "zod": "catalog:" }, "devDependencies": { + "@openstatus/test-utils": "workspace:*", "@openstatus/tsconfig": "workspace:*", - "@std/expect": "jsr:^1.0.19", - "@std/testing": "jsr:^1.0.19", "@types/node": "catalog:", "@types/react": "catalog:", "@types/react-dom": "catalog:", - "typescript": "catalog:" + "typescript": "catalog:", + "vitest": "catalog:" } } diff --git a/packages/api/src/router/blob.test.ts b/packages/api/src/router/blob.test.ts index f8498a1e3..37ac7da88 100644 --- a/packages/api/src/router/blob.test.ts +++ b/packages/api/src/router/blob.test.ts @@ -1,5 +1,4 @@ -import { expect } from "@std/expect"; -import { describe, it } from "@std/testing/bdd"; +import { describe, expect, it } from "vitest"; import { isSvgFile, sanitizeSvg } from "./blob"; diff --git a/packages/api/src/router/checker.test.ts b/packages/api/src/router/checker.test.ts index 4ab6e3afa..c90429215 100644 --- a/packages/api/src/router/checker.test.ts +++ b/packages/api/src/router/checker.test.ts @@ -1,5 +1,4 @@ -import { expect } from "@std/expect"; -import { afterEach, describe, test } from "@std/testing/bdd"; +import { afterEach, describe, expect, test } from "vitest"; import { testGrpc, testHttp, testTcp } from "./checker"; diff --git a/packages/api/src/router/domain.test.ts b/packages/api/src/router/domain.test.ts index 10ee4cb3f..d8cad6a95 100644 --- a/packages/api/src/router/domain.test.ts +++ b/packages/api/src/router/domain.test.ts @@ -4,9 +4,8 @@ import { createPage, createTestWorkspace, } from "@openstatus/db/src/test/factories"; -import { expect } from "@std/expect"; -import { afterAll, beforeAll, test } from "@std/testing/bdd"; import { TRPCError } from "@trpc/server"; +import { afterAll, beforeAll, expect, test } from "vitest"; import { edgeRouter } from "../edge"; import { vercelFetch } from "../lib/vercel"; diff --git a/packages/api/src/router/effective-status.test.ts b/packages/api/src/router/effective-status.test.ts index 9c7ef9e86..a184fed57 100644 --- a/packages/api/src/router/effective-status.test.ts +++ b/packages/api/src/router/effective-status.test.ts @@ -1,5 +1,4 @@ -import { expect } from "@std/expect"; -import { describe, test } from "@std/testing/bdd"; +import { describe, expect, test } from "vitest"; import { computeEffectiveStatus } from "./effective-status"; diff --git a/packages/api/src/router/feedback.test.ts b/packages/api/src/router/feedback.test.ts index ec4390241..aeb2c3d27 100644 --- a/packages/api/src/router/feedback.test.ts +++ b/packages/api/src/router/feedback.test.ts @@ -1,6 +1,5 @@ import { createTestWorkspace } from "@openstatus/db/src/test/factories"; -import { expect } from "@std/expect"; -import { test } from "@std/testing/bdd"; +import { expect, test } from "vitest"; import { edgeRouter } from "../edge"; import { createInnerTRPCContext } from "../trpc"; diff --git a/packages/api/src/router/import.test.ts b/packages/api/src/router/import.test.ts index 40d9ac0ce..352d07482 100644 --- a/packages/api/src/router/import.test.ts +++ b/packages/api/src/router/import.test.ts @@ -23,8 +23,7 @@ import { MOCK_SUBSCRIBERS, } from "@openstatus/importers/statuspage/fixtures"; import { clearAuditLogFor } from "@openstatus/services/test/helpers"; -import { expect } from "@std/expect"; -import { afterAll, beforeAll, test } from "@std/testing/bdd"; +import { afterAll, beforeAll, expect, test } from "vitest"; import { edgeRouter } from "../edge"; import { createInnerTRPCContext } from "../trpc"; diff --git a/packages/api/src/router/incident.test.ts b/packages/api/src/router/incident.test.ts index c4a5d61b0..ea8852f5e 100644 --- a/packages/api/src/router/incident.test.ts +++ b/packages/api/src/router/incident.test.ts @@ -6,9 +6,8 @@ import { } from "@openstatus/db/src/schema"; import { createIncident } from "@openstatus/db/src/test/factories"; import { clearAuditLogFor } from "@openstatus/services/test/helpers"; -import { expect } from "@std/expect"; -import { afterAll, test } from "@std/testing/bdd"; import { TRPCError } from "@trpc/server"; +import { afterAll, expect, test } from "vitest"; import { edgeRouter } from "../edge"; import { createInnerTRPCContext } from "../trpc"; diff --git a/packages/api/src/router/maintenance.test.ts b/packages/api/src/router/maintenance.test.ts index 52b5ffa79..daca86524 100644 --- a/packages/api/src/router/maintenance.test.ts +++ b/packages/api/src/router/maintenance.test.ts @@ -7,9 +7,8 @@ import { pageComponent, } from "@openstatus/db/src/schema"; import { clearAuditLogFor } from "@openstatus/services/test/helpers"; -import { expect } from "@std/expect"; -import { afterAll, beforeAll, test } from "@std/testing/bdd"; import { TRPCError } from "@trpc/server"; +import { afterAll, beforeAll, expect, test } from "vitest"; import { edgeRouter } from "../edge"; import { createInnerTRPCContext } from "../trpc"; diff --git a/packages/api/src/router/monitor.test.ts b/packages/api/src/router/monitor.test.ts index 62079b222..9148f4c25 100644 --- a/packages/api/src/router/monitor.test.ts +++ b/packages/api/src/router/monitor.test.ts @@ -13,9 +13,8 @@ import { createMonitor, createWorkspace, } from "@openstatus/db/src/test/factories"; -import { expect } from "@std/expect"; -import { afterAll, beforeAll, test } from "@std/testing/bdd"; import { TRPCError } from "@trpc/server"; +import { afterAll, beforeAll, expect, test } from "vitest"; import { edgeRouter } from "../edge"; import { createInnerTRPCContext } from "../trpc"; diff --git a/packages/api/src/router/notification.test.ts b/packages/api/src/router/notification.test.ts index a0ef37bf0..7137e5db4 100644 --- a/packages/api/src/router/notification.test.ts +++ b/packages/api/src/router/notification.test.ts @@ -3,9 +3,8 @@ import { notification, notificationsToMonitors, } from "@openstatus/db/src/schema"; -import { expect } from "@std/expect"; -import { afterAll, beforeAll, test } from "@std/testing/bdd"; import { TRPCError } from "@trpc/server"; +import { afterAll, beforeAll, expect, test } from "vitest"; import { edgeRouter } from "../edge"; import { createInnerTRPCContext } from "../trpc"; diff --git a/packages/api/src/router/oauth.test.ts b/packages/api/src/router/oauth.test.ts index cc88a3ef0..fe65cdef6 100644 --- a/packages/api/src/router/oauth.test.ts +++ b/packages/api/src/router/oauth.test.ts @@ -12,9 +12,8 @@ import { verifyAccessToken, } from "@openstatus/services/oauth"; import { clearAuditLogFor } from "@openstatus/services/test/helpers"; -import { expect } from "@std/expect"; -import { afterAll, beforeAll, describe, test } from "@std/testing/bdd"; import { NextRequest } from "next/server.js"; +import { afterAll, beforeAll, describe, expect, test } from "vitest"; import { edgeRouter } from "../edge"; import { createInnerTRPCContext, createTRPCContext } from "../trpc"; diff --git a/packages/api/src/router/page.custom-domain.test.ts b/packages/api/src/router/page.custom-domain.test.ts index 14882194e..eea4ab215 100644 --- a/packages/api/src/router/page.custom-domain.test.ts +++ b/packages/api/src/router/page.custom-domain.test.ts @@ -4,9 +4,8 @@ import { selectWorkspaceSchema, workspace, } from "@openstatus/db/src/schema"; -import { expect } from "@std/expect"; -import { test } from "@std/testing/bdd"; import { TRPCError } from "@trpc/server"; +import { expect, test } from "vitest"; import { edgeRouter } from "../edge"; import { createInnerTRPCContext } from "../trpc"; diff --git a/packages/api/src/router/pageComponent.test.ts b/packages/api/src/router/pageComponent.test.ts index 187cef84c..a874351c5 100644 --- a/packages/api/src/router/pageComponent.test.ts +++ b/packages/api/src/router/pageComponent.test.ts @@ -1,9 +1,8 @@ import { db, eq } from "@openstatus/db"; import { monitor } from "@openstatus/db/src/schema"; import { allPlans } from "@openstatus/db/src/schema/plan/config"; -import { expect } from "@std/expect"; -import { afterAll, beforeAll, test } from "@std/testing/bdd"; import { TRPCError } from "@trpc/server"; +import { afterAll, beforeAll, expect, test } from "vitest"; import { edgeRouter } from "../edge"; import { createInnerTRPCContext } from "../trpc"; diff --git a/packages/api/src/router/privateLocation.test.ts b/packages/api/src/router/privateLocation.test.ts index 9c0737d21..186771ae0 100644 --- a/packages/api/src/router/privateLocation.test.ts +++ b/packages/api/src/router/privateLocation.test.ts @@ -8,9 +8,8 @@ import { createMonitor, createWorkspace, } from "@openstatus/db/src/test/factories"; -import { expect } from "@std/expect"; -import { afterAll, beforeAll, test } from "@std/testing/bdd"; import { TRPCError } from "@trpc/server"; +import { afterAll, beforeAll, expect, test } from "vitest"; import { edgeRouter } from "../edge"; import { createInnerTRPCContext } from "../trpc"; diff --git a/packages/api/src/router/slackUser.test.ts b/packages/api/src/router/slackUser.test.ts index 0d6051759..9fe031d0a 100644 --- a/packages/api/src/router/slackUser.test.ts +++ b/packages/api/src/router/slackUser.test.ts @@ -6,9 +6,8 @@ import { createWorkspace, } from "@openstatus/db/src/test/factories"; import { signSlackLinkToken } from "@openstatus/services/slack-user"; -import { expect } from "@std/expect"; -import { beforeAll, test } from "@std/testing/bdd"; import { TRPCError } from "@trpc/server"; +import { beforeAll, expect, test } from "vitest"; import { lambdaRouter } from "../lambda"; import { createInnerTRPCContext } from "../trpc"; diff --git a/packages/api/src/router/statusPage.access.test.ts b/packages/api/src/router/statusPage.access.test.ts index 86b0c923e..e01b375d6 100644 --- a/packages/api/src/router/statusPage.access.test.ts +++ b/packages/api/src/router/statusPage.access.test.ts @@ -5,11 +5,10 @@ import { createPageComponent, createTestWorkspace, } from "@openstatus/db/src/test/factories"; -import { expect } from "@std/expect"; -import { afterAll, beforeAll, describe, test } from "@std/testing/bdd"; import { TRPCError } from "@trpc/server"; import { fetchRequestHandler } from "@trpc/server/adapters/fetch"; import { NextRequest } from "next/server.js"; +import { afterAll, beforeAll, describe, expect, test } from "vitest"; import { appRouter } from "../root"; import { createInnerTRPCContext, createTRPCContext } from "../trpc"; diff --git a/packages/api/src/router/statusPage.e2e.test.ts b/packages/api/src/router/statusPage.e2e.test.ts index dfbb340c0..5eb68d45c 100644 --- a/packages/api/src/router/statusPage.e2e.test.ts +++ b/packages/api/src/router/statusPage.e2e.test.ts @@ -7,8 +7,7 @@ import { pageSubscriber, workspace, } from "@openstatus/db/src/schema"; -import { expect } from "@std/expect"; -import { afterAll, beforeAll, describe, test } from "@std/testing/bdd"; +import { afterAll, beforeAll, describe, expect, test } from "vitest"; /** * End-to-end integration tests for the full unsubscribe flow. diff --git a/packages/api/src/router/statusPage.subscribe.test.ts b/packages/api/src/router/statusPage.subscribe.test.ts index c64c8a546..989b4e4ae 100644 --- a/packages/api/src/router/statusPage.subscribe.test.ts +++ b/packages/api/src/router/statusPage.subscribe.test.ts @@ -4,9 +4,8 @@ import { createPage, createTestWorkspace, } from "@openstatus/db/src/test/factories"; -import { expect } from "@std/expect"; -import { afterAll, beforeAll, describe, test } from "@std/testing/bdd"; import { NextRequest } from "next/server.js"; +import { afterAll, beforeAll, describe, expect, test } from "vitest"; import { appRouter } from "../root"; import { createInnerTRPCContext } from "../trpc"; diff --git a/packages/api/src/router/statusPage.unsubscribe.test.ts b/packages/api/src/router/statusPage.unsubscribe.test.ts index 8cb66d5f6..3ff5b5490 100644 --- a/packages/api/src/router/statusPage.unsubscribe.test.ts +++ b/packages/api/src/router/statusPage.unsubscribe.test.ts @@ -1,7 +1,6 @@ import { db, eq } from "@openstatus/db"; import { page, pageSubscriber } from "@openstatus/db/src/schema"; -import { expect } from "@std/expect"; -import { afterAll, beforeAll, describe, test } from "@std/testing/bdd"; +import { afterAll, beforeAll, describe, expect, test } from "vitest"; // Test data setup let testPageId: number; diff --git a/packages/api/src/router/statusPage.utils.test.ts b/packages/api/src/router/statusPage.utils.test.ts index 56542e0a8..f3e0e7f05 100644 --- a/packages/api/src/router/statusPage.utils.test.ts +++ b/packages/api/src/router/statusPage.utils.test.ts @@ -5,8 +5,7 @@ import type { StatusReport, StatusReportUpdate, } from "@openstatus/db/src/schema"; -import { expect } from "@std/expect"; -import { describe, it } from "@std/testing/bdd"; +import { describe, expect, it } from "vitest"; import { currentImpactByComponent, @@ -2250,8 +2249,8 @@ describe("withTinybirdFallback", () => { }); it("falls back when the read exceeds the timeout", async () => { - // keep a handle on the slow read's timer — deno's leak sanitizer fails - // the test if it outlives the assertion + // keep a handle on the slow read's timer so it can be cleared before + // the test ends let slowRead!: ReturnType; const result = await withTinybirdFallback( () => diff --git a/packages/api/src/router/statusReport.test.ts b/packages/api/src/router/statusReport.test.ts index 794086412..2e9e06e10 100644 --- a/packages/api/src/router/statusReport.test.ts +++ b/packages/api/src/router/statusReport.test.ts @@ -7,9 +7,8 @@ import { statusReportsToPageComponents, } from "@openstatus/db/src/schema"; import { clearAuditLogFor } from "@openstatus/services/test/helpers"; -import { expect } from "@std/expect"; -import { afterAll, beforeAll, test } from "@std/testing/bdd"; import { TRPCError } from "@trpc/server"; +import { afterAll, beforeAll, expect, test } from "vitest"; import { edgeRouter } from "../edge"; import { createInnerTRPCContext } from "../trpc"; diff --git a/packages/api/src/router/stripe/shared.test.ts b/packages/api/src/router/stripe/shared.test.ts index 346b49796..1d8a287cd 100644 --- a/packages/api/src/router/stripe/shared.test.ts +++ b/packages/api/src/router/stripe/shared.test.ts @@ -1,6 +1,5 @@ -import { expect } from "@std/expect"; -import { describe, test } from "@std/testing/bdd"; import type Stripe from "stripe"; +import { describe, expect, test } from "vitest"; import { syncCustomerEmailToNewOwner } from "./shared"; diff --git a/packages/api/src/router/stripe/trial-lifecycle.test.ts b/packages/api/src/router/stripe/trial-lifecycle.test.ts index 7f400049b..297be9212 100644 --- a/packages/api/src/router/stripe/trial-lifecycle.test.ts +++ b/packages/api/src/router/stripe/trial-lifecycle.test.ts @@ -12,10 +12,16 @@ import { createWorkspace, } from "@openstatus/db/src/test/factories"; import { delivery, resend } from "@openstatus/emails/src/send"; -import { expect } from "@std/expect"; -import { afterEach, beforeEach, describe, test } from "@std/testing/bdd"; -import { assertSpyCalls, type Stub, stub } from "@std/testing/mock"; import type Stripe from "stripe"; +import { + afterEach, + beforeEach, + describe, + expect, + test, + vi, + type MockInstance, +} from "vitest"; import { edgeRouter } from "../../edge"; import { createInnerTRPCContext } from "../../trpc"; @@ -127,67 +133,71 @@ async function readTrialedAt(workspaceId: number) { describe("trial lifecycle", () => { let live: Stripe.Subscription[]; let customer: Stripe.Customer; - let stubs: Stub[]; - let updateSubscription: Stub; - let cancelStripeSubscription: Stub; - let createSession: Stub; + let stubs: MockInstance[]; + let updateSubscription: MockInstance; + let cancelStripeSubscription: MockInstance; + let createSession: MockInstance; beforeEach(() => { live = []; customer = customerWith(null); - updateSubscription = stub( - stripe.subscriptions, - "update", - (id: string, params?: Stripe.SubscriptionUpdateParams) => { - const base = live.find((s) => s.id === id) ?? subscription("cus_x"); - const items = params?.items?.[0]?.price - ? { - ...base.items, - data: [ - { - ...base.items.data[0], - price: { id: params.items[0].price }, - } as Stripe.SubscriptionItem, - ], - } - : base.items; - return Promise.resolve({ - ...base, - items, - status: params?.trial_end === "now" ? "active" : base.status, - trial_end: params?.trial_end === "now" ? null : base.trial_end, - } as Stripe.Response); - }, - ); - cancelStripeSubscription = stub(stripe.subscriptions, "cancel", () => - Promise.resolve({} as Stripe.Response), - ); - createSession = stub(stripe.checkout.sessions, "create", () => - Promise.resolve({ - url: "https://checkout.stripe.test/session", - } as Stripe.Response), - ); + updateSubscription = vi + .spyOn(stripe.subscriptions, "update") + .mockImplementation( + (id: string, params?: Stripe.SubscriptionUpdateParams) => { + const base = live.find((s) => s.id === id) ?? subscription("cus_x"); + const items = params?.items?.[0]?.price + ? { + ...base.items, + data: [ + { + ...base.items.data[0], + price: { id: params.items[0].price }, + } as Stripe.SubscriptionItem, + ], + } + : base.items; + return Promise.resolve({ + ...base, + items, + status: params?.trial_end === "now" ? "active" : base.status, + trial_end: params?.trial_end === "now" ? null : base.trial_end, + } as Stripe.Response); + }, + ); + cancelStripeSubscription = vi + .spyOn(stripe.subscriptions, "cancel") + .mockImplementation(() => + Promise.resolve({} as Stripe.Response), + ); + createSession = vi + .spyOn(stripe.checkout.sessions, "create") + .mockImplementation(() => + Promise.resolve({ + url: "https://checkout.stripe.test/session", + } as Stripe.Response), + ); stubs = [ updateSubscription, cancelStripeSubscription, createSession, - stub( - stripe.subscriptions, - "list", + vi.spyOn(stripe.subscriptions, "list").mockImplementation( () => ({ autoPagingToArray: (_opts: { limit: number }) => Promise.resolve(live), }) as Stripe.ApiListPromise, ), - stub(stripe.customers, "retrieve", () => - Promise.resolve(customer as Stripe.Response), - ), + vi + .spyOn(stripe.customers, "retrieve") + .mockImplementation(() => + Promise.resolve(customer as Stripe.Response), + ), ]; }); afterEach(() => { - for (const s of stubs) s.restore(); + for (const s of stubs) s.mockRestore(); }); function withCard() { @@ -208,8 +218,8 @@ describe("trial lifecycle", () => { }); expect(result?.type).toBe("checkout"); - assertSpyCalls(updateSubscription, 0); - expect(createSession.calls[0]?.args[0]).toMatchObject({ + expect(updateSubscription).toHaveBeenCalledTimes(0); + expect(createSession.mock.calls[0]?.[0]).toMatchObject({ mode: "subscription", customer: s.stripeId, line_items: [{ price: TEAM_PRICE, quantity: 1 }], @@ -230,7 +240,7 @@ describe("trial lifecycle", () => { }); expect(result?.type).toBe("setup"); - expect(createSession.calls[0]?.args[0]).toMatchObject({ + expect(createSession.mock.calls[0]?.[0]).toMatchObject({ mode: "setup", customer: s.stripeId, setup_intent_data: { metadata: { subscriptionId: sub.id } }, @@ -252,7 +262,7 @@ describe("trial lifecycle", () => { }); expect(result?.type).toBe("updated"); - expect(updateSubscription.calls[0]?.args).toEqual([ + expect(updateSubscription.mock.calls[0]).toEqual([ sub.id, { items: [{ id: "si_plan", price: TEAM_PRICE }], @@ -280,7 +290,7 @@ describe("trial lifecycle", () => { value: true, }), ).rejects.toMatchObject({ code: "PRECONDITION_FAILED" }); - assertSpyCalls(updateSubscription, 0); + expect(updateSubscription).toHaveBeenCalledTimes(0); }); test("trialing with a card → trial ended before the addon is added", async () => { @@ -289,19 +299,21 @@ describe("trial lifecycle", () => { const sub = subscription(s.stripeId); live = [sub]; const planItem = sub.items.data[0]; - const listItems = stub( - stripe.subscriptionItems, - "list", - () => - Promise.resolve({ - data: planItem ? [planItem] : [], - } as Stripe.Response< - Stripe.ApiList - >) as Stripe.ApiListPromise, - ); - const createItem = stub(stripe.subscriptionItems, "create", () => - Promise.resolve({} as Stripe.Response), - ); + const listItems = vi + .spyOn(stripe.subscriptionItems, "list") + .mockImplementation( + () => + Promise.resolve({ + data: planItem ? [planItem] : [], + } as Stripe.Response< + Stripe.ApiList + >) as Stripe.ApiListPromise, + ); + const createItem = vi + .spyOn(stripe.subscriptionItems, "create") + .mockImplementation(() => + Promise.resolve({} as Stripe.Response), + ); stubs.push(listItems, createItem); await stripeRouter.createCaller(asCaller(s)).addAddon({ @@ -310,11 +322,11 @@ describe("trial lifecycle", () => { value: true, }); - expect(updateSubscription.calls[0]?.args).toEqual([ + expect(updateSubscription.mock.calls[0]).toEqual([ sub.id, { trial_end: "now", payment_behavior: "error_if_incomplete" }, ]); - expect(createItem.calls[0]?.args[0]).toMatchObject({ + expect(createItem.mock.calls[0]?.[0]).toMatchObject({ price: WHITE_LABEL_PRICE, subscription: sub.id, }); @@ -327,9 +339,13 @@ describe("trial lifecycle", () => { const s = await seedTrial(); withCard(); live = [subscription(s.stripeId)]; - const delItem = stub(stripe.subscriptionItems, "del", () => - Promise.resolve({} as Stripe.Response), - ); + const delItem = vi + .spyOn(stripe.subscriptionItems, "del") + .mockImplementation(() => + Promise.resolve( + {} as Stripe.Response, + ), + ); stubs.push(delItem); await expect( @@ -339,8 +355,8 @@ describe("trial lifecycle", () => { value: false, }), ).rejects.toMatchObject({ code: "BAD_REQUEST" }); - assertSpyCalls(delItem, 0); - assertSpyCalls(updateSubscription, 0); + expect(delItem).toHaveBeenCalledTimes(0); + expect(updateSubscription).toHaveBeenCalledTimes(0); }); }); @@ -351,8 +367,8 @@ describe("trial lifecycle", () => { await cancelSubscription("cus_cancel_trial"); - expect(cancelStripeSubscription.calls[0]?.args[0]).toBe(sub.id); - assertSpyCalls(updateSubscription, 0); + expect(cancelStripeSubscription.mock.calls[0]?.[0]).toBe(sub.id); + expect(updateSubscription).toHaveBeenCalledTimes(0); }); test("active → cancelled at period end", async () => { @@ -361,8 +377,8 @@ describe("trial lifecycle", () => { await cancelSubscription("cus_cancel_active"); - assertSpyCalls(cancelStripeSubscription, 0); - expect(updateSubscription.calls[0]?.args[1]).toMatchObject({ + expect(cancelStripeSubscription).toHaveBeenCalledTimes(0); + expect(updateSubscription.mock.calls[0]?.[1]).toMatchObject({ cancel_at_period_end: true, }); }); @@ -376,7 +392,7 @@ describe("trial lifecycle", () => { await edgeRouter.createCaller(asCaller(s)).user.deleteAccount(); - expect(cancelStripeSubscription.calls[0]?.args[0]).toBe(sub.id); + expect(cancelStripeSubscription.mock.calls[0]?.[0]).toBe(sub.id); const after = await readWorkspace(s.ws.id); expect(after?.plan).toBe("free"); expect(after?.trialEndsAt).toBeNull(); @@ -399,7 +415,7 @@ describe("trial lifecycle", () => { edgeRouter.createCaller(asCaller(s)).user.deleteAccount(), ).rejects.toMatchObject({ code: "PRECONDITION_FAILED" }); - assertSpyCalls(cancelStripeSubscription, 0); + expect(cancelStripeSubscription).toHaveBeenCalledTimes(0); const after = await readWorkspace(s.ws.id); expect(after?.plan).toBe("starter"); expect(after?.trialEndsAt).toEqual(s.trialEndsAt); @@ -430,19 +446,25 @@ describe("trial lifecycle", () => { payment_method: "pm_new", metadata: { subscriptionId: "sub_setup" }, }; - const retrieveIntent = stub(stripe.setupIntents, "retrieve", () => - Promise.resolve(intent as Stripe.Response), - ); - const retrieveSub = stub(stripe.subscriptions, "retrieve", () => - Promise.resolve( - subscription("cus_setup", { - id: "sub_setup", - }) as Stripe.Response, - ), - ); - const updateCustomer = stub(stripe.customers, "update", () => - Promise.resolve({} as Stripe.Response), - ); + const retrieveIntent = vi + .spyOn(stripe.setupIntents, "retrieve") + .mockImplementation(() => + Promise.resolve(intent as Stripe.Response), + ); + const retrieveSub = vi + .spyOn(stripe.subscriptions, "retrieve") + .mockImplementation(() => + Promise.resolve( + subscription("cus_setup", { + id: "sub_setup", + }) as Stripe.Response, + ), + ); + const updateCustomer = vi + .spyOn(stripe.customers, "update") + .mockImplementation(() => + Promise.resolve({} as Stripe.Response), + ); stubs.push(retrieveIntent, retrieveSub, updateCustomer); await caller().sessionCompleted( @@ -453,11 +475,11 @@ describe("trial lifecycle", () => { } as Stripe.Checkout.Session), ); - expect(updateCustomer.calls[0]?.args).toEqual([ + expect(updateCustomer.mock.calls[0]).toEqual([ "cus_setup", { invoice_settings: { default_payment_method: "pm_new" } }, ]); - expect(updateSubscription.calls[0]?.args).toEqual([ + expect(updateSubscription.mock.calls[0]).toEqual([ "sub_setup", { default_payment_method: "pm_new" }, ]); @@ -465,22 +487,24 @@ describe("trial lifecycle", () => { function stubSetup() { stubs.push( - stub(stripe.setupIntents, "retrieve", () => + vi.spyOn(stripe.setupIntents, "retrieve").mockImplementation(() => Promise.resolve({ payment_method: "pm_new", metadata: { subscriptionId: "sub_setup" }, } as unknown as Stripe.Response), ), - stub(stripe.subscriptions, "retrieve", () => + vi.spyOn(stripe.subscriptions, "retrieve").mockImplementation(() => Promise.resolve( subscription("cus_setup", { id: "sub_setup", }) as Stripe.Response, ), ), - stub(stripe.customers, "update", () => - Promise.resolve({} as Stripe.Response), - ), + vi + .spyOn(stripe.customers, "update") + .mockImplementation(() => + Promise.resolve({} as Stripe.Response), + ), ); } @@ -496,7 +520,7 @@ describe("trial lifecycle", () => { await caller().sessionCompleted(setupCompleted()); - expect(updateSubscription.calls.map((c) => c.args)).toEqual([ + expect(updateSubscription.mock.calls.map((c) => c)).toEqual([ ["sub_setup", { default_payment_method: "pm_new" }], ["sub_setup", { automatic_tax: { enabled: true } }], ]); @@ -505,24 +529,24 @@ describe("trial lifecycle", () => { test("Stripe rejecting the tax location still keeps the card", async () => { stubSetup(); stubs = stubs.filter((s) => s !== updateSubscription); - updateSubscription.restore(); - const update = stub( - stripe.subscriptions, - "update", - (_id: string, params?: Stripe.SubscriptionUpdateParams) => - params?.automatic_tax - ? Promise.reject(new Error("customer_tax_location_invalid")) - : Promise.resolve({} as Stripe.Response), - ); + updateSubscription.mockRestore(); + const update = vi + .spyOn(stripe.subscriptions, "update") + .mockImplementation( + (_id: string, params?: Stripe.SubscriptionUpdateParams) => + params?.automatic_tax + ? Promise.reject(new Error("customer_tax_location_invalid")) + : Promise.resolve({} as Stripe.Response), + ); stubs.push(update); await caller().sessionCompleted(setupCompleted()); - expect(update.calls[0]?.args).toEqual([ + expect(update.mock.calls[0]).toEqual([ "sub_setup", { default_payment_method: "pm_new" }, ]); - assertSpyCalls(update, 2); + expect(update).toHaveBeenCalledTimes(2); }); test("checkout completed with a trial → trial_started, customer stamped", async () => { @@ -540,12 +564,16 @@ describe("trial lifecycle", () => { intent: "Slack alerts", }, }); - const retrieveSub = stub(stripe.subscriptions, "retrieve", () => - Promise.resolve(sub as Stripe.Response), - ); - const updateCustomer = stub(stripe.customers, "update", () => - Promise.resolve({} as Stripe.Response), - ); + const retrieveSub = vi + .spyOn(stripe.subscriptions, "retrieve") + .mockImplementation(() => + Promise.resolve(sub as Stripe.Response), + ); + const updateCustomer = vi + .spyOn(stripe.customers, "update") + .mockImplementation(() => + Promise.resolve({} as Stripe.Response), + ); stubs.push(retrieveSub, updateCustomer); const session = { mode: "subscription", @@ -562,7 +590,7 @@ describe("trial lifecycle", () => { expect(after?.subscriptionId).toBe(sub.id); expect(after?.trialEndsAt).toEqual(new Date((sub.trial_end ?? 0) * 1000)); expect(await lastPlanAuditReason(f.workspace.id)).toBe("trial_started"); - expect(updateCustomer.calls[0]?.args).toEqual([ + expect(updateCustomer.mock.calls[0]).toEqual([ stripeId, { metadata: { trialed: "true" } }, ]); @@ -574,7 +602,7 @@ describe("trial lifecycle", () => { await caller().sessionCompleted( event("checkout.session.completed", session), ); - assertSpyCalls(updateCustomer, 2); + expect(updateCustomer).toHaveBeenCalledTimes(2); expect((await readWorkspace(f.workspace.id))?.plan).toBe("starter"); expect(await readTrialedAt(f.workspace.id)).toEqual(trialedAt); }); @@ -588,12 +616,16 @@ describe("trial lifecycle", () => { trialEndsAt: null, }); const sub = subscription(stripeId, { status: "active", trial_end: null }); - const retrieveSub = stub(stripe.subscriptions, "retrieve", () => - Promise.resolve(sub as Stripe.Response), - ); - const updateCustomer = stub(stripe.customers, "update", () => - Promise.resolve({} as Stripe.Response), - ); + const retrieveSub = vi + .spyOn(stripe.subscriptions, "retrieve") + .mockImplementation(() => + Promise.resolve(sub as Stripe.Response), + ); + const updateCustomer = vi + .spyOn(stripe.customers, "update") + .mockImplementation(() => + Promise.resolve({} as Stripe.Response), + ); stubs.push(retrieveSub, updateCustomer); await caller().sessionCompleted( @@ -607,7 +639,7 @@ describe("trial lifecycle", () => { expect(await lastPlanAuditReason(f.workspace.id)).toBe( "checkout_session_completed", ); - assertSpyCalls(updateCustomer, 0); + expect(updateCustomer).toHaveBeenCalledTimes(0); expect(await readTrialedAt(f.workspace.id)).toBeNull(); }); @@ -639,8 +671,12 @@ describe("trial lifecycle", () => { }); test("a trial that ran out is audited as trial_ended, not churn", async () => { - const send = stub(resend.emails, "send"); - const enabled = stub(delivery, "enabled", () => true); + const send = vi + .spyOn(resend.emails, "send") + .mockImplementation(() => undefined as never); + const enabled = vi + .spyOn(delivery, "enabled") + .mockImplementation(() => true); stubs.push(send, enabled); const s = await seedTrial(); live = []; @@ -659,8 +695,12 @@ describe("trial lifecycle", () => { }); test("a trial the customer cancelled is audited as trial_cancelled", async () => { - const send = stub(resend.emails, "send"); - const enabled = stub(delivery, "enabled", () => true); + const send = vi + .spyOn(resend.emails, "send") + .mockImplementation(() => undefined as never); + const enabled = vi + .spyOn(delivery, "enabled") + .mockImplementation(() => true); stubs.push(send, enabled); const s = await seedTrial(); live = []; @@ -684,8 +724,12 @@ describe("trial lifecycle", () => { }); test("cancelled inside the slack after trial_end: card → churn, no card → trial", async () => { - const send = stub(resend.emails, "send"); - const enabled = stub(delivery, "enabled", () => true); + const send = vi + .spyOn(resend.emails, "send") + .mockImplementation(() => undefined as never); + const enabled = vi + .spyOn(delivery, "enabled") + .mockImplementation(() => true); stubs.push(send, enabled); live = []; const trialEnd = now() - 2 * 60 * 60; @@ -719,8 +763,12 @@ describe("trial lifecycle", () => { }); test("a converted trial cancelled later is churn even if the marker is stale", async () => { - const send = stub(resend.emails, "send"); - const enabled = stub(delivery, "enabled", () => true); + const send = vi + .spyOn(resend.emails, "send") + .mockImplementation(() => undefined as never); + const enabled = vi + .spyOn(delivery, "enabled") + .mockImplementation(() => true); stubs.push(send, enabled); // The trial-to-active webhook never landed, so `trialEndsAt` is set. const s = await seedTrial(); @@ -748,8 +796,12 @@ describe("trial lifecycle", () => { }); test("deleted on an already-free workspace → nothing happens", async () => { - const send = stub(resend.emails, "send"); - const enabled = stub(delivery, "enabled", () => true); + const send = vi + .spyOn(resend.emails, "send") + .mockImplementation(() => undefined as never); + const enabled = vi + .spyOn(delivery, "enabled") + .mockImplementation(() => true); stubs.push(send, enabled); const stripeId = `cus_${crypto.randomUUID()}`; await createTestWorkspace({ @@ -765,7 +817,7 @@ describe("trial lifecycle", () => { ), ); - assertSpyCalls(send, 0); + expect(send).toHaveBeenCalledTimes(0); }); }); }); diff --git a/packages/api/src/router/stripe/trial.test.ts b/packages/api/src/router/stripe/trial.test.ts index c3df132ad..fb163ae3b 100644 --- a/packages/api/src/router/stripe/trial.test.ts +++ b/packages/api/src/router/stripe/trial.test.ts @@ -10,10 +10,16 @@ import { createTestWorkspace, createUser, } from "@openstatus/db/src/test/factories"; -import { expect } from "@std/expect"; -import { afterEach, beforeEach, describe, test } from "@std/testing/bdd"; -import { assertSpyCalls, type Stub, stub } from "@std/testing/mock"; import type Stripe from "stripe"; +import { + afterEach, + beforeEach, + describe, + expect, + test, + vi, + type MockInstance, +} from "vitest"; import { createInnerTRPCContext } from "../../trpc"; import { workspaceRouter } from "../workspace"; @@ -81,38 +87,40 @@ describe("trial checkout", () => { let customerId: string; let customers: Stripe.Customer[]; let subscriptions: Partial[]; - let stubs: Stub[]; - let listCustomers: Stub; - let createCustomer: Stub; - let createSession: Stub; + let stubs: MockInstance[]; + let listCustomers: MockInstance; + let createCustomer: MockInstance; + let createSession: MockInstance; beforeEach(() => { customerId = `cus_${crypto.randomUUID()}`; customers = []; subscriptions = []; - listCustomers = stub( - stripe.customers, - "list", - () => - Promise.resolve({ data: customers } as Stripe.Response< - Stripe.ApiList - >) as Stripe.ApiListPromise, - ); - createCustomer = stub(stripe.customers, "create", () => - Promise.resolve({ id: customerId } as Stripe.Response), - ); - createSession = stub(stripe.checkout.sessions, "create", () => - Promise.resolve({ - url: "https://checkout.stripe.test/session", - } as Stripe.Response), - ); + listCustomers = vi + .spyOn(stripe.customers, "list") + .mockImplementation( + () => + Promise.resolve({ data: customers } as Stripe.Response< + Stripe.ApiList + >) as Stripe.ApiListPromise, + ); + createCustomer = vi + .spyOn(stripe.customers, "create") + .mockImplementation(() => + Promise.resolve({ id: customerId } as Stripe.Response), + ); + createSession = vi + .spyOn(stripe.checkout.sessions, "create") + .mockImplementation(() => + Promise.resolve({ + url: "https://checkout.stripe.test/session", + } as Stripe.Response), + ); stubs = [ listCustomers, createCustomer, createSession, - stub( - stripe.subscriptions, - "list", + vi.spyOn(stripe.subscriptions, "list").mockImplementation( // Awaited for the eligibility scan, auto-paged for the live lookup. () => Object.assign(Promise.resolve({ data: subscriptions }), { @@ -123,7 +131,7 @@ describe("trial checkout", () => { }); afterEach(() => { - for (const s of stubs) s.restore(); + for (const s of stubs) s.mockRestore(); }); describe("getCheckoutSession", () => { @@ -135,9 +143,9 @@ describe("trial checkout", () => { ); expect(result?.type).toBe("checkout"); - assertSpyCalls(createCustomer, 1); - assertSpyCalls(createSession, 1); - const params = createSession.calls[0]?.args[0]; + expect(createCustomer).toHaveBeenCalledTimes(1); + expect(createSession).toHaveBeenCalledTimes(1); + const params = createSession.mock.calls[0]?.[0]; expect(params).toMatchObject({ mode: "subscription", customer: customerId, @@ -164,7 +172,7 @@ describe("trial checkout", () => { await asCaller(f).getCheckoutSession(trialInput(f, { intent: " " })); expect( - createSession.calls[0]?.args[0]?.subscription_data?.metadata, + createSession.mock.calls[0]?.[0]?.subscription_data?.metadata, ).toEqual({ source: "trial_opt_in" }); }); @@ -177,7 +185,7 @@ describe("trial checkout", () => { plan: "team", }); - const params = createSession.calls[0]?.args[0]; + const params = createSession.mock.calls[0]?.[0]; expect(params?.payment_method_collection).toBeUndefined(); expect(params?.subscription_data).toBeUndefined(); expect(params?.success_url).toContain("?success=true"); @@ -191,8 +199,8 @@ describe("trial checkout", () => { const result = await asCaller(f).getCheckoutSession(trialInput(f)); expect(result?.type).toBe("checkout"); - assertSpyCalls(createCustomer, 0); - expect(createSession.calls[0]?.args[0]).toMatchObject({ + expect(createCustomer).toHaveBeenCalledTimes(0); + expect(createSession.mock.calls[0]?.[0]).toMatchObject({ customer: stripeId, }); }); @@ -207,7 +215,7 @@ describe("trial checkout", () => { code: "PRECONDITION_FAILED", message: "This workspace has already had a subscription.", }); - assertSpyCalls(createSession, 0); + expect(createSession).toHaveBeenCalledTimes(0); }); test("refuses a magic-link-only account", async () => { @@ -219,8 +227,8 @@ describe("trial checkout", () => { code: "PRECONDITION_FAILED", message: "Sign in with GitHub or Google to start a trial.", }); - assertSpyCalls(listCustomers, 0); - assertSpyCalls(createCustomer, 0); + expect(listCustomers).toHaveBeenCalledTimes(0); + expect(createCustomer).toHaveBeenCalledTimes(0); }); test("refuses an SSO-only account", async () => { @@ -232,7 +240,7 @@ describe("trial checkout", () => { code: "PRECONDITION_FAILED", message: "Trials are not available for SSO accounts.", }); - assertSpyCalls(createCustomer, 0); + expect(createCustomer).toHaveBeenCalledTimes(0); }); test("an account linked to both GitHub and a magic link qualifies", async () => { @@ -253,7 +261,7 @@ describe("trial checkout", () => { await expect( asCaller(f).getCheckoutSession(trialInput(f)), ).rejects.toMatchObject({ code: "PRECONDITION_FAILED" }); - assertSpyCalls(listCustomers, 0); + expect(listCustomers).toHaveBeenCalledTimes(0); }); test("refuses an email that already had a trial", async () => { @@ -269,11 +277,11 @@ describe("trial checkout", () => { code: "PRECONDITION_FAILED", message: "This workspace or email has already used a trial.", }); - expect(listCustomers.calls[0]?.args[0]).toEqual({ + expect(listCustomers.mock.calls[0]?.[0]).toEqual({ email: f.user.email, limit: 100, }); - assertSpyCalls(createCustomer, 0); + expect(createCustomer).toHaveBeenCalledTimes(0); }); test("checks both spellings of a mixed-case email", async () => { @@ -283,7 +291,7 @@ describe("trial checkout", () => { await asCaller(f).getCheckoutSession(trialInput(f)); - expect(listCustomers.calls.map((c) => c.args[0])).toEqual([ + expect(listCustomers.mock.calls.map((c) => c[0])).toEqual([ { email, limit: 100 }, { email: email.toLowerCase(), limit: 100 }, ]); @@ -298,7 +306,7 @@ describe("trial checkout", () => { code: "PRECONDITION_FAILED", message: "This workspace or email has already used a trial.", }); - assertSpyCalls(listCustomers, 0); + expect(listCustomers).toHaveBeenCalledTimes(0); }); test("refuses a member who is not the owner", async () => { @@ -321,7 +329,7 @@ describe("trial checkout", () => { await expect( asCaller(f).getCheckoutSession(trialInput(f)), ).rejects.toMatchObject({ code: "PRECONDITION_FAILED" }); - assertSpyCalls(createCustomer, 0); + expect(createCustomer).toHaveBeenCalledTimes(0); }); test("refuses a trial on any plan but Starter monthly", async () => { @@ -333,7 +341,7 @@ describe("trial checkout", () => { await expect( asCaller(f).getCheckoutSession(trialInput(f, { interval: "yearly" })), ).rejects.toMatchObject({ code: "BAD_REQUEST" }); - assertSpyCalls(createSession, 0); + expect(createSession).toHaveBeenCalledTimes(0); }); }); @@ -357,7 +365,7 @@ describe("trial checkout", () => { expect((await getWorkspace(f)).trial).toEqual({ eligible: true, }); - assertSpyCalls(listCustomers, 0); + expect(listCustomers).toHaveBeenCalledTimes(0); }); test("not eligible once the workspace has trialed", async () => { diff --git a/packages/api/src/router/stripe/utils.test.ts b/packages/api/src/router/stripe/utils.test.ts index 491036e4e..e2037a3e6 100644 --- a/packages/api/src/router/stripe/utils.test.ts +++ b/packages/api/src/router/stripe/utils.test.ts @@ -2,9 +2,8 @@ import { getAddonPackSize, getLimits, } from "@openstatus/db/src/schema/plan/utils"; -import { expect } from "@std/expect"; -import { describe, test } from "@std/testing/bdd"; import type Stripe from "stripe"; +import { describe, expect, test } from "vitest"; import { FEATURES, diff --git a/packages/api/src/router/stripe/webhook.test.ts b/packages/api/src/router/stripe/webhook.test.ts index a3b2811a8..4b4b0ef21 100644 --- a/packages/api/src/router/stripe/webhook.test.ts +++ b/packages/api/src/router/stripe/webhook.test.ts @@ -12,10 +12,16 @@ import { createUser, } from "@openstatus/db/src/test/factories"; import { delivery, resend } from "@openstatus/emails/src/send"; -import { expect } from "@std/expect"; -import { afterEach, beforeEach, describe, test } from "@std/testing/bdd"; -import { assertSpyCalls, type Stub, stub } from "@std/testing/mock"; import type Stripe from "stripe"; +import { + afterEach, + beforeEach, + describe, + expect, + test, + vi, + type MockInstance, +} from "vitest"; import { createInnerTRPCContext } from "../../trpc"; import { stripe } from "./shared"; @@ -28,7 +34,7 @@ const DAY = 86_400; const now = () => Math.floor(Date.now() / 1000); // biome-ignore lint/suspicious/noExplicitAny: stubs over the Stripe and Resend clients -type AnyStub = Stub; +type AnyStub = MockInstance; // `current_period_end` lives on the items since API version 2025-03-31. function subscription( @@ -84,26 +90,32 @@ describe("stripe webhook emails", () => { live = []; // biome-ignore lint/suspicious/noExplicitAny: Resend result double const ok = { data: { id: "email_reminder_1" }, error: null } as any; - send = stub(resend.emails, "send", () => Promise.resolve(ok)); - batch = stub(resend.batch, "send", () => Promise.resolve(ok)); - cancelEmail = stub(resend.emails, "cancel", () => Promise.resolve(ok)); - updateSubscription = stub(stripe.subscriptions, "update", () => - // biome-ignore lint/suspicious/noExplicitAny: Stripe response double - Promise.resolve({ cancel_at_period_end: true } as any), - ); + send = vi + .spyOn(resend.emails, "send") + .mockImplementation(() => Promise.resolve(ok)); + batch = vi + .spyOn(resend.batch, "send") + .mockImplementation(() => Promise.resolve(ok)); + cancelEmail = vi + .spyOn(resend.emails, "cancel") + .mockImplementation(() => Promise.resolve(ok)); + updateSubscription = vi + .spyOn(stripe.subscriptions, "update") + .mockImplementation(() => + // biome-ignore lint/suspicious/noExplicitAny: Stripe response double + Promise.resolve({ cancel_at_period_end: true } as any), + ); stubs = [ send, batch, cancelEmail, updateSubscription, - stub(delivery, "enabled", () => true), - stub( - stripe.subscriptions, - "list", + vi.spyOn(delivery, "enabled").mockImplementation(() => true), + vi.spyOn(stripe.subscriptions, "list").mockImplementation( // biome-ignore lint/suspicious/noExplicitAny: auto-paging list double () => ({ autoPagingToArray: () => Promise.resolve(live) }) as any, ), - stub(stripe.customers, "retrieve", () => + vi.spyOn(stripe.customers, "retrieve").mockImplementation(() => Promise.resolve({ deleted: false, email: "Billing-Contact@example.test", @@ -114,19 +126,19 @@ describe("stripe webhook emails", () => { }); afterEach(() => { - for (const s of stubs) s.restore(); + for (const s of stubs) s.mockRestore(); }); function failSends() { - send.restore(); - batch.restore(); + send.mockRestore(); + batch.mockRestore(); stubs = stubs.filter((s) => s !== send && s !== batch); - send = stub(resend.emails, "send", () => - Promise.reject(new Error("resend is down")), - ); - batch = stub(resend.batch, "send", () => - Promise.reject(new Error("resend is down")), - ); + send = vi + .spyOn(resend.emails, "send") + .mockImplementation(() => Promise.reject(new Error("resend is down"))); + batch = vi + .spyOn(resend.batch, "send") + .mockImplementation(() => Promise.reject(new Error("resend is down"))); stubs.push(send, batch); } @@ -167,8 +179,8 @@ describe("stripe webhook emails", () => { .where(eq(workspace.id, s.workspace.id)) .get(); expect(ws?.plan).toBe("team"); - assertSpyCalls(send, 0); - assertSpyCalls(batch, 0); + expect(send).toHaveBeenCalledTimes(0); + expect(batch).toHaveBeenCalledTimes(0); }); test("none left → plan-downgraded to owners + customer, member-removed per trimmed member", async () => { @@ -198,8 +210,8 @@ describe("stripe webhook emails", () => { ); await caller().customerSubscriptionDeleted(evt); - assertSpyCalls(send, 1); - const [payload, options] = send.calls[0].args; + expect(send).toHaveBeenCalledTimes(1); + const [payload, options] = send.mock.calls[0]; expect(payload.to).toEqual([ s.ownerEmail, "Billing-Contact@example.test", @@ -212,8 +224,8 @@ describe("stripe webhook emails", () => { }); expect(payload.react.props.loss.membersRemoved).toBe(2); - assertSpyCalls(batch, 1); - const [emails, batchOptions] = batch.calls[0].args; + expect(batch).toHaveBeenCalledTimes(1); + const [emails, batchOptions] = batch.mock.calls[0]; expect(emails.map((e: { to: string }) => e.to)).toEqual([memberEmail]); expect(emails[0].html).toContain(s.ownerEmail); expect(batchOptions).toEqual({ @@ -251,8 +263,8 @@ describe("stripe webhook emails", () => { .where(eq(workspace.id, s.workspace.id)) .get(); expect(ws?.plan).toBe("free"); - assertSpyCalls(send, 1); - assertSpyCalls(batch, 1); + expect(send).toHaveBeenCalledTimes(1); + expect(batch).toHaveBeenCalledTimes(1); const members = await db .select() .from(usersToWorkspaces) @@ -278,8 +290,8 @@ describe("stripe webhook emails", () => { }); await caller().customerSubscriptionUpdated(evt); - assertSpyCalls(send, 2); - const [confirmation, confirmationOptions] = send.calls[0].args; + expect(send).toHaveBeenCalledTimes(2); + const [confirmation, confirmationOptions] = send.mock.calls[0]; expect(confirmation.subject).toBe("Your openstatus cancellation"); expect(confirmation.to).toEqual([ s.ownerEmail, @@ -289,7 +301,7 @@ describe("stripe webhook emails", () => { idempotencyKey: `stripe:${evt.event.id}:cancellation-scheduled`, }); - const [reminder, reminderOptions] = send.calls[1].args; + const [reminder, reminderOptions] = send.mock.calls[1]; expect(new Date(reminder.scheduledAt).getTime()).toBe( (sub.items.data[0].current_period_end - 3 * DAY) * 1000, ); @@ -297,8 +309,8 @@ describe("stripe webhook emails", () => { idempotencyKey: `stripe:${evt.event.id}:plan-ending-soon`, }); - assertSpyCalls(updateSubscription, 1); - expect(updateSubscription.calls[0].args).toEqual([ + expect(updateSubscription).toHaveBeenCalledTimes(1); + expect(updateSubscription.mock.calls[0]).toEqual([ sub.id, { metadata: { reminder_email_id: "email_reminder_1" } }, ]); @@ -309,10 +321,10 @@ describe("stripe webhook emails", () => { const memberEmail = await addMember(s.workspace.id); const sub = subscription(s.stripeId, { cancel_at_period_end: true }); live = [sub]; - send.restore(); + send.mockRestore(); stubs = stubs.filter((x) => x !== send); let n = 0; - send = stub(resend.emails, "send", () => + send = vi.spyOn(resend.emails, "send").mockImplementation(() => // biome-ignore lint/suspicious/noExplicitAny: Resend result double Promise.resolve({ data: { id: `email_${n++}` }, error: null } as any), ); @@ -323,8 +335,8 @@ describe("stripe webhook emails", () => { }); await caller().customerSubscriptionUpdated(evt); - assertSpyCalls(send, 3); - const [warning, options] = send.calls[2].args; + expect(send).toHaveBeenCalledTimes(3); + const [warning, options] = send.mock.calls[2]; expect(warning.to).toEqual([memberEmail]); expect(warning.subject).toContain("ends on"); expect(warning.react.props.owners).toEqual([s.ownerEmail]); @@ -334,8 +346,8 @@ describe("stripe webhook emails", () => { expect(options).toEqual({ idempotencyKey: `stripe:${evt.event.id}:member-access-ending:0`, }); - assertSpyCalls(batch, 0); - expect(updateSubscription.calls[0].args).toEqual([ + expect(batch).toHaveBeenCalledTimes(0); + expect(updateSubscription.mock.calls[0]).toEqual([ sub.id, { metadata: { @@ -362,12 +374,12 @@ describe("stripe webhook emails", () => { }), ); - expect(cancelEmail.calls.map((c) => c.args[0])).toEqual([ + expect(cancelEmail.mock.calls.map((c) => c[0])).toEqual([ "email_owner", "email_m1", "email_m2", ]); - expect(updateSubscription.calls[0].args).toEqual([ + expect(updateSubscription.mock.calls[0]).toEqual([ sub.id, { metadata: { reminder_email_id: "", member_reminder_email_ids: "" } }, ]); @@ -387,9 +399,9 @@ describe("stripe webhook emails", () => { }), ); - assertSpyCalls(send, 1); - expect(send.calls[0].args[0].scheduledAt).toBeUndefined(); - assertSpyCalls(updateSubscription, 0); + expect(send).toHaveBeenCalledTimes(1); + expect(send.mock.calls[0][0].scheduledAt).toBeUndefined(); + expect(updateSubscription).toHaveBeenCalledTimes(0); }); test("true → false: the scheduled reminder is cancelled and the id cleared", async () => { @@ -405,10 +417,10 @@ describe("stripe webhook emails", () => { }), ); - assertSpyCalls(send, 0); - assertSpyCalls(cancelEmail, 1); - expect(cancelEmail.calls[0].args[0]).toBe("email_reminder_1"); - expect(updateSubscription.calls[0].args).toEqual([ + expect(send).toHaveBeenCalledTimes(0); + expect(cancelEmail).toHaveBeenCalledTimes(1); + expect(cancelEmail.mock.calls[0][0]).toBe("email_reminder_1"); + expect(updateSubscription.mock.calls[0]).toEqual([ sub.id, { metadata: { reminder_email_id: "" } }, ]); @@ -426,20 +438,22 @@ describe("stripe webhook emails", () => { ), ); - assertSpyCalls(send, 0); - assertSpyCalls(updateSubscription, 0); + expect(send).toHaveBeenCalledTimes(0); + expect(updateSubscription).toHaveBeenCalledTimes(0); }); test("a resume racing the metadata write cancels the fresh reminder", async () => { const s = await seed(); const sub = subscription(s.stripeId, { cancel_at_period_end: true }); live = [sub]; - updateSubscription.restore(); + updateSubscription.mockRestore(); stubs = stubs.filter((x) => x !== updateSubscription); - updateSubscription = stub(stripe.subscriptions, "update", () => - // biome-ignore lint/suspicious/noExplicitAny: Stripe response double - Promise.resolve({ cancel_at_period_end: false } as any), - ); + updateSubscription = vi + .spyOn(stripe.subscriptions, "update") + .mockImplementation(() => + // biome-ignore lint/suspicious/noExplicitAny: Stripe response double + Promise.resolve({ cancel_at_period_end: false } as any), + ); stubs.push(updateSubscription); await caller().customerSubscriptionUpdated( @@ -448,8 +462,8 @@ describe("stripe webhook emails", () => { }), ); - expect(cancelEmail.calls[0].args[0]).toBe("email_reminder_1"); - expect(updateSubscription.calls.map((c) => c.args[1])).toEqual([ + expect(cancelEmail.mock.calls[0][0]).toBe("email_reminder_1"); + expect(updateSubscription.mock.calls.map((c) => c[1])).toEqual([ { metadata: { reminder_email_id: "email_reminder_1" } }, { metadata: { reminder_email_id: "" } }, ]); @@ -461,9 +475,9 @@ describe("stripe webhook emails", () => { metadata: { reminder_email_id: "email_reminder_1" }, }); live = [sub]; - cancelEmail.restore(); + cancelEmail.mockRestore(); stubs = stubs.filter((x) => x !== cancelEmail); - cancelEmail = stub(resend.emails, "cancel", () => + cancelEmail = vi.spyOn(resend.emails, "cancel").mockImplementation(() => Promise.resolve({ data: null, error: { name: "application_error" }, @@ -478,18 +492,18 @@ describe("stripe webhook emails", () => { }), ); - assertSpyCalls(cancelEmail, 1); - assertSpyCalls(updateSubscription, 0); + expect(cancelEmail).toHaveBeenCalledTimes(1); + expect(updateSubscription).toHaveBeenCalledTimes(0); }); test("a failed confirmation does not suppress the reminder", async () => { const s = await seed(); const sub = subscription(s.stripeId, { cancel_at_period_end: true }); live = [sub]; - send.restore(); + send.mockRestore(); stubs = stubs.filter((x) => x !== send); let calls = 0; - send = stub(resend.emails, "send", () => + send = vi.spyOn(resend.emails, "send").mockImplementation(() => calls++ === 0 ? Promise.reject(new Error("resend is down")) : // biome-ignore lint/suspicious/noExplicitAny: Resend result double @@ -506,9 +520,9 @@ describe("stripe webhook emails", () => { }), ); - assertSpyCalls(send, 2); - expect(send.calls[1].args[0].scheduledAt).toBeDefined(); - assertSpyCalls(updateSubscription, 1); + expect(send).toHaveBeenCalledTimes(2); + expect(send.mock.calls[1][0].scheduledAt).toBeDefined(); + expect(updateSubscription).toHaveBeenCalledTimes(1); }); test("metadata-only update and unrelated updates send nothing", async () => { @@ -523,9 +537,9 @@ describe("stripe webhook emails", () => { event("customer.subscription.updated", sub), ); - assertSpyCalls(send, 0); - assertSpyCalls(cancelEmail, 0); - assertSpyCalls(updateSubscription, 0); + expect(send).toHaveBeenCalledTimes(0); + expect(cancelEmail).toHaveBeenCalledTimes(0); + expect(updateSubscription).toHaveBeenCalledTimes(0); }); test("a replayed event reuses the same idempotency keys", async () => { @@ -539,7 +553,7 @@ describe("stripe webhook emails", () => { await caller().customerSubscriptionUpdated(evt); await caller().customerSubscriptionUpdated(evt); - const keys = send.calls.map((c) => c.args[1].idempotencyKey); + const keys = send.mock.calls.map((c) => c[1].idempotencyKey); expect(keys.slice(0, 2)).toEqual(keys.slice(2, 4)); }); @@ -624,8 +638,8 @@ describe("stripe webhook emails", () => { await caller().customerSubscriptionTrialWillEnd(evt); - assertSpyCalls(send, 1); - const [payload, options] = send.calls[0].args; + expect(send).toHaveBeenCalledTimes(1); + const [payload, options] = send.mock.calls[0]; expect(payload.subject).toContain("add a payment method to keep"); expect(payload.to).toContain(s.ownerEmail); expect(payload.react.props).toMatchObject({ @@ -649,8 +663,8 @@ describe("stripe webhook emails", () => { ); await caller().customerSubscriptionTrialWillEnd(evt); - assertSpyCalls(send, 2); - const [warning, options] = send.calls[1].args; + expect(send).toHaveBeenCalledTimes(2); + const [warning, options] = send.mock.calls[1]; expect(warning.to).toEqual([memberEmail]); expect(warning.react.props.reason).toBe("trial"); expect(warning.scheduledAt).toBeUndefined(); @@ -668,7 +682,7 @@ describe("stripe webhook emails", () => { }), ), ); - assertSpyCalls(send, 3); + expect(send).toHaveBeenCalledTimes(3); }); test("card on file → plan continues, no loss lines", async () => { @@ -693,8 +707,8 @@ describe("stripe webhook emails", () => { ), ); - assertSpyCalls(send, 1); - const [payload] = send.calls[0].args; + expect(send).toHaveBeenCalledTimes(1); + const [payload] = send.mock.calls[0]; expect(payload.subject).toContain("plan continues"); expect(payload.react.props).toMatchObject({ hasPaymentMethod: true }); expect(payload.react.props.loss).toBeUndefined(); @@ -707,7 +721,7 @@ describe("stripe webhook emails", () => { subscription(s.stripeId, { status: "active", trial_end: now() - 1 }), ); await caller().customerSubscriptionTrialWillEnd(evt); - assertSpyCalls(send, 0); + expect(send).toHaveBeenCalledTimes(0); }); test("a mail failure fails the webhook so Stripe redelivers", async () => { @@ -725,9 +739,9 @@ describe("stripe webhook emails", () => { test("a Resend error response fails the webhook too", async () => { const s = await seed(); - send.restore(); + send.mockRestore(); stubs = stubs.filter((x) => x !== send); - send = stub(resend.emails, "send", () => + send = vi.spyOn(resend.emails, "send").mockImplementation(() => Promise.resolve({ data: null, error: { name: "application_error", message: "boom" }, @@ -757,7 +771,7 @@ describe("stripe webhook emails", () => { subscription(s.stripeId, { trial_end: null }), ), ); - assertSpyCalls(send, 0); + expect(send).toHaveBeenCalledTimes(0); }); }); }); diff --git a/packages/api/src/router/tinybird/index.test.ts b/packages/api/src/router/tinybird/index.test.ts index bd16f0471..369b50f3a 100644 --- a/packages/api/src/router/tinybird/index.test.ts +++ b/packages/api/src/router/tinybird/index.test.ts @@ -1,6 +1,5 @@ -import { expect } from "@std/expect"; -import { test } from "@std/testing/bdd"; import { TRPCError } from "@trpc/server"; +import { expect, test } from "vitest"; import { edgeRouter } from "../../edge"; import { createInnerTRPCContext } from "../../trpc"; diff --git a/packages/api/src/router/workspace.test.ts b/packages/api/src/router/workspace.test.ts index 3812aff70..4b62c5ffa 100644 --- a/packages/api/src/router/workspace.test.ts +++ b/packages/api/src/router/workspace.test.ts @@ -1,5 +1,4 @@ -import { expect } from "@std/expect"; -import { test } from "@std/testing/bdd"; +import { expect, test } from "vitest"; import { edgeRouter } from "../edge"; import { createInnerTRPCContext } from "../trpc"; diff --git a/packages/api/src/test/upstash.mock.ts b/packages/api/src/test/upstash.mock.ts index df729463e..439c2d618 100644 --- a/packages/api/src/test/upstash.mock.ts +++ b/packages/api/src/test/upstash.mock.ts @@ -1,5 +1,5 @@ -// Test double for @openstatus/upstash, swapped in via --import-map in the -// `deno test` command. Keeps tests off a real Upstash Redis instance. +// Test double for @openstatus/upstash, swapped in via test.importmap.json. +// Keeps tests off a real Upstash Redis instance. const noopRedis = { get: () => Promise.resolve(null), set: () => Promise.resolve("OK"), diff --git a/packages/api/src/trpc-errors.test.ts b/packages/api/src/trpc-errors.test.ts index c367350f7..48f4be8c6 100644 --- a/packages/api/src/trpc-errors.test.ts +++ b/packages/api/src/trpc-errors.test.ts @@ -1,6 +1,5 @@ -import { expect } from "@std/expect"; -import { describe, test } from "@std/testing/bdd"; import { TRPCError } from "@trpc/server"; +import { describe, expect, test } from "vitest"; import { EXPECTED_TRPC_CODES, diff --git a/packages/api/src/trpc.test.ts b/packages/api/src/trpc.test.ts index 82887bec2..bd5460068 100644 --- a/packages/api/src/trpc.test.ts +++ b/packages/api/src/trpc.test.ts @@ -1,6 +1,5 @@ -import { expect } from "@std/expect"; -import { test } from "@std/testing/bdd"; import { NextRequest } from "next/server.js"; +import { expect, test } from "vitest"; import type { ResolveActiveWorkspaceResult } from "./auth/resolve-active-workspace"; import { diff --git a/packages/api/vitest.config.ts b/packages/api/vitest.config.ts new file mode 100644 index 000000000..8b57687a1 --- /dev/null +++ b/packages/api/vitest.config.ts @@ -0,0 +1,6 @@ +import { defineTestConfig } from "@openstatus/test-utils/vitest"; + +export default defineTestConfig(import.meta.dirname, { + importMap: "test.importmap.json", + envFile: ".env.test", +}); diff --git a/packages/assertions/package.json b/packages/assertions/package.json index e28403cb2..f9eb2b602 100644 --- a/packages/assertions/package.json +++ b/packages/assertions/package.json @@ -8,16 +8,16 @@ "main": "src/index.ts", "scripts": { "check": "tsc --noEmit", - "test": "deno test --parallel -A --no-check --sloppy-imports" + "test": "vitest run" }, "dependencies": { "jsonpath-plus": "catalog:", "zod": "catalog:" }, "devDependencies": { + "@openstatus/test-utils": "workspace:*", "@openstatus/tsconfig": "workspace:*", - "@std/expect": "jsr:^1.0.19", - "@std/testing": "jsr:^1.0.19", - "typescript": "catalog:" + "typescript": "catalog:", + "vitest": "catalog:" } } diff --git a/packages/assertions/src/serializing.test.ts b/packages/assertions/src/serializing.test.ts index 5ae7c405f..7aaa8f1ea 100644 --- a/packages/assertions/src/serializing.test.ts +++ b/packages/assertions/src/serializing.test.ts @@ -1,5 +1,4 @@ -import { expect } from "@std/expect"; -import { describe, it } from "@std/testing/bdd"; +import { describe, expect, it } from "vitest"; import { deserialize, serialize } from "./serializing"; import { diff --git a/packages/assertions/src/v1.test.ts b/packages/assertions/src/v1.test.ts index f9f060c17..795e84875 100644 --- a/packages/assertions/src/v1.test.ts +++ b/packages/assertions/src/v1.test.ts @@ -1,5 +1,4 @@ -import { expect } from "@std/expect"; -import { describe, it } from "@std/testing/bdd"; +import { describe, expect, it } from "vitest"; import type { HttpAssertionRequest } from "./types"; import { @@ -198,19 +197,19 @@ describe("JsonBodyAssertion", () => { // JSONPath returns an array, which is compared to the target as-is, so // these fail today. Un-ignore once the matched value is unwrapped. - it.ignore("passes eq when the matched value equals the target", () => { + it.skip("passes eq when the matched value equals the target", () => { expect(json("$.status", "eq", "ok").assert(http({ body })).success).toBe( true, ); }); - it.ignore("fails not_eq when the matched value equals the target", () => { + it.skip("fails not_eq when the matched value equals the target", () => { expect( json("$.status", "not_eq", "ok").assert(http({ body })).success, ).toBe(false); }); - it.ignore("matches substrings with contains", () => { + it.skip("matches substrings with contains", () => { expect( json("$.status", "contains", "o").assert(http({ body })).success, ).toBe(true); diff --git a/packages/assertions/vitest.config.ts b/packages/assertions/vitest.config.ts new file mode 100644 index 000000000..26bdb37da --- /dev/null +++ b/packages/assertions/vitest.config.ts @@ -0,0 +1,3 @@ +import { defineTestConfig } from "@openstatus/test-utils/vitest"; + +export default defineTestConfig(import.meta.dirname); diff --git a/packages/db/package.json b/packages/db/package.json index 0b2717e57..7271a2f8b 100644 --- a/packages/db/package.json +++ b/packages/db/package.json @@ -27,7 +27,8 @@ "studio": "drizzle-kit studio", "env": "node env.ts", "seed": "node --import @oxc-node/core/register --env-file-if-exists=.env src/seed.mts", - "dev": "turso dev --db-file ../../openstatus-dev.db" + "dev": "turso dev --db-file ../../openstatus-dev.db", + "test": "vitest run" }, "dependencies": { "@libsql/client": "catalog:", @@ -44,13 +45,13 @@ "zod": "catalog:" }, "devDependencies": { + "@openstatus/test-utils": "workspace:*", "@openstatus/tsconfig": "workspace:*", - "@std/expect": "jsr:^1.0.19", - "@std/testing": "jsr:^1.0.19", "@types/bcryptjs": "catalog:", "@types/node": "catalog:", "drizzle-kit": "catalog:", "next-auth": "catalog:", - "typescript": "catalog:" + "typescript": "catalog:", + "vitest": "catalog:" } } diff --git a/packages/db/script/region-migration.test.ts b/packages/db/script/region-migration.test.ts index 38c6bc261..7df3227d1 100644 --- a/packages/db/script/region-migration.test.ts +++ b/packages/db/script/region-migration.test.ts @@ -1,5 +1,4 @@ -import { expect } from "@std/expect"; -import { beforeEach, describe, test } from "@std/testing/bdd"; +import { beforeEach, describe, expect, test } from "vitest"; import type { z } from "zod"; import type { monitorRegionSchema } from "../src/schema/constants"; diff --git a/packages/db/src/schema/plan/utils.test.ts b/packages/db/src/schema/plan/utils.test.ts index 620af10cb..1d931bb24 100644 --- a/packages/db/src/schema/plan/utils.test.ts +++ b/packages/db/src/schema/plan/utils.test.ts @@ -1,5 +1,4 @@ -import { expect } from "@std/expect"; -import { describe, it } from "@std/testing/bdd"; +import { describe, expect, it } from "vitest"; import { allPlans } from "./config"; import { YEARLY_ADDON_MULTIPLIER, getAddonPriceConfig } from "./utils"; diff --git a/packages/db/src/utils/api-key.test.ts b/packages/db/src/utils/api-key.test.ts index 4d987dda4..3dd625755 100644 --- a/packages/db/src/utils/api-key.test.ts +++ b/packages/db/src/utils/api-key.test.ts @@ -1,5 +1,4 @@ -import { expect } from "@std/expect"; -import { describe, it } from "@std/testing/bdd"; +import { describe, expect, it } from "vitest"; import { generateApiKey, diff --git a/packages/db/vitest.config.ts b/packages/db/vitest.config.ts new file mode 100644 index 000000000..26bdb37da --- /dev/null +++ b/packages/db/vitest.config.ts @@ -0,0 +1,3 @@ +import { defineTestConfig } from "@openstatus/test-utils/vitest"; + +export default defineTestConfig(import.meta.dirname); diff --git a/packages/emails/package.json b/packages/emails/package.json index 14791886e..a195c29ab 100644 --- a/packages/emails/package.json +++ b/packages/emails/package.json @@ -14,7 +14,7 @@ "scripts": { "check": "tsc --noEmit", "dev:email": "email dev", - "test": "deno test --parallel -A --no-check --sloppy-imports" + "test": "vitest run" }, "dependencies": { "@openstatus/utils": "workspace:*", @@ -27,12 +27,12 @@ "zod": "catalog:" }, "devDependencies": { + "@openstatus/test-utils": "workspace:*", "@openstatus/tsconfig": "workspace:*", "@react-email/ui": "catalog:", - "@std/expect": "jsr:^1.0.19", - "@std/testing": "jsr:^1.0.19", "@types/node": "catalog:", "@types/react": "catalog:", - "typescript": "catalog:" + "typescript": "catalog:", + "vitest": "catalog:" } } diff --git a/packages/emails/src/billing.test.tsx b/packages/emails/src/billing.test.tsx index 321c34c61..d193f8aa4 100644 --- a/packages/emails/src/billing.test.tsx +++ b/packages/emails/src/billing.test.tsx @@ -1,10 +1,16 @@ /** @jsxRuntime automatic @jsxImportSource react */ import "./test-preload.ts"; -import { expect } from "@std/expect"; -import { afterEach, beforeEach, describe, test } from "@std/testing/bdd"; -import { assertSpyCalls, type Stub, stub } from "@std/testing/mock"; import { render } from "react-email"; +import { + afterEach, + beforeEach, + describe, + expect, + test, + vi, + type MockInstance, +} from "vitest"; import type { PlanLoss } from "../emails/_components/plan-loss"; import CancellationScheduledEmail from "../emails/cancellation-scheduled"; @@ -366,20 +372,25 @@ describe("reminderScheduledAt", () => { }); describe("send plumbing", () => { - let send: Stub, batch: Stub, cancel: Stub, enabled: Stub; + let send: MockInstance, + batch: MockInstance, + cancel: MockInstance, + enabled: MockInstance; // safe because the verbs only read `data.id` and `error.name` const result = (value: unknown) => Promise.resolve(value as never); const ok = { data: { id: "email_123" }, error: null }; beforeEach(() => { - enabled = stub(delivery, "enabled", () => true); - send = stub(resend.emails, "send", () => result(ok)); - batch = stub(resend.batch, "send", () => result(ok)); - cancel = stub(resend.emails, "cancel", () => result(ok)); + enabled = vi.spyOn(delivery, "enabled").mockImplementation(() => true); + send = vi.spyOn(resend.emails, "send").mockImplementation(() => result(ok)); + batch = vi.spyOn(resend.batch, "send").mockImplementation(() => result(ok)); + cancel = vi + .spyOn(resend.emails, "cancel") + .mockImplementation(() => result(ok)); }); afterEach(() => { - for (const s of [enabled, send, batch, cancel]) s.restore(); + for (const s of [enabled, send, batch, cancel]) s.mockRestore(); }); const email = { @@ -402,43 +413,45 @@ describe("send plumbing", () => { { idempotencyKey: "k", scheduledAt: "2026-09-22T00:00:00.000Z" }, ); expect(id).toBe("email_123"); - expect(send.calls[0].args[0].scheduledAt).toBe("2026-09-22T00:00:00.000Z"); - expect(send.calls[0].args[0].replyTo).toBe("r@openstatus.dev"); - expect(send.calls[0].args[1]).toEqual({ idempotencyKey: "k" }); + expect(send.mock.calls[0][0].scheduledAt).toBe("2026-09-22T00:00:00.000Z"); + expect(send.mock.calls[0][0].replyTo).toBe("r@openstatus.dev"); + expect(send.mock.calls[0][1]).toEqual({ idempotencyKey: "k" }); }); test("sendEmail omits both when not given", async () => { await sendEmail(email); - expect("scheduledAt" in send.calls[0].args[0]).toBe(false); - expect(send.calls[0].args[1]).toBeUndefined(); + expect("scheduledAt" in send.mock.calls[0][0]).toBe(false); + expect(send.mock.calls[0][1]).toBeUndefined(); }); test("sendEmail does nothing outside production", async () => { - enabled.restore(); - enabled = stub(delivery, "enabled", () => false); + enabled.mockRestore(); + enabled = vi.spyOn(delivery, "enabled").mockImplementation(() => false); expect(await sendEmail(email)).toBeUndefined(); - assertSpyCalls(send, 0); + expect(send).toHaveBeenCalledTimes(0); }); test("an idempotency conflict or Resend error never throws", async () => { for (const name of ["invalid_idempotent_request", "application_error"]) { - send.restore(); - send = stub(resend.emails, "send", () => - result({ data: null, error: { name } }), - ); + send.mockRestore(); + send = vi + .spyOn(resend.emails, "send") + .mockImplementation(() => result({ data: null, error: { name } })); expect(await sendEmail(email, { idempotencyKey: "k" })).toBeUndefined(); - assertSpyCalls(send, 1); + expect(send).toHaveBeenCalledTimes(1); } }); test("cancelScheduledEmail wraps emails.cancel and reports success", async () => { expect(await cancelScheduledEmail("email_123")).toBe(true); - expect(cancel.calls[0].args[0]).toBe("email_123"); + expect(cancel.mock.calls[0][0]).toBe("email_123"); - cancel.restore(); - cancel = stub(resend.emails, "cancel", () => - result({ data: null, error: { name: "application_error" } }), - ); + cancel.mockRestore(); + cancel = vi + .spyOn(resend.emails, "cancel") + .mockImplementation(() => + result({ data: null, error: { name: "application_error" } }), + ); expect(await cancelScheduledEmail("email_123")).toBe(false); }); @@ -454,10 +467,10 @@ describe("send plumbing", () => { owners: ["max@acme.dev"], now, }); - assertSpyCalls(send, 2); - expect(send.calls.map((c) => c.args[0].to.length)).toEqual([50, 10]); - expect(send.calls[0].args[0].scheduledAt).toBe("2026-09-22T00:00:00.000Z"); - expect(send.calls[1].args[1]).toEqual({ + expect(send).toHaveBeenCalledTimes(2); + expect(send.mock.calls.map((c) => c[0].to.length)).toEqual([50, 10]); + expect(send.mock.calls[0][0].scheduledAt).toBe("2026-09-22T00:00:00.000Z"); + expect(send.mock.calls[1][1]).toEqual({ idempotencyKey: "stripe:evt_1:member-access-ending:1", }); expect(ids.length).toBe(2); @@ -480,7 +493,7 @@ describe("send plumbing", () => { ...req, to: ["a@acme.dev", "b@acme.dev"], }); - expect(send.calls[0].args[0].to).toEqual(send.calls[1].args[0].to); + expect(send.mock.calls[0][0].to).toEqual(send.mock.calls[1][0].to); }); test("scheduleMemberAccessEnding inside the lead window sends now, no ids", async () => { @@ -494,8 +507,8 @@ describe("send plumbing", () => { owners: [], now: new Date(endsAt.getTime() - 86_400_000), }); - assertSpyCalls(send, 1); - expect("scheduledAt" in send.calls[0].args[0]).toBe(false); + expect(send).toHaveBeenCalledTimes(1); + expect("scheduledAt" in send.mock.calls[0][0]).toBe(false); expect(ids).toEqual([]); const unscheduled = await scheduleMemberAccessEnding({ @@ -509,8 +522,8 @@ describe("send plumbing", () => { schedule: false, now: new Date(endsAt.getTime() - 10 * 86_400_000), }); - assertSpyCalls(send, 2); - expect("scheduledAt" in send.calls[1].args[0]).toBe(false); + expect(send).toHaveBeenCalledTimes(2); + expect("scheduledAt" in send.mock.calls[1][0]).toBe(false); expect(unscheduled).toEqual([]); expect( @@ -524,7 +537,7 @@ describe("send plumbing", () => { owners: [], }), ).toEqual([]); - assertSpyCalls(send, 2); + expect(send).toHaveBeenCalledTimes(2); }); test("sendMemberRemoved chunks at Resend's 100-email batch limit", async () => { @@ -535,9 +548,9 @@ describe("send plumbing", () => { reason: "downgrade", owners: [], }); - assertSpyCalls(batch, 3); - expect(batch.calls.map((c) => c.args[0].length)).toEqual([100, 100, 50]); - expect(batch.calls.map((c) => c.args[1].idempotencyKey)).toEqual([ + expect(batch).toHaveBeenCalledTimes(3); + expect(batch.mock.calls.map((c) => c[0].length)).toEqual([100, 100, 50]); + expect(batch.mock.calls.map((c) => c[1].idempotencyKey)).toEqual([ "stripe:evt_1:member-removed:0", "stripe:evt_1:member-removed:1", "stripe:evt_1:member-removed:2", @@ -545,17 +558,19 @@ describe("send plumbing", () => { }); test("a batch error is swallowed, not thrown", async () => { - batch.restore(); - batch = stub(resend.batch, "send", () => - result({ data: null, error: { name: "application_error" } }), - ); + batch.mockRestore(); + batch = vi + .spyOn(resend.batch, "send") + .mockImplementation(() => + result({ data: null, error: { name: "application_error" } }), + ); await sendMemberRemoved({ to: ["a@acme.dev"], workspaceName: "Acme", reason: "downgrade", owners: [], }); - assertSpyCalls(batch, 1); + expect(batch).toHaveBeenCalledTimes(1); }); test("sendPlanDowngraded", async () => { @@ -566,7 +581,7 @@ describe("send plumbing", () => { previousPlan: "team", loss, }); - const [payload, options] = send.calls[0].args; + const [payload, options] = send.mock.calls[0]; expect(payload.to).toEqual(["max@acme.dev"]); expect(payload.from).toBe( "Thibault from openstatus ", @@ -586,7 +601,7 @@ describe("send plumbing", () => { reason: "downgrade", owners: ["max@acme.dev"], }); - const [emails, options] = batch.calls[0].args; + const [emails, options] = batch.mock.calls[0]; expect(emails.map((e: { to: string }) => e.to)).toEqual([ "a@acme.dev", "b@acme.dev", @@ -612,9 +627,9 @@ describe("send plumbing", () => { formerOwner: "max@acme.dev", to: { new: "web@acme.dev", former: "max@acme.dev" }, }); - assertSpyCalls(send, 2); - const [toNew, newOptions] = send.calls[0].args; - const [toFormer, formerOptions] = send.calls[1].args; + expect(send).toHaveBeenCalledTimes(2); + const [toNew, newOptions] = send.mock.calls[0]; + const [toFormer, formerOptions] = send.mock.calls[1]; expect(toNew.to).toEqual(["web@acme.dev"]); expect(toNew.subject).toBe("You are now the owner of Acme on openstatus"); expect(toNew.replyTo).toBe("ping@openstatus.dev"); @@ -634,8 +649,8 @@ describe("send plumbing", () => { formerOwner: "Max", to: { new: "web@acme.dev", former: null }, }); - assertSpyCalls(send, 1); - const [toNew] = send.calls[0].args; + expect(send).toHaveBeenCalledTimes(1); + const [toNew] = send.mock.calls[0]; expect(toNew.to).toEqual(["web@acme.dev"]); expect(toNew.react.props.formerOwner).toBe("Max"); }); @@ -648,7 +663,7 @@ describe("send plumbing", () => { endsAt, loss, }); - const [payload, options] = send.calls[0].args; + const [payload, options] = send.mock.calls[0]; expect(payload.from).toBe( "Thibault from openstatus ", ); @@ -669,7 +684,7 @@ describe("send plumbing", () => { now: new Date("2026-09-01T00:00:00Z"), }); expect(id).toBe("email_123"); - const [payload, options] = send.calls[0].args; + const [payload, options] = send.mock.calls[0]; expect(payload.from).toBe( "openstatus ", ); @@ -691,7 +706,7 @@ describe("send plumbing", () => { now: new Date("2026-09-01T00:00:00Z"), }); expect(id).toBeUndefined(); - assertSpyCalls(send, 0); + expect(send).toHaveBeenCalledTimes(0); }); test("sendTrialEnding", async () => { @@ -704,7 +719,7 @@ describe("send plumbing", () => { hasPaymentMethod: false, loss, }); - const [payload, options] = send.calls[0].args; + const [payload, options] = send.mock.calls[0]; expect(payload.from).toBe( "openstatus ", ); @@ -737,7 +752,7 @@ describe("send plumbing", () => { plan: "starter", hasPaymentMethod: false, }); - assertSpyCalls(send, 0); - assertSpyCalls(batch, 0); + expect(send).toHaveBeenCalledTimes(0); + expect(batch).toHaveBeenCalledTimes(0); }); }); diff --git a/packages/emails/src/client.integration.test.tsx b/packages/emails/src/client.integration.test.tsx index f594f6e47..94f5487d5 100644 --- a/packages/emails/src/client.integration.test.tsx +++ b/packages/emails/src/client.integration.test.tsx @@ -1,9 +1,8 @@ /** @jsxRuntime automatic @jsxImportSource react */ import "./test-preload.ts"; -import { expect } from "@std/expect"; -import { describe, test } from "@std/testing/bdd"; import { render } from "react-email"; +import { describe, expect, test } from "vitest"; import PageUpdateEmail from "../emails/page-update"; import { pageUpdateSubject } from "./client"; diff --git a/packages/emails/src/client.test.ts b/packages/emails/src/client.test.ts index 6879f8a35..13e375f2e 100644 --- a/packages/emails/src/client.test.ts +++ b/packages/emails/src/client.test.ts @@ -1,12 +1,13 @@ import "./test-preload.ts"; -import { expect } from "@std/expect"; -import { afterEach, beforeEach, describe, test } from "@std/testing/bdd"; import { - assertSpyCalls, - returnsNext, - type Stub, - stub, -} from "@std/testing/mock"; + afterEach, + beforeEach, + describe, + expect, + test, + vi, + type MockInstance, +} from "vitest"; import { EmailClient } from "./client"; @@ -46,7 +47,7 @@ const idempotencyConflict = { describe("EmailClient.sendPageUpdate - idempotency & chunking", () => { let client: EmailClient; // biome-ignore lint/suspicious/noExplicitAny: stub over the Resend batch method - let batchSend: Stub; + let batchSend: MockInstance; beforeEach(() => { // zero backoff so the retry test doesn't wait on the real exponential sleep @@ -54,11 +55,13 @@ describe("EmailClient.sendPageUpdate - idempotency & chunking", () => { apiKey: "re_test_123", retryBackoff: "0 millis", }); - batchSend = stub(client.client.batch, "send", () => Promise.resolve(ok)); + batchSend = vi + .spyOn(client.client.batch, "send") + .mockImplementation(() => Promise.resolve(ok)); }); afterEach(() => { - batchSend.restore(); + batchSend.mockRestore(); }); test("passes the base idempotency key suffixed with the batch index", async () => { @@ -66,8 +69,8 @@ describe("EmailClient.sendPageUpdate - idempotency & chunking", () => { baseReq({ idempotencyKey: "status-report-update:5" }), ); - assertSpyCalls(batchSend, 1); - const options = batchSend.calls[0].args[1]; + expect(batchSend).toHaveBeenCalledTimes(1); + const options = batchSend.mock.calls[0][1]; expect(options).toEqual({ idempotencyKey: "status-report-update:5:0" }); }); @@ -79,31 +82,30 @@ describe("EmailClient.sendPageUpdate - idempotency & chunking", () => { }), ); - assertSpyCalls(batchSend, 3); - const keys = batchSend.calls.map((c) => c.args[1]?.idempotencyKey); + expect(batchSend).toHaveBeenCalledTimes(3); + const keys = batchSend.mock.calls.map((c) => c[1]?.idempotencyKey); expect(keys).toEqual([ "status-report-update:9:0", "status-report-update:9:1", "status-report-update:9:2", ]); - const sizes = batchSend.calls.map((c) => c.args[0].length); + const sizes = batchSend.mock.calls.map((c) => c[0].length); expect(sizes).toEqual([100, 100, 50]); }); test("omits the option entirely when no base key is provided", async () => { await client.sendPageUpdate(baseReq()); - const options = batchSend.calls[0].args[1]; + const options = batchSend.mock.calls[0][1]; expect(options).toBeUndefined(); }); test("reuses the same key across a retry so Resend dedupes the resend", async () => { - batchSend.restore(); - batchSend = stub( - client.client.batch, - "send", - returnsNext([Promise.resolve(fail), Promise.resolve(ok)]), - ); + batchSend.mockRestore(); + batchSend = vi + .spyOn(client.client.batch, "send") + .mockResolvedValueOnce(fail) + .mockResolvedValueOnce(ok); await client.sendPageUpdate( baseReq({ idempotencyKey: "status-report-update:7" }), @@ -111,8 +113,8 @@ describe("EmailClient.sendPageUpdate - idempotency & chunking", () => { // failure → retry: the second attempt must carry the identical key, or // Resend would treat the retry as a fresh batch and double-send. - assertSpyCalls(batchSend, 2); - const keys = batchSend.calls.map((c) => c.args[1]?.idempotencyKey); + expect(batchSend).toHaveBeenCalledTimes(2); + const keys = batchSend.mock.calls.map((c) => c[1]?.idempotencyKey); expect(keys).toEqual([ "status-report-update:7:0", "status-report-update:7:0", @@ -120,10 +122,10 @@ describe("EmailClient.sendPageUpdate - idempotency & chunking", () => { }); test("does not retry a deterministic idempotency conflict", async () => { - batchSend.restore(); - batchSend = stub(client.client.batch, "send", () => - Promise.resolve(idempotencyConflict), - ); + batchSend.mockRestore(); + batchSend = vi + .spyOn(client.client.batch, "send") + .mockImplementation(() => Promise.resolve(idempotencyConflict)); await client.sendPageUpdate( baseReq({ idempotencyKey: "status-report-update:7" }), @@ -131,22 +133,24 @@ describe("EmailClient.sendPageUpdate - idempotency & chunking", () => { // a 409 invalid_idempotent_request replays identically on every attempt; // retrying only re-logs the same error. - assertSpyCalls(batchSend, 1); + expect(batchSend).toHaveBeenCalledTimes(1); }); }); describe("EmailClient.sendMonitorAlert", () => { let client: EmailClient; // biome-ignore lint/suspicious/noExplicitAny: stub over the Resend send method - let send: Stub; + let send: MockInstance; beforeEach(() => { client = new EmailClient({ apiKey: "re_test_123" }); - send = stub(client.client.emails, "send", () => Promise.resolve(ok)); + send = vi + .spyOn(client.client.emails, "send") + .mockImplementation(() => Promise.resolve(ok)); }); afterEach(() => { - send.restore(); + send.mockRestore(); }); const req = { @@ -164,8 +168,8 @@ describe("EmailClient.sendMonitorAlert", () => { test("sends the rendered template from the system sender", async () => { await client.sendMonitorAlert(req); - assertSpyCalls(send, 1); - const payload = send.calls[0].args[0]; + expect(send).toHaveBeenCalledTimes(1); + const payload = send.mock.calls[0][0]; expect(payload.to).toBe("ping@openstatus.dev"); expect(payload.from).toBe( "openstatus ", @@ -180,8 +184,10 @@ describe("EmailClient.sendMonitorAlert", () => { }); test("rethrows a Resend error so the outbox can retry", async () => { - send.restore(); - send = stub(client.client.emails, "send", () => Promise.resolve(fail)); + send.mockRestore(); + send = vi + .spyOn(client.client.emails, "send") + .mockImplementation(() => Promise.resolve(fail)); await expect(client.sendMonitorAlert(req)).rejects.toEqual({ name: "application_error", @@ -192,15 +198,17 @@ describe("EmailClient.sendMonitorAlert", () => { describe("EmailClient.sendPrivateLocationAlert", () => { let client: EmailClient; // biome-ignore lint/suspicious/noExplicitAny: stub over the Resend batch method - let batchSend: Stub; + let batchSend: MockInstance; beforeEach(() => { client = new EmailClient({ apiKey: "re_test_123" }); - batchSend = stub(client.client.batch, "send", () => Promise.resolve(ok)); + batchSend = vi + .spyOn(client.client.batch, "send") + .mockImplementation(() => Promise.resolve(ok)); }); afterEach(() => { - batchSend.restore(); + batchSend.mockRestore(); }); const req = { @@ -214,8 +222,8 @@ describe("EmailClient.sendPrivateLocationAlert", () => { test("sends one mail per member with subject, sender and body", async () => { await client.sendPrivateLocationAlert(req); - assertSpyCalls(batchSend, 1); - const emails = batchSend.calls[0].args[0]; + expect(batchSend).toHaveBeenCalledTimes(1); + const emails = batchSend.mock.calls[0][0]; expect(emails.map((e: { to: string }) => e.to)).toEqual(req.to); expect(emails[0].from).toBe( "openstatus ", @@ -229,13 +237,15 @@ describe("EmailClient.sendPrivateLocationAlert", () => { test("does nothing without recipients", async () => { await client.sendPrivateLocationAlert({ ...req, to: [] }); - assertSpyCalls(batchSend, 0); + expect(batchSend).toHaveBeenCalledTimes(0); }); test("swallows a non rate-limit failure", async () => { - batchSend.restore(); - batchSend = stub(client.client.batch, "send", () => Promise.resolve(fail)); + batchSend.mockRestore(); + batchSend = vi + .spyOn(client.client.batch, "send") + .mockImplementation(() => Promise.resolve(fail)); await client.sendPrivateLocationAlert(req); - assertSpyCalls(batchSend, 1); + expect(batchSend).toHaveBeenCalledTimes(1); }); }); diff --git a/packages/emails/src/format.test.ts b/packages/emails/src/format.test.ts index 678822f0a..f1627b075 100644 --- a/packages/emails/src/format.test.ts +++ b/packages/emails/src/format.test.ts @@ -1,5 +1,4 @@ -import { expect } from "@std/expect"; -import { describe, test } from "@std/testing/bdd"; +import { describe, expect, test } from "vitest"; import { formatDateTime, diff --git a/packages/emails/src/templates.test.tsx b/packages/emails/src/templates.test.tsx index 6ebe9f700..9c9aa5579 100644 --- a/packages/emails/src/templates.test.tsx +++ b/packages/emails/src/templates.test.tsx @@ -1,9 +1,8 @@ /** @jsxRuntime automatic @jsxImportSource react */ import "./test-preload.ts"; -import { expect } from "@std/expect"; -import { describe, test } from "@std/testing/bdd"; import { render } from "react-email"; +import { describe, expect, test } from "vitest"; import { Callout } from "../emails/_components/callout"; import { Footer, POSTAL_ADDRESS } from "../emails/_components/footer"; diff --git a/packages/emails/src/test-preload.ts b/packages/emails/src/test-preload.ts index 16a592413..4f3780caf 100644 --- a/packages/emails/src/test-preload.ts +++ b/packages/emails/src/test-preload.ts @@ -2,8 +2,7 @@ * Test env setup. Import this FIRST in any test file so env vars are set * before module-level env validations fire on import. * - * Bun's test runner set NODE_ENV=test automatically; Deno does not, so we set - * it here. `./env` validates RESEND_API_KEY at module load and snapshots + * `./env` validates RESEND_API_KEY at module load and snapshots * NODE_ENV, so both have to be in place before `./client` or `./send` load. */ process.env.NODE_ENV = "test"; diff --git a/packages/emails/vitest.config.ts b/packages/emails/vitest.config.ts new file mode 100644 index 000000000..26bdb37da --- /dev/null +++ b/packages/emails/vitest.config.ts @@ -0,0 +1,3 @@ +import { defineTestConfig } from "@openstatus/test-utils/vitest"; + +export default defineTestConfig(import.meta.dirname); diff --git a/packages/header-analysis/package.json b/packages/header-analysis/package.json index 105de0e4a..9c777efd1 100644 --- a/packages/header-analysis/package.json +++ b/packages/header-analysis/package.json @@ -8,14 +8,14 @@ "main": "src/index.ts", "scripts": { "check": "tsc --noEmit", - "test": "deno test --parallel -A --no-check --sloppy-imports", - "test:watch": "deno test --parallel -A --no-check --sloppy-imports --watch" + "test": "vitest run", + "test:watch": "vitest" }, "dependencies": {}, "devDependencies": { + "@openstatus/test-utils": "workspace:*", "@openstatus/tsconfig": "workspace:*", - "@std/expect": "jsr:^1.0.19", - "@std/testing": "jsr:^1.0.19", - "typescript": "catalog:" + "typescript": "catalog:", + "vitest": "catalog:" } } diff --git a/packages/header-analysis/src/cdn/detect-cdn.test.ts b/packages/header-analysis/src/cdn/detect-cdn.test.ts index 7d7def9e2..997669c1c 100644 --- a/packages/header-analysis/src/cdn/detect-cdn.test.ts +++ b/packages/header-analysis/src/cdn/detect-cdn.test.ts @@ -1,5 +1,4 @@ -import { expect } from "@std/expect"; -import { describe, test } from "@std/testing/bdd"; +import { describe, expect, test } from "vitest"; import { detectCdn } from "./detect-cdn"; diff --git a/packages/header-analysis/src/cdn/edge-pop.test.ts b/packages/header-analysis/src/cdn/edge-pop.test.ts index bbec53c1a..40e1c2d80 100644 --- a/packages/header-analysis/src/cdn/edge-pop.test.ts +++ b/packages/header-analysis/src/cdn/edge-pop.test.ts @@ -1,5 +1,4 @@ -import { expect } from "@std/expect"; -import { describe, test } from "@std/testing/bdd"; +import { describe, expect, test } from "vitest"; import { extractEdgePop } from "./edge-pop"; diff --git a/packages/header-analysis/src/cdn/infer-topology.test.ts b/packages/header-analysis/src/cdn/infer-topology.test.ts index 05eeb786a..fa730dfb9 100644 --- a/packages/header-analysis/src/cdn/infer-topology.test.ts +++ b/packages/header-analysis/src/cdn/infer-topology.test.ts @@ -1,5 +1,4 @@ -import { expect } from "@std/expect"; -import { describe, test } from "@std/testing/bdd"; +import { describe, expect, test } from "vitest"; import { inferTopology } from "./infer-topology"; diff --git a/packages/header-analysis/src/cdn/normalize-cache-status.test.ts b/packages/header-analysis/src/cdn/normalize-cache-status.test.ts index f6f3743c4..e00de1f15 100644 --- a/packages/header-analysis/src/cdn/normalize-cache-status.test.ts +++ b/packages/header-analysis/src/cdn/normalize-cache-status.test.ts @@ -1,5 +1,4 @@ -import { expect } from "@std/expect"; -import { describe, test } from "@std/testing/bdd"; +import { describe, expect, test } from "vitest"; import { normalizeCacheStatus } from "./normalize-cache-status"; diff --git a/packages/header-analysis/src/parser/cache-control.test.ts b/packages/header-analysis/src/parser/cache-control.test.ts index 487bd6ad2..cda81f377 100644 --- a/packages/header-analysis/src/parser/cache-control.test.ts +++ b/packages/header-analysis/src/parser/cache-control.test.ts @@ -1,5 +1,4 @@ -import { expect } from "@std/expect"; -import { describe, it } from "@std/testing/bdd"; +import { describe, expect, it } from "vitest"; import { parseCacheControlHeader } from "./cache-control"; diff --git a/packages/header-analysis/src/parser/cf-cache-status.test.ts b/packages/header-analysis/src/parser/cf-cache-status.test.ts index d5a81eb45..e46bcd5c8 100644 --- a/packages/header-analysis/src/parser/cf-cache-status.test.ts +++ b/packages/header-analysis/src/parser/cf-cache-status.test.ts @@ -1,5 +1,4 @@ -import { expect } from "@std/expect"; -import { describe, it } from "@std/testing/bdd"; +import { describe, expect, it } from "vitest"; import { parseCfCacheStatus } from "./cf-cache-status"; diff --git a/packages/header-analysis/src/parser/cf-ray.test.ts b/packages/header-analysis/src/parser/cf-ray.test.ts index 10d586649..33c980994 100644 --- a/packages/header-analysis/src/parser/cf-ray.test.ts +++ b/packages/header-analysis/src/parser/cf-ray.test.ts @@ -1,5 +1,4 @@ -import { expect } from "@std/expect"; -import { describe, it } from "@std/testing/bdd"; +import { describe, expect, it } from "vitest"; import { parseCfRay } from "./cf-ray"; diff --git a/packages/header-analysis/src/parser/fly-request-id.test.ts b/packages/header-analysis/src/parser/fly-request-id.test.ts index c3fd32bdd..cf6ee7547 100644 --- a/packages/header-analysis/src/parser/fly-request-id.test.ts +++ b/packages/header-analysis/src/parser/fly-request-id.test.ts @@ -1,5 +1,4 @@ -import { expect } from "@std/expect"; -import { describe, it } from "@std/testing/bdd"; +import { describe, expect, it } from "vitest"; import { parseFlyRequestId } from "./fly-request-id"; diff --git a/packages/header-analysis/src/parser/x-vercel-cache.test.ts b/packages/header-analysis/src/parser/x-vercel-cache.test.ts index 2780dd223..fc1c67c77 100644 --- a/packages/header-analysis/src/parser/x-vercel-cache.test.ts +++ b/packages/header-analysis/src/parser/x-vercel-cache.test.ts @@ -1,5 +1,4 @@ -import { expect } from "@std/expect"; -import { describe, it } from "@std/testing/bdd"; +import { describe, expect, it } from "vitest"; import { parseXVercelCache } from "./x-vercel-cache"; diff --git a/packages/header-analysis/src/parser/x-vercel-id.test.ts b/packages/header-analysis/src/parser/x-vercel-id.test.ts index fd1c0f31d..01560a603 100644 --- a/packages/header-analysis/src/parser/x-vercel-id.test.ts +++ b/packages/header-analysis/src/parser/x-vercel-id.test.ts @@ -1,5 +1,4 @@ -import { expect } from "@std/expect"; -import { describe, it } from "@std/testing/bdd"; +import { describe, expect, it } from "vitest"; import { parseXVercelId } from "./x-vercel-id"; diff --git a/packages/header-analysis/vitest.config.ts b/packages/header-analysis/vitest.config.ts new file mode 100644 index 000000000..26bdb37da --- /dev/null +++ b/packages/header-analysis/vitest.config.ts @@ -0,0 +1,3 @@ +import { defineTestConfig } from "@openstatus/test-utils/vitest"; + +export default defineTestConfig(import.meta.dirname); diff --git a/packages/importers/package.json b/packages/importers/package.json index 742f8fbeb..c472a50ff 100644 --- a/packages/importers/package.json +++ b/packages/importers/package.json @@ -17,8 +17,8 @@ }, "scripts": { "check": "tsc --noEmit", - "test": "deno test --parallel -A --no-check --sloppy-imports", - "test:watch": "deno test --parallel -A --no-check --sloppy-imports --watch", + "test": "vitest run", + "test:watch": "vitest", "tsc": "tsc --noEmit" }, "dependencies": { @@ -26,9 +26,9 @@ }, "devDependencies": { "@openstatus/regions": "workspace:*", + "@openstatus/test-utils": "workspace:*", "@openstatus/tsconfig": "workspace:*", - "@std/expect": "jsr:^1.0.19", - "@std/testing": "jsr:^1.0.19", - "typescript": "catalog:" + "typescript": "catalog:", + "vitest": "catalog:" } } diff --git a/packages/importers/src/providers/betterstack/client.test.ts b/packages/importers/src/providers/betterstack/client.test.ts index fbcd501e7..47c0792c9 100644 --- a/packages/importers/src/providers/betterstack/client.test.ts +++ b/packages/importers/src/providers/betterstack/client.test.ts @@ -1,6 +1,12 @@ -import { expect } from "@std/expect"; -import { afterEach, beforeEach, describe, test } from "@std/testing/bdd"; -import { assertSpyCalls, spy } from "@std/testing/mock"; +import { + afterEach, + beforeEach, + describe, + expect, + test, + vi, + type Mock, +} from "vitest"; import { createBetterstackClient } from "./client"; import { @@ -18,7 +24,7 @@ const originalFetch = globalThis.fetch; function mockFetchPaginated(data: unknown[], status = 200) { let callCount = 0; - globalThis.fetch = spy(() => { + globalThis.fetch = vi.fn(() => { callCount++; const items = callCount === 1 ? data : []; const body = { @@ -41,7 +47,7 @@ function mockFetchPaginated(data: unknown[], status = 200) { } function mockFetchError(status: number, statusText: string) { - globalThis.fetch = spy(() => + globalThis.fetch = vi.fn(() => Promise.resolve( new Response(JSON.stringify({ error: statusText }), { status, @@ -145,9 +151,9 @@ describe("BetterstackClient", () => { test("sends correct auth header", async () => { mockFetchPaginated(MOCK_MONITORS); await client.getMonitors(); - const fetchMock = globalThis.fetch as unknown as ReturnType; - assertSpyCalls(fetchMock, 1); - const [url, options] = fetchMock.calls[0].args as [string, RequestInit]; + const fetchMock = globalThis.fetch as unknown as Mock; + expect(fetchMock).toHaveBeenCalledTimes(1); + const [url, options] = fetchMock.mock.calls[0] as [string, RequestInit]; expect(url).toBe("https://uptime.betterstack.com/api/v2/monitors"); expect((options.headers as Record).Authorization).toBe( "Bearer test-api-key", @@ -156,7 +162,7 @@ describe("BetterstackClient", () => { test("follows pagination.next for multiple pages", async () => { let callCount = 0; - globalThis.fetch = spy(() => { + globalThis.fetch = vi.fn(() => { callCount++; const body = callCount === 1 diff --git a/packages/importers/src/providers/betterstack/mapper.test.ts b/packages/importers/src/providers/betterstack/mapper.test.ts index f514fe93b..6d7a483c0 100644 --- a/packages/importers/src/providers/betterstack/mapper.test.ts +++ b/packages/importers/src/providers/betterstack/mapper.test.ts @@ -1,6 +1,5 @@ import { regionDict } from "@openstatus/regions"; -import { expect } from "@std/expect"; -import { describe, test } from "@std/testing/bdd"; +import { describe, expect, test } from "vitest"; import { MOCK_INCIDENTS, diff --git a/packages/importers/src/providers/betterstack/provider.test.ts b/packages/importers/src/providers/betterstack/provider.test.ts index 28aa05453..f37d1142e 100644 --- a/packages/importers/src/providers/betterstack/provider.test.ts +++ b/packages/importers/src/providers/betterstack/provider.test.ts @@ -1,6 +1,4 @@ -import { expect } from "@std/expect"; -import { afterEach, beforeEach, describe, test } from "@std/testing/bdd"; -import { spy } from "@std/testing/mock"; +import { afterEach, beforeEach, describe, expect, test, vi } from "vitest"; import { MOCK_INCIDENTS, @@ -26,7 +24,7 @@ function makePaginated(data: unknown[]) { } function setupMockFetch() { - globalThis.fetch = spy((url: string) => { + globalThis.fetch = vi.fn((url: string) => { const path = new URL(url as string).pathname; let body: unknown; @@ -88,7 +86,7 @@ describe("BetterstackProvider", () => { }); test("validate returns friendly 401 error", async () => { - globalThis.fetch = spy(() => + globalThis.fetch = vi.fn(() => Promise.resolve( new Response(JSON.stringify({ error: "Unauthorized" }), { status: 401, diff --git a/packages/importers/src/providers/checkly/client.test.ts b/packages/importers/src/providers/checkly/client.test.ts index 0f4277d55..98b3e3d28 100644 --- a/packages/importers/src/providers/checkly/client.test.ts +++ b/packages/importers/src/providers/checkly/client.test.ts @@ -1,6 +1,12 @@ -import { expect } from "@std/expect"; -import { afterEach, beforeEach, describe, test } from "@std/testing/bdd"; -import { assertSpyCalls, spy } from "@std/testing/mock"; +import { + afterEach, + beforeEach, + describe, + expect, + test, + vi, + type Mock, +} from "vitest"; import { createChecklyClient } from "./client"; import { @@ -23,7 +29,7 @@ function jsonResponse(body: unknown, status = 200) { // Returns each body in order; repeats the last one once exhausted. function mockFetchSequence(bodies: unknown[], status = 200) { let call = 0; - globalThis.fetch = spy(() => { + globalThis.fetch = vi.fn(() => { const body = bodies[Math.min(call, bodies.length - 1)]; call++; return Promise.resolve(jsonResponse(body, status)); @@ -82,9 +88,9 @@ describe("ChecklyClient", () => { test("sends both auth headers and correct path", async () => { mockFetchSequence([MOCK_CHECKS]); await client.getChecks(); - const fetchMock = globalThis.fetch as unknown as ReturnType; - assertSpyCalls(fetchMock, 1); - const [url, options] = fetchMock.calls[0].args as [string, RequestInit]; + const fetchMock = globalThis.fetch as unknown as Mock; + expect(fetchMock).toHaveBeenCalledTimes(1); + const [url, options] = fetchMock.mock.calls[0] as [string, RequestInit]; expect(url).toContain("https://api.checklyhq.com/v1/checks"); const headers = options.headers as Record; expect(headers.Authorization).toBe("Bearer test-api-key"); @@ -103,9 +109,9 @@ describe("ChecklyClient", () => { ]); const pages = await client.getStatusPages(); expect(pages).toHaveLength(2); - const fetchMock = globalThis.fetch as unknown as ReturnType; - assertSpyCalls(fetchMock, 2); - const secondUrl = fetchMock.calls[1].args[0] as string; + const fetchMock = globalThis.fetch as unknown as Mock; + expect(fetchMock).toHaveBeenCalledTimes(2); + const secondUrl = fetchMock.mock.calls[1][0] as string; expect(secondUrl).toContain("nextId=cursor-2"); }); @@ -117,8 +123,8 @@ describe("ChecklyClient", () => { mockFetchSequence([fullPage, []]); const checks = await client.getChecks(); expect(checks).toHaveLength(100); - const fetchMock = globalThis.fetch as unknown as ReturnType; - assertSpyCalls(fetchMock, 2); - expect(fetchMock.calls[1].args[0] as string).toContain("page=2"); + const fetchMock = globalThis.fetch as unknown as Mock; + expect(fetchMock).toHaveBeenCalledTimes(2); + expect(fetchMock.mock.calls[1][0] as string).toContain("page=2"); }); }); diff --git a/packages/importers/src/providers/checkly/mapper.test.ts b/packages/importers/src/providers/checkly/mapper.test.ts index 5c32a5e6b..c2d6610b0 100644 --- a/packages/importers/src/providers/checkly/mapper.test.ts +++ b/packages/importers/src/providers/checkly/mapper.test.ts @@ -1,6 +1,5 @@ import { regionDict } from "@openstatus/regions"; -import { expect } from "@std/expect"; -import { describe, test } from "@std/testing/bdd"; +import { describe, expect, test } from "vitest"; import { MOCK_CHECKS, diff --git a/packages/importers/src/providers/checkly/provider.test.ts b/packages/importers/src/providers/checkly/provider.test.ts index 52f9511ed..3171a3b1a 100644 --- a/packages/importers/src/providers/checkly/provider.test.ts +++ b/packages/importers/src/providers/checkly/provider.test.ts @@ -1,6 +1,4 @@ -import { expect } from "@std/expect"; -import { afterEach, describe, test } from "@std/testing/bdd"; -import { spy } from "@std/testing/mock"; +import { afterEach, describe, expect, test, vi } from "vitest"; import type { PhaseResult } from "../../types"; import { @@ -22,7 +20,7 @@ function jsonResponse(body: unknown, status = 200) { } function mockFetchByPath(status = 200) { - globalThis.fetch = spy((input: string | URL | Request) => { + globalThis.fetch = vi.fn((input: string | URL | Request) => { const url = String(input); let body: unknown = []; // incidents must be matched before the status-pages prefix diff --git a/packages/importers/src/providers/instatus/client.test.ts b/packages/importers/src/providers/instatus/client.test.ts index df139d124..076b0dba5 100644 --- a/packages/importers/src/providers/instatus/client.test.ts +++ b/packages/importers/src/providers/instatus/client.test.ts @@ -1,6 +1,12 @@ -import { expect } from "@std/expect"; -import { afterEach, beforeEach, describe, test } from "@std/testing/bdd"; -import { spy } from "@std/testing/mock"; +import { + afterEach, + beforeEach, + describe, + expect, + test, + vi, + type Mock, +} from "vitest"; import { createInstatusClient } from "./client"; import { @@ -14,7 +20,7 @@ import { const originalFetch = globalThis.fetch; function mockFetch(data: unknown, status = 200) { - globalThis.fetch = spy(() => + globalThis.fetch = vi.fn(() => Promise.resolve( new Response(JSON.stringify(data), { status, @@ -27,7 +33,7 @@ function mockFetch(data: unknown, status = 200) { function mockFetchPaginated(data: unknown, status = 200) { let callCount = 0; - globalThis.fetch = spy(() => { + globalThis.fetch = vi.fn(() => { callCount++; const body = callCount === 1 ? data : []; return Promise.resolve( @@ -124,8 +130,8 @@ describe("InstatusClient", () => { test("sends correct Bearer auth header", async () => { mockFetchPaginated(MOCK_PAGES); await client.getPages(); - const fetchMock = globalThis.fetch as unknown as ReturnType; - const [url, options] = fetchMock.calls[0].args as [string, RequestInit]; + const fetchMock = globalThis.fetch as unknown as Mock; + const [url, options] = fetchMock.mock.calls[0] as [string, RequestInit]; expect(url).toBe("https://api.instatus.com/v2/pages?page=1&per_page=100"); expect((options.headers as Record).Authorization).toBe( "Bearer test-api-key", @@ -141,7 +147,7 @@ describe("InstatusClient", () => { })); let callCount = 0; - globalThis.fetch = spy(() => { + globalThis.fetch = vi.fn(() => { callCount++; // First call: 100 items (= per_page), second call: empty array const body = callCount === 1 ? items : []; @@ -162,8 +168,8 @@ describe("InstatusClient", () => { test("uses v1 for incidents and v2 for other endpoints", async () => { mockFetchPaginated(MOCK_INCIDENTS); await client.getIncidents("in_page_001"); - const fetchMock = globalThis.fetch as unknown as ReturnType; - const [url] = fetchMock.calls[0].args as [string, RequestInit]; + const fetchMock = globalThis.fetch as unknown as Mock; + const [url] = fetchMock.mock.calls[0] as [string, RequestInit]; expect(url).toContain("/v1/in_page_001/incidents"); }); }); diff --git a/packages/importers/src/providers/instatus/mapper.test.ts b/packages/importers/src/providers/instatus/mapper.test.ts index a451afcf6..f8931d730 100644 --- a/packages/importers/src/providers/instatus/mapper.test.ts +++ b/packages/importers/src/providers/instatus/mapper.test.ts @@ -1,5 +1,4 @@ -import { expect } from "@std/expect"; -import { describe, it } from "@std/testing/bdd"; +import { describe, expect, it } from "vitest"; import { MOCK_COMPONENTS, diff --git a/packages/importers/src/providers/instatus/provider.test.ts b/packages/importers/src/providers/instatus/provider.test.ts index 0b904b362..541677db0 100644 --- a/packages/importers/src/providers/instatus/provider.test.ts +++ b/packages/importers/src/providers/instatus/provider.test.ts @@ -1,5 +1,4 @@ -import { expect } from "@std/expect"; -import { afterAll, beforeAll, describe, it } from "@std/testing/bdd"; +import { afterAll, beforeAll, describe, expect, it } from "vitest"; import { MOCK_COMPONENTS, diff --git a/packages/importers/src/providers/statuspage/client.test.ts b/packages/importers/src/providers/statuspage/client.test.ts index a75fcd5c2..44fccc744 100644 --- a/packages/importers/src/providers/statuspage/client.test.ts +++ b/packages/importers/src/providers/statuspage/client.test.ts @@ -1,6 +1,12 @@ -import { expect } from "@std/expect"; -import { afterEach, beforeEach, describe, test } from "@std/testing/bdd"; -import { assertSpyCalls, spy } from "@std/testing/mock"; +import { + afterEach, + beforeEach, + describe, + expect, + test, + vi, + type Mock, +} from "vitest"; import { createStatuspageClient } from "./client"; import { @@ -14,7 +20,7 @@ import { const originalFetch = globalThis.fetch; function mockFetch(data: unknown, status = 200) { - globalThis.fetch = spy(() => + globalThis.fetch = vi.fn(() => Promise.resolve( new Response(JSON.stringify(data), { status, @@ -30,7 +36,7 @@ function mockFetch(data: unknown, status = 200) { */ function mockFetchPaginated(data: unknown, status = 200) { let callCount = 0; - globalThis.fetch = spy(() => { + globalThis.fetch = vi.fn(() => { callCount++; const body = callCount === 1 ? data : []; return Promise.resolve( @@ -223,8 +229,8 @@ describe("StatuspageClient", () => { test("getPage calls correct URL path", async () => { mockFetch(MOCK_PAGES[0]); await client.getPage("sp_page_001"); - const fetchMock = globalThis.fetch as unknown as ReturnType; - const [url] = fetchMock.calls[0].args as [string, RequestInit]; + const fetchMock = globalThis.fetch as unknown as Mock; + const [url] = fetchMock.mock.calls[0] as [string, RequestInit]; expect(url).toBe("https://api.statuspage.io/v1/pages/sp_page_001"); }); @@ -243,9 +249,9 @@ describe("StatuspageClient", () => { test("sends correct auth header", async () => { mockFetch(MOCK_PAGES); await client.getPages(); - const fetchMock = globalThis.fetch as unknown as ReturnType; - assertSpyCalls(fetchMock, 1); - const [url, options] = fetchMock.calls[0].args as [string, RequestInit]; + const fetchMock = globalThis.fetch as unknown as Mock; + expect(fetchMock).toHaveBeenCalledTimes(1); + const [url, options] = fetchMock.mock.calls[0] as [string, RequestInit]; expect(url).toBe("https://api.statuspage.io/v1/pages"); expect((options.headers as Record).Authorization).toBe( "OAuth test-api-key", diff --git a/packages/importers/src/providers/statuspage/mapper.test.ts b/packages/importers/src/providers/statuspage/mapper.test.ts index dabfc431c..c2d13da2e 100644 --- a/packages/importers/src/providers/statuspage/mapper.test.ts +++ b/packages/importers/src/providers/statuspage/mapper.test.ts @@ -1,5 +1,4 @@ -import { expect } from "@std/expect"; -import { describe, it } from "@std/testing/bdd"; +import { describe, expect, it } from "vitest"; import { MOCK_COMPONENTS, diff --git a/packages/importers/src/providers/statuspage/provider.test.ts b/packages/importers/src/providers/statuspage/provider.test.ts index 3f314f054..d4f5de167 100644 --- a/packages/importers/src/providers/statuspage/provider.test.ts +++ b/packages/importers/src/providers/statuspage/provider.test.ts @@ -1,5 +1,4 @@ -import { expect } from "@std/expect"; -import { afterAll, beforeAll, describe, it } from "@std/testing/bdd"; +import { afterAll, beforeAll, describe, expect, it } from "vitest"; import { MOCK_COMPONENTS, diff --git a/packages/importers/src/schemas.test.ts b/packages/importers/src/schemas.test.ts index 7559f30fe..8e62e7227 100644 --- a/packages/importers/src/schemas.test.ts +++ b/packages/importers/src/schemas.test.ts @@ -1,5 +1,4 @@ -import { expect } from "@std/expect"; -import { describe, it } from "@std/testing/bdd"; +import { describe, expect, it } from "vitest"; import { lenientEnum } from "./schemas"; diff --git a/packages/importers/vitest.config.ts b/packages/importers/vitest.config.ts new file mode 100644 index 000000000..26bdb37da --- /dev/null +++ b/packages/importers/vitest.config.ts @@ -0,0 +1,3 @@ +import { defineTestConfig } from "@openstatus/test-utils/vitest"; + +export default defineTestConfig(import.meta.dirname); diff --git a/packages/notifications/base/package.json b/packages/notifications/base/package.json index ae923362c..001109d67 100644 --- a/packages/notifications/base/package.json +++ b/packages/notifications/base/package.json @@ -8,7 +8,7 @@ "main": "src/index.ts", "scripts": { "check": "tsc --noEmit", - "test": "deno test --parallel -A --no-check --sloppy-imports", + "test": "vitest run", "tsc": "tsc" }, "dependencies": { @@ -16,10 +16,10 @@ "@openstatus/regions": "workspace:*" }, "devDependencies": { + "@openstatus/test-utils": "workspace:*", "@openstatus/tsconfig": "workspace:*", - "@std/expect": "jsr:^1.0.19", - "@std/testing": "jsr:^1.0.19", "@types/node": "catalog:", - "typescript": "catalog:" + "typescript": "catalog:", + "vitest": "catalog:" } } diff --git a/packages/notifications/base/src/utils/duration.test.ts b/packages/notifications/base/src/utils/duration.test.ts index cd99abb7f..557da289f 100644 --- a/packages/notifications/base/src/utils/duration.test.ts +++ b/packages/notifications/base/src/utils/duration.test.ts @@ -1,5 +1,4 @@ -import { expect } from "@std/expect"; -import { describe, it } from "@std/testing/bdd"; +import { describe, expect, it } from "vitest"; import { calculateDuration, formatDuration } from "./duration"; diff --git a/packages/notifications/base/src/utils/incident.test.ts b/packages/notifications/base/src/utils/incident.test.ts index a3d3f9c67..1d4ac117c 100644 --- a/packages/notifications/base/src/utils/incident.test.ts +++ b/packages/notifications/base/src/utils/incident.test.ts @@ -1,6 +1,5 @@ import type { MonitorIncident } from "@openstatus/db/src/schema"; -import { expect } from "@std/expect"; -import { describe, it } from "@std/testing/bdd"; +import { describe, expect, it } from "vitest"; import { getIncidentDuration } from "./incident"; diff --git a/packages/notifications/base/src/utils/timestamp.test.ts b/packages/notifications/base/src/utils/timestamp.test.ts index 08a0793f1..ddeec454c 100644 --- a/packages/notifications/base/src/utils/timestamp.test.ts +++ b/packages/notifications/base/src/utils/timestamp.test.ts @@ -1,5 +1,4 @@ -import { expect } from "@std/expect"; -import { describe, it } from "@std/testing/bdd"; +import { describe, expect, it } from "vitest"; import { formatTimestamp } from "./timestamp"; diff --git a/packages/notifications/base/tsconfig.json b/packages/notifications/base/tsconfig.json index a74701aa0..aed15c74a 100644 --- a/packages/notifications/base/tsconfig.json +++ b/packages/notifications/base/tsconfig.json @@ -1,12 +1,4 @@ { "extends": "@openstatus/tsconfig/base.json", - "include": ["src", "*.ts"], - "exclude": [ - "**/*.test.ts", - "**/*.test.tsx", - "**/__tests__/**", - "**/test/**", - "**/*.mock.ts", - "**/test-preload.ts" - ] + "include": ["src", "*.ts"] } diff --git a/packages/notifications/base/vitest.config.ts b/packages/notifications/base/vitest.config.ts new file mode 100644 index 000000000..26bdb37da --- /dev/null +++ b/packages/notifications/base/vitest.config.ts @@ -0,0 +1,3 @@ +import { defineTestConfig } from "@openstatus/test-utils/vitest"; + +export default defineTestConfig(import.meta.dirname); diff --git a/packages/notifications/bird-whatsapp/package.json b/packages/notifications/bird-whatsapp/package.json index ed1a0b79b..85d586483 100644 --- a/packages/notifications/bird-whatsapp/package.json +++ b/packages/notifications/bird-whatsapp/package.json @@ -4,7 +4,7 @@ "main": "src/index.ts", "scripts": { "check": "tsc --noEmit", - "test": "deno test --parallel -A --no-check --sloppy-imports", + "test": "vitest run", "tsc": "tsc" }, "dependencies": { @@ -14,10 +14,10 @@ "zod": "catalog:" }, "devDependencies": { + "@openstatus/test-utils": "workspace:*", "@openstatus/tsconfig": "workspace:*", - "@std/expect": "jsr:^1.0.19", - "@std/testing": "jsr:^1.0.19", "@types/node": "catalog:", - "typescript": "catalog:" + "typescript": "catalog:", + "vitest": "catalog:" } } diff --git a/packages/notifications/bird-whatsapp/src/index.test.ts b/packages/notifications/bird-whatsapp/src/index.test.ts index ea2039ae9..146f9937f 100644 --- a/packages/notifications/bird-whatsapp/src/index.test.ts +++ b/packages/notifications/bird-whatsapp/src/index.test.ts @@ -1,8 +1,14 @@ import type { Monitor } from "@openstatus/db/src/schema"; import { selectNotificationSchema } from "@openstatus/db/src/schema"; -import { expect } from "@std/expect"; -import { afterEach, beforeEach, describe, test } from "@std/testing/bdd"; -import { assertSpyCalls, type Stub, stub } from "@std/testing/mock"; +import { + afterEach, + beforeEach, + describe, + expect, + test, + vi, + type MockInstance, +} from "vitest"; import { sendAlert, sendDegraded, sendRecovery, sendTest } from "./index"; @@ -27,7 +33,7 @@ function createNotification() { }); } -function parseRequestBody(call: Stub): { +function parseRequestBody(call: MockInstance): { messageRequests: Array<{ receiver: { contacts: Array<{ identifierKey: string; identifierValue: string }>; @@ -40,21 +46,23 @@ function parseRequestBody(call: Stub): { }; }>; } { - const body = call.calls[0].args[1]?.body; + const body = call.mock.calls[0][1]?.body; return JSON.parse(body as string); } describe("WhatsApp Notifications (Bird)", () => { - let fetchMock: Stub; + let fetchMock: MockInstance; beforeEach(() => { - fetchMock = stub(globalThis, "fetch", () => - Promise.resolve(new Response(null, { status: 200 })), - ); + fetchMock = vi + .spyOn(globalThis, "fetch") + .mockImplementation(() => + Promise.resolve(new Response(null, { status: 200 })), + ); }); afterEach(() => { - fetchMock.restore(); + fetchMock.mockRestore(); }); test("sendAlert calls Bird API with alert template", async () => { @@ -66,11 +74,11 @@ describe("WhatsApp Notifications (Bird)", () => { cronTimestamp: Date.now(), }); - assertSpyCalls(fetchMock, 1); - const url = fetchMock.calls[0].args[0] as string; + expect(fetchMock).toHaveBeenCalledTimes(1); + const url = fetchMock.mock.calls[0][0] as string; expect(url).toMatch(BIRD_URL_PATTERN); - const options = fetchMock.calls[0].args[1] as RequestInit; + const options = fetchMock.mock.calls[0][1] as RequestInit; expect(options.headers).toHaveProperty("Authorization"); expect((options.headers as Record).Authorization).toMatch( /^AccessKey /, @@ -107,7 +115,7 @@ describe("WhatsApp Notifications (Bird)", () => { cronTimestamp: Date.now(), }); - assertSpyCalls(fetchMock, 1); + expect(fetchMock).toHaveBeenCalledTimes(1); const body = parseRequestBody(fetchMock); expect(body.messageRequests[0].template.projectId).toBe( "2068001e-330d-4093-8d06-7071e9d34aa1", @@ -129,7 +137,7 @@ describe("WhatsApp Notifications (Bird)", () => { cronTimestamp: Date.now(), }); - assertSpyCalls(fetchMock, 1); + expect(fetchMock).toHaveBeenCalledTimes(1); const body = parseRequestBody(fetchMock); expect(body.messageRequests[0].template.projectId).toBe( "c9a842d9-cf9e-4a13-8cde-024d57e29143", @@ -145,7 +153,7 @@ describe("WhatsApp Notifications (Bird)", () => { test("sendTest calls Bird API with test template and openstat.us URL", async () => { await sendTest({ phoneNumber: "+33623456789" }); - assertSpyCalls(fetchMock, 1); + expect(fetchMock).toHaveBeenCalledTimes(1); const body = parseRequestBody(fetchMock); expect(body.messageRequests[0].receiver.contacts[0].identifierValue).toBe( "+33623456789", diff --git a/packages/notifications/bird-whatsapp/vitest.config.ts b/packages/notifications/bird-whatsapp/vitest.config.ts new file mode 100644 index 000000000..26bdb37da --- /dev/null +++ b/packages/notifications/bird-whatsapp/vitest.config.ts @@ -0,0 +1,3 @@ +import { defineTestConfig } from "@openstatus/test-utils/vitest"; + +export default defineTestConfig(import.meta.dirname); diff --git a/packages/notifications/discord/package.json b/packages/notifications/discord/package.json index 91917cdce..c761eabf8 100644 --- a/packages/notifications/discord/package.json +++ b/packages/notifications/discord/package.json @@ -4,7 +4,7 @@ "main": "src/index.ts", "scripts": { "check": "tsc --noEmit", - "test": "deno test --parallel -A --no-check --sloppy-imports", + "test": "vitest run", "tsc": "tsc" }, "dependencies": { @@ -14,10 +14,10 @@ "zod": "catalog:" }, "devDependencies": { + "@openstatus/test-utils": "workspace:*", "@openstatus/tsconfig": "workspace:*", - "@std/expect": "jsr:^1.0.19", - "@std/testing": "jsr:^1.0.19", "@types/node": "catalog:", - "typescript": "catalog:" + "typescript": "catalog:", + "vitest": "catalog:" } } diff --git a/packages/notifications/discord/src/index.test.ts b/packages/notifications/discord/src/index.test.ts index 72e7baa7e..40cac8ea2 100644 --- a/packages/notifications/discord/src/index.test.ts +++ b/packages/notifications/discord/src/index.test.ts @@ -1,8 +1,14 @@ import { selectNotificationSchema } from "@openstatus/db/src/schema"; import { COLOR_DECIMALS } from "@openstatus/notification-base"; -import { expect } from "@std/expect"; -import { afterEach, beforeEach, describe, test } from "@std/testing/bdd"; -import { assertSpyCalls, type Stub, stub } from "@std/testing/mock"; +import { + afterEach, + beforeEach, + describe, + expect, + test, + vi, + type MockInstance, +} from "vitest"; import { buildAlertEmbed, @@ -20,14 +26,14 @@ const ok = (): Promise => Promise.resolve(new Response(null, { status: 200 })); describe("Discord Notifications", () => { - let fetchMock: Stub; + let fetchMock: MockInstance; beforeEach(() => { - fetchMock = stub(globalThis, "fetch", ok); + fetchMock = vi.spyOn(globalThis, "fetch").mockImplementation(ok); }); afterEach(() => { - fetchMock.restore(); + fetchMock.mockRestore(); }); const createMockMonitor = () => ({ @@ -67,8 +73,8 @@ describe("Discord Notifications", () => { cronTimestamp: Date.now(), }); - assertSpyCalls(fetchMock, 1); - const callArgs = fetchMock.calls[0].args; + expect(fetchMock).toHaveBeenCalledTimes(1); + const callArgs = fetchMock.mock.calls[0]; expect(callArgs[0]).toBe( "https://discord.com/api/webhooks/123456789/abcdefgh", ); @@ -99,8 +105,8 @@ describe("Discord Notifications", () => { cronTimestamp: Date.now(), }); - assertSpyCalls(fetchMock, 1); - const callArgs = fetchMock.calls[0].args; + expect(fetchMock).toHaveBeenCalledTimes(1); + const callArgs = fetchMock.mock.calls[0]; const body = JSON.parse(callArgs[1].body); expect(body.embeds).toBeDefined(); expect(body.embeds[0].title).toContain("is recovered"); @@ -123,8 +129,8 @@ describe("Discord Notifications", () => { cronTimestamp: Date.now(), }); - assertSpyCalls(fetchMock, 1); - const callArgs = fetchMock.calls[0].args; + expect(fetchMock).toHaveBeenCalledTimes(1); + const callArgs = fetchMock.mock.calls[0]; const body = JSON.parse(callArgs[1].body); expect(body.embeds).toBeDefined(); expect(body.embeds[0].title).toContain("is degraded"); @@ -136,8 +142,8 @@ describe("Discord Notifications", () => { const webhookUrl = "https://discord.com/api/webhooks/123456789/abcdefgh"; await sendTestDiscordMessage(webhookUrl); - assertSpyCalls(fetchMock, 1); - const callArgs = fetchMock.calls[0].args; + expect(fetchMock).toHaveBeenCalledTimes(1); + const callArgs = fetchMock.mock.calls[0]; expect(callArgs[0]).toBe(webhookUrl); const body = JSON.parse(callArgs[1].body); expect(body.embeds).toBeDefined(); @@ -145,21 +151,21 @@ describe("Discord Notifications", () => { }); test("Send Test Discord Message with empty webhookUrl", async () => { - expect(sendTestDiscordMessage("")).rejects.toThrow(); - assertSpyCalls(fetchMock, 0); + await expect(sendTestDiscordMessage("")).rejects.toThrow(); + expect(fetchMock).toHaveBeenCalledTimes(0); }); test("Handle fetch error gracefully", async () => { - fetchMock.restore(); - fetchMock = stub(globalThis, "fetch", () => - Promise.reject(new Error("Network error")), - ); + fetchMock.mockRestore(); + fetchMock = vi + .spyOn(globalThis, "fetch") + .mockImplementation(() => Promise.reject(new Error("Network error"))); const monitor = createMockMonitor(); const notification = selectNotificationSchema.parse( createMockNotification(), ); - expect( + await expect( sendAlert({ // @ts-expect-error monitor, diff --git a/packages/notifications/discord/vitest.config.ts b/packages/notifications/discord/vitest.config.ts new file mode 100644 index 000000000..26bdb37da --- /dev/null +++ b/packages/notifications/discord/vitest.config.ts @@ -0,0 +1,3 @@ +import { defineTestConfig } from "@openstatus/test-utils/vitest"; + +export default defineTestConfig(import.meta.dirname); diff --git a/packages/notifications/email/package.json b/packages/notifications/email/package.json index b2c785e5c..e38b3ab22 100644 --- a/packages/notifications/email/package.json +++ b/packages/notifications/email/package.json @@ -5,7 +5,7 @@ "main": "src/index.ts", "scripts": { "check": "tsc --noEmit", - "test": "deno test --parallel -A --no-check --sloppy-imports", + "test": "vitest run", "tsc": "tsc" }, "dependencies": { @@ -25,6 +25,7 @@ "@openstatus/tsconfig": "workspace:*", "@types/node": "catalog:", "@types/react-dom": "catalog:", - "typescript": "catalog:" + "typescript": "catalog:", + "vitest": "catalog:" } } diff --git a/packages/notifications/email/src/index.test.ts b/packages/notifications/email/src/index.test.ts index d567dc17e..e40e420a9 100644 --- a/packages/notifications/email/src/index.test.ts +++ b/packages/notifications/email/src/index.test.ts @@ -3,29 +3,28 @@ import { selectNotificationSchema } from "@openstatus/db/src/schema"; import { EmailClient } from "@openstatus/emails/src/client"; import { afterEach, - assertSpyCalls, beforeEach, describe, expect, - type Stub, - stub, test, -} from "@openstatus/test-utils"; + vi, + type MockInstance, +} from "vitest"; import { sendAlert, sendDegraded, sendRecovery } from "./index"; // biome-ignore lint/suspicious/noExplicitAny: stub over the EmailClient method -let sendMonitorAlertMock: Stub; +let sendMonitorAlertMock: MockInstance; describe("Email Notifications", () => { beforeEach(() => { - sendMonitorAlertMock = stub(EmailClient.prototype, "sendMonitorAlert", () => - Promise.resolve(), - ); + sendMonitorAlertMock = vi + .spyOn(EmailClient.prototype, "sendMonitorAlert") + .mockImplementation(() => Promise.resolve()); }); afterEach(() => { - sendMonitorAlertMock.restore(); + sendMonitorAlertMock.mockRestore(); }); const createMockMonitor = () => ({ @@ -67,8 +66,8 @@ describe("Email Notifications", () => { cronTimestamp: Date.now(), }); - assertSpyCalls(sendMonitorAlertMock, 1); - const callArgs = sendMonitorAlertMock.calls[0].args[0]; + expect(sendMonitorAlertMock).toHaveBeenCalledTimes(1); + const callArgs = sendMonitorAlertMock.mock.calls[0][0]; expect(callArgs.name).toBe("API Health Check"); expect(callArgs.type).toBe("alert"); expect(callArgs.to).toBe("ping@openstatus.dev"); @@ -94,8 +93,8 @@ describe("Email Notifications", () => { cronTimestamp: Date.now(), }); - assertSpyCalls(sendMonitorAlertMock, 1); - const callArgs = sendMonitorAlertMock.calls[0].args[0]; + expect(sendMonitorAlertMock).toHaveBeenCalledTimes(1); + const callArgs = sendMonitorAlertMock.mock.calls[0][0]; expect(callArgs.status).toBeUndefined(); expect(callArgs.latency).toBe("N/A"); expect(callArgs.region).toBe("N/A"); @@ -117,8 +116,8 @@ describe("Email Notifications", () => { cronTimestamp: Date.now(), }); - assertSpyCalls(sendMonitorAlertMock, 1); - const callArgs = sendMonitorAlertMock.calls[0].args[0]; + expect(sendMonitorAlertMock).toHaveBeenCalledTimes(1); + const callArgs = sendMonitorAlertMock.mock.calls[0][0]; expect(callArgs.type).toBe("recovery"); expect(callArgs.name).toBe("API Health Check"); expect(callArgs.to).toBe("ping@openstatus.dev"); @@ -143,8 +142,8 @@ describe("Email Notifications", () => { cronTimestamp: Date.now(), }); - assertSpyCalls(sendMonitorAlertMock, 1); - const callArgs = sendMonitorAlertMock.calls[0].args[0]; + expect(sendMonitorAlertMock).toHaveBeenCalledTimes(1); + const callArgs = sendMonitorAlertMock.mock.calls[0][0]; expect(callArgs.type).toBe("degraded"); expect(callArgs.name).toBe("API Health Check"); expect(callArgs.status).toBe("503"); @@ -170,8 +169,8 @@ describe("Email Notifications", () => { incident: { startedAt }, }); - assertSpyCalls(sendMonitorAlertMock, 1); - const callArgs = sendMonitorAlertMock.calls[0].args[0]; + expect(sendMonitorAlertMock).toHaveBeenCalledTimes(1); + const callArgs = sendMonitorAlertMock.mock.calls[0][0]; expect(callArgs.degradedAfter).toBe(250); expect(callArgs.firstSeen).toBe(startedAt.toISOString()); }); @@ -196,6 +195,6 @@ describe("Email Notifications", () => { }); // Should not call sendMonitorAlert when data is invalid - assertSpyCalls(sendMonitorAlertMock, 0); + expect(sendMonitorAlertMock).toHaveBeenCalledTimes(0); }); }); diff --git a/packages/notifications/email/src/test-preload.ts b/packages/notifications/email/src/test-preload.ts index 3e4c5cf31..e7cc1bb4a 100644 --- a/packages/notifications/email/src/test-preload.ts +++ b/packages/notifications/email/src/test-preload.ts @@ -2,8 +2,7 @@ * Test env setup. Import this FIRST so env vars are set before module-level * env validations fire on import. * - * Bun's test runner set NODE_ENV=test automatically; Deno does not. This - * package's env skips validation when NODE_ENV=test, and @openstatus/emails + * This package's env skips validation when NODE_ENV=test, and @openstatus/emails * validates RESEND_API_KEY at module load. */ process.env.NODE_ENV = "test"; diff --git a/packages/notifications/email/vitest.config.ts b/packages/notifications/email/vitest.config.ts new file mode 100644 index 000000000..26bdb37da --- /dev/null +++ b/packages/notifications/email/vitest.config.ts @@ -0,0 +1,3 @@ +import { defineTestConfig } from "@openstatus/test-utils/vitest"; + +export default defineTestConfig(import.meta.dirname); diff --git a/packages/notifications/google-chat/package.json b/packages/notifications/google-chat/package.json index 5fd6df27d..14477c5d0 100644 --- a/packages/notifications/google-chat/package.json +++ b/packages/notifications/google-chat/package.json @@ -4,7 +4,7 @@ "main": "src/index.ts", "scripts": { "check": "tsc --noEmit", - "test": "deno test --parallel -A --no-check --sloppy-imports", + "test": "vitest run", "tsc": "tsc" }, "dependencies": { @@ -14,10 +14,10 @@ "zod": "catalog:" }, "devDependencies": { + "@openstatus/test-utils": "workspace:*", "@openstatus/tsconfig": "workspace:*", - "@std/expect": "jsr:^1.0.19", - "@std/testing": "jsr:^1.0.19", "@types/node": "catalog:", - "typescript": "catalog:" + "typescript": "catalog:", + "vitest": "catalog:" } } diff --git a/packages/notifications/google-chat/src/index.test.ts b/packages/notifications/google-chat/src/index.test.ts index 6a84bffdd..909d82904 100644 --- a/packages/notifications/google-chat/src/index.test.ts +++ b/packages/notifications/google-chat/src/index.test.ts @@ -1,21 +1,29 @@ import { selectNotificationSchema } from "@openstatus/db/src/schema"; -import { expect } from "@std/expect"; -import { afterEach, beforeEach, describe, test } from "@std/testing/bdd"; -import { assertSpyCalls, stub, type Stub } from "@std/testing/mock"; +import { + afterEach, + beforeEach, + describe, + expect, + test, + vi, + type MockInstance, +} from "vitest"; import { sendAlert, sendDegraded, sendRecovery, sendTest } from "./index"; describe("Google Chat Notifications", () => { - let fetchMock: Stub; + let fetchMock: MockInstance; beforeEach(() => { - fetchMock = stub(globalThis, "fetch", () => - Promise.resolve(new Response(null, { status: 200 })), - ); + fetchMock = vi + .spyOn(globalThis, "fetch") + .mockImplementation(() => + Promise.resolve(new Response(null, { status: 200 })), + ); }); afterEach(() => { - fetchMock.restore(); + fetchMock.mockRestore(); }); const createMockMonitor = () => ({ @@ -55,8 +63,8 @@ describe("Google Chat Notifications", () => { cronTimestamp: Date.now(), }); - assertSpyCalls(fetchMock, 1); - const callArgs = fetchMock.calls[0].args; + expect(fetchMock).toHaveBeenCalledTimes(1); + const callArgs = fetchMock.mock.calls[0]; expect(callArgs[0]).toBe( "https://google.com/api/webhooks/123456789/abcdefgh", ); @@ -85,8 +93,8 @@ describe("Google Chat Notifications", () => { cronTimestamp: Date.now(), }); - assertSpyCalls(fetchMock, 1); - const callArgs = fetchMock.calls[0].args; + expect(fetchMock).toHaveBeenCalledTimes(1); + const callArgs = fetchMock.mock.calls[0]; const body = JSON.parse(callArgs[1].body); expect(body.text).toContain("✅ Recovered"); expect(body.text).toContain("API Health Check"); @@ -107,8 +115,8 @@ describe("Google Chat Notifications", () => { cronTimestamp: Date.now(), }); - assertSpyCalls(fetchMock, 1); - const callArgs = fetchMock.calls[0].args; + expect(fetchMock).toHaveBeenCalledTimes(1); + const callArgs = fetchMock.mock.calls[0]; const body = JSON.parse(callArgs[1].body); expect(body.text).toContain("⚠️ Degraded"); expect(body.text).toContain("API Health Check"); @@ -120,8 +128,8 @@ describe("Google Chat Notifications", () => { const result = await sendTest(webhookUrl); expect(result).toBe(true); - assertSpyCalls(fetchMock, 1); - const callArgs = fetchMock.calls[0].args; + expect(fetchMock).toHaveBeenCalledTimes(1); + const callArgs = fetchMock.mock.calls[0]; expect(callArgs[0]).toBe(webhookUrl); const body = JSON.parse(callArgs[1].body); expect(body.text).toContain("🧪 Test"); @@ -132,20 +140,20 @@ describe("Google Chat Notifications", () => { const result = await sendTest(""); expect(result).toBe(false); - assertSpyCalls(fetchMock, 0); + expect(fetchMock).toHaveBeenCalledTimes(0); }); test("Handle fetch error gracefully", async () => { - fetchMock.restore(); - fetchMock = stub(globalThis, "fetch", () => - Promise.reject(new Error("Network error")), - ); + fetchMock.mockRestore(); + fetchMock = vi + .spyOn(globalThis, "fetch") + .mockImplementation(() => Promise.reject(new Error("Network error"))); const monitor = createMockMonitor(); const notification = selectNotificationSchema.parse( createMockNotification(), ); - expect( + await expect( sendAlert({ // @ts-expect-error monitor, diff --git a/packages/notifications/google-chat/vitest.config.ts b/packages/notifications/google-chat/vitest.config.ts new file mode 100644 index 000000000..26bdb37da --- /dev/null +++ b/packages/notifications/google-chat/vitest.config.ts @@ -0,0 +1,3 @@ +import { defineTestConfig } from "@openstatus/test-utils/vitest"; + +export default defineTestConfig(import.meta.dirname); diff --git a/packages/notifications/grafana-oncall/package.json b/packages/notifications/grafana-oncall/package.json index 137d8931e..e220799f6 100644 --- a/packages/notifications/grafana-oncall/package.json +++ b/packages/notifications/grafana-oncall/package.json @@ -4,7 +4,7 @@ "main": "src/index.ts", "scripts": { "check": "tsc --noEmit", - "test": "deno test --parallel -A --no-check --sloppy-imports", + "test": "vitest run", "tsc": "tsc" }, "dependencies": { @@ -14,10 +14,10 @@ "zod": "catalog:" }, "devDependencies": { + "@openstatus/test-utils": "workspace:*", "@openstatus/tsconfig": "workspace:*", - "@std/expect": "jsr:^1.0.19", - "@std/testing": "jsr:^1.0.19", "@types/node": "catalog:", - "typescript": "catalog:" + "typescript": "catalog:", + "vitest": "catalog:" } } diff --git a/packages/notifications/grafana-oncall/src/index.test.ts b/packages/notifications/grafana-oncall/src/index.test.ts index 5676f5c72..c0a5512a2 100644 --- a/packages/notifications/grafana-oncall/src/index.test.ts +++ b/packages/notifications/grafana-oncall/src/index.test.ts @@ -1,21 +1,29 @@ import { selectNotificationSchema } from "@openstatus/db/src/schema"; -import { expect } from "@std/expect"; -import { afterEach, beforeEach, describe, test } from "@std/testing/bdd"; -import { assertSpyCalls, stub, type Stub } from "@std/testing/mock"; +import { + afterEach, + beforeEach, + describe, + expect, + test, + vi, + type MockInstance, +} from "vitest"; import { sendAlert, sendDegraded, sendRecovery, sendTest } from "./index"; describe("Grafana OnCall Notifications", () => { - let fetchMock: Stub; + let fetchMock: MockInstance; beforeEach(() => { - fetchMock = stub(globalThis, "fetch", () => - Promise.resolve(new Response(null, { status: 200 })), - ); + fetchMock = vi + .spyOn(globalThis, "fetch") + .mockImplementation(() => + Promise.resolve(new Response(null, { status: 200 })), + ); }); afterEach(() => { - fetchMock.restore(); + fetchMock.mockRestore(); }); const createMockMonitor = () => ({ @@ -55,8 +63,8 @@ describe("Grafana OnCall Notifications", () => { cronTimestamp: Date.now(), }); - assertSpyCalls(fetchMock, 1); - const callArgs = fetchMock.calls[0].args; + expect(fetchMock).toHaveBeenCalledTimes(1); + const callArgs = fetchMock.mock.calls[0]; expect(callArgs[0]).toBe( "https://oncall.example.com/integrations/v1/webhook/abc123", ); @@ -87,8 +95,8 @@ describe("Grafana OnCall Notifications", () => { cronTimestamp: Date.now(), }); - assertSpyCalls(fetchMock, 1); - const callArgs = fetchMock.calls[0].args; + expect(fetchMock).toHaveBeenCalledTimes(1); + const callArgs = fetchMock.mock.calls[0]; const body = JSON.parse(callArgs[1].body); expect(body.message).toBe("Status code: 503"); }); @@ -108,8 +116,8 @@ describe("Grafana OnCall Notifications", () => { cronTimestamp: Date.now(), }); - assertSpyCalls(fetchMock, 1); - const callArgs = fetchMock.calls[0].args; + expect(fetchMock).toHaveBeenCalledTimes(1); + const callArgs = fetchMock.mock.calls[0]; expect(callArgs[0]).toBe( "https://oncall.example.com/integrations/v1/webhook/abc123", ); @@ -136,8 +144,8 @@ describe("Grafana OnCall Notifications", () => { cronTimestamp: Date.now(), }); - assertSpyCalls(fetchMock, 1); - const callArgs = fetchMock.calls[0].args; + expect(fetchMock).toHaveBeenCalledTimes(1); + const callArgs = fetchMock.mock.calls[0]; expect(callArgs[0]).toBe( "https://oncall.example.com/integrations/v1/webhook/abc123", ); @@ -156,8 +164,8 @@ describe("Grafana OnCall Notifications", () => { const result = await sendTest({ webhookUrl }); expect(result).toBe(true); - assertSpyCalls(fetchMock, 1); - const callArgs = fetchMock.calls[0].args; + expect(fetchMock).toHaveBeenCalledTimes(1); + const callArgs = fetchMock.mock.calls[0]; expect(callArgs[0]).toBe(webhookUrl); expect(callArgs[1].method).toBe("POST"); @@ -170,36 +178,38 @@ describe("Grafana OnCall Notifications", () => { }); test("Send Test returns false on network error", async () => { - fetchMock.restore(); - fetchMock = stub(globalThis, "fetch", () => - Promise.reject(new Error("Network error")), - ); + fetchMock.mockRestore(); + fetchMock = vi + .spyOn(globalThis, "fetch") + .mockImplementation(() => Promise.reject(new Error("Network error"))); const result = await sendTest({ webhookUrl: "https://oncall.example.com/integrations/v1/webhook/test123", }); expect(result).toBe(false); - assertSpyCalls(fetchMock, 1); + expect(fetchMock).toHaveBeenCalledTimes(1); }); test("Send Test returns false on non-ok response", async () => { - fetchMock.restore(); - fetchMock = stub(globalThis, "fetch", () => - Promise.resolve(new Response(null, { status: 401 })), - ); + fetchMock.mockRestore(); + fetchMock = vi + .spyOn(globalThis, "fetch") + .mockImplementation(() => + Promise.resolve(new Response(null, { status: 401 })), + ); const result = await sendTest({ webhookUrl: "https://oncall.example.com/integrations/v1/webhook/test123", }); expect(result).toBe(false); - assertSpyCalls(fetchMock, 1); + expect(fetchMock).toHaveBeenCalledTimes(1); }); test("Send Alert throws on non-ok response", async () => { - fetchMock.restore(); - fetchMock = stub(globalThis, "fetch", () => + fetchMock.mockRestore(); + fetchMock = vi.spyOn(globalThis, "fetch").mockImplementation(() => Promise.resolve( new Response(null, { status: 500, @@ -213,7 +223,7 @@ describe("Grafana OnCall Notifications", () => { createMockNotification(), ); - expect( + await expect( sendAlert({ // @ts-expect-error monitor, @@ -226,10 +236,10 @@ describe("Grafana OnCall Notifications", () => { }); test("Handle fetch error gracefully", async () => { - fetchMock.restore(); - fetchMock = stub(globalThis, "fetch", () => - Promise.reject(new Error("Network error")), - ); + fetchMock.mockRestore(); + fetchMock = vi + .spyOn(globalThis, "fetch") + .mockImplementation(() => Promise.reject(new Error("Network error"))); const monitor = createMockMonitor(); const notification = selectNotificationSchema.parse( @@ -247,6 +257,6 @@ describe("Grafana OnCall Notifications", () => { }), ).rejects.toThrow(); - assertSpyCalls(fetchMock, 1); + expect(fetchMock).toHaveBeenCalledTimes(1); }); }); diff --git a/packages/notifications/grafana-oncall/vitest.config.ts b/packages/notifications/grafana-oncall/vitest.config.ts new file mode 100644 index 000000000..26bdb37da --- /dev/null +++ b/packages/notifications/grafana-oncall/vitest.config.ts @@ -0,0 +1,3 @@ +import { defineTestConfig } from "@openstatus/test-utils/vitest"; + +export default defineTestConfig(import.meta.dirname); diff --git a/packages/notifications/ms-teams/package.json b/packages/notifications/ms-teams/package.json index 54f5a0eb9..5675dda39 100644 --- a/packages/notifications/ms-teams/package.json +++ b/packages/notifications/ms-teams/package.json @@ -4,7 +4,7 @@ "main": "src/index.ts", "scripts": { "check": "tsc --noEmit", - "test": "deno test --parallel -A --no-check --sloppy-imports", + "test": "vitest run", "tsc": "tsc" }, "dependencies": { @@ -14,10 +14,10 @@ "zod": "catalog:" }, "devDependencies": { + "@openstatus/test-utils": "workspace:*", "@openstatus/tsconfig": "workspace:*", - "@std/expect": "jsr:^1.0.19", - "@std/testing": "jsr:^1.0.19", "@types/node": "catalog:", - "typescript": "catalog:" + "typescript": "catalog:", + "vitest": "catalog:" } } diff --git a/packages/notifications/ms-teams/src/index.test.ts b/packages/notifications/ms-teams/src/index.test.ts index 18b41f49b..1badbbd4e 100644 --- a/packages/notifications/ms-teams/src/index.test.ts +++ b/packages/notifications/ms-teams/src/index.test.ts @@ -1,12 +1,18 @@ import type { MonitorIncident, Monitor } from "@openstatus/db/src/schema"; import { selectNotificationSchema } from "@openstatus/db/src/schema"; -import { expect } from "@std/expect"; -import { afterEach, beforeEach, describe, test } from "@std/testing/bdd"; -import { assertSpyCalls, stub, type Stub } from "@std/testing/mock"; +import { + afterEach, + beforeEach, + describe, + expect, + test, + vi, + type MockInstance, +} from "vitest"; import { sendAlert, sendDegraded, sendRecovery, sendTest } from "./index"; -type FetchMock = Stub; +type FetchMock = MockInstance; type AdaptiveCardElement = { type: string; @@ -42,13 +48,15 @@ describe("Microsoft Teams Notifications", () => { let fetchMock: FetchMock; beforeEach(() => { - fetchMock = stub(globalThis, "fetch", () => - Promise.resolve(new Response(null, { status: 200 })), - ); + fetchMock = vi + .spyOn(globalThis, "fetch") + .mockImplementation(() => + Promise.resolve(new Response(null, { status: 200 })), + ); }); afterEach(() => { - fetchMock.restore(); + fetchMock.mockRestore(); }); const createMockMonitor = (): Monitor => ({ @@ -116,8 +124,7 @@ describe("Microsoft Teams Notifications", () => { }); const readMessagePayload = (): MessagePayload => { - const calls = fetchMock.calls; - const init = calls[0].args[1]; + const init = fetchMock.mock.calls[0]?.[1]; if (!init || typeof init.body !== "string") { throw new Error("expected string body"); } @@ -171,8 +178,8 @@ describe("Microsoft Teams Notifications", () => { regions: ["iad"], }); - assertSpyCalls(fetchMock, 1); - const [url, init] = fetchMock.calls[0].args; + expect(fetchMock).toHaveBeenCalledTimes(1); + const [url, init] = fetchMock.mock.calls[0]; expect(String(url)).toContain("logic.azure.com"); expect(init?.method).toBe("POST"); @@ -304,7 +311,7 @@ describe("Microsoft Teams Notifications", () => { "https://prod-00.westeurope.logic.azure.com:443/workflows/test/triggers/manual/paths/invoke", }); - assertSpyCalls(fetchMock, 1); + expect(fetchMock).toHaveBeenCalledTimes(1); const card = getCard(readMessagePayload()); const header = findContainerByStyle(card, "good"); expect(header?.items?.[0]?.text).toBe("Test notification"); @@ -313,14 +320,14 @@ describe("Microsoft Teams Notifications", () => { test("Send Test with empty webhookUrl", async () => { await expect(sendTest({ webhookUrl: "" })).rejects.toThrow(); - assertSpyCalls(fetchMock, 0); + expect(fetchMock).toHaveBeenCalledTimes(0); }); test("Handle fetch error", async () => { - fetchMock.restore(); - fetchMock = stub(globalThis, "fetch", () => - Promise.reject(new Error("Network error")), - ); + fetchMock.mockRestore(); + fetchMock = vi + .spyOn(globalThis, "fetch") + .mockImplementation(() => Promise.reject(new Error("Network error"))); const monitor = createMockMonitor(); const notification = selectNotificationSchema.parse( diff --git a/packages/notifications/ms-teams/vitest.config.ts b/packages/notifications/ms-teams/vitest.config.ts new file mode 100644 index 000000000..26bdb37da --- /dev/null +++ b/packages/notifications/ms-teams/vitest.config.ts @@ -0,0 +1,3 @@ +import { defineTestConfig } from "@openstatus/test-utils/vitest"; + +export default defineTestConfig(import.meta.dirname); diff --git a/packages/notifications/ntfy/package.json b/packages/notifications/ntfy/package.json index 68637e3d8..d7f88b0ab 100644 --- a/packages/notifications/ntfy/package.json +++ b/packages/notifications/ntfy/package.json @@ -4,7 +4,7 @@ "main": "src/index.ts", "scripts": { "check": "tsc --noEmit", - "test": "deno test --parallel -A --no-check --sloppy-imports", + "test": "vitest run", "tsc": "tsc" }, "dependencies": { @@ -14,10 +14,10 @@ "zod": "catalog:" }, "devDependencies": { + "@openstatus/test-utils": "workspace:*", "@openstatus/tsconfig": "workspace:*", - "@std/expect": "jsr:^1.0.19", - "@std/testing": "jsr:^1.0.19", "@types/node": "catalog:", - "typescript": "catalog:" + "typescript": "catalog:", + "vitest": "catalog:" } } diff --git a/packages/notifications/ntfy/src/index.test.ts b/packages/notifications/ntfy/src/index.test.ts index a46f9284c..c10aae59f 100644 --- a/packages/notifications/ntfy/src/index.test.ts +++ b/packages/notifications/ntfy/src/index.test.ts @@ -1,21 +1,29 @@ import { selectNotificationSchema } from "@openstatus/db/src/schema"; -import { expect } from "@std/expect"; -import { afterEach, beforeEach, describe, test } from "@std/testing/bdd"; -import { assertSpyCalls, stub, type Stub } from "@std/testing/mock"; +import { + afterEach, + beforeEach, + describe, + expect, + test, + vi, + type MockInstance, +} from "vitest"; import { sendAlert, sendDegraded, sendRecovery, sendTest } from "./index"; describe("Ntfy Notifications", () => { - let fetchMock: Stub; + let fetchMock: MockInstance; beforeEach(() => { - fetchMock = stub(globalThis, "fetch", () => - Promise.resolve(new Response(null, { status: 200 })), - ); + fetchMock = vi + .spyOn(globalThis, "fetch") + .mockImplementation(() => + Promise.resolve(new Response(null, { status: 200 })), + ); }); afterEach(() => { - fetchMock.restore(); + fetchMock.mockRestore(); }); const createMockMonitor = () => ({ @@ -70,8 +78,8 @@ describe("Ntfy Notifications", () => { cronTimestamp: Date.now(), }); - assertSpyCalls(fetchMock, 1); - const callArgs = fetchMock.calls[0].args; + expect(fetchMock).toHaveBeenCalledTimes(1); + const callArgs = fetchMock.mock.calls[0]; expect(callArgs[0]).toBe("https://ntfy.sh/my-topic"); expect(callArgs[1].method).toBe("post"); expect(callArgs[1].body).toContain("API Health Check"); @@ -94,8 +102,8 @@ describe("Ntfy Notifications", () => { cronTimestamp: Date.now(), }); - assertSpyCalls(fetchMock, 1); - const callArgs = fetchMock.calls[0].args; + expect(fetchMock).toHaveBeenCalledTimes(1); + const callArgs = fetchMock.mock.calls[0]; expect(callArgs[0]).toBe("https://ntfy.example.com/my-topic"); }); @@ -114,8 +122,8 @@ describe("Ntfy Notifications", () => { cronTimestamp: Date.now(), }); - assertSpyCalls(fetchMock, 1); - const callArgs = fetchMock.calls[0].args; + expect(fetchMock).toHaveBeenCalledTimes(1); + const callArgs = fetchMock.mock.calls[0]; expect(callArgs[1].headers.Authorization).toBe("Bearer test-token-123"); }); @@ -133,8 +141,8 @@ describe("Ntfy Notifications", () => { cronTimestamp: Date.now(), }); - assertSpyCalls(fetchMock, 1); - const callArgs = fetchMock.calls[0].args; + expect(fetchMock).toHaveBeenCalledTimes(1); + const callArgs = fetchMock.mock.calls[0]; expect(callArgs[1].body).toContain("error: Connection timeout"); }); @@ -153,8 +161,8 @@ describe("Ntfy Notifications", () => { cronTimestamp: Date.now(), }); - assertSpyCalls(fetchMock, 1); - const callArgs = fetchMock.calls[0].args; + expect(fetchMock).toHaveBeenCalledTimes(1); + const callArgs = fetchMock.mock.calls[0]; expect(callArgs[1].body).toContain("is up again"); }); @@ -171,8 +179,8 @@ describe("Ntfy Notifications", () => { cronTimestamp: Date.now(), }); - assertSpyCalls(fetchMock, 1); - const callArgs = fetchMock.calls[0].args; + expect(fetchMock).toHaveBeenCalledTimes(1); + const callArgs = fetchMock.mock.calls[0]; expect(callArgs[1].headers.Authorization).toBe("Bearer test-token-123"); }); @@ -191,8 +199,8 @@ describe("Ntfy Notifications", () => { cronTimestamp: Date.now(), }); - assertSpyCalls(fetchMock, 1); - const callArgs = fetchMock.calls[0].args; + expect(fetchMock).toHaveBeenCalledTimes(1); + const callArgs = fetchMock.mock.calls[0]; expect(callArgs[1].body).toContain("is degraded"); }); @@ -202,8 +210,8 @@ describe("Ntfy Notifications", () => { }); expect(result).toBe(true); - assertSpyCalls(fetchMock, 1); - const callArgs = fetchMock.calls[0].args; + expect(fetchMock).toHaveBeenCalledTimes(1); + const callArgs = fetchMock.mock.calls[0]; expect(callArgs[0]).toBe("https://ntfy.sh/test-topic"); expect(callArgs[1].body).toBe("This is a test message from OpenStatus"); expect(callArgs[1].headers).not.toHaveProperty("Authorization"); @@ -216,8 +224,8 @@ describe("Ntfy Notifications", () => { }); expect(result).toBe(true); - assertSpyCalls(fetchMock, 1); - const callArgs = fetchMock.calls[0].args; + expect(fetchMock).toHaveBeenCalledTimes(1); + const callArgs = fetchMock.mock.calls[0]; expect(callArgs[0]).toBe("https://ntfy.example.com/test-topic"); }); @@ -228,16 +236,16 @@ describe("Ntfy Notifications", () => { }); expect(result).toBe(true); - assertSpyCalls(fetchMock, 1); - const callArgs = fetchMock.calls[0].args; + expect(fetchMock).toHaveBeenCalledTimes(1); + const callArgs = fetchMock.mock.calls[0]; expect(callArgs[1].headers.Authorization).toBe("Bearer test-token"); }); test("Handle fetch error gracefully", async () => { - fetchMock.restore(); - fetchMock = stub(globalThis, "fetch", () => - Promise.reject(new Error("Network error")), - ); + fetchMock.mockRestore(); + fetchMock = vi + .spyOn(globalThis, "fetch") + .mockImplementation(() => Promise.reject(new Error("Network error"))); const monitor = createMockMonitor(); const notification = selectNotificationSchema.parse( @@ -256,20 +264,20 @@ describe("Ntfy Notifications", () => { }), ).rejects.toThrow(); - assertSpyCalls(fetchMock, 1); + expect(fetchMock).toHaveBeenCalledTimes(1); }); test("Send Test returns false on error", async () => { - fetchMock.restore(); - fetchMock = stub(globalThis, "fetch", () => - Promise.reject(new Error("Network error")), - ); + fetchMock.mockRestore(); + fetchMock = vi + .spyOn(globalThis, "fetch") + .mockImplementation(() => Promise.reject(new Error("Network error"))); const result = await sendTest({ topic: "test-topic", }); expect(result).toBe(false); - assertSpyCalls(fetchMock, 1); + expect(fetchMock).toHaveBeenCalledTimes(1); }); }); diff --git a/packages/notifications/ntfy/vitest.config.ts b/packages/notifications/ntfy/vitest.config.ts new file mode 100644 index 000000000..26bdb37da --- /dev/null +++ b/packages/notifications/ntfy/vitest.config.ts @@ -0,0 +1,3 @@ +import { defineTestConfig } from "@openstatus/test-utils/vitest"; + +export default defineTestConfig(import.meta.dirname); diff --git a/packages/notifications/opsgenie/package.json b/packages/notifications/opsgenie/package.json index fee08931d..1f4f4aa34 100644 --- a/packages/notifications/opsgenie/package.json +++ b/packages/notifications/opsgenie/package.json @@ -4,7 +4,7 @@ "main": "src/index.ts", "scripts": { "check": "tsc --noEmit", - "test": "deno test --parallel -A --no-check --sloppy-imports", + "test": "vitest run", "tsc": "tsc" }, "dependencies": { @@ -16,12 +16,12 @@ "zod": "catalog:" }, "devDependencies": { + "@openstatus/test-utils": "workspace:*", "@openstatus/tsconfig": "workspace:*", - "@std/expect": "jsr:^1.0.19", - "@std/testing": "jsr:^1.0.19", "@types/node": "catalog:", "@types/react": "catalog:", "@types/react-dom": "catalog:", - "typescript": "catalog:" + "typescript": "catalog:", + "vitest": "catalog:" } } diff --git a/packages/notifications/opsgenie/src/index.test.ts b/packages/notifications/opsgenie/src/index.test.ts index 67712e534..a85480823 100644 --- a/packages/notifications/opsgenie/src/index.test.ts +++ b/packages/notifications/opsgenie/src/index.test.ts @@ -1,21 +1,29 @@ import { selectNotificationSchema } from "@openstatus/db/src/schema"; -import { expect } from "@std/expect"; -import { afterEach, beforeEach, describe, test } from "@std/testing/bdd"; -import { assertSpyCalls, type Stub, stub } from "@std/testing/mock"; +import { + afterEach, + beforeEach, + describe, + expect, + test, + vi, + type MockInstance, +} from "vitest"; import { sendAlert, sendDegraded, sendRecovery, sendTest } from "./index"; describe("OpsGenie Notifications", () => { - let fetchMock: Stub; + let fetchMock: MockInstance; beforeEach(() => { - fetchMock = stub(globalThis, "fetch", () => - Promise.resolve(new Response(null, { status: 200 })), - ); + fetchMock = vi + .spyOn(globalThis, "fetch") + .mockImplementation(() => + Promise.resolve(new Response(null, { status: 200 })), + ); }); afterEach(() => { - fetchMock.restore(); + fetchMock.mockRestore(); }); const createMockMonitor = () => ({ @@ -73,8 +81,8 @@ describe("OpsGenie Notifications", () => { cronTimestamp: Date.now(), }); - assertSpyCalls(fetchMock, 1); - const callArgs = fetchMock.calls[0].args; + expect(fetchMock).toHaveBeenCalledTimes(1); + const callArgs = fetchMock.mock.calls[0]; expect(callArgs[0]).toBe("https://api.opsgenie.com/v2/alerts"); expect(callArgs[1].method).toBe("POST"); expect(callArgs[1].headers["Content-Type"]).toBe("application/json"); @@ -105,8 +113,8 @@ describe("OpsGenie Notifications", () => { cronTimestamp: Date.now(), }); - assertSpyCalls(fetchMock, 1); - const callArgs = fetchMock.calls[0].args; + expect(fetchMock).toHaveBeenCalledTimes(1); + const callArgs = fetchMock.mock.calls[0]; expect(callArgs[0]).toBe("https://api.eu.opsgenie.com/v2/alerts"); }); @@ -127,8 +135,8 @@ describe("OpsGenie Notifications", () => { cronTimestamp: Date.now(), }); - assertSpyCalls(fetchMock, 1); - const callArgs = fetchMock.calls[0].args; + expect(fetchMock).toHaveBeenCalledTimes(1); + const callArgs = fetchMock.mock.calls[0]; const body = JSON.parse(callArgs[1].body); expect(body.details.severity).toBe("degraded"); expect(body.message).toBe("API Health Check is degraded"); @@ -147,10 +155,12 @@ describe("OpsGenie Notifications", () => { }, ] as const) { test(`Recovery closes the monitor alias in ${region}`, async () => { - fetchMock.restore(); - fetchMock = stub(globalThis, "fetch", () => - Promise.resolve(new Response(null, { status: 202 })), - ); + fetchMock.mockRestore(); + fetchMock = vi + .spyOn(globalThis, "fetch") + .mockImplementation(() => + Promise.resolve(new Response(null, { status: 202 })), + ); await sendRecovery({ // @ts-expect-error @@ -163,8 +173,8 @@ describe("OpsGenie Notifications", () => { cronTimestamp: Date.now(), }); - assertSpyCalls(fetchMock, 1); - const [requestUrl, options] = fetchMock.calls[0].args; + expect(fetchMock).toHaveBeenCalledTimes(1); + const [requestUrl, options] = fetchMock.mock.calls[0]; expect(requestUrl).toBe(url); expect(options.method).toBe("POST"); expect(options.headers["Content-Type"]).toBe("application/json"); @@ -174,10 +184,12 @@ describe("OpsGenie Notifications", () => { } test("Recovery rejects an unsuccessful close response", async () => { - fetchMock.restore(); - fetchMock = stub(globalThis, "fetch", () => - Promise.resolve(new Response(null, { status: 503 })), - ); + fetchMock.mockRestore(); + fetchMock = vi + .spyOn(globalThis, "fetch") + .mockImplementation(() => + Promise.resolve(new Response(null, { status: 503 })), + ); await expect( sendRecovery({ @@ -188,13 +200,15 @@ describe("OpsGenie Notifications", () => { cronTimestamp: Date.now(), }), ).rejects.toThrow("503"); - assertSpyCalls(fetchMock, 1); + expect(fetchMock).toHaveBeenCalledTimes(1); }); test("Recovery propagates a rejected close request", async () => { const error = new Error("Connection reset"); - fetchMock.restore(); - fetchMock = stub(globalThis, "fetch", () => Promise.reject(error)); + fetchMock.mockRestore(); + fetchMock = vi + .spyOn(globalThis, "fetch") + .mockImplementation(() => Promise.reject(error)); await expect( sendRecovery({ @@ -205,21 +219,21 @@ describe("OpsGenie Notifications", () => { cronTimestamp: Date.now(), }), ).rejects.toBe(error); - assertSpyCalls(fetchMock, 1); + expect(fetchMock).toHaveBeenCalledTimes(1); }); test("Handle fetch error gracefully", async () => { - fetchMock.restore(); - fetchMock = stub(globalThis, "fetch", () => - Promise.reject(new Error("Network error")), - ); + fetchMock.mockRestore(); + fetchMock = vi + .spyOn(globalThis, "fetch") + .mockImplementation(() => Promise.reject(new Error("Network error"))); const monitor = createMockMonitor(); const notification = selectNotificationSchema.parse( createMockNotification(), ); const incident = createMockIncident(); - expect( + await expect( sendAlert({ // @ts-expect-error monitor, @@ -232,14 +246,14 @@ describe("OpsGenie Notifications", () => { }), ).rejects.toThrow(); - assertSpyCalls(fetchMock, 1); + expect(fetchMock).toHaveBeenCalledTimes(1); }); test("Send Test returns false on error", async () => { - fetchMock.restore(); - fetchMock = stub(globalThis, "fetch", () => - Promise.reject(new Error("Network error")), - ); + fetchMock.mockRestore(); + fetchMock = vi + .spyOn(globalThis, "fetch") + .mockImplementation(() => Promise.reject(new Error("Network error"))); const result = await sendTest({ apiKey: "test-api-key", @@ -247,6 +261,6 @@ describe("OpsGenie Notifications", () => { }); expect(result).toBe(false); - assertSpyCalls(fetchMock, 1); + expect(fetchMock).toHaveBeenCalledTimes(1); }); }); diff --git a/packages/notifications/opsgenie/vitest.config.ts b/packages/notifications/opsgenie/vitest.config.ts new file mode 100644 index 000000000..26bdb37da --- /dev/null +++ b/packages/notifications/opsgenie/vitest.config.ts @@ -0,0 +1,3 @@ +import { defineTestConfig } from "@openstatus/test-utils/vitest"; + +export default defineTestConfig(import.meta.dirname); diff --git a/packages/notifications/pagerduty/package.json b/packages/notifications/pagerduty/package.json index 080b9d03e..36ae8fe55 100644 --- a/packages/notifications/pagerduty/package.json +++ b/packages/notifications/pagerduty/package.json @@ -4,7 +4,7 @@ "main": "src/index.ts", "scripts": { "check": "tsc --noEmit", - "test": "deno test --parallel -A --no-check --sloppy-imports", + "test": "vitest run", "tsc": "tsc" }, "dependencies": { @@ -16,12 +16,12 @@ "zod": "catalog:" }, "devDependencies": { + "@openstatus/test-utils": "workspace:*", "@openstatus/tsconfig": "workspace:*", - "@std/expect": "jsr:^1.0.19", - "@std/testing": "jsr:^1.0.19", "@types/node": "catalog:", "@types/react": "catalog:", "@types/react-dom": "catalog:", - "typescript": "catalog:" + "typescript": "catalog:", + "vitest": "catalog:" } } diff --git a/packages/notifications/pagerduty/src/index.test.ts b/packages/notifications/pagerduty/src/index.test.ts index a1a94b335..5866915fd 100644 --- a/packages/notifications/pagerduty/src/index.test.ts +++ b/packages/notifications/pagerduty/src/index.test.ts @@ -1,21 +1,29 @@ import { selectNotificationSchema } from "@openstatus/db/src/schema"; -import { expect } from "@std/expect"; -import { afterEach, beforeEach, describe, test } from "@std/testing/bdd"; -import { assertSpyCalls, stub, type Stub } from "@std/testing/mock"; +import { + afterEach, + beforeEach, + describe, + expect, + test, + vi, + type MockInstance, +} from "vitest"; import { sendAlert, sendDegraded, sendRecovery, sendTest } from "./index"; describe("PagerDuty Notifications", () => { - let fetchMock: Stub; + let fetchMock: MockInstance; beforeEach(() => { - fetchMock = stub(globalThis, "fetch", () => - Promise.resolve(new Response(null, { status: 200 })), - ); + fetchMock = vi + .spyOn(globalThis, "fetch") + .mockImplementation(() => + Promise.resolve(new Response(null, { status: 200 })), + ); }); afterEach(() => { - fetchMock.restore(); + fetchMock.mockRestore(); }); const createMockMonitor = () => ({ @@ -68,8 +76,8 @@ describe("PagerDuty Notifications", () => { cronTimestamp: Date.now(), }); - assertSpyCalls(fetchMock, 1); - const callArgs = fetchMock.calls[0].args; + expect(fetchMock).toHaveBeenCalledTimes(1); + const callArgs = fetchMock.mock.calls[0]; expect(callArgs[0]).toBe("https://events.pagerduty.com/v2/enqueue"); expect(callArgs[1].method).toBe("POST"); @@ -107,9 +115,9 @@ describe("PagerDuty Notifications", () => { cronTimestamp: Date.now(), }); - assertSpyCalls(fetchMock, 2); - expect(fetchMock.calls[0].args[1].body).toContain("key1"); - expect(fetchMock.calls[1].args[1].body).toContain("key2"); + expect(fetchMock).toHaveBeenCalledTimes(2); + expect(fetchMock.mock.calls[0][1].body).toContain("key1"); + expect(fetchMock.mock.calls[1][1].body).toContain("key2"); }); test("Send Degraded", async () => { @@ -130,8 +138,8 @@ describe("PagerDuty Notifications", () => { cronTimestamp: Date.now(), }); - assertSpyCalls(fetchMock, 1); - const callArgs = fetchMock.calls[0].args; + expect(fetchMock).toHaveBeenCalledTimes(1); + const callArgs = fetchMock.mock.calls[0]; const body = JSON.parse(callArgs[1].body); expect(body.payload.summary).toBe("API Health Check is degraded"); expect(body.payload.severity).toBe("warning"); @@ -156,8 +164,8 @@ describe("PagerDuty Notifications", () => { cronTimestamp: Date.now(), }); - assertSpyCalls(fetchMock, 1); - const callArgs = fetchMock.calls[0].args; + expect(fetchMock).toHaveBeenCalledTimes(1); + const callArgs = fetchMock.mock.calls[0]; expect(callArgs[0]).toBe("https://events.pagerduty.com/v2/enqueue"); const body = JSON.parse(callArgs[1].body); expect(body.routing_key).toBe("my_key"); @@ -171,8 +179,8 @@ describe("PagerDuty Notifications", () => { }); expect(result).toBe(true); - assertSpyCalls(fetchMock, 1); - const callArgs = fetchMock.calls[0].args; + expect(fetchMock).toHaveBeenCalledTimes(1); + const callArgs = fetchMock.mock.calls[0]; expect(callArgs[0]).toBe("https://events.pagerduty.com/v2/enqueue"); expect(callArgs[1].method).toBe("POST"); @@ -186,24 +194,24 @@ describe("PagerDuty Notifications", () => { }); test("Send Test returns false on error", async () => { - fetchMock.restore(); - fetchMock = stub(globalThis, "fetch", () => - Promise.reject(new Error("Network error")), - ); + fetchMock.mockRestore(); + fetchMock = vi + .spyOn(globalThis, "fetch") + .mockImplementation(() => Promise.reject(new Error("Network error"))); const result = await sendTest({ integrationKey: "test-key", }); expect(result).toBe(false); - assertSpyCalls(fetchMock, 1); + expect(fetchMock).toHaveBeenCalledTimes(1); }); test("Handle fetch error gracefully", async () => { - fetchMock.restore(); - fetchMock = stub(globalThis, "fetch", () => - Promise.reject(new Error("Network error")), - ); + fetchMock.mockRestore(); + fetchMock = vi + .spyOn(globalThis, "fetch") + .mockImplementation(() => Promise.reject(new Error("Network error"))); const monitor = createMockMonitor(); const notification = selectNotificationSchema.parse( @@ -211,7 +219,7 @@ describe("PagerDuty Notifications", () => { ); const incident = createMockIncident(); - expect( + await expect( sendAlert({ // @ts-expect-error monitor, @@ -224,6 +232,6 @@ describe("PagerDuty Notifications", () => { }), ).rejects.toThrow(); - assertSpyCalls(fetchMock, 1); + expect(fetchMock).toHaveBeenCalledTimes(1); }); }); diff --git a/packages/notifications/pagerduty/vitest.config.ts b/packages/notifications/pagerduty/vitest.config.ts new file mode 100644 index 000000000..26bdb37da --- /dev/null +++ b/packages/notifications/pagerduty/vitest.config.ts @@ -0,0 +1,3 @@ +import { defineTestConfig } from "@openstatus/test-utils/vitest"; + +export default defineTestConfig(import.meta.dirname); diff --git a/packages/notifications/slack/package.json b/packages/notifications/slack/package.json index 8e6aacff0..f36a2502b 100644 --- a/packages/notifications/slack/package.json +++ b/packages/notifications/slack/package.json @@ -4,7 +4,7 @@ "main": "src/index.ts", "scripts": { "check": "tsc --noEmit", - "test": "deno test --parallel -A --no-check --sloppy-imports", + "test": "vitest run", "tsc": "tsc" }, "dependencies": { @@ -14,10 +14,10 @@ "zod": "catalog:" }, "devDependencies": { + "@openstatus/test-utils": "workspace:*", "@openstatus/tsconfig": "workspace:*", - "@std/expect": "jsr:^1.0.19", - "@std/testing": "jsr:^1.0.19", "@types/node": "catalog:", - "typescript": "catalog:" + "typescript": "catalog:", + "vitest": "catalog:" } } diff --git a/packages/notifications/slack/src/index.test.ts b/packages/notifications/slack/src/index.test.ts index a9b42b712..222e45e4c 100644 --- a/packages/notifications/slack/src/index.test.ts +++ b/packages/notifications/slack/src/index.test.ts @@ -1,8 +1,14 @@ import { selectNotificationSchema } from "@openstatus/db/src/schema"; import { COLORS } from "@openstatus/notification-base"; -import { expect } from "@std/expect"; -import { afterEach, beforeEach, describe, test } from "@std/testing/bdd"; -import { assertSpyCalls, type Stub, stub } from "@std/testing/mock"; +import { + afterEach, + beforeEach, + describe, + expect, + test, + vi, + type MockInstance, +} from "vitest"; import { buildAlertBlocks, @@ -18,16 +24,18 @@ import { } from "./index"; describe("Slack Notifications", () => { - let fetchMock: Stub; + let fetchMock: MockInstance; beforeEach(() => { - fetchMock = stub(globalThis, "fetch", () => - Promise.resolve(new Response(null, { status: 200 })), - ); + fetchMock = vi + .spyOn(globalThis, "fetch") + .mockImplementation(() => + Promise.resolve(new Response(null, { status: 200 })), + ); }); afterEach(() => { - fetchMock.restore(); + fetchMock.mockRestore(); }); const createMockMonitor = () => ({ @@ -67,8 +75,8 @@ describe("Slack Notifications", () => { cronTimestamp: Date.now(), }); - assertSpyCalls(fetchMock, 1); - const callArgs = fetchMock.calls[0].args; + expect(fetchMock).toHaveBeenCalledTimes(1); + const callArgs = fetchMock.mock.calls[0]; expect(callArgs[0]).toBe("https://hooks.slack.com/services/url"); expect(callArgs[1].method).toBe("POST"); @@ -94,8 +102,8 @@ describe("Slack Notifications", () => { cronTimestamp: Date.now(), }); - assertSpyCalls(fetchMock, 1); - const callArgs = fetchMock.calls[0].args; + expect(fetchMock).toHaveBeenCalledTimes(1); + const callArgs = fetchMock.mock.calls[0]; const body = JSON.parse(callArgs[1].body); expect(body.attachments[0].color).toBe(COLORS.red); expect(body.attachments[0].blocks[3].fields[0].text).toContain("Unknown"); @@ -116,8 +124,8 @@ describe("Slack Notifications", () => { cronTimestamp: Date.now(), }); - assertSpyCalls(fetchMock, 1); - const callArgs = fetchMock.calls[0].args; + expect(fetchMock).toHaveBeenCalledTimes(1); + const callArgs = fetchMock.mock.calls[0]; const body = JSON.parse(callArgs[1].body); expect(body.attachments).toBeDefined(); expect(body.attachments[0].color).toBe(COLORS.green); @@ -139,8 +147,8 @@ describe("Slack Notifications", () => { cronTimestamp: Date.now(), }); - assertSpyCalls(fetchMock, 1); - const callArgs = fetchMock.calls[0].args; + expect(fetchMock).toHaveBeenCalledTimes(1); + const callArgs = fetchMock.mock.calls[0]; const body = JSON.parse(callArgs[1].body); expect(body.attachments).toBeDefined(); expect(body.attachments[0].color).toBe(COLORS.yellow); @@ -152,8 +160,8 @@ describe("Slack Notifications", () => { await sendTestSlackMessage(webhookUrl); - assertSpyCalls(fetchMock, 1); - const callArgs = fetchMock.calls[0].args; + expect(fetchMock).toHaveBeenCalledTimes(1); + const callArgs = fetchMock.mock.calls[0]; expect(callArgs[0]).toBe(webhookUrl); const body = JSON.parse(callArgs[1].body); @@ -163,20 +171,20 @@ describe("Slack Notifications", () => { }); test("Send Test Slack Message throws error on empty webhookUrl", async () => { - fetchMock.restore(); - fetchMock = stub(globalThis, "fetch", () => - Promise.reject(new Error("Network error")), - ); + fetchMock.mockRestore(); + fetchMock = vi + .spyOn(globalThis, "fetch") + .mockImplementation(() => Promise.reject(new Error("Network error"))); - expect(sendTestSlackMessage("")).rejects.toThrow(); - assertSpyCalls(fetchMock, 0); + await expect(sendTestSlackMessage("")).rejects.toThrow(); + expect(fetchMock).toHaveBeenCalledTimes(0); }); test("Handle fetch error gracefully", async () => { - fetchMock.restore(); - fetchMock = stub(globalThis, "fetch", () => - Promise.reject(new Error("Network error")), - ); + fetchMock.mockRestore(); + fetchMock = vi + .spyOn(globalThis, "fetch") + .mockImplementation(() => Promise.reject(new Error("Network error"))); const monitor = createMockMonitor(); const notification = selectNotificationSchema.parse( @@ -194,7 +202,7 @@ describe("Slack Notifications", () => { }), ).rejects.toThrow(); - assertSpyCalls(fetchMock, 1); + expect(fetchMock).toHaveBeenCalledTimes(1); }); }); diff --git a/packages/notifications/slack/vitest.config.ts b/packages/notifications/slack/vitest.config.ts new file mode 100644 index 000000000..26bdb37da --- /dev/null +++ b/packages/notifications/slack/vitest.config.ts @@ -0,0 +1,3 @@ +import { defineTestConfig } from "@openstatus/test-utils/vitest"; + +export default defineTestConfig(import.meta.dirname); diff --git a/packages/notifications/telegram/package.json b/packages/notifications/telegram/package.json index 5d1d723cf..9c8de2b94 100644 --- a/packages/notifications/telegram/package.json +++ b/packages/notifications/telegram/package.json @@ -4,7 +4,7 @@ "main": "src/index.ts", "scripts": { "check": "tsc --noEmit", - "test": "deno test --parallel -A --no-check --sloppy-imports", + "test": "vitest run", "tsc": "tsc" }, "dependencies": { @@ -13,10 +13,10 @@ "zod": "catalog:" }, "devDependencies": { + "@openstatus/test-utils": "workspace:*", "@openstatus/tsconfig": "workspace:*", - "@std/expect": "jsr:^1.0.19", - "@std/testing": "jsr:^1.0.19", "@types/node": "catalog:", - "typescript": "catalog:" + "typescript": "catalog:", + "vitest": "catalog:" } } diff --git a/packages/notifications/telegram/src/index.test.ts b/packages/notifications/telegram/src/index.test.ts index 635b0c89c..8a8ee4469 100644 --- a/packages/notifications/telegram/src/index.test.ts +++ b/packages/notifications/telegram/src/index.test.ts @@ -1,7 +1,13 @@ import { selectNotificationSchema } from "@openstatus/db/src/schema"; -import { expect } from "@std/expect"; -import { afterEach, beforeEach, describe, test } from "@std/testing/bdd"; -import { assertSpyCalls, type Stub, stub } from "@std/testing/mock"; +import { + afterEach, + beforeEach, + describe, + expect, + test, + vi, + type MockInstance, +} from "vitest"; import { sendAlert, @@ -12,18 +18,20 @@ import { } from "./index"; describe("Telegram Notifications", () => { - let fetchMock: Stub; + let fetchMock: MockInstance; const originalEnv = process.env.TELEGRAM_BOT_TOKEN; beforeEach(() => { process.env.TELEGRAM_BOT_TOKEN = "test-bot-token-123"; - fetchMock = stub(globalThis, "fetch", () => - Promise.resolve(new Response(null, { status: 200 })), - ); + fetchMock = vi + .spyOn(globalThis, "fetch") + .mockImplementation(() => + Promise.resolve(new Response(null, { status: 200 })), + ); }); afterEach(() => { - fetchMock.restore(); + fetchMock.mockRestore(); if (originalEnv) { process.env.TELEGRAM_BOT_TOKEN = originalEnv; } else { @@ -38,8 +46,8 @@ describe("Telegram Notifications", () => { await sendMessage({ chatId: "-123456789", message }); - assertSpyCalls(fetchMock, 1); - const url = new URL(new Request(fetchMock.calls[0].args[0]).url); + expect(fetchMock).toHaveBeenCalledTimes(1); + const url = new URL(new Request(fetchMock.mock.calls[0][0]).url); expect([...url.searchParams]).toEqual([ ["chat_id", "-123456789"], ["text", message], @@ -88,8 +96,8 @@ describe("Telegram Notifications", () => { cronTimestamp: Date.now(), }); - assertSpyCalls(fetchMock, 1); - const callArgs = fetchMock.calls[0].args; + expect(fetchMock).toHaveBeenCalledTimes(1); + const callArgs = fetchMock.mock.calls[0]; expect(callArgs[0]).toContain( "https://api.telegram.org/bottest-bot-token-123/sendMessage", ); @@ -113,8 +121,8 @@ describe("Telegram Notifications", () => { cronTimestamp: Date.now(), }); - assertSpyCalls(fetchMock, 1); - const callArgs = fetchMock.calls[0].args; + expect(fetchMock).toHaveBeenCalledTimes(1); + const callArgs = fetchMock.mock.calls[0]; const text = new URL(new Request(callArgs[0]).url).searchParams.get("text"); expect(text).toContain("Connection timeout"); }); @@ -134,8 +142,8 @@ describe("Telegram Notifications", () => { cronTimestamp: Date.now(), }); - assertSpyCalls(fetchMock, 1); - const callArgs = fetchMock.calls[0].args; + expect(fetchMock).toHaveBeenCalledTimes(1); + const callArgs = fetchMock.mock.calls[0]; const text = new URL(new Request(callArgs[0]).url).searchParams.get("text"); expect(text).toContain(monitor.name); expect(text).toContain(monitor.url); @@ -156,8 +164,8 @@ describe("Telegram Notifications", () => { cronTimestamp: Date.now(), }); - assertSpyCalls(fetchMock, 1); - const callArgs = fetchMock.calls[0].args; + expect(fetchMock).toHaveBeenCalledTimes(1); + const callArgs = fetchMock.mock.calls[0]; const text = new URL(new Request(callArgs[0]).url).searchParams.get("text"); expect(text).toContain(monitor.name); expect(text).toContain(monitor.url); @@ -169,8 +177,8 @@ describe("Telegram Notifications", () => { }); expect(result).toBe(true); - assertSpyCalls(fetchMock, 1); - const callArgs = fetchMock.calls[0].args; + expect(fetchMock).toHaveBeenCalledTimes(1); + const callArgs = fetchMock.mock.calls[0]; expect(callArgs[0]).toContain( "https://api.telegram.org/bottest-bot-token-123/sendMessage", ); @@ -178,24 +186,24 @@ describe("Telegram Notifications", () => { }); test("Send Test returns false on error", async () => { - fetchMock.restore(); - fetchMock = stub(globalThis, "fetch", () => - Promise.reject(new Error("Network error")), - ); + fetchMock.mockRestore(); + fetchMock = vi + .spyOn(globalThis, "fetch") + .mockImplementation(() => Promise.reject(new Error("Network error"))); const result = await sendTest({ chatId: "123456789", }); expect(result).toBe(false); - assertSpyCalls(fetchMock, 1); + expect(fetchMock).toHaveBeenCalledTimes(1); }); test("Handle fetch error gracefully", async () => { - fetchMock.restore(); - fetchMock = stub(globalThis, "fetch", () => - Promise.reject(new Error("Network error")), - ); + fetchMock.mockRestore(); + fetchMock = vi + .spyOn(globalThis, "fetch") + .mockImplementation(() => Promise.reject(new Error("Network error"))); const monitor = createMockMonitor(); const notification = selectNotificationSchema.parse( @@ -213,7 +221,7 @@ describe("Telegram Notifications", () => { }), ).rejects.toThrow(); - assertSpyCalls(fetchMock, 1); + expect(fetchMock).toHaveBeenCalledTimes(1); }); test("fetch not called when TELEGRAM_BOT_TOKEN is not set", async () => { @@ -235,6 +243,6 @@ describe("Telegram Notifications", () => { }), ).rejects.toThrow(); // Should not call fetch when token is missing - assertSpyCalls(fetchMock, 0); + expect(fetchMock).toHaveBeenCalledTimes(0); }); }); diff --git a/packages/notifications/telegram/vitest.config.ts b/packages/notifications/telegram/vitest.config.ts new file mode 100644 index 000000000..26bdb37da --- /dev/null +++ b/packages/notifications/telegram/vitest.config.ts @@ -0,0 +1,3 @@ +import { defineTestConfig } from "@openstatus/test-utils/vitest"; + +export default defineTestConfig(import.meta.dirname); diff --git a/packages/notifications/twillio-sms/package.json b/packages/notifications/twillio-sms/package.json index bac3f742e..554966eff 100644 --- a/packages/notifications/twillio-sms/package.json +++ b/packages/notifications/twillio-sms/package.json @@ -4,7 +4,7 @@ "main": "src/index.ts", "scripts": { "check": "tsc --noEmit", - "test": "deno test --parallel -A --no-check --sloppy-imports", + "test": "vitest run", "tsc": "tsc" }, "dependencies": { @@ -15,11 +15,11 @@ "zod": "catalog:" }, "devDependencies": { + "@openstatus/test-utils": "workspace:*", "@openstatus/tsconfig": "workspace:*", - "@std/expect": "jsr:^1.0.19", - "@std/testing": "jsr:^1.0.19", "@types/node": "catalog:", "@types/validator": "catalog:", - "typescript": "catalog:" + "typescript": "catalog:", + "vitest": "catalog:" } } diff --git a/packages/notifications/twillio-sms/src/index.test.ts b/packages/notifications/twillio-sms/src/index.test.ts index 21b334ca8..1a012c247 100644 --- a/packages/notifications/twillio-sms/src/index.test.ts +++ b/packages/notifications/twillio-sms/src/index.test.ts @@ -1,12 +1,18 @@ import { selectNotificationSchema } from "@openstatus/db/src/schema"; -import { expect } from "@std/expect"; -import { afterEach, beforeEach, describe, test } from "@std/testing/bdd"; -import { assertSpyCalls, stub, type Stub } from "@std/testing/mock"; +import { + afterEach, + beforeEach, + describe, + expect, + test, + vi, + type MockInstance, +} from "vitest"; import { sendAlert, sendDegraded, sendRecovery } from "./index"; describe("Twilio SMS Notifications", () => { - let fetchMock: Stub; + let fetchMock: MockInstance; const originalEnv = { TWILLIO_ACCOUNT_ID: process.env.TWILLIO_ACCOUNT_ID, TWILLIO_AUTH_TOKEN: process.env.TWILLIO_AUTH_TOKEN, @@ -15,13 +21,15 @@ describe("Twilio SMS Notifications", () => { beforeEach(() => { process.env.TWILLIO_ACCOUNT_ID = "test-account-id"; process.env.TWILLIO_AUTH_TOKEN = "test-auth-token"; - fetchMock = stub(globalThis, "fetch", () => - Promise.resolve(new Response(null, { status: 200 })), - ); + fetchMock = vi + .spyOn(globalThis, "fetch") + .mockImplementation(() => + Promise.resolve(new Response(null, { status: 200 })), + ); }); afterEach(() => { - fetchMock.restore(); + fetchMock.mockRestore(); if (originalEnv.TWILLIO_ACCOUNT_ID) { process.env.TWILLIO_ACCOUNT_ID = originalEnv.TWILLIO_ACCOUNT_ID; } else { @@ -71,8 +79,8 @@ describe("Twilio SMS Notifications", () => { cronTimestamp: Date.now(), }); - assertSpyCalls(fetchMock, 1); - const callArgs = fetchMock.calls[0].args; + expect(fetchMock).toHaveBeenCalledTimes(1); + const callArgs = fetchMock.mock.calls[0]; expect(callArgs[0]).toBe( "https://api.twilio.com/2010-04-01/Accounts/test-account-id/Messages.json", ); @@ -102,8 +110,8 @@ describe("Twilio SMS Notifications", () => { cronTimestamp: Date.now(), }); - assertSpyCalls(fetchMock, 1); - const callArgs = fetchMock.calls[0].args; + expect(fetchMock).toHaveBeenCalledTimes(1); + const callArgs = fetchMock.mock.calls[0]; const formData = callArgs[1].body as FormData; expect(formData.get("Body")).toContain("error: Connection timeout"); }); @@ -123,8 +131,8 @@ describe("Twilio SMS Notifications", () => { cronTimestamp: Date.now(), }); - assertSpyCalls(fetchMock, 1); - const callArgs = fetchMock.calls[0].args; + expect(fetchMock).toHaveBeenCalledTimes(1); + const callArgs = fetchMock.mock.calls[0]; const formData = callArgs[1].body as FormData; expect(formData.get("Body")).toContain("is up again"); expect(formData.get("Body")).toContain("API Health Check"); @@ -145,25 +153,25 @@ describe("Twilio SMS Notifications", () => { cronTimestamp: Date.now(), }); - assertSpyCalls(fetchMock, 1); - const callArgs = fetchMock.calls[0].args; + expect(fetchMock).toHaveBeenCalledTimes(1); + const callArgs = fetchMock.mock.calls[0]; const formData = callArgs[1].body as FormData; expect(formData.get("Body")).toContain("is degraded"); expect(formData.get("Body")).toContain("API Health Check"); }); test("Handle fetch error gracefully", async () => { - fetchMock.restore(); - fetchMock = stub(globalThis, "fetch", () => - Promise.reject(new Error("Network error")), - ); + fetchMock.mockRestore(); + fetchMock = vi + .spyOn(globalThis, "fetch") + .mockImplementation(() => Promise.reject(new Error("Network error"))); const monitor = createMockMonitor(); const notification = selectNotificationSchema.parse( createMockNotification(), ); - expect( + await expect( sendAlert({ // @ts-expect-error monitor, @@ -174,6 +182,6 @@ describe("Twilio SMS Notifications", () => { }), ).rejects.toThrow(); - assertSpyCalls(fetchMock, 1); + expect(fetchMock).toHaveBeenCalledTimes(1); }); }); diff --git a/packages/notifications/twillio-sms/vitest.config.ts b/packages/notifications/twillio-sms/vitest.config.ts new file mode 100644 index 000000000..26bdb37da --- /dev/null +++ b/packages/notifications/twillio-sms/vitest.config.ts @@ -0,0 +1,3 @@ +import { defineTestConfig } from "@openstatus/test-utils/vitest"; + +export default defineTestConfig(import.meta.dirname); diff --git a/packages/notifications/webhook/package.json b/packages/notifications/webhook/package.json index a2253a0cb..0eb54db1b 100644 --- a/packages/notifications/webhook/package.json +++ b/packages/notifications/webhook/package.json @@ -4,7 +4,7 @@ "main": "src/index.ts", "scripts": { "check": "tsc --noEmit", - "test": "deno test --parallel -A --no-check --sloppy-imports", + "test": "vitest run", "tsc": "tsc" }, "dependencies": { @@ -14,10 +14,10 @@ "zod": "catalog:" }, "devDependencies": { + "@openstatus/test-utils": "workspace:*", "@openstatus/tsconfig": "workspace:*", - "@std/expect": "jsr:^1.0.19", - "@std/testing": "jsr:^1.0.19", "@types/node": "catalog:", - "typescript": "catalog:" + "typescript": "catalog:", + "vitest": "catalog:" } } diff --git a/packages/notifications/webhook/src/index.test.ts b/packages/notifications/webhook/src/index.test.ts index f0000ee15..5c81d8ffa 100644 --- a/packages/notifications/webhook/src/index.test.ts +++ b/packages/notifications/webhook/src/index.test.ts @@ -1,35 +1,45 @@ -import { expect } from "@std/expect"; -import { afterEach, beforeEach, describe, test } from "@std/testing/bdd"; -import { assertSpyCalls, stub, type Stub } from "@std/testing/mock"; +import { + afterEach, + beforeEach, + describe, + expect, + test, + vi, + type MockInstance, +} from "vitest"; import { sendTest } from "./index"; describe("Webhook sendTest", () => { - let fetchMock: Stub; + let fetchMock: MockInstance; beforeEach(() => { - fetchMock = stub(globalThis, "fetch", () => - Promise.resolve(new Response(null, { status: 200 })), - ); + fetchMock = vi + .spyOn(globalThis, "fetch") + .mockImplementation(() => + Promise.resolve(new Response(null, { status: 200 })), + ); }); afterEach(() => { - fetchMock.restore(); + fetchMock.mockRestore(); }); test("should send test webhook successfully", async () => { const url = "https://example.com/webhook"; - fetchMock.restore(); - fetchMock = stub(globalThis, "fetch", () => - Promise.resolve(new Response(null, { status: 200 })), - ); + fetchMock.mockRestore(); + fetchMock = vi + .spyOn(globalThis, "fetch") + .mockImplementation(() => + Promise.resolve(new Response(null, { status: 200 })), + ); const result = await sendTest({ url }); expect(result).toBe(true); - assertSpyCalls(fetchMock, 1); - expect(fetchMock.calls[0].args[0]).toBe(url); - expect(fetchMock.calls[0].args[1]).toEqual( + expect(fetchMock).toHaveBeenCalledTimes(1); + expect(fetchMock.mock.calls[0][0]).toBe(url); + expect(fetchMock.mock.calls[0][1]).toEqual( expect.objectContaining({ method: "post", body: expect.any(String), @@ -37,7 +47,7 @@ describe("Webhook sendTest", () => { }), ); - const callArgs = fetchMock.calls[0].args; + const callArgs = fetchMock.mock.calls[0]; const body = JSON.parse(callArgs[1].body); expect(body).toMatchObject({ monitor: { @@ -58,17 +68,19 @@ describe("Webhook sendTest", () => { { key: "Authorization", value: "Bearer token123" }, { key: "Content-Type", value: "application/json" }, ]; - fetchMock.restore(); - fetchMock = stub(globalThis, "fetch", () => - Promise.resolve(new Response(null, { status: 200 })), - ); + fetchMock.mockRestore(); + fetchMock = vi + .spyOn(globalThis, "fetch") + .mockImplementation(() => + Promise.resolve(new Response(null, { status: 200 })), + ); const result = await sendTest({ url, headers }); expect(result).toBe(true); - assertSpyCalls(fetchMock, 1); - expect(fetchMock.calls[0].args[0]).toBe(url); - expect(fetchMock.calls[0].args[1]).toEqual( + expect(fetchMock).toHaveBeenCalledTimes(1); + expect(fetchMock.mock.calls[0][0]).toBe(url); + expect(fetchMock.mock.calls[0][1]).toEqual( expect.objectContaining({ method: "post", body: expect.any(String), @@ -82,63 +94,73 @@ describe("Webhook sendTest", () => { test("should throw error when response is not ok", async () => { const url = "https://example.com/webhook"; - fetchMock.restore(); - fetchMock = stub(globalThis, "fetch", () => - Promise.resolve(new Response(null, { status: 400 })), - ); + fetchMock.mockRestore(); + fetchMock = vi + .spyOn(globalThis, "fetch") + .mockImplementation(() => + Promise.resolve(new Response(null, { status: 400 })), + ); await expect(sendTest({ url })).rejects.toThrow("Failed to send test"); - assertSpyCalls(fetchMock, 1); + expect(fetchMock).toHaveBeenCalledTimes(1); }); test("should throw error when fetch fails", async () => { const url = "https://example.com/webhook"; const networkError = new Error("Network error"); - fetchMock.restore(); - fetchMock = stub(globalThis, "fetch", () => Promise.reject(networkError)); + fetchMock.mockRestore(); + fetchMock = vi + .spyOn(globalThis, "fetch") + .mockImplementation(() => Promise.reject(networkError)); await expect(sendTest({ url })).rejects.toThrow("Failed to send test"); - assertSpyCalls(fetchMock, 1); + expect(fetchMock).toHaveBeenCalledTimes(1); }); test("should send test webhook with empty headers array", async () => { const url = "https://example.com/webhook"; const headers: { key: string; value: string }[] = []; - fetchMock.restore(); - fetchMock = stub(globalThis, "fetch", () => - Promise.resolve(new Response(null, { status: 200 })), - ); + fetchMock.mockRestore(); + fetchMock = vi + .spyOn(globalThis, "fetch") + .mockImplementation(() => + Promise.resolve(new Response(null, { status: 200 })), + ); const result = await sendTest({ url, headers }); expect(result).toBe(true); - assertSpyCalls(fetchMock, 1); - const callArgs = fetchMock.calls[0].args; + expect(fetchMock).toHaveBeenCalledTimes(1); + const callArgs = fetchMock.mock.calls[0]; // Empty headers array should still include Content-Type expect(callArgs[1].headers).toEqual({ "Content-Type": "application/json" }); }); test("should send test webhook with 500 status code", async () => { const url = "https://example.com/webhook"; - fetchMock.restore(); - fetchMock = stub(globalThis, "fetch", () => - Promise.resolve(new Response(null, { status: 500 })), - ); + fetchMock.mockRestore(); + fetchMock = vi + .spyOn(globalThis, "fetch") + .mockImplementation(() => + Promise.resolve(new Response(null, { status: 500 })), + ); await expect(sendTest({ url })).rejects.toThrow("Failed to send test"); - assertSpyCalls(fetchMock, 1); + expect(fetchMock).toHaveBeenCalledTimes(1); }); test("should send test webhook with 201 status code (success)", async () => { const url = "https://example.com/webhook"; - fetchMock.restore(); - fetchMock = stub(globalThis, "fetch", () => - Promise.resolve(new Response(null, { status: 201 })), - ); + fetchMock.mockRestore(); + fetchMock = vi + .spyOn(globalThis, "fetch") + .mockImplementation(() => + Promise.resolve(new Response(null, { status: 201 })), + ); const result = await sendTest({ url }); expect(result).toBe(true); - assertSpyCalls(fetchMock, 1); + expect(fetchMock).toHaveBeenCalledTimes(1); }); }); diff --git a/packages/notifications/webhook/tsconfig.json b/packages/notifications/webhook/tsconfig.json index c240dd4b7..aed15c74a 100644 --- a/packages/notifications/webhook/tsconfig.json +++ b/packages/notifications/webhook/tsconfig.json @@ -1,13 +1,4 @@ { "extends": "@openstatus/tsconfig/base.json", - "include": ["src", "*.ts"], - "exclude": [ - "**/*.test.ts", - "**/*.spec.ts", - "**/*.test.tsx", - "**/__tests__/**", - "**/test/**", - "**/*.mock.ts", - "**/test-preload.ts" - ] + "include": ["src", "*.ts"] } diff --git a/packages/notifications/webhook/vitest.config.ts b/packages/notifications/webhook/vitest.config.ts new file mode 100644 index 000000000..26bdb37da --- /dev/null +++ b/packages/notifications/webhook/vitest.config.ts @@ -0,0 +1,3 @@ +import { defineTestConfig } from "@openstatus/test-utils/vitest"; + +export default defineTestConfig(import.meta.dirname); diff --git a/packages/proto/tsconfig.json b/packages/proto/tsconfig.json index 5e6c0e614..001da3db2 100644 --- a/packages/proto/tsconfig.json +++ b/packages/proto/tsconfig.json @@ -11,13 +11,5 @@ "allowImportingTsExtensions": true }, "include": ["gen/ts/**/*.ts", "scripts/**/*.ts"], - "exclude": [ - "node_modules", - "**/*.test.ts", - "**/*.test.tsx", - "**/__tests__/**", - "**/test/**", - "**/*.mock.ts", - "**/test-preload.ts" - ] + "exclude": ["node_modules"] } diff --git a/packages/services/package.json b/packages/services/package.json index da014c1bc..85583de2c 100644 --- a/packages/services/package.json +++ b/packages/services/package.json @@ -156,7 +156,7 @@ }, "scripts": { "check": "tsc --noEmit", - "test": "NODE_ENV=test RESEND_API_KEY=test-key deno test --parallel -A --no-check --sloppy-imports", + "test": "NODE_ENV=test RESEND_API_KEY=test-key vitest run", "tsc": "tsc --noEmit" }, "dependencies": { @@ -175,10 +175,10 @@ "zod": "catalog:" }, "devDependencies": { + "@openstatus/test-utils": "workspace:*", "@openstatus/tsconfig": "workspace:*", - "@std/expect": "jsr:^1.0.19", - "@std/testing": "jsr:^1.0.19", "@types/node": "catalog:", - "typescript": "catalog:" + "typescript": "catalog:", + "vitest": "catalog:" } } diff --git a/packages/services/src/__tests__/attribution.test.ts b/packages/services/src/__tests__/attribution.test.ts index 4a9d4206c..06ed549a5 100644 --- a/packages/services/src/__tests__/attribution.test.ts +++ b/packages/services/src/__tests__/attribution.test.ts @@ -1,5 +1,4 @@ -import { expect } from "@std/expect"; -import { describe, test } from "@std/testing/bdd"; +import { describe, expect, test } from "vitest"; import { toAgentUser, toAttributedUserDetail } from "../attribution"; diff --git a/packages/services/src/__tests__/context.test.ts b/packages/services/src/__tests__/context.test.ts index c285add6a..2af0b6b6e 100644 --- a/packages/services/src/__tests__/context.test.ts +++ b/packages/services/src/__tests__/context.test.ts @@ -1,7 +1,6 @@ import { db, eq } from "@openstatus/db"; import { monitor, page } from "@openstatus/db/src/schema"; -import { expect } from "@std/expect"; -import { beforeAll, describe, test } from "@std/testing/bdd"; +import { beforeAll, describe, expect, test } from "vitest"; import { createWorkspaceFixture, diff --git a/packages/services/src/__tests__/features.test.ts b/packages/services/src/__tests__/features.test.ts index 334971a8c..6b083b890 100644 --- a/packages/services/src/__tests__/features.test.ts +++ b/packages/services/src/__tests__/features.test.ts @@ -1,5 +1,4 @@ -import { expect } from "@std/expect"; -import { describe, test } from "@std/testing/bdd"; +import { describe, expect, test } from "vitest"; import { ForbiddenError } from "../errors"; import { type Feature, isFeatureEnabled, requireFeature } from "../features"; diff --git a/packages/services/src/__tests__/limits.test.ts b/packages/services/src/__tests__/limits.test.ts index b8c84952d..c3d500bb7 100644 --- a/packages/services/src/__tests__/limits.test.ts +++ b/packages/services/src/__tests__/limits.test.ts @@ -7,8 +7,7 @@ import { pageComponent, workspace, } from "@openstatus/db/src/schema"; -import { expect } from "@std/expect"; -import { beforeAll, describe, test } from "@std/testing/bdd"; +import { beforeAll, describe, expect, test } from "vitest"; import { createWorkspaceFixture, diff --git a/packages/services/src/__tests__/retry.test.ts b/packages/services/src/__tests__/retry.test.ts index ada57ea13..f014ac750 100644 --- a/packages/services/src/__tests__/retry.test.ts +++ b/packages/services/src/__tests__/retry.test.ts @@ -1,5 +1,4 @@ -import { expect } from "@std/expect"; -import { describe, test } from "@std/testing/bdd"; +import { describe, expect, test } from "vitest"; import { NotFoundError } from "../errors"; import { diff --git a/packages/services/src/__tests__/with-test-transaction.test.ts b/packages/services/src/__tests__/with-test-transaction.test.ts index cb9a1f643..f7b90799b 100644 --- a/packages/services/src/__tests__/with-test-transaction.test.ts +++ b/packages/services/src/__tests__/with-test-transaction.test.ts @@ -1,7 +1,6 @@ import { db, eq } from "@openstatus/db"; import { auditLog } from "@openstatus/db/src/schema"; -import { expect } from "@std/expect"; -import { afterAll, describe, test } from "@std/testing/bdd"; +import { afterAll, describe, expect, test } from "vitest"; import { SEEDED_WORKSPACE_TEAM_ID } from "../../test/fixtures"; import { withTestTransaction } from "../../test/helpers"; diff --git a/packages/services/src/__tests__/with-transaction.test.ts b/packages/services/src/__tests__/with-transaction.test.ts index cf6a9d3d9..bb32929f3 100644 --- a/packages/services/src/__tests__/with-transaction.test.ts +++ b/packages/services/src/__tests__/with-transaction.test.ts @@ -1,5 +1,4 @@ -import { expect } from "@std/expect"; -import { beforeAll, describe, test } from "@std/testing/bdd"; +import { beforeAll, describe, expect, test } from "vitest"; import { createWorkspaceFixture } from "../../test/helpers"; import type { DB, ServiceContext } from "../context"; diff --git a/packages/services/src/agent-tools/__tests__/content.test.ts b/packages/services/src/agent-tools/__tests__/content.test.ts index c2704eccb..cf9d390bc 100644 --- a/packages/services/src/agent-tools/__tests__/content.test.ts +++ b/packages/services/src/agent-tools/__tests__/content.test.ts @@ -1,6 +1,4 @@ -import { expect } from "@std/expect"; -import { afterEach, describe, test } from "@std/testing/bdd"; -import { assertSpyCalls, spy } from "@std/testing/mock"; +import { afterEach, describe, expect, test, vi } from "vitest"; import type { ServiceContext } from "../../context"; import { getContentPageTool, searchContentTool } from "../content"; @@ -15,7 +13,7 @@ afterEach(() => { }); function mockFetch(response: Response) { - const fn = spy(async () => response); + const fn = vi.fn(async () => response); globalThis.fetch = fn as unknown as typeof fetch; return fn; } @@ -43,8 +41,8 @@ describe("search_content", () => { ctx, input: searchContentTool.inputSchema.parse({ query: "pricing" }), }); - assertSpyCalls(fn, 1); - const url = String((fn.calls[0] as { args: unknown[] }).args[0]); + expect(fn).toHaveBeenCalledTimes(1); + const url = String((fn.mock.calls[0] as unknown[] | undefined)?.[0]); expect(url).toContain("/api/search?p=all&q=pricing"); }); @@ -54,7 +52,7 @@ describe("search_content", () => { ctx, input: { query: "pricing", type: "compare" }, }); - const url = String((fn.calls[0] as { args: unknown[] }).args[0]); + const url = String((fn.mock.calls[0] as unknown[] | undefined)?.[0]); expect(url).toContain("/api/search?p=compare&q=pricing"); }); @@ -140,7 +138,7 @@ describe("get_content_page", () => { expect(result.markdown).toBe("# Pricing\n\nPlans."); expect(result.truncated).toBe(false); expect(result.url).toBe("https://www.openstatus.dev/pricing"); - const url = String((fn.calls[0] as { args: unknown[] }).args[0]); + const url = String((fn.mock.calls[0] as unknown[] | undefined)?.[0]); expect(url).toBe("https://www.openstatus.dev/api/markdown/pricing"); }); @@ -198,6 +196,6 @@ describe("get_content_page", () => { true, ); } - assertSpyCalls(fn, 0); + expect(fn).toHaveBeenCalledTimes(0); }); }); diff --git a/packages/services/src/agent-tools/__tests__/docs.test.ts b/packages/services/src/agent-tools/__tests__/docs.test.ts index 1537e16d5..70cd4d90d 100644 --- a/packages/services/src/agent-tools/__tests__/docs.test.ts +++ b/packages/services/src/agent-tools/__tests__/docs.test.ts @@ -1,6 +1,4 @@ -import { expect } from "@std/expect"; -import { afterEach, describe, test } from "@std/testing/bdd"; -import { assertSpyCalls, spy } from "@std/testing/mock"; +import { afterEach, describe, expect, test, vi } from "vitest"; import type { ServiceContext } from "../../context"; import { getDocPageTool, searchDocsTool } from "../docs"; @@ -15,7 +13,7 @@ afterEach(() => { }); function mockFetch(response: Response) { - const fn = spy(async () => response); + const fn = vi.fn(async () => response); globalThis.fetch = fn as unknown as typeof fetch; return fn; } @@ -163,6 +161,6 @@ describe("get_doc_page", () => { ); expect(getDocPageTool.outputSchema.safeParse(result).success).toBe(true); } - assertSpyCalls(fn, 0); + expect(fn).toHaveBeenCalledTimes(0); }); }); diff --git a/packages/services/src/agent-tools/__tests__/incident.test.ts b/packages/services/src/agent-tools/__tests__/incident.test.ts index 2e0506b6c..eb952f3ac 100644 --- a/packages/services/src/agent-tools/__tests__/incident.test.ts +++ b/packages/services/src/agent-tools/__tests__/incident.test.ts @@ -1,5 +1,4 @@ -import { expect } from "@std/expect"; -import { beforeAll, describe, test } from "@std/testing/bdd"; +import { beforeAll, describe, expect, test } from "vitest"; import { createWorkspaceFixture, diff --git a/packages/services/src/agent-tools/__tests__/maintenance-updates.test.ts b/packages/services/src/agent-tools/__tests__/maintenance-updates.test.ts index 7687bdecf..35865b96d 100644 --- a/packages/services/src/agent-tools/__tests__/maintenance-updates.test.ts +++ b/packages/services/src/agent-tools/__tests__/maintenance-updates.test.ts @@ -1,5 +1,4 @@ -import { expect } from "@std/expect"; -import { describe, test } from "@std/testing/bdd"; +import { describe, expect, test } from "vitest"; import { agentTools, buildAgentSystemPrompt } from "../index"; diff --git a/packages/services/src/agent-tools/__tests__/prompt.test.ts b/packages/services/src/agent-tools/__tests__/prompt.test.ts index 23950fd94..440c95b38 100644 --- a/packages/services/src/agent-tools/__tests__/prompt.test.ts +++ b/packages/services/src/agent-tools/__tests__/prompt.test.ts @@ -1,5 +1,4 @@ -import { expect } from "@std/expect"; -import { describe, test } from "@std/testing/bdd"; +import { describe, expect, test } from "vitest"; import { type AgentSystemPromptOptions, diff --git a/packages/services/src/agent-tools/__tests__/shape-equivalence.test.ts b/packages/services/src/agent-tools/__tests__/shape-equivalence.test.ts index cde341d05..f87ee9324 100644 --- a/packages/services/src/agent-tools/__tests__/shape-equivalence.test.ts +++ b/packages/services/src/agent-tools/__tests__/shape-equivalence.test.ts @@ -1,6 +1,5 @@ import { createMonitor } from "@openstatus/db/src/test/factories"; -import { expect } from "@std/expect"; -import { beforeAll, describe, test } from "@std/testing/bdd"; +import { beforeAll, describe, expect, test } from "vitest"; import { createWorkspaceFixture, makeUserCtx } from "../../../test/helpers"; import type { ServiceContext } from "../../context"; diff --git a/packages/services/src/agent-tools/__tests__/status-report-impacts.test.ts b/packages/services/src/agent-tools/__tests__/status-report-impacts.test.ts index 2d0907dd4..213dfcd99 100644 --- a/packages/services/src/agent-tools/__tests__/status-report-impacts.test.ts +++ b/packages/services/src/agent-tools/__tests__/status-report-impacts.test.ts @@ -1,6 +1,5 @@ import { page, pageComponent } from "@openstatus/db/src/schema"; -import { expect } from "@std/expect"; -import { beforeAll, describe, test } from "@std/testing/bdd"; +import { beforeAll, describe, expect, test } from "vitest"; import { createWorkspaceFixture, diff --git a/packages/services/src/agent-tools/__tests__/summary-refs.test.ts b/packages/services/src/agent-tools/__tests__/summary-refs.test.ts index 1c47d8222..0049d42a4 100644 --- a/packages/services/src/agent-tools/__tests__/summary-refs.test.ts +++ b/packages/services/src/agent-tools/__tests__/summary-refs.test.ts @@ -1,5 +1,4 @@ -import { expect } from "@std/expect"; -import { describe, test } from "@std/testing/bdd"; +import { describe, expect, test } from "vitest"; import { agentTools } from "../index"; import type { AnyAgentTool, SummaryLine } from "../types"; diff --git a/packages/services/src/api-key/__tests__/api-key.test.ts b/packages/services/src/api-key/__tests__/api-key.test.ts index 0c50123fa..eb04afcd3 100644 --- a/packages/services/src/api-key/__tests__/api-key.test.ts +++ b/packages/services/src/api-key/__tests__/api-key.test.ts @@ -1,7 +1,6 @@ import { eq } from "@openstatus/db"; import { apiKey } from "@openstatus/db/src/schema"; -import { expect } from "@std/expect"; -import { beforeAll, describe, test } from "@std/testing/bdd"; +import { beforeAll, describe, expect, test } from "vitest"; import { createWorkspaceFixture, diff --git a/packages/services/src/audit/__tests__/audit.test.ts b/packages/services/src/audit/__tests__/audit.test.ts index 6296e1096..4e622426d 100644 --- a/packages/services/src/audit/__tests__/audit.test.ts +++ b/packages/services/src/audit/__tests__/audit.test.ts @@ -1,8 +1,7 @@ import { db, eq } from "@openstatus/db"; import { auditLog, user } from "@openstatus/db/src/schema"; import type { AuditEntry } from "@openstatus/db/src/schema"; -import { expect } from "@std/expect"; -import { afterAll, beforeAll, describe, test } from "@std/testing/bdd"; +import { afterAll, beforeAll, describe, expect, test } from "vitest"; import { clearAuditLog, diff --git a/packages/services/src/auth/__tests__/matches-scope.test.ts b/packages/services/src/auth/__tests__/matches-scope.test.ts index 0a70f4750..41c7c03a6 100644 --- a/packages/services/src/auth/__tests__/matches-scope.test.ts +++ b/packages/services/src/auth/__tests__/matches-scope.test.ts @@ -1,6 +1,5 @@ import type { Scope } from "@openstatus/db/src/schema"; -import { expect } from "@std/expect"; -import { describe, test } from "@std/testing/bdd"; +import { describe, expect, test } from "vitest"; import { matchesScope } from "../matches-scope"; diff --git a/packages/services/src/auth/__tests__/require-role.test.ts b/packages/services/src/auth/__tests__/require-role.test.ts index a18621a5c..3dec2c1c9 100644 --- a/packages/services/src/auth/__tests__/require-role.test.ts +++ b/packages/services/src/auth/__tests__/require-role.test.ts @@ -3,8 +3,7 @@ import { addUserToWorkspace, createUser, } from "@openstatus/db/src/test/factories"; -import { expect } from "@std/expect"; -import { beforeAll, describe, test } from "@std/testing/bdd"; +import { beforeAll, describe, expect, test } from "vitest"; import { createWorkspaceFixture, diff --git a/packages/services/src/auth/__tests__/require-scope.test.ts b/packages/services/src/auth/__tests__/require-scope.test.ts index b2c708d87..96962b628 100644 --- a/packages/services/src/auth/__tests__/require-scope.test.ts +++ b/packages/services/src/auth/__tests__/require-scope.test.ts @@ -1,5 +1,4 @@ -import { expect } from "@std/expect"; -import { describe, test } from "@std/testing/bdd"; +import { describe, expect, test } from "vitest"; import type { Actor, ServiceContext } from "../../context"; import { ForbiddenError } from "../../errors"; diff --git a/packages/services/src/external-service-component/__tests__/external-service-component.test.ts b/packages/services/src/external-service-component/__tests__/external-service-component.test.ts index e8062d017..068462018 100644 --- a/packages/services/src/external-service-component/__tests__/external-service-component.test.ts +++ b/packages/services/src/external-service-component/__tests__/external-service-component.test.ts @@ -4,8 +4,7 @@ import { externalService, externalServiceComponent, } from "@openstatus/db/src/schema"; -import { expect } from "@std/expect"; -import { afterEach, describe, test } from "@std/testing/bdd"; +import { afterEach, describe, expect, test } from "vitest"; import { getExternalComponentBySlug, diff --git a/packages/services/src/external-service-component/__tests__/internal.test.ts b/packages/services/src/external-service-component/__tests__/internal.test.ts index 7b304e958..9f2ef2c02 100644 --- a/packages/services/src/external-service-component/__tests__/internal.test.ts +++ b/packages/services/src/external-service-component/__tests__/internal.test.ts @@ -1,5 +1,4 @@ -import { expect } from "@std/expect"; -import { describe, test } from "@std/testing/bdd"; +import { describe, expect, test } from "vitest"; import { assignComponentSlugs, slugifyComponentName } from "../internal"; diff --git a/packages/services/src/external-service-incident/__tests__/external-service-incident.test.ts b/packages/services/src/external-service-incident/__tests__/external-service-incident.test.ts index 9f1ba0138..f2c8a01d7 100644 --- a/packages/services/src/external-service-incident/__tests__/external-service-incident.test.ts +++ b/packages/services/src/external-service-incident/__tests__/external-service-incident.test.ts @@ -3,8 +3,7 @@ import { externalService, externalServiceIncident, } from "@openstatus/db/src/schema"; -import { expect } from "@std/expect"; -import { afterEach, describe, test } from "@std/testing/bdd"; +import { afterEach, describe, expect, test } from "vitest"; import { type UpsertExternalIncidentInput, diff --git a/packages/services/src/external-service-report/__tests__/record.test.ts b/packages/services/src/external-service-report/__tests__/record.test.ts index 853831f78..b9f31f928 100644 --- a/packages/services/src/external-service-report/__tests__/record.test.ts +++ b/packages/services/src/external-service-report/__tests__/record.test.ts @@ -4,8 +4,7 @@ import { externalServiceComponent, externalServiceReport, } from "@openstatus/db/src/schema"; -import { expect } from "@std/expect"; -import { afterEach, describe, test } from "@std/testing/bdd"; +import { afterEach, describe, expect, test } from "vitest"; import { NotFoundError } from "../../errors"; import { diff --git a/packages/services/src/external-service/__tests__/external-service.test.ts b/packages/services/src/external-service/__tests__/external-service.test.ts index 6dc0c8977..be3791012 100644 --- a/packages/services/src/external-service/__tests__/external-service.test.ts +++ b/packages/services/src/external-service/__tests__/external-service.test.ts @@ -1,7 +1,6 @@ import { db, like, sql } from "@openstatus/db"; import { externalService } from "@openstatus/db/src/schema"; -import { expect } from "@std/expect"; -import { afterEach, describe, test } from "@std/testing/bdd"; +import { afterEach, describe, expect, test } from "vitest"; import { ConflictError } from "../../errors"; import { assertSlugAvailable } from "../internal"; diff --git a/packages/services/src/external-service/__tests__/update-provider.test.ts b/packages/services/src/external-service/__tests__/update-provider.test.ts index 5eb9cec57..957edabaa 100644 --- a/packages/services/src/external-service/__tests__/update-provider.test.ts +++ b/packages/services/src/external-service/__tests__/update-provider.test.ts @@ -1,7 +1,6 @@ import { db, eq, like } from "@openstatus/db"; import { externalService } from "@openstatus/db/src/schema"; -import { expect } from "@std/expect"; -import { afterEach, describe, test } from "@std/testing/bdd"; +import { afterEach, describe, expect, test } from "vitest"; import { applyDetectedProvider } from "../update-provider"; diff --git a/packages/services/src/frozen-uptime/__tests__/compute.test.ts b/packages/services/src/frozen-uptime/__tests__/compute.test.ts index c7464b513..10a1fc0ab 100644 --- a/packages/services/src/frozen-uptime/__tests__/compute.test.ts +++ b/packages/services/src/frozen-uptime/__tests__/compute.test.ts @@ -1,5 +1,4 @@ -import { expect } from "@std/expect"; -import { describe, test } from "@std/testing/bdd"; +import { describe, expect, test } from "vitest"; import { type ComputeCountRow, diff --git a/packages/services/src/frozen-uptime/__tests__/freeze.test.ts b/packages/services/src/frozen-uptime/__tests__/freeze.test.ts index 5f20f598f..fafc0877f 100644 --- a/packages/services/src/frozen-uptime/__tests__/freeze.test.ts +++ b/packages/services/src/frozen-uptime/__tests__/freeze.test.ts @@ -4,8 +4,7 @@ import { monitor, workspace, } from "@openstatus/db/src/schema"; -import { expect } from "@std/expect"; -import { beforeAll, describe, test } from "@std/testing/bdd"; +import { beforeAll, describe, expect, test } from "vitest"; import { createWorkspaceFixture, diff --git a/packages/services/src/frozen-uptime/__tests__/get-history.test.ts b/packages/services/src/frozen-uptime/__tests__/get-history.test.ts index 2a86168cf..f214bfcc8 100644 --- a/packages/services/src/frozen-uptime/__tests__/get-history.test.ts +++ b/packages/services/src/frozen-uptime/__tests__/get-history.test.ts @@ -10,8 +10,7 @@ import { statusReportUpdateToPageComponents, } from "@openstatus/db/src/schema"; import type { FrozenMonitorUptimeDay } from "@openstatus/db/src/schema"; -import { expect } from "@std/expect"; -import { beforeAll, describe, test } from "@std/testing/bdd"; +import { beforeAll, describe, expect, test } from "vitest"; import { createWorkspaceFixture, diff --git a/packages/services/src/frozen-uptime/__tests__/run.test.ts b/packages/services/src/frozen-uptime/__tests__/run.test.ts index dcb7d4862..e31fccfa2 100644 --- a/packages/services/src/frozen-uptime/__tests__/run.test.ts +++ b/packages/services/src/frozen-uptime/__tests__/run.test.ts @@ -1,7 +1,6 @@ import { eq } from "@openstatus/db"; import { frozenMonitorUptime, monitor } from "@openstatus/db/src/schema"; -import { expect } from "@std/expect"; -import { describe, test } from "@std/testing/bdd"; +import { describe, expect, test } from "vitest"; import { SEEDED_WORKSPACE_TEAM_ID } from "../../../test/fixtures"; import { withTestTransaction } from "../../../test/helpers"; diff --git a/packages/services/src/frozen-uptime/__tests__/schemas.test.ts b/packages/services/src/frozen-uptime/__tests__/schemas.test.ts index 1250673de..5cc8f3438 100644 --- a/packages/services/src/frozen-uptime/__tests__/schemas.test.ts +++ b/packages/services/src/frozen-uptime/__tests__/schemas.test.ts @@ -1,5 +1,4 @@ -import { expect } from "@std/expect"; -import { describe, test } from "@std/testing/bdd"; +import { describe, expect, test } from "vitest"; import { FreezeMonitorMonthInput } from "../schemas"; diff --git a/packages/services/src/import/__tests__/attribution.test.ts b/packages/services/src/import/__tests__/attribution.test.ts index 6385cf854..61fa478b2 100644 --- a/packages/services/src/import/__tests__/attribution.test.ts +++ b/packages/services/src/import/__tests__/attribution.test.ts @@ -6,8 +6,7 @@ import { statusReportUpdate, } from "@openstatus/db/src/schema"; import type { PhaseResult } from "@openstatus/importers"; -import { expect } from "@std/expect"; -import { beforeAll, describe, test } from "@std/testing/bdd"; +import { beforeAll, describe, expect, test } from "vitest"; import { createWorkspaceFixture, diff --git a/packages/services/src/import/__tests__/dedupe-and-atomicity.test.ts b/packages/services/src/import/__tests__/dedupe-and-atomicity.test.ts index f65d68880..563d1cd27 100644 --- a/packages/services/src/import/__tests__/dedupe-and-atomicity.test.ts +++ b/packages/services/src/import/__tests__/dedupe-and-atomicity.test.ts @@ -10,8 +10,7 @@ import { statusReportUpdate, } from "@openstatus/db/src/schema"; import type { PhaseResult } from "@openstatus/importers"; -import { expect } from "@std/expect"; -import { afterAll, beforeAll, describe, test } from "@std/testing/bdd"; +import { afterAll, beforeAll, describe, expect, test } from "vitest"; import { clearAuditLogFor, diff --git a/packages/services/src/import/__tests__/incidents-phase.test.ts b/packages/services/src/import/__tests__/incidents-phase.test.ts index 77e3514ff..94c186a88 100644 --- a/packages/services/src/import/__tests__/incidents-phase.test.ts +++ b/packages/services/src/import/__tests__/incidents-phase.test.ts @@ -8,8 +8,7 @@ import { statusReportsToPageComponents, } from "@openstatus/db/src/schema"; import type { PhaseResult } from "@openstatus/importers"; -import { expect } from "@std/expect"; -import { beforeAll, describe, test } from "@std/testing/bdd"; +import { beforeAll, describe, expect, test } from "vitest"; import { createWorkspaceFixture, diff --git a/packages/services/src/import/__tests__/unit.test.ts b/packages/services/src/import/__tests__/unit.test.ts index ad3b74cfa..aa2addd95 100644 --- a/packages/services/src/import/__tests__/unit.test.ts +++ b/packages/services/src/import/__tests__/unit.test.ts @@ -3,8 +3,7 @@ import { monitor, pageComponent } from "@openstatus/db/src/schema"; import { allPlans } from "@openstatus/db/src/schema/plan/config"; import type { Limits } from "@openstatus/db/src/schema/plan/schema"; import type { ImportSummary } from "@openstatus/importers"; -import { expect } from "@std/expect"; -import { beforeAll, describe, test } from "@std/testing/bdd"; +import { beforeAll, describe, expect, test } from "vitest"; import { addLimitWarnings, clampPeriodicity, computePhaseStatus } from "../"; import { withTestTransaction } from "../../../test/helpers"; diff --git a/packages/services/src/incident/__tests__/effects.test.ts b/packages/services/src/incident/__tests__/effects.test.ts index 7447190ed..be84f8b44 100644 --- a/packages/services/src/incident/__tests__/effects.test.ts +++ b/packages/services/src/incident/__tests__/effects.test.ts @@ -4,8 +4,7 @@ import { addUserToWorkspace, createUser, } from "@openstatus/db/src/test/factories"; -import { expect } from "@std/expect"; -import { beforeAll, describe, test } from "@std/testing/bdd"; +import { beforeAll, describe, expect, test } from "vitest"; import { createWorkspaceFixture, diff --git a/packages/services/src/incident/__tests__/event-source.test.ts b/packages/services/src/incident/__tests__/event-source.test.ts index bf3489ded..a5a9d170b 100644 --- a/packages/services/src/incident/__tests__/event-source.test.ts +++ b/packages/services/src/incident/__tests__/event-source.test.ts @@ -2,8 +2,7 @@ import { addUserToWorkspace, createUser, } from "@openstatus/db/src/test/factories"; -import { expect } from "@std/expect"; -import { beforeAll, describe, test } from "@std/testing/bdd"; +import { beforeAll, describe, expect, test } from "vitest"; import { createWorkspaceFixture, diff --git a/packages/services/src/incident/__tests__/hooks.test.ts b/packages/services/src/incident/__tests__/hooks.test.ts index 0f149c6d5..ceaf298df 100644 --- a/packages/services/src/incident/__tests__/hooks.test.ts +++ b/packages/services/src/incident/__tests__/hooks.test.ts @@ -6,8 +6,7 @@ import { createSlackUser, createUser, } from "@openstatus/db/src/test/factories"; -import { expect } from "@std/expect"; -import { beforeAll, describe, test } from "@std/testing/bdd"; +import { beforeAll, describe, expect, test } from "vitest"; import { createWorkspaceFixture, diff --git a/packages/services/src/incident/__tests__/incident.test.ts b/packages/services/src/incident/__tests__/incident.test.ts index ddb4ab98d..ea5720588 100644 --- a/packages/services/src/incident/__tests__/incident.test.ts +++ b/packages/services/src/incident/__tests__/incident.test.ts @@ -14,8 +14,7 @@ import { createMonitor, createUser, } from "@openstatus/db/src/test/factories"; -import { expect } from "@std/expect"; -import { beforeAll, describe, test } from "@std/testing/bdd"; +import { beforeAll, describe, expect, test } from "vitest"; import { clearAuditLogFor, diff --git a/packages/services/src/incident/__tests__/legacy-source.test.ts b/packages/services/src/incident/__tests__/legacy-source.test.ts index 69d005215..e325b1088 100644 --- a/packages/services/src/incident/__tests__/legacy-source.test.ts +++ b/packages/services/src/incident/__tests__/legacy-source.test.ts @@ -1,5 +1,4 @@ -import { expect } from "@std/expect"; -import { describe, test } from "@std/testing/bdd"; +import { describe, expect, test } from "vitest"; import { withLegacySource } from "../legacy-source"; diff --git a/packages/services/src/incident/__tests__/postmortem-draft.test.ts b/packages/services/src/incident/__tests__/postmortem-draft.test.ts index 1b8b4e09a..bbe716438 100644 --- a/packages/services/src/incident/__tests__/postmortem-draft.test.ts +++ b/packages/services/src/incident/__tests__/postmortem-draft.test.ts @@ -1,6 +1,5 @@ import { integration } from "@openstatus/db/src/schema"; -import { expect } from "@std/expect"; -import { beforeAll, describe, test } from "@std/testing/bdd"; +import { beforeAll, describe, expect, test } from "vitest"; import { createWorkspaceFixture, diff --git a/packages/services/src/incident/__tests__/postmortem.test.ts b/packages/services/src/incident/__tests__/postmortem.test.ts index c1a749daf..ea4a0fffb 100644 --- a/packages/services/src/incident/__tests__/postmortem.test.ts +++ b/packages/services/src/incident/__tests__/postmortem.test.ts @@ -4,8 +4,7 @@ import { addUserToWorkspace, createUser, } from "@openstatus/db/src/test/factories"; -import { expect } from "@std/expect"; -import { beforeAll, describe, test } from "@std/testing/bdd"; +import { beforeAll, describe, expect, test } from "vitest"; import { createWorkspaceFixture, diff --git a/packages/services/src/incident/__tests__/reminders.test.ts b/packages/services/src/incident/__tests__/reminders.test.ts index cf89978b5..f0a711861 100644 --- a/packages/services/src/incident/__tests__/reminders.test.ts +++ b/packages/services/src/incident/__tests__/reminders.test.ts @@ -4,8 +4,7 @@ import { createIncidentEvent, createSlackUser, } from "@openstatus/db/src/test/factories"; -import { expect } from "@std/expect"; -import { beforeAll, describe, test } from "@std/testing/bdd"; +import { beforeAll, describe, expect, test } from "vitest"; import { createWorkspaceFixture, diff --git a/packages/services/src/incident/__tests__/slack-flow.test.ts b/packages/services/src/incident/__tests__/slack-flow.test.ts index d5647d7ff..8ec9f79d1 100644 --- a/packages/services/src/incident/__tests__/slack-flow.test.ts +++ b/packages/services/src/incident/__tests__/slack-flow.test.ts @@ -4,8 +4,7 @@ import { addUserToWorkspace, createUser, } from "@openstatus/db/src/test/factories"; -import { expect } from "@std/expect"; -import { beforeAll, describe, test } from "@std/testing/bdd"; +import { beforeAll, describe, expect, test } from "vitest"; import { createWorkspaceFixture, diff --git a/packages/services/src/integration/__tests__/install-slack-agent.test.ts b/packages/services/src/integration/__tests__/install-slack-agent.test.ts index fd9e3e9a9..c136ea825 100644 --- a/packages/services/src/integration/__tests__/install-slack-agent.test.ts +++ b/packages/services/src/integration/__tests__/install-slack-agent.test.ts @@ -1,7 +1,6 @@ import { and, desc, eq } from "@openstatus/db"; import { integration } from "@openstatus/db/src/schema"; -import { expect } from "@std/expect"; -import { beforeAll, describe, test } from "@std/testing/bdd"; +import { beforeAll, describe, expect, test } from "vitest"; import { clearAuditLog, diff --git a/packages/services/src/integration/__tests__/slack-scopes.test.ts b/packages/services/src/integration/__tests__/slack-scopes.test.ts index df658546d..f7ad66a56 100644 --- a/packages/services/src/integration/__tests__/slack-scopes.test.ts +++ b/packages/services/src/integration/__tests__/slack-scopes.test.ts @@ -1,5 +1,4 @@ -import { expect } from "@std/expect"; -import { describe, test } from "@std/testing/bdd"; +import { describe, expect, test } from "vitest"; import { SLACK_BOT_SCOPES, missingSlackScopes } from "../slack-scopes"; diff --git a/packages/services/src/integration/__tests__/uninstall-slack-agent.test.ts b/packages/services/src/integration/__tests__/uninstall-slack-agent.test.ts index 8ee389377..24e0cc394 100644 --- a/packages/services/src/integration/__tests__/uninstall-slack-agent.test.ts +++ b/packages/services/src/integration/__tests__/uninstall-slack-agent.test.ts @@ -10,8 +10,7 @@ import { createPage, createSlackUser, } from "@openstatus/db/src/test/factories"; -import { expect } from "@std/expect"; -import { describe, test } from "@std/testing/bdd"; +import { describe, expect, test } from "vitest"; import { createWorkspaceFixture, diff --git a/packages/services/src/invitation/__tests__/invitation.test.ts b/packages/services/src/invitation/__tests__/invitation.test.ts index 8fcd501eb..507d4c53d 100644 --- a/packages/services/src/invitation/__tests__/invitation.test.ts +++ b/packages/services/src/invitation/__tests__/invitation.test.ts @@ -5,8 +5,7 @@ import { workspace, } from "@openstatus/db/src/schema"; import { createUser } from "@openstatus/db/src/test/factories"; -import { expect } from "@std/expect"; -import { beforeAll, describe, test } from "@std/testing/bdd"; +import { beforeAll, describe, expect, test } from "vitest"; import { createWorkspaceFixture, diff --git a/packages/services/src/maintenance/__tests__/attribution.test.ts b/packages/services/src/maintenance/__tests__/attribution.test.ts index bb46958ea..41f46e650 100644 --- a/packages/services/src/maintenance/__tests__/attribution.test.ts +++ b/packages/services/src/maintenance/__tests__/attribution.test.ts @@ -1,8 +1,7 @@ import { db, eq } from "@openstatus/db"; import { maintenance, page } from "@openstatus/db/src/schema"; import { createUser } from "@openstatus/db/src/test/factories"; -import { expect } from "@std/expect"; -import { beforeAll, describe, test } from "@std/testing/bdd"; +import { beforeAll, describe, expect, test } from "vitest"; import { createWorkspaceFixture, diff --git a/packages/services/src/maintenance/__tests__/maintenance.test.ts b/packages/services/src/maintenance/__tests__/maintenance.test.ts index 98cdcb359..cf4bc1398 100644 --- a/packages/services/src/maintenance/__tests__/maintenance.test.ts +++ b/packages/services/src/maintenance/__tests__/maintenance.test.ts @@ -5,14 +5,14 @@ import { page, pageComponent, } from "@openstatus/db/src/schema"; -import { expect } from "@std/expect"; import { afterAll, beforeAll, beforeEach, describe, + expect, test, -} from "@std/testing/bdd"; +} from "vitest"; import { expectAuditRow, diff --git a/packages/services/src/member/__tests__/display-name.test.ts b/packages/services/src/member/__tests__/display-name.test.ts index 8cc7adc83..595b11b50 100644 --- a/packages/services/src/member/__tests__/display-name.test.ts +++ b/packages/services/src/member/__tests__/display-name.test.ts @@ -2,8 +2,7 @@ import { addUserToWorkspace, createUser, } from "@openstatus/db/src/test/factories"; -import { expect } from "@std/expect"; -import { beforeAll, describe, test } from "@std/testing/bdd"; +import { beforeAll, describe, expect, test } from "vitest"; import { createWorkspaceFixture, makeSystemCtx } from "../../../test/helpers"; import type { ServiceContext } from "../../context"; diff --git a/packages/services/src/member/__tests__/find-by-email.test.ts b/packages/services/src/member/__tests__/find-by-email.test.ts index f08197e96..8aa569a65 100644 --- a/packages/services/src/member/__tests__/find-by-email.test.ts +++ b/packages/services/src/member/__tests__/find-by-email.test.ts @@ -4,8 +4,7 @@ import { addUserToWorkspace, createUser, } from "@openstatus/db/src/test/factories"; -import { expect } from "@std/expect"; -import { beforeAll, describe, test } from "@std/testing/bdd"; +import { beforeAll, describe, expect, test } from "vitest"; import { createWorkspaceFixture, diff --git a/packages/services/src/member/__tests__/member.test.ts b/packages/services/src/member/__tests__/member.test.ts index ce1c1953f..179dec3b8 100644 --- a/packages/services/src/member/__tests__/member.test.ts +++ b/packages/services/src/member/__tests__/member.test.ts @@ -4,8 +4,7 @@ import { addUserToWorkspace, createUser, } from "@openstatus/db/src/test/factories"; -import { expect } from "@std/expect"; -import { beforeAll, describe, test } from "@std/testing/bdd"; +import { beforeAll, describe, expect, test } from "vitest"; import { createWorkspaceFixture, diff --git a/packages/services/src/member/__tests__/transfer-ownership.test.ts b/packages/services/src/member/__tests__/transfer-ownership.test.ts index 643962ffb..42f7f6890 100644 --- a/packages/services/src/member/__tests__/transfer-ownership.test.ts +++ b/packages/services/src/member/__tests__/transfer-ownership.test.ts @@ -4,8 +4,7 @@ import { addUserToWorkspace, createUser, } from "@openstatus/db/src/test/factories"; -import { expect } from "@std/expect"; -import { afterAll, beforeAll, describe, test } from "@std/testing/bdd"; +import { afterAll, beforeAll, describe, expect, test } from "vitest"; import { clearAuditLogFor, diff --git a/packages/services/src/monitor-incident/__tests__/monitor-incident.test.ts b/packages/services/src/monitor-incident/__tests__/monitor-incident.test.ts index cfcb02e09..a5cbf141f 100644 --- a/packages/services/src/monitor-incident/__tests__/monitor-incident.test.ts +++ b/packages/services/src/monitor-incident/__tests__/monitor-incident.test.ts @@ -1,7 +1,6 @@ import { db, eq } from "@openstatus/db"; import { monitorIncidentTable, monitor } from "@openstatus/db/src/schema"; -import { expect } from "@std/expect"; -import { afterAll, beforeAll, describe, test } from "@std/testing/bdd"; +import { afterAll, beforeAll, describe, expect, test } from "vitest"; import { expectAuditRow, diff --git a/packages/services/src/monitor-tag/__tests__/monitor-tag.test.ts b/packages/services/src/monitor-tag/__tests__/monitor-tag.test.ts index 1f8e4712a..4f983ff76 100644 --- a/packages/services/src/monitor-tag/__tests__/monitor-tag.test.ts +++ b/packages/services/src/monitor-tag/__tests__/monitor-tag.test.ts @@ -1,7 +1,6 @@ import { eq } from "@openstatus/db"; import { monitorTag, workspace } from "@openstatus/db/src/schema"; -import { expect } from "@std/expect"; -import { beforeAll, describe, test } from "@std/testing/bdd"; +import { beforeAll, describe, expect, test } from "vitest"; import { clearAuditLog, diff --git a/packages/services/src/monitor/__tests__/monitor.test.ts b/packages/services/src/monitor/__tests__/monitor.test.ts index 29699f5f3..e45598ca9 100644 --- a/packages/services/src/monitor/__tests__/monitor.test.ts +++ b/packages/services/src/monitor/__tests__/monitor.test.ts @@ -10,8 +10,7 @@ import { workspace, } from "@openstatus/db/src/schema"; import { createWorkspace } from "@openstatus/db/src/test/factories"; -import { expect } from "@std/expect"; -import { afterAll, beforeAll, describe, test } from "@std/testing/bdd"; +import { afterAll, beforeAll, describe, expect, test } from "vitest"; import { clearAuditLog, diff --git a/packages/services/src/monitor/__tests__/reads.test.ts b/packages/services/src/monitor/__tests__/reads.test.ts index 61416d7dd..4fea52569 100644 --- a/packages/services/src/monitor/__tests__/reads.test.ts +++ b/packages/services/src/monitor/__tests__/reads.test.ts @@ -5,8 +5,7 @@ import { privateLocation, privateLocationToMonitors, } from "@openstatus/db/src/schema"; -import { expect } from "@std/expect"; -import { beforeAll, describe, test } from "@std/testing/bdd"; +import { beforeAll, describe, expect, test } from "vitest"; import { createWorkspaceFixture, diff --git a/packages/services/src/monitor/__tests__/response-logs-cursor.test.ts b/packages/services/src/monitor/__tests__/response-logs-cursor.test.ts index 3432605b0..7854c3b02 100644 --- a/packages/services/src/monitor/__tests__/response-logs-cursor.test.ts +++ b/packages/services/src/monitor/__tests__/response-logs-cursor.test.ts @@ -1,5 +1,4 @@ -import { expect } from "@std/expect"; -import { describe, test } from "@std/testing/bdd"; +import { describe, expect, test } from "vitest"; import { selectWindow, diff --git a/packages/services/src/monitor/__tests__/response-logs-internal.test.ts b/packages/services/src/monitor/__tests__/response-logs-internal.test.ts index d483f1bdf..978674f0e 100644 --- a/packages/services/src/monitor/__tests__/response-logs-internal.test.ts +++ b/packages/services/src/monitor/__tests__/response-logs-internal.test.ts @@ -1,5 +1,4 @@ -import { expect } from "@std/expect"; -import { describe, test } from "@std/testing/bdd"; +import { describe, expect, test } from "vitest"; import { redactSensitiveBody } from "../response-logs-internal"; diff --git a/packages/services/src/monitor/__tests__/stream-monitor-preview.test.ts b/packages/services/src/monitor/__tests__/stream-monitor-preview.test.ts index 7e777e13e..dcb211dca 100644 --- a/packages/services/src/monitor/__tests__/stream-monitor-preview.test.ts +++ b/packages/services/src/monitor/__tests__/stream-monitor-preview.test.ts @@ -1,5 +1,4 @@ -import { expect } from "@std/expect"; -import { afterAll, beforeAll, describe, test } from "@std/testing/bdd"; +import { afterAll, beforeAll, describe, expect, test } from "vitest"; import { createWorkspaceFixture, diff --git a/packages/services/src/monitor/__tests__/url-safety.test.ts b/packages/services/src/monitor/__tests__/url-safety.test.ts index 4d7256af1..414cbd098 100644 --- a/packages/services/src/monitor/__tests__/url-safety.test.ts +++ b/packages/services/src/monitor/__tests__/url-safety.test.ts @@ -1,7 +1,6 @@ import { selectWorkspaceSchema } from "@openstatus/db/src/schema"; import { createWorkspace } from "@openstatus/db/src/test/factories"; -import { expect } from "@std/expect"; -import { beforeAll, describe, test } from "@std/testing/bdd"; +import { beforeAll, describe, expect, test } from "vitest"; import { makeUserCtx, withTestTransaction } from "../../../test/helpers"; import type { ServiceContext } from "../../context"; diff --git a/packages/services/src/notification/__tests__/notification.test.ts b/packages/services/src/notification/__tests__/notification.test.ts index b66915efd..c9f864ee3 100644 --- a/packages/services/src/notification/__tests__/notification.test.ts +++ b/packages/services/src/notification/__tests__/notification.test.ts @@ -7,8 +7,7 @@ import { workspace, } from "@openstatus/db/src/schema"; import { getLimits } from "@openstatus/db/src/schema/plan/utils"; -import { expect } from "@std/expect"; -import { afterAll, beforeAll, describe, test } from "@std/testing/bdd"; +import { afterAll, beforeAll, describe, expect, test } from "vitest"; import { expectAuditRow, diff --git a/packages/services/src/oauth/__tests__/cimd.test.ts b/packages/services/src/oauth/__tests__/cimd.test.ts index dd9960411..bbbc4666b 100644 --- a/packages/services/src/oauth/__tests__/cimd.test.ts +++ b/packages/services/src/oauth/__tests__/cimd.test.ts @@ -1,7 +1,6 @@ import { eq } from "@openstatus/db"; import { oauthClient } from "@openstatus/db/src/schema"; -import { expect } from "@std/expect"; -import { beforeAll, describe, test } from "@std/testing/bdd"; +import { beforeAll, describe, expect, test } from "vitest"; import { createWorkspaceFixture, diff --git a/packages/services/src/oauth/__tests__/crypto.test.ts b/packages/services/src/oauth/__tests__/crypto.test.ts index 9bc0f3714..a1b753b76 100644 --- a/packages/services/src/oauth/__tests__/crypto.test.ts +++ b/packages/services/src/oauth/__tests__/crypto.test.ts @@ -1,5 +1,4 @@ -import { expect } from "@std/expect"; -import { describe, test } from "@std/testing/bdd"; +import { describe, expect, test } from "vitest"; import { constantTimeEqual, diff --git a/packages/services/src/oauth/__tests__/metadata.test.ts b/packages/services/src/oauth/__tests__/metadata.test.ts index b7f950b33..3a7e8e78a 100644 --- a/packages/services/src/oauth/__tests__/metadata.test.ts +++ b/packages/services/src/oauth/__tests__/metadata.test.ts @@ -1,5 +1,4 @@ -import { expect } from "@std/expect"; -import { describe, test } from "@std/testing/bdd"; +import { describe, expect, test } from "vitest"; import { authorizationServerMetadata, diff --git a/packages/services/src/oauth/__tests__/oauth.test.ts b/packages/services/src/oauth/__tests__/oauth.test.ts index cdc14e378..60b657d5d 100644 --- a/packages/services/src/oauth/__tests__/oauth.test.ts +++ b/packages/services/src/oauth/__tests__/oauth.test.ts @@ -11,8 +11,7 @@ import { addUserToWorkspace, createUser, } from "@openstatus/db/src/test/factories"; -import { expect } from "@std/expect"; -import { beforeAll, describe, test } from "@std/testing/bdd"; +import { beforeAll, describe, expect, test } from "vitest"; import { createWorkspaceFixture, diff --git a/packages/services/src/oauth/__tests__/redirect-allowlist.test.ts b/packages/services/src/oauth/__tests__/redirect-allowlist.test.ts index 07b79b43d..c80a404c1 100644 --- a/packages/services/src/oauth/__tests__/redirect-allowlist.test.ts +++ b/packages/services/src/oauth/__tests__/redirect-allowlist.test.ts @@ -1,5 +1,4 @@ -import { expect } from "@std/expect"; -import { describe, test } from "@std/testing/bdd"; +import { describe, expect, test } from "vitest"; import { isAllowedRedirectUri, diff --git a/packages/services/src/oauth/__tests__/schemas.test.ts b/packages/services/src/oauth/__tests__/schemas.test.ts index e1e5971fd..2927ee542 100644 --- a/packages/services/src/oauth/__tests__/schemas.test.ts +++ b/packages/services/src/oauth/__tests__/schemas.test.ts @@ -1,5 +1,4 @@ -import { expect } from "@std/expect"; -import { describe, test } from "@std/testing/bdd"; +import { describe, expect, test } from "vitest"; import { RegisterClientInput, diff --git a/packages/services/src/page-access/access-predicates.test.ts b/packages/services/src/page-access/access-predicates.test.ts index e5d7bb223..5f1ef8921 100644 --- a/packages/services/src/page-access/access-predicates.test.ts +++ b/packages/services/src/page-access/access-predicates.test.ts @@ -1,5 +1,4 @@ -import { expect } from "@std/expect"; -import { describe, test } from "@std/testing/bdd"; +import { describe, expect, test } from "vitest"; import { isEmailDomainAuthorized, diff --git a/packages/services/src/page-access/client-ip.test.ts b/packages/services/src/page-access/client-ip.test.ts index 45b2f7c42..0d1f7a858 100644 --- a/packages/services/src/page-access/client-ip.test.ts +++ b/packages/services/src/page-access/client-ip.test.ts @@ -1,5 +1,4 @@ -import { expect } from "@std/expect"; -import { describe, test } from "@std/testing/bdd"; +import { describe, expect, test } from "vitest"; import { resolveClientIp } from "./client-ip"; diff --git a/packages/services/src/page-access/evaluate-markdown-gate.test.ts b/packages/services/src/page-access/evaluate-markdown-gate.test.ts index 2386573a4..4a76ea45a 100644 --- a/packages/services/src/page-access/evaluate-markdown-gate.test.ts +++ b/packages/services/src/page-access/evaluate-markdown-gate.test.ts @@ -1,5 +1,4 @@ -import { expect } from "@std/expect"; -import { describe, test } from "@std/testing/bdd"; +import { describe, expect, test } from "vitest"; import { evaluateMarkdownGate } from "./evaluate-markdown-gate"; diff --git a/packages/services/src/page-access/is-ip-allowed.test.ts b/packages/services/src/page-access/is-ip-allowed.test.ts index c963ca317..8aecc42a5 100644 --- a/packages/services/src/page-access/is-ip-allowed.test.ts +++ b/packages/services/src/page-access/is-ip-allowed.test.ts @@ -1,5 +1,4 @@ -import { expect } from "@std/expect"; -import { describe, test } from "@std/testing/bdd"; +import { describe, expect, test } from "vitest"; import { isIpAllowed } from "./is-ip-allowed"; diff --git a/packages/services/src/page-component-group/__tests__/page-component-group.test.ts b/packages/services/src/page-component-group/__tests__/page-component-group.test.ts index 0eb1e637b..a6b51cbee 100644 --- a/packages/services/src/page-component-group/__tests__/page-component-group.test.ts +++ b/packages/services/src/page-component-group/__tests__/page-component-group.test.ts @@ -1,7 +1,6 @@ import { db, eq } from "@openstatus/db"; import { page, pageComponentGroup } from "@openstatus/db/src/schema"; -import { expect } from "@std/expect"; -import { afterAll, beforeAll, describe, test } from "@std/testing/bdd"; +import { afterAll, beforeAll, describe, expect, test } from "vitest"; import { createWorkspaceFixture, diff --git a/packages/services/src/page-component/__tests__/get-daily-summary.test.ts b/packages/services/src/page-component/__tests__/get-daily-summary.test.ts index 9f10c94a0..342420d33 100644 --- a/packages/services/src/page-component/__tests__/get-daily-summary.test.ts +++ b/packages/services/src/page-component/__tests__/get-daily-summary.test.ts @@ -9,8 +9,7 @@ import { statusReportUpdateToPageComponents, statusReportsToPageComponents, } from "@openstatus/db/src/schema"; -import { expect } from "@std/expect"; -import { beforeAll, describe, test } from "@std/testing/bdd"; +import { beforeAll, describe, expect, test } from "vitest"; import { createWorkspaceFixture, diff --git a/packages/services/src/page-component/__tests__/page-component.test.ts b/packages/services/src/page-component/__tests__/page-component.test.ts index 3c24d041e..66c19ff06 100644 --- a/packages/services/src/page-component/__tests__/page-component.test.ts +++ b/packages/services/src/page-component/__tests__/page-component.test.ts @@ -6,8 +6,7 @@ import { pageComponentGroup, workspace, } from "@openstatus/db/src/schema"; -import { expect } from "@std/expect"; -import { afterAll, beforeAll, describe, test } from "@std/testing/bdd"; +import { afterAll, beforeAll, describe, expect, test } from "vitest"; import { expectAuditRow, diff --git a/packages/services/src/page-subscriber/__tests__/page-subscriber.test.ts b/packages/services/src/page-subscriber/__tests__/page-subscriber.test.ts index b453e054f..563db004f 100644 --- a/packages/services/src/page-subscriber/__tests__/page-subscriber.test.ts +++ b/packages/services/src/page-subscriber/__tests__/page-subscriber.test.ts @@ -9,16 +9,16 @@ import { createPage, createPageComponent, } from "@openstatus/db/src/test/factories"; -import { expect } from "@std/expect"; import { afterAll, afterEach, beforeAll, beforeEach, describe, + expect, test, -} from "@std/testing/bdd"; -import { stub } from "@std/testing/mock"; + vi, +} from "vitest"; import { clearAuditLog, @@ -482,7 +482,7 @@ describe("upsertSelfSignupSubscriber", () => { test("rejects when the workspace plan disables status-subscribers", async () => { // Rolled-back tx so the free-workspace page never commits — a // committed page would trip `page.test.ts`'s free-plan - // `status-pages` quota assertions under `deno test --parallel`. + // `status-pages` quota assertions when the suites run in parallel. await withTestTransaction(async (tx) => { const freePage = await tx .insert(page) @@ -540,18 +540,20 @@ describe("verifySelfSignupSubscriber", () => { | undefined; const transaction = db.transaction.bind(db); // Commit the competing signup after token lookup, before acceptance starts. - const intercept = stub(db, "transaction", async (fn, config) => { - intercept.restore(); - expanded = await upsertSelfSignupSubscriber({ - visitor: null, - input: { - email: first.email, - pageId: PAGE_ID, - componentIds: scope === "entire page" ? [] : [COMPONENT_2], - }, + const intercept = vi + .spyOn(db, "transaction") + .mockImplementation(async (fn, config) => { + intercept.mockRestore(); + expanded = await upsertSelfSignupSubscriber({ + visitor: null, + input: { + email: first.email, + pageId: PAGE_ID, + componentIds: scope === "entire page" ? [] : [COMPONENT_2], + }, + }); + return transaction(fn, config); }); - return transaction(fn, config); - }); try { const result = await verifySelfSignupSubscriber({ input: { token: first.token, domain: PAGE_SLUG }, @@ -581,7 +583,7 @@ describe("verifySelfSignupSubscriber", () => { : [COMPONENT_1, COMPONENT_2].sort((a, b) => a - b), ); } finally { - if (!intercept.restored) intercept.restore(); + intercept.mockRestore(); await db.delete(pageSubscriber).where(eq(pageSubscriber.id, first.id)); await clearAuditLogFor({ entityType: "page_subscriber", diff --git a/packages/services/src/page-subscriber/__tests__/slack.test.ts b/packages/services/src/page-subscriber/__tests__/slack.test.ts index 28c7305b6..cadde9dcf 100644 --- a/packages/services/src/page-subscriber/__tests__/slack.test.ts +++ b/packages/services/src/page-subscriber/__tests__/slack.test.ts @@ -1,8 +1,7 @@ import { db, eq, inArray } from "@openstatus/db"; import { pageSubscriber } from "@openstatus/db/src/schema"; import { createPage } from "@openstatus/db/src/test/factories"; -import { expect } from "@std/expect"; -import { afterAll, beforeAll, describe, test } from "@std/testing/bdd"; +import { afterAll, beforeAll, describe, expect, test } from "vitest"; import { createWorkspaceFixture, expectAuditRow } from "../../../test/helpers"; import { PreconditionFailedError } from "../../errors"; diff --git a/packages/services/src/page/__tests__/page.test.ts b/packages/services/src/page/__tests__/page.test.ts index c391e37c7..46158472b 100644 --- a/packages/services/src/page/__tests__/page.test.ts +++ b/packages/services/src/page/__tests__/page.test.ts @@ -8,8 +8,7 @@ import { workspace, } from "@openstatus/db/src/schema"; import { getLimits } from "@openstatus/db/src/schema/plan/utils"; -import { expect } from "@std/expect"; -import { afterAll, beforeAll, describe, test } from "@std/testing/bdd"; +import { afterAll, beforeAll, describe, expect, test } from "vitest"; import { createWorkspaceFixture, diff --git a/packages/services/src/page/__tests__/status-update-types.test.ts b/packages/services/src/page/__tests__/status-update-types.test.ts index ad75658c7..555ffcb00 100644 --- a/packages/services/src/page/__tests__/status-update-types.test.ts +++ b/packages/services/src/page/__tests__/status-update-types.test.ts @@ -1,5 +1,4 @@ -import { expect } from "@std/expect"; -import { test } from "@std/testing/bdd"; +import { expect, test } from "vitest"; import { getStatusUpdateTypes } from "../status-update-types"; diff --git a/packages/services/src/private-location/__tests__/private-location.test.ts b/packages/services/src/private-location/__tests__/private-location.test.ts index e103fb5f6..410ddea33 100644 --- a/packages/services/src/private-location/__tests__/private-location.test.ts +++ b/packages/services/src/private-location/__tests__/private-location.test.ts @@ -7,8 +7,7 @@ import { workspace, } from "@openstatus/db/src/schema"; import { getLimits } from "@openstatus/db/src/schema/plan/utils"; -import { expect } from "@std/expect"; -import { afterAll, beforeAll, describe, test } from "@std/testing/bdd"; +import { afterAll, beforeAll, describe, expect, test } from "vitest"; import { createWorkspaceFixture, diff --git a/packages/services/src/slack-user/__tests__/slack-user.test.ts b/packages/services/src/slack-user/__tests__/slack-user.test.ts index 166f043b8..576497129 100644 --- a/packages/services/src/slack-user/__tests__/slack-user.test.ts +++ b/packages/services/src/slack-user/__tests__/slack-user.test.ts @@ -4,8 +4,7 @@ import { addUserToWorkspace, createUser, } from "@openstatus/db/src/test/factories"; -import { expect } from "@std/expect"; -import { beforeAll, describe, test } from "@std/testing/bdd"; +import { beforeAll, describe, expect, test } from "vitest"; import { createWorkspaceFixture, diff --git a/packages/services/src/sso/__tests__/sso.test.ts b/packages/services/src/sso/__tests__/sso.test.ts index 77b672e58..2ad919f8a 100644 --- a/packages/services/src/sso/__tests__/sso.test.ts +++ b/packages/services/src/sso/__tests__/sso.test.ts @@ -8,8 +8,7 @@ import { addUserToWorkspace, createUser, } from "@openstatus/db/src/test/factories"; -import { expect } from "@std/expect"; -import { beforeAll, describe, test } from "@std/testing/bdd"; +import { beforeAll, describe, expect, test } from "vitest"; import { createWorkspaceFixture, diff --git a/packages/services/src/status-report/__tests__/attribution.test.ts b/packages/services/src/status-report/__tests__/attribution.test.ts index da91cd0a4..326942d8a 100644 --- a/packages/services/src/status-report/__tests__/attribution.test.ts +++ b/packages/services/src/status-report/__tests__/attribution.test.ts @@ -5,8 +5,7 @@ import { statusReportUpdate, } from "@openstatus/db/src/schema"; import { createUser } from "@openstatus/db/src/test/factories"; -import { expect } from "@std/expect"; -import { beforeAll, describe, test } from "@std/testing/bdd"; +import { beforeAll, describe, expect, test } from "vitest"; import { createWorkspaceFixture, diff --git a/packages/services/src/status-report/__tests__/derive-status.test.ts b/packages/services/src/status-report/__tests__/derive-status.test.ts index fe6560ad4..c33639a72 100644 --- a/packages/services/src/status-report/__tests__/derive-status.test.ts +++ b/packages/services/src/status-report/__tests__/derive-status.test.ts @@ -4,8 +4,7 @@ import { statusReport, statusReportUpdate, } from "@openstatus/db/src/schema"; -import { expect } from "@std/expect"; -import { afterAll, beforeAll, describe, test } from "@std/testing/bdd"; +import { afterAll, beforeAll, describe, expect, test } from "vitest"; import { createWorkspaceFixture, diff --git a/packages/services/src/status-report/__tests__/incident-id.test.ts b/packages/services/src/status-report/__tests__/incident-id.test.ts index a80b58aa3..e28cb6a6d 100644 --- a/packages/services/src/status-report/__tests__/incident-id.test.ts +++ b/packages/services/src/status-report/__tests__/incident-id.test.ts @@ -1,8 +1,7 @@ import { db } from "@openstatus/db"; import { page } from "@openstatus/db/src/schema"; import { createIncident } from "@openstatus/db/src/test/factories"; -import { expect } from "@std/expect"; -import { beforeAll, describe, test } from "@std/testing/bdd"; +import { beforeAll, describe, expect, test } from "vitest"; import { createWorkspaceFixture, diff --git a/packages/services/src/status-report/__tests__/status-report.test.ts b/packages/services/src/status-report/__tests__/status-report.test.ts index 3bb4ecab9..5990cfde9 100644 --- a/packages/services/src/status-report/__tests__/status-report.test.ts +++ b/packages/services/src/status-report/__tests__/status-report.test.ts @@ -8,14 +8,14 @@ import { statusReportUpdate, statusReportUpdateToPageComponents, } from "@openstatus/db/src/schema"; -import { expect } from "@std/expect"; import { afterAll, beforeAll, beforeEach, describe, + expect, test, -} from "@std/testing/bdd"; +} from "vitest"; import { createWorkspaceFixture, diff --git a/packages/services/src/status-timeline/__tests__/downtime.test.ts b/packages/services/src/status-timeline/__tests__/downtime.test.ts index 61300a8f7..d67943e55 100644 --- a/packages/services/src/status-timeline/__tests__/downtime.test.ts +++ b/packages/services/src/status-timeline/__tests__/downtime.test.ts @@ -1,5 +1,4 @@ -import { expect } from "@std/expect"; -import { describe, test } from "@std/testing/bdd"; +import { describe, expect, test } from "vitest"; import { type WeightedInterval, mergedDowntimeMs } from "../downtime"; diff --git a/packages/services/src/status-timeline/__tests__/events.test.ts b/packages/services/src/status-timeline/__tests__/events.test.ts index 2fb8bf9ee..c7814f115 100644 --- a/packages/services/src/status-timeline/__tests__/events.test.ts +++ b/packages/services/src/status-timeline/__tests__/events.test.ts @@ -1,6 +1,4 @@ -import { expect } from "@std/expect"; -import { describe, test } from "@std/testing/bdd"; -import { FakeTime } from "@std/testing/time"; +import { describe, expect, test, vi } from "vitest"; import { activeReportStatus, @@ -104,7 +102,7 @@ describe("getEvents incident timestamps", () => { describe("getEvents lookback", () => { test("keeps overlapping intervals and clips incident downtime to the window", () => { - const time = new FakeTime(day("2026-03-01")); + vi.useFakeTimers({ now: day("2026-03-01") }); try { const from = day("2025-12-01"); const threshold = day("2026-01-15"); @@ -169,8 +167,8 @@ describe("getEvents lookback", () => { expect( durationDowntimeMs(events, { start: threshold.getTime(), - end: time.now, - now: time.now, + end: Date.now(), + now: Date.now(), }), ).toBe(3_888_000_000); expect( @@ -180,7 +178,7 @@ describe("getEvents lookback", () => { ).status, ).toBe("maintenance"); } finally { - time.restore(); + vi.useRealTimers(); } }); }); diff --git a/packages/services/src/status-timeline/__tests__/uptime.test.ts b/packages/services/src/status-timeline/__tests__/uptime.test.ts index 81f9cfebb..c81c2ce18 100644 --- a/packages/services/src/status-timeline/__tests__/uptime.test.ts +++ b/packages/services/src/status-timeline/__tests__/uptime.test.ts @@ -1,5 +1,4 @@ -import { expect } from "@std/expect"; -import { describe, test } from "@std/testing/bdd"; +import { describe, expect, test } from "vitest"; import type { Event } from "../events"; import { diff --git a/packages/services/src/user/__tests__/providers.test.ts b/packages/services/src/user/__tests__/providers.test.ts index 188f21689..476301919 100644 --- a/packages/services/src/user/__tests__/providers.test.ts +++ b/packages/services/src/user/__tests__/providers.test.ts @@ -1,8 +1,7 @@ import { db } from "@openstatus/db"; import { account } from "@openstatus/db/src/schema"; import { createUser } from "@openstatus/db/src/test/factories"; -import { expect } from "@std/expect"; -import { describe, test } from "@std/testing/bdd"; +import { describe, expect, test } from "vitest"; import { listUserProviders } from "../index.ts"; diff --git a/packages/services/src/workspace/__tests__/billing.test.ts b/packages/services/src/workspace/__tests__/billing.test.ts index 2161d9e9c..8ba9e06d5 100644 --- a/packages/services/src/workspace/__tests__/billing.test.ts +++ b/packages/services/src/workspace/__tests__/billing.test.ts @@ -6,8 +6,7 @@ import { createTestWorkspace, createUser, } from "@openstatus/db/src/test/factories"; -import { expect } from "@std/expect"; -import { describe, test } from "@std/testing/bdd"; +import { describe, expect, test } from "vitest"; import { ZodError } from "zod"; import { diff --git a/packages/services/src/workspace/__tests__/downgrade.test.ts b/packages/services/src/workspace/__tests__/downgrade.test.ts index 669e7f4b7..fd748620b 100644 --- a/packages/services/src/workspace/__tests__/downgrade.test.ts +++ b/packages/services/src/workspace/__tests__/downgrade.test.ts @@ -10,8 +10,7 @@ import { workspace, } from "@openstatus/db/src/schema"; import { getLimits } from "@openstatus/db/src/schema/plan/utils"; -import { expect } from "@std/expect"; -import { describe, test } from "@std/testing/bdd"; +import { describe, expect, test } from "vitest"; import { expectAuditRow, diff --git a/packages/services/src/workspace/__tests__/trial.test.ts b/packages/services/src/workspace/__tests__/trial.test.ts index 064dc5dbe..dfeebc0e0 100644 --- a/packages/services/src/workspace/__tests__/trial.test.ts +++ b/packages/services/src/workspace/__tests__/trial.test.ts @@ -6,8 +6,7 @@ import { createUser, createWorkspace, } from "@openstatus/db/src/test/factories"; -import { expect } from "@std/expect"; -import { describe, test } from "@std/testing/bdd"; +import { describe, expect, test } from "vitest"; import { makeUserCtx } from "../../../test/helpers"; import { diff --git a/packages/services/src/workspace/__tests__/workspace.test.ts b/packages/services/src/workspace/__tests__/workspace.test.ts index 2825c81fa..16efd35a9 100644 --- a/packages/services/src/workspace/__tests__/workspace.test.ts +++ b/packages/services/src/workspace/__tests__/workspace.test.ts @@ -9,8 +9,7 @@ import { workspace, } from "@openstatus/db/src/schema"; import { getLimits } from "@openstatus/db/src/schema/plan/utils"; -import { expect } from "@std/expect"; -import { beforeAll, describe, test } from "@std/testing/bdd"; +import { beforeAll, describe, expect, test } from "vitest"; import { createWorkspaceFixture, diff --git a/packages/services/test/helpers.ts b/packages/services/test/helpers.ts index bd5c0fa91..584359bd7 100644 --- a/packages/services/test/helpers.ts +++ b/packages/services/test/helpers.ts @@ -5,7 +5,7 @@ import { selectWorkspaceSchema, } from "@openstatus/db/src/schema"; import { createTestWorkspace } from "@openstatus/db/src/test/factories"; -import { expect } from "@std/expect"; +import { expect } from "vitest"; import type { Actor, DB, DrizzleTx, ServiceContext } from "../src/context"; import type { Workspace } from "../src/types"; diff --git a/packages/services/vitest.config.ts b/packages/services/vitest.config.ts new file mode 100644 index 000000000..26bdb37da --- /dev/null +++ b/packages/services/vitest.config.ts @@ -0,0 +1,3 @@ +import { defineTestConfig } from "@openstatus/test-utils/vitest"; + +export default defineTestConfig(import.meta.dirname); diff --git a/packages/status-fetcher/__tests__/detect.test.ts b/packages/status-fetcher/__tests__/detect.test.ts index e1f465fa4..f55e34baa 100644 --- a/packages/status-fetcher/__tests__/detect.test.ts +++ b/packages/status-fetcher/__tests__/detect.test.ts @@ -1,6 +1,5 @@ -import { expect } from "@std/expect"; -import { describe, it } from "@std/testing/bdd"; import { Effect } from "effect"; +import { describe, expect, it } from "vitest"; import { detectProvider } from "../src/detect"; import { installMockFetch } from "./helpers"; @@ -70,7 +69,7 @@ describe("detectProvider", () => { expect(result.currentProviderValidated).toBe(false); expect(result.matches.map((m) => m.provider)).toEqual(["instatus"]); expect(result.hostnameSuggestions).toEqual([]); - expect(fetchMock.calls.length).toBe(3); + expect(fetchMock.mock.calls.length).toBe(3); }); it("derives probe endpoints from urls with query or trailing slash", async () => { @@ -96,7 +95,7 @@ describe("detectProvider", () => { ); expect(result.currentProviderValidated).toBe(true); expect(result.matches).toEqual([]); - expect(fetchMock.calls.length).toBe(3); + expect(fetchMock.mock.calls.length).toBe(3); }); it("accepts pair label drift: short-circuits even when the page is served by the api-identical peer", async () => { @@ -115,7 +114,7 @@ describe("detectProvider", () => { ); expect(result.currentProviderValidated).toBe(true); expect(result.matches).toEqual([]); - expect(fetchMock.calls.length).toBe(3); + expect(fetchMock.mock.calls.length).toBe(3); }); it("tiebreaks the ambiguous pair to atlassian via html marker", async () => { @@ -242,7 +241,7 @@ describe("detectProvider", () => { "hostname tiebreak: atlassian-statuspage", ); // 3 probes at the old base, the html fetch, 3 probes at the new base - expect(fetchMock.calls.length).toBe(7); + expect(fetchMock.mock.calls.length).toBe(7); }); it("probes the new origin root when the redirect lands on a subpath", async () => { @@ -274,7 +273,7 @@ describe("detectProvider", () => { detectProvider({ statusPageUrl: PAGE_URL, currentProvider: "unknown" }), ); expect(result.movedTo).toBeUndefined(); - expect(fetchMock.calls.length).toBe(4); + expect(fetchMock.mock.calls.length).toBe(4); }); it("suggests instatus from an html marker when nothing validates", async () => { diff --git a/packages/status-fetcher/__tests__/fetch.test.ts b/packages/status-fetcher/__tests__/fetch.test.ts index fef8d9913..e84d79b8c 100644 --- a/packages/status-fetcher/__tests__/fetch.test.ts +++ b/packages/status-fetcher/__tests__/fetch.test.ts @@ -1,7 +1,5 @@ -import { expect } from "@std/expect"; -import { beforeEach, describe, it } from "@std/testing/bdd"; -import { spy } from "@std/testing/mock"; import { Cause, Effect, Exit, Option } from "effect"; +import { beforeEach, describe, expect, it, vi } from "vitest"; import { z } from "zod"; import { @@ -16,7 +14,7 @@ const TEST_URL = "https://api.example.com/status"; type FetchImpl = (url: string, init?: RequestInit) => Promise; const installMockFetch = (impl: FetchImpl) => { - const fn = spy(impl); + const fn = vi.fn(impl); globalThis.fetch = fn as unknown as typeof fetch; return fn; }; @@ -86,7 +84,7 @@ describe("fetchJson", () => { expect(err.kind).toBe("http"); expect(err.fetcherName).toBe("test"); expect(err.entryId).toBe("x"); - expect(fetchMock.calls.length).toBe(1); + expect(fetchMock.mock.calls.length).toBe(1); }); it("retries up to 3 times on 5xx then fails", async () => { @@ -99,7 +97,7 @@ describe("fetchJson", () => { ); const err = expectFailure(exit); expect(err.httpStatus).toBe(503); - expect(fetchMock.calls.length).toBe(4); + expect(fetchMock.mock.calls.length).toBe(4); }); it("retries up to 3 times on network error then fails", async () => { @@ -113,7 +111,7 @@ describe("fetchJson", () => { const err = expectFailure(exit); expect(err.httpStatus).toBeUndefined(); expect(err.kind).toBe("network"); - expect(fetchMock.calls.length).toBe(4); + expect(fetchMock.mock.calls.length).toBe(4); }); it("fails with FetchError on schema mismatch without retrying", async () => { @@ -128,7 +126,7 @@ describe("fetchJson", () => { expect(err.cause).toBeInstanceOf(Error); expect(err.kind).toBe("schema"); expect(err.message).toBe(`[FetchError] schema mismatch: ${TEST_URL}`); - expect(fetchMock.calls.length).toBe(1); + expect(fetchMock.mock.calls.length).toBe(1); }); it("classifies a non-JSON 200 body as parse", async () => { @@ -236,7 +234,7 @@ describe("fetchText", () => { const exit = await Effect.runPromiseExit(fetchText({ url: TEST_URL })); const err = expectFailure(exit); expect(err.httpStatus).toBe(500); - expect(fetchMock.calls.length).toBe(4); + expect(fetchMock.mock.calls.length).toBe(4); }); }); diff --git a/packages/status-fetcher/__tests__/fetchers/atlassian.test.ts b/packages/status-fetcher/__tests__/fetchers/atlassian.test.ts index e7adf54ed..e3e0251d3 100644 --- a/packages/status-fetcher/__tests__/fetchers/atlassian.test.ts +++ b/packages/status-fetcher/__tests__/fetchers/atlassian.test.ts @@ -1,5 +1,4 @@ -import { expect } from "@std/expect"; -import { beforeEach, describe, it } from "@std/testing/bdd"; +import { beforeEach, describe, expect, it } from "vitest"; import { AtlassianFetcher } from "../../src/fetchers/atlassian"; import type { StatusPageEntry } from "../../src/types"; @@ -114,11 +113,11 @@ describe("AtlassianFetcher", () => { expect(result.description).toBe("All Systems Operational"); expect(result.timezone).toBe("Etc/UTC"); expect(typeof result.updated_at).toBe("number"); - const call = fetchMock.calls[fetchMock.calls.length - 1]; - expect(call.args[0]).toBe( + const call = fetchMock.mock.lastCall; + expect(call?.[0]).toBe( "https://www.githubstatus.com/api/v2/summary.json", ); - expect(call.args[1]).toEqual( + expect(call?.[1]).toEqual( expect.objectContaining({ headers: expect.objectContaining({ "User-Agent": "OpenStatus-Directory/1.0", @@ -238,9 +237,9 @@ describe("AtlassianFetcher", () => { await runFetcher(fetcher, entry); - const call = fetchMock.calls[fetchMock.calls.length - 1]; - expect(call.args[0]).toBe("https://custom.endpoint.com/status.json"); - expect(call.args[1]).toEqual(expect.any(Object)); + const call = fetchMock.mock.lastCall; + expect(call?.[0]).toBe("https://custom.endpoint.com/status.json"); + expect(call?.[1]).toEqual(expect.any(Object)); }); it("should fail with FetchError on non-200 response", async () => { @@ -335,11 +334,11 @@ describe("AtlassianFetcher", () => { const incidents = await runIncidents(fetcher, entry); - const call = fetchMock.calls[fetchMock.calls.length - 1]; - expect(call.args[0]).toBe( + const call = fetchMock.mock.lastCall; + expect(call?.[0]).toBe( "https://www.githubstatus.com/api/v2/incidents.json", ); - expect(call.args[1]).toEqual(expect.any(Object)); + expect(call?.[1]).toEqual(expect.any(Object)); expect(incidents).toHaveLength(2); expect(incidents[0]).toMatchObject({ providerIncidentId: "abc", diff --git a/packages/status-fetcher/__tests__/fetchers/betterstack.test.ts b/packages/status-fetcher/__tests__/fetchers/betterstack.test.ts index d1bb0869a..693d5fa43 100644 --- a/packages/status-fetcher/__tests__/fetchers/betterstack.test.ts +++ b/packages/status-fetcher/__tests__/fetchers/betterstack.test.ts @@ -1,5 +1,4 @@ -import { expect } from "@std/expect"; -import { beforeEach, describe, it } from "@std/testing/bdd"; +import { beforeEach, describe, expect, it } from "vitest"; import { BetterStackFetcher } from "../../src/fetchers/betterstack"; import type { StatusPageEntry } from "../../src/types"; @@ -122,9 +121,9 @@ describe("BetterStackFetcher", () => { expect(result.description).toBe("All Systems Operational"); expect(result.timezone).toBe("America/New_York"); expect(typeof result.updated_at).toBe("number"); - const call = fetchMock.calls[fetchMock.calls.length - 1]; - expect(call.args[0]).toBe("https://status.test.com/index.json"); - expect(call.args[1]).toEqual( + const call = fetchMock.mock.lastCall; + expect(call?.[0]).toBe("https://status.test.com/index.json"); + expect(call?.[1]).toEqual( expect.objectContaining({ headers: expect.objectContaining({ "User-Agent": "OpenStatus-Directory/1.0", @@ -242,9 +241,9 @@ describe("BetterStackFetcher", () => { await runFetcher(fetcher, entry); - const call = fetchMock.calls[fetchMock.calls.length - 1]; - expect(call.args[0]).toBe("https://custom.endpoint.com/status.json"); - expect(call.args[1]).toEqual(expect.any(Object)); + const call = fetchMock.mock.lastCall; + expect(call?.[0]).toBe("https://custom.endpoint.com/status.json"); + expect(call?.[1]).toEqual(expect.any(Object)); }); it("should fail with FetchError on 4xx response", async () => { diff --git a/packages/status-fetcher/__tests__/fetchers/components.test.ts b/packages/status-fetcher/__tests__/fetchers/components.test.ts index d87f3ae31..45c355bef 100644 --- a/packages/status-fetcher/__tests__/fetchers/components.test.ts +++ b/packages/status-fetcher/__tests__/fetchers/components.test.ts @@ -1,5 +1,4 @@ -import { expect } from "@std/expect"; -import { beforeEach, describe, it } from "@std/testing/bdd"; +import { beforeEach, describe, expect, it } from "vitest"; import { AtlassianFetcher } from "../../src/fetchers/atlassian"; import { IncidentioFetcher } from "../../src/fetchers/incidentio"; @@ -82,11 +81,9 @@ function itNormalizes( const components = await runComponents(fetcher, entry); - const call = fetchMock.calls[fetchMock.calls.length - 1]; - expect(call.args[0]).toBe( - `${entry.status_page_url}/api/v2/components.json`, - ); - expect(call.args[1]).toEqual(expect.any(Object)); + const call = fetchMock.mock.lastCall; + expect(call?.[0]).toBe(`${entry.status_page_url}/api/v2/components.json`); + expect(call?.[1]).toEqual(expect.any(Object)); // group container "grp" is excluded; only the three leaves remain expect(components).toHaveLength(3); diff --git a/packages/status-fetcher/__tests__/fetchers/custom.test.ts b/packages/status-fetcher/__tests__/fetchers/custom.test.ts index a30914704..4b47fd333 100644 --- a/packages/status-fetcher/__tests__/fetchers/custom.test.ts +++ b/packages/status-fetcher/__tests__/fetchers/custom.test.ts @@ -1,5 +1,4 @@ -import { expect } from "@std/expect"; -import { beforeEach, describe, it } from "@std/testing/bdd"; +import { beforeEach, describe, expect, it } from "vitest"; import { CustomApiFetcher } from "../../src/fetchers/custom"; import type { StatusPageEntry } from "../../src/types"; diff --git a/packages/status-fetcher/__tests__/fetchers/edge-cases.test.ts b/packages/status-fetcher/__tests__/fetchers/edge-cases.test.ts index 01226d4f5..c81c92172 100644 --- a/packages/status-fetcher/__tests__/fetchers/edge-cases.test.ts +++ b/packages/status-fetcher/__tests__/fetchers/edge-cases.test.ts @@ -1,6 +1,5 @@ -import { expect } from "@std/expect"; -import { describe, it } from "@std/testing/bdd"; import { Exit } from "effect"; +import { describe, expect, it } from "vitest"; import { AtlassianFetcher } from "../../src/fetchers/atlassian"; import { BetterStackFetcher } from "../../src/fetchers/betterstack"; diff --git a/packages/status-fetcher/__tests__/fetchers/html.test.ts b/packages/status-fetcher/__tests__/fetchers/html.test.ts index ddbac6a13..73153fab4 100644 --- a/packages/status-fetcher/__tests__/fetchers/html.test.ts +++ b/packages/status-fetcher/__tests__/fetchers/html.test.ts @@ -1,5 +1,4 @@ -import { expect } from "@std/expect"; -import { beforeEach, describe, it } from "@std/testing/bdd"; +import { beforeEach, describe, expect, it } from "vitest"; import { HtmlScraperFetcher } from "../../src/fetchers/html"; import type { StatusPageEntry } from "../../src/types"; @@ -93,9 +92,9 @@ describe("HtmlScraperFetcher", () => { expect(result.description).toBe("All Systems Operational"); expect(result.timezone).toBe("UTC"); expect(typeof result.updated_at).toBe("number"); - const call = fetchMock.calls[fetchMock.calls.length - 1]; - expect(call.args[0]).toBe("https://status.test.com"); - expect(call.args[1]).toEqual( + const call = fetchMock.mock.lastCall; + expect(call?.[0]).toBe("https://status.test.com"); + expect(call?.[1]).toEqual( expect.objectContaining({ headers: expect.objectContaining({ "User-Agent": "Mozilla/5.0 (compatible; OpenStatus-Bot/1.0)", diff --git a/packages/status-fetcher/__tests__/fetchers/incidentio.test.ts b/packages/status-fetcher/__tests__/fetchers/incidentio.test.ts index 6a759fdd1..2bf33fb17 100644 --- a/packages/status-fetcher/__tests__/fetchers/incidentio.test.ts +++ b/packages/status-fetcher/__tests__/fetchers/incidentio.test.ts @@ -1,5 +1,4 @@ -import { expect } from "@std/expect"; -import { beforeEach, describe, it } from "@std/testing/bdd"; +import { beforeEach, describe, expect, it } from "vitest"; import { IncidentioFetcher } from "../../src/fetchers/incidentio"; import type { StatusPageEntry } from "../../src/types"; @@ -112,9 +111,9 @@ describe("IncidentioFetcher", () => { expect(result.status).toBe("operational"); expect(result.description).toBe("All Systems Operational"); expect(typeof result.updated_at).toBe("number"); - const call = fetchMock.calls[fetchMock.calls.length - 1]; - expect(call.args[0]).toBe("https://status.test.com/api/v2/summary.json"); - expect(call.args[1]).toEqual( + const call = fetchMock.mock.lastCall; + expect(call?.[0]).toBe("https://status.test.com/api/v2/summary.json"); + expect(call?.[1]).toEqual( expect.objectContaining({ headers: expect.objectContaining({ "User-Agent": "OpenStatus-Directory/1.0", @@ -236,9 +235,9 @@ describe("IncidentioFetcher", () => { await runFetcher(fetcher, entry); - const call = fetchMock.calls[fetchMock.calls.length - 1]; - expect(call.args[0]).toBe("https://custom.endpoint.com/summary.json"); - expect(call.args[1]).toEqual(expect.any(Object)); + const call = fetchMock.mock.lastCall; + expect(call?.[0]).toBe("https://custom.endpoint.com/summary.json"); + expect(call?.[1]).toEqual(expect.any(Object)); }); it("should fail with FetchError on 5xx response", async () => { @@ -324,11 +323,9 @@ describe("IncidentioFetcher", () => { const incidents = await runIncidents(fetcher, entry); - const call = fetchMock.calls[fetchMock.calls.length - 1]; - expect(call.args[0]).toBe( - "https://status.linear.app/api/v2/incidents.json", - ); - expect(call.args[1]).toEqual(expect.any(Object)); + const call = fetchMock.mock.lastCall; + expect(call?.[0]).toBe("https://status.linear.app/api/v2/incidents.json"); + expect(call?.[1]).toEqual(expect.any(Object)); expect(incidents).toHaveLength(1); expect(incidents[0]).toMatchObject({ providerIncidentId: "INC-1", diff --git a/packages/status-fetcher/__tests__/fetchers/instatus.test.ts b/packages/status-fetcher/__tests__/fetchers/instatus.test.ts index fefc068bf..8d19e6f25 100644 --- a/packages/status-fetcher/__tests__/fetchers/instatus.test.ts +++ b/packages/status-fetcher/__tests__/fetchers/instatus.test.ts @@ -1,5 +1,4 @@ -import { expect } from "@std/expect"; -import { beforeEach, describe, it } from "@std/testing/bdd"; +import { beforeEach, describe, expect, it } from "vitest"; import { InstatusFetcher } from "../../src/fetchers/instatus"; import type { StatusPageEntry } from "../../src/types"; @@ -106,9 +105,9 @@ describe("InstatusFetcher", () => { expect(result.description).toBe("All Systems Operational"); expect(result.timezone).toBe("UTC"); expect(typeof result.updated_at).toBe("number"); - const call = fetchMock.calls[fetchMock.calls.length - 1]; - expect(call.args[0]).toBe("https://test.instatus.com/summary.json"); - expect(call.args[1]).toEqual( + const call = fetchMock.mock.lastCall; + expect(call?.[0]).toBe("https://test.instatus.com/summary.json"); + expect(call?.[1]).toEqual( expect.objectContaining({ headers: expect.objectContaining({ "User-Agent": "OpenStatus-Directory/1.0", @@ -229,9 +228,9 @@ describe("InstatusFetcher", () => { }, }); - const call = fetchMock.calls[fetchMock.calls.length - 1]; - expect(call.args[0]).toBe("https://custom.endpoint.com/status.json"); - expect(call.args[1]).toEqual(expect.any(Object)); + const call = fetchMock.mock.lastCall; + expect(call?.[0]).toBe("https://custom.endpoint.com/status.json"); + expect(call?.[1]).toEqual(expect.any(Object)); }); it("should fail with FetchError on non-200 response (after retries)", async () => { diff --git a/packages/status-fetcher/__tests__/fetchers/uptimerobot.test.ts b/packages/status-fetcher/__tests__/fetchers/uptimerobot.test.ts index 0169bd4d9..8f49ed8e5 100644 --- a/packages/status-fetcher/__tests__/fetchers/uptimerobot.test.ts +++ b/packages/status-fetcher/__tests__/fetchers/uptimerobot.test.ts @@ -1,6 +1,4 @@ -import { expect } from "@std/expect"; -import { afterEach, beforeEach, describe, it } from "@std/testing/bdd"; -import { spy } from "@std/testing/mock"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { UptimeRobotFetcher } from "../../src/fetchers/uptimerobot"; import type { StatusPageEntry } from "../../src/types"; @@ -140,9 +138,9 @@ describe("UptimeRobotFetcher", () => { expect(result.description).toBe("0 monitors down (3 total)"); expect(result.timezone).toBe("UTC"); expect(typeof result.updated_at).toBe("number"); - const lastCall = fetchMock.calls[fetchMock.calls.length - 1]; - expect(lastCall.args[0]).toBe(ENDPOINT); - expect(lastCall.args[1]).toEqual( + const lastCall = fetchMock.mock.lastCall; + expect(lastCall?.[0]).toBe(ENDPOINT); + expect(lastCall?.[1]).toEqual( expect.objectContaining({ headers: expect.objectContaining({ "User-Agent": "OpenStatus-Directory/1.0", @@ -275,7 +273,7 @@ describe("UptimeRobotFetcher", () => { }); it("unknown statusClass folds to operational with a warn", async () => { - const warnSpy = spy(() => {}); + const warnSpy = vi.fn(() => {}); const originalWarn = console.warn; console.warn = warnSpy; @@ -290,7 +288,7 @@ describe("UptimeRobotFetcher", () => { expect(result.severity).toBe("none"); expect(result.status).toBe("operational"); - expect(warnSpy.calls.length).toBeGreaterThan(0); + expect(warnSpy.mock.calls.length).toBeGreaterThan(0); } finally { console.warn = originalWarn; } diff --git a/packages/status-fetcher/__tests__/helpers.ts b/packages/status-fetcher/__tests__/helpers.ts index 05cde49f5..a38850086 100644 --- a/packages/status-fetcher/__tests__/helpers.ts +++ b/packages/status-fetcher/__tests__/helpers.ts @@ -1,5 +1,5 @@ -import { spy } from "@std/testing/mock"; import { Cause, Effect, Exit, Option } from "effect"; +import { vi } from "vitest"; import type { FetchError } from "../src/fetch"; import type { @@ -13,7 +13,7 @@ import type { type FetchImpl = (url: string, init?: RequestInit) => Promise; export const installMockFetch = (impl: FetchImpl) => { - const fn = spy(impl); + const fn = vi.fn(impl); globalThis.fetch = fn as unknown as typeof fetch; return fn; }; diff --git a/packages/status-fetcher/__tests__/integration.test.ts b/packages/status-fetcher/__tests__/integration.test.ts index 546292208..d8accd042 100644 --- a/packages/status-fetcher/__tests__/integration.test.ts +++ b/packages/status-fetcher/__tests__/integration.test.ts @@ -1,6 +1,5 @@ -import { expect } from "@std/expect"; -import { describe, it } from "@std/testing/bdd"; import { Effect } from "effect"; +import { describe, expect, it } from "vitest"; import { fetchers as allFetchers } from "../src/fetchers"; import type { StatusPageEntry, StatusPageProvider } from "../src/types"; diff --git a/packages/status-fetcher/__tests__/utils.test.ts b/packages/status-fetcher/__tests__/utils.test.ts index f2d78f56b..a9341b298 100644 --- a/packages/status-fetcher/__tests__/utils.test.ts +++ b/packages/status-fetcher/__tests__/utils.test.ts @@ -1,5 +1,4 @@ -import { expect } from "@std/expect"; -import { describe, it } from "@std/testing/bdd"; +import { describe, expect, it } from "vitest"; import { inferStatus, urlHostnameEndsWith } from "../src/utils"; diff --git a/packages/status-fetcher/package.json b/packages/status-fetcher/package.json index c291e86fd..dfe89f3c8 100644 --- a/packages/status-fetcher/package.json +++ b/packages/status-fetcher/package.json @@ -12,8 +12,8 @@ }, "scripts": { "check": "tsc --noEmit", - "test": "deno test --parallel -A --no-check --sloppy-imports", - "test:watch": "deno test --parallel -A --no-check --sloppy-imports --watch", + "test": "vitest run", + "test:watch": "vitest", "tsc": "tsc --noEmit" }, "dependencies": { @@ -22,8 +22,8 @@ "zod": "catalog:" }, "devDependencies": { - "@std/expect": "jsr:^1.0.19", - "@std/testing": "jsr:^1.0.19", - "typescript": "catalog:" + "@openstatus/test-utils": "workspace:*", + "typescript": "catalog:", + "vitest": "catalog:" } } diff --git a/packages/status-fetcher/vitest.config.ts b/packages/status-fetcher/vitest.config.ts new file mode 100644 index 000000000..26bdb37da --- /dev/null +++ b/packages/status-fetcher/vitest.config.ts @@ -0,0 +1,3 @@ +import { defineTestConfig } from "@openstatus/test-utils/vitest"; + +export default defineTestConfig(import.meta.dirname); diff --git a/packages/subscriptions/package.json b/packages/subscriptions/package.json index d50647e2f..7d8eea7f8 100644 --- a/packages/subscriptions/package.json +++ b/packages/subscriptions/package.json @@ -16,7 +16,7 @@ }, "scripts": { "check": "tsc --noEmit", - "test": "deno test --parallel -A --no-check --sloppy-imports" + "test": "vitest run" }, "dependencies": { "@openstatus/db": "workspace:*", @@ -29,10 +29,10 @@ "zod": "catalog:" }, "devDependencies": { + "@openstatus/test-utils": "workspace:*", "@openstatus/tsconfig": "workspace:*", - "@std/expect": "jsr:^1.0.19", - "@std/testing": "jsr:^1.0.19", "@types/node": "catalog:", - "typescript": "catalog:" + "typescript": "catalog:", + "vitest": "catalog:" } } diff --git a/packages/subscriptions/src/channels/email.test.ts b/packages/subscriptions/src/channels/email.test.ts index eda3ace15..7c616dcd4 100644 --- a/packages/subscriptions/src/channels/email.test.ts +++ b/packages/subscriptions/src/channels/email.test.ts @@ -1,8 +1,14 @@ import "../test-preload.ts"; import { EmailClient } from "@openstatus/emails"; -import { expect } from "@std/expect"; -import { afterEach, beforeEach, describe, test } from "@std/testing/bdd"; -import { assertSpyCalls, type Stub, stub } from "@std/testing/mock"; +import { + afterEach, + beforeEach, + describe, + expect, + test, + vi, + type MockInstance, +} from "vitest"; import type { PageUpdate, Subscription } from "../types"; import { @@ -14,8 +20,8 @@ import { // RESEND_API_KEY is set in test-preload.ts (see bunfig.toml) so @openstatus/emails // loads successfully and EmailClient prototype methods can be spied on. -let sendPageSubscriptionMock: Stub; -let sendPageUpdateMock: Stub; +let sendPageSubscriptionMock: MockInstance; +let sendPageUpdateMock: MockInstance; function makeSub(overrides: Partial = {}): Subscription { return { @@ -46,19 +52,17 @@ function makeUpdate(overrides: Partial = {}): PageUpdate { } beforeEach(() => { - sendPageSubscriptionMock = stub( - EmailClient.prototype, - "sendPageSubscription", - () => Promise.resolve(undefined), - ); - sendPageUpdateMock = stub(EmailClient.prototype, "sendPageUpdate", () => - Promise.resolve(undefined), - ); + sendPageSubscriptionMock = vi + .spyOn(EmailClient.prototype, "sendPageSubscription") + .mockImplementation(() => Promise.resolve(undefined)); + sendPageUpdateMock = vi + .spyOn(EmailClient.prototype, "sendPageUpdate") + .mockImplementation(() => Promise.resolve(undefined)); }); afterEach(() => { - sendPageSubscriptionMock.restore(); - sendPageUpdateMock.restore(); + sendPageSubscriptionMock.mockRestore(); + sendPageUpdateMock.mockRestore(); }); // ─── validateEmailConfig ────────────────────────────────────────────────────── @@ -100,8 +104,8 @@ describe("sendEmailVerification", () => { const sub = makeSub({ email: "user@example.com", pageName: "My Page" }); await sendEmailVerification(sub, "https://example.com/verify/abc"); - assertSpyCalls(sendPageSubscriptionMock, 1); - const [args] = sendPageSubscriptionMock.calls[0].args; + expect(sendPageSubscriptionMock).toHaveBeenCalledTimes(1); + const [args] = sendPageSubscriptionMock.mock.calls[0]; expect(args.to).toBe("user@example.com"); expect(args.link).toBe("https://example.com/verify/abc"); expect(args.page).toBe("My Page"); @@ -113,13 +117,13 @@ describe("sendEmailVerification", () => { describe("sendEmailNotifications", () => { test("does nothing for an empty subscriptions array", async () => { await sendEmailNotifications([], makeUpdate()); - assertSpyCalls(sendPageUpdateMock, 0); + expect(sendPageUpdateMock).toHaveBeenCalledTimes(0); }); test("filters out subscriptions without an email address", async () => { const sub = makeSub({ email: undefined }); await sendEmailNotifications([sub], makeUpdate()); - assertSpyCalls(sendPageUpdateMock, 0); + expect(sendPageUpdateMock).toHaveBeenCalledTimes(0); }); test("calls sendPageUpdate once with all valid subscribers", async () => { @@ -129,8 +133,8 @@ describe("sendEmailNotifications", () => { await sendEmailNotifications([sub1, sub2], update); - assertSpyCalls(sendPageUpdateMock, 1); - const [args] = sendPageUpdateMock.calls[0].args; + expect(sendPageUpdateMock).toHaveBeenCalledTimes(1); + const [args] = sendPageUpdateMock.mock.calls[0]; expect(args.subscribers).toHaveLength(2); expect(args.subscribers[0].email).toBe("a@example.com"); expect(args.subscribers[1].email).toBe("b@example.com"); @@ -144,7 +148,7 @@ describe("sendEmailNotifications", () => { await sendEmailNotifications([sub], update); - const [args] = sendPageUpdateMock.calls[0].args; + const [args] = sendPageUpdateMock.mock.calls[0]; expect(args.pageComponents).toEqual(["API", "Database"]); }); @@ -152,7 +156,7 @@ describe("sendEmailNotifications", () => { const sub = makeSub(); await sendEmailNotifications([sub], makeUpdate({ updateId: 77 })); - const [args] = sendPageUpdateMock.calls[0].args; + const [args] = sendPageUpdateMock.mock.calls[0]; expect(args.idempotencyKey).toMatch(/^status-report-update:77:[0-9a-z]+$/); }); @@ -163,7 +167,7 @@ describe("sendEmailNotifications", () => { makeUpdate({ id: 17, updateId: undefined, status: "maintenance" }), ); - const [args] = sendPageUpdateMock.calls[0].args; + const [args] = sendPageUpdateMock.mock.calls[0]; expect(args.idempotencyKey).toMatch( /^page-update:17:maintenance:[0-9a-z]+$/, ); @@ -174,7 +178,7 @@ describe("sendEmailNotifications", () => { await sendEmailNotifications([makeSub()], update); await sendEmailNotifications([makeSub()], update); - const keys = sendPageUpdateMock.calls.map((c) => c.args[0].idempotencyKey); + const keys = sendPageUpdateMock.mock.calls.map((c) => c[0].idempotencyKey); expect(keys[0]).toBe(keys[1]); }); @@ -186,7 +190,7 @@ describe("sendEmailNotifications", () => { update, ); - const keys = sendPageUpdateMock.calls.map((c) => c.args[0].idempotencyKey); + const keys = sendPageUpdateMock.mock.calls.map((c) => c[0].idempotencyKey); expect(keys[0]).not.toBe(keys[1]); }); @@ -202,7 +206,7 @@ describe("sendEmailNotifications", () => { }), ); - const [args] = sendPageUpdateMock.calls[0].args; + const [args] = sendPageUpdateMock.mock.calls[0]; expect(args.date).toBe("2026-07-16T11:00:00Z"); expect(args.startsAt).toBe("2026-07-20T10:00:00Z"); expect(args.endsAt).toBe("2026-07-20T11:00:00Z"); @@ -224,7 +228,7 @@ describe("sendEmailNotifications", () => { makeUpdate({ ...base, endsAt: "2026-07-20T12:00:00Z" }), ); - const keys = sendPageUpdateMock.calls.map((c) => c.args[0].idempotencyKey); + const keys = sendPageUpdateMock.mock.calls.map((c) => c[0].idempotencyKey); expect(keys[0]).not.toBe(keys[1]); }); @@ -239,7 +243,7 @@ describe("sendEmailNotifications", () => { makeUpdate({ updateId: 77, date, message: "Root cause identified." }), ); - const keys = sendPageUpdateMock.calls.map((c) => c.args[0].idempotencyKey); + const keys = sendPageUpdateMock.mock.calls.map((c) => c[0].idempotencyKey); expect(keys[0]).not.toBe(keys[1]); }); }); diff --git a/packages/subscriptions/src/channels/index.test.ts b/packages/subscriptions/src/channels/index.test.ts index 7d2263e07..399a21c1d 100644 --- a/packages/subscriptions/src/channels/index.test.ts +++ b/packages/subscriptions/src/channels/index.test.ts @@ -1,6 +1,5 @@ import "../test-preload.ts"; -import { expect } from "@std/expect"; -import { describe, test } from "@std/testing/bdd"; +import { describe, expect, test } from "vitest"; import { getChannel } from "./index"; diff --git a/packages/subscriptions/src/channels/slack-blocks.test.ts b/packages/subscriptions/src/channels/slack-blocks.test.ts index d33ece0ad..0a7569b59 100644 --- a/packages/subscriptions/src/channels/slack-blocks.test.ts +++ b/packages/subscriptions/src/channels/slack-blocks.test.ts @@ -1,5 +1,4 @@ -import { expect } from "@std/expect"; -import { describe, it as test } from "@std/testing/bdd"; +import { describe, expect, test } from "vitest"; import type { PageUpdate, Subscription } from "../types"; import { buildReplyMessage, buildRootMessage } from "./slack-blocks"; diff --git a/packages/subscriptions/src/channels/slack.test.ts b/packages/subscriptions/src/channels/slack.test.ts index 495a7f1f8..a63264b56 100644 --- a/packages/subscriptions/src/channels/slack.test.ts +++ b/packages/subscriptions/src/channels/slack.test.ts @@ -1,5 +1,4 @@ -import { expect } from "@std/expect"; -import { describe, it as test } from "@std/testing/bdd"; +import { describe, expect, test } from "vitest"; import type { PageUpdate, Subscription } from "../types"; import { diff --git a/packages/subscriptions/src/channels/webhook.test.ts b/packages/subscriptions/src/channels/webhook.test.ts index 749f75fe0..e2d606cf0 100644 --- a/packages/subscriptions/src/channels/webhook.test.ts +++ b/packages/subscriptions/src/channels/webhook.test.ts @@ -1,6 +1,12 @@ -import { expect } from "@std/expect"; -import { afterEach, beforeEach, describe, test } from "@std/testing/bdd"; -import { assertSpyCalls, type Stub, stub } from "@std/testing/mock"; +import { + afterEach, + beforeEach, + describe, + expect, + test, + vi, + type MockInstance, +} from "vitest"; import { webhookPayloadSchema } from "../payload"; import type { PageUpdate, Subscription } from "../types"; @@ -21,7 +27,7 @@ const TEAMS_WORKFLOWS_URL = const TEAMS_LEGACY_URL = "https://acme.webhook.office.com/webhookb2/abc@def/IncomingWebhook/xyz/pid"; -let fetchMock: Stub; +let fetchMock: MockInstance; function setFetch( impl: ( @@ -29,8 +35,8 @@ function setFetch( init?: RequestInit, ) => Promise, ) { - fetchMock?.restore(); - fetchMock = stub(globalThis, "fetch", impl); + fetchMock?.mockRestore(); + fetchMock = vi.spyOn(globalThis, "fetch").mockImplementation(impl); } function resolveFetch(response: Response) { @@ -66,13 +72,15 @@ function makeUpdate(overrides: Partial = {}): PageUpdate { } beforeEach(() => { - fetchMock = stub(globalThis, "fetch", () => - Promise.resolve(new Response(null, { status: 200 })), - ); + fetchMock = vi + .spyOn(globalThis, "fetch") + .mockImplementation(() => + Promise.resolve(new Response(null, { status: 200 })), + ); }); afterEach(() => { - fetchMock.restore(); + fetchMock.mockRestore(); }); // ─── validateWebhookConfig ──────────────────────────────────────────────────── @@ -146,8 +154,8 @@ describe("sendWebhookVerification", () => { }); await sendWebhookVerification(sub, "https://example.com/verify/my-token"); - assertSpyCalls(fetchMock, 1); - const [url, init] = fetchMock.calls[0].args; + expect(fetchMock).toHaveBeenCalledTimes(1); + const [url, init] = fetchMock.mock.calls[0]; expect(url).toBe("https://hooks.example.com/sub"); expect(init?.method).toBe("POST"); const body = JSON.parse(init?.body as string); @@ -163,7 +171,7 @@ describe("sendWebhookNotifications", () => { test("skips subscriptions without a webhookUrl", async () => { const sub = makeSub({ webhookUrl: undefined }); await sendWebhookNotifications([sub], makeUpdate()); - assertSpyCalls(fetchMock, 0); + expect(fetchMock).toHaveBeenCalledTimes(0); }); test("sends a POST request to each webhook URL", async () => { @@ -177,8 +185,8 @@ describe("sendWebhookNotifications", () => { await sendWebhookNotifications([sub1, sub2], makeUpdate()); - assertSpyCalls(fetchMock, 2); - const urls = fetchMock.calls.map((c) => c.args[0]); + expect(fetchMock).toHaveBeenCalledTimes(2); + const urls = fetchMock.mock.calls.map((c) => c[0]); expect(urls).toContain("https://hooks.slack.com/services/T1/B1/aaa"); expect(urls).toContain("https://hooks.slack.com/services/T2/B2/bbb"); }); @@ -196,10 +204,10 @@ describe("sendWebhookNotifications", () => { makeUpdate({ updateId: 42 }), ); - assertSpyCalls(fetchMock, 2); + expect(fetchMock).toHaveBeenCalledTimes(2); const byUrl = new Map( - fetchMock.calls.map((c) => { - const [url, init] = c.args as [string, RequestInit]; + fetchMock.mock.calls.map((c) => { + const [url, init] = c as [string, RequestInit]; return [url, JSON.parse(init.body as string)]; }), ); @@ -222,7 +230,7 @@ describe("sendWebhookNotifications", () => { }), ); - const [, init] = fetchMock.calls[0].args; + const [, init] = fetchMock.mock.calls[0]; const sentBody = JSON.parse(init?.body as string); const result = webhookPayloadSchema.safeParse(sentBody); expect(result.success).toBe(true); @@ -242,7 +250,7 @@ describe("sendWebhookNotifications", () => { }), ); - const [, init] = fetchMock.calls[0].args; + const [, init] = fetchMock.mock.calls[0]; const sentBody = JSON.parse(init?.body as string); const result = webhookPayloadSchema.safeParse(sentBody); expect(result.success).toBe(true); @@ -258,7 +266,7 @@ describe("sendWebhookNotifications", () => { await sendWebhookNotifications([sub], makeUpdate()); - const [, init] = fetchMock.calls[0].args; + const [, init] = fetchMock.mock.calls[0]; const headers = init?.headers as Record; expect(headers["X-Custom-Header"]).toBe("my-value"); }); @@ -280,7 +288,7 @@ describe("sendWebhookNotifications", () => { sendWebhookNotifications([sub1, sub2], makeUpdate()), ).resolves.toBeUndefined(); - const calls = fetchMock.calls.map((c) => String(c.args[0])); + const calls = fetchMock.mock.calls.map((c) => String(c[0])); expect(calls).toContain("https://hooks.slack.com/services/T2/B2/succeed"); }); @@ -312,7 +320,7 @@ describe("sendWebhookNotifications", () => { await sendWebhookNotifications([makeSub()], makeUpdate()); - assertSpyCalls(fetchMock, 1); + expect(fetchMock).toHaveBeenCalledTimes(1); }); }); @@ -371,7 +379,7 @@ describe("sendWebhookNotifications (flavor detection)", () => { await sendWebhookNotifications([sub], makeUpdate()); - const [, init] = fetchMock.calls[0].args; + const [, init] = fetchMock.mock.calls[0]; const body = JSON.parse(init?.body as string); expect(body.attachments).toBeDefined(); expect(body.attachments[0].blocks).toBeInstanceOf(Array); @@ -383,7 +391,7 @@ describe("sendWebhookNotifications (flavor detection)", () => { await sendWebhookNotifications([sub], makeUpdate()); - const [, init] = fetchMock.calls[0].args; + const [, init] = fetchMock.mock.calls[0]; const body = JSON.parse(init?.body as string); expect(body.embeds).toBeInstanceOf(Array); expect(body.embeds[0].title).toBe("Test Incident"); @@ -398,7 +406,7 @@ describe("sendWebhookNotifications (flavor detection)", () => { await sendWebhookNotifications([sub], makeUpdate()); - const [, init] = fetchMock.calls[0].args; + const [, init] = fetchMock.mock.calls[0]; const stringified = init?.body as string; expect(stringified).toContain("https://demo.openstatus.dev/manage/tok-123"); expect(stringified).toContain( @@ -415,7 +423,7 @@ describe("sendWebhookNotifications (flavor detection)", () => { await sendWebhookNotifications([sub], makeUpdate()); - const [, init] = fetchMock.calls[0].args; + const [, init] = fetchMock.mock.calls[0]; const stringified = init?.body as string; expect(stringified).toContain("https://status.partner.com/manage/tok-123"); }); @@ -426,7 +434,7 @@ describe("sendWebhookNotifications (flavor detection)", () => { await sendWebhookNotifications([sub], makeUpdate({ id: 99 })); - const [, init] = fetchMock.calls[0].args; + const [, init] = fetchMock.mock.calls[0]; const body = JSON.parse(init?.body as string); const stringified = init?.body as string; // title stays in a plain_text header block @@ -453,7 +461,7 @@ describe("sendWebhookNotifications (flavor detection)", () => { makeUpdate({ id: 7, status: "maintenance" }), ); - const [, init] = fetchMock.calls[0].args; + const [, init] = fetchMock.mock.calls[0]; const body = JSON.parse(init?.body as string); expect(body.embeds[0].url).toBe( "https://demo.openstatus.dev/events/maintenance/7", @@ -473,7 +481,7 @@ describe("sendWebhookNotifications (flavor detection)", () => { await sendWebhookNotifications([sub], makeUpdate({ id: 42 })); - const [, init] = fetchMock.calls[0].args; + const [, init] = fetchMock.mock.calls[0]; const body = JSON.parse(init?.body as string); expect(body.type).toBe("message"); expect(body.attachments[0].contentType).toBe( @@ -510,7 +518,7 @@ describe("sendWebhookNotifications (flavor detection)", () => { await sendWebhookNotifications([sub], makeUpdate()); - const [, init] = fetchMock.calls[0].args; + const [, init] = fetchMock.mock.calls[0]; const stringified = init?.body as string; expect(stringified).toContain( "[Manage](https://demo.openstatus.dev/manage/tok-123)", @@ -532,7 +540,7 @@ describe("sendWebhookNotifications (flavor detection)", () => { makeUpdate({ id: 7, status: "maintenance" }), ); - const [, init] = fetchMock.calls[0].args; + const [, init] = fetchMock.mock.calls[0]; const body = JSON.parse(init?.body as string); const card = body.attachments[0].content; expect(card.body[0].style).toBe("accent"); @@ -737,8 +745,8 @@ describe("sendTestWebhookRequest", () => { flavor: "generic", }); - assertSpyCalls(fetchMock, 1); - const [url, init] = fetchMock.calls[0].args; + expect(fetchMock).toHaveBeenCalledTimes(1); + const [url, init] = fetchMock.mock.calls[0]; expect(url).toBe("https://example.com/hook"); expect(init?.method).toBe("POST"); const body = JSON.parse(init?.body as string); @@ -763,7 +771,7 @@ describe("sendTestWebhookRequest", () => { headers: { "X-Custom": "abc" }, }); - const [, init] = fetchMock.calls[0].args; + const [, init] = fetchMock.mock.calls[0]; const headers = init?.headers as Record; expect(headers["X-Custom"]).toBe("abc"); }); diff --git a/packages/subscriptions/src/dispatcher.test.ts b/packages/subscriptions/src/dispatcher.test.ts index 8bf065463..25a67b602 100644 --- a/packages/subscriptions/src/dispatcher.test.ts +++ b/packages/subscriptions/src/dispatcher.test.ts @@ -17,16 +17,17 @@ import { createTestWorkspace, } from "@openstatus/db/src/test/factories"; import { EmailClient } from "@openstatus/emails"; -import { expect } from "@std/expect"; import { afterAll, afterEach, beforeAll, beforeEach, describe, + expect, test, -} from "@std/testing/bdd"; -import { assertSpyCalls, type Stub, stub } from "@std/testing/mock"; + vi, + type MockInstance, +} from "vitest"; import { dispatchMaintenanceUpdate, @@ -38,7 +39,7 @@ import type { PageUpdate } from "./types"; // RESEND_API_KEY is set in test-preload.ts (see bunfig.toml) so @openstatus/emails // loads successfully and EmailClient prototype methods can be spied on. -let sendPageUpdateMock: Stub; +let sendPageUpdateMock: MockInstance; let rejectNextSend: Error | null = null; // Built in `beforeAll` — a private page keeps the subscriber set this suite @@ -133,18 +134,20 @@ afterAll(cleanAll); beforeEach(() => { rejectNextSend = null; - sendPageUpdateMock = stub(EmailClient.prototype, "sendPageUpdate", () => { - if (rejectNextSend) { - const error = rejectNextSend; - rejectNextSend = null; - return Promise.reject(error); - } - return Promise.resolve(undefined); - }); + sendPageUpdateMock = vi + .spyOn(EmailClient.prototype, "sendPageUpdate") + .mockImplementation(() => { + if (rejectNextSend) { + const error = rejectNextSend; + rejectNextSend = null; + return Promise.reject(error); + } + return Promise.resolve(undefined); + }); }); afterEach(() => { - sendPageUpdateMock.restore(); + sendPageUpdateMock.mockRestore(); }); // ─── dispatchPageUpdate - component filtering ───────────────────────────────── @@ -155,8 +158,8 @@ describe("dispatchPageUpdate - component filtering", () => { makePageUpdate({ pageComponentIds: [COMPONENT_1] }), ); - assertSpyCalls(sendPageUpdateMock, 1); - const { subscribers } = sendPageUpdateMock.calls[0].args[0]; + expect(sendPageUpdateMock).toHaveBeenCalledTimes(1); + const { subscribers } = sendPageUpdateMock.mock.calls[0][0]; const emails = subscribers.map((s: { email: string }) => s.email); expect(emails).toContain(EMAILS.entirePage); @@ -169,7 +172,7 @@ describe("dispatchPageUpdate - component filtering", () => { makePageUpdate({ pageComponentIds: [COMPONENT_2] }), ); - const { subscribers } = sendPageUpdateMock.calls[0].args[0]; + const { subscribers } = sendPageUpdateMock.mock.calls[0][0]; const emails = subscribers.map((s: { email: string }) => s.email); expect(emails).toContain(EMAILS.entirePage); @@ -182,7 +185,7 @@ describe("dispatchPageUpdate - component filtering", () => { makePageUpdate({ pageComponentIds: [COMPONENT_1, COMPONENT_2] }), ); - const { subscribers } = sendPageUpdateMock.calls[0].args[0]; + const { subscribers } = sendPageUpdateMock.mock.calls[0][0]; const emails = subscribers.map((s: { email: string }) => s.email); expect(emails).toContain(EMAILS.entirePage); @@ -193,8 +196,8 @@ describe("dispatchPageUpdate - component filtering", () => { test("notifies only the entire-page subscriber when update has no affected components", async () => { await dispatchPageUpdate(makePageUpdate({ pageComponentIds: [] })); - assertSpyCalls(sendPageUpdateMock, 1); - const { subscribers } = sendPageUpdateMock.calls[0].args[0]; + expect(sendPageUpdateMock).toHaveBeenCalledTimes(1); + const { subscribers } = sendPageUpdateMock.mock.calls[0][0]; const emails = subscribers.map((s: { email: string }) => s.email); // Assert only against this suite's own subscribers — package test suites @@ -212,7 +215,7 @@ describe("dispatchPageUpdate - component filtering", () => { makePageUpdate({ pageComponentIds: [COMPONENT_1] }), ); - const { subscribers } = sendPageUpdateMock.calls[0].args[0]; + const { subscribers } = sendPageUpdateMock.mock.calls[0][0]; const emails = subscribers.map((s: { email: string }) => s.email); expect(emails).not.toContain(EMAILS.component2); @@ -227,7 +230,7 @@ describe("dispatchPageUpdate - edge cases", () => { makePageUpdate({ pageId: 99999, pageComponentIds: [COMPONENT_1] }), ); - assertSpyCalls(sendPageUpdateMock, 0); + expect(sendPageUpdateMock).toHaveBeenCalledTimes(0); }); test("does not propagate channel failure — resolves even when sendPageUpdate throws", async () => { @@ -277,7 +280,7 @@ describe("dispatchMaintenanceUpdate", () => { await dispatchMaintenanceUpdate(update.id); - const args = sendPageUpdateMock.calls[0].args[0]; + const args = sendPageUpdateMock.mock.calls[0][0]; expect(args.message).toBe("specific update message"); expect(args.date).toBe(occurredAt.toISOString()); expect(args.startsAt).toBe(startsAt.toISOString()); @@ -343,8 +346,8 @@ describe("dispatchStatusReportUpdate - impacts", () => { try { await dispatchStatusReportUpdate(earlier.id); - assertSpyCalls(sendPageUpdateMock, 1); - const args = sendPageUpdateMock.calls[0].args[0]; + expect(sendPageUpdateMock).toHaveBeenCalledTimes(1); + const args = sendPageUpdateMock.mock.calls[0][0]; expect( args.componentImpacts.map((c: { impact: string }) => c.impact), ).toEqual(["major_outage"]); diff --git a/packages/subscriptions/src/test-preload.ts b/packages/subscriptions/src/test-preload.ts index b26048d3c..deccbf2a1 100644 --- a/packages/subscriptions/src/test-preload.ts +++ b/packages/subscriptions/src/test-preload.ts @@ -3,8 +3,7 @@ * before module-level env validations (e.g. @openstatus/emails, @openstatus/db) * fire on import. * - * Bun's test runner set NODE_ENV=test automatically; Deno does not, so we set - * it here. The @openstatus/db env hack maps NODE_ENV=test to the local turso + * The @openstatus/db env hack maps NODE_ENV=test to the local turso * URL, and @openstatus/emails validates RESEND_API_KEY at module load. */ process.env.NODE_ENV = "test"; diff --git a/packages/subscriptions/vitest.config.ts b/packages/subscriptions/vitest.config.ts new file mode 100644 index 000000000..26bdb37da --- /dev/null +++ b/packages/subscriptions/vitest.config.ts @@ -0,0 +1,3 @@ +import { defineTestConfig } from "@openstatus/test-utils/vitest"; + +export default defineTestConfig(import.meta.dirname); diff --git a/packages/test-utils/package.json b/packages/test-utils/package.json index ee345766c..8a7eb88be 100644 --- a/packages/test-utils/package.json +++ b/packages/test-utils/package.json @@ -2,16 +2,15 @@ "name": "@openstatus/test-utils", "version": "1.0.0", "private": true, - "main": "src/index.ts", "exports": { - ".": "./src/index.ts" + "./vitest": "./src/vitest.ts" }, "scripts": { - "check": "deno check --sloppy-imports ." + "check": "tsc --noEmit" }, "dependencies": { - "@std/expect": "jsr:^1.0.19", - "@std/testing": "jsr:^1.0.19" + "vite": "catalog:", + "vitest": "catalog:" }, "devDependencies": { "@openstatus/tsconfig": "workspace:*", diff --git a/packages/test-utils/src/index.ts b/packages/test-utils/src/index.ts deleted file mode 100644 index 25c2570b3..000000000 --- a/packages/test-utils/src/index.ts +++ /dev/null @@ -1,27 +0,0 @@ -// Single import surface for migrated tests: `import { ... } from "@openstatus/test-utils"`. -// Importing this module registers the Bun-compat matchers (./matchers side-effect). -import "./matchers.ts"; - -export { - after, - afterAll, - afterEach, - before, - beforeAll, - beforeEach, - describe, - it, - test, -} from "./runner.ts"; -export { expect } from "jsr:@std/expect"; -export { type MockFn, mock } from "./mock-fn.ts"; -export { - assertSpyCall, - assertSpyCalls, - returnsNext, - type Spy, - spy, - type Stub, - stub, -} from "jsr:@std/testing/mock"; -export { FakeTime } from "jsr:@std/testing/time"; diff --git a/packages/test-utils/src/matchers.ts b/packages/test-utils/src/matchers.ts deleted file mode 100644 index 36d58bd32..000000000 --- a/packages/test-utils/src/matchers.ts +++ /dev/null @@ -1,95 +0,0 @@ -import { expect } from "jsr:@std/expect"; - -// @std/expect stopped re-exporting its matcher-context type; these matchers only -// read `value`, so a minimal local shape is enough. -type ExpectMatcherContext = { value: unknown }; - -type MatcherResult = { pass: boolean; message: () => string }; - -const isObject = (v: unknown): v is Record => - typeof v === "object" && v !== null; - -const size = (v: unknown): number => { - if (typeof v === "string" || Array.isArray(v)) return v.length; - if (v instanceof Map || v instanceof Set) return v.size; - if (isObject(v)) return Object.keys(v).length; - return -1; -}; - -// Bun's `expect` extension matchers, reimplemented on @std/expect so the ~150 -// existing call sites keep working under `deno test`. -expect.extend({ - toBeNumber(context: ExpectMatcherContext): MatcherResult { - const pass = - typeof context.value === "number" && !Number.isNaN(context.value); - return { pass, message: () => `expected ${context.value} to be a number` }; - }, - toBeString(context: ExpectMatcherContext): MatcherResult { - return { - pass: typeof context.value === "string", - message: () => `expected ${context.value} to be a string`, - }; - }, - toBeBoolean(context: ExpectMatcherContext): MatcherResult { - return { - pass: typeof context.value === "boolean", - message: () => `expected ${context.value} to be a boolean`, - }; - }, - toBeArray(context: ExpectMatcherContext): MatcherResult { - return { - pass: Array.isArray(context.value), - message: () => `expected ${context.value} to be an array`, - }; - }, - toBeTrue(context: ExpectMatcherContext): MatcherResult { - return { - pass: context.value === true, - message: () => `expected ${context.value} to be true`, - }; - }, - toBeFalse(context: ExpectMatcherContext): MatcherResult { - return { - pass: context.value === false, - message: () => `expected ${context.value} to be false`, - }; - }, - toBeNil(context: ExpectMatcherContext): MatcherResult { - return { - pass: context.value === null || context.value === undefined, - message: () => `expected ${context.value} to be null or undefined`, - }; - }, - toBeEmpty(context: ExpectMatcherContext): MatcherResult { - return { - pass: size(context.value) === 0, - message: () => `expected ${context.value} to be empty`, - }; - }, - toBeOneOf( - context: ExpectMatcherContext, - expected: readonly unknown[], - ): MatcherResult { - return { - pass: expected.includes(context.value), - message: () => - `expected ${context.value} to be one of ${JSON.stringify(expected)}`, - }; - }, - toContainValue(context: ExpectMatcherContext, value: unknown): MatcherResult { - const pass = - isObject(context.value) && Object.values(context.value).includes(value); - return { pass, message: () => `expected object to contain value ${value}` }; - }, - toContainKey(context: ExpectMatcherContext, key: string): MatcherResult { - const pass = - isObject(context.value) && Object.keys(context.value).includes(key); - return { pass, message: () => `expected object to contain key ${key}` }; - }, - pass(_context: ExpectMatcherContext, _message?: string): MatcherResult { - return { pass: true, message: () => _message ?? "passed" }; - }, - fail(_context: ExpectMatcherContext, message?: string): MatcherResult { - return { pass: false, message: () => message ?? "failed" }; - }, -}); diff --git a/packages/test-utils/src/mock-fn.ts b/packages/test-utils/src/mock-fn.ts deleted file mode 100644 index 2d71214fb..000000000 --- a/packages/test-utils/src/mock-fn.ts +++ /dev/null @@ -1,112 +0,0 @@ -// Bun-compatible `mock()` for the Deno migration. Bun's mock function isn't -// available under `deno test`; this reimplements the subset our suites use -// (the jest-style `.mock.calls` + `.mockX()` helpers) on plain closures. -// -// NOTE: bun's `mock.module(specifier, factory)` has no Deno equivalent and is -// intentionally NOT provided here — module replacement is done via the -// `--import-map` flag and test-double modules instead. - -// oxlint-disable-next-line typescript/no-explicit-any -type AnyFn = (...args: any[]) => any; - -interface MockState { - // oxlint-disable-next-line typescript/no-explicit-any - calls: any[][]; - results: { type: "return" | "throw"; value: unknown }[]; -} - -export interface MockFn extends AnyFn { - mock: MockState; - mockClear(): MockFn; - mockReset(): MockFn; - mockImplementation(fn: AnyFn): MockFn; - mockImplementationOnce(fn: AnyFn): MockFn; - mockReturnValue(value: unknown): MockFn; - mockReturnValueOnce(value: unknown): MockFn; - mockResolvedValue(value: unknown): MockFn; - mockResolvedValueOnce(value: unknown): MockFn; - mockRejectedValue(value: unknown): MockFn; - mockRestore(): MockFn; -} - -export function mock(impl?: AnyFn): MockFn { - let defaultImpl = impl; - const onceQueue: AnyFn[] = []; - const state: MockState = { calls: [], results: [] }; - - // oxlint-disable-next-line typescript/no-explicit-any - const fn = ((...args: any[]) => { - state.calls.push(args); - const use = onceQueue.length ? onceQueue.shift() : defaultImpl; - try { - const value = use ? use(...args) : undefined; - state.results.push({ type: "return", value }); - return value; - } catch (err) { - state.results.push({ type: "throw", value: err }); - throw err; - } - }) as MockFn; - - fn.mock = state; - // Expose the @std/expect mock interface (Symbol.for("@MOCK")) so its - // `.toHaveBeenCalled*` matchers recognize this shim, not just `.mock.calls`. - Object.defineProperty(fn, Symbol.for("@MOCK"), { - get() { - return { - calls: state.calls.map((args, i) => { - const r = state.results[i]; - return { - args, - returned: r?.type === "return" ? r.value : undefined, - thrown: r?.type === "throw" ? r.value : undefined, - timestamp: 0, - returns: r?.type === "return", - throws: r?.type === "throw", - }; - }), - }; - }, - }); - fn.mockClear = () => { - state.calls = []; - state.results = []; - return fn; - }; - fn.mockReset = () => { - fn.mockClear(); - onceQueue.length = 0; - defaultImpl = undefined; - return fn; - }; - fn.mockImplementation = (f) => { - defaultImpl = f; - return fn; - }; - fn.mockImplementationOnce = (f) => { - onceQueue.push(f); - return fn; - }; - fn.mockReturnValue = (value) => { - defaultImpl = () => value; - return fn; - }; - fn.mockReturnValueOnce = (value) => { - onceQueue.push(() => value); - return fn; - }; - fn.mockResolvedValue = (value) => { - defaultImpl = () => Promise.resolve(value); - return fn; - }; - fn.mockResolvedValueOnce = (value) => { - onceQueue.push(() => Promise.resolve(value)); - return fn; - }; - fn.mockRejectedValue = (value) => { - defaultImpl = () => Promise.reject(value); - return fn; - }; - fn.mockRestore = () => fn; - return fn; -} diff --git a/packages/test-utils/src/runner.ts b/packages/test-utils/src/runner.ts deleted file mode 100644 index 55dd22f4a..000000000 --- a/packages/test-utils/src/runner.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { - describe as nodeDescribe, - it as nodeIt, - test as nodeTest, -} from "node:test"; - -type Body = () => void | Promise; -type BaseFn = (name: string, body: Body) => void; -type EachCase = readonly unknown[]; - -const formatName = (name: string, args: EachCase, index: number): string => { - let i = 0; - const interpolated = name.replace(/%[spdijfo%]/g, (token) => { - if (token === "%%") return "%"; - const arg = args[i++]; - return typeof arg === "string" ? arg : JSON.stringify(arg); - }); - return interpolated === name && args.length > 0 - ? `${name} [${index}]` - : interpolated; -}; - -const makeEach = - (base: BaseFn) => - (cases: readonly T[]) => - (name: string, body: (...args: T) => void | Promise): void => { - cases.forEach((args, index) => { - base(formatName(name, args, index), () => body(...args)); - }); - }; - -// node:test's `test`/`it`/`describe` lack Jest/Bun's `.each`; everything else -// (.skip/.todo/.only) exists natively and is preserved by reference. -type Augmented = F & { - each: ( - cases: readonly T[], - ) => (name: string, body: (...args: T) => void | Promise) => void; -}; - -const augment = (base: F): Augmented => { - const fn = base as Augmented; - fn.each = makeEach(base as unknown as BaseFn); - return fn; -}; - -export const test = augment(nodeTest); -export const it = augment(nodeIt); -export const describe = augment( - nodeDescribe as unknown as typeof nodeTest, -) as unknown as Augmented; -export { - after, - after as afterAll, - afterEach, - before, - before as beforeAll, - beforeEach, -} from "node:test"; diff --git a/packages/test-utils/src/vitest.ts b/packages/test-utils/src/vitest.ts new file mode 100644 index 000000000..c0a0f1611 --- /dev/null +++ b/packages/test-utils/src/vitest.ts @@ -0,0 +1,91 @@ +import { readFileSync } from "node:fs"; +import { dirname, resolve } from "node:path"; +import { parseEnv } from "node:util"; + +import type { Plugin } from "vite"; +import { defineConfig } from "vitest/config"; + +type Options = { + /** Test-double swaps, in Deno import-map format, relative to the package. */ + importMap?: string; + /** Loaded into `process.env` for every test file, relative to the package. */ + envFile?: string; + include?: string[]; +}; + +/** + * Applies a Deno-style import map: exact bare keys, `/`-suffixed prefix keys, + * and relative keys matched against the importer-resolved path without + * extension probing — so `./page-urls` is swapped but `./page-urls.ts` is not. + */ +export function importMap(file: string): Plugin { + const base = dirname(file); + const { imports } = JSON.parse(readFileSync(file, "utf8")) as { + imports: Record; + }; + const exact = new Map(); + const prefixes: [string, string][] = []; + const paths = new Map(); + for (const [key, value] of Object.entries(imports)) { + const target = resolve(base, value); + if (key.startsWith("./") || key.startsWith("../")) { + paths.set(resolve(base, key), target); + } else if (key.endsWith("/")) { + prefixes.push([key, value.endsWith("/") ? `${target}/` : target]); + } else { + exact.set(key, target); + } + } + + return { + name: "openstatus:import-map", + enforce: "pre", + resolveId(source, importer, options) { + const swapped = + exact.get(source) ?? + (importer && (source.startsWith("./") || source.startsWith("../")) + ? paths.get(resolve(dirname(importer), source)) + : undefined); + if (swapped) { + return this.resolve(swapped, importer, { ...options, skipSelf: true }); + } + for (const [prefix, target] of prefixes) { + if (source.startsWith(prefix)) { + return this.resolve(target + source.slice(prefix.length), importer, { + ...options, + skipSelf: true, + }); + } + } + return null; + }, + }; +} + +export function defineTestConfig(root: string, options: Options = {}) { + // Like `--env-file`: the file never overrides a variable already set (CI + // points DATABASE_URL at its own sqld). + const fileEnv = options.envFile + ? parseEnv(readFileSync(resolve(root, options.envFile), "utf8")) + : {}; + const env = Object.fromEntries( + Object.entries(fileEnv).filter(([key]) => process.env[key] === undefined), + ); + return defineConfig({ + root, + plugins: options.importMap + ? [importMap(resolve(root, options.importMap))] + : [], + resolve: { tsconfigPaths: true }, + oxc: { jsx: { runtime: "automatic", importSource: "react" } }, + test: { + include: options.include ?? ["**/*.test.{ts,tsx}"], + exclude: ["**/node_modules/**", "**/dist/**", "**/.next/**"], + env: env as Record, + // Integration suites talk to a real libSQL server; vitest's 5s default + // is too tight for the larger seeded fixtures. + testTimeout: 30_000, + hookTimeout: 30_000, + }, + }); +} diff --git a/packages/test-utils/tsconfig.json b/packages/test-utils/tsconfig.json new file mode 100644 index 000000000..82f44d0f0 --- /dev/null +++ b/packages/test-utils/tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "@openstatus/tsconfig/base.json", + "include": ["src"] +} diff --git a/packages/theme-store/package.json b/packages/theme-store/package.json index b0c485e03..8e8f715d1 100644 --- a/packages/theme-store/package.json +++ b/packages/theme-store/package.json @@ -8,14 +8,14 @@ "main": "src/index.ts", "scripts": { "check": "tsc --noEmit", - "test": "deno test --parallel -A --no-check --sloppy-imports" + "test": "vitest run" }, "dependencies": {}, "devDependencies": { + "@openstatus/test-utils": "workspace:*", "@openstatus/tsconfig": "workspace:*", - "@std/expect": "jsr:^1.0.19", - "@std/testing": "jsr:^1.0.19", "@types/node": "catalog:", - "typescript": "catalog:" + "typescript": "catalog:", + "vitest": "catalog:" } } diff --git a/packages/theme-store/src/__tests__/custom-theme.test.ts b/packages/theme-store/src/__tests__/custom-theme.test.ts index f6a255cb3..aea331c5c 100644 --- a/packages/theme-store/src/__tests__/custom-theme.test.ts +++ b/packages/theme-store/src/__tests__/custom-theme.test.ts @@ -1,5 +1,4 @@ -import { expect } from "@std/expect"; -import { describe, test } from "@std/testing/bdd"; +import { describe, expect, test } from "vitest"; import { CUSTOM_THEME_VALUE_MAX_LENGTH, diff --git a/packages/theme-store/src/__tests__/theme-io.test.ts b/packages/theme-store/src/__tests__/theme-io.test.ts index cc2033f1b..6d9c183f9 100644 --- a/packages/theme-store/src/__tests__/theme-io.test.ts +++ b/packages/theme-store/src/__tests__/theme-io.test.ts @@ -1,7 +1,6 @@ import { readdirSync, readFileSync } from "node:fs"; -import { expect } from "@std/expect"; -import { describe, test } from "@std/testing/bdd"; +import { describe, expect, test } from "vitest"; import { parseThemeInput, diff --git a/packages/theme-store/vitest.config.ts b/packages/theme-store/vitest.config.ts new file mode 100644 index 000000000..26bdb37da --- /dev/null +++ b/packages/theme-store/vitest.config.ts @@ -0,0 +1,3 @@ +import { defineTestConfig } from "@openstatus/test-utils/vitest"; + +export default defineTestConfig(import.meta.dirname); diff --git a/packages/tinybird/package.json b/packages/tinybird/package.json index 5945d7c6c..34135eaad 100644 --- a/packages/tinybird/package.json +++ b/packages/tinybird/package.json @@ -5,7 +5,7 @@ "main": "src/index.ts", "scripts": { "check": "tsc --noEmit", - "test": "NODE_ENV=test deno test --parallel -A --no-check --sloppy-imports" + "test": "NODE_ENV=test vitest run" }, "dependencies": { "@chronark/zod-bird": "catalog:", @@ -14,12 +14,12 @@ "devDependencies": { "@libsql/client": "catalog:", "@openstatus/db": "workspace:*", + "@openstatus/test-utils": "workspace:*", "@openstatus/tsconfig": "workspace:*", "@openstatus/utils": "workspace:*", - "@std/expect": "jsr:^1.0.19", - "@std/testing": "jsr:^1.0.19", "@types/node": "catalog:", "drizzle-orm": "catalog:", - "typescript": "catalog:" + "typescript": "catalog:", + "vitest": "catalog:" } } diff --git a/packages/tinybird/src/client.test.ts b/packages/tinybird/src/client.test.ts index b8e06cc9b..ac5d53cfe 100644 --- a/packages/tinybird/src/client.test.ts +++ b/packages/tinybird/src/client.test.ts @@ -1,5 +1,4 @@ -import { expect } from "@std/expect"; -import { describe, test } from "@std/testing/bdd"; +import { describe, expect, test } from "vitest"; import { z } from "zod"; import { diff --git a/packages/tinybird/src/schema.test.ts b/packages/tinybird/src/schema.test.ts index de42ed20f..965eebf60 100644 --- a/packages/tinybird/src/schema.test.ts +++ b/packages/tinybird/src/schema.test.ts @@ -1,5 +1,4 @@ -import { expect } from "@std/expect"; -import { describe, test } from "@std/testing/bdd"; +import { describe, expect, test } from "vitest"; import { calculateTiming } from "./schema"; diff --git a/packages/tinybird/vitest.config.ts b/packages/tinybird/vitest.config.ts new file mode 100644 index 000000000..26bdb37da --- /dev/null +++ b/packages/tinybird/vitest.config.ts @@ -0,0 +1,3 @@ +import { defineTestConfig } from "@openstatus/test-utils/vitest"; + +export default defineTestConfig(import.meta.dirname); diff --git a/packages/tracker/package.json b/packages/tracker/package.json index 56ca0b044..003592341 100644 --- a/packages/tracker/package.json +++ b/packages/tracker/package.json @@ -8,7 +8,7 @@ "main": "src/index.ts", "scripts": { "check": "tsc --noEmit", - "test": "deno test --parallel -A --no-check --sloppy-imports" + "test": "vitest run" }, "dependencies": { "@openstatus/db": "workspace:*", @@ -16,9 +16,9 @@ "zod": "catalog:" }, "devDependencies": { + "@openstatus/test-utils": "workspace:*", "@openstatus/tsconfig": "workspace:*", - "@std/expect": "jsr:^1.0.19", - "@std/testing": "jsr:^1.0.19", - "typescript": "catalog:" + "typescript": "catalog:", + "vitest": "catalog:" } } diff --git a/packages/tracker/src/tracker.test.ts b/packages/tracker/src/tracker.test.ts index d2ace251a..0a5897fb7 100644 --- a/packages/tracker/src/tracker.test.ts +++ b/packages/tracker/src/tracker.test.ts @@ -4,8 +4,7 @@ import type { StatusReport, StatusReportUpdate, } from "@openstatus/db/src/schema"; -import { expect } from "@std/expect"; -import { describe, test } from "@std/testing/bdd"; +import { describe, expect, test } from "vitest"; import { blacklistDates } from "./blacklist"; import { classNames, statusDetails } from "./config"; diff --git a/packages/tracker/src/utils.test.ts b/packages/tracker/src/utils.test.ts index 6615cc76f..0c70142fa 100644 --- a/packages/tracker/src/utils.test.ts +++ b/packages/tracker/src/utils.test.ts @@ -1,5 +1,4 @@ -import { expect } from "@std/expect"; -import { describe, test } from "@std/testing/bdd"; +import { describe, expect, test } from "vitest"; import { isInBlacklist } from "./blacklist"; import { endOfDay, isSameDay, startOfDay } from "./utils"; diff --git a/packages/tracker/vitest.config.ts b/packages/tracker/vitest.config.ts new file mode 100644 index 000000000..26bdb37da --- /dev/null +++ b/packages/tracker/vitest.config.ts @@ -0,0 +1,3 @@ +import { defineTestConfig } from "@openstatus/test-utils/vitest"; + +export default defineTestConfig(import.meta.dirname); diff --git a/packages/upstash/package.json b/packages/upstash/package.json index 1622f7587..36e67ead6 100644 --- a/packages/upstash/package.json +++ b/packages/upstash/package.json @@ -5,18 +5,18 @@ "main": "./src/index.ts", "scripts": { "check": "tsc --noEmit", - "test": "deno test --parallel -A --no-check --sloppy-imports" + "test": "vitest run" }, "dependencies": { "@upstash/qstash": "catalog:", "@upstash/redis": "catalog:" }, "devDependencies": { + "@openstatus/test-utils": "workspace:*", "@openstatus/tsconfig": "workspace:*", - "@std/expect": "jsr:^1.0.19", - "@std/testing": "jsr:^1.0.19", "@types/node": "catalog:", "tsup": "catalog:", - "typescript": "catalog:" + "typescript": "catalog:", + "vitest": "catalog:" } } diff --git a/packages/upstash/src/redis/incr-with-ttl.test.ts b/packages/upstash/src/redis/incr-with-ttl.test.ts index 17befdcd5..a262e64c6 100644 --- a/packages/upstash/src/redis/incr-with-ttl.test.ts +++ b/packages/upstash/src/redis/incr-with-ttl.test.ts @@ -1,5 +1,4 @@ -import { expect } from "@std/expect"; -import { describe, test } from "@std/testing/bdd"; +import { describe, expect, test } from "vitest"; import { incrWithTtl } from "./incr-with-ttl"; diff --git a/packages/upstash/vitest.config.ts b/packages/upstash/vitest.config.ts new file mode 100644 index 000000000..26bdb37da --- /dev/null +++ b/packages/upstash/vitest.config.ts @@ -0,0 +1,3 @@ +import { defineTestConfig } from "@openstatus/test-utils/vitest"; + +export default defineTestConfig(import.meta.dirname); diff --git a/packages/utils/package.json b/packages/utils/package.json index c5bc3bf82..aecda2c8a 100644 --- a/packages/utils/package.json +++ b/packages/utils/package.json @@ -8,7 +8,7 @@ "main": "src/index.ts", "scripts": { "check": "tsc --noEmit", - "test": "deno test --parallel -A --no-check --sloppy-imports" + "test": "vitest run" }, "dependencies": { "@openstatus/assertions": "workspace:*", @@ -16,9 +16,9 @@ "zod": "catalog:" }, "devDependencies": { + "@openstatus/test-utils": "workspace:*", "@openstatus/tsconfig": "workspace:*", - "@std/expect": "jsr:^1.0.19", - "@std/testing": "jsr:^1.0.19", - "typescript": "catalog:" + "typescript": "catalog:", + "vitest": "catalog:" } } diff --git a/packages/utils/src/curl.test.ts b/packages/utils/src/curl.test.ts index c64c4fc88..bde04a48d 100644 --- a/packages/utils/src/curl.test.ts +++ b/packages/utils/src/curl.test.ts @@ -1,8 +1,42 @@ -import { expect } from "@std/expect"; -import { describe, it } from "@std/testing/bdd"; +import { spawn } from "node:child_process"; +import { createServer } from "node:http"; +import type { AddressInfo } from "node:net"; + +import { describe, expect, it } from "vitest"; import { buildCurlCommand } from "./curl"; +async function serve(onBody: (body: Buffer) => Buffer | string) { + const server = createServer((req, res) => { + const chunks: Buffer[] = []; + req.on("data", (chunk: Buffer) => chunks.push(chunk)); + req.on("end", () => res.end(onBody(Buffer.concat(chunks)))); + }); + await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve)); + return { + port: (server.address() as AddressInfo).port, + close: () => new Promise((resolve) => server.close(() => resolve())), + }; +} + +// Async on purpose: the server above runs in this process, so a sync spawn +// would block the request it is waiting on. +function sh(command: string) { + return new Promise<{ code: number | null; stdout: Uint8Array }>( + (resolve, reject) => { + const child = spawn("sh", ["-c", command], { + env: { ...process.env, NO_PROXY: "*" }, + }); + const chunks: Buffer[] = []; + child.stdout.on("data", (chunk: Buffer) => chunks.push(chunk)); + child.on("error", reject); + child.on("close", (code) => + resolve({ code, stdout: new Uint8Array(Buffer.concat(chunks)) }), + ); + }, + ); +} + describe("buildCurlCommand", () => { it("renders a plain GET without an explicit method", () => { expect(buildCurlCommand({ url: "https://example.com" })).toBe( @@ -31,10 +65,7 @@ describe("buildCurlCommand", () => { }); it("sends decoded binary POST bytes without shell or text conversion", async () => { - const server = Deno.serve( - { hostname: "127.0.0.1", port: 0 }, - async (request) => new Response(await request.arrayBuffer()), - ); + const server = await serve((body) => body); try { for (const { body, bytes } of [ { @@ -48,31 +79,26 @@ describe("buildCurlCommand", () => { }, ]) { const command = buildCurlCommand({ - url: `http://127.0.0.1:${server.addr.port}/upload`, + url: `http://127.0.0.1:${server.port}/upload`, method: "POST", body, headers: [{ key: "Content-Type", value: "application/octet-stream" }], timeout: 5000, }); - const result = await new Deno.Command("sh", { - args: ["-c", command], - env: { NO_PROXY: "*" }, - stdout: "piped", - stderr: "piped", - }).output(); + const result = await sh(command); expect(result.code).toBe(0); expect(result.stdout).toEqual(new Uint8Array(bytes)); } } finally { - await server.shutdown(); + await server.close(); } }); it("rejects malformed binary bodies before sending a request", async () => { let requests = 0; - const server = Deno.serve({ hostname: "127.0.0.1", port: 0 }, () => { + const server = await serve(() => { requests++; - return new Response("received"); + return "received"; }); try { for (const body of [ @@ -84,23 +110,18 @@ describe("buildCurlCommand", () => { "data:application/octet-stream;base64,aGVsbG8=\u2028", ]) { const command = buildCurlCommand({ - url: `http://127.0.0.1:${server.addr.port}/upload`, + url: `http://127.0.0.1:${server.port}/upload`, method: "POST", body, headers: [{ key: "Content-Type", value: "application/octet-stream" }], timeout: 5000, }); - const result = await new Deno.Command("sh", { - args: ["-c", command], - env: { NO_PROXY: "*" }, - stdout: "piped", - stderr: "piped", - }).output(); + const result = await sh(command); expect(requests).toBe(0); expect(result.code).not.toBe(0); } } finally { - await server.shutdown(); + await server.close(); } }); diff --git a/packages/utils/src/headers.test.ts b/packages/utils/src/headers.test.ts index 61c746acd..f41247147 100644 --- a/packages/utils/src/headers.test.ts +++ b/packages/utils/src/headers.test.ts @@ -1,5 +1,4 @@ -import { expect } from "@std/expect"; -import { describe, test } from "@std/testing/bdd"; +import { describe, expect, test } from "vitest"; import { headerPairSchema } from "./headers"; diff --git a/packages/utils/src/payloads.test.ts b/packages/utils/src/payloads.test.ts index 13a5fb124..3310d1df7 100644 --- a/packages/utils/src/payloads.test.ts +++ b/packages/utils/src/payloads.test.ts @@ -1,5 +1,4 @@ -import { expect } from "@std/expect"; -import { describe, it } from "@std/testing/bdd"; +import { describe, expect, it } from "vitest"; import { DNSPayloadSchema, diff --git a/packages/utils/src/person-name.test.ts b/packages/utils/src/person-name.test.ts index fed7a4402..bfa1b2117 100644 --- a/packages/utils/src/person-name.test.ts +++ b/packages/utils/src/person-name.test.ts @@ -1,5 +1,4 @@ -import { expect } from "@std/expect"; -import { describe, it } from "@std/testing/bdd"; +import { describe, expect, it } from "vitest"; import { personName } from "./person-name"; diff --git a/packages/utils/src/ssrf.test.ts b/packages/utils/src/ssrf.test.ts index 587e3de8e..8d5a3083c 100644 --- a/packages/utils/src/ssrf.test.ts +++ b/packages/utils/src/ssrf.test.ts @@ -1,5 +1,4 @@ -import { expect } from "@std/expect"; -import { describe, it } from "@std/testing/bdd"; +import { describe, expect, it } from "vitest"; import { assertSafeUrl, diff --git a/packages/utils/src/status.test.ts b/packages/utils/src/status.test.ts index 7e1eae0b9..8949ae5bb 100644 --- a/packages/utils/src/status.test.ts +++ b/packages/utils/src/status.test.ts @@ -1,5 +1,4 @@ -import { expect } from "@std/expect"; -import { describe, it } from "@std/testing/bdd"; +import { describe, expect, it } from "vitest"; import { type PageUpdateStatus, statusLabel } from "./status"; diff --git a/packages/utils/src/stream.test.ts b/packages/utils/src/stream.test.ts index 687964064..e46cdac99 100644 --- a/packages/utils/src/stream.test.ts +++ b/packages/utils/src/stream.test.ts @@ -1,5 +1,4 @@ -import { expect } from "@std/expect"; -import { describe, it } from "@std/testing/bdd"; +import { describe, expect, it } from "vitest"; import { iteratorToStream, yieldMany } from "./stream"; diff --git a/packages/utils/vitest.config.ts b/packages/utils/vitest.config.ts new file mode 100644 index 000000000..26bdb37da --- /dev/null +++ b/packages/utils/vitest.config.ts @@ -0,0 +1,3 @@ +import { defineTestConfig } from "@openstatus/test-utils/vitest"; + +export default defineTestConfig(import.meta.dirname); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a179dda09..9c89c5f5d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -659,6 +659,12 @@ catalogs: vaul: specifier: 1.1.2 version: 1.1.2 + vite: + specifier: 8.3.4 + version: 8.3.4 + vitest: + specifier: 5.0.3 + version: 5.0.3 zod: specifier: 4.6.5 version: 4.6.5 @@ -974,12 +980,9 @@ importers: specifier: 'catalog:' version: 4.6.5 devDependencies: - '@std/expect': - specifier: jsr:^1.0.19 - version: '@jsr/std__expect@1.0.20' - '@std/testing': - specifier: jsr:^1.0.19 - version: '@jsr/std__testing@1.0.20' + '@openstatus/test-utils': + specifier: workspace:* + version: link:../../packages/test-utils '@tailwindcss/postcss': specifier: 'catalog:' version: 4.3.3 @@ -1010,6 +1013,9 @@ importers: typescript: specifier: 'catalog:' version: 7.0.2 + vitest: + specifier: 'catalog:' + version: 5.0.3(@opentelemetry/api@1.9.1)(@types/node@26.6.2)(jsdom@30.1.1)(msw@2.15.0(@types/node@26.6.2)(typescript@7.0.2))(vite@8.3.4(@types/node@26.6.2)(tsx@4.23.15)) apps/screenshot-service: dependencies: @@ -1255,12 +1261,6 @@ importers: '@oxc-node/core': specifier: 'catalog:' version: 0.1.5 - '@std/expect': - specifier: jsr:^1.0.19 - version: '@jsr/std__expect@1.0.20' - '@std/testing': - specifier: jsr:^1.0.19 - version: '@jsr/std__testing@1.0.20' '@std/yaml': specifier: jsr:1.3.0 version: '@jsr/std__yaml@1.3.0' @@ -1279,6 +1279,9 @@ importers: typescript: specifier: 'catalog:' version: 7.0.2 + vitest: + specifier: 'catalog:' + version: 5.0.3(@opentelemetry/api@1.9.1)(@types/node@26.6.2)(jsdom@30.1.1)(msw@2.15.0(@types/node@26.6.2)(typescript@7.0.2))(vite@8.3.4(@types/node@26.6.2)(tsx@4.23.15)) apps/status-page: dependencies: @@ -1487,15 +1490,12 @@ importers: specifier: 'catalog:' version: 4.6.5 devDependencies: + '@openstatus/test-utils': + specifier: workspace:* + version: link:../../packages/test-utils '@openstatus/tsconfig': specifier: workspace:* version: link:../../packages/tsconfig - '@std/expect': - specifier: jsr:^1.0.19 - version: '@jsr/std__expect@1.0.20' - '@std/testing': - specifier: jsr:^1.0.19 - version: '@jsr/std__testing@1.0.20' '@tailwindcss/postcss': specifier: 'catalog:' version: 4.3.3 @@ -1526,6 +1526,9 @@ importers: typescript: specifier: 'catalog:' version: 7.0.2 + vitest: + specifier: 'catalog:' + version: 5.0.3(@opentelemetry/api@1.9.1)(@types/node@26.6.2)(jsdom@30.1.1)(msw@2.15.0(@types/node@26.6.2)(typescript@7.0.2))(vite@8.3.4(@types/node@26.6.2)(tsx@4.23.15)) apps/web: dependencies: @@ -1773,15 +1776,12 @@ importers: specifier: 'catalog:' version: 4.6.5 devDependencies: + '@openstatus/test-utils': + specifier: workspace:* + version: link:../../packages/test-utils '@openstatus/tsconfig': specifier: workspace:* version: link:../../packages/tsconfig - '@std/expect': - specifier: jsr:^1.0.19 - version: '@jsr/std__expect@1.0.20' - '@std/testing': - specifier: jsr:^1.0.19 - version: '@jsr/std__testing@1.0.20' '@tailwindcss/postcss': specifier: 'catalog:' version: 4.3.3 @@ -1815,6 +1815,9 @@ importers: unified: specifier: 'catalog:' version: 11.0.5 + vitest: + specifier: 'catalog:' + version: 5.0.3(@opentelemetry/api@1.9.1)(@types/node@26.6.2)(jsdom@30.1.1)(msw@2.15.0(@types/node@26.6.2)(typescript@7.0.2))(vite@8.3.4(@types/node@26.6.2)(tsx@4.23.15)) apps/workflows: dependencies: @@ -1972,6 +1975,9 @@ importers: typescript: specifier: 'catalog:' version: 7.0.2 + vitest: + specifier: 'catalog:' + version: 5.0.3(@opentelemetry/api@1.9.1)(@types/node@26.6.2)(jsdom@30.1.1)(msw@2.15.0(@types/node@26.6.2)(typescript@7.0.2))(vite@8.3.4(@types/node@26.6.2)(tsx@4.23.15)) packages/ai: dependencies: @@ -1985,21 +1991,21 @@ importers: '@openstatus/db': specifier: workspace:* version: link:../db + '@openstatus/test-utils': + specifier: workspace:* + version: link:../test-utils '@openstatus/tsconfig': specifier: workspace:* version: link:../tsconfig - '@std/expect': - specifier: jsr:^1.0.19 - version: '@jsr/std__expect@1.0.20' - '@std/testing': - specifier: jsr:^1.0.19 - version: '@jsr/std__testing@1.0.20' '@types/node': specifier: 'catalog:' version: 26.6.2 typescript: specifier: 'catalog:' version: 7.0.2 + vitest: + specifier: 'catalog:' + version: 5.0.3(@opentelemetry/api@1.9.1)(@types/node@26.6.2)(jsdom@30.1.1)(msw@2.15.0(@types/node@26.6.2)(typescript@7.0.2))(vite@8.3.4(@types/node@26.6.2)(tsx@4.23.15)) packages/analytics: dependencies: @@ -2170,15 +2176,12 @@ importers: specifier: 'catalog:' version: 4.6.5 devDependencies: + '@openstatus/test-utils': + specifier: workspace:* + version: link:../test-utils '@openstatus/tsconfig': specifier: workspace:* version: link:../tsconfig - '@std/expect': - specifier: jsr:^1.0.19 - version: '@jsr/std__expect@1.0.20' - '@std/testing': - specifier: jsr:^1.0.19 - version: '@jsr/std__testing@1.0.20' '@types/node': specifier: 'catalog:' version: 26.6.2 @@ -2191,6 +2194,9 @@ importers: typescript: specifier: 'catalog:' version: 7.0.2 + vitest: + specifier: 'catalog:' + version: 5.0.3(@opentelemetry/api@1.9.1)(@types/node@26.6.2)(jsdom@30.1.1)(msw@2.15.0(@types/node@26.6.2)(typescript@7.0.2))(vite@8.3.4(@types/node@26.6.2)) packages/assertions: dependencies: @@ -2201,18 +2207,18 @@ importers: specifier: 'catalog:' version: 4.6.5 devDependencies: + '@openstatus/test-utils': + specifier: workspace:* + version: link:../test-utils '@openstatus/tsconfig': specifier: workspace:* version: link:../tsconfig - '@std/expect': - specifier: jsr:^1.0.19 - version: '@jsr/std__expect@1.0.20' - '@std/testing': - specifier: jsr:^1.0.19 - version: '@jsr/std__testing@1.0.20' typescript: specifier: 'catalog:' version: 7.0.2 + vitest: + specifier: 'catalog:' + version: 5.0.3(@opentelemetry/api@1.9.1)(@types/node@26.6.2)(jsdom@30.1.1)(msw@2.15.0(@types/node@26.6.2)(typescript@7.0.2))(vite@8.3.4(@types/node@26.6.2)) packages/db: dependencies: @@ -2253,15 +2259,12 @@ importers: specifier: 'catalog:' version: 4.6.5 devDependencies: + '@openstatus/test-utils': + specifier: workspace:* + version: link:../test-utils '@openstatus/tsconfig': specifier: workspace:* version: link:../tsconfig - '@std/expect': - specifier: jsr:^1.0.19 - version: '@jsr/std__expect@1.0.20' - '@std/testing': - specifier: jsr:^1.0.19 - version: '@jsr/std__testing@1.0.20' '@types/bcryptjs': specifier: 'catalog:' version: 3.0.0 @@ -2277,6 +2280,9 @@ importers: typescript: specifier: 'catalog:' version: 7.0.2 + vitest: + specifier: 'catalog:' + version: 5.0.3(@opentelemetry/api@1.9.1)(@types/node@26.6.2)(jsdom@30.1.1)(msw@2.15.0(@types/node@26.6.2)(typescript@7.0.2))(vite@8.3.4(@types/node@26.6.2)) packages/emails: dependencies: @@ -2305,18 +2311,15 @@ importers: specifier: 'catalog:' version: 4.6.5 devDependencies: + '@openstatus/test-utils': + specifier: workspace:* + version: link:../test-utils '@openstatus/tsconfig': specifier: workspace:* version: link:../tsconfig '@react-email/ui': specifier: 'catalog:' version: 6.9.5(@babel/core@7.29.7(supports-color@8.1.1))(@opentelemetry/api@1.9.1)(@types/node@26.6.2)(babel-plugin-macros@3.1.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0) - '@std/expect': - specifier: jsr:^1.0.19 - version: '@jsr/std__expect@1.0.20' - '@std/testing': - specifier: jsr:^1.0.19 - version: '@jsr/std__testing@1.0.20' '@types/node': specifier: 'catalog:' version: 26.6.2 @@ -2326,6 +2329,9 @@ importers: typescript: specifier: 'catalog:' version: 7.0.2 + vitest: + specifier: 'catalog:' + version: 5.0.3(@opentelemetry/api@1.9.1)(@types/node@26.6.2)(jsdom@30.1.1)(msw@2.15.0(@types/node@26.6.2)(typescript@7.0.2))(vite@8.3.4(@types/node@26.6.2)) packages/error: dependencies: @@ -2342,18 +2348,18 @@ importers: packages/header-analysis: devDependencies: + '@openstatus/test-utils': + specifier: workspace:* + version: link:../test-utils '@openstatus/tsconfig': specifier: workspace:* version: link:../tsconfig - '@std/expect': - specifier: jsr:^1.0.19 - version: '@jsr/std__expect@1.0.20' - '@std/testing': - specifier: jsr:^1.0.19 - version: '@jsr/std__testing@1.0.20' typescript: specifier: 'catalog:' version: 7.0.2 + vitest: + specifier: 'catalog:' + version: 5.0.3(@opentelemetry/api@1.9.1)(@types/node@26.6.2)(jsdom@30.1.1)(msw@2.15.0(@types/node@26.6.2)(typescript@7.0.2))(vite@8.3.4(@types/node@26.6.2)(tsx@4.23.15)) packages/icons: dependencies: @@ -2386,18 +2392,18 @@ importers: '@openstatus/regions': specifier: workspace:* version: link:../regions + '@openstatus/test-utils': + specifier: workspace:* + version: link:../test-utils '@openstatus/tsconfig': specifier: workspace:* version: link:../tsconfig - '@std/expect': - specifier: jsr:^1.0.19 - version: '@jsr/std__expect@1.0.20' - '@std/testing': - specifier: jsr:^1.0.19 - version: '@jsr/std__testing@1.0.20' typescript: specifier: 'catalog:' version: 7.0.2 + vitest: + specifier: 'catalog:' + version: 5.0.3(@opentelemetry/api@1.9.1)(@types/node@26.6.2)(jsdom@30.1.1)(msw@2.15.0(@types/node@26.6.2)(typescript@7.0.2))(vite@8.3.4(@types/node@26.6.2)) packages/locales: dependencies: @@ -2421,21 +2427,21 @@ importers: specifier: workspace:* version: link:../../regions devDependencies: + '@openstatus/test-utils': + specifier: workspace:* + version: link:../../test-utils '@openstatus/tsconfig': specifier: workspace:* version: link:../../tsconfig - '@std/expect': - specifier: jsr:^1.0.19 - version: '@jsr/std__expect@1.0.20' - '@std/testing': - specifier: jsr:^1.0.19 - version: '@jsr/std__testing@1.0.20' '@types/node': specifier: 'catalog:' version: 26.6.2 typescript: specifier: 'catalog:' version: 7.0.2 + vitest: + specifier: 'catalog:' + version: 5.0.3(@opentelemetry/api@1.9.1)(@types/node@26.6.2)(jsdom@30.1.1)(msw@2.15.0(@types/node@26.6.2)(typescript@7.0.2))(vite@8.3.4(@types/node@26.6.2)) packages/notifications/bird-whatsapp: dependencies: @@ -2452,21 +2458,21 @@ importers: specifier: 'catalog:' version: 4.6.5 devDependencies: + '@openstatus/test-utils': + specifier: workspace:* + version: link:../../test-utils '@openstatus/tsconfig': specifier: workspace:* version: link:../../tsconfig - '@std/expect': - specifier: jsr:^1.0.19 - version: '@jsr/std__expect@1.0.20' - '@std/testing': - specifier: jsr:^1.0.19 - version: '@jsr/std__testing@1.0.20' '@types/node': specifier: 'catalog:' version: 26.6.2 typescript: specifier: 'catalog:' version: 7.0.2 + vitest: + specifier: 'catalog:' + version: 5.0.3(@opentelemetry/api@1.9.1)(@types/node@26.6.2)(jsdom@30.1.1)(msw@2.15.0(@types/node@26.6.2)(typescript@7.0.2))(vite@8.3.4(@types/node@26.6.2)) packages/notifications/discord: dependencies: @@ -2483,21 +2489,21 @@ importers: specifier: 'catalog:' version: 4.6.5 devDependencies: + '@openstatus/test-utils': + specifier: workspace:* + version: link:../../test-utils '@openstatus/tsconfig': specifier: workspace:* version: link:../../tsconfig - '@std/expect': - specifier: jsr:^1.0.19 - version: '@jsr/std__expect@1.0.20' - '@std/testing': - specifier: jsr:^1.0.19 - version: '@jsr/std__testing@1.0.20' '@types/node': specifier: 'catalog:' version: 26.6.2 typescript: specifier: 'catalog:' version: 7.0.2 + vitest: + specifier: 'catalog:' + version: 5.0.3(@opentelemetry/api@1.9.1)(@types/node@26.6.2)(jsdom@30.1.1)(msw@2.15.0(@types/node@26.6.2)(typescript@7.0.2))(vite@8.3.4(@types/node@26.6.2)) packages/notifications/email: dependencies: @@ -2547,6 +2553,9 @@ importers: typescript: specifier: 'catalog:' version: 7.0.2 + vitest: + specifier: 'catalog:' + version: 5.0.3(@opentelemetry/api@1.9.1)(@types/node@26.6.2)(jsdom@30.1.1)(msw@2.15.0(@types/node@26.6.2)(typescript@7.0.2))(vite@8.3.4(@types/node@26.6.2)) packages/notifications/google-chat: dependencies: @@ -2563,21 +2572,21 @@ importers: specifier: 'catalog:' version: 4.6.5 devDependencies: + '@openstatus/test-utils': + specifier: workspace:* + version: link:../../test-utils '@openstatus/tsconfig': specifier: workspace:* version: link:../../tsconfig - '@std/expect': - specifier: jsr:^1.0.19 - version: '@jsr/std__expect@1.0.20' - '@std/testing': - specifier: jsr:^1.0.19 - version: '@jsr/std__testing@1.0.20' '@types/node': specifier: 'catalog:' version: 26.6.2 typescript: specifier: 'catalog:' version: 7.0.2 + vitest: + specifier: 'catalog:' + version: 5.0.3(@opentelemetry/api@1.9.1)(@types/node@26.6.2)(jsdom@30.1.1)(msw@2.15.0(@types/node@26.6.2)(typescript@7.0.2))(vite@8.3.4(@types/node@26.6.2)) packages/notifications/grafana-oncall: dependencies: @@ -2594,21 +2603,21 @@ importers: specifier: 'catalog:' version: 4.6.5 devDependencies: + '@openstatus/test-utils': + specifier: workspace:* + version: link:../../test-utils '@openstatus/tsconfig': specifier: workspace:* version: link:../../tsconfig - '@std/expect': - specifier: jsr:^1.0.19 - version: '@jsr/std__expect@1.0.20' - '@std/testing': - specifier: jsr:^1.0.19 - version: '@jsr/std__testing@1.0.20' '@types/node': specifier: 'catalog:' version: 26.6.2 typescript: specifier: 'catalog:' version: 7.0.2 + vitest: + specifier: 'catalog:' + version: 5.0.3(@opentelemetry/api@1.9.1)(@types/node@26.6.2)(jsdom@30.1.1)(msw@2.15.0(@types/node@26.6.2)(typescript@7.0.2))(vite@8.3.4(@types/node@26.6.2)) packages/notifications/ms-teams: dependencies: @@ -2625,21 +2634,21 @@ importers: specifier: 'catalog:' version: 4.6.5 devDependencies: + '@openstatus/test-utils': + specifier: workspace:* + version: link:../../test-utils '@openstatus/tsconfig': specifier: workspace:* version: link:../../tsconfig - '@std/expect': - specifier: jsr:^1.0.19 - version: '@jsr/std__expect@1.0.20' - '@std/testing': - specifier: jsr:^1.0.19 - version: '@jsr/std__testing@1.0.20' '@types/node': specifier: 'catalog:' version: 26.6.2 typescript: specifier: 'catalog:' version: 7.0.2 + vitest: + specifier: 'catalog:' + version: 5.0.3(@opentelemetry/api@1.9.1)(@types/node@26.6.2)(jsdom@30.1.1)(msw@2.15.0(@types/node@26.6.2)(typescript@7.0.2))(vite@8.3.4(@types/node@26.6.2)) packages/notifications/ntfy: dependencies: @@ -2656,21 +2665,21 @@ importers: specifier: 'catalog:' version: 4.6.5 devDependencies: + '@openstatus/test-utils': + specifier: workspace:* + version: link:../../test-utils '@openstatus/tsconfig': specifier: workspace:* version: link:../../tsconfig - '@std/expect': - specifier: jsr:^1.0.19 - version: '@jsr/std__expect@1.0.20' - '@std/testing': - specifier: jsr:^1.0.19 - version: '@jsr/std__testing@1.0.20' '@types/node': specifier: 'catalog:' version: 26.6.2 typescript: specifier: 'catalog:' version: 7.0.2 + vitest: + specifier: 'catalog:' + version: 5.0.3(@opentelemetry/api@1.9.1)(@types/node@26.6.2)(jsdom@30.1.1)(msw@2.15.0(@types/node@26.6.2)(typescript@7.0.2))(vite@8.3.4(@types/node@26.6.2)) packages/notifications/opsgenie: dependencies: @@ -2693,15 +2702,12 @@ importers: specifier: 'catalog:' version: 4.6.5 devDependencies: + '@openstatus/test-utils': + specifier: workspace:* + version: link:../../test-utils '@openstatus/tsconfig': specifier: workspace:* version: link:../../tsconfig - '@std/expect': - specifier: jsr:^1.0.19 - version: '@jsr/std__expect@1.0.20' - '@std/testing': - specifier: jsr:^1.0.19 - version: '@jsr/std__testing@1.0.20' '@types/node': specifier: 'catalog:' version: 26.6.2 @@ -2714,6 +2720,9 @@ importers: typescript: specifier: 'catalog:' version: 7.0.2 + vitest: + specifier: 'catalog:' + version: 5.0.3(@opentelemetry/api@1.9.1)(@types/node@26.6.2)(jsdom@30.1.1)(msw@2.15.0(@types/node@26.6.2)(typescript@7.0.2))(vite@8.3.4(@types/node@26.6.2)) packages/notifications/pagerduty: dependencies: @@ -2736,15 +2745,12 @@ importers: specifier: 'catalog:' version: 4.6.5 devDependencies: + '@openstatus/test-utils': + specifier: workspace:* + version: link:../../test-utils '@openstatus/tsconfig': specifier: workspace:* version: link:../../tsconfig - '@std/expect': - specifier: jsr:^1.0.19 - version: '@jsr/std__expect@1.0.20' - '@std/testing': - specifier: jsr:^1.0.19 - version: '@jsr/std__testing@1.0.20' '@types/node': specifier: 'catalog:' version: 26.6.2 @@ -2757,6 +2763,9 @@ importers: typescript: specifier: 'catalog:' version: 7.0.2 + vitest: + specifier: 'catalog:' + version: 5.0.3(@opentelemetry/api@1.9.1)(@types/node@26.6.2)(jsdom@30.1.1)(msw@2.15.0(@types/node@26.6.2)(typescript@7.0.2))(vite@8.3.4(@types/node@26.6.2)) packages/notifications/slack: dependencies: @@ -2773,21 +2782,21 @@ importers: specifier: 'catalog:' version: 4.6.5 devDependencies: + '@openstatus/test-utils': + specifier: workspace:* + version: link:../../test-utils '@openstatus/tsconfig': specifier: workspace:* version: link:../../tsconfig - '@std/expect': - specifier: jsr:^1.0.19 - version: '@jsr/std__expect@1.0.20' - '@std/testing': - specifier: jsr:^1.0.19 - version: '@jsr/std__testing@1.0.20' '@types/node': specifier: 'catalog:' version: 26.6.2 typescript: specifier: 'catalog:' version: 7.0.2 + vitest: + specifier: 'catalog:' + version: 5.0.3(@opentelemetry/api@1.9.1)(@types/node@26.6.2)(jsdom@30.1.1)(msw@2.15.0(@types/node@26.6.2)(typescript@7.0.2))(vite@8.3.4(@types/node@26.6.2)) packages/notifications/telegram: dependencies: @@ -2801,21 +2810,21 @@ importers: specifier: 'catalog:' version: 4.6.5 devDependencies: + '@openstatus/test-utils': + specifier: workspace:* + version: link:../../test-utils '@openstatus/tsconfig': specifier: workspace:* version: link:../../tsconfig - '@std/expect': - specifier: jsr:^1.0.19 - version: '@jsr/std__expect@1.0.20' - '@std/testing': - specifier: jsr:^1.0.19 - version: '@jsr/std__testing@1.0.20' '@types/node': specifier: 'catalog:' version: 26.6.2 typescript: specifier: 'catalog:' version: 7.0.2 + vitest: + specifier: 'catalog:' + version: 5.0.3(@opentelemetry/api@1.9.1)(@types/node@26.6.2)(jsdom@30.1.1)(msw@2.15.0(@types/node@26.6.2)(typescript@7.0.2))(vite@8.3.4(@types/node@26.6.2)) packages/notifications/twillio-sms: dependencies: @@ -2835,15 +2844,12 @@ importers: specifier: 'catalog:' version: 4.6.5 devDependencies: + '@openstatus/test-utils': + specifier: workspace:* + version: link:../../test-utils '@openstatus/tsconfig': specifier: workspace:* version: link:../../tsconfig - '@std/expect': - specifier: jsr:^1.0.19 - version: '@jsr/std__expect@1.0.20' - '@std/testing': - specifier: jsr:^1.0.19 - version: '@jsr/std__testing@1.0.20' '@types/node': specifier: 'catalog:' version: 26.6.2 @@ -2853,6 +2859,9 @@ importers: typescript: specifier: 'catalog:' version: 7.0.2 + vitest: + specifier: 'catalog:' + version: 5.0.3(@opentelemetry/api@1.9.1)(@types/node@26.6.2)(jsdom@30.1.1)(msw@2.15.0(@types/node@26.6.2)(typescript@7.0.2))(vite@8.3.4(@types/node@26.6.2)) packages/notifications/webhook: dependencies: @@ -2869,21 +2878,21 @@ importers: specifier: 'catalog:' version: 4.6.5 devDependencies: + '@openstatus/test-utils': + specifier: workspace:* + version: link:../../test-utils '@openstatus/tsconfig': specifier: workspace:* version: link:../../tsconfig - '@std/expect': - specifier: jsr:^1.0.19 - version: '@jsr/std__expect@1.0.20' - '@std/testing': - specifier: jsr:^1.0.19 - version: '@jsr/std__testing@1.0.20' '@types/node': specifier: 'catalog:' version: 26.6.2 typescript: specifier: 'catalog:' version: 7.0.2 + vitest: + specifier: 'catalog:' + version: 5.0.3(@opentelemetry/api@1.9.1)(@types/node@26.6.2)(jsdom@30.1.1)(msw@2.15.0(@types/node@26.6.2)(typescript@7.0.2))(vite@8.3.4(@types/node@26.6.2)) packages/proto: dependencies: @@ -2993,21 +3002,21 @@ importers: specifier: 'catalog:' version: 4.6.5 devDependencies: + '@openstatus/test-utils': + specifier: workspace:* + version: link:../test-utils '@openstatus/tsconfig': specifier: workspace:* version: link:../tsconfig - '@std/expect': - specifier: jsr:^1.0.19 - version: '@jsr/std__expect@1.0.20' - '@std/testing': - specifier: jsr:^1.0.19 - version: '@jsr/std__testing@1.0.20' '@types/node': specifier: 'catalog:' version: 26.6.2 typescript: specifier: 'catalog:' version: 7.0.2 + vitest: + specifier: 'catalog:' + version: 5.0.3(@opentelemetry/api@1.9.1)(@types/node@26.6.2)(jsdom@30.1.1)(msw@2.15.0(@types/node@26.6.2)(typescript@7.0.2))(vite@8.3.4(@types/node@26.6.2)) packages/status-fetcher: dependencies: @@ -3021,15 +3030,15 @@ importers: specifier: 'catalog:' version: 4.6.5 devDependencies: - '@std/expect': - specifier: jsr:^1.0.19 - version: '@jsr/std__expect@1.0.20' - '@std/testing': - specifier: jsr:^1.0.19 - version: '@jsr/std__testing@1.0.20' + '@openstatus/test-utils': + specifier: workspace:* + version: link:../test-utils typescript: specifier: 'catalog:' version: 7.0.2 + vitest: + specifier: 'catalog:' + version: 5.0.3(@opentelemetry/api@1.9.1)(@types/node@26.6.2)(jsdom@30.1.1)(msw@2.15.0(@types/node@26.6.2)(typescript@7.0.2))(vite@8.3.4(@types/node@26.6.2)) packages/subscriptions: dependencies: @@ -3058,30 +3067,30 @@ importers: specifier: 'catalog:' version: 4.6.5 devDependencies: + '@openstatus/test-utils': + specifier: workspace:* + version: link:../test-utils '@openstatus/tsconfig': specifier: workspace:* version: link:../tsconfig - '@std/expect': - specifier: jsr:^1.0.19 - version: '@jsr/std__expect@1.0.20' - '@std/testing': - specifier: jsr:^1.0.19 - version: '@jsr/std__testing@1.0.20' '@types/node': specifier: 'catalog:' version: 26.6.2 typescript: specifier: 'catalog:' version: 7.0.2 + vitest: + specifier: 'catalog:' + version: 5.0.3(@opentelemetry/api@1.9.1)(@types/node@26.6.2)(jsdom@30.1.1)(msw@2.15.0(@types/node@26.6.2)(typescript@7.0.2))(vite@8.3.4(@types/node@26.6.2)) packages/test-utils: dependencies: - '@std/expect': - specifier: jsr:^1.0.19 - version: '@jsr/std__expect@1.0.20' - '@std/testing': - specifier: jsr:^1.0.19 - version: '@jsr/std__testing@1.0.20' + vite: + specifier: 'catalog:' + version: 8.3.4(@types/node@26.6.2)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.1) + vitest: + specifier: 'catalog:' + version: 5.0.3(@opentelemetry/api@1.9.1)(@types/node@26.6.2)(jsdom@30.1.1)(msw@2.15.0(@types/node@26.6.2)(typescript@7.0.2))(vite@8.3.4(@types/node@26.6.2)(tsx@4.23.15)) devDependencies: '@openstatus/tsconfig': specifier: workspace:* @@ -3092,21 +3101,21 @@ importers: packages/theme-store: devDependencies: + '@openstatus/test-utils': + specifier: workspace:* + version: link:../test-utils '@openstatus/tsconfig': specifier: workspace:* version: link:../tsconfig - '@std/expect': - specifier: jsr:^1.0.19 - version: '@jsr/std__expect@1.0.20' - '@std/testing': - specifier: jsr:^1.0.19 - version: '@jsr/std__testing@1.0.20' '@types/node': specifier: 'catalog:' version: 26.6.2 typescript: specifier: 'catalog:' version: 7.0.2 + vitest: + specifier: 'catalog:' + version: 5.0.3(@opentelemetry/api@1.9.1)(@types/node@26.6.2)(jsdom@30.1.1)(msw@2.15.0(@types/node@26.6.2)(typescript@7.0.2))(vite@8.3.4(@types/node@26.6.2)) packages/tinybird: dependencies: @@ -3123,18 +3132,15 @@ importers: '@openstatus/db': specifier: workspace:* version: link:../db + '@openstatus/test-utils': + specifier: workspace:* + version: link:../test-utils '@openstatus/tsconfig': specifier: workspace:* version: link:../tsconfig '@openstatus/utils': specifier: workspace:* version: link:../utils - '@std/expect': - specifier: jsr:^1.0.19 - version: '@jsr/std__expect@1.0.20' - '@std/testing': - specifier: jsr:^1.0.19 - version: '@jsr/std__testing@1.0.20' '@types/node': specifier: 'catalog:' version: 26.6.2 @@ -3144,6 +3150,9 @@ importers: typescript: specifier: 'catalog:' version: 7.0.2 + vitest: + specifier: 'catalog:' + version: 5.0.3(@opentelemetry/api@1.9.1)(@types/node@26.6.2)(jsdom@30.1.1)(msw@2.15.0(@types/node@26.6.2)(typescript@7.0.2))(vite@8.3.4(@types/node@26.6.2)) packages/tracker: dependencies: @@ -3157,18 +3166,18 @@ importers: specifier: 'catalog:' version: 4.6.5 devDependencies: + '@openstatus/test-utils': + specifier: workspace:* + version: link:../test-utils '@openstatus/tsconfig': specifier: workspace:* version: link:../tsconfig - '@std/expect': - specifier: jsr:^1.0.19 - version: '@jsr/std__expect@1.0.20' - '@std/testing': - specifier: jsr:^1.0.19 - version: '@jsr/std__testing@1.0.20' typescript: specifier: 'catalog:' version: 7.0.2 + vitest: + specifier: 'catalog:' + version: 5.0.3(@opentelemetry/api@1.9.1)(@types/node@26.6.2)(jsdom@30.1.1)(msw@2.15.0(@types/node@26.6.2)(typescript@7.0.2))(vite@8.3.4(@types/node@26.6.2)) packages/tsconfig: {} @@ -3371,15 +3380,12 @@ importers: specifier: 'catalog:' version: 1.39.0 devDependencies: + '@openstatus/test-utils': + specifier: workspace:* + version: link:../test-utils '@openstatus/tsconfig': specifier: workspace:* version: link:../tsconfig - '@std/expect': - specifier: jsr:^1.0.19 - version: '@jsr/std__expect@1.0.20' - '@std/testing': - specifier: jsr:^1.0.19 - version: '@jsr/std__testing@1.0.20' '@types/node': specifier: 'catalog:' version: 26.6.2 @@ -3389,6 +3395,9 @@ importers: typescript: specifier: 'catalog:' version: 7.0.2 + vitest: + specifier: 'catalog:' + version: 5.0.3(@opentelemetry/api@1.9.1)(@types/node@26.6.2)(jsdom@30.1.1)(msw@2.15.0(@types/node@26.6.2)(typescript@7.0.2))(vite@8.3.4(@types/node@26.6.2)(tsx@4.23.15)) packages/utils: dependencies: @@ -3402,18 +3411,18 @@ importers: specifier: 'catalog:' version: 4.6.5 devDependencies: + '@openstatus/test-utils': + specifier: workspace:* + version: link:../test-utils '@openstatus/tsconfig': specifier: workspace:* version: link:../tsconfig - '@std/expect': - specifier: jsr:^1.0.19 - version: '@jsr/std__expect@1.0.20' - '@std/testing': - specifier: jsr:^1.0.19 - version: '@jsr/std__testing@1.0.20' typescript: specifier: 'catalog:' version: 7.0.2 + vitest: + specifier: 'catalog:' + version: 5.0.3(@opentelemetry/api@1.9.1)(@types/node@26.6.2)(jsdom@30.1.1)(msw@2.15.0(@types/node@26.6.2)(typescript@7.0.2))(vite@8.3.4(@types/node@26.6.2)) packages: @@ -5287,30 +5296,6 @@ packages: peerDependencies: jsep: ^0.4.0||^1.0.0 - '@jsr/std__assert@1.0.19': - resolution: {integrity: sha512-pEj6RPkGbqlgRmyKwATp4cUs6+ijxtdrv3bq8v1d2I2CEcMEyPaO8cVKro61wGRDH4cNg8Zx6haztvK/9m7gkA==, tarball: https://npm.jsr.io/~/11/@jsr/std__assert/1.0.19.tgz} - - '@jsr/std__async@1.5.0': - resolution: {integrity: sha512-I2Qekl1oQYM+dpI2RQUCKi1TBuaA7Od3LNzxcqls9s1e2ytiZyREL4403qHJ6UbLNAwbqO2ZhdWAWDUQAajyiA==, tarball: https://npm.jsr.io/~/11/@jsr/std__async/1.5.0.tgz} - - '@jsr/std__data-structures@1.1.2': - resolution: {integrity: sha512-YaNAaUYGr3qBnq8LJ0/FSW6tsnDBMX7hcPuGU7Zi/+aWlAJsZ4OroBNSpRIflizbOvCJCycAnXe9yLjIwDF2KA==, tarball: https://npm.jsr.io/~/11/@jsr/std__data-structures/1.1.2.tgz} - - '@jsr/std__expect@1.0.20': - resolution: {integrity: sha512-AkDNshk9qIKP7OzRcSZ9qb4txPJVOxd2mDWl0XtU19eZecIzcNrxrG9gnz1j5t+U+vm6AEHkjMdwnvIfFQsJ7g==, tarball: https://npm.jsr.io/~/11/@jsr/std__expect/1.0.20.tgz} - - '@jsr/std__fs@1.0.24': - resolution: {integrity: sha512-UunW2Rg++9sA74NJnYMGTsMVQgoFRAwPP02wRRohL0PQBNorbyxHiyCMRaxYQCtLepTLkC4nUyMtcwrGKqTR6g==, tarball: https://npm.jsr.io/~/11/@jsr/std__fs/1.0.24.tgz} - - '@jsr/std__internal@1.0.14': - resolution: {integrity: sha512-JT8b/t40WcR9q0GDwRZUooY7aXeXnFal8iidE/5TckdAFtvpVAsaJ71/Xgf1SfoChs41s6CZkuCYM8toaNgdvA==, tarball: https://npm.jsr.io/~/11/@jsr/std__internal/1.0.14.tgz} - - '@jsr/std__path@1.1.6': - resolution: {integrity: sha512-H1Cmg6z8jFyIsQbVV+vZB4eOx4k6Qg8lyUM6l7nZysj5MZumg89kb0M1bsPBTGQis3fw6mJFkJELo5+AK6oCFA==, tarball: https://npm.jsr.io/~/11/@jsr/std__path/1.1.6.tgz} - - '@jsr/std__testing@1.0.20': - resolution: {integrity: sha512-DRCuU1u4OXD/AXBJ2q9IvMHthMSti1V1604b2+xa5WaAcm6Ghazk0ac1AsMwiAOXEpK0nfIXd1MD3ttT8LQ5Rw==, tarball: https://npm.jsr.io/~/11/@jsr/std__testing/1.0.20.tgz} - '@jsr/std__yaml@1.3.0': resolution: {integrity: sha512-TuFbXlSy3nRuQuR8co2t7yzhv53ObdYZZidn8M958Glj6vBp5b7nxg7wTN8VTyCwC3vpbr6Mfdx9auNe/BMAVA==, tarball: https://npm.jsr.io/~/11/@jsr/std__yaml/1.3.0.tgz} @@ -8211,6 +8196,9 @@ packages: resolution: {integrity: sha512-WRZOuCuaz8UcZZE4R5HXTco2goQSI2XxjGY3hbM/xDvwmqFWd4ivooImsMx65OKM6CtNKbnZ5YL+YwAwK7c1dg==} deprecated: This is a stub types definition. bcryptjs provides its own type definitions, so you do not need this installed. + '@types/chai@5.2.3': + resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} + '@types/cors@2.8.19': resolution: {integrity: sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==} @@ -8247,6 +8235,9 @@ packages: '@types/debug@4.1.13': resolution: {integrity: sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw==} + '@types/deep-eql@4.0.2': + resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} + '@types/dom-speech-recognition@0.0.12': resolution: {integrity: sha512-SmLovKV3e/J71U5CBmKYe03Q75biuw7jiEWGoO1arc47CjtmxCr+W7cPJcAAwySSJFwr+jWkr/fPVuVuL+D6Dw==} @@ -8486,6 +8477,20 @@ packages: resolution: {integrity: sha512-TcbdH3YtGS2KU98T99dN4sbf+E4vbJZsJTRN97wKQ7QWQ0BR7U02QX3KN4+5GHxBtKTj1c5CnvTTitc2ZRad0A==} engines: {node: '>= 20'} + '@vitest/mocker@5.0.3': + resolution: {integrity: sha512-T8sWAIbkSyAjkwTcaEc3Iu0o9A27X1/kdXrizhZkGuSKScRQtRzclfAMpOTcGdXCsqxeWlpGy3XjqaW8CpLORg==} + peerDependencies: + msw: ^2.4.9 + vite: ^6.0.0 || ^7.0.0 || ^8.0.0 + peerDependenciesMeta: + msw: + optional: true + vite: + optional: true + + '@vitest/spy@5.0.3': + resolution: {integrity: sha512-XhFysQTB8AZ+P4gMi+Lpo99vg2AZi0qKpaB9yXQl37+CaMEAPO3iH/wGVnSyL5MPERiLezpqTVtrR6UZH5GCXg==} + '@webassemblyjs/ast@1.14.1': resolution: {integrity: sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==} @@ -8795,6 +8800,10 @@ packages: resolution: {integrity: sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==} engines: {node: '>=10'} + assertion-error@2.0.1: + resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} + engines: {node: '>=12'} + ast-types@0.16.3: resolution: {integrity: sha512-FvWoWYfSCM6kRxCSH+MGLHIKKGRL6A6AW7Zek2O32REPQRdg131428uRTKMBYAeRd3XXAaHDS60Wpri7CdKDrA==} engines: {node: '>=4'} @@ -8922,6 +8931,10 @@ packages: ccount@2.0.1: resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==} + chai@6.3.0: + resolution: {integrity: sha512-XWAtwJ6OHO+tj0EKCs0Y2UamnyOxseZWltU4x2U2wh8g4AigdjwvtUjvLP2tqkA/avxHEtzxNaqGq/YGNwckKg==} + engines: {node: '>=18'} + chalk@5.6.2: resolution: {integrity: sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==} engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} @@ -9644,6 +9657,10 @@ packages: resolution: {integrity: sha512-9Be3ZoN4LmYR90tUoVu2te2BsbzHfhJyfEiAVfz7N5/zv+jduIfLrV2xdQXOHbaD6KgpGdO9PRPM1Y4Q9QkPkA==} engines: {node: ^18.19.0 || >=20.5.0} + expect-type@1.4.0: + resolution: {integrity: sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==} + engines: {node: '>=12.0.0'} + express-rate-limit@8.7.0: resolution: {integrity: sha512-hOwV7WOxXfjRpAM1DSJWZDXx3GhplwD8IfwuwvogD8i1Qnkgosw/H45s4ZnFAUHDAhPjlY9hLBvJhKmGMyY26g==} engines: {node: '>= 16'} @@ -10288,30 +10305,60 @@ packages: cpu: [arm64] os: [android] + lightningcss-android-arm64@1.33.0: + resolution: {integrity: sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [android] + lightningcss-darwin-arm64@1.32.0: resolution: {integrity: sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==} engines: {node: '>= 12.0.0'} cpu: [arm64] os: [darwin] + lightningcss-darwin-arm64@1.33.0: + resolution: {integrity: sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [darwin] + lightningcss-darwin-x64@1.32.0: resolution: {integrity: sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [darwin] + lightningcss-darwin-x64@1.33.0: + resolution: {integrity: sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [darwin] + lightningcss-freebsd-x64@1.32.0: resolution: {integrity: sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [freebsd] + lightningcss-freebsd-x64@1.33.0: + resolution: {integrity: sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [freebsd] + lightningcss-linux-arm-gnueabihf@1.32.0: resolution: {integrity: sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==} engines: {node: '>= 12.0.0'} cpu: [arm] os: [linux] + lightningcss-linux-arm-gnueabihf@1.33.0: + resolution: {integrity: sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm] + os: [linux] + lightningcss-linux-arm64-gnu@1.32.0: resolution: {integrity: sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==} engines: {node: '>= 12.0.0'} @@ -10319,6 +10366,13 @@ packages: os: [linux] libc: [glibc] + lightningcss-linux-arm64-gnu@1.33.0: + resolution: {integrity: sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + libc: [glibc] + lightningcss-linux-arm64-musl@1.32.0: resolution: {integrity: sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==} engines: {node: '>= 12.0.0'} @@ -10326,6 +10380,13 @@ packages: os: [linux] libc: [musl] + lightningcss-linux-arm64-musl@1.33.0: + resolution: {integrity: sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + libc: [musl] + lightningcss-linux-x64-gnu@1.32.0: resolution: {integrity: sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==} engines: {node: '>= 12.0.0'} @@ -10333,6 +10394,13 @@ packages: os: [linux] libc: [glibc] + lightningcss-linux-x64-gnu@1.33.0: + resolution: {integrity: sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + libc: [glibc] + lightningcss-linux-x64-musl@1.32.0: resolution: {integrity: sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==} engines: {node: '>= 12.0.0'} @@ -10340,22 +10408,45 @@ packages: os: [linux] libc: [musl] + lightningcss-linux-x64-musl@1.33.0: + resolution: {integrity: sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + libc: [musl] + lightningcss-win32-arm64-msvc@1.32.0: resolution: {integrity: sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==} engines: {node: '>= 12.0.0'} cpu: [arm64] os: [win32] + lightningcss-win32-arm64-msvc@1.33.0: + resolution: {integrity: sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [win32] + lightningcss-win32-x64-msvc@1.32.0: resolution: {integrity: sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [win32] + lightningcss-win32-x64-msvc@1.33.0: + resolution: {integrity: sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [win32] + lightningcss@1.32.0: resolution: {integrity: sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==} engines: {node: '>= 12.0.0'} + lightningcss@1.33.0: + resolution: {integrity: sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==} + engines: {node: '>= 12.0.0'} + lilconfig@3.1.3: resolution: {integrity: sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==} engines: {node: '>=14'} @@ -10422,6 +10513,9 @@ packages: magic-string@0.30.21: resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + magic-string@1.4.3: + resolution: {integrity: sha512-z12OxmaPGE0F4xlpGdjuAUckipLpQlTAuAmyGhNoD7ODpYUzvDfvtxUbjM/jwznyP178oju/KDdyi220wNJ0RQ==} + mailchecker@6.0.21: resolution: {integrity: sha512-Zd2Lgj4RMnC0IBbbSjnF76Wbd+kkgKMRONgV9yOMKJhj6hcKIAQtj9MrmiGnT2Db+Dy6MsMQoG2HEhPk2YPuGA==} engines: {node: '>=0.10'} @@ -10756,6 +10850,11 @@ packages: engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true + nanoid@3.3.20: + resolution: {integrity: sha512-uKdg2G3GNCKQn9byYOpxbGqrT2fGO5KRt5J/8b3pok8rT6qxGWF6hxMyJiEYtAf+FVyYuD9hRaDqX5uPFYJ4ZQ==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + nanoid@5.1.16: resolution: {integrity: sha512-kVrnsrJqMR8+oLJnGEmSWw9BivK5mt7H3FZatVRjrc5wGqFYuBxX1yG7+A7Gi5AefkX6t/oCkizcQgpu0cY1dQ==} engines: {node: ^18 || >=20} @@ -11246,6 +11345,10 @@ packages: resolution: {integrity: sha512-RRuzqDtt5Y9h3quz5hWhK+TPnsmVs6WwSU6LkJMeY4HstUEDuYTG8UJSdawMRzmzAtV+KEoG8N3Qg2qLy5vM/A==} engines: {node: ^10 || ^12 || >=14} + postcss@8.5.31: + resolution: {integrity: sha512-YUbWYCgErwnMAVkjUnYa+KlMBzGpHwiIt/DE7f0G33AuX2LH9UcTk6KgWxd3zRU+pN3u/C+Xl3EnMLM4Hea7Sg==} + engines: {node: ^10 || ^12 || >=14} + postgres-array@2.0.0: resolution: {integrity: sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==} engines: {node: '>=4'} @@ -11817,6 +11920,10 @@ packages: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} + source-map-js@1.2.2: + resolution: {integrity: sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw==} + engines: {node: '>=0.10.0'} + source-map-support@0.5.21: resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==} @@ -11852,6 +11959,9 @@ packages: resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==} engines: {node: '>= 0.8'} + std-env@4.3.0: + resolution: {integrity: sha512-OtU/EgQ1kIm5KwqQpBC6ZEMXrZRui11w8zgfTWp8cdO9B8OaPsbA8bTHO2P+HNo1VlUTGMVBwPhydu6poeXiag==} + stdin-discarder@0.2.2: resolution: {integrity: sha512-UhDfHmA92YAlNnCfhmq0VeNL5bDbiZGg7sZ2IvPsXubGkiNa9EC+tUTsjBRsYUAz87btI6/1wf4XoVvQ3uRnmQ==} engines: {node: '>=18'} @@ -12024,6 +12134,10 @@ packages: tiny-invariant@1.3.3: resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==} + tinybench@6.2.2: + resolution: {integrity: sha512-nIZ2N2dFpjBsg2hgitKu7mA+FvsIJuGJOwysRngmVRRmWgR/UyPjwJj8c/fT/dhm8EkHnF5EOogRf+0Z2qrimA==} + engines: {node: '>=20.0.0'} + tinyexec@0.3.2: resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==} @@ -12354,6 +12468,90 @@ packages: victory-vendor@36.9.2: resolution: {integrity: sha512-PnpQQMuxlwYdocC8fIJqVXvkeViHYzotI+NJrCuav0ZYFoq912ZHBk3mCeuj+5/VpodOjPe1z0Fk2ihgzlXqjQ==} + vite@8.3.4: + resolution: {integrity: sha512-IftLE8phJhdNRIGNEV4JCCvfNScoZQVfcXaN8TQropWGJtOHcu+iAB9pHcuGvmEb/kddw0LBe/jS8ztdC6xdiQ==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + '@types/node': ^20.19.0 || >=22.12.0 + '@vitejs/devtools': ^0.7.1 + esbuild: ^0.27.0 || ^0.28.0 + jiti: '>=1.21.0' + less: ^4.0.0 + sass: ^1.70.0 + sass-embedded: ^1.70.0 + stylus: '>=0.54.8' + sugarss: ^5.0.0 + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + '@types/node': + optional: true + '@vitejs/devtools': + optional: true + esbuild: + optional: true + jiti: + optional: true + less: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + + vitest@5.0.3: + resolution: {integrity: sha512-xMw97S3rjdtj5dkVat7jCsqWBpvchs3RlpQctUqwJD0KkERk40vz2fJ77lDwW/Vzh/pk18eItYAzkodhSes3jQ==} + engines: {node: ^22.12.0 || ^24.0.0 || >=26.0.0} + hasBin: true + peerDependencies: + '@edge-runtime/vm': '*' + '@opentelemetry/api': ^1.9.0 + '@types/node': ^22.0.0 || >=24.0.0 + '@vitest/browser-playwright': 5.0.3 + '@vitest/browser-preview': 5.0.3 + '@vitest/browser-webdriverio': ^5.0.0-beta.5 || >=5.0.0 + '@vitest/coverage-istanbul': 5.0.3 + '@vitest/coverage-v8': 5.0.3 + '@vitest/ui': 5.0.3 + happy-dom: '*' + jsdom: '*' + vite: ^6.4.0 || ^7.0.0 || ^8.0.0 + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@opentelemetry/api': + optional: true + '@types/node': + optional: true + '@vitest/browser-playwright': + optional: true + '@vitest/browser-preview': + optional: true + '@vitest/browser-webdriverio': + optional: true + '@vitest/coverage-istanbul': + optional: true + '@vitest/coverage-v8': + optional: true + '@vitest/ui': + optional: true + happy-dom: + optional: true + jsdom: + optional: true + w3c-xmlserializer@6.0.0: resolution: {integrity: sha512-4Nsy8K5Tr6SPDH9jhKJOHf7ChDrc1zufZTVSF7x72hwuEXBqxqk9G6cK+K2NRUtB3iELRJqjXb4JPDMBjMTl2Q==} engines: {node: ^22.22.2 || ^24.15.0 || >=26.0.0} @@ -12419,6 +12617,11 @@ packages: engines: {node: ^16.13.0 || >=18.0.0} hasBin: true + why-is-node-running@3.2.1: + resolution: {integrity: sha512-Tb2FUhB4vUsGQlfSquQLYkApkuPAFQXGFzxWKHHumVz2dK+X1RUm/HnID4+TfIGYJ1kTcwOaCk/buYCEJr6YjQ==} + engines: {node: '>=20.11'} + hasBin: true + wrap-ansi@6.2.0: resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==} engines: {node: '>=8'} @@ -14100,44 +14303,6 @@ snapshots: dependencies: jsep: 1.4.0 - '@jsr/std__assert@1.0.19': - dependencies: - '@jsr/std__internal': 1.0.14 - - '@jsr/std__async@1.5.0': - dependencies: - '@jsr/std__data-structures': 1.1.2 - - '@jsr/std__data-structures@1.1.2': - dependencies: - '@jsr/std__assert': 1.0.19 - - '@jsr/std__expect@1.0.20': - dependencies: - '@jsr/std__assert': 1.0.19 - '@jsr/std__internal': 1.0.14 - '@jsr/std__path': 1.1.6 - - '@jsr/std__fs@1.0.24': - dependencies: - '@jsr/std__internal': 1.0.14 - '@jsr/std__path': 1.1.6 - - '@jsr/std__internal@1.0.14': {} - - '@jsr/std__path@1.1.6': - dependencies: - '@jsr/std__internal': 1.0.14 - - '@jsr/std__testing@1.0.20': - dependencies: - '@jsr/std__assert': 1.0.19 - '@jsr/std__async': 1.5.0 - '@jsr/std__data-structures': 1.1.2 - '@jsr/std__fs': 1.0.24 - '@jsr/std__internal': 1.0.14 - '@jsr/std__path': 1.1.6 - '@jsr/std__yaml@1.3.0': {} '@kobalte/core@0.13.14(solid-js@1.9.15)': @@ -16721,6 +16886,11 @@ snapshots: dependencies: bcryptjs: 3.0.3 + '@types/chai@5.2.3': + dependencies: + '@types/deep-eql': 4.0.2 + assertion-error: 2.0.1 + '@types/cors@2.8.19': dependencies: '@types/node': 26.6.2 @@ -16755,6 +16925,8 @@ snapshots: dependencies: '@types/ms': 2.1.0 + '@types/deep-eql@4.0.2': {} + '@types/dom-speech-recognition@0.0.12': {} '@types/estree-jsx@1.0.5': @@ -16947,6 +17119,18 @@ snapshots: '@vercel/cli-exec': 1.0.1 jose: 5.10.0 + '@vitest/mocker@5.0.3(msw@2.15.0(@types/node@26.6.2)(typescript@7.0.2))(vite@8.3.4(@types/node@26.6.2))': + dependencies: + '@jridgewell/trace-mapping': 0.3.31 + '@vitest/spy': 5.0.3 + estree-walker: 3.0.3 + magic-string: 1.4.3 + optionalDependencies: + msw: 2.15.0(@types/node@26.6.2)(typescript@7.0.2) + vite: 8.3.4(@types/node@26.6.2)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.1) + + '@vitest/spy@5.0.3': {} + '@webassemblyjs/ast@1.14.1': dependencies: '@webassemblyjs/helper-numbers': 1.13.2 @@ -17182,6 +17366,8 @@ snapshots: dependencies: tslib: 2.8.1 + assertion-error@2.0.1: {} + ast-types@0.16.3: dependencies: tslib: 2.8.1 @@ -17299,6 +17485,8 @@ snapshots: ccount@2.0.1: {} + chai@6.3.0: {} + chalk@5.6.2: {} character-entities-html4@2.1.0: {} @@ -18008,6 +18196,8 @@ snapshots: strip-final-newline: 4.0.0 yoctocolors: 2.2.0 + expect-type@1.4.0: {} + express-rate-limit@8.7.0(express@5.2.1(supports-color@8.1.1))(supports-color@8.1.1): dependencies: debug: 4.4.3(supports-color@8.1.1) @@ -18742,36 +18932,69 @@ snapshots: lightningcss-android-arm64@1.32.0: optional: true + lightningcss-android-arm64@1.33.0: + optional: true + lightningcss-darwin-arm64@1.32.0: optional: true + lightningcss-darwin-arm64@1.33.0: + optional: true + lightningcss-darwin-x64@1.32.0: optional: true + lightningcss-darwin-x64@1.33.0: + optional: true + lightningcss-freebsd-x64@1.32.0: optional: true + lightningcss-freebsd-x64@1.33.0: + optional: true + lightningcss-linux-arm-gnueabihf@1.32.0: optional: true + lightningcss-linux-arm-gnueabihf@1.33.0: + optional: true + lightningcss-linux-arm64-gnu@1.32.0: optional: true + lightningcss-linux-arm64-gnu@1.33.0: + optional: true + lightningcss-linux-arm64-musl@1.32.0: optional: true + lightningcss-linux-arm64-musl@1.33.0: + optional: true + lightningcss-linux-x64-gnu@1.32.0: optional: true + lightningcss-linux-x64-gnu@1.33.0: + optional: true + lightningcss-linux-x64-musl@1.32.0: optional: true + lightningcss-linux-x64-musl@1.33.0: + optional: true + lightningcss-win32-arm64-msvc@1.32.0: optional: true + lightningcss-win32-arm64-msvc@1.33.0: + optional: true + lightningcss-win32-x64-msvc@1.32.0: optional: true + lightningcss-win32-x64-msvc@1.33.0: + optional: true + lightningcss@1.32.0: dependencies: detect-libc: 2.1.2 @@ -18788,6 +19011,22 @@ snapshots: lightningcss-win32-arm64-msvc: 1.32.0 lightningcss-win32-x64-msvc: 1.32.0 + lightningcss@1.33.0: + dependencies: + detect-libc: 2.1.2 + optionalDependencies: + lightningcss-android-arm64: 1.33.0 + lightningcss-darwin-arm64: 1.33.0 + lightningcss-darwin-x64: 1.33.0 + lightningcss-freebsd-x64: 1.33.0 + lightningcss-linux-arm-gnueabihf: 1.33.0 + lightningcss-linux-arm64-gnu: 1.33.0 + lightningcss-linux-arm64-musl: 1.33.0 + lightningcss-linux-x64-gnu: 1.33.0 + lightningcss-linux-x64-musl: 1.33.0 + lightningcss-win32-arm64-msvc: 1.33.0 + lightningcss-win32-x64-msvc: 1.33.0 + lilconfig@3.1.3: {} limiter@4.1.0: {} @@ -18845,6 +19084,10 @@ snapshots: dependencies: '@jridgewell/sourcemap-codec': 1.6.0 + magic-string@1.4.3: + dependencies: + '@jridgewell/sourcemap-codec': 1.6.0 + mailchecker@6.0.21: {} markdown-extensions@2.0.0: {} @@ -19405,6 +19648,8 @@ snapshots: nanoid@3.3.19: {} + nanoid@3.3.20: {} + nanoid@5.1.16: {} nanoid@6.0.1: {} @@ -19924,6 +20169,12 @@ snapshots: picocolors: 1.1.1 source-map-js: 1.2.1 + postcss@8.5.31: + dependencies: + nanoid: 3.3.20 + picocolors: 1.1.1 + source-map-js: 1.2.2 + postgres-array@2.0.0: optional: true @@ -20773,6 +21024,8 @@ snapshots: source-map-js@1.2.1: {} + source-map-js@1.2.2: {} + source-map-support@0.5.21: dependencies: buffer-from: 1.1.2 @@ -20801,6 +21054,8 @@ snapshots: statuses@2.0.2: {} + std-env@4.3.0: {} + stdin-discarder@0.2.2: {} stream-events@1.0.5: @@ -20956,6 +21211,8 @@ snapshots: tiny-invariant@1.3.3: {} + tinybench@6.2.2: {} + tinyexec@0.3.2: {} tinyexec@1.3.1: {} @@ -21330,6 +21587,68 @@ snapshots: d3-time: 3.1.0 d3-timer: 3.0.1 + vite@8.3.4(@types/node@26.6.2)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.1): + dependencies: + lightningcss: 1.33.0 + picomatch: 4.0.7 + postcss: 8.5.31 + rolldown: 1.2.13 + tinyglobby: 0.2.17 + optionalDependencies: + '@types/node': 26.6.2 + esbuild: 0.28.2 + fsevents: 2.3.3 + jiti: 2.7.0 + terser: 5.51.2 + tsx: 4.23.15 + yaml: 2.9.1 + + vitest@5.0.3(@opentelemetry/api@1.9.1)(@types/node@26.6.2)(jsdom@30.1.1)(msw@2.15.0(@types/node@26.6.2)(typescript@7.0.2))(vite@8.3.4(@types/node@26.6.2)(tsx@4.23.15)): + dependencies: + '@types/chai': 5.2.3 + '@vitest/mocker': 5.0.3(msw@2.15.0(@types/node@26.6.2)(typescript@7.0.2))(vite@8.3.4(@types/node@26.6.2)) + chai: 6.3.0 + es-module-lexer: 2.3.2 + expect-type: 1.4.0 + magic-string: 1.4.3 + obug: 2.2.1 + picomatch: 4.0.7 + std-env: 4.3.0 + tinybench: 6.2.2 + tinyexec: 1.3.1 + tinyglobby: 0.2.17 + vite: 8.3.4(@types/node@26.6.2)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.1) + why-is-node-running: 3.2.1 + optionalDependencies: + '@opentelemetry/api': 1.9.1 + '@types/node': 26.6.2 + jsdom: 30.1.1 + transitivePeerDependencies: + - msw + + vitest@5.0.3(@opentelemetry/api@1.9.1)(@types/node@26.6.2)(jsdom@30.1.1)(msw@2.15.0(@types/node@26.6.2)(typescript@7.0.2))(vite@8.3.4(@types/node@26.6.2)): + dependencies: + '@types/chai': 5.2.3 + '@vitest/mocker': 5.0.3(msw@2.15.0(@types/node@26.6.2)(typescript@7.0.2))(vite@8.3.4(@types/node@26.6.2)) + chai: 6.3.0 + es-module-lexer: 2.3.2 + expect-type: 1.4.0 + magic-string: 1.4.3 + obug: 2.2.1 + picomatch: 4.0.7 + std-env: 4.3.0 + tinybench: 6.2.2 + tinyexec: 1.3.1 + tinyglobby: 0.2.17 + vite: 8.3.4(@types/node@26.6.2)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.1) + why-is-node-running: 3.2.1 + optionalDependencies: + '@opentelemetry/api': 1.9.1 + '@types/node': 26.6.2 + jsdom: 30.1.1 + transitivePeerDependencies: + - msw + w3c-xmlserializer@6.0.0: dependencies: xml-name-validator: 5.0.0 @@ -21455,6 +21774,8 @@ snapshots: dependencies: isexe: 3.1.5 + why-is-node-running@3.2.1: {} + wrap-ansi@6.2.0: dependencies: ansi-styles: 4.3.0 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index d51813724..9290e3846 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -220,4 +220,6 @@ catalog: unified: 11.0.5 validator: 13.15.35 vaul: 1.1.2 + vite: 8.3.4 + vitest: 5.0.3 zod: 4.6.5 -- 2.54.0 From c0a53897885defe8ab6a46f42145cd45f31a7645 Mon Sep 17 00:00:00 2001 From: Thibault Le Ouay Date: Tue, 20 Oct 2026 10:30:27 +0200 Subject: [PATCH 3/6] chore: remove deno from the repo (#2892) Deno no longer runs anything after the node runtime and vitest moves: - delete deno.lock, the deno.jsonc files and the bun-to-deno migration plan - drop setup-deno from the check, dx, migrate, proto-check and test workflows - drop deno from devbox; the lock also picks up the node 26.8.1 entry the runtime PR bumped in devbox.json but never locked - remove deno compile artifacts from the server and workflows .gitignore - reword comments and docs that pointed at deno (toolchain, NODE_ENV folding, edge-safety, import-map format) Verified with a failing deno shim first on PATH: pnpm verify, both tsdown builds, migrate and the full test run pass with no deno invocation. --- .github/workflows/check.yml | 5 - .github/workflows/dx.yml | 5 - .github/workflows/migrate.yml | 5 - .github/workflows/proto-check.yml | 5 - .github/workflows/test.yml | 5 - AGENTS.md | 6 +- CONTRIBUTING.md | 2 +- README.md | 1 - apps/dashboard/README.md | 2 +- apps/server/.gitignore | 4 - apps/server/deno.jsonc | 6 - apps/web/deno.jsonc | 10 - apps/workflows/.gitignore | 3 +- apps/workflows/deno-migration-plan.md | 437 -------------------------- deno.lock | 330 ------------------- devbox.json | 1 - devbox.lock | 62 ---- oxlint.config.ts | 2 +- packages/analytics/env.ts | 8 +- packages/emails/src/env.ts | 7 +- packages/services/src/env.ts | 10 +- packages/services/src/oauth/crypto.ts | 2 +- packages/test-utils/src/vitest.ts | 4 +- packages/tinybird/src/env.ts | 10 +- 24 files changed, 21 insertions(+), 911 deletions(-) delete mode 100644 apps/server/deno.jsonc delete mode 100644 apps/web/deno.jsonc delete mode 100644 apps/workflows/deno-migration-plan.md delete mode 100644 deno.lock diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index 46d92188f..f049e32f6 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -27,11 +27,6 @@ jobs: node-version: 26.8.1 cache: "pnpm" - - name: 🦕 Setup Deno - uses: denoland/setup-deno@v2 - with: - deno-version: 2.9.7 - - name: 📥 Download deps run: pnpm install diff --git a/.github/workflows/dx.yml b/.github/workflows/dx.yml index 1d5b0bcec..b197ba43d 100644 --- a/.github/workflows/dx.yml +++ b/.github/workflows/dx.yml @@ -41,10 +41,5 @@ jobs: - name: 📥 Download deps run: pnpm install - - name: 🦕 Setup Deno - uses: denoland/setup-deno@v2 - with: - deno-version: 2.9.7 - - name: 🔥 DX task run: pnpm dx diff --git a/.github/workflows/migrate.yml b/.github/workflows/migrate.yml index e2dcffc93..2bb1ce170 100644 --- a/.github/workflows/migrate.yml +++ b/.github/workflows/migrate.yml @@ -26,11 +26,6 @@ jobs: node-version: 26.8.1 cache: "pnpm" - - name: 🦕 Install deno - uses: denoland/setup-deno@v2 - with: - deno-version: 2.9.7 - - name: 📥 Download deps run: pnpm install diff --git a/.github/workflows/proto-check.yml b/.github/workflows/proto-check.yml index 39e5072ac..cfe2183a4 100644 --- a/.github/workflows/proto-check.yml +++ b/.github/workflows/proto-check.yml @@ -35,11 +35,6 @@ jobs: node-version: 26.8.1 cache: "pnpm" - - name: 🦕 Setup Deno - uses: denoland/setup-deno@v2 - with: - deno-version: 2.9.7 - - name: 🐹 Setup Go uses: actions/setup-go@v5 with: diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 3bf2506b8..13ff630a5 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -69,11 +69,6 @@ jobs: node-version: 26.8.1 cache: "pnpm" - - name: 🦕 Setup Deno - uses: denoland/setup-deno@v2 - with: - deno-version: 2.9.7 - - name: 📥 Download deps run: pnpm install diff --git a/AGENTS.md b/AGENTS.md index 85176e90a..1576d1d90 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -17,9 +17,9 @@ are in `apps/dashboard/README.md`. ## Toolchain -`devbox.json` pins exact node, deno, bun, turso-cli and sqld versions; the CI -workflows pin the same node and deno. Never float one back to `@latest` or -`v2.x` — `tsc` and test results differ by version, so a drifting pin makes +`devbox.json` pins exact node, bun, turso-cli and sqld versions; the CI +workflows pin the same node. Never float one back to `@latest` or +`v26.x` — `tsc` and test results differ by version, so a drifting pin makes `pnpm verify` disagree with CI for reasons unrelated to your change. ## Architecture diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index b59f41ec8..df9eb12da 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -34,7 +34,7 @@ To contribute code changes, follow these steps: packages your diff touches; those do need a local libSQL server. See the [Requirements](./README.md#requirements) list in the README for the - full toolchain (Node.js, pnpm, Bun, Deno, Turso CLI). + full toolchain (Node.js, pnpm, Bun, Turso CLI). 4. Make commits with clear and descriptive messages. Each commit should have a single logical purpose. 5. Push your branch to your forked repository. 6. Open a pull request (PR) from your branch to the original repository's `main` branch. diff --git a/README.md b/README.md index 8362d6d10..c39167777 100644 --- a/README.md +++ b/README.md @@ -135,7 +135,6 @@ The template source and the setup instructions are in [ephraimduncan/openstatus- - [Node.js](https://nodejs.org/en/) - [pnpm](https://pnpm.io/) -- [Deno](https://deno.com/) - [Turso CLI](https://docs.turso.tech/quickstart). #### Dashboard diff --git a/apps/dashboard/README.md b/apps/dashboard/README.md index 73c8ea9a2..f691c6482 100644 --- a/apps/dashboard/README.md +++ b/apps/dashboard/README.md @@ -4,7 +4,7 @@ ### Requirements -- [Node.js](https://nodejs.org/en/) >= 20.0.0 — runtime +- [Node.js](https://nodejs.org/en/) >= 26.2 — runtime (`devbox.json` pins the exact version) - [pnpm](https://pnpm.io/) >= 8 — monorepo package manager (see root `package.json` for pinned version) - [Bun](https://bun.sh/) — optional, used by the utility scripts in `src/scripts` - [Turso CLI](https://docs.turso.tech/quickstart) — starts the local libSQL server via `pnpm --filter '@openstatus/db' dev` diff --git a/apps/server/.gitignore b/apps/server/.gitignore index 12a21cd50..27ec8abbf 100644 --- a/apps/server/.gitignore +++ b/apps/server/.gitignore @@ -1,6 +1,2 @@ node_modules -app -src/_serve.bundle.mjs -.deno_compile_bundle_*.mjs -app.tmp-* slack-manifest.dev.json diff --git a/apps/server/deno.jsonc b/apps/server/deno.jsonc deleted file mode 100644 index d3de83dde..000000000 --- a/apps/server/deno.jsonc +++ /dev/null @@ -1,6 +0,0 @@ -/* Needed to fix the import aliases */ -{ - "imports": { - "@/": "./src/", - }, -} diff --git a/apps/web/deno.jsonc b/apps/web/deno.jsonc deleted file mode 100644 index 5ae72d87e..000000000 --- a/apps/web/deno.jsonc +++ /dev/null @@ -1,10 +0,0 @@ -/* Needed to fix the import aliases, and to render components in `deno test` */ -{ - "compilerOptions": { - "jsx": "react-jsx", - "jsxImportSource": "react", - }, - "imports": { - "@/": "./src/", - }, -} diff --git a/apps/workflows/.gitignore b/apps/workflows/.gitignore index e4037038c..47d38dfda 100644 --- a/apps/workflows/.gitignore +++ b/apps/workflows/.gitignore @@ -1,6 +1,5 @@ # deps node_modules/ -# build artifacts / local dev db -app +# local dev db dev.db* diff --git a/apps/workflows/deno-migration-plan.md b/apps/workflows/deno-migration-plan.md deleted file mode 100644 index 2b7b2f0f9..000000000 --- a/apps/workflows/deno-migration-plan.md +++ /dev/null @@ -1,437 +0,0 @@ -# Plan: migrate `apps/server` + `apps/workflows` from bun to Deno 2.9 - -Goal: remove **bun** from the project. Three pillars, shipped as **one big-bang PR**: - -1. **Build** — `bun build --compile` → `deno compile` (server + workflows). -2. **Install** — the Docker `pnpm install` stage → `deno install` (server + workflows). -3. **Tests** — `bun:test` → **`node:test` runner + `@std/expect`**, run via **`deno test`**, - across **all ~270 test files** (12 packages + apps server, workflows, web, status-page). - -The other apps (`web`, `dashboard`, `status-page`, `docs`, `screenshot-service`) keep -**pnpm** for install + their existing builds; their only bun usage was `bun test`, removed by -pillar 3. `pnpm` remains the local-dev + non-migrated-app package manager. - ---- - -## Agreed decisions (the design) - -| # | Area | Decision | -|---|------|----------| -| 1 | Test scope | All ~270 `bun:test` files (12 packages + server/workflows/web/status-page) | -| 2 | Rollout | One big-bang PR, guarded by a baseline test-count parity gate (#16) | -| 3 | Entrypoint | **Split**: `index.ts` exports `app`; new `serve.ts` calls `Deno.serve`, is the compile target | -| 4 | Assertions | Keep Jest-style `expect` via **`@std/expect`** | -| 5 | Mocks | **`@std/testing/mock`** (`spy`/`stub`, `using`) + **`@std/testing/time`** (`FakeTime`) | -| 6 | `mock.module` | **Hybrid**: `stub` the reachable seams; **default-param DI** for the rest; delete dead ones | -| 7 | Test infra | New **`@openstatus/test-utils`** package: matchers (`expect.extend`), setup, shared fakes | -| 8 | DI style | **Default-parameter injection** for `dispatch*` / `getChannel` | -| 9 | Snapshots / type tests | `@std/testing/snapshot` / `expect-type` | -| 10 | Typecheck | Replace `tsc` with **`deno check`**, scoped to `apps/server` + `apps/workflows` (src + tests) | -| 11 | Permissions | `-A` for `deno test` and `deno compile` now; tighten the binary in a follow-up | -| 12 | Docker install | `deno install --prod --frozen --allow-scripts`, hoisted node_modules; `pnpm-workspace.yaml` stays authoritative; commit `deno.lock` only | -| 13 | CI install | CI test/migrate jobs **keep `pnpm install`**; `deno install` is exercised by the docker-publish build | -| 14 | CI test orchestration | Keep **turbo**; each package `"test": "deno test -A"`; swap `setup-bun` → `setup-deno` | -| 15 | Integration tests | Stay in the same `deno test -A` run, against the CI libsql service (as today) | -| 16 | Safety net | Record base-commit `bun test` pass count; assert `deno test` count ≥ it (fail CI on drop) | -| 17 | Other apps | Stay on pnpm install + existing build | -| 18 | Codemod | `ts-morph`/`jscodeshift` for the mechanical rewrite; hand-fix the ~60-file residue | - ---- - -## Resolved by codebase exploration (facts, not assumptions) - -- **Entrypoint must split** — tests do `import { app } from "@/index"`; a top-level - `Deno.serve` in `index.ts` would boot a server on every test import. -- **Server needs no libsql side-car** — `packages/db` uses `drizzle-orm/libsql/http` (pure - HTTP). Only `apps/workflows/src/lib/db.ts` uses the native `@libsql/client` embedded replica - → native-addon compile risk is **workflows-only**. -- **Tests are integration tests against a real libSQL DB** — CI runs a `libsql-server` - service + migrate + seed. The preload mocks only Tinybird/Redis/subscriptions; DB is real. -- **Step D seams are mixed**: `@/libs/clients` (`tb`/`redis` singletons) + `Redis.fromEnv` - are stub-able; `@openstatus/subscriptions` `dispatch*` + `getChannel` are destructured live - bindings called directly → need DI. -- **`node-fetch` mock is dead** — the SUT uses **global `fetch`**; tests set - `global.fetch = mockFetch` (the effective line) plus a redundant `mock.module("node-fetch")` - (not a dependency). → `stub(globalThis, "fetch", …)` and delete the `mock.module` line. -- **`start.sh` is unused by Fly** (entrypoint is the binary; compose `command` commented) — - prod migrations run via CI `migrate.yml`. We keep `start.sh` anyway and port its command. -- **migrate/seed have no `Bun.*`** — `packages/db/src/migrate.mts` + `seed.mts` use only - `@libsql/client`/drizzle/`process` → run under `deno run -A` unchanged. -- **`Bun.*` lives only in dev/build scripts** — `packages/db/env.ts`, `apps/server/env.ts`, - `apps/workflows/src/build-docker.ts` (`Bun.file`/`Bun.write`). -- **web/status-page tests are pure logic/util** — no DOM/`@testing-library`/React rendering; - run under `deno test` with no jsdom. -- **Only `apps/server` runs `tsc`** today (`tsc --noEmit`, `types: ["bun-types"]`). - ---- - -## Phase 0 — Spikes (gate the big-bang) — ✅ DONE (1,2) / deferred (3,4) - -Toolchain confirmed: **deno 2.9.0**, bun 1.3.11, pnpm 11.2.1, node 24, dofigen 2.8.0. - -1. ✅ **`Deno.serve` + Hono compile/serve.** Compiled, served, `/ping`→200, embedded asset - read via `Deno.readTextFile(new URL(...))`. **Correction: the flag is `--include`, not - `--include-as-is`** (the latter does not exist in 2.9). -2. ✅ **libSQL native addon under `deno compile` — make-or-break, PASSED with a bonus.** - `deno compile` **embeds the native addon into the binary** (10.7MB) and the binary runs from - a clean dir with **no node_modules**. **Consequence: the workflows `docker` + `libsql` - side-car stages are no longer needed** (they existed only because `bun build --compile` - could not embed native addons). One caveat for the real monorepo build: `--node-modules-dir=manual` - against the giant shared `node_modules` hangs (tries to embed everything) — use deno's - managed npm resolution / `--exclude-unused-npm` so only reachable packages embed. Final flag - combo to be settled in Phase 5 against the real Docker build. -3. ⏳ **Test stack interop** — validated at the start of Phase 2 by migrating one real route - test (faster than a synthetic spike): `@std/expect` matchers, `toHaveBeenCalled*` vs - `@std/testing/mock` spies (else `assertSpyCalls`), `expect.extend`, `stub` on `tb` getters + - `globalThis.fetch`, `FakeTime`. -4. ⏳ **`deno install` on the repo** — done carefully in Phase 3 (it may rewrite manifests and - re-layout `node_modules`, which would disrupt the in-progress pnpm-based work), in an - isolated copy first. - ---- - -## ✅ RESOLVED — deep workspace imports: exports maps + sloppy (no deno.json) - -**Solution (verified under `deno run` + `deno compile` + bun):** add an `exports` map to the -4 deep-imported packages (`@openstatus/db`, `@openstatus/api`, `@openstatus/emails`, -`@openstatus/tracker`) and pass `--unstable-sloppy-imports` on every deno command. The exports -map crosses the package boundary (sloppy can't); sloppy resolves the relative extensionless -imports inside packages. **No deno.json. 4 package.json touched, not 512 imports.** - -Exports map shape (per package), covering the 36 distinct targets (29 files, 7 dirs all in db): - -```jsonc -"exports": { - ".": "./src/index.ts", - "./env.mjs": "./env.mjs", // db only (non-src deep import) - "./src/schema": "./src/schema/index.ts", // + the 6 other directory targets - "./src/client": "./src/client.tsx", // .tsx files need explicit entries - "./src/*": "./src/*.ts" // wildcard handles the .ts files -} -``` - -Every deno invocation (`test`/`compile`/`check`/`run`) must include `--unstable-sloppy-imports`. - -### Original investigation (kept for context) - -The codebase has **512 bare deep-subpath imports across 400 files** of the form -`@openstatus/db/src/schema/constants` (also `@openstatus/api/src/*`, `@openstatus/emails/src/*`, -`@openstatus/tracker/src/*`). Targets are a **mix of `.ts` files, `.tsx` files, and directories** -(`@openstatus/db/src/schema` is a dir with `index.ts`). Verified facts (Deno 2.9.0 experiments): - -- **`--unstable-sloppy-imports` resolves *relative* extensionless imports** (incl. dir-index and - `.tsx`) for `deno run`/`compile`/`test`. ✅ -- It does **NOT** resolve **bare cross-package** deep subpaths — those go through node_modules - package resolution where sloppy does not apply. Fails for run/compile/test (works only for the - lenient `deno check` TS resolver). ❌ — true even in Deno workspace mode (`deno install`). -- **Explicit extension** (`@openstatus/db/src/schema/constants.ts`) resolves under both Deno and - bun — but directory imports still need `/index.ts`, and it risks the Next.js apps (turbopack/tsc). -- **`exports` map** with a `./src/*` wildcard handles `.ts` files but **not** the mixed - dir/`.tsx` cases reliably (Deno does not fall through exports fallback arrays on a missing file). -- **deno.json import map** prefix (`"@openstatus/db/src/": "./packages/db/src/"`) + sloppy - **resolves ALL cases** (file `.ts`/`.tsx` + dir-index) for run **and** compile. ✅ — but it is a - `deno.json`, which the user asked to avoid. - -**This is a hard fork:** the only clean, complete fix is a 4-entry `deno.json` import map, which -collides with the "no deno.json" preference ([[feedback-no-deno-json]]). Awaiting a decision (see -chat) before proceeding with Phase 2+. Everything below assumes that decision is made. - -## Implementation findings (verified during execution) - -- **`@/*` tsconfig path alias** — Deno doesn't read tsconfig `paths`. 153 `@/…` imports across - 113 files in `apps/server` rewritten to **relative paths** (codemod). workflows uses none. ✅ -- **`deno install` foundation** — works, with one workaround: it **crashes seeding `deno.lock` - from `pnpm-lock.yaml`** on a pnpm alias (`string-width-cjs@string-width@4.2.3`). Fix: move - `pnpm-lock.yaml` aside during `deno install` so Deno resolves fresh, then restore it. Deno - migrates the **catalog into root `package.json`** (not deno.json) and writes `deno.lock`. - After install, the full server graph resolves (deep imports + `zod` + `hono`). ✅ -- **`@std/expect` `toHaveBeenCalled*` does NOT accept `@std/testing/mock` spies** (verified: - "Received function must be a mock or spy function"). → the ~187 `toHaveBeenCalled*` sites must - use **`assertSpyCalls`/`assertSpyCall`** instead. Basic `expect`, `expect.extend` custom - matchers, and `FakeTime` all work. ⚠ (more test rework than planned) -- **✅ `deno compile` of `apps/server` WORKS in a clean Docker container** — the local 12-min - hang was the **polluted local node_modules** (Deno-managed mix + broken `.next` symlinks + - every app's deps). In Docker with a **scoped `pnpm install --filter @openstatus/server...`** - (20.8s) then `deno compile --node-modules-dir=manual`, the binary builds fast and **runs**: - `/ping`→200, `/openapi.yaml`→embedded asset, full app graph boots ("Listening on :3000"). - - **Winning build recipe** (`apps/server/Dockerfile.deno-test`): node:24-slim → `pnpm install - --prod --frozen-lockfile --filter @openstatus/server...` → denoland/deno:2.9.0 → `deno - compile -A --no-check --unstable-sloppy-imports --node-modules-dir=manual --include - static/openapi.yaml --include static/openapi-v1.json --output app src/serve.ts` → debian - runtime with just the binary. - - **No `deno install`** in the build — pnpm install (scoped) is what produces node_modules; - Deno reads the catalog from root `package.json` (migrated once) + consumes pnpm's layout. - This sidesteps `deno install`'s whole-monorepo install + the pnpm-alias lockfile crash. - - Catalog must be present in root `package.json` for Deno to resolve `catalog:` specifiers. - - Sandbox note: needed `docker build --network=host` here (flaky build-network); normal CI ok. - - Image 495MB (debian + binary). Could slim further later. -- **✅ `apps/workflows` also builds in Docker** (same recipe, `--filter @openstatus/workflows...`, - 465MB) and **embeds the libSQL native addon** (no side-car). Verified the binary executes (it - reaches the embedded-replica `createClient` and opens the sync connection). Full boot needs a - real Turso `DATABASE_URL` — in the sandbox it blocks in SYN_SENT to an unreachable sync URL at - module load (the `createClient` runs before logging, so no logs appear). Not a build defect. -- **⚠ `@sentry/bun` → `@sentry/deno`** — workflows used `@sentry/bun` (bun-only; hangs under - Deno) in `index.ts`, `lib/sentry.ts`, `cron/index.ts`. Swapped all three to `@sentry/deno` - (`10.61.0`, API-compatible: `init`/`captureException`/`captureMessage`/`captureCheckIn`/`flush`), - updated `package.json` + `pnpm-lock.yaml`. The server uses `@hono/sentry` (unaffected). -- **⚠ embedded-replica `createClient` blocks at module load** until the sync URL connects — so - the workflows process won't serve `/ping` until the DB is reachable. Pre-existing behavior - (bun did the same); flagged for prod startup robustness. - -## Phase 2 — Test migration — 🔶 IN PROGRESS - -- ✅ `@openstatus/test-utils` package built + validated: re-exports `node:test` runner with a - Bun-compat **`.each`** shim, `@std/expect` `expect` extended with the ~12 **bun-only matchers** - (`toBeNumber`, `toContainValue`, …), and `@std/testing/mock` (`spy`/`stub`/`assertSpyCalls`) + - `@std/testing/time` (`FakeTime`). Uses `jsr:@std/*` so only Deno needs them. -- ✅ Codemod swapped `bun:test` → `@openstatus/test-utils` in **194 files**; updated 21+ - `package.json` (`deno test` script, test-utils devDep, dropped `@types/bun`/`bun-types`); - removed `bunfig.toml`. -- ✅ **`@/*` alias → relative** rewrite: server (153), status-page (211), web (332). Deno - ignores tsconfig paths. -- ✅ **Import-only suites PASS under `deno test`**: header-analysis (7), status-page (51), web - (23). Mock pattern validated: discord + 5 more notifications PASS. -- 🔶 **Remaining mock work (~47 files)**: `@std/expect` `toHaveBeenCalled*` does NOT accept std - spies → use `assertSpyCalls`. Per-file quirks remain: 6 notifications (webhook double-stub, - bird-whatsapp `jest`, slack/grafana call-count-after-restub), importers clients, status-fetcher, - tracker (`FakeTime`), subscriptions/api/server (**`mock.module` → stub + default-param DI**, - and these are **DB-integration tests needing a running libSQL to verify**). -- ⏳ snapshots (`toMatchSnapshot` → `@std/testing/snapshot`) + type tests (`expectTypeOf` → - `expect-type`) not started. - -## Phase 1 — Source changes — ✅ DONE - -- ✅ split `index.ts`/`serve.ts` (both apps); server static asset → `Deno.readTextFile`. -- ✅ `Bun.*` → `Deno.copyFile` in `apps/server/env.ts`, `packages/db/env.ts`. -- ✅ `migrate`/`seed`/`env` scripts + `start.sh` → `deno run -A`; app `dev`/`start`/`test` → deno. - -(Detail of the changes below.) - -### Entrypoint split (both apps) - -`index.ts`: keep `export const app = new Hono(...)`, **remove** `const server = {...}` + -`export default server`. New `serve.ts`: - -```ts -import { app } from "./index"; -import { env } from "./env"; -Deno.serve({ port: env.PORT ?? 3000 }, app.fetch); -``` - -`serve.ts` is the `deno compile` target. Tests still import `app` with no server side-effect. - -### Server static asset (`apps/server/src/index.ts:22`) - -Drop `import openapiYaml … with { type: "text" }`. Embed via `--include-as-is` (Phase 4) and -read at startup: - -```ts -const openapiYaml = await Deno.readTextFile(new URL("../static/openapi.yaml", import.meta.url)); -``` - -JSON imports (`with { type: "json" }`) stay — Deno supports them. - -### `Bun.*` → Deno APIs (dev/build scripts) - -`packages/db/env.ts`, `apps/server/env.ts`: `Bun.file(p).text()/.json()` → -`Deno.readTextFile(p)` (+ `JSON.parse`); `Bun.write(p, s)` → `Deno.writeTextFile(p, s)`. -(`apps/workflows/src/build-docker.ts` is deleted in Phase 5 — no port needed.) - -### migrate / `start.sh` - -`packages/db/package.json`: `"migrate": "deno run -A src/migrate.mts"`, -`"seed": "deno run -A src/seed.mts"`, `"env": "deno run -A env.ts"`. Keep `start.sh`; change -`bun src/migrate.mts` → `deno run -A src/migrate.mts`. - ---- - -## Phase 2 — Test migration (`bun:test` → `node:test` + `@std/expect`, `deno test`) - -Surface: **~270 files**, ~2920 `expect` assertions, ~150 bun-only matchers, 47 `mock` / -21 `spyOn` / 10 `setSystemTime` / 6 `jest.*`, 4 `mock.module` seams (+ dead `node-fetch`), -12 snapshot files, 10 type-test files. - -### Step 0 — `@openstatus/test-utils` package (Decision 7) - -``` -packages/test-utils/ - src/setup.ts # registers expect.extend matchers; installs the global fetch stub helper - src/matchers.ts # the ~150 bun-only matchers (toBeNumber, toContainValue, toBeString, …) - src/fakes/ # tinybird.ts, redis.ts, subscriptions.ts — shared test doubles -``` - -Each tester adds `@openstatus/test-utils` as a devDependency; every test file imports -`@openstatus/test-utils/setup` **first** (codemod injects). - -### Step A — codemod (`ts-morph`/`jscodeshift`, ~270 files) - -Per file: replace the `bun:test` import with `node:test` + `@std/expect`, alias -`beforeAll→before`/`afterAll→after`, and inject `import "@openstatus/test-utils/setup";` as -the first import. `expect(...)` bodies untouched. - -### Step B — bun-only matchers - -Implemented once in `@openstatus/test-utils` via `expect.extend` (verified in Phase 0 spike 3). -`pass()`/`fail()` helpers → explicit `expect(...)` assertions in the few sites that use them. - -### Step C — mocks (`@std/testing/mock` + `@std/testing/time`) - -`mock(fn)`→`spy(fn)`; `spyOn(o,"m")`→`using s = spy(o,"m")` / `stub(o,"m",impl)`; sequential -returns→`returnsNext`; `setSystemTime`→`using t = new FakeTime(date)` + `t.tick`. Prefer -`using` (auto-restore) — deletes most `afterEach` teardown. Keep `toHaveBeenCalled*` if Phase-0 -interop holds, else `assertSpyCalls`/`assertSpyCall`. - -### Step D — module mocks (hybrid; Decisions 6 + 8) - -- **stub-able** → `@std/testing/mock` `stub`, fakes from `@openstatus/test-utils/fakes`: - `tb`/`redis` (`@/libs/clients` singletons), `Redis.fromEnv` (`@openstatus/upstash`). -- **global fetch** → `stub(globalThis, "fetch", …)`; **delete** every `mock.module("node-fetch")`. -- **DI (default-param)** → `dispatchMaintenanceUpdate`, `dispatchStatusReportUpdate` - (`maintenances/post.ts`, `statusReports/post.ts`), `getChannel` - (`rpc/handlers/status-page/index.ts`): add `deps = { … }` last param; tests pass fakes. -- The old global `preload.ts` is deleted; its four mocks move to the above mechanisms. - -### Step E — preload replacement - -`bunfig.toml` preload is gone; the `@openstatus/test-utils/setup` import (Step A) replaces it. -Import ordering: setup first, then the SUT. - -### Step F — snapshots + type tests - -Snapshots → `@std/testing/snapshot` `assertSnapshot(t, value)` (regenerate + diff the 12 -files). Type tests → `expect-type` (`expectTypeOf().toEqualTypeOf()`), validated by -`deno check` where in scope (the two apps). - -### Step G — invocation - -Each package/app `"test": "deno test -A"`. Pull `@std/expect`, `@std/testing`, `expect-type` -via `jsr add`/devDeps so pnpm install resolves them. Remove `bunfig.toml`, `bun-types`, -`@types/bun`. - ---- - -## Phase 3 — Install via `deno install` (Docker, server + workflows) - -Deno 2.9 reads `pnpm-workspace.yaml` catalogs (482 `catalog:` refs) + workspace globs -(244 `workspace:*`) and seeds `deno.lock` from `pnpm-lock.yaml`. dofigen `install` stage: - -```yaml - install: - fromImage: denoland/deno:2.9.0 - workdir: /app/ - env: - DENO_NODE_MODULES_DIR: auto # hoisted layout for compile + libsql side-car - bind: - - pnpm-workspace.yaml # catalog + workspace source of truth - - pnpm-lock.yaml - - deno.lock - - package.json - - …all workspace package.json (same bind list as today)… - run: deno install --prod --frozen --allow-scripts - cache: - - /deno-dir -``` - -`pnpm-workspace.yaml` stays authoritative (Decision 12); commit `deno.lock`; do **not** commit -any catalog/`deno.json` that 2.9 may emit. Verify (Phase 0 spike 4) the migration write stays -additive so pnpm keeps working for the other apps. - ---- - -## Phase 4 — `apps/server/dofigen.yml` (build) - -`build` stage → Deno + `deno compile` (target `serve.ts`). No libsql side-car (HTTP driver). - -```yaml - build: - fromImage: denoland/deno:2.9.0 - workdir: /app/apps/server - env: { NODE_ENV: production } - copy: - - . /app/ - - { fromBuilder: install, source: /app/node_modules, target: /app/node_modules } - - { fromBuilder: install, source: /app/apps/server/node_modules, target: /app/apps/server/node_modules } - - { fromBuilder: install, source: /app/packages, target: /app/packages } - run: >- - deno compile -A --no-check --node-modules-dir - --include static/openapi.yaml --include static/openapi-v1.json - --output app serve.ts -``` - -`--no-check` keeps compile fast (`deno check` is the typecheck gate, Decision 10). Optionally -benchmark experimental `--bundle` for a smaller binary. Runtime copy `/app/apps/server/app` → -`/bin/app` unchanged. - ---- - -## Phase 5 — `apps/workflows/dofigen.yml` (build) — side-car DROPPED - -Spike 2 proved `deno compile` **embeds** the libSQL native addon into the binary, so the whole -side-car apparatus goes away: - -1. **`build`** → `deno compile -A --no-check --output app serve.ts`. Settle the npm-embed flags - here (`--exclude-unused-npm` / managed npm) so it embeds reachable packages, not the whole - `node_modules` (spike 2 caveat). -2. **DELETE the `docker` stage** (`build-docker.ts` existed only to generate the side-car - `package.json`) and **DELETE the `libsql` stage**. -3. **`build-docker.ts`** itself becomes dead code → remove (its sole purpose was the side-car). -4. **runtime copy** → just the self-contained `app` binary. No `node_modules` copied at all. - -This makes the workflows runtime image dramatically simpler (binary only, like server). - ---- - -## Phase 6 — CI - -- `test.yml` + `migrate.yml`: replace `oven-sh/setup-bun` with `denoland/setup-deno@v2` - (Deno 2.9). **Keep `pnpm install`** (Decision 13), the libsql service, migrate, seed. - `pnpm test` → turbo → per-package `deno test -A` (Decision 14). -- Add a **`deno check`** step for `apps/server` + `apps/workflows` (src + tests) replacing the - server `tsc` step (Decision 10). -- **Parity gate** (Decision 16): a CI step records the base-commit `bun test` total pass count - (artifact / committed baseline number), then asserts `deno test`'s pass count ≥ baseline. -- docker-publish workflows exercise `deno install` + `deno compile` via the Dockerfiles. - ---- - -## Phase 7 — Verify & ship - -Per app: `dofigen generate`; `docker buildx build -f apps//Dockerfile .` from repo root; -run, `GET /ping` → 200; server `GET /openapi.yaml` → body; workflows embedded replica syncs + -a cron endpoint responds. Locally: `pnpm install && pnpm migrate && pnpm seed && pnpm test` -green; parity count ≥ baseline; `deno check` clean for the two apps. Watch Fly RSS/OOM on -workflows after deploy (Deno V8 heap defaults differ from bun — see `research.md`). - ---- - -## Risks - -| Risk | Mitigation | -|------|-----------| -| ~~libSQL native addon won't `deno compile`~~ | ✅ RESOLVED (spike 2): deno embeds the addon; side-car dropped | -| Big-bang PR hides a regression / silent skip | Parity gate (16); split into logical commits within the one PR for bisect | -| `@std/expect` `toHaveBeenCalled*` ✗ interop with `@std/testing/mock` spies | Phase-0 spike 3; fallback `assertSpyCalls`/`assertSpyCall` | -| `deno install` rewrites manifests / breaks pnpm for other apps | Phase-0 spike 4; keep `pnpm-workspace.yaml` authoritative, don't commit deno catalog | -| `deno check` surfaces new diagnostics vs `tsc` | Scoped to two apps (10); fix as part of the PR | -| ~270-file codemod churn | AST codemod (18) + parity gate; hand-fix the ~60-file residue | -| `stub` can't reach `tb` getter methods | Phase-0 spike 3; fallback = make `tb`/`redis` injectable like the dispatch seams | - ---- - -## TODO - -- [ ] **Phase 0** spikes: serve+compile · libsql-compile (workflows) · test-stack interop · `deno install` -- [ ] Phase 1: split `index.ts`/`serve.ts` (both) · server static asset · `Bun.*`→Deno · migrate/seed/`start.sh`→deno -- [ ] Phase 2.0: create `@openstatus/test-utils` (matchers, setup, fakes) -- [ ] Phase 2.A: codemod imports + lifecycle + setup-inject (~270 files) -- [ ] Phase 2.B–C: matchers via `expect.extend`; mocks → `@std/testing/mock`/`time` -- [ ] Phase 2.D: hybrid module mocks — stub `tb`/`redis`/`Redis.fromEnv` + `globalThis.fetch`; DI `dispatch*`/`getChannel`; delete `node-fetch` mocks + `preload.ts` -- [ ] Phase 2.F: snapshots → `@std/testing/snapshot`; type tests → `expect-type` -- [ ] Phase 2.G: `"test": "deno test -A"` everywhere; add JSR devDeps; remove `bunfig.toml`/`bun-types` -- [ ] Phase 3: `deno install` stage (both dofigen); commit `deno.lock` -- [ ] Phase 4: `apps/server` build → `deno compile` (serve.ts, `--include-as-is`) -- [ ] Phase 5: `apps/workflows` build → `deno compile`; `build-docker.ts`→deno; `libsql` stage→npm -- [ ] Phase 6: CI `setup-deno`, keep `pnpm install`, `deno check` (2 apps), parity gate -- [ ] Phase 7: `dofigen generate` + docker build + verify each app; ship; watch Fly RSS -- [ ] Update CLAUDE.md memory note "use bun test directly" → `deno test` - diff --git a/deno.lock b/deno.lock deleted file mode 100644 index a0899fd5e..000000000 --- a/deno.lock +++ /dev/null @@ -1,330 +0,0 @@ -{ - "version": "5", - "specifiers": { - "jsr:@std/assert@^1.0.19": "1.0.19", - "jsr:@std/async@^1.4.0": "1.4.0", - "jsr:@std/data-structures@^1.1.0": "1.1.0", - "jsr:@std/expect@*": "1.0.19", - "jsr:@std/internal@^1.0.12": "1.0.14", - "jsr:@std/internal@^1.0.13": "1.0.14", - "jsr:@std/path@^1.1.4": "1.1.4", - "jsr:@std/testing@*": "1.0.19", - "jsr:@std/yaml@^1.0.9": "1.2.0" - }, - "jsr": { - "@std/assert@1.0.19": { - "integrity": "eaada96ee120cb980bc47e040f82814d786fe8162ecc53c91d8df60b8755991e", - "dependencies": [ - "jsr:@std/internal@^1.0.12" - ] - }, - "@std/async@1.4.0": { - "integrity": "4d70b008634f571cff9b554090d628c76141c32613aae0ff283fd5fa23d0c379" - }, - "@std/data-structures@1.1.0": { - "integrity": "c35ae4ad5d8e41a38573c2fe3e19b18ea2505f63cfea201edcb8720aca1f7f58" - }, - "@std/expect@1.0.19": { - "integrity": "25271785688c7ff902fa54875d6aa4001f552c5578f7f0fefb5b5aac1fc2ed8a", - "dependencies": [ - "jsr:@std/assert", - "jsr:@std/internal@^1.0.13", - "jsr:@std/path" - ] - }, - "@std/internal@1.0.14": { - "integrity": "291516b3d4c35024d6ffbc0a9df5bf4c64116e05b50012cf846710152d2ffdf7" - }, - "@std/path@1.1.4": { - "integrity": "1d2d43f39efb1b42f0b1882a25486647cb851481862dc7313390b2bb044314b5", - "dependencies": [ - "jsr:@std/internal@^1.0.12" - ] - }, - "@std/testing@1.0.19": { - "integrity": "f4236172365b216728dc3cc8b5e80a9f4c33083d1e4ede7613d5b25b4014898e", - "dependencies": [ - "jsr:@std/assert", - "jsr:@std/async", - "jsr:@std/data-structures" - ] - }, - "@std/yaml@1.2.0": { - "integrity": "20beb41e4983ba3437dbefac62b14061ab058e8a187596f19d28ff9035f6e6cf" - } - }, - "workspace": { - "packageJson": { - "dependencies": [ - "npm:turbo@2.11.2" - ] - }, - "members": { - "apps/dashboard": { - "packageJson": { - "dependencies": [ - "npm:@jsr/std__expect@^1.0.19", - "npm:@jsr/std__testing@^1.0.19" - ] - } - }, - "apps/server": { - "packageJson": { - "dependencies": [ - "npm:@jsr/std__expect@^1.0.19", - "npm:@jsr/std__testing@^1.0.19", - "npm:@jsr/std__yaml@^1.0.9" - ] - } - }, - "apps/status-page": { - "packageJson": { - "dependencies": [ - "npm:@jsr/std__expect@^1.0.19", - "npm:@jsr/std__testing@^1.0.19" - ] - } - }, - "apps/web": { - "packageJson": { - "dependencies": [ - "npm:@jsr/std__expect@^1.0.19", - "npm:@jsr/std__testing@^1.0.19" - ] - } - }, - "packages/ai": { - "packageJson": { - "dependencies": [ - "npm:@jsr/std__expect@^1.0.19", - "npm:@jsr/std__testing@^1.0.19" - ] - } - }, - "packages/api": { - "packageJson": { - "dependencies": [ - "npm:@jsr/std__expect@^1.0.19", - "npm:@jsr/std__testing@^1.0.19" - ] - } - }, - "packages/assertions": { - "packageJson": { - "dependencies": [ - "npm:@jsr/std__expect@^1.0.19", - "npm:@jsr/std__testing@^1.0.19" - ] - } - }, - "packages/db": { - "packageJson": { - "dependencies": [ - "npm:@jsr/std__expect@^1.0.19", - "npm:@jsr/std__testing@^1.0.19" - ] - } - }, - "packages/emails": { - "packageJson": { - "dependencies": [ - "npm:@jsr/std__expect@^1.0.19", - "npm:@jsr/std__testing@^1.0.19" - ] - } - }, - "packages/header-analysis": { - "packageJson": { - "dependencies": [ - "npm:@jsr/std__expect@^1.0.19", - "npm:@jsr/std__testing@^1.0.19" - ] - } - }, - "packages/importers": { - "packageJson": { - "dependencies": [ - "npm:@jsr/std__expect@^1.0.19", - "npm:@jsr/std__testing@^1.0.19" - ] - } - }, - "packages/notifications/base": { - "packageJson": { - "dependencies": [ - "npm:@jsr/std__expect@^1.0.19", - "npm:@jsr/std__testing@^1.0.19" - ] - } - }, - "packages/notifications/bird-whatsapp": { - "packageJson": { - "dependencies": [ - "npm:@jsr/std__expect@^1.0.19", - "npm:@jsr/std__testing@^1.0.19" - ] - } - }, - "packages/notifications/discord": { - "packageJson": { - "dependencies": [ - "npm:@jsr/std__expect@^1.0.19", - "npm:@jsr/std__testing@^1.0.19" - ] - } - }, - "packages/notifications/google-chat": { - "packageJson": { - "dependencies": [ - "npm:@jsr/std__expect@^1.0.19", - "npm:@jsr/std__testing@^1.0.19" - ] - } - }, - "packages/notifications/grafana-oncall": { - "packageJson": { - "dependencies": [ - "npm:@jsr/std__expect@^1.0.19", - "npm:@jsr/std__testing@^1.0.19" - ] - } - }, - "packages/notifications/ms-teams": { - "packageJson": { - "dependencies": [ - "npm:@jsr/std__expect@^1.0.19", - "npm:@jsr/std__testing@^1.0.19" - ] - } - }, - "packages/notifications/ntfy": { - "packageJson": { - "dependencies": [ - "npm:@jsr/std__expect@^1.0.19", - "npm:@jsr/std__testing@^1.0.19" - ] - } - }, - "packages/notifications/opsgenie": { - "packageJson": { - "dependencies": [ - "npm:@jsr/std__expect@^1.0.19", - "npm:@jsr/std__testing@^1.0.19" - ] - } - }, - "packages/notifications/pagerduty": { - "packageJson": { - "dependencies": [ - "npm:@jsr/std__expect@^1.0.19", - "npm:@jsr/std__testing@^1.0.19" - ] - } - }, - "packages/notifications/slack": { - "packageJson": { - "dependencies": [ - "npm:@jsr/std__expect@^1.0.19", - "npm:@jsr/std__testing@^1.0.19" - ] - } - }, - "packages/notifications/telegram": { - "packageJson": { - "dependencies": [ - "npm:@jsr/std__expect@^1.0.19", - "npm:@jsr/std__testing@^1.0.19" - ] - } - }, - "packages/notifications/twillio-sms": { - "packageJson": { - "dependencies": [ - "npm:@jsr/std__expect@^1.0.19", - "npm:@jsr/std__testing@^1.0.19" - ] - } - }, - "packages/notifications/webhook": { - "packageJson": { - "dependencies": [ - "npm:@jsr/std__expect@^1.0.19", - "npm:@jsr/std__testing@^1.0.19" - ] - } - }, - "packages/services": { - "packageJson": { - "dependencies": [ - "npm:@jsr/std__expect@^1.0.19", - "npm:@jsr/std__testing@^1.0.19" - ] - } - }, - "packages/status-fetcher": { - "packageJson": { - "dependencies": [ - "npm:@jsr/std__expect@^1.0.19", - "npm:@jsr/std__testing@^1.0.19" - ] - } - }, - "packages/subscriptions": { - "packageJson": { - "dependencies": [ - "npm:@jsr/std__expect@^1.0.19", - "npm:@jsr/std__testing@^1.0.19" - ] - } - }, - "packages/test-utils": { - "packageJson": { - "dependencies": [ - "npm:@jsr/std__expect@^1.0.19", - "npm:@jsr/std__testing@^1.0.19" - ] - } - }, - "packages/theme-store": { - "packageJson": { - "dependencies": [ - "npm:@jsr/std__expect@^1.0.19", - "npm:@jsr/std__testing@^1.0.19" - ] - } - }, - "packages/tinybird": { - "packageJson": { - "dependencies": [ - "npm:@jsr/std__expect@^1.0.19", - "npm:@jsr/std__testing@^1.0.19" - ] - } - }, - "packages/tracker": { - "packageJson": { - "dependencies": [ - "npm:@jsr/std__expect@^1.0.19", - "npm:@jsr/std__testing@^1.0.19" - ] - } - }, - "packages/upstash": { - "packageJson": { - "dependencies": [ - "npm:@jsr/std__expect@^1.0.19", - "npm:@jsr/std__testing@^1.0.19" - ] - } - }, - "packages/utils": { - "packageJson": { - "dependencies": [ - "npm:@jsr/std__expect@^1.0.19", - "npm:@jsr/std__testing@^1.0.19" - ] - } - } - } - } -} diff --git a/devbox.json b/devbox.json index 78ea1f227..841fffa6c 100644 --- a/devbox.json +++ b/devbox.json @@ -3,7 +3,6 @@ "packages": [ "turso-cli@1.0.15", "nodejs@26.8.1", - "deno@2.9.5", "bun@1.3.13", "sqld@0.24.32" ], diff --git a/devbox.lock b/devbox.lock index 3c276e53d..0a109169a 100644 --- a/devbox.lock +++ b/devbox.lock @@ -49,68 +49,6 @@ } } }, - "deno@2.9.5": { - "last_modified": "2026-08-27T07:16:00Z", - "resolved": "github:NixOS/nixpkgs/c27cdad491a991b11ed731760aa2ef8db0cb0410#deno", - "source": "devbox-search", - "version": "2.9.5", - "systems": { - "aarch64-darwin": { - "outputs": [ - { - "name": "out", - "path": "/nix/store/94pdxnq7cfdymzmr19144spl0rp34mjg-deno-2.9.5", - "default": true - }, - { - "name": "denort", - "path": "/nix/store/ci5jh3b5yr2dz36q8qb9qs4za2nkkgz3-deno-2.9.5-denort" - }, - { - "name": "libdenort", - "path": "/nix/store/lcxc1mm8hz5i9jhdpv0cb233nxf9r64g-deno-2.9.5-libdenort" - } - ], - "store_path": "/nix/store/94pdxnq7cfdymzmr19144spl0rp34mjg-deno-2.9.5" - }, - "aarch64-linux": { - "outputs": [ - { - "name": "out", - "path": "/nix/store/8rzh2fpa3lwqzx9iiz07qbk3b7lsjniw-deno-2.9.5", - "default": true - }, - { - "name": "denort", - "path": "/nix/store/2h16nirjr5ddd2bz7vxd19q0lawl5cd3-deno-2.9.5-denort" - }, - { - "name": "libdenort", - "path": "/nix/store/8hn63ayil0mk81a7mim21qh4155n3r6j-deno-2.9.5-libdenort" - } - ], - "store_path": "/nix/store/8rzh2fpa3lwqzx9iiz07qbk3b7lsjniw-deno-2.9.5" - }, - "x86_64-linux": { - "outputs": [ - { - "name": "out", - "path": "/nix/store/7hk2ax3hgs4152cdqsqcq4lfq1a2kla6-deno-2.9.5", - "default": true - }, - { - "name": "libdenort", - "path": "/nix/store/3m3rdq6vl231165fycmk5rjc48anrzxi-deno-2.9.5-libdenort" - }, - { - "name": "denort", - "path": "/nix/store/180ds5dqaynm1k7c1j6s5lwlak943z9k-deno-2.9.5-denort" - } - ], - "store_path": "/nix/store/7hk2ax3hgs4152cdqsqcq4lfq1a2kla6-deno-2.9.5" - } - } - }, "github:NixOS/nixpkgs/nixpkgs-unstable": { "last_modified": "2025-11-05T16:44:39Z", "resolved": "github:NixOS/nixpkgs/ffcdcf99d65c61956d882df249a9be53e5902ea5?lastModified=1762361079&narHash=sha256-lz718rr1BDpZBYk7%2BG8cE6wee3PiBUpn8aomG%2FvLLiY%3D" diff --git a/oxlint.config.ts b/oxlint.config.ts index 03fc287cc..c7d9adc1a 100644 --- a/oxlint.config.ts +++ b/oxlint.config.ts @@ -126,7 +126,7 @@ export default defineConfig({ excludeFiles: ["**/__tests__/**", "**/*.test.ts"], rules: { "openstatus/services-mutation-guards": "error", - // apps/workflows runs this on Deno, and it stays Edge-safe by design. + // The package stays Edge-safe by design. "no-restricted-imports": [ "error", { diff --git a/packages/analytics/env.ts b/packages/analytics/env.ts index 4611ed752..15132ee96 100644 --- a/packages/analytics/env.ts +++ b/packages/analytics/env.ts @@ -3,11 +3,9 @@ import { z } from "zod"; /** * `NODE_ENV` is read through this alias rather than as a literal - * `process.env.NODE_ENV` member access: `deno bundle` (esbuild) constant-folds - * that exact expression into a string at build time — `"development"` unless - * `--minify` is passed — which left `setupAnalytics` permanently no-op inside - * the compiled `apps/server` binary. The other keys stay as literal member - * accesses because Next.js needs them that way to inline `NEXT_PUBLIC_*`. + * `process.env.NODE_ENV` member access, which some bundlers fold into a + * build-time string. The other keys stay as literal member accesses because + * Next.js needs them that way to inline `NEXT_PUBLIC_*`. */ const processEnv: Record = process.env; diff --git a/packages/emails/src/env.ts b/packages/emails/src/env.ts index d378e5e88..6c4942ac3 100644 --- a/packages/emails/src/env.ts +++ b/packages/emails/src/env.ts @@ -3,11 +3,8 @@ import { z } from "zod"; /** * `NODE_ENV` is read through this alias rather than as a literal - * `process.env.NODE_ENV` member access: `deno bundle` (esbuild) constant-folds - * that exact expression into a string at build time — `"development"` unless - * `--minify` is passed — which silently disabled every production guard in - * this package inside the compiled `apps/server` binary. Aliasing the object - * defeats the fold, so the value is read from the real environment at runtime. + * `process.env.NODE_ENV` member access, which some bundlers fold into a + * build-time string; the alias keeps it a runtime value. */ const processEnv: Record = process.env; diff --git a/packages/services/src/env.ts b/packages/services/src/env.ts index 201dc8061..a57868cfb 100644 --- a/packages/services/src/env.ts +++ b/packages/services/src/env.ts @@ -1,11 +1,7 @@ /** - * `NODE_ENV`, read so that bundlers cannot fold it away. - * - * `deno bundle` (esbuild) rewrites the exact expression `process.env.NODE_ENV` - * to a string literal at build time — `"development"` unless `--minify` is - * passed, and `apps/server` bundles without it (see `apps/server/dofigen.yml`). - * Reading through an aliased object defeats that substitution, so the value - * comes from the real environment at runtime. + * `NODE_ENV`, read so that bundlers cannot fold it away: some replace the + * exact expression `process.env.NODE_ENV` with a build-time literal. Reading + * through an aliased object keeps it a runtime value. */ export const env = { get NODE_ENV(): string { diff --git a/packages/services/src/oauth/crypto.ts b/packages/services/src/oauth/crypto.ts index e098e0cf5..787f43a37 100644 --- a/packages/services/src/oauth/crypto.ts +++ b/packages/services/src/oauth/crypto.ts @@ -1,4 +1,4 @@ -// Web Crypto only: this package runs on Deno and stays Edge-safe. +// Web Crypto only: this package stays Edge-safe. const encoder = new TextEncoder(); diff --git a/packages/test-utils/src/vitest.ts b/packages/test-utils/src/vitest.ts index c0a0f1611..c8793ce86 100644 --- a/packages/test-utils/src/vitest.ts +++ b/packages/test-utils/src/vitest.ts @@ -6,7 +6,7 @@ import type { Plugin } from "vite"; import { defineConfig } from "vitest/config"; type Options = { - /** Test-double swaps, in Deno import-map format, relative to the package. */ + /** Test-double swaps, in import-map format, relative to the package. */ importMap?: string; /** Loaded into `process.env` for every test file, relative to the package. */ envFile?: string; @@ -14,7 +14,7 @@ type Options = { }; /** - * Applies a Deno-style import map: exact bare keys, `/`-suffixed prefix keys, + * Applies an import map: exact bare keys, `/`-suffixed prefix keys, * and relative keys matched against the importer-resolved path without * extension probing — so `./page-urls` is swapped but `./page-urls.ts` is not. */ diff --git a/packages/tinybird/src/env.ts b/packages/tinybird/src/env.ts index 201dc8061..a57868cfb 100644 --- a/packages/tinybird/src/env.ts +++ b/packages/tinybird/src/env.ts @@ -1,11 +1,7 @@ /** - * `NODE_ENV`, read so that bundlers cannot fold it away. - * - * `deno bundle` (esbuild) rewrites the exact expression `process.env.NODE_ENV` - * to a string literal at build time — `"development"` unless `--minify` is - * passed, and `apps/server` bundles without it (see `apps/server/dofigen.yml`). - * Reading through an aliased object defeats that substitution, so the value - * comes from the real environment at runtime. + * `NODE_ENV`, read so that bundlers cannot fold it away: some replace the + * exact expression `process.env.NODE_ENV` with a build-time literal. Reading + * through an aliased object keeps it a runtime value. */ export const env = { get NODE_ENV(): string { -- 2.54.0 From 0ec1d187775f23d02a108b7c0fc4d799160ce4c3 Mon Sep 17 00:00:00 2001 From: Thibault Le Ouay Date: Tue, 20 Oct 2026 10:36:43 +0200 Subject: [PATCH 4/6] bugs: minor fix (#2942) --- packages/api/src/router/statusPage.ts | 3 +- packages/api/src/router/stripe/shared.ts | 29 +++++++++----------- packages/api/src/service/telegram-updates.ts | 16 +++++++++++ packages/services/src/page/update.ts | 3 +- 4 files changed, 33 insertions(+), 18 deletions(-) diff --git a/packages/api/src/router/statusPage.ts b/packages/api/src/router/statusPage.ts index 03988ac11..684ae756f 100644 --- a/packages/api/src/router/statusPage.ts +++ b/packages/api/src/router/statusPage.ts @@ -1541,10 +1541,11 @@ export const statusPageRouter = createTRPCRouter({ }); } + // Public procedure: never return the raw row (password, IP allow-list). return { email: opts.input.email, slug: opts.input.slug, - page: _page, + page: { title: _page.title }, }; }), diff --git a/packages/api/src/router/stripe/shared.ts b/packages/api/src/router/stripe/shared.ts index 0b1341463..cffade88e 100644 --- a/packages/api/src/router/stripe/shared.ts +++ b/packages/api/src/router/stripe/shared.ts @@ -159,24 +159,21 @@ export async function cancelSupersededSubscriptions( export async function cancelSubscription(customer?: string) { if (!customer) return; - try { - const { current } = await getCurrentSubscription(customer); - if (!current) return; - - if (current.status === "trialing") { - return await stripe.subscriptions.cancel(current.id); - } + // Throws on Stripe failure so the caller never deletes an account whose + // subscription is still billing. + const { current } = await getCurrentSubscription(customer); + if (!current) return; - return await stripe.subscriptions.update(current.id, { - cancel_at_period_end: true, - cancellation_details: { - comment: "Customer deleted their OpenStatus project.", - }, - }); - } catch (error) { - console.log("Error cancelling Stripe subscription", error); - return; + if (current.status === "trialing") { + return await stripe.subscriptions.cancel(current.id); } + + return await stripe.subscriptions.update(current.id, { + cancel_at_period_end: true, + cancellation_details: { + comment: "Customer deleted their OpenStatus project.", + }, + }); } export function customerIdOf(subscription: Stripe.Subscription) { diff --git a/packages/api/src/service/telegram-updates.ts b/packages/api/src/service/telegram-updates.ts index d48e402d6..235707715 100644 --- a/packages/api/src/service/telegram-updates.ts +++ b/packages/api/src/service/telegram-updates.ts @@ -91,6 +91,9 @@ export type ValidUpdate = user: { id: number; first_name: string; username?: string }; }; +// Matches the phase-1 token expiry in `createTelegramToken`. +const TELEGRAM_CHAT_BINDING_TTL_SECONDS = 1800; + // ---- Helpers ---------------------------------------------------------------- function extractPrivateChatStart( @@ -221,6 +224,8 @@ export async function processTelegramUpdates(args: { }) : updates; + const chatKey = `telegram:workspace_chat:${workspaceId}`; + // 2. Phase 1: private /start (no privateChatId filter) if (!privateChatId) { const tokenKey = `telegram:workspace_token:${workspaceId}`; @@ -232,6 +237,9 @@ export async function processTelegramUpdates(args: { if (result) { // Single-use token: delete and stop await redisClient.del(tokenKey); + await redisClient.set(chatKey, result.chatId, { + ex: TELEGRAM_CHAT_BINDING_TTL_SECONDS, + }); validUpdates.push({ chatType: "private", ...result }); break; } @@ -240,6 +248,14 @@ export async function processTelegramUpdates(args: { } // 3. Phase 2: group/supergroup additions else { + // Only the chat that completed phase 1 for this workspace may be used; + // otherwise any caller could read another user's group additions. + // String(): Upstash deserializes numeric strings into numbers. + const boundChatId = await redisClient.get(chatKey); + if (boundChatId == null || String(boundChatId) !== privateChatId) { + return validUpdates; + } + for (const update of recentUpdates) { const result = extractGroupBotAddition( update, diff --git a/packages/services/src/page/update.ts b/packages/services/src/page/update.ts index e0d90a615..3e097d5ff 100644 --- a/packages/services/src/page/update.ts +++ b/packages/services/src/page/update.ts @@ -155,7 +155,8 @@ export async function updatePagePasswordProtection(args: { // non-empty joins (e.g. `"a@b.com"` / `"10.0.0.0/24"`) pass // through unchanged. authEmailDomains: input.authEmailDomains?.join(",") || null, - password: input.password, + // Drop a retired password; `undefined` keeps the stored one. + password: input.accessType === "password" ? input.password : null, allowedIpRanges: input.allowedIpRanges?.join(",") || null, ...(input.allowIndex !== undefined && { allowIndex: input.allowIndex }), updatedAt: new Date(), -- 2.54.0 From 7fb0d33f8dc3d9a228d56322d8bed657abbd2b9b Mon Sep 17 00:00:00 2001 From: Thibault Le Ouay Date: Tue, 20 Oct 2026 10:45:38 +0200 Subject: [PATCH 5/6] monitoring: longer timeout 5min (#2943) * monitoring: longer timeout 5min * fix pr * fix pr --- apps/checker/checker/tcp.go | 22 ++++++++-- apps/checker/checker/tcp_test.go | 35 ++++++++++++++-- apps/checker/handlers/checker.go | 2 +- apps/checker/handlers/dns.go | 20 ++++++++-- apps/checker/handlers/dns_internal_test.go | 24 +++++++++++ apps/checker/handlers/grpc.go | 2 +- apps/checker/handlers/handler.go | 8 ++++ apps/checker/handlers/icmp.go | 2 +- apps/checker/handlers/tcp.go | 6 +-- apps/checker/pkg/job/tcp_job.go | 2 +- apps/checker/pkg/job/tcp_job_test.go | 2 +- apps/railway-proxy/main.go | 9 ++++- .../monitor/__tests__/monitor.test.ts | 6 +-- .../rpc/handlers/monitor/validators.test.ts | 8 ++-- .../routes/rpc/handlers/monitor/validators.ts | 4 +- apps/server/static/openapi-yaml.ts | 2 +- apps/server/static/openapi.json | 40 +++++++++---------- apps/server/static/openapi.yaml | 40 +++++++++---------- apps/web/src/app/api/checker/cron/_cron.ts | 3 ++ .../pages/docs/reference/http-monitor.mdx | 2 +- .../pages/docs/sdk/nodejs/monitor-service.mdx | 6 +-- apps/workflows/src/cron/checker.ts | 3 ++ packages/db/src/schema/monitors/validation.ts | 4 +- .../openstatus/monitor/v1/dns_monitor.proto | 8 ++-- .../openstatus/monitor/v1/grpc_monitor.proto | 8 ++-- .../openstatus/monitor/v1/http_monitor.proto | 8 ++-- .../openstatus/monitor/v1/icmp_monitor.proto | 8 ++-- .../openstatus/monitor/v1/tcp_monitor.proto | 8 ++-- packages/proto/gen/openapi.yaml | 40 +++++++++---------- .../openstatus/monitor/v1/dns_monitor_pb.ts | 6 +-- .../openstatus/monitor/v1/grpc_monitor_pb.ts | 6 +-- .../openstatus/monitor/v1/http_monitor_pb.ts | 6 +-- .../openstatus/monitor/v1/icmp_monitor_pb.ts | 6 +-- .../openstatus/monitor/v1/tcp_monitor_pb.ts | 6 +-- .../src/monitor/__tests__/monitor.test.ts | 14 +++---- packages/services/src/monitor/schemas.ts | 18 ++++----- 36 files changed, 244 insertions(+), 150 deletions(-) diff --git a/apps/checker/checker/tcp.go b/apps/checker/checker/tcp.go index d1a36cfef..2b7fc5ca5 100644 --- a/apps/checker/checker/tcp.go +++ b/apps/checker/checker/tcp.go @@ -1,6 +1,8 @@ package checker import ( + "context" + "errors" "fmt" "net" "strings" @@ -31,14 +33,26 @@ type TCPResponse struct { Error uint8 `json:"error,omitempty"` } -func PingTCP(timeout int, url string) (TCPResponseTiming, error) { +// tcpDefaultTimeout is the dial timeout in milliseconds when none is given. +const tcpDefaultTimeout = 45_000 + +// PingTCP dials url and reports how long the connection took. timeoutMs is in +// milliseconds, like every monitor timeout; the dial also stops when ctx ends. +func PingTCP(ctx context.Context, timeoutMs int64, url string) (TCPResponseTiming, error) { + if timeoutMs <= 0 { + timeoutMs = tcpDefaultTimeout + } + + dialer := net.Dialer{Timeout: time.Duration(timeoutMs) * time.Millisecond} + start := time.Now().UTC().UnixMilli() - conn, err := net.DialTimeout("tcp", url, time.Duration(timeout)*time.Second) + conn, err := dialer.DialContext(ctx, "tcp", url) stop := time.Now().UTC().UnixMilli() if err != nil { - if e := err.(*net.OpError).Timeout(); e { - return TCPResponseTiming{}, fmt.Errorf("timeout after %d ms", timeout*1000) + var netErr net.Error + if errors.As(err, &netErr) && netErr.Timeout() { + return TCPResponseTiming{}, fmt.Errorf("timeout after %d ms", timeoutMs) } if strings.Contains(err.Error(), "connection refused") { return TCPResponseTiming{}, fmt.Errorf("connection refused") diff --git a/apps/checker/checker/tcp_test.go b/apps/checker/checker/tcp_test.go index 5f2d135dd..1f607552e 100644 --- a/apps/checker/checker/tcp_test.go +++ b/apps/checker/checker/tcp_test.go @@ -1,7 +1,9 @@ package checker_test import ( + "context" "testing" + "time" "github.com/openstatushq/openstatus/apps/checker/checker" ) @@ -9,7 +11,7 @@ import ( func TestPingTcp(t *testing.T) { type args struct { url string - timeout int + timeout int64 } tests := []struct { name string @@ -17,12 +19,12 @@ func TestPingTcp(t *testing.T) { want checker.TCPResponseTiming wantErr bool }{ - {name: "will failed", args: args{url: "error", timeout: 60}, wantErr: true}, - {name: "will be ok", args: args{url: "openstat.us:443", timeout: 60}, wantErr: false}, + {name: "will failed", args: args{url: "error", timeout: 5000}, wantErr: true}, + {name: "will be ok", args: args{url: "openstat.us:443", timeout: 5000}, wantErr: false}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got, err := checker.PingTCP(tt.args.timeout, tt.args.url) + got, err := checker.PingTCP(context.Background(), tt.args.timeout, tt.args.url) if (err != nil) != tt.wantErr { t.Errorf("PingTcp() error = %v, wantErr %v", err, tt.wantErr) return @@ -39,3 +41,28 @@ func TestPingTcp(t *testing.T) { }) } } + +func TestPingTcpTimeoutIsMilliseconds(t *testing.T) { + // 192.0.2.0/24 (TEST-NET-1) is never routed, so the dial hangs until the timeout. + start := time.Now() + _, err := checker.PingTCP(context.Background(), 500, "192.0.2.1:80") + if err == nil { + t.Fatal("PingTCP() expected an error dialing TEST-NET-1") + } + if elapsed := time.Since(start); elapsed > 5*time.Second { + t.Fatalf("PingTCP() took %s, a 500ms timeout was read as seconds", elapsed) + } +} + +func TestPingTcpStopsWhenContextEnds(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), 200*time.Millisecond) + defer cancel() + + start := time.Now() + if _, err := checker.PingTCP(ctx, 60_000, "192.0.2.1:80"); err == nil { + t.Fatal("PingTCP() expected an error once the context ended") + } + if elapsed := time.Since(start); elapsed > 5*time.Second { + t.Fatalf("PingTCP() ignored the context, took %s", elapsed) + } +} diff --git a/apps/checker/handlers/checker.go b/apps/checker/handlers/checker.go index 0c9c1e3fc..74d6851da 100644 --- a/apps/checker/handlers/checker.go +++ b/apps/checker/handlers/checker.go @@ -274,7 +274,7 @@ func (h Handler) HTTPCheckerHandler(c *gin.Context) { return struct{}{}, nil } - if _, err := backoff.Retry(ctx, op, backoff.WithBackOff(backoff.NewExponentialBackOff()), backoff.WithMaxTries(uint(retry))); err != nil { + if _, err := backoff.Retry(ctx, op, backoff.WithBackOff(backoff.NewExponentialBackOff()), backoff.WithMaxTries(uint(retry)), backoff.WithMaxElapsedTime(retryBudget)); err != nil { id, e := uuid.NewV7() if e != nil { log.Ctx(ctx).Error().Err(e).Msg("failed to send event to tinybird") diff --git a/apps/checker/handlers/dns.go b/apps/checker/handlers/dns.go index face331a7..26a06789e 100644 --- a/apps/checker/handlers/dns.go +++ b/apps/checker/handlers/dns.go @@ -1,6 +1,7 @@ package handlers import ( + "context" "encoding/json" "fmt" "net/http" @@ -142,8 +143,10 @@ func (h Handler) DNSHandler(c *gin.Context) { op := func() (*checker.DnsResponse, error) { called++ log.Ctx(ctx).Debug().Msgf("performing dns check for %s (attempt %d/%d)", req.URI, called, retry) + attemptCtx, cancel := dnsAttemptContext(ctx, req.Timeout) + defer cancel() start := time.Now().UTC().UnixMilli() - response, err := checker.Dns(ctx, req.URI) + response, err := checker.Dns(attemptCtx, req.URI) latency = time.Now().UTC().UnixMilli() - start if err != nil { @@ -167,7 +170,7 @@ func (h Handler) DNSHandler(c *gin.Context) { return response, nil } - result, err := backoff.Retry(ctx, op, backoff.WithBackOff(backoff.NewExponentialBackOff()), backoff.WithMaxTries(uint(retry))) + result, err := backoff.Retry(ctx, op, backoff.WithBackOff(backoff.NewExponentialBackOff()), backoff.WithMaxTries(uint(retry)), backoff.WithMaxElapsedTime(retryBudget)) data.Latency = latency if result != nil { data.Records = checker.FormatDNSRecords(result) @@ -312,8 +315,10 @@ func (h Handler) DNSHandlerRegion(c *gin.Context) { op := func() (*checker.DnsResponse, error) { called++ log.Ctx(ctx).Debug().Msgf("performing dns check for %s (attempt %d/%d)", req.URI, called, retry) + attemptCtx, cancel := dnsAttemptContext(ctx, req.Timeout) + defer cancel() start := time.Now().UTC().UnixMilli() - response, err := checker.Dns(ctx, req.URI) + response, err := checker.Dns(attemptCtx, req.URI) latency = time.Now().UTC().UnixMilli() - start if err != nil { @@ -399,3 +404,12 @@ func EvaluateDNSAssertions(rawAssertions []json.RawMessage, response *checker.Dn } return true, nil } + +// dnsAttemptContext bounds one lookup attempt by the monitor timeout +// (milliseconds), so a stalled resolver cannot outlive the retry budget. +func dnsAttemptContext(ctx context.Context, timeoutMs int64) (context.Context, context.CancelFunc) { + if timeoutMs <= 0 { + return context.WithCancel(ctx) + } + return context.WithTimeout(ctx, time.Duration(timeoutMs)*time.Millisecond) +} diff --git a/apps/checker/handlers/dns_internal_test.go b/apps/checker/handlers/dns_internal_test.go index 0d3b64c33..6765f35fa 100644 --- a/apps/checker/handlers/dns_internal_test.go +++ b/apps/checker/handlers/dns_internal_test.go @@ -1,9 +1,11 @@ package handlers import ( + "context" "encoding/json" "reflect" "testing" + "time" ) func TestTinybirdEventRecordsIsString(t *testing.T) { @@ -82,3 +84,25 @@ func TestDNSResponseHTTPShapeKeepsObject(t *testing.T) { t.Errorf("HTTP records is not a JSON object: %s", raw["records"]) } } + +func TestDNSAttemptContextAppliesMonitorTimeout(t *testing.T) { + ctx, cancel := dnsAttemptContext(context.Background(), 1500) + defer cancel() + + deadline, ok := ctx.Deadline() + if !ok { + t.Fatal("dnsAttemptContext() set no deadline for a positive timeout") + } + if left := time.Until(deadline); left <= time.Second || left > 1500*time.Millisecond { + t.Fatalf("dnsAttemptContext() deadline in %s, want about 1.5s", left) + } +} + +func TestDNSAttemptContextWithoutTimeout(t *testing.T) { + ctx, cancel := dnsAttemptContext(context.Background(), 0) + defer cancel() + + if _, ok := ctx.Deadline(); ok { + t.Fatal("dnsAttemptContext() set a deadline for a zero timeout") + } +} diff --git a/apps/checker/handlers/grpc.go b/apps/checker/handlers/grpc.go index a0f324d3d..7e82c8fcd 100644 --- a/apps/checker/handlers/grpc.go +++ b/apps/checker/handlers/grpc.go @@ -231,7 +231,7 @@ func (h Handler) GRPCHandler(c *gin.Context) { return struct{}{}, nil } - if _, err := backoff.Retry(ctx, op, backoff.WithBackOff(backoff.NewExponentialBackOff()), backoff.WithMaxTries(uint(retry))); err != nil { + if _, err := backoff.Retry(ctx, op, backoff.WithBackOff(backoff.NewExponentialBackOff()), backoff.WithMaxTries(uint(retry)), backoff.WithMaxElapsedTime(retryBudget)); err != nil { id, e := uuid.NewV7() if e != nil { log.Ctx(ctx).Error().Err(e).Msg("failed to send event to tinybird") diff --git a/apps/checker/handlers/handler.go b/apps/checker/handlers/handler.go index b8e50f36d..7ee164b7a 100644 --- a/apps/checker/handlers/handler.go +++ b/apps/checker/handlers/handler.go @@ -2,10 +2,18 @@ package handlers import ( "net/http" + "time" "github.com/openstatushq/openstatus/apps/checker/pkg/tinybird" ) +// retryBudget stops scheduling new attempts once a cron check's retry loop has +// run this long. The attempt in flight still finishes, so the worst case is +// retryBudget plus one monitor timeout (max 5m): 20m, inside the 30m Cloud +// Tasks dispatch deadline. Past that deadline Cloud Tasks resends the task and +// the check is recorded twice. +const retryBudget = 15 * time.Minute + type Handler struct { TbClient tinybird.Client Secret string diff --git a/apps/checker/handlers/icmp.go b/apps/checker/handlers/icmp.go index 9efa0dac6..36213f731 100644 --- a/apps/checker/handlers/icmp.go +++ b/apps/checker/handlers/icmp.go @@ -213,7 +213,7 @@ func (h Handler) ICMPHandler(c *gin.Context) { return struct{}{}, nil } - if _, err := backoff.Retry(ctx, op, backoff.WithBackOff(backoff.NewExponentialBackOff()), backoff.WithMaxTries(uint(retry))); err != nil { + if _, err := backoff.Retry(ctx, op, backoff.WithBackOff(backoff.NewExponentialBackOff()), backoff.WithMaxTries(uint(retry)), backoff.WithMaxElapsedTime(retryBudget)); err != nil { id, e := uuid.NewV7() if e != nil { log.Ctx(ctx).Error().Err(e).Msg("failed to send event to tinybird") diff --git a/apps/checker/handlers/tcp.go b/apps/checker/handlers/tcp.go index 12a06a8fd..ed1a7cfce 100644 --- a/apps/checker/handlers/tcp.go +++ b/apps/checker/handlers/tcp.go @@ -109,7 +109,7 @@ func (h Handler) TCPHandler(c *gin.Context) { } op := func() (struct{}, error) { - res, err := checker.PingTCP(int(req.Timeout), req.URI) + res, err := checker.PingTCP(ctx, req.Timeout, req.URI) if err != nil { return struct{}{}, fmt.Errorf("unable to check tcp %s", err) @@ -207,7 +207,7 @@ func (h Handler) TCPHandler(c *gin.Context) { return struct{}{}, nil } - if _, err := backoff.Retry(ctx, op, backoff.WithBackOff(backoff.NewExponentialBackOff()), backoff.WithMaxTries(uint(retry))); err != nil { + if _, err := backoff.Retry(ctx, op, backoff.WithBackOff(backoff.NewExponentialBackOff()), backoff.WithMaxTries(uint(retry)), backoff.WithMaxElapsedTime(retryBudget)); err != nil { id, e := uuid.NewV7() if e != nil { @@ -298,7 +298,7 @@ func (h Handler) TCPHandlerRegion(c *gin.Context) { op := func() (struct{}, error) { called++ timestamp := time.Now().UTC().UnixMilli() - res, err := checker.PingTCP(int(req.Timeout), req.URI) + res, err := checker.PingTCP(ctx, req.Timeout, req.URI) if err != nil { return struct{}{}, fmt.Errorf("unable to check tcp %s", err) diff --git a/apps/checker/pkg/job/tcp_job.go b/apps/checker/pkg/job/tcp_job.go index a4891bffd..60ef46d69 100644 --- a/apps/checker/pkg/job/tcp_job.go +++ b/apps/checker/pkg/job/tcp_job.go @@ -54,7 +54,7 @@ func (jobRunner) TCPJob(ctx context.Context, monitor *v1.TCPMonitor, region stri op := func() (*TCPPrivateRegionData, error) { called++ - res, err := checker.PingTCP(int(monitor.Timeout), monitor.Uri) + res, err := checker.PingTCP(ctx, monitor.Timeout, monitor.Uri) if err != nil { if called < int(retry) { return nil, fmt.Errorf("TCP connection failed: %w", err) diff --git a/apps/checker/pkg/job/tcp_job_test.go b/apps/checker/pkg/job/tcp_job_test.go index 2fad24eff..d93dc1990 100644 --- a/apps/checker/pkg/job/tcp_job_test.go +++ b/apps/checker/pkg/job/tcp_job_test.go @@ -12,7 +12,7 @@ func TestTCPJob_Success(t *testing.T) { monitor := &v1.TCPMonitor{ Uri: "openstatus.dev:80", - Timeout: 1, + Timeout: 10_000, Retry: 1, } data, err := job.NewJobRunner().TCPJob(context.Background(), monitor, "test-region") diff --git a/apps/railway-proxy/main.go b/apps/railway-proxy/main.go index af2c30d73..3d7c7657e 100644 --- a/apps/railway-proxy/main.go +++ b/apps/railway-proxy/main.go @@ -80,6 +80,11 @@ func isDialError(err error) bool { return false } +// The checker responds only after its whole retry loop, which can run for +// several monitor timeouts (up to 5m each). Matches the Cloud Tasks dispatch +// deadline set by the workflows cron. +const checkerResponseTimeout = 30 * time.Minute + // Tuned for Railway internal networking: short dial timeout so a dead pod // fails fast instead of stalling for the default 30s, plus aggressive idle // pruning so we don't reuse connections that the platform has silently torn down. @@ -94,7 +99,7 @@ func newTransport() *http.Transport { IdleConnTimeout: 60 * time.Second, TLSHandshakeTimeout: 10 * time.Second, ExpectContinueTimeout: 1 * time.Second, - ResponseHeaderTimeout: 60 * time.Second, + ResponseHeaderTimeout: checkerResponseTimeout, ForceAttemptHTTP2: true, } } @@ -233,7 +238,7 @@ func main() { Handler: router, ReadHeaderTimeout: 10 * time.Second, ReadTimeout: 60 * time.Second, - WriteTimeout: 60 * time.Second, + WriteTimeout: checkerResponseTimeout, IdleTimeout: 120 * time.Second, } diff --git a/apps/server/src/routes/rpc/handlers/monitor/__tests__/monitor.test.ts b/apps/server/src/routes/rpc/handlers/monitor/__tests__/monitor.test.ts index 2a44dd445..09c31b603 100644 --- a/apps/server/src/routes/rpc/handlers/monitor/__tests__/monitor.test.ts +++ b/apps/server/src/routes/rpc/handlers/monitor/__tests__/monitor.test.ts @@ -1690,8 +1690,8 @@ describe("MonitorService.UpdateICMPMonitor", () => { for (const [field, monitor] of [ ["retry above max", { retry: "11" }], ["negative retry", { retry: "-1" }], - ["timeout above max", { timeout: "120001" }], - ["degradedAt above max", { degradedAt: "120001" }], + ["timeout above max", { timeout: "300001" }], + ["degradedAt above max", { degradedAt: "300001" }], ["over-long description", { description: "d".repeat(1025) }], ["over-long name", { name: "n".repeat(257) }], ["over-long uri", { uri: "u".repeat(2049) }], @@ -1740,7 +1740,7 @@ describe("MonitorService.UpdateICMPMonitor", () => { "UpdateICMPMonitor", { id: String(testIcmpMonitorId), - monitor: { retry: "10", timeout: "120000" }, + monitor: { retry: "10", timeout: "300000" }, }, { "x-openstatus-key": "1" }, ); diff --git a/apps/server/src/routes/rpc/handlers/monitor/validators.test.ts b/apps/server/src/routes/rpc/handlers/monitor/validators.test.ts index a25af51ba..0463945b2 100644 --- a/apps/server/src/routes/rpc/handlers/monitor/validators.test.ts +++ b/apps/server/src/routes/rpc/handlers/monitor/validators.test.ts @@ -190,8 +190,8 @@ describe("validateMonitorPatchBounds", () => { name: "a".repeat(256), uri: "b".repeat(2048), description: "c".repeat(1024), - timeout: BigInt(120_000), - degradedAt: BigInt(120_000), + timeout: BigInt(300_000), + degradedAt: BigInt(300_000), retry: BigInt(10), }); }); @@ -212,13 +212,13 @@ describe("validateMonitorPatchBounds", () => { test("rejects a timeout above the proto maximum", () => { expect(() => - validateMonitorPatchBounds({ timeout: BigInt(120_001) }), + validateMonitorPatchBounds({ timeout: BigInt(300_001) }), ).toThrow(ConnectError); }); test("rejects a degradedAt outside its range", () => { expect(() => - validateMonitorPatchBounds({ degradedAt: BigInt(120_001) }), + validateMonitorPatchBounds({ degradedAt: BigInt(300_001) }), ).toThrow(ConnectError); expect(() => validateMonitorPatchBounds({ degradedAt: BigInt(-1) }), diff --git a/apps/server/src/routes/rpc/handlers/monitor/validators.ts b/apps/server/src/routes/rpc/handlers/monitor/validators.ts index 21902c8a6..68b65c647 100644 --- a/apps/server/src/routes/rpc/handlers/monitor/validators.ts +++ b/apps/server/src/routes/rpc/handlers/monitor/validators.ts @@ -78,8 +78,8 @@ const MONITOR_BOUNDS = { nameMaxLen: 256, uriMaxLen: 2048, descriptionMaxLen: 1024, - timeoutMaxMs: 120_000, - degradedAtMaxMs: 120_000, + timeoutMaxMs: 300_000, + degradedAtMaxMs: 300_000, retryMax: 10, regionsMaxItems: 28, serviceMaxLen: 512, diff --git a/apps/server/static/openapi-yaml.ts b/apps/server/static/openapi-yaml.ts index 6447f34b0..dcc7f1870 100644 --- a/apps/server/static/openapi-yaml.ts +++ b/apps/server/static/openapi-yaml.ts @@ -1,2 +1,2 @@ // Generated from openapi.yaml — run `pnpm --filter @openstatus/server openapi:json`. -export default "openapi: 3.1.0\ninfo:\n description: OpenStatus is a open-source status page platform with global uptime monitoring. The OpenStatus API allows you to interact with the OpenStatus platform programmatically. To get started you need to create an account on https://www.openstatus.dev/ and create an api token in your settings. Requests are rate limited per API key or token (600 per minute, 100 per 10 seconds); exceeding a limit returns HTTP 429 with a Retry-After header. See https://www.openstatus.dev/docs/reference/api-rate-limits.\n title: OpenStatus API\n version: v2.0.0\n contact:\n email: ping@openstatus.dev\n url: https://www.openstatus.dev\nservers:\n - url: https://api.openstatus.dev\n description: Production\nexternalDocs:\n description: OpenStatus Documentation\n url: https://www.openstatus.dev/docs\ncomponents:\n responses:\n RateLimited:\n description: Rate limit exceeded (Connect code resource_exhausted). Retry after the number of seconds in the Retry-After header. See https://www.openstatus.dev/docs/reference/api-rate-limits.\n headers:\n Retry-After:\n description: Seconds to wait before retrying.\n schema:\n type: integer\n example: 7\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/connect.error'\n securitySchemes:\n ApiKeyAuth:\n type: apiKey\n in: header\n name: x-openstatus-key\n schemas:\n connect.error:\n type: object\n properties:\n code:\n type: string\n examples:\n - not_found\n enum:\n - canceled\n - unknown\n - invalid_argument\n - deadline_exceeded\n - not_found\n - already_exists\n - permission_denied\n - resource_exhausted\n - failed_precondition\n - aborted\n - out_of_range\n - unimplemented\n - internal\n - unavailable\n - data_loss\n - unauthenticated\n description: The status code, which should be an enum value of [google.rpc.Code][google.rpc.Code].\n message:\n type: string\n description: A developer-facing error message, which should be in English. Any user-facing error message should be localized and sent in the [google.rpc.Status.details][google.rpc.Status.details] field, or localized by the client.\n details:\n type: array\n items:\n $ref: '#/components/schemas/connect.error_details.Any'\n description: A list of messages that carry the error details. There is no limit on the number of messages.\n title: Connect Error\n additionalProperties: true\n description: 'Error type returned by Connect: https://connectrpc.com/docs/go/errors/#http-representation'\n connect.error_details.Any:\n type: object\n properties:\n type:\n type: string\n description: 'A URL that acts as a globally unique identifier for the type of the serialized message. For example: `type.googleapis.com/google.rpc.ErrorInfo`. This is used to determine the schema of the data in the `value` field and is the discriminator for the `debug` field.'\n value:\n type: string\n format: binary\n description: The Protobuf message, serialized as bytes and base64-encoded. The specific message type is identified by the `type` field.\n debug:\n oneOf:\n - type: object\n title: Any\n additionalProperties: true\n description: Detailed error information.\n discriminator:\n propertyName: type\n title: Debug\n description: Deserialized error detail payload. The 'type' field indicates the schema. This field is for easier debugging and should not be relied upon for application logic.\n additionalProperties: true\n description: Contains an arbitrary serialized message along with a @type that describes the type of the serialized message, with an additional debug field for ConnectRPC error details.\n openstatus.health.v1.CheckRequest:\n type: object\n properties:\n service:\n type: string\n title: service\n description: Optional service name to check. If empty, checks overall service health.\n title: CheckRequest\n additionalProperties: false\n description: CheckRequest is the request message for health checks.\n openstatus.health.v1.CheckResponse:\n type: object\n properties:\n status:\n title: status\n description: The serving status of the service.\n $ref: '#/components/schemas/openstatus.health.v1.CheckResponse.ServingStatus'\n title: CheckResponse\n additionalProperties: false\n description: CheckResponse is the response message for health checks.\n openstatus.health.v1.CheckResponse.ServingStatus:\n type: string\n title: ServingStatus\n enum:\n - SERVING_STATUS_UNSPECIFIED\n - SERVING_STATUS_SERVING\n - SERVING_STATUS_NOT_SERVING\n description: ServingStatus represents the health status of the service.\n openstatus.incident.v1.AddIncidentNoteRequest:\n type: object\n properties:\n id:\n type: string\n title: id\n minLength: 1\n description: ID of the incident (required).\n message:\n type: string\n title: message\n maxLength: 10000\n minLength: 1\n description: Note text, markdown (required, 1-10000 characters).\n title: AddIncidentNoteRequest\n additionalProperties: false\n description: AddIncidentNoteRequest is the request to add a note to an incident timeline.\n openstatus.incident.v1.AddIncidentNoteResponse:\n type: object\n properties:\n event:\n title: event\n description: The timeline event that was added.\n $ref: '#/components/schemas/openstatus.incident.v1.IncidentEvent'\n title: AddIncidentNoteResponse\n additionalProperties: false\n description: AddIncidentNoteResponse is the response after adding a note.\n openstatus.incident.v1.ApprovePostmortemRequest:\n type: object\n properties:\n incidentId:\n type: string\n title: incident_id\n minLength: 1\n description: ID of the incident (required).\n close:\n type:\n - boolean\n - \"null\"\n title: close\n description: Also close the incident (optional, defaults to false).\n title: ApprovePostmortemRequest\n additionalProperties: false\n description: ApprovePostmortemRequest is the request to approve the postmortem of an incident.\n openstatus.incident.v1.ApprovePostmortemResponse:\n type: object\n properties:\n postmortem:\n title: postmortem\n description: The approved postmortem.\n $ref: '#/components/schemas/openstatus.incident.v1.Postmortem'\n incident:\n title: incident\n description: The incident after approval (without its timeline).\n $ref: '#/components/schemas/openstatus.incident.v1.Incident'\n title: ApprovePostmortemResponse\n additionalProperties: false\n description: ApprovePostmortemResponse is the response after approving the postmortem.\n openstatus.incident.v1.CloseIncidentRequest:\n type: object\n properties:\n id:\n type: string\n title: id\n minLength: 1\n description: ID of the incident (required).\n skipPostmortem:\n type:\n - boolean\n - \"null\"\n title: skip_postmortem\n description: Close without an approved postmortem (optional, defaults to false).\n title: CloseIncidentRequest\n additionalProperties: false\n description: CloseIncidentRequest is the request to close a resolved incident.\n openstatus.incident.v1.CloseIncidentResponse:\n type: object\n properties:\n incident:\n title: incident\n description: The closed incident (without its timeline).\n $ref: '#/components/schemas/openstatus.incident.v1.Incident'\n title: CloseIncidentResponse\n additionalProperties: false\n description: CloseIncidentResponse is the response after closing an incident.\n openstatus.incident.v1.DeclareIncidentRequest:\n type: object\n properties:\n title:\n type: string\n examples:\n - Checkout API returns 502\n title: title\n maxLength: 256\n minLength: 1\n description: Title of the incident (required, 1-256 characters).\n severity:\n not:\n enum:\n - INCIDENT_SEVERITY_UNSPECIFIED\n title: severity\n description: Severity of the incident (required).\n $ref: '#/components/schemas/openstatus.incident.v1.IncidentSeverity'\n summary:\n type:\n - string\n - \"null\"\n title: summary\n maxLength: 4000\n minLength: 1\n description: Short human summary (optional, 1-4000 characters).\n commanderEmail:\n type:\n - string\n - \"null\"\n examples:\n - jane@example.com\n title: commander_email\n format: email\n description: Email of the member who leads the response (optional, defaults to unassigned).\n startedAt:\n type:\n - string\n - \"null\"\n examples:\n - \"2024-03-15T10:30:00Z\"\n title: started_at\n pattern: ^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(\\.\\d{1,9})?(Z|[+-]\\d{2}:\\d{2})$\n description: When the impact began (RFC 3339 format, optional, defaults to now).\n statusReportId:\n type:\n - string\n - \"null\"\n title: status_report_id\n minLength: 1\n description: ID of a status report to link (optional).\n openSlackChannel:\n type:\n - boolean\n - \"null\"\n title: open_slack_channel\n description: Whether to open a Slack channel for the incident when Slack is connected (optional, defaults to false).\n title: DeclareIncidentRequest\n additionalProperties: false\n description: DeclareIncidentRequest is the request to declare a new incident.\n openstatus.incident.v1.DeclareIncidentResponse:\n type: object\n properties:\n incident:\n title: incident\n description: The declared incident.\n $ref: '#/components/schemas/openstatus.incident.v1.Incident'\n title: DeclareIncidentResponse\n additionalProperties: false\n description: DeclareIncidentResponse is the response after declaring an incident.\n openstatus.incident.v1.DeleteIncidentRequest:\n type: object\n properties:\n id:\n type: string\n title: id\n minLength: 1\n description: ID of the incident (required).\n title: DeleteIncidentRequest\n additionalProperties: false\n description: DeleteIncidentRequest is the request to delete an incident.\n openstatus.incident.v1.DeleteIncidentResponse:\n type: object\n properties:\n success:\n type: boolean\n title: success\n description: Whether the deletion was successful.\n title: DeleteIncidentResponse\n additionalProperties: false\n description: DeleteIncidentResponse is the response after deleting an incident.\n openstatus.incident.v1.GetIncidentRequest:\n type: object\n properties:\n id:\n type: string\n title: id\n minLength: 1\n description: ID of the incident to retrieve (required).\n title: GetIncidentRequest\n additionalProperties: false\n description: GetIncidentRequest is the request to get an incident by ID.\n openstatus.incident.v1.GetIncidentResponse:\n type: object\n properties:\n incident:\n title: incident\n description: The requested incident.\n $ref: '#/components/schemas/openstatus.incident.v1.Incident'\n title: GetIncidentResponse\n additionalProperties: false\n description: GetIncidentResponse is the response containing the incident and its timeline.\n openstatus.incident.v1.GetPostmortemRequest:\n type: object\n properties:\n incidentId:\n type: string\n title: incident_id\n minLength: 1\n description: ID of the incident (required).\n title: GetPostmortemRequest\n additionalProperties: false\n description: GetPostmortemRequest is the request to get the postmortem of an incident.\n openstatus.incident.v1.GetPostmortemResponse:\n type: object\n properties:\n postmortem:\n oneOf:\n - $ref: '#/components/schemas/openstatus.incident.v1.Postmortem'\n - type: \"null\"\n title: postmortem\n description: The postmortem (unset when none has been written yet).\n title: GetPostmortemResponse\n additionalProperties: false\n description: GetPostmortemResponse is the response containing the postmortem, if any.\n openstatus.incident.v1.Incident:\n type: object\n properties:\n id:\n type: string\n title: id\n description: Unique identifier for the incident.\n title:\n type: string\n title: title\n description: Title of the incident.\n severity:\n title: severity\n description: Severity of the incident.\n $ref: '#/components/schemas/openstatus.incident.v1.IncidentSeverity'\n status:\n title: status\n description: Current status of the incident.\n $ref: '#/components/schemas/openstatus.incident.v1.IncidentStatus'\n summary:\n type:\n - string\n - \"null\"\n title: summary\n description: Short human summary.\n commander:\n oneOf:\n - $ref: '#/components/schemas/openstatus.incident.v1.IncidentUser'\n - type: \"null\"\n title: commander\n description: Member leading the response (unset when unassigned).\n declaredBy:\n oneOf:\n - $ref: '#/components/schemas/openstatus.incident.v1.IncidentUser'\n - type: \"null\"\n title: declared_by\n description: Member who declared the incident (unset for API keys without a creator).\n resolvedBy:\n oneOf:\n - $ref: '#/components/schemas/openstatus.incident.v1.IncidentUser'\n - type: \"null\"\n title: resolved_by\n description: Member who last resolved the incident (unset again when the incident is reopened).\n declaredAt:\n type: string\n title: declared_at\n description: Timestamp when the incident was declared (RFC 3339 format).\n startedAt:\n type: string\n title: started_at\n description: Timestamp when the impact began (RFC 3339 format).\n mitigatedAt:\n type:\n - string\n - \"null\"\n title: mitigated_at\n description: Timestamp when the incident was first mitigated (RFC 3339 format).\n resolvedAt:\n type:\n - string\n - \"null\"\n title: resolved_at\n description: Timestamp of the last resolution (RFC 3339 format). Kept when the incident is reopened.\n closedAt:\n type:\n - string\n - \"null\"\n title: closed_at\n description: |-\n Timestamp when the incident was closed or canceled (RFC 3339 format).\n A closed incident is read-only except for its postmortem.\n statusReport:\n oneOf:\n - $ref: '#/components/schemas/openstatus.incident.v1.IncidentStatusReport'\n - type: \"null\"\n title: status_report\n description: Linked public status report.\n slackChannelUrl:\n type:\n - string\n - \"null\"\n title: slack_channel_url\n description: Link to the bound Slack channel.\n allowedTransitions:\n type: array\n items:\n $ref: '#/components/schemas/openstatus.incident.v1.IncidentStatus'\n title: allowed_transitions\n description: Statuses SetIncidentStatus accepts from the current state (empty once closed).\n deletable:\n type: boolean\n title: deletable\n description: 'Whether DeleteIncident is allowed: only while open and never mitigated, resolved or closed.'\n events:\n type: array\n items:\n $ref: '#/components/schemas/openstatus.incident.v1.IncidentEvent'\n title: events\n description: Timeline, newest first (only included in GetIncident).\n createdAt:\n type: string\n title: created_at\n description: Timestamp when the incident was created (RFC 3339 format).\n updatedAt:\n type: string\n title: updated_at\n description: Timestamp when the incident was last updated (RFC 3339 format).\n title: Incident\n additionalProperties: false\n description: Incident is a managed incident with full details.\n openstatus.incident.v1.IncidentEvent:\n type: object\n properties:\n id:\n type: string\n title: id\n description: Unique identifier for the event.\n type:\n title: type\n description: Kind of event.\n $ref: '#/components/schemas/openstatus.incident.v1.IncidentEventType'\n message:\n type: string\n title: message\n description: 'Text of the event: the note for notes, a rendered summary otherwise.'\n createdBy:\n oneOf:\n - $ref: '#/components/schemas/openstatus.incident.v1.IncidentUser'\n - type: \"null\"\n title: created_by\n description: Member who caused the event (unset for system events and API keys without a creator).\n createdAt:\n type: string\n title: created_at\n description: Timestamp when the event was recorded (RFC 3339 format).\n title: IncidentEvent\n additionalProperties: false\n description: IncidentEvent is one entry of an incident timeline.\n openstatus.incident.v1.IncidentEventType:\n type: string\n title: IncidentEventType\n enum:\n - INCIDENT_EVENT_TYPE_UNSPECIFIED\n - INCIDENT_EVENT_TYPE_DECLARED\n - INCIDENT_EVENT_TYPE_SEVERITY_CHANGED\n - INCIDENT_EVENT_TYPE_STATUS_CHANGED\n - INCIDENT_EVENT_TYPE_COMMANDER_CHANGED\n - INCIDENT_EVENT_TYPE_STARTED_AT_CHANGED\n - INCIDENT_EVENT_TYPE_NOTE\n - INCIDENT_EVENT_TYPE_STATUS_REPORT_LINKED\n - INCIDENT_EVENT_TYPE_STATUS_REPORT_UNLINKED\n - INCIDENT_EVENT_TYPE_SLACK_CHANNEL_BOUND\n - INCIDENT_EVENT_TYPE_SLACK_CHANNEL_UNBOUND\n - INCIDENT_EVENT_TYPE_RESOLVED\n - INCIDENT_EVENT_TYPE_CANCELED\n - INCIDENT_EVENT_TYPE_POSTMORTEM_DRAFTED\n - INCIDENT_EVENT_TYPE_POSTMORTEM_UPDATED\n - INCIDENT_EVENT_TYPE_POSTMORTEM_APPROVED\n - INCIDENT_EVENT_TYPE_CLOSED\n description: IncidentEventType is the kind of entry in an incident timeline.\n openstatus.incident.v1.IncidentSeverity:\n type: string\n title: IncidentSeverity\n enum:\n - INCIDENT_SEVERITY_UNSPECIFIED\n - INCIDENT_SEVERITY_CRITICAL\n - INCIDENT_SEVERITY_MAJOR\n - INCIDENT_SEVERITY_MINOR\n description: IncidentSeverity is how bad an incident is.\n openstatus.incident.v1.IncidentStatus:\n type: string\n title: IncidentStatus\n enum:\n - INCIDENT_STATUS_UNSPECIFIED\n - INCIDENT_STATUS_OPEN\n - INCIDENT_STATUS_MITIGATED\n - INCIDENT_STATUS_RESOLVED\n - INCIDENT_STATUS_CANCELED\n description: |-\n IncidentStatus is the lifecycle state of an incident.\n Closed is not a status: a closed incident has closed_at set and keeps its last status.\n openstatus.incident.v1.IncidentStatusReport:\n type: object\n properties:\n id:\n type: string\n title: id\n description: ID of the status report.\n title:\n type: string\n title: title\n description: Title of the status report.\n status:\n title: status\n description: Current status of the status report.\n $ref: '#/components/schemas/openstatus.status_report.v1.StatusReportStatus'\n pageId:\n type: string\n title: page_id\n description: ID of the status page the report belongs to (empty in ListIncidents results).\n title: IncidentStatusReport\n additionalProperties: false\n description: IncidentStatusReport is the public status report linked to an incident.\n openstatus.incident.v1.IncidentSummary:\n type: object\n properties:\n id:\n type: string\n title: id\n description: Unique identifier for the incident.\n title:\n type: string\n title: title\n description: Title of the incident.\n severity:\n title: severity\n description: Severity of the incident.\n $ref: '#/components/schemas/openstatus.incident.v1.IncidentSeverity'\n status:\n title: status\n description: Current status of the incident.\n $ref: '#/components/schemas/openstatus.incident.v1.IncidentStatus'\n commander:\n oneOf:\n - $ref: '#/components/schemas/openstatus.incident.v1.IncidentUser'\n - type: \"null\"\n title: commander\n description: Member leading the response (unset when unassigned).\n declaredAt:\n type: string\n title: declared_at\n description: Timestamp when the incident was declared (RFC 3339 format).\n startedAt:\n type: string\n title: started_at\n description: Timestamp when the impact began (RFC 3339 format).\n resolvedAt:\n type:\n - string\n - \"null\"\n title: resolved_at\n description: Timestamp of the last resolution (RFC 3339 format). Kept when the incident is reopened.\n closedAt:\n type:\n - string\n - \"null\"\n title: closed_at\n description: Timestamp when the incident was closed or canceled (RFC 3339 format).\n statusReport:\n oneOf:\n - $ref: '#/components/schemas/openstatus.incident.v1.IncidentStatusReport'\n - type: \"null\"\n title: status_report\n description: Linked public status report.\n createdAt:\n type: string\n title: created_at\n description: Timestamp when the incident was created (RFC 3339 format).\n updatedAt:\n type: string\n title: updated_at\n description: Timestamp when the incident was last updated (RFC 3339 format).\n title: IncidentSummary\n additionalProperties: false\n description: IncidentSummary is the metadata of an incident (used in list responses).\n openstatus.incident.v1.IncidentUser:\n type: object\n properties:\n email:\n type: string\n title: email\n description: Email address of the member (empty for a deleted account).\n name:\n type: string\n title: name\n description: Display name of the member (\"Deleted user\" for a deleted account).\n title: IncidentUser\n additionalProperties: false\n description: IncidentUser is a workspace member referenced by an incident.\n openstatus.incident.v1.LinkStatusReportRequest:\n type: object\n properties:\n id:\n type: string\n title: id\n minLength: 1\n description: ID of the incident (required).\n statusReportId:\n type: string\n title: status_report_id\n minLength: 1\n description: ID of the status report to link (required).\n title: LinkStatusReportRequest\n additionalProperties: false\n description: LinkStatusReportRequest is the request to link a status report to an incident.\n openstatus.incident.v1.LinkStatusReportResponse:\n type: object\n properties:\n incident:\n title: incident\n description: The updated incident (without its timeline).\n $ref: '#/components/schemas/openstatus.incident.v1.Incident'\n title: LinkStatusReportResponse\n additionalProperties: false\n description: LinkStatusReportResponse is the response after linking a status report.\n openstatus.incident.v1.ListIncidentsRequest:\n type: object\n properties:\n limit:\n type:\n - integer\n - \"null\"\n title: limit\n maximum: 100\n minimum: 1\n format: int32\n description: Maximum number of incidents to return (1-100, defaults to 50).\n offset:\n type:\n - integer\n - \"null\"\n title: offset\n minimum: 0\n format: int32\n description: Number of incidents to skip for pagination (defaults to 0).\n statuses:\n type: array\n items:\n $ref: '#/components/schemas/openstatus.incident.v1.IncidentStatus'\n title: statuses\n description: Filter by status (optional). If empty, returns all statuses.\n closed:\n type:\n - boolean\n - \"null\"\n title: closed\n description: Filter by closed state (optional). If unset, returns both.\n title: ListIncidentsRequest\n additionalProperties: false\n description: ListIncidentsRequest is the request to list incidents.\n openstatus.incident.v1.ListIncidentsResponse:\n type: object\n properties:\n incidents:\n type: array\n items:\n $ref: '#/components/schemas/openstatus.incident.v1.IncidentSummary'\n title: incidents\n description: List of incidents (metadata only, use GetIncident for full details).\n totalSize:\n type: integer\n title: total_size\n format: int32\n description: Total number of incidents matching the filter.\n title: ListIncidentsResponse\n additionalProperties: false\n description: ListIncidentsResponse is the response containing incident summaries.\n openstatus.incident.v1.Postmortem:\n type: object\n properties:\n incidentId:\n type: string\n title: incident_id\n description: ID of the incident the postmortem belongs to.\n status:\n title: status\n description: Review state of the postmortem.\n $ref: '#/components/schemas/openstatus.incident.v1.PostmortemStatus'\n content:\n type: string\n title: content\n description: Markdown body of the postmortem.\n draftedBy:\n title: drafted_by\n description: Who wrote the current body.\n $ref: '#/components/schemas/openstatus.incident.v1.PostmortemAuthor'\n approvedBy:\n oneOf:\n - $ref: '#/components/schemas/openstatus.incident.v1.IncidentUser'\n - type: \"null\"\n title: approved_by\n description: Member who approved the postmortem.\n approvedAt:\n type:\n - string\n - \"null\"\n title: approved_at\n description: Timestamp when the postmortem was approved (RFC 3339 format).\n createdAt:\n type: string\n title: created_at\n description: Timestamp when the postmortem was created (RFC 3339 format).\n updatedAt:\n type: string\n title: updated_at\n description: Timestamp when the postmortem was last updated (RFC 3339 format).\n title: Postmortem\n additionalProperties: false\n description: Postmortem is the review written after an incident is resolved.\n openstatus.incident.v1.PostmortemAuthor:\n type: string\n title: PostmortemAuthor\n enum:\n - POSTMORTEM_AUTHOR_UNSPECIFIED\n - POSTMORTEM_AUTHOR_AGENT\n - POSTMORTEM_AUTHOR_USER\n description: PostmortemAuthor is who wrote the current postmortem body.\n openstatus.incident.v1.PostmortemStatus:\n type: string\n title: PostmortemStatus\n enum:\n - POSTMORTEM_STATUS_UNSPECIFIED\n - POSTMORTEM_STATUS_DRAFT\n - POSTMORTEM_STATUS_APPROVED\n description: PostmortemStatus is the review state of a postmortem.\n openstatus.incident.v1.SetIncidentStatusRequest:\n type: object\n properties:\n id:\n type: string\n title: id\n minLength: 1\n description: ID of the incident (required).\n status:\n not:\n enum:\n - INCIDENT_STATUS_UNSPECIFIED\n title: status\n description: Target status (required).\n $ref: '#/components/schemas/openstatus.incident.v1.IncidentStatus'\n note:\n type:\n - string\n - \"null\"\n title: note\n maxLength: 10000\n description: Note recorded with the change (optional, up to 10000 characters).\n title: SetIncidentStatusRequest\n additionalProperties: false\n description: SetIncidentStatusRequest is the request to change the status of an incident.\n openstatus.incident.v1.SetIncidentStatusResponse:\n type: object\n properties:\n incident:\n title: incident\n description: The updated incident (without its timeline).\n $ref: '#/components/schemas/openstatus.incident.v1.Incident'\n title: SetIncidentStatusResponse\n additionalProperties: false\n description: SetIncidentStatusResponse is the response after changing the status of an incident.\n openstatus.incident.v1.UnlinkStatusReportRequest:\n type: object\n properties:\n id:\n type: string\n title: id\n minLength: 1\n description: ID of the incident (required).\n title: UnlinkStatusReportRequest\n additionalProperties: false\n description: UnlinkStatusReportRequest is the request to unlink the status report of an incident.\n openstatus.incident.v1.UnlinkStatusReportResponse:\n type: object\n properties:\n incident:\n title: incident\n description: The updated incident (without its timeline).\n $ref: '#/components/schemas/openstatus.incident.v1.Incident'\n title: UnlinkStatusReportResponse\n additionalProperties: false\n description: UnlinkStatusReportResponse is the response after unlinking the status report.\n openstatus.incident.v1.UpdateIncidentRequest:\n type: object\n properties:\n id:\n type: string\n title: id\n minLength: 1\n description: ID of the incident to update (required).\n title:\n type:\n - string\n - \"null\"\n title: title\n maxLength: 256\n minLength: 1\n description: New title (optional, 1-256 characters).\n severity:\n oneOf:\n - $ref: '#/components/schemas/openstatus.incident.v1.IncidentSeverity'\n - type: \"null\"\n not:\n enum:\n - INCIDENT_SEVERITY_UNSPECIFIED\n title: severity\n description: New severity (optional).\n summary:\n type:\n - string\n - \"null\"\n title: summary\n maxLength: 4000\n minLength: 1\n description: New summary (optional, 1-4000 characters).\n clearSummary:\n type:\n - boolean\n - \"null\"\n title: clear_summary\n description: Set to true to remove the summary. Cannot be combined with summary.\n commanderEmail:\n type:\n - string\n - \"null\"\n title: commander_email\n format: email\n description: Email of the new commander (optional).\n clearCommander:\n type:\n - boolean\n - \"null\"\n title: clear_commander\n description: Set to true to unassign the commander. Cannot be combined with commander_email.\n startedAt:\n type:\n - string\n - \"null\"\n title: started_at\n pattern: ^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(\\.\\d{1,9})?(Z|[+-]\\d{2}:\\d{2})$\n description: New start of impact (RFC 3339 format, optional).\n title: UpdateIncidentRequest\n additionalProperties: false\n description: |\n UpdateIncidentRequest is the request to edit an incident.\n commander_email_or_clear_commander // set either commander_email or clear_commander, not both\n summary_or_clear_summary // set either summary or clear_summary, not both\n openstatus.incident.v1.UpdateIncidentResponse:\n type: object\n properties:\n incident:\n title: incident\n description: The updated incident (without its timeline).\n $ref: '#/components/schemas/openstatus.incident.v1.Incident'\n title: UpdateIncidentResponse\n additionalProperties: false\n description: UpdateIncidentResponse is the response after updating an incident.\n openstatus.incident.v1.UpdatePostmortemRequest:\n type: object\n properties:\n incidentId:\n type: string\n title: incident_id\n minLength: 1\n description: ID of the incident (required).\n content:\n type: string\n title: content\n maxLength: 100000\n minLength: 1\n description: Markdown body (required, 1-100000 characters). Replaces the current body; an approved postmortem stays approved.\n title: UpdatePostmortemRequest\n additionalProperties: false\n description: UpdatePostmortemRequest is the request to write the postmortem of a resolved incident.\n openstatus.incident.v1.UpdatePostmortemResponse:\n type: object\n properties:\n postmortem:\n title: postmortem\n description: The saved postmortem.\n $ref: '#/components/schemas/openstatus.incident.v1.Postmortem'\n title: UpdatePostmortemResponse\n additionalProperties: false\n description: UpdatePostmortemResponse is the response after writing the postmortem.\n openstatus.maintenance.v1.AddMaintenanceUpdateRequest:\n type: object\n properties:\n maintenanceId:\n type: string\n title: maintenance_id\n minLength: 1\n description: ID of the maintenance to update (required).\n message:\n type: string\n title: message\n minLength: 1\n description: Public update message (required).\n date:\n type:\n - string\n - \"null\"\n title: date\n pattern: ^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(\\.\\d{1,9})?(Z|[+-]\\d{2}:\\d{2})$\n description: Optional date for the update (RFC 3339 format). Defaults to current time.\n notify:\n type:\n - boolean\n - \"null\"\n title: notify\n description: Whether to notify subscribers about this update.\n title: AddMaintenanceUpdateRequest\n additionalProperties: false\n description: AddMaintenanceUpdateRequest is the request to append a maintenance update.\n openstatus.maintenance.v1.AddMaintenanceUpdateResponse:\n type: object\n properties:\n maintenanceUpdate:\n title: maintenance_update\n description: The created update.\n $ref: '#/components/schemas/openstatus.maintenance.v1.MaintenanceUpdate'\n title: AddMaintenanceUpdateResponse\n additionalProperties: false\n description: AddMaintenanceUpdateResponse is the response after appending an update.\n openstatus.maintenance.v1.CreateMaintenanceRequest:\n type: object\n properties:\n title:\n type: string\n examples:\n - Database Migration\n title: title\n maxLength: 256\n minLength: 1\n description: Title of the maintenance (required, 1-256 characters).\n message:\n type: string\n title: message\n minLength: 1\n description: |-\n Message describing the maintenance (required). Posted as the first\n maintenance update.\n from:\n type: string\n examples:\n - \"2024-03-01T02:00:00Z\"\n title: from\n pattern: ^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(\\.\\d{1,9})?(Z|[+-]\\d{2}:\\d{2})$\n description: Start time of the maintenance window (RFC 3339 format, required).\n to:\n type: string\n examples:\n - \"2024-03-01T06:00:00Z\"\n title: to\n pattern: ^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(\\.\\d{1,9})?(Z|[+-]\\d{2}:\\d{2})$\n description: End time of the maintenance window (RFC 3339 format, required).\n pageId:\n type: string\n title: page_id\n minLength: 1\n description: Page ID to associate with this maintenance (required).\n pageComponentIds:\n type: array\n items:\n type: string\n title: page_component_ids\n description: Page component IDs to associate with this maintenance (optional).\n notify:\n type:\n - boolean\n - \"null\"\n title: notify\n description: Whether to notify subscribers about this maintenance (optional, defaults to false).\n title: CreateMaintenanceRequest\n additionalProperties: false\n description: CreateMaintenanceRequest is the request to create a new maintenance window.\n openstatus.maintenance.v1.CreateMaintenanceResponse:\n type: object\n properties:\n maintenance:\n title: maintenance\n description: The created maintenance.\n $ref: '#/components/schemas/openstatus.maintenance.v1.Maintenance'\n title: CreateMaintenanceResponse\n additionalProperties: false\n description: CreateMaintenanceResponse is the response after creating a maintenance window.\n openstatus.maintenance.v1.DeleteMaintenanceRequest:\n type: object\n properties:\n id:\n type: string\n title: id\n minLength: 1\n description: ID of the maintenance to delete (required).\n title: DeleteMaintenanceRequest\n additionalProperties: false\n description: DeleteMaintenanceRequest is the request to delete a maintenance window.\n openstatus.maintenance.v1.DeleteMaintenanceResponse:\n type: object\n properties:\n success:\n type: boolean\n title: success\n description: Whether the deletion was successful.\n title: DeleteMaintenanceResponse\n additionalProperties: false\n description: DeleteMaintenanceResponse is the response after deleting a maintenance window.\n openstatus.maintenance.v1.DeleteMaintenanceUpdateRequest:\n type: object\n properties:\n id:\n type: string\n title: id\n minLength: 1\n description: ID of the update to remove (required).\n title: DeleteMaintenanceUpdateRequest\n additionalProperties: false\n description: DeleteMaintenanceUpdateRequest is the request to remove an update.\n openstatus.maintenance.v1.DeleteMaintenanceUpdateResponse:\n type: object\n properties:\n success:\n type: boolean\n title: success\n description: Whether the deletion was successful.\n title: DeleteMaintenanceUpdateResponse\n additionalProperties: false\n description: DeleteMaintenanceUpdateResponse is the response after removing an update.\n openstatus.maintenance.v1.GetMaintenanceRequest:\n type: object\n properties:\n id:\n type: string\n title: id\n minLength: 1\n description: ID of the maintenance to retrieve (required).\n title: GetMaintenanceRequest\n additionalProperties: false\n description: GetMaintenanceRequest is the request to get a maintenance window by ID.\n openstatus.maintenance.v1.GetMaintenanceResponse:\n type: object\n properties:\n maintenance:\n title: maintenance\n description: The requested maintenance.\n $ref: '#/components/schemas/openstatus.maintenance.v1.Maintenance'\n title: GetMaintenanceResponse\n additionalProperties: false\n description: GetMaintenanceResponse is the response containing the maintenance window.\n openstatus.maintenance.v1.ListMaintenancesRequest:\n type: object\n properties:\n limit:\n type:\n - integer\n - \"null\"\n title: limit\n maximum: 100\n minimum: 1\n format: int32\n description: Maximum number of maintenances to return (1-100, defaults to 50).\n offset:\n type:\n - integer\n - \"null\"\n title: offset\n minimum: 0\n format: int32\n description: Number of maintenances to skip for pagination (defaults to 0).\n pageId:\n type:\n - string\n - \"null\"\n title: page_id\n description: Filter by page ID (optional).\n title: ListMaintenancesRequest\n additionalProperties: false\n description: ListMaintenancesRequest is the request to list maintenance windows.\n openstatus.maintenance.v1.ListMaintenancesResponse:\n type: object\n properties:\n maintenances:\n type: array\n items:\n $ref: '#/components/schemas/openstatus.maintenance.v1.MaintenanceSummary'\n title: maintenances\n description: List of maintenances.\n totalSize:\n type: integer\n title: total_size\n format: int32\n description: Total number of maintenances matching the filter.\n title: ListMaintenancesResponse\n additionalProperties: false\n description: ListMaintenancesResponse is the response containing maintenance window summaries.\n openstatus.maintenance.v1.Maintenance:\n type: object\n properties:\n id:\n type: string\n title: id\n description: Unique identifier for the maintenance.\n title:\n type: string\n title: title\n description: Title of the maintenance.\n message:\n type: string\n title: message\n description: Message of the newest maintenance update.\n from:\n type: string\n title: from\n description: Start time of the maintenance window (RFC 3339 format).\n to:\n type: string\n title: to\n description: End time of the maintenance window (RFC 3339 format).\n pageId:\n type: string\n title: page_id\n description: ID of the page this maintenance is associated with.\n pageComponentIds:\n type: array\n items:\n type: string\n title: page_component_ids\n description: IDs of affected page components.\n createdAt:\n type: string\n title: created_at\n description: Timestamp when the maintenance was created (RFC 3339 format).\n updatedAt:\n type: string\n title: updated_at\n description: Timestamp when the maintenance was last updated (RFC 3339 format).\n updates:\n type: array\n items:\n $ref: '#/components/schemas/openstatus.maintenance.v1.MaintenanceUpdate'\n title: updates\n description: Full maintenance update timeline, newest first.\n title: Maintenance\n additionalProperties: false\n description: Maintenance represents a maintenance window with full details.\n openstatus.maintenance.v1.MaintenanceSummary:\n type: object\n properties:\n id:\n type: string\n title: id\n description: Unique identifier for the maintenance.\n title:\n type: string\n title: title\n description: Title of the maintenance.\n message:\n type: string\n title: message\n description: Message of the newest maintenance update.\n from:\n type: string\n title: from\n description: Start time of the maintenance window (RFC 3339 format).\n to:\n type: string\n title: to\n description: End time of the maintenance window (RFC 3339 format).\n pageId:\n type: string\n title: page_id\n description: ID of the page this maintenance is associated with.\n pageComponentIds:\n type: array\n items:\n type: string\n title: page_component_ids\n description: IDs of affected page components.\n createdAt:\n type: string\n title: created_at\n description: Timestamp when the maintenance was created (RFC 3339 format).\n updatedAt:\n type: string\n title: updated_at\n description: Timestamp when the maintenance was last updated (RFC 3339 format).\n title: MaintenanceSummary\n additionalProperties: false\n description: MaintenanceSummary represents metadata for a maintenance window (used in list responses).\n openstatus.maintenance.v1.MaintenanceUpdate:\n type: object\n properties:\n id:\n type: string\n title: id\n description: Unique identifier for the update.\n message:\n type: string\n title: message\n description: Public update message.\n date:\n type: string\n title: date\n description: Date of the update (RFC 3339 format).\n maintenanceId:\n type: string\n title: maintenance_id\n description: ID of the maintenance this update belongs to.\n createdAt:\n type: string\n title: created_at\n description: Timestamp when the update was created (RFC 3339 format).\n updatedAt:\n type: string\n title: updated_at\n description: Timestamp when the update was last updated (RFC 3339 format).\n title: MaintenanceUpdate\n additionalProperties: false\n description: MaintenanceUpdate represents a timeline entry for a maintenance window.\n openstatus.maintenance.v1.UpdateMaintenanceRequest:\n type: object\n properties:\n id:\n type: string\n title: id\n minLength: 1\n description: ID of the maintenance to update (required).\n title:\n type:\n - string\n - \"null\"\n title: title\n maxLength: 256\n minLength: 1\n description: New title for the maintenance (optional).\n message:\n type:\n - string\n - \"null\"\n title: message\n description: Replaces the message of the newest maintenance update (optional).\n from:\n type:\n - string\n - \"null\"\n title: from\n pattern: ^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(\\.\\d{1,9})?(Z|[+-]\\d{2}:\\d{2})$\n description: New start time (RFC 3339 format, optional).\n to:\n type:\n - string\n - \"null\"\n title: to\n pattern: ^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(\\.\\d{1,9})?(Z|[+-]\\d{2}:\\d{2})$\n description: New end time (RFC 3339 format, optional).\n pageId:\n type:\n - string\n - \"null\"\n title: page_id\n description: 'Deprecated: page_id is now derived from page_component_ids.'\n deprecated: true\n pageComponentIds:\n type: array\n items:\n type: string\n title: page_component_ids\n description: New list of page component IDs (optional, replaces existing list).\n updatePageComponentIds:\n type:\n - boolean\n - \"null\"\n title: update_page_component_ids\n description: |-\n Set to true to update page component associations.\n When true, page_component_ids replaces the existing list (empty clears all).\n When false or unset, page_component_ids is ignored and existing associations are preserved.\n title: UpdateMaintenanceRequest\n additionalProperties: false\n description: UpdateMaintenanceRequest is the request to update a maintenance window.\n openstatus.maintenance.v1.UpdateMaintenanceResponse:\n type: object\n properties:\n maintenance:\n title: maintenance\n description: The updated maintenance.\n $ref: '#/components/schemas/openstatus.maintenance.v1.Maintenance'\n title: UpdateMaintenanceResponse\n additionalProperties: false\n description: UpdateMaintenanceResponse is the response after updating a maintenance window.\n openstatus.maintenance.v1.UpdateMaintenanceUpdateRequest:\n type: object\n properties:\n id:\n type: string\n title: id\n minLength: 1\n description: ID of the update to edit (required).\n message:\n type:\n - string\n - \"null\"\n title: message\n minLength: 1\n description: New public message.\n date:\n type:\n - string\n - \"null\"\n title: date\n pattern: ^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(\\.\\d{1,9})?(Z|[+-]\\d{2}:\\d{2})$\n description: New update date (RFC 3339 format).\n title: UpdateMaintenanceUpdateRequest\n additionalProperties: false\n description: UpdateMaintenanceUpdateRequest is the request to edit an update.\n openstatus.maintenance.v1.UpdateMaintenanceUpdateResponse:\n type: object\n properties:\n maintenanceUpdate:\n title: maintenance_update\n description: The updated timeline entry.\n $ref: '#/components/schemas/openstatus.maintenance.v1.MaintenanceUpdate'\n title: UpdateMaintenanceUpdateResponse\n additionalProperties: false\n description: UpdateMaintenanceUpdateResponse is the response after editing an update.\n openstatus.monitor.v1.BodyAssertion:\n type: object\n properties:\n target:\n type: string\n title: target\n description: Target value to compare against.\n comparator:\n not:\n enum:\n - STRING_COMPARATOR_UNSPECIFIED\n title: comparator\n description: Comparison operation (required, must not be UNSPECIFIED).\n $ref: '#/components/schemas/openstatus.monitor.v1.StringComparator'\n title: BodyAssertion\n additionalProperties: false\n description: BodyAssertion defines an assertion for response body content.\n openstatus.monitor.v1.CreateDNSMonitorRequest:\n type: object\n properties:\n monitor:\n title: monitor\n description: Monitor configuration (required).\n $ref: '#/components/schemas/openstatus.monitor.v1.DNSMonitor'\n title: CreateDNSMonitorRequest\n required:\n - monitor\n additionalProperties: false\n description: CreateDNSMonitorRequest is the request to create a new DNS monitor.\n openstatus.monitor.v1.CreateDNSMonitorResponse:\n type: object\n properties:\n monitor:\n title: monitor\n description: The created monitor with assigned ID.\n $ref: '#/components/schemas/openstatus.monitor.v1.DNSMonitor'\n title: CreateDNSMonitorResponse\n additionalProperties: false\n description: CreateDNSMonitorResponse is the response after creating a DNS monitor.\n openstatus.monitor.v1.CreateGRPCMonitorRequest:\n type: object\n properties:\n monitor:\n title: monitor\n description: Monitor configuration (required).\n $ref: '#/components/schemas/openstatus.monitor.v1.GRPCMonitor'\n title: CreateGRPCMonitorRequest\n required:\n - monitor\n additionalProperties: false\n description: CreateGRPCMonitorRequest is the request to create a new gRPC monitor.\n openstatus.monitor.v1.CreateGRPCMonitorResponse:\n type: object\n properties:\n monitor:\n title: monitor\n description: The created monitor with assigned ID.\n $ref: '#/components/schemas/openstatus.monitor.v1.GRPCMonitor'\n title: CreateGRPCMonitorResponse\n additionalProperties: false\n description: CreateGRPCMonitorResponse is the response after creating a gRPC monitor.\n openstatus.monitor.v1.CreateHTTPMonitorRequest:\n type: object\n properties:\n monitor:\n title: monitor\n description: Monitor configuration (required).\n $ref: '#/components/schemas/openstatus.monitor.v1.HTTPMonitor'\n title: CreateHTTPMonitorRequest\n required:\n - monitor\n additionalProperties: false\n description: CreateHTTPMonitorRequest is the request to create a new HTTP monitor.\n openstatus.monitor.v1.CreateHTTPMonitorResponse:\n type: object\n properties:\n monitor:\n title: monitor\n description: The created monitor with assigned ID.\n $ref: '#/components/schemas/openstatus.monitor.v1.HTTPMonitor'\n title: CreateHTTPMonitorResponse\n additionalProperties: false\n description: CreateHTTPMonitorResponse is the response after creating an HTTP monitor.\n openstatus.monitor.v1.CreateICMPMonitorRequest:\n type: object\n properties:\n monitor:\n title: monitor\n description: Monitor configuration (required).\n $ref: '#/components/schemas/openstatus.monitor.v1.ICMPMonitor'\n title: CreateICMPMonitorRequest\n required:\n - monitor\n additionalProperties: false\n description: CreateICMPMonitorRequest is the request to create a new ICMP monitor.\n openstatus.monitor.v1.CreateICMPMonitorResponse:\n type: object\n properties:\n monitor:\n title: monitor\n description: The created monitor with assigned ID.\n $ref: '#/components/schemas/openstatus.monitor.v1.ICMPMonitor'\n title: CreateICMPMonitorResponse\n additionalProperties: false\n description: CreateICMPMonitorResponse is the response after creating an ICMP monitor.\n openstatus.monitor.v1.CreateTCPMonitorRequest:\n type: object\n properties:\n monitor:\n title: monitor\n description: Monitor configuration (required).\n $ref: '#/components/schemas/openstatus.monitor.v1.TCPMonitor'\n title: CreateTCPMonitorRequest\n required:\n - monitor\n additionalProperties: false\n description: CreateTCPMonitorRequest is the request to create a new TCP monitor.\n openstatus.monitor.v1.CreateTCPMonitorResponse:\n type: object\n properties:\n monitor:\n title: monitor\n description: The created monitor with assigned ID.\n $ref: '#/components/schemas/openstatus.monitor.v1.TCPMonitor'\n title: CreateTCPMonitorResponse\n additionalProperties: false\n description: CreateTCPMonitorResponse is the response after creating a TCP monitor.\n openstatus.monitor.v1.DNSMonitor:\n type: object\n properties:\n id:\n type: string\n title: id\n description: Unique identifier for the monitor (output only for create requests).\n name:\n type: string\n examples:\n - DNS Resolution Check\n title: name\n maxLength: 256\n minLength: 1\n description: Name of the monitor (required, max 256 characters).\n uri:\n type: string\n examples:\n - example.com\n title: uri\n maxLength: 2048\n minLength: 1\n description: Domain to resolve (required, max 2048 characters).\n periodicity:\n not:\n enum:\n - PERIODICITY_UNSPECIFIED\n title: periodicity\n description: Check periodicity (required).\n $ref: '#/components/schemas/openstatus.monitor.v1.Periodicity'\n timeout:\n type:\n - integer\n - string\n title: timeout\n maximum: 120000\n minimum: 0\n format: int64\n description: Timeout in milliseconds (0-120000, defaults to 45000).\n degradedAt:\n type:\n - integer\n - string\n - \"null\"\n title: degraded_at\n maximum: 120000\n minimum: 0\n format: int64\n description: Latency threshold for degraded status in milliseconds (optional, 0-120000).\n retry:\n type:\n - integer\n - string\n title: retry\n maximum: 10\n minimum: 0\n format: int64\n description: Number of retry attempts (0-10, defaults to 3).\n recordAssertions:\n type: array\n items:\n $ref: '#/components/schemas/openstatus.monitor.v1.RecordAssertion'\n title: record_assertions\n maxItems: 10\n description: DNS record assertions for validation.\n description:\n type:\n - string\n - \"null\"\n title: description\n maxLength: 1024\n description: Description of the monitor (optional).\n active:\n type:\n - boolean\n - \"null\"\n title: active\n description: Whether the monitor is active (defaults to false).\n public:\n type:\n - boolean\n - \"null\"\n title: public\n description: Whether the monitor is publicly visible (defaults to false).\n regions:\n type: array\n items:\n $ref: '#/components/schemas/openstatus.monitor.v1.Region'\n title: regions\n maxItems: 28\n description: Geographic regions to run checks from.\n openTelemetry:\n title: open_telemetry\n description: OpenTelemetry configuration for exporting metrics.\n $ref: '#/components/schemas/openstatus.monitor.v1.OpenTelemetryConfig'\n status:\n title: status\n description: Current operational status of the monitor.\n $ref: '#/components/schemas/openstatus.monitor.v1.MonitorStatus'\n privateLocationIds:\n type: array\n items:\n type: string\n readOnly: true\n title: private_location_ids\n description: IDs of private locations that run this monitor. Read-only.\n readOnly: true\n title: DNSMonitor\n additionalProperties: false\n description: DNSMonitor defines the configuration for a DNS monitor.\n openstatus.monitor.v1.DeleteMonitorRequest:\n type: object\n properties:\n id:\n type: string\n title: id\n minLength: 1\n description: Monitor ID to delete (required).\n title: DeleteMonitorRequest\n additionalProperties: false\n description: DeleteMonitorRequest is the request to delete a monitor.\n openstatus.monitor.v1.DeleteMonitorResponse:\n type: object\n properties:\n success:\n type: boolean\n title: success\n description: Whether the deletion was successful.\n title: DeleteMonitorResponse\n additionalProperties: false\n description: DeleteMonitorResponse is the response after deleting a monitor.\n openstatus.monitor.v1.GRPCMonitor:\n type: object\n properties:\n id:\n type: string\n title: id\n description: Unique identifier for the monitor (output only for create requests).\n name:\n type: string\n examples:\n - Checkout gRPC\n title: name\n maxLength: 256\n minLength: 1\n description: Name of the monitor (required, max 256 characters).\n uri:\n type: string\n examples:\n - api.example.com:443\n title: uri\n maxLength: 2048\n minLength: 1\n pattern: ^(\\[[0-9a-fA-F:]+\\]|[^:/\\s]+):[0-9]{1,5}$\n description: Target in \"host:port\" form. IPv6 addresses must be bracketed.\n periodicity:\n not:\n enum:\n - PERIODICITY_UNSPECIFIED\n title: periodicity\n description: Check periodicity (required).\n $ref: '#/components/schemas/openstatus.monitor.v1.Periodicity'\n timeout:\n type:\n - integer\n - string\n title: timeout\n maximum: 120000\n minimum: 0\n format: int64\n description: Timeout in milliseconds (0-120000, defaults to 45000).\n degradedAt:\n type:\n - integer\n - string\n - \"null\"\n title: degraded_at\n maximum: 120000\n minimum: 0\n format: int64\n description: Latency threshold for degraded status in milliseconds (optional, 0-120000).\n retry:\n type:\n - integer\n - string\n title: retry\n maximum: 10\n minimum: 0\n format: int64\n description: Number of retry attempts (0-10, defaults to 3).\n description:\n type:\n - string\n - \"null\"\n title: description\n maxLength: 1024\n description: Description of the monitor (optional).\n active:\n type:\n - boolean\n - \"null\"\n title: active\n description: Whether the monitor is active (defaults to false).\n public:\n type:\n - boolean\n - \"null\"\n title: public\n description: Whether the monitor is publicly visible (defaults to false).\n regions:\n type: array\n items:\n $ref: '#/components/schemas/openstatus.monitor.v1.Region'\n title: regions\n maxItems: 28\n description: Geographic regions to run checks from.\n openTelemetry:\n title: open_telemetry\n description: OpenTelemetry configuration for exporting metrics.\n $ref: '#/components/schemas/openstatus.monitor.v1.OpenTelemetryConfig'\n status:\n title: status\n description: Current operational status of the monitor.\n $ref: '#/components/schemas/openstatus.monitor.v1.MonitorStatus'\n privateLocationIds:\n type: array\n items:\n type: string\n readOnly: true\n title: private_location_ids\n description: IDs of private locations that run this monitor. Read-only.\n readOnly: true\n service:\n type:\n - string\n - \"null\"\n examples:\n - checkout.v1.CheckoutService\n title: service\n maxLength: 512\n description: Service name passed to Health/Check. Empty means overall server health.\n tlsMode:\n oneOf:\n - $ref: '#/components/schemas/openstatus.monitor.v1.GRPCTlsMode'\n - type: \"null\"\n title: tls_mode\n description: How the connection to the target is secured. Defaults to TLS.\n metadata:\n type: array\n items:\n $ref: '#/components/schemas/openstatus.monitor.v1.Headers'\n title: metadata\n maxItems: 20\n description: Metadata sent with the health check request.\n title: GRPCMonitor\n additionalProperties: false\n description: |-\n GRPCMonitor defines the configuration for a gRPC health check monitor.\n The probe calls grpc.health.v1.Health/Check on the target.\n openstatus.monitor.v1.GRPCTlsMode:\n type: string\n title: GRPCTlsMode\n enum:\n - GRPC_TLS_MODE_UNSPECIFIED\n - GRPC_TLS_MODE_TLS\n - GRPC_TLS_MODE_PLAINTEXT\n - GRPC_TLS_MODE_TLS_INSECURE\n description: GRPCTlsMode selects how the probe secures its connection to the target.\n openstatus.monitor.v1.GetMonitorHTTPResponseLogRequest:\n type: object\n properties:\n id:\n type: string\n title: id\n minLength: 1\n description: Monitor ID to get a response log for (required).\n logId:\n type: string\n title: log_id\n minLength: 1\n description: Response log ID to retrieve (required).\n title: GetMonitorHTTPResponseLogRequest\n additionalProperties: false\n description: GetMonitorHTTPResponseLogRequest is the request to get one response log.\n openstatus.monitor.v1.GetMonitorHTTPResponseLogResponse:\n type: object\n properties:\n log:\n title: log\n description: Response log details.\n $ref: '#/components/schemas/openstatus.monitor.v1.HTTPResponseLogDetail'\n title: GetMonitorHTTPResponseLogResponse\n additionalProperties: false\n description: GetMonitorHTTPResponseLogResponse is the response containing one response log.\n openstatus.monitor.v1.GetMonitorRequest:\n type: object\n properties:\n id:\n type: string\n title: id\n minLength: 1\n description: Monitor ID to retrieve (required).\n title: GetMonitorRequest\n additionalProperties: false\n description: GetMonitorRequest is the request to get a single monitor by ID.\n openstatus.monitor.v1.GetMonitorResponse:\n type: object\n properties:\n monitor:\n title: monitor\n description: The monitor configuration (one of HTTP, TCP, DNS, ICMP, or gRPC).\n $ref: '#/components/schemas/openstatus.monitor.v1.MonitorConfig'\n title: GetMonitorResponse\n additionalProperties: false\n description: GetMonitorResponse is the response containing the monitor.\n openstatus.monitor.v1.GetMonitorStatusRequest:\n type: object\n properties:\n id:\n type: string\n title: id\n minLength: 1\n description: Monitor ID to get status for (required).\n title: GetMonitorStatusRequest\n additionalProperties: false\n description: GetMonitorStatusRequest is the request to get the status of all regions for a monitor.\n openstatus.monitor.v1.GetMonitorStatusResponse:\n type: object\n properties:\n id:\n type: string\n title: id\n description: Monitor ID.\n regions:\n type: array\n items:\n $ref: '#/components/schemas/openstatus.monitor.v1.RegionStatus'\n title: regions\n description: Status for each region.\n title: GetMonitorStatusResponse\n additionalProperties: false\n description: GetMonitorStatusResponse is the response containing the status of all regions for a monitor.\n openstatus.monitor.v1.GetMonitorSummaryRequest:\n type: object\n properties:\n id:\n type: string\n title: id\n minLength: 1\n description: Monitor ID to get summary for (required).\n timeRange:\n title: time_range\n description: Time range for metrics aggregation (defaults to 1 day if unspecified).\n $ref: '#/components/schemas/openstatus.monitor.v1.TimeRange'\n regions:\n type: array\n items:\n $ref: '#/components/schemas/openstatus.monitor.v1.Region'\n title: regions\n maxItems: 28\n description: Optional filter by regions. If empty, returns metrics for all regions.\n title: GetMonitorSummaryRequest\n additionalProperties: false\n description: GetMonitorSummaryRequest is the request to get aggregated metrics for a monitor.\n openstatus.monitor.v1.GetMonitorSummaryResponse:\n type: object\n properties:\n id:\n type: string\n title: id\n description: Monitor ID.\n lastPingAt:\n type: string\n title: last_ping_at\n description: Timestamp of the last check in RFC 3339 format.\n totalSuccessful:\n type:\n - integer\n - string\n title: total_successful\n format: int64\n description: Total number of successful requests.\n totalDegraded:\n type:\n - integer\n - string\n title: total_degraded\n format: int64\n description: Total number of degraded requests.\n totalFailed:\n type:\n - integer\n - string\n title: total_failed\n format: int64\n description: Total number of failed requests.\n p50:\n type:\n - integer\n - string\n title: p50\n format: int64\n description: 50th percentile (median) latency in milliseconds.\n p75:\n type:\n - integer\n - string\n title: p75\n format: int64\n description: 75th percentile latency in milliseconds.\n p90:\n type:\n - integer\n - string\n title: p90\n format: int64\n description: 90th percentile latency in milliseconds.\n p95:\n type:\n - integer\n - string\n title: p95\n format: int64\n description: 95th percentile latency in milliseconds.\n p99:\n type:\n - integer\n - string\n title: p99\n format: int64\n description: 99th percentile latency in milliseconds.\n timeRange:\n title: time_range\n description: Time range used for the metrics.\n $ref: '#/components/schemas/openstatus.monitor.v1.TimeRange'\n regions:\n type: array\n items:\n $ref: '#/components/schemas/openstatus.monitor.v1.Region'\n title: regions\n description: Regions included in the metrics.\n title: GetMonitorSummaryResponse\n additionalProperties: false\n description: GetMonitorSummaryResponse is the response containing aggregated metrics for a monitor.\n openstatus.monitor.v1.HTTPMethod:\n type: string\n title: HTTPMethod\n enum:\n - HTTP_METHOD_UNSPECIFIED\n - HTTP_METHOD_GET\n - HTTP_METHOD_POST\n - HTTP_METHOD_HEAD\n - HTTP_METHOD_PUT\n - HTTP_METHOD_PATCH\n - HTTP_METHOD_DELETE\n - HTTP_METHOD_TRACE\n - HTTP_METHOD_CONNECT\n - HTTP_METHOD_OPTIONS\n description: HTTP methods supported for monitors.\n openstatus.monitor.v1.HTTPMonitor:\n type: object\n properties:\n id:\n type: string\n title: id\n description: Unique identifier for the monitor (output only for create requests).\n name:\n type: string\n examples:\n - Production API Health Check\n title: name\n maxLength: 256\n minLength: 1\n description: Name of the monitor (required, max 256 characters).\n url:\n type: string\n examples:\n - https://api.example.com/health\n title: url\n maxLength: 2048\n minLength: 1\n format: uri\n description: URL to monitor (required, max 2048 characters).\n periodicity:\n not:\n enum:\n - PERIODICITY_UNSPECIFIED\n title: periodicity\n description: Check periodicity (required).\n $ref: '#/components/schemas/openstatus.monitor.v1.Periodicity'\n method:\n not:\n enum:\n - HTTP_METHOD_UNSPECIFIED\n title: method\n description: HTTP method to use (defaults to GET).\n $ref: '#/components/schemas/openstatus.monitor.v1.HTTPMethod'\n body:\n type: string\n examples:\n - map[key:value]\n title: body\n description: Request body (optional).\n timeout:\n type:\n - integer\n - string\n title: timeout\n maximum: 120000\n minimum: 0\n format: int64\n description: Timeout in milliseconds (0-120000, defaults to 45000).\n degradedAt:\n type:\n - integer\n - string\n - \"null\"\n title: degraded_at\n maximum: 120000\n minimum: 0\n format: int64\n description: Latency threshold for degraded status in milliseconds (optional, 0-120000).\n retry:\n type:\n - integer\n - string\n title: retry\n maximum: 10\n minimum: 0\n format: int64\n description: Number of retry attempts (0-10, defaults to 3).\n followRedirects:\n type:\n - boolean\n - \"null\"\n title: follow_redirects\n description: Whether to follow HTTP redirects (defaults to true when not specified).\n headers:\n type: array\n items:\n $ref: '#/components/schemas/openstatus.monitor.v1.Headers'\n title: headers\n maxItems: 20\n description: Custom headers for the request.\n statusCodeAssertions:\n type: array\n items:\n $ref: '#/components/schemas/openstatus.monitor.v1.StatusCodeAssertion'\n title: status_code_assertions\n maxItems: 10\n description: Status code assertions for the response.\n bodyAssertions:\n type: array\n items:\n $ref: '#/components/schemas/openstatus.monitor.v1.BodyAssertion'\n title: body_assertions\n maxItems: 10\n description: Body content assertions for the response.\n headerAssertions:\n type: array\n items:\n $ref: '#/components/schemas/openstatus.monitor.v1.HeaderAssertion'\n title: header_assertions\n maxItems: 10\n description: Header assertions for the response.\n description:\n type:\n - string\n - \"null\"\n title: description\n maxLength: 1024\n description: Description of the monitor (optional).\n active:\n type:\n - boolean\n - \"null\"\n title: active\n description: Whether the monitor is active (defaults to false).\n public:\n type:\n - boolean\n - \"null\"\n title: public\n description: Whether the monitor is publicly visible (defaults to false).\n regions:\n type: array\n items:\n $ref: '#/components/schemas/openstatus.monitor.v1.Region'\n title: regions\n maxItems: 28\n description: Geographic regions to run checks from.\n openTelemetry:\n title: open_telemetry\n description: OpenTelemetry configuration for exporting metrics.\n $ref: '#/components/schemas/openstatus.monitor.v1.OpenTelemetryConfig'\n status:\n title: status\n description: Current operational status of the monitor.\n $ref: '#/components/schemas/openstatus.monitor.v1.MonitorStatus'\n privateLocationIds:\n type: array\n items:\n type: string\n readOnly: true\n title: private_location_ids\n description: IDs of private locations that run this monitor. Read-only.\n readOnly: true\n title: HTTPMonitor\n additionalProperties: false\n description: HTTPMonitor defines the configuration for an HTTP monitor.\n openstatus.monitor.v1.HTTPResponseLogDetail:\n type: object\n properties:\n log:\n title: log\n description: Compact response log fields.\n $ref: '#/components/schemas/openstatus.monitor.v1.HTTPResponseLogListItem'\n url:\n type: string\n title: url\n description: Checked URL.\n error:\n type: boolean\n title: error\n description: Whether the check errored.\n message:\n type:\n - string\n - \"null\"\n title: message\n description: Error message, when present.\n headers:\n type: object\n title: headers\n additionalProperties:\n type: string\n title: value\n description: Redacted response headers.\n assertions:\n type:\n - string\n - \"null\"\n title: assertions\n description: Serialized assertions used for the check.\n title: HTTPResponseLogDetail\n additionalProperties: false\n description: HTTPResponseLogDetail contains full response log debugging data.\n openstatus.monitor.v1.HTTPResponseLogDetail.HeadersEntry:\n type: object\n properties:\n key:\n type: string\n title: key\n value:\n type: string\n title: value\n title: HeadersEntry\n additionalProperties: false\n openstatus.monitor.v1.HTTPResponseLogListItem:\n type: object\n properties:\n id:\n type:\n - string\n - \"null\"\n title: id\n description: Response log ID.\n latency:\n type: integer\n title: latency\n format: int32\n description: Latency in milliseconds.\n statusCode:\n type:\n - integer\n - \"null\"\n title: status_code\n format: int32\n description: HTTP status code.\n monitorId:\n type: string\n title: monitor_id\n description: Monitor ID.\n requestStatus:\n title: request_status\n description: Request status classification.\n $ref: '#/components/schemas/openstatus.monitor.v1.HTTPResponseLogRequestStatus'\n region:\n title: region\n description: Region where the check ran.\n $ref: '#/components/schemas/openstatus.monitor.v1.Region'\n cronTimestamp:\n type:\n - integer\n - string\n title: cron_timestamp\n format: int64\n description: Cron bucket timestamp in Unix milliseconds.\n trigger:\n title: trigger\n description: Check trigger.\n $ref: '#/components/schemas/openstatus.monitor.v1.HTTPResponseLogTrigger'\n timestamp:\n type:\n - integer\n - string\n title: timestamp\n format: int64\n description: Response timestamp in Unix milliseconds.\n timing:\n oneOf:\n - $ref: '#/components/schemas/openstatus.monitor.v1.HTTPResponseLogTiming'\n - type: \"null\"\n title: timing\n description: Timing phases.\n title: HTTPResponseLogListItem\n additionalProperties: false\n description: HTTPResponseLogListItem is a compact response log entry.\n openstatus.monitor.v1.HTTPResponseLogPagination:\n type: object\n properties:\n limit:\n type: integer\n title: limit\n format: int32\n description: Requested page size.\n offset:\n type: integer\n title: offset\n format: int32\n description: Requested offset.\n hasMore:\n type: boolean\n title: has_more\n description: Whether more logs are available.\n nextOffset:\n type:\n - integer\n - \"null\"\n title: next_offset\n format: int32\n description: Next offset if more logs are available.\n title: HTTPResponseLogPagination\n additionalProperties: false\n description: HTTPResponseLogPagination contains offset pagination metadata.\n openstatus.monitor.v1.HTTPResponseLogRequestStatus:\n type: string\n title: HTTPResponseLogRequestStatus\n enum:\n - HTTP_RESPONSE_LOG_REQUEST_STATUS_UNSPECIFIED\n - HTTP_RESPONSE_LOG_REQUEST_STATUS_SUCCESS\n - HTTP_RESPONSE_LOG_REQUEST_STATUS_ERROR\n - HTTP_RESPONSE_LOG_REQUEST_STATUS_DEGRADED\n description: HTTPResponseLogRequestStatus is the result classification for an HTTP response log.\n openstatus.monitor.v1.HTTPResponseLogTiming:\n type: object\n properties:\n dns:\n type: integer\n title: dns\n format: int32\n description: DNS lookup duration.\n connect:\n type: integer\n title: connect\n format: int32\n description: TCP connection duration.\n tls:\n type: integer\n title: tls\n format: int32\n description: TLS handshake duration.\n ttfb:\n type: integer\n title: ttfb\n format: int32\n description: Time to first byte duration.\n transfer:\n type: integer\n title: transfer\n format: int32\n description: Response transfer duration.\n title: HTTPResponseLogTiming\n additionalProperties: false\n description: HTTPResponseLogTiming contains calculated timing phases in milliseconds.\n openstatus.monitor.v1.HTTPResponseLogTrigger:\n type: string\n title: HTTPResponseLogTrigger\n enum:\n - HTTP_RESPONSE_LOG_TRIGGER_UNSPECIFIED\n - HTTP_RESPONSE_LOG_TRIGGER_CRON\n - HTTP_RESPONSE_LOG_TRIGGER_API\n description: HTTPResponseLogTrigger describes what started the monitor check.\n openstatus.monitor.v1.HeaderAssertion:\n type: object\n properties:\n target:\n type: string\n title: target\n description: Target value to compare against.\n comparator:\n not:\n enum:\n - STRING_COMPARATOR_UNSPECIFIED\n title: comparator\n description: Comparison operation (required, must not be UNSPECIFIED).\n $ref: '#/components/schemas/openstatus.monitor.v1.StringComparator'\n key:\n type: string\n title: key\n minLength: 1\n description: Header key to check (required).\n title: HeaderAssertion\n additionalProperties: false\n description: HeaderAssertion defines an assertion for response headers.\n openstatus.monitor.v1.Headers:\n type: object\n properties:\n key:\n type: string\n examples:\n - Authorization\n title: key\n minLength: 1\n description: Header name.\n value:\n type: string\n examples:\n - Bearer token123\n title: value\n description: Header value.\n title: Headers\n additionalProperties: false\n description: Headers represents a key-value pair for HTTP headers.\n openstatus.monitor.v1.ICMPMonitor:\n type: object\n properties:\n id:\n type: string\n title: id\n description: Unique identifier for the monitor (output only for create requests).\n name:\n type: string\n examples:\n - Ping Gateway\n title: name\n maxLength: 256\n minLength: 1\n description: Name of the monitor (required, max 256 characters).\n uri:\n type: string\n examples:\n - 1.1.1.1\n title: uri\n maxLength: 2048\n minLength: 1\n description: URI to monitor in format \"host or IP\" (required, max 2048 characters).\n periodicity:\n not:\n enum:\n - PERIODICITY_UNSPECIFIED\n title: periodicity\n description: Check periodicity (required).\n $ref: '#/components/schemas/openstatus.monitor.v1.Periodicity'\n timeout:\n type:\n - integer\n - string\n title: timeout\n maximum: 120000\n minimum: 0\n format: int64\n description: Timeout in milliseconds (0-120000, defaults to 45000).\n degradedAt:\n type:\n - integer\n - string\n - \"null\"\n title: degraded_at\n maximum: 120000\n minimum: 0\n format: int64\n description: Latency threshold for degraded status in milliseconds (optional, 0-120000).\n retry:\n type:\n - integer\n - string\n title: retry\n maximum: 10\n minimum: 0\n format: int64\n description: Number of retry attempts (0-10, defaults to 3).\n description:\n type:\n - string\n - \"null\"\n title: description\n maxLength: 1024\n description: Description of the monitor (optional).\n active:\n type:\n - boolean\n - \"null\"\n title: active\n description: Whether the monitor is active (defaults to false).\n public:\n type:\n - boolean\n - \"null\"\n title: public\n description: Whether the monitor is publicly visible (defaults to false).\n regions:\n type: array\n items:\n $ref: '#/components/schemas/openstatus.monitor.v1.Region'\n title: regions\n maxItems: 28\n description: Geographic regions to run checks from.\n openTelemetry:\n title: open_telemetry\n description: OpenTelemetry configuration for exporting metrics.\n $ref: '#/components/schemas/openstatus.monitor.v1.OpenTelemetryConfig'\n status:\n title: status\n description: Current operational status of the monitor.\n $ref: '#/components/schemas/openstatus.monitor.v1.MonitorStatus'\n privateLocationIds:\n type: array\n items:\n type: string\n readOnly: true\n title: private_location_ids\n description: IDs of private locations that run this monitor. Read-only.\n readOnly: true\n title: ICMPMonitor\n additionalProperties: false\n description: ICMPMonitor defines the configuration for a ICMP monitor.\n openstatus.monitor.v1.ListMonitorHTTPResponseLogsRequest:\n type: object\n properties:\n id:\n type: string\n title: id\n minLength: 1\n description: Monitor ID to list response logs for (required).\n fromTimestamp:\n type:\n - integer\n - string\n - \"null\"\n title: from_timestamp\n format: int64\n description: Start of the response log window as Unix milliseconds within the 14-day retention window.\n toTimestamp:\n type:\n - integer\n - string\n - \"null\"\n title: to_timestamp\n format: int64\n description: End of the response log window as Unix milliseconds within the 14-day retention window.\n limit:\n type:\n - integer\n - \"null\"\n title: limit\n maximum: 100\n minimum: 1\n format: int32\n description: Maximum number of logs to return (1-100, defaults to 25).\n offset:\n type:\n - integer\n - \"null\"\n title: offset\n minimum: 0\n format: int32\n description: Number of logs to skip for pagination (defaults to 0).\n title: ListMonitorHTTPResponseLogsRequest\n additionalProperties: false\n description: ListMonitorHTTPResponseLogsRequest is the request to list response logs within the 14-day HTTP response-log window.\n openstatus.monitor.v1.ListMonitorHTTPResponseLogsResponse:\n type: object\n properties:\n logs:\n type: array\n items:\n $ref: '#/components/schemas/openstatus.monitor.v1.HTTPResponseLogListItem'\n title: logs\n description: Response logs.\n pagination:\n title: pagination\n description: Pagination metadata.\n $ref: '#/components/schemas/openstatus.monitor.v1.HTTPResponseLogPagination'\n title: ListMonitorHTTPResponseLogsResponse\n additionalProperties: false\n description: ListMonitorHTTPResponseLogsResponse is the response containing response logs.\n openstatus.monitor.v1.ListMonitorsRequest:\n type: object\n properties:\n limit:\n type:\n - integer\n - \"null\"\n title: limit\n maximum: 100\n minimum: 1\n format: int32\n description: Maximum number of monitors to return (1-100, defaults to 50).\n offset:\n type:\n - integer\n - \"null\"\n title: offset\n minimum: 0\n format: int32\n description: Number of monitors to skip for pagination (defaults to 0).\n title: ListMonitorsRequest\n additionalProperties: false\n description: ListMonitorsRequest is the request to list monitors.\n openstatus.monitor.v1.ListMonitorsResponse:\n type: object\n properties:\n httpMonitors:\n type: array\n items:\n $ref: '#/components/schemas/openstatus.monitor.v1.HTTPMonitor'\n title: http_monitors\n description: HTTP monitors in the workspace.\n tcpMonitors:\n type: array\n items:\n $ref: '#/components/schemas/openstatus.monitor.v1.TCPMonitor'\n title: tcp_monitors\n description: TCP monitors in the workspace.\n dnsMonitors:\n type: array\n items:\n $ref: '#/components/schemas/openstatus.monitor.v1.DNSMonitor'\n title: dns_monitors\n description: DNS monitors in the workspace.\n icmpMonitors:\n type: array\n items:\n $ref: '#/components/schemas/openstatus.monitor.v1.ICMPMonitor'\n title: icmp_monitors\n description: ICMP monitors in the workspace.\n grpcMonitors:\n type: array\n items:\n $ref: '#/components/schemas/openstatus.monitor.v1.GRPCMonitor'\n title: grpc_monitors\n description: gRPC monitors in the workspace.\n totalSize:\n type: integer\n title: total_size\n format: int32\n description: Total number of monitors across all types.\n title: ListMonitorsResponse\n additionalProperties: false\n description: ListMonitorsResponse is the response containing a list of monitors.\n openstatus.monitor.v1.MonitorConfig:\n type: object\n oneOf:\n - type: object\n properties:\n dns:\n title: dns\n description: DNS monitor configuration.\n $ref: '#/components/schemas/openstatus.monitor.v1.DNSMonitor'\n title: dns\n required:\n - dns\n - type: object\n properties:\n grpc:\n title: grpc\n description: gRPC monitor configuration.\n $ref: '#/components/schemas/openstatus.monitor.v1.GRPCMonitor'\n title: grpc\n required:\n - grpc\n - type: object\n properties:\n http:\n title: http\n description: HTTP monitor configuration.\n $ref: '#/components/schemas/openstatus.monitor.v1.HTTPMonitor'\n title: http\n required:\n - http\n - type: object\n properties:\n icmp:\n title: icmp\n description: ICMP monitor configuration.\n $ref: '#/components/schemas/openstatus.monitor.v1.ICMPMonitor'\n title: icmp\n required:\n - icmp\n - type: object\n properties:\n tcp:\n title: tcp\n description: TCP monitor configuration.\n $ref: '#/components/schemas/openstatus.monitor.v1.TCPMonitor'\n title: tcp\n required:\n - tcp\n title: MonitorConfig\n additionalProperties: false\n description: MonitorConfig represents the type-specific configuration for a monitor.\n openstatus.monitor.v1.MonitorStatus:\n type: string\n title: MonitorStatus\n enum:\n - MONITOR_STATUS_UNSPECIFIED\n - MONITOR_STATUS_ACTIVE\n - MONITOR_STATUS_DEGRADED\n - MONITOR_STATUS_ERROR\n description: MonitorStatus represents the operational status of a monitor.\n openstatus.monitor.v1.NumberComparator:\n type: string\n title: NumberComparator\n enum:\n - NUMBER_COMPARATOR_UNSPECIFIED\n - NUMBER_COMPARATOR_EQUAL\n - NUMBER_COMPARATOR_NOT_EQUAL\n - NUMBER_COMPARATOR_GREATER_THAN\n - NUMBER_COMPARATOR_GREATER_THAN_OR_EQUAL\n - NUMBER_COMPARATOR_LESS_THAN\n - NUMBER_COMPARATOR_LESS_THAN_OR_EQUAL\n description: NumberComparator defines comparison operations for numeric values.\n openstatus.monitor.v1.OpenTelemetryConfig:\n type: object\n properties:\n endpoint:\n type: string\n title: endpoint\n maxLength: 2048\n description: OTEL endpoint URL.\n headers:\n type: array\n items:\n $ref: '#/components/schemas/openstatus.monitor.v1.Headers'\n title: headers\n maxItems: 20\n description: Custom headers for OTEL requests.\n title: OpenTelemetryConfig\n additionalProperties: false\n description: OpenTelemetry configuration for exporting metrics.\n openstatus.monitor.v1.Periodicity:\n type: string\n title: Periodicity\n enum:\n - PERIODICITY_UNSPECIFIED\n - PERIODICITY_30S\n - PERIODICITY_1M\n - PERIODICITY_5M\n - PERIODICITY_10M\n - PERIODICITY_30M\n - PERIODICITY_1H\n description: Monitor periodicity options.\n openstatus.monitor.v1.RecordAssertion:\n type: object\n properties:\n record:\n type: string\n title: record\n enum:\n - A\n - AAAA\n - CNAME\n - MX\n - TXT\n description: DNS record type (e.g., \"A\", \"AAAA\", \"CNAME\", \"MX\", \"TXT\").\n comparator:\n not:\n enum:\n - RECORD_COMPARATOR_UNSPECIFIED\n title: comparator\n description: Comparison operation (required, must not be UNSPECIFIED).\n $ref: '#/components/schemas/openstatus.monitor.v1.RecordComparator'\n target:\n type: string\n title: target\n description: Target value to compare against.\n title: RecordAssertion\n additionalProperties: false\n description: RecordAssertion defines an assertion for DNS records.\n openstatus.monitor.v1.RecordComparator:\n type: string\n title: RecordComparator\n enum:\n - RECORD_COMPARATOR_UNSPECIFIED\n - RECORD_COMPARATOR_EQUAL\n - RECORD_COMPARATOR_NOT_EQUAL\n - RECORD_COMPARATOR_CONTAINS\n - RECORD_COMPARATOR_NOT_CONTAINS\n description: RecordComparator defines comparison operations for DNS records.\n openstatus.monitor.v1.Region:\n type: string\n title: Region\n enum:\n - REGION_UNSPECIFIED\n - REGION_FLY_AMS\n - REGION_FLY_ARN\n - REGION_FLY_BOM\n - REGION_FLY_CDG\n - REGION_FLY_DFW\n - REGION_FLY_EWR\n - REGION_FLY_FRA\n - REGION_FLY_GRU\n - REGION_FLY_IAD\n - REGION_FLY_JNB\n - REGION_FLY_LAX\n - REGION_FLY_LHR\n - REGION_FLY_NRT\n - REGION_FLY_ORD\n - REGION_FLY_SJC\n - REGION_FLY_SIN\n - REGION_FLY_SYD\n - REGION_FLY_YYZ\n - REGION_KOYEB_FRA\n - REGION_KOYEB_PAR\n - REGION_KOYEB_SFO\n - REGION_KOYEB_SIN\n - REGION_KOYEB_TYO\n - REGION_KOYEB_WAS\n - REGION_RAILWAY_US_WEST2\n - REGION_RAILWAY_US_EAST4\n - REGION_RAILWAY_EUROPE_WEST4\n - REGION_RAILWAY_ASIA_SOUTHEAST1\n description: |-\n Geographic regions where monitors can run checks from.\n REGION_FLY_BOM is deprecated and rejected on create/update; use REGION_FLY_SIN.\n openstatus.monitor.v1.RegionStatus:\n type: object\n properties:\n region:\n title: region\n description: The region identifier.\n $ref: '#/components/schemas/openstatus.monitor.v1.Region'\n status:\n title: status\n description: The status of the monitor in this region.\n $ref: '#/components/schemas/openstatus.monitor.v1.MonitorStatus'\n title: RegionStatus\n additionalProperties: false\n description: RegionStatus represents the status of a monitor in a specific region.\n openstatus.monitor.v1.StatusCodeAssertion:\n type: object\n properties:\n target:\n type:\n - integer\n - string\n title: target\n maximum: 599\n minimum: 100\n format: int64\n description: Target status code to compare against (100-599).\n comparator:\n not:\n enum:\n - NUMBER_COMPARATOR_UNSPECIFIED\n title: comparator\n description: Comparison operation (required, must not be UNSPECIFIED).\n $ref: '#/components/schemas/openstatus.monitor.v1.NumberComparator'\n title: StatusCodeAssertion\n additionalProperties: false\n description: StatusCodeAssertion defines an assertion for HTTP status codes.\n openstatus.monitor.v1.StringComparator:\n type: string\n title: StringComparator\n enum:\n - STRING_COMPARATOR_UNSPECIFIED\n - STRING_COMPARATOR_CONTAINS\n - STRING_COMPARATOR_NOT_CONTAINS\n - STRING_COMPARATOR_EQUAL\n - STRING_COMPARATOR_NOT_EQUAL\n - STRING_COMPARATOR_EMPTY\n - STRING_COMPARATOR_NOT_EMPTY\n - STRING_COMPARATOR_GREATER_THAN\n - STRING_COMPARATOR_GREATER_THAN_OR_EQUAL\n - STRING_COMPARATOR_LESS_THAN\n - STRING_COMPARATOR_LESS_THAN_OR_EQUAL\n description: StringComparator defines comparison operations for string values.\n openstatus.monitor.v1.TCPMonitor:\n type: object\n properties:\n id:\n type: string\n title: id\n description: Unique identifier for the monitor (output only for create requests).\n name:\n type: string\n examples:\n - Database Connection Check\n title: name\n maxLength: 256\n minLength: 1\n description: Name of the monitor (required, max 256 characters).\n uri:\n type: string\n examples:\n - tcp://db.example.com:5432\n title: uri\n maxLength: 2048\n minLength: 1\n description: URI to monitor in format \"host:port\" (required, max 2048 characters).\n periodicity:\n not:\n enum:\n - PERIODICITY_UNSPECIFIED\n title: periodicity\n description: Check periodicity (required).\n $ref: '#/components/schemas/openstatus.monitor.v1.Periodicity'\n timeout:\n type:\n - integer\n - string\n title: timeout\n maximum: 120000\n minimum: 0\n format: int64\n description: Timeout in milliseconds (0-120000, defaults to 45000).\n degradedAt:\n type:\n - integer\n - string\n - \"null\"\n title: degraded_at\n maximum: 120000\n minimum: 0\n format: int64\n description: Latency threshold for degraded status in milliseconds (optional, 0-120000).\n retry:\n type:\n - integer\n - string\n title: retry\n maximum: 10\n minimum: 0\n format: int64\n description: Number of retry attempts (0-10, defaults to 3).\n description:\n type:\n - string\n - \"null\"\n title: description\n maxLength: 1024\n description: Description of the monitor (optional).\n active:\n type:\n - boolean\n - \"null\"\n title: active\n description: Whether the monitor is active (defaults to false).\n public:\n type:\n - boolean\n - \"null\"\n title: public\n description: Whether the monitor is publicly visible (defaults to false).\n regions:\n type: array\n items:\n $ref: '#/components/schemas/openstatus.monitor.v1.Region'\n title: regions\n maxItems: 28\n description: Geographic regions to run checks from.\n openTelemetry:\n title: open_telemetry\n description: OpenTelemetry configuration for exporting metrics.\n $ref: '#/components/schemas/openstatus.monitor.v1.OpenTelemetryConfig'\n status:\n title: status\n description: Current operational status of the monitor.\n $ref: '#/components/schemas/openstatus.monitor.v1.MonitorStatus'\n privateLocationIds:\n type: array\n items:\n type: string\n readOnly: true\n title: private_location_ids\n description: IDs of private locations that run this monitor. Read-only.\n readOnly: true\n title: TCPMonitor\n additionalProperties: false\n description: TCPMonitor defines the configuration for a TCP monitor.\n openstatus.monitor.v1.TimeRange:\n type: string\n title: TimeRange\n enum:\n - TIME_RANGE_UNSPECIFIED\n - TIME_RANGE_1D\n - TIME_RANGE_7D\n - TIME_RANGE_14D\n description: TimeRange represents the time period for metrics aggregation.\n openstatus.monitor.v1.TriggerMonitorRequest:\n type: object\n properties:\n id:\n type: string\n title: id\n minLength: 1\n description: Monitor ID to trigger (required).\n title: TriggerMonitorRequest\n additionalProperties: false\n description: TriggerMonitorRequest is the request to trigger a monitor check.\n openstatus.monitor.v1.TriggerMonitorResponse:\n type: object\n properties:\n success:\n type: boolean\n title: success\n description: Whether the trigger was successful.\n title: TriggerMonitorResponse\n additionalProperties: false\n description: TriggerMonitorResponse is the response after triggering a monitor.\n openstatus.monitor.v1.UpdateDNSMonitorRequest:\n type: object\n properties:\n id:\n type: string\n title: id\n minLength: 1\n description: Monitor ID to update (required).\n monitor:\n oneOf:\n - $ref: '#/components/schemas/openstatus.monitor.v1.DNSMonitor'\n - type: \"null\"\n title: monitor\n description: Updated monitor configuration (all fields optional for partial updates).\n title: UpdateDNSMonitorRequest\n additionalProperties: false\n description: UpdateDNSMonitorRequest is the request to update an existing DNS monitor.\n openstatus.monitor.v1.UpdateDNSMonitorResponse:\n type: object\n properties:\n monitor:\n title: monitor\n description: The updated monitor.\n $ref: '#/components/schemas/openstatus.monitor.v1.DNSMonitor'\n title: UpdateDNSMonitorResponse\n additionalProperties: false\n description: UpdateDNSMonitorResponse is the response after updating a DNS monitor.\n openstatus.monitor.v1.UpdateGRPCMonitorRequest:\n type: object\n properties:\n id:\n type: string\n title: id\n minLength: 1\n description: Monitor ID to update (required).\n monitor:\n oneOf:\n - $ref: '#/components/schemas/openstatus.monitor.v1.GRPCMonitor'\n - type: \"null\"\n title: monitor\n description: Updated monitor configuration (all fields optional for partial updates).\n title: UpdateGRPCMonitorRequest\n additionalProperties: false\n description: UpdateGRPCMonitorRequest is the request to update an existing gRPC monitor.\n openstatus.monitor.v1.UpdateGRPCMonitorResponse:\n type: object\n properties:\n monitor:\n title: monitor\n description: The updated monitor.\n $ref: '#/components/schemas/openstatus.monitor.v1.GRPCMonitor'\n title: UpdateGRPCMonitorResponse\n additionalProperties: false\n description: UpdateGRPCMonitorResponse is the response after updating a gRPC monitor.\n openstatus.monitor.v1.UpdateHTTPMonitorRequest:\n type: object\n properties:\n id:\n type: string\n title: id\n minLength: 1\n description: Monitor ID to update (required).\n monitor:\n oneOf:\n - $ref: '#/components/schemas/openstatus.monitor.v1.HTTPMonitor'\n - type: \"null\"\n title: monitor\n description: Updated monitor configuration (all fields optional for partial updates).\n title: UpdateHTTPMonitorRequest\n additionalProperties: false\n description: UpdateHTTPMonitorRequest is the request to update an existing HTTP monitor.\n openstatus.monitor.v1.UpdateHTTPMonitorResponse:\n type: object\n properties:\n monitor:\n title: monitor\n description: The updated monitor.\n $ref: '#/components/schemas/openstatus.monitor.v1.HTTPMonitor'\n title: UpdateHTTPMonitorResponse\n additionalProperties: false\n description: UpdateHTTPMonitorResponse is the response after updating an HTTP monitor.\n openstatus.monitor.v1.UpdateICMPMonitorRequest:\n type: object\n properties:\n id:\n type: string\n title: id\n minLength: 1\n description: Monitor ID to update (required).\n monitor:\n oneOf:\n - $ref: '#/components/schemas/openstatus.monitor.v1.ICMPMonitor'\n - type: \"null\"\n title: monitor\n description: Updated monitor configuration (all fields optional for partial updates).\n title: UpdateICMPMonitorRequest\n additionalProperties: false\n description: UpdateICMPMonitorRequest is the request to update an existing ICMP monitor.\n openstatus.monitor.v1.UpdateICMPMonitorResponse:\n type: object\n properties:\n monitor:\n title: monitor\n description: The updated monitor.\n $ref: '#/components/schemas/openstatus.monitor.v1.ICMPMonitor'\n title: UpdateICMPMonitorResponse\n additionalProperties: false\n description: UpdateICMPMonitorResponse is the response after updating an ICMP monitor.\n openstatus.monitor.v1.UpdateTCPMonitorRequest:\n type: object\n properties:\n id:\n type: string\n title: id\n minLength: 1\n description: Monitor ID to update (required).\n monitor:\n oneOf:\n - $ref: '#/components/schemas/openstatus.monitor.v1.TCPMonitor'\n - type: \"null\"\n title: monitor\n description: Updated monitor configuration (all fields optional for partial updates).\n title: UpdateTCPMonitorRequest\n additionalProperties: false\n description: UpdateTCPMonitorRequest is the request to update an existing TCP monitor.\n openstatus.monitor.v1.UpdateTCPMonitorResponse:\n type: object\n properties:\n monitor:\n title: monitor\n description: The updated monitor.\n $ref: '#/components/schemas/openstatus.monitor.v1.TCPMonitor'\n title: UpdateTCPMonitorResponse\n additionalProperties: false\n description: UpdateTCPMonitorResponse is the response after updating a TCP monitor.\n openstatus.notification.v1.CheckNotificationLimitRequest:\n type: object\n title: CheckNotificationLimitRequest\n additionalProperties: false\n description: CheckNotificationLimitRequest is the request to check notification limits.\n openstatus.notification.v1.CheckNotificationLimitResponse:\n type: object\n properties:\n limitReached:\n type: boolean\n title: limit_reached\n description: Whether the workspace has reached its notification limit.\n currentCount:\n type: integer\n title: current_count\n format: int32\n description: Current number of notification channels.\n maxCount:\n type: integer\n title: max_count\n format: int32\n description: Maximum allowed notification channels.\n title: CheckNotificationLimitResponse\n additionalProperties: false\n description: CheckNotificationLimitResponse is the response containing limit information.\n openstatus.notification.v1.CreateNotificationRequest:\n type: object\n properties:\n name:\n type: string\n examples:\n - Slack Ops Channel\n title: name\n minLength: 1\n description: Display name for the notification channel.\n provider:\n not:\n enum:\n - NOTIFICATION_PROVIDER_UNSPECIFIED\n title: provider\n description: Provider type.\n $ref: '#/components/schemas/openstatus.notification.v1.NotificationProvider'\n data:\n title: data\n description: Provider-specific configuration.\n $ref: '#/components/schemas/openstatus.notification.v1.NotificationData'\n monitorIds:\n type: array\n items:\n type: string\n title: monitor_ids\n description: IDs of monitors to associate with this notification.\n title: CreateNotificationRequest\n required:\n - data\n additionalProperties: false\n description: CreateNotificationRequest is the request to create a new notification channel.\n openstatus.notification.v1.CreateNotificationResponse:\n type: object\n properties:\n notification:\n title: notification\n description: The created notification channel.\n $ref: '#/components/schemas/openstatus.notification.v1.Notification'\n title: CreateNotificationResponse\n additionalProperties: false\n description: CreateNotificationResponse is the response after creating a notification channel.\n openstatus.notification.v1.DeleteNotificationRequest:\n type: object\n properties:\n id:\n type: string\n title: id\n minLength: 1\n description: Notification ID to delete (required).\n title: DeleteNotificationRequest\n additionalProperties: false\n description: DeleteNotificationRequest is the request to delete a notification channel.\n openstatus.notification.v1.DeleteNotificationResponse:\n type: object\n properties:\n success:\n type: boolean\n title: success\n description: Whether the deletion was successful.\n title: DeleteNotificationResponse\n additionalProperties: false\n description: DeleteNotificationResponse is the response after deleting a notification channel.\n openstatus.notification.v1.DiscordData:\n type: object\n properties:\n webhookUrl:\n type: string\n examples:\n - https://discord.com/api/webhooks/123/abc\n title: webhook_url\n format: uri\n description: Discord webhook URL.\n title: DiscordData\n additionalProperties: false\n description: DiscordData contains configuration for Discord notifications.\n openstatus.notification.v1.EmailData:\n type: object\n properties:\n email:\n type: string\n examples:\n - ops-team@example.com\n title: email\n format: email\n description: Email address to send notifications to.\n title: EmailData\n additionalProperties: false\n description: EmailData contains configuration for email notifications.\n openstatus.notification.v1.GetNotificationRequest:\n type: object\n properties:\n id:\n type: string\n title: id\n minLength: 1\n description: Notification ID to retrieve (required).\n title: GetNotificationRequest\n additionalProperties: false\n description: GetNotificationRequest is the request to get a notification channel.\n openstatus.notification.v1.GetNotificationResponse:\n type: object\n properties:\n notification:\n title: notification\n description: The notification channel.\n $ref: '#/components/schemas/openstatus.notification.v1.Notification'\n title: GetNotificationResponse\n additionalProperties: false\n description: GetNotificationResponse is the response containing the notification channel.\n openstatus.notification.v1.GoogleChatData:\n type: object\n properties:\n webhookUrl:\n type: string\n title: webhook_url\n format: uri\n description: Google Chat webhook URL.\n title: GoogleChatData\n additionalProperties: false\n description: GoogleChatData contains configuration for Google Chat notifications.\n openstatus.notification.v1.GrafanaOncallData:\n type: object\n properties:\n webhookUrl:\n type: string\n title: webhook_url\n format: uri\n description: Grafana OnCall webhook URL.\n title: GrafanaOncallData\n additionalProperties: false\n description: GrafanaOncallData contains configuration for Grafana OnCall notifications.\n openstatus.notification.v1.ListNotificationsRequest:\n type: object\n properties:\n limit:\n type:\n - integer\n - \"null\"\n title: limit\n maximum: 100\n minimum: 1\n format: int32\n description: Maximum number of notifications to return (1-100, defaults to 50).\n offset:\n type:\n - integer\n - \"null\"\n title: offset\n minimum: 0\n format: int32\n description: Number of notifications to skip for pagination (defaults to 0).\n title: ListNotificationsRequest\n additionalProperties: false\n description: ListNotificationsRequest is the request to list notification channels.\n openstatus.notification.v1.ListNotificationsResponse:\n type: object\n properties:\n notifications:\n type: array\n items:\n $ref: '#/components/schemas/openstatus.notification.v1.NotificationSummary'\n title: notifications\n description: Notification channel summaries.\n totalSize:\n type: integer\n title: total_size\n format: int32\n description: Total number of notification channels.\n title: ListNotificationsResponse\n additionalProperties: false\n description: ListNotificationsResponse is the response containing notification channels.\n openstatus.notification.v1.MsTeamsData:\n type: object\n properties:\n webhookUrl:\n type: string\n examples:\n - https://prod-00.westeurope.logic.azure.com:443/workflows/abc/triggers/manual/paths/invoke\n title: webhook_url\n format: uri\n description: Microsoft Teams webhook URL (Power Automate Workflows).\n title: MsTeamsData\n additionalProperties: false\n description: MsTeamsData contains configuration for Microsoft Teams notifications.\n openstatus.notification.v1.Notification:\n type: object\n properties:\n id:\n type: string\n title: id\n description: Unique identifier for the notification.\n name:\n type: string\n title: name\n description: Display name for the notification channel.\n provider:\n title: provider\n description: Provider type.\n $ref: '#/components/schemas/openstatus.notification.v1.NotificationProvider'\n data:\n title: data\n description: Provider-specific configuration.\n $ref: '#/components/schemas/openstatus.notification.v1.NotificationData'\n monitorIds:\n type: array\n items:\n type: string\n title: monitor_ids\n description: IDs of monitors associated with this notification.\n createdAt:\n type: string\n title: created_at\n description: Timestamp when the notification was created (RFC 3339).\n updatedAt:\n type: string\n title: updated_at\n description: Timestamp when the notification was last updated (RFC 3339).\n title: Notification\n additionalProperties: false\n description: Notification represents a notification channel with full details.\n openstatus.notification.v1.NotificationData:\n type: object\n oneOf:\n - type: object\n properties:\n discord:\n title: discord\n description: Discord configuration.\n $ref: '#/components/schemas/openstatus.notification.v1.DiscordData'\n title: discord\n required:\n - discord\n - type: object\n properties:\n email:\n title: email\n description: Email configuration.\n $ref: '#/components/schemas/openstatus.notification.v1.EmailData'\n title: email\n required:\n - email\n - type: object\n properties:\n googleChat:\n title: google_chat\n description: Google Chat configuration.\n $ref: '#/components/schemas/openstatus.notification.v1.GoogleChatData'\n title: google_chat\n required:\n - googleChat\n - type: object\n properties:\n grafanaOncall:\n title: grafana_oncall\n description: Grafana OnCall configuration.\n $ref: '#/components/schemas/openstatus.notification.v1.GrafanaOncallData'\n title: grafana_oncall\n required:\n - grafanaOncall\n - type: object\n properties:\n msTeams:\n title: ms_teams\n description: Microsoft Teams configuration.\n $ref: '#/components/schemas/openstatus.notification.v1.MsTeamsData'\n title: ms_teams\n required:\n - msTeams\n - type: object\n properties:\n ntfy:\n title: ntfy\n description: Ntfy configuration.\n $ref: '#/components/schemas/openstatus.notification.v1.NtfyData'\n title: ntfy\n required:\n - ntfy\n - type: object\n properties:\n opsgenie:\n title: opsgenie\n description: Opsgenie configuration.\n $ref: '#/components/schemas/openstatus.notification.v1.OpsgenieData'\n title: opsgenie\n required:\n - opsgenie\n - type: object\n properties:\n pagerduty:\n title: pagerduty\n description: PagerDuty configuration.\n $ref: '#/components/schemas/openstatus.notification.v1.PagerDutyData'\n title: pagerduty\n required:\n - pagerduty\n - type: object\n properties:\n slack:\n title: slack\n description: Slack configuration.\n $ref: '#/components/schemas/openstatus.notification.v1.SlackData'\n title: slack\n required:\n - slack\n - type: object\n properties:\n sms:\n title: sms\n description: 'Deprecated: SMS is no longer offered; use whatsapp.'\n deprecated: true\n $ref: '#/components/schemas/openstatus.notification.v1.SmsData'\n title: sms\n required:\n - sms\n - type: object\n properties:\n telegram:\n title: telegram\n description: Telegram configuration.\n $ref: '#/components/schemas/openstatus.notification.v1.TelegramData'\n title: telegram\n required:\n - telegram\n - type: object\n properties:\n webhook:\n title: webhook\n description: Webhook configuration.\n $ref: '#/components/schemas/openstatus.notification.v1.WebhookData'\n title: webhook\n required:\n - webhook\n - type: object\n properties:\n whatsapp:\n title: whatsapp\n description: WhatsApp configuration.\n $ref: '#/components/schemas/openstatus.notification.v1.WhatsappData'\n title: whatsapp\n required:\n - whatsapp\n title: NotificationData\n additionalProperties: false\n description: NotificationData is a union of provider-specific configuration.\n openstatus.notification.v1.NotificationProvider:\n type: string\n title: NotificationProvider\n enum:\n - NOTIFICATION_PROVIDER_UNSPECIFIED\n - NOTIFICATION_PROVIDER_DISCORD\n - NOTIFICATION_PROVIDER_EMAIL\n - NOTIFICATION_PROVIDER_GOOGLE_CHAT\n - NOTIFICATION_PROVIDER_GRAFANA_ONCALL\n - NOTIFICATION_PROVIDER_NTFY\n - NOTIFICATION_PROVIDER_PAGERDUTY\n - NOTIFICATION_PROVIDER_OPSGENIE\n - NOTIFICATION_PROVIDER_SLACK\n - NOTIFICATION_PROVIDER_SMS\n - NOTIFICATION_PROVIDER_TELEGRAM\n - NOTIFICATION_PROVIDER_WEBHOOK\n - NOTIFICATION_PROVIDER_WHATSAPP\n - NOTIFICATION_PROVIDER_MS_TEAMS\n description: NotificationProvider represents the supported notification channel types.\n openstatus.notification.v1.NotificationSummary:\n type: object\n properties:\n id:\n type: string\n title: id\n description: Unique identifier for the notification.\n name:\n type: string\n title: name\n description: Display name for the notification channel.\n provider:\n title: provider\n description: Provider type.\n $ref: '#/components/schemas/openstatus.notification.v1.NotificationProvider'\n monitorCount:\n type: integer\n title: monitor_count\n format: int32\n description: Number of monitors associated with this notification.\n createdAt:\n type: string\n title: created_at\n description: Timestamp when the notification was created (RFC 3339).\n updatedAt:\n type: string\n title: updated_at\n description: Timestamp when the notification was last updated (RFC 3339).\n title: NotificationSummary\n additionalProperties: false\n description: NotificationSummary represents a notification channel summary for list responses.\n openstatus.notification.v1.NtfyData:\n type: object\n properties:\n topic:\n type: string\n examples:\n - openstatus-alerts\n title: topic\n minLength: 1\n description: Ntfy topic to publish to.\n serverUrl:\n type: string\n title: server_url\n description: Ntfy server URL (defaults to https://ntfy.sh).\n token:\n type:\n - string\n - \"null\"\n title: token\n description: Optional authentication token.\n title: NtfyData\n additionalProperties: false\n description: NtfyData contains configuration for Ntfy notifications.\n openstatus.notification.v1.OpsgenieData:\n type: object\n properties:\n apiKey:\n type: string\n title: api_key\n minLength: 1\n description: Opsgenie API key.\n region:\n title: region\n description: Opsgenie region.\n $ref: '#/components/schemas/openstatus.notification.v1.OpsgenieRegion'\n title: OpsgenieData\n additionalProperties: false\n description: OpsgenieData contains configuration for Opsgenie notifications.\n openstatus.notification.v1.OpsgenieRegion:\n type: string\n title: OpsgenieRegion\n enum:\n - OPSGENIE_REGION_UNSPECIFIED\n - OPSGENIE_REGION_US\n - OPSGENIE_REGION_EU\n description: OpsgenieRegion represents the Opsgenie API region.\n openstatus.notification.v1.PagerDutyData:\n type: object\n properties:\n integrationKey:\n type: string\n examples:\n - a1b2c3d4e5f6g7h8i9j0\n title: integration_key\n minLength: 1\n description: PagerDuty integration key.\n title: PagerDutyData\n additionalProperties: false\n description: PagerDutyData contains configuration for PagerDuty notifications.\n openstatus.notification.v1.SendTestNotificationRequest:\n type: object\n properties:\n provider:\n not:\n enum:\n - NOTIFICATION_PROVIDER_UNSPECIFIED\n title: provider\n description: Provider type.\n $ref: '#/components/schemas/openstatus.notification.v1.NotificationProvider'\n data:\n title: data\n description: Provider-specific configuration.\n $ref: '#/components/schemas/openstatus.notification.v1.NotificationData'\n title: SendTestNotificationRequest\n required:\n - data\n additionalProperties: false\n description: SendTestNotificationRequest is the request to send a test notification.\n openstatus.notification.v1.SendTestNotificationResponse:\n type: object\n properties:\n success:\n type: boolean\n title: success\n description: Whether the test was successful.\n errorMessage:\n type:\n - string\n - \"null\"\n title: error_message\n description: Optional error message if the test failed.\n title: SendTestNotificationResponse\n additionalProperties: false\n description: SendTestNotificationResponse is the response after sending a test notification.\n openstatus.notification.v1.SlackData:\n type: object\n properties:\n webhookUrl:\n type: string\n examples:\n - https://hooks.slack.com/services/T00/B00/xxx\n title: webhook_url\n format: uri\n description: Slack webhook URL.\n title: SlackData\n additionalProperties: false\n description: SlackData contains configuration for Slack notifications.\n openstatus.notification.v1.SmsData:\n type: object\n properties:\n phoneNumber:\n type: string\n examples:\n - \"+14155551234\"\n title: phone_number\n minLength: 1\n description: Phone number to send SMS to.\n title: SmsData\n additionalProperties: false\n description: 'Deprecated: SMS is no longer offered; use WhatsappData. Kept for existing channels.'\n deprecated: true\n openstatus.notification.v1.TelegramData:\n type: object\n properties:\n chatId:\n type: string\n examples:\n - \"-1001234567890\"\n title: chat_id\n minLength: 1\n description: Telegram chat ID.\n title: TelegramData\n additionalProperties: false\n description: TelegramData contains configuration for Telegram notifications.\n openstatus.notification.v1.UpdateNotificationRequest:\n type: object\n properties:\n id:\n type: string\n title: id\n minLength: 1\n description: Notification ID to update (required).\n name:\n type:\n - string\n - \"null\"\n title: name\n description: Updated display name.\n data:\n oneOf:\n - $ref: '#/components/schemas/openstatus.notification.v1.NotificationData'\n - type: \"null\"\n title: data\n description: Updated provider-specific configuration.\n monitorIds:\n type: array\n items:\n type: string\n title: monitor_ids\n description: Updated monitor IDs to associate.\n updateMonitorIds:\n type:\n - boolean\n - \"null\"\n title: update_monitor_ids\n description: |-\n Set to true to update monitor associations.\n When true, monitor_ids replaces the existing list (empty clears all).\n When false or unset, monitor_ids is ignored and existing associations are preserved.\n title: UpdateNotificationRequest\n additionalProperties: false\n description: UpdateNotificationRequest is the request to update a notification channel.\n openstatus.notification.v1.UpdateNotificationResponse:\n type: object\n properties:\n notification:\n title: notification\n description: The updated notification channel.\n $ref: '#/components/schemas/openstatus.notification.v1.Notification'\n title: UpdateNotificationResponse\n additionalProperties: false\n description: UpdateNotificationResponse is the response after updating a notification channel.\n openstatus.notification.v1.WebhookData:\n type: object\n properties:\n endpoint:\n type: string\n examples:\n - https://api.example.com/webhooks/openstatus\n title: endpoint\n format: uri\n description: Webhook endpoint URL.\n headers:\n type: array\n items:\n $ref: '#/components/schemas/openstatus.notification.v1.WebhookHeader'\n title: headers\n description: Optional custom headers.\n title: WebhookData\n additionalProperties: false\n description: WebhookData contains configuration for custom webhook notifications.\n openstatus.notification.v1.WebhookHeader:\n type: object\n properties:\n key:\n type: string\n title: key\n minLength: 1\n description: Header name.\n value:\n type: string\n title: value\n description: Header value.\n title: WebhookHeader\n additionalProperties: false\n description: WebhookHeader represents a custom header for webhook requests.\n openstatus.notification.v1.WhatsappData:\n type: object\n properties:\n phoneNumber:\n type: string\n title: phone_number\n minLength: 1\n description: Phone number to send WhatsApp messages to.\n title: WhatsappData\n additionalProperties: false\n description: WhatsappData contains configuration for WhatsApp notifications.\n openstatus.private_location.v1.CreatePrivateLocationRequest:\n type: object\n properties:\n name:\n type: string\n examples:\n - eu-west-agent\n title: name\n maxLength: 256\n minLength: 1\n description: Display name for the private location (required, 1-256 characters).\n monitorIds:\n type: array\n items:\n type: string\n title: monitor_ids\n description: IDs of monitors this private location should run (optional).\n metadata:\n type: object\n title: metadata\n maxProperties: 20\n additionalProperties:\n type: string\n title: value\n maxLength: 256\n description: User-defined key/value labels (optional, up to 20 entries).\n title: CreatePrivateLocationRequest\n additionalProperties: false\n description: CreatePrivateLocationRequest is the request to create a new private location.\n openstatus.private_location.v1.CreatePrivateLocationRequest.MetadataEntry:\n type: object\n properties:\n key:\n type: string\n title: key\n value:\n type: string\n title: value\n title: MetadataEntry\n additionalProperties: false\n openstatus.private_location.v1.CreatePrivateLocationResponse:\n type: object\n properties:\n privateLocation:\n title: private_location\n description: The created private location, including its generated agent token.\n $ref: '#/components/schemas/openstatus.private_location.v1.PrivateLocation'\n title: CreatePrivateLocationResponse\n additionalProperties: false\n description: CreatePrivateLocationResponse is the response after creating a private location.\n openstatus.private_location.v1.DeletePrivateLocationRequest:\n type: object\n properties:\n id:\n type: string\n title: id\n minLength: 1\n description: ID of the private location to delete (required).\n title: DeletePrivateLocationRequest\n additionalProperties: false\n description: DeletePrivateLocationRequest is the request to delete a private location.\n openstatus.private_location.v1.DeletePrivateLocationResponse:\n type: object\n properties:\n success:\n type: boolean\n title: success\n description: Whether the deletion was successful.\n title: DeletePrivateLocationResponse\n additionalProperties: false\n description: DeletePrivateLocationResponse is the response after deleting a private location.\n openstatus.private_location.v1.GetPrivateLocationRequest:\n type: object\n properties:\n id:\n type: string\n title: id\n minLength: 1\n description: ID of the private location to retrieve (required).\n title: GetPrivateLocationRequest\n additionalProperties: false\n description: GetPrivateLocationRequest is the request to get a private location by ID.\n openstatus.private_location.v1.GetPrivateLocationResponse:\n type: object\n properties:\n privateLocation:\n title: private_location\n description: The requested private location.\n $ref: '#/components/schemas/openstatus.private_location.v1.PrivateLocation'\n title: GetPrivateLocationResponse\n additionalProperties: false\n description: GetPrivateLocationResponse is the response containing the private location.\n openstatus.private_location.v1.ListPrivateLocationsRequest:\n type: object\n properties:\n limit:\n type:\n - integer\n - \"null\"\n title: limit\n maximum: 100\n minimum: 1\n format: int32\n description: Maximum number of private locations to return (1-100, defaults to 50).\n offset:\n type:\n - integer\n - \"null\"\n title: offset\n minimum: 0\n format: int32\n description: Number of private locations to skip for pagination (defaults to 0).\n title: ListPrivateLocationsRequest\n additionalProperties: false\n description: ListPrivateLocationsRequest is the request to list private locations.\n openstatus.private_location.v1.ListPrivateLocationsResponse:\n type: object\n properties:\n privateLocations:\n type: array\n items:\n $ref: '#/components/schemas/openstatus.private_location.v1.PrivateLocationSummary'\n title: private_locations\n description: List of private locations.\n totalSize:\n type: integer\n title: total_size\n format: int32\n description: Total number of private locations in the workspace.\n title: ListPrivateLocationsResponse\n additionalProperties: false\n description: ListPrivateLocationsResponse is the response containing private location summaries.\n openstatus.private_location.v1.PrivateLocation:\n type: object\n properties:\n id:\n type: string\n title: id\n description: Unique identifier for the private location.\n name:\n type: string\n title: name\n description: Display name for the private location.\n token:\n type: string\n title: token\n description: |-\n Agent credential. Generated by the server on creation and used by the\n agent to authenticate; treat it as a secret.\n monitorIds:\n type: array\n items:\n type: string\n title: monitor_ids\n description: IDs of monitors this private location runs.\n lastSeenAt:\n type: string\n title: last_seen_at\n description: Last time the agent reported a result (RFC 3339), empty if it never has.\n createdAt:\n type: string\n title: created_at\n description: Timestamp when the private location was created (RFC 3339 format).\n updatedAt:\n type: string\n title: updated_at\n description: Timestamp when the private location was last updated (RFC 3339 format).\n metadata:\n type: object\n title: metadata\n additionalProperties:\n type: string\n title: value\n description: User-defined key/value labels attached to this private location.\n status:\n title: status\n description: Computed health of the agent. Read-only.\n $ref: '#/components/schemas/openstatus.private_location.v1.PrivateLocationStatus'\n title: PrivateLocation\n additionalProperties: false\n description: PrivateLocation represents a self-hosted checker agent with full details.\n openstatus.private_location.v1.PrivateLocation.MetadataEntry:\n type: object\n properties:\n key:\n type: string\n title: key\n value:\n type: string\n title: value\n title: MetadataEntry\n additionalProperties: false\n openstatus.private_location.v1.PrivateLocationStatus:\n type: string\n title: PrivateLocationStatus\n enum:\n - PRIVATE_LOCATION_STATUS_UNSPECIFIED\n - PRIVATE_LOCATION_STATUS_ACTIVE\n - PRIVATE_LOCATION_STATUS_ERROR\n description: |-\n PrivateLocationStatus is the computed health of a private location, derived\n from the agent heartbeat. Read-only — it cannot be set through the API.\n openstatus.private_location.v1.PrivateLocationSummary:\n type: object\n properties:\n id:\n type: string\n title: id\n description: Unique identifier for the private location.\n name:\n type: string\n title: name\n description: Display name for the private location.\n monitorCount:\n type: integer\n title: monitor_count\n format: int32\n description: Number of monitors this private location runs.\n lastSeenAt:\n type: string\n title: last_seen_at\n description: Last time the agent reported a result (RFC 3339), empty if it never has.\n createdAt:\n type: string\n title: created_at\n description: Timestamp when the private location was created (RFC 3339 format).\n updatedAt:\n type: string\n title: updated_at\n description: Timestamp when the private location was last updated (RFC 3339 format).\n status:\n title: status\n description: Computed health of the agent. Read-only.\n $ref: '#/components/schemas/openstatus.private_location.v1.PrivateLocationStatus'\n metadata:\n type: object\n title: metadata\n additionalProperties:\n type: string\n title: value\n description: User-defined key/value labels attached to this private location.\n title: PrivateLocationSummary\n additionalProperties: false\n description: |-\n PrivateLocationSummary represents metadata for a private location (used in\n list responses). The agent token is intentionally omitted.\n openstatus.private_location.v1.PrivateLocationSummary.MetadataEntry:\n type: object\n properties:\n key:\n type: string\n title: key\n value:\n type: string\n title: value\n title: MetadataEntry\n additionalProperties: false\n openstatus.private_location.v1.UpdatePrivateLocationRequest:\n type: object\n properties:\n id:\n type: string\n title: id\n minLength: 1\n description: ID of the private location to update (required).\n name:\n type:\n - string\n - \"null\"\n title: name\n maxLength: 256\n minLength: 1\n description: New display name for the private location (optional).\n monitorIds:\n type: array\n items:\n type: string\n title: monitor_ids\n description: New list of monitor IDs. Only applied when update_monitor_ids is true.\n updateMonitorIds:\n type:\n - boolean\n - \"null\"\n title: update_monitor_ids\n description: |-\n When true, monitor_ids replaces the current associations (an empty list\n clears all). When false or unset, monitor_ids is ignored and existing\n associations are preserved.\n metadata:\n type: object\n title: metadata\n maxProperties: 20\n additionalProperties:\n type: string\n title: value\n maxLength: 256\n description: New key/value labels. Only applied when update_metadata is true.\n updateMetadata:\n type:\n - boolean\n - \"null\"\n title: update_metadata\n description: |-\n When true, metadata replaces the current labels (an empty map clears all).\n When false or unset, metadata is ignored and existing labels are preserved.\n title: UpdatePrivateLocationRequest\n additionalProperties: false\n description: UpdatePrivateLocationRequest is the request to update a private location.\n openstatus.private_location.v1.UpdatePrivateLocationRequest.MetadataEntry:\n type: object\n properties:\n key:\n type: string\n title: key\n value:\n type: string\n title: value\n title: MetadataEntry\n additionalProperties: false\n openstatus.private_location.v1.UpdatePrivateLocationResponse:\n type: object\n properties:\n privateLocation:\n title: private_location\n description: The updated private location.\n $ref: '#/components/schemas/openstatus.private_location.v1.PrivateLocation'\n title: UpdatePrivateLocationResponse\n additionalProperties: false\n description: UpdatePrivateLocationResponse is the response after updating a private location.\n openstatus.status_page.v1.AddMonitorComponentRequest:\n type: object\n properties:\n pageId:\n type: string\n title: page_id\n minLength: 1\n description: ID of the status page to add the component to (required).\n monitorId:\n type: string\n title: monitor_id\n minLength: 1\n description: ID of the monitor to associate with this component (required).\n name:\n type:\n - string\n - \"null\"\n title: name\n maxLength: 256\n description: Display name for the component (optional, defaults to monitor name).\n description:\n type:\n - string\n - \"null\"\n title: description\n maxLength: 1024\n description: Description of the component (optional).\n order:\n type:\n - integer\n - \"null\"\n title: order\n format: int32\n description: Display order of the component (optional).\n groupId:\n type:\n - string\n - \"null\"\n title: group_id\n description: ID of the group to add this component to (optional).\n title: AddMonitorComponentRequest\n additionalProperties: false\n description: AddMonitorComponentRequest is the request to add a monitor-based component to a status page.\n openstatus.status_page.v1.AddMonitorComponentResponse:\n type: object\n properties:\n component:\n title: component\n description: The created component.\n $ref: '#/components/schemas/openstatus.status_page.v1.PageComponent'\n title: AddMonitorComponentResponse\n additionalProperties: false\n description: AddMonitorComponentResponse is the response after adding a monitor component.\n openstatus.status_page.v1.AddStaticComponentRequest:\n type: object\n properties:\n pageId:\n type: string\n title: page_id\n minLength: 1\n description: ID of the status page to add the component to (required).\n name:\n type: string\n title: name\n maxLength: 256\n minLength: 1\n description: Display name for the component (required).\n description:\n type:\n - string\n - \"null\"\n title: description\n maxLength: 1024\n description: Description of the component (optional).\n order:\n type:\n - integer\n - \"null\"\n title: order\n format: int32\n description: Display order of the component (optional).\n groupId:\n type:\n - string\n - \"null\"\n title: group_id\n description: ID of the group to add this component to (optional).\n title: AddStaticComponentRequest\n additionalProperties: false\n description: AddStaticComponentRequest is the request to add a static component to a status page.\n openstatus.status_page.v1.AddStaticComponentResponse:\n type: object\n properties:\n component:\n title: component\n description: The created component.\n $ref: '#/components/schemas/openstatus.status_page.v1.PageComponent'\n title: AddStaticComponentResponse\n additionalProperties: false\n description: AddStaticComponentResponse is the response after adding a static component.\n openstatus.status_page.v1.ComponentDayBucket:\n type: object\n properties:\n day:\n type: string\n title: day\n description: Day in RFC 3339 format (UTC midnight).\n count:\n type:\n - integer\n - string\n title: count\n format: int64\n description: Total checks that day.\n ok:\n type:\n - integer\n - string\n title: ok\n format: int64\n description: Successful checks.\n degraded:\n type:\n - integer\n - string\n title: degraded\n format: int64\n description: Degraded checks.\n error:\n type:\n - integer\n - string\n title: error\n format: int64\n description: Failed checks.\n status:\n title: status\n description: Convenience resolved status for the day.\n $ref: '#/components/schemas/openstatus.status_page.v1.ComponentDayStatus'\n impact:\n oneOf:\n - $ref: '#/components/schemas/openstatus.status_report.v1.PageComponentImpact'\n - type: \"null\"\n title: impact\n description: Worst status-report impact overlapping the day (absent when none).\n title: ComponentDayBucket\n additionalProperties: false\n description: ComponentDayBucket is one day of status data for a component.\n openstatus.status_page.v1.ComponentDayStatus:\n type: string\n title: ComponentDayStatus\n enum:\n - COMPONENT_DAY_STATUS_UNSPECIFIED\n - COMPONENT_DAY_STATUS_OPERATIONAL\n - COMPONENT_DAY_STATUS_DEGRADED\n - COMPONENT_DAY_STATUS_DOWN\n - COMPONENT_DAY_STATUS_MAINTENANCE\n - COMPONENT_DAY_STATUS_EMPTY\n description: ComponentDayStatus is the resolved status of a component on a given day.\n openstatus.status_page.v1.ComponentEvent:\n type: object\n properties:\n id:\n type: string\n title: id\n description: Identifier of the underlying event.\n name:\n type: string\n title: name\n description: Human-readable name (incident \"Downtime\", maintenance / report title).\n type:\n title: type\n description: Kind of event.\n $ref: '#/components/schemas/openstatus.status_page.v1.ComponentEventType'\n status:\n title: status\n description: Projected status this event contributes.\n $ref: '#/components/schemas/openstatus.status_page.v1.ComponentEventStatus'\n from:\n type: string\n title: from\n description: Start time (RFC 3339).\n to:\n type:\n - string\n - \"null\"\n title: to\n description: End time (RFC 3339); absent while the event is ongoing.\n impact:\n oneOf:\n - $ref: '#/components/schemas/openstatus.status_report.v1.PageComponentImpact'\n - type: \"null\"\n title: impact\n description: Worst impact over the event (reports only; absent otherwise).\n title: ComponentEvent\n additionalProperties: false\n description: ComponentEvent is a single incident / maintenance / report affecting a component.\n openstatus.status_page.v1.ComponentEventStatus:\n type: string\n title: ComponentEventStatus\n enum:\n - COMPONENT_EVENT_STATUS_UNSPECIFIED\n - COMPONENT_EVENT_STATUS_OPERATIONAL\n - COMPONENT_EVENT_STATUS_DEGRADED\n - COMPONENT_EVENT_STATUS_DOWN\n - COMPONENT_EVENT_STATUS_MAINTENANCE\n description: ComponentEventStatus is the projected status an event contributes.\n openstatus.status_page.v1.ComponentEventType:\n type: string\n title: ComponentEventType\n enum:\n - COMPONENT_EVENT_TYPE_UNSPECIFIED\n - COMPONENT_EVENT_TYPE_MAINTENANCE\n - COMPONENT_EVENT_TYPE_INCIDENT\n - COMPONENT_EVENT_TYPE_REPORT\n description: ComponentEventType is the kind of timeline event affecting a component.\n openstatus.status_page.v1.ComponentStatus:\n type: object\n properties:\n componentId:\n type: string\n title: component_id\n description: ID of the component.\n status:\n title: status\n description: Current status of the component.\n $ref: '#/components/schemas/openstatus.status_page.v1.OverallStatus'\n title: ComponentStatus\n additionalProperties: false\n description: ComponentStatus represents the status of a single component.\n openstatus.status_page.v1.CreateComponentGroupRequest:\n type: object\n properties:\n pageId:\n type: string\n title: page_id\n minLength: 1\n description: ID of the status page to create the group in (required).\n name:\n type: string\n title: name\n maxLength: 256\n minLength: 1\n description: Display name for the group (required).\n defaultOpen:\n type:\n - boolean\n - \"null\"\n title: default_open\n description: Whether the group should be expanded by default on the status page (optional, defaults to false).\n title: CreateComponentGroupRequest\n additionalProperties: false\n description: CreateComponentGroupRequest is the request to create a new component group.\n openstatus.status_page.v1.CreateComponentGroupResponse:\n type: object\n properties:\n group:\n title: group\n description: The created component group.\n $ref: '#/components/schemas/openstatus.status_page.v1.PageComponentGroup'\n title: CreateComponentGroupResponse\n additionalProperties: false\n description: CreateComponentGroupResponse is the response after creating a component group.\n openstatus.status_page.v1.CreatePageSubscriptionRequest:\n type: object\n allOf:\n - properties:\n pageId:\n type: string\n title: page_id\n minLength: 1\n description: ID of the status page (required).\n name:\n type:\n - string\n - \"null\"\n title: name\n maxLength: 255\n description: Optional human-readable label.\n componentIds:\n type: array\n items:\n type: string\n title: component_ids\n description: Component scope. Empty = entire page.\n - oneOf:\n - type: object\n properties:\n emailChannel:\n title: email_channel\n $ref: '#/components/schemas/openstatus.status_page.v1.EmailChannel'\n title: email_channel\n required:\n - emailChannel\n - type: object\n properties:\n webhookChannel:\n title: webhook_channel\n $ref: '#/components/schemas/openstatus.status_page.v1.WebhookChannel'\n title: webhook_channel\n required:\n - webhookChannel\n title: CreatePageSubscriptionRequest\n additionalProperties: false\n description: CreatePageSubscriptionRequest is the request to add a vendor-managed subscriber to a status page.\n openstatus.status_page.v1.CreatePageSubscriptionResponse:\n type: object\n properties:\n subscriber:\n title: subscriber\n description: The created subscriber.\n $ref: '#/components/schemas/openstatus.status_page.v1.PageSubscriber'\n title: CreatePageSubscriptionResponse\n additionalProperties: false\n description: CreatePageSubscriptionResponse is the response after creating a vendor-managed subscription.\n openstatus.status_page.v1.CreateStatusPageRequest:\n type: object\n properties:\n title:\n type: string\n examples:\n - Acme Corp Status\n title: title\n maxLength: 256\n minLength: 1\n description: Title of the status page (required).\n description:\n type:\n - string\n - \"null\"\n title: description\n maxLength: 1024\n description: Description of the status page (optional).\n slug:\n type: string\n examples:\n - my-status-page\n title: slug\n maxLength: 256\n minLength: 1\n pattern: ^[a-z0-9]+(?:-[a-z0-9]+)*$\n description: URL-friendly slug for the status page (required). Must be lowercase alphanumeric with hyphens.\n homepageUrl:\n type:\n - string\n - \"null\"\n examples:\n - https://www.example.com\n title: homepage_url\n description: URL to the homepage (optional).\n contactUrl:\n type:\n - string\n - \"null\"\n title: contact_url\n description: URL to the contact page (optional).\n defaultLocale:\n oneOf:\n - $ref: '#/components/schemas/openstatus.status_page.v1.Locale'\n - type: \"null\"\n title: default_locale\n description: Default locale for the status page (optional, defaults to EN).\n locales:\n type: array\n items:\n $ref: '#/components/schemas/openstatus.status_page.v1.Locale'\n title: locales\n description: Enabled locales for the status page (optional).\n icon:\n type:\n - string\n - \"null\"\n title: icon\n maxLength: 1024\n description: Icon URL for the status page (optional).\n customDomain:\n type:\n - string\n - \"null\"\n title: custom_domain\n maxLength: 256\n description: Custom domain for the status page (optional).\n theme:\n oneOf:\n - $ref: '#/components/schemas/openstatus.status_page.v1.PageTheme'\n - type: \"null\"\n title: theme\n description: Visual theme for the status page (optional, defaults to SYSTEM).\n accessType:\n oneOf:\n - $ref: '#/components/schemas/openstatus.status_page.v1.PageAccessType'\n - type: \"null\"\n title: access_type\n description: Access type for the status page (optional, defaults to PUBLIC).\n password:\n type:\n - string\n - \"null\"\n title: password\n maxLength: 256\n minLength: 1\n description: Password for the status page (required when access_type is PASSWORD_PROTECTED).\n authEmailDomains:\n type: array\n items:\n type: string\n title: auth_email_domains\n description: Email domains allowed to access the page (used when access_type is AUTHENTICATED).\n allowIndex:\n type:\n - boolean\n - \"null\"\n title: allow_index\n description: Whether search engines are allowed to index this status page (optional, defaults to true).\n allowedIpRanges:\n type:\n - string\n - \"null\"\n title: allowed_ip_ranges\n description: Comma-separated IPv4 CIDR ranges (required when access_type is IP_RESTRICTED).\n customTheme:\n oneOf:\n - $ref: '#/components/schemas/openstatus.status_page.v1.CustomTheme'\n - type: \"null\"\n title: custom_theme\n description: |-\n Per-mode CSS variable overrides merged over the theme (optional).\n Only supported variable names are accepted. Requires the custom-theme plan feature.\n title: CreateStatusPageRequest\n additionalProperties: false\n description: CreateStatusPageRequest is the request to create a new status page.\n openstatus.status_page.v1.CreateStatusPageResponse:\n type: object\n properties:\n statusPage:\n title: status_page\n description: The created status page.\n $ref: '#/components/schemas/openstatus.status_page.v1.StatusPage'\n title: CreateStatusPageResponse\n additionalProperties: false\n description: CreateStatusPageResponse is the response after creating a status page.\n openstatus.status_page.v1.CustomTheme:\n type: object\n properties:\n light:\n type: object\n title: light\n additionalProperties:\n type: string\n title: value\n description: 'CSS variable overrides applied in light mode, keyed by variable name (e.g. \"--primary\": \"hsl(24 94% 50%)\").'\n dark:\n type: object\n title: dark\n additionalProperties:\n type: string\n title: value\n description: CSS variable overrides applied in dark mode, keyed by variable name.\n title: CustomTheme\n additionalProperties: false\n description: CustomTheme holds per-mode CSS variable overrides merged over the page theme.\n openstatus.status_page.v1.CustomTheme.DarkEntry:\n type: object\n properties:\n key:\n type: string\n title: key\n value:\n type: string\n title: value\n title: DarkEntry\n additionalProperties: false\n openstatus.status_page.v1.CustomTheme.LightEntry:\n type: object\n properties:\n key:\n type: string\n title: key\n value:\n type: string\n title: value\n title: LightEntry\n additionalProperties: false\n openstatus.status_page.v1.DeleteComponentGroupRequest:\n type: object\n properties:\n id:\n type: string\n title: id\n minLength: 1\n description: ID of the component group to delete (required).\n title: DeleteComponentGroupRequest\n additionalProperties: false\n description: DeleteComponentGroupRequest is the request to delete a component group.\n openstatus.status_page.v1.DeleteComponentGroupResponse:\n type: object\n properties:\n success:\n type: boolean\n title: success\n description: Whether the deletion was successful.\n title: DeleteComponentGroupResponse\n additionalProperties: false\n description: DeleteComponentGroupResponse is the response after deleting a component group.\n openstatus.status_page.v1.DeleteStatusPageRequest:\n type: object\n properties:\n id:\n type: string\n title: id\n minLength: 1\n description: ID of the status page to delete (required).\n title: DeleteStatusPageRequest\n additionalProperties: false\n description: DeleteStatusPageRequest is the request to delete a status page.\n openstatus.status_page.v1.DeleteStatusPageResponse:\n type: object\n properties:\n success:\n type: boolean\n title: success\n description: Whether the deletion was successful.\n title: DeleteStatusPageResponse\n additionalProperties: false\n description: DeleteStatusPageResponse is the response after deleting a status page.\n openstatus.status_page.v1.EmailChannel:\n type: object\n properties:\n email:\n type: string\n title: email\n format: email\n description: Email address of the subscriber (required).\n title: EmailChannel\n additionalProperties: false\n description: EmailChannel carries the email-channel fields for CreatePageSubscription.\n openstatus.status_page.v1.GetOverallStatusRequest:\n type: object\n oneOf:\n - type: object\n properties:\n id:\n type: string\n title: id\n description: ID of the status page.\n title: id\n required:\n - id\n - type: object\n properties:\n slug:\n type: string\n title: slug\n description: Slug of the status page.\n title: slug\n required:\n - slug\n title: GetOverallStatusRequest\n additionalProperties: false\n description: GetOverallStatusRequest is the request to get the overall status of a status page.\n openstatus.status_page.v1.GetOverallStatusResponse:\n type: object\n properties:\n overallStatus:\n title: overall_status\n description: Aggregated status across all components.\n $ref: '#/components/schemas/openstatus.status_page.v1.OverallStatus'\n componentStatuses:\n type: array\n items:\n $ref: '#/components/schemas/openstatus.status_page.v1.ComponentStatus'\n title: component_statuses\n description: Status of individual components.\n title: GetOverallStatusResponse\n additionalProperties: false\n description: GetOverallStatusResponse is the response containing the overall status and individual component statuses.\n openstatus.status_page.v1.GetPageComponentDailySummaryRequest:\n type: object\n allOf:\n - properties:\n componentIds:\n type: array\n items:\n type: string\n minLength: 1\n title: component_ids\n description: Restrict the result to these component ids (optional, defaults to all components).\n days:\n type:\n - integer\n - \"null\"\n title: days\n maximum: 45\n minimum: 1\n format: int32\n description: Number of days to return (1-45, defaults to 45 if unspecified).\n - oneOf:\n - type: object\n properties:\n id:\n type: string\n title: id\n description: ID of the status page.\n title: id\n required:\n - id\n - type: object\n properties:\n slug:\n type: string\n title: slug\n description: Slug of the status page.\n title: slug\n required:\n - slug\n title: GetPageComponentDailySummaryRequest\n additionalProperties: false\n description: GetPageComponentDailySummaryRequest is the request for per-component daily summaries.\n openstatus.status_page.v1.GetPageComponentDailySummaryResponse:\n type: object\n properties:\n components:\n type: array\n items:\n $ref: '#/components/schemas/openstatus.status_page.v1.PageComponentDailySummary'\n title: components\n description: Per-component daily summaries, in page order.\n title: GetPageComponentDailySummaryResponse\n additionalProperties: false\n description: GetPageComponentDailySummaryResponse is the response containing per-component summaries.\n openstatus.status_page.v1.GetPageComponentRequest:\n type: object\n properties:\n id:\n type: string\n title: id\n minLength: 1\n description: ID of the component to retrieve (required).\n title: GetPageComponentRequest\n additionalProperties: false\n description: GetPageComponentRequest is the request to fetch a single component by ID.\n openstatus.status_page.v1.GetPageComponentResponse:\n type: object\n properties:\n component:\n title: component\n description: The requested component.\n $ref: '#/components/schemas/openstatus.status_page.v1.PageComponent'\n title: GetPageComponentResponse\n additionalProperties: false\n description: GetPageComponentResponse is the response containing the component.\n openstatus.status_page.v1.GetStatusPageContentRequest:\n type: object\n oneOf:\n - type: object\n properties:\n id:\n type: string\n title: id\n description: ID of the status page.\n title: id\n required:\n - id\n - type: object\n properties:\n slug:\n type: string\n title: slug\n description: Slug of the status page.\n title: slug\n required:\n - slug\n title: GetStatusPageContentRequest\n additionalProperties: false\n description: GetStatusPageContentRequest is the request to get the full content of a status page.\n openstatus.status_page.v1.GetStatusPageContentResponse:\n type: object\n properties:\n statusPage:\n title: status_page\n description: The status page details.\n $ref: '#/components/schemas/openstatus.status_page.v1.StatusPage'\n components:\n type: array\n items:\n $ref: '#/components/schemas/openstatus.status_page.v1.PageComponent'\n title: components\n description: Components on the status page.\n groups:\n type: array\n items:\n $ref: '#/components/schemas/openstatus.status_page.v1.PageComponentGroup'\n title: groups\n description: Component groups on the status page.\n statusReports:\n type: array\n items:\n $ref: '#/components/schemas/openstatus.status_report.v1.StatusReport'\n title: status_reports\n description: Active and recent status reports.\n maintenances:\n type: array\n items:\n $ref: '#/components/schemas/openstatus.maintenance.v1.MaintenanceSummary'\n title: maintenances\n description: Scheduled maintenances.\n title: GetStatusPageContentResponse\n additionalProperties: false\n description: GetStatusPageContentResponse is the response containing the full status page content.\n openstatus.status_page.v1.GetStatusPageOverviewRequest:\n type: object\n properties:\n id:\n type: string\n title: id\n minLength: 1\n description: ID of the status page (required, workspace-scoped).\n title: GetStatusPageOverviewRequest\n additionalProperties: false\n description: GetStatusPageOverviewRequest requests the full overview of a status page by id.\n openstatus.status_page.v1.GetStatusPageOverviewResponse:\n type: object\n properties:\n statusPage:\n title: status_page\n description: The status page details.\n $ref: '#/components/schemas/openstatus.status_page.v1.StatusPage'\n configuration:\n title: configuration\n description: Rich rendering configuration of the page.\n $ref: '#/components/schemas/openstatus.status_page.v1.PageConfiguration'\n components:\n type: array\n items:\n $ref: '#/components/schemas/openstatus.status_page.v1.PageComponent'\n title: components\n description: Components on the status page.\n groups:\n type: array\n items:\n $ref: '#/components/schemas/openstatus.status_page.v1.PageComponentGroup'\n title: groups\n description: Component groups on the status page.\n statusReports:\n type: array\n items:\n $ref: '#/components/schemas/openstatus.status_report.v1.StatusReport'\n title: status_reports\n description: Active and recent status reports.\n maintenances:\n type: array\n items:\n $ref: '#/components/schemas/openstatus.maintenance.v1.MaintenanceSummary'\n title: maintenances\n description: Scheduled maintenances.\n overallStatus:\n title: overall_status\n description: Aggregated status across all components.\n $ref: '#/components/schemas/openstatus.status_page.v1.OverallStatus'\n componentStatuses:\n type: array\n items:\n $ref: '#/components/schemas/openstatus.status_page.v1.ComponentStatus'\n title: component_statuses\n description: Status of individual components.\n title: GetStatusPageOverviewResponse\n additionalProperties: false\n description: GetStatusPageOverviewResponse bundles all data for a single status page.\n openstatus.status_page.v1.GetStatusPageRequest:\n type: object\n properties:\n id:\n type: string\n title: id\n minLength: 1\n description: ID of the status page to retrieve (required).\n title: GetStatusPageRequest\n additionalProperties: false\n description: GetStatusPageRequest is the request to get a status page by ID.\n openstatus.status_page.v1.GetStatusPageResponse:\n type: object\n properties:\n statusPage:\n title: status_page\n description: The requested status page.\n $ref: '#/components/schemas/openstatus.status_page.v1.StatusPage'\n title: GetStatusPageResponse\n additionalProperties: false\n description: GetStatusPageResponse is the response containing the status page.\n openstatus.status_page.v1.ListStatusPagesRequest:\n type: object\n properties:\n limit:\n type:\n - integer\n - \"null\"\n title: limit\n maximum: 100\n minimum: 1\n format: int32\n description: Maximum number of pages to return (1-100, defaults to 50).\n offset:\n type:\n - integer\n - \"null\"\n title: offset\n minimum: 0\n format: int32\n description: Number of pages to skip for pagination (defaults to 0).\n title: ListStatusPagesRequest\n additionalProperties: false\n description: ListStatusPagesRequest is the request to list status pages.\n openstatus.status_page.v1.ListStatusPagesResponse:\n type: object\n properties:\n statusPages:\n type: array\n items:\n $ref: '#/components/schemas/openstatus.status_page.v1.StatusPageSummary'\n title: status_pages\n description: List of status pages (metadata only).\n totalSize:\n type: integer\n title: total_size\n format: int32\n description: Total number of status pages.\n title: ListStatusPagesResponse\n additionalProperties: false\n description: ListStatusPagesResponse is the response containing status page summaries.\n openstatus.status_page.v1.ListSubscribersRequest:\n type: object\n properties:\n pageId:\n type: string\n title: page_id\n minLength: 1\n description: ID of the status page to list subscribers for (required).\n limit:\n type:\n - integer\n - \"null\"\n title: limit\n maximum: 100\n minimum: 1\n format: int32\n description: Maximum number of subscribers to return (1-100, defaults to 50).\n offset:\n type:\n - integer\n - \"null\"\n title: offset\n minimum: 0\n format: int32\n description: Number of subscribers to skip for pagination (defaults to 0).\n includeUnsubscribed:\n type:\n - boolean\n - \"null\"\n title: include_unsubscribed\n description: Whether to include unsubscribed users (defaults to false).\n title: ListSubscribersRequest\n additionalProperties: false\n description: ListSubscribersRequest is the request to list subscribers of a status page.\n openstatus.status_page.v1.ListSubscribersResponse:\n type: object\n properties:\n subscribers:\n type: array\n items:\n $ref: '#/components/schemas/openstatus.status_page.v1.PageSubscriber'\n title: subscribers\n description: List of subscribers.\n totalSize:\n type: integer\n title: total_size\n format: int32\n description: Total number of subscribers matching the filter.\n title: ListSubscribersResponse\n additionalProperties: false\n description: ListSubscribersResponse is the response containing status page subscribers.\n openstatus.status_page.v1.Locale:\n type: string\n title: Locale\n enum:\n - LOCALE_UNSPECIFIED\n - LOCALE_EN\n - LOCALE_FR\n - LOCALE_DE\n - LOCALE_TR\n - LOCALE_HI\n - LOCALE_KO\n - LOCALE_JA\n description: Locale defines the supported languages for a status page.\n openstatus.status_page.v1.OverallStatus:\n type: string\n title: OverallStatus\n enum:\n - OVERALL_STATUS_UNSPECIFIED\n - OVERALL_STATUS_OPERATIONAL\n - OVERALL_STATUS_DEGRADED\n - OVERALL_STATUS_PARTIAL_OUTAGE\n - OVERALL_STATUS_MAJOR_OUTAGE\n - OVERALL_STATUS_MAINTENANCE\n - OVERALL_STATUS_UNKNOWN\n description: OverallStatus represents the aggregated status of all components on a page.\n openstatus.status_page.v1.PageAccessType:\n type: string\n title: PageAccessType\n enum:\n - PAGE_ACCESS_TYPE_UNSPECIFIED\n - PAGE_ACCESS_TYPE_PUBLIC\n - PAGE_ACCESS_TYPE_PASSWORD_PROTECTED\n - PAGE_ACCESS_TYPE_AUTHENTICATED\n - PAGE_ACCESS_TYPE_IP_RESTRICTED\n description: PageAccessType defines who can access the status page.\n openstatus.status_page.v1.PageBarType:\n type: string\n title: PageBarType\n enum:\n - PAGE_BAR_TYPE_UNSPECIFIED\n - PAGE_BAR_TYPE_ABSOLUTE\n - PAGE_BAR_TYPE_MANUAL\n description: PageBarType mirrors page.configuration.type (how the status bar is computed).\n openstatus.status_page.v1.PageComponent:\n type: object\n properties:\n id:\n type: string\n title: id\n description: Unique identifier for the component.\n pageId:\n type: string\n title: page_id\n description: ID of the status page this component belongs to.\n name:\n type: string\n title: name\n description: Display name of the component.\n description:\n type: string\n title: description\n description: Description of the component (optional).\n type:\n title: type\n description: Type of the component (monitor or static).\n $ref: '#/components/schemas/openstatus.status_page.v1.PageComponentType'\n monitorId:\n type: string\n title: monitor_id\n description: ID of the monitor if type is MONITOR (optional).\n order:\n type: integer\n title: order\n format: int32\n description: Display order of the component.\n groupId:\n type: string\n title: group_id\n description: ID of the group this component belongs to (optional).\n groupOrder:\n type: integer\n title: group_order\n format: int32\n description: Order within the group if grouped.\n createdAt:\n type: string\n title: created_at\n description: Timestamp when the component was created (RFC 3339 format).\n updatedAt:\n type: string\n title: updated_at\n description: Timestamp when the component was last updated (RFC 3339 format).\n title: PageComponent\n additionalProperties: false\n description: PageComponent represents a component displayed on a status page.\n openstatus.status_page.v1.PageComponentDailySummary:\n type: object\n properties:\n componentId:\n type: string\n title: component_id\n description: ID of the component.\n type:\n title: type\n description: Component type (monitor or static).\n $ref: '#/components/schemas/openstatus.status_page.v1.PageComponentType'\n monitorId:\n type:\n - string\n - \"null\"\n title: monitor_id\n description: Monitor id backing the component (absent for static components).\n name:\n type: string\n title: name\n description: Display name of the component.\n buckets:\n type: array\n items:\n $ref: '#/components/schemas/openstatus.status_page.v1.ComponentDayBucket'\n title: buckets\n description: Per-day buckets, oldest first.\n events:\n type: array\n items:\n $ref: '#/components/schemas/openstatus.status_page.v1.ComponentEvent'\n title: events\n description: Events affecting the component within the window.\n title: PageComponentDailySummary\n additionalProperties: false\n description: PageComponentDailySummary is the per-day buckets + event timeline for one component.\n openstatus.status_page.v1.PageComponentGroup:\n type: object\n properties:\n id:\n type: string\n title: id\n description: Unique identifier for the group.\n pageId:\n type: string\n title: page_id\n description: ID of the status page this group belongs to.\n name:\n type: string\n title: name\n description: Display name of the group.\n createdAt:\n type: string\n title: created_at\n description: Timestamp when the group was created (RFC 3339 format).\n updatedAt:\n type: string\n title: updated_at\n description: Timestamp when the group was last updated (RFC 3339 format).\n defaultOpen:\n type: boolean\n title: default_open\n description: Whether the group should be expanded by default on the status page.\n title: PageComponentGroup\n additionalProperties: false\n description: PageComponentGroup represents a group of components on a status page.\n openstatus.status_page.v1.PageComponentType:\n type: string\n title: PageComponentType\n enum:\n - PAGE_COMPONENT_TYPE_UNSPECIFIED\n - PAGE_COMPONENT_TYPE_MONITOR\n - PAGE_COMPONENT_TYPE_STATIC\n description: PageComponentType defines the type of a component on a status page.\n openstatus.status_page.v1.PageConfiguration:\n type: object\n properties:\n metricType:\n title: metric_type\n description: Which metric the status bars represent (configuration.value).\n $ref: '#/components/schemas/openstatus.status_page.v1.PageMetricType'\n barType:\n title: bar_type\n description: How the status bar is computed (configuration.type).\n $ref: '#/components/schemas/openstatus.status_page.v1.PageBarType'\n showUptime:\n type: boolean\n title: show_uptime\n description: Whether to show the uptime percentage (configuration.uptime).\n themeKey:\n type: string\n title: theme_key\n description: |-\n Theme key from the theme store (configuration.theme), e.g. \"default\".\n Free-form string rather than an enum because the theme catalog is dynamic.\n days:\n type: integer\n title: days\n format: int32\n description: 'Number of uptime-bar days rendered on the page (configuration.days): 30 or 45.'\n title: PageConfiguration\n additionalProperties: false\n description: PageConfiguration is the rich rendering config stored in page.configuration.\n openstatus.status_page.v1.PageMetricType:\n type: string\n title: PageMetricType\n enum:\n - PAGE_METRIC_TYPE_UNSPECIFIED\n - PAGE_METRIC_TYPE_DURATION\n - PAGE_METRIC_TYPE_REQUESTS\n - PAGE_METRIC_TYPE_MANUAL\n description: PageMetricType mirrors page.configuration.value (which metric the status bars represent).\n openstatus.status_page.v1.PageSubscriber:\n type: object\n properties:\n id:\n type: string\n title: id\n description: Unique identifier for the subscriber.\n pageId:\n type: string\n title: page_id\n description: ID of the status page the user is subscribed to.\n email:\n type: string\n title: email\n description: Email address of the subscriber (empty for webhook channels).\n acceptedAt:\n type: string\n title: accepted_at\n description: Timestamp when the subscription was accepted/confirmed (RFC 3339 format, optional).\n unsubscribedAt:\n type: string\n title: unsubscribed_at\n description: Timestamp when the user unsubscribed (RFC 3339 format, optional).\n createdAt:\n type: string\n title: created_at\n description: Timestamp when the subscription was created (RFC 3339 format).\n updatedAt:\n type: string\n title: updated_at\n description: Timestamp when the subscription was last updated (RFC 3339 format).\n source:\n title: source\n description: How the subscription was created. Vendor-added rows skip verification.\n $ref: '#/components/schemas/openstatus.status_page.v1.SubscriberSource'\n name:\n type:\n - string\n - \"null\"\n title: name\n description: 'Optional human-readable label (e.g. \"Supabase #incidents\").'\n channelType:\n type: string\n title: channel_type\n description: 'Channel type: \"email\" or \"webhook\".'\n webhookUrl:\n type:\n - string\n - \"null\"\n title: webhook_url\n description: Webhook URL (populated only for channel_type = \"webhook\").\n channelConfig:\n type:\n - string\n - \"null\"\n title: channel_config\n description: JSON-encoded channel config (e.g. custom headers). Populated for webhook channels.\n componentIds:\n type: array\n items:\n type: string\n title: component_ids\n description: IDs of components this subscription is scoped to. Empty = entire page.\n title: PageSubscriber\n additionalProperties: false\n description: PageSubscriber represents a subscriber to a status page.\n openstatus.status_page.v1.PageTheme:\n type: string\n title: PageTheme\n enum:\n - PAGE_THEME_UNSPECIFIED\n - PAGE_THEME_SYSTEM\n - PAGE_THEME_LIGHT\n - PAGE_THEME_DARK\n description: PageTheme defines the visual theme of the status page.\n openstatus.status_page.v1.RemoveComponentRequest:\n type: object\n properties:\n id:\n type: string\n title: id\n minLength: 1\n description: ID of the component to remove (required).\n title: RemoveComponentRequest\n additionalProperties: false\n description: RemoveComponentRequest is the request to remove a component from a status page.\n openstatus.status_page.v1.RemoveComponentResponse:\n type: object\n properties:\n success:\n type: boolean\n title: success\n description: Whether the removal was successful.\n title: RemoveComponentResponse\n additionalProperties: false\n description: RemoveComponentResponse is the response after removing a component.\n openstatus.status_page.v1.StatusPage:\n type: object\n properties:\n id:\n type: string\n title: id\n description: Unique identifier for the status page.\n title:\n type: string\n title: title\n description: Title of the status page.\n description:\n type: string\n title: description\n description: Description of the status page.\n slug:\n type: string\n examples:\n - acme-corp\n title: slug\n description: URL-friendly slug for the status page.\n customDomain:\n type: string\n examples:\n - status.example.com\n title: custom_domain\n description: Custom domain for the status page (optional).\n published:\n type: boolean\n title: published\n description: Whether the status page is published and visible.\n accessType:\n title: access_type\n description: Access type for the status page.\n $ref: '#/components/schemas/openstatus.status_page.v1.PageAccessType'\n theme:\n title: theme\n description: Visual theme for the status page.\n $ref: '#/components/schemas/openstatus.status_page.v1.PageTheme'\n homepageUrl:\n type: string\n title: homepage_url\n description: URL to the homepage (optional).\n contactUrl:\n type: string\n title: contact_url\n description: URL to the contact page (optional).\n icon:\n type: string\n title: icon\n description: Icon URL for the status page (optional).\n createdAt:\n type: string\n examples:\n - \"2024-01-15T09:00:00Z\"\n title: created_at\n description: Timestamp when the page was created (RFC 3339 format).\n updatedAt:\n type: string\n examples:\n - \"2024-06-20T14:30:00Z\"\n title: updated_at\n description: Timestamp when the page was last updated (RFC 3339 format).\n defaultLocale:\n title: default_locale\n description: Default locale for the status page.\n $ref: '#/components/schemas/openstatus.status_page.v1.Locale'\n locales:\n type: array\n items:\n $ref: '#/components/schemas/openstatus.status_page.v1.Locale'\n title: locales\n description: Enabled locales for the status page.\n password:\n type: string\n title: password\n description: Password for the status page (only set when access_type is PASSWORD_PROTECTED).\n authEmailDomains:\n type: array\n items:\n type: string\n title: auth_email_domains\n description: Email domains allowed to access the page (only set when access_type is AUTHENTICATED).\n allowIndex:\n type: boolean\n title: allow_index\n description: Whether search engines are allowed to index this status page.\n allowedIpRanges:\n type: string\n title: allowed_ip_ranges\n description: Comma-separated IPv4 CIDR ranges (only set when access_type is IP_RESTRICTED).\n customTheme:\n oneOf:\n - $ref: '#/components/schemas/openstatus.status_page.v1.CustomTheme'\n - type: \"null\"\n title: custom_theme\n description: Per-mode CSS variable overrides merged over the theme (only set when configured).\n title: StatusPage\n additionalProperties: false\n description: StatusPage represents a full status page with all details.\n openstatus.status_page.v1.StatusPageSummary:\n type: object\n properties:\n id:\n type: string\n title: id\n description: Unique identifier for the status page.\n title:\n type: string\n title: title\n description: Title of the status page.\n slug:\n type: string\n title: slug\n description: URL-friendly slug for the status page.\n published:\n type: boolean\n title: published\n description: Whether the status page is published and visible.\n createdAt:\n type: string\n title: created_at\n description: Timestamp when the page was created (RFC 3339 format).\n updatedAt:\n type: string\n title: updated_at\n description: Timestamp when the page was last updated (RFC 3339 format).\n customDomain:\n type: string\n examples:\n - status.example.com\n title: custom_domain\n description: Custom domain for the status page (optional).\n title: StatusPageSummary\n additionalProperties: false\n description: StatusPageSummary represents metadata for a status page (used in list responses).\n openstatus.status_page.v1.SubscribeToPageRequest:\n type: object\n properties:\n pageId:\n type: string\n title: page_id\n minLength: 1\n description: ID of the status page to subscribe to (required).\n email:\n type: string\n examples:\n - user@example.com\n title: email\n format: email\n description: Email address to subscribe (required).\n title: SubscribeToPageRequest\n additionalProperties: false\n description: SubscribeToPageRequest is the request to subscribe an email to a status page.\n openstatus.status_page.v1.SubscribeToPageResponse:\n type: object\n properties:\n subscriber:\n title: subscriber\n description: The created subscriber.\n $ref: '#/components/schemas/openstatus.status_page.v1.PageSubscriber'\n title: SubscribeToPageResponse\n additionalProperties: false\n description: SubscribeToPageResponse is the response after subscribing to a status page.\n openstatus.status_page.v1.SubscriberSource:\n type: string\n title: SubscriberSource\n enum:\n - SUBSCRIBER_SOURCE_UNSPECIFIED\n - SUBSCRIBER_SOURCE_SELF_SIGNUP\n - SUBSCRIBER_SOURCE_VENDOR\n - SUBSCRIBER_SOURCE_IMPORT\n description: SubscriberSource indicates how the subscription was created.\n openstatus.status_page.v1.UnsubscribeFromPageRequest:\n type: object\n allOf:\n - properties:\n pageId:\n type: string\n title: page_id\n minLength: 1\n description: ID of the status page to unsubscribe from (required).\n - oneOf:\n - type: object\n properties:\n email:\n type: string\n title: email\n description: Email address to unsubscribe.\n title: email\n required:\n - email\n - type: object\n properties:\n id:\n type: string\n title: id\n description: Subscriber ID.\n title: id\n required:\n - id\n title: UnsubscribeFromPageRequest\n additionalProperties: false\n description: UnsubscribeFromPageRequest is the request to unsubscribe from a status page.\n openstatus.status_page.v1.UnsubscribeFromPageResponse:\n type: object\n properties:\n success:\n type: boolean\n title: success\n description: Whether the unsubscription was successful.\n title: UnsubscribeFromPageResponse\n additionalProperties: false\n description: UnsubscribeFromPageResponse is the response after unsubscribing from a status page.\n openstatus.status_page.v1.UpdateComponentGroupRequest:\n type: object\n properties:\n id:\n type: string\n title: id\n minLength: 1\n description: ID of the component group to update (required).\n name:\n type:\n - string\n - \"null\"\n title: name\n maxLength: 256\n minLength: 1\n description: New display name for the group (optional).\n defaultOpen:\n type:\n - boolean\n - \"null\"\n title: default_open\n description: Whether the group should be expanded by default on the status page (optional).\n title: UpdateComponentGroupRequest\n additionalProperties: false\n description: UpdateComponentGroupRequest is the request to update a component group.\n openstatus.status_page.v1.UpdateComponentGroupResponse:\n type: object\n properties:\n group:\n title: group\n description: The updated component group.\n $ref: '#/components/schemas/openstatus.status_page.v1.PageComponentGroup'\n title: UpdateComponentGroupResponse\n additionalProperties: false\n description: UpdateComponentGroupResponse is the response after updating a component group.\n openstatus.status_page.v1.UpdateComponentRequest:\n type: object\n properties:\n id:\n type: string\n title: id\n minLength: 1\n description: ID of the component to update (required).\n name:\n type:\n - string\n - \"null\"\n title: name\n maxLength: 256\n description: New display name for the component (optional).\n description:\n type:\n - string\n - \"null\"\n title: description\n maxLength: 1024\n description: New description for the component (optional).\n order:\n type:\n - integer\n - \"null\"\n title: order\n format: int32\n description: New display order (optional).\n groupId:\n type:\n - string\n - \"null\"\n title: group_id\n description: New group ID (optional, set to empty string to remove from group).\n groupOrder:\n type:\n - integer\n - \"null\"\n title: group_order\n format: int32\n description: New order within the group (optional).\n title: UpdateComponentRequest\n additionalProperties: false\n description: UpdateComponentRequest is the request to update a component.\n openstatus.status_page.v1.UpdateComponentResponse:\n type: object\n properties:\n component:\n title: component\n description: The updated component.\n $ref: '#/components/schemas/openstatus.status_page.v1.PageComponent'\n title: UpdateComponentResponse\n additionalProperties: false\n description: UpdateComponentResponse is the response after updating a component.\n openstatus.status_page.v1.UpdateStatusPageRequest:\n type: object\n properties:\n id:\n type: string\n title: id\n minLength: 1\n description: ID of the status page to update (required).\n title:\n type:\n - string\n - \"null\"\n title: title\n maxLength: 256\n minLength: 1\n description: New title for the status page (optional).\n description:\n type:\n - string\n - \"null\"\n title: description\n maxLength: 1024\n description: New description for the status page (optional).\n slug:\n type:\n - string\n - \"null\"\n title: slug\n maxLength: 256\n minLength: 1\n pattern: ^[a-z0-9]+(?:-[a-z0-9]+)*$\n description: New slug for the status page (optional).\n homepageUrl:\n type:\n - string\n - \"null\"\n title: homepage_url\n description: New homepage URL (optional).\n contactUrl:\n type:\n - string\n - \"null\"\n title: contact_url\n description: New contact URL (optional).\n defaultLocale:\n oneOf:\n - $ref: '#/components/schemas/openstatus.status_page.v1.Locale'\n - type: \"null\"\n title: default_locale\n description: New default locale for the status page (optional).\n locales:\n type: array\n items:\n $ref: '#/components/schemas/openstatus.status_page.v1.Locale'\n title: locales\n description: New enabled locales for the status page (optional).\n icon:\n type:\n - string\n - \"null\"\n title: icon\n maxLength: 1024\n description: New icon URL for the status page (optional).\n customDomain:\n type:\n - string\n - \"null\"\n title: custom_domain\n maxLength: 256\n description: New custom domain (optional).\n theme:\n oneOf:\n - $ref: '#/components/schemas/openstatus.status_page.v1.PageTheme'\n - type: \"null\"\n title: theme\n description: New visual theme (optional).\n accessType:\n oneOf:\n - $ref: '#/components/schemas/openstatus.status_page.v1.PageAccessType'\n - type: \"null\"\n title: access_type\n description: New access type (optional).\n password:\n type:\n - string\n - \"null\"\n title: password\n maxLength: 256\n minLength: 1\n description: New password (optional, required when access_type is PASSWORD_PROTECTED).\n authEmailDomains:\n type: array\n items:\n type: string\n title: auth_email_domains\n description: New email domains (optional, required when access_type is AUTHENTICATED).\n allowIndex:\n type:\n - boolean\n - \"null\"\n title: allow_index\n description: Whether search engines are allowed to index this status page (optional).\n allowedIpRanges:\n type:\n - string\n - \"null\"\n title: allowed_ip_ranges\n description: Comma-separated IPv4 CIDR ranges (required when access_type is IP_RESTRICTED).\n customTheme:\n oneOf:\n - $ref: '#/components/schemas/openstatus.status_page.v1.CustomTheme'\n - type: \"null\"\n title: custom_theme\n description: |-\n New per-mode CSS variable overrides (optional). Omit to keep the current\n value; send an empty message to clear. Requires the custom-theme plan feature.\n title: UpdateStatusPageRequest\n additionalProperties: false\n description: UpdateStatusPageRequest is the request to update a status page.\n openstatus.status_page.v1.UpdateStatusPageResponse:\n type: object\n properties:\n statusPage:\n title: status_page\n description: The updated status page.\n $ref: '#/components/schemas/openstatus.status_page.v1.StatusPage'\n title: UpdateStatusPageResponse\n additionalProperties: false\n description: UpdateStatusPageResponse is the response after updating a status page.\n openstatus.status_page.v1.WebhookChannel:\n type: object\n properties:\n webhookUrl:\n type: string\n title: webhook_url\n maxLength: 2048\n minLength: 1\n format: uri\n description: Webhook URL (required). Payload flavor (Slack / Discord / generic) is auto-detected from the URL prefix.\n headers:\n type: array\n items:\n $ref: '#/components/schemas/openstatus.status_page.v1.WebhookChannelHeader'\n title: headers\n description: Optional custom HTTP headers attached to every dispatch.\n title: WebhookChannel\n additionalProperties: false\n description: WebhookChannel carries the webhook-channel fields for CreatePageSubscription.\n openstatus.status_page.v1.WebhookChannelHeader:\n type: object\n properties:\n key:\n type: string\n title: key\n minLength: 1\n value:\n type: string\n title: value\n title: WebhookChannelHeader\n additionalProperties: false\n description: WebhookChannelHeader is a single custom HTTP header.\n openstatus.status_report.v1.AddStatusReportUpdateRequest:\n type: object\n properties:\n statusReportId:\n type: string\n title: status_report_id\n minLength: 1\n description: ID of the status report to update (required).\n status:\n title: status\n description: New status for the report (required).\n $ref: '#/components/schemas/openstatus.status_report.v1.StatusReportStatus'\n message:\n type: string\n title: message\n minLength: 1\n description: Message describing what changed (required).\n date:\n type:\n - string\n - \"null\"\n title: date\n pattern: ^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(\\.\\d{1,9})?(Z|[+-]\\d{2}:\\d{2})$\n description: Optional date for the update (RFC 3339 format). Defaults to current time if not provided.\n notify:\n type:\n - boolean\n - \"null\"\n title: notify\n description: Whether to notify subscribers about this update (optional, defaults to false).\n componentImpacts:\n type: array\n items:\n $ref: '#/components/schemas/openstatus.status_report.v1.ComponentImpact'\n title: component_impacts\n description: |-\n Per-component impacts this update sets (optional). Components named here\n are added to the report's affected set; omitted components keep their\n prior impact.\n title: AddStatusReportUpdateRequest\n additionalProperties: false\n description: AddStatusReportUpdateRequest is the request to add a new update to a status report.\n openstatus.status_report.v1.AddStatusReportUpdateResponse:\n type: object\n properties:\n statusReport:\n title: status_report\n description: The updated status report with the new update included.\n $ref: '#/components/schemas/openstatus.status_report.v1.StatusReport'\n title: AddStatusReportUpdateResponse\n additionalProperties: false\n description: AddStatusReportUpdateResponse is the response after adding an update to a status report.\n openstatus.status_report.v1.ComponentImpact:\n type: object\n properties:\n pageComponentId:\n type: string\n title: page_component_id\n description: ID of the affected page component.\n impact:\n title: impact\n description: Impact set for the component.\n $ref: '#/components/schemas/openstatus.status_report.v1.PageComponentImpact'\n title: ComponentImpact\n additionalProperties: false\n description: ComponentImpact pairs a page component with the impact an update set for it.\n openstatus.status_report.v1.CreateStatusReportRequest:\n type: object\n properties:\n title:\n type: string\n examples:\n - API Degradation Investigation\n title: title\n minLength: 1\n description: Title of the status report (required).\n status:\n title: status\n description: Initial status (required).\n $ref: '#/components/schemas/openstatus.status_report.v1.StatusReportStatus'\n message:\n type: string\n examples:\n - We are investigating reports of increased API latency.\n title: message\n minLength: 1\n description: Initial message describing the incident (required).\n date:\n type: string\n examples:\n - \"2024-03-15T10:30:00Z\"\n title: date\n pattern: ^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(\\.\\d{1,9})?(Z|[+-]\\d{2}:\\d{2})$\n description: Date when the event occurred (RFC 3339 format, required).\n pageId:\n type: string\n title: page_id\n minLength: 1\n description: Page ID to associate with this report (required).\n pageComponentIds:\n type: array\n items:\n type: string\n title: page_component_ids\n description: Page component IDs to associate with this report (optional).\n notify:\n type:\n - boolean\n - \"null\"\n title: notify\n description: Whether to notify subscribers about this status report (optional, defaults to false).\n componentImpacts:\n type: array\n items:\n $ref: '#/components/schemas/openstatus.status_report.v1.ComponentImpact'\n title: component_impacts\n description: |-\n Per-component impacts set by the initial update (optional). When provided,\n the named components are added to the report's affected set. Omitting this\n field creates a legacy report without impact tracking.\n incidentId:\n type:\n - string\n - \"null\"\n title: incident_id\n minLength: 1\n description: |-\n ID of an incident to link the report to (optional). Linked in the same\n step: a closed incident, or one already linked to a report, fails the create.\n title: CreateStatusReportRequest\n additionalProperties: false\n description: CreateStatusReportRequest is the request to create a new status report.\n openstatus.status_report.v1.CreateStatusReportResponse:\n type: object\n properties:\n statusReport:\n title: status_report\n description: The created status report.\n $ref: '#/components/schemas/openstatus.status_report.v1.StatusReport'\n title: CreateStatusReportResponse\n additionalProperties: false\n description: CreateStatusReportResponse is the response after creating a status report.\n openstatus.status_report.v1.DeleteStatusReportRequest:\n type: object\n properties:\n id:\n type: string\n title: id\n minLength: 1\n description: ID of the status report to delete (required).\n title: DeleteStatusReportRequest\n additionalProperties: false\n description: DeleteStatusReportRequest is the request to delete a status report.\n openstatus.status_report.v1.DeleteStatusReportResponse:\n type: object\n properties:\n success:\n type: boolean\n title: success\n description: Whether the deletion was successful.\n title: DeleteStatusReportResponse\n additionalProperties: false\n description: DeleteStatusReportResponse is the response after deleting a status report.\n openstatus.status_report.v1.DeleteStatusReportUpdateRequest:\n type: object\n properties:\n id:\n type: string\n title: id\n minLength: 1\n description: ID of the update to remove (required).\n title: DeleteStatusReportUpdateRequest\n additionalProperties: false\n description: DeleteStatusReportUpdateRequest is the request to remove a status report update.\n openstatus.status_report.v1.DeleteStatusReportUpdateResponse:\n type: object\n properties:\n success:\n type: boolean\n title: success\n description: Whether the deletion was successful.\n title: DeleteStatusReportUpdateResponse\n additionalProperties: false\n description: DeleteStatusReportUpdateResponse is the response after removing a status report update.\n openstatus.status_report.v1.GetStatusReportRequest:\n type: object\n properties:\n id:\n type: string\n title: id\n minLength: 1\n description: ID of the status report to retrieve (required).\n title: GetStatusReportRequest\n additionalProperties: false\n description: GetStatusReportRequest is the request to get a status report by ID.\n openstatus.status_report.v1.GetStatusReportResponse:\n type: object\n properties:\n statusReport:\n title: status_report\n description: The requested status report.\n $ref: '#/components/schemas/openstatus.status_report.v1.StatusReport'\n title: GetStatusReportResponse\n additionalProperties: false\n description: GetStatusReportResponse is the response containing the status report with its full update timeline.\n openstatus.status_report.v1.ListStatusReportsRequest:\n type: object\n properties:\n limit:\n type:\n - integer\n - \"null\"\n title: limit\n maximum: 100\n minimum: 1\n format: int32\n description: Maximum number of reports to return (1-100, defaults to 50).\n offset:\n type:\n - integer\n - \"null\"\n title: offset\n minimum: 0\n format: int32\n description: Number of reports to skip for pagination (defaults to 0).\n statuses:\n type: array\n items:\n $ref: '#/components/schemas/openstatus.status_report.v1.StatusReportStatus'\n title: statuses\n description: Filter by status (optional). If empty, returns all statuses.\n title: ListStatusReportsRequest\n additionalProperties: false\n description: ListStatusReportsRequest is the request to list status reports.\n openstatus.status_report.v1.ListStatusReportsResponse:\n type: object\n properties:\n statusReports:\n type: array\n items:\n $ref: '#/components/schemas/openstatus.status_report.v1.StatusReportSummary'\n title: status_reports\n description: List of status reports (metadata only, use GetStatusReport for full details).\n totalSize:\n type: integer\n title: total_size\n format: int32\n description: Total number of reports matching the filter.\n title: ListStatusReportsResponse\n additionalProperties: false\n description: ListStatusReportsResponse is the response containing status report summaries.\n openstatus.status_report.v1.PageComponentImpact:\n type: string\n title: PageComponentImpact\n enum:\n - PAGE_COMPONENT_IMPACT_UNSPECIFIED\n - PAGE_COMPONENT_IMPACT_OPERATIONAL\n - PAGE_COMPONENT_IMPACT_DEGRADED_PERFORMANCE\n - PAGE_COMPONENT_IMPACT_PARTIAL_OUTAGE\n - PAGE_COMPONENT_IMPACT_MAJOR_OUTAGE\n description: |-\n PageComponentImpact is the per-component impact a status report update sets.\n UNSPECIFIED means the caller doesn't speak impact (legacy) — it is NOT operational.\n openstatus.status_report.v1.StatusReport:\n type: object\n properties:\n id:\n type: string\n title: id\n description: Unique identifier for the status report.\n status:\n title: status\n description: Current status of the report.\n $ref: '#/components/schemas/openstatus.status_report.v1.StatusReportStatus'\n title:\n type: string\n title: title\n description: Title of the status report.\n pageComponentIds:\n type: array\n items:\n type: string\n title: page_component_ids\n description: IDs of affected page components.\n updates:\n type: array\n items:\n $ref: '#/components/schemas/openstatus.status_report.v1.StatusReportUpdate'\n title: updates\n description: Timeline of updates for this report (only included in GetStatusReport).\n createdAt:\n type: string\n title: created_at\n description: Timestamp when the report was created (RFC 3339 format).\n updatedAt:\n type: string\n title: updated_at\n description: Timestamp when the report was last updated (RFC 3339 format).\n incidentId:\n type:\n - string\n - \"null\"\n title: incident_id\n description: ID of the incident this report communicates (unset when not linked).\n title: StatusReport\n additionalProperties: false\n description: StatusReport represents an incident or maintenance report with full details.\n openstatus.status_report.v1.StatusReportStatus:\n type: string\n title: StatusReportStatus\n enum:\n - STATUS_REPORT_STATUS_UNSPECIFIED\n - STATUS_REPORT_STATUS_INVESTIGATING\n - STATUS_REPORT_STATUS_IDENTIFIED\n - STATUS_REPORT_STATUS_MONITORING\n - STATUS_REPORT_STATUS_RESOLVED\n description: StatusReportStatus represents the current state of a status report.\n openstatus.status_report.v1.StatusReportSummary:\n type: object\n properties:\n id:\n type: string\n title: id\n description: Unique identifier for the status report.\n status:\n title: status\n description: Current status of the report.\n $ref: '#/components/schemas/openstatus.status_report.v1.StatusReportStatus'\n title:\n type: string\n title: title\n description: Title of the status report.\n pageComponentIds:\n type: array\n items:\n type: string\n title: page_component_ids\n description: IDs of affected page components.\n createdAt:\n type: string\n title: created_at\n description: Timestamp when the report was created (RFC 3339 format).\n updatedAt:\n type: string\n title: updated_at\n description: Timestamp when the report was last updated (RFC 3339 format).\n incidentId:\n type:\n - string\n - \"null\"\n title: incident_id\n description: ID of the incident this report communicates (unset when not linked).\n title: StatusReportSummary\n additionalProperties: false\n description: StatusReportSummary represents metadata for a status report (used in list responses).\n openstatus.status_report.v1.StatusReportUpdate:\n type: object\n properties:\n id:\n type: string\n title: id\n description: Unique identifier for the update.\n status:\n title: status\n description: Status at the time of this update.\n $ref: '#/components/schemas/openstatus.status_report.v1.StatusReportStatus'\n date:\n type: string\n title: date\n description: Timestamp when this update occurred (RFC 3339 format).\n message:\n type: string\n title: message\n description: Message describing the update.\n createdAt:\n type: string\n title: created_at\n description: Timestamp when the update was created (RFC 3339 format).\n componentImpacts:\n type: array\n items:\n $ref: '#/components/schemas/openstatus.status_report.v1.ComponentImpact'\n title: component_impacts\n description: Per-component impacts this update set (empty for legacy reports).\n title: StatusReportUpdate\n additionalProperties: false\n description: StatusReportUpdate represents a single update entry in a status report timeline.\n openstatus.status_report.v1.UpdateStatusReportRequest:\n type: object\n properties:\n id:\n type: string\n title: id\n minLength: 1\n description: ID of the status report to update (required).\n title:\n type:\n - string\n - \"null\"\n title: title\n description: New title for the report (optional).\n pageComponentIds:\n type: array\n items:\n type: string\n title: page_component_ids\n description: New list of page component IDs (optional, replaces existing list).\n updatePageComponentIds:\n type:\n - boolean\n - \"null\"\n title: update_page_component_ids\n description: |-\n Set to true to update page component associations.\n When true, page_component_ids replaces the existing list (empty clears all).\n When false or unset, page_component_ids is ignored and existing associations are preserved.\n title: UpdateStatusReportRequest\n additionalProperties: false\n description: UpdateStatusReportRequest is the request to update a status report's metadata.\n openstatus.status_report.v1.UpdateStatusReportResponse:\n type: object\n properties:\n statusReport:\n title: status_report\n description: The updated status report.\n $ref: '#/components/schemas/openstatus.status_report.v1.StatusReport'\n title: UpdateStatusReportResponse\n additionalProperties: false\n description: UpdateStatusReportResponse is the response after updating a status report.\n openstatus.status_report.v1.UpdateStatusReportUpdateRequest:\n type: object\n properties:\n id:\n type: string\n title: id\n minLength: 1\n description: ID of the update to edit (required).\n status:\n oneOf:\n - $ref: '#/components/schemas/openstatus.status_report.v1.StatusReportStatus'\n - type: \"null\"\n title: status\n description: New status for this entry (optional).\n message:\n type:\n - string\n - \"null\"\n title: message\n minLength: 1\n description: New message (optional).\n date:\n type:\n - string\n - \"null\"\n title: date\n pattern: ^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(\\.\\d{1,9})?(Z|[+-]\\d{2}:\\d{2})$\n description: New date for this entry (RFC 3339 format, optional).\n componentImpacts:\n type: array\n items:\n $ref: '#/components/schemas/openstatus.status_report.v1.ComponentImpact'\n title: component_impacts\n description: Replacement impact set for this update (optional, see update_component_impacts).\n updateComponentImpacts:\n type:\n - boolean\n - \"null\"\n title: update_component_impacts\n description: |-\n Set to true to replace the update's component impacts with component_impacts\n (empty clears them). When false or unset, component_impacts is ignored.\n title: UpdateStatusReportUpdateRequest\n additionalProperties: false\n description: UpdateStatusReportUpdateRequest is the request to edit a status report update.\n openstatus.status_report.v1.UpdateStatusReportUpdateResponse:\n type: object\n properties:\n statusReportUpdate:\n title: status_report_update\n description: The edited timeline entry.\n $ref: '#/components/schemas/openstatus.status_report.v1.StatusReportUpdate'\n title: UpdateStatusReportUpdateResponse\n additionalProperties: false\n description: UpdateStatusReportUpdateResponse is the response after editing a status report update.\nsecurity:\n - ApiKeyAuth: []\ntags:\n - name: MonitorService\n description: |\n Create, update, delete, and query monitors. Supports HTTP, TCP, and DNS monitor types\n with configurable check intervals, regions, assertions, and alerting thresholds.\n - name: StatusPageService\n description: |\n Manage public status pages with components, component groups, and email subscribers.\n Includes endpoints for retrieving full page content and aggregated status.\n - name: NotificationService\n description: |\n Configure notification channels (Slack, Discord, PagerDuty, email, webhooks, etc.)\n and associate them with monitors. Supports 12 notification providers.\n - name: StatusReportService\n description: |\n Create and manage public status reports with status updates. Reports follow a lifecycle:\n investigating -> identified -> monitoring -> resolved.\n - name: IncidentService\n description: |\n Declare and run incidents: lifecycle (open -> mitigated -> resolved, or canceled),\n timeline notes, a linked status report, and the postmortem.\n - name: MaintenanceService\n description: |\n Schedule maintenance windows for status page components. Subscribers can be\n notified automatically when maintenance is created.\n - name: HealthService\n description: Health check endpoint for load balancer probes. No authentication required.\n - name: PrivateLocationService\n description: |-\n PrivateLocationService provides CRUD operations for private locations —\n self-hosted checker agents that run monitors from your own network.\npaths:\n /rpc/openstatus.health.v1.HealthService/Check:\n get:\n tags:\n - HealthService\n summary: Check\n description: Check returns the current serving status of the service.\n operationId: HealthService_Check.get\n parameters:\n - name: message\n in: query\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.health.v1.CheckRequest'\n responses:\n \"429\":\n $ref: '#/components/responses/RateLimited'\n default:\n description: Error\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/connect.error'\n \"200\":\n description: Success\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.health.v1.CheckResponse'\n post:\n tags:\n - HealthService\n summary: Check\n description: Check returns the current serving status of the service.\n operationId: HealthService_Check\n requestBody:\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.health.v1.CheckRequest'\n required: true\n responses:\n \"429\":\n $ref: '#/components/responses/RateLimited'\n default:\n description: Error\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/connect.error'\n \"200\":\n description: Success\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.health.v1.CheckResponse'\n /rpc/openstatus.incident.v1.IncidentService/AddIncidentNote:\n post:\n tags:\n - IncidentService\n summary: AddIncidentNote\n description: AddIncidentNote appends a note to the incident timeline.\n operationId: IncidentService_AddIncidentNote\n requestBody:\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.incident.v1.AddIncidentNoteRequest'\n required: true\n responses:\n \"429\":\n $ref: '#/components/responses/RateLimited'\n default:\n description: Error\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/connect.error'\n \"200\":\n description: Success\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.incident.v1.AddIncidentNoteResponse'\n /rpc/openstatus.incident.v1.IncidentService/ApprovePostmortem:\n post:\n tags:\n - IncidentService\n summary: ApprovePostmortem\n description: Approves the draft postmortem of a resolved incident. With close set to true the incident is closed in the same step. Fails with not_found when no postmortem has been written, and with failed_precondition when the incident is not resolved or the postmortem is already approved. Allowed for workspace owners and admins and for the incident commander. The API key acts as its creator, with the creator's current role; keys without a creator cannot call this.\n operationId: IncidentService_ApprovePostmortem\n requestBody:\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.incident.v1.ApprovePostmortemRequest'\n required: true\n responses:\n \"429\":\n $ref: '#/components/responses/RateLimited'\n default:\n description: Error\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/connect.error'\n \"200\":\n description: Success\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.incident.v1.ApprovePostmortemResponse'\n /rpc/openstatus.incident.v1.IncidentService/CloseIncident:\n post:\n tags:\n - IncidentService\n summary: CloseIncident\n description: Closes a resolved incident, after which only its postmortem can change. Requires an approved postmortem, or skip_postmortem set to true. Allowed for workspace owners and admins and for the incident commander. The API key acts as its creator, with the creator's current role; keys without a creator cannot call this.\n operationId: IncidentService_CloseIncident\n requestBody:\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.incident.v1.CloseIncidentRequest'\n required: true\n responses:\n \"429\":\n $ref: '#/components/responses/RateLimited'\n default:\n description: Error\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/connect.error'\n \"200\":\n description: Success\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.incident.v1.CloseIncidentResponse'\n /rpc/openstatus.incident.v1.IncidentService/DeclareIncident:\n post:\n tags:\n - IncidentService\n summary: DeclareIncident\n description: Declares a new incident in the open status. The commander is set by email and must be a member of the workspace; without one the incident is unassigned. A newly assigned commander is emailed unless they own the API key. When open_slack_channel is true and Slack is connected, a dedicated channel is created and the team invited. started_at defaults to now and can be set in the past for a retroactive declare. A status_report_id that does not exist in the workspace fails with not_found; one already linked to another incident fails with failed_precondition.\n operationId: IncidentService_DeclareIncident\n requestBody:\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.incident.v1.DeclareIncidentRequest'\n required: true\n responses:\n \"429\":\n $ref: '#/components/responses/RateLimited'\n default:\n description: Error\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/connect.error'\n \"200\":\n description: Success\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.incident.v1.DeclareIncidentResponse'\n /rpc/openstatus.incident.v1.IncidentService/DeleteIncident:\n post:\n tags:\n - IncidentService\n summary: DeleteIncident\n description: Deletes an incident declared by mistake. Only allowed while the incident is open and was never mitigated, resolved or closed (Incident.deletable); cancel it otherwise. Allowed for workspace owners and admins only. The API key acts as its creator, with the creator's current role; keys without a creator cannot call this.\n operationId: IncidentService_DeleteIncident\n requestBody:\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.incident.v1.DeleteIncidentRequest'\n required: true\n responses:\n \"429\":\n $ref: '#/components/responses/RateLimited'\n default:\n description: Error\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/connect.error'\n \"200\":\n description: Success\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.incident.v1.DeleteIncidentResponse'\n /rpc/openstatus.incident.v1.IncidentService/GetIncident:\n get:\n tags:\n - IncidentService\n summary: GetIncident\n description: GetIncident retrieves an incident by ID, including its timeline.\n operationId: IncidentService_GetIncident.get\n parameters:\n - name: message\n in: query\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.incident.v1.GetIncidentRequest'\n responses:\n \"429\":\n $ref: '#/components/responses/RateLimited'\n default:\n description: Error\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/connect.error'\n \"200\":\n description: Success\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.incident.v1.GetIncidentResponse'\n post:\n tags:\n - IncidentService\n summary: GetIncident\n description: GetIncident retrieves an incident by ID, including its timeline.\n operationId: IncidentService_GetIncident\n requestBody:\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.incident.v1.GetIncidentRequest'\n required: true\n responses:\n \"429\":\n $ref: '#/components/responses/RateLimited'\n default:\n description: Error\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/connect.error'\n \"200\":\n description: Success\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.incident.v1.GetIncidentResponse'\n /rpc/openstatus.incident.v1.IncidentService/GetPostmortem:\n get:\n tags:\n - IncidentService\n summary: GetPostmortem\n description: GetPostmortem retrieves the postmortem of an incident, if any.\n operationId: IncidentService_GetPostmortem.get\n parameters:\n - name: message\n in: query\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.incident.v1.GetPostmortemRequest'\n responses:\n \"429\":\n $ref: '#/components/responses/RateLimited'\n default:\n description: Error\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/connect.error'\n \"200\":\n description: Success\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.incident.v1.GetPostmortemResponse'\n post:\n tags:\n - IncidentService\n summary: GetPostmortem\n description: GetPostmortem retrieves the postmortem of an incident, if any.\n operationId: IncidentService_GetPostmortem\n requestBody:\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.incident.v1.GetPostmortemRequest'\n required: true\n responses:\n \"429\":\n $ref: '#/components/responses/RateLimited'\n default:\n description: Error\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/connect.error'\n \"200\":\n description: Success\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.incident.v1.GetPostmortemResponse'\n /rpc/openstatus.incident.v1.IncidentService/LinkStatusReport:\n post:\n tags:\n - IncidentService\n summary: LinkStatusReport\n description: |-\n LinkStatusReport links a public status report to the incident.\n A report already linked to another incident fails with failed_precondition.\n operationId: IncidentService_LinkStatusReport\n requestBody:\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.incident.v1.LinkStatusReportRequest'\n required: true\n responses:\n \"429\":\n $ref: '#/components/responses/RateLimited'\n default:\n description: Error\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/connect.error'\n \"200\":\n description: Success\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.incident.v1.LinkStatusReportResponse'\n /rpc/openstatus.incident.v1.IncidentService/ListIncidents:\n get:\n tags:\n - IncidentService\n summary: ListIncidents\n description: ListIncidents returns the incidents of the workspace (metadata only), newest declared first.\n operationId: IncidentService_ListIncidents.get\n parameters:\n - name: message\n in: query\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.incident.v1.ListIncidentsRequest'\n responses:\n \"429\":\n $ref: '#/components/responses/RateLimited'\n default:\n description: Error\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/connect.error'\n \"200\":\n description: Success\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.incident.v1.ListIncidentsResponse'\n post:\n tags:\n - IncidentService\n summary: ListIncidents\n description: ListIncidents returns the incidents of the workspace (metadata only), newest declared first.\n operationId: IncidentService_ListIncidents\n requestBody:\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.incident.v1.ListIncidentsRequest'\n required: true\n responses:\n \"429\":\n $ref: '#/components/responses/RateLimited'\n default:\n description: Error\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/connect.error'\n \"200\":\n description: Success\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.incident.v1.ListIncidentsResponse'\n /rpc/openstatus.incident.v1.IncidentService/SetIncidentStatus:\n post:\n tags:\n - IncidentService\n summary: SetIncidentStatus\n description: 'Moves an incident to a new status. Allowed transitions: open -> mitigated, resolved or canceled; mitigated -> resolved, open or canceled; resolved -> open. Canceled is terminal and closes the incident. The same status, a forbidden transition or a closed incident fail with failed_precondition; Incident.allowed_transitions lists what is valid. A linked status report is never updated: post the public update with StatusReportService.AddStatusReportUpdate.'\n operationId: IncidentService_SetIncidentStatus\n requestBody:\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.incident.v1.SetIncidentStatusRequest'\n required: true\n responses:\n \"429\":\n $ref: '#/components/responses/RateLimited'\n default:\n description: Error\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/connect.error'\n \"200\":\n description: Success\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.incident.v1.SetIncidentStatusResponse'\n /rpc/openstatus.incident.v1.IncidentService/UnlinkStatusReport:\n post:\n tags:\n - IncidentService\n summary: UnlinkStatusReport\n description: UnlinkStatusReport removes the link to the incident's status report.\n operationId: IncidentService_UnlinkStatusReport\n requestBody:\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.incident.v1.UnlinkStatusReportRequest'\n required: true\n responses:\n \"429\":\n $ref: '#/components/responses/RateLimited'\n default:\n description: Error\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/connect.error'\n \"200\":\n description: Success\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.incident.v1.UnlinkStatusReportResponse'\n /rpc/openstatus.incident.v1.IncidentService/UpdateIncident:\n post:\n tags:\n - IncidentService\n summary: UpdateIncident\n description: UpdateIncident edits the title, severity, summary, commander or start time of an incident.\n operationId: IncidentService_UpdateIncident\n requestBody:\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.incident.v1.UpdateIncidentRequest'\n required: true\n responses:\n \"429\":\n $ref: '#/components/responses/RateLimited'\n default:\n description: Error\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/connect.error'\n \"200\":\n description: Success\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.incident.v1.UpdateIncidentResponse'\n /rpc/openstatus.incident.v1.IncidentService/UpdatePostmortem:\n post:\n tags:\n - IncidentService\n summary: UpdatePostmortem\n description: UpdatePostmortem creates or replaces the postmortem body of a resolved incident.\n operationId: IncidentService_UpdatePostmortem\n requestBody:\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.incident.v1.UpdatePostmortemRequest'\n required: true\n responses:\n \"429\":\n $ref: '#/components/responses/RateLimited'\n default:\n description: Error\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/connect.error'\n \"200\":\n description: Success\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.incident.v1.UpdatePostmortemResponse'\n /rpc/openstatus.maintenance.v1.MaintenanceService/AddMaintenanceUpdate:\n post:\n tags:\n - MaintenanceService\n summary: AddMaintenanceUpdate\n description: AddMaintenanceUpdate appends a public update to a maintenance timeline.\n operationId: MaintenanceService_AddMaintenanceUpdate\n requestBody:\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.maintenance.v1.AddMaintenanceUpdateRequest'\n required: true\n responses:\n \"429\":\n $ref: '#/components/responses/RateLimited'\n default:\n description: Error\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/connect.error'\n \"200\":\n description: Success\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.maintenance.v1.AddMaintenanceUpdateResponse'\n /rpc/openstatus.maintenance.v1.MaintenanceService/CreateMaintenance:\n post:\n tags:\n - MaintenanceService\n summary: CreateMaintenance\n description: CreateMaintenance creates a new maintenance window.\n operationId: MaintenanceService_CreateMaintenance\n requestBody:\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.maintenance.v1.CreateMaintenanceRequest'\n required: true\n responses:\n \"429\":\n $ref: '#/components/responses/RateLimited'\n default:\n description: Error\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/connect.error'\n \"200\":\n description: Success\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.maintenance.v1.CreateMaintenanceResponse'\n /rpc/openstatus.maintenance.v1.MaintenanceService/DeleteMaintenance:\n post:\n tags:\n - MaintenanceService\n summary: DeleteMaintenance\n description: DeleteMaintenance removes a maintenance window.\n operationId: MaintenanceService_DeleteMaintenance\n requestBody:\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.maintenance.v1.DeleteMaintenanceRequest'\n required: true\n responses:\n \"429\":\n $ref: '#/components/responses/RateLimited'\n default:\n description: Error\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/connect.error'\n \"200\":\n description: Success\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.maintenance.v1.DeleteMaintenanceResponse'\n /rpc/openstatus.maintenance.v1.MaintenanceService/DeleteMaintenanceUpdate:\n post:\n tags:\n - MaintenanceService\n summary: DeleteMaintenanceUpdate\n description: DeleteMaintenanceUpdate removes an existing maintenance update.\n operationId: MaintenanceService_DeleteMaintenanceUpdate\n requestBody:\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.maintenance.v1.DeleteMaintenanceUpdateRequest'\n required: true\n responses:\n \"429\":\n $ref: '#/components/responses/RateLimited'\n default:\n description: Error\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/connect.error'\n \"200\":\n description: Success\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.maintenance.v1.DeleteMaintenanceUpdateResponse'\n /rpc/openstatus.maintenance.v1.MaintenanceService/GetMaintenance:\n get:\n tags:\n - MaintenanceService\n summary: GetMaintenance\n description: GetMaintenance retrieves a specific maintenance window by ID.\n operationId: MaintenanceService_GetMaintenance.get\n parameters:\n - name: message\n in: query\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.maintenance.v1.GetMaintenanceRequest'\n responses:\n \"429\":\n $ref: '#/components/responses/RateLimited'\n default:\n description: Error\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/connect.error'\n \"200\":\n description: Success\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.maintenance.v1.GetMaintenanceResponse'\n post:\n tags:\n - MaintenanceService\n summary: GetMaintenance\n description: GetMaintenance retrieves a specific maintenance window by ID.\n operationId: MaintenanceService_GetMaintenance\n requestBody:\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.maintenance.v1.GetMaintenanceRequest'\n required: true\n responses:\n \"429\":\n $ref: '#/components/responses/RateLimited'\n default:\n description: Error\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/connect.error'\n \"200\":\n description: Success\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.maintenance.v1.GetMaintenanceResponse'\n /rpc/openstatus.maintenance.v1.MaintenanceService/ListMaintenances:\n get:\n tags:\n - MaintenanceService\n summary: ListMaintenances\n description: ListMaintenances returns all maintenance windows for the workspace.\n operationId: MaintenanceService_ListMaintenances.get\n parameters:\n - name: message\n in: query\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.maintenance.v1.ListMaintenancesRequest'\n responses:\n \"429\":\n $ref: '#/components/responses/RateLimited'\n default:\n description: Error\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/connect.error'\n \"200\":\n description: Success\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.maintenance.v1.ListMaintenancesResponse'\n post:\n tags:\n - MaintenanceService\n summary: ListMaintenances\n description: ListMaintenances returns all maintenance windows for the workspace.\n operationId: MaintenanceService_ListMaintenances\n requestBody:\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.maintenance.v1.ListMaintenancesRequest'\n required: true\n responses:\n \"429\":\n $ref: '#/components/responses/RateLimited'\n default:\n description: Error\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/connect.error'\n \"200\":\n description: Success\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.maintenance.v1.ListMaintenancesResponse'\n /rpc/openstatus.maintenance.v1.MaintenanceService/UpdateMaintenance:\n post:\n tags:\n - MaintenanceService\n summary: UpdateMaintenance\n description: UpdateMaintenance updates a maintenance window.\n operationId: MaintenanceService_UpdateMaintenance\n requestBody:\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.maintenance.v1.UpdateMaintenanceRequest'\n required: true\n responses:\n \"429\":\n $ref: '#/components/responses/RateLimited'\n default:\n description: Error\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/connect.error'\n \"200\":\n description: Success\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.maintenance.v1.UpdateMaintenanceResponse'\n /rpc/openstatus.maintenance.v1.MaintenanceService/UpdateMaintenanceUpdate:\n post:\n tags:\n - MaintenanceService\n summary: UpdateMaintenanceUpdate\n description: UpdateMaintenanceUpdate edits an existing maintenance update.\n operationId: MaintenanceService_UpdateMaintenanceUpdate\n requestBody:\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.maintenance.v1.UpdateMaintenanceUpdateRequest'\n required: true\n responses:\n \"429\":\n $ref: '#/components/responses/RateLimited'\n default:\n description: Error\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/connect.error'\n \"200\":\n description: Success\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.maintenance.v1.UpdateMaintenanceUpdateResponse'\n /rpc/openstatus.monitor.v1.MonitorService/CreateDNSMonitor:\n post:\n tags:\n - MonitorService\n summary: CreateDNSMonitor\n description: CreateDNSMonitor creates a new DNS monitor.\n operationId: MonitorService_CreateDNSMonitor\n requestBody:\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.monitor.v1.CreateDNSMonitorRequest'\n required: true\n responses:\n \"429\":\n $ref: '#/components/responses/RateLimited'\n default:\n description: Error\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/connect.error'\n \"200\":\n description: Success\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.monitor.v1.CreateDNSMonitorResponse'\n /rpc/openstatus.monitor.v1.MonitorService/CreateGRPCMonitor:\n post:\n tags:\n - MonitorService\n summary: CreateGRPCMonitor\n description: CreateGRPCMonitor creates a new gRPC health check monitor.\n operationId: MonitorService_CreateGRPCMonitor\n requestBody:\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.monitor.v1.CreateGRPCMonitorRequest'\n required: true\n responses:\n \"429\":\n $ref: '#/components/responses/RateLimited'\n default:\n description: Error\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/connect.error'\n \"200\":\n description: Success\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.monitor.v1.CreateGRPCMonitorResponse'\n /rpc/openstatus.monitor.v1.MonitorService/CreateHTTPMonitor:\n post:\n tags:\n - MonitorService\n summary: CreateHTTPMonitor\n description: Creates a new HTTP monitor in the authenticated workspace. Configure the target URL, HTTP method, request headers and body, response assertions (status code, body content, headers), check periodicity, geographic regions, and optional OpenTelemetry export. The monitor starts checking immediately if set to active.\n operationId: MonitorService_CreateHTTPMonitor\n requestBody:\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.monitor.v1.CreateHTTPMonitorRequest'\n required: true\n responses:\n \"429\":\n $ref: '#/components/responses/RateLimited'\n default:\n description: Error\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/connect.error'\n \"200\":\n description: Success\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.monitor.v1.CreateHTTPMonitorResponse'\n /rpc/openstatus.monitor.v1.MonitorService/CreateICMPMonitor:\n post:\n tags:\n - MonitorService\n summary: CreateICMPMonitor\n description: CreateICMPMonitor creates a new ICMP monitor.\n operationId: MonitorService_CreateICMPMonitor\n requestBody:\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.monitor.v1.CreateICMPMonitorRequest'\n required: true\n responses:\n \"429\":\n $ref: '#/components/responses/RateLimited'\n default:\n description: Error\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/connect.error'\n \"200\":\n description: Success\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.monitor.v1.CreateICMPMonitorResponse'\n /rpc/openstatus.monitor.v1.MonitorService/CreateTCPMonitor:\n post:\n tags:\n - MonitorService\n summary: CreateTCPMonitor\n description: CreateTCPMonitor creates a new TCP monitor.\n operationId: MonitorService_CreateTCPMonitor\n requestBody:\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.monitor.v1.CreateTCPMonitorRequest'\n required: true\n responses:\n \"429\":\n $ref: '#/components/responses/RateLimited'\n default:\n description: Error\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/connect.error'\n \"200\":\n description: Success\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.monitor.v1.CreateTCPMonitorResponse'\n /rpc/openstatus.monitor.v1.MonitorService/DeleteMonitor:\n post:\n tags:\n - MonitorService\n summary: DeleteMonitor\n description: DeleteMonitor removes a monitor.\n operationId: MonitorService_DeleteMonitor\n requestBody:\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.monitor.v1.DeleteMonitorRequest'\n required: true\n responses:\n \"429\":\n $ref: '#/components/responses/RateLimited'\n default:\n description: Error\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/connect.error'\n \"200\":\n description: Success\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.monitor.v1.DeleteMonitorResponse'\n /rpc/openstatus.monitor.v1.MonitorService/GetMonitor:\n get:\n tags:\n - MonitorService\n summary: GetMonitor\n description: |-\n GetMonitor returns a single monitor by ID within the authenticated workspace.\n Returns the monitor configuration (HTTP, TCP, DNS, ICMP, or gRPC) using the MonitorConfig oneof type.\n operationId: MonitorService_GetMonitor.get\n parameters:\n - name: message\n in: query\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.monitor.v1.GetMonitorRequest'\n responses:\n \"429\":\n $ref: '#/components/responses/RateLimited'\n default:\n description: Error\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/connect.error'\n \"200\":\n description: Success\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.monitor.v1.GetMonitorResponse'\n post:\n tags:\n - MonitorService\n summary: GetMonitor\n description: |-\n GetMonitor returns a single monitor by ID within the authenticated workspace.\n Returns the monitor configuration (HTTP, TCP, DNS, ICMP, or gRPC) using the MonitorConfig oneof type.\n operationId: MonitorService_GetMonitor\n requestBody:\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.monitor.v1.GetMonitorRequest'\n required: true\n responses:\n \"429\":\n $ref: '#/components/responses/RateLimited'\n default:\n description: Error\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/connect.error'\n \"200\":\n description: Success\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.monitor.v1.GetMonitorResponse'\n /rpc/openstatus.monitor.v1.MonitorService/GetMonitorHTTPResponseLog:\n get:\n tags:\n - MonitorService\n summary: GetMonitorHTTPResponseLog\n description: GetMonitorHTTPResponseLog returns one response log for an HTTP monitor.\n operationId: MonitorService_GetMonitorHTTPResponseLog.get\n parameters:\n - name: message\n in: query\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.monitor.v1.GetMonitorHTTPResponseLogRequest'\n responses:\n \"429\":\n $ref: '#/components/responses/RateLimited'\n default:\n description: Error\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/connect.error'\n \"200\":\n description: Success\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.monitor.v1.GetMonitorHTTPResponseLogResponse'\n post:\n tags:\n - MonitorService\n summary: GetMonitorHTTPResponseLog\n description: GetMonitorHTTPResponseLog returns one response log for an HTTP monitor.\n operationId: MonitorService_GetMonitorHTTPResponseLog\n requestBody:\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.monitor.v1.GetMonitorHTTPResponseLogRequest'\n required: true\n responses:\n \"429\":\n $ref: '#/components/responses/RateLimited'\n default:\n description: Error\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/connect.error'\n \"200\":\n description: Success\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.monitor.v1.GetMonitorHTTPResponseLogResponse'\n /rpc/openstatus.monitor.v1.MonitorService/GetMonitorStatus:\n get:\n tags:\n - MonitorService\n summary: GetMonitorStatus\n description: GetMonitorStatus returns the current status of all regions for a monitor.\n operationId: MonitorService_GetMonitorStatus.get\n parameters:\n - name: message\n in: query\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.monitor.v1.GetMonitorStatusRequest'\n responses:\n \"429\":\n $ref: '#/components/responses/RateLimited'\n default:\n description: Error\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/connect.error'\n \"200\":\n description: Success\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.monitor.v1.GetMonitorStatusResponse'\n post:\n tags:\n - MonitorService\n summary: GetMonitorStatus\n description: GetMonitorStatus returns the current status of all regions for a monitor.\n operationId: MonitorService_GetMonitorStatus\n requestBody:\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.monitor.v1.GetMonitorStatusRequest'\n required: true\n responses:\n \"429\":\n $ref: '#/components/responses/RateLimited'\n default:\n description: Error\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/connect.error'\n \"200\":\n description: Success\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.monitor.v1.GetMonitorStatusResponse'\n /rpc/openstatus.monitor.v1.MonitorService/GetMonitorSummary:\n get:\n tags:\n - MonitorService\n summary: GetMonitorSummary\n description: Returns aggregated metrics for a monitor including latency percentiles (p50, p75, p90, p95, p99), request counts by status (successful, degraded, failed), and the timestamp of the last check. Metrics can be scoped to a time range (1 day, 7 days, or 14 days) and filtered by specific regions.\n operationId: MonitorService_GetMonitorSummary.get\n parameters:\n - name: message\n in: query\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.monitor.v1.GetMonitorSummaryRequest'\n responses:\n \"429\":\n $ref: '#/components/responses/RateLimited'\n default:\n description: Error\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/connect.error'\n \"200\":\n description: Success\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.monitor.v1.GetMonitorSummaryResponse'\n post:\n tags:\n - MonitorService\n summary: GetMonitorSummary\n description: Returns aggregated metrics for a monitor including latency percentiles (p50, p75, p90, p95, p99), request counts by status (successful, degraded, failed), and the timestamp of the last check. Metrics can be scoped to a time range (1 day, 7 days, or 14 days) and filtered by specific regions.\n operationId: MonitorService_GetMonitorSummary\n requestBody:\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.monitor.v1.GetMonitorSummaryRequest'\n required: true\n responses:\n \"429\":\n $ref: '#/components/responses/RateLimited'\n default:\n description: Error\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/connect.error'\n \"200\":\n description: Success\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.monitor.v1.GetMonitorSummaryResponse'\n /rpc/openstatus.monitor.v1.MonitorService/ListMonitorHTTPResponseLogs:\n get:\n tags:\n - MonitorService\n summary: ListMonitorHTTPResponseLogs\n description: ListMonitorHTTPResponseLogs returns paginated response logs for an HTTP monitor from the 14-day HTTP response-log window.\n operationId: MonitorService_ListMonitorHTTPResponseLogs.get\n parameters:\n - name: message\n in: query\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.monitor.v1.ListMonitorHTTPResponseLogsRequest'\n responses:\n \"429\":\n $ref: '#/components/responses/RateLimited'\n default:\n description: Error\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/connect.error'\n \"200\":\n description: Success\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.monitor.v1.ListMonitorHTTPResponseLogsResponse'\n post:\n tags:\n - MonitorService\n summary: ListMonitorHTTPResponseLogs\n description: ListMonitorHTTPResponseLogs returns paginated response logs for an HTTP monitor from the 14-day HTTP response-log window.\n operationId: MonitorService_ListMonitorHTTPResponseLogs\n requestBody:\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.monitor.v1.ListMonitorHTTPResponseLogsRequest'\n required: true\n responses:\n \"429\":\n $ref: '#/components/responses/RateLimited'\n default:\n description: Error\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/connect.error'\n \"200\":\n description: Success\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.monitor.v1.ListMonitorHTTPResponseLogsResponse'\n /rpc/openstatus.monitor.v1.MonitorService/ListMonitors:\n get:\n tags:\n - MonitorService\n summary: ListMonitors\n description: ListMonitors returns a paginated list of all monitors in the workspace.\n operationId: MonitorService_ListMonitors.get\n parameters:\n - name: message\n in: query\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.monitor.v1.ListMonitorsRequest'\n responses:\n \"429\":\n $ref: '#/components/responses/RateLimited'\n default:\n description: Error\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/connect.error'\n \"200\":\n description: Success\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.monitor.v1.ListMonitorsResponse'\n post:\n tags:\n - MonitorService\n summary: ListMonitors\n description: ListMonitors returns a paginated list of all monitors in the workspace.\n operationId: MonitorService_ListMonitors\n requestBody:\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.monitor.v1.ListMonitorsRequest'\n required: true\n responses:\n \"429\":\n $ref: '#/components/responses/RateLimited'\n default:\n description: Error\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/connect.error'\n \"200\":\n description: Success\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.monitor.v1.ListMonitorsResponse'\n /rpc/openstatus.monitor.v1.MonitorService/TriggerMonitor:\n post:\n tags:\n - MonitorService\n summary: TriggerMonitor\n description: Manually triggers an immediate check for the specified monitor across all configured regions. This operation is rate-limited under the synthetic-checks quota. A monitor run record is created and the check is dispatched to the checker service.\n operationId: MonitorService_TriggerMonitor\n requestBody:\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.monitor.v1.TriggerMonitorRequest'\n required: true\n responses:\n \"429\":\n $ref: '#/components/responses/RateLimited'\n default:\n description: Error\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/connect.error'\n \"200\":\n description: Success\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.monitor.v1.TriggerMonitorResponse'\n /rpc/openstatus.monitor.v1.MonitorService/UpdateDNSMonitor:\n post:\n tags:\n - MonitorService\n summary: UpdateDNSMonitor\n description: UpdateDNSMonitor updates an existing DNS monitor.\n operationId: MonitorService_UpdateDNSMonitor\n requestBody:\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.monitor.v1.UpdateDNSMonitorRequest'\n required: true\n responses:\n \"429\":\n $ref: '#/components/responses/RateLimited'\n default:\n description: Error\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/connect.error'\n \"200\":\n description: Success\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.monitor.v1.UpdateDNSMonitorResponse'\n /rpc/openstatus.monitor.v1.MonitorService/UpdateGRPCMonitor:\n post:\n tags:\n - MonitorService\n summary: UpdateGRPCMonitor\n description: UpdateGRPCMonitor updates an existing gRPC monitor.\n operationId: MonitorService_UpdateGRPCMonitor\n requestBody:\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.monitor.v1.UpdateGRPCMonitorRequest'\n required: true\n responses:\n \"429\":\n $ref: '#/components/responses/RateLimited'\n default:\n description: Error\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/connect.error'\n \"200\":\n description: Success\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.monitor.v1.UpdateGRPCMonitorResponse'\n /rpc/openstatus.monitor.v1.MonitorService/UpdateHTTPMonitor:\n post:\n tags:\n - MonitorService\n summary: UpdateHTTPMonitor\n description: UpdateHTTPMonitor updates an existing HTTP monitor.\n operationId: MonitorService_UpdateHTTPMonitor\n requestBody:\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.monitor.v1.UpdateHTTPMonitorRequest'\n required: true\n responses:\n \"429\":\n $ref: '#/components/responses/RateLimited'\n default:\n description: Error\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/connect.error'\n \"200\":\n description: Success\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.monitor.v1.UpdateHTTPMonitorResponse'\n /rpc/openstatus.monitor.v1.MonitorService/UpdateICMPMonitor:\n post:\n tags:\n - MonitorService\n summary: UpdateICMPMonitor\n description: UpdateICMPMonitor updates an existing ICMP monitor.\n operationId: MonitorService_UpdateICMPMonitor\n requestBody:\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.monitor.v1.UpdateICMPMonitorRequest'\n required: true\n responses:\n \"429\":\n $ref: '#/components/responses/RateLimited'\n default:\n description: Error\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/connect.error'\n \"200\":\n description: Success\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.monitor.v1.UpdateICMPMonitorResponse'\n /rpc/openstatus.monitor.v1.MonitorService/UpdateTCPMonitor:\n post:\n tags:\n - MonitorService\n summary: UpdateTCPMonitor\n description: UpdateTCPMonitor updates an existing TCP monitor.\n operationId: MonitorService_UpdateTCPMonitor\n requestBody:\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.monitor.v1.UpdateTCPMonitorRequest'\n required: true\n responses:\n \"429\":\n $ref: '#/components/responses/RateLimited'\n default:\n description: Error\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/connect.error'\n \"200\":\n description: Success\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.monitor.v1.UpdateTCPMonitorResponse'\n /rpc/openstatus.notification.v1.NotificationService/CheckNotificationLimit:\n get:\n tags:\n - NotificationService\n summary: CheckNotificationLimit\n description: CheckNotificationLimit checks if the workspace has reached its notification limit.\n operationId: NotificationService_CheckNotificationLimit.get\n parameters:\n - name: message\n in: query\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.notification.v1.CheckNotificationLimitRequest'\n responses:\n \"429\":\n $ref: '#/components/responses/RateLimited'\n default:\n description: Error\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/connect.error'\n \"200\":\n description: Success\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.notification.v1.CheckNotificationLimitResponse'\n post:\n tags:\n - NotificationService\n summary: CheckNotificationLimit\n description: CheckNotificationLimit checks if the workspace has reached its notification limit.\n operationId: NotificationService_CheckNotificationLimit\n requestBody:\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.notification.v1.CheckNotificationLimitRequest'\n required: true\n responses:\n \"429\":\n $ref: '#/components/responses/RateLimited'\n default:\n description: Error\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/connect.error'\n \"200\":\n description: Success\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.notification.v1.CheckNotificationLimitResponse'\n /rpc/openstatus.notification.v1.NotificationService/CreateNotification:\n post:\n tags:\n - NotificationService\n summary: CreateNotification\n description: CreateNotification creates a new notification channel.\n operationId: NotificationService_CreateNotification\n requestBody:\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.notification.v1.CreateNotificationRequest'\n required: true\n responses:\n \"429\":\n $ref: '#/components/responses/RateLimited'\n default:\n description: Error\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/connect.error'\n \"200\":\n description: Success\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.notification.v1.CreateNotificationResponse'\n /rpc/openstatus.notification.v1.NotificationService/DeleteNotification:\n post:\n tags:\n - NotificationService\n summary: DeleteNotification\n description: DeleteNotification removes a notification channel.\n operationId: NotificationService_DeleteNotification\n requestBody:\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.notification.v1.DeleteNotificationRequest'\n required: true\n responses:\n \"429\":\n $ref: '#/components/responses/RateLimited'\n default:\n description: Error\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/connect.error'\n \"200\":\n description: Success\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.notification.v1.DeleteNotificationResponse'\n /rpc/openstatus.notification.v1.NotificationService/GetNotification:\n get:\n tags:\n - NotificationService\n summary: GetNotification\n description: GetNotification retrieves a notification channel by ID.\n operationId: NotificationService_GetNotification.get\n parameters:\n - name: message\n in: query\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.notification.v1.GetNotificationRequest'\n responses:\n \"429\":\n $ref: '#/components/responses/RateLimited'\n default:\n description: Error\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/connect.error'\n \"200\":\n description: Success\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.notification.v1.GetNotificationResponse'\n post:\n tags:\n - NotificationService\n summary: GetNotification\n description: GetNotification retrieves a notification channel by ID.\n operationId: NotificationService_GetNotification\n requestBody:\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.notification.v1.GetNotificationRequest'\n required: true\n responses:\n \"429\":\n $ref: '#/components/responses/RateLimited'\n default:\n description: Error\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/connect.error'\n \"200\":\n description: Success\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.notification.v1.GetNotificationResponse'\n /rpc/openstatus.notification.v1.NotificationService/ListNotifications:\n get:\n tags:\n - NotificationService\n summary: ListNotifications\n description: ListNotifications returns a list of notification channels.\n operationId: NotificationService_ListNotifications.get\n parameters:\n - name: message\n in: query\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.notification.v1.ListNotificationsRequest'\n responses:\n \"429\":\n $ref: '#/components/responses/RateLimited'\n default:\n description: Error\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/connect.error'\n \"200\":\n description: Success\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.notification.v1.ListNotificationsResponse'\n post:\n tags:\n - NotificationService\n summary: ListNotifications\n description: ListNotifications returns a list of notification channels.\n operationId: NotificationService_ListNotifications\n requestBody:\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.notification.v1.ListNotificationsRequest'\n required: true\n responses:\n \"429\":\n $ref: '#/components/responses/RateLimited'\n default:\n description: Error\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/connect.error'\n \"200\":\n description: Success\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.notification.v1.ListNotificationsResponse'\n /rpc/openstatus.notification.v1.NotificationService/SendTestNotification:\n post:\n tags:\n - NotificationService\n summary: SendTestNotification\n description: Sends a test notification to the specified provider to verify that the configuration is correct. This does not require an existing notification channel - just provide the provider type and its configuration data. Returns success status and an error message if the test failed.\n operationId: NotificationService_SendTestNotification\n requestBody:\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.notification.v1.SendTestNotificationRequest'\n required: true\n responses:\n \"429\":\n $ref: '#/components/responses/RateLimited'\n default:\n description: Error\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/connect.error'\n \"200\":\n description: Success\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.notification.v1.SendTestNotificationResponse'\n /rpc/openstatus.notification.v1.NotificationService/UpdateNotification:\n post:\n tags:\n - NotificationService\n summary: UpdateNotification\n description: UpdateNotification updates an existing notification channel.\n operationId: NotificationService_UpdateNotification\n requestBody:\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.notification.v1.UpdateNotificationRequest'\n required: true\n responses:\n \"429\":\n $ref: '#/components/responses/RateLimited'\n default:\n description: Error\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/connect.error'\n \"200\":\n description: Success\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.notification.v1.UpdateNotificationResponse'\n /rpc/openstatus.private_location.v1.PrivateLocationService/CreatePrivateLocation:\n post:\n tags:\n - PrivateLocationService\n summary: CreatePrivateLocation\n description: Creates a private location. The agent token is generated by the server and returned in the response - it cannot be supplied by the caller. Use the token to configure the agent so it can pull its monitors and report results.\n operationId: PrivateLocationService_CreatePrivateLocation\n requestBody:\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.private_location.v1.CreatePrivateLocationRequest'\n required: true\n responses:\n \"429\":\n $ref: '#/components/responses/RateLimited'\n default:\n description: Error\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/connect.error'\n \"200\":\n description: Success\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.private_location.v1.CreatePrivateLocationResponse'\n /rpc/openstatus.private_location.v1.PrivateLocationService/DeletePrivateLocation:\n post:\n tags:\n - PrivateLocationService\n summary: DeletePrivateLocation\n description: DeletePrivateLocation removes a private location.\n operationId: PrivateLocationService_DeletePrivateLocation\n requestBody:\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.private_location.v1.DeletePrivateLocationRequest'\n required: true\n responses:\n \"429\":\n $ref: '#/components/responses/RateLimited'\n default:\n description: Error\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/connect.error'\n \"200\":\n description: Success\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.private_location.v1.DeletePrivateLocationResponse'\n /rpc/openstatus.private_location.v1.PrivateLocationService/GetPrivateLocation:\n get:\n tags:\n - PrivateLocationService\n summary: GetPrivateLocation\n description: |-\n GetPrivateLocation retrieves a single private location by ID, including\n its agent token.\n operationId: PrivateLocationService_GetPrivateLocation.get\n parameters:\n - name: message\n in: query\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.private_location.v1.GetPrivateLocationRequest'\n responses:\n \"429\":\n $ref: '#/components/responses/RateLimited'\n default:\n description: Error\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/connect.error'\n \"200\":\n description: Success\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.private_location.v1.GetPrivateLocationResponse'\n post:\n tags:\n - PrivateLocationService\n summary: GetPrivateLocation\n description: |-\n GetPrivateLocation retrieves a single private location by ID, including\n its agent token.\n operationId: PrivateLocationService_GetPrivateLocation\n requestBody:\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.private_location.v1.GetPrivateLocationRequest'\n required: true\n responses:\n \"429\":\n $ref: '#/components/responses/RateLimited'\n default:\n description: Error\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/connect.error'\n \"200\":\n description: Success\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.private_location.v1.GetPrivateLocationResponse'\n /rpc/openstatus.private_location.v1.PrivateLocationService/ListPrivateLocations:\n get:\n tags:\n - PrivateLocationService\n summary: ListPrivateLocations\n description: |-\n ListPrivateLocations returns a paginated list of private location\n summaries. Agent tokens are not included - use GetPrivateLocation.\n operationId: PrivateLocationService_ListPrivateLocations.get\n parameters:\n - name: message\n in: query\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.private_location.v1.ListPrivateLocationsRequest'\n responses:\n \"429\":\n $ref: '#/components/responses/RateLimited'\n default:\n description: Error\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/connect.error'\n \"200\":\n description: Success\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.private_location.v1.ListPrivateLocationsResponse'\n post:\n tags:\n - PrivateLocationService\n summary: ListPrivateLocations\n description: |-\n ListPrivateLocations returns a paginated list of private location\n summaries. Agent tokens are not included - use GetPrivateLocation.\n operationId: PrivateLocationService_ListPrivateLocations\n requestBody:\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.private_location.v1.ListPrivateLocationsRequest'\n required: true\n responses:\n \"429\":\n $ref: '#/components/responses/RateLimited'\n default:\n description: Error\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/connect.error'\n \"200\":\n description: Success\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.private_location.v1.ListPrivateLocationsResponse'\n /rpc/openstatus.private_location.v1.PrivateLocationService/UpdatePrivateLocation:\n post:\n tags:\n - PrivateLocationService\n summary: UpdatePrivateLocation\n description: UpdatePrivateLocation updates a private location.\n operationId: PrivateLocationService_UpdatePrivateLocation\n requestBody:\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.private_location.v1.UpdatePrivateLocationRequest'\n required: true\n responses:\n \"429\":\n $ref: '#/components/responses/RateLimited'\n default:\n description: Error\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/connect.error'\n \"200\":\n description: Success\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.private_location.v1.UpdatePrivateLocationResponse'\n /rpc/openstatus.status_page.v1.StatusPageService/AddMonitorComponent:\n post:\n tags:\n - StatusPageService\n summary: AddMonitorComponent\n description: AddMonitorComponent adds a monitor-based component to a status page.\n operationId: StatusPageService_AddMonitorComponent\n requestBody:\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.status_page.v1.AddMonitorComponentRequest'\n required: true\n responses:\n \"429\":\n $ref: '#/components/responses/RateLimited'\n default:\n description: Error\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/connect.error'\n \"200\":\n description: Success\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.status_page.v1.AddMonitorComponentResponse'\n /rpc/openstatus.status_page.v1.StatusPageService/AddStaticComponent:\n post:\n tags:\n - StatusPageService\n summary: AddStaticComponent\n description: AddStaticComponent adds a static component to a status page.\n operationId: StatusPageService_AddStaticComponent\n requestBody:\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.status_page.v1.AddStaticComponentRequest'\n required: true\n responses:\n \"429\":\n $ref: '#/components/responses/RateLimited'\n default:\n description: Error\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/connect.error'\n \"200\":\n description: Success\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.status_page.v1.AddStaticComponentResponse'\n /rpc/openstatus.status_page.v1.StatusPageService/CreateComponentGroup:\n post:\n tags:\n - StatusPageService\n summary: CreateComponentGroup\n description: CreateComponentGroup creates a new component group.\n operationId: StatusPageService_CreateComponentGroup\n requestBody:\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.status_page.v1.CreateComponentGroupRequest'\n required: true\n responses:\n \"429\":\n $ref: '#/components/responses/RateLimited'\n default:\n description: Error\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/connect.error'\n \"200\":\n description: Success\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.status_page.v1.CreateComponentGroupResponse'\n /rpc/openstatus.status_page.v1.StatusPageService/CreatePageSubscription:\n post:\n tags:\n - StatusPageService\n summary: 'CreatePageSubscription: operator-added subscriber (email or webhook), no verification.'\n description: Operator-added subscriber (email or webhook) with no verification flow — the partner starts receiving notifications immediately. Supports Slack and Discord webhook URLs in addition to email. A management token is still generated so the subscriber can self-manage (update scope, unsubscribe) without operator involvement. Use this for vendor/partner integrations where consent is established out-of-band; use SubscribeToPage for subscriber-initiated signups that require double opt-in.\n operationId: StatusPageService_CreatePageSubscription\n requestBody:\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.status_page.v1.CreatePageSubscriptionRequest'\n required: true\n responses:\n \"429\":\n $ref: '#/components/responses/RateLimited'\n default:\n description: Error\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/connect.error'\n \"200\":\n description: Success\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.status_page.v1.CreatePageSubscriptionResponse'\n /rpc/openstatus.status_page.v1.StatusPageService/CreateStatusPage:\n post:\n tags:\n - StatusPageService\n summary: CreateStatusPage\n description: CreateStatusPage creates a new status page.\n operationId: StatusPageService_CreateStatusPage\n requestBody:\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.status_page.v1.CreateStatusPageRequest'\n required: true\n responses:\n \"429\":\n $ref: '#/components/responses/RateLimited'\n default:\n description: Error\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/connect.error'\n \"200\":\n description: Success\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.status_page.v1.CreateStatusPageResponse'\n /rpc/openstatus.status_page.v1.StatusPageService/DeleteComponentGroup:\n post:\n tags:\n - StatusPageService\n summary: DeleteComponentGroup\n description: DeleteComponentGroup removes a component group.\n operationId: StatusPageService_DeleteComponentGroup\n requestBody:\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.status_page.v1.DeleteComponentGroupRequest'\n required: true\n responses:\n \"429\":\n $ref: '#/components/responses/RateLimited'\n default:\n description: Error\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/connect.error'\n \"200\":\n description: Success\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.status_page.v1.DeleteComponentGroupResponse'\n /rpc/openstatus.status_page.v1.StatusPageService/DeleteStatusPage:\n post:\n tags:\n - StatusPageService\n summary: DeleteStatusPage\n description: DeleteStatusPage removes a status page.\n operationId: StatusPageService_DeleteStatusPage\n requestBody:\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.status_page.v1.DeleteStatusPageRequest'\n required: true\n responses:\n \"429\":\n $ref: '#/components/responses/RateLimited'\n default:\n description: Error\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/connect.error'\n \"200\":\n description: Success\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.status_page.v1.DeleteStatusPageResponse'\n /rpc/openstatus.status_page.v1.StatusPageService/GetOverallStatus:\n get:\n tags:\n - StatusPageService\n summary: GetOverallStatus\n description: 'Returns the overall status of a status page along with individual component statuses. The overall status is computed from active status reports and maintenances with the following priority: degraded (from active status reports) > maintenance (from active maintenance windows) > operational.'\n operationId: StatusPageService_GetOverallStatus.get\n parameters:\n - name: message\n in: query\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.status_page.v1.GetOverallStatusRequest'\n responses:\n \"429\":\n $ref: '#/components/responses/RateLimited'\n default:\n description: Error\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/connect.error'\n \"200\":\n description: Success\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.status_page.v1.GetOverallStatusResponse'\n post:\n tags:\n - StatusPageService\n summary: GetOverallStatus\n description: 'Returns the overall status of a status page along with individual component statuses. The overall status is computed from active status reports and maintenances with the following priority: degraded (from active status reports) > maintenance (from active maintenance windows) > operational.'\n operationId: StatusPageService_GetOverallStatus\n requestBody:\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.status_page.v1.GetOverallStatusRequest'\n required: true\n responses:\n \"429\":\n $ref: '#/components/responses/RateLimited'\n default:\n description: Error\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/connect.error'\n \"200\":\n description: Success\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.status_page.v1.GetOverallStatusResponse'\n /rpc/openstatus.status_page.v1.StatusPageService/GetPageComponent:\n get:\n tags:\n - StatusPageService\n summary: GetPageComponent\n description: Returns a single status-page component by its ID, scoped to the authenticated workspace. Use this to resolve a component id to its name (and other fields) instead of fetching the whole page via GetStatusPageContent and filtering .components.\n operationId: StatusPageService_GetPageComponent.get\n parameters:\n - name: message\n in: query\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.status_page.v1.GetPageComponentRequest'\n responses:\n \"429\":\n $ref: '#/components/responses/RateLimited'\n default:\n description: Error\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/connect.error'\n \"200\":\n description: Success\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.status_page.v1.GetPageComponentResponse'\n post:\n tags:\n - StatusPageService\n summary: GetPageComponent\n description: Returns a single status-page component by its ID, scoped to the authenticated workspace. Use this to resolve a component id to its name (and other fields) instead of fetching the whole page via GetStatusPageContent and filtering .components.\n operationId: StatusPageService_GetPageComponent\n requestBody:\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.status_page.v1.GetPageComponentRequest'\n required: true\n responses:\n \"429\":\n $ref: '#/components/responses/RateLimited'\n default:\n description: Error\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/connect.error'\n \"200\":\n description: Success\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.status_page.v1.GetPageComponentResponse'\n /rpc/openstatus.status_page.v1.StatusPageService/GetPageComponentDailySummary:\n get:\n tags:\n - StatusPageService\n summary: GetPageComponentDailySummary\n description: 'Returns per-component daily status buckets (ok/degraded/error/count plus a resolved status) merged with the incident, maintenance, and status-report timeline over the last N days (max 45). Suitable as a single source of truth for status-bar and uptime-calendar rendering. Supports two access paths: by id (authenticated, workspace-scoped) or by slug (public access, requires the page to be published with access_type=PUBLIC).'\n operationId: StatusPageService_GetPageComponentDailySummary.get\n parameters:\n - name: message\n in: query\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.status_page.v1.GetPageComponentDailySummaryRequest'\n responses:\n \"429\":\n $ref: '#/components/responses/RateLimited'\n default:\n description: Error\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/connect.error'\n \"200\":\n description: Success\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.status_page.v1.GetPageComponentDailySummaryResponse'\n post:\n tags:\n - StatusPageService\n summary: GetPageComponentDailySummary\n description: 'Returns per-component daily status buckets (ok/degraded/error/count plus a resolved status) merged with the incident, maintenance, and status-report timeline over the last N days (max 45). Suitable as a single source of truth for status-bar and uptime-calendar rendering. Supports two access paths: by id (authenticated, workspace-scoped) or by slug (public access, requires the page to be published with access_type=PUBLIC).'\n operationId: StatusPageService_GetPageComponentDailySummary\n requestBody:\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.status_page.v1.GetPageComponentDailySummaryRequest'\n required: true\n responses:\n \"429\":\n $ref: '#/components/responses/RateLimited'\n default:\n description: Error\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/connect.error'\n \"200\":\n description: Success\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.status_page.v1.GetPageComponentDailySummaryResponse'\n /rpc/openstatus.status_page.v1.StatusPageService/GetStatusPage:\n get:\n tags:\n - StatusPageService\n summary: GetStatusPage\n description: GetStatusPage retrieves a specific status page by ID.\n operationId: StatusPageService_GetStatusPage.get\n parameters:\n - name: message\n in: query\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.status_page.v1.GetStatusPageRequest'\n responses:\n \"429\":\n $ref: '#/components/responses/RateLimited'\n default:\n description: Error\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/connect.error'\n \"200\":\n description: Success\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.status_page.v1.GetStatusPageResponse'\n post:\n tags:\n - StatusPageService\n summary: GetStatusPage\n description: GetStatusPage retrieves a specific status page by ID.\n operationId: StatusPageService_GetStatusPage\n requestBody:\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.status_page.v1.GetStatusPageRequest'\n required: true\n responses:\n \"429\":\n $ref: '#/components/responses/RateLimited'\n default:\n description: Error\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/connect.error'\n \"200\":\n description: Success\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.status_page.v1.GetStatusPageResponse'\n /rpc/openstatus.status_page.v1.StatusPageService/GetStatusPageContent:\n get:\n tags:\n - StatusPageService\n summary: GetStatusPageContent\n description: 'Returns the full content of a status page including its components, component groups, active status reports, and scheduled maintenances. Supports two access paths: by ID (requires authentication, workspace-scoped) or by slug (public access, requires the page to be published with access_type=PUBLIC).'\n operationId: StatusPageService_GetStatusPageContent.get\n parameters:\n - name: message\n in: query\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.status_page.v1.GetStatusPageContentRequest'\n responses:\n \"429\":\n $ref: '#/components/responses/RateLimited'\n default:\n description: Error\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/connect.error'\n \"200\":\n description: Success\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.status_page.v1.GetStatusPageContentResponse'\n post:\n tags:\n - StatusPageService\n summary: GetStatusPageContent\n description: 'Returns the full content of a status page including its components, component groups, active status reports, and scheduled maintenances. Supports two access paths: by ID (requires authentication, workspace-scoped) or by slug (public access, requires the page to be published with access_type=PUBLIC).'\n operationId: StatusPageService_GetStatusPageContent\n requestBody:\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.status_page.v1.GetStatusPageContentRequest'\n required: true\n responses:\n \"429\":\n $ref: '#/components/responses/RateLimited'\n default:\n description: Error\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/connect.error'\n \"200\":\n description: Success\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.status_page.v1.GetStatusPageContentResponse'\n /rpc/openstatus.status_page.v1.StatusPageService/GetStatusPageOverview:\n get:\n tags:\n - StatusPageService\n summary: GetStatusPageOverview\n description: 'Returns everything about a single status page in one authenticated call: the page, its rich rendering configuration, components, component groups, active/recent status reports, maintenances, and the computed overall + per-component statuses. Workspace-scoped by id; there is no public slug access path. Does not include uptime time-series — use GetPageComponentDailySummary for that.'\n operationId: StatusPageService_GetStatusPageOverview.get\n parameters:\n - name: message\n in: query\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.status_page.v1.GetStatusPageOverviewRequest'\n responses:\n \"429\":\n $ref: '#/components/responses/RateLimited'\n default:\n description: Error\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/connect.error'\n \"200\":\n description: Success\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.status_page.v1.GetStatusPageOverviewResponse'\n post:\n tags:\n - StatusPageService\n summary: GetStatusPageOverview\n description: 'Returns everything about a single status page in one authenticated call: the page, its rich rendering configuration, components, component groups, active/recent status reports, maintenances, and the computed overall + per-component statuses. Workspace-scoped by id; there is no public slug access path. Does not include uptime time-series — use GetPageComponentDailySummary for that.'\n operationId: StatusPageService_GetStatusPageOverview\n requestBody:\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.status_page.v1.GetStatusPageOverviewRequest'\n required: true\n responses:\n \"429\":\n $ref: '#/components/responses/RateLimited'\n default:\n description: Error\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/connect.error'\n \"200\":\n description: Success\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.status_page.v1.GetStatusPageOverviewResponse'\n /rpc/openstatus.status_page.v1.StatusPageService/ListStatusPages:\n get:\n tags:\n - StatusPageService\n summary: ListStatusPages\n description: ListStatusPages returns all status pages for the workspace.\n operationId: StatusPageService_ListStatusPages.get\n parameters:\n - name: message\n in: query\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.status_page.v1.ListStatusPagesRequest'\n responses:\n \"429\":\n $ref: '#/components/responses/RateLimited'\n default:\n description: Error\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/connect.error'\n \"200\":\n description: Success\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.status_page.v1.ListStatusPagesResponse'\n post:\n tags:\n - StatusPageService\n summary: ListStatusPages\n description: ListStatusPages returns all status pages for the workspace.\n operationId: StatusPageService_ListStatusPages\n requestBody:\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.status_page.v1.ListStatusPagesRequest'\n required: true\n responses:\n \"429\":\n $ref: '#/components/responses/RateLimited'\n default:\n description: Error\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/connect.error'\n \"200\":\n description: Success\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.status_page.v1.ListStatusPagesResponse'\n /rpc/openstatus.status_page.v1.StatusPageService/ListSubscribers:\n get:\n tags:\n - StatusPageService\n summary: ListSubscribers\n description: ListSubscribers returns all subscribers for a status page.\n operationId: StatusPageService_ListSubscribers.get\n parameters:\n - name: message\n in: query\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.status_page.v1.ListSubscribersRequest'\n responses:\n \"429\":\n $ref: '#/components/responses/RateLimited'\n default:\n description: Error\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/connect.error'\n \"200\":\n description: Success\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.status_page.v1.ListSubscribersResponse'\n post:\n tags:\n - StatusPageService\n summary: ListSubscribers\n description: ListSubscribers returns all subscribers for a status page.\n operationId: StatusPageService_ListSubscribers\n requestBody:\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.status_page.v1.ListSubscribersRequest'\n required: true\n responses:\n \"429\":\n $ref: '#/components/responses/RateLimited'\n default:\n description: Error\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/connect.error'\n \"200\":\n description: Success\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.status_page.v1.ListSubscribersResponse'\n /rpc/openstatus.status_page.v1.StatusPageService/RemoveComponent:\n post:\n tags:\n - StatusPageService\n summary: RemoveComponent\n description: RemoveComponent removes a component from a status page.\n operationId: StatusPageService_RemoveComponent\n requestBody:\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.status_page.v1.RemoveComponentRequest'\n required: true\n responses:\n \"429\":\n $ref: '#/components/responses/RateLimited'\n default:\n description: Error\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/connect.error'\n \"200\":\n description: Success\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.status_page.v1.RemoveComponentResponse'\n /rpc/openstatus.status_page.v1.StatusPageService/SubscribeToPage:\n post:\n tags:\n - StatusPageService\n summary: 'SubscribeToPage: end-user email self-signup with double opt-in verification.'\n description: End-user email self-signup with double opt-in verification. A verification email is sent and the subscription activates only after the recipient confirms. If the email was previously unsubscribed, the existing row is reactivated instead of a duplicate being created. Use this for subscriber-initiated signups on the public status page; use CreatePageSubscription for operator-initiated (vendor) subscriptions that should skip verification.\n operationId: StatusPageService_SubscribeToPage\n requestBody:\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.status_page.v1.SubscribeToPageRequest'\n required: true\n responses:\n \"429\":\n $ref: '#/components/responses/RateLimited'\n default:\n description: Error\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/connect.error'\n \"200\":\n description: Success\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.status_page.v1.SubscribeToPageResponse'\n /rpc/openstatus.status_page.v1.StatusPageService/UnsubscribeFromPage:\n post:\n tags:\n - StatusPageService\n summary: UnsubscribeFromPage\n description: UnsubscribeFromPage removes a subscription from a status page.\n operationId: StatusPageService_UnsubscribeFromPage\n requestBody:\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.status_page.v1.UnsubscribeFromPageRequest'\n required: true\n responses:\n \"429\":\n $ref: '#/components/responses/RateLimited'\n default:\n description: Error\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/connect.error'\n \"200\":\n description: Success\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.status_page.v1.UnsubscribeFromPageResponse'\n /rpc/openstatus.status_page.v1.StatusPageService/UpdateComponent:\n post:\n tags:\n - StatusPageService\n summary: UpdateComponent\n description: UpdateComponent updates an existing component.\n operationId: StatusPageService_UpdateComponent\n requestBody:\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.status_page.v1.UpdateComponentRequest'\n required: true\n responses:\n \"429\":\n $ref: '#/components/responses/RateLimited'\n default:\n description: Error\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/connect.error'\n \"200\":\n description: Success\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.status_page.v1.UpdateComponentResponse'\n /rpc/openstatus.status_page.v1.StatusPageService/UpdateComponentGroup:\n post:\n tags:\n - StatusPageService\n summary: UpdateComponentGroup\n description: UpdateComponentGroup updates an existing component group.\n operationId: StatusPageService_UpdateComponentGroup\n requestBody:\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.status_page.v1.UpdateComponentGroupRequest'\n required: true\n responses:\n \"429\":\n $ref: '#/components/responses/RateLimited'\n default:\n description: Error\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/connect.error'\n \"200\":\n description: Success\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.status_page.v1.UpdateComponentGroupResponse'\n /rpc/openstatus.status_page.v1.StatusPageService/UpdateStatusPage:\n post:\n tags:\n - StatusPageService\n summary: UpdateStatusPage\n description: UpdateStatusPage updates an existing status page.\n operationId: StatusPageService_UpdateStatusPage\n requestBody:\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.status_page.v1.UpdateStatusPageRequest'\n required: true\n responses:\n \"429\":\n $ref: '#/components/responses/RateLimited'\n default:\n description: Error\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/connect.error'\n \"200\":\n description: Success\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.status_page.v1.UpdateStatusPageResponse'\n /rpc/openstatus.status_report.v1.StatusReportService/AddStatusReportUpdate:\n post:\n tags:\n - StatusReportService\n summary: AddStatusReportUpdate\n description: 'Adds a new update entry to an existing status report and transitions the report to the specified status. Status reports follow a lifecycle: investigating -> identified -> monitoring -> resolved. If notify is true, subscribers of the associated page are notified by email about the update.'\n operationId: StatusReportService_AddStatusReportUpdate\n requestBody:\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.status_report.v1.AddStatusReportUpdateRequest'\n required: true\n responses:\n \"429\":\n $ref: '#/components/responses/RateLimited'\n default:\n description: Error\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/connect.error'\n \"200\":\n description: Success\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.status_report.v1.AddStatusReportUpdateResponse'\n /rpc/openstatus.status_report.v1.StatusReportService/CreateStatusReport:\n post:\n tags:\n - StatusReportService\n summary: CreateStatusReport\n description: Creates a new status report with an initial update entry. The report is associated with a status page and optionally specific page components. An initial StatusReportUpdate is created automatically with the provided status, message, and date. If notify is true, subscribers of the associated page are notified by email.\n operationId: StatusReportService_CreateStatusReport\n requestBody:\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.status_report.v1.CreateStatusReportRequest'\n required: true\n responses:\n \"429\":\n $ref: '#/components/responses/RateLimited'\n default:\n description: Error\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/connect.error'\n \"200\":\n description: Success\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.status_report.v1.CreateStatusReportResponse'\n /rpc/openstatus.status_report.v1.StatusReportService/DeleteStatusReport:\n post:\n tags:\n - StatusReportService\n summary: DeleteStatusReport\n description: DeleteStatusReport removes a status report and all its updates.\n operationId: StatusReportService_DeleteStatusReport\n requestBody:\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.status_report.v1.DeleteStatusReportRequest'\n required: true\n responses:\n \"429\":\n $ref: '#/components/responses/RateLimited'\n default:\n description: Error\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/connect.error'\n \"200\":\n description: Success\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.status_report.v1.DeleteStatusReportResponse'\n /rpc/openstatus.status_report.v1.StatusReportService/DeleteStatusReportUpdate:\n post:\n tags:\n - StatusReportService\n summary: DeleteStatusReportUpdate\n description: 'Removes a status report update. The report''s status is re-derived from the remaining updates. Deleting the last remaining update is rejected with INVALID_ARGUMENT: delete the status report instead.'\n operationId: StatusReportService_DeleteStatusReportUpdate\n requestBody:\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.status_report.v1.DeleteStatusReportUpdateRequest'\n required: true\n responses:\n \"429\":\n $ref: '#/components/responses/RateLimited'\n default:\n description: Error\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/connect.error'\n \"200\":\n description: Success\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.status_report.v1.DeleteStatusReportUpdateResponse'\n /rpc/openstatus.status_report.v1.StatusReportService/GetStatusReport:\n get:\n tags:\n - StatusReportService\n summary: GetStatusReport\n description: GetStatusReport retrieves a specific status report by ID (includes full update timeline).\n operationId: StatusReportService_GetStatusReport.get\n parameters:\n - name: message\n in: query\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.status_report.v1.GetStatusReportRequest'\n responses:\n \"429\":\n $ref: '#/components/responses/RateLimited'\n default:\n description: Error\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/connect.error'\n \"200\":\n description: Success\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.status_report.v1.GetStatusReportResponse'\n post:\n tags:\n - StatusReportService\n summary: GetStatusReport\n description: GetStatusReport retrieves a specific status report by ID (includes full update timeline).\n operationId: StatusReportService_GetStatusReport\n requestBody:\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.status_report.v1.GetStatusReportRequest'\n required: true\n responses:\n \"429\":\n $ref: '#/components/responses/RateLimited'\n default:\n description: Error\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/connect.error'\n \"200\":\n description: Success\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.status_report.v1.GetStatusReportResponse'\n /rpc/openstatus.status_report.v1.StatusReportService/ListStatusReports:\n get:\n tags:\n - StatusReportService\n summary: ListStatusReports\n description: ListStatusReports returns all status reports for the workspace (metadata only).\n operationId: StatusReportService_ListStatusReports.get\n parameters:\n - name: message\n in: query\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.status_report.v1.ListStatusReportsRequest'\n responses:\n \"429\":\n $ref: '#/components/responses/RateLimited'\n default:\n description: Error\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/connect.error'\n \"200\":\n description: Success\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.status_report.v1.ListStatusReportsResponse'\n post:\n tags:\n - StatusReportService\n summary: ListStatusReports\n description: ListStatusReports returns all status reports for the workspace (metadata only).\n operationId: StatusReportService_ListStatusReports\n requestBody:\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.status_report.v1.ListStatusReportsRequest'\n required: true\n responses:\n \"429\":\n $ref: '#/components/responses/RateLimited'\n default:\n description: Error\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/connect.error'\n \"200\":\n description: Success\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.status_report.v1.ListStatusReportsResponse'\n /rpc/openstatus.status_report.v1.StatusReportService/UpdateStatusReport:\n post:\n tags:\n - StatusReportService\n summary: UpdateStatusReport\n description: UpdateStatusReport updates the metadata of a status report (title, page components).\n operationId: StatusReportService_UpdateStatusReport\n requestBody:\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.status_report.v1.UpdateStatusReportRequest'\n required: true\n responses:\n \"429\":\n $ref: '#/components/responses/RateLimited'\n default:\n description: Error\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/connect.error'\n \"200\":\n description: Success\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.status_report.v1.UpdateStatusReportResponse'\n /rpc/openstatus.status_report.v1.StatusReportService/UpdateStatusReportUpdate:\n post:\n tags:\n - StatusReportService\n summary: UpdateStatusReportUpdate\n description: Edits an existing status report update in place. Editing the status or date of the latest update re-derives the report's overall status. Subscribers are not notified.\n operationId: StatusReportService_UpdateStatusReportUpdate\n requestBody:\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.status_report.v1.UpdateStatusReportUpdateRequest'\n required: true\n responses:\n \"429\":\n $ref: '#/components/responses/RateLimited'\n default:\n description: Error\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/connect.error'\n \"200\":\n description: Success\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.status_report.v1.UpdateStatusReportUpdateResponse'\n"; +export default "openapi: 3.1.0\ninfo:\n description: OpenStatus is a open-source status page platform with global uptime monitoring. The OpenStatus API allows you to interact with the OpenStatus platform programmatically. To get started you need to create an account on https://www.openstatus.dev/ and create an api token in your settings. Requests are rate limited per API key or token (600 per minute, 100 per 10 seconds); exceeding a limit returns HTTP 429 with a Retry-After header. See https://www.openstatus.dev/docs/reference/api-rate-limits.\n title: OpenStatus API\n version: v2.0.0\n contact:\n email: ping@openstatus.dev\n url: https://www.openstatus.dev\nservers:\n - url: https://api.openstatus.dev\n description: Production\nexternalDocs:\n description: OpenStatus Documentation\n url: https://www.openstatus.dev/docs\ncomponents:\n responses:\n RateLimited:\n description: Rate limit exceeded (Connect code resource_exhausted). Retry after the number of seconds in the Retry-After header. See https://www.openstatus.dev/docs/reference/api-rate-limits.\n headers:\n Retry-After:\n description: Seconds to wait before retrying.\n schema:\n type: integer\n example: 7\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/connect.error'\n securitySchemes:\n ApiKeyAuth:\n type: apiKey\n in: header\n name: x-openstatus-key\n schemas:\n connect.error:\n type: object\n properties:\n code:\n type: string\n examples:\n - not_found\n enum:\n - canceled\n - unknown\n - invalid_argument\n - deadline_exceeded\n - not_found\n - already_exists\n - permission_denied\n - resource_exhausted\n - failed_precondition\n - aborted\n - out_of_range\n - unimplemented\n - internal\n - unavailable\n - data_loss\n - unauthenticated\n description: The status code, which should be an enum value of [google.rpc.Code][google.rpc.Code].\n message:\n type: string\n description: A developer-facing error message, which should be in English. Any user-facing error message should be localized and sent in the [google.rpc.Status.details][google.rpc.Status.details] field, or localized by the client.\n details:\n type: array\n items:\n $ref: '#/components/schemas/connect.error_details.Any'\n description: A list of messages that carry the error details. There is no limit on the number of messages.\n title: Connect Error\n additionalProperties: true\n description: 'Error type returned by Connect: https://connectrpc.com/docs/go/errors/#http-representation'\n connect.error_details.Any:\n type: object\n properties:\n type:\n type: string\n description: 'A URL that acts as a globally unique identifier for the type of the serialized message. For example: `type.googleapis.com/google.rpc.ErrorInfo`. This is used to determine the schema of the data in the `value` field and is the discriminator for the `debug` field.'\n value:\n type: string\n format: binary\n description: The Protobuf message, serialized as bytes and base64-encoded. The specific message type is identified by the `type` field.\n debug:\n oneOf:\n - type: object\n title: Any\n additionalProperties: true\n description: Detailed error information.\n discriminator:\n propertyName: type\n title: Debug\n description: Deserialized error detail payload. The 'type' field indicates the schema. This field is for easier debugging and should not be relied upon for application logic.\n additionalProperties: true\n description: Contains an arbitrary serialized message along with a @type that describes the type of the serialized message, with an additional debug field for ConnectRPC error details.\n openstatus.health.v1.CheckRequest:\n type: object\n properties:\n service:\n type: string\n title: service\n description: Optional service name to check. If empty, checks overall service health.\n title: CheckRequest\n additionalProperties: false\n description: CheckRequest is the request message for health checks.\n openstatus.health.v1.CheckResponse:\n type: object\n properties:\n status:\n title: status\n description: The serving status of the service.\n $ref: '#/components/schemas/openstatus.health.v1.CheckResponse.ServingStatus'\n title: CheckResponse\n additionalProperties: false\n description: CheckResponse is the response message for health checks.\n openstatus.health.v1.CheckResponse.ServingStatus:\n type: string\n title: ServingStatus\n enum:\n - SERVING_STATUS_UNSPECIFIED\n - SERVING_STATUS_SERVING\n - SERVING_STATUS_NOT_SERVING\n description: ServingStatus represents the health status of the service.\n openstatus.incident.v1.AddIncidentNoteRequest:\n type: object\n properties:\n id:\n type: string\n title: id\n minLength: 1\n description: ID of the incident (required).\n message:\n type: string\n title: message\n maxLength: 10000\n minLength: 1\n description: Note text, markdown (required, 1-10000 characters).\n title: AddIncidentNoteRequest\n additionalProperties: false\n description: AddIncidentNoteRequest is the request to add a note to an incident timeline.\n openstatus.incident.v1.AddIncidentNoteResponse:\n type: object\n properties:\n event:\n title: event\n description: The timeline event that was added.\n $ref: '#/components/schemas/openstatus.incident.v1.IncidentEvent'\n title: AddIncidentNoteResponse\n additionalProperties: false\n description: AddIncidentNoteResponse is the response after adding a note.\n openstatus.incident.v1.ApprovePostmortemRequest:\n type: object\n properties:\n incidentId:\n type: string\n title: incident_id\n minLength: 1\n description: ID of the incident (required).\n close:\n type:\n - boolean\n - \"null\"\n title: close\n description: Also close the incident (optional, defaults to false).\n title: ApprovePostmortemRequest\n additionalProperties: false\n description: ApprovePostmortemRequest is the request to approve the postmortem of an incident.\n openstatus.incident.v1.ApprovePostmortemResponse:\n type: object\n properties:\n postmortem:\n title: postmortem\n description: The approved postmortem.\n $ref: '#/components/schemas/openstatus.incident.v1.Postmortem'\n incident:\n title: incident\n description: The incident after approval (without its timeline).\n $ref: '#/components/schemas/openstatus.incident.v1.Incident'\n title: ApprovePostmortemResponse\n additionalProperties: false\n description: ApprovePostmortemResponse is the response after approving the postmortem.\n openstatus.incident.v1.CloseIncidentRequest:\n type: object\n properties:\n id:\n type: string\n title: id\n minLength: 1\n description: ID of the incident (required).\n skipPostmortem:\n type:\n - boolean\n - \"null\"\n title: skip_postmortem\n description: Close without an approved postmortem (optional, defaults to false).\n title: CloseIncidentRequest\n additionalProperties: false\n description: CloseIncidentRequest is the request to close a resolved incident.\n openstatus.incident.v1.CloseIncidentResponse:\n type: object\n properties:\n incident:\n title: incident\n description: The closed incident (without its timeline).\n $ref: '#/components/schemas/openstatus.incident.v1.Incident'\n title: CloseIncidentResponse\n additionalProperties: false\n description: CloseIncidentResponse is the response after closing an incident.\n openstatus.incident.v1.DeclareIncidentRequest:\n type: object\n properties:\n title:\n type: string\n examples:\n - Checkout API returns 502\n title: title\n maxLength: 256\n minLength: 1\n description: Title of the incident (required, 1-256 characters).\n severity:\n not:\n enum:\n - INCIDENT_SEVERITY_UNSPECIFIED\n title: severity\n description: Severity of the incident (required).\n $ref: '#/components/schemas/openstatus.incident.v1.IncidentSeverity'\n summary:\n type:\n - string\n - \"null\"\n title: summary\n maxLength: 4000\n minLength: 1\n description: Short human summary (optional, 1-4000 characters).\n commanderEmail:\n type:\n - string\n - \"null\"\n examples:\n - jane@example.com\n title: commander_email\n format: email\n description: Email of the member who leads the response (optional, defaults to unassigned).\n startedAt:\n type:\n - string\n - \"null\"\n examples:\n - \"2024-03-15T10:30:00Z\"\n title: started_at\n pattern: ^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(\\.\\d{1,9})?(Z|[+-]\\d{2}:\\d{2})$\n description: When the impact began (RFC 3339 format, optional, defaults to now).\n statusReportId:\n type:\n - string\n - \"null\"\n title: status_report_id\n minLength: 1\n description: ID of a status report to link (optional).\n openSlackChannel:\n type:\n - boolean\n - \"null\"\n title: open_slack_channel\n description: Whether to open a Slack channel for the incident when Slack is connected (optional, defaults to false).\n title: DeclareIncidentRequest\n additionalProperties: false\n description: DeclareIncidentRequest is the request to declare a new incident.\n openstatus.incident.v1.DeclareIncidentResponse:\n type: object\n properties:\n incident:\n title: incident\n description: The declared incident.\n $ref: '#/components/schemas/openstatus.incident.v1.Incident'\n title: DeclareIncidentResponse\n additionalProperties: false\n description: DeclareIncidentResponse is the response after declaring an incident.\n openstatus.incident.v1.DeleteIncidentRequest:\n type: object\n properties:\n id:\n type: string\n title: id\n minLength: 1\n description: ID of the incident (required).\n title: DeleteIncidentRequest\n additionalProperties: false\n description: DeleteIncidentRequest is the request to delete an incident.\n openstatus.incident.v1.DeleteIncidentResponse:\n type: object\n properties:\n success:\n type: boolean\n title: success\n description: Whether the deletion was successful.\n title: DeleteIncidentResponse\n additionalProperties: false\n description: DeleteIncidentResponse is the response after deleting an incident.\n openstatus.incident.v1.GetIncidentRequest:\n type: object\n properties:\n id:\n type: string\n title: id\n minLength: 1\n description: ID of the incident to retrieve (required).\n title: GetIncidentRequest\n additionalProperties: false\n description: GetIncidentRequest is the request to get an incident by ID.\n openstatus.incident.v1.GetIncidentResponse:\n type: object\n properties:\n incident:\n title: incident\n description: The requested incident.\n $ref: '#/components/schemas/openstatus.incident.v1.Incident'\n title: GetIncidentResponse\n additionalProperties: false\n description: GetIncidentResponse is the response containing the incident and its timeline.\n openstatus.incident.v1.GetPostmortemRequest:\n type: object\n properties:\n incidentId:\n type: string\n title: incident_id\n minLength: 1\n description: ID of the incident (required).\n title: GetPostmortemRequest\n additionalProperties: false\n description: GetPostmortemRequest is the request to get the postmortem of an incident.\n openstatus.incident.v1.GetPostmortemResponse:\n type: object\n properties:\n postmortem:\n oneOf:\n - $ref: '#/components/schemas/openstatus.incident.v1.Postmortem'\n - type: \"null\"\n title: postmortem\n description: The postmortem (unset when none has been written yet).\n title: GetPostmortemResponse\n additionalProperties: false\n description: GetPostmortemResponse is the response containing the postmortem, if any.\n openstatus.incident.v1.Incident:\n type: object\n properties:\n id:\n type: string\n title: id\n description: Unique identifier for the incident.\n title:\n type: string\n title: title\n description: Title of the incident.\n severity:\n title: severity\n description: Severity of the incident.\n $ref: '#/components/schemas/openstatus.incident.v1.IncidentSeverity'\n status:\n title: status\n description: Current status of the incident.\n $ref: '#/components/schemas/openstatus.incident.v1.IncidentStatus'\n summary:\n type:\n - string\n - \"null\"\n title: summary\n description: Short human summary.\n commander:\n oneOf:\n - $ref: '#/components/schemas/openstatus.incident.v1.IncidentUser'\n - type: \"null\"\n title: commander\n description: Member leading the response (unset when unassigned).\n declaredBy:\n oneOf:\n - $ref: '#/components/schemas/openstatus.incident.v1.IncidentUser'\n - type: \"null\"\n title: declared_by\n description: Member who declared the incident (unset for API keys without a creator).\n resolvedBy:\n oneOf:\n - $ref: '#/components/schemas/openstatus.incident.v1.IncidentUser'\n - type: \"null\"\n title: resolved_by\n description: Member who last resolved the incident (unset again when the incident is reopened).\n declaredAt:\n type: string\n title: declared_at\n description: Timestamp when the incident was declared (RFC 3339 format).\n startedAt:\n type: string\n title: started_at\n description: Timestamp when the impact began (RFC 3339 format).\n mitigatedAt:\n type:\n - string\n - \"null\"\n title: mitigated_at\n description: Timestamp when the incident was first mitigated (RFC 3339 format).\n resolvedAt:\n type:\n - string\n - \"null\"\n title: resolved_at\n description: Timestamp of the last resolution (RFC 3339 format). Kept when the incident is reopened.\n closedAt:\n type:\n - string\n - \"null\"\n title: closed_at\n description: |-\n Timestamp when the incident was closed or canceled (RFC 3339 format).\n A closed incident is read-only except for its postmortem.\n statusReport:\n oneOf:\n - $ref: '#/components/schemas/openstatus.incident.v1.IncidentStatusReport'\n - type: \"null\"\n title: status_report\n description: Linked public status report.\n slackChannelUrl:\n type:\n - string\n - \"null\"\n title: slack_channel_url\n description: Link to the bound Slack channel.\n allowedTransitions:\n type: array\n items:\n $ref: '#/components/schemas/openstatus.incident.v1.IncidentStatus'\n title: allowed_transitions\n description: Statuses SetIncidentStatus accepts from the current state (empty once closed).\n deletable:\n type: boolean\n title: deletable\n description: 'Whether DeleteIncident is allowed: only while open and never mitigated, resolved or closed.'\n events:\n type: array\n items:\n $ref: '#/components/schemas/openstatus.incident.v1.IncidentEvent'\n title: events\n description: Timeline, newest first (only included in GetIncident).\n createdAt:\n type: string\n title: created_at\n description: Timestamp when the incident was created (RFC 3339 format).\n updatedAt:\n type: string\n title: updated_at\n description: Timestamp when the incident was last updated (RFC 3339 format).\n title: Incident\n additionalProperties: false\n description: Incident is a managed incident with full details.\n openstatus.incident.v1.IncidentEvent:\n type: object\n properties:\n id:\n type: string\n title: id\n description: Unique identifier for the event.\n type:\n title: type\n description: Kind of event.\n $ref: '#/components/schemas/openstatus.incident.v1.IncidentEventType'\n message:\n type: string\n title: message\n description: 'Text of the event: the note for notes, a rendered summary otherwise.'\n createdBy:\n oneOf:\n - $ref: '#/components/schemas/openstatus.incident.v1.IncidentUser'\n - type: \"null\"\n title: created_by\n description: Member who caused the event (unset for system events and API keys without a creator).\n createdAt:\n type: string\n title: created_at\n description: Timestamp when the event was recorded (RFC 3339 format).\n title: IncidentEvent\n additionalProperties: false\n description: IncidentEvent is one entry of an incident timeline.\n openstatus.incident.v1.IncidentEventType:\n type: string\n title: IncidentEventType\n enum:\n - INCIDENT_EVENT_TYPE_UNSPECIFIED\n - INCIDENT_EVENT_TYPE_DECLARED\n - INCIDENT_EVENT_TYPE_SEVERITY_CHANGED\n - INCIDENT_EVENT_TYPE_STATUS_CHANGED\n - INCIDENT_EVENT_TYPE_COMMANDER_CHANGED\n - INCIDENT_EVENT_TYPE_STARTED_AT_CHANGED\n - INCIDENT_EVENT_TYPE_NOTE\n - INCIDENT_EVENT_TYPE_STATUS_REPORT_LINKED\n - INCIDENT_EVENT_TYPE_STATUS_REPORT_UNLINKED\n - INCIDENT_EVENT_TYPE_SLACK_CHANNEL_BOUND\n - INCIDENT_EVENT_TYPE_SLACK_CHANNEL_UNBOUND\n - INCIDENT_EVENT_TYPE_RESOLVED\n - INCIDENT_EVENT_TYPE_CANCELED\n - INCIDENT_EVENT_TYPE_POSTMORTEM_DRAFTED\n - INCIDENT_EVENT_TYPE_POSTMORTEM_UPDATED\n - INCIDENT_EVENT_TYPE_POSTMORTEM_APPROVED\n - INCIDENT_EVENT_TYPE_CLOSED\n description: IncidentEventType is the kind of entry in an incident timeline.\n openstatus.incident.v1.IncidentSeverity:\n type: string\n title: IncidentSeverity\n enum:\n - INCIDENT_SEVERITY_UNSPECIFIED\n - INCIDENT_SEVERITY_CRITICAL\n - INCIDENT_SEVERITY_MAJOR\n - INCIDENT_SEVERITY_MINOR\n description: IncidentSeverity is how bad an incident is.\n openstatus.incident.v1.IncidentStatus:\n type: string\n title: IncidentStatus\n enum:\n - INCIDENT_STATUS_UNSPECIFIED\n - INCIDENT_STATUS_OPEN\n - INCIDENT_STATUS_MITIGATED\n - INCIDENT_STATUS_RESOLVED\n - INCIDENT_STATUS_CANCELED\n description: |-\n IncidentStatus is the lifecycle state of an incident.\n Closed is not a status: a closed incident has closed_at set and keeps its last status.\n openstatus.incident.v1.IncidentStatusReport:\n type: object\n properties:\n id:\n type: string\n title: id\n description: ID of the status report.\n title:\n type: string\n title: title\n description: Title of the status report.\n status:\n title: status\n description: Current status of the status report.\n $ref: '#/components/schemas/openstatus.status_report.v1.StatusReportStatus'\n pageId:\n type: string\n title: page_id\n description: ID of the status page the report belongs to (empty in ListIncidents results).\n title: IncidentStatusReport\n additionalProperties: false\n description: IncidentStatusReport is the public status report linked to an incident.\n openstatus.incident.v1.IncidentSummary:\n type: object\n properties:\n id:\n type: string\n title: id\n description: Unique identifier for the incident.\n title:\n type: string\n title: title\n description: Title of the incident.\n severity:\n title: severity\n description: Severity of the incident.\n $ref: '#/components/schemas/openstatus.incident.v1.IncidentSeverity'\n status:\n title: status\n description: Current status of the incident.\n $ref: '#/components/schemas/openstatus.incident.v1.IncidentStatus'\n commander:\n oneOf:\n - $ref: '#/components/schemas/openstatus.incident.v1.IncidentUser'\n - type: \"null\"\n title: commander\n description: Member leading the response (unset when unassigned).\n declaredAt:\n type: string\n title: declared_at\n description: Timestamp when the incident was declared (RFC 3339 format).\n startedAt:\n type: string\n title: started_at\n description: Timestamp when the impact began (RFC 3339 format).\n resolvedAt:\n type:\n - string\n - \"null\"\n title: resolved_at\n description: Timestamp of the last resolution (RFC 3339 format). Kept when the incident is reopened.\n closedAt:\n type:\n - string\n - \"null\"\n title: closed_at\n description: Timestamp when the incident was closed or canceled (RFC 3339 format).\n statusReport:\n oneOf:\n - $ref: '#/components/schemas/openstatus.incident.v1.IncidentStatusReport'\n - type: \"null\"\n title: status_report\n description: Linked public status report.\n createdAt:\n type: string\n title: created_at\n description: Timestamp when the incident was created (RFC 3339 format).\n updatedAt:\n type: string\n title: updated_at\n description: Timestamp when the incident was last updated (RFC 3339 format).\n title: IncidentSummary\n additionalProperties: false\n description: IncidentSummary is the metadata of an incident (used in list responses).\n openstatus.incident.v1.IncidentUser:\n type: object\n properties:\n email:\n type: string\n title: email\n description: Email address of the member (empty for a deleted account).\n name:\n type: string\n title: name\n description: Display name of the member (\"Deleted user\" for a deleted account).\n title: IncidentUser\n additionalProperties: false\n description: IncidentUser is a workspace member referenced by an incident.\n openstatus.incident.v1.LinkStatusReportRequest:\n type: object\n properties:\n id:\n type: string\n title: id\n minLength: 1\n description: ID of the incident (required).\n statusReportId:\n type: string\n title: status_report_id\n minLength: 1\n description: ID of the status report to link (required).\n title: LinkStatusReportRequest\n additionalProperties: false\n description: LinkStatusReportRequest is the request to link a status report to an incident.\n openstatus.incident.v1.LinkStatusReportResponse:\n type: object\n properties:\n incident:\n title: incident\n description: The updated incident (without its timeline).\n $ref: '#/components/schemas/openstatus.incident.v1.Incident'\n title: LinkStatusReportResponse\n additionalProperties: false\n description: LinkStatusReportResponse is the response after linking a status report.\n openstatus.incident.v1.ListIncidentsRequest:\n type: object\n properties:\n limit:\n type:\n - integer\n - \"null\"\n title: limit\n maximum: 100\n minimum: 1\n format: int32\n description: Maximum number of incidents to return (1-100, defaults to 50).\n offset:\n type:\n - integer\n - \"null\"\n title: offset\n minimum: 0\n format: int32\n description: Number of incidents to skip for pagination (defaults to 0).\n statuses:\n type: array\n items:\n $ref: '#/components/schemas/openstatus.incident.v1.IncidentStatus'\n title: statuses\n description: Filter by status (optional). If empty, returns all statuses.\n closed:\n type:\n - boolean\n - \"null\"\n title: closed\n description: Filter by closed state (optional). If unset, returns both.\n title: ListIncidentsRequest\n additionalProperties: false\n description: ListIncidentsRequest is the request to list incidents.\n openstatus.incident.v1.ListIncidentsResponse:\n type: object\n properties:\n incidents:\n type: array\n items:\n $ref: '#/components/schemas/openstatus.incident.v1.IncidentSummary'\n title: incidents\n description: List of incidents (metadata only, use GetIncident for full details).\n totalSize:\n type: integer\n title: total_size\n format: int32\n description: Total number of incidents matching the filter.\n title: ListIncidentsResponse\n additionalProperties: false\n description: ListIncidentsResponse is the response containing incident summaries.\n openstatus.incident.v1.Postmortem:\n type: object\n properties:\n incidentId:\n type: string\n title: incident_id\n description: ID of the incident the postmortem belongs to.\n status:\n title: status\n description: Review state of the postmortem.\n $ref: '#/components/schemas/openstatus.incident.v1.PostmortemStatus'\n content:\n type: string\n title: content\n description: Markdown body of the postmortem.\n draftedBy:\n title: drafted_by\n description: Who wrote the current body.\n $ref: '#/components/schemas/openstatus.incident.v1.PostmortemAuthor'\n approvedBy:\n oneOf:\n - $ref: '#/components/schemas/openstatus.incident.v1.IncidentUser'\n - type: \"null\"\n title: approved_by\n description: Member who approved the postmortem.\n approvedAt:\n type:\n - string\n - \"null\"\n title: approved_at\n description: Timestamp when the postmortem was approved (RFC 3339 format).\n createdAt:\n type: string\n title: created_at\n description: Timestamp when the postmortem was created (RFC 3339 format).\n updatedAt:\n type: string\n title: updated_at\n description: Timestamp when the postmortem was last updated (RFC 3339 format).\n title: Postmortem\n additionalProperties: false\n description: Postmortem is the review written after an incident is resolved.\n openstatus.incident.v1.PostmortemAuthor:\n type: string\n title: PostmortemAuthor\n enum:\n - POSTMORTEM_AUTHOR_UNSPECIFIED\n - POSTMORTEM_AUTHOR_AGENT\n - POSTMORTEM_AUTHOR_USER\n description: PostmortemAuthor is who wrote the current postmortem body.\n openstatus.incident.v1.PostmortemStatus:\n type: string\n title: PostmortemStatus\n enum:\n - POSTMORTEM_STATUS_UNSPECIFIED\n - POSTMORTEM_STATUS_DRAFT\n - POSTMORTEM_STATUS_APPROVED\n description: PostmortemStatus is the review state of a postmortem.\n openstatus.incident.v1.SetIncidentStatusRequest:\n type: object\n properties:\n id:\n type: string\n title: id\n minLength: 1\n description: ID of the incident (required).\n status:\n not:\n enum:\n - INCIDENT_STATUS_UNSPECIFIED\n title: status\n description: Target status (required).\n $ref: '#/components/schemas/openstatus.incident.v1.IncidentStatus'\n note:\n type:\n - string\n - \"null\"\n title: note\n maxLength: 10000\n description: Note recorded with the change (optional, up to 10000 characters).\n title: SetIncidentStatusRequest\n additionalProperties: false\n description: SetIncidentStatusRequest is the request to change the status of an incident.\n openstatus.incident.v1.SetIncidentStatusResponse:\n type: object\n properties:\n incident:\n title: incident\n description: The updated incident (without its timeline).\n $ref: '#/components/schemas/openstatus.incident.v1.Incident'\n title: SetIncidentStatusResponse\n additionalProperties: false\n description: SetIncidentStatusResponse is the response after changing the status of an incident.\n openstatus.incident.v1.UnlinkStatusReportRequest:\n type: object\n properties:\n id:\n type: string\n title: id\n minLength: 1\n description: ID of the incident (required).\n title: UnlinkStatusReportRequest\n additionalProperties: false\n description: UnlinkStatusReportRequest is the request to unlink the status report of an incident.\n openstatus.incident.v1.UnlinkStatusReportResponse:\n type: object\n properties:\n incident:\n title: incident\n description: The updated incident (without its timeline).\n $ref: '#/components/schemas/openstatus.incident.v1.Incident'\n title: UnlinkStatusReportResponse\n additionalProperties: false\n description: UnlinkStatusReportResponse is the response after unlinking the status report.\n openstatus.incident.v1.UpdateIncidentRequest:\n type: object\n properties:\n id:\n type: string\n title: id\n minLength: 1\n description: ID of the incident to update (required).\n title:\n type:\n - string\n - \"null\"\n title: title\n maxLength: 256\n minLength: 1\n description: New title (optional, 1-256 characters).\n severity:\n oneOf:\n - $ref: '#/components/schemas/openstatus.incident.v1.IncidentSeverity'\n - type: \"null\"\n not:\n enum:\n - INCIDENT_SEVERITY_UNSPECIFIED\n title: severity\n description: New severity (optional).\n summary:\n type:\n - string\n - \"null\"\n title: summary\n maxLength: 4000\n minLength: 1\n description: New summary (optional, 1-4000 characters).\n clearSummary:\n type:\n - boolean\n - \"null\"\n title: clear_summary\n description: Set to true to remove the summary. Cannot be combined with summary.\n commanderEmail:\n type:\n - string\n - \"null\"\n title: commander_email\n format: email\n description: Email of the new commander (optional).\n clearCommander:\n type:\n - boolean\n - \"null\"\n title: clear_commander\n description: Set to true to unassign the commander. Cannot be combined with commander_email.\n startedAt:\n type:\n - string\n - \"null\"\n title: started_at\n pattern: ^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(\\.\\d{1,9})?(Z|[+-]\\d{2}:\\d{2})$\n description: New start of impact (RFC 3339 format, optional).\n title: UpdateIncidentRequest\n additionalProperties: false\n description: |\n UpdateIncidentRequest is the request to edit an incident.\n commander_email_or_clear_commander // set either commander_email or clear_commander, not both\n summary_or_clear_summary // set either summary or clear_summary, not both\n openstatus.incident.v1.UpdateIncidentResponse:\n type: object\n properties:\n incident:\n title: incident\n description: The updated incident (without its timeline).\n $ref: '#/components/schemas/openstatus.incident.v1.Incident'\n title: UpdateIncidentResponse\n additionalProperties: false\n description: UpdateIncidentResponse is the response after updating an incident.\n openstatus.incident.v1.UpdatePostmortemRequest:\n type: object\n properties:\n incidentId:\n type: string\n title: incident_id\n minLength: 1\n description: ID of the incident (required).\n content:\n type: string\n title: content\n maxLength: 100000\n minLength: 1\n description: Markdown body (required, 1-100000 characters). Replaces the current body; an approved postmortem stays approved.\n title: UpdatePostmortemRequest\n additionalProperties: false\n description: UpdatePostmortemRequest is the request to write the postmortem of a resolved incident.\n openstatus.incident.v1.UpdatePostmortemResponse:\n type: object\n properties:\n postmortem:\n title: postmortem\n description: The saved postmortem.\n $ref: '#/components/schemas/openstatus.incident.v1.Postmortem'\n title: UpdatePostmortemResponse\n additionalProperties: false\n description: UpdatePostmortemResponse is the response after writing the postmortem.\n openstatus.maintenance.v1.AddMaintenanceUpdateRequest:\n type: object\n properties:\n maintenanceId:\n type: string\n title: maintenance_id\n minLength: 1\n description: ID of the maintenance to update (required).\n message:\n type: string\n title: message\n minLength: 1\n description: Public update message (required).\n date:\n type:\n - string\n - \"null\"\n title: date\n pattern: ^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(\\.\\d{1,9})?(Z|[+-]\\d{2}:\\d{2})$\n description: Optional date for the update (RFC 3339 format). Defaults to current time.\n notify:\n type:\n - boolean\n - \"null\"\n title: notify\n description: Whether to notify subscribers about this update.\n title: AddMaintenanceUpdateRequest\n additionalProperties: false\n description: AddMaintenanceUpdateRequest is the request to append a maintenance update.\n openstatus.maintenance.v1.AddMaintenanceUpdateResponse:\n type: object\n properties:\n maintenanceUpdate:\n title: maintenance_update\n description: The created update.\n $ref: '#/components/schemas/openstatus.maintenance.v1.MaintenanceUpdate'\n title: AddMaintenanceUpdateResponse\n additionalProperties: false\n description: AddMaintenanceUpdateResponse is the response after appending an update.\n openstatus.maintenance.v1.CreateMaintenanceRequest:\n type: object\n properties:\n title:\n type: string\n examples:\n - Database Migration\n title: title\n maxLength: 256\n minLength: 1\n description: Title of the maintenance (required, 1-256 characters).\n message:\n type: string\n title: message\n minLength: 1\n description: |-\n Message describing the maintenance (required). Posted as the first\n maintenance update.\n from:\n type: string\n examples:\n - \"2024-03-01T02:00:00Z\"\n title: from\n pattern: ^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(\\.\\d{1,9})?(Z|[+-]\\d{2}:\\d{2})$\n description: Start time of the maintenance window (RFC 3339 format, required).\n to:\n type: string\n examples:\n - \"2024-03-01T06:00:00Z\"\n title: to\n pattern: ^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(\\.\\d{1,9})?(Z|[+-]\\d{2}:\\d{2})$\n description: End time of the maintenance window (RFC 3339 format, required).\n pageId:\n type: string\n title: page_id\n minLength: 1\n description: Page ID to associate with this maintenance (required).\n pageComponentIds:\n type: array\n items:\n type: string\n title: page_component_ids\n description: Page component IDs to associate with this maintenance (optional).\n notify:\n type:\n - boolean\n - \"null\"\n title: notify\n description: Whether to notify subscribers about this maintenance (optional, defaults to false).\n title: CreateMaintenanceRequest\n additionalProperties: false\n description: CreateMaintenanceRequest is the request to create a new maintenance window.\n openstatus.maintenance.v1.CreateMaintenanceResponse:\n type: object\n properties:\n maintenance:\n title: maintenance\n description: The created maintenance.\n $ref: '#/components/schemas/openstatus.maintenance.v1.Maintenance'\n title: CreateMaintenanceResponse\n additionalProperties: false\n description: CreateMaintenanceResponse is the response after creating a maintenance window.\n openstatus.maintenance.v1.DeleteMaintenanceRequest:\n type: object\n properties:\n id:\n type: string\n title: id\n minLength: 1\n description: ID of the maintenance to delete (required).\n title: DeleteMaintenanceRequest\n additionalProperties: false\n description: DeleteMaintenanceRequest is the request to delete a maintenance window.\n openstatus.maintenance.v1.DeleteMaintenanceResponse:\n type: object\n properties:\n success:\n type: boolean\n title: success\n description: Whether the deletion was successful.\n title: DeleteMaintenanceResponse\n additionalProperties: false\n description: DeleteMaintenanceResponse is the response after deleting a maintenance window.\n openstatus.maintenance.v1.DeleteMaintenanceUpdateRequest:\n type: object\n properties:\n id:\n type: string\n title: id\n minLength: 1\n description: ID of the update to remove (required).\n title: DeleteMaintenanceUpdateRequest\n additionalProperties: false\n description: DeleteMaintenanceUpdateRequest is the request to remove an update.\n openstatus.maintenance.v1.DeleteMaintenanceUpdateResponse:\n type: object\n properties:\n success:\n type: boolean\n title: success\n description: Whether the deletion was successful.\n title: DeleteMaintenanceUpdateResponse\n additionalProperties: false\n description: DeleteMaintenanceUpdateResponse is the response after removing an update.\n openstatus.maintenance.v1.GetMaintenanceRequest:\n type: object\n properties:\n id:\n type: string\n title: id\n minLength: 1\n description: ID of the maintenance to retrieve (required).\n title: GetMaintenanceRequest\n additionalProperties: false\n description: GetMaintenanceRequest is the request to get a maintenance window by ID.\n openstatus.maintenance.v1.GetMaintenanceResponse:\n type: object\n properties:\n maintenance:\n title: maintenance\n description: The requested maintenance.\n $ref: '#/components/schemas/openstatus.maintenance.v1.Maintenance'\n title: GetMaintenanceResponse\n additionalProperties: false\n description: GetMaintenanceResponse is the response containing the maintenance window.\n openstatus.maintenance.v1.ListMaintenancesRequest:\n type: object\n properties:\n limit:\n type:\n - integer\n - \"null\"\n title: limit\n maximum: 100\n minimum: 1\n format: int32\n description: Maximum number of maintenances to return (1-100, defaults to 50).\n offset:\n type:\n - integer\n - \"null\"\n title: offset\n minimum: 0\n format: int32\n description: Number of maintenances to skip for pagination (defaults to 0).\n pageId:\n type:\n - string\n - \"null\"\n title: page_id\n description: Filter by page ID (optional).\n title: ListMaintenancesRequest\n additionalProperties: false\n description: ListMaintenancesRequest is the request to list maintenance windows.\n openstatus.maintenance.v1.ListMaintenancesResponse:\n type: object\n properties:\n maintenances:\n type: array\n items:\n $ref: '#/components/schemas/openstatus.maintenance.v1.MaintenanceSummary'\n title: maintenances\n description: List of maintenances.\n totalSize:\n type: integer\n title: total_size\n format: int32\n description: Total number of maintenances matching the filter.\n title: ListMaintenancesResponse\n additionalProperties: false\n description: ListMaintenancesResponse is the response containing maintenance window summaries.\n openstatus.maintenance.v1.Maintenance:\n type: object\n properties:\n id:\n type: string\n title: id\n description: Unique identifier for the maintenance.\n title:\n type: string\n title: title\n description: Title of the maintenance.\n message:\n type: string\n title: message\n description: Message of the newest maintenance update.\n from:\n type: string\n title: from\n description: Start time of the maintenance window (RFC 3339 format).\n to:\n type: string\n title: to\n description: End time of the maintenance window (RFC 3339 format).\n pageId:\n type: string\n title: page_id\n description: ID of the page this maintenance is associated with.\n pageComponentIds:\n type: array\n items:\n type: string\n title: page_component_ids\n description: IDs of affected page components.\n createdAt:\n type: string\n title: created_at\n description: Timestamp when the maintenance was created (RFC 3339 format).\n updatedAt:\n type: string\n title: updated_at\n description: Timestamp when the maintenance was last updated (RFC 3339 format).\n updates:\n type: array\n items:\n $ref: '#/components/schemas/openstatus.maintenance.v1.MaintenanceUpdate'\n title: updates\n description: Full maintenance update timeline, newest first.\n title: Maintenance\n additionalProperties: false\n description: Maintenance represents a maintenance window with full details.\n openstatus.maintenance.v1.MaintenanceSummary:\n type: object\n properties:\n id:\n type: string\n title: id\n description: Unique identifier for the maintenance.\n title:\n type: string\n title: title\n description: Title of the maintenance.\n message:\n type: string\n title: message\n description: Message of the newest maintenance update.\n from:\n type: string\n title: from\n description: Start time of the maintenance window (RFC 3339 format).\n to:\n type: string\n title: to\n description: End time of the maintenance window (RFC 3339 format).\n pageId:\n type: string\n title: page_id\n description: ID of the page this maintenance is associated with.\n pageComponentIds:\n type: array\n items:\n type: string\n title: page_component_ids\n description: IDs of affected page components.\n createdAt:\n type: string\n title: created_at\n description: Timestamp when the maintenance was created (RFC 3339 format).\n updatedAt:\n type: string\n title: updated_at\n description: Timestamp when the maintenance was last updated (RFC 3339 format).\n title: MaintenanceSummary\n additionalProperties: false\n description: MaintenanceSummary represents metadata for a maintenance window (used in list responses).\n openstatus.maintenance.v1.MaintenanceUpdate:\n type: object\n properties:\n id:\n type: string\n title: id\n description: Unique identifier for the update.\n message:\n type: string\n title: message\n description: Public update message.\n date:\n type: string\n title: date\n description: Date of the update (RFC 3339 format).\n maintenanceId:\n type: string\n title: maintenance_id\n description: ID of the maintenance this update belongs to.\n createdAt:\n type: string\n title: created_at\n description: Timestamp when the update was created (RFC 3339 format).\n updatedAt:\n type: string\n title: updated_at\n description: Timestamp when the update was last updated (RFC 3339 format).\n title: MaintenanceUpdate\n additionalProperties: false\n description: MaintenanceUpdate represents a timeline entry for a maintenance window.\n openstatus.maintenance.v1.UpdateMaintenanceRequest:\n type: object\n properties:\n id:\n type: string\n title: id\n minLength: 1\n description: ID of the maintenance to update (required).\n title:\n type:\n - string\n - \"null\"\n title: title\n maxLength: 256\n minLength: 1\n description: New title for the maintenance (optional).\n message:\n type:\n - string\n - \"null\"\n title: message\n description: Replaces the message of the newest maintenance update (optional).\n from:\n type:\n - string\n - \"null\"\n title: from\n pattern: ^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(\\.\\d{1,9})?(Z|[+-]\\d{2}:\\d{2})$\n description: New start time (RFC 3339 format, optional).\n to:\n type:\n - string\n - \"null\"\n title: to\n pattern: ^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(\\.\\d{1,9})?(Z|[+-]\\d{2}:\\d{2})$\n description: New end time (RFC 3339 format, optional).\n pageId:\n type:\n - string\n - \"null\"\n title: page_id\n description: 'Deprecated: page_id is now derived from page_component_ids.'\n deprecated: true\n pageComponentIds:\n type: array\n items:\n type: string\n title: page_component_ids\n description: New list of page component IDs (optional, replaces existing list).\n updatePageComponentIds:\n type:\n - boolean\n - \"null\"\n title: update_page_component_ids\n description: |-\n Set to true to update page component associations.\n When true, page_component_ids replaces the existing list (empty clears all).\n When false or unset, page_component_ids is ignored and existing associations are preserved.\n title: UpdateMaintenanceRequest\n additionalProperties: false\n description: UpdateMaintenanceRequest is the request to update a maintenance window.\n openstatus.maintenance.v1.UpdateMaintenanceResponse:\n type: object\n properties:\n maintenance:\n title: maintenance\n description: The updated maintenance.\n $ref: '#/components/schemas/openstatus.maintenance.v1.Maintenance'\n title: UpdateMaintenanceResponse\n additionalProperties: false\n description: UpdateMaintenanceResponse is the response after updating a maintenance window.\n openstatus.maintenance.v1.UpdateMaintenanceUpdateRequest:\n type: object\n properties:\n id:\n type: string\n title: id\n minLength: 1\n description: ID of the update to edit (required).\n message:\n type:\n - string\n - \"null\"\n title: message\n minLength: 1\n description: New public message.\n date:\n type:\n - string\n - \"null\"\n title: date\n pattern: ^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(\\.\\d{1,9})?(Z|[+-]\\d{2}:\\d{2})$\n description: New update date (RFC 3339 format).\n title: UpdateMaintenanceUpdateRequest\n additionalProperties: false\n description: UpdateMaintenanceUpdateRequest is the request to edit an update.\n openstatus.maintenance.v1.UpdateMaintenanceUpdateResponse:\n type: object\n properties:\n maintenanceUpdate:\n title: maintenance_update\n description: The updated timeline entry.\n $ref: '#/components/schemas/openstatus.maintenance.v1.MaintenanceUpdate'\n title: UpdateMaintenanceUpdateResponse\n additionalProperties: false\n description: UpdateMaintenanceUpdateResponse is the response after editing an update.\n openstatus.monitor.v1.BodyAssertion:\n type: object\n properties:\n target:\n type: string\n title: target\n description: Target value to compare against.\n comparator:\n not:\n enum:\n - STRING_COMPARATOR_UNSPECIFIED\n title: comparator\n description: Comparison operation (required, must not be UNSPECIFIED).\n $ref: '#/components/schemas/openstatus.monitor.v1.StringComparator'\n title: BodyAssertion\n additionalProperties: false\n description: BodyAssertion defines an assertion for response body content.\n openstatus.monitor.v1.CreateDNSMonitorRequest:\n type: object\n properties:\n monitor:\n title: monitor\n description: Monitor configuration (required).\n $ref: '#/components/schemas/openstatus.monitor.v1.DNSMonitor'\n title: CreateDNSMonitorRequest\n required:\n - monitor\n additionalProperties: false\n description: CreateDNSMonitorRequest is the request to create a new DNS monitor.\n openstatus.monitor.v1.CreateDNSMonitorResponse:\n type: object\n properties:\n monitor:\n title: monitor\n description: The created monitor with assigned ID.\n $ref: '#/components/schemas/openstatus.monitor.v1.DNSMonitor'\n title: CreateDNSMonitorResponse\n additionalProperties: false\n description: CreateDNSMonitorResponse is the response after creating a DNS monitor.\n openstatus.monitor.v1.CreateGRPCMonitorRequest:\n type: object\n properties:\n monitor:\n title: monitor\n description: Monitor configuration (required).\n $ref: '#/components/schemas/openstatus.monitor.v1.GRPCMonitor'\n title: CreateGRPCMonitorRequest\n required:\n - monitor\n additionalProperties: false\n description: CreateGRPCMonitorRequest is the request to create a new gRPC monitor.\n openstatus.monitor.v1.CreateGRPCMonitorResponse:\n type: object\n properties:\n monitor:\n title: monitor\n description: The created monitor with assigned ID.\n $ref: '#/components/schemas/openstatus.monitor.v1.GRPCMonitor'\n title: CreateGRPCMonitorResponse\n additionalProperties: false\n description: CreateGRPCMonitorResponse is the response after creating a gRPC monitor.\n openstatus.monitor.v1.CreateHTTPMonitorRequest:\n type: object\n properties:\n monitor:\n title: monitor\n description: Monitor configuration (required).\n $ref: '#/components/schemas/openstatus.monitor.v1.HTTPMonitor'\n title: CreateHTTPMonitorRequest\n required:\n - monitor\n additionalProperties: false\n description: CreateHTTPMonitorRequest is the request to create a new HTTP monitor.\n openstatus.monitor.v1.CreateHTTPMonitorResponse:\n type: object\n properties:\n monitor:\n title: monitor\n description: The created monitor with assigned ID.\n $ref: '#/components/schemas/openstatus.monitor.v1.HTTPMonitor'\n title: CreateHTTPMonitorResponse\n additionalProperties: false\n description: CreateHTTPMonitorResponse is the response after creating an HTTP monitor.\n openstatus.monitor.v1.CreateICMPMonitorRequest:\n type: object\n properties:\n monitor:\n title: monitor\n description: Monitor configuration (required).\n $ref: '#/components/schemas/openstatus.monitor.v1.ICMPMonitor'\n title: CreateICMPMonitorRequest\n required:\n - monitor\n additionalProperties: false\n description: CreateICMPMonitorRequest is the request to create a new ICMP monitor.\n openstatus.monitor.v1.CreateICMPMonitorResponse:\n type: object\n properties:\n monitor:\n title: monitor\n description: The created monitor with assigned ID.\n $ref: '#/components/schemas/openstatus.monitor.v1.ICMPMonitor'\n title: CreateICMPMonitorResponse\n additionalProperties: false\n description: CreateICMPMonitorResponse is the response after creating an ICMP monitor.\n openstatus.monitor.v1.CreateTCPMonitorRequest:\n type: object\n properties:\n monitor:\n title: monitor\n description: Monitor configuration (required).\n $ref: '#/components/schemas/openstatus.monitor.v1.TCPMonitor'\n title: CreateTCPMonitorRequest\n required:\n - monitor\n additionalProperties: false\n description: CreateTCPMonitorRequest is the request to create a new TCP monitor.\n openstatus.monitor.v1.CreateTCPMonitorResponse:\n type: object\n properties:\n monitor:\n title: monitor\n description: The created monitor with assigned ID.\n $ref: '#/components/schemas/openstatus.monitor.v1.TCPMonitor'\n title: CreateTCPMonitorResponse\n additionalProperties: false\n description: CreateTCPMonitorResponse is the response after creating a TCP monitor.\n openstatus.monitor.v1.DNSMonitor:\n type: object\n properties:\n id:\n type: string\n title: id\n description: Unique identifier for the monitor (output only for create requests).\n name:\n type: string\n examples:\n - DNS Resolution Check\n title: name\n maxLength: 256\n minLength: 1\n description: Name of the monitor (required, max 256 characters).\n uri:\n type: string\n examples:\n - example.com\n title: uri\n maxLength: 2048\n minLength: 1\n description: Domain to resolve (required, max 2048 characters).\n periodicity:\n not:\n enum:\n - PERIODICITY_UNSPECIFIED\n title: periodicity\n description: Check periodicity (required).\n $ref: '#/components/schemas/openstatus.monitor.v1.Periodicity'\n timeout:\n type:\n - integer\n - string\n title: timeout\n maximum: 300000\n minimum: 0\n format: int64\n description: Timeout in milliseconds (0-300000, defaults to 45000).\n degradedAt:\n type:\n - integer\n - string\n - \"null\"\n title: degraded_at\n maximum: 300000\n minimum: 0\n format: int64\n description: Latency threshold for degraded status in milliseconds (optional, 0-300000).\n retry:\n type:\n - integer\n - string\n title: retry\n maximum: 10\n minimum: 0\n format: int64\n description: Number of retry attempts (0-10, defaults to 3).\n recordAssertions:\n type: array\n items:\n $ref: '#/components/schemas/openstatus.monitor.v1.RecordAssertion'\n title: record_assertions\n maxItems: 10\n description: DNS record assertions for validation.\n description:\n type:\n - string\n - \"null\"\n title: description\n maxLength: 1024\n description: Description of the monitor (optional).\n active:\n type:\n - boolean\n - \"null\"\n title: active\n description: Whether the monitor is active (defaults to false).\n public:\n type:\n - boolean\n - \"null\"\n title: public\n description: Whether the monitor is publicly visible (defaults to false).\n regions:\n type: array\n items:\n $ref: '#/components/schemas/openstatus.monitor.v1.Region'\n title: regions\n maxItems: 28\n description: Geographic regions to run checks from.\n openTelemetry:\n title: open_telemetry\n description: OpenTelemetry configuration for exporting metrics.\n $ref: '#/components/schemas/openstatus.monitor.v1.OpenTelemetryConfig'\n status:\n title: status\n description: Current operational status of the monitor.\n $ref: '#/components/schemas/openstatus.monitor.v1.MonitorStatus'\n privateLocationIds:\n type: array\n items:\n type: string\n readOnly: true\n title: private_location_ids\n description: IDs of private locations that run this monitor. Read-only.\n readOnly: true\n title: DNSMonitor\n additionalProperties: false\n description: DNSMonitor defines the configuration for a DNS monitor.\n openstatus.monitor.v1.DeleteMonitorRequest:\n type: object\n properties:\n id:\n type: string\n title: id\n minLength: 1\n description: Monitor ID to delete (required).\n title: DeleteMonitorRequest\n additionalProperties: false\n description: DeleteMonitorRequest is the request to delete a monitor.\n openstatus.monitor.v1.DeleteMonitorResponse:\n type: object\n properties:\n success:\n type: boolean\n title: success\n description: Whether the deletion was successful.\n title: DeleteMonitorResponse\n additionalProperties: false\n description: DeleteMonitorResponse is the response after deleting a monitor.\n openstatus.monitor.v1.GRPCMonitor:\n type: object\n properties:\n id:\n type: string\n title: id\n description: Unique identifier for the monitor (output only for create requests).\n name:\n type: string\n examples:\n - Checkout gRPC\n title: name\n maxLength: 256\n minLength: 1\n description: Name of the monitor (required, max 256 characters).\n uri:\n type: string\n examples:\n - api.example.com:443\n title: uri\n maxLength: 2048\n minLength: 1\n pattern: ^(\\[[0-9a-fA-F:]+\\]|[^:/\\s]+):[0-9]{1,5}$\n description: Target in \"host:port\" form. IPv6 addresses must be bracketed.\n periodicity:\n not:\n enum:\n - PERIODICITY_UNSPECIFIED\n title: periodicity\n description: Check periodicity (required).\n $ref: '#/components/schemas/openstatus.monitor.v1.Periodicity'\n timeout:\n type:\n - integer\n - string\n title: timeout\n maximum: 300000\n minimum: 0\n format: int64\n description: Timeout in milliseconds (0-300000, defaults to 45000).\n degradedAt:\n type:\n - integer\n - string\n - \"null\"\n title: degraded_at\n maximum: 300000\n minimum: 0\n format: int64\n description: Latency threshold for degraded status in milliseconds (optional, 0-300000).\n retry:\n type:\n - integer\n - string\n title: retry\n maximum: 10\n minimum: 0\n format: int64\n description: Number of retry attempts (0-10, defaults to 3).\n description:\n type:\n - string\n - \"null\"\n title: description\n maxLength: 1024\n description: Description of the monitor (optional).\n active:\n type:\n - boolean\n - \"null\"\n title: active\n description: Whether the monitor is active (defaults to false).\n public:\n type:\n - boolean\n - \"null\"\n title: public\n description: Whether the monitor is publicly visible (defaults to false).\n regions:\n type: array\n items:\n $ref: '#/components/schemas/openstatus.monitor.v1.Region'\n title: regions\n maxItems: 28\n description: Geographic regions to run checks from.\n openTelemetry:\n title: open_telemetry\n description: OpenTelemetry configuration for exporting metrics.\n $ref: '#/components/schemas/openstatus.monitor.v1.OpenTelemetryConfig'\n status:\n title: status\n description: Current operational status of the monitor.\n $ref: '#/components/schemas/openstatus.monitor.v1.MonitorStatus'\n privateLocationIds:\n type: array\n items:\n type: string\n readOnly: true\n title: private_location_ids\n description: IDs of private locations that run this monitor. Read-only.\n readOnly: true\n service:\n type:\n - string\n - \"null\"\n examples:\n - checkout.v1.CheckoutService\n title: service\n maxLength: 512\n description: Service name passed to Health/Check. Empty means overall server health.\n tlsMode:\n oneOf:\n - $ref: '#/components/schemas/openstatus.monitor.v1.GRPCTlsMode'\n - type: \"null\"\n title: tls_mode\n description: How the connection to the target is secured. Defaults to TLS.\n metadata:\n type: array\n items:\n $ref: '#/components/schemas/openstatus.monitor.v1.Headers'\n title: metadata\n maxItems: 20\n description: Metadata sent with the health check request.\n title: GRPCMonitor\n additionalProperties: false\n description: |-\n GRPCMonitor defines the configuration for a gRPC health check monitor.\n The probe calls grpc.health.v1.Health/Check on the target.\n openstatus.monitor.v1.GRPCTlsMode:\n type: string\n title: GRPCTlsMode\n enum:\n - GRPC_TLS_MODE_UNSPECIFIED\n - GRPC_TLS_MODE_TLS\n - GRPC_TLS_MODE_PLAINTEXT\n - GRPC_TLS_MODE_TLS_INSECURE\n description: GRPCTlsMode selects how the probe secures its connection to the target.\n openstatus.monitor.v1.GetMonitorHTTPResponseLogRequest:\n type: object\n properties:\n id:\n type: string\n title: id\n minLength: 1\n description: Monitor ID to get a response log for (required).\n logId:\n type: string\n title: log_id\n minLength: 1\n description: Response log ID to retrieve (required).\n title: GetMonitorHTTPResponseLogRequest\n additionalProperties: false\n description: GetMonitorHTTPResponseLogRequest is the request to get one response log.\n openstatus.monitor.v1.GetMonitorHTTPResponseLogResponse:\n type: object\n properties:\n log:\n title: log\n description: Response log details.\n $ref: '#/components/schemas/openstatus.monitor.v1.HTTPResponseLogDetail'\n title: GetMonitorHTTPResponseLogResponse\n additionalProperties: false\n description: GetMonitorHTTPResponseLogResponse is the response containing one response log.\n openstatus.monitor.v1.GetMonitorRequest:\n type: object\n properties:\n id:\n type: string\n title: id\n minLength: 1\n description: Monitor ID to retrieve (required).\n title: GetMonitorRequest\n additionalProperties: false\n description: GetMonitorRequest is the request to get a single monitor by ID.\n openstatus.monitor.v1.GetMonitorResponse:\n type: object\n properties:\n monitor:\n title: monitor\n description: The monitor configuration (one of HTTP, TCP, DNS, ICMP, or gRPC).\n $ref: '#/components/schemas/openstatus.monitor.v1.MonitorConfig'\n title: GetMonitorResponse\n additionalProperties: false\n description: GetMonitorResponse is the response containing the monitor.\n openstatus.monitor.v1.GetMonitorStatusRequest:\n type: object\n properties:\n id:\n type: string\n title: id\n minLength: 1\n description: Monitor ID to get status for (required).\n title: GetMonitorStatusRequest\n additionalProperties: false\n description: GetMonitorStatusRequest is the request to get the status of all regions for a monitor.\n openstatus.monitor.v1.GetMonitorStatusResponse:\n type: object\n properties:\n id:\n type: string\n title: id\n description: Monitor ID.\n regions:\n type: array\n items:\n $ref: '#/components/schemas/openstatus.monitor.v1.RegionStatus'\n title: regions\n description: Status for each region.\n title: GetMonitorStatusResponse\n additionalProperties: false\n description: GetMonitorStatusResponse is the response containing the status of all regions for a monitor.\n openstatus.monitor.v1.GetMonitorSummaryRequest:\n type: object\n properties:\n id:\n type: string\n title: id\n minLength: 1\n description: Monitor ID to get summary for (required).\n timeRange:\n title: time_range\n description: Time range for metrics aggregation (defaults to 1 day if unspecified).\n $ref: '#/components/schemas/openstatus.monitor.v1.TimeRange'\n regions:\n type: array\n items:\n $ref: '#/components/schemas/openstatus.monitor.v1.Region'\n title: regions\n maxItems: 28\n description: Optional filter by regions. If empty, returns metrics for all regions.\n title: GetMonitorSummaryRequest\n additionalProperties: false\n description: GetMonitorSummaryRequest is the request to get aggregated metrics for a monitor.\n openstatus.monitor.v1.GetMonitorSummaryResponse:\n type: object\n properties:\n id:\n type: string\n title: id\n description: Monitor ID.\n lastPingAt:\n type: string\n title: last_ping_at\n description: Timestamp of the last check in RFC 3339 format.\n totalSuccessful:\n type:\n - integer\n - string\n title: total_successful\n format: int64\n description: Total number of successful requests.\n totalDegraded:\n type:\n - integer\n - string\n title: total_degraded\n format: int64\n description: Total number of degraded requests.\n totalFailed:\n type:\n - integer\n - string\n title: total_failed\n format: int64\n description: Total number of failed requests.\n p50:\n type:\n - integer\n - string\n title: p50\n format: int64\n description: 50th percentile (median) latency in milliseconds.\n p75:\n type:\n - integer\n - string\n title: p75\n format: int64\n description: 75th percentile latency in milliseconds.\n p90:\n type:\n - integer\n - string\n title: p90\n format: int64\n description: 90th percentile latency in milliseconds.\n p95:\n type:\n - integer\n - string\n title: p95\n format: int64\n description: 95th percentile latency in milliseconds.\n p99:\n type:\n - integer\n - string\n title: p99\n format: int64\n description: 99th percentile latency in milliseconds.\n timeRange:\n title: time_range\n description: Time range used for the metrics.\n $ref: '#/components/schemas/openstatus.monitor.v1.TimeRange'\n regions:\n type: array\n items:\n $ref: '#/components/schemas/openstatus.monitor.v1.Region'\n title: regions\n description: Regions included in the metrics.\n title: GetMonitorSummaryResponse\n additionalProperties: false\n description: GetMonitorSummaryResponse is the response containing aggregated metrics for a monitor.\n openstatus.monitor.v1.HTTPMethod:\n type: string\n title: HTTPMethod\n enum:\n - HTTP_METHOD_UNSPECIFIED\n - HTTP_METHOD_GET\n - HTTP_METHOD_POST\n - HTTP_METHOD_HEAD\n - HTTP_METHOD_PUT\n - HTTP_METHOD_PATCH\n - HTTP_METHOD_DELETE\n - HTTP_METHOD_TRACE\n - HTTP_METHOD_CONNECT\n - HTTP_METHOD_OPTIONS\n description: HTTP methods supported for monitors.\n openstatus.monitor.v1.HTTPMonitor:\n type: object\n properties:\n id:\n type: string\n title: id\n description: Unique identifier for the monitor (output only for create requests).\n name:\n type: string\n examples:\n - Production API Health Check\n title: name\n maxLength: 256\n minLength: 1\n description: Name of the monitor (required, max 256 characters).\n url:\n type: string\n examples:\n - https://api.example.com/health\n title: url\n maxLength: 2048\n minLength: 1\n format: uri\n description: URL to monitor (required, max 2048 characters).\n periodicity:\n not:\n enum:\n - PERIODICITY_UNSPECIFIED\n title: periodicity\n description: Check periodicity (required).\n $ref: '#/components/schemas/openstatus.monitor.v1.Periodicity'\n method:\n not:\n enum:\n - HTTP_METHOD_UNSPECIFIED\n title: method\n description: HTTP method to use (defaults to GET).\n $ref: '#/components/schemas/openstatus.monitor.v1.HTTPMethod'\n body:\n type: string\n examples:\n - map[key:value]\n title: body\n description: Request body (optional).\n timeout:\n type:\n - integer\n - string\n title: timeout\n maximum: 300000\n minimum: 0\n format: int64\n description: Timeout in milliseconds (0-300000, defaults to 45000).\n degradedAt:\n type:\n - integer\n - string\n - \"null\"\n title: degraded_at\n maximum: 300000\n minimum: 0\n format: int64\n description: Latency threshold for degraded status in milliseconds (optional, 0-300000).\n retry:\n type:\n - integer\n - string\n title: retry\n maximum: 10\n minimum: 0\n format: int64\n description: Number of retry attempts (0-10, defaults to 3).\n followRedirects:\n type:\n - boolean\n - \"null\"\n title: follow_redirects\n description: Whether to follow HTTP redirects (defaults to true when not specified).\n headers:\n type: array\n items:\n $ref: '#/components/schemas/openstatus.monitor.v1.Headers'\n title: headers\n maxItems: 20\n description: Custom headers for the request.\n statusCodeAssertions:\n type: array\n items:\n $ref: '#/components/schemas/openstatus.monitor.v1.StatusCodeAssertion'\n title: status_code_assertions\n maxItems: 10\n description: Status code assertions for the response.\n bodyAssertions:\n type: array\n items:\n $ref: '#/components/schemas/openstatus.monitor.v1.BodyAssertion'\n title: body_assertions\n maxItems: 10\n description: Body content assertions for the response.\n headerAssertions:\n type: array\n items:\n $ref: '#/components/schemas/openstatus.monitor.v1.HeaderAssertion'\n title: header_assertions\n maxItems: 10\n description: Header assertions for the response.\n description:\n type:\n - string\n - \"null\"\n title: description\n maxLength: 1024\n description: Description of the monitor (optional).\n active:\n type:\n - boolean\n - \"null\"\n title: active\n description: Whether the monitor is active (defaults to false).\n public:\n type:\n - boolean\n - \"null\"\n title: public\n description: Whether the monitor is publicly visible (defaults to false).\n regions:\n type: array\n items:\n $ref: '#/components/schemas/openstatus.monitor.v1.Region'\n title: regions\n maxItems: 28\n description: Geographic regions to run checks from.\n openTelemetry:\n title: open_telemetry\n description: OpenTelemetry configuration for exporting metrics.\n $ref: '#/components/schemas/openstatus.monitor.v1.OpenTelemetryConfig'\n status:\n title: status\n description: Current operational status of the monitor.\n $ref: '#/components/schemas/openstatus.monitor.v1.MonitorStatus'\n privateLocationIds:\n type: array\n items:\n type: string\n readOnly: true\n title: private_location_ids\n description: IDs of private locations that run this monitor. Read-only.\n readOnly: true\n title: HTTPMonitor\n additionalProperties: false\n description: HTTPMonitor defines the configuration for an HTTP monitor.\n openstatus.monitor.v1.HTTPResponseLogDetail:\n type: object\n properties:\n log:\n title: log\n description: Compact response log fields.\n $ref: '#/components/schemas/openstatus.monitor.v1.HTTPResponseLogListItem'\n url:\n type: string\n title: url\n description: Checked URL.\n error:\n type: boolean\n title: error\n description: Whether the check errored.\n message:\n type:\n - string\n - \"null\"\n title: message\n description: Error message, when present.\n headers:\n type: object\n title: headers\n additionalProperties:\n type: string\n title: value\n description: Redacted response headers.\n assertions:\n type:\n - string\n - \"null\"\n title: assertions\n description: Serialized assertions used for the check.\n title: HTTPResponseLogDetail\n additionalProperties: false\n description: HTTPResponseLogDetail contains full response log debugging data.\n openstatus.monitor.v1.HTTPResponseLogDetail.HeadersEntry:\n type: object\n properties:\n key:\n type: string\n title: key\n value:\n type: string\n title: value\n title: HeadersEntry\n additionalProperties: false\n openstatus.monitor.v1.HTTPResponseLogListItem:\n type: object\n properties:\n id:\n type:\n - string\n - \"null\"\n title: id\n description: Response log ID.\n latency:\n type: integer\n title: latency\n format: int32\n description: Latency in milliseconds.\n statusCode:\n type:\n - integer\n - \"null\"\n title: status_code\n format: int32\n description: HTTP status code.\n monitorId:\n type: string\n title: monitor_id\n description: Monitor ID.\n requestStatus:\n title: request_status\n description: Request status classification.\n $ref: '#/components/schemas/openstatus.monitor.v1.HTTPResponseLogRequestStatus'\n region:\n title: region\n description: Region where the check ran.\n $ref: '#/components/schemas/openstatus.monitor.v1.Region'\n cronTimestamp:\n type:\n - integer\n - string\n title: cron_timestamp\n format: int64\n description: Cron bucket timestamp in Unix milliseconds.\n trigger:\n title: trigger\n description: Check trigger.\n $ref: '#/components/schemas/openstatus.monitor.v1.HTTPResponseLogTrigger'\n timestamp:\n type:\n - integer\n - string\n title: timestamp\n format: int64\n description: Response timestamp in Unix milliseconds.\n timing:\n oneOf:\n - $ref: '#/components/schemas/openstatus.monitor.v1.HTTPResponseLogTiming'\n - type: \"null\"\n title: timing\n description: Timing phases.\n title: HTTPResponseLogListItem\n additionalProperties: false\n description: HTTPResponseLogListItem is a compact response log entry.\n openstatus.monitor.v1.HTTPResponseLogPagination:\n type: object\n properties:\n limit:\n type: integer\n title: limit\n format: int32\n description: Requested page size.\n offset:\n type: integer\n title: offset\n format: int32\n description: Requested offset.\n hasMore:\n type: boolean\n title: has_more\n description: Whether more logs are available.\n nextOffset:\n type:\n - integer\n - \"null\"\n title: next_offset\n format: int32\n description: Next offset if more logs are available.\n title: HTTPResponseLogPagination\n additionalProperties: false\n description: HTTPResponseLogPagination contains offset pagination metadata.\n openstatus.monitor.v1.HTTPResponseLogRequestStatus:\n type: string\n title: HTTPResponseLogRequestStatus\n enum:\n - HTTP_RESPONSE_LOG_REQUEST_STATUS_UNSPECIFIED\n - HTTP_RESPONSE_LOG_REQUEST_STATUS_SUCCESS\n - HTTP_RESPONSE_LOG_REQUEST_STATUS_ERROR\n - HTTP_RESPONSE_LOG_REQUEST_STATUS_DEGRADED\n description: HTTPResponseLogRequestStatus is the result classification for an HTTP response log.\n openstatus.monitor.v1.HTTPResponseLogTiming:\n type: object\n properties:\n dns:\n type: integer\n title: dns\n format: int32\n description: DNS lookup duration.\n connect:\n type: integer\n title: connect\n format: int32\n description: TCP connection duration.\n tls:\n type: integer\n title: tls\n format: int32\n description: TLS handshake duration.\n ttfb:\n type: integer\n title: ttfb\n format: int32\n description: Time to first byte duration.\n transfer:\n type: integer\n title: transfer\n format: int32\n description: Response transfer duration.\n title: HTTPResponseLogTiming\n additionalProperties: false\n description: HTTPResponseLogTiming contains calculated timing phases in milliseconds.\n openstatus.monitor.v1.HTTPResponseLogTrigger:\n type: string\n title: HTTPResponseLogTrigger\n enum:\n - HTTP_RESPONSE_LOG_TRIGGER_UNSPECIFIED\n - HTTP_RESPONSE_LOG_TRIGGER_CRON\n - HTTP_RESPONSE_LOG_TRIGGER_API\n description: HTTPResponseLogTrigger describes what started the monitor check.\n openstatus.monitor.v1.HeaderAssertion:\n type: object\n properties:\n target:\n type: string\n title: target\n description: Target value to compare against.\n comparator:\n not:\n enum:\n - STRING_COMPARATOR_UNSPECIFIED\n title: comparator\n description: Comparison operation (required, must not be UNSPECIFIED).\n $ref: '#/components/schemas/openstatus.monitor.v1.StringComparator'\n key:\n type: string\n title: key\n minLength: 1\n description: Header key to check (required).\n title: HeaderAssertion\n additionalProperties: false\n description: HeaderAssertion defines an assertion for response headers.\n openstatus.monitor.v1.Headers:\n type: object\n properties:\n key:\n type: string\n examples:\n - Authorization\n title: key\n minLength: 1\n description: Header name.\n value:\n type: string\n examples:\n - Bearer token123\n title: value\n description: Header value.\n title: Headers\n additionalProperties: false\n description: Headers represents a key-value pair for HTTP headers.\n openstatus.monitor.v1.ICMPMonitor:\n type: object\n properties:\n id:\n type: string\n title: id\n description: Unique identifier for the monitor (output only for create requests).\n name:\n type: string\n examples:\n - Ping Gateway\n title: name\n maxLength: 256\n minLength: 1\n description: Name of the monitor (required, max 256 characters).\n uri:\n type: string\n examples:\n - 1.1.1.1\n title: uri\n maxLength: 2048\n minLength: 1\n description: URI to monitor in format \"host or IP\" (required, max 2048 characters).\n periodicity:\n not:\n enum:\n - PERIODICITY_UNSPECIFIED\n title: periodicity\n description: Check periodicity (required).\n $ref: '#/components/schemas/openstatus.monitor.v1.Periodicity'\n timeout:\n type:\n - integer\n - string\n title: timeout\n maximum: 300000\n minimum: 0\n format: int64\n description: Timeout in milliseconds (0-300000, defaults to 45000).\n degradedAt:\n type:\n - integer\n - string\n - \"null\"\n title: degraded_at\n maximum: 300000\n minimum: 0\n format: int64\n description: Latency threshold for degraded status in milliseconds (optional, 0-300000).\n retry:\n type:\n - integer\n - string\n title: retry\n maximum: 10\n minimum: 0\n format: int64\n description: Number of retry attempts (0-10, defaults to 3).\n description:\n type:\n - string\n - \"null\"\n title: description\n maxLength: 1024\n description: Description of the monitor (optional).\n active:\n type:\n - boolean\n - \"null\"\n title: active\n description: Whether the monitor is active (defaults to false).\n public:\n type:\n - boolean\n - \"null\"\n title: public\n description: Whether the monitor is publicly visible (defaults to false).\n regions:\n type: array\n items:\n $ref: '#/components/schemas/openstatus.monitor.v1.Region'\n title: regions\n maxItems: 28\n description: Geographic regions to run checks from.\n openTelemetry:\n title: open_telemetry\n description: OpenTelemetry configuration for exporting metrics.\n $ref: '#/components/schemas/openstatus.monitor.v1.OpenTelemetryConfig'\n status:\n title: status\n description: Current operational status of the monitor.\n $ref: '#/components/schemas/openstatus.monitor.v1.MonitorStatus'\n privateLocationIds:\n type: array\n items:\n type: string\n readOnly: true\n title: private_location_ids\n description: IDs of private locations that run this monitor. Read-only.\n readOnly: true\n title: ICMPMonitor\n additionalProperties: false\n description: ICMPMonitor defines the configuration for a ICMP monitor.\n openstatus.monitor.v1.ListMonitorHTTPResponseLogsRequest:\n type: object\n properties:\n id:\n type: string\n title: id\n minLength: 1\n description: Monitor ID to list response logs for (required).\n fromTimestamp:\n type:\n - integer\n - string\n - \"null\"\n title: from_timestamp\n format: int64\n description: Start of the response log window as Unix milliseconds within the 14-day retention window.\n toTimestamp:\n type:\n - integer\n - string\n - \"null\"\n title: to_timestamp\n format: int64\n description: End of the response log window as Unix milliseconds within the 14-day retention window.\n limit:\n type:\n - integer\n - \"null\"\n title: limit\n maximum: 100\n minimum: 1\n format: int32\n description: Maximum number of logs to return (1-100, defaults to 25).\n offset:\n type:\n - integer\n - \"null\"\n title: offset\n minimum: 0\n format: int32\n description: Number of logs to skip for pagination (defaults to 0).\n title: ListMonitorHTTPResponseLogsRequest\n additionalProperties: false\n description: ListMonitorHTTPResponseLogsRequest is the request to list response logs within the 14-day HTTP response-log window.\n openstatus.monitor.v1.ListMonitorHTTPResponseLogsResponse:\n type: object\n properties:\n logs:\n type: array\n items:\n $ref: '#/components/schemas/openstatus.monitor.v1.HTTPResponseLogListItem'\n title: logs\n description: Response logs.\n pagination:\n title: pagination\n description: Pagination metadata.\n $ref: '#/components/schemas/openstatus.monitor.v1.HTTPResponseLogPagination'\n title: ListMonitorHTTPResponseLogsResponse\n additionalProperties: false\n description: ListMonitorHTTPResponseLogsResponse is the response containing response logs.\n openstatus.monitor.v1.ListMonitorsRequest:\n type: object\n properties:\n limit:\n type:\n - integer\n - \"null\"\n title: limit\n maximum: 100\n minimum: 1\n format: int32\n description: Maximum number of monitors to return (1-100, defaults to 50).\n offset:\n type:\n - integer\n - \"null\"\n title: offset\n minimum: 0\n format: int32\n description: Number of monitors to skip for pagination (defaults to 0).\n title: ListMonitorsRequest\n additionalProperties: false\n description: ListMonitorsRequest is the request to list monitors.\n openstatus.monitor.v1.ListMonitorsResponse:\n type: object\n properties:\n httpMonitors:\n type: array\n items:\n $ref: '#/components/schemas/openstatus.monitor.v1.HTTPMonitor'\n title: http_monitors\n description: HTTP monitors in the workspace.\n tcpMonitors:\n type: array\n items:\n $ref: '#/components/schemas/openstatus.monitor.v1.TCPMonitor'\n title: tcp_monitors\n description: TCP monitors in the workspace.\n dnsMonitors:\n type: array\n items:\n $ref: '#/components/schemas/openstatus.monitor.v1.DNSMonitor'\n title: dns_monitors\n description: DNS monitors in the workspace.\n icmpMonitors:\n type: array\n items:\n $ref: '#/components/schemas/openstatus.monitor.v1.ICMPMonitor'\n title: icmp_monitors\n description: ICMP monitors in the workspace.\n grpcMonitors:\n type: array\n items:\n $ref: '#/components/schemas/openstatus.monitor.v1.GRPCMonitor'\n title: grpc_monitors\n description: gRPC monitors in the workspace.\n totalSize:\n type: integer\n title: total_size\n format: int32\n description: Total number of monitors across all types.\n title: ListMonitorsResponse\n additionalProperties: false\n description: ListMonitorsResponse is the response containing a list of monitors.\n openstatus.monitor.v1.MonitorConfig:\n type: object\n oneOf:\n - type: object\n properties:\n dns:\n title: dns\n description: DNS monitor configuration.\n $ref: '#/components/schemas/openstatus.monitor.v1.DNSMonitor'\n title: dns\n required:\n - dns\n - type: object\n properties:\n grpc:\n title: grpc\n description: gRPC monitor configuration.\n $ref: '#/components/schemas/openstatus.monitor.v1.GRPCMonitor'\n title: grpc\n required:\n - grpc\n - type: object\n properties:\n http:\n title: http\n description: HTTP monitor configuration.\n $ref: '#/components/schemas/openstatus.monitor.v1.HTTPMonitor'\n title: http\n required:\n - http\n - type: object\n properties:\n icmp:\n title: icmp\n description: ICMP monitor configuration.\n $ref: '#/components/schemas/openstatus.monitor.v1.ICMPMonitor'\n title: icmp\n required:\n - icmp\n - type: object\n properties:\n tcp:\n title: tcp\n description: TCP monitor configuration.\n $ref: '#/components/schemas/openstatus.monitor.v1.TCPMonitor'\n title: tcp\n required:\n - tcp\n title: MonitorConfig\n additionalProperties: false\n description: MonitorConfig represents the type-specific configuration for a monitor.\n openstatus.monitor.v1.MonitorStatus:\n type: string\n title: MonitorStatus\n enum:\n - MONITOR_STATUS_UNSPECIFIED\n - MONITOR_STATUS_ACTIVE\n - MONITOR_STATUS_DEGRADED\n - MONITOR_STATUS_ERROR\n description: MonitorStatus represents the operational status of a monitor.\n openstatus.monitor.v1.NumberComparator:\n type: string\n title: NumberComparator\n enum:\n - NUMBER_COMPARATOR_UNSPECIFIED\n - NUMBER_COMPARATOR_EQUAL\n - NUMBER_COMPARATOR_NOT_EQUAL\n - NUMBER_COMPARATOR_GREATER_THAN\n - NUMBER_COMPARATOR_GREATER_THAN_OR_EQUAL\n - NUMBER_COMPARATOR_LESS_THAN\n - NUMBER_COMPARATOR_LESS_THAN_OR_EQUAL\n description: NumberComparator defines comparison operations for numeric values.\n openstatus.monitor.v1.OpenTelemetryConfig:\n type: object\n properties:\n endpoint:\n type: string\n title: endpoint\n maxLength: 2048\n description: OTEL endpoint URL.\n headers:\n type: array\n items:\n $ref: '#/components/schemas/openstatus.monitor.v1.Headers'\n title: headers\n maxItems: 20\n description: Custom headers for OTEL requests.\n title: OpenTelemetryConfig\n additionalProperties: false\n description: OpenTelemetry configuration for exporting metrics.\n openstatus.monitor.v1.Periodicity:\n type: string\n title: Periodicity\n enum:\n - PERIODICITY_UNSPECIFIED\n - PERIODICITY_30S\n - PERIODICITY_1M\n - PERIODICITY_5M\n - PERIODICITY_10M\n - PERIODICITY_30M\n - PERIODICITY_1H\n description: Monitor periodicity options.\n openstatus.monitor.v1.RecordAssertion:\n type: object\n properties:\n record:\n type: string\n title: record\n enum:\n - A\n - AAAA\n - CNAME\n - MX\n - TXT\n description: DNS record type (e.g., \"A\", \"AAAA\", \"CNAME\", \"MX\", \"TXT\").\n comparator:\n not:\n enum:\n - RECORD_COMPARATOR_UNSPECIFIED\n title: comparator\n description: Comparison operation (required, must not be UNSPECIFIED).\n $ref: '#/components/schemas/openstatus.monitor.v1.RecordComparator'\n target:\n type: string\n title: target\n description: Target value to compare against.\n title: RecordAssertion\n additionalProperties: false\n description: RecordAssertion defines an assertion for DNS records.\n openstatus.monitor.v1.RecordComparator:\n type: string\n title: RecordComparator\n enum:\n - RECORD_COMPARATOR_UNSPECIFIED\n - RECORD_COMPARATOR_EQUAL\n - RECORD_COMPARATOR_NOT_EQUAL\n - RECORD_COMPARATOR_CONTAINS\n - RECORD_COMPARATOR_NOT_CONTAINS\n description: RecordComparator defines comparison operations for DNS records.\n openstatus.monitor.v1.Region:\n type: string\n title: Region\n enum:\n - REGION_UNSPECIFIED\n - REGION_FLY_AMS\n - REGION_FLY_ARN\n - REGION_FLY_BOM\n - REGION_FLY_CDG\n - REGION_FLY_DFW\n - REGION_FLY_EWR\n - REGION_FLY_FRA\n - REGION_FLY_GRU\n - REGION_FLY_IAD\n - REGION_FLY_JNB\n - REGION_FLY_LAX\n - REGION_FLY_LHR\n - REGION_FLY_NRT\n - REGION_FLY_ORD\n - REGION_FLY_SJC\n - REGION_FLY_SIN\n - REGION_FLY_SYD\n - REGION_FLY_YYZ\n - REGION_KOYEB_FRA\n - REGION_KOYEB_PAR\n - REGION_KOYEB_SFO\n - REGION_KOYEB_SIN\n - REGION_KOYEB_TYO\n - REGION_KOYEB_WAS\n - REGION_RAILWAY_US_WEST2\n - REGION_RAILWAY_US_EAST4\n - REGION_RAILWAY_EUROPE_WEST4\n - REGION_RAILWAY_ASIA_SOUTHEAST1\n description: |-\n Geographic regions where monitors can run checks from.\n REGION_FLY_BOM is deprecated and rejected on create/update; use REGION_FLY_SIN.\n openstatus.monitor.v1.RegionStatus:\n type: object\n properties:\n region:\n title: region\n description: The region identifier.\n $ref: '#/components/schemas/openstatus.monitor.v1.Region'\n status:\n title: status\n description: The status of the monitor in this region.\n $ref: '#/components/schemas/openstatus.monitor.v1.MonitorStatus'\n title: RegionStatus\n additionalProperties: false\n description: RegionStatus represents the status of a monitor in a specific region.\n openstatus.monitor.v1.StatusCodeAssertion:\n type: object\n properties:\n target:\n type:\n - integer\n - string\n title: target\n maximum: 599\n minimum: 100\n format: int64\n description: Target status code to compare against (100-599).\n comparator:\n not:\n enum:\n - NUMBER_COMPARATOR_UNSPECIFIED\n title: comparator\n description: Comparison operation (required, must not be UNSPECIFIED).\n $ref: '#/components/schemas/openstatus.monitor.v1.NumberComparator'\n title: StatusCodeAssertion\n additionalProperties: false\n description: StatusCodeAssertion defines an assertion for HTTP status codes.\n openstatus.monitor.v1.StringComparator:\n type: string\n title: StringComparator\n enum:\n - STRING_COMPARATOR_UNSPECIFIED\n - STRING_COMPARATOR_CONTAINS\n - STRING_COMPARATOR_NOT_CONTAINS\n - STRING_COMPARATOR_EQUAL\n - STRING_COMPARATOR_NOT_EQUAL\n - STRING_COMPARATOR_EMPTY\n - STRING_COMPARATOR_NOT_EMPTY\n - STRING_COMPARATOR_GREATER_THAN\n - STRING_COMPARATOR_GREATER_THAN_OR_EQUAL\n - STRING_COMPARATOR_LESS_THAN\n - STRING_COMPARATOR_LESS_THAN_OR_EQUAL\n description: StringComparator defines comparison operations for string values.\n openstatus.monitor.v1.TCPMonitor:\n type: object\n properties:\n id:\n type: string\n title: id\n description: Unique identifier for the monitor (output only for create requests).\n name:\n type: string\n examples:\n - Database Connection Check\n title: name\n maxLength: 256\n minLength: 1\n description: Name of the monitor (required, max 256 characters).\n uri:\n type: string\n examples:\n - tcp://db.example.com:5432\n title: uri\n maxLength: 2048\n minLength: 1\n description: URI to monitor in format \"host:port\" (required, max 2048 characters).\n periodicity:\n not:\n enum:\n - PERIODICITY_UNSPECIFIED\n title: periodicity\n description: Check periodicity (required).\n $ref: '#/components/schemas/openstatus.monitor.v1.Periodicity'\n timeout:\n type:\n - integer\n - string\n title: timeout\n maximum: 300000\n minimum: 0\n format: int64\n description: Timeout in milliseconds (0-300000, defaults to 45000).\n degradedAt:\n type:\n - integer\n - string\n - \"null\"\n title: degraded_at\n maximum: 300000\n minimum: 0\n format: int64\n description: Latency threshold for degraded status in milliseconds (optional, 0-300000).\n retry:\n type:\n - integer\n - string\n title: retry\n maximum: 10\n minimum: 0\n format: int64\n description: Number of retry attempts (0-10, defaults to 3).\n description:\n type:\n - string\n - \"null\"\n title: description\n maxLength: 1024\n description: Description of the monitor (optional).\n active:\n type:\n - boolean\n - \"null\"\n title: active\n description: Whether the monitor is active (defaults to false).\n public:\n type:\n - boolean\n - \"null\"\n title: public\n description: Whether the monitor is publicly visible (defaults to false).\n regions:\n type: array\n items:\n $ref: '#/components/schemas/openstatus.monitor.v1.Region'\n title: regions\n maxItems: 28\n description: Geographic regions to run checks from.\n openTelemetry:\n title: open_telemetry\n description: OpenTelemetry configuration for exporting metrics.\n $ref: '#/components/schemas/openstatus.monitor.v1.OpenTelemetryConfig'\n status:\n title: status\n description: Current operational status of the monitor.\n $ref: '#/components/schemas/openstatus.monitor.v1.MonitorStatus'\n privateLocationIds:\n type: array\n items:\n type: string\n readOnly: true\n title: private_location_ids\n description: IDs of private locations that run this monitor. Read-only.\n readOnly: true\n title: TCPMonitor\n additionalProperties: false\n description: TCPMonitor defines the configuration for a TCP monitor.\n openstatus.monitor.v1.TimeRange:\n type: string\n title: TimeRange\n enum:\n - TIME_RANGE_UNSPECIFIED\n - TIME_RANGE_1D\n - TIME_RANGE_7D\n - TIME_RANGE_14D\n description: TimeRange represents the time period for metrics aggregation.\n openstatus.monitor.v1.TriggerMonitorRequest:\n type: object\n properties:\n id:\n type: string\n title: id\n minLength: 1\n description: Monitor ID to trigger (required).\n title: TriggerMonitorRequest\n additionalProperties: false\n description: TriggerMonitorRequest is the request to trigger a monitor check.\n openstatus.monitor.v1.TriggerMonitorResponse:\n type: object\n properties:\n success:\n type: boolean\n title: success\n description: Whether the trigger was successful.\n title: TriggerMonitorResponse\n additionalProperties: false\n description: TriggerMonitorResponse is the response after triggering a monitor.\n openstatus.monitor.v1.UpdateDNSMonitorRequest:\n type: object\n properties:\n id:\n type: string\n title: id\n minLength: 1\n description: Monitor ID to update (required).\n monitor:\n oneOf:\n - $ref: '#/components/schemas/openstatus.monitor.v1.DNSMonitor'\n - type: \"null\"\n title: monitor\n description: Updated monitor configuration (all fields optional for partial updates).\n title: UpdateDNSMonitorRequest\n additionalProperties: false\n description: UpdateDNSMonitorRequest is the request to update an existing DNS monitor.\n openstatus.monitor.v1.UpdateDNSMonitorResponse:\n type: object\n properties:\n monitor:\n title: monitor\n description: The updated monitor.\n $ref: '#/components/schemas/openstatus.monitor.v1.DNSMonitor'\n title: UpdateDNSMonitorResponse\n additionalProperties: false\n description: UpdateDNSMonitorResponse is the response after updating a DNS monitor.\n openstatus.monitor.v1.UpdateGRPCMonitorRequest:\n type: object\n properties:\n id:\n type: string\n title: id\n minLength: 1\n description: Monitor ID to update (required).\n monitor:\n oneOf:\n - $ref: '#/components/schemas/openstatus.monitor.v1.GRPCMonitor'\n - type: \"null\"\n title: monitor\n description: Updated monitor configuration (all fields optional for partial updates).\n title: UpdateGRPCMonitorRequest\n additionalProperties: false\n description: UpdateGRPCMonitorRequest is the request to update an existing gRPC monitor.\n openstatus.monitor.v1.UpdateGRPCMonitorResponse:\n type: object\n properties:\n monitor:\n title: monitor\n description: The updated monitor.\n $ref: '#/components/schemas/openstatus.monitor.v1.GRPCMonitor'\n title: UpdateGRPCMonitorResponse\n additionalProperties: false\n description: UpdateGRPCMonitorResponse is the response after updating a gRPC monitor.\n openstatus.monitor.v1.UpdateHTTPMonitorRequest:\n type: object\n properties:\n id:\n type: string\n title: id\n minLength: 1\n description: Monitor ID to update (required).\n monitor:\n oneOf:\n - $ref: '#/components/schemas/openstatus.monitor.v1.HTTPMonitor'\n - type: \"null\"\n title: monitor\n description: Updated monitor configuration (all fields optional for partial updates).\n title: UpdateHTTPMonitorRequest\n additionalProperties: false\n description: UpdateHTTPMonitorRequest is the request to update an existing HTTP monitor.\n openstatus.monitor.v1.UpdateHTTPMonitorResponse:\n type: object\n properties:\n monitor:\n title: monitor\n description: The updated monitor.\n $ref: '#/components/schemas/openstatus.monitor.v1.HTTPMonitor'\n title: UpdateHTTPMonitorResponse\n additionalProperties: false\n description: UpdateHTTPMonitorResponse is the response after updating an HTTP monitor.\n openstatus.monitor.v1.UpdateICMPMonitorRequest:\n type: object\n properties:\n id:\n type: string\n title: id\n minLength: 1\n description: Monitor ID to update (required).\n monitor:\n oneOf:\n - $ref: '#/components/schemas/openstatus.monitor.v1.ICMPMonitor'\n - type: \"null\"\n title: monitor\n description: Updated monitor configuration (all fields optional for partial updates).\n title: UpdateICMPMonitorRequest\n additionalProperties: false\n description: UpdateICMPMonitorRequest is the request to update an existing ICMP monitor.\n openstatus.monitor.v1.UpdateICMPMonitorResponse:\n type: object\n properties:\n monitor:\n title: monitor\n description: The updated monitor.\n $ref: '#/components/schemas/openstatus.monitor.v1.ICMPMonitor'\n title: UpdateICMPMonitorResponse\n additionalProperties: false\n description: UpdateICMPMonitorResponse is the response after updating an ICMP monitor.\n openstatus.monitor.v1.UpdateTCPMonitorRequest:\n type: object\n properties:\n id:\n type: string\n title: id\n minLength: 1\n description: Monitor ID to update (required).\n monitor:\n oneOf:\n - $ref: '#/components/schemas/openstatus.monitor.v1.TCPMonitor'\n - type: \"null\"\n title: monitor\n description: Updated monitor configuration (all fields optional for partial updates).\n title: UpdateTCPMonitorRequest\n additionalProperties: false\n description: UpdateTCPMonitorRequest is the request to update an existing TCP monitor.\n openstatus.monitor.v1.UpdateTCPMonitorResponse:\n type: object\n properties:\n monitor:\n title: monitor\n description: The updated monitor.\n $ref: '#/components/schemas/openstatus.monitor.v1.TCPMonitor'\n title: UpdateTCPMonitorResponse\n additionalProperties: false\n description: UpdateTCPMonitorResponse is the response after updating a TCP monitor.\n openstatus.notification.v1.CheckNotificationLimitRequest:\n type: object\n title: CheckNotificationLimitRequest\n additionalProperties: false\n description: CheckNotificationLimitRequest is the request to check notification limits.\n openstatus.notification.v1.CheckNotificationLimitResponse:\n type: object\n properties:\n limitReached:\n type: boolean\n title: limit_reached\n description: Whether the workspace has reached its notification limit.\n currentCount:\n type: integer\n title: current_count\n format: int32\n description: Current number of notification channels.\n maxCount:\n type: integer\n title: max_count\n format: int32\n description: Maximum allowed notification channels.\n title: CheckNotificationLimitResponse\n additionalProperties: false\n description: CheckNotificationLimitResponse is the response containing limit information.\n openstatus.notification.v1.CreateNotificationRequest:\n type: object\n properties:\n name:\n type: string\n examples:\n - Slack Ops Channel\n title: name\n minLength: 1\n description: Display name for the notification channel.\n provider:\n not:\n enum:\n - NOTIFICATION_PROVIDER_UNSPECIFIED\n title: provider\n description: Provider type.\n $ref: '#/components/schemas/openstatus.notification.v1.NotificationProvider'\n data:\n title: data\n description: Provider-specific configuration.\n $ref: '#/components/schemas/openstatus.notification.v1.NotificationData'\n monitorIds:\n type: array\n items:\n type: string\n title: monitor_ids\n description: IDs of monitors to associate with this notification.\n title: CreateNotificationRequest\n required:\n - data\n additionalProperties: false\n description: CreateNotificationRequest is the request to create a new notification channel.\n openstatus.notification.v1.CreateNotificationResponse:\n type: object\n properties:\n notification:\n title: notification\n description: The created notification channel.\n $ref: '#/components/schemas/openstatus.notification.v1.Notification'\n title: CreateNotificationResponse\n additionalProperties: false\n description: CreateNotificationResponse is the response after creating a notification channel.\n openstatus.notification.v1.DeleteNotificationRequest:\n type: object\n properties:\n id:\n type: string\n title: id\n minLength: 1\n description: Notification ID to delete (required).\n title: DeleteNotificationRequest\n additionalProperties: false\n description: DeleteNotificationRequest is the request to delete a notification channel.\n openstatus.notification.v1.DeleteNotificationResponse:\n type: object\n properties:\n success:\n type: boolean\n title: success\n description: Whether the deletion was successful.\n title: DeleteNotificationResponse\n additionalProperties: false\n description: DeleteNotificationResponse is the response after deleting a notification channel.\n openstatus.notification.v1.DiscordData:\n type: object\n properties:\n webhookUrl:\n type: string\n examples:\n - https://discord.com/api/webhooks/123/abc\n title: webhook_url\n format: uri\n description: Discord webhook URL.\n title: DiscordData\n additionalProperties: false\n description: DiscordData contains configuration for Discord notifications.\n openstatus.notification.v1.EmailData:\n type: object\n properties:\n email:\n type: string\n examples:\n - ops-team@example.com\n title: email\n format: email\n description: Email address to send notifications to.\n title: EmailData\n additionalProperties: false\n description: EmailData contains configuration for email notifications.\n openstatus.notification.v1.GetNotificationRequest:\n type: object\n properties:\n id:\n type: string\n title: id\n minLength: 1\n description: Notification ID to retrieve (required).\n title: GetNotificationRequest\n additionalProperties: false\n description: GetNotificationRequest is the request to get a notification channel.\n openstatus.notification.v1.GetNotificationResponse:\n type: object\n properties:\n notification:\n title: notification\n description: The notification channel.\n $ref: '#/components/schemas/openstatus.notification.v1.Notification'\n title: GetNotificationResponse\n additionalProperties: false\n description: GetNotificationResponse is the response containing the notification channel.\n openstatus.notification.v1.GoogleChatData:\n type: object\n properties:\n webhookUrl:\n type: string\n title: webhook_url\n format: uri\n description: Google Chat webhook URL.\n title: GoogleChatData\n additionalProperties: false\n description: GoogleChatData contains configuration for Google Chat notifications.\n openstatus.notification.v1.GrafanaOncallData:\n type: object\n properties:\n webhookUrl:\n type: string\n title: webhook_url\n format: uri\n description: Grafana OnCall webhook URL.\n title: GrafanaOncallData\n additionalProperties: false\n description: GrafanaOncallData contains configuration for Grafana OnCall notifications.\n openstatus.notification.v1.ListNotificationsRequest:\n type: object\n properties:\n limit:\n type:\n - integer\n - \"null\"\n title: limit\n maximum: 100\n minimum: 1\n format: int32\n description: Maximum number of notifications to return (1-100, defaults to 50).\n offset:\n type:\n - integer\n - \"null\"\n title: offset\n minimum: 0\n format: int32\n description: Number of notifications to skip for pagination (defaults to 0).\n title: ListNotificationsRequest\n additionalProperties: false\n description: ListNotificationsRequest is the request to list notification channels.\n openstatus.notification.v1.ListNotificationsResponse:\n type: object\n properties:\n notifications:\n type: array\n items:\n $ref: '#/components/schemas/openstatus.notification.v1.NotificationSummary'\n title: notifications\n description: Notification channel summaries.\n totalSize:\n type: integer\n title: total_size\n format: int32\n description: Total number of notification channels.\n title: ListNotificationsResponse\n additionalProperties: false\n description: ListNotificationsResponse is the response containing notification channels.\n openstatus.notification.v1.MsTeamsData:\n type: object\n properties:\n webhookUrl:\n type: string\n examples:\n - https://prod-00.westeurope.logic.azure.com:443/workflows/abc/triggers/manual/paths/invoke\n title: webhook_url\n format: uri\n description: Microsoft Teams webhook URL (Power Automate Workflows).\n title: MsTeamsData\n additionalProperties: false\n description: MsTeamsData contains configuration for Microsoft Teams notifications.\n openstatus.notification.v1.Notification:\n type: object\n properties:\n id:\n type: string\n title: id\n description: Unique identifier for the notification.\n name:\n type: string\n title: name\n description: Display name for the notification channel.\n provider:\n title: provider\n description: Provider type.\n $ref: '#/components/schemas/openstatus.notification.v1.NotificationProvider'\n data:\n title: data\n description: Provider-specific configuration.\n $ref: '#/components/schemas/openstatus.notification.v1.NotificationData'\n monitorIds:\n type: array\n items:\n type: string\n title: monitor_ids\n description: IDs of monitors associated with this notification.\n createdAt:\n type: string\n title: created_at\n description: Timestamp when the notification was created (RFC 3339).\n updatedAt:\n type: string\n title: updated_at\n description: Timestamp when the notification was last updated (RFC 3339).\n title: Notification\n additionalProperties: false\n description: Notification represents a notification channel with full details.\n openstatus.notification.v1.NotificationData:\n type: object\n oneOf:\n - type: object\n properties:\n discord:\n title: discord\n description: Discord configuration.\n $ref: '#/components/schemas/openstatus.notification.v1.DiscordData'\n title: discord\n required:\n - discord\n - type: object\n properties:\n email:\n title: email\n description: Email configuration.\n $ref: '#/components/schemas/openstatus.notification.v1.EmailData'\n title: email\n required:\n - email\n - type: object\n properties:\n googleChat:\n title: google_chat\n description: Google Chat configuration.\n $ref: '#/components/schemas/openstatus.notification.v1.GoogleChatData'\n title: google_chat\n required:\n - googleChat\n - type: object\n properties:\n grafanaOncall:\n title: grafana_oncall\n description: Grafana OnCall configuration.\n $ref: '#/components/schemas/openstatus.notification.v1.GrafanaOncallData'\n title: grafana_oncall\n required:\n - grafanaOncall\n - type: object\n properties:\n msTeams:\n title: ms_teams\n description: Microsoft Teams configuration.\n $ref: '#/components/schemas/openstatus.notification.v1.MsTeamsData'\n title: ms_teams\n required:\n - msTeams\n - type: object\n properties:\n ntfy:\n title: ntfy\n description: Ntfy configuration.\n $ref: '#/components/schemas/openstatus.notification.v1.NtfyData'\n title: ntfy\n required:\n - ntfy\n - type: object\n properties:\n opsgenie:\n title: opsgenie\n description: Opsgenie configuration.\n $ref: '#/components/schemas/openstatus.notification.v1.OpsgenieData'\n title: opsgenie\n required:\n - opsgenie\n - type: object\n properties:\n pagerduty:\n title: pagerduty\n description: PagerDuty configuration.\n $ref: '#/components/schemas/openstatus.notification.v1.PagerDutyData'\n title: pagerduty\n required:\n - pagerduty\n - type: object\n properties:\n slack:\n title: slack\n description: Slack configuration.\n $ref: '#/components/schemas/openstatus.notification.v1.SlackData'\n title: slack\n required:\n - slack\n - type: object\n properties:\n sms:\n title: sms\n description: 'Deprecated: SMS is no longer offered; use whatsapp.'\n deprecated: true\n $ref: '#/components/schemas/openstatus.notification.v1.SmsData'\n title: sms\n required:\n - sms\n - type: object\n properties:\n telegram:\n title: telegram\n description: Telegram configuration.\n $ref: '#/components/schemas/openstatus.notification.v1.TelegramData'\n title: telegram\n required:\n - telegram\n - type: object\n properties:\n webhook:\n title: webhook\n description: Webhook configuration.\n $ref: '#/components/schemas/openstatus.notification.v1.WebhookData'\n title: webhook\n required:\n - webhook\n - type: object\n properties:\n whatsapp:\n title: whatsapp\n description: WhatsApp configuration.\n $ref: '#/components/schemas/openstatus.notification.v1.WhatsappData'\n title: whatsapp\n required:\n - whatsapp\n title: NotificationData\n additionalProperties: false\n description: NotificationData is a union of provider-specific configuration.\n openstatus.notification.v1.NotificationProvider:\n type: string\n title: NotificationProvider\n enum:\n - NOTIFICATION_PROVIDER_UNSPECIFIED\n - NOTIFICATION_PROVIDER_DISCORD\n - NOTIFICATION_PROVIDER_EMAIL\n - NOTIFICATION_PROVIDER_GOOGLE_CHAT\n - NOTIFICATION_PROVIDER_GRAFANA_ONCALL\n - NOTIFICATION_PROVIDER_NTFY\n - NOTIFICATION_PROVIDER_PAGERDUTY\n - NOTIFICATION_PROVIDER_OPSGENIE\n - NOTIFICATION_PROVIDER_SLACK\n - NOTIFICATION_PROVIDER_SMS\n - NOTIFICATION_PROVIDER_TELEGRAM\n - NOTIFICATION_PROVIDER_WEBHOOK\n - NOTIFICATION_PROVIDER_WHATSAPP\n - NOTIFICATION_PROVIDER_MS_TEAMS\n description: NotificationProvider represents the supported notification channel types.\n openstatus.notification.v1.NotificationSummary:\n type: object\n properties:\n id:\n type: string\n title: id\n description: Unique identifier for the notification.\n name:\n type: string\n title: name\n description: Display name for the notification channel.\n provider:\n title: provider\n description: Provider type.\n $ref: '#/components/schemas/openstatus.notification.v1.NotificationProvider'\n monitorCount:\n type: integer\n title: monitor_count\n format: int32\n description: Number of monitors associated with this notification.\n createdAt:\n type: string\n title: created_at\n description: Timestamp when the notification was created (RFC 3339).\n updatedAt:\n type: string\n title: updated_at\n description: Timestamp when the notification was last updated (RFC 3339).\n title: NotificationSummary\n additionalProperties: false\n description: NotificationSummary represents a notification channel summary for list responses.\n openstatus.notification.v1.NtfyData:\n type: object\n properties:\n topic:\n type: string\n examples:\n - openstatus-alerts\n title: topic\n minLength: 1\n description: Ntfy topic to publish to.\n serverUrl:\n type: string\n title: server_url\n description: Ntfy server URL (defaults to https://ntfy.sh).\n token:\n type:\n - string\n - \"null\"\n title: token\n description: Optional authentication token.\n title: NtfyData\n additionalProperties: false\n description: NtfyData contains configuration for Ntfy notifications.\n openstatus.notification.v1.OpsgenieData:\n type: object\n properties:\n apiKey:\n type: string\n title: api_key\n minLength: 1\n description: Opsgenie API key.\n region:\n title: region\n description: Opsgenie region.\n $ref: '#/components/schemas/openstatus.notification.v1.OpsgenieRegion'\n title: OpsgenieData\n additionalProperties: false\n description: OpsgenieData contains configuration for Opsgenie notifications.\n openstatus.notification.v1.OpsgenieRegion:\n type: string\n title: OpsgenieRegion\n enum:\n - OPSGENIE_REGION_UNSPECIFIED\n - OPSGENIE_REGION_US\n - OPSGENIE_REGION_EU\n description: OpsgenieRegion represents the Opsgenie API region.\n openstatus.notification.v1.PagerDutyData:\n type: object\n properties:\n integrationKey:\n type: string\n examples:\n - a1b2c3d4e5f6g7h8i9j0\n title: integration_key\n minLength: 1\n description: PagerDuty integration key.\n title: PagerDutyData\n additionalProperties: false\n description: PagerDutyData contains configuration for PagerDuty notifications.\n openstatus.notification.v1.SendTestNotificationRequest:\n type: object\n properties:\n provider:\n not:\n enum:\n - NOTIFICATION_PROVIDER_UNSPECIFIED\n title: provider\n description: Provider type.\n $ref: '#/components/schemas/openstatus.notification.v1.NotificationProvider'\n data:\n title: data\n description: Provider-specific configuration.\n $ref: '#/components/schemas/openstatus.notification.v1.NotificationData'\n title: SendTestNotificationRequest\n required:\n - data\n additionalProperties: false\n description: SendTestNotificationRequest is the request to send a test notification.\n openstatus.notification.v1.SendTestNotificationResponse:\n type: object\n properties:\n success:\n type: boolean\n title: success\n description: Whether the test was successful.\n errorMessage:\n type:\n - string\n - \"null\"\n title: error_message\n description: Optional error message if the test failed.\n title: SendTestNotificationResponse\n additionalProperties: false\n description: SendTestNotificationResponse is the response after sending a test notification.\n openstatus.notification.v1.SlackData:\n type: object\n properties:\n webhookUrl:\n type: string\n examples:\n - https://hooks.slack.com/services/T00/B00/xxx\n title: webhook_url\n format: uri\n description: Slack webhook URL.\n title: SlackData\n additionalProperties: false\n description: SlackData contains configuration for Slack notifications.\n openstatus.notification.v1.SmsData:\n type: object\n properties:\n phoneNumber:\n type: string\n examples:\n - \"+14155551234\"\n title: phone_number\n minLength: 1\n description: Phone number to send SMS to.\n title: SmsData\n additionalProperties: false\n description: 'Deprecated: SMS is no longer offered; use WhatsappData. Kept for existing channels.'\n deprecated: true\n openstatus.notification.v1.TelegramData:\n type: object\n properties:\n chatId:\n type: string\n examples:\n - \"-1001234567890\"\n title: chat_id\n minLength: 1\n description: Telegram chat ID.\n title: TelegramData\n additionalProperties: false\n description: TelegramData contains configuration for Telegram notifications.\n openstatus.notification.v1.UpdateNotificationRequest:\n type: object\n properties:\n id:\n type: string\n title: id\n minLength: 1\n description: Notification ID to update (required).\n name:\n type:\n - string\n - \"null\"\n title: name\n description: Updated display name.\n data:\n oneOf:\n - $ref: '#/components/schemas/openstatus.notification.v1.NotificationData'\n - type: \"null\"\n title: data\n description: Updated provider-specific configuration.\n monitorIds:\n type: array\n items:\n type: string\n title: monitor_ids\n description: Updated monitor IDs to associate.\n updateMonitorIds:\n type:\n - boolean\n - \"null\"\n title: update_monitor_ids\n description: |-\n Set to true to update monitor associations.\n When true, monitor_ids replaces the existing list (empty clears all).\n When false or unset, monitor_ids is ignored and existing associations are preserved.\n title: UpdateNotificationRequest\n additionalProperties: false\n description: UpdateNotificationRequest is the request to update a notification channel.\n openstatus.notification.v1.UpdateNotificationResponse:\n type: object\n properties:\n notification:\n title: notification\n description: The updated notification channel.\n $ref: '#/components/schemas/openstatus.notification.v1.Notification'\n title: UpdateNotificationResponse\n additionalProperties: false\n description: UpdateNotificationResponse is the response after updating a notification channel.\n openstatus.notification.v1.WebhookData:\n type: object\n properties:\n endpoint:\n type: string\n examples:\n - https://api.example.com/webhooks/openstatus\n title: endpoint\n format: uri\n description: Webhook endpoint URL.\n headers:\n type: array\n items:\n $ref: '#/components/schemas/openstatus.notification.v1.WebhookHeader'\n title: headers\n description: Optional custom headers.\n title: WebhookData\n additionalProperties: false\n description: WebhookData contains configuration for custom webhook notifications.\n openstatus.notification.v1.WebhookHeader:\n type: object\n properties:\n key:\n type: string\n title: key\n minLength: 1\n description: Header name.\n value:\n type: string\n title: value\n description: Header value.\n title: WebhookHeader\n additionalProperties: false\n description: WebhookHeader represents a custom header for webhook requests.\n openstatus.notification.v1.WhatsappData:\n type: object\n properties:\n phoneNumber:\n type: string\n title: phone_number\n minLength: 1\n description: Phone number to send WhatsApp messages to.\n title: WhatsappData\n additionalProperties: false\n description: WhatsappData contains configuration for WhatsApp notifications.\n openstatus.private_location.v1.CreatePrivateLocationRequest:\n type: object\n properties:\n name:\n type: string\n examples:\n - eu-west-agent\n title: name\n maxLength: 256\n minLength: 1\n description: Display name for the private location (required, 1-256 characters).\n monitorIds:\n type: array\n items:\n type: string\n title: monitor_ids\n description: IDs of monitors this private location should run (optional).\n metadata:\n type: object\n title: metadata\n maxProperties: 20\n additionalProperties:\n type: string\n title: value\n maxLength: 256\n description: User-defined key/value labels (optional, up to 20 entries).\n title: CreatePrivateLocationRequest\n additionalProperties: false\n description: CreatePrivateLocationRequest is the request to create a new private location.\n openstatus.private_location.v1.CreatePrivateLocationRequest.MetadataEntry:\n type: object\n properties:\n key:\n type: string\n title: key\n value:\n type: string\n title: value\n title: MetadataEntry\n additionalProperties: false\n openstatus.private_location.v1.CreatePrivateLocationResponse:\n type: object\n properties:\n privateLocation:\n title: private_location\n description: The created private location, including its generated agent token.\n $ref: '#/components/schemas/openstatus.private_location.v1.PrivateLocation'\n title: CreatePrivateLocationResponse\n additionalProperties: false\n description: CreatePrivateLocationResponse is the response after creating a private location.\n openstatus.private_location.v1.DeletePrivateLocationRequest:\n type: object\n properties:\n id:\n type: string\n title: id\n minLength: 1\n description: ID of the private location to delete (required).\n title: DeletePrivateLocationRequest\n additionalProperties: false\n description: DeletePrivateLocationRequest is the request to delete a private location.\n openstatus.private_location.v1.DeletePrivateLocationResponse:\n type: object\n properties:\n success:\n type: boolean\n title: success\n description: Whether the deletion was successful.\n title: DeletePrivateLocationResponse\n additionalProperties: false\n description: DeletePrivateLocationResponse is the response after deleting a private location.\n openstatus.private_location.v1.GetPrivateLocationRequest:\n type: object\n properties:\n id:\n type: string\n title: id\n minLength: 1\n description: ID of the private location to retrieve (required).\n title: GetPrivateLocationRequest\n additionalProperties: false\n description: GetPrivateLocationRequest is the request to get a private location by ID.\n openstatus.private_location.v1.GetPrivateLocationResponse:\n type: object\n properties:\n privateLocation:\n title: private_location\n description: The requested private location.\n $ref: '#/components/schemas/openstatus.private_location.v1.PrivateLocation'\n title: GetPrivateLocationResponse\n additionalProperties: false\n description: GetPrivateLocationResponse is the response containing the private location.\n openstatus.private_location.v1.ListPrivateLocationsRequest:\n type: object\n properties:\n limit:\n type:\n - integer\n - \"null\"\n title: limit\n maximum: 100\n minimum: 1\n format: int32\n description: Maximum number of private locations to return (1-100, defaults to 50).\n offset:\n type:\n - integer\n - \"null\"\n title: offset\n minimum: 0\n format: int32\n description: Number of private locations to skip for pagination (defaults to 0).\n title: ListPrivateLocationsRequest\n additionalProperties: false\n description: ListPrivateLocationsRequest is the request to list private locations.\n openstatus.private_location.v1.ListPrivateLocationsResponse:\n type: object\n properties:\n privateLocations:\n type: array\n items:\n $ref: '#/components/schemas/openstatus.private_location.v1.PrivateLocationSummary'\n title: private_locations\n description: List of private locations.\n totalSize:\n type: integer\n title: total_size\n format: int32\n description: Total number of private locations in the workspace.\n title: ListPrivateLocationsResponse\n additionalProperties: false\n description: ListPrivateLocationsResponse is the response containing private location summaries.\n openstatus.private_location.v1.PrivateLocation:\n type: object\n properties:\n id:\n type: string\n title: id\n description: Unique identifier for the private location.\n name:\n type: string\n title: name\n description: Display name for the private location.\n token:\n type: string\n title: token\n description: |-\n Agent credential. Generated by the server on creation and used by the\n agent to authenticate; treat it as a secret.\n monitorIds:\n type: array\n items:\n type: string\n title: monitor_ids\n description: IDs of monitors this private location runs.\n lastSeenAt:\n type: string\n title: last_seen_at\n description: Last time the agent reported a result (RFC 3339), empty if it never has.\n createdAt:\n type: string\n title: created_at\n description: Timestamp when the private location was created (RFC 3339 format).\n updatedAt:\n type: string\n title: updated_at\n description: Timestamp when the private location was last updated (RFC 3339 format).\n metadata:\n type: object\n title: metadata\n additionalProperties:\n type: string\n title: value\n description: User-defined key/value labels attached to this private location.\n status:\n title: status\n description: Computed health of the agent. Read-only.\n $ref: '#/components/schemas/openstatus.private_location.v1.PrivateLocationStatus'\n title: PrivateLocation\n additionalProperties: false\n description: PrivateLocation represents a self-hosted checker agent with full details.\n openstatus.private_location.v1.PrivateLocation.MetadataEntry:\n type: object\n properties:\n key:\n type: string\n title: key\n value:\n type: string\n title: value\n title: MetadataEntry\n additionalProperties: false\n openstatus.private_location.v1.PrivateLocationStatus:\n type: string\n title: PrivateLocationStatus\n enum:\n - PRIVATE_LOCATION_STATUS_UNSPECIFIED\n - PRIVATE_LOCATION_STATUS_ACTIVE\n - PRIVATE_LOCATION_STATUS_ERROR\n description: |-\n PrivateLocationStatus is the computed health of a private location, derived\n from the agent heartbeat. Read-only — it cannot be set through the API.\n openstatus.private_location.v1.PrivateLocationSummary:\n type: object\n properties:\n id:\n type: string\n title: id\n description: Unique identifier for the private location.\n name:\n type: string\n title: name\n description: Display name for the private location.\n monitorCount:\n type: integer\n title: monitor_count\n format: int32\n description: Number of monitors this private location runs.\n lastSeenAt:\n type: string\n title: last_seen_at\n description: Last time the agent reported a result (RFC 3339), empty if it never has.\n createdAt:\n type: string\n title: created_at\n description: Timestamp when the private location was created (RFC 3339 format).\n updatedAt:\n type: string\n title: updated_at\n description: Timestamp when the private location was last updated (RFC 3339 format).\n status:\n title: status\n description: Computed health of the agent. Read-only.\n $ref: '#/components/schemas/openstatus.private_location.v1.PrivateLocationStatus'\n metadata:\n type: object\n title: metadata\n additionalProperties:\n type: string\n title: value\n description: User-defined key/value labels attached to this private location.\n title: PrivateLocationSummary\n additionalProperties: false\n description: |-\n PrivateLocationSummary represents metadata for a private location (used in\n list responses). The agent token is intentionally omitted.\n openstatus.private_location.v1.PrivateLocationSummary.MetadataEntry:\n type: object\n properties:\n key:\n type: string\n title: key\n value:\n type: string\n title: value\n title: MetadataEntry\n additionalProperties: false\n openstatus.private_location.v1.UpdatePrivateLocationRequest:\n type: object\n properties:\n id:\n type: string\n title: id\n minLength: 1\n description: ID of the private location to update (required).\n name:\n type:\n - string\n - \"null\"\n title: name\n maxLength: 256\n minLength: 1\n description: New display name for the private location (optional).\n monitorIds:\n type: array\n items:\n type: string\n title: monitor_ids\n description: New list of monitor IDs. Only applied when update_monitor_ids is true.\n updateMonitorIds:\n type:\n - boolean\n - \"null\"\n title: update_monitor_ids\n description: |-\n When true, monitor_ids replaces the current associations (an empty list\n clears all). When false or unset, monitor_ids is ignored and existing\n associations are preserved.\n metadata:\n type: object\n title: metadata\n maxProperties: 20\n additionalProperties:\n type: string\n title: value\n maxLength: 256\n description: New key/value labels. Only applied when update_metadata is true.\n updateMetadata:\n type:\n - boolean\n - \"null\"\n title: update_metadata\n description: |-\n When true, metadata replaces the current labels (an empty map clears all).\n When false or unset, metadata is ignored and existing labels are preserved.\n title: UpdatePrivateLocationRequest\n additionalProperties: false\n description: UpdatePrivateLocationRequest is the request to update a private location.\n openstatus.private_location.v1.UpdatePrivateLocationRequest.MetadataEntry:\n type: object\n properties:\n key:\n type: string\n title: key\n value:\n type: string\n title: value\n title: MetadataEntry\n additionalProperties: false\n openstatus.private_location.v1.UpdatePrivateLocationResponse:\n type: object\n properties:\n privateLocation:\n title: private_location\n description: The updated private location.\n $ref: '#/components/schemas/openstatus.private_location.v1.PrivateLocation'\n title: UpdatePrivateLocationResponse\n additionalProperties: false\n description: UpdatePrivateLocationResponse is the response after updating a private location.\n openstatus.status_page.v1.AddMonitorComponentRequest:\n type: object\n properties:\n pageId:\n type: string\n title: page_id\n minLength: 1\n description: ID of the status page to add the component to (required).\n monitorId:\n type: string\n title: monitor_id\n minLength: 1\n description: ID of the monitor to associate with this component (required).\n name:\n type:\n - string\n - \"null\"\n title: name\n maxLength: 256\n description: Display name for the component (optional, defaults to monitor name).\n description:\n type:\n - string\n - \"null\"\n title: description\n maxLength: 1024\n description: Description of the component (optional).\n order:\n type:\n - integer\n - \"null\"\n title: order\n format: int32\n description: Display order of the component (optional).\n groupId:\n type:\n - string\n - \"null\"\n title: group_id\n description: ID of the group to add this component to (optional).\n title: AddMonitorComponentRequest\n additionalProperties: false\n description: AddMonitorComponentRequest is the request to add a monitor-based component to a status page.\n openstatus.status_page.v1.AddMonitorComponentResponse:\n type: object\n properties:\n component:\n title: component\n description: The created component.\n $ref: '#/components/schemas/openstatus.status_page.v1.PageComponent'\n title: AddMonitorComponentResponse\n additionalProperties: false\n description: AddMonitorComponentResponse is the response after adding a monitor component.\n openstatus.status_page.v1.AddStaticComponentRequest:\n type: object\n properties:\n pageId:\n type: string\n title: page_id\n minLength: 1\n description: ID of the status page to add the component to (required).\n name:\n type: string\n title: name\n maxLength: 256\n minLength: 1\n description: Display name for the component (required).\n description:\n type:\n - string\n - \"null\"\n title: description\n maxLength: 1024\n description: Description of the component (optional).\n order:\n type:\n - integer\n - \"null\"\n title: order\n format: int32\n description: Display order of the component (optional).\n groupId:\n type:\n - string\n - \"null\"\n title: group_id\n description: ID of the group to add this component to (optional).\n title: AddStaticComponentRequest\n additionalProperties: false\n description: AddStaticComponentRequest is the request to add a static component to a status page.\n openstatus.status_page.v1.AddStaticComponentResponse:\n type: object\n properties:\n component:\n title: component\n description: The created component.\n $ref: '#/components/schemas/openstatus.status_page.v1.PageComponent'\n title: AddStaticComponentResponse\n additionalProperties: false\n description: AddStaticComponentResponse is the response after adding a static component.\n openstatus.status_page.v1.ComponentDayBucket:\n type: object\n properties:\n day:\n type: string\n title: day\n description: Day in RFC 3339 format (UTC midnight).\n count:\n type:\n - integer\n - string\n title: count\n format: int64\n description: Total checks that day.\n ok:\n type:\n - integer\n - string\n title: ok\n format: int64\n description: Successful checks.\n degraded:\n type:\n - integer\n - string\n title: degraded\n format: int64\n description: Degraded checks.\n error:\n type:\n - integer\n - string\n title: error\n format: int64\n description: Failed checks.\n status:\n title: status\n description: Convenience resolved status for the day.\n $ref: '#/components/schemas/openstatus.status_page.v1.ComponentDayStatus'\n impact:\n oneOf:\n - $ref: '#/components/schemas/openstatus.status_report.v1.PageComponentImpact'\n - type: \"null\"\n title: impact\n description: Worst status-report impact overlapping the day (absent when none).\n title: ComponentDayBucket\n additionalProperties: false\n description: ComponentDayBucket is one day of status data for a component.\n openstatus.status_page.v1.ComponentDayStatus:\n type: string\n title: ComponentDayStatus\n enum:\n - COMPONENT_DAY_STATUS_UNSPECIFIED\n - COMPONENT_DAY_STATUS_OPERATIONAL\n - COMPONENT_DAY_STATUS_DEGRADED\n - COMPONENT_DAY_STATUS_DOWN\n - COMPONENT_DAY_STATUS_MAINTENANCE\n - COMPONENT_DAY_STATUS_EMPTY\n description: ComponentDayStatus is the resolved status of a component on a given day.\n openstatus.status_page.v1.ComponentEvent:\n type: object\n properties:\n id:\n type: string\n title: id\n description: Identifier of the underlying event.\n name:\n type: string\n title: name\n description: Human-readable name (incident \"Downtime\", maintenance / report title).\n type:\n title: type\n description: Kind of event.\n $ref: '#/components/schemas/openstatus.status_page.v1.ComponentEventType'\n status:\n title: status\n description: Projected status this event contributes.\n $ref: '#/components/schemas/openstatus.status_page.v1.ComponentEventStatus'\n from:\n type: string\n title: from\n description: Start time (RFC 3339).\n to:\n type:\n - string\n - \"null\"\n title: to\n description: End time (RFC 3339); absent while the event is ongoing.\n impact:\n oneOf:\n - $ref: '#/components/schemas/openstatus.status_report.v1.PageComponentImpact'\n - type: \"null\"\n title: impact\n description: Worst impact over the event (reports only; absent otherwise).\n title: ComponentEvent\n additionalProperties: false\n description: ComponentEvent is a single incident / maintenance / report affecting a component.\n openstatus.status_page.v1.ComponentEventStatus:\n type: string\n title: ComponentEventStatus\n enum:\n - COMPONENT_EVENT_STATUS_UNSPECIFIED\n - COMPONENT_EVENT_STATUS_OPERATIONAL\n - COMPONENT_EVENT_STATUS_DEGRADED\n - COMPONENT_EVENT_STATUS_DOWN\n - COMPONENT_EVENT_STATUS_MAINTENANCE\n description: ComponentEventStatus is the projected status an event contributes.\n openstatus.status_page.v1.ComponentEventType:\n type: string\n title: ComponentEventType\n enum:\n - COMPONENT_EVENT_TYPE_UNSPECIFIED\n - COMPONENT_EVENT_TYPE_MAINTENANCE\n - COMPONENT_EVENT_TYPE_INCIDENT\n - COMPONENT_EVENT_TYPE_REPORT\n description: ComponentEventType is the kind of timeline event affecting a component.\n openstatus.status_page.v1.ComponentStatus:\n type: object\n properties:\n componentId:\n type: string\n title: component_id\n description: ID of the component.\n status:\n title: status\n description: Current status of the component.\n $ref: '#/components/schemas/openstatus.status_page.v1.OverallStatus'\n title: ComponentStatus\n additionalProperties: false\n description: ComponentStatus represents the status of a single component.\n openstatus.status_page.v1.CreateComponentGroupRequest:\n type: object\n properties:\n pageId:\n type: string\n title: page_id\n minLength: 1\n description: ID of the status page to create the group in (required).\n name:\n type: string\n title: name\n maxLength: 256\n minLength: 1\n description: Display name for the group (required).\n defaultOpen:\n type:\n - boolean\n - \"null\"\n title: default_open\n description: Whether the group should be expanded by default on the status page (optional, defaults to false).\n title: CreateComponentGroupRequest\n additionalProperties: false\n description: CreateComponentGroupRequest is the request to create a new component group.\n openstatus.status_page.v1.CreateComponentGroupResponse:\n type: object\n properties:\n group:\n title: group\n description: The created component group.\n $ref: '#/components/schemas/openstatus.status_page.v1.PageComponentGroup'\n title: CreateComponentGroupResponse\n additionalProperties: false\n description: CreateComponentGroupResponse is the response after creating a component group.\n openstatus.status_page.v1.CreatePageSubscriptionRequest:\n type: object\n allOf:\n - properties:\n pageId:\n type: string\n title: page_id\n minLength: 1\n description: ID of the status page (required).\n name:\n type:\n - string\n - \"null\"\n title: name\n maxLength: 255\n description: Optional human-readable label.\n componentIds:\n type: array\n items:\n type: string\n title: component_ids\n description: Component scope. Empty = entire page.\n - oneOf:\n - type: object\n properties:\n emailChannel:\n title: email_channel\n $ref: '#/components/schemas/openstatus.status_page.v1.EmailChannel'\n title: email_channel\n required:\n - emailChannel\n - type: object\n properties:\n webhookChannel:\n title: webhook_channel\n $ref: '#/components/schemas/openstatus.status_page.v1.WebhookChannel'\n title: webhook_channel\n required:\n - webhookChannel\n title: CreatePageSubscriptionRequest\n additionalProperties: false\n description: CreatePageSubscriptionRequest is the request to add a vendor-managed subscriber to a status page.\n openstatus.status_page.v1.CreatePageSubscriptionResponse:\n type: object\n properties:\n subscriber:\n title: subscriber\n description: The created subscriber.\n $ref: '#/components/schemas/openstatus.status_page.v1.PageSubscriber'\n title: CreatePageSubscriptionResponse\n additionalProperties: false\n description: CreatePageSubscriptionResponse is the response after creating a vendor-managed subscription.\n openstatus.status_page.v1.CreateStatusPageRequest:\n type: object\n properties:\n title:\n type: string\n examples:\n - Acme Corp Status\n title: title\n maxLength: 256\n minLength: 1\n description: Title of the status page (required).\n description:\n type:\n - string\n - \"null\"\n title: description\n maxLength: 1024\n description: Description of the status page (optional).\n slug:\n type: string\n examples:\n - my-status-page\n title: slug\n maxLength: 256\n minLength: 1\n pattern: ^[a-z0-9]+(?:-[a-z0-9]+)*$\n description: URL-friendly slug for the status page (required). Must be lowercase alphanumeric with hyphens.\n homepageUrl:\n type:\n - string\n - \"null\"\n examples:\n - https://www.example.com\n title: homepage_url\n description: URL to the homepage (optional).\n contactUrl:\n type:\n - string\n - \"null\"\n title: contact_url\n description: URL to the contact page (optional).\n defaultLocale:\n oneOf:\n - $ref: '#/components/schemas/openstatus.status_page.v1.Locale'\n - type: \"null\"\n title: default_locale\n description: Default locale for the status page (optional, defaults to EN).\n locales:\n type: array\n items:\n $ref: '#/components/schemas/openstatus.status_page.v1.Locale'\n title: locales\n description: Enabled locales for the status page (optional).\n icon:\n type:\n - string\n - \"null\"\n title: icon\n maxLength: 1024\n description: Icon URL for the status page (optional).\n customDomain:\n type:\n - string\n - \"null\"\n title: custom_domain\n maxLength: 256\n description: Custom domain for the status page (optional).\n theme:\n oneOf:\n - $ref: '#/components/schemas/openstatus.status_page.v1.PageTheme'\n - type: \"null\"\n title: theme\n description: Visual theme for the status page (optional, defaults to SYSTEM).\n accessType:\n oneOf:\n - $ref: '#/components/schemas/openstatus.status_page.v1.PageAccessType'\n - type: \"null\"\n title: access_type\n description: Access type for the status page (optional, defaults to PUBLIC).\n password:\n type:\n - string\n - \"null\"\n title: password\n maxLength: 256\n minLength: 1\n description: Password for the status page (required when access_type is PASSWORD_PROTECTED).\n authEmailDomains:\n type: array\n items:\n type: string\n title: auth_email_domains\n description: Email domains allowed to access the page (used when access_type is AUTHENTICATED).\n allowIndex:\n type:\n - boolean\n - \"null\"\n title: allow_index\n description: Whether search engines are allowed to index this status page (optional, defaults to true).\n allowedIpRanges:\n type:\n - string\n - \"null\"\n title: allowed_ip_ranges\n description: Comma-separated IPv4 CIDR ranges (required when access_type is IP_RESTRICTED).\n customTheme:\n oneOf:\n - $ref: '#/components/schemas/openstatus.status_page.v1.CustomTheme'\n - type: \"null\"\n title: custom_theme\n description: |-\n Per-mode CSS variable overrides merged over the theme (optional).\n Only supported variable names are accepted. Requires the custom-theme plan feature.\n title: CreateStatusPageRequest\n additionalProperties: false\n description: CreateStatusPageRequest is the request to create a new status page.\n openstatus.status_page.v1.CreateStatusPageResponse:\n type: object\n properties:\n statusPage:\n title: status_page\n description: The created status page.\n $ref: '#/components/schemas/openstatus.status_page.v1.StatusPage'\n title: CreateStatusPageResponse\n additionalProperties: false\n description: CreateStatusPageResponse is the response after creating a status page.\n openstatus.status_page.v1.CustomTheme:\n type: object\n properties:\n light:\n type: object\n title: light\n additionalProperties:\n type: string\n title: value\n description: 'CSS variable overrides applied in light mode, keyed by variable name (e.g. \"--primary\": \"hsl(24 94% 50%)\").'\n dark:\n type: object\n title: dark\n additionalProperties:\n type: string\n title: value\n description: CSS variable overrides applied in dark mode, keyed by variable name.\n title: CustomTheme\n additionalProperties: false\n description: CustomTheme holds per-mode CSS variable overrides merged over the page theme.\n openstatus.status_page.v1.CustomTheme.DarkEntry:\n type: object\n properties:\n key:\n type: string\n title: key\n value:\n type: string\n title: value\n title: DarkEntry\n additionalProperties: false\n openstatus.status_page.v1.CustomTheme.LightEntry:\n type: object\n properties:\n key:\n type: string\n title: key\n value:\n type: string\n title: value\n title: LightEntry\n additionalProperties: false\n openstatus.status_page.v1.DeleteComponentGroupRequest:\n type: object\n properties:\n id:\n type: string\n title: id\n minLength: 1\n description: ID of the component group to delete (required).\n title: DeleteComponentGroupRequest\n additionalProperties: false\n description: DeleteComponentGroupRequest is the request to delete a component group.\n openstatus.status_page.v1.DeleteComponentGroupResponse:\n type: object\n properties:\n success:\n type: boolean\n title: success\n description: Whether the deletion was successful.\n title: DeleteComponentGroupResponse\n additionalProperties: false\n description: DeleteComponentGroupResponse is the response after deleting a component group.\n openstatus.status_page.v1.DeleteStatusPageRequest:\n type: object\n properties:\n id:\n type: string\n title: id\n minLength: 1\n description: ID of the status page to delete (required).\n title: DeleteStatusPageRequest\n additionalProperties: false\n description: DeleteStatusPageRequest is the request to delete a status page.\n openstatus.status_page.v1.DeleteStatusPageResponse:\n type: object\n properties:\n success:\n type: boolean\n title: success\n description: Whether the deletion was successful.\n title: DeleteStatusPageResponse\n additionalProperties: false\n description: DeleteStatusPageResponse is the response after deleting a status page.\n openstatus.status_page.v1.EmailChannel:\n type: object\n properties:\n email:\n type: string\n title: email\n format: email\n description: Email address of the subscriber (required).\n title: EmailChannel\n additionalProperties: false\n description: EmailChannel carries the email-channel fields for CreatePageSubscription.\n openstatus.status_page.v1.GetOverallStatusRequest:\n type: object\n oneOf:\n - type: object\n properties:\n id:\n type: string\n title: id\n description: ID of the status page.\n title: id\n required:\n - id\n - type: object\n properties:\n slug:\n type: string\n title: slug\n description: Slug of the status page.\n title: slug\n required:\n - slug\n title: GetOverallStatusRequest\n additionalProperties: false\n description: GetOverallStatusRequest is the request to get the overall status of a status page.\n openstatus.status_page.v1.GetOverallStatusResponse:\n type: object\n properties:\n overallStatus:\n title: overall_status\n description: Aggregated status across all components.\n $ref: '#/components/schemas/openstatus.status_page.v1.OverallStatus'\n componentStatuses:\n type: array\n items:\n $ref: '#/components/schemas/openstatus.status_page.v1.ComponentStatus'\n title: component_statuses\n description: Status of individual components.\n title: GetOverallStatusResponse\n additionalProperties: false\n description: GetOverallStatusResponse is the response containing the overall status and individual component statuses.\n openstatus.status_page.v1.GetPageComponentDailySummaryRequest:\n type: object\n allOf:\n - properties:\n componentIds:\n type: array\n items:\n type: string\n minLength: 1\n title: component_ids\n description: Restrict the result to these component ids (optional, defaults to all components).\n days:\n type:\n - integer\n - \"null\"\n title: days\n maximum: 45\n minimum: 1\n format: int32\n description: Number of days to return (1-45, defaults to 45 if unspecified).\n - oneOf:\n - type: object\n properties:\n id:\n type: string\n title: id\n description: ID of the status page.\n title: id\n required:\n - id\n - type: object\n properties:\n slug:\n type: string\n title: slug\n description: Slug of the status page.\n title: slug\n required:\n - slug\n title: GetPageComponentDailySummaryRequest\n additionalProperties: false\n description: GetPageComponentDailySummaryRequest is the request for per-component daily summaries.\n openstatus.status_page.v1.GetPageComponentDailySummaryResponse:\n type: object\n properties:\n components:\n type: array\n items:\n $ref: '#/components/schemas/openstatus.status_page.v1.PageComponentDailySummary'\n title: components\n description: Per-component daily summaries, in page order.\n title: GetPageComponentDailySummaryResponse\n additionalProperties: false\n description: GetPageComponentDailySummaryResponse is the response containing per-component summaries.\n openstatus.status_page.v1.GetPageComponentRequest:\n type: object\n properties:\n id:\n type: string\n title: id\n minLength: 1\n description: ID of the component to retrieve (required).\n title: GetPageComponentRequest\n additionalProperties: false\n description: GetPageComponentRequest is the request to fetch a single component by ID.\n openstatus.status_page.v1.GetPageComponentResponse:\n type: object\n properties:\n component:\n title: component\n description: The requested component.\n $ref: '#/components/schemas/openstatus.status_page.v1.PageComponent'\n title: GetPageComponentResponse\n additionalProperties: false\n description: GetPageComponentResponse is the response containing the component.\n openstatus.status_page.v1.GetStatusPageContentRequest:\n type: object\n oneOf:\n - type: object\n properties:\n id:\n type: string\n title: id\n description: ID of the status page.\n title: id\n required:\n - id\n - type: object\n properties:\n slug:\n type: string\n title: slug\n description: Slug of the status page.\n title: slug\n required:\n - slug\n title: GetStatusPageContentRequest\n additionalProperties: false\n description: GetStatusPageContentRequest is the request to get the full content of a status page.\n openstatus.status_page.v1.GetStatusPageContentResponse:\n type: object\n properties:\n statusPage:\n title: status_page\n description: The status page details.\n $ref: '#/components/schemas/openstatus.status_page.v1.StatusPage'\n components:\n type: array\n items:\n $ref: '#/components/schemas/openstatus.status_page.v1.PageComponent'\n title: components\n description: Components on the status page.\n groups:\n type: array\n items:\n $ref: '#/components/schemas/openstatus.status_page.v1.PageComponentGroup'\n title: groups\n description: Component groups on the status page.\n statusReports:\n type: array\n items:\n $ref: '#/components/schemas/openstatus.status_report.v1.StatusReport'\n title: status_reports\n description: Active and recent status reports.\n maintenances:\n type: array\n items:\n $ref: '#/components/schemas/openstatus.maintenance.v1.MaintenanceSummary'\n title: maintenances\n description: Scheduled maintenances.\n title: GetStatusPageContentResponse\n additionalProperties: false\n description: GetStatusPageContentResponse is the response containing the full status page content.\n openstatus.status_page.v1.GetStatusPageOverviewRequest:\n type: object\n properties:\n id:\n type: string\n title: id\n minLength: 1\n description: ID of the status page (required, workspace-scoped).\n title: GetStatusPageOverviewRequest\n additionalProperties: false\n description: GetStatusPageOverviewRequest requests the full overview of a status page by id.\n openstatus.status_page.v1.GetStatusPageOverviewResponse:\n type: object\n properties:\n statusPage:\n title: status_page\n description: The status page details.\n $ref: '#/components/schemas/openstatus.status_page.v1.StatusPage'\n configuration:\n title: configuration\n description: Rich rendering configuration of the page.\n $ref: '#/components/schemas/openstatus.status_page.v1.PageConfiguration'\n components:\n type: array\n items:\n $ref: '#/components/schemas/openstatus.status_page.v1.PageComponent'\n title: components\n description: Components on the status page.\n groups:\n type: array\n items:\n $ref: '#/components/schemas/openstatus.status_page.v1.PageComponentGroup'\n title: groups\n description: Component groups on the status page.\n statusReports:\n type: array\n items:\n $ref: '#/components/schemas/openstatus.status_report.v1.StatusReport'\n title: status_reports\n description: Active and recent status reports.\n maintenances:\n type: array\n items:\n $ref: '#/components/schemas/openstatus.maintenance.v1.MaintenanceSummary'\n title: maintenances\n description: Scheduled maintenances.\n overallStatus:\n title: overall_status\n description: Aggregated status across all components.\n $ref: '#/components/schemas/openstatus.status_page.v1.OverallStatus'\n componentStatuses:\n type: array\n items:\n $ref: '#/components/schemas/openstatus.status_page.v1.ComponentStatus'\n title: component_statuses\n description: Status of individual components.\n title: GetStatusPageOverviewResponse\n additionalProperties: false\n description: GetStatusPageOverviewResponse bundles all data for a single status page.\n openstatus.status_page.v1.GetStatusPageRequest:\n type: object\n properties:\n id:\n type: string\n title: id\n minLength: 1\n description: ID of the status page to retrieve (required).\n title: GetStatusPageRequest\n additionalProperties: false\n description: GetStatusPageRequest is the request to get a status page by ID.\n openstatus.status_page.v1.GetStatusPageResponse:\n type: object\n properties:\n statusPage:\n title: status_page\n description: The requested status page.\n $ref: '#/components/schemas/openstatus.status_page.v1.StatusPage'\n title: GetStatusPageResponse\n additionalProperties: false\n description: GetStatusPageResponse is the response containing the status page.\n openstatus.status_page.v1.ListStatusPagesRequest:\n type: object\n properties:\n limit:\n type:\n - integer\n - \"null\"\n title: limit\n maximum: 100\n minimum: 1\n format: int32\n description: Maximum number of pages to return (1-100, defaults to 50).\n offset:\n type:\n - integer\n - \"null\"\n title: offset\n minimum: 0\n format: int32\n description: Number of pages to skip for pagination (defaults to 0).\n title: ListStatusPagesRequest\n additionalProperties: false\n description: ListStatusPagesRequest is the request to list status pages.\n openstatus.status_page.v1.ListStatusPagesResponse:\n type: object\n properties:\n statusPages:\n type: array\n items:\n $ref: '#/components/schemas/openstatus.status_page.v1.StatusPageSummary'\n title: status_pages\n description: List of status pages (metadata only).\n totalSize:\n type: integer\n title: total_size\n format: int32\n description: Total number of status pages.\n title: ListStatusPagesResponse\n additionalProperties: false\n description: ListStatusPagesResponse is the response containing status page summaries.\n openstatus.status_page.v1.ListSubscribersRequest:\n type: object\n properties:\n pageId:\n type: string\n title: page_id\n minLength: 1\n description: ID of the status page to list subscribers for (required).\n limit:\n type:\n - integer\n - \"null\"\n title: limit\n maximum: 100\n minimum: 1\n format: int32\n description: Maximum number of subscribers to return (1-100, defaults to 50).\n offset:\n type:\n - integer\n - \"null\"\n title: offset\n minimum: 0\n format: int32\n description: Number of subscribers to skip for pagination (defaults to 0).\n includeUnsubscribed:\n type:\n - boolean\n - \"null\"\n title: include_unsubscribed\n description: Whether to include unsubscribed users (defaults to false).\n title: ListSubscribersRequest\n additionalProperties: false\n description: ListSubscribersRequest is the request to list subscribers of a status page.\n openstatus.status_page.v1.ListSubscribersResponse:\n type: object\n properties:\n subscribers:\n type: array\n items:\n $ref: '#/components/schemas/openstatus.status_page.v1.PageSubscriber'\n title: subscribers\n description: List of subscribers.\n totalSize:\n type: integer\n title: total_size\n format: int32\n description: Total number of subscribers matching the filter.\n title: ListSubscribersResponse\n additionalProperties: false\n description: ListSubscribersResponse is the response containing status page subscribers.\n openstatus.status_page.v1.Locale:\n type: string\n title: Locale\n enum:\n - LOCALE_UNSPECIFIED\n - LOCALE_EN\n - LOCALE_FR\n - LOCALE_DE\n - LOCALE_TR\n - LOCALE_HI\n - LOCALE_KO\n - LOCALE_JA\n description: Locale defines the supported languages for a status page.\n openstatus.status_page.v1.OverallStatus:\n type: string\n title: OverallStatus\n enum:\n - OVERALL_STATUS_UNSPECIFIED\n - OVERALL_STATUS_OPERATIONAL\n - OVERALL_STATUS_DEGRADED\n - OVERALL_STATUS_PARTIAL_OUTAGE\n - OVERALL_STATUS_MAJOR_OUTAGE\n - OVERALL_STATUS_MAINTENANCE\n - OVERALL_STATUS_UNKNOWN\n description: OverallStatus represents the aggregated status of all components on a page.\n openstatus.status_page.v1.PageAccessType:\n type: string\n title: PageAccessType\n enum:\n - PAGE_ACCESS_TYPE_UNSPECIFIED\n - PAGE_ACCESS_TYPE_PUBLIC\n - PAGE_ACCESS_TYPE_PASSWORD_PROTECTED\n - PAGE_ACCESS_TYPE_AUTHENTICATED\n - PAGE_ACCESS_TYPE_IP_RESTRICTED\n description: PageAccessType defines who can access the status page.\n openstatus.status_page.v1.PageBarType:\n type: string\n title: PageBarType\n enum:\n - PAGE_BAR_TYPE_UNSPECIFIED\n - PAGE_BAR_TYPE_ABSOLUTE\n - PAGE_BAR_TYPE_MANUAL\n description: PageBarType mirrors page.configuration.type (how the status bar is computed).\n openstatus.status_page.v1.PageComponent:\n type: object\n properties:\n id:\n type: string\n title: id\n description: Unique identifier for the component.\n pageId:\n type: string\n title: page_id\n description: ID of the status page this component belongs to.\n name:\n type: string\n title: name\n description: Display name of the component.\n description:\n type: string\n title: description\n description: Description of the component (optional).\n type:\n title: type\n description: Type of the component (monitor or static).\n $ref: '#/components/schemas/openstatus.status_page.v1.PageComponentType'\n monitorId:\n type: string\n title: monitor_id\n description: ID of the monitor if type is MONITOR (optional).\n order:\n type: integer\n title: order\n format: int32\n description: Display order of the component.\n groupId:\n type: string\n title: group_id\n description: ID of the group this component belongs to (optional).\n groupOrder:\n type: integer\n title: group_order\n format: int32\n description: Order within the group if grouped.\n createdAt:\n type: string\n title: created_at\n description: Timestamp when the component was created (RFC 3339 format).\n updatedAt:\n type: string\n title: updated_at\n description: Timestamp when the component was last updated (RFC 3339 format).\n title: PageComponent\n additionalProperties: false\n description: PageComponent represents a component displayed on a status page.\n openstatus.status_page.v1.PageComponentDailySummary:\n type: object\n properties:\n componentId:\n type: string\n title: component_id\n description: ID of the component.\n type:\n title: type\n description: Component type (monitor or static).\n $ref: '#/components/schemas/openstatus.status_page.v1.PageComponentType'\n monitorId:\n type:\n - string\n - \"null\"\n title: monitor_id\n description: Monitor id backing the component (absent for static components).\n name:\n type: string\n title: name\n description: Display name of the component.\n buckets:\n type: array\n items:\n $ref: '#/components/schemas/openstatus.status_page.v1.ComponentDayBucket'\n title: buckets\n description: Per-day buckets, oldest first.\n events:\n type: array\n items:\n $ref: '#/components/schemas/openstatus.status_page.v1.ComponentEvent'\n title: events\n description: Events affecting the component within the window.\n title: PageComponentDailySummary\n additionalProperties: false\n description: PageComponentDailySummary is the per-day buckets + event timeline for one component.\n openstatus.status_page.v1.PageComponentGroup:\n type: object\n properties:\n id:\n type: string\n title: id\n description: Unique identifier for the group.\n pageId:\n type: string\n title: page_id\n description: ID of the status page this group belongs to.\n name:\n type: string\n title: name\n description: Display name of the group.\n createdAt:\n type: string\n title: created_at\n description: Timestamp when the group was created (RFC 3339 format).\n updatedAt:\n type: string\n title: updated_at\n description: Timestamp when the group was last updated (RFC 3339 format).\n defaultOpen:\n type: boolean\n title: default_open\n description: Whether the group should be expanded by default on the status page.\n title: PageComponentGroup\n additionalProperties: false\n description: PageComponentGroup represents a group of components on a status page.\n openstatus.status_page.v1.PageComponentType:\n type: string\n title: PageComponentType\n enum:\n - PAGE_COMPONENT_TYPE_UNSPECIFIED\n - PAGE_COMPONENT_TYPE_MONITOR\n - PAGE_COMPONENT_TYPE_STATIC\n description: PageComponentType defines the type of a component on a status page.\n openstatus.status_page.v1.PageConfiguration:\n type: object\n properties:\n metricType:\n title: metric_type\n description: Which metric the status bars represent (configuration.value).\n $ref: '#/components/schemas/openstatus.status_page.v1.PageMetricType'\n barType:\n title: bar_type\n description: How the status bar is computed (configuration.type).\n $ref: '#/components/schemas/openstatus.status_page.v1.PageBarType'\n showUptime:\n type: boolean\n title: show_uptime\n description: Whether to show the uptime percentage (configuration.uptime).\n themeKey:\n type: string\n title: theme_key\n description: |-\n Theme key from the theme store (configuration.theme), e.g. \"default\".\n Free-form string rather than an enum because the theme catalog is dynamic.\n days:\n type: integer\n title: days\n format: int32\n description: 'Number of uptime-bar days rendered on the page (configuration.days): 30 or 45.'\n title: PageConfiguration\n additionalProperties: false\n description: PageConfiguration is the rich rendering config stored in page.configuration.\n openstatus.status_page.v1.PageMetricType:\n type: string\n title: PageMetricType\n enum:\n - PAGE_METRIC_TYPE_UNSPECIFIED\n - PAGE_METRIC_TYPE_DURATION\n - PAGE_METRIC_TYPE_REQUESTS\n - PAGE_METRIC_TYPE_MANUAL\n description: PageMetricType mirrors page.configuration.value (which metric the status bars represent).\n openstatus.status_page.v1.PageSubscriber:\n type: object\n properties:\n id:\n type: string\n title: id\n description: Unique identifier for the subscriber.\n pageId:\n type: string\n title: page_id\n description: ID of the status page the user is subscribed to.\n email:\n type: string\n title: email\n description: Email address of the subscriber (empty for webhook channels).\n acceptedAt:\n type: string\n title: accepted_at\n description: Timestamp when the subscription was accepted/confirmed (RFC 3339 format, optional).\n unsubscribedAt:\n type: string\n title: unsubscribed_at\n description: Timestamp when the user unsubscribed (RFC 3339 format, optional).\n createdAt:\n type: string\n title: created_at\n description: Timestamp when the subscription was created (RFC 3339 format).\n updatedAt:\n type: string\n title: updated_at\n description: Timestamp when the subscription was last updated (RFC 3339 format).\n source:\n title: source\n description: How the subscription was created. Vendor-added rows skip verification.\n $ref: '#/components/schemas/openstatus.status_page.v1.SubscriberSource'\n name:\n type:\n - string\n - \"null\"\n title: name\n description: 'Optional human-readable label (e.g. \"Supabase #incidents\").'\n channelType:\n type: string\n title: channel_type\n description: 'Channel type: \"email\" or \"webhook\".'\n webhookUrl:\n type:\n - string\n - \"null\"\n title: webhook_url\n description: Webhook URL (populated only for channel_type = \"webhook\").\n channelConfig:\n type:\n - string\n - \"null\"\n title: channel_config\n description: JSON-encoded channel config (e.g. custom headers). Populated for webhook channels.\n componentIds:\n type: array\n items:\n type: string\n title: component_ids\n description: IDs of components this subscription is scoped to. Empty = entire page.\n title: PageSubscriber\n additionalProperties: false\n description: PageSubscriber represents a subscriber to a status page.\n openstatus.status_page.v1.PageTheme:\n type: string\n title: PageTheme\n enum:\n - PAGE_THEME_UNSPECIFIED\n - PAGE_THEME_SYSTEM\n - PAGE_THEME_LIGHT\n - PAGE_THEME_DARK\n description: PageTheme defines the visual theme of the status page.\n openstatus.status_page.v1.RemoveComponentRequest:\n type: object\n properties:\n id:\n type: string\n title: id\n minLength: 1\n description: ID of the component to remove (required).\n title: RemoveComponentRequest\n additionalProperties: false\n description: RemoveComponentRequest is the request to remove a component from a status page.\n openstatus.status_page.v1.RemoveComponentResponse:\n type: object\n properties:\n success:\n type: boolean\n title: success\n description: Whether the removal was successful.\n title: RemoveComponentResponse\n additionalProperties: false\n description: RemoveComponentResponse is the response after removing a component.\n openstatus.status_page.v1.StatusPage:\n type: object\n properties:\n id:\n type: string\n title: id\n description: Unique identifier for the status page.\n title:\n type: string\n title: title\n description: Title of the status page.\n description:\n type: string\n title: description\n description: Description of the status page.\n slug:\n type: string\n examples:\n - acme-corp\n title: slug\n description: URL-friendly slug for the status page.\n customDomain:\n type: string\n examples:\n - status.example.com\n title: custom_domain\n description: Custom domain for the status page (optional).\n published:\n type: boolean\n title: published\n description: Whether the status page is published and visible.\n accessType:\n title: access_type\n description: Access type for the status page.\n $ref: '#/components/schemas/openstatus.status_page.v1.PageAccessType'\n theme:\n title: theme\n description: Visual theme for the status page.\n $ref: '#/components/schemas/openstatus.status_page.v1.PageTheme'\n homepageUrl:\n type: string\n title: homepage_url\n description: URL to the homepage (optional).\n contactUrl:\n type: string\n title: contact_url\n description: URL to the contact page (optional).\n icon:\n type: string\n title: icon\n description: Icon URL for the status page (optional).\n createdAt:\n type: string\n examples:\n - \"2024-01-15T09:00:00Z\"\n title: created_at\n description: Timestamp when the page was created (RFC 3339 format).\n updatedAt:\n type: string\n examples:\n - \"2024-06-20T14:30:00Z\"\n title: updated_at\n description: Timestamp when the page was last updated (RFC 3339 format).\n defaultLocale:\n title: default_locale\n description: Default locale for the status page.\n $ref: '#/components/schemas/openstatus.status_page.v1.Locale'\n locales:\n type: array\n items:\n $ref: '#/components/schemas/openstatus.status_page.v1.Locale'\n title: locales\n description: Enabled locales for the status page.\n password:\n type: string\n title: password\n description: Password for the status page (only set when access_type is PASSWORD_PROTECTED).\n authEmailDomains:\n type: array\n items:\n type: string\n title: auth_email_domains\n description: Email domains allowed to access the page (only set when access_type is AUTHENTICATED).\n allowIndex:\n type: boolean\n title: allow_index\n description: Whether search engines are allowed to index this status page.\n allowedIpRanges:\n type: string\n title: allowed_ip_ranges\n description: Comma-separated IPv4 CIDR ranges (only set when access_type is IP_RESTRICTED).\n customTheme:\n oneOf:\n - $ref: '#/components/schemas/openstatus.status_page.v1.CustomTheme'\n - type: \"null\"\n title: custom_theme\n description: Per-mode CSS variable overrides merged over the theme (only set when configured).\n title: StatusPage\n additionalProperties: false\n description: StatusPage represents a full status page with all details.\n openstatus.status_page.v1.StatusPageSummary:\n type: object\n properties:\n id:\n type: string\n title: id\n description: Unique identifier for the status page.\n title:\n type: string\n title: title\n description: Title of the status page.\n slug:\n type: string\n title: slug\n description: URL-friendly slug for the status page.\n published:\n type: boolean\n title: published\n description: Whether the status page is published and visible.\n createdAt:\n type: string\n title: created_at\n description: Timestamp when the page was created (RFC 3339 format).\n updatedAt:\n type: string\n title: updated_at\n description: Timestamp when the page was last updated (RFC 3339 format).\n customDomain:\n type: string\n examples:\n - status.example.com\n title: custom_domain\n description: Custom domain for the status page (optional).\n title: StatusPageSummary\n additionalProperties: false\n description: StatusPageSummary represents metadata for a status page (used in list responses).\n openstatus.status_page.v1.SubscribeToPageRequest:\n type: object\n properties:\n pageId:\n type: string\n title: page_id\n minLength: 1\n description: ID of the status page to subscribe to (required).\n email:\n type: string\n examples:\n - user@example.com\n title: email\n format: email\n description: Email address to subscribe (required).\n title: SubscribeToPageRequest\n additionalProperties: false\n description: SubscribeToPageRequest is the request to subscribe an email to a status page.\n openstatus.status_page.v1.SubscribeToPageResponse:\n type: object\n properties:\n subscriber:\n title: subscriber\n description: The created subscriber.\n $ref: '#/components/schemas/openstatus.status_page.v1.PageSubscriber'\n title: SubscribeToPageResponse\n additionalProperties: false\n description: SubscribeToPageResponse is the response after subscribing to a status page.\n openstatus.status_page.v1.SubscriberSource:\n type: string\n title: SubscriberSource\n enum:\n - SUBSCRIBER_SOURCE_UNSPECIFIED\n - SUBSCRIBER_SOURCE_SELF_SIGNUP\n - SUBSCRIBER_SOURCE_VENDOR\n - SUBSCRIBER_SOURCE_IMPORT\n description: SubscriberSource indicates how the subscription was created.\n openstatus.status_page.v1.UnsubscribeFromPageRequest:\n type: object\n allOf:\n - properties:\n pageId:\n type: string\n title: page_id\n minLength: 1\n description: ID of the status page to unsubscribe from (required).\n - oneOf:\n - type: object\n properties:\n email:\n type: string\n title: email\n description: Email address to unsubscribe.\n title: email\n required:\n - email\n - type: object\n properties:\n id:\n type: string\n title: id\n description: Subscriber ID.\n title: id\n required:\n - id\n title: UnsubscribeFromPageRequest\n additionalProperties: false\n description: UnsubscribeFromPageRequest is the request to unsubscribe from a status page.\n openstatus.status_page.v1.UnsubscribeFromPageResponse:\n type: object\n properties:\n success:\n type: boolean\n title: success\n description: Whether the unsubscription was successful.\n title: UnsubscribeFromPageResponse\n additionalProperties: false\n description: UnsubscribeFromPageResponse is the response after unsubscribing from a status page.\n openstatus.status_page.v1.UpdateComponentGroupRequest:\n type: object\n properties:\n id:\n type: string\n title: id\n minLength: 1\n description: ID of the component group to update (required).\n name:\n type:\n - string\n - \"null\"\n title: name\n maxLength: 256\n minLength: 1\n description: New display name for the group (optional).\n defaultOpen:\n type:\n - boolean\n - \"null\"\n title: default_open\n description: Whether the group should be expanded by default on the status page (optional).\n title: UpdateComponentGroupRequest\n additionalProperties: false\n description: UpdateComponentGroupRequest is the request to update a component group.\n openstatus.status_page.v1.UpdateComponentGroupResponse:\n type: object\n properties:\n group:\n title: group\n description: The updated component group.\n $ref: '#/components/schemas/openstatus.status_page.v1.PageComponentGroup'\n title: UpdateComponentGroupResponse\n additionalProperties: false\n description: UpdateComponentGroupResponse is the response after updating a component group.\n openstatus.status_page.v1.UpdateComponentRequest:\n type: object\n properties:\n id:\n type: string\n title: id\n minLength: 1\n description: ID of the component to update (required).\n name:\n type:\n - string\n - \"null\"\n title: name\n maxLength: 256\n description: New display name for the component (optional).\n description:\n type:\n - string\n - \"null\"\n title: description\n maxLength: 1024\n description: New description for the component (optional).\n order:\n type:\n - integer\n - \"null\"\n title: order\n format: int32\n description: New display order (optional).\n groupId:\n type:\n - string\n - \"null\"\n title: group_id\n description: New group ID (optional, set to empty string to remove from group).\n groupOrder:\n type:\n - integer\n - \"null\"\n title: group_order\n format: int32\n description: New order within the group (optional).\n title: UpdateComponentRequest\n additionalProperties: false\n description: UpdateComponentRequest is the request to update a component.\n openstatus.status_page.v1.UpdateComponentResponse:\n type: object\n properties:\n component:\n title: component\n description: The updated component.\n $ref: '#/components/schemas/openstatus.status_page.v1.PageComponent'\n title: UpdateComponentResponse\n additionalProperties: false\n description: UpdateComponentResponse is the response after updating a component.\n openstatus.status_page.v1.UpdateStatusPageRequest:\n type: object\n properties:\n id:\n type: string\n title: id\n minLength: 1\n description: ID of the status page to update (required).\n title:\n type:\n - string\n - \"null\"\n title: title\n maxLength: 256\n minLength: 1\n description: New title for the status page (optional).\n description:\n type:\n - string\n - \"null\"\n title: description\n maxLength: 1024\n description: New description for the status page (optional).\n slug:\n type:\n - string\n - \"null\"\n title: slug\n maxLength: 256\n minLength: 1\n pattern: ^[a-z0-9]+(?:-[a-z0-9]+)*$\n description: New slug for the status page (optional).\n homepageUrl:\n type:\n - string\n - \"null\"\n title: homepage_url\n description: New homepage URL (optional).\n contactUrl:\n type:\n - string\n - \"null\"\n title: contact_url\n description: New contact URL (optional).\n defaultLocale:\n oneOf:\n - $ref: '#/components/schemas/openstatus.status_page.v1.Locale'\n - type: \"null\"\n title: default_locale\n description: New default locale for the status page (optional).\n locales:\n type: array\n items:\n $ref: '#/components/schemas/openstatus.status_page.v1.Locale'\n title: locales\n description: New enabled locales for the status page (optional).\n icon:\n type:\n - string\n - \"null\"\n title: icon\n maxLength: 1024\n description: New icon URL for the status page (optional).\n customDomain:\n type:\n - string\n - \"null\"\n title: custom_domain\n maxLength: 256\n description: New custom domain (optional).\n theme:\n oneOf:\n - $ref: '#/components/schemas/openstatus.status_page.v1.PageTheme'\n - type: \"null\"\n title: theme\n description: New visual theme (optional).\n accessType:\n oneOf:\n - $ref: '#/components/schemas/openstatus.status_page.v1.PageAccessType'\n - type: \"null\"\n title: access_type\n description: New access type (optional).\n password:\n type:\n - string\n - \"null\"\n title: password\n maxLength: 256\n minLength: 1\n description: New password (optional, required when access_type is PASSWORD_PROTECTED).\n authEmailDomains:\n type: array\n items:\n type: string\n title: auth_email_domains\n description: New email domains (optional, required when access_type is AUTHENTICATED).\n allowIndex:\n type:\n - boolean\n - \"null\"\n title: allow_index\n description: Whether search engines are allowed to index this status page (optional).\n allowedIpRanges:\n type:\n - string\n - \"null\"\n title: allowed_ip_ranges\n description: Comma-separated IPv4 CIDR ranges (required when access_type is IP_RESTRICTED).\n customTheme:\n oneOf:\n - $ref: '#/components/schemas/openstatus.status_page.v1.CustomTheme'\n - type: \"null\"\n title: custom_theme\n description: |-\n New per-mode CSS variable overrides (optional). Omit to keep the current\n value; send an empty message to clear. Requires the custom-theme plan feature.\n title: UpdateStatusPageRequest\n additionalProperties: false\n description: UpdateStatusPageRequest is the request to update a status page.\n openstatus.status_page.v1.UpdateStatusPageResponse:\n type: object\n properties:\n statusPage:\n title: status_page\n description: The updated status page.\n $ref: '#/components/schemas/openstatus.status_page.v1.StatusPage'\n title: UpdateStatusPageResponse\n additionalProperties: false\n description: UpdateStatusPageResponse is the response after updating a status page.\n openstatus.status_page.v1.WebhookChannel:\n type: object\n properties:\n webhookUrl:\n type: string\n title: webhook_url\n maxLength: 2048\n minLength: 1\n format: uri\n description: Webhook URL (required). Payload flavor (Slack / Discord / generic) is auto-detected from the URL prefix.\n headers:\n type: array\n items:\n $ref: '#/components/schemas/openstatus.status_page.v1.WebhookChannelHeader'\n title: headers\n description: Optional custom HTTP headers attached to every dispatch.\n title: WebhookChannel\n additionalProperties: false\n description: WebhookChannel carries the webhook-channel fields for CreatePageSubscription.\n openstatus.status_page.v1.WebhookChannelHeader:\n type: object\n properties:\n key:\n type: string\n title: key\n minLength: 1\n value:\n type: string\n title: value\n title: WebhookChannelHeader\n additionalProperties: false\n description: WebhookChannelHeader is a single custom HTTP header.\n openstatus.status_report.v1.AddStatusReportUpdateRequest:\n type: object\n properties:\n statusReportId:\n type: string\n title: status_report_id\n minLength: 1\n description: ID of the status report to update (required).\n status:\n title: status\n description: New status for the report (required).\n $ref: '#/components/schemas/openstatus.status_report.v1.StatusReportStatus'\n message:\n type: string\n title: message\n minLength: 1\n description: Message describing what changed (required).\n date:\n type:\n - string\n - \"null\"\n title: date\n pattern: ^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(\\.\\d{1,9})?(Z|[+-]\\d{2}:\\d{2})$\n description: Optional date for the update (RFC 3339 format). Defaults to current time if not provided.\n notify:\n type:\n - boolean\n - \"null\"\n title: notify\n description: Whether to notify subscribers about this update (optional, defaults to false).\n componentImpacts:\n type: array\n items:\n $ref: '#/components/schemas/openstatus.status_report.v1.ComponentImpact'\n title: component_impacts\n description: |-\n Per-component impacts this update sets (optional). Components named here\n are added to the report's affected set; omitted components keep their\n prior impact.\n title: AddStatusReportUpdateRequest\n additionalProperties: false\n description: AddStatusReportUpdateRequest is the request to add a new update to a status report.\n openstatus.status_report.v1.AddStatusReportUpdateResponse:\n type: object\n properties:\n statusReport:\n title: status_report\n description: The updated status report with the new update included.\n $ref: '#/components/schemas/openstatus.status_report.v1.StatusReport'\n title: AddStatusReportUpdateResponse\n additionalProperties: false\n description: AddStatusReportUpdateResponse is the response after adding an update to a status report.\n openstatus.status_report.v1.ComponentImpact:\n type: object\n properties:\n pageComponentId:\n type: string\n title: page_component_id\n description: ID of the affected page component.\n impact:\n title: impact\n description: Impact set for the component.\n $ref: '#/components/schemas/openstatus.status_report.v1.PageComponentImpact'\n title: ComponentImpact\n additionalProperties: false\n description: ComponentImpact pairs a page component with the impact an update set for it.\n openstatus.status_report.v1.CreateStatusReportRequest:\n type: object\n properties:\n title:\n type: string\n examples:\n - API Degradation Investigation\n title: title\n minLength: 1\n description: Title of the status report (required).\n status:\n title: status\n description: Initial status (required).\n $ref: '#/components/schemas/openstatus.status_report.v1.StatusReportStatus'\n message:\n type: string\n examples:\n - We are investigating reports of increased API latency.\n title: message\n minLength: 1\n description: Initial message describing the incident (required).\n date:\n type: string\n examples:\n - \"2024-03-15T10:30:00Z\"\n title: date\n pattern: ^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(\\.\\d{1,9})?(Z|[+-]\\d{2}:\\d{2})$\n description: Date when the event occurred (RFC 3339 format, required).\n pageId:\n type: string\n title: page_id\n minLength: 1\n description: Page ID to associate with this report (required).\n pageComponentIds:\n type: array\n items:\n type: string\n title: page_component_ids\n description: Page component IDs to associate with this report (optional).\n notify:\n type:\n - boolean\n - \"null\"\n title: notify\n description: Whether to notify subscribers about this status report (optional, defaults to false).\n componentImpacts:\n type: array\n items:\n $ref: '#/components/schemas/openstatus.status_report.v1.ComponentImpact'\n title: component_impacts\n description: |-\n Per-component impacts set by the initial update (optional). When provided,\n the named components are added to the report's affected set. Omitting this\n field creates a legacy report without impact tracking.\n incidentId:\n type:\n - string\n - \"null\"\n title: incident_id\n minLength: 1\n description: |-\n ID of an incident to link the report to (optional). Linked in the same\n step: a closed incident, or one already linked to a report, fails the create.\n title: CreateStatusReportRequest\n additionalProperties: false\n description: CreateStatusReportRequest is the request to create a new status report.\n openstatus.status_report.v1.CreateStatusReportResponse:\n type: object\n properties:\n statusReport:\n title: status_report\n description: The created status report.\n $ref: '#/components/schemas/openstatus.status_report.v1.StatusReport'\n title: CreateStatusReportResponse\n additionalProperties: false\n description: CreateStatusReportResponse is the response after creating a status report.\n openstatus.status_report.v1.DeleteStatusReportRequest:\n type: object\n properties:\n id:\n type: string\n title: id\n minLength: 1\n description: ID of the status report to delete (required).\n title: DeleteStatusReportRequest\n additionalProperties: false\n description: DeleteStatusReportRequest is the request to delete a status report.\n openstatus.status_report.v1.DeleteStatusReportResponse:\n type: object\n properties:\n success:\n type: boolean\n title: success\n description: Whether the deletion was successful.\n title: DeleteStatusReportResponse\n additionalProperties: false\n description: DeleteStatusReportResponse is the response after deleting a status report.\n openstatus.status_report.v1.DeleteStatusReportUpdateRequest:\n type: object\n properties:\n id:\n type: string\n title: id\n minLength: 1\n description: ID of the update to remove (required).\n title: DeleteStatusReportUpdateRequest\n additionalProperties: false\n description: DeleteStatusReportUpdateRequest is the request to remove a status report update.\n openstatus.status_report.v1.DeleteStatusReportUpdateResponse:\n type: object\n properties:\n success:\n type: boolean\n title: success\n description: Whether the deletion was successful.\n title: DeleteStatusReportUpdateResponse\n additionalProperties: false\n description: DeleteStatusReportUpdateResponse is the response after removing a status report update.\n openstatus.status_report.v1.GetStatusReportRequest:\n type: object\n properties:\n id:\n type: string\n title: id\n minLength: 1\n description: ID of the status report to retrieve (required).\n title: GetStatusReportRequest\n additionalProperties: false\n description: GetStatusReportRequest is the request to get a status report by ID.\n openstatus.status_report.v1.GetStatusReportResponse:\n type: object\n properties:\n statusReport:\n title: status_report\n description: The requested status report.\n $ref: '#/components/schemas/openstatus.status_report.v1.StatusReport'\n title: GetStatusReportResponse\n additionalProperties: false\n description: GetStatusReportResponse is the response containing the status report with its full update timeline.\n openstatus.status_report.v1.ListStatusReportsRequest:\n type: object\n properties:\n limit:\n type:\n - integer\n - \"null\"\n title: limit\n maximum: 100\n minimum: 1\n format: int32\n description: Maximum number of reports to return (1-100, defaults to 50).\n offset:\n type:\n - integer\n - \"null\"\n title: offset\n minimum: 0\n format: int32\n description: Number of reports to skip for pagination (defaults to 0).\n statuses:\n type: array\n items:\n $ref: '#/components/schemas/openstatus.status_report.v1.StatusReportStatus'\n title: statuses\n description: Filter by status (optional). If empty, returns all statuses.\n title: ListStatusReportsRequest\n additionalProperties: false\n description: ListStatusReportsRequest is the request to list status reports.\n openstatus.status_report.v1.ListStatusReportsResponse:\n type: object\n properties:\n statusReports:\n type: array\n items:\n $ref: '#/components/schemas/openstatus.status_report.v1.StatusReportSummary'\n title: status_reports\n description: List of status reports (metadata only, use GetStatusReport for full details).\n totalSize:\n type: integer\n title: total_size\n format: int32\n description: Total number of reports matching the filter.\n title: ListStatusReportsResponse\n additionalProperties: false\n description: ListStatusReportsResponse is the response containing status report summaries.\n openstatus.status_report.v1.PageComponentImpact:\n type: string\n title: PageComponentImpact\n enum:\n - PAGE_COMPONENT_IMPACT_UNSPECIFIED\n - PAGE_COMPONENT_IMPACT_OPERATIONAL\n - PAGE_COMPONENT_IMPACT_DEGRADED_PERFORMANCE\n - PAGE_COMPONENT_IMPACT_PARTIAL_OUTAGE\n - PAGE_COMPONENT_IMPACT_MAJOR_OUTAGE\n description: |-\n PageComponentImpact is the per-component impact a status report update sets.\n UNSPECIFIED means the caller doesn't speak impact (legacy) — it is NOT operational.\n openstatus.status_report.v1.StatusReport:\n type: object\n properties:\n id:\n type: string\n title: id\n description: Unique identifier for the status report.\n status:\n title: status\n description: Current status of the report.\n $ref: '#/components/schemas/openstatus.status_report.v1.StatusReportStatus'\n title:\n type: string\n title: title\n description: Title of the status report.\n pageComponentIds:\n type: array\n items:\n type: string\n title: page_component_ids\n description: IDs of affected page components.\n updates:\n type: array\n items:\n $ref: '#/components/schemas/openstatus.status_report.v1.StatusReportUpdate'\n title: updates\n description: Timeline of updates for this report (only included in GetStatusReport).\n createdAt:\n type: string\n title: created_at\n description: Timestamp when the report was created (RFC 3339 format).\n updatedAt:\n type: string\n title: updated_at\n description: Timestamp when the report was last updated (RFC 3339 format).\n incidentId:\n type:\n - string\n - \"null\"\n title: incident_id\n description: ID of the incident this report communicates (unset when not linked).\n title: StatusReport\n additionalProperties: false\n description: StatusReport represents an incident or maintenance report with full details.\n openstatus.status_report.v1.StatusReportStatus:\n type: string\n title: StatusReportStatus\n enum:\n - STATUS_REPORT_STATUS_UNSPECIFIED\n - STATUS_REPORT_STATUS_INVESTIGATING\n - STATUS_REPORT_STATUS_IDENTIFIED\n - STATUS_REPORT_STATUS_MONITORING\n - STATUS_REPORT_STATUS_RESOLVED\n description: StatusReportStatus represents the current state of a status report.\n openstatus.status_report.v1.StatusReportSummary:\n type: object\n properties:\n id:\n type: string\n title: id\n description: Unique identifier for the status report.\n status:\n title: status\n description: Current status of the report.\n $ref: '#/components/schemas/openstatus.status_report.v1.StatusReportStatus'\n title:\n type: string\n title: title\n description: Title of the status report.\n pageComponentIds:\n type: array\n items:\n type: string\n title: page_component_ids\n description: IDs of affected page components.\n createdAt:\n type: string\n title: created_at\n description: Timestamp when the report was created (RFC 3339 format).\n updatedAt:\n type: string\n title: updated_at\n description: Timestamp when the report was last updated (RFC 3339 format).\n incidentId:\n type:\n - string\n - \"null\"\n title: incident_id\n description: ID of the incident this report communicates (unset when not linked).\n title: StatusReportSummary\n additionalProperties: false\n description: StatusReportSummary represents metadata for a status report (used in list responses).\n openstatus.status_report.v1.StatusReportUpdate:\n type: object\n properties:\n id:\n type: string\n title: id\n description: Unique identifier for the update.\n status:\n title: status\n description: Status at the time of this update.\n $ref: '#/components/schemas/openstatus.status_report.v1.StatusReportStatus'\n date:\n type: string\n title: date\n description: Timestamp when this update occurred (RFC 3339 format).\n message:\n type: string\n title: message\n description: Message describing the update.\n createdAt:\n type: string\n title: created_at\n description: Timestamp when the update was created (RFC 3339 format).\n componentImpacts:\n type: array\n items:\n $ref: '#/components/schemas/openstatus.status_report.v1.ComponentImpact'\n title: component_impacts\n description: Per-component impacts this update set (empty for legacy reports).\n title: StatusReportUpdate\n additionalProperties: false\n description: StatusReportUpdate represents a single update entry in a status report timeline.\n openstatus.status_report.v1.UpdateStatusReportRequest:\n type: object\n properties:\n id:\n type: string\n title: id\n minLength: 1\n description: ID of the status report to update (required).\n title:\n type:\n - string\n - \"null\"\n title: title\n description: New title for the report (optional).\n pageComponentIds:\n type: array\n items:\n type: string\n title: page_component_ids\n description: New list of page component IDs (optional, replaces existing list).\n updatePageComponentIds:\n type:\n - boolean\n - \"null\"\n title: update_page_component_ids\n description: |-\n Set to true to update page component associations.\n When true, page_component_ids replaces the existing list (empty clears all).\n When false or unset, page_component_ids is ignored and existing associations are preserved.\n title: UpdateStatusReportRequest\n additionalProperties: false\n description: UpdateStatusReportRequest is the request to update a status report's metadata.\n openstatus.status_report.v1.UpdateStatusReportResponse:\n type: object\n properties:\n statusReport:\n title: status_report\n description: The updated status report.\n $ref: '#/components/schemas/openstatus.status_report.v1.StatusReport'\n title: UpdateStatusReportResponse\n additionalProperties: false\n description: UpdateStatusReportResponse is the response after updating a status report.\n openstatus.status_report.v1.UpdateStatusReportUpdateRequest:\n type: object\n properties:\n id:\n type: string\n title: id\n minLength: 1\n description: ID of the update to edit (required).\n status:\n oneOf:\n - $ref: '#/components/schemas/openstatus.status_report.v1.StatusReportStatus'\n - type: \"null\"\n title: status\n description: New status for this entry (optional).\n message:\n type:\n - string\n - \"null\"\n title: message\n minLength: 1\n description: New message (optional).\n date:\n type:\n - string\n - \"null\"\n title: date\n pattern: ^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(\\.\\d{1,9})?(Z|[+-]\\d{2}:\\d{2})$\n description: New date for this entry (RFC 3339 format, optional).\n componentImpacts:\n type: array\n items:\n $ref: '#/components/schemas/openstatus.status_report.v1.ComponentImpact'\n title: component_impacts\n description: Replacement impact set for this update (optional, see update_component_impacts).\n updateComponentImpacts:\n type:\n - boolean\n - \"null\"\n title: update_component_impacts\n description: |-\n Set to true to replace the update's component impacts with component_impacts\n (empty clears them). When false or unset, component_impacts is ignored.\n title: UpdateStatusReportUpdateRequest\n additionalProperties: false\n description: UpdateStatusReportUpdateRequest is the request to edit a status report update.\n openstatus.status_report.v1.UpdateStatusReportUpdateResponse:\n type: object\n properties:\n statusReportUpdate:\n title: status_report_update\n description: The edited timeline entry.\n $ref: '#/components/schemas/openstatus.status_report.v1.StatusReportUpdate'\n title: UpdateStatusReportUpdateResponse\n additionalProperties: false\n description: UpdateStatusReportUpdateResponse is the response after editing a status report update.\nsecurity:\n - ApiKeyAuth: []\ntags:\n - name: MonitorService\n description: |\n Create, update, delete, and query monitors. Supports HTTP, TCP, and DNS monitor types\n with configurable check intervals, regions, assertions, and alerting thresholds.\n - name: StatusPageService\n description: |\n Manage public status pages with components, component groups, and email subscribers.\n Includes endpoints for retrieving full page content and aggregated status.\n - name: NotificationService\n description: |\n Configure notification channels (Slack, Discord, PagerDuty, email, webhooks, etc.)\n and associate them with monitors. Supports 12 notification providers.\n - name: StatusReportService\n description: |\n Create and manage public status reports with status updates. Reports follow a lifecycle:\n investigating -> identified -> monitoring -> resolved.\n - name: IncidentService\n description: |\n Declare and run incidents: lifecycle (open -> mitigated -> resolved, or canceled),\n timeline notes, a linked status report, and the postmortem.\n - name: MaintenanceService\n description: |\n Schedule maintenance windows for status page components. Subscribers can be\n notified automatically when maintenance is created.\n - name: HealthService\n description: Health check endpoint for load balancer probes. No authentication required.\n - name: PrivateLocationService\n description: |-\n PrivateLocationService provides CRUD operations for private locations —\n self-hosted checker agents that run monitors from your own network.\npaths:\n /rpc/openstatus.health.v1.HealthService/Check:\n get:\n tags:\n - HealthService\n summary: Check\n description: Check returns the current serving status of the service.\n operationId: HealthService_Check.get\n parameters:\n - name: message\n in: query\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.health.v1.CheckRequest'\n responses:\n \"429\":\n $ref: '#/components/responses/RateLimited'\n default:\n description: Error\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/connect.error'\n \"200\":\n description: Success\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.health.v1.CheckResponse'\n post:\n tags:\n - HealthService\n summary: Check\n description: Check returns the current serving status of the service.\n operationId: HealthService_Check\n requestBody:\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.health.v1.CheckRequest'\n required: true\n responses:\n \"429\":\n $ref: '#/components/responses/RateLimited'\n default:\n description: Error\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/connect.error'\n \"200\":\n description: Success\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.health.v1.CheckResponse'\n /rpc/openstatus.incident.v1.IncidentService/AddIncidentNote:\n post:\n tags:\n - IncidentService\n summary: AddIncidentNote\n description: AddIncidentNote appends a note to the incident timeline.\n operationId: IncidentService_AddIncidentNote\n requestBody:\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.incident.v1.AddIncidentNoteRequest'\n required: true\n responses:\n \"429\":\n $ref: '#/components/responses/RateLimited'\n default:\n description: Error\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/connect.error'\n \"200\":\n description: Success\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.incident.v1.AddIncidentNoteResponse'\n /rpc/openstatus.incident.v1.IncidentService/ApprovePostmortem:\n post:\n tags:\n - IncidentService\n summary: ApprovePostmortem\n description: Approves the draft postmortem of a resolved incident. With close set to true the incident is closed in the same step. Fails with not_found when no postmortem has been written, and with failed_precondition when the incident is not resolved or the postmortem is already approved. Allowed for workspace owners and admins and for the incident commander. The API key acts as its creator, with the creator's current role; keys without a creator cannot call this.\n operationId: IncidentService_ApprovePostmortem\n requestBody:\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.incident.v1.ApprovePostmortemRequest'\n required: true\n responses:\n \"429\":\n $ref: '#/components/responses/RateLimited'\n default:\n description: Error\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/connect.error'\n \"200\":\n description: Success\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.incident.v1.ApprovePostmortemResponse'\n /rpc/openstatus.incident.v1.IncidentService/CloseIncident:\n post:\n tags:\n - IncidentService\n summary: CloseIncident\n description: Closes a resolved incident, after which only its postmortem can change. Requires an approved postmortem, or skip_postmortem set to true. Allowed for workspace owners and admins and for the incident commander. The API key acts as its creator, with the creator's current role; keys without a creator cannot call this.\n operationId: IncidentService_CloseIncident\n requestBody:\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.incident.v1.CloseIncidentRequest'\n required: true\n responses:\n \"429\":\n $ref: '#/components/responses/RateLimited'\n default:\n description: Error\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/connect.error'\n \"200\":\n description: Success\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.incident.v1.CloseIncidentResponse'\n /rpc/openstatus.incident.v1.IncidentService/DeclareIncident:\n post:\n tags:\n - IncidentService\n summary: DeclareIncident\n description: Declares a new incident in the open status. The commander is set by email and must be a member of the workspace; without one the incident is unassigned. A newly assigned commander is emailed unless they own the API key. When open_slack_channel is true and Slack is connected, a dedicated channel is created and the team invited. started_at defaults to now and can be set in the past for a retroactive declare. A status_report_id that does not exist in the workspace fails with not_found; one already linked to another incident fails with failed_precondition.\n operationId: IncidentService_DeclareIncident\n requestBody:\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.incident.v1.DeclareIncidentRequest'\n required: true\n responses:\n \"429\":\n $ref: '#/components/responses/RateLimited'\n default:\n description: Error\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/connect.error'\n \"200\":\n description: Success\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.incident.v1.DeclareIncidentResponse'\n /rpc/openstatus.incident.v1.IncidentService/DeleteIncident:\n post:\n tags:\n - IncidentService\n summary: DeleteIncident\n description: Deletes an incident declared by mistake. Only allowed while the incident is open and was never mitigated, resolved or closed (Incident.deletable); cancel it otherwise. Allowed for workspace owners and admins only. The API key acts as its creator, with the creator's current role; keys without a creator cannot call this.\n operationId: IncidentService_DeleteIncident\n requestBody:\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.incident.v1.DeleteIncidentRequest'\n required: true\n responses:\n \"429\":\n $ref: '#/components/responses/RateLimited'\n default:\n description: Error\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/connect.error'\n \"200\":\n description: Success\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.incident.v1.DeleteIncidentResponse'\n /rpc/openstatus.incident.v1.IncidentService/GetIncident:\n get:\n tags:\n - IncidentService\n summary: GetIncident\n description: GetIncident retrieves an incident by ID, including its timeline.\n operationId: IncidentService_GetIncident.get\n parameters:\n - name: message\n in: query\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.incident.v1.GetIncidentRequest'\n responses:\n \"429\":\n $ref: '#/components/responses/RateLimited'\n default:\n description: Error\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/connect.error'\n \"200\":\n description: Success\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.incident.v1.GetIncidentResponse'\n post:\n tags:\n - IncidentService\n summary: GetIncident\n description: GetIncident retrieves an incident by ID, including its timeline.\n operationId: IncidentService_GetIncident\n requestBody:\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.incident.v1.GetIncidentRequest'\n required: true\n responses:\n \"429\":\n $ref: '#/components/responses/RateLimited'\n default:\n description: Error\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/connect.error'\n \"200\":\n description: Success\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.incident.v1.GetIncidentResponse'\n /rpc/openstatus.incident.v1.IncidentService/GetPostmortem:\n get:\n tags:\n - IncidentService\n summary: GetPostmortem\n description: GetPostmortem retrieves the postmortem of an incident, if any.\n operationId: IncidentService_GetPostmortem.get\n parameters:\n - name: message\n in: query\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.incident.v1.GetPostmortemRequest'\n responses:\n \"429\":\n $ref: '#/components/responses/RateLimited'\n default:\n description: Error\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/connect.error'\n \"200\":\n description: Success\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.incident.v1.GetPostmortemResponse'\n post:\n tags:\n - IncidentService\n summary: GetPostmortem\n description: GetPostmortem retrieves the postmortem of an incident, if any.\n operationId: IncidentService_GetPostmortem\n requestBody:\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.incident.v1.GetPostmortemRequest'\n required: true\n responses:\n \"429\":\n $ref: '#/components/responses/RateLimited'\n default:\n description: Error\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/connect.error'\n \"200\":\n description: Success\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.incident.v1.GetPostmortemResponse'\n /rpc/openstatus.incident.v1.IncidentService/LinkStatusReport:\n post:\n tags:\n - IncidentService\n summary: LinkStatusReport\n description: |-\n LinkStatusReport links a public status report to the incident.\n A report already linked to another incident fails with failed_precondition.\n operationId: IncidentService_LinkStatusReport\n requestBody:\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.incident.v1.LinkStatusReportRequest'\n required: true\n responses:\n \"429\":\n $ref: '#/components/responses/RateLimited'\n default:\n description: Error\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/connect.error'\n \"200\":\n description: Success\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.incident.v1.LinkStatusReportResponse'\n /rpc/openstatus.incident.v1.IncidentService/ListIncidents:\n get:\n tags:\n - IncidentService\n summary: ListIncidents\n description: ListIncidents returns the incidents of the workspace (metadata only), newest declared first.\n operationId: IncidentService_ListIncidents.get\n parameters:\n - name: message\n in: query\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.incident.v1.ListIncidentsRequest'\n responses:\n \"429\":\n $ref: '#/components/responses/RateLimited'\n default:\n description: Error\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/connect.error'\n \"200\":\n description: Success\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.incident.v1.ListIncidentsResponse'\n post:\n tags:\n - IncidentService\n summary: ListIncidents\n description: ListIncidents returns the incidents of the workspace (metadata only), newest declared first.\n operationId: IncidentService_ListIncidents\n requestBody:\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.incident.v1.ListIncidentsRequest'\n required: true\n responses:\n \"429\":\n $ref: '#/components/responses/RateLimited'\n default:\n description: Error\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/connect.error'\n \"200\":\n description: Success\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.incident.v1.ListIncidentsResponse'\n /rpc/openstatus.incident.v1.IncidentService/SetIncidentStatus:\n post:\n tags:\n - IncidentService\n summary: SetIncidentStatus\n description: 'Moves an incident to a new status. Allowed transitions: open -> mitigated, resolved or canceled; mitigated -> resolved, open or canceled; resolved -> open. Canceled is terminal and closes the incident. The same status, a forbidden transition or a closed incident fail with failed_precondition; Incident.allowed_transitions lists what is valid. A linked status report is never updated: post the public update with StatusReportService.AddStatusReportUpdate.'\n operationId: IncidentService_SetIncidentStatus\n requestBody:\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.incident.v1.SetIncidentStatusRequest'\n required: true\n responses:\n \"429\":\n $ref: '#/components/responses/RateLimited'\n default:\n description: Error\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/connect.error'\n \"200\":\n description: Success\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.incident.v1.SetIncidentStatusResponse'\n /rpc/openstatus.incident.v1.IncidentService/UnlinkStatusReport:\n post:\n tags:\n - IncidentService\n summary: UnlinkStatusReport\n description: UnlinkStatusReport removes the link to the incident's status report.\n operationId: IncidentService_UnlinkStatusReport\n requestBody:\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.incident.v1.UnlinkStatusReportRequest'\n required: true\n responses:\n \"429\":\n $ref: '#/components/responses/RateLimited'\n default:\n description: Error\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/connect.error'\n \"200\":\n description: Success\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.incident.v1.UnlinkStatusReportResponse'\n /rpc/openstatus.incident.v1.IncidentService/UpdateIncident:\n post:\n tags:\n - IncidentService\n summary: UpdateIncident\n description: UpdateIncident edits the title, severity, summary, commander or start time of an incident.\n operationId: IncidentService_UpdateIncident\n requestBody:\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.incident.v1.UpdateIncidentRequest'\n required: true\n responses:\n \"429\":\n $ref: '#/components/responses/RateLimited'\n default:\n description: Error\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/connect.error'\n \"200\":\n description: Success\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.incident.v1.UpdateIncidentResponse'\n /rpc/openstatus.incident.v1.IncidentService/UpdatePostmortem:\n post:\n tags:\n - IncidentService\n summary: UpdatePostmortem\n description: UpdatePostmortem creates or replaces the postmortem body of a resolved incident.\n operationId: IncidentService_UpdatePostmortem\n requestBody:\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.incident.v1.UpdatePostmortemRequest'\n required: true\n responses:\n \"429\":\n $ref: '#/components/responses/RateLimited'\n default:\n description: Error\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/connect.error'\n \"200\":\n description: Success\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.incident.v1.UpdatePostmortemResponse'\n /rpc/openstatus.maintenance.v1.MaintenanceService/AddMaintenanceUpdate:\n post:\n tags:\n - MaintenanceService\n summary: AddMaintenanceUpdate\n description: AddMaintenanceUpdate appends a public update to a maintenance timeline.\n operationId: MaintenanceService_AddMaintenanceUpdate\n requestBody:\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.maintenance.v1.AddMaintenanceUpdateRequest'\n required: true\n responses:\n \"429\":\n $ref: '#/components/responses/RateLimited'\n default:\n description: Error\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/connect.error'\n \"200\":\n description: Success\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.maintenance.v1.AddMaintenanceUpdateResponse'\n /rpc/openstatus.maintenance.v1.MaintenanceService/CreateMaintenance:\n post:\n tags:\n - MaintenanceService\n summary: CreateMaintenance\n description: CreateMaintenance creates a new maintenance window.\n operationId: MaintenanceService_CreateMaintenance\n requestBody:\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.maintenance.v1.CreateMaintenanceRequest'\n required: true\n responses:\n \"429\":\n $ref: '#/components/responses/RateLimited'\n default:\n description: Error\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/connect.error'\n \"200\":\n description: Success\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.maintenance.v1.CreateMaintenanceResponse'\n /rpc/openstatus.maintenance.v1.MaintenanceService/DeleteMaintenance:\n post:\n tags:\n - MaintenanceService\n summary: DeleteMaintenance\n description: DeleteMaintenance removes a maintenance window.\n operationId: MaintenanceService_DeleteMaintenance\n requestBody:\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.maintenance.v1.DeleteMaintenanceRequest'\n required: true\n responses:\n \"429\":\n $ref: '#/components/responses/RateLimited'\n default:\n description: Error\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/connect.error'\n \"200\":\n description: Success\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.maintenance.v1.DeleteMaintenanceResponse'\n /rpc/openstatus.maintenance.v1.MaintenanceService/DeleteMaintenanceUpdate:\n post:\n tags:\n - MaintenanceService\n summary: DeleteMaintenanceUpdate\n description: DeleteMaintenanceUpdate removes an existing maintenance update.\n operationId: MaintenanceService_DeleteMaintenanceUpdate\n requestBody:\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.maintenance.v1.DeleteMaintenanceUpdateRequest'\n required: true\n responses:\n \"429\":\n $ref: '#/components/responses/RateLimited'\n default:\n description: Error\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/connect.error'\n \"200\":\n description: Success\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.maintenance.v1.DeleteMaintenanceUpdateResponse'\n /rpc/openstatus.maintenance.v1.MaintenanceService/GetMaintenance:\n get:\n tags:\n - MaintenanceService\n summary: GetMaintenance\n description: GetMaintenance retrieves a specific maintenance window by ID.\n operationId: MaintenanceService_GetMaintenance.get\n parameters:\n - name: message\n in: query\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.maintenance.v1.GetMaintenanceRequest'\n responses:\n \"429\":\n $ref: '#/components/responses/RateLimited'\n default:\n description: Error\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/connect.error'\n \"200\":\n description: Success\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.maintenance.v1.GetMaintenanceResponse'\n post:\n tags:\n - MaintenanceService\n summary: GetMaintenance\n description: GetMaintenance retrieves a specific maintenance window by ID.\n operationId: MaintenanceService_GetMaintenance\n requestBody:\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.maintenance.v1.GetMaintenanceRequest'\n required: true\n responses:\n \"429\":\n $ref: '#/components/responses/RateLimited'\n default:\n description: Error\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/connect.error'\n \"200\":\n description: Success\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.maintenance.v1.GetMaintenanceResponse'\n /rpc/openstatus.maintenance.v1.MaintenanceService/ListMaintenances:\n get:\n tags:\n - MaintenanceService\n summary: ListMaintenances\n description: ListMaintenances returns all maintenance windows for the workspace.\n operationId: MaintenanceService_ListMaintenances.get\n parameters:\n - name: message\n in: query\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.maintenance.v1.ListMaintenancesRequest'\n responses:\n \"429\":\n $ref: '#/components/responses/RateLimited'\n default:\n description: Error\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/connect.error'\n \"200\":\n description: Success\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.maintenance.v1.ListMaintenancesResponse'\n post:\n tags:\n - MaintenanceService\n summary: ListMaintenances\n description: ListMaintenances returns all maintenance windows for the workspace.\n operationId: MaintenanceService_ListMaintenances\n requestBody:\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.maintenance.v1.ListMaintenancesRequest'\n required: true\n responses:\n \"429\":\n $ref: '#/components/responses/RateLimited'\n default:\n description: Error\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/connect.error'\n \"200\":\n description: Success\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.maintenance.v1.ListMaintenancesResponse'\n /rpc/openstatus.maintenance.v1.MaintenanceService/UpdateMaintenance:\n post:\n tags:\n - MaintenanceService\n summary: UpdateMaintenance\n description: UpdateMaintenance updates a maintenance window.\n operationId: MaintenanceService_UpdateMaintenance\n requestBody:\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.maintenance.v1.UpdateMaintenanceRequest'\n required: true\n responses:\n \"429\":\n $ref: '#/components/responses/RateLimited'\n default:\n description: Error\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/connect.error'\n \"200\":\n description: Success\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.maintenance.v1.UpdateMaintenanceResponse'\n /rpc/openstatus.maintenance.v1.MaintenanceService/UpdateMaintenanceUpdate:\n post:\n tags:\n - MaintenanceService\n summary: UpdateMaintenanceUpdate\n description: UpdateMaintenanceUpdate edits an existing maintenance update.\n operationId: MaintenanceService_UpdateMaintenanceUpdate\n requestBody:\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.maintenance.v1.UpdateMaintenanceUpdateRequest'\n required: true\n responses:\n \"429\":\n $ref: '#/components/responses/RateLimited'\n default:\n description: Error\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/connect.error'\n \"200\":\n description: Success\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.maintenance.v1.UpdateMaintenanceUpdateResponse'\n /rpc/openstatus.monitor.v1.MonitorService/CreateDNSMonitor:\n post:\n tags:\n - MonitorService\n summary: CreateDNSMonitor\n description: CreateDNSMonitor creates a new DNS monitor.\n operationId: MonitorService_CreateDNSMonitor\n requestBody:\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.monitor.v1.CreateDNSMonitorRequest'\n required: true\n responses:\n \"429\":\n $ref: '#/components/responses/RateLimited'\n default:\n description: Error\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/connect.error'\n \"200\":\n description: Success\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.monitor.v1.CreateDNSMonitorResponse'\n /rpc/openstatus.monitor.v1.MonitorService/CreateGRPCMonitor:\n post:\n tags:\n - MonitorService\n summary: CreateGRPCMonitor\n description: CreateGRPCMonitor creates a new gRPC health check monitor.\n operationId: MonitorService_CreateGRPCMonitor\n requestBody:\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.monitor.v1.CreateGRPCMonitorRequest'\n required: true\n responses:\n \"429\":\n $ref: '#/components/responses/RateLimited'\n default:\n description: Error\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/connect.error'\n \"200\":\n description: Success\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.monitor.v1.CreateGRPCMonitorResponse'\n /rpc/openstatus.monitor.v1.MonitorService/CreateHTTPMonitor:\n post:\n tags:\n - MonitorService\n summary: CreateHTTPMonitor\n description: Creates a new HTTP monitor in the authenticated workspace. Configure the target URL, HTTP method, request headers and body, response assertions (status code, body content, headers), check periodicity, geographic regions, and optional OpenTelemetry export. The monitor starts checking immediately if set to active.\n operationId: MonitorService_CreateHTTPMonitor\n requestBody:\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.monitor.v1.CreateHTTPMonitorRequest'\n required: true\n responses:\n \"429\":\n $ref: '#/components/responses/RateLimited'\n default:\n description: Error\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/connect.error'\n \"200\":\n description: Success\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.monitor.v1.CreateHTTPMonitorResponse'\n /rpc/openstatus.monitor.v1.MonitorService/CreateICMPMonitor:\n post:\n tags:\n - MonitorService\n summary: CreateICMPMonitor\n description: CreateICMPMonitor creates a new ICMP monitor.\n operationId: MonitorService_CreateICMPMonitor\n requestBody:\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.monitor.v1.CreateICMPMonitorRequest'\n required: true\n responses:\n \"429\":\n $ref: '#/components/responses/RateLimited'\n default:\n description: Error\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/connect.error'\n \"200\":\n description: Success\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.monitor.v1.CreateICMPMonitorResponse'\n /rpc/openstatus.monitor.v1.MonitorService/CreateTCPMonitor:\n post:\n tags:\n - MonitorService\n summary: CreateTCPMonitor\n description: CreateTCPMonitor creates a new TCP monitor.\n operationId: MonitorService_CreateTCPMonitor\n requestBody:\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.monitor.v1.CreateTCPMonitorRequest'\n required: true\n responses:\n \"429\":\n $ref: '#/components/responses/RateLimited'\n default:\n description: Error\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/connect.error'\n \"200\":\n description: Success\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.monitor.v1.CreateTCPMonitorResponse'\n /rpc/openstatus.monitor.v1.MonitorService/DeleteMonitor:\n post:\n tags:\n - MonitorService\n summary: DeleteMonitor\n description: DeleteMonitor removes a monitor.\n operationId: MonitorService_DeleteMonitor\n requestBody:\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.monitor.v1.DeleteMonitorRequest'\n required: true\n responses:\n \"429\":\n $ref: '#/components/responses/RateLimited'\n default:\n description: Error\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/connect.error'\n \"200\":\n description: Success\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.monitor.v1.DeleteMonitorResponse'\n /rpc/openstatus.monitor.v1.MonitorService/GetMonitor:\n get:\n tags:\n - MonitorService\n summary: GetMonitor\n description: |-\n GetMonitor returns a single monitor by ID within the authenticated workspace.\n Returns the monitor configuration (HTTP, TCP, DNS, ICMP, or gRPC) using the MonitorConfig oneof type.\n operationId: MonitorService_GetMonitor.get\n parameters:\n - name: message\n in: query\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.monitor.v1.GetMonitorRequest'\n responses:\n \"429\":\n $ref: '#/components/responses/RateLimited'\n default:\n description: Error\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/connect.error'\n \"200\":\n description: Success\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.monitor.v1.GetMonitorResponse'\n post:\n tags:\n - MonitorService\n summary: GetMonitor\n description: |-\n GetMonitor returns a single monitor by ID within the authenticated workspace.\n Returns the monitor configuration (HTTP, TCP, DNS, ICMP, or gRPC) using the MonitorConfig oneof type.\n operationId: MonitorService_GetMonitor\n requestBody:\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.monitor.v1.GetMonitorRequest'\n required: true\n responses:\n \"429\":\n $ref: '#/components/responses/RateLimited'\n default:\n description: Error\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/connect.error'\n \"200\":\n description: Success\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.monitor.v1.GetMonitorResponse'\n /rpc/openstatus.monitor.v1.MonitorService/GetMonitorHTTPResponseLog:\n get:\n tags:\n - MonitorService\n summary: GetMonitorHTTPResponseLog\n description: GetMonitorHTTPResponseLog returns one response log for an HTTP monitor.\n operationId: MonitorService_GetMonitorHTTPResponseLog.get\n parameters:\n - name: message\n in: query\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.monitor.v1.GetMonitorHTTPResponseLogRequest'\n responses:\n \"429\":\n $ref: '#/components/responses/RateLimited'\n default:\n description: Error\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/connect.error'\n \"200\":\n description: Success\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.monitor.v1.GetMonitorHTTPResponseLogResponse'\n post:\n tags:\n - MonitorService\n summary: GetMonitorHTTPResponseLog\n description: GetMonitorHTTPResponseLog returns one response log for an HTTP monitor.\n operationId: MonitorService_GetMonitorHTTPResponseLog\n requestBody:\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.monitor.v1.GetMonitorHTTPResponseLogRequest'\n required: true\n responses:\n \"429\":\n $ref: '#/components/responses/RateLimited'\n default:\n description: Error\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/connect.error'\n \"200\":\n description: Success\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.monitor.v1.GetMonitorHTTPResponseLogResponse'\n /rpc/openstatus.monitor.v1.MonitorService/GetMonitorStatus:\n get:\n tags:\n - MonitorService\n summary: GetMonitorStatus\n description: GetMonitorStatus returns the current status of all regions for a monitor.\n operationId: MonitorService_GetMonitorStatus.get\n parameters:\n - name: message\n in: query\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.monitor.v1.GetMonitorStatusRequest'\n responses:\n \"429\":\n $ref: '#/components/responses/RateLimited'\n default:\n description: Error\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/connect.error'\n \"200\":\n description: Success\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.monitor.v1.GetMonitorStatusResponse'\n post:\n tags:\n - MonitorService\n summary: GetMonitorStatus\n description: GetMonitorStatus returns the current status of all regions for a monitor.\n operationId: MonitorService_GetMonitorStatus\n requestBody:\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.monitor.v1.GetMonitorStatusRequest'\n required: true\n responses:\n \"429\":\n $ref: '#/components/responses/RateLimited'\n default:\n description: Error\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/connect.error'\n \"200\":\n description: Success\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.monitor.v1.GetMonitorStatusResponse'\n /rpc/openstatus.monitor.v1.MonitorService/GetMonitorSummary:\n get:\n tags:\n - MonitorService\n summary: GetMonitorSummary\n description: Returns aggregated metrics for a monitor including latency percentiles (p50, p75, p90, p95, p99), request counts by status (successful, degraded, failed), and the timestamp of the last check. Metrics can be scoped to a time range (1 day, 7 days, or 14 days) and filtered by specific regions.\n operationId: MonitorService_GetMonitorSummary.get\n parameters:\n - name: message\n in: query\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.monitor.v1.GetMonitorSummaryRequest'\n responses:\n \"429\":\n $ref: '#/components/responses/RateLimited'\n default:\n description: Error\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/connect.error'\n \"200\":\n description: Success\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.monitor.v1.GetMonitorSummaryResponse'\n post:\n tags:\n - MonitorService\n summary: GetMonitorSummary\n description: Returns aggregated metrics for a monitor including latency percentiles (p50, p75, p90, p95, p99), request counts by status (successful, degraded, failed), and the timestamp of the last check. Metrics can be scoped to a time range (1 day, 7 days, or 14 days) and filtered by specific regions.\n operationId: MonitorService_GetMonitorSummary\n requestBody:\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.monitor.v1.GetMonitorSummaryRequest'\n required: true\n responses:\n \"429\":\n $ref: '#/components/responses/RateLimited'\n default:\n description: Error\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/connect.error'\n \"200\":\n description: Success\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.monitor.v1.GetMonitorSummaryResponse'\n /rpc/openstatus.monitor.v1.MonitorService/ListMonitorHTTPResponseLogs:\n get:\n tags:\n - MonitorService\n summary: ListMonitorHTTPResponseLogs\n description: ListMonitorHTTPResponseLogs returns paginated response logs for an HTTP monitor from the 14-day HTTP response-log window.\n operationId: MonitorService_ListMonitorHTTPResponseLogs.get\n parameters:\n - name: message\n in: query\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.monitor.v1.ListMonitorHTTPResponseLogsRequest'\n responses:\n \"429\":\n $ref: '#/components/responses/RateLimited'\n default:\n description: Error\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/connect.error'\n \"200\":\n description: Success\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.monitor.v1.ListMonitorHTTPResponseLogsResponse'\n post:\n tags:\n - MonitorService\n summary: ListMonitorHTTPResponseLogs\n description: ListMonitorHTTPResponseLogs returns paginated response logs for an HTTP monitor from the 14-day HTTP response-log window.\n operationId: MonitorService_ListMonitorHTTPResponseLogs\n requestBody:\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.monitor.v1.ListMonitorHTTPResponseLogsRequest'\n required: true\n responses:\n \"429\":\n $ref: '#/components/responses/RateLimited'\n default:\n description: Error\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/connect.error'\n \"200\":\n description: Success\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.monitor.v1.ListMonitorHTTPResponseLogsResponse'\n /rpc/openstatus.monitor.v1.MonitorService/ListMonitors:\n get:\n tags:\n - MonitorService\n summary: ListMonitors\n description: ListMonitors returns a paginated list of all monitors in the workspace.\n operationId: MonitorService_ListMonitors.get\n parameters:\n - name: message\n in: query\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.monitor.v1.ListMonitorsRequest'\n responses:\n \"429\":\n $ref: '#/components/responses/RateLimited'\n default:\n description: Error\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/connect.error'\n \"200\":\n description: Success\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.monitor.v1.ListMonitorsResponse'\n post:\n tags:\n - MonitorService\n summary: ListMonitors\n description: ListMonitors returns a paginated list of all monitors in the workspace.\n operationId: MonitorService_ListMonitors\n requestBody:\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.monitor.v1.ListMonitorsRequest'\n required: true\n responses:\n \"429\":\n $ref: '#/components/responses/RateLimited'\n default:\n description: Error\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/connect.error'\n \"200\":\n description: Success\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.monitor.v1.ListMonitorsResponse'\n /rpc/openstatus.monitor.v1.MonitorService/TriggerMonitor:\n post:\n tags:\n - MonitorService\n summary: TriggerMonitor\n description: Manually triggers an immediate check for the specified monitor across all configured regions. This operation is rate-limited under the synthetic-checks quota. A monitor run record is created and the check is dispatched to the checker service.\n operationId: MonitorService_TriggerMonitor\n requestBody:\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.monitor.v1.TriggerMonitorRequest'\n required: true\n responses:\n \"429\":\n $ref: '#/components/responses/RateLimited'\n default:\n description: Error\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/connect.error'\n \"200\":\n description: Success\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.monitor.v1.TriggerMonitorResponse'\n /rpc/openstatus.monitor.v1.MonitorService/UpdateDNSMonitor:\n post:\n tags:\n - MonitorService\n summary: UpdateDNSMonitor\n description: UpdateDNSMonitor updates an existing DNS monitor.\n operationId: MonitorService_UpdateDNSMonitor\n requestBody:\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.monitor.v1.UpdateDNSMonitorRequest'\n required: true\n responses:\n \"429\":\n $ref: '#/components/responses/RateLimited'\n default:\n description: Error\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/connect.error'\n \"200\":\n description: Success\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.monitor.v1.UpdateDNSMonitorResponse'\n /rpc/openstatus.monitor.v1.MonitorService/UpdateGRPCMonitor:\n post:\n tags:\n - MonitorService\n summary: UpdateGRPCMonitor\n description: UpdateGRPCMonitor updates an existing gRPC monitor.\n operationId: MonitorService_UpdateGRPCMonitor\n requestBody:\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.monitor.v1.UpdateGRPCMonitorRequest'\n required: true\n responses:\n \"429\":\n $ref: '#/components/responses/RateLimited'\n default:\n description: Error\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/connect.error'\n \"200\":\n description: Success\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.monitor.v1.UpdateGRPCMonitorResponse'\n /rpc/openstatus.monitor.v1.MonitorService/UpdateHTTPMonitor:\n post:\n tags:\n - MonitorService\n summary: UpdateHTTPMonitor\n description: UpdateHTTPMonitor updates an existing HTTP monitor.\n operationId: MonitorService_UpdateHTTPMonitor\n requestBody:\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.monitor.v1.UpdateHTTPMonitorRequest'\n required: true\n responses:\n \"429\":\n $ref: '#/components/responses/RateLimited'\n default:\n description: Error\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/connect.error'\n \"200\":\n description: Success\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.monitor.v1.UpdateHTTPMonitorResponse'\n /rpc/openstatus.monitor.v1.MonitorService/UpdateICMPMonitor:\n post:\n tags:\n - MonitorService\n summary: UpdateICMPMonitor\n description: UpdateICMPMonitor updates an existing ICMP monitor.\n operationId: MonitorService_UpdateICMPMonitor\n requestBody:\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.monitor.v1.UpdateICMPMonitorRequest'\n required: true\n responses:\n \"429\":\n $ref: '#/components/responses/RateLimited'\n default:\n description: Error\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/connect.error'\n \"200\":\n description: Success\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.monitor.v1.UpdateICMPMonitorResponse'\n /rpc/openstatus.monitor.v1.MonitorService/UpdateTCPMonitor:\n post:\n tags:\n - MonitorService\n summary: UpdateTCPMonitor\n description: UpdateTCPMonitor updates an existing TCP monitor.\n operationId: MonitorService_UpdateTCPMonitor\n requestBody:\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.monitor.v1.UpdateTCPMonitorRequest'\n required: true\n responses:\n \"429\":\n $ref: '#/components/responses/RateLimited'\n default:\n description: Error\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/connect.error'\n \"200\":\n description: Success\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.monitor.v1.UpdateTCPMonitorResponse'\n /rpc/openstatus.notification.v1.NotificationService/CheckNotificationLimit:\n get:\n tags:\n - NotificationService\n summary: CheckNotificationLimit\n description: CheckNotificationLimit checks if the workspace has reached its notification limit.\n operationId: NotificationService_CheckNotificationLimit.get\n parameters:\n - name: message\n in: query\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.notification.v1.CheckNotificationLimitRequest'\n responses:\n \"429\":\n $ref: '#/components/responses/RateLimited'\n default:\n description: Error\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/connect.error'\n \"200\":\n description: Success\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.notification.v1.CheckNotificationLimitResponse'\n post:\n tags:\n - NotificationService\n summary: CheckNotificationLimit\n description: CheckNotificationLimit checks if the workspace has reached its notification limit.\n operationId: NotificationService_CheckNotificationLimit\n requestBody:\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.notification.v1.CheckNotificationLimitRequest'\n required: true\n responses:\n \"429\":\n $ref: '#/components/responses/RateLimited'\n default:\n description: Error\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/connect.error'\n \"200\":\n description: Success\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.notification.v1.CheckNotificationLimitResponse'\n /rpc/openstatus.notification.v1.NotificationService/CreateNotification:\n post:\n tags:\n - NotificationService\n summary: CreateNotification\n description: CreateNotification creates a new notification channel.\n operationId: NotificationService_CreateNotification\n requestBody:\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.notification.v1.CreateNotificationRequest'\n required: true\n responses:\n \"429\":\n $ref: '#/components/responses/RateLimited'\n default:\n description: Error\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/connect.error'\n \"200\":\n description: Success\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.notification.v1.CreateNotificationResponse'\n /rpc/openstatus.notification.v1.NotificationService/DeleteNotification:\n post:\n tags:\n - NotificationService\n summary: DeleteNotification\n description: DeleteNotification removes a notification channel.\n operationId: NotificationService_DeleteNotification\n requestBody:\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.notification.v1.DeleteNotificationRequest'\n required: true\n responses:\n \"429\":\n $ref: '#/components/responses/RateLimited'\n default:\n description: Error\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/connect.error'\n \"200\":\n description: Success\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.notification.v1.DeleteNotificationResponse'\n /rpc/openstatus.notification.v1.NotificationService/GetNotification:\n get:\n tags:\n - NotificationService\n summary: GetNotification\n description: GetNotification retrieves a notification channel by ID.\n operationId: NotificationService_GetNotification.get\n parameters:\n - name: message\n in: query\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.notification.v1.GetNotificationRequest'\n responses:\n \"429\":\n $ref: '#/components/responses/RateLimited'\n default:\n description: Error\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/connect.error'\n \"200\":\n description: Success\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.notification.v1.GetNotificationResponse'\n post:\n tags:\n - NotificationService\n summary: GetNotification\n description: GetNotification retrieves a notification channel by ID.\n operationId: NotificationService_GetNotification\n requestBody:\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.notification.v1.GetNotificationRequest'\n required: true\n responses:\n \"429\":\n $ref: '#/components/responses/RateLimited'\n default:\n description: Error\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/connect.error'\n \"200\":\n description: Success\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.notification.v1.GetNotificationResponse'\n /rpc/openstatus.notification.v1.NotificationService/ListNotifications:\n get:\n tags:\n - NotificationService\n summary: ListNotifications\n description: ListNotifications returns a list of notification channels.\n operationId: NotificationService_ListNotifications.get\n parameters:\n - name: message\n in: query\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.notification.v1.ListNotificationsRequest'\n responses:\n \"429\":\n $ref: '#/components/responses/RateLimited'\n default:\n description: Error\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/connect.error'\n \"200\":\n description: Success\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.notification.v1.ListNotificationsResponse'\n post:\n tags:\n - NotificationService\n summary: ListNotifications\n description: ListNotifications returns a list of notification channels.\n operationId: NotificationService_ListNotifications\n requestBody:\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.notification.v1.ListNotificationsRequest'\n required: true\n responses:\n \"429\":\n $ref: '#/components/responses/RateLimited'\n default:\n description: Error\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/connect.error'\n \"200\":\n description: Success\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.notification.v1.ListNotificationsResponse'\n /rpc/openstatus.notification.v1.NotificationService/SendTestNotification:\n post:\n tags:\n - NotificationService\n summary: SendTestNotification\n description: Sends a test notification to the specified provider to verify that the configuration is correct. This does not require an existing notification channel - just provide the provider type and its configuration data. Returns success status and an error message if the test failed.\n operationId: NotificationService_SendTestNotification\n requestBody:\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.notification.v1.SendTestNotificationRequest'\n required: true\n responses:\n \"429\":\n $ref: '#/components/responses/RateLimited'\n default:\n description: Error\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/connect.error'\n \"200\":\n description: Success\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.notification.v1.SendTestNotificationResponse'\n /rpc/openstatus.notification.v1.NotificationService/UpdateNotification:\n post:\n tags:\n - NotificationService\n summary: UpdateNotification\n description: UpdateNotification updates an existing notification channel.\n operationId: NotificationService_UpdateNotification\n requestBody:\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.notification.v1.UpdateNotificationRequest'\n required: true\n responses:\n \"429\":\n $ref: '#/components/responses/RateLimited'\n default:\n description: Error\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/connect.error'\n \"200\":\n description: Success\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.notification.v1.UpdateNotificationResponse'\n /rpc/openstatus.private_location.v1.PrivateLocationService/CreatePrivateLocation:\n post:\n tags:\n - PrivateLocationService\n summary: CreatePrivateLocation\n description: Creates a private location. The agent token is generated by the server and returned in the response - it cannot be supplied by the caller. Use the token to configure the agent so it can pull its monitors and report results.\n operationId: PrivateLocationService_CreatePrivateLocation\n requestBody:\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.private_location.v1.CreatePrivateLocationRequest'\n required: true\n responses:\n \"429\":\n $ref: '#/components/responses/RateLimited'\n default:\n description: Error\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/connect.error'\n \"200\":\n description: Success\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.private_location.v1.CreatePrivateLocationResponse'\n /rpc/openstatus.private_location.v1.PrivateLocationService/DeletePrivateLocation:\n post:\n tags:\n - PrivateLocationService\n summary: DeletePrivateLocation\n description: DeletePrivateLocation removes a private location.\n operationId: PrivateLocationService_DeletePrivateLocation\n requestBody:\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.private_location.v1.DeletePrivateLocationRequest'\n required: true\n responses:\n \"429\":\n $ref: '#/components/responses/RateLimited'\n default:\n description: Error\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/connect.error'\n \"200\":\n description: Success\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.private_location.v1.DeletePrivateLocationResponse'\n /rpc/openstatus.private_location.v1.PrivateLocationService/GetPrivateLocation:\n get:\n tags:\n - PrivateLocationService\n summary: GetPrivateLocation\n description: |-\n GetPrivateLocation retrieves a single private location by ID, including\n its agent token.\n operationId: PrivateLocationService_GetPrivateLocation.get\n parameters:\n - name: message\n in: query\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.private_location.v1.GetPrivateLocationRequest'\n responses:\n \"429\":\n $ref: '#/components/responses/RateLimited'\n default:\n description: Error\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/connect.error'\n \"200\":\n description: Success\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.private_location.v1.GetPrivateLocationResponse'\n post:\n tags:\n - PrivateLocationService\n summary: GetPrivateLocation\n description: |-\n GetPrivateLocation retrieves a single private location by ID, including\n its agent token.\n operationId: PrivateLocationService_GetPrivateLocation\n requestBody:\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.private_location.v1.GetPrivateLocationRequest'\n required: true\n responses:\n \"429\":\n $ref: '#/components/responses/RateLimited'\n default:\n description: Error\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/connect.error'\n \"200\":\n description: Success\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.private_location.v1.GetPrivateLocationResponse'\n /rpc/openstatus.private_location.v1.PrivateLocationService/ListPrivateLocations:\n get:\n tags:\n - PrivateLocationService\n summary: ListPrivateLocations\n description: |-\n ListPrivateLocations returns a paginated list of private location\n summaries. Agent tokens are not included - use GetPrivateLocation.\n operationId: PrivateLocationService_ListPrivateLocations.get\n parameters:\n - name: message\n in: query\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.private_location.v1.ListPrivateLocationsRequest'\n responses:\n \"429\":\n $ref: '#/components/responses/RateLimited'\n default:\n description: Error\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/connect.error'\n \"200\":\n description: Success\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.private_location.v1.ListPrivateLocationsResponse'\n post:\n tags:\n - PrivateLocationService\n summary: ListPrivateLocations\n description: |-\n ListPrivateLocations returns a paginated list of private location\n summaries. Agent tokens are not included - use GetPrivateLocation.\n operationId: PrivateLocationService_ListPrivateLocations\n requestBody:\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.private_location.v1.ListPrivateLocationsRequest'\n required: true\n responses:\n \"429\":\n $ref: '#/components/responses/RateLimited'\n default:\n description: Error\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/connect.error'\n \"200\":\n description: Success\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.private_location.v1.ListPrivateLocationsResponse'\n /rpc/openstatus.private_location.v1.PrivateLocationService/UpdatePrivateLocation:\n post:\n tags:\n - PrivateLocationService\n summary: UpdatePrivateLocation\n description: UpdatePrivateLocation updates a private location.\n operationId: PrivateLocationService_UpdatePrivateLocation\n requestBody:\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.private_location.v1.UpdatePrivateLocationRequest'\n required: true\n responses:\n \"429\":\n $ref: '#/components/responses/RateLimited'\n default:\n description: Error\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/connect.error'\n \"200\":\n description: Success\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.private_location.v1.UpdatePrivateLocationResponse'\n /rpc/openstatus.status_page.v1.StatusPageService/AddMonitorComponent:\n post:\n tags:\n - StatusPageService\n summary: AddMonitorComponent\n description: AddMonitorComponent adds a monitor-based component to a status page.\n operationId: StatusPageService_AddMonitorComponent\n requestBody:\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.status_page.v1.AddMonitorComponentRequest'\n required: true\n responses:\n \"429\":\n $ref: '#/components/responses/RateLimited'\n default:\n description: Error\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/connect.error'\n \"200\":\n description: Success\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.status_page.v1.AddMonitorComponentResponse'\n /rpc/openstatus.status_page.v1.StatusPageService/AddStaticComponent:\n post:\n tags:\n - StatusPageService\n summary: AddStaticComponent\n description: AddStaticComponent adds a static component to a status page.\n operationId: StatusPageService_AddStaticComponent\n requestBody:\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.status_page.v1.AddStaticComponentRequest'\n required: true\n responses:\n \"429\":\n $ref: '#/components/responses/RateLimited'\n default:\n description: Error\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/connect.error'\n \"200\":\n description: Success\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.status_page.v1.AddStaticComponentResponse'\n /rpc/openstatus.status_page.v1.StatusPageService/CreateComponentGroup:\n post:\n tags:\n - StatusPageService\n summary: CreateComponentGroup\n description: CreateComponentGroup creates a new component group.\n operationId: StatusPageService_CreateComponentGroup\n requestBody:\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.status_page.v1.CreateComponentGroupRequest'\n required: true\n responses:\n \"429\":\n $ref: '#/components/responses/RateLimited'\n default:\n description: Error\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/connect.error'\n \"200\":\n description: Success\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.status_page.v1.CreateComponentGroupResponse'\n /rpc/openstatus.status_page.v1.StatusPageService/CreatePageSubscription:\n post:\n tags:\n - StatusPageService\n summary: 'CreatePageSubscription: operator-added subscriber (email or webhook), no verification.'\n description: Operator-added subscriber (email or webhook) with no verification flow — the partner starts receiving notifications immediately. Supports Slack and Discord webhook URLs in addition to email. A management token is still generated so the subscriber can self-manage (update scope, unsubscribe) without operator involvement. Use this for vendor/partner integrations where consent is established out-of-band; use SubscribeToPage for subscriber-initiated signups that require double opt-in.\n operationId: StatusPageService_CreatePageSubscription\n requestBody:\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.status_page.v1.CreatePageSubscriptionRequest'\n required: true\n responses:\n \"429\":\n $ref: '#/components/responses/RateLimited'\n default:\n description: Error\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/connect.error'\n \"200\":\n description: Success\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.status_page.v1.CreatePageSubscriptionResponse'\n /rpc/openstatus.status_page.v1.StatusPageService/CreateStatusPage:\n post:\n tags:\n - StatusPageService\n summary: CreateStatusPage\n description: CreateStatusPage creates a new status page.\n operationId: StatusPageService_CreateStatusPage\n requestBody:\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.status_page.v1.CreateStatusPageRequest'\n required: true\n responses:\n \"429\":\n $ref: '#/components/responses/RateLimited'\n default:\n description: Error\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/connect.error'\n \"200\":\n description: Success\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.status_page.v1.CreateStatusPageResponse'\n /rpc/openstatus.status_page.v1.StatusPageService/DeleteComponentGroup:\n post:\n tags:\n - StatusPageService\n summary: DeleteComponentGroup\n description: DeleteComponentGroup removes a component group.\n operationId: StatusPageService_DeleteComponentGroup\n requestBody:\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.status_page.v1.DeleteComponentGroupRequest'\n required: true\n responses:\n \"429\":\n $ref: '#/components/responses/RateLimited'\n default:\n description: Error\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/connect.error'\n \"200\":\n description: Success\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.status_page.v1.DeleteComponentGroupResponse'\n /rpc/openstatus.status_page.v1.StatusPageService/DeleteStatusPage:\n post:\n tags:\n - StatusPageService\n summary: DeleteStatusPage\n description: DeleteStatusPage removes a status page.\n operationId: StatusPageService_DeleteStatusPage\n requestBody:\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.status_page.v1.DeleteStatusPageRequest'\n required: true\n responses:\n \"429\":\n $ref: '#/components/responses/RateLimited'\n default:\n description: Error\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/connect.error'\n \"200\":\n description: Success\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.status_page.v1.DeleteStatusPageResponse'\n /rpc/openstatus.status_page.v1.StatusPageService/GetOverallStatus:\n get:\n tags:\n - StatusPageService\n summary: GetOverallStatus\n description: 'Returns the overall status of a status page along with individual component statuses. The overall status is computed from active status reports and maintenances with the following priority: degraded (from active status reports) > maintenance (from active maintenance windows) > operational.'\n operationId: StatusPageService_GetOverallStatus.get\n parameters:\n - name: message\n in: query\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.status_page.v1.GetOverallStatusRequest'\n responses:\n \"429\":\n $ref: '#/components/responses/RateLimited'\n default:\n description: Error\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/connect.error'\n \"200\":\n description: Success\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.status_page.v1.GetOverallStatusResponse'\n post:\n tags:\n - StatusPageService\n summary: GetOverallStatus\n description: 'Returns the overall status of a status page along with individual component statuses. The overall status is computed from active status reports and maintenances with the following priority: degraded (from active status reports) > maintenance (from active maintenance windows) > operational.'\n operationId: StatusPageService_GetOverallStatus\n requestBody:\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.status_page.v1.GetOverallStatusRequest'\n required: true\n responses:\n \"429\":\n $ref: '#/components/responses/RateLimited'\n default:\n description: Error\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/connect.error'\n \"200\":\n description: Success\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.status_page.v1.GetOverallStatusResponse'\n /rpc/openstatus.status_page.v1.StatusPageService/GetPageComponent:\n get:\n tags:\n - StatusPageService\n summary: GetPageComponent\n description: Returns a single status-page component by its ID, scoped to the authenticated workspace. Use this to resolve a component id to its name (and other fields) instead of fetching the whole page via GetStatusPageContent and filtering .components.\n operationId: StatusPageService_GetPageComponent.get\n parameters:\n - name: message\n in: query\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.status_page.v1.GetPageComponentRequest'\n responses:\n \"429\":\n $ref: '#/components/responses/RateLimited'\n default:\n description: Error\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/connect.error'\n \"200\":\n description: Success\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.status_page.v1.GetPageComponentResponse'\n post:\n tags:\n - StatusPageService\n summary: GetPageComponent\n description: Returns a single status-page component by its ID, scoped to the authenticated workspace. Use this to resolve a component id to its name (and other fields) instead of fetching the whole page via GetStatusPageContent and filtering .components.\n operationId: StatusPageService_GetPageComponent\n requestBody:\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.status_page.v1.GetPageComponentRequest'\n required: true\n responses:\n \"429\":\n $ref: '#/components/responses/RateLimited'\n default:\n description: Error\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/connect.error'\n \"200\":\n description: Success\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.status_page.v1.GetPageComponentResponse'\n /rpc/openstatus.status_page.v1.StatusPageService/GetPageComponentDailySummary:\n get:\n tags:\n - StatusPageService\n summary: GetPageComponentDailySummary\n description: 'Returns per-component daily status buckets (ok/degraded/error/count plus a resolved status) merged with the incident, maintenance, and status-report timeline over the last N days (max 45). Suitable as a single source of truth for status-bar and uptime-calendar rendering. Supports two access paths: by id (authenticated, workspace-scoped) or by slug (public access, requires the page to be published with access_type=PUBLIC).'\n operationId: StatusPageService_GetPageComponentDailySummary.get\n parameters:\n - name: message\n in: query\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.status_page.v1.GetPageComponentDailySummaryRequest'\n responses:\n \"429\":\n $ref: '#/components/responses/RateLimited'\n default:\n description: Error\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/connect.error'\n \"200\":\n description: Success\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.status_page.v1.GetPageComponentDailySummaryResponse'\n post:\n tags:\n - StatusPageService\n summary: GetPageComponentDailySummary\n description: 'Returns per-component daily status buckets (ok/degraded/error/count plus a resolved status) merged with the incident, maintenance, and status-report timeline over the last N days (max 45). Suitable as a single source of truth for status-bar and uptime-calendar rendering. Supports two access paths: by id (authenticated, workspace-scoped) or by slug (public access, requires the page to be published with access_type=PUBLIC).'\n operationId: StatusPageService_GetPageComponentDailySummary\n requestBody:\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.status_page.v1.GetPageComponentDailySummaryRequest'\n required: true\n responses:\n \"429\":\n $ref: '#/components/responses/RateLimited'\n default:\n description: Error\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/connect.error'\n \"200\":\n description: Success\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.status_page.v1.GetPageComponentDailySummaryResponse'\n /rpc/openstatus.status_page.v1.StatusPageService/GetStatusPage:\n get:\n tags:\n - StatusPageService\n summary: GetStatusPage\n description: GetStatusPage retrieves a specific status page by ID.\n operationId: StatusPageService_GetStatusPage.get\n parameters:\n - name: message\n in: query\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.status_page.v1.GetStatusPageRequest'\n responses:\n \"429\":\n $ref: '#/components/responses/RateLimited'\n default:\n description: Error\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/connect.error'\n \"200\":\n description: Success\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.status_page.v1.GetStatusPageResponse'\n post:\n tags:\n - StatusPageService\n summary: GetStatusPage\n description: GetStatusPage retrieves a specific status page by ID.\n operationId: StatusPageService_GetStatusPage\n requestBody:\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.status_page.v1.GetStatusPageRequest'\n required: true\n responses:\n \"429\":\n $ref: '#/components/responses/RateLimited'\n default:\n description: Error\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/connect.error'\n \"200\":\n description: Success\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.status_page.v1.GetStatusPageResponse'\n /rpc/openstatus.status_page.v1.StatusPageService/GetStatusPageContent:\n get:\n tags:\n - StatusPageService\n summary: GetStatusPageContent\n description: 'Returns the full content of a status page including its components, component groups, active status reports, and scheduled maintenances. Supports two access paths: by ID (requires authentication, workspace-scoped) or by slug (public access, requires the page to be published with access_type=PUBLIC).'\n operationId: StatusPageService_GetStatusPageContent.get\n parameters:\n - name: message\n in: query\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.status_page.v1.GetStatusPageContentRequest'\n responses:\n \"429\":\n $ref: '#/components/responses/RateLimited'\n default:\n description: Error\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/connect.error'\n \"200\":\n description: Success\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.status_page.v1.GetStatusPageContentResponse'\n post:\n tags:\n - StatusPageService\n summary: GetStatusPageContent\n description: 'Returns the full content of a status page including its components, component groups, active status reports, and scheduled maintenances. Supports two access paths: by ID (requires authentication, workspace-scoped) or by slug (public access, requires the page to be published with access_type=PUBLIC).'\n operationId: StatusPageService_GetStatusPageContent\n requestBody:\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.status_page.v1.GetStatusPageContentRequest'\n required: true\n responses:\n \"429\":\n $ref: '#/components/responses/RateLimited'\n default:\n description: Error\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/connect.error'\n \"200\":\n description: Success\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.status_page.v1.GetStatusPageContentResponse'\n /rpc/openstatus.status_page.v1.StatusPageService/GetStatusPageOverview:\n get:\n tags:\n - StatusPageService\n summary: GetStatusPageOverview\n description: 'Returns everything about a single status page in one authenticated call: the page, its rich rendering configuration, components, component groups, active/recent status reports, maintenances, and the computed overall + per-component statuses. Workspace-scoped by id; there is no public slug access path. Does not include uptime time-series — use GetPageComponentDailySummary for that.'\n operationId: StatusPageService_GetStatusPageOverview.get\n parameters:\n - name: message\n in: query\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.status_page.v1.GetStatusPageOverviewRequest'\n responses:\n \"429\":\n $ref: '#/components/responses/RateLimited'\n default:\n description: Error\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/connect.error'\n \"200\":\n description: Success\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.status_page.v1.GetStatusPageOverviewResponse'\n post:\n tags:\n - StatusPageService\n summary: GetStatusPageOverview\n description: 'Returns everything about a single status page in one authenticated call: the page, its rich rendering configuration, components, component groups, active/recent status reports, maintenances, and the computed overall + per-component statuses. Workspace-scoped by id; there is no public slug access path. Does not include uptime time-series — use GetPageComponentDailySummary for that.'\n operationId: StatusPageService_GetStatusPageOverview\n requestBody:\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.status_page.v1.GetStatusPageOverviewRequest'\n required: true\n responses:\n \"429\":\n $ref: '#/components/responses/RateLimited'\n default:\n description: Error\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/connect.error'\n \"200\":\n description: Success\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.status_page.v1.GetStatusPageOverviewResponse'\n /rpc/openstatus.status_page.v1.StatusPageService/ListStatusPages:\n get:\n tags:\n - StatusPageService\n summary: ListStatusPages\n description: ListStatusPages returns all status pages for the workspace.\n operationId: StatusPageService_ListStatusPages.get\n parameters:\n - name: message\n in: query\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.status_page.v1.ListStatusPagesRequest'\n responses:\n \"429\":\n $ref: '#/components/responses/RateLimited'\n default:\n description: Error\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/connect.error'\n \"200\":\n description: Success\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.status_page.v1.ListStatusPagesResponse'\n post:\n tags:\n - StatusPageService\n summary: ListStatusPages\n description: ListStatusPages returns all status pages for the workspace.\n operationId: StatusPageService_ListStatusPages\n requestBody:\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.status_page.v1.ListStatusPagesRequest'\n required: true\n responses:\n \"429\":\n $ref: '#/components/responses/RateLimited'\n default:\n description: Error\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/connect.error'\n \"200\":\n description: Success\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.status_page.v1.ListStatusPagesResponse'\n /rpc/openstatus.status_page.v1.StatusPageService/ListSubscribers:\n get:\n tags:\n - StatusPageService\n summary: ListSubscribers\n description: ListSubscribers returns all subscribers for a status page.\n operationId: StatusPageService_ListSubscribers.get\n parameters:\n - name: message\n in: query\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.status_page.v1.ListSubscribersRequest'\n responses:\n \"429\":\n $ref: '#/components/responses/RateLimited'\n default:\n description: Error\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/connect.error'\n \"200\":\n description: Success\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.status_page.v1.ListSubscribersResponse'\n post:\n tags:\n - StatusPageService\n summary: ListSubscribers\n description: ListSubscribers returns all subscribers for a status page.\n operationId: StatusPageService_ListSubscribers\n requestBody:\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.status_page.v1.ListSubscribersRequest'\n required: true\n responses:\n \"429\":\n $ref: '#/components/responses/RateLimited'\n default:\n description: Error\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/connect.error'\n \"200\":\n description: Success\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.status_page.v1.ListSubscribersResponse'\n /rpc/openstatus.status_page.v1.StatusPageService/RemoveComponent:\n post:\n tags:\n - StatusPageService\n summary: RemoveComponent\n description: RemoveComponent removes a component from a status page.\n operationId: StatusPageService_RemoveComponent\n requestBody:\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.status_page.v1.RemoveComponentRequest'\n required: true\n responses:\n \"429\":\n $ref: '#/components/responses/RateLimited'\n default:\n description: Error\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/connect.error'\n \"200\":\n description: Success\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.status_page.v1.RemoveComponentResponse'\n /rpc/openstatus.status_page.v1.StatusPageService/SubscribeToPage:\n post:\n tags:\n - StatusPageService\n summary: 'SubscribeToPage: end-user email self-signup with double opt-in verification.'\n description: End-user email self-signup with double opt-in verification. A verification email is sent and the subscription activates only after the recipient confirms. If the email was previously unsubscribed, the existing row is reactivated instead of a duplicate being created. Use this for subscriber-initiated signups on the public status page; use CreatePageSubscription for operator-initiated (vendor) subscriptions that should skip verification.\n operationId: StatusPageService_SubscribeToPage\n requestBody:\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.status_page.v1.SubscribeToPageRequest'\n required: true\n responses:\n \"429\":\n $ref: '#/components/responses/RateLimited'\n default:\n description: Error\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/connect.error'\n \"200\":\n description: Success\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.status_page.v1.SubscribeToPageResponse'\n /rpc/openstatus.status_page.v1.StatusPageService/UnsubscribeFromPage:\n post:\n tags:\n - StatusPageService\n summary: UnsubscribeFromPage\n description: UnsubscribeFromPage removes a subscription from a status page.\n operationId: StatusPageService_UnsubscribeFromPage\n requestBody:\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.status_page.v1.UnsubscribeFromPageRequest'\n required: true\n responses:\n \"429\":\n $ref: '#/components/responses/RateLimited'\n default:\n description: Error\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/connect.error'\n \"200\":\n description: Success\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.status_page.v1.UnsubscribeFromPageResponse'\n /rpc/openstatus.status_page.v1.StatusPageService/UpdateComponent:\n post:\n tags:\n - StatusPageService\n summary: UpdateComponent\n description: UpdateComponent updates an existing component.\n operationId: StatusPageService_UpdateComponent\n requestBody:\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.status_page.v1.UpdateComponentRequest'\n required: true\n responses:\n \"429\":\n $ref: '#/components/responses/RateLimited'\n default:\n description: Error\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/connect.error'\n \"200\":\n description: Success\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.status_page.v1.UpdateComponentResponse'\n /rpc/openstatus.status_page.v1.StatusPageService/UpdateComponentGroup:\n post:\n tags:\n - StatusPageService\n summary: UpdateComponentGroup\n description: UpdateComponentGroup updates an existing component group.\n operationId: StatusPageService_UpdateComponentGroup\n requestBody:\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.status_page.v1.UpdateComponentGroupRequest'\n required: true\n responses:\n \"429\":\n $ref: '#/components/responses/RateLimited'\n default:\n description: Error\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/connect.error'\n \"200\":\n description: Success\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.status_page.v1.UpdateComponentGroupResponse'\n /rpc/openstatus.status_page.v1.StatusPageService/UpdateStatusPage:\n post:\n tags:\n - StatusPageService\n summary: UpdateStatusPage\n description: UpdateStatusPage updates an existing status page.\n operationId: StatusPageService_UpdateStatusPage\n requestBody:\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.status_page.v1.UpdateStatusPageRequest'\n required: true\n responses:\n \"429\":\n $ref: '#/components/responses/RateLimited'\n default:\n description: Error\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/connect.error'\n \"200\":\n description: Success\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.status_page.v1.UpdateStatusPageResponse'\n /rpc/openstatus.status_report.v1.StatusReportService/AddStatusReportUpdate:\n post:\n tags:\n - StatusReportService\n summary: AddStatusReportUpdate\n description: 'Adds a new update entry to an existing status report and transitions the report to the specified status. Status reports follow a lifecycle: investigating -> identified -> monitoring -> resolved. If notify is true, subscribers of the associated page are notified by email about the update.'\n operationId: StatusReportService_AddStatusReportUpdate\n requestBody:\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.status_report.v1.AddStatusReportUpdateRequest'\n required: true\n responses:\n \"429\":\n $ref: '#/components/responses/RateLimited'\n default:\n description: Error\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/connect.error'\n \"200\":\n description: Success\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.status_report.v1.AddStatusReportUpdateResponse'\n /rpc/openstatus.status_report.v1.StatusReportService/CreateStatusReport:\n post:\n tags:\n - StatusReportService\n summary: CreateStatusReport\n description: Creates a new status report with an initial update entry. The report is associated with a status page and optionally specific page components. An initial StatusReportUpdate is created automatically with the provided status, message, and date. If notify is true, subscribers of the associated page are notified by email.\n operationId: StatusReportService_CreateStatusReport\n requestBody:\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.status_report.v1.CreateStatusReportRequest'\n required: true\n responses:\n \"429\":\n $ref: '#/components/responses/RateLimited'\n default:\n description: Error\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/connect.error'\n \"200\":\n description: Success\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.status_report.v1.CreateStatusReportResponse'\n /rpc/openstatus.status_report.v1.StatusReportService/DeleteStatusReport:\n post:\n tags:\n - StatusReportService\n summary: DeleteStatusReport\n description: DeleteStatusReport removes a status report and all its updates.\n operationId: StatusReportService_DeleteStatusReport\n requestBody:\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.status_report.v1.DeleteStatusReportRequest'\n required: true\n responses:\n \"429\":\n $ref: '#/components/responses/RateLimited'\n default:\n description: Error\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/connect.error'\n \"200\":\n description: Success\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.status_report.v1.DeleteStatusReportResponse'\n /rpc/openstatus.status_report.v1.StatusReportService/DeleteStatusReportUpdate:\n post:\n tags:\n - StatusReportService\n summary: DeleteStatusReportUpdate\n description: 'Removes a status report update. The report''s status is re-derived from the remaining updates. Deleting the last remaining update is rejected with INVALID_ARGUMENT: delete the status report instead.'\n operationId: StatusReportService_DeleteStatusReportUpdate\n requestBody:\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.status_report.v1.DeleteStatusReportUpdateRequest'\n required: true\n responses:\n \"429\":\n $ref: '#/components/responses/RateLimited'\n default:\n description: Error\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/connect.error'\n \"200\":\n description: Success\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.status_report.v1.DeleteStatusReportUpdateResponse'\n /rpc/openstatus.status_report.v1.StatusReportService/GetStatusReport:\n get:\n tags:\n - StatusReportService\n summary: GetStatusReport\n description: GetStatusReport retrieves a specific status report by ID (includes full update timeline).\n operationId: StatusReportService_GetStatusReport.get\n parameters:\n - name: message\n in: query\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.status_report.v1.GetStatusReportRequest'\n responses:\n \"429\":\n $ref: '#/components/responses/RateLimited'\n default:\n description: Error\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/connect.error'\n \"200\":\n description: Success\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.status_report.v1.GetStatusReportResponse'\n post:\n tags:\n - StatusReportService\n summary: GetStatusReport\n description: GetStatusReport retrieves a specific status report by ID (includes full update timeline).\n operationId: StatusReportService_GetStatusReport\n requestBody:\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.status_report.v1.GetStatusReportRequest'\n required: true\n responses:\n \"429\":\n $ref: '#/components/responses/RateLimited'\n default:\n description: Error\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/connect.error'\n \"200\":\n description: Success\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.status_report.v1.GetStatusReportResponse'\n /rpc/openstatus.status_report.v1.StatusReportService/ListStatusReports:\n get:\n tags:\n - StatusReportService\n summary: ListStatusReports\n description: ListStatusReports returns all status reports for the workspace (metadata only).\n operationId: StatusReportService_ListStatusReports.get\n parameters:\n - name: message\n in: query\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.status_report.v1.ListStatusReportsRequest'\n responses:\n \"429\":\n $ref: '#/components/responses/RateLimited'\n default:\n description: Error\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/connect.error'\n \"200\":\n description: Success\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.status_report.v1.ListStatusReportsResponse'\n post:\n tags:\n - StatusReportService\n summary: ListStatusReports\n description: ListStatusReports returns all status reports for the workspace (metadata only).\n operationId: StatusReportService_ListStatusReports\n requestBody:\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.status_report.v1.ListStatusReportsRequest'\n required: true\n responses:\n \"429\":\n $ref: '#/components/responses/RateLimited'\n default:\n description: Error\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/connect.error'\n \"200\":\n description: Success\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.status_report.v1.ListStatusReportsResponse'\n /rpc/openstatus.status_report.v1.StatusReportService/UpdateStatusReport:\n post:\n tags:\n - StatusReportService\n summary: UpdateStatusReport\n description: UpdateStatusReport updates the metadata of a status report (title, page components).\n operationId: StatusReportService_UpdateStatusReport\n requestBody:\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.status_report.v1.UpdateStatusReportRequest'\n required: true\n responses:\n \"429\":\n $ref: '#/components/responses/RateLimited'\n default:\n description: Error\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/connect.error'\n \"200\":\n description: Success\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.status_report.v1.UpdateStatusReportResponse'\n /rpc/openstatus.status_report.v1.StatusReportService/UpdateStatusReportUpdate:\n post:\n tags:\n - StatusReportService\n summary: UpdateStatusReportUpdate\n description: Edits an existing status report update in place. Editing the status or date of the latest update re-derives the report's overall status. Subscribers are not notified.\n operationId: StatusReportService_UpdateStatusReportUpdate\n requestBody:\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.status_report.v1.UpdateStatusReportUpdateRequest'\n required: true\n responses:\n \"429\":\n $ref: '#/components/responses/RateLimited'\n default:\n description: Error\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/connect.error'\n \"200\":\n description: Success\n content:\n application/json:\n schema:\n $ref: '#/components/schemas/openstatus.status_report.v1.UpdateStatusReportUpdateResponse'\n"; diff --git a/apps/server/static/openapi.json b/apps/server/static/openapi.json index 9f2837e6b..e4b08d0f1 100644 --- a/apps/server/static/openapi.json +++ b/apps/server/static/openapi.json @@ -1822,18 +1822,18 @@ "timeout": { "type": ["integer", "string"], "title": "timeout", - "maximum": 120000, + "maximum": 300000, "minimum": 0, "format": "int64", - "description": "Timeout in milliseconds (0-120000, defaults to 45000)." + "description": "Timeout in milliseconds (0-300000, defaults to 45000)." }, "degradedAt": { "type": ["integer", "string", "null"], "title": "degraded_at", - "maximum": 120000, + "maximum": 300000, "minimum": 0, "format": "int64", - "description": "Latency threshold for degraded status in milliseconds (optional, 0-120000)." + "description": "Latency threshold for degraded status in milliseconds (optional, 0-300000)." }, "retry": { "type": ["integer", "string"], @@ -1965,18 +1965,18 @@ "timeout": { "type": ["integer", "string"], "title": "timeout", - "maximum": 120000, + "maximum": 300000, "minimum": 0, "format": "int64", - "description": "Timeout in milliseconds (0-120000, defaults to 45000)." + "description": "Timeout in milliseconds (0-300000, defaults to 45000)." }, "degradedAt": { "type": ["integer", "string", "null"], "title": "degraded_at", - "maximum": 120000, + "maximum": 300000, "minimum": 0, "format": "int64", - "description": "Latency threshold for degraded status in milliseconds (optional, 0-120000)." + "description": "Latency threshold for degraded status in milliseconds (optional, 0-300000)." }, "retry": { "type": ["integer", "string"], @@ -2344,18 +2344,18 @@ "timeout": { "type": ["integer", "string"], "title": "timeout", - "maximum": 120000, + "maximum": 300000, "minimum": 0, "format": "int64", - "description": "Timeout in milliseconds (0-120000, defaults to 45000)." + "description": "Timeout in milliseconds (0-300000, defaults to 45000)." }, "degradedAt": { "type": ["integer", "string", "null"], "title": "degraded_at", - "maximum": 120000, + "maximum": 300000, "minimum": 0, "format": "int64", - "description": "Latency threshold for degraded status in milliseconds (optional, 0-120000)." + "description": "Latency threshold for degraded status in milliseconds (optional, 0-300000)." }, "retry": { "type": ["integer", "string"], @@ -2755,18 +2755,18 @@ "timeout": { "type": ["integer", "string"], "title": "timeout", - "maximum": 120000, + "maximum": 300000, "minimum": 0, "format": "int64", - "description": "Timeout in milliseconds (0-120000, defaults to 45000)." + "description": "Timeout in milliseconds (0-300000, defaults to 45000)." }, "degradedAt": { "type": ["integer", "string", "null"], "title": "degraded_at", - "maximum": 120000, + "maximum": 300000, "minimum": 0, "format": "int64", - "description": "Latency threshold for degraded status in milliseconds (optional, 0-120000)." + "description": "Latency threshold for degraded status in milliseconds (optional, 0-300000)." }, "retry": { "type": ["integer", "string"], @@ -3265,18 +3265,18 @@ "timeout": { "type": ["integer", "string"], "title": "timeout", - "maximum": 120000, + "maximum": 300000, "minimum": 0, "format": "int64", - "description": "Timeout in milliseconds (0-120000, defaults to 45000)." + "description": "Timeout in milliseconds (0-300000, defaults to 45000)." }, "degradedAt": { "type": ["integer", "string", "null"], "title": "degraded_at", - "maximum": 120000, + "maximum": 300000, "minimum": 0, "format": "int64", - "description": "Latency threshold for degraded status in milliseconds (optional, 0-120000)." + "description": "Latency threshold for degraded status in milliseconds (optional, 0-300000)." }, "retry": { "type": ["integer", "string"], diff --git a/apps/server/static/openapi.yaml b/apps/server/static/openapi.yaml index 9726fe555..1166177c0 100644 --- a/apps/server/static/openapi.yaml +++ b/apps/server/static/openapi.yaml @@ -1513,20 +1513,20 @@ components: - integer - string title: timeout - maximum: 120000 + maximum: 300000 minimum: 0 format: int64 - description: Timeout in milliseconds (0-120000, defaults to 45000). + description: Timeout in milliseconds (0-300000, defaults to 45000). degradedAt: type: - integer - string - "null" title: degraded_at - maximum: 120000 + maximum: 300000 minimum: 0 format: int64 - description: Latency threshold for degraded status in milliseconds (optional, 0-120000). + description: Latency threshold for degraded status in milliseconds (optional, 0-300000). retry: type: - integer @@ -1645,20 +1645,20 @@ components: - integer - string title: timeout - maximum: 120000 + maximum: 300000 minimum: 0 format: int64 - description: Timeout in milliseconds (0-120000, defaults to 45000). + description: Timeout in milliseconds (0-300000, defaults to 45000). degradedAt: type: - integer - string - "null" title: degraded_at - maximum: 120000 + maximum: 300000 minimum: 0 format: int64 - description: Latency threshold for degraded status in milliseconds (optional, 0-120000). + description: Latency threshold for degraded status in milliseconds (optional, 0-300000). retry: type: - integer @@ -1986,20 +1986,20 @@ components: - integer - string title: timeout - maximum: 120000 + maximum: 300000 minimum: 0 format: int64 - description: Timeout in milliseconds (0-120000, defaults to 45000). + description: Timeout in milliseconds (0-300000, defaults to 45000). degradedAt: type: - integer - string - "null" title: degraded_at - maximum: 120000 + maximum: 300000 minimum: 0 format: int64 - description: Latency threshold for degraded status in milliseconds (optional, 0-120000). + description: Latency threshold for degraded status in milliseconds (optional, 0-300000). retry: type: - integer @@ -2347,20 +2347,20 @@ components: - integer - string title: timeout - maximum: 120000 + maximum: 300000 minimum: 0 format: int64 - description: Timeout in milliseconds (0-120000, defaults to 45000). + description: Timeout in milliseconds (0-300000, defaults to 45000). degradedAt: type: - integer - string - "null" title: degraded_at - maximum: 120000 + maximum: 300000 minimum: 0 format: int64 - description: Latency threshold for degraded status in milliseconds (optional, 0-120000). + description: Latency threshold for degraded status in milliseconds (optional, 0-300000). retry: type: - integer @@ -2801,20 +2801,20 @@ components: - integer - string title: timeout - maximum: 120000 + maximum: 300000 minimum: 0 format: int64 - description: Timeout in milliseconds (0-120000, defaults to 45000). + description: Timeout in milliseconds (0-300000, defaults to 45000). degradedAt: type: - integer - string - "null" title: degraded_at - maximum: 120000 + maximum: 300000 minimum: 0 format: int64 - description: Latency threshold for degraded status in milliseconds (optional, 0-120000). + description: Latency threshold for degraded status in milliseconds (optional, 0-300000). retry: type: - integer diff --git a/apps/web/src/app/api/checker/cron/_cron.ts b/apps/web/src/app/api/checker/cron/_cron.ts index 370077d69..96e7ab95e 100644 --- a/apps/web/src/app/api/checker/cron/_cron.ts +++ b/apps/web/src/app/api/checker/cron/_cron.ts @@ -262,6 +262,9 @@ const createCronTask = async ({ scheduleTime: { seconds: timestamp / 1000, }, + // The checker answers only after its whole retry loop, which a 5m timeout + // can stretch well past Cloud Tasks' 10m default; 30m is the max allowed. + dispatchDeadline: { seconds: 30 * 60 }, }; const request = { parent: parent, task: newTask }; diff --git a/apps/web/src/content/pages/docs/reference/http-monitor.mdx b/apps/web/src/content/pages/docs/reference/http-monitor.mdx index d93b12a73..1a227f1ee 100644 --- a/apps/web/src/content/pages/docs/reference/http-monitor.mdx +++ b/apps/web/src/content/pages/docs/reference/http-monitor.mdx @@ -133,7 +133,7 @@ Which frequencies you can select depends on your plan: **Hobby** offers `10m`, ` **Type:** Duration (optional) **Default:** `45 seconds` -**Range:** 0–120000 ms +**Range:** 0–300000 ms (5 minutes) The maximum duration to wait for the HTTP request to complete. If the request exceeds this time, it is considered a failure. diff --git a/apps/web/src/content/pages/docs/sdk/nodejs/monitor-service.mdx b/apps/web/src/content/pages/docs/sdk/nodejs/monitor-service.mdx index 08be19694..3b2a33aa0 100644 --- a/apps/web/src/content/pages/docs/sdk/nodejs/monitor-service.mdx +++ b/apps/web/src/content/pages/docs/sdk/nodejs/monitor-service.mdx @@ -98,7 +98,7 @@ const { monitor } = await client.monitor.v1.MonitorService.updateHTTPMonitor({ | `method` | HTTPMethod | No | HTTP method (default: GET) | | `body` | string | No | Request body | | `headers` | Headers[] | No | Custom headers `{ key, value }[]` | -| `timeout` | bigint | No | Timeout in ms (default: 45000, max: 120000) | +| `timeout` | bigint | No | Timeout in ms (default: 45000, max: 300000) | | `retry` | bigint | No | Retry attempts (default: 3, max: 10) | | `followRedirects` | boolean | No | Follow redirects (default: true) | | `regions` | Region[] | No | Regions for checks | @@ -155,7 +155,7 @@ const { monitor } = await client.monitor.v1.MonitorService.updateTCPMonitor({ | `name` | string | Yes | Monitor name (max 256 chars) | | `uri` | string | Yes | `host:port` to monitor (max 2048 chars) | | `periodicity` | Periodicity | Yes | Check interval | -| `timeout` | bigint | No | Timeout in ms (default: 45000, max: 120000) | +| `timeout` | bigint | No | Timeout in ms (default: 45000, max: 300000) | | `retry` | bigint | No | Retry attempts (default: 3, max: 10) | | `regions` | Region[] | No | Regions for checks | | `active` | boolean | No | Enable monitoring (default: false) | @@ -221,7 +221,7 @@ const { monitor } = await client.monitor.v1.MonitorService.updateDNSMonitor({ | `name` | string | Yes | Monitor name (max 256 chars) | | `uri` | string | Yes | Domain to resolve (max 2048 chars) | | `periodicity` | Periodicity | Yes | Check interval | -| `timeout` | bigint | No | Timeout in ms (default: 45000, max: 120000) | +| `timeout` | bigint | No | Timeout in ms (default: 45000, max: 300000) | | `retry` | bigint | No | Retry attempts (default: 3, max: 10) | | `regions` | Region[] | No | Regions for checks | | `active` | boolean | No | Enable monitoring (default: false) | diff --git a/apps/workflows/src/cron/checker.ts b/apps/workflows/src/cron/checker.ts index b7a0a4350..837124e77 100644 --- a/apps/workflows/src/cron/checker.ts +++ b/apps/workflows/src/cron/checker.ts @@ -431,6 +431,9 @@ const createCronTask = async ( scheduleTime: { seconds: timestamp / 1000, }, + // The checker answers only after its whole retry loop, which a 5m timeout + // can stretch well past Cloud Tasks' 10m default; 30m is the max allowed. + dispatchDeadline: { seconds: 30 * 60 }, }; const request = { parent: parent, task: newTask }; diff --git a/packages/db/src/schema/monitors/validation.ts b/packages/db/src/schema/monitors/validation.ts index 16a01bf21..e3cf7eb8e 100644 --- a/packages/db/src/schema/monitors/validation.ts +++ b/packages/db/src/schema/monitors/validation.ts @@ -85,8 +85,8 @@ export const insertMonitorSchema = createInsertSchema(monitor, { statusAssertions: z.array(assertions.statusAssertion).optional(), headerAssertions: z.array(assertions.headerAssertion).optional(), textBodyAssertions: z.array(assertions.textBodyAssertion).optional(), - timeout: z.coerce.number().gte(0).lte(60000).prefault(45000), - degradedAfter: z.coerce.number().gte(0).lte(60000).nullish(), + timeout: z.coerce.number().gte(0).lte(300_000).prefault(45000), + degradedAfter: z.coerce.number().gte(0).lte(300_000).nullish(), }); export type InsertMonitor = z.infer; diff --git a/packages/proto/api/openstatus/monitor/v1/dns_monitor.proto b/packages/proto/api/openstatus/monitor/v1/dns_monitor.proto index ad94cffb3..9f7734eaf 100644 --- a/packages/proto/api/openstatus/monitor/v1/dns_monitor.proto +++ b/packages/proto/api/openstatus/monitor/v1/dns_monitor.proto @@ -38,16 +38,16 @@ message DNSMonitor { not_in: [0] }]; - // Timeout in milliseconds (0-120000, defaults to 45000). + // Timeout in milliseconds (0-300000, defaults to 45000). int64 timeout = 5 [(buf.validate.field).int64 = { gte: 0 - lte: 120000 + lte: 300000 }]; - // Latency threshold for degraded status in milliseconds (optional, 0-120000). + // Latency threshold for degraded status in milliseconds (optional, 0-300000). optional int64 degraded_at = 6 [(buf.validate.field).int64 = { gte: 0 - lte: 120000 + lte: 300000 }]; // Number of retry attempts (0-10, defaults to 3). diff --git a/packages/proto/api/openstatus/monitor/v1/grpc_monitor.proto b/packages/proto/api/openstatus/monitor/v1/grpc_monitor.proto index 18048422e..304d8550f 100644 --- a/packages/proto/api/openstatus/monitor/v1/grpc_monitor.proto +++ b/packages/proto/api/openstatus/monitor/v1/grpc_monitor.proto @@ -54,16 +54,16 @@ message GRPCMonitor { not_in: [0] }]; - // Timeout in milliseconds (0-120000, defaults to 45000). + // Timeout in milliseconds (0-300000, defaults to 45000). int64 timeout = 5 [(buf.validate.field).int64 = { gte: 0 - lte: 120000 + lte: 300000 }]; - // Latency threshold for degraded status in milliseconds (optional, 0-120000). + // Latency threshold for degraded status in milliseconds (optional, 0-300000). optional int64 degraded_at = 6 [(buf.validate.field).int64 = { gte: 0 - lte: 120000 + lte: 300000 }]; // Number of retry attempts (0-10, defaults to 3). diff --git a/packages/proto/api/openstatus/monitor/v1/http_monitor.proto b/packages/proto/api/openstatus/monitor/v1/http_monitor.proto index 49a29d497..d459d3afe 100644 --- a/packages/proto/api/openstatus/monitor/v1/http_monitor.proto +++ b/packages/proto/api/openstatus/monitor/v1/http_monitor.proto @@ -84,16 +84,16 @@ message HTTPMonitor { (gnostic.openapi.v3.property) = {example: {yaml: "{\"key\": \"value\"}"}} ]; - // Timeout in milliseconds (0-120000, defaults to 45000). + // Timeout in milliseconds (0-300000, defaults to 45000). int64 timeout = 7 [(buf.validate.field).int64 = { gte: 0 - lte: 120000 + lte: 300000 }]; - // Latency threshold for degraded status in milliseconds (optional, 0-120000). + // Latency threshold for degraded status in milliseconds (optional, 0-300000). optional int64 degraded_at = 8 [(buf.validate.field).int64 = { gte: 0 - lte: 120000 + lte: 300000 }]; // Number of retry attempts (0-10, defaults to 3). diff --git a/packages/proto/api/openstatus/monitor/v1/icmp_monitor.proto b/packages/proto/api/openstatus/monitor/v1/icmp_monitor.proto index 0982563a3..74376e53c 100644 --- a/packages/proto/api/openstatus/monitor/v1/icmp_monitor.proto +++ b/packages/proto/api/openstatus/monitor/v1/icmp_monitor.proto @@ -37,16 +37,16 @@ message ICMPMonitor { not_in: [0] }]; - // Timeout in milliseconds (0-120000, defaults to 45000). + // Timeout in milliseconds (0-300000, defaults to 45000). int64 timeout = 5 [(buf.validate.field).int64 = { gte: 0 - lte: 120000 + lte: 300000 }]; - // Latency threshold for degraded status in milliseconds (optional, 0-120000). + // Latency threshold for degraded status in milliseconds (optional, 0-300000). optional int64 degraded_at = 6 [(buf.validate.field).int64 = { gte: 0 - lte: 120000 + lte: 300000 }]; // Number of retry attempts (0-10, defaults to 3). diff --git a/packages/proto/api/openstatus/monitor/v1/tcp_monitor.proto b/packages/proto/api/openstatus/monitor/v1/tcp_monitor.proto index 0ef71475a..aca093865 100644 --- a/packages/proto/api/openstatus/monitor/v1/tcp_monitor.proto +++ b/packages/proto/api/openstatus/monitor/v1/tcp_monitor.proto @@ -37,16 +37,16 @@ message TCPMonitor { not_in: [0] }]; - // Timeout in milliseconds (0-120000, defaults to 45000). + // Timeout in milliseconds (0-300000, defaults to 45000). int64 timeout = 5 [(buf.validate.field).int64 = { gte: 0 - lte: 120000 + lte: 300000 }]; - // Latency threshold for degraded status in milliseconds (optional, 0-120000). + // Latency threshold for degraded status in milliseconds (optional, 0-300000). optional int64 degraded_at = 6 [(buf.validate.field).int64 = { gte: 0 - lte: 120000 + lte: 300000 }]; // Number of retry attempts (0-10, defaults to 3). diff --git a/packages/proto/gen/openapi.yaml b/packages/proto/gen/openapi.yaml index 9726fe555..1166177c0 100644 --- a/packages/proto/gen/openapi.yaml +++ b/packages/proto/gen/openapi.yaml @@ -1513,20 +1513,20 @@ components: - integer - string title: timeout - maximum: 120000 + maximum: 300000 minimum: 0 format: int64 - description: Timeout in milliseconds (0-120000, defaults to 45000). + description: Timeout in milliseconds (0-300000, defaults to 45000). degradedAt: type: - integer - string - "null" title: degraded_at - maximum: 120000 + maximum: 300000 minimum: 0 format: int64 - description: Latency threshold for degraded status in milliseconds (optional, 0-120000). + description: Latency threshold for degraded status in milliseconds (optional, 0-300000). retry: type: - integer @@ -1645,20 +1645,20 @@ components: - integer - string title: timeout - maximum: 120000 + maximum: 300000 minimum: 0 format: int64 - description: Timeout in milliseconds (0-120000, defaults to 45000). + description: Timeout in milliseconds (0-300000, defaults to 45000). degradedAt: type: - integer - string - "null" title: degraded_at - maximum: 120000 + maximum: 300000 minimum: 0 format: int64 - description: Latency threshold for degraded status in milliseconds (optional, 0-120000). + description: Latency threshold for degraded status in milliseconds (optional, 0-300000). retry: type: - integer @@ -1986,20 +1986,20 @@ components: - integer - string title: timeout - maximum: 120000 + maximum: 300000 minimum: 0 format: int64 - description: Timeout in milliseconds (0-120000, defaults to 45000). + description: Timeout in milliseconds (0-300000, defaults to 45000). degradedAt: type: - integer - string - "null" title: degraded_at - maximum: 120000 + maximum: 300000 minimum: 0 format: int64 - description: Latency threshold for degraded status in milliseconds (optional, 0-120000). + description: Latency threshold for degraded status in milliseconds (optional, 0-300000). retry: type: - integer @@ -2347,20 +2347,20 @@ components: - integer - string title: timeout - maximum: 120000 + maximum: 300000 minimum: 0 format: int64 - description: Timeout in milliseconds (0-120000, defaults to 45000). + description: Timeout in milliseconds (0-300000, defaults to 45000). degradedAt: type: - integer - string - "null" title: degraded_at - maximum: 120000 + maximum: 300000 minimum: 0 format: int64 - description: Latency threshold for degraded status in milliseconds (optional, 0-120000). + description: Latency threshold for degraded status in milliseconds (optional, 0-300000). retry: type: - integer @@ -2801,20 +2801,20 @@ components: - integer - string title: timeout - maximum: 120000 + maximum: 300000 minimum: 0 format: int64 - description: Timeout in milliseconds (0-120000, defaults to 45000). + description: Timeout in milliseconds (0-300000, defaults to 45000). degradedAt: type: - integer - string - "null" title: degraded_at - maximum: 120000 + maximum: 300000 minimum: 0 format: int64 - description: Latency threshold for degraded status in milliseconds (optional, 0-120000). + description: Latency threshold for degraded status in milliseconds (optional, 0-300000). retry: type: - integer diff --git a/packages/proto/gen/ts/openstatus/monitor/v1/dns_monitor_pb.ts b/packages/proto/gen/ts/openstatus/monitor/v1/dns_monitor_pb.ts index 88a3a6342..8e52f22fe 100644 --- a/packages/proto/gen/ts/openstatus/monitor/v1/dns_monitor_pb.ts +++ b/packages/proto/gen/ts/openstatus/monitor/v1/dns_monitor_pb.ts @@ -18,7 +18,7 @@ import type { Message } from "@bufbuild/protobuf"; * Describes the file openstatus/monitor/v1/dns_monitor.proto. */ export const file_openstatus_monitor_v1_dns_monitor: GenFile = /*@__PURE__*/ - fileDesc("CidvcGVuc3RhdHVzL21vbml0b3IvdjEvZG5zX21vbml0b3IucHJvdG8SFW9wZW5zdGF0dXMubW9uaXRvci52MSLLBQoKRE5TTW9uaXRvchIKCgJpZBgBIAEoCRIzCgRuYW1lGAIgASgJQiW6Rxg6FhIURE5TIFJlc29sdXRpb24gQ2hlY2u6SAdyBRABGIACEikKA3VyaRgDIAEoCUIcukcPOg0SC2V4YW1wbGUuY29tukgHcgUQARiAEBJBCgtwZXJpb2RpY2l0eRgEIAEoDjIiLm9wZW5zdGF0dXMubW9uaXRvci52MS5QZXJpb2RpY2l0eUIIukgFggECIAASHAoHdGltZW91dBgFIAEoA0ILukgIIgYYwKkHKAASJQoLZGVncmFkZWRfYXQYBiABKANCC7pICCIGGMCpBygASACIAQESGAoFcmV0cnkYByABKANCCbpIBiIEGAooABJLChFyZWNvcmRfYXNzZXJ0aW9ucxgIIAMoCzImLm9wZW5zdGF0dXMubW9uaXRvci52MS5SZWNvcmRBc3NlcnRpb25CCLpIBZIBAhAKEiIKC2Rlc2NyaXB0aW9uGAkgASgJQgi6SAVyAxiACEgBiAEBEhMKBmFjdGl2ZRgKIAEoCEgCiAEBEhMKBnB1YmxpYxgLIAEoCEgDiAEBEj8KB3JlZ2lvbnMYDCADKA4yHS5vcGVuc3RhdHVzLm1vbml0b3IudjEuUmVnaW9uQg+6SAySAQkQHCIFggECIAASQgoOb3Blbl90ZWxlbWV0cnkYDSABKAsyKi5vcGVuc3RhdHVzLm1vbml0b3IudjEuT3BlblRlbGVtZXRyeUNvbmZpZxI0CgZzdGF0dXMYDiABKA4yJC5vcGVuc3RhdHVzLm1vbml0b3IudjEuTW9uaXRvclN0YXR1cxIjChRwcml2YXRlX2xvY2F0aW9uX2lkcxgPIAMoCUIFukcCGAFCDgoMX2RlZ3JhZGVkX2F0Qg4KDF9kZXNjcmlwdGlvbkIJCgdfYWN0aXZlQgkKB19wdWJsaWNCU1pRZ2l0aHViLmNvbS9vcGVuc3RhdHVzaHEvb3BlbnN0YXR1cy9wYWNrYWdlcy9wcm90by9vcGVuc3RhdHVzL21vbml0b3IvdjE7bW9uaXRvcnYxYgZwcm90bzM", [file_buf_validate_validate, file_gnostic_openapi_v3_annotations, file_openstatus_monitor_v1_assertions, file_openstatus_monitor_v1_http_monitor, file_openstatus_monitor_v1_monitor]); + fileDesc("CidvcGVuc3RhdHVzL21vbml0b3IvdjEvZG5zX21vbml0b3IucHJvdG8SFW9wZW5zdGF0dXMubW9uaXRvci52MSLLBQoKRE5TTW9uaXRvchIKCgJpZBgBIAEoCRIzCgRuYW1lGAIgASgJQiW6Rxg6FhIURE5TIFJlc29sdXRpb24gQ2hlY2u6SAdyBRABGIACEikKA3VyaRgDIAEoCUIcukcPOg0SC2V4YW1wbGUuY29tukgHcgUQARiAEBJBCgtwZXJpb2RpY2l0eRgEIAEoDjIiLm9wZW5zdGF0dXMubW9uaXRvci52MS5QZXJpb2RpY2l0eUIIukgFggECIAASHAoHdGltZW91dBgFIAEoA0ILukgIIgYY4KcSKAASJQoLZGVncmFkZWRfYXQYBiABKANCC7pICCIGGOCnEigASACIAQESGAoFcmV0cnkYByABKANCCbpIBiIEGAooABJLChFyZWNvcmRfYXNzZXJ0aW9ucxgIIAMoCzImLm9wZW5zdGF0dXMubW9uaXRvci52MS5SZWNvcmRBc3NlcnRpb25CCLpIBZIBAhAKEiIKC2Rlc2NyaXB0aW9uGAkgASgJQgi6SAVyAxiACEgBiAEBEhMKBmFjdGl2ZRgKIAEoCEgCiAEBEhMKBnB1YmxpYxgLIAEoCEgDiAEBEj8KB3JlZ2lvbnMYDCADKA4yHS5vcGVuc3RhdHVzLm1vbml0b3IudjEuUmVnaW9uQg+6SAySAQkQHCIFggECIAASQgoOb3Blbl90ZWxlbWV0cnkYDSABKAsyKi5vcGVuc3RhdHVzLm1vbml0b3IudjEuT3BlblRlbGVtZXRyeUNvbmZpZxI0CgZzdGF0dXMYDiABKA4yJC5vcGVuc3RhdHVzLm1vbml0b3IudjEuTW9uaXRvclN0YXR1cxIjChRwcml2YXRlX2xvY2F0aW9uX2lkcxgPIAMoCUIFukcCGAFCDgoMX2RlZ3JhZGVkX2F0Qg4KDF9kZXNjcmlwdGlvbkIJCgdfYWN0aXZlQgkKB19wdWJsaWNCU1pRZ2l0aHViLmNvbS9vcGVuc3RhdHVzaHEvb3BlbnN0YXR1cy9wYWNrYWdlcy9wcm90by9vcGVuc3RhdHVzL21vbml0b3IvdjE7bW9uaXRvcnYxYgZwcm90bzM", [file_buf_validate_validate, file_gnostic_openapi_v3_annotations, file_openstatus_monitor_v1_assertions, file_openstatus_monitor_v1_http_monitor, file_openstatus_monitor_v1_monitor]); /** * DNSMonitor defines the configuration for a DNS monitor. @@ -55,14 +55,14 @@ export type DNSMonitor = Message<"openstatus.monitor.v1.DNSMonitor"> & { periodicity: Periodicity; /** - * Timeout in milliseconds (0-120000, defaults to 45000). + * Timeout in milliseconds (0-300000, defaults to 45000). * * @generated from field: int64 timeout = 5; */ timeout: bigint; /** - * Latency threshold for degraded status in milliseconds (optional, 0-120000). + * Latency threshold for degraded status in milliseconds (optional, 0-300000). * * @generated from field: optional int64 degraded_at = 6; */ diff --git a/packages/proto/gen/ts/openstatus/monitor/v1/grpc_monitor_pb.ts b/packages/proto/gen/ts/openstatus/monitor/v1/grpc_monitor_pb.ts index ad958782e..d3b94c273 100644 --- a/packages/proto/gen/ts/openstatus/monitor/v1/grpc_monitor_pb.ts +++ b/packages/proto/gen/ts/openstatus/monitor/v1/grpc_monitor_pb.ts @@ -16,7 +16,7 @@ import type { Message } from "@bufbuild/protobuf"; * Describes the file openstatus/monitor/v1/grpc_monitor.proto. */ export const file_openstatus_monitor_v1_grpc_monitor: GenFile = /*@__PURE__*/ - fileDesc("CihvcGVuc3RhdHVzL21vbml0b3IvdjEvZ3JwY19tb25pdG9yLnByb3RvEhVvcGVuc3RhdHVzLm1vbml0b3IudjEi/QYKC0dSUENNb25pdG9yEgoKAmlkGAEgASgJEiwKBG5hbWUYAiABKAlCHrpHEToPEg1DaGVja291dCBnUlBDukgHcgUQARiAAhJcCgN1cmkYAyABKAlCT7pHFzoVEhNhcGkuZXhhbXBsZS5jb206NDQzukgycjAQARiAEDIpXihcW1swLTlhLWZBLUY6XStcXXxbXjovXHNdKyk6WzAtOV17MSw1fSQSQQoLcGVyaW9kaWNpdHkYBCABKA4yIi5vcGVuc3RhdHVzLm1vbml0b3IudjEuUGVyaW9kaWNpdHlCCLpIBYIBAiAAEhwKB3RpbWVvdXQYBSABKANCC7pICCIGGMCpBygAEiUKC2RlZ3JhZGVkX2F0GAYgASgDQgu6SAgiBhjAqQcoAEgAiAEBEhgKBXJldHJ5GAcgASgDQgm6SAYiBBgKKAASIgoLZGVzY3JpcHRpb24YCCABKAlCCLpIBXIDGIAISAGIAQESEwoGYWN0aXZlGAkgASgISAKIAQESEwoGcHVibGljGAogASgISAOIAQESPwoHcmVnaW9ucxgLIAMoDjIdLm9wZW5zdGF0dXMubW9uaXRvci52MS5SZWdpb25CD7pIDJIBCRAcIgWCAQIgABJCCg5vcGVuX3RlbGVtZXRyeRgMIAEoCzIqLm9wZW5zdGF0dXMubW9uaXRvci52MS5PcGVuVGVsZW1ldHJ5Q29uZmlnEjQKBnN0YXR1cxgNIAEoDjIkLm9wZW5zdGF0dXMubW9uaXRvci52MS5Nb25pdG9yU3RhdHVzEiMKFHByaXZhdGVfbG9jYXRpb25faWRzGA4gAygJQgW6RwIYARJACgdzZXJ2aWNlGA8gASgJQiq6Rx86HRIbY2hlY2tvdXQudjEuQ2hlY2tvdXRTZXJ2aWNlukgFcgMYgARIBIgBARI5Cgh0bHNfbW9kZRgQIAEoDjIiLm9wZW5zdGF0dXMubW9uaXRvci52MS5HUlBDVGxzTW9kZUgFiAEBEjoKCG1ldGFkYXRhGBEgAygLMh4ub3BlbnN0YXR1cy5tb25pdG9yLnYxLkhlYWRlcnNCCLpIBZIBAhAUQg4KDF9kZWdyYWRlZF9hdEIOCgxfZGVzY3JpcHRpb25CCQoHX2FjdGl2ZUIJCgdfcHVibGljQgoKCF9zZXJ2aWNlQgsKCV90bHNfbW9kZSqAAQoLR1JQQ1Rsc01vZGUSHQoZR1JQQ19UTFNfTU9ERV9VTlNQRUNJRklFRBAAEhUKEUdSUENfVExTX01PREVfVExTEAESGwoXR1JQQ19UTFNfTU9ERV9QTEFJTlRFWFQQAhIeChpHUlBDX1RMU19NT0RFX1RMU19JTlNFQ1VSRRADQlNaUWdpdGh1Yi5jb20vb3BlbnN0YXR1c2hxL29wZW5zdGF0dXMvcGFja2FnZXMvcHJvdG8vb3BlbnN0YXR1cy9tb25pdG9yL3YxO21vbml0b3J2MWIGcHJvdG8z", [file_buf_validate_validate, file_gnostic_openapi_v3_annotations, file_openstatus_monitor_v1_http_monitor, file_openstatus_monitor_v1_monitor]); + fileDesc("CihvcGVuc3RhdHVzL21vbml0b3IvdjEvZ3JwY19tb25pdG9yLnByb3RvEhVvcGVuc3RhdHVzLm1vbml0b3IudjEi/QYKC0dSUENNb25pdG9yEgoKAmlkGAEgASgJEiwKBG5hbWUYAiABKAlCHrpHEToPEg1DaGVja291dCBnUlBDukgHcgUQARiAAhJcCgN1cmkYAyABKAlCT7pHFzoVEhNhcGkuZXhhbXBsZS5jb206NDQzukgycjAQARiAEDIpXihcW1swLTlhLWZBLUY6XStcXXxbXjovXHNdKyk6WzAtOV17MSw1fSQSQQoLcGVyaW9kaWNpdHkYBCABKA4yIi5vcGVuc3RhdHVzLm1vbml0b3IudjEuUGVyaW9kaWNpdHlCCLpIBYIBAiAAEhwKB3RpbWVvdXQYBSABKANCC7pICCIGGOCnEigAEiUKC2RlZ3JhZGVkX2F0GAYgASgDQgu6SAgiBhjgpxIoAEgAiAEBEhgKBXJldHJ5GAcgASgDQgm6SAYiBBgKKAASIgoLZGVzY3JpcHRpb24YCCABKAlCCLpIBXIDGIAISAGIAQESEwoGYWN0aXZlGAkgASgISAKIAQESEwoGcHVibGljGAogASgISAOIAQESPwoHcmVnaW9ucxgLIAMoDjIdLm9wZW5zdGF0dXMubW9uaXRvci52MS5SZWdpb25CD7pIDJIBCRAcIgWCAQIgABJCCg5vcGVuX3RlbGVtZXRyeRgMIAEoCzIqLm9wZW5zdGF0dXMubW9uaXRvci52MS5PcGVuVGVsZW1ldHJ5Q29uZmlnEjQKBnN0YXR1cxgNIAEoDjIkLm9wZW5zdGF0dXMubW9uaXRvci52MS5Nb25pdG9yU3RhdHVzEiMKFHByaXZhdGVfbG9jYXRpb25faWRzGA4gAygJQgW6RwIYARJACgdzZXJ2aWNlGA8gASgJQiq6Rx86HRIbY2hlY2tvdXQudjEuQ2hlY2tvdXRTZXJ2aWNlukgFcgMYgARIBIgBARI5Cgh0bHNfbW9kZRgQIAEoDjIiLm9wZW5zdGF0dXMubW9uaXRvci52MS5HUlBDVGxzTW9kZUgFiAEBEjoKCG1ldGFkYXRhGBEgAygLMh4ub3BlbnN0YXR1cy5tb25pdG9yLnYxLkhlYWRlcnNCCLpIBZIBAhAUQg4KDF9kZWdyYWRlZF9hdEIOCgxfZGVzY3JpcHRpb25CCQoHX2FjdGl2ZUIJCgdfcHVibGljQgoKCF9zZXJ2aWNlQgsKCV90bHNfbW9kZSqAAQoLR1JQQ1Rsc01vZGUSHQoZR1JQQ19UTFNfTU9ERV9VTlNQRUNJRklFRBAAEhUKEUdSUENfVExTX01PREVfVExTEAESGwoXR1JQQ19UTFNfTU9ERV9QTEFJTlRFWFQQAhIeChpHUlBDX1RMU19NT0RFX1RMU19JTlNFQ1VSRRADQlNaUWdpdGh1Yi5jb20vb3BlbnN0YXR1c2hxL29wZW5zdGF0dXMvcGFja2FnZXMvcHJvdG8vb3BlbnN0YXR1cy9tb25pdG9yL3YxO21vbml0b3J2MWIGcHJvdG8z", [file_buf_validate_validate, file_gnostic_openapi_v3_annotations, file_openstatus_monitor_v1_http_monitor, file_openstatus_monitor_v1_monitor]); /** * GRPCMonitor defines the configuration for a gRPC health check monitor. @@ -54,14 +54,14 @@ export type GRPCMonitor = Message<"openstatus.monitor.v1.GRPCMonitor"> & { periodicity: Periodicity; /** - * Timeout in milliseconds (0-120000, defaults to 45000). + * Timeout in milliseconds (0-300000, defaults to 45000). * * @generated from field: int64 timeout = 5; */ timeout: bigint; /** - * Latency threshold for degraded status in milliseconds (optional, 0-120000). + * Latency threshold for degraded status in milliseconds (optional, 0-300000). * * @generated from field: optional int64 degraded_at = 6; */ diff --git a/packages/proto/gen/ts/openstatus/monitor/v1/http_monitor_pb.ts b/packages/proto/gen/ts/openstatus/monitor/v1/http_monitor_pb.ts index 574766d95..eec116639 100644 --- a/packages/proto/gen/ts/openstatus/monitor/v1/http_monitor_pb.ts +++ b/packages/proto/gen/ts/openstatus/monitor/v1/http_monitor_pb.ts @@ -16,7 +16,7 @@ import type { Message } from "@bufbuild/protobuf"; * Describes the file openstatus/monitor/v1/http_monitor.proto. */ export const file_openstatus_monitor_v1_http_monitor: GenFile = /*@__PURE__*/ - fileDesc("CihvcGVuc3RhdHVzL21vbml0b3IvdjEvaHR0cF9tb25pdG9yLnByb3RvEhVvcGVuc3RhdHVzLm1vbml0b3IudjEiWgoHSGVhZGVycxIoCgNrZXkYASABKAlCG7pHEToPEg1BdXRob3JpemF0aW9uukgEcgIQARIlCgV2YWx1ZRgCIAEoCUIWukcTOhESD0JlYXJlciB0b2tlbjEyMyJsChNPcGVuVGVsZW1ldHJ5Q29uZmlnEhoKCGVuZHBvaW50GAEgASgJQgi6SAVyAxiAEBI5CgdoZWFkZXJzGAIgAygLMh4ub3BlbnN0YXR1cy5tb25pdG9yLnYxLkhlYWRlcnNCCLpIBZIBAhAUItsICgtIVFRQTW9uaXRvchIKCgJpZBgBIAEoCRI6CgRuYW1lGAIgASgJQiy6Rx86HRIbUHJvZHVjdGlvbiBBUEkgSGVhbHRoIENoZWNrukgHcgUQARiAAhI/CgN1cmwYAyABKAlCMrpHIjogEh5odHRwczovL2FwaS5leGFtcGxlLmNvbS9oZWFsdGi6SApyCBABGIAQiAEBEkEKC3BlcmlvZGljaXR5GAQgASgOMiIub3BlbnN0YXR1cy5tb25pdG9yLnYxLlBlcmlvZGljaXR5Qgi6SAWCAQIgABI7CgZtZXRob2QYBSABKA4yIS5vcGVuc3RhdHVzLm1vbml0b3IudjEuSFRUUE1ldGhvZEIIukgFggECIAASJQoEYm9keRgGIAEoCUIXukcUOhISEHsia2V5IjogInZhbHVlIn0SHAoHdGltZW91dBgHIAEoA0ILukgIIgYYwKkHKAASJQoLZGVncmFkZWRfYXQYCCABKANCC7pICCIGGMCpBygASACIAQESGAoFcmV0cnkYCSABKANCCbpIBiIEGAooABIdChBmb2xsb3dfcmVkaXJlY3RzGAogASgISAGIAQESOQoHaGVhZGVycxgLIAMoCzIeLm9wZW5zdGF0dXMubW9uaXRvci52MS5IZWFkZXJzQgi6SAWSAQIQFBJUChZzdGF0dXNfY29kZV9hc3NlcnRpb25zGAwgAygLMioub3BlbnN0YXR1cy5tb25pdG9yLnYxLlN0YXR1c0NvZGVBc3NlcnRpb25CCLpIBZIBAhAKEkcKD2JvZHlfYXNzZXJ0aW9ucxgNIAMoCzIkLm9wZW5zdGF0dXMubW9uaXRvci52MS5Cb2R5QXNzZXJ0aW9uQgi6SAWSAQIQChJLChFoZWFkZXJfYXNzZXJ0aW9ucxgOIAMoCzImLm9wZW5zdGF0dXMubW9uaXRvci52MS5IZWFkZXJBc3NlcnRpb25CCLpIBZIBAhAKEiIKC2Rlc2NyaXB0aW9uGA8gASgJQgi6SAVyAxiACEgCiAEBEhMKBmFjdGl2ZRgQIAEoCEgDiAEBEhMKBnB1YmxpYxgRIAEoCEgEiAEBEj8KB3JlZ2lvbnMYEiADKA4yHS5vcGVuc3RhdHVzLm1vbml0b3IudjEuUmVnaW9uQg+6SAySAQkQHCIFggECIAASQgoOb3Blbl90ZWxlbWV0cnkYEyABKAsyKi5vcGVuc3RhdHVzLm1vbml0b3IudjEuT3BlblRlbGVtZXRyeUNvbmZpZxI0CgZzdGF0dXMYFCABKA4yJC5vcGVuc3RhdHVzLm1vbml0b3IudjEuTW9uaXRvclN0YXR1cxIjChRwcml2YXRlX2xvY2F0aW9uX2lkcxgVIAMoCUIFukcCGAFCDgoMX2RlZ3JhZGVkX2F0QhMKEV9mb2xsb3dfcmVkaXJlY3RzQg4KDF9kZXNjcmlwdGlvbkIJCgdfYWN0aXZlQgkKB19wdWJsaWMq9wEKCkhUVFBNZXRob2QSGwoXSFRUUF9NRVRIT0RfVU5TUEVDSUZJRUQQABITCg9IVFRQX01FVEhPRF9HRVQQARIUChBIVFRQX01FVEhPRF9QT1NUEAISFAoQSFRUUF9NRVRIT0RfSEVBRBADEhMKD0hUVFBfTUVUSE9EX1BVVBAEEhUKEUhUVFBfTUVUSE9EX1BBVENIEAUSFgoSSFRUUF9NRVRIT0RfREVMRVRFEAYSFQoRSFRUUF9NRVRIT0RfVFJBQ0UQBxIXChNIVFRQX01FVEhPRF9DT05ORUNUEAgSFwoTSFRUUF9NRVRIT0RfT1BUSU9OUxAJQlNaUWdpdGh1Yi5jb20vb3BlbnN0YXR1c2hxL29wZW5zdGF0dXMvcGFja2FnZXMvcHJvdG8vb3BlbnN0YXR1cy9tb25pdG9yL3YxO21vbml0b3J2MWIGcHJvdG8z", [file_buf_validate_validate, file_gnostic_openapi_v3_annotations, file_openstatus_monitor_v1_assertions, file_openstatus_monitor_v1_monitor]); + fileDesc("CihvcGVuc3RhdHVzL21vbml0b3IvdjEvaHR0cF9tb25pdG9yLnByb3RvEhVvcGVuc3RhdHVzLm1vbml0b3IudjEiWgoHSGVhZGVycxIoCgNrZXkYASABKAlCG7pHEToPEg1BdXRob3JpemF0aW9uukgEcgIQARIlCgV2YWx1ZRgCIAEoCUIWukcTOhESD0JlYXJlciB0b2tlbjEyMyJsChNPcGVuVGVsZW1ldHJ5Q29uZmlnEhoKCGVuZHBvaW50GAEgASgJQgi6SAVyAxiAEBI5CgdoZWFkZXJzGAIgAygLMh4ub3BlbnN0YXR1cy5tb25pdG9yLnYxLkhlYWRlcnNCCLpIBZIBAhAUItsICgtIVFRQTW9uaXRvchIKCgJpZBgBIAEoCRI6CgRuYW1lGAIgASgJQiy6Rx86HRIbUHJvZHVjdGlvbiBBUEkgSGVhbHRoIENoZWNrukgHcgUQARiAAhI/CgN1cmwYAyABKAlCMrpHIjogEh5odHRwczovL2FwaS5leGFtcGxlLmNvbS9oZWFsdGi6SApyCBABGIAQiAEBEkEKC3BlcmlvZGljaXR5GAQgASgOMiIub3BlbnN0YXR1cy5tb25pdG9yLnYxLlBlcmlvZGljaXR5Qgi6SAWCAQIgABI7CgZtZXRob2QYBSABKA4yIS5vcGVuc3RhdHVzLm1vbml0b3IudjEuSFRUUE1ldGhvZEIIukgFggECIAASJQoEYm9keRgGIAEoCUIXukcUOhISEHsia2V5IjogInZhbHVlIn0SHAoHdGltZW91dBgHIAEoA0ILukgIIgYY4KcSKAASJQoLZGVncmFkZWRfYXQYCCABKANCC7pICCIGGOCnEigASACIAQESGAoFcmV0cnkYCSABKANCCbpIBiIEGAooABIdChBmb2xsb3dfcmVkaXJlY3RzGAogASgISAGIAQESOQoHaGVhZGVycxgLIAMoCzIeLm9wZW5zdGF0dXMubW9uaXRvci52MS5IZWFkZXJzQgi6SAWSAQIQFBJUChZzdGF0dXNfY29kZV9hc3NlcnRpb25zGAwgAygLMioub3BlbnN0YXR1cy5tb25pdG9yLnYxLlN0YXR1c0NvZGVBc3NlcnRpb25CCLpIBZIBAhAKEkcKD2JvZHlfYXNzZXJ0aW9ucxgNIAMoCzIkLm9wZW5zdGF0dXMubW9uaXRvci52MS5Cb2R5QXNzZXJ0aW9uQgi6SAWSAQIQChJLChFoZWFkZXJfYXNzZXJ0aW9ucxgOIAMoCzImLm9wZW5zdGF0dXMubW9uaXRvci52MS5IZWFkZXJBc3NlcnRpb25CCLpIBZIBAhAKEiIKC2Rlc2NyaXB0aW9uGA8gASgJQgi6SAVyAxiACEgCiAEBEhMKBmFjdGl2ZRgQIAEoCEgDiAEBEhMKBnB1YmxpYxgRIAEoCEgEiAEBEj8KB3JlZ2lvbnMYEiADKA4yHS5vcGVuc3RhdHVzLm1vbml0b3IudjEuUmVnaW9uQg+6SAySAQkQHCIFggECIAASQgoOb3Blbl90ZWxlbWV0cnkYEyABKAsyKi5vcGVuc3RhdHVzLm1vbml0b3IudjEuT3BlblRlbGVtZXRyeUNvbmZpZxI0CgZzdGF0dXMYFCABKA4yJC5vcGVuc3RhdHVzLm1vbml0b3IudjEuTW9uaXRvclN0YXR1cxIjChRwcml2YXRlX2xvY2F0aW9uX2lkcxgVIAMoCUIFukcCGAFCDgoMX2RlZ3JhZGVkX2F0QhMKEV9mb2xsb3dfcmVkaXJlY3RzQg4KDF9kZXNjcmlwdGlvbkIJCgdfYWN0aXZlQgkKB19wdWJsaWMq9wEKCkhUVFBNZXRob2QSGwoXSFRUUF9NRVRIT0RfVU5TUEVDSUZJRUQQABITCg9IVFRQX01FVEhPRF9HRVQQARIUChBIVFRQX01FVEhPRF9QT1NUEAISFAoQSFRUUF9NRVRIT0RfSEVBRBADEhMKD0hUVFBfTUVUSE9EX1BVVBAEEhUKEUhUVFBfTUVUSE9EX1BBVENIEAUSFgoSSFRUUF9NRVRIT0RfREVMRVRFEAYSFQoRSFRUUF9NRVRIT0RfVFJBQ0UQBxIXChNIVFRQX01FVEhPRF9DT05ORUNUEAgSFwoTSFRUUF9NRVRIT0RfT1BUSU9OUxAJQlNaUWdpdGh1Yi5jb20vb3BlbnN0YXR1c2hxL29wZW5zdGF0dXMvcGFja2FnZXMvcHJvdG8vb3BlbnN0YXR1cy9tb25pdG9yL3YxO21vbml0b3J2MWIGcHJvdG8z", [file_buf_validate_validate, file_gnostic_openapi_v3_annotations, file_openstatus_monitor_v1_assertions, file_openstatus_monitor_v1_monitor]); /** * Headers represents a key-value pair for HTTP headers. @@ -123,14 +123,14 @@ export type HTTPMonitor = Message<"openstatus.monitor.v1.HTTPMonitor"> & { body: string; /** - * Timeout in milliseconds (0-120000, defaults to 45000). + * Timeout in milliseconds (0-300000, defaults to 45000). * * @generated from field: int64 timeout = 7; */ timeout: bigint; /** - * Latency threshold for degraded status in milliseconds (optional, 0-120000). + * Latency threshold for degraded status in milliseconds (optional, 0-300000). * * @generated from field: optional int64 degraded_at = 8; */ diff --git a/packages/proto/gen/ts/openstatus/monitor/v1/icmp_monitor_pb.ts b/packages/proto/gen/ts/openstatus/monitor/v1/icmp_monitor_pb.ts index 88b6526ae..d7a13e5d8 100644 --- a/packages/proto/gen/ts/openstatus/monitor/v1/icmp_monitor_pb.ts +++ b/packages/proto/gen/ts/openstatus/monitor/v1/icmp_monitor_pb.ts @@ -16,7 +16,7 @@ import type { Message } from "@bufbuild/protobuf"; * Describes the file openstatus/monitor/v1/icmp_monitor.proto. */ export const file_openstatus_monitor_v1_icmp_monitor: GenFile = /*@__PURE__*/ - fileDesc("CihvcGVuc3RhdHVzL21vbml0b3IvdjEvaWNtcF9tb25pdG9yLnByb3RvEhVvcGVuc3RhdHVzLm1vbml0b3IudjEi8wQKC0lDTVBNb25pdG9yEgoKAmlkGAEgASgJEisKBG5hbWUYAiABKAlCHbpHEDoOEgxQaW5nIEdhdGV3YXm6SAdyBRABGIACEiUKA3VyaRgDIAEoCUIYukcLOgkSBzEuMS4xLjG6SAdyBRABGIAQEkEKC3BlcmlvZGljaXR5GAQgASgOMiIub3BlbnN0YXR1cy5tb25pdG9yLnYxLlBlcmlvZGljaXR5Qgi6SAWCAQIgABIcCgd0aW1lb3V0GAUgASgDQgu6SAgiBhjAqQcoABIlCgtkZWdyYWRlZF9hdBgGIAEoA0ILukgIIgYYwKkHKABIAIgBARIYCgVyZXRyeRgHIAEoA0IJukgGIgQYCigAEiIKC2Rlc2NyaXB0aW9uGAggASgJQgi6SAVyAxiACEgBiAEBEhMKBmFjdGl2ZRgJIAEoCEgCiAEBEhMKBnB1YmxpYxgKIAEoCEgDiAEBEj8KB3JlZ2lvbnMYCyADKA4yHS5vcGVuc3RhdHVzLm1vbml0b3IudjEuUmVnaW9uQg+6SAySAQkQHCIFggECIAASQgoOb3Blbl90ZWxlbWV0cnkYDCABKAsyKi5vcGVuc3RhdHVzLm1vbml0b3IudjEuT3BlblRlbGVtZXRyeUNvbmZpZxI0CgZzdGF0dXMYDSABKA4yJC5vcGVuc3RhdHVzLm1vbml0b3IudjEuTW9uaXRvclN0YXR1cxIjChRwcml2YXRlX2xvY2F0aW9uX2lkcxgOIAMoCUIFukcCGAFCDgoMX2RlZ3JhZGVkX2F0Qg4KDF9kZXNjcmlwdGlvbkIJCgdfYWN0aXZlQgkKB19wdWJsaWNCU1pRZ2l0aHViLmNvbS9vcGVuc3RhdHVzaHEvb3BlbnN0YXR1cy9wYWNrYWdlcy9wcm90by9vcGVuc3RhdHVzL21vbml0b3IvdjE7bW9uaXRvcnYxYgZwcm90bzM", [file_buf_validate_validate, file_gnostic_openapi_v3_annotations, file_openstatus_monitor_v1_http_monitor, file_openstatus_monitor_v1_monitor]); + fileDesc("CihvcGVuc3RhdHVzL21vbml0b3IvdjEvaWNtcF9tb25pdG9yLnByb3RvEhVvcGVuc3RhdHVzLm1vbml0b3IudjEi8wQKC0lDTVBNb25pdG9yEgoKAmlkGAEgASgJEisKBG5hbWUYAiABKAlCHbpHEDoOEgxQaW5nIEdhdGV3YXm6SAdyBRABGIACEiUKA3VyaRgDIAEoCUIYukcLOgkSBzEuMS4xLjG6SAdyBRABGIAQEkEKC3BlcmlvZGljaXR5GAQgASgOMiIub3BlbnN0YXR1cy5tb25pdG9yLnYxLlBlcmlvZGljaXR5Qgi6SAWCAQIgABIcCgd0aW1lb3V0GAUgASgDQgu6SAgiBhjgpxIoABIlCgtkZWdyYWRlZF9hdBgGIAEoA0ILukgIIgYY4KcSKABIAIgBARIYCgVyZXRyeRgHIAEoA0IJukgGIgQYCigAEiIKC2Rlc2NyaXB0aW9uGAggASgJQgi6SAVyAxiACEgBiAEBEhMKBmFjdGl2ZRgJIAEoCEgCiAEBEhMKBnB1YmxpYxgKIAEoCEgDiAEBEj8KB3JlZ2lvbnMYCyADKA4yHS5vcGVuc3RhdHVzLm1vbml0b3IudjEuUmVnaW9uQg+6SAySAQkQHCIFggECIAASQgoOb3Blbl90ZWxlbWV0cnkYDCABKAsyKi5vcGVuc3RhdHVzLm1vbml0b3IudjEuT3BlblRlbGVtZXRyeUNvbmZpZxI0CgZzdGF0dXMYDSABKA4yJC5vcGVuc3RhdHVzLm1vbml0b3IudjEuTW9uaXRvclN0YXR1cxIjChRwcml2YXRlX2xvY2F0aW9uX2lkcxgOIAMoCUIFukcCGAFCDgoMX2RlZ3JhZGVkX2F0Qg4KDF9kZXNjcmlwdGlvbkIJCgdfYWN0aXZlQgkKB19wdWJsaWNCU1pRZ2l0aHViLmNvbS9vcGVuc3RhdHVzaHEvb3BlbnN0YXR1cy9wYWNrYWdlcy9wcm90by9vcGVuc3RhdHVzL21vbml0b3IvdjE7bW9uaXRvcnYxYgZwcm90bzM", [file_buf_validate_validate, file_gnostic_openapi_v3_annotations, file_openstatus_monitor_v1_http_monitor, file_openstatus_monitor_v1_monitor]); /** * ICMPMonitor defines the configuration for a ICMP monitor. @@ -53,14 +53,14 @@ export type ICMPMonitor = Message<"openstatus.monitor.v1.ICMPMonitor"> & { periodicity: Periodicity; /** - * Timeout in milliseconds (0-120000, defaults to 45000). + * Timeout in milliseconds (0-300000, defaults to 45000). * * @generated from field: int64 timeout = 5; */ timeout: bigint; /** - * Latency threshold for degraded status in milliseconds (optional, 0-120000). + * Latency threshold for degraded status in milliseconds (optional, 0-300000). * * @generated from field: optional int64 degraded_at = 6; */ diff --git a/packages/proto/gen/ts/openstatus/monitor/v1/tcp_monitor_pb.ts b/packages/proto/gen/ts/openstatus/monitor/v1/tcp_monitor_pb.ts index 999d55750..4dac4df0f 100644 --- a/packages/proto/gen/ts/openstatus/monitor/v1/tcp_monitor_pb.ts +++ b/packages/proto/gen/ts/openstatus/monitor/v1/tcp_monitor_pb.ts @@ -16,7 +16,7 @@ import type { Message } from "@bufbuild/protobuf"; * Describes the file openstatus/monitor/v1/tcp_monitor.proto. */ export const file_openstatus_monitor_v1_tcp_monitor: GenFile = /*@__PURE__*/ - fileDesc("CidvcGVuc3RhdHVzL21vbml0b3IvdjEvdGNwX21vbml0b3IucHJvdG8SFW9wZW5zdGF0dXMubW9uaXRvci52MSKRBQoKVENQTW9uaXRvchIKCgJpZBgBIAEoCRI4CgRuYW1lGAIgASgJQiq6Rx06GxIZRGF0YWJhc2UgQ29ubmVjdGlvbiBDaGVja7pIB3IFEAEYgAISNwoDdXJpGAMgASgJQiq6Rx06GxIZdGNwOi8vZGIuZXhhbXBsZS5jb206NTQzMrpIB3IFEAEYgBASQQoLcGVyaW9kaWNpdHkYBCABKA4yIi5vcGVuc3RhdHVzLm1vbml0b3IudjEuUGVyaW9kaWNpdHlCCLpIBYIBAiAAEhwKB3RpbWVvdXQYBSABKANCC7pICCIGGMCpBygAEiUKC2RlZ3JhZGVkX2F0GAYgASgDQgu6SAgiBhjAqQcoAEgAiAEBEhgKBXJldHJ5GAcgASgDQgm6SAYiBBgKKAASIgoLZGVzY3JpcHRpb24YCCABKAlCCLpIBXIDGIAISAGIAQESEwoGYWN0aXZlGAkgASgISAKIAQESEwoGcHVibGljGAogASgISAOIAQESPwoHcmVnaW9ucxgLIAMoDjIdLm9wZW5zdGF0dXMubW9uaXRvci52MS5SZWdpb25CD7pIDJIBCRAcIgWCAQIgABJCCg5vcGVuX3RlbGVtZXRyeRgMIAEoCzIqLm9wZW5zdGF0dXMubW9uaXRvci52MS5PcGVuVGVsZW1ldHJ5Q29uZmlnEjQKBnN0YXR1cxgNIAEoDjIkLm9wZW5zdGF0dXMubW9uaXRvci52MS5Nb25pdG9yU3RhdHVzEiMKFHByaXZhdGVfbG9jYXRpb25faWRzGA4gAygJQgW6RwIYAUIOCgxfZGVncmFkZWRfYXRCDgoMX2Rlc2NyaXB0aW9uQgkKB19hY3RpdmVCCQoHX3B1YmxpY0JTWlFnaXRodWIuY29tL29wZW5zdGF0dXNocS9vcGVuc3RhdHVzL3BhY2thZ2VzL3Byb3RvL29wZW5zdGF0dXMvbW9uaXRvci92MTttb25pdG9ydjFiBnByb3RvMw", [file_buf_validate_validate, file_gnostic_openapi_v3_annotations, file_openstatus_monitor_v1_http_monitor, file_openstatus_monitor_v1_monitor]); + fileDesc("CidvcGVuc3RhdHVzL21vbml0b3IvdjEvdGNwX21vbml0b3IucHJvdG8SFW9wZW5zdGF0dXMubW9uaXRvci52MSKRBQoKVENQTW9uaXRvchIKCgJpZBgBIAEoCRI4CgRuYW1lGAIgASgJQiq6Rx06GxIZRGF0YWJhc2UgQ29ubmVjdGlvbiBDaGVja7pIB3IFEAEYgAISNwoDdXJpGAMgASgJQiq6Rx06GxIZdGNwOi8vZGIuZXhhbXBsZS5jb206NTQzMrpIB3IFEAEYgBASQQoLcGVyaW9kaWNpdHkYBCABKA4yIi5vcGVuc3RhdHVzLm1vbml0b3IudjEuUGVyaW9kaWNpdHlCCLpIBYIBAiAAEhwKB3RpbWVvdXQYBSABKANCC7pICCIGGOCnEigAEiUKC2RlZ3JhZGVkX2F0GAYgASgDQgu6SAgiBhjgpxIoAEgAiAEBEhgKBXJldHJ5GAcgASgDQgm6SAYiBBgKKAASIgoLZGVzY3JpcHRpb24YCCABKAlCCLpIBXIDGIAISAGIAQESEwoGYWN0aXZlGAkgASgISAKIAQESEwoGcHVibGljGAogASgISAOIAQESPwoHcmVnaW9ucxgLIAMoDjIdLm9wZW5zdGF0dXMubW9uaXRvci52MS5SZWdpb25CD7pIDJIBCRAcIgWCAQIgABJCCg5vcGVuX3RlbGVtZXRyeRgMIAEoCzIqLm9wZW5zdGF0dXMubW9uaXRvci52MS5PcGVuVGVsZW1ldHJ5Q29uZmlnEjQKBnN0YXR1cxgNIAEoDjIkLm9wZW5zdGF0dXMubW9uaXRvci52MS5Nb25pdG9yU3RhdHVzEiMKFHByaXZhdGVfbG9jYXRpb25faWRzGA4gAygJQgW6RwIYAUIOCgxfZGVncmFkZWRfYXRCDgoMX2Rlc2NyaXB0aW9uQgkKB19hY3RpdmVCCQoHX3B1YmxpY0JTWlFnaXRodWIuY29tL29wZW5zdGF0dXNocS9vcGVuc3RhdHVzL3BhY2thZ2VzL3Byb3RvL29wZW5zdGF0dXMvbW9uaXRvci92MTttb25pdG9ydjFiBnByb3RvMw", [file_buf_validate_validate, file_gnostic_openapi_v3_annotations, file_openstatus_monitor_v1_http_monitor, file_openstatus_monitor_v1_monitor]); /** * TCPMonitor defines the configuration for a TCP monitor. @@ -53,14 +53,14 @@ export type TCPMonitor = Message<"openstatus.monitor.v1.TCPMonitor"> & { periodicity: Periodicity; /** - * Timeout in milliseconds (0-120000, defaults to 45000). + * Timeout in milliseconds (0-300000, defaults to 45000). * * @generated from field: int64 timeout = 5; */ timeout: bigint; /** - * Latency threshold for degraded status in milliseconds (optional, 0-120000). + * Latency threshold for degraded status in milliseconds (optional, 0-300000). * * @generated from field: optional int64 degraded_at = 6; */ diff --git a/packages/services/src/monitor/__tests__/monitor.test.ts b/packages/services/src/monitor/__tests__/monitor.test.ts index e45598ca9..ea66f8a5f 100644 --- a/packages/services/src/monitor/__tests__/monitor.test.ts +++ b/packages/services/src/monitor/__tests__/monitor.test.ts @@ -1302,18 +1302,18 @@ describe("updateMonitorConfig clearing + replacement", () => { }); }); - // The proto declares 0–120_000 ms for `timeout`/`degraded_at`; the RPC API + // The proto declares 0–300_000 ms for `timeout`/`degraded_at`; the RPC API // accepted that range before these verbs existed, so they must too. - test("accepts the proto's full 0–120_000 ms timeout range", async () => { + test("accepts the proto's full 0–300_000 ms timeout range", async () => { await withTestTransaction(async (tx) => { const ctx = { ...teamCtx, db: tx }; const row = await seed(tx, `${TEST_PREFIX}-wide-timeout`); const updated = await updateMonitorConfig({ ctx, - input: { id: row.id, timeout: 120_000, degradedAfter: 90_000 }, + input: { id: row.id, timeout: 300_000, degradedAfter: 90_000 }, }); - expect(updated.timeout).toBe(120_000); + expect(updated.timeout).toBe(300_000); expect(updated.degradedAfter).toBe(90_000); const created = await createMonitor({ @@ -1326,11 +1326,11 @@ describe("updateMonitorConfig clearing + replacement", () => { headers: [], assertions: [], active: false, - timeout: 120_000, + timeout: 300_000, degradedAfter: 90_000, }, }); - expect(created.timeout).toBe(120_000); + expect(created.timeout).toBe(300_000); expect(created.degradedAfter).toBe(90_000); }); }); @@ -1341,7 +1341,7 @@ describe("updateMonitorConfig clearing + replacement", () => { const row = await seed(tx, `${TEST_PREFIX}-over-timeout`); await expect( - updateMonitorConfig({ ctx, input: { id: row.id, timeout: 120_001 } }), + updateMonitorConfig({ ctx, input: { id: row.id, timeout: 300_001 } }), ).rejects.toThrow(); }); }); diff --git a/packages/services/src/monitor/schemas.ts b/packages/services/src/monitor/schemas.ts index ae1d0a84c..a6e537ceb 100644 --- a/packages/services/src/monitor/schemas.ts +++ b/packages/services/src/monitor/schemas.ts @@ -25,13 +25,9 @@ const assertion = z.discriminatedUnion("type", [ recordAssertion, ]); -// Bounds mirror `insertMonitorSchema` (0–60_000 ms) — what the dashboard -// forms accept. -const timeoutMs = z.coerce.number().gte(0).lte(60_000); - -// The RPC API's proto contract declares 0–120_000 ms and validates it there, -// so the whole-object verbs must not reject a value the proto accepts. -const apiTimeoutMs = z.coerce.number().gte(0).lte(120_000); +// Bounds mirror `insertMonitorSchema` and the RPC API's proto contract +// (0–300_000 ms), so no verb rejects a value another surface accepts. +const timeoutMs = z.coerce.number().gte(0).lte(300_000); /** * Create a new monitor. Regions and periodicity are optional — when unset, @@ -54,8 +50,8 @@ export const CreateMonitorInput = z.object({ // falls through to the column default rather than overwriting it. description: z.string().optional(), public: z.boolean().optional(), - timeout: apiTimeoutMs.optional(), - degradedAfter: apiTimeoutMs.nullish(), + timeout: timeoutMs.optional(), + degradedAfter: timeoutMs.nullish(), retry: z.number().int().min(0).optional(), followRedirects: z.boolean().optional(), grpcService: z.string().optional(), @@ -87,8 +83,8 @@ export const UpdateMonitorConfigInput = z.object({ regions: z.array(z.string()).optional(), description: z.string().optional(), public: z.boolean().optional(), - timeout: apiTimeoutMs.optional(), - degradedAfter: apiTimeoutMs.nullish(), + timeout: timeoutMs.optional(), + degradedAfter: timeoutMs.nullish(), retry: z.number().int().min(0).optional(), followRedirects: z.boolean().optional(), grpcService: z.string().optional(), -- 2.54.0 From 7c3c47ca72d6d33e19de937c01044af4dd8c1e7a Mon Sep 17 00:00:00 2001 From: Thibault Le Ouay Date: Tue, 20 Oct 2026 10:58:36 +0200 Subject: [PATCH 6/6] server workflow gracefull shutdown (#2949) * server workflow gracefull shutdown * fix pr --- apps/server/fly.toml | 4 ++++ apps/server/src/serve.ts | 28 ++++++++++++++++++++++-- apps/workflows/fly.toml | 2 ++ apps/workflows/src/cron/scheduler.ts | 32 +++++++++++++++++++++++++++- apps/workflows/src/serve.ts | 24 ++++++++++++++++----- 5 files changed, 82 insertions(+), 8 deletions(-) diff --git a/apps/server/fly.toml b/apps/server/fly.toml index 456af16de..1211b2a8b 100644 --- a/apps/server/fly.toml +++ b/apps/server/fly.toml @@ -6,6 +6,10 @@ app = "openstatus-api" primary_region = "ams" +# serve.ts drains in-flight requests on SIGTERM; Fly defaults to SIGINT. +kill_signal = "SIGTERM" +kill_timeout = "30s" + [build] dockerfile = "./Dockerfile" diff --git a/apps/server/src/serve.ts b/apps/server/src/serve.ts index 7ee67e50d..ed38f922b 100644 --- a/apps/server/src/serve.ts +++ b/apps/server/src/serve.ts @@ -1,11 +1,17 @@ +import { setTimeout as sleep } from "node:timers/promises"; + import { serve } from "@hono/node-server"; -import { getLogger } from "@logtape/logtape"; +import { dispose, getLogger } from "@logtape/logtape"; import { env } from "./env"; import { app } from "./index"; +import { settleBackgroundTasks } from "./libs/background"; const port = 3000; +// Leaves headroom under Fly's kill_timeout (30s) for the OTLP log flush. +const SHUTDOWN_GRACE_MS = 20_000; + // Node terminates the process on unhandled rejections; log instead so one // stray background promise (e.g. a fire-and-forget fetch) can't kill the instance. process.on("unhandledRejection", (reason) => { @@ -20,4 +26,22 @@ getLogger("api-server").info("Starting server", { environment: env.NODE_ENV, }); -serve({ fetch: app.fetch, port }); +const server = serve({ fetch: app.fetch, port }); + +// Fly sends kill_signal (SIGTERM, see fly.toml) on deploy and stop, then +// SIGKILLs after kill_timeout. Stop accepting connections, let in-flight +// requests and the background work they started (Slack acks, see +// runInBackground) finish, then flush buffered OTLP logs before exiting. +process.once("SIGTERM", () => { + void (async () => { + getLogger("api-server").info("SIGTERM received, draining connections"); + const drained = (async () => { + await new Promise((resolve) => server.close(() => resolve())); + // After the HTTP drain: a request finishing late can still spawn a task. + await settleBackgroundTasks(); + })(); + await Promise.race([drained, sleep(SHUTDOWN_GRACE_MS)]); + await dispose(); + process.exit(0); + })(); +}); diff --git a/apps/workflows/fly.toml b/apps/workflows/fly.toml index 9e670264a..9e022146d 100644 --- a/apps/workflows/fly.toml +++ b/apps/workflows/fly.toml @@ -9,6 +9,8 @@ primary_region = 'ams' # Long enough for shutdownOutbox to wait out an in-flight provider call # (NOTIFICATION_TIMEOUT_MS + 5s) before Fly SIGKILLs. kill_timeout = "25s" +# serve.ts only handles SIGTERM; Fly defaults to SIGINT. +kill_signal = "SIGTERM" [build] dockerfile = "./Dockerfile" diff --git a/apps/workflows/src/cron/scheduler.ts b/apps/workflows/src/cron/scheduler.ts index fbc326d65..6197da542 100644 --- a/apps/workflows/src/cron/scheduler.ts +++ b/apps/workflows/src/cron/scheduler.ts @@ -54,9 +54,22 @@ type RunningTask = Fiber.Fiber; let running: RunningTask[] = []; +// Interrupting a fiber does not cancel the promise `Effect.tryPromise` is +// waiting on, so the task keeps running detached. Track it so shutdown can wait +// for, e.g., a claimed incident reminder window to actually be sent. +const inFlight = new Set>(); + +function track(task: ScheduledTask): Promise { + const promise = task.run().finally(() => { + inFlight.delete(promise); + }); + inFlight.add(promise); + return promise; +} + function scheduleTask(task: ScheduledTask): RunningTask { const body = Effect.tryPromise({ - try: () => task.run(), + try: () => track(task), catch: (error) => error instanceof Error ? error : new Error(String(error)), }).pipe( @@ -106,3 +119,20 @@ export async function stopScheduler(): Promise { }), ); } + +/** + * Resolves once every scheduled task already started has settled, or after + * `graceMs`. Call after `stopScheduler` so no new run can start meanwhile. + */ +export async function drainScheduledTasks(graceMs: number): Promise { + if (inFlight.size === 0) return; + let timer: ReturnType | undefined; + const grace = new Promise((resolve) => { + timer = setTimeout(resolve, graceMs); + }); + await Promise.race([ + Promise.allSettled(inFlight).then(() => undefined), + grace, + ]); + if (timer !== undefined) clearTimeout(timer); +} diff --git a/apps/workflows/src/serve.ts b/apps/workflows/src/serve.ts index 5a2b8408d..5dea945f9 100644 --- a/apps/workflows/src/serve.ts +++ b/apps/workflows/src/serve.ts @@ -1,8 +1,12 @@ import { serve } from "@hono/node-server"; -import { getLogger } from "@logtape/logtape"; +import { dispose, getLogger } from "@logtape/logtape"; import { shutdownOutbox, startOutboxConsumer } from "./checker/outbox"; -import { startScheduler, stopScheduler } from "./cron/scheduler"; +import { + drainScheduledTasks, + startScheduler, + stopScheduler, +} from "./cron/scheduler"; import { env } from "./env"; import { app } from "./index"; @@ -20,11 +24,21 @@ startScheduler(); const server = serve({ fetch: app.fetch, port: PORT }); -process.on("SIGTERM", () => { +process.once("SIGTERM", () => { void (async () => { logger.info("SIGTERM received, releasing outbox claims"); + const httpClosed = new Promise((resolve) => + server.close(() => resolve()), + ); await stopScheduler(); - await shutdownOutbox(); - server.close(); + // Same window shutdownOutbox gives in-flight sends, so both fit inside + // Fly's kill_timeout with room left for the OTLP log flush. + await Promise.all([ + shutdownOutbox(), + drainScheduledTasks(env().NOTIFICATION_TIMEOUT_MS + 5_000), + ]); + await httpClosed; + await dispose(); + process.exit(0); })(); });