@withbuddi/buddi 0.1.0-pre.38 → 0.1.0-pre.39
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/extension/background.js +635 -21
- package/extension/manifest.json +2 -2
- package/node_modules/@buddi/cli/dist/.tsbuildinfo +1 -1
- package/node_modules/@buddi/cli/dist/access-cmd.d.ts +1 -1
- package/node_modules/@buddi/cli/dist/access-cmd.d.ts.map +1 -1
- package/node_modules/@buddi/cli/dist/access-cmd.js +23 -3
- package/node_modules/@buddi/cli/dist/access-cmd.js.map +1 -1
- package/node_modules/@buddi/cli/dist/args.d.ts +2 -2
- package/node_modules/@buddi/cli/dist/args.d.ts.map +1 -1
- package/node_modules/@buddi/cli/dist/args.js +7 -2
- package/node_modules/@buddi/cli/dist/args.js.map +1 -1
- package/node_modules/@buddi/cli/dist/commands.d.ts.map +1 -1
- package/node_modules/@buddi/cli/dist/commands.js +11 -0
- package/node_modules/@buddi/cli/dist/commands.js.map +1 -1
- package/node_modules/@buddi/cli/dist/main.d.ts.map +1 -1
- package/node_modules/@buddi/cli/dist/main.js +2 -1
- package/node_modules/@buddi/cli/dist/main.js.map +1 -1
- package/node_modules/@buddi/gateway/dist/.tsbuildinfo +1 -1
- package/node_modules/@buddi/gateway/dist/missions/ask-cap.d.ts +26 -0
- package/node_modules/@buddi/gateway/dist/missions/ask-cap.d.ts.map +1 -0
- package/node_modules/@buddi/gateway/dist/missions/ask-cap.js +83 -0
- package/node_modules/@buddi/gateway/dist/missions/ask-cap.js.map +1 -0
- package/node_modules/@buddi/gateway/dist/missions/execute.d.ts +12 -1
- package/node_modules/@buddi/gateway/dist/missions/execute.d.ts.map +1 -1
- package/node_modules/@buddi/gateway/dist/missions/execute.js +73 -11
- package/node_modules/@buddi/gateway/dist/missions/execute.js.map +1 -1
- package/node_modules/@buddi/gateway/dist/missions/parked.d.ts +6 -0
- package/node_modules/@buddi/gateway/dist/missions/parked.d.ts.map +1 -1
- package/node_modules/@buddi/gateway/dist/missions/parked.js +7 -1
- package/node_modules/@buddi/gateway/dist/missions/parked.js.map +1 -1
- package/node_modules/@buddi/gateway/dist/serve.js +1 -1
- package/node_modules/@buddi/gateway/dist/serve.js.map +1 -1
- package/node_modules/@buddi/gateway/dist/system-context.d.ts +7 -0
- package/node_modules/@buddi/gateway/dist/system-context.d.ts.map +1 -1
- package/node_modules/@buddi/gateway/dist/system-context.js +8 -0
- package/node_modules/@buddi/gateway/dist/system-context.js.map +1 -1
- package/node_modules/@buddi/gateway/dist/web/api-routes.d.ts.map +1 -1
- package/node_modules/@buddi/gateway/dist/web/api-routes.js +11 -3
- package/node_modules/@buddi/gateway/dist/web/api-routes.js.map +1 -1
- package/node_modules/@buddi/gateway/dist/web/extension.d.ts +13 -1
- package/node_modules/@buddi/gateway/dist/web/extension.d.ts.map +1 -1
- package/node_modules/@buddi/gateway/dist/web/extension.js +125 -5
- package/node_modules/@buddi/gateway/dist/web/extension.js.map +1 -1
- package/node_modules/@buddi/gateway/dist/web/remote-hand.d.ts.map +1 -1
- package/node_modules/@buddi/gateway/dist/web/remote-hand.js +45 -2
- package/node_modules/@buddi/gateway/dist/web/remote-hand.js.map +1 -1
- package/node_modules/@buddi/gateway/dist/web/secrets.d.ts +26 -1
- package/node_modules/@buddi/gateway/dist/web/secrets.d.ts.map +1 -1
- package/node_modules/@buddi/gateway/dist/web/secrets.js +66 -3
- package/node_modules/@buddi/gateway/dist/web/secrets.js.map +1 -1
- package/node_modules/@buddi/gateway/dist/web/server.d.ts.map +1 -1
- package/node_modules/@buddi/gateway/dist/web/server.js +63 -7
- package/node_modules/@buddi/gateway/dist/web/server.js.map +1 -1
- package/node_modules/@buddi/gateway/dist/web/version.d.ts +2 -0
- package/node_modules/@buddi/gateway/dist/web/version.d.ts.map +1 -1
- package/node_modules/@buddi/gateway/dist/web/version.js +5 -1
- package/node_modules/@buddi/gateway/dist/web/version.js.map +1 -1
- package/node_modules/@buddi/install/dist/.tsbuildinfo +1 -1
- package/node_modules/@buddi/install/dist/app-layout.d.ts +127 -0
- package/node_modules/@buddi/install/dist/app-layout.d.ts.map +1 -0
- package/node_modules/@buddi/install/dist/app-layout.js +323 -0
- package/node_modules/@buddi/install/dist/app-layout.js.map +1 -0
- package/node_modules/@buddi/install/dist/index.d.ts +1 -0
- package/node_modules/@buddi/install/dist/index.d.ts.map +1 -1
- package/node_modules/@buddi/install/dist/index.js +1 -0
- package/node_modules/@buddi/install/dist/index.js.map +1 -1
- package/node_modules/@buddi/install/dist/launcher.d.ts.map +1 -1
- package/node_modules/@buddi/install/dist/launcher.js +18 -4
- package/node_modules/@buddi/install/dist/launcher.js.map +1 -1
- package/node_modules/@buddi/install/dist/provenance.d.ts +34 -0
- package/node_modules/@buddi/install/dist/provenance.d.ts.map +1 -0
- package/node_modules/@buddi/install/dist/provenance.js +200 -0
- package/node_modules/@buddi/install/dist/provenance.js.map +1 -0
- package/node_modules/@buddi/install/dist/supervisor.d.ts.map +1 -1
- package/node_modules/@buddi/install/dist/supervisor.js +9 -4
- package/node_modules/@buddi/install/dist/supervisor.js.map +1 -1
- package/node_modules/@buddi/install/dist/upgrade.d.ts +77 -3
- package/node_modules/@buddi/install/dist/upgrade.d.ts.map +1 -1
- package/node_modules/@buddi/install/dist/upgrade.js +325 -43
- package/node_modules/@buddi/install/dist/upgrade.js.map +1 -1
- package/node_modules/@buddi/tool-browser/dist/.tsbuildinfo +1 -1
- package/node_modules/@buddi/tool-browser/dist/controller.d.ts +6 -0
- package/node_modules/@buddi/tool-browser/dist/controller.d.ts.map +1 -1
- package/node_modules/@buddi/tool-browser/dist/controller.js +80 -6
- package/node_modules/@buddi/tool-browser/dist/controller.js.map +1 -1
- package/node_modules/@buddi/tool-browser/dist/driver.d.ts +3 -1
- package/node_modules/@buddi/tool-browser/dist/driver.d.ts.map +1 -1
- package/node_modules/@buddi/tool-browser/dist/driver.js +82 -1
- package/node_modules/@buddi/tool-browser/dist/driver.js.map +1 -1
- package/node_modules/@buddi/tool-browser/dist/extension.d.ts +60 -7
- package/node_modules/@buddi/tool-browser/dist/extension.d.ts.map +1 -1
- package/node_modules/@buddi/tool-browser/dist/extension.js +90 -13
- package/node_modules/@buddi/tool-browser/dist/extension.js.map +1 -1
- package/node_modules/@buddi/tool-browser/dist/index.d.ts +6 -4
- package/node_modules/@buddi/tool-browser/dist/index.d.ts.map +1 -1
- package/node_modules/@buddi/tool-browser/dist/index.js +7 -4
- package/node_modules/@buddi/tool-browser/dist/index.js.map +1 -1
- package/node_modules/@buddi/tool-browser/dist/logins.d.ts +150 -0
- package/node_modules/@buddi/tool-browser/dist/logins.d.ts.map +1 -0
- package/node_modules/@buddi/tool-browser/dist/logins.js +476 -0
- package/node_modules/@buddi/tool-browser/dist/logins.js.map +1 -0
- package/node_modules/@buddi/tool-browser/dist/routes.d.ts +22 -1
- package/node_modules/@buddi/tool-browser/dist/routes.d.ts.map +1 -1
- package/node_modules/@buddi/tool-browser/dist/routes.js +51 -3
- package/node_modules/@buddi/tool-browser/dist/routes.js.map +1 -1
- package/node_modules/@buddi/tool-browser/dist/secrets.d.ts +3 -1
- package/node_modules/@buddi/tool-browser/dist/secrets.d.ts.map +1 -1
- package/node_modules/@buddi/tool-browser/dist/secrets.js +10 -6
- package/node_modules/@buddi/tool-browser/dist/secrets.js.map +1 -1
- package/node_modules/@buddi/tool-browser/dist/service.d.ts +26 -2
- package/node_modules/@buddi/tool-browser/dist/service.d.ts.map +1 -1
- package/node_modules/@buddi/tool-browser/dist/service.js +117 -3
- package/node_modules/@buddi/tool-browser/dist/service.js.map +1 -1
- package/node_modules/@buddi/tool-browser/dist/types.d.ts +88 -0
- package/node_modules/@buddi/tool-browser/dist/types.d.ts.map +1 -1
- package/node_modules/@buddi/tool-browser/dist/types.js +18 -0
- package/node_modules/@buddi/tool-browser/dist/types.js.map +1 -1
- package/node_modules/hono/dist/cjs/hono-base.js +2 -0
- package/node_modules/hono/dist/cjs/middleware/language/language.js +1 -1
- package/node_modules/hono/dist/cjs/middleware/mount/index.js +71 -0
- package/node_modules/hono/dist/cjs/request.js +4 -4
- package/node_modules/hono/dist/hono-base.js +2 -0
- package/node_modules/hono/dist/middleware/mount/index.js +70 -0
- package/node_modules/hono/dist/request.js +4 -4
- package/node_modules/hono/dist/types/hono-base.d.ts +2 -0
- package/node_modules/hono/dist/types/jsx/base.d.ts +1 -1
- package/node_modules/hono/dist/types/middleware/mount/index.d.ts +46 -0
- package/node_modules/hono/dist/types/request.d.ts +4 -4
- package/node_modules/hono/package.json +9 -1
- package/package.json +2 -2
- package/packages/cli/dist/.tsbuildinfo +1 -1
- package/packages/cli/dist/access-cmd.d.ts +1 -1
- package/packages/cli/dist/access-cmd.d.ts.map +1 -1
- package/packages/cli/dist/access-cmd.js +23 -3
- package/packages/cli/dist/access-cmd.js.map +1 -1
- package/packages/cli/dist/args.d.ts +2 -2
- package/packages/cli/dist/args.d.ts.map +1 -1
- package/packages/cli/dist/args.js +7 -2
- package/packages/cli/dist/args.js.map +1 -1
- package/packages/cli/dist/commands.d.ts.map +1 -1
- package/packages/cli/dist/commands.js +11 -0
- package/packages/cli/dist/commands.js.map +1 -1
- package/packages/cli/dist/main.d.ts.map +1 -1
- package/packages/cli/dist/main.js +2 -1
- package/packages/cli/dist/main.js.map +1 -1
- package/packages/gateway/dist/.tsbuildinfo +1 -1
- package/packages/gateway/dist/missions/ask-cap.d.ts +26 -0
- package/packages/gateway/dist/missions/ask-cap.d.ts.map +1 -0
- package/packages/gateway/dist/missions/ask-cap.js +83 -0
- package/packages/gateway/dist/missions/ask-cap.js.map +1 -0
- package/packages/gateway/dist/missions/execute.d.ts +12 -1
- package/packages/gateway/dist/missions/execute.d.ts.map +1 -1
- package/packages/gateway/dist/missions/execute.js +73 -11
- package/packages/gateway/dist/missions/execute.js.map +1 -1
- package/packages/gateway/dist/missions/parked.d.ts +6 -0
- package/packages/gateway/dist/missions/parked.d.ts.map +1 -1
- package/packages/gateway/dist/missions/parked.js +7 -1
- package/packages/gateway/dist/missions/parked.js.map +1 -1
- package/packages/gateway/dist/serve.js +1 -1
- package/packages/gateway/dist/serve.js.map +1 -1
- package/packages/gateway/dist/system-context.d.ts +7 -0
- package/packages/gateway/dist/system-context.d.ts.map +1 -1
- package/packages/gateway/dist/system-context.js +8 -0
- package/packages/gateway/dist/system-context.js.map +1 -1
- package/packages/gateway/dist/web/api-routes.d.ts.map +1 -1
- package/packages/gateway/dist/web/api-routes.js +11 -3
- package/packages/gateway/dist/web/api-routes.js.map +1 -1
- package/packages/gateway/dist/web/extension.d.ts +13 -1
- package/packages/gateway/dist/web/extension.d.ts.map +1 -1
- package/packages/gateway/dist/web/extension.js +125 -5
- package/packages/gateway/dist/web/extension.js.map +1 -1
- package/packages/gateway/dist/web/remote-hand.d.ts.map +1 -1
- package/packages/gateway/dist/web/remote-hand.js +45 -2
- package/packages/gateway/dist/web/remote-hand.js.map +1 -1
- package/packages/gateway/dist/web/secrets.d.ts +26 -1
- package/packages/gateway/dist/web/secrets.d.ts.map +1 -1
- package/packages/gateway/dist/web/secrets.js +66 -3
- package/packages/gateway/dist/web/secrets.js.map +1 -1
- package/packages/gateway/dist/web/server.d.ts.map +1 -1
- package/packages/gateway/dist/web/server.js +63 -7
- package/packages/gateway/dist/web/server.js.map +1 -1
- package/packages/gateway/dist/web/version.d.ts +2 -0
- package/packages/gateway/dist/web/version.d.ts.map +1 -1
- package/packages/gateway/dist/web/version.js +5 -1
- package/packages/gateway/dist/web/version.js.map +1 -1
- package/packages/install/dist/.tsbuildinfo +1 -1
- package/packages/install/dist/app-layout.d.ts +127 -0
- package/packages/install/dist/app-layout.d.ts.map +1 -0
- package/packages/install/dist/app-layout.js +323 -0
- package/packages/install/dist/app-layout.js.map +1 -0
- package/packages/install/dist/index.d.ts +1 -0
- package/packages/install/dist/index.d.ts.map +1 -1
- package/packages/install/dist/index.js +1 -0
- package/packages/install/dist/index.js.map +1 -1
- package/packages/install/dist/launcher.d.ts.map +1 -1
- package/packages/install/dist/launcher.js +18 -4
- package/packages/install/dist/launcher.js.map +1 -1
- package/packages/install/dist/provenance.d.ts +34 -0
- package/packages/install/dist/provenance.d.ts.map +1 -0
- package/packages/install/dist/provenance.js +200 -0
- package/packages/install/dist/provenance.js.map +1 -0
- package/packages/install/dist/supervisor.d.ts.map +1 -1
- package/packages/install/dist/supervisor.js +9 -4
- package/packages/install/dist/supervisor.js.map +1 -1
- package/packages/install/dist/upgrade.d.ts +77 -3
- package/packages/install/dist/upgrade.d.ts.map +1 -1
- package/packages/install/dist/upgrade.js +325 -43
- package/packages/install/dist/upgrade.js.map +1 -1
- package/packages/tools/browser/dist/.tsbuildinfo +1 -1
- package/packages/tools/browser/dist/controller.d.ts +6 -0
- package/packages/tools/browser/dist/controller.d.ts.map +1 -1
- package/packages/tools/browser/dist/controller.js +80 -6
- package/packages/tools/browser/dist/controller.js.map +1 -1
- package/packages/tools/browser/dist/driver.d.ts +3 -1
- package/packages/tools/browser/dist/driver.d.ts.map +1 -1
- package/packages/tools/browser/dist/driver.js +82 -1
- package/packages/tools/browser/dist/driver.js.map +1 -1
- package/packages/tools/browser/dist/extension.d.ts +60 -7
- package/packages/tools/browser/dist/extension.d.ts.map +1 -1
- package/packages/tools/browser/dist/extension.js +90 -13
- package/packages/tools/browser/dist/extension.js.map +1 -1
- package/packages/tools/browser/dist/index.d.ts +6 -4
- package/packages/tools/browser/dist/index.d.ts.map +1 -1
- package/packages/tools/browser/dist/index.js +7 -4
- package/packages/tools/browser/dist/index.js.map +1 -1
- package/packages/tools/browser/dist/logins.d.ts +150 -0
- package/packages/tools/browser/dist/logins.d.ts.map +1 -0
- package/packages/tools/browser/dist/logins.js +476 -0
- package/packages/tools/browser/dist/logins.js.map +1 -0
- package/packages/tools/browser/dist/routes.d.ts +22 -1
- package/packages/tools/browser/dist/routes.d.ts.map +1 -1
- package/packages/tools/browser/dist/routes.js +51 -3
- package/packages/tools/browser/dist/routes.js.map +1 -1
- package/packages/tools/browser/dist/secrets.d.ts +3 -1
- package/packages/tools/browser/dist/secrets.d.ts.map +1 -1
- package/packages/tools/browser/dist/secrets.js +10 -6
- package/packages/tools/browser/dist/secrets.js.map +1 -1
- package/packages/tools/browser/dist/service.d.ts +26 -2
- package/packages/tools/browser/dist/service.d.ts.map +1 -1
- package/packages/tools/browser/dist/service.js +117 -3
- package/packages/tools/browser/dist/service.js.map +1 -1
- package/packages/tools/browser/dist/types.d.ts +88 -0
- package/packages/tools/browser/dist/types.d.ts.map +1 -1
- package/packages/tools/browser/dist/types.js +18 -0
- package/packages/tools/browser/dist/types.js.map +1 -1
- package/packages/web/dist/assets/index-DYfeUW-0.css +1 -0
- package/packages/web/dist/assets/index-vpc904_b.js +146 -0
- package/packages/web/dist/assets/{lottie_light-CuiSMqnl.js → lottie_light-BFAMYgLA.js} +1 -1
- package/packages/web/dist/build.json +1 -1
- package/packages/web/dist/index.html +2 -2
- package/packages/web/dist/precache.json +4 -4
- package/packages/web/dist/assets/index-Bac-CdD-.css +0 -1
- package/packages/web/dist/assets/index-DI8bp_D7.js +0 -146
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { FIELD_KIND } from './secrets.js';
|
|
2
|
+
/** How long a seen sign-in waits for the owner before its password is dropped. */
|
|
3
|
+
export declare const LOGIN_HOLD_MS: number;
|
|
4
|
+
/** Where a captured login may go: the field kind, first use asks the owner. */
|
|
5
|
+
export declare const LOGIN_RULE: "first-time";
|
|
6
|
+
export { FIELD_KIND as LOGIN_KIND };
|
|
7
|
+
export type LoginDecision = 'save' | 'later' | 'never';
|
|
8
|
+
/** What the page side saw. The password lives in this object and in the keeper's memory only. */
|
|
9
|
+
export interface SeenLogin {
|
|
10
|
+
/** The origin of the frame the form sat on, as the backend read it. */
|
|
11
|
+
origin: string;
|
|
12
|
+
username: string;
|
|
13
|
+
password: string;
|
|
14
|
+
}
|
|
15
|
+
/** What the owner is asked about: the site and the user name. Never the password. */
|
|
16
|
+
export interface LoginPrompt {
|
|
17
|
+
id: string;
|
|
18
|
+
sessionId: string;
|
|
19
|
+
site: string;
|
|
20
|
+
username: string;
|
|
21
|
+
/** A login kept for this site and user name already: the question is "Update the login for amazon.com?", and Save replaces its value. */
|
|
22
|
+
update?: true;
|
|
23
|
+
}
|
|
24
|
+
/** A login buddi kept for the owner: the secret's name, where it may go, and the label. */
|
|
25
|
+
export interface SavedLogin {
|
|
26
|
+
name: string;
|
|
27
|
+
site: string;
|
|
28
|
+
origin: string;
|
|
29
|
+
username: string;
|
|
30
|
+
savedAt: string;
|
|
31
|
+
/**
|
|
32
|
+
* HMAC-SHA256 of the password, hex, under this install's key (`LoginStoreKey`):
|
|
33
|
+
* on disk beside the label, so a sign-in with the same password stays quiet
|
|
34
|
+
* across restarts and a new one asks to update. Never the password; the key
|
|
35
|
+
* lives in the vault, not in this file. Absent on a login saved before it existed.
|
|
36
|
+
*/
|
|
37
|
+
mark?: string;
|
|
38
|
+
}
|
|
39
|
+
/** What the store gets: the secret's name and value, and where it may go. */
|
|
40
|
+
export interface LoginStoreInput {
|
|
41
|
+
name: string;
|
|
42
|
+
value: string;
|
|
43
|
+
bindings: Array<{
|
|
44
|
+
kind: typeof FIELD_KIND;
|
|
45
|
+
target: string;
|
|
46
|
+
rule: typeof LOGIN_RULE;
|
|
47
|
+
}>;
|
|
48
|
+
}
|
|
49
|
+
/** The owner-secret store, handed in by the gateway. */
|
|
50
|
+
export type LoginStore = (input: LoginStoreInput) => Promise<void>;
|
|
51
|
+
/** The names the owner-secret store holds now (names only), so a new login never takes one already there. */
|
|
52
|
+
export type LoginStoreNames = () => Promise<readonly string[]>;
|
|
53
|
+
/** This install's key for the saved passwords' marks, from the vault (made once). Null when there is no vault to keep one. */
|
|
54
|
+
export type LoginStoreKey = () => Promise<Buffer | null>;
|
|
55
|
+
/** What a sign-in seen in the owner's Chrome should be asked about, if anything. */
|
|
56
|
+
export type LoginAsk = 'save' | 'update' | 'none';
|
|
57
|
+
export type LoginOutcome = 'saved' | 'dismissed' | 'never' | 'gone';
|
|
58
|
+
/**
|
|
59
|
+
* The owner's word for a captured login, and what agents ask for it by. The
|
|
60
|
+
* login already kept for this site and user name keeps its name (Save then
|
|
61
|
+
* replaces its value); otherwise the first name that neither another kept
|
|
62
|
+
* login nor any other secret in the store (`existing`) holds.
|
|
63
|
+
*/
|
|
64
|
+
export declare function loginName(site: string, username: string, saved: readonly SavedLogin[], existing?: ReadonlySet<string>): string;
|
|
65
|
+
/** A user name as Settings says it: an address's mailbox and "@…", anything long cut short. */
|
|
66
|
+
export declare function shortUsername(username: string): string;
|
|
67
|
+
export declare class LoginKeeper {
|
|
68
|
+
#private;
|
|
69
|
+
readonly file: string | undefined;
|
|
70
|
+
readonly options: {
|
|
71
|
+
holdMs?: number;
|
|
72
|
+
now?: () => number;
|
|
73
|
+
};
|
|
74
|
+
constructor(file: string | undefined, options?: {
|
|
75
|
+
holdMs?: number;
|
|
76
|
+
now?: () => number;
|
|
77
|
+
});
|
|
78
|
+
/** The gateway's owner-secret store, the names it holds, and the install's key for the marks; without a store, Save says so. */
|
|
79
|
+
useStore(store: LoginStore, names?: LoginStoreNames, key?: LoginStoreKey): void;
|
|
80
|
+
/** The owner's Chrome asking before it asks the owner: what this sign-in is worth asking. The password is compared, never kept. */
|
|
81
|
+
check(login: SeenLogin): Promise<LoginAsk>;
|
|
82
|
+
get canSave(): boolean;
|
|
83
|
+
/** Read the labels and the never-list once. A missing or broken file is an empty one. */
|
|
84
|
+
load(): Promise<void>;
|
|
85
|
+
/** Hear every new question for the owner. Returns the unsubscribe. */
|
|
86
|
+
onSeen(listener: (prompt: LoginPrompt) => void): () => void;
|
|
87
|
+
/** Sites the owner said Never for. */
|
|
88
|
+
never(): string[];
|
|
89
|
+
/** Logins buddi kept, labels only. */
|
|
90
|
+
saved(): SavedLogin[];
|
|
91
|
+
/** The questions still open, for one page or all. */
|
|
92
|
+
pending(sessionId?: string): LoginPrompt[];
|
|
93
|
+
/**
|
|
94
|
+
* The owner's own browser saw a sign-in go out. Held for two minutes; the
|
|
95
|
+
* listeners hear the site and the user name. Undefined when there is
|
|
96
|
+
* nothing to ask: no password, a site on the never-list, or a login kept
|
|
97
|
+
* with this user name and this password (its mark says so). A kept login
|
|
98
|
+
* whose password differs, or one saved before marks existed, is asked about
|
|
99
|
+
* as an update.
|
|
100
|
+
*/
|
|
101
|
+
seen(sessionId: string, login: SeenLogin): Promise<LoginPrompt | undefined>;
|
|
102
|
+
/**
|
|
103
|
+
* The owner's answer to one question. `gone` when its two minutes are up. A
|
|
104
|
+
* Save the store refuses throws and leaves the question open, so the owner
|
|
105
|
+
* can try again while the two minutes last.
|
|
106
|
+
*/
|
|
107
|
+
decide(id: string, decision: LoginDecision): Promise<{
|
|
108
|
+
outcome: LoginOutcome;
|
|
109
|
+
saved?: SavedLogin;
|
|
110
|
+
}>;
|
|
111
|
+
/**
|
|
112
|
+
* A sign-in the owner already answered in their own Chrome's tab: Save
|
|
113
|
+
* with the pair, Never with the site alone.
|
|
114
|
+
*/
|
|
115
|
+
decided(login: Omit<SeenLogin, 'password'> & {
|
|
116
|
+
password?: string;
|
|
117
|
+
}, decision: 'save' | 'never'): Promise<{
|
|
118
|
+
outcome: LoginOutcome;
|
|
119
|
+
saved?: SavedLogin;
|
|
120
|
+
}>;
|
|
121
|
+
/** The secret behind a label is gone (Settings → Remove): drop the label. */
|
|
122
|
+
forget(name: string): Promise<boolean>;
|
|
123
|
+
/** The secret behind a label was renamed (Settings → Rename): the label follows it. */
|
|
124
|
+
rename(from: string, to: string): Promise<boolean>;
|
|
125
|
+
/** Every held password goes. */
|
|
126
|
+
clear(): void;
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Watch one document for a sign-in going out, and report the pair.
|
|
130
|
+
*
|
|
131
|
+
* Serialised into the page (buddi's own browser runs it in an isolated world
|
|
132
|
+
* the page's scripts cannot see), so it is self-contained: it reads the DOM it
|
|
133
|
+
* shares with the page and touches nothing. A sign-in is a form with a filled
|
|
134
|
+
* password field submitted, Enter in a field of it, a click on its button, or
|
|
135
|
+
* a fetch right after the password changed that left the field gone or empty
|
|
136
|
+
* (a page that signs in without a form). A click on a show-password eye, a
|
|
137
|
+
* Forgot link or Cancel is not one.
|
|
138
|
+
*/
|
|
139
|
+
export declare function watchLogins(report: (found: {
|
|
140
|
+
username: string;
|
|
141
|
+
password: string;
|
|
142
|
+
}) => void): void;
|
|
143
|
+
/** The binding the isolated world reports through, and its world's name. */
|
|
144
|
+
export declare const LOGIN_BINDING = "__buddiLoginSeen";
|
|
145
|
+
export declare const LOGIN_WORLD = "buddi-login-watch";
|
|
146
|
+
/** The script buddi's own browser runs in that world: the watcher, reporting through the binding with the frame's own origin. */
|
|
147
|
+
export declare function loginWatchSource(): string;
|
|
148
|
+
/** The binding's payload, read strictly. Undefined for anything else. */
|
|
149
|
+
export declare function readLoginPayload(payload: unknown): SeenLogin | undefined;
|
|
150
|
+
//# sourceMappingURL=logins.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"logins.d.ts","sourceRoot":"","sources":["../src/logins.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,476 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A sign-in the owner just made, kept for them by buddi itself
|
|
3
|
+
* (docs/browser.md, "Saving a sign-in"; docs/owner-secrets.md §6).
|
|
4
|
+
*
|
|
5
|
+
* While the owner holds a page — the remote hand on buddi's own browser, or a
|
|
6
|
+
* held tab in their Chrome — the page side watches for a credential form
|
|
7
|
+
* going out: a form with a password field submitted (Enter or a click), or a
|
|
8
|
+
* fetch right after the password changed with the field gone. What it saw, the
|
|
9
|
+
* user name and the password, comes here and nowhere else: not to a model, not
|
|
10
|
+
* to a transcript, not to a tool result, not to a log line.
|
|
11
|
+
*
|
|
12
|
+
* Buddi's own browser asks the owner afterwards: the pair waits in this
|
|
13
|
+
* process for two minutes while the Page tab shows "Save this login for
|
|
14
|
+
* amazon.com?" (the socket carries the site and the user name, never the
|
|
15
|
+
* password), and goes when the owner decides or the two minutes are up. In
|
|
16
|
+
* the owner's Chrome the question is asked in the tab itself, so what arrives
|
|
17
|
+
* here has already been decided.
|
|
18
|
+
*
|
|
19
|
+
* Save hands the pair to the owner-secret store the gateway gave this keeper
|
|
20
|
+
* (`useStore`): the password as the secret `login · amazon.com`, bound as
|
|
21
|
+
* `browser.field` to the origin the form sat on — the same binding the sign-in
|
|
22
|
+
* card's "Save a login for next time" makes, so `secret.fill` finds it — and
|
|
23
|
+
* the user name kept beside it as a label, which is not a secret. Never
|
|
24
|
+
* remembers the site. The labels and the never-list are a small file in the
|
|
25
|
+
* plugin's directory; the password is never written by this module.
|
|
26
|
+
*/
|
|
27
|
+
import { createHmac, randomBytes, randomUUID, timingSafeEqual } from 'node:crypto';
|
|
28
|
+
import { mkdir, readFile, rename, writeFile } from 'node:fs/promises';
|
|
29
|
+
import path from 'node:path';
|
|
30
|
+
import { siteListed, siteOf } from './routes.js';
|
|
31
|
+
import { canonicalOrigin, FIELD_KIND } from './secrets.js';
|
|
32
|
+
/** How long a seen sign-in waits for the owner before its password is dropped. */
|
|
33
|
+
export const LOGIN_HOLD_MS = 2 * 60_000;
|
|
34
|
+
/** Where a captured login may go: the field kind, first use asks the owner. */
|
|
35
|
+
export const LOGIN_RULE = 'first-time';
|
|
36
|
+
export { FIELD_KIND as LOGIN_KIND };
|
|
37
|
+
/**
|
|
38
|
+
* The owner's word for a captured login, and what agents ask for it by. The
|
|
39
|
+
* login already kept for this site and user name keeps its name (Save then
|
|
40
|
+
* replaces its value); otherwise the first name that neither another kept
|
|
41
|
+
* login nor any other secret in the store (`existing`) holds.
|
|
42
|
+
*/
|
|
43
|
+
export function loginName(site, username, saved, existing = new Set()) {
|
|
44
|
+
const mine = saved.find((login) => login.site === site && login.username === username);
|
|
45
|
+
if (mine)
|
|
46
|
+
return mine.name;
|
|
47
|
+
const free = (name) => !saved.some((login) => login.name === name) && !existing.has(name);
|
|
48
|
+
const plain = `login · ${site}`;
|
|
49
|
+
if (free(plain))
|
|
50
|
+
return plain;
|
|
51
|
+
const named = `login · ${site} · ${username}`.slice(0, 120);
|
|
52
|
+
if (free(named))
|
|
53
|
+
return named;
|
|
54
|
+
for (let n = 2; n < 100; n++) {
|
|
55
|
+
const numbered = `${named.slice(0, 112)} · ${n}`;
|
|
56
|
+
if (free(numbered))
|
|
57
|
+
return numbered;
|
|
58
|
+
}
|
|
59
|
+
return `${named.slice(0, 100)} · ${randomUUID().slice(0, 8)}`;
|
|
60
|
+
}
|
|
61
|
+
/** A user name as Settings says it: an address's mailbox and "@…", anything long cut short. */
|
|
62
|
+
export function shortUsername(username) {
|
|
63
|
+
const at = username.indexOf('@');
|
|
64
|
+
if (at > 0)
|
|
65
|
+
return `${username.slice(0, Math.min(at, 24))}@…`;
|
|
66
|
+
return username.length > 24 ? `${username.slice(0, 23)}…` : username;
|
|
67
|
+
}
|
|
68
|
+
const MAX_USERNAME = 200;
|
|
69
|
+
const MAX_PASSWORD = 1024;
|
|
70
|
+
/** A seen login worth asking about: an http(s) origin, a password, a sane user name. */
|
|
71
|
+
function usable(login) {
|
|
72
|
+
const origin = canonicalOrigin(login.origin);
|
|
73
|
+
if (!origin)
|
|
74
|
+
return undefined;
|
|
75
|
+
if (typeof login.password !== 'string' || login.password === '' || login.password.length > MAX_PASSWORD)
|
|
76
|
+
return undefined;
|
|
77
|
+
const username = typeof login.username === 'string' ? login.username.trim().slice(0, MAX_USERNAME) : '';
|
|
78
|
+
if (/[\u0000-\u001f\u007f]/.test(username))
|
|
79
|
+
return undefined;
|
|
80
|
+
const site = siteOf(origin);
|
|
81
|
+
return site ? { origin, site, username } : undefined;
|
|
82
|
+
}
|
|
83
|
+
export class LoginKeeper {
|
|
84
|
+
file;
|
|
85
|
+
options;
|
|
86
|
+
#pending = new Map();
|
|
87
|
+
#listeners = new Set();
|
|
88
|
+
#store;
|
|
89
|
+
#names;
|
|
90
|
+
#state = { saved: [], never: [] };
|
|
91
|
+
#loaded;
|
|
92
|
+
#writing = Promise.resolve();
|
|
93
|
+
/** One save at a time per site: the name is picked and stored before the next save there looks. */
|
|
94
|
+
#saving = new Map();
|
|
95
|
+
#keyFor;
|
|
96
|
+
/** The install's key once read; a key that could not be read is not remembered, so the next sign-in tries again. */
|
|
97
|
+
#key;
|
|
98
|
+
/**
|
|
99
|
+
* Marks made with a key of this process's own, when the vault had none to
|
|
100
|
+
* give: in memory only, so they never pass for the install's.
|
|
101
|
+
*/
|
|
102
|
+
#fallbackMarks = new Map();
|
|
103
|
+
#fallbackKey = randomBytes(32);
|
|
104
|
+
constructor(file, options = {}) {
|
|
105
|
+
this.file = file;
|
|
106
|
+
this.options = options;
|
|
107
|
+
}
|
|
108
|
+
/** The gateway's owner-secret store, the names it holds, and the install's key for the marks; without a store, Save says so. */
|
|
109
|
+
useStore(store, names, key) { this.#store = store; this.#names = names; this.#keyFor = key; this.#key = undefined; }
|
|
110
|
+
async #installKey() {
|
|
111
|
+
if (this.#key)
|
|
112
|
+
return this.#key;
|
|
113
|
+
try {
|
|
114
|
+
const key = await this.#keyFor?.();
|
|
115
|
+
if (key && key.length >= 16)
|
|
116
|
+
this.#key = key;
|
|
117
|
+
}
|
|
118
|
+
catch { /* no vault right now: this process's own key, in memory */ }
|
|
119
|
+
return this.#key;
|
|
120
|
+
}
|
|
121
|
+
/** The password's mark: the install's (kept on disk) when there is a key, else this process's (memory only). */
|
|
122
|
+
async #markOf(password) {
|
|
123
|
+
const key = await this.#installKey();
|
|
124
|
+
return { mark: createHmac('sha256', key ?? this.#fallbackKey).update(password).digest(), durable: key !== undefined };
|
|
125
|
+
}
|
|
126
|
+
/** True when this password is the one kept for the login, as far as its mark says; undefined when no mark says either way. */
|
|
127
|
+
async #samePassword(kept, password) {
|
|
128
|
+
const { mark, durable } = await this.#markOf(password);
|
|
129
|
+
const known = durable && typeof kept.mark === 'string' && /^[0-9a-f]{64}$/.test(kept.mark) ? Buffer.from(kept.mark, 'hex') : !durable ? this.#fallbackMarks.get(kept.name) : undefined;
|
|
130
|
+
if (!known)
|
|
131
|
+
return undefined;
|
|
132
|
+
return timingSafeEqual(known, mark);
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* What to ask about a sign-in: nothing (no password, a never-listed site,
|
|
136
|
+
* the kept password again), Save (no login kept for this user name), or
|
|
137
|
+
* Update (a kept login whose password differs, or one saved before marks
|
|
138
|
+
* existed — asked once, then its mark is kept on Save).
|
|
139
|
+
*/
|
|
140
|
+
async #assess(login) {
|
|
141
|
+
const facts = usable(login);
|
|
142
|
+
if (!facts || siteListed(facts.site, this.#state.never))
|
|
143
|
+
return undefined;
|
|
144
|
+
const kept = this.#kept(facts.site, facts.username);
|
|
145
|
+
if (!kept)
|
|
146
|
+
return { facts, ask: 'save' };
|
|
147
|
+
return (await this.#samePassword(kept, login.password)) === true ? undefined : { facts, ask: 'update' };
|
|
148
|
+
}
|
|
149
|
+
/** The owner's Chrome asking before it asks the owner: what this sign-in is worth asking. The password is compared, never kept. */
|
|
150
|
+
async check(login) {
|
|
151
|
+
await this.load();
|
|
152
|
+
return (await this.#assess(login))?.ask ?? 'none';
|
|
153
|
+
}
|
|
154
|
+
get canSave() { return this.#store !== undefined; }
|
|
155
|
+
/** Read the labels and the never-list once. A missing or broken file is an empty one. */
|
|
156
|
+
load() {
|
|
157
|
+
this.#loaded ??= (async () => {
|
|
158
|
+
if (!this.file)
|
|
159
|
+
return;
|
|
160
|
+
try {
|
|
161
|
+
const raw = JSON.parse(await readFile(this.file, 'utf8'));
|
|
162
|
+
this.#state = {
|
|
163
|
+
saved: Array.isArray(raw.saved) ? raw.saved.filter((login) => typeof login?.name === 'string' && typeof login.site === 'string') : [],
|
|
164
|
+
never: Array.isArray(raw.never) ? raw.never.filter((site) => typeof site === 'string') : [],
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
catch { /* none yet */ }
|
|
168
|
+
})();
|
|
169
|
+
return this.#loaded;
|
|
170
|
+
}
|
|
171
|
+
#persist() {
|
|
172
|
+
const file = this.file;
|
|
173
|
+
if (!file)
|
|
174
|
+
return Promise.resolve();
|
|
175
|
+
const snapshot = JSON.stringify(this.#state);
|
|
176
|
+
const next = this.#writing.catch(() => undefined).then(async () => {
|
|
177
|
+
await mkdir(path.dirname(file), { recursive: true, mode: 0o700 });
|
|
178
|
+
const temp = `${file}.${randomUUID()}.tmp`;
|
|
179
|
+
await writeFile(temp, snapshot, { mode: 0o600 });
|
|
180
|
+
await rename(temp, file);
|
|
181
|
+
});
|
|
182
|
+
this.#writing = next;
|
|
183
|
+
return next;
|
|
184
|
+
}
|
|
185
|
+
/** Hear every new question for the owner. Returns the unsubscribe. */
|
|
186
|
+
onSeen(listener) {
|
|
187
|
+
this.#listeners.add(listener);
|
|
188
|
+
return () => { this.#listeners.delete(listener); };
|
|
189
|
+
}
|
|
190
|
+
/** Sites the owner said Never for. */
|
|
191
|
+
never() { return [...this.#state.never]; }
|
|
192
|
+
/** Logins buddi kept, labels only. */
|
|
193
|
+
saved() { return this.#state.saved.map(({ mark: _mark, ...login }) => ({ ...login })); }
|
|
194
|
+
/** The questions still open, for one page or all. */
|
|
195
|
+
pending(sessionId) {
|
|
196
|
+
return [...this.#pending.values()].filter((entry) => sessionId === undefined || entry.prompt.sessionId === sessionId).map((entry) => ({ ...entry.prompt }));
|
|
197
|
+
}
|
|
198
|
+
/** The login already kept for this site and user name, if any. */
|
|
199
|
+
#kept(site, username) {
|
|
200
|
+
return this.#state.saved.find((login) => login.site === site && login.username === username);
|
|
201
|
+
}
|
|
202
|
+
#expire(id) {
|
|
203
|
+
const timer = setTimeout(() => { this.#pending.delete(id); }, this.options.holdMs ?? LOGIN_HOLD_MS);
|
|
204
|
+
timer.unref?.();
|
|
205
|
+
return timer;
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* The owner's own browser saw a sign-in go out. Held for two minutes; the
|
|
209
|
+
* listeners hear the site and the user name. Undefined when there is
|
|
210
|
+
* nothing to ask: no password, a site on the never-list, or a login kept
|
|
211
|
+
* with this user name and this password (its mark says so). A kept login
|
|
212
|
+
* whose password differs, or one saved before marks existed, is asked about
|
|
213
|
+
* as an update.
|
|
214
|
+
*/
|
|
215
|
+
async seen(sessionId, login) {
|
|
216
|
+
await this.load();
|
|
217
|
+
const assessed = await this.#assess(login);
|
|
218
|
+
if (!assessed)
|
|
219
|
+
return undefined;
|
|
220
|
+
const { facts } = assessed;
|
|
221
|
+
const kept = assessed.ask === 'update';
|
|
222
|
+
// The same sign-in again (a second click on the button): one question, the newest password, two fresh minutes.
|
|
223
|
+
const again = [...this.#pending.values()].find((entry) => entry.prompt.sessionId === sessionId && entry.prompt.site === facts.site && entry.prompt.username === facts.username);
|
|
224
|
+
if (again) {
|
|
225
|
+
again.password = login.password;
|
|
226
|
+
again.origin = facts.origin;
|
|
227
|
+
clearTimeout(again.timer);
|
|
228
|
+
again.timer = this.#expire(again.prompt.id);
|
|
229
|
+
return { ...again.prompt };
|
|
230
|
+
}
|
|
231
|
+
const prompt = { id: randomUUID(), sessionId, site: facts.site, username: facts.username, ...(kept ? { update: true } : {}) };
|
|
232
|
+
this.#pending.set(prompt.id, { prompt, origin: facts.origin, password: login.password, timer: this.#expire(prompt.id) });
|
|
233
|
+
for (const listener of this.#listeners) {
|
|
234
|
+
try {
|
|
235
|
+
listener({ ...prompt });
|
|
236
|
+
}
|
|
237
|
+
catch { /* a listener never decides */ }
|
|
238
|
+
}
|
|
239
|
+
return { ...prompt };
|
|
240
|
+
}
|
|
241
|
+
#take(id) {
|
|
242
|
+
const entry = this.#pending.get(id);
|
|
243
|
+
if (!entry)
|
|
244
|
+
return undefined;
|
|
245
|
+
this.#pending.delete(id);
|
|
246
|
+
clearTimeout(entry.timer);
|
|
247
|
+
return entry;
|
|
248
|
+
}
|
|
249
|
+
/**
|
|
250
|
+
* The owner's answer to one question. `gone` when its two minutes are up. A
|
|
251
|
+
* Save the store refuses throws and leaves the question open, so the owner
|
|
252
|
+
* can try again while the two minutes last.
|
|
253
|
+
*/
|
|
254
|
+
async decide(id, decision) {
|
|
255
|
+
await this.load();
|
|
256
|
+
const entry = this.#take(id);
|
|
257
|
+
if (!entry)
|
|
258
|
+
return { outcome: 'gone' };
|
|
259
|
+
try {
|
|
260
|
+
return await this.#apply(entry.prompt.site, entry.origin, entry.prompt.username, entry.password, decision);
|
|
261
|
+
}
|
|
262
|
+
catch (error) {
|
|
263
|
+
if (!this.#pending.has(id))
|
|
264
|
+
this.#pending.set(id, { ...entry, timer: this.#expire(id) });
|
|
265
|
+
throw error;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
/**
|
|
269
|
+
* A sign-in the owner already answered in their own Chrome's tab: Save
|
|
270
|
+
* with the pair, Never with the site alone.
|
|
271
|
+
*/
|
|
272
|
+
async decided(login, decision) {
|
|
273
|
+
await this.load();
|
|
274
|
+
const facts = usable({ ...login, password: login.password ?? (decision === 'never' ? '-' : '') });
|
|
275
|
+
if (!facts)
|
|
276
|
+
return { outcome: 'gone' };
|
|
277
|
+
return this.#apply(facts.site, facts.origin, facts.username, login.password ?? '', decision);
|
|
278
|
+
}
|
|
279
|
+
async #apply(site, origin, username, password, decision) {
|
|
280
|
+
if (decision === 'later')
|
|
281
|
+
return { outcome: 'dismissed' };
|
|
282
|
+
if (decision === 'never') {
|
|
283
|
+
if (!siteListed(site, this.#state.never))
|
|
284
|
+
this.#state.never = [...this.#state.never, site].slice(-500);
|
|
285
|
+
for (const [id, entry] of this.#pending)
|
|
286
|
+
if (entry.prompt.site === site)
|
|
287
|
+
this.#take(id);
|
|
288
|
+
await this.#persist();
|
|
289
|
+
return { outcome: 'never' };
|
|
290
|
+
}
|
|
291
|
+
const store = this.#store;
|
|
292
|
+
if (!store)
|
|
293
|
+
throw new Error('This buddi has nowhere to keep a login right now.');
|
|
294
|
+
if (password === '')
|
|
295
|
+
return { outcome: 'gone' };
|
|
296
|
+
// One site at a time: two accounts saved at once must not pick the same name, or the second replaces the first.
|
|
297
|
+
const before = this.#saving.get(site) ?? Promise.resolve();
|
|
298
|
+
const run = before.catch(() => undefined).then(async () => {
|
|
299
|
+
const existing = new Set(this.#names ? await this.#names() : []);
|
|
300
|
+
const name = loginName(site, username, this.#state.saved, existing);
|
|
301
|
+
await store({ name, value: password, bindings: [{ kind: FIELD_KIND, target: origin, rule: LOGIN_RULE }] });
|
|
302
|
+
const { mark, durable } = await this.#markOf(password);
|
|
303
|
+
if (!durable)
|
|
304
|
+
this.#fallbackMarks.set(name, mark);
|
|
305
|
+
const saved = { name, site, origin, username, savedAt: new Date(this.options.now?.() ?? Date.now()).toISOString(), ...(durable ? { mark: mark.toString('hex') } : {}) };
|
|
306
|
+
this.#state.saved = [...this.#state.saved.filter((login) => login.name !== name), saved].slice(-500);
|
|
307
|
+
await this.#persist();
|
|
308
|
+
const { mark: _mark, ...label } = saved;
|
|
309
|
+
return { outcome: 'saved', saved: label };
|
|
310
|
+
});
|
|
311
|
+
this.#saving.set(site, run);
|
|
312
|
+
try {
|
|
313
|
+
return await run;
|
|
314
|
+
}
|
|
315
|
+
finally {
|
|
316
|
+
if (this.#saving.get(site) === run)
|
|
317
|
+
this.#saving.delete(site);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
/** The secret behind a label is gone (Settings → Remove): drop the label. */
|
|
321
|
+
async forget(name) {
|
|
322
|
+
await this.load();
|
|
323
|
+
const before = this.#state.saved.length;
|
|
324
|
+
this.#state.saved = this.#state.saved.filter((login) => login.name !== name);
|
|
325
|
+
this.#fallbackMarks.delete(name);
|
|
326
|
+
if (this.#state.saved.length === before)
|
|
327
|
+
return false;
|
|
328
|
+
await this.#persist();
|
|
329
|
+
return true;
|
|
330
|
+
}
|
|
331
|
+
/** The secret behind a label was renamed (Settings → Rename): the label follows it. */
|
|
332
|
+
async rename(from, to) {
|
|
333
|
+
await this.load();
|
|
334
|
+
const login = this.#state.saved.find((entry) => entry.name === from);
|
|
335
|
+
if (!login || from === to)
|
|
336
|
+
return false;
|
|
337
|
+
this.#state.saved = [...this.#state.saved.filter((entry) => entry.name !== from && entry.name !== to), { ...login, name: to }];
|
|
338
|
+
const mark = this.#fallbackMarks.get(from);
|
|
339
|
+
this.#fallbackMarks.delete(from);
|
|
340
|
+
if (mark)
|
|
341
|
+
this.#fallbackMarks.set(to, mark);
|
|
342
|
+
await this.#persist();
|
|
343
|
+
return true;
|
|
344
|
+
}
|
|
345
|
+
/** Every held password goes. */
|
|
346
|
+
clear() {
|
|
347
|
+
for (const id of [...this.#pending.keys()])
|
|
348
|
+
this.#take(id);
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
/* ---------------- the page side ---------------- */
|
|
352
|
+
/**
|
|
353
|
+
* Watch one document for a sign-in going out, and report the pair.
|
|
354
|
+
*
|
|
355
|
+
* Serialised into the page (buddi's own browser runs it in an isolated world
|
|
356
|
+
* the page's scripts cannot see), so it is self-contained: it reads the DOM it
|
|
357
|
+
* shares with the page and touches nothing. A sign-in is a form with a filled
|
|
358
|
+
* password field submitted, Enter in a field of it, a click on its button, or
|
|
359
|
+
* a fetch right after the password changed that left the field gone or empty
|
|
360
|
+
* (a page that signs in without a form). A click on a show-password eye, a
|
|
361
|
+
* Forgot link or Cancel is not one.
|
|
362
|
+
*/
|
|
363
|
+
export function watchLogins(report) {
|
|
364
|
+
const holder = globalThis;
|
|
365
|
+
const existing = holder['__buddiLoginWatch'];
|
|
366
|
+
if (existing) {
|
|
367
|
+
existing.report = report;
|
|
368
|
+
return;
|
|
369
|
+
}
|
|
370
|
+
const doc = globalThis.document;
|
|
371
|
+
if (!doc)
|
|
372
|
+
return;
|
|
373
|
+
const state = { report, last: '', lastAt: 0, typedAt: 0, cached: undefined, timer: undefined };
|
|
374
|
+
holder['__buddiLoginWatch'] = state;
|
|
375
|
+
const NOT_SUBMIT = /\b(show|hide|reveal|toggle|eye|forgot|reset|cancel|back|close|sign ?up|register|create)\b/i;
|
|
376
|
+
const USER = /user|email|login|account|ident|phone|mail/i;
|
|
377
|
+
const passwords = (root) => Array.from(root.querySelectorAll('input[type="password" i]'));
|
|
378
|
+
const usernameFor = (password) => {
|
|
379
|
+
const scope = password.form ?? doc;
|
|
380
|
+
const inputs = Array.from(scope.querySelectorAll('input'))
|
|
381
|
+
.filter((input) => ['text', 'email', 'tel', ''].includes((input.getAttribute('type') ?? '').toLowerCase()) && input.value.trim() !== '');
|
|
382
|
+
const marked = inputs.find((input) => (input.getAttribute('autocomplete') ?? '').toLowerCase().includes('username') || (input.getAttribute('type') ?? '').toLowerCase() === 'email');
|
|
383
|
+
if (marked)
|
|
384
|
+
return marked.value.trim();
|
|
385
|
+
const before = inputs.filter((input) => (input.compareDocumentPosition(password) & 4) !== 0);
|
|
386
|
+
const pick = before[before.length - 1] ?? inputs.find((input) => USER.test(`${input.name} ${input.id} ${input.getAttribute('autocomplete') ?? ''}`));
|
|
387
|
+
return pick ? pick.value.trim() : '';
|
|
388
|
+
};
|
|
389
|
+
const read = (scope) => {
|
|
390
|
+
const field = passwords(scope ?? doc).find((input) => input.value !== '');
|
|
391
|
+
return field ? { username: usernameFor(field), password: field.value } : undefined;
|
|
392
|
+
};
|
|
393
|
+
const send = (found) => {
|
|
394
|
+
if (!found || found.password === '')
|
|
395
|
+
return;
|
|
396
|
+
const key = `${found.username}\u0000${found.password}`;
|
|
397
|
+
const now = Date.now();
|
|
398
|
+
if (key === state.last && now - state.lastAt < 5_000)
|
|
399
|
+
return;
|
|
400
|
+
state.last = key;
|
|
401
|
+
state.lastAt = now;
|
|
402
|
+
state.cached = undefined;
|
|
403
|
+
try {
|
|
404
|
+
state.report(found);
|
|
405
|
+
}
|
|
406
|
+
catch { /* the reporter is buddi's; a failure is not the page's business */ }
|
|
407
|
+
};
|
|
408
|
+
doc.addEventListener('submit', (event) => { send(read(event.target) ?? state.cached); }, true);
|
|
409
|
+
doc.addEventListener('keydown', (event) => {
|
|
410
|
+
if (event.key !== 'Enter')
|
|
411
|
+
return;
|
|
412
|
+
const target = event.target;
|
|
413
|
+
if (!target || target.tagName !== 'INPUT')
|
|
414
|
+
return;
|
|
415
|
+
send(read(target.form ?? doc));
|
|
416
|
+
}, true);
|
|
417
|
+
doc.addEventListener('click', (event) => {
|
|
418
|
+
const target = event.target;
|
|
419
|
+
const button = target?.closest?.('button, input[type="submit" i], input[type="image" i], input[type="button" i], [role="button"]');
|
|
420
|
+
if (!button)
|
|
421
|
+
return;
|
|
422
|
+
const words = `${button.textContent ?? ''} ${button.getAttribute('aria-label') ?? ''} ${button.getAttribute('value') ?? ''} ${button.getAttribute('title') ?? ''}`;
|
|
423
|
+
if (NOT_SUBMIT.test(words))
|
|
424
|
+
return;
|
|
425
|
+
send(read(button.form ?? doc));
|
|
426
|
+
}, true);
|
|
427
|
+
doc.addEventListener('input', (event) => {
|
|
428
|
+
const target = event.target;
|
|
429
|
+
if ((target?.getAttribute?.('type') ?? '').toLowerCase() !== 'password')
|
|
430
|
+
return;
|
|
431
|
+
state.typedAt = Date.now();
|
|
432
|
+
state.cached = read(target.form ?? doc);
|
|
433
|
+
}, true);
|
|
434
|
+
const Observer = globalThis.PerformanceObserver;
|
|
435
|
+
if (typeof Observer === 'function') {
|
|
436
|
+
try {
|
|
437
|
+
new Observer((list) => {
|
|
438
|
+
if (!state.cached || Date.now() - state.typedAt > 10_000)
|
|
439
|
+
return;
|
|
440
|
+
const fetched = list.getEntries().some((entry) => ['fetch', 'xmlhttprequest'].includes(entry.initiatorType));
|
|
441
|
+
if (!fetched)
|
|
442
|
+
return;
|
|
443
|
+
if (state.timer)
|
|
444
|
+
clearTimeout(state.timer);
|
|
445
|
+
state.timer = setTimeout(() => {
|
|
446
|
+
const still = passwords(doc).some((input) => input.isConnected && input.value !== '' && input.getClientRects().length > 0);
|
|
447
|
+
if (!still)
|
|
448
|
+
send(state.cached);
|
|
449
|
+
}, 1_500);
|
|
450
|
+
}).observe({ type: 'resource', buffered: false });
|
|
451
|
+
}
|
|
452
|
+
catch { /* no resource timing here */ }
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
/** The binding the isolated world reports through, and its world's name. */
|
|
456
|
+
export const LOGIN_BINDING = '__buddiLoginSeen';
|
|
457
|
+
export const LOGIN_WORLD = 'buddi-login-watch';
|
|
458
|
+
/** The script buddi's own browser runs in that world: the watcher, reporting through the binding with the frame's own origin. */
|
|
459
|
+
export function loginWatchSource() {
|
|
460
|
+
return `(${watchLogins.toString()})((found) => { try { globalThis[${JSON.stringify(LOGIN_BINDING)}](JSON.stringify({ origin: location.origin, username: found.username, password: found.password })); } catch {} });`;
|
|
461
|
+
}
|
|
462
|
+
/** The binding's payload, read strictly. Undefined for anything else. */
|
|
463
|
+
export function readLoginPayload(payload) {
|
|
464
|
+
if (typeof payload !== 'string' || payload.length > 4096)
|
|
465
|
+
return undefined;
|
|
466
|
+
try {
|
|
467
|
+
const parsed = JSON.parse(payload);
|
|
468
|
+
if (typeof parsed.origin !== 'string' || typeof parsed.username !== 'string' || typeof parsed.password !== 'string')
|
|
469
|
+
return undefined;
|
|
470
|
+
return { origin: parsed.origin, username: parsed.username, password: parsed.password };
|
|
471
|
+
}
|
|
472
|
+
catch {
|
|
473
|
+
return undefined;
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
//# sourceMappingURL=logins.js.map
|