@naviyra/cloud-panel 0.0.3-beta → 0.0.3-beta2
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.
|
@@ -1,733 +1,857 @@
|
|
|
1
|
-
import { spawn } from "node:child_process";
|
|
2
|
-
import { existsSync } from "node:fs";
|
|
3
|
-
import { readFile, writeFile, rename, mkdir } from "node:fs/promises";
|
|
4
|
-
import path from "node:path";
|
|
5
|
-
import os from "node:os";
|
|
6
|
-
import { fileURLToPath } from "node:url";
|
|
7
|
-
import { safeEnvironment } from "./native.mjs";
|
|
8
|
-
|
|
9
|
-
const cfId = /^[a-f0-9]{32}$/i;
|
|
10
|
-
const emailPattern = /^[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,}$/i;
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
);
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
accountId =
|
|
42
|
-
apiToken =
|
|
43
|
-
|
|
44
|
-
const
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
zoneId =
|
|
88
|
-
accountId =
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
return [
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
path.join(
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
return
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
if (
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
}
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
const
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
).
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
throw Object.assign(Error("
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
if (!
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
}
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
}
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
}
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
const
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
};
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
});
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
}
|
|
646
|
-
|
|
647
|
-
const
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
)
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
)
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
const
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
(
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
}
|
|
697
|
-
|
|
698
|
-
const
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
);
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
}
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import { readFile, writeFile, rename, mkdir, copyFile, rm } from "node:fs/promises";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import os from "node:os";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
7
|
+
import { safeEnvironment } from "./native.mjs";
|
|
8
|
+
|
|
9
|
+
const cfId = /^[a-f0-9]{32}$/i;
|
|
10
|
+
const emailPattern = /^[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,}$/i;
|
|
11
|
+
const WRANGLER_CLIENT_ID = "54d11594-84e4-41aa-b438-e81b8fa78ee7";
|
|
12
|
+
const WRANGLER_TOKEN_URL = "https://dash.cloudflare.com/oauth2/token";
|
|
13
|
+
const REFRESH_SKEW_MS = 60_000;
|
|
14
|
+
let login = {
|
|
15
|
+
state: "idle",
|
|
16
|
+
log: "",
|
|
17
|
+
error: "",
|
|
18
|
+
skipped: false,
|
|
19
|
+
};
|
|
20
|
+
let child = null;
|
|
21
|
+
let apiLogin = { state: "idle", log: "", error: "", skipped: false };
|
|
22
|
+
let apiChild = null;
|
|
23
|
+
let refreshing = null;
|
|
24
|
+
let refreshFailureUntil = 0;
|
|
25
|
+
|
|
26
|
+
export function certPath() {
|
|
27
|
+
return (
|
|
28
|
+
process.env.CLOUDFLARE_CERT ||
|
|
29
|
+
path.join(os.homedir(), ".cloudflared", "cert.pem")
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
export function parseOriginCert(pem) {
|
|
33
|
+
const match = String(pem).match(
|
|
34
|
+
/-----BEGIN ARGO TUNNEL TOKEN-----([A-Za-z0-9+/=\s]+)-----END ARGO TUNNEL TOKEN-----/,
|
|
35
|
+
);
|
|
36
|
+
if (!match) throw Error("Cloudflare certificate has no tunnel token");
|
|
37
|
+
const decoded = Buffer.from(match[1].replace(/\s/g, ""), "base64").toString(
|
|
38
|
+
"utf8",
|
|
39
|
+
);
|
|
40
|
+
let zoneId = "",
|
|
41
|
+
accountId = "",
|
|
42
|
+
apiToken = "";
|
|
43
|
+
try {
|
|
44
|
+
const json = JSON.parse(decoded);
|
|
45
|
+
zoneId = json.zoneID || json.zoneId || "";
|
|
46
|
+
accountId = json.accountID || json.accountId || "";
|
|
47
|
+
apiToken = json.apiToken || json.serviceKey || "";
|
|
48
|
+
} catch {
|
|
49
|
+
const lines = decoded
|
|
50
|
+
.split(/\r?\n/)
|
|
51
|
+
.map((line) => line.trim())
|
|
52
|
+
.filter(Boolean);
|
|
53
|
+
if (lines.length >= 2) {
|
|
54
|
+
zoneId = lines[0];
|
|
55
|
+
apiToken = lines[1];
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
if (!cfId.test(zoneId) || apiToken.length < 8)
|
|
59
|
+
throw Error("Cloudflare certificate is missing a zone or token");
|
|
60
|
+
if (accountId && !cfId.test(accountId)) accountId = "";
|
|
61
|
+
return { zoneId, accountId, apiToken };
|
|
62
|
+
}
|
|
63
|
+
async function readOriginCert() {
|
|
64
|
+
return parseOriginCert(await readFile(certPath()));
|
|
65
|
+
}
|
|
66
|
+
function pushLog(chunk) {
|
|
67
|
+
const text = String(chunk)
|
|
68
|
+
.replace(/Bearer\s+\S+/gi, "Bearer [redacted]")
|
|
69
|
+
.replace(/apiToken\\?":\\?"[^"\\]+/gi, 'apiToken":"[redacted]');
|
|
70
|
+
login.log = (login.log + text).slice(-8000);
|
|
71
|
+
}
|
|
72
|
+
function loginUrl() {
|
|
73
|
+
return login.log.match(/https:\/\/\S+/)?.[0]?.replace(/[),.\]]+$/, "") || "";
|
|
74
|
+
}
|
|
75
|
+
function publicLogin() {
|
|
76
|
+
return {
|
|
77
|
+
state: login.state,
|
|
78
|
+
log: login.log,
|
|
79
|
+
error: login.error,
|
|
80
|
+
skipped: login.skipped,
|
|
81
|
+
url: loginUrl(),
|
|
82
|
+
registered: login.state === "done",
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
export async function cloudflareStatus() {
|
|
86
|
+
let registered = false,
|
|
87
|
+
zoneId = null,
|
|
88
|
+
accountId = null;
|
|
89
|
+
try {
|
|
90
|
+
const cert = await readOriginCert();
|
|
91
|
+
registered = true;
|
|
92
|
+
zoneId = cert.zoneId;
|
|
93
|
+
accountId = cert.accountId || null;
|
|
94
|
+
} catch {
|
|
95
|
+
registered = false;
|
|
96
|
+
}
|
|
97
|
+
return {
|
|
98
|
+
registered,
|
|
99
|
+
zone_id: zoneId,
|
|
100
|
+
account_id: accountId,
|
|
101
|
+
cert_path: certPath(),
|
|
102
|
+
tunnels_enabled: process.env.NATIVE_TUNNELS !== "false",
|
|
103
|
+
api_authorized: accessTokenCurrent(await ensureWranglerToken()),
|
|
104
|
+
login: registered
|
|
105
|
+
? {
|
|
106
|
+
state: "done",
|
|
107
|
+
log: "",
|
|
108
|
+
error: "",
|
|
109
|
+
skipped: true,
|
|
110
|
+
url: "",
|
|
111
|
+
registered: true,
|
|
112
|
+
}
|
|
113
|
+
: publicLogin(),
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
async function enableNativeTunnels() {
|
|
117
|
+
process.env.NATIVE_TUNNELS = "true";
|
|
118
|
+
const envFile = fileURLToPath(new URL("../../../.env", import.meta.url));
|
|
119
|
+
try {
|
|
120
|
+
let text = await readFile(envFile, "utf8");
|
|
121
|
+
if (/^NATIVE_TUNNELS=/m.test(text))
|
|
122
|
+
text = text.replace(/^NATIVE_TUNNELS=.*$/m, "NATIVE_TUNNELS=true");
|
|
123
|
+
else text = `${text.trimEnd()}\nNATIVE_TUNNELS=true\n`;
|
|
124
|
+
await writeFile(envFile, text);
|
|
125
|
+
} catch {
|
|
126
|
+
// This process still publishes tunnels until it restarts.
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
export async function startCloudflareLogin() {
|
|
130
|
+
try {
|
|
131
|
+
await readOriginCert();
|
|
132
|
+
login = {
|
|
133
|
+
state: "done",
|
|
134
|
+
log: "Cloudflare is already registered.",
|
|
135
|
+
error: "",
|
|
136
|
+
skipped: true,
|
|
137
|
+
};
|
|
138
|
+
return { ...publicLogin(), registered: true, skipped: true };
|
|
139
|
+
} catch {
|
|
140
|
+
// A missing or unreadable certificate starts the browser login.
|
|
141
|
+
}
|
|
142
|
+
if (login.state === "running") return publicLogin();
|
|
143
|
+
login = {
|
|
144
|
+
state: "running",
|
|
145
|
+
log: "Starting cloudflared tunnel login…\n",
|
|
146
|
+
error: "",
|
|
147
|
+
skipped: false,
|
|
148
|
+
};
|
|
149
|
+
const file = certPath();
|
|
150
|
+
try {
|
|
151
|
+
await rename(file, `${file}.invalid`);
|
|
152
|
+
pushLog("Moved an unreadable certificate aside.\n");
|
|
153
|
+
} catch {
|
|
154
|
+
// No previous certificate.
|
|
155
|
+
}
|
|
156
|
+
await mkdir(path.dirname(file), { recursive: true });
|
|
157
|
+
const bin = process.env.CLOUDFLARED_BIN || "cloudflared";
|
|
158
|
+
child = spawn(bin, ["tunnel", "login"], {
|
|
159
|
+
env: { ...safeEnvironment(), TUNNEL_ORIGIN_CERT: file },
|
|
160
|
+
windowsHide: true,
|
|
161
|
+
shell: false,
|
|
162
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
163
|
+
});
|
|
164
|
+
child.stdout?.on("data", pushLog);
|
|
165
|
+
child.stderr?.on("data", pushLog);
|
|
166
|
+
const timer = setTimeout(() => {
|
|
167
|
+
child?.kill();
|
|
168
|
+
if (login.state === "running") {
|
|
169
|
+
login.state = "error";
|
|
170
|
+
login.error =
|
|
171
|
+
"Cloudflare login timed out. Approve the browser prompt and try again.";
|
|
172
|
+
}
|
|
173
|
+
}, 5 * 60 * 1000);
|
|
174
|
+
child.on("error", (error) => {
|
|
175
|
+
clearTimeout(timer);
|
|
176
|
+
login.state = "error";
|
|
177
|
+
login.error =
|
|
178
|
+
error.code === "ENOENT"
|
|
179
|
+
? "cloudflared is not installed. Install it, set CLOUDFLARED_BIN if needed, and restart the agent."
|
|
180
|
+
: error.message;
|
|
181
|
+
child = null;
|
|
182
|
+
});
|
|
183
|
+
child.on("exit", async () => {
|
|
184
|
+
clearTimeout(timer);
|
|
185
|
+
child = null;
|
|
186
|
+
try {
|
|
187
|
+
await readOriginCert();
|
|
188
|
+
await enableNativeTunnels();
|
|
189
|
+
login.state = "done";
|
|
190
|
+
login.error = "";
|
|
191
|
+
pushLog("\nCloudflare account registered.\n");
|
|
192
|
+
} catch {
|
|
193
|
+
if (login.state === "running") {
|
|
194
|
+
login.state = "error";
|
|
195
|
+
login.error =
|
|
196
|
+
"Login finished without a certificate. Approve a zone in the browser and try again.";
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
});
|
|
200
|
+
return publicLogin();
|
|
201
|
+
}
|
|
202
|
+
export function cloudflareLoginStatus() {
|
|
203
|
+
return publicLogin();
|
|
204
|
+
}
|
|
205
|
+
function wranglerAuthPaths() {
|
|
206
|
+
if (process.env.WRANGLER_AUTH_FILE) return [process.env.WRANGLER_AUTH_FILE];
|
|
207
|
+
const home = os.homedir();
|
|
208
|
+
const appData =
|
|
209
|
+
process.env.APPDATA || path.join(home, "AppData", "Roaming");
|
|
210
|
+
const xdg = process.env.XDG_CONFIG_HOME || path.join(home, ".config");
|
|
211
|
+
return [
|
|
212
|
+
path.join(home, ".wrangler", "config", "default.toml"),
|
|
213
|
+
path.join(appData, "xdg.config", ".wrangler", "config", "default.toml"),
|
|
214
|
+
path.join(xdg, ".wrangler", "config", "default.toml"),
|
|
215
|
+
path.join(xdg, "wrangler", "config", "default.toml"),
|
|
216
|
+
];
|
|
217
|
+
}
|
|
218
|
+
function tomlValue(text, key) {
|
|
219
|
+
const match = text.match(
|
|
220
|
+
new RegExp(`^${key}\\s*=\\s*"((?:\\\\.|[^"\\\\])*)"`, "m"),
|
|
221
|
+
);
|
|
222
|
+
return match ? match[1].replace(/\\"/g, '"') : "";
|
|
223
|
+
}
|
|
224
|
+
function parseScopes(text) {
|
|
225
|
+
const match = text.match(/^scopes\s*=\s*\[([^\]]*)\]/m);
|
|
226
|
+
if (!match) return [];
|
|
227
|
+
return [...match[1].matchAll(/"((?:\\.|[^"\\])*)"/g)].map((item) =>
|
|
228
|
+
item[1].replace(/\\"/g, '"'),
|
|
229
|
+
);
|
|
230
|
+
}
|
|
231
|
+
function tomlString(value) {
|
|
232
|
+
return `"${String(value).replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
|
|
233
|
+
}
|
|
234
|
+
function accessTokenCurrent(auth) {
|
|
235
|
+
if (!auth?.token) return false;
|
|
236
|
+
if (!auth.expiration) return true;
|
|
237
|
+
const when = Date.parse(auth.expiration);
|
|
238
|
+
if (Number.isNaN(when)) return true;
|
|
239
|
+
return when - Date.now() > REFRESH_SKEW_MS;
|
|
240
|
+
}
|
|
241
|
+
export async function readWranglerAuth() {
|
|
242
|
+
for (const file of wranglerAuthPaths()) {
|
|
243
|
+
let text;
|
|
244
|
+
try {
|
|
245
|
+
text = await readFile(file, "utf8");
|
|
246
|
+
} catch {
|
|
247
|
+
continue;
|
|
248
|
+
}
|
|
249
|
+
const token = tomlValue(text, "oauth_token");
|
|
250
|
+
if (token.length < 20) continue;
|
|
251
|
+
return {
|
|
252
|
+
token,
|
|
253
|
+
refreshToken: tomlValue(text, "refresh_token"),
|
|
254
|
+
expiration: tomlValue(text, "expiration_time"),
|
|
255
|
+
scopes: parseScopes(text),
|
|
256
|
+
file,
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
return null;
|
|
260
|
+
}
|
|
261
|
+
async function writeWranglerAuth(auth) {
|
|
262
|
+
const scopes = (auth.scopes.length ? auth.scopes : ["offline_access"])
|
|
263
|
+
.map((scope) => tomlString(scope))
|
|
264
|
+
.join(", ");
|
|
265
|
+
const text = [
|
|
266
|
+
`oauth_token = ${tomlString(auth.token)}`,
|
|
267
|
+
`expiration_time = ${tomlString(auth.expiration)}`,
|
|
268
|
+
`refresh_token = ${tomlString(auth.refreshToken)}`,
|
|
269
|
+
`scopes = [ ${scopes} ]`,
|
|
270
|
+
"",
|
|
271
|
+
].join("\n");
|
|
272
|
+
const temporary = `${auth.file}.${process.pid}.tmp`;
|
|
273
|
+
await writeFile(temporary, text, "utf8");
|
|
274
|
+
try {
|
|
275
|
+
await copyFile(temporary, auth.file);
|
|
276
|
+
} finally {
|
|
277
|
+
await rm(temporary, { force: true });
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
async function refreshAndStore(auth, fetchImpl) {
|
|
281
|
+
if (Date.now() < refreshFailureUntil) return auth;
|
|
282
|
+
let response;
|
|
283
|
+
try {
|
|
284
|
+
response = await fetchImpl(WRANGLER_TOKEN_URL, {
|
|
285
|
+
method: "POST",
|
|
286
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
287
|
+
body: new URLSearchParams({
|
|
288
|
+
grant_type: "refresh_token",
|
|
289
|
+
refresh_token: auth.refreshToken,
|
|
290
|
+
client_id: WRANGLER_CLIENT_ID,
|
|
291
|
+
}),
|
|
292
|
+
signal: AbortSignal.timeout(20000),
|
|
293
|
+
});
|
|
294
|
+
} catch {
|
|
295
|
+
refreshFailureUntil = Date.now() + 60_000;
|
|
296
|
+
return auth;
|
|
297
|
+
}
|
|
298
|
+
const data = await response.json().catch(() => ({}));
|
|
299
|
+
if (
|
|
300
|
+
!response.ok ||
|
|
301
|
+
data.error ||
|
|
302
|
+
typeof data.access_token !== "string" ||
|
|
303
|
+
data.access_token.length < 20
|
|
304
|
+
) {
|
|
305
|
+
refreshFailureUntil = Date.now() + 60_000;
|
|
306
|
+
return auth;
|
|
307
|
+
}
|
|
308
|
+
const next = {
|
|
309
|
+
token: data.access_token,
|
|
310
|
+
refreshToken:
|
|
311
|
+
typeof data.refresh_token === "string" && data.refresh_token.length >= 20
|
|
312
|
+
? data.refresh_token
|
|
313
|
+
: auth.refreshToken,
|
|
314
|
+
expiration: new Date(
|
|
315
|
+
Date.now() + Number(data.expires_in || 3600) * 1000,
|
|
316
|
+
).toISOString(),
|
|
317
|
+
scopes:
|
|
318
|
+
typeof data.scope === "string" && data.scope.trim()
|
|
319
|
+
? data.scope.split(" ").filter(Boolean)
|
|
320
|
+
: auth.scopes,
|
|
321
|
+
file: auth.file,
|
|
322
|
+
};
|
|
323
|
+
try {
|
|
324
|
+
await writeWranglerAuth(next);
|
|
325
|
+
} catch {
|
|
326
|
+
refreshFailureUntil = Date.now() + 60_000;
|
|
327
|
+
return auth;
|
|
328
|
+
}
|
|
329
|
+
refreshFailureUntil = 0;
|
|
330
|
+
return next;
|
|
331
|
+
}
|
|
332
|
+
export async function ensureWranglerToken(fetchImpl = fetch, { force = false } = {}) {
|
|
333
|
+
const auth = await readWranglerAuth();
|
|
334
|
+
if (!auth) return null;
|
|
335
|
+
if (!force && accessTokenCurrent(auth)) return auth;
|
|
336
|
+
if (!auth.refreshToken || auth.refreshToken.length < 20) return auth;
|
|
337
|
+
if (!refreshing)
|
|
338
|
+
refreshing = refreshAndStore(auth, fetchImpl).finally(() => {
|
|
339
|
+
refreshing = null;
|
|
340
|
+
});
|
|
341
|
+
return refreshing;
|
|
342
|
+
}
|
|
343
|
+
function apiLoginView() {
|
|
344
|
+
const url =
|
|
345
|
+
apiLogin.log.match(/https:\/\/\S+/)?.[0]?.replace(/[),.\]]+$/, "") || "";
|
|
346
|
+
return {
|
|
347
|
+
state: apiLogin.state,
|
|
348
|
+
log: apiLogin.log,
|
|
349
|
+
error: apiLogin.error,
|
|
350
|
+
skipped: apiLogin.skipped,
|
|
351
|
+
url,
|
|
352
|
+
registered: apiLogin.state === "done",
|
|
353
|
+
};
|
|
354
|
+
}
|
|
355
|
+
function pushApiLog(chunk) {
|
|
356
|
+
const text = String(chunk)
|
|
357
|
+
.replace(/Bearer\s+\S+/gi, "Bearer [redacted]")
|
|
358
|
+
.replace(/oauth_token\\?":\\?"[^"\\]+/gi, 'oauth_token":"[redacted]');
|
|
359
|
+
apiLogin.log = (apiLogin.log + text).slice(-8000);
|
|
360
|
+
}
|
|
361
|
+
function wranglerLaunch() {
|
|
362
|
+
if (process.env.WRANGLER_BIN)
|
|
363
|
+
return { file: process.env.WRANGLER_BIN, args: ["login"] };
|
|
364
|
+
const cli = path.join(
|
|
365
|
+
path.dirname(process.execPath),
|
|
366
|
+
"node_modules",
|
|
367
|
+
"npm",
|
|
368
|
+
"bin",
|
|
369
|
+
"npx-cli.js",
|
|
370
|
+
);
|
|
371
|
+
if (existsSync(cli))
|
|
372
|
+
return {
|
|
373
|
+
file: process.execPath,
|
|
374
|
+
args: [cli, "--yes", "wrangler", "login"],
|
|
375
|
+
};
|
|
376
|
+
return {
|
|
377
|
+
file: process.env.ComSpec || "cmd.exe",
|
|
378
|
+
args: ["/d", "/s", "/c", "npx", "--yes", "wrangler", "login"],
|
|
379
|
+
};
|
|
380
|
+
}
|
|
381
|
+
export async function startCloudflareApiLogin() {
|
|
382
|
+
if (accessTokenCurrent(await ensureWranglerToken())) {
|
|
383
|
+
apiLogin = {
|
|
384
|
+
state: "done",
|
|
385
|
+
log: "Wrangler is already authorized.",
|
|
386
|
+
error: "",
|
|
387
|
+
skipped: true,
|
|
388
|
+
};
|
|
389
|
+
return { ...apiLoginView(), registered: true, skipped: true };
|
|
390
|
+
}
|
|
391
|
+
if (apiLogin.state === "running") return apiLoginView();
|
|
392
|
+
apiLogin = {
|
|
393
|
+
state: "running",
|
|
394
|
+
log: "Starting wrangler login…\n",
|
|
395
|
+
error: "",
|
|
396
|
+
skipped: false,
|
|
397
|
+
};
|
|
398
|
+
const launch = wranglerLaunch();
|
|
399
|
+
apiChild = spawn(launch.file, launch.args, {
|
|
400
|
+
env: safeEnvironment(),
|
|
401
|
+
windowsHide: true,
|
|
402
|
+
shell: false,
|
|
403
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
404
|
+
});
|
|
405
|
+
apiChild.stdout?.on("data", pushApiLog);
|
|
406
|
+
apiChild.stderr?.on("data", pushApiLog);
|
|
407
|
+
const timer = setTimeout(() => {
|
|
408
|
+
apiChild?.kill();
|
|
409
|
+
if (apiLogin.state === "running") {
|
|
410
|
+
apiLogin.state = "error";
|
|
411
|
+
apiLogin.error =
|
|
412
|
+
"Wrangler login timed out. Approve the browser prompt and try again.";
|
|
413
|
+
}
|
|
414
|
+
}, 8 * 60 * 1000);
|
|
415
|
+
apiChild.on("error", (error) => {
|
|
416
|
+
clearTimeout(timer);
|
|
417
|
+
apiLogin.state = "error";
|
|
418
|
+
apiLogin.error = error.message;
|
|
419
|
+
apiChild = null;
|
|
420
|
+
});
|
|
421
|
+
apiChild.on("exit", async () => {
|
|
422
|
+
clearTimeout(timer);
|
|
423
|
+
apiChild = null;
|
|
424
|
+
if (await readWranglerAuth()) {
|
|
425
|
+
apiLogin.state = "done";
|
|
426
|
+
apiLogin.error = "";
|
|
427
|
+
pushApiLog("\nCloudflare API authorized.\n");
|
|
428
|
+
} else if (apiLogin.state === "running") {
|
|
429
|
+
apiLogin.state = "error";
|
|
430
|
+
apiLogin.error =
|
|
431
|
+
"Wrangler login finished without API access. Approve the browser prompt and try again.";
|
|
432
|
+
}
|
|
433
|
+
});
|
|
434
|
+
return apiLoginView();
|
|
435
|
+
}
|
|
436
|
+
export function cloudflareApiLoginStatus() {
|
|
437
|
+
return apiLoginView();
|
|
438
|
+
}
|
|
439
|
+
async function credentials(purpose = "dns") {
|
|
440
|
+
let cert;
|
|
441
|
+
try {
|
|
442
|
+
cert = await readOriginCert();
|
|
443
|
+
} catch {
|
|
444
|
+
throw Object.assign(
|
|
445
|
+
Error("Connect Cloudflare with cloudflared tunnel login first"),
|
|
446
|
+
{ status: 400 },
|
|
447
|
+
);
|
|
448
|
+
}
|
|
449
|
+
const zoneFromEnv = process.env.CLOUDFLARE_ZONE_ID || "";
|
|
450
|
+
const zoneId = cfId.test(zoneFromEnv) ? zoneFromEnv : cert.zoneId;
|
|
451
|
+
if (purpose === "email") {
|
|
452
|
+
const wrangler = await ensureWranglerToken();
|
|
453
|
+
const envToken = process.env.CLOUDFLARE_API_TOKEN || "";
|
|
454
|
+
const token =
|
|
455
|
+
(accessTokenCurrent(wrangler) ? wrangler.token : "") ||
|
|
456
|
+
(envToken.length >= 20 ? envToken : "") ||
|
|
457
|
+
wrangler?.token ||
|
|
458
|
+
"";
|
|
459
|
+
if (!token)
|
|
460
|
+
throw Object.assign(
|
|
461
|
+
Error(
|
|
462
|
+
"Authorize the Cloudflare API with Wrangler. The CLI login opens a browser and does not need CLOUDFLARE_API_TOKEN.",
|
|
463
|
+
),
|
|
464
|
+
{ status: 403, needsApiLogin: true },
|
|
465
|
+
);
|
|
466
|
+
return { token, zoneId, accountId: cert.accountId };
|
|
467
|
+
}
|
|
468
|
+
return { token: cert.apiToken, zoneId, accountId: cert.accountId };
|
|
469
|
+
}
|
|
470
|
+
export async function deleteTunnelCname(subdomain, tunnelId) {
|
|
471
|
+
const { token, zoneId } = await credentials("dns");
|
|
472
|
+
const base = `https://api.cloudflare.com/client/v4/zones/${zoneId}/dns_records`;
|
|
473
|
+
const call = async (url, method = "GET") => {
|
|
474
|
+
const response = await fetch(url, {
|
|
475
|
+
method,
|
|
476
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
477
|
+
signal: AbortSignal.timeout(15000),
|
|
478
|
+
});
|
|
479
|
+
const data = await response.json().catch(() => ({}));
|
|
480
|
+
if (!response.ok || data.success === false)
|
|
481
|
+
throw Error("Cloudflare DNS cleanup failed");
|
|
482
|
+
return data.result;
|
|
483
|
+
};
|
|
484
|
+
for (const record of await call(
|
|
485
|
+
`${base}?name=${encodeURIComponent(subdomain)}&type=CNAME`,
|
|
486
|
+
))
|
|
487
|
+
if (record.content === `${tunnelId}.cfargotunnel.com`)
|
|
488
|
+
await call(`${base}/${record.id}`, "DELETE");
|
|
489
|
+
}
|
|
490
|
+
async function cf(pathname, options = {}) {
|
|
491
|
+
const { method = "GET", body, query, purpose = "dns" } = options;
|
|
492
|
+
const { token } = await credentials(purpose);
|
|
493
|
+
const url = new URL(`https://api.cloudflare.com/client/v4${pathname}`);
|
|
494
|
+
for (const [key, value] of Object.entries(query || {}))
|
|
495
|
+
if (value != null && value !== "") url.searchParams.set(key, String(value));
|
|
496
|
+
let response;
|
|
497
|
+
try {
|
|
498
|
+
response = await fetch(url, {
|
|
499
|
+
method,
|
|
500
|
+
headers: {
|
|
501
|
+
Authorization: `Bearer ${token}`,
|
|
502
|
+
"Content-Type": "application/json",
|
|
503
|
+
},
|
|
504
|
+
body: body == null ? undefined : JSON.stringify(body),
|
|
505
|
+
signal: AbortSignal.timeout(20000),
|
|
506
|
+
});
|
|
507
|
+
} catch {
|
|
508
|
+
throw Object.assign(Error("Cloudflare API is unreachable"), { status: 502 });
|
|
509
|
+
}
|
|
510
|
+
const data = await response.json().catch(() => ({}));
|
|
511
|
+
if (
|
|
512
|
+
response.status === 401 &&
|
|
513
|
+
purpose === "email" &&
|
|
514
|
+
!options.retried
|
|
515
|
+
) {
|
|
516
|
+
const renewed = await ensureWranglerToken(fetch, { force: true });
|
|
517
|
+
if (renewed?.token && renewed.token !== token)
|
|
518
|
+
return cf(pathname, { ...options, retried: true });
|
|
519
|
+
}
|
|
520
|
+
if (!response.ok || data.success === false) {
|
|
521
|
+
let message =
|
|
522
|
+
data.errors?.[0]?.message || `Cloudflare API returned ${response.status}`;
|
|
523
|
+
if (response.status === 401 || response.status === 403)
|
|
524
|
+
message +=
|
|
525
|
+
". Authorize with Wrangler from the Email screen. That CLI login does not use an API token in .env.";
|
|
526
|
+
throw Object.assign(Error(message), {
|
|
527
|
+
status: response.status === 401 || response.status === 403 ? 403 : 502,
|
|
528
|
+
});
|
|
529
|
+
}
|
|
530
|
+
return data;
|
|
531
|
+
}
|
|
532
|
+
function recordView(record) {
|
|
533
|
+
return {
|
|
534
|
+
id: record.id,
|
|
535
|
+
type: record.type,
|
|
536
|
+
name: record.name,
|
|
537
|
+
content: record.content,
|
|
538
|
+
ttl: record.ttl,
|
|
539
|
+
proxied: Boolean(record.proxied),
|
|
540
|
+
proxiable: Boolean(record.proxiable),
|
|
541
|
+
priority: record.priority ?? null,
|
|
542
|
+
comment: record.comment || "",
|
|
543
|
+
tags: record.tags || [],
|
|
544
|
+
locked: Boolean(record.locked),
|
|
545
|
+
created_on: record.created_on || null,
|
|
546
|
+
modified_on: record.modified_on || null,
|
|
547
|
+
};
|
|
548
|
+
}
|
|
549
|
+
export function assertCfId(id) {
|
|
550
|
+
if (!cfId.test(id || ""))
|
|
551
|
+
throw Object.assign(Error("Invalid Cloudflare identifier"), { status: 400 });
|
|
552
|
+
return id;
|
|
553
|
+
}
|
|
554
|
+
export async function listDns() {
|
|
555
|
+
const { zoneId } = await credentials();
|
|
556
|
+
const zone = await cf(`/zones/${zoneId}`);
|
|
557
|
+
const records = [];
|
|
558
|
+
for (let page = 1; page <= 20; page++) {
|
|
559
|
+
const data = await cf(`/zones/${zoneId}/dns_records`, {
|
|
560
|
+
query: { page, per_page: 100 },
|
|
561
|
+
});
|
|
562
|
+
records.push(...(data.result || []).map(recordView));
|
|
563
|
+
if (!data.result_info || page >= data.result_info.total_pages) break;
|
|
564
|
+
}
|
|
565
|
+
return {
|
|
566
|
+
zone: {
|
|
567
|
+
id: zone.result.id,
|
|
568
|
+
name: zone.result.name,
|
|
569
|
+
status: zone.result.status,
|
|
570
|
+
},
|
|
571
|
+
records,
|
|
572
|
+
};
|
|
573
|
+
}
|
|
574
|
+
export async function getDnsRecord(id) {
|
|
575
|
+
assertCfId(id);
|
|
576
|
+
const { zoneId } = await credentials();
|
|
577
|
+
return recordView((await cf(`/zones/${zoneId}/dns_records/${id}`)).result);
|
|
578
|
+
}
|
|
579
|
+
export async function updateDnsRecord(id, input) {
|
|
580
|
+
assertCfId(id);
|
|
581
|
+
const name = String(input?.name || "").trim();
|
|
582
|
+
const content = String(input?.content ?? "").trim();
|
|
583
|
+
const ttl = Number(input?.ttl);
|
|
584
|
+
if (!name || name.length > 253 || /[^a-z0-9._@*-]/i.test(name))
|
|
585
|
+
throw Object.assign(Error("Enter a valid record name"), { status: 400 });
|
|
586
|
+
if (!content || content.length > 4096)
|
|
587
|
+
throw Object.assign(Error("Enter record content"), { status: 400 });
|
|
588
|
+
if (!(ttl === 1 || (Number.isInteger(ttl) && ttl >= 60 && ttl <= 86400)))
|
|
589
|
+
throw Object.assign(
|
|
590
|
+
Error("TTL must be Auto or between 60 and 86400 seconds"),
|
|
591
|
+
{ status: 400 },
|
|
592
|
+
);
|
|
593
|
+
const current = await getDnsRecord(id);
|
|
594
|
+
if (current.locked)
|
|
595
|
+
throw Object.assign(Error("This DNS record is locked"), { status: 400 });
|
|
596
|
+
const body = {
|
|
597
|
+
type: current.type,
|
|
598
|
+
name,
|
|
599
|
+
content,
|
|
600
|
+
ttl,
|
|
601
|
+
comment: String(input?.comment || "").slice(0, 500),
|
|
602
|
+
};
|
|
603
|
+
if (current.proxiable) body.proxied = Boolean(input?.proxied);
|
|
604
|
+
if (["MX", "SRV", "URI"].includes(current.type)) {
|
|
605
|
+
const priority = Number(input?.priority);
|
|
606
|
+
if (!Number.isInteger(priority) || priority < 0 || priority > 65535)
|
|
607
|
+
throw Object.assign(Error("Enter a priority from 0 to 65535"), {
|
|
608
|
+
status: 400,
|
|
609
|
+
});
|
|
610
|
+
body.priority = priority;
|
|
611
|
+
}
|
|
612
|
+
const { zoneId } = await credentials();
|
|
613
|
+
return recordView(
|
|
614
|
+
(
|
|
615
|
+
await cf(`/zones/${zoneId}/dns_records/${id}`, {
|
|
616
|
+
method: "PUT",
|
|
617
|
+
body,
|
|
618
|
+
})
|
|
619
|
+
).result,
|
|
620
|
+
);
|
|
621
|
+
}
|
|
622
|
+
function withMailbox(input) {
|
|
623
|
+
if (!input || input.matcher === "all") return input;
|
|
624
|
+
if (input.local == null && input.domain == null) return input;
|
|
625
|
+
const local = String(input.local || "")
|
|
626
|
+
.trim()
|
|
627
|
+
.toLowerCase();
|
|
628
|
+
const domain = String(input.domain || "")
|
|
629
|
+
.trim()
|
|
630
|
+
.toLowerCase();
|
|
631
|
+
if (!/^[a-z0-9](?:[a-z0-9._+-]*[a-z0-9])?$/i.test(local))
|
|
632
|
+
throw Object.assign(Error("Enter the mailbox name"), { status: 400 });
|
|
633
|
+
if (!/^[a-z0-9.-]+\.[a-z]{2,}$/i.test(domain))
|
|
634
|
+
throw Object.assign(Error("Choose a domain"), { status: 400 });
|
|
635
|
+
return { ...input, custom_address: `${local}@${domain}`, domain };
|
|
636
|
+
}
|
|
637
|
+
function addressDomain(value) {
|
|
638
|
+
const address = String(value || "").toLowerCase();
|
|
639
|
+
const at = address.lastIndexOf("@");
|
|
640
|
+
return at === -1 ? "" : address.slice(at + 1);
|
|
641
|
+
}
|
|
642
|
+
async function zoneForDomain(domain) {
|
|
643
|
+
const { zoneId } = await credentials("email");
|
|
644
|
+
if (!domain) return zoneId;
|
|
645
|
+
const current = await cf(`/zones/${zoneId}`, { purpose: "email" });
|
|
646
|
+
if (current.result?.name?.toLowerCase() === domain) return zoneId;
|
|
647
|
+
const listed = await cf("/zones", {
|
|
648
|
+
query: { name: domain, per_page: 20 },
|
|
649
|
+
purpose: "email",
|
|
650
|
+
});
|
|
651
|
+
const match = (listed.result || []).find(
|
|
652
|
+
(zone) => zone.name?.toLowerCase() === domain,
|
|
653
|
+
);
|
|
654
|
+
if (!match)
|
|
655
|
+
throw Object.assign(Error("Choose a domain on this Cloudflare account"), {
|
|
656
|
+
status: 400,
|
|
657
|
+
});
|
|
658
|
+
return match.id;
|
|
659
|
+
}
|
|
660
|
+
async function emailDomains(zoneId, currentName) {
|
|
661
|
+
const domains = [];
|
|
662
|
+
try {
|
|
663
|
+
const listed = await cf("/zones", {
|
|
664
|
+
query: { per_page: 50, status: "active" },
|
|
665
|
+
purpose: "email",
|
|
666
|
+
});
|
|
667
|
+
for (const zone of listed.result || [])
|
|
668
|
+
if (zone?.name && cfId.test(zone.id)) domains.push(zone.name);
|
|
669
|
+
} catch {
|
|
670
|
+
/* The connected zone is still shown when the account list is unavailable. */
|
|
671
|
+
}
|
|
672
|
+
if (currentName && !domains.some((name) => name.toLowerCase() === currentName.toLowerCase()))
|
|
673
|
+
domains.unshift(currentName);
|
|
674
|
+
const names = [...new Set(domains.map((name) => name.toLowerCase()))];
|
|
675
|
+
names.sort((a, b) => a.localeCompare(b));
|
|
676
|
+
const current = currentName.toLowerCase();
|
|
677
|
+
if (names.includes(current)) {
|
|
678
|
+
names.splice(names.indexOf(current), 1);
|
|
679
|
+
names.unshift(current);
|
|
680
|
+
}
|
|
681
|
+
return names;
|
|
682
|
+
}
|
|
683
|
+
function ruleView(rule) {
|
|
684
|
+
const matcher = rule.matchers?.[0] || {};
|
|
685
|
+
const action = rule.actions?.[0] || {};
|
|
686
|
+
return {
|
|
687
|
+
id: rule.id,
|
|
688
|
+
name: rule.name || "",
|
|
689
|
+
enabled: Boolean(rule.enabled),
|
|
690
|
+
matcher: matcher.type || "",
|
|
691
|
+
custom_address: matcher.value || "",
|
|
692
|
+
action: action.type || "",
|
|
693
|
+
destination: Array.isArray(action.value) ? action.value[0] || "" : "",
|
|
694
|
+
priority: rule.priority ?? 0,
|
|
695
|
+
};
|
|
696
|
+
}
|
|
697
|
+
function ruleBody(input) {
|
|
698
|
+
const name = String(input?.name || "").trim();
|
|
699
|
+
const matcher = input?.matcher === "all" ? "all" : "literal";
|
|
700
|
+
const action = input?.action === "drop" ? "drop" : "forward";
|
|
701
|
+
const custom = String(input?.custom_address || "")
|
|
702
|
+
.trim()
|
|
703
|
+
.toLowerCase();
|
|
704
|
+
const destination = String(input?.destination || "")
|
|
705
|
+
.trim()
|
|
706
|
+
.toLowerCase();
|
|
707
|
+
if (name.length < 1 || name.length > 80)
|
|
708
|
+
throw Object.assign(Error("Enter a rule name"), { status: 400 });
|
|
709
|
+
if (matcher === "literal" && !emailPattern.test(custom))
|
|
710
|
+
throw Object.assign(Error("Enter the custom address to match"), {
|
|
711
|
+
status: 400,
|
|
712
|
+
});
|
|
713
|
+
if (action === "forward" && !emailPattern.test(destination))
|
|
714
|
+
throw Object.assign(Error("Enter a destination address"), { status: 400 });
|
|
715
|
+
return {
|
|
716
|
+
name,
|
|
717
|
+
enabled: input?.enabled !== false,
|
|
718
|
+
matchers:
|
|
719
|
+
matcher === "all"
|
|
720
|
+
? [{ type: "all" }]
|
|
721
|
+
: [{ type: "literal", field: "to", value: custom }],
|
|
722
|
+
actions:
|
|
723
|
+
action === "drop"
|
|
724
|
+
? [{ type: "drop" }]
|
|
725
|
+
: [{ type: "forward", value: [destination] }],
|
|
726
|
+
};
|
|
727
|
+
}
|
|
728
|
+
export async function emailOverview() {
|
|
729
|
+
const { zoneId, accountId } = await credentials("email");
|
|
730
|
+
const settings = await cf(`/zones/${zoneId}/email/routing`, {
|
|
731
|
+
purpose: "email",
|
|
732
|
+
});
|
|
733
|
+
const rules = await cf(`/zones/${zoneId}/email/routing/rules`, {
|
|
734
|
+
query: { per_page: 50 },
|
|
735
|
+
purpose: "email",
|
|
736
|
+
});
|
|
737
|
+
let addresses = [],
|
|
738
|
+
address_error = "";
|
|
739
|
+
if (accountId) {
|
|
740
|
+
try {
|
|
741
|
+
addresses =
|
|
742
|
+
(await cf(`/accounts/${accountId}/email/routing/addresses`, {
|
|
743
|
+
purpose: "email",
|
|
744
|
+
})).result ||
|
|
745
|
+
[];
|
|
746
|
+
} catch (error) {
|
|
747
|
+
address_error = error.message;
|
|
748
|
+
}
|
|
749
|
+
} else
|
|
750
|
+
address_error =
|
|
751
|
+
"This certificate has no account id, so destination addresses cannot be listed.";
|
|
752
|
+
return {
|
|
753
|
+
zone: settings.result?.name || "",
|
|
754
|
+
domains: await emailDomains(zoneId, settings.result?.name || ""),
|
|
755
|
+
settings: {
|
|
756
|
+
enabled: Boolean(settings.result?.enabled),
|
|
757
|
+
status: settings.result?.status || "unknown",
|
|
758
|
+
name: settings.result?.name || "",
|
|
759
|
+
support_subaddress: Boolean(settings.result?.support_subaddress),
|
|
760
|
+
},
|
|
761
|
+
rules: (rules.result || []).map(ruleView),
|
|
762
|
+
addresses: addresses.map((item) => ({
|
|
763
|
+
id: item.id,
|
|
764
|
+
email: item.email,
|
|
765
|
+
verified: item.verified || null,
|
|
766
|
+
})),
|
|
767
|
+
address_error,
|
|
768
|
+
};
|
|
769
|
+
}
|
|
770
|
+
export async function setEmailRouting(enabled) {
|
|
771
|
+
const { zoneId } = await credentials("email");
|
|
772
|
+
const data = await cf(
|
|
773
|
+
`/zones/${zoneId}/email/routing/${enabled ? "enable" : "disable"}`,
|
|
774
|
+
{ method: "POST", purpose: "email" },
|
|
775
|
+
);
|
|
776
|
+
return {
|
|
777
|
+
enabled: Boolean(data.result?.enabled),
|
|
778
|
+
status: data.result?.status || "",
|
|
779
|
+
};
|
|
780
|
+
}
|
|
781
|
+
export async function createEmailRule(input) {
|
|
782
|
+
const next = withMailbox(input);
|
|
783
|
+
const body = ruleBody(next);
|
|
784
|
+
const zoneId = await zoneForDomain(
|
|
785
|
+
next.domain || addressDomain(body.matchers?.[0]?.value),
|
|
786
|
+
);
|
|
787
|
+
return ruleView(
|
|
788
|
+
(
|
|
789
|
+
await cf(`/zones/${zoneId}/email/routing/rules`, {
|
|
790
|
+
method: "POST",
|
|
791
|
+
body,
|
|
792
|
+
purpose: "email",
|
|
793
|
+
})
|
|
794
|
+
)
|
|
795
|
+
.result,
|
|
796
|
+
);
|
|
797
|
+
}
|
|
798
|
+
export async function updateEmailRule(id, input) {
|
|
799
|
+
assertCfId(id);
|
|
800
|
+
const body = ruleBody(input);
|
|
801
|
+
const zoneId = await zoneForDomain(addressDomain(body.matchers?.[0]?.value));
|
|
802
|
+
return ruleView(
|
|
803
|
+
(
|
|
804
|
+
await cf(`/zones/${zoneId}/email/routing/rules/${id}`, {
|
|
805
|
+
method: "PUT",
|
|
806
|
+
body,
|
|
807
|
+
purpose: "email",
|
|
808
|
+
})
|
|
809
|
+
).result,
|
|
810
|
+
);
|
|
811
|
+
}
|
|
812
|
+
export async function deleteEmailRule(id, domain = "") {
|
|
813
|
+
assertCfId(id);
|
|
814
|
+
const zoneId = await zoneForDomain(String(domain || "").toLowerCase());
|
|
815
|
+
await cf(`/zones/${zoneId}/email/routing/rules/${id}`, {
|
|
816
|
+
method: "DELETE",
|
|
817
|
+
purpose: "email",
|
|
818
|
+
});
|
|
819
|
+
return { ok: true };
|
|
820
|
+
}
|
|
821
|
+
export async function addDestination(email) {
|
|
822
|
+
const address = String(email || "")
|
|
823
|
+
.trim()
|
|
824
|
+
.toLowerCase();
|
|
825
|
+
if (!emailPattern.test(address))
|
|
826
|
+
throw Object.assign(Error("Enter a valid email"), { status: 400 });
|
|
827
|
+
const { accountId } = await credentials("email");
|
|
828
|
+
if (!accountId)
|
|
829
|
+
throw Object.assign(
|
|
830
|
+
Error("Account id is missing from the Cloudflare certificate"),
|
|
831
|
+
{ status: 400 },
|
|
832
|
+
);
|
|
833
|
+
const data = await cf(`/accounts/${accountId}/email/routing/addresses`, {
|
|
834
|
+
method: "POST",
|
|
835
|
+
body: { email: address },
|
|
836
|
+
purpose: "email",
|
|
837
|
+
});
|
|
838
|
+
return {
|
|
839
|
+
id: data.result?.id,
|
|
840
|
+
email: data.result?.email,
|
|
841
|
+
verified: data.result?.verified || null,
|
|
842
|
+
};
|
|
843
|
+
}
|
|
844
|
+
export async function deleteDestination(id) {
|
|
845
|
+
assertCfId(id);
|
|
846
|
+
const { accountId } = await credentials("email");
|
|
847
|
+
if (!accountId)
|
|
848
|
+
throw Object.assign(
|
|
849
|
+
Error("Account id is missing from the Cloudflare certificate"),
|
|
850
|
+
{ status: 400 },
|
|
851
|
+
);
|
|
852
|
+
await cf(`/accounts/${accountId}/email/routing/addresses/${id}`, {
|
|
853
|
+
method: "DELETE",
|
|
854
|
+
purpose: "email",
|
|
855
|
+
});
|
|
856
|
+
return { ok: true };
|
|
857
|
+
}
|