@muhammedaksam/easiarr 1.3.5 → 1.4.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 (94) hide show
  1. package/README.md +8 -5
  2. package/dist/index.js +29837 -0
  3. package/package.json +18 -16
  4. package/src/VersionInfo.ts +0 -12
  5. package/src/api/arr-api.ts +0 -508
  6. package/src/api/auto-setup-types.ts +0 -64
  7. package/src/api/bazarr-api.ts +0 -356
  8. package/src/api/cloudflare-api.ts +0 -577
  9. package/src/api/custom-format-api.ts +0 -212
  10. package/src/api/grafana-api.ts +0 -314
  11. package/src/api/heimdall-api.ts +0 -209
  12. package/src/api/homarr-api.ts +0 -296
  13. package/src/api/huntarr-api.ts +0 -622
  14. package/src/api/index.ts +0 -1
  15. package/src/api/jellyfin-api.ts +0 -473
  16. package/src/api/jellyseerr-api.ts +0 -660
  17. package/src/api/maintainerr-api.ts +0 -731
  18. package/src/api/naming-config.ts +0 -93
  19. package/src/api/overseerr-api.ts +0 -513
  20. package/src/api/plex-api.ts +0 -358
  21. package/src/api/portainer-api.ts +0 -467
  22. package/src/api/profilarr-api.ts +0 -380
  23. package/src/api/prowlarr-api.ts +0 -859
  24. package/src/api/qbittorrent-api.ts +0 -339
  25. package/src/api/quality-profile-api.ts +0 -269
  26. package/src/api/tautulli-api.ts +0 -277
  27. package/src/api/uptime-kuma-api.ts +0 -342
  28. package/src/apps/categories.ts +0 -14
  29. package/src/apps/index.ts +0 -2
  30. package/src/apps/registry.ts +0 -1255
  31. package/src/compose/caddy-config.ts +0 -129
  32. package/src/compose/generator.ts +0 -262
  33. package/src/compose/index.ts +0 -3
  34. package/src/compose/templates.ts +0 -78
  35. package/src/compose/traefik-config.ts +0 -185
  36. package/src/config/bookmarks-generator.ts +0 -149
  37. package/src/config/defaults.ts +0 -37
  38. package/src/config/homepage-config.ts +0 -376
  39. package/src/config/index.ts +0 -3
  40. package/src/config/manager.ts +0 -149
  41. package/src/config/recyclarr-config.ts +0 -179
  42. package/src/config/schema.ts +0 -310
  43. package/src/config/slskd-config.ts +0 -85
  44. package/src/config/soularr-config.ts +0 -105
  45. package/src/config/trash-quality-definitions.ts +0 -213
  46. package/src/data/lidarr-custom-formats.ts +0 -127
  47. package/src/data/trash-profiles.ts +0 -326
  48. package/src/docker/client.ts +0 -291
  49. package/src/docker/index.ts +0 -1
  50. package/src/index.ts +0 -77
  51. package/src/structure/manager.ts +0 -128
  52. package/src/ui/App.ts +0 -109
  53. package/src/ui/components/ApplicationSelector.ts +0 -272
  54. package/src/ui/components/FileEditor.ts +0 -91
  55. package/src/ui/components/FooterHint.ts +0 -119
  56. package/src/ui/components/PageLayout.ts +0 -152
  57. package/src/ui/components/UpdateNotification.ts +0 -101
  58. package/src/ui/index.ts +0 -1
  59. package/src/ui/screens/AdvancedSettings.ts +0 -181
  60. package/src/ui/screens/ApiKeyViewer.ts +0 -500
  61. package/src/ui/screens/AppConfigurator.ts +0 -731
  62. package/src/ui/screens/AppManager.ts +0 -276
  63. package/src/ui/screens/CloudflaredSetup.ts +0 -974
  64. package/src/ui/screens/ContainerControl.ts +0 -579
  65. package/src/ui/screens/FullAutoSetup.ts +0 -1719
  66. package/src/ui/screens/HomepageSetup.ts +0 -306
  67. package/src/ui/screens/JellyfinSetup.ts +0 -471
  68. package/src/ui/screens/JellyseerrSetup.ts +0 -612
  69. package/src/ui/screens/LogsViewer.ts +0 -468
  70. package/src/ui/screens/MainMenu.ts +0 -288
  71. package/src/ui/screens/MonitorDashboard.ts +0 -866
  72. package/src/ui/screens/ProwlarrSetup.ts +0 -623
  73. package/src/ui/screens/QBittorrentSetup.ts +0 -268
  74. package/src/ui/screens/QuickSetup.ts +0 -1161
  75. package/src/ui/screens/RecyclarrSetup.ts +0 -418
  76. package/src/ui/screens/SecretsEditor.ts +0 -214
  77. package/src/ui/screens/SettingsScreen.ts +0 -605
  78. package/src/ui/screens/TRaSHProfileSetup.ts +0 -395
  79. package/src/ui/screens/index.ts +0 -4
  80. package/src/util/arch.ts +0 -78
  81. package/src/utils/browser.ts +0 -26
  82. package/src/utils/categories.ts +0 -59
  83. package/src/utils/debug.ts +0 -91
  84. package/src/utils/env.ts +0 -137
  85. package/src/utils/logs.ts +0 -118
  86. package/src/utils/migrations/1765626338_rename_env_variables.ts +0 -101
  87. package/src/utils/migrations/1765707135_rename_easiarr_status.ts +0 -90
  88. package/src/utils/migrations/1765732722_remove_cloudflare_dns_api_token.ts +0 -44
  89. package/src/utils/migrations/1769515650_migrate_to_xdg.ts +0 -107
  90. package/src/utils/migrations.ts +0 -164
  91. package/src/utils/password.ts +0 -42
  92. package/src/utils/unraid.ts +0 -101
  93. package/src/utils/update-checker.ts +0 -210
  94. package/src/utils/url-utils.ts +0 -38
@@ -1,577 +0,0 @@
1
- /**
2
- * Cloudflare API client for tunnel and DNS management
3
- */
4
-
5
- import { debugLog } from "../utils/debug"
6
-
7
- const CLOUDFLARE_API_BASE = "https://api.cloudflare.com/client/v4"
8
-
9
- interface CloudflareResponse<T> {
10
- success: boolean
11
- errors: Array<{ code: number; message: string }>
12
- messages: string[]
13
- result: T
14
- }
15
-
16
- interface Zone {
17
- id: string
18
- name: string
19
- status: string
20
- }
21
-
22
- interface Tunnel {
23
- id: string
24
- name: string
25
- status: string
26
- created_at: string
27
- connections: Array<{
28
- id: string
29
- is_pending_reconnect: boolean
30
- }>
31
- }
32
-
33
- interface TunnelCredentials {
34
- account_tag: string
35
- tunnel_secret: string
36
- tunnel_id: string
37
- tunnel_name: string
38
- }
39
-
40
- interface DnsRecord {
41
- id: string
42
- name: string
43
- type: string
44
- content: string
45
- proxied: boolean
46
- }
47
-
48
- export class CloudflareApi {
49
- private apiToken: string
50
- private accountId: string | null = null
51
-
52
- constructor(apiToken: string) {
53
- this.apiToken = apiToken
54
- debugLog("CloudflareAPI", "Client initialized")
55
- }
56
-
57
- private async request<T>(method: string, endpoint: string, body?: unknown): Promise<CloudflareResponse<T>> {
58
- debugLog("CloudflareAPI", `${method} ${endpoint}`)
59
- if (body) {
60
- debugLog("CloudflareAPI", `Request body: ${JSON.stringify(body)}`)
61
- }
62
-
63
- const response = await fetch(`${CLOUDFLARE_API_BASE}${endpoint}`, {
64
- method,
65
- headers: {
66
- Authorization: `Bearer ${this.apiToken}`,
67
- "Content-Type": "application/json",
68
- },
69
- body: body ? JSON.stringify(body) : undefined,
70
- })
71
-
72
- const data = (await response.json()) as CloudflareResponse<T>
73
-
74
- if (!data.success) {
75
- const errors = data.errors.map((e) => e.message).join(", ")
76
- debugLog("CloudflareAPI", `Error: ${errors}`)
77
- throw new Error(`Cloudflare API error: ${errors}`)
78
- }
79
-
80
- debugLog("CloudflareAPI", `Response success: ${data.success}`)
81
- return data
82
- }
83
-
84
- /**
85
- * Get account ID from token
86
- */
87
- async getAccountId(): Promise<string> {
88
- if (this.accountId) return this.accountId
89
-
90
- const response = await this.request<{ id: string }[]>("GET", "/accounts")
91
- if (response.result.length === 0) {
92
- throw new Error(
93
- "No Cloudflare accounts found. Your API token is missing the 'Account Settings:Read' permission. " +
94
- "Please edit your token in the Cloudflare dashboard and add: Account → Account Settings → Read"
95
- )
96
- }
97
-
98
- this.accountId = response.result[0].id
99
- return this.accountId
100
- }
101
-
102
- /**
103
- * List all zones (domains) in the account
104
- */
105
- async listZones(): Promise<Zone[]> {
106
- const response = await this.request<Zone[]>("GET", "/zones")
107
- return response.result
108
- }
109
-
110
- /**
111
- * Get zone ID by domain name
112
- */
113
- async getZoneId(domain: string): Promise<string> {
114
- const response = await this.request<Zone[]>("GET", `/zones?name=${encodeURIComponent(domain)}`)
115
- if (response.result.length === 0) {
116
- throw new Error(`Zone not found for domain: ${domain}`)
117
- }
118
- return response.result[0].id
119
- }
120
-
121
- /**
122
- * List all tunnels in the account
123
- */
124
- async listTunnels(): Promise<Tunnel[]> {
125
- const accountId = await this.getAccountId()
126
- const response = await this.request<Tunnel[]>("GET", `/accounts/${accountId}/cfd_tunnel`)
127
- return response.result
128
- }
129
-
130
- /**
131
- * Get tunnel by name
132
- */
133
- async getTunnelByName(name: string): Promise<Tunnel | null> {
134
- const tunnels = await this.listTunnels()
135
- return tunnels.find((t) => t.name === name) || null
136
- }
137
-
138
- /**
139
- * Create a new tunnel
140
- */
141
- async createTunnel(name: string): Promise<{ tunnel: Tunnel; credentials: TunnelCredentials }> {
142
- const accountId = await this.getAccountId()
143
-
144
- // Generate a random secret for the tunnel
145
- const secret = Buffer.from(crypto.getRandomValues(new Uint8Array(32))).toString("base64")
146
-
147
- const response = await this.request<Tunnel>("POST", `/accounts/${accountId}/cfd_tunnel`, {
148
- name,
149
- tunnel_secret: secret,
150
- config_src: "cloudflare", // Manage config from Cloudflare dashboard/API
151
- })
152
-
153
- return {
154
- tunnel: response.result,
155
- credentials: {
156
- account_tag: accountId,
157
- tunnel_secret: secret,
158
- tunnel_id: response.result.id,
159
- tunnel_name: name,
160
- },
161
- }
162
- }
163
-
164
- /**
165
- * Get tunnel token (for TUNNEL_TOKEN env var)
166
- * The token is base64-encoded JSON containing account_tag, tunnel_id, and tunnel_secret
167
- */
168
- async getTunnelToken(tunnelId: string): Promise<string> {
169
- const accountId = await this.getAccountId()
170
- const response = await this.request<string>("GET", `/accounts/${accountId}/cfd_tunnel/${tunnelId}/token`)
171
- return response.result
172
- }
173
-
174
- /**
175
- * Configure tunnel ingress rules
176
- * @param warpRouting Enable WARP routing for private network access (VPN)
177
- */
178
- async configureTunnel(
179
- tunnelId: string,
180
- ingress: Array<{ hostname?: string; service: string; originRequest?: Record<string, unknown> }>,
181
- warpRouting = false
182
- ): Promise<void> {
183
- const accountId = await this.getAccountId()
184
-
185
- // Ensure there's a catch-all rule at the end
186
- const hasChatchAll = ingress.some((r) => !r.hostname)
187
- if (!hasChatchAll) {
188
- ingress.push({ service: "http_status:404" })
189
- }
190
-
191
- await this.request("PUT", `/accounts/${accountId}/cfd_tunnel/${tunnelId}/configurations`, {
192
- config: {
193
- ingress,
194
- "warp-routing": { enabled: warpRouting },
195
- },
196
- })
197
- }
198
-
199
- /**
200
- * List DNS records for a zone
201
- */
202
- async listDnsRecords(zoneId: string): Promise<DnsRecord[]> {
203
- const response = await this.request<DnsRecord[]>("GET", `/zones/${zoneId}/dns_records`)
204
- return response.result
205
- }
206
-
207
- /**
208
- * Create a CNAME DNS record pointing to the tunnel
209
- */
210
- async createDnsRecord(zoneId: string, name: string, tunnelId: string, proxied = true): Promise<DnsRecord> {
211
- const target = `${tunnelId}.cfargotunnel.com`
212
-
213
- // Check if record already exists
214
- const existing = await this.request<DnsRecord[]>(
215
- "GET",
216
- `/zones/${zoneId}/dns_records?type=CNAME&name=${encodeURIComponent(name)}`
217
- )
218
-
219
- if (existing.result.length > 0) {
220
- // Update existing record
221
- const recordId = existing.result[0].id
222
- const response = await this.request<DnsRecord>("PATCH", `/zones/${zoneId}/dns_records/${recordId}`, {
223
- type: "CNAME",
224
- name,
225
- content: target,
226
- proxied,
227
- })
228
- return response.result
229
- }
230
-
231
- // Create new record
232
- const response = await this.request<DnsRecord>("POST", `/zones/${zoneId}/dns_records`, {
233
- type: "CNAME",
234
- name,
235
- content: target,
236
- proxied,
237
- })
238
-
239
- return response.result
240
- }
241
-
242
- /**
243
- * Delete a tunnel
244
- */
245
- async deleteTunnel(tunnelId: string): Promise<void> {
246
- const accountId = await this.getAccountId()
247
- await this.request("DELETE", `/accounts/${accountId}/cfd_tunnel/${tunnelId}`)
248
- }
249
-
250
- // ==================== Zero Trust Private Network API ====================
251
-
252
- /**
253
- * Add a private network route to a tunnel (for WARP VPN access)
254
- * This allows WARP clients to access the specified network through the tunnel
255
- */
256
- async addTunnelRoute(tunnelId: string, networkCidr: string, comment = "easiarr private network"): Promise<string> {
257
- const accountId = await this.getAccountId()
258
- const response = await this.request<{ id: string }>("POST", `/accounts/${accountId}/teamnet/routes`, {
259
- network: networkCidr,
260
- tunnel_id: tunnelId,
261
- comment,
262
- })
263
- return response.result.id
264
- }
265
-
266
- /**
267
- * List existing tunnel routes for the account
268
- */
269
- async listTunnelRoutes(): Promise<Array<{ id: string; network: string; tunnel_id: string; comment?: string }>> {
270
- const accountId = await this.getAccountId()
271
- const response = await this.request<Array<{ id: string; network: string; tunnel_id: string; comment?: string }>>(
272
- "GET",
273
- `/accounts/${accountId}/teamnet/routes`
274
- )
275
- return response.result
276
- }
277
-
278
- /**
279
- * Delete a tunnel route
280
- */
281
- async deleteTunnelRoute(routeId: string): Promise<void> {
282
- const accountId = await this.getAccountId()
283
- await this.request("DELETE", `/accounts/${accountId}/teamnet/routes/${routeId}`)
284
- }
285
-
286
- /**
287
- * Check if a tunnel route already exists for the given network
288
- */
289
- async getTunnelRouteForNetwork(networkCidr: string): Promise<{ id: string; tunnel_id: string } | null> {
290
- const routes = await this.listTunnelRoutes()
291
- return routes.find((r) => r.network === networkCidr) || null
292
- }
293
-
294
- // ==================== Cloudflare Access API ====================
295
-
296
- /**
297
- * Create an Access application
298
- */
299
- async createAccessApplication(
300
- domain: string,
301
- name = "easiarr",
302
- sessionDuration = "24h"
303
- ): Promise<{ id: string; name: string }> {
304
- const accountId = await this.getAccountId()
305
-
306
- // Check if app already exists
307
- const existing = await this.request<Array<{ id: string; name: string; domain: string }>>(
308
- "GET",
309
- `/accounts/${accountId}/access/apps`
310
- )
311
-
312
- const existingApp = existing.result.find((app) => app.name === name || app.domain === `*.${domain}`)
313
- if (existingApp) {
314
- return { id: existingApp.id, name: existingApp.name }
315
- }
316
-
317
- // Create new application
318
- const response = await this.request<{ id: string; name: string }>("POST", `/accounts/${accountId}/access/apps`, {
319
- name,
320
- domain: `*.${domain}`,
321
- type: "self_hosted",
322
- session_duration: sessionDuration,
323
- auto_redirect_to_identity: true,
324
- })
325
-
326
- return response.result
327
- }
328
-
329
- /**
330
- * Create an Access policy for an application
331
- */
332
- async createAccessPolicy(
333
- appId: string,
334
- allowedEmails: string[],
335
- policyName = "Allow Emails"
336
- ): Promise<{ id: string }> {
337
- const accountId = await this.getAccountId()
338
-
339
- // Check if policy already exists
340
- const existing = await this.request<Array<{ id: string; name: string }>>(
341
- "GET",
342
- `/accounts/${accountId}/access/apps/${appId}/policies`
343
- )
344
-
345
- const existingPolicy = existing.result.find((p) => p.name === policyName)
346
- if (existingPolicy) {
347
- return { id: existingPolicy.id }
348
- }
349
-
350
- // Create email-based allow policy
351
- // Each email needs to be a separate include rule
352
- // Use next available precedence (existing count + 1)
353
- const response = await this.request<{ id: string }>(
354
- "POST",
355
- `/accounts/${accountId}/access/apps/${appId}/policies`,
356
- {
357
- name: policyName,
358
- decision: "allow",
359
- include: allowedEmails.map((email) => ({
360
- email: { email },
361
- })),
362
- precedence: existing.result.length + 1,
363
- }
364
- )
365
-
366
- return response.result
367
- }
368
-
369
- /**
370
- * Create bypass policy for an Access app (e.g., for home IP)
371
- */
372
- async createBypassPolicy(
373
- appId: string,
374
- bypassIp: string,
375
- policyName = "easiarr-web-bypass"
376
- ): Promise<{ id: string }> {
377
- const accountId = await this.getAccountId()
378
-
379
- // Check if policy already exists
380
- const existing = await this.request<Array<{ id: string; name: string }>>(
381
- "GET",
382
- `/accounts/${accountId}/access/apps/${appId}/policies`
383
- )
384
-
385
- const existingPolicy = existing.result.find((p) => p.name === policyName)
386
- if (existingPolicy) {
387
- return { id: existingPolicy.id }
388
- }
389
-
390
- // Create IP-based bypass policy
391
- const response = await this.request<{ id: string }>(
392
- "POST",
393
- `/accounts/${accountId}/access/apps/${appId}/policies`,
394
- {
395
- name: policyName,
396
- decision: "bypass",
397
- include: [
398
- {
399
- ip: { ip: bypassIp },
400
- },
401
- ],
402
- precedence: existing.result.length + 1,
403
- }
404
- )
405
-
406
- return response.result
407
- }
408
-
409
- /**
410
- * Create Access application with email policy and optional IP bypass
411
- */
412
- async setupAccessProtection(
413
- domain: string,
414
- allowedEmails: string[],
415
- appName = "easiarr",
416
- bypassIp?: string
417
- ): Promise<{ appId: string; policyId: string; bypassPolicyId?: string }> {
418
- const app = await this.createAccessApplication(domain, appName)
419
- const policy = await this.createAccessPolicy(app.id, allowedEmails, "easiarr-web-allow")
420
-
421
- let bypassPolicyId: string | undefined
422
- if (bypassIp) {
423
- const bypassPolicy = await this.createBypassPolicy(app.id, bypassIp, "easiarr-web-bypass")
424
- bypassPolicyId = bypassPolicy.id
425
- }
426
-
427
- return { appId: app.id, policyId: policy.id, bypassPolicyId }
428
- }
429
-
430
- // ==================== WARP Device Enrollment API ====================
431
-
432
- /**
433
- * Get or create device enrollment application (type: warp)
434
- */
435
- async getDeviceEnrollmentApp(): Promise<{ id: string; name: string } | null> {
436
- const accountId = await this.getAccountId()
437
- const apps = await this.request<Array<{ id: string; name: string; type: string }>>(
438
- "GET",
439
- `/accounts/${accountId}/access/apps`
440
- )
441
- return apps.result.find((a) => a.type === "warp") || null
442
- }
443
-
444
- /**
445
- * Create device enrollment policy for WARP
446
- * This allows specified emails to enroll their devices
447
- * Also creates a bypass policy for local network access
448
- */
449
- async setupDeviceEnrollment(
450
- allowedEmails: string[],
451
- privateNetworkCidr?: string
452
- ): Promise<{ appId: string; allowPolicyId: string; bypassPolicyId?: string }> {
453
- const accountId = await this.getAccountId()
454
-
455
- // Check if WARP enrollment app exists
456
- let warpApp = await this.getDeviceEnrollmentApp()
457
-
458
- if (!warpApp) {
459
- // Create WARP enrollment app
460
- const response = await this.request<{ id: string; name: string }>("POST", `/accounts/${accountId}/access/apps`, {
461
- type: "warp",
462
- name: "Device Enrollment",
463
- session_duration: "24h",
464
- })
465
- warpApp = response.result
466
- }
467
-
468
- // Get existing policies
469
- const existingPolicies = await this.request<Array<{ id: string; name: string }>>(
470
- "GET",
471
- `/accounts/${accountId}/access/apps/${warpApp.id}/policies`
472
- )
473
-
474
- let allowPolicyId: string
475
- let bypassPolicyId: string | undefined
476
-
477
- // 1. Create/get email-based Allow policy
478
- const allowPolicyName = "easiarr-vpn-allow"
479
- const existingAllow = existingPolicies.result.find((p) => p.name === allowPolicyName)
480
- if (existingAllow) {
481
- allowPolicyId = existingAllow.id
482
- } else {
483
- const policy = await this.request<{ id: string }>(
484
- "POST",
485
- `/accounts/${accountId}/access/apps/${warpApp.id}/policies`,
486
- {
487
- name: allowPolicyName,
488
- decision: "allow",
489
- include: allowedEmails.map((email) => ({
490
- email: { email },
491
- })),
492
- precedence: existingPolicies.result.length + 1,
493
- }
494
- )
495
- allowPolicyId = policy.result.id
496
- }
497
-
498
- // 2. Create/get Bypass policy for local network (if CIDR provided)
499
- if (privateNetworkCidr) {
500
- const bypassPolicyName = "easiarr-vpn-bypass"
501
- const existingBypass = existingPolicies.result.find((p) => p.name === bypassPolicyName)
502
- if (existingBypass) {
503
- bypassPolicyId = existingBypass.id
504
- } else {
505
- const bypassPolicy = await this.request<{ id: string }>(
506
- "POST",
507
- `/accounts/${accountId}/access/apps/${warpApp.id}/policies`,
508
- {
509
- name: bypassPolicyName,
510
- decision: "bypass",
511
- include: [
512
- {
513
- ip: { ip: privateNetworkCidr },
514
- },
515
- ],
516
- precedence: existingPolicies.result.length + 2,
517
- }
518
- )
519
- bypassPolicyId = bypassPolicy.result.id
520
- }
521
- }
522
-
523
- return { appId: warpApp.id, allowPolicyId, bypassPolicyId }
524
- }
525
- }
526
-
527
- /**
528
- * Helper to create a fully configured tunnel with DNS
529
- */
530
- export async function setupCloudflaredTunnel(
531
- apiToken: string,
532
- domain: string,
533
- tunnelName = "easiarr",
534
- warpRouting = false
535
- ): Promise<{ tunnelToken: string; tunnelId: string; accountId: string }> {
536
- const api = new CloudflareApi(apiToken)
537
-
538
- // Get account ID first (needed for Homepage widget)
539
- const accountId = await api.getAccountId()
540
-
541
- // 1. Check if tunnel already exists
542
- let tunnel = await api.getTunnelByName(tunnelName)
543
- let tunnelToken: string
544
-
545
- if (tunnel) {
546
- // Get existing tunnel token
547
- tunnelToken = await api.getTunnelToken(tunnel.id)
548
- } else {
549
- // 2. Create new tunnel
550
- const result = await api.createTunnel(tunnelName)
551
- tunnel = result.tunnel
552
- tunnelToken = await api.getTunnelToken(tunnel.id)
553
- }
554
-
555
- // 3. Configure ingress rules (enable warp-routing if VPN is enabled)
556
- await api.configureTunnel(
557
- tunnel.id,
558
- [
559
- {
560
- hostname: `*.${domain}`,
561
- service: "http://traefik:80",
562
- originRequest: {},
563
- },
564
- ],
565
- warpRouting
566
- )
567
-
568
- // 4. Add DNS CNAME record (wildcard)
569
- const zoneId = await api.getZoneId(domain)
570
- await api.createDnsRecord(zoneId, `*.${domain}`, tunnel.id)
571
-
572
- return {
573
- tunnelToken,
574
- tunnelId: tunnel.id,
575
- accountId,
576
- }
577
- }