alchemy 0.80.1 → 0.81.1

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 (53) hide show
  1. package/bin/alchemy.js +1660 -1382
  2. package/bin/services/execute-alchemy.ts +18 -6
  3. package/lib/alchemy.js +1 -1
  4. package/lib/alchemy.js.map +1 -1
  5. package/lib/cloudflare/compatibility-date.gen.d.ts +1 -1
  6. package/lib/cloudflare/compatibility-date.gen.js +1 -1
  7. package/lib/cloudflare/hyperdrive.d.ts +23 -5
  8. package/lib/cloudflare/hyperdrive.d.ts.map +1 -1
  9. package/lib/cloudflare/hyperdrive.js.map +1 -1
  10. package/lib/cloudflare/index.d.ts +3 -0
  11. package/lib/cloudflare/index.d.ts.map +1 -1
  12. package/lib/cloudflare/index.js +3 -0
  13. package/lib/cloudflare/index.js.map +1 -1
  14. package/lib/cloudflare/miniflare/miniflare-worker-proxy.d.ts.map +1 -1
  15. package/lib/cloudflare/miniflare/miniflare-worker-proxy.js +7 -0
  16. package/lib/cloudflare/miniflare/miniflare-worker-proxy.js.map +1 -1
  17. package/lib/cloudflare/queue.d.ts +0 -3
  18. package/lib/cloudflare/queue.d.ts.map +1 -1
  19. package/lib/cloudflare/queue.js +53 -31
  20. package/lib/cloudflare/queue.js.map +1 -1
  21. package/lib/cloudflare/redirect-rule.d.ts +12 -2
  22. package/lib/cloudflare/redirect-rule.d.ts.map +1 -1
  23. package/lib/cloudflare/redirect-rule.js +73 -98
  24. package/lib/cloudflare/redirect-rule.js.map +1 -1
  25. package/lib/cloudflare/tunnel-route.d.ts +172 -0
  26. package/lib/cloudflare/tunnel-route.d.ts.map +1 -0
  27. package/lib/cloudflare/tunnel-route.js +251 -0
  28. package/lib/cloudflare/tunnel-route.js.map +1 -0
  29. package/lib/cloudflare/warp-default-profile.d.ts +133 -0
  30. package/lib/cloudflare/warp-default-profile.d.ts.map +1 -0
  31. package/lib/cloudflare/warp-default-profile.js +138 -0
  32. package/lib/cloudflare/warp-default-profile.js.map +1 -0
  33. package/lib/cloudflare/warp-device-profile.d.ts +272 -0
  34. package/lib/cloudflare/warp-device-profile.d.ts.map +1 -0
  35. package/lib/cloudflare/warp-device-profile.js +291 -0
  36. package/lib/cloudflare/warp-device-profile.js.map +1 -0
  37. package/lib/scope.d.ts +1 -1
  38. package/lib/scope.d.ts.map +1 -1
  39. package/lib/scope.js +1 -1
  40. package/lib/scope.js.map +1 -1
  41. package/package.json +1 -1
  42. package/src/alchemy.ts +1 -1
  43. package/src/cloudflare/compatibility-date.gen.ts +1 -1
  44. package/src/cloudflare/hyperdrive.ts +27 -5
  45. package/src/cloudflare/index.ts +3 -0
  46. package/src/cloudflare/miniflare/miniflare-worker-proxy.ts +11 -0
  47. package/src/cloudflare/queue.ts +97 -56
  48. package/src/cloudflare/redirect-rule.ts +129 -150
  49. package/src/cloudflare/tunnel-route.ts +495 -0
  50. package/src/cloudflare/warp-default-profile.ts +320 -0
  51. package/src/cloudflare/warp-device-profile.ts +616 -0
  52. package/src/scope.ts +2 -2
  53. package/workers/tunnel-proxy.js +1 -1
@@ -0,0 +1,495 @@
1
+ import type { Context } from "../context.ts";
2
+ import { Resource, ResourceKind } from "../resource.ts";
3
+ import { logger } from "../util/logger.ts";
4
+ import { handleApiError } from "./api-error.ts";
5
+ import type {
6
+ CloudflareApiListResponse,
7
+ CloudflareApiResponse,
8
+ } from "./api-response.ts";
9
+ import {
10
+ createCloudflareApi,
11
+ type CloudflareApi,
12
+ type CloudflareApiOptions,
13
+ } from "./api.ts";
14
+ import type { Tunnel } from "./tunnel.ts";
15
+
16
+ /**
17
+ * Route data as returned by Cloudflare API
18
+ * @internal
19
+ */
20
+ interface CloudflareRoute {
21
+ id: string;
22
+ network: string;
23
+ tunnel_id: string;
24
+ comment?: string;
25
+ virtual_network_id?: string;
26
+ created_at: string;
27
+ deleted_at: string | null;
28
+ }
29
+
30
+ /**
31
+ * Properties for creating or updating a Cloudflare Tunnel Route
32
+ */
33
+ export interface TunnelRouteProps extends CloudflareApiOptions {
34
+ /**
35
+ * The private IPv4 or IPv6 range connected by the route, in CIDR notation
36
+ * (e.g., "172.16.0.0/16" or "2001:db8::/32")
37
+ *
38
+ * Note: The network CIDR is immutable and cannot be changed after creation.
39
+ * When updating a route, any network change will trigger a replacement.
40
+ */
41
+ network: string;
42
+
43
+ /**
44
+ * The tunnel to route traffic through
45
+ * Can be a Tunnel resource or a tunnel ID (UUID)
46
+ */
47
+ tunnel: string | Tunnel;
48
+
49
+ /**
50
+ * Optional remark describing the route
51
+ * @maxLength 100
52
+ */
53
+ comment?: string;
54
+
55
+ /**
56
+ * UUID of the virtual network
57
+ * If not provided, the route will be added to the default virtual network
58
+ */
59
+ virtualNetworkId?: string;
60
+
61
+ /**
62
+ * Whether to adopt an existing route with the same network and tunnel if it exists
63
+ * If true and a route with the same network and tunnel exists, it will be adopted rather than creating a new one
64
+ *
65
+ * @default false
66
+ */
67
+ adopt?: boolean;
68
+
69
+ /**
70
+ * Whether to delete the route when removed from Alchemy
71
+ * If set to false, the route will remain but the resource will be removed from state
72
+ *
73
+ * @default true
74
+ */
75
+ delete?: boolean;
76
+
77
+ /**
78
+ * Internal route ID for lifecycle management
79
+ * @internal
80
+ */
81
+ routeId?: string;
82
+ }
83
+
84
+ export function isTunnelRoute(resource: any): resource is TunnelRoute {
85
+ return resource?.[ResourceKind] === "cloudflare::TunnelRoute";
86
+ }
87
+
88
+ /**
89
+ * Output returned after TunnelRoute creation/update
90
+ */
91
+ export interface TunnelRoute
92
+ extends Omit<TunnelRouteProps, "delete" | "tunnel" | "routeId"> {
93
+ /**
94
+ * The ID of the route
95
+ */
96
+ id: string;
97
+
98
+ /**
99
+ * The private IPv4 or IPv6 range connected by the route, in CIDR notation
100
+ */
101
+ network: string;
102
+
103
+ /**
104
+ * The UUID of the tunnel this route uses
105
+ */
106
+ tunnelId: string;
107
+
108
+ /**
109
+ * Optional remark describing the route
110
+ */
111
+ comment?: string;
112
+
113
+ /**
114
+ * UUID of the virtual network
115
+ */
116
+ virtualNetworkId?: string;
117
+
118
+ /**
119
+ * Time at which the route was created
120
+ */
121
+ createdAt: string;
122
+
123
+ /**
124
+ * Time at which the route was deleted (null if active)
125
+ */
126
+ deletedAt: string | null;
127
+
128
+ /**
129
+ * Resource type identifier for binding
130
+ * @internal
131
+ */
132
+ type: "cloudflare::TunnelRoute";
133
+ }
134
+
135
+ /**
136
+ * Creates and manages a Cloudflare Tunnel Route, which routes private network traffic
137
+ * through a Cloudflare Tunnel. This resource handles the route lifecycle (create, update, delete).
138
+ *
139
+ * @remarks
140
+ * Tunnel Routes enable private network access by routing CIDR ranges through Cloudflare Tunnels.
141
+ * This is commonly used for Zero Trust network access scenarios where you need to connect
142
+ * private networks to Cloudflare's edge.
143
+ *
144
+ * @see https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/private-net/
145
+ * @see https://developers.cloudflare.com/api/operations/zero-trust-networks-routes-create-a-tunnel-route
146
+ *
147
+ * @example
148
+ * // Create a basic tunnel route for a private network
149
+ * const tunnel = await Tunnel("my-tunnel", {
150
+ * name: "my-tunnel"
151
+ * });
152
+ *
153
+ * const route = await TunnelRoute("private-network", {
154
+ * network: "172.16.0.0/16",
155
+ * tunnel: tunnel
156
+ * });
157
+ *
158
+ * @example
159
+ * // Create a route with a comment and virtual network
160
+ * const route = await TunnelRoute("vpc-route", {
161
+ * network: "10.0.0.0/8",
162
+ * tunnel: "f70ff985-a4ef-4643-bbbc-4a0ed4fc8415",
163
+ * comment: "Main VPC network route",
164
+ * virtualNetworkId: "f70ff985-a4ef-4643-bbbc-4a0ed4fc8415"
165
+ * });
166
+ *
167
+ * @example
168
+ * // Adopt an existing route if it already exists
169
+ * const route = await TunnelRoute("existing-route", {
170
+ * network: "192.168.1.0/24",
171
+ * tunnel: tunnel,
172
+ * adopt: true,
173
+ * comment: "Updated comment"
174
+ * });
175
+ *
176
+ * @example
177
+ * // Create a route without deleting it when removed from Alchemy
178
+ * const route = await TunnelRoute("persistent-route", {
179
+ * network: "10.1.0.0/16",
180
+ * tunnel: tunnel,
181
+ * delete: false
182
+ * });
183
+ */
184
+ export const TunnelRoute = Resource(
185
+ "cloudflare::TunnelRoute",
186
+ async function (
187
+ this: Context<TunnelRoute>,
188
+ id: string,
189
+ props: TunnelRouteProps,
190
+ ): Promise<TunnelRoute> {
191
+ // Create Cloudflare API client with automatic account discovery
192
+ const api = await createCloudflareApi(props);
193
+
194
+ // Resolve tunnel ID from either string or Tunnel resource
195
+ const tunnelId =
196
+ typeof props.tunnel === "string" ? props.tunnel : props.tunnel.tunnelId;
197
+
198
+ const routeId = props.routeId || this.output?.id;
199
+ const adopt = props.adopt ?? this.scope.adopt;
200
+
201
+ if (this.phase === "delete") {
202
+ // For delete operations, check if the route ID exists in the output
203
+ if (routeId && props.delete !== false) {
204
+ await deleteRoute(api, routeId);
205
+ }
206
+
207
+ // Return destroyed state
208
+ return this.destroy();
209
+ }
210
+
211
+ // Check if network is being changed - network is immutable
212
+ if (
213
+ this.phase === "update" &&
214
+ this.output?.network &&
215
+ this.output.network !== props.network
216
+ ) {
217
+ logger.log(
218
+ `Network changed from '${this.output.network}' to '${props.network}', replacing route`,
219
+ );
220
+ this.replace(true);
221
+ }
222
+
223
+ // Check if tunnel is being changed - tunnel is immutable
224
+ if (
225
+ this.phase === "update" &&
226
+ this.output?.tunnelId &&
227
+ this.output.tunnelId !== tunnelId
228
+ ) {
229
+ logger.log(
230
+ `Tunnel changed from '${this.output.tunnelId}' to '${tunnelId}', replacing route`,
231
+ );
232
+ this.replace(true);
233
+ }
234
+
235
+ let routeData: CloudflareRoute;
236
+
237
+ if (this.phase === "update" && routeId) {
238
+ // Update existing route (only comment and virtualNetworkId can be updated)
239
+ routeData = await updateRoute(api, routeId, {
240
+ comment: props.comment,
241
+ virtualNetworkId: props.virtualNetworkId,
242
+ });
243
+ } else {
244
+ // Create new route
245
+ try {
246
+ routeData = await createRoute(api, {
247
+ network: props.network,
248
+ tunnelId,
249
+ comment: props.comment,
250
+ virtualNetworkId: props.virtualNetworkId,
251
+ });
252
+ } catch (error) {
253
+ // Check if this is a "route already exists" error and adopt is enabled
254
+ if (
255
+ adopt &&
256
+ error instanceof Error &&
257
+ (error.message.includes("already exists") ||
258
+ error.message.includes("duplicate") ||
259
+ (error as any).status === 409)
260
+ ) {
261
+ logger.log(
262
+ `Route for network '${props.network}' and tunnel '${tunnelId}' already exists, adopting it`,
263
+ );
264
+
265
+ // Find the existing route by network and tunnel
266
+ const existingRoute = await findRouteByNetworkAndTunnel(
267
+ api,
268
+ props.network,
269
+ tunnelId,
270
+ );
271
+
272
+ if (!existingRoute) {
273
+ throw new Error(
274
+ `Failed to find existing route for network '${props.network}' and tunnel '${tunnelId}' for adoption`,
275
+ );
276
+ }
277
+
278
+ routeData = existingRoute;
279
+
280
+ // Update comment/virtualNetworkId if provided
281
+ if (
282
+ props.comment !== undefined ||
283
+ props.virtualNetworkId !== undefined
284
+ ) {
285
+ routeData = await updateRoute(api, existingRoute.id, {
286
+ comment: props.comment,
287
+ virtualNetworkId: props.virtualNetworkId,
288
+ });
289
+ }
290
+ } else {
291
+ // Re-throw the error if adopt is false or it's not an "already exists" error
292
+ throw error;
293
+ }
294
+ }
295
+ }
296
+
297
+ // Transform API response to our interface
298
+ return {
299
+ id: routeData.id,
300
+ network: routeData.network,
301
+ tunnelId: routeData.tunnel_id,
302
+ comment: routeData.comment,
303
+ virtualNetworkId: routeData.virtual_network_id,
304
+ createdAt: routeData.created_at,
305
+ deletedAt: routeData.deleted_at,
306
+ type: "cloudflare::TunnelRoute",
307
+ };
308
+ },
309
+ );
310
+
311
+ /**
312
+ * Get tunnel route details
313
+ * @internal
314
+ */
315
+ export async function getTunnelRoute(
316
+ api: CloudflareApi,
317
+ routeId: string,
318
+ ): Promise<CloudflareRoute> {
319
+ const response = await api.get(
320
+ `/accounts/${api.accountId}/teamnet/routes/${routeId}`,
321
+ );
322
+
323
+ if (!response.ok) {
324
+ await handleApiError(response, "get", "route", routeId);
325
+ }
326
+
327
+ const data =
328
+ (await response.json()) as CloudflareApiResponse<CloudflareRoute>;
329
+ return data.result;
330
+ }
331
+
332
+ /**
333
+ * List all tunnel routes with pagination support
334
+ * @internal
335
+ */
336
+ export async function listTunnelRoutes(
337
+ api: CloudflareApi,
338
+ options?: {
339
+ /** Maximum number of routes to return */
340
+ limit?: number;
341
+ },
342
+ ): Promise<CloudflareRoute[]> {
343
+ const routes: CloudflareRoute[] = [];
344
+ let page = 1;
345
+ const perPage = 100; // Maximum allowed by API
346
+ let hasMorePages = true;
347
+ const limit = options?.limit;
348
+
349
+ while (hasMorePages) {
350
+ const params = new URLSearchParams({
351
+ page: page.toString(),
352
+ per_page: perPage.toString(),
353
+ });
354
+
355
+ const response = await api.get(
356
+ `/accounts/${api.accountId}/teamnet/routes?${params.toString()}`,
357
+ );
358
+
359
+ if (!response.ok) {
360
+ await handleApiError(response, "list", "route", "all");
361
+ }
362
+
363
+ const data =
364
+ (await response.json()) as CloudflareApiListResponse<CloudflareRoute>;
365
+
366
+ routes.push(...data.result);
367
+ const resultInfo = data.result_info;
368
+
369
+ // Check if we've reached the limit
370
+ if (limit && routes.length >= limit) {
371
+ return routes.slice(0, limit);
372
+ }
373
+
374
+ // Check if we've seen all pages
375
+ hasMorePages =
376
+ resultInfo.page * resultInfo.per_page < resultInfo.total_count;
377
+ page++;
378
+ }
379
+
380
+ return routes;
381
+ }
382
+
383
+ /**
384
+ * Find a route by network and tunnel ID
385
+ * @internal
386
+ */
387
+ export async function findRouteByNetworkAndTunnel(
388
+ api: CloudflareApi,
389
+ network: string,
390
+ tunnelId: string,
391
+ ): Promise<CloudflareRoute | null> {
392
+ const routes = await listTunnelRoutes(api);
393
+
394
+ // Look for a route with matching network and tunnel_id
395
+ const match = routes.find(
396
+ (route) => route.network === network && route.tunnel_id === tunnelId,
397
+ );
398
+
399
+ return match || null;
400
+ }
401
+
402
+ /**
403
+ * Delete a route
404
+ * @internal
405
+ */
406
+ async function deleteRoute(api: CloudflareApi, routeId: string): Promise<void> {
407
+ const response = await api.delete(
408
+ `/accounts/${api.accountId}/teamnet/routes/${routeId}`,
409
+ );
410
+
411
+ if (!response.ok && response.status !== 404) {
412
+ await handleApiError(response, "delete", "route", routeId);
413
+ }
414
+ }
415
+
416
+ /**
417
+ * Create a new route
418
+ * @internal
419
+ */
420
+ async function createRoute(
421
+ api: CloudflareApi,
422
+ props: {
423
+ network: string;
424
+ tunnelId: string;
425
+ comment?: string;
426
+ virtualNetworkId?: string;
427
+ },
428
+ ): Promise<CloudflareRoute> {
429
+ const payload: Record<string, any> = {
430
+ network: props.network,
431
+ tunnel_id: props.tunnelId,
432
+ };
433
+
434
+ if (props.comment !== undefined) {
435
+ payload.comment = props.comment;
436
+ }
437
+
438
+ if (props.virtualNetworkId !== undefined) {
439
+ payload.virtual_network_id = props.virtualNetworkId;
440
+ }
441
+
442
+ const response = await api.post(
443
+ `/accounts/${api.accountId}/teamnet/routes`,
444
+ payload,
445
+ );
446
+
447
+ if (!response.ok) {
448
+ await handleApiError(
449
+ response,
450
+ "create",
451
+ "route",
452
+ `${props.network} -> ${props.tunnelId}`,
453
+ );
454
+ }
455
+
456
+ const data =
457
+ (await response.json()) as CloudflareApiResponse<CloudflareRoute>;
458
+ return data.result;
459
+ }
460
+
461
+ /**
462
+ * Update route configuration
463
+ * @internal
464
+ */
465
+ async function updateRoute(
466
+ api: CloudflareApi,
467
+ routeId: string,
468
+ props: {
469
+ comment?: string;
470
+ virtualNetworkId?: string;
471
+ },
472
+ ): Promise<CloudflareRoute> {
473
+ const payload: Record<string, any> = {};
474
+
475
+ if (props.comment !== undefined) {
476
+ payload.comment = props.comment;
477
+ }
478
+
479
+ if (props.virtualNetworkId !== undefined) {
480
+ payload.virtual_network_id = props.virtualNetworkId;
481
+ }
482
+
483
+ const response = await api.patch(
484
+ `/accounts/${api.accountId}/teamnet/routes/${routeId}`,
485
+ payload,
486
+ );
487
+
488
+ if (!response.ok) {
489
+ await handleApiError(response, "update", "route", routeId);
490
+ }
491
+
492
+ const data =
493
+ (await response.json()) as CloudflareApiResponse<CloudflareRoute>;
494
+ return data.result;
495
+ }