@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.
Files changed (253) hide show
  1. package/extension/background.js +635 -21
  2. package/extension/manifest.json +2 -2
  3. package/node_modules/@buddi/cli/dist/.tsbuildinfo +1 -1
  4. package/node_modules/@buddi/cli/dist/access-cmd.d.ts +1 -1
  5. package/node_modules/@buddi/cli/dist/access-cmd.d.ts.map +1 -1
  6. package/node_modules/@buddi/cli/dist/access-cmd.js +23 -3
  7. package/node_modules/@buddi/cli/dist/access-cmd.js.map +1 -1
  8. package/node_modules/@buddi/cli/dist/args.d.ts +2 -2
  9. package/node_modules/@buddi/cli/dist/args.d.ts.map +1 -1
  10. package/node_modules/@buddi/cli/dist/args.js +7 -2
  11. package/node_modules/@buddi/cli/dist/args.js.map +1 -1
  12. package/node_modules/@buddi/cli/dist/commands.d.ts.map +1 -1
  13. package/node_modules/@buddi/cli/dist/commands.js +11 -0
  14. package/node_modules/@buddi/cli/dist/commands.js.map +1 -1
  15. package/node_modules/@buddi/cli/dist/main.d.ts.map +1 -1
  16. package/node_modules/@buddi/cli/dist/main.js +2 -1
  17. package/node_modules/@buddi/cli/dist/main.js.map +1 -1
  18. package/node_modules/@buddi/gateway/dist/.tsbuildinfo +1 -1
  19. package/node_modules/@buddi/gateway/dist/missions/ask-cap.d.ts +26 -0
  20. package/node_modules/@buddi/gateway/dist/missions/ask-cap.d.ts.map +1 -0
  21. package/node_modules/@buddi/gateway/dist/missions/ask-cap.js +83 -0
  22. package/node_modules/@buddi/gateway/dist/missions/ask-cap.js.map +1 -0
  23. package/node_modules/@buddi/gateway/dist/missions/execute.d.ts +12 -1
  24. package/node_modules/@buddi/gateway/dist/missions/execute.d.ts.map +1 -1
  25. package/node_modules/@buddi/gateway/dist/missions/execute.js +73 -11
  26. package/node_modules/@buddi/gateway/dist/missions/execute.js.map +1 -1
  27. package/node_modules/@buddi/gateway/dist/missions/parked.d.ts +6 -0
  28. package/node_modules/@buddi/gateway/dist/missions/parked.d.ts.map +1 -1
  29. package/node_modules/@buddi/gateway/dist/missions/parked.js +7 -1
  30. package/node_modules/@buddi/gateway/dist/missions/parked.js.map +1 -1
  31. package/node_modules/@buddi/gateway/dist/serve.js +1 -1
  32. package/node_modules/@buddi/gateway/dist/serve.js.map +1 -1
  33. package/node_modules/@buddi/gateway/dist/system-context.d.ts +7 -0
  34. package/node_modules/@buddi/gateway/dist/system-context.d.ts.map +1 -1
  35. package/node_modules/@buddi/gateway/dist/system-context.js +8 -0
  36. package/node_modules/@buddi/gateway/dist/system-context.js.map +1 -1
  37. package/node_modules/@buddi/gateway/dist/web/api-routes.d.ts.map +1 -1
  38. package/node_modules/@buddi/gateway/dist/web/api-routes.js +11 -3
  39. package/node_modules/@buddi/gateway/dist/web/api-routes.js.map +1 -1
  40. package/node_modules/@buddi/gateway/dist/web/extension.d.ts +13 -1
  41. package/node_modules/@buddi/gateway/dist/web/extension.d.ts.map +1 -1
  42. package/node_modules/@buddi/gateway/dist/web/extension.js +125 -5
  43. package/node_modules/@buddi/gateway/dist/web/extension.js.map +1 -1
  44. package/node_modules/@buddi/gateway/dist/web/remote-hand.d.ts.map +1 -1
  45. package/node_modules/@buddi/gateway/dist/web/remote-hand.js +45 -2
  46. package/node_modules/@buddi/gateway/dist/web/remote-hand.js.map +1 -1
  47. package/node_modules/@buddi/gateway/dist/web/secrets.d.ts +26 -1
  48. package/node_modules/@buddi/gateway/dist/web/secrets.d.ts.map +1 -1
  49. package/node_modules/@buddi/gateway/dist/web/secrets.js +66 -3
  50. package/node_modules/@buddi/gateway/dist/web/secrets.js.map +1 -1
  51. package/node_modules/@buddi/gateway/dist/web/server.d.ts.map +1 -1
  52. package/node_modules/@buddi/gateway/dist/web/server.js +63 -7
  53. package/node_modules/@buddi/gateway/dist/web/server.js.map +1 -1
  54. package/node_modules/@buddi/gateway/dist/web/version.d.ts +2 -0
  55. package/node_modules/@buddi/gateway/dist/web/version.d.ts.map +1 -1
  56. package/node_modules/@buddi/gateway/dist/web/version.js +5 -1
  57. package/node_modules/@buddi/gateway/dist/web/version.js.map +1 -1
  58. package/node_modules/@buddi/install/dist/.tsbuildinfo +1 -1
  59. package/node_modules/@buddi/install/dist/app-layout.d.ts +127 -0
  60. package/node_modules/@buddi/install/dist/app-layout.d.ts.map +1 -0
  61. package/node_modules/@buddi/install/dist/app-layout.js +323 -0
  62. package/node_modules/@buddi/install/dist/app-layout.js.map +1 -0
  63. package/node_modules/@buddi/install/dist/index.d.ts +1 -0
  64. package/node_modules/@buddi/install/dist/index.d.ts.map +1 -1
  65. package/node_modules/@buddi/install/dist/index.js +1 -0
  66. package/node_modules/@buddi/install/dist/index.js.map +1 -1
  67. package/node_modules/@buddi/install/dist/launcher.d.ts.map +1 -1
  68. package/node_modules/@buddi/install/dist/launcher.js +18 -4
  69. package/node_modules/@buddi/install/dist/launcher.js.map +1 -1
  70. package/node_modules/@buddi/install/dist/provenance.d.ts +34 -0
  71. package/node_modules/@buddi/install/dist/provenance.d.ts.map +1 -0
  72. package/node_modules/@buddi/install/dist/provenance.js +200 -0
  73. package/node_modules/@buddi/install/dist/provenance.js.map +1 -0
  74. package/node_modules/@buddi/install/dist/supervisor.d.ts.map +1 -1
  75. package/node_modules/@buddi/install/dist/supervisor.js +9 -4
  76. package/node_modules/@buddi/install/dist/supervisor.js.map +1 -1
  77. package/node_modules/@buddi/install/dist/upgrade.d.ts +77 -3
  78. package/node_modules/@buddi/install/dist/upgrade.d.ts.map +1 -1
  79. package/node_modules/@buddi/install/dist/upgrade.js +325 -43
  80. package/node_modules/@buddi/install/dist/upgrade.js.map +1 -1
  81. package/node_modules/@buddi/tool-browser/dist/.tsbuildinfo +1 -1
  82. package/node_modules/@buddi/tool-browser/dist/controller.d.ts +6 -0
  83. package/node_modules/@buddi/tool-browser/dist/controller.d.ts.map +1 -1
  84. package/node_modules/@buddi/tool-browser/dist/controller.js +80 -6
  85. package/node_modules/@buddi/tool-browser/dist/controller.js.map +1 -1
  86. package/node_modules/@buddi/tool-browser/dist/driver.d.ts +3 -1
  87. package/node_modules/@buddi/tool-browser/dist/driver.d.ts.map +1 -1
  88. package/node_modules/@buddi/tool-browser/dist/driver.js +82 -1
  89. package/node_modules/@buddi/tool-browser/dist/driver.js.map +1 -1
  90. package/node_modules/@buddi/tool-browser/dist/extension.d.ts +60 -7
  91. package/node_modules/@buddi/tool-browser/dist/extension.d.ts.map +1 -1
  92. package/node_modules/@buddi/tool-browser/dist/extension.js +90 -13
  93. package/node_modules/@buddi/tool-browser/dist/extension.js.map +1 -1
  94. package/node_modules/@buddi/tool-browser/dist/index.d.ts +6 -4
  95. package/node_modules/@buddi/tool-browser/dist/index.d.ts.map +1 -1
  96. package/node_modules/@buddi/tool-browser/dist/index.js +7 -4
  97. package/node_modules/@buddi/tool-browser/dist/index.js.map +1 -1
  98. package/node_modules/@buddi/tool-browser/dist/logins.d.ts +150 -0
  99. package/node_modules/@buddi/tool-browser/dist/logins.d.ts.map +1 -0
  100. package/node_modules/@buddi/tool-browser/dist/logins.js +476 -0
  101. package/node_modules/@buddi/tool-browser/dist/logins.js.map +1 -0
  102. package/node_modules/@buddi/tool-browser/dist/routes.d.ts +22 -1
  103. package/node_modules/@buddi/tool-browser/dist/routes.d.ts.map +1 -1
  104. package/node_modules/@buddi/tool-browser/dist/routes.js +51 -3
  105. package/node_modules/@buddi/tool-browser/dist/routes.js.map +1 -1
  106. package/node_modules/@buddi/tool-browser/dist/secrets.d.ts +3 -1
  107. package/node_modules/@buddi/tool-browser/dist/secrets.d.ts.map +1 -1
  108. package/node_modules/@buddi/tool-browser/dist/secrets.js +10 -6
  109. package/node_modules/@buddi/tool-browser/dist/secrets.js.map +1 -1
  110. package/node_modules/@buddi/tool-browser/dist/service.d.ts +26 -2
  111. package/node_modules/@buddi/tool-browser/dist/service.d.ts.map +1 -1
  112. package/node_modules/@buddi/tool-browser/dist/service.js +117 -3
  113. package/node_modules/@buddi/tool-browser/dist/service.js.map +1 -1
  114. package/node_modules/@buddi/tool-browser/dist/types.d.ts +88 -0
  115. package/node_modules/@buddi/tool-browser/dist/types.d.ts.map +1 -1
  116. package/node_modules/@buddi/tool-browser/dist/types.js +18 -0
  117. package/node_modules/@buddi/tool-browser/dist/types.js.map +1 -1
  118. package/node_modules/hono/dist/cjs/hono-base.js +2 -0
  119. package/node_modules/hono/dist/cjs/middleware/language/language.js +1 -1
  120. package/node_modules/hono/dist/cjs/middleware/mount/index.js +71 -0
  121. package/node_modules/hono/dist/cjs/request.js +4 -4
  122. package/node_modules/hono/dist/hono-base.js +2 -0
  123. package/node_modules/hono/dist/middleware/mount/index.js +70 -0
  124. package/node_modules/hono/dist/request.js +4 -4
  125. package/node_modules/hono/dist/types/hono-base.d.ts +2 -0
  126. package/node_modules/hono/dist/types/jsx/base.d.ts +1 -1
  127. package/node_modules/hono/dist/types/middleware/mount/index.d.ts +46 -0
  128. package/node_modules/hono/dist/types/request.d.ts +4 -4
  129. package/node_modules/hono/package.json +9 -1
  130. package/package.json +2 -2
  131. package/packages/cli/dist/.tsbuildinfo +1 -1
  132. package/packages/cli/dist/access-cmd.d.ts +1 -1
  133. package/packages/cli/dist/access-cmd.d.ts.map +1 -1
  134. package/packages/cli/dist/access-cmd.js +23 -3
  135. package/packages/cli/dist/access-cmd.js.map +1 -1
  136. package/packages/cli/dist/args.d.ts +2 -2
  137. package/packages/cli/dist/args.d.ts.map +1 -1
  138. package/packages/cli/dist/args.js +7 -2
  139. package/packages/cli/dist/args.js.map +1 -1
  140. package/packages/cli/dist/commands.d.ts.map +1 -1
  141. package/packages/cli/dist/commands.js +11 -0
  142. package/packages/cli/dist/commands.js.map +1 -1
  143. package/packages/cli/dist/main.d.ts.map +1 -1
  144. package/packages/cli/dist/main.js +2 -1
  145. package/packages/cli/dist/main.js.map +1 -1
  146. package/packages/gateway/dist/.tsbuildinfo +1 -1
  147. package/packages/gateway/dist/missions/ask-cap.d.ts +26 -0
  148. package/packages/gateway/dist/missions/ask-cap.d.ts.map +1 -0
  149. package/packages/gateway/dist/missions/ask-cap.js +83 -0
  150. package/packages/gateway/dist/missions/ask-cap.js.map +1 -0
  151. package/packages/gateway/dist/missions/execute.d.ts +12 -1
  152. package/packages/gateway/dist/missions/execute.d.ts.map +1 -1
  153. package/packages/gateway/dist/missions/execute.js +73 -11
  154. package/packages/gateway/dist/missions/execute.js.map +1 -1
  155. package/packages/gateway/dist/missions/parked.d.ts +6 -0
  156. package/packages/gateway/dist/missions/parked.d.ts.map +1 -1
  157. package/packages/gateway/dist/missions/parked.js +7 -1
  158. package/packages/gateway/dist/missions/parked.js.map +1 -1
  159. package/packages/gateway/dist/serve.js +1 -1
  160. package/packages/gateway/dist/serve.js.map +1 -1
  161. package/packages/gateway/dist/system-context.d.ts +7 -0
  162. package/packages/gateway/dist/system-context.d.ts.map +1 -1
  163. package/packages/gateway/dist/system-context.js +8 -0
  164. package/packages/gateway/dist/system-context.js.map +1 -1
  165. package/packages/gateway/dist/web/api-routes.d.ts.map +1 -1
  166. package/packages/gateway/dist/web/api-routes.js +11 -3
  167. package/packages/gateway/dist/web/api-routes.js.map +1 -1
  168. package/packages/gateway/dist/web/extension.d.ts +13 -1
  169. package/packages/gateway/dist/web/extension.d.ts.map +1 -1
  170. package/packages/gateway/dist/web/extension.js +125 -5
  171. package/packages/gateway/dist/web/extension.js.map +1 -1
  172. package/packages/gateway/dist/web/remote-hand.d.ts.map +1 -1
  173. package/packages/gateway/dist/web/remote-hand.js +45 -2
  174. package/packages/gateway/dist/web/remote-hand.js.map +1 -1
  175. package/packages/gateway/dist/web/secrets.d.ts +26 -1
  176. package/packages/gateway/dist/web/secrets.d.ts.map +1 -1
  177. package/packages/gateway/dist/web/secrets.js +66 -3
  178. package/packages/gateway/dist/web/secrets.js.map +1 -1
  179. package/packages/gateway/dist/web/server.d.ts.map +1 -1
  180. package/packages/gateway/dist/web/server.js +63 -7
  181. package/packages/gateway/dist/web/server.js.map +1 -1
  182. package/packages/gateway/dist/web/version.d.ts +2 -0
  183. package/packages/gateway/dist/web/version.d.ts.map +1 -1
  184. package/packages/gateway/dist/web/version.js +5 -1
  185. package/packages/gateway/dist/web/version.js.map +1 -1
  186. package/packages/install/dist/.tsbuildinfo +1 -1
  187. package/packages/install/dist/app-layout.d.ts +127 -0
  188. package/packages/install/dist/app-layout.d.ts.map +1 -0
  189. package/packages/install/dist/app-layout.js +323 -0
  190. package/packages/install/dist/app-layout.js.map +1 -0
  191. package/packages/install/dist/index.d.ts +1 -0
  192. package/packages/install/dist/index.d.ts.map +1 -1
  193. package/packages/install/dist/index.js +1 -0
  194. package/packages/install/dist/index.js.map +1 -1
  195. package/packages/install/dist/launcher.d.ts.map +1 -1
  196. package/packages/install/dist/launcher.js +18 -4
  197. package/packages/install/dist/launcher.js.map +1 -1
  198. package/packages/install/dist/provenance.d.ts +34 -0
  199. package/packages/install/dist/provenance.d.ts.map +1 -0
  200. package/packages/install/dist/provenance.js +200 -0
  201. package/packages/install/dist/provenance.js.map +1 -0
  202. package/packages/install/dist/supervisor.d.ts.map +1 -1
  203. package/packages/install/dist/supervisor.js +9 -4
  204. package/packages/install/dist/supervisor.js.map +1 -1
  205. package/packages/install/dist/upgrade.d.ts +77 -3
  206. package/packages/install/dist/upgrade.d.ts.map +1 -1
  207. package/packages/install/dist/upgrade.js +325 -43
  208. package/packages/install/dist/upgrade.js.map +1 -1
  209. package/packages/tools/browser/dist/.tsbuildinfo +1 -1
  210. package/packages/tools/browser/dist/controller.d.ts +6 -0
  211. package/packages/tools/browser/dist/controller.d.ts.map +1 -1
  212. package/packages/tools/browser/dist/controller.js +80 -6
  213. package/packages/tools/browser/dist/controller.js.map +1 -1
  214. package/packages/tools/browser/dist/driver.d.ts +3 -1
  215. package/packages/tools/browser/dist/driver.d.ts.map +1 -1
  216. package/packages/tools/browser/dist/driver.js +82 -1
  217. package/packages/tools/browser/dist/driver.js.map +1 -1
  218. package/packages/tools/browser/dist/extension.d.ts +60 -7
  219. package/packages/tools/browser/dist/extension.d.ts.map +1 -1
  220. package/packages/tools/browser/dist/extension.js +90 -13
  221. package/packages/tools/browser/dist/extension.js.map +1 -1
  222. package/packages/tools/browser/dist/index.d.ts +6 -4
  223. package/packages/tools/browser/dist/index.d.ts.map +1 -1
  224. package/packages/tools/browser/dist/index.js +7 -4
  225. package/packages/tools/browser/dist/index.js.map +1 -1
  226. package/packages/tools/browser/dist/logins.d.ts +150 -0
  227. package/packages/tools/browser/dist/logins.d.ts.map +1 -0
  228. package/packages/tools/browser/dist/logins.js +476 -0
  229. package/packages/tools/browser/dist/logins.js.map +1 -0
  230. package/packages/tools/browser/dist/routes.d.ts +22 -1
  231. package/packages/tools/browser/dist/routes.d.ts.map +1 -1
  232. package/packages/tools/browser/dist/routes.js +51 -3
  233. package/packages/tools/browser/dist/routes.js.map +1 -1
  234. package/packages/tools/browser/dist/secrets.d.ts +3 -1
  235. package/packages/tools/browser/dist/secrets.d.ts.map +1 -1
  236. package/packages/tools/browser/dist/secrets.js +10 -6
  237. package/packages/tools/browser/dist/secrets.js.map +1 -1
  238. package/packages/tools/browser/dist/service.d.ts +26 -2
  239. package/packages/tools/browser/dist/service.d.ts.map +1 -1
  240. package/packages/tools/browser/dist/service.js +117 -3
  241. package/packages/tools/browser/dist/service.js.map +1 -1
  242. package/packages/tools/browser/dist/types.d.ts +88 -0
  243. package/packages/tools/browser/dist/types.d.ts.map +1 -1
  244. package/packages/tools/browser/dist/types.js +18 -0
  245. package/packages/tools/browser/dist/types.js.map +1 -1
  246. package/packages/web/dist/assets/index-DYfeUW-0.css +1 -0
  247. package/packages/web/dist/assets/index-vpc904_b.js +146 -0
  248. package/packages/web/dist/assets/{lottie_light-CuiSMqnl.js → lottie_light-BFAMYgLA.js} +1 -1
  249. package/packages/web/dist/build.json +1 -1
  250. package/packages/web/dist/index.html +2 -2
  251. package/packages/web/dist/precache.json +4 -4
  252. package/packages/web/dist/assets/index-Bac-CdD-.css +0 -1
  253. 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":"AA8BA,OAAO,EAAmB,UAAU,EAAE,MAAM,cAAc,CAAC;AAE3D,kFAAkF;AAClF,eAAO,MAAM,aAAa,QAAa,CAAC;AACxC,+EAA+E;AAC/E,eAAO,MAAM,UAAU,EAAG,YAAqB,CAAC;AAChD,OAAO,EAAE,UAAU,IAAI,UAAU,EAAE,CAAC;AAEpC,MAAM,MAAM,aAAa,GAAG,MAAM,GAAG,OAAO,GAAG,OAAO,CAAC;AAEvD,iGAAiG;AACjG,MAAM,WAAW,SAAS;IACxB,uEAAuE;IACvE,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,qFAAqF;AACrF,MAAM,WAAW,WAAW;IAC1B,EAAE,EAAE,MAAM,CAAC;IACX,SAAS,EAAE,MAAM,CAAC;IAClB,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,yIAAyI;IACzI,MAAM,CAAC,EAAE,IAAI,CAAC;CACf;AAED,2FAA2F;AAC3F,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB;;;;;OAKG;IACH,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED,6EAA6E;AAC7E,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,OAAO,UAAU,CAAC;QAAC,MAAM,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,OAAO,UAAU,CAAA;KAAE,CAAC,CAAC;CACvF;AACD,wDAAwD;AACxD,MAAM,MAAM,UAAU,GAAG,CAAC,KAAK,EAAE,eAAe,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;AACnE,6GAA6G;AAC7G,MAAM,MAAM,eAAe,GAAG,MAAM,OAAO,CAAC,SAAS,MAAM,EAAE,CAAC,CAAC;AAC/D,8HAA8H;AAC9H,MAAM,MAAM,aAAa,GAAG,MAAM,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;AACzD,oFAAoF;AACpF,MAAM,MAAM,QAAQ,GAAG,MAAM,GAAG,QAAQ,GAAG,MAAM,CAAC;AAElD,MAAM,MAAM,YAAY,GAAG,OAAO,GAAG,WAAW,GAAG,OAAO,GAAG,MAAM,CAAC;AAWpE;;;;;GAKG;AACH,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,SAAS,UAAU,EAAE,EAAE,QAAQ,GAAE,WAAW,CAAC,MAAM,CAAa,GAAG,MAAM,CAazI;AAED,+FAA+F;AAC/F,wBAAgB,aAAa,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAItD;AAgBD,qBAAa,WAAW;;IAmBV,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,SAAS;IAAE,QAAQ,CAAC,OAAO,EAAE;QAAE,MAAM,CAAC,EAAE,MAAM,CAAC;QAAC,GAAG,CAAC,EAAE,MAAM,MAAM,CAAA;KAAE;gBAAnF,IAAI,EAAE,MAAM,GAAG,SAAS,EAAW,OAAO,GAAE;QAAE,MAAM,CAAC,EAAE,MAAM,CAAC;QAAC,GAAG,CAAC,EAAE,MAAM,MAAM,CAAA;KAAO;IAE7G,gIAAgI;IAChI,QAAQ,CAAC,KAAK,EAAE,UAAU,EAAE,KAAK,CAAC,EAAE,eAAe,EAAE,GAAG,CAAC,EAAE,aAAa,GAAG,IAAI;IAuC/E,mIAAmI;IAC7H,KAAK,CAAC,KAAK,EAAE,SAAS,GAAG,OAAO,CAAC,QAAQ,CAAC;IAIhD,IAAI,OAAO,IAAI,OAAO,CAAsC;IAE5D,yFAAyF;IACzF,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IA4BrB,sEAAsE;IACtE,MAAM,CAAC,QAAQ,EAAE,CAAC,MAAM,EAAE,WAAW,KAAK,IAAI,GAAG,MAAM,IAAI;IAK3D,sCAAsC;IACtC,KAAK,IAAI,MAAM,EAAE;IACjB,sCAAsC;IACtC,KAAK,IAAI,UAAU,EAAE;IAErB,qDAAqD;IACrD,OAAO,CAAC,SAAS,CAAC,EAAE,MAAM,GAAG,WAAW,EAAE;IAe1C;;;;;;;OAOG;IACG,IAAI,CAAC,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,SAAS,GAAG,OAAO,CAAC,WAAW,GAAG,SAAS,CAAC;IA+BjF;;;;OAIG;IACG,MAAM,CAAC,EAAE,EAAE,MAAM,EAAE,QAAQ,EAAE,aAAa,GAAG,OAAO,CAAC;QAAE,OAAO,EAAE,YAAY,CAAC;QAAC,KAAK,CAAC,EAAE,UAAU,CAAA;KAAE,CAAC;IAYzG;;;OAGG;IACG,OAAO,CAAC,KAAK,EAAE,IAAI,CAAC,SAAS,EAAE,UAAU,CAAC,GAAG;QAAE,QAAQ,CAAC,EAAE,MAAM,CAAA;KAAE,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,GAAG,OAAO,CAAC;QAAE,OAAO,EAAE,YAAY,CAAC;QAAC,KAAK,CAAC,EAAE,UAAU,CAAA;KAAE,CAAC;IAqC7J,6EAA6E;IACvE,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAU5C,uFAAuF;IACjF,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAYxD,gCAAgC;IAChC,KAAK,IAAI,IAAI;CAGd;AAID;;;;;;;;;;GAUG;AACH,wBAAgB,WAAW,CAAC,MAAM,EAAE,CAAC,KAAK,EAAE;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,KAAK,IAAI,GAAG,IAAI,CAuEjG;AAED,4EAA4E;AAC5E,eAAO,MAAM,aAAa,qBAAqB,CAAC;AAChD,eAAO,MAAM,WAAW,sBAAsB,CAAC;AAE/C,iIAAiI;AACjI,wBAAgB,gBAAgB,IAAI,MAAM,CAEzC;AAED,yEAAyE;AACzE,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,OAAO,GAAG,SAAS,GAAG,SAAS,CAOxE"}
@@ -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