frizz 0.7.7 → 0.7.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 +11 -46
- package/dist/dev-child.js +201 -63
- package/dist/frizz.js +918 -398
- package/package.json +1 -1
- package/runtime/cc-worker/DECISIONS.md +2 -1
- package/runtime/cc-worker/bin/frizz-mcp.mjs +3 -2
- package/web-dist/assets/{TerminalPane-Cb8jOFP9.js → TerminalPane-ubcEP7X3.js} +1 -1
- package/web-dist/assets/{abnfDiagram-VRR7QNED-Dtbg2eZ0.js → abnfDiagram-VRR7QNED-0brfnieb.js} +1 -1
- package/web-dist/assets/architecture-TIHT7OUA-DksosNDW.js +1 -0
- package/web-dist/assets/{architectureDiagram-ZJ3FMSHR-DzQjWW7u.js → architectureDiagram-ZJ3FMSHR-Dji-XBdF.js} +1 -1
- package/web-dist/assets/{blockDiagram-677ZJIJ3-BJnpkLMt.js → blockDiagram-677ZJIJ3-BQMyz3Q9.js} +1 -1
- package/web-dist/assets/{c4Diagram-LMCZKHZV-COdatsYu.js → c4Diagram-LMCZKHZV-BMNws0Oi.js} +1 -1
- package/web-dist/assets/channel-DqoPvfQz.js +1 -0
- package/web-dist/assets/{chunk-32BRIVSS-CORMw4Vm.js → chunk-32BRIVSS-CVv1ae7n.js} +1 -1
- package/web-dist/assets/{chunk-52WLFC77-C7lEadTg.js → chunk-52WLFC77-BYhPlKCT.js} +1 -1
- package/web-dist/assets/{chunk-C7G6YPKG-DZy4F97l.js → chunk-C7G6YPKG-Cn4HhNwb.js} +1 -1
- package/web-dist/assets/{chunk-EX3LRPZG-HynlfV9p.js → chunk-EX3LRPZG-P3ZYFC1W.js} +1 -1
- package/web-dist/assets/{chunk-FWX5IMBZ-BbwKNhKm.js → chunk-FWX5IMBZ-Ba3NpEQa.js} +2 -2
- package/web-dist/assets/{chunk-HOUHSVGY-lIhOZSmL.js → chunk-HOUHSVGY-BCWyMhwJ.js} +1 -1
- package/web-dist/assets/{chunk-ICXQ74PX-WGxaMO8c.js → chunk-ICXQ74PX-DWAAqHL7.js} +1 -1
- package/web-dist/assets/{chunk-MOJQB5TN-B-cx0f3u.js → chunk-MOJQB5TN-pc5VJ4Nq.js} +1 -1
- package/web-dist/assets/{chunk-OGEWGWER-DJceVwZH.js → chunk-OGEWGWER-DUxmbiuX.js} +1 -1
- package/web-dist/assets/{chunk-PUDLZKDR-CGwHZns8.js → chunk-PUDLZKDR-DRdZWzxX.js} +1 -1
- package/web-dist/assets/{chunk-Q4XR5HBZ-PTSWtmk1.js → chunk-Q4XR5HBZ-Bafzu7s-.js} +1 -1
- package/web-dist/assets/{chunk-V7JOEXUC-VNxHZPcf.js → chunk-V7JOEXUC-Bj0A8ftW.js} +1 -1
- package/web-dist/assets/{chunk-VAUOI2AC-ZXYCz_Aw.js → chunk-VAUOI2AC-DBgVQKIO.js} +1 -1
- package/web-dist/assets/{chunk-VR4S4FIN-9WCtmFJp.js → chunk-VR4S4FIN-DU-1KtuM.js} +1 -1
- package/web-dist/assets/{chunk-WYO6CB5R-V4EBjpmt.js → chunk-WYO6CB5R-DzVb3-Rj.js} +1 -1
- package/web-dist/assets/{chunk-ZGVPDNZ5-CPW0Z0ae.js → chunk-ZGVPDNZ5-BPKQvduj.js} +1 -1
- package/web-dist/assets/classDiagram-OUVF2IWQ-DwUs74RB.js +1 -0
- package/web-dist/assets/classDiagram-v2-EOCWNBFH-DwUs74RB.js +1 -0
- package/web-dist/assets/{cynefin-VYW2F7L2-E7PklfJD.js → cynefin-VYW2F7L2-lo8PqVCk.js} +1 -1
- package/web-dist/assets/{cynefinDiagram-TSTJHNR4-JIXAPjDf.js → cynefinDiagram-TSTJHNR4-BtQLaDV0.js} +1 -1
- package/web-dist/assets/{dagre-VKFMJZFB-DjXrdcNx.js → dagre-VKFMJZFB-BJ2-f-A0.js} +1 -1
- package/web-dist/assets/{diagram-FQU43EPY--i9i0qhm.js → diagram-FQU43EPY-CLM-8r74.js} +1 -1
- package/web-dist/assets/{diagram-G47NLZAW-DxgcENZ_.js → diagram-G47NLZAW-CfuoJGpN.js} +1 -1
- package/web-dist/assets/{diagram-NH7WQ7WH-CUy9Nr90.js → diagram-NH7WQ7WH-DfxxRwH8.js} +1 -1
- package/web-dist/assets/{diagram-OA4YK3LP-DbTbUaCb.js → diagram-OA4YK3LP-D7knxZee.js} +1 -1
- package/web-dist/assets/{diagram-WEI45ONY-2gk6xhOq.js → diagram-WEI45ONY-C2WfMXWl.js} +1 -1
- package/web-dist/assets/{ebnfDiagram-CCIWWBDH-uE2iDETg.js → ebnfDiagram-CCIWWBDH-DRSZ24tF.js} +1 -1
- package/web-dist/assets/{erDiagram-Q63AITRT-dztd4MUw.js → erDiagram-Q63AITRT-DXe0OrbY.js} +1 -1
- package/web-dist/assets/eventmodeling-45OFAUF4-CUIjQ7kE.js +1 -0
- package/web-dist/assets/flowDiagram-23GEKE2U-Dka-3F3n.js +1 -0
- package/web-dist/assets/{ganttDiagram-NO4QXBWP-DAds6Aht.js → ganttDiagram-NO4QXBWP-wGqOgjvs.js} +1 -1
- package/web-dist/assets/{gitGraph-TEB2WS4Q-Ci0XVvIa.js → gitGraph-TEB2WS4Q-BygokG6y.js} +1 -1
- package/web-dist/assets/{gitGraphDiagram-IHSO6WYX-C6FULyt8.js → gitGraphDiagram-IHSO6WYX-CV1933_M.js} +1 -1
- package/web-dist/assets/index-CmrDpsD8.css +1 -0
- package/web-dist/assets/{index-CNy5uOpI.js → index-DB1d3eMJ.js} +75 -75
- package/web-dist/assets/{info-DKCQHKI2-9N77dOiw.js → info-DKCQHKI2-DLavbzui.js} +1 -1
- package/web-dist/assets/{infoDiagram-FWYZ7A6U-BymP3PgG.js → infoDiagram-FWYZ7A6U-Biouxd9G.js} +1 -1
- package/web-dist/assets/{ishikawaDiagram-FXEZZL3T-CU_kQBz1.js → ishikawaDiagram-FXEZZL3T-DJxwwS3R.js} +1 -1
- package/web-dist/assets/{journeyDiagram-5HDEW3XC-DZN15HSd.js → journeyDiagram-5HDEW3XC-Clx_WkdW.js} +1 -1
- package/web-dist/assets/{kanban-definition-HUTT4EX6-J4wR5g_3.js → kanban-definition-HUTT4EX6-Bf7bX_96.js} +1 -1
- package/web-dist/assets/{line-SRzGpiDr.js → line-CV2v49IN.js} +1 -1
- package/web-dist/assets/{mermaid-parser.core-CGBscNGo.js → mermaid-parser.core-BPsFhEi6.js} +3 -3
- package/web-dist/assets/{mermaid.core-xNdwXb1r.js → mermaid.core-0EWDNL_R.js} +3 -3
- package/web-dist/assets/{mindmap-definition-LN4V7U3C-gR2dbzrj.js → mindmap-definition-LN4V7U3C-C1y4yy3Z.js} +1 -1
- package/web-dist/assets/{packet-7NZHBO7P-DrR4W6I3.js → packet-7NZHBO7P-CpcpxruB.js} +1 -1
- package/web-dist/assets/{pegDiagram-2B236MQR-DN7R-_t4.js → pegDiagram-2B236MQR-CVlVP4cH.js} +1 -1
- package/web-dist/assets/{pie-RZYD4A2V-C8VjQAFZ.js → pie-RZYD4A2V-BcVPOhTZ.js} +1 -1
- package/web-dist/assets/{pieDiagram-ENE6RG2P-5w64zdD1.js → pieDiagram-ENE6RG2P-DZv4Tfae.js} +1 -1
- package/web-dist/assets/{quadrantDiagram-ABIIQ3AL-B4rbezq_.js → quadrantDiagram-ABIIQ3AL-DOzS_U_a.js} +1 -1
- package/web-dist/assets/{radar-I7S5WNFK-CTbbPB8m.js → radar-I7S5WNFK-8YADxkdT.js} +1 -1
- package/web-dist/assets/{railroad-3IZDKUUU-CY1er15b.js → railroad-3IZDKUUU-DzndPrRy.js} +1 -1
- package/web-dist/assets/railroad-abnf-AHOZXSZD-CCnWa6K9.js +1 -0
- package/web-dist/assets/railroad-ebnf-EBAXGLYW-jtVdzv7e.js +1 -0
- package/web-dist/assets/railroad-peg-LSFZ7HO6-B8D6TyEe.js +1 -0
- package/web-dist/assets/{railroadDiagram-RFXS5EU6-Bx2HJ3yT.js → railroadDiagram-RFXS5EU6-CPJAH-r7.js} +1 -1
- package/web-dist/assets/{requirementDiagram-TGXJPOKE-BLK-Y2rx.js → requirementDiagram-TGXJPOKE-BVFJZfwl.js} +1 -1
- package/web-dist/assets/{sankeyDiagram-HTMAVEWB-Dk-8Miv-.js → sankeyDiagram-HTMAVEWB-CUhvFPmE.js} +1 -1
- package/web-dist/assets/{sequenceDiagram-DBY2YBRQ-DL2alfR-.js → sequenceDiagram-DBY2YBRQ-Cy9CGhNq.js} +1 -1
- package/web-dist/assets/{stateDiagram-2N3HPSRC-Bka44wf1.js → stateDiagram-2N3HPSRC-CqOnmPLp.js} +1 -1
- package/web-dist/assets/stateDiagram-v2-6OUMAXLB-DWKW-4qu.js +1 -0
- package/web-dist/assets/{swimlanes-5IMT3BWC-e0GbMwIr.js → swimlanes-5IMT3BWC-c_zBqkb9.js} +1 -1
- package/web-dist/assets/swimlanesDiagram-G3AALYLV-DZKoB7ae.js +8 -0
- package/web-dist/assets/{timeline-definition-FHXFAJF6-CT3GFq89.js → timeline-definition-FHXFAJF6-y_QM62SK.js} +1 -1
- package/web-dist/assets/{treeView-QDETBFTQ-BJauJ0bm.js → treeView-QDETBFTQ-T3y_1KXV.js} +1 -1
- package/web-dist/assets/{treemap-6X3UGDF4-DCX7A-nx.js → treemap-6X3UGDF4-CzvqucqY.js} +1 -1
- package/web-dist/assets/{vennDiagram-L72KCM5P-D4dm2iTR.js → vennDiagram-L72KCM5P-6-HNOpN0.js} +1 -1
- package/web-dist/assets/{wardley-OPB4EBWU-DklTDSId.js → wardley-OPB4EBWU-BrsaqsLL.js} +1 -1
- package/web-dist/assets/{wardleyDiagram-EHGQE667-CikP5hBd.js → wardleyDiagram-EHGQE667-Cr_rCzwd.js} +1 -1
- package/web-dist/assets/{xychartDiagram-FW5EYKEG-FXuOAFpz.js → xychartDiagram-FW5EYKEG-BnX6ccx4.js} +1 -1
- package/web-dist/index.html +2 -2
- package/web-dist/assets/architecture-TIHT7OUA-CTxVpl9_.js +0 -1
- package/web-dist/assets/channel-COS0Ygbp.js +0 -1
- package/web-dist/assets/classDiagram-OUVF2IWQ-Df5TLnrN.js +0 -1
- package/web-dist/assets/classDiagram-v2-EOCWNBFH-Df5TLnrN.js +0 -1
- package/web-dist/assets/eventmodeling-45OFAUF4-DPVPOEaJ.js +0 -1
- package/web-dist/assets/flowDiagram-23GEKE2U-BMIMgVYd.js +0 -1
- package/web-dist/assets/index-D6LsViUY.css +0 -1
- package/web-dist/assets/railroad-abnf-AHOZXSZD-DfEpkrtJ.js +0 -1
- package/web-dist/assets/railroad-ebnf-EBAXGLYW-Csunejgu.js +0 -1
- package/web-dist/assets/railroad-peg-LSFZ7HO6-BgRslbM_.js +0 -1
- package/web-dist/assets/stateDiagram-v2-6OUMAXLB-6KcScVPX.js +0 -1
- package/web-dist/assets/swimlanesDiagram-G3AALYLV-jaJhlDbE.js +0 -8
package/dist/frizz.js
CHANGED
|
@@ -140,52 +140,9 @@ function isTrustedLocalHttpRequest(headers, expectedPort, allowMissingOrigin = f
|
|
|
140
140
|
function allowedLocalCorsOrigin(origin, expectedPort, policy) {
|
|
141
141
|
return parseLocalHttpOrigin(origin, expectedPort, policy) ? origin : void 0;
|
|
142
142
|
}
|
|
143
|
-
function normalizeBindHost(value) {
|
|
144
|
-
const trimmed = value.trim();
|
|
145
|
-
if (!trimmed) throw new Error("--host requires an address");
|
|
146
|
-
const bare = trimmed.startsWith("[") && trimmed.endsWith("]") ? trimmed.slice(1, -1) : trimmed;
|
|
147
|
-
const lower = bare.toLowerCase();
|
|
148
|
-
if (lower === "localhost") return LOOPBACK_BIND_HOST;
|
|
149
|
-
if (isIP(bare) === 0) {
|
|
150
|
-
throw new Error(`invalid --host address: ${value} (use an IP address such as 0.0.0.0, :: or 127.0.0.1)`);
|
|
151
|
-
}
|
|
152
|
-
return bare;
|
|
153
|
-
}
|
|
154
143
|
function bindHostIsExposed(host) {
|
|
155
144
|
return !LOOPBACK_BIND_HOSTS.has(host.trim().toLowerCase());
|
|
156
145
|
}
|
|
157
|
-
function normalizePublicOrigin(value) {
|
|
158
|
-
const trimmed = value.trim();
|
|
159
|
-
const invalid = (why) => new Error(`invalid --public-origin: ${value} (${why})`);
|
|
160
|
-
if (!trimmed) throw new Error("--public-origin requires a URL");
|
|
161
|
-
let url;
|
|
162
|
-
try {
|
|
163
|
-
url = new URL(trimmed);
|
|
164
|
-
} catch {
|
|
165
|
-
throw invalid("use a full URL such as https://frizz.example.com");
|
|
166
|
-
}
|
|
167
|
-
if (url.protocol !== "https:" && url.protocol !== "http:") throw invalid("only http and https are supported");
|
|
168
|
-
if (url.username || url.password) throw invalid("an origin carries no credentials");
|
|
169
|
-
if (url.pathname !== "/" || url.search || url.hash) throw invalid("an origin is a scheme and host only, with no path");
|
|
170
|
-
if (!url.hostname) throw invalid("no hostname");
|
|
171
|
-
return url.origin;
|
|
172
|
-
}
|
|
173
|
-
function machineHostNames(hostname) {
|
|
174
|
-
const full = hostname.trim().toLowerCase().replace(/\.$/, "");
|
|
175
|
-
if (!full || full === "localhost" || isIP(full) !== 0) return [];
|
|
176
|
-
const short = full.split(".")[0];
|
|
177
|
-
return [.../* @__PURE__ */ new Set([full, short, `${short}.local`])].filter((name) => name !== "localhost" && name !== "localhost.local");
|
|
178
|
-
}
|
|
179
|
-
function normalizeAllowedHosts(values) {
|
|
180
|
-
const seen = /* @__PURE__ */ new Set();
|
|
181
|
-
for (const value of values) {
|
|
182
|
-
for (const entry of value.split(",")) {
|
|
183
|
-
const host = entry.trim().toLowerCase();
|
|
184
|
-
if (host) seen.add(host);
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
return [...seen];
|
|
188
|
-
}
|
|
189
146
|
function rejectWebSocketUpgrade(socket, status = 403, reason = "Forbidden") {
|
|
190
147
|
const body = `${reason}
|
|
191
148
|
`;
|
|
@@ -206,7 +163,7 @@ function rejectWebSocketUpgrade(socket, status = 403, reason = "Forbidden") {
|
|
|
206
163
|
}
|
|
207
164
|
}
|
|
208
165
|
}
|
|
209
|
-
var LOCAL_HOSTNAMES, FORWARDED_HEADERS, LOOPBACK_BIND_HOST,
|
|
166
|
+
var LOCAL_HOSTNAMES, FORWARDED_HEADERS, LOOPBACK_BIND_HOST, LOOPBACK_BIND_HOSTS;
|
|
210
167
|
var init_local_origin = __esm({
|
|
211
168
|
"packages/server/src/local-origin.ts"() {
|
|
212
169
|
"use strict";
|
|
@@ -219,7 +176,6 @@ var init_local_origin = __esm({
|
|
|
219
176
|
"x-forwarded-proto"
|
|
220
177
|
];
|
|
221
178
|
LOOPBACK_BIND_HOST = "127.0.0.1";
|
|
222
|
-
ALL_INTERFACES_BIND_HOST = "0.0.0.0";
|
|
223
179
|
LOOPBACK_BIND_HOSTS = /* @__PURE__ */ new Set(["127.0.0.1", "::1", "localhost", "[::1]"]);
|
|
224
180
|
}
|
|
225
181
|
});
|
|
@@ -17842,9 +17798,9 @@ var init_wrapper = __esm({
|
|
|
17842
17798
|
|
|
17843
17799
|
// packages/server/src/frizz-paths.ts
|
|
17844
17800
|
import { existsSync as existsSync2 } from "node:fs";
|
|
17845
|
-
import { homedir as
|
|
17801
|
+
import { homedir as homedir5, tmpdir } from "node:os";
|
|
17846
17802
|
import { dirname as dirname4, join as join5 } from "node:path";
|
|
17847
|
-
function legacyFrizzRoot(home =
|
|
17803
|
+
function legacyFrizzRoot(home = homedir5()) {
|
|
17848
17804
|
return join5(home, LEGACY_DIR_NAME);
|
|
17849
17805
|
}
|
|
17850
17806
|
function xdg(env, name) {
|
|
@@ -17886,7 +17842,7 @@ function xdgRoots(env, home) {
|
|
|
17886
17842
|
function frizzPaths(options2 = {}) {
|
|
17887
17843
|
const env = options2.env ?? process.env;
|
|
17888
17844
|
const platform = options2.platform ?? process.platform;
|
|
17889
|
-
const home = options2.home ??
|
|
17845
|
+
const home = options2.home ?? homedir5();
|
|
17890
17846
|
const exists = options2.exists ?? existsSync2;
|
|
17891
17847
|
const legacyRoot = legacyFrizzRoot(home);
|
|
17892
17848
|
if (exists(legacyRoot)) {
|
|
@@ -17925,22 +17881,22 @@ import {
|
|
|
17925
17881
|
mkdirSync as mkdirSync4,
|
|
17926
17882
|
openSync,
|
|
17927
17883
|
readdirSync,
|
|
17928
|
-
rmSync,
|
|
17884
|
+
rmSync as rmSync2,
|
|
17929
17885
|
statSync as statSync2,
|
|
17930
17886
|
symlinkSync,
|
|
17931
17887
|
writeFileSync as writeFileSync4,
|
|
17932
17888
|
writeSync
|
|
17933
17889
|
} from "node:fs";
|
|
17934
|
-
import { homedir as
|
|
17890
|
+
import { homedir as homedir6 } from "node:os";
|
|
17935
17891
|
import { join as join6 } from "node:path";
|
|
17936
|
-
function defaultLogRoot(stateDir, home =
|
|
17892
|
+
function defaultLogRoot(stateDir, home = homedir6()) {
|
|
17937
17893
|
return stateDir ? join6(stateDir, "logs") : join6(frizzPaths({ home }).state, "logs");
|
|
17938
17894
|
}
|
|
17939
17895
|
function runStamp(at) {
|
|
17940
17896
|
const pad = (value) => String(value).padStart(2, "0");
|
|
17941
17897
|
return `${at.getFullYear()}-${pad(at.getMonth() + 1)}-${pad(at.getDate())}T${pad(at.getHours())}-${pad(at.getMinutes())}-${pad(at.getSeconds())}`;
|
|
17942
17898
|
}
|
|
17943
|
-
function runLogPath(stateDir, at = /* @__PURE__ */ new Date(), pid = process.pid, home =
|
|
17899
|
+
function runLogPath(stateDir, at = /* @__PURE__ */ new Date(), pid = process.pid, home = homedir6(), env = process.env) {
|
|
17944
17900
|
const name = `frizz-${runStamp(at)}-${pid}.log`;
|
|
17945
17901
|
const override = env[LOG_PATH_ENV]?.trim();
|
|
17946
17902
|
if (override) return override.endsWith(".log") ? override : join6(override, name);
|
|
@@ -17952,7 +17908,7 @@ function latestLogPath(dir) {
|
|
|
17952
17908
|
function linkLatest(dir, target2) {
|
|
17953
17909
|
const link = latestLogPath(dir);
|
|
17954
17910
|
try {
|
|
17955
|
-
|
|
17911
|
+
rmSync2(link, { force: true });
|
|
17956
17912
|
symlinkSync(target2, link);
|
|
17957
17913
|
} catch {
|
|
17958
17914
|
try {
|
|
@@ -17980,7 +17936,7 @@ function pruneRunLogs(dir, keep = RETAINED_RUNS, days = RETAINED_DAYS, now = Dat
|
|
|
17980
17936
|
const stale = dated.filter((entry, index) => index >= keep || entry.at < cutoff);
|
|
17981
17937
|
for (const entry of stale) {
|
|
17982
17938
|
try {
|
|
17983
|
-
|
|
17939
|
+
rmSync2(join6(dir, entry.name), { force: true });
|
|
17984
17940
|
} catch {
|
|
17985
17941
|
}
|
|
17986
17942
|
}
|
|
@@ -18252,11 +18208,11 @@ import {
|
|
|
18252
18208
|
readFileSync as readFileSync6,
|
|
18253
18209
|
realpathSync as realpathSync2,
|
|
18254
18210
|
renameSync,
|
|
18255
|
-
rmSync as
|
|
18211
|
+
rmSync as rmSync3,
|
|
18256
18212
|
statSync as statSync3,
|
|
18257
18213
|
writeFileSync as writeFileSync5
|
|
18258
18214
|
} from "node:fs";
|
|
18259
|
-
import { homedir as
|
|
18215
|
+
import { homedir as homedir7 } from "node:os";
|
|
18260
18216
|
import { dirname as dirname5, join as join8, resolve } from "node:path";
|
|
18261
18217
|
import { setTimeout as delay } from "node:timers/promises";
|
|
18262
18218
|
function errorCode2(error) {
|
|
@@ -18272,7 +18228,7 @@ function canonicalHome(home) {
|
|
|
18272
18228
|
return resolve(home);
|
|
18273
18229
|
}
|
|
18274
18230
|
}
|
|
18275
|
-
function globalLaunchLockPath(home =
|
|
18231
|
+
function globalLaunchLockPath(home = homedir7()) {
|
|
18276
18232
|
return join8(frizzPaths({ home: canonicalHome(home) }).state, GLOBAL_LOCK_NAME);
|
|
18277
18233
|
}
|
|
18278
18234
|
function namedLaunchLockPath(home, name) {
|
|
@@ -18356,7 +18312,7 @@ function quarantineLock(lockPath) {
|
|
|
18356
18312
|
return false;
|
|
18357
18313
|
}
|
|
18358
18314
|
try {
|
|
18359
|
-
|
|
18315
|
+
rmSync3(quarantine2, { recursive: true, force: true });
|
|
18360
18316
|
} catch {
|
|
18361
18317
|
}
|
|
18362
18318
|
return true;
|
|
@@ -18402,7 +18358,7 @@ function tryAcquireLock(lockPath, adapter) {
|
|
|
18402
18358
|
const quarantine2 = `${lockPath}.release-${process.pid}-${owner.token}`;
|
|
18403
18359
|
try {
|
|
18404
18360
|
renameSync(lockPath, quarantine2);
|
|
18405
|
-
|
|
18361
|
+
rmSync3(quarantine2, { recursive: true, force: true });
|
|
18406
18362
|
} catch {
|
|
18407
18363
|
}
|
|
18408
18364
|
};
|
|
@@ -18410,10 +18366,10 @@ function tryAcquireLock(lockPath, adapter) {
|
|
|
18410
18366
|
function tryAcquireGlobalLaunchLock(home, adapter) {
|
|
18411
18367
|
return tryAcquireLock(globalLaunchLockPath(home), adapter);
|
|
18412
18368
|
}
|
|
18413
|
-
function portReservationPath(port, home =
|
|
18369
|
+
function portReservationPath(port, home = homedir7()) {
|
|
18414
18370
|
return join8(frizzPaths({ home: canonicalHome(home) }).state, PORT_LOCK_DIR, `${port}.lock`);
|
|
18415
18371
|
}
|
|
18416
|
-
function tryReservePort(port, home =
|
|
18372
|
+
function tryReservePort(port, home = homedir7(), adapter = defaultProcessPlatformAdapter) {
|
|
18417
18373
|
const lockPath = portReservationPath(port, home);
|
|
18418
18374
|
const release = tryAcquireLock(lockPath, adapter);
|
|
18419
18375
|
if (release) return release;
|
|
@@ -18426,7 +18382,7 @@ function lockTimeoutError(observation) {
|
|
|
18426
18382
|
observation.ownerPid === void 0 ? "another Frizz launch still owns the global launch lock" : `another Frizz launch (pid ${observation.ownerPid}) still owns the global launch lock`
|
|
18427
18383
|
);
|
|
18428
18384
|
}
|
|
18429
|
-
async function acquireGlobalLaunchLock(home =
|
|
18385
|
+
async function acquireGlobalLaunchLock(home = homedir7(), timeoutMs = 1e4, adapter = defaultProcessPlatformAdapter) {
|
|
18430
18386
|
const deadline = Date.now() + Math.max(0, timeoutMs);
|
|
18431
18387
|
let observation = { stale: false };
|
|
18432
18388
|
for (; ; ) {
|
|
@@ -18594,7 +18550,7 @@ function resolvedIdentity(worktree) {
|
|
|
18594
18550
|
}
|
|
18595
18551
|
return { ...worktree, id: worktreeId };
|
|
18596
18552
|
}
|
|
18597
|
-
function resolveGitProjectIdentity(dir, home =
|
|
18553
|
+
function resolveGitProjectIdentity(dir, home = homedir7()) {
|
|
18598
18554
|
let worktree = resolveGitWorktree(dir);
|
|
18599
18555
|
const existing = resolvedIdentity(worktree);
|
|
18600
18556
|
if (existing) return existing;
|
|
@@ -18668,7 +18624,7 @@ import {
|
|
|
18668
18624
|
openSync as openSync3,
|
|
18669
18625
|
readFileSync as readFileSync7,
|
|
18670
18626
|
renameSync as renameSync2,
|
|
18671
|
-
rmSync as
|
|
18627
|
+
rmSync as rmSync4,
|
|
18672
18628
|
statSync as statSync4,
|
|
18673
18629
|
writeFileSync as writeFileSync6
|
|
18674
18630
|
} from "node:fs";
|
|
@@ -18715,7 +18671,7 @@ function atomicJson(path, value) {
|
|
|
18715
18671
|
}
|
|
18716
18672
|
}
|
|
18717
18673
|
try {
|
|
18718
|
-
|
|
18674
|
+
rmSync4(temp, { force: true });
|
|
18719
18675
|
} catch {
|
|
18720
18676
|
}
|
|
18721
18677
|
throw error;
|
|
@@ -18729,7 +18685,7 @@ function quarantine(path, suffix) {
|
|
|
18729
18685
|
return false;
|
|
18730
18686
|
}
|
|
18731
18687
|
try {
|
|
18732
|
-
|
|
18688
|
+
rmSync4(moved, { recursive: true, force: true });
|
|
18733
18689
|
} catch {
|
|
18734
18690
|
}
|
|
18735
18691
|
return true;
|
|
@@ -19116,7 +19072,7 @@ var init_project_launch = __esm({
|
|
|
19116
19072
|
});
|
|
19117
19073
|
|
|
19118
19074
|
// packages/server/src/boot-progress.ts
|
|
19119
|
-
import { mkdirSync as mkdirSync7, readFileSync as readFileSync8, renameSync as renameSync3, rmSync as
|
|
19075
|
+
import { mkdirSync as mkdirSync7, readFileSync as readFileSync8, renameSync as renameSync3, rmSync as rmSync5, writeFileSync as writeFileSync7 } from "node:fs";
|
|
19120
19076
|
import { basename as basename3, dirname as dirname7, join as join10 } from "node:path";
|
|
19121
19077
|
function bootProgressPath(stateDir) {
|
|
19122
19078
|
return join10(stateDir, BOOT_PROGRESS_NAME);
|
|
@@ -19140,8 +19096,8 @@ var init_boot_progress = __esm({
|
|
|
19140
19096
|
|
|
19141
19097
|
// packages/server/src/project-root.ts
|
|
19142
19098
|
import { createHash as createHash3, randomUUID as randomUUID4 } from "node:crypto";
|
|
19143
|
-
import { closeSync as closeSync3, existsSync as existsSync3, fsyncSync as fsyncSync3, mkdirSync as mkdirSync8, openSync as openSync4, readFileSync as readFileSync9, realpathSync as realpathSync3, renameSync as renameSync4, rmSync as
|
|
19144
|
-
import { homedir as
|
|
19099
|
+
import { closeSync as closeSync3, existsSync as existsSync3, fsyncSync as fsyncSync3, mkdirSync as mkdirSync8, openSync as openSync4, readFileSync as readFileSync9, realpathSync as realpathSync3, renameSync as renameSync4, rmSync as rmSync6, writeFileSync as writeFileSync8 } from "node:fs";
|
|
19100
|
+
import { homedir as homedir8 } from "node:os";
|
|
19145
19101
|
import { dirname as dirname8, join as join11, parse, resolve as resolve2 } from "node:path";
|
|
19146
19102
|
function projectIdPath(root) {
|
|
19147
19103
|
return join11(root, FRIZZ_DIR, ID_FILE);
|
|
@@ -19188,7 +19144,7 @@ function writeProjectIdFile(root, id) {
|
|
|
19188
19144
|
}
|
|
19189
19145
|
}
|
|
19190
19146
|
try {
|
|
19191
|
-
|
|
19147
|
+
rmSync6(temp, { force: true });
|
|
19192
19148
|
} catch {
|
|
19193
19149
|
}
|
|
19194
19150
|
throw error;
|
|
@@ -19198,7 +19154,7 @@ function writeProjectIdFile(root, id) {
|
|
|
19198
19154
|
function projectRootLockName(root) {
|
|
19199
19155
|
return `identity-path-${createHash3("sha256").update(root).digest("hex")}.lock`;
|
|
19200
19156
|
}
|
|
19201
|
-
function ensureProjectIdFile(root, home =
|
|
19157
|
+
function ensureProjectIdFile(root, home = homedir8(), seed) {
|
|
19202
19158
|
const existing = readProjectIdFile(root);
|
|
19203
19159
|
if (existing) return existing;
|
|
19204
19160
|
const release = acquireNamedLaunchLockSync(home, projectRootLockName(root));
|
|
@@ -19220,7 +19176,7 @@ function isNotAGitWorktree(error) {
|
|
|
19220
19176
|
function hasAny(dir, names) {
|
|
19221
19177
|
return names.some((name) => existsSync3(join11(dir, name)));
|
|
19222
19178
|
}
|
|
19223
|
-
function isHomeDirectory(dir, home =
|
|
19179
|
+
function isHomeDirectory(dir, home = homedir8()) {
|
|
19224
19180
|
const canonical = (value) => {
|
|
19225
19181
|
try {
|
|
19226
19182
|
return realpathSync3(resolve2(value));
|
|
@@ -19249,7 +19205,7 @@ function isExistingProjectRoot(dir) {
|
|
|
19249
19205
|
function hasProjectMarker(dir) {
|
|
19250
19206
|
return hasAny(dir, REPO_MARKERS) || hasAny(dir, PROJECT_MARKERS);
|
|
19251
19207
|
}
|
|
19252
|
-
function discoverProjectRoot(cwd = process.cwd(), home =
|
|
19208
|
+
function discoverProjectRoot(cwd = process.cwd(), home = homedir8()) {
|
|
19253
19209
|
let dir;
|
|
19254
19210
|
try {
|
|
19255
19211
|
dir = resolve2(cwd);
|
|
@@ -19293,13 +19249,13 @@ var init_project_root = __esm({
|
|
|
19293
19249
|
});
|
|
19294
19250
|
|
|
19295
19251
|
// packages/server/src/project-registry.ts
|
|
19296
|
-
import { closeSync as closeSync4, existsSync as existsSync4, fsyncSync as fsyncSync4, mkdirSync as mkdirSync9, openSync as openSync5, readFileSync as readFileSync10, readdirSync as readdirSync2, realpathSync as realpathSync4, renameSync as renameSync5, rmSync as
|
|
19297
|
-
import { homedir as
|
|
19252
|
+
import { closeSync as closeSync4, existsSync as existsSync4, fsyncSync as fsyncSync4, mkdirSync as mkdirSync9, openSync as openSync5, readFileSync as readFileSync10, readdirSync as readdirSync2, realpathSync as realpathSync4, renameSync as renameSync5, rmSync as rmSync7, writeFileSync as writeFileSync9 } from "node:fs";
|
|
19253
|
+
import { homedir as homedir9 } from "node:os";
|
|
19298
19254
|
import { basename as basename4, dirname as dirname9, join as join12 } from "node:path";
|
|
19299
|
-
function registryPath(home =
|
|
19255
|
+
function registryPath(home = homedir9()) {
|
|
19300
19256
|
return join12(frizzPaths({ home }).data, "registry.json");
|
|
19301
19257
|
}
|
|
19302
|
-
function readRegistry(home =
|
|
19258
|
+
function readRegistry(home = homedir9()) {
|
|
19303
19259
|
let raw;
|
|
19304
19260
|
try {
|
|
19305
19261
|
raw = readFileSync10(registryPath(home), "utf8");
|
|
@@ -19319,7 +19275,7 @@ function readRegistry(home = homedir8()) {
|
|
|
19319
19275
|
return { ...EMPTY, projects: [] };
|
|
19320
19276
|
}
|
|
19321
19277
|
}
|
|
19322
|
-
function writeRegistry(registry, home =
|
|
19278
|
+
function writeRegistry(registry, home = homedir9()) {
|
|
19323
19279
|
const path = registryPath(home);
|
|
19324
19280
|
mkdirSync9(dirname9(path), { recursive: true });
|
|
19325
19281
|
const temp = `${path}.${process.pid}.tmp`;
|
|
@@ -19340,7 +19296,7 @@ function writeRegistry(registry, home = homedir8()) {
|
|
|
19340
19296
|
}
|
|
19341
19297
|
}
|
|
19342
19298
|
try {
|
|
19343
|
-
|
|
19299
|
+
rmSync7(temp, { force: true });
|
|
19344
19300
|
} catch {
|
|
19345
19301
|
}
|
|
19346
19302
|
throw error;
|
|
@@ -19375,7 +19331,7 @@ function canonicalPath(dir) {
|
|
|
19375
19331
|
return dir;
|
|
19376
19332
|
}
|
|
19377
19333
|
}
|
|
19378
|
-
function registerProject(input, home =
|
|
19334
|
+
function registerProject(input, home = homedir9()) {
|
|
19379
19335
|
input = { ...input, dir: canonicalPath(input.dir) };
|
|
19380
19336
|
const registry = readRegistry(home);
|
|
19381
19337
|
const at = (input.now ?? (() => /* @__PURE__ */ new Date()))().toISOString();
|
|
@@ -19410,7 +19366,7 @@ function registerProject(input, home = homedir8()) {
|
|
|
19410
19366
|
writeRegistry(registry, home);
|
|
19411
19367
|
return { entry, action: "created" };
|
|
19412
19368
|
}
|
|
19413
|
-
function listProjects(home =
|
|
19369
|
+
function listProjects(home = homedir9()) {
|
|
19414
19370
|
const byRecency = (a, b) => a.lastOpenedAt < b.lastOpenedAt ? 1 : a.lastOpenedAt > b.lastOpenedAt ? -1 : 0;
|
|
19415
19371
|
return readRegistry(home).projects.map((p) => ({ ...p, stale: !existsSync4(p.path) })).sort((a, b) => {
|
|
19416
19372
|
if (a.order === void 0 && b.order === void 0) return byRecency(a, b);
|
|
@@ -19419,7 +19375,7 @@ function listProjects(home = homedir8()) {
|
|
|
19419
19375
|
return a.order - b.order;
|
|
19420
19376
|
});
|
|
19421
19377
|
}
|
|
19422
|
-
function findByPath(path, home =
|
|
19378
|
+
function findByPath(path, home = homedir9()) {
|
|
19423
19379
|
const canonical = canonicalPath(path);
|
|
19424
19380
|
return readRegistry(home).projects.find((p) => p.path === canonical || p.path === path);
|
|
19425
19381
|
}
|
|
@@ -19475,7 +19431,7 @@ var init_project_registry = __esm({
|
|
|
19475
19431
|
// src/production.ts
|
|
19476
19432
|
init_local_origin();
|
|
19477
19433
|
import { readFileSync as readFileSync13 } from "node:fs";
|
|
19478
|
-
import { homedir as
|
|
19434
|
+
import { homedir as homedir11 } from "node:os";
|
|
19479
19435
|
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
19480
19436
|
import { join as join15 } from "node:path";
|
|
19481
19437
|
|
|
@@ -19723,7 +19679,7 @@ function openCdpSession(browserPath, dataPath) {
|
|
|
19723
19679
|
cmdPipe.on("error", failAll);
|
|
19724
19680
|
resPipe.on("error", failAll);
|
|
19725
19681
|
child.on("exit", () => failAll(new Error("chrome exited before the CDP handshake completed")));
|
|
19726
|
-
const
|
|
19682
|
+
const call2 = (method, params = {}, timeoutMs = 2e4) => new Promise((resolve5, reject) => {
|
|
19727
19683
|
const id = ++seq;
|
|
19728
19684
|
pending.set(id, { resolve: resolve5, reject });
|
|
19729
19685
|
cmdPipe.write(JSON.stringify({ id, method, params }) + "\0");
|
|
@@ -19735,7 +19691,7 @@ function openCdpSession(browserPath, dataPath) {
|
|
|
19735
19691
|
}, timeoutMs).unref();
|
|
19736
19692
|
});
|
|
19737
19693
|
const close = async () => {
|
|
19738
|
-
await
|
|
19694
|
+
await call2("Browser.close").catch(() => {
|
|
19739
19695
|
});
|
|
19740
19696
|
if (child.exitCode !== null || child.signalCode !== null) return;
|
|
19741
19697
|
await new Promise((resolve5) => {
|
|
@@ -19750,7 +19706,7 @@ function openCdpSession(browserPath, dataPath) {
|
|
|
19750
19706
|
});
|
|
19751
19707
|
});
|
|
19752
19708
|
};
|
|
19753
|
-
return { call, close, kill: () => child.kill() };
|
|
19709
|
+
return { call: call2, close, kill: () => child.kill() };
|
|
19754
19710
|
}
|
|
19755
19711
|
async function cdpInstallPwa(browserPath, url, dataPath) {
|
|
19756
19712
|
const manifestId = manifestIdFor(url);
|
|
@@ -19935,8 +19891,41 @@ import { chmodSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
|
19935
19891
|
import { dirname, join as join2 } from "node:path";
|
|
19936
19892
|
var CODE_BYTES = 16;
|
|
19937
19893
|
var SESSION_BYTES = 32;
|
|
19894
|
+
var SESSION_ID_BYTES = 6;
|
|
19938
19895
|
var DEFAULT_CODE_TTL_MS = 5 * 6e4;
|
|
19939
19896
|
var DEFAULT_SESSION_TTL_MS = 365 * 24 * 60 * 6e4;
|
|
19897
|
+
function memorySessionDirectory(seed = []) {
|
|
19898
|
+
const records = new Map(seed.map((r) => [r.id, r]));
|
|
19899
|
+
return {
|
|
19900
|
+
record: (r) => void records.set(r.id, r),
|
|
19901
|
+
isRevoked: (id) => records.get(id)?.revokedAt !== void 0,
|
|
19902
|
+
// Reverse FIRST, then sort. Array.sort is stable, so two devices that redeemed in the same
|
|
19903
|
+
// millisecond — which two taps on one page reliably are — keep newest-inserted-first instead of
|
|
19904
|
+
// falling back to insertion order and reading as oldest first.
|
|
19905
|
+
list: () => [...records.values()].reverse().sort((a, b) => b.createdAt - a.createdAt),
|
|
19906
|
+
revoke(id) {
|
|
19907
|
+
const found = records.get(id);
|
|
19908
|
+
if (!found || found.revokedAt !== void 0) return false;
|
|
19909
|
+
found.revokedAt = Date.now();
|
|
19910
|
+
return true;
|
|
19911
|
+
},
|
|
19912
|
+
revokeAll() {
|
|
19913
|
+
let n = 0;
|
|
19914
|
+
for (const r of records.values()) if (r.revokedAt === void 0) {
|
|
19915
|
+
r.revokedAt = Date.now();
|
|
19916
|
+
n++;
|
|
19917
|
+
}
|
|
19918
|
+
return n;
|
|
19919
|
+
}
|
|
19920
|
+
};
|
|
19921
|
+
}
|
|
19922
|
+
function describeDevice(userAgent) {
|
|
19923
|
+
const ua = userAgent ?? "";
|
|
19924
|
+
if (!ua.trim()) return "unknown device";
|
|
19925
|
+
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";
|
|
19926
|
+
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" : "";
|
|
19927
|
+
return browser ? `${browser} on ${os}` : os;
|
|
19928
|
+
}
|
|
19940
19929
|
function defaultRandomToken(bytes) {
|
|
19941
19930
|
return randomBytes(bytes).toString("base64url");
|
|
19942
19931
|
}
|
|
@@ -19945,13 +19934,15 @@ function secretsMatch(a, b) {
|
|
|
19945
19934
|
const right = Buffer.from(b);
|
|
19946
19935
|
return left.length === right.length && timingSafeEqual(left, right);
|
|
19947
19936
|
}
|
|
19948
|
-
var AccessStore = class {
|
|
19937
|
+
var AccessStore = class _AccessStore {
|
|
19949
19938
|
codes = /* @__PURE__ */ new Map();
|
|
19950
19939
|
options;
|
|
19940
|
+
sessions;
|
|
19951
19941
|
/** Signing key for sessions. Persisted by the caller; a fresh one signs every device out. */
|
|
19952
19942
|
signingKey;
|
|
19953
19943
|
constructor(options2 = {}) {
|
|
19954
19944
|
this.signingKey = options2.signingKey ?? randomBytes(32);
|
|
19945
|
+
this.sessions = options2.sessions ?? memorySessionDirectory();
|
|
19955
19946
|
this.options = {
|
|
19956
19947
|
codeTtlMs: options2.codeTtlMs ?? DEFAULT_CODE_TTL_MS,
|
|
19957
19948
|
sessionTtlMs: options2.sessionTtlMs ?? DEFAULT_SESSION_TTL_MS,
|
|
@@ -19980,7 +19971,7 @@ var AccessStore = class {
|
|
|
19980
19971
|
* atomicity is what makes "single-use" true rather than aspirational — it is the first thing the
|
|
19981
19972
|
* tests attack.
|
|
19982
19973
|
*/
|
|
19983
|
-
redeem(code) {
|
|
19974
|
+
redeem(code, device) {
|
|
19984
19975
|
if (!code) return { ok: false, reason: "unknown" };
|
|
19985
19976
|
const stored = this.codes.get(code);
|
|
19986
19977
|
if (!stored || !secretsMatch(stored.code, code)) return { ok: false, reason: "unknown" };
|
|
@@ -19993,18 +19984,32 @@ var AccessStore = class {
|
|
|
19993
19984
|
stored.consumedAt = now;
|
|
19994
19985
|
this.sweep();
|
|
19995
19986
|
this.options.onConsumed?.(code);
|
|
19996
|
-
|
|
19987
|
+
const minted = this.mintSession(now);
|
|
19988
|
+
this.sessions.record({ id: minted.id, label: device ?? "unknown device", createdAt: now });
|
|
19989
|
+
return { ok: true, session: minted.session, expiresAt: now + this.options.sessionTtlMs, id: minted.id };
|
|
19997
19990
|
}
|
|
19998
19991
|
/**
|
|
19999
|
-
* A session is `<expiry>.<hmac>` —
|
|
20000
|
-
*
|
|
20001
|
-
*
|
|
19992
|
+
* A session is `<expiry>.<id>.<nonce>.<hmac>` — signed with a key the caller persists so it survives
|
|
19993
|
+
* a restart, and self-describing enough that a stale cookie is rejected without any lookup.
|
|
19994
|
+
*
|
|
19995
|
+
* The `id` is what makes ONE device signable-out. It is short because an operator types it into
|
|
19996
|
+
* `frizz --sign-out <id>`, and it is inside the signed payload so it cannot be swapped for another
|
|
19997
|
+
* device's. Rotating the key still revokes everything at once; this is the scalpel beside that axe.
|
|
19998
|
+
*
|
|
19999
|
+
* A session minted before ids existed has a two-part payload and still verifies — it simply has no id
|
|
20000
|
+
* to revoke individually. Upgrading must not sign every device out.
|
|
20002
20001
|
*/
|
|
20003
20002
|
mintSession(now) {
|
|
20004
20003
|
const expiresAt = now + this.options.sessionTtlMs;
|
|
20004
|
+
const id = this.options.randomToken(SESSION_ID_BYTES);
|
|
20005
20005
|
const nonce = this.options.randomToken(SESSION_BYTES);
|
|
20006
|
-
const payload = `${expiresAt}.${nonce}`;
|
|
20007
|
-
return `${payload}.${this.sign(payload)}
|
|
20006
|
+
const payload = `${expiresAt}.${id}.${nonce}`;
|
|
20007
|
+
return { session: `${payload}.${this.sign(payload)}`, id };
|
|
20008
|
+
}
|
|
20009
|
+
/** The id inside a session payload, or null for a legacy two-part one. */
|
|
20010
|
+
static idOf(payload) {
|
|
20011
|
+
const parts = payload.split(".");
|
|
20012
|
+
return parts.length >= 3 ? parts[1] ?? null : null;
|
|
20008
20013
|
}
|
|
20009
20014
|
sign(payload) {
|
|
20010
20015
|
return createHmac("sha256", this.signingKey).update(payload).digest("base64url");
|
|
@@ -20018,7 +20023,9 @@ var AccessStore = class {
|
|
|
20018
20023
|
const signature = session.slice(cut + 1);
|
|
20019
20024
|
if (!secretsMatch(signature, this.sign(payload))) return false;
|
|
20020
20025
|
const expiresAt = Number(payload.slice(0, payload.indexOf(".")));
|
|
20021
|
-
|
|
20026
|
+
if (!Number.isFinite(expiresAt) || this.options.now() >= expiresAt) return false;
|
|
20027
|
+
const id = _AccessStore.idOf(payload);
|
|
20028
|
+
return !(id && this.sessions.isRevoked(id));
|
|
20022
20029
|
}
|
|
20023
20030
|
/**
|
|
20024
20031
|
* Drop codes past their expiry so a long-lived board does not accumulate them forever.
|
|
@@ -20040,6 +20047,46 @@ var AccessStore = class {
|
|
|
20040
20047
|
return [...this.codes.values()].filter((stored) => stored.consumedAt === void 0).map(({ code, createdAt, expiresAt }) => ({ code, createdAt, expiresAt }));
|
|
20041
20048
|
}
|
|
20042
20049
|
};
|
|
20050
|
+
function fileSessionDirectory(stateDir, now = Date.now) {
|
|
20051
|
+
const path = join2(stateDir, "sessions.json");
|
|
20052
|
+
let records = [];
|
|
20053
|
+
try {
|
|
20054
|
+
const parsed = JSON.parse(readFileSync(path, "utf8"));
|
|
20055
|
+
if (Array.isArray(parsed)) {
|
|
20056
|
+
records = parsed.filter(
|
|
20057
|
+
(r) => !!r && typeof r.id === "string" && typeof r.createdAt === "number"
|
|
20058
|
+
);
|
|
20059
|
+
}
|
|
20060
|
+
} catch {
|
|
20061
|
+
}
|
|
20062
|
+
const inner = memorySessionDirectory(records);
|
|
20063
|
+
const flush = () => {
|
|
20064
|
+
try {
|
|
20065
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
20066
|
+
writeFileSync(path, JSON.stringify(inner.list()), { mode: 384 });
|
|
20067
|
+
} catch (error) {
|
|
20068
|
+
if (process.env.FRIZZ_DEBUG_SESSIONS) console.error("[sessions] flush failed:", error);
|
|
20069
|
+
}
|
|
20070
|
+
};
|
|
20071
|
+
return {
|
|
20072
|
+
record(r) {
|
|
20073
|
+
inner.record(r);
|
|
20074
|
+
flush();
|
|
20075
|
+
},
|
|
20076
|
+
isRevoked: (id) => inner.isRevoked(id),
|
|
20077
|
+
list: () => inner.list(),
|
|
20078
|
+
revoke(id) {
|
|
20079
|
+
const ok = inner.revoke(id);
|
|
20080
|
+
if (ok) flush();
|
|
20081
|
+
return ok;
|
|
20082
|
+
},
|
|
20083
|
+
revokeAll() {
|
|
20084
|
+
const n = inner.revokeAll();
|
|
20085
|
+
if (n > 0) flush();
|
|
20086
|
+
return n;
|
|
20087
|
+
}
|
|
20088
|
+
};
|
|
20089
|
+
}
|
|
20043
20090
|
function loadOrCreateSessionKey(stateDir) {
|
|
20044
20091
|
const path = join2(stateDir, "session-key");
|
|
20045
20092
|
try {
|
|
@@ -21746,6 +21793,7 @@ var SUPERVISOR_RESTART_PATH = `${SUPERVISOR_CONTROL_PREFIX}/restart`;
|
|
|
21746
21793
|
var SUPERVISOR_UPDATE_RESTART_PATH = `${SUPERVISOR_CONTROL_PREFIX}/update-restart`;
|
|
21747
21794
|
var SUPERVISOR_STATUS_PATH = `${SUPERVISOR_CONTROL_PREFIX}/status`;
|
|
21748
21795
|
var SUPERVISOR_ACCESS_CODE_PATH = `${SUPERVISOR_CONTROL_PREFIX}/access-code`;
|
|
21796
|
+
var SUPERVISOR_SESSIONS_PATH = `${SUPERVISOR_CONTROL_PREFIX}/sessions`;
|
|
21749
21797
|
var SUPERVISOR_CONTROL_PROTOCOL = 1;
|
|
21750
21798
|
function responseJson(res, status, value) {
|
|
21751
21799
|
res.writeHead(status, { "content-type": "application/json; charset=utf-8", "cache-control": "no-store" });
|
|
@@ -21786,7 +21834,7 @@ function unlistedHostPage(name) {
|
|
|
21786
21834
|
}
|
|
21787
21835
|
function isControlRequest(req) {
|
|
21788
21836
|
const url = new URL(req.url ?? "/", "http://frizz.invalid");
|
|
21789
|
-
return url.pathname === SUPERVISOR_RESTART_PATH || url.pathname === SUPERVISOR_UPDATE_RESTART_PATH || url.pathname === SUPERVISOR_STATUS_PATH || url.pathname === SUPERVISOR_ACCESS_CODE_PATH;
|
|
21837
|
+
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;
|
|
21790
21838
|
}
|
|
21791
21839
|
function isLocalImageRequest(req) {
|
|
21792
21840
|
if (req.method !== "GET" && req.method !== "HEAD") return false;
|
|
@@ -21794,6 +21842,22 @@ function isLocalImageRequest(req) {
|
|
|
21794
21842
|
return LOCAL_IMAGE_PATH.test(pathname);
|
|
21795
21843
|
}
|
|
21796
21844
|
var LOCAL_IMAGE_PATH = new RegExp(`^${FRIZZ_ROUTE_PREFIX}(?:/[^/]+)?/local-image$`);
|
|
21845
|
+
async function readJsonBody(req, limit = 64 * 1024) {
|
|
21846
|
+
const chunks = [];
|
|
21847
|
+
let size = 0;
|
|
21848
|
+
for await (const chunk of req) {
|
|
21849
|
+
size += chunk.byteLength;
|
|
21850
|
+
if (size > limit) return null;
|
|
21851
|
+
chunks.push(chunk);
|
|
21852
|
+
}
|
|
21853
|
+
if (chunks.length === 0) return {};
|
|
21854
|
+
try {
|
|
21855
|
+
const parsed = JSON.parse(Buffer.concat(chunks).toString("utf8"));
|
|
21856
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
|
|
21857
|
+
} catch {
|
|
21858
|
+
return null;
|
|
21859
|
+
}
|
|
21860
|
+
}
|
|
21797
21861
|
var RestartSupervisorProxy = class {
|
|
21798
21862
|
server = null;
|
|
21799
21863
|
restartInFlight = null;
|
|
@@ -21808,6 +21872,8 @@ var RestartSupervisorProxy = class {
|
|
|
21808
21872
|
* judged here first.
|
|
21809
21873
|
*/
|
|
21810
21874
|
policy;
|
|
21875
|
+
/** The origin a tunnel or proxy serves this board at; undefined while loopback-only. Live: see setPublicOrigin. */
|
|
21876
|
+
publicOrigin;
|
|
21811
21877
|
/**
|
|
21812
21878
|
* Codes and sessions for the public origin, or null when no public origin is declared (loopback-only
|
|
21813
21879
|
* boards are never gated, so there is nothing to store).
|
|
@@ -21818,14 +21884,28 @@ var RestartSupervisorProxy = class {
|
|
|
21818
21884
|
constructor(options2) {
|
|
21819
21885
|
this.options = options2;
|
|
21820
21886
|
this.host = options2.host ?? LOOPBACK_BIND_HOST;
|
|
21821
|
-
this.
|
|
21887
|
+
this.publicOrigin = options2.publicOrigin;
|
|
21888
|
+
this.access = new AccessStore({
|
|
21822
21889
|
onConsumed: () => options2.onCodeConsumed?.(),
|
|
21823
|
-
...options2.sessionKey ? { signingKey: options2.sessionKey } : {}
|
|
21824
|
-
|
|
21825
|
-
|
|
21890
|
+
...options2.sessionKey ? { signingKey: options2.sessionKey } : {},
|
|
21891
|
+
...options2.sessionDirectory ? { sessions: options2.sessionDirectory } : {}
|
|
21892
|
+
});
|
|
21893
|
+
this.policy = this.buildPolicy();
|
|
21894
|
+
}
|
|
21895
|
+
/**
|
|
21896
|
+
* Change the public origin on a running board — what the R pane does when the operator picks or
|
|
21897
|
+
* clears a remote setup. Requests already in flight finish under the policy they started with; the
|
|
21898
|
+
* next one is judged by the new origin. Sessions survive the change.
|
|
21899
|
+
*/
|
|
21900
|
+
setPublicOrigin(origin) {
|
|
21901
|
+
this.publicOrigin = origin;
|
|
21902
|
+
this.policy = this.buildPolicy();
|
|
21903
|
+
}
|
|
21904
|
+
buildPolicy() {
|
|
21905
|
+
return {
|
|
21826
21906
|
exposed: bindHostIsExposed(this.host),
|
|
21827
|
-
allowedHosts:
|
|
21828
|
-
...
|
|
21907
|
+
allowedHosts: this.options.allowedHosts ?? [],
|
|
21908
|
+
...this.publicOrigin ? { publicOrigin: this.publicOrigin } : {}
|
|
21829
21909
|
};
|
|
21830
21910
|
}
|
|
21831
21911
|
/**
|
|
@@ -21835,11 +21915,11 @@ var RestartSupervisorProxy = class {
|
|
|
21835
21915
|
* asking at once, must not silently break the first QR somebody is still walking towards.
|
|
21836
21916
|
*/
|
|
21837
21917
|
issueAccessCode() {
|
|
21838
|
-
return this.access
|
|
21918
|
+
return this.publicOrigin ? this.access.issue() : null;
|
|
21839
21919
|
}
|
|
21840
21920
|
/** The full URL to show as a link or a QR. */
|
|
21841
21921
|
accessUrl(code) {
|
|
21842
|
-
const origin = this.
|
|
21922
|
+
const origin = this.publicOrigin;
|
|
21843
21923
|
return origin ? `${origin}/?${ACCESS_CODE_PARAM}=${encodeURIComponent(code)}` : null;
|
|
21844
21924
|
}
|
|
21845
21925
|
async listen() {
|
|
@@ -21980,6 +22060,42 @@ var RestartSupervisorProxy = class {
|
|
|
21980
22060
|
responseJson(res, 200, { url, expiresAt: link.expiresAt });
|
|
21981
22061
|
return;
|
|
21982
22062
|
}
|
|
22063
|
+
if (pathname === SUPERVISOR_SESSIONS_PATH) {
|
|
22064
|
+
if (this.arrivedPublicly(req)) {
|
|
22065
|
+
res.writeHead(403);
|
|
22066
|
+
res.end("Forbidden");
|
|
22067
|
+
return;
|
|
22068
|
+
}
|
|
22069
|
+
if (!this.publicOrigin) {
|
|
22070
|
+
responseJson(res, 409, { error: "this board has no public origin, so nothing is signed in" });
|
|
22071
|
+
return;
|
|
22072
|
+
}
|
|
22073
|
+
if (req.method === "GET") {
|
|
22074
|
+
responseJson(res, 200, { sessions: this.access.sessions.list() });
|
|
22075
|
+
return;
|
|
22076
|
+
}
|
|
22077
|
+
if (req.method !== "POST") {
|
|
22078
|
+
res.writeHead(405, { allow: "GET, POST" });
|
|
22079
|
+
res.end();
|
|
22080
|
+
return;
|
|
22081
|
+
}
|
|
22082
|
+
const body = await readJsonBody(req);
|
|
22083
|
+
const id = typeof body?.id === "string" ? body.id : void 0;
|
|
22084
|
+
if (body?.all === true) {
|
|
22085
|
+
responseJson(res, 200, { signedOut: this.access.sessions.revokeAll() });
|
|
22086
|
+
return;
|
|
22087
|
+
}
|
|
22088
|
+
if (!id) {
|
|
22089
|
+
responseJson(res, 400, { error: "name a device id, or pass all: true" });
|
|
22090
|
+
return;
|
|
22091
|
+
}
|
|
22092
|
+
if (!this.access.sessions.revoke(id)) {
|
|
22093
|
+
responseJson(res, 404, { error: `no signed-in device has the id ${id}` });
|
|
22094
|
+
return;
|
|
22095
|
+
}
|
|
22096
|
+
responseJson(res, 200, { signedOut: 1 });
|
|
22097
|
+
return;
|
|
22098
|
+
}
|
|
21983
22099
|
if (pathname === SUPERVISOR_STATUS_PATH && req.method === "GET") {
|
|
21984
22100
|
responseJson(res, 200, { protocol: SUPERVISOR_CONTROL_PROTOCOL, ...this.status() });
|
|
21985
22101
|
return;
|
|
@@ -22036,7 +22152,7 @@ var RestartSupervisorProxy = class {
|
|
|
22036
22152
|
* a tunnelled board stays bound to 127.0.0.1 while being reachable from the internet by name.
|
|
22037
22153
|
*/
|
|
22038
22154
|
arrivedPublicly(req) {
|
|
22039
|
-
const declared = this.
|
|
22155
|
+
const declared = this.publicOrigin;
|
|
22040
22156
|
if (!declared) return false;
|
|
22041
22157
|
const host = parseLocalHost(req.headers.host, this.options.port, this.policy);
|
|
22042
22158
|
return !!host && !LOOPBACK_HOSTNAMES.has(host.hostname);
|
|
@@ -22056,7 +22172,7 @@ var RestartSupervisorProxy = class {
|
|
|
22056
22172
|
* staying in somebody's URL bar.
|
|
22057
22173
|
*/
|
|
22058
22174
|
sessionAccepted(req) {
|
|
22059
|
-
if (!this.
|
|
22175
|
+
if (!this.publicOrigin) return true;
|
|
22060
22176
|
return this.access.verifySession(readCookie(req.headers.cookie, SESSION_COOKIE));
|
|
22061
22177
|
}
|
|
22062
22178
|
/**
|
|
@@ -22067,7 +22183,7 @@ var RestartSupervisorProxy = class {
|
|
|
22067
22183
|
const url = new URL(req.url ?? "/", "http://frizz.invalid");
|
|
22068
22184
|
const code = url.searchParams.get(ACCESS_CODE_PARAM);
|
|
22069
22185
|
if (code === null) return false;
|
|
22070
|
-
const redeemed = this.access
|
|
22186
|
+
const redeemed = this.access.redeem(code, describeDevice(req.headers["user-agent"]));
|
|
22071
22187
|
if (!redeemed?.ok) {
|
|
22072
22188
|
res.writeHead(401, { "content-type": "text/html; charset=utf-8", "cache-control": "no-store" });
|
|
22073
22189
|
res.end(unauthorizedPage(redeemed?.reason));
|
|
@@ -22171,66 +22287,78 @@ var RestartSupervisorProxy = class {
|
|
|
22171
22287
|
}
|
|
22172
22288
|
};
|
|
22173
22289
|
|
|
22174
|
-
// src/
|
|
22175
|
-
var
|
|
22176
|
-
|
|
22177
|
-
|
|
22178
|
-
|
|
22179
|
-
|
|
22180
|
-
|
|
22181
|
-
|
|
22182
|
-
|
|
22183
|
-
|
|
22184
|
-
function
|
|
22185
|
-
|
|
22290
|
+
// src/sessions-cli.ts
|
|
22291
|
+
var ago = (at, now = Date.now()) => {
|
|
22292
|
+
const seconds = Math.max(0, Math.round((now - at) / 1e3));
|
|
22293
|
+
if (seconds < 90) return "just now";
|
|
22294
|
+
const minutes = Math.round(seconds / 60);
|
|
22295
|
+
if (minutes < 90) return `${minutes}m ago`;
|
|
22296
|
+
const hours = Math.round(minutes / 60);
|
|
22297
|
+
if (hours < 36) return `${hours}h ago`;
|
|
22298
|
+
return `${Math.round(hours / 24)}d ago`;
|
|
22299
|
+
};
|
|
22300
|
+
async function call(port, init) {
|
|
22301
|
+
const response = await fetch(`http://127.0.0.1:${port}${SUPERVISOR_SESSIONS_PATH}`, {
|
|
22302
|
+
...init,
|
|
22303
|
+
headers: { origin: `http://127.0.0.1:${port}`, ...init?.body ? { "content-type": "application/json" } : {}, ...init?.headers }
|
|
22304
|
+
}).catch(() => void 0);
|
|
22305
|
+
if (!response) {
|
|
22306
|
+
console.error("frizz: the running board did not answer");
|
|
22307
|
+
process.exit(1);
|
|
22308
|
+
}
|
|
22309
|
+
const body = await response.json().catch(() => void 0);
|
|
22310
|
+
if (!response.ok) {
|
|
22311
|
+
console.error(`frizz: ${body?.error ?? `the board answered ${response.status}`}`);
|
|
22312
|
+
process.exit(1);
|
|
22313
|
+
}
|
|
22314
|
+
return body;
|
|
22315
|
+
}
|
|
22316
|
+
async function listSessions(port) {
|
|
22317
|
+
const body = await call(port);
|
|
22318
|
+
const sessions = body?.sessions ?? [];
|
|
22319
|
+
const live = sessions.filter((s) => s.revokedAt === void 0);
|
|
22320
|
+
if (live.length === 0) {
|
|
22321
|
+
console.log("No device has redeemed an access link.");
|
|
22322
|
+
process.exit(0);
|
|
22323
|
+
}
|
|
22324
|
+
const width = Math.max(...live.map((s) => s.id.length));
|
|
22325
|
+
for (const session of live) {
|
|
22326
|
+
console.log(` ${session.id.padEnd(width)} ${session.label} ${ago(session.createdAt)}`);
|
|
22327
|
+
}
|
|
22328
|
+
console.log(`
|
|
22329
|
+
frizz --sign-out <id> sign one out
|
|
22330
|
+
frizz --sign-out all sign every device out`);
|
|
22331
|
+
process.exit(0);
|
|
22332
|
+
}
|
|
22333
|
+
async function signOutSession(port, target2) {
|
|
22334
|
+
const all = target2 === "all";
|
|
22335
|
+
const body = await call(port, {
|
|
22336
|
+
method: "POST",
|
|
22337
|
+
body: JSON.stringify(all ? { all: true } : { id: target2 })
|
|
22338
|
+
});
|
|
22339
|
+
const count = Number(body?.signedOut ?? 0);
|
|
22340
|
+
if (all) {
|
|
22341
|
+
console.log(count === 0 ? "No device was signed in." : `Signed out ${count} device${count === 1 ? "" : "s"}.`);
|
|
22342
|
+
} else {
|
|
22343
|
+
console.log(`Signed out ${target2}.`);
|
|
22344
|
+
}
|
|
22345
|
+
process.exit(0);
|
|
22186
22346
|
}
|
|
22187
|
-
|
|
22347
|
+
|
|
22348
|
+
// src/pane-host.ts
|
|
22349
|
+
var CTRL_C = "";
|
|
22350
|
+
function installPaneHost(options2) {
|
|
22188
22351
|
const input = options2.input ?? process.stdin;
|
|
22189
22352
|
const output = options2.output ?? process.stdout;
|
|
22190
|
-
const now = options2.now ?? Date.now;
|
|
22191
22353
|
const onInterrupt = options2.onInterrupt ?? (() => process.kill(process.pid, "SIGINT"));
|
|
22192
22354
|
if (!input.isTTY || !output.isTTY || typeof input.setRawMode !== "function") return null;
|
|
22193
|
-
let
|
|
22355
|
+
let active = null;
|
|
22194
22356
|
let disposed = false;
|
|
22195
|
-
|
|
22196
|
-
|
|
22197
|
-
|
|
22198
|
-
|
|
22199
|
-
|
|
22200
|
-
const status = consumed ? "This link has been used. Press L for another." : remaining === 0 ? "This link has expired. Press L for another." : `Single use, expires in ${remaining}s.`;
|
|
22201
|
-
output.write(CLEAR);
|
|
22202
|
-
output.write("\n");
|
|
22203
|
-
for (const row of renderQrLines(shown.url)) output.write(` ${row}
|
|
22204
|
-
`);
|
|
22205
|
-
output.write(`
|
|
22206
|
-
${shown.url}
|
|
22207
|
-
`);
|
|
22208
|
-
output.write(`
|
|
22209
|
-
${DIM}${status} Press any other key to close.${RESET2}
|
|
22210
|
-
`);
|
|
22211
|
-
};
|
|
22212
|
-
let ticker;
|
|
22213
|
-
const close = () => {
|
|
22214
|
-
if (!open) return;
|
|
22215
|
-
open = false;
|
|
22216
|
-
shown = null;
|
|
22217
|
-
consumed = false;
|
|
22218
|
-
if (ticker) clearInterval(ticker);
|
|
22219
|
-
ticker = void 0;
|
|
22220
|
-
output.write(ALT_SCREEN_OFF);
|
|
22221
|
-
output.write(SHOW_CURSOR);
|
|
22222
|
-
};
|
|
22223
|
-
const openPane = () => {
|
|
22224
|
-
const link = options2.issue();
|
|
22225
|
-
if (!link) return;
|
|
22226
|
-
shown = link;
|
|
22227
|
-
consumed = false;
|
|
22228
|
-
open = true;
|
|
22229
|
-
output.write(ALT_SCREEN_ON);
|
|
22230
|
-
output.write(HIDE_CURSOR);
|
|
22231
|
-
paint();
|
|
22232
|
-
ticker = setInterval(paint, 1e3);
|
|
22233
|
-
ticker.unref?.();
|
|
22357
|
+
const closeActive = () => {
|
|
22358
|
+
if (!active) return;
|
|
22359
|
+
const pane = active;
|
|
22360
|
+
active = null;
|
|
22361
|
+
pane.close();
|
|
22234
22362
|
};
|
|
22235
22363
|
const onKey = (chunk) => {
|
|
22236
22364
|
const key = chunk.toString();
|
|
@@ -22239,16 +22367,17 @@ function installAccessPane(options2) {
|
|
|
22239
22367
|
onInterrupt();
|
|
22240
22368
|
return;
|
|
22241
22369
|
}
|
|
22242
|
-
if (
|
|
22243
|
-
close();
|
|
22370
|
+
if (active) {
|
|
22371
|
+
if (active.key(key) === "close") closeActive();
|
|
22244
22372
|
return;
|
|
22245
22373
|
}
|
|
22246
|
-
|
|
22374
|
+
const pane = options2.bindings[key];
|
|
22375
|
+
if (pane && pane.open()) active = pane;
|
|
22247
22376
|
};
|
|
22248
22377
|
const restore = () => {
|
|
22249
22378
|
if (disposed) return;
|
|
22250
22379
|
disposed = true;
|
|
22251
|
-
|
|
22380
|
+
closeActive();
|
|
22252
22381
|
input.off("data", onKey);
|
|
22253
22382
|
try {
|
|
22254
22383
|
input.setRawMode?.(false);
|
|
@@ -22262,9 +22391,70 @@ function installAccessPane(options2) {
|
|
|
22262
22391
|
input.setEncoding("utf8");
|
|
22263
22392
|
input.on("data", onKey);
|
|
22264
22393
|
process.on("exit", restore);
|
|
22394
|
+
return { dispose: restore, isOpen: () => active !== null };
|
|
22395
|
+
}
|
|
22396
|
+
|
|
22397
|
+
// src/access-pane.ts
|
|
22398
|
+
var ALT_SCREEN_ON = "\x1B[?1049h";
|
|
22399
|
+
var ALT_SCREEN_OFF = "\x1B[?1049l";
|
|
22400
|
+
var HIDE_CURSOR = "\x1B[?25l";
|
|
22401
|
+
var SHOW_CURSOR = "\x1B[?25h";
|
|
22402
|
+
var CLEAR = "\x1B[2J\x1B[H";
|
|
22403
|
+
var DIM = "\x1B[2m";
|
|
22404
|
+
var RESET2 = "\x1B[0m";
|
|
22405
|
+
function secondsUntil(expiresAt, now) {
|
|
22406
|
+
return Math.max(0, Math.ceil((expiresAt - now) / 1e3));
|
|
22407
|
+
}
|
|
22408
|
+
function createAccessPane(options2) {
|
|
22409
|
+
const output = options2.output ?? process.stdout;
|
|
22410
|
+
const now = options2.now ?? Date.now;
|
|
22411
|
+
let open = false;
|
|
22412
|
+
let shown = null;
|
|
22413
|
+
let consumed = false;
|
|
22414
|
+
let ticker;
|
|
22415
|
+
const paint = () => {
|
|
22416
|
+
if (!shown) return;
|
|
22417
|
+
const remaining = secondsUntil(shown.expiresAt, now());
|
|
22418
|
+
const status = consumed ? "This link has been used. Press L for another." : remaining === 0 ? "This link has expired. Press L for another." : `Single use, expires in ${remaining}s.`;
|
|
22419
|
+
output.write(CLEAR);
|
|
22420
|
+
output.write("\n");
|
|
22421
|
+
for (const row of renderQrLines(shown.url)) output.write(` ${row}
|
|
22422
|
+
`);
|
|
22423
|
+
output.write(`
|
|
22424
|
+
${shown.url}
|
|
22425
|
+
`);
|
|
22426
|
+
output.write(`
|
|
22427
|
+
${DIM}${status} Press any other key to close.${RESET2}
|
|
22428
|
+
`);
|
|
22429
|
+
};
|
|
22265
22430
|
return {
|
|
22266
|
-
|
|
22267
|
-
|
|
22431
|
+
open() {
|
|
22432
|
+
const link = options2.issue();
|
|
22433
|
+
if (!link) return false;
|
|
22434
|
+
shown = link;
|
|
22435
|
+
consumed = false;
|
|
22436
|
+
open = true;
|
|
22437
|
+
output.write(ALT_SCREEN_ON);
|
|
22438
|
+
output.write(HIDE_CURSOR);
|
|
22439
|
+
paint();
|
|
22440
|
+
ticker = setInterval(paint, 1e3);
|
|
22441
|
+
ticker.unref?.();
|
|
22442
|
+
return true;
|
|
22443
|
+
},
|
|
22444
|
+
key() {
|
|
22445
|
+
return "close";
|
|
22446
|
+
},
|
|
22447
|
+
close() {
|
|
22448
|
+
if (!open) return;
|
|
22449
|
+
open = false;
|
|
22450
|
+
shown = null;
|
|
22451
|
+
consumed = false;
|
|
22452
|
+
if (ticker) clearInterval(ticker);
|
|
22453
|
+
ticker = void 0;
|
|
22454
|
+
output.write(ALT_SCREEN_OFF);
|
|
22455
|
+
output.write(SHOW_CURSOR);
|
|
22456
|
+
},
|
|
22457
|
+
markConsumed() {
|
|
22268
22458
|
if (!open) return;
|
|
22269
22459
|
consumed = true;
|
|
22270
22460
|
paint();
|
|
@@ -22272,10 +22462,13 @@ function installAccessPane(options2) {
|
|
|
22272
22462
|
};
|
|
22273
22463
|
}
|
|
22274
22464
|
|
|
22465
|
+
// src/remote-controller.ts
|
|
22466
|
+
import { homedir as homedir4 } from "node:os";
|
|
22467
|
+
|
|
22275
22468
|
// src/cloud.ts
|
|
22276
22469
|
init_src();
|
|
22277
22470
|
import { spawn as spawn2 } from "node:child_process";
|
|
22278
|
-
import { existsSync, mkdirSync as mkdirSync3, readFileSync as readFileSync4, writeFileSync as writeFileSync3 } from "node:fs";
|
|
22471
|
+
import { existsSync, mkdirSync as mkdirSync3, readFileSync as readFileSync4, writeFileSync as writeFileSync3, rmSync } from "node:fs";
|
|
22279
22472
|
import { createInterface } from "node:readline/promises";
|
|
22280
22473
|
import { homedir as homedir3 } from "node:os";
|
|
22281
22474
|
import { dirname as dirname3, join as join4 } from "node:path";
|
|
@@ -22724,6 +22917,14 @@ function connectRelay(options2) {
|
|
|
22724
22917
|
}
|
|
22725
22918
|
|
|
22726
22919
|
// src/cloud.ts
|
|
22920
|
+
function isExternalConfig(config) {
|
|
22921
|
+
return config.serve === "external";
|
|
22922
|
+
}
|
|
22923
|
+
function describeCloudConfig(config) {
|
|
22924
|
+
if (isClaimedConfig(config)) return "frizz.sh";
|
|
22925
|
+
if (isExternalConfig(config)) return config.provider === "tailscale" ? "Tailscale" : "your proxy";
|
|
22926
|
+
return "Cloudflare Tunnel";
|
|
22927
|
+
}
|
|
22727
22928
|
var REGISTRAR_IS_LIVE = true;
|
|
22728
22929
|
function isClaimedConfig(config) {
|
|
22729
22930
|
return typeof config.claim === "string" && config.claim.length > 0;
|
|
@@ -22731,6 +22932,22 @@ function isClaimedConfig(config) {
|
|
|
22731
22932
|
function isRelayConfig(config) {
|
|
22732
22933
|
return isClaimedConfig(config) && config.serve === "relay";
|
|
22733
22934
|
}
|
|
22935
|
+
var CLAIM_ZONE = "frizz.sh";
|
|
22936
|
+
function zoneClaimLabel(hostname) {
|
|
22937
|
+
const suffix = `.${CLAIM_ZONE}`;
|
|
22938
|
+
if (!hostname.endsWith(suffix)) return null;
|
|
22939
|
+
const label = hostname.slice(0, -suffix.length);
|
|
22940
|
+
return label.length > 0 && !label.includes(".") ? label : null;
|
|
22941
|
+
}
|
|
22942
|
+
async function reconcileCloudConfig(config, port, home = homedir3(), onNotice, origin, github = githubCli) {
|
|
22943
|
+
if (isRelayConfig(config)) return config;
|
|
22944
|
+
const label = config.claim ?? zoneClaimLabel(config.hostname);
|
|
22945
|
+
if (!label) return config;
|
|
22946
|
+
onNotice?.(
|
|
22947
|
+
isClaimedConfig(config) ? `${config.hostname} is served by the Frizz relay now; moving this board off its tunnel` : `${config.hostname} is inside ${CLAIM_ZONE}, where names are claimed, not tunnelled; claiming ${label}`
|
|
22948
|
+
);
|
|
22949
|
+
return establishCloudConfig(label, port, home, origin, github);
|
|
22950
|
+
}
|
|
22734
22951
|
function cloudConfigPath(home = homedir3()) {
|
|
22735
22952
|
return join4(home, ".frizz", "cloud.json");
|
|
22736
22953
|
}
|
|
@@ -22740,12 +22957,14 @@ function readCloudConfig(home = homedir3()) {
|
|
|
22740
22957
|
if (typeof parsed.hostname !== "string" || !parsed.hostname) return null;
|
|
22741
22958
|
const claimed = typeof parsed.claim === "string" && parsed.claim.length > 0;
|
|
22742
22959
|
const named = typeof parsed.tunnel === "string" && parsed.tunnel.length > 0;
|
|
22743
|
-
|
|
22960
|
+
const external = parsed.serve === "external";
|
|
22961
|
+
if (!claimed && !named && !external) return null;
|
|
22744
22962
|
return {
|
|
22745
22963
|
hostname: parsed.hostname,
|
|
22746
22964
|
...named ? { tunnel: parsed.tunnel } : {},
|
|
22747
22965
|
...claimed ? { claim: parsed.claim } : {},
|
|
22748
|
-
...parsed.serve === "relay" || parsed.serve === "tunnel" ? { serve: parsed.serve } : {},
|
|
22966
|
+
...parsed.serve === "relay" || parsed.serve === "tunnel" || external ? { serve: parsed.serve } : {},
|
|
22967
|
+
...external && (parsed.provider === "tailscale" || parsed.provider === "other") ? { provider: parsed.provider } : {},
|
|
22749
22968
|
...parsed.config ? { config: parsed.config } : {}
|
|
22750
22969
|
};
|
|
22751
22970
|
} catch {
|
|
@@ -22769,6 +22988,9 @@ function writeTunnelToken(token, home = homedir3()) {
|
|
|
22769
22988
|
writeFileSync3(path, `${token}
|
|
22770
22989
|
`, { mode: 384 });
|
|
22771
22990
|
}
|
|
22991
|
+
function deleteCloudConfig(home = homedir3()) {
|
|
22992
|
+
rmSync(cloudConfigPath(home), { force: true });
|
|
22993
|
+
}
|
|
22772
22994
|
function writeCloudConfig(config, home = homedir3()) {
|
|
22773
22995
|
const path = cloudConfigPath(home);
|
|
22774
22996
|
mkdirSync3(dirname3(path), { recursive: true });
|
|
@@ -22782,19 +23004,6 @@ function normalizeHostname(raw) {
|
|
|
22782
23004
|
}
|
|
22783
23005
|
return trimmed.toLowerCase();
|
|
22784
23006
|
}
|
|
22785
|
-
async function promptForCloudName() {
|
|
22786
|
-
if (!process.stdin.isTTY) {
|
|
22787
|
-
throw new Error(
|
|
22788
|
-
"--cloud needs a saved name when there is no terminal to ask \u2014 run it once interactively first"
|
|
22789
|
-
);
|
|
22790
|
-
}
|
|
22791
|
-
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
22792
|
-
try {
|
|
22793
|
-
return (await rl.question("Name for this board \u2014 a word claims <name>.frizz.sh, or paste a hostname you already run a tunnel for: ")).trim();
|
|
22794
|
-
} finally {
|
|
22795
|
-
rl.close();
|
|
22796
|
-
}
|
|
22797
|
-
}
|
|
22798
23007
|
async function promptForTunnelName(hostname, fallback) {
|
|
22799
23008
|
if (!process.stdin.isTTY) return fallback;
|
|
22800
23009
|
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
@@ -22808,8 +23017,8 @@ async function promptForTunnelName(hostname, fallback) {
|
|
|
22808
23017
|
async function establishCloudConfig(answer, port, home = homedir3(), origin, github = githubCli) {
|
|
22809
23018
|
if (answer.includes(".")) {
|
|
22810
23019
|
const hostname = normalizeHostname(answer);
|
|
22811
|
-
const
|
|
22812
|
-
return { hostname, tunnel
|
|
23020
|
+
const tunnel = await promptForTunnelName(hostname, hostname.split(".")[0]);
|
|
23021
|
+
return { hostname, tunnel };
|
|
22813
23022
|
}
|
|
22814
23023
|
if (!REGISTRAR_IS_LIVE && !origin && !process.env.FRIZZ_REGISTRAR) {
|
|
22815
23024
|
throw new Error(
|
|
@@ -22912,6 +23121,420 @@ function startTunnel(config, onExit, onError, home = homedir3(), runToken) {
|
|
|
22912
23121
|
};
|
|
22913
23122
|
}
|
|
22914
23123
|
|
|
23124
|
+
// src/remote-controller.ts
|
|
23125
|
+
function createRemoteController(options2) {
|
|
23126
|
+
const home = options2.home ?? homedir4();
|
|
23127
|
+
let config = null;
|
|
23128
|
+
let transport = null;
|
|
23129
|
+
const stopTransport = () => {
|
|
23130
|
+
transport?.stop();
|
|
23131
|
+
transport = null;
|
|
23132
|
+
};
|
|
23133
|
+
const startTransport = async (next, justClaimed) => {
|
|
23134
|
+
if (isExternalConfig(next)) return;
|
|
23135
|
+
if (isRelayConfig(next)) {
|
|
23136
|
+
if (!justClaimed) {
|
|
23137
|
+
await resolveRunToken(next, options2.port, home, (message) => {
|
|
23138
|
+
options2.log.warn("relay", message);
|
|
23139
|
+
options2.say(message);
|
|
23140
|
+
});
|
|
23141
|
+
}
|
|
23142
|
+
transport = await startRelay(next, options2.port, home, (message) => options2.log.info("relay", message));
|
|
23143
|
+
options2.log.info("relay", `serving ${next.hostname} through the Frizz relay`);
|
|
23144
|
+
return;
|
|
23145
|
+
}
|
|
23146
|
+
const runToken = isClaimedConfig(next) ? justClaimed ? readTunnelToken(home) : await resolveRunToken(next, options2.port, home, (message) => {
|
|
23147
|
+
options2.log.warn("tunnel", message);
|
|
23148
|
+
options2.say(message);
|
|
23149
|
+
}) : null;
|
|
23150
|
+
transport = startTunnel(
|
|
23151
|
+
next,
|
|
23152
|
+
(code) => {
|
|
23153
|
+
if (code === 0 || code === null) return;
|
|
23154
|
+
options2.log.error("tunnel", `cloudflared exited with code ${code}; the public hostname is now unreachable`);
|
|
23155
|
+
},
|
|
23156
|
+
(message) => {
|
|
23157
|
+
options2.log.error("tunnel", message);
|
|
23158
|
+
options2.say(message);
|
|
23159
|
+
},
|
|
23160
|
+
home,
|
|
23161
|
+
runToken ?? void 0
|
|
23162
|
+
);
|
|
23163
|
+
options2.log.info("tunnel", `running cloudflared for ${next.hostname}`);
|
|
23164
|
+
};
|
|
23165
|
+
const originOf = (next) => next ? `https://${next.hostname}` : void 0;
|
|
23166
|
+
return {
|
|
23167
|
+
current: () => config,
|
|
23168
|
+
origin: () => originOf(config),
|
|
23169
|
+
async serveSaved() {
|
|
23170
|
+
const saved = readCloudConfig(home);
|
|
23171
|
+
if (!saved) return;
|
|
23172
|
+
const reconciled = await reconcileCloudConfig(saved, options2.port, home, (message) => {
|
|
23173
|
+
options2.log.warn("cloud", message);
|
|
23174
|
+
options2.say(message);
|
|
23175
|
+
});
|
|
23176
|
+
const justClaimed = reconciled !== saved;
|
|
23177
|
+
if (justClaimed) writeCloudConfig(reconciled, home);
|
|
23178
|
+
await startTransport(reconciled, justClaimed);
|
|
23179
|
+
config = reconciled;
|
|
23180
|
+
options2.host.setPublicOrigin(originOf(reconciled));
|
|
23181
|
+
},
|
|
23182
|
+
async apply(next, applyOptions = {}) {
|
|
23183
|
+
stopTransport();
|
|
23184
|
+
options2.host.setPublicOrigin(void 0);
|
|
23185
|
+
if (!next) {
|
|
23186
|
+
deleteCloudConfig(home);
|
|
23187
|
+
config = null;
|
|
23188
|
+
return;
|
|
23189
|
+
}
|
|
23190
|
+
writeCloudConfig(next, home);
|
|
23191
|
+
try {
|
|
23192
|
+
await startTransport(next, applyOptions.justClaimed ?? false);
|
|
23193
|
+
} catch (error) {
|
|
23194
|
+
config = null;
|
|
23195
|
+
throw error;
|
|
23196
|
+
}
|
|
23197
|
+
config = next;
|
|
23198
|
+
options2.host.setPublicOrigin(originOf(next));
|
|
23199
|
+
},
|
|
23200
|
+
stop: stopTransport
|
|
23201
|
+
};
|
|
23202
|
+
}
|
|
23203
|
+
|
|
23204
|
+
// src/remote-detect.ts
|
|
23205
|
+
import { execFile as execFile3 } from "node:child_process";
|
|
23206
|
+
var PROBE_TIMEOUT_MS = 4e3;
|
|
23207
|
+
function run2(command, args2) {
|
|
23208
|
+
return new Promise((resolve5) => {
|
|
23209
|
+
execFile3(command, args2, { encoding: "utf8", timeout: PROBE_TIMEOUT_MS }, (error, stdout) => {
|
|
23210
|
+
resolve5(error ? null : stdout);
|
|
23211
|
+
});
|
|
23212
|
+
});
|
|
23213
|
+
}
|
|
23214
|
+
async function probeGithub() {
|
|
23215
|
+
const version = await run2("gh", ["--version"]);
|
|
23216
|
+
if (version === null) return { login: null, installed: false };
|
|
23217
|
+
const login = await run2("gh", ["api", "user", "--jq", ".login"]);
|
|
23218
|
+
return { login: login?.trim() || null, installed: true };
|
|
23219
|
+
}
|
|
23220
|
+
async function probeCloudflared() {
|
|
23221
|
+
const out = await run2("cloudflared", ["--version"]);
|
|
23222
|
+
const version = out?.match(/\d+\.\d+\.\d+/)?.[0] ?? (out ? out.trim() : null);
|
|
23223
|
+
return { version };
|
|
23224
|
+
}
|
|
23225
|
+
async function probeTailscale() {
|
|
23226
|
+
const out = await run2("tailscale", ["status", "--json"]);
|
|
23227
|
+
if (out === null) return { installed: await run2("tailscale", ["--version"]) !== null, dnsName: null };
|
|
23228
|
+
try {
|
|
23229
|
+
const status = JSON.parse(out);
|
|
23230
|
+
const dnsName = status.Self?.DNSName?.replace(/\.$/, "") || null;
|
|
23231
|
+
return { installed: true, dnsName };
|
|
23232
|
+
} catch {
|
|
23233
|
+
return { installed: true, dnsName: null };
|
|
23234
|
+
}
|
|
23235
|
+
}
|
|
23236
|
+
|
|
23237
|
+
// src/remote-pane.ts
|
|
23238
|
+
var CHOICES = [
|
|
23239
|
+
{ kind: "frizz", title: "frizz.sh name", blurb: "<name>.frizz.sh \u2014 nothing to install; needs the GitHub CLI" },
|
|
23240
|
+
{ kind: "cloudflare", title: "Cloudflare Tunnel", blurb: "a domain you own on Cloudflare; cloudflared on this machine" },
|
|
23241
|
+
{ kind: "tailscale", title: "Tailscale", blurb: "your tailnet; tailscale serve does the TLS" },
|
|
23242
|
+
{ kind: "other", title: "Something else", blurb: "any proxy or tunnel you run \u2014 tell Frizz its address" },
|
|
23243
|
+
{ kind: "off", title: "Off", blurb: "loopback only" }
|
|
23244
|
+
];
|
|
23245
|
+
var ENTER = /* @__PURE__ */ new Set(["\r", "\n"]);
|
|
23246
|
+
var BACKSPACE = /* @__PURE__ */ new Set(["\x7F", "\b"]);
|
|
23247
|
+
var ESC = "\x1B";
|
|
23248
|
+
var UP = "\x1B[A";
|
|
23249
|
+
var DOWN = "\x1B[B";
|
|
23250
|
+
var TAB = " ";
|
|
23251
|
+
var SHIFT_TAB = "\x1B[Z";
|
|
23252
|
+
function kindOf(config) {
|
|
23253
|
+
if (!config) return "off";
|
|
23254
|
+
if (isClaimedConfig(config)) return "frizz";
|
|
23255
|
+
if (isExternalConfig(config)) return config.provider === "tailscale" ? "tailscale" : "other";
|
|
23256
|
+
return "cloudflare";
|
|
23257
|
+
}
|
|
23258
|
+
function wrap(text, width = 74) {
|
|
23259
|
+
const lines = [];
|
|
23260
|
+
for (const paragraph of text.split("\n")) {
|
|
23261
|
+
let line = "";
|
|
23262
|
+
for (const word of paragraph.split(" ")) {
|
|
23263
|
+
if (line && line.length + 1 + word.length > width) {
|
|
23264
|
+
lines.push(line);
|
|
23265
|
+
line = word;
|
|
23266
|
+
} else line = line ? `${line} ${word}` : word;
|
|
23267
|
+
}
|
|
23268
|
+
lines.push(line);
|
|
23269
|
+
}
|
|
23270
|
+
return lines;
|
|
23271
|
+
}
|
|
23272
|
+
function createRemotePane(options2) {
|
|
23273
|
+
const output = options2.output ?? process.stdout;
|
|
23274
|
+
let open = false;
|
|
23275
|
+
let screen = { name: "menu", index: 0 };
|
|
23276
|
+
let github = null;
|
|
23277
|
+
let cloudflared = null;
|
|
23278
|
+
let tailscale = null;
|
|
23279
|
+
const write = (lines) => {
|
|
23280
|
+
if (!open) return;
|
|
23281
|
+
output.write(CLEAR);
|
|
23282
|
+
output.write("\n");
|
|
23283
|
+
for (const line of lines) output.write(` ${line}
|
|
23284
|
+
`);
|
|
23285
|
+
};
|
|
23286
|
+
const check = (ok, text) => ok ? `${text} \u2713` : text;
|
|
23287
|
+
const paint = () => {
|
|
23288
|
+
if (!open) return;
|
|
23289
|
+
const s = screen;
|
|
23290
|
+
if (s.name === "menu") {
|
|
23291
|
+
const current = kindOf(options2.current());
|
|
23292
|
+
write([
|
|
23293
|
+
"Reach this board from anywhere",
|
|
23294
|
+
"",
|
|
23295
|
+
...wrap(
|
|
23296
|
+
"Frizz binds 127.0.0.1 and has no login. Reaching it from another device means something in front of it does the authenticating. Whatever you pick here is remembered; from then on a plain `npx frizz` serves it."
|
|
23297
|
+
),
|
|
23298
|
+
"",
|
|
23299
|
+
...CHOICES.map((choice, index) => {
|
|
23300
|
+
const marker = index === s.index ? "\u276F" : " ";
|
|
23301
|
+
const title = choice.title.padEnd(18);
|
|
23302
|
+
const now = choice.kind === current ? ` ${DIM}(current)${RESET2}` : "";
|
|
23303
|
+
return `${marker} ${title}${DIM}${choice.blurb}${RESET2}${now}`;
|
|
23304
|
+
}),
|
|
23305
|
+
"",
|
|
23306
|
+
`${DIM}\u2191\u2193 move \xB7 enter choose \xB7 esc back${RESET2}`
|
|
23307
|
+
]);
|
|
23308
|
+
return;
|
|
23309
|
+
}
|
|
23310
|
+
if (s.name === "form") {
|
|
23311
|
+
const head = formHead(s.kind);
|
|
23312
|
+
const fields = s.fields.map((field, index) => {
|
|
23313
|
+
const focused = index === s.focus;
|
|
23314
|
+
const shown = field.value || (field.placeholder ? `${DIM}${field.placeholder}${RESET2}` : "");
|
|
23315
|
+
const cursor = focused ? "\u2588" : "";
|
|
23316
|
+
return `${field.label.padEnd(10)} ${shown}${cursor}`;
|
|
23317
|
+
});
|
|
23318
|
+
write([...head, "", ...fields, ...s.note ? ["", s.note] : [], "", `${DIM}enter ${s.kind === "frizz" ? "claim" : "save"} \xB7 tab next field \xB7 esc back${RESET2}`]);
|
|
23319
|
+
return;
|
|
23320
|
+
}
|
|
23321
|
+
if (s.name === "busy") {
|
|
23322
|
+
write([s.message, "", `${DIM}working\u2026${RESET2}`]);
|
|
23323
|
+
return;
|
|
23324
|
+
}
|
|
23325
|
+
if (s.name === "done") {
|
|
23326
|
+
const lines = [s.message, ""];
|
|
23327
|
+
if (s.link) {
|
|
23328
|
+
for (const row of renderQrLines(s.link.url)) lines.push(row);
|
|
23329
|
+
lines.push("", s.link.url, "", `${DIM}Scan to sign in on a phone. Single use, expires in 5 minutes. Press L later for another.${RESET2}`);
|
|
23330
|
+
}
|
|
23331
|
+
lines.push("", `${DIM}press any key to return${RESET2}`);
|
|
23332
|
+
write(lines);
|
|
23333
|
+
return;
|
|
23334
|
+
}
|
|
23335
|
+
write([`Could not apply that: ${s.message}`, "", `${DIM}press any key to go back${RESET2}`]);
|
|
23336
|
+
};
|
|
23337
|
+
const formHead = (kind) => {
|
|
23338
|
+
if (kind === "frizz") {
|
|
23339
|
+
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";
|
|
23340
|
+
return [
|
|
23341
|
+
"frizz.sh name",
|
|
23342
|
+
"",
|
|
23343
|
+
...wrap(
|
|
23344
|
+
"Claims <name>.frizz.sh for your GitHub account. The board dials out to frizz.sh \u2014 no port, no DNS record, no tunnel binary. One name per account; a name nobody runs for 30 days is released."
|
|
23345
|
+
),
|
|
23346
|
+
"",
|
|
23347
|
+
gh
|
|
23348
|
+
];
|
|
23349
|
+
}
|
|
23350
|
+
if (kind === "cloudflare") {
|
|
23351
|
+
const cf = cloudflared === "pending" || cloudflared === null ? `${DIM}cloudflared checking\u2026${RESET2}` : cloudflared.version ? check(true, `cloudflared found, ${cloudflared.version}`) : "cloudflared not found \u2014 install it from https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/downloads/";
|
|
23352
|
+
return [
|
|
23353
|
+
"Cloudflare Tunnel",
|
|
23354
|
+
"",
|
|
23355
|
+
"Needs a domain on Cloudflare and cloudflared on this machine.",
|
|
23356
|
+
cf,
|
|
23357
|
+
"",
|
|
23358
|
+
"Create the tunnel and its DNS record once, in another terminal:",
|
|
23359
|
+
"",
|
|
23360
|
+
" cloudflared tunnel login",
|
|
23361
|
+
" cloudflared tunnel create my-board",
|
|
23362
|
+
" cloudflared tunnel route dns my-board board.example.com",
|
|
23363
|
+
"",
|
|
23364
|
+
...wrap(
|
|
23365
|
+
`Then name them here. Frizz writes ~/.cloudflared/frizz.yml (ingress to http://127.0.0.1:${options2.port}) and runs the tunnel beside the board, so the two share a lifetime.`
|
|
23366
|
+
)
|
|
23367
|
+
];
|
|
23368
|
+
}
|
|
23369
|
+
if (kind === "tailscale") {
|
|
23370
|
+
const ts = tailscale === "pending" || tailscale === null ? `${DIM}tailscale checking\u2026${RESET2}` : !tailscale.installed ? "tailscale not found \u2014 install it from https://tailscale.com/download" : tailscale.dnsName ? check(true, `tailscale found, this machine is ${tailscale.dnsName}`) : "tailscale found, but the daemon did not answer \u2014 is it signed in?";
|
|
23371
|
+
return [
|
|
23372
|
+
"Tailscale",
|
|
23373
|
+
"",
|
|
23374
|
+
...wrap("Serves the board on your tailnet. Tailscale terminates TLS and only devices signed into the tailnet can reach the name."),
|
|
23375
|
+
ts,
|
|
23376
|
+
"",
|
|
23377
|
+
"Run once, in another terminal:",
|
|
23378
|
+
"",
|
|
23379
|
+
` tailscale serve --bg ${options2.port}`,
|
|
23380
|
+
"",
|
|
23381
|
+
"Tailscale answers at the address below \u2014 that is the origin Frizz accepts."
|
|
23382
|
+
];
|
|
23383
|
+
}
|
|
23384
|
+
return [
|
|
23385
|
+
"Something else",
|
|
23386
|
+
"",
|
|
23387
|
+
...wrap(
|
|
23388
|
+
`Terminate TLS wherever you like, proxy to http://127.0.0.1:${options2.port}, and give Frizz the exact origin a browser will show \u2014 scheme and host, no path. Frizz answers only to that Host and prints a single-use sign-in link.`
|
|
23389
|
+
)
|
|
23390
|
+
];
|
|
23391
|
+
};
|
|
23392
|
+
const openForm = (kind) => {
|
|
23393
|
+
const current = options2.current();
|
|
23394
|
+
const same = kindOf(current) === kind ? current : null;
|
|
23395
|
+
let fields;
|
|
23396
|
+
if (kind === "frizz") {
|
|
23397
|
+
github = "pending";
|
|
23398
|
+
void options2.probes.github().then((result) => {
|
|
23399
|
+
github = result;
|
|
23400
|
+
if (screen.name === "form" && screen.kind === "frizz" && !screen.fields[0].value && result.login) {
|
|
23401
|
+
screen.fields[0].placeholder = result.login;
|
|
23402
|
+
}
|
|
23403
|
+
paint();
|
|
23404
|
+
});
|
|
23405
|
+
fields = [{ label: "Name", value: same?.claim ?? "" }];
|
|
23406
|
+
} else if (kind === "cloudflare") {
|
|
23407
|
+
cloudflared = "pending";
|
|
23408
|
+
void options2.probes.cloudflared().then((result) => {
|
|
23409
|
+
cloudflared = result;
|
|
23410
|
+
paint();
|
|
23411
|
+
});
|
|
23412
|
+
fields = [
|
|
23413
|
+
{ label: "Hostname", value: same?.hostname ?? "", placeholder: "board.example.com" },
|
|
23414
|
+
{ label: "Tunnel", value: same?.tunnel ?? "", placeholder: "my-board" }
|
|
23415
|
+
];
|
|
23416
|
+
} else if (kind === "tailscale") {
|
|
23417
|
+
tailscale = "pending";
|
|
23418
|
+
void options2.probes.tailscale().then((result) => {
|
|
23419
|
+
tailscale = result;
|
|
23420
|
+
if (screen.name === "form" && screen.kind === "tailscale" && !screen.fields[0].value && result.dnsName) {
|
|
23421
|
+
screen.fields[0].placeholder = `https://${result.dnsName}`;
|
|
23422
|
+
}
|
|
23423
|
+
paint();
|
|
23424
|
+
});
|
|
23425
|
+
fields = [{ label: "Origin", value: same ? `https://${same.hostname}` : "", placeholder: "https://mac-mini.your-tailnet.ts.net" }];
|
|
23426
|
+
} else {
|
|
23427
|
+
fields = [{ label: "Origin", value: same ? `https://${same.hostname}` : "", placeholder: "https://board.example.com" }];
|
|
23428
|
+
}
|
|
23429
|
+
screen = { name: "form", kind, fields, focus: 0 };
|
|
23430
|
+
paint();
|
|
23431
|
+
};
|
|
23432
|
+
const valueOf = (field) => field.value || field.placeholder || "";
|
|
23433
|
+
const submit = async (form) => {
|
|
23434
|
+
const back = form;
|
|
23435
|
+
try {
|
|
23436
|
+
let next;
|
|
23437
|
+
let justClaimed = false;
|
|
23438
|
+
if (form.kind === "frizz") {
|
|
23439
|
+
const name = valueOf(form.fields[0]).trim();
|
|
23440
|
+
if (!name) throw new Error("a name is needed");
|
|
23441
|
+
screen = { name: "busy", message: `claiming ${name}.frizz.sh\u2026` };
|
|
23442
|
+
paint();
|
|
23443
|
+
next = await options2.claim(name);
|
|
23444
|
+
justClaimed = true;
|
|
23445
|
+
} else if (form.kind === "cloudflare") {
|
|
23446
|
+
const hostname = normalizeHostname(valueOf(form.fields[0]));
|
|
23447
|
+
const tunnel = valueOf(form.fields[1]).trim();
|
|
23448
|
+
if (!tunnel) throw new Error("the tunnel's name is needed");
|
|
23449
|
+
next = { hostname, tunnel };
|
|
23450
|
+
} else {
|
|
23451
|
+
const hostname = normalizeHostname(valueOf(form.fields[0]));
|
|
23452
|
+
next = { hostname, serve: "external", provider: form.kind === "tailscale" ? "tailscale" : "other" };
|
|
23453
|
+
}
|
|
23454
|
+
screen = { name: "busy", message: `serving ${next.hostname}\u2026` };
|
|
23455
|
+
paint();
|
|
23456
|
+
await options2.apply(next, { justClaimed });
|
|
23457
|
+
options2.onChanged?.(next);
|
|
23458
|
+
screen = { name: "done", message: `Serving https://${next.hostname} (${describeCloudConfig(next)}).`, link: options2.issueLink(), config: next };
|
|
23459
|
+
} catch (error) {
|
|
23460
|
+
screen = { name: "error", message: error instanceof Error ? error.message : String(error), back };
|
|
23461
|
+
}
|
|
23462
|
+
paint();
|
|
23463
|
+
};
|
|
23464
|
+
const turnOff = async () => {
|
|
23465
|
+
screen = { name: "busy", message: "back to loopback only\u2026" };
|
|
23466
|
+
paint();
|
|
23467
|
+
try {
|
|
23468
|
+
await options2.apply(null);
|
|
23469
|
+
options2.onChanged?.(null);
|
|
23470
|
+
screen = { name: "done", message: "Loopback only. This board is reachable from this machine alone.", link: null, config: null };
|
|
23471
|
+
} catch (error) {
|
|
23472
|
+
screen = { name: "error", message: error instanceof Error ? error.message : String(error), back: { name: "menu", index: CHOICES.length - 1 } };
|
|
23473
|
+
}
|
|
23474
|
+
paint();
|
|
23475
|
+
};
|
|
23476
|
+
return {
|
|
23477
|
+
open() {
|
|
23478
|
+
open = true;
|
|
23479
|
+
const current = kindOf(options2.current());
|
|
23480
|
+
screen = { name: "menu", index: Math.max(0, CHOICES.findIndex((choice) => choice.kind === current)) };
|
|
23481
|
+
output.write(ALT_SCREEN_ON);
|
|
23482
|
+
output.write(HIDE_CURSOR);
|
|
23483
|
+
paint();
|
|
23484
|
+
return true;
|
|
23485
|
+
},
|
|
23486
|
+
key(key) {
|
|
23487
|
+
const s = screen;
|
|
23488
|
+
if (s.name === "busy") return "keep";
|
|
23489
|
+
if (s.name === "done") return "close";
|
|
23490
|
+
if (s.name === "error") {
|
|
23491
|
+
screen = s.back;
|
|
23492
|
+
paint();
|
|
23493
|
+
return "keep";
|
|
23494
|
+
}
|
|
23495
|
+
if (s.name === "menu") {
|
|
23496
|
+
if (key === ESC || key === "q") return "close";
|
|
23497
|
+
if (key === UP || key === "k") s.index = (s.index + CHOICES.length - 1) % CHOICES.length;
|
|
23498
|
+
else if (key === DOWN || key === "j") s.index = (s.index + 1) % CHOICES.length;
|
|
23499
|
+
else if (/^[1-5]$/.test(key)) s.index = Number(key) - 1;
|
|
23500
|
+
else if (ENTER.has(key)) {
|
|
23501
|
+
const choice = CHOICES[s.index];
|
|
23502
|
+
if (choice.kind === "off") void turnOff();
|
|
23503
|
+
else openForm(choice.kind);
|
|
23504
|
+
return "keep";
|
|
23505
|
+
}
|
|
23506
|
+
paint();
|
|
23507
|
+
return "keep";
|
|
23508
|
+
}
|
|
23509
|
+
if (key === ESC) {
|
|
23510
|
+
screen = { name: "menu", index: CHOICES.findIndex((choice) => choice.kind === s.kind) };
|
|
23511
|
+
paint();
|
|
23512
|
+
return "keep";
|
|
23513
|
+
}
|
|
23514
|
+
const field = s.fields[s.focus];
|
|
23515
|
+
if (ENTER.has(key)) {
|
|
23516
|
+
if (s.focus < s.fields.length - 1 && valueOf(field)) s.focus += 1;
|
|
23517
|
+
else void submit(s);
|
|
23518
|
+
} else if (key === TAB || key === DOWN) s.focus = (s.focus + 1) % s.fields.length;
|
|
23519
|
+
else if (key === SHIFT_TAB || key === UP) s.focus = (s.focus + s.fields.length - 1) % s.fields.length;
|
|
23520
|
+
else if (BACKSPACE.has(key)) field.value = field.value.slice(0, -1);
|
|
23521
|
+
else if (key.length === 1 && key >= " ") field.value += key;
|
|
23522
|
+
else if (!key.startsWith(ESC)) field.value += key.replace(/[\r\n\t]/g, "");
|
|
23523
|
+
paint();
|
|
23524
|
+
return "keep";
|
|
23525
|
+
},
|
|
23526
|
+
close() {
|
|
23527
|
+
if (!open) return;
|
|
23528
|
+
open = false;
|
|
23529
|
+
output.write(ALT_SCREEN_OFF);
|
|
23530
|
+
output.write(SHOW_CURSOR);
|
|
23531
|
+
}
|
|
23532
|
+
};
|
|
23533
|
+
}
|
|
23534
|
+
|
|
23535
|
+
// src/production.ts
|
|
23536
|
+
init_local_origin();
|
|
23537
|
+
|
|
22915
23538
|
// src/readout.ts
|
|
22916
23539
|
var SPINNER = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
|
|
22917
23540
|
var SYNC_BEGIN = "\x1B[?2026h";
|
|
@@ -23259,32 +23882,26 @@ init_project_registry();
|
|
|
23259
23882
|
import { execFileSync as execFileSync3 } from "node:child_process";
|
|
23260
23883
|
import { createServer as createServer2 } from "node:net";
|
|
23261
23884
|
import { mkdirSync as mkdirSync10, readFileSync as readFileSync11, realpathSync as realpathSync5, writeFileSync as writeFileSync10 } from "node:fs";
|
|
23262
|
-
import { homedir as
|
|
23885
|
+
import { homedir as homedir10 } from "node:os";
|
|
23263
23886
|
import { basename as basename5, join as join13, resolve as resolve3 } from "node:path";
|
|
23264
23887
|
import { setTimeout as delay2 } from "node:timers/promises";
|
|
23265
23888
|
var PORT_SCAN_COUNT = 100;
|
|
23266
23889
|
var LAUNCH_TIMEOUT_MS = 3e4;
|
|
23267
23890
|
var LAUNCH_HARD_TIMEOUT_MS = 10 * 6e4;
|
|
23268
|
-
|
|
23269
|
-
|
|
23270
|
-
|
|
23271
|
-
normalizeBindHost(value);
|
|
23272
|
-
return true;
|
|
23273
|
-
} catch {
|
|
23274
|
-
return false;
|
|
23275
|
-
}
|
|
23891
|
+
var RETIRED_NETWORK_FLAGS = ["--host", "--allowed-host", "--public-origin", "--cloud"];
|
|
23892
|
+
function retiredFlagMessage(flag) {
|
|
23893
|
+
return `${flag} was retired \u2014 how a board is reached is set up from the running board now: press R in its terminal to pick a frizz.sh name, a Cloudflare Tunnel, Tailscale or a proxy of your own. The choice is remembered, so a plain launch serves it.`;
|
|
23276
23894
|
}
|
|
23277
23895
|
function parseCliArgs(argv) {
|
|
23278
23896
|
const args2 = new Set(argv);
|
|
23279
|
-
let upCommand = false;
|
|
23280
23897
|
let rawPort;
|
|
23281
|
-
let
|
|
23282
|
-
let rawPublicOrigin;
|
|
23283
|
-
const rawAllowedHosts = [];
|
|
23898
|
+
let rawSignOut;
|
|
23284
23899
|
const consumed = /* @__PURE__ */ new Set();
|
|
23285
23900
|
for (let index = 0; index < argv.length; index++) {
|
|
23286
23901
|
const arg = argv[index];
|
|
23287
23902
|
if (consumed.has(index)) continue;
|
|
23903
|
+
const retired = arg === "up" ? "up" : RETIRED_NETWORK_FLAGS.find((flag) => arg === flag || arg.startsWith(`${flag}=`));
|
|
23904
|
+
if (retired) throw new Error(retiredFlagMessage(retired));
|
|
23288
23905
|
if (arg === "--port") {
|
|
23289
23906
|
rawPort = argv[++index];
|
|
23290
23907
|
if (rawPort === void 0 || rawPort.startsWith("-"))
|
|
@@ -23296,49 +23913,21 @@ function parseCliArgs(argv) {
|
|
|
23296
23913
|
rawPort = arg.slice("--port=".length);
|
|
23297
23914
|
continue;
|
|
23298
23915
|
}
|
|
23299
|
-
if (arg === "--
|
|
23300
|
-
|
|
23301
|
-
rawHost = argv[++index];
|
|
23302
|
-
consumed.add(index);
|
|
23303
|
-
} else {
|
|
23304
|
-
rawHost = ALL_INTERFACES_BIND_HOST;
|
|
23305
|
-
}
|
|
23306
|
-
continue;
|
|
23307
|
-
}
|
|
23308
|
-
if (arg.startsWith("--host=")) {
|
|
23309
|
-
rawHost = arg.slice("--host=".length);
|
|
23310
|
-
continue;
|
|
23311
|
-
}
|
|
23312
|
-
if (arg === "--allowed-host") {
|
|
23313
|
-
const value = argv[++index];
|
|
23916
|
+
if (arg === "--sign-out") {
|
|
23917
|
+
const value = argv[index + 1];
|
|
23314
23918
|
if (value === void 0 || value.startsWith("-"))
|
|
23315
|
-
throw new Error("--
|
|
23316
|
-
|
|
23317
|
-
rawAllowedHosts.push(value);
|
|
23318
|
-
continue;
|
|
23319
|
-
}
|
|
23320
|
-
if (arg.startsWith("--allowed-host=")) {
|
|
23321
|
-
rawAllowedHosts.push(arg.slice("--allowed-host=".length));
|
|
23322
|
-
continue;
|
|
23323
|
-
}
|
|
23324
|
-
if (arg === "--public-origin") {
|
|
23325
|
-
const value = argv[++index];
|
|
23326
|
-
if (value === void 0 || value.startsWith("-"))
|
|
23327
|
-
throw new Error("--public-origin requires a value");
|
|
23919
|
+
throw new Error("--sign-out requires a device id, or `all` to sign out every device");
|
|
23920
|
+
index++;
|
|
23328
23921
|
consumed.add(index);
|
|
23329
|
-
|
|
23922
|
+
rawSignOut = value;
|
|
23330
23923
|
continue;
|
|
23331
23924
|
}
|
|
23332
|
-
if (arg.startsWith("--
|
|
23333
|
-
|
|
23925
|
+
if (arg.startsWith("--sign-out=")) {
|
|
23926
|
+
rawSignOut = arg.slice("--sign-out=".length);
|
|
23927
|
+
if (!rawSignOut) throw new Error("--sign-out requires a device id, or `all` to sign out every device");
|
|
23334
23928
|
continue;
|
|
23335
23929
|
}
|
|
23336
23930
|
if (arg.startsWith("-")) continue;
|
|
23337
|
-
if (arg === "up") {
|
|
23338
|
-
upCommand = true;
|
|
23339
|
-
consumed.add(index);
|
|
23340
|
-
continue;
|
|
23341
|
-
}
|
|
23342
23931
|
throw new Error(
|
|
23343
23932
|
`unexpected argument: ${arg} \u2014 Frizz takes no repository path, because one server serves every project on this machine. cd into the directory you want and run it with no arguments.`
|
|
23344
23933
|
);
|
|
@@ -23349,9 +23938,6 @@ function parseCliArgs(argv) {
|
|
|
23349
23938
|
if (!Number.isInteger(port) || port < 1 || port > 65535)
|
|
23350
23939
|
throw new Error(`invalid --port value: ${rawPort}`);
|
|
23351
23940
|
}
|
|
23352
|
-
if ((upCommand || args2.has("--cloud")) && rawPublicOrigin !== void 0)
|
|
23353
|
-
throw new Error("choose either up (the saved public hostname) or --public-origin, not both");
|
|
23354
|
-
const host = rawHost === void 0 ? void 0 : normalizeBindHost(rawHost);
|
|
23355
23941
|
const known = /* @__PURE__ */ new Set([
|
|
23356
23942
|
"--app",
|
|
23357
23943
|
"--no-app",
|
|
@@ -23360,15 +23946,13 @@ function parseCliArgs(argv) {
|
|
|
23360
23946
|
"--stop",
|
|
23361
23947
|
"--status",
|
|
23362
23948
|
"--link",
|
|
23363
|
-
"--
|
|
23949
|
+
"--sessions",
|
|
23950
|
+
"--sign-out",
|
|
23364
23951
|
"--help",
|
|
23365
23952
|
"-h",
|
|
23366
23953
|
"--dev",
|
|
23367
23954
|
"--prod",
|
|
23368
23955
|
"--port",
|
|
23369
|
-
"--host",
|
|
23370
|
-
"--allowed-host",
|
|
23371
|
-
"--public-origin",
|
|
23372
23956
|
"--debug"
|
|
23373
23957
|
]);
|
|
23374
23958
|
for (let index = 0; index < argv.length; index++) {
|
|
@@ -23378,7 +23962,11 @@ function parseCliArgs(argv) {
|
|
|
23378
23962
|
index++;
|
|
23379
23963
|
continue;
|
|
23380
23964
|
}
|
|
23381
|
-
if (arg
|
|
23965
|
+
if (arg === "--sign-out") {
|
|
23966
|
+
index++;
|
|
23967
|
+
continue;
|
|
23968
|
+
}
|
|
23969
|
+
if (arg.startsWith("--port=") || arg.startsWith("--sign-out=")) continue;
|
|
23382
23970
|
if (!known.has(arg)) throw new Error(`unknown option: ${arg}`);
|
|
23383
23971
|
}
|
|
23384
23972
|
if (args2.has("--detach"))
|
|
@@ -23392,65 +23980,19 @@ function parseCliArgs(argv) {
|
|
|
23392
23980
|
foreground: true,
|
|
23393
23981
|
stop: args2.has("--stop"),
|
|
23394
23982
|
link: args2.has("--link"),
|
|
23395
|
-
|
|
23983
|
+
sessions: args2.has("--sessions"),
|
|
23984
|
+
...rawSignOut !== void 0 ? { signOut: rawSignOut } : {},
|
|
23396
23985
|
status: args2.has("--status"),
|
|
23397
23986
|
help: args2.has("--help") || args2.has("-h"),
|
|
23398
23987
|
dev: args2.has("--dev"),
|
|
23399
23988
|
debug: args2.has("--debug"),
|
|
23400
|
-
port
|
|
23401
|
-
host,
|
|
23402
|
-
allowedHosts: normalizeAllowedHosts(rawAllowedHosts),
|
|
23403
|
-
...rawPublicOrigin === void 0 ? {} : { publicOrigin: normalizePublicOrigin(rawPublicOrigin) }
|
|
23404
|
-
};
|
|
23405
|
-
}
|
|
23406
|
-
var EXPOSED_WARNING = "Anyone who can reach this address can run commands on this machine as you. Frizz has no login \u2014 only do this on a network you trust.";
|
|
23407
|
-
var REUSED_NETWORK_FLAGS_WARNING = "This joined the Frizz already running on this machine, so --host/--public-origin were ignored. Stop it first (frizz --stop) and relaunch with the flag to change how the board is reached.";
|
|
23408
|
-
var PUBLIC_ORIGIN_WARNING = "Frizz has no login of its own, so whatever sits in front of this origin IS the access control. Require authentication there (Cloudflare Access, Tailscale) \u2014 an unauthenticated tunnel lets anyone with the URL run commands on this machine as you.";
|
|
23409
|
-
function resolveBindSelection(options2, env = process.env, machineName = osHostname()) {
|
|
23410
|
-
const fromEnv = env.FRIZZ_HOST?.trim();
|
|
23411
|
-
const host = options2.host ?? (fromEnv ? normalizeBindHost(fromEnv) : LOOPBACK_BIND_HOST);
|
|
23412
|
-
const exposed = bindHostIsExposed(host);
|
|
23413
|
-
const ownNames = exposed ? machineHostNames(machineName) : [];
|
|
23414
|
-
const allowedHosts = normalizeAllowedHosts([
|
|
23415
|
-
...options2.allowedHosts,
|
|
23416
|
-
...env.FRIZZ_ALLOWED_HOSTS ? [env.FRIZZ_ALLOWED_HOSTS] : [],
|
|
23417
|
-
...ownNames
|
|
23418
|
-
]);
|
|
23419
|
-
const publicOriginRaw = options2.publicOrigin ?? env.FRIZZ_PUBLIC_ORIGIN?.trim();
|
|
23420
|
-
const publicOrigin = publicOriginRaw ? normalizePublicOrigin(publicOriginRaw) : void 0;
|
|
23421
|
-
return {
|
|
23422
|
-
host,
|
|
23423
|
-
exposed,
|
|
23424
|
-
allowedHosts,
|
|
23425
|
-
...ownNames.length > 0 ? { hostname: ownNames[0] } : {},
|
|
23426
|
-
...publicOrigin ? { publicOrigin } : {}
|
|
23989
|
+
port
|
|
23427
23990
|
};
|
|
23428
23991
|
}
|
|
23429
23992
|
function boardAddress(url) {
|
|
23430
23993
|
return url.includes("?") || url.endsWith("/") ? url : `${url}/`;
|
|
23431
23994
|
}
|
|
23432
|
-
function
|
|
23433
|
-
if (!bindHostIsExposed(host)) return [];
|
|
23434
|
-
const urls = hostname ? [`http://${hostname}:${port}`] : [];
|
|
23435
|
-
const wildcard = host === ALL_INTERFACES_BIND_HOST || host === "::";
|
|
23436
|
-
const push = (address, family) => {
|
|
23437
|
-
if (address.startsWith("fe80:")) return;
|
|
23438
|
-
urls.push(family === "IPv6" ? `http://[${address}]:${port}` : `http://${address}:${port}`);
|
|
23439
|
-
};
|
|
23440
|
-
if (!wildcard) {
|
|
23441
|
-
push(host, host.includes(":") ? "IPv6" : "IPv4");
|
|
23442
|
-
return urls;
|
|
23443
|
-
}
|
|
23444
|
-
for (const entries of Object.values(interfaces())) {
|
|
23445
|
-
for (const entry of entries ?? []) {
|
|
23446
|
-
if (entry.internal) continue;
|
|
23447
|
-
if (host === ALL_INTERFACES_BIND_HOST && entry.family !== "IPv4") continue;
|
|
23448
|
-
push(entry.address, entry.family);
|
|
23449
|
-
}
|
|
23450
|
-
}
|
|
23451
|
-
return urls;
|
|
23452
|
-
}
|
|
23453
|
-
function resolveLaunchIntent(cwd = process.cwd(), home = homedir9(), env = process.env) {
|
|
23995
|
+
function resolveLaunchIntent(cwd = process.cwd(), home = homedir10(), env = process.env) {
|
|
23454
23996
|
const candidate = realpathSync5(discoverProjectRoot(cwd, home));
|
|
23455
23997
|
const known = isExistingProjectRoot(candidate) || findByPath(candidate, home) !== void 0;
|
|
23456
23998
|
if ((known || hasProjectMarker(candidate)) && !isHomeDirectory(candidate, home))
|
|
@@ -23470,7 +24012,7 @@ function mostRecentProject(home, env) {
|
|
|
23470
24012
|
}
|
|
23471
24013
|
return void 0;
|
|
23472
24014
|
}
|
|
23473
|
-
function resolveWorkspace(cwd = process.cwd(), home =
|
|
24015
|
+
function resolveWorkspace(cwd = process.cwd(), home = homedir10(), env = process.env) {
|
|
23474
24016
|
let gitRoot;
|
|
23475
24017
|
try {
|
|
23476
24018
|
gitRoot = execFileSync3("git", ["rev-parse", "--show-toplevel"], {
|
|
@@ -23745,12 +24287,15 @@ function devCrashRetryDelay(attempt) {
|
|
|
23745
24287
|
return Math.min(DEV_CRASH_RETRY_BASE_MS * 2 ** exponent, DEV_CRASH_RETRY_MAX_MS);
|
|
23746
24288
|
}
|
|
23747
24289
|
var SUPERVISOR_ESCALATE_GRACE_MS = 500;
|
|
24290
|
+
var SUPERVISOR_DRAIN_DEADLINE_MS = CHILD_STOP_TIMEOUT_MS + 5e3;
|
|
23748
24291
|
function stripPrefix(line) {
|
|
23749
24292
|
return line.startsWith("[frizz] ") ? line.slice("[frizz] ".length) : line;
|
|
23750
24293
|
}
|
|
23751
24294
|
function createSupervisorShutdownHandler(options2) {
|
|
23752
24295
|
const now = options2.now ?? (() => Date.now());
|
|
23753
24296
|
const escalateAfterMs = options2.escalateAfterMs ?? SUPERVISOR_ESCALATE_GRACE_MS;
|
|
24297
|
+
const drainDeadlineMs = options2.drainDeadlineMs ?? SUPERVISOR_DRAIN_DEADLINE_MS;
|
|
24298
|
+
const scheduleForce = options2.scheduleForce ?? ((callback, delayMs) => setTimeout(callback, delayMs));
|
|
23754
24299
|
let startedAt = 0;
|
|
23755
24300
|
let stopping = false;
|
|
23756
24301
|
let decided = false;
|
|
@@ -23764,20 +24309,27 @@ function createSupervisorShutdownHandler(options2) {
|
|
|
23764
24309
|
}
|
|
23765
24310
|
options2.exit(code);
|
|
23766
24311
|
};
|
|
24312
|
+
const abandon = (reason) => {
|
|
24313
|
+
options2.error?.(`[frizz] ${reason} \u2014 abandoning the graceful drain`);
|
|
24314
|
+
try {
|
|
24315
|
+
options2.force?.();
|
|
24316
|
+
} catch (error) {
|
|
24317
|
+
options2.error?.(`[frizz] forced supervisor stop failed: ${error instanceof Error ? error.message : error}`);
|
|
24318
|
+
}
|
|
24319
|
+
decide(1);
|
|
24320
|
+
};
|
|
23767
24321
|
return () => {
|
|
23768
24322
|
if (stopping) {
|
|
23769
24323
|
if (decided || now() - startedAt < escalateAfterMs) return;
|
|
23770
|
-
|
|
23771
|
-
try {
|
|
23772
|
-
options2.force?.();
|
|
23773
|
-
} catch (error) {
|
|
23774
|
-
options2.error?.(`[frizz] forced supervisor stop failed: ${error instanceof Error ? error.message : error}`);
|
|
23775
|
-
}
|
|
23776
|
-
decide(1);
|
|
24324
|
+
abandon("second stop signal");
|
|
23777
24325
|
return;
|
|
23778
24326
|
}
|
|
23779
24327
|
stopping = true;
|
|
23780
24328
|
startedAt = now();
|
|
24329
|
+
options2.onStop?.();
|
|
24330
|
+
scheduleForce(() => {
|
|
24331
|
+
if (!decided) abandon(`graceful stop did not finish in ${drainDeadlineMs}ms`);
|
|
24332
|
+
}, drainDeadlineMs).unref?.();
|
|
23781
24333
|
void options2.close().then(
|
|
23782
24334
|
() => decide(0),
|
|
23783
24335
|
(error) => {
|
|
@@ -23987,6 +24539,7 @@ var Supervisor = class {
|
|
|
23987
24539
|
publicOrigin: opts.publicOrigin,
|
|
23988
24540
|
onCodeConsumed: opts.onCodeConsumed,
|
|
23989
24541
|
sessionKey: opts.sessionKey,
|
|
24542
|
+
sessionDirectory: opts.sessionDirectory,
|
|
23990
24543
|
childPort: () => this.childPort,
|
|
23991
24544
|
restart: () => this.restartFromBrowser(),
|
|
23992
24545
|
updateRestart: this.updateRestart ? () => this.updateFromBrowser() : void 0,
|
|
@@ -24012,6 +24565,9 @@ var Supervisor = class {
|
|
|
24012
24565
|
return this.boot;
|
|
24013
24566
|
}
|
|
24014
24567
|
/** Mint a single-use access link for the public origin, or null when none is declared. */
|
|
24568
|
+
setPublicOrigin(origin) {
|
|
24569
|
+
this.publicProxy.setPublicOrigin(origin);
|
|
24570
|
+
}
|
|
24015
24571
|
issueAccessLink() {
|
|
24016
24572
|
const code = this.publicProxy.issueAccessCode();
|
|
24017
24573
|
if (!code) return null;
|
|
@@ -24082,7 +24638,7 @@ var Supervisor = class {
|
|
|
24082
24638
|
if (this.closed) return;
|
|
24083
24639
|
this.reloadLauncher ||= reloadLauncher;
|
|
24084
24640
|
if (this.debounce) clearTimeout(this.debounce);
|
|
24085
|
-
const
|
|
24641
|
+
const run3 = () => {
|
|
24086
24642
|
this.debounce = null;
|
|
24087
24643
|
const scope = this.reloadLauncher ? "control plane + launcher" : "control plane";
|
|
24088
24644
|
this.logLine(
|
|
@@ -24091,8 +24647,8 @@ var Supervisor = class {
|
|
|
24091
24647
|
this.writeStatus("restarting", reason);
|
|
24092
24648
|
void this.restart();
|
|
24093
24649
|
};
|
|
24094
|
-
if (immediate)
|
|
24095
|
-
else this.debounce = setTimeout(
|
|
24650
|
+
if (immediate) run3();
|
|
24651
|
+
else this.debounce = setTimeout(run3, delayMs);
|
|
24096
24652
|
}
|
|
24097
24653
|
async restart() {
|
|
24098
24654
|
if (this.restartRunning) {
|
|
@@ -24479,7 +25035,7 @@ async function startDevSupervisor(opts) {
|
|
|
24479
25035
|
}
|
|
24480
25036
|
|
|
24481
25037
|
// src/production-update.ts
|
|
24482
|
-
import { execFile as
|
|
25038
|
+
import { execFile as execFile4, spawn as spawn3 } from "node:child_process";
|
|
24483
25039
|
var PRODUCTION_REEXEC_FLAG = "--_frizz-production-reexec";
|
|
24484
25040
|
function compareReleaseVersions(a, b) {
|
|
24485
25041
|
const parse2 = (value) => {
|
|
@@ -24528,7 +25084,7 @@ function handoffToRegistrySuccessor(plan, request, adapter) {
|
|
|
24528
25084
|
var npmRegistryReleaseAdapter = {
|
|
24529
25085
|
latestVersion(packageName) {
|
|
24530
25086
|
return new Promise((resolveVersion, reject) => {
|
|
24531
|
-
|
|
25087
|
+
execFile4("npm", ["view", `${packageName}@latest`, "version", "--json"], { encoding: "utf8" }, (error, stdout) => {
|
|
24532
25088
|
if (error) return reject(new Error(`could not check npm for ${packageName}: ${error.message}`));
|
|
24533
25089
|
try {
|
|
24534
25090
|
const parsed = JSON.parse(stdout);
|
|
@@ -24649,11 +25205,7 @@ if (options.help) {
|
|
|
24649
25205
|
console.log(
|
|
24650
25206
|
`Frizz production launcher
|
|
24651
25207
|
|
|
24652
|
-
Usage: npx ${PACKAGE_NAME} [
|
|
24653
|
-
|
|
24654
|
-
Commands:
|
|
24655
|
-
up serve at the saved public hostname and run its tunnel; asked once,
|
|
24656
|
-
then remembered
|
|
25208
|
+
Usage: npx ${PACKAGE_NAME} [options]
|
|
24657
25209
|
|
|
24658
25210
|
Run it in the directory you want to work in. One server serves EVERY project on this machine,
|
|
24659
25211
|
each at its own /project/<name> URL, so a second run joins the one already going. Runs the
|
|
@@ -24663,45 +25215,23 @@ for a source checkout.
|
|
|
24663
25215
|
Options:
|
|
24664
25216
|
--no-app print the URL without opening a browser
|
|
24665
25217
|
--port <port> request a fixed port for a new workspace server
|
|
24666
|
-
--host [address] serve on a network address instead of loopback (bare --host means 0.0.0.0)
|
|
24667
|
-
--allowed-host <name> with --host, also accept this DNS name as the board's address (repeatable)
|
|
24668
|
-
--public-origin <url> serve behind a proxy/tunnel reachable at this exact origin
|
|
24669
|
-
--cloud alias of up, for saved commands
|
|
24670
25218
|
--link print a fresh single-use access link for the running board
|
|
24671
25219
|
--debug stream the full event feed to the terminal instead of the compact readout
|
|
24672
25220
|
-h, --help show this help
|
|
24673
25221
|
|
|
24674
|
-
Environment:
|
|
24675
|
-
FRIZZ_HOST same as --host
|
|
24676
|
-
FRIZZ_ALLOWED_HOSTS same as --allowed-host, comma separated
|
|
24677
|
-
FRIZZ_PUBLIC_ORIGIN same as --public-origin
|
|
24678
|
-
|
|
24679
|
-
--host puts a board that can run shell commands as you on the network, and Frizz has no login: anyone
|
|
24680
|
-
who reaches the port controls it. Only do this on a network you trust. An IP address and this
|
|
24681
|
-
machine's own hostname work as-is; any other DNS name must be listed with --allowed-host ("*"
|
|
24682
|
-
allows any).
|
|
24683
25222
|
|
|
24684
|
-
|
|
24685
|
-
|
|
24686
|
-
|
|
24687
|
-
|
|
25223
|
+
To reach the board from a phone or another machine, press R in the terminal running it: a short
|
|
25224
|
+
walkthrough sets up a frizz.sh name, a Cloudflare Tunnel, Tailscale, or a proxy of your own, and
|
|
25225
|
+
remembers the choice, so a plain launch serves it from then on. The board stays on loopback and
|
|
25226
|
+
shows a single-use sign-in link as a QR; press L for a fresh one, or run --link from another shell.`
|
|
24688
25227
|
);
|
|
24689
25228
|
process.exit(0);
|
|
24690
25229
|
}
|
|
24691
25230
|
if (options.dev || rawArgs.includes("--prod")) fail("--dev and --prod are not available from the registry launcher");
|
|
24692
|
-
var
|
|
24693
|
-
|
|
24694
|
-
var tunnel = null;
|
|
25231
|
+
var remote = null;
|
|
25232
|
+
var paneHost = null;
|
|
24695
25233
|
var accessPane = null;
|
|
24696
25234
|
var activeAccessLink = null;
|
|
24697
|
-
var justClaimed = false;
|
|
24698
|
-
var bind = (() => {
|
|
24699
|
-
try {
|
|
24700
|
-
return resolveBindSelection(options, process.env);
|
|
24701
|
-
} catch (error) {
|
|
24702
|
-
return fail(error);
|
|
24703
|
-
}
|
|
24704
|
-
})();
|
|
24705
25235
|
var launchIntent;
|
|
24706
25236
|
var workspace = (() => {
|
|
24707
25237
|
try {
|
|
@@ -24726,6 +25256,15 @@ var workspace = (() => {
|
|
|
24726
25256
|
}
|
|
24727
25257
|
})();
|
|
24728
25258
|
process.chdir(workspace.root);
|
|
25259
|
+
if (options.sessions || options.signOut !== void 0) {
|
|
25260
|
+
const running = liveWorkspaceOwner(workspace.stateDir, workspaceLaunchTarget(workspace));
|
|
25261
|
+
if (!running?.port) {
|
|
25262
|
+
console.error("frizz: no board is running for this workspace");
|
|
25263
|
+
process.exit(1);
|
|
25264
|
+
}
|
|
25265
|
+
if (options.signOut !== void 0) await signOutSession(running.port, options.signOut);
|
|
25266
|
+
await listSessions(running.port);
|
|
25267
|
+
}
|
|
24729
25268
|
if (options.link) {
|
|
24730
25269
|
const running = liveWorkspaceOwner(workspace.stateDir, workspaceLaunchTarget(workspace));
|
|
24731
25270
|
if (!running?.port) {
|
|
@@ -24770,7 +25309,7 @@ function ownSlug() {
|
|
|
24770
25309
|
try {
|
|
24771
25310
|
return registerProject(
|
|
24772
25311
|
{ dir: workspace.root, id: target.projectId, remoteOwner: resolveProjectLabel(workspace.root)?.split("/")[0] },
|
|
24773
|
-
|
|
25312
|
+
homedir11()
|
|
24774
25313
|
).entry?.slug;
|
|
24775
25314
|
} catch {
|
|
24776
25315
|
return void 0;
|
|
@@ -24821,27 +25360,19 @@ async function openOrPrint(port, reused, path = "") {
|
|
|
24821
25360
|
} else {
|
|
24822
25361
|
readout?.settle("browser", "done", url);
|
|
24823
25362
|
}
|
|
24824
|
-
const
|
|
24825
|
-
const
|
|
24826
|
-
const networkFlagsIgnored = reused && (bind.publicOrigin !== void 0 || bindHostIsExposed(bind.host));
|
|
24827
|
-
const warnings = [
|
|
24828
|
-
...network.length > 0 ? [EXPOSED_WARNING] : [],
|
|
24829
|
-
...publicOrigin ? [PUBLIC_ORIGIN_WARNING] : [],
|
|
24830
|
-
...networkFlagsIgnored ? [REUSED_NETWORK_FLAGS_WARNING] : []
|
|
24831
|
-
];
|
|
25363
|
+
const publicOrigin = reused ? void 0 : remote?.origin();
|
|
25364
|
+
const warnings = [];
|
|
24832
25365
|
if (!readout) {
|
|
24833
25366
|
console.log(`${reused ? "reusing" : "started"} Frizz ${PACKAGE_VERSION} for ${workspace.root}`);
|
|
24834
25367
|
console.log(url);
|
|
24835
|
-
for (const address of network) console.log(address);
|
|
24836
25368
|
if (publicOrigin) console.log(activeAccessLink?.url ?? `${publicOrigin}/`);
|
|
24837
25369
|
for (const warning of warnings) console.log(warning);
|
|
24838
25370
|
return;
|
|
24839
25371
|
}
|
|
24840
|
-
const home =
|
|
25372
|
+
const home = homedir11();
|
|
24841
25373
|
readout.ready(
|
|
24842
25374
|
[
|
|
24843
25375
|
{ label: "Local", value: boardAddress(url), accent: true },
|
|
24844
|
-
...network.map((address) => ({ label: "Network", value: `${address}/`, accent: true })),
|
|
24845
25376
|
...publicOrigin ? [{ label: "Public", value: activeAccessLink?.url ?? `${publicOrigin}/`, accent: true }] : [],
|
|
24846
25377
|
{ label: "Project", value: `${workspace.name} \u2014 ${tildePath(workspace.root, home)}` },
|
|
24847
25378
|
...logger.file ? [{ label: "Logs", value: tildePath(logger.file, home) }] : []
|
|
@@ -24849,10 +25380,11 @@ async function openOrPrint(port, reused, path = "") {
|
|
|
24849
25380
|
reused ? (
|
|
24850
25381
|
// This launch owns nothing and exits immediately, so ctrl-c would not stop what it reopened.
|
|
24851
25382
|
"reopened the server already running for this project \xB7 stop it from the terminal that started it"
|
|
24852
|
-
) : options.debug ? void 0 : "press ctrl-c to stop
|
|
25383
|
+
) : options.debug ? void 0 : publicOrigin ? "press L for a fresh sign-in link \xB7 R to change how this board is reached \xB7 ctrl-c to stop" : "press R to reach this board from a phone or another machine \xB7 ctrl-c to stop",
|
|
24853
25384
|
{
|
|
24854
25385
|
...reused ? { status: `already running on port ${port}` } : {},
|
|
24855
|
-
...warnings.length > 0 ? { warning: warnings.join(" ") } : {}
|
|
25386
|
+
...warnings.length > 0 ? { warning: warnings.join(" ") } : {},
|
|
25387
|
+
...activeAccessLink && !reused ? { qr: renderQrLines(activeAccessLink.url) } : {}
|
|
24856
25388
|
}
|
|
24857
25389
|
);
|
|
24858
25390
|
}
|
|
@@ -24889,21 +25421,17 @@ async function runSupervisor(port, token) {
|
|
|
24889
25421
|
void refreshUpdateAvailable();
|
|
24890
25422
|
const updateAvailablePoll = setInterval(() => void refreshUpdateAvailable(), 30 * 60 * 1e3);
|
|
24891
25423
|
updateAvailablePoll.unref();
|
|
24892
|
-
if (options.cloud && !cloudConfig) {
|
|
24893
|
-
cloudConfig = await establishCloudConfig(await promptForCloudName(), port);
|
|
24894
|
-
writeCloudConfig(cloudConfig);
|
|
24895
|
-
bind.publicOrigin = `https://${cloudConfig.hostname}`;
|
|
24896
|
-
justClaimed = true;
|
|
24897
|
-
}
|
|
24898
25424
|
const supervisor = await startDevSupervisor({
|
|
24899
25425
|
port,
|
|
24900
|
-
host:
|
|
24901
|
-
allowedHosts:
|
|
24902
|
-
...bind.publicOrigin ? { publicOrigin: bind.publicOrigin } : {},
|
|
25426
|
+
host: LOOPBACK_BIND_HOST,
|
|
25427
|
+
allowedHosts: [],
|
|
24903
25428
|
// A spent code repaints the open QR pane as stale, so nobody photographs a dead link.
|
|
24904
25429
|
onCodeConsumed: () => accessPane?.markConsumed(),
|
|
24905
25430
|
// Persisted beside the project's other state, so a restart does not sign every device out.
|
|
24906
|
-
|
|
25431
|
+
// Always, not only when public: the origin can be switched on later (press R), and a phone signed
|
|
25432
|
+
// in through one name stays signed in through the next.
|
|
25433
|
+
sessionKey: loadOrCreateSessionKey(workspace.stateDir),
|
|
25434
|
+
sessionDirectory: fileSessionDirectory(workspace.stateDir),
|
|
24907
25435
|
cwd: workspace.root,
|
|
24908
25436
|
stateDir: workspace.stateDir,
|
|
24909
25437
|
launchTarget: target,
|
|
@@ -24945,49 +25473,41 @@ async function runSupervisor(port, token) {
|
|
|
24945
25473
|
process.exit(0);
|
|
24946
25474
|
}
|
|
24947
25475
|
});
|
|
24948
|
-
|
|
24949
|
-
|
|
24950
|
-
|
|
24951
|
-
|
|
24952
|
-
|
|
24953
|
-
|
|
24954
|
-
|
|
24955
|
-
});
|
|
24956
|
-
}
|
|
24957
|
-
tunnel = await startRelay(cloudConfig, port, homedir10(), (message) => logger.info("relay", message));
|
|
24958
|
-
logger.info("relay", `serving ${cloudConfig.hostname} through the Frizz relay`);
|
|
24959
|
-
} else {
|
|
24960
|
-
const runToken = justClaimed ? readTunnelToken() : await resolveRunToken(cloudConfig, port, homedir10(), (message) => {
|
|
24961
|
-
logger.warn("tunnel", message);
|
|
24962
|
-
console.error(`frizz: ${message}`);
|
|
24963
|
-
});
|
|
24964
|
-
tunnel = startTunnel(
|
|
24965
|
-
cloudConfig,
|
|
24966
|
-
(code) => {
|
|
24967
|
-
if (code === 0 || code === null) return;
|
|
24968
|
-
logger.error("tunnel", `cloudflared exited with code ${code}; the public hostname is now unreachable`);
|
|
24969
|
-
},
|
|
24970
|
-
(message) => {
|
|
24971
|
-
logger.error("tunnel", message);
|
|
24972
|
-
console.error(`frizz: ${message}`);
|
|
24973
|
-
},
|
|
24974
|
-
homedir10(),
|
|
24975
|
-
runToken ?? void 0
|
|
24976
|
-
);
|
|
24977
|
-
logger.info("tunnel", `running cloudflared for ${cloudConfig.hostname}`);
|
|
24978
|
-
}
|
|
25476
|
+
remote = createRemoteController({ host: supervisor, port, log: logger, say: (message) => console.error(`frizz: ${message}`) });
|
|
25477
|
+
try {
|
|
25478
|
+
await remote.serveSaved();
|
|
25479
|
+
} catch (error) {
|
|
25480
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
25481
|
+
logger.error("remote", message);
|
|
25482
|
+
console.error(`frizz: the saved remote setup could not start: ${message}`);
|
|
24979
25483
|
}
|
|
24980
|
-
|
|
25484
|
+
activeAccessLink = remote.origin() ? supervisor.issueAccessLink() : null;
|
|
25485
|
+
accessPane = createAccessPane({ issue: () => supervisor.issueAccessLink() });
|
|
25486
|
+
const remotePane = createRemotePane({
|
|
25487
|
+
port,
|
|
25488
|
+
current: () => remote?.current() ?? null,
|
|
25489
|
+
apply: (next, applyOptions) => remote.apply(next, applyOptions),
|
|
25490
|
+
claim: (name) => establishCloudConfig(name, port),
|
|
25491
|
+
issueLink: () => supervisor.issueAccessLink(),
|
|
25492
|
+
probes: { github: probeGithub, cloudflared: probeCloudflared, tailscale: probeTailscale },
|
|
25493
|
+
onChanged: (config) => logger.info("remote", config ? `reached at https://${config.hostname}` : "loopback only")
|
|
25494
|
+
});
|
|
25495
|
+
paneHost = installPaneHost({ bindings: { l: accessPane, L: accessPane, r: remotePane, R: remotePane } });
|
|
24981
25496
|
const stop = createSupervisorShutdownHandler({
|
|
24982
25497
|
close: () => supervisor.close(),
|
|
24983
25498
|
force: () => supervisor.forceStop(),
|
|
24984
25499
|
release: () => owner.release(),
|
|
25500
|
+
// Acknowledge the first signal on the spot; the drain that follows is bounded but not instant.
|
|
25501
|
+
onStop: () => {
|
|
25502
|
+
logger.info("launcher", "stop signal received; draining the control plane");
|
|
25503
|
+
readout?.notice("progress", "Stopping", "draining the control plane \u2014 press ctrl-c again to force");
|
|
25504
|
+
},
|
|
24985
25505
|
exit: (code) => {
|
|
24986
25506
|
logger.info("launcher", `stopped with code ${code}`);
|
|
24987
|
-
|
|
24988
|
-
|
|
25507
|
+
paneHost?.dispose();
|
|
25508
|
+
remote?.stop();
|
|
24989
25509
|
const suffix = logger.file ? `
|
|
24990
|
-
log: ${tildePath(logger.file,
|
|
25510
|
+
log: ${tildePath(logger.file, homedir11())}` : "";
|
|
24991
25511
|
process.stdout.write(
|
|
24992
25512
|
code === 0 ? `
|
|
24993
25513
|
Frizz stopped. Running agents keep going \u2014 they are detached daemons.${suffix}
|
|
@@ -25031,7 +25551,7 @@ try {
|
|
|
25031
25551
|
let release = await acquireGlobalLaunchLock();
|
|
25032
25552
|
let portReservation;
|
|
25033
25553
|
try {
|
|
25034
|
-
const allocation = await allocatePort(options.port, readPreferredPort(workspace.stateDir), { host:
|
|
25554
|
+
const allocation = await allocatePort(options.port, readPreferredPort(workspace.stateDir), { host: LOOPBACK_BIND_HOST });
|
|
25035
25555
|
const port = allocation.port;
|
|
25036
25556
|
portReservation = allocation.release;
|
|
25037
25557
|
const running = runSupervisor(port, claim.lease.token);
|