alchemy 0.57.2 → 0.59.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 +1 -1
- package/lib/apply.js +3 -3
- package/lib/apply.js.map +1 -1
- package/lib/cloudflare/api-error.js +1 -1
- package/lib/cloudflare/api-error.js.map +1 -1
- package/lib/cloudflare/api-response.d.ts +19 -0
- package/lib/cloudflare/api-response.d.ts.map +1 -1
- package/lib/cloudflare/api-response.js.map +1 -1
- package/lib/cloudflare/api.d.ts.map +1 -1
- package/lib/cloudflare/api.js +10 -2
- package/lib/cloudflare/api.js.map +1 -1
- package/lib/cloudflare/assets.d.ts +1 -1
- package/lib/cloudflare/assets.d.ts.map +1 -1
- package/lib/cloudflare/bucket.d.ts +21 -3
- package/lib/cloudflare/bucket.d.ts.map +1 -1
- package/lib/cloudflare/bucket.js +83 -53
- package/lib/cloudflare/bucket.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/container.d.ts +8 -0
- package/lib/cloudflare/container.d.ts.map +1 -1
- package/lib/cloudflare/container.js +7 -1
- package/lib/cloudflare/container.js.map +1 -1
- package/lib/cloudflare/custom-domain.d.ts +8 -0
- package/lib/cloudflare/custom-domain.d.ts.map +1 -1
- package/lib/cloudflare/custom-domain.js +11 -0
- package/lib/cloudflare/custom-domain.js.map +1 -1
- package/lib/cloudflare/d1-database.d.ts +15 -1
- package/lib/cloudflare/d1-database.d.ts.map +1 -1
- package/lib/cloudflare/d1-database.js +24 -17
- package/lib/cloudflare/d1-database.js.map +1 -1
- package/lib/cloudflare/d1-import.d.ts.map +1 -1
- package/lib/cloudflare/d1-import.js.map +1 -1
- package/lib/cloudflare/hyperdrive.d.ts +37 -9
- package/lib/cloudflare/hyperdrive.d.ts.map +1 -1
- package/lib/cloudflare/hyperdrive.js +91 -5
- package/lib/cloudflare/hyperdrive.js.map +1 -1
- 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/kv-namespace.d.ts +23 -4
- package/lib/cloudflare/kv-namespace.d.ts.map +1 -1
- package/lib/cloudflare/kv-namespace.js +70 -58
- package/lib/cloudflare/kv-namespace.js.map +1 -1
- package/lib/cloudflare/miniflare/build-worker-options.d.ts +2 -0
- package/lib/cloudflare/miniflare/build-worker-options.d.ts.map +1 -1
- package/lib/cloudflare/miniflare/build-worker-options.js +9 -7
- package/lib/cloudflare/miniflare/build-worker-options.js.map +1 -1
- package/lib/cloudflare/miniflare/delete.d.ts +21 -0
- package/lib/cloudflare/miniflare/delete.d.ts.map +1 -0
- package/lib/cloudflare/miniflare/delete.js +91 -0
- package/lib/cloudflare/miniflare/delete.js.map +1 -0
- package/lib/cloudflare/miniflare/miniflare-controller.d.ts.map +1 -1
- package/lib/cloudflare/miniflare/miniflare-controller.js +26 -9
- package/lib/cloudflare/miniflare/miniflare-controller.js.map +1 -1
- package/lib/cloudflare/miniflare/remote-binding-proxy.d.ts +2 -0
- package/lib/cloudflare/miniflare/remote-binding-proxy.d.ts.map +1 -1
- package/lib/cloudflare/miniflare/remote-binding-proxy.js +3 -4
- package/lib/cloudflare/miniflare/remote-binding-proxy.js.map +1 -1
- package/lib/cloudflare/pipeline.d.ts +3 -0
- package/lib/cloudflare/pipeline.d.ts.map +1 -1
- package/lib/cloudflare/pipeline.js +14 -1
- package/lib/cloudflare/pipeline.js.map +1 -1
- package/lib/cloudflare/queue-consumer.d.ts +8 -0
- package/lib/cloudflare/queue-consumer.d.ts.map +1 -1
- package/lib/cloudflare/queue-consumer.js +15 -4
- package/lib/cloudflare/queue-consumer.js.map +1 -1
- package/lib/cloudflare/queue.d.ts +22 -4
- package/lib/cloudflare/queue.d.ts.map +1 -1
- package/lib/cloudflare/queue.js +32 -9
- package/lib/cloudflare/queue.js.map +1 -1
- package/lib/cloudflare/route.d.ts +8 -0
- package/lib/cloudflare/route.d.ts.map +1 -1
- package/lib/cloudflare/route.js +13 -0
- package/lib/cloudflare/route.js.map +1 -1
- package/lib/cloudflare/tunnel.d.ts +448 -0
- package/lib/cloudflare/tunnel.d.ts.map +1 -0
- package/lib/cloudflare/tunnel.js +467 -0
- package/lib/cloudflare/tunnel.js.map +1 -0
- package/lib/cloudflare/worker-subdomain.d.ts +8 -0
- package/lib/cloudflare/worker-subdomain.d.ts.map +1 -1
- package/lib/cloudflare/worker-subdomain.js +9 -1
- package/lib/cloudflare/worker-subdomain.js.map +1 -1
- package/lib/cloudflare/worker.d.ts +7 -1
- package/lib/cloudflare/worker.d.ts.map +1 -1
- package/lib/cloudflare/worker.js +405 -375
- package/lib/cloudflare/worker.js.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 +24 -12
- package/lib/cloudflare/wrangler.json.js.map +1 -1
- package/lib/destroy.d.ts.map +1 -1
- package/lib/destroy.js +4 -8
- package/lib/destroy.js.map +1 -1
- package/lib/esbuild/bundle.d.ts +1 -1
- package/lib/secret.d.ts +18 -1
- package/lib/secret.d.ts.map +1 -1
- package/lib/secret.js +25 -0
- package/lib/secret.js.map +1 -1
- package/package.json +1 -1
- package/src/apply.ts +5 -6
- package/src/cloudflare/api-error.ts +2 -2
- package/src/cloudflare/api-response.ts +22 -0
- package/src/cloudflare/api.ts +13 -2
- package/src/cloudflare/bucket.ts +126 -68
- package/src/cloudflare/compatibility-date.gen.ts +1 -1
- package/src/cloudflare/container.ts +18 -1
- package/src/cloudflare/custom-domain.ts +22 -0
- package/src/cloudflare/d1-database.ts +44 -19
- package/src/cloudflare/d1-import.ts +1 -14
- package/src/cloudflare/hyperdrive.ts +170 -14
- package/src/cloudflare/index.ts +1 -0
- package/src/cloudflare/kv-namespace.ts +117 -73
- package/src/cloudflare/miniflare/build-worker-options.ts +12 -8
- package/src/cloudflare/miniflare/delete.ts +136 -0
- package/src/cloudflare/miniflare/miniflare-controller.ts +26 -9
- package/src/cloudflare/miniflare/remote-binding-proxy.ts +5 -4
- package/src/cloudflare/pipeline.ts +17 -1
- package/src/cloudflare/queue-consumer.ts +27 -5
- package/src/cloudflare/queue.ts +62 -12
- package/src/cloudflare/route.ts +24 -0
- package/src/cloudflare/tunnel.ts +911 -0
- package/src/cloudflare/worker-subdomain.ts +29 -6
- package/src/cloudflare/worker.ts +484 -465
- package/src/cloudflare/wrangler.json.ts +34 -13
- package/src/destroy.ts +4 -8
- package/src/secret.ts +30 -1
- package/templates/astro/README.md +2 -0
- package/templates/nuxt/README.md +2 -0
- package/templates/react-router/README.md +2 -0
- package/templates/rwsdk/README.md +2 -0
- package/templates/sveltekit/README.md +2 -0
- package/templates/tanstack-start/README.md +2 -0
- package/templates/typescript/README.md +2 -0
- package/templates/vite/README.md +2 -0
|
@@ -0,0 +1,911 @@
|
|
|
1
|
+
import { alchemy } from "../alchemy.ts";
|
|
2
|
+
import type { Context } from "../context.ts";
|
|
3
|
+
import { Resource, ResourceKind } from "../resource.ts";
|
|
4
|
+
import type { Secret } from "../secret.ts";
|
|
5
|
+
import { logger } from "../util/logger.ts";
|
|
6
|
+
import { handleApiError } from "./api-error.ts";
|
|
7
|
+
import type {
|
|
8
|
+
CloudflareApiListResponse,
|
|
9
|
+
CloudflareApiResponse,
|
|
10
|
+
} from "./api-response.ts";
|
|
11
|
+
import {
|
|
12
|
+
createCloudflareApi,
|
|
13
|
+
type CloudflareApi,
|
|
14
|
+
type CloudflareApiOptions,
|
|
15
|
+
} from "./api.ts";
|
|
16
|
+
import { DnsRecords } from "./dns-records.ts";
|
|
17
|
+
import { findZoneForHostname } from "./zone.ts";
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Tunnel data as returned by Cloudflare API
|
|
21
|
+
*/
|
|
22
|
+
interface CloudflareTunnel {
|
|
23
|
+
id: string;
|
|
24
|
+
account_tag: string;
|
|
25
|
+
created_at: string;
|
|
26
|
+
deleted_at: string | null;
|
|
27
|
+
name: string;
|
|
28
|
+
metadata?: Record<string, any>;
|
|
29
|
+
credentials_file?: {
|
|
30
|
+
AccountTag: string;
|
|
31
|
+
TunnelID: string;
|
|
32
|
+
TunnelName: string;
|
|
33
|
+
TunnelSecret: string;
|
|
34
|
+
};
|
|
35
|
+
token?: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Properties for creating or updating a Cloudflare Tunnel
|
|
40
|
+
*
|
|
41
|
+
* @remarks
|
|
42
|
+
* This interface includes all configuration options supported by Cloudflare Tunnels
|
|
43
|
+
* for both remotely-managed (configSrc: 'cloudflare') and locally-managed tunnels.
|
|
44
|
+
*/
|
|
45
|
+
export interface TunnelProps extends CloudflareApiOptions {
|
|
46
|
+
/**
|
|
47
|
+
* Name for the tunnel
|
|
48
|
+
*
|
|
49
|
+
* Note: Tunnel names are immutable and cannot be changed after creation.
|
|
50
|
+
* When updating a tunnel, any name change will be ignored.
|
|
51
|
+
*
|
|
52
|
+
* @default id
|
|
53
|
+
*/
|
|
54
|
+
name?: string;
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Secret for the tunnel
|
|
58
|
+
* If not provided, will be generated automatically
|
|
59
|
+
*/
|
|
60
|
+
tunnelSecret?: Secret<string>;
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Optional metadata object for the tunnel
|
|
64
|
+
*/
|
|
65
|
+
metadata?: Record<string, any>;
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Configuration source
|
|
69
|
+
* - 'cloudflare' - Use Cloudflare configuration (default, managed via API)
|
|
70
|
+
* - 'local' - Use local configuration (managed via config file)
|
|
71
|
+
*
|
|
72
|
+
* @default 'cloudflare'
|
|
73
|
+
* @see https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/get-started/tunnel-useful-terms/#remotely-managed-tunnel
|
|
74
|
+
* @see https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/get-started/tunnel-useful-terms/#locally-managed-tunnel
|
|
75
|
+
*/
|
|
76
|
+
configSrc?: "cloudflare" | "local";
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Ingress rules defining how requests are routed
|
|
80
|
+
* Must include a catch-all rule at the end
|
|
81
|
+
* Only used when configSrc is 'cloudflare'
|
|
82
|
+
*
|
|
83
|
+
* @see https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/configure-tunnels/origin-configuration/#ingress-rules
|
|
84
|
+
*/
|
|
85
|
+
ingress?: IngressRule[];
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* WarpRouting configuration for private network access
|
|
89
|
+
* Only used when configSrc is 'cloudflare'
|
|
90
|
+
*
|
|
91
|
+
* @see https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/private-net/
|
|
92
|
+
*/
|
|
93
|
+
warpRouting?: {
|
|
94
|
+
enabled?: boolean;
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Origin request configuration to apply to all rules
|
|
99
|
+
* Only used when configSrc is 'cloudflare'
|
|
100
|
+
*
|
|
101
|
+
* @see https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/configure-tunnels/origin-configuration/#origin-request-parameters
|
|
102
|
+
*/
|
|
103
|
+
originRequest?: OriginRequestConfig;
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Whether to adopt an existing tunnel with the same name if it exists
|
|
107
|
+
* If true and a tunnel with the same name exists, it will be adopted rather than creating a new one
|
|
108
|
+
*
|
|
109
|
+
* @default false
|
|
110
|
+
*/
|
|
111
|
+
adopt?: boolean;
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Whether to delete the tunnel.
|
|
115
|
+
* If set to false, the tunnel will remain but the resource will be removed from state
|
|
116
|
+
*
|
|
117
|
+
* @default true
|
|
118
|
+
*/
|
|
119
|
+
delete?: boolean;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Tunnel configuration for routing traffic
|
|
124
|
+
*/
|
|
125
|
+
export interface TunnelConfig {
|
|
126
|
+
/**
|
|
127
|
+
* Ingress rules defining how requests are routed
|
|
128
|
+
* Must include a catch-all rule at the end
|
|
129
|
+
*/
|
|
130
|
+
ingress?: IngressRule[];
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* WarpRouting configuration for private network access
|
|
134
|
+
*/
|
|
135
|
+
warpRouting?: {
|
|
136
|
+
enabled?: boolean;
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Origin request configuration to apply to all rules
|
|
141
|
+
*/
|
|
142
|
+
originRequest?: OriginRequestConfig;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Ingress rule defining how a hostname is routed
|
|
147
|
+
*
|
|
148
|
+
* @see https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/configure-tunnels/origin-configuration/#ingress-rules
|
|
149
|
+
*/
|
|
150
|
+
export interface IngressRule {
|
|
151
|
+
/**
|
|
152
|
+
* Hostname to match for this rule
|
|
153
|
+
* Use service: "http_status:404" as catch-all
|
|
154
|
+
*/
|
|
155
|
+
hostname?: string;
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Service to route to (e.g., "http://localhost:8000" or "http_status:404")
|
|
159
|
+
*
|
|
160
|
+
* @see https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/configure-tunnels/origin-configuration/#supported-protocols
|
|
161
|
+
*/
|
|
162
|
+
service: string;
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Path to match for this rule
|
|
166
|
+
*/
|
|
167
|
+
path?: string;
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Origin request configuration for this specific rule
|
|
171
|
+
*/
|
|
172
|
+
originRequest?: OriginRequestConfig;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Origin request configuration
|
|
177
|
+
*
|
|
178
|
+
* @see https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/configure-tunnels/origin-configuration/#origin-request-parameters
|
|
179
|
+
*/
|
|
180
|
+
export interface OriginRequestConfig {
|
|
181
|
+
/**
|
|
182
|
+
* Timeout for origin server to respond to a request
|
|
183
|
+
*/
|
|
184
|
+
connectTimeout?: number;
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Timeout for closing the connection to the origin server
|
|
188
|
+
*/
|
|
189
|
+
tlsTimeout?: number;
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Timeout for TCP connections to the origin server
|
|
193
|
+
*/
|
|
194
|
+
tcpKeepAlive?: number;
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Disable keep-alive connections
|
|
198
|
+
*/
|
|
199
|
+
noHappyEyeballs?: boolean;
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Keep connections open after a request
|
|
203
|
+
*/
|
|
204
|
+
keepAliveConnections?: number;
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Timeout for keep-alive connections
|
|
208
|
+
*/
|
|
209
|
+
keepAliveTimeout?: number;
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* HTTP/2 origin support
|
|
213
|
+
*/
|
|
214
|
+
http2Origin?: boolean;
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Headers to add to origin requests
|
|
218
|
+
*/
|
|
219
|
+
httpHostHeader?: string;
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* CA pool for origin TLS verification
|
|
223
|
+
*/
|
|
224
|
+
caPool?: string;
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Disable TLS verification
|
|
228
|
+
*/
|
|
229
|
+
noTLSVerify?: boolean;
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Disable chunked encoding
|
|
233
|
+
*/
|
|
234
|
+
disableChunkedEncoding?: boolean;
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Rewrite the Host header
|
|
238
|
+
*/
|
|
239
|
+
bastionMode?: boolean;
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Proxy protocol version
|
|
243
|
+
*/
|
|
244
|
+
proxyProtocol?: "off" | "v1" | "v2";
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Proxy outgoing connections through a specified address
|
|
248
|
+
*/
|
|
249
|
+
proxyAddress?: string;
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Port to use for proxy connections
|
|
253
|
+
*/
|
|
254
|
+
proxyPort?: number;
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Type of proxy to use
|
|
258
|
+
*/
|
|
259
|
+
proxyType?: string;
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Enable TCP keep-alive for connection pooling
|
|
263
|
+
*/
|
|
264
|
+
tcpKeepAliveInterval?: number;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
export function isTunnel(resource: Resource): resource is Tunnel {
|
|
268
|
+
return resource[ResourceKind] === "cloudflare::Tunnel";
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Output returned after Tunnel creation/update
|
|
273
|
+
*/
|
|
274
|
+
export interface Tunnel
|
|
275
|
+
extends Resource<"cloudflare::Tunnel">,
|
|
276
|
+
Omit<TunnelProps, "delete" | "tunnelSecret"> {
|
|
277
|
+
/**
|
|
278
|
+
* The ID of the tunnel
|
|
279
|
+
*/
|
|
280
|
+
tunnelId: string;
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* The account ID that owns the tunnel
|
|
284
|
+
*/
|
|
285
|
+
accountTag: string;
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Time at which the tunnel was created
|
|
289
|
+
*/
|
|
290
|
+
createdAt: string;
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Time at which the tunnel was deleted (null if active)
|
|
294
|
+
*/
|
|
295
|
+
deletedAt: string | null;
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* Credentials for connecting to the tunnel
|
|
299
|
+
*/
|
|
300
|
+
credentials: {
|
|
301
|
+
accountTag: string;
|
|
302
|
+
tunnelId: string;
|
|
303
|
+
tunnelName: string;
|
|
304
|
+
tunnelSecret: Secret<string>;
|
|
305
|
+
};
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* Token for running the tunnel
|
|
309
|
+
*
|
|
310
|
+
* @remarks
|
|
311
|
+
* Use this token with `cloudflared tunnel run --token <token>` to start the tunnel
|
|
312
|
+
*
|
|
313
|
+
* @see https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/get-started/create-remote-tunnel-api/#install-and-run-the-tunnel
|
|
314
|
+
*/
|
|
315
|
+
token: Secret<string>;
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* DNS records automatically created for hostnames in ingress rules
|
|
319
|
+
* Maps hostname to DNS record ID
|
|
320
|
+
* @internal
|
|
321
|
+
*/
|
|
322
|
+
dnsRecords?: Record<string, string>;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* Creates and manages a Cloudflare Tunnel, which provides a secure connection between
|
|
327
|
+
* your origin server and Cloudflare's edge. This resource handles the tunnel lifecycle
|
|
328
|
+
* (create, update, delete) and configuration.
|
|
329
|
+
*
|
|
330
|
+
* @remarks
|
|
331
|
+
* After creating a tunnel, use the returned credentials and token to run the
|
|
332
|
+
* cloudflared connector on your origin server.
|
|
333
|
+
*
|
|
334
|
+
* When hostnames are specified in ingress rules, this resource automatically creates
|
|
335
|
+
* the required DNS CNAME records pointing to <tunnel-id>.cfargotunnel.com, following
|
|
336
|
+
* the Cloudflare API documentation for connecting applications (step 3a).
|
|
337
|
+
*
|
|
338
|
+
* @see https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/
|
|
339
|
+
* @see https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/get-started/create-remote-tunnel-api/
|
|
340
|
+
* @see https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/configure-tunnels/
|
|
341
|
+
*
|
|
342
|
+
* @example
|
|
343
|
+
* // Create a basic tunnel
|
|
344
|
+
* const tunnel = await Tunnel("my-app", {
|
|
345
|
+
* name: "my-app-tunnel"
|
|
346
|
+
* });
|
|
347
|
+
*
|
|
348
|
+
* // Run cloudflared with:
|
|
349
|
+
* // cloudflared tunnel run --token <tunnel.token.unencrypted>
|
|
350
|
+
*
|
|
351
|
+
* @example
|
|
352
|
+
* // Create a tunnel with ingress configuration for a web app
|
|
353
|
+
* // DNS records are automatically created for each hostname
|
|
354
|
+
* const webTunnel = await Tunnel("web-app", {
|
|
355
|
+
* name: "web-app-tunnel",
|
|
356
|
+
* ingress: [
|
|
357
|
+
* {
|
|
358
|
+
* hostname: "app.example.com",
|
|
359
|
+
* service: "http://localhost:3000"
|
|
360
|
+
* },
|
|
361
|
+
* {
|
|
362
|
+
* service: "http_status:404" // catch-all rule
|
|
363
|
+
* }
|
|
364
|
+
* ]
|
|
365
|
+
* });
|
|
366
|
+
* // A CNAME record for app.example.com → webTunnel.tunnelId.cfargotunnel.com
|
|
367
|
+
* // is automatically created in the appropriate zone
|
|
368
|
+
*
|
|
369
|
+
* @example
|
|
370
|
+
* // Create a tunnel with multiple services and origin configuration
|
|
371
|
+
* const apiTunnel = await Tunnel("api", {
|
|
372
|
+
* name: "api-tunnel",
|
|
373
|
+
* ingress: [
|
|
374
|
+
* {
|
|
375
|
+
* hostname: "api.example.com",
|
|
376
|
+
* path: "/v1/*",
|
|
377
|
+
* service: "http://localhost:8080",
|
|
378
|
+
* originRequest: {
|
|
379
|
+
* httpHostHeader: "api.internal",
|
|
380
|
+
* connectTimeout: 30
|
|
381
|
+
* }
|
|
382
|
+
* },
|
|
383
|
+
* {
|
|
384
|
+
* hostname: "api.example.com",
|
|
385
|
+
* path: "/v2/*",
|
|
386
|
+
* service: "http://localhost:8081"
|
|
387
|
+
* },
|
|
388
|
+
* {
|
|
389
|
+
* service: "http_status:404"
|
|
390
|
+
* }
|
|
391
|
+
* ]
|
|
392
|
+
* });
|
|
393
|
+
*
|
|
394
|
+
* @example
|
|
395
|
+
* // Create a tunnel for private network access with WARP
|
|
396
|
+
* const privateTunnel = await Tunnel("private-network", {
|
|
397
|
+
* name: "private-network-tunnel",
|
|
398
|
+
* warpRouting: {
|
|
399
|
+
* enabled: true
|
|
400
|
+
* }
|
|
401
|
+
* });
|
|
402
|
+
*
|
|
403
|
+
* @example
|
|
404
|
+
* // Create a tunnel with origin request configuration
|
|
405
|
+
* const secureTunnel = await Tunnel("secure", {
|
|
406
|
+
* name: "secure-tunnel",
|
|
407
|
+
* originRequest: {
|
|
408
|
+
* noTLSVerify: false,
|
|
409
|
+
* connectTimeout: 30,
|
|
410
|
+
* httpHostHeader: "internal.service"
|
|
411
|
+
* },
|
|
412
|
+
* ingress: [
|
|
413
|
+
* {
|
|
414
|
+
* hostname: "secure.example.com",
|
|
415
|
+
* service: "https://localhost:8443"
|
|
416
|
+
* },
|
|
417
|
+
* {
|
|
418
|
+
* service: "http_status:404"
|
|
419
|
+
* }
|
|
420
|
+
* ]
|
|
421
|
+
* });
|
|
422
|
+
*
|
|
423
|
+
* @example
|
|
424
|
+
* // Adopt an existing tunnel if it already exists
|
|
425
|
+
* const existingTunnel = await Tunnel("existing", {
|
|
426
|
+
* name: "existing-tunnel",
|
|
427
|
+
* adopt: true,
|
|
428
|
+
* ingress: [
|
|
429
|
+
* {
|
|
430
|
+
* hostname: "updated.example.com",
|
|
431
|
+
* service: "http://localhost:5000"
|
|
432
|
+
* },
|
|
433
|
+
* {
|
|
434
|
+
* service: "http_status:404"
|
|
435
|
+
* }
|
|
436
|
+
* ]
|
|
437
|
+
* });
|
|
438
|
+
*
|
|
439
|
+
* @example
|
|
440
|
+
* // Tunnel with automatic DNS record creation
|
|
441
|
+
* // The Tunnel resource automatically creates DNS records for hostnames in ingress rules
|
|
442
|
+
* const appTunnel = await Tunnel("app", {
|
|
443
|
+
* name: "app-tunnel",
|
|
444
|
+
* ingress: [
|
|
445
|
+
* {
|
|
446
|
+
* hostname: "app.example.com",
|
|
447
|
+
* service: "http://localhost:3000"
|
|
448
|
+
* },
|
|
449
|
+
* {
|
|
450
|
+
* hostname: "api.example.com",
|
|
451
|
+
* service: "http://localhost:8080"
|
|
452
|
+
* },
|
|
453
|
+
* {
|
|
454
|
+
* service: "http_status:404"
|
|
455
|
+
* }
|
|
456
|
+
* ]
|
|
457
|
+
* });
|
|
458
|
+
* // DNS CNAME records are automatically created:
|
|
459
|
+
* // - app.example.com → {tunnelId}.cfargotunnel.com
|
|
460
|
+
* // - api.example.com → {tunnelId}.cfargotunnel.com
|
|
461
|
+
*
|
|
462
|
+
* // Run the tunnel:
|
|
463
|
+
* // cloudflared tunnel run --token <appTunnel.token.unencrypted>
|
|
464
|
+
*
|
|
465
|
+
* @example
|
|
466
|
+
* // For advanced DNS control, you can still manually manage DNS records
|
|
467
|
+
* // by omitting hostnames from ingress rules:
|
|
468
|
+
* const tunnel = await Tunnel("manual-dns", {
|
|
469
|
+
* name: "manual-dns-tunnel",
|
|
470
|
+
* ingress: [
|
|
471
|
+
* {
|
|
472
|
+
* service: "http://localhost:3000"
|
|
473
|
+
* },
|
|
474
|
+
* {
|
|
475
|
+
* service: "http_status:404"
|
|
476
|
+
* }
|
|
477
|
+
* ]
|
|
478
|
+
* });
|
|
479
|
+
* // Then create DNS records separately with custom configuration
|
|
480
|
+
*/
|
|
481
|
+
export const Tunnel = Resource(
|
|
482
|
+
"cloudflare::Tunnel",
|
|
483
|
+
async function (
|
|
484
|
+
this: Context<Tunnel>,
|
|
485
|
+
id: string,
|
|
486
|
+
props: TunnelProps,
|
|
487
|
+
): Promise<Tunnel> {
|
|
488
|
+
// Create Cloudflare API client with automatic account discovery
|
|
489
|
+
const api = await createCloudflareApi(props);
|
|
490
|
+
|
|
491
|
+
const name = props.name ?? id;
|
|
492
|
+
|
|
493
|
+
if (this.phase === "delete") {
|
|
494
|
+
// For delete operations, check if the tunnel ID exists in the output
|
|
495
|
+
const tunnelId = this.output?.tunnelId;
|
|
496
|
+
if (tunnelId && props.delete !== false) {
|
|
497
|
+
await deleteTunnel(api, tunnelId);
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
// Return destroyed state
|
|
501
|
+
return this.destroy();
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
// For create or update operations
|
|
505
|
+
let tunnelData: CloudflareTunnel;
|
|
506
|
+
|
|
507
|
+
if (this.phase === "update" && this.output?.tunnelId) {
|
|
508
|
+
// Get existing tunnel data
|
|
509
|
+
tunnelData = await getTunnel(api, this.output.tunnelId);
|
|
510
|
+
|
|
511
|
+
// Check if name is being changed - tunnel names are immutable
|
|
512
|
+
if (props.name && props.name !== tunnelData.name) {
|
|
513
|
+
this.replace(true);
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
// Update configuration if provided
|
|
517
|
+
if (
|
|
518
|
+
(props.ingress || props.warpRouting || props.originRequest) &&
|
|
519
|
+
props.configSrc !== "local"
|
|
520
|
+
) {
|
|
521
|
+
const config: TunnelConfig = {
|
|
522
|
+
ingress: props.ingress,
|
|
523
|
+
warpRouting: props.warpRouting,
|
|
524
|
+
originRequest: props.originRequest,
|
|
525
|
+
};
|
|
526
|
+
await updateTunnelConfiguration(api, this.output.tunnelId, config);
|
|
527
|
+
}
|
|
528
|
+
} else {
|
|
529
|
+
// Create new tunnel
|
|
530
|
+
try {
|
|
531
|
+
tunnelData = await createTunnel(api, {
|
|
532
|
+
name,
|
|
533
|
+
configSrc: props.configSrc,
|
|
534
|
+
tunnelSecret: props.tunnelSecret,
|
|
535
|
+
metadata: props.metadata,
|
|
536
|
+
});
|
|
537
|
+
|
|
538
|
+
// Configure tunnel if config is provided
|
|
539
|
+
if (
|
|
540
|
+
(props.ingress || props.warpRouting || props.originRequest) &&
|
|
541
|
+
props.configSrc !== "local"
|
|
542
|
+
) {
|
|
543
|
+
await updateTunnelConfiguration(api, tunnelData.id, {
|
|
544
|
+
ingress: props.ingress,
|
|
545
|
+
warpRouting: props.warpRouting,
|
|
546
|
+
originRequest: props.originRequest,
|
|
547
|
+
});
|
|
548
|
+
}
|
|
549
|
+
} catch (error) {
|
|
550
|
+
// Check if this is a "tunnel already exists" error and adopt is enabled
|
|
551
|
+
if (
|
|
552
|
+
props.adopt &&
|
|
553
|
+
error instanceof Error &&
|
|
554
|
+
(error.message.includes("already have a tunnel with this name") ||
|
|
555
|
+
error.message.includes("already exists"))
|
|
556
|
+
) {
|
|
557
|
+
console.log(error);
|
|
558
|
+
logger.log(`Tunnel '${name}' already exists, adopting it`);
|
|
559
|
+
|
|
560
|
+
// Find the existing tunnel by name
|
|
561
|
+
const existingTunnel = await findTunnelByName(api, name);
|
|
562
|
+
|
|
563
|
+
if (!existingTunnel) {
|
|
564
|
+
throw new Error(
|
|
565
|
+
`Failed to find existing tunnel '${name}' for adoption`,
|
|
566
|
+
);
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
tunnelData = existingTunnel;
|
|
570
|
+
|
|
571
|
+
// Update configuration if provided
|
|
572
|
+
if (
|
|
573
|
+
(props.ingress || props.warpRouting || props.originRequest) &&
|
|
574
|
+
props.configSrc !== "local"
|
|
575
|
+
) {
|
|
576
|
+
const config: TunnelConfig = {
|
|
577
|
+
ingress: props.ingress,
|
|
578
|
+
warpRouting: props.warpRouting,
|
|
579
|
+
originRequest: props.originRequest,
|
|
580
|
+
};
|
|
581
|
+
await updateTunnelConfiguration(api, existingTunnel.id, config);
|
|
582
|
+
}
|
|
583
|
+
} else {
|
|
584
|
+
// Re-throw the error if adopt is false or it's not an "already exists" error
|
|
585
|
+
throw error;
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
// Handle DNS records for ingress hostnames
|
|
591
|
+
let dnsRecords = this.output?.dnsRecords || {};
|
|
592
|
+
|
|
593
|
+
// Extract hostnames from ingress rules
|
|
594
|
+
const hostnames = new Set<string>();
|
|
595
|
+
if (props.ingress) {
|
|
596
|
+
for (const rule of props.ingress) {
|
|
597
|
+
if (rule.hostname && !rule.hostname.includes("*")) {
|
|
598
|
+
// Skip wildcard hostnames as they need special handling
|
|
599
|
+
hostnames.add(rule.hostname);
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
// Create or update DNS records for each hostname
|
|
605
|
+
if (hostnames.size > 0) {
|
|
606
|
+
// Group hostnames by zone
|
|
607
|
+
const hostnamesByZone = new Map<
|
|
608
|
+
string,
|
|
609
|
+
{ zoneId: string; hostnames: string[] }
|
|
610
|
+
>();
|
|
611
|
+
|
|
612
|
+
for (const hostname of hostnames) {
|
|
613
|
+
const { zoneId } = await findZoneForHostname(api, hostname);
|
|
614
|
+
if (!hostnamesByZone.has(zoneId)) {
|
|
615
|
+
hostnamesByZone.set(zoneId, { zoneId, hostnames: [] });
|
|
616
|
+
}
|
|
617
|
+
hostnamesByZone.get(zoneId)!.hostnames.push(hostname);
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
// Create DNS records for each zone in parallel
|
|
621
|
+
const dnsPromises = Array.from(hostnamesByZone.entries()).map(
|
|
622
|
+
async ([zoneId, { hostnames: zoneHostnames }]) => {
|
|
623
|
+
const dnsResourceId = `${id}-dns-${zoneId}`;
|
|
624
|
+
const dnsResource = await DnsRecords(dnsResourceId, {
|
|
625
|
+
zoneId,
|
|
626
|
+
records: zoneHostnames.map((hostname) => ({
|
|
627
|
+
name: hostname,
|
|
628
|
+
type: "CNAME" as const,
|
|
629
|
+
content: `${tunnelData.id}.cfargotunnel.com`,
|
|
630
|
+
proxied: true,
|
|
631
|
+
comment: `Cloudflare Tunnel: ${tunnelData.name}`,
|
|
632
|
+
})),
|
|
633
|
+
});
|
|
634
|
+
|
|
635
|
+
// Return records for mapping storage
|
|
636
|
+
return dnsResource.records;
|
|
637
|
+
},
|
|
638
|
+
);
|
|
639
|
+
|
|
640
|
+
const allDnsRecords = await Promise.all(dnsPromises);
|
|
641
|
+
|
|
642
|
+
// Store DNS record mappings
|
|
643
|
+
for (const records of allDnsRecords) {
|
|
644
|
+
for (const record of records) {
|
|
645
|
+
dnsRecords[record.name] = record.id;
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
// Clean up DNS records that are no longer needed
|
|
651
|
+
const currentHostnames = Array.from(hostnames);
|
|
652
|
+
for (const hostname of Object.keys(dnsRecords)) {
|
|
653
|
+
if (!currentHostnames.includes(hostname)) {
|
|
654
|
+
delete dnsRecords[hostname];
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
// Transform API response to our interface
|
|
659
|
+
return this({
|
|
660
|
+
tunnelId: tunnelData.id,
|
|
661
|
+
accountTag: tunnelData.account_tag,
|
|
662
|
+
name: tunnelData.name,
|
|
663
|
+
createdAt: tunnelData.created_at,
|
|
664
|
+
deletedAt: tunnelData.deleted_at,
|
|
665
|
+
credentials: tunnelData.credentials_file
|
|
666
|
+
? {
|
|
667
|
+
accountTag: tunnelData.credentials_file.AccountTag,
|
|
668
|
+
tunnelId: tunnelData.credentials_file.TunnelID,
|
|
669
|
+
tunnelName: tunnelData.credentials_file.TunnelName,
|
|
670
|
+
tunnelSecret: alchemy.secret(
|
|
671
|
+
tunnelData.credentials_file.TunnelSecret,
|
|
672
|
+
),
|
|
673
|
+
}
|
|
674
|
+
: this.output?.credentials || {
|
|
675
|
+
accountTag: tunnelData.account_tag,
|
|
676
|
+
tunnelId: tunnelData.id,
|
|
677
|
+
tunnelName: tunnelData.name,
|
|
678
|
+
tunnelSecret: alchemy.secret(""),
|
|
679
|
+
},
|
|
680
|
+
token: tunnelData.token
|
|
681
|
+
? alchemy.secret(tunnelData.token)
|
|
682
|
+
: this.output?.token || alchemy.secret(""),
|
|
683
|
+
metadata: props.metadata,
|
|
684
|
+
ingress: props.ingress,
|
|
685
|
+
warpRouting: props.warpRouting,
|
|
686
|
+
originRequest: props.originRequest,
|
|
687
|
+
configSrc: props.configSrc,
|
|
688
|
+
dnsRecords: Object.keys(dnsRecords).length > 0 ? dnsRecords : undefined,
|
|
689
|
+
});
|
|
690
|
+
},
|
|
691
|
+
);
|
|
692
|
+
|
|
693
|
+
/**
|
|
694
|
+
* Get tunnel details
|
|
695
|
+
* @internal
|
|
696
|
+
*/
|
|
697
|
+
export async function getTunnel(
|
|
698
|
+
api: CloudflareApi,
|
|
699
|
+
tunnelId: string,
|
|
700
|
+
): Promise<CloudflareTunnel> {
|
|
701
|
+
const response = await api.get(
|
|
702
|
+
`/accounts/${api.accountId}/cfd_tunnel/${tunnelId}`,
|
|
703
|
+
);
|
|
704
|
+
|
|
705
|
+
if (!response.ok) {
|
|
706
|
+
await handleApiError(response, "get", "tunnel", tunnelId);
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
const data =
|
|
710
|
+
(await response.json()) as CloudflareApiResponse<CloudflareTunnel>;
|
|
711
|
+
return data.result;
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
/**
|
|
715
|
+
* Get tunnel configuration
|
|
716
|
+
* @internal
|
|
717
|
+
*/
|
|
718
|
+
export async function getTunnelConfiguration(
|
|
719
|
+
api: CloudflareApi,
|
|
720
|
+
tunnelId: string,
|
|
721
|
+
): Promise<TunnelConfig> {
|
|
722
|
+
const response = await api.get(
|
|
723
|
+
`/accounts/${api.accountId}/cfd_tunnel/${tunnelId}/configurations`,
|
|
724
|
+
);
|
|
725
|
+
|
|
726
|
+
if (!response.ok) {
|
|
727
|
+
await handleApiError(response, "get configuration", "tunnel", tunnelId);
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
const data = (await response.json()) as CloudflareApiResponse<{
|
|
731
|
+
config: TunnelConfig;
|
|
732
|
+
}>;
|
|
733
|
+
return data.result.config;
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
/**
|
|
737
|
+
* Delete a tunnel
|
|
738
|
+
* @internal
|
|
739
|
+
*/
|
|
740
|
+
async function deleteTunnel(
|
|
741
|
+
api: CloudflareApi,
|
|
742
|
+
tunnelId: string,
|
|
743
|
+
): Promise<void> {
|
|
744
|
+
const response = await api.delete(
|
|
745
|
+
`/accounts/${api.accountId}/cfd_tunnel/${tunnelId}`,
|
|
746
|
+
);
|
|
747
|
+
|
|
748
|
+
if (!response.ok && response.status !== 404) {
|
|
749
|
+
await handleApiError(response, "delete", "tunnel", tunnelId);
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
/**
|
|
754
|
+
* Create a new tunnel
|
|
755
|
+
* @internal
|
|
756
|
+
*/
|
|
757
|
+
async function createTunnel(
|
|
758
|
+
api: CloudflareApi,
|
|
759
|
+
props: {
|
|
760
|
+
name: string;
|
|
761
|
+
configSrc?: "cloudflare" | "local";
|
|
762
|
+
tunnelSecret?: Secret<string>;
|
|
763
|
+
metadata?: Record<string, any>;
|
|
764
|
+
},
|
|
765
|
+
): Promise<CloudflareTunnel> {
|
|
766
|
+
const payload: Record<string, any> = {
|
|
767
|
+
name: props.name,
|
|
768
|
+
config_src: props.configSrc || "cloudflare",
|
|
769
|
+
};
|
|
770
|
+
|
|
771
|
+
if (props.tunnelSecret) {
|
|
772
|
+
payload.tunnel_secret = props.tunnelSecret.unencrypted;
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
if (props.metadata) {
|
|
776
|
+
payload.metadata = props.metadata;
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
const response = await api.post(
|
|
780
|
+
`/accounts/${api.accountId}/cfd_tunnel`,
|
|
781
|
+
payload,
|
|
782
|
+
);
|
|
783
|
+
|
|
784
|
+
if (!response.ok) {
|
|
785
|
+
await handleApiError(response, "create", "tunnel", props.name);
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
const data =
|
|
789
|
+
(await response.json()) as CloudflareApiResponse<CloudflareTunnel>;
|
|
790
|
+
return data.result;
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
/**
|
|
794
|
+
* Update tunnel configuration
|
|
795
|
+
* @internal
|
|
796
|
+
*/
|
|
797
|
+
async function updateTunnelConfiguration(
|
|
798
|
+
api: CloudflareApi,
|
|
799
|
+
tunnelId: string,
|
|
800
|
+
config: TunnelConfig,
|
|
801
|
+
): Promise<void> {
|
|
802
|
+
const response = await api.put(
|
|
803
|
+
`/accounts/${api.accountId}/cfd_tunnel/${tunnelId}/configurations`,
|
|
804
|
+
{ config },
|
|
805
|
+
);
|
|
806
|
+
|
|
807
|
+
if (!response.ok) {
|
|
808
|
+
await handleApiError(response, "update configuration", "tunnel", tunnelId);
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
/**
|
|
813
|
+
* List all tunnels with pagination support
|
|
814
|
+
* @internal
|
|
815
|
+
*/
|
|
816
|
+
export async function listTunnels(
|
|
817
|
+
api: CloudflareApi,
|
|
818
|
+
options?: {
|
|
819
|
+
/** Whether to include deleted tunnels */
|
|
820
|
+
includeDeleted?: boolean;
|
|
821
|
+
/** Maximum number of tunnels to return */
|
|
822
|
+
limit?: number;
|
|
823
|
+
},
|
|
824
|
+
): Promise<CloudflareTunnel[]> {
|
|
825
|
+
const tunnels: CloudflareTunnel[] = [];
|
|
826
|
+
let page = 1;
|
|
827
|
+
const perPage = 100; // Maximum allowed by API
|
|
828
|
+
let hasMorePages = true;
|
|
829
|
+
const limit = options?.limit;
|
|
830
|
+
const includeDeleted = options?.includeDeleted ?? false;
|
|
831
|
+
|
|
832
|
+
while (hasMorePages) {
|
|
833
|
+
const params = new URLSearchParams({
|
|
834
|
+
page: page.toString(),
|
|
835
|
+
per_page: perPage.toString(),
|
|
836
|
+
});
|
|
837
|
+
|
|
838
|
+
if (!includeDeleted) {
|
|
839
|
+
params.append("is_deleted", "false");
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
const response = await api.get(
|
|
843
|
+
`/accounts/${api.accountId}/cfd_tunnel?${params.toString()}`,
|
|
844
|
+
);
|
|
845
|
+
|
|
846
|
+
if (!response.ok) {
|
|
847
|
+
await handleApiError(response, "list", "tunnel", "all");
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
const data =
|
|
851
|
+
(await response.json()) as CloudflareApiListResponse<CloudflareTunnel>;
|
|
852
|
+
|
|
853
|
+
tunnels.push(...data.result);
|
|
854
|
+
const resultInfo = data.result_info;
|
|
855
|
+
|
|
856
|
+
// Check if we've reached the limit
|
|
857
|
+
if (limit && tunnels.length >= limit) {
|
|
858
|
+
return tunnels.slice(0, limit);
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
// Check if we've seen all pages
|
|
862
|
+
hasMorePages =
|
|
863
|
+
resultInfo.page * resultInfo.per_page < resultInfo.total_count;
|
|
864
|
+
page++;
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
return tunnels;
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
/**
|
|
871
|
+
* Find a tunnel by name with pagination support
|
|
872
|
+
* @internal
|
|
873
|
+
*/
|
|
874
|
+
export async function findTunnelByName(
|
|
875
|
+
api: CloudflareApi,
|
|
876
|
+
name: string,
|
|
877
|
+
): Promise<CloudflareTunnel | null> {
|
|
878
|
+
let page = 1;
|
|
879
|
+
const perPage = 100; // Maximum allowed by API
|
|
880
|
+
let hasMorePages = true;
|
|
881
|
+
|
|
882
|
+
while (hasMorePages) {
|
|
883
|
+
const response = await api.get(
|
|
884
|
+
`/accounts/${api.accountId}/cfd_tunnel?page=${page}&per_page=${perPage}&is_deleted=false`,
|
|
885
|
+
);
|
|
886
|
+
|
|
887
|
+
if (!response.ok) {
|
|
888
|
+
await handleApiError(response, "list", "tunnel", "all");
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
const data =
|
|
892
|
+
(await response.json()) as CloudflareApiListResponse<CloudflareTunnel>;
|
|
893
|
+
|
|
894
|
+
const tunnels = data.result;
|
|
895
|
+
const resultInfo = data.result_info;
|
|
896
|
+
|
|
897
|
+
// Look for a tunnel with matching name
|
|
898
|
+
const match = tunnels.find((tunnel) => tunnel.name === name);
|
|
899
|
+
if (match) {
|
|
900
|
+
return match;
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
// Check if we've seen all pages
|
|
904
|
+
hasMorePages =
|
|
905
|
+
resultInfo.page * resultInfo.per_page < resultInfo.total_count;
|
|
906
|
+
page++;
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
// No matching tunnel found
|
|
910
|
+
return null;
|
|
911
|
+
}
|