alchemy 0.80.0 → 0.81.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.
- package/bin/alchemy.js +11 -5
- package/bin/services/execute-alchemy.ts +18 -6
- package/lib/alchemy.js +1 -1
- package/lib/alchemy.js.map +1 -1
- package/lib/cloudflare/compatibility-date.gen.d.ts +1 -1
- package/lib/cloudflare/compatibility-date.gen.js +1 -1
- package/lib/cloudflare/hyperdrive.d.ts +23 -5
- package/lib/cloudflare/hyperdrive.d.ts.map +1 -1
- package/lib/cloudflare/hyperdrive.js.map +1 -1
- package/lib/cloudflare/index.d.ts +3 -0
- package/lib/cloudflare/index.d.ts.map +1 -1
- package/lib/cloudflare/index.js +3 -0
- package/lib/cloudflare/index.js.map +1 -1
- package/lib/cloudflare/miniflare/miniflare-worker-proxy.d.ts.map +1 -1
- package/lib/cloudflare/miniflare/miniflare-worker-proxy.js +7 -0
- package/lib/cloudflare/miniflare/miniflare-worker-proxy.js.map +1 -1
- package/lib/cloudflare/queue.d.ts +0 -3
- package/lib/cloudflare/queue.d.ts.map +1 -1
- package/lib/cloudflare/queue.js +53 -31
- package/lib/cloudflare/queue.js.map +1 -1
- package/lib/cloudflare/tunnel-route.d.ts +172 -0
- package/lib/cloudflare/tunnel-route.d.ts.map +1 -0
- package/lib/cloudflare/tunnel-route.js +251 -0
- package/lib/cloudflare/tunnel-route.js.map +1 -0
- package/lib/cloudflare/vite/vite.d.ts.map +1 -1
- package/lib/cloudflare/vite/vite.js +38 -1
- package/lib/cloudflare/vite/vite.js.map +1 -1
- package/lib/cloudflare/warp-default-profile.d.ts +133 -0
- package/lib/cloudflare/warp-default-profile.d.ts.map +1 -0
- package/lib/cloudflare/warp-default-profile.js +138 -0
- package/lib/cloudflare/warp-default-profile.js.map +1 -0
- package/lib/cloudflare/warp-device-profile.d.ts +272 -0
- package/lib/cloudflare/warp-device-profile.d.ts.map +1 -0
- package/lib/cloudflare/warp-device-profile.js +291 -0
- package/lib/cloudflare/warp-device-profile.js.map +1 -0
- package/lib/scope.d.ts +1 -1
- package/lib/scope.d.ts.map +1 -1
- package/lib/scope.js +1 -1
- package/lib/scope.js.map +1 -1
- package/package.json +1 -1
- package/src/alchemy.ts +1 -1
- package/src/cloudflare/compatibility-date.gen.ts +1 -1
- package/src/cloudflare/hyperdrive.ts +27 -5
- package/src/cloudflare/index.ts +3 -0
- package/src/cloudflare/miniflare/miniflare-worker-proxy.ts +11 -0
- package/src/cloudflare/queue.ts +97 -56
- package/src/cloudflare/tunnel-route.ts +495 -0
- package/src/cloudflare/vite/vite.ts +29 -1
- package/src/cloudflare/warp-default-profile.ts +320 -0
- package/src/cloudflare/warp-device-profile.ts +616 -0
- package/src/scope.ts +2 -2
- package/workers/tunnel-proxy.js +1 -1
package/src/cloudflare/queue.ts
CHANGED
|
@@ -502,34 +502,92 @@ export async function updateQueue(
|
|
|
502
502
|
/**
|
|
503
503
|
* List all Cloudflare Queues in an account
|
|
504
504
|
*/
|
|
505
|
-
|
|
505
|
+
type CloudflareQueueListItem = {
|
|
506
|
+
queue_name: string;
|
|
507
|
+
queue_id: string;
|
|
508
|
+
created_on?: string;
|
|
509
|
+
modified_on?: string;
|
|
510
|
+
settings?: {
|
|
511
|
+
delivery_delay?: number;
|
|
512
|
+
delivery_paused?: boolean;
|
|
513
|
+
message_retention_period?: number;
|
|
514
|
+
};
|
|
515
|
+
};
|
|
516
|
+
|
|
517
|
+
type CloudflareQueueListResponse = {
|
|
518
|
+
success: boolean;
|
|
519
|
+
errors?: Array<{ code: number; message: string }>;
|
|
520
|
+
result?: CloudflareQueueListItem[];
|
|
521
|
+
result_info?: {
|
|
522
|
+
per_page?: number;
|
|
523
|
+
total_pages?: number;
|
|
524
|
+
};
|
|
525
|
+
};
|
|
526
|
+
|
|
527
|
+
const CLOUDFLARE_QUEUE_PAGE_SIZE = 100;
|
|
528
|
+
|
|
529
|
+
async function fetchQueuePage(
|
|
506
530
|
api: CloudflareApi,
|
|
507
|
-
|
|
508
|
-
|
|
531
|
+
page: number,
|
|
532
|
+
perPage = CLOUDFLARE_QUEUE_PAGE_SIZE,
|
|
533
|
+
): Promise<CloudflareQueueListResponse> {
|
|
534
|
+
const searchParams = new URLSearchParams({
|
|
535
|
+
page: page.toString(),
|
|
536
|
+
per_page: perPage.toString(),
|
|
537
|
+
});
|
|
538
|
+
const response = await api.get(
|
|
539
|
+
`/accounts/${api.accountId}/queues?${searchParams.toString()}`,
|
|
540
|
+
);
|
|
509
541
|
|
|
510
542
|
if (!response.ok) {
|
|
511
|
-
|
|
512
|
-
`Failed to list queues: ${response.statusText}`,
|
|
513
|
-
response,
|
|
514
|
-
);
|
|
543
|
+
return await handleApiError(response, "listing", "Queues", "");
|
|
515
544
|
}
|
|
516
545
|
|
|
517
|
-
const data = (await response.json()) as
|
|
518
|
-
success: boolean;
|
|
519
|
-
errors?: Array<{ code: number; message: string }>;
|
|
520
|
-
result?: Array<{
|
|
521
|
-
queue_name: string;
|
|
522
|
-
queue_id: string;
|
|
523
|
-
}>;
|
|
524
|
-
};
|
|
546
|
+
const data = (await response.json()) as CloudflareQueueListResponse;
|
|
525
547
|
|
|
526
548
|
if (!data.success) {
|
|
527
549
|
const errorMessage = data.errors?.[0]?.message || "Unknown error";
|
|
528
550
|
throw new Error(`Failed to list queues: ${errorMessage}`);
|
|
529
551
|
}
|
|
530
552
|
|
|
553
|
+
return data;
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
function hasMoreQueuePages(
|
|
557
|
+
data: CloudflareQueueListResponse,
|
|
558
|
+
currentPage: number,
|
|
559
|
+
perPage: number,
|
|
560
|
+
): boolean {
|
|
561
|
+
if (data.result_info?.total_pages !== undefined) {
|
|
562
|
+
return currentPage < data.result_info.total_pages;
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
const pageSize = data.result_info?.per_page ?? perPage;
|
|
566
|
+
const currentCount = data.result?.length ?? 0;
|
|
567
|
+
|
|
568
|
+
return currentCount > 0 && currentCount >= pageSize;
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
export async function listQueues(
|
|
572
|
+
api: CloudflareApi,
|
|
573
|
+
): Promise<{ name: string; id: string }[]> {
|
|
574
|
+
const queues: CloudflareQueueListItem[] = [];
|
|
575
|
+
let page = 1;
|
|
576
|
+
|
|
577
|
+
// Paginate through all queues to ensure nothing is missed on later pages
|
|
578
|
+
while (true) {
|
|
579
|
+
const data = await fetchQueuePage(api, page);
|
|
580
|
+
queues.push(...(data.result ?? []));
|
|
581
|
+
|
|
582
|
+
if (!hasMoreQueuePages(data, page, CLOUDFLARE_QUEUE_PAGE_SIZE)) {
|
|
583
|
+
break;
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
page += 1;
|
|
587
|
+
}
|
|
588
|
+
|
|
531
589
|
// Transform API response
|
|
532
|
-
return
|
|
590
|
+
return queues.map((queue) => ({
|
|
533
591
|
name: queue.queue_name,
|
|
534
592
|
id: queue.queue_id,
|
|
535
593
|
}));
|
|
@@ -542,48 +600,31 @@ export async function findQueueByName(
|
|
|
542
600
|
api: CloudflareApi,
|
|
543
601
|
queueName: string,
|
|
544
602
|
): Promise<CloudflareQueueResponse | null> {
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
603
|
+
let page = 1;
|
|
604
|
+
|
|
605
|
+
while (true) {
|
|
606
|
+
const data = await fetchQueuePage(api, page);
|
|
607
|
+
const queue = data.result?.find((q) => q.queue_name === queueName);
|
|
608
|
+
|
|
609
|
+
if (queue) {
|
|
610
|
+
return {
|
|
611
|
+
result: {
|
|
612
|
+
queue_id: queue.queue_id,
|
|
613
|
+
queue_name: queue.queue_name,
|
|
614
|
+
created_on: queue.created_on,
|
|
615
|
+
modified_on: queue.modified_on,
|
|
616
|
+
settings: queue.settings,
|
|
617
|
+
},
|
|
618
|
+
success: true,
|
|
619
|
+
errors: [],
|
|
620
|
+
messages: [],
|
|
563
621
|
};
|
|
564
|
-
}
|
|
565
|
-
};
|
|
622
|
+
}
|
|
566
623
|
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
}
|
|
624
|
+
if (!hasMoreQueuePages(data, page, CLOUDFLARE_QUEUE_PAGE_SIZE)) {
|
|
625
|
+
return null;
|
|
626
|
+
}
|
|
571
627
|
|
|
572
|
-
|
|
573
|
-
if (!queue) {
|
|
574
|
-
return null;
|
|
628
|
+
page += 1;
|
|
575
629
|
}
|
|
576
|
-
|
|
577
|
-
return {
|
|
578
|
-
result: {
|
|
579
|
-
queue_id: queue.queue_id,
|
|
580
|
-
queue_name: queue.queue_name,
|
|
581
|
-
created_on: queue.created_on,
|
|
582
|
-
modified_on: queue.modified_on,
|
|
583
|
-
settings: queue.settings,
|
|
584
|
-
},
|
|
585
|
-
success: true,
|
|
586
|
-
errors: [],
|
|
587
|
-
messages: [],
|
|
588
|
-
};
|
|
589
630
|
}
|
|
@@ -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
|
+
}
|