@keystrokehq/sentry 0.0.15 → 0.0.16-integration-id-canonicalization.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.
Files changed (80) hide show
  1. package/README.md +40 -187
  2. package/dist/credential-sets/index.d.mts +2 -0
  3. package/dist/credential-sets/index.mjs +4 -0
  4. package/dist/index.d.mts +4 -1
  5. package/dist/index.mjs +6 -1
  6. package/dist/operations/index.d.mts +2 -0
  7. package/dist/operations/index.mjs +3 -0
  8. package/dist/{schemas.d.mts → schemas/index.d.mts} +3 -3
  9. package/dist/schemas/index.mjs +3 -0
  10. package/dist/{schemas.mjs → sentry-api-DjJ24hwX.mjs} +2 -2
  11. package/dist/sentry-app.credential-set-4DLRUKF9.d.mts +59 -0
  12. package/dist/sentry-app.credential-set-Cdj6Qxiz.mjs +23 -0
  13. package/dist/sentry.credential-set-2b3TPFqv.mjs +696 -0
  14. package/dist/view-organization-notification-actions.operation-BI2aB_Mj.mjs +4548 -0
  15. package/dist/view-organization-notification-actions.operation-C7pc8cl_.d.mts +6840 -0
  16. package/package.json +14 -121
  17. package/dist/_official/index.d.mts +0 -41
  18. package/dist/_official/index.mjs +0 -3
  19. package/dist/_runtime/index.d.mts +0 -2
  20. package/dist/_runtime/index.mjs +0 -3
  21. package/dist/alerts.d.mts +0 -462
  22. package/dist/alerts.mjs +0 -300
  23. package/dist/client.d.mts +0 -86
  24. package/dist/client.mjs +0 -233
  25. package/dist/connection.d.mts +0 -3
  26. package/dist/connection.mjs +0 -3
  27. package/dist/dashboards.d.mts +0 -162
  28. package/dist/dashboards.mjs +0 -122
  29. package/dist/debug-files.d.mts +0 -102
  30. package/dist/debug-files.mjs +0 -73
  31. package/dist/deploys.d.mts +0 -86
  32. package/dist/deploys.mjs +0 -67
  33. package/dist/discover.d.mts +0 -194
  34. package/dist/discover.mjs +0 -138
  35. package/dist/endpoint-factory-DcT49a8O.mjs +0 -96
  36. package/dist/environments.d.mts +0 -96
  37. package/dist/environments.mjs +0 -71
  38. package/dist/errors-DfEFwcTe.mjs +0 -142
  39. package/dist/events-api.d.mts +0 -254
  40. package/dist/events-api.mjs +0 -184
  41. package/dist/events.d.mts +0 -754
  42. package/dist/events.mjs +0 -142
  43. package/dist/feedback.d.mts +0 -82
  44. package/dist/feedback.mjs +0 -63
  45. package/dist/integration-D7KCM1-p.mjs +0 -75
  46. package/dist/integration-veqa_7yB.d.mts +0 -33
  47. package/dist/issues.d.mts +0 -458
  48. package/dist/issues.mjs +0 -324
  49. package/dist/members.d.mts +0 -308
  50. package/dist/members.mjs +0 -156
  51. package/dist/messaging.d.mts +0 -1
  52. package/dist/messaging.mjs +0 -1
  53. package/dist/monitors.d.mts +0 -458
  54. package/dist/monitors.mjs +0 -234
  55. package/dist/notifications.d.mts +0 -180
  56. package/dist/notifications.mjs +0 -133
  57. package/dist/organizations.d.mts +0 -442
  58. package/dist/organizations.mjs +0 -334
  59. package/dist/pagination-PlgAqbZt.mjs +0 -116
  60. package/dist/project-keys.d.mts +0 -246
  61. package/dist/project-keys.mjs +0 -122
  62. package/dist/projects.d.mts +0 -534
  63. package/dist/projects.mjs +0 -378
  64. package/dist/releases.d.mts +0 -780
  65. package/dist/releases.mjs +0 -479
  66. package/dist/replays.d.mts +0 -234
  67. package/dist/replays.mjs +0 -192
  68. package/dist/scim.d.mts +0 -324
  69. package/dist/scim.mjs +0 -249
  70. package/dist/scopes-RRU0vt-a.mjs +0 -36
  71. package/dist/teams.d.mts +0 -448
  72. package/dist/teams.mjs +0 -266
  73. package/dist/triggers.d.mts +0 -41
  74. package/dist/triggers.mjs +0 -56
  75. package/dist/user-emails.d.mts +0 -84
  76. package/dist/user-emails.mjs +0 -60
  77. package/dist/verification.d.mts +0 -30
  78. package/dist/verification.mjs +0 -83
  79. package/dist/webhooks.d.mts +0 -158
  80. package/dist/webhooks.mjs +0 -108
@@ -0,0 +1,696 @@
1
+ import { CredentialSet } from "@keystrokehq/core";
2
+ import { z } from "zod";
3
+
4
+ //#region ../../packages/credential-connection/src/token.ts
5
+ /**
6
+ * Shared OAuth 2.0 token-response plumbing.
7
+ *
8
+ * `parseOAuthTokenResponse` handles the two common wire formats (JSON and
9
+ * form-urlencoded, the latter used by GitHub unless you explicitly ask for
10
+ * JSON). `normalizeOAuthTokens` pulls the standard fields out of the parsed
11
+ * body in both the flat form and Slack's nested `authed_user.access_token`
12
+ * form. These helpers are exposed so integration authors overriding
13
+ * `exchangeCode` / `refreshToken` do not have to re-implement them.
14
+ */
15
+ var TokenExchangeError = class extends Error {
16
+ httpStatus;
17
+ constructor(httpStatus) {
18
+ super(`Token exchange failed: HTTP ${httpStatus}`);
19
+ this.name = "TokenExchangeError";
20
+ this.httpStatus = httpStatus;
21
+ }
22
+ };
23
+ async function parseOAuthTokenResponse(response) {
24
+ const contentType = response.headers.get("content-type")?.toLowerCase() ?? "";
25
+ if (contentType.includes("application/json")) return await response.json();
26
+ const text = await response.text();
27
+ if (!text.trim()) return {};
28
+ if (contentType.includes("application/x-www-form-urlencoded") || text.includes("=")) {
29
+ const params = new URLSearchParams(text);
30
+ return Object.fromEntries(params.entries());
31
+ }
32
+ throw new Error(`Unsupported OAuth token response content type: ${contentType || "unknown"}`);
33
+ }
34
+ function normalizeOAuthTokens(tokenData) {
35
+ const authedUser = tokenData.authed_user;
36
+ const rawAccessToken = tokenData.access_token ?? authedUser?.access_token;
37
+ const rawRefreshToken = tokenData.refresh_token;
38
+ const rawExpiresIn = tokenData.expires_in;
39
+ const rawInstanceUrl = tokenData.instance_url;
40
+ return {
41
+ accessToken: typeof rawAccessToken === "string" ? rawAccessToken : void 0,
42
+ refreshToken: typeof rawRefreshToken === "string" ? rawRefreshToken : void 0,
43
+ expiresIn: typeof rawExpiresIn === "number" ? rawExpiresIn : typeof rawExpiresIn === "string" ? Number(rawExpiresIn) || void 0 : void 0,
44
+ instanceUrl: typeof rawInstanceUrl === "string" ? rawInstanceUrl : void 0
45
+ };
46
+ }
47
+
48
+ //#endregion
49
+ //#region ../../packages/credential-connection/src/defaults.ts
50
+ function buildAuthUrl(ctx) {
51
+ const { oauthClient, redirectUri, state, connection } = ctx;
52
+ const scopes = ctx.requestedScopes ?? connection.scopes;
53
+ const url = new URL(connection.authUrl);
54
+ url.searchParams.set("client_id", oauthClient.clientId);
55
+ url.searchParams.set("scope", scopes.join(" "));
56
+ url.searchParams.set("redirect_uri", redirectUri);
57
+ url.searchParams.set("state", state);
58
+ url.searchParams.set("response_type", "code");
59
+ return url.toString();
60
+ }
61
+ async function exchangeToken(params) {
62
+ const response = await fetch(params.tokenUrl, {
63
+ method: "POST",
64
+ headers: {
65
+ Accept: "application/json",
66
+ "Content-Type": "application/x-www-form-urlencoded"
67
+ },
68
+ body: params.body
69
+ });
70
+ if (!response.ok) throw new TokenExchangeError(response.status);
71
+ const raw = await parseOAuthTokenResponse(response);
72
+ const { accessToken, refreshToken, expiresIn, instanceUrl } = normalizeOAuthTokens(raw);
73
+ if (!accessToken) throw new Error("No access token in response");
74
+ return {
75
+ accessToken,
76
+ refreshToken,
77
+ expiresIn,
78
+ instanceUrl,
79
+ raw
80
+ };
81
+ }
82
+ async function exchangeCode(ctx) {
83
+ const { oauthClient, code, redirectUri, connection } = ctx;
84
+ if (!code) throw new Error("Missing authorization code");
85
+ return exchangeToken({
86
+ tokenUrl: connection.tokenUrl,
87
+ body: new URLSearchParams({
88
+ code,
89
+ client_id: oauthClient.clientId,
90
+ client_secret: oauthClient.clientSecret,
91
+ redirect_uri: redirectUri,
92
+ grant_type: "authorization_code"
93
+ })
94
+ });
95
+ }
96
+ async function refreshToken(ctx) {
97
+ const { oauthClient, refreshToken, connection } = ctx;
98
+ return exchangeToken({
99
+ tokenUrl: connection.tokenUrl,
100
+ body: new URLSearchParams({
101
+ grant_type: "refresh_token",
102
+ refresh_token: refreshToken,
103
+ client_id: oauthClient.clientId,
104
+ client_secret: oauthClient.clientSecret
105
+ })
106
+ });
107
+ }
108
+ const oauthDefaults = {
109
+ buildAuthUrl,
110
+ exchangeCode,
111
+ exchangeToken,
112
+ refreshToken
113
+ };
114
+
115
+ //#endregion
116
+ //#region src/utils/errors.ts
117
+ /**
118
+ * Thrown from `createSentryClient(...)` methods on every non-2xx response
119
+ * (after the retry-with-backoff loop gives up) and on any DSN-in-bearer-token
120
+ * misuse. Operation files rely on the `kind` field to choose retry vs
121
+ * bail-out behavior.
122
+ */
123
+ var SentryApiError = class extends Error {
124
+ kind;
125
+ status;
126
+ providerCode;
127
+ requestId;
128
+ retryable;
129
+ retryAfterSec;
130
+ responseBody;
131
+ constructor(init) {
132
+ super(init.message, init.cause !== void 0 ? { cause: init.cause } : void 0);
133
+ this.name = "SentryApiError";
134
+ this.kind = init.kind;
135
+ this.status = init.status;
136
+ this.providerCode = init.providerCode;
137
+ this.requestId = init.requestId;
138
+ this.retryable = init.retryable;
139
+ this.retryAfterSec = init.retryAfterSec;
140
+ this.responseBody = init.responseBody;
141
+ }
142
+ };
143
+ /**
144
+ * Classifies an HTTP response into a typed `SentryApiError`. The raw body may
145
+ * already have been parsed as JSON or left as text; callers pass whichever
146
+ * they produced.
147
+ */
148
+ function classifyHttpResponse(response, body) {
149
+ const requestId = response.headers.get("x-request-id");
150
+ const retryAfterSec = parseRetryAfter(response.headers.get("retry-after"));
151
+ const providerCode = extractProviderCode(body);
152
+ const detail = extractDetail(body, response.status);
153
+ if (response.status === 401 || response.status === 403) return new SentryApiError({
154
+ kind: "auth",
155
+ message: `Sentry API authentication failed (${response.status}): ${detail}`,
156
+ status: response.status,
157
+ providerCode,
158
+ requestId,
159
+ retryable: false,
160
+ retryAfterSec,
161
+ responseBody: body
162
+ });
163
+ if (response.status === 404) return new SentryApiError({
164
+ kind: "not_found",
165
+ message: `Sentry API resource not found (404): ${detail}`,
166
+ status: 404,
167
+ providerCode,
168
+ requestId,
169
+ retryable: false,
170
+ retryAfterSec,
171
+ responseBody: body
172
+ });
173
+ if (response.status === 429) return new SentryApiError({
174
+ kind: "rate_limit",
175
+ message: `Sentry API rate limited (429): ${detail}`,
176
+ status: 429,
177
+ providerCode,
178
+ requestId,
179
+ retryable: true,
180
+ retryAfterSec,
181
+ responseBody: body
182
+ });
183
+ if (response.status === 400 || response.status === 422) return new SentryApiError({
184
+ kind: "validation",
185
+ message: `Sentry API validation failed (${response.status}): ${detail}`,
186
+ status: response.status,
187
+ providerCode,
188
+ requestId,
189
+ retryable: false,
190
+ retryAfterSec,
191
+ responseBody: body
192
+ });
193
+ if (response.status >= 500) return new SentryApiError({
194
+ kind: "http",
195
+ message: `Sentry API server error (${response.status}): ${detail}`,
196
+ status: response.status,
197
+ providerCode,
198
+ requestId,
199
+ retryable: true,
200
+ retryAfterSec,
201
+ responseBody: body
202
+ });
203
+ return new SentryApiError({
204
+ kind: "http",
205
+ message: `Sentry API request failed (${response.status}): ${detail}`,
206
+ status: response.status,
207
+ providerCode,
208
+ requestId,
209
+ retryable: false,
210
+ retryAfterSec,
211
+ responseBody: body
212
+ });
213
+ }
214
+ function parseRetryAfter(header) {
215
+ if (header == null) return null;
216
+ const asNumber = Number(header);
217
+ if (Number.isFinite(asNumber) && asNumber >= 0) return asNumber;
218
+ const asDate = Date.parse(header);
219
+ if (Number.isFinite(asDate)) {
220
+ const diff = Math.ceil((asDate - Date.now()) / 1e3);
221
+ return diff >= 0 ? diff : 0;
222
+ }
223
+ return null;
224
+ }
225
+ function extractDetail(body, status) {
226
+ if (body == null) return `HTTP ${status}`;
227
+ if (typeof body === "string") return body || `HTTP ${status}`;
228
+ if (typeof body === "object") {
229
+ const obj = body;
230
+ if (typeof obj.detail === "string") return obj.detail;
231
+ if (typeof obj.error === "string") return obj.error;
232
+ if (typeof obj.message === "string") return obj.message;
233
+ }
234
+ return `HTTP ${status}`;
235
+ }
236
+ function extractProviderCode(body) {
237
+ if (body == null || typeof body !== "object") return null;
238
+ const obj = body;
239
+ if (typeof obj.code === "string") return obj.code;
240
+ if (typeof obj.error_code === "string") return obj.error_code;
241
+ return null;
242
+ }
243
+
244
+ //#endregion
245
+ //#region src/utils/pagination.ts
246
+ function parseSentryLinkHeader(header) {
247
+ if (header == null || header.length === 0) return {
248
+ previous: null,
249
+ next: null
250
+ };
251
+ let previous = null;
252
+ let next = null;
253
+ for (const entry of splitLinkEntries(header)) {
254
+ const parsed = parseLinkEntry(entry);
255
+ if (!parsed) continue;
256
+ if (parsed.rel === "previous") previous = toPageLink(parsed.url);
257
+ else if (parsed.rel === "next") next = toPageLink(parsed.url);
258
+ }
259
+ return {
260
+ previous,
261
+ next
262
+ };
263
+ }
264
+ /**
265
+ * Splits a single raw `Link` header value into its comma-separated entries.
266
+ * Naive splitting on `,` is unsafe because cursor strings can legally contain
267
+ * commas; we track angle-bracket depth.
268
+ */
269
+ function splitLinkEntries(header) {
270
+ const entries = [];
271
+ let depth = 0;
272
+ let start = 0;
273
+ for (let i = 0; i < header.length; i += 1) {
274
+ const ch = header.charAt(i);
275
+ if (ch === "<") depth += 1;
276
+ else if (ch === ">") depth -= 1;
277
+ else if (ch === "," && depth === 0) {
278
+ entries.push(header.slice(start, i));
279
+ start = i + 1;
280
+ }
281
+ }
282
+ entries.push(header.slice(start));
283
+ return entries.map((e) => e.trim()).filter(Boolean);
284
+ }
285
+ function parseLinkEntry(entry) {
286
+ const openBracket = entry.indexOf("<");
287
+ const closeBracket = entry.indexOf(">", openBracket);
288
+ if (openBracket === -1 || closeBracket === -1) return null;
289
+ const url = entry.slice(openBracket + 1, closeBracket);
290
+ const params = parseLinkParams(entry.slice(closeBracket + 1));
291
+ const rel = params.rel ?? "";
292
+ const results = params.results === "true";
293
+ if (!rel) return null;
294
+ return {
295
+ url,
296
+ rel,
297
+ results
298
+ };
299
+ }
300
+ function parseLinkParams(paramsSection) {
301
+ const params = {};
302
+ for (const part of paramsSection.split(";").map((s) => s.trim())) {
303
+ if (!part) continue;
304
+ const eq = part.indexOf("=");
305
+ if (eq === -1) continue;
306
+ const key = part.slice(0, eq).trim();
307
+ let value = part.slice(eq + 1).trim();
308
+ if (value.startsWith("\"") && value.endsWith("\"")) value = value.slice(1, -1);
309
+ if (key) params[key] = value;
310
+ }
311
+ return params;
312
+ }
313
+ function toPageLink(url) {
314
+ return {
315
+ url,
316
+ cursor: extractCursor(url),
317
+ results: extractResults(url)
318
+ };
319
+ }
320
+ function extractCursor(url) {
321
+ const queryIndex = url.indexOf("?");
322
+ if (queryIndex === -1) return null;
323
+ const query = url.slice(queryIndex + 1);
324
+ for (const pair of query.split("&")) {
325
+ const eq = pair.indexOf("=");
326
+ if (eq === -1) continue;
327
+ if (pair.slice(0, eq) === "cursor") return decodeURIComponent(pair.slice(eq + 1));
328
+ }
329
+ return null;
330
+ }
331
+ function extractResults(url) {
332
+ const queryIndex = url.indexOf("?");
333
+ if (queryIndex === -1) return false;
334
+ const query = url.slice(queryIndex + 1);
335
+ for (const pair of query.split("&")) {
336
+ const eq = pair.indexOf("=");
337
+ if (eq === -1) continue;
338
+ if (pair.slice(0, eq) === "results") return pair.slice(eq + 1) === "true";
339
+ }
340
+ return false;
341
+ }
342
+ /**
343
+ * Detects a Sentry DSN pasted where a bearer auth token was expected. DSNs
344
+ * look like `https://<publicKey>@o<org>.ingest.sentry.io/<projectId>` (or
345
+ * http(s) against a self-hosted host with an `@` before the host). We never
346
+ * want to send these to the management API.
347
+ */
348
+ function looksLikeSentryDsn(value) {
349
+ if (!value.includes("://")) return false;
350
+ try {
351
+ const parsed = new URL(value);
352
+ if (parsed.protocol !== "https:" && parsed.protocol !== "http:") return false;
353
+ return parsed.username.length > 0;
354
+ } catch {
355
+ return false;
356
+ }
357
+ }
358
+
359
+ //#endregion
360
+ //#region src/utils/client.ts
361
+ /**
362
+ * Typed fetch-based client for the Sentry management API.
363
+ *
364
+ * The factory accepts the public connection credentials
365
+ * (`SentryCredentials`) and returns a thin object with one method per HTTP
366
+ * verb, plus a `paginate` helper that walks Sentry's `Link`-header cursor
367
+ * chain and yields decoded results one batch at a time.
368
+ *
369
+ * Design notes:
370
+ * - All requests go through `request()` so retry, DSN rejection, and error
371
+ * normalization live in one place.
372
+ * - Retry policy: honours `Retry-After` on 429 and does exponential backoff
373
+ * with jitter on 5xx; all other 4xx responses throw immediately.
374
+ * - Base URL: `SENTRY_REGION_URL` (persisted at install time by the OAuth
375
+ * hook) wins; otherwise default `https://sentry.io`. Self-hosted deployments
376
+ * override the region URL.
377
+ * - The SDK path is `/api/0/` on whatever base URL the caller resolved.
378
+ */
379
+ const SENTRY_DEFAULT_BASE_URL = "https://sentry.io";
380
+ const DEFAULT_MAX_RETRIES = 3;
381
+ const DEFAULT_BASE_DELAY_MS = 250;
382
+ const DEFAULT_MAX_DELAY_MS = 3e4;
383
+ const DEFAULT_TIMEOUT_MS = 3e4;
384
+ function createSentryClient(credentials, options = {}) {
385
+ const token = credentials.SENTRY_ACCESS_TOKEN;
386
+ if (looksLikeSentryDsn(token)) throw new SentryApiError({
387
+ kind: "dsn_detected",
388
+ message: "A Sentry DSN was passed in place of a management-API bearer token. DSNs can only be used to send events; the management API requires an OAuth access token, auth token, or internal integration token.",
389
+ status: null,
390
+ providerCode: null,
391
+ requestId: null,
392
+ retryable: false,
393
+ retryAfterSec: null
394
+ });
395
+ const baseUrl = (credentials.SENTRY_REGION_URL ?? SENTRY_DEFAULT_BASE_URL).replace(/\/$/u, "");
396
+ const fetchImpl = options.fetch ?? fetch;
397
+ const maxRetries = options.maxRetries ?? DEFAULT_MAX_RETRIES;
398
+ const retryBaseDelayMs = options.retryBaseDelayMs ?? DEFAULT_BASE_DELAY_MS;
399
+ const maxDelayMs = options.maxDelayMs ?? DEFAULT_MAX_DELAY_MS;
400
+ const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
401
+ const sleep = options.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
402
+ async function request(init) {
403
+ const url = buildUrl(baseUrl, init.path, init.query);
404
+ const headers = {
405
+ Authorization: `Bearer ${token}`,
406
+ Accept: "application/json",
407
+ ...init.headers ?? {}
408
+ };
409
+ let body;
410
+ if (init.rawBody !== void 0) body = init.rawBody;
411
+ else if (init.body !== void 0 && init.method !== "GET" && init.method !== "DELETE") if (init.contentType === "application/x-www-form-urlencoded") {
412
+ headers["Content-Type"] = "application/x-www-form-urlencoded";
413
+ body = new URLSearchParams(init.body).toString();
414
+ } else {
415
+ headers["Content-Type"] = init.contentType ?? "application/json";
416
+ body = typeof init.body === "string" || init.body instanceof ArrayBuffer ? init.body : JSON.stringify(init.body);
417
+ }
418
+ let attempt = 0;
419
+ while (true) {
420
+ attempt += 1;
421
+ const controller = new AbortController();
422
+ const timeoutHandle = setTimeout(() => controller.abort(), timeoutMs);
423
+ let response;
424
+ try {
425
+ response = await fetchImpl(url, {
426
+ method: init.method,
427
+ headers,
428
+ body,
429
+ signal: controller.signal
430
+ });
431
+ } catch (cause) {
432
+ clearTimeout(timeoutHandle);
433
+ if (attempt > maxRetries) throw new SentryApiError({
434
+ kind: "network",
435
+ message: `Sentry API network error: ${extractCauseMessage(cause)}`,
436
+ status: null,
437
+ providerCode: null,
438
+ requestId: null,
439
+ retryable: true,
440
+ retryAfterSec: null,
441
+ cause
442
+ });
443
+ await sleep(computeBackoffDelay(attempt, retryBaseDelayMs, maxDelayMs));
444
+ continue;
445
+ }
446
+ clearTimeout(timeoutHandle);
447
+ if (response.ok) {
448
+ const data = await parseResponseBody(response, init.parseAs ?? "json");
449
+ const links = parseSentryLinkHeader(response.headers.get("link"));
450
+ return {
451
+ data,
452
+ status: response.status,
453
+ headers: response.headers,
454
+ nextCursor: links.next?.results ? links.next.cursor : null,
455
+ previousCursor: links.previous?.results ? links.previous.cursor : null
456
+ };
457
+ }
458
+ const errorBody = await parseErrorBody(response);
459
+ const error = classifyHttpResponse(response, errorBody);
460
+ if (!error.retryable || attempt > maxRetries) throw error;
461
+ await sleep(error.retryAfterSec != null ? Math.min(error.retryAfterSec * 1e3, maxDelayMs) : computeBackoffDelay(attempt, retryBaseDelayMs, maxDelayMs));
462
+ }
463
+ }
464
+ async function get(path, query) {
465
+ return request({
466
+ method: "GET",
467
+ path,
468
+ query
469
+ });
470
+ }
471
+ async function post(path, bodyValue, query) {
472
+ return request({
473
+ method: "POST",
474
+ path,
475
+ body: bodyValue,
476
+ query
477
+ });
478
+ }
479
+ async function put(path, bodyValue, query) {
480
+ return request({
481
+ method: "PUT",
482
+ path,
483
+ body: bodyValue,
484
+ query
485
+ });
486
+ }
487
+ async function patch(path, bodyValue, query) {
488
+ return request({
489
+ method: "PATCH",
490
+ path,
491
+ body: bodyValue,
492
+ query
493
+ });
494
+ }
495
+ async function del(path, query) {
496
+ return request({
497
+ method: "DELETE",
498
+ path,
499
+ query,
500
+ parseAs: "none"
501
+ });
502
+ }
503
+ async function* paginate(path, query = {}) {
504
+ let cursor = null;
505
+ do {
506
+ const merged = { ...query };
507
+ if (cursor != null) merged.cursor = cursor;
508
+ const page = await get(path, merged);
509
+ yield page;
510
+ cursor = page.nextCursor;
511
+ } while (cursor !== null);
512
+ }
513
+ async function listAll(path, query) {
514
+ const acc = [];
515
+ for await (const page of paginate(path, query)) acc.push(...page.data);
516
+ return acc;
517
+ }
518
+ return {
519
+ get,
520
+ post,
521
+ put,
522
+ patch,
523
+ delete: del,
524
+ paginate,
525
+ listAll,
526
+ request,
527
+ resolveBaseUrl: () => baseUrl
528
+ };
529
+ }
530
+ function buildUrl(baseUrl, path, query) {
531
+ const base = `${baseUrl}${path.startsWith("/") ? path : `/${path}`}`;
532
+ if (!query) return base;
533
+ const params = new URLSearchParams();
534
+ for (const [key, value] of Object.entries(query)) {
535
+ if (value == null) continue;
536
+ if (Array.isArray(value)) for (const v of value) {
537
+ if (v == null) continue;
538
+ params.append(key, String(v));
539
+ }
540
+ else if (typeof value === "boolean") params.append(key, value ? "true" : "false");
541
+ else params.append(key, String(value));
542
+ }
543
+ const qs = params.toString();
544
+ return qs ? `${base}?${qs}` : base;
545
+ }
546
+ async function parseResponseBody(response, mode) {
547
+ if (mode === "none") return void 0;
548
+ if (mode === "text") return await response.text();
549
+ if (response.status === 204) return void 0;
550
+ const raw = await response.text();
551
+ if (raw.length === 0) return void 0;
552
+ try {
553
+ return JSON.parse(raw);
554
+ } catch (cause) {
555
+ throw new SentryApiError({
556
+ kind: "http",
557
+ message: `Sentry API returned non-JSON body (status ${response.status})`,
558
+ status: response.status,
559
+ providerCode: null,
560
+ requestId: response.headers.get("x-request-id"),
561
+ retryable: false,
562
+ retryAfterSec: null,
563
+ responseBody: raw,
564
+ cause
565
+ });
566
+ }
567
+ }
568
+ async function parseErrorBody(response) {
569
+ const raw = await response.text();
570
+ if (raw.length === 0) return null;
571
+ try {
572
+ return JSON.parse(raw);
573
+ } catch {
574
+ return raw;
575
+ }
576
+ }
577
+ function computeBackoffDelay(attempt, baseMs, maxMs) {
578
+ const exponential = baseMs * 2 ** Math.max(0, attempt - 1);
579
+ const jitter = Math.random() * baseMs;
580
+ return Math.min(exponential + jitter, maxMs);
581
+ }
582
+ function extractCauseMessage(cause) {
583
+ if (cause instanceof Error) return cause.message;
584
+ if (typeof cause === "string") return cause;
585
+ return "unknown network error";
586
+ }
587
+
588
+ //#endregion
589
+ //#region src/utils/oauth-connection.ts
590
+ const SENTRY_OAUTH_SCOPES = [
591
+ "org:read",
592
+ "project:read",
593
+ "project:write",
594
+ "project:admin",
595
+ "team:read",
596
+ "team:write",
597
+ "team:admin",
598
+ "member:read",
599
+ "member:write",
600
+ "event:read",
601
+ "event:admin",
602
+ "alerts:read",
603
+ "alerts:write",
604
+ "release:admin"
605
+ ];
606
+ const sentryOAuthConnection = {
607
+ kind: "oauth",
608
+ tokenType: "refreshable",
609
+ authUrl: "https://sentry.io/oauth/authorize/",
610
+ tokenUrl: "https://sentry.io/oauth/token/",
611
+ revokeUrl: null,
612
+ scopes: [...SENTRY_OAUTH_SCOPES],
613
+ vault: { accessToken: "SENTRY_ACCESS_TOKEN" },
614
+ async exchangeCode(ctx) {
615
+ const result = await oauthDefaults.exchangeCode(ctx);
616
+ const orgResponse = await fetch(`${SENTRY_DEFAULT_BASE_URL}/api/0/organizations/`, {
617
+ method: "GET",
618
+ headers: {
619
+ Authorization: `Bearer ${result.accessToken}`,
620
+ Accept: "application/json"
621
+ }
622
+ });
623
+ let firstOrg = null;
624
+ if (orgResponse.ok) {
625
+ const orgs = await orgResponse.json();
626
+ if (Array.isArray(orgs) && orgs.length > 0 && isOrganizationProbe(orgs[0])) firstOrg = orgs[0];
627
+ }
628
+ result.raw._sentryOrganization = firstOrg;
629
+ result.raw._sentryUser = pickUser(result.raw);
630
+ result.raw._sentryScopes = typeof result.raw.scope === "string" ? result.raw.scope : void 0;
631
+ return result;
632
+ },
633
+ extractInstallationInfo({ tokenResult }) {
634
+ const org = tokenResult.raw._sentryOrganization;
635
+ const user = tokenResult.raw._sentryUser;
636
+ const metadata = {};
637
+ if (org?.name) metadata.organizationName = org.name;
638
+ if (org?.slug) metadata.organizationSlug = org.slug;
639
+ const regionUrl = org?.links?.regionUrl;
640
+ if (regionUrl) metadata.regionUrl = regionUrl;
641
+ if (user?.name) metadata.userName = user.name;
642
+ if (user?.email) metadata.userEmail = user.email;
643
+ return {
644
+ externalInstallationId: org?.slug,
645
+ externalWorkspaceId: org?.id,
646
+ externalBotUserId: user?.id,
647
+ metadata
648
+ };
649
+ }
650
+ };
651
+ function isOrganizationProbe(value) {
652
+ return typeof value === "object" && value !== null;
653
+ }
654
+ function pickUser(raw) {
655
+ const user = raw.user;
656
+ if (user == null || typeof user !== "object") return null;
657
+ const u = user;
658
+ return {
659
+ id: typeof u.id === "string" ? u.id : void 0,
660
+ name: typeof u.name === "string" ? u.name : void 0,
661
+ email: typeof u.email === "string" ? u.email : void 0
662
+ };
663
+ }
664
+
665
+ //#endregion
666
+ //#region src/credential-sets/sentry.credential-set.ts
667
+ const sentryAuthSchema = z.object({
668
+ SENTRY_ACCESS_TOKEN: z.string().min(1),
669
+ SENTRY_REGION_URL: z.string().url().optional(),
670
+ SENTRY_ORG_SLUG: z.string().min(1).optional()
671
+ });
672
+ /**
673
+ * User-facing Sentry credentials.
674
+ *
675
+ * Supports OAuth-installed integrations (Internal/Public Integrations) and
676
+ * manual personal/organization auth tokens via the same vault shape:
677
+ * `SENTRY_ACCESS_TOKEN` holds the bearer token in both cases.
678
+ */
679
+ const sentryCredentialSet = new CredentialSet({
680
+ id: "sentry",
681
+ name: "Sentry",
682
+ description: "Sentry — issue lifecycle, release tracking, alerts, monitors, replays, feedback, and integration-platform webhook triggers",
683
+ auth: sentryAuthSchema,
684
+ proxy: { hosts: [
685
+ "sentry.io",
686
+ "us.sentry.io",
687
+ "de.sentry.io"
688
+ ] },
689
+ connections: [{
690
+ id: "oauth",
691
+ ...sentryOAuthConnection
692
+ }]
693
+ });
694
+
695
+ //#endregion
696
+ export { createSentryClient as n, sentryCredentialSet as t };