@wenathlan/devthink 2.0.13 → 2.0.15

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 (444) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/accounts.ts +2087 -0
  3. package/agent.ts +2331 -0
  4. package/antigravity.ts +2139 -0
  5. package/apifreeze.ts +4146 -0
  6. package/auth.ts +642 -0
  7. package/background.ts +16837 -0
  8. package/bridge.ts +2080 -0
  9. package/build.ts +54 -0
  10. package/bun.ts +26 -0
  11. package/capture.ts +1614 -0
  12. package/checksecrets.ts +72 -0
  13. package/cjs.ts +9 -0
  14. package/cli.ts +3500 -0
  15. package/commands.ts +1101 -0
  16. package/companion.ts +450 -0
  17. package/compatibility.ts +37 -0
  18. package/config.ts +3137 -0
  19. package/constants.ts +943 -0
  20. package/core.ts +1473 -0
  21. package/crossbrowser.ts +1359 -0
  22. package/dashdone.ts +605 -0
  23. package/data.ts +943 -0
  24. package/database.ts +133 -0
  25. package/debug.ts +850 -0
  26. package/deno.ts +27 -0
  27. package/devthink.ts +707 -0
  28. package/dist/accounts.d.ts +409 -0
  29. package/dist/agent.d.ts +837 -0
  30. package/dist/antigravity.d.ts +380 -0
  31. package/dist/apifreeze.d.ts +113 -0
  32. package/dist/auth.d.ts +224 -0
  33. package/{bridge.d.ts → dist/bridge.d.ts} +1 -1
  34. package/dist/capture.d.ts +595 -0
  35. package/dist/cjs.d.ts +6 -0
  36. package/{cli.js → dist/cli.js} +2 -2
  37. package/dist/commands.d.ts +310 -0
  38. package/dist/compatibility.d.ts +17 -0
  39. package/dist/config.d.ts +1853 -0
  40. package/dist/constants.d.ts +637 -0
  41. package/dist/core.d.ts +361 -0
  42. package/dist/data.d.ts +330 -0
  43. package/dist/database.d.ts +24 -0
  44. package/dist/debug.d.ts +270 -0
  45. package/dist/engine.d.ts +74 -0
  46. package/dist/environments.d.ts +244 -0
  47. package/dist/evidence.d.ts +277 -0
  48. package/dist/export.d.ts +311 -0
  49. package/dist/fingerprint.d.ts +594 -0
  50. package/dist/flow.d.ts +300 -0
  51. package/dist/gates.d.ts +103 -0
  52. package/dist/identity.d.ts +51 -0
  53. package/dist/ids.d.ts +7 -0
  54. package/dist/llm.d.ts +652 -0
  55. package/dist/models.d.ts +1807 -0
  56. package/dist/net.d.ts +325 -0
  57. package/dist/neutral.d.ts +41 -0
  58. package/dist/oauth.d.ts +703 -0
  59. package/dist/page.d.ts +1166 -0
  60. package/dist/perf.d.ts +481 -0
  61. package/dist/plan.d.ts +134 -0
  62. package/dist/project.d.ts +395 -0
  63. package/dist/providers.d.ts +37 -0
  64. package/dist/quota.d.ts +453 -0
  65. package/dist/recovery.d.ts +621 -0
  66. package/dist/request.d.ts +1492 -0
  67. package/dist/run.d.ts +779 -0
  68. package/dist/runtime.d.ts +212 -0
  69. package/dist/search.d.ts +375 -0
  70. package/dist/security.d.ts +534 -0
  71. package/dist/serve.d.ts +136 -0
  72. package/dist/session.d.ts +296 -0
  73. package/dist/storage.d.ts +53 -0
  74. package/dist/streaming.d.ts +1119 -0
  75. package/dist/swarm.d.ts +328 -0
  76. package/dist/tools.d.ts +170 -0
  77. package/dist/types.d.ts +8357 -0
  78. package/dist/utils.d.ts +82 -0
  79. package/dist/version.d.ts +7 -0
  80. package/dist/versionregistry.d.ts +406 -0
  81. package/dist/views.d.ts +579 -0
  82. package/dist/workbenchsession.d.ts +69 -0
  83. package/dist/workflow.d.ts +649 -0
  84. package/docs/readme.md +92 -0
  85. package/docs/schemas/antigravity.json +192 -0
  86. package/engine.ts +1524 -0
  87. package/environments.ts +1005 -0
  88. package/evidence.ts +643 -0
  89. package/export.ts +889 -0
  90. package/fingerprint.ts +1458 -0
  91. package/flow.ts +1190 -0
  92. package/gates.ts +307 -0
  93. package/gateway.ts +34 -0
  94. package/hardening.ts +191 -0
  95. package/headless.ts +369 -0
  96. package/http.ts +2110 -0
  97. package/identity.ts +246 -0
  98. package/ids.ts +26 -0
  99. package/index.ts +369 -0
  100. package/llm.ts +2682 -0
  101. package/mcp.ts +1835 -0
  102. package/{memory.js → memory.ts} +3834 -2310
  103. package/models.ts +3805 -0
  104. package/modes.ts +43 -0
  105. package/net.ts +1214 -0
  106. package/neutral.ts +362 -0
  107. package/node.ts +26 -0
  108. package/oauth.ts +2216 -0
  109. package/offscreen.ts +170 -0
  110. package/pack.ts +956 -0
  111. package/package.json +175 -82
  112. package/page.ts +7824 -0
  113. package/pagebridge.ts +1116 -0
  114. package/perf.ts +957 -0
  115. package/plan.ts +672 -0
  116. package/plugin.ts +76 -0
  117. package/{policy.js → policy.ts} +5073 -4679
  118. package/prisma.config.ts +27 -0
  119. package/progress.ts +1308 -0
  120. package/project.ts +1291 -0
  121. package/protocol.ts +4187 -0
  122. package/providers.ts +246 -0
  123. package/quota.ts +1887 -0
  124. package/recovery.ts +2433 -0
  125. package/request.ts +4482 -0
  126. package/run.ts +2135 -0
  127. package/runtime.ts +526 -0
  128. package/search.ts +1760 -0
  129. package/security.ts +1673 -0
  130. package/serve.ts +419 -0
  131. package/server.ts +5538 -0
  132. package/session.ts +1162 -0
  133. package/storage.ts +182 -0
  134. package/streaming.ts +4101 -0
  135. package/swarm.ts +884 -0
  136. package/sync.ts +56 -0
  137. package/tests/agentcert.test.ts +128 -0
  138. package/tests/agentfleet.test.ts +214 -0
  139. package/tests/agentreview.test.ts +241 -0
  140. package/tests/agentstream.test.ts +335 -0
  141. package/tests/agentwork.test.ts +356 -0
  142. package/tests/apifreeze.test.ts +290 -0
  143. package/tests/apimap.test.ts +112 -0
  144. package/tests/approvalgate.test.ts +175 -0
  145. package/tests/artifactmanifest.test.ts +204 -0
  146. package/tests/attentionfeed.test.ts +163 -0
  147. package/tests/backgroundruns.test.ts +100 -0
  148. package/tests/batchscheduling.test.ts +131 -0
  149. package/tests/blackboard.test.ts +238 -0
  150. package/tests/bridge.test.ts +1088 -0
  151. package/tests/browsercoverage.test.ts +134 -0
  152. package/tests/browserpolyfills.test.ts +124 -0
  153. package/tests/build.mjs +1260 -0
  154. package/tests/clientauth.test.ts +223 -0
  155. package/tests/clitools.test.ts +578 -0
  156. package/tests/confirmgates.test.ts +190 -0
  157. package/tests/containerpack.test.ts +143 -0
  158. package/tests/controlflow.test.ts +2120 -0
  159. package/tests/coordination.test.ts +345 -0
  160. package/tests/copyscreen.test.ts +139 -0
  161. package/tests/core.test.ts +417 -0
  162. package/tests/costcert.test.ts +95 -0
  163. package/tests/crossbrowser.test.ts +115 -0
  164. package/tests/cspaudit.test.ts +79 -0
  165. package/tests/dashdone.test.ts +615 -0
  166. package/tests/datagrid.test.ts +137 -0
  167. package/tests/doccheck.test.ts +107 -0
  168. package/tests/dombatch.test.ts +109 -0
  169. package/tests/emulation.test.ts +282 -0
  170. package/tests/environments.test.ts +259 -0
  171. package/tests/evidenceviews.test.ts +88 -0
  172. package/tests/exporttools.test.ts +185 -0
  173. package/tests/firefoxprep.test.ts +134 -0
  174. package/tests/flowlibrary.test.ts +242 -0
  175. package/tests/flowrun.test.ts +303 -0
  176. package/tests/forensics.test.ts +439 -0
  177. package/tests/gateway.test.ts +939 -0
  178. package/tests/headlesslib.test.ts +171 -0
  179. package/tests/httpstream.test.ts +277 -0
  180. package/tests/immutablelog.test.ts +157 -0
  181. package/tests/inboundguard.test.ts +177 -0
  182. package/tests/lazyload.test.ts +72 -0
  183. package/tests/librarymodes.test.ts +623 -0
  184. package/tests/llm.test.ts +696 -0
  185. package/tests/manifest.test.ts +226 -0
  186. package/tests/maskinputs.test.ts +150 -0
  187. package/tests/matrixverify.test.ts +111 -0
  188. package/tests/mavenpack.test.ts +75 -0
  189. package/tests/mcpserver.test.ts +704 -0
  190. package/tests/memory.test.ts +4155 -0
  191. package/tests/memorycare.test.ts +230 -0
  192. package/tests/migrateplan.test.ts +349 -0
  193. package/tests/minimization.test.ts +414 -0
  194. package/tests/modelroute.test.ts +112 -0
  195. package/tests/multiagent.test.ts +440 -0
  196. package/tests/nativebridge.test.ts +807 -0
  197. package/tests/navigation.test.ts +284 -0
  198. package/tests/nugetpack.test.ts +83 -0
  199. package/tests/orchestration.test.ts +449 -0
  200. package/tests/originpolicy.test.ts +405 -0
  201. package/tests/outputcompare.test.ts +97 -0
  202. package/tests/package.test.ts +91 -0
  203. package/tests/pagebridge.test.ts +5909 -0
  204. package/tests/pentest.test.ts +75 -0
  205. package/tests/perfrecords.test.ts +257 -0
  206. package/tests/permdiff.test.ts +85 -0
  207. package/tests/phishguard.test.ts +128 -0
  208. package/tests/pickerviews.test.ts +155 -0
  209. package/tests/pipelines.test.ts +381 -0
  210. package/tests/planlint.test.ts +240 -0
  211. package/tests/planreview.test.ts +383 -0
  212. package/tests/platformtargets.test.ts +56 -0
  213. package/tests/policy.test.ts +8284 -0
  214. package/tests/portability.test.ts +96 -0
  215. package/tests/progress.test.ts +1036 -0
  216. package/tests/promptlibrary.test.ts +116 -0
  217. package/tests/protocol.test.ts +4793 -0
  218. package/tests/publishingpipeline.test.ts +314 -0
  219. package/tests/quickactions.test.ts +164 -0
  220. package/tests/readiness.test.ts +78 -0
  221. package/tests/recipes.test.ts +163 -0
  222. package/tests/redactshots.test.ts +216 -0
  223. package/tests/registernvidiakeys.ts +51 -0
  224. package/tests/relayserve.test.ts +304 -0
  225. package/tests/releasenotes.test.ts +276 -0
  226. package/tests/resilience.test.ts +467 -0
  227. package/tests/resourceaware.test.ts +187 -0
  228. package/tests/runbudget.test.ts +125 -0
  229. package/tests/runhistory.test.ts +299 -0
  230. package/tests/runreplay.test.ts +113 -0
  231. package/tests/runresume.test.ts +184 -0
  232. package/tests/runstate.test.ts +288 -0
  233. package/tests/runtimeadapters.test.ts +251 -0
  234. package/tests/safariskeleton.test.ts +115 -0
  235. package/tests/sandboxframe.test.ts +196 -0
  236. package/tests/sbom.test.ts +105 -0
  237. package/tests/secretvault.test.ts +188 -0
  238. package/tests/selcache.test.ts +111 -0
  239. package/tests/sessioninterface.test.ts +791 -0
  240. package/tests/sessions.test.ts +591 -0
  241. package/tests/siteprefs.test.ts +228 -0
  242. package/tests/snapshotdelta.test.ts +165 -0
  243. package/tests/statusviews.test.ts +237 -0
  244. package/tests/stepmeter.test.ts +200 -0
  245. package/tests/streamparse.test.ts +163 -0
  246. package/tests/stylepruning.test.ts +90 -0
  247. package/tests/surfaces.test.ts +282 -0
  248. package/tests/sweep.test.ts +78 -0
  249. package/tests/syncbridge.test.ts +164 -0
  250. package/tests/taskqueue.test.ts +206 -0
  251. package/tests/toolcalls.test.ts +336 -0
  252. package/tests/toolcatalog.test.ts +201 -0
  253. package/tests/torture-agent.test.ts +706 -0
  254. package/tests/torture-auth.test.ts +461 -0
  255. package/tests/torture-commands.test.ts +895 -0
  256. package/tests/torture-data.test.ts +923 -0
  257. package/tests/torture-environments.test.ts +285 -0
  258. package/tests/torture-export.test.ts +263 -0
  259. package/tests/torture-http.test.ts +1379 -0
  260. package/tests/torture-llm.test.ts +808 -0
  261. package/tests/torture-memory.test.ts +1084 -0
  262. package/tests/torture-plan.test.ts +829 -0
  263. package/tests/torture-run.test.ts +626 -0
  264. package/tests/torture-session.test.ts +928 -0
  265. package/tests/torture-swarm.test.ts +1245 -0
  266. package/tests/torture-tools.test.ts +964 -0
  267. package/tests/torture-workflow.test.ts +2354 -0
  268. package/tests/tourviews.test.ts +52 -0
  269. package/tests/transparency.test.ts +147 -0
  270. package/tests/trigger.test.ts +941 -0
  271. package/tests/v1sunset.test.ts +428 -0
  272. package/tests/virtlist.test.ts +94 -0
  273. package/tests/vision.test.ts +540 -0
  274. package/tests/vsixpack.test.ts +145 -0
  275. package/tests/webapi.test.ts +523 -0
  276. package/tests/workflow.test.ts +1785 -0
  277. package/tests/workfloweditor.test.ts +759 -0
  278. package/tests/xpipack.test.ts +128 -0
  279. package/tools.ts +734 -0
  280. package/types.ts +9908 -0
  281. package/umd.ts +9 -0
  282. package/utils.ts +249 -0
  283. package/version.ts +8 -0
  284. package/versionregistry.ts +1310 -0
  285. package/views.ts +2153 -0
  286. package/vitest.config.ts +12 -0
  287. package/web/App.tsx +78 -0
  288. package/web/README.md +47 -0
  289. package/web/capacitor.config.ts +46 -0
  290. package/web/const.ts +19 -0
  291. package/web/control.shell.tsx +66 -0
  292. package/web/dashboardpage.ts +554 -0
  293. package/web/db.ts +493 -0
  294. package/web/design.html +541 -0
  295. package/web/gatewayview/Gateway.tsx +345 -0
  296. package/web/gatewayview/config.ts +1042 -0
  297. package/web/gatewayview/definition.ts +174 -0
  298. package/web/icons.ts +15 -0
  299. package/{manifest.json → web/manifest.json} +1 -1
  300. package/web/optionspage.ts +691 -0
  301. package/web/popup.ts +1651 -0
  302. package/web/schema.prisma +176 -0
  303. package/web/sidepanel.ts +7941 -0
  304. package/web/sitemanifest.json +13 -0
  305. package/web/terminal-ui.tsx +622 -0
  306. package/web/transparencypage.ts +541 -0
  307. package/web/vite.config.ts +39 -0
  308. package/web/workspace.ts +23 -0
  309. package/workbenchmemory.ts +104 -0
  310. package/workbenchsession.ts +304 -0
  311. package/workflow.ts +4495 -0
  312. package/bridge.d.ts.map +0 -1
  313. package/bridge.js +0 -1877
  314. package/bun.d.ts.map +0 -1
  315. package/bun.js +0 -52329
  316. package/caps/background.json +0 -712
  317. package/caps/cli.json +0 -360
  318. package/caps/library.json +0 -3611
  319. package/caps/mcp.json +0 -126
  320. package/caps/pagebridge.json +0 -377
  321. package/caps/popup.json +0 -51
  322. package/caps/sidepanel.json +0 -522
  323. package/checksums.txt +0 -142
  324. package/cli.d.ts.map +0 -1
  325. package/companion.d.ts.map +0 -1
  326. package/companion.js +0 -389
  327. package/crossbrowser.d.ts.map +0 -1
  328. package/crossbrowser.js +0 -1191
  329. package/dashdone.d.ts.map +0 -1
  330. package/dashdone.js +0 -368
  331. package/deno.d.ts.map +0 -1
  332. package/deno.js +0 -52325
  333. package/devthink.umd.js +0 -52346
  334. package/fixtures/bun.ts +0 -7
  335. package/fixtures/cjs.js +0 -6
  336. package/fixtures/deno.ts +0 -6
  337. package/fixtures/example-org-pagestate.json +0 -22
  338. package/fixtures/importers/automa-workflow.json +0 -18
  339. package/fixtures/importers/selenium-side.json +0 -20
  340. package/fixtures/importers/tabular-plan.csv +0 -7
  341. package/fixtures/importers/uivision-macro.json +0 -10
  342. package/fixtures/importers/v1-plan.json +0 -13
  343. package/fixtures/mcp-client.mjs +0 -14
  344. package/fixtures/pages/checkout.html +0 -192
  345. package/fixtures/pages/dashboard.html +0 -110
  346. package/fixtures/pages/feed.html +0 -113
  347. package/fixtures/pages/product-grid.html +0 -150
  348. package/fixtures/plans/form-inventory-plan.json +0 -11
  349. package/fixtures/plans/release-notes-plan.json +0 -11
  350. package/fixtures/recipes/agentscompete.json +0 -11
  351. package/fixtures/recipes/agentsforms.json +0 -11
  352. package/fixtures/recipes/agentsmonitor.json +0 -11
  353. package/fixtures/recipes/agentsreview.json +0 -29
  354. package/fixtures/recipes/agentswarm.json +0 -11
  355. package/fixtures/recipes/fillcalendar.json +0 -12
  356. package/fixtures/recipes/fillcheckout.json +0 -13
  357. package/fixtures/recipes/filldependentdropdowns.json +0 -12
  358. package/fixtures/recipes/fillfromcsv.json +0 -13
  359. package/fixtures/recipes/fillloginform.json +0 -12
  360. package/fixtures/recipes/fillmultistepwizard.json +0 -13
  361. package/fixtures/recipes/fillregistration.json +0 -12
  362. package/fixtures/recipes/fillsearchfilters.json +0 -12
  363. package/fixtures/recipes/filltypeahead.json +0 -11
  364. package/fixtures/recipes/monitoralerts.json +0 -12
  365. package/fixtures/recipes/monitoravailability.json +0 -11
  366. package/fixtures/recipes/monitornetworkquiet.json +0 -12
  367. package/fixtures/recipes/monitorpagechanges.json +0 -11
  368. package/fixtures/recipes/monitorpricechange.json +0 -11
  369. package/fixtures/recipes/monitorschedule.json +0 -12
  370. package/fixtures/recipes/scrapedynamicprices.json +0 -13
  371. package/fixtures/recipes/scrapeimagegallery.json +0 -12
  372. package/fixtures/recipes/scrapeinfinitefeed.json +0 -13
  373. package/fixtures/recipes/scrapenestedlists.json +0 -12
  374. package/fixtures/recipes/scrapeproductgrid.json +0 -12
  375. package/fixtures/recipes/scrapereviews.json +0 -12
  376. package/fixtures/recipes/scrapesearchresults.json +0 -12
  377. package/fixtures/recipes/scrapetablepagination.json +0 -12
  378. package/fixtures/recipes/testaccessibility.json +0 -34
  379. package/fixtures/recipes/testconsoleerrors.json +0 -12
  380. package/fixtures/recipes/testdeeplinks.json +0 -12
  381. package/fixtures/recipes/testformvalidation.json +0 -13
  382. package/fixtures/recipes/testlayoutshift.json +0 -12
  383. package/fixtures/recipes/testlinkcheck.json +0 -12
  384. package/fixtures/recipes/testloadtiming.json +0 -12
  385. package/fixtures/recipes/testvisualdiff.json +0 -12
  386. package/fixtures/workflows/changelog-digest-workflow.json +0 -13
  387. package/fixtures/workflows/form-survey-workflow.json +0 -13
  388. package/gallery.json +0 -706
  389. package/hardening.d.ts.map +0 -1
  390. package/hardening.js +0 -188
  391. package/headless.d.ts.map +0 -1
  392. package/headless.js +0 -11522
  393. package/http.d.ts.map +0 -1
  394. package/http.js +0 -2165
  395. package/index.cjs +0 -54682
  396. package/index.d.ts.map +0 -1
  397. package/index.js +0 -52310
  398. package/index.neutral.js +0 -46343
  399. package/mcp.d.ts.map +0 -1
  400. package/mcp.js +0 -20515
  401. package/memory.d.ts.map +0 -1
  402. package/nativehost.template.json +0 -10
  403. package/node.cjs +0 -54686
  404. package/node.d.ts.map +0 -1
  405. package/pack.d.ts.map +0 -1
  406. package/pack.js +0 -805
  407. package/policy.d.ts.map +0 -1
  408. package/progress.d.ts.map +0 -1
  409. package/progress.js +0 -645
  410. package/protocol.d.ts.map +0 -1
  411. package/protocol.js +0 -8334
  412. package/schemas/audit.schema.json +0 -226
  413. package/schemas/capability.schema.json +0 -113
  414. package/schemas/envelope.schema.json +0 -162
  415. package/schemas/memory.schema.json +0 -51
  416. package/schemas/observation.schema.json +0 -113
  417. package/schemas/plan.schema.json +0 -208
  418. package/schemas/progress.schema.json +0 -126
  419. package/schemas/proposal.schema.json +0 -68
  420. package/schemas/session.schema.json +0 -82
  421. package/schemas/tool.schema.json +0 -94
  422. package/server.d.ts.map +0 -1
  423. package/server.js +0 -28877
  424. package/umd-example.html +0 -18
  425. package/umd.d.ts.map +0 -1
  426. /package/{bun.d.ts → dist/bun.d.ts} +0 -0
  427. /package/{cli.d.ts → dist/cli.d.ts} +0 -0
  428. /package/{companion.d.ts → dist/companion.d.ts} +0 -0
  429. /package/{crossbrowser.d.ts → dist/crossbrowser.d.ts} +0 -0
  430. /package/{dashdone.d.ts → dist/dashdone.d.ts} +0 -0
  431. /package/{deno.d.ts → dist/deno.d.ts} +0 -0
  432. /package/{hardening.d.ts → dist/hardening.d.ts} +0 -0
  433. /package/{headless.d.ts → dist/headless.d.ts} +0 -0
  434. /package/{http.d.ts → dist/http.d.ts} +0 -0
  435. /package/{index.d.ts → dist/index.d.ts} +0 -0
  436. /package/{mcp.d.ts → dist/mcp.d.ts} +0 -0
  437. /package/{memory.d.ts → dist/memory.d.ts} +0 -0
  438. /package/{node.d.ts → dist/node.d.ts} +0 -0
  439. /package/{pack.d.ts → dist/pack.d.ts} +0 -0
  440. /package/{policy.d.ts → dist/policy.d.ts} +0 -0
  441. /package/{progress.d.ts → dist/progress.d.ts} +0 -0
  442. /package/{protocol.d.ts → dist/protocol.d.ts} +0 -0
  443. /package/{server.d.ts → dist/server.d.ts} +0 -0
  444. /package/{umd.d.ts → dist/umd.d.ts} +0 -0
@@ -0,0 +1,807 @@
1
+ import { mkdtemp, readFile, rm, stat, writeFile } from "node:fs/promises";
2
+ import { spawn } from "node:child_process";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+ import { describe, expect, it } from "vitest";
6
+ import {
7
+ attachnativehost,
8
+ companionhandshakeframe,
9
+ crashnativehost,
10
+ detachnativehost,
11
+ installnativehost,
12
+ hostmanifestdestination,
13
+ nativedefaultstate,
14
+ nativediagnostics,
15
+ nativeframecheck,
16
+ nativeframeof,
17
+ nativeheartbeatframe,
18
+ nativecallrecordof,
19
+ nativecorrelationid,
20
+ nativecallevent,
21
+ nativedegradationof,
22
+ nativeerrorof,
23
+ nativefailureof,
24
+ nativehostcapabilities,
25
+ nativehostidplaceholder,
26
+ nativehostinstallerversion,
27
+ nativehostmanifesttemplate,
28
+ nativeportliveness,
29
+ nativeprotocolcompatible,
30
+ nativeratecheck,
31
+ nativesecretexclusion,
32
+ nativesurfacecatalog,
33
+ nativesurfacegrant,
34
+ nativesurfaceresult,
35
+ nativetransportenabled,
36
+ nativechoices,
37
+ negotiatenativecapabilities,
38
+ parsecompanionhandshake,
39
+ reattachnativehost,
40
+ recordnativecall,
41
+ redactnativeframe,
42
+ uninstallnativehost,
43
+ } from "../bridge.js";
44
+ import {
45
+ wsbridgeadvertiseframe,
46
+ wsbridgebindcheck,
47
+ wsbridgeenvelopeof,
48
+ wsbridgeextensionconnect,
49
+ wsbridgeframeauth,
50
+ wsbridgeframecounted,
51
+ wsbridgeframeof,
52
+ wsbridgeidlesweep,
53
+ wsbridgereport,
54
+ wsbridgesessionstart,
55
+ } from "../bridge.js";
56
+ import {
57
+ nativeescapehatchgate,
58
+ nativeheadlessgate,
59
+ nativeheartbeatintervalvalid,
60
+ nativeidlewindowvalid,
61
+ nativeinstallconsentgate,
62
+ nativekillswitchgate,
63
+ nativeratecapvalid,
64
+ nativesensitiveapprovalgate,
65
+ nativetransportconsentgate,
66
+ } from "../policy.js";
67
+ import type { nativecallrecord, nativehoststate, runsettings } from "../types.js";
68
+
69
+ const now = 1_800_000_000_000;
70
+ const hashof = (text: string): string => `hash(${text})`;
71
+ const installed: nativehoststate = {
72
+ installed: true,
73
+ hostname: "com.example.devthink",
74
+ extensionid: "extensionid0000000000000000000",
75
+ installerversion: nativehostinstallerversion,
76
+ companionversion: "1.1.85",
77
+ companionprotocol: "1",
78
+ port: "attached",
79
+ wsbridgeport: 49152,
80
+ installedat: now - 5000,
81
+ attachedat: now - 1000,
82
+ updatedat: now - 1000,
83
+ lasterrors: [],
84
+ };
85
+ const settings: runsettings = {
86
+ nativeinstallconsent: true,
87
+ nativetransportconsent: true,
88
+ nativecallclassconsents: ["read", "interaction"],
89
+ nativesurfaceconsents: ["notification"],
90
+ nativeidlewindow: 30_000,
91
+ nativeheartbeatinterval: 5000,
92
+ nativecallratelimit: 3,
93
+ nativecallratewindow: 60_000,
94
+ };
95
+
96
+ /** Spawns the fake host fixture with its knobs and returns the process handle with its frame reader. */
97
+ async function fakehost(
98
+ ...args: string[]
99
+ ): Promise<{
100
+ process: ReturnType<typeof spawn>;
101
+ send: (frame: unknown) => void;
102
+ read: () => Promise<Record<string, unknown>>;
103
+ close: () => Promise<void>;
104
+ }> {
105
+ const child = spawn(process.execPath, [join(import.meta.dirname, "fakehost.mjs"), ...args], {
106
+ stdio: ["pipe", "pipe", "inherit"],
107
+ });
108
+ const chunks: Buffer[] = [];
109
+ child.stdout.on("data", (chunk) => chunks.push(chunk));
110
+ const send = (frame: unknown): void => {
111
+ const payload = Buffer.from(JSON.stringify(frame), "utf8");
112
+ const head = Buffer.alloc(4);
113
+ head.writeUInt32LE(payload.length, 0);
114
+ child.stdin.write(Buffer.concat([head, payload]));
115
+ };
116
+ const read = async (): Promise<Record<string, unknown>> => {
117
+ for (let index = 0; index < 100; index += 1) {
118
+ const buffer = Buffer.concat(chunks);
119
+ if (buffer.length >= 4) {
120
+ const length = buffer.readUInt32LE(0);
121
+ if (buffer.length >= 4 + length) {
122
+ const payload = buffer.subarray(4, 4 + length);
123
+ chunks.splice(0, chunks.length, buffer.subarray(4 + length));
124
+ return JSON.parse(payload.toString("utf8")) as Record<string, unknown>;
125
+ }
126
+ }
127
+ await new Promise((resolve) => setTimeout(resolve, 20));
128
+ }
129
+ throw new Error("The fake host never answered.");
130
+ };
131
+ return {
132
+ process: child,
133
+ send,
134
+ read,
135
+ close: async () => {
136
+ child.stdin.end();
137
+ await new Promise((resolve) => child.on("close", resolve));
138
+ },
139
+ };
140
+ }
141
+
142
+ describe("native host manifest template and installer", () => {
143
+ it("builds the host manifest template with the generated extension id placeholder", () => {
144
+ const template = nativehostmanifesttemplate({
145
+ hostname: "com.example.devthink",
146
+ companionpath: "/home/user/devthink/dist/companion.js",
147
+ });
148
+ expect(template.manifest.name).toBe("com.example.devthink");
149
+ expect(template.manifest.type).toBe("stdio");
150
+ expect(template.manifest.allowed_origins).toEqual([`chrome-extension://${nativehostidplaceholder}/`]);
151
+ expect(template.text).toContain(nativehostidplaceholder);
152
+ const filled = nativehostmanifesttemplate({
153
+ hostname: "com.example.devthink",
154
+ companionpath: "/home/user/devthink/dist/companion.js",
155
+ extensionid: "extensionid0000000000000000000",
156
+ });
157
+ expect(filled.manifest.allowed_origins).toEqual(["chrome-extension://extensionid0000000000000000000/"]);
158
+ expect(() => nativehostmanifesttemplate({ hostname: "not a host", companionpath: "/x" })).toThrow();
159
+ expect(() => nativehostmanifesttemplate({ hostname: "com.example.devthink", companionpath: " " })).toThrow();
160
+ });
161
+
162
+ it("computes the manifest destination inside the user profile and refuses system wide without the flag", () => {
163
+ expect(hostmanifestdestination({ hostname: "com.example.devthink", profiledir: "/home/user/profile" })).toEqual({
164
+ path: "/home/user/profile/NativeMessagingHosts/com.example.devthink.json",
165
+ systemwide: false,
166
+ });
167
+ const refused = hostmanifestdestination({ hostname: "com.example.devthink" });
168
+ expect(refused.refused).toMatch(/explicit flag/);
169
+ expect(hostmanifestdestination({ hostname: "com.example.devthink", systemwide: true, platform: "linux" })).toEqual({
170
+ path: "/etc/opt/chrome/native-messaging-hosts/com.example.devthink.json",
171
+ systemwide: true,
172
+ });
173
+ expect(
174
+ hostmanifestdestination({ hostname: "com.example.devthink", systemwide: true, platform: "macos" }).path,
175
+ ).toContain("/Library/Google/Chrome/NativeMessagingHosts");
176
+ });
177
+
178
+ it("writes and removes the host manifest in a temp profile behind the install consent", async () => {
179
+ const profiledir = await mkdtemp(join(tmpdir(), "devthink-native-"));
180
+ const written: Record<string, string> = {};
181
+ const directories: string[] = [];
182
+ let removed = "";
183
+ const install = await installnativehost({
184
+ profiledir,
185
+ hostname: "com.example.devthink",
186
+ extensionid: "extensionid0000000000000000000",
187
+ companionpath: "/home/user/devthink/dist/companion.js",
188
+ consent: false,
189
+ now,
190
+ io: {
191
+ writefile: async (path, text) => {
192
+ written[path] = text;
193
+ },
194
+ mkdir: async (dir) => {
195
+ directories.push(dir);
196
+ },
197
+ },
198
+ });
199
+ expect(install.refused).toMatch(/install consent gate/i);
200
+ expect(install.state).toEqual(nativedefaultstate());
201
+ const success = await installnativehost({
202
+ profiledir,
203
+ hostname: "com.example.devthink",
204
+ extensionid: "extensionid0000000000000000000",
205
+ companionpath: "/home/user/devthink/dist/companion.js",
206
+ consent: true,
207
+ now,
208
+ io: {
209
+ writefile: async (path, text) => {
210
+ written[path] = text;
211
+ },
212
+ mkdir: async (dir) => {
213
+ directories.push(dir);
214
+ },
215
+ },
216
+ });
217
+ expect(success.refused).toBeUndefined();
218
+ expect(success.state.installed).toBe(true);
219
+ expect(success.state.installerversion).toBe(nativehostinstallerversion);
220
+ expect(success.state.port).toBe("detached");
221
+ const manifestpath = join(profiledir, "NativeMessagingHosts", "com.example.devthink.json");
222
+ expect(written[manifestpath]).toContain("chrome-extension://extensionid0000000000000000000/");
223
+ expect(written[manifestpath]).not.toContain(nativehostidplaceholder);
224
+ const both = await installnativehost({
225
+ profiledir,
226
+ hostname: "com.example.devthink",
227
+ extensionid: "x",
228
+ companionpath: "/x",
229
+ consent: true,
230
+ now,
231
+ systemwide: true,
232
+ io: { writefile: async () => {}, mkdir: async () => {} },
233
+ });
234
+ expect(both.refused).toMatch(/never both at once/);
235
+ const uninstall = await uninstallnativehost({
236
+ profiledir,
237
+ hostname: "com.example.devthink",
238
+ now: now + 1000,
239
+ io: {
240
+ exists: async (path) => path === manifestpath,
241
+ removefile: async (path) => {
242
+ removed = path;
243
+ },
244
+ },
245
+ });
246
+ expect(uninstall.removed).toBe(true);
247
+ expect(removed).toBe(manifestpath);
248
+ expect(uninstall.state).toEqual(nativedefaultstate());
249
+ await rm(profiledir, { recursive: true, force: true });
250
+ });
251
+
252
+ it("installs and removes a real host manifest through the io seams of a temp profile", async () => {
253
+ const profiledir = await mkdtemp(join(tmpdir(), "devthink-native-real-"));
254
+ const install = await installnativehost({
255
+ profiledir,
256
+ hostname: "com.example.devthink",
257
+ extensionid: "extensionid0000000000000000000",
258
+ companionpath: "/home/user/devthink/dist/companion.js",
259
+ consent: true,
260
+ now,
261
+ io: {
262
+ writefile: async (path, text) => {
263
+ await writeFile(path, text, "utf8");
264
+ },
265
+ mkdir: async (dir) => {
266
+ await (await import("node:fs/promises")).mkdir(dir, { recursive: true });
267
+ },
268
+ },
269
+ });
270
+ expect(install.refused).toBeUndefined();
271
+ const manifestpath = install.manifestpath;
272
+ expect((await stat(manifestpath)).isFile()).toBe(true);
273
+ const stored = JSON.parse(await readFile(manifestpath, "utf8")) as { name: string; allowed_origins: string[] };
274
+ expect(stored.name).toBe("com.example.devthink");
275
+ expect(stored.allowed_origins).toEqual(["chrome-extension://extensionid0000000000000000000/"]);
276
+ const uninstall = await uninstallnativehost({
277
+ profiledir,
278
+ hostname: "com.example.devthink",
279
+ now: now + 1,
280
+ io: {
281
+ exists: async (path) => {
282
+ try {
283
+ await stat(path);
284
+ return true;
285
+ } catch {
286
+ return false;
287
+ }
288
+ },
289
+ removefile: async (path) => {
290
+ await rm(path, { force: true });
291
+ },
292
+ },
293
+ });
294
+ expect(uninstall.removed).toBe(true);
295
+ await expect(stat(manifestpath)).rejects.toThrow();
296
+ await rm(profiledir, { recursive: true, force: true });
297
+ });
298
+ });
299
+
300
+ describe("native consent gates", () => {
301
+ it("explains the install scope before any host registration", () => {
302
+ const refused = nativeinstallconsentgate({
303
+ consent: false,
304
+ profiledir: "/home/user/profile",
305
+ hostname: "com.example.devthink",
306
+ });
307
+ expect(refused.allowed).toBe(false);
308
+ expect(refused.reason).toMatch(/scope/i);
309
+ expect(nativeinstallconsentgate({ consent: true, profiledir: " ", hostname: "com.example.devthink" }).allowed).toBe(
310
+ false,
311
+ );
312
+ expect(nativeinstallconsentgate({ consent: true, profiledir: "/home/user/profile", hostname: " " }).allowed).toBe(
313
+ false,
314
+ );
315
+ expect(
316
+ nativeinstallconsentgate({ consent: true, profiledir: "/home/user/profile", hostname: "com.example.devthink" })
317
+ .allowed,
318
+ ).toBe(true);
319
+ });
320
+
321
+ it("blocks native calls per class before the approval and widens no read grant", () => {
322
+ const denied = nativetransportconsentgate({ consent: false, installed: true, callclass: "read" });
323
+ expect(denied.allowed).toBe(false);
324
+ expect(denied.reason).toMatch(/deny by default|disabled/i);
325
+ const uninstalled = nativetransportconsentgate({
326
+ consent: true,
327
+ installed: false,
328
+ callclass: "read",
329
+ classconsents: ["read"],
330
+ });
331
+ expect(uninstalled.allowed).toBe(false);
332
+ expect(uninstalled.reason).toMatch(/uninstalled/i);
333
+ const nowide = nativetransportconsentgate({
334
+ consent: true,
335
+ installed: true,
336
+ callclass: "sensitive",
337
+ classconsents: ["read"],
338
+ });
339
+ expect(nowide.allowed).toBe(false);
340
+ expect(nowide.reason).toMatch(/never widens a read grant/);
341
+ const granted = nativetransportconsentgate({
342
+ consent: true,
343
+ installed: true,
344
+ callclass: "interaction",
345
+ classconsents: ["read", "interaction"],
346
+ });
347
+ expect(granted.allowed).toBe(true);
348
+ const stopped = nativetransportconsentgate({
349
+ consent: true,
350
+ installed: true,
351
+ callclass: "read",
352
+ classconsents: ["read"],
353
+ killswitch: true,
354
+ });
355
+ expect(stopped.allowed).toBe(false);
356
+ expect(stopped.reason).toMatch(/kill switch/i);
357
+ });
358
+
359
+ it("routes the sensitive native calls through the human approval gate", () => {
360
+ expect(nativesensitiveapprovalgate({ callclass: "read" }).allowed).toBe(true);
361
+ expect(nativesensitiveapprovalgate({ callclass: "sensitive" }).allowed).toBe(false);
362
+ expect(nativesensitiveapprovalgate({ callclass: "sensitive", approval: true }).allowed).toBe(true);
363
+ });
364
+
365
+ it("keeps the native transport disabled in headless mode without a configured host", () => {
366
+ expect(nativeheadlessgate({ headless: true, hostconfigured: false }).allowed).toBe(false);
367
+ expect(nativeheadlessgate({ headless: true, hostconfigured: true }).allowed).toBe(true);
368
+ expect(nativeheadlessgate({ headless: false, hostconfigured: false }).allowed).toBe(true);
369
+ });
370
+
371
+ it("validates the user configured windows and caps with no code default", () => {
372
+ expect(nativeidlewindowvalid(undefined).allowed).toBe(true);
373
+ expect(nativeidlewindowvalid(0).allowed).toBe(false);
374
+ expect(nativeidlewindowvalid(30_000).allowed).toBe(true);
375
+ expect(nativeheartbeatintervalvalid(-1).allowed).toBe(false);
376
+ expect(nativeheartbeatintervalvalid(5000).allowed).toBe(true);
377
+ expect(nativeratecapvalid(0).allowed).toBe(false);
378
+ expect(nativeratecapvalid(1.5).allowed).toBe(false);
379
+ expect(nativeratecapvalid(3).allowed).toBe(true);
380
+ });
381
+ });
382
+
383
+ describe("native host port lifecycle against a fake host process", () => {
384
+ it("handshakes the fake host over the length prefixed wire and attaches the port", async () => {
385
+ const host = await fakehost("--build", "fixture-1.1.85", "--protocol", "1", "--port", "49152");
386
+ const correlationid = nativecorrelationid("ext", 1);
387
+ host.send(companionhandshakeframe(correlationid));
388
+ let answer = await host.read();
389
+ while (answer.kind !== "handshake") answer = await host.read();
390
+ const parsed = parsecompanionhandshake(
391
+ {
392
+ kind: answer.kind as "handshake",
393
+ correlationid: String(answer.correlationid),
394
+ ...(answer.body !== undefined ? { body: answer.body as Record<string, unknown> } : {}),
395
+ },
396
+ now,
397
+ );
398
+ expect(parsed.handshake).toMatchObject({ build: "fixture-1.1.85", protocol: "1", wsbridgeport: 49152 });
399
+ const attached = attachnativehost(nativedefaultstate(), parsed.handshake!, now);
400
+ expect(attached.port).toBe("attached");
401
+ expect(attached.companionversion).toBe("fixture-1.1.85");
402
+ expect(attached.wsbridgeport).toBe(49152);
403
+ host.send(nativeheartbeatframe(correlationid, now));
404
+ let heartbeat = await host.read();
405
+ while (heartbeat.kind !== "heartbeat") heartbeat = await host.read();
406
+ expect(heartbeat.kind).toBe("heartbeat");
407
+ await host.close();
408
+ });
409
+
410
+ it("negotiates versions with the companion and reports the mismatch of another major version", () => {
411
+ const agreed = negotiatenativecapabilities({
412
+ ours: nativehostcapabilities(),
413
+ theirs: { protocol: "1.1.85", surfaces: ["osdialog", "notification"], heartbeat: true },
414
+ });
415
+ expect(agreed.ok).toBe(true);
416
+ expect(agreed.protocol).toBe(1);
417
+ expect(agreed.surfaces).toEqual(["osdialog", "notification"]);
418
+ expect(agreed.heartbeat).toBe(true);
419
+ const mismatch = negotiatenativecapabilities({
420
+ ours: nativehostcapabilities(),
421
+ theirs: { protocol: 2, surfaces: ["notification"], heartbeat: true },
422
+ });
423
+ expect(mismatch.ok).toBe(false);
424
+ expect(mismatch.reason).toMatch(/one major version/);
425
+ expect(nativeprotocolcompatible("1.1.85", "1.0.0").ok).toBe(true);
426
+ expect(nativeprotocolcompatible("1.1.85", "2.0.0").ok).toBe(false);
427
+ });
428
+
429
+ it("crashes, reattaches and degrades while the run stays alive", () => {
430
+ const crashed = crashnativehost(installed, now + 1000);
431
+ expect(crashed.port).toBe("crashed");
432
+ expect(crashed.lasterrors?.[0]).toMatchObject({ family: "port", retry: "reconnect" });
433
+ const reattached = reattachnativehost(
434
+ crashed,
435
+ { build: "1.1.85", protocol: "1", surfaces: ["osdialog", "notification"], wsbridgeport: 49153 },
436
+ now + 2000,
437
+ );
438
+ expect(reattached.port).toBe("attached");
439
+ expect(reattached.wsbridgeport).toBe(49153);
440
+ expect(detachnativehost(reattached, now + 3000).port).toBe("detached");
441
+ const absent = nativedegradationof({ state: nativedefaultstate() });
442
+ expect(absent.degraded).toBe(true);
443
+ expect(absent.reason).toMatch(/absent/i);
444
+ const outdated = nativedegradationof({
445
+ state: installed,
446
+ negotiated: { ok: false, reason: "The companion speaks the native bridge protocol major version 2." },
447
+ });
448
+ expect(outdated.degraded).toBe(true);
449
+ expect(outdated.reason).toMatch(/major version 2/);
450
+ expect(nativedegradationof({ state: installed }).degraded).toBe(false);
451
+ });
452
+
453
+ it("detects the host liveness from the heartbeat frames under the user configured interval", () => {
454
+ expect(nativeportliveness({ now, interval: 5000 }).alive).toBe(false);
455
+ expect(nativeportliveness({ now }).alive).toBe(true);
456
+ expect(nativeportliveness({ lastheartbeatat: now - 1000, now, interval: 5000 }).alive).toBe(true);
457
+ const silent = nativeportliveness({ lastheartbeatat: now - 6000, now, interval: 5000 });
458
+ expect(silent.alive).toBe(false);
459
+ expect(silent.reason).toMatch(/silent/);
460
+ const frame = nativeheartbeatframe(nativecorrelationid("ext", 1), now);
461
+ expect(frame.kind).toBe("heartbeat");
462
+ expect(frame.body).toEqual({ at: now });
463
+ });
464
+ });
465
+
466
+ describe("native frames, secrets and correlation", () => {
467
+ it("validates every incoming frame through the origin and session checks", () => {
468
+ const sessionid = "sess-1";
469
+ expect(
470
+ nativeframecheck(nativeframeof("event", nativecorrelationid("ext", 1), {}, sessionid), sessionid).allowed,
471
+ ).toBe(true);
472
+ expect(nativeframecheck({ kind: "event", correlationid: " " }, sessionid).allowed).toBe(false);
473
+ expect(nativeframecheck(nativeframeof("event", "run-x-1", {}, "sess-2"), sessionid).allowed).toBe(false);
474
+ expect(nativeframecheck(nativeframeof("event", "run-x-1"), " ").allowed).toBe(false);
475
+ });
476
+
477
+ it("stamps the correlation ids across one run", () => {
478
+ expect(nativecorrelationid("ext", 1)).toBe("run-ext-1");
479
+ expect(nativecorrelationid("ext", 2)).toBe("run-ext-2");
480
+ expect(() => nativecorrelationid(" ", 1)).toThrow();
481
+ expect(() => nativecorrelationid("ext", 0)).toThrow();
482
+ });
483
+
484
+ it("keeps key vault material out of the native frames and the logs", () => {
485
+ const frame = nativeframeof("call", "run-ext-1", { apikey: "material", token: "material", surface: "osdialog" });
486
+ const excluded = nativesecretexclusion(frame);
487
+ expect(excluded.ok).toBe(false);
488
+ expect(excluded.held).toEqual(["apikey", "token"]);
489
+ const redacted = redactnativeframe(frame);
490
+ expect(redacted.body).toEqual({ surface: "osdialog" });
491
+ expect(
492
+ nativesecretexclusion(nativeframeof("call", "run-ext-1", { surface: "notification", text: "done" })).ok,
493
+ ).toBe(true);
494
+ });
495
+
496
+ it("maps native failures to structured errors with retry hints", () => {
497
+ const error = nativeerrorof("handshake", "The companion never answered.", "reconnect", now);
498
+ expect(nativefailureof(error)).toEqual({
499
+ retryhint: "retry",
500
+ reason: "handshake failure: The companion never answered.",
501
+ });
502
+ expect(
503
+ nativefailureof(nativeerrorof("installer", "The profile directory refused.", "reinstall", now)).retryhint,
504
+ ).toBe("wait");
505
+ expect(nativefailureof(nativeerrorof("surface", "No consent grant.", "none", now)).retryhint).toBe("none");
506
+ });
507
+ });
508
+
509
+ describe("native audit trail and rate caps", () => {
510
+ it("records every native call with class and outcome", () => {
511
+ const first = nativecallrecordof({
512
+ id: "n1",
513
+ correlationid: "run-ext-1",
514
+ surface: "notification",
515
+ callclass: "interaction",
516
+ outcome: "ok",
517
+ now,
518
+ });
519
+ const refused = nativecallrecordof({
520
+ id: "n2",
521
+ correlationid: "run-ext-1",
522
+ surface: "osdialog",
523
+ callclass: "sensitive",
524
+ outcome: "refused",
525
+ reason: "The gates held.",
526
+ now: now + 1,
527
+ });
528
+ const failed = nativecallrecordof({
529
+ id: "n3",
530
+ correlationid: "run-ext-2",
531
+ surface: "notification",
532
+ callclass: "interaction",
533
+ outcome: "error",
534
+ reason: "The port stayed detached.",
535
+ now: now + 2,
536
+ });
537
+ let records = recordnativecall([], first);
538
+ records = recordnativecall(records, refused);
539
+ records = recordnativecall(records, failed);
540
+ expect(records.map((record) => record.outcome)).toEqual(["error", "refused", "ok"]);
541
+ expect(records[1]?.reason).toMatch(/gates held/);
542
+ const replaced = recordnativecall(
543
+ records,
544
+ nativecallrecordof({
545
+ id: "n3",
546
+ correlationid: "run-ext-2",
547
+ surface: "notification",
548
+ callclass: "interaction",
549
+ outcome: "ok",
550
+ now: now + 3,
551
+ }),
552
+ );
553
+ expect(replaced).toHaveLength(3);
554
+ expect(replaced[0]?.outcome).toBe("ok");
555
+ const event = nativecallevent(failed);
556
+ expect(event.method).toBe("native/call");
557
+ expect(event.params).toMatchObject({
558
+ surface: "notification",
559
+ callclass: "interaction",
560
+ outcome: "error",
561
+ correlationid: "run-ext-2",
562
+ });
563
+ expect(JSON.stringify(event.params)).not.toContain("reason");
564
+ });
565
+
566
+ it("caps the native calls per session inside the user configured window", () => {
567
+ const calls: nativecallrecord[] = [
568
+ nativecallrecordof({
569
+ id: "n1",
570
+ correlationid: "run-ext-1",
571
+ surface: "notification",
572
+ callclass: "interaction",
573
+ outcome: "ok",
574
+ now: now - 90_000,
575
+ }),
576
+ nativecallrecordof({
577
+ id: "n2",
578
+ correlationid: "run-ext-1",
579
+ surface: "notification",
580
+ callclass: "interaction",
581
+ outcome: "ok",
582
+ now: now - 30_000,
583
+ }),
584
+ nativecallrecordof({
585
+ id: "n3",
586
+ correlationid: "run-ext-1",
587
+ surface: "notification",
588
+ callclass: "interaction",
589
+ outcome: "ok",
590
+ now: now - 10_000,
591
+ }),
592
+ nativecallrecordof({
593
+ id: "n4",
594
+ correlationid: "run-ext-1",
595
+ surface: "notification",
596
+ callclass: "interaction",
597
+ outcome: "ok",
598
+ now: now - 5000,
599
+ }),
600
+ ];
601
+ expect(nativeratecheck({ calls, now })).toEqual({ allowed: true, used: 4 });
602
+ const capped = nativeratecheck({ calls, cap: 3, window: 60_000, now });
603
+ expect(capped.allowed).toBe(false);
604
+ expect(capped.used).toBe(3);
605
+ expect(capped.reason).toMatch(/call cap/);
606
+ const fresh = nativeratecheck({ calls, cap: 3, window: 60_000, now: now + 120_000 });
607
+ expect(fresh.allowed).toBe(true);
608
+ expect(fresh.used).toBe(0);
609
+ expect(nativeratecheck({ calls, cap: 0, now }).allowed).toBe(false);
610
+ });
611
+
612
+ it("stops in flight native calls through the kill switch and the escape hatch", () => {
613
+ expect(nativekillswitchgate({ engaged: true, operation: "native surface call" }).allowed).toBe(false);
614
+ expect(nativekillswitchgate({ engaged: false, operation: "native surface call" }).allowed).toBe(true);
615
+ const escape = nativeescapehatchgate({ pressed: true, inflight: 2 });
616
+ expect(escape.allowed).toBe(false);
617
+ expect(escape.reason).toMatch(/2 in flight calls halted/);
618
+ expect(nativeescapehatchgate({ pressed: false, inflight: 2 }).allowed).toBe(true);
619
+ });
620
+ });
621
+
622
+ describe("native surfaces and diagnostics", () => {
623
+ it("enumerates the native surfaces through the capability negotiation", () => {
624
+ const catalog = nativesurfacecatalog();
625
+ expect(catalog.map((entry) => entry.surface)).toEqual(["osdialog", "notification"]);
626
+ expect(catalog.map((entry) => entry.callclass)).toEqual(["sensitive", "interaction"]);
627
+ const negotiation = negotiatenativecapabilities({
628
+ ours: nativehostcapabilities(),
629
+ theirs: { protocol: 1, surfaces: ["notification"], heartbeat: false },
630
+ });
631
+ expect(negotiation.surfaces).toEqual(["notification"]);
632
+ expect(negotiation.heartbeat).toBe(false);
633
+ });
634
+
635
+ it("refuses the surface execution without an active consent grant and shapes the structured results", () => {
636
+ expect(nativesurfacegrant("osdialog", settings).allowed).toBe(false);
637
+ expect(nativesurfacegrant("notification", settings).allowed).toBe(true);
638
+ expect(nativesurfacegrant("unknown", settings).allowed).toBe(false);
639
+ expect(nativesurfacegrant("notification", {}).allowed).toBe(false);
640
+ const result = nativesurfaceresult({
641
+ surface: "notification",
642
+ callclass: "read",
643
+ details: { title: "done", apikey: "material" },
644
+ now,
645
+ });
646
+ expect(result.callclass).toBe("interaction");
647
+ expect(result.details).toEqual({ title: "done" });
648
+ });
649
+
650
+ it("reports the port state, the versions and the last errors through the diagnostics", () => {
651
+ const report = nativediagnostics({
652
+ state: {
653
+ ...installed,
654
+ port: "crashed",
655
+ lasterrors: [nativeerrorof("port", "The companion crashed.", "reconnect", now)],
656
+ },
657
+ settings,
658
+ sessions: [],
659
+ now,
660
+ });
661
+ expect(report.installed).toBe(true);
662
+ expect(report.port).toBe("crashed");
663
+ expect(report.companionversion).toBe("1.1.85");
664
+ expect(report.installerversion).toBe(nativehostinstallerversion);
665
+ expect(report.nativebridgeprotocol).toBe(1);
666
+ expect(report.transportconsent).toBe(true);
667
+ expect(report.classconsents).toEqual(["read", "interaction"]);
668
+ expect(report.surfaceconsents).toEqual(["notification"]);
669
+ expect(report.lasterrors).toHaveLength(1);
670
+ const clean = nativediagnostics({ state: undefined, now });
671
+ expect(clean.installed).toBe(false);
672
+ expect(clean.degraded.degraded).toBe(true);
673
+ });
674
+
675
+ it("keeps the deny by default posture and reads the choices from the settings", () => {
676
+ expect(nativetransportenabled(undefined, settings)).toBe(false);
677
+ expect(nativetransportenabled(installed, {})).toBe(false);
678
+ expect(nativetransportenabled(installed, settings)).toBe(true);
679
+ expect(nativechoices(settings)).toMatchObject({
680
+ nativeinstallconsent: true,
681
+ nativetransportconsent: true,
682
+ nativecallratelimit: 3,
683
+ });
684
+ expect(nativechoices(undefined)).toEqual({});
685
+ });
686
+ });
687
+
688
+ describe("wsbridge sessions", () => {
689
+ it("refuses every bind outside localhost", () => {
690
+ expect(wsbridgebindcheck({ bind: "127.0.0.1", port: 0 })).toMatchObject({ ok: true, bind: "127.0.0.1", port: 0 });
691
+ expect(wsbridgebindcheck({ bind: "localhost" }).ok).toBe(true);
692
+ expect(wsbridgebindcheck({ bind: "::1" }).ok).toBe(true);
693
+ expect(wsbridgebindcheck({ bind: "" })).toMatchObject({ ok: true, bind: "127.0.0.1" });
694
+ const remote = wsbridgebindcheck({ bind: "0.0.0.0" });
695
+ expect(remote.ok).toBe(false);
696
+ expect(remote.reason).toMatch(/other machines/);
697
+ expect(wsbridgebindcheck({ bind: "192.168.1.10" }).ok).toBe(false);
698
+ expect(wsbridgebindcheck({ bind: "127.0.0.1", port: 70000 }).ok).toBe(false);
699
+ });
700
+
701
+ it("authenticates the frames with the per session token and expires the idle sessions", () => {
702
+ const session = wsbridgesessionstart({ port: 49152, now, token: "session-token", hashof });
703
+ expect(session.tokenhash).toBe("hash(session-token)");
704
+ expect(session.extensionconnected).toBe(false);
705
+ expect(wsbridgeframeauth(session, "session-token", hashof).ok).toBe(true);
706
+ const wrong = wsbridgeframeauth(session, "wrong", hashof);
707
+ expect(wrong.ok).toBe(false);
708
+ expect(wrong.reason).toMatch(/authentication/);
709
+ expect(wsbridgeframeauth(session, "", hashof).ok).toBe(false);
710
+ const idle = wsbridgesessionstart({ port: 49152, now, token: "session-token", hashof, idlewindow: 1000 });
711
+ expect(idle.expiresat).toBe(now + 1000);
712
+ expect(wsbridgeidlesweep(idle, now + 1500).expired).toBe(true);
713
+ expect(wsbridgeidlesweep(idle, now + 500).expired).toBe(false);
714
+ const withoutwindow = wsbridgesessionstart({ port: 49152, now, token: "session-token", hashof });
715
+ expect(wsbridgeidlesweep(withoutwindow, now + 10_000_000).expired).toBe(false);
716
+ const counted = wsbridgeframecounted(idle, "received", now + 900);
717
+ expect(wsbridgeidlesweep(counted, now + 1800).expired).toBe(false);
718
+ expect(wsbridgeidlesweep(counted, now + 2000).expired).toBe(true);
719
+ });
720
+
721
+ it("holds one extension connection at a time and reports the session without the token", () => {
722
+ const session = wsbridgesessionstart({ port: 49152, now, token: "session-token", hashof, idlewindow: 5000 });
723
+ const first = wsbridgeextensionconnect(session, now + 1);
724
+ expect(first.session?.extensionconnected).toBe(true);
725
+ expect(first.session?.connections).toBe(1);
726
+ const second = wsbridgeextensionconnect(first.session!, now + 2);
727
+ expect(second.refused).toMatch(/one extension connection at a time/);
728
+ expect(second.session).toBeUndefined();
729
+ const report = wsbridgereport(first.session!, now + 3);
730
+ expect(report).toMatchObject({ port: 49152, extensionconnected: true, expired: false });
731
+ expect(JSON.stringify(report)).not.toContain("session-token");
732
+ expect(JSON.stringify(report)).not.toContain("tokenhash");
733
+ });
734
+
735
+ it("wraps the native frames into the servercontract envelopes and advertises the port and token over the native port", () => {
736
+ const session = wsbridgesessionstart({ port: 49152, now, token: "session-token", hashof });
737
+ const correlationid = nativecorrelationid("ext", 1);
738
+ const frame = nativeframeof("event", correlationid, { surface: "notification" }, "sess-1");
739
+ const envelope = wsbridgeenvelopeof({
740
+ opid: "op-ext-1",
741
+ at: now,
742
+ frame,
743
+ sessionid: "sess-1",
744
+ token: "session-token",
745
+ });
746
+ expect(envelope.op).toBe("eventpost");
747
+ expect(envelope.version).toBe(1);
748
+ expect(envelope.sessionid).toBe("sess-1");
749
+ const unwrapped = wsbridgeframeof(envelope);
750
+ expect(unwrapped.frame).toEqual(frame);
751
+ expect(wsbridgeframeof({ op: "sessioncreate" }).reason).toMatch(/eventpost/);
752
+ expect(wsbridgeframeof({ op: "eventpost", body: { stream: "other", payload: { frame } } }).reason).toMatch(
753
+ /nativetransport/,
754
+ );
755
+ expect(wsbridgeframeof({ op: "eventpost", body: { stream: "nativetransport", payload: {} } }).reason).toMatch(
756
+ /frame key/,
757
+ );
758
+ const advertisement = wsbridgeadvertiseframe(session, "session-token", correlationid);
759
+ expect(advertisement.kind).toBe("advertisement");
760
+ expect(advertisement.body).toMatchObject({ port: session.port, token: "session-token" });
761
+ expect(() => wsbridgeadvertiseframe(session, " ", correlationid)).toThrow();
762
+ });
763
+ });
764
+
765
+ describe("native bridge smoke against the fake host process", () => {
766
+ it("runs the handshake, the surface call and the refusal path against the fake host", async () => {
767
+ const host = await fakehost("--build", "fixture-1.1.85", "--protocol", "1", "--port", "0");
768
+ const correlationid = nativecorrelationid("ext", 1);
769
+ host.send(companionhandshakeframe(correlationid));
770
+ const handshake = await host.read();
771
+ expect(handshake.kind).toBe("handshake");
772
+ host.send({ kind: "call", correlationid, body: { surface: "osdialog", classconsent: true, surfaceconsent: true } });
773
+ const call = await host.read();
774
+ expect(call.body).toMatchObject({ surface: "osdialog", delivered: true });
775
+ host.send({ kind: "call", correlationid, body: { surface: "osdialog" } });
776
+ const refusal = await host.read();
777
+ expect(refusal.kind).toBe("error");
778
+ expect((refusal.body as { message?: string }).message).toMatch(/consent grant/);
779
+ await host.close();
780
+ });
781
+
782
+ it("refuses the outdated host handshake of another protocol major version", async () => {
783
+ const host = await fakehost("--build", "fixture-2.0.0", "--protocol", "2");
784
+ host.send(companionhandshakeframe(nativecorrelationid("ext", 1)));
785
+ const answer = await host.read();
786
+ const parsed = parsecompanionhandshake(
787
+ {
788
+ kind: answer.kind as "handshake",
789
+ correlationid: String(answer.correlationid),
790
+ ...(answer.body !== undefined ? { body: answer.body as Record<string, unknown> } : {}),
791
+ },
792
+ now,
793
+ );
794
+ const negotiation = negotiatenativecapabilities({
795
+ ours: nativehostcapabilities(),
796
+ theirs: {
797
+ protocol: parsed.handshake?.protocol ?? "2",
798
+ surfaces: parsed.handshake?.surfaces ?? [],
799
+ heartbeat: true,
800
+ },
801
+ });
802
+ expect(negotiation.ok).toBe(false);
803
+ expect(negotiation.reason).toMatch(/one major version/);
804
+ expect(nativedegradationof({ state: installed, negotiated: negotiation }).degraded).toBe(true);
805
+ await host.close();
806
+ });
807
+ });