brane 0.0.2 → 0.3.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 (338) hide show
  1. package/AGENTS.md +362 -0
  2. package/CHANGELOG.md +33 -0
  3. package/CONTRIBUTING.md +91 -0
  4. package/DESIGN.md +402 -0
  5. package/Dockerfile +61 -0
  6. package/LICENSE +656 -17
  7. package/README.md +174 -2
  8. package/SECURITY.md +205 -0
  9. package/dist/absorb/brane-id.js +11 -0
  10. package/dist/absorb/chatgpt.js +161 -0
  11. package/dist/absorb/claude-code.js +504 -0
  12. package/dist/absorb/index.js +543 -0
  13. package/dist/absorb/organelle.js +345 -0
  14. package/dist/absorb/run.js +520 -0
  15. package/dist/absorb/sources.js +168 -0
  16. package/dist/absorb/types.js +3 -0
  17. package/dist/agent-env.js +45 -0
  18. package/dist/agents.js +1168 -0
  19. package/dist/claude-live-session.js +124 -0
  20. package/dist/cli.js +50 -0
  21. package/dist/codex-tool-log.js +144 -0
  22. package/dist/commands/absorb.js +291 -0
  23. package/dist/commands/attach.js +31 -0
  24. package/dist/commands/certify.js +62 -0
  25. package/dist/commands/config.js +116 -0
  26. package/dist/commands/cost.js +126 -0
  27. package/dist/commands/daemon.js +350 -0
  28. package/dist/commands/doctor.js +262 -0
  29. package/dist/commands/fold.js +95 -0
  30. package/dist/commands/list.js +89 -0
  31. package/dist/commands/mcp.js +84 -0
  32. package/dist/commands/meld.js +332 -0
  33. package/dist/commands/memory.js +215 -0
  34. package/dist/commands/revive.js +74 -0
  35. package/dist/commands/run.js +85 -0
  36. package/dist/commands/spawn.js +1094 -0
  37. package/dist/commands/token.js +153 -0
  38. package/dist/copilot-tool-log.js +145 -0
  39. package/dist/cost/pricing.js +116 -0
  40. package/dist/cost/reprice.js +136 -0
  41. package/dist/crush-tool-log.js +293 -0
  42. package/dist/daemon/agent-scope.js +116 -0
  43. package/dist/daemon/awaiting.js +54 -0
  44. package/dist/daemon/cert-manager.js +349 -0
  45. package/dist/daemon/client.js +262 -0
  46. package/dist/daemon/gated-routes.js +112 -0
  47. package/dist/daemon/keep-awake.js +103 -0
  48. package/dist/daemon/local-socket.js +145 -0
  49. package/dist/daemon/presence-gate.js +206 -0
  50. package/dist/daemon/server.js +6349 -0
  51. package/dist/daemon/signal-source.js +35 -0
  52. package/dist/daemon/tailscale-rebind.js +58 -0
  53. package/dist/daemon/token-ring.js +263 -0
  54. package/dist/daemon/token-routes.js +190 -0
  55. package/dist/db/events.js +152 -0
  56. package/dist/db/mentions.js +179 -0
  57. package/dist/db/schema.js +633 -0
  58. package/dist/db/tokens.js +583 -0
  59. package/dist/db/tool-uses.js +647 -0
  60. package/dist/diagram/index.js +85 -0
  61. package/dist/diagram/label.js +455 -0
  62. package/dist/diagram/layout.js +476 -0
  63. package/dist/diagram/links.js +50 -0
  64. package/dist/diagram/metrics.js +152 -0
  65. package/dist/diagram/parse.js +487 -0
  66. package/dist/diagram/svg.js +389 -0
  67. package/dist/diagram/types.js +9 -0
  68. package/dist/export.js +629 -0
  69. package/dist/files/archive-types.js +2 -0
  70. package/dist/files/archive.js +182 -0
  71. package/dist/files/browse.js +327 -0
  72. package/dist/files/bytes.js +162 -0
  73. package/dist/files/codecs.js +33 -0
  74. package/dist/files/find.js +212 -0
  75. package/dist/files/tar.js +223 -0
  76. package/dist/files/zip.js +209 -0
  77. package/dist/folds.js +72 -0
  78. package/dist/grammars/ebnf.js +92 -0
  79. package/dist/highlight.js +250 -0
  80. package/dist/ids.js +24 -0
  81. package/dist/markdown-diagnostics.js +233 -0
  82. package/dist/math.js +168 -0
  83. package/dist/mcp/server.js +886 -0
  84. package/dist/memory/embedder.js +145 -0
  85. package/dist/memory/embedding-worker.js +71 -0
  86. package/dist/memory/embeddings.js +91 -0
  87. package/dist/memory/index.js +232 -0
  88. package/dist/memory/storage.js +587 -0
  89. package/dist/memory/wiki.js +688 -0
  90. package/dist/memory/wordpiece.js +247 -0
  91. package/dist/mentions-scan.js +40 -0
  92. package/dist/presence.js +259 -0
  93. package/dist/session-affinity.js +185 -0
  94. package/dist/shared/api-request.js +124 -0
  95. package/dist/shared/block-glyphs.js +114 -0
  96. package/dist/shared/diagram-css.js +17 -0
  97. package/dist/shared/export-selection.js +101 -0
  98. package/dist/shared/find-in-text.js +110 -0
  99. package/dist/shared/glow-hues.js +225 -0
  100. package/dist/shared/hex-dump.js +62 -0
  101. package/dist/shared/hex-pages.js +52 -0
  102. package/dist/shared/hit-drum.js +79 -0
  103. package/dist/shared/known-nids.js +58 -0
  104. package/dist/shared/list-scroll-keys.js +56 -0
  105. package/dist/shared/markdown-lists.js +72 -0
  106. package/dist/shared/markdown-math.js +485 -0
  107. package/dist/shared/markdown-mentions.js +79 -0
  108. package/dist/shared/markdown-nids.js +41 -0
  109. package/dist/shared/markdown-protocols.js +53 -0
  110. package/dist/shared/markdown-refs.js +123 -0
  111. package/dist/shared/membrane-invasion.js +342 -0
  112. package/dist/shared/membrane-motion.js +83 -0
  113. package/dist/shared/membrane-palette.js +253 -0
  114. package/dist/shared/membrane-terrain.js +212 -0
  115. package/dist/shared/message-selection.js +123 -0
  116. package/dist/shared/mo-glyph.js +57 -0
  117. package/dist/shared/narrative-format.js +237 -0
  118. package/dist/shared/narrative-menu.js +82 -0
  119. package/dist/shared/narrative-sort.js +47 -0
  120. package/dist/shared/narrative-status.js +52 -0
  121. package/dist/shared/no-pan.js +85 -0
  122. package/dist/shared/pane-layout.js +50 -0
  123. package/dist/shared/payload-inline.js +83 -0
  124. package/dist/shared/speaker-color.js +61 -0
  125. package/dist/shared/token-rotation.js +14 -0
  126. package/dist/shared/view-url.js +197 -0
  127. package/dist/shared/wiki-links.js +70 -0
  128. package/dist/shared/wiki-window.js +4 -0
  129. package/dist/tailscale.js +55 -0
  130. package/dist/test/absorb-claude.test.js +892 -0
  131. package/dist/test/agent-scope.test.js +193 -0
  132. package/dist/test/agents.test.js +55 -0
  133. package/dist/test/api-request.test.js +122 -0
  134. package/dist/test/awaiting-glow.test.js +54 -0
  135. package/dist/test/awaiting.test.js +122 -0
  136. package/dist/test/bind-token.test.js +88 -0
  137. package/dist/test/block-glyphs.test.js +68 -0
  138. package/dist/test/cert-manager.test.js +262 -0
  139. package/dist/test/claude-live-session.test.js +110 -0
  140. package/dist/test/codex-tool-log.test.js +568 -0
  141. package/dist/test/config.test.js +71 -0
  142. package/dist/test/copilot-tool-log.test.js +93 -0
  143. package/dist/test/cors-preflight.test.js +49 -0
  144. package/dist/test/cost-linger.test.js +31 -0
  145. package/dist/test/cost-pricing.test.js +72 -0
  146. package/dist/test/cost-reprice.test.js +139 -0
  147. package/dist/test/cost-sessions.test.js +301 -0
  148. package/dist/test/crush-tool-log.test.js +289 -0
  149. package/dist/test/daemon-client.test.js +109 -0
  150. package/dist/test/daemon-socket.test.js +293 -0
  151. package/dist/test/diagram-arrows.test.js +205 -0
  152. package/dist/test/diagram-export.test.js +210 -0
  153. package/dist/test/diagram-labels.test.js +313 -0
  154. package/dist/test/diagram-parse.test.js +191 -0
  155. package/dist/test/diagram-render.test.js +175 -0
  156. package/dist/test/diagram-review.test.js +288 -0
  157. package/dist/test/diagram-self-loop-labels.test.js +303 -0
  158. package/dist/test/embedder.test.js +84 -0
  159. package/dist/test/events.test.js +123 -0
  160. package/dist/test/export-selection.test.js +102 -0
  161. package/dist/test/export.test.js +505 -0
  162. package/dist/test/files-browse.test.js +393 -0
  163. package/dist/test/files-find.test.js +272 -0
  164. package/dist/test/find-in-text.test.js +86 -0
  165. package/dist/test/first-paint-viewport.test.js +59 -0
  166. package/dist/test/fixtures/embedding-golden.js +23 -0
  167. package/dist/test/fixtures/wordpiece-golden.js +167 -0
  168. package/dist/test/glow-hues.test.js +202 -0
  169. package/dist/test/harness-cost.test.js +423 -0
  170. package/dist/test/helpers.js +21 -0
  171. package/dist/test/hex-pages.test.js +68 -0
  172. package/dist/test/highlight.test.js +69 -0
  173. package/dist/test/hit-drum.test.js +71 -0
  174. package/dist/test/hook-config.test.js +262 -0
  175. package/dist/test/json-gzip.test.js +70 -0
  176. package/dist/test/key-routing.test.js +58 -0
  177. package/dist/test/known-nids.test.js +106 -0
  178. package/dist/test/link-preview.test.js +67 -0
  179. package/dist/test/list-scroll-keys.test.js +66 -0
  180. package/dist/test/listen-own-words.test.js +147 -0
  181. package/dist/test/listen-timeout.test.js +33 -0
  182. package/dist/test/markdown-diagnostics.test.js +121 -0
  183. package/dist/test/markdown-lists.test.js +182 -0
  184. package/dist/test/markdown-math.test.js +256 -0
  185. package/dist/test/markdown-nids.test.js +146 -0
  186. package/dist/test/markdown-protocols.test.js +139 -0
  187. package/dist/test/mcp-bridge-cancel.test.js +98 -0
  188. package/dist/test/mcp-bridge-reconnect.test.js +121 -0
  189. package/dist/test/mcp-narrative.test.js +193 -0
  190. package/dist/test/mcp.test.js +548 -0
  191. package/dist/test/meld.test.js +85 -0
  192. package/dist/test/membrane-invasion.test.js +482 -0
  193. package/dist/test/membrane-motion.test.js +101 -0
  194. package/dist/test/membrane-palette.test.js +166 -0
  195. package/dist/test/membrane-terrain.test.js +170 -0
  196. package/dist/test/memory-log.test.js +202 -0
  197. package/dist/test/mentions-e2e.test.js +261 -0
  198. package/dist/test/mentions-ledger.test.js +265 -0
  199. package/dist/test/mentions-listen.test.js +265 -0
  200. package/dist/test/mentions.test.js +238 -0
  201. package/dist/test/message-selection.test.js +114 -0
  202. package/dist/test/mo-glyph.test.js +22 -0
  203. package/dist/test/model-resolution.test.js +166 -0
  204. package/dist/test/narrate-validate.test.js +77 -0
  205. package/dist/test/narrate-wait.test.js +160 -0
  206. package/dist/test/narrative-format.test.js +219 -0
  207. package/dist/test/narrative-menu.test.js +137 -0
  208. package/dist/test/narrative-sort.test.js +75 -0
  209. package/dist/test/no-pan.test.js +60 -0
  210. package/dist/test/pane-layout.test.js +153 -0
  211. package/dist/test/paths.test.js +37 -0
  212. package/dist/test/payload-inline.test.js +91 -0
  213. package/dist/test/precache-fonts.test.js +72 -0
  214. package/dist/test/presence-gate.test.js +440 -0
  215. package/dist/test/presence.test.js +160 -0
  216. package/dist/test/read-events.test.js +220 -0
  217. package/dist/test/remote-meld.test.js +115 -0
  218. package/dist/test/revive-cli.test.js +93 -0
  219. package/dist/test/search-floor.test.js +206 -0
  220. package/dist/test/search-fusion.test.js +168 -0
  221. package/dist/test/search.test.js +205 -0
  222. package/dist/test/security.test.js +48 -0
  223. package/dist/test/selection-wiring.test.js +61 -0
  224. package/dist/test/session-affinity.test.js +295 -0
  225. package/dist/test/signal-source.test.js +32 -0
  226. package/dist/test/socket-account.test.js +268 -0
  227. package/dist/test/spawn-toml.test.js +72 -0
  228. package/dist/test/speaker-color.test.js +90 -0
  229. package/dist/test/tailscale-rebind.test.js +79 -0
  230. package/dist/test/terminal-cursor.test.js +61 -0
  231. package/dist/test/terminal-resync.test.js +23 -0
  232. package/dist/test/thin-cli.test.js +382 -0
  233. package/dist/test/token-bootstrap.test.js +272 -0
  234. package/dist/test/token-devices.test.js +587 -0
  235. package/dist/test/token-lapse.test.js +94 -0
  236. package/dist/test/token-ring.test.js +188 -0
  237. package/dist/test/token-rotate-cli.test.js +58 -0
  238. package/dist/test/token-routes.test.js +95 -0
  239. package/dist/test/tool-uses.test.js +1006 -0
  240. package/dist/test/usage-ledger.test.js +126 -0
  241. package/dist/test/view-url.test.js +581 -0
  242. package/dist/test/wiki-links.test.js +57 -0
  243. package/dist/test/wiki-sources.test.js +85 -0
  244. package/dist/test/wiki-window.test.js +31 -0
  245. package/dist/test/wiki.test.js +399 -0
  246. package/dist/test/wordpiece.test.js +68 -0
  247. package/dist/tls.js +148 -0
  248. package/dist/tmux.js +202 -0
  249. package/dist/user-agent.js +46 -0
  250. package/docker-entrypoint.sh +141 -0
  251. package/docs/README.md +3 -0
  252. package/docs/absorb.md +231 -0
  253. package/docs/cost-ledger.md +125 -0
  254. package/docs/daemon-api.md +189 -0
  255. package/docs/diagrams.md +163 -0
  256. package/docs/doctor.md +36 -0
  257. package/docs/export-scope.md +69 -0
  258. package/docs/files-view.md +133 -0
  259. package/docs/link-previews.md +38 -0
  260. package/docs/math.md +32 -0
  261. package/docs/mcp.md +69 -0
  262. package/docs/memory-search.md +75 -0
  263. package/docs/mentions.md +101 -0
  264. package/docs/multi-brane.md +14 -0
  265. package/docs/passkeys.md +165 -0
  266. package/docs/presence.md +202 -0
  267. package/docs/releasing.md +53 -0
  268. package/docs/rendering-warnings.md +22 -0
  269. package/docs/request-layer.md +72 -0
  270. package/docs/selection-store.md +50 -0
  271. package/docs/session-affinity.md +61 -0
  272. package/docs/terminal-mirroring.md +211 -0
  273. package/docs/token-rotation.md +413 -0
  274. package/docs/view-urls.md +114 -0
  275. package/docs/web-app.md +23 -0
  276. package/docs/wiki.md +43 -0
  277. package/expo/dist/_expo/static/js/web/entry-e56efc1291dca7258ea00b8108b033a4.js +1097 -0
  278. package/expo/dist/apple-touch-icon.png +0 -0
  279. package/expo/dist/favicon.svg +3 -0
  280. package/expo/dist/index.html +177 -0
  281. package/expo/dist/katex/fonts/KaTeX_AMS-Regular.woff2 +0 -0
  282. package/expo/dist/katex/fonts/KaTeX_Caligraphic-Bold.woff2 +0 -0
  283. package/expo/dist/katex/fonts/KaTeX_Caligraphic-Regular.woff2 +0 -0
  284. package/expo/dist/katex/fonts/KaTeX_Fraktur-Bold.woff2 +0 -0
  285. package/expo/dist/katex/fonts/KaTeX_Fraktur-Regular.woff2 +0 -0
  286. package/expo/dist/katex/fonts/KaTeX_Main-Bold.woff2 +0 -0
  287. package/expo/dist/katex/fonts/KaTeX_Main-BoldItalic.woff2 +0 -0
  288. package/expo/dist/katex/fonts/KaTeX_Main-Italic.woff2 +0 -0
  289. package/expo/dist/katex/fonts/KaTeX_Main-Regular.woff2 +0 -0
  290. package/expo/dist/katex/fonts/KaTeX_Math-BoldItalic.woff2 +0 -0
  291. package/expo/dist/katex/fonts/KaTeX_Math-Italic.woff2 +0 -0
  292. package/expo/dist/katex/fonts/KaTeX_SansSerif-Bold.woff2 +0 -0
  293. package/expo/dist/katex/fonts/KaTeX_SansSerif-Italic.woff2 +0 -0
  294. package/expo/dist/katex/fonts/KaTeX_SansSerif-Regular.woff2 +0 -0
  295. package/expo/dist/katex/fonts/KaTeX_Script-Regular.woff2 +0 -0
  296. package/expo/dist/katex/fonts/KaTeX_Size1-Regular.woff2 +0 -0
  297. package/expo/dist/katex/fonts/KaTeX_Size2-Regular.woff2 +0 -0
  298. package/expo/dist/katex/fonts/KaTeX_Size3-Regular.woff2 +0 -0
  299. package/expo/dist/katex/fonts/KaTeX_Size4-Regular.woff2 +0 -0
  300. package/expo/dist/katex/fonts/KaTeX_Typewriter-Regular.woff2 +0 -0
  301. package/expo/dist/katex/katex.min.css +1 -0
  302. package/expo/dist/manifest.json +16 -0
  303. package/expo/dist/metadata.json +1 -0
  304. package/expo/dist/og/membrane.v1.png +0 -0
  305. package/expo/dist/pdf.min.mjs +28 -0
  306. package/expo/dist/pdf.worker.min.mjs +28 -0
  307. package/expo/dist/sw.js +154 -0
  308. package/package.json +81 -23
  309. package/protocols/LIVE_loop.md +407 -0
  310. package/protocols/bebrief.md +41 -0
  311. package/protocols/digest.md +231 -0
  312. package/protocols/fly.md +266 -0
  313. package/protocols/idle.md +81 -0
  314. package/protocols/keeper.md +829 -0
  315. package/protocols/markdown.md +80 -0
  316. package/protocols/memory.md +419 -0
  317. package/protocols/mentions.md +93 -0
  318. package/protocols/payloads.md +53 -0
  319. package/protocols/prelude.md +91 -0
  320. package/protocols/ramify.md +105 -0
  321. package/protocols/review.md +356 -0
  322. package/protocols/rrr.md +232 -0
  323. package/protocols/scribe.md +429 -0
  324. package/protocols/summarize.md +203 -0
  325. package/protocols/twin.md +174 -0
  326. package/protocols/wiki.md +438 -0
  327. package/protocols/worker.md +38 -0
  328. package/wiki/shared/append-only-event-log.md +148 -0
  329. package/wiki/shared/index.md +48 -0
  330. package/wiki/shared/owning-your-narratives.md +161 -0
  331. package/wiki/shared/premature-ontologies.md +435 -0
  332. package/wiki/shared/sedimentary-superintelligence.md +128 -0
  333. package/wiki/shared/sequestering-the-narrative.md +143 -0
  334. package/wiki/shared/why-wikis-work.md +216 -0
  335. package/.npmignore +0 -2
  336. package/.travis.yml +0 -5
  337. package/index.js +0 -8
  338. package/index.js~ +0 -12
@@ -0,0 +1,440 @@
1
+ /**
2
+ * The presence gate (src/daemon/presence-gate.ts): which callers and acts it
3
+ * holds, and how it behaves while a prompt is up. Every prompt here is a
4
+ * fake `confirm`, so nothing appears on anyone's screen.
5
+ */
6
+ import { describe, it } from "node:test";
7
+ import assert from "node:assert/strict";
8
+ import { createTestDb } from "./helpers.js";
9
+ import { DbTokenStore } from "../db/tokens.js";
10
+ import { TokenRing, hashToken } from "../daemon/token-ring.js";
11
+ import { PRESENCE_PROMPTS, PresenceGate, gateStanding, revokeSignsOutDevice, } from "../daemon/presence-gate.js";
12
+ import { parseMintBody, planRevoke, revokeTokenRoute } from "../daemon/token-routes.js";
13
+ import { cancelPresenceRoute, gatedMintRoute, gatedRemoteMintRoute, gatedRevokeRoute, gatedRotateRoute } from "../daemon/gated-routes.js";
14
+ import { announcePresenceWait, presenceWaitNote } from "../commands/meld.js";
15
+ const T0 = Date.UTC(2026, 9, 5, 12);
16
+ /** A `confirm` the test answers by hand, recording what it was asked. */
17
+ function manualConfirm() {
18
+ const asked = [];
19
+ const confirm = (reason, { timeoutMs, signal }) => new Promise((resolve) => {
20
+ asked.push({ reason, timeoutMs, signal, answer: resolve });
21
+ // As runPlan does: an abort kills the helper and reports `aborted`.
22
+ signal.addEventListener("abort", () => resolve({ ok: false, method: "macos-local-auth", reason: "aborted" }), { once: true });
23
+ });
24
+ return { asked, confirm };
25
+ }
26
+ function gate(confirm, clock = { t: T0 }) {
27
+ const changes = [];
28
+ const lines = [];
29
+ const g = new PresenceGate({
30
+ confirm,
31
+ now: () => clock.t,
32
+ timeoutMs: 45_000,
33
+ cooldownMs: 15_000,
34
+ onChange: (p) => changes.push(p),
35
+ log: (l) => lines.push(l),
36
+ });
37
+ return { g, changes, lines, clock };
38
+ }
39
+ const tick = () => new Promise((r) => setImmediate(r));
40
+ describe("who the gate holds", () => {
41
+ it("asks this machine's shell, refuses an agent unasked, and lets a device by", () => {
42
+ assert.equal(gateStanding({ kind: "local" }), "ask");
43
+ assert.equal(gateStanding({ kind: "agent" }), "refuse");
44
+ assert.equal(gateStanding({ kind: "device" }), "pass");
45
+ });
46
+ it("gates a revoke only when it signs out a device", () => {
47
+ const db = createTestDb();
48
+ const store = new DbTokenStore(db);
49
+ const phone = store.insert(hashToken("phone"), "device", "phone", T0);
50
+ const agent = store.insert(hashToken("agent"), "agent", "agent", T0);
51
+ assert.equal(revokeSignsOutDevice(store, [phone.id]), true);
52
+ assert.equal(revokeSignsOutDevice(store, [agent.id]), false);
53
+ assert.equal(revokeSignsOutDevice(store, [agent.id, phone.id]), true);
54
+ assert.equal(revokeSignsOutDevice(store, ["no-such-id"]), false);
55
+ });
56
+ it("words each prompt itself, the same every time", () => {
57
+ assert.deepEqual(PRESENCE_PROMPTS, {
58
+ meld: "add a device to brane",
59
+ "meld-remote": "add a device to brane on another machine",
60
+ "revoke-device": "sign out a device from brane",
61
+ rotate: "sign out every other device from brane",
62
+ });
63
+ });
64
+ });
65
+ describe("the presence gate", () => {
66
+ it("lets an approved act through, announcing the prompt while it is up", async () => {
67
+ const { asked, confirm } = manualConfirm();
68
+ const { g, changes, lines } = gate(confirm);
69
+ const outcome = g.check("meld", "this machine (daemon.sock)");
70
+ await tick();
71
+ assert.equal(asked.length, 1);
72
+ assert.equal(asked[0].reason, "add a device to brane");
73
+ assert.equal(asked[0].timeoutMs, 45_000);
74
+ assert.deepEqual(g.current(), {
75
+ id: "presence-1",
76
+ act: "meld",
77
+ prompt: "add a device to brane",
78
+ by: "this machine (daemon.sock)",
79
+ startedAt: T0,
80
+ expiresAt: T0 + 45_000,
81
+ });
82
+ asked[0].answer({ ok: true, method: "macos-local-auth" });
83
+ assert.deepEqual(await outcome, { ok: true });
84
+ assert.equal(g.current(), null);
85
+ assert.equal(changes.length, 2);
86
+ assert.equal(changes[0]?.id, "presence-1");
87
+ assert.equal(changes[1], null);
88
+ assert.deepEqual(lines, ["[presence] meld for this machine (daemon.sock): approved (macos-local-auth)"]);
89
+ });
90
+ it("refuses a second request while a prompt is up, without queueing it", async () => {
91
+ const { asked, confirm } = manualConfirm();
92
+ const { g } = gate(confirm);
93
+ const first = g.check("meld", "a");
94
+ await tick();
95
+ const second = await g.check("rotate", "b");
96
+ assert.equal(second.ok, false);
97
+ assert.equal(second.ok === false && second.status, 409);
98
+ assert.equal(second.ok === false && second.reason, "busy");
99
+ assert.equal(asked.length, 1, "no second prompt was raised");
100
+ // It names the prompt that is up and who asked, which may not be you.
101
+ assert.match(second.ok === false ? second.error : "", /\("add a device to brane", asked by a\)/);
102
+ assert.match(second.ok === false ? second.error : "", /If you did not start it/);
103
+ asked[0].answer({ ok: true, method: "macos-local-auth" });
104
+ assert.deepEqual(await first, { ok: true });
105
+ });
106
+ it("cools down after a prompt that was not approved, then asks again", async () => {
107
+ const { asked, confirm } = manualConfirm();
108
+ const { g, clock } = gate(confirm);
109
+ const first = g.check("meld", "a");
110
+ await tick();
111
+ asked[0].answer({ ok: false, method: "macos-local-auth", reason: "canceled" });
112
+ const refused = await first;
113
+ assert.equal(refused.ok === false && refused.status, 403);
114
+ assert.equal(refused.ok === false && refused.reason, "canceled");
115
+ clock.t += 5_000;
116
+ const cooling = await g.check("meld", "a");
117
+ assert.equal(cooling.ok === false && cooling.status, 429);
118
+ assert.equal(cooling.ok === false && cooling.retryAfterMs, 10_000);
119
+ assert.equal(asked.length, 1);
120
+ clock.t += 10_000;
121
+ const again = g.check("meld", "a");
122
+ await tick();
123
+ assert.equal(asked.length, 2);
124
+ asked[1].answer({ ok: true, method: "macos-local-auth" });
125
+ assert.deepEqual(await again, { ok: true });
126
+ });
127
+ it("cools down after a timeout too", async () => {
128
+ const { asked, confirm } = manualConfirm();
129
+ const { g } = gate(confirm);
130
+ const first = g.check("rotate", "a");
131
+ await tick();
132
+ asked[0].answer({ ok: false, method: "macos-local-auth", reason: "timeout" });
133
+ assert.equal((await first).ok, false);
134
+ const next = await g.check("rotate", "a");
135
+ assert.equal(next.ok === false && next.reason, "cooldown");
136
+ });
137
+ it("does not cool down when nothing could be shown", async () => {
138
+ const { asked, confirm } = manualConfirm();
139
+ const { g } = gate(confirm);
140
+ const first = g.check("meld", "a");
141
+ await tick();
142
+ asked[0].answer({ ok: false, method: null, reason: "unavailable", detail: "linux" });
143
+ const refused = await first;
144
+ assert.equal(refused.ok === false && refused.reason, "unavailable");
145
+ assert.match(refused.ok === false ? refused.error : "", /cannot show a presence prompt \(linux\)/);
146
+ const next = g.check("meld", "a");
147
+ await tick();
148
+ assert.equal(asked.length, 2, "asked again at once");
149
+ asked[1].answer({ ok: true, method: "macos-local-auth" });
150
+ await next;
151
+ });
152
+ it("closes the prompt when the asker withdraws, and cools down: giving up and asking again is the loop", async () => {
153
+ const { asked, confirm } = manualConfirm();
154
+ const { g } = gate(confirm);
155
+ const request = new AbortController();
156
+ const first = g.check("meld", "a", request.signal);
157
+ await tick();
158
+ request.abort();
159
+ const refused = await first;
160
+ assert.equal(refused.ok === false && refused.reason, "aborted");
161
+ assert.equal(asked[0].signal.aborted, true, "the helper's signal fired");
162
+ assert.equal(g.current(), null);
163
+ const next = await g.check("meld", "a");
164
+ assert.equal(next.ok === false && next.reason, "cooldown");
165
+ assert.equal(asked.length, 1);
166
+ });
167
+ it("never prompts for a request already withdrawn", async () => {
168
+ const { asked, confirm } = manualConfirm();
169
+ const { g } = gate(confirm);
170
+ const request = new AbortController();
171
+ request.abort();
172
+ const refused = await g.check("meld", "a", request.signal);
173
+ assert.equal(refused.ok === false && refused.reason, "aborted");
174
+ assert.equal(asked.length, 0);
175
+ });
176
+ it("lets any client cancel the prompt by its id, and only that prompt", async () => {
177
+ const { asked, confirm } = manualConfirm();
178
+ const { g } = gate(confirm);
179
+ const first = g.check("meld", "a");
180
+ await tick();
181
+ assert.equal(g.cancel("presence-0"), false, "a stale id closes nothing");
182
+ assert.equal(asked[0].signal.aborted, false);
183
+ assert.equal(g.cancel("presence-1"), true);
184
+ const refused = await first;
185
+ assert.equal(refused.ok === false && refused.reason, "remote-cancel");
186
+ assert.equal(g.cancel(), false, "nothing left to cancel");
187
+ // A canceled prompt counts as declined.
188
+ assert.equal((await g.check("meld", "a")).ok === false, true);
189
+ });
190
+ it("logs an attempt refused without a prompt, and leaves the gate free", async () => {
191
+ const { asked, confirm } = manualConfirm();
192
+ const { g, lines, changes } = gate(confirm);
193
+ g.refuse("meld", "agent claude-aaaaaaaa", "an agent's token");
194
+ assert.deepEqual(lines, ["[presence] meld for agent claude-aaaaaaaa: refused: an agent's token"]);
195
+ assert.deepEqual(changes, [], "no prompt opened");
196
+ // No cooldown: nothing was shown, so the shell is asked at once.
197
+ const next = g.check("meld", "a");
198
+ await tick();
199
+ assert.equal(asked.length, 1);
200
+ asked[0].answer({ ok: true, method: "macos-local-auth" });
201
+ await next;
202
+ });
203
+ it("calls a confirm that throws a failure, and still closes the prompt", async () => {
204
+ const { g, changes } = gate(() => Promise.reject(new Error("boom")));
205
+ const refused = await g.check("meld", "a");
206
+ assert.equal(refused.ok === false && refused.reason, "failed");
207
+ assert.equal(g.current(), null);
208
+ assert.equal(changes.at(-1), null);
209
+ });
210
+ });
211
+ describe("the routes check before they ask", () => {
212
+ it("parses a mint body without minting", () => {
213
+ assert.deepEqual(parseMintBody('{"label":"ipad\\nforged"}', T0), { label: "ipad forged" });
214
+ assert.deepEqual(parseMintBody("", T0), { label: "melded 2026-10-05" });
215
+ assert.deepEqual(parseMintBody("[1]", T0), { error: "Body must be a JSON object" });
216
+ assert.deepEqual(parseMintBody("{", T0), { error: "Body must be a JSON object" });
217
+ });
218
+ it("plans a revoke without doing it, refusing what the revoke would", () => {
219
+ const db = createTestDb();
220
+ const store = new DbTokenStore(db);
221
+ const ring = new TokenRing(store, () => "unused");
222
+ const local = store.insert(hashToken("local"), "local", "this machine", T0);
223
+ const phone = store.insert(hashToken("phone"), "device", "phone", T0 + 1);
224
+ assert.equal(planRevoke(store, local, "no-such-id").status, 404);
225
+ assert.equal(planRevoke(store, phone, local.id).status, 400);
226
+ assert.equal(planRevoke(store, phone, phone.id).status, 409);
227
+ const plan = planRevoke(store, local, phone.id);
228
+ assert.ok(!("status" in plan));
229
+ assert.deepEqual(plan.ids, [phone.id]);
230
+ assert.ok(store.get(phone.id), "planning revoked nothing");
231
+ assert.equal(revokeTokenRoute(store, ring, local, phone.id, T0 + 2).status, 200);
232
+ // The revoke re-checks: what was planned is gone now.
233
+ assert.equal(revokeTokenRoute(store, ring, local, phone.id, T0 + 3).status, 404);
234
+ });
235
+ });
236
+ describe("the CLI's waiting line", () => {
237
+ it("is not printed in an agent's session, which the daemon refuses unasked", () => {
238
+ const lines = [];
239
+ const orig = console.error;
240
+ console.error = (...a) => void lines.push(a.join(" "));
241
+ try {
242
+ announcePresenceWait({ BRANE_NARRATIVE_SECRET: "s" });
243
+ assert.deepEqual(lines, []);
244
+ announcePresenceWait({});
245
+ assert.equal(lines.length, 1);
246
+ }
247
+ finally {
248
+ console.error = orig;
249
+ }
250
+ });
251
+ it("names the prompt this machine shows", () => {
252
+ assert.match(presenceWaitNote("darwin"), /Touch ID/);
253
+ assert.match(presenceWaitNote("win32"), /Windows Hello/);
254
+ assert.match(presenceWaitNote("linux"), /authentication prompt/);
255
+ });
256
+ });
257
+ describe("the gated routes", () => {
258
+ const T = () => T0;
259
+ function setup() {
260
+ const db = createTestDb();
261
+ const store = new DbTokenStore(db);
262
+ const ring = new TokenRing(store, () => "minted-token");
263
+ const local = store.insert(hashToken("local"), "local", "this machine", T0);
264
+ const phone = store.insert(hashToken("phone"), "device", "phone", T0 + 1);
265
+ const agent = store.insert(hashToken("agent"), "agent", "claude-aaaaaaaa", T0 + 2);
266
+ const as = (record) => ({ record, by: record.label });
267
+ return { store, ring, local, phone, agent, as };
268
+ }
269
+ /** A gate that records what it was asked and answers `ok`, after `during`. */
270
+ function fakeGate(ok, during = () => { }) {
271
+ const asked = [];
272
+ const refused = [];
273
+ return {
274
+ asked,
275
+ refused,
276
+ refuse: (act, by, why) => void refused.push(`${act} for ${by}: ${why}`),
277
+ check: async (act) => {
278
+ asked.push(act);
279
+ during();
280
+ return ok
281
+ ? { ok: true }
282
+ : { ok: false, status: 403, reason: "canceled", error: "declined" };
283
+ },
284
+ cancel: (id) => id === "presence-1",
285
+ };
286
+ }
287
+ it("never asks a device", async () => {
288
+ const { ring, store, phone, as } = setup();
289
+ const gate = fakeGate(false);
290
+ assert.equal((await gatedMintRoute({ gate, ring }, as(phone), "{}", undefined, T)).status, 200);
291
+ const laptop = store.insert(hashToken("laptop"), "device", "laptop", T0 + 3);
292
+ assert.equal((await gatedRevokeRoute({ gate, ring, store }, as(phone), laptop.id, [], undefined, T)).status, 200);
293
+ assert.equal((await gatedRotateRoute({ gate, ring }, as(phone))).status, 200);
294
+ assert.deepEqual(gate.asked, []);
295
+ });
296
+ it("asks this machine before minting", async () => {
297
+ const { ring, local, as } = setup();
298
+ const gate = fakeGate(false);
299
+ const refused = await gatedMintRoute({ gate, ring }, as(local), "{}", undefined, T);
300
+ assert.deepEqual(refused, { status: 403, body: { error: "declined", presence: "canceled" } });
301
+ assert.deepEqual(gate.asked, ["meld"]);
302
+ assert.equal((await gatedMintRoute({ gate: fakeGate(true), ring }, as(local), "{}", undefined, T)).status, 200);
303
+ });
304
+ it("refuses an agent every gated act without raising a prompt, even one that would be approved", async () => {
305
+ const { ring, store, phone, agent, as } = setup();
306
+ const gate = fakeGate(true);
307
+ const mint = await gatedMintRoute({ gate, ring }, as(agent), "{}", undefined, T);
308
+ assert.equal(mint.status, 403);
309
+ assert.deepEqual(mint.body.presence, "agent");
310
+ assert.match(mint.body.error, /An agent cannot add a device/);
311
+ const revoke = await gatedRevokeRoute({ gate, ring, store }, as(agent), phone.id, [], undefined, T);
312
+ assert.equal(revoke.status, 403);
313
+ assert.ok(store.get(phone.id), "nothing was revoked");
314
+ assert.equal((await gatedRotateRoute({ gate, ring }, as(agent))).status, 403);
315
+ assert.deepEqual(gate.asked, [], "no prompt was ever raised");
316
+ // But each attempt is recorded, as one that was asked would be.
317
+ assert.deepEqual(gate.refused, [
318
+ "meld for claude-aaaaaaaa: an agent's token",
319
+ "revoke-device for claude-aaaaaaaa: an agent's token",
320
+ "rotate for claude-aaaaaaaa: an agent's token",
321
+ ]);
322
+ // Stopping an agent is still an agent's to do.
323
+ const other = store.insert(hashToken("other-agent"), "agent", "claude-bbbbbbbb", T0 + 4);
324
+ assert.equal((await gatedRevokeRoute({ gate, ring, store }, as(agent), other.id, [], undefined, T)).status, 200);
325
+ });
326
+ it("asks this machine before minting for a remote daemon, and stores nothing", async () => {
327
+ const { store, local, phone, agent, as } = setup();
328
+ const before = store.list(T0).length;
329
+ const mint = () => ({ token: "remote-token", hash: hashToken("remote-token") });
330
+ const declined = fakeGate(false);
331
+ assert.deepEqual(await gatedRemoteMintRoute({ gate: declined, mint }, as(local)), {
332
+ status: 403,
333
+ body: { error: "declined", presence: "canceled" },
334
+ });
335
+ assert.deepEqual(declined.asked, ["meld-remote"]);
336
+ const approved = fakeGate(true);
337
+ assert.deepEqual(await gatedRemoteMintRoute({ gate: approved, mint }, as(local)), {
338
+ status: 200,
339
+ body: { token: "remote-token", hash: hashToken("remote-token") },
340
+ });
341
+ // A device is never asked; an agent is refused unasked.
342
+ const gate = fakeGate(false);
343
+ assert.equal((await gatedRemoteMintRoute({ gate, mint }, as(phone))).status, 200);
344
+ const refused = await gatedRemoteMintRoute({ gate, mint }, as(agent));
345
+ assert.equal(refused.status, 403);
346
+ assert.match(refused.body.error, /An agent cannot add a device to a remote daemon/);
347
+ assert.deepEqual(gate.asked, []);
348
+ assert.equal(store.list(T0).length, before, "no row was written");
349
+ });
350
+ it("refuses a bad mint body without asking", async () => {
351
+ const { ring, local, as } = setup();
352
+ const gate = fakeGate(true);
353
+ assert.equal((await gatedMintRoute({ gate, ring }, as(local), "[1]", undefined, T)).status, 400);
354
+ assert.deepEqual(gate.asked, []);
355
+ });
356
+ it("asks before signing out a device, never before stopping an agent", async () => {
357
+ const { ring, store, local, phone, agent, as } = setup();
358
+ const gate = fakeGate(false);
359
+ assert.equal((await gatedRevokeRoute({ gate, ring, store }, as(local), agent.id, [], undefined, T)).status, 200);
360
+ assert.deepEqual(gate.asked, []);
361
+ assert.equal((await gatedRevokeRoute({ gate, ring, store }, as(local), phone.id, [], undefined, T)).status, 403);
362
+ assert.deepEqual(gate.asked, ["revoke-device"]);
363
+ assert.ok(store.get(phone.id), "a declined revoke revoked nothing");
364
+ });
365
+ it("refuses what the revoke would refuse, without asking", async () => {
366
+ const { ring, store, local, as } = setup();
367
+ const gate = fakeGate(true);
368
+ assert.equal((await gatedRevokeRoute({ gate, ring, store }, as(local), "no-such-id", [], undefined, T)).status, 404);
369
+ assert.equal((await gatedRevokeRoute({ gate, ring, store }, as(local), local.id, [], undefined, T)).status, 400);
370
+ assert.deepEqual(gate.asked, []);
371
+ });
372
+ it("checks the revoke again after the prompt", async () => {
373
+ const { ring, store, local, phone, as } = setup();
374
+ // The row goes while the prompt is up — revoked from the app, say.
375
+ const gate = fakeGate(true, () => store.revoke([phone.id], T0 + 5));
376
+ const answer = await gatedRevokeRoute({ gate, ring, store }, as(local), phone.id, [], undefined, T);
377
+ assert.deepEqual(gate.asked, ["revoke-device"]);
378
+ assert.equal(answer.status, 404);
379
+ });
380
+ it("never begins a rotation, or signs anyone out, when refused: a pending rotation survives", async () => {
381
+ const { local, phone, agent, as } = setup();
382
+ let begun = 0;
383
+ let signedOut = 0;
384
+ const ring = {
385
+ beginRotation: () => {
386
+ begun++;
387
+ return { token: "new", expiresAt: T0 + 60_000, replaced: begun > 1 };
388
+ },
389
+ signOutDevices: () => {
390
+ signedOut++;
391
+ return { ids: ["d1", "d2", "x1"], devices: 2, replaced: false };
392
+ },
393
+ };
394
+ const declined = fakeGate(false);
395
+ assert.equal((await gatedRotateRoute({ gate: declined, ring }, as(local))).status, 403);
396
+ assert.deepEqual([begun, signedOut], [0, 0], "a declined rotate reached neither");
397
+ const forAgent = fakeGate(true);
398
+ assert.equal((await gatedRotateRoute({ gate: forAgent, ring }, as(agent))).status, 403);
399
+ assert.deepEqual(forAgent.asked, [], "an agent is refused unasked");
400
+ assert.deepEqual([begun, signedOut], [0, 0]);
401
+ // A device begins the two-step rotation, unasked.
402
+ const forDevice = fakeGate(true);
403
+ const begunAnswer = await gatedRotateRoute({ gate: forDevice, ring }, as(phone));
404
+ assert.deepEqual(begunAnswer.body, { token: "new", expires_at: T0 + 60_000, replaced: false });
405
+ assert.deepEqual(forDevice.asked, []);
406
+ assert.deepEqual([begun, signedOut], [1, 0]);
407
+ });
408
+ it("signs every device out in the shell's one request, once approved, and hands back no token", async () => {
409
+ const { local, as } = setup();
410
+ const ring = {
411
+ beginRotation: () => assert.fail("this account holds no token to rotate"),
412
+ signOutDevices: () => ({ ids: ["d1", "d2", "x1"], devices: 2, replaced: true }),
413
+ };
414
+ const gate = fakeGate(true);
415
+ const answer = await gatedRotateRoute({ gate, ring }, as(local));
416
+ assert.deepEqual(gate.asked, ["rotate"]);
417
+ assert.equal(answer.status, 200);
418
+ assert.deepEqual(answer.body, { signed_out: 2, replaced: true });
419
+ assert.deepEqual(answer.signedOut?.ids, ["d1", "d2", "x1"], "what the server closes the sockets of");
420
+ });
421
+ it("lets only a device cancel a prompt", () => {
422
+ const { local, phone, agent } = setup();
423
+ const gate = fakeGate(true);
424
+ assert.equal(cancelPresenceRoute(gate, local, "presence-1").status, 403);
425
+ assert.equal(cancelPresenceRoute(gate, agent, "presence-1").status, 403);
426
+ assert.deepEqual(cancelPresenceRoute(gate, phone, "presence-1"), { status: 200, body: { canceled: true } });
427
+ assert.deepEqual(cancelPresenceRoute(gate, phone, "presence-0"), { status: 200, body: { canceled: false } });
428
+ });
429
+ it("passes the cooldown on as Retry-After", async () => {
430
+ const { ring, local, as } = setup();
431
+ const gate = {
432
+ check: async () => ({ ok: false, status: 429, reason: "cooldown", retryAfterMs: 9_500, error: "wait" }),
433
+ refuse: () => { },
434
+ };
435
+ const answer = await gatedMintRoute({ gate, ring }, as(local), "{}", undefined, T);
436
+ assert.equal(answer.status, 429);
437
+ assert.equal(answer.retryAfterMs, 9_500);
438
+ });
439
+ });
440
+ //# sourceMappingURL=presence-gate.test.js.map
@@ -0,0 +1,160 @@
1
+ import { describe, it } from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import { detectPresencePlatform, linuxPlan, macPlan, presencePlan, windowsPlan, windowsScript, confirmPresence, POLKIT_ACTION, WINDOWS_POWERSHELL, WSL_POWERSHELL, runPlan, } from "../presence.js";
4
+ // Every plan is checked as data: none of these tests runs a prompt, so the
5
+ // suite passes on any platform and never puts a dialog on anyone's screen.
6
+ describe("detectPresencePlatform", () => {
7
+ const noProc = () => {
8
+ throw new Error("ENOENT");
9
+ };
10
+ it("maps darwin and win32 directly", () => {
11
+ assert.equal(detectPresencePlatform("darwin", noProc, {}), "macos");
12
+ assert.equal(detectPresencePlatform("win32", noProc, {}), "windows");
13
+ });
14
+ it("recognises WSL by its env var or by /proc/version", () => {
15
+ assert.equal(detectPresencePlatform("linux", noProc, { WSL_DISTRO_NAME: "Ubuntu" }), "wsl");
16
+ assert.equal(detectPresencePlatform("linux", () => "Linux version 5.15.153.1-microsoft-standard-WSL2", {}), "wsl");
17
+ });
18
+ it("is plain linux otherwise, and unsupported elsewhere", () => {
19
+ assert.equal(detectPresencePlatform("linux", () => "Linux version 6.8.0-generic", {}), "linux");
20
+ assert.equal(detectPresencePlatform("linux", noProc, {}), "linux");
21
+ assert.equal(detectPresencePlatform("freebsd", noProc, {}), "unsupported");
22
+ });
23
+ });
24
+ describe("macPlan", () => {
25
+ it("passes the reason as an argument, never inside the script", () => {
26
+ const reason = `enroll "x"'); doShellScript('rm -rf ~`;
27
+ const plan = macPlan(reason);
28
+ assert.equal(plan.command, "/usr/bin/osascript");
29
+ assert.deepEqual(plan.args.slice(0, 3), ["-l", "JavaScript", "-e"]);
30
+ assert.equal(plan.args[4], reason);
31
+ assert.ok(!plan.args[3].includes(reason));
32
+ });
33
+ it("asks for owner authentication (policy 2), which falls back to the password", () => {
34
+ assert.match(macPlan("r").args[3], /evaluatePolicyLocalizedReasonReply\(2,/);
35
+ assert.match(macPlan("r").args[3], /canEvaluatePolicyError\(2,/);
36
+ });
37
+ it("reads approve, cancel, unavailable and garbage", () => {
38
+ const { interpret } = macPlan("r");
39
+ assert.deepEqual(interpret('{"ok":true}\n', "", 0), { ok: true, method: "macos-local-auth" });
40
+ // Recorded from a real prompt, cancelled by the operator.
41
+ assert.deepEqual(interpret('{"ok":false,"code":-2,"error":"Authentication canceled."}', "", 0), {
42
+ ok: false,
43
+ method: "macos-local-auth",
44
+ reason: "canceled",
45
+ detail: "Authentication canceled.",
46
+ });
47
+ assert.equal(interpret('{"ok":false,"unavailable":true,"error":"No identities"}', "", 0).ok, false);
48
+ assert.equal(interpret('{"ok":false,"unavailable":true}', "", 0).reason, "unavailable");
49
+ assert.equal(interpret('{"ok":false,"code":-1}', "", 0).reason, "failed");
50
+ // systemCancel: the system took the dialog away; the operator said nothing.
51
+ assert.equal(interpret('{"ok":false,"code":-4}', "", 0).reason, "failed");
52
+ assert.deepEqual(interpret("", "execution error: boom", 1), {
53
+ ok: false,
54
+ method: "macos-local-auth",
55
+ reason: "failed",
56
+ detail: "execution error: boom",
57
+ });
58
+ });
59
+ });
60
+ describe("windowsPlan", () => {
61
+ it("encodes the script as UTF-16LE base64 for -EncodedCommand", () => {
62
+ const plan = windowsPlan("approve device");
63
+ assert.equal(plan.command, WINDOWS_POWERSHELL);
64
+ const i = plan.args.indexOf("-EncodedCommand");
65
+ assert.ok(i >= 0);
66
+ const decoded = Buffer.from(plan.args[i + 1], "base64").toString("utf16le");
67
+ assert.equal(decoded, windowsScript("approve device"));
68
+ assert.match(decoded, /RequestVerificationAsync\('approve device'\)/);
69
+ });
70
+ it("runs powershell.exe by a fixed absolute path, never one from the environment", () => {
71
+ assert.equal(windowsPlan("r", "windows").command, "C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe");
72
+ assert.equal(windowsPlan("r", "wsl").command, WSL_POWERSHELL);
73
+ assert.equal(presencePlan("r", "wsl")?.command, WSL_POWERSHELL);
74
+ });
75
+ it("quotes the reason as a PowerShell literal", () => {
76
+ assert.match(windowsScript("it's $(evil)"), /RequestVerificationAsync\('it''s \$\(evil\)'\)/);
77
+ });
78
+ it("reads Windows Hello's results", () => {
79
+ const { interpret } = windowsPlan("r");
80
+ assert.deepEqual(interpret("result:Verified\r\n", "", 0), { ok: true, method: "windows-hello" });
81
+ assert.equal(interpret("result:Canceled", "", 0).reason, "canceled");
82
+ assert.equal(interpret("result:RetriesExhausted", "", 0).reason, "failed");
83
+ assert.equal(interpret("unavailable:NotConfiguredForUser", "", 0).reason, "unavailable");
84
+ assert.equal(interpret("result:DeviceBusy", "", 0).reason, "failed");
85
+ assert.equal(interpret("", "Add-Type : boom", 1).reason, "failed");
86
+ });
87
+ });
88
+ describe("linuxPlan", () => {
89
+ it("checks brane's own polkit action for this process, interactively", () => {
90
+ const plan = linuxPlan(4242);
91
+ assert.equal(plan.command, "/usr/bin/pkcheck");
92
+ assert.deepEqual(plan.args, ["--action-id", POLKIT_ACTION, "--process", "4242", "--allow-user-interaction"]);
93
+ });
94
+ it("reads pkcheck's exit codes", () => {
95
+ const { interpret } = linuxPlan(1);
96
+ const reason = (r) => (r.ok ? "ok" : r.reason);
97
+ assert.equal(reason(interpret("", "", 0)), "ok");
98
+ assert.equal(reason(interpret("", "", 3)), "canceled");
99
+ assert.equal(reason(interpret("", "Authorization requires authentication but no agent is available", 2)), "unavailable");
100
+ // An unregistered action is matched by its message, whatever the code.
101
+ assert.equal(reason(interpret("", `Action ${POLKIT_ACTION} is not registered`, 127)), "unavailable");
102
+ assert.equal(reason(interpret("", `No action with action id ${POLKIT_ACTION}`, 1)), "unavailable");
103
+ // Exit 1 includes "polkit never asked" (a daemon outside the desktop session).
104
+ assert.equal(reason(interpret("", "Not authorized.", 1)), "failed");
105
+ assert.equal(reason(interpret("", "", 127)), "failed");
106
+ });
107
+ });
108
+ describe("presencePlan / confirmPresence", () => {
109
+ it("routes WSL to Windows Hello", () => {
110
+ assert.equal(presencePlan("r", "wsl")?.method, "windows-hello");
111
+ assert.equal(presencePlan("r", "windows")?.method, "windows-hello");
112
+ assert.equal(presencePlan("r", "macos")?.method, "macos-local-auth");
113
+ assert.equal(presencePlan("r", "linux")?.method, "polkit");
114
+ assert.equal(presencePlan("r", "unsupported"), null);
115
+ });
116
+ it("is unavailable, not a throw, where there is no backend", async () => {
117
+ const r = await confirmPresence("r", { platform: "unsupported" });
118
+ assert.equal(r.ok, false);
119
+ assert.equal(r.reason, "unavailable");
120
+ });
121
+ });
122
+ describe("runPlan", () => {
123
+ // Real child processes, none of which shows a prompt: node itself stands
124
+ // in for the platform tool, and polkit's reading of exit codes is reused.
125
+ const node = (script) => ({ ...linuxPlan(1), command: process.execPath, args: ["-e", script] });
126
+ it("hands the exit code to the plan", async () => {
127
+ assert.deepEqual(await runPlan(node("process.exit(0)"), 10_000), { ok: true, method: "polkit" });
128
+ assert.equal((await runPlan(node("process.exit(3)"), 10_000)).reason, "canceled");
129
+ const r = await runPlan(node("console.error('Not authorized.'); process.exit(1)"), 10_000);
130
+ assert.deepEqual(r, { ok: false, method: "polkit", reason: "failed", detail: "Not authorized." });
131
+ });
132
+ it("is unavailable when the tool is missing", async () => {
133
+ const r = await runPlan({ ...linuxPlan(1), command: "/nonexistent/brane-presence-tool" }, 10_000);
134
+ assert.equal(r.ok, false);
135
+ assert.equal(r.reason, "unavailable");
136
+ });
137
+ it("times out, killing the tool", async () => {
138
+ const r = await runPlan(node("setTimeout(() => {}, 10_000)"), 100);
139
+ assert.equal(r.reason, "timeout");
140
+ });
141
+ it("is aborted, killing the tool, when the caller withdraws", async () => {
142
+ const ac = new AbortController();
143
+ const started = Date.now();
144
+ setTimeout(() => ac.abort(), 50);
145
+ const r = await runPlan(node("setTimeout(() => {}, 10_000)"), 10_000, ac.signal);
146
+ assert.equal(r.reason, "aborted");
147
+ assert.ok(Date.now() - started < 5_000, "the tool was killed, not waited out");
148
+ });
149
+ it("never starts the tool for a signal already aborted", async () => {
150
+ const ac = new AbortController();
151
+ ac.abort();
152
+ const r = await runPlan({ ...linuxPlan(1), command: "/nonexistent/brane-presence-tool" }, 10_000, ac.signal);
153
+ assert.equal(r.reason, "aborted");
154
+ });
155
+ it("calls oversized output failed, not a timeout", async () => {
156
+ const r = await runPlan(node("process.stdout.write('x'.repeat(2 * 1024 * 1024))"), 10_000);
157
+ assert.equal(r.reason, "failed");
158
+ });
159
+ });
160
+ //# sourceMappingURL=presence.test.js.map