@wenathlan/devthink 2.0.12 → 2.0.14

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 +20 -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,251 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
3
+ import { join } from "node:path";
4
+ import {
5
+ adapterdeclarationsof,
6
+ capabilityprobeof,
7
+ chromestorageadapter,
8
+ denokvadapter,
9
+ dommapof,
10
+ downgradesof,
11
+ fetchshellof,
12
+ filesystemstorageadapter,
13
+ memorystorageadapter,
14
+ portablecapabilityset,
15
+ runtimeadapterdeclarationof,
16
+ runtimeadapterof,
17
+ runtimecapabilitiestable,
18
+ timershellof,
19
+ workermapof,
20
+ } from "../runtime.js";
21
+ import { actionkindcatalog, actionrisk, adaptermappinggate, capabilitydowngradegate } from "../policy.js";
22
+ import { sessionmemory } from "../memory.js";
23
+ import type { capabilityprobe } from "../types.js";
24
+
25
+ const now = 1_800_000_000_000;
26
+
27
+ describe("capability probes", () => {
28
+ it("probes the dom, storage, network and worker capabilities of one runtime without failing anything", () => {
29
+ expect(capabilityprobeof({ dom: true, storage: true, network: true, worker: true })).toEqual({
30
+ dom: true,
31
+ storage: true,
32
+ network: true,
33
+ worker: true,
34
+ });
35
+ const table = runtimecapabilitiestable();
36
+ expect(table.browser).toEqual({ dom: true, storage: true, network: true, worker: true });
37
+ expect(table.node.dom).toBe(false);
38
+ expect(table.bun.storage).toBe(true);
39
+ expect(table.deno.worker).toBe(true);
40
+ });
41
+
42
+ it("declares the adapter mapping of every platform target", () => {
43
+ expect(runtimeadapterdeclarationof("browser")).toMatchObject({
44
+ runtime: "browser",
45
+ storage: "chrome",
46
+ worker: "webworker",
47
+ dom: "livepage",
48
+ });
49
+ expect(runtimeadapterdeclarationof("browser", true).dom).toBe("remote");
50
+ expect(runtimeadapterdeclarationof("node")).toMatchObject({
51
+ storage: "filesystem",
52
+ worker: "workerthreads",
53
+ dom: "remote",
54
+ });
55
+ expect(runtimeadapterdeclarationof("bun").storage).toBe("filesystem");
56
+ expect(runtimeadapterdeclarationof("deno")).toMatchObject({
57
+ storage: "denokv",
58
+ worker: "webworker",
59
+ dom: "remote",
60
+ });
61
+ expect(adapterdeclarationsof()).toHaveLength(4);
62
+ expect(workermapof("node")).toBe("workerthreads");
63
+ expect(workermapof("deno")).toBe("webworker");
64
+ expect(dommapof("browser")).toBe("livepage");
65
+ expect(dommapof("headless")).toBe("remote");
66
+ });
67
+
68
+ it("gates the adapter mapping against its platform target", () => {
69
+ for (const runtime of ["browser", "node", "bun", "deno"] as const) {
70
+ expect(adaptermappinggate({ adapter: runtimeadapterdeclarationof(runtime) }).allowed).toBe(true);
71
+ expect(
72
+ adaptermappinggate({
73
+ adapter: runtimeadapterdeclarationof(runtime, runtime === "browser"),
74
+ headless: runtime === "browser",
75
+ }).allowed,
76
+ ).toBe(true);
77
+ }
78
+ expect(adaptermappinggate({ adapter: { ...runtimeadapterdeclarationof("node"), storage: "chrome" } }).allowed).toBe(
79
+ false,
80
+ );
81
+ expect(
82
+ adaptermappinggate({ adapter: { ...runtimeadapterdeclarationof("deno"), storage: "filesystem" } }).allowed,
83
+ ).toBe(false);
84
+ expect(
85
+ adaptermappinggate({ adapter: { ...runtimeadapterdeclarationof("bun"), worker: "webworker" } }).allowed,
86
+ ).toBe(false);
87
+ expect(
88
+ adaptermappinggate({ adapter: { ...runtimeadapterdeclarationof("browser"), dom: "remote" }, headless: false })
89
+ .allowed,
90
+ ).toBe(false);
91
+ expect(
92
+ adaptermappinggate({ adapter: { ...runtimeadapterdeclarationof("node"), fetch: "shim" as never } }).allowed,
93
+ ).toBe(false);
94
+ });
95
+
96
+ it("downgrades features instead of failing the runtime", () => {
97
+ const probes = capabilityprobeof({ dom: false, storage: true, network: true, worker: true });
98
+ const downgrades = downgradesof({
99
+ features: [
100
+ { feature: "page capture", capability: "dom" },
101
+ { feature: "profile store", capability: "storage" },
102
+ ],
103
+ probes,
104
+ });
105
+ expect(downgrades).toHaveLength(1);
106
+ expect(downgrades[0]).toMatchObject({ feature: "page capture", capability: "dom" });
107
+ expect(downgrades[0]?.reason).toMatch(/downgrades instead of failing the runtime/);
108
+ expect(capabilitydowngradegate({ downgraded: downgrades, failed: [] }).allowed).toBe(true);
109
+ expect(capabilitydowngradegate({ downgraded: [], failed: ["dom"] }).allowed).toBe(false);
110
+ expect(capabilitydowngradegate({ downgraded: [], failed: [] }).reason).toMatch(/no feature downgrades/);
111
+ });
112
+
113
+ it("derives the portable capability set from the probes and the reviewed vocabulary", () => {
114
+ const vocabulary = actionkindcatalog();
115
+ expect(vocabulary).toContain("observe");
116
+ expect(vocabulary).toContain("click");
117
+ const domprobes = capabilityprobeof({ dom: true, storage: true, network: true, worker: true });
118
+ expect(portablecapabilityset({ vocabulary, probes: domprobes })).toEqual(vocabulary);
119
+ const domless = portablecapabilityset({
120
+ vocabulary,
121
+ probes: capabilityprobeof({ dom: false, storage: true, network: true, worker: true }),
122
+ domlesskinds: vocabulary.filter((kind) => {
123
+ try {
124
+ return actionrisk(kind as never) === "read";
125
+ } catch {
126
+ return false;
127
+ }
128
+ }),
129
+ });
130
+ expect(domless).not.toContain("click");
131
+ expect(domless).toContain("readtext");
132
+ expect(
133
+ portablecapabilityset({
134
+ vocabulary,
135
+ probes: { dom: false, storage: true, network: true, worker: true } as capabilityprobe,
136
+ }),
137
+ ).toEqual([]);
138
+ });
139
+ });
140
+
141
+ describe("runtime shells", () => {
142
+ it("maps the filesystem onto the storage seam for node and bun runtimes", async () => {
143
+ const base = join("tests", "artifacts", "librarymodes", "storage");
144
+ await rm(base, { recursive: true, force: true });
145
+ const adapter = filesystemstorageadapter(
146
+ {
147
+ readfile: (path) => readFile(path, "utf8"),
148
+ writefile: (path, data) => writeFile(path, data, "utf8"),
149
+ mkdir: async (path) => {
150
+ await mkdir(path, { recursive: true });
151
+ },
152
+ join: (...parts) => join(...parts),
153
+ },
154
+ base,
155
+ );
156
+ expect(await adapter.get("config")).toBeUndefined();
157
+ await adapter.set("config", { provider: "local" });
158
+ expect(await adapter.get("config")).toMatchObject({ provider: "local" });
159
+ await adapter.set("audit", [{ id: "a1" }]);
160
+ expect(await adapter.get<Array<{ id: string }>>("audit")).toEqual([{ id: "a1" }]);
161
+ await rm(base, { recursive: true, force: true });
162
+ });
163
+
164
+ it("maps chrome storage and deno kv onto the same seam", async () => {
165
+ const area = new Map<string, unknown>();
166
+ const chrome = chromestorageadapter({
167
+ get: async (key) => area.get(key),
168
+ set: async (key, value) => {
169
+ area.set(key, value);
170
+ },
171
+ });
172
+ await chrome.set("session", { id: "s1" });
173
+ expect(await chrome.get("session")).toMatchObject({ id: "s1" });
174
+ const kv = new Map<string, unknown>();
175
+ const deno = denokvadapter({
176
+ get: async <T>(key: string) => kv.get(key) as T | undefined,
177
+ set: async <T>(key: string, value: T) => {
178
+ kv.set(key, value);
179
+ },
180
+ });
181
+ await deno.set("profile", { name: "work" });
182
+ expect(await deno.get("profile")).toMatchObject({ name: "work" });
183
+ const memory = memorystorageadapter();
184
+ await memory.set("plan", { id: "p1" });
185
+ expect(await memory.get("plan")).toMatchObject({ id: "p1" });
186
+ const seeded = memorystorageadapter(new Map([["seeded", 7]]));
187
+ expect(await seeded.get("seeded")).toBe(7);
188
+ });
189
+
190
+ it("drives the same session memory through every adapter so the profile store format stays one", async () => {
191
+ const store = new Map<string, unknown>();
192
+ const memory = new sessionmemory(
193
+ chromestorageadapter({
194
+ get: async (key) => store.get(key),
195
+ set: async (key, value) => {
196
+ store.set(key, value);
197
+ },
198
+ }),
199
+ );
200
+ await memory.setconfig({ provider: "local" } as never);
201
+ const filesystem = new sessionmemory(memorystorageadapter(store));
202
+ expect(await filesystem.getconfig()).toMatchObject({ provider: "local" });
203
+ });
204
+
205
+ it("provides the timer and fetch shells from the injected platform primitives", async () => {
206
+ const scheduled: Array<() => void> = [];
207
+ const timer = timershellof({
208
+ settimeout: (callback) => {
209
+ scheduled.push(callback);
210
+ return scheduled.length;
211
+ },
212
+ now: () => now,
213
+ });
214
+ const handle = timer.schedule(() => "ran", 50);
215
+ expect(handle).toBe(1);
216
+ expect(timer.now()).toBe(now);
217
+ (scheduled[0] as () => void)();
218
+ const fetches: string[] = [];
219
+ const fetchshell = fetchshellof({
220
+ fetch: async (input) => {
221
+ fetches.push(input);
222
+ return { ok: true };
223
+ },
224
+ });
225
+ await fetchshell.fetch("https://example.org");
226
+ expect(fetches).toEqual(["https://example.org"]);
227
+ });
228
+
229
+ it("composes one runtime adapter from its declaration, probes and injected primitives", () => {
230
+ const store = new Map<string, unknown>();
231
+ const adapter = runtimeadapterof({
232
+ runtime: "node",
233
+ storage: memorystorageadapter(store),
234
+ timer: {
235
+ settimeout: (callback) => {
236
+ callback();
237
+ return 0;
238
+ },
239
+ now: () => now,
240
+ },
241
+ fetch: { fetch: async (input) => input },
242
+ });
243
+ expect(adapter.declaration).toMatchObject({ runtime: "node", storage: "filesystem" });
244
+ expect(adapter.probes.dom).toBe(false);
245
+ expect(adapter.storage).toBeDefined();
246
+ expect(adapter.timer?.now()).toBe(now);
247
+ expect(adapter.fetch).toBeDefined();
248
+ expect(runtimeadapterof({ runtime: "browser" }).storage).toBeUndefined();
249
+ expect(runtimeadapterof({ runtime: "deno" }).declaration.storage).toBe("denokv");
250
+ });
251
+ });
@@ -0,0 +1,115 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import {
3
+ safariskeletonbuild,
4
+ safariskeletonprojectfiles,
5
+ safariskeletonpopoverof,
6
+ safarizipfile,
7
+ safaricentraldirectory,
8
+ safariendrecord,
9
+ } from "../crossbrowser.js";
10
+ import type { safariskeletoninput } from "../types.js";
11
+
12
+ const input: safariskeletoninput = {
13
+ version: "1.1.86",
14
+ bundleid: "com.wenathlan.devthink",
15
+ extensionpayload: [
16
+ { name: "manifest.json", bytes: new Uint8Array([0, 1, 2]) },
17
+ { name: "background.js", bytes: new Uint8Array([3, 4, 5]) },
18
+ ],
19
+ entitlements: ["com.apple.security.app-sandbox", "com.apple.security.network.client"],
20
+ };
21
+
22
+ describe("safariskeleton", () => {
23
+ it("generates the xcode project wrapper for the extension", () => {
24
+ const files = safariskeletonprojectfiles();
25
+ expect(files.length).toBeGreaterThan(0);
26
+ expect(files.some((file) => file.path.endsWith("project.pbxproj"))).toBe(true);
27
+ expect(files.some((file) => file.path.endsWith("Info.plist"))).toBe(true);
28
+ expect(files.some((file) => file.path.endsWith("AppDelegate.swift"))).toBe(true);
29
+ });
30
+
31
+ it("embeds the chromium build as the safari web extension payload", () => {
32
+ const output = safariskeletonbuild(input);
33
+ expect(output.archive.name).toBe("devthink-safari-1.1.86.zip");
34
+ expect(output.archive.bytes.length).toBeGreaterThan(100);
35
+ expect(output.projectfiles.length).toBeGreaterThan(0);
36
+ });
37
+
38
+ it("declares the app entitlements for extension distribution", () => {
39
+ const output = safariskeletonbuild(input);
40
+ expect(output.entitlements).toContain("com.apple.security.app-sandbox");
41
+ expect(output.entitlements).toContain("com.apple.security.network.client");
42
+ const entitlementsfile = safariskeletonprojectfiles().find((file) => file.path.endsWith(".entitlements"));
43
+ expect(entitlementsfile).toBeDefined();
44
+ expect(entitlementsfile?.text).toContain("com.apple.security.app-sandbox");
45
+ });
46
+
47
+ it("includes a minimal app shell that opens the extension", () => {
48
+ const output = safariskeletonbuild(input);
49
+ expect(output.appshell).toBe("Devthink/AppDelegate.swift");
50
+ const shell = safariskeletonprojectfiles().find((file) => file.path.endsWith("AppDelegate.swift"));
51
+ expect(shell?.text).toContain("SFSafariApplication");
52
+ });
53
+
54
+ it("renders the sidepanel surface as a popover equivalent", () => {
55
+ const popover = safariskeletonpopoverof();
56
+ expect(popover.path).toBe("sidepanel.html");
57
+ expect(popover.bounds.width).toBe(480);
58
+ expect(popover.bounds.height).toBe(720);
59
+ });
60
+
61
+ it("stamps the release version into the project files", () => {
62
+ const output = safariskeletonbuild(input);
63
+ const plist = output.projectfiles.find((file) => file.path.endsWith("Info.plist"));
64
+ expect(plist?.text).toContain("1.1.86");
65
+ });
66
+
67
+ it("stamps the bundle id into the project files", () => {
68
+ const output = safariskeletonbuild(input);
69
+ const plist = output.projectfiles.find((file) => file.path.endsWith("Info.plist"));
70
+ expect(plist?.text).toContain("com.wenathlan.devthink");
71
+ });
72
+
73
+ it("refuses an empty payload or an empty entitlements list", () => {
74
+ expect(() => safariskeletonbuild({ ...input, extensionpayload: [] })).toThrow();
75
+ expect(() => safariskeletonbuild({ ...input, entitlements: [] })).toThrow();
76
+ });
77
+
78
+ it("refuses a non semver version or an invalid bundle id", () => {
79
+ expect(() => safariskeletonbuild({ ...input, version: "not-a-version" })).toThrow();
80
+ expect(() => safariskeletonbuild({ ...input, bundleid: "" })).toThrow();
81
+ expect(() => safariskeletonbuild({ ...input, bundleid: "Invalid Bundle Id" })).toThrow();
82
+ });
83
+
84
+ it("writes the wrapper files and the payload entries as stored zip entries with readable offsets", () => {
85
+ const header = safarizipfile("Resources/manifest.json", Buffer.from("{}", "utf8"));
86
+ expect(header.readUInt32LE(0)).toBe(0x04034b50);
87
+ expect(header.readUInt16LE(8)).toBe(0);
88
+ const central = safaricentraldirectory([{ name: "Resources/manifest.json", size: 2, offset: 0 }]);
89
+ expect(central.bytes.readUInt32LE(0)).toBe(0x02014b50);
90
+ expect(central.bytes.readUInt32LE(20)).toBe(2);
91
+ expect(central.bytes.readUInt32LE(42)).toBe(0);
92
+ const end = safariendrecord(1, central.bytes.length, header.length);
93
+ expect(end.readUInt32LE(0)).toBe(0x06054b50);
94
+ expect(end.readUInt32LE(16)).toBe(header.length);
95
+ /* the assembled archive reads back through the standard structure: the end record points at the central directory and every central offset lands on a local file header signature */
96
+ const built = safariskeletonbuild({
97
+ version: "1.1.87",
98
+ bundleid: "devthink.wenathlan.safari",
99
+ extensionpayload: [{ name: "manifest.json", bytes: new Uint8Array([123, 125]) }],
100
+ entitlements: ["com.apple.security.app-sandbox"],
101
+ });
102
+ const archive = Buffer.from(built.archive.bytes);
103
+ const endoffset = archive.length - 22;
104
+ expect(archive.readUInt32LE(endoffset)).toBe(0x06054b50);
105
+ const centralstart = archive.readUInt32LE(endoffset + 16);
106
+ const entrycount = archive.readUInt16LE(endoffset + 10);
107
+ expect(archive.readUInt32LE(centralstart)).toBe(0x02014b50);
108
+ let cursor = centralstart;
109
+ for (let index = 0; index < entrycount; index += 1) {
110
+ const localoffset = archive.readUInt32LE(cursor + 42);
111
+ expect(archive.readUInt32LE(localoffset)).toBe(0x04034b50);
112
+ cursor += 46 + archive.readUInt16LE(cursor + 28);
113
+ }
114
+ });
115
+ });
@@ -0,0 +1,196 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import {
3
+ acceptrenderresult,
4
+ nonceof,
5
+ rendermessage,
6
+ renderprovenance,
7
+ sandboxrenderof,
8
+ stripscripts,
9
+ } from "../environments.js";
10
+
11
+ const now = 1_000;
12
+
13
+ describe("sandbox frame", () => {
14
+ it("strips scripts and event handlers from untrusted markup before render", () => {
15
+ expect(stripscripts("<p>ok</p><script>alert(1)</script>")).toBe("<p>ok</p>");
16
+ expect(stripscripts('<p onclick="alert(1)">ok</p>')).toBe("<p>ok</p>");
17
+ expect(stripscripts("<p onclick='alert(1)'>ok</p>")).toBe("<p>ok</p>");
18
+ expect(stripscripts('<div ONMOUSEOVER="x()">ok</div>')).toBe("<div>ok</div>");
19
+ expect(stripscripts('<a href="javascript:alert(1)">link</a>')).toBe('<a href="alert(1)">link</a>');
20
+ expect(stripscripts('<img src="x" onerror="alert(1)" alt="a">')).toBe('<img src="x" alt="a">');
21
+ expect(stripscripts("<script src='https://evil.example/x.js'></script><b>bold</b>")).toBe("<b>bold</b>");
22
+ expect(stripscripts("<script/>")).toBe("");
23
+ });
24
+
25
+ it("issues a per render nonce for every sandbox render message", () => {
26
+ const one = nonceof("render:1:1000");
27
+ const two = nonceof("render:2:1000");
28
+ expect(one).toMatch(/^[a-z0-9]{16}$/);
29
+ expect(two).toMatch(/^[a-z0-9]{16}$/);
30
+ expect(one).not.toBe(two);
31
+ expect(nonceof("render:1:1000")).toBe(one);
32
+ });
33
+
34
+ it("builds the sandbox render descriptor with its nonce, source origin and sanitized markup", () => {
35
+ const render = sandboxrenderof({
36
+ id: "r1",
37
+ markup: "<p>hello</p><script>alert(1)</script>",
38
+ sourceorigin: "https://example.com",
39
+ stepid: "s1",
40
+ now,
41
+ });
42
+ expect(render.nonce).toMatch(/^[a-z0-9]{16}$/);
43
+ expect(render.markup).toBe("<p>hello</p>");
44
+ expect(render.sourceorigin).toBe("https://example.com");
45
+ expect(render.stepid).toBe("s1");
46
+ expect(() =>
47
+ sandboxrenderof({ id: "r2", markup: " ", sourceorigin: "https://example.com", stepid: "s1", now }),
48
+ ).toThrow(/markup/i);
49
+ expect(() => sandboxrenderof({ id: "r3", markup: "<p/>", sourceorigin: " ", stepid: "s1", now })).toThrow(
50
+ /source origin/i,
51
+ );
52
+ expect(() =>
53
+ sandboxrenderof({ id: "r4", markup: "<p/>", sourceorigin: "https://example.com", stepid: " ", now }),
54
+ ).toThrow(/step/i);
55
+ });
56
+
57
+ it("posts the render through the devthinksandbox channel and accepts the nonce checked answer", () => {
58
+ const render = sandboxrenderof({
59
+ id: "r1",
60
+ markup: "<p>hello</p>",
61
+ sourceorigin: "https://example.com",
62
+ stepid: "s1",
63
+ now,
64
+ });
65
+ const message = rendermessage(render);
66
+ expect(message).toMatchObject({
67
+ channel: "devthinksandbox",
68
+ type: "render",
69
+ nonce: render.nonce,
70
+ markup: "<p>hello</p>",
71
+ });
72
+ const accepted = acceptrenderresult({
73
+ renders: [render],
74
+ message: {
75
+ channel: "devthinksandbox",
76
+ type: "renderresult",
77
+ nonce: render.nonce,
78
+ ok: true,
79
+ text: "<p>hello</p> inert text",
80
+ summary: "The frame answered.",
81
+ },
82
+ now: now + 10,
83
+ });
84
+ expect(accepted.accepted).toBe(true);
85
+ expect(accepted.result?.text).toBe("hello inert text");
86
+ expect(accepted.result?.nonce).toBe(render.nonce);
87
+ expect(accepted.result?.summary).toBe("The frame answered.");
88
+ });
89
+
90
+ it("refuses stale, off channel, unknown nonce and replayed sandbox messages so a render result never reenters the dom outside the frame", () => {
91
+ const render = sandboxrenderof({
92
+ id: "r1",
93
+ markup: "<p>hello</p>",
94
+ sourceorigin: "https://example.com",
95
+ stepid: "s1",
96
+ now,
97
+ });
98
+ expect(
99
+ acceptrenderresult({
100
+ renders: [render],
101
+ message: { channel: "other", type: "renderresult", nonce: render.nonce },
102
+ now: now + 10,
103
+ }).accepted,
104
+ ).toBe(false);
105
+ expect(
106
+ acceptrenderresult({
107
+ renders: [render],
108
+ message: { channel: "devthinksandbox", type: "other", nonce: render.nonce },
109
+ now: now + 10,
110
+ }).accepted,
111
+ ).toBe(false);
112
+ expect(
113
+ acceptrenderresult({
114
+ renders: [render],
115
+ message: { channel: "devthinksandbox", type: "renderresult", nonce: "unknown000000000" },
116
+ now: now + 10,
117
+ }).reason,
118
+ ).toMatch(/no nonce of a known unanswered render/i);
119
+ const early = acceptrenderresult({
120
+ renders: [render],
121
+ message: {
122
+ channel: "devthinksandbox",
123
+ type: "renderresult",
124
+ nonce: render.nonce,
125
+ ok: true,
126
+ text: "early",
127
+ summary: "early",
128
+ },
129
+ now: now - 10,
130
+ });
131
+ expect(early.accepted).toBe(false);
132
+ const plain = acceptrenderresult({
133
+ renders: [render],
134
+ message: {
135
+ channel: "devthinksandbox",
136
+ type: "renderresult",
137
+ nonce: render.nonce,
138
+ ok: true,
139
+ text: "<b>bold</b>",
140
+ summary: " ",
141
+ },
142
+ now: now + 10,
143
+ });
144
+ expect(plain.result?.text).toBe("bold");
145
+ expect(plain.result?.summary).toMatch(/sandbox frame rendered/i);
146
+ const replayed = acceptrenderresult({
147
+ renders: plain.renders,
148
+ message: {
149
+ channel: "devthinksandbox",
150
+ type: "renderresult",
151
+ nonce: render.nonce,
152
+ ok: false,
153
+ text: "",
154
+ summary: "The render failed.",
155
+ },
156
+ now: now + 10,
157
+ });
158
+ expect(replayed.accepted).toBe(false);
159
+ expect(replayed.reason).toMatch(/replayed or already answered/i);
160
+ const second = sandboxrenderof({
161
+ id: "r2",
162
+ markup: "<p>hello</p>",
163
+ sourceorigin: "https://example.com",
164
+ stepid: "s2",
165
+ now: now + 1,
166
+ });
167
+ const failed = acceptrenderresult({
168
+ renders: [second],
169
+ message: {
170
+ channel: "devthinksandbox",
171
+ type: "renderresult",
172
+ nonce: second.nonce,
173
+ ok: false,
174
+ text: "",
175
+ summary: "The render failed.",
176
+ },
177
+ now: now + 10,
178
+ });
179
+ expect(failed.result?.ok).toBe(false);
180
+ });
181
+
182
+ it("records the provenance of every sandbox render with its source origin and nonce", () => {
183
+ const render = sandboxrenderof({
184
+ id: "r1",
185
+ markup: "<p>hello</p>",
186
+ sourceorigin: "https://example.com",
187
+ stepid: "s1",
188
+ now,
189
+ });
190
+ expect(renderprovenance(render)).toEqual({
191
+ origin: "https://example.com",
192
+ stepid: "s1",
193
+ environment: "sandboxframe",
194
+ });
195
+ });
196
+ });
@@ -0,0 +1,105 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import {
3
+ sbomcomponentof,
4
+ sbomcoveragecheck,
5
+ sbominventory,
6
+ sbominventorytext,
7
+ sbomlicense,
8
+ sbomnameof,
9
+ sbomspecversion,
10
+ } from "../pack.js";
11
+
12
+ const valid = "a".repeat(64);
13
+
14
+ describe("sbom", () => {
15
+ it("builds the cyclonedx component record of one artifact with its hash and channels", () => {
16
+ const component = sbomcomponentof({
17
+ name: "devthink1.1.87.zip",
18
+ size: 2048,
19
+ checksum: valid,
20
+ channels: ["github", "chromium"],
21
+ });
22
+ expect(component.type).toBe("file");
23
+ expect(component.name).toBe("devthink1.1.87.zip");
24
+ expect(component.hashes).toEqual([{ alg: "SHA-256", content: valid }]);
25
+ expect(component.properties).toContainEqual({ name: "devthink:channel", value: "github,chromium" });
26
+ expect(component.properties).toContainEqual({ name: "devthink:size", value: "2048" });
27
+ expect(component.size).toBe(2048);
28
+ expect(() => sbomcomponentof({ name: "", size: 1, checksum: valid })).toThrow();
29
+ expect(() => sbomcomponentof({ name: "a.zip", size: 1, checksum: "short" })).toThrow();
30
+ });
31
+
32
+ it("covers every artifact of the release in one cyclonedx inventory", () => {
33
+ const artifacts = [
34
+ { name: "devthink1.1.87.zip", size: 10, checksum: "a".repeat(64) },
35
+ { name: "devthink-vscode-1.1.87.vsix", size: 20, checksum: "b".repeat(64) },
36
+ { name: "devthink-firefox-1.1.87.xpi", size: 30, checksum: "c".repeat(64) },
37
+ ];
38
+ const inventory = JSON.parse(sbominventorytext({ version: "1.1.87", artifacts })) as {
39
+ bomformat: string;
40
+ specversion: string;
41
+ components: Array<{ name?: string; hashes?: Array<{ alg?: string; content?: string }> }>;
42
+ metadata: { component: { name: string; version: string; licenses: Array<{ license: { id: string } }> } };
43
+ };
44
+ const document = sbominventory({ version: "1.1.87", artifacts });
45
+ expect(document.bomformat).toBe("CycloneDX");
46
+ expect(document.specversion).toBe(sbomspecversion);
47
+ expect(document.specversion).toBe("1.5");
48
+ expect(inventory.components).toHaveLength(3);
49
+ const metadata = inventory.metadata;
50
+ expect(metadata.component?.name ?? "").toBe("devthink");
51
+ expect(metadata.component?.version ?? "").toBe("1.1.87");
52
+ expect(metadata.component.licenses[0]?.license.id ?? "").toBe(sbomlicense);
53
+ /* every artifact appears once with its checksum */
54
+ const coverage = sbomcoveragecheck({ inventory, artifacts });
55
+ expect(coverage.ok).toBe(true);
56
+ expect(coverage.missing).toEqual([]);
57
+ });
58
+
59
+ it("renders byte identical output for the same artifact set because no timestamp enters the document", () => {
60
+ const artifacts = [{ name: "devthink-site-1.1.87.zip", size: 5, checksum: "d".repeat(64) }];
61
+ expect(sbominventorytext({ version: "1.1.87", artifacts })).toBe(
62
+ sbominventorytext({ version: "1.1.87", artifacts }),
63
+ );
64
+ expect(sbominventorytext({ version: "1.1.87", artifacts })).not.toContain("timestamp");
65
+ });
66
+
67
+ it("refuses an empty artifact set and a duplicate artifact name", () => {
68
+ expect(() => sbominventory({ version: "1.1.87", artifacts: [] })).toThrow();
69
+ expect(() =>
70
+ sbominventory({
71
+ version: "1.1.87",
72
+ artifacts: [
73
+ { name: "a.zip", size: 1, checksum: valid },
74
+ { name: "a.zip", size: 1, checksum: valid },
75
+ ],
76
+ }),
77
+ ).toThrow();
78
+ expect(() =>
79
+ sbominventory({ version: "not-a-version", artifacts: [{ name: "a.zip", size: 1, checksum: valid }] }),
80
+ ).toThrow();
81
+ });
82
+
83
+ it("reports the artifacts the inventory misses or records with another hash", () => {
84
+ const inventory = JSON.parse(
85
+ sbominventorytext({ version: "1.1.87", artifacts: [{ name: "a.zip", size: 1, checksum: "a".repeat(64) }] }),
86
+ );
87
+ const missing = sbomcoveragecheck({
88
+ inventory,
89
+ artifacts: [
90
+ { name: "a.zip", checksum: "a".repeat(64) },
91
+ { name: "b.zip", checksum: "b".repeat(64) },
92
+ ],
93
+ });
94
+ expect(missing.ok).toBe(false);
95
+ expect(missing.missing).toEqual(["b.zip"]);
96
+ const mismatched = sbomcoveragecheck({ inventory, artifacts: [{ name: "a.zip", checksum: "c".repeat(64) }] });
97
+ expect(mismatched.ok).toBe(false);
98
+ expect(mismatched.missing).toEqual(["a.zip"]);
99
+ });
100
+
101
+ it("names the sbom asset with the release version", () => {
102
+ expect(sbomnameof("1.1.87")).toBe("devthink-sbom-1.1.87.json");
103
+ expect(() => sbomnameof("not-a-version")).toThrow();
104
+ });
105
+ });