alchemy 0.16.5 → 0.16.7
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/lib/cloudflare/account-api-token.d.ts +1 -1
- package/lib/cloudflare/account-api-token.d.ts.map +1 -1
- package/lib/cloudflare/account-api-token.js.map +1 -1
- package/lib/cloudflare/account-id.d.ts +41 -0
- package/lib/cloudflare/account-id.d.ts.map +1 -0
- package/lib/cloudflare/account-id.js +40 -0
- package/lib/cloudflare/account-id.js.map +1 -0
- package/lib/cloudflare/index.d.ts +1 -0
- package/lib/cloudflare/index.d.ts.map +1 -1
- package/lib/cloudflare/index.js +1 -0
- package/lib/cloudflare/index.js.map +1 -1
- package/lib/cloudflare/permission-groups.d.ts +1 -1
- package/lib/cloudflare/permission-groups.d.ts.map +1 -1
- package/lib/cloudflare/wrangler.json.d.ts +1 -1
- package/lib/cloudflare/wrangler.json.d.ts.map +1 -1
- package/lib/cloudflare/wrangler.json.js +1 -1
- package/lib/cloudflare/wrangler.json.js.map +1 -1
- package/lib/sentry/api.d.ts +50 -0
- package/lib/sentry/api.d.ts.map +1 -0
- package/lib/sentry/api.js +76 -0
- package/lib/sentry/api.js.map +1 -0
- package/lib/sentry/client-key.d.ts +156 -0
- package/lib/sentry/client-key.d.ts.map +1 -0
- package/lib/sentry/client-key.js +126 -0
- package/lib/sentry/client-key.js.map +1 -0
- package/lib/sentry/index.d.ts +5 -0
- package/lib/sentry/index.d.ts.map +1 -0
- package/lib/sentry/index.js +5 -0
- package/lib/sentry/index.js.map +1 -0
- package/lib/sentry/project.d.ts +235 -0
- package/lib/sentry/project.d.ts.map +1 -0
- package/lib/sentry/project.js +146 -0
- package/lib/sentry/project.js.map +1 -0
- package/lib/sentry/team.d.ts +109 -0
- package/lib/sentry/team.d.ts.map +1 -0
- package/lib/sentry/team.js +109 -0
- package/lib/sentry/team.js.map +1 -0
- package/package.json +2 -1
- package/src/cloudflare/account-api-token.ts +4 -1
- package/src/cloudflare/account-id.ts +48 -0
- package/src/cloudflare/index.ts +1 -0
- package/src/cloudflare/permission-groups.ts +1 -1
- package/src/cloudflare/wrangler.json.ts +2 -2
- package/src/sentry/api.ts +106 -0
- package/src/sentry/client-key.ts +295 -0
- package/src/sentry/index.ts +4 -0
- package/src/sentry/project.ts +420 -0
- package/src/sentry/team.ts +235 -0
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { Resource } from "../resource.js";
|
|
2
|
+
import { SentryApi } from "./api.js";
|
|
3
|
+
/**
|
|
4
|
+
* Create and manage Sentry teams
|
|
5
|
+
*
|
|
6
|
+
* @example
|
|
7
|
+
* // Create a basic Sentry team:
|
|
8
|
+
* const team = await Team("my-team", {
|
|
9
|
+
* name: "My Team",
|
|
10
|
+
* organization: "my-org"
|
|
11
|
+
* });
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* // Create a team with a custom slug:
|
|
15
|
+
* const team = await Team("custom-team", {
|
|
16
|
+
* name: "Custom Team",
|
|
17
|
+
* organization: "my-org",
|
|
18
|
+
* slug: "custom-team-slug"
|
|
19
|
+
* });
|
|
20
|
+
*
|
|
21
|
+
* @example
|
|
22
|
+
* // Create or adopt an existing team with the same slug:
|
|
23
|
+
* const team = await Team("existing-team", {
|
|
24
|
+
* name: "Existing Team",
|
|
25
|
+
* organization: "my-org",
|
|
26
|
+
* adopt: true
|
|
27
|
+
* });
|
|
28
|
+
*/
|
|
29
|
+
export const Team = Resource("sentry::Team", async function (id, props) {
|
|
30
|
+
const api = new SentryApi({ authToken: props.authToken });
|
|
31
|
+
if (this.phase === "delete") {
|
|
32
|
+
try {
|
|
33
|
+
if (this.output?.id) {
|
|
34
|
+
const response = await api.delete(`/teams/${props.organization}/${this.output.slug || this.output.id}/`);
|
|
35
|
+
if (!response.ok && response.status !== 404) {
|
|
36
|
+
console.error("Error deleting team:", response.statusText);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
catch (error) {
|
|
41
|
+
console.error("Error deleting team:", error);
|
|
42
|
+
}
|
|
43
|
+
return this.destroy();
|
|
44
|
+
}
|
|
45
|
+
else {
|
|
46
|
+
try {
|
|
47
|
+
let response;
|
|
48
|
+
if (this.phase === "update" && this.output?.id) {
|
|
49
|
+
response = await api.put(`/teams/${props.organization}/${this.output.slug || this.output.id}/`, props);
|
|
50
|
+
}
|
|
51
|
+
else {
|
|
52
|
+
try {
|
|
53
|
+
response = await api.post(`/organizations/${props.organization}/teams/`, props);
|
|
54
|
+
}
|
|
55
|
+
catch (error) {
|
|
56
|
+
// Check if this is a "team already exists" error and adopt is enabled
|
|
57
|
+
if (props.adopt &&
|
|
58
|
+
error instanceof Error &&
|
|
59
|
+
error.message.includes("already exists")) {
|
|
60
|
+
console.log(`Team '${props.slug || props.name}' already exists, adopting it`);
|
|
61
|
+
// Find the existing team by slug
|
|
62
|
+
const existingTeam = await findTeamBySlug(api, props.organization, props.slug || props.name);
|
|
63
|
+
if (!existingTeam) {
|
|
64
|
+
throw new Error(`Failed to find existing team '${props.slug || props.name}' for adoption`);
|
|
65
|
+
}
|
|
66
|
+
response = await api.get(`/organizations/${props.organization}/teams/${existingTeam.slug}/`);
|
|
67
|
+
}
|
|
68
|
+
else {
|
|
69
|
+
throw error;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
if (!response.ok) {
|
|
74
|
+
throw new Error(`API error: ${response.statusText}`);
|
|
75
|
+
}
|
|
76
|
+
const data = (await response.json());
|
|
77
|
+
return this({
|
|
78
|
+
...props,
|
|
79
|
+
id: data.id,
|
|
80
|
+
dateCreated: data.dateCreated,
|
|
81
|
+
isMember: data.isMember,
|
|
82
|
+
teamRole: data.teamRole,
|
|
83
|
+
flags: data.flags,
|
|
84
|
+
access: data.access,
|
|
85
|
+
hasAccess: data.hasAccess,
|
|
86
|
+
isPending: data.isPending,
|
|
87
|
+
memberCount: data.memberCount,
|
|
88
|
+
avatar: data.avatar,
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
catch (error) {
|
|
92
|
+
console.error("Error creating/updating team:", error);
|
|
93
|
+
throw error;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
/**
|
|
98
|
+
* Find a team by slug
|
|
99
|
+
*/
|
|
100
|
+
async function findTeamBySlug(api, organization, slug) {
|
|
101
|
+
const response = await api.get(`/teams/${organization}/${slug}/`);
|
|
102
|
+
if (!response.ok) {
|
|
103
|
+
throw new Error(`API error: ${response.statusText}`);
|
|
104
|
+
}
|
|
105
|
+
const teams = (await response.json());
|
|
106
|
+
const team = teams.find((t) => t.slug === slug);
|
|
107
|
+
return team ? { id: team.id, slug: team.slug } : null;
|
|
108
|
+
}
|
|
109
|
+
//# sourceMappingURL=team.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"team.js","sourceRoot":"","sources":["../../src/sentry/team.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "alchemy",
|
|
3
|
-
"version": "0.16.
|
|
3
|
+
"version": "0.16.7",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"module": "./lib/index.js",
|
|
6
6
|
"scripts": {
|
|
@@ -23,6 +23,7 @@
|
|
|
23
23
|
"./github": "./lib/github/index.js",
|
|
24
24
|
"./neon": "./lib/neon/index.js",
|
|
25
25
|
"./os": "./lib/os/index.js",
|
|
26
|
+
"./sentry": "./lib/sentry/index.js",
|
|
26
27
|
"./shadcn": "./lib/shadcn/index.js",
|
|
27
28
|
"./stripe": "./lib/stripe/index.js",
|
|
28
29
|
"./test/bun": "./lib/test/bun.js",
|
|
@@ -50,12 +50,15 @@ export interface TokenPolicy {
|
|
|
50
50
|
* @see https://developers.cloudflare.com/fundamentals/api/reference/permissions/
|
|
51
51
|
*/
|
|
52
52
|
type TokenPolicyResourceKey =
|
|
53
|
+
| "com.cloudflare.edge.r2.bucket"
|
|
54
|
+
| `com.cloudflare.edge.r2.bucket.${string}`
|
|
53
55
|
| `com.cloudflare.api.account`
|
|
54
56
|
| `com.cloudflare.api.account.${string}`
|
|
55
57
|
// | `com.cloudflare.api.account.zone`
|
|
56
58
|
| `com.cloudflare.api.account.zone.${string}`
|
|
57
59
|
// | `com.cloudflare.api.user`
|
|
58
|
-
| `com.cloudflare.api.user.${string}
|
|
60
|
+
| `com.cloudflare.api.user.${string}`
|
|
61
|
+
| (string & {});
|
|
59
62
|
|
|
60
63
|
/**
|
|
61
64
|
* Condition for token usage (e.g., IP restrictions)
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { createCloudflareApi, type CloudflareApiOptions } from "./api.js";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Options for getting a Cloudflare account ID
|
|
5
|
+
*/
|
|
6
|
+
export interface GetCloudflareAccountIdOptions extends CloudflareApiOptions {}
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Retrieves a Cloudflare Account ID for use with other Cloudflare resources.
|
|
10
|
+
*
|
|
11
|
+
* This function allows you to programmatically access your Cloudflare account ID,
|
|
12
|
+
* which is required for many Cloudflare API operations.
|
|
13
|
+
*
|
|
14
|
+
* If accountId is provided in the options or via CLOUDFLARE_ACCOUNT_ID environment variable,
|
|
15
|
+
* that value will be used. Otherwise, the account ID will be automatically retrieved
|
|
16
|
+
* from the Cloudflare API.
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* // Get account ID from environment variables or API token
|
|
20
|
+
* const accountId = await AccountId();
|
|
21
|
+
*
|
|
22
|
+
* @example
|
|
23
|
+
* // Provide an API key and email directly
|
|
24
|
+
* const accountId = await AccountId({
|
|
25
|
+
* apiKey: alchemy.secret(process.env.CLOUDFLARE_API_KEY),
|
|
26
|
+
* email: "user@example.com"
|
|
27
|
+
* });
|
|
28
|
+
*
|
|
29
|
+
* @example
|
|
30
|
+
* // Use the account ID with a Worker
|
|
31
|
+
* import { Worker, AccountId } from "alchemy/cloudflare";
|
|
32
|
+
*
|
|
33
|
+
* const accountId = await AccountId();
|
|
34
|
+
*
|
|
35
|
+
* await Worker("my-worker", {
|
|
36
|
+
* name: "my-worker",
|
|
37
|
+
* script: "console.log('Hello, world!')",
|
|
38
|
+
* accountId
|
|
39
|
+
* });
|
|
40
|
+
*/
|
|
41
|
+
export async function AccountId(
|
|
42
|
+
options: GetCloudflareAccountIdOptions = {},
|
|
43
|
+
): Promise<string> {
|
|
44
|
+
// Create Cloudflare API client with automatic account discovery
|
|
45
|
+
const api = await createCloudflareApi(options);
|
|
46
|
+
|
|
47
|
+
return api.accountId;
|
|
48
|
+
}
|
package/src/cloudflare/index.ts
CHANGED
|
@@ -32,7 +32,7 @@ interface PermissionGroupsResponse {
|
|
|
32
32
|
messages: any[];
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
-
export type PermissionGroupName = R2PermissionGroups;
|
|
35
|
+
export type PermissionGroupName = R2PermissionGroups | (string & {});
|
|
36
36
|
|
|
37
37
|
export type R2PermissionGroups =
|
|
38
38
|
| "Workers R2 Storage Write"
|
|
@@ -236,7 +236,7 @@ export interface WranglerJsonSpec {
|
|
|
236
236
|
/**
|
|
237
237
|
* Vectorize index bindings
|
|
238
238
|
*/
|
|
239
|
-
|
|
239
|
+
vectorize?: {
|
|
240
240
|
binding: string;
|
|
241
241
|
index_name: string;
|
|
242
242
|
}[];
|
|
@@ -481,7 +481,7 @@ function processBindings(
|
|
|
481
481
|
}
|
|
482
482
|
|
|
483
483
|
if (vectorizeIndexes.length > 0) {
|
|
484
|
-
spec.
|
|
484
|
+
spec.vectorize = vectorizeIndexes;
|
|
485
485
|
}
|
|
486
486
|
|
|
487
487
|
if (new_sqlite_classes.length > 0 || new_classes.length > 0) {
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { alchemy } from "../alchemy.js";
|
|
2
|
+
import type { Secret } from "../secret.js";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Options for Sentry API requests
|
|
6
|
+
*/
|
|
7
|
+
export interface SentryApiOptions {
|
|
8
|
+
/**
|
|
9
|
+
* Auth token to use (overrides environment variable)
|
|
10
|
+
*/
|
|
11
|
+
authToken?: Secret;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Minimal API client using raw fetch
|
|
16
|
+
*/
|
|
17
|
+
export class SentryApi {
|
|
18
|
+
/** Base URL for API */
|
|
19
|
+
readonly baseUrl: string;
|
|
20
|
+
|
|
21
|
+
/** Auth token */
|
|
22
|
+
readonly authToken: Secret;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Create a new API client
|
|
26
|
+
*
|
|
27
|
+
* @param options API options
|
|
28
|
+
*/
|
|
29
|
+
constructor(options: SentryApiOptions = {}) {
|
|
30
|
+
this.baseUrl = "https://sentry.io/api/0";
|
|
31
|
+
this.authToken =
|
|
32
|
+
options.authToken ?? alchemy.secret(process.env.SENTRY_AUTH_TOKEN || "");
|
|
33
|
+
|
|
34
|
+
if (!this.authToken.unencrypted) {
|
|
35
|
+
throw new Error("SENTRY_AUTH_TOKEN environment variable is required");
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Make a request to the API
|
|
41
|
+
*
|
|
42
|
+
* @param path API path (without base URL)
|
|
43
|
+
* @param init Fetch init options
|
|
44
|
+
* @returns Raw Response object from fetch
|
|
45
|
+
*/
|
|
46
|
+
async fetch(path: string, init: RequestInit = {}): Promise<Response> {
|
|
47
|
+
const headers: Record<string, string> = {
|
|
48
|
+
"Content-Type": "application/json",
|
|
49
|
+
Authorization: `Bearer ${this.authToken.unencrypted}`,
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
if (init.headers) {
|
|
53
|
+
const initHeaders = init.headers as Record<string, string>;
|
|
54
|
+
Object.keys(initHeaders).forEach((key) => {
|
|
55
|
+
headers[key] = initHeaders[key];
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (init.body instanceof FormData) {
|
|
60
|
+
delete headers["Content-Type"];
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return fetch(`${this.baseUrl}${path}`, {
|
|
64
|
+
...init,
|
|
65
|
+
headers,
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Helper for GET requests
|
|
71
|
+
*/
|
|
72
|
+
async get(path: string, init: RequestInit = {}): Promise<Response> {
|
|
73
|
+
return this.fetch(path, { ...init, method: "GET" });
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Helper for POST requests
|
|
78
|
+
*/
|
|
79
|
+
async post(
|
|
80
|
+
path: string,
|
|
81
|
+
body: any,
|
|
82
|
+
init: RequestInit = {},
|
|
83
|
+
): Promise<Response> {
|
|
84
|
+
const requestBody = body instanceof FormData ? body : JSON.stringify(body);
|
|
85
|
+
return this.fetch(path, { ...init, method: "POST", body: requestBody });
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Helper for PUT requests
|
|
90
|
+
*/
|
|
91
|
+
async put(
|
|
92
|
+
path: string,
|
|
93
|
+
body: any,
|
|
94
|
+
init: RequestInit = {},
|
|
95
|
+
): Promise<Response> {
|
|
96
|
+
const requestBody = body instanceof FormData ? body : JSON.stringify(body);
|
|
97
|
+
return this.fetch(path, { ...init, method: "PUT", body: requestBody });
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Helper for DELETE requests
|
|
102
|
+
*/
|
|
103
|
+
async delete(path: string, init: RequestInit = {}): Promise<Response> {
|
|
104
|
+
return this.fetch(path, { ...init, method: "DELETE" });
|
|
105
|
+
}
|
|
106
|
+
}
|
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
import type { Context } from "../context.js";
|
|
2
|
+
import { Resource } from "../resource.js";
|
|
3
|
+
import type { Secret } from "../secret.js";
|
|
4
|
+
import { SentryApi } from "./api.js";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Properties for creating or updating a ClientKey
|
|
8
|
+
*/
|
|
9
|
+
export interface ClientKeyProps {
|
|
10
|
+
/**
|
|
11
|
+
* The name of the key
|
|
12
|
+
*/
|
|
13
|
+
name?: string;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Rate limit configuration
|
|
17
|
+
*/
|
|
18
|
+
rateLimit?: {
|
|
19
|
+
/**
|
|
20
|
+
* Time window in seconds
|
|
21
|
+
*/
|
|
22
|
+
window: number;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Maximum number of events allowed in the window
|
|
26
|
+
*/
|
|
27
|
+
count: number;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* The use case for the key
|
|
32
|
+
*/
|
|
33
|
+
useCase?: "user" | "profiling" | "escalating_issues" | "tempest" | "demo";
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* The project slug that owns the key
|
|
37
|
+
*/
|
|
38
|
+
project: string;
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* The organization ID or slug that owns the key
|
|
42
|
+
*/
|
|
43
|
+
organization: string;
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Auth token to use (overrides environment variable)
|
|
47
|
+
*/
|
|
48
|
+
authToken?: Secret;
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Whether to adopt an existing key with the same name if it exists
|
|
52
|
+
* If true and a key with the same name exists, it will be adopted rather than creating a new one
|
|
53
|
+
*
|
|
54
|
+
* @default false
|
|
55
|
+
*/
|
|
56
|
+
adopt?: boolean;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Output returned after ClientKey creation/update
|
|
61
|
+
*/
|
|
62
|
+
export interface ClientKey
|
|
63
|
+
extends Resource<"sentry::ClientKey">,
|
|
64
|
+
ClientKeyProps {
|
|
65
|
+
/**
|
|
66
|
+
* The ID of the key
|
|
67
|
+
*/
|
|
68
|
+
id: string;
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* The label of the key
|
|
72
|
+
*/
|
|
73
|
+
label: string;
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* The public key
|
|
77
|
+
*/
|
|
78
|
+
public: string;
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* The secret key
|
|
82
|
+
*/
|
|
83
|
+
secret: string;
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* The project ID
|
|
87
|
+
*/
|
|
88
|
+
projectId: number;
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Whether the key is active
|
|
92
|
+
*/
|
|
93
|
+
isActive: boolean;
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* DSN configuration
|
|
97
|
+
*/
|
|
98
|
+
dsn: {
|
|
99
|
+
secret: string;
|
|
100
|
+
public: string;
|
|
101
|
+
csp: string;
|
|
102
|
+
security: string;
|
|
103
|
+
minidump: string;
|
|
104
|
+
nel: string;
|
|
105
|
+
unreal: string;
|
|
106
|
+
cdn: string;
|
|
107
|
+
crons: string;
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Browser SDK version
|
|
112
|
+
*/
|
|
113
|
+
browserSdkVersion: string;
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Browser SDK choices
|
|
117
|
+
*/
|
|
118
|
+
browserSdk: {
|
|
119
|
+
choices: Array<[string, string]>;
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Time at which the key was created
|
|
124
|
+
*/
|
|
125
|
+
dateCreated: string;
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Dynamic SDK loader options
|
|
129
|
+
*/
|
|
130
|
+
dynamicSdkLoaderOptions: {
|
|
131
|
+
hasReplay: boolean;
|
|
132
|
+
hasPerformance: boolean;
|
|
133
|
+
hasDebug: boolean;
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Create and manage Sentry client keys
|
|
139
|
+
*
|
|
140
|
+
* @example
|
|
141
|
+
* // Create a basic Sentry client key:
|
|
142
|
+
* const key = await ClientKey("my-key", {
|
|
143
|
+
* name: "My Key",
|
|
144
|
+
* project: "my-project",
|
|
145
|
+
* organization: "my-org"
|
|
146
|
+
* });
|
|
147
|
+
*
|
|
148
|
+
* @example
|
|
149
|
+
* // Create a client key with rate limiting:
|
|
150
|
+
* const key = await ClientKey("rate-limited-key", {
|
|
151
|
+
* name: "Rate Limited Key",
|
|
152
|
+
* project: "my-project",
|
|
153
|
+
* organization: "my-org",
|
|
154
|
+
* rateLimit: {
|
|
155
|
+
* window: 3600, // 1 hour
|
|
156
|
+
* count: 1000 // 1000 events per hour
|
|
157
|
+
* }
|
|
158
|
+
* });
|
|
159
|
+
*
|
|
160
|
+
* @example
|
|
161
|
+
* // Create a client key for a specific use case:
|
|
162
|
+
* const key = await ClientKey("profiling-key", {
|
|
163
|
+
* name: "Profiling Key",
|
|
164
|
+
* project: "my-project",
|
|
165
|
+
* organization: "my-org",
|
|
166
|
+
* useCase: "profiling"
|
|
167
|
+
* });
|
|
168
|
+
*
|
|
169
|
+
* @example
|
|
170
|
+
* // Create or adopt an existing key with the same name:
|
|
171
|
+
* const key = await ClientKey("existing-key", {
|
|
172
|
+
* name: "Existing Key",
|
|
173
|
+
* project: "my-project",
|
|
174
|
+
* organization: "my-org",
|
|
175
|
+
* adopt: true
|
|
176
|
+
* });
|
|
177
|
+
*/
|
|
178
|
+
export const ClientKey = Resource(
|
|
179
|
+
"sentry::ClientKey",
|
|
180
|
+
async function (
|
|
181
|
+
this: Context<ClientKey>,
|
|
182
|
+
id: string,
|
|
183
|
+
props: ClientKeyProps,
|
|
184
|
+
): Promise<ClientKey> {
|
|
185
|
+
const api = new SentryApi({ authToken: props.authToken });
|
|
186
|
+
|
|
187
|
+
if (this.phase === "delete") {
|
|
188
|
+
try {
|
|
189
|
+
if (this.output?.id) {
|
|
190
|
+
const response = await api.delete(
|
|
191
|
+
`/projects/${props.organization}/${props.project}/keys/${this.output.id}/`,
|
|
192
|
+
);
|
|
193
|
+
if (!response.ok && response.status !== 404) {
|
|
194
|
+
console.error("Error deleting client key:", response.statusText);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
} catch (error) {
|
|
198
|
+
console.error("Error deleting client key:", error);
|
|
199
|
+
}
|
|
200
|
+
return this.destroy();
|
|
201
|
+
} else {
|
|
202
|
+
try {
|
|
203
|
+
let response;
|
|
204
|
+
|
|
205
|
+
if (this.phase === "update" && this.output?.id) {
|
|
206
|
+
response = await api.put(
|
|
207
|
+
`/projects/${props.organization}/${props.project}/keys/${this.output.id}/`,
|
|
208
|
+
props,
|
|
209
|
+
);
|
|
210
|
+
} else {
|
|
211
|
+
try {
|
|
212
|
+
response = await api.post(
|
|
213
|
+
`/projects/${props.organization}/${props.project}/keys/`,
|
|
214
|
+
props,
|
|
215
|
+
);
|
|
216
|
+
} catch (error) {
|
|
217
|
+
// Check if this is a "key already exists" error and adopt is enabled
|
|
218
|
+
if (
|
|
219
|
+
props.adopt &&
|
|
220
|
+
error instanceof Error &&
|
|
221
|
+
error.message.includes("already exists") &&
|
|
222
|
+
props.name
|
|
223
|
+
) {
|
|
224
|
+
console.log(
|
|
225
|
+
`Client key '${props.name}' already exists, adopting it`,
|
|
226
|
+
);
|
|
227
|
+
// Find the existing key by name
|
|
228
|
+
const existingKey = await findClientKeyByName(
|
|
229
|
+
api,
|
|
230
|
+
props.organization,
|
|
231
|
+
props.project,
|
|
232
|
+
props.name,
|
|
233
|
+
);
|
|
234
|
+
if (!existingKey) {
|
|
235
|
+
throw new Error(
|
|
236
|
+
`Failed to find existing client key '${props.name}' for adoption`,
|
|
237
|
+
);
|
|
238
|
+
}
|
|
239
|
+
response = await api.get(
|
|
240
|
+
`/projects/${props.organization}/${props.project}/keys/${existingKey.id}/`,
|
|
241
|
+
);
|
|
242
|
+
} else {
|
|
243
|
+
throw error;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
if (!response.ok) {
|
|
249
|
+
throw new Error(`API error: ${response.statusText}`);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
const data = (await response.json()) as Omit<
|
|
253
|
+
ClientKey,
|
|
254
|
+
keyof ClientKeyProps
|
|
255
|
+
>;
|
|
256
|
+
return this({
|
|
257
|
+
...props,
|
|
258
|
+
id: data.id,
|
|
259
|
+
label: data.label,
|
|
260
|
+
public: data.public,
|
|
261
|
+
secret: data.secret,
|
|
262
|
+
projectId: data.projectId,
|
|
263
|
+
isActive: data.isActive,
|
|
264
|
+
dsn: data.dsn,
|
|
265
|
+
browserSdkVersion: data.browserSdkVersion,
|
|
266
|
+
browserSdk: data.browserSdk,
|
|
267
|
+
dateCreated: data.dateCreated,
|
|
268
|
+
dynamicSdkLoaderOptions: data.dynamicSdkLoaderOptions,
|
|
269
|
+
});
|
|
270
|
+
} catch (error) {
|
|
271
|
+
console.error("Error creating/updating client key:", error);
|
|
272
|
+
throw error;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
},
|
|
276
|
+
);
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Find a client key by name
|
|
280
|
+
*/
|
|
281
|
+
async function findClientKeyByName(
|
|
282
|
+
api: SentryApi,
|
|
283
|
+
organization: string,
|
|
284
|
+
project: string,
|
|
285
|
+
name: string,
|
|
286
|
+
): Promise<{ id: string } | null> {
|
|
287
|
+
const response = await api.get(`/projects/${organization}/${project}/keys/`);
|
|
288
|
+
if (!response.ok) {
|
|
289
|
+
throw new Error(`API error: ${response.statusText}`);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
const keys = (await response.json()) as Array<{ id: string; name: string }>;
|
|
293
|
+
const key = keys.find((k) => k.name === name);
|
|
294
|
+
return key ? { id: key.id } : null;
|
|
295
|
+
}
|