@dutodev/cli 1.0.0 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -124,7 +124,7 @@ People get in one of these ways:
124
124
 
125
125
  Without credentials, a browser is sent to the sign-in page and other clients get `401`. Your local app never receives Duto's credentials: the `Authorization: Basic` that matched, `X-Duto-Auth` and the tunnel cookie are stripped before the request is forwarded (other cookies pass through). An `Authorization: Bearer` from your own app passes unchanged.
126
126
 
127
- **Logging out and sessions.** In a browser, signing in sets a cookie that is valid for that tunnel only. To log out, open `/__duto/logout`: it clears the cookie and returns to the sign-in page. The cookie lasts until the browser closes or the CLI stops. Restarting the CLI signs everyone out, because the key that signs the cookies is new every session. The cookie only counts on requests to the tunnel itself: a page on another origin (another tunnel, another site) that calls the tunnel has to send `X-Duto-Auth` or `Authorization: Basic`. So opening the tunnel from a link on another `*.duto.dev` address (the dashboard or another tunnel) asks for the password again; a link from outside (email, chat) or a typed URL gets straight in.
127
+ **Signing out and sessions.** In a browser, signing in sets a cookie that is valid for that tunnel only. To sign out, open `/__duto/logout`: it clears the cookie and returns to the sign-in page. The cookie lasts until the browser closes or the CLI stops. Restarting the CLI signs everyone out, because the key that signs the cookies is new every session. The cookie only counts on requests to the tunnel itself: a page on another origin (another tunnel, another site) that calls the tunnel has to send `X-Duto-Auth` or `Authorization: Basic`. So opening the tunnel from a link on another `*.duto.dev` address (the dashboard or another tunnel) asks for the password again; a link from outside (email, chat) or a typed URL gets straight in.
128
128
 
129
129
  **QR code.** With the full screen open, key `m` shows the QR of the selected tunnel in place of the request list. On a tunnel without a password, the QR points to the tunnel URL. On a tunnel with a password, it points to a link that signs in directly, as the first user in the `auth` list. That link is valid for 5 minutes and for one device only: once opened, or after it expires, it falls back to the sign-in page. On the QR screen, `g` generates a new link (and invalidates the previous one) and `esc` goes back to the list.
130
130
 
@@ -0,0 +1,462 @@
1
+ #!/usr/bin/env node
2
+
3
+ // ../shared/src/names.ts
4
+ var EMAIL_SENDING_SUBDOMAINS = ["send"];
5
+ var RESERVED_NAMES = [
6
+ "www",
7
+ "api",
8
+ "app",
9
+ "dash",
10
+ "admin",
11
+ "docs",
12
+ "status",
13
+ "mail",
14
+ "custom",
15
+ "cname",
16
+ "duto",
17
+ "auth",
18
+ "login",
19
+ "signup",
20
+ "invite",
21
+ "static",
22
+ "assets",
23
+ "cdn",
24
+ "help",
25
+ "support",
26
+ "blog",
27
+ ...EMAIL_SENDING_SUBDOMAINS
28
+ ];
29
+ var MAX_NAME = 40;
30
+ var FORMAT = /^[a-z0-9]+$/;
31
+ function normalizeName(input) {
32
+ return input.trim().toLowerCase();
33
+ }
34
+ function validateName(name, max = MAX_NAME) {
35
+ if (name.length === 0 || name.length > max) return { ok: false, reason: "length" };
36
+ if (!FORMAT.test(name)) return { ok: false, reason: "format" };
37
+ if (RESERVED_NAMES.includes(name)) return { ok: false, reason: "reserved" };
38
+ return { ok: true };
39
+ }
40
+
41
+ // ../shared/src/places.ts
42
+ var PLACES = {
43
+ AAE: ["Annaba", "Algeria"],
44
+ ABJ: ["Abidjan", "C\xF4te d\u2019Ivoire"],
45
+ ABQ: ["Albuquerque", "United States"],
46
+ ACC: ["Accra", "Ghana"],
47
+ ACX: ["Xingyi", "China"],
48
+ ADB: ["Izmir", "Turkey"],
49
+ ADD: ["Addis Ababa", "Ethiopia"],
50
+ ADL: ["Adelaide", "Australia"],
51
+ AGR: ["Agra", "India"],
52
+ AIP: ["Jalandhar", "India"],
53
+ AKL: ["Auckland", "New Zealand"],
54
+ AKX: ["Aktobe", "Kazakhstan"],
55
+ ALA: ["Almaty", "Kazakhstan"],
56
+ ALG: ["Algiers", "Algeria"],
57
+ AMD: ["Ahmedabad", "India"],
58
+ AMM: ["Amman", "Jordan"],
59
+ AMS: ["Amsterdam", "Netherlands"],
60
+ ANC: ["Anchorage", "United States"],
61
+ ARI: ["Arica", "Chile"],
62
+ ARN: ["Stockholm", "Sweden"],
63
+ ARU: ["Aracatuba", "Brazil"],
64
+ ASK: ["Yamoussoukro", "C\xF4te d\u2019Ivoire"],
65
+ ASU: ["Asunci\xF3n", "Paraguay"],
66
+ ATH: ["Athens", "Greece"],
67
+ ATL: ["Atlanta", "United States"],
68
+ AUS: ["Austin", "United States"],
69
+ AVA: ["Anshun", "China"],
70
+ BAH: ["Manama", "Bahrain"],
71
+ BAQ: ["Barranquilla", "Colombia"],
72
+ BBI: ["Bhubaneswar", "India"],
73
+ BCN: ["Barcelona", "Spain"],
74
+ BDQ: ["Jamnagar", "India"],
75
+ BEG: ["Belgrade", "Serbia"],
76
+ BEL: ["Bel\xE9m", "Brazil"],
77
+ BEY: ["Beirut", "Lebanon"],
78
+ BGI: ["Bridgetown", "Barbados"],
79
+ BGR: ["Bangor", "United States"],
80
+ BGW: ["Baghdad", "Iraq"],
81
+ BKK: ["Bangkok", "Thailand"],
82
+ BLR: ["Bangalore", "India"],
83
+ BNA: ["Nashville", "United States"],
84
+ BNE: ["Brisbane", "Australia"],
85
+ BOD: ["Bordeaux", "France"],
86
+ BOG: ["Bogot\xE1", "Colombia"],
87
+ BOM: ["Mumbai", "India"],
88
+ BOS: ["Boston", "United States"],
89
+ BRU: ["Brussels", "Belgium"],
90
+ BSB: ["Brasilia", "Brazil"],
91
+ BSR: ["Basra", "Iraq"],
92
+ BTS: ["Bratislava", "Slovakia"],
93
+ BUD: ["Budapest", "Hungary"],
94
+ BWN: ["Bandar Seri Begawan", "Brunei"],
95
+ CAI: ["Cairo", "Egypt"],
96
+ CAN: ["Guangzhou", "China"],
97
+ CAW: ["Campos dos Goytacazes", "Brazil"],
98
+ CBR: ["Canberra", "Australia"],
99
+ CCU: ["Kolkata", "India"],
100
+ CDG: ["Paris", "France"],
101
+ CEB: ["Cebu", "Philippines"],
102
+ CFC: ["Cacador", "Brazil"],
103
+ CGB: ["Cuiaba", "Brazil"],
104
+ CGD: ["Changde", "China"],
105
+ CGK: ["Jakarta", "Indonesia"],
106
+ CGO: ["Zhengzhou", "China"],
107
+ CGP: ["Chittagong", "Bangladesh"],
108
+ CGY: ["Cagayan de Oro", "Philippines"],
109
+ CHC: ["Christchurch", "New Zealand"],
110
+ CJB: ["Coimbatore", "India"],
111
+ CKG: ["Chongqing", "China"],
112
+ CLE: ["Cleveland", "United States"],
113
+ CLO: ["Cali", "Colombia"],
114
+ CLT: ["Charlotte", "United States"],
115
+ CMB: ["Colombo", "Sri Lanka"],
116
+ CMH: ["Columbus", "United States"],
117
+ CNF: ["Belo Horizonte", "Brazil"],
118
+ CNN: ["Kannur", "India"],
119
+ CNX: ["Chiang Mai", "Thailand"],
120
+ COK: ["Kochi", "India"],
121
+ COR: ["C\xF3rdoba", "Argentina"],
122
+ CPH: ["Copenhagen", "Denmark"],
123
+ CPT: ["Cape Town", "South Africa"],
124
+ CRK: ["Tarlac City", "Philippines"],
125
+ CSX: ["Changsha", "China"],
126
+ CTU: ["Chengdu", "China"],
127
+ CVG: ["Cincinnati", "United States"],
128
+ CWB: ["Curitiba", "Brazil"],
129
+ CZL: ["Constantine", "Algeria"],
130
+ CZX: ["Changzhou", "China"],
131
+ DAC: ["Dhaka", "Bangladesh"],
132
+ DAD: ["Da Nang", "Vietnam"],
133
+ DAR: ["Dar es Salaam", "Tanzania"],
134
+ DEL: ["New Delhi", "India"],
135
+ DEN: ["Denver", "United States"],
136
+ DFW: ["Dallas", "United States"],
137
+ DKR: ["Dakar", "Senegal"],
138
+ DLA: ["Douala", "Cameroon"],
139
+ DLC: ["Dalian", "China"],
140
+ DME: ["Moscow", "Russia"],
141
+ DMM: ["Dammam", "Saudi Arabia"],
142
+ DOH: ["Doha", "Qatar"],
143
+ DPS: ["Denpasar", "Indonesia"],
144
+ DTW: ["Detroit", "United States"],
145
+ DUB: ["Dublin", "Ireland"],
146
+ DUR: ["Durban", "South Africa"],
147
+ DUS: ["D\xFCsseldorf", "Germany"],
148
+ DXB: ["Dubai", "United Arab Emirates"],
149
+ DYU: ["Dushanbe", "Tajikistan"],
150
+ EBB: ["Kampala", "Uganda"],
151
+ EBL: ["Erbil", "Iraq"],
152
+ EVN: ["Yerevan", "Armenia"],
153
+ EWR: ["Newark", "United States"],
154
+ EZE: ["Buenos Aires", "Argentina"],
155
+ FCO: ["Rome", "Italy"],
156
+ FLN: ["Florianopolis", "Brazil"],
157
+ FOC: ["Fuzhou", "China"],
158
+ FOR: ["Fortaleza", "Brazil"],
159
+ FRA: ["Frankfurt", "Germany"],
160
+ FRU: ["Bishkek", "Kyrgyzstan"],
161
+ FSD: ["Sioux Falls", "United States"],
162
+ FUK: ["Fukuoka", "Japan"],
163
+ FUO: ["Foshan", "China"],
164
+ GBE: ["Gaborone", "Botswana"],
165
+ GDL: ["Guadalajara", "Mexico"],
166
+ GEO: ["Georgetown", "Guyana"],
167
+ GIG: ["Rio de Janeiro", "Brazil"],
168
+ GND: ["St. George's", "Grenada"],
169
+ GOT: ["Gothenburg", "Sweden"],
170
+ GRU: ["S\xE3o Paulo", "Brazil"],
171
+ GUA: ["Guatemala City", "Guatemala"],
172
+ GUM: ["Hagatna", "Guam"],
173
+ GVA: ["Geneva", "Switzerland"],
174
+ GYD: ["Baku", "Azerbaijan"],
175
+ GYE: ["Guayaquil", "Ecuador"],
176
+ GYN: ["Goiania", "Brazil"],
177
+ HAK: ["Chengmai", "China"],
178
+ HAM: ["Hamburg", "Germany"],
179
+ HAN: ["Hanoi", "Vietnam"],
180
+ HBA: ["Hobart", "Australia"],
181
+ HEL: ["Helsinki", "Finland"],
182
+ HFA: ["Haifa", "Israel"],
183
+ HGH: ["Shaoxing", "China"],
184
+ HKG: ["Hong Kong", null],
185
+ HNL: ["Honolulu", "United States"],
186
+ HRE: ["Harare", "Zimbabwe"],
187
+ HYD: ["Hyderabad", "India"],
188
+ HYN: ["Taizhou", "China"],
189
+ IAD: ["Ashburn", "United States"],
190
+ IAH: ["Houston", "United States"],
191
+ ICN: ["Seoul", "South Korea"],
192
+ IND: ["Indianapolis", "United States"],
193
+ ISB: ["Islamabad", "Pakistan"],
194
+ IST: ["Istanbul", "Turkey"],
195
+ ISU: ["Sulaymaniyah", "Iraq"],
196
+ IXC: ["Chandigarh", "India"],
197
+ JAI: ["\u201CJaipur\u201D", "India"],
198
+ JAX: ["Jacksonville", "United States"],
199
+ JDO: ["Juazeiro do Norte", "Brazil"],
200
+ JED: ["Jeddah", "Saudi Arabia"],
201
+ JHB: ["Johor Bahru", "Malaysia"],
202
+ JIB: ["Djibouti", "Djibouti"],
203
+ JNB: ["Johannesburg", "South Africa"],
204
+ JOG: ["Yogyakarta", "Indonesia"],
205
+ JOI: ["Joinville", "Brazil"],
206
+ JRG: ["Sambalpur", "India"],
207
+ JXG: ["Jiaxing", "China"],
208
+ KBP: ["Kyiv", "Ukraine"],
209
+ KCH: ["Kuching", "Malaysia"],
210
+ KEF: ["Reykjav\xEDk", "Iceland"],
211
+ KGL: ["Kigali", "Rwanda"],
212
+ KHH: ["Kaohsiung City", null],
213
+ KHI: ["Karachi", "Pakistan"],
214
+ KHN: ["Nanchang", "China"],
215
+ KIN: ["Kingston", "Jamaica"],
216
+ KIV: ["Chi\u0219in\u0103u", "Moldova"],
217
+ KIX: ["Osaka", "Japan"],
218
+ KMG: ["Kunming", "China"],
219
+ KNU: ["Kanpur", "India"],
220
+ KTM: ["Kathmandu", "Nepal"],
221
+ KUL: ["Kuala Lumpur", "Malaysia"],
222
+ KWE: ["Guiyang", "China"],
223
+ KWI: ["Kuwait City", "Kuwait"],
224
+ LAD: ["Luanda", "Angola"],
225
+ LAS: ["Las Vegas", "United States"],
226
+ LAX: ["Los Angeles", "United States"],
227
+ LCA: ["Nicosia", "Cyprus"],
228
+ LED: ["Saint Petersburg", "Russia"],
229
+ LHE: ["Lahore", "Pakistan"],
230
+ LHR: ["London", "United Kingdom"],
231
+ LHW: ["Lanzhou", "China"],
232
+ LIM: ["Lima", "Peru"],
233
+ LIS: ["Lisbon", "Portugal"],
234
+ LJU: ["Ljubljana", "Slovenia"],
235
+ LLK: ["Astara", "Azerbaijan"],
236
+ LLW: ["Lilongwe", "Malawi"],
237
+ LOS: ["Lagos", "Nigeria"],
238
+ LPB: ["La Paz", "Bolivia"],
239
+ LUH: ["Ludhiana", "India"],
240
+ LUN: ["Lusaka", "Zambia"],
241
+ LUX: ["Luxembourg City", "Luxembourg"],
242
+ LYA: ["Luoyang", "China"],
243
+ LYS: ["Lyon", "France"],
244
+ MAA: ["Chennai", "India"],
245
+ MAD: ["Madrid", "Spain"],
246
+ MAN: ["Manchester", "United Kingdom"],
247
+ MAO: ["Manaus", "Brazil"],
248
+ MBA: ["Mombasa", "Kenya"],
249
+ MCI: ["Kansas City", "United States"],
250
+ MCT: ["Muscat", "Oman"],
251
+ MDE: ["Medell\xEDn", "Colombia"],
252
+ MEL: ["Melbourne", "Australia"],
253
+ MEM: ["Memphis", "United States"],
254
+ MEX: ["Mexico City", "Mexico"],
255
+ MFM: ["Macau", null],
256
+ MIA: ["Miami", "United States"],
257
+ MLA: ["Valletta", "Malta"],
258
+ MLE: ["Male", "Maldives"],
259
+ MLG: ["Malang", "Indonesia"],
260
+ MNL: ["Manila", "Philippines"],
261
+ MPM: ["Maputo", "Mozambique"],
262
+ MRS: ["Marseille", "France"],
263
+ MRU: ["Port Louis", "Mauritius"],
264
+ MSP: ["Minneapolis", "United States"],
265
+ MSQ: ["Minsk", "Belarus"],
266
+ MUC: ["Munich", "Germany"],
267
+ MXP: ["Milan", "Italy"],
268
+ NAG: ["Nagpur", "India"],
269
+ NBO: ["Nairobi", "Kenya"],
270
+ NJF: ["Najaf", "Iraq"],
271
+ NOU: ["Noumea", "New Caledonia"],
272
+ NQN: ["Neuquen", "Argentina"],
273
+ NQZ: ["Astana", "Kazakhstan"],
274
+ NRT: ["Tokyo", "Japan"],
275
+ NVT: ["Timbo", "Brazil"],
276
+ OKA: ["Naha", "Japan"],
277
+ OKC: ["Oklahoma City", "United States"],
278
+ OMA: ["Omaha", "United States"],
279
+ ORD: ["Chicago", "United States"],
280
+ ORF: ["Norfolk", "United States"],
281
+ ORN: ["Oran", "Algeria"],
282
+ OSL: ["Oslo", "Norway"],
283
+ OTP: ["Bucharest", "Romania"],
284
+ OUA: ["Ouagadougou", "Burkina Faso"],
285
+ PAT: ["Patna", "India"],
286
+ PBH: ["Thimphu", "Bhutan"],
287
+ PBM: ["Paramaribo", "Suriname"],
288
+ PDX: ["Portland", "United States"],
289
+ PER: ["Perth", "Australia"],
290
+ PHL: ["Philadelphia", "United States"],
291
+ PHX: ["Phoenix", "United States"],
292
+ PIT: ["Pittsburgh", "United States"],
293
+ PKX: ["Beijing", "China"],
294
+ PMO: ["Palermo", "Italy"],
295
+ PMW: ["Palmas", "Brazil"],
296
+ PNH: ["Phnom Penh", "Cambodia"],
297
+ PNQ: ["Pune", "India"],
298
+ POA: ["Porto Alegre", "Brazil"],
299
+ POS: ["Port of Spain", "Trinidad & Tobago"],
300
+ PPT: ["Tahiti", "French Polynesia"],
301
+ PRG: ["Prague", "Czechia"],
302
+ PRN: ["Prishtine", "Kosovo"],
303
+ PTY: ["Panama City", "Panama"],
304
+ QRO: ["Queretaro", "Mexico"],
305
+ QWJ: ["Americana", "Brazil"],
306
+ RAO: ["Ribeirao Preto", "Brazil"],
307
+ RDU: ["Durham", "United States"],
308
+ REC: ["Recife", "Brazil"],
309
+ RIC: ["Richmond", "United States"],
310
+ RIX: ["Riga", "Latvia"],
311
+ RUH: ["Riyadh", "Saudi Arabia"],
312
+ RUN: ["Saint-Denis", "R\xE9union"],
313
+ SAN: ["San Diego", "United States"],
314
+ SAP: ["San Pedro Sula", "Honduras"],
315
+ SAT: ["San Antonio", "United States"],
316
+ SCL: ["Santiago", "Chile"],
317
+ SDQ: ["Santo Domingo", "Dominican Republic"],
318
+ SEA: ["Seattle", "United States"],
319
+ SFO: ["San Francisco", "United States"],
320
+ SGN: ["Ho Chi Minh City", "Vietnam"],
321
+ SHA: ["Shanghai", "China"],
322
+ SIN: ["Singapore", "Singapore"],
323
+ SJC: ["San Jose", "United States"],
324
+ SJK: ["S\xE3o Jos\xE9 dos Campos", "Brazil"],
325
+ SJO: ["San Jos\xE9", "Costa Rica"],
326
+ SJP: ["S\xE3o Jos\xE9 do Rio Preto", "Brazil"],
327
+ SJU: ["San Juan", "Puerto Rico"],
328
+ SJW: ["Hengshui", "China"],
329
+ SKG: ["Thessaloniki", "Greece"],
330
+ SKP: ["Skopje", "Macedonia"],
331
+ SLC: ["Salt Lake City", "United States"],
332
+ SMF: ["Sacramento", "United States"],
333
+ SOD: ["Sorocaba", "Brazil"],
334
+ SOF: ["Sofia", "Bulgaria"],
335
+ SSA: ["Salvador", "Brazil"],
336
+ STI: ["Santiago de los Caballeros", "Dominican Republic"],
337
+ STL: ["St. Louis", "United States"],
338
+ STR: ["Stuttgart", "Germany"],
339
+ SUV: ["Suva", "Fiji"],
340
+ SYD: ["Sydney", "Australia"],
341
+ SZX: ["Shenzhen", "China"],
342
+ TAO: ["Qingdao", "China"],
343
+ TBS: ["Tbilisi", "Georgia"],
344
+ TEN: ["Tongren", "China"],
345
+ TGU: ["Tegucigalpa", "Honduras"],
346
+ TIA: ["Tirana", "Albania"],
347
+ TLH: ["Tallahassee", "United States"],
348
+ TLL: ["Tallinn", "Estonia"],
349
+ TLV: ["Tel Aviv", "Israel"],
350
+ TNA: ["Jinan", "China"],
351
+ TNR: ["Antananarivo", "Madagascar"],
352
+ TPA: ["Tampa", "United States"],
353
+ TPE: ["Taipei", null],
354
+ TUN: ["Tunis", "Tunisia"],
355
+ TXL: ["Berlin", "Germany"],
356
+ TYN: ["Yangquan", "China"],
357
+ UDI: ["Uberlandia", "Brazil"],
358
+ UDR: ["Udaipur", "India"],
359
+ UIO: ["Quito", "Ecuador"],
360
+ ULN: ["Ulaanbaatar", "Mongolia"],
361
+ URT: ["Surat Thani", "Thailand"],
362
+ VCP: ["Campinas", "Brazil"],
363
+ VIE: ["Vienna", "Austria"],
364
+ VIX: ["Vitoria", "Brazil"],
365
+ VNO: ["Vilnius", "Lithuania"],
366
+ VTE: ["Vientiane", "Laos"],
367
+ WAW: ["Warsaw", "Poland"],
368
+ WDH: ["Windhoek", "Namibia"],
369
+ WLG: ["Wellington", "New Zealand"],
370
+ WRO: ["Wroclaw", "Poland"],
371
+ XAP: ["Chapeco", "Brazil"],
372
+ XFN: ["Xiangyang", "China"],
373
+ XIY: ["Baoji", "China"],
374
+ XNH: ["Nasiriyah", "Iraq"],
375
+ YHZ: ["Halifax", "Canada"],
376
+ YUL: ["Montr\xE9al", "Canada"],
377
+ YVR: ["Vancouver", "Canada"],
378
+ YWG: ["Winnipeg", "Canada"],
379
+ YXE: ["Saskatoon", "Canada"],
380
+ YYC: ["Calgary", "Canada"],
381
+ YYZ: ["Toronto", "Canada"],
382
+ ZAG: ["Zagreb", "Croatia"],
383
+ ZDM: ["Ramallah", null],
384
+ ZRH: ["Zurich", "Switzerland"]
385
+ };
386
+ function placeOf(colo) {
387
+ const entry = colo ? PLACES[colo] : void 0;
388
+ return entry ? { city: entry[0], country: entry[1] } : null;
389
+ }
390
+
391
+ // src/ui/theme.ts
392
+ var C = {
393
+ signal: "#2dd4bf",
394
+ muted: "#8a8f8b",
395
+ strong: "#f3f4f1",
396
+ ok: "#4ade80",
397
+ warn: "#facc15",
398
+ err: "#f87171"
399
+ };
400
+ var TUNNEL_COLORS = ["#93c5fd", "#c4b5fd", "#facc15", "#4ade80", "#f9a8d4", "#fdba74", "#67e8f9"];
401
+ function tunnelColor(index) {
402
+ return TUNNEL_COLORS[index % TUNNEL_COLORS.length];
403
+ }
404
+ function fit(text, width) {
405
+ if (width <= 0) return "";
406
+ return text.length > width ? `${text.slice(0, Math.max(0, width - 1))}\u2026` : text.padEnd(width);
407
+ }
408
+
409
+ // src/session/bodyNote.ts
410
+ var TRUNCATED_NOTE = "Body cut at 64 KB.";
411
+ function bodyNote(r, side) {
412
+ if (side === "req") {
413
+ if (r.reqBodyBytes === 0) return "No body.";
414
+ if (r.reqBody === null && !r.reqTruncated) return "Binary body; not stored.";
415
+ return null;
416
+ }
417
+ if (r.bytes === 0) return "No body.";
418
+ if (r.resBody === null) return "Binary body; not stored.";
419
+ return null;
420
+ }
421
+
422
+ // src/ui/format.ts
423
+ function statusColor(status) {
424
+ if (status === null) return "muted";
425
+ if (status === 101) return "signal";
426
+ if (status >= 500) return "err";
427
+ if (status >= 400) return "warn";
428
+ if (status >= 300) return "muted";
429
+ return "ok";
430
+ }
431
+ function formatDuration(ms) {
432
+ return ms < 1e3 ? `${Math.round(ms)}ms` : `${(ms / 1e3).toFixed(1)}s`;
433
+ }
434
+ function formatBytes(n) {
435
+ if (n < 1024) return `${n} B`;
436
+ if (n < 1024 * 1024) return `${(n / 1024).toFixed(1)} KB`;
437
+ return `${(n / 1024 / 1024).toFixed(1)} MB`;
438
+ }
439
+ function formatTime(ms) {
440
+ const d = new Date(ms);
441
+ return [d.getHours(), d.getMinutes(), d.getSeconds()].map((n) => String(n).padStart(2, "0")).join(":");
442
+ }
443
+ function localLabel(t) {
444
+ return t.host ? `localhost:${t.port} (Host: ${t.host})` : `localhost:${t.port}`;
445
+ }
446
+
447
+ export {
448
+ MAX_NAME,
449
+ normalizeName,
450
+ validateName,
451
+ placeOf,
452
+ C,
453
+ tunnelColor,
454
+ fit,
455
+ TRUNCATED_NOTE,
456
+ bodyNote,
457
+ statusColor,
458
+ formatDuration,
459
+ formatBytes,
460
+ formatTime,
461
+ localLabel
462
+ };
@@ -1,6 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  C,
4
+ MAX_NAME,
4
5
  TRUNCATED_NOTE,
5
6
  bodyNote,
6
7
  fit,
@@ -12,7 +13,7 @@ import {
12
13
  statusColor,
13
14
  tunnelColor,
14
15
  validateName
15
- } from "./chunk-BPOXISIM.js";
16
+ } from "./chunk-KBH2C2UC.js";
16
17
 
17
18
  // src/ui/fullscreen.tsx
18
19
  import { render } from "ink";
@@ -369,7 +370,7 @@ function App({ session, version, system, onExit, inspector = null, inspectorErro
369
370
  const name = raw ? normalizeName(raw) : null;
370
371
  if (name !== null) {
371
372
  const v = validateName(name);
372
- if (!v.ok) return setMessage({ text: v.reason === "reserved" ? `Reserved name: "${raw}"` : "Invalid name: use lowercase letters and numbers only (no hyphens)", tone: "err" });
373
+ if (!v.ok) return setMessage({ text: v.reason === "reserved" ? `"${raw}" is reserved. Pick another name.` : `Invalid name. Use only lowercase letters and digits (no hyphens), up to ${MAX_NAME} characters.`, tone: "err" });
373
374
  }
374
375
  setMode({ kind: "list" });
375
376
  void session.add({ name, port });
package/dist/index.js CHANGED
@@ -1,6 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  C,
4
+ MAX_NAME,
4
5
  TRUNCATED_NOTE,
5
6
  bodyNote,
6
7
  formatBytes,
@@ -8,9 +9,10 @@ import {
8
9
  formatTime,
9
10
  localLabel,
10
11
  normalizeName,
12
+ placeOf,
11
13
  statusColor,
12
14
  validateName
13
- } from "./chunk-BPOXISIM.js";
15
+ } from "./chunk-KBH2C2UC.js";
14
16
 
15
17
  // src/ui/brand.ts
16
18
  function colorOn(stream = process.stdout) {
@@ -35,7 +37,7 @@ var fail = (msg) => `${paint("err", "\u2716", process.stderr)} ${msg}`;
35
37
  var warn = (msg) => `${paint("warn", "\u26A0")} ${msg}`;
36
38
 
37
39
  // src/index.ts
38
- import { hostname } from "os";
40
+ import { hostname as hostname2 } from "os";
39
41
  import { join as join6, relative } from "path";
40
42
  import cac from "cac";
41
43
 
@@ -50,21 +52,25 @@ var DutoApiError = class extends Error {
50
52
  status;
51
53
  code;
52
54
  };
55
+ var BOOKMARK_HEADER = "x-d1-bookmark";
53
56
  var MESSAGES = {
54
- unauthorized: "Invalid authtoken. Generate a new one at https://duto.dev and run: duto login <token>",
57
+ unauthorized: "This machine is not logged in. Run: duto login",
55
58
  suspended: "Your account is suspended.",
56
- network: "Could not reach duto.dev. Check your connection."
59
+ network: "Could not reach duto.dev. Check your connection.",
60
+ loginExpired: "The login code expired. Run duto login again."
57
61
  };
58
62
  function createApiClient(o) {
59
63
  const doFetch = o.fetchImpl ?? fetch;
64
+ let bookmark = null;
60
65
  async function call(method, path, body) {
61
66
  let res;
62
67
  try {
63
68
  res = await doFetch(`${o.baseUrl}${path}`, {
64
69
  method,
65
70
  headers: {
66
- authorization: `Bearer ${o.token}`,
71
+ ...o.token === void 0 ? {} : { authorization: `Bearer ${o.token}` },
67
72
  ...body === void 0 ? {} : { "content-type": "application/json" },
73
+ ...bookmark === null ? {} : { [BOOKMARK_HEADER]: bookmark },
68
74
  "user-agent": "duto-cli"
69
75
  },
70
76
  body: body === void 0 ? void 0 : JSON.stringify(body)
@@ -72,6 +78,7 @@ function createApiClient(o) {
72
78
  } catch {
73
79
  throw new DutoApiError(MESSAGES.network, 0, "network");
74
80
  }
81
+ bookmark = res.headers.get(BOOKMARK_HEADER) ?? bookmark;
75
82
  return res;
76
83
  }
77
84
  async function fail3(res) {
@@ -98,6 +105,12 @@ function createApiClient(o) {
98
105
  const res = await call("DELETE", `/api/tunnels/${encodeURIComponent(id)}`);
99
106
  if (!res.ok && res.status !== 404) await fail3(res);
100
107
  },
108
+ startLogin: (r) => json("POST", "/api/cli/login", r),
109
+ pollLogin: (deviceCode) => json("POST", "/api/cli/login/poll", { deviceCode }),
110
+ async revokeSelf() {
111
+ const res = await call("DELETE", "/api/cli/token");
112
+ if (!res.ok && res.status !== 401 && res.status !== 404) await fail3(res);
113
+ },
101
114
  me: () => json("GET", "/api/me"),
102
115
  endpoints: () => json("GET", "/api/endpoints")
103
116
  };
@@ -417,7 +430,7 @@ function parseConfig(text, file = CONFIG_FILES[0], env = process.env) {
417
430
  const v = validateName(name);
418
431
  if (!v.ok) {
419
432
  throw fail3(
420
- v.reason === "reserved" ? `reserved name: "${raw}". Choose another.` : `invalid name: "${raw}" (use only lowercase letters and digits, no hyphen)`,
433
+ v.reason === "reserved" ? `"${raw}" is reserved. Pick another name.` : `invalid name: "${raw}" (use only lowercase letters and digits, no hyphen)`,
421
434
  line
422
435
  );
423
436
  }
@@ -522,6 +535,108 @@ function findConfig(cwd) {
522
535
  }
523
536
  }
524
537
 
538
+ // src/login.ts
539
+ import { spawn } from "child_process";
540
+ import { hostname } from "os";
541
+ var SLOW_DOWN_MS = 5e3;
542
+ var MAX_NETWORK_ERRORS_IN_A_ROW = 3;
543
+ async function browserLogin(o) {
544
+ const api = createApiClient({ baseUrl: o.baseUrl, fetchImpl: o.fetchImpl });
545
+ const start = await api.startLogin({
546
+ deviceId: o.deviceId,
547
+ deviceName: hostname(),
548
+ cliVersion: "1.2.0",
549
+ nodeVersion: process.version,
550
+ os: process.platform,
551
+ arch: process.arch
552
+ });
553
+ o.out(` Visit ${start.verificationUrl}`);
554
+ tryOpen(o.open, start.verificationUrl);
555
+ let waitMs = start.interval * 1e3;
556
+ let networkErrors = 0;
557
+ for (; ; ) {
558
+ await o.sleep(waitMs);
559
+ let poll;
560
+ try {
561
+ poll = await api.pollLogin(start.deviceCode);
562
+ networkErrors = 0;
563
+ } catch (err) {
564
+ const isNetwork = err instanceof DutoApiError && err.code === "network";
565
+ if (!isNetwork || ++networkErrors > MAX_NETWORK_ERRORS_IN_A_ROW) throw err;
566
+ continue;
567
+ }
568
+ if (poll.status === "approved") return { token: poll.token, username: poll.username };
569
+ if (poll.status === "expired") throw new DutoApiError(MESSAGES.loginExpired, 410, "expired");
570
+ if (poll.status === "slow_down") waitMs += SLOW_DOWN_MS;
571
+ }
572
+ }
573
+ function tryOpen(open, url) {
574
+ try {
575
+ open(url);
576
+ } catch {
577
+ return;
578
+ }
579
+ }
580
+ function openCommand(url) {
581
+ if (process.platform === "darwin") return { command: "open", args: [url] };
582
+ if (process.platform === "win32") return { command: "cmd", args: ["/c", "start", '""', url] };
583
+ return { command: "xdg-open", args: [url] };
584
+ }
585
+ function loggedInLine(username) {
586
+ return ok(`Logged in as ${username}.`);
587
+ }
588
+ function loginSuccessLines(username) {
589
+ return [
590
+ loggedInLine(username),
591
+ "",
592
+ "To open a tunnel, run `duto 3000`, or `duto` in a folder with a duto.yml."
593
+ ];
594
+ }
595
+ function openBrowser(url) {
596
+ try {
597
+ const { command, args } = openCommand(url);
598
+ const child = spawn(command, args, { detached: true, stdio: "ignore" });
599
+ child.on("error", () => {
600
+ });
601
+ child.unref();
602
+ } catch {
603
+ return;
604
+ }
605
+ }
606
+ var FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
607
+ function startSpinner(label, stream = process.stderr) {
608
+ if (!stream.isTTY) {
609
+ stream.write(`${label}
610
+ `);
611
+ return () => {
612
+ };
613
+ }
614
+ let frame = 0;
615
+ const draw = () => stream.write(`\r${FRAMES[frame++ % FRAMES.length]} ${label}`);
616
+ draw();
617
+ const timer = setInterval(draw, 80);
618
+ return () => {
619
+ clearInterval(timer);
620
+ stream.write("\r\x1B[K");
621
+ };
622
+ }
623
+
624
+ // src/inspector/edge.ts
625
+ var RAY_COLO = /-([A-Za-z]{3})$/;
626
+ function edgeOf(headers) {
627
+ const name = Object.keys(headers).find((k) => k.toLowerCase() === "cf-ray");
628
+ const code = name ? RAY_COLO.exec(headers[name] ?? "")?.[1] : void 0;
629
+ if (!code) return null;
630
+ const colo = code.toUpperCase();
631
+ const place = placeOf(colo);
632
+ return { colo, city: place?.city ?? null, country: place?.country ?? null };
633
+ }
634
+ function edgeLabel(edge) {
635
+ if (!edge) return "";
636
+ const place = [edge.city, edge.country].filter(Boolean).join(", ");
637
+ return place ? "via " + edge.colo + " (" + place + ")" : "via " + edge.colo;
638
+ }
639
+
525
640
  // src/inspector/jsonView.ts
526
641
  function countLabel(n, one, many) {
527
642
  return n + " " + (n === 1 ? one : many);
@@ -702,6 +817,7 @@ th { color: ${JSON_KEY}; font-weight: 400; white-space: nowrap; }
702
817
  `;
703
818
  var SCRIPT = String.raw`"use strict";
704
819
  var countLabel = (${countLabel.toString()});
820
+ var edgeLabel = (${edgeLabel.toString()});
705
821
  var jsonSyntaxError = (${jsonSyntaxError.toString()});
706
822
  var jsonTokens = (${jsonTokens.toString()});
707
823
  var jsonIndent = (${jsonIndent.toString()});
@@ -875,6 +991,7 @@ function renderDetail() {
875
991
  if (duration(d)) parts.push(duration(d));
876
992
  parts.push(size(d.bytes));
877
993
  parts.push(d.tunnel);
994
+ if (d.edge) parts.push(edgeLabel(d.edge));
878
995
  if (d.replayOf !== undefined) parts.push("↻ replay of #" + d.replayOf);
879
996
  head.appendChild(el("span", "mut", " · " + parts.join(" · ")));
880
997
  if (d.error) head.appendChild(el("div", "msg err", d.error));
@@ -1268,6 +1385,7 @@ function detail(r, session) {
1268
1385
  }
1269
1386
  return {
1270
1387
  ...listItem(r),
1388
+ edge: edgeOf(r.reqHeaders),
1271
1389
  reqHeaders: r.reqHeaders,
1272
1390
  resHeaders: r.resHeaders,
1273
1391
  reqBody: r.reqBody,
@@ -1431,7 +1549,7 @@ data: ${JSON.stringify({ ids })}
1431
1549
  }
1432
1550
 
1433
1551
  // src/session/session.ts
1434
- import { spawn } from "child_process";
1552
+ import { spawn as spawn2 } from "child_process";
1435
1553
 
1436
1554
  // src/portcheck.ts
1437
1555
  import { connect } from "net";
@@ -1455,7 +1573,7 @@ async function isPortListening(port, timeoutMs = 500) {
1455
1573
  // src/session/auth.ts
1456
1574
  import { createHash as createHash3, createHmac, randomBytes as randomBytes2, timingSafeEqual as timingSafeEqual2 } from "crypto";
1457
1575
  var TUNNEL_COOKIE = "__Host-duto_tunnel";
1458
- var SAME_SITE_SIBLING_TUNNEL_COOKIE_REJECTED = "same-site";
1576
+ var SEC_FETCH_SITE_SAME_SITE = "same-site";
1459
1577
  var QR_TTL_MS = 5 * 6e4;
1460
1578
  var sha2 = (s) => createHash3("sha256").update(s).digest();
1461
1579
  var constantTimeEqual2 = (a, b) => timingSafeEqual2(sha2(a), sha2(b));
@@ -1470,7 +1588,7 @@ function parseBasic(value) {
1470
1588
  var first2 = (v) => Array.isArray(v) ? v[0] : v;
1471
1589
  var bareHost = (s) => s.replace(/:\d+$/, "").toLowerCase();
1472
1590
  function sameHostOrigin(h) {
1473
- if (first2(h["sec-fetch-site"])?.toLowerCase() === SAME_SITE_SIBLING_TUNNEL_COOKIE_REJECTED) return false;
1591
+ if (first2(h["sec-fetch-site"])?.toLowerCase() === SEC_FETCH_SITE_SAME_SITE) return false;
1474
1592
  const origin = first2(h.origin);
1475
1593
  if (origin === void 0) return true;
1476
1594
  let host;
@@ -1694,7 +1812,7 @@ function loginPage(o) {
1694
1812
  ${o.error ? `<p class="error" role="alert">Wrong username or password.</p>
1695
1813
  ` : ""}<button type="submit">Sign in</button>
1696
1814
  </form>
1697
- <div class="retry">You stay signed in until the tunnel closes.</div>
1815
+ <div class="retry">You stay signed in until you close the browser or the tunnel closes.</div>
1698
1816
  </main>`,
1699
1817
  CSS
1700
1818
  );
@@ -2390,7 +2508,7 @@ var Session = class {
2390
2508
  if (this.child && this.token === token) return;
2391
2509
  if (this.child) this.child.kill("SIGTERM");
2392
2510
  this.token = token;
2393
- const c = spawn(this.o.cloudflaredPath, ["tunnel", "--no-autoupdate", "run"], {
2511
+ const c = spawn2(this.o.cloudflaredPath, ["tunnel", "--no-autoupdate", "run"], {
2394
2512
  env: { ...process.env, TUNNEL_TOKEN: token },
2395
2513
  stdio: ["ignore", "ignore", "pipe"]
2396
2514
  });
@@ -2519,7 +2637,7 @@ function runPlain(session, out) {
2519
2637
  }
2520
2638
 
2521
2639
  // src/index.ts
2522
- var LOGIN_HINT = "Log in first: duto login <token> (get your token at https://duto.dev)";
2640
+ var LOGIN_HINT = "Log in first: duto login";
2523
2641
  function fail2(message2) {
2524
2642
  console.error(fail(message2));
2525
2643
  process.exit(1);
@@ -2534,7 +2652,7 @@ function parseName(raw) {
2534
2652
  if (raw === void 0 || raw === true || raw === "") return void 0;
2535
2653
  const name = normalizeName(String(raw));
2536
2654
  const v = validateName(name);
2537
- if (!v.ok) fail2(v.reason === "reserved" ? `Reserved name: "${name}". Pick another.` : "Invalid name: use lowercase letters and numbers only (no hyphens)");
2655
+ if (!v.ok) fail2(v.reason === "reserved" ? `"${name}" is reserved. Pick another name.` : `Invalid name. Use only lowercase letters and digits (no hyphens), up to ${MAX_NAME} characters.`);
2538
2656
  return name;
2539
2657
  }
2540
2658
  function parseHostFlag(raw) {
@@ -2574,7 +2692,7 @@ async function runSession(specs, source, flags) {
2574
2692
  } catch (err) {
2575
2693
  fail2(err instanceof Error ? err.message : String(err));
2576
2694
  }
2577
- const session = new Session({ api, deviceId: config.deviceId, deviceName: hostname(), cloudflaredPath, source, runDir: join6(dutoHome(), "run") });
2695
+ const session = new Session({ api, deviceId: config.deviceId, deviceName: hostname2(), cloudflaredPath, source, runDir: join6(dutoHome(), "run") });
2578
2696
  let inspector = null;
2579
2697
  let inspectorError;
2580
2698
  if (flags.inspect !== false) {
@@ -2585,12 +2703,12 @@ async function runSession(specs, source, flags) {
2585
2703
  }
2586
2704
  }
2587
2705
  if (wantsUi(flags)) {
2588
- const { runFullscreen } = await import("./fullscreen-IWZCH3IP.js");
2589
- const code = await runFullscreen(session, specs, "1.0.0", inspector, inspectorError);
2706
+ const { runFullscreen } = await import("./fullscreen-2I3U6JL7.js");
2707
+ const code = await runFullscreen(session, specs, "1.2.0", inspector, inspectorError);
2590
2708
  await inspector?.close().catch(() => void 0);
2591
2709
  process.exit(code);
2592
2710
  }
2593
- console.log(`${mark()} ${paint("muted", `${"1.0.0"} \xB7 ${source} \xB7 Ctrl+C to stop`)}`);
2711
+ console.log(`${mark()} ${paint("muted", `${"1.2.0"} \xB7 ${source} \xB7 Ctrl+C to stop`)}`);
2594
2712
  if (inspector) console.log(paint("muted", `inspector: ${inspector.openUrl}`));
2595
2713
  if (inspectorError) console.log(warn(`inspector failed to start: ${inspectorError}`));
2596
2714
  const stopLog = runPlain(session, (l) => console.log(l));
@@ -2649,19 +2767,56 @@ ${paint("signal", "Tip:")} create a duto.yml with your project's tunnels and jus
2649
2767
  var cli = cac("duto");
2650
2768
  cli.command("[port]", "No port: open the tunnels in duto.yml. With a port: open a tunnel to it").option("--name <name>", "Fixed address name (e.g. web \u2192 https://web-<user>.duto.dev)").option("--host <host>", "Host the local service receives instead of the public one (e.g. myapp.test)").option("--no-rewrite-redirects", "With --host: do not swap the local host for the public one in redirects").option("--auth <user:password>", "Ask for a username and password to open the address (repeatable)").option("--allow <range>", "With --auth: IP, CIDR range or name (stripe, github) that gets in without a password (repeatable)").option("--no-ui", "No full screen: just a request log (useful in scripts)").option("--no-inspect", "Do not start the browser inspector (127.0.0.1:4141)").option("--ui", "Force full screen (off by default on Windows)").action((port, opts) => tunnel(port, opts));
2651
2769
  cli.command("http <port>", "Same as duto <port> (for people coming from ngrok)").option("--name <name>", "Fixed address name").option("--host <host>", "Host the local service receives instead of the public one").option("--no-rewrite-redirects", "With --host: keep redirects as the service sent them").option("--auth <user:password>", "Ask for a username and password to open the address (repeatable)").option("--allow <range>", "With --auth: IP, CIDR range or name (stripe, github) that gets in without a password (repeatable)").option("--no-ui", "No full screen").option("--no-inspect", "Do not start the browser inspector (127.0.0.1:4141)").action((port, opts) => tunnel(port, opts));
2652
- cli.command("login <token>", "Save your authtoken (get it at https://duto.dev)").action(async (token) => {
2770
+ cli.command("login [token]", "Log in through the browser, or save an authtoken").action(async (token) => {
2653
2771
  try {
2772
+ if (token === void 0) return await loginWithBrowser();
2654
2773
  const me = await createApiClient({ baseUrl: apiUrl(), token: String(token) }).me();
2655
2774
  saveConfig({ ...loadConfig(), authtoken: String(token) });
2656
- console.log(ok(`Logged in as ${paint("strong", me.username ?? me.email)}`));
2775
+ console.log(loggedInLine(me.username ?? me.email));
2657
2776
  } catch (err) {
2658
2777
  fail2(err instanceof Error ? err.message : String(err));
2659
2778
  }
2660
2779
  });
2661
- cli.command("logout", "Remove the authtoken from this machine").action(() => {
2662
- const { authtoken: _removed, ...rest } = loadConfig();
2780
+ async function loginWithBrowser() {
2781
+ const config = loadConfig();
2782
+ let stopSpinner = () => {
2783
+ };
2784
+ try {
2785
+ const { token, username } = await browserLogin({
2786
+ baseUrl: apiUrl(),
2787
+ deviceId: config.deviceId,
2788
+ open: openBrowser,
2789
+ sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
2790
+ out: (line) => {
2791
+ console.log(line);
2792
+ stopSpinner = startSpinner("Waiting for authentication...");
2793
+ }
2794
+ });
2795
+ stopSpinner();
2796
+ saveConfig({ ...config, authtoken: token });
2797
+ for (const line of loginSuccessLines(username)) console.log(line);
2798
+ } catch (err) {
2799
+ stopSpinner();
2800
+ throw err;
2801
+ }
2802
+ }
2803
+ cli.command("logout", "Remove the authtoken from this machine").action(async () => {
2804
+ const { authtoken, ...rest } = loadConfig();
2805
+ let reachable = true;
2663
2806
  saveConfig(rest);
2664
- console.log(ok("Authtoken removed"));
2807
+ if (authtoken) {
2808
+ try {
2809
+ await createApiClient({ baseUrl: apiUrl(), token: authtoken }).revokeSelf();
2810
+ } catch (err) {
2811
+ if (!(err instanceof DutoApiError) || err.code !== "network") fail2(err instanceof Error ? err.message : String(err));
2812
+ reachable = false;
2813
+ }
2814
+ }
2815
+ console.log(
2816
+ ok(
2817
+ reachable ? "Authtoken removed" : "Authtoken removed from this machine. Could not reach duto.dev; revoke it on the Machines page."
2818
+ )
2819
+ );
2665
2820
  });
2666
2821
  cli.command("status", "Show your account and live tunnels").action(async () => {
2667
2822
  const config = loadConfig();
@@ -2681,7 +2836,7 @@ cli.command("status", "Show your account and live tunnels").action(async () => {
2681
2836
  }
2682
2837
  });
2683
2838
  cli.help();
2684
- cli.version("1.0.0");
2839
+ cli.version("1.2.0");
2685
2840
  try {
2686
2841
  cli.parse(process.argv, { run: false });
2687
2842
  await cli.runMatchedCommand();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dutodev/cli",
3
- "version": "1.0.0",
3
+ "version": "1.2.0",
4
4
  "description": "Expose a local port at https://<name>-<user>.duto.dev with one command.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -1,110 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- // ../shared/src/names.ts
4
- var EMAIL_SENDING_SUBDOMAINS = ["send"];
5
- var RESERVED_NAMES = [
6
- "www",
7
- "api",
8
- "app",
9
- "dash",
10
- "admin",
11
- "docs",
12
- "status",
13
- "mail",
14
- "custom",
15
- "cname",
16
- "duto",
17
- "auth",
18
- "login",
19
- "signup",
20
- "invite",
21
- "static",
22
- "assets",
23
- "cdn",
24
- "help",
25
- "support",
26
- "blog",
27
- ...EMAIL_SENDING_SUBDOMAINS
28
- ];
29
- var MAX_NAME = 40;
30
- var FORMAT = /^[a-z0-9]+$/;
31
- function normalizeName(input) {
32
- return input.trim().toLowerCase();
33
- }
34
- function validateName(name, max = MAX_NAME) {
35
- if (name.length === 0 || name.length > max) return { ok: false, reason: "length" };
36
- if (!FORMAT.test(name)) return { ok: false, reason: "format" };
37
- if (RESERVED_NAMES.includes(name)) return { ok: false, reason: "reserved" };
38
- return { ok: true };
39
- }
40
-
41
- // src/ui/theme.ts
42
- var C = {
43
- signal: "#2dd4bf",
44
- muted: "#8a8f8b",
45
- strong: "#f3f4f1",
46
- ok: "#4ade80",
47
- warn: "#facc15",
48
- err: "#f87171"
49
- };
50
- var TUNNEL_COLORS = ["#93c5fd", "#c4b5fd", "#facc15", "#4ade80", "#f9a8d4", "#fdba74", "#67e8f9"];
51
- function tunnelColor(index) {
52
- return TUNNEL_COLORS[index % TUNNEL_COLORS.length];
53
- }
54
- function fit(text, width) {
55
- if (width <= 0) return "";
56
- return text.length > width ? `${text.slice(0, Math.max(0, width - 1))}\u2026` : text.padEnd(width);
57
- }
58
-
59
- // src/session/bodyNote.ts
60
- var TRUNCATED_NOTE = "Body cut at 64 KB.";
61
- function bodyNote(r, side) {
62
- if (side === "req") {
63
- if (r.reqBodyBytes === 0) return "No body.";
64
- if (r.reqBody === null && !r.reqTruncated) return "Binary body; not stored.";
65
- return null;
66
- }
67
- if (r.bytes === 0) return "No body.";
68
- if (r.resBody === null) return "Binary body; not stored.";
69
- return null;
70
- }
71
-
72
- // src/ui/format.ts
73
- function statusColor(status) {
74
- if (status === null) return "muted";
75
- if (status === 101) return "signal";
76
- if (status >= 500) return "err";
77
- if (status >= 400) return "warn";
78
- if (status >= 300) return "muted";
79
- return "ok";
80
- }
81
- function formatDuration(ms) {
82
- return ms < 1e3 ? `${Math.round(ms)}ms` : `${(ms / 1e3).toFixed(1)}s`;
83
- }
84
- function formatBytes(n) {
85
- if (n < 1024) return `${n} B`;
86
- if (n < 1024 * 1024) return `${(n / 1024).toFixed(1)} KB`;
87
- return `${(n / 1024 / 1024).toFixed(1)} MB`;
88
- }
89
- function formatTime(ms) {
90
- const d = new Date(ms);
91
- return [d.getHours(), d.getMinutes(), d.getSeconds()].map((n) => String(n).padStart(2, "0")).join(":");
92
- }
93
- function localLabel(t) {
94
- return t.host ? `localhost:${t.port} (Host: ${t.host})` : `localhost:${t.port}`;
95
- }
96
-
97
- export {
98
- normalizeName,
99
- validateName,
100
- C,
101
- tunnelColor,
102
- fit,
103
- TRUNCATED_NOTE,
104
- bodyNote,
105
- statusColor,
106
- formatDuration,
107
- formatBytes,
108
- formatTime,
109
- localLabel
110
- };