@intentic/keyring 1.344.0 → 1.346.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (231) hide show
  1. package/README.md +141 -45
  2. package/dist/address.d.ts +0 -1
  3. package/dist/address.d.ts.map +1 -1
  4. package/dist/address.js +1 -1
  5. package/dist/address.js.map +1 -1
  6. package/dist/app.d.ts.map +1 -1
  7. package/dist/app.js +49 -48
  8. package/dist/app.js.map +1 -1
  9. package/dist/askpass/answer.d.ts +12 -0
  10. package/dist/askpass/answer.d.ts.map +1 -0
  11. package/dist/askpass/answer.js +204 -0
  12. package/dist/askpass/answer.js.map +1 -0
  13. package/dist/askpass/askpass.d.ts +6 -0
  14. package/dist/askpass/askpass.d.ts.map +1 -1
  15. package/dist/askpass/askpass.js +28 -0
  16. package/dist/askpass/askpass.js.map +1 -1
  17. package/dist/audit.d.ts +5 -2
  18. package/dist/audit.d.ts.map +1 -1
  19. package/dist/audit.js +15 -2
  20. package/dist/audit.js.map +1 -1
  21. package/dist/consent/consent-posix.d.ts +7 -3
  22. package/dist/consent/consent-posix.d.ts.map +1 -1
  23. package/dist/consent/consent-posix.js +82 -12
  24. package/dist/consent/consent-posix.js.map +1 -1
  25. package/dist/consent/consent-windows.d.ts +3 -1
  26. package/dist/consent/consent-windows.d.ts.map +1 -1
  27. package/dist/consent/consent-windows.js +174 -62
  28. package/dist/consent/consent-windows.js.map +1 -1
  29. package/dist/consent/consent.d.ts +41 -2
  30. package/dist/consent/consent.d.ts.map +1 -1
  31. package/dist/consent/consent.js +7 -5
  32. package/dist/consent/consent.js.map +1 -1
  33. package/dist/core.d.ts +3 -30
  34. package/dist/core.d.ts.map +1 -1
  35. package/dist/core.js +189 -62
  36. package/dist/core.js.map +1 -1
  37. package/dist/git-credential/git-credential.d.ts +18 -0
  38. package/dist/git-credential/git-credential.d.ts.map +1 -0
  39. package/dist/git-credential/git-credential.js +68 -0
  40. package/dist/git-credential/git-credential.js.map +1 -0
  41. package/dist/index.d.ts +5 -2
  42. package/dist/index.d.ts.map +1 -1
  43. package/dist/index.js +5 -2
  44. package/dist/index.js.map +1 -1
  45. package/dist/native-host/framing.d.ts +10 -0
  46. package/dist/native-host/framing.d.ts.map +1 -0
  47. package/dist/native-host/framing.js +33 -0
  48. package/dist/native-host/framing.js.map +1 -0
  49. package/dist/native-host/native-host.d.ts +11 -0
  50. package/dist/native-host/native-host.d.ts.map +1 -0
  51. package/dist/native-host/native-host.js +130 -0
  52. package/dist/native-host/native-host.js.map +1 -0
  53. package/dist/passwords/access.d.ts +73 -0
  54. package/dist/passwords/access.d.ts.map +1 -0
  55. package/dist/passwords/access.js +3 -0
  56. package/dist/passwords/access.js.map +1 -0
  57. package/dist/passwords/app-ops.d.ts +18 -0
  58. package/dist/passwords/app-ops.d.ts.map +1 -0
  59. package/dist/passwords/app-ops.js +261 -0
  60. package/dist/passwords/app-ops.js.map +1 -0
  61. package/dist/passwords/capture.d.ts +35 -0
  62. package/dist/passwords/capture.d.ts.map +1 -0
  63. package/dist/passwords/capture.js +306 -0
  64. package/dist/passwords/capture.js.map +1 -0
  65. package/dist/passwords/entries.d.ts +20 -0
  66. package/dist/passwords/entries.d.ts.map +1 -0
  67. package/dist/passwords/entries.js +57 -0
  68. package/dist/passwords/entries.js.map +1 -0
  69. package/dist/passwords/fills.d.ts +10 -0
  70. package/dist/passwords/fills.d.ts.map +1 -0
  71. package/dist/passwords/fills.js +87 -0
  72. package/dist/passwords/fills.js.map +1 -0
  73. package/dist/passwords/needs.d.ts +11 -0
  74. package/dist/passwords/needs.d.ts.map +1 -0
  75. package/dist/passwords/needs.js +232 -0
  76. package/dist/passwords/needs.js.map +1 -0
  77. package/dist/passwords/phone.d.ts +19 -0
  78. package/dist/passwords/phone.d.ts.map +1 -0
  79. package/dist/passwords/phone.js +125 -0
  80. package/dist/passwords/phone.js.map +1 -0
  81. package/dist/protocol.d.ts +1169 -0
  82. package/dist/protocol.d.ts.map +1 -1
  83. package/dist/protocol.js +86 -1
  84. package/dist/protocol.js.map +1 -1
  85. package/dist/service/channel.d.ts +10 -3
  86. package/dist/service/channel.d.ts.map +1 -1
  87. package/dist/service/channel.js +31 -13
  88. package/dist/service/channel.js.map +1 -1
  89. package/dist/service/door.d.ts +45 -4
  90. package/dist/service/door.d.ts.map +1 -1
  91. package/dist/service/door.js +31 -2
  92. package/dist/service/door.js.map +1 -1
  93. package/dist/service/helper.d.ts +18 -1
  94. package/dist/service/helper.d.ts.map +1 -1
  95. package/dist/service/helper.js +68 -28
  96. package/dist/service/helper.js.map +1 -1
  97. package/dist/service/linux/install-linux.d.ts +12 -0
  98. package/dist/service/linux/install-linux.d.ts.map +1 -0
  99. package/dist/service/linux/install-linux.js +133 -0
  100. package/dist/service/linux/install-linux.js.map +1 -0
  101. package/dist/service/linux/paths-linux.d.ts +21 -0
  102. package/dist/service/linux/paths-linux.d.ts.map +1 -0
  103. package/dist/service/linux/paths-linux.js +31 -0
  104. package/dist/service/linux/paths-linux.js.map +1 -0
  105. package/dist/service/linux/polkit-linux.d.ts +29 -0
  106. package/dist/service/linux/polkit-linux.d.ts.map +1 -0
  107. package/dist/service/linux/polkit-linux.js +89 -0
  108. package/dist/service/linux/polkit-linux.js.map +1 -0
  109. package/dist/service/linux/service-run-linux.d.ts +4 -0
  110. package/dist/service/linux/service-run-linux.d.ts.map +1 -0
  111. package/dist/service/linux/service-run-linux.js +52 -0
  112. package/dist/service/linux/service-run-linux.js.map +1 -0
  113. package/dist/service/linux/session-linux.d.ts +4 -0
  114. package/dist/service/linux/session-linux.d.ts.map +1 -0
  115. package/dist/service/linux/session-linux.js +13 -0
  116. package/dist/service/linux/session-linux.js.map +1 -0
  117. package/dist/service/linux/unit-linux.d.ts +16 -0
  118. package/dist/service/linux/unit-linux.d.ts.map +1 -0
  119. package/dist/service/linux/unit-linux.js +67 -0
  120. package/dist/service/linux/unit-linux.js.map +1 -0
  121. package/dist/service/platform.d.ts +7 -0
  122. package/dist/service/platform.d.ts.map +1 -0
  123. package/dist/service/platform.js +62 -0
  124. package/dist/service/platform.js.map +1 -0
  125. package/dist/service/recovery.d.ts +3 -0
  126. package/dist/service/recovery.d.ts.map +1 -0
  127. package/dist/service/recovery.js +22 -0
  128. package/dist/service/recovery.js.map +1 -0
  129. package/dist/service/remote.d.ts +4 -1
  130. package/dist/service/remote.d.ts.map +1 -1
  131. package/dist/service/remote.js +41 -6
  132. package/dist/service/remote.js.map +1 -1
  133. package/dist/service/service-key.d.ts +9 -0
  134. package/dist/service/service-key.d.ts.map +1 -0
  135. package/dist/service/service-key.js +55 -0
  136. package/dist/service/service-key.js.map +1 -0
  137. package/dist/service/service-run.d.ts +20 -2
  138. package/dist/service/service-run.d.ts.map +1 -1
  139. package/dist/service/service-run.js +52 -46
  140. package/dist/service/service-run.js.map +1 -1
  141. package/dist/service/user.d.ts +8 -0
  142. package/dist/service/user.d.ts.map +1 -0
  143. package/dist/service/user.js +39 -0
  144. package/dist/service/user.js.map +1 -0
  145. package/dist/service/{acl.d.ts → windows/acl-windows.d.ts} +3 -3
  146. package/dist/service/windows/acl-windows.d.ts.map +1 -0
  147. package/dist/service/{acl.js → windows/acl-windows.js} +3 -3
  148. package/dist/service/windows/acl-windows.js.map +1 -0
  149. package/dist/service/{install.d.ts → windows/install-windows.d.ts} +4 -2
  150. package/dist/service/windows/install-windows.d.ts.map +1 -0
  151. package/dist/service/{install.js → windows/install-windows.js} +13 -12
  152. package/dist/service/windows/install-windows.js.map +1 -0
  153. package/dist/service/windows/leftovers-windows.d.ts +8 -0
  154. package/dist/service/windows/leftovers-windows.d.ts.map +1 -0
  155. package/dist/service/windows/leftovers-windows.js +44 -0
  156. package/dist/service/windows/leftovers-windows.js.map +1 -0
  157. package/dist/service/{paths.d.ts → windows/paths-windows.d.ts} +4 -5
  158. package/dist/service/windows/paths-windows.d.ts.map +1 -0
  159. package/dist/service/{paths.js → windows/paths-windows.js} +5 -5
  160. package/dist/service/windows/paths-windows.js.map +1 -0
  161. package/dist/service/windows/service-run-windows.d.ts +4 -0
  162. package/dist/service/windows/service-run-windows.d.ts.map +1 -0
  163. package/dist/service/windows/service-run-windows.js +46 -0
  164. package/dist/service/windows/service-run-windows.js.map +1 -0
  165. package/dist/service/windows/session-windows.d.ts +4 -0
  166. package/dist/service/windows/session-windows.d.ts.map +1 -0
  167. package/dist/service/windows/session-windows.js +13 -0
  168. package/dist/service/windows/session-windows.js.map +1 -0
  169. package/dist/service/windows/sid-windows.d.ts +3 -0
  170. package/dist/service/windows/sid-windows.d.ts.map +1 -0
  171. package/dist/service/{sid.js → windows/sid-windows.js} +2 -2
  172. package/dist/service/windows/sid-windows.js.map +1 -0
  173. package/dist/service/{task.d.ts → windows/task-windows.d.ts} +4 -3
  174. package/dist/service/windows/task-windows.d.ts.map +1 -0
  175. package/dist/service/{task.js → windows/task-windows.js} +19 -2
  176. package/dist/service/windows/task-windows.js.map +1 -0
  177. package/dist/service/wire.d.ts +32 -0
  178. package/dist/service/wire.d.ts.map +1 -1
  179. package/dist/service/wire.js +15 -0
  180. package/dist/service/wire.js.map +1 -1
  181. package/dist/typing/clipboard.d.ts +13 -0
  182. package/dist/typing/clipboard.d.ts.map +1 -0
  183. package/dist/typing/clipboard.js +56 -0
  184. package/dist/typing/clipboard.js.map +1 -0
  185. package/dist/typing/index.d.ts.map +1 -1
  186. package/dist/typing/index.js +2 -1
  187. package/dist/typing/index.js.map +1 -1
  188. package/dist/typing/typist.d.ts +3 -1
  189. package/dist/typing/typist.d.ts.map +1 -1
  190. package/dist/typing/typist.js +2 -1
  191. package/dist/typing/typist.js.map +1 -1
  192. package/dist/vault.d.ts +785 -0
  193. package/dist/vault.d.ts.map +1 -1
  194. package/dist/vault.js +24 -2
  195. package/dist/vault.js.map +1 -1
  196. package/dist/watch/apps.d.ts +10 -0
  197. package/dist/watch/apps.d.ts.map +1 -0
  198. package/dist/watch/apps.js +95 -0
  199. package/dist/watch/apps.js.map +1 -0
  200. package/dist/watch/chip.d.ts +52 -0
  201. package/dist/watch/chip.d.ts.map +1 -0
  202. package/dist/watch/chip.js +76 -0
  203. package/dist/watch/chip.js.map +1 -0
  204. package/dist/watch/lines-child.d.ts +9 -0
  205. package/dist/watch/lines-child.d.ts.map +1 -0
  206. package/dist/watch/lines-child.js +34 -0
  207. package/dist/watch/lines-child.js.map +1 -0
  208. package/dist/watch/watch-linux.d.ts +6 -0
  209. package/dist/watch/watch-linux.d.ts.map +1 -0
  210. package/dist/watch/watch-linux.js +52 -0
  211. package/dist/watch/watch-linux.js.map +1 -0
  212. package/dist/watch/watch-windows.d.ts +6 -0
  213. package/dist/watch/watch-windows.d.ts.map +1 -0
  214. package/dist/watch/watch-windows.js +110 -0
  215. package/dist/watch/watch-windows.js.map +1 -0
  216. package/package.json +5 -5
  217. package/dist/serve.d.ts +0 -17
  218. package/dist/serve.d.ts.map +0 -1
  219. package/dist/serve.js +0 -65
  220. package/dist/serve.js.map +0 -1
  221. package/dist/service/acl.d.ts.map +0 -1
  222. package/dist/service/acl.js.map +0 -1
  223. package/dist/service/install.d.ts.map +0 -1
  224. package/dist/service/install.js.map +0 -1
  225. package/dist/service/paths.d.ts.map +0 -1
  226. package/dist/service/paths.js.map +0 -1
  227. package/dist/service/sid.d.ts +0 -3
  228. package/dist/service/sid.d.ts.map +0 -1
  229. package/dist/service/sid.js.map +0 -1
  230. package/dist/service/task.d.ts.map +0 -1
  231. package/dist/service/task.js.map +0 -1
package/dist/core.js CHANGED
@@ -1,26 +1,43 @@
1
1
  import { serialLock } from "@intentic/base/async";
2
2
  import { KeyringSandboxKeysSchema, } from "@intentic/sandbox-contract/schemas/keyring";
3
3
  import { keyFingerprint, newSealKeyPair, seal, unseal } from "@intentic/sandbox-contract/keyring-seal";
4
- import { askpassMatch, parseAskpassPrompt } from "./askpass/askpass.js";
4
+ import { NOT_SET_UP, refuse } from "./passwords/access.js";
5
+ import { AppOps } from "./passwords/app-ops.js";
6
+ import { Askpass } from "./askpass/answer.js";
7
+ import { Captures, seenIn } from "./passwords/capture.js";
8
+ import { DEFAULT_SETTINGS, entryInfoOf, healthOf, liveEntries, usedEntry, withEntry } from "./passwords/entries.js";
9
+ import { Fills } from "./passwords/fills.js";
5
10
  import { pinOf, samePin } from "./keys.js";
6
- import { acceptGrant, applyPatch, hostOf, isUnlocked, MINUTE_MS, modeNeedsPerson, NonceBook, rememberedFresh, releaseTargetOf, sandboxKey, shareWith, typeTargetOf, typingDecision, wideningOf, } from "./policy.js";
11
+ import { Needs } from "./passwords/needs.js";
12
+ import { Phones } from "./passwords/phone.js";
13
+ import { acceptGrant, applyPatch, hostOf, isUnlocked, MINUTE_MS, modeNeedsPerson, NonceBook, releaseTargetOf, sandboxKey, shareWith, typeTargetOf, typingDecision, wideningOf, } from "./policy.js";
7
14
  import { emptyOpenPart, helloChallenge, helloWrapOf, newVaultKey, openDocument, PENDING_INBOX_MAX, PENDING_SYNC_MAX, SCRYPT_COST, sealDocument, StoredEntrySchema, unwrapHello, unwrapPassphrase, VALUE_MAX_BYTES, wrapWithHello, wrapWithPassphrase, } from "./vault.js";
8
- const RELAYED_OPS = ["shares", "release", "type", "inbox", "copies", "sync-state", "sync-pair", "sync-outbox", "sync-deliver"];
15
+ const RELAYED_OPS = ["shares", "release", "type", "inbox", "copies", "sync-state", "sync-pair", "sync-outbox", "sync-deliver", "capture", "need", "phone-pair", "phone-fill"];
16
+ const APP_OPS = [
17
+ "capture-batch",
18
+ "undo-capture",
19
+ "seen",
20
+ "reveal",
21
+ "copy",
22
+ "history",
23
+ "restore",
24
+ "settings",
25
+ "set-settings",
26
+ "activity",
27
+ "fill-window",
28
+ "fill-value",
29
+ "candidates",
30
+ "needs",
31
+ "answer-need",
32
+ ];
9
33
  const RELAYED = new Set(RELAYED_OPS);
34
+ const APP = new Set(APP_OPS);
10
35
  const isRelayed = (request) => RELAYED.has(request.op);
11
- const refuse = (code, refusal) => ({ ok: false, code, refusal });
36
+ const isApp = (request) => APP.has(request.op);
12
37
  const fromConsent = (answer) => refuse(answer.code, answer.reason);
13
- const NOT_SET_UP = "The keyring on this computer is not set up yet: `intentic-machine keyring setup` sets it up.";
14
38
  const SETUP_UNLOCK_MINUTES = 15;
15
39
  const FOCUS_SETTLE_MS = 400;
16
40
  const SyncEntrySchema = StoredEntrySchema.omit({ shares: true, revision: true });
17
- const entryInfoOf = (entry) => {
18
- const info = { name: entry.name, ...entry.rules, source: entry.source, shares: entry.shares, createdAt: entry.createdAt, updatedAt: entry.updatedAt };
19
- if (entry.backend !== undefined) {
20
- info.backend = entry.backend;
21
- }
22
- return info;
23
- };
24
41
  const syncEntryOf = (entry) => {
25
42
  const sent = { name: entry.name, rules: entry.rules, source: entry.source, createdAt: entry.createdAt, updatedAt: entry.updatedAt };
26
43
  if (entry.backend !== undefined) {
@@ -51,18 +68,42 @@ const rulesWords = (rules) => {
51
68
  const field = rules.kind === "password" && !rules.anyField ? "password fields only" : "any field";
52
69
  return `${programs}; ${field}`;
53
70
  };
54
- const withEntry = (doc, entry) => ({ ...doc, entries: { ...doc.entries, [entry.name]: entry } });
55
71
  export class Keyring {
56
72
  deps;
57
73
  #file;
58
74
  #keys;
59
75
  #unlocked;
60
76
  #nonces = new NonceBook();
61
- #remembered = new Map();
62
77
  #lock = serialLock();
63
78
  #loaded = false;
79
+ #access;
80
+ #captures;
81
+ #appOps;
82
+ #fills;
83
+ #needs;
84
+ #phones;
85
+ #askpass;
64
86
  constructor(deps) {
65
87
  this.deps = deps;
88
+ this.#access = {
89
+ deps,
90
+ file: () => this.#file,
91
+ keys: async () => await this.#load(),
92
+ held: () => this.#held(),
93
+ open: async (file, request) => await this.#open(file, request),
94
+ commit: async (opened, pending) => await this.#commit(opened, pending),
95
+ writeOpen: async (open) => await this.#writeOpen(open),
96
+ record: async (record) => await this.#record(record),
97
+ valueOf: async (doc, entry) => await this.#valueOf(doc, entry),
98
+ fetchKeys: async (sandbox) => await this.#fetchKeys(sandbox),
99
+ typeInto: async (place, seen, value, asked) => await this.#typeInto(place, seen, value, asked),
100
+ };
101
+ this.#captures = new Captures(this.#access);
102
+ this.#appOps = new AppOps(this.#access);
103
+ this.#fills = new Fills(this.#access, this.#captures);
104
+ this.#needs = new Needs(this.#access);
105
+ this.#phones = new Phones(this.#access);
106
+ this.#askpass = new Askpass(this.#access, this.#captures);
66
107
  }
67
108
  async #load() {
68
109
  if (!this.#loaded || this.#keys === undefined) {
@@ -81,7 +122,7 @@ export class Keyring {
81
122
  #forget() {
82
123
  this.#unlocked?.key.fill(0);
83
124
  this.#unlocked = undefined;
84
- this.#remembered.clear();
125
+ this.#askpass.forget();
85
126
  this.deps.backends.forget();
86
127
  }
87
128
  #opener(file) {
@@ -96,23 +137,42 @@ export class Keyring {
96
137
  if (held !== undefined && !request.person) {
97
138
  return { doc: held.doc, key: held.key, asked: false, how: undefined };
98
139
  }
99
- const answer = await this.deps.consent.ask({ title: request.title, message: request.message, unlock: held === undefined, opener: this.#opener(file) });
140
+ const picked = await this.#choose(request);
141
+ if (!picked.ok) {
142
+ return picked;
143
+ }
144
+ const chosen = picked.choice;
145
+ const label = request.choices?.find((choice) => choice.id === chosen)?.label;
146
+ const message = label === undefined ? request.message : `${request.message} You chose: ${label}.`;
147
+ const answer = await this.deps.consent.ask({ title: request.title, message, unlock: held === undefined, opener: this.#opener(file) });
100
148
  if (!answer.ok) {
101
149
  return fromConsent(answer);
102
150
  }
103
151
  if (held !== undefined) {
104
- return { doc: held.doc, key: held.key, asked: true, how: answer.how };
152
+ return { doc: held.doc, key: held.key, asked: true, how: answer.how, choice: chosen };
105
153
  }
106
154
  if (answer.vaultKey === undefined) {
107
- return refuse("locked", "The keyring could not be opened with that answer: unlock it with its passphrase.");
155
+ return refuse("locked", "Passwords could not be opened with that answer: unlock it with its recovery key.");
108
156
  }
109
157
  try {
110
- return { doc: openDocument(answer.vaultKey, file.sealed), key: answer.vaultKey, asked: true, how: answer.how };
158
+ return { doc: openDocument(answer.vaultKey, file.sealed), key: answer.vaultKey, asked: true, how: answer.how, choice: chosen };
111
159
  }
112
160
  catch (error) {
113
161
  return refuse("unavailable", error instanceof Error ? error.message : "The vault could not be opened.");
114
162
  }
115
163
  }
164
+ async #choose(request) {
165
+ const choices = request.choices;
166
+ const choose = this.deps.consent.choose;
167
+ if (choices === undefined || choices.length === 0 || choose === undefined) {
168
+ return { ok: true, choice: choices?.[0]?.id };
169
+ }
170
+ const picked = await choose({ title: request.title, message: request.message, choices });
171
+ if (picked.kind === "cancelled") {
172
+ return refuse("declined", "The person at the computer said no.");
173
+ }
174
+ return { ok: true, choice: picked.kind === "chosen" ? picked.id : choices[0]?.id };
175
+ }
116
176
  #copiesOf(doc, entries) {
117
177
  const copies = new Map();
118
178
  for (const entry of entries) {
@@ -142,6 +202,7 @@ export class Keyring {
142
202
  pendingInbox: pending?.pendingInbox ?? file.open.pendingInbox,
143
203
  pendingSync: pending?.pendingSync ?? file.open.pendingSync,
144
204
  };
205
+ this.#openExtras(open, doc, file.open, pending);
145
206
  const next = { ...file, sealed: sealDocument(opened.key, doc), open };
146
207
  await this.deps.vault.write(next);
147
208
  this.#file = next;
@@ -152,6 +213,27 @@ export class Keyring {
152
213
  this.#unlocked = { ...this.#unlocked, doc };
153
214
  }
154
215
  }
216
+ #openExtras(open, doc, before, pending) {
217
+ if (doc.settings !== undefined) {
218
+ open.settings = doc.settings;
219
+ }
220
+ const phones = Object.entries(doc.phones ?? {}).map(([device, pin]) => ({ device, label: pin.keys.label, pairedAt: pin.pairedAt }));
221
+ if (phones.length > 0) {
222
+ open.phones = phones;
223
+ }
224
+ const captures = pending?.pendingCaptures ?? before.pendingCaptures;
225
+ if (captures !== undefined && captures.length > 0) {
226
+ open.pendingCaptures = captures;
227
+ }
228
+ const needs = pending?.pendingNeeds ?? before.pendingNeeds;
229
+ if (needs !== undefined && needs.length > 0) {
230
+ open.pendingNeeds = needs;
231
+ }
232
+ const seen = pending?.pendingSeen ?? before.pendingSeen;
233
+ if (seen !== undefined && seen.length > 0) {
234
+ open.pendingSeen = seen;
235
+ }
236
+ }
155
237
  async #writeOpen(open) {
156
238
  const file = this.#file;
157
239
  if (file !== undefined) {
@@ -188,7 +270,10 @@ export class Keyring {
188
270
  }
189
271
  async handle(request) {
190
272
  await this.#load();
191
- return isRelayed(request) ? await this.#relayed(request) : await this.#local(request);
273
+ if (isRelayed(request)) {
274
+ return await this.#relayed(request);
275
+ }
276
+ return isApp(request) ? await this.#app(request) : await this.#local(request);
192
277
  }
193
278
  async #local(request) {
194
279
  switch (request.op) {
@@ -217,11 +302,49 @@ export class Keyring {
217
302
  case "backend":
218
303
  return await this.#lock(async () => await this.backend(request.config));
219
304
  case "askpass":
220
- return await this.askpass(request.prompt, request.parent);
305
+ return await this.#askpass.askpass(request.prompt, request.parent);
306
+ case "git-credential":
307
+ return await this.#lock(async () => await this.#askpass.gitCredential(request.action, request.url, request.username));
221
308
  case "consent-upgrade":
222
309
  return await this.consentUpgrade(request.version);
223
310
  }
224
311
  }
312
+ async #app(request) {
313
+ switch (request.op) {
314
+ case "capture-batch":
315
+ return await this.#lock(async () => await this.#captures.batch(request.items, request.from));
316
+ case "undo-capture":
317
+ return await this.#lock(async () => await this.#captures.undo(request.name, request.at));
318
+ case "seen":
319
+ return await this.#lock(async () => await this.#captures.seen(request.names));
320
+ case "reveal":
321
+ return await this.#appOps.reveal(request.name, request.version);
322
+ case "copy":
323
+ return await this.#appOps.copy(request.name, request.field, request.clearAfterSeconds);
324
+ case "history":
325
+ return await this.#appOps.history(request.name);
326
+ case "restore":
327
+ return await this.#lock(async () => await this.#appOps.restore(request.name, request.version));
328
+ case "settings": {
329
+ const phones = this.#file?.open.phones;
330
+ return phones === undefined ? { ok: true, settings: this.#appOps.settings() } : { ok: true, settings: this.#appOps.settings(), phones };
331
+ }
332
+ case "set-settings":
333
+ return await this.#lock(async () => await this.#appOps.setSettings(request.patch));
334
+ case "activity":
335
+ return await this.#appOps.activity(request.name, request.limit);
336
+ case "fill-window":
337
+ return await this.#lock(async () => await this.#fills.fillWindow(request.name));
338
+ case "fill-value":
339
+ return await this.#lock(async () => await this.#appOps.fillValue(request.origin, request.name));
340
+ case "candidates":
341
+ return this.#appOps.candidates(request.origin);
342
+ case "needs":
343
+ return this.#needs.needs();
344
+ case "answer-need":
345
+ return await this.#lock(async () => await this.#needs.answerNeed(request.id, request.answer, request.name, request.sealed, request.at));
346
+ }
347
+ }
225
348
  async #relayed(request) {
226
349
  switch (request.op) {
227
350
  case "shares":
@@ -242,6 +365,14 @@ export class Keyring {
242
365
  return await this.#lock(async () => await this.syncOutbox());
243
366
  case "sync-deliver":
244
367
  return await this.#lock(async () => await this.syncDeliver(request.deliver.items));
368
+ case "capture":
369
+ return await this.#lock(async () => await this.#captures.capture(request.item, request.sandbox));
370
+ case "need":
371
+ return await this.#needs.need(request.sandbox, request.need);
372
+ case "phone-pair":
373
+ return await this.#lock(async () => await this.#phones.pair(request.sandbox, request.phone));
374
+ case "phone-fill":
375
+ return await this.#lock(async () => await this.#phones.fill(request.sandbox, request.fill));
245
376
  }
246
377
  }
247
378
  async state() {
@@ -255,7 +386,11 @@ export class Keyring {
255
386
  locked: held === undefined,
256
387
  unlock: helloReady ? ["hello", "passphrase"] : ["passphrase"],
257
388
  pendingInbox: file?.open.pendingInbox.length ?? 0,
389
+ pendingCaptures: file?.open.pendingCaptures?.length ?? 0,
258
390
  };
391
+ if (file !== undefined) {
392
+ state.remember = (held === undefined ? file.open.settings : held.doc.settings)?.remember ?? DEFAULT_SETTINGS.remember;
393
+ }
259
394
  if (this.#unlocked !== undefined && held !== undefined) {
260
395
  state.unlockedUntil = this.#unlocked.until;
261
396
  }
@@ -296,10 +431,12 @@ export class Keyring {
296
431
  return refuse("locked", NOT_SET_UP);
297
432
  }
298
433
  const pending = file.open.pendingInbox;
434
+ const captured = file.open.pendingCaptures?.length ?? 0;
299
435
  const waiting = pending.length === 0 ? "" : ` Waiting to be taken in: ${pending.map((item) => `${item.item.name} from ${hostOf(item.item.sandbox)}`).join(", ")}.`;
436
+ const saving = captured === 0 ? "" : ` And save ${String(captured)} new ${captured === 1 ? "password" : "passwords"}.`;
300
437
  const opened = await this.#open(file, {
301
438
  title: "Unlock the Intentic keyring",
302
- message: `For ${String(minutes)} minutes, entries shared "while you're here" go to their sandboxes without asking.${waiting}`,
439
+ message: `For ${String(minutes)} minutes, entries shared "while you're here" go to their sandboxes without asking.${waiting}${saving}`,
303
440
  person: true,
304
441
  });
305
442
  if ("ok" in opened) {
@@ -314,7 +451,9 @@ export class Keyring {
314
451
  }
315
452
  async #takeInPending(opened) {
316
453
  const file = this.#file;
317
- if (file === undefined || (file.open.pendingInbox.length === 0 && file.open.pendingSync.length === 0)) {
454
+ const captures = file?.open.pendingCaptures ?? [];
455
+ const seen = file?.open.pendingSeen ?? [];
456
+ if (file === undefined || (file.open.pendingInbox.length === 0 && file.open.pendingSync.length === 0 && captures.length === 0 && seen.length === 0)) {
318
457
  return;
319
458
  }
320
459
  let doc = opened.doc;
@@ -328,12 +467,25 @@ export class Keyring {
328
467
  }
329
468
  }
330
469
  doc = this.#mergeSync(doc, file.open.pendingSync).doc;
331
- await this.#commit({ doc, key: opened.key }, { pendingInbox: [], pendingSync: [] });
470
+ doc = seenIn(this.#captures.takeIn(doc, captures), new Set(seen));
471
+ await this.#commit({ doc, key: opened.key }, { pendingInbox: [], pendingSync: [], pendingCaptures: [], pendingSeen: [] });
332
472
  }
333
473
  list() {
334
474
  const held = this.#held();
335
475
  const open = this.#file?.open;
336
- const entries = held === undefined ? (open?.entries ?? []) : Object.values(held.doc.entries).filter((entry) => entry.deleted !== true).map(entryInfoOf);
476
+ if (held === undefined) {
477
+ return { ok: true, entries: open?.entries ?? [], backends: open?.backends ?? [] };
478
+ }
479
+ const live = liveEntries(held.doc);
480
+ const health = healthOf(live);
481
+ const entries = live.map((entry) => {
482
+ const info = entryInfoOf(entry);
483
+ const verdict = health.get(entry.name);
484
+ if (verdict !== undefined) {
485
+ info.health = verdict;
486
+ }
487
+ return info;
488
+ });
337
489
  return { ok: true, entries, backends: open?.backends ?? [] };
338
490
  }
339
491
  #sentValue(doc, name, sealed) {
@@ -575,6 +727,7 @@ export class Keyring {
575
727
  const sealed = target.target.kind === "relay"
576
728
  ? seal(target.target.sealTo, value.value, { kind: "type", sandbox: grant.sandbox, secret: grant.secret, nonce: grant.nonce })
577
729
  : seal(pin.seal, value.value, { kind: "release", sandbox: grant.sandbox, secret: grant.secret, nonce: grant.nonce });
730
+ await this.#markUsed(granted.opened, granted.entry);
578
731
  await this.#record({
579
732
  op: "release",
580
733
  outcome: "granted",
@@ -657,7 +810,17 @@ export class Keyring {
657
810
  return refuse("guard", onFile.verdict === "refuse" ? onFile.reason : "The entry on file differs from what this use was judged against: the sandbox asks again.");
658
811
  }
659
812
  const value = await this.#valueOf(granted.opened.doc, granted.entry);
660
- return value.ok ? await this.#typeInto(place, seen, value.value, granted.opened.asked) : value;
813
+ if (!value.ok) {
814
+ return value;
815
+ }
816
+ const typed = await this.#typeInto(place, seen, value.value, granted.opened.asked);
817
+ if (typed.ok) {
818
+ await this.#markUsed(granted.opened, granted.entry);
819
+ }
820
+ return typed;
821
+ }
822
+ async #markUsed(opened, entry) {
823
+ await this.#commit({ doc: withEntry(opened.doc, usedEntry(entry, this.deps.now())), key: opened.key });
661
824
  }
662
825
  async #typeInto(place, seen, value, asked) {
663
826
  let now = seen;
@@ -831,42 +994,6 @@ export class Keyring {
831
994
  }
832
995
  return { doc: { ...doc, entries }, accepted };
833
996
  }
834
- async askpass(prompt, parent) {
835
- const file = this.#file;
836
- if (file === undefined) {
837
- return refuse("locked", NOT_SET_UP);
838
- }
839
- const question = parseAskpassPrompt(prompt);
840
- const shown = file.open.entries.find((entry) => askpassMatch(question, entry.askpass) !== undefined);
841
- const matcher = shown === undefined ? undefined : askpassMatch(question, shown.askpass);
842
- if (shown === undefined || matcher === undefined) {
843
- return refuse("unknown", `No keyring entry answers "${question.prompt}": \`intentic-machine keyring edit NAME --askpass ${question.matchers[0] ?? "prompt:…"}\` adds one.`);
844
- }
845
- if (question.wants === "username") {
846
- return shown.username === undefined ? refuse("unknown", `${shown.name} has no user name to answer with.`) : { ok: true, value: shown.username };
847
- }
848
- const now = this.deps.now();
849
- const remembered = this.#held() !== undefined && rememberedFresh(this.#remembered.get(matcher), shown.askpassRememberMinutes, now);
850
- const asker = parent ?? "A program";
851
- const opened = await this.#open(file, { title: `Answer ${asker}'s prompt?`, message: `${asker} asks: "${question.prompt}". The keyring answers with ${shown.name}.`, person: !remembered });
852
- if ("ok" in opened) {
853
- await this.#record({ op: "askpass", outcome: "refused", code: opened.code, secret: shown.name, target: matcher });
854
- return opened;
855
- }
856
- const entry = opened.doc.entries[shown.name];
857
- if (entry === undefined || entry.deleted === true || askpassMatch(question, entry.rules.askpass) === undefined) {
858
- return refuse("unknown", `No keyring entry answers "${question.prompt}".`);
859
- }
860
- const value = await this.#valueOf(opened.doc, entry);
861
- if (!value.ok) {
862
- return value;
863
- }
864
- if (entry.rules.askpassRememberMinutes > 0) {
865
- this.#remembered.set(matcher, now);
866
- }
867
- await this.#record({ op: "askpass", outcome: "granted", secret: entry.name, target: matcher, asked: opened.asked, program: parent });
868
- return { ok: true, value: value.value };
869
- }
870
997
  async consentUpgrade(version) {
871
998
  const file = this.#file;
872
999
  const answer = await this.deps.consent.ask({