alchemy 0.43.5 → 0.44.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 (140) hide show
  1. package/bin/alchemy.mjs +430 -343
  2. package/bin/alchemy.ts +7 -21
  3. package/bin/commands/create.ts +79 -23
  4. package/bin/services/template-manager.ts +9 -4
  5. package/bin/services/vibe-rules.ts +47 -0
  6. package/bin/types.ts +25 -5
  7. package/lib/alchemy.d.ts.map +1 -1
  8. package/lib/alchemy.js +8 -11
  9. package/lib/alchemy.js.map +1 -1
  10. package/lib/apply.js +2 -0
  11. package/lib/apply.js.map +1 -1
  12. package/lib/build-date.d.ts +1 -1
  13. package/lib/build-date.js +1 -1
  14. package/lib/cloudflare/assets.d.ts +1 -1
  15. package/lib/cloudflare/assets.d.ts.map +1 -1
  16. package/lib/cloudflare/bucket.d.ts.map +1 -1
  17. package/lib/cloudflare/bucket.js +9 -6
  18. package/lib/cloudflare/bucket.js.map +1 -1
  19. package/lib/cloudflare/bundle/bundle-worker-dev.d.ts.map +1 -1
  20. package/lib/cloudflare/bundle/bundle-worker-dev.js +4 -1
  21. package/lib/cloudflare/bundle/bundle-worker-dev.js.map +1 -1
  22. package/lib/cloudflare/bundle/bundle-worker.d.ts.map +1 -1
  23. package/lib/cloudflare/bundle/bundle-worker.js +4 -1
  24. package/lib/cloudflare/bundle/bundle-worker.js.map +1 -1
  25. package/lib/cloudflare/bundle/external.d.ts +1 -0
  26. package/lib/cloudflare/bundle/external.d.ts.map +1 -1
  27. package/lib/cloudflare/bundle/external.js +1 -1
  28. package/lib/cloudflare/bundle/external.js.map +1 -1
  29. package/lib/cloudflare/bundle/nodejs-import-warning-plugin.d.ts +10 -0
  30. package/lib/cloudflare/bundle/nodejs-import-warning-plugin.d.ts.map +1 -0
  31. package/lib/cloudflare/bundle/nodejs-import-warning-plugin.js +67 -0
  32. package/lib/cloudflare/bundle/nodejs-import-warning-plugin.js.map +1 -0
  33. package/lib/cloudflare/container.d.ts +27 -2
  34. package/lib/cloudflare/container.d.ts.map +1 -1
  35. package/lib/cloudflare/container.js +82 -24
  36. package/lib/cloudflare/container.js.map +1 -1
  37. package/lib/cloudflare/redirect-rule.js +1 -1
  38. package/lib/cloudflare/redirect-rule.js.map +1 -1
  39. package/lib/cloudflare/website.d.ts +17 -0
  40. package/lib/cloudflare/website.d.ts.map +1 -1
  41. package/lib/cloudflare/website.js +1 -0
  42. package/lib/cloudflare/website.js.map +1 -1
  43. package/lib/cloudflare/worker/miniflare.d.ts +4 -1
  44. package/lib/cloudflare/worker/miniflare.d.ts.map +1 -1
  45. package/lib/cloudflare/worker/miniflare.js +22 -2
  46. package/lib/cloudflare/worker/miniflare.js.map +1 -1
  47. package/lib/cloudflare/worker-metadata.d.ts.map +1 -1
  48. package/lib/cloudflare/worker-metadata.js +12 -0
  49. package/lib/cloudflare/worker-metadata.js.map +1 -1
  50. package/lib/cloudflare/worker.d.ts +1 -1
  51. package/lib/cloudflare/wrangler.json.d.ts +24 -0
  52. package/lib/cloudflare/wrangler.json.d.ts.map +1 -1
  53. package/lib/cloudflare/wrangler.json.js +20 -2
  54. package/lib/cloudflare/wrangler.json.js.map +1 -1
  55. package/lib/context.d.ts +6 -1
  56. package/lib/context.d.ts.map +1 -1
  57. package/lib/context.js +2 -1
  58. package/lib/context.js.map +1 -1
  59. package/lib/destroy.d.ts.map +1 -1
  60. package/lib/destroy.js +1 -0
  61. package/lib/destroy.js.map +1 -1
  62. package/lib/docker/api.d.ts +11 -0
  63. package/lib/docker/api.d.ts.map +1 -1
  64. package/lib/docker/api.js +41 -1
  65. package/lib/docker/api.js.map +1 -1
  66. package/lib/docker/image.d.ts +1 -1
  67. package/lib/docker/image.d.ts.map +1 -1
  68. package/lib/docker/image.js +11 -5
  69. package/lib/docker/image.js.map +1 -1
  70. package/lib/esbuild/bundle.d.ts +2 -2
  71. package/lib/index.d.ts.map +1 -1
  72. package/lib/index.js.map +1 -1
  73. package/lib/llms.cloudflare.txt +379 -0
  74. package/lib/llms.d.ts +10 -0
  75. package/lib/llms.d.ts.map +1 -0
  76. package/lib/llms.js +12 -0
  77. package/lib/llms.js.map +1 -0
  78. package/lib/planetscale/index.d.ts +3 -2
  79. package/lib/planetscale/index.d.ts.map +1 -1
  80. package/lib/planetscale/index.js +3 -2
  81. package/lib/planetscale/index.js.map +1 -1
  82. package/lib/planetscale/password.d.ts +233 -0
  83. package/lib/planetscale/password.d.ts.map +1 -0
  84. package/lib/planetscale/password.js +238 -0
  85. package/lib/planetscale/password.js.map +1 -0
  86. package/lib/scope.d.ts +6 -5
  87. package/lib/scope.d.ts.map +1 -1
  88. package/lib/scope.js +18 -7
  89. package/lib/scope.js.map +1 -1
  90. package/lib/test/bun.d.ts.map +1 -1
  91. package/lib/test/bun.js +1 -0
  92. package/lib/test/bun.js.map +1 -1
  93. package/lib/test/vitest.d.ts.map +1 -1
  94. package/lib/test/vitest.js +1 -0
  95. package/lib/test/vitest.js.map +1 -1
  96. package/lib/util/nanoid.d.ts +3 -0
  97. package/lib/util/nanoid.d.ts.map +1 -0
  98. package/lib/util/nanoid.js +13 -0
  99. package/lib/util/nanoid.js.map +1 -0
  100. package/lib/util/safe-fetch.d.ts.map +1 -1
  101. package/lib/util/safe-fetch.js +0 -1
  102. package/lib/util/safe-fetch.js.map +1 -1
  103. package/lib/util/telemetry/client.d.ts.map +1 -1
  104. package/lib/util/telemetry/client.js +13 -3
  105. package/lib/util/telemetry/client.js.map +1 -1
  106. package/lib/util/telemetry/constants.d.ts +2 -1
  107. package/lib/util/telemetry/constants.d.ts.map +1 -1
  108. package/lib/util/telemetry/constants.js +3 -2
  109. package/lib/util/telemetry/constants.js.map +1 -1
  110. package/package.json +10 -2
  111. package/src/alchemy.ts +8 -13
  112. package/src/apply.ts +2 -0
  113. package/src/build-date.ts +1 -1
  114. package/src/cloudflare/bucket.ts +22 -14
  115. package/src/cloudflare/bundle/bundle-worker-dev.ts +4 -1
  116. package/src/cloudflare/bundle/bundle-worker.ts +4 -1
  117. package/src/cloudflare/bundle/external.ts +1 -1
  118. package/src/cloudflare/bundle/nodejs-import-warning-plugin.ts +94 -0
  119. package/src/cloudflare/container.ts +113 -28
  120. package/src/cloudflare/redirect-rule.ts +1 -1
  121. package/src/cloudflare/website.ts +21 -1
  122. package/src/cloudflare/worker/miniflare.ts +33 -8
  123. package/src/cloudflare/worker-metadata.ts +15 -0
  124. package/src/cloudflare/wrangler.json.ts +61 -5
  125. package/src/context.ts +7 -0
  126. package/src/destroy.ts +1 -0
  127. package/src/docker/api.ts +57 -1
  128. package/src/docker/image.ts +15 -6
  129. package/src/index.ts +1 -0
  130. package/src/llms.cloudflare.txt +379 -0
  131. package/src/llms.ts +25 -0
  132. package/src/planetscale/index.ts +3 -2
  133. package/src/planetscale/password.ts +366 -0
  134. package/src/scope.ts +25 -11
  135. package/src/test/bun.ts +1 -0
  136. package/src/test/vitest.ts +1 -0
  137. package/src/util/nanoid.ts +13 -0
  138. package/src/util/safe-fetch.ts +0 -1
  139. package/src/util/telemetry/client.ts +17 -3
  140. package/src/util/telemetry/constants.ts +5 -3
@@ -0,0 +1,366 @@
1
+ import { alchemy } from "../alchemy.ts";
2
+ import type { Context } from "../context.ts";
3
+ import { Resource } from "../resource.ts";
4
+ import type { Secret } from "../secret.ts";
5
+ import { logger } from "../util/logger.ts";
6
+ import { lowercaseId } from "../util/nanoid.ts";
7
+ import { PlanetScaleApi } from "./api.ts";
8
+ import type { Branch } from "./branch.ts";
9
+ import type { Database } from "./database.ts";
10
+
11
+ /**
12
+ * Properties for creating or updating a PlanetScale Branch
13
+ */
14
+ export interface PasswordProps {
15
+ /**
16
+ * The name of the password
17
+ */
18
+ name: string;
19
+
20
+ /**
21
+ * The organization ID where the password will be created
22
+ * Required when using string database name, optional when using Database resource
23
+ */
24
+ organizationId?: string;
25
+
26
+ /**
27
+ * The database where the password will be created
28
+ * Can be either a database name (string) or Database resource
29
+ */
30
+ database: string | Database;
31
+
32
+ /**
33
+ * The branch where the password will be created
34
+ * Can be either a branch name (string) or Branch resource
35
+ * Defaults to "main" if not provided
36
+ */
37
+ branch?: string | Branch;
38
+
39
+ /**
40
+ * PlanetScale API token (overrides environment variable)
41
+ */
42
+ apiKey?: Secret;
43
+
44
+ /**
45
+ * The password
46
+ */
47
+ role: "reader" | "writer" | "admin" | "readwriter";
48
+
49
+ /**
50
+ * Whether the password is for a read replica
51
+ */
52
+ replica?: boolean;
53
+
54
+ /**
55
+ * The TTL of the password in seconds
56
+ */
57
+ ttl?: number;
58
+
59
+ /**
60
+ * The CIDRs of the password
61
+ */
62
+ cidrs?: string[];
63
+ }
64
+
65
+ /**
66
+ * Represents a PlanetScale Branch
67
+ */
68
+ export interface Password
69
+ extends Resource<"planetscale::Password">,
70
+ PasswordProps {
71
+ /**
72
+ * The unique identifier for the password
73
+ */
74
+ id: string;
75
+
76
+ /**
77
+ * The timestamp when the password expires (ISO 8601 format)
78
+ */
79
+ expiresAt: string;
80
+
81
+ /**
82
+ * The host URL for database connection
83
+ */
84
+ host: string;
85
+
86
+ /**
87
+ * The username for database authentication
88
+ */
89
+ username: string;
90
+
91
+ /**
92
+ * The encrypted password for database authentication
93
+ */
94
+ password: Secret<string>;
95
+
96
+ /**
97
+ * The name slug for the password
98
+ */
99
+ nameSlug: string;
100
+ }
101
+
102
+ /**
103
+ * Create and manage database passwords for PlanetScale branches. Database passwords provide secure access to your database with specific roles and permissions.
104
+ *
105
+ * @example
106
+ * ## Basic Reader Password
107
+ *
108
+ * Create a read-only password for a database branch:
109
+ *
110
+ * ```ts
111
+ * import { Password } from "alchemy/planetscale";
112
+ *
113
+ * const readerPassword = await Password("app-reader", {
114
+ * name: "app-reader",
115
+ * organizationId: "my-org",
116
+ * database: "my-app-db",
117
+ * branch: "main",
118
+ * role: "reader"
119
+ * });
120
+ *
121
+ * // Access connection details
122
+ * console.log(`Host: ${readerPassword.password.host}`);
123
+ * console.log(`Username: ${readerPassword.password.username}`);
124
+ * console.log(`Password: ${readerPassword.password.password.unencrypted}`);
125
+ * ```
126
+ *
127
+ * @example
128
+ * ## Writer Password with TTL
129
+ *
130
+ * Create a writer password that expires after 24 hours:
131
+ *
132
+ * ```ts
133
+ * import { Password } from "alchemy/planetscale";
134
+ *
135
+ * const writerPassword = await Password("app-writer", {
136
+ * name: "app-writer",
137
+ * organizationId: "my-org",
138
+ * database: "my-app-db",
139
+ * branch: "development",
140
+ * role: "writer",
141
+ * ttl: 86400 // 24 hours in seconds
142
+ * });
143
+ *
144
+ * // Password will expire at the specified time
145
+ * console.log(`Expires at: ${writerPassword.expiresAt}`);
146
+ * ```
147
+ *
148
+ * @example
149
+ * ## Admin Password with IP Restrictions
150
+ *
151
+ * Create an admin password that only allows connections from specific IP addresses:
152
+ *
153
+ * ```ts
154
+ * import { Password } from "alchemy/planetscale";
155
+ *
156
+ * const adminPassword = await Password("admin-access", {
157
+ * name: "admin-access",
158
+ * organizationId: "my-org",
159
+ * database: "my-app-db",
160
+ * branch: "main",
161
+ * role: "admin",
162
+ * cidrs: ["203.0.113.0/24", "198.51.100.0/24"],
163
+ * ttl: 3600 // 1 hour
164
+ * });
165
+ * ```
166
+ *
167
+ * @example
168
+ * ## Database Password with Custom API Key
169
+ *
170
+ * Create a password using a specific API key instead of the default environment variable:
171
+ *
172
+ * ```ts
173
+ * import { Password } from "alchemy/planetscale";
174
+ *
175
+ * const password = await Password("custom-auth", {
176
+ * name: "custom-auth",
177
+ * organizationId: "my-org",
178
+ * database: "my-app-db",
179
+ * branch: "main",
180
+ * role: "readwriter",
181
+ * apiKey: alchemy.secret(process.env.CUSTOM_PLANETSCALE_TOKEN)
182
+ * });
183
+ * ```
184
+ *
185
+ * @example
186
+ * ## Read Replica Password
187
+ *
188
+ * Create a password for accessing a read replica:
189
+ *
190
+ * ```ts
191
+ * import { Password } from "alchemy/planetscale";
192
+ *
193
+ * const replicaPassword = await Password("replica-reader", {
194
+ * name: "replica-reader",
195
+ * organizationId: "my-org",
196
+ * database: "my-app-db",
197
+ * branch: "main",
198
+ * role: "reader",
199
+ * replica: true
200
+ * });
201
+ *
202
+ * @example
203
+ * ## Using Database Resource Instance
204
+ *
205
+ * Create a password using a Database resource instead of string:
206
+ *
207
+ * ```ts
208
+ * import { Database, Password } from "alchemy/planetscale";
209
+ *
210
+ * const database = await Database("my-db", {
211
+ * name: "my-app-db",
212
+ * organizationId: "my-org",
213
+ * clusterSize: "PS_10"
214
+ * });
215
+ *
216
+ * const password = await Password("db-reader", {
217
+ * name: "db-reader",
218
+ * database: database, // Using Database resource
219
+ * role: "reader"
220
+ * });
221
+ * ```
222
+ *
223
+ * @example
224
+ * ## Using Both Database and Branch Resources
225
+ *
226
+ * Create a password using both Database and Branch resources:
227
+ *
228
+ * ```ts
229
+ * import { Database, Branch, Password } from "alchemy/planetscale";
230
+ *
231
+ * const database = await Database("my-db", {
232
+ * name: "my-app-db",
233
+ * organizationId: "my-org",
234
+ * clusterSize: "PS_10"
235
+ * });
236
+ *
237
+ * const branch = await Branch("feature-branch", {
238
+ * name: "feature-branch",
239
+ * organizationId: "my-org",
240
+ * databaseName: "my-app-db",
241
+ * parentBranch: "main",
242
+ * isProduction: false
243
+ * });
244
+ *
245
+ * const password = await Password("feature-writer", {
246
+ * name: "feature-writer",
247
+ * database: database, // Using Database resource
248
+ * branch: branch, // Using Branch resource
249
+ * role: "writer"
250
+ * });
251
+ * ```
252
+ * ```
253
+ */
254
+ export const Password = Resource(
255
+ "planetscale::Password",
256
+ async function (
257
+ this: Context<Password>,
258
+ _id: string,
259
+ props: PasswordProps,
260
+ ): Promise<Password> {
261
+ const apiKey =
262
+ props.apiKey?.unencrypted || process.env.PLANETSCALE_API_TOKEN;
263
+ if (!apiKey) {
264
+ throw new Error("PLANETSCALE_API_TOKEN environment variable is required");
265
+ }
266
+ const nameSlug = this.isReplacement
267
+ ? lowercaseId()
268
+ : (this.output?.nameSlug ?? lowercaseId());
269
+ const name = `${props.name.toLowerCase()}-${nameSlug}`;
270
+
271
+ const api = new PlanetScaleApi({ apiKey });
272
+ const branchName =
273
+ props.branch == null
274
+ ? "main"
275
+ : typeof props.branch === "string"
276
+ ? props.branch
277
+ : props.branch.name;
278
+ const databaseName =
279
+ typeof props.database === "string" ? props.database : props.database.name;
280
+
281
+ if (this.phase === "delete") {
282
+ try {
283
+ if (this.output?.name) {
284
+ const response = await api.delete(
285
+ `/organizations/${props.organizationId}/databases/${databaseName}/branches/${branchName}/passwords/${this.output.id}`,
286
+ );
287
+
288
+ if (!response.ok && response.status !== 404) {
289
+ throw new Error(
290
+ `Failed to delete branch: ${response.statusText} ${await response.text()}`,
291
+ );
292
+ }
293
+ }
294
+ } catch (error) {
295
+ logger.error("Error deleting password:", error);
296
+ throw error;
297
+ }
298
+ return this.destroy();
299
+ }
300
+ if (this.phase === "update") {
301
+ if (
302
+ this.output?.name === name &&
303
+ ((this.output?.cidrs === undefined && props.cidrs === undefined) ||
304
+ (Array.isArray(this.output?.cidrs) &&
305
+ Array.isArray(props.cidrs) &&
306
+ this.output.cidrs.length === props.cidrs.length &&
307
+ this.output.cidrs.every((cidr, i) => cidr === props.cidrs![i])))
308
+ ) {
309
+ return this.replace();
310
+ }
311
+ const updateResponse = await api.patch(
312
+ `/organizations/${props.organizationId}/databases/${databaseName}/branches/${branchName}/passwords/${this.output.id}`,
313
+ {
314
+ name,
315
+ cidrs: props.cidrs,
316
+ },
317
+ );
318
+
319
+ if (!updateResponse.ok) {
320
+ throw new Error(
321
+ `Failed to update password: ${updateResponse.statusText} ${await updateResponse.text()}`,
322
+ );
323
+ }
324
+
325
+ return this({
326
+ ...this.output,
327
+ ...props,
328
+ });
329
+ }
330
+
331
+ try {
332
+ const createResponse = await api.post(
333
+ `/organizations/${props.organizationId}/databases/${databaseName}/branches/${branchName}/passwords`,
334
+ {
335
+ name,
336
+ role: props.role,
337
+ replica: props.replica,
338
+ ttl: props.ttl,
339
+ cidrs: props.cidrs,
340
+ },
341
+ );
342
+
343
+ if (!createResponse.ok) {
344
+ throw new Error(
345
+ `Failed to create password: ${createResponse.statusText} ${await createResponse.text()}`,
346
+ );
347
+ }
348
+
349
+ const data = await createResponse.json<any>();
350
+
351
+ return this({
352
+ id: data.id,
353
+ expiresAt: data.expires_at,
354
+ host: data.access_host_url,
355
+ username: data.username,
356
+ password: alchemy.secret(data.plain_text),
357
+ nameSlug,
358
+ ...props,
359
+ name: `${props.name}-${nameSlug}`,
360
+ });
361
+ } catch (error) {
362
+ logger.error("Error managing password:", error);
363
+ throw error;
364
+ }
365
+ },
366
+ );
package/src/scope.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import { AsyncLocalStorage } from "node:async_hooks";
2
2
  import util from "node:util";
3
3
  import type { Phase } from "./alchemy.ts";
4
+ import { DOStateStore } from "./cloudflare/do-state-store/index.ts";
4
5
  import { destroy, destroyAll } from "./destroy.ts";
5
6
  import { FileSystemStateStore } from "./fs/file-system-state-store.ts";
6
7
  import {
@@ -29,10 +30,9 @@ export class RootScopeStateAttemptError extends Error {
29
30
  }
30
31
 
31
32
  export interface ScopeOptions {
32
- appName?: string;
33
33
  stage?: string;
34
- parent?: Scope;
35
- scopeName?: string;
34
+ parent: Scope | undefined;
35
+ scopeName: string;
36
36
  password?: string;
37
37
  stateStore?: StateStoreType;
38
38
  quiet?: boolean;
@@ -48,7 +48,7 @@ export type PendingDeletions = Array<{
48
48
  }>;
49
49
 
50
50
  // TODO: support browser
51
- const DEFAULT_STAGE =
51
+ export const DEFAULT_STAGE =
52
52
  process.env.ALCHEMY_STAGE ??
53
53
  process.env.USER ??
54
54
  process.env.USERNAME ??
@@ -97,9 +97,9 @@ export class Scope {
97
97
 
98
98
  public readonly resources = new Map<ResourceID, PendingResource>();
99
99
  public readonly children: Map<ResourceID, Scope> = new Map();
100
- public readonly appName: string | undefined;
101
100
  public readonly stage: string;
102
- public readonly scopeName: string | null;
101
+ public readonly name: string;
102
+ public readonly scopeName: string;
103
103
  public readonly parent: Scope | undefined;
104
104
  public readonly password: string | undefined;
105
105
  public readonly state: StateStore;
@@ -117,15 +117,26 @@ export class Scope {
117
117
 
118
118
  private deferred: (() => Promise<any>)[] = [];
119
119
 
120
+ public get appName(): string {
121
+ if (this.parent) {
122
+ return this.parent.appName;
123
+ }
124
+ return this.scopeName;
125
+ }
126
+
120
127
  constructor(options: ScopeOptions) {
121
- this.appName = options.appName ?? options.parent?.appName;
122
- this.scopeName = options.scopeName ?? null;
123
- if (this.scopeName?.includes(":")) {
128
+ this.scopeName = options.scopeName;
129
+ this.name = this.scopeName;
130
+ this.parent = options.parent ?? Scope.getScope();
131
+
132
+ const isChild = this.parent !== undefined;
133
+ if (this.scopeName?.includes(":") && isChild) {
134
+ // TODO(sam): relax this constraint once we move to SQLite3 store
124
135
  throw new Error(
125
136
  `Scope name "${this.scopeName}" cannot contain double colons`,
126
137
  );
127
138
  }
128
- this.parent = options.parent ?? Scope.getScope();
139
+
129
140
  this.stage = options?.stage ?? this.parent?.stage ?? DEFAULT_STAGE;
130
141
  this.parent?.children.set(this.scopeName!, this);
131
142
  this.quiet = options.quiet ?? this.parent?.quiet ?? false;
@@ -161,7 +172,10 @@ export class Scope {
161
172
  this.stateStore =
162
173
  options.stateStore ??
163
174
  this.parent?.stateStore ??
164
- ((scope) => new FileSystemStateStore(scope));
175
+ ((scope) =>
176
+ process.env.ALCHEMY_STATE_STORE === "cloudflare"
177
+ ? new DOStateStore(scope)
178
+ : new FileSystemStateStore(scope));
165
179
  this.state = this.stateStore(this);
166
180
  if (!options.telemetryClient && !this.parent?.telemetryClient) {
167
181
  throw new Error("Telemetry client is required");
package/src/test/bun.ts CHANGED
@@ -133,6 +133,7 @@ export function test(meta: ImportMeta, defaultOptions?: TestOptions): test {
133
133
 
134
134
  // Create local test scope based on filename
135
135
  const scope = new Scope({
136
+ parent: undefined,
136
137
  scopeName: `${defaultOptions.prefix ? `${defaultOptions.prefix}-` : ""}${path.basename(meta.filename)}`,
137
138
  // parent: globalTestScope,
138
139
  stateStore: defaultOptions?.stateStore,
@@ -139,6 +139,7 @@ export function test(
139
139
  };
140
140
 
141
141
  const scope = new Scope({
142
+ parent: undefined,
142
143
  scopeName: `${defaultOptions.prefix ? `${defaultOptions.prefix}-` : ""}${path.basename(meta.filename)}`,
143
144
  stateStore: defaultOptions?.stateStore,
144
145
  phase: "up",
@@ -0,0 +1,13 @@
1
+ export const lowercaseId = idGenerator("abcdefghijklmnopqrstuvwxyz", 10);
2
+
3
+ export function idGenerator(alphabet: string, defaultLength: number) {
4
+ return function generateId(length: number = defaultLength): string {
5
+ let id = "";
6
+ const alphabetLength = alphabet.length;
7
+ for (let i = 0; i < length; i++) {
8
+ const randomIndex = Math.floor(Math.random() * alphabetLength);
9
+ id += alphabet[randomIndex];
10
+ }
11
+ return id;
12
+ };
13
+ }
@@ -15,7 +15,6 @@ export async function safeFetch(
15
15
  dispatcher: await dispatcher(),
16
16
  } as RequestInit);
17
17
  } catch (err: any) {
18
- console.log(url, err);
19
18
  latestErr = err;
20
19
  const shouldRetry =
21
20
  err?.code === "UND_ERR_SOCKET" ||
@@ -1,6 +1,10 @@
1
1
  import os from "node:os";
2
2
  import type { Phase } from "../../alchemy.ts";
3
- import { INGEST_URL, TELEMETRY_DISABLED } from "./constants.ts";
3
+ import {
4
+ POSTHOG_CLIENT_API_HOST,
5
+ POSTHOG_PROJECT_ID,
6
+ TELEMETRY_DISABLED,
7
+ } from "./constants.ts";
4
8
  import { context } from "./context.ts";
5
9
  import type { Telemetry } from "./types.ts";
6
10
 
@@ -92,12 +96,22 @@ export class TelemetryClient implements ITelemetryClient {
92
96
  if (events.length === 0) {
93
97
  return;
94
98
  }
95
- const response = await fetch(INGEST_URL, {
99
+ const response = await fetch(`${POSTHOG_CLIENT_API_HOST}/batch`, {
96
100
  method: "POST",
97
101
  headers: {
98
102
  "Content-Type": "application/json",
99
103
  },
100
- body: JSON.stringify(events),
104
+ body: JSON.stringify({
105
+ api_key: POSTHOG_PROJECT_ID,
106
+ historical_migration: false,
107
+ batch: events.map((e) => ({
108
+ event: e.event,
109
+ properties: {
110
+ distinct_id: this.context?.sessionId,
111
+ },
112
+ timestamp: new Date(e.timestamp).toISOString(),
113
+ })),
114
+ }),
101
115
  });
102
116
  if (!response.ok) {
103
117
  throw new Error(
@@ -8,6 +8,8 @@ export const TELEMETRY_DISABLED =
8
8
  !!process.env.ALCHEMY_TELEMETRY_DISABLED || !!process.env.DO_NOT_TRACK;
9
9
 
10
10
  // TODO(sam): replace with permanent URL
11
- export const INGEST_URL =
12
- process.env.ALCHEMY_TELEMETRY_URL ??
13
- "https://5fbb5d5ca66f44b685468684cfceb325.pipelines.cloudflare.com";
11
+ export const POSTHOG_CLIENT_API_HOST =
12
+ process.env.POSTHOG_CLIENT_API_HOST ?? "https://ph.alchemy.run";
13
+ export const POSTHOG_PROJECT_ID =
14
+ process.env.POSTHOG_PROJECT_ID ??
15
+ "phc_A51Mi7Q63TvnNrvRMgvBxE1il0DAL66rVg4LdWPRsfK";