diff --git a/docs/contributing/api-patterns.md b/docs/contributing/api-patterns.md index 8c84e662a..6a00c581e 100644 --- a/docs/contributing/api-patterns.md +++ b/docs/contributing/api-patterns.md @@ -264,7 +264,11 @@ through the shared request core, `@sentry/toolkit-core/api-client/core`, which builds them from `readErrorResponse` in `@sentry/toolkit-core/api-client/response`. The CLI reads raw error responses with the same helper and maps them to its own exit-coded -`ApiError`. See +`ApiError`. Endpoints that have moved to the shared client live in +`@sentry/toolkit-core/api-client/endpoints/*`; `SentryApiService` calls +them with its Zod schemas, and a malformed response throws +`ApiResponseValidationError` from +`@sentry/toolkit-core/api-client/validate`. See [`packages/toolkit-core/README.md`](../../packages/toolkit-core/README.md#api-client). ### Automatic Retries diff --git a/packages/cli/package.json b/packages/cli/package.json index 0ae7c1743..d5f2cc2a2 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -151,7 +151,7 @@ "string-width": "^8.2.1", "tinyglobby": "^0.2.17", "tsx": "^4.22.4", - "typescript": "^5.9.3", + "typescript": "catalog:", "uuidv7": "^1.2.1", "valibot": "^1.4.2", "vitest": "^4.1.11", diff --git a/packages/mcp-core/src/api-client/client.ts b/packages/mcp-core/src/api-client/client.ts index ffda2320f..7b6ae29c1 100644 --- a/packages/mcp-core/src/api-client/client.ts +++ b/packages/mcp-core/src/api-client/client.ts @@ -3,7 +3,6 @@ import { isAggregateFieldOrEquation, } from "@sentry/toolkit-core/events-fields"; import { - createSentryClient, type GetOrganizationIssueEventData, getOrganizationIssue, getOrganizationIssueAutofixState, @@ -15,13 +14,21 @@ import { listOrganizationIssues, listOrganizationProjects, listOrganizationTeams, - listOrganizations, listProjectEventAttachments, parseSentryLinkHeader, } from "@sentry/api"; import { sentryBearerHeader } from "@sentry/toolkit-core/auth-token"; import type { RetryPolicy } from "@sentry/toolkit-core/api-client/retry"; -import { createApiClientCore } from "@sentry/toolkit-core/api-client/core"; +import { + type ApiTarget, + createApiClientCore, + listPage, +} from "@sentry/toolkit-core/api-client/core"; +import { + getAuthenticatedUser, + getOrganization, + listOrganizations, +} from "@sentry/toolkit-core/api-client/endpoints/organizations"; import { createApiTransport } from "@sentry/toolkit-core/api-client/transport"; import { z } from "zod"; import { DEFAULT_SEARCH_ISSUES_PERIOD } from "../constants"; @@ -322,9 +329,6 @@ const REQUEST_RETRY_POLICY: RetryPolicy = { */ const transport = createApiTransport({ retry: REQUEST_RETRY_POLICY }); -const API_MAX_PER_PAGE = 100; -const MAX_LIST_PAGES = 50; - /** * Default cap for returning attachment bytes inline over the MCP transport. * A 30 MB attachment takes ~80 MB in memory once base64-encoded and serialized, @@ -859,80 +863,14 @@ export class SentryApiService { ); } - /** Let SDK operations build paths and queries while MCP owns transport and errors. */ + /** Let SDK operations build paths and queries while the core owns transport and errors. */ private sdkClient(expectedPath: string, host: string = this.host) { - const origin = new URL(this.origin(host)).origin; - return createSentryClient({ - baseUrl: origin, - fetch: async (sdkRequest) => { - if (!(sdkRequest instanceof Request)) { - throw new ConfigurationError("Unexpected SDK request input"); - } - const url = new URL(sdkRequest.url); - if ( - url.origin !== origin || - url.pathname !== "/api/0".concat(expectedPath) || - sdkRequest.method !== "GET" - ) { - throw new ConfigurationError( - "Unexpected SDK request target or method", - ); - } - const path = `${url.pathname.slice("/api/0".length)}${url.search}`; - const response = await this.request( - path, - { method: "GET", signal: sdkRequest.signal }, - { host }, - ); - const contentType = response.headers.get("content-type"); - if (!contentType?.includes("application/json")) { - await readJsonResponse(response); - } - return response; - }, - }).client; + return this.core.sdkClient(this.origin(host), expectedPath); } - private async sdkListPage( - fetchPage: ( - perPage: number, - cursor: string | undefined, - ) => Promise<{ data: unknown; response: Response }>, - schema: z.ZodType, - limit: number, - initialCursor?: string | null, - ): Promise<{ items: T[]; nextCursor: string | null }> { - if ( - !Number.isSafeInteger(limit) || - limit < 1 || - limit > API_MAX_PER_PAGE * MAX_LIST_PAGES - ) { - throw new ApiValidationError( - `limit must be an integer between 1 and ${API_MAX_PER_PAGE * MAX_LIST_PAGES}`, - ); - } - const items: T[] = []; - const seenCursors = new Set(); - let cursor = initialCursor ?? undefined; - for (let page = 0; page < MAX_LIST_PAGES; page += 1) { - const perPage = Math.min(API_MAX_PER_PAGE, limit - items.length); - const { data, response } = await fetchPage(perPage, cursor); - const rows = schema.parse(data); - if (rows.length > perPage) { - throw new Error("API returned more items than requested"); - } - items.push(...rows); - const nextCursor = getNextCursor(response.headers.get("link")); - if (items.length >= limit || !nextCursor) { - return { items, nextCursor }; - } - if (seenCursors.has(nextCursor) || nextCursor === cursor) { - throw new Error("API returned a repeated pagination cursor"); - } - seenCursors.add(nextCursor); - cursor = nextCursor; - } - throw new Error("API pagination exceeded its page limit"); + /** The core and origin a shared endpoint wrapper sends to. */ + private target(host?: string): ApiTarget { + return { core: this.core, origin: this.origin(host) }; } /** @@ -1516,24 +1454,16 @@ export class SentryApiService { /** * Retrieves the authenticated user's profile information. * - * @param opts Request options including host override * @returns User profile data * @throws {ApiError} If authentication fails or user not found */ - async getAuthenticatedUser(opts?: RequestOptions): Promise { - // User identity belongs to the deployment's control host. - let authHost: string | undefined; - - if (this.isPublicSaas()) { - authHost = "sentry.io"; - } - // Single-tenant and self-hosted deployments keep their configured host. - - const body = await this.requestJSON("/auth/", undefined, { - ...opts, - host: authHost, - }); - return UserSchema.parse(body); + async getAuthenticatedUser(): Promise { + // User identity belongs to the deployment's control host. Single-tenant + // and self-hosted deployments keep their configured host. + return await getAuthenticatedUser( + this.target(this.isPublicSaas() ? "sentry.io" : undefined), + UserSchema, + ); } /** @@ -1563,20 +1493,10 @@ export class SentryApiService { }): Promise<{ organizations: OrganizationList; nextCursor: string | null }> { // Public SaaS lists across regions on sentry.io; single-tenant instances // keep organization discovery on their configured host. - const client = this.sdkClient( - "/organizations/", - this.isPublicSaas() ? "sentry.io" : this.host, - ); - const { items, nextCursor } = await this.sdkListPage( - (perPage, cursor) => - listOrganizations({ - client, - query: { per_page: perPage, query: params?.query, cursor }, - throwOnError: true, - }), + const { items, nextCursor } = await listOrganizations( + this.target(this.isPublicSaas() ? "sentry.io" : this.host), + params ?? {}, OrganizationListSchema, - params?.limit ?? 25, - params?.cursor, ); return { organizations: items, @@ -1599,21 +1519,11 @@ export class SentryApiService { params?: { includeFeatureFlags?: boolean; detailed?: boolean }, opts?: RequestOptions, ) { - const queryParams = new URLSearchParams(); - if (params?.includeFeatureFlags) { - queryParams.set("include_feature_flags", "1"); - } - if (params?.detailed === false) { - queryParams.set("detailed", "0"); - } - const queryString = queryParams.toString(); - const organizationPath = apiPath`/organizations/${organizationSlug}/`; - const body = await this.requestJSON( - `${organizationPath}${queryString ? `?${queryString}` : ""}`, - undefined, - opts, + return await getOrganization( + this.target(opts?.host), + { organizationSlug, ...params }, + OrganizationSchema, ); - return OrganizationSchema.parse(body); } /** @@ -1634,7 +1544,7 @@ export class SentryApiService { ): Promise<{ teams: TeamList; nextCursor: string | null }> { const teamsPath = apiPath`/organizations/${organizationSlug}/teams/`; const client = this.sdkClient(teamsPath, opts?.host); - const { items, nextCursor } = await this.sdkListPage( + const { items, nextCursor } = await listPage( (perPage, cursor) => listOrganizationTeams({ client, @@ -1701,7 +1611,7 @@ export class SentryApiService { ): Promise<{ projects: ProjectList; nextCursor: string | null }> { const projectsPath = apiPath`/organizations/${organizationSlug}/projects/`; const client = this.sdkClient(projectsPath, opts?.host); - const { items, nextCursor } = await this.sdkListPage( + const { items, nextCursor } = await listPage( (perPage, cursor) => listOrganizationProjects({ client, diff --git a/packages/toolkit-core/README.md b/packages/toolkit-core/README.md index ba125a3e5..e3a043047 100644 --- a/packages/toolkit-core/README.md +++ b/packages/toolkit-core/README.md @@ -43,8 +43,16 @@ the one exception. to `/api/0`, turns network failures into readable errors (`ConfigurationError` for an unreachable host), and throws the status's `ApiError` for an error response unless the caller allows that status. - `requestJson` also parses the body. The MCP's `SentryApiService` sends - every request through it. + `requestJson` also parses the body, and `sdkClient` gives a `@sentry/api` + operation a client that sends one GET through `request`. `listPage` walks + a list endpoint's pages up to a limit and validates each page. The MCP's + `SentryApiService` sends every request through the core. +- `endpoints/` holds the endpoint wrappers by domain, on `@sentry/api` + operations where they exist. Each takes an `ApiTarget` (the core and an + origin) and the host's schema, and returns that schema's output. The host + picks the origin, such as the control host for the current user. + `endpoints/organizations` has `getAuthenticatedUser`, `listOrganizations`, + and `getOrganization`. - `pagination`, `retry`, `transport`, and `response-cache` are described in the sections below. The `node:zlib` body codec stays outside, at `response-cache/codec`, since browsers lack it. diff --git a/packages/toolkit-core/package.json b/packages/toolkit-core/package.json index 1e2bc943f..063aece6a 100644 --- a/packages/toolkit-core/package.json +++ b/packages/toolkit-core/package.json @@ -78,6 +78,7 @@ "tsc": "tsc --noEmit && tsc -p tsconfig.browser.json" }, "dependencies": { + "@sentry/api": "catalog:", "@sentry/search-syntax": "catalog:", "@standard-schema/spec": "1.1.0", "http-cache-semantics": "^4.2.0" diff --git a/packages/toolkit-core/src/api-client/core.test.ts b/packages/toolkit-core/src/api-client/core.test.ts index fb097ecc5..b002e63f4 100644 --- a/packages/toolkit-core/src/api-client/core.test.ts +++ b/packages/toolkit-core/src/api-client/core.test.ts @@ -1,11 +1,18 @@ import { describe, expect, it, vi } from "vitest"; -import { type ApiRequestTransport, createApiClientCore } from "./core.js"; +import { z } from "zod"; +import { + type ApiRequestTransport, + createApiClientCore, + listPage, +} from "./core.js"; import { ApiNotFoundError, ApiRateLimitError, ApiServerError, + ApiValidationError, ConfigurationError, } from "./errors.js"; +import { ApiResponseValidationError } from "./validate.js"; const ORIGIN = "https://us.sentry.io"; @@ -248,3 +255,59 @@ describe("createApiClientCore requestJson", () => { ); }); }); + +describe("listPage", () => { + const rows = z.array(z.object({ id: z.number() })); + + function page(ids: number[], nextCursor?: string) { + const headers: Record = nextCursor + ? { + link: `<${ORIGIN}/api/0/x/?cursor=${nextCursor}>; rel="next"; results="true"; cursor="${nextCursor}"`, + } + : {}; + return { + data: ids.map((id) => ({ id })), + response: new Response(null, { headers }), + }; + } + + it.each([0, 1.5, 5001])("rejects limit %s", async (limit) => { + await expect( + listPage(async () => page([]), rows, limit), + ).rejects.toBeInstanceOf(ApiValidationError); + }); + + it("rejects a page with more rows than requested", async () => { + await expect( + listPage(async () => page([1, 2, 3]), rows, 2), + ).rejects.toThrow("API returned more items than requested"); + }); + + it("rejects a repeated cursor", async () => { + await expect( + listPage(async () => page([1], "0:1:0"), rows, 5), + ).rejects.toThrow("API returned a repeated pagination cursor"); + }); + + it("validates each page with the schema", async () => { + await expect( + listPage( + async () => ({ data: [{ id: "x" }], response: new Response(null) }), + rows, + 5, + ), + ).rejects.toBeInstanceOf(ApiResponseValidationError); + }); +}); + +describe("createApiClientCore sdkClient", () => { + it("rejects an SDK request to another path", async () => { + const { core, transport } = setup(async () => jsonResponse([])); + const client = core.sdkClient(ORIGIN, "/organizations/"); + + await expect( + client.get({ url: "/api/0/projects/", throwOnError: true }), + ).rejects.toThrow("Unexpected SDK request target or method"); + expect(transport.request).not.toHaveBeenCalled(); + }); +}); diff --git a/packages/toolkit-core/src/api-client/core.ts b/packages/toolkit-core/src/api-client/core.ts index e55399d6e..1d943b4f4 100644 --- a/packages/toolkit-core/src/api-client/core.ts +++ b/packages/toolkit-core/src/api-client/core.ts @@ -1,19 +1,33 @@ /** * The request core that endpoint wrappers build on. It sends a request * through the host's transport, turns network failures and error responses - * into the shared errors, and reads JSON bodies. + * into the shared errors, and reads JSON bodies. `@sentry/api` operations + * get a client that sends through the same path, and {@link listPage} walks + * their pages. * * The host supplies the transport (retries, caching), the per-request * headers (credentials, user agent, attribution), and logging. */ +import { + createSentryClient, + parseSentryLinkHeader, + type SentryClient, +} from "@sentry/api"; import { buildSentryApiUrl } from "../api-request.js"; -import { type ApiError, ConfigurationError, createApiError } from "./errors.js"; +import { + type ApiError, + ApiValidationError, + ConfigurationError, + createApiError, +} from "./errors.js"; +import { API_MAX_PER_PAGE } from "./pagination.js"; import { type ErrorResponseBody, readErrorResponse, readJsonResponse, } from "./response.js"; +import { type ResponseSchema, validateResponse } from "./validate.js"; /** What the core needs from the host's transport. */ export type ApiRequestTransport = { @@ -60,8 +74,23 @@ export type ApiClientCore = { path: string, init?: RequestInit, ): Promise; + /** + * A `@sentry/api` client for one GET to `expectedPath` on `origin`. The + * operation builds the path and query; the request goes through + * {@link ApiClientCore.request}. Any other target or method throws. + */ + sdkClient(origin: string, expectedPath: string): SentryClient["client"]; }; +/** Where an endpoint wrapper sends: the core, and the region or host origin. */ +export type ApiTarget = { core: ApiClientCore; origin: string }; + +/** Most pages {@link listPage} fetches for one call. */ +export const MAX_LIST_PAGES = 50; + +/** Rows from {@link listPage}, and the cursor for the next call. */ +export type ListPage = { items: T[]; nextCursor: string | null }; + /** Readable messages for common network error codes. */ const NETWORK_ERROR_MESSAGES: Record = { EAI_AGAIN: "DNS temporarily unavailable. Check your internet connection.", @@ -71,6 +100,8 @@ const NETWORK_ERROR_MESSAGES: Record = { ECONNRESET: "Connection reset. Try again in a moment.", }; +const API_PREFIX = "/api/0"; + /** Network failures that mean the host is wrong or unreachable. */ const CONFIGURATION_ERROR_CODES = new Set([ "ENOTFOUND", @@ -219,5 +250,94 @@ export function createApiClientCore({ async requestJson(origin, path, init) { return await readJsonResponse(await request(origin, path, init)); }, + + sdkClient(origin, expectedPath) { + const base = new URL(origin).origin; + return createSentryClient({ + baseUrl: base, + fetch: async (sdkRequest) => { + if (!(sdkRequest instanceof Request)) { + throw new ConfigurationError("Unexpected SDK request input"); + } + const url = new URL(sdkRequest.url); + if ( + url.origin !== base || + url.pathname !== API_PREFIX.concat(expectedPath) || + sdkRequest.method !== "GET" + ) { + throw new ConfigurationError( + "Unexpected SDK request target or method", + ); + } + const response = await request( + origin, + `${url.pathname.slice(API_PREFIX.length)}${url.search}`, + { method: "GET", signal: sdkRequest.signal }, + ); + const contentType = response.headers.get("content-type"); + if (!contentType?.includes("application/json")) { + await readJsonResponse(response); + } + return response; + }, + }).client; + }, }; } + +/** + * Fetch up to `limit` rows from a list endpoint, starting at `cursor`. + * + * Each page asks for exactly the rows still missing, up to + * {@link API_MAX_PER_PAGE}, and is validated with `schema`. The walk stops + * once it has `limit` rows or the server has no next page, and returns the + * server's cursor for the page after the last one fetched. + * + * @throws {ApiValidationError} When `limit` is not between 1 and + * `API_MAX_PER_PAGE * MAX_LIST_PAGES` + * @throws {ApiResponseValidationError} When a page does not match `schema` + * @throws {Error} When a page has more rows than requested, a cursor + * repeats, or the walk passes {@link MAX_LIST_PAGES} + */ +export async function listPage( + fetchPage: ( + perPage: number, + cursor: string | undefined, + ) => Promise<{ data: unknown; response: Response }>, + schema: ResponseSchema, + limit: number, + initialCursor?: string | null, +): Promise> { + if ( + !Number.isSafeInteger(limit) || + limit < 1 || + limit > API_MAX_PER_PAGE * MAX_LIST_PAGES + ) { + throw new ApiValidationError( + `limit must be an integer between 1 and ${API_MAX_PER_PAGE * MAX_LIST_PAGES}`, + ); + } + const items: T[] = []; + const seenCursors = new Set(); + let cursor = initialCursor ?? undefined; + for (let page = 0; page < MAX_LIST_PAGES; page += 1) { + const perPage = Math.min(API_MAX_PER_PAGE, limit - items.length); + const { data, response } = await fetchPage(perPage, cursor); + const rows = validateResponse(schema, data); + if (rows.length > perPage) { + throw new Error("API returned more items than requested"); + } + items.push(...rows); + const nextCursor = + parseSentryLinkHeader(response.headers.get("link")).nextCursor ?? null; + if (items.length >= limit || !nextCursor) { + return { items, nextCursor }; + } + if (seenCursors.has(nextCursor) || nextCursor === cursor) { + throw new Error("API returned a repeated pagination cursor"); + } + seenCursors.add(nextCursor); + cursor = nextCursor; + } + throw new Error("API pagination exceeded its page limit"); +} diff --git a/packages/toolkit-core/src/api-client/endpoints/organizations.test.ts b/packages/toolkit-core/src/api-client/endpoints/organizations.test.ts new file mode 100644 index 000000000..a087f4bd6 --- /dev/null +++ b/packages/toolkit-core/src/api-client/endpoints/organizations.test.ts @@ -0,0 +1,159 @@ +import { describe, expect, it, vi } from "vitest"; +import * as v from "valibot"; +import { z } from "zod"; +import { type ApiTarget, createApiClientCore } from "../core.js"; +import { ApiResponseValidationError } from "../validate.js"; +import { + getAuthenticatedUser, + getOrganization, + listOrganizations, +} from "./organizations.js"; + +const ORIGIN = "https://us.sentry.io"; + +function jsonResponse(body: unknown, headers: Record = {}) { + return new Response(JSON.stringify(body), { + headers: { "content-type": "application/json", ...headers }, + }); +} + +function nextLink(cursor: string) { + return { + link: `<${ORIGIN}/api/0/organizations/?cursor=${cursor}>; rel="next"; results="true"; cursor="${cursor}"`, + }; +} + +/** A target whose transport answers each request with the next response. */ +function target(...responses: Response[]) { + const request = vi.fn(async (_url: string, _init: RequestInit) => { + const response = responses.shift(); + if (!response) { + throw new Error("unexpected request"); + } + return response; + }); + const core = createApiClientCore({ + transport: { request }, + headers: () => ({}), + logger: { warn: vi.fn() }, + }); + const apiTarget: ApiTarget = { core, origin: ORIGIN }; + return { apiTarget, request }; +} + +const OrganizationZod = z.object({ slug: z.string() }); +const OrganizationValibot = v.object({ slug: v.string() }); + +describe("getAuthenticatedUser", () => { + it("reads /auth/ and returns the host schema's output", async () => { + const { apiTarget, request } = target( + jsonResponse({ id: "1", email: "jane@example.com", extra: true }), + ); + + const user = await getAuthenticatedUser( + apiTarget, + z.object({ id: z.string(), email: z.string() }), + ); + + expect(user).toEqual({ id: "1", email: "jane@example.com" }); + expect(request.mock.calls[0]?.[0]).toBe(`${ORIGIN}/api/0/auth/`); + }); + + it("throws the shared validation error for a mismatched body", async () => { + const { apiTarget } = target(jsonResponse({ id: 1 })); + + await expect( + getAuthenticatedUser(apiTarget, z.object({ id: z.string() })), + ).rejects.toBeInstanceOf(ApiResponseValidationError); + }); +}); + +describe("getOrganization", () => { + it.each([ + [{}, ""], + [{ includeFeatureFlags: true }, "?include_feature_flags=1"], + [{ detailed: false }, "?detailed=0"], + [{ detailed: true }, ""], + ])("sends %o as %s", async (params, query) => { + const { apiTarget, request } = target(jsonResponse({ slug: "acme" })); + + await getOrganization( + apiTarget, + { organizationSlug: "acme", ...params }, + OrganizationZod, + ); + + expect(request.mock.calls[0]?.[0]).toBe( + `${ORIGIN}/api/0/organizations/acme/${query}`, + ); + }); + + it("encodes the slug as a path segment", async () => { + const { apiTarget, request } = target(jsonResponse({ slug: "a/b" })); + + await getOrganization( + apiTarget, + { organizationSlug: "a/b" }, + OrganizationValibot, + ); + + expect(request.mock.calls[0]?.[0]).toBe( + `${ORIGIN}/api/0/organizations/a%2Fb/`, + ); + }); +}); + +describe("listOrganizations", () => { + it("follows next cursors until it has the limit", async () => { + const { apiTarget, request } = target( + jsonResponse([{ slug: "a" }, { slug: "b" }], nextLink("0:2:0")), + jsonResponse([{ slug: "c" }], nextLink("0:3:0")), + ); + + const page = await listOrganizations( + apiTarget, + { query: "ac", limit: 3 }, + z.array(OrganizationZod), + ); + + expect(page).toEqual({ + items: [{ slug: "a" }, { slug: "b" }, { slug: "c" }], + nextCursor: "0:3:0", + }); + const urls = request.mock.calls.map(([url]) => new URL(url)); + expect(urls.map((url) => url.pathname)).toEqual([ + "/api/0/organizations/", + "/api/0/organizations/", + ]); + expect(urls[0]?.searchParams.get("per_page")).toBe("3"); + expect(urls[0]?.searchParams.get("query")).toBe("ac"); + expect(urls[1]?.searchParams.get("per_page")).toBe("1"); + expect(urls[1]?.searchParams.get("cursor")).toBe("0:2:0"); + }); + + it("returns no cursor once the server has no next page", async () => { + const { apiTarget } = target(jsonResponse([{ slug: "a" }])); + + const page = await listOrganizations( + apiTarget, + {}, + v.array(OrganizationValibot), + ); + + expect(page).toEqual({ items: [{ slug: "a" }], nextCursor: null }); + }); + + it("starts from the given cursor", async () => { + const { apiTarget, request } = target(jsonResponse([])); + + await listOrganizations( + apiTarget, + { cursor: "0:25:0" }, + z.array(OrganizationZod), + ); + + expect( + new URL(request.mock.calls[0]?.[0] ?? "").searchParams.get("cursor"), + ).toBe("0:25:0"); + }); +}); diff --git a/packages/toolkit-core/src/api-client/endpoints/organizations.ts b/packages/toolkit-core/src/api-client/endpoints/organizations.ts new file mode 100644 index 000000000..69691c8ca --- /dev/null +++ b/packages/toolkit-core/src/api-client/endpoints/organizations.ts @@ -0,0 +1,78 @@ +/** + * Organization and current-user endpoints. Each wrapper validates the + * response with the schema the host passes, so the host's schema decides the + * result type. Hosts pick the origin: user identity and the organization + * list live on the control host, an organization on its region. + */ + +import { listOrganizations as listOrganizationsOperation } from "@sentry/api"; +import { apiPath } from "../api-path.js"; +import { type ApiTarget, type ListPage, listPage } from "../core.js"; +import { type ResponseSchema, validateResponse } from "../validate.js"; + +/** The user the request's credential belongs to (`GET /auth/`). */ +export async function getAuthenticatedUser( + { core, origin }: ApiTarget, + schema: ResponseSchema, +): Promise { + return validateResponse(schema, await core.requestJson(origin, "/auth/")); +} + +export type ListOrganizationsParams = { + /** Filters by name or slug. */ + query?: string; + /** Most organizations to return. Default: 25. */ + limit?: number; + /** Resumes from a previous call's `nextCursor`. */ + cursor?: string | null; +}; + +/** A page of the organizations the credential can access. */ +export async function listOrganizations( + { core, origin }: ApiTarget, + params: ListOrganizationsParams, + schema: ResponseSchema, +): Promise> { + const client = core.sdkClient(origin, "/organizations/"); + return await listPage( + (perPage, cursor) => + listOrganizationsOperation({ + client, + query: { per_page: perPage, query: params.query, cursor }, + throwOnError: true, + }), + schema, + params.limit ?? 25, + params.cursor, + ); +} + +export type GetOrganizationParams = { + organizationSlug: string; + /** Include `features`, which Sentry omits otherwise. */ + includeFeatureFlags?: boolean; + /** Set false to leave out projects and teams; Sentry defaults to true. */ + detailed?: boolean; +}; + +/** One organization by slug. */ +export async function getOrganization( + { core, origin }: ApiTarget, + params: GetOrganizationParams, + schema: ResponseSchema, +): Promise { + const query = new URLSearchParams(); + if (params.includeFeatureFlags) { + query.set("include_feature_flags", "1"); + } + if (params.detailed === false) { + query.set("detailed", "0"); + } + const queryString = query.toString(); + const path = apiPath`/organizations/${params.organizationSlug}/`; + const body = await core.requestJson( + origin, + `${path}${queryString ? `?${queryString}` : ""}`, + ); + return validateResponse(schema, body); +} diff --git a/packages/toolkit-core/src/api-client/response-cache/policy.ts b/packages/toolkit-core/src/api-client/response-cache/policy.ts index c9e477f25..9ebd3ac5b 100644 --- a/packages/toolkit-core/src/api-client/response-cache/policy.ts +++ b/packages/toolkit-core/src/api-client/response-cache/policy.ts @@ -171,6 +171,15 @@ function buildResponseHeaders( return result; } +/** + * A decoded body as a `Response` body. TypeScript 5.9's DOM `BodyInit` + * takes only `ArrayBuffer`-backed views; stored bodies never sit on a + * `SharedArrayBuffer`. + */ +function responseBody(bytes: Uint8Array): Uint8Array { + return bytes as Uint8Array; +} + /** A `stale` lookup for an entry with an `ETag`, or `undefined` without one. */ function staleLookup( options: LookupOptions, @@ -213,7 +222,7 @@ function staleLookup( store .set(key, renewed, retentionMs(result.policy, entry.tier)) .catch((error: unknown) => onRenewError?.(error)); - return new Response(body, { + return new Response(responseBody(body), { status: entry.status, headers: buildResponseHeaders(result.policy, entry), }); @@ -253,7 +262,7 @@ export async function lookupResponse( } return { kind: "hit", - response: new Response(body, { + response: new Response(responseBody(body), { status: entry.status, headers: buildResponseHeaders(policy, entry), }), diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f79e03b94..c349635fd 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -163,8 +163,8 @@ catalogs: specifier: ^1.3.4 version: 1.3.5 typescript: - specifier: ^5.8.3 - version: 5.8.3 + specifier: ^5.9.3 + version: 5.9.3 valibot: specifier: ^1.4.2 version: 1.4.2 @@ -241,19 +241,19 @@ importers: version: 2.13.1 tsdown: specifier: 'catalog:' - version: 0.12.9(typescript@5.8.3) + version: 0.12.9(typescript@5.9.3) tsx: specifier: 'catalog:' version: 4.20.3 typescript: specifier: 'catalog:' - version: 5.8.3 + version: 5.9.3 valibot: specifier: 'catalog:' - version: 1.4.2(typescript@5.8.3) + version: 1.4.2(typescript@5.9.3) vitest: specifier: 'catalog:' - version: 4.1.11(@opentelemetry/api@1.9.1)(@types/node@22.16.0)(@vitest/coverage-v8@4.1.11)(jsdom@28.1.0)(msw@2.10.2(@types/node@22.16.0)(typescript@5.8.3))(vite@8.3.3(@types/node@22.16.0)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.20.3)(yaml@2.9.0)) + version: 4.1.11(@opentelemetry/api@1.9.1)(@types/node@22.16.0)(@vitest/coverage-v8@4.1.11)(jsdom@28.1.0)(msw@2.10.2(@types/node@22.16.0)(typescript@5.9.3))(vite@8.3.3(@types/node@22.16.0)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.20.3)(yaml@2.9.0)) vitest-evals: specifier: 'catalog:' version: 0.4.0(tinyrainbow@3.1.0)(vitest@4.1.11) @@ -337,7 +337,7 @@ importers: dependencies: '@hookform/resolvers': specifier: ^5.9.1 - version: 5.9.1(@standard-schema/spec@1.1.0)(ajv@8.20.0)(react-hook-form@7.90.0(@types/react@19.3.0)(react@19.2.8))(valibot@1.4.2(typescript@5.8.3))(zod@4.6.5) + version: 5.9.1(@standard-schema/spec@1.1.0)(ajv@8.20.0)(react-hook-form@7.90.0(@types/react@19.3.0)(react@19.2.8))(valibot@1.4.2(typescript@5.9.3))(zod@4.6.5) '@radix-ui/react-dialog': specifier: ^1.1.23 version: 1.3.0(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) @@ -412,7 +412,7 @@ importers: version: 2.8.1 valibot: specifier: 'catalog:' - version: 1.4.2(typescript@5.8.3) + version: 1.4.2(typescript@5.9.3) devDependencies: '@spotlightjs/spotlight': specifier: ^4.11.7 @@ -443,13 +443,13 @@ importers: version: 1.87.0(oxlint-tsgolint@7.0.2004) typescript: specifier: 'catalog:' - version: 5.8.3 + version: 5.9.3 vite: specifier: ^8.3.3 version: 8.3.3(@types/node@24.13.5)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0) vitest: specifier: ^4.1.9 - version: 4.1.11(@opentelemetry/api@1.9.1)(@types/node@24.13.5)(@vitest/coverage-v8@4.1.11)(jsdom@28.1.0)(msw@2.10.2(@types/node@24.13.5)(typescript@5.8.3))(vite@8.3.3(@types/node@24.13.5)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0)) + version: 4.1.11(@opentelemetry/api@1.9.1)(@types/node@24.13.5)(@vitest/coverage-v8@4.1.11)(jsdom@28.1.0)(msw@2.10.2(@types/node@24.13.5)(typescript@5.9.3))(vite@8.3.3(@types/node@24.13.5)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0)) packages/cli: devDependencies: @@ -616,7 +616,7 @@ importers: specifier: ^4.22.4 version: 4.23.13 typescript: - specifier: ^5.9.3 + specifier: 'catalog:' version: 5.9.3 uuidv7: specifier: ^1.2.1 @@ -887,16 +887,16 @@ importers: version: 22.16.0 tsdown: specifier: 'catalog:' - version: 0.12.9(typescript@5.8.3) + version: 0.12.9(typescript@5.9.3) tsx: specifier: 'catalog:' version: 4.20.3 typescript: specifier: 'catalog:' - version: 5.8.3 + version: 5.9.3 vitest: specifier: 'catalog:' - version: 4.1.11(@opentelemetry/api@1.9.1)(@types/node@22.16.0)(@vitest/coverage-v8@4.1.11)(jsdom@28.1.0)(msw@2.10.2(@types/node@22.16.0)(typescript@5.8.3))(vite@8.3.3(@types/node@22.16.0)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.20.3)(yaml@2.9.0)) + version: 4.1.11(@opentelemetry/api@1.9.1)(@types/node@22.16.0)(@vitest/coverage-v8@4.1.11)(jsdom@28.1.0)(msw@2.10.2(@types/node@22.16.0)(typescript@5.9.3))(vite@8.3.3(@types/node@22.16.0)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.20.3)(yaml@2.9.0)) packages/mcp-server-evals: dependencies: @@ -929,13 +929,13 @@ importers: version: 16.6.1 msw: specifier: 'catalog:' - version: 2.10.2(@types/node@24.13.5)(typescript@5.8.3) + version: 2.10.2(@types/node@24.13.5)(typescript@5.9.3) typescript: specifier: 'catalog:' - version: 5.8.3 + version: 5.9.3 vitest: specifier: 'catalog:' - version: 4.1.11(@opentelemetry/api@1.9.1)(@types/node@24.13.5)(@vitest/coverage-v8@4.1.11)(jsdom@28.1.0)(msw@2.10.2(@types/node@24.13.5)(typescript@5.8.3))(vite@8.3.3(@types/node@24.13.5)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0)) + version: 4.1.11(@opentelemetry/api@1.9.1)(@types/node@24.13.5)(@vitest/coverage-v8@4.1.11)(jsdom@28.1.0)(msw@2.10.2(@types/node@24.13.5)(typescript@5.9.3))(vite@8.3.3(@types/node@24.13.5)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0)) vitest-evals: specifier: 'catalog:' version: 0.4.0(tinyrainbow@3.1.0)(vitest@4.1.11) @@ -999,16 +999,16 @@ importers: devDependencies: tsdown: specifier: 'catalog:' - version: 0.12.9(typescript@5.8.3) + version: 0.12.9(typescript@5.9.3) tsx: specifier: 'catalog:' version: 4.20.3 typescript: specifier: 'catalog:' - version: 5.8.3 + version: 5.9.3 vitest: specifier: 'catalog:' - version: 4.1.11(@opentelemetry/api@1.9.1)(@types/node@24.13.5)(@vitest/coverage-v8@4.1.11)(jsdom@28.1.0)(msw@2.10.2(@types/node@24.13.5)(typescript@5.8.3))(vite@8.3.3(@types/node@24.13.5)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.20.3)(yaml@2.9.0)) + version: 4.1.11(@opentelemetry/api@1.9.1)(@types/node@24.13.5)(@vitest/coverage-v8@4.1.11)(jsdom@28.1.0)(msw@2.10.2(@types/node@24.13.5)(typescript@5.9.3))(vite@8.3.3(@types/node@24.13.5)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.20.3)(yaml@2.9.0)) packages/smoke-tests: devDependencies: @@ -1021,6 +1021,9 @@ importers: packages/toolkit-core: dependencies: + '@sentry/api': + specifier: 'catalog:' + version: 0.425.0(typescript@5.9.3)(valibot@1.4.2(typescript@5.9.3))(zod@4.6.7) '@sentry/search-syntax': specifier: 'catalog:' version: 1.0.0 @@ -1039,13 +1042,13 @@ importers: version: 4.10.1 typescript: specifier: 'catalog:' - version: 5.8.3 + version: 5.9.3 valibot: specifier: ^1.4.2 - version: 1.4.2(typescript@5.8.3) + version: 1.4.2(typescript@5.9.3) vitest: specifier: 'catalog:' - version: 4.1.11(@opentelemetry/api@1.9.1)(@types/node@24.13.5)(@vitest/coverage-v8@4.1.11)(jsdom@28.1.0)(msw@2.10.2(@types/node@24.13.5)(typescript@5.8.3))(vite@8.3.3(@types/node@24.13.5)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0)) + version: 4.1.11(@opentelemetry/api@1.9.1)(@types/node@24.13.5)(@vitest/coverage-v8@4.1.11)(jsdom@28.1.0)(msw@2.10.2(@types/node@24.13.5)(typescript@5.9.3))(vite@8.3.3(@types/node@24.13.5)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0)) zod: specifier: 'catalog:' version: 4.6.7 @@ -8974,11 +8977,6 @@ packages: engines: {node: '>=14.17'} hasBin: true - typescript@5.8.3: - resolution: {integrity: sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==} - engines: {node: '>=14.17'} - hasBin: true - typescript@5.9.3: resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} engines: {node: '>=14.17'} @@ -11037,14 +11035,14 @@ snapshots: dependencies: hono: 4.13.13 - '@hookform/resolvers@5.9.1(@standard-schema/spec@1.1.0)(ajv@8.20.0)(react-hook-form@7.90.0(@types/react@19.3.0)(react@19.2.8))(valibot@1.4.2(typescript@5.8.3))(zod@4.6.5)': + '@hookform/resolvers@5.9.1(@standard-schema/spec@1.1.0)(ajv@8.20.0)(react-hook-form@7.90.0(@types/react@19.3.0)(react@19.2.8))(valibot@1.4.2(typescript@5.9.3))(zod@4.6.5)': dependencies: '@standard-schema/utils': 0.3.0 react-hook-form: 7.90.0(@types/react@19.3.0)(react@19.2.8) optionalDependencies: '@standard-schema/spec': 1.1.0 ajv: 8.20.0 - valibot: 1.4.2(typescript@5.8.3) + valibot: 1.4.2(typescript@5.9.3) zod: 4.6.5 '@img/colour@1.1.0': {} @@ -13478,7 +13476,7 @@ snapshots: obug: 2.1.1 std-env: 4.0.0 tinyrainbow: 3.1.0 - vitest: 4.1.11(@opentelemetry/api@1.9.1)(@types/node@22.16.0)(@vitest/coverage-v8@4.1.11)(jsdom@28.1.0)(msw@2.10.2(@types/node@22.16.0)(typescript@5.8.3))(vite@8.3.3(@types/node@22.16.0)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.20.3)(yaml@2.9.0)) + vitest: 4.1.11(@opentelemetry/api@1.9.1)(@types/node@22.16.0)(@vitest/coverage-v8@4.1.11)(jsdom@28.1.0)(msw@2.10.2(@types/node@22.16.0)(typescript@5.9.3))(vite@8.3.3(@types/node@22.16.0)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.20.3)(yaml@2.9.0)) '@vitest/expect@4.1.11': dependencies: @@ -13489,13 +13487,13 @@ snapshots: chai: 6.2.2 tinyrainbow: 3.1.0 - '@vitest/mocker@4.1.11(msw@2.10.2(@types/node@22.16.0)(typescript@5.8.3))(vite@8.3.3(@types/node@22.16.0)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.20.3)(yaml@2.9.0))': + '@vitest/mocker@4.1.11(msw@2.10.2(@types/node@22.16.0)(typescript@5.9.3))(vite@8.3.3(@types/node@22.16.0)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.20.3)(yaml@2.9.0))': dependencies: '@vitest/spy': 4.1.11 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: - msw: 2.10.2(@types/node@22.16.0)(typescript@5.8.3) + msw: 2.10.2(@types/node@22.16.0)(typescript@5.9.3) vite: 8.3.3(@types/node@22.16.0)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.20.3)(yaml@2.9.0) '@vitest/mocker@4.1.11(msw@2.10.2(@types/node@22.16.0)(typescript@5.9.3))(vite@8.3.3(@types/node@22.16.0)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0))': @@ -13525,32 +13523,32 @@ snapshots: msw: 2.10.2(@types/node@22.20.3)(typescript@5.9.3) vite: 8.3.3(@types/node@22.20.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0) - '@vitest/mocker@4.1.11(msw@2.10.2(@types/node@24.13.5)(typescript@5.8.3))(vite@8.3.3(@types/node@24.13.5)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.20.3)(yaml@2.9.0))': + '@vitest/mocker@4.1.11(msw@2.10.2(@types/node@24.13.5)(typescript@5.9.3))(vite@6.4.5(@types/node@24.13.5)(jiti@2.7.0)(lightningcss@1.33.0)(tsx@4.23.13)(yaml@2.9.0))': dependencies: '@vitest/spy': 4.1.11 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: - msw: 2.10.2(@types/node@24.13.5)(typescript@5.8.3) - vite: 8.3.3(@types/node@24.13.5)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.20.3)(yaml@2.9.0) + msw: 2.10.2(@types/node@24.13.5)(typescript@5.9.3) + vite: 6.4.5(@types/node@24.13.5)(jiti@2.7.0)(lightningcss@1.33.0)(tsx@4.23.13)(yaml@2.9.0) - '@vitest/mocker@4.1.11(msw@2.10.2(@types/node@24.13.5)(typescript@5.8.3))(vite@8.3.3(@types/node@24.13.5)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0))': + '@vitest/mocker@4.1.11(msw@2.10.2(@types/node@24.13.5)(typescript@5.9.3))(vite@8.3.3(@types/node@24.13.5)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.20.3)(yaml@2.9.0))': dependencies: '@vitest/spy': 4.1.11 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: - msw: 2.10.2(@types/node@24.13.5)(typescript@5.8.3) - vite: 8.3.3(@types/node@24.13.5)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0) + msw: 2.10.2(@types/node@24.13.5)(typescript@5.9.3) + vite: 8.3.3(@types/node@24.13.5)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.20.3)(yaml@2.9.0) - '@vitest/mocker@4.1.11(msw@2.10.2(@types/node@24.13.5)(typescript@5.9.3))(vite@6.4.5(@types/node@24.13.5)(jiti@2.7.0)(lightningcss@1.33.0)(tsx@4.23.13)(yaml@2.9.0))': + '@vitest/mocker@4.1.11(msw@2.10.2(@types/node@24.13.5)(typescript@5.9.3))(vite@8.3.3(@types/node@24.13.5)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0))': dependencies: '@vitest/spy': 4.1.11 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: msw: 2.10.2(@types/node@24.13.5)(typescript@5.9.3) - vite: 6.4.5(@types/node@24.13.5)(jiti@2.7.0)(lightningcss@1.33.0)(tsx@4.23.13)(yaml@2.9.0) + vite: 8.3.3(@types/node@24.13.5)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0) '@vitest/pretty-format@4.1.11': dependencies: @@ -16428,32 +16426,6 @@ snapshots: ms@2.1.3: {} - msw@2.10.2(@types/node@22.16.0)(typescript@5.8.3): - dependencies: - '@bundled-es-modules/cookie': 2.0.1 - '@bundled-es-modules/statuses': 1.0.1 - '@bundled-es-modules/tough-cookie': 0.1.6 - '@inquirer/confirm': 5.1.21(@types/node@22.16.0) - '@mswjs/interceptors': 0.39.8 - '@open-draft/deferred-promise': 2.2.0 - '@open-draft/until': 2.1.0 - '@types/cookie': 0.6.0 - '@types/statuses': 2.0.6 - graphql: 16.14.2 - headers-polyfill: 4.0.3 - is-node-process: 1.2.0 - outvariant: 1.4.3 - path-to-regexp: 6.3.0 - picocolors: 1.1.1 - strict-event-emitter: 0.5.1 - type-fest: 4.41.0 - yargs: 17.7.3 - optionalDependencies: - typescript: 5.8.3 - transitivePeerDependencies: - - '@types/node' - optional: true - msw@2.10.2(@types/node@22.16.0)(typescript@5.9.3): dependencies: '@bundled-es-modules/cookie': 2.0.1 @@ -16532,31 +16504,6 @@ snapshots: - '@types/node' optional: true - msw@2.10.2(@types/node@24.13.5)(typescript@5.8.3): - dependencies: - '@bundled-es-modules/cookie': 2.0.1 - '@bundled-es-modules/statuses': 1.0.1 - '@bundled-es-modules/tough-cookie': 0.1.6 - '@inquirer/confirm': 5.1.21(@types/node@24.13.5) - '@mswjs/interceptors': 0.39.8 - '@open-draft/deferred-promise': 2.2.0 - '@open-draft/until': 2.1.0 - '@types/cookie': 0.6.0 - '@types/statuses': 2.0.6 - graphql: 16.14.2 - headers-polyfill: 4.0.3 - is-node-process: 1.2.0 - outvariant: 1.4.3 - path-to-regexp: 6.3.0 - picocolors: 1.1.1 - strict-event-emitter: 0.5.1 - type-fest: 4.41.0 - yargs: 17.7.3 - optionalDependencies: - typescript: 5.8.3 - transitivePeerDependencies: - - '@types/node' - msw@2.10.2(@types/node@24.13.5)(typescript@5.9.3): dependencies: '@bundled-es-modules/cookie': 2.0.1 @@ -17421,23 +17368,6 @@ snapshots: rfdc@1.4.1: {} - rolldown-plugin-dts@0.13.13(rolldown@1.0.0-beta.23)(typescript@5.8.3): - dependencies: - '@babel/generator': 7.28.0 - '@babel/parser': 7.28.0 - '@babel/types': 7.28.0 - ast-kit: 2.1.1 - birpc: 2.4.0 - debug: 4.4.3 - dts-resolver: 2.1.1 - get-tsconfig: 4.10.1 - rolldown: 1.0.0-beta.23 - optionalDependencies: - typescript: 5.8.3 - transitivePeerDependencies: - - oxc-resolver - - supports-color - rolldown-plugin-dts@0.13.13(rolldown@1.0.0-beta.23)(typescript@5.9.3): dependencies: '@babel/generator': 7.28.0 @@ -18089,29 +18019,6 @@ snapshots: dependencies: typescript: 5.7.3 - tsdown@0.12.9(typescript@5.8.3): - dependencies: - ansis: 4.1.0 - cac: 6.7.14 - chokidar: 4.0.3 - debug: 4.4.3 - diff: 8.0.2 - empathic: 2.0.0 - hookable: 5.5.3 - rolldown: 1.0.0-beta.23 - rolldown-plugin-dts: 0.13.13(rolldown@1.0.0-beta.23)(typescript@5.8.3) - semver: 7.7.4 - tinyexec: 1.0.4 - tinyglobby: 0.2.15 - unconfig: 7.3.2 - optionalDependencies: - typescript: 5.8.3 - transitivePeerDependencies: - - '@typescript/native-preview' - - oxc-resolver - - supports-color - - vue-tsc - tsdown@0.12.9(typescript@5.9.3): dependencies: ansis: 4.1.0 @@ -18170,8 +18077,6 @@ snapshots: typescript@5.7.3: {} - typescript@5.8.3: {} - typescript@5.9.3: {} uc.micro@2.1.0: {} @@ -18357,10 +18262,6 @@ snapshots: uuidv7@1.2.1: {} - valibot@1.4.2(typescript@5.8.3): - optionalDependencies: - typescript: 5.8.3 - valibot@1.4.2(typescript@5.9.3): optionalDependencies: typescript: 5.9.3 @@ -18512,12 +18413,12 @@ snapshots: vitest-evals@0.4.0(tinyrainbow@3.1.0)(vitest@4.1.11): dependencies: tinyrainbow: 3.1.0 - vitest: 4.1.11(@opentelemetry/api@1.9.1)(@types/node@22.16.0)(@vitest/coverage-v8@4.1.11)(jsdom@28.1.0)(msw@2.10.2(@types/node@22.16.0)(typescript@5.8.3))(vite@8.3.3(@types/node@22.16.0)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.20.3)(yaml@2.9.0)) + vitest: 4.1.11(@opentelemetry/api@1.9.1)(@types/node@22.16.0)(@vitest/coverage-v8@4.1.11)(jsdom@28.1.0)(msw@2.10.2(@types/node@22.16.0)(typescript@5.9.3))(vite@8.3.3(@types/node@22.16.0)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.20.3)(yaml@2.9.0)) - vitest@4.1.11(@opentelemetry/api@1.9.1)(@types/node@22.16.0)(@vitest/coverage-v8@4.1.11)(jsdom@28.1.0)(msw@2.10.2(@types/node@22.16.0)(typescript@5.8.3))(vite@8.3.3(@types/node@22.16.0)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.20.3)(yaml@2.9.0)): + vitest@4.1.11(@opentelemetry/api@1.9.1)(@types/node@22.16.0)(@vitest/coverage-v8@4.1.11)(jsdom@28.1.0)(msw@2.10.2(@types/node@22.16.0)(typescript@5.9.3))(vite@8.3.3(@types/node@22.16.0)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.20.3)(yaml@2.9.0)): dependencies: '@vitest/expect': 4.1.11 - '@vitest/mocker': 4.1.11(msw@2.10.2(@types/node@22.16.0)(typescript@5.8.3))(vite@8.3.3(@types/node@22.16.0)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.20.3)(yaml@2.9.0)) + '@vitest/mocker': 4.1.11(msw@2.10.2(@types/node@22.16.0)(typescript@5.9.3))(vite@8.3.3(@types/node@22.16.0)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.20.3)(yaml@2.9.0)) '@vitest/pretty-format': 4.1.11 '@vitest/runner': 4.1.11 '@vitest/snapshot': 4.1.11 @@ -18634,10 +18535,10 @@ snapshots: transitivePeerDependencies: - msw - vitest@4.1.11(@opentelemetry/api@1.9.1)(@types/node@24.13.5)(@vitest/coverage-v8@4.1.11)(jsdom@28.1.0)(msw@2.10.2(@types/node@24.13.5)(typescript@5.8.3))(vite@8.3.3(@types/node@24.13.5)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.20.3)(yaml@2.9.0)): + vitest@4.1.11(@opentelemetry/api@1.9.1)(@types/node@24.13.5)(@vitest/coverage-v8@4.1.11)(jsdom@28.1.0)(msw@2.10.2(@types/node@24.13.5)(typescript@5.9.3))(vite@6.4.5(@types/node@24.13.5)(jiti@2.7.0)(lightningcss@1.33.0)(tsx@4.23.13)(yaml@2.9.0)): dependencies: '@vitest/expect': 4.1.11 - '@vitest/mocker': 4.1.11(msw@2.10.2(@types/node@24.13.5)(typescript@5.8.3))(vite@8.3.3(@types/node@24.13.5)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.20.3)(yaml@2.9.0)) + '@vitest/mocker': 4.1.11(msw@2.10.2(@types/node@24.13.5)(typescript@5.9.3))(vite@6.4.5(@types/node@24.13.5)(jiti@2.7.0)(lightningcss@1.33.0)(tsx@4.23.13)(yaml@2.9.0)) '@vitest/pretty-format': 4.1.11 '@vitest/runner': 4.1.11 '@vitest/snapshot': 4.1.11 @@ -18654,7 +18555,7 @@ snapshots: tinyexec: 1.3.1 tinyglobby: 0.2.17 tinyrainbow: 3.1.0 - vite: 8.3.3(@types/node@24.13.5)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.20.3)(yaml@2.9.0) + vite: 6.4.5(@types/node@24.13.5)(jiti@2.7.0)(lightningcss@1.33.0)(tsx@4.23.13)(yaml@2.9.0) why-is-node-running: 2.3.0 optionalDependencies: '@opentelemetry/api': 1.9.1 @@ -18664,10 +18565,10 @@ snapshots: transitivePeerDependencies: - msw - vitest@4.1.11(@opentelemetry/api@1.9.1)(@types/node@24.13.5)(@vitest/coverage-v8@4.1.11)(jsdom@28.1.0)(msw@2.10.2(@types/node@24.13.5)(typescript@5.8.3))(vite@8.3.3(@types/node@24.13.5)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0)): + vitest@4.1.11(@opentelemetry/api@1.9.1)(@types/node@24.13.5)(@vitest/coverage-v8@4.1.11)(jsdom@28.1.0)(msw@2.10.2(@types/node@24.13.5)(typescript@5.9.3))(vite@8.3.3(@types/node@24.13.5)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.20.3)(yaml@2.9.0)): dependencies: '@vitest/expect': 4.1.11 - '@vitest/mocker': 4.1.11(msw@2.10.2(@types/node@24.13.5)(typescript@5.8.3))(vite@8.3.3(@types/node@24.13.5)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0)) + '@vitest/mocker': 4.1.11(msw@2.10.2(@types/node@24.13.5)(typescript@5.9.3))(vite@8.3.3(@types/node@24.13.5)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.20.3)(yaml@2.9.0)) '@vitest/pretty-format': 4.1.11 '@vitest/runner': 4.1.11 '@vitest/snapshot': 4.1.11 @@ -18684,7 +18585,7 @@ snapshots: tinyexec: 1.3.1 tinyglobby: 0.2.17 tinyrainbow: 3.1.0 - vite: 8.3.3(@types/node@24.13.5)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0) + vite: 8.3.3(@types/node@24.13.5)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.20.3)(yaml@2.9.0) why-is-node-running: 2.3.0 optionalDependencies: '@opentelemetry/api': 1.9.1 @@ -18694,10 +18595,10 @@ snapshots: transitivePeerDependencies: - msw - vitest@4.1.11(@opentelemetry/api@1.9.1)(@types/node@24.13.5)(@vitest/coverage-v8@4.1.11)(jsdom@28.1.0)(msw@2.10.2(@types/node@24.13.5)(typescript@5.9.3))(vite@6.4.5(@types/node@24.13.5)(jiti@2.7.0)(lightningcss@1.33.0)(tsx@4.23.13)(yaml@2.9.0)): + vitest@4.1.11(@opentelemetry/api@1.9.1)(@types/node@24.13.5)(@vitest/coverage-v8@4.1.11)(jsdom@28.1.0)(msw@2.10.2(@types/node@24.13.5)(typescript@5.9.3))(vite@8.3.3(@types/node@24.13.5)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0)): dependencies: '@vitest/expect': 4.1.11 - '@vitest/mocker': 4.1.11(msw@2.10.2(@types/node@24.13.5)(typescript@5.9.3))(vite@6.4.5(@types/node@24.13.5)(jiti@2.7.0)(lightningcss@1.33.0)(tsx@4.23.13)(yaml@2.9.0)) + '@vitest/mocker': 4.1.11(msw@2.10.2(@types/node@24.13.5)(typescript@5.9.3))(vite@8.3.3(@types/node@24.13.5)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0)) '@vitest/pretty-format': 4.1.11 '@vitest/runner': 4.1.11 '@vitest/snapshot': 4.1.11 @@ -18714,7 +18615,7 @@ snapshots: tinyexec: 1.3.1 tinyglobby: 0.2.17 tinyrainbow: 3.1.0 - vite: 6.4.5(@types/node@24.13.5)(jiti@2.7.0)(lightningcss@1.33.0)(tsx@4.23.13)(yaml@2.9.0) + vite: 8.3.3(@types/node@24.13.5)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0) why-is-node-running: 2.3.0 optionalDependencies: '@opentelemetry/api': 1.9.1 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 698e7d363..515a820ae 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -94,7 +94,7 @@ catalog: tsdown: ^0.12.9 tsx: ^4.20.3 tw-animate-css: ^1.3.4 - typescript: ^5.8.3 + typescript: ^5.9.3 valibot: ^1.4.2 vite: ^6.4.5 vitest: ^4.1.11