@sentry/api 1.25.0 → 1.26.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -553,7 +553,7 @@ Generic helpers delegate cancellation to your fetcher. Pass its signal through t
553
553
 
554
554
  `@sentry/api/client` is the request layer the Sentry CLI and MCP servers share, built on the generated operations. It uses only web-standard APIs (`fetch`, `Request`, `Headers`, `URL`, `AbortSignal`, `crypto.subtle`), so it runs in Node, Cloudflare Workers, and browsers. Hosts inject what differs between them:
555
555
 
556
- - **Transport.** `createApiTransport` sets `Authorization` from a `CredentialProvider`, refreshes the credential once after a 401, times out each attempt, retries through the shared retry policy (429 for any method; 500, 502, 503, 504, timeouts, and network errors only for GET, HEAD, OPTIONS, or writes marked idempotent), follows redirects only to trusted origins, and serves and invalidates cached GETs. The host passes credentials, a `TransportCache`, the request-scoped `fresh` hook, a logger, a span hook, and its error classes.
556
+ - **Transport.** `createApiTransport` sets `Authorization` from a `CredentialProvider`, refreshes the credential once after a 401, times out each attempt, retries through the shared retry policy (429 for any method; 500, 502, 503, 504, timeouts, and network errors only for GET, HEAD, OPTIONS, or writes marked idempotent), follows redirects only to trusted origins, and serves and invalidates cached GETs. `CredentialProvider.current` receives the request URL and the organization and project its path targets (`parseApiPathScope`), so a host with several logins can pick the one that can reach that organization. The host passes credentials, a `TransportCache`, the request-scoped `fresh` hook, a logger, a span hook, and its error classes.
557
557
  - **Request core.** `createApiClientCore` sends `<origin>/api/0<path>` through the transport and throws the status's `ApiError` subclass for an error response. `sdkClient` gives a generated operation a client that sends through the same path.
558
558
  - **Response cache.** The cache policy (`lookupResponse`, `storeResponse`, tiers, identity-scoped keys, invalidation) works on any `CacheStore` and `BodyCodec`. `createMemoryStore` is a byte-budgeted in-memory store. Compressing codecs need host APIs such as `node:zlib`, so hosts supply them.
559
559
  - **Validation.** Endpoint wrappers validate responses with any [Standard Schema](https://standardschema.dev) validator the host passes in, such as the `@sentry/api/zod` or `@sentry/api/valibot` schemas. The client depends on neither.
@@ -30,5 +30,25 @@ export declare function buildSentryApiUrl(baseUrl: string, endpoint: string): st
30
30
  * ```
31
31
  */
32
32
  export declare function apiPath(strings: TemplateStringsArray, ...values: (string | number)[]): string;
33
+ /** The organization, and project when present, an API path targets. */
34
+ export type ApiPathScope = {
35
+ organization: string;
36
+ project?: string;
37
+ };
38
+ /**
39
+ * The organization, and project when present, a Sentry API request targets,
40
+ * read from its path: `/organizations/{org}/`, `/projects/{org}/{project}/`,
41
+ * `/teams/{org}/`, and `/organizations/{org}/projects/{project}/`.
42
+ *
43
+ * Accepts a full URL, whose path must contain `/api/0/` (a self-hosted
44
+ * install may serve it under a prefix), or a path relative to `/api/0/` with
45
+ * or without that prefix. Undefined for unscoped paths, list endpoints such
46
+ * as `/organizations/`, and schema placeholders.
47
+ *
48
+ * Hosts with several credentials use it to pick the one that can reach the
49
+ * organization; {@link createApiTransport} passes it to
50
+ * `CredentialProvider.current`.
51
+ */
52
+ export declare function parseApiPathScope(urlOrPath: string): ApiPathScope | undefined;
33
53
  /** `path` with `query` appended, or `path` alone when `query` is empty. */
34
54
  export declare function withQuery(path: string, query: URLSearchParams): string;
@@ -240,6 +240,41 @@ function apiPath(strings, ...values) {
240
240
  return acc + literal + encoded;
241
241
  }, "");
242
242
  }
243
+ var API_PATH_IDENTIFIER = /^[a-zA-Z0-9][a-zA-Z0-9._-]*$/;
244
+ function isApiPathIdentifier(segment) {
245
+ return segment !== void 0 && API_PATH_IDENTIFIER.test(segment);
246
+ }
247
+ function apiPathSegments(urlOrPath) {
248
+ const isUrl = URL.canParse(urlOrPath);
249
+ const path = isUrl ? new URL(urlOrPath).pathname : urlOrPath.split(/[?#]/, 1)[0] ?? "";
250
+ const segments = path.split("/").filter((segment) => segment.length > 0);
251
+ for (let index = 0; index < segments.length - 1; index += 1) {
252
+ if (segments[index] === "api" && segments[index + 1] === "0") {
253
+ return segments.slice(index + 2);
254
+ }
255
+ }
256
+ return isUrl ? void 0 : segments;
257
+ }
258
+ function parseApiPathScope(urlOrPath) {
259
+ const segments = apiPathSegments(urlOrPath);
260
+ if (!segments) {
261
+ return;
262
+ }
263
+ const [resource, organization, next, project] = segments;
264
+ if (!isApiPathIdentifier(organization)) {
265
+ return;
266
+ }
267
+ if (resource === "organizations") {
268
+ return next === "projects" && isApiPathIdentifier(project) ? { organization, project } : { organization };
269
+ }
270
+ if (resource === "projects") {
271
+ return isApiPathIdentifier(next) ? { organization, project: next } : { organization };
272
+ }
273
+ if (resource === "teams") {
274
+ return { organization };
275
+ }
276
+ return;
277
+ }
243
278
  function withQuery(path, query) {
244
279
  const queryString = query.toString();
245
280
  return queryString ? `${path}?${queryString}` : path;
@@ -1703,7 +1738,7 @@ function createApiTransport(config) {
1703
1738
  const run = async () => {
1704
1739
  const startTime = performance.now();
1705
1740
  const elapsed = () => (performance.now() - startTime).toFixed(0);
1706
- const credential = credentials ? await credentials.current() : ANONYMOUS;
1741
+ const credential = credentials ? await credentials.current(credentialRequest(url)) : ANONYMOUS;
1707
1742
  const lookup = cache && method === "GET" && !options.validateRedirects && !config.fresh?.() ? await cache.lookup({
1708
1743
  url,
1709
1744
  requestHeaders: lookupHeaders(credential),
@@ -1737,6 +1772,10 @@ function createApiTransport(config) {
1737
1772
  }
1738
1773
  return { request };
1739
1774
  }
1775
+ function credentialRequest(url) {
1776
+ const scope = parseApiPathScope(url);
1777
+ return scope ? { url, scope } : { url };
1778
+ }
1740
1779
  async function drainBody(response, signal) {
1741
1780
  const drained = response.body?.pipeTo(new WritableStream());
1742
1781
  if (!drained) {
@@ -1858,6 +1897,7 @@ export {
1858
1897
  normalizeStatsPeriod,
1859
1898
  normalizeUrl,
1860
1899
  paginate,
1900
+ parseApiPathScope,
1861
1901
  parseRetryDelayMs,
1862
1902
  parseStatsPeriod,
1863
1903
  readErrorResponse,
@@ -10,6 +10,7 @@
10
10
  * and spans.
11
11
  */
12
12
  import { type RetryPolicy } from "./retry.js";
13
+ import { type ApiPathScope } from "./api-path.js";
13
14
  import type { CacheLookup } from "./response-cache/policy.js";
14
15
  type FetchInput = Request | string | URL;
15
16
  /** The fields the transport reads; hosts extend it with their own. */
@@ -18,9 +19,23 @@ export type TransportCredential = {
18
19
  /** Stable hash of the login, for cache scoping; never a raw token. */
19
20
  identity: string;
20
21
  };
22
+ /** The request a credential is chosen for; see {@link CredentialProvider.current}. */
23
+ export type CredentialRequest = {
24
+ url: string;
25
+ /**
26
+ * The organization, and project when present, the URL path targets
27
+ * ({@link parseApiPathScope}). Absent for unscoped paths.
28
+ */
29
+ scope?: ApiPathScope;
30
+ };
21
31
  export type CredentialProvider<C extends TransportCredential> = {
22
- /** The credential for a new request, before any refresh. Cache lookups use it. */
23
- current(): C | Promise<C>;
32
+ /**
33
+ * The credential for a new request, before any refresh. Cache lookups use
34
+ * it. A host with several logins can use `request.scope` to pick the one
35
+ * that can reach the organization. The URL is already built, so the
36
+ * credential must be valid for its origin.
37
+ */
38
+ current(request: CredentialRequest): C | Promise<C>;
24
39
  /** Called after a cache miss, before the first attempt, to refresh ahead of expiry. */
25
40
  prepare?(credential: C): Promise<C>;
26
41
  /** After a 401: the refreshed credential, or undefined when it cannot refresh. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sentry/api",
3
- "version": "1.25.0",
3
+ "version": "1.26.0",
4
4
  "description": "Official auto-generated TypeScript client for the Sentry public REST API",
5
5
  "keywords": [
6
6
  "sentry",