demobite 1.19.1 → 1.21.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/launcher/index.mjs +31 -6
- package/package.json +1 -1
- package/skill/scripts/signout.mjs +81 -0
package/launcher/index.mjs
CHANGED
|
@@ -113,12 +113,16 @@ if (arg === "logs") {
|
|
|
113
113
|
let text = ""; try { text = fs.readFileSync(LOG, "utf8"); } catch { /* none */ }
|
|
114
114
|
const lines = text.trimEnd().split("\n"); console.log(lines.slice(-n).join("\n") || " (no log yet)"); process.exit(0);
|
|
115
115
|
}
|
|
116
|
-
|
|
116
|
+
// The login service, removed: the file, the registration, and (unless we ARE the service) the listener.
|
|
117
|
+
const removeService = () => {
|
|
117
118
|
const def = serviceDef();
|
|
119
|
+
fs.rmSync(def.file, { force: true });
|
|
118
120
|
if (process.platform === "darwin") spawnSync("launchctl", ["bootout", `gui/${process.getuid()}/${SERVICE_LABEL}`], { stdio: "ignore" });
|
|
119
121
|
else if (process.platform === "linux") spawnSync("systemctl", ["--user", "disable", "--now", SERVICE_UNIT], { stdio: "ignore" });
|
|
120
122
|
else spawnSync("reg", ["delete", "HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Run", "/v", RUN_NAME, "/f"], { stdio: "ignore" });
|
|
121
|
-
|
|
123
|
+
};
|
|
124
|
+
if (arg === "off") {
|
|
125
|
+
removeService();
|
|
122
126
|
const pid = killListener();
|
|
123
127
|
// the browsers of an interrupted take go with it
|
|
124
128
|
for (const c of readJson(CHILDREN) ?? []) { try { process.kill(process.platform === "win32" ? c : -c, "SIGTERM"); } catch { /* gone */ } }
|
|
@@ -562,8 +566,9 @@ if (arg === "listen") {
|
|
|
562
566
|
// in the app (look phrases), its verdict posted as the job's result.
|
|
563
567
|
const runLookOrSignin = async (job) => {
|
|
564
568
|
const isLook = job.kind === "look";
|
|
565
|
-
const
|
|
566
|
-
|
|
569
|
+
const isOut = job.kind === "signout";
|
|
570
|
+
const script = path.join(dest, "scripts", isLook ? "look.mjs" : isOut ? "signout.mjs" : "signin.mjs");
|
|
571
|
+
console.log(`\n ${isLook ? "◉ Looking at" : isOut ? "⊘ Signing out of" : "⚿ Sign-in at"} ${job.url}`);
|
|
567
572
|
noteState({ busy: true, job: job.id });
|
|
568
573
|
await event(job.id, "started", { machine: name });
|
|
569
574
|
// Alive while the person signs in (a window can wait minutes): the heartbeat, or the rail calls this machine offline.
|
|
@@ -590,15 +595,35 @@ if (arg === "listen") {
|
|
|
590
595
|
if (isLook) {
|
|
591
596
|
if (verdict) await event(job.id, "looked", verdict);
|
|
592
597
|
else await event(job.id, "failed", { message: "The look did not finish" });
|
|
598
|
+
} else if (isOut) {
|
|
599
|
+
if (code === 0) await event(job.id, "ready", { signedOut: true, url: job.url, ...(verdict ?? {}) });
|
|
600
|
+
else await event(job.id, "failed", { message: "The sign-out did not complete" });
|
|
593
601
|
} else if (code === 0) await event(job.id, "ready", { signedIn: true, url: job.url });
|
|
594
602
|
else await event(job.id, "failed", { message: "The sign-in did not complete" });
|
|
595
|
-
console.log(` ■ ${isLook ? `Look: ${verdict?.access ?? "failed"}` : code === 0 ? "Signed in." : "Sign-in did not complete."}\n`);
|
|
603
|
+
console.log(` ■ ${isLook ? `Look: ${verdict?.access ?? "failed"}` : isOut ? (code === 0 ? "Signed out." : "Sign-out did not complete.") : code === 0 ? "Signed in." : "Sign-in did not complete."}\n`);
|
|
596
604
|
waitOn();
|
|
597
605
|
};
|
|
598
606
|
|
|
607
|
+
// DISCONNECT FROM THE APP (1.21.0, founder 2026-10-16: "go back to the connected machine via my browser
|
|
608
|
+
// and disconnect"): the app's order, taken at check-in. Says so to the app first (the key is needed for
|
|
609
|
+
// that), then the sign-ins, the ledger and the key go from this machine, the login service is removed,
|
|
610
|
+
// and the process ends. `demobite on` with a new line from the app connects it again.
|
|
611
|
+
const runOff = async (job) => {
|
|
612
|
+
console.log(`\n ⏏ Disconnecting this machine, on your word from the app.`);
|
|
613
|
+
noteState({ busy: true, job: job.id });
|
|
614
|
+
await event(job.id, "started", { machine: name });
|
|
615
|
+
await event(job.id, "ready", { disconnected: true, machine: name });
|
|
616
|
+
const rec = path.join(process.cwd(), ".recorder");
|
|
617
|
+
for (const f of ["profile", "signins.json", "config.json", "listen.json"]) fs.rmSync(path.join(rec, f), { recursive: true, force: true });
|
|
618
|
+
noteState({ busy: false, job: null, last_error: null, disconnected: true });
|
|
619
|
+
console.log(` ○ Disconnected. The sign-ins and the key are gone from this machine; the login service is removed.`);
|
|
620
|
+
if (SERVICE) removeService(); // bootout ends this process; the handlers below clear the lock
|
|
621
|
+
bye("OFF");
|
|
622
|
+
};
|
|
599
623
|
const runJob = async (job) => {
|
|
600
624
|
waitOff();
|
|
601
|
-
if (job.kind === "
|
|
625
|
+
if (job.kind === "off") return runOff(job);
|
|
626
|
+
if (job.kind === "look" || job.kind === "signin" || job.kind === "signout") return runLookOrSignin(job);
|
|
602
627
|
const model = typeof job.model === "string" && job.model ? job.model : JOB_MODEL;
|
|
603
628
|
// Alive while filming (1.13.1): a heartbeat every 5 s so the rail never shows this machine
|
|
604
629
|
// offline mid-take (the gap after the last step used to do exactly that).
|
package/package.json
CHANGED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// SIGN OUT (founder 2026-10-16: "there should be an option to sign out… the credentials will be
|
|
3
|
+
// removed"). The recorder films in its own Chrome profile (.recorder/profile), apart from the
|
|
4
|
+
// person's browser. This script forgets ONE SITE in that profile: every cookie of the site and
|
|
5
|
+
// its subdomains, the site's local storage, and the ledger entries (.recorder/signins.json) that
|
|
6
|
+
// said the recorder was signed in there. The person's own browser is never touched.
|
|
7
|
+
//
|
|
8
|
+
// node scripts/signout.mjs <url or host>
|
|
9
|
+
//
|
|
10
|
+
// A site is the registrable domain (app.acme.com and auth.acme.com are one site, as the cookies
|
|
11
|
+
// are), so a sign-in made on one subdomain and used on another goes in one move. Headless: no
|
|
12
|
+
// window, nothing to click. Prints one line; RESULT {…} for the listener.
|
|
13
|
+
import fs from "node:fs";
|
|
14
|
+
import path from "node:path";
|
|
15
|
+
import { postEvent } from "./listen-event.mjs";
|
|
16
|
+
|
|
17
|
+
const arg = process.argv[2];
|
|
18
|
+
if (!arg) { console.error("Usage: node scripts/signout.mjs <url or host>"); process.exit(2); }
|
|
19
|
+
let target;
|
|
20
|
+
try { target = new URL(/^https?:\/\//i.test(arg) ? arg : `https://${arg}`); } catch { console.error("Not an address."); process.exit(2); }
|
|
21
|
+
|
|
22
|
+
// The registrable domain, near enough: the last two labels, or three under a short second level
|
|
23
|
+
// (co.uk, com.au). Local names and IPs stay as they are.
|
|
24
|
+
const SLD = new Set(["co", "com", "net", "org", "gov", "edu", "ac"]);
|
|
25
|
+
export const siteOf = (hostname) => {
|
|
26
|
+
const h = hostname.toLowerCase();
|
|
27
|
+
if (/^[\d.]+$/.test(h) || h === "localhost" || !h.includes(".")) return h;
|
|
28
|
+
const l = h.split(".");
|
|
29
|
+
if (l.length >= 3 && SLD.has(l[l.length - 2]) && l[l.length - 1].length === 2) return l.slice(-3).join(".");
|
|
30
|
+
return l.slice(-2).join(".");
|
|
31
|
+
};
|
|
32
|
+
const site = siteOf(target.hostname);
|
|
33
|
+
const ofSite = (hostname) => { const h = String(hostname).replace(/^\./, "").toLowerCase(); return h === site || h.endsWith("." + site); };
|
|
34
|
+
|
|
35
|
+
const ledgerPath = path.resolve(process.cwd(), ".recorder", "signins.json");
|
|
36
|
+
let ledger = { origins: {} };
|
|
37
|
+
try { ledger = JSON.parse(fs.readFileSync(ledgerPath, "utf8")); } catch { /* none */ }
|
|
38
|
+
const origins = Object.keys(ledger.origins ?? {}).filter((o) => { try { return ofSite(new URL(o).hostname); } catch { return false; } });
|
|
39
|
+
const visit = [...new Set([target.origin, ...origins])];
|
|
40
|
+
|
|
41
|
+
const { chromium } = await import("playwright");
|
|
42
|
+
const profileDir = process.env.RECORDER_PROFILE || path.resolve(process.cwd(), ".recorder", "profile");
|
|
43
|
+
const launchOpts = {
|
|
44
|
+
headless: true,
|
|
45
|
+
viewport: { width: 1440, height: 900 },
|
|
46
|
+
args: ["--force-color-profile=srgb", "--disable-blink-features=AutomationControlled", "--disable-features=MediaRouter,DialMediaRouteProvider", "--disable-component-update", "--no-default-browser-check"],
|
|
47
|
+
ignoreDefaultArgs: ["--enable-automation"],
|
|
48
|
+
};
|
|
49
|
+
let ctx;
|
|
50
|
+
try { ctx = await chromium.launchPersistentContext(profileDir, { ...launchOpts, channel: "chrome" }); }
|
|
51
|
+
catch { ctx = await chromium.launchPersistentContext(profileDir, launchOpts); }
|
|
52
|
+
const page = ctx.pages()[0] ?? (await ctx.newPage());
|
|
53
|
+
|
|
54
|
+
// Storage first (a page load could set a fresh anonymous cookie), then every cookie of the site.
|
|
55
|
+
let stores = 0;
|
|
56
|
+
for (const o of visit) {
|
|
57
|
+
try {
|
|
58
|
+
await page.goto(o, { waitUntil: "domcontentloaded", timeout: 15000 });
|
|
59
|
+
await page.evaluate(async () => {
|
|
60
|
+
try { localStorage.clear(); } catch { /* opaque */ }
|
|
61
|
+
try { sessionStorage.clear(); } catch { /* opaque */ }
|
|
62
|
+
try { const dbs = (await indexedDB.databases?.()) ?? []; for (const d of dbs) if (d.name) indexedDB.deleteDatabase(d.name); } catch { /* none */ }
|
|
63
|
+
});
|
|
64
|
+
stores++;
|
|
65
|
+
} catch { /* unreachable now: the cookies still go */ }
|
|
66
|
+
}
|
|
67
|
+
const before = (await ctx.cookies()).filter((c) => ofSite(c.domain));
|
|
68
|
+
try { await ctx.clearCookies({ domain: new RegExp(`(^|\\.)${site.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}$`, "i") }); }
|
|
69
|
+
catch { for (const c of before) { try { await ctx.clearCookies({ name: c.name, domain: c.domain }); } catch { /* old playwright */ } } }
|
|
70
|
+
const after = (await ctx.cookies()).filter((c) => ofSite(c.domain));
|
|
71
|
+
await ctx.close();
|
|
72
|
+
|
|
73
|
+
if (origins.length) {
|
|
74
|
+
for (const o of origins) delete ledger.origins[o];
|
|
75
|
+
try { fs.writeFileSync(ledgerPath, JSON.stringify(ledger, null, 2) + "\n"); } catch { /* the cookies are gone either way */ }
|
|
76
|
+
}
|
|
77
|
+
const cleared = before.length - after.length;
|
|
78
|
+
console.log(`✓ Signed out of ${site} on this machine: ${cleared} cookie${cleared === 1 ? "" : "s"} cleared, ${stores} page store${stores === 1 ? "" : "s"} emptied${origins.length ? `, ${origins.length} saved sign-in${origins.length === 1 ? "" : "s"} forgotten` : ""}.`);
|
|
79
|
+
await postEvent("signed_out", { site, origins, cleared });
|
|
80
|
+
console.log("RESULT " + JSON.stringify({ site, origins, cleared, left: after.length }));
|
|
81
|
+
process.exit(0);
|