@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
package/export.ts ADDED
@@ -0,0 +1,889 @@
1
+ /** The export module of the 1.1.90 consolidation: every correlated variation of the data egress, the minimization and the portability logic interned in this one file, so the module family carries one surface without duplicate variations. */
2
+
3
+ /* ── Merged from exporttools.ts: the 1.1.90 consolidation interns the correlated exporttools logic here, so no variation of the same file lives beside another. ── */
4
+ import type {
5
+ cookiejarrecord,
6
+ datainventory,
7
+ dropimportsession,
8
+ exportallbundle,
9
+ exportdescriptor,
10
+ exportresult,
11
+ immutablelogentry,
12
+ importexportpayload,
13
+ localrule,
14
+ purgepolicy,
15
+ quarantineentry,
16
+ scanverdict,
17
+ sitenote,
18
+ storedrunlog,
19
+ syncsettings,
20
+ syncrecord,
21
+ telemetrypolicy,
22
+ } from "./types.js";
23
+ import { defaultmaskshapes, maskexport, maskmarker } from "./security.js";
24
+ import { exportchaingate, exportmaskgate, importexportgate } from "./policy.js";
25
+ import { verifylogchain } from "./security.js";
26
+
27
+ /**
28
+ * Exporttools logic of the 1.1.67 family.
29
+ * Runs, extractions and notes move to disk here: every export carries its descriptor with the format and the scope, the log chain verifies before any byte leaves the store, the maskinputs verdicts apply in every format, and an unmasked value refuses the export in full.
30
+ * The tools never widen an export: a refused export writes nothing, and no format ships a value the mask verdicts held back.
31
+ */
32
+
33
+ /** The export formats the tools render: csv for tables, json for structured consumers, log for the chain lines, jsonl for the json lines streams and markdown for the table documents. */
34
+ export const exportformats: readonly string[] = ["csv", "json", "log", "jsonl", "markdown"];
35
+
36
+ /** The export scopes the tools serve: the runs of a chain, the extractions of a run, the notes of a profile, beside the session, audit and extraction scopes the 1.1.80 exportdata command exports. */
37
+ export const exportscopes: readonly string[] = ["runs", "extractions", "notes", "session", "audit", "extraction"];
38
+
39
+ /** Builds and validates one export descriptor: the format and the scope stay the reviewed pair the command declared. */
40
+ export function exportdescriptorof(format: string, scope: string): exportdescriptor {
41
+ if (!exportformats.includes(format))
42
+ throw new Error(`The export format ${format} stays outside csv, json, log, jsonl and markdown.`);
43
+ if (!exportscopes.includes(scope))
44
+ throw new Error(`The export scope ${scope} stays outside runs, extractions, notes, session, audit and extraction.`);
45
+ return { format: format as exportdescriptor["format"], scope: scope as exportdescriptor["scope"] };
46
+ }
47
+
48
+ /** Escapes one csv field: the field quotes when it carries a comma, a quote or a newline, and a quote doubles inside. */
49
+ export function csvfield(value: string): string {
50
+ return /[",\n]/.test(value) ? `"${value.replace(/"/g, '""')}"` : value;
51
+ }
52
+
53
+ /** Renders rows as csv: the header carries the sorted field names and every row escapes its fields the csv way. */
54
+ export function csvof(rows: Array<Record<string, string>>): string {
55
+ if (rows.length === 0) return "";
56
+ const fields = [...new Set(rows.flatMap((row) => Object.keys(row)))].sort();
57
+ const header = fields.map(csvfield).join(",");
58
+ const body = rows.map((row) => fields.map((field) => csvfield(row[field] ?? "")).join(","));
59
+ return [header, ...body].join("\n");
60
+ }
61
+
62
+ /** Escapes one markdown table cell: the backslash escapes first so a value that already carries one never collides with the pipe escape, the pipe follows and a newline breaks the cell into a line break the table carries literally. */
63
+ export function markdownfield(value: string): string {
64
+ return value.replace(/\\/g, "\\\\").replace(/\|/g, "\\|").replace(/\r?\n/g, "<br>");
65
+ }
66
+
67
+ /** Renders rows as one markdown table: the header carries the sorted field names, the separator row fixes the column count and every row escapes its cells the markdown way. */
68
+ export function markdownof(rows: Array<Record<string, string>>): string {
69
+ if (rows.length === 0) return "";
70
+ const fields = [...new Set(rows.flatMap((row) => Object.keys(row)))].sort();
71
+ const header = `| ${fields.map(markdownfield).join(" | ")} |`;
72
+ const separator = `| ${fields.map(() => "---").join(" | ")} |`;
73
+ const body = rows.map((row) => `| ${fields.map((field) => markdownfield(row[field] ?? "")).join(" | ")} |`);
74
+ return [header, separator, ...body].join("\n");
75
+ }
76
+
77
+ /** Renders records as json lines: one compact json document per line, so a stream consumer reads one record at a time with no delimiter bookkeeping. */
78
+ export function jsonlinesof(records: Array<Record<string, unknown>>): string {
79
+ if (records.length === 0) return "";
80
+ return records.map((record) => JSON.stringify(record)).join("\n");
81
+ }
82
+
83
+ /** The secret field shapes an export refuses in full when their values carry no mask marker: the same shapes the importexport bundle refuses, so no format launders an unmasked store value. */
84
+ export const secretfieldshapes: readonly string[] = ["secret", "token", "password", "apikey", "authorization"];
85
+
86
+ /** Reads the secret named fields whose raw values carry no mask marker: every one of them refuses the export under any format, because masking at export time would launder an unmasked store value. */
87
+ export function unmaskedfieldsof(records: Array<Record<string, unknown>>): string[] {
88
+ const unmasked = new Set<string>();
89
+ for (const record of records) {
90
+ for (const [name, value] of Object.entries(record)) {
91
+ if (typeof value !== "string" || value.trim() === "") continue;
92
+ if (secretfieldshapes.some((shape) => name.toLowerCase().includes(shape)) && !value.includes(maskmarker))
93
+ unmasked.add(name);
94
+ }
95
+ }
96
+ return [...unmasked].sort();
97
+ }
98
+
99
+ /** Exports records in the descriptor format with the mask verdicts honored: the verdicts mask the fields they hold back in every format, a raw secret value refuses the export in full because masking at export time would launder an unmasked store, and a refused export writes nothing. */
100
+ export function exportrecords(input: {
101
+ descriptor: exportdescriptor;
102
+ records: Array<Record<string, unknown>>;
103
+ shapes: string[];
104
+ path?: string;
105
+ }): { result: exportresult; content: string } {
106
+ const unmasked = unmaskedfieldsof(input.records);
107
+ const maskgate = exportmaskgate({ unmasked });
108
+ if (!maskgate.allowed)
109
+ return {
110
+ result: {
111
+ descriptor: input.descriptor,
112
+ bytes: 0,
113
+ rows: 0,
114
+ ...(input.path !== undefined ? { path: input.path } : {}),
115
+ reason: maskgate.reason ?? "The export refused an unmasked value.",
116
+ },
117
+ content: "",
118
+ };
119
+ const masked = input.records.map((record) => maskexport(record, input.shapes) as Record<string, unknown>);
120
+ const stringrows = masked.map(
121
+ (record) =>
122
+ Object.fromEntries(
123
+ Object.entries(record).map(([name, value]) => [
124
+ name,
125
+ typeof value === "string" ? value : JSON.stringify(value),
126
+ ]),
127
+ ) as Record<string, string>,
128
+ );
129
+ if (input.descriptor.format === "csv") {
130
+ const content = csvof(stringrows);
131
+ return {
132
+ result: {
133
+ descriptor: input.descriptor,
134
+ bytes: content.length,
135
+ rows: stringrows.length,
136
+ ...(input.path !== undefined ? { path: input.path } : {}),
137
+ },
138
+ content,
139
+ };
140
+ }
141
+ if (input.descriptor.format === "json") {
142
+ const content = JSON.stringify(masked, null, 2);
143
+ return {
144
+ result: {
145
+ descriptor: input.descriptor,
146
+ bytes: content.length,
147
+ rows: masked.length,
148
+ ...(input.path !== undefined ? { path: input.path } : {}),
149
+ },
150
+ content,
151
+ };
152
+ }
153
+ if (input.descriptor.format === "jsonl") {
154
+ const content = jsonlinesof(masked);
155
+ return {
156
+ result: {
157
+ descriptor: input.descriptor,
158
+ bytes: content.length,
159
+ rows: masked.length,
160
+ ...(input.path !== undefined ? { path: input.path } : {}),
161
+ },
162
+ content,
163
+ };
164
+ }
165
+ if (input.descriptor.format === "markdown") {
166
+ const content = markdownof(stringrows);
167
+ return {
168
+ result: {
169
+ descriptor: input.descriptor,
170
+ bytes: content.length,
171
+ rows: stringrows.length,
172
+ ...(input.path !== undefined ? { path: input.path } : {}),
173
+ },
174
+ content,
175
+ };
176
+ }
177
+ const content = masked
178
+ .map((record) =>
179
+ Object.entries(record)
180
+ .map(([name, value]) => `${name}=${typeof value === "string" ? value : JSON.stringify(value)}`)
181
+ .join(" "),
182
+ )
183
+ .join("\n");
184
+ return {
185
+ result: {
186
+ descriptor: input.descriptor,
187
+ bytes: content.length,
188
+ rows: masked.length,
189
+ ...(input.path !== undefined ? { path: input.path } : {}),
190
+ },
191
+ content,
192
+ };
193
+ }
194
+
195
+ /** Reads the row view of one immutable log chain: one row per entry with its identity, kind, origin, step, summary and time. */
196
+ export function chainrows(entries: immutablelogentry[]): Array<Record<string, string>> {
197
+ return entries.map((entry) => ({
198
+ id: entry.id,
199
+ kind: entry.kind,
200
+ origin: entry.origin,
201
+ ...(entry.stepid !== undefined ? { stepid: entry.stepid } : {}),
202
+ summary: entry.summary,
203
+ at: String(entry.at),
204
+ }));
205
+ }
206
+
207
+ /** Exports one sealed run log in the descriptor format: the chain verifies before any byte leaves the store, so a broken chain refuses the export in full. */
208
+ export async function exportchain(input: {
209
+ descriptor: exportdescriptor;
210
+ log: storedrunlog;
211
+ shapes: string[];
212
+ path?: string;
213
+ }): Promise<{ result: exportresult; content: string }> {
214
+ const verification = await verifylogchain(input.log.entries);
215
+ const chaingate = exportchaingate({
216
+ chainvalid: verification.valid,
217
+ ...(verification.reason !== "" ? { reason: verification.reason } : {}),
218
+ });
219
+ if (!chaingate.allowed)
220
+ return {
221
+ result: {
222
+ descriptor: input.descriptor,
223
+ bytes: 0,
224
+ rows: 0,
225
+ ...(input.path !== undefined ? { path: input.path } : {}),
226
+ reason: chaingate.reason ?? "The export refused an unverified chain.",
227
+ },
228
+ content: "",
229
+ };
230
+ return exportrecords({
231
+ descriptor: input.descriptor,
232
+ records: chainrows(input.log.entries),
233
+ shapes: input.shapes,
234
+ ...(input.path !== undefined ? { path: input.path } : {}),
235
+ });
236
+ }
237
+
238
+ /** Reads the export row view of one site note: the identity and provenance beside the plain body or the sealed body exactly as the note stores it, so a sensitive note never ships its plaintext. */
239
+ export function noterow(note: sitenote): Record<string, string> {
240
+ return {
241
+ id: note.id,
242
+ origin: note.origin,
243
+ title: note.title,
244
+ author: note.author,
245
+ sensitive: String(note.sensitive),
246
+ ...(note.body !== undefined ? { body: note.body } : {}),
247
+ ...(note.sealedbody !== undefined ? { sealedbody: note.sealedbody } : {}),
248
+ updatedat: String(note.updatedat),
249
+ };
250
+ }
251
+
252
+ /** Exports notes in the descriptor format with the mask verdicts honored: the plain bodies and the sealed bodies ride the same masking gate as every other record. */
253
+ export function exportnotes(input: {
254
+ descriptor: exportdescriptor;
255
+ notes: sitenote[];
256
+ shapes: string[];
257
+ path?: string;
258
+ }): { result: exportresult; content: string } {
259
+ return exportrecords({
260
+ descriptor: input.descriptor,
261
+ records: input.notes.map(noterow),
262
+ shapes: input.shapes,
263
+ ...(input.path !== undefined ? { path: input.path } : {}),
264
+ });
265
+ }
266
+
267
+ /** Exports extraction rows in the descriptor format: the extracted values mask under the same verdicts, and an unmasked sensitive value refuses the export. */
268
+ export function exportextractions(input: {
269
+ descriptor: exportdescriptor;
270
+ rows: Array<Record<string, unknown>>;
271
+ shapes: string[];
272
+ path?: string;
273
+ }): { result: exportresult; content: string } {
274
+ return exportrecords({
275
+ descriptor: input.descriptor,
276
+ records: input.rows,
277
+ shapes: input.shapes,
278
+ ...(input.path !== undefined ? { path: input.path } : {}),
279
+ });
280
+ }
281
+
282
+ /* ── Merged from minimization.ts: the 1.1.90 consolidation interns the correlated minimization logic here, so no variation of the same file lives beside another. ── */
283
+ import { randomid } from "./memory.js";
284
+
285
+ /**
286
+ * Data minimization logic of the 1.1.79 family.
287
+ * Every correlated rule for the smallest possible data footprint lives in this one module: the localfirst pass that keeps extraction, aggregation and diffing on the device and strips the identity fields the reviewed plan never listed while the localrule fields never leave the device at all; the notelemetry posture that keeps every counter inside the local memory with the worker free of outbound usage calls; the optinsync consent that stays disabled until the user turns each data class on and stamps the consent per class; the encryptsync pass that derives the key from the user passphrase, encrypts every payload before the transport and stamps the format version while refusing a sync without the passphrase; the purgeonrequest pass that deletes stored data by scope on the user request, demands the typed confirmation for the full scope, preserves the immutable audit hashes and reports every deleted key; the exportall bundle that carries every stored record — runs, memory, captures, settings and provenance — into one portable file the streaming pass walks without a size cap; the cookiejar isolation that assigns one jar per task run, scopes every cookie read and write to the active jar and seals the jar at the run completion under the user expiry window; the cleanupafterrun pass that clears the task artifacts of a completed run while the artifacts the user flagged for retention stay; and the enforcequarantine rule that holds every download in the sandbox folder until the scanner verdict arrives and releases or deletes the file by the verdict alone.
288
+ * The module extends the reviewed families instead of duplicating them: the cookie entries ride the cookierecord shapes the cookie control family already writes, the quarantine verdicts reuse the scanverdict grammar and the quarantineentry record of the security family with its new lifecycle status, the exportall provenance composes the provlog entries of the pipeline family, and the purge receipts answer the audit trail the immutable log family already seals.
289
+ * Every impure move stays behind an injected seam — the key derivation, the payload encryption, the sync transport and the scanner hook resolve through functions the executor wires — while every cadence, retention, scope, timing and expiry stays the user's with no code default: an absent sync cadence keeps every pass off, an absent cleanup timing leaves every pass to the explicit user action, an absent jar expiry keeps every sealed jar until the user purges it, and no minimization path ever bypasses a review.
290
+ */
291
+
292
+ /** Key derivation seam: one user passphrase resolves to the derived key material; the extension executor wires the webcrypto derivation, tests wire plain fixtures. The passphrase itself never persists and never rides a payload. */
293
+ export type keyderive = (passphrase: string) => Promise<string>;
294
+
295
+ /** Payload encryption seam: one derived key with one plaintext payload resolves to the cipher text; the extension executor wires the webcrypto cipher, tests wire plain fixtures. */
296
+ export type payloadencrypt = (key: string, payload: string) => Promise<string>;
297
+
298
+ /** Sync transport seam: one encrypted payload with the classes it carries resolves to the delivery state; the extension executor wires the user configured sync backend, tests wire plain fixtures and counters. */
299
+ export type syncsend = (payload: string, classes: string[]) => Promise<{ delivered: boolean; endpoint?: string }>;
300
+
301
+ /** Scanner hook seam: one quarantined file resolves to the verdict of the user configured scanner; the extension executor wires the configured scan hook endpoint, tests wire plain fixtures. */
302
+ export type scannerhook = (entry: { path: string; reason: string; digest?: string }) => Promise<scanverdict>;
303
+
304
+ /** The minimization kinds of the 1.1.79 family, listed among the available capabilities of every proposal request. */
305
+ export const minimizationkinds: string[] = [
306
+ "localfirst",
307
+ "notelemetry",
308
+ "optinsync",
309
+ "encryptsync",
310
+ "purgeonrequest",
311
+ "exportall",
312
+ "cookiejar",
313
+ "cleanupartifacts",
314
+ "enforcequarantine",
315
+ ];
316
+
317
+ /** Reads the identity field names the localfirst pass strips when the reviewed plan never listed them: the common account, session and tracking shapes a page carries that an extraction keeps only when the review names the field. */
318
+ const identityfields: string[] = [
319
+ "email",
320
+ "phone",
321
+ "name",
322
+ "address",
323
+ "account",
324
+ "sessionid",
325
+ "userid",
326
+ "ip",
327
+ "geolocation",
328
+ "cookie",
329
+ "token",
330
+ ];
331
+
332
+ /**
333
+ * Processes one extraction through the local first pass: extraction, aggregation and diffing stay on the device, the identity fields strip unless the reviewed plan listed them, and the localrule fields of the origin strip always — they never leave the device for any review — while the report names every stripped and every local held field so the audit trail answers what the extraction stopped carrying.
334
+ */
335
+ export function localfirst(input: {
336
+ origin: string;
337
+ rows: Array<Record<string, string>>;
338
+ reviewedfields?: string[];
339
+ rules: localrule[];
340
+ now: number;
341
+ }): { rows: Array<Record<string, string>>; stripped: string[]; localheld: string[]; reason: string } {
342
+ if (input.origin.trim() === "")
343
+ throw new Error("The localfirst pass names its origin; an unnamespaced extraction answers no review.");
344
+ const reviewed = new Set(
345
+ (input.reviewedfields ?? []).map((field) => field.trim().toLowerCase()).filter((field) => field !== ""),
346
+ );
347
+ const localfields = new Set(
348
+ input.rules
349
+ .filter((rule) => rule.origin === input.origin || rule.origin === "*")
350
+ .flatMap((rule) => rule.fields.map((field) => field.trim().toLowerCase()).filter((field) => field !== "")),
351
+ );
352
+ const stripped = new Set<string>();
353
+ const localheld = new Set<string>();
354
+ const rows = input.rows.map((row) => {
355
+ const next: Record<string, string> = {};
356
+ for (const [field, value] of Object.entries(row)) {
357
+ const key = field.trim().toLowerCase();
358
+ if (localfields.has(key)) {
359
+ localheld.add(field);
360
+ continue;
361
+ }
362
+ if (
363
+ identityfields.some(
364
+ (identity) => key === identity || key.endsWith(`_${identity}`) || key.endsWith(`-${identity}`),
365
+ ) &&
366
+ !reviewed.has(key)
367
+ ) {
368
+ stripped.add(field);
369
+ continue;
370
+ }
371
+ next[field] = value;
372
+ }
373
+ return next;
374
+ });
375
+ return {
376
+ rows,
377
+ stripped: [...stripped],
378
+ localheld: [...localheld],
379
+ reason: `The localfirst pass kept ${rows.length} row${rows.length === 1 ? "" : "s"} on the device${stripped.size > 0 ? `, stripped ${stripped.size} identity field${stripped.size === 1 ? "" : "s"} the review never listed` : ""}${localheld.size > 0 ? ` and held ${localheld.size} local rule field${localheld.size === 1 ? "" : "s"} that never leave the device` : ""}.`,
380
+ };
381
+ }
382
+
383
+ /**
384
+ * Lists the local rule fields an outbound payload carries: the localgate and the protocol boundary refuse any payload that answers with one entry or more, because a field marked local never leaves the device for any transport.
385
+ */
386
+ export function localfieldsof(payload: Record<string, unknown>, rules: localrule[]): string[] {
387
+ const keys = Object.keys(payload).map((key) => key.trim().toLowerCase());
388
+ return rules
389
+ .flatMap((rule) => rule.fields.map((field) => field.trim().toLowerCase()))
390
+ .filter((field) => field !== "" && keys.includes(field));
391
+ }
392
+
393
+ /**
394
+ * Reads the telemetry policy fixed to off: the enabled literal stays false by construction — the type makes an on state unrepresentable — and every counter keeps living inside the local memory, so the popup names the posture and the tests hold the invariant.
395
+ */
396
+ export function fixedtelemetrypolicy(now: number): telemetrypolicy {
397
+ return { enabled: false, counters: "local", at: now };
398
+ }
399
+
400
+ /**
401
+ * Turns the sync opt in on per data class: sync stays disabled until the user lists every data class and enables each one, every enablement stamps the consent per class for the audit trail, and a class the listing never offered never enables — an unlisted class answers no consent.
402
+ */
403
+ export function optinsync(input: { current?: syncsettings; offered: string[]; enable: string[]; now: number }): {
404
+ settings: syncsettings;
405
+ consent: Array<{ dataclass: string; at: number }>;
406
+ } {
407
+ const offered = input.offered.map((item) => item.trim().toLowerCase()).filter((item) => item !== "");
408
+ if (offered.length === 0)
409
+ throw new Error("The sync opt in lists every data class before any enablement; an empty listing enables nothing.");
410
+ const enable = input.enable.map((item) => item.trim().toLowerCase()).filter((item) => item !== "");
411
+ const unknown = enable.filter((item) => !offered.includes(item));
412
+ if (unknown.length > 0)
413
+ throw new Error(
414
+ `The sync opt in never enables a class the listing did not offer: ${unknown.join(", ")} stay${unknown.length === 1 ? "s" : ""} off until the user lists them.`,
415
+ );
416
+ const consented = new Map((input.current?.consent ?? []).map((entry) => [entry.dataclass, entry.at]));
417
+ const enabled = new Set(input.current?.enabled ?? []);
418
+ for (const dataclass of enable) {
419
+ enabled.add(dataclass);
420
+ consented.set(dataclass, input.now);
421
+ }
422
+ return {
423
+ settings: {
424
+ enabled: [...enabled],
425
+ ...(input.current?.cadence !== undefined ? { cadence: input.current.cadence } : {}),
426
+ consent: [...consented.entries()].map(([dataclass, at]) => ({ dataclass, at })),
427
+ at: input.now,
428
+ },
429
+ consent: enable.map((dataclass) => ({ dataclass, at: input.now })),
430
+ };
431
+ }
432
+
433
+ /**
434
+ * Encrypts one sync payload before the transport: the key derives from the user passphrase through the derivation seam, the payload encrypts through the cipher seam, the format version tags the envelope, and a sync without the passphrase refuses — a passphrase the user never typed never defaults in code.
435
+ */
436
+ export async function encryptsync(input: {
437
+ payload: string;
438
+ passphrase?: string;
439
+ derive: keyderive;
440
+ encrypt: payloadencrypt;
441
+ }): Promise<{ cipher: string; formattag: string; reason: string }> {
442
+ const passphrase = input.passphrase ?? "";
443
+ if (passphrase.trim() === "")
444
+ throw new Error(
445
+ "The encryptsync pass needs the user passphrase; a sync without the passphrase never transports because the key never defaults in code.",
446
+ );
447
+ if (input.payload.length === 0)
448
+ throw new Error("The encryptsync pass encrypts a non-empty payload; an empty sync carries nothing to sync.");
449
+ const key = await input.derive(passphrase);
450
+ const cipher = await input.encrypt(key, input.payload);
451
+ const formattag = "devthink-sync-1";
452
+ return {
453
+ cipher,
454
+ formattag,
455
+ reason: `The encryptsync pass derived its key from the user passphrase, encrypted the ${input.payload.length} character payload and stamped the ${formattag} format version; the passphrase never rides the envelope.`,
456
+ };
457
+ }
458
+
459
+ /**
460
+ * Runs one sync pass through the seams: only the classes the user opted in ride the payload, the encryptsync pass encrypts before the transport seam fires, and the syncrecord carries the payload hash with the format tag — a plaintext or an unconsented class never reaches the transport.
461
+ */
462
+ export async function syncpass(input: {
463
+ payload: string;
464
+ classes: string[];
465
+ settings: syncsettings;
466
+ passphrase?: string;
467
+ derive: keyderive;
468
+ encrypt: payloadencrypt;
469
+ send: syncsend;
470
+ now: number;
471
+ }): Promise<{ record: syncrecord; delivered: boolean; reason: string }> {
472
+ const enabled = new Set(input.settings.enabled.map((item) => item.toLowerCase()));
473
+ const unconsented = input.classes.filter((item) => !enabled.has(item.toLowerCase()));
474
+ if (unconsented.length > 0)
475
+ throw new Error(
476
+ `The sync pass carries the classes ${unconsented.join(", ")} the user never opted in; an unconsented class never reaches the transport.`,
477
+ );
478
+ const encrypted = await encryptsync({
479
+ payload: input.payload,
480
+ ...(input.passphrase !== undefined ? { passphrase: input.passphrase } : {}),
481
+ derive: input.derive,
482
+ encrypt: input.encrypt,
483
+ });
484
+ const delivery = await input.send(encrypted.cipher, input.classes);
485
+ const record: syncrecord = {
486
+ id: randomid(),
487
+ classes: input.classes,
488
+ payloadhash: await shaof(encrypted.cipher),
489
+ formattag: encrypted.formattag,
490
+ encrypted: true,
491
+ syncedat: input.now,
492
+ };
493
+ return {
494
+ record,
495
+ delivered: delivery.delivered,
496
+ reason: `The sync pass encrypted the ${input.classes.join(", ")} payload, delivered it${delivery.endpoint !== undefined ? ` through ${delivery.endpoint}` : ""} and recorded the payload hash; every class rode with its own consent stamp.`,
497
+ };
498
+ }
499
+
500
+ /**
501
+ * Deletes stored data by scope on the user request: the purge answers the inventory the device holds, the full scope demands the typed confirmation phrase, the audit class never deletes because the immutable audit hashes survive every purge, and the report lists every deleted key for the audit trail.
502
+ */
503
+ export function purgeonrequest(input: {
504
+ policy: purgepolicy;
505
+ typed: string;
506
+ inventory: datainventory[];
507
+ now: number;
508
+ }): { deleted: string[]; kept: string[]; audithashespreserved: boolean; reason: string } {
509
+ const scope = input.policy.scope.map((item) => item.trim().toLowerCase()).filter((item) => item !== "");
510
+ if (scope.length === 0)
511
+ throw new Error("The purge request names its scope; a purge without a scope deletes nothing.");
512
+ const full =
513
+ scope.includes("all") ||
514
+ (scope.includes("runs") &&
515
+ scope.includes("memory") &&
516
+ scope.includes("captures") &&
517
+ scope.includes("settings") &&
518
+ scope.includes("provenance"));
519
+ if (full && input.typed.trim() !== input.policy.confirmation)
520
+ throw new Error("The full scope purge needs the typed confirmation phrase; a purge nobody typed out never runs.");
521
+ const deleted: string[] = [];
522
+ const kept: string[] = [];
523
+ for (const entry of input.inventory) {
524
+ if (entry.dataclass === "audit") {
525
+ kept.push(entry.key);
526
+ continue;
527
+ }
528
+ if (scope.includes(entry.dataclass) || full) deleted.push(entry.key);
529
+ else kept.push(entry.key);
530
+ }
531
+ return {
532
+ deleted,
533
+ kept,
534
+ audithashespreserved: true,
535
+ reason: `The purge deleted ${deleted.length} stored key${deleted.length === 1 ? "" : "s"} of the ${scope.join(", ")} scope${full ? " after the typed confirmation" : ""} — ${deleted.join(", ") || "none"} — while the audit hashes survived${kept.length > 0 ? ` and ${kept.length} key${kept.length === 1 ? "" : "s"} stayed` : ""}; every deleted key entered the audit trail.`,
536
+ };
537
+ }
538
+
539
+ /**
540
+ * Bundles every stored record into one portable file: the runs, the memory items, the captures, the settings and the provenance entries ride one exportall bundle the user explicitly asked for, the record count and the byte size name what left, and nothing here caps the size because a bundle the user asked for streams whole.
541
+ */
542
+ export function exportall(input: {
543
+ runs: string[];
544
+ memory: string[];
545
+ captures: string[];
546
+ settings: boolean;
547
+ provenance: string[];
548
+ now: number;
549
+ }): exportallbundle {
550
+ if (
551
+ input.runs.length === 0 &&
552
+ input.memory.length === 0 &&
553
+ input.captures.length === 0 &&
554
+ input.provenance.length === 0 &&
555
+ !input.settings
556
+ )
557
+ throw new Error("The exportall bundle carries the stored records; a device holding nothing exports nothing.");
558
+ const records =
559
+ input.runs.length +
560
+ input.memory.length +
561
+ input.captures.length +
562
+ input.provenance.length +
563
+ (input.settings ? 1 : 0);
564
+ const bytes = JSON.stringify({
565
+ runs: input.runs,
566
+ memory: input.memory,
567
+ captures: input.captures,
568
+ settings: input.settings,
569
+ provenance: input.provenance,
570
+ }).length;
571
+ return {
572
+ id: randomid(),
573
+ runs: input.runs,
574
+ memory: input.memory,
575
+ captures: input.captures,
576
+ settings: input.settings,
577
+ provenance: input.provenance,
578
+ records,
579
+ bytes,
580
+ at: input.now,
581
+ };
582
+ }
583
+
584
+ /**
585
+ * Streams one exportall bundle in chunks without a size cap: the chunk size stays the explicit choice of the caller because no code ceiling ever refuses a large bundle; the chunks concatenate back into the bundle payload byte for byte.
586
+ */
587
+ export function streambundle(input: { bundle: exportallbundle; chunksize: number }): string[] {
588
+ if (!Number.isInteger(input.chunksize) || input.chunksize <= 0)
589
+ throw new Error(
590
+ "The bundle streaming names its chunk size as a positive whole number of characters; the chunk size never defaults in code.",
591
+ );
592
+ const payload = JSON.stringify(input.bundle);
593
+ const chunks: string[] = [];
594
+ for (let offset = 0; offset < payload.length; offset += input.chunksize)
595
+ chunks.push(payload.slice(offset, offset + input.chunksize));
596
+ return chunks;
597
+ }
598
+
599
+ /**
600
+ * Assigns one cookie jar to a task run: the jar carries its id, the run it isolates and the cookie entries scoped inside it, so one task run never shares its cookie state with another; an empty jar at the run start holds nothing until a reviewed cookie step writes.
601
+ */
602
+ export function newjar(input: { runid: string; now: number; id?: string }): cookiejarrecord {
603
+ if (input.runid.trim() === "") throw new Error("The cookie jar names its run; an unnamespaced jar isolates no task.");
604
+ return {
605
+ jarid: input.id ?? randomid(),
606
+ runid: input.runid,
607
+ cookies: [],
608
+ sealed: false,
609
+ createdat: input.now,
610
+ updatedat: input.now,
611
+ };
612
+ }
613
+
614
+ /**
615
+ * Scopes one cookie read or write to the active jar: a write appends or replaces the entries inside the jar it belongs to — and a sealed jar refuses every write because a completed run never changes its cookie state — while a read answers exactly the entries the jar holds for the domain the step named.
616
+ */
617
+ export function jarscope(input: {
618
+ jar: cookiejarrecord;
619
+ operation: "read" | "write";
620
+ cookies?: Array<{ name: string; domain: string; value: string; path?: string; expiresat?: number }>;
621
+ domain?: string;
622
+ now: number;
623
+ }): {
624
+ jar: cookiejarrecord;
625
+ entries: Array<{ name: string; domain: string; value: string; path?: string; expiresat?: number }>;
626
+ reason: string;
627
+ } {
628
+ if (input.operation === "write") {
629
+ if (input.jar.sealed)
630
+ throw new Error(
631
+ `The jar ${input.jar.jarid} of the run ${input.jar.runid} sealed at the run completion; a sealed jar refuses every write.`,
632
+ );
633
+ const cookies = input.cookies ?? [];
634
+ if (cookies.length === 0)
635
+ throw new Error("The jar write carries its cookie entries; an empty write changes nothing.");
636
+ const merged = [...input.jar.cookies];
637
+ for (const cookie of cookies) {
638
+ const index = merged.findIndex((entry) => entry.name === cookie.name && entry.domain === cookie.domain);
639
+ if (index >= 0) merged[index] = cookie;
640
+ else merged.push(cookie);
641
+ }
642
+ return {
643
+ jar: { ...input.jar, cookies: merged, updatedat: input.now },
644
+ entries: cookies,
645
+ reason: `The jar write scoped ${cookies.length} cookie entr${cookies.length === 1 ? "y" : "ies"} inside the jar ${input.jar.jarid} of the run ${input.jar.runid}; no other run reads them.`,
646
+ };
647
+ }
648
+ const entries = input.jar.cookies.filter((entry) => input.domain === undefined || entry.domain === input.domain);
649
+ return {
650
+ jar: input.jar,
651
+ entries,
652
+ reason: `The jar read answered ${entries.length} cookie entr${entries.length === 1 ? "y" : "ies"} of the jar ${input.jar.jarid}${input.domain !== undefined ? ` for the domain ${input.domain}` : ""}; the read stays inside the active jar.`,
653
+ };
654
+ }
655
+
656
+ /**
657
+ * Seals the jar of a completed run: the seal stamps the completion time, the user configured expiry window marks when the sealed jar's entries expire for the automatic cleanup, and an absent window keeps the jar until the user purges it because the expiry never defaults in code.
658
+ */
659
+ export function sealjar(input: { jar: cookiejarrecord; now: number; expiry?: number }): cookiejarrecord {
660
+ if (input.jar.sealed) return { ...input.jar, updatedat: input.now };
661
+ if (input.expiry !== undefined && !(input.expiry > 0))
662
+ throw new Error(
663
+ "The jar expiry stays a positive number of milliseconds the user chose; the expiry never defaults in code.",
664
+ );
665
+ return {
666
+ ...input.jar,
667
+ sealed: true,
668
+ sealedat: input.now,
669
+ ...(input.expiry !== undefined ? { expiresat: input.now + input.expiry } : {}),
670
+ updatedat: input.now,
671
+ };
672
+ }
673
+
674
+ /**
675
+ * Clears the task artifacts of a completed run: the classes the cleanup schedule names leave while the artifacts the user flagged for retention stay, and an absent class list clears nothing because the cleanup stays the user's schedule with no code default.
676
+ */
677
+ export function cleanupafterrun(input: {
678
+ runid: string;
679
+ artifacts: Array<{ id: string; dataclass: string; retained?: boolean }>;
680
+ classes: string[];
681
+ now: number;
682
+ }): { cleared: string[]; kept: string[]; reason: string } {
683
+ if (input.runid.trim() === "")
684
+ throw new Error("The cleanup pass names its run; an unnamespaced cleanup clears nothing.");
685
+ const classes = input.classes.map((item) => item.trim().toLowerCase()).filter((item) => item !== "");
686
+ if (classes.includes("audit"))
687
+ throw new Error(
688
+ "The cleanup pass never deletes the audit history without the user consent; the immutable hashes survive every pass.",
689
+ );
690
+ const cleared: string[] = [];
691
+ const kept: string[] = [];
692
+ for (const artifact of input.artifacts) {
693
+ if (artifact.retained === true) {
694
+ kept.push(artifact.id);
695
+ continue;
696
+ }
697
+ if (classes.includes(artifact.dataclass.toLowerCase())) cleared.push(artifact.id);
698
+ else kept.push(artifact.id);
699
+ }
700
+ return {
701
+ cleared,
702
+ kept,
703
+ reason: `The cleanup pass cleared ${cleared.length} artifact${cleared.length === 1 ? "" : "s"} of the run ${input.runid} under the classes ${classes.join(", ") || "none"} while ${kept.length} stayed${kept.length > 0 ? " — the retained artifacts the user flagged never leave" : ""}.`,
704
+ };
705
+ }
706
+
707
+ /**
708
+ * Holds a download in the sandbox folder until the scanner verdict arrives and releases or deletes the file by the verdict alone: a clean verdict releases, a flagged verdict deletes, and a pending or error verdict holds — a file without a verdict never opens, and a hook failure never releases anything.
709
+ */
710
+ export function enforcequarantine(input: {
711
+ path: string;
712
+ reason: string;
713
+ scan?: scanverdict;
714
+ digest?: string;
715
+ now: number;
716
+ id?: string;
717
+ }): { entry: quarantineentry; action: "hold" | "release" | "delete"; reason: string } {
718
+ if (input.path.trim() === "")
719
+ throw new Error("The quarantine names its file path; a download without a path quarantines nowhere.");
720
+ const scan = input.scan ?? "pending";
721
+ const entry: quarantineentry = {
722
+ id: input.id ?? randomid(),
723
+ path: input.path,
724
+ reason: input.reason,
725
+ scan,
726
+ status: scan === "clean" ? "released" : scan === "flagged" ? "deleted" : "held",
727
+ ...(input.digest !== undefined && /^[0-9a-f]{64}$/.test(input.digest.trim().toLowerCase())
728
+ ? { digest: input.digest.trim().toLowerCase() }
729
+ : {}),
730
+ at: input.now,
731
+ updatedat: input.now,
732
+ };
733
+ if (scan === "clean")
734
+ return {
735
+ entry,
736
+ action: "release",
737
+ reason: `The scanner verdict read clean so the quarantined file ${input.path} releases from the sandbox folder to the reviewed download flow.`,
738
+ };
739
+ if (scan === "flagged")
740
+ return {
741
+ entry,
742
+ action: "delete",
743
+ reason: `The scanner verdict read flagged so the quarantined file ${input.path} deletes from the sandbox folder; a flagged download never reaches the downloads folder.`,
744
+ };
745
+ return {
746
+ entry,
747
+ action: "hold",
748
+ reason: `The scanner verdict reads ${scan} so the quarantined file ${input.path} stays held in the sandbox folder; a file without a clean verdict never opens.`,
749
+ };
750
+ }
751
+
752
+ /**
753
+ * Builds one data inventory entry of a stored family: the serialized size and the record count answer what the device holds, so the purge and the exportall bundle scope exactly over the inventory and never guess.
754
+ */
755
+ export function inventoryentry(input: { key: string; dataclass: string; value: unknown; now: number }): datainventory {
756
+ if (input.key.trim() === "")
757
+ throw new Error("The inventory entry names its storage key; an unnamespaced key answers no family.");
758
+ const serialized = JSON.stringify(input.value ?? null) ?? "null";
759
+ const records = Array.isArray(input.value)
760
+ ? input.value.length
761
+ : input.value === undefined || input.value === null
762
+ ? 0
763
+ : 1;
764
+ return { key: input.key, dataclass: input.dataclass, size: serialized.length, records, at: input.now };
765
+ }
766
+
767
+ /** Hashes one payload with the subtle crypto digest when available and falls back to the plain length mix the tests seed, because the payload hash names the sync and never decrypts anything. */
768
+ async function shaof(payload: string): Promise<string> {
769
+ const subtle = globalThis.crypto?.subtle;
770
+ if (subtle !== undefined) {
771
+ const digest = await subtle.digest("SHA-256", new TextEncoder().encode(payload));
772
+ return [...new Uint8Array(digest)].map((byte) => byte.toString(16).padStart(2, "0")).join("");
773
+ }
774
+ return `len-${payload.length}`;
775
+ }
776
+
777
+ /* ── Merged from portability.ts: the 1.1.90 consolidation interns the correlated portability logic here, so no variation of the same file lives beside another. ── */
778
+ /**
779
+ * Importexport and dropimport logic of the 1.1.65 family.
780
+ * Settings and data move between profiles here: the importexport payload carries the originprofiles, the siteprofiles, the site notes and the preferences of one profile while the secretvault values and the unmasked logs never enter any bundle under any flag; and the dropimport accepts the csv, json and workflow files the user drops on the optionspage and the dashboardpage with its file kind detection naming what it found.
781
+ * No bundle is ever forced: every import lands under the same review, and no export path ever ships a secret or an unmasked log value.
782
+ */
783
+
784
+ /** Builds one importexport payload: the settings and data bundle of one profile with the originprofiles, the siteprofiles, the notes and the preferences and the honest exclusion list of everything that stayed out. */
785
+ export function importexportpayloadof(input: {
786
+ profile: string;
787
+ originprofiles: Array<Record<string, unknown>>;
788
+ siteprofiles: Array<Record<string, unknown>>;
789
+ notes: Array<Record<string, unknown>>;
790
+ preferences: Record<string, unknown>;
791
+ at: number;
792
+ }): importexportpayload {
793
+ if (input.profile.trim() === "") throw new Error("The importexport payload needs its profile name.");
794
+ const secrets =
795
+ [...input.originprofiles, ...input.siteprofiles, ...input.notes, ...Object.values(input.preferences)].find(
796
+ (record) => secretcarrying(record),
797
+ ) !== undefined;
798
+ const gate = importexportgate({ containssecrets: secrets, unmaskedlogs: false });
799
+ if (!gate.allowed) throw new Error(gate.reason);
800
+ return {
801
+ version: 1,
802
+ kind: "settings",
803
+ profile: input.profile.trim(),
804
+ exportedat: input.at,
805
+ contents: {
806
+ originprofiles: input.originprofiles,
807
+ siteprofiles: input.siteprofiles,
808
+ notes: input.notes,
809
+ preferences: input.preferences,
810
+ },
811
+ exclusions: ["secretvault values", "unmasked logs"],
812
+ };
813
+ }
814
+
815
+ /** Detects whether one record carries a secret vault value by its key shapes so the export refuses before any bundle ships. */
816
+ function secretcarrying(record: unknown): boolean {
817
+ if (record === null || typeof record !== "object") return false;
818
+ const entries = Object.entries(record as Record<string, unknown>);
819
+ const secretkeys = ["secret", "token", "password", "apikey", "authorization"];
820
+ return entries.some(
821
+ ([key, value]) =>
822
+ secretkeys.some((shape) => key.toLowerCase().includes(shape)) && typeof value === "string" && value.trim() !== "",
823
+ );
824
+ }
825
+
826
+ /** Validates one importexport payload for import: a bundle that carries secretvault values or unmasked logs refuses under any flag, and the payload needs its profile and contents. */
827
+ export function importexportvalidate(payload: importexportpayload): { ok: boolean; reason: string } {
828
+ const records = [
829
+ ...payload.contents.originprofiles,
830
+ ...payload.contents.siteprofiles,
831
+ ...payload.contents.notes,
832
+ ...Object.values(payload.contents.preferences),
833
+ ];
834
+ const preferencessecrets = Object.entries(payload.contents.preferences).some(([key, value]) =>
835
+ secretcarrying({ [key]: value }),
836
+ );
837
+ const gate = importexportgate({
838
+ containssecrets: records.some((record) => secretcarrying(record)) || preferencessecrets,
839
+ unmaskedlogs: payload.contents.unmaskedlogs !== undefined,
840
+ });
841
+ if (!gate.allowed) return { ok: false, reason: gate.reason ?? "The importexport bundle refuses." };
842
+ if (payload.profile.trim() === "") return { ok: false, reason: "The importexport bundle needs its profile name." };
843
+ return {
844
+ ok: true,
845
+ reason: `The importexport bundle of the profile ${payload.profile} validates with ${payload.contents.originprofiles.length} origin profile${payload.contents.originprofiles.length === 1 ? "" : "s"}, ${payload.contents.siteprofiles.length} site profile${payload.contents.siteprofiles.length === 1 ? "" : "s"} and ${payload.contents.notes.length} note${payload.contents.notes.length === 1 ? "" : "s"}; ${payload.exclusions.join(" and ")} never enter any bundle.`,
846
+ };
847
+ }
848
+
849
+ /** Applies one importexport payload to the current preferences: the imported keys replace their current values while the secrets exclusion list stays untouched because no secret ever rides a bundle. */
850
+ export function applyimport(
851
+ payload: importexportpayload,
852
+ current: Record<string, unknown>,
853
+ ): { preferences: Record<string, unknown>; applied: string[] } {
854
+ const validation = importexportvalidate(payload);
855
+ if (!validation.ok) throw new Error(validation.reason);
856
+ const applied = Object.keys(payload.contents.preferences);
857
+ return { preferences: { ...current, ...payload.contents.preferences }, applied };
858
+ }
859
+
860
+ /** Detects the file kind of one dropped file: the extension names csv, json and workflow files while the head of the content confirms the json and workflow shapes. */
861
+ export function detectfilekind(filename: string, head: string): dropimportsession["kind"] | undefined {
862
+ const extension = filename.toLowerCase().split(".").pop() ?? "";
863
+ if (extension === "csv") return "csv";
864
+ if (extension === "json") {
865
+ const trimmed = head.trim();
866
+ if (trimmed.startsWith("{") || trimmed.startsWith("[")) return trimmed.includes('"steps"') ? "workflow" : "json";
867
+ return "json";
868
+ }
869
+ if (extension === "yaml" || extension === "yml") return "workflow";
870
+ return undefined;
871
+ }
872
+
873
+ /** Builds one dropimport session of a file dropped on the optionspage or the dashboardpage: the detected kind names what the import path takes while an unknown kind refuses the session. */
874
+ export function dropimportof(input: { filename: string; bytes: number; head: string; at: number }): dropimportsession {
875
+ if (input.filename.trim() === "") throw new Error("The dropimport session needs its filename.");
876
+ const kind = detectfilekind(input.filename, input.head);
877
+ if (kind === undefined)
878
+ throw new Error(
879
+ `The dropimport detects no csv, json or workflow kind in ${input.filename}; the import path refuses the file.`,
880
+ );
881
+ return {
882
+ id: `${input.filename}:${input.at}`,
883
+ filename: input.filename,
884
+ kind,
885
+ bytes: input.bytes,
886
+ accepted: true,
887
+ at: input.at,
888
+ };
889
+ }