frizz 0.13.7 → 0.13.8
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 +7 -7
- package/dist/frizz.js +1770 -1457
- package/package.json +2 -2
package/dist/frizz.js
CHANGED
|
@@ -180,6 +180,324 @@ var init_local_origin = __esm({
|
|
|
180
180
|
}
|
|
181
181
|
});
|
|
182
182
|
|
|
183
|
+
// packages/server/src/access-codes.ts
|
|
184
|
+
import { randomBytes, timingSafeEqual, createHmac } from "node:crypto";
|
|
185
|
+
import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
186
|
+
import { dirname, join as join2 } from "node:path";
|
|
187
|
+
function memorySessionDirectory(seed = []) {
|
|
188
|
+
const records = new Map(seed.map((r) => [r.id, r]));
|
|
189
|
+
return {
|
|
190
|
+
record: (r) => void records.set(r.id, r),
|
|
191
|
+
isRevoked: (id) => records.get(id)?.revokedAt !== void 0,
|
|
192
|
+
// Reverse FIRST, then sort. Array.sort is stable, so two devices that redeemed in the same
|
|
193
|
+
// millisecond — which two taps on one page reliably are — keep newest-inserted-first instead of
|
|
194
|
+
// falling back to insertion order and reading as oldest first.
|
|
195
|
+
list: () => [...records.values()].reverse().sort((a, b) => b.createdAt - a.createdAt),
|
|
196
|
+
revoke(id) {
|
|
197
|
+
const found = records.get(id);
|
|
198
|
+
if (!found || found.revokedAt !== void 0) return false;
|
|
199
|
+
found.revokedAt = Date.now();
|
|
200
|
+
return true;
|
|
201
|
+
},
|
|
202
|
+
revokeAll() {
|
|
203
|
+
let n = 0;
|
|
204
|
+
const now = Date.now();
|
|
205
|
+
for (const r of records.values()) {
|
|
206
|
+
if (r.revokedAt !== void 0) continue;
|
|
207
|
+
r.revokedAt = now;
|
|
208
|
+
if (r.expiresAt === void 0 || r.expiresAt > now) n++;
|
|
209
|
+
}
|
|
210
|
+
return n;
|
|
211
|
+
}
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
function describeDevice(userAgent) {
|
|
215
|
+
const ua = userAgent ?? "";
|
|
216
|
+
if (!ua.trim()) return "unknown device";
|
|
217
|
+
const os = /iPhone/i.test(ua) ? "iPhone" : /iPad/i.test(ua) ? "iPad" : /Android/i.test(ua) ? "Android" : /Mac OS X|Macintosh/i.test(ua) ? "macOS" : /Windows/i.test(ua) ? "Windows" : /Linux/i.test(ua) ? "Linux" : "unknown device";
|
|
218
|
+
const browser = /Edg\//i.test(ua) ? "Edge" : /OPR\/|Opera/i.test(ua) ? "Opera" : /Firefox\//i.test(ua) ? "Firefox" : /Chrome\//i.test(ua) ? "Chrome" : /Safari\//i.test(ua) ? "Safari" : "";
|
|
219
|
+
return browser ? `${browser} on ${os}` : os;
|
|
220
|
+
}
|
|
221
|
+
function defaultRandomToken(bytes) {
|
|
222
|
+
return randomBytes(bytes).toString("base64url");
|
|
223
|
+
}
|
|
224
|
+
function secretsMatch(a, b) {
|
|
225
|
+
const left = Buffer.from(a);
|
|
226
|
+
const right = Buffer.from(b);
|
|
227
|
+
return left.length === right.length && timingSafeEqual(left, right);
|
|
228
|
+
}
|
|
229
|
+
function fileSessionDirectory(stateDir, now = Date.now) {
|
|
230
|
+
const path = join2(stateDir, "sessions.json");
|
|
231
|
+
let records = [];
|
|
232
|
+
try {
|
|
233
|
+
const parsed = JSON.parse(readFileSync(path, "utf8"));
|
|
234
|
+
if (Array.isArray(parsed)) {
|
|
235
|
+
records = parsed.filter(
|
|
236
|
+
(r) => !!r && typeof r.id === "string" && typeof r.createdAt === "number"
|
|
237
|
+
);
|
|
238
|
+
}
|
|
239
|
+
} catch {
|
|
240
|
+
}
|
|
241
|
+
const inner = memorySessionDirectory(records);
|
|
242
|
+
const flush = () => {
|
|
243
|
+
try {
|
|
244
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
245
|
+
writeFileSync(path, JSON.stringify(inner.list()), { mode: 384 });
|
|
246
|
+
} catch (error) {
|
|
247
|
+
if (process.env.FRIZZ_DEBUG_SESSIONS) console.error("[sessions] flush failed:", error);
|
|
248
|
+
}
|
|
249
|
+
};
|
|
250
|
+
return {
|
|
251
|
+
record(r) {
|
|
252
|
+
inner.record(r);
|
|
253
|
+
flush();
|
|
254
|
+
},
|
|
255
|
+
isRevoked: (id) => inner.isRevoked(id),
|
|
256
|
+
list: () => inner.list(),
|
|
257
|
+
revoke(id) {
|
|
258
|
+
const ok = inner.revoke(id);
|
|
259
|
+
if (ok) flush();
|
|
260
|
+
return ok;
|
|
261
|
+
},
|
|
262
|
+
revokeAll() {
|
|
263
|
+
const n = inner.revokeAll();
|
|
264
|
+
if (n > 0) flush();
|
|
265
|
+
return n;
|
|
266
|
+
}
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
function writeSessionKey(path, key) {
|
|
270
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
271
|
+
writeFileSync(path, key, { mode: 384 });
|
|
272
|
+
try {
|
|
273
|
+
chmodSync(path, 384);
|
|
274
|
+
} catch {
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
function loadOrCreateSessionKey(stateDir) {
|
|
278
|
+
const path = join2(stateDir, SESSION_KEY_FILE);
|
|
279
|
+
try {
|
|
280
|
+
const existing = readFileSync(path);
|
|
281
|
+
if (existing.byteLength >= 32) return existing;
|
|
282
|
+
} catch {
|
|
283
|
+
}
|
|
284
|
+
return rotateSessionKey(stateDir);
|
|
285
|
+
}
|
|
286
|
+
function rotateSessionKey(stateDir) {
|
|
287
|
+
const key = randomBytes(32);
|
|
288
|
+
writeSessionKey(join2(stateDir, SESSION_KEY_FILE), key);
|
|
289
|
+
return key;
|
|
290
|
+
}
|
|
291
|
+
function readSessionEpoch(stateDir) {
|
|
292
|
+
try {
|
|
293
|
+
const parsed = JSON.parse(readFileSync(join2(stateDir, SESSION_EPOCH_FILE), "utf8"));
|
|
294
|
+
return Number.isInteger(parsed?.epoch) && parsed.epoch >= 0 ? parsed.epoch : 0;
|
|
295
|
+
} catch {
|
|
296
|
+
return 0;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
function advanceSessionEpoch(stateDir, epoch = SESSION_EPOCH) {
|
|
300
|
+
const from = readSessionEpoch(stateDir);
|
|
301
|
+
if (from >= epoch) return { advanced: false, epoch: from };
|
|
302
|
+
const keyPath = join2(stateDir, SESSION_KEY_FILE);
|
|
303
|
+
const rotatedKey = existsSync(keyPath);
|
|
304
|
+
if (rotatedKey) rotateSessionKey(stateDir);
|
|
305
|
+
mkdirSync(stateDir, { recursive: true });
|
|
306
|
+
writeFileSync(
|
|
307
|
+
join2(stateDir, SESSION_EPOCH_FILE),
|
|
308
|
+
JSON.stringify({ epoch, advancedAt: (/* @__PURE__ */ new Date()).toISOString() }),
|
|
309
|
+
{ mode: 384 }
|
|
310
|
+
);
|
|
311
|
+
return { advanced: true, from, to: epoch, rotatedKey };
|
|
312
|
+
}
|
|
313
|
+
function sessionEpochNotice(count) {
|
|
314
|
+
if (count <= 0) return null;
|
|
315
|
+
const devices = count === 1 ? "1 remote device" : `${count} remote devices`;
|
|
316
|
+
return `Security update: signed out ${devices} (a phone or browser reaching this board through frizz.sh or a tunnel). Each must sign in again with a fresh link.`;
|
|
317
|
+
}
|
|
318
|
+
function sessionEpochLogLine(advance, signedOut) {
|
|
319
|
+
if (!advance.advanced || !advance.rotatedKey) return null;
|
|
320
|
+
const count = signedOut === null ? "an unknown number of" : String(signedOut);
|
|
321
|
+
return `session epoch ${advance.from} -> ${advance.to}: rotated the session key and signed out ${count} recorded remote device(s); every older remote session is refused`;
|
|
322
|
+
}
|
|
323
|
+
function loadSessionState(stateDir, epoch = SESSION_EPOCH) {
|
|
324
|
+
const advance = advanceSessionEpoch(stateDir, epoch);
|
|
325
|
+
const key = loadOrCreateSessionKey(stateDir);
|
|
326
|
+
const directory = fileSessionDirectory(stateDir);
|
|
327
|
+
const signedOut = advance.advanced ? directory.revokeAll() : 0;
|
|
328
|
+
return { key, directory, rotateKey: () => rotateSessionKey(stateDir), advance, signedOut };
|
|
329
|
+
}
|
|
330
|
+
var CODE_BYTES, SESSION_BYTES, SESSION_ID_BYTES, DEFAULT_CODE_TTL_MS, DEFAULT_SESSION_TTL_MS, SESSION_EPOCH, AccessStore, SESSION_KEY_FILE, SESSION_EPOCH_FILE;
|
|
331
|
+
var init_access_codes = __esm({
|
|
332
|
+
"packages/server/src/access-codes.ts"() {
|
|
333
|
+
"use strict";
|
|
334
|
+
CODE_BYTES = 16;
|
|
335
|
+
SESSION_BYTES = 32;
|
|
336
|
+
SESSION_ID_BYTES = 6;
|
|
337
|
+
DEFAULT_CODE_TTL_MS = 5 * 6e4;
|
|
338
|
+
DEFAULT_SESSION_TTL_MS = 30 * 24 * 60 * 6e4;
|
|
339
|
+
SESSION_EPOCH = 1;
|
|
340
|
+
AccessStore = class _AccessStore {
|
|
341
|
+
codes = /* @__PURE__ */ new Map();
|
|
342
|
+
options;
|
|
343
|
+
sessions;
|
|
344
|
+
/** Signing key for sessions. Persisted by the caller; a fresh one signs every device out. */
|
|
345
|
+
signingKey;
|
|
346
|
+
rotateSigningKey;
|
|
347
|
+
constructor(options2 = {}) {
|
|
348
|
+
this.signingKey = options2.signingKey ?? randomBytes(32);
|
|
349
|
+
this.rotateSigningKey = options2.rotateSigningKey ?? (() => randomBytes(32));
|
|
350
|
+
this.sessions = options2.sessions ?? memorySessionDirectory();
|
|
351
|
+
this.options = {
|
|
352
|
+
codeTtlMs: options2.codeTtlMs ?? DEFAULT_CODE_TTL_MS,
|
|
353
|
+
sessionTtlMs: options2.sessionTtlMs ?? DEFAULT_SESSION_TTL_MS,
|
|
354
|
+
now: options2.now ?? Date.now,
|
|
355
|
+
randomToken: options2.randomToken ?? defaultRandomToken,
|
|
356
|
+
onConsumed: options2.onConsumed
|
|
357
|
+
};
|
|
358
|
+
}
|
|
359
|
+
/** Mint a code to show as a QR or a link. Minting does not invalidate codes already outstanding. */
|
|
360
|
+
issue() {
|
|
361
|
+
this.sweep();
|
|
362
|
+
const now = this.options.now();
|
|
363
|
+
const code = {
|
|
364
|
+
code: this.options.randomToken(CODE_BYTES),
|
|
365
|
+
createdAt: now,
|
|
366
|
+
expiresAt: now + this.options.codeTtlMs
|
|
367
|
+
};
|
|
368
|
+
this.codes.set(code.code, code);
|
|
369
|
+
return { code: code.code, createdAt: code.createdAt, expiresAt: code.expiresAt };
|
|
370
|
+
}
|
|
371
|
+
/**
|
|
372
|
+
* Trade a code for a session, at most once, ever.
|
|
373
|
+
*
|
|
374
|
+
* The consumed marker is set BEFORE anything else can observe success, and JS runs this method to
|
|
375
|
+
* completion without interleaving, so two browsers racing the same code cannot both win. That
|
|
376
|
+
* atomicity is what makes "single-use" true rather than aspirational — it is the first thing the
|
|
377
|
+
* tests attack.
|
|
378
|
+
*/
|
|
379
|
+
redeem(code, device) {
|
|
380
|
+
if (!code) return { ok: false, reason: "unknown" };
|
|
381
|
+
const stored = this.codes.get(code);
|
|
382
|
+
if (!stored || !secretsMatch(stored.code, code)) return { ok: false, reason: "unknown" };
|
|
383
|
+
const now = this.options.now();
|
|
384
|
+
if (stored.consumedAt !== void 0) return { ok: false, reason: "already-used" };
|
|
385
|
+
if (now >= stored.expiresAt) {
|
|
386
|
+
this.sweep();
|
|
387
|
+
return { ok: false, reason: "expired" };
|
|
388
|
+
}
|
|
389
|
+
stored.consumedAt = now;
|
|
390
|
+
this.sweep();
|
|
391
|
+
this.options.onConsumed?.(code);
|
|
392
|
+
const minted = this.mintSession(now);
|
|
393
|
+
const expiresAt = now + this.options.sessionTtlMs;
|
|
394
|
+
this.sessions.record({ id: minted.id, label: device ?? "unknown device", createdAt: now, expiresAt });
|
|
395
|
+
return { ok: true, session: minted.session, expiresAt, id: minted.id };
|
|
396
|
+
}
|
|
397
|
+
/**
|
|
398
|
+
* Sign out EVERY device: `frizz --sign-out all`, and what a session-epoch bump asks of a running board.
|
|
399
|
+
*
|
|
400
|
+
* The denylist alone is not enough for that. It names only the ids it recorded, so a session minted
|
|
401
|
+
* before ids existed (2026-08-25), or while the directory was held in memory, would survive it. A new
|
|
402
|
+
* key kills every outstanding session whatever it carries; the denylist entries are what make
|
|
403
|
+
* `frizz --sessions` stop listing those devices as signed in. Returns how many recorded devices that was.
|
|
404
|
+
*/
|
|
405
|
+
signOutAll() {
|
|
406
|
+
this.signingKey = this.rotateSigningKey();
|
|
407
|
+
return this.sessions.revokeAll();
|
|
408
|
+
}
|
|
409
|
+
/**
|
|
410
|
+
* A session is `<expiry>.<id>.<nonce>.<hmac>` — signed with a key the caller persists so it survives
|
|
411
|
+
* a restart, and self-describing enough that a stale cookie is rejected without any lookup.
|
|
412
|
+
*
|
|
413
|
+
* The `id` is what makes ONE device signable-out. It is short because an operator types it into
|
|
414
|
+
* `frizz --sign-out <id>`, and it is inside the signed payload so it cannot be swapped for another
|
|
415
|
+
* device's. Rotating the key still revokes everything at once; this is the scalpel beside that axe.
|
|
416
|
+
*
|
|
417
|
+
* A session minted before ids existed has a two-part payload and still verifies — it simply has no id
|
|
418
|
+
* to revoke individually. Adding ids was not meant to sign every device out; when that IS the intent,
|
|
419
|
+
* SESSION_EPOCH does it on purpose, by rotating the key.
|
|
420
|
+
*/
|
|
421
|
+
mintSession(now) {
|
|
422
|
+
const expiresAt = now + this.options.sessionTtlMs;
|
|
423
|
+
const id = this.options.randomToken(SESSION_ID_BYTES);
|
|
424
|
+
const nonce = this.options.randomToken(SESSION_BYTES);
|
|
425
|
+
const payload = `${expiresAt}.${id}.${nonce}`;
|
|
426
|
+
return { session: `${payload}.${this.sign(payload)}`, id };
|
|
427
|
+
}
|
|
428
|
+
/** The expiry a session carries in its own payload. Only called on a session that already verified. */
|
|
429
|
+
static expiryOf(session) {
|
|
430
|
+
return Number(session.slice(0, session.indexOf(".")));
|
|
431
|
+
}
|
|
432
|
+
/** The id inside a session payload, or null for a legacy two-part one. */
|
|
433
|
+
static idOf(payload) {
|
|
434
|
+
const parts = payload.split(".");
|
|
435
|
+
return parts.length >= 3 ? parts[1] ?? null : null;
|
|
436
|
+
}
|
|
437
|
+
sign(payload) {
|
|
438
|
+
return createHmac("sha256", this.signingKey).update(payload).digest("base64url");
|
|
439
|
+
}
|
|
440
|
+
/** Is this cookie a session signed by this board's key, and still in date? */
|
|
441
|
+
verifySession(session) {
|
|
442
|
+
if (!session) return false;
|
|
443
|
+
const cut = session.lastIndexOf(".");
|
|
444
|
+
if (cut <= 0) return false;
|
|
445
|
+
const payload = session.slice(0, cut);
|
|
446
|
+
const signature = session.slice(cut + 1);
|
|
447
|
+
if (!secretsMatch(signature, this.sign(payload))) return false;
|
|
448
|
+
const expiresAt = Number(payload.slice(0, payload.indexOf(".")));
|
|
449
|
+
if (!Number.isFinite(expiresAt) || this.options.now() >= expiresAt) return false;
|
|
450
|
+
const id = _AccessStore.idOf(payload);
|
|
451
|
+
return !(id && this.sessions.isRevoked(id));
|
|
452
|
+
}
|
|
453
|
+
/**
|
|
454
|
+
* Sign out the device HOLDING this session — the "Sign out this device" row, not `--sign-out`.
|
|
455
|
+
*
|
|
456
|
+
* The id comes from the session itself, and only after its signature verifies, so the one thing this
|
|
457
|
+
* can ever revoke is the credential the caller just proved it holds. There is deliberately no
|
|
458
|
+
* parameter naming any other id: signing out SOMEONE ELSE stays loopback-only, because a stolen
|
|
459
|
+
* session must not be able to evict the owner and keep the board.
|
|
460
|
+
*
|
|
461
|
+
* - `revoked`: the id is on the denylist now, so a surviving copy of this cookie is refused too.
|
|
462
|
+
* - `legacy`: a session minted before ids existed verifies but has no id to revoke on its own. The
|
|
463
|
+
* caller can still clear the cookie; signOutAll(), which rotates the key, is the way to kill a copy.
|
|
464
|
+
* - `no-session`: nothing valid was presented — absent, forged, expired, or already signed out.
|
|
465
|
+
*/
|
|
466
|
+
signOutSession(session) {
|
|
467
|
+
if (!session || !this.verifySession(session)) return { result: "no-session" };
|
|
468
|
+
const id = _AccessStore.idOf(session.slice(0, session.lastIndexOf(".")));
|
|
469
|
+
if (!id) return { result: "legacy" };
|
|
470
|
+
if (!this.sessions.revoke(id)) {
|
|
471
|
+
this.sessions.record({ id, label: "unknown device", createdAt: this.options.now(), expiresAt: _AccessStore.expiryOf(session) });
|
|
472
|
+
this.sessions.revoke(id);
|
|
473
|
+
}
|
|
474
|
+
return this.sessions.isRevoked(id) ? { result: "revoked", id } : { result: "no-session" };
|
|
475
|
+
}
|
|
476
|
+
/**
|
|
477
|
+
* Drop codes past their expiry so a long-lived board does not accumulate them forever.
|
|
478
|
+
*
|
|
479
|
+
* A CONSUMED code is deliberately kept until that same expiry rather than deleted on use. Deleting it
|
|
480
|
+
* immediately would make a replay indistinguishable from a typo — both "unknown" — and the difference
|
|
481
|
+
* matters: "already-used" is the message that tells someone their link was used by somebody else.
|
|
482
|
+
* It costs one small entry for at most the code TTL.
|
|
483
|
+
*/
|
|
484
|
+
sweep() {
|
|
485
|
+
const now = this.options.now();
|
|
486
|
+
for (const [key, stored] of this.codes) {
|
|
487
|
+
if (now >= stored.expiresAt) this.codes.delete(key);
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
/** Outstanding codes that could still be redeemed — neither consumed nor expired. */
|
|
491
|
+
outstanding() {
|
|
492
|
+
this.sweep();
|
|
493
|
+
return [...this.codes.values()].filter((stored) => stored.consumedAt === void 0).map(({ code, createdAt, expiresAt }) => ({ code, createdAt, expiresAt }));
|
|
494
|
+
}
|
|
495
|
+
};
|
|
496
|
+
SESSION_KEY_FILE = "session-key";
|
|
497
|
+
SESSION_EPOCH_FILE = "session-epoch";
|
|
498
|
+
}
|
|
499
|
+
});
|
|
500
|
+
|
|
183
501
|
// packages/server/src/local-path.ts
|
|
184
502
|
function normalizeLocalPath(path, platform = process.platform) {
|
|
185
503
|
return platform === "win32" ? path.replace(/^\/([a-zA-Z]:[\\/])/, "$1") : path;
|
|
@@ -191,12 +509,12 @@ var init_local_path = __esm({
|
|
|
191
509
|
});
|
|
192
510
|
|
|
193
511
|
// packages/server/src/local-image.ts
|
|
194
|
-
import {
|
|
512
|
+
import { createReadStream, realpathSync, statSync } from "node:fs";
|
|
195
513
|
import { extname, isAbsolute } from "node:path";
|
|
196
|
-
function resolveLocalImage(rawPath) {
|
|
514
|
+
function resolveLocalImage(rawPath, range) {
|
|
197
515
|
if (rawPath) rawPath = normalizeLocalPath(rawPath);
|
|
198
516
|
if (!rawPath || !isAbsolute(rawPath)) return { status: 400 };
|
|
199
|
-
const contentType =
|
|
517
|
+
const contentType = CONTENT_TYPE[extname(rawPath).toLowerCase()];
|
|
200
518
|
if (!contentType) return { status: 400 };
|
|
201
519
|
let real;
|
|
202
520
|
try {
|
|
@@ -204,14 +522,48 @@ function resolveLocalImage(rawPath) {
|
|
|
204
522
|
} catch {
|
|
205
523
|
return { status: 404 };
|
|
206
524
|
}
|
|
525
|
+
let size;
|
|
207
526
|
try {
|
|
208
|
-
|
|
209
|
-
return { status:
|
|
527
|
+
const stat = statSync(real);
|
|
528
|
+
if (!stat.isFile()) return { status: 404 };
|
|
529
|
+
size = stat.size;
|
|
210
530
|
} catch {
|
|
211
531
|
return { status: 404 };
|
|
212
532
|
}
|
|
533
|
+
const span = range ? byteRange(range, size) : null;
|
|
534
|
+
if (span === "unsatisfiable") return { status: 416, size };
|
|
535
|
+
if (span) return { status: 206, contentType, file: real, size, start: span.start, end: span.end };
|
|
536
|
+
return { status: 200, contentType, file: real, size, start: 0, end: size - 1 };
|
|
537
|
+
}
|
|
538
|
+
function byteRange(header, size) {
|
|
539
|
+
const match = /^bytes=(\d*)-(\d*)$/.exec(header.trim());
|
|
540
|
+
if (!match) return null;
|
|
541
|
+
const [, first, last] = match;
|
|
542
|
+
if (first === "" && last === "") return null;
|
|
543
|
+
if (first === "") {
|
|
544
|
+
const suffix = Number(last);
|
|
545
|
+
if (suffix === 0 || size === 0) return "unsatisfiable";
|
|
546
|
+
return { start: Math.max(size - suffix, 0), end: size - 1 };
|
|
547
|
+
}
|
|
548
|
+
const start = Number(first);
|
|
549
|
+
if (last !== "" && Number(last) < start) return null;
|
|
550
|
+
if (start >= size) return "unsatisfiable";
|
|
551
|
+
return { start, end: last === "" ? size - 1 : Math.min(Number(last), size - 1) };
|
|
552
|
+
}
|
|
553
|
+
function localImageHeaders(result) {
|
|
554
|
+
return {
|
|
555
|
+
"content-type": result.contentType,
|
|
556
|
+
"content-length": String(result.end - result.start + 1),
|
|
557
|
+
"accept-ranges": "bytes",
|
|
558
|
+
"cache-control": "private, max-age=60",
|
|
559
|
+
...result.status === 206 ? { "content-range": `bytes ${result.start}-${result.end}/${result.size}` } : {}
|
|
560
|
+
};
|
|
561
|
+
}
|
|
562
|
+
function localImageStream(result) {
|
|
563
|
+
if (result.end < result.start) return null;
|
|
564
|
+
return createReadStream(result.file, { start: result.start, end: result.end });
|
|
213
565
|
}
|
|
214
|
-
var IMAGE_CONTENT_TYPE;
|
|
566
|
+
var IMAGE_CONTENT_TYPE, VIDEO_CONTENT_TYPE, CONTENT_TYPE;
|
|
215
567
|
var init_local_image = __esm({
|
|
216
568
|
"packages/server/src/local-image.ts"() {
|
|
217
569
|
"use strict";
|
|
@@ -223,65 +575,333 @@ var init_local_image = __esm({
|
|
|
223
575
|
".gif": "image/gif",
|
|
224
576
|
".webp": "image/webp"
|
|
225
577
|
};
|
|
578
|
+
VIDEO_CONTENT_TYPE = {
|
|
579
|
+
".mp4": "video/mp4",
|
|
580
|
+
".m4v": "video/mp4",
|
|
581
|
+
".webm": "video/webm",
|
|
582
|
+
".mov": "video/quicktime"
|
|
583
|
+
};
|
|
584
|
+
CONTENT_TYPE = { ...IMAGE_CONTENT_TYPE, ...VIDEO_CONTENT_TYPE };
|
|
226
585
|
}
|
|
227
586
|
});
|
|
228
587
|
|
|
229
|
-
//
|
|
230
|
-
|
|
231
|
-
|
|
588
|
+
// packages/server/src/supervisor-pages.ts
|
|
589
|
+
function escapeHtml(value) {
|
|
590
|
+
return value.replace(/[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[c]);
|
|
591
|
+
}
|
|
592
|
+
function retryTarget(url) {
|
|
593
|
+
return url.startsWith("/") && !url.startsWith("//") ? url : "/";
|
|
594
|
+
}
|
|
595
|
+
function documentShell(options2) {
|
|
596
|
+
return `<!doctype html><html lang="en" data-font="sans"><head><meta charset="utf-8"><title>${options2.title}</title><meta name="viewport" content="width=device-width,initial-scale=1">` + (options2.appearance ? `<meta name="color-scheme" content="dark light"><meta name="theme-color" content="#0d0e10">` : `<meta name="color-scheme" content="dark light"><meta name="theme-color" media="(prefers-color-scheme: dark)" content="#0d0e10"><meta name="theme-color" media="(prefers-color-scheme: light)" content="#f7f7f7">`) + `<style>${options2.style ? `${BASE_STYLE}
|
|
597
|
+
${options2.style}` : BASE_STYLE}</style>` + (options2.appearance ? `<script>${THEME_SCRIPT}</script>` : "") + `</head><body${options2.bodyAttributes ? ` ${options2.bodyAttributes}` : ""}><main>${options2.body}</main>` + (options2.script ? `<script>${options2.script}</script>` : "") + `</body></html>`;
|
|
598
|
+
}
|
|
599
|
+
function recoveryPage(url, variant = "starting") {
|
|
600
|
+
const copy = RECOVERY_COPY[variant];
|
|
601
|
+
const target2 = escapeHtml(retryTarget(url));
|
|
602
|
+
return documentShell({
|
|
603
|
+
title: copy.title,
|
|
604
|
+
style: RECOVERY_STYLE,
|
|
605
|
+
appearance: true,
|
|
606
|
+
bodyAttributes: `data-target="${target2}"`,
|
|
607
|
+
body: `<h1><span class="dot" id="dot" aria-hidden="true"></span><span id="heading">${copy.heading}</span></h1><p class="detail" id="detail">${copy.detail}</p><pre class="log" id="log" hidden></pre><p class="note">${THREADS_NOTE}</p><div class="actions"><a class="btn" href="${target2}">Try again</a><button class="btn" type="button" id="restart">Restart Frizz</button></div><p class="probe" id="probe" aria-live="polite"></p>`,
|
|
608
|
+
script: RECOVERY_SCRIPT
|
|
609
|
+
});
|
|
610
|
+
}
|
|
611
|
+
function unauthorizedPage(reason) {
|
|
612
|
+
const detail = reason === "already-used" ? "That link has already been used. Generate a fresh one." : reason === "expired" ? "That link expired. Generate a fresh one." : "This address needs a current access link.";
|
|
613
|
+
return documentShell({
|
|
614
|
+
title: "Unauthorized",
|
|
615
|
+
body: `<h1>Unauthorized</h1><p class="detail">${detail}</p>`
|
|
616
|
+
});
|
|
617
|
+
}
|
|
618
|
+
function unlistedHostPage(name) {
|
|
619
|
+
const shown = escapeHtml(name);
|
|
620
|
+
return documentShell({
|
|
621
|
+
title: "Not served by this name",
|
|
622
|
+
appearance: true,
|
|
623
|
+
body: `<h1>Not served by this name</h1><p class="detail">Frizz does not answer to <code>${shown}</code>. A name a browser has to resolve is how DNS rebinding makes another site's page count as this board, so an exposed board accepts only its own hostname and the names its operator lists.</p><p class="note">Open the board by its IP address, or relaunch with <code>--allowed-host ${shown}</code> (or <code>FRIZZ_ALLOWED_HOSTS=${shown}</code>) to accept this one.</p>`
|
|
624
|
+
});
|
|
625
|
+
}
|
|
626
|
+
var BASE_STYLE, RECOVERY_STYLE, THEME_SCRIPT, RECOVERY_SCRIPT, RECOVERY_COPY, THREADS_NOTE;
|
|
627
|
+
var init_supervisor_pages = __esm({
|
|
628
|
+
"packages/server/src/supervisor-pages.ts"() {
|
|
232
629
|
"use strict";
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
case MAP:
|
|
260
|
-
case SCALAR:
|
|
261
|
-
case SEQ:
|
|
262
|
-
return true;
|
|
263
|
-
}
|
|
264
|
-
return false;
|
|
265
|
-
}
|
|
266
|
-
var hasAnchor = (node) => (isScalar(node) || isCollection(node)) && !!node.anchor;
|
|
267
|
-
exports.ALIAS = ALIAS;
|
|
268
|
-
exports.DOC = DOC;
|
|
269
|
-
exports.MAP = MAP;
|
|
270
|
-
exports.NODE_TYPE = NODE_TYPE;
|
|
271
|
-
exports.PAIR = PAIR;
|
|
272
|
-
exports.SCALAR = SCALAR;
|
|
273
|
-
exports.SEQ = SEQ;
|
|
274
|
-
exports.hasAnchor = hasAnchor;
|
|
275
|
-
exports.isAlias = isAlias;
|
|
276
|
-
exports.isCollection = isCollection;
|
|
277
|
-
exports.isDocument = isDocument;
|
|
278
|
-
exports.isMap = isMap;
|
|
279
|
-
exports.isNode = isNode;
|
|
280
|
-
exports.isPair = isPair;
|
|
281
|
-
exports.isScalar = isScalar;
|
|
282
|
-
exports.isSeq = isSeq;
|
|
630
|
+
BASE_STYLE = `
|
|
631
|
+
*,*::before,*::after{box-sizing:border-box}
|
|
632
|
+
:root{
|
|
633
|
+
color-scheme:dark;
|
|
634
|
+
--bg:#0d0e10;--panel:#131519;--panel-2:#181b20;
|
|
635
|
+
--border:#26282d;--border-strong:#33363c;--control-border:#26282d;--control-strong:#33363c;
|
|
636
|
+
--fg:#e6e7e9;--muted:#8b8f96;--accent:#e8b923;--danger:#fca5a5;
|
|
637
|
+
--focus-border:color-mix(in srgb,var(--accent) 55%,transparent);--focus-ring:color-mix(in srgb,var(--accent) 22%,transparent);
|
|
638
|
+
--sans:system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;
|
|
639
|
+
--mono:ui-monospace,"JetBrains Mono","SF Mono","Cascadia Mono",Menlo,Consolas,monospace;
|
|
640
|
+
--ease:cubic-bezier(0,0,.2,1);
|
|
641
|
+
}
|
|
642
|
+
:root[data-theme=light]{
|
|
643
|
+
color-scheme:light;
|
|
644
|
+
--bg:#f7f7f7;--panel:#fff;--panel-2:#fafafa;
|
|
645
|
+
--border:#dedede;--border-strong:#cecece;--control-border:#858585;--control-strong:#7d7d7d;
|
|
646
|
+
--fg:#292929;--muted:#606060;--accent:#416896;--danger:#cf222e;
|
|
647
|
+
--focus-border:#416896;--focus-ring:#416896;
|
|
648
|
+
}
|
|
649
|
+
@media(prefers-color-scheme:light){
|
|
650
|
+
:root:not([data-theme]){
|
|
651
|
+
color-scheme:light;
|
|
652
|
+
--bg:#f7f7f7;--panel:#fff;--panel-2:#fafafa;
|
|
653
|
+
--border:#dedede;--border-strong:#cecece;--control-border:#858585;--control-strong:#7d7d7d;
|
|
654
|
+
--fg:#292929;--muted:#606060;--accent:#416896;--danger:#cf222e;
|
|
655
|
+
--focus-border:#416896;--focus-ring:#416896;
|
|
283
656
|
}
|
|
284
|
-
}
|
|
657
|
+
}
|
|
658
|
+
html,body{background:var(--bg)}
|
|
659
|
+
body{
|
|
660
|
+
margin:0;min-height:100vh;min-height:100dvh;
|
|
661
|
+
display:flex;align-items:center;justify-content:center;padding:24px;
|
|
662
|
+
color:var(--fg);font-family:var(--sans);font-size:12.5px;line-height:1.55;
|
|
663
|
+
-webkit-font-smoothing:antialiased;
|
|
664
|
+
}
|
|
665
|
+
main{width:100%;max-width:28rem;padding:18px;border:1px solid var(--border-strong);border-radius:12px;background:var(--panel-2)}
|
|
666
|
+
h1{margin:0;display:flex;align-items:baseline;gap:7px;font-size:14px;font-weight:600;letter-spacing:-.01em;line-height:1.4}
|
|
667
|
+
p{margin:0;text-wrap:pretty}
|
|
668
|
+
.detail{margin-top:7px;color:color-mix(in srgb,var(--fg) 88%,transparent)}
|
|
669
|
+
.note{margin-top:12px;padding-top:11px;border-top:1px solid var(--border);color:var(--muted);font-size:11.5px;line-height:1.6}
|
|
670
|
+
code{font-family:var(--mono);font-size:.92em;padding:.1em .35em;border-radius:4px;background:var(--panel);border:1px solid var(--border);white-space:nowrap}
|
|
671
|
+
[hidden]{display:none!important}
|
|
672
|
+
`.trim();
|
|
673
|
+
RECOVERY_STYLE = `
|
|
674
|
+
.actions{margin-top:14px;display:flex;flex-wrap:wrap;gap:8px}
|
|
675
|
+
/* ErrorBoundary's ACTION_CLASS, one size up for a page that is alone in a viewport. */
|
|
676
|
+
.btn{
|
|
677
|
+
appearance:none;display:inline-flex;align-items:center;
|
|
678
|
+
border:1px solid var(--control-border);border-radius:6px;background:transparent;
|
|
679
|
+
padding:5px 10px;font:inherit;font-size:11.5px;color:color-mix(in srgb,var(--fg) 90%,transparent);
|
|
680
|
+
cursor:pointer;text-decoration:none;
|
|
681
|
+
transition:background-color 120ms var(--ease),border-color 120ms var(--ease),color 120ms var(--ease);
|
|
682
|
+
}
|
|
683
|
+
.btn:hover{background:var(--panel);border-color:var(--control-strong);color:var(--fg)}
|
|
684
|
+
.btn:focus-visible{outline:none;border-color:var(--focus-border);box-shadow:0 0 0 2px var(--focus-ring)}
|
|
685
|
+
.btn:disabled{opacity:.5;pointer-events:none}
|
|
686
|
+
/* min-height holds the slot open while it is LIVE, so a changing status line never nudges the card;
|
|
687
|
+
:empty collapses it when there is nothing live to say, so a halted card has no trailing gutter. */
|
|
688
|
+
.probe{margin-top:12px;font-family:var(--mono);font-size:11px;line-height:1.5;color:var(--muted);min-height:1.5em}
|
|
689
|
+
.probe:empty{margin-top:0;min-height:0}
|
|
690
|
+
/* RestartFailureNotice's block, at this altitude: a supervisor \`message\` is raw build output, and it
|
|
691
|
+
reads as what it is \u2014 a terminal excerpt \u2014 only inside a scrolling mono box. */
|
|
692
|
+
.log{
|
|
693
|
+
margin-top:10px;max-height:16rem;overflow:auto;white-space:pre-wrap;overflow-wrap:break-word;
|
|
694
|
+
border:1px solid var(--border);border-radius:6px;background:var(--panel);padding:8px 10px;
|
|
695
|
+
font-family:var(--mono);font-size:11px;line-height:1.6;color:color-mix(in srgb,var(--fg) 70%,transparent);
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
/* THE LIVE DOT, and it is the app's own \`.frizz-live-dot\` \u2014 same 6px, same 28%/14% halo, same 1.25s
|
|
699
|
+
cadence \u2014 because it means the same thing here that it means there: work is in flight.
|
|
700
|
+
|
|
701
|
+
Alignment is an INK problem. A baseline-aligned flex item with no text of its own contributes its
|
|
702
|
+
BORDER-BOX BOTTOM as the baseline, so the dot's ink centre lands 3px above it while the cap band's
|
|
703
|
+
centre sits 0.5cap above it. \`cap\` is the resolved font's own cap height, so the correction holds
|
|
704
|
+
at any size, with nothing to re-measure when the type scale moves. The em spelling is the fallback for a browser without the unit (.35em ~= 0.5 x a
|
|
705
|
+
0.7em cap) and is overwritten wherever \`cap\` parses. */
|
|
706
|
+
.dot{
|
|
707
|
+
width:6px;height:6px;flex:0 0 auto;border-radius:9999px;background:var(--accent);
|
|
708
|
+
box-shadow:0 0 0 1px color-mix(in srgb,var(--accent) 28%,transparent);
|
|
709
|
+
animation:frizz-live-pulse 1.25s ease-in-out infinite;
|
|
710
|
+
transform:translateY(calc(3px - .35em));
|
|
711
|
+
transform:translateY(calc(3px - .5cap));
|
|
712
|
+
}
|
|
713
|
+
.dot[data-state=idle]{animation:none;background:var(--muted);box-shadow:none}
|
|
714
|
+
.dot[data-state=bad]{animation:none;background:var(--danger);box-shadow:0 0 0 1px color-mix(in srgb,var(--danger) 28%,transparent)}
|
|
715
|
+
@keyframes frizz-live-pulse{50%{opacity:.62;box-shadow:0 0 0 2px color-mix(in srgb,var(--accent) 14%,transparent)}}
|
|
716
|
+
@media (prefers-reduced-motion:reduce){
|
|
717
|
+
.dot{animation:none;background:transparent;border:2px solid var(--accent);box-shadow:none}
|
|
718
|
+
.dot[data-state=idle]{border-color:var(--muted)}
|
|
719
|
+
.dot[data-state=bad]{border-color:var(--danger);background:transparent}
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
/* The app's \`.shimmer-text\`: a highlight sweeping muted text is how this product says "working", and
|
|
723
|
+
the probe line is the only thing on the page that is. */
|
|
724
|
+
.probe[data-live=yes]{
|
|
725
|
+
color:transparent;
|
|
726
|
+
background:linear-gradient(90deg,var(--muted) 0%,var(--muted) 35%,var(--fg) 50%,var(--muted) 65%,var(--muted) 100%) 0 0/200% 100%;
|
|
727
|
+
-webkit-background-clip:text;background-clip:text;
|
|
728
|
+
animation:frizz-shimmer 2.2s linear infinite;
|
|
729
|
+
}
|
|
730
|
+
@keyframes frizz-shimmer{from{background-position:200% 0}to{background-position:-200% 0}}
|
|
731
|
+
@media (prefers-reduced-motion:reduce){
|
|
732
|
+
.probe[data-live=yes]{animation:none;background:none;color:var(--muted)}
|
|
733
|
+
}
|
|
734
|
+
`.trim();
|
|
735
|
+
THEME_SCRIPT = `var p;try{p=localStorage.getItem("frizz-theme")}catch{}var d=false;try{d=matchMedia&&matchMedia("(prefers-color-scheme: dark)").matches}catch{}var t=p==="dark"||p==="light"?p:d?"dark":"light";document.documentElement.dataset.theme=t;document.documentElement.style.colorScheme=t;document.querySelector('meta[name="theme-color"]').content=t==="dark"?"#0d0e10":"#f7f7f7"`;
|
|
736
|
+
RECOVERY_SCRIPT = `
|
|
737
|
+
(function(){
|
|
738
|
+
var target=document.body.dataset.target||"/"
|
|
739
|
+
var heading=document.getElementById("heading")
|
|
740
|
+
var detail=document.getElementById("detail")
|
|
741
|
+
var probe=document.getElementById("probe")
|
|
742
|
+
var log=document.getElementById("log")
|
|
743
|
+
var dot=document.getElementById("dot")
|
|
744
|
+
var restart=document.getElementById("restart")
|
|
745
|
+
var BUDGET=120000,MIN=600,MAX=4000,GROWTH=1.4
|
|
746
|
+
var delay=MIN,deadline=Date.now()+BUDGET,timer=0,stopped=false,busy=false,told=""
|
|
747
|
+
|
|
748
|
+
function say(text,live){probe.textContent=text;probe.setAttribute("data-live",live?"yes":"no")}
|
|
749
|
+
function halt(text,bad){stopped=true;clearTimeout(timer);dot.setAttribute("data-state",bad?"bad":"idle");say(text,false)}
|
|
750
|
+
function showLog(message){if(message){log.textContent=message;log.hidden=false}else{log.hidden=true}}
|
|
751
|
+
function resume(){stopped=false;busy=false;delay=MIN;deadline=Date.now()+BUDGET;dot.removeAttribute("data-state");clearTimeout(timer);timer=setTimeout(tick,0)}
|
|
752
|
+
|
|
753
|
+
// 503 is this proxy's own "no child". Every other answer \u2014 200, 404, even a 500 from the app \u2014 means
|
|
754
|
+
// something is listening again, and the reader's own URL is the right thing to ask.
|
|
755
|
+
function alive(){
|
|
756
|
+
return fetch(target,{method:"HEAD",cache:"no-store",credentials:"same-origin"})
|
|
757
|
+
.then(function(r){return r.status!==503}).catch(function(){return false})
|
|
758
|
+
}
|
|
759
|
+
function readStatus(){
|
|
760
|
+
return fetch("/_frizz/control/status",{cache:"no-store",headers:{"cache-control":"no-store"}})
|
|
761
|
+
.then(function(r){return r.ok?r.json():null})
|
|
762
|
+
.then(function(b){return b&&b.protocol===1?b:null}).catch(function(){return null})
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
// An unreachable launcher leaves the server-rendered copy alone: it is already right for the case
|
|
766
|
+
// that rendered it, and guessing over it would be worse than saying nothing.
|
|
767
|
+
function narrate(status){
|
|
768
|
+
if(!status||status.state===told)return
|
|
769
|
+
told=status.state
|
|
770
|
+
if(status.state==="failed"){
|
|
771
|
+
heading.textContent="Frizz did not restart"
|
|
772
|
+
// Not a second copy of the note below it \u2014 a lead-in to the log, which is the next thing down.
|
|
773
|
+
detail.textContent="The launcher could not bring the application server back, and reported this:"
|
|
774
|
+
showLog(status.message||"The launcher gave no reason.")
|
|
775
|
+
halt("",true)
|
|
776
|
+
return
|
|
777
|
+
}
|
|
778
|
+
showLog("")
|
|
779
|
+
if(status.state==="restarting"){
|
|
780
|
+
heading.textContent="Frizz is restarting"
|
|
781
|
+
detail.textContent="The application server is coming back up."
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
function tick(){
|
|
786
|
+
if(stopped||busy)return
|
|
787
|
+
// A hidden tab neither polls nor spends its budget; visibilitychange starts the loop again.
|
|
788
|
+
if(document.hidden)return
|
|
789
|
+
alive().then(function(up){
|
|
790
|
+
if(up){say("Frizz is back. Reloading.",true);location.replace(target);return}
|
|
791
|
+
return readStatus().then(function(status){
|
|
792
|
+
narrate(status)
|
|
793
|
+
if(stopped)return
|
|
794
|
+
if(Date.now()>deadline){halt("No answer after 2m. This page stopped checking.",false);return}
|
|
795
|
+
delay=Math.min(MAX,delay*GROWTH)
|
|
796
|
+
say("Waiting for Frizz",true)
|
|
797
|
+
timer=setTimeout(tick,delay)
|
|
798
|
+
})
|
|
799
|
+
})
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
document.addEventListener("visibilitychange",function(){
|
|
803
|
+
if(document.hidden||stopped||busy)return
|
|
804
|
+
resume()
|
|
805
|
+
})
|
|
806
|
+
|
|
807
|
+
restart.addEventListener("click",function(){
|
|
808
|
+
if(busy)return
|
|
809
|
+
busy=true;stopped=false;told="";clearTimeout(timer)
|
|
810
|
+
restart.disabled=true;restart.textContent="Restarting"
|
|
811
|
+
dot.removeAttribute("data-state");showLog("");say("Restarting Frizz",true)
|
|
812
|
+
fetch("/_frizz/control/restart",{method:"POST",headers:{"cache-control":"no-store"}})
|
|
813
|
+
.then(function(r){return r.json().catch(function(){return null}).then(function(b){return{ok:r.ok,status:r.status,body:b}})})
|
|
814
|
+
.catch(function(){return{ok:false,status:0,body:null}})
|
|
815
|
+
.then(function(r){
|
|
816
|
+
restart.disabled=false;restart.textContent="Restart Frizz";busy=false
|
|
817
|
+
if(!r.ok){
|
|
818
|
+
heading.textContent="Frizz did not restart"
|
|
819
|
+
detail.textContent="The launcher could not bring the application server back, and reported this:"
|
|
820
|
+
showLog((r.body&&r.body.message)||("The launcher refused the restart ("+(r.status||"no response")+")."))
|
|
821
|
+
halt("",true)
|
|
822
|
+
return
|
|
823
|
+
}
|
|
824
|
+
// Accepted. The probe, not this response, decides whether the child is actually back.
|
|
825
|
+
resume()
|
|
826
|
+
})
|
|
827
|
+
})
|
|
828
|
+
|
|
829
|
+
say("Waiting for Frizz",true)
|
|
830
|
+
tick()
|
|
831
|
+
})()
|
|
832
|
+
`.trim();
|
|
833
|
+
RECOVERY_COPY = {
|
|
834
|
+
starting: {
|
|
835
|
+
title: "Restarting | Frizz",
|
|
836
|
+
heading: "Frizz is restarting",
|
|
837
|
+
detail: "The application server is not listening yet. This page reloads as soon as it answers."
|
|
838
|
+
},
|
|
839
|
+
unreachable: {
|
|
840
|
+
title: "Not responding | Frizz",
|
|
841
|
+
heading: "Frizz is not responding",
|
|
842
|
+
detail: "The application server accepted this request and then dropped it. Restarting it usually clears this."
|
|
843
|
+
}
|
|
844
|
+
};
|
|
845
|
+
THREADS_NOTE = "Threads already running are not affected. Each runs as its own detached process, and Frizz reattaches to it on restart.";
|
|
846
|
+
}
|
|
847
|
+
});
|
|
848
|
+
|
|
849
|
+
// node_modules/.pnpm/yaml@2.9.0/node_modules/yaml/dist/nodes/identity.js
|
|
850
|
+
var require_identity = __commonJS({
|
|
851
|
+
"node_modules/.pnpm/yaml@2.9.0/node_modules/yaml/dist/nodes/identity.js"(exports) {
|
|
852
|
+
"use strict";
|
|
853
|
+
var ALIAS = Symbol.for("yaml.alias");
|
|
854
|
+
var DOC = Symbol.for("yaml.document");
|
|
855
|
+
var MAP = Symbol.for("yaml.map");
|
|
856
|
+
var PAIR = Symbol.for("yaml.pair");
|
|
857
|
+
var SCALAR = Symbol.for("yaml.scalar");
|
|
858
|
+
var SEQ = Symbol.for("yaml.seq");
|
|
859
|
+
var NODE_TYPE = Symbol.for("yaml.node.type");
|
|
860
|
+
var isAlias = (node) => !!node && typeof node === "object" && node[NODE_TYPE] === ALIAS;
|
|
861
|
+
var isDocument = (node) => !!node && typeof node === "object" && node[NODE_TYPE] === DOC;
|
|
862
|
+
var isMap = (node) => !!node && typeof node === "object" && node[NODE_TYPE] === MAP;
|
|
863
|
+
var isPair = (node) => !!node && typeof node === "object" && node[NODE_TYPE] === PAIR;
|
|
864
|
+
var isScalar = (node) => !!node && typeof node === "object" && node[NODE_TYPE] === SCALAR;
|
|
865
|
+
var isSeq = (node) => !!node && typeof node === "object" && node[NODE_TYPE] === SEQ;
|
|
866
|
+
function isCollection(node) {
|
|
867
|
+
if (node && typeof node === "object")
|
|
868
|
+
switch (node[NODE_TYPE]) {
|
|
869
|
+
case MAP:
|
|
870
|
+
case SEQ:
|
|
871
|
+
return true;
|
|
872
|
+
}
|
|
873
|
+
return false;
|
|
874
|
+
}
|
|
875
|
+
function isNode(node) {
|
|
876
|
+
if (node && typeof node === "object")
|
|
877
|
+
switch (node[NODE_TYPE]) {
|
|
878
|
+
case ALIAS:
|
|
879
|
+
case MAP:
|
|
880
|
+
case SCALAR:
|
|
881
|
+
case SEQ:
|
|
882
|
+
return true;
|
|
883
|
+
}
|
|
884
|
+
return false;
|
|
885
|
+
}
|
|
886
|
+
var hasAnchor = (node) => (isScalar(node) || isCollection(node)) && !!node.anchor;
|
|
887
|
+
exports.ALIAS = ALIAS;
|
|
888
|
+
exports.DOC = DOC;
|
|
889
|
+
exports.MAP = MAP;
|
|
890
|
+
exports.NODE_TYPE = NODE_TYPE;
|
|
891
|
+
exports.PAIR = PAIR;
|
|
892
|
+
exports.SCALAR = SCALAR;
|
|
893
|
+
exports.SEQ = SEQ;
|
|
894
|
+
exports.hasAnchor = hasAnchor;
|
|
895
|
+
exports.isAlias = isAlias;
|
|
896
|
+
exports.isCollection = isCollection;
|
|
897
|
+
exports.isDocument = isDocument;
|
|
898
|
+
exports.isMap = isMap;
|
|
899
|
+
exports.isNode = isNode;
|
|
900
|
+
exports.isPair = isPair;
|
|
901
|
+
exports.isScalar = isScalar;
|
|
902
|
+
exports.isSeq = isSeq;
|
|
903
|
+
}
|
|
904
|
+
});
|
|
285
905
|
|
|
286
906
|
// node_modules/.pnpm/yaml@2.9.0/node_modules/yaml/dist/visit.js
|
|
287
907
|
var require_visit = __commonJS({
|
|
@@ -12362,6 +12982,31 @@ var init_drainable_worker = __esm({
|
|
|
12362
12982
|
}
|
|
12363
12983
|
});
|
|
12364
12984
|
|
|
12985
|
+
// packages/shared/src/frizz-md.ts
|
|
12986
|
+
var FrizzMdLanding, FrizzMdAutonomy, FRIZZ_MD_NOTES_MAX, FrizzMdAnswers, FrizzMdStatus;
|
|
12987
|
+
var init_frizz_md = __esm({
|
|
12988
|
+
"packages/shared/src/frizz-md.ts"() {
|
|
12989
|
+
"use strict";
|
|
12990
|
+
init_zod();
|
|
12991
|
+
FrizzMdLanding = external_exports.enum(["pull-request", "main", "main-push", "branch"]);
|
|
12992
|
+
FrizzMdAutonomy = external_exports.enum(["decide", "balanced", "check-in"]);
|
|
12993
|
+
FRIZZ_MD_NOTES_MAX = 8e3;
|
|
12994
|
+
FrizzMdAnswers = external_exports.object({
|
|
12995
|
+
landing: FrizzMdLanding,
|
|
12996
|
+
autonomy: FrizzMdAutonomy,
|
|
12997
|
+
notes: external_exports.string().max(FRIZZ_MD_NOTES_MAX)
|
|
12998
|
+
}).strict();
|
|
12999
|
+
FrizzMdStatus = external_exports.object({
|
|
13000
|
+
/** A FRIZZ.md is already at the project root: the project has its rules, so nothing is asked. */
|
|
13001
|
+
exists: external_exports.boolean(),
|
|
13002
|
+
/** The operator skipped the questionnaire for this project: it is not asked again. */
|
|
13003
|
+
skipped: external_exports.boolean(),
|
|
13004
|
+
/** The branch the landing rules name — the remote's default, else the checked-out one, else `main`. */
|
|
13005
|
+
defaultBranch: external_exports.string()
|
|
13006
|
+
});
|
|
13007
|
+
}
|
|
13008
|
+
});
|
|
13009
|
+
|
|
12365
13010
|
// packages/shared/src/receipt-bus.ts
|
|
12366
13011
|
var init_receipt_bus = __esm({
|
|
12367
13012
|
"packages/shared/src/receipt-bus.ts"() {
|
|
@@ -12425,9 +13070,6 @@ var init_relay_protocol = __esm({
|
|
|
12425
13070
|
RELAY_MAX_FRAME_BODY = 512 * 1024;
|
|
12426
13071
|
RELAY_MAX_WS_CHUNK = 64 * 1024;
|
|
12427
13072
|
WsMessageAssembler = class {
|
|
12428
|
-
constructor(maxBytes = 8 * 1024 * 1024) {
|
|
12429
|
-
this.maxBytes = maxBytes;
|
|
12430
|
-
}
|
|
12431
13073
|
parts = /* @__PURE__ */ new Map();
|
|
12432
13074
|
sizes = /* @__PURE__ */ new Map();
|
|
12433
13075
|
/**
|
|
@@ -12438,6 +13080,10 @@ var init_relay_protocol = __esm({
|
|
|
12438
13080
|
* lighter and corrupt. The run stays poisoned until the frame that terminates it.
|
|
12439
13081
|
*/
|
|
12440
13082
|
poisoned = /* @__PURE__ */ new Set();
|
|
13083
|
+
maxBytes;
|
|
13084
|
+
constructor(maxBytes = 8 * 1024 * 1024) {
|
|
13085
|
+
this.maxBytes = maxBytes;
|
|
13086
|
+
}
|
|
12441
13087
|
/** Returns the complete message, or null while more is still to come (or once the run overflowed). */
|
|
12442
13088
|
push(id, data, more) {
|
|
12443
13089
|
if (this.poisoned.has(id)) {
|
|
@@ -12504,22 +13150,27 @@ function signoffNudgeAwaitingLines(needsInput) {
|
|
|
12504
13150
|
return [
|
|
12505
13151
|
"- `` ```awaiting `` \u2014 you are WAITING on work that is actually running: a background shell, a",
|
|
12506
13152
|
" sub-agent, a timer or a registered PR. FRONTMATTER, THEN MARKDOWN: one YAML list per kind of thing",
|
|
12507
|
-
" you are waiting on, a REQUIRED `for:` duration, a REQUIRED `
|
|
12508
|
-
"
|
|
13153
|
+
" you are waiting on, a REQUIRED `for:` duration, a REQUIRED `status:`, then optionally a `---` line",
|
|
13154
|
+
" and prose.",
|
|
12509
13155
|
"",
|
|
12510
13156
|
" ```awaiting",
|
|
12511
13157
|
" agents: [<the id your runtime gave you>]",
|
|
12512
|
-
"
|
|
13158
|
+
" status: working",
|
|
12513
13159
|
" for: 2h",
|
|
12514
13160
|
" ```",
|
|
12515
13161
|
"",
|
|
12516
|
-
" `
|
|
12517
|
-
"
|
|
12518
|
-
"
|
|
12519
|
-
"
|
|
12520
|
-
"
|
|
12521
|
-
"
|
|
12522
|
-
"
|
|
13162
|
+
" `status:` says where the thread sits while you wait, in one of three words:",
|
|
13163
|
+
" - `working` \u2014 what you named finishes by itself: a test, a build, a benchmark, CI, a sub-agent doing",
|
|
13164
|
+
" a task. The thread shows as running until the work wakes you.",
|
|
13165
|
+
" - `watching` \u2014 something has to HAPPEN outside the thread: a release, a review, another agent's",
|
|
13166
|
+
" merge, the next daily cycle. The thread is snoozed, and nothing on its row moves, until a named",
|
|
13167
|
+
" item reports or `for:` runs out.",
|
|
13168
|
+
" - `needs_input` \u2014 the human can read, try or act on something NOW while the work runs (a partial",
|
|
13169
|
+
" result, a file you wrote, a server to try), even if you need nothing back from them. The thread",
|
|
13170
|
+
" goes into their queue, and the prose under `---` says what to look at.",
|
|
13171
|
+
" `working` and `watching` owe NO write-up: the fence alone is the whole message. If you wrote ANY",
|
|
13172
|
+
" words for the human at this rest, it is `needs_input` \u2014 nothing else is put in front of them. A rest",
|
|
13173
|
+
" on running work with NO fence is a bare rest, and it lands in the human's queue."
|
|
12523
13174
|
];
|
|
12524
13175
|
}
|
|
12525
13176
|
function signoffNudgeText(needsInput) {
|
|
@@ -12565,10 +13216,20 @@ function signoffNudgeText(needsInput) {
|
|
|
12565
13216
|
" A fence that names NOTHING is not a park at all \u2014 if you are not waiting on anything, you are not",
|
|
12566
13217
|
" awaiting, you are done. Register a PR with `mcp__frizz__watch_pr` and a timer with",
|
|
12567
13218
|
" `mcp__frizz__timer`; `mcp__frizz__activity` reads back everything you have running, with its id.",
|
|
13219
|
+
"- `` ```awaiting `` with `steps:` \u2014 the human must PERFORM something you cannot: sign in, approve,",
|
|
13220
|
+
" merge, press a button you may not. List each step as a `- ` line under `steps:`, written to be",
|
|
13221
|
+
" followed cold: a real Markdown link to every page and file it names, every command in a code span,",
|
|
13222
|
+
" every value the act needs written in. Frizz reads them verbatim. Steps name the HUMAN as the wait,",
|
|
13223
|
+
" so the fence needs no other name and no `for:`, and the thread goes into their queue. Their Done",
|
|
13224
|
+
" comes back to you as their reply; anything else they need to say comes as a message of their own.",
|
|
13225
|
+
"- `` ```awaiting `` with `questions:` \u2014 a question you registered at an EARLIER rest is still open and",
|
|
13226
|
+
" you still need its answer. Name every such question (`questions: [qst_\u2026]`) and withdraw the rest with",
|
|
13227
|
+
" `mcp__frizz__unask`: a fence that leaves an open question out is refused. A named card is drawn at",
|
|
13228
|
+
" this rest; an unnamed one stays where you asked it.",
|
|
12568
13229
|
"",
|
|
12569
13230
|
"**STILL OWED counts things you are not going to do yourself.** A decision you are RECOMMENDING, a",
|
|
12570
13231
|
"draft you wrote but did not send, follow-up work you discovered \u2014 all of it dies with the card, even",
|
|
12571
|
-
"the part that is someone else's to do. Each ends on a question with your recommendation as option
|
|
13232
|
+
"the part that is someone else's to do. Each ends on a question with your recommendation as option 1,",
|
|
12572
13233
|
"or you DO it first \u2014 a sub-agent's result comes BACK to you, so it lands on your card; a new card via",
|
|
12573
13234
|
"`mcp__frizz__spawn_thread` is the LAST resort, since nothing it learns returns to you or its siblings.",
|
|
12574
13235
|
"None of them is a `done`. And what is not",
|
|
@@ -12593,7 +13254,7 @@ function requireEffortOutsideAcp(profile, ctx) {
|
|
|
12593
13254
|
function fallbackPort(base) {
|
|
12594
13255
|
return base + 1e4;
|
|
12595
13256
|
}
|
|
12596
|
-
var import_yaml, ATTACHMENT_IMAGE_EXTENSIONS, ATTACHMENT_TEXT_EXTENSIONS, ATTACHMENT_OFFICE_EXTENSIONS, ATTACHMENT_DATA_EXTENSIONS, ATTACHMENT_ARCHIVE_EXTENSIONS, ATTACHMENT_DOC_EXTENSIONS, ATTACHMENT_EXTENSIONS, ATTACHMENT_MAX_BASE64_CHARS, ATTACHMENT_MAX_BYTES, ATTACHMENT_EXT_SET, ATTACHMENT_ACCEPT, FrizzStatus, BlockMechanism, RuntimeState, Backend, CodexModel, ClaudeModel, AcpAgent, AcpAgentModel, AcpAgentModels, AcpAgentModelsInput, ThreadProfileOption, ThreadAgent, SubAgentView, BgShellView, CompletionHoldOp, CompletionHold, AskOption, AskQuestion, PendingAsk, AwaitingHint, RETIRED_AWAITING_KINDS, RETIRED_LINE_RE, AWAITING_FOR_MAX_MS, SnoozeUntil, SNOOZE_PROMPT_MAX, SnoozePrompt, RECURRING_PROMPT_MAX, RECURRING_MIN_INTERVAL_SECONDS, RECURRING_MAX_INTERVAL_SECONDS, RecurringPromptText, RecurringIntervalSeconds, ThreadRecurringPrompt, TIMER_PROMPT_MAX, TIMER_MAX_DELAY_SECONDS, TimerPromptText, SIGNOFF_NUDGE_MARKER, SIGNOFF_NUDGE_MESSAGE, SIGNOFF_NUDGE_MESSAGE_NEEDS_INPUT, ThreadWatchKind, GithubChecksState, GithubMergeState, GithubWatchStatus, GithubIssueStatus, ThreadWatchView, PR_WATCH_FOR_MAX_MS, PR_WATCH_DEFAULT_FOR_MS, PrWatchView, AddOwnPrWatchInput, AddOwnPrWatchResult, DropOwnPrWatchInput, DropOwnPrWatchResult, OwnWatchKind, AddOwnWatchInput, OwnWatchView, AddOwnWatchResult, DropOwnWatchInput, DropOwnWatchResult, AskQuestionKind, AskedOptionSchema, AskedQuestionSchema, AskInput, RegisteredQuestionView, AskResult, UnaskInput, UnaskResult, QuestionAnswerSchema, AnswerQuestionsInput, AnswerQuestionsResult, DismissQuestionsInput, DismissQuestionsResult, SettledQuestionView, ThreadSettledQuestionsResult, MarkOwnDoneInput, MarkOwnDoneResult, ListOwnPrWatchesInput, OwnPrWatchesResult, ThreadTimerView, ThreadFence, LimitWindow, LimitPause, ProviderError, ThreadLinkView, UpsertOwnLinkInput, UpsertOwnLinkResult, DropOwnLinkInput, DropOwnLinkResult, LiveTool, ThreadView, BoardErrorItem, BoardSnapshot, QuotaWindow, ProviderQuota, QuotaSnapshot, ProviderAuth, AccountEmails, AuthSnapshot, AccountBackend, AccountLogoutInput, AccountLogoutResult, PermissionMode, LocalFileOpener, Settings, DispatchProviderPreferences, DispatchPreferences, DispatchProfileSnapshotShape, DispatchProfileSnapshot, SetDispatchPreferenceInput, DispatchInput, ADOPT_THREAD_MESSAGE_MAX_CHARS, AdoptThreadInput, AdoptThreadResult, AdoptSessionInput, FollowUpInput, DismissFailedFollowUpInput, DismissFailedFollowUpResult, UnqueueFollowUpInput, UnqueueFollowUpResult, DeliverQueuedNowInput, DeliverQueuedNowResult, CompactThreadInput, SetThreadSnoozeInput, SetThreadPinnedInput, SetThreadRecurringPromptInput, SetOwnThreadRecurringPromptInput, SetOwnThreadRecurringPromptResult, GetOwnThreadRecurringPromptInput, OwnThreadRecurringPromptResult, SetOwnThreadTimerInput, CancelOwnThreadTimerInput, ListOwnThreadTimersInput, OwnThreadTimersResult, ThreadActivityItem, ListOwnThreadActivityInput, OwnThreadActivityResult, SetOwnThreadTimerResult, CancelOwnThreadTimerResult, SetOwnThreadStopHookInput, SetOwnThreadHeartbeatInput, ThreadPluginReloadResult, RenameThreadInput, AiRenameThreadInput, AiRenameThreadResult, SetOwnThreadTitleInput, SetOwnThreadTitleResult, SetThreadPermissionInput, SetThreadPermissionResult, ThreadProfileOptionsInput, ThreadProfileOptionsResult, ThreadSkillSource, ThreadSkill, ThreadSkillsInput, ThreadSkillsResult, SetThreadProfileInput, SetThreadProfileResult, UpgradeThreadModelInput, UpgradeThreadModelResult, DISPATCH_TASK_BANNER, DISPATCH_TASK_BANNER_MARKER, HUMAN_GAP_FLOOR_MS, GithubWakeItem, GithubWakeSteer, WAKE_SCOPE, WAKE_REF, WAKE_SINGLE, WAKE_MULTI_HEAD, PR_WATCH_FINISHED, PR_WATCH_CI_PASSED, PR_WATCH_CI_GATED, PR_WATCH_STATE, PR_WATCH_CI_FAILED, GithubStatus, GithubRefCard, GithubRefPreviewInput, GithubRefPreviewResult, GithubItem, GithubListInput, GithubListResult, GithubBatchInput, GithubBatchResult, BoardMeta, ServerEvent, TranscriptEdit, TranscriptTodo, TranscriptToolCall, TranscriptPart, TranscriptMessage, TranscriptPageCursor, EditedFile, TranscriptPage, TranscriptEarlierInput, SocketTranscriptSlug, SocketFilePath, SocketTranscriptClientMsg, SocketFileClientMsg, SocketClientMsg, ProjectCard, ProjectRailCounts, PROJECT_ICON_MAX_BASE64_CHARS, DirectoryPickResult, ThreadLocation, FRIZZ_ROUTE_PREFIX, DEFAULT_PORT;
|
|
13257
|
+
var import_yaml, ATTACHMENT_IMAGE_EXTENSIONS, ATTACHMENT_TEXT_EXTENSIONS, ATTACHMENT_OFFICE_EXTENSIONS, ATTACHMENT_DATA_EXTENSIONS, ATTACHMENT_ARCHIVE_EXTENSIONS, ATTACHMENT_DOC_EXTENSIONS, ATTACHMENT_EXTENSIONS, ATTACHMENT_MAX_BASE64_CHARS, ATTACHMENT_MAX_BYTES, ATTACHMENT_EXT_SET, ATTACHMENT_ACCEPT, FrizzStatus, BlockMechanism, RuntimeState, Backend, CodexModel, ClaudeModel, AcpAgent, AcpAgentModel, AcpAgentModels, AcpAgentModelsInput, ThreadProfileOption, ThreadAgent, SubAgentView, BgShellView, CompletionHoldOp, CompletionHold, AskOption, AskQuestion, PendingAsk, AwaitingHint, RETIRED_AWAITING_KINDS, RETIRED_LINE_RE, AWAITING_FOR_MAX_MS, SnoozeUntil, SNOOZE_PROMPT_MAX, SnoozePrompt, RECURRING_PROMPT_MAX, RECURRING_MIN_INTERVAL_SECONDS, RECURRING_MAX_INTERVAL_SECONDS, RecurringPromptText, RecurringIntervalSeconds, ThreadRecurringPrompt, TIMER_PROMPT_MAX, TIMER_MAX_DELAY_SECONDS, TimerPromptText, SIGNOFF_NUDGE_MARKER, SIGNOFF_NUDGE_MESSAGE, SIGNOFF_NUDGE_MESSAGE_NEEDS_INPUT, ThreadWatchKind, GithubChecksState, GithubMergeState, GithubWatchStatus, GithubIssueStatus, ThreadWatchView, PR_WATCH_FOR_MAX_MS, PR_WATCH_DEFAULT_FOR_MS, PrWatchView, AddOwnPrWatchInput, AddOwnPrWatchResult, DropOwnPrWatchInput, DropOwnPrWatchResult, OwnWatchKind, AddOwnWatchInput, OwnWatchView, AddOwnWatchResult, DropOwnWatchInput, DropOwnWatchResult, AskQuestionKind, AskedOptionSchema, AskedQuestionSchema, AskInput, RegisteredQuestionView, SECRET_TTL_MS, AskResult, UnaskInput, UnaskResult, QuestionAnswerSchema, AnswerQuestionsInput, AnswerQuestionsResult, DismissQuestionsInput, DismissQuestionsResult, SettledQuestionView, ThreadSettledQuestionsResult, MarkOwnDoneInput, MarkOwnDoneResult, ListOwnPrWatchesInput, OwnPrWatchesResult, ThreadTimerView, ThreadFence, LimitWindow, LimitPause, ProviderError, ThreadLinkView, UpsertOwnLinkInput, UpsertOwnLinkResult, DropOwnLinkInput, DropOwnLinkResult, LiveTool, ThreadView, BoardErrorItem, BoardSnapshot, QuotaWindow, ProviderQuota, QuotaSnapshot, ProviderAuth, AccountEmails, AuthSnapshot, AccountBackend, AccountLogoutInput, AccountLogoutResult, PermissionMode, LocalFileOpener, Settings, DispatchProviderPreferences, DispatchPreferences, DispatchProfileSnapshotShape, DispatchProfileSnapshot, SetDispatchPreferenceInput, DispatchInput, ADOPT_THREAD_MESSAGE_MAX_CHARS, AdoptThreadInput, AdoptThreadResult, AdoptSessionInput, FollowUpInput, DismissFailedFollowUpInput, DismissFailedFollowUpResult, UnqueueFollowUpInput, UnqueueFollowUpResult, DeliverQueuedNowInput, DeliverQueuedNowResult, CompactThreadInput, SetThreadSnoozeInput, SetThreadPinnedInput, SetThreadRecurringPromptInput, SetOwnThreadRecurringPromptInput, SetOwnThreadRecurringPromptResult, GetOwnThreadRecurringPromptInput, OwnThreadRecurringPromptResult, SetOwnThreadTimerInput, CancelOwnThreadTimerInput, ListOwnThreadTimersInput, OwnThreadTimersResult, ThreadActivityItem, ListOwnThreadActivityInput, OwnThreadActivityResult, SetOwnThreadTimerResult, CancelOwnThreadTimerResult, SetOwnThreadStopHookInput, SetOwnThreadHeartbeatInput, ThreadPluginReloadResult, RenameThreadInput, AiRenameThreadInput, AiRenameThreadResult, SetOwnThreadTitleInput, SetOwnThreadTitleResult, SetThreadPermissionInput, SetThreadPermissionResult, ThreadProfileOptionsInput, ThreadProfileOptionsResult, ThreadSkillSource, ThreadSkill, ThreadSkillsInput, ThreadSkillsResult, SetThreadProfileInput, SetThreadProfileResult, UpgradeThreadModelInput, UpgradeThreadModelResult, DISPATCH_TASK_BANNER, DISPATCH_TASK_BANNER_MARKER, HUMAN_GAP_FLOOR_MS, GithubWakeItem, GithubWakeSteer, WAKE_SCOPE, WAKE_REF, WAKE_SINGLE, WAKE_MULTI_HEAD, PR_WATCH_FINISHED, PR_WATCH_CI_PASSED, PR_WATCH_CI_GATED, PR_WATCH_STATE, PR_WATCH_CI_FAILED, GithubStatus, GithubRefCard, GithubRefPreviewInput, GithubRefPreviewResult, GithubItem, GithubListInput, GithubListResult, GithubBatchInput, GithubBatchResult, BoardMeta, ServerEvent, TranscriptEdit, TranscriptTodo, TranscriptToolCall, TranscriptPart, TranscriptMessage, TranscriptPageCursor, EditedFile, TranscriptPage, TranscriptEarlierInput, SocketTranscriptSlug, SocketFilePath, SocketTranscriptClientMsg, SocketFileClientMsg, SocketClientMsg, ProjectCard, ProjectRailCounts, PROJECT_ICON_MAX_BASE64_CHARS, DirectoryPickResult, ThreadLocation, FRIZZ_ROUTE_PREFIX, DEFAULT_PORT;
|
|
12597
13258
|
var init_src = __esm({
|
|
12598
13259
|
"packages/shared/src/index.ts"() {
|
|
12599
13260
|
"use strict";
|
|
@@ -12606,6 +13267,7 @@ var init_src = __esm({
|
|
|
12606
13267
|
init_code_fences();
|
|
12607
13268
|
init_delta();
|
|
12608
13269
|
init_drainable_worker();
|
|
13270
|
+
init_frizz_md();
|
|
12609
13271
|
init_interactions();
|
|
12610
13272
|
init_receipt_bus();
|
|
12611
13273
|
init_relay_protocol();
|
|
@@ -12931,7 +13593,7 @@ var init_src = __esm({
|
|
|
12931
13593
|
questions: external_exports.array(AskQuestion)
|
|
12932
13594
|
});
|
|
12933
13595
|
AwaitingHint = external_exports.object({
|
|
12934
|
-
kind: external_exports.enum(["shell", "agent", "timer", "pr", "issue", "for", "title", "needs_input"]),
|
|
13596
|
+
kind: external_exports.enum(["shell", "agent", "timer", "pr", "issue", "for", "title", "status", "needs_input", "step", "question"]),
|
|
12935
13597
|
value: external_exports.string()
|
|
12936
13598
|
});
|
|
12937
13599
|
RETIRED_AWAITING_KINDS = ["watch", "pr-watch", "human", "ci", "session", "shell", "agent", "timer", "pr", "reason"];
|
|
@@ -13199,10 +13861,11 @@ var init_src = __esm({
|
|
|
13199
13861
|
header: external_exports.string().trim().max(24).optional(),
|
|
13200
13862
|
kind: AskQuestionKind,
|
|
13201
13863
|
danger: external_exports.boolean().optional(),
|
|
13864
|
+
secret: external_exports.boolean().optional(),
|
|
13202
13865
|
// UNBOUNDED, deliberately. This carried `.max(8)` from launch until 2026-09-03, when the maintainer
|
|
13203
13866
|
// asked for the cap to go ("allow arbitrary numbers of options"): a `multi` over a long list — which
|
|
13204
|
-
// gates to run, which of twenty findings to act on — is a real shape, and the card
|
|
13205
|
-
// (
|
|
13867
|
+
// gates to run, which of twenty findings to act on — is a real shape, and the card numbers past 9
|
|
13868
|
+
// (only 1–9 are keys) already. The count is the worker's to choose; the answer's `chosen` is unbounded to match.
|
|
13206
13869
|
options: external_exports.array(AskedOptionSchema).optional()
|
|
13207
13870
|
}).strict());
|
|
13208
13871
|
AskInput = external_exports.object({
|
|
@@ -13216,8 +13879,12 @@ var init_src = __esm({
|
|
|
13216
13879
|
* an id alone cannot be correlated back to what was asked. */
|
|
13217
13880
|
id: external_exports.string(),
|
|
13218
13881
|
spec: AskedQuestionSchema,
|
|
13219
|
-
askedAt: external_exports.string()
|
|
13882
|
+
askedAt: external_exports.string(),
|
|
13883
|
+
/** A `secret` question only: the path its value will be served at. Known at registration, so the
|
|
13884
|
+
* worker can prepare the command that reads it before the human has answered. */
|
|
13885
|
+
secretPath: external_exports.string().optional()
|
|
13220
13886
|
}).strict();
|
|
13887
|
+
SECRET_TTL_MS = 15 * 6e4;
|
|
13221
13888
|
AskResult = external_exports.object({
|
|
13222
13889
|
registered: external_exports.array(RegisteredQuestionView),
|
|
13223
13890
|
/** Everything still open on this thread afterwards, so a worker never needs a second call. */
|
|
@@ -13532,6 +14199,13 @@ var init_src = __esm({
|
|
|
13532
14199
|
// Optional like needsYou/crashed: absent ⇒ a pre-restart server or a non-session row; the client
|
|
13533
14200
|
// treats absence as false.
|
|
13534
14201
|
awaitingBackground: external_exports.boolean().optional(),
|
|
14202
|
+
// SERVER-DERIVED: which band a rest OUT OF THE QUEUE sits in (board.deriveWaitStatus, 2026-10-05).
|
|
14203
|
+
// `working` — the work it waits on finishes by itself, so it is Running and spins; `watching` —
|
|
14204
|
+
// something must happen outside the thread, so it is Snoozed and still. Present only for a thread at
|
|
14205
|
+
// rest behind a wait the server honoured with no queue card; absent everywhere else, including every
|
|
14206
|
+
// queued rest, which never spins. Absent from a pre-2026-10-05 server, which the client reads as the
|
|
14207
|
+
// old Running placement.
|
|
14208
|
+
waitStatus: external_exports.enum(["working", "watching"]).optional(),
|
|
13535
14209
|
// Exact typed-interaction presence for this CURRENT registered session. The board already derives
|
|
13536
14210
|
// this from the scoped durable journal to compute needsYou; exposing the reason lets React avoid a
|
|
13537
14211
|
// pendingInteractions RPC for every unrelated question/completion card. Optional preserves rolling
|
|
@@ -13600,7 +14274,7 @@ var init_src = __esm({
|
|
|
13600
14274
|
// further to do; otherwise the composer offers the upgrade, and the first message after the thread's
|
|
13601
14275
|
// next compaction takes it on its own.
|
|
13602
14276
|
modelUpgrade: external_exports.object({ label: external_exports.string(), staged: external_exports.boolean() }).optional(),
|
|
13603
|
-
// How full the session's context window is right now — the
|
|
14277
|
+
// How full the session's context window is right now — the header's fullness readout. BOTH halves
|
|
13604
14278
|
// are provider-measured and the field is emitted ONLY when both are present, so a client never has
|
|
13605
14279
|
// to decide what to do with half a fraction: absent ⇒ no reading, never a 0% dial. Codex reports
|
|
13606
14280
|
// both on every `token_count`; a Claude row gets `tokens` from each assistant record's usage but
|
|
@@ -14702,14 +15376,588 @@ ${DISPATCH_TASK_BANNER}
|
|
|
14702
15376
|
}
|
|
14703
15377
|
});
|
|
14704
15378
|
|
|
14705
|
-
// packages/server/src/
|
|
14706
|
-
import {
|
|
14707
|
-
import {
|
|
14708
|
-
import {
|
|
14709
|
-
function
|
|
14710
|
-
|
|
14711
|
-
|
|
14712
|
-
|
|
15379
|
+
// packages/server/src/restart-supervisor.ts
|
|
15380
|
+
import { request as requestHttp, createServer } from "node:http";
|
|
15381
|
+
import { connect } from "node:net";
|
|
15382
|
+
import { pipeline } from "node:stream";
|
|
15383
|
+
function responseJson(res, status, value) {
|
|
15384
|
+
res.writeHead(status, { "content-type": "application/json; charset=utf-8", "cache-control": "no-store" });
|
|
15385
|
+
res.end(JSON.stringify(value));
|
|
15386
|
+
}
|
|
15387
|
+
function proxyHeaders(req, childPort, vouchSameOrigin = false) {
|
|
15388
|
+
const headers = { ...req.headers };
|
|
15389
|
+
headers.host = `127.0.0.1:${childPort}`;
|
|
15390
|
+
if (typeof headers.origin === "string") headers.origin = `http://127.0.0.1:${childPort}`;
|
|
15391
|
+
for (const name of FORWARDED_HEADERS) delete headers[name];
|
|
15392
|
+
if (vouchSameOrigin) headers["sec-fetch-site"] = "same-origin";
|
|
15393
|
+
delete headers.connection;
|
|
15394
|
+
return headers;
|
|
15395
|
+
}
|
|
15396
|
+
function readCookie(header, name) {
|
|
15397
|
+
if (!header) return void 0;
|
|
15398
|
+
for (const pair of header.split(";")) {
|
|
15399
|
+
const eq = pair.indexOf("=");
|
|
15400
|
+
if (eq === -1) continue;
|
|
15401
|
+
if (pair.slice(0, eq).trim() === name) return pair.slice(eq + 1).trim();
|
|
15402
|
+
}
|
|
15403
|
+
return void 0;
|
|
15404
|
+
}
|
|
15405
|
+
function isControlRequest(req) {
|
|
15406
|
+
const url = new URL(req.url ?? "/", "http://frizz.invalid");
|
|
15407
|
+
return url.pathname === SUPERVISOR_RESTART_PATH || url.pathname === SUPERVISOR_UPDATE_RESTART_PATH || url.pathname === SUPERVISOR_STATUS_PATH || url.pathname === SUPERVISOR_ACCESS_CODE_PATH || url.pathname === SUPERVISOR_SESSIONS_PATH || url.pathname === SUPERVISOR_SIGN_OUT_PATH;
|
|
15408
|
+
}
|
|
15409
|
+
function isLocalImageRequest(req) {
|
|
15410
|
+
if (req.method !== "GET" && req.method !== "HEAD") return false;
|
|
15411
|
+
const { pathname } = new URL(req.url ?? "/", "http://frizz.invalid");
|
|
15412
|
+
return LOCAL_IMAGE_PATH.test(pathname);
|
|
15413
|
+
}
|
|
15414
|
+
async function readJsonBody(req, limit = 64 * 1024) {
|
|
15415
|
+
const chunks = [];
|
|
15416
|
+
let size = 0;
|
|
15417
|
+
for await (const chunk of req) {
|
|
15418
|
+
size += chunk.byteLength;
|
|
15419
|
+
if (size > limit) return null;
|
|
15420
|
+
chunks.push(chunk);
|
|
15421
|
+
}
|
|
15422
|
+
if (chunks.length === 0) return {};
|
|
15423
|
+
try {
|
|
15424
|
+
const parsed = JSON.parse(Buffer.concat(chunks).toString("utf8"));
|
|
15425
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
|
|
15426
|
+
} catch {
|
|
15427
|
+
return null;
|
|
15428
|
+
}
|
|
15429
|
+
}
|
|
15430
|
+
var SUPERVISOR_CONTROL_PREFIX, SUPERVISOR_RESTART_PATH, SUPERVISOR_UPDATE_RESTART_PATH, SUPERVISOR_STATUS_PATH, SUPERVISOR_ACCESS_CODE_PATH, SUPERVISOR_SESSIONS_PATH, SUPERVISOR_SIGN_OUT_PATH, SUPERVISOR_CONTROL_PROTOCOL, ACCESS_CODE_PARAM, SESSION_COOKIE, LOOPBACK_HOSTNAMES, LOCAL_IMAGE_PATH, RestartSupervisorProxy;
|
|
15431
|
+
var init_restart_supervisor = __esm({
|
|
15432
|
+
"packages/server/src/restart-supervisor.ts"() {
|
|
15433
|
+
"use strict";
|
|
15434
|
+
init_access_codes();
|
|
15435
|
+
init_local_origin();
|
|
15436
|
+
init_local_image();
|
|
15437
|
+
init_supervisor_pages();
|
|
15438
|
+
init_src();
|
|
15439
|
+
SUPERVISOR_CONTROL_PREFIX = "/_frizz/control";
|
|
15440
|
+
SUPERVISOR_RESTART_PATH = `${SUPERVISOR_CONTROL_PREFIX}/restart`;
|
|
15441
|
+
SUPERVISOR_UPDATE_RESTART_PATH = `${SUPERVISOR_CONTROL_PREFIX}/update-restart`;
|
|
15442
|
+
SUPERVISOR_STATUS_PATH = `${SUPERVISOR_CONTROL_PREFIX}/status`;
|
|
15443
|
+
SUPERVISOR_ACCESS_CODE_PATH = `${SUPERVISOR_CONTROL_PREFIX}/access-code`;
|
|
15444
|
+
SUPERVISOR_SESSIONS_PATH = `${SUPERVISOR_CONTROL_PREFIX}/sessions`;
|
|
15445
|
+
SUPERVISOR_SIGN_OUT_PATH = `${SUPERVISOR_CONTROL_PREFIX}/sign-out`;
|
|
15446
|
+
SUPERVISOR_CONTROL_PROTOCOL = 1;
|
|
15447
|
+
ACCESS_CODE_PARAM = "frizz_code";
|
|
15448
|
+
SESSION_COOKIE = "frizz_session";
|
|
15449
|
+
LOOPBACK_HOSTNAMES = /* @__PURE__ */ new Set(["localhost", "127.0.0.1", "::1"]);
|
|
15450
|
+
LOCAL_IMAGE_PATH = new RegExp(`^${FRIZZ_ROUTE_PREFIX}(?:/[^/]+)?/local-image$`);
|
|
15451
|
+
RestartSupervisorProxy = class {
|
|
15452
|
+
server = null;
|
|
15453
|
+
restartInFlight = null;
|
|
15454
|
+
/** Which action `restartInFlight` is. An update's prepare phase leaves the child serving; a restart does not. */
|
|
15455
|
+
transition = null;
|
|
15456
|
+
state = "ready";
|
|
15457
|
+
message;
|
|
15458
|
+
options;
|
|
15459
|
+
host;
|
|
15460
|
+
/**
|
|
15461
|
+
* The authorities a browser may name for the PUBLIC port. This proxy is the whole boundary: it
|
|
15462
|
+
* rewrites Host/Origin to the child's private loopback authority, so the child's own strict gate
|
|
15463
|
+
* can never see (or reject) the real browser authority. Everything this class forwards has to be
|
|
15464
|
+
* judged here first.
|
|
15465
|
+
*/
|
|
15466
|
+
policy;
|
|
15467
|
+
/** The origin a tunnel or proxy serves this board at; undefined while loopback-only. Live: see setPublicOrigin. */
|
|
15468
|
+
publicOrigin;
|
|
15469
|
+
/**
|
|
15470
|
+
* Codes and sessions for the public origin, or null when no public origin is declared (loopback-only
|
|
15471
|
+
* boards are never gated, so there is nothing to store).
|
|
15472
|
+
*/
|
|
15473
|
+
access;
|
|
15474
|
+
/** Both halves of every live upgraded pair, so close() can actually finish. See close(). */
|
|
15475
|
+
upgradedSockets = /* @__PURE__ */ new Set();
|
|
15476
|
+
constructor(options2) {
|
|
15477
|
+
this.options = options2;
|
|
15478
|
+
this.host = options2.host ?? LOOPBACK_BIND_HOST;
|
|
15479
|
+
this.publicOrigin = options2.publicOrigin;
|
|
15480
|
+
this.access = new AccessStore({
|
|
15481
|
+
onConsumed: () => options2.onCodeConsumed?.(),
|
|
15482
|
+
...options2.sessionKey ? { signingKey: options2.sessionKey } : {},
|
|
15483
|
+
...options2.rotateSessionKey ? { rotateSigningKey: options2.rotateSessionKey } : {},
|
|
15484
|
+
...options2.sessionDirectory ? { sessions: options2.sessionDirectory } : {}
|
|
15485
|
+
});
|
|
15486
|
+
this.policy = this.buildPolicy();
|
|
15487
|
+
}
|
|
15488
|
+
/**
|
|
15489
|
+
* Change the public origin on a running board — what the R pane does when the operator picks or
|
|
15490
|
+
* clears a remote setup. Requests already in flight finish under the policy they started with; the
|
|
15491
|
+
* next one is judged by the new origin. Sessions survive the change.
|
|
15492
|
+
*/
|
|
15493
|
+
setPublicOrigin(origin) {
|
|
15494
|
+
this.publicOrigin = origin;
|
|
15495
|
+
this.policy = this.buildPolicy();
|
|
15496
|
+
}
|
|
15497
|
+
buildPolicy() {
|
|
15498
|
+
return {
|
|
15499
|
+
exposed: bindHostIsExposed(this.host),
|
|
15500
|
+
allowedHosts: this.options.allowedHosts ?? [],
|
|
15501
|
+
...this.publicOrigin ? { publicOrigin: this.publicOrigin } : {}
|
|
15502
|
+
};
|
|
15503
|
+
}
|
|
15504
|
+
/**
|
|
15505
|
+
* Mint a single-use code for this board, or null when no public origin is declared.
|
|
15506
|
+
*
|
|
15507
|
+
* Issuing does NOT invalidate codes already outstanding — pressing the key twice, or two people
|
|
15508
|
+
* asking at once, must not silently break the first QR somebody is still walking towards.
|
|
15509
|
+
*/
|
|
15510
|
+
issueAccessCode() {
|
|
15511
|
+
return this.publicOrigin ? this.access.issue() : null;
|
|
15512
|
+
}
|
|
15513
|
+
/** The full URL to show as a link or a QR. */
|
|
15514
|
+
accessUrl(code) {
|
|
15515
|
+
const origin = this.publicOrigin;
|
|
15516
|
+
return origin ? `${origin}/?${ACCESS_CODE_PARAM}=${encodeURIComponent(code)}` : null;
|
|
15517
|
+
}
|
|
15518
|
+
async listen() {
|
|
15519
|
+
if (this.server) return;
|
|
15520
|
+
const server = createServer((req, res) => this.handle(req, res));
|
|
15521
|
+
server.keepAliveTimeout = 5e3;
|
|
15522
|
+
server.headersTimeout = 1e4;
|
|
15523
|
+
server.on("upgrade", (req, socket, head) => this.handleUpgrade(req, socket, head));
|
|
15524
|
+
await new Promise((resolveListen, rejectListen) => {
|
|
15525
|
+
server.once("error", rejectListen);
|
|
15526
|
+
server.listen(this.options.port, this.host, () => {
|
|
15527
|
+
server.off("error", rejectListen);
|
|
15528
|
+
resolveListen();
|
|
15529
|
+
});
|
|
15530
|
+
});
|
|
15531
|
+
this.server = server;
|
|
15532
|
+
}
|
|
15533
|
+
/**
|
|
15534
|
+
* Is this request allowed to reach the child at all?
|
|
15535
|
+
*
|
|
15536
|
+
* Deliberately the WEAKER of the two gates: a missing Origin passes here and the child then applies
|
|
15537
|
+
* its own per-route rule for it (only /health, /control/stop and `Sec-Fetch-Site: same-origin` may
|
|
15538
|
+
* omit one). What this stops is the thing the child cannot: an Origin naming somebody ELSE being
|
|
15539
|
+
* laundered into a valid same-origin one by proxyHeaders below.
|
|
15540
|
+
*/
|
|
15541
|
+
authorityAccepted(req, allowMissingOrigin = true) {
|
|
15542
|
+
return isTrustedLocalHttpRequest(req.headers, this.options.port, allowMissingOrigin, this.policy);
|
|
15543
|
+
}
|
|
15544
|
+
/**
|
|
15545
|
+
* Is this an Origin-less request that only LOOKS suspicious because the browser could not send
|
|
15546
|
+
* Fetch Metadata to a non-loopback authority? See authoritySendsFetchMetadata.
|
|
15547
|
+
*
|
|
15548
|
+
* Frizz's missing-Origin rules ask for `Sec-Fetch-Site: same-origin`, which Chrome never sends over
|
|
15549
|
+
* plain HTTP to a LAN address — so without this, `--host` serves the shell and then 403s every RPC
|
|
15550
|
+
* the app makes. Narrow on purpose: the Host must already be an authority this proxy accepts, there
|
|
15551
|
+
* must be no Origin at all (a present one still has to match, and is rejected above if it does not),
|
|
15552
|
+
* and loopback traffic is never touched, so the default posture keeps the real Sec-Fetch signal.
|
|
15553
|
+
*
|
|
15554
|
+
* The condition is `authoritySendsFetchMetadata`, not "is the bind address exposed", because those
|
|
15555
|
+
* two came apart with `--public-origin`: a tunnelled board stays on loopback yet is reached at a
|
|
15556
|
+
* name. An https proxy origin is potentially trustworthy, so the real signal survives and this
|
|
15557
|
+
* never fires for it; a plain-http one is in the same boat as a LAN address and needs the vouch.
|
|
15558
|
+
*
|
|
15559
|
+
* What this gives up, stated plainly: on an exposed board a cross-site GET carries no Origin and no
|
|
15560
|
+
* Fetch Metadata, so it is indistinguishable from the app's own read and is allowed. The response is
|
|
15561
|
+
* still opaque to the caller (no CORS), Frizz's GET procedures are reads, and every mutation is a POST
|
|
15562
|
+
* — which a browser always stamps with an Origin, and which is therefore still refused.
|
|
15563
|
+
*/
|
|
15564
|
+
vouchesSameOrigin(req) {
|
|
15565
|
+
if (req.headers.origin !== void 0 || req.headers["sec-fetch-site"] !== void 0) return false;
|
|
15566
|
+
const host = parseLocalHost(req.headers.host, this.options.port, this.policy);
|
|
15567
|
+
return !!host && !authoritySendsFetchMetadata(host);
|
|
15568
|
+
}
|
|
15569
|
+
async close() {
|
|
15570
|
+
const server = this.server;
|
|
15571
|
+
this.server = null;
|
|
15572
|
+
if (!server) return;
|
|
15573
|
+
await new Promise((resolveClose, rejectClose) => {
|
|
15574
|
+
server.close((error) => error ? rejectClose(error) : resolveClose());
|
|
15575
|
+
server.closeAllConnections();
|
|
15576
|
+
for (const socket of this.upgradedSockets) socket.destroy();
|
|
15577
|
+
this.upgradedSockets.clear();
|
|
15578
|
+
});
|
|
15579
|
+
}
|
|
15580
|
+
status() {
|
|
15581
|
+
const delegated = this.options.status?.();
|
|
15582
|
+
const updateVersion = this.options.updateVersion?.();
|
|
15583
|
+
const version = typeof this.options.version === "function" ? this.options.version() : this.options.version;
|
|
15584
|
+
const owner = { state: this.state, ...this.message ? { message: this.message } : {} };
|
|
15585
|
+
const reported = this.transition === "update" && delegated ? { ...delegated, ...delegated.state === "ready" ? { preparing: true } : {} } : this.restartInFlight !== null || this.state === "failed" ? owner : delegated ?? owner;
|
|
15586
|
+
return {
|
|
15587
|
+
...delegated ?? {},
|
|
15588
|
+
...reported,
|
|
15589
|
+
// Never infer this from the generic protocol: legacy/static supervisors can recover a child
|
|
15590
|
+
// but cannot build and promote the canonical Frizz artifact.
|
|
15591
|
+
updateRestart: typeof this.options.updateRestart === "function",
|
|
15592
|
+
// Sent ONLY when the launcher can actually answer it, so an older client — and frizz-dev, which
|
|
15593
|
+
// has no notion of "already current" — keeps today's behaviour on its absence.
|
|
15594
|
+
...this.options.updateAvailable ? { updateAvailable: this.options.updateAvailable() === true } : {},
|
|
15595
|
+
// The active SERVER version changes on a child-only update. Its absence keeps frizz-dev and
|
|
15596
|
+
// old monolithic supervisors' status payload and versionless UI exactly as they were.
|
|
15597
|
+
...version ? { version } : {},
|
|
15598
|
+
...this.options.launcherVersion ? { launcherVersion: this.options.launcherVersion } : {},
|
|
15599
|
+
...updateVersion ? { updateVersion } : {},
|
|
15600
|
+
// Sent only when TRUE, so a published Frizz's payload is byte-identical to what it sends today
|
|
15601
|
+
// and an older client is unaffected. Absent therefore means "not a development build".
|
|
15602
|
+
...this.options.dev ? { dev: true } : {}
|
|
15603
|
+
};
|
|
15604
|
+
}
|
|
15605
|
+
async runAction(action, transition) {
|
|
15606
|
+
if (this.restartInFlight) return this.restartInFlight;
|
|
15607
|
+
this.transition = transition;
|
|
15608
|
+
this.state = "restarting";
|
|
15609
|
+
this.message = void 0;
|
|
15610
|
+
const work = (async () => {
|
|
15611
|
+
await new Promise((resume) => setImmediate(resume));
|
|
15612
|
+
return action();
|
|
15613
|
+
})().then(
|
|
15614
|
+
(result) => {
|
|
15615
|
+
this.state = result.state;
|
|
15616
|
+
this.message = result.message;
|
|
15617
|
+
return result;
|
|
15618
|
+
},
|
|
15619
|
+
(error) => {
|
|
15620
|
+
const result = { state: "failed", message: error instanceof Error ? error.message : String(error) };
|
|
15621
|
+
this.state = result.state;
|
|
15622
|
+
this.message = result.message;
|
|
15623
|
+
return result;
|
|
15624
|
+
}
|
|
15625
|
+
).finally(() => {
|
|
15626
|
+
this.restartInFlight = null;
|
|
15627
|
+
this.transition = null;
|
|
15628
|
+
});
|
|
15629
|
+
this.restartInFlight = work;
|
|
15630
|
+
return work;
|
|
15631
|
+
}
|
|
15632
|
+
async handleControl(req, res) {
|
|
15633
|
+
const pathname = new URL(req.url ?? "/", "http://frizz.invalid").pathname;
|
|
15634
|
+
if (pathname !== SUPERVISOR_SIGN_OUT_PATH && this.arrivedPublicly(req) && !this.sessionAccepted(req)) {
|
|
15635
|
+
res.writeHead(401, { "cache-control": "no-store" });
|
|
15636
|
+
res.end();
|
|
15637
|
+
return;
|
|
15638
|
+
}
|
|
15639
|
+
const sameOrigin = req.headers["sec-fetch-site"] === "same-origin" || this.vouchesSameOrigin(req);
|
|
15640
|
+
const allowMissingOrigin = pathname === SUPERVISOR_STATUS_PATH && req.method === "GET" && sameOrigin;
|
|
15641
|
+
if (!this.authorityAccepted(req, allowMissingOrigin)) {
|
|
15642
|
+
res.writeHead(403);
|
|
15643
|
+
res.end("Forbidden");
|
|
15644
|
+
return;
|
|
15645
|
+
}
|
|
15646
|
+
if (pathname === SUPERVISOR_ACCESS_CODE_PATH) {
|
|
15647
|
+
if (this.arrivedPublicly(req)) {
|
|
15648
|
+
res.writeHead(403);
|
|
15649
|
+
res.end("Forbidden");
|
|
15650
|
+
return;
|
|
15651
|
+
}
|
|
15652
|
+
if (req.method !== "POST") {
|
|
15653
|
+
res.writeHead(405, { allow: "POST" });
|
|
15654
|
+
res.end();
|
|
15655
|
+
return;
|
|
15656
|
+
}
|
|
15657
|
+
const link = this.issueAccessCode();
|
|
15658
|
+
const url = link && this.accessUrl(link.code);
|
|
15659
|
+
if (!link || !url) {
|
|
15660
|
+
responseJson(res, 409, { error: "this board has no public origin, so there is nothing to link to" });
|
|
15661
|
+
return;
|
|
15662
|
+
}
|
|
15663
|
+
responseJson(res, 200, { url, expiresAt: link.expiresAt });
|
|
15664
|
+
return;
|
|
15665
|
+
}
|
|
15666
|
+
if (pathname === SUPERVISOR_SESSIONS_PATH) {
|
|
15667
|
+
if (this.arrivedPublicly(req)) {
|
|
15668
|
+
res.writeHead(403);
|
|
15669
|
+
res.end("Forbidden");
|
|
15670
|
+
return;
|
|
15671
|
+
}
|
|
15672
|
+
if (!this.publicOrigin) {
|
|
15673
|
+
responseJson(res, 409, { error: "this board has no public origin, so nothing is signed in" });
|
|
15674
|
+
return;
|
|
15675
|
+
}
|
|
15676
|
+
if (req.method === "GET") {
|
|
15677
|
+
responseJson(res, 200, { sessions: this.access.sessions.list() });
|
|
15678
|
+
return;
|
|
15679
|
+
}
|
|
15680
|
+
if (req.method !== "POST") {
|
|
15681
|
+
res.writeHead(405, { allow: "GET, POST" });
|
|
15682
|
+
res.end();
|
|
15683
|
+
return;
|
|
15684
|
+
}
|
|
15685
|
+
const body = await readJsonBody(req);
|
|
15686
|
+
const id = typeof body?.id === "string" ? body.id : void 0;
|
|
15687
|
+
if (body?.all === true) {
|
|
15688
|
+
responseJson(res, 200, { signedOut: this.access.signOutAll() });
|
|
15689
|
+
return;
|
|
15690
|
+
}
|
|
15691
|
+
if (!id) {
|
|
15692
|
+
responseJson(res, 400, { error: "name a device id, or pass all: true" });
|
|
15693
|
+
return;
|
|
15694
|
+
}
|
|
15695
|
+
if (!this.access.sessions.revoke(id)) {
|
|
15696
|
+
responseJson(res, 404, { error: `no signed-in device has the id ${id}` });
|
|
15697
|
+
return;
|
|
15698
|
+
}
|
|
15699
|
+
responseJson(res, 200, { signedOut: 1 });
|
|
15700
|
+
return;
|
|
15701
|
+
}
|
|
15702
|
+
if (pathname === SUPERVISOR_SIGN_OUT_PATH) {
|
|
15703
|
+
if (req.method !== "POST") {
|
|
15704
|
+
res.writeHead(405, { allow: "POST" });
|
|
15705
|
+
res.end();
|
|
15706
|
+
return;
|
|
15707
|
+
}
|
|
15708
|
+
this.signOutThisDevice(req, res);
|
|
15709
|
+
return;
|
|
15710
|
+
}
|
|
15711
|
+
if (pathname === SUPERVISOR_STATUS_PATH && req.method === "GET") {
|
|
15712
|
+
responseJson(res, 200, {
|
|
15713
|
+
protocol: SUPERVISOR_CONTROL_PROTOCOL,
|
|
15714
|
+
...this.status(),
|
|
15715
|
+
// Sent ONLY when true, so a loopback tab's payload is byte-identical to what it was. The web
|
|
15716
|
+
// app cannot tell on its own: the session cookie is HttpOnly, and the page is the same bundle
|
|
15717
|
+
// whichever way it was reached. It is a fact about THIS request, never about the board.
|
|
15718
|
+
...this.carriesRemoteSession(req) ? { remoteSession: true } : {}
|
|
15719
|
+
});
|
|
15720
|
+
return;
|
|
15721
|
+
}
|
|
15722
|
+
if (pathname !== SUPERVISOR_RESTART_PATH && pathname !== SUPERVISOR_UPDATE_RESTART_PATH || req.method !== "POST") {
|
|
15723
|
+
res.writeHead(405, { allow: pathname === SUPERVISOR_STATUS_PATH ? "GET" : "POST" });
|
|
15724
|
+
res.end();
|
|
15725
|
+
return;
|
|
15726
|
+
}
|
|
15727
|
+
if (pathname === SUPERVISOR_UPDATE_RESTART_PATH && !this.options.updateRestart) {
|
|
15728
|
+
responseJson(res, 409, { protocol: SUPERVISOR_CONTROL_PROTOCOL, state: "failed", message: "Update & Restart is available only for a stable immutable Frizz artifact" });
|
|
15729
|
+
return;
|
|
15730
|
+
}
|
|
15731
|
+
if (pathname === SUPERVISOR_UPDATE_RESTART_PATH) {
|
|
15732
|
+
void this.runAction(this.options.updateRestart, "update");
|
|
15733
|
+
responseJson(res, 202, { protocol: SUPERVISOR_CONTROL_PROTOCOL, state: "restarting" });
|
|
15734
|
+
return;
|
|
15735
|
+
}
|
|
15736
|
+
const result = await this.runAction(this.options.restart, "restart");
|
|
15737
|
+
responseJson(res, result.state === "ready" ? 202 : 503, { protocol: SUPERVISOR_CONTROL_PROTOCOL, ...result });
|
|
15738
|
+
}
|
|
15739
|
+
handleLocalImage(req, res) {
|
|
15740
|
+
const allowMissingOrigin = req.headers.origin === void 0 && (req.headers["sec-fetch-site"] === "same-origin" || this.vouchesSameOrigin(req));
|
|
15741
|
+
if (!this.authorityAccepted(req, allowMissingOrigin)) {
|
|
15742
|
+
res.writeHead(403, { "content-type": "text/plain; charset=UTF-8" });
|
|
15743
|
+
res.end("Forbidden");
|
|
15744
|
+
return;
|
|
15745
|
+
}
|
|
15746
|
+
const url = new URL(req.url ?? "/", "http://frizz.invalid");
|
|
15747
|
+
const result = resolveLocalImage(url.searchParams.get("path") ?? void 0, req.headers.range);
|
|
15748
|
+
const origin = typeof req.headers.origin === "string" ? allowedLocalCorsOrigin(req.headers.origin, this.options.port, this.policy) : void 0;
|
|
15749
|
+
const sharedHeaders = {
|
|
15750
|
+
...origin ? { "access-control-allow-origin": origin } : {},
|
|
15751
|
+
"access-control-expose-headers": "x-frizz-boot",
|
|
15752
|
+
vary: "Origin"
|
|
15753
|
+
};
|
|
15754
|
+
if (result.status === 416) {
|
|
15755
|
+
res.writeHead(416, { ...sharedHeaders, "content-range": `bytes */${result.size}`, "accept-ranges": "bytes" });
|
|
15756
|
+
res.end();
|
|
15757
|
+
return;
|
|
15758
|
+
}
|
|
15759
|
+
if (result.status === 400 || result.status === 404) {
|
|
15760
|
+
res.writeHead(result.status, { ...sharedHeaders, "content-type": "text/plain; charset=UTF-8" });
|
|
15761
|
+
res.end(String(result.status));
|
|
15762
|
+
return;
|
|
15763
|
+
}
|
|
15764
|
+
res.writeHead(result.status, { ...sharedHeaders, ...localImageHeaders(result) });
|
|
15765
|
+
const stream = req.method === "HEAD" ? null : localImageStream(result);
|
|
15766
|
+
if (!stream) {
|
|
15767
|
+
res.end();
|
|
15768
|
+
return;
|
|
15769
|
+
}
|
|
15770
|
+
pipeline(stream, res, () => void 0);
|
|
15771
|
+
}
|
|
15772
|
+
/**
|
|
15773
|
+
* Is this request arriving as the declared public origin rather than over loopback?
|
|
15774
|
+
*
|
|
15775
|
+
* The token gate keys on this and not on "is the port exposed", because the two are independent:
|
|
15776
|
+
* a tunnelled board stays bound to 127.0.0.1 while being reachable from the internet by name.
|
|
15777
|
+
*/
|
|
15778
|
+
arrivedPublicly(req) {
|
|
15779
|
+
const declared = this.publicOrigin;
|
|
15780
|
+
if (!declared) return false;
|
|
15781
|
+
const host = parseLocalHost(req.headers.host, this.options.port, this.policy);
|
|
15782
|
+
return !!host && !LOOPBACK_HOSTNAMES.has(host.hostname);
|
|
15783
|
+
}
|
|
15784
|
+
/**
|
|
15785
|
+
* Bearer check for the public origin. Two ways in: the cookie the exchange below sets, or the
|
|
15786
|
+
* one-time `?frizz_token=` in the URL the operator was handed at launch.
|
|
15787
|
+
*
|
|
15788
|
+
* Compared with `timingSafeEqual` on equal-length buffers — a plain `===` on a secret leaks its
|
|
15789
|
+
* prefix to a patient attacker, and this secret is the only thing between the internet and a shell.
|
|
15790
|
+
*/
|
|
15791
|
+
/**
|
|
15792
|
+
* Is this request carrying a session this process minted?
|
|
15793
|
+
*
|
|
15794
|
+
* Sessions only. A raw `?frizz_code=` is NOT accepted here — it is redeemed once by exchangeCode()
|
|
15795
|
+
* below and traded for a session, so a code that has already been spent cannot keep working just by
|
|
15796
|
+
* staying in somebody's URL bar.
|
|
15797
|
+
*/
|
|
15798
|
+
sessionAccepted(req) {
|
|
15799
|
+
if (!this.publicOrigin) return true;
|
|
15800
|
+
return this.access.verifySession(readCookie(req.headers.cookie, SESSION_COOKIE));
|
|
15801
|
+
}
|
|
15802
|
+
/** Did this request arrive through the public origin carrying a live session? Loopback never does. */
|
|
15803
|
+
carriesRemoteSession(req) {
|
|
15804
|
+
return this.arrivedPublicly(req) && this.access.verifySession(readCookie(req.headers.cookie, SESSION_COOKIE));
|
|
15805
|
+
}
|
|
15806
|
+
/**
|
|
15807
|
+
* End the session this request carries, and nothing else — see SUPERVISOR_SIGN_OUT_PATH in
|
|
15808
|
+
* handleControl for why that makes it safe to reach from the tunnel.
|
|
15809
|
+
*
|
|
15810
|
+
* A loopback request is a no-op even if it somehow carries a cookie: the operator's own tab has no
|
|
15811
|
+
* remote session to end, and loopback is the place `--sign-out` already covers.
|
|
15812
|
+
*/
|
|
15813
|
+
signOutThisDevice(req, res) {
|
|
15814
|
+
const reply = (status, body, clearCookie) => {
|
|
15815
|
+
res.writeHead(status, {
|
|
15816
|
+
"content-type": "application/json; charset=utf-8",
|
|
15817
|
+
"cache-control": "no-store",
|
|
15818
|
+
// Same attributes the exchange set, so the browser matches and drops that exact cookie.
|
|
15819
|
+
...clearCookie ? { "set-cookie": `${SESSION_COOKIE}=; Path=/; HttpOnly; Secure; SameSite=Lax; Max-Age=0` } : {}
|
|
15820
|
+
});
|
|
15821
|
+
res.end(JSON.stringify(body));
|
|
15822
|
+
};
|
|
15823
|
+
if (!this.arrivedPublicly(req)) {
|
|
15824
|
+
reply(200, { protocol: SUPERVISOR_CONTROL_PROTOCOL, result: "no-remote-session" }, false);
|
|
15825
|
+
return;
|
|
15826
|
+
}
|
|
15827
|
+
const outcome = this.access.signOutSession(readCookie(req.headers.cookie, SESSION_COOKIE));
|
|
15828
|
+
if (outcome.result === "revoked") {
|
|
15829
|
+
reply(200, { protocol: SUPERVISOR_CONTROL_PROTOCOL, result: "signed-out", id: outcome.id }, true);
|
|
15830
|
+
return;
|
|
15831
|
+
}
|
|
15832
|
+
if (outcome.result === "legacy") {
|
|
15833
|
+
reply(200, { protocol: SUPERVISOR_CONTROL_PROTOCOL, result: "cookie-cleared" }, true);
|
|
15834
|
+
return;
|
|
15835
|
+
}
|
|
15836
|
+
reply(401, { protocol: SUPERVISOR_CONTROL_PROTOCOL, result: "no-remote-session" }, true);
|
|
15837
|
+
}
|
|
15838
|
+
/**
|
|
15839
|
+
* Trade `?frizz_code=…` for a cookie, then bounce to the same URL
|
|
15840
|
+
* without the secret, so it never lands in history, a Referer header, or a screenshot.
|
|
15841
|
+
*/
|
|
15842
|
+
exchangeCode(req, res) {
|
|
15843
|
+
const url = new URL(req.url ?? "/", "http://frizz.invalid");
|
|
15844
|
+
const code = url.searchParams.get(ACCESS_CODE_PARAM);
|
|
15845
|
+
if (code === null) return false;
|
|
15846
|
+
const redeemed = this.access.redeem(code, describeDevice(req.headers["user-agent"]));
|
|
15847
|
+
if (!redeemed?.ok) {
|
|
15848
|
+
res.writeHead(401, { "content-type": "text/html; charset=utf-8", "cache-control": "no-store" });
|
|
15849
|
+
res.end(unauthorizedPage(redeemed?.reason));
|
|
15850
|
+
return true;
|
|
15851
|
+
}
|
|
15852
|
+
const cookie = `${SESSION_COOKIE}=${redeemed.session}`;
|
|
15853
|
+
const maxAge = Math.max(0, Math.floor((redeemed.expiresAt - Date.now()) / 1e3));
|
|
15854
|
+
url.searchParams.delete(ACCESS_CODE_PARAM);
|
|
15855
|
+
const target2 = `${url.pathname}${url.search}${url.hash}` || "/";
|
|
15856
|
+
res.writeHead(302, {
|
|
15857
|
+
location: target2,
|
|
15858
|
+
"set-cookie": `${cookie}; Path=/; HttpOnly; Secure; SameSite=Lax; Max-Age=${maxAge}`,
|
|
15859
|
+
"cache-control": "no-store"
|
|
15860
|
+
});
|
|
15861
|
+
res.end();
|
|
15862
|
+
return true;
|
|
15863
|
+
}
|
|
15864
|
+
handle(req, res) {
|
|
15865
|
+
if (isControlRequest(req)) {
|
|
15866
|
+
void this.handleControl(req, res);
|
|
15867
|
+
return;
|
|
15868
|
+
}
|
|
15869
|
+
if (this.arrivedPublicly(req) && this.exchangeCode(req, res)) return;
|
|
15870
|
+
if (this.arrivedPublicly(req) && !this.sessionAccepted(req)) {
|
|
15871
|
+
res.writeHead(401, { "content-type": "text/html; charset=utf-8", "cache-control": "no-store" });
|
|
15872
|
+
res.end(unauthorizedPage());
|
|
15873
|
+
return;
|
|
15874
|
+
}
|
|
15875
|
+
if (!this.authorityAccepted(req)) {
|
|
15876
|
+
const name = unlistedHostName(req.headers.host, this.options.port, this.policy);
|
|
15877
|
+
if (name) {
|
|
15878
|
+
res.writeHead(403, { "content-type": "text/html; charset=utf-8", "cache-control": "no-store" });
|
|
15879
|
+
res.end(req.method === "HEAD" ? void 0 : unlistedHostPage(name));
|
|
15880
|
+
return;
|
|
15881
|
+
}
|
|
15882
|
+
res.writeHead(403, { "content-type": "text/plain; charset=UTF-8" });
|
|
15883
|
+
res.end("Forbidden");
|
|
15884
|
+
return;
|
|
15885
|
+
}
|
|
15886
|
+
if (isLocalImageRequest(req)) {
|
|
15887
|
+
this.handleLocalImage(req, res);
|
|
15888
|
+
return;
|
|
15889
|
+
}
|
|
15890
|
+
const childPort = this.options.childPort();
|
|
15891
|
+
if (!childPort) {
|
|
15892
|
+
res.writeHead(503, { "content-type": "text/html; charset=utf-8", "cache-control": "no-store", "retry-after": "2" });
|
|
15893
|
+
res.end(recoveryPage(req.url ?? "/"));
|
|
15894
|
+
return;
|
|
15895
|
+
}
|
|
15896
|
+
const upstream = requestHttp({
|
|
15897
|
+
host: "127.0.0.1",
|
|
15898
|
+
port: childPort,
|
|
15899
|
+
method: req.method,
|
|
15900
|
+
path: req.url,
|
|
15901
|
+
headers: proxyHeaders(req, childPort, this.vouchesSameOrigin(req))
|
|
15902
|
+
}, (upstreamResponse) => {
|
|
15903
|
+
res.writeHead(upstreamResponse.statusCode ?? 502, upstreamResponse.headers);
|
|
15904
|
+
upstreamResponse.pipe(res);
|
|
15905
|
+
});
|
|
15906
|
+
upstream.once("error", () => {
|
|
15907
|
+
if (res.headersSent) return res.destroy();
|
|
15908
|
+
res.writeHead(503, { "content-type": "text/html; charset=utf-8", "cache-control": "no-store" });
|
|
15909
|
+
res.end(recoveryPage(req.url ?? "/", "unreachable"));
|
|
15910
|
+
});
|
|
15911
|
+
req.pipe(upstream);
|
|
15912
|
+
}
|
|
15913
|
+
handleUpgrade(req, socket, head) {
|
|
15914
|
+
if (this.arrivedPublicly(req) && !this.sessionAccepted(req)) {
|
|
15915
|
+
rejectWebSocketUpgrade(socket, 401, "Unauthorized");
|
|
15916
|
+
return;
|
|
15917
|
+
}
|
|
15918
|
+
if (!this.authorityAccepted(req, false)) {
|
|
15919
|
+
socket.destroy();
|
|
15920
|
+
return;
|
|
15921
|
+
}
|
|
15922
|
+
const childPort = this.options.childPort();
|
|
15923
|
+
if (!childPort) {
|
|
15924
|
+
socket.destroy();
|
|
15925
|
+
return;
|
|
15926
|
+
}
|
|
15927
|
+
const upstream = connect(childPort, "127.0.0.1");
|
|
15928
|
+
for (const half of [socket, upstream]) {
|
|
15929
|
+
this.upgradedSockets.add(half);
|
|
15930
|
+
half.once("close", () => this.upgradedSockets.delete(half));
|
|
15931
|
+
}
|
|
15932
|
+
upstream.once("connect", () => {
|
|
15933
|
+
const headers = proxyHeaders(req, childPort);
|
|
15934
|
+
headers.connection = req.headers.connection ?? "Upgrade";
|
|
15935
|
+
const lines = [`${req.method ?? "GET"} ${req.url ?? "/"} HTTP/${req.httpVersion}`];
|
|
15936
|
+
for (const [name, value] of Object.entries(headers)) {
|
|
15937
|
+
if (value === void 0) continue;
|
|
15938
|
+
for (const entry of Array.isArray(value) ? value : [value]) lines.push(`${name}: ${entry}`);
|
|
15939
|
+
}
|
|
15940
|
+
upstream.write(`${lines.join("\r\n")}\r
|
|
15941
|
+
\r
|
|
15942
|
+
`);
|
|
15943
|
+
if (head.length > 0) upstream.write(head);
|
|
15944
|
+
socket.pipe(upstream).pipe(socket);
|
|
15945
|
+
});
|
|
15946
|
+
upstream.once("error", () => socket.destroy());
|
|
15947
|
+
socket.once("error", () => upstream.destroy());
|
|
15948
|
+
}
|
|
15949
|
+
};
|
|
15950
|
+
}
|
|
15951
|
+
});
|
|
15952
|
+
|
|
15953
|
+
// packages/server/src/frizz-paths.ts
|
|
15954
|
+
import { existsSync as existsSync2 } from "node:fs";
|
|
15955
|
+
import { homedir as homedir2, tmpdir } from "node:os";
|
|
15956
|
+
import { dirname as dirname2, isAbsolute as isAbsolute2, join as join3, relative, resolve, sep as sep2 } from "node:path";
|
|
15957
|
+
function legacyFrizzRoot(home = homedir2()) {
|
|
15958
|
+
return join3(home, LEGACY_DIR_NAME);
|
|
15959
|
+
}
|
|
15960
|
+
function xdg(env, name) {
|
|
14713
15961
|
const value = env[name];
|
|
14714
15962
|
return value && (value.startsWith("/") || /^[A-Za-z]:[\\/]/u.test(value)) ? value : void 0;
|
|
14715
15963
|
}
|
|
@@ -14749,7 +15997,7 @@ function frizzPaths(options2 = {}) {
|
|
|
14749
15997
|
const env = options2.env ?? process.env;
|
|
14750
15998
|
const platform = options2.platform ?? process.platform;
|
|
14751
15999
|
const home = options2.home ?? homedir2();
|
|
14752
|
-
const exists = options2.exists ??
|
|
16000
|
+
const exists = options2.exists ?? existsSync2;
|
|
14753
16001
|
const legacyRoot = legacyFrizzRoot(home);
|
|
14754
16002
|
if (exists(legacyRoot)) {
|
|
14755
16003
|
return { data: legacyRoot, state: legacyRoot, cache: legacyRoot, legacy: true };
|
|
@@ -18666,7 +19914,7 @@ var init_logging = __esm({
|
|
|
18666
19914
|
// packages/server/src/process-generation.ts
|
|
18667
19915
|
import { execFileSync } from "node:child_process";
|
|
18668
19916
|
import { randomUUID } from "node:crypto";
|
|
18669
|
-
import { readFileSync as
|
|
19917
|
+
import { readFileSync as readFileSync4 } from "node:fs";
|
|
18670
19918
|
import { join as join7 } from "node:path";
|
|
18671
19919
|
function errorCode(error) {
|
|
18672
19920
|
return error && typeof error === "object" && "code" in error && typeof error.code === "string" ? error.code : void 0;
|
|
@@ -18682,9 +19930,9 @@ function processAlive(pid) {
|
|
|
18682
19930
|
}
|
|
18683
19931
|
function linuxGeneration(pid) {
|
|
18684
19932
|
try {
|
|
18685
|
-
const bootId =
|
|
19933
|
+
const bootId = readFileSync4("/proc/sys/kernel/random/boot_id", "utf8").trim().toLowerCase();
|
|
18686
19934
|
if (!/^[0-9a-f-]{36}$/u.test(bootId)) return null;
|
|
18687
|
-
const stat =
|
|
19935
|
+
const stat = readFileSync4(`/proc/${pid}/stat`, "utf8").trim();
|
|
18688
19936
|
const suffixAt = stat.lastIndexOf(") ");
|
|
18689
19937
|
if (suffixAt < 0) return null;
|
|
18690
19938
|
const fields = stat.slice(suffixAt + 2).trim().split(/\s+/u);
|
|
@@ -18805,7 +20053,7 @@ import {
|
|
|
18805
20053
|
fsyncSync,
|
|
18806
20054
|
mkdirSync as mkdirSync5,
|
|
18807
20055
|
openSync as openSync2,
|
|
18808
|
-
readFileSync as
|
|
20056
|
+
readFileSync as readFileSync5,
|
|
18809
20057
|
realpathSync as realpathSync2,
|
|
18810
20058
|
renameSync,
|
|
18811
20059
|
rmSync as rmSync3,
|
|
@@ -18867,7 +20115,7 @@ function lockOwnerPath(lockPath) {
|
|
|
18867
20115
|
}
|
|
18868
20116
|
function readOwner(lockPath) {
|
|
18869
20117
|
try {
|
|
18870
|
-
const value = JSON.parse(
|
|
20118
|
+
const value = JSON.parse(readFileSync5(lockOwnerPath(lockPath), "utf8"));
|
|
18871
20119
|
if (value.version !== 1 || !Number.isInteger(value.pid) || value.pid <= 0 || value.processStart !== void 0 && (typeof value.processStart !== "string" || value.processStart.length > 256) || typeof value.token !== "string" || !PROJECT_ID_RE.test(value.token) || typeof value.at !== "string") return void 0;
|
|
18872
20120
|
return value;
|
|
18873
20121
|
} catch {
|
|
@@ -18876,7 +20124,7 @@ function readOwner(lockPath) {
|
|
|
18876
20124
|
}
|
|
18877
20125
|
function legacyOwnerPid(lockPath) {
|
|
18878
20126
|
try {
|
|
18879
|
-
const value = JSON.parse(
|
|
20127
|
+
const value = JSON.parse(readFileSync5(lockOwnerPath(lockPath), "utf8"));
|
|
18880
20128
|
return Number.isInteger(value.pid) && value.pid > 0 ? value.pid : void 0;
|
|
18881
20129
|
} catch {
|
|
18882
20130
|
return void 0;
|
|
@@ -19222,7 +20470,7 @@ import {
|
|
|
19222
20470
|
fsyncSync as fsyncSync2,
|
|
19223
20471
|
mkdirSync as mkdirSync6,
|
|
19224
20472
|
openSync as openSync3,
|
|
19225
|
-
readFileSync as
|
|
20473
|
+
readFileSync as readFileSync6,
|
|
19226
20474
|
renameSync as renameSync2,
|
|
19227
20475
|
rmSync as rmSync4,
|
|
19228
20476
|
statSync as statSync4,
|
|
@@ -19302,7 +20550,7 @@ function parseGeneration(value) {
|
|
|
19302
20550
|
}
|
|
19303
20551
|
function parseGuard(path) {
|
|
19304
20552
|
try {
|
|
19305
|
-
const value = JSON.parse(
|
|
20553
|
+
const value = JSON.parse(readFileSync6(path, "utf8"));
|
|
19306
20554
|
const generation = parseGeneration(value);
|
|
19307
20555
|
if (value.version !== 1 || !generation || typeof value.token !== "string" || !UUID_RE.test(value.token) || !validText(value.at, 128)) return null;
|
|
19308
20556
|
return { version: 1, token: value.token, at: value.at, ...generation };
|
|
@@ -19382,7 +20630,7 @@ function parseDelegates(value) {
|
|
|
19382
20630
|
}
|
|
19383
20631
|
function parseOwner(path) {
|
|
19384
20632
|
try {
|
|
19385
|
-
const value = JSON.parse(
|
|
20633
|
+
const value = JSON.parse(readFileSync6(path, "utf8"));
|
|
19386
20634
|
const generation = parseGeneration(value);
|
|
19387
20635
|
if (value.version !== 1 && value.version !== OWNER_VERSION || !generation || typeof value.token !== "string" || !UUID_RE.test(value.token) || typeof value.projectId !== "string" || !UUID_RE.test(value.projectId) || !validText(value.projectDir) || !isAbsolute3(value.projectDir) || !validRole(value.role) || !validText(value.acquiredAt, 128) || !validText(value.updatedAt, 128)) return null;
|
|
19388
20636
|
const delegates = value.version === 1 ? [] : parseDelegates(value.delegates);
|
|
@@ -19424,7 +20672,7 @@ function removeStatusesForToken(stateDir, token) {
|
|
|
19424
20672
|
for (const name of ["dev-supervisor.lock", "server.lock"]) {
|
|
19425
20673
|
const path = join9(stateDir, name);
|
|
19426
20674
|
try {
|
|
19427
|
-
const value = JSON.parse(
|
|
20675
|
+
const value = JSON.parse(readFileSync6(path, "utf8"));
|
|
19428
20676
|
if (value.ownerToken === token) quarantine(path, "stale");
|
|
19429
20677
|
} catch {
|
|
19430
20678
|
}
|
|
@@ -19638,7 +20886,7 @@ function writeProjectStatus(path, value) {
|
|
|
19638
20886
|
}
|
|
19639
20887
|
function removeProjectStatus(path, expected2) {
|
|
19640
20888
|
try {
|
|
19641
|
-
const value = JSON.parse(
|
|
20889
|
+
const value = JSON.parse(readFileSync6(path, "utf8"));
|
|
19642
20890
|
if (value.pid !== expected2.pid || value.processStart !== expected2.processStart || value.publisherToken !== expected2.publisherToken || value.ownerToken !== expected2.ownerToken) return false;
|
|
19643
20891
|
return quarantine(path, "release");
|
|
19644
20892
|
} catch {
|
|
@@ -19672,14 +20920,14 @@ var init_project_launch = __esm({
|
|
|
19672
20920
|
});
|
|
19673
20921
|
|
|
19674
20922
|
// packages/server/src/boot-progress.ts
|
|
19675
|
-
import { mkdirSync as mkdirSync7, readFileSync as
|
|
20923
|
+
import { mkdirSync as mkdirSync7, readFileSync as readFileSync7, renameSync as renameSync3, rmSync as rmSync5, writeFileSync as writeFileSync7 } from "node:fs";
|
|
19676
20924
|
import { basename as basename3, dirname as dirname7, join as join10 } from "node:path";
|
|
19677
20925
|
function bootProgressPath(stateDir) {
|
|
19678
20926
|
return join10(stateDir, BOOT_PROGRESS_NAME);
|
|
19679
20927
|
}
|
|
19680
20928
|
function readBootProgress(stateDir) {
|
|
19681
20929
|
try {
|
|
19682
|
-
const value = JSON.parse(
|
|
20930
|
+
const value = JSON.parse(readFileSync7(bootProgressPath(stateDir), "utf8"));
|
|
19683
20931
|
if (!Number.isInteger(value.pid) || !Number.isInteger(value.step) || typeof value.phase !== "string" || typeof value.at !== "string") return null;
|
|
19684
20932
|
return value;
|
|
19685
20933
|
} catch {
|
|
@@ -19698,7 +20946,7 @@ var init_boot_progress = __esm({
|
|
|
19698
20946
|
|
|
19699
20947
|
// packages/server/src/project-root.ts
|
|
19700
20948
|
import { createHash as createHash3, randomUUID as randomUUID4 } from "node:crypto";
|
|
19701
|
-
import { closeSync as closeSync3, existsSync as
|
|
20949
|
+
import { closeSync as closeSync3, existsSync as existsSync4, fsyncSync as fsyncSync3, mkdirSync as mkdirSync8, openSync as openSync4, readFileSync as readFileSync8, realpathSync as realpathSync3, renameSync as renameSync4, rmSync as rmSync6, writeFileSync as writeFileSync8 } from "node:fs";
|
|
19702
20950
|
import { homedir as homedir8 } from "node:os";
|
|
19703
20951
|
import { dirname as dirname8, join as join11, parse, resolve as resolve3 } from "node:path";
|
|
19704
20952
|
function projectIdPath(root) {
|
|
@@ -19707,7 +20955,7 @@ function projectIdPath(root) {
|
|
|
19707
20955
|
function readProjectIdFile(root) {
|
|
19708
20956
|
let raw;
|
|
19709
20957
|
try {
|
|
19710
|
-
raw =
|
|
20958
|
+
raw = readFileSync8(projectIdPath(root), "utf8");
|
|
19711
20959
|
} catch {
|
|
19712
20960
|
return void 0;
|
|
19713
20961
|
}
|
|
@@ -19721,7 +20969,7 @@ function writeProjectIdFile(root, id) {
|
|
|
19721
20969
|
const dir = join11(root, FRIZZ_DIR);
|
|
19722
20970
|
mkdirSync8(dir, { recursive: true });
|
|
19723
20971
|
const ignore = join11(dir, SELF_IGNORE);
|
|
19724
|
-
if (!
|
|
20972
|
+
if (!existsSync4(ignore)) {
|
|
19725
20973
|
try {
|
|
19726
20974
|
writeFileSync8(ignore, "*\n", { flag: "wx" });
|
|
19727
20975
|
} catch {
|
|
@@ -19776,7 +21024,7 @@ function isNotAGitWorktree(error) {
|
|
|
19776
21024
|
return /not a git repository/iu.test(stderr) || /must be run in a work ?tree/iu.test(stderr);
|
|
19777
21025
|
}
|
|
19778
21026
|
function hasAny(dir, names) {
|
|
19779
|
-
return names.some((name) =>
|
|
21027
|
+
return names.some((name) => existsSync4(join11(dir, name)));
|
|
19780
21028
|
}
|
|
19781
21029
|
function isHomeDirectory(dir, home = homedir8()) {
|
|
19782
21030
|
const canonical = (value) => {
|
|
@@ -19818,7 +21066,7 @@ function discoverProjectRoot(cwd = process.cwd(), home = homedir8()) {
|
|
|
19818
21066
|
const filesystemRoot = parse(dir).root;
|
|
19819
21067
|
for (let at = dir; ; at = dirname8(at)) {
|
|
19820
21068
|
if (at === stop || at === filesystemRoot) break;
|
|
19821
|
-
if (
|
|
21069
|
+
if (existsSync4(projectIdPath(at))) return at;
|
|
19822
21070
|
if (hasAny(at, REPO_MARKERS)) return at;
|
|
19823
21071
|
if (hasAny(at, PROJECT_MARKERS)) return at;
|
|
19824
21072
|
if (dirname8(at) === at) break;
|
|
@@ -19851,7 +21099,7 @@ var init_project_root = __esm({
|
|
|
19851
21099
|
});
|
|
19852
21100
|
|
|
19853
21101
|
// packages/server/src/project-registry.ts
|
|
19854
|
-
import { closeSync as closeSync4, existsSync as
|
|
21102
|
+
import { closeSync as closeSync4, existsSync as existsSync5, fsyncSync as fsyncSync4, mkdirSync as mkdirSync9, openSync as openSync5, readFileSync as readFileSync9, readdirSync as readdirSync2, realpathSync as realpathSync4, renameSync as renameSync5, rmSync as rmSync7, writeFileSync as writeFileSync9 } from "node:fs";
|
|
19855
21103
|
import { homedir as homedir9 } from "node:os";
|
|
19856
21104
|
import { basename as basename4, dirname as dirname9, join as join12 } from "node:path";
|
|
19857
21105
|
function registryPath(home = homedir9()) {
|
|
@@ -19860,7 +21108,7 @@ function registryPath(home = homedir9()) {
|
|
|
19860
21108
|
function readRegistry(home = homedir9()) {
|
|
19861
21109
|
let raw;
|
|
19862
21110
|
try {
|
|
19863
|
-
raw =
|
|
21111
|
+
raw = readFileSync9(registryPath(home), "utf8");
|
|
19864
21112
|
} catch {
|
|
19865
21113
|
return { ...EMPTY, projects: [] };
|
|
19866
21114
|
}
|
|
@@ -19944,7 +21192,7 @@ function registerProject(input, home = homedir9()) {
|
|
|
19944
21192
|
writeRegistry(registry, home);
|
|
19945
21193
|
return { entry: byId, action: "reopened" };
|
|
19946
21194
|
}
|
|
19947
|
-
if (
|
|
21195
|
+
if (existsSync5(join12(byId.path, ".frizz", ".id"))) return { action: "duplicate" };
|
|
19948
21196
|
byId.path = input.dir;
|
|
19949
21197
|
byId.lastOpenedAt = at;
|
|
19950
21198
|
writeRegistry(registry, home);
|
|
@@ -19970,7 +21218,7 @@ function registerProject(input, home = homedir9()) {
|
|
|
19970
21218
|
}
|
|
19971
21219
|
function listProjects(home = homedir9()) {
|
|
19972
21220
|
const byRecency = (a, b) => a.lastOpenedAt < b.lastOpenedAt ? 1 : a.lastOpenedAt > b.lastOpenedAt ? -1 : 0;
|
|
19973
|
-
return readRegistry(home).projects.map((p) => ({ ...p, stale: !
|
|
21221
|
+
return readRegistry(home).projects.map((p) => ({ ...p, stale: !existsSync5(p.path) })).sort((a, b) => {
|
|
19974
21222
|
if (a.order === void 0 && b.order === void 0) return byRecency(a, b);
|
|
19975
21223
|
if (a.order === void 0) return 1;
|
|
19976
21224
|
if (b.order === void 0) return -1;
|
|
@@ -20032,7 +21280,7 @@ var init_project_registry = __esm({
|
|
|
20032
21280
|
|
|
20033
21281
|
// src/production.ts
|
|
20034
21282
|
init_local_origin();
|
|
20035
|
-
import { readFileSync as
|
|
21283
|
+
import { readFileSync as readFileSync14 } from "node:fs";
|
|
20036
21284
|
import { homedir as homedir11 } from "node:os";
|
|
20037
21285
|
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
20038
21286
|
import { join as join17 } from "node:path";
|
|
@@ -20487,268 +21735,29 @@ html:not([tabsintitlebar="true"]) #tabbrowser-tabs {
|
|
|
20487
21735
|
).unref();
|
|
20488
21736
|
}
|
|
20489
21737
|
|
|
20490
|
-
//
|
|
20491
|
-
|
|
20492
|
-
|
|
20493
|
-
|
|
20494
|
-
var
|
|
20495
|
-
|
|
20496
|
-
|
|
20497
|
-
|
|
20498
|
-
|
|
20499
|
-
|
|
20500
|
-
|
|
20501
|
-
|
|
20502
|
-
|
|
20503
|
-
|
|
20504
|
-
|
|
20505
|
-
|
|
20506
|
-
|
|
20507
|
-
|
|
20508
|
-
|
|
20509
|
-
|
|
20510
|
-
|
|
20511
|
-
|
|
20512
|
-
|
|
20513
|
-
},
|
|
20514
|
-
revokeAll() {
|
|
20515
|
-
let n = 0;
|
|
20516
|
-
for (const r of records.values()) if (r.revokedAt === void 0) {
|
|
20517
|
-
r.revokedAt = Date.now();
|
|
20518
|
-
n++;
|
|
20519
|
-
}
|
|
20520
|
-
return n;
|
|
20521
|
-
}
|
|
20522
|
-
};
|
|
20523
|
-
}
|
|
20524
|
-
function describeDevice(userAgent) {
|
|
20525
|
-
const ua = userAgent ?? "";
|
|
20526
|
-
if (!ua.trim()) return "unknown device";
|
|
20527
|
-
const os = /iPhone/i.test(ua) ? "iPhone" : /iPad/i.test(ua) ? "iPad" : /Android/i.test(ua) ? "Android" : /Mac OS X|Macintosh/i.test(ua) ? "macOS" : /Windows/i.test(ua) ? "Windows" : /Linux/i.test(ua) ? "Linux" : "unknown device";
|
|
20528
|
-
const browser = /Edg\//i.test(ua) ? "Edge" : /OPR\/|Opera/i.test(ua) ? "Opera" : /Firefox\//i.test(ua) ? "Firefox" : /Chrome\//i.test(ua) ? "Chrome" : /Safari\//i.test(ua) ? "Safari" : "";
|
|
20529
|
-
return browser ? `${browser} on ${os}` : os;
|
|
20530
|
-
}
|
|
20531
|
-
function defaultRandomToken(bytes) {
|
|
20532
|
-
return randomBytes(bytes).toString("base64url");
|
|
20533
|
-
}
|
|
20534
|
-
function secretsMatch(a, b) {
|
|
20535
|
-
const left = Buffer.from(a);
|
|
20536
|
-
const right = Buffer.from(b);
|
|
20537
|
-
return left.length === right.length && timingSafeEqual(left, right);
|
|
20538
|
-
}
|
|
20539
|
-
var AccessStore = class _AccessStore {
|
|
20540
|
-
codes = /* @__PURE__ */ new Map();
|
|
20541
|
-
options;
|
|
20542
|
-
sessions;
|
|
20543
|
-
/** Signing key for sessions. Persisted by the caller; a fresh one signs every device out. */
|
|
20544
|
-
signingKey;
|
|
20545
|
-
constructor(options2 = {}) {
|
|
20546
|
-
this.signingKey = options2.signingKey ?? randomBytes(32);
|
|
20547
|
-
this.sessions = options2.sessions ?? memorySessionDirectory();
|
|
20548
|
-
this.options = {
|
|
20549
|
-
codeTtlMs: options2.codeTtlMs ?? DEFAULT_CODE_TTL_MS,
|
|
20550
|
-
sessionTtlMs: options2.sessionTtlMs ?? DEFAULT_SESSION_TTL_MS,
|
|
20551
|
-
now: options2.now ?? Date.now,
|
|
20552
|
-
randomToken: options2.randomToken ?? defaultRandomToken,
|
|
20553
|
-
onConsumed: options2.onConsumed
|
|
20554
|
-
};
|
|
20555
|
-
}
|
|
20556
|
-
/** Mint a code to show as a QR or a link. Minting does not invalidate codes already outstanding. */
|
|
20557
|
-
issue() {
|
|
20558
|
-
this.sweep();
|
|
20559
|
-
const now = this.options.now();
|
|
20560
|
-
const code = {
|
|
20561
|
-
code: this.options.randomToken(CODE_BYTES),
|
|
20562
|
-
createdAt: now,
|
|
20563
|
-
expiresAt: now + this.options.codeTtlMs
|
|
20564
|
-
};
|
|
20565
|
-
this.codes.set(code.code, code);
|
|
20566
|
-
return { code: code.code, createdAt: code.createdAt, expiresAt: code.expiresAt };
|
|
20567
|
-
}
|
|
20568
|
-
/**
|
|
20569
|
-
* Trade a code for a session, at most once, ever.
|
|
20570
|
-
*
|
|
20571
|
-
* The consumed marker is set BEFORE anything else can observe success, and JS runs this method to
|
|
20572
|
-
* completion without interleaving, so two browsers racing the same code cannot both win. That
|
|
20573
|
-
* atomicity is what makes "single-use" true rather than aspirational — it is the first thing the
|
|
20574
|
-
* tests attack.
|
|
20575
|
-
*/
|
|
20576
|
-
redeem(code, device) {
|
|
20577
|
-
if (!code) return { ok: false, reason: "unknown" };
|
|
20578
|
-
const stored = this.codes.get(code);
|
|
20579
|
-
if (!stored || !secretsMatch(stored.code, code)) return { ok: false, reason: "unknown" };
|
|
20580
|
-
const now = this.options.now();
|
|
20581
|
-
if (stored.consumedAt !== void 0) return { ok: false, reason: "already-used" };
|
|
20582
|
-
if (now >= stored.expiresAt) {
|
|
20583
|
-
this.sweep();
|
|
20584
|
-
return { ok: false, reason: "expired" };
|
|
20585
|
-
}
|
|
20586
|
-
stored.consumedAt = now;
|
|
20587
|
-
this.sweep();
|
|
20588
|
-
this.options.onConsumed?.(code);
|
|
20589
|
-
const minted = this.mintSession(now);
|
|
20590
|
-
this.sessions.record({ id: minted.id, label: device ?? "unknown device", createdAt: now });
|
|
20591
|
-
return { ok: true, session: minted.session, expiresAt: now + this.options.sessionTtlMs, id: minted.id };
|
|
20592
|
-
}
|
|
20593
|
-
/**
|
|
20594
|
-
* A session is `<expiry>.<id>.<nonce>.<hmac>` — signed with a key the caller persists so it survives
|
|
20595
|
-
* a restart, and self-describing enough that a stale cookie is rejected without any lookup.
|
|
20596
|
-
*
|
|
20597
|
-
* The `id` is what makes ONE device signable-out. It is short because an operator types it into
|
|
20598
|
-
* `frizz --sign-out <id>`, and it is inside the signed payload so it cannot be swapped for another
|
|
20599
|
-
* device's. Rotating the key still revokes everything at once; this is the scalpel beside that axe.
|
|
20600
|
-
*
|
|
20601
|
-
* A session minted before ids existed has a two-part payload and still verifies — it simply has no id
|
|
20602
|
-
* to revoke individually. Upgrading must not sign every device out.
|
|
20603
|
-
*/
|
|
20604
|
-
mintSession(now) {
|
|
20605
|
-
const expiresAt = now + this.options.sessionTtlMs;
|
|
20606
|
-
const id = this.options.randomToken(SESSION_ID_BYTES);
|
|
20607
|
-
const nonce = this.options.randomToken(SESSION_BYTES);
|
|
20608
|
-
const payload = `${expiresAt}.${id}.${nonce}`;
|
|
20609
|
-
return { session: `${payload}.${this.sign(payload)}`, id };
|
|
20610
|
-
}
|
|
20611
|
-
/** The id inside a session payload, or null for a legacy two-part one. */
|
|
20612
|
-
static idOf(payload) {
|
|
20613
|
-
const parts = payload.split(".");
|
|
20614
|
-
return parts.length >= 3 ? parts[1] ?? null : null;
|
|
20615
|
-
}
|
|
20616
|
-
sign(payload) {
|
|
20617
|
-
return createHmac("sha256", this.signingKey).update(payload).digest("base64url");
|
|
20618
|
-
}
|
|
20619
|
-
/** Is this cookie a session signed by this board's key, and still in date? */
|
|
20620
|
-
verifySession(session) {
|
|
20621
|
-
if (!session) return false;
|
|
20622
|
-
const cut = session.lastIndexOf(".");
|
|
20623
|
-
if (cut <= 0) return false;
|
|
20624
|
-
const payload = session.slice(0, cut);
|
|
20625
|
-
const signature = session.slice(cut + 1);
|
|
20626
|
-
if (!secretsMatch(signature, this.sign(payload))) return false;
|
|
20627
|
-
const expiresAt = Number(payload.slice(0, payload.indexOf(".")));
|
|
20628
|
-
if (!Number.isFinite(expiresAt) || this.options.now() >= expiresAt) return false;
|
|
20629
|
-
const id = _AccessStore.idOf(payload);
|
|
20630
|
-
return !(id && this.sessions.isRevoked(id));
|
|
20631
|
-
}
|
|
20632
|
-
/**
|
|
20633
|
-
* Sign out the device HOLDING this session — the "Sign out this device" row, not `--sign-out`.
|
|
20634
|
-
*
|
|
20635
|
-
* The id comes from the session itself, and only after its signature verifies, so the one thing this
|
|
20636
|
-
* can ever revoke is the credential the caller just proved it holds. There is deliberately no
|
|
20637
|
-
* parameter naming any other id: signing out SOMEONE ELSE stays loopback-only, because a stolen
|
|
20638
|
-
* session must not be able to evict the owner and keep the board.
|
|
20639
|
-
*
|
|
20640
|
-
* - `revoked`: the id is on the denylist now, so a surviving copy of this cookie is refused too.
|
|
20641
|
-
* - `legacy`: a session minted before ids existed verifies but has no id to revoke on its own. The
|
|
20642
|
-
* caller can still clear the cookie; rotating the key remains the way to kill a copy of it.
|
|
20643
|
-
* - `no-session`: nothing valid was presented — absent, forged, expired, or already signed out.
|
|
20644
|
-
*/
|
|
20645
|
-
signOutSession(session) {
|
|
20646
|
-
if (!session || !this.verifySession(session)) return { result: "no-session" };
|
|
20647
|
-
const id = _AccessStore.idOf(session.slice(0, session.lastIndexOf(".")));
|
|
20648
|
-
if (!id) return { result: "legacy" };
|
|
20649
|
-
if (!this.sessions.revoke(id)) {
|
|
20650
|
-
this.sessions.record({ id, label: "unknown device", createdAt: this.options.now() });
|
|
20651
|
-
this.sessions.revoke(id);
|
|
20652
|
-
}
|
|
20653
|
-
return this.sessions.isRevoked(id) ? { result: "revoked", id } : { result: "no-session" };
|
|
20654
|
-
}
|
|
20655
|
-
/**
|
|
20656
|
-
* Drop codes past their expiry so a long-lived board does not accumulate them forever.
|
|
20657
|
-
*
|
|
20658
|
-
* A CONSUMED code is deliberately kept until that same expiry rather than deleted on use. Deleting it
|
|
20659
|
-
* immediately would make a replay indistinguishable from a typo — both "unknown" — and the difference
|
|
20660
|
-
* matters: "already-used" is the message that tells someone their link was used by somebody else.
|
|
20661
|
-
* It costs one small entry for at most the code TTL.
|
|
20662
|
-
*/
|
|
20663
|
-
sweep() {
|
|
20664
|
-
const now = this.options.now();
|
|
20665
|
-
for (const [key, stored] of this.codes) {
|
|
20666
|
-
if (now >= stored.expiresAt) this.codes.delete(key);
|
|
20667
|
-
}
|
|
20668
|
-
}
|
|
20669
|
-
/** Outstanding codes that could still be redeemed — neither consumed nor expired. */
|
|
20670
|
-
outstanding() {
|
|
20671
|
-
this.sweep();
|
|
20672
|
-
return [...this.codes.values()].filter((stored) => stored.consumedAt === void 0).map(({ code, createdAt, expiresAt }) => ({ code, createdAt, expiresAt }));
|
|
20673
|
-
}
|
|
20674
|
-
};
|
|
20675
|
-
function fileSessionDirectory(stateDir, now = Date.now) {
|
|
20676
|
-
const path = join2(stateDir, "sessions.json");
|
|
20677
|
-
let records = [];
|
|
20678
|
-
try {
|
|
20679
|
-
const parsed = JSON.parse(readFileSync(path, "utf8"));
|
|
20680
|
-
if (Array.isArray(parsed)) {
|
|
20681
|
-
records = parsed.filter(
|
|
20682
|
-
(r) => !!r && typeof r.id === "string" && typeof r.createdAt === "number"
|
|
20683
|
-
);
|
|
20684
|
-
}
|
|
20685
|
-
} catch {
|
|
20686
|
-
}
|
|
20687
|
-
const inner = memorySessionDirectory(records);
|
|
20688
|
-
const flush = () => {
|
|
20689
|
-
try {
|
|
20690
|
-
mkdirSync(dirname(path), { recursive: true });
|
|
20691
|
-
writeFileSync(path, JSON.stringify(inner.list()), { mode: 384 });
|
|
20692
|
-
} catch (error) {
|
|
20693
|
-
if (process.env.FRIZZ_DEBUG_SESSIONS) console.error("[sessions] flush failed:", error);
|
|
20694
|
-
}
|
|
20695
|
-
};
|
|
20696
|
-
return {
|
|
20697
|
-
record(r) {
|
|
20698
|
-
inner.record(r);
|
|
20699
|
-
flush();
|
|
20700
|
-
},
|
|
20701
|
-
isRevoked: (id) => inner.isRevoked(id),
|
|
20702
|
-
list: () => inner.list(),
|
|
20703
|
-
revoke(id) {
|
|
20704
|
-
const ok = inner.revoke(id);
|
|
20705
|
-
if (ok) flush();
|
|
20706
|
-
return ok;
|
|
20707
|
-
},
|
|
20708
|
-
revokeAll() {
|
|
20709
|
-
const n = inner.revokeAll();
|
|
20710
|
-
if (n > 0) flush();
|
|
20711
|
-
return n;
|
|
20712
|
-
}
|
|
20713
|
-
};
|
|
20714
|
-
}
|
|
20715
|
-
function loadOrCreateSessionKey(stateDir) {
|
|
20716
|
-
const path = join2(stateDir, "session-key");
|
|
20717
|
-
try {
|
|
20718
|
-
const existing = readFileSync(path);
|
|
20719
|
-
if (existing.byteLength >= 32) return existing;
|
|
20720
|
-
} catch {
|
|
20721
|
-
}
|
|
20722
|
-
const key = randomBytes(32);
|
|
20723
|
-
mkdirSync(dirname(path), { recursive: true });
|
|
20724
|
-
writeFileSync(path, key, { mode: 384 });
|
|
20725
|
-
try {
|
|
20726
|
-
chmodSync(path, 384);
|
|
20727
|
-
} catch {
|
|
20728
|
-
}
|
|
20729
|
-
return key;
|
|
20730
|
-
}
|
|
20731
|
-
|
|
20732
|
-
// node_modules/.pnpm/qrcode-generator@2.0.4/node_modules/qrcode-generator/dist/qrcode.mjs
|
|
20733
|
-
var qrcode = function(typeNumber, errorCorrectionLevel) {
|
|
20734
|
-
const PAD0 = 236;
|
|
20735
|
-
const PAD1 = 17;
|
|
20736
|
-
let _typeNumber = typeNumber;
|
|
20737
|
-
const _errorCorrectionLevel = QRErrorCorrectionLevel[errorCorrectionLevel];
|
|
20738
|
-
let _modules = null;
|
|
20739
|
-
let _moduleCount = 0;
|
|
20740
|
-
let _dataCache = null;
|
|
20741
|
-
const _dataList = [];
|
|
20742
|
-
const _this = {};
|
|
20743
|
-
const makeImpl = function(test, maskPattern) {
|
|
20744
|
-
_moduleCount = _typeNumber * 4 + 17;
|
|
20745
|
-
_modules = (function(moduleCount) {
|
|
20746
|
-
const modules = new Array(moduleCount);
|
|
20747
|
-
for (let row = 0; row < moduleCount; row += 1) {
|
|
20748
|
-
modules[row] = new Array(moduleCount);
|
|
20749
|
-
for (let col = 0; col < moduleCount; col += 1) {
|
|
20750
|
-
modules[row][col] = null;
|
|
20751
|
-
}
|
|
21738
|
+
// src/production.ts
|
|
21739
|
+
init_access_codes();
|
|
21740
|
+
|
|
21741
|
+
// node_modules/.pnpm/qrcode-generator@2.0.4/node_modules/qrcode-generator/dist/qrcode.mjs
|
|
21742
|
+
var qrcode = function(typeNumber, errorCorrectionLevel) {
|
|
21743
|
+
const PAD0 = 236;
|
|
21744
|
+
const PAD1 = 17;
|
|
21745
|
+
let _typeNumber = typeNumber;
|
|
21746
|
+
const _errorCorrectionLevel = QRErrorCorrectionLevel[errorCorrectionLevel];
|
|
21747
|
+
let _modules = null;
|
|
21748
|
+
let _moduleCount = 0;
|
|
21749
|
+
let _dataCache = null;
|
|
21750
|
+
const _dataList = [];
|
|
21751
|
+
const _this = {};
|
|
21752
|
+
const makeImpl = function(test, maskPattern) {
|
|
21753
|
+
_moduleCount = _typeNumber * 4 + 17;
|
|
21754
|
+
_modules = (function(moduleCount) {
|
|
21755
|
+
const modules = new Array(moduleCount);
|
|
21756
|
+
for (let row = 0; row < moduleCount; row += 1) {
|
|
21757
|
+
modules[row] = new Array(moduleCount);
|
|
21758
|
+
for (let col = 0; col < moduleCount; col += 1) {
|
|
21759
|
+
modules[row][col] = null;
|
|
21760
|
+
}
|
|
20752
21761
|
}
|
|
20753
21762
|
return modules;
|
|
20754
21763
|
})(_moduleCount);
|
|
@@ -22305,982 +23314,168 @@ var gifImage = function(width, height) {
|
|
|
22305
23314
|
s = s + c;
|
|
22306
23315
|
} else {
|
|
22307
23316
|
bitOut.write(table.indexOf(s), bitLength);
|
|
22308
|
-
if (table.size() < 4095) {
|
|
22309
|
-
if (table.size() == 1 << bitLength) {
|
|
22310
|
-
bitLength += 1;
|
|
22311
|
-
}
|
|
22312
|
-
table.add(s + c);
|
|
22313
|
-
}
|
|
22314
|
-
s = c;
|
|
22315
|
-
}
|
|
22316
|
-
}
|
|
22317
|
-
bitOut.write(table.indexOf(s), bitLength);
|
|
22318
|
-
bitOut.write(endCode, bitLength);
|
|
22319
|
-
bitOut.flush();
|
|
22320
|
-
return byteOut.toByteArray();
|
|
22321
|
-
};
|
|
22322
|
-
const lzwTable = function() {
|
|
22323
|
-
const _map = {};
|
|
22324
|
-
let _size = 0;
|
|
22325
|
-
const _this2 = {};
|
|
22326
|
-
_this2.add = function(key) {
|
|
22327
|
-
if (_this2.contains(key)) {
|
|
22328
|
-
throw "dup key:" + key;
|
|
22329
|
-
}
|
|
22330
|
-
_map[key] = _size;
|
|
22331
|
-
_size += 1;
|
|
22332
|
-
};
|
|
22333
|
-
_this2.size = function() {
|
|
22334
|
-
return _size;
|
|
22335
|
-
};
|
|
22336
|
-
_this2.indexOf = function(key) {
|
|
22337
|
-
return _map[key];
|
|
22338
|
-
};
|
|
22339
|
-
_this2.contains = function(key) {
|
|
22340
|
-
return typeof _map[key] != "undefined";
|
|
22341
|
-
};
|
|
22342
|
-
return _this2;
|
|
22343
|
-
};
|
|
22344
|
-
return _this;
|
|
22345
|
-
};
|
|
22346
|
-
var createDataURL = function(width, height, getPixel) {
|
|
22347
|
-
const gif = gifImage(width, height);
|
|
22348
|
-
for (let y = 0; y < height; y += 1) {
|
|
22349
|
-
for (let x = 0; x < width; x += 1) {
|
|
22350
|
-
gif.setPixel(x, y, getPixel(x, y));
|
|
22351
|
-
}
|
|
22352
|
-
}
|
|
22353
|
-
const b = byteArrayOutputStream();
|
|
22354
|
-
gif.write(b);
|
|
22355
|
-
const base64 = base64EncodeOutputStream();
|
|
22356
|
-
const bytes = b.toByteArray();
|
|
22357
|
-
for (let i = 0; i < bytes.length; i += 1) {
|
|
22358
|
-
base64.writeByte(bytes[i]);
|
|
22359
|
-
}
|
|
22360
|
-
base64.flush();
|
|
22361
|
-
return "data:image/gif;base64," + base64;
|
|
22362
|
-
};
|
|
22363
|
-
var qrcode_default = qrcode;
|
|
22364
|
-
var stringToBytes = qrcode.stringToBytes;
|
|
22365
|
-
|
|
22366
|
-
// packages/server/src/qr.ts
|
|
22367
|
-
var DARK = 232;
|
|
22368
|
-
var LIGHT = 231;
|
|
22369
|
-
var fg = (dark) => `\x1B[38;5;${dark ? DARK : LIGHT}m`;
|
|
22370
|
-
var bg = (dark) => `\x1B[48;5;${dark ? DARK : LIGHT}m`;
|
|
22371
|
-
var RESET = "\x1B[0m";
|
|
22372
|
-
var UPPER_HALF = "\u2580";
|
|
22373
|
-
var LOWER_HALF = "\u2584";
|
|
22374
|
-
var QUIET_ZONE = 4;
|
|
22375
|
-
var BLOCK_DRAWING_PROGRAMS = /* @__PURE__ */ new Set(["iTerm.app", "WezTerm", "ghostty", "WarpTerminal", "vscode"]);
|
|
22376
|
-
var BLOCK_DRAWING_TERMS = /* @__PURE__ */ new Set(["xterm-kitty", "xterm-ghostty", "alacritty", "wezterm"]);
|
|
22377
|
-
function drawsBlockElements(env = process.env) {
|
|
22378
|
-
const term = env.TERM ?? "";
|
|
22379
|
-
if (env.TMUX || env.STY || env.TERM_PROGRAM === "tmux" || term.startsWith("screen") || term.startsWith("tmux")) return false;
|
|
22380
|
-
if (BLOCK_DRAWING_PROGRAMS.has(env.TERM_PROGRAM ?? "")) return true;
|
|
22381
|
-
if (BLOCK_DRAWING_TERMS.has(term)) return true;
|
|
22382
|
-
if (env.LC_TERMINAL === "iTerm2") return true;
|
|
22383
|
-
return Boolean(
|
|
22384
|
-
env.KITTY_WINDOW_ID || env.ALACRITTY_WINDOW_ID || env.WEZTERM_PANE || env.GHOSTTY_RESOURCES_DIR || env.WT_SESSION || env.VTE_VERSION
|
|
22385
|
-
);
|
|
22386
|
-
}
|
|
22387
|
-
function encode(value, errorCorrection) {
|
|
22388
|
-
const code = qrcode_default(0, errorCorrection);
|
|
22389
|
-
code.addData(value);
|
|
22390
|
-
code.make();
|
|
22391
|
-
const modules = code.getModuleCount();
|
|
22392
|
-
const size = modules + QUIET_ZONE * 2;
|
|
22393
|
-
return {
|
|
22394
|
-
size,
|
|
22395
|
-
at: (x, y) => {
|
|
22396
|
-
const mx = x - QUIET_ZONE;
|
|
22397
|
-
const my = y - QUIET_ZONE;
|
|
22398
|
-
if (mx < 0 || my < 0 || mx >= modules || my >= modules) return false;
|
|
22399
|
-
return code.isDark(my, mx);
|
|
22400
|
-
}
|
|
22401
|
-
};
|
|
22402
|
-
}
|
|
22403
|
-
function sizeOf(size, style) {
|
|
22404
|
-
return style === "paint" ? { columns: size * 2, rows: size } : { columns: size, rows: Math.ceil(size / 2) };
|
|
22405
|
-
}
|
|
22406
|
-
function qrSize(value, style = "half", errorCorrection = "M") {
|
|
22407
|
-
return sizeOf(encode(value, errorCorrection).size, style);
|
|
22408
|
-
}
|
|
22409
|
-
function qrStyleFor(value, options2 = {}) {
|
|
22410
|
-
const { area } = options2;
|
|
22411
|
-
if (!area || drawsBlockElements(options2.env)) return "half";
|
|
22412
|
-
const paint = qrSize(value, "paint", options2.errorCorrection);
|
|
22413
|
-
return paint.columns <= area.columns && paint.rows <= area.rows ? "paint" : "half";
|
|
22414
|
-
}
|
|
22415
|
-
function qrAreaOf(stream, frame) {
|
|
22416
|
-
if (!stream.columns || !stream.rows) return void 0;
|
|
22417
|
-
return { columns: stream.columns - frame.indent, rows: stream.rows - frame.rows };
|
|
22418
|
-
}
|
|
22419
|
-
function halfLines(size, at, plain) {
|
|
22420
|
-
const lines = [];
|
|
22421
|
-
for (let y = 0; y < size; y += 2) {
|
|
22422
|
-
let line = "";
|
|
22423
|
-
for (let x = 0; x < size; x++) {
|
|
22424
|
-
const top = at(x, y);
|
|
22425
|
-
const bottom = y + 1 < size ? at(x, y + 1) : false;
|
|
22426
|
-
if (plain) {
|
|
22427
|
-
line += top && bottom ? "#" : top ? "^" : bottom ? "v" : " ";
|
|
22428
|
-
continue;
|
|
22429
|
-
}
|
|
22430
|
-
if (top === bottom) {
|
|
22431
|
-
line += `${bg(top)} `;
|
|
22432
|
-
continue;
|
|
22433
|
-
}
|
|
22434
|
-
const above = y > 0 ? at(x, y - 1) : false;
|
|
22435
|
-
const below = y + 2 < size ? at(x, y + 2) : false;
|
|
22436
|
-
line += below === bottom && above !== top ? `${fg(top)}${bg(bottom)}${UPPER_HALF}` : `${fg(bottom)}${bg(top)}${LOWER_HALF}`;
|
|
22437
|
-
}
|
|
22438
|
-
lines.push(plain ? line : `${line}${RESET}`);
|
|
22439
|
-
}
|
|
22440
|
-
return lines;
|
|
22441
|
-
}
|
|
22442
|
-
function paintLines(size, at, plain) {
|
|
22443
|
-
const lines = [];
|
|
22444
|
-
for (let y = 0; y < size; y++) {
|
|
22445
|
-
let line = "";
|
|
22446
|
-
let last;
|
|
22447
|
-
for (let x = 0; x < size; x++) {
|
|
22448
|
-
const dark = at(x, y);
|
|
22449
|
-
if (plain) line += dark ? "##" : " ";
|
|
22450
|
-
else {
|
|
22451
|
-
if (dark !== last) line += bg(dark);
|
|
22452
|
-
line += " ";
|
|
22453
|
-
}
|
|
22454
|
-
last = dark;
|
|
22455
|
-
}
|
|
22456
|
-
lines.push(plain ? line : `${line}${RESET}`);
|
|
22457
|
-
}
|
|
22458
|
-
return lines;
|
|
22459
|
-
}
|
|
22460
|
-
function renderQrLines(value, options2 = {}) {
|
|
22461
|
-
if (!value) throw new Error("renderQr requires a value");
|
|
22462
|
-
const errorCorrection = options2.errorCorrection ?? "M";
|
|
22463
|
-
const { size, at } = encode(value, errorCorrection);
|
|
22464
|
-
const style = options2.style ?? qrStyleFor(value, { area: options2.area, env: options2.env, errorCorrection });
|
|
22465
|
-
return style === "paint" ? paintLines(size, at, options2.plain ?? false) : halfLines(size, at, options2.plain ?? false);
|
|
22466
|
-
}
|
|
22467
|
-
|
|
22468
|
-
// packages/server/src/restart-supervisor.ts
|
|
22469
|
-
import { request as requestHttp, createServer } from "node:http";
|
|
22470
|
-
import { connect } from "node:net";
|
|
22471
|
-
init_local_origin();
|
|
22472
|
-
init_local_image();
|
|
22473
|
-
|
|
22474
|
-
// packages/server/src/supervisor-pages.ts
|
|
22475
|
-
function escapeHtml(value) {
|
|
22476
|
-
return value.replace(/[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[c]);
|
|
22477
|
-
}
|
|
22478
|
-
function retryTarget(url) {
|
|
22479
|
-
return url.startsWith("/") && !url.startsWith("//") ? url : "/";
|
|
22480
|
-
}
|
|
22481
|
-
var BASE_STYLE = `
|
|
22482
|
-
*,*::before,*::after{box-sizing:border-box}
|
|
22483
|
-
:root{
|
|
22484
|
-
color-scheme:dark;
|
|
22485
|
-
--bg:#0d0e10;--panel:#131519;--panel-2:#181b20;
|
|
22486
|
-
--border:#26282d;--border-strong:#33363c;--control-border:#26282d;--control-strong:#33363c;
|
|
22487
|
-
--fg:#e6e7e9;--muted:#8b8f96;--accent:#e8b923;--danger:#fca5a5;
|
|
22488
|
-
--focus-border:color-mix(in srgb,var(--accent) 55%,transparent);--focus-ring:color-mix(in srgb,var(--accent) 22%,transparent);
|
|
22489
|
-
--sans:system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;
|
|
22490
|
-
--mono:ui-monospace,"JetBrains Mono","SF Mono","Cascadia Mono",Menlo,Consolas,monospace;
|
|
22491
|
-
--ease:cubic-bezier(0,0,.2,1);
|
|
22492
|
-
}
|
|
22493
|
-
:root[data-theme=light]{
|
|
22494
|
-
color-scheme:light;
|
|
22495
|
-
--bg:#f7f7f7;--panel:#fff;--panel-2:#fafafa;
|
|
22496
|
-
--border:#dedede;--border-strong:#cecece;--control-border:#858585;--control-strong:#7d7d7d;
|
|
22497
|
-
--fg:#292929;--muted:#606060;--accent:#416896;--danger:#cf222e;
|
|
22498
|
-
--focus-border:#416896;--focus-ring:#416896;
|
|
22499
|
-
}
|
|
22500
|
-
@media(prefers-color-scheme:light){
|
|
22501
|
-
:root:not([data-theme]){
|
|
22502
|
-
color-scheme:light;
|
|
22503
|
-
--bg:#f7f7f7;--panel:#fff;--panel-2:#fafafa;
|
|
22504
|
-
--border:#dedede;--border-strong:#cecece;--control-border:#858585;--control-strong:#7d7d7d;
|
|
22505
|
-
--fg:#292929;--muted:#606060;--accent:#416896;--danger:#cf222e;
|
|
22506
|
-
--focus-border:#416896;--focus-ring:#416896;
|
|
22507
|
-
}
|
|
22508
|
-
}
|
|
22509
|
-
html,body{background:var(--bg)}
|
|
22510
|
-
body{
|
|
22511
|
-
margin:0;min-height:100vh;min-height:100dvh;
|
|
22512
|
-
display:flex;align-items:center;justify-content:center;padding:24px;
|
|
22513
|
-
color:var(--fg);font-family:var(--sans);font-size:12.5px;line-height:1.55;
|
|
22514
|
-
-webkit-font-smoothing:antialiased;
|
|
22515
|
-
}
|
|
22516
|
-
main{width:100%;max-width:28rem;padding:18px;border:1px solid var(--border-strong);border-radius:12px;background:var(--panel-2)}
|
|
22517
|
-
h1{margin:0;display:flex;align-items:baseline;gap:7px;font-size:14px;font-weight:600;letter-spacing:-.01em;line-height:1.4}
|
|
22518
|
-
p{margin:0;text-wrap:pretty}
|
|
22519
|
-
.detail{margin-top:7px;color:color-mix(in srgb,var(--fg) 88%,transparent)}
|
|
22520
|
-
.note{margin-top:12px;padding-top:11px;border-top:1px solid var(--border);color:var(--muted);font-size:11.5px;line-height:1.6}
|
|
22521
|
-
code{font-family:var(--mono);font-size:.92em;padding:.1em .35em;border-radius:4px;background:var(--panel);border:1px solid var(--border);white-space:nowrap}
|
|
22522
|
-
[hidden]{display:none!important}
|
|
22523
|
-
`.trim();
|
|
22524
|
-
var RECOVERY_STYLE = `
|
|
22525
|
-
.actions{margin-top:14px;display:flex;flex-wrap:wrap;gap:8px}
|
|
22526
|
-
/* ErrorBoundary's ACTION_CLASS, one size up for a page that is alone in a viewport. */
|
|
22527
|
-
.btn{
|
|
22528
|
-
appearance:none;display:inline-flex;align-items:center;
|
|
22529
|
-
border:1px solid var(--control-border);border-radius:6px;background:transparent;
|
|
22530
|
-
padding:5px 10px;font:inherit;font-size:11.5px;color:color-mix(in srgb,var(--fg) 90%,transparent);
|
|
22531
|
-
cursor:pointer;text-decoration:none;
|
|
22532
|
-
transition:background-color 120ms var(--ease),border-color 120ms var(--ease),color 120ms var(--ease);
|
|
22533
|
-
}
|
|
22534
|
-
.btn:hover{background:var(--panel);border-color:var(--control-strong);color:var(--fg)}
|
|
22535
|
-
.btn:focus-visible{outline:none;border-color:var(--focus-border);box-shadow:0 0 0 2px var(--focus-ring)}
|
|
22536
|
-
.btn:disabled{opacity:.5;pointer-events:none}
|
|
22537
|
-
/* min-height holds the slot open while it is LIVE, so a changing status line never nudges the card;
|
|
22538
|
-
:empty collapses it when there is nothing live to say, so a halted card has no trailing gutter. */
|
|
22539
|
-
.probe{margin-top:12px;font-family:var(--mono);font-size:11px;line-height:1.5;color:var(--muted);min-height:1.5em}
|
|
22540
|
-
.probe:empty{margin-top:0;min-height:0}
|
|
22541
|
-
/* RestartFailureNotice's block, at this altitude: a supervisor \`message\` is raw build output, and it
|
|
22542
|
-
reads as what it is \u2014 a terminal excerpt \u2014 only inside a scrolling mono box. */
|
|
22543
|
-
.log{
|
|
22544
|
-
margin-top:10px;max-height:16rem;overflow:auto;white-space:pre-wrap;overflow-wrap:break-word;
|
|
22545
|
-
border:1px solid var(--border);border-radius:6px;background:var(--panel);padding:8px 10px;
|
|
22546
|
-
font-family:var(--mono);font-size:11px;line-height:1.6;color:color-mix(in srgb,var(--fg) 70%,transparent);
|
|
22547
|
-
}
|
|
22548
|
-
|
|
22549
|
-
/* THE LIVE DOT, and it is the app's own \`.frizz-live-dot\` \u2014 same 6px, same 28%/14% halo, same 1.25s
|
|
22550
|
-
cadence \u2014 because it means the same thing here that it means there: work is in flight.
|
|
22551
|
-
|
|
22552
|
-
Alignment is an INK problem. A baseline-aligned flex item with no text of its own contributes its
|
|
22553
|
-
BORDER-BOX BOTTOM as the baseline, so the dot's ink centre lands 3px above it while the cap band's
|
|
22554
|
-
centre sits 0.5cap above it. \`cap\` is the resolved font's own cap height, so the correction holds
|
|
22555
|
-
at any size, with nothing to re-measure when the type scale moves. The em spelling is the fallback for a browser without the unit (.35em ~= 0.5 x a
|
|
22556
|
-
0.7em cap) and is overwritten wherever \`cap\` parses. */
|
|
22557
|
-
.dot{
|
|
22558
|
-
width:6px;height:6px;flex:0 0 auto;border-radius:9999px;background:var(--accent);
|
|
22559
|
-
box-shadow:0 0 0 1px color-mix(in srgb,var(--accent) 28%,transparent);
|
|
22560
|
-
animation:frizz-live-pulse 1.25s ease-in-out infinite;
|
|
22561
|
-
transform:translateY(calc(3px - .35em));
|
|
22562
|
-
transform:translateY(calc(3px - .5cap));
|
|
22563
|
-
}
|
|
22564
|
-
.dot[data-state=idle]{animation:none;background:var(--muted);box-shadow:none}
|
|
22565
|
-
.dot[data-state=bad]{animation:none;background:var(--danger);box-shadow:0 0 0 1px color-mix(in srgb,var(--danger) 28%,transparent)}
|
|
22566
|
-
@keyframes frizz-live-pulse{50%{opacity:.62;box-shadow:0 0 0 2px color-mix(in srgb,var(--accent) 14%,transparent)}}
|
|
22567
|
-
@media (prefers-reduced-motion:reduce){
|
|
22568
|
-
.dot{animation:none;background:transparent;border:2px solid var(--accent);box-shadow:none}
|
|
22569
|
-
.dot[data-state=idle]{border-color:var(--muted)}
|
|
22570
|
-
.dot[data-state=bad]{border-color:var(--danger);background:transparent}
|
|
22571
|
-
}
|
|
22572
|
-
|
|
22573
|
-
/* The app's \`.shimmer-text\`: a highlight sweeping muted text is how this product says "working", and
|
|
22574
|
-
the probe line is the only thing on the page that is. */
|
|
22575
|
-
.probe[data-live=yes]{
|
|
22576
|
-
color:transparent;
|
|
22577
|
-
background:linear-gradient(90deg,var(--muted) 0%,var(--muted) 35%,var(--fg) 50%,var(--muted) 65%,var(--muted) 100%) 0 0/200% 100%;
|
|
22578
|
-
-webkit-background-clip:text;background-clip:text;
|
|
22579
|
-
animation:frizz-shimmer 2.2s linear infinite;
|
|
22580
|
-
}
|
|
22581
|
-
@keyframes frizz-shimmer{from{background-position:200% 0}to{background-position:-200% 0}}
|
|
22582
|
-
@media (prefers-reduced-motion:reduce){
|
|
22583
|
-
.probe[data-live=yes]{animation:none;background:none;color:var(--muted)}
|
|
22584
|
-
}
|
|
22585
|
-
`.trim();
|
|
22586
|
-
var THEME_SCRIPT = `var p;try{p=localStorage.getItem("frizz-theme")}catch{}var d=false;try{d=matchMedia&&matchMedia("(prefers-color-scheme: dark)").matches}catch{}var t=p==="dark"||p==="light"?p:d?"dark":"light";document.documentElement.dataset.theme=t;document.documentElement.style.colorScheme=t;document.querySelector('meta[name="theme-color"]').content=t==="dark"?"#0d0e10":"#f7f7f7"`;
|
|
22587
|
-
var RECOVERY_SCRIPT = `
|
|
22588
|
-
(function(){
|
|
22589
|
-
var target=document.body.dataset.target||"/"
|
|
22590
|
-
var heading=document.getElementById("heading")
|
|
22591
|
-
var detail=document.getElementById("detail")
|
|
22592
|
-
var probe=document.getElementById("probe")
|
|
22593
|
-
var log=document.getElementById("log")
|
|
22594
|
-
var dot=document.getElementById("dot")
|
|
22595
|
-
var restart=document.getElementById("restart")
|
|
22596
|
-
var BUDGET=120000,MIN=600,MAX=4000,GROWTH=1.4
|
|
22597
|
-
var delay=MIN,deadline=Date.now()+BUDGET,timer=0,stopped=false,busy=false,told=""
|
|
22598
|
-
|
|
22599
|
-
function say(text,live){probe.textContent=text;probe.setAttribute("data-live",live?"yes":"no")}
|
|
22600
|
-
function halt(text,bad){stopped=true;clearTimeout(timer);dot.setAttribute("data-state",bad?"bad":"idle");say(text,false)}
|
|
22601
|
-
function showLog(message){if(message){log.textContent=message;log.hidden=false}else{log.hidden=true}}
|
|
22602
|
-
function resume(){stopped=false;busy=false;delay=MIN;deadline=Date.now()+BUDGET;dot.removeAttribute("data-state");clearTimeout(timer);timer=setTimeout(tick,0)}
|
|
22603
|
-
|
|
22604
|
-
// 503 is this proxy's own "no child". Every other answer \u2014 200, 404, even a 500 from the app \u2014 means
|
|
22605
|
-
// something is listening again, and the reader's own URL is the right thing to ask.
|
|
22606
|
-
function alive(){
|
|
22607
|
-
return fetch(target,{method:"HEAD",cache:"no-store",credentials:"same-origin"})
|
|
22608
|
-
.then(function(r){return r.status!==503}).catch(function(){return false})
|
|
22609
|
-
}
|
|
22610
|
-
function readStatus(){
|
|
22611
|
-
return fetch("/_frizz/control/status",{cache:"no-store",headers:{"cache-control":"no-store"}})
|
|
22612
|
-
.then(function(r){return r.ok?r.json():null})
|
|
22613
|
-
.then(function(b){return b&&b.protocol===1?b:null}).catch(function(){return null})
|
|
22614
|
-
}
|
|
22615
|
-
|
|
22616
|
-
// An unreachable launcher leaves the server-rendered copy alone: it is already right for the case
|
|
22617
|
-
// that rendered it, and guessing over it would be worse than saying nothing.
|
|
22618
|
-
function narrate(status){
|
|
22619
|
-
if(!status||status.state===told)return
|
|
22620
|
-
told=status.state
|
|
22621
|
-
if(status.state==="failed"){
|
|
22622
|
-
heading.textContent="Frizz did not restart"
|
|
22623
|
-
// Not a second copy of the note below it \u2014 a lead-in to the log, which is the next thing down.
|
|
22624
|
-
detail.textContent="The launcher could not bring the application server back, and reported this:"
|
|
22625
|
-
showLog(status.message||"The launcher gave no reason.")
|
|
22626
|
-
halt("",true)
|
|
22627
|
-
return
|
|
22628
|
-
}
|
|
22629
|
-
showLog("")
|
|
22630
|
-
if(status.state==="restarting"){
|
|
22631
|
-
heading.textContent="Frizz is restarting"
|
|
22632
|
-
detail.textContent="The application server is coming back up."
|
|
22633
|
-
}
|
|
22634
|
-
}
|
|
22635
|
-
|
|
22636
|
-
function tick(){
|
|
22637
|
-
if(stopped||busy)return
|
|
22638
|
-
// A hidden tab neither polls nor spends its budget; visibilitychange starts the loop again.
|
|
22639
|
-
if(document.hidden)return
|
|
22640
|
-
alive().then(function(up){
|
|
22641
|
-
if(up){say("Frizz is back. Reloading.",true);location.replace(target);return}
|
|
22642
|
-
return readStatus().then(function(status){
|
|
22643
|
-
narrate(status)
|
|
22644
|
-
if(stopped)return
|
|
22645
|
-
if(Date.now()>deadline){halt("No answer after 2m. This page stopped checking.",false);return}
|
|
22646
|
-
delay=Math.min(MAX,delay*GROWTH)
|
|
22647
|
-
say("Waiting for Frizz",true)
|
|
22648
|
-
timer=setTimeout(tick,delay)
|
|
22649
|
-
})
|
|
22650
|
-
})
|
|
22651
|
-
}
|
|
22652
|
-
|
|
22653
|
-
document.addEventListener("visibilitychange",function(){
|
|
22654
|
-
if(document.hidden||stopped||busy)return
|
|
22655
|
-
resume()
|
|
22656
|
-
})
|
|
22657
|
-
|
|
22658
|
-
restart.addEventListener("click",function(){
|
|
22659
|
-
if(busy)return
|
|
22660
|
-
busy=true;stopped=false;told="";clearTimeout(timer)
|
|
22661
|
-
restart.disabled=true;restart.textContent="Restarting"
|
|
22662
|
-
dot.removeAttribute("data-state");showLog("");say("Restarting Frizz",true)
|
|
22663
|
-
fetch("/_frizz/control/restart",{method:"POST",headers:{"cache-control":"no-store"}})
|
|
22664
|
-
.then(function(r){return r.json().catch(function(){return null}).then(function(b){return{ok:r.ok,status:r.status,body:b}})})
|
|
22665
|
-
.catch(function(){return{ok:false,status:0,body:null}})
|
|
22666
|
-
.then(function(r){
|
|
22667
|
-
restart.disabled=false;restart.textContent="Restart Frizz";busy=false
|
|
22668
|
-
if(!r.ok){
|
|
22669
|
-
heading.textContent="Frizz did not restart"
|
|
22670
|
-
detail.textContent="The launcher could not bring the application server back, and reported this:"
|
|
22671
|
-
showLog((r.body&&r.body.message)||("The launcher refused the restart ("+(r.status||"no response")+")."))
|
|
22672
|
-
halt("",true)
|
|
22673
|
-
return
|
|
22674
|
-
}
|
|
22675
|
-
// Accepted. The probe, not this response, decides whether the child is actually back.
|
|
22676
|
-
resume()
|
|
22677
|
-
})
|
|
22678
|
-
})
|
|
22679
|
-
|
|
22680
|
-
say("Waiting for Frizz",true)
|
|
22681
|
-
tick()
|
|
22682
|
-
})()
|
|
22683
|
-
`.trim();
|
|
22684
|
-
function documentShell(options2) {
|
|
22685
|
-
return `<!doctype html><html lang="en" data-font="sans"><head><meta charset="utf-8"><title>${options2.title}</title><meta name="viewport" content="width=device-width,initial-scale=1">` + (options2.appearance ? `<meta name="color-scheme" content="dark light"><meta name="theme-color" content="#0d0e10">` : `<meta name="color-scheme" content="dark light"><meta name="theme-color" media="(prefers-color-scheme: dark)" content="#0d0e10"><meta name="theme-color" media="(prefers-color-scheme: light)" content="#f7f7f7">`) + `<style>${options2.style ? `${BASE_STYLE}
|
|
22686
|
-
${options2.style}` : BASE_STYLE}</style>` + (options2.appearance ? `<script>${THEME_SCRIPT}</script>` : "") + `</head><body${options2.bodyAttributes ? ` ${options2.bodyAttributes}` : ""}><main>${options2.body}</main>` + (options2.script ? `<script>${options2.script}</script>` : "") + `</body></html>`;
|
|
22687
|
-
}
|
|
22688
|
-
var RECOVERY_COPY = {
|
|
22689
|
-
starting: {
|
|
22690
|
-
title: "Restarting | Frizz",
|
|
22691
|
-
heading: "Frizz is restarting",
|
|
22692
|
-
detail: "The application server is not listening yet. This page reloads as soon as it answers."
|
|
22693
|
-
},
|
|
22694
|
-
unreachable: {
|
|
22695
|
-
title: "Not responding | Frizz",
|
|
22696
|
-
heading: "Frizz is not responding",
|
|
22697
|
-
detail: "The application server accepted this request and then dropped it. Restarting it usually clears this."
|
|
22698
|
-
}
|
|
22699
|
-
};
|
|
22700
|
-
var THREADS_NOTE = "Threads already running are not affected. Each runs as its own detached process, and Frizz reattaches to it on restart.";
|
|
22701
|
-
function recoveryPage(url, variant = "starting") {
|
|
22702
|
-
const copy = RECOVERY_COPY[variant];
|
|
22703
|
-
const target2 = escapeHtml(retryTarget(url));
|
|
22704
|
-
return documentShell({
|
|
22705
|
-
title: copy.title,
|
|
22706
|
-
style: RECOVERY_STYLE,
|
|
22707
|
-
appearance: true,
|
|
22708
|
-
bodyAttributes: `data-target="${target2}"`,
|
|
22709
|
-
body: `<h1><span class="dot" id="dot" aria-hidden="true"></span><span id="heading">${copy.heading}</span></h1><p class="detail" id="detail">${copy.detail}</p><pre class="log" id="log" hidden></pre><p class="note">${THREADS_NOTE}</p><div class="actions"><a class="btn" href="${target2}">Try again</a><button class="btn" type="button" id="restart">Restart Frizz</button></div><p class="probe" id="probe" aria-live="polite"></p>`,
|
|
22710
|
-
script: RECOVERY_SCRIPT
|
|
22711
|
-
});
|
|
22712
|
-
}
|
|
22713
|
-
function unauthorizedPage(reason) {
|
|
22714
|
-
const detail = reason === "already-used" ? "That link has already been used. Generate a fresh one." : reason === "expired" ? "That link expired. Generate a fresh one." : "This address needs a current access link.";
|
|
22715
|
-
return documentShell({
|
|
22716
|
-
title: "Unauthorized",
|
|
22717
|
-
body: `<h1>Unauthorized</h1><p class="detail">${detail}</p>`
|
|
22718
|
-
});
|
|
22719
|
-
}
|
|
22720
|
-
function unlistedHostPage(name) {
|
|
22721
|
-
const shown = escapeHtml(name);
|
|
22722
|
-
return documentShell({
|
|
22723
|
-
title: "Not served by this name",
|
|
22724
|
-
appearance: true,
|
|
22725
|
-
body: `<h1>Not served by this name</h1><p class="detail">Frizz does not answer to <code>${shown}</code>. A name a browser has to resolve is how DNS rebinding makes another site's page count as this board, so an exposed board accepts only its own hostname and the names its operator lists.</p><p class="note">Open the board by its IP address, or relaunch with <code>--allowed-host ${shown}</code> (or <code>FRIZZ_ALLOWED_HOSTS=${shown}</code>) to accept this one.</p>`
|
|
22726
|
-
});
|
|
22727
|
-
}
|
|
22728
|
-
|
|
22729
|
-
// packages/server/src/restart-supervisor.ts
|
|
22730
|
-
init_src();
|
|
22731
|
-
var SUPERVISOR_CONTROL_PREFIX = "/_frizz/control";
|
|
22732
|
-
var SUPERVISOR_RESTART_PATH = `${SUPERVISOR_CONTROL_PREFIX}/restart`;
|
|
22733
|
-
var SUPERVISOR_UPDATE_RESTART_PATH = `${SUPERVISOR_CONTROL_PREFIX}/update-restart`;
|
|
22734
|
-
var SUPERVISOR_STATUS_PATH = `${SUPERVISOR_CONTROL_PREFIX}/status`;
|
|
22735
|
-
var SUPERVISOR_ACCESS_CODE_PATH = `${SUPERVISOR_CONTROL_PREFIX}/access-code`;
|
|
22736
|
-
var SUPERVISOR_SESSIONS_PATH = `${SUPERVISOR_CONTROL_PREFIX}/sessions`;
|
|
22737
|
-
var SUPERVISOR_SIGN_OUT_PATH = `${SUPERVISOR_CONTROL_PREFIX}/sign-out`;
|
|
22738
|
-
var SUPERVISOR_CONTROL_PROTOCOL = 1;
|
|
22739
|
-
function responseJson(res, status, value) {
|
|
22740
|
-
res.writeHead(status, { "content-type": "application/json; charset=utf-8", "cache-control": "no-store" });
|
|
22741
|
-
res.end(JSON.stringify(value));
|
|
22742
|
-
}
|
|
22743
|
-
function proxyHeaders(req, childPort, vouchSameOrigin = false) {
|
|
22744
|
-
const headers = { ...req.headers };
|
|
22745
|
-
headers.host = `127.0.0.1:${childPort}`;
|
|
22746
|
-
if (typeof headers.origin === "string") headers.origin = `http://127.0.0.1:${childPort}`;
|
|
22747
|
-
for (const name of FORWARDED_HEADERS) delete headers[name];
|
|
22748
|
-
if (vouchSameOrigin) headers["sec-fetch-site"] = "same-origin";
|
|
22749
|
-
delete headers.connection;
|
|
22750
|
-
return headers;
|
|
22751
|
-
}
|
|
22752
|
-
var ACCESS_CODE_PARAM = "frizz_code";
|
|
22753
|
-
var SESSION_COOKIE = "frizz_session";
|
|
22754
|
-
var LOOPBACK_HOSTNAMES = /* @__PURE__ */ new Set(["localhost", "127.0.0.1", "::1"]);
|
|
22755
|
-
function readCookie(header, name) {
|
|
22756
|
-
if (!header) return void 0;
|
|
22757
|
-
for (const pair of header.split(";")) {
|
|
22758
|
-
const eq = pair.indexOf("=");
|
|
22759
|
-
if (eq === -1) continue;
|
|
22760
|
-
if (pair.slice(0, eq).trim() === name) return pair.slice(eq + 1).trim();
|
|
22761
|
-
}
|
|
22762
|
-
return void 0;
|
|
22763
|
-
}
|
|
22764
|
-
function isControlRequest(req) {
|
|
22765
|
-
const url = new URL(req.url ?? "/", "http://frizz.invalid");
|
|
22766
|
-
return url.pathname === SUPERVISOR_RESTART_PATH || url.pathname === SUPERVISOR_UPDATE_RESTART_PATH || url.pathname === SUPERVISOR_STATUS_PATH || url.pathname === SUPERVISOR_ACCESS_CODE_PATH || url.pathname === SUPERVISOR_SESSIONS_PATH || url.pathname === SUPERVISOR_SIGN_OUT_PATH;
|
|
22767
|
-
}
|
|
22768
|
-
function isLocalImageRequest(req) {
|
|
22769
|
-
if (req.method !== "GET" && req.method !== "HEAD") return false;
|
|
22770
|
-
const { pathname } = new URL(req.url ?? "/", "http://frizz.invalid");
|
|
22771
|
-
return LOCAL_IMAGE_PATH.test(pathname);
|
|
22772
|
-
}
|
|
22773
|
-
var LOCAL_IMAGE_PATH = new RegExp(`^${FRIZZ_ROUTE_PREFIX}(?:/[^/]+)?/local-image$`);
|
|
22774
|
-
async function readJsonBody(req, limit = 64 * 1024) {
|
|
22775
|
-
const chunks = [];
|
|
22776
|
-
let size = 0;
|
|
22777
|
-
for await (const chunk of req) {
|
|
22778
|
-
size += chunk.byteLength;
|
|
22779
|
-
if (size > limit) return null;
|
|
22780
|
-
chunks.push(chunk);
|
|
22781
|
-
}
|
|
22782
|
-
if (chunks.length === 0) return {};
|
|
22783
|
-
try {
|
|
22784
|
-
const parsed = JSON.parse(Buffer.concat(chunks).toString("utf8"));
|
|
22785
|
-
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
|
|
22786
|
-
} catch {
|
|
22787
|
-
return null;
|
|
22788
|
-
}
|
|
22789
|
-
}
|
|
22790
|
-
var RestartSupervisorProxy = class {
|
|
22791
|
-
server = null;
|
|
22792
|
-
restartInFlight = null;
|
|
22793
|
-
/** Which action `restartInFlight` is. An update's prepare phase leaves the child serving; a restart does not. */
|
|
22794
|
-
transition = null;
|
|
22795
|
-
state = "ready";
|
|
22796
|
-
message;
|
|
22797
|
-
options;
|
|
22798
|
-
host;
|
|
22799
|
-
/**
|
|
22800
|
-
* The authorities a browser may name for the PUBLIC port. This proxy is the whole boundary: it
|
|
22801
|
-
* rewrites Host/Origin to the child's private loopback authority, so the child's own strict gate
|
|
22802
|
-
* can never see (or reject) the real browser authority. Everything this class forwards has to be
|
|
22803
|
-
* judged here first.
|
|
22804
|
-
*/
|
|
22805
|
-
policy;
|
|
22806
|
-
/** The origin a tunnel or proxy serves this board at; undefined while loopback-only. Live: see setPublicOrigin. */
|
|
22807
|
-
publicOrigin;
|
|
22808
|
-
/**
|
|
22809
|
-
* Codes and sessions for the public origin, or null when no public origin is declared (loopback-only
|
|
22810
|
-
* boards are never gated, so there is nothing to store).
|
|
22811
|
-
*/
|
|
22812
|
-
access;
|
|
22813
|
-
/** Both halves of every live upgraded pair, so close() can actually finish. See close(). */
|
|
22814
|
-
upgradedSockets = /* @__PURE__ */ new Set();
|
|
22815
|
-
constructor(options2) {
|
|
22816
|
-
this.options = options2;
|
|
22817
|
-
this.host = options2.host ?? LOOPBACK_BIND_HOST;
|
|
22818
|
-
this.publicOrigin = options2.publicOrigin;
|
|
22819
|
-
this.access = new AccessStore({
|
|
22820
|
-
onConsumed: () => options2.onCodeConsumed?.(),
|
|
22821
|
-
...options2.sessionKey ? { signingKey: options2.sessionKey } : {},
|
|
22822
|
-
...options2.sessionDirectory ? { sessions: options2.sessionDirectory } : {}
|
|
22823
|
-
});
|
|
22824
|
-
this.policy = this.buildPolicy();
|
|
22825
|
-
}
|
|
22826
|
-
/**
|
|
22827
|
-
* Change the public origin on a running board — what the R pane does when the operator picks or
|
|
22828
|
-
* clears a remote setup. Requests already in flight finish under the policy they started with; the
|
|
22829
|
-
* next one is judged by the new origin. Sessions survive the change.
|
|
22830
|
-
*/
|
|
22831
|
-
setPublicOrigin(origin) {
|
|
22832
|
-
this.publicOrigin = origin;
|
|
22833
|
-
this.policy = this.buildPolicy();
|
|
22834
|
-
}
|
|
22835
|
-
buildPolicy() {
|
|
22836
|
-
return {
|
|
22837
|
-
exposed: bindHostIsExposed(this.host),
|
|
22838
|
-
allowedHosts: this.options.allowedHosts ?? [],
|
|
22839
|
-
...this.publicOrigin ? { publicOrigin: this.publicOrigin } : {}
|
|
22840
|
-
};
|
|
22841
|
-
}
|
|
22842
|
-
/**
|
|
22843
|
-
* Mint a single-use code for this board, or null when no public origin is declared.
|
|
22844
|
-
*
|
|
22845
|
-
* Issuing does NOT invalidate codes already outstanding — pressing the key twice, or two people
|
|
22846
|
-
* asking at once, must not silently break the first QR somebody is still walking towards.
|
|
22847
|
-
*/
|
|
22848
|
-
issueAccessCode() {
|
|
22849
|
-
return this.publicOrigin ? this.access.issue() : null;
|
|
22850
|
-
}
|
|
22851
|
-
/** The full URL to show as a link or a QR. */
|
|
22852
|
-
accessUrl(code) {
|
|
22853
|
-
const origin = this.publicOrigin;
|
|
22854
|
-
return origin ? `${origin}/?${ACCESS_CODE_PARAM}=${encodeURIComponent(code)}` : null;
|
|
22855
|
-
}
|
|
22856
|
-
async listen() {
|
|
22857
|
-
if (this.server) return;
|
|
22858
|
-
const server = createServer((req, res) => this.handle(req, res));
|
|
22859
|
-
server.keepAliveTimeout = 5e3;
|
|
22860
|
-
server.headersTimeout = 1e4;
|
|
22861
|
-
server.on("upgrade", (req, socket, head) => this.handleUpgrade(req, socket, head));
|
|
22862
|
-
await new Promise((resolveListen, rejectListen) => {
|
|
22863
|
-
server.once("error", rejectListen);
|
|
22864
|
-
server.listen(this.options.port, this.host, () => {
|
|
22865
|
-
server.off("error", rejectListen);
|
|
22866
|
-
resolveListen();
|
|
22867
|
-
});
|
|
22868
|
-
});
|
|
22869
|
-
this.server = server;
|
|
22870
|
-
}
|
|
22871
|
-
/**
|
|
22872
|
-
* Is this request allowed to reach the child at all?
|
|
22873
|
-
*
|
|
22874
|
-
* Deliberately the WEAKER of the two gates: a missing Origin passes here and the child then applies
|
|
22875
|
-
* its own per-route rule for it (only /health, /control/stop and `Sec-Fetch-Site: same-origin` may
|
|
22876
|
-
* omit one). What this stops is the thing the child cannot: an Origin naming somebody ELSE being
|
|
22877
|
-
* laundered into a valid same-origin one by proxyHeaders below.
|
|
22878
|
-
*/
|
|
22879
|
-
authorityAccepted(req, allowMissingOrigin = true) {
|
|
22880
|
-
return isTrustedLocalHttpRequest(req.headers, this.options.port, allowMissingOrigin, this.policy);
|
|
22881
|
-
}
|
|
22882
|
-
/**
|
|
22883
|
-
* Is this an Origin-less request that only LOOKS suspicious because the browser could not send
|
|
22884
|
-
* Fetch Metadata to a non-loopback authority? See authoritySendsFetchMetadata.
|
|
22885
|
-
*
|
|
22886
|
-
* Frizz's missing-Origin rules ask for `Sec-Fetch-Site: same-origin`, which Chrome never sends over
|
|
22887
|
-
* plain HTTP to a LAN address — so without this, `--host` serves the shell and then 403s every RPC
|
|
22888
|
-
* the app makes. Narrow on purpose: the Host must already be an authority this proxy accepts, there
|
|
22889
|
-
* must be no Origin at all (a present one still has to match, and is rejected above if it does not),
|
|
22890
|
-
* and loopback traffic is never touched, so the default posture keeps the real Sec-Fetch signal.
|
|
22891
|
-
*
|
|
22892
|
-
* The condition is `authoritySendsFetchMetadata`, not "is the bind address exposed", because those
|
|
22893
|
-
* two came apart with `--public-origin`: a tunnelled board stays on loopback yet is reached at a
|
|
22894
|
-
* name. An https proxy origin is potentially trustworthy, so the real signal survives and this
|
|
22895
|
-
* never fires for it; a plain-http one is in the same boat as a LAN address and needs the vouch.
|
|
22896
|
-
*
|
|
22897
|
-
* What this gives up, stated plainly: on an exposed board a cross-site GET carries no Origin and no
|
|
22898
|
-
* Fetch Metadata, so it is indistinguishable from the app's own read and is allowed. The response is
|
|
22899
|
-
* still opaque to the caller (no CORS), Frizz's GET procedures are reads, and every mutation is a POST
|
|
22900
|
-
* — which a browser always stamps with an Origin, and which is therefore still refused.
|
|
22901
|
-
*/
|
|
22902
|
-
vouchesSameOrigin(req) {
|
|
22903
|
-
if (req.headers.origin !== void 0 || req.headers["sec-fetch-site"] !== void 0) return false;
|
|
22904
|
-
const host = parseLocalHost(req.headers.host, this.options.port, this.policy);
|
|
22905
|
-
return !!host && !authoritySendsFetchMetadata(host);
|
|
22906
|
-
}
|
|
22907
|
-
async close() {
|
|
22908
|
-
const server = this.server;
|
|
22909
|
-
this.server = null;
|
|
22910
|
-
if (!server) return;
|
|
22911
|
-
await new Promise((resolveClose, rejectClose) => {
|
|
22912
|
-
server.close((error) => error ? rejectClose(error) : resolveClose());
|
|
22913
|
-
server.closeAllConnections();
|
|
22914
|
-
for (const socket of this.upgradedSockets) socket.destroy();
|
|
22915
|
-
this.upgradedSockets.clear();
|
|
22916
|
-
});
|
|
22917
|
-
}
|
|
22918
|
-
status() {
|
|
22919
|
-
const delegated = this.options.status?.();
|
|
22920
|
-
const updateVersion = this.options.updateVersion?.();
|
|
22921
|
-
const version = typeof this.options.version === "function" ? this.options.version() : this.options.version;
|
|
22922
|
-
const owner = { state: this.state, ...this.message ? { message: this.message } : {} };
|
|
22923
|
-
const reported = this.transition === "update" && delegated ? { ...delegated, ...delegated.state === "ready" ? { preparing: true } : {} } : this.restartInFlight !== null || this.state === "failed" ? owner : delegated ?? owner;
|
|
22924
|
-
return {
|
|
22925
|
-
...delegated ?? {},
|
|
22926
|
-
...reported,
|
|
22927
|
-
// Never infer this from the generic protocol: legacy/static supervisors can recover a child
|
|
22928
|
-
// but cannot build and promote the canonical Frizz artifact.
|
|
22929
|
-
updateRestart: typeof this.options.updateRestart === "function",
|
|
22930
|
-
// Sent ONLY when the launcher can actually answer it, so an older client — and frizz-dev, which
|
|
22931
|
-
// has no notion of "already current" — keeps today's behaviour on its absence.
|
|
22932
|
-
...this.options.updateAvailable ? { updateAvailable: this.options.updateAvailable() === true } : {},
|
|
22933
|
-
// The active SERVER version changes on a child-only update. Its absence keeps frizz-dev and
|
|
22934
|
-
// old monolithic supervisors' status payload and versionless UI exactly as they were.
|
|
22935
|
-
...version ? { version } : {},
|
|
22936
|
-
...this.options.launcherVersion ? { launcherVersion: this.options.launcherVersion } : {},
|
|
22937
|
-
...updateVersion ? { updateVersion } : {},
|
|
22938
|
-
// Sent only when TRUE, so a published Frizz's payload is byte-identical to what it sends today
|
|
22939
|
-
// and an older client is unaffected. Absent therefore means "not a development build".
|
|
22940
|
-
...this.options.dev ? { dev: true } : {}
|
|
22941
|
-
};
|
|
22942
|
-
}
|
|
22943
|
-
async runAction(action, transition) {
|
|
22944
|
-
if (this.restartInFlight) return this.restartInFlight;
|
|
22945
|
-
this.transition = transition;
|
|
22946
|
-
this.state = "restarting";
|
|
22947
|
-
this.message = void 0;
|
|
22948
|
-
const work = (async () => {
|
|
22949
|
-
await new Promise((resume) => setImmediate(resume));
|
|
22950
|
-
return action();
|
|
22951
|
-
})().then(
|
|
22952
|
-
(result) => {
|
|
22953
|
-
this.state = result.state;
|
|
22954
|
-
this.message = result.message;
|
|
22955
|
-
return result;
|
|
22956
|
-
},
|
|
22957
|
-
(error) => {
|
|
22958
|
-
const result = { state: "failed", message: error instanceof Error ? error.message : String(error) };
|
|
22959
|
-
this.state = result.state;
|
|
22960
|
-
this.message = result.message;
|
|
22961
|
-
return result;
|
|
22962
|
-
}
|
|
22963
|
-
).finally(() => {
|
|
22964
|
-
this.restartInFlight = null;
|
|
22965
|
-
this.transition = null;
|
|
22966
|
-
});
|
|
22967
|
-
this.restartInFlight = work;
|
|
22968
|
-
return work;
|
|
22969
|
-
}
|
|
22970
|
-
async handleControl(req, res) {
|
|
22971
|
-
const pathname = new URL(req.url ?? "/", "http://frizz.invalid").pathname;
|
|
22972
|
-
if (pathname !== SUPERVISOR_SIGN_OUT_PATH && this.arrivedPublicly(req) && !this.sessionAccepted(req)) {
|
|
22973
|
-
res.writeHead(401, { "cache-control": "no-store" });
|
|
22974
|
-
res.end();
|
|
22975
|
-
return;
|
|
22976
|
-
}
|
|
22977
|
-
const sameOrigin = req.headers["sec-fetch-site"] === "same-origin" || this.vouchesSameOrigin(req);
|
|
22978
|
-
const allowMissingOrigin = pathname === SUPERVISOR_STATUS_PATH && req.method === "GET" && sameOrigin;
|
|
22979
|
-
if (!this.authorityAccepted(req, allowMissingOrigin)) {
|
|
22980
|
-
res.writeHead(403);
|
|
22981
|
-
res.end("Forbidden");
|
|
22982
|
-
return;
|
|
22983
|
-
}
|
|
22984
|
-
if (pathname === SUPERVISOR_ACCESS_CODE_PATH) {
|
|
22985
|
-
if (this.arrivedPublicly(req)) {
|
|
22986
|
-
res.writeHead(403);
|
|
22987
|
-
res.end("Forbidden");
|
|
22988
|
-
return;
|
|
22989
|
-
}
|
|
22990
|
-
if (req.method !== "POST") {
|
|
22991
|
-
res.writeHead(405, { allow: "POST" });
|
|
22992
|
-
res.end();
|
|
22993
|
-
return;
|
|
22994
|
-
}
|
|
22995
|
-
const link = this.issueAccessCode();
|
|
22996
|
-
const url = link && this.accessUrl(link.code);
|
|
22997
|
-
if (!link || !url) {
|
|
22998
|
-
responseJson(res, 409, { error: "this board has no public origin, so there is nothing to link to" });
|
|
22999
|
-
return;
|
|
23000
|
-
}
|
|
23001
|
-
responseJson(res, 200, { url, expiresAt: link.expiresAt });
|
|
23002
|
-
return;
|
|
23003
|
-
}
|
|
23004
|
-
if (pathname === SUPERVISOR_SESSIONS_PATH) {
|
|
23005
|
-
if (this.arrivedPublicly(req)) {
|
|
23006
|
-
res.writeHead(403);
|
|
23007
|
-
res.end("Forbidden");
|
|
23008
|
-
return;
|
|
23009
|
-
}
|
|
23010
|
-
if (!this.publicOrigin) {
|
|
23011
|
-
responseJson(res, 409, { error: "this board has no public origin, so nothing is signed in" });
|
|
23012
|
-
return;
|
|
23013
|
-
}
|
|
23014
|
-
if (req.method === "GET") {
|
|
23015
|
-
responseJson(res, 200, { sessions: this.access.sessions.list() });
|
|
23016
|
-
return;
|
|
23017
|
-
}
|
|
23018
|
-
if (req.method !== "POST") {
|
|
23019
|
-
res.writeHead(405, { allow: "GET, POST" });
|
|
23020
|
-
res.end();
|
|
23021
|
-
return;
|
|
23022
|
-
}
|
|
23023
|
-
const body = await readJsonBody(req);
|
|
23024
|
-
const id = typeof body?.id === "string" ? body.id : void 0;
|
|
23025
|
-
if (body?.all === true) {
|
|
23026
|
-
responseJson(res, 200, { signedOut: this.access.sessions.revokeAll() });
|
|
23027
|
-
return;
|
|
23028
|
-
}
|
|
23029
|
-
if (!id) {
|
|
23030
|
-
responseJson(res, 400, { error: "name a device id, or pass all: true" });
|
|
23031
|
-
return;
|
|
23032
|
-
}
|
|
23033
|
-
if (!this.access.sessions.revoke(id)) {
|
|
23034
|
-
responseJson(res, 404, { error: `no signed-in device has the id ${id}` });
|
|
23035
|
-
return;
|
|
23036
|
-
}
|
|
23037
|
-
responseJson(res, 200, { signedOut: 1 });
|
|
23038
|
-
return;
|
|
23039
|
-
}
|
|
23040
|
-
if (pathname === SUPERVISOR_SIGN_OUT_PATH) {
|
|
23041
|
-
if (req.method !== "POST") {
|
|
23042
|
-
res.writeHead(405, { allow: "POST" });
|
|
23043
|
-
res.end();
|
|
23044
|
-
return;
|
|
23045
|
-
}
|
|
23046
|
-
this.signOutThisDevice(req, res);
|
|
23047
|
-
return;
|
|
23048
|
-
}
|
|
23049
|
-
if (pathname === SUPERVISOR_STATUS_PATH && req.method === "GET") {
|
|
23050
|
-
responseJson(res, 200, {
|
|
23051
|
-
protocol: SUPERVISOR_CONTROL_PROTOCOL,
|
|
23052
|
-
...this.status(),
|
|
23053
|
-
// Sent ONLY when true, so a loopback tab's payload is byte-identical to what it was. The web
|
|
23054
|
-
// app cannot tell on its own: the session cookie is HttpOnly, and the page is the same bundle
|
|
23055
|
-
// whichever way it was reached. It is a fact about THIS request, never about the board.
|
|
23056
|
-
...this.carriesRemoteSession(req) ? { remoteSession: true } : {}
|
|
23057
|
-
});
|
|
23058
|
-
return;
|
|
23059
|
-
}
|
|
23060
|
-
if (pathname !== SUPERVISOR_RESTART_PATH && pathname !== SUPERVISOR_UPDATE_RESTART_PATH || req.method !== "POST") {
|
|
23061
|
-
res.writeHead(405, { allow: pathname === SUPERVISOR_STATUS_PATH ? "GET" : "POST" });
|
|
23062
|
-
res.end();
|
|
23063
|
-
return;
|
|
23064
|
-
}
|
|
23065
|
-
if (pathname === SUPERVISOR_UPDATE_RESTART_PATH && !this.options.updateRestart) {
|
|
23066
|
-
responseJson(res, 409, { protocol: SUPERVISOR_CONTROL_PROTOCOL, state: "failed", message: "Update & Restart is available only for a stable immutable Frizz artifact" });
|
|
23067
|
-
return;
|
|
23068
|
-
}
|
|
23069
|
-
if (pathname === SUPERVISOR_UPDATE_RESTART_PATH) {
|
|
23070
|
-
void this.runAction(this.options.updateRestart, "update");
|
|
23071
|
-
responseJson(res, 202, { protocol: SUPERVISOR_CONTROL_PROTOCOL, state: "restarting" });
|
|
23072
|
-
return;
|
|
23073
|
-
}
|
|
23074
|
-
const result = await this.runAction(this.options.restart, "restart");
|
|
23075
|
-
responseJson(res, result.state === "ready" ? 202 : 503, { protocol: SUPERVISOR_CONTROL_PROTOCOL, ...result });
|
|
23076
|
-
}
|
|
23077
|
-
handleLocalImage(req, res) {
|
|
23078
|
-
const allowMissingOrigin = req.headers.origin === void 0 && (req.headers["sec-fetch-site"] === "same-origin" || this.vouchesSameOrigin(req));
|
|
23079
|
-
if (!this.authorityAccepted(req, allowMissingOrigin)) {
|
|
23080
|
-
res.writeHead(403, { "content-type": "text/plain; charset=UTF-8" });
|
|
23081
|
-
res.end("Forbidden");
|
|
23082
|
-
return;
|
|
23317
|
+
if (table.size() < 4095) {
|
|
23318
|
+
if (table.size() == 1 << bitLength) {
|
|
23319
|
+
bitLength += 1;
|
|
23320
|
+
}
|
|
23321
|
+
table.add(s + c);
|
|
23322
|
+
}
|
|
23323
|
+
s = c;
|
|
23324
|
+
}
|
|
23083
23325
|
}
|
|
23084
|
-
|
|
23085
|
-
|
|
23086
|
-
|
|
23087
|
-
|
|
23088
|
-
|
|
23089
|
-
|
|
23090
|
-
|
|
23326
|
+
bitOut.write(table.indexOf(s), bitLength);
|
|
23327
|
+
bitOut.write(endCode, bitLength);
|
|
23328
|
+
bitOut.flush();
|
|
23329
|
+
return byteOut.toByteArray();
|
|
23330
|
+
};
|
|
23331
|
+
const lzwTable = function() {
|
|
23332
|
+
const _map = {};
|
|
23333
|
+
let _size = 0;
|
|
23334
|
+
const _this2 = {};
|
|
23335
|
+
_this2.add = function(key) {
|
|
23336
|
+
if (_this2.contains(key)) {
|
|
23337
|
+
throw "dup key:" + key;
|
|
23338
|
+
}
|
|
23339
|
+
_map[key] = _size;
|
|
23340
|
+
_size += 1;
|
|
23091
23341
|
};
|
|
23092
|
-
|
|
23093
|
-
|
|
23094
|
-
res.end(String(result.status));
|
|
23095
|
-
return;
|
|
23096
|
-
}
|
|
23097
|
-
res.writeHead(200, {
|
|
23098
|
-
...sharedHeaders,
|
|
23099
|
-
"content-type": result.contentType,
|
|
23100
|
-
"content-length": result.body.length,
|
|
23101
|
-
"cache-control": "private, max-age=60"
|
|
23102
|
-
});
|
|
23103
|
-
res.end(req.method === "HEAD" ? void 0 : result.body);
|
|
23104
|
-
}
|
|
23105
|
-
/**
|
|
23106
|
-
* Is this request arriving as the declared public origin rather than over loopback?
|
|
23107
|
-
*
|
|
23108
|
-
* The token gate keys on this and not on "is the port exposed", because the two are independent:
|
|
23109
|
-
* a tunnelled board stays bound to 127.0.0.1 while being reachable from the internet by name.
|
|
23110
|
-
*/
|
|
23111
|
-
arrivedPublicly(req) {
|
|
23112
|
-
const declared = this.publicOrigin;
|
|
23113
|
-
if (!declared) return false;
|
|
23114
|
-
const host = parseLocalHost(req.headers.host, this.options.port, this.policy);
|
|
23115
|
-
return !!host && !LOOPBACK_HOSTNAMES.has(host.hostname);
|
|
23116
|
-
}
|
|
23117
|
-
/**
|
|
23118
|
-
* Bearer check for the public origin. Two ways in: the cookie the exchange below sets, or the
|
|
23119
|
-
* one-time `?frizz_token=` in the URL the operator was handed at launch.
|
|
23120
|
-
*
|
|
23121
|
-
* Compared with `timingSafeEqual` on equal-length buffers — a plain `===` on a secret leaks its
|
|
23122
|
-
* prefix to a patient attacker, and this secret is the only thing between the internet and a shell.
|
|
23123
|
-
*/
|
|
23124
|
-
/**
|
|
23125
|
-
* Is this request carrying a session this process minted?
|
|
23126
|
-
*
|
|
23127
|
-
* Sessions only. A raw `?frizz_code=` is NOT accepted here — it is redeemed once by exchangeCode()
|
|
23128
|
-
* below and traded for a session, so a code that has already been spent cannot keep working just by
|
|
23129
|
-
* staying in somebody's URL bar.
|
|
23130
|
-
*/
|
|
23131
|
-
sessionAccepted(req) {
|
|
23132
|
-
if (!this.publicOrigin) return true;
|
|
23133
|
-
return this.access.verifySession(readCookie(req.headers.cookie, SESSION_COOKIE));
|
|
23134
|
-
}
|
|
23135
|
-
/** Did this request arrive through the public origin carrying a live session? Loopback never does. */
|
|
23136
|
-
carriesRemoteSession(req) {
|
|
23137
|
-
return this.arrivedPublicly(req) && this.access.verifySession(readCookie(req.headers.cookie, SESSION_COOKIE));
|
|
23138
|
-
}
|
|
23139
|
-
/**
|
|
23140
|
-
* End the session this request carries, and nothing else — see SUPERVISOR_SIGN_OUT_PATH in
|
|
23141
|
-
* handleControl for why that makes it safe to reach from the tunnel.
|
|
23142
|
-
*
|
|
23143
|
-
* A loopback request is a no-op even if it somehow carries a cookie: the operator's own tab has no
|
|
23144
|
-
* remote session to end, and loopback is the place `--sign-out` already covers.
|
|
23145
|
-
*/
|
|
23146
|
-
signOutThisDevice(req, res) {
|
|
23147
|
-
const reply = (status, body, clearCookie) => {
|
|
23148
|
-
res.writeHead(status, {
|
|
23149
|
-
"content-type": "application/json; charset=utf-8",
|
|
23150
|
-
"cache-control": "no-store",
|
|
23151
|
-
// Same attributes the exchange set, so the browser matches and drops that exact cookie.
|
|
23152
|
-
...clearCookie ? { "set-cookie": `${SESSION_COOKIE}=; Path=/; HttpOnly; Secure; SameSite=Lax; Max-Age=0` } : {}
|
|
23153
|
-
});
|
|
23154
|
-
res.end(JSON.stringify(body));
|
|
23342
|
+
_this2.size = function() {
|
|
23343
|
+
return _size;
|
|
23155
23344
|
};
|
|
23156
|
-
|
|
23157
|
-
|
|
23158
|
-
|
|
23159
|
-
|
|
23160
|
-
|
|
23161
|
-
|
|
23162
|
-
|
|
23163
|
-
|
|
23164
|
-
|
|
23165
|
-
|
|
23166
|
-
|
|
23167
|
-
|
|
23345
|
+
_this2.indexOf = function(key) {
|
|
23346
|
+
return _map[key];
|
|
23347
|
+
};
|
|
23348
|
+
_this2.contains = function(key) {
|
|
23349
|
+
return typeof _map[key] != "undefined";
|
|
23350
|
+
};
|
|
23351
|
+
return _this2;
|
|
23352
|
+
};
|
|
23353
|
+
return _this;
|
|
23354
|
+
};
|
|
23355
|
+
var createDataURL = function(width, height, getPixel) {
|
|
23356
|
+
const gif = gifImage(width, height);
|
|
23357
|
+
for (let y = 0; y < height; y += 1) {
|
|
23358
|
+
for (let x = 0; x < width; x += 1) {
|
|
23359
|
+
gif.setPixel(x, y, getPixel(x, y));
|
|
23168
23360
|
}
|
|
23169
|
-
reply(401, { protocol: SUPERVISOR_CONTROL_PROTOCOL, result: "no-remote-session" }, true);
|
|
23170
23361
|
}
|
|
23171
|
-
|
|
23172
|
-
|
|
23173
|
-
|
|
23174
|
-
|
|
23175
|
-
|
|
23176
|
-
|
|
23177
|
-
const code = url.searchParams.get(ACCESS_CODE_PARAM);
|
|
23178
|
-
if (code === null) return false;
|
|
23179
|
-
const redeemed = this.access.redeem(code, describeDevice(req.headers["user-agent"]));
|
|
23180
|
-
if (!redeemed?.ok) {
|
|
23181
|
-
res.writeHead(401, { "content-type": "text/html; charset=utf-8", "cache-control": "no-store" });
|
|
23182
|
-
res.end(unauthorizedPage(redeemed?.reason));
|
|
23183
|
-
return true;
|
|
23184
|
-
}
|
|
23185
|
-
const cookie = `${SESSION_COOKIE}=${redeemed.session}`;
|
|
23186
|
-
url.searchParams.delete(ACCESS_CODE_PARAM);
|
|
23187
|
-
const target2 = `${url.pathname}${url.search}${url.hash}` || "/";
|
|
23188
|
-
res.writeHead(302, {
|
|
23189
|
-
location: target2,
|
|
23190
|
-
"set-cookie": `${cookie}; Path=/; HttpOnly; Secure; SameSite=Lax; Max-Age=31536000`,
|
|
23191
|
-
"cache-control": "no-store"
|
|
23192
|
-
});
|
|
23193
|
-
res.end();
|
|
23194
|
-
return true;
|
|
23362
|
+
const b = byteArrayOutputStream();
|
|
23363
|
+
gif.write(b);
|
|
23364
|
+
const base64 = base64EncodeOutputStream();
|
|
23365
|
+
const bytes = b.toByteArray();
|
|
23366
|
+
for (let i = 0; i < bytes.length; i += 1) {
|
|
23367
|
+
base64.writeByte(bytes[i]);
|
|
23195
23368
|
}
|
|
23196
|
-
|
|
23197
|
-
|
|
23198
|
-
|
|
23199
|
-
|
|
23200
|
-
|
|
23201
|
-
|
|
23202
|
-
|
|
23203
|
-
|
|
23204
|
-
|
|
23205
|
-
|
|
23369
|
+
base64.flush();
|
|
23370
|
+
return "data:image/gif;base64," + base64;
|
|
23371
|
+
};
|
|
23372
|
+
var qrcode_default = qrcode;
|
|
23373
|
+
var stringToBytes = qrcode.stringToBytes;
|
|
23374
|
+
|
|
23375
|
+
// packages/server/src/qr.ts
|
|
23376
|
+
var DARK = 232;
|
|
23377
|
+
var LIGHT = 231;
|
|
23378
|
+
var fg = (dark) => `\x1B[38;5;${dark ? DARK : LIGHT}m`;
|
|
23379
|
+
var bg = (dark) => `\x1B[48;5;${dark ? DARK : LIGHT}m`;
|
|
23380
|
+
var RESET = "\x1B[0m";
|
|
23381
|
+
var UPPER_HALF = "\u2580";
|
|
23382
|
+
var LOWER_HALF = "\u2584";
|
|
23383
|
+
var QUIET_ZONE = 4;
|
|
23384
|
+
var BLOCK_DRAWING_PROGRAMS = /* @__PURE__ */ new Set(["iTerm.app", "WezTerm", "ghostty", "WarpTerminal", "vscode"]);
|
|
23385
|
+
var BLOCK_DRAWING_TERMS = /* @__PURE__ */ new Set(["xterm-kitty", "xterm-ghostty", "alacritty", "wezterm"]);
|
|
23386
|
+
function drawsBlockElements(env = process.env) {
|
|
23387
|
+
const term = env.TERM ?? "";
|
|
23388
|
+
if (env.TMUX || env.STY || env.TERM_PROGRAM === "tmux" || term.startsWith("screen") || term.startsWith("tmux")) return false;
|
|
23389
|
+
if (BLOCK_DRAWING_PROGRAMS.has(env.TERM_PROGRAM ?? "")) return true;
|
|
23390
|
+
if (BLOCK_DRAWING_TERMS.has(term)) return true;
|
|
23391
|
+
if (env.LC_TERMINAL === "iTerm2") return true;
|
|
23392
|
+
return Boolean(
|
|
23393
|
+
env.KITTY_WINDOW_ID || env.ALACRITTY_WINDOW_ID || env.WEZTERM_PANE || env.GHOSTTY_RESOURCES_DIR || env.WT_SESSION || env.VTE_VERSION
|
|
23394
|
+
);
|
|
23395
|
+
}
|
|
23396
|
+
function encode(value, errorCorrection) {
|
|
23397
|
+
const code = qrcode_default(0, errorCorrection);
|
|
23398
|
+
code.addData(value);
|
|
23399
|
+
code.make();
|
|
23400
|
+
const modules = code.getModuleCount();
|
|
23401
|
+
const size = modules + QUIET_ZONE * 2;
|
|
23402
|
+
return {
|
|
23403
|
+
size,
|
|
23404
|
+
at: (x, y) => {
|
|
23405
|
+
const mx = x - QUIET_ZONE;
|
|
23406
|
+
const my = y - QUIET_ZONE;
|
|
23407
|
+
if (mx < 0 || my < 0 || mx >= modules || my >= modules) return false;
|
|
23408
|
+
return code.isDark(my, mx);
|
|
23206
23409
|
}
|
|
23207
|
-
|
|
23208
|
-
|
|
23209
|
-
|
|
23210
|
-
|
|
23211
|
-
|
|
23212
|
-
|
|
23410
|
+
};
|
|
23411
|
+
}
|
|
23412
|
+
function sizeOf(size, style) {
|
|
23413
|
+
return style === "paint" ? { columns: size * 2, rows: size } : { columns: size, rows: Math.ceil(size / 2) };
|
|
23414
|
+
}
|
|
23415
|
+
function qrSize(value, style = "half", errorCorrection = "M") {
|
|
23416
|
+
return sizeOf(encode(value, errorCorrection).size, style);
|
|
23417
|
+
}
|
|
23418
|
+
function qrStyleFor(value, options2 = {}) {
|
|
23419
|
+
const { area } = options2;
|
|
23420
|
+
if (!area || drawsBlockElements(options2.env)) return "half";
|
|
23421
|
+
const paint = qrSize(value, "paint", options2.errorCorrection);
|
|
23422
|
+
return paint.columns <= area.columns && paint.rows <= area.rows ? "paint" : "half";
|
|
23423
|
+
}
|
|
23424
|
+
function qrAreaOf(stream, frame) {
|
|
23425
|
+
if (!stream.columns || !stream.rows) return void 0;
|
|
23426
|
+
return { columns: stream.columns - frame.indent, rows: stream.rows - frame.rows };
|
|
23427
|
+
}
|
|
23428
|
+
function halfLines(size, at, plain) {
|
|
23429
|
+
const lines = [];
|
|
23430
|
+
for (let y = 0; y < size; y += 2) {
|
|
23431
|
+
let line = "";
|
|
23432
|
+
for (let x = 0; x < size; x++) {
|
|
23433
|
+
const top = at(x, y);
|
|
23434
|
+
const bottom = y + 1 < size ? at(x, y + 1) : false;
|
|
23435
|
+
if (plain) {
|
|
23436
|
+
line += top && bottom ? "#" : top ? "^" : bottom ? "v" : " ";
|
|
23437
|
+
continue;
|
|
23213
23438
|
}
|
|
23214
|
-
|
|
23215
|
-
|
|
23216
|
-
|
|
23217
|
-
|
|
23218
|
-
|
|
23219
|
-
|
|
23220
|
-
|
|
23221
|
-
}
|
|
23222
|
-
const childPort = this.options.childPort();
|
|
23223
|
-
if (!childPort) {
|
|
23224
|
-
res.writeHead(503, { "content-type": "text/html; charset=utf-8", "cache-control": "no-store", "retry-after": "2" });
|
|
23225
|
-
res.end(recoveryPage(req.url ?? "/"));
|
|
23226
|
-
return;
|
|
23439
|
+
if (top === bottom) {
|
|
23440
|
+
line += `${bg(top)} `;
|
|
23441
|
+
continue;
|
|
23442
|
+
}
|
|
23443
|
+
const above = y > 0 ? at(x, y - 1) : false;
|
|
23444
|
+
const below = y + 2 < size ? at(x, y + 2) : false;
|
|
23445
|
+
line += below === bottom && above !== top ? `${fg(top)}${bg(bottom)}${UPPER_HALF}` : `${fg(bottom)}${bg(top)}${LOWER_HALF}`;
|
|
23227
23446
|
}
|
|
23228
|
-
|
|
23229
|
-
host: "127.0.0.1",
|
|
23230
|
-
port: childPort,
|
|
23231
|
-
method: req.method,
|
|
23232
|
-
path: req.url,
|
|
23233
|
-
headers: proxyHeaders(req, childPort, this.vouchesSameOrigin(req))
|
|
23234
|
-
}, (upstreamResponse) => {
|
|
23235
|
-
res.writeHead(upstreamResponse.statusCode ?? 502, upstreamResponse.headers);
|
|
23236
|
-
upstreamResponse.pipe(res);
|
|
23237
|
-
});
|
|
23238
|
-
upstream.once("error", () => {
|
|
23239
|
-
if (res.headersSent) return res.destroy();
|
|
23240
|
-
res.writeHead(503, { "content-type": "text/html; charset=utf-8", "cache-control": "no-store" });
|
|
23241
|
-
res.end(recoveryPage(req.url ?? "/", "unreachable"));
|
|
23242
|
-
});
|
|
23243
|
-
req.pipe(upstream);
|
|
23447
|
+
lines.push(plain ? line : `${line}${RESET}`);
|
|
23244
23448
|
}
|
|
23245
|
-
|
|
23246
|
-
|
|
23247
|
-
|
|
23248
|
-
|
|
23249
|
-
|
|
23250
|
-
|
|
23251
|
-
|
|
23252
|
-
|
|
23253
|
-
|
|
23254
|
-
|
|
23255
|
-
|
|
23256
|
-
|
|
23257
|
-
|
|
23258
|
-
}
|
|
23259
|
-
const upstream = connect(childPort, "127.0.0.1");
|
|
23260
|
-
for (const half of [socket, upstream]) {
|
|
23261
|
-
this.upgradedSockets.add(half);
|
|
23262
|
-
half.once("close", () => this.upgradedSockets.delete(half));
|
|
23263
|
-
}
|
|
23264
|
-
upstream.once("connect", () => {
|
|
23265
|
-
const headers = proxyHeaders(req, childPort);
|
|
23266
|
-
headers.connection = req.headers.connection ?? "Upgrade";
|
|
23267
|
-
const lines = [`${req.method ?? "GET"} ${req.url ?? "/"} HTTP/${req.httpVersion}`];
|
|
23268
|
-
for (const [name, value] of Object.entries(headers)) {
|
|
23269
|
-
if (value === void 0) continue;
|
|
23270
|
-
for (const entry of Array.isArray(value) ? value : [value]) lines.push(`${name}: ${entry}`);
|
|
23449
|
+
return lines;
|
|
23450
|
+
}
|
|
23451
|
+
function paintLines(size, at, plain) {
|
|
23452
|
+
const lines = [];
|
|
23453
|
+
for (let y = 0; y < size; y++) {
|
|
23454
|
+
let line = "";
|
|
23455
|
+
let last;
|
|
23456
|
+
for (let x = 0; x < size; x++) {
|
|
23457
|
+
const dark = at(x, y);
|
|
23458
|
+
if (plain) line += dark ? "##" : " ";
|
|
23459
|
+
else {
|
|
23460
|
+
if (dark !== last) line += bg(dark);
|
|
23461
|
+
line += " ";
|
|
23271
23462
|
}
|
|
23272
|
-
|
|
23273
|
-
|
|
23274
|
-
`);
|
|
23275
|
-
if (head.length > 0) upstream.write(head);
|
|
23276
|
-
socket.pipe(upstream).pipe(socket);
|
|
23277
|
-
});
|
|
23278
|
-
upstream.once("error", () => socket.destroy());
|
|
23279
|
-
socket.once("error", () => upstream.destroy());
|
|
23463
|
+
last = dark;
|
|
23464
|
+
}
|
|
23465
|
+
lines.push(plain ? line : `${line}${RESET}`);
|
|
23280
23466
|
}
|
|
23281
|
-
|
|
23467
|
+
return lines;
|
|
23468
|
+
}
|
|
23469
|
+
function renderQrLines(value, options2 = {}) {
|
|
23470
|
+
if (!value) throw new Error("renderQr requires a value");
|
|
23471
|
+
const errorCorrection = options2.errorCorrection ?? "M";
|
|
23472
|
+
const { size, at } = encode(value, errorCorrection);
|
|
23473
|
+
const style = options2.style ?? qrStyleFor(value, { area: options2.area, env: options2.env, errorCorrection });
|
|
23474
|
+
return style === "paint" ? paintLines(size, at, options2.plain ?? false) : halfLines(size, at, options2.plain ?? false);
|
|
23475
|
+
}
|
|
23282
23476
|
|
|
23283
23477
|
// src/sessions-cli.ts
|
|
23478
|
+
init_restart_supervisor();
|
|
23284
23479
|
var ago = (at, now = Date.now()) => {
|
|
23285
23480
|
const seconds = Math.max(0, Math.round((now - at) / 1e3));
|
|
23286
23481
|
if (seconds < 90) return "just now";
|
|
@@ -23309,9 +23504,10 @@ async function call(port, init) {
|
|
|
23309
23504
|
async function listSessions(port) {
|
|
23310
23505
|
const body = await call(port);
|
|
23311
23506
|
const sessions = body?.sessions ?? [];
|
|
23312
|
-
const
|
|
23507
|
+
const now = Date.now();
|
|
23508
|
+
const live = sessions.filter((s) => s.revokedAt === void 0 && (s.expiresAt === void 0 || s.expiresAt > now));
|
|
23313
23509
|
if (live.length === 0) {
|
|
23314
|
-
console.log("No device
|
|
23510
|
+
console.log("No device is signed in.");
|
|
23315
23511
|
process.exit(0);
|
|
23316
23512
|
}
|
|
23317
23513
|
const width = Math.max(...live.map((s) => s.id.length));
|
|
@@ -23338,6 +23534,9 @@ async function signOutSession(port, target2) {
|
|
|
23338
23534
|
process.exit(0);
|
|
23339
23535
|
}
|
|
23340
23536
|
|
|
23537
|
+
// src/production.ts
|
|
23538
|
+
init_restart_supervisor();
|
|
23539
|
+
|
|
23341
23540
|
// src/pane-host.ts
|
|
23342
23541
|
var CTRL_C = "";
|
|
23343
23542
|
function raiseInterrupt() {
|
|
@@ -23506,7 +23705,7 @@ import { homedir as homedir5 } from "node:os";
|
|
|
23506
23705
|
init_frizz_paths();
|
|
23507
23706
|
init_src();
|
|
23508
23707
|
import { spawn as spawn2 } from "node:child_process";
|
|
23509
|
-
import { existsSync as
|
|
23708
|
+
import { existsSync as existsSync3, mkdirSync as mkdirSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync3, rmSync } from "node:fs";
|
|
23510
23709
|
import { createInterface } from "node:readline/promises";
|
|
23511
23710
|
import { homedir as homedir4 } from "node:os";
|
|
23512
23711
|
import { dirname as dirname4, join as join5 } from "node:path";
|
|
@@ -23514,7 +23713,7 @@ import { dirname as dirname4, join as join5 } from "node:path";
|
|
|
23514
23713
|
// src/identity.ts
|
|
23515
23714
|
init_frizz_paths();
|
|
23516
23715
|
init_src();
|
|
23517
|
-
import { mkdirSync as mkdirSync2, readFileSync as
|
|
23716
|
+
import { mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "node:fs";
|
|
23518
23717
|
import { homedir as homedir3 } from "node:os";
|
|
23519
23718
|
import { dirname as dirname3, join as join4 } from "node:path";
|
|
23520
23719
|
function claimIdentityPath(home = homedir3(), env) {
|
|
@@ -23524,7 +23723,7 @@ async function loadOrCreateClaimIdentity(home = homedir3()) {
|
|
|
23524
23723
|
const path = claimIdentityPath(home);
|
|
23525
23724
|
let stored;
|
|
23526
23725
|
try {
|
|
23527
|
-
stored =
|
|
23726
|
+
stored = readFileSync2(path);
|
|
23528
23727
|
} catch {
|
|
23529
23728
|
stored = void 0;
|
|
23530
23729
|
}
|
|
@@ -23541,52 +23740,144 @@ async function loadOrCreateClaimIdentity(home = homedir3()) {
|
|
|
23541
23740
|
return identity;
|
|
23542
23741
|
}
|
|
23543
23742
|
|
|
23544
|
-
// src/github-
|
|
23545
|
-
|
|
23546
|
-
|
|
23547
|
-
|
|
23548
|
-
|
|
23743
|
+
// src/github-device-flow.ts
|
|
23744
|
+
var GITHUB_CLIENT_ID_PLACEHOLDER = "frizz-oauth-app-client-id-not-set";
|
|
23745
|
+
var FRIZZ_GITHUB_CLIENT_ID = GITHUB_CLIENT_ID_PLACEHOLDER;
|
|
23746
|
+
function githubClientId(env = process.env) {
|
|
23747
|
+
const id = env.FRIZZ_GITHUB_CLIENT_ID?.trim() || FRIZZ_GITHUB_CLIENT_ID;
|
|
23748
|
+
return id === GITHUB_CLIENT_ID_PLACEHOLDER ? null : id;
|
|
23749
|
+
}
|
|
23750
|
+
var GithubSignInError = class extends Error {
|
|
23549
23751
|
constructor(message, code) {
|
|
23550
23752
|
super(message);
|
|
23551
23753
|
this.code = code;
|
|
23552
|
-
this.name = "
|
|
23754
|
+
this.name = "GithubSignInError";
|
|
23553
23755
|
}
|
|
23554
23756
|
};
|
|
23555
|
-
|
|
23556
|
-
|
|
23757
|
+
var GITHUB_DEVICE_URL = "https://github.com/login/device";
|
|
23758
|
+
function abortableSleep(ms, signal) {
|
|
23759
|
+
return new Promise((resolve7, reject) => {
|
|
23760
|
+
if (signal?.aborted) return reject(cancelled());
|
|
23761
|
+
const timer = setTimeout(() => {
|
|
23762
|
+
signal?.removeEventListener("abort", onAbort);
|
|
23763
|
+
resolve7();
|
|
23764
|
+
}, ms);
|
|
23765
|
+
const onAbort = () => {
|
|
23766
|
+
clearTimeout(timer);
|
|
23767
|
+
reject(cancelled());
|
|
23768
|
+
};
|
|
23769
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
23770
|
+
});
|
|
23771
|
+
}
|
|
23772
|
+
var cancelled = () => new GithubSignInError("the GitHub sign-in was cancelled", "cancelled");
|
|
23773
|
+
async function postForm(fetchImpl, url, fields, signal) {
|
|
23774
|
+
let response;
|
|
23557
23775
|
try {
|
|
23558
|
-
|
|
23776
|
+
response = await fetchImpl(url, {
|
|
23777
|
+
method: "POST",
|
|
23778
|
+
headers: {
|
|
23779
|
+
accept: "application/json",
|
|
23780
|
+
"content-type": "application/x-www-form-urlencoded",
|
|
23781
|
+
"user-agent": "frizz"
|
|
23782
|
+
},
|
|
23783
|
+
body: new URLSearchParams(fields).toString(),
|
|
23784
|
+
...signal ? { signal } : {}
|
|
23785
|
+
});
|
|
23559
23786
|
} catch (error) {
|
|
23560
|
-
|
|
23561
|
-
|
|
23562
|
-
|
|
23563
|
-
|
|
23564
|
-
|
|
23565
|
-
|
|
23566
|
-
|
|
23567
|
-
|
|
23568
|
-
|
|
23569
|
-
|
|
23787
|
+
if (signal?.aborted) throw cancelled();
|
|
23788
|
+
throw new GithubSignInError(
|
|
23789
|
+
`could not reach GitHub to sign in: ${error instanceof Error ? error.message : error}`,
|
|
23790
|
+
"unreachable"
|
|
23791
|
+
);
|
|
23792
|
+
}
|
|
23793
|
+
const body = await response.json().catch(() => null);
|
|
23794
|
+
return { status: response.status, body: body ?? {} };
|
|
23795
|
+
}
|
|
23796
|
+
var oauthErrorText = (body, fallback) => typeof body.error_description === "string" && body.error_description ? body.error_description : typeof body.error === "string" && body.error ? body.error : fallback;
|
|
23797
|
+
async function runDeviceFlow(options2) {
|
|
23798
|
+
const clientId = options2.clientId === void 0 ? githubClientId() : options2.clientId;
|
|
23799
|
+
if (!clientId) {
|
|
23800
|
+
throw new GithubSignInError(
|
|
23801
|
+
"custom frizz.sh names are not available in this version of Frizz yet: its GitHub sign-in is not set up. A private frizz.sh name needs no account and works today.",
|
|
23802
|
+
"not-configured"
|
|
23570
23803
|
);
|
|
23571
23804
|
}
|
|
23572
|
-
const
|
|
23573
|
-
|
|
23574
|
-
|
|
23575
|
-
|
|
23576
|
-
|
|
23805
|
+
const fetchImpl = options2.fetchImpl ?? fetch;
|
|
23806
|
+
const now = options2.now ?? Date.now;
|
|
23807
|
+
const sleep = options2.sleep ?? abortableSleep;
|
|
23808
|
+
const githubOrigin = (options2.githubOrigin ?? "https://github.com").replace(/\/$/, "");
|
|
23809
|
+
const apiOrigin = (options2.apiOrigin ?? "https://api.github.com").replace(/\/$/, "");
|
|
23810
|
+
const { signal } = options2;
|
|
23811
|
+
const start = await postForm(fetchImpl, `${githubOrigin}/login/device/code`, { client_id: clientId }, signal);
|
|
23812
|
+
const deviceCode = start.body.device_code;
|
|
23813
|
+
const userCode = start.body.user_code;
|
|
23814
|
+
if (start.status !== 200 || typeof deviceCode !== "string" || typeof userCode !== "string") {
|
|
23815
|
+
throw new GithubSignInError(
|
|
23816
|
+
`GitHub would not start a sign-in: ${oauthErrorText(start.body, `HTTP ${start.status}`)}`,
|
|
23817
|
+
"failed"
|
|
23818
|
+
);
|
|
23819
|
+
}
|
|
23820
|
+
const verificationUri = typeof start.body.verification_uri === "string" ? start.body.verification_uri : GITHUB_DEVICE_URL;
|
|
23821
|
+
const expiresIn = typeof start.body.expires_in === "number" ? start.body.expires_in : 900;
|
|
23822
|
+
let interval = typeof start.body.interval === "number" ? start.body.interval : 5;
|
|
23823
|
+
const expiresAt = now() + expiresIn * 1e3;
|
|
23824
|
+
options2.onPrompt({ userCode, verificationUri, expiresAt });
|
|
23825
|
+
for (; ; ) {
|
|
23826
|
+
await sleep(interval * 1e3, signal);
|
|
23827
|
+
if (now() >= expiresAt) {
|
|
23828
|
+
throw new GithubSignInError("the GitHub code expired before it was entered \u2014 try again", "expired");
|
|
23829
|
+
}
|
|
23830
|
+
const poll = await postForm(
|
|
23831
|
+
fetchImpl,
|
|
23832
|
+
`${githubOrigin}/login/oauth/access_token`,
|
|
23833
|
+
{ client_id: clientId, device_code: deviceCode, grant_type: "urn:ietf:params:oauth:grant-type:device_code" },
|
|
23834
|
+
signal
|
|
23577
23835
|
);
|
|
23836
|
+
const error = poll.body.error;
|
|
23837
|
+
if (error === "authorization_pending") continue;
|
|
23838
|
+
if (error === "slow_down") {
|
|
23839
|
+
interval = typeof poll.body.interval === "number" ? poll.body.interval : interval + 5;
|
|
23840
|
+
continue;
|
|
23841
|
+
}
|
|
23842
|
+
if (error === "expired_token") {
|
|
23843
|
+
throw new GithubSignInError("the GitHub code expired before it was entered \u2014 try again", "expired");
|
|
23844
|
+
}
|
|
23845
|
+
if (error === "access_denied") {
|
|
23846
|
+
throw new GithubSignInError("the GitHub sign-in was declined, so no name was claimed", "denied");
|
|
23847
|
+
}
|
|
23848
|
+
const token = poll.body.access_token;
|
|
23849
|
+
if (typeof error === "string" || typeof token !== "string" || !token) {
|
|
23850
|
+
throw new GithubSignInError(`GitHub sign-in failed: ${oauthErrorText(poll.body, `HTTP ${poll.status}`)}`, "failed");
|
|
23851
|
+
}
|
|
23852
|
+
const scope = typeof poll.body.scope === "string" ? poll.body.scope.trim() : "";
|
|
23853
|
+
if (scope) {
|
|
23854
|
+
throw new GithubSignInError(
|
|
23855
|
+
`GitHub granted this sign-in more access than a name needs (${scope}), so Frizz did not use it`,
|
|
23856
|
+
"scoped"
|
|
23857
|
+
);
|
|
23858
|
+
}
|
|
23859
|
+
return { token, login: await readLogin(fetchImpl, apiOrigin, token) };
|
|
23578
23860
|
}
|
|
23579
|
-
return token;
|
|
23580
23861
|
}
|
|
23581
|
-
async function
|
|
23862
|
+
async function readLogin(fetchImpl, apiOrigin, token) {
|
|
23582
23863
|
try {
|
|
23583
|
-
const
|
|
23584
|
-
|
|
23864
|
+
const response = await fetchImpl(`${apiOrigin}/user`, {
|
|
23865
|
+
headers: { authorization: `Bearer ${token}`, accept: "application/vnd.github+json", "user-agent": "frizz" }
|
|
23866
|
+
});
|
|
23867
|
+
if (!response.ok) return null;
|
|
23868
|
+
const user = await response.json();
|
|
23869
|
+
return typeof user.login === "string" ? user.login : null;
|
|
23585
23870
|
} catch {
|
|
23586
23871
|
return null;
|
|
23587
23872
|
}
|
|
23588
23873
|
}
|
|
23589
|
-
|
|
23874
|
+
function deviceCodeMessage(prompt) {
|
|
23875
|
+
return `to confirm your GitHub account, open ${prompt.verificationUri} and enter ${prompt.userCode} (Frizz asks GitHub for no permissions)`;
|
|
23876
|
+
}
|
|
23877
|
+
function githubDeviceFlow(options2 = {}) {
|
|
23878
|
+
const onPrompt = options2.onPrompt ?? ((prompt) => console.log(` ${deviceCodeMessage(prompt)}`));
|
|
23879
|
+
return { authorize: () => runDeviceFlow({ onPrompt, ...options2.signal ? { signal: options2.signal } : {} }) };
|
|
23880
|
+
}
|
|
23590
23881
|
|
|
23591
23882
|
// src/registrar-client.ts
|
|
23592
23883
|
init_src();
|
|
@@ -23596,8 +23887,9 @@ function registrarOrigin(env = process.env) {
|
|
|
23596
23887
|
}
|
|
23597
23888
|
var ADVICE = {
|
|
23598
23889
|
"name-taken": "that name is already taken \u2014 try another",
|
|
23599
|
-
"github-required": "claiming a name needs a
|
|
23600
|
-
"github-rejected": "GitHub did not recognise that
|
|
23890
|
+
"github-required": "claiming a custom name needs a GitHub sign-in \u2014 try again and enter the code GitHub shows",
|
|
23891
|
+
"github-rejected": "GitHub did not recognise that sign-in \u2014 try again",
|
|
23892
|
+
"github-token-scoped": "the registrar accepts only a GitHub sign-in that grants no permissions \u2014 update Frizz and try again",
|
|
23601
23893
|
"github-too-new": "that GitHub account is too new to claim a name",
|
|
23602
23894
|
"one-name-per-account": "that GitHub account already holds a name \u2014 release it first",
|
|
23603
23895
|
"one-name-per-key": "this machine already holds a name \u2014 release it first",
|
|
@@ -23674,11 +23966,11 @@ function serveRelayRequest(frame, options2) {
|
|
|
23674
23966
|
const target2 = new URL(frame.url);
|
|
23675
23967
|
const local = new URL(options2.origin);
|
|
23676
23968
|
const publicUrl = new URL(options2.publicOrigin);
|
|
23677
|
-
let
|
|
23969
|
+
let cancelled2 = false;
|
|
23678
23970
|
let abort;
|
|
23679
23971
|
const done = new Promise((resolve7) => {
|
|
23680
23972
|
const send = (upFrame) => {
|
|
23681
|
-
if (!
|
|
23973
|
+
if (!cancelled2) options2.send(upFrame);
|
|
23682
23974
|
};
|
|
23683
23975
|
const headers = {};
|
|
23684
23976
|
for (const [name, value] of stripHopByHop(frame.headers)) headers[name] = value;
|
|
@@ -23772,8 +24064,8 @@ function serveRelayRequest(frame, options2) {
|
|
|
23772
24064
|
return {
|
|
23773
24065
|
done,
|
|
23774
24066
|
cancel: () => {
|
|
23775
|
-
if (
|
|
23776
|
-
|
|
24067
|
+
if (cancelled2) return;
|
|
24068
|
+
cancelled2 = true;
|
|
23777
24069
|
abort?.();
|
|
23778
24070
|
}
|
|
23779
24071
|
};
|
|
@@ -24053,7 +24345,7 @@ function zoneClaimLabel(hostname) {
|
|
|
24053
24345
|
const label = hostname.slice(0, -suffix.length);
|
|
24054
24346
|
return label.length > 0 && !label.includes(".") ? label : null;
|
|
24055
24347
|
}
|
|
24056
|
-
async function reconcileCloudConfig(config, port, home = homedir4(), onNotice, origin, github =
|
|
24348
|
+
async function reconcileCloudConfig(config, port, home = homedir4(), onNotice, origin, github = githubDeviceFlow(onNotice ? { onPrompt: (prompt) => onNotice(deviceCodeMessage(prompt)) } : {})) {
|
|
24057
24349
|
if (isRelayConfig(config)) return config;
|
|
24058
24350
|
const label = config.claim ?? zoneClaimLabel(config.hostname);
|
|
24059
24351
|
if (!label) return config;
|
|
@@ -24067,7 +24359,7 @@ function cloudConfigPath(home = homedir4()) {
|
|
|
24067
24359
|
}
|
|
24068
24360
|
function readCloudConfig(home = homedir4()) {
|
|
24069
24361
|
try {
|
|
24070
|
-
const parsed = JSON.parse(
|
|
24362
|
+
const parsed = JSON.parse(readFileSync3(cloudConfigPath(home), "utf8"));
|
|
24071
24363
|
if (typeof parsed.hostname !== "string" || !parsed.hostname) return null;
|
|
24072
24364
|
const claimed = typeof parsed.claim === "string" && parsed.claim.length > 0;
|
|
24073
24365
|
const named = typeof parsed.tunnel === "string" && parsed.tunnel.length > 0;
|
|
@@ -24090,7 +24382,7 @@ function tunnelTokenPath(home = homedir4()) {
|
|
|
24090
24382
|
}
|
|
24091
24383
|
function readTunnelToken(home = homedir4()) {
|
|
24092
24384
|
try {
|
|
24093
|
-
const token =
|
|
24385
|
+
const token = readFileSync3(tunnelTokenPath(home), "utf8").trim();
|
|
24094
24386
|
return token.length > 0 ? token : null;
|
|
24095
24387
|
} catch {
|
|
24096
24388
|
return null;
|
|
@@ -24128,7 +24420,7 @@ async function promptForTunnelName(hostname, fallback) {
|
|
|
24128
24420
|
rl.close();
|
|
24129
24421
|
}
|
|
24130
24422
|
}
|
|
24131
|
-
async function establishCloudConfig(answer, port, home = homedir4(), origin, github =
|
|
24423
|
+
async function establishCloudConfig(answer, port, home = homedir4(), origin, github = githubDeviceFlow()) {
|
|
24132
24424
|
if (answer.includes(".")) {
|
|
24133
24425
|
const hostname = normalizeHostname(answer);
|
|
24134
24426
|
const tunnel = await promptForTunnelName(hostname, hostname.split(".")[0]);
|
|
@@ -24147,9 +24439,9 @@ async function establishCloudConfig(answer, port, home = homedir4(), origin, git
|
|
|
24147
24439
|
const identity = await loadOrCreateClaimIdentity(home);
|
|
24148
24440
|
let token;
|
|
24149
24441
|
if (!anonymous) {
|
|
24150
|
-
|
|
24151
|
-
|
|
24152
|
-
if (login) console.log(` claiming ${name}.frizz.sh for GitHub user ${login}`);
|
|
24442
|
+
const authorization = await github.authorize();
|
|
24443
|
+
token = authorization.token;
|
|
24444
|
+
if (authorization.login) console.log(` claiming ${name}.frizz.sh for GitHub user ${authorization.login}`);
|
|
24153
24445
|
} else {
|
|
24154
24446
|
console.log(" claiming a private name on frizz.sh \u2014 no account needed");
|
|
24155
24447
|
}
|
|
@@ -24192,7 +24484,7 @@ async function resolveRunToken(config, port, home = homedir4(), onWarning, origi
|
|
|
24192
24484
|
function resolveTunnelConfigPath(config, home = homedir4()) {
|
|
24193
24485
|
if (config.config) return config.config;
|
|
24194
24486
|
const candidate = join5(home, ".cloudflared", "frizz.yml");
|
|
24195
|
-
return
|
|
24487
|
+
return existsSync3(candidate) ? candidate : null;
|
|
24196
24488
|
}
|
|
24197
24489
|
async function startRelay(config, port, home = homedir4(), onStatus) {
|
|
24198
24490
|
const identity = await loadOrCreateClaimIdentity(home);
|
|
@@ -24322,29 +24614,23 @@ function createRemoteController(options2) {
|
|
|
24322
24614
|
}
|
|
24323
24615
|
|
|
24324
24616
|
// src/remote-detect.ts
|
|
24325
|
-
import { execFile as
|
|
24617
|
+
import { execFile as execFile2 } from "node:child_process";
|
|
24326
24618
|
var PROBE_TIMEOUT_MS = 4e3;
|
|
24327
|
-
function
|
|
24619
|
+
function run(command, args2) {
|
|
24328
24620
|
return new Promise((resolve7) => {
|
|
24329
|
-
|
|
24621
|
+
execFile2(command, args2, { encoding: "utf8", timeout: PROBE_TIMEOUT_MS }, (error, stdout) => {
|
|
24330
24622
|
resolve7(error ? null : stdout);
|
|
24331
24623
|
});
|
|
24332
24624
|
});
|
|
24333
24625
|
}
|
|
24334
|
-
async function probeGithub() {
|
|
24335
|
-
const version = await run2("gh", ["--version"]);
|
|
24336
|
-
if (version === null) return { login: null, installed: false };
|
|
24337
|
-
const login = await run2("gh", ["api", "user", "--jq", ".login"]);
|
|
24338
|
-
return { login: login?.trim() || null, installed: true };
|
|
24339
|
-
}
|
|
24340
24626
|
async function probeCloudflared() {
|
|
24341
|
-
const out = await
|
|
24627
|
+
const out = await run("cloudflared", ["--version"]);
|
|
24342
24628
|
const version = out?.match(/\d+\.\d+\.\d+/)?.[0] ?? (out ? out.trim() : null);
|
|
24343
24629
|
return { version };
|
|
24344
24630
|
}
|
|
24345
24631
|
async function probeTailscale() {
|
|
24346
|
-
const out = await
|
|
24347
|
-
if (out === null) return { installed: await
|
|
24632
|
+
const out = await run("tailscale", ["status", "--json"]);
|
|
24633
|
+
if (out === null) return { installed: await run("tailscale", ["--version"]) !== null, dnsName: null };
|
|
24348
24634
|
try {
|
|
24349
24635
|
const status = JSON.parse(out);
|
|
24350
24636
|
const dnsName = status.Self?.DNSName?.replace(/\.$/, "") || null;
|
|
@@ -24358,7 +24644,7 @@ async function probeTailscale() {
|
|
|
24358
24644
|
init_src();
|
|
24359
24645
|
var CHOICES = [
|
|
24360
24646
|
{ kind: "private", title: "Private name", blurb: "an unguessable name on frizz.sh \u2014 no account, nothing to install" },
|
|
24361
|
-
{ kind: "frizz", title: "Custom name", blurb: "<name>.frizz.sh of your choosing; needs
|
|
24647
|
+
{ kind: "frizz", title: "Custom name", blurb: "<name>.frizz.sh of your choosing; needs a GitHub account" },
|
|
24362
24648
|
{ kind: "cloudflare", title: "Cloudflare Tunnel", blurb: "a domain you own on Cloudflare; cloudflared on this machine" },
|
|
24363
24649
|
{ kind: "tailscale", title: "Tailscale", blurb: "your tailnet; tailscale serve does the TLS" },
|
|
24364
24650
|
{ kind: "other", title: "Something else", blurb: "any proxy or tunnel you run \u2014 tell Frizz its address" },
|
|
@@ -24395,7 +24681,6 @@ function createRemotePane(options2) {
|
|
|
24395
24681
|
const output = options2.output ?? process.stdout;
|
|
24396
24682
|
let open = false;
|
|
24397
24683
|
let screen = { name: "menu", index: 0 };
|
|
24398
|
-
let github = null;
|
|
24399
24684
|
let cloudflared = null;
|
|
24400
24685
|
let tailscale = null;
|
|
24401
24686
|
const write = (lines) => {
|
|
@@ -24440,6 +24725,10 @@ function createRemotePane(options2) {
|
|
|
24440
24725
|
return;
|
|
24441
24726
|
}
|
|
24442
24727
|
if (s.name === "busy") {
|
|
24728
|
+
if (s.detail) {
|
|
24729
|
+
write([s.message, "", ...s.detail, "", `${DIM}waiting for GitHub\u2026 \xB7 esc cancel${RESET2}`]);
|
|
24730
|
+
return;
|
|
24731
|
+
}
|
|
24443
24732
|
write([s.message, "", `${DIM}working\u2026${RESET2}`]);
|
|
24444
24733
|
return;
|
|
24445
24734
|
}
|
|
@@ -24469,7 +24758,6 @@ function createRemotePane(options2) {
|
|
|
24469
24758
|
};
|
|
24470
24759
|
const formHead = (kind) => {
|
|
24471
24760
|
if (kind === "frizz") {
|
|
24472
|
-
const gh = github === "pending" || github === null ? `${DIM}GitHub CLI checking\u2026${RESET2}` : !github.installed ? "GitHub CLI not installed \u2014 see https://cli.github.com, then `gh auth login`" : github.login ? check(true, `GitHub CLI signed in as ${github.login}`) : "GitHub CLI not signed in \u2014 run `gh auth login` in another terminal";
|
|
24473
24761
|
return [
|
|
24474
24762
|
"Custom frizz.sh name",
|
|
24475
24763
|
"",
|
|
@@ -24478,7 +24766,9 @@ function createRemotePane(options2) {
|
|
|
24478
24766
|
),
|
|
24479
24767
|
...options2.sandbox ? ["", ...wrap("This is a sandbox, but a claim is real: it binds this machine's one name to your account, and your real board keeps it. Only the setup saved here is thrown away.")] : [],
|
|
24480
24768
|
"",
|
|
24481
|
-
|
|
24769
|
+
...wrap(
|
|
24770
|
+
"When you claim, Frizz shows a code to enter at github.com/login/device. The sign-in grants Frizz no permissions: it only tells the registrar which account you are."
|
|
24771
|
+
)
|
|
24482
24772
|
];
|
|
24483
24773
|
}
|
|
24484
24774
|
if (kind === "cloudflare") {
|
|
@@ -24528,14 +24818,6 @@ function createRemotePane(options2) {
|
|
|
24528
24818
|
const same = kindOf(current) === kind ? current : null;
|
|
24529
24819
|
let fields;
|
|
24530
24820
|
if (kind === "frizz") {
|
|
24531
|
-
github = "pending";
|
|
24532
|
-
void options2.probes.github().then((result) => {
|
|
24533
|
-
github = result;
|
|
24534
|
-
if (screen.name === "form" && screen.kind === "frizz" && !screen.fields[0].value && result.login) {
|
|
24535
|
-
screen.fields[0].placeholder = result.login;
|
|
24536
|
-
}
|
|
24537
|
-
paint();
|
|
24538
|
-
});
|
|
24539
24821
|
fields = [{ label: "Name", value: same?.claim ?? "" }];
|
|
24540
24822
|
} else if (kind === "cloudflare") {
|
|
24541
24823
|
cloudflared = "pending";
|
|
@@ -24574,7 +24856,24 @@ function createRemotePane(options2) {
|
|
|
24574
24856
|
if (!name) throw new Error("a name is needed");
|
|
24575
24857
|
screen = { name: "busy", message: `claiming ${name}.frizz.sh\u2026` };
|
|
24576
24858
|
paint();
|
|
24577
|
-
|
|
24859
|
+
const abort = new AbortController();
|
|
24860
|
+
next = await options2.claim(name, {
|
|
24861
|
+
signal: abort.signal,
|
|
24862
|
+
onDeviceCode: (prompt) => {
|
|
24863
|
+
screen = {
|
|
24864
|
+
name: "busy",
|
|
24865
|
+
message: `Confirm your GitHub account to claim ${name}.frizz.sh`,
|
|
24866
|
+
detail: [
|
|
24867
|
+
`Open ${prompt.verificationUri}`,
|
|
24868
|
+
`Enter ${prompt.userCode}`,
|
|
24869
|
+
"",
|
|
24870
|
+
...wrap("on this or any other device. Frizz asks GitHub for no permissions; the sign-in only names your account to the registrar.")
|
|
24871
|
+
],
|
|
24872
|
+
cancel: () => abort.abort()
|
|
24873
|
+
};
|
|
24874
|
+
paint();
|
|
24875
|
+
}
|
|
24876
|
+
});
|
|
24578
24877
|
justClaimed = true;
|
|
24579
24878
|
} else if (form.kind === "cloudflare") {
|
|
24580
24879
|
const hostname = normalizeHostname(valueOf(form.fields[0]));
|
|
@@ -24600,7 +24899,8 @@ function createRemotePane(options2) {
|
|
|
24600
24899
|
screen = { name: "busy", message: "claiming a private name on frizz.sh\u2026" };
|
|
24601
24900
|
paint();
|
|
24602
24901
|
try {
|
|
24603
|
-
const next = await options2.claim("")
|
|
24902
|
+
const next = await options2.claim("", { signal: new AbortController().signal, onDeviceCode: () => {
|
|
24903
|
+
} });
|
|
24604
24904
|
screen = { name: "busy", message: `serving ${next.hostname}\u2026` };
|
|
24605
24905
|
paint();
|
|
24606
24906
|
await options2.apply(next, { justClaimed: true });
|
|
@@ -24635,7 +24935,13 @@ function createRemotePane(options2) {
|
|
|
24635
24935
|
},
|
|
24636
24936
|
key(key) {
|
|
24637
24937
|
const s = screen;
|
|
24638
|
-
if (s.name === "busy")
|
|
24938
|
+
if (s.name === "busy") {
|
|
24939
|
+
if (key === ESC && s.cancel) {
|
|
24940
|
+
s.cancel();
|
|
24941
|
+
s.cancel = void 0;
|
|
24942
|
+
}
|
|
24943
|
+
return "keep";
|
|
24944
|
+
}
|
|
24639
24945
|
if (s.name === "done") return "close";
|
|
24640
24946
|
if (s.name === "error") {
|
|
24641
24947
|
screen = s.back;
|
|
@@ -25033,7 +25339,7 @@ init_boot_progress();
|
|
|
25033
25339
|
init_frizz_paths();
|
|
25034
25340
|
import { execFileSync as execFileSync3 } from "node:child_process";
|
|
25035
25341
|
import { createServer as createServer2 } from "node:net";
|
|
25036
|
-
import { existsSync as
|
|
25342
|
+
import { existsSync as existsSync6, mkdirSync as mkdirSync10, mkdtempSync, readdirSync as readdirSync3, readFileSync as readFileSync10, realpathSync as realpathSync5, rmSync as rmSync8, symlinkSync as symlinkSync2, writeFileSync as writeFileSync10 } from "node:fs";
|
|
25037
25343
|
import { homedir as homedir10, tmpdir as tmpdir2 } from "node:os";
|
|
25038
25344
|
import { basename as basename5, dirname as dirname10, join as join13, resolve as resolve4 } from "node:path";
|
|
25039
25345
|
import { setTimeout as delay2 } from "node:timers/promises";
|
|
@@ -25187,21 +25493,21 @@ function shareCredentials(realHome, home, realEnv, sandboxEnv) {
|
|
|
25187
25493
|
mkdirSync10(dirname10(realKey), { recursive: true });
|
|
25188
25494
|
mkdirSync10(dirname10(sandboxKey), { recursive: true });
|
|
25189
25495
|
link(realKey, sandboxKey);
|
|
25190
|
-
if (
|
|
25496
|
+
if (existsSync6(join13(realHome, ".config", "gh"))) {
|
|
25191
25497
|
mkdirSync10(join13(home, ".config"), { recursive: true });
|
|
25192
25498
|
link(join13(realHome, ".config", "gh"), join13(home, ".config", "gh"));
|
|
25193
25499
|
}
|
|
25194
25500
|
const cloudflared = join13(realHome, ".cloudflared");
|
|
25195
|
-
if (
|
|
25501
|
+
if (existsSync6(cloudflared)) {
|
|
25196
25502
|
mkdirSync10(join13(home, ".cloudflared"), { recursive: true });
|
|
25197
25503
|
for (const entry of readdirSync3(cloudflared)) {
|
|
25198
25504
|
if (entry === "cert.pem" || entry.endsWith(".json")) link(join13(cloudflared, entry), join13(home, ".cloudflared", entry));
|
|
25199
25505
|
}
|
|
25200
25506
|
}
|
|
25201
25507
|
for (const entry of [".claude", ".claude.json", ".codex"]) {
|
|
25202
|
-
if (
|
|
25508
|
+
if (existsSync6(join13(realHome, entry))) link(join13(realHome, entry), join13(home, entry));
|
|
25203
25509
|
}
|
|
25204
|
-
if (process.platform === "darwin" &&
|
|
25510
|
+
if (process.platform === "darwin" && existsSync6(join13(realHome, "Library", "Keychains"))) {
|
|
25205
25511
|
mkdirSync10(join13(home, "Library"), { recursive: true });
|
|
25206
25512
|
link(join13(realHome, "Library", "Keychains"), join13(home, "Library", "Keychains"));
|
|
25207
25513
|
}
|
|
@@ -25294,7 +25600,7 @@ function workspaceFromLaunchTarget(target2, env = process.env) {
|
|
|
25294
25600
|
function parseStatusFile(path, authoritative, expected2, adapter = defaultProcessPlatformAdapter) {
|
|
25295
25601
|
try {
|
|
25296
25602
|
const value = JSON.parse(
|
|
25297
|
-
|
|
25603
|
+
readFileSync10(path, "utf8")
|
|
25298
25604
|
);
|
|
25299
25605
|
if (!Number.isInteger(value.pid) || value.pid <= 0 || !Number.isInteger(value.port) || value.port < 1 || value.port > 65535)
|
|
25300
25606
|
return null;
|
|
@@ -25336,7 +25642,7 @@ function liveWorkspaceOwner(stateDir, expected2, adapter = defaultProcessPlatfor
|
|
|
25336
25642
|
function readPreferredPort(stateDir) {
|
|
25337
25643
|
try {
|
|
25338
25644
|
const value = JSON.parse(
|
|
25339
|
-
|
|
25645
|
+
readFileSync10(join13(stateDir, "launcher.json"), "utf8")
|
|
25340
25646
|
);
|
|
25341
25647
|
return Number.isInteger(value.port) && value.port > 0 && value.port <= 65535 ? value.port : void 0;
|
|
25342
25648
|
} catch {
|
|
@@ -25572,15 +25878,16 @@ init_project_launch();
|
|
|
25572
25878
|
|
|
25573
25879
|
// packages/server/src/dev-supervisor.ts
|
|
25574
25880
|
init_project_launch();
|
|
25881
|
+
init_restart_supervisor();
|
|
25882
|
+
init_logging();
|
|
25883
|
+
init_boot_progress();
|
|
25575
25884
|
import { fork } from "node:child_process";
|
|
25576
25885
|
import { randomUUID as randomUUID5 } from "node:crypto";
|
|
25577
|
-
import { existsSync as
|
|
25886
|
+
import { existsSync as existsSync7, mkdirSync as mkdirSync11, readFileSync as readFileSync11 } from "node:fs";
|
|
25578
25887
|
import { createServer as createNetServer } from "node:net";
|
|
25579
25888
|
import { basename as basename6, dirname as dirname11, extname as extname2, isAbsolute as isAbsolute4, relative as relative2, resolve as resolve5, sep as sep3 } from "node:path";
|
|
25580
25889
|
import { fileURLToPath } from "node:url";
|
|
25581
25890
|
import watcher from "@parcel/watcher";
|
|
25582
|
-
init_logging();
|
|
25583
|
-
init_boot_progress();
|
|
25584
25891
|
var DEV_RESTART_DEBOUNCE_MS = 180;
|
|
25585
25892
|
var DEV_CRASH_STABLE_MS = 5e3;
|
|
25586
25893
|
var DEV_CRASH_RETRY_BASE_MS = 500;
|
|
@@ -25681,8 +25988,8 @@ function isWithin(path, root) {
|
|
|
25681
25988
|
const rel = relative2(root, path);
|
|
25682
25989
|
return rel === "" || rel !== ".." && !rel.startsWith(`..${sep3}`) && !isAbsolute4(rel);
|
|
25683
25990
|
}
|
|
25684
|
-
function ignoredDevPath(
|
|
25685
|
-
const parts =
|
|
25991
|
+
function ignoredDevPath(relPath) {
|
|
25992
|
+
const parts = relPath.split(sep3);
|
|
25686
25993
|
const name = parts.at(-1) ?? "";
|
|
25687
25994
|
return parts.some((part) => GENERATED_DIRS.has(part) || part === "fixtures" || part.endsWith(".fixtures")) || /\.(?:test|spec)\.[^.]+$/.test(name) || name.includes(".golden.");
|
|
25688
25995
|
}
|
|
@@ -25690,9 +25997,9 @@ function classifyDevChange(path, roots = defaultDevWatchRoots()) {
|
|
|
25690
25997
|
const absolute = resolve5(path);
|
|
25691
25998
|
const root = roots.find((candidate) => isWithin(absolute, resolve5(candidate)));
|
|
25692
25999
|
if (!root) return null;
|
|
25693
|
-
if (ignoredDevPath(absolute)) return null;
|
|
25694
|
-
const name = basename6(absolute);
|
|
25695
26000
|
const relToWorkspace = relative2(resolve5(root), absolute);
|
|
26001
|
+
if (ignoredDevPath(relToWorkspace)) return null;
|
|
26002
|
+
const name = basename6(absolute);
|
|
25696
26003
|
const parts = relToWorkspace.split(sep3);
|
|
25697
26004
|
const packageName = parts[0] === "packages" ? parts[1] : void 0;
|
|
25698
26005
|
if (name === "package.json" && parts.length === 3 && packageName && CHILD_PACKAGE_METADATA.has(packageName)) {
|
|
@@ -25718,7 +26025,7 @@ function devConfigSyntaxError(path) {
|
|
|
25718
26025
|
if (name !== "package.json" && !isTsconfig) return null;
|
|
25719
26026
|
let text;
|
|
25720
26027
|
try {
|
|
25721
|
-
text =
|
|
26028
|
+
text = readFileSync11(path, "utf8");
|
|
25722
26029
|
} catch (err) {
|
|
25723
26030
|
return `${name}: ${err instanceof Error ? err.message : err}`;
|
|
25724
26031
|
}
|
|
@@ -25896,6 +26203,7 @@ var Supervisor = class {
|
|
|
25896
26203
|
publicOrigin: opts.publicOrigin,
|
|
25897
26204
|
onCodeConsumed: opts.onCodeConsumed,
|
|
25898
26205
|
sessionKey: opts.sessionKey,
|
|
26206
|
+
rotateSessionKey: opts.rotateSessionKey,
|
|
25899
26207
|
sessionDirectory: opts.sessionDirectory,
|
|
25900
26208
|
childPort: () => this.childPort,
|
|
25901
26209
|
restart: () => this.restartFromBrowser(),
|
|
@@ -25941,7 +26249,7 @@ var Supervisor = class {
|
|
|
25941
26249
|
await this.publicProxy.listen();
|
|
25942
26250
|
if (this.watchEnabled) {
|
|
25943
26251
|
const settled = await Promise.allSettled(
|
|
25944
|
-
this.roots.filter(
|
|
26252
|
+
this.roots.filter(existsSync7).map((root) => this.watchSubscribe(root, (err, events) => this.onWatch(err, events), {
|
|
25945
26253
|
ignore: DEV_WATCH_IGNORE
|
|
25946
26254
|
}))
|
|
25947
26255
|
);
|
|
@@ -26000,7 +26308,7 @@ var Supervisor = class {
|
|
|
26000
26308
|
}
|
|
26001
26309
|
this.reloadLauncher ||= reloadLauncher;
|
|
26002
26310
|
if (this.debounce) clearTimeout(this.debounce);
|
|
26003
|
-
const
|
|
26311
|
+
const run2 = () => {
|
|
26004
26312
|
this.debounce = null;
|
|
26005
26313
|
const scope = this.reloadLauncher ? "control plane + launcher" : "control plane";
|
|
26006
26314
|
this.logLine(
|
|
@@ -26009,8 +26317,8 @@ var Supervisor = class {
|
|
|
26009
26317
|
this.writeStatus("restarting", reason);
|
|
26010
26318
|
void this.restart();
|
|
26011
26319
|
};
|
|
26012
|
-
if (immediate)
|
|
26013
|
-
else this.debounce = setTimeout(
|
|
26320
|
+
if (immediate) run2();
|
|
26321
|
+
else this.debounce = setTimeout(run2, delayMs);
|
|
26014
26322
|
}
|
|
26015
26323
|
async restart() {
|
|
26016
26324
|
if (this.updateChildRunning) {
|
|
@@ -26602,13 +26910,13 @@ async function startDevSupervisor(opts) {
|
|
|
26602
26910
|
}
|
|
26603
26911
|
|
|
26604
26912
|
// src/production-update.ts
|
|
26605
|
-
import { execFile as
|
|
26606
|
-
import { existsSync as
|
|
26913
|
+
import { execFile as execFile3, spawn as spawn3 } from "node:child_process";
|
|
26914
|
+
import { existsSync as existsSync8 } from "node:fs";
|
|
26607
26915
|
import { dirname as dirname12, isAbsolute as isAbsolute5, join as join14 } from "node:path";
|
|
26608
26916
|
init_src();
|
|
26609
26917
|
var PRODUCTION_REEXEC_FLAG = "--_frizz-production-reexec";
|
|
26610
26918
|
var PRODUCTION_PRINT_LAUNCHER_FLAG = "--_frizz-print-launcher";
|
|
26611
|
-
function resolveNpmInvocation(env = process.env, execPath = process.execPath, exists =
|
|
26919
|
+
function resolveNpmInvocation(env = process.env, execPath = process.execPath, exists = existsSync8, platform = process.platform) {
|
|
26612
26920
|
const nodeDir = dirname12(execPath);
|
|
26613
26921
|
const candidates = [
|
|
26614
26922
|
// pnpm, yarn and bun set `npm_execpath` too, to their own entry file. Their directories hold no
|
|
@@ -26706,7 +27014,7 @@ function createNpmRegistryReleaseAdapter(options2 = {}) {
|
|
|
26706
27014
|
} catch (error) {
|
|
26707
27015
|
return reject(error);
|
|
26708
27016
|
}
|
|
26709
|
-
|
|
27017
|
+
execFile3(
|
|
26710
27018
|
npm.command,
|
|
26711
27019
|
[...npm.prefixArgs, "view", `${packageName}@latest`, "version", "--json"],
|
|
26712
27020
|
{ encoding: "utf8", windowsHide: true, timeout: viewTimeoutMs, killSignal: NPM_KILL_SIGNAL },
|
|
@@ -26774,15 +27082,15 @@ var npmRegistryReleaseAdapter = createNpmRegistryReleaseAdapter();
|
|
|
26774
27082
|
|
|
26775
27083
|
// src/server-release.ts
|
|
26776
27084
|
init_frizz_paths();
|
|
26777
|
-
import { execFile as
|
|
27085
|
+
import { execFile as execFile4 } from "node:child_process";
|
|
26778
27086
|
import { createHash as createHash4, randomUUID as randomUUID6 } from "node:crypto";
|
|
26779
27087
|
import {
|
|
26780
27088
|
closeSync as closeSync5,
|
|
26781
|
-
existsSync as
|
|
27089
|
+
existsSync as existsSync9,
|
|
26782
27090
|
fsyncSync as fsyncSync5,
|
|
26783
27091
|
mkdirSync as mkdirSync12,
|
|
26784
27092
|
openSync as openSync6,
|
|
26785
|
-
readFileSync as
|
|
27093
|
+
readFileSync as readFileSync12,
|
|
26786
27094
|
realpathSync as realpathSync6,
|
|
26787
27095
|
renameSync as renameSync6,
|
|
26788
27096
|
rmSync as rmSync9,
|
|
@@ -26808,7 +27116,7 @@ function assertSpec(spec, compatibility = CURRENT_COMPATIBILITY) {
|
|
|
26808
27116
|
throw new Error("this Frizz server requires a newer launcher or a data migration; restart with a compatible Frizz release");
|
|
26809
27117
|
}
|
|
26810
27118
|
function readJson(path) {
|
|
26811
|
-
return JSON.parse(
|
|
27119
|
+
return JSON.parse(readFileSync12(path, "utf8"));
|
|
26812
27120
|
}
|
|
26813
27121
|
function record(value) {
|
|
26814
27122
|
if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("invalid Frizz server manifest");
|
|
@@ -26873,12 +27181,12 @@ function resolveNpmCli(env = process.env, executable = process.execPath) {
|
|
|
26873
27181
|
} catch {
|
|
26874
27182
|
}
|
|
26875
27183
|
}
|
|
26876
|
-
const found = candidates.find((path) =>
|
|
27184
|
+
const found = candidates.find((path) => existsSync9(path) && statSync5(path).isFile());
|
|
26877
27185
|
if (!found) throw new Error("npm was not found beside Node or on PATH; install Node with npm to install Frizz server releases");
|
|
26878
27186
|
return resolve6(found);
|
|
26879
27187
|
}
|
|
26880
27188
|
function npmServerPackageInstaller(env = process.env) {
|
|
26881
|
-
const
|
|
27189
|
+
const run2 = (args2, cwd) => new Promise((resolveOutput, reject) => {
|
|
26882
27190
|
let cli;
|
|
26883
27191
|
try {
|
|
26884
27192
|
cli = resolveNpmCli(env);
|
|
@@ -26886,7 +27194,7 @@ function npmServerPackageInstaller(env = process.env) {
|
|
|
26886
27194
|
reject(error);
|
|
26887
27195
|
return;
|
|
26888
27196
|
}
|
|
26889
|
-
|
|
27197
|
+
execFile4(process.execPath, [cli, ...args2], {
|
|
26890
27198
|
env,
|
|
26891
27199
|
cwd,
|
|
26892
27200
|
encoding: "utf8",
|
|
@@ -26904,7 +27212,7 @@ function npmServerPackageInstaller(env = process.env) {
|
|
|
26904
27212
|
return {
|
|
26905
27213
|
async install(prefix, spec) {
|
|
26906
27214
|
assertSpec(spec);
|
|
26907
|
-
await
|
|
27215
|
+
await run2([
|
|
26908
27216
|
"install",
|
|
26909
27217
|
"--prefix",
|
|
26910
27218
|
prefix,
|
|
@@ -26922,7 +27230,7 @@ function npmServerPackageInstaller(env = process.env) {
|
|
|
26922
27230
|
},
|
|
26923
27231
|
async latestVersion(packageName) {
|
|
26924
27232
|
if (!PACKAGE.test(packageName)) throw new Error("invalid Frizz server package name");
|
|
26925
|
-
const parsed = JSON.parse(await
|
|
27233
|
+
const parsed = JSON.parse(await run2(["view", `${packageName}@latest`, "version", "--json"]));
|
|
26926
27234
|
const version = Array.isArray(parsed) && parsed.length === 1 ? parsed[0] : parsed;
|
|
26927
27235
|
if (typeof version !== "string" || !VERSION.test(version)) throw new Error("npm returned an invalid Frizz server version");
|
|
26928
27236
|
return version;
|
|
@@ -26975,7 +27283,7 @@ var ServerReleaseStore = class {
|
|
|
26975
27283
|
return join15(this.generations, id, "node_modules", this.spec.package);
|
|
26976
27284
|
}
|
|
26977
27285
|
readCompatibilityMarker() {
|
|
26978
|
-
if (!
|
|
27286
|
+
if (!existsSync9(this.compatibilityMarker)) return void 0;
|
|
26979
27287
|
try {
|
|
26980
27288
|
const marker = record(readJson(this.compatibilityMarker));
|
|
26981
27289
|
const compatibility = { protocol: marker.protocol, dataEpoch: marker.dataEpoch };
|
|
@@ -27012,7 +27320,7 @@ var ServerReleaseStore = class {
|
|
|
27012
27320
|
}
|
|
27013
27321
|
async load() {
|
|
27014
27322
|
this.assertMarkerIsSupported();
|
|
27015
|
-
if (!
|
|
27323
|
+
if (!existsSync9(this.selection)) return this.prepareInitial(this.spec.version);
|
|
27016
27324
|
const selected = record(readJson(this.selection));
|
|
27017
27325
|
const spec = selected;
|
|
27018
27326
|
assertSpecShape(spec);
|
|
@@ -27024,7 +27332,7 @@ var ServerReleaseStore = class {
|
|
|
27024
27332
|
if (spec.dataEpoch > this.compatibility.dataEpoch)
|
|
27025
27333
|
throw new Error("this Frizz server requires a newer launcher or a data migration; restart with a compatible Frizz release");
|
|
27026
27334
|
const root = this.packageRoot(selected.id);
|
|
27027
|
-
const generation = !
|
|
27335
|
+
const generation = !existsSync9(root) ? await this.prepare(spec.version) : validateServerGeneration(root, spec, selected.id, this.compatibility);
|
|
27028
27336
|
this.markCompatibilityBeforeLaunch();
|
|
27029
27337
|
return generation;
|
|
27030
27338
|
}
|
|
@@ -27059,7 +27367,7 @@ var ServerReleaseStore = class {
|
|
|
27059
27367
|
init_frizz_paths();
|
|
27060
27368
|
init_project_launch();
|
|
27061
27369
|
import { randomUUID as randomUUID7 } from "node:crypto";
|
|
27062
|
-
import { mkdirSync as mkdirSync13, readFileSync as
|
|
27370
|
+
import { mkdirSync as mkdirSync13, readFileSync as readFileSync13, realpathSync as realpathSync7 } from "node:fs";
|
|
27063
27371
|
import { join as join16 } from "node:path";
|
|
27064
27372
|
var STABLE_SERVER_OWNER_NAME = "frizz-server";
|
|
27065
27373
|
var STABLE_SERVER_OWNER_PROJECT_ID = "c1fd5810-0f8a-4c1d-91a0-6d7445d28e5a";
|
|
@@ -27082,7 +27390,7 @@ function matchingTarget(owner, target2) {
|
|
|
27082
27390
|
}
|
|
27083
27391
|
function readAddress(path) {
|
|
27084
27392
|
try {
|
|
27085
|
-
const value = JSON.parse(
|
|
27393
|
+
const value = JSON.parse(readFileSync13(path, "utf8"));
|
|
27086
27394
|
if (value.version !== 1 || typeof value.ownerToken !== "string" || !UUID_RE2.test(value.ownerToken) || typeof value.publisherToken !== "string" || !UUID_RE2.test(value.publisherToken) || typeof value.pid !== "number" || !Number.isInteger(value.pid) || value.pid <= 0 || typeof value.processStart !== "string" || value.processStart.length === 0 || typeof value.port !== "number" || !Number.isInteger(value.port) || value.port < 1 || value.port > 65535) return null;
|
|
27087
27395
|
return value;
|
|
27088
27396
|
} catch {
|
|
@@ -27203,7 +27511,7 @@ init_project_identity();
|
|
|
27203
27511
|
var PACKAGE_NAME = process.env.FRIZZ_REGISTRY_PACKAGE ?? "frizz";
|
|
27204
27512
|
function resolvePackageVersion() {
|
|
27205
27513
|
try {
|
|
27206
|
-
const manifest = JSON.parse(
|
|
27514
|
+
const manifest = JSON.parse(readFileSync14(join17(import.meta.dirname, "..", "package.json"), "utf8"));
|
|
27207
27515
|
if (typeof manifest.version === "string" && manifest.version.trim()) return manifest.version.trim();
|
|
27208
27516
|
} catch {
|
|
27209
27517
|
}
|
|
@@ -27270,6 +27578,7 @@ var paneHost = null;
|
|
|
27270
27578
|
var globalInstall = process.env[GLOBAL_INSTALL_ENV] === "1" || isGlobalInstall(import.meta.dirname);
|
|
27271
27579
|
var accessPane = null;
|
|
27272
27580
|
var activeAccessLink = null;
|
|
27581
|
+
var sessionNotice = null;
|
|
27273
27582
|
var serverOwner;
|
|
27274
27583
|
process.on("exit", () => {
|
|
27275
27584
|
if (serverOwner) releaseStableServerOwner(serverOwner);
|
|
@@ -27434,6 +27743,7 @@ async function openOrPrint(port, reused, path = "") {
|
|
|
27434
27743
|
const publicOrigin = reused ? void 0 : remote?.origin();
|
|
27435
27744
|
const warnings = [];
|
|
27436
27745
|
if (sandbox) warnings.push(`Sandbox: everything here is throwaway (${sandbox.home}) and is deleted when this terminal closes.`);
|
|
27746
|
+
if (sessionNotice && !reused) warnings.push(sessionNotice);
|
|
27437
27747
|
if (!readout) {
|
|
27438
27748
|
console.log(`${reused ? "reusing" : "started"} Frizz for ${workspace.root}`);
|
|
27439
27749
|
console.log(url);
|
|
@@ -27477,7 +27787,7 @@ async function runSupervisor(port, token, onPrepared = () => {
|
|
|
27477
27787
|
owner.token
|
|
27478
27788
|
);
|
|
27479
27789
|
const spec = serverReleaseSpec(
|
|
27480
|
-
JSON.parse(
|
|
27790
|
+
JSON.parse(readFileSync14(join17(import.meta.dirname, "..", "package.json"), "utf8")),
|
|
27481
27791
|
process.env.FRIZZ_SERVER_PACKAGE
|
|
27482
27792
|
);
|
|
27483
27793
|
const installer = npmServerPackageInstaller(env);
|
|
@@ -27502,17 +27812,19 @@ async function runSupervisor(port, token, onPrepared = () => {
|
|
|
27502
27812
|
void refreshUpdateAvailable();
|
|
27503
27813
|
const updateAvailablePoll = setInterval(() => void refreshUpdateAvailable(), 30 * 60 * 1e3);
|
|
27504
27814
|
updateAvailablePoll.unref();
|
|
27815
|
+
const sessions = loadSessionState(workspace.stateDir);
|
|
27816
|
+
const epochRecord = sessionEpochLogLine(sessions.advance, sessions.signedOut);
|
|
27817
|
+
if (epochRecord) logger.warn("launcher", epochRecord);
|
|
27818
|
+
sessionNotice = sessionEpochNotice(sessions.signedOut);
|
|
27505
27819
|
const supervisor = await startDevSupervisor({
|
|
27506
27820
|
port,
|
|
27507
27821
|
host: LOOPBACK_BIND_HOST,
|
|
27508
27822
|
allowedHosts: [],
|
|
27509
27823
|
// A spent code repaints the open QR pane as stale, so nobody photographs a dead link.
|
|
27510
27824
|
onCodeConsumed: () => accessPane?.markConsumed(),
|
|
27511
|
-
|
|
27512
|
-
|
|
27513
|
-
|
|
27514
|
-
sessionKey: loadOrCreateSessionKey(workspace.stateDir),
|
|
27515
|
-
sessionDirectory: fileSessionDirectory(workspace.stateDir),
|
|
27825
|
+
sessionKey: sessions.key,
|
|
27826
|
+
rotateSessionKey: sessions.rotateKey,
|
|
27827
|
+
sessionDirectory: sessions.directory,
|
|
27516
27828
|
cwd: workspace.root,
|
|
27517
27829
|
stateDir: workspace.stateDir,
|
|
27518
27830
|
launchTarget: target,
|
|
@@ -27578,9 +27890,10 @@ async function runSupervisor(port, token, onPrepared = () => {
|
|
|
27578
27890
|
port,
|
|
27579
27891
|
current: () => remote?.current() ?? null,
|
|
27580
27892
|
apply: (next, applyOptions) => remote.apply(next, applyOptions),
|
|
27581
|
-
|
|
27893
|
+
// The pane shows the GitHub device code itself: a console line would land under its alternate screen.
|
|
27894
|
+
claim: (name, signIn) => establishCloudConfig(name, port, void 0, void 0, githubDeviceFlow({ onPrompt: signIn.onDeviceCode, signal: signIn.signal })),
|
|
27582
27895
|
issueLink: () => supervisor.issueAccessLink(),
|
|
27583
|
-
probes: {
|
|
27896
|
+
probes: { cloudflared: probeCloudflared, tailscale: probeTailscale },
|
|
27584
27897
|
onChanged: (config) => logger.info("remote", config ? `reached at https://${config.hostname}` : "loopback only"),
|
|
27585
27898
|
sandbox: sandbox !== null
|
|
27586
27899
|
});
|