@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.
- package/README.md +8 -5
- package/dist/index.js +29837 -0
- package/package.json +18 -16
- package/src/VersionInfo.ts +0 -12
- package/src/api/arr-api.ts +0 -508
- package/src/api/auto-setup-types.ts +0 -64
- package/src/api/bazarr-api.ts +0 -356
- package/src/api/cloudflare-api.ts +0 -577
- package/src/api/custom-format-api.ts +0 -212
- package/src/api/grafana-api.ts +0 -314
- package/src/api/heimdall-api.ts +0 -209
- package/src/api/homarr-api.ts +0 -296
- package/src/api/huntarr-api.ts +0 -622
- package/src/api/index.ts +0 -1
- package/src/api/jellyfin-api.ts +0 -473
- package/src/api/jellyseerr-api.ts +0 -660
- package/src/api/maintainerr-api.ts +0 -731
- package/src/api/naming-config.ts +0 -93
- package/src/api/overseerr-api.ts +0 -513
- package/src/api/plex-api.ts +0 -358
- package/src/api/portainer-api.ts +0 -467
- package/src/api/profilarr-api.ts +0 -380
- package/src/api/prowlarr-api.ts +0 -859
- package/src/api/qbittorrent-api.ts +0 -339
- package/src/api/quality-profile-api.ts +0 -269
- package/src/api/tautulli-api.ts +0 -277
- package/src/api/uptime-kuma-api.ts +0 -342
- package/src/apps/categories.ts +0 -14
- package/src/apps/index.ts +0 -2
- package/src/apps/registry.ts +0 -1255
- package/src/compose/caddy-config.ts +0 -129
- package/src/compose/generator.ts +0 -262
- package/src/compose/index.ts +0 -3
- package/src/compose/templates.ts +0 -78
- package/src/compose/traefik-config.ts +0 -185
- package/src/config/bookmarks-generator.ts +0 -149
- package/src/config/defaults.ts +0 -37
- package/src/config/homepage-config.ts +0 -376
- package/src/config/index.ts +0 -3
- package/src/config/manager.ts +0 -149
- package/src/config/recyclarr-config.ts +0 -179
- package/src/config/schema.ts +0 -310
- package/src/config/slskd-config.ts +0 -85
- package/src/config/soularr-config.ts +0 -105
- package/src/config/trash-quality-definitions.ts +0 -213
- package/src/data/lidarr-custom-formats.ts +0 -127
- package/src/data/trash-profiles.ts +0 -326
- package/src/docker/client.ts +0 -291
- package/src/docker/index.ts +0 -1
- package/src/index.ts +0 -77
- package/src/structure/manager.ts +0 -128
- package/src/ui/App.ts +0 -109
- package/src/ui/components/ApplicationSelector.ts +0 -272
- package/src/ui/components/FileEditor.ts +0 -91
- package/src/ui/components/FooterHint.ts +0 -119
- package/src/ui/components/PageLayout.ts +0 -152
- package/src/ui/components/UpdateNotification.ts +0 -101
- package/src/ui/index.ts +0 -1
- package/src/ui/screens/AdvancedSettings.ts +0 -181
- package/src/ui/screens/ApiKeyViewer.ts +0 -500
- package/src/ui/screens/AppConfigurator.ts +0 -731
- package/src/ui/screens/AppManager.ts +0 -276
- package/src/ui/screens/CloudflaredSetup.ts +0 -974
- package/src/ui/screens/ContainerControl.ts +0 -579
- package/src/ui/screens/FullAutoSetup.ts +0 -1719
- package/src/ui/screens/HomepageSetup.ts +0 -306
- package/src/ui/screens/JellyfinSetup.ts +0 -471
- package/src/ui/screens/JellyseerrSetup.ts +0 -612
- package/src/ui/screens/LogsViewer.ts +0 -468
- package/src/ui/screens/MainMenu.ts +0 -288
- package/src/ui/screens/MonitorDashboard.ts +0 -866
- package/src/ui/screens/ProwlarrSetup.ts +0 -623
- package/src/ui/screens/QBittorrentSetup.ts +0 -268
- package/src/ui/screens/QuickSetup.ts +0 -1161
- package/src/ui/screens/RecyclarrSetup.ts +0 -418
- package/src/ui/screens/SecretsEditor.ts +0 -214
- package/src/ui/screens/SettingsScreen.ts +0 -605
- package/src/ui/screens/TRaSHProfileSetup.ts +0 -395
- package/src/ui/screens/index.ts +0 -4
- package/src/util/arch.ts +0 -78
- package/src/utils/browser.ts +0 -26
- package/src/utils/categories.ts +0 -59
- package/src/utils/debug.ts +0 -91
- package/src/utils/env.ts +0 -137
- package/src/utils/logs.ts +0 -118
- package/src/utils/migrations/1765626338_rename_env_variables.ts +0 -101
- package/src/utils/migrations/1765707135_rename_easiarr_status.ts +0 -90
- package/src/utils/migrations/1765732722_remove_cloudflare_dns_api_token.ts +0 -44
- package/src/utils/migrations/1769515650_migrate_to_xdg.ts +0 -107
- package/src/utils/migrations.ts +0 -164
- package/src/utils/password.ts +0 -42
- package/src/utils/unraid.ts +0 -101
- package/src/utils/update-checker.ts +0 -210
- 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
|
-
}
|