dirpush 0.1.4 → 0.1.6
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.
|
@@ -13,14 +13,14 @@ Always drive the browser through `dirpush ab` (agent-browser on the agents' own
|
|
|
13
13
|
|
|
14
14
|
## Creating a password without ever seeing it
|
|
15
15
|
|
|
16
|
-
|
|
16
|
+
With the sign-up (or reset) page open in `dirpush ab`, one command generates the password, types it into every field you name, checks the site got it, and saves it to the vault. It never appears in your context or any file:
|
|
17
17
|
|
|
18
18
|
```bash
|
|
19
|
-
|
|
20
|
-
|
|
19
|
+
dirpush browser password <directory-id>-<product-id> --url <this page's url> --username <email> \
|
|
20
|
+
[--login-url <the site's sign-in page>] '<password selector>' ['<confirm selector>']
|
|
21
21
|
```
|
|
22
22
|
|
|
23
|
-
|
|
23
|
+
Passwords always have upper, lower, digit and symbol (24 chars). Pass `--login-url` whenever the page isn't the sign-in page (a sign-up form, a reset link): the vault opens that URL for every later `browser login`. Never generate passwords yourself (`openssl rand -base64` can lack a symbol, and sites reject it).
|
|
24
24
|
|
|
25
25
|
## Continue with Google (`accounts.oauth_google`)
|
|
26
26
|
|
package/package.json
CHANGED
package/src/browser.ts
CHANGED
|
@@ -78,8 +78,22 @@ export async function stopBrowser() {
|
|
|
78
78
|
return { stopped: true };
|
|
79
79
|
}
|
|
80
80
|
|
|
81
|
+
/**
|
|
82
|
+
* The CDP port of the agents' Chrome: the one `browser start` runs on BROWSER_PORT, else the one `dirpush ab` launched
|
|
83
|
+
* through agent-browser (its port is in the profile's DevToolsActivePort file). Only a live port counts.
|
|
84
|
+
*/
|
|
85
|
+
async function agentsChromePort() {
|
|
86
|
+
const live = async (port: number) => fetch(`http://127.0.0.1:${port}/json/version`, { signal: AbortSignal.timeout(1500) }).then((r) => r.ok, () => false);
|
|
87
|
+
if (await live(BROWSER_PORT)) return BROWSER_PORT;
|
|
88
|
+
const file = Bun.file(join(PROFILE, "DevToolsActivePort"));
|
|
89
|
+
const port = (await file.exists()) ? Number((await file.text()).split("\n")[0]) : NaN;
|
|
90
|
+
if (port && (await live(port))) return port;
|
|
91
|
+
throw new Error("the agents' browser is not open: open the page with 'dirpush ab open <url>' first");
|
|
92
|
+
}
|
|
93
|
+
let cdpPort = BROWSER_PORT;
|
|
94
|
+
|
|
81
95
|
function ab(args: string[], stdin?: string) {
|
|
82
|
-
const p = Bun.spawnSync(["agent-browser", "--session", SECRETS_SESSION, "--cdp", String(
|
|
96
|
+
const p = Bun.spawnSync(["agent-browser", "--session", SECRETS_SESSION, "--cdp", String(cdpPort), ...args], { stdin: stdin === undefined ? "ignore" : Buffer.from(stdin), stdout: "pipe", stderr: "pipe", timeout: 60_000 });
|
|
83
97
|
const out = p.stdout.toString().trim();
|
|
84
98
|
if (p.exitCode !== 0) throw new Error(`agent-browser ${args[0]} failed: ${(p.stderr.toString() || out).trim().slice(0, 300)}`);
|
|
85
99
|
return out;
|
|
@@ -88,8 +102,9 @@ function ab(args: string[], stdin?: string) {
|
|
|
88
102
|
/** The open tab on `site`'s host (the one the agent is working in); the most recently active one if several. */
|
|
89
103
|
async function pageOn(site: string) {
|
|
90
104
|
const host = new URL(site.includes("://") ? site : `https://${site}`).hostname.replace(/^www\./, "");
|
|
91
|
-
|
|
92
|
-
const
|
|
105
|
+
cdpPort = await agentsChromePort();
|
|
106
|
+
const list = (await (await fetch(`http://127.0.0.1:${cdpPort}/json/list`, { signal: AbortSignal.timeout(1500) })).json()) as Target[];
|
|
107
|
+
const page = list.find((t) => t.type === "page" && /^https?:/.test(t.url) && new URL(t.url).hostname.replace(/^www\./, "").endsWith(host));
|
|
93
108
|
if (!page) throw new Error(`no open tab on ${host}; open the login/sign-up page first`);
|
|
94
109
|
ab(["tab", page.id]);
|
|
95
110
|
return page;
|
|
@@ -97,6 +112,7 @@ async function pageOn(site: string) {
|
|
|
97
112
|
|
|
98
113
|
/** Fills a saved login (agent-browser vault) into the open tab on that site. The password never leaves the vault. */
|
|
99
114
|
export async function vaultLogin(name: string) {
|
|
115
|
+
cdpPort = await agentsChromePort();
|
|
100
116
|
const meta = JSON.parse(ab(["auth", "show", name, "--json"])) as { data?: { profile?: { url?: string } } };
|
|
101
117
|
const url = meta.data?.profile?.url;
|
|
102
118
|
if (!url) throw new Error(`vault entry ${name} has no login URL`);
|
|
@@ -105,19 +121,31 @@ export async function vaultLogin(name: string) {
|
|
|
105
121
|
return { vault: name, page: page.url.slice(0, 200), filled: true };
|
|
106
122
|
}
|
|
107
123
|
|
|
124
|
+
/** 24 random characters with every class sites ask for (upper, lower, digit, symbol), symbols that forms accept. */
|
|
125
|
+
export function strongPassword() {
|
|
126
|
+
const sets = ["ABCDEFGHJKLMNPQRSTUVWXYZ", "abcdefghijkmnopqrstuvwxyz", "23456789", "!@#%^*-_=+?"];
|
|
127
|
+
const all = sets.join("");
|
|
128
|
+
const pick = (set: string, b: number) => set[b % set.length]!;
|
|
129
|
+
const bytes = randomBytes(32);
|
|
130
|
+
const chars = [...sets.map((set, i) => pick(set, bytes[i]!)), ...Array.from({ length: 20 }, (_, i) => pick(all, bytes[i + 4]!))];
|
|
131
|
+
for (let i = chars.length - 1; i > 0; i--) { const j = bytes[(i + 8) % 32]! % (i + 1); [chars[i], chars[j]] = [chars[j]!, chars[i]!]; }
|
|
132
|
+
return chars.join("");
|
|
133
|
+
}
|
|
134
|
+
|
|
108
135
|
/**
|
|
109
136
|
* New account password: generated here, typed into every given field of the open tab on `url`'s site, checked,
|
|
110
137
|
* then saved to the vault as `name`. Returns no secret.
|
|
111
138
|
*/
|
|
112
|
-
export async function newPassword(name: string, url: string, username: string, selectors: string[]) {
|
|
139
|
+
export async function newPassword(name: string, url: string, username: string, selectors: string[], loginUrl = url) {
|
|
113
140
|
if (!selectors.length) throw new Error("pass the password field selector(s), e.g. 'input[type=password]'");
|
|
114
141
|
const page = await pageOn(url);
|
|
115
|
-
const pw =
|
|
142
|
+
const pw = strongPassword();
|
|
116
143
|
for (const s of selectors) ab(["fill", s, pw]);
|
|
117
144
|
// A fill can "succeed" on a selector that matched nothing; never save a password the site didn't get.
|
|
118
145
|
const lengths = JSON.parse(ab(["eval", `JSON.stringify(${JSON.stringify(selectors)}.map((s) => document.querySelector(s)?.value.length ?? -1))`]));
|
|
119
146
|
const got = (typeof lengths === "string" ? JSON.parse(lengths) : lengths) as number[];
|
|
120
147
|
if (got.some((n) => n !== pw.length)) throw new Error(`password fields not filled (lengths ${got.join(", ")}); nothing saved`);
|
|
121
|
-
|
|
148
|
+
// The vault keeps the sign-in page: `browser login` opens on it later, so a reset or sign-up URL there breaks logins.
|
|
149
|
+
ab(["auth", "save", name, "--url", loginUrl, "--username", username, "--password-stdin"], pw);
|
|
122
150
|
return { vault: name, username, fields: selectors.length, page: page.url.slice(0, 200), saved: true };
|
|
123
151
|
}
|
package/src/cli.ts
CHANGED
|
@@ -79,6 +79,7 @@ const { values: flags, positionals } = parseArgs({
|
|
|
79
79
|
source: { type: "string" },
|
|
80
80
|
job: { type: "string" },
|
|
81
81
|
url: { type: "string" },
|
|
82
|
+
"login-url": { type: "string" },
|
|
82
83
|
username: { type: "string" },
|
|
83
84
|
rel: { type: "string" }, // registry set-link
|
|
84
85
|
samples: { type: "string" },
|
|
@@ -655,8 +656,8 @@ const commands: Record<string, (() => Promise<unknown> | unknown) | undefined> =
|
|
|
655
656
|
"browser stop": () => stopBrowser(),
|
|
656
657
|
"browser login": () => vaultLogin(arg(0, "vault-name")),
|
|
657
658
|
"browser password": () => {
|
|
658
|
-
if (!flags.url || !flags.username) throw new Error("browser password <vault-name> --url <
|
|
659
|
-
return newPassword(arg(0, "vault-name"), flags.url, flags.username, args.slice(1));
|
|
659
|
+
if (!flags.url || !flags.username) throw new Error("browser password <vault-name> --url <page-url> --username <email> [--login-url <sign-in page>] <field-selector…>");
|
|
660
|
+
return newPassword(arg(0, "vault-name"), flags.url, flags.username, args.slice(1), flags["login-url"]);
|
|
660
661
|
},
|
|
661
662
|
async "serve"() {
|
|
662
663
|
const { serve } = await import("./server");
|
|
@@ -730,7 +731,7 @@ const HELP: [string, [string, string][]][] = [
|
|
|
730
731
|
["google-login", "the human signs into Google once in the agents' Chrome profile"],
|
|
731
732
|
["browser start | status | stop", "the dedicated Chrome (real profile, Google services blocked) agents drive"],
|
|
732
733
|
["browser login <vault-name>", "fill a saved login into the open tab on that site (password never shown)"],
|
|
733
|
-
["browser password <vault-name> --url u --username e <selector…>", "new
|
|
734
|
+
["browser password <vault-name> --url u --username e [--login-url u] <selector…>", "new password (sign-up or reset): generate, fill, verify and save it (never shown); --login-url when the page isn't the sign-in page (e.g. a reset link)"],
|
|
734
735
|
]],
|
|
735
736
|
["Other", [
|
|
736
737
|
["dr show | dr refresh [--all]", "Domain Rating history (refresh needs AHREFS_API_KEY)"],
|