alchemy 0.16.4 → 0.16.6

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 (42) hide show
  1. package/lib/cloudflare/bindings.d.ts +4 -1
  2. package/lib/cloudflare/bindings.d.ts.map +1 -1
  3. package/lib/cloudflare/bindings.js.map +1 -1
  4. package/lib/cloudflare/bound.d.ts +3 -2
  5. package/lib/cloudflare/bound.d.ts.map +1 -1
  6. package/lib/cloudflare/kv-namespace.js +1 -1
  7. package/lib/cloudflare/kv-namespace.js.map +1 -1
  8. package/lib/cloudflare/worker.js +1 -1
  9. package/lib/cloudflare/worker.js.map +1 -1
  10. package/lib/cloudflare/wrangler.json.js +1 -1
  11. package/lib/cloudflare/wrangler.json.js.map +1 -1
  12. package/lib/sentry/api.d.ts +50 -0
  13. package/lib/sentry/api.d.ts.map +1 -0
  14. package/lib/sentry/api.js +76 -0
  15. package/lib/sentry/api.js.map +1 -0
  16. package/lib/sentry/client-key.d.ts +156 -0
  17. package/lib/sentry/client-key.d.ts.map +1 -0
  18. package/lib/sentry/client-key.js +126 -0
  19. package/lib/sentry/client-key.js.map +1 -0
  20. package/lib/sentry/index.d.ts +5 -0
  21. package/lib/sentry/index.d.ts.map +1 -0
  22. package/lib/sentry/index.js +5 -0
  23. package/lib/sentry/index.js.map +1 -0
  24. package/lib/sentry/project.d.ts +235 -0
  25. package/lib/sentry/project.d.ts.map +1 -0
  26. package/lib/sentry/project.js +146 -0
  27. package/lib/sentry/project.js.map +1 -0
  28. package/lib/sentry/team.d.ts +109 -0
  29. package/lib/sentry/team.d.ts.map +1 -0
  30. package/lib/sentry/team.js +109 -0
  31. package/lib/sentry/team.js.map +1 -0
  32. package/package.json +2 -1
  33. package/src/cloudflare/bindings.ts +4 -0
  34. package/src/cloudflare/bound.ts +1 -2
  35. package/src/cloudflare/kv-namespace.ts +1 -1
  36. package/src/cloudflare/worker.ts +2 -1
  37. package/src/cloudflare/wrangler.json.ts +1 -1
  38. package/src/sentry/api.ts +106 -0
  39. package/src/sentry/client-key.ts +295 -0
  40. package/src/sentry/index.ts +4 -0
  41. package/src/sentry/project.ts +420 -0
  42. 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.4",
3
+ "version": "0.16.6",
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",
@@ -41,6 +41,10 @@ export type Binding =
41
41
  | DurableObjectNamespace
42
42
  | Hyperdrive
43
43
  | KVNamespace
44
+ | {
45
+ type: "kv_namespace";
46
+ id: string;
47
+ }
44
48
  | Pipeline
45
49
  | Queue
46
50
  | R2Bucket
@@ -9,7 +9,6 @@ import type { R2Bucket as _R2Bucket } from "./bucket.js";
9
9
  import type { D1Database as _D1Database } from "./d1-database.js";
10
10
  import type { DurableObjectNamespace as _DurableObjectNamespace } from "./durable-object-namespace.js";
11
11
  import type { Hyperdrive as _Hyperdrive } from "./hyperdrive.js";
12
- import type { KVNamespace as _KVNamespace } from "./kv-namespace.js";
13
12
  import type { Pipeline as _Pipeline } from "./pipeline.js";
14
13
  import type { Queue as _Queue } from "./queue.js";
15
14
  import type { VectorizeIndex as _VectorizeIndex } from "./vectorize-index.js";
@@ -18,7 +17,7 @@ import type { Workflow as _Workflow } from "./workflow.js";
18
17
 
19
18
  export type Bound<T extends Binding> = T extends _DurableObjectNamespace
20
19
  ? DurableObjectNamespace
21
- : T extends _KVNamespace
20
+ : T extends { type: "kv_namespace" }
22
21
  ? KVNamespace
23
22
  : T extends _Worker
24
23
  ? Worker
@@ -218,7 +218,7 @@ export const KVNamespace = Resource(
218
218
 
219
219
  return this({
220
220
  type: "kv_namespace",
221
- namespaceId: namespaceId,
221
+ namespaceId,
222
222
  title: props.title,
223
223
  values: props.values,
224
224
  createdAt: createdAt,
@@ -808,7 +808,8 @@ async function prepareWorkerMetadata<B extends Bindings>(
808
808
  meta.bindings.push({
809
809
  type: "kv_namespace",
810
810
  name: bindingName,
811
- namespace_id: binding.namespaceId,
811
+ namespace_id:
812
+ "namespaceId" in binding ? binding.namespaceId : binding.id,
812
813
  });
813
814
  } else if (binding.type === "service") {
814
815
  meta.bindings.push({
@@ -381,7 +381,7 @@ function processBindings(
381
381
  // KV Namespace binding
382
382
  kvNamespaces.push({
383
383
  binding: bindingName,
384
- id: binding.namespaceId,
384
+ id: "namespaceId" in binding ? binding.namespaceId : binding.id,
385
385
  });
386
386
  } else if (
387
387
  typeof binding === "object" &&
@@ -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
+ }
@@ -0,0 +1,4 @@
1
+ export * from "./api.js";
2
+ export * from "./client-key.js";
3
+ export * from "./project.js";
4
+ export * from "./team.js";