diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4648eb31b..9211a0424 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -24,5 +24,8 @@ jobs: cache: "pnpm" - run: pnpm install - run: pnpm openapi:check --explicit + - run: pnpm check:api-client + - run: pnpm build-packages - run: pnpm test + - run: pnpm test:packages - run: pnpm build diff --git a/.gitignore b/.gitignore index 7ac75d6c7..5085c3251 100644 --- a/.gitignore +++ b/.gitignore @@ -23,6 +23,7 @@ next-env.d.ts /public/recorder.js /geo /dist +/packages/*/dist /generated /src/generated pm2.yml diff --git a/Dockerfile b/Dockerfile index 79e670fcc..10034b9a8 100644 --- a/Dockerfile +++ b/Dockerfile @@ -10,10 +10,13 @@ ARG PNPM_VERSION # Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed. RUN apk add --no-cache libc6-compat WORKDIR /app -COPY package.json pnpm-lock.yaml ./ +COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./ +# Workspace packages (@umami/api-client, @umami/mcp) must be present for a frozen install. +COPY packages/api-client/package.json ./packages/api-client/ +COPY packages/mcp/package.json ./packages/mcp/ RUN npm install -g pnpm@${PNPM_VERSION} -RUN printf 'strictDepBuilds: false\n' > pnpm-workspace.yaml +RUN printf 'strictDepBuilds: false\n' >> pnpm-workspace.yaml RUN pnpm install --frozen-lockfile diff --git a/package.json b/package.json index 0143eece0..f7babee9c 100644 --- a/package.json +++ b/package.json @@ -12,9 +12,9 @@ "type": "module", "scripts": { "dev": "dotenv next dev --turbo", - "build": "npm-run-all check-env build-db check-db build-tracker build-recorder build-geo build-openapi build-app", + "build": "npm-run-all check-env build-db check-db build-tracker build-recorder build-geo build-openapi build-packages build-app", "start": "next start", - "build-docker": "npm-run-all build-db build-tracker build-recorder build-geo build-openapi build-app", + "build-docker": "npm-run-all build-db build-tracker build-recorder build-geo build-openapi build-packages build-app", "start-docker": "npm-run-all check-db update-tracker start-server", "start-env": "node scripts/start-env.js", "start-server": "node server.js", @@ -29,7 +29,7 @@ "build-prisma-client": "node scripts/build-prisma-client.js", "build-lang": "npm-run-all download-country-names download-language-names", "build-geo": "node scripts/build-geo.js", - "build-openapi": "pnpm openapi:generate", + "build-openapi": "npm-run-all openapi:generate generate:api-client", "build-db": "npm-run-all build-db-client build-prisma-client", "build-db-schema": "prisma db pull", "build-db-client": "prisma generate", @@ -45,6 +45,11 @@ "openapi:generate": "tsx scripts/generate-openapi.ts", "openapi:contracts": "tsx scripts/generate-openapi-contracts.ts", "openapi:check": "tsx scripts/check-openapi.ts", + "generate:api": "npm-run-all openapi:generate generate:api-client", + "generate:api-client": "tsx scripts/generate-api-client.ts", + "check:api-client": "tsx scripts/generate-api-client.ts --check", + "build-packages": "pnpm --filter \"./packages/**\" -r build", + "test:packages": "pnpm --filter \"./packages/**\" -r test", "postbuild": "node scripts/postbuild.js", "pretest": "pnpm build-db-client", "test": "vitest run", @@ -69,6 +74,8 @@ "@prisma/extension-read-replicas": "^0.5.0", "@svgr/cli": "^8.1.0", "@tanstack/react-query": "^5.102.8", + "@umami/api-client": "workspace:*", + "@umami/mcp": "workspace:*", "@umami/react-zen": "^0.251.0", "bcryptjs": "^3.0.2", "chalk": "^5.6.2", @@ -141,6 +148,7 @@ "dotenv-cli": "^11.0.0", "jsdom": "^30.0.1", "msw": "^2.15.0", + "openapi-typescript": "^7.13.0", "postcss": "^8.5.26", "postcss-flexbugs-fixes": "^5.0.2", "postcss-preset-env": "11.3.2", diff --git a/packages/api-client/MIGRATION.md b/packages/api-client/MIGRATION.md new file mode 100644 index 000000000..b4f683ef9 --- /dev/null +++ b/packages/api-client/MIGRATION.md @@ -0,0 +1,56 @@ +# Migrating from the hand-written `@umami/api-client` + +The previous client (`umami-software/api-client`) maintained every endpoint by hand. This package +is generated from Umami's OpenAPI contract and replaces it. That repository will be archived once +this package is stable. + +## What changed + +| Before | After | +| ------------------------------------------------------- | --------------------------------------------------------------------------------------- | +| `getClient({ apiEndpoint, apiKey, userId, secret })` | `new UmamiClient({ baseUrl, apiKey, token })` | +| Methods return `{ ok, data, status, error }` | Methods return the response body and **throw** `UmamiApiError` on non-2xx | +| Positional arguments `getWebsiteStats(websiteId, params)` | Single object `getWebsiteStats({ websiteId, ...params })` (positional still works for common analytics methods, see below) | +| `getWebsites()` | `listWebsites()` (`getWebsites` kept as a deprecated alias) | +| `userId` + `secret` mint a token client-side | Removed. Mint the token yourself and pass `token`. | + +## Errors + +```ts +// before +const { ok, data, error } = await client.getWebsiteStats(id, params); +if (!ok) { … } + +// after +try { + const data = await client.getWebsiteStats({ websiteId: id, ...params }); +} catch (error) { + if (isUmamiApiError(error)) { error.status; error.code; } +} +``` + +## Compatibility overloads + +To ease upgrades these methods still accept the old positional form: + +`getWebsite`, `updateWebsite`, `deleteWebsite`, `getWebsiteActive`, `getWebsiteDateRange`, +`getWebsiteStats`, `getWebsitePageviews`, `getWebsiteMetrics`, `getWebsiteExpandedMetrics`, +`getWebsiteEvents`, `getWebsiteEventSeries`, `getWebsiteEventStats`, `getEventData`, +`getEventDataStats`, `getEventDataProperties`, `getEventDataValues`, `getWebsiteSessions`, +`getWebsiteSessionStats`, `getWebsiteSession`, `getWebsiteSessionActivity`, +`getWebsiteSessionProperties`, `getWebsiteReports`. + +They are deprecated and will be removed in the next major release. The object form is canonical. + +## Method names + +Method names are OpenAPI `operationId`s. Most match the old names (`getWebsiteStats`, +`runFunnelReport`, `getRealtime`, `createTeam`, …). Endpoints that were not in the old client are +now available automatically. Endpoints under `/api/admin`, `/api/config`, `/api/dashboard` and +other internal routes are intentionally excluded. + +## Triage of the old surface + +- **Kept**: websites, analytics, events, sessions, session data, reports, teams, users, boards, + links, pixels, shares, account (`me`), auth, two-factor, tracking (`send`, `batch`, `record`). +- **Dropped**: admin endpoints (not part of the public API), client-side token minting. diff --git a/packages/api-client/README.md b/packages/api-client/README.md new file mode 100644 index 000000000..dc858c561 --- /dev/null +++ b/packages/api-client/README.md @@ -0,0 +1,82 @@ +# @umami/api-client + +Typed TypeScript client for the [Umami](https://umami.is) API. Every method is generated from +Umami's OpenAPI contract (`public/openapi.json` in the main repository), so the client can never +drift from the server. + +```bash +pnpm add @umami/api-client +``` + +## Usage + +```ts +import { UmamiClient } from '@umami/api-client'; + +// Umami Cloud +const umami = new UmamiClient({ apiKey: process.env.UMAMI_API_KEY }); + +// Self-hosted +const umami = new UmamiClient({ + baseUrl: 'https://analytics.example.com/api', + token: process.env.UMAMI_API_TOKEN, // API key (umami_…) or login token +}); + +const websites = await umami.listWebsites(); + +const stats = await umami.getWebsiteStats({ + websiteId, + startAt: Date.now() - 7 * 24 * 60 * 60 * 1000, + endAt: Date.now(), +}); + +const pages = await umami.getWebsiteMetrics({ websiteId, startAt, endAt, type: 'path', limit: 10 }); +``` + +Every method takes **one object** that merges path parameters, query parameters and the request +body. Method names are the OpenAPI `operationId`s; hover in your editor for the parameter and +response types, or import `OperationInput<'getWebsiteStats'>` / `OperationOutput<'…'>`. + +### Options + +| Option | Description | +| --------- | ----------------------------------------------------------------------------------------------------------- | +| `baseUrl` | API root. Defaults to `https://api.umami.is/v1`. Self-hosted: `https://your-umami/api`. | +| `token` | Bearer token: login token, OAuth access token, or self-hosted API key. | +| `apiKey` | Umami Cloud API key (sent as `x-umami-api-key`; also sent as a bearer token when `token` is not provided). | +| `headers` | Extra headers for every request. | +| `fetch` | Custom `fetch` (tests, other runtimes, in-process dispatch). | +| `timeout` | Per-request timeout in milliseconds. | + +### Errors + +Non-2xx responses throw `UmamiApiError` with `status`, `code` (`unauthorized`, `forbidden`, +`not-found`, `bad-request`, …), `message` and the parsed `body`. + +```ts +import { isUmamiApiError } from '@umami/api-client'; + +try { + await umami.getWebsite({ websiteId }); +} catch (error) { + if (isUmamiApiError(error) && error.isNotFound) { + // … + } +} +``` + +### Escape hatch + +`umami.call('operationId', input)` invokes any operation by ID; `operations` exports the full +table of methods, paths and parameters. + +## Regenerating + +From the Umami repository root: + +```bash +pnpm generate:api # public/openapi.json → packages/api-client/src/generated +pnpm check:api-client # CI: fail if generated files are stale +``` + +See [MIGRATION.md](./MIGRATION.md) if you are upgrading from the previous hand-written client. diff --git a/packages/api-client/package.json b/packages/api-client/package.json new file mode 100644 index 000000000..e7d2ca2ab --- /dev/null +++ b/packages/api-client/package.json @@ -0,0 +1,28 @@ +{ + "name": "@umami/api-client", + "version": "1.0.0-alpha.0", + "description": "TypeScript client for the Umami API, generated from the Umami OpenAPI contract.", + "author": "Umami Software, Inc. ", + "license": "MIT", + "type": "module", + "main": "./dist/index.js", + "types": "./dist/index.d.ts", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js" + } + }, + "files": ["dist", "README.md"], + "scripts": { + "build": "tsup", + "test": "vitest run", + "typecheck": "tsc --noEmit" + }, + "devDependencies": { + "@types/node": "^26.4.0", + "tsup": "^8.5.0", + "typescript": "^6.0.3", + "vitest": "^4.1.11" + } +} diff --git a/packages/api-client/src/client.test.ts b/packages/api-client/src/client.test.ts new file mode 100644 index 000000000..14355b1aa --- /dev/null +++ b/packages/api-client/src/client.test.ts @@ -0,0 +1,127 @@ +import { describe, expect, test } from 'vitest'; +import { UmamiClient } from './client'; +import { operations } from './generated/operations'; + +interface Captured { + url: URL; + method: string; + headers: Record; + body?: string; +} + +function createClient(response: unknown = {}, options: Record = {}) { + const calls: Captured[] = []; + const client = new UmamiClient({ + baseUrl: 'https://example.com/api', + token: 'token', + ...options, + fetch: async (url, init) => { + calls.push({ + url: new URL(url.toString()), + method: init?.method ?? 'GET', + headers: init?.headers as Record, + body: init?.body as string | undefined, + }); + + return new Response(JSON.stringify(response), { + status: 200, + headers: { 'content-type': 'application/json' }, + }); + }, + }); + + return { client, calls }; +} + +describe('UmamiClient', () => { + test('exposes a method for every generated operation', () => { + const client = new UmamiClient({ fetch: async () => new Response('{}') }); + + for (const operationId of Object.keys(operations)) { + expect(typeof (client as unknown as Record)[operationId]).toBe('function'); + } + }); + + test('builds GET requests with path and query parameters', async () => { + const { client, calls } = createClient({ pageviews: 1 }); + + await client.getWebsiteStats({ websiteId: 'w1', startAt: 1, endAt: 2, browser: 'chrome' }); + + expect(calls[0].method).toBe('GET'); + expect(calls[0].url.pathname).toBe('/api/websites/w1/stats'); + expect(calls[0].url.searchParams.get('startAt')).toBe('1'); + expect(calls[0].url.searchParams.get('browser')).toBe('chrome'); + expect(calls[0].headers.authorization).toBe('Bearer token'); + }); + + test('supports the legacy positional signature', async () => { + const { client, calls } = createClient([]); + + await client.getWebsiteMetrics('w1', { startAt: 1, endAt: 2, type: 'url' }); + await client.getWebsiteSession('w1', 's1'); + + expect(calls[0].url.pathname).toBe('/api/websites/w1/metrics'); + expect(calls[0].url.searchParams.get('type')).toBe('url'); + expect(calls[1].url.pathname).toBe('/api/websites/w1/sessions/s1'); + }); + + test('sends JSON bodies for POST operations', async () => { + const { client, calls } = createClient([]); + + await client.runFunnelReport({ + websiteId: 'w1', + type: 'funnel', + filters: {}, + parameters: { startDate: 'a', endDate: 'b', window: 60, steps: [] }, + }); + + expect(calls[0].method).toBe('POST'); + expect(calls[0].url.pathname).toBe('/api/reports/funnel'); + expect(JSON.parse(calls[0].body as string)).toMatchObject({ websiteId: 'w1', type: 'funnel' }); + }); + + test('uses the Cloud base URL and API key header by default', async () => { + const calls: Captured[] = []; + const client = new UmamiClient({ + apiKey: 'api-key', + fetch: async (url, init) => { + calls.push({ + url: new URL(url.toString()), + method: init?.method ?? 'GET', + headers: init?.headers as Record, + }); + return new Response('{"data":[]}', { headers: { 'content-type': 'application/json' } }); + }, + }); + + await client.listWebsites(); + + expect(calls[0].url.toString()).toBe('https://api.umami.is/v1/websites'); + expect(calls[0].headers['x-umami-api-key']).toBe('api-key'); + }); + + test('merges custom and per-request headers', async () => { + const { client, calls } = createClient({}, { headers: { 'x-a': '1' } }); + + await client.getMe(undefined, { headers: { 'x-b': '2' } }); + + expect(calls[0].headers).toMatchObject({ 'x-a': '1', 'x-b': '2' }); + }); + + test('withToken returns a client using the new token', async () => { + const { client, calls } = createClient({}); + + await client.withToken('other').getMe(); + + expect(calls[0].headers.authorization).toBe('Bearer other'); + }); + + test('call() invokes any operation by id', async () => { + const { client, calls } = createClient({ visitors: 3 }); + + const result = await client.call('getWebsiteActive', { websiteId: 'w1' }); + + expect(result).toEqual({ visitors: 3 }); + expect(calls[0].url.pathname).toBe('/api/websites/w1/active'); + }); +}); diff --git a/packages/api-client/src/client.ts b/packages/api-client/src/client.ts new file mode 100644 index 000000000..54afb6df8 --- /dev/null +++ b/packages/api-client/src/client.ts @@ -0,0 +1,525 @@ +import { + GeneratedUmamiClient, + type OperationId, + type OperationInput, + type OperationOutput, + operations, +} from './generated/operations'; +import { + API_KEY_HEADER, + buildAuthHeaders, + buildPath, + buildUrl, + DEFAULT_BASE_URL, + resolveFetch, + sendRequest, + splitInput, + trimSlashes, +} from './http'; +import type { FetchLike, RequestOptions, UmamiClientOptions } from './types'; + +type Input = OperationInput; +type Output = Promise>; +type Params = Omit, P>; + +function positional( + key: string, + first: string | Input, + second?: Record | RequestOptions, + third?: RequestOptions, +): [Input, RequestOptions | undefined] { + if (typeof first === 'string') { + return [{ ...(second as Record), [key]: first } as Input, third]; + } + + return [first, second as RequestOptions | undefined]; +} + +/** + * Typed client for the Umami API. Every method corresponds to an OpenAPI `operationId`. + * + * ```ts + * const umami = new UmamiClient({ baseUrl: 'https://api.umami.is/v1', apiKey: process.env.UMAMI_API_KEY }); + * const stats = await umami.getWebsiteStats({ websiteId, startAt, endAt }); + * ``` + * + * Methods accept a single object that merges path parameters, query parameters and the request + * body. Non-2xx responses throw {@link UmamiApiError}. + */ +export class UmamiClient extends GeneratedUmamiClient { + readonly baseUrl: string; + readonly headers: Record; + readonly timeout?: number; + private readonly fetchImpl: FetchLike; + + constructor(options: UmamiClientOptions = {}) { + super(); + this.baseUrl = trimSlashes(options.baseUrl || DEFAULT_BASE_URL); + this.headers = { ...buildAuthHeaders(options), ...options.headers }; + this.timeout = options.timeout; + this.fetchImpl = resolveFetch(options.fetch); + } + + /** Returns a copy of this client using a different bearer token. */ + withToken(token: string) { + const { authorization: _auth, [API_KEY_HEADER]: _key, ...headers } = this.headers; + + return new UmamiClient({ + baseUrl: this.baseUrl, + token, + headers, + fetch: this.fetchImpl, + timeout: this.timeout, + }); + } + + /** Low-level escape hatch: call any operation by ID. */ + call( + operationId: K, + input?: Input, + options?: RequestOptions, + ): Output { + return this.execute(operationId, input, options); + } + + protected async execute( + operationId: K, + input: Record | undefined, + options?: RequestOptions, + ): Output { + const operation = operations[operationId]; + const { path, query, body } = splitInput(operation, input); + const url = buildUrl(this.baseUrl, buildPath(operation.path, path), query); + + return sendRequest(this.fetchImpl, { + method: operation.method, + url, + headers: { ...this.headers, ...options?.headers }, + body, + signal: options?.signal, + timeout: this.timeout, + }); + } + + // --------------------------------------------------------------------------- + // Compatibility overloads for the previous hand-written @umami/api-client. + // The object form is canonical; positional forms are kept so that + // `client.getWebsiteStats(websiteId, params)` keeps working. + // --------------------------------------------------------------------------- + + /** @deprecated Use `listWebsites`. */ + getWebsites(input?: Input<'listWebsites'>, options?: RequestOptions) { + return this.listWebsites(input, options); + } + + override getWebsite(input: Input<'getWebsite'>, options?: RequestOptions): Output<'getWebsite'>; + override getWebsite(websiteId: string, options?: RequestOptions): Output<'getWebsite'>; + override getWebsite(first: string | Input<'getWebsite'>, second?: RequestOptions) { + const [input, options] = positional<'getWebsite'>('websiteId', first, undefined, second); + return super.getWebsite(input, options); + } + + override updateWebsite( + input: Input<'updateWebsite'>, + options?: RequestOptions, + ): Output<'updateWebsite'>; + override updateWebsite( + websiteId: string, + data: Params<'updateWebsite', 'websiteId'>, + options?: RequestOptions, + ): Output<'updateWebsite'>; + override updateWebsite( + first: string | Input<'updateWebsite'>, + second?: Record | RequestOptions, + third?: RequestOptions, + ) { + const [input, options] = positional<'updateWebsite'>('websiteId', first, second, third); + return super.updateWebsite(input, options); + } + + override deleteWebsite( + input: Input<'deleteWebsite'>, + options?: RequestOptions, + ): Output<'deleteWebsite'>; + override deleteWebsite(websiteId: string, options?: RequestOptions): Output<'deleteWebsite'>; + override deleteWebsite(first: string | Input<'deleteWebsite'>, second?: RequestOptions) { + const [input, options] = positional<'deleteWebsite'>('websiteId', first, undefined, second); + return super.deleteWebsite(input, options); + } + + override getWebsiteActive( + input: Input<'getWebsiteActive'>, + options?: RequestOptions, + ): Output<'getWebsiteActive'>; + override getWebsiteActive( + websiteId: string, + options?: RequestOptions, + ): Output<'getWebsiteActive'>; + override getWebsiteActive(first: string | Input<'getWebsiteActive'>, second?: RequestOptions) { + const [input, options] = positional<'getWebsiteActive'>('websiteId', first, undefined, second); + return super.getWebsiteActive(input, options); + } + + override getWebsiteDateRange( + input: Input<'getWebsiteDateRange'>, + options?: RequestOptions, + ): Output<'getWebsiteDateRange'>; + override getWebsiteDateRange( + websiteId: string, + options?: RequestOptions, + ): Output<'getWebsiteDateRange'>; + override getWebsiteDateRange( + first: string | Input<'getWebsiteDateRange'>, + second?: RequestOptions, + ) { + const [input, options] = positional<'getWebsiteDateRange'>( + 'websiteId', + first, + undefined, + second, + ); + return super.getWebsiteDateRange(input, options); + } + + override getWebsiteStats( + input: Input<'getWebsiteStats'>, + options?: RequestOptions, + ): Output<'getWebsiteStats'>; + override getWebsiteStats( + websiteId: string, + params: Params<'getWebsiteStats', 'websiteId'>, + options?: RequestOptions, + ): Output<'getWebsiteStats'>; + override getWebsiteStats( + first: string | Input<'getWebsiteStats'>, + second?: Record | RequestOptions, + third?: RequestOptions, + ) { + const [input, options] = positional<'getWebsiteStats'>('websiteId', first, second, third); + return super.getWebsiteStats(input, options); + } + + override getWebsitePageviews( + input: Input<'getWebsitePageviews'>, + options?: RequestOptions, + ): Output<'getWebsitePageviews'>; + override getWebsitePageviews( + websiteId: string, + params: Params<'getWebsitePageviews', 'websiteId'>, + options?: RequestOptions, + ): Output<'getWebsitePageviews'>; + override getWebsitePageviews( + first: string | Input<'getWebsitePageviews'>, + second?: Record | RequestOptions, + third?: RequestOptions, + ) { + const [input, options] = positional<'getWebsitePageviews'>('websiteId', first, second, third); + return super.getWebsitePageviews(input, options); + } + + override getWebsiteMetrics( + input: Input<'getWebsiteMetrics'>, + options?: RequestOptions, + ): Output<'getWebsiteMetrics'>; + override getWebsiteMetrics( + websiteId: string, + params: Params<'getWebsiteMetrics', 'websiteId'>, + options?: RequestOptions, + ): Output<'getWebsiteMetrics'>; + override getWebsiteMetrics( + first: string | Input<'getWebsiteMetrics'>, + second?: Record | RequestOptions, + third?: RequestOptions, + ) { + const [input, options] = positional<'getWebsiteMetrics'>('websiteId', first, second, third); + return super.getWebsiteMetrics(input, options); + } + + override getWebsiteExpandedMetrics( + input: Input<'getWebsiteExpandedMetrics'>, + options?: RequestOptions, + ): Output<'getWebsiteExpandedMetrics'>; + override getWebsiteExpandedMetrics( + websiteId: string, + params: Params<'getWebsiteExpandedMetrics', 'websiteId'>, + options?: RequestOptions, + ): Output<'getWebsiteExpandedMetrics'>; + override getWebsiteExpandedMetrics( + first: string | Input<'getWebsiteExpandedMetrics'>, + second?: Record | RequestOptions, + third?: RequestOptions, + ) { + const [input, options] = positional<'getWebsiteExpandedMetrics'>( + 'websiteId', + first, + second, + third, + ); + return super.getWebsiteExpandedMetrics(input, options); + } + + override getWebsiteEvents( + input: Input<'getWebsiteEvents'>, + options?: RequestOptions, + ): Output<'getWebsiteEvents'>; + override getWebsiteEvents( + websiteId: string, + params: Params<'getWebsiteEvents', 'websiteId'>, + options?: RequestOptions, + ): Output<'getWebsiteEvents'>; + override getWebsiteEvents( + first: string | Input<'getWebsiteEvents'>, + second?: Record | RequestOptions, + third?: RequestOptions, + ) { + const [input, options] = positional<'getWebsiteEvents'>('websiteId', first, second, third); + return super.getWebsiteEvents(input, options); + } + + override getWebsiteEventSeries( + input: Input<'getWebsiteEventSeries'>, + options?: RequestOptions, + ): Output<'getWebsiteEventSeries'>; + override getWebsiteEventSeries( + websiteId: string, + params: Params<'getWebsiteEventSeries', 'websiteId'>, + options?: RequestOptions, + ): Output<'getWebsiteEventSeries'>; + override getWebsiteEventSeries( + first: string | Input<'getWebsiteEventSeries'>, + second?: Record | RequestOptions, + third?: RequestOptions, + ) { + const [input, options] = positional<'getWebsiteEventSeries'>('websiteId', first, second, third); + return super.getWebsiteEventSeries(input, options); + } + + override getWebsiteEventStats( + input: Input<'getWebsiteEventStats'>, + options?: RequestOptions, + ): Output<'getWebsiteEventStats'>; + override getWebsiteEventStats( + websiteId: string, + params: Params<'getWebsiteEventStats', 'websiteId'>, + options?: RequestOptions, + ): Output<'getWebsiteEventStats'>; + override getWebsiteEventStats( + first: string | Input<'getWebsiteEventStats'>, + second?: Record | RequestOptions, + third?: RequestOptions, + ) { + const [input, options] = positional<'getWebsiteEventStats'>('websiteId', first, second, third); + return super.getWebsiteEventStats(input, options); + } + + override getEventData( + input: Input<'getEventData'>, + options?: RequestOptions, + ): Output<'getEventData'>; + override getEventData( + websiteId: string, + params: Params<'getEventData', 'websiteId'>, + options?: RequestOptions, + ): Output<'getEventData'>; + override getEventData( + first: string | Input<'getEventData'>, + second?: Record | RequestOptions, + third?: RequestOptions, + ) { + const [input, options] = positional<'getEventData'>('websiteId', first, second, third); + return super.getEventData(input, options); + } + + override getEventDataStats( + input: Input<'getEventDataStats'>, + options?: RequestOptions, + ): Output<'getEventDataStats'>; + override getEventDataStats( + websiteId: string, + params: Params<'getEventDataStats', 'websiteId'>, + options?: RequestOptions, + ): Output<'getEventDataStats'>; + override getEventDataStats( + first: string | Input<'getEventDataStats'>, + second?: Record | RequestOptions, + third?: RequestOptions, + ) { + const [input, options] = positional<'getEventDataStats'>('websiteId', first, second, third); + return super.getEventDataStats(input, options); + } + + override getEventDataProperties( + input: Input<'getEventDataProperties'>, + options?: RequestOptions, + ): Output<'getEventDataProperties'>; + override getEventDataProperties( + websiteId: string, + params: Params<'getEventDataProperties', 'websiteId'>, + options?: RequestOptions, + ): Output<'getEventDataProperties'>; + override getEventDataProperties( + first: string | Input<'getEventDataProperties'>, + second?: Record | RequestOptions, + third?: RequestOptions, + ) { + const [input, options] = positional<'getEventDataProperties'>( + 'websiteId', + first, + second, + third, + ); + return super.getEventDataProperties(input, options); + } + + override getEventDataValues( + input: Input<'getEventDataValues'>, + options?: RequestOptions, + ): Output<'getEventDataValues'>; + override getEventDataValues( + websiteId: string, + params: Params<'getEventDataValues', 'websiteId'>, + options?: RequestOptions, + ): Output<'getEventDataValues'>; + override getEventDataValues( + first: string | Input<'getEventDataValues'>, + second?: Record | RequestOptions, + third?: RequestOptions, + ) { + const [input, options] = positional<'getEventDataValues'>('websiteId', first, second, third); + return super.getEventDataValues(input, options); + } + + override getWebsiteSessions( + input: Input<'getWebsiteSessions'>, + options?: RequestOptions, + ): Output<'getWebsiteSessions'>; + override getWebsiteSessions( + websiteId: string, + params: Params<'getWebsiteSessions', 'websiteId'>, + options?: RequestOptions, + ): Output<'getWebsiteSessions'>; + override getWebsiteSessions( + first: string | Input<'getWebsiteSessions'>, + second?: Record | RequestOptions, + third?: RequestOptions, + ) { + const [input, options] = positional<'getWebsiteSessions'>('websiteId', first, second, third); + return super.getWebsiteSessions(input, options); + } + + override getWebsiteSessionStats( + input: Input<'getWebsiteSessionStats'>, + options?: RequestOptions, + ): Output<'getWebsiteSessionStats'>; + override getWebsiteSessionStats( + websiteId: string, + params: Params<'getWebsiteSessionStats', 'websiteId'>, + options?: RequestOptions, + ): Output<'getWebsiteSessionStats'>; + override getWebsiteSessionStats( + first: string | Input<'getWebsiteSessionStats'>, + second?: Record | RequestOptions, + third?: RequestOptions, + ) { + const [input, options] = positional<'getWebsiteSessionStats'>( + 'websiteId', + first, + second, + third, + ); + return super.getWebsiteSessionStats(input, options); + } + + override getWebsiteSession( + input: Input<'getWebsiteSession'>, + options?: RequestOptions, + ): Output<'getWebsiteSession'>; + override getWebsiteSession( + websiteId: string, + sessionId: string, + options?: RequestOptions, + ): Output<'getWebsiteSession'>; + override getWebsiteSession( + first: string | Input<'getWebsiteSession'>, + second?: string | RequestOptions, + third?: RequestOptions, + ) { + if (typeof first === 'string') { + return super.getWebsiteSession({ websiteId: first, sessionId: second as string }, third); + } + + return super.getWebsiteSession(first, second as RequestOptions | undefined); + } + + override getWebsiteSessionActivity( + input: Input<'getWebsiteSessionActivity'>, + options?: RequestOptions, + ): Output<'getWebsiteSessionActivity'>; + override getWebsiteSessionActivity( + websiteId: string, + sessionId: string, + params: Params<'getWebsiteSessionActivity', 'websiteId' | 'sessionId'>, + options?: RequestOptions, + ): Output<'getWebsiteSessionActivity'>; + override getWebsiteSessionActivity( + first: string | Input<'getWebsiteSessionActivity'>, + second?: string | RequestOptions, + third?: Record | RequestOptions, + fourth?: RequestOptions, + ) { + if (typeof first === 'string') { + return super.getWebsiteSessionActivity( + { + ...(third as Record), + websiteId: first, + sessionId: second as string, + } as Input<'getWebsiteSessionActivity'>, + fourth, + ); + } + + return super.getWebsiteSessionActivity(first, second as RequestOptions | undefined); + } + + override getWebsiteSessionProperties( + input: Input<'getWebsiteSessionProperties'>, + options?: RequestOptions, + ): Output<'getWebsiteSessionProperties'>; + override getWebsiteSessionProperties( + websiteId: string, + sessionId: string, + options?: RequestOptions, + ): Output<'getWebsiteSessionProperties'>; + override getWebsiteSessionProperties( + first: string | Input<'getWebsiteSessionProperties'>, + second?: string | RequestOptions, + third?: RequestOptions, + ) { + if (typeof first === 'string') { + return super.getWebsiteSessionProperties( + { websiteId: first, sessionId: second as string }, + third, + ); + } + + return super.getWebsiteSessionProperties(first, second as RequestOptions | undefined); + } + + override getWebsiteReports( + input: Input<'getWebsiteReports'>, + options?: RequestOptions, + ): Output<'getWebsiteReports'>; + override getWebsiteReports( + websiteId: string, + params?: Params<'getWebsiteReports', 'websiteId'>, + options?: RequestOptions, + ): Output<'getWebsiteReports'>; + override getWebsiteReports( + first: string | Input<'getWebsiteReports'>, + second?: Record | RequestOptions, + third?: RequestOptions, + ) { + const [input, options] = positional<'getWebsiteReports'>('websiteId', first, second, third); + return super.getWebsiteReports(input, options); + } +} diff --git a/packages/api-client/src/errors.ts b/packages/api-client/src/errors.ts new file mode 100644 index 000000000..c4298552a --- /dev/null +++ b/packages/api-client/src/errors.ts @@ -0,0 +1,100 @@ +export interface UmamiApiErrorBody { + error?: { + message?: string; + code?: string; + status?: number; + [key: string]: unknown; + }; + [key: string]: unknown; +} + +/** + * Thrown for any non-2xx response. `code` mirrors the API's error code + * (`unauthorized`, `forbidden`, `not-found`, `bad-request`, `server-error`, …). + */ +export class UmamiApiError extends Error { + readonly status: number; + readonly code: string; + readonly method: string; + readonly url: string; + readonly body: unknown; + + constructor(options: { + status: number; + code?: string; + message?: string; + method: string; + url: string; + body?: unknown; + }) { + super(options.message ?? defaultMessage(options.status)); + this.name = 'UmamiApiError'; + this.status = options.status; + this.code = options.code ?? defaultCode(options.status); + this.method = options.method; + this.url = options.url; + this.body = options.body; + } + + get isUnauthorized() { + return this.status === 401; + } + + get isForbidden() { + return this.status === 403; + } + + get isNotFound() { + return this.status === 404; + } + + get isRateLimited() { + return this.status === 429; + } + + get isServerError() { + return this.status >= 500; + } +} + +export function isUmamiApiError(error: unknown): error is UmamiApiError { + return error instanceof UmamiApiError || (error as { name?: string })?.name === 'UmamiApiError'; +} + +function defaultCode(status: number) { + switch (status) { + case 400: + return 'bad-request'; + case 401: + return 'unauthorized'; + case 403: + return 'forbidden'; + case 404: + return 'not-found'; + case 413: + return 'payload-too-large'; + case 429: + return 'rate-limited'; + case 503: + return 'service-unavailable'; + default: + return status >= 500 ? 'server-error' : 'request-failed'; + } +} + +function defaultMessage(status: number) { + switch (status) { + case 400: + return 'Bad request'; + case 401: + return 'Unauthorized'; + case 403: + return 'Forbidden'; + case 404: + return 'Not found'; + case 429: + return 'Too many requests'; + default: + return status >= 500 ? 'Server error' : `Request failed with status ${status}`; + } +} diff --git a/packages/api-client/src/generated/operations.ts b/packages/api-client/src/generated/operations.ts new file mode 100644 index 000000000..36821e308 --- /dev/null +++ b/packages/api-client/src/generated/operations.ts @@ -0,0 +1,4496 @@ +// GENERATED FILE. DO NOT EDIT. +// Source: public/openapi.json — regenerate with `pnpm generate:api`. + +import type { RequestOptions } from '../types'; +import type { components, operations as OperationTypes } from './types'; + +export const API_VERSION = '3.3.0'; + +export type HttpMethod = 'get' | 'post' | 'put' | 'patch' | 'delete'; + +export interface OperationDefinition { + readonly operationId: string; + readonly method: HttpMethod; + readonly path: string; + readonly pathParams: readonly string[]; + readonly queryParams: readonly string[]; + readonly hasBody: boolean; + readonly scope?: string; +} + +export const operations = { + batch: { + operationId: 'batch', + method: 'post', + path: '/api/batch', + pathParams: [], + queryParams: [], + hasBody: true, + }, + cancelTwoFactorSetup: { + operationId: 'cancelTwoFactorSetup', + method: 'post', + path: '/api/2fa/setup/cancel', + pathParams: [], + queryParams: [], + hasBody: false, + }, + cloneBoard: { + operationId: 'cloneBoard', + method: 'post', + path: '/api/boards/{boardId}/clone', + pathParams: ['boardId'], + queryParams: [], + hasBody: true, + }, + confirmTwoFactorSetup: { + operationId: 'confirmTwoFactorSetup', + method: 'post', + path: '/api/2fa/setup/confirm', + pathParams: [], + queryParams: [], + hasBody: true, + }, + createBoard: { + operationId: 'createBoard', + method: 'post', + path: '/api/boards', + pathParams: [], + queryParams: [], + hasBody: true, + }, + createBoardShare: { + operationId: 'createBoardShare', + method: 'post', + path: '/api/boards/{boardId}/shares', + pathParams: ['boardId'], + queryParams: [], + hasBody: true, + }, + createLink: { + operationId: 'createLink', + method: 'post', + path: '/api/links', + pathParams: [], + queryParams: [], + hasBody: true, + }, + createLinkShare: { + operationId: 'createLinkShare', + method: 'post', + path: '/api/links/{linkId}/shares', + pathParams: ['linkId'], + queryParams: [], + hasBody: true, + }, + createMyApiKey: { + operationId: 'createMyApiKey', + method: 'post', + path: '/api/me/api-keys', + pathParams: [], + queryParams: [], + hasBody: true, + }, + createPixel: { + operationId: 'createPixel', + method: 'post', + path: '/api/pixels', + pathParams: [], + queryParams: [], + hasBody: true, + }, + createPixelShare: { + operationId: 'createPixelShare', + method: 'post', + path: '/api/pixels/{pixelId}/shares', + pathParams: ['pixelId'], + queryParams: [], + hasBody: true, + }, + createReport: { + operationId: 'createReport', + method: 'post', + path: '/api/reports', + pathParams: [], + queryParams: [], + hasBody: true, + }, + createShare: { + operationId: 'createShare', + method: 'post', + path: '/api/share', + pathParams: [], + queryParams: [], + hasBody: true, + }, + createTeam: { + operationId: 'createTeam', + method: 'post', + path: '/api/teams', + pathParams: [], + queryParams: [], + hasBody: true, + }, + createTeamUser: { + operationId: 'createTeamUser', + method: 'post', + path: '/api/teams/{teamId}/users', + pathParams: ['teamId'], + queryParams: [], + hasBody: true, + }, + createUser: { + operationId: 'createUser', + method: 'post', + path: '/api/users', + pathParams: [], + queryParams: [], + hasBody: true, + }, + createWebsite: { + operationId: 'createWebsite', + method: 'post', + path: '/api/websites', + pathParams: [], + queryParams: [], + hasBody: true, + }, + createWebsiteAnnotation: { + operationId: 'createWebsiteAnnotation', + method: 'post', + path: '/api/websites/{websiteId}/annotations', + pathParams: ['websiteId'], + queryParams: [], + hasBody: true, + }, + createWebsiteSegment: { + operationId: 'createWebsiteSegment', + method: 'post', + path: '/api/websites/{websiteId}/segments', + pathParams: ['websiteId'], + queryParams: [], + hasBody: true, + }, + createWebsiteShare: { + operationId: 'createWebsiteShare', + method: 'post', + path: '/api/websites/{websiteId}/shares', + pathParams: ['websiteId'], + queryParams: [], + hasBody: true, + }, + deleteBoard: { + operationId: 'deleteBoard', + method: 'delete', + path: '/api/boards/{boardId}', + pathParams: ['boardId'], + queryParams: [], + hasBody: false, + }, + deleteLink: { + operationId: 'deleteLink', + method: 'delete', + path: '/api/links/{linkId}', + pathParams: ['linkId'], + queryParams: [], + hasBody: false, + }, + deleteMyApiKey: { + operationId: 'deleteMyApiKey', + method: 'delete', + path: '/api/me/api-keys/{keyId}', + pathParams: ['keyId'], + queryParams: [], + hasBody: false, + }, + deletePixel: { + operationId: 'deletePixel', + method: 'delete', + path: '/api/pixels/{pixelId}', + pathParams: ['pixelId'], + queryParams: [], + hasBody: false, + }, + deleteReport: { + operationId: 'deleteReport', + method: 'delete', + path: '/api/reports/{reportId}', + pathParams: ['reportId'], + queryParams: [], + hasBody: false, + }, + deleteShare: { + operationId: 'deleteShare', + method: 'delete', + path: '/api/share/id/{shareId}', + pathParams: ['shareId'], + queryParams: [], + hasBody: false, + }, + deleteTeam: { + operationId: 'deleteTeam', + method: 'delete', + path: '/api/teams/{teamId}', + pathParams: ['teamId'], + queryParams: [], + hasBody: false, + }, + deleteTeamUser: { + operationId: 'deleteTeamUser', + method: 'delete', + path: '/api/teams/{teamId}/users/{userId}', + pathParams: ['teamId', 'userId'], + queryParams: [], + hasBody: false, + }, + deleteUser: { + operationId: 'deleteUser', + method: 'delete', + path: '/api/users/{userId}', + pathParams: ['userId'], + queryParams: [], + hasBody: false, + }, + deleteWebsite: { + operationId: 'deleteWebsite', + method: 'delete', + path: '/api/websites/{websiteId}', + pathParams: ['websiteId'], + queryParams: [], + hasBody: false, + }, + deleteWebsiteAnnotation: { + operationId: 'deleteWebsiteAnnotation', + method: 'delete', + path: '/api/websites/{websiteId}/annotations/{annotationId}', + pathParams: ['websiteId', 'annotationId'], + queryParams: [], + hasBody: false, + }, + deleteWebsiteSegment: { + operationId: 'deleteWebsiteSegment', + method: 'delete', + path: '/api/websites/{websiteId}/segments/{segmentId}', + pathParams: ['websiteId', 'segmentId'], + queryParams: [], + hasBody: false, + }, + deleteWebsiteSession: { + operationId: 'deleteWebsiteSession', + method: 'delete', + path: '/api/websites/{websiteId}/sessions/{sessionId}', + pathParams: ['websiteId', 'sessionId'], + queryParams: [], + hasBody: false, + }, + disableTwoFactor: { + operationId: 'disableTwoFactor', + method: 'post', + path: '/api/2fa/disable', + pathParams: [], + queryParams: [], + hasBody: true, + }, + exportWebsite: { + operationId: 'exportWebsite', + method: 'get', + path: '/api/websites/{websiteId}/export', + pathParams: ['websiteId'], + queryParams: [ + 'startAt', + 'endAt', + 'startDate', + 'endDate', + 'timezone', + 'unit', + 'compare', + 'page', + 'pageSize', + 'maxResults', + ], + hasBody: false, + }, + getBoard: { + operationId: 'getBoard', + method: 'get', + path: '/api/boards/{boardId}', + pathParams: ['boardId'], + queryParams: [], + hasBody: false, + }, + getBoards: { + operationId: 'getBoards', + method: 'get', + path: '/api/boards', + pathParams: [], + queryParams: ['page', 'pageSize', 'maxResults', 'search', 'orderBy', 'sortDescending'], + hasBody: false, + }, + getBoardShares: { + operationId: 'getBoardShares', + method: 'get', + path: '/api/boards/{boardId}/shares', + pathParams: ['boardId'], + queryParams: [ + 'path', + 'referrer', + 'title', + 'query', + 'os', + 'browser', + 'device', + 'country', + 'region', + 'city', + 'tag', + 'hostname', + 'distinctId', + 'language', + 'event', + 'utmSource', + 'utmMedium', + 'utmCampaign', + 'utmContent', + 'utmTerm', + 'excludeBounce', + 'segment', + 'cohort', + 'eventType', + 'match', + 'page', + 'pageSize', + 'maxResults', + ], + hasBody: false, + }, + getEventData: { + operationId: 'getEventData', + method: 'get', + path: '/api/websites/{websiteId}/event-data', + pathParams: ['websiteId'], + queryParams: [ + 'startAt', + 'endAt', + 'path', + 'referrer', + 'title', + 'query', + 'os', + 'browser', + 'device', + 'country', + 'region', + 'city', + 'tag', + 'hostname', + 'distinctId', + 'language', + 'event', + 'utmSource', + 'utmMedium', + 'utmCampaign', + 'utmContent', + 'utmTerm', + 'excludeBounce', + 'segment', + 'cohort', + 'eventType', + 'match', + 'page', + 'pageSize', + 'maxResults', + ], + hasBody: false, + scope: 'analytics:read', + }, + getEventDataArraySeries: { + operationId: 'getEventDataArraySeries', + method: 'get', + path: '/api/websites/{websiteId}/event-data-pivot/array-series', + pathParams: ['websiteId'], + queryParams: [ + 'startAt', + 'endAt', + 'eventName', + 'propertyName', + 'timezone', + 'unit', + 'path', + 'referrer', + 'title', + 'query', + 'os', + 'browser', + 'device', + 'country', + 'region', + 'city', + 'tag', + 'hostname', + 'distinctId', + 'language', + 'event', + 'utmSource', + 'utmMedium', + 'utmCampaign', + 'utmContent', + 'utmTerm', + 'excludeBounce', + 'segment', + 'cohort', + 'eventType', + 'match', + ], + hasBody: false, + }, + getEventDataById: { + operationId: 'getEventDataById', + method: 'get', + path: '/api/websites/{websiteId}/event-data/{eventId}', + pathParams: ['websiteId', 'eventId'], + queryParams: [], + hasBody: false, + }, + getEventDataDateSeries: { + operationId: 'getEventDataDateSeries', + method: 'get', + path: '/api/websites/{websiteId}/event-data-pivot/date-series', + pathParams: ['websiteId'], + queryParams: [ + 'startAt', + 'endAt', + 'eventName', + 'propertyName', + 'timezone', + 'path', + 'referrer', + 'title', + 'query', + 'os', + 'browser', + 'device', + 'country', + 'region', + 'city', + 'tag', + 'hostname', + 'distinctId', + 'language', + 'event', + 'utmSource', + 'utmMedium', + 'utmCampaign', + 'utmContent', + 'utmTerm', + 'excludeBounce', + 'segment', + 'cohort', + 'eventType', + 'match', + ], + hasBody: false, + }, + getEventDataEvents: { + operationId: 'getEventDataEvents', + method: 'get', + path: '/api/websites/{websiteId}/event-data/events', + pathParams: ['websiteId'], + queryParams: [ + 'startAt', + 'endAt', + 'event', + 'path', + 'referrer', + 'title', + 'query', + 'os', + 'browser', + 'device', + 'country', + 'region', + 'city', + 'tag', + 'hostname', + 'distinctId', + 'language', + 'utmSource', + 'utmMedium', + 'utmCampaign', + 'utmContent', + 'utmTerm', + 'excludeBounce', + 'segment', + 'cohort', + 'eventType', + 'match', + ], + hasBody: false, + }, + getEventDataFields: { + operationId: 'getEventDataFields', + method: 'get', + path: '/api/websites/{websiteId}/event-data/fields', + pathParams: ['websiteId'], + queryParams: [ + 'startAt', + 'endAt', + 'eventName', + 'path', + 'referrer', + 'title', + 'query', + 'os', + 'browser', + 'device', + 'country', + 'region', + 'city', + 'tag', + 'hostname', + 'distinctId', + 'language', + 'event', + 'utmSource', + 'utmMedium', + 'utmCampaign', + 'utmContent', + 'utmTerm', + 'excludeBounce', + 'segment', + 'cohort', + 'eventType', + 'match', + ], + hasBody: false, + }, + getEventDataNumericSeries: { + operationId: 'getEventDataNumericSeries', + method: 'get', + path: '/api/websites/{websiteId}/event-data-pivot/numeric-series', + pathParams: ['websiteId'], + queryParams: [ + 'startAt', + 'endAt', + 'eventName', + 'propertyName', + 'metric', + 'timezone', + 'unit', + 'path', + 'referrer', + 'title', + 'query', + 'os', + 'browser', + 'device', + 'country', + 'region', + 'city', + 'tag', + 'hostname', + 'distinctId', + 'language', + 'event', + 'utmSource', + 'utmMedium', + 'utmCampaign', + 'utmContent', + 'utmTerm', + 'excludeBounce', + 'segment', + 'cohort', + 'eventType', + 'match', + ], + hasBody: false, + }, + getEventDataNumericStats: { + operationId: 'getEventDataNumericStats', + method: 'get', + path: '/api/websites/{websiteId}/event-data-pivot/numeric-stats', + pathParams: ['websiteId'], + queryParams: [ + 'startAt', + 'endAt', + 'eventName', + 'propertyName', + 'path', + 'referrer', + 'title', + 'query', + 'os', + 'browser', + 'device', + 'country', + 'region', + 'city', + 'tag', + 'hostname', + 'distinctId', + 'language', + 'event', + 'utmSource', + 'utmMedium', + 'utmCampaign', + 'utmContent', + 'utmTerm', + 'excludeBounce', + 'segment', + 'cohort', + 'eventType', + 'match', + ], + hasBody: false, + }, + getEventDataPivot: { + operationId: 'getEventDataPivot', + method: 'get', + path: '/api/websites/{websiteId}/event-data-pivot', + pathParams: ['websiteId'], + queryParams: [ + 'startAt', + 'endAt', + 'eventName', + 'timezone', + 'unit', + 'path', + 'referrer', + 'title', + 'query', + 'os', + 'browser', + 'device', + 'country', + 'region', + 'city', + 'tag', + 'hostname', + 'distinctId', + 'language', + 'event', + 'utmSource', + 'utmMedium', + 'utmCampaign', + 'utmContent', + 'utmTerm', + 'excludeBounce', + 'segment', + 'cohort', + 'eventType', + 'match', + 'page', + 'pageSize', + 'maxResults', + ], + hasBody: false, + }, + getEventDataProperties: { + operationId: 'getEventDataProperties', + method: 'get', + path: '/api/websites/{websiteId}/event-data/properties', + pathParams: ['websiteId'], + queryParams: [ + 'startAt', + 'endAt', + 'path', + 'referrer', + 'title', + 'query', + 'os', + 'browser', + 'device', + 'country', + 'region', + 'city', + 'tag', + 'hostname', + 'distinctId', + 'language', + 'event', + 'utmSource', + 'utmMedium', + 'utmCampaign', + 'utmContent', + 'utmTerm', + 'excludeBounce', + 'segment', + 'cohort', + 'eventType', + 'match', + ], + hasBody: false, + scope: 'analytics:read', + }, + getEventDataPropertySeries: { + operationId: 'getEventDataPropertySeries', + method: 'get', + path: '/api/websites/{websiteId}/event-data-pivot/property-series', + pathParams: ['websiteId'], + queryParams: [ + 'startAt', + 'endAt', + 'eventName', + 'propertyName', + 'timezone', + 'unit', + 'path', + 'referrer', + 'title', + 'query', + 'os', + 'browser', + 'device', + 'country', + 'region', + 'city', + 'tag', + 'hostname', + 'distinctId', + 'language', + 'event', + 'utmSource', + 'utmMedium', + 'utmCampaign', + 'utmContent', + 'utmTerm', + 'excludeBounce', + 'segment', + 'cohort', + 'eventType', + 'match', + ], + hasBody: false, + }, + getEventDataStats: { + operationId: 'getEventDataStats', + method: 'get', + path: '/api/websites/{websiteId}/event-data/stats', + pathParams: ['websiteId'], + queryParams: [ + 'startAt', + 'endAt', + 'path', + 'referrer', + 'title', + 'query', + 'os', + 'browser', + 'device', + 'country', + 'region', + 'city', + 'tag', + 'hostname', + 'distinctId', + 'language', + 'event', + 'utmSource', + 'utmMedium', + 'utmCampaign', + 'utmContent', + 'utmTerm', + 'excludeBounce', + 'segment', + 'cohort', + 'eventType', + 'match', + ], + hasBody: false, + scope: 'analytics:read', + }, + getEventDataValues: { + operationId: 'getEventDataValues', + method: 'get', + path: '/api/websites/{websiteId}/event-data/values', + pathParams: ['websiteId'], + queryParams: [ + 'startAt', + 'endAt', + 'eventName', + 'propertyName', + 'dataType', + 'path', + 'referrer', + 'title', + 'query', + 'os', + 'browser', + 'device', + 'country', + 'region', + 'city', + 'tag', + 'hostname', + 'distinctId', + 'language', + 'event', + 'utmSource', + 'utmMedium', + 'utmCampaign', + 'utmContent', + 'utmTerm', + 'excludeBounce', + 'segment', + 'cohort', + 'eventType', + 'match', + ], + hasBody: false, + scope: 'analytics:read', + }, + getLink: { + operationId: 'getLink', + method: 'get', + path: '/api/links/{linkId}', + pathParams: ['linkId'], + queryParams: [], + hasBody: false, + }, + getLinks: { + operationId: 'getLinks', + method: 'get', + path: '/api/links', + pathParams: [], + queryParams: ['page', 'pageSize', 'maxResults', 'search', 'orderBy', 'sortDescending'], + hasBody: false, + }, + getLinksCharts: { + operationId: 'getLinksCharts', + method: 'get', + path: '/api/links/charts', + pathParams: [], + queryParams: ['ids', 'startAt', 'endAt', 'timezone'], + hasBody: false, + }, + getLinkShares: { + operationId: 'getLinkShares', + method: 'get', + path: '/api/links/{linkId}/shares', + pathParams: ['linkId'], + queryParams: [ + 'path', + 'referrer', + 'title', + 'query', + 'os', + 'browser', + 'device', + 'country', + 'region', + 'city', + 'tag', + 'hostname', + 'distinctId', + 'language', + 'event', + 'utmSource', + 'utmMedium', + 'utmCampaign', + 'utmContent', + 'utmTerm', + 'excludeBounce', + 'segment', + 'cohort', + 'eventType', + 'match', + 'page', + 'pageSize', + 'maxResults', + ], + hasBody: false, + }, + getMe: { + operationId: 'getMe', + method: 'get', + path: '/api/me', + pathParams: [], + queryParams: [], + hasBody: false, + scope: 'websites:read', + }, + getMyApiKeys: { + operationId: 'getMyApiKeys', + method: 'get', + path: '/api/me/api-keys', + pathParams: [], + queryParams: [], + hasBody: false, + }, + getMyTeams: { + operationId: 'getMyTeams', + method: 'get', + path: '/api/me/teams', + pathParams: [], + queryParams: ['page', 'pageSize', 'maxResults', 'orderBy', 'sortDescending'], + hasBody: false, + }, + getMyWebsites: { + operationId: 'getMyWebsites', + method: 'get', + path: '/api/me/websites', + pathParams: [], + queryParams: ['page', 'pageSize', 'maxResults', 'orderBy', 'sortDescending', 'includeTeams'], + hasBody: false, + }, + getPixel: { + operationId: 'getPixel', + method: 'get', + path: '/api/pixels/{pixelId}', + pathParams: ['pixelId'], + queryParams: [], + hasBody: false, + }, + getPixels: { + operationId: 'getPixels', + method: 'get', + path: '/api/pixels', + pathParams: [], + queryParams: ['page', 'pageSize', 'maxResults', 'search', 'orderBy', 'sortDescending'], + hasBody: false, + }, + getPixelsCharts: { + operationId: 'getPixelsCharts', + method: 'get', + path: '/api/pixels/charts', + pathParams: [], + queryParams: ['ids', 'startAt', 'endAt', 'timezone'], + hasBody: false, + }, + getPixelShares: { + operationId: 'getPixelShares', + method: 'get', + path: '/api/pixels/{pixelId}/shares', + pathParams: ['pixelId'], + queryParams: [ + 'path', + 'referrer', + 'title', + 'query', + 'os', + 'browser', + 'device', + 'country', + 'region', + 'city', + 'tag', + 'hostname', + 'distinctId', + 'language', + 'event', + 'utmSource', + 'utmMedium', + 'utmCampaign', + 'utmContent', + 'utmTerm', + 'excludeBounce', + 'segment', + 'cohort', + 'eventType', + 'match', + 'page', + 'pageSize', + 'maxResults', + ], + hasBody: false, + }, + getRealtime: { + operationId: 'getRealtime', + method: 'get', + path: '/api/realtime/{websiteId}', + pathParams: ['websiteId'], + queryParams: [ + 'timezone', + 'unit', + 'path', + 'referrer', + 'title', + 'query', + 'os', + 'browser', + 'device', + 'country', + 'region', + 'city', + 'tag', + 'hostname', + 'distinctId', + 'language', + 'event', + 'utmSource', + 'utmMedium', + 'utmCampaign', + 'utmContent', + 'utmTerm', + 'excludeBounce', + 'segment', + 'cohort', + 'eventType', + 'match', + ], + hasBody: false, + scope: 'analytics:read', + }, + getReport: { + operationId: 'getReport', + method: 'get', + path: '/api/reports/{reportId}', + pathParams: ['reportId'], + queryParams: [], + hasBody: false, + }, + getReports: { + operationId: 'getReports', + method: 'get', + path: '/api/reports', + pathParams: [], + queryParams: ['websiteId', 'type', 'page', 'pageSize', 'maxResults'], + hasBody: false, + }, + getSessionDataArraySeries: { + operationId: 'getSessionDataArraySeries', + method: 'get', + path: '/api/websites/{websiteId}/session-data/array-series', + pathParams: ['websiteId'], + queryParams: [ + 'startAt', + 'endAt', + 'propertyName', + 'timezone', + 'unit', + 'path', + 'referrer', + 'title', + 'query', + 'os', + 'browser', + 'device', + 'country', + 'region', + 'city', + 'tag', + 'hostname', + 'distinctId', + 'language', + 'event', + 'utmSource', + 'utmMedium', + 'utmCampaign', + 'utmContent', + 'utmTerm', + 'excludeBounce', + 'segment', + 'cohort', + 'eventType', + 'match', + ], + hasBody: false, + }, + getSessionDataDateSeries: { + operationId: 'getSessionDataDateSeries', + method: 'get', + path: '/api/websites/{websiteId}/session-data/date-series', + pathParams: ['websiteId'], + queryParams: [ + 'startAt', + 'endAt', + 'propertyName', + 'timezone', + 'path', + 'referrer', + 'title', + 'query', + 'os', + 'browser', + 'device', + 'country', + 'region', + 'city', + 'tag', + 'hostname', + 'distinctId', + 'language', + 'event', + 'utmSource', + 'utmMedium', + 'utmCampaign', + 'utmContent', + 'utmTerm', + 'excludeBounce', + 'segment', + 'cohort', + 'eventType', + 'match', + ], + hasBody: false, + }, + getSessionDataNumericSeries: { + operationId: 'getSessionDataNumericSeries', + method: 'get', + path: '/api/websites/{websiteId}/session-data/numeric-series', + pathParams: ['websiteId'], + queryParams: [ + 'startAt', + 'endAt', + 'propertyName', + 'metric', + 'timezone', + 'unit', + 'path', + 'referrer', + 'title', + 'query', + 'os', + 'browser', + 'device', + 'country', + 'region', + 'city', + 'tag', + 'hostname', + 'distinctId', + 'language', + 'event', + 'utmSource', + 'utmMedium', + 'utmCampaign', + 'utmContent', + 'utmTerm', + 'excludeBounce', + 'segment', + 'cohort', + 'eventType', + 'match', + ], + hasBody: false, + }, + getSessionDataNumericStats: { + operationId: 'getSessionDataNumericStats', + method: 'get', + path: '/api/websites/{websiteId}/session-data/numeric-stats', + pathParams: ['websiteId'], + queryParams: [ + 'startAt', + 'endAt', + 'propertyName', + 'timezone', + 'path', + 'referrer', + 'title', + 'query', + 'os', + 'browser', + 'device', + 'country', + 'region', + 'city', + 'tag', + 'hostname', + 'distinctId', + 'language', + 'event', + 'utmSource', + 'utmMedium', + 'utmCampaign', + 'utmContent', + 'utmTerm', + 'excludeBounce', + 'segment', + 'cohort', + 'eventType', + 'match', + ], + hasBody: false, + }, + getSessionDataPivot: { + operationId: 'getSessionDataPivot', + method: 'get', + path: '/api/websites/{websiteId}/session-data-pivot', + pathParams: ['websiteId'], + queryParams: [ + 'startAt', + 'endAt', + 'propertyName', + 'timezone', + 'unit', + 'path', + 'referrer', + 'title', + 'query', + 'os', + 'browser', + 'device', + 'country', + 'region', + 'city', + 'tag', + 'hostname', + 'distinctId', + 'language', + 'event', + 'utmSource', + 'utmMedium', + 'utmCampaign', + 'utmContent', + 'utmTerm', + 'excludeBounce', + 'segment', + 'cohort', + 'eventType', + 'match', + 'page', + 'pageSize', + 'maxResults', + ], + hasBody: false, + }, + getSessionDataProperties: { + operationId: 'getSessionDataProperties', + method: 'get', + path: '/api/websites/{websiteId}/session-data/properties', + pathParams: ['websiteId'], + queryParams: [ + 'startAt', + 'endAt', + 'propertyName', + 'path', + 'referrer', + 'title', + 'query', + 'os', + 'browser', + 'device', + 'country', + 'region', + 'city', + 'tag', + 'hostname', + 'distinctId', + 'language', + 'event', + 'utmSource', + 'utmMedium', + 'utmCampaign', + 'utmContent', + 'utmTerm', + 'excludeBounce', + 'segment', + 'cohort', + 'eventType', + 'match', + ], + hasBody: false, + }, + getSessionDataPropertySeries: { + operationId: 'getSessionDataPropertySeries', + method: 'get', + path: '/api/websites/{websiteId}/session-data/property-series', + pathParams: ['websiteId'], + queryParams: [ + 'startAt', + 'endAt', + 'propertyName', + 'timezone', + 'unit', + 'path', + 'referrer', + 'title', + 'query', + 'os', + 'browser', + 'device', + 'country', + 'region', + 'city', + 'tag', + 'hostname', + 'distinctId', + 'language', + 'event', + 'utmSource', + 'utmMedium', + 'utmCampaign', + 'utmContent', + 'utmTerm', + 'excludeBounce', + 'segment', + 'cohort', + 'eventType', + 'match', + ], + hasBody: false, + }, + getSessionDataStats: { + operationId: 'getSessionDataStats', + method: 'get', + path: '/api/websites/{websiteId}/session-data/stats', + pathParams: ['websiteId'], + queryParams: [ + 'startAt', + 'endAt', + 'propertyName', + 'timezone', + 'unit', + 'path', + 'referrer', + 'title', + 'query', + 'os', + 'browser', + 'device', + 'country', + 'region', + 'city', + 'tag', + 'hostname', + 'distinctId', + 'language', + 'event', + 'utmSource', + 'utmMedium', + 'utmCampaign', + 'utmContent', + 'utmTerm', + 'excludeBounce', + 'segment', + 'cohort', + 'eventType', + 'match', + ], + hasBody: false, + }, + getSessionDataValues: { + operationId: 'getSessionDataValues', + method: 'get', + path: '/api/websites/{websiteId}/session-data/values', + pathParams: ['websiteId'], + queryParams: [ + 'startAt', + 'endAt', + 'propertyName', + 'dataType', + 'path', + 'referrer', + 'title', + 'query', + 'os', + 'browser', + 'device', + 'country', + 'region', + 'city', + 'tag', + 'hostname', + 'distinctId', + 'language', + 'event', + 'utmSource', + 'utmMedium', + 'utmCampaign', + 'utmContent', + 'utmTerm', + 'excludeBounce', + 'segment', + 'cohort', + 'eventType', + 'match', + ], + hasBody: false, + }, + getShare: { + operationId: 'getShare', + method: 'get', + path: '/api/share/id/{shareId}', + pathParams: ['shareId'], + queryParams: [], + hasBody: false, + }, + getShareBySlug: { + operationId: 'getShareBySlug', + method: 'get', + path: '/api/share/{slug}', + pathParams: ['slug'], + queryParams: [], + hasBody: false, + }, + getSubscription: { + operationId: 'getSubscription', + method: 'get', + path: '/api/auth/subscription', + pathParams: [], + queryParams: ['teamId'], + hasBody: false, + }, + getTeam: { + operationId: 'getTeam', + method: 'get', + path: '/api/teams/{teamId}', + pathParams: ['teamId'], + queryParams: [], + hasBody: false, + }, + getTeamBoards: { + operationId: 'getTeamBoards', + method: 'get', + path: '/api/teams/{teamId}/boards', + pathParams: ['teamId'], + queryParams: ['page', 'pageSize', 'maxResults', 'search', 'orderBy', 'sortDescending'], + hasBody: false, + }, + getTeamLinks: { + operationId: 'getTeamLinks', + method: 'get', + path: '/api/teams/{teamId}/links', + pathParams: ['teamId'], + queryParams: ['page', 'pageSize', 'maxResults', 'search', 'orderBy', 'sortDescending'], + hasBody: false, + }, + getTeamPixels: { + operationId: 'getTeamPixels', + method: 'get', + path: '/api/teams/{teamId}/pixels', + pathParams: ['teamId'], + queryParams: ['page', 'pageSize', 'maxResults', 'search', 'orderBy', 'sortDescending'], + hasBody: false, + }, + getTeams: { + operationId: 'getTeams', + method: 'get', + path: '/api/teams', + pathParams: [], + queryParams: ['page', 'pageSize', 'maxResults', 'orderBy', 'sortDescending'], + hasBody: false, + }, + getTeamUser: { + operationId: 'getTeamUser', + method: 'get', + path: '/api/teams/{teamId}/users/{userId}', + pathParams: ['teamId', 'userId'], + queryParams: [], + hasBody: false, + }, + getTeamUsers: { + operationId: 'getTeamUsers', + method: 'get', + path: '/api/teams/{teamId}/users', + pathParams: ['teamId'], + queryParams: ['page', 'pageSize', 'maxResults', 'search'], + hasBody: false, + }, + getTeamWebsites: { + operationId: 'getTeamWebsites', + method: 'get', + path: '/api/teams/{teamId}/websites', + pathParams: ['teamId'], + queryParams: ['page', 'pageSize', 'maxResults', 'search', 'orderBy', 'sortDescending'], + hasBody: false, + }, + getTwoFactorStatus: { + operationId: 'getTwoFactorStatus', + method: 'get', + path: '/api/2fa/status', + pathParams: [], + queryParams: [], + hasBody: false, + }, + getUser: { + operationId: 'getUser', + method: 'get', + path: '/api/users/{userId}', + pathParams: ['userId'], + queryParams: [], + hasBody: false, + }, + getUserTeams: { + operationId: 'getUserTeams', + method: 'get', + path: '/api/users/{userId}/teams', + pathParams: ['userId'], + queryParams: ['page', 'pageSize', 'maxResults', 'orderBy', 'sortDescending'], + hasBody: false, + }, + getUserWebsites: { + operationId: 'getUserWebsites', + method: 'get', + path: '/api/users/{userId}/websites', + pathParams: ['userId'], + queryParams: [ + 'page', + 'pageSize', + 'maxResults', + 'search', + 'orderBy', + 'sortDescending', + 'includeTeams', + ], + hasBody: false, + }, + getWebsite: { + operationId: 'getWebsite', + method: 'get', + path: '/api/websites/{websiteId}', + pathParams: ['websiteId'], + queryParams: [], + hasBody: false, + scope: 'websites:read', + }, + getWebsiteActive: { + operationId: 'getWebsiteActive', + method: 'get', + path: '/api/websites/{websiteId}/active', + pathParams: ['websiteId'], + queryParams: [], + hasBody: false, + scope: 'analytics:read', + }, + getWebsiteAnnotation: { + operationId: 'getWebsiteAnnotation', + method: 'get', + path: '/api/websites/{websiteId}/annotations/{annotationId}', + pathParams: ['websiteId', 'annotationId'], + queryParams: [], + hasBody: false, + }, + getWebsiteAnnotations: { + operationId: 'getWebsiteAnnotations', + method: 'get', + path: '/api/websites/{websiteId}/annotations', + pathParams: ['websiteId'], + queryParams: ['startAt', 'endAt', 'search', 'page', 'pageSize', 'maxResults'], + hasBody: false, + }, + getWebsiteDateRange: { + operationId: 'getWebsiteDateRange', + method: 'get', + path: '/api/websites/{websiteId}/daterange', + pathParams: ['websiteId'], + queryParams: [], + hasBody: false, + scope: 'analytics:read', + }, + getWebsiteEvents: { + operationId: 'getWebsiteEvents', + method: 'get', + path: '/api/websites/{websiteId}/events', + pathParams: ['websiteId'], + queryParams: [ + 'startAt', + 'endAt', + 'startDate', + 'endDate', + 'timezone', + 'unit', + 'compare', + 'path', + 'referrer', + 'title', + 'query', + 'os', + 'browser', + 'device', + 'country', + 'region', + 'city', + 'tag', + 'hostname', + 'distinctId', + 'language', + 'event', + 'utmSource', + 'utmMedium', + 'utmCampaign', + 'utmContent', + 'utmTerm', + 'segment', + 'cohort', + 'eventType', + 'excludeBounce', + 'match', + 'page', + 'pageSize', + 'maxResults', + 'search', + ], + hasBody: false, + scope: 'analytics:read', + }, + getWebsiteEventSeries: { + operationId: 'getWebsiteEventSeries', + method: 'get', + path: '/api/websites/{websiteId}/events/series', + pathParams: ['websiteId'], + queryParams: [ + 'startAt', + 'endAt', + 'unit', + 'timezone', + 'limit', + 'path', + 'referrer', + 'title', + 'query', + 'os', + 'browser', + 'device', + 'country', + 'region', + 'city', + 'tag', + 'hostname', + 'distinctId', + 'language', + 'event', + 'utmSource', + 'utmMedium', + 'utmCampaign', + 'utmContent', + 'utmTerm', + 'excludeBounce', + 'segment', + 'cohort', + 'eventType', + 'match', + ], + hasBody: false, + scope: 'analytics:read', + }, + getWebsiteEventStats: { + operationId: 'getWebsiteEventStats', + method: 'get', + path: '/api/websites/{websiteId}/events/stats', + pathParams: ['websiteId'], + queryParams: [ + 'startAt', + 'endAt', + 'startDate', + 'endDate', + 'timezone', + 'unit', + 'compare', + 'path', + 'referrer', + 'title', + 'query', + 'os', + 'browser', + 'device', + 'country', + 'region', + 'city', + 'tag', + 'hostname', + 'distinctId', + 'language', + 'event', + 'utmSource', + 'utmMedium', + 'utmCampaign', + 'utmContent', + 'utmTerm', + 'excludeBounce', + 'segment', + 'cohort', + 'eventType', + 'match', + ], + hasBody: false, + scope: 'analytics:read', + }, + getWebsiteExpandedMetrics: { + operationId: 'getWebsiteExpandedMetrics', + method: 'get', + path: '/api/websites/{websiteId}/metrics/expanded', + pathParams: ['websiteId'], + queryParams: [ + 'startAt', + 'endAt', + 'startDate', + 'endDate', + 'timezone', + 'unit', + 'compare', + 'type', + 'limit', + 'offset', + 'search', + 'path', + 'referrer', + 'title', + 'query', + 'os', + 'browser', + 'device', + 'country', + 'region', + 'city', + 'tag', + 'hostname', + 'distinctId', + 'language', + 'event', + 'utmSource', + 'utmMedium', + 'utmCampaign', + 'utmContent', + 'utmTerm', + 'excludeBounce', + 'segment', + 'cohort', + 'eventType', + 'match', + ], + hasBody: false, + scope: 'analytics:read', + }, + getWebsiteMetrics: { + operationId: 'getWebsiteMetrics', + method: 'get', + path: '/api/websites/{websiteId}/metrics', + pathParams: ['websiteId'], + queryParams: [ + 'startAt', + 'endAt', + 'startDate', + 'endDate', + 'timezone', + 'unit', + 'compare', + 'type', + 'limit', + 'offset', + 'search', + 'path', + 'referrer', + 'title', + 'query', + 'os', + 'browser', + 'device', + 'country', + 'region', + 'city', + 'tag', + 'hostname', + 'distinctId', + 'language', + 'event', + 'utmSource', + 'utmMedium', + 'utmCampaign', + 'utmContent', + 'utmTerm', + 'segment', + 'cohort', + 'eventType', + 'excludeBounce', + 'match', + ], + hasBody: false, + scope: 'analytics:read', + }, + getWebsitePageviews: { + operationId: 'getWebsitePageviews', + method: 'get', + path: '/api/websites/{websiteId}/pageviews', + pathParams: ['websiteId'], + queryParams: [ + 'startAt', + 'endAt', + 'startDate', + 'endDate', + 'timezone', + 'unit', + 'compare', + 'path', + 'referrer', + 'title', + 'query', + 'os', + 'browser', + 'device', + 'country', + 'region', + 'city', + 'tag', + 'hostname', + 'distinctId', + 'language', + 'event', + 'utmSource', + 'utmMedium', + 'utmCampaign', + 'utmContent', + 'utmTerm', + 'segment', + 'cohort', + 'eventType', + 'excludeBounce', + 'match', + ], + hasBody: false, + scope: 'analytics:read', + }, + getWebsiteRecorderConfig: { + operationId: 'getWebsiteRecorderConfig', + method: 'get', + path: '/api/websites/{websiteId}/recorder', + pathParams: ['websiteId'], + queryParams: [], + hasBody: false, + }, + getWebsiteReplay: { + operationId: 'getWebsiteReplay', + method: 'get', + path: '/api/websites/{websiteId}/replays/{replayId}', + pathParams: ['websiteId', 'replayId'], + queryParams: [], + hasBody: false, + }, + getWebsiteReplays: { + operationId: 'getWebsiteReplays', + method: 'get', + path: '/api/websites/{websiteId}/replays', + pathParams: ['websiteId'], + queryParams: [ + 'startAt', + 'endAt', + 'startDate', + 'endDate', + 'timezone', + 'unit', + 'compare', + 'path', + 'referrer', + 'title', + 'query', + 'os', + 'browser', + 'device', + 'country', + 'region', + 'city', + 'tag', + 'hostname', + 'distinctId', + 'language', + 'event', + 'utmSource', + 'utmMedium', + 'utmCampaign', + 'utmContent', + 'utmTerm', + 'excludeBounce', + 'segment', + 'cohort', + 'eventType', + 'match', + 'minDuration', + 'page', + 'pageSize', + 'maxResults', + 'search', + ], + hasBody: false, + }, + getWebsiteReplaySaved: { + operationId: 'getWebsiteReplaySaved', + method: 'get', + path: '/api/websites/{websiteId}/replays/saved/{replayId}', + pathParams: ['websiteId', 'replayId'], + queryParams: [], + hasBody: false, + }, + getWebsiteReports: { + operationId: 'getWebsiteReports', + method: 'get', + path: '/api/websites/{websiteId}/reports', + pathParams: ['websiteId'], + queryParams: ['type', 'page', 'pageSize', 'maxResults'], + hasBody: false, + }, + getWebsiteRevenueChart: { + operationId: 'getWebsiteRevenueChart', + method: 'get', + path: '/api/websites/{websiteId}/revenue/chart', + pathParams: ['websiteId'], + queryParams: [ + 'startAt', + 'endAt', + 'startDate', + 'endDate', + 'timezone', + 'unit', + 'compare', + 'currency', + 'path', + 'referrer', + 'title', + 'query', + 'os', + 'browser', + 'device', + 'country', + 'region', + 'city', + 'tag', + 'hostname', + 'distinctId', + 'language', + 'event', + 'utmSource', + 'utmMedium', + 'utmCampaign', + 'utmContent', + 'utmTerm', + 'excludeBounce', + 'segment', + 'cohort', + 'eventType', + 'match', + ], + hasBody: false, + }, + getWebsiteRevenueMetrics: { + operationId: 'getWebsiteRevenueMetrics', + method: 'get', + path: '/api/websites/{websiteId}/revenue/metrics', + pathParams: ['websiteId'], + queryParams: [ + 'startAt', + 'endAt', + 'startDate', + 'endDate', + 'timezone', + 'unit', + 'compare', + 'type', + 'currency', + 'path', + 'referrer', + 'title', + 'query', + 'os', + 'browser', + 'device', + 'country', + 'region', + 'city', + 'tag', + 'hostname', + 'distinctId', + 'language', + 'event', + 'utmSource', + 'utmMedium', + 'utmCampaign', + 'utmContent', + 'utmTerm', + 'excludeBounce', + 'segment', + 'cohort', + 'eventType', + 'match', + ], + hasBody: false, + }, + getWebsiteRevenueSessions: { + operationId: 'getWebsiteRevenueSessions', + method: 'get', + path: '/api/websites/{websiteId}/revenue/sessions', + pathParams: ['websiteId'], + queryParams: [ + 'startAt', + 'endAt', + 'startDate', + 'endDate', + 'timezone', + 'unit', + 'compare', + 'currency', + 'path', + 'referrer', + 'title', + 'query', + 'os', + 'browser', + 'device', + 'country', + 'region', + 'city', + 'tag', + 'hostname', + 'distinctId', + 'language', + 'event', + 'utmSource', + 'utmMedium', + 'utmCampaign', + 'utmContent', + 'utmTerm', + 'excludeBounce', + 'segment', + 'cohort', + 'eventType', + 'match', + 'page', + 'pageSize', + 'maxResults', + 'search', + ], + hasBody: false, + }, + getWebsiteRevenueStats: { + operationId: 'getWebsiteRevenueStats', + method: 'get', + path: '/api/websites/{websiteId}/revenue/stats', + pathParams: ['websiteId'], + queryParams: [ + 'startAt', + 'endAt', + 'startDate', + 'endDate', + 'timezone', + 'unit', + 'compare', + 'currency', + 'path', + 'referrer', + 'title', + 'query', + 'os', + 'browser', + 'device', + 'country', + 'region', + 'city', + 'tag', + 'hostname', + 'distinctId', + 'language', + 'event', + 'utmSource', + 'utmMedium', + 'utmCampaign', + 'utmContent', + 'utmTerm', + 'excludeBounce', + 'segment', + 'cohort', + 'eventType', + 'match', + ], + hasBody: false, + }, + getWebsiteSavedReplays: { + operationId: 'getWebsiteSavedReplays', + method: 'get', + path: '/api/websites/{websiteId}/replays/saved', + pathParams: ['websiteId'], + queryParams: ['page', 'pageSize', 'maxResults', 'search'], + hasBody: false, + }, + getWebsitesCharts: { + operationId: 'getWebsitesCharts', + method: 'get', + path: '/api/websites/charts', + pathParams: [], + queryParams: ['ids', 'startAt', 'endAt', 'timezone'], + hasBody: false, + }, + getWebsiteSegment: { + operationId: 'getWebsiteSegment', + method: 'get', + path: '/api/websites/{websiteId}/segments/{segmentId}', + pathParams: ['websiteId', 'segmentId'], + queryParams: [], + hasBody: false, + }, + getWebsiteSegments: { + operationId: 'getWebsiteSegments', + method: 'get', + path: '/api/websites/{websiteId}/segments', + pathParams: ['websiteId'], + queryParams: ['type', 'search'], + hasBody: false, + }, + getWebsiteSession: { + operationId: 'getWebsiteSession', + method: 'get', + path: '/api/websites/{websiteId}/sessions/{sessionId}', + pathParams: ['websiteId', 'sessionId'], + queryParams: [], + hasBody: false, + scope: 'analytics:read', + }, + getWebsiteSessionActivity: { + operationId: 'getWebsiteSessionActivity', + method: 'get', + path: '/api/websites/{websiteId}/sessions/{sessionId}/activity', + pathParams: ['websiteId', 'sessionId'], + queryParams: ['startAt', 'endAt', 'distinctId'], + hasBody: false, + scope: 'analytics:read', + }, + getWebsiteSessionProperties: { + operationId: 'getWebsiteSessionProperties', + method: 'get', + path: '/api/websites/{websiteId}/sessions/{sessionId}/properties', + pathParams: ['websiteId', 'sessionId'], + queryParams: [], + hasBody: false, + scope: 'analytics:read', + }, + getWebsiteSessionReplays: { + operationId: 'getWebsiteSessionReplays', + method: 'get', + path: '/api/websites/{websiteId}/sessions/{sessionId}/replays', + pathParams: ['websiteId', 'sessionId'], + queryParams: [ + 'startAt', + 'endAt', + 'startDate', + 'endDate', + 'timezone', + 'unit', + 'compare', + 'page', + 'pageSize', + 'maxResults', + 'search', + ], + hasBody: false, + }, + getWebsiteSessions: { + operationId: 'getWebsiteSessions', + method: 'get', + path: '/api/websites/{websiteId}/sessions', + pathParams: ['websiteId'], + queryParams: [ + 'startAt', + 'endAt', + 'startDate', + 'endDate', + 'timezone', + 'unit', + 'compare', + 'path', + 'referrer', + 'title', + 'query', + 'os', + 'browser', + 'device', + 'country', + 'region', + 'city', + 'tag', + 'hostname', + 'distinctId', + 'language', + 'event', + 'utmSource', + 'utmMedium', + 'utmCampaign', + 'utmContent', + 'utmTerm', + 'segment', + 'cohort', + 'eventType', + 'excludeBounce', + 'match', + 'page', + 'pageSize', + 'maxResults', + 'search', + ], + hasBody: false, + scope: 'analytics:read', + }, + getWebsiteSessionStats: { + operationId: 'getWebsiteSessionStats', + method: 'get', + path: '/api/websites/{websiteId}/sessions/stats', + pathParams: ['websiteId'], + queryParams: [ + 'startAt', + 'endAt', + 'path', + 'referrer', + 'title', + 'query', + 'os', + 'browser', + 'device', + 'country', + 'region', + 'city', + 'tag', + 'hostname', + 'distinctId', + 'language', + 'event', + 'utmSource', + 'utmMedium', + 'utmCampaign', + 'utmContent', + 'utmTerm', + 'excludeBounce', + 'segment', + 'cohort', + 'eventType', + 'match', + ], + hasBody: false, + scope: 'analytics:read', + }, + getWebsiteSessionsWeekly: { + operationId: 'getWebsiteSessionsWeekly', + method: 'get', + path: '/api/websites/{websiteId}/sessions/weekly', + pathParams: ['websiteId'], + queryParams: [ + 'startAt', + 'endAt', + 'timezone', + 'path', + 'referrer', + 'title', + 'query', + 'os', + 'browser', + 'device', + 'country', + 'region', + 'city', + 'tag', + 'hostname', + 'distinctId', + 'language', + 'event', + 'utmSource', + 'utmMedium', + 'utmCampaign', + 'utmContent', + 'utmTerm', + 'excludeBounce', + 'segment', + 'cohort', + 'eventType', + 'match', + ], + hasBody: false, + }, + getWebsiteShares: { + operationId: 'getWebsiteShares', + method: 'get', + path: '/api/websites/{websiteId}/shares', + pathParams: ['websiteId'], + queryParams: [ + 'path', + 'referrer', + 'title', + 'query', + 'os', + 'browser', + 'device', + 'country', + 'region', + 'city', + 'tag', + 'hostname', + 'distinctId', + 'language', + 'event', + 'utmSource', + 'utmMedium', + 'utmCampaign', + 'utmContent', + 'utmTerm', + 'excludeBounce', + 'segment', + 'cohort', + 'eventType', + 'match', + 'page', + 'pageSize', + 'maxResults', + ], + hasBody: false, + }, + getWebsiteStats: { + operationId: 'getWebsiteStats', + method: 'get', + path: '/api/websites/{websiteId}/stats', + pathParams: ['websiteId'], + queryParams: [ + 'startAt', + 'endAt', + 'startDate', + 'endDate', + 'timezone', + 'unit', + 'compare', + 'path', + 'referrer', + 'title', + 'query', + 'os', + 'browser', + 'device', + 'country', + 'region', + 'city', + 'tag', + 'hostname', + 'distinctId', + 'language', + 'event', + 'utmSource', + 'utmMedium', + 'utmCampaign', + 'utmContent', + 'utmTerm', + 'segment', + 'cohort', + 'eventType', + 'excludeBounce', + 'match', + ], + hasBody: false, + scope: 'analytics:read', + }, + getWebsiteValues: { + operationId: 'getWebsiteValues', + method: 'get', + path: '/api/websites/{websiteId}/values', + pathParams: ['websiteId'], + queryParams: [ + 'startAt', + 'endAt', + 'startDate', + 'endDate', + 'timezone', + 'unit', + 'compare', + 'type', + 'search', + ], + hasBody: false, + }, + initiateTwoFactorSetup: { + operationId: 'initiateTwoFactorSetup', + method: 'post', + path: '/api/2fa/setup/initiate', + pathParams: [], + queryParams: [], + hasBody: false, + }, + joinTeam: { + operationId: 'joinTeam', + method: 'post', + path: '/api/teams/join', + pathParams: [], + queryParams: [], + hasBody: true, + }, + listWebsites: { + operationId: 'listWebsites', + method: 'get', + path: '/api/websites', + pathParams: [], + queryParams: [ + 'page', + 'pageSize', + 'maxResults', + 'search', + 'orderBy', + 'sortDescending', + 'includeTeams', + ], + hasBody: false, + scope: 'websites:read', + }, + login: { + operationId: 'login', + method: 'post', + path: '/api/auth/login', + pathParams: [], + queryParams: [], + hasBody: true, + }, + logout: { + operationId: 'logout', + method: 'post', + path: '/api/auth/logout', + pathParams: [], + queryParams: [], + hasBody: false, + }, + record: { + operationId: 'record', + method: 'post', + path: '/api/record', + pathParams: [], + queryParams: [], + hasBody: true, + }, + resetWebsite: { + operationId: 'resetWebsite', + method: 'post', + path: '/api/websites/{websiteId}/reset', + pathParams: ['websiteId'], + queryParams: [], + hasBody: false, + }, + runAttributionReport: { + operationId: 'runAttributionReport', + method: 'post', + path: '/api/reports/attribution', + pathParams: [], + queryParams: [], + hasBody: true, + scope: 'analytics:read', + }, + runBreakdownReport: { + operationId: 'runBreakdownReport', + method: 'post', + path: '/api/reports/breakdown', + pathParams: [], + queryParams: [], + hasBody: true, + scope: 'analytics:read', + }, + runFunnelReport: { + operationId: 'runFunnelReport', + method: 'post', + path: '/api/reports/funnel', + pathParams: [], + queryParams: [], + hasBody: true, + scope: 'analytics:read', + }, + runGoalReport: { + operationId: 'runGoalReport', + method: 'post', + path: '/api/reports/goal', + pathParams: [], + queryParams: [], + hasBody: true, + }, + runHeatmapReport: { + operationId: 'runHeatmapReport', + method: 'post', + path: '/api/reports/heatmap', + pathParams: [], + queryParams: [], + hasBody: true, + }, + runJourneyReport: { + operationId: 'runJourneyReport', + method: 'post', + path: '/api/reports/journey', + pathParams: [], + queryParams: [], + hasBody: true, + scope: 'analytics:read', + }, + runPerformanceReport: { + operationId: 'runPerformanceReport', + method: 'post', + path: '/api/reports/performance', + pathParams: [], + queryParams: [], + hasBody: true, + }, + runRetentionReport: { + operationId: 'runRetentionReport', + method: 'post', + path: '/api/reports/retention', + pathParams: [], + queryParams: [], + hasBody: true, + scope: 'analytics:read', + }, + runRevenueReport: { + operationId: 'runRevenueReport', + method: 'post', + path: '/api/reports/revenue', + pathParams: [], + queryParams: [], + hasBody: true, + scope: 'analytics:read', + }, + runUtmReport: { + operationId: 'runUtmReport', + method: 'post', + path: '/api/reports/utm', + pathParams: [], + queryParams: [], + hasBody: true, + scope: 'analytics:read', + }, + saveWebsiteReplay: { + operationId: 'saveWebsiteReplay', + method: 'post', + path: '/api/websites/{websiteId}/replays/saved/{replayId}', + pathParams: ['websiteId', 'replayId'], + queryParams: [], + hasBody: true, + }, + send: { + operationId: 'send', + method: 'post', + path: '/api/send', + pathParams: [], + queryParams: [], + hasBody: true, + }, + sso: { + operationId: 'sso', + method: 'post', + path: '/api/auth/sso', + pathParams: [], + queryParams: [], + hasBody: false, + }, + transferWebsite: { + operationId: 'transferWebsite', + method: 'post', + path: '/api/websites/{websiteId}/transfer', + pathParams: ['websiteId'], + queryParams: [], + hasBody: true, + }, + updateBoard: { + operationId: 'updateBoard', + method: 'post', + path: '/api/boards/{boardId}', + pathParams: ['boardId'], + queryParams: [], + hasBody: true, + }, + updateLink: { + operationId: 'updateLink', + method: 'post', + path: '/api/links/{linkId}', + pathParams: ['linkId'], + queryParams: [], + hasBody: true, + }, + updateMyPassword: { + operationId: 'updateMyPassword', + method: 'post', + path: '/api/me/password', + pathParams: [], + queryParams: [], + hasBody: true, + }, + updatePixel: { + operationId: 'updatePixel', + method: 'post', + path: '/api/pixels/{pixelId}', + pathParams: ['pixelId'], + queryParams: [], + hasBody: true, + }, + updateReport: { + operationId: 'updateReport', + method: 'post', + path: '/api/reports/{reportId}', + pathParams: ['reportId'], + queryParams: [], + hasBody: true, + }, + updateShare: { + operationId: 'updateShare', + method: 'post', + path: '/api/share/id/{shareId}', + pathParams: ['shareId'], + queryParams: [], + hasBody: true, + }, + updateTeam: { + operationId: 'updateTeam', + method: 'post', + path: '/api/teams/{teamId}', + pathParams: ['teamId'], + queryParams: [], + hasBody: true, + }, + updateTeamUser: { + operationId: 'updateTeamUser', + method: 'post', + path: '/api/teams/{teamId}/users/{userId}', + pathParams: ['teamId', 'userId'], + queryParams: [], + hasBody: true, + }, + updateUser: { + operationId: 'updateUser', + method: 'post', + path: '/api/users/{userId}', + pathParams: ['userId'], + queryParams: [], + hasBody: true, + }, + updateWebsite: { + operationId: 'updateWebsite', + method: 'post', + path: '/api/websites/{websiteId}', + pathParams: ['websiteId'], + queryParams: [], + hasBody: true, + }, + updateWebsiteAnnotation: { + operationId: 'updateWebsiteAnnotation', + method: 'post', + path: '/api/websites/{websiteId}/annotations/{annotationId}', + pathParams: ['websiteId', 'annotationId'], + queryParams: [], + hasBody: true, + }, + updateWebsiteSegment: { + operationId: 'updateWebsiteSegment', + method: 'post', + path: '/api/websites/{websiteId}/segments/{segmentId}', + pathParams: ['websiteId', 'segmentId'], + queryParams: [], + hasBody: true, + }, + verify: { + operationId: 'verify', + method: 'post', + path: '/api/auth/verify', + pathParams: [], + queryParams: [], + hasBody: false, + }, + verifyTwoFactor: { + operationId: 'verifyTwoFactor', + method: 'post', + path: '/api/2fa/verify', + pathParams: [], + queryParams: [], + hasBody: true, + }, +} as const satisfies Record; + +export type OperationId = keyof typeof operations; + +export type Schemas = components['schemas']; + +type Clean = [T] extends [never] ? Record : T; + +type OperationPathParams = OperationTypes[K]['parameters'] extends { + path: infer P; +} + ? Clean

+ : Record; + +type OperationQuery = Clean< + NonNullable +>; + +type OperationBody = OperationTypes[K] extends { requestBody?: infer R } + ? NonNullable extends { content: { 'application/json': infer B } } + ? Clean + : Record + : Record; + +export type OperationInput = OperationPathParams & + OperationQuery & + OperationBody & + Record; + +export type OperationOutput = OperationTypes[K]['responses'] extends { + 200: { content: { 'application/json': infer R } }; +} + ? R + : unknown; + +export abstract class GeneratedUmamiClient { + protected abstract execute( + operationId: K, + input: Record | undefined, + options?: RequestOptions, + ): Promise>; + + /** + * Create or update batch + * `POST /api/batch` + */ + batch( + input: OperationInput<'batch'>, + options?: RequestOptions, + ): Promise> { + return this.execute('batch', input, options); + } + + /** + * Create or update 2fa setup cancel + * `POST /api/2fa/setup/cancel` + */ + cancelTwoFactorSetup( + input?: OperationInput<'cancelTwoFactorSetup'>, + options?: RequestOptions, + ): Promise> { + return this.execute('cancelTwoFactorSetup', input, options); + } + + /** + * Create or update boards board id clone + * `POST /api/boards/{boardId}/clone` + */ + cloneBoard( + input: OperationInput<'cloneBoard'>, + options?: RequestOptions, + ): Promise> { + return this.execute('cloneBoard', input, options); + } + + /** + * Create or update 2fa setup confirm + * `POST /api/2fa/setup/confirm` + */ + confirmTwoFactorSetup( + input: OperationInput<'confirmTwoFactorSetup'>, + options?: RequestOptions, + ): Promise> { + return this.execute('confirmTwoFactorSetup', input, options); + } + + /** + * Create or update boards + * `POST /api/boards` + */ + createBoard( + input: OperationInput<'createBoard'>, + options?: RequestOptions, + ): Promise> { + return this.execute('createBoard', input, options); + } + + /** + * Create or update boards board id shares + * `POST /api/boards/{boardId}/shares` + */ + createBoardShare( + input: OperationInput<'createBoardShare'>, + options?: RequestOptions, + ): Promise> { + return this.execute('createBoardShare', input, options); + } + + /** + * Create or update links + * `POST /api/links` + */ + createLink( + input: OperationInput<'createLink'>, + options?: RequestOptions, + ): Promise> { + return this.execute('createLink', input, options); + } + + /** + * Create or update links link id shares + * `POST /api/links/{linkId}/shares` + */ + createLinkShare( + input: OperationInput<'createLinkShare'>, + options?: RequestOptions, + ): Promise> { + return this.execute('createLinkShare', input, options); + } + + /** + * Create or update me api keys + * `POST /api/me/api-keys` + */ + createMyApiKey( + input: OperationInput<'createMyApiKey'>, + options?: RequestOptions, + ): Promise> { + return this.execute('createMyApiKey', input, options); + } + + /** + * Create or update pixels + * `POST /api/pixels` + */ + createPixel( + input: OperationInput<'createPixel'>, + options?: RequestOptions, + ): Promise> { + return this.execute('createPixel', input, options); + } + + /** + * Create or update pixels pixel id shares + * `POST /api/pixels/{pixelId}/shares` + */ + createPixelShare( + input: OperationInput<'createPixelShare'>, + options?: RequestOptions, + ): Promise> { + return this.execute('createPixelShare', input, options); + } + + /** + * Create or update reports + * `POST /api/reports` + */ + createReport( + input: OperationInput<'createReport'>, + options?: RequestOptions, + ): Promise> { + return this.execute('createReport', input, options); + } + + /** + * Create or update share + * `POST /api/share` + */ + createShare( + input: OperationInput<'createShare'>, + options?: RequestOptions, + ): Promise> { + return this.execute('createShare', input, options); + } + + /** + * Create or update teams + * `POST /api/teams` + */ + createTeam( + input: OperationInput<'createTeam'>, + options?: RequestOptions, + ): Promise> { + return this.execute('createTeam', input, options); + } + + /** + * Create or update teams team id users + * `POST /api/teams/{teamId}/users` + */ + createTeamUser( + input: OperationInput<'createTeamUser'>, + options?: RequestOptions, + ): Promise> { + return this.execute('createTeamUser', input, options); + } + + /** + * Create or update users + * `POST /api/users` + */ + createUser( + input: OperationInput<'createUser'>, + options?: RequestOptions, + ): Promise> { + return this.execute('createUser', input, options); + } + + /** + * Create a website + * `POST /api/websites` + */ + createWebsite( + input: OperationInput<'createWebsite'>, + options?: RequestOptions, + ): Promise> { + return this.execute('createWebsite', input, options); + } + + /** + * Create or update websites website id annotations + * `POST /api/websites/{websiteId}/annotations` + */ + createWebsiteAnnotation( + input: OperationInput<'createWebsiteAnnotation'>, + options?: RequestOptions, + ): Promise> { + return this.execute('createWebsiteAnnotation', input, options); + } + + /** + * Create or update websites website id segments + * `POST /api/websites/{websiteId}/segments` + */ + createWebsiteSegment( + input: OperationInput<'createWebsiteSegment'>, + options?: RequestOptions, + ): Promise> { + return this.execute('createWebsiteSegment', input, options); + } + + /** + * Create or update websites website id shares + * `POST /api/websites/{websiteId}/shares` + */ + createWebsiteShare( + input: OperationInput<'createWebsiteShare'>, + options?: RequestOptions, + ): Promise> { + return this.execute('createWebsiteShare', input, options); + } + + /** + * Delete boards board id + * `DELETE /api/boards/{boardId}` + */ + deleteBoard( + input: OperationInput<'deleteBoard'>, + options?: RequestOptions, + ): Promise> { + return this.execute('deleteBoard', input, options); + } + + /** + * Delete links link id + * `DELETE /api/links/{linkId}` + */ + deleteLink( + input: OperationInput<'deleteLink'>, + options?: RequestOptions, + ): Promise> { + return this.execute('deleteLink', input, options); + } + + /** + * Delete me api keys key id + * `DELETE /api/me/api-keys/{keyId}` + */ + deleteMyApiKey( + input: OperationInput<'deleteMyApiKey'>, + options?: RequestOptions, + ): Promise> { + return this.execute('deleteMyApiKey', input, options); + } + + /** + * Delete pixels pixel id + * `DELETE /api/pixels/{pixelId}` + */ + deletePixel( + input: OperationInput<'deletePixel'>, + options?: RequestOptions, + ): Promise> { + return this.execute('deletePixel', input, options); + } + + /** + * Delete reports report id + * `DELETE /api/reports/{reportId}` + */ + deleteReport( + input: OperationInput<'deleteReport'>, + options?: RequestOptions, + ): Promise> { + return this.execute('deleteReport', input, options); + } + + /** + * Delete share id share id + * `DELETE /api/share/id/{shareId}` + */ + deleteShare( + input: OperationInput<'deleteShare'>, + options?: RequestOptions, + ): Promise> { + return this.execute('deleteShare', input, options); + } + + /** + * Delete teams team id + * `DELETE /api/teams/{teamId}` + */ + deleteTeam( + input: OperationInput<'deleteTeam'>, + options?: RequestOptions, + ): Promise> { + return this.execute('deleteTeam', input, options); + } + + /** + * Delete teams team id users user id + * `DELETE /api/teams/{teamId}/users/{userId}` + */ + deleteTeamUser( + input: OperationInput<'deleteTeamUser'>, + options?: RequestOptions, + ): Promise> { + return this.execute('deleteTeamUser', input, options); + } + + /** + * Delete users user id + * `DELETE /api/users/{userId}` + */ + deleteUser( + input: OperationInput<'deleteUser'>, + options?: RequestOptions, + ): Promise> { + return this.execute('deleteUser', input, options); + } + + /** + * Delete a website + * `DELETE /api/websites/{websiteId}` + */ + deleteWebsite( + input: OperationInput<'deleteWebsite'>, + options?: RequestOptions, + ): Promise> { + return this.execute('deleteWebsite', input, options); + } + + /** + * Delete websites website id annotations annotation id + * `DELETE /api/websites/{websiteId}/annotations/{annotationId}` + */ + deleteWebsiteAnnotation( + input: OperationInput<'deleteWebsiteAnnotation'>, + options?: RequestOptions, + ): Promise> { + return this.execute('deleteWebsiteAnnotation', input, options); + } + + /** + * Delete websites website id segments segment id + * `DELETE /api/websites/{websiteId}/segments/{segmentId}` + */ + deleteWebsiteSegment( + input: OperationInput<'deleteWebsiteSegment'>, + options?: RequestOptions, + ): Promise> { + return this.execute('deleteWebsiteSegment', input, options); + } + + /** + * Delete websites website id sessions session id + * `DELETE /api/websites/{websiteId}/sessions/{sessionId}` + */ + deleteWebsiteSession( + input: OperationInput<'deleteWebsiteSession'>, + options?: RequestOptions, + ): Promise> { + return this.execute('deleteWebsiteSession', input, options); + } + + /** + * Create or update 2fa disable + * `POST /api/2fa/disable` + */ + disableTwoFactor( + input: OperationInput<'disableTwoFactor'>, + options?: RequestOptions, + ): Promise> { + return this.execute('disableTwoFactor', input, options); + } + + /** + * Get websites website id export + * `GET /api/websites/{websiteId}/export` + */ + exportWebsite( + input: OperationInput<'exportWebsite'>, + options?: RequestOptions, + ): Promise> { + return this.execute('exportWebsite', input, options); + } + + /** + * Get boards board id + * `GET /api/boards/{boardId}` + */ + getBoard( + input: OperationInput<'getBoard'>, + options?: RequestOptions, + ): Promise> { + return this.execute('getBoard', input, options); + } + + /** + * Get boards + * `GET /api/boards` + */ + getBoards( + input?: OperationInput<'getBoards'>, + options?: RequestOptions, + ): Promise> { + return this.execute('getBoards', input, options); + } + + /** + * Get boards board id shares + * `GET /api/boards/{boardId}/shares` + */ + getBoardShares( + input: OperationInput<'getBoardShares'>, + options?: RequestOptions, + ): Promise> { + return this.execute('getBoardShares', input, options); + } + + /** + * Get websites website id event data + * `GET /api/websites/{websiteId}/event-data` + * OAuth scope: `analytics:read` + */ + getEventData( + input: OperationInput<'getEventData'>, + options?: RequestOptions, + ): Promise> { + return this.execute('getEventData', input, options); + } + + /** + * Get websites website id event data pivot array series + * `GET /api/websites/{websiteId}/event-data-pivot/array-series` + */ + getEventDataArraySeries( + input: OperationInput<'getEventDataArraySeries'>, + options?: RequestOptions, + ): Promise> { + return this.execute('getEventDataArraySeries', input, options); + } + + /** + * Get websites website id event data event id + * `GET /api/websites/{websiteId}/event-data/{eventId}` + */ + getEventDataById( + input: OperationInput<'getEventDataById'>, + options?: RequestOptions, + ): Promise> { + return this.execute('getEventDataById', input, options); + } + + /** + * Get websites website id event data pivot date series + * `GET /api/websites/{websiteId}/event-data-pivot/date-series` + */ + getEventDataDateSeries( + input: OperationInput<'getEventDataDateSeries'>, + options?: RequestOptions, + ): Promise> { + return this.execute('getEventDataDateSeries', input, options); + } + + /** + * Get websites website id event data events + * `GET /api/websites/{websiteId}/event-data/events` + */ + getEventDataEvents( + input: OperationInput<'getEventDataEvents'>, + options?: RequestOptions, + ): Promise> { + return this.execute('getEventDataEvents', input, options); + } + + /** + * Get websites website id event data fields + * `GET /api/websites/{websiteId}/event-data/fields` + */ + getEventDataFields( + input: OperationInput<'getEventDataFields'>, + options?: RequestOptions, + ): Promise> { + return this.execute('getEventDataFields', input, options); + } + + /** + * Get websites website id event data pivot numeric series + * `GET /api/websites/{websiteId}/event-data-pivot/numeric-series` + */ + getEventDataNumericSeries( + input: OperationInput<'getEventDataNumericSeries'>, + options?: RequestOptions, + ): Promise> { + return this.execute('getEventDataNumericSeries', input, options); + } + + /** + * Get websites website id event data pivot numeric stats + * `GET /api/websites/{websiteId}/event-data-pivot/numeric-stats` + */ + getEventDataNumericStats( + input: OperationInput<'getEventDataNumericStats'>, + options?: RequestOptions, + ): Promise> { + return this.execute('getEventDataNumericStats', input, options); + } + + /** + * Get websites website id event data pivot + * `GET /api/websites/{websiteId}/event-data-pivot` + */ + getEventDataPivot( + input: OperationInput<'getEventDataPivot'>, + options?: RequestOptions, + ): Promise> { + return this.execute('getEventDataPivot', input, options); + } + + /** + * Get websites website id event data properties + * `GET /api/websites/{websiteId}/event-data/properties` + * OAuth scope: `analytics:read` + */ + getEventDataProperties( + input: OperationInput<'getEventDataProperties'>, + options?: RequestOptions, + ): Promise> { + return this.execute('getEventDataProperties', input, options); + } + + /** + * Get websites website id event data pivot property series + * `GET /api/websites/{websiteId}/event-data-pivot/property-series` + */ + getEventDataPropertySeries( + input: OperationInput<'getEventDataPropertySeries'>, + options?: RequestOptions, + ): Promise> { + return this.execute('getEventDataPropertySeries', input, options); + } + + /** + * Get websites website id event data stats + * `GET /api/websites/{websiteId}/event-data/stats` + * OAuth scope: `analytics:read` + */ + getEventDataStats( + input: OperationInput<'getEventDataStats'>, + options?: RequestOptions, + ): Promise> { + return this.execute('getEventDataStats', input, options); + } + + /** + * Get websites website id event data values + * `GET /api/websites/{websiteId}/event-data/values` + * OAuth scope: `analytics:read` + */ + getEventDataValues( + input: OperationInput<'getEventDataValues'>, + options?: RequestOptions, + ): Promise> { + return this.execute('getEventDataValues', input, options); + } + + /** + * Get links link id + * `GET /api/links/{linkId}` + */ + getLink( + input: OperationInput<'getLink'>, + options?: RequestOptions, + ): Promise> { + return this.execute('getLink', input, options); + } + + /** + * Get links + * `GET /api/links` + */ + getLinks( + input?: OperationInput<'getLinks'>, + options?: RequestOptions, + ): Promise> { + return this.execute('getLinks', input, options); + } + + /** + * Get links charts + * `GET /api/links/charts` + */ + getLinksCharts( + input: OperationInput<'getLinksCharts'>, + options?: RequestOptions, + ): Promise> { + return this.execute('getLinksCharts', input, options); + } + + /** + * Get links link id shares + * `GET /api/links/{linkId}/shares` + */ + getLinkShares( + input: OperationInput<'getLinkShares'>, + options?: RequestOptions, + ): Promise> { + return this.execute('getLinkShares', input, options); + } + + /** + * Get me + * `GET /api/me` + * OAuth scope: `websites:read` + */ + getMe( + input?: OperationInput<'getMe'>, + options?: RequestOptions, + ): Promise> { + return this.execute('getMe', input, options); + } + + /** + * Get me api keys + * `GET /api/me/api-keys` + */ + getMyApiKeys( + input?: OperationInput<'getMyApiKeys'>, + options?: RequestOptions, + ): Promise> { + return this.execute('getMyApiKeys', input, options); + } + + /** + * Get me teams + * `GET /api/me/teams` + */ + getMyTeams( + input?: OperationInput<'getMyTeams'>, + options?: RequestOptions, + ): Promise> { + return this.execute('getMyTeams', input, options); + } + + /** + * Get me websites + * `GET /api/me/websites` + */ + getMyWebsites( + input?: OperationInput<'getMyWebsites'>, + options?: RequestOptions, + ): Promise> { + return this.execute('getMyWebsites', input, options); + } + + /** + * Get pixels pixel id + * `GET /api/pixels/{pixelId}` + */ + getPixel( + input: OperationInput<'getPixel'>, + options?: RequestOptions, + ): Promise> { + return this.execute('getPixel', input, options); + } + + /** + * Get pixels + * `GET /api/pixels` + */ + getPixels( + input?: OperationInput<'getPixels'>, + options?: RequestOptions, + ): Promise> { + return this.execute('getPixels', input, options); + } + + /** + * Get pixels charts + * `GET /api/pixels/charts` + */ + getPixelsCharts( + input: OperationInput<'getPixelsCharts'>, + options?: RequestOptions, + ): Promise> { + return this.execute('getPixelsCharts', input, options); + } + + /** + * Get pixels pixel id shares + * `GET /api/pixels/{pixelId}/shares` + */ + getPixelShares( + input: OperationInput<'getPixelShares'>, + options?: RequestOptions, + ): Promise> { + return this.execute('getPixelShares', input, options); + } + + /** + * Get realtime website id + * `GET /api/realtime/{websiteId}` + * OAuth scope: `analytics:read` + */ + getRealtime( + input: OperationInput<'getRealtime'>, + options?: RequestOptions, + ): Promise> { + return this.execute('getRealtime', input, options); + } + + /** + * Get reports report id + * `GET /api/reports/{reportId}` + */ + getReport( + input: OperationInput<'getReport'>, + options?: RequestOptions, + ): Promise> { + return this.execute('getReport', input, options); + } + + /** + * Get reports + * `GET /api/reports` + */ + getReports( + input: OperationInput<'getReports'>, + options?: RequestOptions, + ): Promise> { + return this.execute('getReports', input, options); + } + + /** + * Get websites website id session data array series + * `GET /api/websites/{websiteId}/session-data/array-series` + */ + getSessionDataArraySeries( + input: OperationInput<'getSessionDataArraySeries'>, + options?: RequestOptions, + ): Promise> { + return this.execute('getSessionDataArraySeries', input, options); + } + + /** + * Get websites website id session data date series + * `GET /api/websites/{websiteId}/session-data/date-series` + */ + getSessionDataDateSeries( + input: OperationInput<'getSessionDataDateSeries'>, + options?: RequestOptions, + ): Promise> { + return this.execute('getSessionDataDateSeries', input, options); + } + + /** + * Get websites website id session data numeric series + * `GET /api/websites/{websiteId}/session-data/numeric-series` + */ + getSessionDataNumericSeries( + input: OperationInput<'getSessionDataNumericSeries'>, + options?: RequestOptions, + ): Promise> { + return this.execute('getSessionDataNumericSeries', input, options); + } + + /** + * Get websites website id session data numeric stats + * `GET /api/websites/{websiteId}/session-data/numeric-stats` + */ + getSessionDataNumericStats( + input: OperationInput<'getSessionDataNumericStats'>, + options?: RequestOptions, + ): Promise> { + return this.execute('getSessionDataNumericStats', input, options); + } + + /** + * Get websites website id session data pivot + * `GET /api/websites/{websiteId}/session-data-pivot` + */ + getSessionDataPivot( + input: OperationInput<'getSessionDataPivot'>, + options?: RequestOptions, + ): Promise> { + return this.execute('getSessionDataPivot', input, options); + } + + /** + * Get websites website id session data properties + * `GET /api/websites/{websiteId}/session-data/properties` + */ + getSessionDataProperties( + input: OperationInput<'getSessionDataProperties'>, + options?: RequestOptions, + ): Promise> { + return this.execute('getSessionDataProperties', input, options); + } + + /** + * Get websites website id session data property series + * `GET /api/websites/{websiteId}/session-data/property-series` + */ + getSessionDataPropertySeries( + input: OperationInput<'getSessionDataPropertySeries'>, + options?: RequestOptions, + ): Promise> { + return this.execute('getSessionDataPropertySeries', input, options); + } + + /** + * Get websites website id session data stats + * `GET /api/websites/{websiteId}/session-data/stats` + */ + getSessionDataStats( + input: OperationInput<'getSessionDataStats'>, + options?: RequestOptions, + ): Promise> { + return this.execute('getSessionDataStats', input, options); + } + + /** + * Get websites website id session data values + * `GET /api/websites/{websiteId}/session-data/values` + */ + getSessionDataValues( + input: OperationInput<'getSessionDataValues'>, + options?: RequestOptions, + ): Promise> { + return this.execute('getSessionDataValues', input, options); + } + + /** + * Get share id share id + * `GET /api/share/id/{shareId}` + */ + getShare( + input: OperationInput<'getShare'>, + options?: RequestOptions, + ): Promise> { + return this.execute('getShare', input, options); + } + + /** + * Get share slug + * `GET /api/share/{slug}` + */ + getShareBySlug( + input: OperationInput<'getShareBySlug'>, + options?: RequestOptions, + ): Promise> { + return this.execute('getShareBySlug', input, options); + } + + /** + * Get auth subscription + * `GET /api/auth/subscription` + */ + getSubscription( + input?: OperationInput<'getSubscription'>, + options?: RequestOptions, + ): Promise> { + return this.execute('getSubscription', input, options); + } + + /** + * Get teams team id + * `GET /api/teams/{teamId}` + */ + getTeam( + input: OperationInput<'getTeam'>, + options?: RequestOptions, + ): Promise> { + return this.execute('getTeam', input, options); + } + + /** + * Get teams team id boards + * `GET /api/teams/{teamId}/boards` + */ + getTeamBoards( + input: OperationInput<'getTeamBoards'>, + options?: RequestOptions, + ): Promise> { + return this.execute('getTeamBoards', input, options); + } + + /** + * Get teams team id links + * `GET /api/teams/{teamId}/links` + */ + getTeamLinks( + input: OperationInput<'getTeamLinks'>, + options?: RequestOptions, + ): Promise> { + return this.execute('getTeamLinks', input, options); + } + + /** + * Get teams team id pixels + * `GET /api/teams/{teamId}/pixels` + */ + getTeamPixels( + input: OperationInput<'getTeamPixels'>, + options?: RequestOptions, + ): Promise> { + return this.execute('getTeamPixels', input, options); + } + + /** + * Get teams + * `GET /api/teams` + */ + getTeams( + input?: OperationInput<'getTeams'>, + options?: RequestOptions, + ): Promise> { + return this.execute('getTeams', input, options); + } + + /** + * Get teams team id users user id + * `GET /api/teams/{teamId}/users/{userId}` + */ + getTeamUser( + input: OperationInput<'getTeamUser'>, + options?: RequestOptions, + ): Promise> { + return this.execute('getTeamUser', input, options); + } + + /** + * Get teams team id users + * `GET /api/teams/{teamId}/users` + */ + getTeamUsers( + input: OperationInput<'getTeamUsers'>, + options?: RequestOptions, + ): Promise> { + return this.execute('getTeamUsers', input, options); + } + + /** + * Get teams team id websites + * `GET /api/teams/{teamId}/websites` + */ + getTeamWebsites( + input: OperationInput<'getTeamWebsites'>, + options?: RequestOptions, + ): Promise> { + return this.execute('getTeamWebsites', input, options); + } + + /** + * Get 2fa status + * `GET /api/2fa/status` + */ + getTwoFactorStatus( + input?: OperationInput<'getTwoFactorStatus'>, + options?: RequestOptions, + ): Promise> { + return this.execute('getTwoFactorStatus', input, options); + } + + /** + * Get users user id + * `GET /api/users/{userId}` + */ + getUser( + input: OperationInput<'getUser'>, + options?: RequestOptions, + ): Promise> { + return this.execute('getUser', input, options); + } + + /** + * Get users user id teams + * `GET /api/users/{userId}/teams` + */ + getUserTeams( + input: OperationInput<'getUserTeams'>, + options?: RequestOptions, + ): Promise> { + return this.execute('getUserTeams', input, options); + } + + /** + * Get users user id websites + * `GET /api/users/{userId}/websites` + */ + getUserWebsites( + input: OperationInput<'getUserWebsites'>, + options?: RequestOptions, + ): Promise> { + return this.execute('getUserWebsites', input, options); + } + + /** + * Get a website + * `GET /api/websites/{websiteId}` + * OAuth scope: `websites:read` + */ + getWebsite( + input: OperationInput<'getWebsite'>, + options?: RequestOptions, + ): Promise> { + return this.execute('getWebsite', input, options); + } + + /** + * Get current active visitors + * Returns the number of visitors active on the website in the last few minutes. + * `GET /api/websites/{websiteId}/active` + * OAuth scope: `analytics:read` + */ + getWebsiteActive( + input: OperationInput<'getWebsiteActive'>, + options?: RequestOptions, + ): Promise> { + return this.execute('getWebsiteActive', input, options); + } + + /** + * Get websites website id annotations annotation id + * `GET /api/websites/{websiteId}/annotations/{annotationId}` + */ + getWebsiteAnnotation( + input: OperationInput<'getWebsiteAnnotation'>, + options?: RequestOptions, + ): Promise> { + return this.execute('getWebsiteAnnotation', input, options); + } + + /** + * Get websites website id annotations + * `GET /api/websites/{websiteId}/annotations` + */ + getWebsiteAnnotations( + input: OperationInput<'getWebsiteAnnotations'>, + options?: RequestOptions, + ): Promise> { + return this.execute('getWebsiteAnnotations', input, options); + } + + /** + * Get websites website id daterange + * `GET /api/websites/{websiteId}/daterange` + * OAuth scope: `analytics:read` + */ + getWebsiteDateRange( + input: OperationInput<'getWebsiteDateRange'>, + options?: RequestOptions, + ): Promise> { + return this.execute('getWebsiteDateRange', input, options); + } + + /** + * List tracked events + * Returns a page of pageviews and custom events in the date range, newest first. Filter with `event` for a specific event name or `search` for free text. + * `GET /api/websites/{websiteId}/events` + * OAuth scope: `analytics:read` + */ + getWebsiteEvents( + input: OperationInput<'getWebsiteEvents'>, + options?: RequestOptions, + ): Promise> { + return this.execute('getWebsiteEvents', input, options); + } + + /** + * Get websites website id events series + * `GET /api/websites/{websiteId}/events/series` + * OAuth scope: `analytics:read` + */ + getWebsiteEventSeries( + input: OperationInput<'getWebsiteEventSeries'>, + options?: RequestOptions, + ): Promise> { + return this.execute('getWebsiteEventSeries', input, options); + } + + /** + * Get websites website id events stats + * `GET /api/websites/{websiteId}/events/stats` + * OAuth scope: `analytics:read` + */ + getWebsiteEventStats( + input: OperationInput<'getWebsiteEventStats'>, + options?: RequestOptions, + ): Promise> { + return this.execute('getWebsiteEventStats', input, options); + } + + /** + * Get websites website id metrics expanded + * `GET /api/websites/{websiteId}/metrics/expanded` + * OAuth scope: `analytics:read` + */ + getWebsiteExpandedMetrics( + input: OperationInput<'getWebsiteExpandedMetrics'>, + options?: RequestOptions, + ): Promise> { + return this.execute('getWebsiteExpandedMetrics', input, options); + } + + /** + * Get ranked metrics for a dimension + * Returns the top values for one dimension (`type`), such as pages, referrers, countries, browsers, UTM parameters or events, ordered by count. Page-type dimensions count views/events; visitor dimensions count unique visitors. + * `GET /api/websites/{websiteId}/metrics` + * OAuth scope: `analytics:read` + */ + getWebsiteMetrics( + input: OperationInput<'getWebsiteMetrics'>, + options?: RequestOptions, + ): Promise> { + return this.execute('getWebsiteMetrics', input, options); + } + + /** + * Get pageview and session time series + * Returns pageviews and sessions bucketed by `unit` (minute, hour, day, month, year) in the given `timezone`. When `compare` is set the comparison period is included. + * `GET /api/websites/{websiteId}/pageviews` + * OAuth scope: `analytics:read` + */ + getWebsitePageviews( + input: OperationInput<'getWebsitePageviews'>, + options?: RequestOptions, + ): Promise> { + return this.execute('getWebsitePageviews', input, options); + } + + /** + * Get websites website id recorder + * `GET /api/websites/{websiteId}/recorder` + */ + getWebsiteRecorderConfig( + input: OperationInput<'getWebsiteRecorderConfig'>, + options?: RequestOptions, + ): Promise> { + return this.execute('getWebsiteRecorderConfig', input, options); + } + + /** + * Get websites website id replays replay id + * `GET /api/websites/{websiteId}/replays/{replayId}` + */ + getWebsiteReplay( + input: OperationInput<'getWebsiteReplay'>, + options?: RequestOptions, + ): Promise> { + return this.execute('getWebsiteReplay', input, options); + } + + /** + * Get websites website id replays + * `GET /api/websites/{websiteId}/replays` + */ + getWebsiteReplays( + input: OperationInput<'getWebsiteReplays'>, + options?: RequestOptions, + ): Promise> { + return this.execute('getWebsiteReplays', input, options); + } + + /** + * Get websites website id replays saved replay id + * `GET /api/websites/{websiteId}/replays/saved/{replayId}` + */ + getWebsiteReplaySaved( + input: OperationInput<'getWebsiteReplaySaved'>, + options?: RequestOptions, + ): Promise> { + return this.execute('getWebsiteReplaySaved', input, options); + } + + /** + * Get websites website id reports + * `GET /api/websites/{websiteId}/reports` + */ + getWebsiteReports( + input: OperationInput<'getWebsiteReports'>, + options?: RequestOptions, + ): Promise> { + return this.execute('getWebsiteReports', input, options); + } + + /** + * Get websites website id revenue chart + * `GET /api/websites/{websiteId}/revenue/chart` + */ + getWebsiteRevenueChart( + input: OperationInput<'getWebsiteRevenueChart'>, + options?: RequestOptions, + ): Promise> { + return this.execute('getWebsiteRevenueChart', input, options); + } + + /** + * Get websites website id revenue metrics + * `GET /api/websites/{websiteId}/revenue/metrics` + */ + getWebsiteRevenueMetrics( + input: OperationInput<'getWebsiteRevenueMetrics'>, + options?: RequestOptions, + ): Promise> { + return this.execute('getWebsiteRevenueMetrics', input, options); + } + + /** + * Get websites website id revenue sessions + * `GET /api/websites/{websiteId}/revenue/sessions` + */ + getWebsiteRevenueSessions( + input: OperationInput<'getWebsiteRevenueSessions'>, + options?: RequestOptions, + ): Promise> { + return this.execute('getWebsiteRevenueSessions', input, options); + } + + /** + * Get websites website id revenue stats + * `GET /api/websites/{websiteId}/revenue/stats` + */ + getWebsiteRevenueStats( + input: OperationInput<'getWebsiteRevenueStats'>, + options?: RequestOptions, + ): Promise> { + return this.execute('getWebsiteRevenueStats', input, options); + } + + /** + * Get websites website id replays saved + * `GET /api/websites/{websiteId}/replays/saved` + */ + getWebsiteSavedReplays( + input: OperationInput<'getWebsiteSavedReplays'>, + options?: RequestOptions, + ): Promise> { + return this.execute('getWebsiteSavedReplays', input, options); + } + + /** + * Get websites charts + * `GET /api/websites/charts` + */ + getWebsitesCharts( + input: OperationInput<'getWebsitesCharts'>, + options?: RequestOptions, + ): Promise> { + return this.execute('getWebsitesCharts', input, options); + } + + /** + * Get websites website id segments segment id + * `GET /api/websites/{websiteId}/segments/{segmentId}` + */ + getWebsiteSegment( + input: OperationInput<'getWebsiteSegment'>, + options?: RequestOptions, + ): Promise> { + return this.execute('getWebsiteSegment', input, options); + } + + /** + * Get websites website id segments + * `GET /api/websites/{websiteId}/segments` + */ + getWebsiteSegments( + input: OperationInput<'getWebsiteSegments'>, + options?: RequestOptions, + ): Promise> { + return this.execute('getWebsiteSegments', input, options); + } + + /** + * Get websites website id sessions session id + * `GET /api/websites/{websiteId}/sessions/{sessionId}` + * OAuth scope: `analytics:read` + */ + getWebsiteSession( + input: OperationInput<'getWebsiteSession'>, + options?: RequestOptions, + ): Promise> { + return this.execute('getWebsiteSession', input, options); + } + + /** + * Get websites website id sessions session id activity + * `GET /api/websites/{websiteId}/sessions/{sessionId}/activity` + * OAuth scope: `analytics:read` + */ + getWebsiteSessionActivity( + input: OperationInput<'getWebsiteSessionActivity'>, + options?: RequestOptions, + ): Promise> { + return this.execute('getWebsiteSessionActivity', input, options); + } + + /** + * Get websites website id sessions session id properties + * `GET /api/websites/{websiteId}/sessions/{sessionId}/properties` + * OAuth scope: `analytics:read` + */ + getWebsiteSessionProperties( + input: OperationInput<'getWebsiteSessionProperties'>, + options?: RequestOptions, + ): Promise> { + return this.execute('getWebsiteSessionProperties', input, options); + } + + /** + * Get websites website id sessions session id replays + * `GET /api/websites/{websiteId}/sessions/{sessionId}/replays` + */ + getWebsiteSessionReplays( + input: OperationInput<'getWebsiteSessionReplays'>, + options?: RequestOptions, + ): Promise> { + return this.execute('getWebsiteSessionReplays', input, options); + } + + /** + * List visitor sessions + * Returns a page of visitor sessions in the date range, newest first. `search` matches distinct ID, city, browser, OS or device. + * `GET /api/websites/{websiteId}/sessions` + * OAuth scope: `analytics:read` + */ + getWebsiteSessions( + input: OperationInput<'getWebsiteSessions'>, + options?: RequestOptions, + ): Promise> { + return this.execute('getWebsiteSessions', input, options); + } + + /** + * Get websites website id sessions stats + * `GET /api/websites/{websiteId}/sessions/stats` + * OAuth scope: `analytics:read` + */ + getWebsiteSessionStats( + input: OperationInput<'getWebsiteSessionStats'>, + options?: RequestOptions, + ): Promise> { + return this.execute('getWebsiteSessionStats', input, options); + } + + /** + * Get websites website id sessions weekly + * `GET /api/websites/{websiteId}/sessions/weekly` + */ + getWebsiteSessionsWeekly( + input: OperationInput<'getWebsiteSessionsWeekly'>, + options?: RequestOptions, + ): Promise> { + return this.execute('getWebsiteSessionsWeekly', input, options); + } + + /** + * Get websites website id shares + * `GET /api/websites/{websiteId}/shares` + */ + getWebsiteShares( + input: OperationInput<'getWebsiteShares'>, + options?: RequestOptions, + ): Promise> { + return this.execute('getWebsiteShares', input, options); + } + + /** + * Get website summary stats + * Returns pageviews, unique visitors, visits, bounces and total time on site for the date range, plus the same totals for the comparison period (`compare`: prev or yoy). + * `GET /api/websites/{websiteId}/stats` + * OAuth scope: `analytics:read` + */ + getWebsiteStats( + input: OperationInput<'getWebsiteStats'>, + options?: RequestOptions, + ): Promise> { + return this.execute('getWebsiteStats', input, options); + } + + /** + * Get websites website id values + * `GET /api/websites/{websiteId}/values` + */ + getWebsiteValues( + input: OperationInput<'getWebsiteValues'>, + options?: RequestOptions, + ): Promise> { + return this.execute('getWebsiteValues', input, options); + } + + /** + * Create or update 2fa setup initiate + * `POST /api/2fa/setup/initiate` + */ + initiateTwoFactorSetup( + input?: OperationInput<'initiateTwoFactorSetup'>, + options?: RequestOptions, + ): Promise> { + return this.execute('initiateTwoFactorSetup', input, options); + } + + /** + * Create or update teams join + * `POST /api/teams/join` + */ + joinTeam( + input: OperationInput<'joinTeam'>, + options?: RequestOptions, + ): Promise> { + return this.execute('joinTeam', input, options); + } + + /** + * List websites + * Returns websites owned by the authenticated user. + * `GET /api/websites` + * OAuth scope: `websites:read` + */ + listWebsites( + input?: OperationInput<'listWebsites'>, + options?: RequestOptions, + ): Promise> { + return this.execute('listWebsites', input, options); + } + + /** + * Log in + * Authenticates a self-hosted Umami user. Users with two-factor authentication receive a short-lived partial token instead of a full bearer token. + * `POST /api/auth/login` + */ + login( + input: OperationInput<'login'>, + options?: RequestOptions, + ): Promise> { + return this.execute('login', input, options); + } + + /** + * Create or update auth logout + * `POST /api/auth/logout` + */ + logout( + input?: OperationInput<'logout'>, + options?: RequestOptions, + ): Promise> { + return this.execute('logout', input, options); + } + + /** + * Create or update record + * `POST /api/record` + */ + record( + input: OperationInput<'record'>, + options?: RequestOptions, + ): Promise> { + return this.execute('record', input, options); + } + + /** + * Create or update websites website id reset + * `POST /api/websites/{websiteId}/reset` + */ + resetWebsite( + input: OperationInput<'resetWebsite'>, + options?: RequestOptions, + ): Promise> { + return this.execute('resetWebsite', input, options); + } + + /** + * Create or update reports attribution + * `POST /api/reports/attribution` + * OAuth scope: `analytics:read` + */ + runAttributionReport( + input: OperationInput<'runAttributionReport'>, + options?: RequestOptions, + ): Promise> { + return this.execute('runAttributionReport', input, options); + } + + /** + * Create or update reports breakdown + * `POST /api/reports/breakdown` + * OAuth scope: `analytics:read` + */ + runBreakdownReport( + input: OperationInput<'runBreakdownReport'>, + options?: RequestOptions, + ): Promise> { + return this.execute('runBreakdownReport', input, options); + } + + /** + * Create or update reports funnel + * `POST /api/reports/funnel` + * OAuth scope: `analytics:read` + */ + runFunnelReport( + input: OperationInput<'runFunnelReport'>, + options?: RequestOptions, + ): Promise> { + return this.execute('runFunnelReport', input, options); + } + + /** + * Create or update reports goal + * `POST /api/reports/goal` + */ + runGoalReport( + input: OperationInput<'runGoalReport'>, + options?: RequestOptions, + ): Promise> { + return this.execute('runGoalReport', input, options); + } + + /** + * Create or update reports heatmap + * `POST /api/reports/heatmap` + */ + runHeatmapReport( + input: OperationInput<'runHeatmapReport'>, + options?: RequestOptions, + ): Promise> { + return this.execute('runHeatmapReport', input, options); + } + + /** + * Create or update reports journey + * `POST /api/reports/journey` + * OAuth scope: `analytics:read` + */ + runJourneyReport( + input: OperationInput<'runJourneyReport'>, + options?: RequestOptions, + ): Promise> { + return this.execute('runJourneyReport', input, options); + } + + /** + * Create or update reports performance + * `POST /api/reports/performance` + */ + runPerformanceReport( + input: OperationInput<'runPerformanceReport'>, + options?: RequestOptions, + ): Promise> { + return this.execute('runPerformanceReport', input, options); + } + + /** + * Create or update reports retention + * `POST /api/reports/retention` + * OAuth scope: `analytics:read` + */ + runRetentionReport( + input: OperationInput<'runRetentionReport'>, + options?: RequestOptions, + ): Promise> { + return this.execute('runRetentionReport', input, options); + } + + /** + * Create or update reports revenue + * `POST /api/reports/revenue` + * OAuth scope: `analytics:read` + */ + runRevenueReport( + input: OperationInput<'runRevenueReport'>, + options?: RequestOptions, + ): Promise> { + return this.execute('runRevenueReport', input, options); + } + + /** + * Create or update reports utm + * `POST /api/reports/utm` + * OAuth scope: `analytics:read` + */ + runUtmReport( + input: OperationInput<'runUtmReport'>, + options?: RequestOptions, + ): Promise> { + return this.execute('runUtmReport', input, options); + } + + /** + * Create or update websites website id replays saved replay id + * `POST /api/websites/{websiteId}/replays/saved/{replayId}` + */ + saveWebsiteReplay( + input: OperationInput<'saveWebsiteReplay'>, + options?: RequestOptions, + ): Promise> { + return this.execute('saveWebsiteReplay', input, options); + } + + /** + * Create or update send + * `POST /api/send` + */ + send(input: OperationInput<'send'>, options?: RequestOptions): Promise> { + return this.execute('send', input, options); + } + + /** + * Create or update auth sso + * `POST /api/auth/sso` + */ + sso(input?: OperationInput<'sso'>, options?: RequestOptions): Promise> { + return this.execute('sso', input, options); + } + + /** + * Create or update websites website id transfer + * `POST /api/websites/{websiteId}/transfer` + */ + transferWebsite( + input: OperationInput<'transferWebsite'>, + options?: RequestOptions, + ): Promise> { + return this.execute('transferWebsite', input, options); + } + + /** + * Create or update boards board id + * `POST /api/boards/{boardId}` + */ + updateBoard( + input: OperationInput<'updateBoard'>, + options?: RequestOptions, + ): Promise> { + return this.execute('updateBoard', input, options); + } + + /** + * Create or update links link id + * `POST /api/links/{linkId}` + */ + updateLink( + input: OperationInput<'updateLink'>, + options?: RequestOptions, + ): Promise> { + return this.execute('updateLink', input, options); + } + + /** + * Create or update me password + * `POST /api/me/password` + */ + updateMyPassword( + input: OperationInput<'updateMyPassword'>, + options?: RequestOptions, + ): Promise> { + return this.execute('updateMyPassword', input, options); + } + + /** + * Create or update pixels pixel id + * `POST /api/pixels/{pixelId}` + */ + updatePixel( + input: OperationInput<'updatePixel'>, + options?: RequestOptions, + ): Promise> { + return this.execute('updatePixel', input, options); + } + + /** + * Create or update reports report id + * `POST /api/reports/{reportId}` + */ + updateReport( + input: OperationInput<'updateReport'>, + options?: RequestOptions, + ): Promise> { + return this.execute('updateReport', input, options); + } + + /** + * Create or update share id share id + * `POST /api/share/id/{shareId}` + */ + updateShare( + input: OperationInput<'updateShare'>, + options?: RequestOptions, + ): Promise> { + return this.execute('updateShare', input, options); + } + + /** + * Create or update teams team id + * `POST /api/teams/{teamId}` + */ + updateTeam( + input: OperationInput<'updateTeam'>, + options?: RequestOptions, + ): Promise> { + return this.execute('updateTeam', input, options); + } + + /** + * Create or update teams team id users user id + * `POST /api/teams/{teamId}/users/{userId}` + */ + updateTeamUser( + input: OperationInput<'updateTeamUser'>, + options?: RequestOptions, + ): Promise> { + return this.execute('updateTeamUser', input, options); + } + + /** + * Create or update users user id + * `POST /api/users/{userId}` + */ + updateUser( + input: OperationInput<'updateUser'>, + options?: RequestOptions, + ): Promise> { + return this.execute('updateUser', input, options); + } + + /** + * Update a website + * `POST /api/websites/{websiteId}` + */ + updateWebsite( + input: OperationInput<'updateWebsite'>, + options?: RequestOptions, + ): Promise> { + return this.execute('updateWebsite', input, options); + } + + /** + * Create or update websites website id annotations annotation id + * `POST /api/websites/{websiteId}/annotations/{annotationId}` + */ + updateWebsiteAnnotation( + input: OperationInput<'updateWebsiteAnnotation'>, + options?: RequestOptions, + ): Promise> { + return this.execute('updateWebsiteAnnotation', input, options); + } + + /** + * Create or update websites website id segments segment id + * `POST /api/websites/{websiteId}/segments/{segmentId}` + */ + updateWebsiteSegment( + input: OperationInput<'updateWebsiteSegment'>, + options?: RequestOptions, + ): Promise> { + return this.execute('updateWebsiteSegment', input, options); + } + + /** + * Create or update auth verify + * `POST /api/auth/verify` + */ + verify( + input?: OperationInput<'verify'>, + options?: RequestOptions, + ): Promise> { + return this.execute('verify', input, options); + } + + /** + * Create or update 2fa verify + * `POST /api/2fa/verify` + */ + verifyTwoFactor( + input: OperationInput<'verifyTwoFactor'>, + options?: RequestOptions, + ): Promise> { + return this.execute('verifyTwoFactor', input, options); + } +} diff --git a/packages/api-client/src/generated/types.ts b/packages/api-client/src/generated/types.ts new file mode 100644 index 000000000..1c46d6b69 --- /dev/null +++ b/packages/api-client/src/generated/types.ts @@ -0,0 +1,17720 @@ +// GENERATED FILE. DO NOT EDIT. +// Source: public/openapi.json — regenerate with `pnpm generate:api`. + +export interface paths { + '/api/2fa/disable': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** Create or update 2fa disable */ + post: operations['disableTwoFactor']; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/api/2fa/setup/cancel': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** Create or update 2fa setup cancel */ + post: operations['cancelTwoFactorSetup']; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/api/2fa/setup/confirm': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** Create or update 2fa setup confirm */ + post: operations['confirmTwoFactorSetup']; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/api/2fa/setup/initiate': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** Create or update 2fa setup initiate */ + post: operations['initiateTwoFactorSetup']; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/api/2fa/status': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Get 2fa status */ + get: operations['getTwoFactorStatus']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/api/2fa/verify': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** Create or update 2fa verify */ + post: operations['verifyTwoFactor']; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/api/admin/2fa/global': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** Create or update admin 2fa global */ + post: operations['postAdmin2faGlobal']; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/api/admin/teams': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Get admin teams */ + get: operations['getAdminTeams']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/api/admin/teams/{teamId}/2fa': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** Create or update admin teams team id 2fa */ + post: operations['postAdminTeamsTeamId2fa']; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/api/admin/users': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Get admin users */ + get: operations['getAdminUsers']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/api/admin/users/{userId}/2fa': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Get admin users user id 2fa */ + get: operations['getAdminUsersUserId2fa']; + put?: never; + /** Create or update admin users user id 2fa */ + post: operations['postAdminUsersUserId2fa']; + /** Delete admin users user id 2fa */ + delete: operations['deleteAdminUsersUserId2fa']; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/api/admin/websites': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Get admin websites */ + get: operations['getAdminWebsites']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/api/auth/login': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Log in + * @description Authenticates a self-hosted Umami user. Users with two-factor authentication receive a short-lived partial token instead of a full bearer token. + */ + post: operations['login']; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/api/auth/logout': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** Create or update auth logout */ + post: operations['logout']; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/api/auth/sso': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** Create or update auth sso */ + post: operations['sso']; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/api/auth/subscription': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Get auth subscription */ + get: operations['getSubscription']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/api/auth/verify': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** Create or update auth verify */ + post: operations['verify']; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/api/batch': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** Create or update batch */ + post: operations['batch']; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/api/boards': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Get boards */ + get: operations['getBoards']; + put?: never; + /** Create or update boards */ + post: operations['createBoard']; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/api/boards/{boardId}': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Get boards board id */ + get: operations['getBoard']; + put?: never; + /** Create or update boards board id */ + post: operations['updateBoard']; + /** Delete boards board id */ + delete: operations['deleteBoard']; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/api/boards/{boardId}/clone': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** Create or update boards board id clone */ + post: operations['cloneBoard']; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/api/boards/{boardId}/shares': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Get boards board id shares */ + get: operations['getBoardShares']; + put?: never; + /** Create or update boards board id shares */ + post: operations['createBoardShare']; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/api/config': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Get config */ + get: operations['getConfig']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/api/dashboard': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Get dashboard */ + get: operations['getDashboard']; + put?: never; + /** Create or update dashboard */ + post: operations['postDashboard']; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/api/heartbeat': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Get heartbeat */ + get: operations['getHeartbeat']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/api/links': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Get links */ + get: operations['getLinks']; + put?: never; + /** Create or update links */ + post: operations['createLink']; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/api/links/{linkId}': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Get links link id */ + get: operations['getLink']; + put?: never; + /** Create or update links link id */ + post: operations['updateLink']; + /** Delete links link id */ + delete: operations['deleteLink']; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/api/links/{linkId}/shares': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Get links link id shares */ + get: operations['getLinkShares']; + put?: never; + /** Create or update links link id shares */ + post: operations['createLinkShare']; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/api/links/charts': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Get links charts */ + get: operations['getLinksCharts']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/api/me': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Get me */ + get: operations['getMe']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/api/me/api-keys': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Get me api keys */ + get: operations['getMyApiKeys']; + put?: never; + /** Create or update me api keys */ + post: operations['createMyApiKey']; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/api/me/api-keys/{keyId}': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post?: never; + /** Delete me api keys key id */ + delete: operations['deleteMyApiKey']; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/api/me/password': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** Create or update me password */ + post: operations['updateMyPassword']; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/api/me/teams': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Get me teams */ + get: operations['getMyTeams']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/api/me/websites': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Get me websites */ + get: operations['getMyWebsites']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/api/oauth/authorize': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Describe a pending OAuth authorization request + * @description Validates an OAuth 2.1 authorization request and returns the client and scopes for the consent screen. Requires an interactive user session. + */ + get: operations['describeOAuthAuthorization']; + put?: never; + /** + * Approve or deny an OAuth authorization request + * @description Records the user decision. On approval an authorization code is issued and the redirect URL (with code, state and iss) is returned. + */ + post: operations['decideOAuthAuthorization']; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/api/oauth/register': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Register an OAuth client (dynamic client registration) + * @description RFC 7591 compatibility endpoint for MCP clients without Client ID Metadata Document support. Registers a public client. Prefer Client ID Metadata Documents (https URL client_id). + */ + post: operations['oauthRegisterClient']; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/api/oauth/revoke': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Revoke an OAuth refresh token + * @description RFC 7009 revocation endpoint. Revokes the given refresh token; unknown tokens are accepted silently. + */ + post: operations['oauthRevoke']; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/api/oauth/token': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * OAuth 2.1 token endpoint + * @description Exchanges an authorization code (with PKCE code_verifier) or a refresh token for an access token. Accepts application/x-www-form-urlencoded or JSON. Refresh tokens are rotated on every use. + */ + post: operations['oauthToken']; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/api/pixels': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Get pixels */ + get: operations['getPixels']; + put?: never; + /** Create or update pixels */ + post: operations['createPixel']; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/api/pixels/{pixelId}': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Get pixels pixel id */ + get: operations['getPixel']; + put?: never; + /** Create or update pixels pixel id */ + post: operations['updatePixel']; + /** Delete pixels pixel id */ + delete: operations['deletePixel']; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/api/pixels/{pixelId}/shares': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Get pixels pixel id shares */ + get: operations['getPixelShares']; + put?: never; + /** Create or update pixels pixel id shares */ + post: operations['createPixelShare']; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/api/pixels/charts': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Get pixels charts */ + get: operations['getPixelsCharts']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/api/realtime/{websiteId}': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Get realtime website id */ + get: operations['getRealtime']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/api/record': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** Create or update record */ + post: operations['record']; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/api/reports': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Get reports */ + get: operations['getReports']; + put?: never; + /** Create or update reports */ + post: operations['createReport']; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/api/reports/{reportId}': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Get reports report id */ + get: operations['getReport']; + put?: never; + /** Create or update reports report id */ + post: operations['updateReport']; + /** Delete reports report id */ + delete: operations['deleteReport']; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/api/reports/attribution': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** Create or update reports attribution */ + post: operations['runAttributionReport']; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/api/reports/breakdown': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** Create or update reports breakdown */ + post: operations['runBreakdownReport']; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/api/reports/funnel': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** Create or update reports funnel */ + post: operations['runFunnelReport']; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/api/reports/goal': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** Create or update reports goal */ + post: operations['runGoalReport']; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/api/reports/heatmap': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** Create or update reports heatmap */ + post: operations['runHeatmapReport']; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/api/reports/journey': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** Create or update reports journey */ + post: operations['runJourneyReport']; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/api/reports/performance': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** Create or update reports performance */ + post: operations['runPerformanceReport']; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/api/reports/retention': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** Create or update reports retention */ + post: operations['runRetentionReport']; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/api/reports/revenue': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** Create or update reports revenue */ + post: operations['runRevenueReport']; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/api/reports/utm': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** Create or update reports utm */ + post: operations['runUtmReport']; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/api/scripts/telemetry': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Get scripts telemetry */ + get: operations['getScriptsTelemetry']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/api/send': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** Create or update send */ + post: operations['send']; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/api/share': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** Create or update share */ + post: operations['createShare']; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/api/share/{slug}': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Get share slug */ + get: operations['getShareBySlug']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/api/share/id/{shareId}': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Get share id share id */ + get: operations['getShare']; + put?: never; + /** Create or update share id share id */ + post: operations['updateShare']; + /** Delete share id share id */ + delete: operations['deleteShare']; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/api/teams': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Get teams */ + get: operations['getTeams']; + put?: never; + /** Create or update teams */ + post: operations['createTeam']; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/api/teams/{teamId}': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Get teams team id */ + get: operations['getTeam']; + put?: never; + /** Create or update teams team id */ + post: operations['updateTeam']; + /** Delete teams team id */ + delete: operations['deleteTeam']; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/api/teams/{teamId}/boards': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Get teams team id boards */ + get: operations['getTeamBoards']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/api/teams/{teamId}/links': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Get teams team id links */ + get: operations['getTeamLinks']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/api/teams/{teamId}/pixels': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Get teams team id pixels */ + get: operations['getTeamPixels']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/api/teams/{teamId}/users': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Get teams team id users */ + get: operations['getTeamUsers']; + put?: never; + /** Create or update teams team id users */ + post: operations['createTeamUser']; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/api/teams/{teamId}/users/{userId}': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Get teams team id users user id */ + get: operations['getTeamUser']; + put?: never; + /** Create or update teams team id users user id */ + post: operations['updateTeamUser']; + /** Delete teams team id users user id */ + delete: operations['deleteTeamUser']; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/api/teams/{teamId}/websites': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Get teams team id websites */ + get: operations['getTeamWebsites']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/api/teams/join': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** Create or update teams join */ + post: operations['joinTeam']; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/api/users': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** Create or update users */ + post: operations['createUser']; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/api/users/{userId}': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Get users user id */ + get: operations['getUser']; + put?: never; + /** Create or update users user id */ + post: operations['updateUser']; + /** Delete users user id */ + delete: operations['deleteUser']; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/api/users/{userId}/teams': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Get users user id teams */ + get: operations['getUserTeams']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/api/users/{userId}/websites': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Get users user id websites */ + get: operations['getUserWebsites']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/api/websites': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * List websites + * @description Returns websites owned by the authenticated user. + */ + get: operations['listWebsites']; + put?: never; + /** Create a website */ + post: operations['createWebsite']; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/api/websites/{websiteId}': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Get a website */ + get: operations['getWebsite']; + put?: never; + /** Update a website */ + post: operations['updateWebsite']; + /** Delete a website */ + delete: operations['deleteWebsite']; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/api/websites/{websiteId}/active': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Get current active visitors + * @description Returns the number of visitors active on the website in the last few minutes. + */ + get: operations['getWebsiteActive']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/api/websites/{websiteId}/annotations': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Get websites website id annotations */ + get: operations['getWebsiteAnnotations']; + put?: never; + /** Create or update websites website id annotations */ + post: operations['createWebsiteAnnotation']; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/api/websites/{websiteId}/annotations/{annotationId}': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Get websites website id annotations annotation id */ + get: operations['getWebsiteAnnotation']; + put?: never; + /** Create or update websites website id annotations annotation id */ + post: operations['updateWebsiteAnnotation']; + /** Delete websites website id annotations annotation id */ + delete: operations['deleteWebsiteAnnotation']; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/api/websites/{websiteId}/daterange': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Get websites website id daterange */ + get: operations['getWebsiteDateRange']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/api/websites/{websiteId}/event-data': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Get websites website id event data */ + get: operations['getEventData']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/api/websites/{websiteId}/event-data-pivot': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Get websites website id event data pivot */ + get: operations['getEventDataPivot']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/api/websites/{websiteId}/event-data-pivot/array-series': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Get websites website id event data pivot array series */ + get: operations['getEventDataArraySeries']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/api/websites/{websiteId}/event-data-pivot/date-series': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Get websites website id event data pivot date series */ + get: operations['getEventDataDateSeries']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/api/websites/{websiteId}/event-data-pivot/numeric-series': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Get websites website id event data pivot numeric series */ + get: operations['getEventDataNumericSeries']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/api/websites/{websiteId}/event-data-pivot/numeric-stats': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Get websites website id event data pivot numeric stats */ + get: operations['getEventDataNumericStats']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/api/websites/{websiteId}/event-data-pivot/property-series': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Get websites website id event data pivot property series */ + get: operations['getEventDataPropertySeries']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/api/websites/{websiteId}/event-data/{eventId}': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Get websites website id event data event id */ + get: operations['getEventDataById']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/api/websites/{websiteId}/event-data/events': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Get websites website id event data events */ + get: operations['getEventDataEvents']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/api/websites/{websiteId}/event-data/fields': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Get websites website id event data fields */ + get: operations['getEventDataFields']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/api/websites/{websiteId}/event-data/properties': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Get websites website id event data properties */ + get: operations['getEventDataProperties']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/api/websites/{websiteId}/event-data/stats': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Get websites website id event data stats */ + get: operations['getEventDataStats']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/api/websites/{websiteId}/event-data/values': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Get websites website id event data values */ + get: operations['getEventDataValues']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/api/websites/{websiteId}/events': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * List tracked events + * @description Returns a page of pageviews and custom events in the date range, newest first. Filter with `event` for a specific event name or `search` for free text. + */ + get: operations['getWebsiteEvents']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/api/websites/{websiteId}/events/series': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Get websites website id events series */ + get: operations['getWebsiteEventSeries']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/api/websites/{websiteId}/events/stats': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Get websites website id events stats */ + get: operations['getWebsiteEventStats']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/api/websites/{websiteId}/export': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Get websites website id export */ + get: operations['exportWebsite']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/api/websites/{websiteId}/metrics': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Get ranked metrics for a dimension + * @description Returns the top values for one dimension (`type`), such as pages, referrers, countries, browsers, UTM parameters or events, ordered by count. Page-type dimensions count views/events; visitor dimensions count unique visitors. + */ + get: operations['getWebsiteMetrics']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/api/websites/{websiteId}/metrics/expanded': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Get websites website id metrics expanded */ + get: operations['getWebsiteExpandedMetrics']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/api/websites/{websiteId}/pageviews': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Get pageview and session time series + * @description Returns pageviews and sessions bucketed by `unit` (minute, hour, day, month, year) in the given `timezone`. When `compare` is set the comparison period is included. + */ + get: operations['getWebsitePageviews']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/api/websites/{websiteId}/recorder': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Get websites website id recorder */ + get: operations['getWebsiteRecorderConfig']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/api/websites/{websiteId}/replays': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Get websites website id replays */ + get: operations['getWebsiteReplays']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/api/websites/{websiteId}/replays/{replayId}': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Get websites website id replays replay id */ + get: operations['getWebsiteReplay']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/api/websites/{websiteId}/replays/saved': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Get websites website id replays saved */ + get: operations['getWebsiteSavedReplays']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/api/websites/{websiteId}/replays/saved/{replayId}': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Get websites website id replays saved replay id */ + get: operations['getWebsiteReplaySaved']; + put?: never; + /** Create or update websites website id replays saved replay id */ + post: operations['saveWebsiteReplay']; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/api/websites/{websiteId}/reports': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Get websites website id reports */ + get: operations['getWebsiteReports']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/api/websites/{websiteId}/reset': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** Create or update websites website id reset */ + post: operations['resetWebsite']; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/api/websites/{websiteId}/revenue/chart': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Get websites website id revenue chart */ + get: operations['getWebsiteRevenueChart']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/api/websites/{websiteId}/revenue/metrics': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Get websites website id revenue metrics */ + get: operations['getWebsiteRevenueMetrics']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/api/websites/{websiteId}/revenue/sessions': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Get websites website id revenue sessions */ + get: operations['getWebsiteRevenueSessions']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/api/websites/{websiteId}/revenue/stats': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Get websites website id revenue stats */ + get: operations['getWebsiteRevenueStats']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/api/websites/{websiteId}/segments': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Get websites website id segments */ + get: operations['getWebsiteSegments']; + put?: never; + /** Create or update websites website id segments */ + post: operations['createWebsiteSegment']; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/api/websites/{websiteId}/segments/{segmentId}': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Get websites website id segments segment id */ + get: operations['getWebsiteSegment']; + put?: never; + /** Create or update websites website id segments segment id */ + post: operations['updateWebsiteSegment']; + /** Delete websites website id segments segment id */ + delete: operations['deleteWebsiteSegment']; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/api/websites/{websiteId}/session-data-pivot': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Get websites website id session data pivot */ + get: operations['getSessionDataPivot']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/api/websites/{websiteId}/session-data/array-series': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Get websites website id session data array series */ + get: operations['getSessionDataArraySeries']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/api/websites/{websiteId}/session-data/date-series': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Get websites website id session data date series */ + get: operations['getSessionDataDateSeries']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/api/websites/{websiteId}/session-data/numeric-series': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Get websites website id session data numeric series */ + get: operations['getSessionDataNumericSeries']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/api/websites/{websiteId}/session-data/numeric-stats': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Get websites website id session data numeric stats */ + get: operations['getSessionDataNumericStats']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/api/websites/{websiteId}/session-data/properties': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Get websites website id session data properties */ + get: operations['getSessionDataProperties']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/api/websites/{websiteId}/session-data/property-series': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Get websites website id session data property series */ + get: operations['getSessionDataPropertySeries']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/api/websites/{websiteId}/session-data/stats': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Get websites website id session data stats */ + get: operations['getSessionDataStats']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/api/websites/{websiteId}/session-data/values': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Get websites website id session data values */ + get: operations['getSessionDataValues']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/api/websites/{websiteId}/sessions': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * List visitor sessions + * @description Returns a page of visitor sessions in the date range, newest first. `search` matches distinct ID, city, browser, OS or device. + */ + get: operations['getWebsiteSessions']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/api/websites/{websiteId}/sessions/{sessionId}': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Get websites website id sessions session id */ + get: operations['getWebsiteSession']; + put?: never; + post?: never; + /** Delete websites website id sessions session id */ + delete: operations['deleteWebsiteSession']; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/api/websites/{websiteId}/sessions/{sessionId}/activity': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Get websites website id sessions session id activity */ + get: operations['getWebsiteSessionActivity']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/api/websites/{websiteId}/sessions/{sessionId}/properties': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Get websites website id sessions session id properties */ + get: operations['getWebsiteSessionProperties']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/api/websites/{websiteId}/sessions/{sessionId}/replays': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Get websites website id sessions session id replays */ + get: operations['getWebsiteSessionReplays']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/api/websites/{websiteId}/sessions/stats': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Get websites website id sessions stats */ + get: operations['getWebsiteSessionStats']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/api/websites/{websiteId}/sessions/weekly': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Get websites website id sessions weekly */ + get: operations['getWebsiteSessionsWeekly']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/api/websites/{websiteId}/shares': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Get websites website id shares */ + get: operations['getWebsiteShares']; + put?: never; + /** Create or update websites website id shares */ + post: operations['createWebsiteShare']; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/api/websites/{websiteId}/stats': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Get website summary stats + * @description Returns pageviews, unique visitors, visits, bounces and total time on site for the date range, plus the same totals for the comparison period (`compare`: prev or yoy). + */ + get: operations['getWebsiteStats']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/api/websites/{websiteId}/transfer': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** Create or update websites website id transfer */ + post: operations['transferWebsite']; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/api/websites/{websiteId}/values': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Get websites website id values */ + get: operations['getWebsiteValues']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/api/websites/charts': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Get websites charts */ + get: operations['getWebsitesCharts']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; +} +export type webhooks = Record; +export interface components { + schemas: { + ActiveVisitors: { + visitors: number; + }; + /** @description Standard Umami API error response. */ + ApiError: { + error: { + code: string; + message: string; + status: number; + } & { + [key: string]: unknown; + }; + }; + CreateWebsiteRequest: { + domain: string; + id?: string | null; + name: string; + shareId?: string | null; + teamId?: string | null; + }; + LoginRequest: { + /** @description Umami password. */ + password: string; + /** @description Umami username. */ + username: string; + }; + LoginResponse: + | { + partialToken: string; + /** @constant */ + requiresTwoFactor: true; + } + | { + token: string; + user: components['schemas']['LoginUser']; + }; + LoginTeam: { + /** Format: uuid */ + id: string; + logoUrl: string | null; + name: string; + }; + LoginUser: { + createdAt: string | null; + /** Format: uuid */ + id: string; + isAdmin: boolean; + role: string; + teams: components['schemas']['LoginTeam'][]; + username: string; + }; + MetricRow: { + t?: string; + /** @description Dimension value (e.g. a path or country). */ + x: string | null; + /** @description Count of views/events or unique visitors. */ + y: number; + }; + MetricRows: components['schemas']['MetricRow'][]; + OAuthAuthorizationDecision: { + client_id: string; + code_challenge?: string; + code_challenge_method?: string; + /** @enum {string} */ + decision: 'approve' | 'deny'; + redirect_uri?: string; + resource?: string; + response_type?: string; + scope?: string; + state?: string; + }; + OAuthAuthorizationDetails: { + client: { + id: string; + logoUri?: string; + name: string; + /** @enum {string} */ + source: 'metadata-document' | 'registered'; + uri?: string; + }; + redirectUri: string; + resource: string; + scopes: { + description: string; + scope: string; + }[]; + }; + OAuthAuthorizationRedirect: { + redirectUrl: string; + }; + OAuthClientRegistrationRequest: { + /** @enum {string} */ + application_type?: 'web' | 'native'; + client_name: string; + client_uri?: string; + grant_types?: string[]; + logo_uri?: string; + redirect_uris: string[]; + response_types?: string[]; + scope?: string; + software_id?: string; + software_version?: string; + /** @constant */ + token_endpoint_auth_method?: 'none'; + }; + OAuthClientRegistrationResponse: { + /** @enum {string} */ + application_type?: 'web' | 'native'; + client_id: string; + client_id_issued_at: number; + client_name: string; + client_uri?: string; + grant_types: string[]; + logo_uri?: string; + redirect_uris: string[]; + response_types: string[]; + scope?: string; + software_id?: string; + software_version?: string; + /** @constant */ + token_endpoint_auth_method: 'none'; + }; + OAuthErrorResponse: { + error: string; + error_description?: string; + }; + OAuthRevocationRequest: { + token: string; + token_type_hint?: string; + }; + OAuthTokenRequest: { + client_id?: string; + code?: string; + code_verifier?: string; + /** @enum {string} */ + grant_type: 'authorization_code' | 'refresh_token'; + redirect_uri?: string; + refresh_token?: string; + resource?: string; + scope?: string; + }; + OAuthTokenResponse: { + access_token: string; + expires_in: number; + refresh_token: string; + scope: string; + /** @constant */ + token_type: 'Bearer'; + }; + /** @description Successful operation response. */ + Ok: { + /** @constant */ + ok: true; + }; + ReplayConfig: { + blockSelector?: string; + heatmapEnabled?: boolean; + heatmapSampleRate?: number; + /** @enum {string} */ + maskLevel?: 'strict' | 'moderate'; + maxDuration?: number; + replayEnabled?: boolean; + sampleRate?: number; + } & { + [key: string]: unknown; + }; + ReplayConfigInput: { + blockSelector?: string; + heatmapEnabled?: boolean; + heatmapSampleRate?: number; + /** @enum {string} */ + maskLevel?: 'strict' | 'moderate'; + maxDuration?: number; + replayEnabled?: boolean; + sampleRate?: number; + }; + TimeSeriesPoint: { + /** @description Bucket start (ISO date/time). */ + x: string; + /** @description Value for the bucket. */ + y: number; + }; + UpdateWebsiteRequest: { + domain?: string; + name?: string; + replayConfig?: components['schemas']['ReplayConfigInput'] | null; + shareId?: string | null; + }; + Website: { + createdAt: string | null; + createdBy: string | null; + deletedAt: string | null; + domain: string | null; + /** Format: uuid */ + id: string; + name: string; + recorderEnabled: boolean; + replayConfig: components['schemas']['ReplayConfig'] | null; + resetAt: string | null; + shareId: string | null; + teamId: string | null; + updatedAt: string | null; + user?: components['schemas']['WebsiteUser']; + userId: string | null; + }; + WebsiteEvent: { + /** Format: date-time */ + createdAt: string; + distinctId?: string | null; + eventName?: string | null; + eventType: number; + hostname?: string | null; + /** Format: uuid */ + id: string; + pageTitle?: string | null; + referrerDomain?: string | null; + /** Format: uuid */ + sessionId: string; + urlPath?: string | null; + urlQuery?: string | null; + /** Format: uuid */ + websiteId: string; + } & { + [key: string]: unknown; + }; + WebsiteEventPage: { + count: number; + data: components['schemas']['WebsiteEvent'][]; + isCapped?: boolean; + page: number; + pageSize: number; + }; + WebsitePage: { + count: number; + data: components['schemas']['Website'][]; + orderBy?: string; + page: number; + pageSize: number; + search?: string; + }; + WebsitePageviews: { + /** @description Present when `compare` was requested. */ + compare?: { + /** Format: date-time */ + endDate: string; + pageviews: components['schemas']['TimeSeriesPoint'][]; + sessions: components['schemas']['TimeSeriesPoint'][]; + /** Format: date-time */ + startDate: string; + }; + /** Format: date-time */ + endDate?: string; + pageviews: components['schemas']['TimeSeriesPoint'][]; + sessions: components['schemas']['TimeSeriesPoint'][]; + /** Format: date-time */ + startDate?: string; + }; + WebsiteSession: { + browser: string | null; + city: string | null; + country: string | null; + /** Format: date-time */ + createdAt: string; + device: string | null; + distinctId?: string | null; + events?: number; + /** Format: date-time */ + firstAt: string; + hostname: string | null; + /** Format: uuid */ + id: string; + language: string | null; + /** Format: date-time */ + lastAt: string; + os: string | null; + region: string | null; + screen: string | null; + views: number; + visits: number; + /** Format: uuid */ + websiteId: string; + } & { + [key: string]: unknown; + }; + WebsiteSessionPage: { + count: number; + data: components['schemas']['WebsiteSession'][]; + isCapped?: boolean; + page: number; + pageSize: number; + }; + WebsiteStats: { + bounces: number; + /** @description The same totals for the comparison period (previous period by default). */ + comparison: components['schemas']['WebsiteStatsValues']; + pageviews: number; + totaltime: number; + visitors: number; + visits: number; + }; + WebsiteStatsValues: { + bounces: number; + pageviews: number; + totaltime: number; + visitors: number; + visits: number; + }; + WebsiteUser: { + /** Format: uuid */ + id: string; + username: string; + }; + }; + responses: { + /** @description Bad request. */ + BadRequestResponse: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "bad-request", + * "message": "Bad request.", + * "status": 400 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + /** @description Forbidden. */ + ForbiddenResponse: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "forbidden", + * "message": "Forbidden.", + * "status": 403 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + /** @description Not found. */ + NotFoundResponse: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "not-found", + * "message": "Not found.", + * "status": 404 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + /** @description Payload too large. */ + PayloadTooLargeResponse: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "payload-too-large", + * "message": "Payload too large.", + * "status": 413 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + /** @description Server error. */ + ServerErrorResponse: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "server-error", + * "message": "Server error.", + * "status": 500 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + /** @description Service unavailable. */ + ServiceUnavailableResponse: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "service-unavailable", + * "message": "Service unavailable.", + * "status": 503 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + /** @description Unauthorized. */ + UnauthorizedResponse: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "unauthorized", + * "message": "Unauthorized.", + * "status": 401 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + }; + parameters: never; + requestBodies: never; + headers: never; + pathItems: never; +} +export type $defs = Record; +export interface operations { + disableTwoFactor: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + 'application/json': { + password: string; + token: string; + }; + }; + }; + responses: { + /** @description The operation completed successfully. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': + | { + error: { + code: string; + /** Format: date-time */ + lockedUntil: string; + message: string; + }; + } + | { + ok: boolean; + }; + }; + }; + /** @description Bad request. */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "bad-request", + * "message": "Bad request.", + * "status": 400 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + /** @description Unauthorized. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "unauthorized", + * "message": "Unauthorized.", + * "status": 401 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + /** @description Forbidden. */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "forbidden", + * "message": "Forbidden.", + * "status": 403 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + /** @description Not found. */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "not-found", + * "message": "Not found.", + * "status": 404 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + /** @description The operation completed successfully. */ + 429: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + error: { + code: string; + /** Format: date-time */ + lockedUntil: string; + message: string; + }; + }; + }; + }; + /** @description Service unavailable. */ + 503: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "service-unavailable", + * "message": "Service unavailable.", + * "status": 503 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + }; + }; + cancelTwoFactorSetup: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description The operation completed successfully. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + ok: boolean; + }; + }; + }; + /** @description Unauthorized. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "unauthorized", + * "message": "Unauthorized.", + * "status": 401 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + /** @description Not found. */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "not-found", + * "message": "Not found.", + * "status": 404 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + }; + }; + confirmTwoFactorSetup: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + 'application/json': { + token: string; + }; + }; + }; + responses: { + /** @description The operation completed successfully. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': + | { + error: { + code: string; + /** Format: date-time */ + lockedUntil: string; + message: string; + }; + } + | { + backupCodes: string[]; + }; + }; + }; + /** @description Bad request. */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "bad-request", + * "message": "Bad request.", + * "status": 400 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + /** @description Unauthorized. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "unauthorized", + * "message": "Unauthorized.", + * "status": 401 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + /** @description Not found. */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "not-found", + * "message": "Not found.", + * "status": 404 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + /** @description The operation completed successfully. */ + 429: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + error: { + code: string; + /** Format: date-time */ + lockedUntil: string; + message: string; + }; + }; + }; + }; + /** @description Service unavailable. */ + 503: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "service-unavailable", + * "message": "Service unavailable.", + * "status": 503 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + }; + }; + initiateTwoFactorSetup: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description The operation completed successfully. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + manualKey: string; + qrCodeDataUrl: string; + }; + }; + }; + /** @description Bad request. */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "bad-request", + * "message": "Bad request.", + * "status": 400 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + /** @description Unauthorized. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "unauthorized", + * "message": "Unauthorized.", + * "status": 401 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + /** @description Not found. */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "not-found", + * "message": "Not found.", + * "status": 404 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + /** @description Service unavailable. */ + 503: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "service-unavailable", + * "message": "Service unavailable.", + * "status": 503 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + }; + }; + getTwoFactorStatus: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description The operation completed successfully. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': + | { + globalRequired: boolean; + isConfigured: boolean; + isEnabled: boolean; + isRequired: boolean; + requiredReason: null; + } + | { + globalRequired: boolean; + isConfigured: boolean; + isEnabled: boolean; + isRequired: boolean; + requiredReason: string; + }; + }; + }; + /** @description Unauthorized. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "unauthorized", + * "message": "Unauthorized.", + * "status": 401 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + }; + }; + verifyTwoFactor: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + 'application/json': + | { + token: string; + } + | { + backupCode: string; + }; + }; + }; + responses: { + /** @description The operation completed successfully. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': + | { + error: { + code: string; + /** Format: date-time */ + lockedUntil: string; + message: string; + }; + } + | { + token: string; + user: { + /** Format: date-time */ + createdAt: string; + id: string; + isAdmin: boolean; + role: string; + teams: { + id: string; + logoUrl: string; + name: string; + }[]; + username: string; + }; + }; + }; + }; + /** @description Bad request. */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "bad-request", + * "message": "Bad request.", + * "status": 400 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + /** @description Unauthorized. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "unauthorized", + * "message": "Unauthorized.", + * "status": 401 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + /** @description Not found. */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "not-found", + * "message": "Not found.", + * "status": 404 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + /** @description The operation completed successfully. */ + 429: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + error: { + code: string; + /** Format: date-time */ + lockedUntil: string; + message: string; + }; + }; + }; + }; + /** @description Service unavailable. */ + 503: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "service-unavailable", + * "message": "Service unavailable.", + * "status": 503 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + }; + }; + postAdmin2faGlobal: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + 'application/json': { + required: boolean; + }; + }; + }; + responses: { + /** @description The operation completed successfully. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + ok: boolean; + required: unknown; + }; + }; + }; + /** @description Bad request. */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "bad-request", + * "message": "Bad request.", + * "status": 400 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + /** @description Unauthorized. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "unauthorized", + * "message": "Unauthorized.", + * "status": 401 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + /** @description Not found. */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "not-found", + * "message": "Not found.", + * "status": 404 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + /** @description Service unavailable. */ + 503: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "service-unavailable", + * "message": "Service unavailable.", + * "status": 503 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + }; + }; + getAdminTeams: { + parameters: { + query?: { + maxResults?: number; + orderBy?: string; + page?: number; + pageSize?: number; + search?: string; + sortDescending?: 'true' | 'false'; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description The operation completed successfully. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + count: number; + data: { + accessCode: string; + /** Format: date-time */ + createdAt: string; + /** Format: date-time */ + deletedAt: string; + id: string; + logoUrl: string; + name: string; + twoFactorRequired: boolean; + /** Format: date-time */ + updatedAt: string; + }[]; + isCapped?: boolean; + orderBy?: string; + page: number; + pageSize: number; + search?: string; + sortDescending?: boolean; + }; + }; + }; + /** @description Bad request. */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "bad-request", + * "message": "Bad request.", + * "status": 400 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + /** @description Unauthorized. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "unauthorized", + * "message": "Unauthorized.", + * "status": 401 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + }; + }; + postAdminTeamsTeamId2fa: { + parameters: { + query?: never; + header?: never; + path: { + teamId: string; + }; + cookie?: never; + }; + requestBody: { + content: { + 'application/json': { + required: boolean; + }; + }; + }; + responses: { + /** @description The operation completed successfully. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + ok: boolean; + teamId: string; + twoFactorRequired: unknown; + }; + }; + }; + /** @description Bad request. */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "bad-request", + * "message": "Bad request.", + * "status": 400 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + /** @description Unauthorized. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "unauthorized", + * "message": "Unauthorized.", + * "status": 401 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + /** @description Not found. */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "not-found", + * "message": "Not found.", + * "status": 404 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + /** @description Service unavailable. */ + 503: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "service-unavailable", + * "message": "Service unavailable.", + * "status": 503 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + }; + }; + getAdminUsers: { + parameters: { + query?: { + maxResults?: number; + orderBy?: string; + page?: number; + pageSize?: number; + search?: string; + sortDescending?: 'true' | 'false'; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description The operation completed successfully. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + count: number; + data: { + _count?: { + websites: number; + }; + /** Format: date-time */ + createdAt: string; + id: string; + role: string; + twoFactorRequired: boolean; + username: string; + }[]; + isCapped?: boolean; + orderBy?: string; + page: number; + pageSize: number; + search?: string; + sortDescending?: boolean; + }; + }; + }; + /** @description Bad request. */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "bad-request", + * "message": "Bad request.", + * "status": 400 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + /** @description Unauthorized. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "unauthorized", + * "message": "Unauthorized.", + * "status": 401 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + }; + }; + getAdminUsersUserId2fa: { + parameters: { + query?: never; + header?: never; + path: { + userId: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description The operation completed successfully. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + isEnabled: boolean; + }; + }; + }; + /** @description Unauthorized. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "unauthorized", + * "message": "Unauthorized.", + * "status": 401 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + /** @description Not found. */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "not-found", + * "message": "Not found.", + * "status": 404 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + }; + }; + postAdminUsersUserId2fa: { + parameters: { + query?: never; + header?: never; + path: { + userId: string; + }; + cookie?: never; + }; + requestBody: { + content: { + 'application/json': { + required: boolean; + }; + }; + }; + responses: { + /** @description The operation completed successfully. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + ok: boolean; + twoFactorRequired: unknown; + userId: string; + }; + }; + }; + /** @description Bad request. */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "bad-request", + * "message": "Bad request.", + * "status": 400 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + /** @description Unauthorized. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "unauthorized", + * "message": "Unauthorized.", + * "status": 401 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + /** @description Not found. */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "not-found", + * "message": "Not found.", + * "status": 404 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + /** @description Service unavailable. */ + 503: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "service-unavailable", + * "message": "Service unavailable.", + * "status": 503 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + }; + }; + deleteAdminUsersUserId2fa: { + parameters: { + query?: never; + header?: never; + path: { + userId: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description The operation completed successfully. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + ok: boolean; + reset: { + backupCodes: unknown; + otpUsed: unknown; + rateLimit: unknown; + twoFactorAuth: unknown; + }; + userId: string; + }; + }; + }; + /** @description Unauthorized. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "unauthorized", + * "message": "Unauthorized.", + * "status": 401 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + /** @description Not found. */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "not-found", + * "message": "Not found.", + * "status": 404 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + }; + }; + getAdminWebsites: { + parameters: { + query?: { + maxResults?: number; + orderBy?: string; + page?: number; + pageSize?: number; + search?: string; + sortDescending?: 'true' | 'false'; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description The operation completed successfully. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + count: number; + data: ({ + /** Format: date-time */ + createdAt: string; + createdBy: string; + /** Format: date-time */ + deletedAt: string; + domain: string; + id: string; + name: string; + recorderEnabled: boolean; + replayConfig: + | string + | number + | false + | true + | { + [key: string]: + | string + | number + | false + | true + | unknown + | { + length: number; + }; + } + | { + length: number; + }; + /** Format: date-time */ + resetAt: string; + teamId: string; + /** Format: date-time */ + updatedAt: string; + userId: string; + } & { + createUser?: { + id: string; + username: string; + }; + shareId: string; + team?: { + members: { + role: string; + userId: string; + }[]; + }; + user?: { + id: string; + username: string; + }; + })[]; + isCapped?: boolean; + orderBy?: string; + page: number; + pageSize: number; + search?: string; + sortDescending?: boolean; + }; + }; + }; + /** @description Bad request. */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "bad-request", + * "message": "Bad request.", + * "status": 400 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + /** @description Unauthorized. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "unauthorized", + * "message": "Unauthorized.", + * "status": 401 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + }; + }; + login: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + 'application/json': components['schemas']['LoginRequest']; + }; + }; + responses: { + /** @description Authenticated successfully. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['LoginResponse']; + }; + }; + /** @description Bad request. */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "bad-request", + * "message": "Bad request.", + * "status": 400 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + /** @description Unauthorized. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "unauthorized", + * "message": "Unauthorized.", + * "status": 401 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + /** @description Service unavailable. */ + 503: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "service-unavailable", + * "message": "Service unavailable.", + * "status": 503 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + }; + }; + logout: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description The operation completed successfully. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + /** @constant */ + ok: true; + }; + }; + }; + /** @description Unauthorized. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "unauthorized", + * "message": "Unauthorized.", + * "status": 401 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + }; + }; + sso: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description The operation completed successfully. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + token: string; + user: unknown; + }; + }; + }; + /** @description Unauthorized. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "unauthorized", + * "message": "Unauthorized.", + * "status": 401 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + /** @description Server error. */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "server-error", + * "message": "Server error.", + * "status": 500 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + }; + }; + getSubscription: { + parameters: { + query?: { + teamId?: string; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description The operation completed successfully. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': + | { + error: { + code: string; + message: string; + status: number; + }; + } + | { + hasSubscription: boolean; + isBusiness: boolean; + isNoBilling: boolean; + isPro: boolean; + unlimitedWebsites: boolean; + }; + }; + }; + /** @description Bad request. */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "bad-request", + * "message": "Bad request.", + * "status": 400 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + /** @description Unauthorized. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "unauthorized", + * "message": "Unauthorized.", + * "status": 401 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + }; + }; + verify: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description The operation completed successfully. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + teams: unknown; + }; + }; + }; + /** @description Unauthorized. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "unauthorized", + * "message": "Unauthorized.", + * "status": 401 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + }; + }; + batch: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + 'application/json': { + [key: string]: unknown; + }[]; + }; + }; + responses: { + /** @description The operation completed successfully. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + cache: unknown; + details: unknown[]; + errors: number; + processed: number; + size: unknown; + }; + }; + }; + /** @description Bad request. */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "bad-request", + * "message": "Bad request.", + * "status": 400 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + /** @description Server error. */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "server-error", + * "message": "Server error.", + * "status": 500 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + }; + }; + getBoards: { + parameters: { + query?: { + maxResults?: number; + orderBy?: string; + page?: number; + pageSize?: number; + search?: string; + sortDescending?: 'true' | 'false'; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description The operation completed successfully. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + count: number; + data: { + /** Format: date-time */ + createdAt: string; + description: string; + id: string; + name: string; + parameters: + | string + | number + | false + | true + | { + [key: string]: + | string + | number + | false + | true + | unknown + | { + length: number; + }; + } + | { + length: number; + }; + teamId: string; + type: string; + /** Format: date-time */ + updatedAt: string; + userId: string; + }[]; + isCapped?: boolean; + orderBy?: string; + page: number; + pageSize: number; + search?: string; + sortDescending?: boolean; + }; + }; + }; + /** @description Bad request. */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "bad-request", + * "message": "Bad request.", + * "status": 400 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + /** @description Unauthorized. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "unauthorized", + * "message": "Unauthorized.", + * "status": 401 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + }; + }; + createBoard: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + 'application/json': { + description?: string; + name: string; + parameters?: { + /** Format: uuid */ + linkId?: string; + /** Format: uuid */ + pixelId?: string; + /** Format: uuid */ + websiteId?: string; + }; + teamId?: string | null; + type: never | 'open'; + userId?: string | null; + }; + }; + }; + responses: { + /** @description The operation completed successfully. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + /** Format: date-time */ + createdAt: string; + description: string; + id: string; + name: string; + parameters: + | string + | number + | false + | true + | { + [key: string]: + | string + | number + | false + | true + | unknown + | { + length: number; + }; + } + | { + length: number; + }; + teamId: string; + type: string; + /** Format: date-time */ + updatedAt: string; + userId: string; + }; + }; + }; + /** @description Bad request. */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "bad-request", + * "message": "Bad request.", + * "status": 400 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + /** @description Unauthorized. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "unauthorized", + * "message": "Unauthorized.", + * "status": 401 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + }; + }; + getBoard: { + parameters: { + query?: never; + header?: never; + path: { + boardId: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description The operation completed successfully. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + /** Format: date-time */ + createdAt: string; + description: string; + id: string; + name: string; + parameters: + | string + | number + | false + | true + | { + [key: string]: + | string + | number + | false + | true + | unknown + | { + length: number; + }; + } + | { + length: number; + }; + teamId: string; + type: string; + /** Format: date-time */ + updatedAt: string; + userId: string; + }; + }; + }; + /** @description Unauthorized. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "unauthorized", + * "message": "Unauthorized.", + * "status": 401 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + }; + }; + updateBoard: { + parameters: { + query?: never; + header?: never; + path: { + boardId: string; + }; + cookie?: never; + }; + requestBody: { + content: { + 'application/json': { + description?: string; + name?: string; + parameters?: Record; + type?: never | 'open'; + }; + }; + }; + responses: { + /** @description The operation completed successfully. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + /** Format: date-time */ + createdAt: string; + description: string; + id: string; + name: string; + parameters: + | string + | number + | false + | true + | { + [key: string]: + | string + | number + | false + | true + | unknown + | { + length: number; + }; + } + | { + length: number; + }; + teamId: string; + type: string; + /** Format: date-time */ + updatedAt: string; + userId: string; + }; + }; + }; + /** @description Bad request. */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "bad-request", + * "message": "Bad request.", + * "status": 400 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + /** @description Unauthorized. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "unauthorized", + * "message": "Unauthorized.", + * "status": 401 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + /** @description Server error. */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "server-error", + * "message": "Server error.", + * "status": 500 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + }; + }; + deleteBoard: { + parameters: { + query?: never; + header?: never; + path: { + boardId: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description The operation completed successfully. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + /** @constant */ + ok: true; + }; + }; + }; + /** @description Unauthorized. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "unauthorized", + * "message": "Unauthorized.", + * "status": 401 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + }; + }; + cloneBoard: { + parameters: { + query?: never; + header?: never; + path: { + boardId: string; + }; + cookie?: never; + }; + requestBody: { + content: { + 'application/json': { + description?: string; + name?: string; + parameters?: Record; + }; + }; + }; + responses: { + /** @description The operation completed successfully. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + /** Format: date-time */ + createdAt: string; + description: string; + id: string; + name: string; + parameters: + | string + | number + | false + | true + | { + [key: string]: + | string + | number + | false + | true + | unknown + | { + length: number; + }; + } + | { + length: number; + }; + teamId: string; + type: string; + /** Format: date-time */ + updatedAt: string; + userId: string; + }; + }; + }; + /** @description Bad request. */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "bad-request", + * "message": "Bad request.", + * "status": 400 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + /** @description Unauthorized. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "unauthorized", + * "message": "Unauthorized.", + * "status": 401 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + }; + }; + getBoardShares: { + parameters: { + query?: { + browser?: string; + city?: string; + cohort?: string; + country?: string; + device?: string; + distinctId?: string; + event?: string; + eventType?: number; + excludeBounce?: string; + hostname?: string; + language?: string; + match?: 'all' | 'any'; + maxResults?: number; + os?: string; + page?: number; + pageSize?: number; + path?: string; + query?: string; + referrer?: string; + region?: string; + segment?: string; + tag?: string; + title?: string; + utmCampaign?: string; + utmContent?: string; + utmMedium?: string; + utmSource?: string; + utmTerm?: string; + }; + header?: never; + path: { + boardId: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description The operation completed successfully. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + count: number; + data: { + /** Format: date-time */ + createdAt: string; + entityId: string; + id: string; + name: string; + parameters: + | string + | number + | false + | true + | { + [key: string]: + | string + | number + | false + | true + | unknown + | { + length: number; + }; + } + | { + length: number; + }; + shareType: number; + slug: string; + /** Format: date-time */ + updatedAt: string; + }[]; + isCapped?: boolean; + orderBy?: string; + page: number; + pageSize: number; + search?: string; + sortDescending?: boolean; + }; + }; + }; + /** @description Bad request. */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "bad-request", + * "message": "Bad request.", + * "status": 400 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + /** @description Unauthorized. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "unauthorized", + * "message": "Unauthorized.", + * "status": 401 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + }; + }; + createBoardShare: { + parameters: { + query?: never; + header?: never; + path: { + boardId: string; + }; + cookie?: never; + }; + requestBody: { + content: { + 'application/json': { + name: string; + parameters?: { + [key: string]: unknown; + }; + }; + }; + }; + responses: { + /** @description The operation completed successfully. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + /** Format: date-time */ + createdAt: string; + entityId: string; + id: string; + name: string; + parameters: + | string + | number + | false + | true + | { + [key: string]: + | string + | number + | false + | true + | unknown + | { + length: number; + }; + } + | { + length: number; + }; + shareType: number; + slug: string; + /** Format: date-time */ + updatedAt: string; + }; + }; + }; + /** @description Bad request. */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "bad-request", + * "message": "Bad request.", + * "status": 400 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + /** @description Unauthorized. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "unauthorized", + * "message": "Unauthorized.", + * "status": 401 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + }; + }; + getConfig: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description The operation completed successfully. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + cloudMode: boolean; + faviconUrl: string; + linksUrl: string; + pixelsUrl: string; + privateMode: boolean; + sessionDeletionEnabled: boolean; + telemetryDisabled: boolean; + trackerScriptName: string; + updatesDisabled: boolean; + }; + }; + }; + }; + }; + getDashboard: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description The operation completed successfully. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + /** Format: date-time */ + createdAt: string; + description: string; + id: string; + name: string; + parameters: + | string + | number + | false + | true + | { + [key: string]: + | string + | number + | false + | true + | unknown + | { + length: number; + }; + } + | { + length: number; + }; + teamId: string; + type: string; + /** Format: date-time */ + updatedAt: string; + userId: string; + }; + }; + }; + /** @description Unauthorized. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "unauthorized", + * "message": "Unauthorized.", + * "status": 401 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + }; + }; + postDashboard: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + 'application/json': { + description?: string; + name?: string; + parameters?: Record; + }; + }; + }; + responses: { + /** @description The operation completed successfully. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + /** Format: date-time */ + createdAt: string; + description: string; + id: string; + name: string; + parameters: + | string + | number + | false + | true + | { + [key: string]: + | string + | number + | false + | true + | unknown + | { + length: number; + }; + } + | { + length: number; + }; + teamId: string; + type: string; + /** Format: date-time */ + updatedAt: string; + userId: string; + }; + }; + }; + /** @description Bad request. */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "bad-request", + * "message": "Bad request.", + * "status": 400 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + /** @description Unauthorized. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "unauthorized", + * "message": "Unauthorized.", + * "status": 401 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + }; + }; + getHeartbeat: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description The operation completed successfully. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + ok: boolean; + }; + }; + }; + }; + }; + getLinks: { + parameters: { + query?: { + maxResults?: number; + orderBy?: string; + page?: number; + pageSize?: number; + search?: string; + sortDescending?: 'true' | 'false'; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description The operation completed successfully. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + count: number; + data: { + /** Format: date-time */ + createdAt: string; + /** Format: date-time */ + deletedAt: string; + id: string; + name: string; + slug: string; + teamId: string; + /** Format: date-time */ + updatedAt: string; + url: string; + userId: string; + }[]; + isCapped?: boolean; + orderBy?: string; + page: number; + pageSize: number; + search?: string; + sortDescending?: boolean; + }; + }; + }; + /** @description Bad request. */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "bad-request", + * "message": "Bad request.", + * "status": 400 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + /** @description Unauthorized. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "unauthorized", + * "message": "Unauthorized.", + * "status": 401 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + }; + }; + createLink: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + 'application/json': { + id?: string | null; + name: string; + slug: string; + teamId?: string | null; + url: string; + }; + }; + }; + responses: { + /** @description The operation completed successfully. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + /** Format: date-time */ + createdAt: string; + /** Format: date-time */ + deletedAt: string; + id: string; + name: string; + slug: string; + teamId: string; + /** Format: date-time */ + updatedAt: string; + url: string; + userId: string; + }; + }; + }; + /** @description Bad request. */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "bad-request", + * "message": "Bad request.", + * "status": 400 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + /** @description Unauthorized. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "unauthorized", + * "message": "Unauthorized.", + * "status": 401 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + }; + }; + getLink: { + parameters: { + query?: never; + header?: never; + path: { + linkId: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description The operation completed successfully. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + /** Format: date-time */ + createdAt: string; + /** Format: date-time */ + deletedAt: string; + id: string; + name: string; + slug: string; + teamId: string; + /** Format: date-time */ + updatedAt: string; + url: string; + userId: string; + }; + }; + }; + /** @description Unauthorized. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "unauthorized", + * "message": "Unauthorized.", + * "status": 401 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + }; + }; + updateLink: { + parameters: { + query?: never; + header?: never; + path: { + linkId: string; + }; + cookie?: never; + }; + requestBody: { + content: { + 'application/json': { + name?: string; + slug?: string; + url?: string; + }; + }; + }; + responses: { + /** @description The operation completed successfully. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + /** Format: date-time */ + createdAt: string; + /** Format: date-time */ + deletedAt: string; + id: string; + name: string; + slug: string; + teamId: string; + /** Format: date-time */ + updatedAt: string; + url: string; + userId: string; + }; + }; + }; + /** @description Bad request. */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "bad-request", + * "message": "Bad request.", + * "status": 400 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + /** @description Unauthorized. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "unauthorized", + * "message": "Unauthorized.", + * "status": 401 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + /** @description Server error. */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "server-error", + * "message": "Server error.", + * "status": 500 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + }; + }; + deleteLink: { + parameters: { + query?: never; + header?: never; + path: { + linkId: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description The operation completed successfully. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + /** @constant */ + ok: true; + }; + }; + }; + /** @description Unauthorized. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "unauthorized", + * "message": "Unauthorized.", + * "status": 401 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + }; + }; + getLinkShares: { + parameters: { + query?: { + browser?: string; + city?: string; + cohort?: string; + country?: string; + device?: string; + distinctId?: string; + event?: string; + eventType?: number; + excludeBounce?: string; + hostname?: string; + language?: string; + match?: 'all' | 'any'; + maxResults?: number; + os?: string; + page?: number; + pageSize?: number; + path?: string; + query?: string; + referrer?: string; + region?: string; + segment?: string; + tag?: string; + title?: string; + utmCampaign?: string; + utmContent?: string; + utmMedium?: string; + utmSource?: string; + utmTerm?: string; + }; + header?: never; + path: { + linkId: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description The operation completed successfully. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + count: number; + data: { + /** Format: date-time */ + createdAt: string; + entityId: string; + id: string; + name: string; + parameters: + | string + | number + | false + | true + | { + [key: string]: + | string + | number + | false + | true + | unknown + | { + length: number; + }; + } + | { + length: number; + }; + shareType: number; + slug: string; + /** Format: date-time */ + updatedAt: string; + }[]; + isCapped?: boolean; + orderBy?: string; + page: number; + pageSize: number; + search?: string; + sortDescending?: boolean; + }; + }; + }; + /** @description Bad request. */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "bad-request", + * "message": "Bad request.", + * "status": 400 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + /** @description Unauthorized. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "unauthorized", + * "message": "Unauthorized.", + * "status": 401 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + }; + }; + createLinkShare: { + parameters: { + query?: never; + header?: never; + path: { + linkId: string; + }; + cookie?: never; + }; + requestBody: { + content: { + 'application/json': { + name: string; + parameters?: { + [key: string]: unknown; + }; + }; + }; + }; + responses: { + /** @description The operation completed successfully. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + /** Format: date-time */ + createdAt: string; + entityId: string; + id: string; + name: string; + parameters: + | string + | number + | false + | true + | { + [key: string]: + | string + | number + | false + | true + | unknown + | { + length: number; + }; + } + | { + length: number; + }; + shareType: number; + slug: string; + /** Format: date-time */ + updatedAt: string; + }; + }; + }; + /** @description Bad request. */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "bad-request", + * "message": "Bad request.", + * "status": 400 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + /** @description Unauthorized. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "unauthorized", + * "message": "Unauthorized.", + * "status": 401 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + }; + }; + getLinksCharts: { + parameters: { + query: { + endAt?: number; + ids: string; + startAt?: number; + timezone?: string; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description The operation completed successfully. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + data: { + [key: string]: { + total: number; + values: number[]; + }; + }; + }; + }; + }; + /** @description Bad request. */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "bad-request", + * "message": "Bad request.", + * "status": 400 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + /** @description Unauthorized. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "unauthorized", + * "message": "Unauthorized.", + * "status": 401 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + }; + }; + getMe: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description The operation completed successfully. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + apiKey?: { + id: string; + name: string; + }; + authType?: 'session' | 'share' | 'api-key' | 'oauth'; + oauth?: { + clientId: string; + scopes: string[]; + tokenId?: string; + }; + shareToken?: { + boardId?: string; + linkId?: string; + linkIds?: string[]; + parameters?: { + allowFilter?: boolean; + theme?: 'light' | 'dark'; + } & { + [key: string]: false | true | 'light' | 'dark'; + }; + pixelId?: string; + pixelIds?: string[]; + shareType?: number; + websiteId?: string; + websiteIds?: string[]; + }; + user?: { + id: string; + isAdmin: boolean; + role: string; + username: string; + }; + }; + }; + }; + /** @description Unauthorized. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "unauthorized", + * "message": "Unauthorized.", + * "status": 401 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + }; + }; + getMyApiKeys: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description The operation completed successfully. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + /** Format: date-time */ + createdAt: string; + id: string; + keyPrefix: string; + /** Format: date-time */ + lastUsedAt: string; + name: string; + }[]; + }; + }; + /** @description Unauthorized. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "unauthorized", + * "message": "Unauthorized.", + * "status": 401 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + /** @description Not found. */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "not-found", + * "message": "Not found.", + * "status": 404 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + }; + }; + createMyApiKey: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + 'application/json': { + name: unknown; + }; + }; + }; + responses: { + /** @description The operation completed successfully. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + /** Format: date-time */ + createdAt: string; + id: string; + key: string; + keyPrefix: string; + name: string; + }; + }; + }; + /** @description Bad request. */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "bad-request", + * "message": "Bad request.", + * "status": 400 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + /** @description Unauthorized. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "unauthorized", + * "message": "Unauthorized.", + * "status": 401 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + /** @description Not found. */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "not-found", + * "message": "Not found.", + * "status": 404 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + }; + }; + deleteMyApiKey: { + parameters: { + query?: never; + header?: never; + path: { + keyId: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description The operation completed successfully. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + /** @constant */ + ok: true; + }; + }; + }; + /** @description Unauthorized. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "unauthorized", + * "message": "Unauthorized.", + * "status": 401 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + /** @description Not found. */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "not-found", + * "message": "Not found.", + * "status": 404 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + }; + }; + updateMyPassword: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + 'application/json': { + currentPassword: string; + newPassword: string; + }; + }; + }; + responses: { + /** @description The operation completed successfully. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + /** Format: date-time */ + createdAt: string; + id: string; + role: string; + username: string; + }; + }; + }; + /** @description Bad request. */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "bad-request", + * "message": "Bad request.", + * "status": 400 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + /** @description Unauthorized. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "unauthorized", + * "message": "Unauthorized.", + * "status": 401 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + }; + }; + getMyTeams: { + parameters: { + query?: { + maxResults?: number; + orderBy?: string; + page?: number; + pageSize?: number; + sortDescending?: 'true' | 'false'; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description The operation completed successfully. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + count: number; + data: { + accessCode: string; + /** Format: date-time */ + createdAt: string; + /** Format: date-time */ + deletedAt: string; + id: string; + logoUrl: string; + name: string; + twoFactorRequired: boolean; + /** Format: date-time */ + updatedAt: string; + }[]; + isCapped?: boolean; + orderBy?: string; + page: number; + pageSize: number; + search?: string; + sortDescending?: boolean; + }; + }; + }; + /** @description Bad request. */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "bad-request", + * "message": "Bad request.", + * "status": 400 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + /** @description Unauthorized. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "unauthorized", + * "message": "Unauthorized.", + * "status": 401 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + }; + }; + getMyWebsites: { + parameters: { + query?: { + includeTeams?: string; + maxResults?: number; + orderBy?: string; + page?: number; + pageSize?: number; + sortDescending?: 'true' | 'false'; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description The operation completed successfully. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + count: number; + data: ({ + /** Format: date-time */ + createdAt: string; + createdBy: string; + /** Format: date-time */ + deletedAt: string; + domain: string; + id: string; + name: string; + recorderEnabled: boolean; + replayConfig: + | string + | number + | false + | true + | { + [key: string]: + | string + | number + | false + | true + | unknown + | { + length: number; + }; + } + | { + length: number; + }; + /** Format: date-time */ + resetAt: string; + teamId: string; + /** Format: date-time */ + updatedAt: string; + userId: string; + } & { + createUser?: { + id: string; + username: string; + }; + shareId: string; + team?: { + members: { + role: string; + userId: string; + }[]; + }; + user?: { + id: string; + username: string; + }; + })[]; + isCapped?: boolean; + orderBy?: string; + page: number; + pageSize: number; + search?: string; + sortDescending?: boolean; + }; + }; + }; + /** @description Bad request. */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "bad-request", + * "message": "Bad request.", + * "status": 400 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + /** @description Unauthorized. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "unauthorized", + * "message": "Unauthorized.", + * "status": 401 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + }; + }; + describeOAuthAuthorization: { + parameters: { + query: { + client_id: string; + code_challenge?: string; + code_challenge_method?: string; + redirect_uri?: string; + resource?: string; + response_type?: string; + scope?: string; + state?: string; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Authorization request details. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['OAuthAuthorizationDetails']; + }; + }; + /** @description Invalid authorization request. */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['OAuthErrorResponse']; + }; + }; + /** @description Unauthorized. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "unauthorized", + * "message": "Unauthorized.", + * "status": 401 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + }; + }; + decideOAuthAuthorization: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + 'application/json': components['schemas']['OAuthAuthorizationDecision']; + }; + }; + responses: { + /** @description Redirect URL for the browser. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['OAuthAuthorizationRedirect']; + }; + }; + /** @description Bad request. */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "bad-request", + * "message": "Bad request.", + * "status": 400 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + /** @description Unauthorized. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "unauthorized", + * "message": "Unauthorized.", + * "status": 401 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + }; + }; + oauthRegisterClient: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + 'application/json': components['schemas']['OAuthClientRegistrationRequest']; + }; + }; + responses: { + /** @description Registered client. */ + 201: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['OAuthClientRegistrationResponse']; + }; + }; + /** @description Invalid client metadata. */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['OAuthErrorResponse']; + }; + }; + }; + }; + oauthRevoke: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + 'application/json': components['schemas']['OAuthRevocationRequest']; + 'application/x-www-form-urlencoded': components['schemas']['OAuthRevocationRequest']; + }; + }; + responses: { + /** @description Token revoked (or already invalid). */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': Record; + }; + }; + /** @description Malformed request. */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['OAuthErrorResponse']; + }; + }; + }; + }; + oauthToken: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + 'application/json': components['schemas']['OAuthTokenRequest']; + 'application/x-www-form-urlencoded': components['schemas']['OAuthTokenRequest']; + }; + }; + responses: { + /** @description Access and refresh tokens. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['OAuthTokenResponse']; + }; + }; + /** @description Invalid grant or request. */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['OAuthErrorResponse']; + }; + }; + /** @description Invalid client. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['OAuthErrorResponse']; + }; + }; + }; + }; + getPixels: { + parameters: { + query?: { + maxResults?: number; + orderBy?: string; + page?: number; + pageSize?: number; + search?: string; + sortDescending?: 'true' | 'false'; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description The operation completed successfully. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + count: number; + data: { + /** Format: date-time */ + createdAt: string; + /** Format: date-time */ + deletedAt: string; + id: string; + name: string; + slug: string; + teamId: string; + /** Format: date-time */ + updatedAt: string; + userId: string; + }[]; + isCapped?: boolean; + orderBy?: string; + page: number; + pageSize: number; + search?: string; + sortDescending?: boolean; + }; + }; + }; + /** @description Bad request. */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "bad-request", + * "message": "Bad request.", + * "status": 400 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + /** @description Unauthorized. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "unauthorized", + * "message": "Unauthorized.", + * "status": 401 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + }; + }; + createPixel: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + 'application/json': { + id?: string | null; + name: string; + slug: string; + teamId?: string | null; + }; + }; + }; + responses: { + /** @description The operation completed successfully. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + /** Format: date-time */ + createdAt: string; + /** Format: date-time */ + deletedAt: string; + id: string; + name: string; + slug: string; + teamId: string; + /** Format: date-time */ + updatedAt: string; + userId: string; + }; + }; + }; + /** @description Bad request. */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "bad-request", + * "message": "Bad request.", + * "status": 400 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + /** @description Unauthorized. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "unauthorized", + * "message": "Unauthorized.", + * "status": 401 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + }; + }; + getPixel: { + parameters: { + query?: never; + header?: never; + path: { + pixelId: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description The operation completed successfully. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + /** Format: date-time */ + createdAt: string; + /** Format: date-time */ + deletedAt: string; + id: string; + name: string; + slug: string; + teamId: string; + /** Format: date-time */ + updatedAt: string; + userId: string; + }; + }; + }; + /** @description Unauthorized. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "unauthorized", + * "message": "Unauthorized.", + * "status": 401 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + }; + }; + updatePixel: { + parameters: { + query?: never; + header?: never; + path: { + pixelId: string; + }; + cookie?: never; + }; + requestBody: { + content: { + 'application/json': { + name?: string; + slug?: string; + }; + }; + }; + responses: { + /** @description The operation completed successfully. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + /** Format: date-time */ + createdAt: string; + /** Format: date-time */ + deletedAt: string; + id: string; + name: string; + slug: string; + teamId: string; + /** Format: date-time */ + updatedAt: string; + userId: string; + }; + }; + }; + /** @description Bad request. */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "bad-request", + * "message": "Bad request.", + * "status": 400 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + /** @description Unauthorized. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "unauthorized", + * "message": "Unauthorized.", + * "status": 401 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + /** @description Server error. */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "server-error", + * "message": "Server error.", + * "status": 500 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + }; + }; + deletePixel: { + parameters: { + query?: never; + header?: never; + path: { + pixelId: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description The operation completed successfully. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + /** @constant */ + ok: true; + }; + }; + }; + /** @description Unauthorized. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "unauthorized", + * "message": "Unauthorized.", + * "status": 401 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + }; + }; + getPixelShares: { + parameters: { + query?: { + browser?: string; + city?: string; + cohort?: string; + country?: string; + device?: string; + distinctId?: string; + event?: string; + eventType?: number; + excludeBounce?: string; + hostname?: string; + language?: string; + match?: 'all' | 'any'; + maxResults?: number; + os?: string; + page?: number; + pageSize?: number; + path?: string; + query?: string; + referrer?: string; + region?: string; + segment?: string; + tag?: string; + title?: string; + utmCampaign?: string; + utmContent?: string; + utmMedium?: string; + utmSource?: string; + utmTerm?: string; + }; + header?: never; + path: { + pixelId: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description The operation completed successfully. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + count: number; + data: { + /** Format: date-time */ + createdAt: string; + entityId: string; + id: string; + name: string; + parameters: + | string + | number + | false + | true + | { + [key: string]: + | string + | number + | false + | true + | unknown + | { + length: number; + }; + } + | { + length: number; + }; + shareType: number; + slug: string; + /** Format: date-time */ + updatedAt: string; + }[]; + isCapped?: boolean; + orderBy?: string; + page: number; + pageSize: number; + search?: string; + sortDescending?: boolean; + }; + }; + }; + /** @description Bad request. */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "bad-request", + * "message": "Bad request.", + * "status": 400 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + /** @description Unauthorized. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "unauthorized", + * "message": "Unauthorized.", + * "status": 401 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + }; + }; + createPixelShare: { + parameters: { + query?: never; + header?: never; + path: { + pixelId: string; + }; + cookie?: never; + }; + requestBody: { + content: { + 'application/json': { + name: string; + parameters?: { + [key: string]: unknown; + }; + }; + }; + }; + responses: { + /** @description The operation completed successfully. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + /** Format: date-time */ + createdAt: string; + entityId: string; + id: string; + name: string; + parameters: + | string + | number + | false + | true + | { + [key: string]: + | string + | number + | false + | true + | unknown + | { + length: number; + }; + } + | { + length: number; + }; + shareType: number; + slug: string; + /** Format: date-time */ + updatedAt: string; + }; + }; + }; + /** @description Bad request. */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "bad-request", + * "message": "Bad request.", + * "status": 400 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + /** @description Unauthorized. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "unauthorized", + * "message": "Unauthorized.", + * "status": 401 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + }; + }; + getPixelsCharts: { + parameters: { + query: { + endAt?: number; + ids: string; + startAt?: number; + timezone?: string; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description The operation completed successfully. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + data: { + [key: string]: { + total: number; + values: number[]; + }; + }; + }; + }; + }; + /** @description Bad request. */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "bad-request", + * "message": "Bad request.", + * "status": 400 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + /** @description Unauthorized. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "unauthorized", + * "message": "Unauthorized.", + * "status": 401 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + }; + }; + getRealtime: { + parameters: { + query?: { + browser?: string; + city?: string; + cohort?: string; + country?: string; + device?: string; + distinctId?: string; + event?: string; + eventType?: number; + excludeBounce?: string; + hostname?: string; + language?: string; + match?: 'all' | 'any'; + os?: string; + path?: string; + query?: string; + referrer?: string; + region?: string; + segment?: string; + tag?: string; + timezone?: string; + title?: string; + unit?: string; + utmCampaign?: string; + utmContent?: string; + utmMedium?: string; + utmSource?: string; + utmTerm?: string; + }; + header?: never; + path: { + websiteId: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description The operation completed successfully. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + countries: { + [key: string]: number; + }; + events: { + __type: 'event' | 'session' | 'pageview'; + browser: string; + country: string; + createdAt: string; + device: string; + eventName: string; + hostname: string; + os: string; + referrerDomain: string; + sessionId: string; + urlPath: string; + }[]; + referrers: { + [key: string]: number; + }; + series: { + views: { + t: string; + x: string; + y: number; + }[]; + visitors: { + t: string; + x: string; + y: number; + }[]; + }; + timestamp: number; + totals: { + countries: number; + events: number; + views: number; + visitors: number; + }; + urls: { + [key: string]: number; + }; + }; + }; + }; + /** @description Bad request. */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "bad-request", + * "message": "Bad request.", + * "status": 400 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + /** @description Unauthorized. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "unauthorized", + * "message": "Unauthorized.", + * "status": 401 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + }; + }; + record: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + 'application/json': + | { + payload: { + events: unknown[]; + timestamp?: number; + /** Format: uuid */ + website: string; + }; + /** @constant */ + type: 'record'; + } + | { + payload: { + events: ( + | { + pageH?: number; + pageW?: number; + pageX?: number; + pageY?: number; + timestamp?: number; + /** @constant */ + type: 'click'; + url: string; + viewportH?: number; + viewportW?: number; + x?: number; + y?: number; + } + | { + pageH?: number; + pageW?: number; + scrollPct?: number; + timestamp?: number; + /** @constant */ + type: 'scroll'; + url: string; + viewportH?: number; + viewportW?: number; + } + )[]; + timestamp?: number; + /** Format: uuid */ + website: string; + }; + /** @constant */ + type: 'heatmap'; + }; + }; + }; + responses: { + /** @description The operation completed successfully. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': + | { + ok: boolean; + } + | { + ok: boolean; + reason: string; + } + | { + beep: string; + }; + }; + }; + /** @description Bad request. */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "bad-request", + * "message": "Bad request.", + * "status": 400 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + /** @description Forbidden. */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "forbidden", + * "message": "Forbidden.", + * "status": 403 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + /** @description Payload too large. */ + 413: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "payload-too-large", + * "message": "Payload too large.", + * "status": 413 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + /** @description Server error. */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "server-error", + * "message": "Server error.", + * "status": 500 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + }; + }; + getReports: { + parameters: { + query: { + maxResults?: number; + page?: number; + pageSize?: number; + type?: string; + websiteId: string; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description The operation completed successfully. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + count: number; + data: ({ + /** Format: date-time */ + createdAt: string; + description: string; + id: string; + name: string; + parameters: + | string + | number + | false + | true + | { + [key: string]: + | string + | number + | false + | true + | unknown + | { + length: number; + }; + } + | { + length: number; + }; + type: string; + /** Format: date-time */ + updatedAt: string; + userId: string; + websiteId: string; + } & { + website?: { + domain: string; + userId: string; + }; + })[]; + isCapped?: boolean; + orderBy?: string; + page: number; + pageSize: number; + search?: string; + sortDescending?: boolean; + }; + }; + }; + /** @description Bad request. */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "bad-request", + * "message": "Bad request.", + * "status": 400 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + /** @description Unauthorized. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "unauthorized", + * "message": "Unauthorized.", + * "status": 401 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + }; + }; + createReport: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + 'application/json': { + description?: string; + name: string; + parameters: { + [key: string]: unknown; + }; + type: string; + /** Format: uuid */ + websiteId: string; + }; + }; + }; + responses: { + /** @description The operation completed successfully. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + /** Format: date-time */ + createdAt: string; + description: string; + id: string; + name: string; + parameters: + | string + | number + | false + | true + | { + [key: string]: + | string + | number + | false + | true + | unknown + | { + length: number; + }; + } + | { + length: number; + }; + type: string; + /** Format: date-time */ + updatedAt: string; + userId: string; + websiteId: string; + }; + }; + }; + /** @description Bad request. */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "bad-request", + * "message": "Bad request.", + * "status": 400 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + /** @description Unauthorized. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "unauthorized", + * "message": "Unauthorized.", + * "status": 401 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + }; + }; + getReport: { + parameters: { + query?: never; + header?: never; + path: { + reportId: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description The operation completed successfully. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + /** Format: date-time */ + createdAt: string; + description: string; + id: string; + name: string; + parameters: + | string + | number + | false + | true + | { + [key: string]: + | string + | number + | false + | true + | unknown + | { + length: number; + }; + } + | { + length: number; + }; + type: string; + /** Format: date-time */ + updatedAt: string; + userId: string; + websiteId: string; + }; + }; + }; + /** @description Unauthorized. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "unauthorized", + * "message": "Unauthorized.", + * "status": 401 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + /** @description Not found. */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "not-found", + * "message": "Not found.", + * "status": 404 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + }; + }; + updateReport: { + parameters: { + query?: never; + header?: never; + path: { + reportId: string; + }; + cookie?: never; + }; + requestBody: { + content: { + 'application/json': { + description?: string; + name: string; + parameters: { + [key: string]: unknown; + }; + type: string; + /** Format: uuid */ + websiteId: string; + }; + }; + }; + responses: { + /** @description The operation completed successfully. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + /** Format: date-time */ + createdAt: string; + description: string; + id: string; + name: string; + parameters: + | string + | number + | false + | true + | { + [key: string]: + | string + | number + | false + | true + | unknown + | { + length: number; + }; + } + | { + length: number; + }; + type: string; + /** Format: date-time */ + updatedAt: string; + userId: string; + websiteId: string; + }; + }; + }; + /** @description Bad request. */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "bad-request", + * "message": "Bad request.", + * "status": 400 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + /** @description Unauthorized. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "unauthorized", + * "message": "Unauthorized.", + * "status": 401 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + /** @description Not found. */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "not-found", + * "message": "Not found.", + * "status": 404 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + }; + }; + deleteReport: { + parameters: { + query?: never; + header?: never; + path: { + reportId: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description The operation completed successfully. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + /** @constant */ + ok: true; + }; + }; + }; + /** @description Unauthorized. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "unauthorized", + * "message": "Unauthorized.", + * "status": 401 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + /** @description Not found. */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "not-found", + * "message": "Not found.", + * "status": 404 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + }; + }; + runAttributionReport: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + 'application/json': { + filters: { + [key: string]: unknown; + }; + parameters: { + [key: string]: unknown; + }; + type: string; + /** Format: uuid */ + websiteId: string; + }; + }; + }; + responses: { + /** @description The operation completed successfully. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + paidAds: { + name: string; + value: number; + }[]; + referrer: { + name: string; + value: number; + }[]; + total: { + pageviews: number; + visitors: number; + visits: number; + }; + utm_campaign: { + name: string; + value: number; + }[]; + utm_content: { + name: string; + value: number; + }[]; + utm_medium: { + name: string; + value: number; + }[]; + utm_source: { + name: string; + value: number; + }[]; + utm_term: { + name: string; + value: number; + }[]; + }; + }; + }; + /** @description Bad request. */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "bad-request", + * "message": "Bad request.", + * "status": 400 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + /** @description Unauthorized. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "unauthorized", + * "message": "Unauthorized.", + * "status": 401 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + }; + }; + runBreakdownReport: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + 'application/json': { + filters: { + [key: string]: unknown; + }; + parameters: { + [key: string]: unknown; + }; + type: string; + /** Format: uuid */ + websiteId: string; + }; + }; + }; + responses: { + /** @description The operation completed successfully. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': ({ + bounces: number; + totaltime: number; + views: number; + visitors: number; + visits: number; + } & { + [key: string]: string | number; + })[]; + }; + }; + /** @description Bad request. */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "bad-request", + * "message": "Bad request.", + * "status": 400 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + /** @description Unauthorized. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "unauthorized", + * "message": "Unauthorized.", + * "status": 401 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + }; + }; + runFunnelReport: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + 'application/json': { + filters: { + [key: string]: unknown; + }; + parameters: { + [key: string]: unknown; + }; + type: string; + /** Format: uuid */ + websiteId: string; + }; + }; + }; + responses: { + /** @description The operation completed successfully. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + dropoff: number; + dropped: number; + filters?: { + operator: string; + property: string; + value: string; + }[]; + previous: number; + remaining: number; + type: string; + value: string; + visitors: number; + }[]; + }; + }; + /** @description Bad request. */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "bad-request", + * "message": "Bad request.", + * "status": 400 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + /** @description Unauthorized. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "unauthorized", + * "message": "Unauthorized.", + * "status": 401 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + }; + }; + runGoalReport: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + 'application/json': { + filters: { + [key: string]: unknown; + }; + parameters: { + [key: string]: unknown; + }; + type: string; + /** Format: uuid */ + websiteId: string; + }; + }; + }; + responses: { + /** @description The operation completed successfully. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + num: number; + total: number; + }; + }; + }; + /** @description Bad request. */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "bad-request", + * "message": "Bad request.", + * "status": 400 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + /** @description Unauthorized. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "unauthorized", + * "message": "Unauthorized.", + * "status": 401 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + }; + }; + runHeatmapReport: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + 'application/json': { + filters: { + [key: string]: unknown; + }; + parameters: { + [key: string]: unknown; + }; + type: string; + /** Format: uuid */ + websiteId: string; + }; + }; + }; + responses: { + /** @description The operation completed successfully. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + mode: 'click' | 'scroll'; + pages: { + count: number; + sessions: number; + urlPath: string; + }[]; + points: { + count: number; + pageH: number; + pageW: number; + pageX: number; + pageY: number; + viewportH: number; + viewportW: number; + x: number; + y: number; + }[]; + scroll: { + buckets: { + depth: number; + pageH: number; + pageW: number; + sessions: number; + viewportH: number; + viewportW: number; + }[]; + pageH: number; + pageW: number; + totalSessions: number; + viewportH: number; + viewportW: number; + }; + snapshot: { + id: string; + /** @constant */ + kind: 'iframe'; + pageH: number; + pageW: number; + url: string; + viewportH: number; + viewportW: number; + }; + }; + }; + }; + /** @description Bad request. */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "bad-request", + * "message": "Bad request.", + * "status": 400 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + /** @description Unauthorized. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "unauthorized", + * "message": "Unauthorized.", + * "status": 401 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + }; + }; + runJourneyReport: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + 'application/json': { + filters: { + [key: string]: unknown; + }; + parameters: { + [key: string]: unknown; + }; + type: string; + /** Format: uuid */ + websiteId: string; + }; + }; + }; + responses: { + /** @description The operation completed successfully. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + count: number; + e1: string; + e2: string; + e3: string; + e4: string; + e5: string; + e6: string; + e7: string; + }[]; + }; + }; + /** @description Bad request. */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "bad-request", + * "message": "Bad request.", + * "status": 400 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + /** @description Unauthorized. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "unauthorized", + * "message": "Unauthorized.", + * "status": 401 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + }; + }; + runPerformanceReport: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + 'application/json': { + filters: { + [key: string]: unknown; + }; + parameters: { + [key: string]: unknown; + }; + type: string; + /** Format: uuid */ + websiteId: string; + }; + }; + }; + responses: { + /** @description The operation completed successfully. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + browsers: unknown; + chart: unknown; + devices: unknown; + pages: unknown; + pageTitles: unknown; + summary: unknown; + }; + }; + }; + /** @description Bad request. */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "bad-request", + * "message": "Bad request.", + * "status": 400 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + /** @description Unauthorized. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "unauthorized", + * "message": "Unauthorized.", + * "status": 401 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + }; + }; + runRetentionReport: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + 'application/json': { + filters: { + [key: string]: unknown; + }; + parameters: { + [key: string]: unknown; + }; + type: string; + /** Format: uuid */ + websiteId: string; + }; + }; + }; + responses: { + /** @description The operation completed successfully. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + date: string; + day: number; + percentage: number; + returnVisitors: number; + visitors: number; + }[]; + }; + }; + /** @description Bad request. */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "bad-request", + * "message": "Bad request.", + * "status": 400 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + /** @description Unauthorized. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "unauthorized", + * "message": "Unauthorized.", + * "status": 401 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + }; + }; + runRevenueReport: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + 'application/json': { + filters: { + [key: string]: unknown; + }; + parameters: { + [key: string]: unknown; + }; + type: string; + /** Format: uuid */ + websiteId: string; + }; + }; + }; + responses: { + /** @description The operation completed successfully. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + channel: { + name: string; + value: number; + }[]; + chart: { + count: number; + t: string; + x: string; + y: number; + }[]; + country: { + name: string; + value: number; + }[]; + referrer: { + name: string; + value: number; + }[]; + region: { + country: string; + name: string; + value: number; + }[]; + total: unknown; + }; + }; + }; + /** @description Bad request. */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "bad-request", + * "message": "Bad request.", + * "status": 400 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + /** @description Unauthorized. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "unauthorized", + * "message": "Unauthorized.", + * "status": 401 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + }; + }; + runUtmReport: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + 'application/json': { + filters: { + [key: string]: unknown; + }; + parameters: { + [key: string]: unknown; + }; + type: string; + /** Format: uuid */ + websiteId: string; + }; + }; + }; + responses: { + /** @description The operation completed successfully. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + utm_campaign: unknown[]; + utm_content: unknown[]; + utm_medium: unknown[]; + utm_source: unknown[]; + utm_term: unknown[]; + }; + }; + }; + /** @description Bad request. */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "bad-request", + * "message": "Bad request.", + * "status": 400 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + /** @description Unauthorized. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "unauthorized", + * "message": "Unauthorized.", + * "status": 401 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + }; + }; + getScriptsTelemetry: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful response. The response shape is inferred as free-form because the handler does not expose a reusable response schema. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'text/javascript': string; + }; + }; + }; + }; + send: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + 'application/json': { + payload: { + browser?: string; + cls?: number; + data?: { + [key: string]: unknown; + }; + device?: string; + fcp?: number; + hostname?: string; + id?: string; + inp?: number; + ip?: string; + language?: string; + lcp?: number; + /** Format: uuid */ + link?: string; + name?: unknown; + os?: string; + /** Format: uuid */ + pixel?: string; + referrer?: string; + screen?: string; + tag?: unknown; + timestamp?: number; + title?: string; + ttfb?: number; + url?: string; + userAgent?: string; + /** Format: uuid */ + website?: string; + }; + /** @enum {string} */ + type: 'event' | 'identify' | 'performance'; + }; + }; + }; + responses: { + /** @description The operation completed successfully. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': + | { + beep: string; + } + | { + cache: unknown; + sessionId: unknown; + visitId: unknown; + }; + }; + }; + /** @description Bad request. */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "bad-request", + * "message": "Bad request.", + * "status": 400 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + /** @description Forbidden. */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "forbidden", + * "message": "Forbidden.", + * "status": 403 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + /** @description Server error. */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "server-error", + * "message": "Server error.", + * "status": 500 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + }; + }; + createShare: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + 'application/json': { + /** Format: uuid */ + entityId: string; + name: string; + parameters: { + [key: string]: unknown; + }; + shareType: number; + slug?: string; + }; + }; + }; + responses: { + /** @description The operation completed successfully. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + /** Format: date-time */ + createdAt: string; + entityId: string; + id: string; + name: string; + parameters: + | string + | number + | false + | true + | { + [key: string]: + | string + | number + | false + | true + | unknown + | { + length: number; + }; + } + | { + length: number; + }; + shareType: number; + slug: string; + /** Format: date-time */ + updatedAt: string; + }; + }; + }; + /** @description Bad request. */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "bad-request", + * "message": "Bad request.", + * "status": 400 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + /** @description Unauthorized. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "unauthorized", + * "message": "Unauthorized.", + * "status": 401 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + }; + }; + getShareBySlug: { + parameters: { + query?: never; + header?: never; + path: { + slug: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description The operation completed successfully. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + [key: string]: unknown; + }; + }; + }; + /** @description Not found. */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "not-found", + * "message": "Not found.", + * "status": 404 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + }; + }; + getShare: { + parameters: { + query?: never; + header?: never; + path: { + shareId: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description The operation completed successfully. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + /** Format: date-time */ + createdAt: string; + entityId: string; + id: string; + name: string; + parameters: + | string + | number + | false + | true + | { + [key: string]: + | string + | number + | false + | true + | unknown + | { + length: number; + }; + } + | { + length: number; + }; + shareType: number; + slug: string; + /** Format: date-time */ + updatedAt: string; + }; + }; + }; + /** @description Unauthorized. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "unauthorized", + * "message": "Unauthorized.", + * "status": 401 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + }; + }; + updateShare: { + parameters: { + query?: never; + header?: never; + path: { + shareId: string; + }; + cookie?: never; + }; + requestBody: { + content: { + 'application/json': { + name: string; + parameters: { + [key: string]: unknown; + }; + slug: string; + }; + }; + }; + responses: { + /** @description The operation completed successfully. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + /** Format: date-time */ + createdAt: string; + entityId: string; + id: string; + name: string; + parameters: + | string + | number + | false + | true + | { + [key: string]: + | string + | number + | false + | true + | unknown + | { + length: number; + }; + } + | { + length: number; + }; + shareType: number; + slug: string; + /** Format: date-time */ + updatedAt: string; + }; + }; + }; + /** @description Bad request. */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "bad-request", + * "message": "Bad request.", + * "status": 400 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + /** @description Unauthorized. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "unauthorized", + * "message": "Unauthorized.", + * "status": 401 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + /** @description Not found. */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "not-found", + * "message": "Not found.", + * "status": 404 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + }; + }; + deleteShare: { + parameters: { + query?: never; + header?: never; + path: { + shareId: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description The operation completed successfully. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + /** @constant */ + ok: true; + }; + }; + }; + /** @description Unauthorized. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "unauthorized", + * "message": "Unauthorized.", + * "status": 401 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + }; + }; + getTeams: { + parameters: { + query?: { + maxResults?: number; + orderBy?: string; + page?: number; + pageSize?: number; + sortDescending?: 'true' | 'false'; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description The operation completed successfully. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + count: number; + data: { + accessCode: string; + /** Format: date-time */ + createdAt: string; + /** Format: date-time */ + deletedAt: string; + id: string; + logoUrl: string; + name: string; + twoFactorRequired: boolean; + /** Format: date-time */ + updatedAt: string; + }[]; + isCapped?: boolean; + orderBy?: string; + page: number; + pageSize: number; + search?: string; + sortDescending?: boolean; + }; + }; + }; + /** @description Bad request. */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "bad-request", + * "message": "Bad request.", + * "status": 400 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + /** @description Unauthorized. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "unauthorized", + * "message": "Unauthorized.", + * "status": 401 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + }; + }; + createTeam: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + 'application/json': { + name: string; + /** Format: uuid */ + ownerId?: string; + }; + }; + }; + responses: { + /** @description The operation completed successfully. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': [ + { + accessCode: string; + /** Format: date-time */ + createdAt: string; + /** Format: date-time */ + deletedAt: string; + id: string; + logoUrl: string; + name: string; + twoFactorRequired: boolean; + /** Format: date-time */ + updatedAt: string; + }, + { + /** Format: date-time */ + createdAt: string; + id: string; + role: string; + teamId: string; + /** Format: date-time */ + updatedAt: string; + userId: string; + }, + ]; + }; + }; + /** @description Bad request. */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "bad-request", + * "message": "Bad request.", + * "status": 400 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + /** @description Unauthorized. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "unauthorized", + * "message": "Unauthorized.", + * "status": 401 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + }; + }; + getTeam: { + parameters: { + query?: never; + header?: never; + path: { + teamId: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description The operation completed successfully. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + accessCode: string; + /** Format: date-time */ + createdAt: string; + /** Format: date-time */ + deletedAt: string; + id: string; + logoUrl: string; + name: string; + twoFactorRequired: boolean; + /** Format: date-time */ + updatedAt: string; + }; + }; + }; + /** @description Unauthorized. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "unauthorized", + * "message": "Unauthorized.", + * "status": 401 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + /** @description Not found. */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "not-found", + * "message": "Not found.", + * "status": 404 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + }; + }; + updateTeam: { + parameters: { + query?: never; + header?: never; + path: { + teamId: string; + }; + cookie?: never; + }; + requestBody: { + content: { + 'application/json': { + accessCode?: string; + name?: string; + }; + }; + }; + responses: { + /** @description The operation completed successfully. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + accessCode: string; + /** Format: date-time */ + createdAt: string; + /** Format: date-time */ + deletedAt: string; + id: string; + logoUrl: string; + name: string; + twoFactorRequired: boolean; + /** Format: date-time */ + updatedAt: string; + }; + }; + }; + /** @description Bad request. */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "bad-request", + * "message": "Bad request.", + * "status": 400 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + /** @description Unauthorized. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "unauthorized", + * "message": "Unauthorized.", + * "status": 401 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + }; + }; + deleteTeam: { + parameters: { + query?: never; + header?: never; + path: { + teamId: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description The operation completed successfully. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + /** @constant */ + ok: true; + }; + }; + }; + /** @description Unauthorized. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "unauthorized", + * "message": "Unauthorized.", + * "status": 401 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + }; + }; + getTeamBoards: { + parameters: { + query?: { + maxResults?: number; + orderBy?: string; + page?: number; + pageSize?: number; + search?: string; + sortDescending?: 'true' | 'false'; + }; + header?: never; + path: { + teamId: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description The operation completed successfully. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + count: number; + data: { + /** Format: date-time */ + createdAt: string; + description: string; + id: string; + name: string; + parameters: + | string + | number + | false + | true + | { + [key: string]: + | string + | number + | false + | true + | unknown + | { + length: number; + }; + } + | { + length: number; + }; + teamId: string; + type: string; + /** Format: date-time */ + updatedAt: string; + userId: string; + }[]; + isCapped?: boolean; + orderBy?: string; + page: number; + pageSize: number; + search?: string; + sortDescending?: boolean; + }; + }; + }; + /** @description Bad request. */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "bad-request", + * "message": "Bad request.", + * "status": 400 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + /** @description Unauthorized. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "unauthorized", + * "message": "Unauthorized.", + * "status": 401 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + }; + }; + getTeamLinks: { + parameters: { + query?: { + maxResults?: number; + orderBy?: string; + page?: number; + pageSize?: number; + search?: string; + sortDescending?: 'true' | 'false'; + }; + header?: never; + path: { + teamId: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description The operation completed successfully. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + count: number; + data: { + /** Format: date-time */ + createdAt: string; + /** Format: date-time */ + deletedAt: string; + id: string; + name: string; + slug: string; + teamId: string; + /** Format: date-time */ + updatedAt: string; + url: string; + userId: string; + }[]; + isCapped?: boolean; + orderBy?: string; + page: number; + pageSize: number; + search?: string; + sortDescending?: boolean; + }; + }; + }; + /** @description Bad request. */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "bad-request", + * "message": "Bad request.", + * "status": 400 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + /** @description Unauthorized. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "unauthorized", + * "message": "Unauthorized.", + * "status": 401 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + }; + }; + getTeamPixels: { + parameters: { + query?: { + maxResults?: number; + orderBy?: string; + page?: number; + pageSize?: number; + search?: string; + sortDescending?: 'true' | 'false'; + }; + header?: never; + path: { + teamId: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description The operation completed successfully. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + count: number; + data: { + /** Format: date-time */ + createdAt: string; + /** Format: date-time */ + deletedAt: string; + id: string; + name: string; + slug: string; + teamId: string; + /** Format: date-time */ + updatedAt: string; + userId: string; + }[]; + isCapped?: boolean; + orderBy?: string; + page: number; + pageSize: number; + search?: string; + sortDescending?: boolean; + }; + }; + }; + /** @description Bad request. */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "bad-request", + * "message": "Bad request.", + * "status": 400 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + /** @description Unauthorized. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "unauthorized", + * "message": "Unauthorized.", + * "status": 401 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + }; + }; + getTeamUsers: { + parameters: { + query?: { + maxResults?: number; + page?: number; + pageSize?: number; + search?: string; + }; + header?: never; + path: { + teamId: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description The operation completed successfully. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + count: number; + data: ({ + /** Format: date-time */ + createdAt: string; + id: string; + role: string; + teamId: string; + /** Format: date-time */ + updatedAt: string; + userId: string; + } & { + user?: { + id: string; + username: string; + }; + })[]; + isCapped?: boolean; + orderBy?: string; + page: number; + pageSize: number; + search?: string; + sortDescending?: boolean; + }; + }; + }; + /** @description Bad request. */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "bad-request", + * "message": "Bad request.", + * "status": 400 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + /** @description Unauthorized. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "unauthorized", + * "message": "Unauthorized.", + * "status": 401 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + }; + }; + createTeamUser: { + parameters: { + query?: never; + header?: never; + path: { + teamId: string; + }; + cookie?: never; + }; + requestBody: { + content: { + 'application/json': { + /** @enum {string} */ + role: 'team-member' | 'team-view-only' | 'team-manager'; + /** Format: uuid */ + userId: string; + }; + }; + }; + responses: { + /** @description The operation completed successfully. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + /** Format: date-time */ + createdAt: string; + id: string; + role: string; + teamId: string; + /** Format: date-time */ + updatedAt: string; + userId: string; + }; + }; + }; + /** @description Bad request. */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "bad-request", + * "message": "Bad request.", + * "status": 400 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + /** @description Unauthorized. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "unauthorized", + * "message": "Unauthorized.", + * "status": 401 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + }; + }; + getTeamUser: { + parameters: { + query?: never; + header?: never; + path: { + teamId: string; + userId: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description The operation completed successfully. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + /** Format: date-time */ + createdAt: string; + id: string; + role: string; + teamId: string; + /** Format: date-time */ + updatedAt: string; + userId: string; + }; + }; + }; + /** @description Unauthorized. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "unauthorized", + * "message": "Unauthorized.", + * "status": 401 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + }; + }; + updateTeamUser: { + parameters: { + query?: never; + header?: never; + path: { + teamId: string; + userId: string; + }; + cookie?: never; + }; + requestBody: { + content: { + 'application/json': { + /** @enum {string} */ + role: 'team-member' | 'team-view-only' | 'team-manager'; + }; + }; + }; + responses: { + /** @description The operation completed successfully. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + /** Format: date-time */ + createdAt: string; + id: string; + role: string; + teamId: string; + /** Format: date-time */ + updatedAt: string; + userId: string; + }; + }; + }; + /** @description Bad request. */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "bad-request", + * "message": "Bad request.", + * "status": 400 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + /** @description Unauthorized. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "unauthorized", + * "message": "Unauthorized.", + * "status": 401 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + }; + }; + deleteTeamUser: { + parameters: { + query?: never; + header?: never; + path: { + teamId: string; + userId: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description The operation completed successfully. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + /** @constant */ + ok: true; + }; + }; + }; + /** @description Bad request. */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "bad-request", + * "message": "Bad request.", + * "status": 400 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + /** @description Unauthorized. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "unauthorized", + * "message": "Unauthorized.", + * "status": 401 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + }; + }; + getTeamWebsites: { + parameters: { + query?: { + maxResults?: number; + orderBy?: string; + page?: number; + pageSize?: number; + search?: string; + sortDescending?: 'true' | 'false'; + }; + header?: never; + path: { + teamId: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description The operation completed successfully. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + count: number; + data: ({ + /** Format: date-time */ + createdAt: string; + createdBy: string; + /** Format: date-time */ + deletedAt: string; + domain: string; + id: string; + name: string; + recorderEnabled: boolean; + replayConfig: + | string + | number + | false + | true + | { + [key: string]: + | string + | number + | false + | true + | unknown + | { + length: number; + }; + } + | { + length: number; + }; + /** Format: date-time */ + resetAt: string; + teamId: string; + /** Format: date-time */ + updatedAt: string; + userId: string; + } & { + createUser?: { + id: string; + username: string; + }; + shareId: string; + team?: { + members: { + role: string; + userId: string; + }[]; + }; + user?: { + id: string; + username: string; + }; + })[]; + isCapped?: boolean; + orderBy?: string; + page: number; + pageSize: number; + search?: string; + sortDescending?: boolean; + }; + }; + }; + /** @description Bad request. */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "bad-request", + * "message": "Bad request.", + * "status": 400 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + /** @description Unauthorized. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "unauthorized", + * "message": "Unauthorized.", + * "status": 401 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + }; + }; + joinTeam: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + 'application/json': { + accessCode: string; + }; + }; + }; + responses: { + /** @description The operation completed successfully. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + /** Format: date-time */ + createdAt: string; + id: string; + role: string; + teamId: string; + /** Format: date-time */ + updatedAt: string; + userId: string; + }; + }; + }; + /** @description Bad request. */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "bad-request", + * "message": "Bad request.", + * "status": 400 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + /** @description Unauthorized. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "unauthorized", + * "message": "Unauthorized.", + * "status": 401 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + /** @description Not found. */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "not-found", + * "message": "Not found.", + * "status": 404 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + }; + }; + createUser: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + 'application/json': { + /** Format: uuid */ + id?: string; + password: string; + /** @enum {string} */ + role: 'admin' | 'user' | 'view-only'; + username: string; + }; + }; + }; + responses: { + /** @description The operation completed successfully. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + id: string; + role: string; + username: string; + }; + }; + }; + /** @description Bad request. */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "bad-request", + * "message": "Bad request.", + * "status": 400 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + /** @description Unauthorized. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "unauthorized", + * "message": "Unauthorized.", + * "status": 401 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + }; + }; + getUser: { + parameters: { + query?: never; + header?: never; + path: { + userId: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description The operation completed successfully. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + _count: { + apiKeys: number; + boards: number; + createdBy: number; + links: number; + oauthAuthorizationCodes: number; + oauthRefreshTokens: number; + pixels: number; + reports: number; + teams: number; + twoFactorAuth: number; + twoFactorBackupCodes: number; + twoFactorOtpUseds: number; + twoFactorRateLimit: number; + websites: number; + }; + apiKeys: { + /** Format: date-time */ + createdAt: string; + id: string; + keyHash: string; + keyPrefix: string; + /** Format: date-time */ + lastUsedAt: string; + name: string; + userId: string; + }[]; + boards: { + /** Format: date-time */ + createdAt: string; + description: string; + id: string; + name: string; + parameters: + | string + | number + | false + | true + | { + [key: string]: + | string + | number + | false + | true + | unknown + | { + length: number; + }; + } + | { + length: number; + }; + teamId: string; + type: string; + /** Format: date-time */ + updatedAt: string; + userId: string; + }[]; + /** Format: date-time */ + createdAt: string; + createdBy: { + /** Format: date-time */ + createdAt: string; + createdBy: string; + /** Format: date-time */ + deletedAt: string; + domain: string; + id: string; + name: string; + recorderEnabled: boolean; + replayConfig: + | string + | number + | false + | true + | { + [key: string]: + | string + | number + | false + | true + | unknown + | { + length: number; + }; + } + | { + length: number; + }; + /** Format: date-time */ + resetAt: string; + teamId: string; + /** Format: date-time */ + updatedAt: string; + userId: string; + }[]; + id: string; + links: { + /** Format: date-time */ + createdAt: string; + /** Format: date-time */ + deletedAt: string; + id: string; + name: string; + slug: string; + teamId: string; + /** Format: date-time */ + updatedAt: string; + url: string; + userId: string; + }[]; + oauthAuthorizationCodes: { + clientId: string; + codeChallenge: string; + codeChallengeMethod: string; + codeHash: string; + /** Format: date-time */ + createdAt: string; + /** Format: date-time */ + expiresAt: string; + id: string; + redirectUri: string; + resource: string; + scope: string; + /** Format: date-time */ + usedAt: string; + userId: string; + }[]; + oauthRefreshTokens: { + clientId: string; + /** Format: date-time */ + createdAt: string; + /** Format: date-time */ + expiresAt: string; + id: string; + /** Format: date-time */ + lastUsedAt: string; + resource: string; + /** Format: date-time */ + revokedAt: string; + scope: string; + tokenHash: string; + userId: string; + }[]; + password: string; + pixels: { + /** Format: date-time */ + createdAt: string; + /** Format: date-time */ + deletedAt: string; + id: string; + name: string; + slug: string; + teamId: string; + /** Format: date-time */ + updatedAt: string; + userId: string; + }[]; + reports: { + /** Format: date-time */ + createdAt: string; + description: string; + id: string; + name: string; + parameters: + | string + | number + | false + | true + | { + [key: string]: + | string + | number + | false + | true + | unknown + | { + length: number; + }; + } + | { + length: number; + }; + type: string; + /** Format: date-time */ + updatedAt: string; + userId: string; + websiteId: string; + }[]; + role: string; + teams: { + /** Format: date-time */ + createdAt: string; + id: string; + role: string; + teamId: string; + /** Format: date-time */ + updatedAt: string; + userId: string; + }[]; + twoFactorAuth: { + /** Format: date-time */ + createdAt: string; + id: string; + isEnabled: boolean; + secret: string; + /** Format: date-time */ + updatedAt: string; + userId: string; + }; + twoFactorBackupCodes: { + codeHash: string; + /** Format: date-time */ + createdAt: string; + id: string; + used: boolean; + userId: string; + }[]; + twoFactorOtpUseds: { + /** Format: date-time */ + expiresAt: string; + id: string; + otp: string; + userId: string; + }[]; + twoFactorRateLimit: { + attempts: number; + id: string; + /** Format: date-time */ + lockedUntil: string; + /** Format: date-time */ + updatedAt: string; + userId: string; + }; + twoFactorRequired: boolean; + username: string; + websites: { + /** Format: date-time */ + createdAt: string; + createdBy: string; + /** Format: date-time */ + deletedAt: string; + domain: string; + id: string; + name: string; + recorderEnabled: boolean; + replayConfig: + | string + | number + | false + | true + | { + [key: string]: + | string + | number + | false + | true + | unknown + | { + length: number; + }; + } + | { + length: number; + }; + /** Format: date-time */ + resetAt: string; + teamId: string; + /** Format: date-time */ + updatedAt: string; + userId: string; + }[]; + }; + }; + }; + /** @description Unauthorized. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "unauthorized", + * "message": "Unauthorized.", + * "status": 401 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + }; + }; + updateUser: { + parameters: { + query?: never; + header?: never; + path: { + userId: string; + }; + cookie?: never; + }; + requestBody: { + content: { + 'application/json': { + password?: string; + /** @enum {string} */ + role?: 'admin' | 'user' | 'view-only'; + username?: string; + }; + }; + }; + responses: { + /** @description The operation completed successfully. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + /** Format: date-time */ + createdAt: string; + id: string; + role: string; + username: string; + }; + }; + }; + /** @description Bad request. */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "bad-request", + * "message": "Bad request.", + * "status": 400 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + /** @description Unauthorized. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "unauthorized", + * "message": "Unauthorized.", + * "status": 401 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + /** @description Not found. */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "not-found", + * "message": "Not found.", + * "status": 404 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + }; + }; + deleteUser: { + parameters: { + query?: never; + header?: never; + path: { + userId: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description The operation completed successfully. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + /** @constant */ + ok: true; + }; + }; + }; + /** @description Bad request. */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "bad-request", + * "message": "Bad request.", + * "status": 400 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + /** @description Unauthorized. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "unauthorized", + * "message": "Unauthorized.", + * "status": 401 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + }; + }; + getUserTeams: { + parameters: { + query?: { + maxResults?: number; + orderBy?: string; + page?: number; + pageSize?: number; + sortDescending?: 'true' | 'false'; + }; + header?: never; + path: { + userId: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description The operation completed successfully. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + count: number; + data: { + accessCode: string; + /** Format: date-time */ + createdAt: string; + /** Format: date-time */ + deletedAt: string; + id: string; + logoUrl: string; + name: string; + twoFactorRequired: boolean; + /** Format: date-time */ + updatedAt: string; + }[]; + isCapped?: boolean; + orderBy?: string; + page: number; + pageSize: number; + search?: string; + sortDescending?: boolean; + }; + }; + }; + /** @description Bad request. */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "bad-request", + * "message": "Bad request.", + * "status": 400 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + /** @description Unauthorized. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "unauthorized", + * "message": "Unauthorized.", + * "status": 401 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + }; + }; + getUserWebsites: { + parameters: { + query?: { + includeTeams?: string; + maxResults?: number; + orderBy?: string; + page?: number; + pageSize?: number; + search?: string; + sortDescending?: 'true' | 'false'; + }; + header?: never; + path: { + userId: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description The operation completed successfully. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + count: number; + data: ({ + /** Format: date-time */ + createdAt: string; + createdBy: string; + /** Format: date-time */ + deletedAt: string; + domain: string; + id: string; + name: string; + recorderEnabled: boolean; + replayConfig: + | string + | number + | false + | true + | { + [key: string]: + | string + | number + | false + | true + | unknown + | { + length: number; + }; + } + | { + length: number; + }; + /** Format: date-time */ + resetAt: string; + teamId: string; + /** Format: date-time */ + updatedAt: string; + userId: string; + } & { + createUser?: { + id: string; + username: string; + }; + shareId: string; + team?: { + members: { + role: string; + userId: string; + }[]; + }; + user?: { + id: string; + username: string; + }; + })[]; + isCapped?: boolean; + orderBy?: string; + page: number; + pageSize: number; + search?: string; + sortDescending?: boolean; + }; + }; + }; + /** @description Bad request. */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "bad-request", + * "message": "Bad request.", + * "status": 400 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + /** @description Unauthorized. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "unauthorized", + * "message": "Unauthorized.", + * "status": 401 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + }; + }; + listWebsites: { + parameters: { + query?: { + /** @description When present, include websites accessible through owned or managed teams. */ + includeTeams?: string; + maxResults?: number; + orderBy?: string; + page?: number; + pageSize?: number; + search?: string; + sortDescending?: 'true' | 'false'; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description A page of websites. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['WebsitePage']; + }; + }; + /** @description Bad request. */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "bad-request", + * "message": "Bad request.", + * "status": 400 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + /** @description Unauthorized. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "unauthorized", + * "message": "Unauthorized.", + * "status": 401 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + }; + }; + createWebsite: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + 'application/json': components['schemas']['CreateWebsiteRequest']; + }; + }; + responses: { + /** @description Website created. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['Website']; + }; + }; + /** @description Bad request. */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "bad-request", + * "message": "Bad request.", + * "status": 400 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + /** @description Unauthorized. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "unauthorized", + * "message": "Unauthorized.", + * "status": 401 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + }; + }; + getWebsite: { + parameters: { + query?: never; + header?: never; + path: { + /** @description Website ID. */ + websiteId: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Website details, or null if it does not exist. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['Website'] | null; + }; + }; + /** @description Unauthorized. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "unauthorized", + * "message": "Unauthorized.", + * "status": 401 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + }; + }; + updateWebsite: { + parameters: { + query?: never; + header?: never; + path: { + /** @description Website ID. */ + websiteId: string; + }; + cookie?: never; + }; + requestBody: { + content: { + 'application/json': components['schemas']['UpdateWebsiteRequest']; + }; + }; + responses: { + /** @description Website updated. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['Website']; + }; + }; + /** @description Bad request. */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "bad-request", + * "message": "Bad request.", + * "status": 400 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + /** @description Unauthorized. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "unauthorized", + * "message": "Unauthorized.", + * "status": 401 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + /** @description Server error. */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "server-error", + * "message": "Server error.", + * "status": 500 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + }; + }; + deleteWebsite: { + parameters: { + query?: never; + header?: never; + path: { + /** @description Website ID. */ + websiteId: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Website deleted. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['Ok']; + }; + }; + /** @description Unauthorized. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "unauthorized", + * "message": "Unauthorized.", + * "status": 401 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + }; + }; + getWebsiteActive: { + parameters: { + query?: never; + header?: never; + path: { + /** @description Website ID. */ + websiteId: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Active visitor count. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ActiveVisitors']; + }; + }; + /** @description Unauthorized. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "unauthorized", + * "message": "Unauthorized.", + * "status": 401 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + }; + }; + getWebsiteAnnotations: { + parameters: { + query?: { + endAt?: number; + maxResults?: number; + page?: number; + pageSize?: number; + search?: string; + startAt?: number; + }; + header?: never; + path: { + websiteId: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description The operation completed successfully. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + count: number; + data: { + allDay: boolean; + /** Format: date-time */ + createdAt: string; + /** Format: date-time */ + date: string; + id: string; + note: string; + /** Format: date-time */ + updatedAt: string; + userId: string; + websiteId: string; + }[]; + isCapped?: boolean; + orderBy?: string; + page: number; + pageSize: number; + search?: string; + sortDescending?: boolean; + }; + }; + }; + /** @description Bad request. */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "bad-request", + * "message": "Bad request.", + * "status": 400 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + /** @description Unauthorized. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "unauthorized", + * "message": "Unauthorized.", + * "status": 401 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + }; + }; + createWebsiteAnnotation: { + parameters: { + query?: never; + header?: never; + path: { + websiteId: string; + }; + cookie?: never; + }; + requestBody: { + content: { + 'application/json': { + allDay?: boolean; + /** Format: date-time */ + date: string; + note: string; + }; + }; + }; + responses: { + /** @description The operation completed successfully. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + allDay: boolean; + /** Format: date-time */ + createdAt: string; + /** Format: date-time */ + date: string; + id: string; + note: string; + /** Format: date-time */ + updatedAt: string; + userId: string; + websiteId: string; + }; + }; + }; + /** @description Bad request. */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "bad-request", + * "message": "Bad request.", + * "status": 400 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + /** @description Unauthorized. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "unauthorized", + * "message": "Unauthorized.", + * "status": 401 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + }; + }; + getWebsiteAnnotation: { + parameters: { + query?: never; + header?: never; + path: { + annotationId: string; + websiteId: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description The operation completed successfully. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + allDay: boolean; + /** Format: date-time */ + createdAt: string; + /** Format: date-time */ + date: string; + id: string; + note: string; + /** Format: date-time */ + updatedAt: string; + userId: string; + websiteId: string; + }; + }; + }; + /** @description Unauthorized. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "unauthorized", + * "message": "Unauthorized.", + * "status": 401 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + /** @description Not found. */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "not-found", + * "message": "Not found.", + * "status": 404 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + }; + }; + updateWebsiteAnnotation: { + parameters: { + query?: never; + header?: never; + path: { + annotationId: string; + websiteId: string; + }; + cookie?: never; + }; + requestBody: { + content: { + 'application/json': { + allDay?: boolean; + /** Format: date-time */ + date: string; + note: string; + }; + }; + }; + responses: { + /** @description The operation completed successfully. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + allDay: boolean; + /** Format: date-time */ + createdAt: string; + /** Format: date-time */ + date: string; + id: string; + note: string; + /** Format: date-time */ + updatedAt: string; + userId: string; + websiteId: string; + }; + }; + }; + /** @description Bad request. */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "bad-request", + * "message": "Bad request.", + * "status": 400 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + /** @description Unauthorized. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "unauthorized", + * "message": "Unauthorized.", + * "status": 401 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + /** @description Not found. */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "not-found", + * "message": "Not found.", + * "status": 404 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + }; + }; + deleteWebsiteAnnotation: { + parameters: { + query?: never; + header?: never; + path: { + annotationId: string; + websiteId: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description The operation completed successfully. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + /** @constant */ + ok: true; + }; + }; + }; + /** @description Unauthorized. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "unauthorized", + * "message": "Unauthorized.", + * "status": 401 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + /** @description Not found. */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "not-found", + * "message": "Not found.", + * "status": 404 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + }; + }; + getWebsiteDateRange: { + parameters: { + query?: never; + header?: never; + path: { + websiteId: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description The operation completed successfully. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + /** Format: date-time */ + endDate: string; + /** Format: date-time */ + startDate: string; + }; + }; + }; + /** @description Unauthorized. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "unauthorized", + * "message": "Unauthorized.", + * "status": 401 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + }; + }; + getEventData: { + parameters: { + query: { + browser?: string; + city?: string; + cohort?: string; + country?: string; + device?: string; + distinctId?: string; + endAt: number; + event?: string; + eventType?: number; + excludeBounce?: string; + hostname?: string; + language?: string; + match?: 'all' | 'any'; + maxResults?: number; + os?: string; + page?: number; + pageSize?: number; + path?: string; + query?: string; + referrer?: string; + region?: string; + segment?: string; + startAt: number; + tag?: string; + title?: string; + utmCampaign?: string; + utmContent?: string; + utmMedium?: string; + utmSource?: string; + utmTerm?: string; + }; + header?: never; + path: { + websiteId: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description The operation completed successfully. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + count: number; + data: { + eventId: string; + eventName: string; + eventProperties: { + createdAt: string; + dataKey: string; + dataType: number; + dateValue: string; + numberValue: number; + stringValue: string; + }[]; + websiteId: string; + }[]; + page: number; + pageSize: number; + }; + }; + }; + /** @description Bad request. */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "bad-request", + * "message": "Bad request.", + * "status": 400 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + /** @description Unauthorized. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "unauthorized", + * "message": "Unauthorized.", + * "status": 401 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + }; + }; + getEventDataPivot: { + parameters: { + query: { + browser?: string; + city?: string; + cohort?: string; + country?: string; + device?: string; + distinctId?: string; + endAt: number; + event?: string; + eventName: string; + eventType?: number; + excludeBounce?: string; + hostname?: string; + language?: string; + match?: 'all' | 'any'; + maxResults?: number; + os?: string; + page?: number; + pageSize?: number; + path?: string; + query?: string; + referrer?: string; + region?: string; + segment?: string; + startAt: number; + tag?: string; + timezone?: string; + title?: string; + unit?: string; + utmCampaign?: string; + utmContent?: string; + utmMedium?: string; + utmSource?: string; + utmTerm?: string; + }; + header?: never; + path: { + websiteId: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description The operation completed successfully. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + count: number; + data: { + createdAt: string; + eventId: string; + eventName: string; + propertyKeys: string[]; + propertyValues: string[]; + sessionId: string; + urlPath: string; + }[]; + isCapped?: boolean; + orderBy?: string; + page: number; + pageSize: number; + search?: string; + sortDescending?: boolean; + }; + }; + }; + /** @description Bad request. */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "bad-request", + * "message": "Bad request.", + * "status": 400 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + /** @description Unauthorized. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "unauthorized", + * "message": "Unauthorized.", + * "status": 401 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + }; + }; + getEventDataArraySeries: { + parameters: { + query: { + browser?: string; + city?: string; + cohort?: string; + country?: string; + device?: string; + distinctId?: string; + endAt: number; + event?: string; + eventName: string; + eventType?: number; + excludeBounce?: string; + hostname?: string; + language?: string; + match?: 'all' | 'any'; + os?: string; + path?: string; + propertyName: string; + query?: string; + referrer?: string; + region?: string; + segment?: string; + startAt: number; + tag?: string; + timezone?: string; + title?: string; + unit?: string; + utmCampaign?: string; + utmContent?: string; + utmMedium?: string; + utmSource?: string; + utmTerm?: string; + }; + header?: never; + path: { + websiteId: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description The operation completed successfully. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + t: string; + x: string; + y: number; + }[]; + }; + }; + /** @description Bad request. */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "bad-request", + * "message": "Bad request.", + * "status": 400 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + /** @description Unauthorized. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "unauthorized", + * "message": "Unauthorized.", + * "status": 401 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + }; + }; + getEventDataDateSeries: { + parameters: { + query: { + browser?: string; + city?: string; + cohort?: string; + country?: string; + device?: string; + distinctId?: string; + endAt: number; + event?: string; + eventName: string; + eventType?: number; + excludeBounce?: string; + hostname?: string; + language?: string; + match?: 'all' | 'any'; + os?: string; + path?: string; + propertyName: string; + query?: string; + referrer?: string; + region?: string; + segment?: string; + startAt: number; + tag?: string; + timezone?: string; + title?: string; + utmCampaign?: string; + utmContent?: string; + utmMedium?: string; + utmSource?: string; + utmTerm?: string; + }; + header?: never; + path: { + websiteId: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description The operation completed successfully. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + t: string; + y: number; + }[]; + }; + }; + /** @description Bad request. */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "bad-request", + * "message": "Bad request.", + * "status": 400 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + /** @description Unauthorized. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "unauthorized", + * "message": "Unauthorized.", + * "status": 401 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + }; + }; + getEventDataNumericSeries: { + parameters: { + query: { + browser?: string; + city?: string; + cohort?: string; + country?: string; + device?: string; + distinctId?: string; + endAt: number; + event?: string; + eventName: string; + eventType?: number; + excludeBounce?: string; + hostname?: string; + language?: string; + match?: 'all' | 'any'; + metric?: 'sum' | 'avg' | 'count'; + os?: string; + path?: string; + propertyName: string; + query?: string; + referrer?: string; + region?: string; + segment?: string; + startAt: number; + tag?: string; + timezone?: string; + title?: string; + unit?: string; + utmCampaign?: string; + utmContent?: string; + utmMedium?: string; + utmSource?: string; + utmTerm?: string; + }; + header?: never; + path: { + websiteId: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description The operation completed successfully. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + t: string; + y: number; + }[]; + }; + }; + /** @description Bad request. */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "bad-request", + * "message": "Bad request.", + * "status": 400 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + /** @description Unauthorized. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "unauthorized", + * "message": "Unauthorized.", + * "status": 401 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + }; + }; + getEventDataNumericStats: { + parameters: { + query: { + browser?: string; + city?: string; + cohort?: string; + country?: string; + device?: string; + distinctId?: string; + endAt: number; + event?: string; + eventName: string; + eventType?: number; + excludeBounce?: string; + hostname?: string; + language?: string; + match?: 'all' | 'any'; + os?: string; + path?: string; + propertyName: string; + query?: string; + referrer?: string; + region?: string; + segment?: string; + startAt: number; + tag?: string; + title?: string; + utmCampaign?: string; + utmContent?: string; + utmMedium?: string; + utmSource?: string; + utmTerm?: string; + }; + header?: never; + path: { + websiteId: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description The operation completed successfully. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + average: number; + max: number; + median: number; + min: number; + total: number; + }; + }; + }; + /** @description Bad request. */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "bad-request", + * "message": "Bad request.", + * "status": 400 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + /** @description Unauthorized. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "unauthorized", + * "message": "Unauthorized.", + * "status": 401 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + }; + }; + getEventDataPropertySeries: { + parameters: { + query: { + browser?: string; + city?: string; + cohort?: string; + country?: string; + device?: string; + distinctId?: string; + endAt: number; + event?: string; + eventName: string; + eventType?: number; + excludeBounce?: string; + hostname?: string; + language?: string; + match?: 'all' | 'any'; + os?: string; + path?: string; + propertyName: string; + query?: string; + referrer?: string; + region?: string; + segment?: string; + startAt: number; + tag?: string; + timezone?: string; + title?: string; + unit?: string; + utmCampaign?: string; + utmContent?: string; + utmMedium?: string; + utmSource?: string; + utmTerm?: string; + }; + header?: never; + path: { + websiteId: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description The operation completed successfully. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + t: string; + x: string; + y: number; + }[]; + }; + }; + /** @description Bad request. */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "bad-request", + * "message": "Bad request.", + * "status": 400 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + /** @description Unauthorized. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "unauthorized", + * "message": "Unauthorized.", + * "status": 401 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + }; + }; + getEventDataById: { + parameters: { + query?: never; + header?: never; + path: { + eventId: string; + websiteId: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description The operation completed successfully. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + /** Format: date-time */ + createdAt: string; + dataKey: string; + dataType: number; + /** Format: date-time */ + dateValue: string; + id: string; + numberValue: { + d: number[]; + e: number; + s: number; + }; + stringValue: string; + websiteEventId: string; + websiteId: string; + }[]; + }; + }; + /** @description Unauthorized. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "unauthorized", + * "message": "Unauthorized.", + * "status": 401 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + }; + }; + getEventDataEvents: { + parameters: { + query: { + browser?: string; + city?: string; + cohort?: string; + country?: string; + device?: string; + distinctId?: string; + endAt: number; + event?: string; + eventType?: number; + excludeBounce?: string; + hostname?: string; + language?: string; + match?: 'all' | 'any'; + os?: string; + path?: string; + query?: string; + referrer?: string; + region?: string; + segment?: string; + startAt: number; + tag?: string; + title?: string; + utmCampaign?: string; + utmContent?: string; + utmMedium?: string; + utmSource?: string; + utmTerm?: string; + }; + header?: never; + path: { + websiteId: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description The operation completed successfully. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + dataType: number; + eventName?: string; + propertyName: string; + propertyValue?: string; + total: number; + }[]; + }; + }; + /** @description Bad request. */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "bad-request", + * "message": "Bad request.", + * "status": 400 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + /** @description Unauthorized. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "unauthorized", + * "message": "Unauthorized.", + * "status": 401 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + }; + }; + getEventDataFields: { + parameters: { + query: { + browser?: string; + city?: string; + cohort?: string; + country?: string; + device?: string; + distinctId?: string; + endAt: number; + event?: string; + eventName?: string; + eventType?: number; + excludeBounce?: string; + hostname?: string; + language?: string; + match?: 'all' | 'any'; + os?: string; + path?: string; + query?: string; + referrer?: string; + region?: string; + segment?: string; + startAt: number; + tag?: string; + title?: string; + utmCampaign?: string; + utmContent?: string; + utmMedium?: string; + utmSource?: string; + utmTerm?: string; + }; + header?: never; + path: { + websiteId: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description The operation completed successfully. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + dataType: number; + propertyName: string; + total: number; + }[]; + }; + }; + /** @description Bad request. */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "bad-request", + * "message": "Bad request.", + * "status": 400 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + /** @description Unauthorized. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "unauthorized", + * "message": "Unauthorized.", + * "status": 401 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + }; + }; + getEventDataProperties: { + parameters: { + query: { + browser?: string; + city?: string; + cohort?: string; + country?: string; + device?: string; + distinctId?: string; + endAt: number; + event?: string; + eventType?: number; + excludeBounce?: string; + hostname?: string; + language?: string; + match?: 'all' | 'any'; + os?: string; + path?: string; + query?: string; + referrer?: string; + region?: string; + segment?: string; + startAt: number; + tag?: string; + title?: string; + utmCampaign?: string; + utmContent?: string; + utmMedium?: string; + utmSource?: string; + utmTerm?: string; + }; + header?: never; + path: { + websiteId: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description The operation completed successfully. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + dataType: number; + eventName: string; + propertyName: string; + total: number; + }[]; + }; + }; + /** @description Bad request. */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "bad-request", + * "message": "Bad request.", + * "status": 400 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + /** @description Unauthorized. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "unauthorized", + * "message": "Unauthorized.", + * "status": 401 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + }; + }; + getEventDataStats: { + parameters: { + query: { + browser?: string; + city?: string; + cohort?: string; + country?: string; + device?: string; + distinctId?: string; + endAt: number; + event?: string; + eventType?: number; + excludeBounce?: string; + hostname?: string; + language?: string; + match?: 'all' | 'any'; + os?: string; + path?: string; + query?: string; + referrer?: string; + region?: string; + segment?: string; + startAt: number; + tag?: string; + title?: string; + utmCampaign?: string; + utmContent?: string; + utmMedium?: string; + utmSource?: string; + utmTerm?: string; + }; + header?: never; + path: { + websiteId: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description The operation completed successfully. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + events: number; + properties: number; + records: number; + }; + }; + }; + /** @description Bad request. */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "bad-request", + * "message": "Bad request.", + * "status": 400 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + /** @description Unauthorized. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "unauthorized", + * "message": "Unauthorized.", + * "status": 401 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + }; + }; + getEventDataValues: { + parameters: { + query: { + browser?: string; + city?: string; + cohort?: string; + country?: string; + dataType?: number; + device?: string; + distinctId?: string; + endAt: number; + event?: string; + eventName?: string; + eventType?: number; + excludeBounce?: string; + hostname?: string; + language?: string; + match?: 'all' | 'any'; + os?: string; + path?: string; + propertyName: string; + query?: string; + referrer?: string; + region?: string; + segment?: string; + startAt: number; + tag?: string; + title?: string; + utmCampaign?: string; + utmContent?: string; + utmMedium?: string; + utmSource?: string; + utmTerm?: string; + }; + header?: never; + path: { + websiteId: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description The operation completed successfully. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + total: number; + value: string; + }[]; + }; + }; + /** @description Bad request. */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "bad-request", + * "message": "Bad request.", + * "status": 400 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + /** @description Unauthorized. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "unauthorized", + * "message": "Unauthorized.", + * "status": 401 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + }; + }; + getWebsiteEvents: { + parameters: { + query?: { + browser?: string; + city?: string; + cohort?: string; + compare?: 'prev' | 'yoy'; + country?: string; + device?: string; + distinctId?: string; + endAt?: number; + endDate?: string; + event?: string; + eventType?: number; + excludeBounce?: string; + hostname?: string; + language?: string; + match?: 'all' | 'any'; + maxResults?: number; + os?: string; + page?: number; + pageSize?: number; + path?: string; + query?: string; + referrer?: string; + region?: string; + search?: string; + segment?: string; + startAt?: number; + startDate?: string; + tag?: string; + timezone?: string; + title?: string; + unit?: string; + utmCampaign?: string; + utmContent?: string; + utmMedium?: string; + utmSource?: string; + utmTerm?: string; + }; + header?: never; + path: { + /** @description Website ID. */ + websiteId: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description A page of events. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['WebsiteEventPage']; + }; + }; + /** @description Bad request. */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "bad-request", + * "message": "Bad request.", + * "status": 400 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + /** @description Unauthorized. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "unauthorized", + * "message": "Unauthorized.", + * "status": 401 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + }; + }; + getWebsiteEventSeries: { + parameters: { + query: { + browser?: string; + city?: string; + cohort?: string; + country?: string; + device?: string; + distinctId?: string; + endAt: number; + event?: string; + eventType?: number; + excludeBounce?: string; + hostname?: string; + language?: string; + limit?: number; + match?: 'all' | 'any'; + os?: string; + path?: string; + query?: string; + referrer?: string; + region?: string; + segment?: string; + startAt: number; + tag?: string; + timezone: string; + title?: string; + unit?: string; + utmCampaign?: string; + utmContent?: string; + utmMedium?: string; + utmSource?: string; + utmTerm?: string; + }; + header?: never; + path: { + websiteId: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description The operation completed successfully. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + t: string; + x: string; + y: number; + }[]; + }; + }; + /** @description Bad request. */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "bad-request", + * "message": "Bad request.", + * "status": 400 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + /** @description Unauthorized. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "unauthorized", + * "message": "Unauthorized.", + * "status": 401 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + }; + }; + getWebsiteEventStats: { + parameters: { + query?: { + browser?: string; + city?: string; + cohort?: string; + compare?: 'prev' | 'yoy'; + country?: string; + device?: string; + distinctId?: string; + endAt?: number; + endDate?: string; + event?: string; + eventType?: number; + excludeBounce?: string; + hostname?: string; + language?: string; + match?: 'all' | 'any'; + os?: string; + path?: string; + query?: string; + referrer?: string; + region?: string; + segment?: string; + startAt?: number; + startDate?: string; + tag?: string; + timezone?: string; + title?: string; + unit?: string; + utmCampaign?: string; + utmContent?: string; + utmMedium?: string; + utmSource?: string; + utmTerm?: string; + }; + header?: never; + path: { + websiteId: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description The operation completed successfully. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + data: { + comparison: { + events: number; + uniqueEvents: number; + visitors: number; + visits: number; + }[]; + length: number; + }; + }; + }; + }; + /** @description Bad request. */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "bad-request", + * "message": "Bad request.", + * "status": 400 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + /** @description Unauthorized. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "unauthorized", + * "message": "Unauthorized.", + * "status": 401 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + }; + }; + exportWebsite: { + parameters: { + query?: { + compare?: 'prev' | 'yoy'; + endAt?: number; + endDate?: string; + maxResults?: number; + page?: number; + pageSize?: number; + startAt?: number; + startDate?: string; + timezone?: string; + unit?: string; + }; + header?: never; + path: { + websiteId: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description The operation completed successfully. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + zip: string; + }; + }; + }; + /** @description Bad request. */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "bad-request", + * "message": "Bad request.", + * "status": 400 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + /** @description Unauthorized. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "unauthorized", + * "message": "Unauthorized.", + * "status": 401 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + /** @description Successful response. The response shape is inferred as free-form because the handler does not expose a reusable response schema. */ + 'function toString() { [native code] }': { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': unknown; + }; + }; + }; + }; + getWebsiteMetrics: { + parameters: { + query: { + browser?: string; + city?: string; + cohort?: string; + compare?: 'prev' | 'yoy'; + country?: string; + device?: string; + distinctId?: string; + endAt?: number; + endDate?: string; + event?: string; + eventType?: number; + excludeBounce?: string; + hostname?: string; + language?: string; + limit?: number; + match?: 'all' | 'any'; + offset?: number; + os?: string; + path?: string; + query?: string; + referrer?: string; + region?: string; + search?: string; + segment?: string; + startAt?: number; + startDate?: string; + tag?: string; + timezone?: string; + title?: string; + /** @description Dimension to rank: path, entry, exit, title, query, hostname, referrer, domain, channel, event, tag, browser, os, device, screen, language, country, region, city, distinctId, utmSource, utmMedium, utmCampaign, utmContent, utmTerm. */ + type: string; + unit?: string; + utmCampaign?: string; + utmContent?: string; + utmMedium?: string; + utmSource?: string; + utmTerm?: string; + }; + header?: never; + path: { + /** @description Website ID. */ + websiteId: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Ranked rows. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['MetricRows']; + }; + }; + /** @description Bad request. */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "bad-request", + * "message": "Bad request.", + * "status": 400 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + /** @description Unauthorized. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "unauthorized", + * "message": "Unauthorized.", + * "status": 401 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + }; + }; + getWebsiteExpandedMetrics: { + parameters: { + query: { + browser?: string; + city?: string; + cohort?: string; + compare?: 'prev' | 'yoy'; + country?: string; + device?: string; + distinctId?: string; + endAt?: number; + endDate?: string; + event?: string; + eventType?: number; + excludeBounce?: string; + hostname?: string; + language?: string; + limit?: number; + match?: 'all' | 'any'; + offset?: number; + os?: string; + path?: string; + query?: string; + referrer?: string; + region?: string; + search?: string; + segment?: string; + startAt?: number; + startDate?: string; + tag?: string; + timezone?: string; + title?: string; + type: string; + unit?: string; + utmCampaign?: string; + utmContent?: string; + utmMedium?: string; + utmSource?: string; + utmTerm?: string; + }; + header?: never; + path: { + websiteId: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description The operation completed successfully. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + bounces: number; + name: string; + pageviews: number; + totaltime: number; + visitors: number; + visits: number; + }[]; + }; + }; + /** @description Bad request. */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "bad-request", + * "message": "Bad request.", + * "status": 400 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + /** @description Unauthorized. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "unauthorized", + * "message": "Unauthorized.", + * "status": 401 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + }; + }; + getWebsitePageviews: { + parameters: { + query?: { + browser?: string; + city?: string; + cohort?: string; + compare?: 'prev' | 'yoy'; + country?: string; + device?: string; + distinctId?: string; + endAt?: number; + endDate?: string; + event?: string; + eventType?: number; + excludeBounce?: string; + hostname?: string; + language?: string; + match?: 'all' | 'any'; + os?: string; + path?: string; + query?: string; + referrer?: string; + region?: string; + segment?: string; + startAt?: number; + startDate?: string; + tag?: string; + timezone?: string; + title?: string; + unit?: string; + utmCampaign?: string; + utmContent?: string; + utmMedium?: string; + utmSource?: string; + utmTerm?: string; + }; + header?: never; + path: { + /** @description Website ID. */ + websiteId: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Time series. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['WebsitePageviews']; + }; + }; + /** @description Bad request. */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "bad-request", + * "message": "Bad request.", + * "status": 400 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + /** @description Unauthorized. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "unauthorized", + * "message": "Unauthorized.", + * "status": 401 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + }; + }; + getWebsiteRecorderConfig: { + parameters: { + query?: never; + header?: never; + path: { + websiteId: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description The operation completed successfully. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': + | { + enabled: boolean; + } + | { + blockSelector: string; + enabled: boolean; + heatmapEnabled: boolean; + heatmapSampleRate: number; + maskLevel: 'strict' | 'moderate'; + maxDuration: number; + replayEnabled: boolean; + sampleRate: number; + }; + }; + }; + }; + }; + getWebsiteReplays: { + parameters: { + query?: { + browser?: string; + city?: string; + cohort?: string; + compare?: 'prev' | 'yoy'; + country?: string; + device?: string; + distinctId?: string; + endAt?: number; + endDate?: string; + event?: string; + eventType?: number; + excludeBounce?: string; + hostname?: string; + language?: string; + match?: 'all' | 'any'; + maxResults?: number; + minDuration?: number; + os?: string; + page?: number; + pageSize?: number; + path?: string; + query?: string; + referrer?: string; + region?: string; + search?: string; + segment?: string; + startAt?: number; + startDate?: string; + tag?: string; + timezone?: string; + title?: string; + unit?: string; + utmCampaign?: string; + utmContent?: string; + utmMedium?: string; + utmSource?: string; + utmTerm?: string; + }; + header?: never; + path: { + websiteId: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description The operation completed successfully. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + count: number; + data: { + browser: string; + chunkCount: number; + city: string; + country: string; + createdAt: string; + device: string; + duration: number; + endedAt: string; + eventCount: number; + id: string; + os: string; + sessionId: string; + startedAt: string; + websiteId: string; + }[]; + isCapped?: boolean; + orderBy?: string; + page: number; + pageSize: number; + search?: string; + sortDescending?: boolean; + }; + }; + }; + /** @description Bad request. */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "bad-request", + * "message": "Bad request.", + * "status": 400 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + /** @description Unauthorized. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "unauthorized", + * "message": "Unauthorized.", + * "status": 401 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + }; + }; + getWebsiteReplay: { + parameters: { + query?: never; + header?: never; + path: { + replayId: string; + websiteId: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description The operation completed successfully. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + chunkCount: number; + /** Format: date-time */ + endedAt: string; + eventCount: number; + events: unknown[]; + sessionId: string; + /** Format: date-time */ + startedAt: string; + }; + }; + }; + /** @description Unauthorized. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "unauthorized", + * "message": "Unauthorized.", + * "status": 401 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + }; + }; + getWebsiteSavedReplays: { + parameters: { + query?: { + maxResults?: number; + page?: number; + pageSize?: number; + search?: string; + }; + header?: never; + path: { + websiteId: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description The operation completed successfully. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + count: number; + data: { + /** Format: date-time */ + createdAt: string; + id: string; + name: string; + /** Format: date-time */ + updatedAt: string; + visitId: string; + websiteId: string; + }[]; + isCapped?: boolean; + orderBy?: string; + page: number; + pageSize: number; + search?: string; + sortDescending?: boolean; + }; + }; + }; + /** @description Bad request. */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "bad-request", + * "message": "Bad request.", + * "status": 400 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + /** @description Unauthorized. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "unauthorized", + * "message": "Unauthorized.", + * "status": 401 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + }; + }; + getWebsiteReplaySaved: { + parameters: { + query?: never; + header?: never; + path: { + replayId: string; + websiteId: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description The operation completed successfully. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + isSaved: boolean; + }; + }; + }; + /** @description Unauthorized. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "unauthorized", + * "message": "Unauthorized.", + * "status": 401 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + }; + }; + saveWebsiteReplay: { + parameters: { + query?: never; + header?: never; + path: { + replayId: string; + websiteId: string; + }; + cookie?: never; + }; + requestBody: { + content: { + 'application/json': { + isSaved: boolean; + name?: string; + }; + }; + }; + responses: { + /** @description The operation completed successfully. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + ok: boolean; + }; + }; + }; + /** @description Bad request. */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "bad-request", + * "message": "Bad request.", + * "status": 400 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + /** @description Unauthorized. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "unauthorized", + * "message": "Unauthorized.", + * "status": 401 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + }; + }; + getWebsiteReports: { + parameters: { + query?: { + maxResults?: number; + page?: number; + pageSize?: number; + type?: string; + }; + header?: never; + path: { + websiteId: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description The operation completed successfully. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + count: number; + data: ({ + /** Format: date-time */ + createdAt: string; + description: string; + id: string; + name: string; + parameters: + | string + | number + | false + | true + | { + [key: string]: + | string + | number + | false + | true + | unknown + | { + length: number; + }; + } + | { + length: number; + }; + type: string; + /** Format: date-time */ + updatedAt: string; + userId: string; + websiteId: string; + } & { + website?: { + domain: string; + userId: string; + }; + })[]; + isCapped?: boolean; + orderBy?: string; + page: number; + pageSize: number; + search?: string; + sortDescending?: boolean; + }; + }; + }; + /** @description Bad request. */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "bad-request", + * "message": "Bad request.", + * "status": 400 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + /** @description Unauthorized. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "unauthorized", + * "message": "Unauthorized.", + * "status": 401 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + }; + }; + resetWebsite: { + parameters: { + query?: never; + header?: never; + path: { + websiteId: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description The operation completed successfully. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + /** @constant */ + ok: true; + }; + }; + }; + /** @description Unauthorized. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "unauthorized", + * "message": "Unauthorized.", + * "status": 401 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + }; + }; + getWebsiteRevenueChart: { + parameters: { + query: { + browser?: string; + city?: string; + cohort?: string; + compare?: 'prev' | 'yoy'; + country?: string; + currency: string; + device?: string; + distinctId?: string; + endAt?: number; + endDate?: string; + event?: string; + eventType?: number; + excludeBounce?: string; + hostname?: string; + language?: string; + match?: 'all' | 'any'; + os?: string; + path?: string; + query?: string; + referrer?: string; + region?: string; + segment?: string; + startAt?: number; + startDate?: string; + tag?: string; + timezone?: string; + title?: string; + unit?: string; + utmCampaign?: string; + utmContent?: string; + utmMedium?: string; + utmSource?: string; + utmTerm?: string; + }; + header?: never; + path: { + websiteId: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description The operation completed successfully. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + chart: { + count: number; + t: string; + x: string; + y: number; + }[]; + }; + }; + }; + /** @description Bad request. */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "bad-request", + * "message": "Bad request.", + * "status": 400 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + /** @description Unauthorized. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "unauthorized", + * "message": "Unauthorized.", + * "status": 401 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + }; + }; + getWebsiteRevenueMetrics: { + parameters: { + query: { + browser?: string; + city?: string; + cohort?: string; + compare?: 'prev' | 'yoy'; + country?: string; + currency: string; + device?: string; + distinctId?: string; + endAt?: number; + endDate?: string; + event?: string; + eventType?: number; + excludeBounce?: string; + hostname?: string; + language?: string; + match?: 'all' | 'any'; + os?: string; + path?: string; + query?: string; + referrer?: string; + region?: string; + segment?: string; + startAt?: number; + startDate?: string; + tag?: string; + timezone?: string; + title?: string; + type: 'country' | 'region' | 'referrer' | 'channel'; + unit?: string; + utmCampaign?: string; + utmContent?: string; + utmMedium?: string; + utmSource?: string; + utmTerm?: string; + }; + header?: never; + path: { + websiteId: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description The operation completed successfully. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': + | { + name: string; + value: number; + }[] + | { + country: string; + name: string; + value: number; + }[]; + }; + }; + /** @description Bad request. */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "bad-request", + * "message": "Bad request.", + * "status": 400 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + /** @description Unauthorized. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "unauthorized", + * "message": "Unauthorized.", + * "status": 401 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + }; + }; + getWebsiteRevenueSessions: { + parameters: { + query: { + browser?: string; + city?: string; + cohort?: string; + compare?: 'prev' | 'yoy'; + country?: string; + currency: string; + device?: string; + distinctId?: string; + endAt?: number; + endDate?: string; + event?: string; + eventType?: number; + excludeBounce?: string; + hostname?: string; + language?: string; + match?: 'all' | 'any'; + maxResults?: number; + os?: string; + page?: number; + pageSize?: number; + path?: string; + query?: string; + referrer?: string; + region?: string; + search?: string; + segment?: string; + startAt?: number; + startDate?: string; + tag?: string; + timezone?: string; + title?: string; + unit?: string; + utmCampaign?: string; + utmContent?: string; + utmMedium?: string; + utmSource?: string; + utmTerm?: string; + }; + header?: never; + path: { + websiteId: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description The operation completed successfully. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + count: number; + data: { + browser: string; + city: string; + country: string; + createdAt: string; + device: string; + events: number; + firstAt: string; + hostname: string; + id: string; + language: string; + lastAt: string; + os: string; + region: string; + screen: string; + views: number; + visits: number; + websiteId: string; + }[]; + isCapped?: boolean; + orderBy?: string; + page: number; + pageSize: number; + search?: string; + sortDescending?: boolean; + }; + }; + }; + /** @description Bad request. */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "bad-request", + * "message": "Bad request.", + * "status": 400 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + /** @description Unauthorized. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "unauthorized", + * "message": "Unauthorized.", + * "status": 401 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + }; + }; + getWebsiteRevenueStats: { + parameters: { + query: { + browser?: string; + city?: string; + cohort?: string; + compare?: 'prev' | 'yoy'; + country?: string; + currency: string; + device?: string; + distinctId?: string; + endAt?: number; + endDate?: string; + event?: string; + eventType?: number; + excludeBounce?: string; + hostname?: string; + language?: string; + match?: 'all' | 'any'; + os?: string; + path?: string; + query?: string; + referrer?: string; + region?: string; + segment?: string; + startAt?: number; + startDate?: string; + tag?: string; + timezone?: string; + title?: string; + unit?: string; + utmCampaign?: string; + utmContent?: string; + utmMedium?: string; + utmSource?: string; + utmTerm?: string; + }; + header?: never; + path: { + websiteId: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description The operation completed successfully. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + comparison: unknown; + }; + }; + }; + /** @description Bad request. */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "bad-request", + * "message": "Bad request.", + * "status": 400 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + /** @description Unauthorized. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "unauthorized", + * "message": "Unauthorized.", + * "status": 401 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + }; + }; + getWebsiteSegments: { + parameters: { + query: { + search?: string; + type: 'segment' | 'cohort'; + }; + header?: never; + path: { + websiteId: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description The operation completed successfully. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + count: number; + data: { + /** Format: date-time */ + createdAt: string; + id: string; + name: string; + parameters: + | string + | number + | false + | true + | { + [key: string]: + | string + | number + | false + | true + | unknown + | { + length: number; + }; + } + | { + length: number; + }; + type: string; + /** Format: date-time */ + updatedAt: string; + websiteId: string; + }[]; + isCapped?: boolean; + orderBy?: string; + page: number; + pageSize: number; + search?: string; + sortDescending?: boolean; + }; + }; + }; + /** @description Bad request. */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "bad-request", + * "message": "Bad request.", + * "status": 400 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + /** @description Unauthorized. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "unauthorized", + * "message": "Unauthorized.", + * "status": 401 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + }; + }; + createWebsiteSegment: { + parameters: { + query?: never; + header?: never; + path: { + websiteId: string; + }; + cookie?: never; + }; + requestBody: { + content: { + 'application/json': { + name: string; + parameters: { + action?: { + type: string; + value: string; + }; + dateRange?: string; + filters?: { + [key: string]: unknown; + }[]; + /** @enum {string} */ + match?: 'all' | 'any'; + }; + /** @enum {string} */ + type: 'segment' | 'cohort'; + }; + }; + }; + responses: { + /** @description The operation completed successfully. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + /** Format: date-time */ + createdAt: string; + id: string; + name: string; + parameters: + | string + | number + | false + | true + | { + [key: string]: + | string + | number + | false + | true + | unknown + | { + length: number; + }; + } + | { + length: number; + }; + type: string; + /** Format: date-time */ + updatedAt: string; + websiteId: string; + }; + }; + }; + /** @description Bad request. */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "bad-request", + * "message": "Bad request.", + * "status": 400 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + /** @description Unauthorized. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "unauthorized", + * "message": "Unauthorized.", + * "status": 401 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + }; + }; + getWebsiteSegment: { + parameters: { + query?: never; + header?: never; + path: { + segmentId: string; + websiteId: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description The operation completed successfully. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + /** Format: date-time */ + createdAt: string; + id: string; + name: string; + parameters: + | string + | number + | false + | true + | { + [key: string]: + | string + | number + | false + | true + | unknown + | { + length: number; + }; + } + | { + length: number; + }; + type: string; + /** Format: date-time */ + updatedAt: string; + websiteId: string; + }; + }; + }; + /** @description Unauthorized. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "unauthorized", + * "message": "Unauthorized.", + * "status": 401 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + /** @description Not found. */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "not-found", + * "message": "Not found.", + * "status": 404 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + }; + }; + updateWebsiteSegment: { + parameters: { + query?: never; + header?: never; + path: { + segmentId: string; + websiteId: string; + }; + cookie?: never; + }; + requestBody: { + content: { + 'application/json': { + name: string; + parameters: { + [key: string]: unknown; + }; + /** @enum {string} */ + type: 'segment' | 'cohort'; + }; + }; + }; + responses: { + /** @description The operation completed successfully. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + /** Format: date-time */ + createdAt: string; + id: string; + name: string; + parameters: + | string + | number + | false + | true + | { + [key: string]: + | string + | number + | false + | true + | unknown + | { + length: number; + }; + } + | { + length: number; + }; + type: string; + /** Format: date-time */ + updatedAt: string; + websiteId: string; + }; + }; + }; + /** @description Bad request. */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "bad-request", + * "message": "Bad request.", + * "status": 400 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + /** @description Unauthorized. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "unauthorized", + * "message": "Unauthorized.", + * "status": 401 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + /** @description Not found. */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "not-found", + * "message": "Not found.", + * "status": 404 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + }; + }; + deleteWebsiteSegment: { + parameters: { + query?: never; + header?: never; + path: { + segmentId: string; + websiteId: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description The operation completed successfully. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + /** @constant */ + ok: true; + }; + }; + }; + /** @description Unauthorized. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "unauthorized", + * "message": "Unauthorized.", + * "status": 401 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + /** @description Not found. */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "not-found", + * "message": "Not found.", + * "status": 404 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + }; + }; + getSessionDataPivot: { + parameters: { + query: { + browser?: string; + city?: string; + cohort?: string; + country?: string; + device?: string; + distinctId?: string; + endAt: number; + event?: string; + eventType?: number; + excludeBounce?: string; + hostname?: string; + language?: string; + match?: 'all' | 'any'; + maxResults?: number; + os?: string; + page?: number; + pageSize?: number; + path?: string; + propertyName: string; + query?: string; + referrer?: string; + region?: string; + segment?: string; + startAt: number; + tag?: string; + timezone?: string; + title?: string; + unit?: string; + utmCampaign?: string; + utmContent?: string; + utmMedium?: string; + utmSource?: string; + utmTerm?: string; + }; + header?: never; + path: { + websiteId: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description The operation completed successfully. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + count: number; + data: { + createdAt: string; + distinctId: string; + propertyKeys: string[]; + propertyValues: string[]; + sessionId: string; + }[]; + isCapped?: boolean; + orderBy?: string; + page: number; + pageSize: number; + search?: string; + sortDescending?: boolean; + }; + }; + }; + /** @description Bad request. */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "bad-request", + * "message": "Bad request.", + * "status": 400 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + /** @description Unauthorized. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "unauthorized", + * "message": "Unauthorized.", + * "status": 401 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + }; + }; + getSessionDataArraySeries: { + parameters: { + query: { + browser?: string; + city?: string; + cohort?: string; + country?: string; + device?: string; + distinctId?: string; + endAt: number; + event?: string; + eventType?: number; + excludeBounce?: string; + hostname?: string; + language?: string; + match?: 'all' | 'any'; + os?: string; + path?: string; + propertyName: string; + query?: string; + referrer?: string; + region?: string; + segment?: string; + startAt: number; + tag?: string; + timezone?: string; + title?: string; + unit?: string; + utmCampaign?: string; + utmContent?: string; + utmMedium?: string; + utmSource?: string; + utmTerm?: string; + }; + header?: never; + path: { + websiteId: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description The operation completed successfully. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + t: string; + x: string; + y: number; + }[]; + }; + }; + /** @description Bad request. */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "bad-request", + * "message": "Bad request.", + * "status": 400 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + /** @description Unauthorized. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "unauthorized", + * "message": "Unauthorized.", + * "status": 401 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + }; + }; + getSessionDataDateSeries: { + parameters: { + query: { + browser?: string; + city?: string; + cohort?: string; + country?: string; + device?: string; + distinctId?: string; + endAt: number; + event?: string; + eventType?: number; + excludeBounce?: string; + hostname?: string; + language?: string; + match?: 'all' | 'any'; + os?: string; + path?: string; + propertyName: string; + query?: string; + referrer?: string; + region?: string; + segment?: string; + startAt: number; + tag?: string; + timezone?: string; + title?: string; + utmCampaign?: string; + utmContent?: string; + utmMedium?: string; + utmSource?: string; + utmTerm?: string; + }; + header?: never; + path: { + websiteId: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description The operation completed successfully. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + t: string; + y: number; + }[]; + }; + }; + /** @description Bad request. */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "bad-request", + * "message": "Bad request.", + * "status": 400 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + /** @description Unauthorized. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "unauthorized", + * "message": "Unauthorized.", + * "status": 401 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + }; + }; + getSessionDataNumericSeries: { + parameters: { + query: { + browser?: string; + city?: string; + cohort?: string; + country?: string; + device?: string; + distinctId?: string; + endAt: number; + event?: string; + eventType?: number; + excludeBounce?: string; + hostname?: string; + language?: string; + match?: 'all' | 'any'; + metric?: 'sum' | 'avg' | 'count'; + os?: string; + path?: string; + propertyName: string; + query?: string; + referrer?: string; + region?: string; + segment?: string; + startAt: number; + tag?: string; + timezone?: string; + title?: string; + unit?: string; + utmCampaign?: string; + utmContent?: string; + utmMedium?: string; + utmSource?: string; + utmTerm?: string; + }; + header?: never; + path: { + websiteId: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description The operation completed successfully. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + t: string; + y: number; + }[]; + }; + }; + /** @description Bad request. */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "bad-request", + * "message": "Bad request.", + * "status": 400 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + /** @description Unauthorized. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "unauthorized", + * "message": "Unauthorized.", + * "status": 401 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + }; + }; + getSessionDataNumericStats: { + parameters: { + query: { + browser?: string; + city?: string; + cohort?: string; + country?: string; + device?: string; + distinctId?: string; + endAt: number; + event?: string; + eventType?: number; + excludeBounce?: string; + hostname?: string; + language?: string; + match?: 'all' | 'any'; + os?: string; + path?: string; + propertyName: string; + query?: string; + referrer?: string; + region?: string; + segment?: string; + startAt: number; + tag?: string; + timezone?: string; + title?: string; + utmCampaign?: string; + utmContent?: string; + utmMedium?: string; + utmSource?: string; + utmTerm?: string; + }; + header?: never; + path: { + websiteId: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description The operation completed successfully. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + average: number; + max: number; + median: number; + min: number; + total: number; + }; + }; + }; + /** @description Bad request. */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "bad-request", + * "message": "Bad request.", + * "status": 400 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + /** @description Unauthorized. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "unauthorized", + * "message": "Unauthorized.", + * "status": 401 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + }; + }; + getSessionDataProperties: { + parameters: { + query: { + browser?: string; + city?: string; + cohort?: string; + country?: string; + device?: string; + distinctId?: string; + endAt: number; + event?: string; + eventType?: number; + excludeBounce?: string; + hostname?: string; + language?: string; + match?: 'all' | 'any'; + os?: string; + path?: string; + propertyName?: string; + query?: string; + referrer?: string; + region?: string; + segment?: string; + startAt: number; + tag?: string; + title?: string; + utmCampaign?: string; + utmContent?: string; + utmMedium?: string; + utmSource?: string; + utmTerm?: string; + }; + header?: never; + path: { + websiteId: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description The operation completed successfully. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + dataType: number; + propertyName: string; + total: number; + }[]; + }; + }; + /** @description Bad request. */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "bad-request", + * "message": "Bad request.", + * "status": 400 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + /** @description Unauthorized. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "unauthorized", + * "message": "Unauthorized.", + * "status": 401 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + }; + }; + getSessionDataPropertySeries: { + parameters: { + query: { + browser?: string; + city?: string; + cohort?: string; + country?: string; + device?: string; + distinctId?: string; + endAt: number; + event?: string; + eventType?: number; + excludeBounce?: string; + hostname?: string; + language?: string; + match?: 'all' | 'any'; + os?: string; + path?: string; + propertyName: string; + query?: string; + referrer?: string; + region?: string; + segment?: string; + startAt: number; + tag?: string; + timezone?: string; + title?: string; + unit?: string; + utmCampaign?: string; + utmContent?: string; + utmMedium?: string; + utmSource?: string; + utmTerm?: string; + }; + header?: never; + path: { + websiteId: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description The operation completed successfully. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + t: string; + x: string; + y: number; + }[]; + }; + }; + /** @description Bad request. */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "bad-request", + * "message": "Bad request.", + * "status": 400 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + /** @description Unauthorized. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "unauthorized", + * "message": "Unauthorized.", + * "status": 401 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + }; + }; + getSessionDataStats: { + parameters: { + query: { + browser?: string; + city?: string; + cohort?: string; + country?: string; + device?: string; + distinctId?: string; + endAt: number; + event?: string; + eventType?: number; + excludeBounce?: string; + hostname?: string; + language?: string; + match?: 'all' | 'any'; + os?: string; + path?: string; + propertyName: string; + query?: string; + referrer?: string; + region?: string; + segment?: string; + startAt: number; + tag?: string; + timezone?: string; + title?: string; + unit?: string; + utmCampaign?: string; + utmContent?: string; + utmMedium?: string; + utmSource?: string; + utmTerm?: string; + }; + header?: never; + path: { + websiteId: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description The operation completed successfully. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + activity: number; + events: number; + label: string; + sessions: number; + views: number; + visits: number; + }[]; + }; + }; + /** @description Bad request. */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "bad-request", + * "message": "Bad request.", + * "status": 400 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + /** @description Unauthorized. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "unauthorized", + * "message": "Unauthorized.", + * "status": 401 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + }; + }; + getSessionDataValues: { + parameters: { + query: { + browser?: string; + city?: string; + cohort?: string; + country?: string; + dataType?: number; + device?: string; + distinctId?: string; + endAt: number; + event?: string; + eventType?: number; + excludeBounce?: string; + hostname?: string; + language?: string; + match?: 'all' | 'any'; + os?: string; + path?: string; + propertyName?: string; + query?: string; + referrer?: string; + region?: string; + segment?: string; + startAt: number; + tag?: string; + title?: string; + utmCampaign?: string; + utmContent?: string; + utmMedium?: string; + utmSource?: string; + utmTerm?: string; + }; + header?: never; + path: { + websiteId: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description The operation completed successfully. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + total: number; + value: string; + }[]; + }; + }; + /** @description Bad request. */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "bad-request", + * "message": "Bad request.", + * "status": 400 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + /** @description Unauthorized. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "unauthorized", + * "message": "Unauthorized.", + * "status": 401 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + }; + }; + getWebsiteSessions: { + parameters: { + query?: { + browser?: string; + city?: string; + cohort?: string; + compare?: 'prev' | 'yoy'; + country?: string; + device?: string; + distinctId?: string; + endAt?: number; + endDate?: string; + event?: string; + eventType?: number; + excludeBounce?: string; + hostname?: string; + language?: string; + match?: 'all' | 'any'; + maxResults?: number; + os?: string; + page?: number; + pageSize?: number; + path?: string; + query?: string; + referrer?: string; + region?: string; + search?: string; + segment?: string; + startAt?: number; + startDate?: string; + tag?: string; + timezone?: string; + title?: string; + unit?: string; + utmCampaign?: string; + utmContent?: string; + utmMedium?: string; + utmSource?: string; + utmTerm?: string; + }; + header?: never; + path: { + /** @description Website ID. */ + websiteId: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description A page of sessions. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['WebsiteSessionPage']; + }; + }; + /** @description Bad request. */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "bad-request", + * "message": "Bad request.", + * "status": 400 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + /** @description Unauthorized. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "unauthorized", + * "message": "Unauthorized.", + * "status": 401 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + }; + }; + getWebsiteSession: { + parameters: { + query?: never; + header?: never; + path: { + sessionId: string; + websiteId: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description The operation completed successfully. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + canDelete: unknown; + stitchedSessionCount: unknown; + }; + }; + }; + /** @description Unauthorized. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "unauthorized", + * "message": "Unauthorized.", + * "status": 401 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + /** @description Not found. */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "not-found", + * "message": "Not found.", + * "status": 404 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + }; + }; + deleteWebsiteSession: { + parameters: { + query?: never; + header?: never; + path: { + sessionId: string; + websiteId: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description The operation completed successfully. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + /** @constant */ + ok: true; + }; + }; + }; + /** @description Bad request. */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "bad-request", + * "message": "Bad request.", + * "status": 400 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + /** @description Unauthorized. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "unauthorized", + * "message": "Unauthorized.", + * "status": 401 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + /** @description Not found. */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "not-found", + * "message": "Not found.", + * "status": 404 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + }; + }; + getWebsiteSessionActivity: { + parameters: { + query: { + distinctId?: string; + endAt: number; + startAt: number; + }; + header?: never; + path: { + sessionId: string; + websiteId: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description The operation completed successfully. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + createdAt: string; + eventId: string; + eventName: string; + eventType: number; + hasData: boolean; + hostname: string; + referrerDomain: string; + urlPath: string; + urlQuery: string; + visitId: string; + }[]; + }; + }; + /** @description Bad request. */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "bad-request", + * "message": "Bad request.", + * "status": 400 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + /** @description Unauthorized. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "unauthorized", + * "message": "Unauthorized.", + * "status": 401 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + }; + }; + getWebsiteSessionProperties: { + parameters: { + query?: never; + header?: never; + path: { + sessionId: string; + websiteId: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description The operation completed successfully. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + createdAt: string; + dataKey: string; + dataType: number; + dateValue: string; + numberValue: number; + sessionId: string; + stringValue: string; + websiteId: string; + }[]; + }; + }; + /** @description Unauthorized. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "unauthorized", + * "message": "Unauthorized.", + * "status": 401 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + }; + }; + getWebsiteSessionReplays: { + parameters: { + query?: { + compare?: 'prev' | 'yoy'; + endAt?: number; + endDate?: string; + maxResults?: number; + page?: number; + pageSize?: number; + search?: string; + startAt?: number; + startDate?: string; + timezone?: string; + unit?: string; + }; + header?: never; + path: { + sessionId: string; + websiteId: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description The operation completed successfully. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + count: number; + data: { + browser: string; + chunkCount: number; + city: string; + country: string; + createdAt: string; + device: string; + duration: number; + endedAt: string; + eventCount: number; + id: string; + os: string; + sessionId: string; + startedAt: string; + websiteId: string; + }[]; + isCapped?: boolean; + orderBy?: string; + page: number; + pageSize: number; + search?: string; + sortDescending?: boolean; + }; + }; + }; + /** @description Bad request. */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "bad-request", + * "message": "Bad request.", + * "status": 400 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + /** @description Unauthorized. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "unauthorized", + * "message": "Unauthorized.", + * "status": 401 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + }; + }; + getWebsiteSessionStats: { + parameters: { + query: { + browser?: string; + city?: string; + cohort?: string; + country?: string; + device?: string; + distinctId?: string; + endAt: number; + event?: string; + eventType?: number; + excludeBounce?: string; + hostname?: string; + language?: string; + match?: 'all' | 'any'; + os?: string; + path?: string; + query?: string; + referrer?: string; + region?: string; + segment?: string; + startAt: number; + tag?: string; + title?: string; + utmCampaign?: string; + utmContent?: string; + utmMedium?: string; + utmSource?: string; + utmTerm?: string; + }; + header?: never; + path: { + websiteId: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description The operation completed successfully. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': Record; + }; + }; + /** @description Bad request. */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "bad-request", + * "message": "Bad request.", + * "status": 400 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + /** @description Unauthorized. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "unauthorized", + * "message": "Unauthorized.", + * "status": 401 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + }; + }; + getWebsiteSessionsWeekly: { + parameters: { + query: { + browser?: string; + city?: string; + cohort?: string; + country?: string; + device?: string; + distinctId?: string; + endAt: number; + event?: string; + eventType?: number; + excludeBounce?: string; + hostname?: string; + language?: string; + match?: 'all' | 'any'; + os?: string; + path?: string; + query?: string; + referrer?: string; + region?: string; + segment?: string; + startAt: number; + tag?: string; + timezone: string; + title?: string; + utmCampaign?: string; + utmContent?: string; + utmMedium?: string; + utmSource?: string; + utmTerm?: string; + }; + header?: never; + path: { + websiteId: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description The operation completed successfully. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': number[][]; + }; + }; + /** @description Bad request. */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "bad-request", + * "message": "Bad request.", + * "status": 400 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + /** @description Unauthorized. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "unauthorized", + * "message": "Unauthorized.", + * "status": 401 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + }; + }; + getWebsiteShares: { + parameters: { + query?: { + browser?: string; + city?: string; + cohort?: string; + country?: string; + device?: string; + distinctId?: string; + event?: string; + eventType?: number; + excludeBounce?: string; + hostname?: string; + language?: string; + match?: 'all' | 'any'; + maxResults?: number; + os?: string; + page?: number; + pageSize?: number; + path?: string; + query?: string; + referrer?: string; + region?: string; + segment?: string; + tag?: string; + title?: string; + utmCampaign?: string; + utmContent?: string; + utmMedium?: string; + utmSource?: string; + utmTerm?: string; + }; + header?: never; + path: { + websiteId: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description The operation completed successfully. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + count: number; + data: { + /** Format: date-time */ + createdAt: string; + entityId: string; + id: string; + name: string; + parameters: + | string + | number + | false + | true + | { + [key: string]: + | string + | number + | false + | true + | unknown + | { + length: number; + }; + } + | { + length: number; + }; + shareType: number; + slug: string; + /** Format: date-time */ + updatedAt: string; + }[]; + isCapped?: boolean; + orderBy?: string; + page: number; + pageSize: number; + search?: string; + sortDescending?: boolean; + }; + }; + }; + /** @description Bad request. */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "bad-request", + * "message": "Bad request.", + * "status": 400 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + /** @description Unauthorized. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "unauthorized", + * "message": "Unauthorized.", + * "status": 401 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + }; + }; + createWebsiteShare: { + parameters: { + query?: never; + header?: never; + path: { + websiteId: string; + }; + cookie?: never; + }; + requestBody: { + content: { + 'application/json': { + name: string; + parameters?: { + [key: string]: unknown; + }; + }; + }; + }; + responses: { + /** @description The operation completed successfully. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + /** Format: date-time */ + createdAt: string; + entityId: string; + id: string; + name: string; + parameters: + | string + | number + | false + | true + | { + [key: string]: + | string + | number + | false + | true + | unknown + | { + length: number; + }; + } + | { + length: number; + }; + shareType: number; + slug: string; + /** Format: date-time */ + updatedAt: string; + }; + }; + }; + /** @description Bad request. */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "bad-request", + * "message": "Bad request.", + * "status": 400 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + /** @description Unauthorized. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "unauthorized", + * "message": "Unauthorized.", + * "status": 401 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + }; + }; + getWebsiteStats: { + parameters: { + query?: { + browser?: string; + city?: string; + cohort?: string; + compare?: 'prev' | 'yoy'; + country?: string; + device?: string; + distinctId?: string; + endAt?: number; + endDate?: string; + event?: string; + eventType?: number; + excludeBounce?: string; + hostname?: string; + language?: string; + match?: 'all' | 'any'; + os?: string; + path?: string; + query?: string; + referrer?: string; + region?: string; + segment?: string; + startAt?: number; + startDate?: string; + tag?: string; + timezone?: string; + title?: string; + unit?: string; + utmCampaign?: string; + utmContent?: string; + utmMedium?: string; + utmSource?: string; + utmTerm?: string; + }; + header?: never; + path: { + /** @description Website ID. */ + websiteId: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Summary stats with comparison. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['WebsiteStats']; + }; + }; + /** @description Bad request. */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "bad-request", + * "message": "Bad request.", + * "status": 400 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + /** @description Unauthorized. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "unauthorized", + * "message": "Unauthorized.", + * "status": 401 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + }; + }; + transferWebsite: { + parameters: { + query?: never; + header?: never; + path: { + websiteId: string; + }; + cookie?: never; + }; + requestBody: { + content: { + 'application/json': { + /** Format: uuid */ + teamId?: string; + /** Format: uuid */ + userId?: string; + }; + }; + }; + responses: { + /** @description The operation completed successfully. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + /** Format: date-time */ + createdAt: string; + createdBy: string; + /** Format: date-time */ + deletedAt: string; + domain: string; + id: string; + name: string; + recorderEnabled: boolean; + replayConfig: + | string + | number + | false + | true + | { + [key: string]: + | string + | number + | false + | true + | unknown + | { + length: number; + }; + } + | { + length: number; + }; + /** Format: date-time */ + resetAt: string; + teamId: string; + /** Format: date-time */ + updatedAt: string; + userId: string; + }; + }; + }; + /** @description Bad request. */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "bad-request", + * "message": "Bad request.", + * "status": 400 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + /** @description Unauthorized. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "unauthorized", + * "message": "Unauthorized.", + * "status": 401 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + }; + }; + getWebsiteValues: { + parameters: { + query: { + compare?: 'prev' | 'yoy'; + endAt?: number; + endDate?: string; + search?: string; + startAt?: number; + startDate?: string; + timezone?: string; + type: string; + unit?: string; + }; + header?: never; + path: { + websiteId: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description The operation completed successfully. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': unknown[]; + }; + }; + /** @description Bad request. */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "bad-request", + * "message": "Bad request.", + * "status": 400 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + /** @description Unauthorized. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "unauthorized", + * "message": "Unauthorized.", + * "status": 401 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + }; + }; + getWebsitesCharts: { + parameters: { + query: { + endAt?: number; + ids: string; + startAt?: number; + timezone?: string; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description The operation completed successfully. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + data: { + [key: string]: { + total: number; + values: number[]; + }; + }; + }; + }; + }; + /** @description Bad request. */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "bad-request", + * "message": "Bad request.", + * "status": 400 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + /** @description Unauthorized. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + /** + * @example { + * "error": { + * "code": "unauthorized", + * "message": "Unauthorized.", + * "status": 401 + * } + * } + */ + 'application/json': components['schemas']['ApiError']; + }; + }; + }; + }; +} diff --git a/packages/api-client/src/http.test.ts b/packages/api-client/src/http.test.ts new file mode 100644 index 000000000..13634b22b --- /dev/null +++ b/packages/api-client/src/http.test.ts @@ -0,0 +1,223 @@ +import { describe, expect, test } from 'vitest'; +import { UmamiApiError } from './errors'; +import type { OperationDefinition } from './generated/operations'; +import { + buildAuthHeaders, + buildPath, + buildUrl, + sendRequest, + splitInput, + stripApiPrefix, +} from './http'; + +const statsOperation: OperationDefinition = { + operationId: 'getWebsiteStats', + method: 'get', + path: '/api/websites/{websiteId}/stats', + pathParams: ['websiteId'], + queryParams: ['startAt', 'endAt', 'browser'], + hasBody: false, +}; + +const reportOperation: OperationDefinition = { + operationId: 'runFunnelReport', + method: 'post', + path: '/api/reports/funnel', + pathParams: [], + queryParams: [], + hasBody: true, +}; + +describe('buildPath', () => { + test('encodes path parameters', () => { + expect(buildPath('/api/websites/{websiteId}/stats', { websiteId: 'a b/c' })).toBe( + '/api/websites/a%20b%2Fc/stats', + ); + }); + + test('throws on a missing path parameter', () => { + expect(() => buildPath('/api/websites/{websiteId}', {})).toThrow(/websiteId/); + }); +}); + +describe('buildUrl', () => { + test('strips the /api prefix and joins with the base URL', () => { + expect(stripApiPrefix('/api/websites')).toBe('/websites'); + expect(buildUrl('https://api.umami.is/v1/', '/api/websites', {}).toString()).toBe( + 'https://api.umami.is/v1/websites', + ); + expect(buildUrl('https://example.com/api', '/api/websites', {}).toString()).toBe( + 'https://example.com/api/websites', + ); + }); + + test('serializes query values, arrays and dates and skips nullish values', () => { + const url = buildUrl('https://example.com/api', '/api/websites', { + page: 2, + search: 'a b', + skip: undefined, + nothing: null, + flag: true, + tags: ['x', 'y'], + when: new Date('2024-01-02T03:04:05.000Z'), + }); + + expect(url.searchParams.getAll('tags')).toEqual(['x', 'y']); + expect(url.searchParams.get('page')).toBe('2'); + expect(url.searchParams.get('search')).toBe('a b'); + expect(url.searchParams.get('flag')).toBe('true'); + expect(url.searchParams.get('when')).toBe('2024-01-02T03:04:05.000Z'); + expect(url.searchParams.has('skip')).toBe(false); + expect(url.searchParams.has('nothing')).toBe(false); + }); +}); + +describe('buildAuthHeaders', () => { + test('uses bearer authentication for tokens', () => { + expect(buildAuthHeaders({ token: 'abc' })).toEqual({ authorization: 'Bearer abc' }); + }); + + test('sends API keys as both header conventions when no token is present', () => { + expect(buildAuthHeaders({ apiKey: 'umami_123' })).toEqual({ + 'x-umami-api-key': 'umami_123', + authorization: 'Bearer umami_123', + }); + }); + + test('prefers the explicit token as bearer when both are present', () => { + expect(buildAuthHeaders({ apiKey: 'k', token: 't' })).toEqual({ + 'x-umami-api-key': 'k', + authorization: 'Bearer t', + }); + }); +}); + +describe('splitInput', () => { + test('routes known keys to path/query and unknown keys to query for GET', () => { + expect( + splitInput(statsOperation, { + websiteId: 'w', + startAt: 1, + endAt: 2, + browser1: 'chrome', + pf_plan: 'pro', + }), + ).toEqual({ + path: { websiteId: 'w' }, + query: { startAt: 1, endAt: 2, browser1: 'chrome', pf_plan: 'pro' }, + body: undefined, + }); + }); + + test('routes unknown keys to the body for operations with a request body', () => { + expect( + splitInput(reportOperation, { + websiteId: 'w', + type: 'funnel', + parameters: { steps: [] }, + filters: {}, + }), + ).toEqual({ + path: {}, + query: {}, + body: { websiteId: 'w', type: 'funnel', parameters: { steps: [] }, filters: {} }, + }); + }); +}); + +function jsonResponse(body: unknown, init: ResponseInit = {}) { + return new Response(JSON.stringify(body), { + status: 200, + headers: { 'content-type': 'application/json' }, + ...init, + }); +} + +describe('sendRequest', () => { + test('sends JSON bodies and merges headers', async () => { + let captured: { url: string; init?: RequestInit } | undefined; + const fetchImpl = async (url: string | URL, init?: RequestInit) => { + captured = { url: url.toString(), init }; + return jsonResponse({ ok: true }); + }; + + const result = await sendRequest(fetchImpl, { + method: 'post', + url: new URL('https://example.com/api/reports/funnel'), + headers: { authorization: 'Bearer t', 'x-custom': '1' }, + body: { websiteId: 'w' }, + }); + + expect(result).toEqual({ ok: true }); + expect(captured?.init?.method).toBe('POST'); + expect(captured?.init?.body).toBe(JSON.stringify({ websiteId: 'w' })); + expect(captured?.init?.headers).toMatchObject({ + authorization: 'Bearer t', + 'x-custom': '1', + 'content-type': 'application/json', + accept: 'application/json', + }); + }); + + test('throws UmamiApiError with the API error code for 4xx responses', async () => { + const fetchImpl = async () => + jsonResponse( + { error: { message: 'Nope', code: 'unauthorized', status: 401 } }, + { status: 401 }, + ); + + await expect( + sendRequest(fetchImpl, { + method: 'get', + url: new URL('https://example.com/api/websites'), + headers: {}, + }), + ).rejects.toMatchObject({ + name: 'UmamiApiError', + status: 401, + code: 'unauthorized', + message: 'Nope', + isUnauthorized: true, + }); + }); + + test('throws UmamiApiError for 5xx responses without a JSON body', async () => { + const fetchImpl = async () => new Response('boom', { status: 502 }); + + const error = (await sendRequest(fetchImpl, { + method: 'get', + url: new URL('https://example.com/api/websites'), + headers: {}, + }).catch(e => e)) as UmamiApiError; + + expect(error).toBeInstanceOf(UmamiApiError); + expect(error.status).toBe(502); + expect(error.code).toBe('server-error'); + expect(error.isServerError).toBe(true); + }); + + test('throws on malformed JSON in successful responses', async () => { + const fetchImpl = async () => + new Response('{not json', { status: 200, headers: { 'content-type': 'application/json' } }); + + await expect( + sendRequest(fetchImpl, { + method: 'get', + url: new URL('https://example.com/api/websites'), + headers: {}, + }), + ).rejects.toMatchObject({ code: 'invalid-json' }); + }); + + test('returns undefined for empty bodies', async () => { + const fetchImpl = async () => new Response(null, { status: 204 }); + + await expect( + sendRequest(fetchImpl, { + method: 'delete', + url: new URL('https://example.com/api/websites/w'), + headers: {}, + }), + ).resolves.toBeUndefined(); + }); +}); diff --git a/packages/api-client/src/http.ts b/packages/api-client/src/http.ts new file mode 100644 index 000000000..5e7b60412 --- /dev/null +++ b/packages/api-client/src/http.ts @@ -0,0 +1,238 @@ +import { UmamiApiError, type UmamiApiErrorBody } from './errors'; +import type { HttpMethod, OperationDefinition } from './generated/operations'; +import type { FetchLike, RequestOptions, UmamiClientOptions } from './types'; + +export const DEFAULT_BASE_URL = 'https://api.umami.is/v1'; +export const API_KEY_HEADER = 'x-umami-api-key'; + +type QueryValue = string | number | boolean | Date | null | undefined | QueryValue[]; + +export function trimSlashes(value: string) { + return value.replace(/^\/+|\/+$/g, ''); +} + +/** + * OpenAPI paths are documented as `/api/...`. The base URL already points at the API root + * (`https://api.umami.is/v1` or `https://example.com/api`), so the `/api` prefix is dropped. + */ +export function stripApiPrefix(path: string) { + return path.replace(/^\/api(?=\/|$)/, ''); +} + +export function buildPath(template: string, params: Record) { + return template.replace(/\{([^}]+)}/g, (_, name: string) => { + const value = params[name]; + + if (value === undefined || value === null || value === '') { + throw new Error(`Missing required path parameter "${name}" for ${template}`); + } + + return encodeURIComponent(String(value)); + }); +} + +export function appendQuery(search: URLSearchParams, key: string, value: QueryValue) { + if (value === undefined || value === null) { + return; + } + + if (Array.isArray(value)) { + for (const item of value) { + appendQuery(search, key, item); + } + + return; + } + + if (value instanceof Date) { + search.append(key, value.toISOString()); + return; + } + + search.append(key, String(value)); +} + +export function buildUrl(baseUrl: string, path: string, query: Record) { + const base = trimSlashes(baseUrl || DEFAULT_BASE_URL); + const url = new URL(`${base}/${trimSlashes(stripApiPrefix(path))}`); + + for (const [key, value] of Object.entries(query)) { + appendQuery(url.searchParams, key, value as QueryValue); + } + + return url; +} + +export function buildAuthHeaders(options: Pick) { + const headers: Record = {}; + + if (options.apiKey) { + headers[API_KEY_HEADER] = options.apiKey; + } + + const bearer = options.token ?? options.apiKey; + + if (bearer) { + headers.authorization = `Bearer ${bearer}`; + } + + return headers; +} + +export interface SplitInput { + path: Record; + query: Record; + body: Record | undefined; +} + +/** + * Splits a flat input object into path params, query params and body according to the operation + * definition. Unknown keys go to the query string for body-less operations (dynamic filter params + * such as `browser1` or `pf_*`) and to the body otherwise. + */ +export function splitInput( + operation: OperationDefinition, + input: Record = {}, +): SplitInput { + const path: Record = {}; + const query: Record = {}; + const body: Record = {}; + const pathParams = new Set(operation.pathParams); + const queryParams = new Set(operation.queryParams); + + for (const [key, value] of Object.entries(input)) { + if (pathParams.has(key)) { + path[key] = value; + + if (!operation.hasBody) { + continue; + } + } + + if (queryParams.has(key)) { + query[key] = value; + } else if (operation.hasBody) { + body[key] = value; + } else if (!pathParams.has(key)) { + query[key] = value; + } + } + + return { path, query, body: operation.hasBody ? body : undefined }; +} + +async function parseBody(response: Response): Promise { + const text = await response.text(); + + if (!text) { + return undefined; + } + + const contentType = response.headers.get('content-type') ?? ''; + + if (!contentType.includes('json')) { + return text; + } + + try { + return JSON.parse(text); + } catch { + throw new UmamiApiError({ + status: response.status, + code: 'invalid-json', + message: 'Response body is not valid JSON', + method: 'GET', + url: response.url, + body: text, + }); + } +} + +export interface HttpRequest { + method: HttpMethod; + url: URL; + headers: Record; + body?: unknown; + signal?: AbortSignal; + timeout?: number; +} + +export async function sendRequest(fetchImpl: FetchLike, request: HttpRequest): Promise { + const headers: Record = { accept: 'application/json', ...request.headers }; + const init: RequestInit = { method: request.method.toUpperCase(), headers }; + + if (request.body !== undefined) { + headers['content-type'] = 'application/json'; + init.body = JSON.stringify(request.body); + } + + let timer: ReturnType | undefined; + + if (request.timeout && typeof AbortController !== 'undefined') { + const controller = new AbortController(); + + timer = setTimeout(() => controller.abort(), request.timeout); + request.signal?.addEventListener('abort', () => controller.abort(), { once: true }); + init.signal = controller.signal; + } else if (request.signal) { + init.signal = request.signal; + } + + let response: Response; + + try { + response = await fetchImpl(request.url, init); + } finally { + if (timer) { + clearTimeout(timer); + } + } + + let body: unknown; + + try { + body = await parseBody(response); + } catch (error) { + if (error instanceof UmamiApiError && response.ok) { + throw new UmamiApiError({ + status: response.status, + code: error.code, + message: error.message, + method: request.method.toUpperCase(), + url: request.url.toString(), + body: error.body, + }); + } + + body = undefined; + } + + if (!response.ok) { + const error = (body as UmamiApiErrorBody | undefined)?.error; + + throw new UmamiApiError({ + status: response.status, + code: error?.code, + message: error?.message, + method: request.method.toUpperCase(), + url: request.url.toString(), + body, + }); + } + + return body as T; +} + +export function resolveFetch(custom?: FetchLike): FetchLike { + if (custom) { + return custom; + } + + if (typeof fetch !== 'function') { + throw new Error('No fetch implementation available. Pass `fetch` in UmamiClient options.'); + } + + return (input, init) => fetch(input, init); +} + +export type { RequestOptions }; diff --git a/packages/api-client/src/index.ts b/packages/api-client/src/index.ts new file mode 100644 index 000000000..863a0454c --- /dev/null +++ b/packages/api-client/src/index.ts @@ -0,0 +1,15 @@ +export { UmamiClient } from './client'; +export { isUmamiApiError, UmamiApiError, type UmamiApiErrorBody } from './errors'; +export { + API_VERSION, + type HttpMethod, + type OperationDefinition, + type OperationId, + type OperationInput, + type OperationOutput, + operations, + type Schemas, +} from './generated/operations'; +export type { components, operations as OperationTypes, paths } from './generated/types'; +export { API_KEY_HEADER, DEFAULT_BASE_URL } from './http'; +export type { FetchLike, RequestOptions, UmamiClientOptions } from './types'; diff --git a/packages/api-client/src/types.ts b/packages/api-client/src/types.ts new file mode 100644 index 000000000..a11ce80b1 --- /dev/null +++ b/packages/api-client/src/types.ts @@ -0,0 +1,32 @@ +export type FetchLike = (input: string | URL, init?: RequestInit) => Promise; + +export interface UmamiClientOptions { + /** + * API base URL. Defaults to Umami Cloud (`https://api.umami.is/v1`). + * For self-hosted installs use your instance URL plus `/api`, e.g. `https://analytics.example.com/api`. + */ + baseUrl?: string; + /** + * Bearer token: a login token from `POST /api/auth/login`, an OAuth access token, or a + * self-hosted API key (`umami_…`). + */ + token?: string; + /** + * Umami Cloud API key. Sent as `x-umami-api-key`. When no `token` is provided the key is also + * sent as a bearer token so that self-hosted `umami_…` keys work with either option. + */ + apiKey?: string; + /** Extra headers sent with every request. */ + headers?: Record; + /** Custom fetch implementation (testing, alternative runtimes, in-process dispatch). */ + fetch?: FetchLike; + /** Per-request timeout in milliseconds. Disabled by default. */ + timeout?: number; +} + +export interface RequestOptions { + /** Extra headers for this request only. */ + headers?: Record; + /** Abort signal for this request. */ + signal?: AbortSignal; +} diff --git a/packages/api-client/tsconfig.json b/packages/api-client/tsconfig.json new file mode 100644 index 000000000..440e0c072 --- /dev/null +++ b/packages/api-client/tsconfig.json @@ -0,0 +1,18 @@ +{ + "compilerOptions": { + "target": "es2022", + "module": "esnext", + "moduleResolution": "bundler", + "lib": ["es2022", "dom"], + "strict": true, + "noEmit": true, + "skipLibCheck": true, + "esModuleInterop": true, + "isolatedModules": true, + "resolveJsonModule": true, + "forceConsistentCasingInFileNames": true, + "ignoreDeprecations": "6.0", + "types": ["node"] + }, + "include": ["src/**/*.ts", "tsup.config.ts", "vitest.config.ts"] +} diff --git a/packages/api-client/tsup.config.ts b/packages/api-client/tsup.config.ts new file mode 100644 index 000000000..5edf6a8a2 --- /dev/null +++ b/packages/api-client/tsup.config.ts @@ -0,0 +1,12 @@ +import { defineConfig } from 'tsup'; + +export default defineConfig({ + entry: { index: 'src/index.ts' }, + format: ['esm', 'cjs'], + outDir: 'dist', + dts: true, + sourcemap: true, + clean: true, + splitting: false, + target: 'es2022', +}); diff --git a/packages/api-client/vitest.config.ts b/packages/api-client/vitest.config.ts new file mode 100644 index 000000000..7eeb3f85d --- /dev/null +++ b/packages/api-client/vitest.config.ts @@ -0,0 +1,8 @@ +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + test: { + environment: 'node', + include: ['src/**/*.test.ts'], + }, +}); diff --git a/packages/mcp/README.md b/packages/mcp/README.md new file mode 100644 index 000000000..55a3d9ca7 --- /dev/null +++ b/packages/mcp/README.md @@ -0,0 +1,92 @@ +# @umami/mcp + +[Model Context Protocol](https://modelcontextprotocol.io) server for [Umami](https://umami.is) +analytics. Lets Claude, ChatGPT, Cursor and other MCP clients answer questions about your +website traffic using read-only tools that call the Umami API through `@umami/api-client`. + +The MCP server never talks to a database; every tool goes through the public API and the same +user/team permission checks as the web app. + +## Tools + +| Tool | Purpose | +| --------------------- | ----------------------------------------------------------------------- | +| `list_websites` | Find the websites you can access (call first to get a `websiteId`). | +| `get_website_stats` | Pageviews, visitors, visits, bounce rate, duration + previous period. | +| `get_website_traffic` | Pageview/visit time series by minute, hour, day, month or year. | +| `get_website_metrics` | Top pages, referrers, channels, countries, browsers, devices, UTM, events. | +| `get_realtime` | Visitors active right now. | +| `get_events` | Individual tracked events (paginated). | +| `get_sessions` | Visitor sessions (paginated). | +| `get_session` | One session with its activity timeline and properties. | +| `run_funnel` | Conversion funnel across page/event steps. | +| `run_journey` | Most common paths visitors take. | +| `run_retention` | Cohort retention table. | +| `run_attribution` | First/last-click attribution for a conversion. | +| `get_revenue` | Revenue totals, series and breakdowns. | + +All tools are read-only. Dates are ISO 8601; results are paginated with a hard cap on page size. + +## Remote (Umami Cloud / self-hosted with OAuth) + +Add the MCP endpoint to your client: + +```text +https://cloud.umami.is/mcp +https://your-umami.example.com/mcp +``` + +The client discovers `/.well-known/oauth-protected-resource/mcp`, sends you to Umami to sign in +and approve access (`websites:read`, `analytics:read`), and receives an OAuth token. No API key +is pasted anywhere. Clients register via Client ID Metadata Documents; dynamic client registration +is available for compatibility (`OAUTH_DISABLE_DCR=1` turns it off). + +Self-hosted instances can alternatively pass an API key (`umami_…`) as the bearer token. + +## Local / stdio + +```json +{ + "mcpServers": { + "umami": { + "command": "npx", + "args": ["-y", "@umami/mcp"], + "env": { + "UMAMI_URL": "https://analytics.example.com", + "UMAMI_API_TOKEN": "umami_…" + } + } + } +} +``` + +| Variable | Description | +| ----------------- | ------------------------------------------------------------------ | +| `UMAMI_URL` | Self-hosted instance URL (`/api` is appended). | +| `UMAMI_API_URL` | Full API base URL instead, e.g. `https://api.umami.is/v1`. | +| `UMAMI_API_TOKEN` | API key or login token (self-hosted). | +| `UMAMI_API_KEY` | Umami Cloud API key. | + +## Example prompts + +- Show my websites. +- How many visitors did example.com get last week? +- What were the top 10 pages this month? +- Compare traffic this month with the previous month. +- Where is traffic coming from? +- What signup events occurred yesterday? +- Show sessions for user abc123. + +## Programmatic use + +```ts +import { UmamiClient } from '@umami/api-client'; +import { createUmamiMcpServer } from '@umami/mcp'; + +const server = createUmamiMcpServer({ + client: new UmamiClient({ baseUrl, token }), +}); +``` + +`createUmamiMcpHttpHandler({ createClient })` returns a Streamable HTTP handler for embedding in +any web framework; the host verifies the bearer token and passes `authInfo`. diff --git a/packages/mcp/package.json b/packages/mcp/package.json new file mode 100644 index 000000000..75ed97597 --- /dev/null +++ b/packages/mcp/package.json @@ -0,0 +1,42 @@ +{ + "name": "@umami/mcp", + "version": "1.0.0-alpha.0", + "description": "Model Context Protocol server for Umami analytics.", + "author": "Umami Software, Inc. ", + "license": "MIT", + "type": "module", + "main": "./dist/index.js", + "types": "./dist/index.d.ts", + "bin": { + "umami-mcp": "./dist/cli.js" + }, + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js", + "require": "./dist/index.cjs" + } + }, + "files": ["dist", "README.md"], + "engines": { + "node": ">=20" + }, + "scripts": { + "build": "tsup", + "test": "vitest run", + "typecheck": "tsc --noEmit", + "start": "node dist/cli.js" + }, + "dependencies": { + "@modelcontextprotocol/server": "^2.0.0", + "@umami/api-client": "workspace:*", + "zod": "^4.4.3" + }, + "devDependencies": { + "@modelcontextprotocol/client": "^2.0.0", + "@types/node": "^26.4.0", + "tsup": "^8.5.0", + "typescript": "^6.0.3", + "vitest": "^4.1.11" + } +} diff --git a/packages/mcp/src/cli.ts b/packages/mcp/src/cli.ts new file mode 100644 index 000000000..928afb04d --- /dev/null +++ b/packages/mcp/src/cli.ts @@ -0,0 +1,9 @@ +#!/usr/bin/env node +import { serveUmamiStdio } from './stdio'; + +try { + serveUmamiStdio(); +} catch (error) { + process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`); + process.exit(1); +} diff --git a/packages/mcp/src/http.ts b/packages/mcp/src/http.ts new file mode 100644 index 000000000..59120548f --- /dev/null +++ b/packages/mcp/src/http.ts @@ -0,0 +1,56 @@ +import { + type AuthInfo, + createMcpHandler, + type McpHttpHandler, + type McpRequestContext, +} from '@modelcontextprotocol/server'; +import type { UmamiClient } from '@umami/api-client'; +import type { McpLogger } from './lib/logger'; +import type { AnyToolDefinition } from './lib/tool'; +import { createUmamiMcpServer } from './server'; + +export interface CreateUmamiMcpHttpHandlerOptions { + /** + * Builds the API client for one request. Receives the verified `AuthInfo` (bearer token, + * client ID, scopes) so the token can be propagated to the Umami API unchanged. + */ + createClient: (authInfo: AuthInfo, ctx: McpRequestContext) => UmamiClient | Promise; + logger?: McpLogger; + tools?: AnyToolDefinition[]; + onerror?: (error: Error) => void; +} + +/** + * Streamable HTTP handler (web-standard Request/Response) for the 2026-07-28 stateless protocol. + * The host is responsible for verifying the bearer token and passing `authInfo` to `handler.fetch`. + */ +export function createUmamiMcpHttpHandler( + options: CreateUmamiMcpHttpHandlerOptions, +): McpHttpHandler { + return createMcpHandler( + async ctx => { + const authInfo = ctx.authInfo; + + if (!authInfo) { + throw new Error( + 'MCP request is missing authInfo. Verify the bearer token before dispatch.', + ); + } + + const client = await options.createClient(authInfo, ctx); + const extra = (authInfo.extra ?? {}) as { userId?: string; requestId?: string }; + + return createUmamiMcpServer({ + client, + logger: options.logger, + tools: options.tools, + identity: { + userId: extra.userId, + clientId: authInfo.clientId, + requestId: extra.requestId, + }, + }); + }, + { onerror: options.onerror, legacy: 'stateless' }, + ); +} diff --git a/packages/mcp/src/index.ts b/packages/mcp/src/index.ts new file mode 100644 index 000000000..f1e2f05fb --- /dev/null +++ b/packages/mcp/src/index.ts @@ -0,0 +1,14 @@ +export type { AuthInfo, McpRequestContext } from '@modelcontextprotocol/server'; +export { type CreateUmamiMcpHttpHandlerOptions, createUmamiMcpHttpHandler } from './http'; +export { describeError, McpToolError, type McpToolErrorCode } from './lib/errors'; +export { type McpLogEvent, type McpLogger, silentLogger, stderrLogger } from './lib/logger'; +export type { AnyToolDefinition, ToolContext, ToolDefinition } from './lib/tool'; +export { + type CreateUmamiMcpServerOptions, + createUmamiMcpServer, + SERVER_INSTRUCTIONS, + SERVER_NAME, + SERVER_VERSION, +} from './server'; +export { resolveClientOptions, type StdioEnvironment, serveUmamiStdio } from './stdio'; +export { allTools, coreTools, reportTools } from './tools'; diff --git a/packages/mcp/src/lib/dates.ts b/packages/mcp/src/lib/dates.ts new file mode 100644 index 000000000..3466c12fc --- /dev/null +++ b/packages/mcp/src/lib/dates.ts @@ -0,0 +1,76 @@ +import { z } from 'zod'; +import { McpToolError } from './errors'; + +export const isoTimestamp = z + .string() + .min(4) + .describe('ISO 8601 date or timestamp, e.g. "2024-05-01" or "2024-05-01T00:00:00Z".'); + +export const timeUnit = z + .enum(['minute', 'hour', 'day', 'month', 'year']) + .describe( + 'Bucket size for time series. If the unit is too fine for the range, the API uses the smallest allowed unit.', + ); + +export const timezone = z + .string() + .describe('IANA timezone such as "America/Los_Angeles". Defaults to UTC.'); + +export const dateRangeInput = { + startAt: isoTimestamp.describe('Start of the range (inclusive), ISO 8601.'), + endAt: isoTimestamp + .optional() + .describe('End of the range (inclusive), ISO 8601. Defaults to now.'), +}; + +export function parseTimestamp(value: string, name: string): number { + const ms = Date.parse(value); + + if (Number.isNaN(ms)) { + throw new McpToolError( + 'invalid_date', + `"${name}" is not a valid ISO 8601 date: ${JSON.stringify(value)}. Use a value like 2024-05-01 or 2024-05-01T00:00:00Z.`, + ); + } + + return ms; +} + +export interface DateRange { + startAt: number; + endAt: number; +} + +export function parseDateRange(input: { startAt: string; endAt?: string }): DateRange { + const startAt = parseTimestamp(input.startAt, 'startAt'); + const endAt = input.endAt ? parseTimestamp(input.endAt, 'endAt') : Date.now(); + + if (endAt <= startAt) { + throw new McpToolError( + 'invalid_date_range', + `"endAt" (${new Date(endAt).toISOString()}) must be after "startAt" (${new Date(startAt).toISOString()}).`, + ); + } + + return { startAt, endAt }; +} + +export function toIso(value: number | string | Date | null | undefined) { + if (value === null || value === undefined) { + return null; + } + + const date = value instanceof Date ? value : new Date(value); + + return Number.isNaN(date.getTime()) ? null : date.toISOString(); +} + +/** + * Report endpoints take `startDate`/`endDate` ISO strings instead of millisecond timestamps. + */ +export function toReportDates(range: DateRange) { + return { + startDate: new Date(range.startAt).toISOString(), + endDate: new Date(range.endAt).toISOString(), + }; +} diff --git a/packages/mcp/src/lib/errors.ts b/packages/mcp/src/lib/errors.ts new file mode 100644 index 000000000..2528bba79 --- /dev/null +++ b/packages/mcp/src/lib/errors.ts @@ -0,0 +1,138 @@ +import { isUmamiApiError } from '@umami/api-client'; + +export type McpToolErrorCode = + | 'invalid_input' + | 'invalid_date' + | 'invalid_date_range' + | 'auth_expired' + | 'access_denied' + | 'not_found' + | 'rate_limited' + | 'api_error' + | 'network_error' + | 'unknown'; + +export class McpToolError extends Error { + readonly code: McpToolErrorCode; + + constructor(code: McpToolErrorCode, message: string) { + super(message); + this.name = 'McpToolError'; + this.code = code; + } +} + +export interface DescribedError { + code: McpToolErrorCode; + message: string; + status?: number; + details?: unknown; +} + +interface ErrorContext { + websiteId?: string; + sessionId?: string; + resource?: string; +} + +function resourceLabel(context: ErrorContext) { + if (context.sessionId) { + return `session ${context.sessionId}`; + } + + if (context.websiteId) { + return `website ${context.websiteId}`; + } + + return context.resource ?? 'this resource'; +} + +function sanitizeDetails(details: unknown) { + if (!details || typeof details !== 'object') { + return undefined; + } + + // Validation errors from the API are safe to surface; drop everything else. + const { + message: _message, + code: _code, + status: _status, + ...rest + } = details as Record; + + return Object.keys(rest).length ? rest : undefined; +} + +/** + * Converts any thrown error into a user-facing description. Never leaks stack traces, + * database details or tokens. + */ +export function describeError(error: unknown, context: ErrorContext = {}): DescribedError { + if (error instanceof McpToolError) { + return { code: error.code, message: error.message }; + } + + if (isUmamiApiError(error)) { + const label = resourceLabel(context); + const apiError = (error.body as { error?: Record } | undefined)?.error; + + switch (error.status) { + case 400: + return { + code: 'invalid_input', + status: 400, + message: `Umami rejected the request as invalid${error.message && error.message !== 'Bad request' ? `: ${error.message}` : ''}. Check the parameters and try again.`, + details: sanitizeDetails(apiError), + }; + case 401: + return { + code: context.websiteId || context.sessionId ? 'access_denied' : 'auth_expired', + status: 401, + message: + context.websiteId || context.sessionId + ? `You do not have access to ${label}, or your Umami authorization has expired. Use list_websites to see the websites you can access, and re-authorize if the problem persists.` + : 'Umami authentication failed or has expired. Re-authorize the connection and try again.', + }; + case 403: + return { + code: 'access_denied', + status: 403, + message: `You do not have permission to access ${label}${error.code === 'insufficient_scope' ? ' with the granted OAuth scopes' : ''}.`, + }; + case 404: + return { + code: 'not_found', + status: 404, + message: `${label[0].toUpperCase()}${label.slice(1)} was not found.`, + }; + case 429: + return { + code: 'rate_limited', + status: 429, + message: 'Umami is rate limiting requests. Wait a moment before trying again.', + }; + default: + return { + code: 'api_error', + status: error.status, + message: + error.status >= 500 + ? 'Umami returned a server error. Try again later.' + : `Umami request failed with status ${error.status}.`, + }; + } + } + + if (error instanceof TypeError && /fetch|network|ECONNREFUSED|ENOTFOUND/i.test(error.message)) { + return { + code: 'network_error', + message: 'Could not reach the Umami API. Check the server URL and network connectivity.', + }; + } + + if (error instanceof Error && error.name === 'AbortError') { + return { code: 'network_error', message: 'The request to Umami timed out.' }; + } + + return { code: 'unknown', message: 'An unexpected error occurred while calling Umami.' }; +} diff --git a/packages/mcp/src/lib/filters.ts b/packages/mcp/src/lib/filters.ts new file mode 100644 index 000000000..423d322a5 --- /dev/null +++ b/packages/mcp/src/lib/filters.ts @@ -0,0 +1,57 @@ +import { z } from 'zod'; + +const filterValue = z + .string() + .min(1) + .describe( + 'Exact match by default. Prefix with an operator for other comparisons: "neq.value" (not equals), "c.value" (contains), "dnc.value" (does not contain), "re.regex", "nre.regex". Comma-separate multiple exact values: "chrome,firefox".', + ); + +export const filtersSchema = z + .object({ + path: filterValue.optional().describe('Page path, e.g. "/pricing".'), + referrer: filterValue.optional().describe('Referrer domain, e.g. "google.com".'), + title: filterValue.optional().describe('Page title.'), + query: filterValue.optional().describe('URL query string.'), + hostname: filterValue.optional().describe('Hostname the page was served from.'), + event: filterValue.optional().describe('Custom event name.'), + tag: filterValue.optional().describe('Tracker tag.'), + browser: filterValue.optional().describe('Browser name, e.g. "chrome".'), + os: filterValue.optional().describe('Operating system, e.g. "Mac OS".'), + device: filterValue.optional().describe('Device type: desktop, laptop, tablet or mobile.'), + country: filterValue.optional().describe('ISO 3166-1 alpha-2 country code, e.g. "US".'), + region: filterValue.optional().describe('Region code, e.g. "US-CA".'), + city: filterValue.optional().describe('City name.'), + language: filterValue.optional().describe('Browser language, e.g. "en-US".'), + distinctId: filterValue.optional().describe('Identified user ID (distinct ID).'), + utmSource: filterValue.optional(), + utmMedium: filterValue.optional(), + utmCampaign: filterValue.optional(), + utmContent: filterValue.optional(), + utmTerm: filterValue.optional(), + segment: z.string().uuid().optional().describe('Saved segment ID to apply.'), + match: z + .enum(['all', 'any']) + .optional() + .describe('Whether all filters must match (default) or any filter may match.'), + }) + .strict() + .describe('Optional analytics filters. Only include keys you want to filter on.'); + +export type Filters = z.infer; + +export function toFilterParams(filters?: Filters): Record { + const params: Record = {}; + + if (!filters) { + return params; + } + + for (const [key, value] of Object.entries(filters)) { + if (value !== undefined && value !== null && value !== '') { + params[key] = String(value); + } + } + + return params; +} diff --git a/packages/mcp/src/lib/limits.ts b/packages/mcp/src/lib/limits.ts new file mode 100644 index 000000000..f3aa5bbb9 --- /dev/null +++ b/packages/mcp/src/lib/limits.ts @@ -0,0 +1,28 @@ +export const DEFAULT_PAGE_SIZE = 20; +export const MAX_PAGE_SIZE = 100; +export const DEFAULT_METRIC_LIMIT = 20; +export const MAX_METRIC_LIMIT = 500; +export const MAX_SESSION_ACTIVITY = 200; + +export function clamp(value: number | undefined, fallback: number, max: number) { + if (value === undefined || Number.isNaN(value)) { + return fallback; + } + + return Math.min(Math.max(Math.trunc(value), 1), max); +} + +export interface PageInfo { + page: number; + pageSize: number; + count: number; + hasMore: boolean; +} + +export function pageInfo(result: { page?: number; pageSize?: number; count?: number }): PageInfo { + const page = result.page ?? 1; + const pageSize = result.pageSize ?? DEFAULT_PAGE_SIZE; + const count = result.count ?? 0; + + return { page, pageSize, count, hasMore: page * pageSize < count }; +} diff --git a/packages/mcp/src/lib/logger.ts b/packages/mcp/src/lib/logger.ts new file mode 100644 index 000000000..c873b2100 --- /dev/null +++ b/packages/mcp/src/lib/logger.ts @@ -0,0 +1,33 @@ +export interface McpLogEvent { + event: 'tool_call'; + tool: string; + ok: boolean; + durationMs: number; + websiteId?: string; + errorCode?: string; + status?: number; + userId?: string; + clientId?: string; + requestId?: string; + [key: string]: unknown; +} + +export interface McpLogger { + info(event: McpLogEvent): void; + error(event: McpLogEvent): void; +} + +/** Writes JSON lines to stderr so stdio transports keep stdout clean. */ +export const stderrLogger: McpLogger = { + info(event) { + process.stderr.write(`${JSON.stringify(event)}\n`); + }, + error(event) { + process.stderr.write(`${JSON.stringify(event)}\n`); + }, +}; + +export const silentLogger: McpLogger = { + info() {}, + error() {}, +}; diff --git a/packages/mcp/src/lib/tool.ts b/packages/mcp/src/lib/tool.ts new file mode 100644 index 000000000..5d1414ab1 --- /dev/null +++ b/packages/mcp/src/lib/tool.ts @@ -0,0 +1,109 @@ +import type { CallToolResult, McpServer, ToolAnnotations } from '@modelcontextprotocol/server'; +import type { UmamiClient } from '@umami/api-client'; +import type { z } from 'zod'; +import { describeError } from './errors'; +import type { McpLogger } from './logger'; + +export interface ToolContext { + client: UmamiClient; + logger: McpLogger; + /** Identity attached to the connection (HTTP/OAuth mode). Used for logs only. */ + identity?: { userId?: string; clientId?: string; requestId?: string }; +} + +export interface ToolDefinition> { + name: string; + title: string; + description: string; + inputSchema: Schema; + annotations?: ToolAnnotations; + handler: (input: z.infer, context: ToolContext) => Promise>; +} + +export function defineTool>( + definition: ToolDefinition, +): ToolDefinition { + return definition; +} + +export type AnyToolDefinition = ToolDefinition>; + +const READ_ONLY: ToolAnnotations = { + readOnlyHint: true, + destructiveHint: false, + idempotentHint: true, + openWorldHint: false, +}; + +export function successResult(data: Record): CallToolResult { + return { + content: [{ type: 'text', text: JSON.stringify(data, null, 2) }], + structuredContent: data, + }; +} + +export function errorResult(error: ReturnType): CallToolResult { + return { + content: [{ type: 'text', text: `Error (${error.code}): ${error.message}` }], + structuredContent: { error }, + isError: true, + }; +} + +function getWebsiteId(input: unknown) { + const value = (input as { websiteId?: unknown })?.websiteId; + + return typeof value === 'string' ? value : undefined; +} + +function getSessionId(input: unknown) { + const value = (input as { sessionId?: unknown })?.sessionId; + + return typeof value === 'string' ? value : undefined; +} + +export function registerTool( + server: McpServer, + definition: AnyToolDefinition, + context: ToolContext, +) { + server.registerTool( + definition.name, + { + title: definition.title, + description: definition.description, + inputSchema: definition.inputSchema, + annotations: { ...READ_ONLY, ...definition.annotations }, + }, + async (args: Record) => { + const startedAt = Date.now(); + const websiteId = getWebsiteId(args); + const base = { + event: 'tool_call' as const, + tool: definition.name, + websiteId, + ...context.identity, + }; + + try { + const data = await definition.handler(args, context); + + context.logger.info({ ...base, ok: true, durationMs: Date.now() - startedAt }); + + return successResult(data); + } catch (error) { + const described = describeError(error, { websiteId, sessionId: getSessionId(args) }); + + context.logger.error({ + ...base, + ok: false, + durationMs: Date.now() - startedAt, + errorCode: described.code, + status: described.status, + }); + + return errorResult(described); + } + }, + ); +} diff --git a/packages/mcp/src/server.test.ts b/packages/mcp/src/server.test.ts new file mode 100644 index 000000000..ce2d9f3d0 --- /dev/null +++ b/packages/mcp/src/server.test.ts @@ -0,0 +1,433 @@ +import { Client } from '@modelcontextprotocol/client'; +import { InMemoryTransport } from '@modelcontextprotocol/server'; +import { UmamiClient } from '@umami/api-client'; +import { afterEach, beforeEach, describe, expect, test } from 'vitest'; +import type { McpLogEvent } from './lib/logger'; +import { createUmamiMcpServer } from './server'; + +const WEBSITE_ID = '6f2a7e0e-2b0f-4b3f-9f0a-1234567890ab'; +const SESSION_ID = '0b3b6c2e-1f4a-4d0c-9a5e-abcdefabcdef'; + +type Handler = (url: URL, init?: RequestInit) => { status?: number; body: unknown }; + +interface Call { + url: URL; + init?: RequestInit; +} + +function createHarness(handler: Handler) { + const calls: Call[] = []; + const logs: McpLogEvent[] = []; + const umami = new UmamiClient({ + baseUrl: 'https://example.com/api', + token: 'secret-token', + fetch: async (input, init) => { + const url = new URL(input.toString()); + calls.push({ url, init }); + const { status = 200, body } = handler(url, init); + + return new Response(JSON.stringify(body), { + status, + headers: { 'content-type': 'application/json' }, + }); + }, + }); + const server = createUmamiMcpServer({ + client: umami, + logger: { info: event => logs.push(event), error: event => logs.push(event) }, + }); + const client = new Client({ name: 'test', version: '0.0.0' }); + + return { server, client, calls, logs }; +} + +async function connect(harness: ReturnType) { + const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); + + await harness.server.connect(serverTransport); + await harness.client.connect(clientTransport); +} + +describe('createUmamiMcpServer', () => { + let harness: ReturnType; + + const routes: Handler = url => { + const path = url.pathname; + + if (path === '/api/websites') { + return { + body: { + data: [ + { id: WEBSITE_ID, name: 'Umami', domain: 'umami.is', teamId: null, createdAt: null }, + ], + count: 1, + page: 1, + pageSize: 20, + }, + }; + } + + if (path === `/api/websites/${WEBSITE_ID}/stats`) { + return { + body: { + pageviews: 100, + visitors: 40, + visits: 50, + bounces: 10, + totaltime: 5000, + comparison: { pageviews: 80, visitors: 30, visits: 40, bounces: 8, totaltime: 4000 }, + }, + }; + } + + if (path === `/api/websites/${WEBSITE_ID}/pageviews`) { + return { + body: { + pageviews: [{ x: '2024-01-01', y: 10 }], + sessions: [{ x: '2024-01-01', y: 5 }], + }, + }; + } + + if (path === `/api/websites/${WEBSITE_ID}/metrics`) { + return { + body: [ + { x: '/', y: 50 }, + { x: '/docs', y: 20 }, + ], + }; + } + + if (path === `/api/websites/${WEBSITE_ID}/active`) { + return { body: { visitors: 7 } }; + } + + if (path === `/api/websites/${WEBSITE_ID}/events`) { + return { + body: { + data: [ + { + id: 'e1', + sessionId: SESSION_ID, + createdAt: '2024-01-01T00:00:00.000Z', + eventName: 'signup', + eventType: 2, + urlPath: '/signup', + }, + ], + count: 250, + page: Number(url.searchParams.get('page') ?? 1), + pageSize: Number(url.searchParams.get('pageSize') ?? 20), + }, + }; + } + + if (path === `/api/websites/${WEBSITE_ID}/sessions`) { + return { + body: { + data: [{ id: SESSION_ID, browser: 'chrome', firstAt: '2024-01-01T00:00:00.000Z' }], + count: 1, + page: 1, + pageSize: 20, + }, + }; + } + + if (path === `/api/websites/${WEBSITE_ID}/sessions/${SESSION_ID}`) { + return { + body: { + id: SESSION_ID, + browser: 'chrome', + firstAt: '2024-01-01T00:00:00.000Z', + lastAt: '2024-01-01T01:00:00.000Z', + visits: 1, + views: 3, + }, + }; + } + + if (path === `/api/websites/${WEBSITE_ID}/sessions/${SESSION_ID}/activity`) { + return { + body: [ + { createdAt: '2024-01-01T00:00:00.000Z', urlPath: '/', eventType: 1 }, + { createdAt: '2024-01-01T00:01:00.000Z', eventName: 'signup', eventType: 2 }, + ], + }; + } + + if (path === `/api/websites/${WEBSITE_ID}/sessions/${SESSION_ID}/properties`) { + return { body: [{ dataKey: 'plan', stringValue: 'pro' }] }; + } + + if (path === '/api/reports/funnel') { + return { + body: [ + { + type: 'path', + value: '/', + visitors: 10, + previous: 0, + dropped: 0, + dropoff: 0, + remaining: 1, + }, + { + type: 'event', + value: 'signup', + visitors: 2, + previous: 10, + dropped: 8, + dropoff: 0.8, + remaining: 0.2, + }, + ], + }; + } + + if (path.startsWith('/api/websites/')) { + return { + status: 401, + body: { error: { message: 'Unauthorized', code: 'unauthorized', status: 401 } }, + }; + } + + return { + status: 404, + body: { error: { message: 'Not found', code: 'not-found', status: 404 } }, + }; + }; + + beforeEach(async () => { + harness = createHarness(routes); + await connect(harness); + }); + + afterEach(async () => { + await harness.client.close(); + await harness.server.close(); + }); + + test('registers the expected read-only tools', async () => { + const { tools } = await harness.client.listTools(); + const names = tools.map(tool => tool.name).sort(); + + expect(names).toEqual( + [ + 'list_websites', + 'get_website_stats', + 'get_website_traffic', + 'get_website_metrics', + 'get_realtime', + 'get_events', + 'get_sessions', + 'get_session', + 'run_funnel', + 'run_journey', + 'run_retention', + 'run_attribution', + 'get_revenue', + ].sort(), + ); + + for (const tool of tools) { + expect(tool.annotations?.readOnlyHint).toBe(true); + expect(tool.annotations?.destructiveHint).toBe(false); + expect(tool.description?.length ?? 0).toBeGreaterThan(40); + } + }); + + test('list_websites returns structured websites and pagination', async () => { + const result = await harness.client.callTool({ name: 'list_websites', arguments: {} }); + + expect(result.isError).toBeFalsy(); + expect(result.structuredContent).toMatchObject({ + websites: [{ id: WEBSITE_ID, name: 'Umami', domain: 'umami.is' }], + page: 1, + count: 1, + hasMore: false, + }); + expect(harness.calls[0].url.searchParams.get('includeTeams')).toBe('true'); + expect( + (harness.calls[0].init?.headers as Record | undefined)?.authorization, + ).toBe('Bearer secret-token'); + }); + + test('get_website_stats converts ISO dates to timestamps and summarizes both periods', async () => { + const result = await harness.client.callTool({ + name: 'get_website_stats', + arguments: { + websiteId: WEBSITE_ID, + startAt: '2024-01-01T00:00:00Z', + endAt: '2024-01-08T00:00:00Z', + filters: { browser: 'chrome' }, + }, + }); + + expect(result.isError).toBeFalsy(); + expect(result.structuredContent).toMatchObject({ + current: { pageviews: 100, visitors: 40, visits: 50, bounceRate: 20 }, + previous: { pageviews: 80, visitors: 30 }, + }); + + const url = harness.calls[0].url; + + expect(url.searchParams.get('startAt')).toBe(String(Date.parse('2024-01-01T00:00:00Z'))); + expect(url.searchParams.get('endAt')).toBe(String(Date.parse('2024-01-08T00:00:00Z'))); + expect(url.searchParams.get('browser')).toBe('chrome'); + }); + + test('rejects invalid date ranges without calling the API', async () => { + const result = await harness.client.callTool({ + name: 'get_website_stats', + arguments: { websiteId: WEBSITE_ID, startAt: '2024-02-01', endAt: '2024-01-01' }, + }); + + expect(result.isError).toBe(true); + expect(result.structuredContent).toMatchObject({ error: { code: 'invalid_date_range' } }); + expect(harness.calls).toHaveLength(0); + }); + + test('get_website_traffic returns series with friendly keys', async () => { + const result = await harness.client.callTool({ + name: 'get_website_traffic', + arguments: { websiteId: WEBSITE_ID, startAt: '2024-01-01', unit: 'day' }, + }); + + expect(result.structuredContent).toMatchObject({ + pageviews: [{ date: '2024-01-01', value: 10 }], + visits: [{ date: '2024-01-01', value: 5 }], + }); + }); + + test('get_website_metrics enforces limits and maps rows', async () => { + const result = await harness.client.callTool({ + name: 'get_website_metrics', + arguments: { websiteId: WEBSITE_ID, startAt: '2024-01-01', type: 'path', limit: 500 }, + }); + + expect(result.structuredContent).toMatchObject({ + type: 'path', + measure: 'views', + data: [ + { name: '/', value: 50 }, + { name: '/docs', value: 20 }, + ], + }); + expect(harness.calls[0].url.searchParams.get('limit')).toBe('500'); + + const tooMany = await harness.client.callTool({ + name: 'get_website_metrics', + arguments: { websiteId: WEBSITE_ID, startAt: '2024-01-01', type: 'path', limit: 5000 }, + }); + + expect(tooMany.isError).toBe(true); + }); + + test('get_realtime returns the active visitor count', async () => { + const result = await harness.client.callTool({ + name: 'get_realtime', + arguments: { websiteId: WEBSITE_ID }, + }); + + expect(result.structuredContent).toMatchObject({ activeVisitors: 7 }); + }); + + test('get_events paginates and clamps page size', async () => { + const result = await harness.client.callTool({ + name: 'get_events', + arguments: { + websiteId: WEBSITE_ID, + startAt: '2024-01-01', + event: 'signup', + page: 2, + pageSize: 100, + }, + }); + + expect(result.structuredContent).toMatchObject({ + events: [{ name: 'signup', type: 'event', path: '/signup', sessionId: SESSION_ID }], + page: 2, + pageSize: 100, + count: 250, + hasMore: true, + }); + expect(harness.calls[0].url.searchParams.get('event')).toBe('signup'); + }); + + test('get_sessions and get_session return visitor details', async () => { + const sessions = await harness.client.callTool({ + name: 'get_sessions', + arguments: { websiteId: WEBSITE_ID, startAt: '2024-01-01' }, + }); + + expect(sessions.structuredContent).toMatchObject({ + sessions: [{ id: SESSION_ID, browser: 'chrome' }], + }); + + const session = await harness.client.callTool({ + name: 'get_session', + arguments: { websiteId: WEBSITE_ID, sessionId: SESSION_ID }, + }); + + expect(session.structuredContent).toMatchObject({ + session: { id: SESSION_ID, views: 3 }, + properties: { plan: 'pro' }, + activity: [ + { type: 'pageview', path: '/' }, + { type: 'event', name: 'signup' }, + ], + }); + }); + + test('run_funnel posts a typed report body', async () => { + const result = await harness.client.callTool({ + name: 'run_funnel', + arguments: { + websiteId: WEBSITE_ID, + startAt: '2024-01-01', + endAt: '2024-01-31', + steps: [ + { type: 'path', value: '/' }, + { type: 'event', value: 'signup' }, + ], + }, + }); + + expect(result.isError).toBeFalsy(); + expect(result.structuredContent).toMatchObject({ + steps: [ + { step: 1, visitors: 10 }, + { step: 2, visitors: 2, dropped: 8 }, + ], + }); + + const body = JSON.parse(harness.calls[0].init?.body as string); + + expect(body).toMatchObject({ websiteId: WEBSITE_ID, type: 'funnel' }); + expect(body.parameters.steps).toHaveLength(2); + expect(body.parameters.window).toBe(60); + }); + + test('maps unauthorized website access to a helpful error', async () => { + const otherWebsite = '11111111-2222-4333-8444-555555555555'; + const result = await harness.client.callTool({ + name: 'get_realtime', + arguments: { websiteId: otherWebsite }, + }); + + expect(result.isError).toBe(true); + expect(result.structuredContent).toMatchObject({ + error: { code: 'access_denied', status: 401 }, + }); + expect((result.content[0] as { text: string }).text).toContain(otherWebsite); + expect((result.content[0] as { text: string }).text).not.toContain('secret-token'); + }); + + test('logs tool calls without leaking tokens', async () => { + await harness.client.callTool({ name: 'list_websites', arguments: {} }); + + expect(harness.logs).toHaveLength(1); + expect(harness.logs[0]).toMatchObject({ event: 'tool_call', tool: 'list_websites', ok: true }); + expect(JSON.stringify(harness.logs)).not.toContain('secret-token'); + }); +}); diff --git a/packages/mcp/src/server.ts b/packages/mcp/src/server.ts new file mode 100644 index 000000000..164fd621d --- /dev/null +++ b/packages/mcp/src/server.ts @@ -0,0 +1,49 @@ +import { McpServer } from '@modelcontextprotocol/server'; +import type { UmamiClient } from '@umami/api-client'; +import { type McpLogger, silentLogger } from './lib/logger'; +import { type AnyToolDefinition, registerTool, type ToolContext } from './lib/tool'; +import { allTools } from './tools'; + +export const SERVER_NAME = 'umami'; +export const SERVER_VERSION = '1.0.0'; + +export const SERVER_INSTRUCTIONS = `Umami is a privacy-focused web analytics platform. These tools answer questions about website traffic, visitors, pages, referrers, events, sessions and conversion reports. + +Workflow: +1. Call list_websites to find the websiteId for the site the user is asking about (match by name or domain). +2. Use get_website_stats for totals, get_website_traffic for trends over time, get_website_metrics for rankings (top pages, referrers, countries, browsers, campaigns, events), get_realtime for current visitors, get_events / get_sessions / get_session to inspect individual activity, and run_funnel / run_journey / run_retention / run_attribution / get_revenue for reports. + +Dates are ISO 8601 strings; endAt defaults to now. Results are paginated where noted — request another page rather than a huge pageSize. All tools are read-only.`; + +export interface CreateUmamiMcpServerOptions { + /** API client carrying the caller's credentials. The MCP server never touches storage directly. */ + client: UmamiClient; + /** Structured logger. Defaults to silent; the stdio CLI logs to stderr. */ + logger?: McpLogger; + /** Identity for log correlation (never used for authorization). */ + identity?: ToolContext['identity']; + /** Override the registered tool set (defaults to all read-only tools). */ + tools?: AnyToolDefinition[]; +} + +/** + * Creates an MCP server whose tools call the Umami API through `@umami/api-client`. + * The same factory backs both the stdio CLI and the embedded HTTP endpoint. + */ +export function createUmamiMcpServer(options: CreateUmamiMcpServerOptions) { + const server = new McpServer( + { name: SERVER_NAME, version: SERVER_VERSION }, + { instructions: SERVER_INSTRUCTIONS }, + ); + const context: ToolContext = { + client: options.client, + logger: options.logger ?? silentLogger, + identity: options.identity, + }; + + for (const tool of options.tools ?? allTools) { + registerTool(server, tool, context); + } + + return server; +} diff --git a/packages/mcp/src/stdio.test.ts b/packages/mcp/src/stdio.test.ts new file mode 100644 index 000000000..648f601b6 --- /dev/null +++ b/packages/mcp/src/stdio.test.ts @@ -0,0 +1,24 @@ +import { describe, expect, test } from 'vitest'; +import { resolveClientOptions } from './stdio'; + +describe('resolveClientOptions', () => { + test('appends /api to a self-hosted instance URL', () => { + expect( + resolveClientOptions({ UMAMI_URL: 'https://analytics.example.com/', UMAMI_API_TOKEN: 't' }), + ).toEqual({ baseUrl: 'https://analytics.example.com/api', token: 't', apiKey: undefined }); + }); + + test('uses UMAMI_API_URL verbatim', () => { + expect( + resolveClientOptions({ UMAMI_API_URL: 'https://api.umami.is/v1', UMAMI_API_KEY: 'k' }), + ).toEqual({ baseUrl: 'https://api.umami.is/v1', token: undefined, apiKey: 'k' }); + }); + + test('defaults to Cloud when only an API key is provided', () => { + expect(resolveClientOptions({ UMAMI_API_KEY: 'k' }).baseUrl).toBeUndefined(); + }); + + test('requires credentials', () => { + expect(() => resolveClientOptions({})).toThrow(/UMAMI_API_TOKEN/); + }); +}); diff --git a/packages/mcp/src/stdio.ts b/packages/mcp/src/stdio.ts new file mode 100644 index 000000000..8d4b3d6f9 --- /dev/null +++ b/packages/mcp/src/stdio.ts @@ -0,0 +1,61 @@ +import { serveStdio } from '@modelcontextprotocol/server/stdio'; +import { UmamiClient, type UmamiClientOptions } from '@umami/api-client'; +import { type McpLogger, stderrLogger } from './lib/logger'; +import { createUmamiMcpServer } from './server'; + +export interface StdioEnvironment { + UMAMI_URL?: string; + UMAMI_API_URL?: string; + UMAMI_API_TOKEN?: string; + UMAMI_API_KEY?: string; + [key: string]: string | undefined; +} + +function trimSlashes(value: string) { + return value.replace(/\/+$/, ''); +} + +/** + * Resolves client options from environment variables: + * + * - `UMAMI_API_URL` — full API base URL (e.g. `https://api.umami.is/v1`, `https://example.com/api`). + * - `UMAMI_URL` — self-hosted instance URL; `/api` is appended automatically. + * - `UMAMI_API_TOKEN` — bearer token (login token or self-hosted API key). + * - `UMAMI_API_KEY` — Umami Cloud API key. + */ +export function resolveClientOptions(env: StdioEnvironment = process.env): UmamiClientOptions { + const apiUrl = env.UMAMI_API_URL?.trim(); + const instanceUrl = env.UMAMI_URL?.trim(); + const token = env.UMAMI_API_TOKEN?.trim(); + const apiKey = env.UMAMI_API_KEY?.trim(); + + if (!token && !apiKey) { + throw new Error( + 'Missing credentials. Set UMAMI_API_TOKEN (self-hosted API key or login token) or UMAMI_API_KEY (Umami Cloud).', + ); + } + + let baseUrl: string | undefined; + + if (apiUrl) { + baseUrl = trimSlashes(apiUrl); + } else if (instanceUrl) { + const trimmed = trimSlashes(instanceUrl); + baseUrl = /\/api$/.test(trimmed) ? trimmed : `${trimmed}/api`; + } + + return { baseUrl, token, apiKey }; +} + +export interface ServeStdioOptions { + env?: StdioEnvironment; + logger?: McpLogger; +} + +/** Serves the Umami MCP server over stdio for local agents (Claude Desktop, Cursor, …). */ +export function serveUmamiStdio(options: ServeStdioOptions = {}) { + const client = new UmamiClient(resolveClientOptions(options.env)); + const logger = options.logger ?? stderrLogger; + + return serveStdio(() => createUmamiMcpServer({ client, logger })); +} diff --git a/packages/mcp/src/tools/events.ts b/packages/mcp/src/tools/events.ts new file mode 100644 index 000000000..1fdd79732 --- /dev/null +++ b/packages/mcp/src/tools/events.ts @@ -0,0 +1,92 @@ +import { z } from 'zod'; +import { dateRangeInput, parseDateRange, toIso } from '../lib/dates'; +import { filtersSchema, toFilterParams } from '../lib/filters'; +import { clamp, DEFAULT_PAGE_SIZE, MAX_PAGE_SIZE, pageInfo } from '../lib/limits'; +import { defineTool } from '../lib/tool'; + +interface EventRow { + id?: string; + sessionId?: string; + createdAt?: string | Date; + eventName?: string | null; + eventType?: number; + urlPath?: string; + hostname?: string; + referrerDomain?: string | null; + pageTitle?: string | null; + browser?: string; + os?: string; + device?: string; + country?: string; + distinctId?: string | null; + [key: string]: unknown; +} + +export const getEvents = defineTool({ + name: 'get_events', + title: 'Get tracked events', + description: + 'Lists individual tracked events (pageviews and custom events) for a website during a time range, newest first. ' + + 'Each event includes its name, page path, session ID, timestamp and visitor attributes. ' + + 'Set "event" to only return a specific custom event name, or "search" for a free-text match. ' + + 'To count events by name instead, use get_website_metrics with type "event". Results are paginated. ' + + 'Requires a websiteId from list_websites.', + inputSchema: z.object({ + websiteId: z.string().uuid().describe('Website ID from list_websites.'), + ...dateRangeInput, + event: z.string().optional().describe('Custom event name to filter on, e.g. "signup".'), + search: z.string().optional().describe('Free-text search across event names and URLs.'), + page: z.number().int().positive().optional().describe('Page number, starting at 1.'), + pageSize: z + .number() + .int() + .positive() + .max(MAX_PAGE_SIZE) + .optional() + .describe(`Results per page (default ${DEFAULT_PAGE_SIZE}, max ${MAX_PAGE_SIZE}).`), + filters: filtersSchema.optional(), + }), + async handler(input, { client }) { + const range = parseDateRange(input); + const filters = toFilterParams(input.filters); + + if (input.event) { + filters.event = input.event; + } + + const result = (await client.getWebsiteEvents({ + websiteId: input.websiteId, + startAt: range.startAt, + endAt: range.endAt, + search: input.search, + page: input.page ?? 1, + pageSize: clamp(input.pageSize, DEFAULT_PAGE_SIZE, MAX_PAGE_SIZE), + ...filters, + })) as { data?: EventRow[]; count?: number; page?: number; pageSize?: number }; + + return { + websiteId: input.websiteId, + range: { + startAt: new Date(range.startAt).toISOString(), + endAt: new Date(range.endAt).toISOString(), + }, + events: (result?.data ?? []).map(row => ({ + id: row.id, + sessionId: row.sessionId, + createdAt: toIso(row.createdAt as string), + type: row.eventType === 2 ? 'event' : row.eventType === 1 ? 'pageview' : row.eventType, + name: row.eventName ?? null, + path: row.urlPath, + title: row.pageTitle ?? null, + hostname: row.hostname, + referrer: row.referrerDomain ?? null, + browser: row.browser, + os: row.os, + device: row.device, + country: row.country, + distinctId: row.distinctId ?? null, + })), + ...pageInfo(result ?? {}), + }; + }, +}); diff --git a/packages/mcp/src/tools/index.ts b/packages/mcp/src/tools/index.ts new file mode 100644 index 000000000..8e07adbd1 --- /dev/null +++ b/packages/mcp/src/tools/index.ts @@ -0,0 +1,49 @@ +import type { AnyToolDefinition } from '../lib/tool'; +import { getEvents } from './events'; +import { getWebsiteMetrics } from './metrics'; +import { getRealtime } from './realtime'; +import { getRevenue, runAttribution, runFunnel, runJourney, runRetention } from './reports'; +import { getSession } from './session'; +import { getSessions } from './sessions'; +import { getWebsiteStats } from './stats'; +import { getWebsiteTraffic } from './traffic'; +import { listWebsites } from './websites'; + +/** Core read-only analytics tools. */ +export const coreTools: AnyToolDefinition[] = [ + listWebsites, + getWebsiteStats, + getWebsiteTraffic, + getWebsiteMetrics, + getRealtime, + getEvents, + getSessions, + getSession, +]; + +/** Higher-level report tools. */ +export const reportTools: AnyToolDefinition[] = [ + runFunnel, + runJourney, + runRetention, + runAttribution, + getRevenue, +]; + +export const allTools: AnyToolDefinition[] = [...coreTools, ...reportTools]; + +export { + getEvents, + getRealtime, + getRevenue, + getSession, + getSessions, + getWebsiteMetrics, + getWebsiteStats, + getWebsiteTraffic, + listWebsites, + runAttribution, + runFunnel, + runJourney, + runRetention, +}; diff --git a/packages/mcp/src/tools/metrics.ts b/packages/mcp/src/tools/metrics.ts new file mode 100644 index 000000000..00de257e4 --- /dev/null +++ b/packages/mcp/src/tools/metrics.ts @@ -0,0 +1,148 @@ +import { z } from 'zod'; +import { dateRangeInput, parseDateRange, timezone } from '../lib/dates'; +import { filtersSchema, toFilterParams } from '../lib/filters'; +import { clamp, DEFAULT_METRIC_LIMIT, MAX_METRIC_LIMIT } from '../lib/limits'; +import { defineTool } from '../lib/tool'; + +export const METRIC_TYPES = [ + 'path', + 'entry', + 'exit', + 'title', + 'query', + 'hostname', + 'referrer', + 'domain', + 'channel', + 'event', + 'tag', + 'browser', + 'os', + 'device', + 'screen', + 'language', + 'country', + 'region', + 'city', + 'distinctId', + 'utmSource', + 'utmMedium', + 'utmCampaign', + 'utmContent', + 'utmTerm', +] as const; + +const METRIC_DESCRIPTIONS: Record<(typeof METRIC_TYPES)[number], string> = { + path: 'top pages (URL paths) by pageviews', + entry: 'entry pages (first page of a visit)', + exit: 'exit pages (last page of a visit)', + title: 'page titles', + query: 'URL query strings', + hostname: 'hostnames', + referrer: 'referrer domains (traffic sources)', + domain: 'referrer domains, same as referrer', + channel: 'marketing channels (direct, organic search, social, referral, email, paid…)', + event: 'custom event names by count', + tag: 'tracker tags', + browser: 'browsers', + os: 'operating systems', + device: 'device types (desktop, laptop, tablet, mobile)', + screen: 'screen sizes', + language: 'browser languages', + country: 'countries (ISO codes)', + region: 'regions', + city: 'cities', + distinctId: 'identified users (distinct IDs)', + utmSource: 'UTM sources', + utmMedium: 'UTM mediums', + utmCampaign: 'UTM campaigns', + utmContent: 'UTM content values', + utmTerm: 'UTM terms', +}; + +const VISITOR_TYPES = new Set([ + 'browser', + 'os', + 'device', + 'screen', + 'language', + 'country', + 'region', + 'city', + 'distinctId', + 'referrer', + 'domain', + 'channel', + 'utmSource', + 'utmMedium', + 'utmCampaign', + 'utmContent', + 'utmTerm', +]); + +interface MetricRow { + x: string | null; + y: number; +} + +export const getWebsiteMetrics = defineTool({ + name: 'get_website_metrics', + title: 'Get ranked website metrics', + description: + 'Returns a ranked breakdown of website traffic for one dimension during a time range: ' + + 'top pages, referrers, channels, countries, browsers, devices, UTM campaigns, custom events, and more. ' + + 'The "value" for page-type dimensions (path, entry, exit, title, query, hostname, event, tag) counts pageviews/events; ' + + 'for visitor dimensions (browser, os, device, country, referrer, channel, utm*, …) it counts unique visitors. ' + + 'Available types: ' + + METRIC_TYPES.map(type => `${type} (${METRIC_DESCRIPTIONS[type]})`).join('; ') + + '. Requires a websiteId from list_websites.', + inputSchema: z.object({ + websiteId: z.string().uuid().describe('Website ID from list_websites.'), + ...dateRangeInput, + type: z.enum(METRIC_TYPES).describe('Dimension to rank.'), + limit: z + .number() + .int() + .positive() + .max(MAX_METRIC_LIMIT) + .optional() + .describe( + `Maximum rows to return (default ${DEFAULT_METRIC_LIMIT}, max ${MAX_METRIC_LIMIT}).`, + ), + offset: z.number().int().nonnegative().optional().describe('Rows to skip, for paging.'), + search: z.string().optional().describe('Only include values containing this text.'), + timezone: timezone.optional(), + filters: filtersSchema.optional(), + }), + async handler(input, { client }) { + const range = parseDateRange(input); + const limit = clamp(input.limit, DEFAULT_METRIC_LIMIT, MAX_METRIC_LIMIT); + const rows = (await client.getWebsiteMetrics({ + websiteId: input.websiteId, + startAt: range.startAt, + endAt: range.endAt, + type: input.type, + limit, + offset: input.offset, + search: input.search, + timezone: input.timezone, + ...toFilterParams(input.filters), + })) as MetricRow[]; + + return { + websiteId: input.websiteId, + type: input.type, + measure: VISITOR_TYPES.has(input.type) ? 'visitors' : 'views', + range: { + startAt: new Date(range.startAt).toISOString(), + endAt: new Date(range.endAt).toISOString(), + }, + limit, + offset: input.offset ?? 0, + data: (Array.isArray(rows) ? rows : []).map(row => ({ + name: row.x ?? '(none)', + value: Number(row.y ?? 0), + })), + }; + }, +}); diff --git a/packages/mcp/src/tools/realtime.ts b/packages/mcp/src/tools/realtime.ts new file mode 100644 index 000000000..7ad5e1689 --- /dev/null +++ b/packages/mcp/src/tools/realtime.ts @@ -0,0 +1,23 @@ +import { z } from 'zod'; +import { defineTool } from '../lib/tool'; + +export const getRealtime = defineTool({ + name: 'get_realtime', + title: 'Get current active visitors', + description: + 'Returns the number of visitors currently active on a website (active within the last few minutes). ' + + 'Use this for "how many people are on the site right now?". Requires a websiteId from list_websites.', + inputSchema: z.object({ + websiteId: z.string().uuid().describe('Website ID from list_websites.'), + }), + annotations: { idempotentHint: false }, + async handler(input, { client }) { + const result = await client.getWebsiteActive({ websiteId: input.websiteId }); + + return { + websiteId: input.websiteId, + activeVisitors: Number(result?.visitors ?? 0), + asOf: new Date().toISOString(), + }; + }, +}); diff --git a/packages/mcp/src/tools/reports.ts b/packages/mcp/src/tools/reports.ts new file mode 100644 index 000000000..1bba6bbbb --- /dev/null +++ b/packages/mcp/src/tools/reports.ts @@ -0,0 +1,289 @@ +import { z } from 'zod'; +import { dateRangeInput, parseDateRange, timeUnit, timezone, toReportDates } from '../lib/dates'; +import { filtersSchema, toFilterParams } from '../lib/filters'; +import { defineTool } from '../lib/tool'; + +const websiteId = z.string().uuid().describe('Website ID from list_websites.'); + +function reportFilters( + range: { startAt: number; endAt: number }, + filters?: z.infer, +) { + return { startAt: range.startAt, endAt: range.endAt, ...toFilterParams(filters) }; +} + +function isoRange(range: { startAt: number; endAt: number }) { + return { + startAt: new Date(range.startAt).toISOString(), + endAt: new Date(range.endAt).toISOString(), + }; +} + +export const runFunnel = defineTool({ + name: 'run_funnel', + title: 'Run a funnel report', + description: + 'Calculates how many visitors progressed through an ordered sequence of 2–8 steps (page paths or custom events) ' + + 'within a time window, and where they dropped off. Use this for conversion questions such as ' + + '"how many visitors who viewed /pricing went on to sign up?". Requires a websiteId from list_websites.', + inputSchema: z.object({ + websiteId, + ...dateRangeInput, + steps: z + .array( + z.object({ + type: z + .enum(['path', 'event']) + .describe('"path" for a page path, "event" for a custom event name.'), + value: z + .string() + .min(1) + .describe( + 'The page path (e.g. "/pricing") or event name (e.g. "signup"). Use * as a wildcard.', + ), + }), + ) + .min(2) + .max(8) + .describe('Ordered funnel steps.'), + windowMinutes: z + .number() + .int() + .positive() + .max(60 * 24 * 30) + .optional() + .describe('Maximum minutes allowed between the first and each following step (default 60).'), + filters: filtersSchema.optional(), + }), + async handler(input, { client }) { + const range = parseDateRange(input); + const result = await client.runFunnelReport({ + websiteId: input.websiteId, + type: 'funnel', + filters: reportFilters(range, input.filters), + parameters: { + ...toReportDates(range), + window: input.windowMinutes ?? 60, + steps: input.steps, + }, + }); + + return { + websiteId: input.websiteId, + range: isoRange(range), + windowMinutes: input.windowMinutes ?? 60, + steps: (Array.isArray(result) ? result : []).map((step, index) => ({ + step: index + 1, + type: step.type, + value: step.value, + visitors: Number(step.visitors ?? 0), + previous: Number(step.previous ?? 0), + dropped: Number(step.dropped ?? 0), + dropoffRate: Number(step.dropoff ?? 0), + remainingRate: Number(step.remaining ?? 0), + })), + }; + }, +}); + +export const runJourney = defineTool({ + name: 'run_journey', + title: 'Run a journey report', + description: + 'Returns the most common paths visitors take through a website as sequences of pages/events (up to 7 steps), ' + + 'with the number of visitors following each sequence. Optionally anchor the journey on a start or end step. ' + + 'Use this for "what do visitors do after landing on the homepage?". Requires a websiteId from list_websites.', + inputSchema: z.object({ + websiteId, + ...dateRangeInput, + steps: z + .number() + .int() + .min(2) + .max(7) + .optional() + .describe('Number of steps per journey (default 3).'), + startStep: z + .string() + .optional() + .describe('Only journeys starting with this page path or event.'), + endStep: z.string().optional().describe('Only journeys ending with this page path or event.'), + filters: filtersSchema.optional(), + }), + async handler(input, { client }) { + const range = parseDateRange(input); + const result = await client.runJourneyReport({ + websiteId: input.websiteId, + type: 'journey', + filters: reportFilters(range, input.filters), + parameters: { + ...toReportDates(range), + steps: input.steps ?? 3, + startStep: input.startStep, + endStep: input.endStep, + }, + }); + + return { + websiteId: input.websiteId, + range: isoRange(range), + journeys: (Array.isArray(result) ? result : []).map(row => { + const record = row as { items?: unknown[]; count?: number; visitors?: number }; + + return { + steps: record.items ?? [], + visitors: Number(record.count ?? record.visitors ?? 0), + }; + }), + }; + }, +}); + +export const runRetention = defineTool({ + name: 'run_retention', + title: 'Run a retention report', + description: + 'Returns a cohort retention table: for each day visitors first arrived, the percentage that returned on later days. ' + + 'Use this for "how many visitors come back after their first visit?". Requires a websiteId from list_websites.', + inputSchema: z.object({ + websiteId, + ...dateRangeInput, + timezone: timezone.optional(), + filters: filtersSchema.optional(), + }), + async handler(input, { client }) { + const range = parseDateRange(input); + const result = await client.runRetentionReport({ + websiteId: input.websiteId, + type: 'retention', + filters: reportFilters(range, input.filters), + parameters: { ...toReportDates(range), timezone: input.timezone }, + }); + + return { + websiteId: input.websiteId, + range: isoRange(range), + cohorts: (Array.isArray(result) ? result : []).map(row => { + const record = row as { + date?: string; + day?: number; + visitors?: number; + returnVisitors?: number; + percentage?: number; + }; + + return { + cohortDate: record.date ?? null, + day: Number(record.day ?? 0), + visitors: Number(record.visitors ?? 0), + returningVisitors: Number(record.returnVisitors ?? 0), + retentionRate: Number(record.percentage ?? 0), + }; + }), + }; + }, +}); + +export const runAttribution = defineTool({ + name: 'run_attribution', + title: 'Run an attribution report', + description: + 'Attributes conversions (a target page path or custom event) to the referrers, paid ads and UTM sources, mediums, ' + + 'campaigns, content and terms that brought the converting visitors, using first-click or last-click attribution. ' + + 'Use this for "which channels drive signups?". Requires a websiteId from list_websites.', + inputSchema: z.object({ + websiteId, + ...dateRangeInput, + model: z + .enum(['first-click', 'last-click']) + .optional() + .describe('Attribution model (default last-click).'), + conversionType: z + .enum(['path', 'event']) + .describe('Whether the conversion is a page path or a custom event.'), + conversion: z + .string() + .min(1) + .describe('The conversion page path (e.g. "/thank-you") or event name (e.g. "signup").'), + filters: filtersSchema.optional(), + }), + async handler(input, { client }) { + const range = parseDateRange(input); + const result = (await client.runAttributionReport({ + websiteId: input.websiteId, + type: 'attribution', + filters: reportFilters(range, input.filters), + parameters: { + ...toReportDates(range), + model: input.model ?? 'last-click', + type: input.conversionType, + step: input.conversion, + }, + })) as Record; + + const list = (value: unknown) => + (Array.isArray(value) ? value : []).map(item => { + const record = item as { name?: string; value?: number }; + + return { name: record.name ?? '(none)', value: Number(record.value ?? 0) }; + }); + + return { + websiteId: input.websiteId, + range: isoRange(range), + model: input.model ?? 'last-click', + conversion: { type: input.conversionType, value: input.conversion }, + total: result?.total ?? null, + referrers: list(result?.referrer), + paidAds: list(result?.paidAds), + utmSources: list(result?.utm_source), + utmMediums: list(result?.utm_medium), + utmCampaigns: list(result?.utm_campaign), + utmContent: list(result?.utm_content), + utmTerms: list(result?.utm_term), + }; + }, +}); + +export const getRevenue = defineTool({ + name: 'get_revenue', + title: 'Get revenue report', + description: + 'Returns revenue tracked through Umami revenue events for a time range: totals (sum, count, average, unique ' + + 'customers, ARPU) with a comparison to the previous period, a revenue time series, and breakdowns by country, ' + + 'region, referrer and channel. Requires revenue tracking to be set up and a websiteId from list_websites.', + inputSchema: z.object({ + websiteId, + ...dateRangeInput, + currency: z.string().length(3).optional().describe('ISO 4217 currency code (default USD).'), + unit: timeUnit.optional(), + timezone: timezone.optional(), + filters: filtersSchema.optional(), + }), + async handler(input, { client }) { + const range = parseDateRange(input); + const result = (await client.runRevenueReport({ + websiteId: input.websiteId, + type: 'revenue', + filters: reportFilters(range, input.filters), + parameters: { + ...toReportDates(range), + currency: (input.currency ?? 'USD').toUpperCase(), + unit: input.unit, + timezone: input.timezone, + }, + })) as Record; + + return { + websiteId: input.websiteId, + range: isoRange(range), + currency: (input.currency ?? 'USD').toUpperCase(), + total: result?.total ?? null, + chart: result?.chart ?? [], + byCountry: result?.country ?? [], + byRegion: result?.region ?? [], + byReferrer: result?.referrer ?? [], + byChannel: result?.channel ?? [], + }; + }, +}); diff --git a/packages/mcp/src/tools/session.ts b/packages/mcp/src/tools/session.ts new file mode 100644 index 000000000..4315a20cc --- /dev/null +++ b/packages/mcp/src/tools/session.ts @@ -0,0 +1,108 @@ +import { z } from 'zod'; +import { toIso } from '../lib/dates'; +import { MAX_SESSION_ACTIVITY } from '../lib/limits'; +import { defineTool } from '../lib/tool'; +import { formatSession, type SessionRow } from './sessions'; + +const HOUR = 60 * 60 * 1000; + +interface ActivityRow { + createdAt?: string | Date; + urlPath?: string; + urlQuery?: string | null; + referrerDomain?: string | null; + eventId?: string; + eventType?: number; + eventName?: string | null; + visitId?: string; + hasData?: number | boolean; + [key: string]: unknown; +} + +interface PropertyRow { + dataKey?: string; + dataType?: number; + stringValue?: string | null; + numberValue?: number | null; + dateValue?: string | null; + [key: string]: unknown; +} + +function propertyValue(row: PropertyRow) { + if (row.stringValue !== undefined && row.stringValue !== null) { + return row.stringValue; + } + + if (row.numberValue !== undefined && row.numberValue !== null) { + return row.numberValue; + } + + return row.dateValue ?? null; +} + +export const getSession = defineTool({ + name: 'get_session', + title: 'Get session details', + description: + 'Returns full details for one visitor session: visitor attributes, the ordered list of pageviews and events ' + + 'in the session (activity), and any custom session properties (e.g. plan, company). ' + + 'Use get_sessions first to find a sessionId. Requires the websiteId the session belongs to.', + inputSchema: z.object({ + websiteId: z.string().uuid().describe('Website ID from list_websites.'), + sessionId: z.string().uuid().describe('Session ID from get_sessions or get_events.'), + includeActivity: z + .boolean() + .optional() + .describe( + `Include the pageview/event timeline (default true, capped at ${MAX_SESSION_ACTIVITY} entries).`, + ), + }), + async handler(input, { client }) { + const session = (await client.getWebsiteSession({ + websiteId: input.websiteId, + sessionId: input.sessionId, + })) as unknown as SessionRow; + + const firstAt = Date.parse(String(session?.firstAt ?? '')) || Date.now() - 30 * 24 * HOUR; + const lastAt = Date.parse(String(session?.lastAt ?? '')) || Date.now(); + const includeActivity = input.includeActivity ?? true; + + const [activity, properties] = await Promise.all([ + includeActivity + ? (client.getWebsiteSessionActivity({ + websiteId: input.websiteId, + sessionId: input.sessionId, + startAt: firstAt - HOUR, + endAt: lastAt + HOUR, + }) as Promise) + : Promise.resolve([] as ActivityRow[]), + client.getWebsiteSessionProperties({ + websiteId: input.websiteId, + sessionId: input.sessionId, + }) as Promise, + ]); + + const rows = Array.isArray(activity) ? activity : []; + + return { + websiteId: input.websiteId, + session: formatSession(session), + properties: Object.fromEntries( + (Array.isArray(properties) ? properties : []) + .filter(row => row.dataKey) + .map(row => [row.dataKey as string, propertyValue(row)]), + ), + activity: rows.slice(0, MAX_SESSION_ACTIVITY).map(row => ({ + at: toIso(row.createdAt as string), + type: row.eventType === 2 ? 'event' : 'pageview', + name: row.eventName ?? null, + path: row.urlPath ?? null, + query: row.urlQuery ?? null, + referrer: row.referrerDomain ?? null, + visitId: row.visitId ?? null, + eventId: row.eventId ?? null, + })), + activityTruncated: rows.length > MAX_SESSION_ACTIVITY, + }; + }, +}); diff --git a/packages/mcp/src/tools/sessions.ts b/packages/mcp/src/tools/sessions.ts new file mode 100644 index 000000000..a8f356058 --- /dev/null +++ b/packages/mcp/src/tools/sessions.ts @@ -0,0 +1,97 @@ +import { z } from 'zod'; +import { dateRangeInput, parseDateRange, toIso } from '../lib/dates'; +import { filtersSchema, toFilterParams } from '../lib/filters'; +import { clamp, DEFAULT_PAGE_SIZE, MAX_PAGE_SIZE, pageInfo } from '../lib/limits'; +import { defineTool } from '../lib/tool'; + +export interface SessionRow { + id: string; + websiteId?: string; + hostname?: string; + browser?: string; + os?: string; + device?: string; + screen?: string; + language?: string; + country?: string; + region?: string; + city?: string; + distinctId?: string | null; + firstAt?: string | Date; + lastAt?: string | Date; + visits?: number; + views?: number; + events?: number; + createdAt?: string | Date; + [key: string]: unknown; +} + +export function formatSession(row: SessionRow) { + return { + id: row.id, + firstAt: toIso(row.firstAt as string), + lastAt: toIso(row.lastAt as string), + visits: Number(row.visits ?? 0), + views: Number(row.views ?? 0), + events: Number(row.events ?? 0), + hostname: row.hostname ?? null, + browser: row.browser ?? null, + os: row.os ?? null, + device: row.device ?? null, + screen: row.screen ?? null, + language: row.language ?? null, + country: row.country ?? null, + region: row.region ?? null, + city: row.city ?? null, + distinctId: row.distinctId ?? null, + }; +} + +export const getSessions = defineTool({ + name: 'get_sessions', + title: 'Get visitor sessions', + description: + "Lists visitor sessions for a website during a time range, newest first. Each session includes the visitor's " + + 'browser, OS, device, location, identified user ID (distinctId), number of visits, views and events, and first/last ' + + 'activity timestamps. Use "search" to find sessions by distinct ID, city, browser, OS or device. ' + + 'Use get_session for full details of one session. Results are paginated. Requires a websiteId from list_websites.', + inputSchema: z.object({ + websiteId: z.string().uuid().describe('Website ID from list_websites.'), + ...dateRangeInput, + search: z + .string() + .optional() + .describe('Free-text search (distinct ID, city, browser, OS, device).'), + page: z.number().int().positive().optional().describe('Page number, starting at 1.'), + pageSize: z + .number() + .int() + .positive() + .max(MAX_PAGE_SIZE) + .optional() + .describe(`Results per page (default ${DEFAULT_PAGE_SIZE}, max ${MAX_PAGE_SIZE}).`), + filters: filtersSchema.optional(), + }), + async handler(input, { client }) { + const range = parseDateRange(input); + const result = (await client.getWebsiteSessions({ + websiteId: input.websiteId, + startAt: range.startAt, + endAt: range.endAt, + search: input.search, + page: input.page ?? 1, + pageSize: clamp(input.pageSize, DEFAULT_PAGE_SIZE, MAX_PAGE_SIZE), + ...toFilterParams(input.filters), + })) as { data?: SessionRow[]; count?: number; page?: number; pageSize?: number }; + + return { + websiteId: input.websiteId, + range: { + startAt: new Date(range.startAt).toISOString(), + endAt: new Date(range.endAt).toISOString(), + }, + sessions: (result?.data ?? []).map(formatSession), + ...pageInfo(result ?? {}), + }; + }, +}); diff --git a/packages/mcp/src/tools/stats.ts b/packages/mcp/src/tools/stats.ts new file mode 100644 index 000000000..c3395fb93 --- /dev/null +++ b/packages/mcp/src/tools/stats.ts @@ -0,0 +1,96 @@ +import { z } from 'zod'; +import { dateRangeInput, parseDateRange, timezone } from '../lib/dates'; +import { filtersSchema, toFilterParams } from '../lib/filters'; +import { defineTool } from '../lib/tool'; + +interface StatsRow { + pageviews?: number; + visitors?: number; + visits?: number; + bounces?: number; + totaltime?: number; +} + +function firstRow(value: unknown): StatsRow { + if (Array.isArray(value)) { + return (value[0] ?? {}) as StatsRow; + } + + if (value && typeof value === 'object') { + const record = value as Record; + + if ('pageviews' in record) { + return record as StatsRow; + } + + if (record['0'] && typeof record['0'] === 'object') { + return record['0'] as StatsRow; + } + } + + return {}; +} + +function summarize(row: StatsRow) { + const pageviews = Number(row.pageviews ?? 0); + const visitors = Number(row.visitors ?? 0); + const visits = Number(row.visits ?? 0); + const bounces = Number(row.bounces ?? 0); + const totaltime = Number(row.totaltime ?? 0); + + return { + pageviews, + visitors, + visits, + bounces, + bounceRate: visits ? Math.round((Math.min(bounces, visits) / visits) * 1000) / 10 : 0, + averageVisitDurationSeconds: visits ? Math.round(totaltime / visits) : 0, + viewsPerVisit: visits ? Math.round((pageviews / visits) * 100) / 100 : 0, + }; +} + +export const getWebsiteStats = defineTool({ + name: 'get_website_stats', + title: 'Get website stats', + description: + 'Returns summary traffic totals for a website over a time range: pageviews, unique visitors, visits (sessions), ' + + 'bounces, bounce rate, average visit duration and views per visit. Also returns the same totals for the ' + + 'previous period of equal length (or the same period last year) so you can compare. ' + + 'Use this for questions like "how much traffic did we get last week?". Requires a websiteId from list_websites.', + inputSchema: z.object({ + websiteId: z.string().uuid().describe('Website ID from list_websites.'), + ...dateRangeInput, + timezone: timezone.optional(), + compare: z + .enum(['prev', 'yoy']) + .optional() + .describe( + 'Comparison period: "prev" (previous period, default) or "yoy" (same period last year).', + ), + filters: filtersSchema.optional(), + }), + async handler(input, { client }) { + const range = parseDateRange(input); + const result = await client.getWebsiteStats({ + websiteId: input.websiteId, + startAt: range.startAt, + endAt: range.endAt, + timezone: input.timezone, + compare: input.compare, + ...toFilterParams(input.filters), + }); + const current = firstRow(result); + const comparison = firstRow((result as { comparison?: unknown })?.comparison); + + return { + websiteId: input.websiteId, + range: { + startAt: new Date(range.startAt).toISOString(), + endAt: new Date(range.endAt).toISOString(), + }, + current: summarize(current), + previous: summarize(comparison), + compare: input.compare ?? 'prev', + }; + }, +}); diff --git a/packages/mcp/src/tools/traffic.ts b/packages/mcp/src/tools/traffic.ts new file mode 100644 index 000000000..9388f85f3 --- /dev/null +++ b/packages/mcp/src/tools/traffic.ts @@ -0,0 +1,74 @@ +import { z } from 'zod'; +import { dateRangeInput, parseDateRange, timeUnit, timezone } from '../lib/dates'; +import { filtersSchema, toFilterParams } from '../lib/filters'; +import { defineTool } from '../lib/tool'; + +interface Point { + x: string; + y: number; +} + +function series(value: unknown) { + if (!Array.isArray(value)) { + return []; + } + + return (value as Point[]).map(point => ({ date: point.x, value: Number(point.y ?? 0) })); +} + +export const getWebsiteTraffic = defineTool({ + name: 'get_website_traffic', + title: 'Get website traffic over time', + description: + 'Returns pageviews and visits (sessions) as a time series bucketed by minute, hour, day, month or year. ' + + 'Use this to chart trends, find peak days, or compare two periods. ' + + 'Each point has an ISO date and a count. Requires a websiteId from list_websites.', + inputSchema: z.object({ + websiteId: z.string().uuid().describe('Website ID from list_websites.'), + ...dateRangeInput, + unit: timeUnit.optional(), + timezone: timezone.optional(), + compare: z + .enum(['prev', 'yoy']) + .optional() + .describe('Also return the previous period ("prev") or same period last year ("yoy").'), + filters: filtersSchema.optional(), + }), + async handler(input, { client }) { + const range = parseDateRange(input); + const result = (await client.getWebsitePageviews({ + websiteId: input.websiteId, + startAt: range.startAt, + endAt: range.endAt, + unit: input.unit, + timezone: input.timezone, + compare: input.compare, + ...toFilterParams(input.filters), + })) as { + pageviews?: unknown; + sessions?: unknown; + compare?: { pageviews?: unknown; sessions?: unknown; startDate?: string; endDate?: string }; + }; + + return { + websiteId: input.websiteId, + range: { + startAt: new Date(range.startAt).toISOString(), + endAt: new Date(range.endAt).toISOString(), + }, + unit: input.unit ?? 'auto', + pageviews: series(result?.pageviews), + visits: series(result?.sessions), + ...(result?.compare + ? { + previous: { + startAt: result.compare.startDate ?? null, + endAt: result.compare.endDate ?? null, + pageviews: series(result.compare.pageviews), + visits: series(result.compare.sessions), + }, + } + : {}), + }; + }, +}); diff --git a/packages/mcp/src/tools/websites.ts b/packages/mcp/src/tools/websites.ts new file mode 100644 index 000000000..9eff70b63 --- /dev/null +++ b/packages/mcp/src/tools/websites.ts @@ -0,0 +1,43 @@ +import { z } from 'zod'; +import { toIso } from '../lib/dates'; +import { clamp, DEFAULT_PAGE_SIZE, MAX_PAGE_SIZE, pageInfo } from '../lib/limits'; +import { defineTool } from '../lib/tool'; + +export const listWebsites = defineTool({ + name: 'list_websites', + title: 'List websites', + description: + 'Lists the websites the authenticated user can access, including websites shared through teams. ' + + 'Returns each website ID, name and domain. Call this first to find the websiteId required by every other tool. ' + + 'Results are paginated.', + inputSchema: z.object({ + search: z.string().optional().describe('Filter websites by name or domain.'), + page: z.number().int().positive().optional().describe('Page number, starting at 1.'), + pageSize: z + .number() + .int() + .positive() + .max(MAX_PAGE_SIZE) + .optional() + .describe(`Results per page (default ${DEFAULT_PAGE_SIZE}, max ${MAX_PAGE_SIZE}).`), + }), + async handler(input, { client }) { + const result = await client.listWebsites({ + search: input.search, + page: input.page ?? 1, + pageSize: clamp(input.pageSize, DEFAULT_PAGE_SIZE, MAX_PAGE_SIZE), + includeTeams: 'true', + }); + + return { + websites: result.data.map(website => ({ + id: website.id, + name: website.name, + domain: website.domain, + teamId: website.teamId ?? null, + createdAt: toIso(website.createdAt), + })), + ...pageInfo(result), + }; + }, +}); diff --git a/packages/mcp/tsconfig.json b/packages/mcp/tsconfig.json new file mode 100644 index 000000000..440e0c072 --- /dev/null +++ b/packages/mcp/tsconfig.json @@ -0,0 +1,18 @@ +{ + "compilerOptions": { + "target": "es2022", + "module": "esnext", + "moduleResolution": "bundler", + "lib": ["es2022", "dom"], + "strict": true, + "noEmit": true, + "skipLibCheck": true, + "esModuleInterop": true, + "isolatedModules": true, + "resolveJsonModule": true, + "forceConsistentCasingInFileNames": true, + "ignoreDeprecations": "6.0", + "types": ["node"] + }, + "include": ["src/**/*.ts", "tsup.config.ts", "vitest.config.ts"] +} diff --git a/packages/mcp/tsup.config.ts b/packages/mcp/tsup.config.ts new file mode 100644 index 000000000..94204f357 --- /dev/null +++ b/packages/mcp/tsup.config.ts @@ -0,0 +1,13 @@ +import { defineConfig } from 'tsup'; + +export default defineConfig({ + entry: { index: 'src/index.ts', cli: 'src/cli.ts' }, + format: ['esm', 'cjs'], + outDir: 'dist', + dts: { entry: { index: 'src/index.ts' } }, + sourcemap: true, + clean: true, + splitting: false, + target: 'es2022', + banner: ({ format }) => (format === 'esm' ? {} : {}), +}); diff --git a/packages/mcp/vitest.config.ts b/packages/mcp/vitest.config.ts new file mode 100644 index 000000000..7eeb3f85d --- /dev/null +++ b/packages/mcp/vitest.config.ts @@ -0,0 +1,8 @@ +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + test: { + environment: 'node', + include: ['src/**/*.test.ts'], + }, +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b9d1842c9..d15636235 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -31,10 +31,16 @@ importers: version: 0.5.0(@prisma/client@7.9.1(prisma@7.9.1(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(typescript@6.0.3))(typescript@6.0.3)) '@svgr/cli': specifier: ^8.1.0 - version: 8.1.0(supports-color@7.2.0)(typescript@6.0.3) + version: 8.1.0(supports-color@10.2.2)(typescript@6.0.3) '@tanstack/react-query': specifier: ^5.102.8 version: 5.102.8(react@19.2.8) + '@umami/api-client': + specifier: workspace:* + version: link:packages/api-client + '@umami/mcp': + specifier: workspace:* + version: link:packages/mcp '@umami/react-zen': specifier: ^0.251.0 version: 0.251.0(@date-fns/tz@1.5.0)(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(date-fns@4.4.0)(immer@11.1.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(tailwindcss@4.1.18)(use-sync-external-store@1.6.0(react@19.2.8)) @@ -112,10 +118,10 @@ importers: version: 12.43.0(react-dom@19.2.8(react@19.2.8))(react@19.2.8) next: specifier: 16.3.4 - version: 16.3.4(@babel/core@7.29.0(supports-color@7.2.0))(@playwright/test@1.62.1)(@types/node@26.4.0)(babel-plugin-react-compiler@19.1.0-rc.2)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + version: 16.3.4(@babel/core@7.29.0(supports-color@10.2.2))(@playwright/test@1.62.1)(@types/node@26.4.0)(babel-plugin-react-compiler@19.1.0-rc.2)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) next-intl: specifier: 4.13.4 - version: 4.13.4(@swc/helpers@0.5.23)(next@16.3.4(@babel/core@7.29.0(supports-color@7.2.0))(@playwright/test@1.62.1)(@types/node@26.4.0)(babel-plugin-react-compiler@19.1.0-rc.2)(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(react@19.2.8)(typescript@6.0.3) + version: 4.13.4(@swc/helpers@0.5.23)(next@16.3.4(@babel/core@7.29.0(supports-color@10.2.2))(@playwright/test@1.62.1)(@types/node@26.4.0)(babel-plugin-react-compiler@19.1.0-rc.2)(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(react@19.2.8)(typescript@6.0.3) node-fetch: specifier: ^3.2.8 version: 3.3.2 @@ -246,6 +252,9 @@ importers: msw: specifier: ^2.15.0 version: 2.15.0(@types/node@26.4.0)(typescript@6.0.3) + openapi-typescript: + specifier: ^7.13.0 + version: 7.13.0(typescript@6.0.3) postcss: specifier: ^8.5.26 version: 8.5.26 @@ -277,6 +286,49 @@ importers: specifier: ^6.0.2 version: 6.0.2(zod@4.4.3) + packages/api-client: + devDependencies: + '@types/node': + specifier: ^26.4.0 + version: 26.4.0 + tsup: + specifier: ^8.5.0 + version: 8.5.1(@swc/core@1.15.46(@swc/helpers@0.5.23))(jiti@2.7.0)(postcss@8.5.26)(supports-color@10.2.2)(tsx@4.23.13)(typescript@6.0.3) + typescript: + specifier: ^6.0.3 + version: 6.0.3 + vitest: + specifier: ^4.1.11 + version: 4.1.11(@types/node@26.4.0)(jsdom@30.0.1(@noble/hashes@2.2.0))(msw@2.15.0(@types/node@26.4.0)(typescript@6.0.3))(vite@8.0.11(@types/node@26.4.0)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.46.1)(tsx@4.23.13)) + + packages/mcp: + dependencies: + '@modelcontextprotocol/server': + specifier: ^2.0.0 + version: 2.0.0 + '@umami/api-client': + specifier: workspace:* + version: link:../api-client + zod: + specifier: ^4.4.3 + version: 4.4.3 + devDependencies: + '@modelcontextprotocol/client': + specifier: ^2.0.0 + version: 2.0.0 + '@types/node': + specifier: ^26.4.0 + version: 26.4.0 + tsup: + specifier: ^8.5.0 + version: 8.5.1(@swc/core@1.15.46(@swc/helpers@0.5.23))(jiti@2.7.0)(postcss@8.5.26)(supports-color@10.2.2)(tsx@4.23.13)(typescript@6.0.3) + typescript: + specifier: ^6.0.3 + version: 6.0.3 + vitest: + specifier: ^4.1.11 + version: 4.1.11(@types/node@26.4.0)(jsdom@30.0.1(@noble/hashes@2.2.0))(msw@2.15.0(@types/node@26.4.0)(typescript@6.0.3))(vite@8.0.11(@types/node@26.4.0)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.46.1)(tsx@4.23.13)) + packages: '@adobe/css-tools@4.5.0': @@ -294,10 +346,6 @@ packages: resolution: {integrity: sha512-UJLfKXBhrc8i1vH2eJXuYQMwlsLKWFw3O+CPqXSuVEiikeAim3UgrfWX0k4tA/X8cRFM8iZ7OaqBokFGbYusdg==} engines: {node: ^22.13.0 || >=24.0.0} - '@babel/code-frame@7.27.1': - resolution: {integrity: sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==} - engines: {node: '>=6.9.0'} - '@babel/code-frame@7.29.7': resolution: {integrity: sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==} engines: {node: '>=6.9.0'} @@ -1687,6 +1735,18 @@ packages: '@kurkle/color@0.3.4': resolution: {integrity: sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==} + '@modelcontextprotocol/client@2.0.0': + resolution: {integrity: sha512-8f1OghQ2rjzIOfqgUCP+8GiUWqRs89njoWLNqAe8kWmDePv3s1fZXseej+QXemssEuuOvLLmLO/kqM3IQHtISw==} + engines: {node: '>=20'} + + '@modelcontextprotocol/core@2.0.0': + resolution: {integrity: sha512-pJCEwGG7Lfr/+PQp9ZTwKXNeO5wzbfKL7H3MYpCorM4oFBoQrdjnBgEoqG+RjhsvS1FKrDbKux+M1HhlnGWqcA==} + engines: {node: '>=20'} + + '@modelcontextprotocol/server@2.0.0': + resolution: {integrity: sha512-YhHWdHfpFMQfd0prsEnxKeS3Qz3ytIGmsS0sth4KDjnacIT7hxk6hXHkJ9KysxlkvTM+WZAtQbbcUhdoP4Hvtw==} + engines: {node: '>=20'} + '@mswjs/interceptors@0.41.8': resolution: {integrity: sha512-pRLMNKTSGRoLq+KnEB/7OY5vijw1XmcheAAOiv6pj7W1FG32kAGqj1C/RK/cqxRGr1Fh+zBi8sDur8kj3EQv6A==} engines: {node: '>=18'} @@ -2258,6 +2318,16 @@ packages: peerDependencies: '@redis/client': ^5.12.1 + '@redocly/ajv@8.11.2': + resolution: {integrity: sha512-io1JpnwtIcvojV7QKDUSIuMN/ikdOUd1ReEnUnMKGfDVridQZ31J0MmIuqwuRjWDZfmvr+Q0MqCcfHM2gTivOg==} + + '@redocly/config@0.22.0': + resolution: {integrity: sha512-gAy93Ddo01Z3bHuVdPWfCwzgfaYgMdaZPcfL7JZ7hWJoK9V0lXDbigTWkhiPFAaLWzbOJ+kbUQG1+XwIm0KRGQ==} + + '@redocly/openapi-core@1.34.19': + resolution: {integrity: sha512-o/0VgsBXgwcY1lyeqcVtSGdTQAPnVggo0fbFVPlxl5XVDKUcVH0OLRqt3CbkwByT5FU305E0iE0O7MzThjDblw==} + engines: {node: '>=18.17.0', npm: '>=9.5.0'} + '@rolldown/binding-android-arm64@1.0.0-rc.18': resolution: {integrity: sha512-lIDyUAfD7U3+BWKzdxMbJcsYHuqXqmGz40aeRqvuAm3y5TkJSYTBW2RDrn65DJFPQqVjUAUqq5uz8urzQ8aBdQ==} engines: {node: ^20.19.0 || >=22.12.0} @@ -3013,9 +3083,17 @@ packages: engines: {node: '>=0.4.0'} hasBin: true + agent-base@7.1.4: + resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==} + engines: {node: '>= 14'} + ajv@8.20.0: resolution: {integrity: sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==} + ansi-colors@4.1.3: + resolution: {integrity: sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==} + engines: {node: '>=6'} + ansi-regex@5.0.1: resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} engines: {node: '>=8'} @@ -3212,6 +3290,9 @@ packages: resolution: {integrity: sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==} engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} + change-case@5.4.4: + resolution: {integrity: sha512-HRQyTk2/YPEkt9TnUPbOpr64Uw3KOicFWPVBb+xiHvd6eBx/qPr9xqfBFDT8P2vWsvvz4jbEkfDe71W3VyNu2w==} + chart.js@4.5.1: resolution: {integrity: sha512-GIjfiT9dbmHRiYi6Nl2yFCq7kkwdkp1W/lp2J99rX0yo9tgJGn3lKQATztIjb5tVtevcBtIdICNWqlq5+E8/Pw==} engines: {pnpm: '>=8'} @@ -3285,6 +3366,9 @@ packages: colord@2.10.0: resolution: {integrity: sha512-AidJptpBJmjTclAp9BkLwJi0T93fo5epJnbaZslpg6QVzpHjAiveF55mE9AcUJiGMqRHgMDY8soMsQtuNYMHfw==} + colorette@1.4.0: + resolution: {integrity: sha512-Y2oEozpomLn7Q3HFP7dpww7AtMJplbM9lGZP6RDfHqmbeRjiwRg4n6VM6j4KLmRke85uWEI7JqF17f3pqdRA0g==} + commander@2.20.3: resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==} @@ -3734,6 +3818,14 @@ packages: estree-walker@3.0.3: resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + eventsource-parser@3.1.1: + resolution: {integrity: sha512-EKN1vKAMcZ8MlYMpaNuxN6R9yakzH6uajHcHVTqWJzvu5pWw9DyhbP35HH8MVBQ+dZjAfDxk+A8NiR9KWaXiyQ==} + engines: {node: '>=18.0.0'} + + eventsource@3.0.7: + resolution: {integrity: sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==} + engines: {node: '>=18.0.0'} + expect-type@1.3.0: resolution: {integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==} engines: {node: '>=12.0.0'} @@ -3955,6 +4047,10 @@ packages: resolution: {integrity: sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==} engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + https-proxy-agent@7.0.6: + resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==} + engines: {node: '>= 14'} + iconv-lite@0.7.3: resolution: {integrity: sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ==} engines: {node: '>=0.10.0'} @@ -3976,6 +4072,10 @@ packages: resolution: {integrity: sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==} engines: {node: '>=8'} + index-to-position@1.2.0: + resolution: {integrity: sha512-Yg7+ztRkqslMAS2iFaU+Oa4KTSidr63OsFGlOrJoW981kIYO3CGCS3wA95P1mUi/IVSJkn0D479KTJpVpvFNuw==} + engines: {node: '>=18'} + inflight@1.0.6: resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} deprecated: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful. @@ -4179,10 +4279,17 @@ packages: resolution: {integrity: sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==} hasBin: true + jose@6.2.10: + resolution: {integrity: sha512-iiW7J9qRFlGxvCOIBDBDxFePQSn7ZMAnrYGhrrOo6siO/MIqwfyilLR27pkfDgUk+raLuzADS8A3S/KLBisc0g==} + joycon@3.1.1: resolution: {integrity: sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==} engines: {node: '>=10'} + js-levenshtein@1.1.6: + resolution: {integrity: sha512-X2BB11YZtrRqY4EnQcLX5Rh373zbK4alC1FW7D7MBhL2gtcC17cTnr6DmfHZeS0s2rTHjUTMMHfG7gO8SSdw+g==} + engines: {node: '>=0.10.0'} + js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} @@ -4190,6 +4297,10 @@ packages: resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} hasBin: true + js-yaml@4.3.1: + resolution: {integrity: sha512-CY6crGq313MX8GkwvB7tzgp99vjQxY1++5y10/BKN/GUfHqWaOGQMNZkBvqSzsZKWk/ijwHlWzzkLulsGHhjWQ==} + hasBin: true + jsdom@30.0.1: resolution: {integrity: sha512-52v7mUVUfNQVYYqE1lcdaymWL0njO7lTLUog6ZvW2U5KsbiLk/GnZlVJ+qx0xfNJZ6Gn+KSpPNE52vurbxZwrA==} engines: {node: ^22.22.2 || ^24.15.0 || >=26.0.0} @@ -4428,6 +4539,10 @@ packages: resolution: {integrity: sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==} engines: {node: '>=10'} + minimatch@5.1.9: + resolution: {integrity: sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==} + engines: {node: '>=10'} + minimatch@9.0.5: resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==} engines: {node: '>=16 || 14 >=14.17'} @@ -4611,6 +4726,12 @@ packages: once@1.4.0: resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + openapi-typescript@7.13.0: + resolution: {integrity: sha512-EFP392gcqXS7ntPvbhBzbF8TyBA+baIYEm791Hy5YkjDYKTnk/Tn5OQeKm5BIZvJihpp8Zzr4hzx0Irde1LNGQ==} + hasBin: true + peerDependencies: + typescript: ^5.x + otplib@13.5.0: resolution: {integrity: sha512-RpcC6aq4rANX6MverMuU3pqHVLgMPO7vl6qR8ga7YzoEHlPJknJ58cSVAaf3jBYnasiQVQHXXl2mj/4pYlsVEQ==} @@ -4654,6 +4775,10 @@ packages: resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==} engines: {node: '>=8'} + parse-json@8.3.0: + resolution: {integrity: sha512-ybiGyvspI+fAoRQbIPRddCcSTV9/LsJbf0e/S85VLowVGzRmokfneg2kwVW/KU5rOXrPSbF1qAKPMgNTqqROQQ==} + engines: {node: '>=18'} + parse5@8.0.1: resolution: {integrity: sha512-z1e/HMG90obSGeidlli3hj7cbocou0/wa5HacvI3ASx34PecNjNQeaHNo5WIZpWofN9kgkqV1q5YvXe3F0FoPw==} @@ -4755,6 +4880,10 @@ packages: resolution: {integrity: sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==} engines: {node: '>= 6'} + pkce-challenge@5.0.1: + resolution: {integrity: sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==} + engines: {node: '>=16.20.0'} + pkg-types@1.3.1: resolution: {integrity: sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==} @@ -4771,6 +4900,10 @@ packages: engines: {node: '>=20'} hasBin: true + pluralize@8.0.0: + resolution: {integrity: sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==} + engines: {node: '>=4'} + pngjs@5.0.0: resolution: {integrity: sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==} engines: {node: '>=10.13.0'} @@ -5503,6 +5636,10 @@ packages: engines: {node: '>=16 || 14 >=14.17'} hasBin: true + supports-color@10.2.2: + resolution: {integrity: sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g==} + engines: {node: '>=18'} + supports-color@5.5.0: resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==} engines: {node: '>=4'} @@ -5657,6 +5794,10 @@ packages: engines: {node: '>=18.0.0'} hasBin: true + type-fest@4.41.0: + resolution: {integrity: sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==} + engines: {node: '>=16'} + type-fest@5.6.0: resolution: {integrity: sha512-8ZiHFm91orbSAe2PSAiSVBVko18pbhbiB3U9GglSzF/zCGkR+rxpHx6sEMCUm4kxY4LjDIUGgCfUMtwfZfjfUA==} engines: {node: '>=20'} @@ -5712,6 +5853,9 @@ packages: peerDependencies: browserslist: '>= 4.21.0' + uri-js-replace@1.0.1: + resolution: {integrity: sha512-W+C9NWNLFOoBI2QWDp4UT9pv65r2w5Cx+3sTYFvtMdDBxkKt1syCqsUdSFAChbEe1uK5TfS04wt/nGwmaeIQ0g==} + use-callback-ref@1.3.3: resolution: {integrity: sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==} engines: {node: '>=10'} @@ -5941,6 +6085,9 @@ packages: resolution: {integrity: sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==} engines: {node: '>=18'} + yaml-ast-parser@0.0.43: + resolution: {integrity: sha512-2PTINUwsRqSd+s8XxKaJWQlUuEMHJQyEuh2edBbW8KNJz0SJPwUSD2zRWqezFEdN7IzAgeuYHFUCF7o8zRdZ0A==} + yargs-parser@18.1.3: resolution: {integrity: sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==} engines: {node: '>=6'} @@ -6029,12 +6176,6 @@ snapshots: is-potential-custom-element-name: 1.0.1 lru-cache: 11.5.2 - '@babel/code-frame@7.27.1': - dependencies: - '@babel/helper-validator-identifier': 7.29.7 - js-tokens: 4.0.0 - picocolors: 1.1.1 - '@babel/code-frame@7.29.7': dependencies: '@babel/helper-validator-identifier': 7.29.7 @@ -6046,13 +6187,13 @@ snapshots: '@babel/compat-data@7.29.7': optional: true - '@babel/core@7.28.3(supports-color@7.2.0)': + '@babel/core@7.28.3(supports-color@10.2.2)': dependencies: '@ampproject/remapping': 2.3.0 - '@babel/code-frame': 7.27.1 + '@babel/code-frame': 7.29.7 '@babel/generator': 7.28.3 '@babel/helper-compilation-targets': 7.27.2 - '@babel/helper-module-transforms': 7.28.3(@babel/core@7.28.3(supports-color@7.2.0))(supports-color@7.2.0) + '@babel/helper-module-transforms': 7.28.3(@babel/core@7.28.3(supports-color@10.2.2))(supports-color@10.2.2) '@babel/helpers': 7.28.3 '@babel/parser': 7.28.3 '@babel/template': 7.27.2 @@ -6066,20 +6207,20 @@ snapshots: transitivePeerDependencies: - supports-color - '@babel/core@7.29.0(supports-color@7.2.0)': + '@babel/core@7.29.0(supports-color@10.2.2)': dependencies: '@babel/code-frame': 7.29.7 '@babel/generator': 7.29.7 '@babel/helper-compilation-targets': 7.29.7 - '@babel/helper-module-transforms': 7.29.7(@babel/core@7.29.0(supports-color@7.2.0))(supports-color@7.2.0) + '@babel/helper-module-transforms': 7.29.7(@babel/core@7.29.0(supports-color@10.2.2))(supports-color@10.2.2) '@babel/helpers': 7.29.7 '@babel/parser': 7.29.7 '@babel/template': 7.29.7 - '@babel/traverse': 7.29.7(supports-color@7.2.0) + '@babel/traverse': 7.29.7(supports-color@10.2.2) '@babel/types': 7.29.7 '@jridgewell/remapping': 2.3.5 convert-source-map: 2.0.0 - debug: 4.4.3(supports-color@7.2.0) + debug: 4.4.3(supports-color@10.2.2) gensync: 1.0.0-beta.2 json5: 2.2.3 semver: 6.3.1 @@ -6126,36 +6267,36 @@ snapshots: '@babel/helper-globals@7.29.7': optional: true - '@babel/helper-module-imports@7.27.1(supports-color@7.2.0)': + '@babel/helper-module-imports@7.27.1(supports-color@10.2.2)': dependencies: - '@babel/traverse': 7.28.3(supports-color@7.2.0) + '@babel/traverse': 7.28.3(supports-color@10.2.2) '@babel/types': 7.28.2 transitivePeerDependencies: - supports-color - '@babel/helper-module-imports@7.29.7(supports-color@7.2.0)': + '@babel/helper-module-imports@7.29.7(supports-color@10.2.2)': dependencies: - '@babel/traverse': 7.29.7(supports-color@7.2.0) + '@babel/traverse': 7.29.7(supports-color@10.2.2) '@babel/types': 7.29.7 transitivePeerDependencies: - supports-color optional: true - '@babel/helper-module-transforms@7.28.3(@babel/core@7.28.3(supports-color@7.2.0))(supports-color@7.2.0)': + '@babel/helper-module-transforms@7.28.3(@babel/core@7.28.3(supports-color@10.2.2))(supports-color@10.2.2)': dependencies: - '@babel/core': 7.28.3(supports-color@7.2.0) - '@babel/helper-module-imports': 7.27.1(supports-color@7.2.0) + '@babel/core': 7.28.3(supports-color@10.2.2) + '@babel/helper-module-imports': 7.27.1(supports-color@10.2.2) '@babel/helper-validator-identifier': 7.29.7 - '@babel/traverse': 7.28.3(supports-color@7.2.0) + '@babel/traverse': 7.28.3(supports-color@10.2.2) transitivePeerDependencies: - supports-color - '@babel/helper-module-transforms@7.29.7(@babel/core@7.29.0(supports-color@7.2.0))(supports-color@7.2.0)': + '@babel/helper-module-transforms@7.29.7(@babel/core@7.29.0(supports-color@10.2.2))(supports-color@10.2.2)': dependencies: - '@babel/core': 7.29.0(supports-color@7.2.0) - '@babel/helper-module-imports': 7.29.7(supports-color@7.2.0) + '@babel/core': 7.29.0(supports-color@10.2.2) + '@babel/helper-module-imports': 7.29.7(supports-color@10.2.2) '@babel/helper-validator-identifier': 7.29.7 - '@babel/traverse': 7.29.7(supports-color@7.2.0) + '@babel/traverse': 7.29.7(supports-color@10.2.2) transitivePeerDependencies: - supports-color optional: true @@ -6211,6 +6352,18 @@ snapshots: '@babel/types': 7.29.7 optional: true + '@babel/traverse@7.28.3(supports-color@10.2.2)': + dependencies: + '@babel/code-frame': 7.29.7 + '@babel/generator': 7.28.3 + '@babel/helper-globals': 7.28.0 + '@babel/parser': 7.28.3 + '@babel/template': 7.27.2 + '@babel/types': 7.28.2 + debug: 4.4.3(supports-color@10.2.2) + transitivePeerDependencies: + - supports-color + '@babel/traverse@7.28.3(supports-color@7.2.0)': dependencies: '@babel/code-frame': 7.29.7 @@ -6223,7 +6376,7 @@ snapshots: transitivePeerDependencies: - supports-color - '@babel/traverse@7.29.7(supports-color@7.2.0)': + '@babel/traverse@7.29.7(supports-color@10.2.2)': dependencies: '@babel/code-frame': 7.29.7 '@babel/generator': 7.29.7 @@ -6231,7 +6384,7 @@ snapshots: '@babel/parser': 7.29.7 '@babel/template': 7.29.7 '@babel/types': 7.29.7 - debug: 4.4.3(supports-color@7.2.0) + debug: 4.4.3(supports-color@10.2.2) transitivePeerDependencies: - supports-color optional: true @@ -7242,6 +7395,25 @@ snapshots: '@kurkle/color@0.3.4': {} + '@modelcontextprotocol/client@2.0.0': + dependencies: + '@modelcontextprotocol/core': 2.0.0 + cross-spawn: 7.0.6 + eventsource: 3.0.7 + eventsource-parser: 3.1.1 + jose: 6.2.10 + pkce-challenge: 5.0.1 + zod: 4.4.3 + + '@modelcontextprotocol/core@2.0.0': + dependencies: + zod: 4.4.3 + + '@modelcontextprotocol/server@2.0.0': + dependencies: + '@modelcontextprotocol/core': 2.0.0 + zod: 4.4.3 + '@mswjs/interceptors@0.41.8': dependencies: '@open-draft/deferred-promise': 2.2.0 @@ -7749,6 +7921,29 @@ snapshots: dependencies: '@redis/client': 5.12.1 + '@redocly/ajv@8.11.2': + dependencies: + fast-deep-equal: 3.1.3 + json-schema-traverse: 1.0.0 + require-from-string: 2.0.2 + uri-js-replace: 1.0.1 + + '@redocly/config@0.22.0': {} + + '@redocly/openapi-core@1.34.19(supports-color@10.2.2)': + dependencies: + '@redocly/ajv': 8.11.2 + '@redocly/config': 0.22.0 + colorette: 1.4.0 + https-proxy-agent: 7.0.6(supports-color@10.2.2) + js-levenshtein: 1.1.6 + js-yaml: 4.3.1 + minimatch: 5.1.9 + pluralize: 8.0.0 + yaml-ast-parser: 0.0.43 + transitivePeerDependencies: + - supports-color + '@rolldown/binding-android-arm64@1.0.0-rc.18': optional: true @@ -7959,56 +8154,56 @@ snapshots: '@standard-schema/spec@1.1.0': {} - '@svgr/babel-plugin-add-jsx-attribute@8.0.0(@babel/core@7.28.3(supports-color@7.2.0))': + '@svgr/babel-plugin-add-jsx-attribute@8.0.0(@babel/core@7.28.3(supports-color@10.2.2))': dependencies: - '@babel/core': 7.28.3(supports-color@7.2.0) + '@babel/core': 7.28.3(supports-color@10.2.2) - '@svgr/babel-plugin-remove-jsx-attribute@8.0.0(@babel/core@7.28.3(supports-color@7.2.0))': + '@svgr/babel-plugin-remove-jsx-attribute@8.0.0(@babel/core@7.28.3(supports-color@10.2.2))': dependencies: - '@babel/core': 7.28.3(supports-color@7.2.0) + '@babel/core': 7.28.3(supports-color@10.2.2) - '@svgr/babel-plugin-remove-jsx-empty-expression@8.0.0(@babel/core@7.28.3(supports-color@7.2.0))': + '@svgr/babel-plugin-remove-jsx-empty-expression@8.0.0(@babel/core@7.28.3(supports-color@10.2.2))': dependencies: - '@babel/core': 7.28.3(supports-color@7.2.0) + '@babel/core': 7.28.3(supports-color@10.2.2) - '@svgr/babel-plugin-replace-jsx-attribute-value@8.0.0(@babel/core@7.28.3(supports-color@7.2.0))': + '@svgr/babel-plugin-replace-jsx-attribute-value@8.0.0(@babel/core@7.28.3(supports-color@10.2.2))': dependencies: - '@babel/core': 7.28.3(supports-color@7.2.0) + '@babel/core': 7.28.3(supports-color@10.2.2) - '@svgr/babel-plugin-svg-dynamic-title@8.0.0(@babel/core@7.28.3(supports-color@7.2.0))': + '@svgr/babel-plugin-svg-dynamic-title@8.0.0(@babel/core@7.28.3(supports-color@10.2.2))': dependencies: - '@babel/core': 7.28.3(supports-color@7.2.0) + '@babel/core': 7.28.3(supports-color@10.2.2) - '@svgr/babel-plugin-svg-em-dimensions@8.0.0(@babel/core@7.28.3(supports-color@7.2.0))': + '@svgr/babel-plugin-svg-em-dimensions@8.0.0(@babel/core@7.28.3(supports-color@10.2.2))': dependencies: - '@babel/core': 7.28.3(supports-color@7.2.0) + '@babel/core': 7.28.3(supports-color@10.2.2) - '@svgr/babel-plugin-transform-react-native-svg@8.1.0(@babel/core@7.28.3(supports-color@7.2.0))': + '@svgr/babel-plugin-transform-react-native-svg@8.1.0(@babel/core@7.28.3(supports-color@10.2.2))': dependencies: - '@babel/core': 7.28.3(supports-color@7.2.0) + '@babel/core': 7.28.3(supports-color@10.2.2) - '@svgr/babel-plugin-transform-svg-component@8.0.0(@babel/core@7.28.3(supports-color@7.2.0))': + '@svgr/babel-plugin-transform-svg-component@8.0.0(@babel/core@7.28.3(supports-color@10.2.2))': dependencies: - '@babel/core': 7.28.3(supports-color@7.2.0) + '@babel/core': 7.28.3(supports-color@10.2.2) - '@svgr/babel-preset@8.1.0(@babel/core@7.28.3(supports-color@7.2.0))': + '@svgr/babel-preset@8.1.0(@babel/core@7.28.3(supports-color@10.2.2))': dependencies: - '@babel/core': 7.28.3(supports-color@7.2.0) - '@svgr/babel-plugin-add-jsx-attribute': 8.0.0(@babel/core@7.28.3(supports-color@7.2.0)) - '@svgr/babel-plugin-remove-jsx-attribute': 8.0.0(@babel/core@7.28.3(supports-color@7.2.0)) - '@svgr/babel-plugin-remove-jsx-empty-expression': 8.0.0(@babel/core@7.28.3(supports-color@7.2.0)) - '@svgr/babel-plugin-replace-jsx-attribute-value': 8.0.0(@babel/core@7.28.3(supports-color@7.2.0)) - '@svgr/babel-plugin-svg-dynamic-title': 8.0.0(@babel/core@7.28.3(supports-color@7.2.0)) - '@svgr/babel-plugin-svg-em-dimensions': 8.0.0(@babel/core@7.28.3(supports-color@7.2.0)) - '@svgr/babel-plugin-transform-react-native-svg': 8.1.0(@babel/core@7.28.3(supports-color@7.2.0)) - '@svgr/babel-plugin-transform-svg-component': 8.0.0(@babel/core@7.28.3(supports-color@7.2.0)) + '@babel/core': 7.28.3(supports-color@10.2.2) + '@svgr/babel-plugin-add-jsx-attribute': 8.0.0(@babel/core@7.28.3(supports-color@10.2.2)) + '@svgr/babel-plugin-remove-jsx-attribute': 8.0.0(@babel/core@7.28.3(supports-color@10.2.2)) + '@svgr/babel-plugin-remove-jsx-empty-expression': 8.0.0(@babel/core@7.28.3(supports-color@10.2.2)) + '@svgr/babel-plugin-replace-jsx-attribute-value': 8.0.0(@babel/core@7.28.3(supports-color@10.2.2)) + '@svgr/babel-plugin-svg-dynamic-title': 8.0.0(@babel/core@7.28.3(supports-color@10.2.2)) + '@svgr/babel-plugin-svg-em-dimensions': 8.0.0(@babel/core@7.28.3(supports-color@10.2.2)) + '@svgr/babel-plugin-transform-react-native-svg': 8.1.0(@babel/core@7.28.3(supports-color@10.2.2)) + '@svgr/babel-plugin-transform-svg-component': 8.0.0(@babel/core@7.28.3(supports-color@10.2.2)) - '@svgr/cli@8.1.0(supports-color@7.2.0)(typescript@6.0.3)': + '@svgr/cli@8.1.0(supports-color@10.2.2)(typescript@6.0.3)': dependencies: - '@svgr/core': 8.1.0(supports-color@7.2.0)(typescript@6.0.3) - '@svgr/plugin-jsx': 8.1.0(@svgr/core@8.1.0(supports-color@7.2.0)(typescript@6.0.3))(supports-color@7.2.0) - '@svgr/plugin-prettier': 8.1.0(@svgr/core@8.1.0(supports-color@7.2.0)(typescript@6.0.3)) - '@svgr/plugin-svgo': 8.1.0(@svgr/core@8.1.0(supports-color@7.2.0)(typescript@6.0.3))(typescript@6.0.3) + '@svgr/core': 8.1.0(supports-color@10.2.2)(typescript@6.0.3) + '@svgr/plugin-jsx': 8.1.0(@svgr/core@8.1.0(supports-color@10.2.2)(typescript@6.0.3))(supports-color@10.2.2) + '@svgr/plugin-prettier': 8.1.0(@svgr/core@8.1.0(supports-color@10.2.2)(typescript@6.0.3)) + '@svgr/plugin-svgo': 8.1.0(@svgr/core@8.1.0(supports-color@10.2.2)(typescript@6.0.3))(typescript@6.0.3) camelcase: 6.3.0 chalk: 4.1.2 commander: 9.5.0 @@ -8019,10 +8214,10 @@ snapshots: - supports-color - typescript - '@svgr/core@8.1.0(supports-color@7.2.0)(typescript@6.0.3)': + '@svgr/core@8.1.0(supports-color@10.2.2)(typescript@6.0.3)': dependencies: - '@babel/core': 7.28.3(supports-color@7.2.0) - '@svgr/babel-preset': 8.1.0(@babel/core@7.28.3(supports-color@7.2.0)) + '@babel/core': 7.28.3(supports-color@10.2.2) + '@svgr/babel-preset': 8.1.0(@babel/core@7.28.3(supports-color@10.2.2)) camelcase: 6.3.0 cosmiconfig: 8.3.6(typescript@6.0.3) snake-case: 3.0.4 @@ -8035,25 +8230,25 @@ snapshots: '@babel/types': 7.28.2 entities: 4.5.0 - '@svgr/plugin-jsx@8.1.0(@svgr/core@8.1.0(supports-color@7.2.0)(typescript@6.0.3))(supports-color@7.2.0)': + '@svgr/plugin-jsx@8.1.0(@svgr/core@8.1.0(supports-color@10.2.2)(typescript@6.0.3))(supports-color@10.2.2)': dependencies: - '@babel/core': 7.28.3(supports-color@7.2.0) - '@svgr/babel-preset': 8.1.0(@babel/core@7.28.3(supports-color@7.2.0)) - '@svgr/core': 8.1.0(supports-color@7.2.0)(typescript@6.0.3) + '@babel/core': 7.28.3(supports-color@10.2.2) + '@svgr/babel-preset': 8.1.0(@babel/core@7.28.3(supports-color@10.2.2)) + '@svgr/core': 8.1.0(supports-color@10.2.2)(typescript@6.0.3) '@svgr/hast-util-to-babel-ast': 8.0.0 svg-parser: 2.0.4 transitivePeerDependencies: - supports-color - '@svgr/plugin-prettier@8.1.0(@svgr/core@8.1.0(supports-color@7.2.0)(typescript@6.0.3))': + '@svgr/plugin-prettier@8.1.0(@svgr/core@8.1.0(supports-color@10.2.2)(typescript@6.0.3))': dependencies: - '@svgr/core': 8.1.0(supports-color@7.2.0)(typescript@6.0.3) + '@svgr/core': 8.1.0(supports-color@10.2.2)(typescript@6.0.3) deepmerge: 4.3.1 prettier: 2.8.8 - '@svgr/plugin-svgo@8.1.0(@svgr/core@8.1.0(supports-color@7.2.0)(typescript@6.0.3))(typescript@6.0.3)': + '@svgr/plugin-svgo@8.1.0(@svgr/core@8.1.0(supports-color@10.2.2)(typescript@6.0.3))(typescript@6.0.3)': dependencies: - '@svgr/core': 8.1.0(supports-color@7.2.0)(typescript@6.0.3) + '@svgr/core': 8.1.0(supports-color@10.2.2)(typescript@6.0.3) cosmiconfig: 8.3.6(typescript@6.0.3) deepmerge: 4.3.1 svgo: 3.3.2 @@ -8434,6 +8629,8 @@ snapshots: acorn@8.17.0: {} + agent-base@7.1.4: {} + ajv@8.20.0: dependencies: fast-deep-equal: 3.1.3 @@ -8441,6 +8638,8 @@ snapshots: json-schema-traverse: 1.0.0 require-from-string: 2.0.2 + ansi-colors@4.1.3: {} + ansi-regex@5.0.1: {} ansi-regex@6.2.2: {} @@ -8622,6 +8821,8 @@ snapshots: chalk@5.6.2: {} + change-case@5.4.4: {} + chart.js@4.5.1: dependencies: '@kurkle/color': 0.3.4 @@ -8691,6 +8892,8 @@ snapshots: colord@2.10.0: {} + colorette@1.4.0: {} + commander@2.20.3: {} commander@4.1.1: {} @@ -8918,6 +9121,12 @@ snapshots: date-fns@4.4.0: {} + debug@4.4.3(supports-color@10.2.2): + dependencies: + ms: 2.1.3 + optionalDependencies: + supports-color: 10.2.2 + debug@4.4.3(supports-color@7.2.0): dependencies: ms: 2.1.3 @@ -9209,6 +9418,12 @@ snapshots: dependencies: '@types/estree': 1.0.9 + eventsource-parser@3.1.1: {} + + eventsource@3.0.7: + dependencies: + eventsource-parser: 3.1.1 + expect-type@1.3.0: {} expect@30.4.1: @@ -9430,6 +9645,13 @@ snapshots: transitivePeerDependencies: - '@noble/hashes' + https-proxy-agent@7.0.6(supports-color@10.2.2): + dependencies: + agent-base: 7.1.4 + debug: 4.4.3(supports-color@10.2.2) + transitivePeerDependencies: + - supports-color + iconv-lite@0.7.3: dependencies: safer-buffer: 2.1.2 @@ -9449,6 +9671,8 @@ snapshots: indent-string@4.0.0: {} + index-to-position@1.2.0: {} + inflight@1.0.6: dependencies: once: 1.4.0 @@ -9664,14 +9888,22 @@ snapshots: jiti@2.7.0: {} + jose@6.2.10: {} + joycon@3.1.1: {} + js-levenshtein@1.1.6: {} + js-tokens@4.0.0: {} js-yaml@4.1.0: dependencies: argparse: 2.0.1 + js-yaml@4.3.1: + dependencies: + argparse: 2.0.1 + jsdom@30.0.1(@noble/hashes@2.2.0): dependencies: '@asamuzakjp/css-color': 6.0.5 @@ -9886,6 +10118,10 @@ snapshots: dependencies: brace-expansion: 2.0.2 + minimatch@5.1.9: + dependencies: + brace-expansion: 2.0.2 + minimatch@9.0.5: dependencies: brace-expansion: 2.0.2 @@ -9980,14 +10216,14 @@ snapshots: next-intl-swc-plugin-extractor@4.13.4: {} - next-intl@4.13.4(@swc/helpers@0.5.23)(next@16.3.4(@babel/core@7.29.0(supports-color@7.2.0))(@playwright/test@1.62.1)(@types/node@26.4.0)(babel-plugin-react-compiler@19.1.0-rc.2)(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(react@19.2.8)(typescript@6.0.3): + next-intl@4.13.4(@swc/helpers@0.5.23)(next@16.3.4(@babel/core@7.29.0(supports-color@10.2.2))(@playwright/test@1.62.1)(@types/node@26.4.0)(babel-plugin-react-compiler@19.1.0-rc.2)(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(react@19.2.8)(typescript@6.0.3): dependencies: '@formatjs/intl-localematcher': 0.8.13 '@parcel/watcher': 2.6.0 '@swc/core': 1.15.46(@swc/helpers@0.5.23) icu-minify: 4.13.4 negotiator: 1.0.0 - next: 16.3.4(@babel/core@7.29.0(supports-color@7.2.0))(@playwright/test@1.62.1)(@types/node@26.4.0)(babel-plugin-react-compiler@19.1.0-rc.2)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + next: 16.3.4(@babel/core@7.29.0(supports-color@10.2.2))(@playwright/test@1.62.1)(@types/node@26.4.0)(babel-plugin-react-compiler@19.1.0-rc.2)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) next-intl-swc-plugin-extractor: 4.13.4 po-parser: 2.1.1 react: 19.2.8 @@ -9997,7 +10233,7 @@ snapshots: transitivePeerDependencies: - '@swc/helpers' - next@16.3.4(@babel/core@7.29.0(supports-color@7.2.0))(@playwright/test@1.62.1)(@types/node@26.4.0)(babel-plugin-react-compiler@19.1.0-rc.2)(react-dom@19.2.8(react@19.2.8))(react@19.2.8): + next@16.3.4(@babel/core@7.29.0(supports-color@10.2.2))(@playwright/test@1.62.1)(@types/node@26.4.0)(babel-plugin-react-compiler@19.1.0-rc.2)(react-dom@19.2.8(react@19.2.8))(react@19.2.8): dependencies: '@next/env': 16.3.4 '@swc/helpers': 0.5.23 @@ -10006,7 +10242,7 @@ snapshots: postcss: 8.5.23 react: 19.2.8 react-dom: 19.2.8(react@19.2.8) - styled-jsx: 5.1.6(@babel/core@7.29.0(supports-color@7.2.0))(react@19.2.8) + styled-jsx: 5.1.6(@babel/core@7.29.0(supports-color@10.2.2))(react@19.2.8) optionalDependencies: '@next/swc-darwin-arm64': 16.3.4 '@next/swc-darwin-x64': 16.3.4 @@ -10093,6 +10329,16 @@ snapshots: dependencies: wrappy: 1.0.2 + openapi-typescript@7.13.0(typescript@6.0.3): + dependencies: + '@redocly/openapi-core': 1.34.19(supports-color@10.2.2) + ansi-colors: 4.1.3 + change-case: 5.4.4 + parse-json: 8.3.0 + supports-color: 10.2.2 + typescript: 6.0.3 + yargs-parser: 21.1.1 + otplib@13.5.0: dependencies: '@otplib/core': 13.5.0 @@ -10137,11 +10383,17 @@ snapshots: parse-json@5.2.0: dependencies: - '@babel/code-frame': 7.27.1 + '@babel/code-frame': 7.29.7 error-ex: 1.3.2 json-parse-even-better-errors: 2.3.1 lines-and-columns: 1.2.4 + parse-json@8.3.0: + dependencies: + '@babel/code-frame': 7.29.7 + index-to-position: 1.2.0 + type-fest: 4.41.0 + parse5@8.0.1: dependencies: entities: 8.0.0 @@ -10220,6 +10472,8 @@ snapshots: pirates@4.0.7: {} + pkce-challenge@5.0.1: {} + pkg-types@1.3.1: dependencies: confbox: 0.1.8 @@ -10240,6 +10494,8 @@ snapshots: optionalDependencies: fsevents: 2.3.2 + pluralize@8.0.0: {} + pngjs@5.0.0: {} po-parser@2.1.1: {} @@ -11102,12 +11358,12 @@ snapshots: dependencies: min-indent: 1.0.1 - styled-jsx@5.1.6(@babel/core@7.29.0(supports-color@7.2.0))(react@19.2.8): + styled-jsx@5.1.6(@babel/core@7.29.0(supports-color@10.2.2))(react@19.2.8): dependencies: client-only: 0.0.1 react: 19.2.8 optionalDependencies: - '@babel/core': 7.29.0(supports-color@7.2.0) + '@babel/core': 7.29.0(supports-color@10.2.2) sucrase@3.35.0: dependencies: @@ -11119,6 +11375,8 @@ snapshots: pirates: 4.0.7 ts-interface-checker: 0.1.13 + supports-color@10.2.2: {} + supports-color@5.5.0: dependencies: has-flag: 3.0.0 @@ -11234,6 +11492,35 @@ snapshots: tslib@2.8.1: {} + tsup@8.5.1(@swc/core@1.15.46(@swc/helpers@0.5.23))(jiti@2.7.0)(postcss@8.5.26)(supports-color@10.2.2)(tsx@4.23.13)(typescript@6.0.3): + dependencies: + bundle-require: 5.1.0(esbuild@0.27.7) + cac: 6.7.14 + chokidar: 4.0.3 + consola: 3.4.2 + debug: 4.4.3(supports-color@10.2.2) + esbuild: 0.27.7 + fix-dts-default-cjs-exports: 1.0.1 + joycon: 3.1.1 + picocolors: 1.1.1 + postcss-load-config: 6.0.1(jiti@2.7.0)(postcss@8.5.26)(tsx@4.23.13) + resolve-from: 5.0.0 + rollup: 4.63.1 + source-map: 0.7.6 + sucrase: 3.35.0 + tinyexec: 0.3.2 + tinyglobby: 0.2.14 + tree-kill: 1.2.2 + optionalDependencies: + '@swc/core': 1.15.46(@swc/helpers@0.5.23) + postcss: 8.5.26 + typescript: 6.0.3 + transitivePeerDependencies: + - jiti + - supports-color + - tsx + - yaml + tsup@8.5.1(@swc/core@1.15.46(@swc/helpers@0.5.23))(jiti@2.7.0)(postcss@8.5.26)(supports-color@7.2.0)(tsx@4.23.13)(typescript@6.0.3): dependencies: bundle-require: 5.1.0(esbuild@0.27.7) @@ -11269,6 +11556,8 @@ snapshots: optionalDependencies: fsevents: 2.3.3 + type-fest@4.41.0: {} + type-fest@5.6.0: dependencies: tagged-tag: 1.0.0 @@ -11343,6 +11632,8 @@ snapshots: escalade: 3.2.0 picocolors: 1.1.1 + uri-js-replace@1.0.1: {} + use-callback-ref@1.3.3(@types/react@19.2.18)(react@19.2.8): dependencies: react: 19.2.8 @@ -11542,6 +11833,8 @@ snapshots: yallist@5.0.0: {} + yaml-ast-parser@0.0.43: {} + yargs-parser@18.1.3: dependencies: camelcase: 5.3.1 diff --git a/prisma/migrations/27_add_oauth/migration.sql b/prisma/migrations/27_add_oauth/migration.sql new file mode 100644 index 000000000..937a406d4 --- /dev/null +++ b/prisma/migrations/27_add_oauth/migration.sql @@ -0,0 +1,68 @@ +-- CreateTable +CREATE TABLE "oauth_client" ( + "oauth_client_id" UUID NOT NULL, + "name" VARCHAR(255) NOT NULL, + "redirect_uris" JSONB NOT NULL, + "metadata" JSONB, + "created_at" TIMESTAMPTZ(6) DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "oauth_client_pkey" PRIMARY KEY ("oauth_client_id") +); + +-- CreateTable +CREATE TABLE "oauth_authorization_code" ( + "oauth_code_id" UUID NOT NULL, + "code_hash" VARCHAR(128) NOT NULL, + "user_id" UUID NOT NULL, + "client_id" VARCHAR(2048) NOT NULL, + "redirect_uri" VARCHAR(2048) NOT NULL, + "scope" VARCHAR(500) NOT NULL, + "resource" VARCHAR(2048), + "code_challenge" VARCHAR(128) NOT NULL, + "code_challenge_method" VARCHAR(10) NOT NULL, + "expires_at" TIMESTAMPTZ(6) NOT NULL, + "used_at" TIMESTAMPTZ(6), + "created_at" TIMESTAMPTZ(6) DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "oauth_authorization_code_pkey" PRIMARY KEY ("oauth_code_id") +); + +-- CreateTable +CREATE TABLE "oauth_refresh_token" ( + "oauth_refresh_token_id" UUID NOT NULL, + "token_hash" VARCHAR(128) NOT NULL, + "user_id" UUID NOT NULL, + "client_id" VARCHAR(2048) NOT NULL, + "scope" VARCHAR(500) NOT NULL, + "resource" VARCHAR(2048), + "expires_at" TIMESTAMPTZ(6) NOT NULL, + "revoked_at" TIMESTAMPTZ(6), + "last_used_at" TIMESTAMPTZ(6), + "created_at" TIMESTAMPTZ(6) DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "oauth_refresh_token_pkey" PRIMARY KEY ("oauth_refresh_token_id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "oauth_authorization_code_code_hash_key" ON "oauth_authorization_code"("code_hash"); + +-- CreateIndex +CREATE INDEX "oauth_authorization_code_user_id_idx" ON "oauth_authorization_code"("user_id"); + +-- CreateIndex +CREATE INDEX "oauth_authorization_code_expires_at_idx" ON "oauth_authorization_code"("expires_at"); + +-- CreateIndex +CREATE UNIQUE INDEX "oauth_refresh_token_token_hash_key" ON "oauth_refresh_token"("token_hash"); + +-- CreateIndex +CREATE INDEX "oauth_refresh_token_user_id_idx" ON "oauth_refresh_token"("user_id"); + +-- CreateIndex +CREATE INDEX "oauth_refresh_token_client_id_idx" ON "oauth_refresh_token"("client_id"); + +-- AddForeignKey +ALTER TABLE "oauth_authorization_code" ADD CONSTRAINT "oauth_authorization_code_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "user"("user_id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "oauth_refresh_token" ADD CONSTRAINT "oauth_refresh_token_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "user"("user_id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index a57873235..fd646aaea 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -33,10 +33,62 @@ model User { twoFactorOtpUseds TwoFactorOtpUsed[] twoFactorRateLimit TwoFactorRateLimit? apiKeys ApiKey[] + oauthAuthorizationCodes OauthAuthorizationCode[] + oauthRefreshTokens OauthRefreshToken[] @@map("user") } +model OauthClient { + id String @id() @map("oauth_client_id") @db.Uuid + name String @db.VarChar(255) + redirectUris Json @map("redirect_uris") + metadata Json? + createdAt DateTime? @default(now()) @map("created_at") @db.Timestamptz(6) + + @@map("oauth_client") +} + +model OauthAuthorizationCode { + id String @id() @map("oauth_code_id") @db.Uuid + codeHash String @unique @map("code_hash") @db.VarChar(128) + userId String @map("user_id") @db.Uuid + clientId String @map("client_id") @db.VarChar(2048) + redirectUri String @map("redirect_uri") @db.VarChar(2048) + scope String @db.VarChar(500) + resource String? @db.VarChar(2048) + codeChallenge String @map("code_challenge") @db.VarChar(128) + codeChallengeMethod String @map("code_challenge_method") @db.VarChar(10) + expiresAt DateTime @map("expires_at") @db.Timestamptz(6) + usedAt DateTime? @map("used_at") @db.Timestamptz(6) + createdAt DateTime? @default(now()) @map("created_at") @db.Timestamptz(6) + + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + @@index([userId]) + @@index([expiresAt]) + @@map("oauth_authorization_code") +} + +model OauthRefreshToken { + id String @id() @map("oauth_refresh_token_id") @db.Uuid + tokenHash String @unique @map("token_hash") @db.VarChar(128) + userId String @map("user_id") @db.Uuid + clientId String @map("client_id") @db.VarChar(2048) + scope String @db.VarChar(500) + resource String? @db.VarChar(2048) + expiresAt DateTime @map("expires_at") @db.Timestamptz(6) + revokedAt DateTime? @map("revoked_at") @db.Timestamptz(6) + lastUsedAt DateTime? @map("last_used_at") @db.Timestamptz(6) + createdAt DateTime? @default(now()) @map("created_at") @db.Timestamptz(6) + + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + @@index([userId]) + @@index([clientId]) + @@map("oauth_refresh_token") +} + model ApiKey { id String @id() @map("api_key_id") @db.Uuid userId String @map("user_id") @db.Uuid diff --git a/public/intl/messages/en-US.json b/public/intl/messages/en-US.json index 34b26c98f..33e93580b 100644 --- a/public/intl/messages/en-US.json +++ b/public/intl/messages/en-US.json @@ -431,7 +431,9 @@ "create-api-key": "Create API key", "last-used": "Last used", "never": "Never", - "copy": "Copy" + "copy": "Copy", + "authorize": "Authorize", + "deny": "Deny" }, "message": { "action-confirmation": "Type {confirmation} in the box below to confirm.", @@ -518,6 +520,15 @@ "two-factor-locked-message": "Too many failed attempts. Try again at {time}.", "two-factor-active-description": "Your account is protected with two-factor authentication. Each login requires a code from your authenticator app.", "api-key-created": "Copy your API key now. You won't be able to see it again.", - "no-api-keys": "You have not created any API keys." + "no-api-keys": "You have not created any API keys.", + "oauth-login-prompt": "Sign in to continue.", + "oauth-consent": "{name} wants to access your Umami account.", + "oauth-permissions": "This will allow the application to:", + "oauth-scope-websites-read": "See the websites you have access to", + "oauth-scope-analytics-read": "Read analytics data for those websites", + "oauth-redirect-notice": "After you approve, you will be sent to {url}.", + "oauth-signed-in-as": "Signed in as {username}", + "oauth-invalid-request": "This authorization request is invalid.", + "oauth-redirecting": "Redirecting…" } } diff --git a/public/openapi.json b/public/openapi.json index bb22993c1..d3a09e9bd 100644 --- a/public/openapi.json +++ b/public/openapi.json @@ -122,6 +122,20 @@ } }, "schemas": { + "ActiveVisitors": { + "additionalProperties": false, + "properties": { + "visitors": { + "maximum": 9007199254740991, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "visitors" + ], + "type": "object" + }, "ApiError": { "additionalProperties": false, "description": "Standard Umami API error response.", @@ -337,6 +351,408 @@ ], "type": "object" }, + "MetricRow": { + "additionalProperties": false, + "properties": { + "t": { + "type": "string" + }, + "x": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Dimension value (e.g. a path or country)." + }, + "y": { + "description": "Count of views/events or unique visitors.", + "type": "number" + } + }, + "required": [ + "x", + "y" + ], + "type": "object" + }, + "MetricRows": { + "items": { + "$ref": "#/components/schemas/MetricRow" + }, + "type": "array" + }, + "OAuthAuthorizationDecision": { + "properties": { + "client_id": { + "maxLength": 2048, + "type": "string" + }, + "code_challenge": { + "maxLength": 128, + "type": "string" + }, + "code_challenge_method": { + "maxLength": 10, + "type": "string" + }, + "decision": { + "enum": [ + "approve", + "deny" + ], + "type": "string" + }, + "redirect_uri": { + "maxLength": 2048, + "type": "string" + }, + "resource": { + "maxLength": 2048, + "type": "string" + }, + "response_type": { + "type": "string" + }, + "scope": { + "maxLength": 500, + "type": "string" + }, + "state": { + "maxLength": 1024, + "type": "string" + } + }, + "required": [ + "client_id", + "decision" + ], + "type": "object" + }, + "OAuthAuthorizationDetails": { + "additionalProperties": false, + "properties": { + "client": { + "additionalProperties": false, + "properties": { + "id": { + "type": "string" + }, + "logoUri": { + "type": "string" + }, + "name": { + "type": "string" + }, + "source": { + "enum": [ + "metadata-document", + "registered" + ], + "type": "string" + }, + "uri": { + "type": "string" + } + }, + "required": [ + "id", + "name", + "source" + ], + "type": "object" + }, + "redirectUri": { + "type": "string" + }, + "resource": { + "type": "string" + }, + "scopes": { + "items": { + "additionalProperties": false, + "properties": { + "description": { + "type": "string" + }, + "scope": { + "type": "string" + } + }, + "required": [ + "scope", + "description" + ], + "type": "object" + }, + "type": "array" + } + }, + "required": [ + "client", + "scopes", + "redirectUri", + "resource" + ], + "type": "object" + }, + "OAuthAuthorizationRedirect": { + "additionalProperties": false, + "properties": { + "redirectUrl": { + "type": "string" + } + }, + "required": [ + "redirectUrl" + ], + "type": "object" + }, + "OAuthClientRegistrationRequest": { + "properties": { + "application_type": { + "enum": [ + "web", + "native" + ], + "type": "string" + }, + "client_name": { + "maxLength": 255, + "minLength": 1, + "type": "string" + }, + "client_uri": { + "maxLength": 2048, + "type": "string" + }, + "grant_types": { + "items": { + "type": "string" + }, + "type": "array" + }, + "logo_uri": { + "maxLength": 2048, + "type": "string" + }, + "redirect_uris": { + "items": { + "maxLength": 2048, + "type": "string" + }, + "maxItems": 20, + "minItems": 1, + "type": "array" + }, + "response_types": { + "items": { + "type": "string" + }, + "type": "array" + }, + "scope": { + "maxLength": 500, + "type": "string" + }, + "software_id": { + "maxLength": 255, + "type": "string" + }, + "software_version": { + "maxLength": 255, + "type": "string" + }, + "token_endpoint_auth_method": { + "const": "none", + "type": "string" + } + }, + "required": [ + "redirect_uris", + "client_name" + ], + "type": "object" + }, + "OAuthClientRegistrationResponse": { + "additionalProperties": false, + "properties": { + "application_type": { + "enum": [ + "web", + "native" + ], + "type": "string" + }, + "client_id": { + "type": "string" + }, + "client_id_issued_at": { + "maximum": 9007199254740991, + "minimum": -9007199254740991, + "type": "integer" + }, + "client_name": { + "maxLength": 255, + "minLength": 1, + "type": "string" + }, + "client_uri": { + "maxLength": 2048, + "type": "string" + }, + "grant_types": { + "items": { + "type": "string" + }, + "type": "array" + }, + "logo_uri": { + "maxLength": 2048, + "type": "string" + }, + "redirect_uris": { + "items": { + "maxLength": 2048, + "type": "string" + }, + "maxItems": 20, + "minItems": 1, + "type": "array" + }, + "response_types": { + "items": { + "type": "string" + }, + "type": "array" + }, + "scope": { + "maxLength": 500, + "type": "string" + }, + "software_id": { + "maxLength": 255, + "type": "string" + }, + "software_version": { + "maxLength": 255, + "type": "string" + }, + "token_endpoint_auth_method": { + "const": "none", + "type": "string" + } + }, + "required": [ + "redirect_uris", + "client_name", + "token_endpoint_auth_method", + "grant_types", + "response_types", + "client_id", + "client_id_issued_at" + ], + "type": "object" + }, + "OAuthErrorResponse": { + "additionalProperties": false, + "properties": { + "error": { + "type": "string" + }, + "error_description": { + "type": "string" + } + }, + "required": [ + "error" + ], + "type": "object" + }, + "OAuthRevocationRequest": { + "properties": { + "token": { + "type": "string" + }, + "token_type_hint": { + "type": "string" + } + }, + "required": [ + "token" + ], + "type": "object" + }, + "OAuthTokenRequest": { + "properties": { + "client_id": { + "type": "string" + }, + "code": { + "type": "string" + }, + "code_verifier": { + "type": "string" + }, + "grant_type": { + "enum": [ + "authorization_code", + "refresh_token" + ], + "type": "string" + }, + "redirect_uri": { + "type": "string" + }, + "refresh_token": { + "type": "string" + }, + "resource": { + "type": "string" + }, + "scope": { + "type": "string" + } + }, + "required": [ + "grant_type" + ], + "type": "object" + }, + "OAuthTokenResponse": { + "additionalProperties": false, + "properties": { + "access_token": { + "type": "string" + }, + "expires_in": { + "maximum": 9007199254740991, + "minimum": -9007199254740991, + "type": "integer" + }, + "refresh_token": { + "type": "string" + }, + "scope": { + "type": "string" + }, + "token_type": { + "const": "Bearer", + "type": "string" + } + }, + "required": [ + "access_token", + "token_type", + "expires_in", + "refresh_token", + "scope" + ], + "type": "object" + }, "Ok": { "additionalProperties": false, "description": "Successful operation response.", @@ -424,6 +840,24 @@ }, "type": "object" }, + "TimeSeriesPoint": { + "additionalProperties": false, + "properties": { + "x": { + "description": "Bucket start (ISO date/time).", + "type": "string" + }, + "y": { + "description": "Value for the bucket.", + "type": "number" + } + }, + "required": [ + "x", + "y" + ], + "type": "object" + }, "UpdateWebsiteRequest": { "properties": { "domain": { @@ -609,6 +1043,150 @@ ], "type": "object" }, + "WebsiteEvent": { + "additionalProperties": {}, + "properties": { + "createdAt": { + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", + "type": "string" + }, + "distinctId": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "eventName": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "eventType": { + "maximum": 9007199254740991, + "minimum": -9007199254740991, + "type": "integer" + }, + "hostname": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "id": { + "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$", + "type": "string" + }, + "pageTitle": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "referrerDomain": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "sessionId": { + "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$", + "type": "string" + }, + "urlPath": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "urlQuery": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "websiteId": { + "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$", + "type": "string" + } + }, + "required": [ + "id", + "websiteId", + "sessionId", + "createdAt", + "eventType" + ], + "type": "object" + }, + "WebsiteEventPage": { + "additionalProperties": false, + "properties": { + "count": { + "maximum": 9007199254740991, + "minimum": 0, + "type": "integer" + }, + "data": { + "items": { + "$ref": "#/components/schemas/WebsiteEvent" + }, + "type": "array" + }, + "isCapped": { + "type": "boolean" + }, + "page": { + "exclusiveMinimum": 0, + "maximum": 9007199254740991, + "type": "integer" + }, + "pageSize": { + "maximum": 9007199254740991, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "data", + "count", + "page", + "pageSize" + ], + "type": "object" + }, "WebsitePage": { "additionalProperties": false, "properties": { @@ -648,6 +1226,328 @@ ], "type": "object" }, + "WebsitePageviews": { + "additionalProperties": false, + "properties": { + "compare": { + "additionalProperties": false, + "description": "Present when `compare` was requested.", + "properties": { + "endDate": { + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", + "type": "string" + }, + "pageviews": { + "items": { + "$ref": "#/components/schemas/TimeSeriesPoint" + }, + "type": "array" + }, + "sessions": { + "items": { + "$ref": "#/components/schemas/TimeSeriesPoint" + }, + "type": "array" + }, + "startDate": { + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", + "type": "string" + } + }, + "required": [ + "pageviews", + "sessions", + "startDate", + "endDate" + ], + "type": "object" + }, + "endDate": { + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", + "type": "string" + }, + "pageviews": { + "items": { + "$ref": "#/components/schemas/TimeSeriesPoint" + }, + "type": "array" + }, + "sessions": { + "items": { + "$ref": "#/components/schemas/TimeSeriesPoint" + }, + "type": "array" + }, + "startDate": { + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", + "type": "string" + } + }, + "required": [ + "pageviews", + "sessions" + ], + "type": "object" + }, + "WebsiteSession": { + "additionalProperties": {}, + "properties": { + "browser": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "city": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "country": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "createdAt": { + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", + "type": "string" + }, + "device": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "distinctId": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "events": { + "type": "number" + }, + "firstAt": { + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", + "type": "string" + }, + "hostname": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "id": { + "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$", + "type": "string" + }, + "language": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "lastAt": { + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", + "type": "string" + }, + "os": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "region": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "screen": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "views": { + "type": "number" + }, + "visits": { + "type": "number" + }, + "websiteId": { + "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$", + "type": "string" + } + }, + "required": [ + "id", + "websiteId", + "hostname", + "browser", + "os", + "device", + "screen", + "language", + "country", + "region", + "city", + "firstAt", + "lastAt", + "visits", + "views", + "createdAt" + ], + "type": "object" + }, + "WebsiteSessionPage": { + "additionalProperties": false, + "properties": { + "count": { + "maximum": 9007199254740991, + "minimum": 0, + "type": "integer" + }, + "data": { + "items": { + "$ref": "#/components/schemas/WebsiteSession" + }, + "type": "array" + }, + "isCapped": { + "type": "boolean" + }, + "page": { + "exclusiveMinimum": 0, + "maximum": 9007199254740991, + "type": "integer" + }, + "pageSize": { + "maximum": 9007199254740991, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "data", + "count", + "page", + "pageSize" + ], + "type": "object" + }, + "WebsiteStats": { + "additionalProperties": false, + "properties": { + "bounces": { + "type": "number" + }, + "comparison": { + "$ref": "#/components/schemas/WebsiteStatsValues", + "description": "The same totals for the comparison period (previous period by default)." + }, + "pageviews": { + "type": "number" + }, + "totaltime": { + "type": "number" + }, + "visitors": { + "type": "number" + }, + "visits": { + "type": "number" + } + }, + "required": [ + "pageviews", + "visitors", + "visits", + "bounces", + "totaltime", + "comparison" + ], + "type": "object" + }, + "WebsiteStatsValues": { + "additionalProperties": false, + "properties": { + "bounces": { + "type": "number" + }, + "pageviews": { + "type": "number" + }, + "totaltime": { + "type": "number" + }, + "visitors": { + "type": "number" + }, + "visits": { + "type": "number" + } + }, + "required": [ + "pageviews", + "visitors", + "visits", + "bounces", + "totaltime" + ], + "type": "object" + }, "WebsiteUser": { "additionalProperties": false, "properties": { @@ -674,6 +1574,21 @@ "scheme": "bearer", "type": "http" }, + "oauth2": { + "description": "OAuth 2.1 access token (authorization code + PKCE). Only operations that declare an OAuth scope accept these tokens.", + "flows": { + "authorizationCode": { + "authorizationUrl": "/oauth/authorize", + "refreshUrl": "/api/oauth/token", + "scopes": { + "analytics:read": "Read analytics data (traffic, metrics, events, sessions, reports).", + "websites:read": "List and view websites you have access to." + }, + "tokenUrl": "/api/oauth/token" + } + }, + "type": "oauth2" + }, "shareContext": { "description": "Required context header when authenticating with a share token.", "in": "header", @@ -705,7 +1620,7 @@ "paths": { "/api/2fa/disable": { "post": { - "operationId": "post2faDisable", + "operationId": "disableTwoFactor", "requestBody": { "content": { "application/json": { @@ -918,7 +1833,7 @@ }, "/api/2fa/setup/cancel": { "post": { - "operationId": "post2faSetupCancel", + "operationId": "cancelTwoFactorSetup", "responses": { "200": { "content": { @@ -989,7 +1904,7 @@ }, "/api/2fa/setup/confirm": { "post": { - "operationId": "post2faSetupConfirm", + "operationId": "confirmTwoFactorSetup", "requestBody": { "content": { "application/json": { @@ -1184,7 +2099,7 @@ }, "/api/2fa/setup/initiate": { "post": { - "operationId": "post2faSetupInitiate", + "operationId": "initiateTwoFactorSetup", "responses": { "200": { "content": { @@ -1293,7 +2208,7 @@ }, "/api/2fa/status": { "get": { - "operationId": "get2faStatus", + "operationId": "getTwoFactorStatus", "responses": { "200": { "content": { @@ -1394,7 +2309,7 @@ }, "/api/2fa/verify": { "post": { - "operationId": "post2faVerify", + "operationId": "verifyTwoFactor", "requestBody": { "content": { "application/json": { @@ -3053,7 +3968,7 @@ }, "/api/auth/logout": { "post": { - "operationId": "postAuthLogout", + "operationId": "logout", "responses": { "200": { "content": { @@ -3107,7 +4022,7 @@ }, "/api/auth/sso": { "post": { - "operationId": "postAuthSso", + "operationId": "sso", "responses": { "200": { "content": { @@ -3180,7 +4095,7 @@ }, "/api/auth/subscription": { "get": { - "operationId": "getAuthSubscription", + "operationId": "getSubscription", "parameters": [ { "in": "query", @@ -3313,7 +4228,7 @@ }, "/api/auth/verify": { "post": { - "operationId": "postAuthVerify", + "operationId": "verify", "responses": { "200": { "content": { @@ -3365,7 +4280,7 @@ }, "/api/batch": { "post": { - "operationId": "postBatch", + "operationId": "batch", "requestBody": { "content": { "application/json": { @@ -3711,7 +4626,7 @@ "x-umami-source": "src/app/api/boards/contract.generated.ts" }, "post": { - "operationId": "postBoards", + "operationId": "createBoard", "requestBody": { "content": { "application/json": { @@ -3950,7 +4865,7 @@ }, "/api/boards/{boardId}": { "delete": { - "operationId": "deleteBoardsBoardId", + "operationId": "deleteBoard", "parameters": [ { "in": "path", @@ -4012,7 +4927,7 @@ "x-umami-source": "src/app/api/boards/[boardId]/contract.generated.ts" }, "get": { - "operationId": "getBoardsBoardId", + "operationId": "getBoard", "parameters": [ { "in": "path", @@ -4168,7 +5083,7 @@ "x-umami-source": "src/app/api/boards/[boardId]/contract.generated.ts" }, "post": { - "operationId": "postBoardsBoardId", + "operationId": "updateBoard", "parameters": [ { "in": "path", @@ -4395,7 +5310,7 @@ }, "/api/boards/{boardId}/clone": { "post": { - "operationId": "postBoardsBoardIdClone", + "operationId": "cloneBoard", "parameters": [ { "in": "path", @@ -4594,7 +5509,7 @@ }, "/api/boards/{boardId}/shares": { "get": { - "operationId": "getBoardsBoardIdShares", + "operationId": "getBoardShares", "parameters": [ { "in": "path", @@ -5032,7 +5947,7 @@ "x-umami-source": "src/app/api/boards/[boardId]/shares/contract.generated.ts" }, "post": { - "operationId": "postBoardsBoardIdShares", + "operationId": "createBoardShare", "parameters": [ { "in": "path", @@ -5843,7 +6758,7 @@ "x-umami-source": "src/app/api/links/contract.generated.ts" }, "post": { - "operationId": "postLinks", + "operationId": "createLink", "requestBody": { "content": { "application/json": { @@ -6000,7 +6915,7 @@ }, "/api/links/{linkId}": { "delete": { - "operationId": "deleteLinksLinkId", + "operationId": "deleteLink", "parameters": [ { "in": "path", @@ -6062,7 +6977,7 @@ "x-umami-source": "src/app/api/links/[linkId]/contract.generated.ts" }, "get": { - "operationId": "getLinksLinkId", + "operationId": "getLink", "parameters": [ { "in": "path", @@ -6163,7 +7078,7 @@ "x-umami-source": "src/app/api/links/[linkId]/contract.generated.ts" }, "post": { - "operationId": "postLinksLinkId", + "operationId": "updateLink", "parameters": [ { "in": "path", @@ -6321,7 +7236,7 @@ }, "/api/links/{linkId}/shares": { "get": { - "operationId": "getLinksLinkIdShares", + "operationId": "getLinkShares", "parameters": [ { "in": "path", @@ -6759,7 +7674,7 @@ "x-umami-source": "src/app/api/links/[linkId]/shares/contract.generated.ts" }, "post": { - "operationId": "postLinksLinkIdShares", + "operationId": "createLinkShare", "parameters": [ { "in": "path", @@ -7099,6 +8014,43 @@ ], "type": "object" }, + "authType": { + "anyOf": [ + { + "const": "session" + }, + { + "const": "share" + }, + { + "const": "api-key" + }, + { + "const": "oauth" + } + ] + }, + "oauth": { + "properties": { + "clientId": { + "type": "string" + }, + "scopes": { + "items": { + "type": "string" + }, + "type": "array" + }, + "tokenId": { + "type": "string" + } + }, + "required": [ + "clientId", + "scopes" + ], + "type": "object" + }, "shareToken": { "properties": { "boardId": { @@ -7222,6 +8174,11 @@ "security": [ { "bearerAuth": [] + }, + { + "oauth2": [ + "websites:read" + ] } ], "summary": "Get me", @@ -7230,12 +8187,13 @@ ], "x-umami-audience": "public", "x-umami-contract": "explicit", + "x-umami-oauth-scope": "websites:read", "x-umami-source": "src/app/api/me/contract.generated.ts" } }, "/api/me/api-keys": { "get": { - "operationId": "getMeApiKeys", + "operationId": "getMyApiKeys", "responses": { "200": { "content": { @@ -7325,7 +8283,7 @@ "x-umami-source": "src/app/api/me/api-keys/contract.generated.ts" }, "post": { - "operationId": "postMeApiKeys", + "operationId": "createMyApiKey", "requestBody": { "content": { "application/json": { @@ -7449,7 +8407,7 @@ }, "/api/me/api-keys/{keyId}": { "delete": { - "operationId": "deleteMeApiKeysKeyId", + "operationId": "deleteMyApiKey", "parameters": [ { "in": "path", @@ -7530,7 +8488,7 @@ }, "/api/me/password": { "post": { - "operationId": "postMePassword", + "operationId": "updateMyPassword", "requestBody": { "content": { "application/json": { @@ -7637,7 +8595,7 @@ }, "/api/me/teams": { "get": { - "operationId": "getMeTeams", + "operationId": "getMyTeams", "parameters": [ { "in": "query", @@ -7823,7 +8781,7 @@ }, "/api/me/websites": { "get": { - "operationId": "getMeWebsites", + "operationId": "getMyWebsites", "parameters": [ { "in": "query", @@ -8159,6 +9117,361 @@ "x-umami-source": "src/app/api/me/websites/contract.generated.ts" } }, + "/api/oauth/authorize": { + "get": { + "description": "Validates an OAuth 2.1 authorization request and returns the client and scopes for the consent screen. Requires an interactive user session.", + "operationId": "describeOAuthAuthorization", + "parameters": [ + { + "in": "query", + "name": "response_type", + "schema": { + "type": "string" + } + }, + { + "in": "query", + "name": "client_id", + "required": true, + "schema": { + "maxLength": 2048, + "type": "string" + } + }, + { + "in": "query", + "name": "redirect_uri", + "schema": { + "maxLength": 2048, + "type": "string" + } + }, + { + "in": "query", + "name": "scope", + "schema": { + "maxLength": 500, + "type": "string" + } + }, + { + "in": "query", + "name": "state", + "schema": { + "maxLength": 1024, + "type": "string" + } + }, + { + "in": "query", + "name": "code_challenge", + "schema": { + "maxLength": 128, + "type": "string" + } + }, + { + "in": "query", + "name": "code_challenge_method", + "schema": { + "maxLength": 10, + "type": "string" + } + }, + { + "in": "query", + "name": "resource", + "schema": { + "maxLength": 2048, + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OAuthAuthorizationDetails" + } + } + }, + "description": "Authorization request details." + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OAuthErrorResponse" + } + } + }, + "description": "Invalid authorization request." + }, + "401": { + "content": { + "application/json": { + "example": { + "error": { + "code": "unauthorized", + "message": "Unauthorized.", + "status": 401 + } + }, + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + }, + "description": "Unauthorized." + } + }, + "security": [ + { + "bearerAuth": [] + } + ], + "summary": "Describe a pending OAuth authorization request", + "tags": [ + "OAuth" + ], + "x-umami-audience": "internal", + "x-umami-contract": "explicit", + "x-umami-source": "src/app/api/oauth/authorize/contract.ts" + }, + "post": { + "description": "Records the user decision. On approval an authorization code is issued and the redirect URL (with code, state and iss) is returned.", + "operationId": "decideOAuthAuthorization", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OAuthAuthorizationDecision" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OAuthAuthorizationRedirect" + } + } + }, + "description": "Redirect URL for the browser." + }, + "400": { + "content": { + "application/json": { + "example": { + "error": { + "code": "bad-request", + "message": "Bad request.", + "status": 400 + } + }, + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + }, + "description": "Bad request." + }, + "401": { + "content": { + "application/json": { + "example": { + "error": { + "code": "unauthorized", + "message": "Unauthorized.", + "status": 401 + } + }, + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + }, + "description": "Unauthorized." + } + }, + "security": [ + { + "bearerAuth": [] + } + ], + "summary": "Approve or deny an OAuth authorization request", + "tags": [ + "OAuth" + ], + "x-umami-audience": "internal", + "x-umami-contract": "explicit", + "x-umami-source": "src/app/api/oauth/authorize/contract.ts" + } + }, + "/api/oauth/register": { + "post": { + "description": "RFC 7591 compatibility endpoint for MCP clients without Client ID Metadata Document support. Registers a public client. Prefer Client ID Metadata Documents (https URL client_id).", + "operationId": "oauthRegisterClient", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OAuthClientRegistrationRequest" + } + } + }, + "required": true + }, + "responses": { + "201": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OAuthClientRegistrationResponse" + } + } + }, + "description": "Registered client." + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OAuthErrorResponse" + } + } + }, + "description": "Invalid client metadata." + } + }, + "security": [], + "summary": "Register an OAuth client (dynamic client registration)", + "tags": [ + "OAuth" + ], + "x-umami-audience": "internal", + "x-umami-contract": "explicit", + "x-umami-source": "src/app/api/oauth/register/contract.ts" + } + }, + "/api/oauth/revoke": { + "post": { + "description": "RFC 7009 revocation endpoint. Revokes the given refresh token; unknown tokens are accepted silently.", + "operationId": "oauthRevoke", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OAuthRevocationRequest" + } + }, + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/OAuthRevocationRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "additionalProperties": false, + "properties": {}, + "type": "object" + } + } + }, + "description": "Token revoked (or already invalid)." + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OAuthErrorResponse" + } + } + }, + "description": "Malformed request." + } + }, + "security": [], + "summary": "Revoke an OAuth refresh token", + "tags": [ + "OAuth" + ], + "x-umami-audience": "internal", + "x-umami-contract": "explicit", + "x-umami-source": "src/app/api/oauth/revoke/contract.ts" + } + }, + "/api/oauth/token": { + "post": { + "description": "Exchanges an authorization code (with PKCE code_verifier) or a refresh token for an access token. Accepts application/x-www-form-urlencoded or JSON. Refresh tokens are rotated on every use.", + "operationId": "oauthToken", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OAuthTokenRequest" + } + }, + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/OAuthTokenRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OAuthTokenResponse" + } + } + }, + "description": "Access and refresh tokens." + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OAuthErrorResponse" + } + } + }, + "description": "Invalid grant or request." + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OAuthErrorResponse" + } + } + }, + "description": "Invalid client." + } + }, + "security": [], + "summary": "OAuth 2.1 token endpoint", + "tags": [ + "OAuth" + ], + "x-umami-audience": "internal", + "x-umami-contract": "explicit", + "x-umami-source": "src/app/api/oauth/token/contract.ts" + } + }, "/api/pixels": { "get": { "operationId": "getPixels", @@ -8353,7 +9666,7 @@ "x-umami-source": "src/app/api/pixels/contract.generated.ts" }, "post": { - "operationId": "postPixels", + "operationId": "createPixel", "requestBody": { "content": { "application/json": { @@ -8501,7 +9814,7 @@ }, "/api/pixels/{pixelId}": { "delete": { - "operationId": "deletePixelsPixelId", + "operationId": "deletePixel", "parameters": [ { "in": "path", @@ -8563,7 +9876,7 @@ "x-umami-source": "src/app/api/pixels/[pixelId]/contract.generated.ts" }, "get": { - "operationId": "getPixelsPixelId", + "operationId": "getPixel", "parameters": [ { "in": "path", @@ -8660,7 +9973,7 @@ "x-umami-source": "src/app/api/pixels/[pixelId]/contract.generated.ts" }, "post": { - "operationId": "postPixelsPixelId", + "operationId": "updatePixel", "parameters": [ { "in": "path", @@ -8810,7 +10123,7 @@ }, "/api/pixels/{pixelId}/shares": { "get": { - "operationId": "getPixelsPixelIdShares", + "operationId": "getPixelShares", "parameters": [ { "in": "path", @@ -9248,7 +10561,7 @@ "x-umami-source": "src/app/api/pixels/[pixelId]/shares/contract.generated.ts" }, "post": { - "operationId": "postPixelsPixelIdShares", + "operationId": "createPixelShare", "parameters": [ { "in": "path", @@ -9566,7 +10879,7 @@ }, "/api/realtime/{websiteId}": { "get": { - "operationId": "getRealtimeWebsiteId", + "operationId": "getRealtime", "parameters": [ { "in": "path", @@ -10033,6 +11346,11 @@ { "bearerAuth": [] }, + { + "oauth2": [ + "analytics:read" + ] + }, { "shareContext": [], "shareToken": [] @@ -10044,12 +11362,13 @@ ], "x-umami-audience": "public", "x-umami-contract": "explicit", + "x-umami-oauth-scope": "analytics:read", "x-umami-source": "src/app/api/realtime/[websiteId]/contract.generated.ts" } }, "/api/record": { "post": { - "operationId": "postRecord", + "operationId": "record", "requestBody": { "content": { "application/json": { @@ -10603,7 +11922,7 @@ "x-umami-source": "src/app/api/reports/contract.generated.ts" }, "post": { - "operationId": "postReports", + "operationId": "createReport", "requestBody": { "content": { "application/json": { @@ -10801,7 +12120,7 @@ }, "/api/reports/{reportId}": { "delete": { - "operationId": "deleteReportsReportId", + "operationId": "deleteReport", "parameters": [ { "in": "path", @@ -10880,7 +12199,7 @@ "x-umami-source": "src/app/api/reports/[reportId]/contract.generated.ts" }, "get": { - "operationId": "getReportsReportId", + "operationId": "getReport", "parameters": [ { "in": "path", @@ -11053,7 +12372,7 @@ "x-umami-source": "src/app/api/reports/[reportId]/contract.generated.ts" }, "post": { - "operationId": "postReportsReportId", + "operationId": "updateReport", "parameters": [ { "in": "path", @@ -11278,7 +12597,7 @@ }, "/api/reports/attribution": { "post": { - "operationId": "postReportsAttribution", + "operationId": "runAttributionReport", "requestBody": { "content": { "application/json": { @@ -11519,6 +12838,11 @@ { "bearerAuth": [] }, + { + "oauth2": [ + "analytics:read" + ] + }, { "shareContext": [], "shareToken": [] @@ -11530,12 +12854,13 @@ ], "x-umami-audience": "public", "x-umami-contract": "explicit", + "x-umami-oauth-scope": "analytics:read", "x-umami-source": "src/app/api/reports/attribution/contract.generated.ts" } }, "/api/reports/breakdown": { "post": { - "operationId": "postReportsBreakdown", + "operationId": "runBreakdownReport", "requestBody": { "content": { "application/json": { @@ -11656,6 +12981,11 @@ { "bearerAuth": [] }, + { + "oauth2": [ + "analytics:read" + ] + }, { "shareContext": [], "shareToken": [] @@ -11667,12 +12997,13 @@ ], "x-umami-audience": "public", "x-umami-contract": "explicit", + "x-umami-oauth-scope": "analytics:read", "x-umami-source": "src/app/api/reports/breakdown/contract.generated.ts" } }, "/api/reports/funnel": { "post": { - "operationId": "postReportsFunnel", + "operationId": "runFunnelReport", "requestBody": { "content": { "application/json": { @@ -11813,6 +13144,11 @@ { "bearerAuth": [] }, + { + "oauth2": [ + "analytics:read" + ] + }, { "shareContext": [], "shareToken": [] @@ -11824,12 +13160,13 @@ ], "x-umami-audience": "public", "x-umami-contract": "explicit", + "x-umami-oauth-scope": "analytics:read", "x-umami-source": "src/app/api/reports/funnel/contract.generated.ts" } }, "/api/reports/goal": { "post": { - "operationId": "postReportsGoal", + "operationId": "runGoalReport", "requestBody": { "content": { "application/json": { @@ -11941,7 +13278,7 @@ }, "/api/reports/heatmap": { "post": { - "operationId": "postReportsHeatmap", + "operationId": "runHeatmapReport", "requestBody": { "content": { "application/json": { @@ -12225,7 +13562,7 @@ }, "/api/reports/journey": { "post": { - "operationId": "postReportsJourney", + "operationId": "runJourneyReport", "requestBody": { "content": { "application/json": { @@ -12348,6 +13685,11 @@ { "bearerAuth": [] }, + { + "oauth2": [ + "analytics:read" + ] + }, { "shareContext": [], "shareToken": [] @@ -12359,12 +13701,13 @@ ], "x-umami-audience": "public", "x-umami-contract": "explicit", + "x-umami-oauth-scope": "analytics:read", "x-umami-source": "src/app/api/reports/journey/contract.generated.ts" } }, "/api/reports/performance": { "post": { - "operationId": "postReportsPerformance", + "operationId": "runPerformanceReport", "requestBody": { "content": { "application/json": { @@ -12480,7 +13823,7 @@ }, "/api/reports/retention": { "post": { - "operationId": "postReportsRetention", + "operationId": "runRetentionReport", "requestBody": { "content": { "application/json": { @@ -12591,6 +13934,11 @@ { "bearerAuth": [] }, + { + "oauth2": [ + "analytics:read" + ] + }, { "shareContext": [], "shareToken": [] @@ -12602,12 +13950,13 @@ ], "x-umami-audience": "public", "x-umami-contract": "explicit", + "x-umami-oauth-scope": "analytics:read", "x-umami-source": "src/app/api/reports/retention/contract.generated.ts" } }, "/api/reports/revenue": { "post": { - "operationId": "postReportsRevenue", + "operationId": "runRevenueReport", "requestBody": { "content": { "application/json": { @@ -12804,6 +14153,11 @@ { "bearerAuth": [] }, + { + "oauth2": [ + "analytics:read" + ] + }, { "shareContext": [], "shareToken": [] @@ -12815,12 +14169,13 @@ ], "x-umami-audience": "public", "x-umami-contract": "explicit", + "x-umami-oauth-scope": "analytics:read", "x-umami-source": "src/app/api/reports/revenue/contract.generated.ts" } }, "/api/reports/utm": { "post": { - "operationId": "postReportsUtm", + "operationId": "runUtmReport", "requestBody": { "content": { "application/json": { @@ -12933,6 +14288,11 @@ { "bearerAuth": [] }, + { + "oauth2": [ + "analytics:read" + ] + }, { "shareContext": [], "shareToken": [] @@ -12944,6 +14304,7 @@ ], "x-umami-audience": "public", "x-umami-contract": "explicit", + "x-umami-oauth-scope": "analytics:read", "x-umami-source": "src/app/api/reports/utm/contract.generated.ts" } }, @@ -12974,7 +14335,7 @@ }, "/api/send": { "post": { - "operationId": "postSend", + "operationId": "send", "requestBody": { "content": { "application/json": { @@ -13186,7 +14547,7 @@ }, "/api/share": { "post": { - "operationId": "postShare", + "operationId": "createShare", "requestBody": { "content": { "application/json": { @@ -13380,7 +14741,7 @@ }, "/api/share/{slug}": { "get": { - "operationId": "getShareSlug", + "operationId": "getShareBySlug", "parameters": [ { "in": "path", @@ -13434,7 +14795,7 @@ }, "/api/share/id/{shareId}": { "delete": { - "operationId": "deleteShareIdShareId", + "operationId": "deleteShare", "parameters": [ { "in": "path", @@ -13496,7 +14857,7 @@ "x-umami-source": "src/app/api/share/id/[shareId]/contract.generated.ts" }, "get": { - "operationId": "getShareIdShareId", + "operationId": "getShare", "parameters": [ { "in": "path", @@ -13648,7 +15009,7 @@ "x-umami-source": "src/app/api/share/id/[shareId]/contract.generated.ts" }, "post": { - "operationId": "postShareIdShareId", + "operationId": "updateShare", "parameters": [ { "in": "path", @@ -14045,7 +15406,7 @@ "x-umami-source": "src/app/api/teams/contract.generated.ts" }, "post": { - "operationId": "postTeams", + "operationId": "createTeam", "requestBody": { "content": { "application/json": { @@ -14210,7 +15571,7 @@ }, "/api/teams/{teamId}": { "delete": { - "operationId": "deleteTeamsTeamId", + "operationId": "deleteTeam", "parameters": [ { "in": "path", @@ -14272,7 +15633,7 @@ "x-umami-source": "src/app/api/teams/[teamId]/contract.generated.ts" }, "get": { - "operationId": "getTeamsTeamId", + "operationId": "getTeam", "parameters": [ { "in": "path", @@ -14386,7 +15747,7 @@ "x-umami-source": "src/app/api/teams/[teamId]/contract.generated.ts" }, "post": { - "operationId": "postTeamsTeamId", + "operationId": "updateTeam", "parameters": [ { "in": "path", @@ -14518,7 +15879,7 @@ }, "/api/teams/{teamId}/boards": { "get": { - "operationId": "getTeamsTeamIdBoards", + "operationId": "getTeamBoards", "parameters": [ { "in": "path", @@ -14783,7 +16144,7 @@ }, "/api/teams/{teamId}/links": { "get": { - "operationId": "getTeamsTeamIdLinks", + "operationId": "getTeamLinks", "parameters": [ { "in": "path", @@ -14993,7 +16354,7 @@ }, "/api/teams/{teamId}/pixels": { "get": { - "operationId": "getTeamsTeamIdPixels", + "operationId": "getTeamPixels", "parameters": [ { "in": "path", @@ -15199,7 +16560,7 @@ }, "/api/teams/{teamId}/users": { "get": { - "operationId": "getTeamsTeamIdUsers", + "operationId": "getTeamUsers", "parameters": [ { "in": "path", @@ -15398,7 +16759,7 @@ "x-umami-source": "src/app/api/teams/[teamId]/users/contract.generated.ts" }, "post": { - "operationId": "postTeamsTeamIdUsers", + "operationId": "createTeamUser", "parameters": [ { "in": "path", @@ -15529,7 +16890,7 @@ }, "/api/teams/{teamId}/users/{userId}": { "delete": { - "operationId": "deleteTeamsTeamIdUsersUserId", + "operationId": "deleteTeamUser", "parameters": [ { "in": "path", @@ -15616,7 +16977,7 @@ "x-umami-source": "src/app/api/teams/[teamId]/users/[userId]/contract.generated.ts" }, "get": { - "operationId": "getTeamsTeamIdUsersUserId", + "operationId": "getTeamUser", "parameters": [ { "in": "path", @@ -15708,7 +17069,7 @@ "x-umami-source": "src/app/api/teams/[teamId]/users/[userId]/contract.generated.ts" }, "post": { - "operationId": "postTeamsTeamIdUsersUserId", + "operationId": "updateTeamUser", "parameters": [ { "in": "path", @@ -15842,7 +17203,7 @@ }, "/api/teams/{teamId}/websites": { "get": { - "operationId": "getTeamsTeamIdWebsites", + "operationId": "getTeamWebsites", "parameters": [ { "in": "path", @@ -16192,7 +17553,7 @@ }, "/api/teams/join": { "post": { - "operationId": "postTeamsJoin", + "operationId": "joinTeam", "requestBody": { "content": { "application/json": { @@ -16321,7 +17682,7 @@ }, "/api/users": { "post": { - "operationId": "postUsers", + "operationId": "createUser", "requestBody": { "content": { "application/json": { @@ -16438,7 +17799,7 @@ }, "/api/users/{userId}": { "delete": { - "operationId": "deleteUsersUserId", + "operationId": "deleteUser", "parameters": [ { "in": "path", @@ -16517,7 +17878,7 @@ "x-umami-source": "src/app/api/users/[userId]/contract.generated.ts" }, "get": { - "operationId": "getUsersUserId", + "operationId": "getUser", "parameters": [ { "in": "path", @@ -16548,6 +17909,12 @@ "links": { "type": "number" }, + "oauthAuthorizationCodes": { + "type": "number" + }, + "oauthRefreshTokens": { + "type": "number" + }, "pixels": { "type": "number" }, @@ -16585,7 +17952,9 @@ "twoFactorBackupCodes", "twoFactorOtpUseds", "twoFactorRateLimit", - "apiKeys" + "apiKeys", + "oauthAuthorizationCodes", + "oauthRefreshTokens" ], "type": "object" }, @@ -16907,6 +18276,121 @@ }, "type": "array" }, + "oauthAuthorizationCodes": { + "items": { + "properties": { + "clientId": { + "type": "string" + }, + "codeChallenge": { + "type": "string" + }, + "codeChallengeMethod": { + "type": "string" + }, + "codeHash": { + "type": "string" + }, + "createdAt": { + "format": "date-time", + "type": "string" + }, + "expiresAt": { + "format": "date-time", + "type": "string" + }, + "id": { + "type": "string" + }, + "redirectUri": { + "type": "string" + }, + "resource": { + "type": "string" + }, + "scope": { + "type": "string" + }, + "usedAt": { + "format": "date-time", + "type": "string" + }, + "userId": { + "type": "string" + } + }, + "required": [ + "id", + "userId", + "createdAt", + "codeHash", + "expiresAt", + "clientId", + "redirectUri", + "scope", + "resource", + "codeChallenge", + "codeChallengeMethod", + "usedAt" + ], + "type": "object" + }, + "type": "array" + }, + "oauthRefreshTokens": { + "items": { + "properties": { + "clientId": { + "type": "string" + }, + "createdAt": { + "format": "date-time", + "type": "string" + }, + "expiresAt": { + "format": "date-time", + "type": "string" + }, + "id": { + "type": "string" + }, + "lastUsedAt": { + "format": "date-time", + "type": "string" + }, + "resource": { + "type": "string" + }, + "revokedAt": { + "format": "date-time", + "type": "string" + }, + "scope": { + "type": "string" + }, + "tokenHash": { + "type": "string" + }, + "userId": { + "type": "string" + } + }, + "required": [ + "id", + "userId", + "createdAt", + "expiresAt", + "lastUsedAt", + "clientId", + "scope", + "resource", + "tokenHash", + "revokedAt" + ], + "type": "object" + }, + "type": "array" + }, "password": { "type": "string" }, @@ -17362,6 +18846,8 @@ "twoFactorBackupCodes", "twoFactorOtpUseds", "apiKeys", + "oauthAuthorizationCodes", + "oauthRefreshTokens", "_count" ], "type": "object" @@ -17406,7 +18892,7 @@ "x-umami-source": "src/app/api/users/[userId]/contract.generated.ts" }, "post": { - "operationId": "postUsersUserId", + "operationId": "updateUser", "parameters": [ { "in": "path", @@ -17546,7 +19032,7 @@ }, "/api/users/{userId}/teams": { "get": { - "operationId": "getUsersUserIdTeams", + "operationId": "getUserTeams", "parameters": [ { "in": "path", @@ -17740,7 +19226,7 @@ }, "/api/users/{userId}/websites": { "get": { - "operationId": "getUsersUserIdWebsites", + "operationId": "getUserWebsites", "parameters": [ { "in": "path", @@ -18208,6 +19694,11 @@ "security": [ { "bearerAuth": [] + }, + { + "oauth2": [ + "websites:read" + ] } ], "summary": "List websites", @@ -18216,6 +19707,7 @@ ], "x-umami-audience": "public", "x-umami-contract": "explicit", + "x-umami-oauth-scope": "websites:read", "x-umami-source": "src/app/api/websites/contract.ts" }, "post": { @@ -18405,6 +19897,11 @@ { "bearerAuth": [] }, + { + "oauth2": [ + "websites:read" + ] + }, { "shareContext": [], "shareToken": [] @@ -18416,6 +19913,7 @@ ], "x-umami-audience": "public", "x-umami-contract": "explicit", + "x-umami-oauth-scope": "websites:read", "x-umami-source": "src/app/api/websites/[websiteId]/contract.ts" }, "post": { @@ -18523,13 +20021,18 @@ }, "/api/websites/{websiteId}/active": { "get": { - "operationId": "getWebsitesWebsiteIdActive", + "description": "Returns the number of visitors active on the website in the last few minutes.", + "operationId": "getWebsiteActive", "parameters": [ { + "description": "Website ID.", "in": "path", "name": "websiteId", "required": true, "schema": { + "description": "Website ID.", + "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$", "type": "string" } } @@ -18539,19 +20042,11 @@ "content": { "application/json": { "schema": { - "properties": { - "visitors": { - "type": "number" - } - }, - "required": [ - "visitors" - ], - "type": "object" + "$ref": "#/components/schemas/ActiveVisitors" } } }, - "description": "The operation completed successfully." + "description": "Active visitor count." }, "401": { "content": { @@ -18575,23 +20070,29 @@ { "bearerAuth": [] }, + { + "oauth2": [ + "analytics:read" + ] + }, { "shareContext": [], "shareToken": [] } ], - "summary": "Get websites website id active", + "summary": "Get current active visitors", "tags": [ "Websites" ], "x-umami-audience": "public", "x-umami-contract": "explicit", - "x-umami-source": "src/app/api/websites/[websiteId]/active/contract.generated.ts" + "x-umami-oauth-scope": "analytics:read", + "x-umami-source": "src/app/api/websites/[websiteId]/active/contract.ts" } }, "/api/websites/{websiteId}/annotations": { "get": { - "operationId": "getWebsitesWebsiteIdAnnotations", + "operationId": "getWebsiteAnnotations", "parameters": [ { "in": "path", @@ -18791,7 +20292,7 @@ "x-umami-source": "src/app/api/websites/[websiteId]/annotations/contract.generated.ts" }, "post": { - "operationId": "postWebsitesWebsiteIdAnnotations", + "operationId": "createWebsiteAnnotation", "parameters": [ { "in": "path", @@ -18931,7 +20432,7 @@ }, "/api/websites/{websiteId}/annotations/{annotationId}": { "delete": { - "operationId": "deleteWebsitesWebsiteIdAnnotationsAnnotationId", + "operationId": "deleteWebsiteAnnotation", "parameters": [ { "in": "path", @@ -19018,7 +20519,7 @@ "x-umami-source": "src/app/api/websites/[websiteId]/annotations/[annotationId]/contract.generated.ts" }, "get": { - "operationId": "getWebsitesWebsiteIdAnnotationsAnnotationId", + "operationId": "getWebsiteAnnotation", "parameters": [ { "in": "path", @@ -19140,7 +20641,7 @@ "x-umami-source": "src/app/api/websites/[websiteId]/annotations/[annotationId]/contract.generated.ts" }, "post": { - "operationId": "postWebsitesWebsiteIdAnnotationsAnnotationId", + "operationId": "updateWebsiteAnnotation", "parameters": [ { "in": "path", @@ -19305,7 +20806,7 @@ }, "/api/websites/{websiteId}/daterange": { "get": { - "operationId": "getWebsitesWebsiteIdDaterange", + "operationId": "getWebsiteDateRange", "parameters": [ { "in": "path", @@ -19363,6 +20864,11 @@ { "bearerAuth": [] }, + { + "oauth2": [ + "analytics:read" + ] + }, { "shareContext": [], "shareToken": [] @@ -19374,12 +20880,13 @@ ], "x-umami-audience": "public", "x-umami-contract": "explicit", + "x-umami-oauth-scope": "analytics:read", "x-umami-source": "src/app/api/websites/[websiteId]/daterange/contract.generated.ts" } }, "/api/websites/{websiteId}/event-data": { "get": { - "operationId": "getWebsitesWebsiteIdEventData", + "operationId": "getEventData", "parameters": [ { "in": "path", @@ -19780,6 +21287,11 @@ { "bearerAuth": [] }, + { + "oauth2": [ + "analytics:read" + ] + }, { "shareContext": [], "shareToken": [] @@ -19791,12 +21303,13 @@ ], "x-umami-audience": "public", "x-umami-contract": "explicit", + "x-umami-oauth-scope": "analytics:read", "x-umami-source": "src/app/api/websites/[websiteId]/event-data/contract.generated.ts" } }, "/api/websites/{websiteId}/event-data-pivot": { "get": { - "operationId": "getWebsitesWebsiteIdEventDataPivot", + "operationId": "getEventDataPivot", "parameters": [ { "in": "path", @@ -20228,7 +21741,7 @@ }, "/api/websites/{websiteId}/event-data-pivot/array-series": { "get": { - "operationId": "getWebsitesWebsiteIdEventDataPivotArraySeries", + "operationId": "getEventDataArraySeries", "parameters": [ { "in": "path", @@ -20579,7 +22092,7 @@ }, "/api/websites/{websiteId}/event-data-pivot/date-series": { "get": { - "operationId": "getWebsitesWebsiteIdEventDataPivotDateSeries", + "operationId": "getEventDataDateSeries", "parameters": [ { "in": "path", @@ -20918,7 +22431,7 @@ }, "/api/websites/{websiteId}/event-data-pivot/numeric-series": { "get": { - "operationId": "getWebsitesWebsiteIdEventDataPivotNumericSeries", + "operationId": "getEventDataNumericSeries", "parameters": [ { "in": "path", @@ -21278,7 +22791,7 @@ }, "/api/websites/{websiteId}/event-data-pivot/numeric-stats": { "get": { - "operationId": "getWebsitesWebsiteIdEventDataPivotNumericStats", + "operationId": "getEventDataNumericStats", "parameters": [ { "in": "path", @@ -21618,7 +23131,7 @@ }, "/api/websites/{websiteId}/event-data-pivot/property-series": { "get": { - "operationId": "getWebsitesWebsiteIdEventDataPivotPropertySeries", + "operationId": "getEventDataPropertySeries", "parameters": [ { "in": "path", @@ -21969,7 +23482,7 @@ }, "/api/websites/{websiteId}/event-data/{eventId}": { "get": { - "operationId": "getWebsitesWebsiteIdEventDataEventId", + "operationId": "getEventDataById", "parameters": [ { "in": "path", @@ -22101,7 +23614,7 @@ }, "/api/websites/{websiteId}/event-data/events": { "get": { - "operationId": "getWebsitesWebsiteIdEventDataEvents", + "operationId": "getEventDataEvents", "parameters": [ { "in": "path", @@ -22426,7 +23939,7 @@ }, "/api/websites/{websiteId}/event-data/fields": { "get": { - "operationId": "getWebsitesWebsiteIdEventDataFields", + "operationId": "getEventDataFields", "parameters": [ { "in": "path", @@ -22753,7 +24266,7 @@ }, "/api/websites/{websiteId}/event-data/properties": { "get": { - "operationId": "getWebsitesWebsiteIdEventDataProperties", + "operationId": "getEventDataProperties", "parameters": [ { "in": "path", @@ -23060,6 +24573,11 @@ { "bearerAuth": [] }, + { + "oauth2": [ + "analytics:read" + ] + }, { "shareContext": [], "shareToken": [] @@ -23071,12 +24589,13 @@ ], "x-umami-audience": "public", "x-umami-contract": "explicit", + "x-umami-oauth-scope": "analytics:read", "x-umami-source": "src/app/api/websites/[websiteId]/event-data/properties/contract.generated.ts" } }, "/api/websites/{websiteId}/event-data/stats": { "get": { - "operationId": "getWebsitesWebsiteIdEventDataStats", + "operationId": "getEventDataStats", "parameters": [ { "in": "path", @@ -23376,6 +24895,11 @@ { "bearerAuth": [] }, + { + "oauth2": [ + "analytics:read" + ] + }, { "shareContext": [], "shareToken": [] @@ -23387,12 +24911,13 @@ ], "x-umami-audience": "public", "x-umami-contract": "explicit", + "x-umami-oauth-scope": "analytics:read", "x-umami-source": "src/app/api/websites/[websiteId]/event-data/stats/contract.generated.ts" } }, "/api/websites/{websiteId}/event-data/values": { "get": { - "operationId": "getWebsitesWebsiteIdEventDataValues", + "operationId": "getEventDataValues", "parameters": [ { "in": "path", @@ -23715,6 +25240,11 @@ { "bearerAuth": [] }, + { + "oauth2": [ + "analytics:read" + ] + }, { "shareContext": [], "shareToken": [] @@ -23726,25 +25256,30 @@ ], "x-umami-audience": "public", "x-umami-contract": "explicit", + "x-umami-oauth-scope": "analytics:read", "x-umami-source": "src/app/api/websites/[websiteId]/event-data/values/contract.generated.ts" } }, "/api/websites/{websiteId}/events": { "get": { - "operationId": "getWebsitesWebsiteIdEvents", + "description": "Returns a page of pageviews and custom events in the date range, newest first. Filter with `event` for a specific event name or `search` for free text.", + "operationId": "getWebsiteEvents", "parameters": [ { + "description": "Website ID.", "in": "path", "name": "websiteId", "required": true, "schema": { + "description": "Website ID.", + "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$", "type": "string" } }, { "in": "query", "name": "startAt", - "required": false, "schema": { "type": "number" } @@ -23752,7 +25287,6 @@ { "in": "query", "name": "endAt", - "required": false, "schema": { "type": "number" } @@ -23760,25 +25294,20 @@ { "in": "query", "name": "startDate", - "required": false, "schema": { - "format": "date-time", "type": "string" } }, { "in": "query", "name": "endDate", - "required": false, "schema": { - "format": "date-time", "type": "string" } }, { "in": "query", "name": "timezone", - "required": false, "schema": { "type": "string" } @@ -23786,7 +25315,6 @@ { "in": "query", "name": "unit", - "required": false, "schema": { "type": "string" } @@ -23794,7 +25322,6 @@ { "in": "query", "name": "compare", - "required": false, "schema": { "enum": [ "prev", @@ -23806,7 +25333,6 @@ { "in": "query", "name": "path", - "required": false, "schema": { "type": "string" } @@ -23814,7 +25340,6 @@ { "in": "query", "name": "referrer", - "required": false, "schema": { "type": "string" } @@ -23822,7 +25347,6 @@ { "in": "query", "name": "title", - "required": false, "schema": { "type": "string" } @@ -23830,7 +25354,6 @@ { "in": "query", "name": "query", - "required": false, "schema": { "type": "string" } @@ -23838,7 +25361,6 @@ { "in": "query", "name": "os", - "required": false, "schema": { "type": "string" } @@ -23846,7 +25368,6 @@ { "in": "query", "name": "browser", - "required": false, "schema": { "type": "string" } @@ -23854,7 +25375,6 @@ { "in": "query", "name": "device", - "required": false, "schema": { "type": "string" } @@ -23862,7 +25382,6 @@ { "in": "query", "name": "country", - "required": false, "schema": { "type": "string" } @@ -23870,7 +25389,6 @@ { "in": "query", "name": "region", - "required": false, "schema": { "type": "string" } @@ -23878,7 +25396,6 @@ { "in": "query", "name": "city", - "required": false, "schema": { "type": "string" } @@ -23886,7 +25403,6 @@ { "in": "query", "name": "tag", - "required": false, "schema": { "type": "string" } @@ -23894,7 +25410,6 @@ { "in": "query", "name": "hostname", - "required": false, "schema": { "type": "string" } @@ -23902,7 +25417,6 @@ { "in": "query", "name": "distinctId", - "required": false, "schema": { "type": "string" } @@ -23910,7 +25424,6 @@ { "in": "query", "name": "language", - "required": false, "schema": { "type": "string" } @@ -23918,7 +25431,6 @@ { "in": "query", "name": "event", - "required": false, "schema": { "type": "string" } @@ -23926,7 +25438,6 @@ { "in": "query", "name": "utmSource", - "required": false, "schema": { "type": "string" } @@ -23934,7 +25445,6 @@ { "in": "query", "name": "utmMedium", - "required": false, "schema": { "type": "string" } @@ -23942,7 +25452,6 @@ { "in": "query", "name": "utmCampaign", - "required": false, "schema": { "type": "string" } @@ -23950,7 +25459,6 @@ { "in": "query", "name": "utmContent", - "required": false, "schema": { "type": "string" } @@ -23958,15 +25466,6 @@ { "in": "query", "name": "utmTerm", - "required": false, - "schema": { - "type": "string" - } - }, - { - "in": "query", - "name": "excludeBounce", - "required": false, "schema": { "type": "string" } @@ -23974,34 +25473,40 @@ { "in": "query", "name": "segment", - "required": false, "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$", "type": "string" } }, { "in": "query", "name": "cohort", - "required": false, "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$", "type": "string" } }, { "in": "query", "name": "eventType", - "required": false, "schema": { - "minimum": 1, + "exclusiveMinimum": 0, + "maximum": 9007199254740991, "type": "integer" } }, + { + "in": "query", + "name": "excludeBounce", + "schema": { + "type": "string" + } + }, { "in": "query", "name": "match", - "required": false, "schema": { "enum": [ "all", @@ -24013,34 +25518,33 @@ { "in": "query", "name": "page", - "required": false, "schema": { - "minimum": 1, + "exclusiveMinimum": 0, + "maximum": 9007199254740991, "type": "integer" } }, { "in": "query", "name": "pageSize", - "required": false, "schema": { - "minimum": 1, + "exclusiveMinimum": 0, + "maximum": 9007199254740991, "type": "integer" } }, { "in": "query", "name": "maxResults", - "required": false, "schema": { - "minimum": 1, + "exclusiveMinimum": 0, + "maximum": 9007199254740991, "type": "integer" } }, { "in": "query", "name": "search", - "required": false, "schema": { "type": "string" } @@ -24051,134 +25555,11 @@ "content": { "application/json": { "schema": { - "properties": { - "count": { - "type": "number" - }, - "data": { - "items": { - "properties": { - "browser": { - "type": "string" - }, - "city": { - "type": "string" - }, - "country": { - "type": "string" - }, - "createdAt": { - "anyOf": [ - { - "type": "string" - }, - { - "format": "date-time", - "type": "string" - } - ] - }, - "device": { - "type": "string" - }, - "eventName": { - "type": "string" - }, - "eventType": { - "type": "number" - }, - "hasData": { - "type": "boolean" - }, - "hostname": { - "type": "string" - }, - "id": { - "type": "string" - }, - "os": { - "type": "string" - }, - "pageTitle": { - "type": "string" - }, - "referrerDomain": { - "type": "string" - }, - "referrerPath": { - "type": "string" - }, - "referrerQuery": { - "type": "string" - }, - "sessionId": { - "type": "string" - }, - "urlPath": { - "type": "string" - }, - "urlQuery": { - "type": "string" - }, - "websiteId": { - "type": "string" - } - }, - "required": [ - "id", - "websiteId", - "sessionId", - "createdAt", - "hostname", - "urlPath", - "urlQuery", - "referrerPath", - "referrerQuery", - "referrerDomain", - "country", - "city", - "device", - "os", - "browser", - "pageTitle", - "eventType", - "eventName", - "hasData" - ], - "type": "object" - }, - "type": "array" - }, - "isCapped": { - "type": "boolean" - }, - "orderBy": { - "type": "string" - }, - "page": { - "type": "number" - }, - "pageSize": { - "type": "number" - }, - "search": { - "type": "string" - }, - "sortDescending": { - "type": "boolean" - } - }, - "required": [ - "data", - "count", - "page", - "pageSize" - ], - "type": "object" + "$ref": "#/components/schemas/WebsiteEventPage" } } }, - "description": "The operation completed successfully." + "description": "A page of events." }, "400": { "content": { @@ -24219,23 +25600,29 @@ { "bearerAuth": [] }, + { + "oauth2": [ + "analytics:read" + ] + }, { "shareContext": [], "shareToken": [] } ], - "summary": "Get websites website id events", + "summary": "List tracked events", "tags": [ "Websites" ], "x-umami-audience": "public", "x-umami-contract": "explicit", - "x-umami-source": "src/app/api/websites/[websiteId]/events/contract.generated.ts" + "x-umami-oauth-scope": "analytics:read", + "x-umami-source": "src/app/api/websites/[websiteId]/events/contract.ts" } }, "/api/websites/{websiteId}/events/series": { "get": { - "operationId": "getWebsitesWebsiteIdEventsSeries", + "operationId": "getWebsiteEventSeries", "parameters": [ { "in": "path", @@ -24562,6 +25949,11 @@ { "bearerAuth": [] }, + { + "oauth2": [ + "analytics:read" + ] + }, { "shareContext": [], "shareToken": [] @@ -24573,12 +25965,13 @@ ], "x-umami-audience": "public", "x-umami-contract": "explicit", + "x-umami-oauth-scope": "analytics:read", "x-umami-source": "src/app/api/websites/[websiteId]/events/series/contract.generated.ts" } }, "/api/websites/{websiteId}/events/stats": { "get": { - "operationId": "getWebsitesWebsiteIdEventsStats", + "operationId": "getWebsiteEventStats", "parameters": [ { "in": "path", @@ -24951,6 +26344,11 @@ { "bearerAuth": [] }, + { + "oauth2": [ + "analytics:read" + ] + }, { "shareContext": [], "shareToken": [] @@ -24962,12 +26360,13 @@ ], "x-umami-audience": "public", "x-umami-contract": "explicit", + "x-umami-oauth-scope": "analytics:read", "x-umami-source": "src/app/api/websites/[websiteId]/events/stats/contract.generated.ts" } }, "/api/websites/{websiteId}/export": { "get": { - "operationId": "getWebsitesWebsiteIdExport", + "operationId": "exportWebsite", "parameters": [ { "in": "path", @@ -25149,20 +26548,24 @@ }, "/api/websites/{websiteId}/metrics": { "get": { - "operationId": "getWebsitesWebsiteIdMetrics", + "description": "Returns the top values for one dimension (`type`), such as pages, referrers, countries, browsers, UTM parameters or events, ordered by count. Page-type dimensions count views/events; visitor dimensions count unique visitors.", + "operationId": "getWebsiteMetrics", "parameters": [ { + "description": "Website ID.", "in": "path", "name": "websiteId", "required": true, "schema": { + "description": "Website ID.", + "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$", "type": "string" } }, { "in": "query", "name": "startAt", - "required": false, "schema": { "type": "number" } @@ -25170,7 +26573,6 @@ { "in": "query", "name": "endAt", - "required": false, "schema": { "type": "number" } @@ -25178,25 +26580,20 @@ { "in": "query", "name": "startDate", - "required": false, "schema": { - "format": "date-time", "type": "string" } }, { "in": "query", "name": "endDate", - "required": false, "schema": { - "format": "date-time", "type": "string" } }, { "in": "query", "name": "timezone", - "required": false, "schema": { "type": "string" } @@ -25204,7 +26601,6 @@ { "in": "query", "name": "unit", - "required": false, "schema": { "type": "string" } @@ -25212,7 +26608,6 @@ { "in": "query", "name": "compare", - "required": false, "schema": { "enum": [ "prev", @@ -25222,17 +26617,18 @@ } }, { + "description": "Dimension to rank: path, entry, exit, title, query, hostname, referrer, domain, channel, event, tag, browser, os, device, screen, language, country, region, city, distinctId, utmSource, utmMedium, utmCampaign, utmContent, utmTerm.", "in": "query", "name": "type", "required": true, "schema": { + "description": "Dimension to rank: path, entry, exit, title, query, hostname, referrer, domain, channel, event, tag, browser, os, device, screen, language, country, region, city, distinctId, utmSource, utmMedium, utmCampaign, utmContent, utmTerm.", "type": "string" } }, { "in": "query", "name": "limit", - "required": false, "schema": { "type": "number" } @@ -25240,7 +26636,6 @@ { "in": "query", "name": "offset", - "required": false, "schema": { "type": "number" } @@ -25248,7 +26643,6 @@ { "in": "query", "name": "search", - "required": false, "schema": { "type": "string" } @@ -25256,7 +26650,6 @@ { "in": "query", "name": "path", - "required": false, "schema": { "type": "string" } @@ -25264,7 +26657,6 @@ { "in": "query", "name": "referrer", - "required": false, "schema": { "type": "string" } @@ -25272,7 +26664,6 @@ { "in": "query", "name": "title", - "required": false, "schema": { "type": "string" } @@ -25280,7 +26671,6 @@ { "in": "query", "name": "query", - "required": false, "schema": { "type": "string" } @@ -25288,7 +26678,6 @@ { "in": "query", "name": "os", - "required": false, "schema": { "type": "string" } @@ -25296,7 +26685,6 @@ { "in": "query", "name": "browser", - "required": false, "schema": { "type": "string" } @@ -25304,7 +26692,6 @@ { "in": "query", "name": "device", - "required": false, "schema": { "type": "string" } @@ -25312,7 +26699,6 @@ { "in": "query", "name": "country", - "required": false, "schema": { "type": "string" } @@ -25320,7 +26706,6 @@ { "in": "query", "name": "region", - "required": false, "schema": { "type": "string" } @@ -25328,7 +26713,6 @@ { "in": "query", "name": "city", - "required": false, "schema": { "type": "string" } @@ -25336,7 +26720,6 @@ { "in": "query", "name": "tag", - "required": false, "schema": { "type": "string" } @@ -25344,7 +26727,6 @@ { "in": "query", "name": "hostname", - "required": false, "schema": { "type": "string" } @@ -25352,7 +26734,6 @@ { "in": "query", "name": "distinctId", - "required": false, "schema": { "type": "string" } @@ -25360,7 +26741,6 @@ { "in": "query", "name": "language", - "required": false, "schema": { "type": "string" } @@ -25368,7 +26748,6 @@ { "in": "query", "name": "event", - "required": false, "schema": { "type": "string" } @@ -25376,7 +26755,6 @@ { "in": "query", "name": "utmSource", - "required": false, "schema": { "type": "string" } @@ -25384,7 +26762,6 @@ { "in": "query", "name": "utmMedium", - "required": false, "schema": { "type": "string" } @@ -25392,7 +26769,6 @@ { "in": "query", "name": "utmCampaign", - "required": false, "schema": { "type": "string" } @@ -25400,7 +26776,6 @@ { "in": "query", "name": "utmContent", - "required": false, "schema": { "type": "string" } @@ -25408,15 +26783,6 @@ { "in": "query", "name": "utmTerm", - "required": false, - "schema": { - "type": "string" - } - }, - { - "in": "query", - "name": "excludeBounce", - "required": false, "schema": { "type": "string" } @@ -25424,34 +26790,40 @@ { "in": "query", "name": "segment", - "required": false, "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$", "type": "string" } }, { "in": "query", "name": "cohort", - "required": false, "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$", "type": "string" } }, { "in": "query", "name": "eventType", - "required": false, "schema": { - "minimum": 1, + "exclusiveMinimum": 0, + "maximum": 9007199254740991, "type": "integer" } }, + { + "in": "query", + "name": "excludeBounce", + "schema": { + "type": "string" + } + }, { "in": "query", "name": "match", - "required": false, "schema": { "enum": [ "all", @@ -25466,30 +26838,11 @@ "content": { "application/json": { "schema": { - "items": { - "properties": { - "t": { - "type": "string" - }, - "x": { - "type": "string" - }, - "y": { - "type": "number" - } - }, - "required": [ - "x", - "t", - "y" - ], - "type": "object" - }, - "type": "array" + "$ref": "#/components/schemas/MetricRows" } } }, - "description": "The operation completed successfully." + "description": "Ranked rows." }, "400": { "content": { @@ -25530,23 +26883,29 @@ { "bearerAuth": [] }, + { + "oauth2": [ + "analytics:read" + ] + }, { "shareContext": [], "shareToken": [] } ], - "summary": "Get websites website id metrics", + "summary": "Get ranked metrics for a dimension", "tags": [ "Websites" ], "x-umami-audience": "public", "x-umami-contract": "explicit", - "x-umami-source": "src/app/api/websites/[websiteId]/metrics/contract.generated.ts" + "x-umami-oauth-scope": "analytics:read", + "x-umami-source": "src/app/api/websites/[websiteId]/metrics/contract.ts" } }, "/api/websites/{websiteId}/metrics/expanded": { "get": { - "operationId": "getWebsitesWebsiteIdMetricsExpanded", + "operationId": "getWebsiteExpandedMetrics", "parameters": [ { "in": "path", @@ -25939,6 +27298,11 @@ { "bearerAuth": [] }, + { + "oauth2": [ + "analytics:read" + ] + }, { "shareContext": [], "shareToken": [] @@ -25950,25 +27314,30 @@ ], "x-umami-audience": "public", "x-umami-contract": "explicit", + "x-umami-oauth-scope": "analytics:read", "x-umami-source": "src/app/api/websites/[websiteId]/metrics/expanded/contract.generated.ts" } }, "/api/websites/{websiteId}/pageviews": { "get": { - "operationId": "getWebsitesWebsiteIdPageviews", + "description": "Returns pageviews and sessions bucketed by `unit` (minute, hour, day, month, year) in the given `timezone`. When `compare` is set the comparison period is included.", + "operationId": "getWebsitePageviews", "parameters": [ { + "description": "Website ID.", "in": "path", "name": "websiteId", "required": true, "schema": { + "description": "Website ID.", + "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$", "type": "string" } }, { "in": "query", "name": "startAt", - "required": false, "schema": { "type": "number" } @@ -25976,7 +27345,6 @@ { "in": "query", "name": "endAt", - "required": false, "schema": { "type": "number" } @@ -25984,25 +27352,20 @@ { "in": "query", "name": "startDate", - "required": false, "schema": { - "format": "date-time", "type": "string" } }, { "in": "query", "name": "endDate", - "required": false, "schema": { - "format": "date-time", "type": "string" } }, { "in": "query", "name": "timezone", - "required": false, "schema": { "type": "string" } @@ -26010,7 +27373,6 @@ { "in": "query", "name": "unit", - "required": false, "schema": { "type": "string" } @@ -26018,7 +27380,6 @@ { "in": "query", "name": "compare", - "required": false, "schema": { "enum": [ "prev", @@ -26030,7 +27391,6 @@ { "in": "query", "name": "path", - "required": false, "schema": { "type": "string" } @@ -26038,7 +27398,6 @@ { "in": "query", "name": "referrer", - "required": false, "schema": { "type": "string" } @@ -26046,7 +27405,6 @@ { "in": "query", "name": "title", - "required": false, "schema": { "type": "string" } @@ -26054,7 +27412,6 @@ { "in": "query", "name": "query", - "required": false, "schema": { "type": "string" } @@ -26062,7 +27419,6 @@ { "in": "query", "name": "os", - "required": false, "schema": { "type": "string" } @@ -26070,7 +27426,6 @@ { "in": "query", "name": "browser", - "required": false, "schema": { "type": "string" } @@ -26078,7 +27433,6 @@ { "in": "query", "name": "device", - "required": false, "schema": { "type": "string" } @@ -26086,7 +27440,6 @@ { "in": "query", "name": "country", - "required": false, "schema": { "type": "string" } @@ -26094,7 +27447,6 @@ { "in": "query", "name": "region", - "required": false, "schema": { "type": "string" } @@ -26102,7 +27454,6 @@ { "in": "query", "name": "city", - "required": false, "schema": { "type": "string" } @@ -26110,7 +27461,6 @@ { "in": "query", "name": "tag", - "required": false, "schema": { "type": "string" } @@ -26118,7 +27468,6 @@ { "in": "query", "name": "hostname", - "required": false, "schema": { "type": "string" } @@ -26126,7 +27475,6 @@ { "in": "query", "name": "distinctId", - "required": false, "schema": { "type": "string" } @@ -26134,7 +27482,6 @@ { "in": "query", "name": "language", - "required": false, "schema": { "type": "string" } @@ -26142,7 +27489,6 @@ { "in": "query", "name": "event", - "required": false, "schema": { "type": "string" } @@ -26150,7 +27496,6 @@ { "in": "query", "name": "utmSource", - "required": false, "schema": { "type": "string" } @@ -26158,7 +27503,6 @@ { "in": "query", "name": "utmMedium", - "required": false, "schema": { "type": "string" } @@ -26166,7 +27510,6 @@ { "in": "query", "name": "utmCampaign", - "required": false, "schema": { "type": "string" } @@ -26174,7 +27517,6 @@ { "in": "query", "name": "utmContent", - "required": false, "schema": { "type": "string" } @@ -26182,15 +27524,6 @@ { "in": "query", "name": "utmTerm", - "required": false, - "schema": { - "type": "string" - } - }, - { - "in": "query", - "name": "excludeBounce", - "required": false, "schema": { "type": "string" } @@ -26198,34 +27531,40 @@ { "in": "query", "name": "segment", - "required": false, "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$", "type": "string" } }, { "in": "query", "name": "cohort", - "required": false, "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$", "type": "string" } }, { "in": "query", "name": "eventType", - "required": false, "schema": { - "minimum": 1, + "exclusiveMinimum": 0, + "maximum": 9007199254740991, "type": "integer" } }, + { + "in": "query", + "name": "excludeBounce", + "schema": { + "type": "string" + } + }, { "in": "query", "name": "match", - "required": false, "schema": { "enum": [ "all", @@ -26240,66 +27579,11 @@ "content": { "application/json": { "schema": { - "anyOf": [ - { - "properties": { - "compare": { - "properties": { - "endDate": { - "format": "date-time", - "type": "string" - }, - "pageviews": {}, - "sessions": {}, - "startDate": { - "format": "date-time", - "type": "string" - } - }, - "required": [ - "pageviews", - "sessions", - "startDate", - "endDate" - ], - "type": "object" - }, - "endDate": { - "format": "date-time", - "type": "string" - }, - "pageviews": {}, - "sessions": {}, - "startDate": { - "format": "date-time", - "type": "string" - } - }, - "required": [ - "pageviews", - "sessions", - "startDate", - "endDate", - "compare" - ], - "type": "object" - }, - { - "properties": { - "pageviews": {}, - "sessions": {} - }, - "required": [ - "pageviews", - "sessions" - ], - "type": "object" - } - ] + "$ref": "#/components/schemas/WebsitePageviews" } } }, - "description": "The operation completed successfully." + "description": "Time series." }, "400": { "content": { @@ -26340,23 +27624,29 @@ { "bearerAuth": [] }, + { + "oauth2": [ + "analytics:read" + ] + }, { "shareContext": [], "shareToken": [] } ], - "summary": "Get websites website id pageviews", + "summary": "Get pageview and session time series", "tags": [ "Websites" ], "x-umami-audience": "public", "x-umami-contract": "explicit", - "x-umami-source": "src/app/api/websites/[websiteId]/pageviews/contract.generated.ts" + "x-umami-oauth-scope": "analytics:read", + "x-umami-source": "src/app/api/websites/[websiteId]/pageviews/contract.ts" } }, "/api/websites/{websiteId}/recorder": { "get": { - "operationId": "getWebsitesWebsiteIdRecorder", + "operationId": "getWebsiteRecorderConfig", "parameters": [ { "in": "path", @@ -26449,7 +27739,7 @@ }, "/api/websites/{websiteId}/replays": { "get": { - "operationId": "getWebsitesWebsiteIdReplays", + "operationId": "getWebsiteReplays", "parameters": [ { "in": "path", @@ -26958,7 +28248,7 @@ }, "/api/websites/{websiteId}/replays/{replayId}": { "get": { - "operationId": "getWebsitesWebsiteIdReplaysReplayId", + "operationId": "getWebsiteReplay", "parameters": [ { "in": "path", @@ -27057,7 +28347,7 @@ }, "/api/websites/{websiteId}/replays/saved": { "get": { - "operationId": "getWebsitesWebsiteIdReplaysSaved", + "operationId": "getWebsiteSavedReplays", "parameters": [ { "in": "path", @@ -27234,7 +28524,7 @@ }, "/api/websites/{websiteId}/replays/saved/{replayId}": { "get": { - "operationId": "getWebsitesWebsiteIdReplaysSavedReplayId", + "operationId": "getWebsiteReplaySaved", "parameters": [ { "in": "path", @@ -27308,7 +28598,7 @@ "x-umami-source": "src/app/api/websites/[websiteId]/replays/saved/[replayId]/contract.generated.ts" }, "post": { - "operationId": "postWebsitesWebsiteIdReplaysSavedReplayId", + "operationId": "saveWebsiteReplay", "parameters": [ { "in": "path", @@ -27419,7 +28709,7 @@ }, "/api/websites/{websiteId}/reports": { "get": { - "operationId": "getWebsitesWebsiteIdReports", + "operationId": "getWebsiteReports", "parameters": [ { "in": "path", @@ -27688,7 +28978,7 @@ }, "/api/websites/{websiteId}/reset": { "post": { - "operationId": "postWebsitesWebsiteIdReset", + "operationId": "resetWebsite", "parameters": [ { "in": "path", @@ -27752,7 +29042,7 @@ }, "/api/websites/{websiteId}/revenue/chart": { "get": { - "operationId": "getWebsitesWebsiteIdRevenueChart", + "operationId": "getWebsiteRevenueChart", "parameters": [ { "in": "path", @@ -28137,7 +29427,7 @@ }, "/api/websites/{websiteId}/revenue/metrics": { "get": { - "operationId": "getWebsitesWebsiteIdRevenueMetrics", + "operationId": "getWebsiteRevenueMetrics", "parameters": [ { "in": "path", @@ -28546,7 +29836,7 @@ }, "/api/websites/{websiteId}/revenue/sessions": { "get": { - "operationId": "getWebsitesWebsiteIdRevenueSessions", + "operationId": "getWebsiteRevenueSessions", "parameters": [ { "in": "path", @@ -29066,7 +30356,7 @@ }, "/api/websites/{websiteId}/revenue/stats": { "get": { - "operationId": "getWebsitesWebsiteIdRevenueStats", + "operationId": "getWebsiteRevenueStats", "parameters": [ { "in": "path", @@ -29426,7 +30716,7 @@ }, "/api/websites/{websiteId}/segments": { "get": { - "operationId": "getWebsitesWebsiteIdSegments", + "operationId": "getWebsiteSegments", "parameters": [ { "in": "path", @@ -29646,7 +30936,7 @@ "x-umami-source": "src/app/api/websites/[websiteId]/segments/contract.generated.ts" }, "post": { - "operationId": "postWebsitesWebsiteIdSegments", + "operationId": "createWebsiteSegment", "parameters": [ { "in": "path", @@ -29874,7 +31164,7 @@ }, "/api/websites/{websiteId}/segments/{segmentId}": { "delete": { - "operationId": "deleteWebsitesWebsiteIdSegmentsSegmentId", + "operationId": "deleteWebsiteSegment", "parameters": [ { "in": "path", @@ -29961,7 +31251,7 @@ "x-umami-source": "src/app/api/websites/[websiteId]/segments/[segmentId]/contract.generated.ts" }, "get": { - "operationId": "getWebsitesWebsiteIdSegmentsSegmentId", + "operationId": "getWebsiteSegment", "parameters": [ { "in": "path", @@ -30134,7 +31424,7 @@ "x-umami-source": "src/app/api/websites/[websiteId]/segments/[segmentId]/contract.generated.ts" }, "post": { - "operationId": "postWebsitesWebsiteIdSegmentsSegmentId", + "operationId": "updateWebsiteSegment", "parameters": [ { "in": "path", @@ -30354,7 +31644,7 @@ }, "/api/websites/{websiteId}/session-data-pivot": { "get": { - "operationId": "getWebsitesWebsiteIdSessionDataPivot", + "operationId": "getSessionDataPivot", "parameters": [ { "in": "path", @@ -30778,7 +32068,7 @@ }, "/api/websites/{websiteId}/session-data/array-series": { "get": { - "operationId": "getWebsitesWebsiteIdSessionDataArraySeries", + "operationId": "getSessionDataArraySeries", "parameters": [ { "in": "path", @@ -31121,7 +32411,7 @@ }, "/api/websites/{websiteId}/session-data/date-series": { "get": { - "operationId": "getWebsitesWebsiteIdSessionDataDateSeries", + "operationId": "getSessionDataDateSeries", "parameters": [ { "in": "path", @@ -31452,7 +32742,7 @@ }, "/api/websites/{websiteId}/session-data/numeric-series": { "get": { - "operationId": "getWebsitesWebsiteIdSessionDataNumericSeries", + "operationId": "getSessionDataNumericSeries", "parameters": [ { "in": "path", @@ -31804,7 +33094,7 @@ }, "/api/websites/{websiteId}/session-data/numeric-stats": { "get": { - "operationId": "getWebsitesWebsiteIdSessionDataNumericStats", + "operationId": "getSessionDataNumericStats", "parameters": [ { "in": "path", @@ -32144,7 +33434,7 @@ }, "/api/websites/{websiteId}/session-data/properties": { "get": { - "operationId": "getWebsitesWebsiteIdSessionDataProperties", + "operationId": "getSessionDataProperties", "parameters": [ { "in": "path", @@ -32471,7 +33761,7 @@ }, "/api/websites/{websiteId}/session-data/property-series": { "get": { - "operationId": "getWebsitesWebsiteIdSessionDataPropertySeries", + "operationId": "getSessionDataPropertySeries", "parameters": [ { "in": "path", @@ -32814,7 +34104,7 @@ }, "/api/websites/{websiteId}/session-data/stats": { "get": { - "operationId": "getWebsitesWebsiteIdSessionDataStats", + "operationId": "getSessionDataStats", "parameters": [ { "in": "path", @@ -33169,7 +34459,7 @@ }, "/api/websites/{websiteId}/session-data/values": { "get": { - "operationId": "getWebsitesWebsiteIdSessionDataValues", + "operationId": "getSessionDataValues", "parameters": [ { "in": "path", @@ -33500,20 +34790,24 @@ }, "/api/websites/{websiteId}/sessions": { "get": { - "operationId": "getWebsitesWebsiteIdSessions", + "description": "Returns a page of visitor sessions in the date range, newest first. `search` matches distinct ID, city, browser, OS or device.", + "operationId": "getWebsiteSessions", "parameters": [ { + "description": "Website ID.", "in": "path", "name": "websiteId", "required": true, "schema": { + "description": "Website ID.", + "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$", "type": "string" } }, { "in": "query", "name": "startAt", - "required": false, "schema": { "type": "number" } @@ -33521,7 +34815,6 @@ { "in": "query", "name": "endAt", - "required": false, "schema": { "type": "number" } @@ -33529,25 +34822,20 @@ { "in": "query", "name": "startDate", - "required": false, "schema": { - "format": "date-time", "type": "string" } }, { "in": "query", "name": "endDate", - "required": false, "schema": { - "format": "date-time", "type": "string" } }, { "in": "query", "name": "timezone", - "required": false, "schema": { "type": "string" } @@ -33555,7 +34843,6 @@ { "in": "query", "name": "unit", - "required": false, "schema": { "type": "string" } @@ -33563,7 +34850,6 @@ { "in": "query", "name": "compare", - "required": false, "schema": { "enum": [ "prev", @@ -33575,7 +34861,6 @@ { "in": "query", "name": "path", - "required": false, "schema": { "type": "string" } @@ -33583,7 +34868,6 @@ { "in": "query", "name": "referrer", - "required": false, "schema": { "type": "string" } @@ -33591,7 +34875,6 @@ { "in": "query", "name": "title", - "required": false, "schema": { "type": "string" } @@ -33599,7 +34882,6 @@ { "in": "query", "name": "query", - "required": false, "schema": { "type": "string" } @@ -33607,7 +34889,6 @@ { "in": "query", "name": "os", - "required": false, "schema": { "type": "string" } @@ -33615,7 +34896,6 @@ { "in": "query", "name": "browser", - "required": false, "schema": { "type": "string" } @@ -33623,7 +34903,6 @@ { "in": "query", "name": "device", - "required": false, "schema": { "type": "string" } @@ -33631,7 +34910,6 @@ { "in": "query", "name": "country", - "required": false, "schema": { "type": "string" } @@ -33639,7 +34917,6 @@ { "in": "query", "name": "region", - "required": false, "schema": { "type": "string" } @@ -33647,7 +34924,6 @@ { "in": "query", "name": "city", - "required": false, "schema": { "type": "string" } @@ -33655,7 +34931,6 @@ { "in": "query", "name": "tag", - "required": false, "schema": { "type": "string" } @@ -33663,7 +34938,6 @@ { "in": "query", "name": "hostname", - "required": false, "schema": { "type": "string" } @@ -33671,7 +34945,6 @@ { "in": "query", "name": "distinctId", - "required": false, "schema": { "type": "string" } @@ -33679,7 +34952,6 @@ { "in": "query", "name": "language", - "required": false, "schema": { "type": "string" } @@ -33687,7 +34959,6 @@ { "in": "query", "name": "event", - "required": false, "schema": { "type": "string" } @@ -33695,7 +34966,6 @@ { "in": "query", "name": "utmSource", - "required": false, "schema": { "type": "string" } @@ -33703,7 +34973,6 @@ { "in": "query", "name": "utmMedium", - "required": false, "schema": { "type": "string" } @@ -33711,7 +34980,6 @@ { "in": "query", "name": "utmCampaign", - "required": false, "schema": { "type": "string" } @@ -33719,7 +34987,6 @@ { "in": "query", "name": "utmContent", - "required": false, "schema": { "type": "string" } @@ -33727,15 +34994,6 @@ { "in": "query", "name": "utmTerm", - "required": false, - "schema": { - "type": "string" - } - }, - { - "in": "query", - "name": "excludeBounce", - "required": false, "schema": { "type": "string" } @@ -33743,34 +35001,40 @@ { "in": "query", "name": "segment", - "required": false, "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$", "type": "string" } }, { "in": "query", "name": "cohort", - "required": false, "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$", "type": "string" } }, { "in": "query", "name": "eventType", - "required": false, "schema": { - "minimum": 1, + "exclusiveMinimum": 0, + "maximum": 9007199254740991, "type": "integer" } }, + { + "in": "query", + "name": "excludeBounce", + "schema": { + "type": "string" + } + }, { "in": "query", "name": "match", - "required": false, "schema": { "enum": [ "all", @@ -33782,34 +35046,33 @@ { "in": "query", "name": "page", - "required": false, "schema": { - "minimum": 1, + "exclusiveMinimum": 0, + "maximum": 9007199254740991, "type": "integer" } }, { "in": "query", "name": "pageSize", - "required": false, "schema": { - "minimum": 1, + "exclusiveMinimum": 0, + "maximum": 9007199254740991, "type": "integer" } }, { "in": "query", "name": "maxResults", - "required": false, "schema": { - "minimum": 1, + "exclusiveMinimum": 0, + "maximum": 9007199254740991, "type": "integer" } }, { "in": "query", "name": "search", - "required": false, "schema": { "type": "string" } @@ -33820,142 +35083,11 @@ "content": { "application/json": { "schema": { - "properties": { - "count": { - "type": "number" - }, - "data": { - "items": { - "properties": { - "browser": { - "type": "string" - }, - "city": { - "type": "string" - }, - "country": { - "type": "string" - }, - "createdAt": { - "anyOf": [ - { - "type": "string" - }, - { - "format": "date-time", - "type": "string" - } - ] - }, - "device": { - "type": "string" - }, - "events": { - "type": "number" - }, - "firstAt": { - "anyOf": [ - { - "type": "string" - }, - { - "format": "date-time", - "type": "string" - } - ] - }, - "hostname": { - "type": "string" - }, - "id": { - "type": "string" - }, - "language": { - "type": "string" - }, - "lastAt": { - "anyOf": [ - { - "type": "string" - }, - { - "format": "date-time", - "type": "string" - } - ] - }, - "os": { - "type": "string" - }, - "region": { - "type": "string" - }, - "screen": { - "type": "string" - }, - "views": { - "type": "number" - }, - "visits": { - "type": "number" - }, - "websiteId": { - "type": "string" - } - }, - "required": [ - "id", - "websiteId", - "hostname", - "browser", - "os", - "device", - "screen", - "language", - "country", - "region", - "city", - "firstAt", - "lastAt", - "visits", - "views", - "events", - "createdAt" - ], - "type": "object" - }, - "type": "array" - }, - "isCapped": { - "type": "boolean" - }, - "orderBy": { - "type": "string" - }, - "page": { - "type": "number" - }, - "pageSize": { - "type": "number" - }, - "search": { - "type": "string" - }, - "sortDescending": { - "type": "boolean" - } - }, - "required": [ - "data", - "count", - "page", - "pageSize" - ], - "type": "object" + "$ref": "#/components/schemas/WebsiteSessionPage" } } }, - "description": "The operation completed successfully." + "description": "A page of sessions." }, "400": { "content": { @@ -33996,23 +35128,29 @@ { "bearerAuth": [] }, + { + "oauth2": [ + "analytics:read" + ] + }, { "shareContext": [], "shareToken": [] } ], - "summary": "Get websites website id sessions", + "summary": "List visitor sessions", "tags": [ "Websites" ], "x-umami-audience": "public", "x-umami-contract": "explicit", - "x-umami-source": "src/app/api/websites/[websiteId]/sessions/contract.generated.ts" + "x-umami-oauth-scope": "analytics:read", + "x-umami-source": "src/app/api/websites/[websiteId]/sessions/contract.ts" } }, "/api/websites/{websiteId}/sessions/{sessionId}": { "delete": { - "operationId": "deleteWebsitesWebsiteIdSessionsSessionId", + "operationId": "deleteWebsiteSession", "parameters": [ { "in": "path", @@ -34116,7 +35254,7 @@ "x-umami-source": "src/app/api/websites/[websiteId]/sessions/[sessionId]/contract.generated.ts" }, "get": { - "operationId": "getWebsitesWebsiteIdSessionsSessionId", + "operationId": "getWebsiteSession", "parameters": [ { "in": "path", @@ -34193,6 +35331,11 @@ { "bearerAuth": [] }, + { + "oauth2": [ + "analytics:read" + ] + }, { "shareContext": [], "shareToken": [] @@ -34204,12 +35347,13 @@ ], "x-umami-audience": "public", "x-umami-contract": "explicit", + "x-umami-oauth-scope": "analytics:read", "x-umami-source": "src/app/api/websites/[websiteId]/sessions/[sessionId]/contract.generated.ts" } }, "/api/websites/{websiteId}/sessions/{sessionId}/activity": { "get": { - "operationId": "getWebsitesWebsiteIdSessionsSessionIdActivity", + "operationId": "getWebsiteSessionActivity", "parameters": [ { "in": "path", @@ -34357,6 +35501,11 @@ { "bearerAuth": [] }, + { + "oauth2": [ + "analytics:read" + ] + }, { "shareContext": [], "shareToken": [] @@ -34368,12 +35517,13 @@ ], "x-umami-audience": "public", "x-umami-contract": "explicit", + "x-umami-oauth-scope": "analytics:read", "x-umami-source": "src/app/api/websites/[websiteId]/sessions/[sessionId]/activity/contract.generated.ts" } }, "/api/websites/{websiteId}/sessions/{sessionId}/properties": { "get": { - "operationId": "getWebsitesWebsiteIdSessionsSessionIdProperties", + "operationId": "getWebsiteSessionProperties", "parameters": [ { "in": "path", @@ -34480,6 +35630,11 @@ { "bearerAuth": [] }, + { + "oauth2": [ + "analytics:read" + ] + }, { "shareContext": [], "shareToken": [] @@ -34491,12 +35646,13 @@ ], "x-umami-audience": "public", "x-umami-contract": "explicit", + "x-umami-oauth-scope": "analytics:read", "x-umami-source": "src/app/api/websites/[websiteId]/sessions/[sessionId]/properties/contract.generated.ts" } }, "/api/websites/{websiteId}/sessions/{sessionId}/replays": { "get": { - "operationId": "getWebsitesWebsiteIdSessionsSessionIdReplays", + "operationId": "getWebsiteSessionReplays", "parameters": [ { "in": "path", @@ -34797,7 +35953,7 @@ }, "/api/websites/{websiteId}/sessions/stats": { "get": { - "operationId": "getWebsitesWebsiteIdSessionsStats", + "operationId": "getWebsiteSessionStats", "parameters": [ { "in": "path", @@ -35082,6 +36238,11 @@ { "bearerAuth": [] }, + { + "oauth2": [ + "analytics:read" + ] + }, { "shareContext": [], "shareToken": [] @@ -35093,12 +36254,13 @@ ], "x-umami-audience": "public", "x-umami-contract": "explicit", + "x-umami-oauth-scope": "analytics:read", "x-umami-source": "src/app/api/websites/[websiteId]/sessions/stats/contract.generated.ts" } }, "/api/websites/{websiteId}/sessions/weekly": { "get": { - "operationId": "getWebsitesWebsiteIdSessionsWeekly", + "operationId": "getWebsiteSessionsWeekly", "parameters": [ { "in": "path", @@ -35412,7 +36574,7 @@ }, "/api/websites/{websiteId}/shares": { "get": { - "operationId": "getWebsitesWebsiteIdShares", + "operationId": "getWebsiteShares", "parameters": [ { "in": "path", @@ -35850,7 +37012,7 @@ "x-umami-source": "src/app/api/websites/[websiteId]/shares/contract.generated.ts" }, "post": { - "operationId": "postWebsitesWebsiteIdShares", + "operationId": "createWebsiteShare", "parameters": [ { "in": "path", @@ -36040,20 +37202,24 @@ }, "/api/websites/{websiteId}/stats": { "get": { - "operationId": "getWebsitesWebsiteIdStats", + "description": "Returns pageviews, unique visitors, visits, bounces and total time on site for the date range, plus the same totals for the comparison period (`compare`: prev or yoy).", + "operationId": "getWebsiteStats", "parameters": [ { + "description": "Website ID.", "in": "path", "name": "websiteId", "required": true, "schema": { + "description": "Website ID.", + "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$", "type": "string" } }, { "in": "query", "name": "startAt", - "required": false, "schema": { "type": "number" } @@ -36061,7 +37227,6 @@ { "in": "query", "name": "endAt", - "required": false, "schema": { "type": "number" } @@ -36069,25 +37234,20 @@ { "in": "query", "name": "startDate", - "required": false, "schema": { - "format": "date-time", "type": "string" } }, { "in": "query", "name": "endDate", - "required": false, "schema": { - "format": "date-time", "type": "string" } }, { "in": "query", "name": "timezone", - "required": false, "schema": { "type": "string" } @@ -36095,7 +37255,6 @@ { "in": "query", "name": "unit", - "required": false, "schema": { "type": "string" } @@ -36103,7 +37262,6 @@ { "in": "query", "name": "compare", - "required": false, "schema": { "enum": [ "prev", @@ -36115,7 +37273,6 @@ { "in": "query", "name": "path", - "required": false, "schema": { "type": "string" } @@ -36123,7 +37280,6 @@ { "in": "query", "name": "referrer", - "required": false, "schema": { "type": "string" } @@ -36131,7 +37287,6 @@ { "in": "query", "name": "title", - "required": false, "schema": { "type": "string" } @@ -36139,7 +37294,6 @@ { "in": "query", "name": "query", - "required": false, "schema": { "type": "string" } @@ -36147,7 +37301,6 @@ { "in": "query", "name": "os", - "required": false, "schema": { "type": "string" } @@ -36155,7 +37308,6 @@ { "in": "query", "name": "browser", - "required": false, "schema": { "type": "string" } @@ -36163,7 +37315,6 @@ { "in": "query", "name": "device", - "required": false, "schema": { "type": "string" } @@ -36171,7 +37322,6 @@ { "in": "query", "name": "country", - "required": false, "schema": { "type": "string" } @@ -36179,7 +37329,6 @@ { "in": "query", "name": "region", - "required": false, "schema": { "type": "string" } @@ -36187,7 +37336,6 @@ { "in": "query", "name": "city", - "required": false, "schema": { "type": "string" } @@ -36195,7 +37343,6 @@ { "in": "query", "name": "tag", - "required": false, "schema": { "type": "string" } @@ -36203,7 +37350,6 @@ { "in": "query", "name": "hostname", - "required": false, "schema": { "type": "string" } @@ -36211,7 +37357,6 @@ { "in": "query", "name": "distinctId", - "required": false, "schema": { "type": "string" } @@ -36219,7 +37364,6 @@ { "in": "query", "name": "language", - "required": false, "schema": { "type": "string" } @@ -36227,7 +37371,6 @@ { "in": "query", "name": "event", - "required": false, "schema": { "type": "string" } @@ -36235,7 +37378,6 @@ { "in": "query", "name": "utmSource", - "required": false, "schema": { "type": "string" } @@ -36243,7 +37385,6 @@ { "in": "query", "name": "utmMedium", - "required": false, "schema": { "type": "string" } @@ -36251,7 +37392,6 @@ { "in": "query", "name": "utmCampaign", - "required": false, "schema": { "type": "string" } @@ -36259,7 +37399,6 @@ { "in": "query", "name": "utmContent", - "required": false, "schema": { "type": "string" } @@ -36267,15 +37406,6 @@ { "in": "query", "name": "utmTerm", - "required": false, - "schema": { - "type": "string" - } - }, - { - "in": "query", - "name": "excludeBounce", - "required": false, "schema": { "type": "string" } @@ -36283,34 +37413,40 @@ { "in": "query", "name": "segment", - "required": false, "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$", "type": "string" } }, { "in": "query", "name": "cohort", - "required": false, "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$", "type": "string" } }, { "in": "query", "name": "eventType", - "required": false, "schema": { - "minimum": 1, + "exclusiveMinimum": 0, + "maximum": 9007199254740991, "type": "integer" } }, + { + "in": "query", + "name": "excludeBounce", + "schema": { + "type": "string" + } + }, { "in": "query", "name": "match", - "required": false, "schema": { "enum": [ "all", @@ -36325,50 +37461,11 @@ "content": { "application/json": { "schema": { - "properties": { - "comparison": { - "items": { - "properties": { - "bounces": { - "type": "number" - }, - "pageviews": { - "type": "number" - }, - "totaltime": { - "type": "number" - }, - "visitors": { - "type": "number" - }, - "visits": { - "type": "number" - } - }, - "required": [ - "pageviews", - "visitors", - "visits", - "bounces", - "totaltime" - ], - "type": "object" - }, - "type": "array" - }, - "length": { - "type": "number" - } - }, - "required": [ - "comparison", - "length" - ], - "type": "object" + "$ref": "#/components/schemas/WebsiteStats" } } }, - "description": "The operation completed successfully." + "description": "Summary stats with comparison." }, "400": { "content": { @@ -36409,23 +37506,29 @@ { "bearerAuth": [] }, + { + "oauth2": [ + "analytics:read" + ] + }, { "shareContext": [], "shareToken": [] } ], - "summary": "Get websites website id stats", + "summary": "Get website summary stats", "tags": [ "Websites" ], "x-umami-audience": "public", "x-umami-contract": "explicit", - "x-umami-source": "src/app/api/websites/[websiteId]/stats/contract.generated.ts" + "x-umami-oauth-scope": "analytics:read", + "x-umami-source": "src/app/api/websites/[websiteId]/stats/contract.ts" } }, "/api/websites/{websiteId}/transfer": { "post": { - "operationId": "postWebsitesWebsiteIdTransfer", + "operationId": "transferWebsite", "parameters": [ { "in": "path", @@ -36630,7 +37733,7 @@ }, "/api/websites/{websiteId}/values": { "get": { - "operationId": "getWebsitesWebsiteIdValues", + "operationId": "getWebsiteValues", "parameters": [ { "in": "path", @@ -36938,6 +38041,9 @@ { "name": "Links" }, + { + "name": "OAuth" + }, { "name": "Pixels" }, diff --git a/scripts/generate-api-client.ts b/scripts/generate-api-client.ts new file mode 100644 index 000000000..f845308c2 --- /dev/null +++ b/scripts/generate-api-client.ts @@ -0,0 +1,335 @@ +/** + * Generates `packages/api-client/src/generated/*` from `public/openapi.json`. + * + * pnpm generate:api-client write generated files + * pnpm generate:api-client --check fail if generated files are stale + * + * The OpenAPI document is the single source of truth: operation IDs become client method names, + * parameter/body/response schemas become input/output types. Never edit generated files by hand. + */ +import { execFile } from 'node:child_process'; +import { mkdir, readFile, writeFile } from 'node:fs/promises'; +import path from 'node:path'; +import { promisify } from 'node:util'; +import openapiTS, { astToString } from 'openapi-typescript'; + +const execFileAsync = promisify(execFile); +const projectRoot = process.cwd(); +const specFile = path.join(projectRoot, 'public', 'openapi.json'); +const outputDir = path.join(projectRoot, 'packages', 'api-client', 'src', 'generated'); +const check = process.argv.includes('--check'); + +const HEADER = `// GENERATED FILE. DO NOT EDIT. +// Source: public/openapi.json — regenerate with \`pnpm generate:api\`. +`; + +const HTTP_METHODS = ['get', 'post', 'put', 'patch', 'delete'] as const; +type HttpMethod = (typeof HTTP_METHODS)[number]; +const INCLUDED_AUDIENCES = new Set(['public', 'collect']); +const IDENTIFIER = /^[A-Za-z_$][A-Za-z0-9_$]*$/; + +interface OpenApiParameter { + name: string; + in: 'path' | 'query' | 'header' | 'cookie'; + required?: boolean; +} + +interface OpenApiOperation { + operationId?: string; + summary?: string; + description?: string; + tags?: string[]; + parameters?: OpenApiParameter[]; + requestBody?: { required?: boolean }; + 'x-umami-audience'?: string; + 'x-umami-oauth-scope'?: string; +} + +interface OpenApiDocument { + info: { version: string }; + paths: Record>; +} + +interface OperationDefinition { + operationId: string; + method: HttpMethod; + path: string; + summary: string; + description?: string; + tags: string[]; + pathParams: string[]; + queryParams: string[]; + requiredQueryParams: string[]; + hasBody: boolean; + bodyRequired: boolean; + scope?: string; +} + +function collectOperations(document: OpenApiDocument): OperationDefinition[] { + const operations: OperationDefinition[] = []; + const seen = new Map(); + const errors: string[] = []; + + for (const [route, pathItem] of Object.entries(document.paths)) { + for (const method of HTTP_METHODS) { + const operation = pathItem[method]; + + if (!operation) { + continue; + } + + const key = `${method.toUpperCase()} ${route}`; + const audience = operation['x-umami-audience'] ?? 'public'; + + if (!INCLUDED_AUDIENCES.has(audience)) { + continue; + } + + const operationId = operation.operationId; + + if (!operationId) { + errors.push(`${key} has no operationId.`); + continue; + } + + if (!IDENTIFIER.test(operationId)) { + errors.push(`${key} has an operationId that is not a valid identifier: ${operationId}`); + continue; + } + + const previous = seen.get(operationId); + + if (previous) { + errors.push(`Duplicate operationId ${operationId}: ${previous} and ${key}`); + continue; + } + + seen.set(operationId, key); + + const parameters = operation.parameters ?? []; + const pathParams = [...route.matchAll(/\{([^}]+)}/g)].map(match => match[1]); + const queryParams = parameters + .filter(parameter => parameter.in === 'query') + .map(parameter => parameter.name); + const requiredQueryParams = parameters + .filter(parameter => parameter.in === 'query' && parameter.required) + .map(parameter => parameter.name); + + operations.push({ + operationId, + method, + path: route, + summary: operation.summary ?? operationId, + description: operation.description, + tags: operation.tags ?? [], + pathParams, + queryParams, + requiredQueryParams, + hasBody: !!operation.requestBody, + bodyRequired: operation.requestBody?.required !== false && !!operation.requestBody, + scope: operation['x-umami-oauth-scope'], + }); + } + } + + if (errors.length) { + throw new Error(`Cannot generate API client:\n${errors.map(error => `- ${error}`).join('\n')}`); + } + + return operations.sort((left, right) => left.operationId.localeCompare(right.operationId)); +} + +function jsDoc(lines: (string | undefined)[]) { + const content = lines.filter((line): line is string => !!line); + + if (!content.length) { + return ''; + } + + return ` /**\n${content.map(line => ` * ${line.replace(/\*\//g, '* /')}`).join('\n')}\n */\n`; +} + +function renderOperations(operations: OperationDefinition[], version: string) { + const table = operations + .map(operation => { + const fields = [ + `operationId: '${operation.operationId}'`, + `method: '${operation.method}'`, + `path: '${operation.path}'`, + `pathParams: [${operation.pathParams.map(name => `'${name}'`).join(', ')}]`, + `queryParams: [${operation.queryParams.map(name => `'${name}'`).join(', ')}]`, + `hasBody: ${operation.hasBody}`, + ...(operation.scope ? [`scope: '${operation.scope}'`] : []), + ]; + + return ` ${operation.operationId}: { ${fields.join(', ')} },`; + }) + .join('\n'); + + const methods = operations + .map(operation => { + const inputOptional = + !operation.pathParams.length && + !operation.requiredQueryParams.length && + !operation.bodyRequired; + const doc = jsDoc([ + operation.summary, + operation.description ? '' : undefined, + operation.description, + '', + `\`${operation.method.toUpperCase()} ${operation.path}\``, + operation.scope ? `OAuth scope: \`${operation.scope}\`` : undefined, + ]); + const id = operation.operationId; + + return `${doc} ${id}(input${inputOptional ? '?' : ''}: OperationInput<'${id}'>, options?: RequestOptions): Promise> { + return this.execute('${id}', input, options); + }`; + }) + .join('\n\n'); + + return `${HEADER} +import type { RequestOptions } from '../types'; +import type { components, operations as OperationTypes } from './types'; + +export const API_VERSION = '${version}'; + +export type HttpMethod = ${HTTP_METHODS.map(method => `'${method}'`).join(' | ')}; + +export interface OperationDefinition { + readonly operationId: string; + readonly method: HttpMethod; + readonly path: string; + readonly pathParams: readonly string[]; + readonly queryParams: readonly string[]; + readonly hasBody: boolean; + readonly scope?: string; +} + +export const operations = { +${table} +} as const satisfies Record; + +export type OperationId = keyof typeof operations; + +export type Schemas = components['schemas']; + +type Clean = [T] extends [never] ? Record : T; + +type OperationPathParams = OperationTypes[K]['parameters'] extends { + path: infer P; +} + ? Clean

+ : Record; + +type OperationQuery = Clean< + NonNullable +>; + +type OperationBody = OperationTypes[K] extends { requestBody?: infer R } + ? NonNullable extends { content: { 'application/json': infer B } } + ? Clean + : Record + : Record; + +export type OperationInput = OperationPathParams & + OperationQuery & + OperationBody & + Record; + +export type OperationOutput = OperationTypes[K]['responses'] extends { + 200: { content: { 'application/json': infer R } }; +} + ? R + : unknown; + +export abstract class GeneratedUmamiClient { + protected abstract execute( + operationId: K, + input: Record | undefined, + options?: RequestOptions, + ): Promise>; + +${methods} +} +`; +} + +async function format(files: string[]) { + const biome = path.join( + projectRoot, + 'node_modules', + '.bin', + process.platform === 'win32' ? 'biome.cmd' : 'biome', + ); + + await execFileAsync(biome, ['format', '--write', ...files], { + cwd: projectRoot, + shell: process.platform === 'win32', + }); +} + +async function readExisting(file: string) { + try { + return await readFile(file, 'utf8'); + } catch { + return ''; + } +} + +const document = JSON.parse(await readFile(specFile, 'utf8')) as OpenApiDocument; +const operations = collectOperations(document); +const ast = await openapiTS(document as never, { + alphabetize: true, + exportType: false, +}); + +const files: Record = { + 'types.ts': `${HEADER}\n${astToString(ast)}`, + 'operations.ts': renderOperations(operations, document.info.version), +}; + +const tmpDir = path.join(outputDir, check ? '.check' : ''); + +await mkdir(tmpDir, { recursive: true }); + +const written: string[] = []; + +for (const [name, content] of Object.entries(files)) { + const file = path.join(tmpDir, name); + await writeFile(file, content, 'utf8'); + written.push(file); +} + +await format(written); + +if (check) { + let stale = false; + + for (const name of Object.keys(files)) { + const [expected, actual] = await Promise.all([ + readFile(path.join(tmpDir, name), 'utf8'), + readExisting(path.join(outputDir, name)), + ]); + + if (expected !== actual) { + console.error( + `packages/api-client/src/generated/${name} is stale. Run \`pnpm generate:api\`.`, + ); + stale = true; + } + } + + const { rm } = await import('node:fs/promises'); + await rm(tmpDir, { recursive: true, force: true }); + + if (stale) { + process.exit(1); + } + + console.log(`API client is up to date (${operations.length} operations).`); +} else { + console.log( + `Generated ${operations.length} operations into ${path.relative(projectRoot, outputDir)}.`, + ); +} diff --git a/src/app/.well-known/oauth-authorization-server/route.ts b/src/app/.well-known/oauth-authorization-server/route.ts new file mode 100644 index 000000000..2b1b0ff56 --- /dev/null +++ b/src/app/.well-known/oauth-authorization-server/route.ts @@ -0,0 +1,21 @@ +import { isOAuthEnabled } from '@/lib/oauth/config'; +import { + buildAuthorizationServerMetadata, + corsPreflight, + metadataResponse, +} from '@/lib/oauth/metadata'; +import { notFound } from '@/lib/response'; + +export const dynamic = 'force-dynamic'; + +export function OPTIONS() { + return corsPreflight(); +} + +export async function GET(request: Request) { + if (!isOAuthEnabled()) { + return notFound(); + } + + return metadataResponse(buildAuthorizationServerMetadata(request.headers)); +} diff --git a/src/app/.well-known/oauth-protected-resource/mcp/route.ts b/src/app/.well-known/oauth-protected-resource/mcp/route.ts new file mode 100644 index 000000000..6a22e14ac --- /dev/null +++ b/src/app/.well-known/oauth-protected-resource/mcp/route.ts @@ -0,0 +1,22 @@ +import { isOAuthEnabled } from '@/lib/oauth/config'; +import { + buildProtectedResourceMetadata, + corsPreflight, + metadataResponse, +} from '@/lib/oauth/metadata'; +import { notFound } from '@/lib/response'; + +export const dynamic = 'force-dynamic'; + +export function OPTIONS() { + return corsPreflight(); +} + +/** Path-scoped protected resource metadata for the MCP endpoint (RFC 9728 §3.1). */ +export async function GET(request: Request) { + if (!isOAuthEnabled()) { + return notFound(); + } + + return metadataResponse(buildProtectedResourceMetadata(request.headers)); +} diff --git a/src/app/.well-known/oauth-protected-resource/route.ts b/src/app/.well-known/oauth-protected-resource/route.ts new file mode 100644 index 000000000..a8b6bbb36 --- /dev/null +++ b/src/app/.well-known/oauth-protected-resource/route.ts @@ -0,0 +1,21 @@ +import { isOAuthEnabled } from '@/lib/oauth/config'; +import { + buildProtectedResourceMetadata, + corsPreflight, + metadataResponse, +} from '@/lib/oauth/metadata'; +import { notFound } from '@/lib/response'; + +export const dynamic = 'force-dynamic'; + +export function OPTIONS() { + return corsPreflight(); +} + +export async function GET(request: Request) { + if (!isOAuthEnabled()) { + return notFound(); + } + + return metadataResponse(buildProtectedResourceMetadata(request.headers)); +} diff --git a/src/app/api/2fa/disable/contract.generated.ts b/src/app/api/2fa/disable/contract.generated.ts index ec5562626..d2b2a5482 100644 --- a/src/app/api/2fa/disable/contract.generated.ts +++ b/src/app/api/2fa/disable/contract.generated.ts @@ -9,7 +9,7 @@ const operation1 = defineOperation({ audience: 'public', auth: 'bearer', operation: { - operationId: 'post2faDisable', + operationId: 'disableTwoFactor', summary: 'Create or update 2fa disable', tags: ['Two-factor authentication'], requestBody: { diff --git a/src/app/api/2fa/setup/cancel/contract.generated.ts b/src/app/api/2fa/setup/cancel/contract.generated.ts index 4f0f317be..69752738f 100644 --- a/src/app/api/2fa/setup/cancel/contract.generated.ts +++ b/src/app/api/2fa/setup/cancel/contract.generated.ts @@ -9,7 +9,7 @@ const operation1 = defineOperation({ audience: 'public', auth: 'bearer', operation: { - operationId: 'post2faSetupCancel', + operationId: 'cancelTwoFactorSetup', summary: 'Create or update 2fa setup cancel', tags: ['Two-factor authentication'], responses: { diff --git a/src/app/api/2fa/setup/confirm/contract.generated.ts b/src/app/api/2fa/setup/confirm/contract.generated.ts index 5fea4623b..8dbbeeecd 100644 --- a/src/app/api/2fa/setup/confirm/contract.generated.ts +++ b/src/app/api/2fa/setup/confirm/contract.generated.ts @@ -9,7 +9,7 @@ const operation1 = defineOperation({ audience: 'public', auth: 'bearer', operation: { - operationId: 'post2faSetupConfirm', + operationId: 'confirmTwoFactorSetup', summary: 'Create or update 2fa setup confirm', tags: ['Two-factor authentication'], requestBody: { diff --git a/src/app/api/2fa/setup/initiate/contract.generated.ts b/src/app/api/2fa/setup/initiate/contract.generated.ts index 618065a42..88a768a26 100644 --- a/src/app/api/2fa/setup/initiate/contract.generated.ts +++ b/src/app/api/2fa/setup/initiate/contract.generated.ts @@ -9,7 +9,7 @@ const operation1 = defineOperation({ audience: 'public', auth: 'bearer', operation: { - operationId: 'post2faSetupInitiate', + operationId: 'initiateTwoFactorSetup', summary: 'Create or update 2fa setup initiate', tags: ['Two-factor authentication'], responses: { diff --git a/src/app/api/2fa/status/contract.generated.ts b/src/app/api/2fa/status/contract.generated.ts index a71bb6edb..2b5314b5c 100644 --- a/src/app/api/2fa/status/contract.generated.ts +++ b/src/app/api/2fa/status/contract.generated.ts @@ -9,7 +9,7 @@ const operation1 = defineOperation({ audience: 'public', auth: 'bearer', operation: { - operationId: 'get2faStatus', + operationId: 'getTwoFactorStatus', summary: 'Get 2fa status', tags: ['Two-factor authentication'], responses: { diff --git a/src/app/api/2fa/verify/contract.generated.ts b/src/app/api/2fa/verify/contract.generated.ts index 02885c1c1..9e6fa5db0 100644 --- a/src/app/api/2fa/verify/contract.generated.ts +++ b/src/app/api/2fa/verify/contract.generated.ts @@ -9,7 +9,7 @@ const operation1 = defineOperation({ audience: 'public', auth: 'none', operation: { - operationId: 'post2faVerify', + operationId: 'verifyTwoFactor', summary: 'Create or update 2fa verify', tags: ['Two-factor authentication'], requestBody: { diff --git a/src/app/api/auth/logout/contract.generated.ts b/src/app/api/auth/logout/contract.generated.ts index f73a44f6d..3c99644c1 100644 --- a/src/app/api/auth/logout/contract.generated.ts +++ b/src/app/api/auth/logout/contract.generated.ts @@ -9,7 +9,7 @@ const operation1 = defineOperation({ audience: 'public', auth: 'bearer', operation: { - operationId: 'postAuthLogout', + operationId: 'logout', summary: 'Create or update auth logout', tags: ['Authentication'], responses: { diff --git a/src/app/api/auth/sso/contract.generated.ts b/src/app/api/auth/sso/contract.generated.ts index b8152ebb8..0ef9e7841 100644 --- a/src/app/api/auth/sso/contract.generated.ts +++ b/src/app/api/auth/sso/contract.generated.ts @@ -9,7 +9,7 @@ const operation1 = defineOperation({ audience: 'public', auth: 'bearer', operation: { - operationId: 'postAuthSso', + operationId: 'sso', summary: 'Create or update auth sso', tags: ['Authentication'], responses: { diff --git a/src/app/api/auth/subscription/contract.generated.ts b/src/app/api/auth/subscription/contract.generated.ts index d9031eff7..a3e75a1ce 100644 --- a/src/app/api/auth/subscription/contract.generated.ts +++ b/src/app/api/auth/subscription/contract.generated.ts @@ -9,7 +9,7 @@ const operation1 = defineOperation({ audience: 'public', auth: 'bearer-or-share', operation: { - operationId: 'getAuthSubscription', + operationId: 'getSubscription', summary: 'Get auth subscription', tags: ['Authentication'], parameters: [ diff --git a/src/app/api/auth/verify/contract.generated.ts b/src/app/api/auth/verify/contract.generated.ts index 28f5a8e24..94f7cfcc9 100644 --- a/src/app/api/auth/verify/contract.generated.ts +++ b/src/app/api/auth/verify/contract.generated.ts @@ -9,7 +9,7 @@ const operation1 = defineOperation({ audience: 'public', auth: 'bearer', operation: { - operationId: 'postAuthVerify', + operationId: 'verify', summary: 'Create or update auth verify', tags: ['Authentication'], responses: { diff --git a/src/app/api/batch/contract.generated.ts b/src/app/api/batch/contract.generated.ts index 2de6f21ef..2bfd66f68 100644 --- a/src/app/api/batch/contract.generated.ts +++ b/src/app/api/batch/contract.generated.ts @@ -9,7 +9,7 @@ const operation1 = defineOperation({ audience: 'collect', auth: 'none', operation: { - operationId: 'postBatch', + operationId: 'batch', summary: 'Create or update batch', tags: ['Collection'], requestBody: { diff --git a/src/app/api/boards/[boardId]/clone/contract.generated.ts b/src/app/api/boards/[boardId]/clone/contract.generated.ts index 8c3709ed9..025de5eab 100644 --- a/src/app/api/boards/[boardId]/clone/contract.generated.ts +++ b/src/app/api/boards/[boardId]/clone/contract.generated.ts @@ -9,7 +9,7 @@ const operation1 = defineOperation({ audience: 'public', auth: 'bearer-or-share', operation: { - operationId: 'postBoardsBoardIdClone', + operationId: 'cloneBoard', summary: 'Create or update boards board id clone', tags: ['Boards'], parameters: [ diff --git a/src/app/api/boards/[boardId]/contract.generated.ts b/src/app/api/boards/[boardId]/contract.generated.ts index 002c909a8..917b5f996 100644 --- a/src/app/api/boards/[boardId]/contract.generated.ts +++ b/src/app/api/boards/[boardId]/contract.generated.ts @@ -9,7 +9,7 @@ const operation1 = defineOperation({ audience: 'public', auth: 'bearer', operation: { - operationId: 'deleteBoardsBoardId', + operationId: 'deleteBoard', summary: 'Delete boards board id', tags: ['Boards'], parameters: [ @@ -66,7 +66,7 @@ const operation2 = defineOperation({ audience: 'public', auth: 'bearer-or-share', operation: { - operationId: 'getBoardsBoardId', + operationId: 'getBoard', summary: 'Get boards board id', tags: ['Boards'], parameters: [ @@ -211,7 +211,7 @@ const operation3 = defineOperation({ audience: 'public', auth: 'bearer-or-share', operation: { - operationId: 'postBoardsBoardId', + operationId: 'updateBoard', summary: 'Create or update boards board id', tags: ['Boards'], parameters: [ diff --git a/src/app/api/boards/[boardId]/shares/contract.generated.ts b/src/app/api/boards/[boardId]/shares/contract.generated.ts index 0d784431f..f858cbf99 100644 --- a/src/app/api/boards/[boardId]/shares/contract.generated.ts +++ b/src/app/api/boards/[boardId]/shares/contract.generated.ts @@ -9,7 +9,7 @@ const operation1 = defineOperation({ audience: 'public', auth: 'bearer-or-share', operation: { - operationId: 'getBoardsBoardIdShares', + operationId: 'getBoardShares', summary: 'Get boards board id shares', tags: ['Boards'], parameters: [ @@ -428,7 +428,7 @@ const operation2 = defineOperation({ audience: 'public', auth: 'bearer', operation: { - operationId: 'postBoardsBoardIdShares', + operationId: 'createBoardShare', summary: 'Create or update boards board id shares', tags: ['Boards'], parameters: [ diff --git a/src/app/api/boards/contract.generated.ts b/src/app/api/boards/contract.generated.ts index 640e5989e..6e05b43dd 100644 --- a/src/app/api/boards/contract.generated.ts +++ b/src/app/api/boards/contract.generated.ts @@ -245,7 +245,7 @@ const operation2 = defineOperation({ audience: 'public', auth: 'bearer-or-share', operation: { - operationId: 'postBoards', + operationId: 'createBoard', summary: 'Create or update boards', tags: ['Boards'], requestBody: { diff --git a/src/app/api/links/[linkId]/contract.generated.ts b/src/app/api/links/[linkId]/contract.generated.ts index e54f93c1f..136d94c07 100644 --- a/src/app/api/links/[linkId]/contract.generated.ts +++ b/src/app/api/links/[linkId]/contract.generated.ts @@ -9,7 +9,7 @@ const operation1 = defineOperation({ audience: 'public', auth: 'bearer', operation: { - operationId: 'deleteLinksLinkId', + operationId: 'deleteLink', summary: 'Delete links link id', tags: ['Links'], parameters: [ @@ -66,7 +66,7 @@ const operation2 = defineOperation({ audience: 'public', auth: 'bearer-or-share', operation: { - operationId: 'getLinksLinkId', + operationId: 'getLink', summary: 'Get links link id', tags: ['Links'], parameters: [ @@ -160,7 +160,7 @@ const operation3 = defineOperation({ audience: 'public', auth: 'bearer', operation: { - operationId: 'postLinksLinkId', + operationId: 'updateLink', summary: 'Create or update links link id', tags: ['Links'], parameters: [ diff --git a/src/app/api/links/[linkId]/shares/contract.generated.ts b/src/app/api/links/[linkId]/shares/contract.generated.ts index 7ddee32b8..1bac8364f 100644 --- a/src/app/api/links/[linkId]/shares/contract.generated.ts +++ b/src/app/api/links/[linkId]/shares/contract.generated.ts @@ -9,7 +9,7 @@ const operation1 = defineOperation({ audience: 'public', auth: 'bearer-or-share', operation: { - operationId: 'getLinksLinkIdShares', + operationId: 'getLinkShares', summary: 'Get links link id shares', tags: ['Links'], parameters: [ @@ -428,7 +428,7 @@ const operation2 = defineOperation({ audience: 'public', auth: 'bearer', operation: { - operationId: 'postLinksLinkIdShares', + operationId: 'createLinkShare', summary: 'Create or update links link id shares', tags: ['Links'], parameters: [ diff --git a/src/app/api/links/contract.generated.ts b/src/app/api/links/contract.generated.ts index eb449f171..ece67b50c 100644 --- a/src/app/api/links/contract.generated.ts +++ b/src/app/api/links/contract.generated.ts @@ -194,7 +194,7 @@ const operation2 = defineOperation({ audience: 'public', auth: 'bearer', operation: { - operationId: 'postLinks', + operationId: 'createLink', summary: 'Create or update links', tags: ['Links'], requestBody: { diff --git a/src/app/api/me/api-keys/[keyId]/contract.generated.ts b/src/app/api/me/api-keys/[keyId]/contract.generated.ts index 4596c9378..5e04d5a6e 100644 --- a/src/app/api/me/api-keys/[keyId]/contract.generated.ts +++ b/src/app/api/me/api-keys/[keyId]/contract.generated.ts @@ -9,7 +9,7 @@ const operation1 = defineOperation({ audience: 'public', auth: 'bearer', operation: { - operationId: 'deleteMeApiKeysKeyId', + operationId: 'deleteMyApiKey', summary: 'Delete me api keys key id', tags: ['Account'], parameters: [ diff --git a/src/app/api/me/api-keys/contract.generated.ts b/src/app/api/me/api-keys/contract.generated.ts index 953e17401..90aa02c51 100644 --- a/src/app/api/me/api-keys/contract.generated.ts +++ b/src/app/api/me/api-keys/contract.generated.ts @@ -9,7 +9,7 @@ const operation1 = defineOperation({ audience: 'public', auth: 'bearer', operation: { - operationId: 'getMeApiKeys', + operationId: 'getMyApiKeys', summary: 'Get me api keys', tags: ['Account'], responses: { @@ -90,7 +90,7 @@ const operation2 = defineOperation({ audience: 'public', auth: 'bearer', operation: { - operationId: 'postMeApiKeys', + operationId: 'createMyApiKey', summary: 'Create or update me api keys', tags: ['Account'], requestBody: { diff --git a/src/app/api/me/contract.generated.ts b/src/app/api/me/contract.generated.ts index 18a395cd4..ef734a934 100644 --- a/src/app/api/me/contract.generated.ts +++ b/src/app/api/me/contract.generated.ts @@ -12,6 +12,7 @@ const operation1 = defineOperation({ operationId: 'getMe', summary: 'Get me', tags: ['Account'], + 'x-umami-oauth-scope': 'websites:read', responses: { '200': { description: 'The operation completed successfully.', @@ -50,6 +51,40 @@ const operation1 = defineOperation({ }, required: ['id', 'name'], }, + authType: { + anyOf: [ + { + const: 'session', + }, + { + const: 'share', + }, + { + const: 'api-key', + }, + { + const: 'oauth', + }, + ], + }, + oauth: { + type: 'object', + properties: { + clientId: { + type: 'string', + }, + scopes: { + type: 'array', + items: { + type: 'string', + }, + }, + tokenId: { + type: 'string', + }, + }, + required: ['clientId', 'scopes'], + }, shareToken: { type: 'object', properties: { diff --git a/src/app/api/me/password/contract.generated.ts b/src/app/api/me/password/contract.generated.ts index 7ca4956e8..11798e7b2 100644 --- a/src/app/api/me/password/contract.generated.ts +++ b/src/app/api/me/password/contract.generated.ts @@ -9,7 +9,7 @@ const operation1 = defineOperation({ audience: 'public', auth: 'bearer', operation: { - operationId: 'postMePassword', + operationId: 'updateMyPassword', summary: 'Create or update me password', tags: ['Account'], requestBody: { diff --git a/src/app/api/me/teams/contract.generated.ts b/src/app/api/me/teams/contract.generated.ts index a6029e5c4..421396cc0 100644 --- a/src/app/api/me/teams/contract.generated.ts +++ b/src/app/api/me/teams/contract.generated.ts @@ -9,7 +9,7 @@ const operation1 = defineOperation({ audience: 'public', auth: 'bearer', operation: { - operationId: 'getMeTeams', + operationId: 'getMyTeams', summary: 'Get me teams', tags: ['Account'], parameters: [ diff --git a/src/app/api/me/websites/contract.generated.ts b/src/app/api/me/websites/contract.generated.ts index bd23b9300..2a9363769 100644 --- a/src/app/api/me/websites/contract.generated.ts +++ b/src/app/api/me/websites/contract.generated.ts @@ -9,7 +9,7 @@ const operation1 = defineOperation({ audience: 'public', auth: 'bearer', operation: { - operationId: 'getMeWebsites', + operationId: 'getMyWebsites', summary: 'Get me websites', tags: ['Account'], parameters: [ diff --git a/src/app/api/oauth/authorize/contract.ts b/src/app/api/oauth/authorize/contract.ts new file mode 100644 index 000000000..6ef3b148b --- /dev/null +++ b/src/app/api/oauth/authorize/contract.ts @@ -0,0 +1,54 @@ +import { defineOperation } from '@/openapi/operation'; +import { badRequestResponse, jsonResponse, unauthorizedResponse } from '@/openapi/schemas'; +import { + authorizationDecisionSchema, + authorizationDetailsSchema, + authorizationRedirectSchema, + authorizationRequestSchema, + oauthErrorSchema, +} from '../schema'; + +const describeAuthorizationOperation = defineOperation({ + method: 'get', + path: '/api/oauth/authorize', + audience: 'internal', + auth: 'bearer', + operation: { + operationId: 'describeOAuthAuthorization', + summary: 'Describe a pending OAuth authorization request', + description: + 'Validates an OAuth 2.1 authorization request and returns the client and scopes for the consent screen. Requires an interactive user session.', + tags: ['OAuth'], + requestParams: { query: authorizationRequestSchema }, + responses: { + '200': jsonResponse(authorizationDetailsSchema, 'Authorization request details.'), + '400': jsonResponse(oauthErrorSchema, 'Invalid authorization request.'), + '401': unauthorizedResponse, + }, + }, +}); + +const decideAuthorizationOperation = defineOperation({ + method: 'post', + path: '/api/oauth/authorize', + audience: 'internal', + auth: 'bearer', + operation: { + operationId: 'decideOAuthAuthorization', + summary: 'Approve or deny an OAuth authorization request', + description: + 'Records the user decision. On approval an authorization code is issued and the redirect URL (with code, state and iss) is returned.', + tags: ['OAuth'], + requestBody: { + required: true, + content: { 'application/json': { schema: authorizationDecisionSchema } }, + }, + responses: { + '200': jsonResponse(authorizationRedirectSchema, 'Redirect URL for the browser.'), + '400': badRequestResponse, + '401': unauthorizedResponse, + }, + }, +}); + +export const operations = [describeAuthorizationOperation, decideAuthorizationOperation] as const; diff --git a/src/app/api/oauth/authorize/route.ts b/src/app/api/oauth/authorize/route.ts new file mode 100644 index 000000000..8cea852ea --- /dev/null +++ b/src/app/api/oauth/authorize/route.ts @@ -0,0 +1,100 @@ +import { + buildErrorRedirect, + buildSuccessRedirect, + issueAuthorizationCode, + validateAuthorizationRequest, +} from '@/lib/oauth/authorize'; +import { getIssuer, isOAuthEnabled } from '@/lib/oauth/config'; +import { OAuthError, oauthErrorResponse } from '@/lib/oauth/errors'; +import { OAUTH_SCOPE_DESCRIPTIONS } from '@/lib/oauth/scopes'; +import { parseRequest } from '@/lib/request'; +import { json, notFound, unauthorized } from '@/lib/response'; +import { authorizationDecisionSchema, authorizationRequestSchema } from '../schema'; + +function requireSession(auth: { user?: { id: string }; authType?: string } | null) { + // Only an interactive user session may grant consent: never an API key or another OAuth token. + return !!auth?.user?.id && (auth.authType === undefined || auth.authType === 'session'); +} + +/** + * Describes a pending authorization request for the consent screen. Validation errors are + * returned as JSON (the consent UI renders them) — no redirect happens until the user decides. + */ +export async function GET(request: Request) { + if (!isOAuthEnabled()) { + return notFound(); + } + + const { auth, query, error } = await parseRequest(request, authorizationRequestSchema); + + if (error) { + return error(); + } + + if (!requireSession(auth)) { + return unauthorized(); + } + + try { + const validated = await validateAuthorizationRequest(query, getIssuer(request.headers)); + + return json({ + client: { + id: validated.client.clientId, + name: validated.client.clientName, + uri: validated.client.clientUri, + logoUri: validated.client.logoUri, + source: validated.client.source, + }, + scopes: validated.scopes.map(scope => ({ + scope, + description: OAUTH_SCOPE_DESCRIPTIONS[scope], + })), + redirectUri: validated.redirectUri, + resource: validated.resource, + }); + } catch (err) { + return oauthErrorResponse(err); + } +} + +/** Records the user's decision and returns the redirect URL the browser should navigate to. */ +export async function POST(request: Request) { + if (!isOAuthEnabled()) { + return notFound(); + } + + const { auth, body, error } = await parseRequest(request, authorizationDecisionSchema); + + if (error) { + return error(); + } + + if (!requireSession(auth)) { + return unauthorized(); + } + + const issuer = getIssuer(request.headers); + const { decision, ...params } = body; + + try { + const validated = await validateAuthorizationRequest(params, issuer); + + if (decision !== 'approve') { + return json({ + redirectUrl: buildErrorRedirect( + validated.redirectUri, + new OAuthError('access_denied', 'The user denied the request.'), + validated.state, + issuer, + ), + }); + } + + const code = await issueAuthorizationCode(validated, auth.user.id); + + return json({ redirectUrl: buildSuccessRedirect(validated, code, issuer) }); + } catch (err) { + return oauthErrorResponse(err); + } +} diff --git a/src/app/api/oauth/register/contract.ts b/src/app/api/oauth/register/contract.ts new file mode 100644 index 000000000..b947e448e --- /dev/null +++ b/src/app/api/oauth/register/contract.ts @@ -0,0 +1,31 @@ +import { defineOperation } from '@/openapi/operation'; +import { jsonResponse } from '@/openapi/schemas'; +import { + clientRegistrationRequestSchema, + clientRegistrationResponseSchema, + oauthErrorSchema, +} from '../schema'; + +const registerOperation = defineOperation({ + method: 'post', + path: '/api/oauth/register', + audience: 'internal', + auth: 'none', + operation: { + operationId: 'oauthRegisterClient', + summary: 'Register an OAuth client (dynamic client registration)', + description: + 'RFC 7591 compatibility endpoint for MCP clients without Client ID Metadata Document support. Registers a public client. Prefer Client ID Metadata Documents (https URL client_id).', + tags: ['OAuth'], + requestBody: { + required: true, + content: { 'application/json': { schema: clientRegistrationRequestSchema } }, + }, + responses: { + '201': jsonResponse(clientRegistrationResponseSchema, 'Registered client.'), + '400': jsonResponse(oauthErrorSchema, 'Invalid client metadata.'), + }, + }, +}); + +export const operations = [registerOperation] as const; diff --git a/src/app/api/oauth/register/route.ts b/src/app/api/oauth/register/route.ts new file mode 100644 index 000000000..b66f9d85e --- /dev/null +++ b/src/app/api/oauth/register/route.ts @@ -0,0 +1,107 @@ +import { uuid } from '@/lib/crypto'; +import { getIpAddress } from '@/lib/ip'; +import { isDynamicRegistrationEnabled } from '@/lib/oauth/config'; +import { OAuthError, oauthErrorResponse, oauthJson } from '@/lib/oauth/errors'; +import { corsPreflight } from '@/lib/oauth/metadata'; +import { isAcceptableRedirectUri } from '@/lib/oauth/redirect'; +import redis from '@/lib/redis'; +import { parseRequest } from '@/lib/request'; +import { notFound } from '@/lib/response'; +import { createOauthClient } from '@/queries/prisma/oauth'; +import { clientRegistrationRequestSchema } from '../schema'; + +const REGISTRATIONS_PER_HOUR = 20; + +export function OPTIONS() { + return corsPreflight(); +} + +/** + * Dynamic Client Registration (RFC 7591) compatibility endpoint for MCP clients that do not yet + * support Client ID Metadata Documents. Public clients only; disable with OAUTH_DISABLE_DCR=1. + */ +export async function POST(request: Request) { + if (!isDynamicRegistrationEnabled()) { + return notFound(); + } + + const { body, error } = await parseRequest(request, clientRegistrationRequestSchema, { + skipAuth: true, + }); + + if (error) { + return oauthJson( + { error: 'invalid_client_metadata', error_description: 'Invalid registration request.' }, + { status: 400 }, + ); + } + + try { + if (redis.enabled) { + const ip = getIpAddress(request.headers) ?? 'unknown'; + const allowed = await redis.client.rateLimit( + `oauth:register:${ip}`, + REGISTRATIONS_PER_HOUR, + 60 * 60, + ); + + if (!allowed) { + throw new OAuthError('temporarily_unavailable', 'Too many registrations. Try again later.'); + } + } + + if (!body.redirect_uris.every(isAcceptableRedirectUri)) { + throw new OAuthError( + 'invalid_redirect_uri', + 'redirect_uris must be absolute https, loopback http or custom-scheme URLs without fragments.', + ); + } + + const grantTypes = body.grant_types ?? ['authorization_code', 'refresh_token']; + const responseTypes = body.response_types ?? ['code']; + + if (grantTypes.some(type => !['authorization_code', 'refresh_token'].includes(type))) { + throw new OAuthError('invalid_client_metadata', 'Unsupported grant_types.'); + } + + if (responseTypes.some(type => type !== 'code')) { + throw new OAuthError('invalid_client_metadata', 'Unsupported response_types.'); + } + + const id = uuid(); + const metadata = { + client_uri: body.client_uri, + logo_uri: body.logo_uri, + application_type: body.application_type, + software_id: body.software_id, + software_version: body.software_version, + scope: body.scope, + }; + + const client = await createOauthClient({ + id, + name: body.client_name, + redirectUris: body.redirect_uris, + metadata, + }); + + return oauthJson( + { + client_id: client.id, + client_id_issued_at: Math.floor((client.createdAt ?? new Date()).getTime() / 1000), + client_name: client.name, + redirect_uris: body.redirect_uris, + token_endpoint_auth_method: 'none', + grant_types: grantTypes, + response_types: responseTypes, + ...(body.client_uri ? { client_uri: body.client_uri } : {}), + ...(body.logo_uri ? { logo_uri: body.logo_uri } : {}), + ...(body.application_type ? { application_type: body.application_type } : {}), + ...(body.scope ? { scope: body.scope } : {}), + }, + { status: 201 }, + ); + } catch (err) { + return oauthErrorResponse(err); + } +} diff --git a/src/app/api/oauth/revoke/contract.ts b/src/app/api/oauth/revoke/contract.ts new file mode 100644 index 000000000..c259ead0c --- /dev/null +++ b/src/app/api/oauth/revoke/contract.ts @@ -0,0 +1,31 @@ +import { z } from 'zod'; +import { defineOperation } from '@/openapi/operation'; +import { jsonResponse } from '@/openapi/schemas'; +import { oauthErrorSchema, revocationRequestSchema } from '../schema'; + +const revokeOperation = defineOperation({ + method: 'post', + path: '/api/oauth/revoke', + audience: 'internal', + auth: 'none', + operation: { + operationId: 'oauthRevoke', + summary: 'Revoke an OAuth refresh token', + description: + 'RFC 7009 revocation endpoint. Revokes the given refresh token; unknown tokens are accepted silently.', + tags: ['OAuth'], + requestBody: { + required: true, + content: { + 'application/x-www-form-urlencoded': { schema: revocationRequestSchema }, + 'application/json': { schema: revocationRequestSchema }, + }, + }, + responses: { + '200': jsonResponse(z.object({}), 'Token revoked (or already invalid).'), + '400': jsonResponse(oauthErrorSchema, 'Malformed request.'), + }, + }, +}); + +export const operations = [revokeOperation] as const; diff --git a/src/app/api/oauth/revoke/route.ts b/src/app/api/oauth/revoke/route.ts new file mode 100644 index 000000000..e1c2c5abb --- /dev/null +++ b/src/app/api/oauth/revoke/route.ts @@ -0,0 +1,34 @@ +import { isOAuthEnabled } from '@/lib/oauth/config'; +import { oauthErrorResponse, oauthJson } from '@/lib/oauth/errors'; +import { readOAuthBody } from '@/lib/oauth/form'; +import { corsPreflight } from '@/lib/oauth/metadata'; +import { handleRevocationRequest } from '@/lib/oauth/token'; +import { notFound } from '@/lib/response'; + +export function OPTIONS() { + return corsPreflight(); +} + +/** RFC 7009 token revocation. Always returns 200 for well-formed requests. */ +export async function POST(request: Request) { + if (!isOAuthEnabled()) { + return notFound(); + } + + try { + const params = await readOAuthBody(request); + + if (typeof params.token !== 'string' || !params.token) { + return oauthJson( + { error: 'invalid_request', error_description: 'token is required.' }, + { status: 400 }, + ); + } + + await handleRevocationRequest(params); + + return oauthJson({}); + } catch (error) { + return oauthErrorResponse(error); + } +} diff --git a/src/app/api/oauth/schema.ts b/src/app/api/oauth/schema.ts new file mode 100644 index 000000000..aedfc8dd7 --- /dev/null +++ b/src/app/api/oauth/schema.ts @@ -0,0 +1,102 @@ +import { z } from 'zod'; + +export const authorizationRequestSchema = z + .object({ + response_type: z.string().optional(), + client_id: z.string().max(2048), + redirect_uri: z.string().max(2048).optional(), + scope: z.string().max(500).optional(), + state: z.string().max(1024).optional(), + code_challenge: z.string().max(128).optional(), + code_challenge_method: z.string().max(10).optional(), + resource: z.string().max(2048).optional(), + }) + .meta({ id: 'OAuthAuthorizationRequest' }); + +export const authorizationDecisionSchema = authorizationRequestSchema + .extend({ + decision: z.enum(['approve', 'deny']), + }) + .meta({ id: 'OAuthAuthorizationDecision' }); + +export const authorizationDetailsSchema = z + .object({ + client: z.object({ + id: z.string(), + name: z.string(), + uri: z.string().optional(), + logoUri: z.string().optional(), + source: z.enum(['metadata-document', 'registered']), + }), + scopes: z.array(z.object({ scope: z.string(), description: z.string() })), + redirectUri: z.string(), + resource: z.string(), + }) + .meta({ id: 'OAuthAuthorizationDetails' }); + +export const authorizationRedirectSchema = z + .object({ redirectUrl: z.string() }) + .meta({ id: 'OAuthAuthorizationRedirect' }); + +export const tokenRequestSchema = z + .object({ + grant_type: z.enum(['authorization_code', 'refresh_token']), + code: z.string().optional(), + redirect_uri: z.string().optional(), + client_id: z.string().optional(), + code_verifier: z.string().optional(), + refresh_token: z.string().optional(), + scope: z.string().optional(), + resource: z.string().optional(), + }) + .meta({ id: 'OAuthTokenRequest' }); + +export const tokenResponseSchema = z + .object({ + access_token: z.string(), + token_type: z.literal('Bearer'), + expires_in: z.number().int(), + refresh_token: z.string(), + scope: z.string(), + }) + .meta({ id: 'OAuthTokenResponse' }); + +export const oauthErrorSchema = z + .object({ + error: z.string(), + error_description: z.string().optional(), + }) + .meta({ id: 'OAuthErrorResponse' }); + +export const revocationRequestSchema = z + .object({ + token: z.string(), + token_type_hint: z.string().optional(), + }) + .meta({ id: 'OAuthRevocationRequest' }); + +export const clientRegistrationRequestSchema = z + .object({ + redirect_uris: z.array(z.string().max(2048)).min(1).max(20), + client_name: z.string().trim().min(1).max(255), + client_uri: z.string().max(2048).optional(), + logo_uri: z.string().max(2048).optional(), + token_endpoint_auth_method: z.literal('none').optional(), + grant_types: z.array(z.string()).optional(), + response_types: z.array(z.string()).optional(), + application_type: z.enum(['web', 'native']).optional(), + scope: z.string().max(500).optional(), + software_id: z.string().max(255).optional(), + software_version: z.string().max(255).optional(), + }) + .meta({ id: 'OAuthClientRegistrationRequest' }); + +export const clientRegistrationResponseSchema = clientRegistrationRequestSchema + .extend({ + client_id: z.string(), + client_id_issued_at: z.number().int(), + token_endpoint_auth_method: z.literal('none'), + grant_types: z.array(z.string()), + response_types: z.array(z.string()), + }) + .meta({ id: 'OAuthClientRegistrationResponse' }); diff --git a/src/app/api/oauth/token/contract.ts b/src/app/api/oauth/token/contract.ts new file mode 100644 index 000000000..4738b9cf3 --- /dev/null +++ b/src/app/api/oauth/token/contract.ts @@ -0,0 +1,31 @@ +import { defineOperation } from '@/openapi/operation'; +import { jsonResponse } from '@/openapi/schemas'; +import { oauthErrorSchema, tokenRequestSchema, tokenResponseSchema } from '../schema'; + +const tokenOperation = defineOperation({ + method: 'post', + path: '/api/oauth/token', + audience: 'internal', + auth: 'none', + operation: { + operationId: 'oauthToken', + summary: 'OAuth 2.1 token endpoint', + description: + 'Exchanges an authorization code (with PKCE code_verifier) or a refresh token for an access token. Accepts application/x-www-form-urlencoded or JSON. Refresh tokens are rotated on every use.', + tags: ['OAuth'], + requestBody: { + required: true, + content: { + 'application/x-www-form-urlencoded': { schema: tokenRequestSchema }, + 'application/json': { schema: tokenRequestSchema }, + }, + }, + responses: { + '200': jsonResponse(tokenResponseSchema, 'Access and refresh tokens.'), + '400': jsonResponse(oauthErrorSchema, 'Invalid grant or request.'), + '401': jsonResponse(oauthErrorSchema, 'Invalid client.'), + }, + }, +}); + +export const operations = [tokenOperation] as const; diff --git a/src/app/api/oauth/token/route.ts b/src/app/api/oauth/token/route.ts new file mode 100644 index 000000000..8513ec52a --- /dev/null +++ b/src/app/api/oauth/token/route.ts @@ -0,0 +1,26 @@ +import { getIssuer, isOAuthEnabled } from '@/lib/oauth/config'; +import { oauthErrorResponse, oauthJson } from '@/lib/oauth/errors'; +import { readOAuthBody } from '@/lib/oauth/form'; +import { corsPreflight } from '@/lib/oauth/metadata'; +import { handleTokenRequest } from '@/lib/oauth/token'; +import { notFound } from '@/lib/response'; + +export function OPTIONS() { + return corsPreflight(); +} + +/** OAuth 2.1 token endpoint: authorization_code (PKCE) and refresh_token grants. */ +export async function POST(request: Request) { + if (!isOAuthEnabled()) { + return notFound(); + } + + try { + const params = await readOAuthBody(request); + const tokens = await handleTokenRequest(params, getIssuer(request.headers)); + + return oauthJson(tokens); + } catch (error) { + return oauthErrorResponse(error); + } +} diff --git a/src/app/api/pixels/[pixelId]/contract.generated.ts b/src/app/api/pixels/[pixelId]/contract.generated.ts index 5a58327d1..30bbe44bb 100644 --- a/src/app/api/pixels/[pixelId]/contract.generated.ts +++ b/src/app/api/pixels/[pixelId]/contract.generated.ts @@ -9,7 +9,7 @@ const operation1 = defineOperation({ audience: 'public', auth: 'bearer', operation: { - operationId: 'deletePixelsPixelId', + operationId: 'deletePixel', summary: 'Delete pixels pixel id', tags: ['Pixels'], parameters: [ @@ -66,7 +66,7 @@ const operation2 = defineOperation({ audience: 'public', auth: 'bearer-or-share', operation: { - operationId: 'getPixelsPixelId', + operationId: 'getPixel', summary: 'Get pixels pixel id', tags: ['Pixels'], parameters: [ @@ -156,7 +156,7 @@ const operation3 = defineOperation({ audience: 'public', auth: 'bearer', operation: { - operationId: 'postPixelsPixelId', + operationId: 'updatePixel', summary: 'Create or update pixels pixel id', tags: ['Pixels'], parameters: [ diff --git a/src/app/api/pixels/[pixelId]/shares/contract.generated.ts b/src/app/api/pixels/[pixelId]/shares/contract.generated.ts index acb8b5d1c..654029865 100644 --- a/src/app/api/pixels/[pixelId]/shares/contract.generated.ts +++ b/src/app/api/pixels/[pixelId]/shares/contract.generated.ts @@ -9,7 +9,7 @@ const operation1 = defineOperation({ audience: 'public', auth: 'bearer-or-share', operation: { - operationId: 'getPixelsPixelIdShares', + operationId: 'getPixelShares', summary: 'Get pixels pixel id shares', tags: ['Pixels'], parameters: [ @@ -428,7 +428,7 @@ const operation2 = defineOperation({ audience: 'public', auth: 'bearer', operation: { - operationId: 'postPixelsPixelIdShares', + operationId: 'createPixelShare', summary: 'Create or update pixels pixel id shares', tags: ['Pixels'], parameters: [ diff --git a/src/app/api/pixels/contract.generated.ts b/src/app/api/pixels/contract.generated.ts index 0e7fe26c7..abefc1128 100644 --- a/src/app/api/pixels/contract.generated.ts +++ b/src/app/api/pixels/contract.generated.ts @@ -190,7 +190,7 @@ const operation2 = defineOperation({ audience: 'public', auth: 'bearer', operation: { - operationId: 'postPixels', + operationId: 'createPixel', summary: 'Create or update pixels', tags: ['Pixels'], requestBody: { diff --git a/src/app/api/realtime/[websiteId]/contract.generated.ts b/src/app/api/realtime/[websiteId]/contract.generated.ts index 4082a94eb..7fc0ea81b 100644 --- a/src/app/api/realtime/[websiteId]/contract.generated.ts +++ b/src/app/api/realtime/[websiteId]/contract.generated.ts @@ -9,9 +9,10 @@ const operation1 = defineOperation({ audience: 'public', auth: 'bearer-or-share', operation: { - operationId: 'getRealtimeWebsiteId', + operationId: 'getRealtime', summary: 'Get realtime website id', tags: ['Realtime'], + 'x-umami-oauth-scope': 'analytics:read', parameters: [ { name: 'websiteId', diff --git a/src/app/api/record/contract.generated.ts b/src/app/api/record/contract.generated.ts index 0aa693d2b..c7fad59d6 100644 --- a/src/app/api/record/contract.generated.ts +++ b/src/app/api/record/contract.generated.ts @@ -9,7 +9,7 @@ const operation1 = defineOperation({ audience: 'collect', auth: 'none', operation: { - operationId: 'postRecord', + operationId: 'record', summary: 'Create or update record', tags: ['Collection'], requestBody: { diff --git a/src/app/api/reports/[reportId]/contract.generated.ts b/src/app/api/reports/[reportId]/contract.generated.ts index 6e567c3ca..c3ee5bf6a 100644 --- a/src/app/api/reports/[reportId]/contract.generated.ts +++ b/src/app/api/reports/[reportId]/contract.generated.ts @@ -9,7 +9,7 @@ const operation1 = defineOperation({ audience: 'public', auth: 'bearer', operation: { - operationId: 'deleteReportsReportId', + operationId: 'deleteReport', summary: 'Delete reports report id', tags: ['Reports'], parameters: [ @@ -83,7 +83,7 @@ const operation2 = defineOperation({ audience: 'public', auth: 'bearer-or-share', operation: { - operationId: 'getReportsReportId', + operationId: 'getReport', summary: 'Get reports report id', tags: ['Reports'], parameters: [ @@ -245,7 +245,7 @@ const operation3 = defineOperation({ audience: 'public', auth: 'bearer', operation: { - operationId: 'postReportsReportId', + operationId: 'updateReport', summary: 'Create or update reports report id', tags: ['Reports'], parameters: [ diff --git a/src/app/api/reports/attribution/contract.generated.ts b/src/app/api/reports/attribution/contract.generated.ts index d84884443..9cb5559f2 100644 --- a/src/app/api/reports/attribution/contract.generated.ts +++ b/src/app/api/reports/attribution/contract.generated.ts @@ -9,9 +9,10 @@ const operation1 = defineOperation({ audience: 'public', auth: 'bearer-or-share', operation: { - operationId: 'postReportsAttribution', + operationId: 'runAttributionReport', summary: 'Create or update reports attribution', tags: ['Reports'], + 'x-umami-oauth-scope': 'analytics:read', requestBody: { required: true, content: { diff --git a/src/app/api/reports/breakdown/contract.generated.ts b/src/app/api/reports/breakdown/contract.generated.ts index 1881e47bd..80e81bce0 100644 --- a/src/app/api/reports/breakdown/contract.generated.ts +++ b/src/app/api/reports/breakdown/contract.generated.ts @@ -9,9 +9,10 @@ const operation1 = defineOperation({ audience: 'public', auth: 'bearer-or-share', operation: { - operationId: 'postReportsBreakdown', + operationId: 'runBreakdownReport', summary: 'Create or update reports breakdown', tags: ['Reports'], + 'x-umami-oauth-scope': 'analytics:read', requestBody: { required: true, content: { diff --git a/src/app/api/reports/contract.generated.ts b/src/app/api/reports/contract.generated.ts index 1596bf526..98997777f 100644 --- a/src/app/api/reports/contract.generated.ts +++ b/src/app/api/reports/contract.generated.ts @@ -258,7 +258,7 @@ const operation2 = defineOperation({ audience: 'public', auth: 'bearer', operation: { - operationId: 'postReports', + operationId: 'createReport', summary: 'Create or update reports', tags: ['Reports'], requestBody: { diff --git a/src/app/api/reports/funnel/contract.generated.ts b/src/app/api/reports/funnel/contract.generated.ts index 1810e2ad4..d0ef96987 100644 --- a/src/app/api/reports/funnel/contract.generated.ts +++ b/src/app/api/reports/funnel/contract.generated.ts @@ -9,9 +9,10 @@ const operation1 = defineOperation({ audience: 'public', auth: 'bearer-or-share', operation: { - operationId: 'postReportsFunnel', + operationId: 'runFunnelReport', summary: 'Create or update reports funnel', tags: ['Reports'], + 'x-umami-oauth-scope': 'analytics:read', requestBody: { required: true, content: { diff --git a/src/app/api/reports/goal/contract.generated.ts b/src/app/api/reports/goal/contract.generated.ts index 2d5911d88..f0fd8532d 100644 --- a/src/app/api/reports/goal/contract.generated.ts +++ b/src/app/api/reports/goal/contract.generated.ts @@ -9,7 +9,7 @@ const operation1 = defineOperation({ audience: 'public', auth: 'bearer-or-share', operation: { - operationId: 'postReportsGoal', + operationId: 'runGoalReport', summary: 'Create or update reports goal', tags: ['Reports'], requestBody: { diff --git a/src/app/api/reports/heatmap/contract.generated.ts b/src/app/api/reports/heatmap/contract.generated.ts index 4c3415b9b..5bb363169 100644 --- a/src/app/api/reports/heatmap/contract.generated.ts +++ b/src/app/api/reports/heatmap/contract.generated.ts @@ -9,7 +9,7 @@ const operation1 = defineOperation({ audience: 'public', auth: 'bearer-or-share', operation: { - operationId: 'postReportsHeatmap', + operationId: 'runHeatmapReport', summary: 'Create or update reports heatmap', tags: ['Reports'], requestBody: { diff --git a/src/app/api/reports/journey/contract.generated.ts b/src/app/api/reports/journey/contract.generated.ts index e9464c1de..859b098fc 100644 --- a/src/app/api/reports/journey/contract.generated.ts +++ b/src/app/api/reports/journey/contract.generated.ts @@ -9,9 +9,10 @@ const operation1 = defineOperation({ audience: 'public', auth: 'bearer-or-share', operation: { - operationId: 'postReportsJourney', + operationId: 'runJourneyReport', summary: 'Create or update reports journey', tags: ['Reports'], + 'x-umami-oauth-scope': 'analytics:read', requestBody: { required: true, content: { diff --git a/src/app/api/reports/performance/contract.generated.ts b/src/app/api/reports/performance/contract.generated.ts index 7d1b468f1..acd5bf742 100644 --- a/src/app/api/reports/performance/contract.generated.ts +++ b/src/app/api/reports/performance/contract.generated.ts @@ -9,7 +9,7 @@ const operation1 = defineOperation({ audience: 'public', auth: 'bearer-or-share', operation: { - operationId: 'postReportsPerformance', + operationId: 'runPerformanceReport', summary: 'Create or update reports performance', tags: ['Reports'], requestBody: { diff --git a/src/app/api/reports/retention/contract.generated.ts b/src/app/api/reports/retention/contract.generated.ts index a96f20d0d..356e8bc04 100644 --- a/src/app/api/reports/retention/contract.generated.ts +++ b/src/app/api/reports/retention/contract.generated.ts @@ -9,9 +9,10 @@ const operation1 = defineOperation({ audience: 'public', auth: 'bearer-or-share', operation: { - operationId: 'postReportsRetention', + operationId: 'runRetentionReport', summary: 'Create or update reports retention', tags: ['Reports'], + 'x-umami-oauth-scope': 'analytics:read', requestBody: { required: true, content: { diff --git a/src/app/api/reports/revenue/contract.generated.ts b/src/app/api/reports/revenue/contract.generated.ts index f3eb789ed..b66d45e25 100644 --- a/src/app/api/reports/revenue/contract.generated.ts +++ b/src/app/api/reports/revenue/contract.generated.ts @@ -9,9 +9,10 @@ const operation1 = defineOperation({ audience: 'public', auth: 'bearer-or-share', operation: { - operationId: 'postReportsRevenue', + operationId: 'runRevenueReport', summary: 'Create or update reports revenue', tags: ['Reports'], + 'x-umami-oauth-scope': 'analytics:read', requestBody: { required: true, content: { diff --git a/src/app/api/reports/utm/contract.generated.ts b/src/app/api/reports/utm/contract.generated.ts index df8271678..2b2c1c1b4 100644 --- a/src/app/api/reports/utm/contract.generated.ts +++ b/src/app/api/reports/utm/contract.generated.ts @@ -9,9 +9,10 @@ const operation1 = defineOperation({ audience: 'public', auth: 'bearer-or-share', operation: { - operationId: 'postReportsUtm', + operationId: 'runUtmReport', summary: 'Create or update reports utm', tags: ['Reports'], + 'x-umami-oauth-scope': 'analytics:read', requestBody: { required: true, content: { diff --git a/src/app/api/send/contract.generated.ts b/src/app/api/send/contract.generated.ts index fae0a731c..9abec8e3e 100644 --- a/src/app/api/send/contract.generated.ts +++ b/src/app/api/send/contract.generated.ts @@ -9,7 +9,7 @@ const operation1 = defineOperation({ audience: 'collect', auth: 'none', operation: { - operationId: 'postSend', + operationId: 'send', summary: 'Create or update send', tags: ['Collection'], requestBody: { diff --git a/src/app/api/share/[slug]/contract.generated.ts b/src/app/api/share/[slug]/contract.generated.ts index d194ac889..a1c1ed754 100644 --- a/src/app/api/share/[slug]/contract.generated.ts +++ b/src/app/api/share/[slug]/contract.generated.ts @@ -9,7 +9,7 @@ const operation1 = defineOperation({ audience: 'public', auth: 'none', operation: { - operationId: 'getShareSlug', + operationId: 'getShareBySlug', summary: 'Get share slug', tags: ['Shares'], parameters: [ diff --git a/src/app/api/share/contract.generated.ts b/src/app/api/share/contract.generated.ts index 1881ab89a..92306b37f 100644 --- a/src/app/api/share/contract.generated.ts +++ b/src/app/api/share/contract.generated.ts @@ -9,7 +9,7 @@ const operation1 = defineOperation({ audience: 'public', auth: 'bearer', operation: { - operationId: 'postShare', + operationId: 'createShare', summary: 'Create or update share', tags: ['Shares'], requestBody: { diff --git a/src/app/api/share/id/[shareId]/contract.generated.ts b/src/app/api/share/id/[shareId]/contract.generated.ts index 5c6c80b35..0be07ad1b 100644 --- a/src/app/api/share/id/[shareId]/contract.generated.ts +++ b/src/app/api/share/id/[shareId]/contract.generated.ts @@ -9,7 +9,7 @@ const operation1 = defineOperation({ audience: 'public', auth: 'bearer', operation: { - operationId: 'deleteShareIdShareId', + operationId: 'deleteShare', summary: 'Delete share id share id', tags: ['Shares'], parameters: [ @@ -66,7 +66,7 @@ const operation2 = defineOperation({ audience: 'public', auth: 'bearer-or-share', operation: { - operationId: 'getShareIdShareId', + operationId: 'getShare', summary: 'Get share id share id', tags: ['Shares'], parameters: [ @@ -207,7 +207,7 @@ const operation3 = defineOperation({ audience: 'public', auth: 'bearer', operation: { - operationId: 'postShareIdShareId', + operationId: 'updateShare', summary: 'Create or update share id share id', tags: ['Shares'], parameters: [ diff --git a/src/app/api/teams/[teamId]/boards/contract.generated.ts b/src/app/api/teams/[teamId]/boards/contract.generated.ts index 4b2de64d6..763ce60a3 100644 --- a/src/app/api/teams/[teamId]/boards/contract.generated.ts +++ b/src/app/api/teams/[teamId]/boards/contract.generated.ts @@ -9,7 +9,7 @@ const operation1 = defineOperation({ audience: 'public', auth: 'bearer-or-share', operation: { - operationId: 'getTeamsTeamIdBoards', + operationId: 'getTeamBoards', summary: 'Get teams team id boards', tags: ['Teams'], parameters: [ diff --git a/src/app/api/teams/[teamId]/contract.generated.ts b/src/app/api/teams/[teamId]/contract.generated.ts index 3c155d790..d3f482991 100644 --- a/src/app/api/teams/[teamId]/contract.generated.ts +++ b/src/app/api/teams/[teamId]/contract.generated.ts @@ -9,7 +9,7 @@ const operation1 = defineOperation({ audience: 'public', auth: 'bearer', operation: { - operationId: 'deleteTeamsTeamId', + operationId: 'deleteTeam', summary: 'Delete teams team id', tags: ['Teams'], parameters: [ @@ -66,7 +66,7 @@ const operation2 = defineOperation({ audience: 'public', auth: 'bearer-or-share', operation: { - operationId: 'getTeamsTeamId', + operationId: 'getTeam', summary: 'Get teams team id', tags: ['Teams'], parameters: [ @@ -173,7 +173,7 @@ const operation3 = defineOperation({ audience: 'public', auth: 'bearer', operation: { - operationId: 'postTeamsTeamId', + operationId: 'updateTeam', summary: 'Create or update teams team id', tags: ['Teams'], parameters: [ diff --git a/src/app/api/teams/[teamId]/links/contract.generated.ts b/src/app/api/teams/[teamId]/links/contract.generated.ts index 8fa3c7043..5d2f5cd35 100644 --- a/src/app/api/teams/[teamId]/links/contract.generated.ts +++ b/src/app/api/teams/[teamId]/links/contract.generated.ts @@ -9,7 +9,7 @@ const operation1 = defineOperation({ audience: 'public', auth: 'bearer-or-share', operation: { - operationId: 'getTeamsTeamIdLinks', + operationId: 'getTeamLinks', summary: 'Get teams team id links', tags: ['Teams'], parameters: [ diff --git a/src/app/api/teams/[teamId]/pixels/contract.generated.ts b/src/app/api/teams/[teamId]/pixels/contract.generated.ts index 9a5e8bb22..4feae12d0 100644 --- a/src/app/api/teams/[teamId]/pixels/contract.generated.ts +++ b/src/app/api/teams/[teamId]/pixels/contract.generated.ts @@ -9,7 +9,7 @@ const operation1 = defineOperation({ audience: 'public', auth: 'bearer-or-share', operation: { - operationId: 'getTeamsTeamIdPixels', + operationId: 'getTeamPixels', summary: 'Get teams team id pixels', tags: ['Teams'], parameters: [ diff --git a/src/app/api/teams/[teamId]/users/[userId]/contract.generated.ts b/src/app/api/teams/[teamId]/users/[userId]/contract.generated.ts index 8d5549473..47b4ff6a6 100644 --- a/src/app/api/teams/[teamId]/users/[userId]/contract.generated.ts +++ b/src/app/api/teams/[teamId]/users/[userId]/contract.generated.ts @@ -9,7 +9,7 @@ const operation1 = defineOperation({ audience: 'public', auth: 'bearer', operation: { - operationId: 'deleteTeamsTeamIdUsersUserId', + operationId: 'deleteTeamUser', summary: 'Delete teams team id users user id', tags: ['Teams'], parameters: [ @@ -91,7 +91,7 @@ const operation2 = defineOperation({ audience: 'public', auth: 'bearer', operation: { - operationId: 'getTeamsTeamIdUsersUserId', + operationId: 'getTeamUser', summary: 'Get teams team id users user id', tags: ['Teams'], parameters: [ @@ -173,7 +173,7 @@ const operation3 = defineOperation({ audience: 'public', auth: 'bearer', operation: { - operationId: 'postTeamsTeamIdUsersUserId', + operationId: 'updateTeamUser', summary: 'Create or update teams team id users user id', tags: ['Teams'], parameters: [ diff --git a/src/app/api/teams/[teamId]/users/contract.generated.ts b/src/app/api/teams/[teamId]/users/contract.generated.ts index 683be6702..9c83c0921 100644 --- a/src/app/api/teams/[teamId]/users/contract.generated.ts +++ b/src/app/api/teams/[teamId]/users/contract.generated.ts @@ -9,7 +9,7 @@ const operation1 = defineOperation({ audience: 'public', auth: 'bearer-or-share', operation: { - operationId: 'getTeamsTeamIdUsers', + operationId: 'getTeamUsers', summary: 'Get teams team id users', tags: ['Teams'], parameters: [ @@ -186,7 +186,7 @@ const operation2 = defineOperation({ audience: 'public', auth: 'bearer', operation: { - operationId: 'postTeamsTeamIdUsers', + operationId: 'createTeamUser', summary: 'Create or update teams team id users', tags: ['Teams'], parameters: [ diff --git a/src/app/api/teams/[teamId]/websites/contract.generated.ts b/src/app/api/teams/[teamId]/websites/contract.generated.ts index 8d4ec1f5d..78d8b9fbf 100644 --- a/src/app/api/teams/[teamId]/websites/contract.generated.ts +++ b/src/app/api/teams/[teamId]/websites/contract.generated.ts @@ -9,7 +9,7 @@ const operation1 = defineOperation({ audience: 'public', auth: 'bearer-or-share', operation: { - operationId: 'getTeamsTeamIdWebsites', + operationId: 'getTeamWebsites', summary: 'Get teams team id websites', tags: ['Teams'], parameters: [ diff --git a/src/app/api/teams/contract.generated.ts b/src/app/api/teams/contract.generated.ts index 5456b328c..5e1ca8b4a 100644 --- a/src/app/api/teams/contract.generated.ts +++ b/src/app/api/teams/contract.generated.ts @@ -182,7 +182,7 @@ const operation2 = defineOperation({ audience: 'public', auth: 'bearer', operation: { - operationId: 'postTeams', + operationId: 'createTeam', summary: 'Create or update teams', tags: ['Teams'], requestBody: { diff --git a/src/app/api/teams/join/contract.generated.ts b/src/app/api/teams/join/contract.generated.ts index 24022b9f6..d091d7944 100644 --- a/src/app/api/teams/join/contract.generated.ts +++ b/src/app/api/teams/join/contract.generated.ts @@ -9,7 +9,7 @@ const operation1 = defineOperation({ audience: 'public', auth: 'bearer', operation: { - operationId: 'postTeamsJoin', + operationId: 'joinTeam', summary: 'Create or update teams join', tags: ['Teams'], requestBody: { diff --git a/src/app/api/users/[userId]/contract.generated.ts b/src/app/api/users/[userId]/contract.generated.ts index fdf0b916e..35c0f4315 100644 --- a/src/app/api/users/[userId]/contract.generated.ts +++ b/src/app/api/users/[userId]/contract.generated.ts @@ -9,7 +9,7 @@ const operation1 = defineOperation({ audience: 'public', auth: 'bearer', operation: { - operationId: 'deleteUsersUserId', + operationId: 'deleteUser', summary: 'Delete users user id', tags: ['Users'], parameters: [ @@ -83,7 +83,7 @@ const operation2 = defineOperation({ audience: 'public', auth: 'bearer-or-share', operation: { - operationId: 'getUsersUserId', + operationId: 'getUser', summary: 'Get users user id', tags: ['Users'], parameters: [ @@ -810,6 +810,121 @@ const operation2 = defineOperation({ ], }, }, + oauthAuthorizationCodes: { + type: 'array', + items: { + type: 'object', + properties: { + id: { + type: 'string', + }, + userId: { + type: 'string', + }, + createdAt: { + type: 'string', + format: 'date-time', + }, + codeHash: { + type: 'string', + }, + expiresAt: { + type: 'string', + format: 'date-time', + }, + clientId: { + type: 'string', + }, + redirectUri: { + type: 'string', + }, + scope: { + type: 'string', + }, + resource: { + type: 'string', + }, + codeChallenge: { + type: 'string', + }, + codeChallengeMethod: { + type: 'string', + }, + usedAt: { + type: 'string', + format: 'date-time', + }, + }, + required: [ + 'id', + 'userId', + 'createdAt', + 'codeHash', + 'expiresAt', + 'clientId', + 'redirectUri', + 'scope', + 'resource', + 'codeChallenge', + 'codeChallengeMethod', + 'usedAt', + ], + }, + }, + oauthRefreshTokens: { + type: 'array', + items: { + type: 'object', + properties: { + id: { + type: 'string', + }, + userId: { + type: 'string', + }, + createdAt: { + type: 'string', + format: 'date-time', + }, + expiresAt: { + type: 'string', + format: 'date-time', + }, + lastUsedAt: { + type: 'string', + format: 'date-time', + }, + clientId: { + type: 'string', + }, + scope: { + type: 'string', + }, + resource: { + type: 'string', + }, + tokenHash: { + type: 'string', + }, + revokedAt: { + type: 'string', + format: 'date-time', + }, + }, + required: [ + 'id', + 'userId', + 'createdAt', + 'expiresAt', + 'lastUsedAt', + 'clientId', + 'scope', + 'resource', + 'tokenHash', + 'revokedAt', + ], + }, + }, _count: { type: 'object', properties: { @@ -849,6 +964,12 @@ const operation2 = defineOperation({ apiKeys: { type: 'number', }, + oauthAuthorizationCodes: { + type: 'number', + }, + oauthRefreshTokens: { + type: 'number', + }, }, required: [ 'websites', @@ -863,6 +984,8 @@ const operation2 = defineOperation({ 'twoFactorOtpUseds', 'twoFactorRateLimit', 'apiKeys', + 'oauthAuthorizationCodes', + 'oauthRefreshTokens', ], }, }, @@ -885,6 +1008,8 @@ const operation2 = defineOperation({ 'twoFactorBackupCodes', 'twoFactorOtpUseds', 'apiKeys', + 'oauthAuthorizationCodes', + 'oauthRefreshTokens', '_count', ], }, @@ -918,7 +1043,7 @@ const operation3 = defineOperation({ audience: 'public', auth: 'bearer', operation: { - operationId: 'postUsersUserId', + operationId: 'updateUser', summary: 'Create or update users user id', tags: ['Users'], parameters: [ diff --git a/src/app/api/users/[userId]/teams/contract.generated.ts b/src/app/api/users/[userId]/teams/contract.generated.ts index 94bd9c6c5..d0b828870 100644 --- a/src/app/api/users/[userId]/teams/contract.generated.ts +++ b/src/app/api/users/[userId]/teams/contract.generated.ts @@ -9,7 +9,7 @@ const operation1 = defineOperation({ audience: 'public', auth: 'bearer', operation: { - operationId: 'getUsersUserIdTeams', + operationId: 'getUserTeams', summary: 'Get users user id teams', tags: ['Users'], parameters: [ diff --git a/src/app/api/users/[userId]/websites/contract.generated.ts b/src/app/api/users/[userId]/websites/contract.generated.ts index 2266a11e2..2b34431db 100644 --- a/src/app/api/users/[userId]/websites/contract.generated.ts +++ b/src/app/api/users/[userId]/websites/contract.generated.ts @@ -9,7 +9,7 @@ const operation1 = defineOperation({ audience: 'public', auth: 'bearer', operation: { - operationId: 'getUsersUserIdWebsites', + operationId: 'getUserWebsites', summary: 'Get users user id websites', tags: ['Users'], parameters: [ diff --git a/src/app/api/users/contract.generated.ts b/src/app/api/users/contract.generated.ts index 775a970ed..dbb250ba2 100644 --- a/src/app/api/users/contract.generated.ts +++ b/src/app/api/users/contract.generated.ts @@ -9,7 +9,7 @@ const operation1 = defineOperation({ audience: 'public', auth: 'bearer', operation: { - operationId: 'postUsers', + operationId: 'createUser', summary: 'Create or update users', tags: ['Users'], requestBody: { diff --git a/src/app/api/websites/[websiteId]/active/contract.generated.ts b/src/app/api/websites/[websiteId]/active/contract.generated.ts deleted file mode 100644 index 71afa5ead..000000000 --- a/src/app/api/websites/[websiteId]/active/contract.generated.ts +++ /dev/null @@ -1,63 +0,0 @@ -// Generated by `pnpm openapi:contracts`. Do not edit directly. - -import type { ZodOpenApiOperationObject } from 'zod-openapi'; -import { defineOperation } from '@/openapi/operation'; - -const operation1 = defineOperation({ - method: 'get', - path: '/api/websites/{websiteId}/active', - audience: 'public', - auth: 'bearer-or-share', - operation: { - operationId: 'getWebsitesWebsiteIdActive', - summary: 'Get websites website id active', - tags: ['Websites'], - parameters: [ - { - name: 'websiteId', - in: 'path', - required: true, - schema: { - type: 'string', - }, - }, - ], - responses: { - '200': { - description: 'The operation completed successfully.', - content: { - 'application/json': { - schema: { - type: 'object', - properties: { - visitors: { - type: 'number', - }, - }, - required: ['visitors'], - }, - }, - }, - }, - '401': { - description: 'Unauthorized.', - content: { - 'application/json': { - example: { - error: { - message: 'Unauthorized.', - code: 'unauthorized', - status: 401, - }, - }, - schema: { - $ref: '#/components/schemas/ApiError', - }, - }, - }, - }, - }, - } as ZodOpenApiOperationObject, -}); - -export const operations = [operation1] as const; diff --git a/src/app/api/websites/[websiteId]/active/contract.ts b/src/app/api/websites/[websiteId]/active/contract.ts new file mode 100644 index 000000000..075d431d4 --- /dev/null +++ b/src/app/api/websites/[websiteId]/active/contract.ts @@ -0,0 +1,26 @@ +import { defineOperation } from '@/openapi/operation'; +import { jsonResponse, unauthorizedResponse } from '@/openapi/schemas'; +import { websiteIdPathSchema } from '../../response-schema'; +import { activeVisitorsSchema } from '../analytics-schema'; + +const getWebsiteActiveOperation = defineOperation({ + method: 'get', + path: '/api/websites/{websiteId}/active', + audience: 'public', + auth: 'bearer-or-share', + operation: { + operationId: 'getWebsiteActive', + summary: 'Get current active visitors', + description: 'Returns the number of visitors active on the website in the last few minutes.', + tags: ['Websites'], + requestParams: { + path: websiteIdPathSchema, + }, + responses: { + '200': jsonResponse(activeVisitorsSchema, 'Active visitor count.'), + '401': unauthorizedResponse, + }, + }, +}); + +export const operations = [getWebsiteActiveOperation] as const; diff --git a/src/app/api/websites/[websiteId]/analytics-schema.ts b/src/app/api/websites/[websiteId]/analytics-schema.ts new file mode 100644 index 000000000..989a74433 --- /dev/null +++ b/src/app/api/websites/[websiteId]/analytics-schema.ts @@ -0,0 +1,145 @@ +import { z } from 'zod'; +import { filterParams, pagingParams, searchParams, withDateRange } from '@/lib/schema'; + +/** Shared request/response schemas for the analytics routes under /api/websites/{websiteId}. */ + +export const analyticsQuerySchema = withDateRange({ ...filterParams }).meta({ + id: 'AnalyticsQuery', + description: + 'Date range (startAt/endAt in milliseconds or startDate/endDate ISO) plus optional filters. Filter values match exactly unless prefixed with an operator such as `neq.`, `c.` (contains) or `re.`.', +}); + +export const pagedAnalyticsQuerySchema = withDateRange({ + ...filterParams, + ...pagingParams, + ...searchParams, +}).meta({ id: 'PagedAnalyticsQuery' }); + +export const metricsQuerySchema = withDateRange({ + type: z.string().meta({ + description: + 'Dimension to rank: path, entry, exit, title, query, hostname, referrer, domain, channel, event, tag, browser, os, device, screen, language, country, region, city, distinctId, utmSource, utmMedium, utmCampaign, utmContent, utmTerm.', + }), + limit: z.coerce.number().optional(), + offset: z.coerce.number().optional(), + ...searchParams, + ...filterParams, +}).meta({ id: 'MetricsQuery' }); + +export const websiteStatsValuesSchema = z + .object({ + pageviews: z.number(), + visitors: z.number(), + visits: z.number(), + bounces: z.number(), + totaltime: z.number(), + }) + .meta({ id: 'WebsiteStatsValues' }); + +export const websiteStatsSchema = websiteStatsValuesSchema + .extend({ + comparison: websiteStatsValuesSchema.meta({ + description: 'The same totals for the comparison period (previous period by default).', + }), + }) + .meta({ id: 'WebsiteStats' }); + +export const timeSeriesPointSchema = z + .object({ + x: z.string().meta({ description: 'Bucket start (ISO date/time).' }), + y: z.number().meta({ description: 'Value for the bucket.' }), + }) + .meta({ id: 'TimeSeriesPoint' }); + +const pageviewsSeriesSchema = z.object({ + pageviews: z.array(timeSeriesPointSchema), + sessions: z.array(timeSeriesPointSchema), +}); + +export const websitePageviewsSchema = pageviewsSeriesSchema + .extend({ + startDate: z.iso.datetime().optional(), + endDate: z.iso.datetime().optional(), + compare: pageviewsSeriesSchema + .extend({ + startDate: z.iso.datetime(), + endDate: z.iso.datetime(), + }) + .optional() + .meta({ description: 'Present when `compare` was requested.' }), + }) + .meta({ id: 'WebsitePageviews' }); + +export const metricRowSchema = z + .object({ + x: z.string().nullable().meta({ description: 'Dimension value (e.g. a path or country).' }), + y: z.number().meta({ description: 'Count of views/events or unique visitors.' }), + t: z.string().optional(), + }) + .meta({ id: 'MetricRow' }); + +export const metricRowsSchema = z.array(metricRowSchema).meta({ id: 'MetricRows' }); + +export const activeVisitorsSchema = z + .object({ visitors: z.number().int().nonnegative() }) + .meta({ id: 'ActiveVisitors' }); + +export const websiteSessionSchema = z + .looseObject({ + id: z.uuid(), + websiteId: z.uuid(), + hostname: z.string().nullable(), + browser: z.string().nullable(), + os: z.string().nullable(), + device: z.string().nullable(), + screen: z.string().nullable(), + language: z.string().nullable(), + country: z.string().nullable(), + region: z.string().nullable(), + city: z.string().nullable(), + distinctId: z.string().nullable().optional(), + firstAt: z.iso.datetime(), + lastAt: z.iso.datetime(), + visits: z.number(), + views: z.number(), + events: z.number().optional(), + createdAt: z.iso.datetime(), + }) + .meta({ id: 'WebsiteSession' }); + +export const websiteSessionPageSchema = z + .object({ + data: z.array(websiteSessionSchema), + count: z.number().int().nonnegative(), + page: z.number().int().positive(), + pageSize: z.number().int().nonnegative(), + isCapped: z.boolean().optional(), + }) + .meta({ id: 'WebsiteSessionPage' }); + +export const websiteEventSchema = z + .looseObject({ + id: z.uuid(), + websiteId: z.uuid(), + sessionId: z.uuid(), + createdAt: z.iso.datetime(), + hostname: z.string().nullable().optional(), + urlPath: z.string().nullable().optional(), + urlQuery: z.string().nullable().optional(), + referrerDomain: z.string().nullable().optional(), + pageTitle: z.string().nullable().optional(), + eventType: z.number().int(), + eventName: z.string().nullable().optional(), + distinctId: z.string().nullable().optional(), + }) + .meta({ id: 'WebsiteEvent' }); + +export const websiteEventPageSchema = z + .object({ + data: z.array(websiteEventSchema), + count: z.number().int().nonnegative(), + page: z.number().int().positive(), + pageSize: z.number().int().nonnegative(), + isCapped: z.boolean().optional(), + }) + .meta({ id: 'WebsiteEventPage' }); diff --git a/src/app/api/websites/[websiteId]/annotations/[annotationId]/contract.generated.ts b/src/app/api/websites/[websiteId]/annotations/[annotationId]/contract.generated.ts index 9ded365c6..65d991f2b 100644 --- a/src/app/api/websites/[websiteId]/annotations/[annotationId]/contract.generated.ts +++ b/src/app/api/websites/[websiteId]/annotations/[annotationId]/contract.generated.ts @@ -9,7 +9,7 @@ const operation1 = defineOperation({ audience: 'public', auth: 'bearer', operation: { - operationId: 'deleteWebsitesWebsiteIdAnnotationsAnnotationId', + operationId: 'deleteWebsiteAnnotation', summary: 'Delete websites website id annotations annotation id', tags: ['Websites'], parameters: [ @@ -91,7 +91,7 @@ const operation2 = defineOperation({ audience: 'public', auth: 'bearer-or-share', operation: { - operationId: 'getWebsitesWebsiteIdAnnotationsAnnotationId', + operationId: 'getWebsiteAnnotation', summary: 'Get websites website id annotations annotation id', tags: ['Websites'], parameters: [ @@ -206,7 +206,7 @@ const operation3 = defineOperation({ audience: 'public', auth: 'bearer', operation: { - operationId: 'postWebsitesWebsiteIdAnnotationsAnnotationId', + operationId: 'updateWebsiteAnnotation', summary: 'Create or update websites website id annotations annotation id', tags: ['Websites'], parameters: [ diff --git a/src/app/api/websites/[websiteId]/annotations/contract.generated.ts b/src/app/api/websites/[websiteId]/annotations/contract.generated.ts index 3dabd74b1..b90ef2d4d 100644 --- a/src/app/api/websites/[websiteId]/annotations/contract.generated.ts +++ b/src/app/api/websites/[websiteId]/annotations/contract.generated.ts @@ -9,7 +9,7 @@ const operation1 = defineOperation({ audience: 'public', auth: 'bearer-or-share', operation: { - operationId: 'getWebsitesWebsiteIdAnnotations', + operationId: 'getWebsiteAnnotations', summary: 'Get websites website id annotations', tags: ['Websites'], parameters: [ @@ -197,7 +197,7 @@ const operation2 = defineOperation({ audience: 'public', auth: 'bearer', operation: { - operationId: 'postWebsitesWebsiteIdAnnotations', + operationId: 'createWebsiteAnnotation', summary: 'Create or update websites website id annotations', tags: ['Websites'], parameters: [ diff --git a/src/app/api/websites/[websiteId]/daterange/contract.generated.ts b/src/app/api/websites/[websiteId]/daterange/contract.generated.ts index ce2b52543..ab3654648 100644 --- a/src/app/api/websites/[websiteId]/daterange/contract.generated.ts +++ b/src/app/api/websites/[websiteId]/daterange/contract.generated.ts @@ -9,9 +9,10 @@ const operation1 = defineOperation({ audience: 'public', auth: 'bearer-or-share', operation: { - operationId: 'getWebsitesWebsiteIdDaterange', + operationId: 'getWebsiteDateRange', summary: 'Get websites website id daterange', tags: ['Websites'], + 'x-umami-oauth-scope': 'analytics:read', parameters: [ { name: 'websiteId', diff --git a/src/app/api/websites/[websiteId]/event-data-pivot/array-series/contract.generated.ts b/src/app/api/websites/[websiteId]/event-data-pivot/array-series/contract.generated.ts index b1d457769..a669d6030 100644 --- a/src/app/api/websites/[websiteId]/event-data-pivot/array-series/contract.generated.ts +++ b/src/app/api/websites/[websiteId]/event-data-pivot/array-series/contract.generated.ts @@ -9,7 +9,7 @@ const operation1 = defineOperation({ audience: 'public', auth: 'bearer-or-share', operation: { - operationId: 'getWebsitesWebsiteIdEventDataPivotArraySeries', + operationId: 'getEventDataArraySeries', summary: 'Get websites website id event data pivot array series', tags: ['Websites'], parameters: [ diff --git a/src/app/api/websites/[websiteId]/event-data-pivot/contract.generated.ts b/src/app/api/websites/[websiteId]/event-data-pivot/contract.generated.ts index cc9e5454d..f8db52c1a 100644 --- a/src/app/api/websites/[websiteId]/event-data-pivot/contract.generated.ts +++ b/src/app/api/websites/[websiteId]/event-data-pivot/contract.generated.ts @@ -9,7 +9,7 @@ const operation1 = defineOperation({ audience: 'public', auth: 'bearer-or-share', operation: { - operationId: 'getWebsitesWebsiteIdEventDataPivot', + operationId: 'getEventDataPivot', summary: 'Get websites website id event data pivot', tags: ['Websites'], parameters: [ diff --git a/src/app/api/websites/[websiteId]/event-data-pivot/date-series/contract.generated.ts b/src/app/api/websites/[websiteId]/event-data-pivot/date-series/contract.generated.ts index 78ed535d5..a847a894d 100644 --- a/src/app/api/websites/[websiteId]/event-data-pivot/date-series/contract.generated.ts +++ b/src/app/api/websites/[websiteId]/event-data-pivot/date-series/contract.generated.ts @@ -9,7 +9,7 @@ const operation1 = defineOperation({ audience: 'public', auth: 'bearer-or-share', operation: { - operationId: 'getWebsitesWebsiteIdEventDataPivotDateSeries', + operationId: 'getEventDataDateSeries', summary: 'Get websites website id event data pivot date series', tags: ['Websites'], parameters: [ diff --git a/src/app/api/websites/[websiteId]/event-data-pivot/numeric-series/contract.generated.ts b/src/app/api/websites/[websiteId]/event-data-pivot/numeric-series/contract.generated.ts index 61f38bcf1..f91521677 100644 --- a/src/app/api/websites/[websiteId]/event-data-pivot/numeric-series/contract.generated.ts +++ b/src/app/api/websites/[websiteId]/event-data-pivot/numeric-series/contract.generated.ts @@ -9,7 +9,7 @@ const operation1 = defineOperation({ audience: 'public', auth: 'bearer-or-share', operation: { - operationId: 'getWebsitesWebsiteIdEventDataPivotNumericSeries', + operationId: 'getEventDataNumericSeries', summary: 'Get websites website id event data pivot numeric series', tags: ['Websites'], parameters: [ diff --git a/src/app/api/websites/[websiteId]/event-data-pivot/numeric-stats/contract.generated.ts b/src/app/api/websites/[websiteId]/event-data-pivot/numeric-stats/contract.generated.ts index 1432e2060..e21a0ab45 100644 --- a/src/app/api/websites/[websiteId]/event-data-pivot/numeric-stats/contract.generated.ts +++ b/src/app/api/websites/[websiteId]/event-data-pivot/numeric-stats/contract.generated.ts @@ -9,7 +9,7 @@ const operation1 = defineOperation({ audience: 'public', auth: 'bearer-or-share', operation: { - operationId: 'getWebsitesWebsiteIdEventDataPivotNumericStats', + operationId: 'getEventDataNumericStats', summary: 'Get websites website id event data pivot numeric stats', tags: ['Websites'], parameters: [ diff --git a/src/app/api/websites/[websiteId]/event-data-pivot/property-series/contract.generated.ts b/src/app/api/websites/[websiteId]/event-data-pivot/property-series/contract.generated.ts index c87cd698d..fe131b0ea 100644 --- a/src/app/api/websites/[websiteId]/event-data-pivot/property-series/contract.generated.ts +++ b/src/app/api/websites/[websiteId]/event-data-pivot/property-series/contract.generated.ts @@ -9,7 +9,7 @@ const operation1 = defineOperation({ audience: 'public', auth: 'bearer-or-share', operation: { - operationId: 'getWebsitesWebsiteIdEventDataPivotPropertySeries', + operationId: 'getEventDataPropertySeries', summary: 'Get websites website id event data pivot property series', tags: ['Websites'], parameters: [ diff --git a/src/app/api/websites/[websiteId]/event-data/[eventId]/contract.generated.ts b/src/app/api/websites/[websiteId]/event-data/[eventId]/contract.generated.ts index 3f62fb5db..8d4d15e40 100644 --- a/src/app/api/websites/[websiteId]/event-data/[eventId]/contract.generated.ts +++ b/src/app/api/websites/[websiteId]/event-data/[eventId]/contract.generated.ts @@ -9,7 +9,7 @@ const operation1 = defineOperation({ audience: 'public', auth: 'bearer-or-share', operation: { - operationId: 'getWebsitesWebsiteIdEventDataEventId', + operationId: 'getEventDataById', summary: 'Get websites website id event data event id', tags: ['Websites'], parameters: [ diff --git a/src/app/api/websites/[websiteId]/event-data/contract.generated.ts b/src/app/api/websites/[websiteId]/event-data/contract.generated.ts index 43fc5d2ab..c3abe71c3 100644 --- a/src/app/api/websites/[websiteId]/event-data/contract.generated.ts +++ b/src/app/api/websites/[websiteId]/event-data/contract.generated.ts @@ -9,9 +9,10 @@ const operation1 = defineOperation({ audience: 'public', auth: 'bearer-or-share', operation: { - operationId: 'getWebsitesWebsiteIdEventData', + operationId: 'getEventData', summary: 'Get websites website id event data', tags: ['Websites'], + 'x-umami-oauth-scope': 'analytics:read', parameters: [ { name: 'websiteId', diff --git a/src/app/api/websites/[websiteId]/event-data/events/contract.generated.ts b/src/app/api/websites/[websiteId]/event-data/events/contract.generated.ts index d3a5bff98..75a7ebe75 100644 --- a/src/app/api/websites/[websiteId]/event-data/events/contract.generated.ts +++ b/src/app/api/websites/[websiteId]/event-data/events/contract.generated.ts @@ -9,7 +9,7 @@ const operation1 = defineOperation({ audience: 'public', auth: 'bearer-or-share', operation: { - operationId: 'getWebsitesWebsiteIdEventDataEvents', + operationId: 'getEventDataEvents', summary: 'Get websites website id event data events', tags: ['Websites'], parameters: [ diff --git a/src/app/api/websites/[websiteId]/event-data/fields/contract.generated.ts b/src/app/api/websites/[websiteId]/event-data/fields/contract.generated.ts index cea6e54bf..ef153a52f 100644 --- a/src/app/api/websites/[websiteId]/event-data/fields/contract.generated.ts +++ b/src/app/api/websites/[websiteId]/event-data/fields/contract.generated.ts @@ -9,7 +9,7 @@ const operation1 = defineOperation({ audience: 'public', auth: 'bearer-or-share', operation: { - operationId: 'getWebsitesWebsiteIdEventDataFields', + operationId: 'getEventDataFields', summary: 'Get websites website id event data fields', tags: ['Websites'], parameters: [ diff --git a/src/app/api/websites/[websiteId]/event-data/fields/route.test.ts b/src/app/api/websites/[websiteId]/event-data/fields/route.test.ts index 2d7e42033..3844f03c8 100644 --- a/src/app/api/websites/[websiteId]/event-data/fields/route.test.ts +++ b/src/app/api/websites/[websiteId]/event-data/fields/route.test.ts @@ -41,7 +41,9 @@ test('uses eventName as the explicit event selector for property fields', async parseRequestMock.mockResolvedValue({ auth: {}, query, error: undefined }); canViewWebsiteSectionMock.mockResolvedValue(true); getQueryFiltersMock.mockResolvedValue(filters as any); - getEventDataFieldsMock.mockResolvedValue([{ propertyName: 'currency', dataType: 1, total: 1 }] as any); + getEventDataFieldsMock.mockResolvedValue([ + { propertyName: 'currency', dataType: 1, total: 1 }, + ] as any); const response = await GET( new Request( diff --git a/src/app/api/websites/[websiteId]/event-data/properties/contract.generated.ts b/src/app/api/websites/[websiteId]/event-data/properties/contract.generated.ts index f2d7237a0..3ed48c7c0 100644 --- a/src/app/api/websites/[websiteId]/event-data/properties/contract.generated.ts +++ b/src/app/api/websites/[websiteId]/event-data/properties/contract.generated.ts @@ -9,9 +9,10 @@ const operation1 = defineOperation({ audience: 'public', auth: 'bearer-or-share', operation: { - operationId: 'getWebsitesWebsiteIdEventDataProperties', + operationId: 'getEventDataProperties', summary: 'Get websites website id event data properties', tags: ['Websites'], + 'x-umami-oauth-scope': 'analytics:read', parameters: [ { name: 'websiteId', diff --git a/src/app/api/websites/[websiteId]/event-data/stats/contract.generated.ts b/src/app/api/websites/[websiteId]/event-data/stats/contract.generated.ts index f7d4480c0..ce1e7973c 100644 --- a/src/app/api/websites/[websiteId]/event-data/stats/contract.generated.ts +++ b/src/app/api/websites/[websiteId]/event-data/stats/contract.generated.ts @@ -9,9 +9,10 @@ const operation1 = defineOperation({ audience: 'public', auth: 'bearer-or-share', operation: { - operationId: 'getWebsitesWebsiteIdEventDataStats', + operationId: 'getEventDataStats', summary: 'Get websites website id event data stats', tags: ['Websites'], + 'x-umami-oauth-scope': 'analytics:read', parameters: [ { name: 'websiteId', diff --git a/src/app/api/websites/[websiteId]/event-data/values/contract.generated.ts b/src/app/api/websites/[websiteId]/event-data/values/contract.generated.ts index 32f771fba..2a5cf2182 100644 --- a/src/app/api/websites/[websiteId]/event-data/values/contract.generated.ts +++ b/src/app/api/websites/[websiteId]/event-data/values/contract.generated.ts @@ -9,9 +9,10 @@ const operation1 = defineOperation({ audience: 'public', auth: 'bearer-or-share', operation: { - operationId: 'getWebsitesWebsiteIdEventDataValues', + operationId: 'getEventDataValues', summary: 'Get websites website id event data values', tags: ['Websites'], + 'x-umami-oauth-scope': 'analytics:read', parameters: [ { name: 'websiteId', diff --git a/src/app/api/websites/[websiteId]/events/contract.generated.ts b/src/app/api/websites/[websiteId]/events/contract.generated.ts deleted file mode 100644 index f230ce4a7..000000000 --- a/src/app/api/websites/[websiteId]/events/contract.generated.ts +++ /dev/null @@ -1,490 +0,0 @@ -// Generated by `pnpm openapi:contracts`. Do not edit directly. - -import type { ZodOpenApiOperationObject } from 'zod-openapi'; -import { defineOperation } from '@/openapi/operation'; - -const operation1 = defineOperation({ - method: 'get', - path: '/api/websites/{websiteId}/events', - audience: 'public', - auth: 'bearer-or-share', - operation: { - operationId: 'getWebsitesWebsiteIdEvents', - summary: 'Get websites website id events', - tags: ['Websites'], - parameters: [ - { - name: 'websiteId', - in: 'path', - required: true, - schema: { - type: 'string', - }, - }, - { - name: 'startAt', - in: 'query', - required: false, - schema: { - type: 'number', - }, - }, - { - name: 'endAt', - in: 'query', - required: false, - schema: { - type: 'number', - }, - }, - { - name: 'startDate', - in: 'query', - required: false, - schema: { - type: 'string', - format: 'date-time', - }, - }, - { - name: 'endDate', - in: 'query', - required: false, - schema: { - type: 'string', - format: 'date-time', - }, - }, - { - name: 'timezone', - in: 'query', - required: false, - schema: { - type: 'string', - }, - }, - { - name: 'unit', - in: 'query', - required: false, - schema: { - type: 'string', - }, - }, - { - name: 'compare', - in: 'query', - required: false, - schema: { - type: 'string', - enum: ['prev', 'yoy'], - }, - }, - { - name: 'path', - in: 'query', - required: false, - schema: { - type: 'string', - }, - }, - { - name: 'referrer', - in: 'query', - required: false, - schema: { - type: 'string', - }, - }, - { - name: 'title', - in: 'query', - required: false, - schema: { - type: 'string', - }, - }, - { - name: 'query', - in: 'query', - required: false, - schema: { - type: 'string', - }, - }, - { - name: 'os', - in: 'query', - required: false, - schema: { - type: 'string', - }, - }, - { - name: 'browser', - in: 'query', - required: false, - schema: { - type: 'string', - }, - }, - { - name: 'device', - in: 'query', - required: false, - schema: { - type: 'string', - }, - }, - { - name: 'country', - in: 'query', - required: false, - schema: { - type: 'string', - }, - }, - { - name: 'region', - in: 'query', - required: false, - schema: { - type: 'string', - }, - }, - { - name: 'city', - in: 'query', - required: false, - schema: { - type: 'string', - }, - }, - { - name: 'tag', - in: 'query', - required: false, - schema: { - type: 'string', - }, - }, - { - name: 'hostname', - in: 'query', - required: false, - schema: { - type: 'string', - }, - }, - { - name: 'distinctId', - in: 'query', - required: false, - schema: { - type: 'string', - }, - }, - { - name: 'language', - in: 'query', - required: false, - schema: { - type: 'string', - }, - }, - { - name: 'event', - in: 'query', - required: false, - schema: { - type: 'string', - }, - }, - { - name: 'utmSource', - in: 'query', - required: false, - schema: { - type: 'string', - }, - }, - { - name: 'utmMedium', - in: 'query', - required: false, - schema: { - type: 'string', - }, - }, - { - name: 'utmCampaign', - in: 'query', - required: false, - schema: { - type: 'string', - }, - }, - { - name: 'utmContent', - in: 'query', - required: false, - schema: { - type: 'string', - }, - }, - { - name: 'utmTerm', - in: 'query', - required: false, - schema: { - type: 'string', - }, - }, - { - name: 'excludeBounce', - in: 'query', - required: false, - schema: { - type: 'string', - }, - }, - { - name: 'segment', - in: 'query', - required: false, - schema: { - type: 'string', - format: 'uuid', - }, - }, - { - name: 'cohort', - in: 'query', - required: false, - schema: { - type: 'string', - format: 'uuid', - }, - }, - { - name: 'eventType', - in: 'query', - required: false, - schema: { - type: 'integer', - minimum: 1, - }, - }, - { - name: 'match', - in: 'query', - required: false, - schema: { - type: 'string', - enum: ['all', 'any'], - }, - }, - { - name: 'page', - in: 'query', - required: false, - schema: { - type: 'integer', - minimum: 1, - }, - }, - { - name: 'pageSize', - in: 'query', - required: false, - schema: { - type: 'integer', - minimum: 1, - }, - }, - { - name: 'maxResults', - in: 'query', - required: false, - schema: { - type: 'integer', - minimum: 1, - }, - }, - { - name: 'search', - in: 'query', - required: false, - schema: { - type: 'string', - }, - }, - ], - responses: { - '200': { - description: 'The operation completed successfully.', - content: { - 'application/json': { - schema: { - type: 'object', - properties: { - data: { - type: 'array', - items: { - type: 'object', - properties: { - id: { - type: 'string', - }, - websiteId: { - type: 'string', - }, - sessionId: { - type: 'string', - }, - createdAt: { - anyOf: [ - { - type: 'string', - }, - { - type: 'string', - format: 'date-time', - }, - ], - }, - hostname: { - type: 'string', - }, - urlPath: { - type: 'string', - }, - urlQuery: { - type: 'string', - }, - referrerPath: { - type: 'string', - }, - referrerQuery: { - type: 'string', - }, - referrerDomain: { - type: 'string', - }, - country: { - type: 'string', - }, - city: { - type: 'string', - }, - device: { - type: 'string', - }, - os: { - type: 'string', - }, - browser: { - type: 'string', - }, - pageTitle: { - type: 'string', - }, - eventType: { - type: 'number', - }, - eventName: { - type: 'string', - }, - hasData: { - type: 'boolean', - }, - }, - required: [ - 'id', - 'websiteId', - 'sessionId', - 'createdAt', - 'hostname', - 'urlPath', - 'urlQuery', - 'referrerPath', - 'referrerQuery', - 'referrerDomain', - 'country', - 'city', - 'device', - 'os', - 'browser', - 'pageTitle', - 'eventType', - 'eventName', - 'hasData', - ], - }, - }, - count: { - type: 'number', - }, - page: { - type: 'number', - }, - pageSize: { - type: 'number', - }, - orderBy: { - type: 'string', - }, - sortDescending: { - type: 'boolean', - }, - search: { - type: 'string', - }, - isCapped: { - type: 'boolean', - }, - }, - required: ['data', 'count', 'page', 'pageSize'], - }, - }, - }, - }, - '400': { - description: 'Bad request.', - content: { - 'application/json': { - example: { - error: { - message: 'Bad request.', - code: 'bad-request', - status: 400, - }, - }, - schema: { - $ref: '#/components/schemas/ApiError', - }, - }, - }, - }, - '401': { - description: 'Unauthorized.', - content: { - 'application/json': { - example: { - error: { - message: 'Unauthorized.', - code: 'unauthorized', - status: 401, - }, - }, - schema: { - $ref: '#/components/schemas/ApiError', - }, - }, - }, - }, - }, - } as ZodOpenApiOperationObject, -}); - -export const operations = [operation1] as const; diff --git a/src/app/api/websites/[websiteId]/events/contract.ts b/src/app/api/websites/[websiteId]/events/contract.ts new file mode 100644 index 000000000..8b6a0556a --- /dev/null +++ b/src/app/api/websites/[websiteId]/events/contract.ts @@ -0,0 +1,29 @@ +import { defineOperation } from '@/openapi/operation'; +import { badRequestResponse, jsonResponse, unauthorizedResponse } from '@/openapi/schemas'; +import { websiteIdPathSchema } from '../../response-schema'; +import { pagedAnalyticsQuerySchema, websiteEventPageSchema } from '../analytics-schema'; + +const getWebsiteEventsOperation = defineOperation({ + method: 'get', + path: '/api/websites/{websiteId}/events', + audience: 'public', + auth: 'bearer-or-share', + operation: { + operationId: 'getWebsiteEvents', + summary: 'List tracked events', + description: + 'Returns a page of pageviews and custom events in the date range, newest first. Filter with `event` for a specific event name or `search` for free text.', + tags: ['Websites'], + requestParams: { + path: websiteIdPathSchema, + query: pagedAnalyticsQuerySchema, + }, + responses: { + '200': jsonResponse(websiteEventPageSchema, 'A page of events.'), + '400': badRequestResponse, + '401': unauthorizedResponse, + }, + }, +}); + +export const operations = [getWebsiteEventsOperation] as const; diff --git a/src/app/api/websites/[websiteId]/events/series/contract.generated.ts b/src/app/api/websites/[websiteId]/events/series/contract.generated.ts index 0c923520e..e81634ab3 100644 --- a/src/app/api/websites/[websiteId]/events/series/contract.generated.ts +++ b/src/app/api/websites/[websiteId]/events/series/contract.generated.ts @@ -9,9 +9,10 @@ const operation1 = defineOperation({ audience: 'public', auth: 'bearer-or-share', operation: { - operationId: 'getWebsitesWebsiteIdEventsSeries', + operationId: 'getWebsiteEventSeries', summary: 'Get websites website id events series', tags: ['Websites'], + 'x-umami-oauth-scope': 'analytics:read', parameters: [ { name: 'websiteId', diff --git a/src/app/api/websites/[websiteId]/events/stats/contract.generated.ts b/src/app/api/websites/[websiteId]/events/stats/contract.generated.ts index 688a8348f..a7cdc4406 100644 --- a/src/app/api/websites/[websiteId]/events/stats/contract.generated.ts +++ b/src/app/api/websites/[websiteId]/events/stats/contract.generated.ts @@ -9,9 +9,10 @@ const operation1 = defineOperation({ audience: 'public', auth: 'bearer-or-share', operation: { - operationId: 'getWebsitesWebsiteIdEventsStats', + operationId: 'getWebsiteEventStats', summary: 'Get websites website id events stats', tags: ['Websites'], + 'x-umami-oauth-scope': 'analytics:read', parameters: [ { name: 'websiteId', diff --git a/src/app/api/websites/[websiteId]/export/contract.generated.ts b/src/app/api/websites/[websiteId]/export/contract.generated.ts index 8397aea9f..0829c38d8 100644 --- a/src/app/api/websites/[websiteId]/export/contract.generated.ts +++ b/src/app/api/websites/[websiteId]/export/contract.generated.ts @@ -9,7 +9,7 @@ const operation1 = defineOperation({ audience: 'public', auth: 'bearer-or-share', operation: { - operationId: 'getWebsitesWebsiteIdExport', + operationId: 'exportWebsite', summary: 'Get websites website id export', tags: ['Websites'], parameters: [ diff --git a/src/app/api/websites/[websiteId]/metrics/contract.generated.ts b/src/app/api/websites/[websiteId]/metrics/contract.generated.ts deleted file mode 100644 index cdbe64091..000000000 --- a/src/app/api/websites/[websiteId]/metrics/contract.generated.ts +++ /dev/null @@ -1,384 +0,0 @@ -// Generated by `pnpm openapi:contracts`. Do not edit directly. - -import type { ZodOpenApiOperationObject } from 'zod-openapi'; -import { defineOperation } from '@/openapi/operation'; - -const operation1 = defineOperation({ - method: 'get', - path: '/api/websites/{websiteId}/metrics', - audience: 'public', - auth: 'bearer-or-share', - operation: { - operationId: 'getWebsitesWebsiteIdMetrics', - summary: 'Get websites website id metrics', - tags: ['Websites'], - parameters: [ - { - name: 'websiteId', - in: 'path', - required: true, - schema: { - type: 'string', - }, - }, - { - name: 'startAt', - in: 'query', - required: false, - schema: { - type: 'number', - }, - }, - { - name: 'endAt', - in: 'query', - required: false, - schema: { - type: 'number', - }, - }, - { - name: 'startDate', - in: 'query', - required: false, - schema: { - type: 'string', - format: 'date-time', - }, - }, - { - name: 'endDate', - in: 'query', - required: false, - schema: { - type: 'string', - format: 'date-time', - }, - }, - { - name: 'timezone', - in: 'query', - required: false, - schema: { - type: 'string', - }, - }, - { - name: 'unit', - in: 'query', - required: false, - schema: { - type: 'string', - }, - }, - { - name: 'compare', - in: 'query', - required: false, - schema: { - type: 'string', - enum: ['prev', 'yoy'], - }, - }, - { - name: 'type', - in: 'query', - required: true, - schema: { - type: 'string', - }, - }, - { - name: 'limit', - in: 'query', - required: false, - schema: { - type: 'number', - }, - }, - { - name: 'offset', - in: 'query', - required: false, - schema: { - type: 'number', - }, - }, - { - name: 'search', - in: 'query', - required: false, - schema: { - type: 'string', - }, - }, - { - name: 'path', - in: 'query', - required: false, - schema: { - type: 'string', - }, - }, - { - name: 'referrer', - in: 'query', - required: false, - schema: { - type: 'string', - }, - }, - { - name: 'title', - in: 'query', - required: false, - schema: { - type: 'string', - }, - }, - { - name: 'query', - in: 'query', - required: false, - schema: { - type: 'string', - }, - }, - { - name: 'os', - in: 'query', - required: false, - schema: { - type: 'string', - }, - }, - { - name: 'browser', - in: 'query', - required: false, - schema: { - type: 'string', - }, - }, - { - name: 'device', - in: 'query', - required: false, - schema: { - type: 'string', - }, - }, - { - name: 'country', - in: 'query', - required: false, - schema: { - type: 'string', - }, - }, - { - name: 'region', - in: 'query', - required: false, - schema: { - type: 'string', - }, - }, - { - name: 'city', - in: 'query', - required: false, - schema: { - type: 'string', - }, - }, - { - name: 'tag', - in: 'query', - required: false, - schema: { - type: 'string', - }, - }, - { - name: 'hostname', - in: 'query', - required: false, - schema: { - type: 'string', - }, - }, - { - name: 'distinctId', - in: 'query', - required: false, - schema: { - type: 'string', - }, - }, - { - name: 'language', - in: 'query', - required: false, - schema: { - type: 'string', - }, - }, - { - name: 'event', - in: 'query', - required: false, - schema: { - type: 'string', - }, - }, - { - name: 'utmSource', - in: 'query', - required: false, - schema: { - type: 'string', - }, - }, - { - name: 'utmMedium', - in: 'query', - required: false, - schema: { - type: 'string', - }, - }, - { - name: 'utmCampaign', - in: 'query', - required: false, - schema: { - type: 'string', - }, - }, - { - name: 'utmContent', - in: 'query', - required: false, - schema: { - type: 'string', - }, - }, - { - name: 'utmTerm', - in: 'query', - required: false, - schema: { - type: 'string', - }, - }, - { - name: 'excludeBounce', - in: 'query', - required: false, - schema: { - type: 'string', - }, - }, - { - name: 'segment', - in: 'query', - required: false, - schema: { - type: 'string', - format: 'uuid', - }, - }, - { - name: 'cohort', - in: 'query', - required: false, - schema: { - type: 'string', - format: 'uuid', - }, - }, - { - name: 'eventType', - in: 'query', - required: false, - schema: { - type: 'integer', - minimum: 1, - }, - }, - { - name: 'match', - in: 'query', - required: false, - schema: { - type: 'string', - enum: ['all', 'any'], - }, - }, - ], - responses: { - '200': { - description: 'The operation completed successfully.', - content: { - 'application/json': { - schema: { - type: 'array', - items: { - type: 'object', - properties: { - x: { - type: 'string', - }, - t: { - type: 'string', - }, - y: { - type: 'number', - }, - }, - required: ['x', 't', 'y'], - }, - }, - }, - }, - }, - '400': { - description: 'Bad request.', - content: { - 'application/json': { - example: { - error: { - message: 'Bad request.', - code: 'bad-request', - status: 400, - }, - }, - schema: { - $ref: '#/components/schemas/ApiError', - }, - }, - }, - }, - '401': { - description: 'Unauthorized.', - content: { - 'application/json': { - example: { - error: { - message: 'Unauthorized.', - code: 'unauthorized', - status: 401, - }, - }, - schema: { - $ref: '#/components/schemas/ApiError', - }, - }, - }, - }, - }, - } as ZodOpenApiOperationObject, -}); - -export const operations = [operation1] as const; diff --git a/src/app/api/websites/[websiteId]/metrics/contract.ts b/src/app/api/websites/[websiteId]/metrics/contract.ts new file mode 100644 index 000000000..929a56cee --- /dev/null +++ b/src/app/api/websites/[websiteId]/metrics/contract.ts @@ -0,0 +1,29 @@ +import { defineOperation } from '@/openapi/operation'; +import { badRequestResponse, jsonResponse, unauthorizedResponse } from '@/openapi/schemas'; +import { websiteIdPathSchema } from '../../response-schema'; +import { metricRowsSchema, metricsQuerySchema } from '../analytics-schema'; + +const getWebsiteMetricsOperation = defineOperation({ + method: 'get', + path: '/api/websites/{websiteId}/metrics', + audience: 'public', + auth: 'bearer-or-share', + operation: { + operationId: 'getWebsiteMetrics', + summary: 'Get ranked metrics for a dimension', + description: + 'Returns the top values for one dimension (`type`), such as pages, referrers, countries, browsers, UTM parameters or events, ordered by count. Page-type dimensions count views/events; visitor dimensions count unique visitors.', + tags: ['Websites'], + requestParams: { + path: websiteIdPathSchema, + query: metricsQuerySchema, + }, + responses: { + '200': jsonResponse(metricRowsSchema, 'Ranked rows.'), + '400': badRequestResponse, + '401': unauthorizedResponse, + }, + }, +}); + +export const operations = [getWebsiteMetricsOperation] as const; diff --git a/src/app/api/websites/[websiteId]/metrics/expanded/contract.generated.ts b/src/app/api/websites/[websiteId]/metrics/expanded/contract.generated.ts index 01b868d4a..5a36aacb1 100644 --- a/src/app/api/websites/[websiteId]/metrics/expanded/contract.generated.ts +++ b/src/app/api/websites/[websiteId]/metrics/expanded/contract.generated.ts @@ -9,9 +9,10 @@ const operation1 = defineOperation({ audience: 'public', auth: 'bearer-or-share', operation: { - operationId: 'getWebsitesWebsiteIdMetricsExpanded', + operationId: 'getWebsiteExpandedMetrics', summary: 'Get websites website id metrics expanded', tags: ['Websites'], + 'x-umami-oauth-scope': 'analytics:read', parameters: [ { name: 'websiteId', diff --git a/src/app/api/websites/[websiteId]/pageviews/contract.generated.ts b/src/app/api/websites/[websiteId]/pageviews/contract.generated.ts deleted file mode 100644 index 1b0afdd31..000000000 --- a/src/app/api/websites/[websiteId]/pageviews/contract.generated.ts +++ /dev/null @@ -1,378 +0,0 @@ -// Generated by `pnpm openapi:contracts`. Do not edit directly. - -import type { ZodOpenApiOperationObject } from 'zod-openapi'; -import { defineOperation } from '@/openapi/operation'; - -const operation1 = defineOperation({ - method: 'get', - path: '/api/websites/{websiteId}/pageviews', - audience: 'public', - auth: 'bearer-or-share', - operation: { - operationId: 'getWebsitesWebsiteIdPageviews', - summary: 'Get websites website id pageviews', - tags: ['Websites'], - parameters: [ - { - name: 'websiteId', - in: 'path', - required: true, - schema: { - type: 'string', - }, - }, - { - name: 'startAt', - in: 'query', - required: false, - schema: { - type: 'number', - }, - }, - { - name: 'endAt', - in: 'query', - required: false, - schema: { - type: 'number', - }, - }, - { - name: 'startDate', - in: 'query', - required: false, - schema: { - type: 'string', - format: 'date-time', - }, - }, - { - name: 'endDate', - in: 'query', - required: false, - schema: { - type: 'string', - format: 'date-time', - }, - }, - { - name: 'timezone', - in: 'query', - required: false, - schema: { - type: 'string', - }, - }, - { - name: 'unit', - in: 'query', - required: false, - schema: { - type: 'string', - }, - }, - { - name: 'compare', - in: 'query', - required: false, - schema: { - type: 'string', - enum: ['prev', 'yoy'], - }, - }, - { - name: 'path', - in: 'query', - required: false, - schema: { - type: 'string', - }, - }, - { - name: 'referrer', - in: 'query', - required: false, - schema: { - type: 'string', - }, - }, - { - name: 'title', - in: 'query', - required: false, - schema: { - type: 'string', - }, - }, - { - name: 'query', - in: 'query', - required: false, - schema: { - type: 'string', - }, - }, - { - name: 'os', - in: 'query', - required: false, - schema: { - type: 'string', - }, - }, - { - name: 'browser', - in: 'query', - required: false, - schema: { - type: 'string', - }, - }, - { - name: 'device', - in: 'query', - required: false, - schema: { - type: 'string', - }, - }, - { - name: 'country', - in: 'query', - required: false, - schema: { - type: 'string', - }, - }, - { - name: 'region', - in: 'query', - required: false, - schema: { - type: 'string', - }, - }, - { - name: 'city', - in: 'query', - required: false, - schema: { - type: 'string', - }, - }, - { - name: 'tag', - in: 'query', - required: false, - schema: { - type: 'string', - }, - }, - { - name: 'hostname', - in: 'query', - required: false, - schema: { - type: 'string', - }, - }, - { - name: 'distinctId', - in: 'query', - required: false, - schema: { - type: 'string', - }, - }, - { - name: 'language', - in: 'query', - required: false, - schema: { - type: 'string', - }, - }, - { - name: 'event', - in: 'query', - required: false, - schema: { - type: 'string', - }, - }, - { - name: 'utmSource', - in: 'query', - required: false, - schema: { - type: 'string', - }, - }, - { - name: 'utmMedium', - in: 'query', - required: false, - schema: { - type: 'string', - }, - }, - { - name: 'utmCampaign', - in: 'query', - required: false, - schema: { - type: 'string', - }, - }, - { - name: 'utmContent', - in: 'query', - required: false, - schema: { - type: 'string', - }, - }, - { - name: 'utmTerm', - in: 'query', - required: false, - schema: { - type: 'string', - }, - }, - { - name: 'excludeBounce', - in: 'query', - required: false, - schema: { - type: 'string', - }, - }, - { - name: 'segment', - in: 'query', - required: false, - schema: { - type: 'string', - format: 'uuid', - }, - }, - { - name: 'cohort', - in: 'query', - required: false, - schema: { - type: 'string', - format: 'uuid', - }, - }, - { - name: 'eventType', - in: 'query', - required: false, - schema: { - type: 'integer', - minimum: 1, - }, - }, - { - name: 'match', - in: 'query', - required: false, - schema: { - type: 'string', - enum: ['all', 'any'], - }, - }, - ], - responses: { - '200': { - description: 'The operation completed successfully.', - content: { - 'application/json': { - schema: { - anyOf: [ - { - type: 'object', - properties: { - pageviews: {}, - sessions: {}, - startDate: { - type: 'string', - format: 'date-time', - }, - endDate: { - type: 'string', - format: 'date-time', - }, - compare: { - type: 'object', - properties: { - pageviews: {}, - sessions: {}, - startDate: { - type: 'string', - format: 'date-time', - }, - endDate: { - type: 'string', - format: 'date-time', - }, - }, - required: ['pageviews', 'sessions', 'startDate', 'endDate'], - }, - }, - required: ['pageviews', 'sessions', 'startDate', 'endDate', 'compare'], - }, - { - type: 'object', - properties: { - pageviews: {}, - sessions: {}, - }, - required: ['pageviews', 'sessions'], - }, - ], - }, - }, - }, - }, - '400': { - description: 'Bad request.', - content: { - 'application/json': { - example: { - error: { - message: 'Bad request.', - code: 'bad-request', - status: 400, - }, - }, - schema: { - $ref: '#/components/schemas/ApiError', - }, - }, - }, - }, - '401': { - description: 'Unauthorized.', - content: { - 'application/json': { - example: { - error: { - message: 'Unauthorized.', - code: 'unauthorized', - status: 401, - }, - }, - schema: { - $ref: '#/components/schemas/ApiError', - }, - }, - }, - }, - }, - } as ZodOpenApiOperationObject, -}); - -export const operations = [operation1] as const; diff --git a/src/app/api/websites/[websiteId]/pageviews/contract.ts b/src/app/api/websites/[websiteId]/pageviews/contract.ts new file mode 100644 index 000000000..f92544162 --- /dev/null +++ b/src/app/api/websites/[websiteId]/pageviews/contract.ts @@ -0,0 +1,29 @@ +import { defineOperation } from '@/openapi/operation'; +import { badRequestResponse, jsonResponse, unauthorizedResponse } from '@/openapi/schemas'; +import { websiteIdPathSchema } from '../../response-schema'; +import { analyticsQuerySchema, websitePageviewsSchema } from '../analytics-schema'; + +const getWebsitePageviewsOperation = defineOperation({ + method: 'get', + path: '/api/websites/{websiteId}/pageviews', + audience: 'public', + auth: 'bearer-or-share', + operation: { + operationId: 'getWebsitePageviews', + summary: 'Get pageview and session time series', + description: + 'Returns pageviews and sessions bucketed by `unit` (minute, hour, day, month, year) in the given `timezone`. When `compare` is set the comparison period is included.', + tags: ['Websites'], + requestParams: { + path: websiteIdPathSchema, + query: analyticsQuerySchema, + }, + responses: { + '200': jsonResponse(websitePageviewsSchema, 'Time series.'), + '400': badRequestResponse, + '401': unauthorizedResponse, + }, + }, +}); + +export const operations = [getWebsitePageviewsOperation] as const; diff --git a/src/app/api/websites/[websiteId]/recorder/contract.generated.ts b/src/app/api/websites/[websiteId]/recorder/contract.generated.ts index 1316682c3..157e79eba 100644 --- a/src/app/api/websites/[websiteId]/recorder/contract.generated.ts +++ b/src/app/api/websites/[websiteId]/recorder/contract.generated.ts @@ -9,7 +9,7 @@ const operation1 = defineOperation({ audience: 'collect', auth: 'none', operation: { - operationId: 'getWebsitesWebsiteIdRecorder', + operationId: 'getWebsiteRecorderConfig', summary: 'Get websites website id recorder', tags: ['Websites'], parameters: [ diff --git a/src/app/api/websites/[websiteId]/replays/[replayId]/contract.generated.ts b/src/app/api/websites/[websiteId]/replays/[replayId]/contract.generated.ts index 5b9327342..c82712906 100644 --- a/src/app/api/websites/[websiteId]/replays/[replayId]/contract.generated.ts +++ b/src/app/api/websites/[websiteId]/replays/[replayId]/contract.generated.ts @@ -9,7 +9,7 @@ const operation1 = defineOperation({ audience: 'public', auth: 'bearer-or-share', operation: { - operationId: 'getWebsitesWebsiteIdReplaysReplayId', + operationId: 'getWebsiteReplay', summary: 'Get websites website id replays replay id', tags: ['Websites'], parameters: [ diff --git a/src/app/api/websites/[websiteId]/replays/contract.generated.ts b/src/app/api/websites/[websiteId]/replays/contract.generated.ts index d6ab9b713..ef2491eac 100644 --- a/src/app/api/websites/[websiteId]/replays/contract.generated.ts +++ b/src/app/api/websites/[websiteId]/replays/contract.generated.ts @@ -9,7 +9,7 @@ const operation1 = defineOperation({ audience: 'public', auth: 'bearer-or-share', operation: { - operationId: 'getWebsitesWebsiteIdReplays', + operationId: 'getWebsiteReplays', summary: 'Get websites website id replays', tags: ['Websites'], parameters: [ diff --git a/src/app/api/websites/[websiteId]/replays/route.ts b/src/app/api/websites/[websiteId]/replays/route.ts index 324aa9920..7a2b2efc9 100644 --- a/src/app/api/websites/[websiteId]/replays/route.ts +++ b/src/app/api/websites/[websiteId]/replays/route.ts @@ -1,6 +1,12 @@ import { getQueryFilters, parseRequest } from '@/lib/request'; import { json, unauthorized } from '@/lib/response'; -import { filterParams, pagingParams, replayParams, searchParams, withDateRange } from '@/lib/schema'; +import { + filterParams, + pagingParams, + replayParams, + searchParams, + withDateRange, +} from '@/lib/schema'; import { canViewAuthenticatedWebsite } from '@/permissions'; import { getSessionReplays } from '@/queries/sql'; diff --git a/src/app/api/websites/[websiteId]/replays/saved/[replayId]/contract.generated.ts b/src/app/api/websites/[websiteId]/replays/saved/[replayId]/contract.generated.ts index 38f9a9932..502fa8d2e 100644 --- a/src/app/api/websites/[websiteId]/replays/saved/[replayId]/contract.generated.ts +++ b/src/app/api/websites/[websiteId]/replays/saved/[replayId]/contract.generated.ts @@ -9,7 +9,7 @@ const operation1 = defineOperation({ audience: 'public', auth: 'bearer-or-share', operation: { - operationId: 'getWebsitesWebsiteIdReplaysSavedReplayId', + operationId: 'getWebsiteReplaySaved', summary: 'Get websites website id replays saved replay id', tags: ['Websites'], parameters: [ @@ -74,7 +74,7 @@ const operation2 = defineOperation({ audience: 'public', auth: 'bearer', operation: { - operationId: 'postWebsitesWebsiteIdReplaysSavedReplayId', + operationId: 'saveWebsiteReplay', summary: 'Create or update websites website id replays saved replay id', tags: ['Websites'], parameters: [ diff --git a/src/app/api/websites/[websiteId]/replays/saved/contract.generated.ts b/src/app/api/websites/[websiteId]/replays/saved/contract.generated.ts index 7af9907b5..b401376bb 100644 --- a/src/app/api/websites/[websiteId]/replays/saved/contract.generated.ts +++ b/src/app/api/websites/[websiteId]/replays/saved/contract.generated.ts @@ -9,7 +9,7 @@ const operation1 = defineOperation({ audience: 'public', auth: 'bearer-or-share', operation: { - operationId: 'getWebsitesWebsiteIdReplaysSaved', + operationId: 'getWebsiteSavedReplays', summary: 'Get websites website id replays saved', tags: ['Websites'], parameters: [ diff --git a/src/app/api/websites/[websiteId]/reports/contract.generated.ts b/src/app/api/websites/[websiteId]/reports/contract.generated.ts index 59617859d..8c03eb566 100644 --- a/src/app/api/websites/[websiteId]/reports/contract.generated.ts +++ b/src/app/api/websites/[websiteId]/reports/contract.generated.ts @@ -9,7 +9,7 @@ const operation1 = defineOperation({ audience: 'public', auth: 'bearer-or-share', operation: { - operationId: 'getWebsitesWebsiteIdReports', + operationId: 'getWebsiteReports', summary: 'Get websites website id reports', tags: ['Websites'], parameters: [ diff --git a/src/app/api/websites/[websiteId]/reset/contract.generated.ts b/src/app/api/websites/[websiteId]/reset/contract.generated.ts index 7f2451740..312b8320c 100644 --- a/src/app/api/websites/[websiteId]/reset/contract.generated.ts +++ b/src/app/api/websites/[websiteId]/reset/contract.generated.ts @@ -9,7 +9,7 @@ const operation1 = defineOperation({ audience: 'public', auth: 'bearer', operation: { - operationId: 'postWebsitesWebsiteIdReset', + operationId: 'resetWebsite', summary: 'Create or update websites website id reset', tags: ['Websites'], parameters: [ diff --git a/src/app/api/websites/[websiteId]/revenue/chart/contract.generated.ts b/src/app/api/websites/[websiteId]/revenue/chart/contract.generated.ts index 3a70bf4d3..ebee343cc 100644 --- a/src/app/api/websites/[websiteId]/revenue/chart/contract.generated.ts +++ b/src/app/api/websites/[websiteId]/revenue/chart/contract.generated.ts @@ -9,7 +9,7 @@ const operation1 = defineOperation({ audience: 'public', auth: 'bearer-or-share', operation: { - operationId: 'getWebsitesWebsiteIdRevenueChart', + operationId: 'getWebsiteRevenueChart', summary: 'Get websites website id revenue chart', tags: ['Websites'], parameters: [ diff --git a/src/app/api/websites/[websiteId]/revenue/metrics/contract.generated.ts b/src/app/api/websites/[websiteId]/revenue/metrics/contract.generated.ts index 9947763b2..099a04427 100644 --- a/src/app/api/websites/[websiteId]/revenue/metrics/contract.generated.ts +++ b/src/app/api/websites/[websiteId]/revenue/metrics/contract.generated.ts @@ -9,7 +9,7 @@ const operation1 = defineOperation({ audience: 'public', auth: 'bearer-or-share', operation: { - operationId: 'getWebsitesWebsiteIdRevenueMetrics', + operationId: 'getWebsiteRevenueMetrics', summary: 'Get websites website id revenue metrics', tags: ['Websites'], parameters: [ diff --git a/src/app/api/websites/[websiteId]/revenue/sessions/contract.generated.ts b/src/app/api/websites/[websiteId]/revenue/sessions/contract.generated.ts index e1e44832d..c84eee485 100644 --- a/src/app/api/websites/[websiteId]/revenue/sessions/contract.generated.ts +++ b/src/app/api/websites/[websiteId]/revenue/sessions/contract.generated.ts @@ -9,7 +9,7 @@ const operation1 = defineOperation({ audience: 'public', auth: 'bearer-or-share', operation: { - operationId: 'getWebsitesWebsiteIdRevenueSessions', + operationId: 'getWebsiteRevenueSessions', summary: 'Get websites website id revenue sessions', tags: ['Websites'], parameters: [ diff --git a/src/app/api/websites/[websiteId]/revenue/stats/contract.generated.ts b/src/app/api/websites/[websiteId]/revenue/stats/contract.generated.ts index 2fe78a675..dacd311cc 100644 --- a/src/app/api/websites/[websiteId]/revenue/stats/contract.generated.ts +++ b/src/app/api/websites/[websiteId]/revenue/stats/contract.generated.ts @@ -9,7 +9,7 @@ const operation1 = defineOperation({ audience: 'public', auth: 'bearer-or-share', operation: { - operationId: 'getWebsitesWebsiteIdRevenueStats', + operationId: 'getWebsiteRevenueStats', summary: 'Get websites website id revenue stats', tags: ['Websites'], parameters: [ diff --git a/src/app/api/websites/[websiteId]/segments/[segmentId]/contract.generated.ts b/src/app/api/websites/[websiteId]/segments/[segmentId]/contract.generated.ts index a67318969..3abfcd2b8 100644 --- a/src/app/api/websites/[websiteId]/segments/[segmentId]/contract.generated.ts +++ b/src/app/api/websites/[websiteId]/segments/[segmentId]/contract.generated.ts @@ -9,7 +9,7 @@ const operation1 = defineOperation({ audience: 'public', auth: 'bearer', operation: { - operationId: 'deleteWebsitesWebsiteIdSegmentsSegmentId', + operationId: 'deleteWebsiteSegment', summary: 'Delete websites website id segments segment id', tags: ['Websites'], parameters: [ @@ -91,7 +91,7 @@ const operation2 = defineOperation({ audience: 'public', auth: 'bearer-or-share', operation: { - operationId: 'getWebsitesWebsiteIdSegmentsSegmentId', + operationId: 'getWebsiteSegment', summary: 'Get websites website id segments segment id', tags: ['Websites'], parameters: [ @@ -245,7 +245,7 @@ const operation3 = defineOperation({ audience: 'public', auth: 'bearer', operation: { - operationId: 'postWebsitesWebsiteIdSegmentsSegmentId', + operationId: 'updateWebsiteSegment', summary: 'Create or update websites website id segments segment id', tags: ['Websites'], parameters: [ diff --git a/src/app/api/websites/[websiteId]/segments/contract.generated.ts b/src/app/api/websites/[websiteId]/segments/contract.generated.ts index ec551d5a7..f534755a3 100644 --- a/src/app/api/websites/[websiteId]/segments/contract.generated.ts +++ b/src/app/api/websites/[websiteId]/segments/contract.generated.ts @@ -9,7 +9,7 @@ const operation1 = defineOperation({ audience: 'public', auth: 'bearer-or-share', operation: { - operationId: 'getWebsitesWebsiteIdSegments', + operationId: 'getWebsiteSegments', summary: 'Get websites website id segments', tags: ['Websites'], parameters: [ @@ -210,7 +210,7 @@ const operation2 = defineOperation({ audience: 'public', auth: 'bearer', operation: { - operationId: 'postWebsitesWebsiteIdSegments', + operationId: 'createWebsiteSegment', summary: 'Create or update websites website id segments', tags: ['Websites'], parameters: [ diff --git a/src/app/api/websites/[websiteId]/session-data-pivot/contract.generated.ts b/src/app/api/websites/[websiteId]/session-data-pivot/contract.generated.ts index 2134a2f83..1f68832ad 100644 --- a/src/app/api/websites/[websiteId]/session-data-pivot/contract.generated.ts +++ b/src/app/api/websites/[websiteId]/session-data-pivot/contract.generated.ts @@ -9,7 +9,7 @@ const operation1 = defineOperation({ audience: 'public', auth: 'bearer-or-share', operation: { - operationId: 'getWebsitesWebsiteIdSessionDataPivot', + operationId: 'getSessionDataPivot', summary: 'Get websites website id session data pivot', tags: ['Websites'], parameters: [ diff --git a/src/app/api/websites/[websiteId]/session-data/array-series/contract.generated.ts b/src/app/api/websites/[websiteId]/session-data/array-series/contract.generated.ts index 76afa5175..327e664aa 100644 --- a/src/app/api/websites/[websiteId]/session-data/array-series/contract.generated.ts +++ b/src/app/api/websites/[websiteId]/session-data/array-series/contract.generated.ts @@ -9,7 +9,7 @@ const operation1 = defineOperation({ audience: 'public', auth: 'bearer-or-share', operation: { - operationId: 'getWebsitesWebsiteIdSessionDataArraySeries', + operationId: 'getSessionDataArraySeries', summary: 'Get websites website id session data array series', tags: ['Websites'], parameters: [ diff --git a/src/app/api/websites/[websiteId]/session-data/date-series/contract.generated.ts b/src/app/api/websites/[websiteId]/session-data/date-series/contract.generated.ts index 49aab5a07..2f286fe62 100644 --- a/src/app/api/websites/[websiteId]/session-data/date-series/contract.generated.ts +++ b/src/app/api/websites/[websiteId]/session-data/date-series/contract.generated.ts @@ -9,7 +9,7 @@ const operation1 = defineOperation({ audience: 'public', auth: 'bearer-or-share', operation: { - operationId: 'getWebsitesWebsiteIdSessionDataDateSeries', + operationId: 'getSessionDataDateSeries', summary: 'Get websites website id session data date series', tags: ['Websites'], parameters: [ diff --git a/src/app/api/websites/[websiteId]/session-data/numeric-series/contract.generated.ts b/src/app/api/websites/[websiteId]/session-data/numeric-series/contract.generated.ts index 28c533b07..a2861e557 100644 --- a/src/app/api/websites/[websiteId]/session-data/numeric-series/contract.generated.ts +++ b/src/app/api/websites/[websiteId]/session-data/numeric-series/contract.generated.ts @@ -9,7 +9,7 @@ const operation1 = defineOperation({ audience: 'public', auth: 'bearer-or-share', operation: { - operationId: 'getWebsitesWebsiteIdSessionDataNumericSeries', + operationId: 'getSessionDataNumericSeries', summary: 'Get websites website id session data numeric series', tags: ['Websites'], parameters: [ diff --git a/src/app/api/websites/[websiteId]/session-data/numeric-stats/contract.generated.ts b/src/app/api/websites/[websiteId]/session-data/numeric-stats/contract.generated.ts index 36a63a81b..d66569ac5 100644 --- a/src/app/api/websites/[websiteId]/session-data/numeric-stats/contract.generated.ts +++ b/src/app/api/websites/[websiteId]/session-data/numeric-stats/contract.generated.ts @@ -9,7 +9,7 @@ const operation1 = defineOperation({ audience: 'public', auth: 'bearer-or-share', operation: { - operationId: 'getWebsitesWebsiteIdSessionDataNumericStats', + operationId: 'getSessionDataNumericStats', summary: 'Get websites website id session data numeric stats', tags: ['Websites'], parameters: [ diff --git a/src/app/api/websites/[websiteId]/session-data/properties/contract.generated.ts b/src/app/api/websites/[websiteId]/session-data/properties/contract.generated.ts index 25afc4396..81e909aa7 100644 --- a/src/app/api/websites/[websiteId]/session-data/properties/contract.generated.ts +++ b/src/app/api/websites/[websiteId]/session-data/properties/contract.generated.ts @@ -9,7 +9,7 @@ const operation1 = defineOperation({ audience: 'public', auth: 'bearer-or-share', operation: { - operationId: 'getWebsitesWebsiteIdSessionDataProperties', + operationId: 'getSessionDataProperties', summary: 'Get websites website id session data properties', tags: ['Websites'], parameters: [ diff --git a/src/app/api/websites/[websiteId]/session-data/property-series/contract.generated.ts b/src/app/api/websites/[websiteId]/session-data/property-series/contract.generated.ts index 77c53bd5e..1a02324d3 100644 --- a/src/app/api/websites/[websiteId]/session-data/property-series/contract.generated.ts +++ b/src/app/api/websites/[websiteId]/session-data/property-series/contract.generated.ts @@ -9,7 +9,7 @@ const operation1 = defineOperation({ audience: 'public', auth: 'bearer-or-share', operation: { - operationId: 'getWebsitesWebsiteIdSessionDataPropertySeries', + operationId: 'getSessionDataPropertySeries', summary: 'Get websites website id session data property series', tags: ['Websites'], parameters: [ diff --git a/src/app/api/websites/[websiteId]/session-data/stats/contract.generated.ts b/src/app/api/websites/[websiteId]/session-data/stats/contract.generated.ts index 73917c3f9..aad13780a 100644 --- a/src/app/api/websites/[websiteId]/session-data/stats/contract.generated.ts +++ b/src/app/api/websites/[websiteId]/session-data/stats/contract.generated.ts @@ -9,7 +9,7 @@ const operation1 = defineOperation({ audience: 'public', auth: 'bearer-or-share', operation: { - operationId: 'getWebsitesWebsiteIdSessionDataStats', + operationId: 'getSessionDataStats', summary: 'Get websites website id session data stats', tags: ['Websites'], parameters: [ diff --git a/src/app/api/websites/[websiteId]/session-data/values/contract.generated.ts b/src/app/api/websites/[websiteId]/session-data/values/contract.generated.ts index cf8352214..e1e0beb21 100644 --- a/src/app/api/websites/[websiteId]/session-data/values/contract.generated.ts +++ b/src/app/api/websites/[websiteId]/session-data/values/contract.generated.ts @@ -9,7 +9,7 @@ const operation1 = defineOperation({ audience: 'public', auth: 'bearer-or-share', operation: { - operationId: 'getWebsitesWebsiteIdSessionDataValues', + operationId: 'getSessionDataValues', summary: 'Get websites website id session data values', tags: ['Websites'], parameters: [ diff --git a/src/app/api/websites/[websiteId]/sessions/[sessionId]/activity/contract.generated.ts b/src/app/api/websites/[websiteId]/sessions/[sessionId]/activity/contract.generated.ts index 3d8095bb5..2c4bb3bf1 100644 --- a/src/app/api/websites/[websiteId]/sessions/[sessionId]/activity/contract.generated.ts +++ b/src/app/api/websites/[websiteId]/sessions/[sessionId]/activity/contract.generated.ts @@ -9,9 +9,10 @@ const operation1 = defineOperation({ audience: 'public', auth: 'bearer-or-share', operation: { - operationId: 'getWebsitesWebsiteIdSessionsSessionIdActivity', + operationId: 'getWebsiteSessionActivity', summary: 'Get websites website id sessions session id activity', tags: ['Websites'], + 'x-umami-oauth-scope': 'analytics:read', parameters: [ { name: 'websiteId', diff --git a/src/app/api/websites/[websiteId]/sessions/[sessionId]/contract.generated.ts b/src/app/api/websites/[websiteId]/sessions/[sessionId]/contract.generated.ts index 70c4395b6..dcbd8cfd5 100644 --- a/src/app/api/websites/[websiteId]/sessions/[sessionId]/contract.generated.ts +++ b/src/app/api/websites/[websiteId]/sessions/[sessionId]/contract.generated.ts @@ -9,7 +9,7 @@ const operation1 = defineOperation({ audience: 'public', auth: 'bearer', operation: { - operationId: 'deleteWebsitesWebsiteIdSessionsSessionId', + operationId: 'deleteWebsiteSession', summary: 'Delete websites website id sessions session id', tags: ['Websites'], parameters: [ @@ -108,9 +108,10 @@ const operation2 = defineOperation({ audience: 'public', auth: 'bearer-or-share', operation: { - operationId: 'getWebsitesWebsiteIdSessionsSessionId', + operationId: 'getWebsiteSession', summary: 'Get websites website id sessions session id', tags: ['Websites'], + 'x-umami-oauth-scope': 'analytics:read', parameters: [ { name: 'websiteId', diff --git a/src/app/api/websites/[websiteId]/sessions/[sessionId]/properties/contract.generated.ts b/src/app/api/websites/[websiteId]/sessions/[sessionId]/properties/contract.generated.ts index 0273960cc..8b2f61435 100644 --- a/src/app/api/websites/[websiteId]/sessions/[sessionId]/properties/contract.generated.ts +++ b/src/app/api/websites/[websiteId]/sessions/[sessionId]/properties/contract.generated.ts @@ -9,9 +9,10 @@ const operation1 = defineOperation({ audience: 'public', auth: 'bearer-or-share', operation: { - operationId: 'getWebsitesWebsiteIdSessionsSessionIdProperties', + operationId: 'getWebsiteSessionProperties', summary: 'Get websites website id sessions session id properties', tags: ['Websites'], + 'x-umami-oauth-scope': 'analytics:read', parameters: [ { name: 'websiteId', diff --git a/src/app/api/websites/[websiteId]/sessions/[sessionId]/properties/route.ts b/src/app/api/websites/[websiteId]/sessions/[sessionId]/properties/route.ts index cfbc56c78..c0bdd1812 100644 --- a/src/app/api/websites/[websiteId]/sessions/[sessionId]/properties/route.ts +++ b/src/app/api/websites/[websiteId]/sessions/[sessionId]/properties/route.ts @@ -16,12 +16,7 @@ export async function GET( const { websiteId, sessionId } = await params; if ( - !(await canViewWebsiteSection(auth, websiteId, [ - 'sessions', - 'events', - 'realtime', - 'revenue', - ])) + !(await canViewWebsiteSection(auth, websiteId, ['sessions', 'events', 'realtime', 'revenue'])) ) { return unauthorized(); } diff --git a/src/app/api/websites/[websiteId]/sessions/[sessionId]/replays/contract.generated.ts b/src/app/api/websites/[websiteId]/sessions/[sessionId]/replays/contract.generated.ts index a553dba1a..eb37bc1bf 100644 --- a/src/app/api/websites/[websiteId]/sessions/[sessionId]/replays/contract.generated.ts +++ b/src/app/api/websites/[websiteId]/sessions/[sessionId]/replays/contract.generated.ts @@ -9,7 +9,7 @@ const operation1 = defineOperation({ audience: 'public', auth: 'bearer-or-share', operation: { - operationId: 'getWebsitesWebsiteIdSessionsSessionIdReplays', + operationId: 'getWebsiteSessionReplays', summary: 'Get websites website id sessions session id replays', tags: ['Websites'], parameters: [ diff --git a/src/app/api/websites/[websiteId]/sessions/[sessionId]/route.test.ts b/src/app/api/websites/[websiteId]/sessions/[sessionId]/route.test.ts index 2d5f92ed6..df9d2dc61 100644 --- a/src/app/api/websites/[websiteId]/sessions/[sessionId]/route.test.ts +++ b/src/app/api/websites/[websiteId]/sessions/[sessionId]/route.test.ts @@ -52,8 +52,8 @@ beforeEach(() => { test('GET returns not found when the session does not exist', async () => { parseRequestMock.mockResolvedValue({ auth: {}, error: undefined }); canViewWebsiteSectionMock.mockResolvedValue(true); - isRelationalOnlyMock.mockReturnValue(true); - canDeleteWebsiteMock.mockResolvedValue(false); + isRelationalOnlyMock.mockReturnValue(true); + canDeleteWebsiteMock.mockResolvedValue(false); getWebsiteSessionMock.mockResolvedValue(undefined); const response = await GET( diff --git a/src/app/api/websites/[websiteId]/sessions/contract.generated.ts b/src/app/api/websites/[websiteId]/sessions/contract.generated.ts deleted file mode 100644 index 9aa33b295..000000000 --- a/src/app/api/websites/[websiteId]/sessions/contract.generated.ts +++ /dev/null @@ -1,498 +0,0 @@ -// Generated by `pnpm openapi:contracts`. Do not edit directly. - -import type { ZodOpenApiOperationObject } from 'zod-openapi'; -import { defineOperation } from '@/openapi/operation'; - -const operation1 = defineOperation({ - method: 'get', - path: '/api/websites/{websiteId}/sessions', - audience: 'public', - auth: 'bearer-or-share', - operation: { - operationId: 'getWebsitesWebsiteIdSessions', - summary: 'Get websites website id sessions', - tags: ['Websites'], - parameters: [ - { - name: 'websiteId', - in: 'path', - required: true, - schema: { - type: 'string', - }, - }, - { - name: 'startAt', - in: 'query', - required: false, - schema: { - type: 'number', - }, - }, - { - name: 'endAt', - in: 'query', - required: false, - schema: { - type: 'number', - }, - }, - { - name: 'startDate', - in: 'query', - required: false, - schema: { - type: 'string', - format: 'date-time', - }, - }, - { - name: 'endDate', - in: 'query', - required: false, - schema: { - type: 'string', - format: 'date-time', - }, - }, - { - name: 'timezone', - in: 'query', - required: false, - schema: { - type: 'string', - }, - }, - { - name: 'unit', - in: 'query', - required: false, - schema: { - type: 'string', - }, - }, - { - name: 'compare', - in: 'query', - required: false, - schema: { - type: 'string', - enum: ['prev', 'yoy'], - }, - }, - { - name: 'path', - in: 'query', - required: false, - schema: { - type: 'string', - }, - }, - { - name: 'referrer', - in: 'query', - required: false, - schema: { - type: 'string', - }, - }, - { - name: 'title', - in: 'query', - required: false, - schema: { - type: 'string', - }, - }, - { - name: 'query', - in: 'query', - required: false, - schema: { - type: 'string', - }, - }, - { - name: 'os', - in: 'query', - required: false, - schema: { - type: 'string', - }, - }, - { - name: 'browser', - in: 'query', - required: false, - schema: { - type: 'string', - }, - }, - { - name: 'device', - in: 'query', - required: false, - schema: { - type: 'string', - }, - }, - { - name: 'country', - in: 'query', - required: false, - schema: { - type: 'string', - }, - }, - { - name: 'region', - in: 'query', - required: false, - schema: { - type: 'string', - }, - }, - { - name: 'city', - in: 'query', - required: false, - schema: { - type: 'string', - }, - }, - { - name: 'tag', - in: 'query', - required: false, - schema: { - type: 'string', - }, - }, - { - name: 'hostname', - in: 'query', - required: false, - schema: { - type: 'string', - }, - }, - { - name: 'distinctId', - in: 'query', - required: false, - schema: { - type: 'string', - }, - }, - { - name: 'language', - in: 'query', - required: false, - schema: { - type: 'string', - }, - }, - { - name: 'event', - in: 'query', - required: false, - schema: { - type: 'string', - }, - }, - { - name: 'utmSource', - in: 'query', - required: false, - schema: { - type: 'string', - }, - }, - { - name: 'utmMedium', - in: 'query', - required: false, - schema: { - type: 'string', - }, - }, - { - name: 'utmCampaign', - in: 'query', - required: false, - schema: { - type: 'string', - }, - }, - { - name: 'utmContent', - in: 'query', - required: false, - schema: { - type: 'string', - }, - }, - { - name: 'utmTerm', - in: 'query', - required: false, - schema: { - type: 'string', - }, - }, - { - name: 'excludeBounce', - in: 'query', - required: false, - schema: { - type: 'string', - }, - }, - { - name: 'segment', - in: 'query', - required: false, - schema: { - type: 'string', - format: 'uuid', - }, - }, - { - name: 'cohort', - in: 'query', - required: false, - schema: { - type: 'string', - format: 'uuid', - }, - }, - { - name: 'eventType', - in: 'query', - required: false, - schema: { - type: 'integer', - minimum: 1, - }, - }, - { - name: 'match', - in: 'query', - required: false, - schema: { - type: 'string', - enum: ['all', 'any'], - }, - }, - { - name: 'page', - in: 'query', - required: false, - schema: { - type: 'integer', - minimum: 1, - }, - }, - { - name: 'pageSize', - in: 'query', - required: false, - schema: { - type: 'integer', - minimum: 1, - }, - }, - { - name: 'maxResults', - in: 'query', - required: false, - schema: { - type: 'integer', - minimum: 1, - }, - }, - { - name: 'search', - in: 'query', - required: false, - schema: { - type: 'string', - }, - }, - ], - responses: { - '200': { - description: 'The operation completed successfully.', - content: { - 'application/json': { - schema: { - type: 'object', - properties: { - data: { - type: 'array', - items: { - type: 'object', - properties: { - id: { - type: 'string', - }, - websiteId: { - type: 'string', - }, - hostname: { - type: 'string', - }, - browser: { - type: 'string', - }, - os: { - type: 'string', - }, - device: { - type: 'string', - }, - screen: { - type: 'string', - }, - language: { - type: 'string', - }, - country: { - type: 'string', - }, - region: { - type: 'string', - }, - city: { - type: 'string', - }, - firstAt: { - anyOf: [ - { - type: 'string', - }, - { - type: 'string', - format: 'date-time', - }, - ], - }, - lastAt: { - anyOf: [ - { - type: 'string', - }, - { - type: 'string', - format: 'date-time', - }, - ], - }, - visits: { - type: 'number', - }, - views: { - type: 'number', - }, - events: { - type: 'number', - }, - createdAt: { - anyOf: [ - { - type: 'string', - }, - { - type: 'string', - format: 'date-time', - }, - ], - }, - }, - required: [ - 'id', - 'websiteId', - 'hostname', - 'browser', - 'os', - 'device', - 'screen', - 'language', - 'country', - 'region', - 'city', - 'firstAt', - 'lastAt', - 'visits', - 'views', - 'events', - 'createdAt', - ], - }, - }, - count: { - type: 'number', - }, - page: { - type: 'number', - }, - pageSize: { - type: 'number', - }, - orderBy: { - type: 'string', - }, - sortDescending: { - type: 'boolean', - }, - search: { - type: 'string', - }, - isCapped: { - type: 'boolean', - }, - }, - required: ['data', 'count', 'page', 'pageSize'], - }, - }, - }, - }, - '400': { - description: 'Bad request.', - content: { - 'application/json': { - example: { - error: { - message: 'Bad request.', - code: 'bad-request', - status: 400, - }, - }, - schema: { - $ref: '#/components/schemas/ApiError', - }, - }, - }, - }, - '401': { - description: 'Unauthorized.', - content: { - 'application/json': { - example: { - error: { - message: 'Unauthorized.', - code: 'unauthorized', - status: 401, - }, - }, - schema: { - $ref: '#/components/schemas/ApiError', - }, - }, - }, - }, - }, - } as ZodOpenApiOperationObject, -}); - -export const operations = [operation1] as const; diff --git a/src/app/api/websites/[websiteId]/sessions/contract.ts b/src/app/api/websites/[websiteId]/sessions/contract.ts new file mode 100644 index 000000000..b6a5a67a8 --- /dev/null +++ b/src/app/api/websites/[websiteId]/sessions/contract.ts @@ -0,0 +1,29 @@ +import { defineOperation } from '@/openapi/operation'; +import { badRequestResponse, jsonResponse, unauthorizedResponse } from '@/openapi/schemas'; +import { websiteIdPathSchema } from '../../response-schema'; +import { pagedAnalyticsQuerySchema, websiteSessionPageSchema } from '../analytics-schema'; + +const getWebsiteSessionsOperation = defineOperation({ + method: 'get', + path: '/api/websites/{websiteId}/sessions', + audience: 'public', + auth: 'bearer-or-share', + operation: { + operationId: 'getWebsiteSessions', + summary: 'List visitor sessions', + description: + 'Returns a page of visitor sessions in the date range, newest first. `search` matches distinct ID, city, browser, OS or device.', + tags: ['Websites'], + requestParams: { + path: websiteIdPathSchema, + query: pagedAnalyticsQuerySchema, + }, + responses: { + '200': jsonResponse(websiteSessionPageSchema, 'A page of sessions.'), + '400': badRequestResponse, + '401': unauthorizedResponse, + }, + }, +}); + +export const operations = [getWebsiteSessionsOperation] as const; diff --git a/src/app/api/websites/[websiteId]/sessions/stats/contract.generated.ts b/src/app/api/websites/[websiteId]/sessions/stats/contract.generated.ts index 4a422026d..cead9e3b7 100644 --- a/src/app/api/websites/[websiteId]/sessions/stats/contract.generated.ts +++ b/src/app/api/websites/[websiteId]/sessions/stats/contract.generated.ts @@ -9,9 +9,10 @@ const operation1 = defineOperation({ audience: 'public', auth: 'bearer-or-share', operation: { - operationId: 'getWebsitesWebsiteIdSessionsStats', + operationId: 'getWebsiteSessionStats', summary: 'Get websites website id sessions stats', tags: ['Websites'], + 'x-umami-oauth-scope': 'analytics:read', parameters: [ { name: 'websiteId', diff --git a/src/app/api/websites/[websiteId]/sessions/weekly/contract.generated.ts b/src/app/api/websites/[websiteId]/sessions/weekly/contract.generated.ts index 7613d47c0..ae89eacbb 100644 --- a/src/app/api/websites/[websiteId]/sessions/weekly/contract.generated.ts +++ b/src/app/api/websites/[websiteId]/sessions/weekly/contract.generated.ts @@ -9,7 +9,7 @@ const operation1 = defineOperation({ audience: 'public', auth: 'bearer-or-share', operation: { - operationId: 'getWebsitesWebsiteIdSessionsWeekly', + operationId: 'getWebsiteSessionsWeekly', summary: 'Get websites website id sessions weekly', tags: ['Websites'], parameters: [ diff --git a/src/app/api/websites/[websiteId]/shares/contract.generated.ts b/src/app/api/websites/[websiteId]/shares/contract.generated.ts index 7d4f1350a..aab8a7bd9 100644 --- a/src/app/api/websites/[websiteId]/shares/contract.generated.ts +++ b/src/app/api/websites/[websiteId]/shares/contract.generated.ts @@ -9,7 +9,7 @@ const operation1 = defineOperation({ audience: 'public', auth: 'bearer-or-share', operation: { - operationId: 'getWebsitesWebsiteIdShares', + operationId: 'getWebsiteShares', summary: 'Get websites website id shares', tags: ['Websites'], parameters: [ @@ -428,7 +428,7 @@ const operation2 = defineOperation({ audience: 'public', auth: 'bearer', operation: { - operationId: 'postWebsitesWebsiteIdShares', + operationId: 'createWebsiteShare', summary: 'Create or update websites website id shares', tags: ['Websites'], parameters: [ diff --git a/src/app/api/websites/[websiteId]/stats/contract.generated.ts b/src/app/api/websites/[websiteId]/stats/contract.generated.ts deleted file mode 100644 index 6d30ab32c..000000000 --- a/src/app/api/websites/[websiteId]/stats/contract.generated.ts +++ /dev/null @@ -1,367 +0,0 @@ -// Generated by `pnpm openapi:contracts`. Do not edit directly. - -import type { ZodOpenApiOperationObject } from 'zod-openapi'; -import { defineOperation } from '@/openapi/operation'; - -const operation1 = defineOperation({ - method: 'get', - path: '/api/websites/{websiteId}/stats', - audience: 'public', - auth: 'bearer-or-share', - operation: { - operationId: 'getWebsitesWebsiteIdStats', - summary: 'Get websites website id stats', - tags: ['Websites'], - parameters: [ - { - name: 'websiteId', - in: 'path', - required: true, - schema: { - type: 'string', - }, - }, - { - name: 'startAt', - in: 'query', - required: false, - schema: { - type: 'number', - }, - }, - { - name: 'endAt', - in: 'query', - required: false, - schema: { - type: 'number', - }, - }, - { - name: 'startDate', - in: 'query', - required: false, - schema: { - type: 'string', - format: 'date-time', - }, - }, - { - name: 'endDate', - in: 'query', - required: false, - schema: { - type: 'string', - format: 'date-time', - }, - }, - { - name: 'timezone', - in: 'query', - required: false, - schema: { - type: 'string', - }, - }, - { - name: 'unit', - in: 'query', - required: false, - schema: { - type: 'string', - }, - }, - { - name: 'compare', - in: 'query', - required: false, - schema: { - type: 'string', - enum: ['prev', 'yoy'], - }, - }, - { - name: 'path', - in: 'query', - required: false, - schema: { - type: 'string', - }, - }, - { - name: 'referrer', - in: 'query', - required: false, - schema: { - type: 'string', - }, - }, - { - name: 'title', - in: 'query', - required: false, - schema: { - type: 'string', - }, - }, - { - name: 'query', - in: 'query', - required: false, - schema: { - type: 'string', - }, - }, - { - name: 'os', - in: 'query', - required: false, - schema: { - type: 'string', - }, - }, - { - name: 'browser', - in: 'query', - required: false, - schema: { - type: 'string', - }, - }, - { - name: 'device', - in: 'query', - required: false, - schema: { - type: 'string', - }, - }, - { - name: 'country', - in: 'query', - required: false, - schema: { - type: 'string', - }, - }, - { - name: 'region', - in: 'query', - required: false, - schema: { - type: 'string', - }, - }, - { - name: 'city', - in: 'query', - required: false, - schema: { - type: 'string', - }, - }, - { - name: 'tag', - in: 'query', - required: false, - schema: { - type: 'string', - }, - }, - { - name: 'hostname', - in: 'query', - required: false, - schema: { - type: 'string', - }, - }, - { - name: 'distinctId', - in: 'query', - required: false, - schema: { - type: 'string', - }, - }, - { - name: 'language', - in: 'query', - required: false, - schema: { - type: 'string', - }, - }, - { - name: 'event', - in: 'query', - required: false, - schema: { - type: 'string', - }, - }, - { - name: 'utmSource', - in: 'query', - required: false, - schema: { - type: 'string', - }, - }, - { - name: 'utmMedium', - in: 'query', - required: false, - schema: { - type: 'string', - }, - }, - { - name: 'utmCampaign', - in: 'query', - required: false, - schema: { - type: 'string', - }, - }, - { - name: 'utmContent', - in: 'query', - required: false, - schema: { - type: 'string', - }, - }, - { - name: 'utmTerm', - in: 'query', - required: false, - schema: { - type: 'string', - }, - }, - { - name: 'excludeBounce', - in: 'query', - required: false, - schema: { - type: 'string', - }, - }, - { - name: 'segment', - in: 'query', - required: false, - schema: { - type: 'string', - format: 'uuid', - }, - }, - { - name: 'cohort', - in: 'query', - required: false, - schema: { - type: 'string', - format: 'uuid', - }, - }, - { - name: 'eventType', - in: 'query', - required: false, - schema: { - type: 'integer', - minimum: 1, - }, - }, - { - name: 'match', - in: 'query', - required: false, - schema: { - type: 'string', - enum: ['all', 'any'], - }, - }, - ], - responses: { - '200': { - description: 'The operation completed successfully.', - content: { - 'application/json': { - schema: { - type: 'object', - properties: { - comparison: { - type: 'array', - items: { - type: 'object', - properties: { - pageviews: { - type: 'number', - }, - visitors: { - type: 'number', - }, - visits: { - type: 'number', - }, - bounces: { - type: 'number', - }, - totaltime: { - type: 'number', - }, - }, - required: ['pageviews', 'visitors', 'visits', 'bounces', 'totaltime'], - }, - }, - length: { - type: 'number', - }, - }, - required: ['comparison', 'length'], - }, - }, - }, - }, - '400': { - description: 'Bad request.', - content: { - 'application/json': { - example: { - error: { - message: 'Bad request.', - code: 'bad-request', - status: 400, - }, - }, - schema: { - $ref: '#/components/schemas/ApiError', - }, - }, - }, - }, - '401': { - description: 'Unauthorized.', - content: { - 'application/json': { - example: { - error: { - message: 'Unauthorized.', - code: 'unauthorized', - status: 401, - }, - }, - schema: { - $ref: '#/components/schemas/ApiError', - }, - }, - }, - }, - }, - } as ZodOpenApiOperationObject, -}); - -export const operations = [operation1] as const; diff --git a/src/app/api/websites/[websiteId]/stats/contract.ts b/src/app/api/websites/[websiteId]/stats/contract.ts new file mode 100644 index 000000000..850d10c5e --- /dev/null +++ b/src/app/api/websites/[websiteId]/stats/contract.ts @@ -0,0 +1,29 @@ +import { defineOperation } from '@/openapi/operation'; +import { badRequestResponse, jsonResponse, unauthorizedResponse } from '@/openapi/schemas'; +import { websiteIdPathSchema } from '../../response-schema'; +import { analyticsQuerySchema, websiteStatsSchema } from '../analytics-schema'; + +const getWebsiteStatsOperation = defineOperation({ + method: 'get', + path: '/api/websites/{websiteId}/stats', + audience: 'public', + auth: 'bearer-or-share', + operation: { + operationId: 'getWebsiteStats', + summary: 'Get website summary stats', + description: + 'Returns pageviews, unique visitors, visits, bounces and total time on site for the date range, plus the same totals for the comparison period (`compare`: prev or yoy).', + tags: ['Websites'], + requestParams: { + path: websiteIdPathSchema, + query: analyticsQuerySchema, + }, + responses: { + '200': jsonResponse(websiteStatsSchema, 'Summary stats with comparison.'), + '400': badRequestResponse, + '401': unauthorizedResponse, + }, + }, +}); + +export const operations = [getWebsiteStatsOperation] as const; diff --git a/src/app/api/websites/[websiteId]/transfer/contract.generated.ts b/src/app/api/websites/[websiteId]/transfer/contract.generated.ts index aa5763efc..41e3aa2c3 100644 --- a/src/app/api/websites/[websiteId]/transfer/contract.generated.ts +++ b/src/app/api/websites/[websiteId]/transfer/contract.generated.ts @@ -9,7 +9,7 @@ const operation1 = defineOperation({ audience: 'public', auth: 'bearer', operation: { - operationId: 'postWebsitesWebsiteIdTransfer', + operationId: 'transferWebsite', summary: 'Create or update websites website id transfer', tags: ['Websites'], parameters: [ diff --git a/src/app/api/websites/[websiteId]/values/contract.generated.ts b/src/app/api/websites/[websiteId]/values/contract.generated.ts index 1666bd9f7..ecdb6220f 100644 --- a/src/app/api/websites/[websiteId]/values/contract.generated.ts +++ b/src/app/api/websites/[websiteId]/values/contract.generated.ts @@ -9,7 +9,7 @@ const operation1 = defineOperation({ audience: 'public', auth: 'bearer-or-share', operation: { - operationId: 'getWebsitesWebsiteIdValues', + operationId: 'getWebsiteValues', summary: 'Get websites website id values', tags: ['Websites'], parameters: [ diff --git a/src/app/login/LoginForm.tsx b/src/app/login/LoginForm.tsx index 90da15c56..d529e6f8c 100644 --- a/src/app/login/LoginForm.tsx +++ b/src/app/login/LoginForm.tsx @@ -13,6 +13,7 @@ import { useRouter } from 'next/navigation'; import { useMessages, useUpdateQuery } from '@/components/hooks'; import { Logo } from '@/components/svg'; import { setClientAuthToken } from '@/lib/client'; +import { consumeReturnUrl } from '@/lib/return-url'; import { setUser } from '@/store/app'; export function LoginForm() { @@ -30,7 +31,7 @@ export function LoginForm() { } setClientAuthToken(response.token); setUser(response.user); - router.push('/'); + router.push(consumeReturnUrl() ?? '/'); }, }); }; diff --git a/src/app/login/two-factor/LoginTwoFactorPage.tsx b/src/app/login/two-factor/LoginTwoFactorPage.tsx index d701accb1..0aab6b183 100644 --- a/src/app/login/two-factor/LoginTwoFactorPage.tsx +++ b/src/app/login/two-factor/LoginTwoFactorPage.tsx @@ -17,6 +17,7 @@ import { OtpInput } from '@/components/common/OtpInput'; import { useMessages, useTwoFactorVerifyMutation } from '@/components/hooks'; import { Logo } from '@/components/svg'; import { setClientAuthToken } from '@/lib/client'; +import { consumeReturnUrl } from '@/lib/return-url'; import { setUser } from '@/store/app'; export function LoginTwoFactorPage() { @@ -64,7 +65,7 @@ export function LoginTwoFactorPage() { const data = await mutateAsync({ partialToken, token, backupCode }); setClientAuthToken(data.token); setUser(data.user); - router.push('/'); + router.push(consumeReturnUrl() ?? '/'); } catch (err: any) { if (err.lockedUntil) { setLockUntil(new Date(err.lockedUntil)); diff --git a/src/app/mcp/route.ts b/src/app/mcp/route.ts new file mode 100644 index 000000000..c5fdf284a --- /dev/null +++ b/src/app/mcp/route.ts @@ -0,0 +1,79 @@ +import { UmamiClient } from '@umami/api-client'; +import type { McpLogger } from '@umami/mcp'; +import { createUmamiMcpHttpHandler } from '@umami/mcp'; +import debug from 'debug'; +import { uuid } from '@/lib/crypto'; +import { authenticateMcpRequest, mcpAuthErrorResponse } from '@/lib/mcp/auth'; +import { createInProcessFetch } from '@/lib/mcp/dispatch'; +import { checkMcpRateLimit } from '@/lib/mcp/rate-limit'; +import { getIssuer, isOAuthEnabled } from '@/lib/oauth/config'; +import { corsPreflight } from '@/lib/oauth/metadata'; + +export const dynamic = 'force-dynamic'; + +const log = debug('umami:mcp'); + +const logger: McpLogger = { + info: event => log('%j', event), + error: event => log('%j', event), +}; + +/** + * Remote MCP endpoint (Streamable HTTP, protocol revision 2026-07-28, stateless). + * + * Request path: MCP client → bearer token → @umami/mcp → @umami/api-client → in-process API + * route handlers → existing user/team authorization. MCP never touches storage directly. + */ +const handler = createUmamiMcpHttpHandler({ + logger, + onerror: error => log('handler error: %s', error?.message), + createClient: (authInfo, ctx) => { + const issuer = getIssuer(ctx.requestInfo?.headers); + + return new UmamiClient({ + baseUrl: `${issuer}/api`, + token: authInfo.token, + fetch: createInProcessFetch(), + }); + }, +}); + +async function handle(request: Request) { + if (!isOAuthEnabled() && process.env.MCP_DISABLED === '1') { + return new Response(null, { status: 404 }); + } + + const auth = await authenticateMcpRequest(request); + + if (!auth.ok) { + return mcpAuthErrorResponse(auth, request.headers); + } + + if (!(await checkMcpRateLimit(auth.userId, auth.authInfo.clientId))) { + return Response.json( + { error: 'rate_limited', error_description: 'Too many MCP requests. Slow down.' }, + { status: 429, headers: { 'retry-after': '60' } }, + ); + } + + const requestId = uuid(); + const authInfo = { ...auth.authInfo, extra: { ...auth.authInfo.extra, requestId } }; + + return handler.fetch(request, { authInfo }); +} + +export function OPTIONS() { + return corsPreflight(); +} + +export async function GET(request: Request) { + return handle(request); +} + +export async function POST(request: Request) { + return handle(request); +} + +export async function DELETE(request: Request) { + return handle(request); +} diff --git a/src/app/oauth/authorize/OAuthAuthorizePage.tsx b/src/app/oauth/authorize/OAuthAuthorizePage.tsx new file mode 100644 index 000000000..217fa0fef --- /dev/null +++ b/src/app/oauth/authorize/OAuthAuthorizePage.tsx @@ -0,0 +1,166 @@ +'use client'; +import { Button, Column, Heading, Icon, Loading, Row, Text } from '@umami/react-zen'; +import { usePathname, useSearchParams } from 'next/navigation'; +import { useEffect, useMemo, useState } from 'react'; +import { LoginForm } from '@/app/login/LoginForm'; +import { useApi, useLoginQuery, useMessages } from '@/components/hooks'; +import { Logo } from '@/components/svg'; +import { setReturnUrl } from '@/lib/return-url'; + +interface AuthorizationDetails { + client: { id: string; name: string; uri?: string; logoUri?: string; source: string }; + scopes: { scope: string; description: string }[]; + redirectUri: string; + resource: string; +} + +const SCOPE_MESSAGE_KEYS: Record = { + 'websites:read': 'oauthScopeWebsitesRead', + 'analytics:read': 'oauthScopeAnalyticsRead', +}; + +function getOAuthErrorMessage(error: unknown) { + const record = error as { error_description?: string; message?: string; code?: string } | null; + + return record?.error_description || record?.message || null; +} + +function Frame({ children }: { children: React.ReactNode }) { + return ( + + + + + + umami + {children} + + + ); +} + +export function OAuthAuthorizePage() { + const { t, labels, messages } = useMessages(); + const pathname = usePathname(); + const searchParams = useSearchParams(); + const params = useMemo(() => Object.fromEntries(searchParams.entries()), [searchParams]); + const { user, isLoading } = useLoginQuery(); + const { get, post, useQuery, useMutation } = useApi(); + const [redirecting, setRedirecting] = useState(false); + + useEffect(() => { + if (!isLoading && !user) { + setReturnUrl(`${pathname}?${searchParams.toString()}`); + } + }, [isLoading, user, pathname, searchParams]); + + const details = useQuery({ + queryKey: ['oauth-authorize', params], + queryFn: () => get('/oauth/authorize', params), + enabled: !!user, + retry: false, + }); + + const decide = useMutation({ + mutationFn: (decision: 'approve' | 'deny') => + post('/oauth/authorize', { ...params, decision }) as Promise<{ redirectUrl: string }>, + onSuccess: ({ redirectUrl }) => { + setRedirecting(true); + window.location.assign(redirectUrl); + }, + }); + + if (isLoading) { + return ; + } + + if (!user) { + return ( + + {t(messages.oauthLoginPrompt)} + + + ); + } + + if (details.isLoading) { + return ; + } + + if (details.error || !details.data) { + return ( + + {t(messages.oauthInvalidRequest)} + {getOAuthErrorMessage(details.error) && ( + {getOAuthErrorMessage(details.error)} + )} + + ); + } + + const { client, scopes, redirectUri } = details.data; + const redirectHost = (() => { + try { + return new URL(redirectUri).host || redirectUri; + } catch { + return redirectUri; + } + })(); + const decisionError = getOAuthErrorMessage(decide.error); + + return ( + + + + {t(messages.oauthConsent, { name: client.name })} + + {client.uri && ( + + {client.uri} + + )} + {t(messages.oauthPermissions)} +

    + {scopes.map(({ scope, description }) => ( +
  • + + {SCOPE_MESSAGE_KEYS[scope] ? t(messages[SCOPE_MESSAGE_KEYS[scope]]) : description} + +
  • + ))} +
+ + {t(messages.oauthRedirectNotice, { url: redirectHost })} + + + {t(messages.oauthSignedInAs, { username: user.username })} + + {decisionError && {decisionError}} + + + + + {redirecting && {t(messages.oauthRedirecting)}} + + + ); +} diff --git a/src/app/oauth/authorize/page.tsx b/src/app/oauth/authorize/page.tsx new file mode 100644 index 000000000..fe4b80750 --- /dev/null +++ b/src/app/oauth/authorize/page.tsx @@ -0,0 +1,17 @@ +import type { Metadata } from 'next'; +import { isOAuthEnabled } from '@/lib/oauth/config'; +import { OAuthAuthorizePage } from './OAuthAuthorizePage'; + +export const dynamic = 'force-dynamic'; + +export default async function () { + if (!isOAuthEnabled()) { + return null; + } + + return ; +} + +export const metadata: Metadata = { + title: 'Authorize application', +}; diff --git a/src/components/messages.ts b/src/components/messages.ts index e14f3aeed..3ec52157f 100644 --- a/src/components/messages.ts +++ b/src/components/messages.ts @@ -86,6 +86,8 @@ export const labels: Record = { teamSettings: 'label.team-settings', analytics: 'label.analytics', login: 'label.login', + authorize: 'label.authorize', + deny: 'label.deny', logout: 'label.logout', singleDay: 'label.single-day', dateRange: 'label.date-range', @@ -497,6 +499,15 @@ export const messages: Record = { twoFactorDisableDescription: 'message.two-factor-disable-description', twoFactorRequiredMessage: 'message.two-factor-required-message', twoFactorOverrideNote: 'message.two-factor-override-note', + oauthLoginPrompt: 'message.oauth-login-prompt', + oauthConsent: 'message.oauth-consent', + oauthPermissions: 'message.oauth-permissions', + oauthScopeWebsitesRead: 'message.oauth-scope-websites-read', + oauthScopeAnalyticsRead: 'message.oauth-scope-analytics-read', + oauthRedirectNotice: 'message.oauth-redirect-notice', + oauthSignedInAs: 'message.oauth-signed-in-as', + oauthInvalidRequest: 'message.oauth-invalid-request', + oauthRedirecting: 'message.oauth-redirecting', twoFactorAdminGlobalDescription: 'message.two-factor-admin-global-description', twoFactorRequireUserDescription: 'message.two-factor-require-user-description', twoFactorRequireTeamDescription: 'message.two-factor-require-team-description', diff --git a/src/lib/api-key.ts b/src/lib/api-key.ts index 0840a6b69..a549e536e 100644 --- a/src/lib/api-key.ts +++ b/src/lib/api-key.ts @@ -15,6 +15,7 @@ export const API_KEY_BLOCKED_PATHS = [ '/api/auth', '/api/users', '/api/admin', + '/api/oauth', ]; export function generateApiKey() { diff --git a/src/lib/auth.ts b/src/lib/auth.ts index 1c3bba6c9..4f5923b80 100644 --- a/src/lib/auth.ts +++ b/src/lib/auth.ts @@ -15,6 +15,7 @@ import { } from '@/lib/constants'; import { createAuthKey, hash, secret } from '@/lib/crypto'; import { createSecureToken, parseSecureToken, parseToken } from '@/lib/jwt'; +import { verifyOAuthRequest } from '@/lib/oauth/verify'; import redis from '@/lib/redis'; import { ensureArray } from '@/lib/utils'; import { getApiKeyByHash, updateApiKeyLastUsed } from '@/queries/prisma/apiKey'; @@ -62,6 +63,7 @@ export async function checkApiKeyAuth(request: Request, token: string) { return { token, user, + authType: 'api-key' as const, apiKey: { id: apiKey.id, name: apiKey.name }, }; } @@ -73,6 +75,19 @@ export async function checkAuth(request: Request) { return checkApiKeyAuth(request, token); } + // OAuth access tokens are only honoured on routes that explicitly opted in (see + // src/lib/oauth/scopes.ts) and never fall through to session handling. + const oauth = await verifyOAuthRequest(token, request.method, new URL(request.url).pathname); + + if (oauth.status === 'ok') { + return oauth.auth; + } + + if (oauth.status !== 'not-oauth') { + log('OAuth token rejected', oauth); + return null; + } + const payload = parseSecureToken(token, secret()); const shareToken = await parseShareToken(request); @@ -131,6 +146,7 @@ export async function checkAuth(request: Request) { authKey, shareToken, user, + authType: user ? ('session' as const) : ('share' as const), }; } diff --git a/src/lib/mcp/auth.test.ts b/src/lib/mcp/auth.test.ts new file mode 100644 index 000000000..0b63e79a1 --- /dev/null +++ b/src/lib/mcp/auth.test.ts @@ -0,0 +1,123 @@ +import { beforeEach, describe, expect, test, vi } from 'vitest'; +import { checkApiKeyAuth } from '@/lib/auth'; +import { createAccessToken } from '@/lib/oauth/tokens'; +import { getUser } from '@/queries/prisma/user'; +import { authenticateMcpRequest, mcpAuthErrorResponse } from './auth'; + +vi.mock('@/lib/auth', () => ({ + getBearerToken: (request: Request) => request.headers.get('authorization')?.split(' ')[1], + checkApiKeyAuth: vi.fn(), +})); + +vi.mock('@/queries/prisma/user', () => ({ + getUser: vi.fn(), +})); + +const checkApiKeyAuthMock = vi.mocked(checkApiKeyAuth); +const getUserMock = vi.mocked(getUser); +const ISSUER = 'https://analytics.example.com'; +const USER = { id: 'user-1', username: 'admin', role: 'user', password: 'pw' }; + +function request(token?: string) { + return new Request(`${ISSUER}/mcp`, { + method: 'POST', + headers: { + host: 'analytics.example.com', + 'x-forwarded-proto': 'https', + ...(token ? { authorization: `Bearer ${token}` } : {}), + }, + }); +} + +function accessToken(overrides: Record = {}) { + return createAccessToken({ + userId: USER.id, + clientId: 'https://app.example.com/client.json', + scopes: ['websites:read', 'analytics:read'], + issuer: ISSUER, + resource: `${ISSUER}/mcp`, + passwordHash: USER.password, + ...overrides, + }).token; +} + +beforeEach(() => { + process.env.APP_SECRET = 'test'; + delete process.env.OAUTH_ISSUER; + delete process.env.CLOUD_MODE; + checkApiKeyAuthMock.mockReset(); + getUserMock.mockReset(); + getUserMock.mockResolvedValue({ ...USER } as never); +}); + +describe('authenticateMcpRequest', () => { + test('rejects missing tokens with a resource metadata challenge', async () => { + const result = await authenticateMcpRequest(request()); + + expect(result.ok).toBe(false); + + const response = mcpAuthErrorResponse(result as never, request().headers); + + expect(response.status).toBe(401); + expect(response.headers.get('www-authenticate')).toContain( + `resource_metadata="${ISSUER}/.well-known/oauth-protected-resource/mcp"`, + ); + expect(response.headers.get('www-authenticate')).toContain( + 'scope="websites:read analytics:read"', + ); + }); + + test('accepts OAuth access tokens issued for this resource', async () => { + const result = await authenticateMcpRequest(request(accessToken())); + + expect(result.ok).toBe(true); + + if (result.ok) { + expect(result.userId).toBe(USER.id); + expect(result.authInfo).toMatchObject({ + clientId: 'https://app.example.com/client.json', + scopes: ['analytics:read', 'websites:read'], + }); + expect(result.authInfo.extra).toMatchObject({ userId: USER.id, authType: 'oauth' }); + } + }); + + test('rejects tokens issued for another deployment', async () => { + const foreign = createAccessToken({ + userId: USER.id, + clientId: 'c', + scopes: ['websites:read'], + issuer: 'https://other.example.com', + resource: 'https://other.example.com/mcp', + }).token; + + const result = await authenticateMcpRequest(request(foreign)); + + expect(result).toMatchObject({ ok: false, status: 401, error: 'invalid_token' }); + }); + + test('rejects session tokens and garbage', async () => { + await expect(authenticateMcpRequest(request('not-a-token'))).resolves.toMatchObject({ + ok: false, + status: 401, + }); + }); + + test('accepts self-hosted API keys', async () => { + checkApiKeyAuthMock.mockResolvedValue({ + token: 'umami_key', + user: { id: USER.id, username: 'admin', role: 'user', isAdmin: false }, + authType: 'api-key', + apiKey: { id: 'key-1', name: 'MCP' }, + } as never); + + const result = await authenticateMcpRequest(request('umami_key')); + + expect(result.ok).toBe(true); + + if (result.ok) { + expect(result.authInfo.clientId).toBe('api-key:key-1'); + expect(result.authInfo.scopes).toEqual(['websites:read', 'analytics:read']); + } + }); +}); diff --git a/src/lib/mcp/auth.ts b/src/lib/mcp/auth.ts new file mode 100644 index 000000000..ebbaa275b --- /dev/null +++ b/src/lib/mcp/auth.ts @@ -0,0 +1,136 @@ +import type { AuthInfo } from '@umami/mcp'; +import { isApiKey, isApiKeyEnabled } from '@/lib/api-key'; +import { checkApiKeyAuth, getBearerToken } from '@/lib/auth'; +import { getIssuer, getOAuthEndpoints, isOAuthEnabled } from '@/lib/oauth/config'; +import { OAUTH_SCOPES } from '@/lib/oauth/scopes'; +import { looksLikeAccessToken, verifyAccessToken } from '@/lib/oauth/tokens'; +import { buildOAuthAuth } from '@/lib/oauth/verify'; + +export type McpAuthResult = + | { ok: true; authInfo: AuthInfo; userId: string } + | { ok: false; status: 401 | 403; error: string; description: string; scope?: string }; + +/** + * Authenticates a request to the embedded MCP endpoint. + * + * Accepted credentials: + * - OAuth access tokens issued by this deployment for the `/mcp` resource (primary). + * - Self-hosted API keys (`umami_…`), so a remote MCP client can be configured without OAuth. + * + * Browser session tokens are deliberately rejected. + */ +export async function authenticateMcpRequest(request: Request): Promise { + const token = getBearerToken(request); + + if (!token) { + return { + ok: false, + status: 401, + error: 'invalid_request', + description: 'Missing bearer token.', + }; + } + + if (isApiKeyEnabled() && isApiKey(token)) { + const auth = await checkApiKeyAuth(request, token); + + if (!auth?.user?.id) { + return { ok: false, status: 401, error: 'invalid_token', description: 'Invalid API key.' }; + } + + return { + ok: true, + userId: auth.user.id, + authInfo: { + token, + clientId: `api-key:${auth.apiKey.id}`, + scopes: [...OAUTH_SCOPES], + extra: { userId: auth.user.id, authType: 'api-key' }, + }, + }; + } + + if (!looksLikeAccessToken(token) || !isOAuthEnabled()) { + return { ok: false, status: 401, error: 'invalid_token', description: 'Invalid bearer token.' }; + } + + const verified = verifyAccessToken(token); + + if (!verified) { + return { + ok: false, + status: 401, + error: 'invalid_token', + description: 'Access token is invalid or expired.', + }; + } + + // Audience binding: the token must have been issued for this deployment's MCP resource. + const endpoints = getOAuthEndpoints(request.headers); + + if (verified.resource !== endpoints.resource || verified.issuer !== getIssuer(request.headers)) { + return { + ok: false, + status: 401, + error: 'invalid_token', + description: 'Access token was not issued for this resource.', + }; + } + + const auth = await buildOAuthAuth(token, verified); + + if (!auth) { + return { + ok: false, + status: 401, + error: 'invalid_token', + description: 'Access token is no longer valid.', + }; + } + + return { + ok: true, + userId: auth.user.id, + authInfo: { + token, + clientId: verified.clientId, + scopes: verified.scopes, + expiresAt: Math.floor(verified.expiresAt / 1000), + resource: new URL(verified.resource), + extra: { userId: auth.user.id, authType: 'oauth', tokenId: verified.tokenId }, + }, + }; +} + +export function mcpAuthErrorResponse(result: McpAuthResult, headers: Pick) { + if (result.ok) { + throw new Error('mcpAuthErrorResponse called with a successful result.'); + } + + // Explicit narrowing: the project compiles without strictNullChecks, so the guard above does + // not narrow the union automatically. + const failure = result as Extract; + const endpoints = getOAuthEndpoints(headers); + const parts = [ + `Bearer error="${failure.error}"`, + `error_description="${failure.description.replace(/"/g, "'")}"`, + `resource_metadata="${endpoints.protectedResourceMetadata}"`, + ]; + + if (failure.scope) { + parts.push(`scope="${failure.scope}"`); + } else if (failure.status === 401) { + parts.push(`scope="${OAUTH_SCOPES.join(' ')}"`); + } + + return Response.json( + { error: failure.error, error_description: failure.description }, + { + status: failure.status, + headers: { + 'www-authenticate': parts.join(', '), + 'cache-control': 'no-store', + }, + }, + ); +} diff --git a/src/lib/mcp/dispatch.test.ts b/src/lib/mcp/dispatch.test.ts new file mode 100644 index 000000000..f5d0c622c --- /dev/null +++ b/src/lib/mcp/dispatch.test.ts @@ -0,0 +1,28 @@ +import { describe, expect, test } from 'vitest'; +import { OAUTH_ROUTE_SCOPES } from '@/lib/oauth/scopes'; +import { MCP_DISPATCH_ROUTES, matchDispatchRoute } from './dispatch'; + +describe('MCP dispatch table', () => { + test('covers exactly the OAuth-allowlisted routes', () => { + const dispatch = MCP_DISPATCH_ROUTES.map(route => `${route.method} ${route.path}`).sort(); + const allowlist = OAUTH_ROUTE_SCOPES.map( + route => `${route.method.toUpperCase()} ${route.path}`, + ).sort(); + + expect(dispatch).toEqual(allowlist); + }); + + test('matches concrete paths and extracts parameters', () => { + const match = matchDispatchRoute('GET', '/api/websites/abc/sessions/def/activity'); + + expect(match?.route.path).toBe('/api/websites/{websiteId}/sessions/{sessionId}/activity'); + expect(match?.params).toEqual({ websiteId: 'abc', sessionId: 'def' }); + }); + + test('ignores a base path prefix and rejects unknown routes', () => { + expect(matchDispatchRoute('GET', '/umami/api/websites')?.route.path).toBe('/api/websites'); + expect(matchDispatchRoute('POST', '/api/websites')).toBeNull(); + expect(matchDispatchRoute('GET', '/api/admin/users')).toBeNull(); + expect(matchDispatchRoute('DELETE', '/api/websites/abc')).toBeNull(); + }); +}); diff --git a/src/lib/mcp/dispatch.ts b/src/lib/mcp/dispatch.ts new file mode 100644 index 000000000..092c4e7d9 --- /dev/null +++ b/src/lib/mcp/dispatch.ts @@ -0,0 +1,255 @@ +import { badRequest, notFound, serverError } from '@/lib/response'; + +// Route handlers declare their own concrete `params` shapes; `any` keeps the table assignable. +type RouteHandler = (request: Request, context: { params: Promise }) => Promise; + +type RouteModule = Partial>; + +export interface DispatchRoute { + method: 'GET' | 'POST'; + path: `/api/${string}`; + load: () => Promise; +} + +/** + * The API routes the embedded MCP server may call, dispatched in-process (no loopback HTTP). + * This list must stay identical to `OAUTH_ROUTE_SCOPES` in `src/lib/oauth/scopes.ts`; + * `dispatch.test.ts` enforces that. Anything not listed here is unreachable from MCP. + */ +export const MCP_DISPATCH_ROUTES: readonly DispatchRoute[] = [ + { method: 'GET', path: '/api/me', load: () => import('@/app/api/me/route') }, + { method: 'GET', path: '/api/websites', load: () => import('@/app/api/websites/route') }, + { + method: 'GET', + path: '/api/websites/{websiteId}', + load: () => import('@/app/api/websites/[websiteId]/route'), + }, + { + method: 'GET', + path: '/api/websites/{websiteId}/active', + load: () => import('@/app/api/websites/[websiteId]/active/route'), + }, + { + method: 'GET', + path: '/api/websites/{websiteId}/daterange', + load: () => import('@/app/api/websites/[websiteId]/daterange/route'), + }, + { + method: 'GET', + path: '/api/websites/{websiteId}/stats', + load: () => import('@/app/api/websites/[websiteId]/stats/route'), + }, + { + method: 'GET', + path: '/api/websites/{websiteId}/pageviews', + load: () => import('@/app/api/websites/[websiteId]/pageviews/route'), + }, + { + method: 'GET', + path: '/api/websites/{websiteId}/metrics', + load: () => import('@/app/api/websites/[websiteId]/metrics/route'), + }, + { + method: 'GET', + path: '/api/websites/{websiteId}/metrics/expanded', + load: () => import('@/app/api/websites/[websiteId]/metrics/expanded/route'), + }, + { + method: 'GET', + path: '/api/realtime/{websiteId}', + load: () => import('@/app/api/realtime/[websiteId]/route'), + }, + { + method: 'GET', + path: '/api/websites/{websiteId}/events', + load: () => import('@/app/api/websites/[websiteId]/events/route'), + }, + { + method: 'GET', + path: '/api/websites/{websiteId}/events/stats', + load: () => import('@/app/api/websites/[websiteId]/events/stats/route'), + }, + { + method: 'GET', + path: '/api/websites/{websiteId}/events/series', + load: () => import('@/app/api/websites/[websiteId]/events/series/route'), + }, + { + method: 'GET', + path: '/api/websites/{websiteId}/event-data', + load: () => import('@/app/api/websites/[websiteId]/event-data/route'), + }, + { + method: 'GET', + path: '/api/websites/{websiteId}/event-data/stats', + load: () => import('@/app/api/websites/[websiteId]/event-data/stats/route'), + }, + { + method: 'GET', + path: '/api/websites/{websiteId}/event-data/properties', + load: () => import('@/app/api/websites/[websiteId]/event-data/properties/route'), + }, + { + method: 'GET', + path: '/api/websites/{websiteId}/event-data/values', + load: () => import('@/app/api/websites/[websiteId]/event-data/values/route'), + }, + { + method: 'GET', + path: '/api/websites/{websiteId}/sessions', + load: () => import('@/app/api/websites/[websiteId]/sessions/route'), + }, + { + method: 'GET', + path: '/api/websites/{websiteId}/sessions/stats', + load: () => import('@/app/api/websites/[websiteId]/sessions/stats/route'), + }, + { + method: 'GET', + path: '/api/websites/{websiteId}/sessions/{sessionId}', + load: () => import('@/app/api/websites/[websiteId]/sessions/[sessionId]/route'), + }, + { + method: 'GET', + path: '/api/websites/{websiteId}/sessions/{sessionId}/activity', + load: () => import('@/app/api/websites/[websiteId]/sessions/[sessionId]/activity/route'), + }, + { + method: 'GET', + path: '/api/websites/{websiteId}/sessions/{sessionId}/properties', + load: () => import('@/app/api/websites/[websiteId]/sessions/[sessionId]/properties/route'), + }, + { + method: 'POST', + path: '/api/reports/attribution', + load: () => import('@/app/api/reports/attribution/route'), + }, + { + method: 'POST', + path: '/api/reports/breakdown', + load: () => import('@/app/api/reports/breakdown/route'), + }, + { + method: 'POST', + path: '/api/reports/funnel', + load: () => import('@/app/api/reports/funnel/route'), + }, + { + method: 'POST', + path: '/api/reports/journey', + load: () => import('@/app/api/reports/journey/route'), + }, + { + method: 'POST', + path: '/api/reports/retention', + load: () => import('@/app/api/reports/retention/route'), + }, + { + method: 'POST', + path: '/api/reports/revenue', + load: () => import('@/app/api/reports/revenue/route'), + }, + { method: 'POST', path: '/api/reports/utm', load: () => import('@/app/api/reports/utm/route') }, +]; + +function escapeRegExp(value: string) { + return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); +} + +interface CompiledRoute extends DispatchRoute { + pattern: RegExp; + paramNames: string[]; +} + +function compile(route: DispatchRoute): CompiledRoute { + const paramNames: string[] = []; + const source = route.path + .split('/') + .map(segment => { + const match = /^\{([^}]+)}$/.exec(segment); + + if (match) { + paramNames.push(match[1]); + return '([^/]+)'; + } + + return escapeRegExp(segment); + }) + .join('/'); + + return { ...route, pattern: new RegExp(`^${source}/?$`), paramNames }; +} + +const compiledRoutes = MCP_DISPATCH_ROUTES.map(compile); + +export function matchDispatchRoute(method: string, pathname: string) { + const apiIndex = pathname.indexOf('/api/'); + const apiPath = apiIndex >= 0 ? pathname.slice(apiIndex) : pathname; + const upper = method.toUpperCase(); + + for (const route of compiledRoutes) { + if (route.method !== upper) { + continue; + } + + const match = route.pattern.exec(apiPath); + + if (match) { + const params: Record = {}; + + route.paramNames.forEach((name, index) => { + params[name] = decodeURIComponent(match[index + 1]); + }); + + return { route, params }; + } + } + + return null; +} + +export type InProcessFetch = (input: string | URL, init?: RequestInit) => Promise; + +/** + * A `fetch` replacement that invokes App Router handlers directly. Used by the embedded MCP + * endpoint so tool calls never leave the process, while still passing through the exact same + * request parsing, authentication and permission checks as an external HTTP call. + */ +export function createInProcessFetch(): InProcessFetch { + return async (input, init = {}) => { + const url = new URL(input.toString()); + const method = (init.method ?? 'GET').toUpperCase(); + const matched = matchDispatchRoute(method, url.pathname); + + if (!matched) { + return notFound({ message: `Route ${method} ${url.pathname} is not available to MCP.` }); + } + + let module: RouteModule; + + try { + module = await matched.route.load(); + } catch (error) { + return serverError(error); + } + + const handler = module[method as keyof RouteModule]; + + if (!handler) { + return badRequest({ message: `Method ${method} is not supported.` }); + } + + const request = new Request(url, { + method, + headers: init.headers, + body: init.body ?? undefined, + signal: init.signal ?? undefined, + }); + + try { + return await handler(request, { params: Promise.resolve(matched.params) }); + } catch (error) { + return serverError(error); + } + }; +} diff --git a/src/lib/mcp/rate-limit.test.ts b/src/lib/mcp/rate-limit.test.ts new file mode 100644 index 000000000..36beb8ff7 --- /dev/null +++ b/src/lib/mcp/rate-limit.test.ts @@ -0,0 +1,37 @@ +import { beforeEach, describe, expect, test, vi } from 'vitest'; +import { checkMcpRateLimit, resetMcpRateLimits } from './rate-limit'; + +vi.mock('@/lib/redis', () => ({ + default: { enabled: false, client: {} }, +})); + +beforeEach(() => { + resetMcpRateLimits(); +}); + +describe('checkMcpRateLimit', () => { + test('allows up to the limit per user/client and then blocks', async () => { + for (let i = 0; i < 3; i++) { + await expect(checkMcpRateLimit('u', 'c', 3)).resolves.toBe(true); + } + + await expect(checkMcpRateLimit('u', 'c', 3)).resolves.toBe(false); + await expect(checkMcpRateLimit('u', 'other-client', 3)).resolves.toBe(true); + await expect(checkMcpRateLimit('other-user', 'c', 3)).resolves.toBe(true); + }); + + test('resets after the window', async () => { + vi.useFakeTimers(); + + try { + await expect(checkMcpRateLimit('u', 'c', 1, 1)).resolves.toBe(true); + await expect(checkMcpRateLimit('u', 'c', 1, 1)).resolves.toBe(false); + + vi.advanceTimersByTime(1100); + + await expect(checkMcpRateLimit('u', 'c', 1, 1)).resolves.toBe(true); + } finally { + vi.useRealTimers(); + } + }); +}); diff --git a/src/lib/mcp/rate-limit.ts b/src/lib/mcp/rate-limit.ts new file mode 100644 index 000000000..2895b43fe --- /dev/null +++ b/src/lib/mcp/rate-limit.ts @@ -0,0 +1,54 @@ +import redis from '@/lib/redis'; + +export const MCP_RATE_LIMIT_WINDOW_SECONDS = 60; + +const memory = new Map(); + +export function getMcpRateLimit() { + const configured = Number(process.env.MCP_RATE_LIMIT); + + return Number.isFinite(configured) && configured > 0 ? configured : 120; +} + +/** + * Per-user, per-client limit for MCP requests. Keyed by identity rather than IP because remote + * MCP platforms share egress addresses. Uses Redis when available, otherwise a process-local + * fixed window (sufficient for single-instance self-hosted deployments). + */ +export async function checkMcpRateLimit( + userId: string, + clientId: string, + limit = getMcpRateLimit(), + windowSeconds = MCP_RATE_LIMIT_WINDOW_SECONDS, +): Promise { + const key = `mcp:rate:${userId}:${clientId}`; + + if (redis.enabled) { + return redis.client.rateLimit(key, limit, windowSeconds); + } + + const now = Date.now(); + const entry = memory.get(key); + + if (!entry || entry.resetAt <= now) { + memory.set(key, { count: 1, resetAt: now + windowSeconds * 1000 }); + + if (memory.size > 10_000) { + for (const [k, v] of memory) { + if (v.resetAt <= now) { + memory.delete(k); + } + } + } + + return true; + } + + entry.count += 1; + + return entry.count <= limit; +} + +export function resetMcpRateLimits() { + memory.clear(); +} diff --git a/src/lib/oauth/authorize.test.ts b/src/lib/oauth/authorize.test.ts new file mode 100644 index 000000000..846661281 --- /dev/null +++ b/src/lib/oauth/authorize.test.ts @@ -0,0 +1,171 @@ +import { beforeEach, describe, expect, test, vi } from 'vitest'; +import { createAuthorizationCode } from '@/queries/prisma/oauth'; +import { + buildErrorRedirect, + buildSuccessRedirect, + issueAuthorizationCode, + validateAuthorizationRequest, +} from './authorize'; +import { resolveOAuthClient } from './client'; +import { OAuthError } from './errors'; +import { computeCodeChallenge } from './pkce'; + +vi.mock('./client', () => ({ + resolveOAuthClient: vi.fn(), +})); + +vi.mock('@/queries/prisma/oauth', () => ({ + createAuthorizationCode: vi.fn(), +})); + +const resolveOAuthClientMock = vi.mocked(resolveOAuthClient); +const createAuthorizationCodeMock = vi.mocked(createAuthorizationCode); + +const ISSUER = 'https://analytics.example.com'; +const CLIENT = { + clientId: 'https://app.example.com/client.json', + clientName: 'App', + redirectUris: ['https://app.example.com/cb', 'http://127.0.0.1:1/cb'], + source: 'metadata-document' as const, +}; +const VERIFIER = 'dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk'; + +function request(overrides: Record = {}) { + return { + response_type: 'code', + client_id: CLIENT.clientId, + redirect_uri: 'https://app.example.com/cb', + scope: 'websites:read analytics:read', + state: 'xyz', + code_challenge: computeCodeChallenge(VERIFIER), + code_challenge_method: 'S256', + resource: `${ISSUER}/mcp`, + ...overrides, + }; +} + +beforeEach(() => { + process.env.APP_SECRET = 'test'; + resolveOAuthClientMock.mockReset(); + resolveOAuthClientMock.mockResolvedValue(CLIENT); + createAuthorizationCodeMock.mockReset(); + createAuthorizationCodeMock.mockResolvedValue({} as never); +}); + +describe('validateAuthorizationRequest', () => { + test('accepts a valid request', async () => { + await expect(validateAuthorizationRequest(request(), ISSUER)).resolves.toMatchObject({ + client: CLIENT, + redirectUri: 'https://app.example.com/cb', + scopes: ['analytics:read', 'websites:read'], + state: 'xyz', + codeChallengeMethod: 'S256', + resource: `${ISSUER}/mcp`, + }); + }); + + test('defaults to all scopes and the MCP resource when omitted', async () => { + const result = await validateAuthorizationRequest( + request({ scope: undefined, resource: undefined }), + ISSUER, + ); + + expect(result.scopes).toEqual(['analytics:read', 'websites:read']); + expect(result.resource).toBe(`${ISSUER}/mcp`); + }); + + test('allows loopback redirect ports to vary', async () => { + const result = await validateAuthorizationRequest( + request({ redirect_uri: 'http://127.0.0.1:54321/cb' }), + ISSUER, + ); + + expect(result.redirectUri).toBe('http://127.0.0.1:1/cb'); + }); + + test('rejects unregistered redirect URIs before anything else (not redirectable)', async () => { + const error = await validateAuthorizationRequest( + request({ redirect_uri: 'https://evil.example/cb', response_type: 'token' }), + ISSUER, + ).catch(e => e); + + expect(error).toBeInstanceOf(OAuthError); + expect(error.code).toBe('invalid_redirect_uri'); + expect(error.redirectable).toBe(false); + }); + + test('requires PKCE S256', async () => { + await expect( + validateAuthorizationRequest(request({ code_challenge: undefined }), ISSUER), + ).rejects.toMatchObject({ code: 'invalid_request', redirectable: true }); + await expect( + validateAuthorizationRequest(request({ code_challenge_method: 'plain' }), ISSUER), + ).rejects.toMatchObject({ code: 'invalid_request' }); + }); + + test('rejects unknown scopes, response types and resources', async () => { + await expect( + validateAuthorizationRequest(request({ scope: 'websites:write' }), ISSUER), + ).rejects.toMatchObject({ code: 'invalid_scope' }); + await expect( + validateAuthorizationRequest(request({ response_type: 'token' }), ISSUER), + ).rejects.toMatchObject({ code: 'unsupported_response_type' }); + await expect( + validateAuthorizationRequest(request({ resource: 'https://other.example/mcp' }), ISSUER), + ).rejects.toMatchObject({ code: 'invalid_target' }); + }); + + test('propagates client resolution errors', async () => { + resolveOAuthClientMock.mockRejectedValue(new OAuthError('invalid_client', 'Unknown')); + + await expect(validateAuthorizationRequest(request(), ISSUER)).rejects.toMatchObject({ + code: 'invalid_client', + }); + }); +}); + +describe('issueAuthorizationCode', () => { + test('stores a hashed, expiring, PKCE-bound code and builds the redirect', async () => { + const validated = await validateAuthorizationRequest(request(), ISSUER); + const code = await issueAuthorizationCode(validated, 'user-1'); + + expect(code.length).toBeGreaterThanOrEqual(43); + expect(createAuthorizationCodeMock).toHaveBeenCalledWith( + expect.objectContaining({ + userId: 'user-1', + clientId: CLIENT.clientId, + redirectUri: 'https://app.example.com/cb', + scope: 'analytics:read websites:read', + codeChallenge: computeCodeChallenge(VERIFIER), + codeChallengeMethod: 'S256', + }), + ); + + const stored = createAuthorizationCodeMock.mock.calls[0][0]; + + expect(stored.codeHash).not.toContain(code); + expect(stored.expiresAt.getTime()).toBeLessThanOrEqual(Date.now() + 10 * 60 * 1000); + + const redirect = new URL(buildSuccessRedirect(validated, code, ISSUER)); + + expect(redirect.origin + redirect.pathname).toBe('https://app.example.com/cb'); + expect(redirect.searchParams.get('code')).toBe(code); + expect(redirect.searchParams.get('state')).toBe('xyz'); + expect(redirect.searchParams.get('iss')).toBe(ISSUER); + }); + + test('builds error redirects with iss and state', () => { + const redirect = new URL( + buildErrorRedirect( + 'https://app.example.com/cb', + new OAuthError('access_denied', 'Denied'), + 'st', + ISSUER, + ), + ); + + expect(redirect.searchParams.get('error')).toBe('access_denied'); + expect(redirect.searchParams.get('state')).toBe('st'); + expect(redirect.searchParams.get('iss')).toBe(ISSUER); + }); +}); diff --git a/src/lib/oauth/authorize.ts b/src/lib/oauth/authorize.ts new file mode 100644 index 000000000..048cc7f15 --- /dev/null +++ b/src/lib/oauth/authorize.ts @@ -0,0 +1,183 @@ +import { uuid } from '@/lib/crypto'; +import { + type MetadataFetch, + type ResolvedOAuthClient, + resolveOAuthClient, +} from '@/lib/oauth/client'; +import { + AUTHORIZATION_CODE_TTL_SECONDS, + getMcpResource, + normalizeResource, +} from '@/lib/oauth/config'; +import { OAuthError } from '@/lib/oauth/errors'; +import { isPkceMethod, isValidCodeChallenge } from '@/lib/oauth/pkce'; +import { appendRedirectParams, findMatchingRedirectUri } from '@/lib/oauth/redirect'; +import { + isOAuthScope, + normalizeScopes, + OAUTH_SCOPES, + type OAuthScope, + parseScopes, +} from '@/lib/oauth/scopes'; +import { generateAuthorizationCode, hashToken } from '@/lib/oauth/tokens'; +import { createAuthorizationCode } from '@/queries/prisma/oauth'; + +export interface AuthorizationRequestParams { + response_type?: unknown; + client_id?: unknown; + redirect_uri?: unknown; + scope?: unknown; + state?: unknown; + code_challenge?: unknown; + code_challenge_method?: unknown; + resource?: unknown; +} + +export interface ValidatedAuthorizationRequest { + client: ResolvedOAuthClient; + redirectUri: string; + scopes: OAuthScope[]; + state?: string; + codeChallenge: string; + codeChallengeMethod: 'S256'; + resource: string; +} + +function optionalString(value: unknown, name: string, max = 2048) { + if (value === undefined || value === null || value === '') { + return undefined; + } + + if (typeof value !== 'string' || value.length > max) { + throw new OAuthError('invalid_request', `${name} is invalid.`); + } + + return value; +} + +/** + * Validates an authorization request per OAuth 2.1 + MCP requirements: PKCE S256 is mandatory, + * the redirect URI must be registered, scopes must be known, and the resource indicator must be + * this deployment's MCP server. + * + * Errors thrown before the redirect URI is validated must be shown to the user; errors thrown + * afterwards are marked `redirectable`. + */ +export async function validateAuthorizationRequest( + params: AuthorizationRequestParams, + issuer: string, + options: { fetch?: MetadataFetch } = {}, +): Promise { + const client = await resolveOAuthClient(params.client_id, options); + const redirectUriParam = optionalString(params.redirect_uri, 'redirect_uri'); + const redirectUri = redirectUriParam + ? findMatchingRedirectUri(redirectUriParam, client.redirectUris) + : client.redirectUris.length === 1 + ? client.redirectUris[0] + : null; + + if (!redirectUri) { + throw new OAuthError('invalid_redirect_uri', 'redirect_uri is not registered for this client.'); + } + + const redirectable = { redirectable: true }; + + if (params.response_type !== 'code') { + throw new OAuthError( + 'unsupported_response_type', + 'response_type must be "code".', + redirectable, + ); + } + + if (!isValidCodeChallenge(params.code_challenge)) { + throw new OAuthError( + 'invalid_request', + 'A PKCE code_challenge (S256) is required.', + redirectable, + ); + } + + if (!isPkceMethod(params.code_challenge_method)) { + throw new OAuthError('invalid_request', 'code_challenge_method must be "S256".', redirectable); + } + + const requestedScopes = parseScopes(optionalString(params.scope, 'scope', 500)); + const unknown = requestedScopes.filter( + scope => !isOAuthScope(scope) && scope !== 'offline_access', + ); + + if (unknown.length) { + throw new OAuthError('invalid_scope', `Unknown scope: ${unknown.join(', ')}.`, redirectable); + } + + const scopes = requestedScopes.length + ? normalizeScopes(requestedScopes) + : normalizeScopes([...OAUTH_SCOPES]); + + if (!scopes.length) { + throw new OAuthError('invalid_scope', 'At least one scope is required.', redirectable); + } + + const expectedResource = getMcpResource(issuer); + const resourceParam = optionalString(params.resource, 'resource'); + const resource = resourceParam ? normalizeResource(resourceParam) : expectedResource; + + if (!resource || resource !== expectedResource) { + throw new OAuthError('invalid_target', `resource must be ${expectedResource}.`, redirectable); + } + + return { + client, + redirectUri, + scopes, + state: optionalString(params.state, 'state', 1024), + codeChallenge: params.code_challenge, + codeChallengeMethod: 'S256', + resource, + }; +} + +export async function issueAuthorizationCode( + request: ValidatedAuthorizationRequest, + userId: string, +) { + const code = generateAuthorizationCode(); + + await createAuthorizationCode({ + id: uuid(), + codeHash: hashToken(code), + userId, + clientId: request.client.clientId, + redirectUri: request.redirectUri, + scope: request.scopes.join(' '), + resource: request.resource, + codeChallenge: request.codeChallenge, + codeChallengeMethod: request.codeChallengeMethod, + expiresAt: new Date(Date.now() + AUTHORIZATION_CODE_TTL_SECONDS * 1000), + }); + + return code; +} + +export function buildSuccessRedirect( + request: Pick, + code: string, + issuer: string, +) { + return appendRedirectParams(request.redirectUri, { code, state: request.state, iss: issuer }); +} + +export function buildErrorRedirect( + redirectUri: string, + error: OAuthError, + state: string | undefined, + issuer: string, +) { + return appendRedirectParams(redirectUri, { + error: error.code, + error_description: error.message, + state, + iss: issuer, + }); +} diff --git a/src/lib/oauth/client.test.ts b/src/lib/oauth/client.test.ts new file mode 100644 index 000000000..971122ed3 --- /dev/null +++ b/src/lib/oauth/client.test.ts @@ -0,0 +1,152 @@ +import { beforeEach, describe, expect, test, vi } from 'vitest'; +import { getOauthClient } from '@/queries/prisma/oauth'; +import { + clearClientMetadataCache, + fetchClientMetadata, + isClientMetadataUrl, + resolveOAuthClient, + validateClientMetadata, +} from './client'; + +vi.mock('@/lib/redis', () => ({ + default: { enabled: false, client: { get: vi.fn(), set: vi.fn() } }, +})); + +vi.mock('@/queries/prisma/oauth', () => ({ + getOauthClient: vi.fn(), +})); + +const getOauthClientMock = vi.mocked(getOauthClient); +const CLIENT_ID = 'https://app.example.com/oauth/client.json'; + +const validDocument = { + client_id: CLIENT_ID, + client_name: 'Example MCP Client', + client_uri: 'https://app.example.com', + redirect_uris: ['http://127.0.0.1:3000/callback', 'https://app.example.com/callback'], + token_endpoint_auth_method: 'none', +}; + +beforeEach(() => { + clearClientMetadataCache(); + getOauthClientMock.mockReset(); +}); + +describe('isClientMetadataUrl', () => { + test('requires https, a path and a public host', () => { + expect(isClientMetadataUrl(CLIENT_ID)).toBe(true); + expect(isClientMetadataUrl('https://app.example.com')).toBe(false); + expect(isClientMetadataUrl('https://app.example.com/')).toBe(false); + expect(isClientMetadataUrl('http://app.example.com/client.json')).toBe(false); + expect(isClientMetadataUrl('https://localhost/client.json')).toBe(false); + expect(isClientMetadataUrl('https://10.0.0.1/client.json')).toBe(false); + expect(isClientMetadataUrl('https://user:pw@app.example.com/client.json')).toBe(false); + expect(isClientMetadataUrl('not-a-url')).toBe(false); + }); +}); + +describe('validateClientMetadata', () => { + test('accepts a valid document', () => { + expect(validateClientMetadata(CLIENT_ID, validDocument)).toMatchObject({ + clientId: CLIENT_ID, + clientName: 'Example MCP Client', + redirectUris: validDocument.redirect_uris, + source: 'metadata-document', + }); + }); + + test('rejects mismatched client_id', () => { + expect(() => + validateClientMetadata(CLIENT_ID, { ...validDocument, client_id: 'https://other/x.json' }), + ).toThrow(/does not match/); + }); + + test('rejects missing or invalid redirect URIs and names', () => { + expect(() => + validateClientMetadata(CLIENT_ID, { ...validDocument, redirect_uris: [] }), + ).toThrow(/redirect_uris/); + expect(() => + validateClientMetadata(CLIENT_ID, { + ...validDocument, + redirect_uris: ['http://app.example.com/cb'], + }), + ).toThrow(/redirect_uris/); + expect(() => + validateClientMetadata(CLIENT_ID, { ...validDocument, client_name: undefined }), + ).toThrow(/client_name/); + }); + + test('rejects confidential clients', () => { + expect(() => + validateClientMetadata(CLIENT_ID, { + ...validDocument, + token_endpoint_auth_method: 'client_secret_basic', + }), + ).toThrow(/public clients/); + }); +}); + +describe('fetchClientMetadata', () => { + test('fetches and validates the document', async () => { + const fetchImpl = vi.fn(async () => Response.json(validDocument)); + const client = await fetchClientMetadata(CLIENT_ID, fetchImpl); + + expect(client.clientName).toBe('Example MCP Client'); + expect(fetchImpl).toHaveBeenCalledWith( + CLIENT_ID, + expect.objectContaining({ method: 'GET', redirect: 'error' }), + ); + }); + + test('maps fetch failures to invalid_client', async () => { + await expect( + fetchClientMetadata(CLIENT_ID, async () => new Response('nope', { status: 404 })), + ).rejects.toMatchObject({ code: 'invalid_client' }); + await expect( + fetchClientMetadata(CLIENT_ID, async () => new Response('{bad json')), + ).rejects.toMatchObject({ code: 'invalid_client_metadata' }); + await expect( + fetchClientMetadata(CLIENT_ID, async () => { + throw new TypeError('fetch failed'); + }), + ).rejects.toMatchObject({ code: 'invalid_client' }); + }); +}); + +describe('resolveOAuthClient', () => { + test('caches metadata documents', async () => { + const fetchImpl = vi.fn(async () => Response.json(validDocument)); + + await resolveOAuthClient(CLIENT_ID, { fetch: fetchImpl }); + await resolveOAuthClient(CLIENT_ID, { fetch: fetchImpl }); + + expect(fetchImpl).toHaveBeenCalledTimes(1); + }); + + test('resolves registered clients from the database', async () => { + getOauthClientMock.mockResolvedValue({ + id: 'registered-1', + name: 'Registered', + redirectUris: ['https://a.example/cb'], + metadata: { client_uri: 'https://a.example' }, + createdAt: new Date(), + } as never); + + await expect(resolveOAuthClient('registered-1')).resolves.toMatchObject({ + clientId: 'registered-1', + clientName: 'Registered', + clientUri: 'https://a.example', + source: 'registered', + }); + }); + + test('rejects unknown and malformed client ids', async () => { + getOauthClientMock.mockResolvedValue(null); + + await expect(resolveOAuthClient('missing')).rejects.toMatchObject({ code: 'invalid_client' }); + await expect(resolveOAuthClient('http://insecure.example/x.json')).rejects.toMatchObject({ + code: 'invalid_client', + }); + await expect(resolveOAuthClient('')).rejects.toMatchObject({ code: 'invalid_client' }); + }); +}); diff --git a/src/lib/oauth/client.ts b/src/lib/oauth/client.ts new file mode 100644 index 000000000..0d08813b8 --- /dev/null +++ b/src/lib/oauth/client.ts @@ -0,0 +1,280 @@ +import debug from 'debug'; +import { CLIENT_METADATA_CACHE_SECONDS } from '@/lib/oauth/config'; +import { OAuthError } from '@/lib/oauth/errors'; +import { isAcceptableRedirectUri, parseUrl } from '@/lib/oauth/redirect'; +import redis from '@/lib/redis'; +import { getOauthClient } from '@/queries/prisma/oauth'; + +const log = debug('umami:oauth'); + +const METADATA_FETCH_TIMEOUT_MS = 5000; +const METADATA_MAX_BYTES = 64 * 1024; +const MAX_REDIRECT_URIS = 20; + +export interface ResolvedOAuthClient { + clientId: string; + clientName: string; + clientUri?: string; + logoUri?: string; + redirectUris: string[]; + source: 'metadata-document' | 'registered'; +} + +interface ClientMetadataDocument { + client_id?: unknown; + client_name?: unknown; + client_uri?: unknown; + logo_uri?: unknown; + redirect_uris?: unknown; + token_endpoint_auth_method?: unknown; +} + +const memoryCache = new Map(); + +function isPrivateHostname(hostname: string) { + const host = hostname.toLowerCase(); + + if (host === 'localhost' || host.endsWith('.localhost') || host.endsWith('.local')) { + return true; + } + + // IPv6 literal + if (host.startsWith('[')) { + return true; + } + + // IPv4 literal: block entirely; metadata documents must be served from a DNS name. + if (/^\d{1,3}(\.\d{1,3}){3}$/.test(host)) { + return true; + } + + return false; +} + +/** + * A Client ID Metadata Document identifier is an https URL with a non-root path. + * (draft-ietf-oauth-client-id-metadata-document) + */ +export function isClientMetadataUrl(clientId: string) { + const url = parseUrl(clientId); + + return ( + !!url && + url.protocol === 'https:' && + url.pathname !== '/' && + url.pathname.length > 1 && + !url.hash && + !url.username && + !url.password && + !isPrivateHostname(url.hostname) + ); +} + +function parseRedirectUris(value: unknown) { + if (!Array.isArray(value) || !value.length || value.length > MAX_REDIRECT_URIS) { + return null; + } + + const uris = value.filter((uri): uri is string => typeof uri === 'string'); + + if (uris.length !== value.length || !uris.every(isAcceptableRedirectUri)) { + return null; + } + + return uris; +} + +function asString(value: unknown, max = 255) { + return typeof value === 'string' && value.length <= max ? value : undefined; +} + +export function validateClientMetadata( + clientId: string, + document: ClientMetadataDocument, +): ResolvedOAuthClient { + if (document.client_id !== clientId) { + throw new OAuthError( + 'invalid_client_metadata', + 'client_id in the metadata document does not match the document URL.', + ); + } + + const redirectUris = parseRedirectUris(document.redirect_uris); + + if (!redirectUris) { + throw new OAuthError( + 'invalid_client_metadata', + 'redirect_uris must be a non-empty list of absolute https, loopback or custom-scheme URLs.', + ); + } + + const clientName = asString(document.client_name); + + if (!clientName) { + throw new OAuthError('invalid_client_metadata', 'client_name is required.'); + } + + if ( + document.token_endpoint_auth_method !== undefined && + document.token_endpoint_auth_method !== 'none' + ) { + throw new OAuthError( + 'invalid_client_metadata', + 'Only public clients (token_endpoint_auth_method "none") are supported.', + ); + } + + return { + clientId, + clientName, + clientUri: asString(document.client_uri, 2048), + logoUri: asString(document.logo_uri, 2048), + redirectUris, + source: 'metadata-document', + }; +} + +async function readCache(clientId: string): Promise { + if (redis.enabled) { + return (await redis.client.get(`oauth:client:${clientId}`)) ?? null; + } + + const entry = memoryCache.get(clientId); + + if (entry && entry.expiresAt > Date.now()) { + return entry.value; + } + + memoryCache.delete(clientId); + + return null; +} + +async function writeCache(clientId: string, value: ResolvedOAuthClient) { + if (redis.enabled) { + await redis.client.set(`oauth:client:${clientId}`, value, CLIENT_METADATA_CACHE_SECONDS); + return; + } + + memoryCache.set(clientId, { + value, + expiresAt: Date.now() + CLIENT_METADATA_CACHE_SECONDS * 1000, + }); +} + +export type MetadataFetch = (url: string, init: RequestInit) => Promise; + +export async function fetchClientMetadata( + clientId: string, + fetchImpl: MetadataFetch = fetch, +): Promise { + const controller = new AbortController(); + const timer = setTimeout(() => controller.abort(), METADATA_FETCH_TIMEOUT_MS); + + try { + const response = await fetchImpl(clientId, { + method: 'GET', + headers: { accept: 'application/json' }, + redirect: 'error', + signal: controller.signal, + }); + + if (!response.ok) { + throw new OAuthError( + 'invalid_client', + `Could not fetch client metadata document (${response.status}).`, + ); + } + + const text = await response.text(); + + if (text.length > METADATA_MAX_BYTES) { + throw new OAuthError('invalid_client_metadata', 'Client metadata document is too large.'); + } + + let document: ClientMetadataDocument; + + try { + document = JSON.parse(text); + } catch { + throw new OAuthError('invalid_client_metadata', 'Client metadata document is not JSON.'); + } + + if (!document || typeof document !== 'object' || Array.isArray(document)) { + throw new OAuthError('invalid_client_metadata', 'Client metadata document is not an object.'); + } + + return validateClientMetadata(clientId, document); + } catch (error) { + if (error instanceof OAuthError) { + throw error; + } + + log('client metadata fetch failed', error); + + throw new OAuthError('invalid_client', 'Could not fetch client metadata document.'); + } finally { + clearTimeout(timer); + } +} + +/** + * Resolves a client by ID. URL-shaped IDs are Client ID Metadata Documents (fetched and cached); + * anything else must be a client registered through dynamic registration. + */ +export async function resolveOAuthClient( + clientId: unknown, + options: { fetch?: MetadataFetch } = {}, +): Promise { + if (typeof clientId !== 'string' || !clientId || clientId.length > 2048) { + throw new OAuthError('invalid_client', 'client_id is required.'); + } + + if (isClientMetadataUrl(clientId)) { + const cached = await readCache(clientId); + + if (cached) { + return cached; + } + + const client = await fetchClientMetadata(clientId, options.fetch); + + await writeCache(clientId, client); + + return client; + } + + if (clientId.startsWith('http://') || clientId.startsWith('https://')) { + throw new OAuthError( + 'invalid_client', + 'client_id URLs must use https, include a path and not point at a private host.', + ); + } + + const registered = await getOauthClient(clientId); + + if (!registered) { + throw new OAuthError('invalid_client', 'Unknown client_id.'); + } + + const redirectUris = parseRedirectUris(registered.redirectUris); + + if (!redirectUris) { + throw new OAuthError('invalid_client', 'Registered client has no valid redirect URIs.'); + } + + const metadata = (registered.metadata ?? {}) as Record; + + return { + clientId: registered.id, + clientName: registered.name, + clientUri: asString(metadata.client_uri, 2048), + logoUri: asString(metadata.logo_uri, 2048), + redirectUris, + source: 'registered', + }; +} + +export function clearClientMetadataCache() { + memoryCache.clear(); +} diff --git a/src/lib/oauth/config.ts b/src/lib/oauth/config.ts new file mode 100644 index 000000000..434c769c3 --- /dev/null +++ b/src/lib/oauth/config.ts @@ -0,0 +1,96 @@ +import { getBaseUrl } from '@/lib/get-base-url'; + +/** Authorization codes are single-use and short-lived (OAuth 2.1 recommends ≤ 10 minutes). */ +export const AUTHORIZATION_CODE_TTL_SECONDS = 10 * 60; +/** Access tokens are short-lived JWTs; refresh tokens carry long-lived sessions. */ +export const ACCESS_TOKEN_TTL_SECONDS = 60 * 60; +export const REFRESH_TOKEN_TTL_SECONDS = 30 * 24 * 60 * 60; +/** Cache lifetime for fetched Client ID Metadata Documents. */ +export const CLIENT_METADATA_CACHE_SECONDS = 60 * 60; + +export const OAUTH_ACCESS_TOKEN_TYPE = 'oauth_access'; +export const REFRESH_TOKEN_PREFIX = 'umami_rt_'; +export const MCP_RESOURCE_PATH = '/mcp'; + +export function isOAuthEnabled() { + return process.env.OAUTH_DISABLED !== '1' && !process.env.DISABLE_LOGIN; +} + +export function isDynamicRegistrationEnabled() { + return isOAuthEnabled() && process.env.OAUTH_DISABLE_DCR !== '1'; +} + +function trimTrailingSlash(value: string) { + return value.replace(/\/+$/, ''); +} + +/** + * The OAuth issuer identifier: the public base URL of this Umami instance (including any + * BASE_PATH). Can be pinned with `OAUTH_ISSUER` for deployments behind proxies that do not + * forward host headers. + */ +export function getIssuer(headers?: Pick) { + const configured = process.env.OAUTH_ISSUER?.trim(); + + if (configured) { + return trimTrailingSlash(configured); + } + + const base = trimTrailingSlash(getBaseUrl(headers).toString()); + const basePath = trimTrailingSlash(process.env.BASE_PATH ?? ''); + + return `${base}${basePath}`; +} + +/** Canonical resource identifier for the embedded MCP server (RFC 8707). */ +export function getMcpResource(issuer: string) { + return `${trimTrailingSlash(issuer)}${MCP_RESOURCE_PATH}`; +} + +export interface OAuthEndpoints { + issuer: string; + resource: string; + authorizationEndpoint: string; + tokenEndpoint: string; + revocationEndpoint: string; + registrationEndpoint?: string; + authorizationServerMetadata: string; + protectedResourceMetadata: string; +} + +export function getOAuthEndpoints(headers?: Pick): OAuthEndpoints { + const issuer = getIssuer(headers); + + return { + issuer, + resource: getMcpResource(issuer), + authorizationEndpoint: `${issuer}/oauth/authorize`, + tokenEndpoint: `${issuer}/api/oauth/token`, + revocationEndpoint: `${issuer}/api/oauth/revoke`, + registrationEndpoint: isDynamicRegistrationEnabled() + ? `${issuer}/api/oauth/register` + : undefined, + authorizationServerMetadata: `${issuer}/.well-known/oauth-authorization-server`, + protectedResourceMetadata: `${issuer}/.well-known/oauth-protected-resource${MCP_RESOURCE_PATH}`, + }; +} + +/** + * Normalizes a resource indicator for comparison: lowercase scheme/host, no trailing slash, + * no fragment. + */ +export function normalizeResource(value: string) { + try { + const url = new URL(value); + + if (url.hash) { + return null; + } + + url.hash = ''; + + return trimTrailingSlash(url.toString()); + } catch { + return null; + } +} diff --git a/src/lib/oauth/errors.ts b/src/lib/oauth/errors.ts new file mode 100644 index 000000000..27dcb196f --- /dev/null +++ b/src/lib/oauth/errors.ts @@ -0,0 +1,88 @@ +export type OAuthErrorCode = + | 'invalid_request' + | 'invalid_client' + | 'invalid_grant' + | 'unauthorized_client' + | 'unsupported_grant_type' + | 'unsupported_response_type' + | 'invalid_scope' + | 'invalid_target' + | 'access_denied' + | 'server_error' + | 'temporarily_unavailable' + | 'invalid_client_metadata' + | 'invalid_redirect_uri'; + +export class OAuthError extends Error { + readonly code: OAuthErrorCode; + readonly status: number; + /** When true the error may be delivered to the (validated) redirect URI. */ + readonly redirectable: boolean; + + constructor( + code: OAuthErrorCode, + description: string, + options: { status?: number; redirectable?: boolean } = {}, + ) { + super(description); + this.name = 'OAuthError'; + this.code = code; + this.status = options.status ?? defaultStatus(code); + this.redirectable = options.redirectable ?? false; + } + + toJSON() { + return { error: this.code, error_description: this.message }; + } +} + +function defaultStatus(code: OAuthErrorCode) { + switch (code) { + case 'invalid_client': + return 401; + case 'server_error': + return 500; + case 'temporarily_unavailable': + return 503; + default: + return 400; + } +} + +export function isOAuthError(error: unknown): error is OAuthError { + return error instanceof OAuthError || (error as { name?: string })?.name === 'OAuthError'; +} + +const NO_STORE = { 'cache-control': 'no-store', pragma: 'no-cache' }; + +export function oauthJson(body: unknown, init: ResponseInit = {}) { + return Response.json(body, { + ...init, + headers: { ...NO_STORE, ...(init.headers as Record) }, + }); +} + +export function oauthErrorResponse(error: unknown) { + if (isOAuthError(error)) { + const headers: Record = {}; + + if (error.status === 401) { + headers['www-authenticate'] = `Bearer error="${error.code}"`; + } + + return oauthJson(error.toJSON(), { status: error.status, headers }); + } + + return oauthJson( + { error: 'server_error', error_description: 'Unexpected error.' }, + { status: 500 }, + ); +} + +export function toOAuthError(error: unknown, fallback: OAuthErrorCode = 'server_error') { + if (isOAuthError(error)) { + return error; + } + + return new OAuthError(fallback, 'Unexpected error.'); +} diff --git a/src/lib/oauth/form.ts b/src/lib/oauth/form.ts new file mode 100644 index 000000000..dc93a2776 --- /dev/null +++ b/src/lib/oauth/form.ts @@ -0,0 +1,29 @@ +/** + * OAuth token/revocation/registration endpoints accept `application/x-www-form-urlencoded` + * (per RFC 6749) and, for convenience, JSON. + */ +export async function readOAuthBody(request: Request): Promise> { + const contentType = request.headers.get('content-type') ?? ''; + + try { + if (contentType.includes('application/json')) { + const body = await request.json(); + + return body && typeof body === 'object' && !Array.isArray(body) ? body : {}; + } + + const text = await request.text(); + const params = new URLSearchParams(text); + const result: Record = {}; + + for (const [key, value] of params) { + if (!(key in result)) { + result[key] = value; + } + } + + return result; + } catch { + return {}; + } +} diff --git a/src/lib/oauth/index.ts b/src/lib/oauth/index.ts new file mode 100644 index 000000000..154c93d94 --- /dev/null +++ b/src/lib/oauth/index.ts @@ -0,0 +1,12 @@ +export * from './authorize'; +export * from './client'; +export * from './config'; +export * from './errors'; +export * from './form'; +export * from './metadata'; +export * from './pkce'; +export * from './redirect'; +export * from './scopes'; +export * from './token'; +export * from './tokens'; +export * from './verify'; diff --git a/src/lib/oauth/metadata.ts b/src/lib/oauth/metadata.ts new file mode 100644 index 000000000..262b213b4 --- /dev/null +++ b/src/lib/oauth/metadata.ts @@ -0,0 +1,64 @@ +import { getOAuthEndpoints } from '@/lib/oauth/config'; +import { OAUTH_SCOPES } from '@/lib/oauth/scopes'; + +/** RFC 8414 Authorization Server Metadata. */ +export function buildAuthorizationServerMetadata(headers?: Pick) { + const endpoints = getOAuthEndpoints(headers); + + return { + issuer: endpoints.issuer, + authorization_endpoint: endpoints.authorizationEndpoint, + token_endpoint: endpoints.tokenEndpoint, + revocation_endpoint: endpoints.revocationEndpoint, + ...(endpoints.registrationEndpoint + ? { registration_endpoint: endpoints.registrationEndpoint } + : {}), + response_types_supported: ['code'], + response_modes_supported: ['query'], + grant_types_supported: ['authorization_code', 'refresh_token'], + code_challenge_methods_supported: ['S256'], + token_endpoint_auth_methods_supported: ['none'], + revocation_endpoint_auth_methods_supported: ['none'], + scopes_supported: [...OAUTH_SCOPES], + authorization_response_iss_parameter_supported: true, + client_id_metadata_document_supported: true, + service_documentation: 'https://umami.is/docs/mcp', + }; +} + +/** RFC 9728 Protected Resource Metadata for the embedded MCP server. */ +export function buildProtectedResourceMetadata(headers?: Pick) { + const endpoints = getOAuthEndpoints(headers); + + return { + resource: endpoints.resource, + resource_name: 'Umami MCP', + authorization_servers: [endpoints.issuer], + scopes_supported: [...OAUTH_SCOPES], + bearer_methods_supported: ['header'], + resource_documentation: 'https://umami.is/docs/mcp', + }; +} + +export function metadataResponse(body: unknown) { + return Response.json(body, { + headers: { + 'cache-control': 'public, max-age=300', + 'access-control-allow-origin': '*', + 'access-control-allow-methods': 'GET, OPTIONS', + 'access-control-allow-headers': '*', + }, + }); +} + +export function corsPreflight() { + return new Response(null, { + status: 204, + headers: { + 'access-control-allow-origin': '*', + 'access-control-allow-methods': 'GET, POST, OPTIONS', + 'access-control-allow-headers': '*', + 'access-control-max-age': '86400', + }, + }); +} diff --git a/src/lib/oauth/pkce.test.ts b/src/lib/oauth/pkce.test.ts new file mode 100644 index 000000000..2ff7ead88 --- /dev/null +++ b/src/lib/oauth/pkce.test.ts @@ -0,0 +1,40 @@ +import { describe, expect, test } from 'vitest'; +import { + computeCodeChallenge, + generateSecret, + isValidCodeChallenge, + isValidCodeVerifier, + verifyPkce, +} from './pkce'; + +// RFC 7636 Appendix B test vector. +const VERIFIER = 'dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk'; +const CHALLENGE = 'E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM'; + +describe('pkce', () => { + test('computes S256 challenges', () => { + expect(computeCodeChallenge(VERIFIER)).toBe(CHALLENGE); + }); + + test('verifies matching verifiers and rejects mismatches', () => { + expect(verifyPkce(VERIFIER, CHALLENGE, 'S256')).toBe(true); + expect(verifyPkce(`${VERIFIER.slice(0, -1)}x`, CHALLENGE, 'S256')).toBe(false); + expect(verifyPkce(VERIFIER, CHALLENGE, 'plain')).toBe(false); + expect(verifyPkce(undefined, CHALLENGE, 'S256')).toBe(false); + }); + + test('validates verifier and challenge formats', () => { + expect(isValidCodeVerifier(VERIFIER)).toBe(true); + expect(isValidCodeVerifier('short')).toBe(false); + expect(isValidCodeChallenge(CHALLENGE)).toBe(true); + expect(isValidCodeChallenge('not base64url!')).toBe(false); + }); + + test('generates URL-safe secrets', () => { + const secret = generateSecret(); + + expect(secret).toMatch(/^[A-Za-z0-9_-]+$/); + expect(secret.length).toBeGreaterThanOrEqual(43); + expect(generateSecret()).not.toBe(secret); + }); +}); diff --git a/src/lib/oauth/pkce.ts b/src/lib/oauth/pkce.ts new file mode 100644 index 000000000..c6d379637 --- /dev/null +++ b/src/lib/oauth/pkce.ts @@ -0,0 +1,39 @@ +import crypto from 'node:crypto'; + +export const PKCE_METHODS = ['S256'] as const; +export type PkceMethod = (typeof PKCE_METHODS)[number]; + +const VERIFIER_PATTERN = /^[A-Za-z0-9\-._~]{43,128}$/; +const CHALLENGE_PATTERN = /^[A-Za-z0-9\-_]{43}$/; + +export function isPkceMethod(value: unknown): value is PkceMethod { + return typeof value === 'string' && (PKCE_METHODS as readonly string[]).includes(value); +} + +export function isValidCodeChallenge(value: unknown): value is string { + return typeof value === 'string' && CHALLENGE_PATTERN.test(value); +} + +export function isValidCodeVerifier(value: unknown): value is string { + return typeof value === 'string' && VERIFIER_PATTERN.test(value); +} + +export function computeCodeChallenge(verifier: string) { + return crypto.createHash('sha256').update(verifier, 'ascii').digest('base64url'); +} + +export function verifyPkce(verifier: unknown, challenge: string, method: string) { + if (method !== 'S256' || !isValidCodeVerifier(verifier)) { + return false; + } + + const expected = Buffer.from(challenge); + const actual = Buffer.from(computeCodeChallenge(verifier)); + + return expected.length === actual.length && crypto.timingSafeEqual(expected, actual); +} + +/** URL-safe random secret used for authorization codes and refresh tokens. */ +export function generateSecret(bytes = 32) { + return crypto.randomBytes(bytes).toString('base64url'); +} diff --git a/src/lib/oauth/redirect.test.ts b/src/lib/oauth/redirect.test.ts new file mode 100644 index 000000000..940e0ed3b --- /dev/null +++ b/src/lib/oauth/redirect.test.ts @@ -0,0 +1,60 @@ +import { describe, expect, test } from 'vitest'; +import { + appendRedirectParams, + findMatchingRedirectUri, + isAcceptableRedirectUri, + redirectUriMatches, +} from './redirect'; + +describe('redirect URIs', () => { + test('accepts https, loopback http and custom schemes', () => { + expect(isAcceptableRedirectUri('https://app.example/callback')).toBe(true); + expect(isAcceptableRedirectUri('http://127.0.0.1:3000/callback')).toBe(true); + expect(isAcceptableRedirectUri('http://localhost/callback')).toBe(true); + expect(isAcceptableRedirectUri('myapp://callback')).toBe(true); + }); + + test('rejects non-loopback http, fragments and garbage', () => { + expect(isAcceptableRedirectUri('http://app.example/callback')).toBe(false); + expect(isAcceptableRedirectUri('https://app.example/callback#x')).toBe(false); + expect(isAcceptableRedirectUri('not a url')).toBe(false); + expect(isAcceptableRedirectUri(undefined)).toBe(false); + }); + + test('matches exactly, except loopback ports may vary', () => { + expect(redirectUriMatches('https://a.example/cb', 'https://a.example/cb')).toBe(true); + expect(redirectUriMatches('https://a.example/cb/', 'https://a.example/cb')).toBe(false); + expect(redirectUriMatches('https://a.example/cb?x=1', 'https://a.example/cb')).toBe(false); + expect(redirectUriMatches('http://127.0.0.1:51234/cb', 'http://127.0.0.1:3000/cb')).toBe(true); + expect(redirectUriMatches('http://localhost:51234/cb', 'http://localhost/cb')).toBe(true); + expect(redirectUriMatches('http://127.0.0.1:51234/other', 'http://127.0.0.1:3000/cb')).toBe( + false, + ); + expect(redirectUriMatches('http://localhost:1/cb', 'http://127.0.0.1:1/cb')).toBe(false); + }); + + test('finds the registered match', () => { + expect( + findMatchingRedirectUri('http://127.0.0.1:9999/cb', [ + 'https://a.example/cb', + 'http://127.0.0.1:1/cb', + ]), + ).toBe('http://127.0.0.1:1/cb'); + expect(findMatchingRedirectUri('https://evil.example/cb', ['https://a.example/cb'])).toBeNull(); + }); + + test('appends parameters without dropping existing ones', () => { + const url = new URL( + appendRedirectParams('https://a.example/cb?keep=1', { + code: 'c', + state: undefined, + iss: 'i', + }), + ); + + expect(url.searchParams.get('keep')).toBe('1'); + expect(url.searchParams.get('code')).toBe('c'); + expect(url.searchParams.get('iss')).toBe('i'); + expect(url.searchParams.has('state')).toBe(false); + }); +}); diff --git a/src/lib/oauth/redirect.ts b/src/lib/oauth/redirect.ts new file mode 100644 index 000000000..47ae9ebac --- /dev/null +++ b/src/lib/oauth/redirect.ts @@ -0,0 +1,77 @@ +const LOOPBACK_HOSTS = new Set(['127.0.0.1', '[::1]', 'localhost']); + +export function parseUrl(value: unknown): URL | null { + if (typeof value !== 'string' || !value) { + return null; + } + + try { + return new URL(value); + } catch { + return null; + } +} + +export function isLoopbackUrl(url: URL) { + return url.protocol === 'http:' && LOOPBACK_HOSTS.has(url.hostname); +} + +/** + * Redirect URIs must be valid absolute URLs without fragments. Non-loopback `http` is rejected; + * custom schemes (native apps) and `https` are accepted. + */ +export function isAcceptableRedirectUri(value: unknown) { + const url = parseUrl(value); + + if (!url || url.hash) { + return false; + } + + if (url.protocol === 'http:') { + return isLoopbackUrl(url); + } + + return true; +} + +/** + * Exact string comparison, except that loopback redirect URIs may vary the port + * (RFC 8252 §7.3), since native clients bind an ephemeral port per authorization. + */ +export function redirectUriMatches(candidate: string, registered: string) { + if (candidate === registered) { + return true; + } + + const left = parseUrl(candidate); + const right = parseUrl(registered); + + if (!left || !right || !isLoopbackUrl(left) || !isLoopbackUrl(right)) { + return false; + } + + return ( + left.hostname === right.hostname && + left.pathname === right.pathname && + left.search === right.search + ); +} + +export function findMatchingRedirectUri(candidate: string, registered: readonly string[]) { + return registered.find(uri => redirectUriMatches(candidate, uri)) ?? null; +} + +export function appendRedirectParams( + redirectUri: string, + params: Record, +) { + const url = new URL(redirectUri); + + for (const [key, value] of Object.entries(params)) { + if (value !== undefined) { + url.searchParams.set(key, value); + } + } + + return url.toString(); +} diff --git a/src/lib/oauth/replay.ts b/src/lib/oauth/replay.ts new file mode 100644 index 000000000..96c72bb23 --- /dev/null +++ b/src/lib/oauth/replay.ts @@ -0,0 +1,6 @@ +/** + * Replay detection for authorization codes lives in `consumeAuthorizationCode` + * (`src/queries/prisma/oauth.ts`): a second redemption returns the original grant so that every + * refresh token issued from it can be revoked (OAuth 2.1 §4.1.2). + */ +export {}; diff --git a/src/lib/oauth/scopes.test.ts b/src/lib/oauth/scopes.test.ts new file mode 100644 index 000000000..377a66567 --- /dev/null +++ b/src/lib/oauth/scopes.test.ts @@ -0,0 +1,65 @@ +import { describe, expect, test } from 'vitest'; +import { + getContractOAuthScope, + getRouteOAuthScope, + hasRequiredScope, + normalizeScopes, + OAUTH_ROUTE_SCOPES, + parseScopes, +} from './scopes'; + +describe('getRouteOAuthScope', () => { + test('matches concrete paths against parameterized routes', () => { + expect( + getRouteOAuthScope('GET', '/api/websites/6f2a7e0e-2b0f-4b3f-9f0a-1234567890ab/stats'), + ).toBe('analytics:read'); + expect(getRouteOAuthScope('get', '/api/websites')).toBe('websites:read'); + expect(getRouteOAuthScope('GET', '/api/websites/abc/sessions/def/activity')).toBe( + 'analytics:read', + ); + }); + + test('ignores a base path prefix', () => { + expect(getRouteOAuthScope('GET', '/umami/api/websites')).toBe('websites:read'); + }); + + test('rejects routes that are not allowlisted', () => { + expect(getRouteOAuthScope('POST', '/api/websites')).toBeNull(); + expect(getRouteOAuthScope('DELETE', '/api/websites/abc')).toBeNull(); + expect(getRouteOAuthScope('GET', '/api/admin/users')).toBeNull(); + expect(getRouteOAuthScope('GET', '/api/me/api-keys')).toBeNull(); + expect(getRouteOAuthScope('POST', '/api/reports')).toBeNull(); + }); + + test('does not match partial segments', () => { + expect(getRouteOAuthScope('GET', '/api/websites/abc/stats/extra')).toBeNull(); + expect(getRouteOAuthScope('GET', '/api/websitesX')).toBeNull(); + }); +}); + +describe('getContractOAuthScope', () => { + test('looks up contract paths exactly', () => { + expect(getContractOAuthScope('get', '/api/websites/{websiteId}/stats')).toBe('analytics:read'); + expect(getContractOAuthScope('post', '/api/websites/{websiteId}')).toBeNull(); + }); +}); + +describe('scope helpers', () => { + test('parses and normalizes scope strings', () => { + expect(parseScopes(' analytics:read websites:read ')).toEqual([ + 'analytics:read', + 'websites:read', + ]); + expect(normalizeScopes(['websites:read', 'bogus', 'analytics:read', 'websites:read'])).toEqual([ + 'analytics:read', + 'websites:read', + ]); + expect(hasRequiredScope(['analytics:read'], 'websites:read')).toBe(false); + }); + + test('every allowlisted route is unique', () => { + const keys = OAUTH_ROUTE_SCOPES.map(route => `${route.method} ${route.path}`); + + expect(new Set(keys).size).toBe(keys.length); + }); +}); diff --git a/src/lib/oauth/scopes.ts b/src/lib/oauth/scopes.ts new file mode 100644 index 000000000..dca5c2a6e --- /dev/null +++ b/src/lib/oauth/scopes.ts @@ -0,0 +1,159 @@ +/** + * OAuth scopes and the allowlist of API routes that accept OAuth access tokens. + * + * This module is the single source of truth for both: + * - the OpenAPI document (`src/openapi`), which advertises the scope per operation, and + * - runtime enforcement (`src/lib/auth.ts`), which rejects OAuth tokens on routes that + * have not explicitly opted in. + * + * It must remain free of environment-dependent side effects so that it can be imported + * from build scripts. + */ + +export const OAUTH_SCOPES = ['websites:read', 'analytics:read'] as const; + +export type OAuthScope = (typeof OAUTH_SCOPES)[number]; + +export const OAUTH_SCOPE_DESCRIPTIONS: Record = { + 'websites:read': 'List and view websites you have access to.', + 'analytics:read': 'Read analytics data (traffic, metrics, events, sessions, reports).', +}; + +export type OAuthHttpMethod = 'get' | 'post' | 'put' | 'patch' | 'delete'; + +export interface OAuthRouteScope { + method: OAuthHttpMethod; + path: `/api/${string}`; + scope: OAuthScope; +} + +/** + * Routes that accept OAuth access tokens, and the scope each one requires. + * Paths use OpenAPI-style `{param}` placeholders and must match a discovered route exactly. + * + * Any route not listed here rejects OAuth authentication. Keep this list limited to what the + * MCP server needs; expanding it is an explicit product decision. + */ +export const OAUTH_ROUTE_SCOPES: readonly OAuthRouteScope[] = [ + // Account + { method: 'get', path: '/api/me', scope: 'websites:read' }, + + // Websites + { method: 'get', path: '/api/websites', scope: 'websites:read' }, + { method: 'get', path: '/api/websites/{websiteId}', scope: 'websites:read' }, + { method: 'get', path: '/api/websites/{websiteId}/active', scope: 'analytics:read' }, + { method: 'get', path: '/api/websites/{websiteId}/daterange', scope: 'analytics:read' }, + { method: 'get', path: '/api/websites/{websiteId}/stats', scope: 'analytics:read' }, + { method: 'get', path: '/api/websites/{websiteId}/pageviews', scope: 'analytics:read' }, + { method: 'get', path: '/api/websites/{websiteId}/metrics', scope: 'analytics:read' }, + { method: 'get', path: '/api/websites/{websiteId}/metrics/expanded', scope: 'analytics:read' }, + { method: 'get', path: '/api/realtime/{websiteId}', scope: 'analytics:read' }, + + // Events + { method: 'get', path: '/api/websites/{websiteId}/events', scope: 'analytics:read' }, + { method: 'get', path: '/api/websites/{websiteId}/events/stats', scope: 'analytics:read' }, + { method: 'get', path: '/api/websites/{websiteId}/events/series', scope: 'analytics:read' }, + { method: 'get', path: '/api/websites/{websiteId}/event-data', scope: 'analytics:read' }, + { method: 'get', path: '/api/websites/{websiteId}/event-data/stats', scope: 'analytics:read' }, + { + method: 'get', + path: '/api/websites/{websiteId}/event-data/properties', + scope: 'analytics:read', + }, + { method: 'get', path: '/api/websites/{websiteId}/event-data/values', scope: 'analytics:read' }, + + // Sessions + { method: 'get', path: '/api/websites/{websiteId}/sessions', scope: 'analytics:read' }, + { method: 'get', path: '/api/websites/{websiteId}/sessions/stats', scope: 'analytics:read' }, + { + method: 'get', + path: '/api/websites/{websiteId}/sessions/{sessionId}', + scope: 'analytics:read', + }, + { + method: 'get', + path: '/api/websites/{websiteId}/sessions/{sessionId}/activity', + scope: 'analytics:read', + }, + { + method: 'get', + path: '/api/websites/{websiteId}/sessions/{sessionId}/properties', + scope: 'analytics:read', + }, + + // Reports (read/query only; saved-report CRUD is intentionally excluded) + { method: 'post', path: '/api/reports/attribution', scope: 'analytics:read' }, + { method: 'post', path: '/api/reports/breakdown', scope: 'analytics:read' }, + { method: 'post', path: '/api/reports/funnel', scope: 'analytics:read' }, + { method: 'post', path: '/api/reports/journey', scope: 'analytics:read' }, + { method: 'post', path: '/api/reports/retention', scope: 'analytics:read' }, + { method: 'post', path: '/api/reports/revenue', scope: 'analytics:read' }, + { method: 'post', path: '/api/reports/utm', scope: 'analytics:read' }, +]; + +export function isOAuthScope(value: unknown): value is OAuthScope { + return typeof value === 'string' && (OAUTH_SCOPES as readonly string[]).includes(value); +} + +export function parseScopes(value?: string | null): string[] { + return (value ?? '') + .split(/[\s,]+/) + .map(scope => scope.trim()) + .filter(Boolean); +} + +export function normalizeScopes(scopes: string[]): OAuthScope[] { + return [...new Set(scopes.filter(isOAuthScope))].sort() as OAuthScope[]; +} + +function escapeRegExp(value: string) { + return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); +} + +function toPattern(path: string) { + const source = path + .split('/') + .map(segment => (/^\{[^}]+}$/.test(segment) ? '[^/]+' : escapeRegExp(segment))) + .join('/'); + + return new RegExp(`^${source}/?$`); +} + +const routePatterns = OAUTH_ROUTE_SCOPES.map(route => ({ + ...route, + pattern: toPattern(route.path), +})); + +/** + * Returns the scope required to call the given route with an OAuth token, or `null` when the + * route does not accept OAuth authentication at all. + * + * `pathname` is a concrete request path (e.g. `/api/websites/1234/stats`). Any `BASE_PATH` + * or API URL prefix must be stripped by the caller. + */ +export function getRouteOAuthScope(method: string, pathname: string): OAuthScope | null { + const normalizedMethod = method.toLowerCase(); + const apiIndex = pathname.indexOf('/api/'); + const apiPath = apiIndex >= 0 ? pathname.slice(apiIndex) : pathname; + + for (const route of routePatterns) { + if (route.method === normalizedMethod && route.pattern.test(apiPath)) { + return route.scope; + } + } + + return null; +} + +export function getContractOAuthScope(method: string, path: string): OAuthScope | null { + const normalizedMethod = method.toLowerCase(); + + return ( + OAUTH_ROUTE_SCOPES.find(route => route.method === normalizedMethod && route.path === path) + ?.scope ?? null + ); +} + +export function hasRequiredScope(granted: readonly string[], required: OAuthScope) { + return granted.includes(required); +} diff --git a/src/lib/oauth/token.test.ts b/src/lib/oauth/token.test.ts new file mode 100644 index 000000000..b7aa25040 --- /dev/null +++ b/src/lib/oauth/token.test.ts @@ -0,0 +1,289 @@ +import { beforeEach, describe, expect, test, vi } from 'vitest'; +import { + consumeAuthorizationCode, + createRefreshToken, + getRefreshTokenByHash, + revokeRefreshToken, + revokeRefreshTokensForClient, + touchRefreshToken, +} from '@/queries/prisma/oauth'; +import { getUser } from '@/queries/prisma/user'; +import { computeCodeChallenge } from './pkce'; +import { handleRevocationRequest, handleTokenRequest } from './token'; +import { generateRefreshToken, hashToken, verifyAccessToken } from './tokens'; + +vi.mock('@/queries/prisma/oauth', () => ({ + consumeAuthorizationCode: vi.fn(), + createRefreshToken: vi.fn(), + getRefreshTokenByHash: vi.fn(), + revokeRefreshToken: vi.fn(), + revokeRefreshTokensForClient: vi.fn(), + touchRefreshToken: vi.fn(), +})); + +vi.mock('@/queries/prisma/user', () => ({ + getUser: vi.fn(), +})); + +const consumeAuthorizationCodeMock = vi.mocked(consumeAuthorizationCode); +const createRefreshTokenMock = vi.mocked(createRefreshToken); +const getRefreshTokenByHashMock = vi.mocked(getRefreshTokenByHash); +const revokeRefreshTokenMock = vi.mocked(revokeRefreshToken); +const revokeRefreshTokensForClientMock = vi.mocked(revokeRefreshTokensForClient); +const touchRefreshTokenMock = vi.mocked(touchRefreshToken); +const getUserMock = vi.mocked(getUser); + +const ISSUER = 'https://analytics.example.com'; +const RESOURCE = `${ISSUER}/mcp`; +const CLIENT_ID = 'https://app.example.com/client.json'; +const VERIFIER = 'dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk'; +const USER = { id: 'user-1', username: 'admin', role: 'admin', password: 'hashed' }; + +function codeRecord(overrides: Record = {}) { + return { + id: 'code-1', + codeHash: 'h', + userId: USER.id, + clientId: CLIENT_ID, + redirectUri: 'https://app.example.com/cb', + scope: 'analytics:read websites:read', + resource: RESOURCE, + codeChallenge: computeCodeChallenge(VERIFIER), + codeChallengeMethod: 'S256', + expiresAt: new Date(Date.now() + 60_000), + usedAt: new Date(), + createdAt: new Date(), + ...overrides, + } as never; +} + +function grant(overrides: Record = {}) { + return { + grant_type: 'authorization_code', + code: 'the-code', + client_id: CLIENT_ID, + redirect_uri: 'https://app.example.com/cb', + code_verifier: VERIFIER, + resource: RESOURCE, + ...overrides, + }; +} + +beforeEach(() => { + process.env.APP_SECRET = 'test'; + vi.mocked(consumeAuthorizationCodeMock).mockReset(); + createRefreshTokenMock.mockReset(); + createRefreshTokenMock.mockResolvedValue({} as never); + getRefreshTokenByHashMock.mockReset(); + revokeRefreshTokenMock.mockReset(); + revokeRefreshTokenMock.mockResolvedValue(1); + revokeRefreshTokensForClientMock.mockReset(); + touchRefreshTokenMock.mockReset(); + touchRefreshTokenMock.mockResolvedValue({} as never); + getUserMock.mockReset(); + getUserMock.mockResolvedValue(USER as never); +}); + +describe('authorization_code grant', () => { + test('issues access and refresh tokens', async () => { + consumeAuthorizationCodeMock.mockResolvedValue({ code: codeRecord(), replayed: null }); + + const tokens = await handleTokenRequest(grant(), ISSUER); + + expect(tokens).toMatchObject({ + token_type: 'Bearer', + expires_in: 3600, + scope: 'analytics:read websites:read', + }); + expect(tokens.refresh_token).toMatch(/^umami_rt_/); + expect(verifyAccessToken(tokens.access_token)).toMatchObject({ + userId: USER.id, + clientId: CLIENT_ID, + resource: RESOURCE, + }); + expect(createRefreshTokenMock).toHaveBeenCalledWith( + expect.objectContaining({ + userId: USER.id, + clientId: CLIENT_ID, + tokenHash: hashToken(tokens.refresh_token), + }), + ); + }); + + test('rejects an invalid PKCE verifier', async () => { + consumeAuthorizationCodeMock.mockResolvedValue({ code: codeRecord(), replayed: null }); + + await expect( + handleTokenRequest(grant({ code_verifier: `${VERIFIER.slice(0, -1)}x` }), ISSUER), + ).rejects.toMatchObject({ code: 'invalid_grant', message: /PKCE/ }); + expect(createRefreshTokenMock).not.toHaveBeenCalled(); + }); + + test('rejects client and redirect_uri mismatches', async () => { + consumeAuthorizationCodeMock.mockResolvedValue({ code: codeRecord(), replayed: null }); + await expect( + handleTokenRequest(grant({ client_id: 'https://other.example/c.json' }), ISSUER), + ).rejects.toMatchObject({ code: 'invalid_grant', message: /client_id/ }); + + consumeAuthorizationCodeMock.mockResolvedValue({ code: codeRecord(), replayed: null }); + await expect( + handleTokenRequest(grant({ redirect_uri: 'https://app.example.com/other' }), ISSUER), + ).rejects.toMatchObject({ code: 'invalid_grant', message: /redirect_uri/ }); + }); + + test('rejects expired or unknown codes', async () => { + consumeAuthorizationCodeMock.mockResolvedValue({ code: null, replayed: null }); + + await expect(handleTokenRequest(grant(), ISSUER)).rejects.toMatchObject({ + code: 'invalid_grant', + }); + expect(revokeRefreshTokensForClientMock).not.toHaveBeenCalled(); + }); + + test('revokes the grant family when a code is replayed', async () => { + consumeAuthorizationCodeMock.mockResolvedValue({ code: null, replayed: codeRecord() }); + + await expect(handleTokenRequest(grant(), ISSUER)).rejects.toMatchObject({ + code: 'invalid_grant', + }); + expect(revokeRefreshTokensForClientMock).toHaveBeenCalledWith(USER.id, CLIENT_ID); + }); + + test('rejects a mismatched resource indicator', async () => { + consumeAuthorizationCodeMock.mockResolvedValue({ code: codeRecord(), replayed: null }); + + await expect( + handleTokenRequest(grant({ resource: 'https://other.example/mcp' }), ISSUER), + ).rejects.toMatchObject({ code: 'invalid_target' }); + }); + + test('requires the required parameters', async () => { + await expect( + handleTokenRequest({ grant_type: 'authorization_code' }, ISSUER), + ).rejects.toMatchObject({ code: 'invalid_request' }); + await expect(handleTokenRequest({ grant_type: 'password' }, ISSUER)).rejects.toMatchObject({ + code: 'unsupported_grant_type', + }); + }); +}); + +describe('refresh_token grant', () => { + function refreshRecord(overrides: Record = {}) { + return { + id: 'rt-1', + tokenHash: 'h', + userId: USER.id, + clientId: CLIENT_ID, + scope: 'analytics:read websites:read', + resource: RESOURCE, + expiresAt: new Date(Date.now() + 60_000), + revokedAt: null, + lastUsedAt: null, + createdAt: new Date(), + ...overrides, + } as never; + } + + test('rotates the refresh token', async () => { + const presented = generateRefreshToken(); + getRefreshTokenByHashMock.mockResolvedValue(refreshRecord()); + + const tokens = await handleTokenRequest( + { grant_type: 'refresh_token', refresh_token: presented, client_id: CLIENT_ID }, + ISSUER, + ); + + expect(getRefreshTokenByHashMock).toHaveBeenCalledWith(hashToken(presented)); + expect(revokeRefreshTokenMock).toHaveBeenCalledWith('rt-1'); + expect(tokens.refresh_token).not.toBe(presented); + expect(tokens.scope).toBe('analytics:read websites:read'); + }); + + test('allows narrowing but not widening scope', async () => { + getRefreshTokenByHashMock.mockResolvedValue(refreshRecord({ scope: 'websites:read' })); + + await expect( + handleTokenRequest( + { + grant_type: 'refresh_token', + refresh_token: generateRefreshToken(), + scope: 'analytics:read', + }, + ISSUER, + ), + ).rejects.toMatchObject({ code: 'invalid_scope' }); + + getRefreshTokenByHashMock.mockResolvedValue(refreshRecord()); + const narrowed = await handleTokenRequest( + { + grant_type: 'refresh_token', + refresh_token: generateRefreshToken(), + scope: 'websites:read', + }, + ISSUER, + ); + + expect(narrowed.scope).toBe('websites:read'); + }); + + test('rejects revoked, expired, unknown and concurrently-used tokens', async () => { + getRefreshTokenByHashMock.mockResolvedValue(refreshRecord({ revokedAt: new Date() })); + await expect( + handleTokenRequest( + { grant_type: 'refresh_token', refresh_token: generateRefreshToken() }, + ISSUER, + ), + ).rejects.toMatchObject({ code: 'invalid_grant' }); + + getRefreshTokenByHashMock.mockResolvedValue( + refreshRecord({ expiresAt: new Date(Date.now() - 1) }), + ); + await expect( + handleTokenRequest( + { grant_type: 'refresh_token', refresh_token: generateRefreshToken() }, + ISSUER, + ), + ).rejects.toMatchObject({ code: 'invalid_grant' }); + + getRefreshTokenByHashMock.mockResolvedValue(null); + await expect( + handleTokenRequest( + { grant_type: 'refresh_token', refresh_token: generateRefreshToken() }, + ISSUER, + ), + ).rejects.toMatchObject({ code: 'invalid_grant' }); + + getRefreshTokenByHashMock.mockResolvedValue(refreshRecord()); + revokeRefreshTokenMock.mockResolvedValue(0); + await expect( + handleTokenRequest( + { grant_type: 'refresh_token', refresh_token: generateRefreshToken() }, + ISSUER, + ), + ).rejects.toMatchObject({ code: 'invalid_grant', message: /already been used/ }); + }); + + test('rejects a client mismatch', async () => { + getRefreshTokenByHashMock.mockResolvedValue(refreshRecord()); + + await expect( + handleTokenRequest( + { grant_type: 'refresh_token', refresh_token: generateRefreshToken(), client_id: 'other' }, + ISSUER, + ), + ).rejects.toMatchObject({ code: 'invalid_grant' }); + }); +}); + +describe('revocation', () => { + test('revokes known refresh tokens and ignores everything else', async () => { + getRefreshTokenByHashMock.mockResolvedValue({ id: 'rt-1', revokedAt: null } as never); + await handleRevocationRequest({ token: generateRefreshToken() }); + expect(revokeRefreshTokenMock).toHaveBeenCalledWith('rt-1'); + + revokeRefreshTokenMock.mockClear(); + await handleRevocationRequest({ token: 'not-a-refresh-token' }); + await handleRevocationRequest({ token: undefined }); + expect(revokeRefreshTokenMock).not.toHaveBeenCalled(); + }); +}); diff --git a/src/lib/oauth/token.ts b/src/lib/oauth/token.ts new file mode 100644 index 000000000..33edf0c91 --- /dev/null +++ b/src/lib/oauth/token.ts @@ -0,0 +1,240 @@ +import { uuid } from '@/lib/crypto'; +import { getMcpResource, normalizeResource, REFRESH_TOKEN_TTL_SECONDS } from '@/lib/oauth/config'; +import { OAuthError } from '@/lib/oauth/errors'; +import { verifyPkce } from '@/lib/oauth/pkce'; +import { normalizeScopes, parseScopes } from '@/lib/oauth/scopes'; +import { + createAccessToken, + generateRefreshToken, + hashToken, + isRefreshToken, +} from '@/lib/oauth/tokens'; +import { + consumeAuthorizationCode, + createRefreshToken, + getRefreshTokenByHash, + revokeRefreshToken, + revokeRefreshTokensForClient, + touchRefreshToken, +} from '@/queries/prisma/oauth'; +import { getUser } from '@/queries/prisma/user'; + +export interface TokenRequestParams { + grant_type?: unknown; + code?: unknown; + redirect_uri?: unknown; + client_id?: unknown; + code_verifier?: unknown; + refresh_token?: unknown; + scope?: unknown; + resource?: unknown; +} + +export interface TokenResponse { + access_token: string; + token_type: 'Bearer'; + expires_in: number; + refresh_token: string; + scope: string; +} + +function requireString(value: unknown, name: string, max = 4096) { + if (typeof value !== 'string' || !value || value.length > max) { + throw new OAuthError('invalid_request', `${name} is required.`); + } + + return value; +} + +async function loadUser(userId: string) { + const user = (await getUser(userId, { includePassword: true })) as { + id: string; + password?: string; + deletedAt?: Date | null; + } | null; + + if (!user?.id || user.deletedAt) { + throw new OAuthError('invalid_grant', 'The user account is no longer available.'); + } + + return user; +} + +async function issueTokens(options: { + userId: string; + clientId: string; + scopes: string[]; + resource: string; + issuer: string; + passwordHash?: string; +}): Promise { + const scopes = normalizeScopes(options.scopes); + const access = createAccessToken({ + userId: options.userId, + clientId: options.clientId, + scopes, + issuer: options.issuer, + resource: options.resource, + passwordHash: options.passwordHash, + }); + const refreshToken = generateRefreshToken(); + + await createRefreshToken({ + id: uuid(), + tokenHash: hashToken(refreshToken), + userId: options.userId, + clientId: options.clientId, + scope: scopes.join(' '), + resource: options.resource, + expiresAt: new Date(Date.now() + REFRESH_TOKEN_TTL_SECONDS * 1000), + }); + + return { + access_token: access.token, + token_type: 'Bearer', + expires_in: access.expiresIn, + refresh_token: refreshToken, + scope: scopes.join(' '), + }; +} + +export async function handleAuthorizationCodeGrant( + params: TokenRequestParams, + issuer: string, +): Promise { + const code = requireString(params.code, 'code', 512); + const clientId = requireString(params.client_id, 'client_id', 2048); + const codeVerifier = requireString(params.code_verifier, 'code_verifier', 128); + const { code: record, replayed } = await consumeAuthorizationCode(hashToken(code)); + + if (!record) { + if (replayed) { + // Replay of a redeemed code: revoke everything that code produced (OAuth 2.1 §4.1.2). + await revokeRefreshTokensForClient(replayed.userId, replayed.clientId); + } + + throw new OAuthError( + 'invalid_grant', + 'Authorization code is invalid, expired or already used.', + ); + } + + if (record.clientId !== clientId) { + throw new OAuthError('invalid_grant', 'client_id does not match the authorization code.'); + } + + const redirectUri = typeof params.redirect_uri === 'string' ? params.redirect_uri : undefined; + + if (redirectUri !== undefined && redirectUri !== record.redirectUri) { + throw new OAuthError('invalid_grant', 'redirect_uri does not match the authorization request.'); + } + + if (!verifyPkce(codeVerifier, record.codeChallenge, record.codeChallengeMethod)) { + throw new OAuthError('invalid_grant', 'PKCE verification failed.'); + } + + const expectedResource = record.resource ?? getMcpResource(issuer); + const resourceParam = + typeof params.resource === 'string' ? normalizeResource(params.resource) : null; + + if (params.resource !== undefined && resourceParam !== expectedResource) { + throw new OAuthError('invalid_target', `resource must be ${expectedResource}.`); + } + + const user = await loadUser(record.userId); + + return issueTokens({ + userId: user.id, + clientId: record.clientId, + scopes: parseScopes(record.scope), + resource: expectedResource, + issuer, + passwordHash: user.password, + }); +} + +export async function handleRefreshTokenGrant( + params: TokenRequestParams, + issuer: string, +): Promise { + const refreshToken = requireString(params.refresh_token, 'refresh_token', 512); + + if (!isRefreshToken(refreshToken)) { + throw new OAuthError('invalid_grant', 'Refresh token is invalid.'); + } + + const record = await getRefreshTokenByHash(hashToken(refreshToken)); + + if (!record || record.revokedAt || record.expiresAt.getTime() <= Date.now()) { + throw new OAuthError('invalid_grant', 'Refresh token is invalid, expired or revoked.'); + } + + if (params.client_id !== undefined && params.client_id !== record.clientId) { + throw new OAuthError('invalid_grant', 'client_id does not match the refresh token.'); + } + + const grantedScopes = normalizeScopes(parseScopes(record.scope)); + const requestedScopes = typeof params.scope === 'string' ? parseScopes(params.scope) : []; + const narrowed = requestedScopes.length ? normalizeScopes(requestedScopes) : grantedScopes; + + if (narrowed.some(scope => !grantedScopes.includes(scope))) { + throw new OAuthError('invalid_scope', 'Requested scope exceeds the granted scope.'); + } + + const expectedResource = record.resource ?? getMcpResource(issuer); + + if (params.resource !== undefined) { + const resourceParam = + typeof params.resource === 'string' ? normalizeResource(params.resource) : null; + + if (resourceParam !== expectedResource) { + throw new OAuthError('invalid_target', `resource must be ${expectedResource}.`); + } + } + + const user = await loadUser(record.userId); + + // Rotate: the presented refresh token is retired and a new one issued (OAuth 2.1 §4.3.1). + const revoked = await revokeRefreshToken(record.id); + + if (!revoked) { + throw new OAuthError('invalid_grant', 'Refresh token has already been used.'); + } + + await touchRefreshToken(record.id); + + return issueTokens({ + userId: user.id, + clientId: record.clientId, + scopes: narrowed, + resource: expectedResource, + issuer, + passwordHash: user.password, + }); +} + +export async function handleTokenRequest(params: TokenRequestParams, issuer: string) { + switch (params.grant_type) { + case 'authorization_code': + return handleAuthorizationCodeGrant(params, issuer); + case 'refresh_token': + return handleRefreshTokenGrant(params, issuer); + default: + throw new OAuthError( + 'unsupported_grant_type', + 'grant_type must be "authorization_code" or "refresh_token".', + ); + } +} + +export async function handleRevocationRequest(params: { token?: unknown }) { + // RFC 7009: unknown tokens are silently accepted. Only refresh tokens are revocable; + // access tokens are short-lived and expire on their own. + if (isRefreshToken(params.token)) { + const record = await getRefreshTokenByHash(hashToken(params.token)); + + if (record && !record.revokedAt) { + await revokeRefreshToken(record.id); + } + } +} diff --git a/src/lib/oauth/tokens.test.ts b/src/lib/oauth/tokens.test.ts new file mode 100644 index 000000000..3becd6a90 --- /dev/null +++ b/src/lib/oauth/tokens.test.ts @@ -0,0 +1,96 @@ +import { beforeAll, describe, expect, test } from 'vitest'; +import { + createAccessToken, + generateRefreshToken, + hashToken, + isRefreshToken, + looksLikeAccessToken, + verifyAccessToken, +} from './tokens'; + +const ISSUER = 'https://analytics.example.com'; +const RESOURCE = `${ISSUER}/mcp`; + +beforeAll(() => { + process.env.APP_SECRET = 'oauth-test-secret'; +}); + +describe('access tokens', () => { + test('round-trips claims and binds the audience to the issuer', () => { + const { token, jti, expiresIn } = createAccessToken({ + userId: 'user-1', + clientId: 'https://client.example/metadata.json', + scopes: ['websites:read', 'analytics:read', 'bogus'], + issuer: ISSUER, + resource: RESOURCE, + passwordHash: 'pw', + }); + + expect(expiresIn).toBe(3600); + expect(looksLikeAccessToken(token)).toBe(true); + + const verified = verifyAccessToken(token); + + expect(verified).toMatchObject({ + userId: 'user-1', + clientId: 'https://client.example/metadata.json', + scopes: ['analytics:read', 'websites:read'], + issuer: ISSUER, + resource: RESOURCE, + tokenId: jti, + }); + expect(verified?.passwordFingerprint).toBeDefined(); + expect(verified?.expiresAt).toBeGreaterThan(Date.now()); + }); + + test('rejects tokens whose audience is not the issuer MCP resource', () => { + const { token } = createAccessToken({ + userId: 'user-1', + clientId: 'c', + scopes: ['websites:read'], + issuer: ISSUER, + resource: 'https://other.example/mcp', + }); + + expect(verifyAccessToken(token)).toBeNull(); + }); + + test('rejects expired tokens', () => { + const { token } = createAccessToken({ + userId: 'user-1', + clientId: 'c', + scopes: ['websites:read'], + issuer: ISSUER, + resource: RESOURCE, + expiresInSeconds: -10, + }); + + expect(verifyAccessToken(token)).toBeNull(); + }); + + test('rejects tampered and foreign tokens', () => { + const { token } = createAccessToken({ + userId: 'user-1', + clientId: 'c', + scopes: ['websites:read'], + issuer: ISSUER, + resource: RESOURCE, + }); + const [header, payload] = token.split('.'); + + expect(verifyAccessToken(`${header}.${payload}.invalid`)).toBeNull(); + expect(looksLikeAccessToken('umami_abc')).toBe(false); + expect(looksLikeAccessToken('a.b')).toBe(false); + }); +}); + +describe('refresh tokens', () => { + test('generates prefixed tokens and stable hashes', () => { + const token = generateRefreshToken(); + + expect(isRefreshToken(token)).toBe(true); + expect(isRefreshToken('nope')).toBe(false); + expect(hashToken(token)).toBe(hashToken(token)); + expect(hashToken(token)).not.toContain(token); + }); +}); diff --git a/src/lib/oauth/tokens.ts b/src/lib/oauth/tokens.ts new file mode 100644 index 000000000..0ca0457b4 --- /dev/null +++ b/src/lib/oauth/tokens.ts @@ -0,0 +1,138 @@ +import { hash, secret, uuid } from '@/lib/crypto'; +import { createToken, parseToken } from '@/lib/jwt'; +import { + ACCESS_TOKEN_TTL_SECONDS, + getMcpResource, + OAUTH_ACCESS_TOKEN_TYPE, + REFRESH_TOKEN_PREFIX, +} from '@/lib/oauth/config'; +import { generateSecret } from '@/lib/oauth/pkce'; +import { normalizeScopes, type OAuthScope } from '@/lib/oauth/scopes'; + +export interface AccessTokenClaims { + /** User ID. */ + sub: string; + iss: string; + aud: string; + client_id: string; + scope: string; + type: typeof OAUTH_ACCESS_TOKEN_TYPE; + jti: string; + /** Password fingerprint so that a password change invalidates outstanding tokens. */ + pwd?: string; + iat: number; + exp: number; +} + +export interface CreateAccessTokenOptions { + userId: string; + clientId: string; + scopes: readonly string[]; + issuer: string; + resource: string; + passwordHash?: string; + expiresInSeconds?: number; +} + +export function createAccessToken(options: CreateAccessTokenOptions) { + const expiresIn = options.expiresInSeconds ?? ACCESS_TOKEN_TTL_SECONDS; + const jti = uuid(); + const token = createToken( + { + sub: options.userId, + iss: options.issuer, + aud: options.resource, + client_id: options.clientId, + scope: normalizeScopes([...options.scopes]).join(' '), + type: OAUTH_ACCESS_TOKEN_TYPE, + jti, + ...(options.passwordHash ? { pwd: hash(options.passwordHash) } : {}), + }, + secret(), + { expiresIn }, + ) as string; + + return { token, jti, expiresIn }; +} + +export interface VerifiedAccessToken { + userId: string; + clientId: string; + scopes: OAuthScope[]; + issuer: string; + resource: string; + tokenId: string; + passwordFingerprint?: string; + expiresAt: number; +} + +/** + * Cheap structural check so that ordinary session tokens (encrypted, not plain JWTs) and API + * keys can skip OAuth verification entirely. + */ +export function looksLikeAccessToken(token?: string | null): token is string { + if (!token || token.split('.').length !== 3) { + return false; + } + + try { + const payload = JSON.parse(Buffer.from(token.split('.')[1], 'base64url').toString('utf8')); + + return payload?.type === OAUTH_ACCESS_TOKEN_TYPE; + } catch { + return false; + } +} + +/** + * Verifies signature, expiry, token type and audience binding. The audience must be the MCP + * resource of the issuer that minted the token, so a token can never be replayed against a + * different Umami deployment sharing the same secret. + */ +export function verifyAccessToken(token: string): VerifiedAccessToken | null { + const payload = parseToken(token, secret()) as Partial | null; + + if ( + !payload || + payload.type !== OAUTH_ACCESS_TOKEN_TYPE || + typeof payload.sub !== 'string' || + typeof payload.client_id !== 'string' || + typeof payload.iss !== 'string' || + typeof payload.aud !== 'string' || + typeof payload.jti !== 'string' || + typeof payload.exp !== 'number' + ) { + return null; + } + + if (payload.aud !== getMcpResource(payload.iss)) { + return null; + } + + return { + userId: payload.sub, + clientId: payload.client_id, + scopes: normalizeScopes((payload.scope ?? '').split(' ')), + issuer: payload.iss, + resource: payload.aud, + tokenId: payload.jti, + passwordFingerprint: payload.pwd, + expiresAt: payload.exp * 1000, + }; +} + +export function generateRefreshToken() { + return `${REFRESH_TOKEN_PREFIX}${generateSecret(32)}`; +} + +export function isRefreshToken(value: unknown): value is string { + return typeof value === 'string' && value.startsWith(REFRESH_TOKEN_PREFIX); +} + +export function hashToken(value: string) { + return hash(value); +} + +export function generateAuthorizationCode() { + return generateSecret(32); +} diff --git a/src/lib/oauth/verify.test.ts b/src/lib/oauth/verify.test.ts new file mode 100644 index 000000000..ebcb5847b --- /dev/null +++ b/src/lib/oauth/verify.test.ts @@ -0,0 +1,109 @@ +import { beforeEach, describe, expect, test, vi } from 'vitest'; +import { hash } from '@/lib/crypto'; +import { getUser } from '@/queries/prisma/user'; +import { createAccessToken } from './tokens'; +import { verifyOAuthRequest } from './verify'; + +vi.mock('@/queries/prisma/user', () => ({ + getUser: vi.fn(), +})); + +const getUserMock = vi.mocked(getUser); +const ISSUER = 'https://analytics.example.com'; +const USER = { id: 'user-1', username: 'admin', role: 'user', password: 'pw-hash' }; + +function token(scopes: string[], overrides: Record = {}) { + return createAccessToken({ + userId: USER.id, + clientId: 'https://app.example.com/client.json', + scopes, + issuer: ISSUER, + resource: `${ISSUER}/mcp`, + passwordHash: USER.password, + ...overrides, + }).token; +} + +beforeEach(() => { + process.env.APP_SECRET = 'test'; + delete process.env.OAUTH_DISABLED; + getUserMock.mockReset(); + getUserMock.mockResolvedValue({ ...USER } as never); +}); + +describe('verifyOAuthRequest', () => { + test('ignores non-OAuth tokens', async () => { + await expect(verifyOAuthRequest('umami_key', 'GET', '/api/websites')).resolves.toEqual({ + status: 'not-oauth', + }); + await expect(verifyOAuthRequest(undefined, 'GET', '/api/websites')).resolves.toEqual({ + status: 'not-oauth', + }); + }); + + test('builds an OAuth auth context on an allowlisted route', async () => { + const result = await verifyOAuthRequest( + token(['websites:read', 'analytics:read']), + 'GET', + '/api/websites/abc/stats', + ); + + expect(result.status).toBe('ok'); + + if (result.status === 'ok') { + expect(result.auth.user).toMatchObject({ id: USER.id, isAdmin: false }); + expect(result.auth.user).not.toHaveProperty('password'); + expect(result.auth.authType).toBe('oauth'); + expect(result.auth.oauth).toMatchObject({ + clientId: 'https://app.example.com/client.json', + scopes: ['analytics:read', 'websites:read'], + }); + } + }); + + test('rejects routes that did not opt in to OAuth', async () => { + await expect( + verifyOAuthRequest(token(['websites:read', 'analytics:read']), 'POST', '/api/websites'), + ).resolves.toMatchObject({ status: 'forbidden', reason: 'route-not-allowed' }); + await expect( + verifyOAuthRequest(token(['websites:read', 'analytics:read']), 'GET', '/api/me/api-keys'), + ).resolves.toMatchObject({ status: 'forbidden', reason: 'route-not-allowed' }); + await expect( + verifyOAuthRequest(token(['websites:read', 'analytics:read']), 'DELETE', '/api/websites/abc'), + ).resolves.toMatchObject({ status: 'forbidden' }); + }); + + test('rejects insufficient scope', async () => { + await expect( + verifyOAuthRequest(token(['websites:read']), 'GET', '/api/websites/abc/stats'), + ).resolves.toMatchObject({ + status: 'forbidden', + reason: 'insufficient-scope', + requiredScope: 'analytics:read', + }); + }); + + test('rejects tokens after a password change or when the user is gone', async () => { + getUserMock.mockResolvedValue({ ...USER, password: 'changed' } as never); + await expect( + verifyOAuthRequest(token(['websites:read']), 'GET', '/api/websites'), + ).resolves.toEqual({ status: 'invalid' }); + + getUserMock.mockResolvedValue(null as never); + await expect( + verifyOAuthRequest(token(['websites:read']), 'GET', '/api/websites'), + ).resolves.toEqual({ status: 'invalid' }); + }); + + test('rejects everything when OAuth is disabled', async () => { + process.env.OAUTH_DISABLED = '1'; + + await expect( + verifyOAuthRequest(token(['websites:read']), 'GET', '/api/websites'), + ).resolves.toEqual({ status: 'invalid' }); + }); + + test('password fingerprint uses the hashed password', () => { + expect(hash('pw-hash')).not.toBe('pw-hash'); + }); +}); diff --git a/src/lib/oauth/verify.ts b/src/lib/oauth/verify.ts new file mode 100644 index 000000000..a0f82602d --- /dev/null +++ b/src/lib/oauth/verify.ts @@ -0,0 +1,95 @@ +import debug from 'debug'; +import { ROLES } from '@/lib/constants'; +import { hash } from '@/lib/crypto'; +import { isOAuthEnabled } from '@/lib/oauth/config'; +import { getRouteOAuthScope, hasRequiredScope } from '@/lib/oauth/scopes'; +import { + looksLikeAccessToken, + type VerifiedAccessToken, + verifyAccessToken, +} from '@/lib/oauth/tokens'; +import type { Auth } from '@/lib/types'; +import { getUser } from '@/queries/prisma/user'; + +const log = debug('umami:oauth'); + +export type OAuthAuthResult = + | { status: 'not-oauth' } + | { status: 'invalid' } + | { + status: 'forbidden'; + reason: 'route-not-allowed' | 'insufficient-scope'; + requiredScope?: string; + } + | { status: 'ok'; auth: Auth & { token: string } }; + +/** + * Resolves an OAuth access token into an ordinary Umami `Auth` context. + * + * OAuth tokens are only accepted on routes listed in `OAUTH_ROUTE_SCOPES` and only when the + * token carries the scope that route requires. The resulting user context is subject to all + * normal Umami permission checks; scopes never widen access. + */ +export async function verifyOAuthRequest( + token: string | undefined, + method: string, + pathname: string, +): Promise { + if (!looksLikeAccessToken(token)) { + return { status: 'not-oauth' }; + } + + if (!isOAuthEnabled()) { + return { status: 'invalid' }; + } + + const verified = verifyAccessToken(token); + + if (!verified) { + log('invalid OAuth access token'); + return { status: 'invalid' }; + } + + const requiredScope = getRouteOAuthScope(method, pathname); + + if (!requiredScope) { + log('OAuth token used on non-allowlisted route', method, pathname); + return { status: 'forbidden', reason: 'route-not-allowed' }; + } + + if (!hasRequiredScope(verified.scopes, requiredScope)) { + return { status: 'forbidden', reason: 'insufficient-scope', requiredScope }; + } + + const auth = await buildOAuthAuth(token, verified); + + return auth ? { status: 'ok', auth } : { status: 'invalid' }; +} + +export async function buildOAuthAuth(token: string, verified: VerifiedAccessToken) { + const user: any = await getUser(verified.userId, { includePassword: true }); + + if (!user?.id || user.deletedAt) { + log('OAuth token user not found'); + return null; + } + + if (verified.passwordFingerprint && hash(user.password) !== verified.passwordFingerprint) { + log('OAuth token issued before password change'); + return null; + } + + delete user.password; + user.isAdmin = user.role === ROLES.admin; + + return { + token, + user, + authType: 'oauth' as const, + oauth: { + clientId: verified.clientId, + scopes: verified.scopes, + tokenId: verified.tokenId, + }, + }; +} diff --git a/src/lib/return-url.ts b/src/lib/return-url.ts new file mode 100644 index 000000000..632c76b21 --- /dev/null +++ b/src/lib/return-url.ts @@ -0,0 +1,28 @@ +import { getItem, removeItem, setItem } from '@/lib/storage'; + +const RETURN_URL = 'umami.return-url'; + +/** Only same-origin relative paths are accepted to prevent open redirects. */ +export function isSafeReturnUrl(value: unknown): value is string { + return ( + typeof value === 'string' && + value.startsWith('/') && + !value.startsWith('//') && + !value.startsWith('/\\') && + !/[\r\n]/.test(value) + ); +} + +export function setReturnUrl(url: string) { + if (isSafeReturnUrl(url)) { + setItem(RETURN_URL, url, true); + } +} + +export function consumeReturnUrl(): string | null { + const value = getItem(RETURN_URL, true); + + removeItem(RETURN_URL, true); + + return isSafeReturnUrl(value) ? value : null; +} diff --git a/src/lib/types.ts b/src/lib/types.ts index d6146ea11..be155c7b5 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -12,6 +12,8 @@ export type Role = ObjectValues; export type DynamicDataType = ObjectValues; export type Operator = (typeof OPERATORS)[keyof typeof OPERATORS]; +export type AuthType = 'session' | 'api-key' | 'oauth' | 'share'; + export interface Auth { user?: { id: string; @@ -23,6 +25,14 @@ export interface Auth { id: string; name: string; }; + /** How the request was authenticated. Undefined for legacy callers that build Auth manually. */ + authType?: AuthType; + /** OAuth details when `authType` is `oauth`. Scopes never expand Umami permissions. */ + oauth?: { + clientId: string; + scopes: string[]; + tokenId?: string; + }; shareToken?: { shareType?: number; websiteId?: string; diff --git a/src/openapi/README.md b/src/openapi/README.md index d69bf7e25..0c355641c 100644 --- a/src/openapi/README.md +++ b/src/openapi/README.md @@ -45,3 +45,23 @@ inference layer extracts path parameters, Zod request fields, authentication, ty response status helpers, and response media types without importing route modules. Running `pnpm openapi:contracts` snapshots that behavior into an explicit contract. Curated `contract.ts` modules remain the way to add examples, custom operation IDs, and hand-written prose. + +## Operation IDs + +Operation IDs are public: they become method names in `@umami/api-client`. Routes with a curated +`contract.ts` set `operationId` directly. For inferred contracts, `src/openapi/operation-ids.ts` +overrides the mechanical path-based name (`getWebsitesWebsiteIdStats` -> `getWebsiteStats`). +Never rename an operation ID without a migration note for the client. + +## OAuth scopes + +`src/lib/oauth/scopes.ts` is the single allowlist of routes that accept OAuth access tokens and +the scope each requires. The document builder adds `x-umami-oauth-scope` and an `oauth2` security +requirement to those operations, and fails when an allowlist entry does not match a real route. +The same list is enforced at runtime by `checkAuth`, and mirrored by the in-process MCP dispatch +table (`src/lib/mcp/dispatch.ts`). + +## Generated API client + +`pnpm generate:api` regenerates the OpenAPI document and `packages/api-client/src/generated`. +`pnpm check:api-client` fails in CI when the generated client is stale. diff --git a/src/openapi/document.ts b/src/openapi/document.ts index 783172a00..62ca4a9c7 100644 --- a/src/openapi/document.ts +++ b/src/openapi/document.ts @@ -1,6 +1,7 @@ import { readFile } from 'node:fs/promises'; import path from 'node:path'; import { createDocument, type ZodOpenApiPathsObject } from 'zod-openapi'; +import { getContractOAuthScope, OAUTH_ROUTE_SCOPES } from '@/lib/oauth/scopes'; import { loadApiContracts } from '@/openapi/contracts'; import { analyzeContractCoverage, @@ -9,7 +10,7 @@ import { } from '@/openapi/coverage'; import { discoverApiOperations } from '@/openapi/discover'; import { inferApiContracts } from '@/openapi/infer'; -import type { ApiAudience } from '@/openapi/operation'; +import { type ApiAudience, type ApiOperationContract, getOperationKey } from '@/openapi/operation'; import { errorResponseComponents } from '@/openapi/schemas'; import { getSecurityRequirements, securitySchemes } from '@/openapi/security'; @@ -28,6 +29,19 @@ async function getPackageVersion(projectRoot: string) { return packageJson.version; } +/** + * Every OAuth-allowlisted route must correspond to a real API route, otherwise the runtime + * allowlist and the published contract silently drift apart. + */ +function getOAuthRouteErrors(discovered: Pick[]) { + const discoveredKeys = new Set(discovered.map(getOperationKey)); + + return OAUTH_ROUTE_SCOPES.filter(route => !discoveredKeys.has(getOperationKey(route))).map( + route => + `OAuth scope allowlist entry does not match a route: ${getOperationKey(route)} (src/lib/oauth/scopes.ts)`, + ); +} + function getDocumentTags(paths: ZodOpenApiPathsObject) { const names = new Set(); @@ -56,7 +70,7 @@ export async function buildOpenApiDocument( const inferredContracts = inferApiContracts(discovered, explicitContracts); const contracts = [...explicitContracts, ...inferredContracts]; const coverage = analyzeContractCoverage(discovered, explicitContracts, contracts); - const errors = getCoverageErrors(coverage); + const errors = [...getCoverageErrors(coverage), ...getOAuthRouteErrors(discovered)]; if (errors.length) { throw new Error(`Invalid OpenAPI contracts:\n${errors.map(error => `- ${error}`).join('\n')}`); @@ -77,12 +91,14 @@ export async function buildOpenApiDocument( paths[contract.path] = pathItem; } + const scope = getContractOAuthScope(contract.method, contract.path); const operation = { ...contract.operation, - security: getSecurityRequirements(contract.auth), + security: getSecurityRequirements(contract.auth, scope), 'x-umami-audience': contract.audience, 'x-umami-contract': contract.origin, 'x-umami-source': contract.source, + ...(scope ? { 'x-umami-oauth-scope': scope } : {}), }; Object.assign(pathItem, { [contract.method]: operation }); diff --git a/src/openapi/infer.ts b/src/openapi/infer.ts index 13fcf9cf0..9501796f1 100644 --- a/src/openapi/infer.ts +++ b/src/openapi/infer.ts @@ -8,6 +8,7 @@ import type { import type { DiscoveredApiOperation } from '@/openapi/discover'; import type { ApiAudience, ApiHttpMethod, LoadedApiOperationContract } from '@/openapi/operation'; import { getOperationKey } from '@/openapi/operation'; +import { getOperationIdOverride } from '@/openapi/operation-ids'; import { badRequestResponse, forbiddenResponse, @@ -204,7 +205,7 @@ function getOperation(operation: DiscoveredApiOperation): ZodOpenApiOperationObj : undefined; return { - operationId: getOperationId(method, path), + operationId: getOperationIdOverride(method, path) ?? getOperationId(method, path), summary: getSummary(method, path), description: 'Generated from the App Router handler and its request-validation source. Add a colocated contract.ts to supply exact response models and curated documentation.', diff --git a/src/openapi/operation-ids.ts b/src/openapi/operation-ids.ts new file mode 100644 index 000000000..cf6ebabe1 --- /dev/null +++ b/src/openapi/operation-ids.ts @@ -0,0 +1,198 @@ +/** + * Canonical operation IDs for routes that rely on source-inferred contracts. + * + * Operation IDs become method names in the generated `@umami/api-client`, so they are part of + * the public contract. Entries here override the mechanical path-segment naming used by + * `src/openapi/infer.ts`. Curated `contract.ts` modules set their own `operationId` directly. + * + * Keys are `${METHOD} ${path}` as produced by `getOperationKey`. + */ +export const OPERATION_ID_OVERRIDES: Record = { + // Account + 'GET /api/me/api-keys': 'getMyApiKeys', + 'POST /api/me/api-keys': 'createMyApiKey', + 'DELETE /api/me/api-keys/{keyId}': 'deleteMyApiKey', + 'POST /api/me/password': 'updateMyPassword', + 'GET /api/me/teams': 'getMyTeams', + 'GET /api/me/websites': 'getMyWebsites', + + // Auth + 'POST /api/auth/logout': 'logout', + 'POST /api/auth/sso': 'sso', + 'GET /api/auth/subscription': 'getSubscription', + 'POST /api/auth/verify': 'verify', + + // Two-factor authentication + 'GET /api/2fa/status': 'getTwoFactorStatus', + 'POST /api/2fa/setup/initiate': 'initiateTwoFactorSetup', + 'POST /api/2fa/setup/confirm': 'confirmTwoFactorSetup', + 'POST /api/2fa/setup/cancel': 'cancelTwoFactorSetup', + 'POST /api/2fa/disable': 'disableTwoFactor', + 'POST /api/2fa/verify': 'verifyTwoFactor', + + // Realtime + 'GET /api/realtime/{websiteId}': 'getRealtime', + + // Reports + 'GET /api/reports': 'getReports', + 'POST /api/reports': 'createReport', + 'GET /api/reports/{reportId}': 'getReport', + 'POST /api/reports/{reportId}': 'updateReport', + 'DELETE /api/reports/{reportId}': 'deleteReport', + 'POST /api/reports/attribution': 'runAttributionReport', + 'POST /api/reports/breakdown': 'runBreakdownReport', + 'POST /api/reports/funnel': 'runFunnelReport', + 'POST /api/reports/goal': 'runGoalReport', + 'POST /api/reports/heatmap': 'runHeatmapReport', + 'POST /api/reports/journey': 'runJourneyReport', + 'POST /api/reports/performance': 'runPerformanceReport', + 'POST /api/reports/retention': 'runRetentionReport', + 'POST /api/reports/revenue': 'runRevenueReport', + 'POST /api/reports/utm': 'runUtmReport', + + // Teams + 'POST /api/teams': 'createTeam', + 'POST /api/teams/join': 'joinTeam', + 'GET /api/teams/{teamId}': 'getTeam', + 'POST /api/teams/{teamId}': 'updateTeam', + 'DELETE /api/teams/{teamId}': 'deleteTeam', + 'GET /api/teams/{teamId}/users': 'getTeamUsers', + 'POST /api/teams/{teamId}/users': 'createTeamUser', + 'GET /api/teams/{teamId}/users/{userId}': 'getTeamUser', + 'POST /api/teams/{teamId}/users/{userId}': 'updateTeamUser', + 'DELETE /api/teams/{teamId}/users/{userId}': 'deleteTeamUser', + 'GET /api/teams/{teamId}/websites': 'getTeamWebsites', + 'GET /api/teams/{teamId}/boards': 'getTeamBoards', + 'GET /api/teams/{teamId}/links': 'getTeamLinks', + 'GET /api/teams/{teamId}/pixels': 'getTeamPixels', + + // Users + 'POST /api/users': 'createUser', + 'GET /api/users/{userId}': 'getUser', + 'POST /api/users/{userId}': 'updateUser', + 'DELETE /api/users/{userId}': 'deleteUser', + 'GET /api/users/{userId}/teams': 'getUserTeams', + 'GET /api/users/{userId}/websites': 'getUserWebsites', + + // Boards + 'POST /api/boards': 'createBoard', + 'GET /api/boards/{boardId}': 'getBoard', + 'POST /api/boards/{boardId}': 'updateBoard', + 'DELETE /api/boards/{boardId}': 'deleteBoard', + 'POST /api/boards/{boardId}/clone': 'cloneBoard', + 'GET /api/boards/{boardId}/shares': 'getBoardShares', + 'POST /api/boards/{boardId}/shares': 'createBoardShare', + + // Links + 'POST /api/links': 'createLink', + 'GET /api/links/{linkId}': 'getLink', + 'POST /api/links/{linkId}': 'updateLink', + 'DELETE /api/links/{linkId}': 'deleteLink', + 'GET /api/links/{linkId}/shares': 'getLinkShares', + 'POST /api/links/{linkId}/shares': 'createLinkShare', + + // Pixels + 'POST /api/pixels': 'createPixel', + 'GET /api/pixels/{pixelId}': 'getPixel', + 'POST /api/pixels/{pixelId}': 'updatePixel', + 'DELETE /api/pixels/{pixelId}': 'deletePixel', + 'GET /api/pixels/{pixelId}/shares': 'getPixelShares', + 'POST /api/pixels/{pixelId}/shares': 'createPixelShare', + + // Shares + 'POST /api/share': 'createShare', + 'GET /api/share/{slug}': 'getShareBySlug', + 'GET /api/share/id/{shareId}': 'getShare', + 'POST /api/share/id/{shareId}': 'updateShare', + 'DELETE /api/share/id/{shareId}': 'deleteShare', + + // Websites + 'GET /api/websites/charts': 'getWebsitesCharts', + 'GET /api/websites/{websiteId}/active': 'getWebsiteActive', + 'GET /api/websites/{websiteId}/daterange': 'getWebsiteDateRange', + 'GET /api/websites/{websiteId}/stats': 'getWebsiteStats', + 'GET /api/websites/{websiteId}/pageviews': 'getWebsitePageviews', + 'GET /api/websites/{websiteId}/metrics': 'getWebsiteMetrics', + 'GET /api/websites/{websiteId}/metrics/expanded': 'getWebsiteExpandedMetrics', + 'GET /api/websites/{websiteId}/values': 'getWebsiteValues', + 'GET /api/websites/{websiteId}/export': 'exportWebsite', + 'POST /api/websites/{websiteId}/reset': 'resetWebsite', + 'POST /api/websites/{websiteId}/transfer': 'transferWebsite', + 'GET /api/websites/{websiteId}/reports': 'getWebsiteReports', + 'GET /api/websites/{websiteId}/shares': 'getWebsiteShares', + 'POST /api/websites/{websiteId}/shares': 'createWebsiteShare', + 'GET /api/websites/{websiteId}/recorder': 'getWebsiteRecorderConfig', + + // Annotations + 'GET /api/websites/{websiteId}/annotations': 'getWebsiteAnnotations', + 'POST /api/websites/{websiteId}/annotations': 'createWebsiteAnnotation', + 'GET /api/websites/{websiteId}/annotations/{annotationId}': 'getWebsiteAnnotation', + 'POST /api/websites/{websiteId}/annotations/{annotationId}': 'updateWebsiteAnnotation', + 'DELETE /api/websites/{websiteId}/annotations/{annotationId}': 'deleteWebsiteAnnotation', + + // Segments + 'GET /api/websites/{websiteId}/segments': 'getWebsiteSegments', + 'POST /api/websites/{websiteId}/segments': 'createWebsiteSegment', + 'GET /api/websites/{websiteId}/segments/{segmentId}': 'getWebsiteSegment', + 'POST /api/websites/{websiteId}/segments/{segmentId}': 'updateWebsiteSegment', + 'DELETE /api/websites/{websiteId}/segments/{segmentId}': 'deleteWebsiteSegment', + + // Events + 'GET /api/websites/{websiteId}/events': 'getWebsiteEvents', + 'GET /api/websites/{websiteId}/events/stats': 'getWebsiteEventStats', + 'GET /api/websites/{websiteId}/events/series': 'getWebsiteEventSeries', + 'GET /api/websites/{websiteId}/event-data': 'getEventData', + 'GET /api/websites/{websiteId}/event-data/{eventId}': 'getEventDataById', + 'GET /api/websites/{websiteId}/event-data/events': 'getEventDataEvents', + 'GET /api/websites/{websiteId}/event-data/fields': 'getEventDataFields', + 'GET /api/websites/{websiteId}/event-data/properties': 'getEventDataProperties', + 'GET /api/websites/{websiteId}/event-data/stats': 'getEventDataStats', + 'GET /api/websites/{websiteId}/event-data/values': 'getEventDataValues', + 'GET /api/websites/{websiteId}/event-data-pivot': 'getEventDataPivot', + 'GET /api/websites/{websiteId}/event-data-pivot/array-series': 'getEventDataArraySeries', + 'GET /api/websites/{websiteId}/event-data-pivot/date-series': 'getEventDataDateSeries', + 'GET /api/websites/{websiteId}/event-data-pivot/numeric-series': 'getEventDataNumericSeries', + 'GET /api/websites/{websiteId}/event-data-pivot/numeric-stats': 'getEventDataNumericStats', + 'GET /api/websites/{websiteId}/event-data-pivot/property-series': 'getEventDataPropertySeries', + + // Sessions + 'GET /api/websites/{websiteId}/sessions': 'getWebsiteSessions', + 'GET /api/websites/{websiteId}/sessions/stats': 'getWebsiteSessionStats', + 'GET /api/websites/{websiteId}/sessions/weekly': 'getWebsiteSessionsWeekly', + 'GET /api/websites/{websiteId}/sessions/{sessionId}': 'getWebsiteSession', + 'DELETE /api/websites/{websiteId}/sessions/{sessionId}': 'deleteWebsiteSession', + 'GET /api/websites/{websiteId}/sessions/{sessionId}/activity': 'getWebsiteSessionActivity', + 'GET /api/websites/{websiteId}/sessions/{sessionId}/properties': 'getWebsiteSessionProperties', + 'GET /api/websites/{websiteId}/sessions/{sessionId}/replays': 'getWebsiteSessionReplays', + 'GET /api/websites/{websiteId}/session-data-pivot': 'getSessionDataPivot', + 'GET /api/websites/{websiteId}/session-data/array-series': 'getSessionDataArraySeries', + 'GET /api/websites/{websiteId}/session-data/date-series': 'getSessionDataDateSeries', + 'GET /api/websites/{websiteId}/session-data/numeric-series': 'getSessionDataNumericSeries', + 'GET /api/websites/{websiteId}/session-data/numeric-stats': 'getSessionDataNumericStats', + 'GET /api/websites/{websiteId}/session-data/properties': 'getSessionDataProperties', + 'GET /api/websites/{websiteId}/session-data/property-series': 'getSessionDataPropertySeries', + 'GET /api/websites/{websiteId}/session-data/stats': 'getSessionDataStats', + 'GET /api/websites/{websiteId}/session-data/values': 'getSessionDataValues', + + // Replays + 'GET /api/websites/{websiteId}/replays': 'getWebsiteReplays', + 'GET /api/websites/{websiteId}/replays/{replayId}': 'getWebsiteReplay', + 'GET /api/websites/{websiteId}/replays/saved': 'getWebsiteSavedReplays', + 'GET /api/websites/{websiteId}/replays/saved/{replayId}': 'getWebsiteReplaySaved', + 'POST /api/websites/{websiteId}/replays/saved/{replayId}': 'saveWebsiteReplay', + + // Revenue + 'GET /api/websites/{websiteId}/revenue/chart': 'getWebsiteRevenueChart', + 'GET /api/websites/{websiteId}/revenue/metrics': 'getWebsiteRevenueMetrics', + 'GET /api/websites/{websiteId}/revenue/sessions': 'getWebsiteRevenueSessions', + 'GET /api/websites/{websiteId}/revenue/stats': 'getWebsiteRevenueStats', + + // Collection + 'POST /api/send': 'send', + 'POST /api/batch': 'batch', + 'POST /api/record': 'record', +}; + +export function getOperationIdOverride(method: string, path: string) { + return OPERATION_ID_OVERRIDES[`${method.toUpperCase()} ${path}`]; +} diff --git a/src/openapi/security.ts b/src/openapi/security.ts index 1f00e35e4..2a2d67741 100644 --- a/src/openapi/security.ts +++ b/src/openapi/security.ts @@ -1,3 +1,4 @@ +import { OAUTH_SCOPE_DESCRIPTIONS, type OAuthScope } from '@/lib/oauth/scopes'; import type { ApiAuth } from '@/openapi/operation'; export const securitySchemes = { @@ -8,6 +9,19 @@ export const securitySchemes = { description: 'Token returned by POST /api/auth/login, or an API key (`umami_…`) created under Settings → API keys (self-hosted only).', }, + oauth2: { + type: 'oauth2' as const, + description: + 'OAuth 2.1 access token (authorization code + PKCE). Only operations that declare an OAuth scope accept these tokens.', + flows: { + authorizationCode: { + authorizationUrl: '/oauth/authorize', + tokenUrl: '/api/oauth/token', + refreshUrl: '/api/oauth/token', + scopes: OAUTH_SCOPE_DESCRIPTIONS, + }, + }, + }, shareToken: { type: 'apiKey' as const, in: 'header' as const, @@ -22,14 +36,20 @@ export const securitySchemes = { }, }; -export function getSecurityRequirements(auth: ApiAuth) { +export function getSecurityRequirements(auth: ApiAuth, scope?: OAuthScope | null) { if (auth === 'none') { return []; } - if (auth === 'bearer-or-share') { - return [{ bearerAuth: [] }, { shareToken: [], shareContext: [] }]; + const requirements: Record[] = [{ bearerAuth: [] }]; + + if (scope) { + requirements.push({ oauth2: [scope] }); } - return [{ bearerAuth: [] }]; + if (auth === 'bearer-or-share') { + requirements.push({ shareToken: [], shareContext: [] }); + } + + return requirements; } diff --git a/src/queries/prisma/oauth.ts b/src/queries/prisma/oauth.ts new file mode 100644 index 000000000..fcee957d0 --- /dev/null +++ b/src/queries/prisma/oauth.ts @@ -0,0 +1,112 @@ +import prisma from '@/lib/prisma'; + +export async function createOauthClient(data: { + id: string; + name: string; + redirectUris: string[]; + metadata?: Record; +}) { + return prisma.client.oauthClient.create({ + data: { + id: data.id, + name: data.name, + redirectUris: data.redirectUris, + metadata: data.metadata as any, + }, + }); +} + +export async function getOauthClient(id: string) { + return prisma.client.oauthClient.findUnique({ where: { id } }); +} + +export async function createAuthorizationCode(data: { + id: string; + codeHash: string; + userId: string; + clientId: string; + redirectUri: string; + scope: string; + resource?: string | null; + codeChallenge: string; + codeChallengeMethod: string; + expiresAt: Date; +}) { + return prisma.client.oauthAuthorizationCode.create({ data }); +} + +/** + * Atomically marks a code as used and returns it, or `null` when the code is unknown, expired or + * has already been redeemed. Reuse of a code is a replay signal, handled by the caller. + */ +export async function consumeAuthorizationCode(codeHash: string) { + const now = new Date(); + const { count } = await prisma.client.oauthAuthorizationCode.updateMany({ + where: { codeHash, usedAt: null, expiresAt: { gt: now } }, + data: { usedAt: now }, + }); + + const code = await prisma.client.oauthAuthorizationCode.findUnique({ where: { codeHash } }); + + if (!code) { + return { code: null, replayed: null }; + } + + if (count === 1) { + return { code, replayed: null }; + } + + // Code exists but was not consumable: either expired or already redeemed (replay). + return { code: null, replayed: code.usedAt && code.usedAt < new Date() ? code : null }; +} + +export async function createRefreshToken(data: { + id: string; + tokenHash: string; + userId: string; + clientId: string; + scope: string; + resource?: string | null; + expiresAt: Date; +}) { + return prisma.client.oauthRefreshToken.create({ data }); +} + +export async function getRefreshTokenByHash(tokenHash: string) { + return prisma.client.oauthRefreshToken.findUnique({ where: { tokenHash } }); +} + +export async function revokeRefreshToken(id: string) { + const { count } = await prisma.client.oauthRefreshToken.updateMany({ + where: { id, revokedAt: null }, + data: { revokedAt: new Date() }, + }); + + return count; +} + +export async function revokeRefreshTokensForClient(userId: string, clientId: string) { + const { count } = await prisma.client.oauthRefreshToken.updateMany({ + where: { userId, clientId, revokedAt: null }, + data: { revokedAt: new Date() }, + }); + + return count; +} + +export async function touchRefreshToken(id: string) { + return prisma.client.oauthRefreshToken.update({ + where: { id }, + data: { lastUsedAt: new Date() }, + }); +} + +export async function deleteExpiredOauthRecords() { + const now = new Date(); + const [codes, tokens] = await Promise.all([ + prisma.client.oauthAuthorizationCode.deleteMany({ where: { expiresAt: { lt: now } } }), + prisma.client.oauthRefreshToken.deleteMany({ where: { expiresAt: { lt: now } } }), + ]); + + return { codes: codes.count, tokens: tokens.count }; +}