@wenathlan/devthink 2.0.14 → 2.0.19

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 +60 -0
  2. package/{dist/bridge.d.ts → bridge.d.ts} +1 -1
  3. package/bridge.d.ts.map +1 -0
  4. package/bridge.js +1877 -0
  5. package/bun.d.ts.map +1 -0
  6. package/bun.js +52329 -0
  7. package/caps/background.json +712 -0
  8. package/caps/cli.json +360 -0
  9. package/caps/library.json +3611 -0
  10. package/caps/mcp.json +126 -0
  11. package/caps/pagebridge.json +377 -0
  12. package/caps/popup.json +51 -0
  13. package/caps/sidepanel.json +522 -0
  14. package/checksums.txt +142 -0
  15. package/cli.d.ts.map +1 -0
  16. package/{dist/cli.js → cli.js} +2 -2
  17. package/companion.d.ts.map +1 -0
  18. package/companion.js +389 -0
  19. package/crossbrowser.d.ts.map +1 -0
  20. package/crossbrowser.js +1191 -0
  21. package/dashdone.d.ts.map +1 -0
  22. package/dashdone.js +368 -0
  23. package/deno.d.ts.map +1 -0
  24. package/deno.js +52325 -0
  25. package/devthink.umd.js +52346 -0
  26. package/fixtures/bun.ts +7 -0
  27. package/fixtures/cjs.js +6 -0
  28. package/fixtures/deno.ts +6 -0
  29. package/fixtures/example-org-pagestate.json +22 -0
  30. package/fixtures/importers/automa-workflow.json +18 -0
  31. package/fixtures/importers/selenium-side.json +20 -0
  32. package/fixtures/importers/tabular-plan.csv +7 -0
  33. package/fixtures/importers/uivision-macro.json +10 -0
  34. package/fixtures/importers/v1-plan.json +13 -0
  35. package/fixtures/mcp-client.mjs +14 -0
  36. package/fixtures/pages/checkout.html +192 -0
  37. package/fixtures/pages/dashboard.html +110 -0
  38. package/fixtures/pages/feed.html +113 -0
  39. package/fixtures/pages/product-grid.html +150 -0
  40. package/fixtures/plans/form-inventory-plan.json +11 -0
  41. package/fixtures/plans/release-notes-plan.json +11 -0
  42. package/fixtures/recipes/agentscompete.json +11 -0
  43. package/fixtures/recipes/agentsforms.json +11 -0
  44. package/fixtures/recipes/agentsmonitor.json +11 -0
  45. package/fixtures/recipes/agentsreview.json +29 -0
  46. package/fixtures/recipes/agentswarm.json +11 -0
  47. package/fixtures/recipes/fillcalendar.json +12 -0
  48. package/fixtures/recipes/fillcheckout.json +13 -0
  49. package/fixtures/recipes/filldependentdropdowns.json +12 -0
  50. package/fixtures/recipes/fillfromcsv.json +13 -0
  51. package/fixtures/recipes/fillloginform.json +12 -0
  52. package/fixtures/recipes/fillmultistepwizard.json +13 -0
  53. package/fixtures/recipes/fillregistration.json +12 -0
  54. package/fixtures/recipes/fillsearchfilters.json +12 -0
  55. package/fixtures/recipes/filltypeahead.json +11 -0
  56. package/fixtures/recipes/monitoralerts.json +12 -0
  57. package/fixtures/recipes/monitoravailability.json +11 -0
  58. package/fixtures/recipes/monitornetworkquiet.json +12 -0
  59. package/fixtures/recipes/monitorpagechanges.json +11 -0
  60. package/fixtures/recipes/monitorpricechange.json +11 -0
  61. package/fixtures/recipes/monitorschedule.json +12 -0
  62. package/fixtures/recipes/scrapedynamicprices.json +13 -0
  63. package/fixtures/recipes/scrapeimagegallery.json +12 -0
  64. package/fixtures/recipes/scrapeinfinitefeed.json +13 -0
  65. package/fixtures/recipes/scrapenestedlists.json +12 -0
  66. package/fixtures/recipes/scrapeproductgrid.json +12 -0
  67. package/fixtures/recipes/scrapereviews.json +12 -0
  68. package/fixtures/recipes/scrapesearchresults.json +12 -0
  69. package/fixtures/recipes/scrapetablepagination.json +12 -0
  70. package/fixtures/recipes/testaccessibility.json +34 -0
  71. package/fixtures/recipes/testconsoleerrors.json +12 -0
  72. package/fixtures/recipes/testdeeplinks.json +12 -0
  73. package/fixtures/recipes/testformvalidation.json +13 -0
  74. package/fixtures/recipes/testlayoutshift.json +12 -0
  75. package/fixtures/recipes/testlinkcheck.json +12 -0
  76. package/fixtures/recipes/testloadtiming.json +12 -0
  77. package/fixtures/recipes/testvisualdiff.json +12 -0
  78. package/fixtures/workflows/changelog-digest-workflow.json +13 -0
  79. package/fixtures/workflows/form-survey-workflow.json +13 -0
  80. package/gallery.json +706 -0
  81. package/hardening.d.ts.map +1 -0
  82. package/hardening.js +188 -0
  83. package/headless.d.ts.map +1 -0
  84. package/headless.js +11522 -0
  85. package/http.d.ts.map +1 -0
  86. package/http.js +2165 -0
  87. package/index.cjs +54682 -0
  88. package/index.d.ts.map +1 -0
  89. package/index.js +52310 -0
  90. package/index.neutral.js +46343 -0
  91. package/{web/manifest.json → manifest.json} +1 -1
  92. package/mcp.d.ts.map +1 -0
  93. package/mcp.js +20515 -0
  94. package/memory.d.ts.map +1 -0
  95. package/{memory.ts → memory.js} +2310 -3834
  96. package/nativehost.template.json +10 -0
  97. package/node.cjs +54686 -0
  98. package/node.d.ts.map +1 -0
  99. package/pack.d.ts.map +1 -0
  100. package/pack.js +805 -0
  101. package/package.json +83 -176
  102. package/policy.d.ts.map +1 -0
  103. package/{policy.ts → policy.js} +4679 -5073
  104. package/progress.d.ts.map +1 -0
  105. package/progress.js +645 -0
  106. package/protocol.d.ts.map +1 -0
  107. package/protocol.js +8334 -0
  108. package/schemas/audit.schema.json +226 -0
  109. package/schemas/capability.schema.json +113 -0
  110. package/schemas/envelope.schema.json +162 -0
  111. package/schemas/memory.schema.json +51 -0
  112. package/schemas/observation.schema.json +113 -0
  113. package/schemas/plan.schema.json +208 -0
  114. package/schemas/progress.schema.json +126 -0
  115. package/schemas/proposal.schema.json +68 -0
  116. package/schemas/session.schema.json +82 -0
  117. package/schemas/tool.schema.json +94 -0
  118. package/server.d.ts.map +1 -0
  119. package/server.js +28877 -0
  120. package/umd-example.html +18 -0
  121. package/umd.d.ts.map +1 -0
  122. package/accounts.ts +0 -2087
  123. package/agent.ts +0 -2331
  124. package/antigravity.ts +0 -2139
  125. package/apifreeze.ts +0 -4146
  126. package/auth.ts +0 -642
  127. package/background.ts +0 -16837
  128. package/bridge.ts +0 -2080
  129. package/build.ts +0 -54
  130. package/bun.ts +0 -26
  131. package/capture.ts +0 -1614
  132. package/checksecrets.ts +0 -72
  133. package/cjs.ts +0 -9
  134. package/cli.ts +0 -3500
  135. package/commands.ts +0 -1101
  136. package/companion.ts +0 -450
  137. package/compatibility.ts +0 -37
  138. package/config.ts +0 -3137
  139. package/constants.ts +0 -943
  140. package/core.ts +0 -1473
  141. package/crossbrowser.ts +0 -1359
  142. package/dashdone.ts +0 -605
  143. package/data.ts +0 -943
  144. package/database.ts +0 -133
  145. package/debug.ts +0 -850
  146. package/deno.ts +0 -27
  147. package/devthink.ts +0 -707
  148. package/dist/accounts.d.ts +0 -409
  149. package/dist/agent.d.ts +0 -837
  150. package/dist/antigravity.d.ts +0 -380
  151. package/dist/apifreeze.d.ts +0 -113
  152. package/dist/auth.d.ts +0 -224
  153. package/dist/capture.d.ts +0 -595
  154. package/dist/cjs.d.ts +0 -6
  155. package/dist/commands.d.ts +0 -310
  156. package/dist/compatibility.d.ts +0 -17
  157. package/dist/config.d.ts +0 -1853
  158. package/dist/constants.d.ts +0 -637
  159. package/dist/core.d.ts +0 -361
  160. package/dist/data.d.ts +0 -330
  161. package/dist/database.d.ts +0 -24
  162. package/dist/debug.d.ts +0 -270
  163. package/dist/engine.d.ts +0 -74
  164. package/dist/environments.d.ts +0 -244
  165. package/dist/evidence.d.ts +0 -277
  166. package/dist/export.d.ts +0 -311
  167. package/dist/fingerprint.d.ts +0 -594
  168. package/dist/flow.d.ts +0 -300
  169. package/dist/gates.d.ts +0 -103
  170. package/dist/identity.d.ts +0 -51
  171. package/dist/ids.d.ts +0 -7
  172. package/dist/llm.d.ts +0 -652
  173. package/dist/models.d.ts +0 -1807
  174. package/dist/net.d.ts +0 -325
  175. package/dist/neutral.d.ts +0 -41
  176. package/dist/oauth.d.ts +0 -703
  177. package/dist/page.d.ts +0 -1166
  178. package/dist/perf.d.ts +0 -481
  179. package/dist/plan.d.ts +0 -134
  180. package/dist/project.d.ts +0 -395
  181. package/dist/providers.d.ts +0 -37
  182. package/dist/quota.d.ts +0 -453
  183. package/dist/recovery.d.ts +0 -621
  184. package/dist/request.d.ts +0 -1492
  185. package/dist/run.d.ts +0 -779
  186. package/dist/runtime.d.ts +0 -212
  187. package/dist/search.d.ts +0 -375
  188. package/dist/security.d.ts +0 -534
  189. package/dist/serve.d.ts +0 -136
  190. package/dist/session.d.ts +0 -296
  191. package/dist/storage.d.ts +0 -53
  192. package/dist/streaming.d.ts +0 -1119
  193. package/dist/swarm.d.ts +0 -328
  194. package/dist/tools.d.ts +0 -170
  195. package/dist/types.d.ts +0 -8357
  196. package/dist/utils.d.ts +0 -82
  197. package/dist/version.d.ts +0 -7
  198. package/dist/versionregistry.d.ts +0 -406
  199. package/dist/views.d.ts +0 -579
  200. package/dist/workbenchsession.d.ts +0 -69
  201. package/dist/workflow.d.ts +0 -649
  202. package/docs/readme.md +0 -92
  203. package/docs/schemas/antigravity.json +0 -192
  204. package/engine.ts +0 -1524
  205. package/environments.ts +0 -1005
  206. package/evidence.ts +0 -643
  207. package/export.ts +0 -889
  208. package/fingerprint.ts +0 -1458
  209. package/flow.ts +0 -1190
  210. package/gates.ts +0 -307
  211. package/gateway.ts +0 -34
  212. package/hardening.ts +0 -191
  213. package/headless.ts +0 -369
  214. package/http.ts +0 -2110
  215. package/identity.ts +0 -246
  216. package/ids.ts +0 -26
  217. package/index.ts +0 -369
  218. package/llm.ts +0 -2682
  219. package/mcp.ts +0 -1835
  220. package/models.ts +0 -3805
  221. package/modes.ts +0 -43
  222. package/net.ts +0 -1214
  223. package/neutral.ts +0 -362
  224. package/node.ts +0 -26
  225. package/oauth.ts +0 -2216
  226. package/offscreen.ts +0 -170
  227. package/pack.ts +0 -956
  228. package/page.ts +0 -7824
  229. package/pagebridge.ts +0 -1116
  230. package/perf.ts +0 -957
  231. package/plan.ts +0 -672
  232. package/plugin.ts +0 -76
  233. package/prisma.config.ts +0 -27
  234. package/progress.ts +0 -1308
  235. package/project.ts +0 -1291
  236. package/protocol.ts +0 -4187
  237. package/providers.ts +0 -246
  238. package/quota.ts +0 -1887
  239. package/recovery.ts +0 -2433
  240. package/request.ts +0 -4482
  241. package/run.ts +0 -2135
  242. package/runtime.ts +0 -526
  243. package/search.ts +0 -1760
  244. package/security.ts +0 -1673
  245. package/serve.ts +0 -419
  246. package/server.ts +0 -5538
  247. package/session.ts +0 -1162
  248. package/storage.ts +0 -182
  249. package/streaming.ts +0 -4101
  250. package/swarm.ts +0 -884
  251. package/sync.ts +0 -56
  252. package/tests/agentcert.test.ts +0 -128
  253. package/tests/agentfleet.test.ts +0 -214
  254. package/tests/agentreview.test.ts +0 -241
  255. package/tests/agentstream.test.ts +0 -335
  256. package/tests/agentwork.test.ts +0 -356
  257. package/tests/apifreeze.test.ts +0 -290
  258. package/tests/apimap.test.ts +0 -112
  259. package/tests/approvalgate.test.ts +0 -175
  260. package/tests/artifactmanifest.test.ts +0 -204
  261. package/tests/attentionfeed.test.ts +0 -163
  262. package/tests/backgroundruns.test.ts +0 -100
  263. package/tests/batchscheduling.test.ts +0 -131
  264. package/tests/blackboard.test.ts +0 -238
  265. package/tests/bridge.test.ts +0 -1088
  266. package/tests/browsercoverage.test.ts +0 -134
  267. package/tests/browserpolyfills.test.ts +0 -124
  268. package/tests/build.mjs +0 -1260
  269. package/tests/clientauth.test.ts +0 -223
  270. package/tests/clitools.test.ts +0 -578
  271. package/tests/confirmgates.test.ts +0 -190
  272. package/tests/containerpack.test.ts +0 -143
  273. package/tests/controlflow.test.ts +0 -2120
  274. package/tests/coordination.test.ts +0 -345
  275. package/tests/copyscreen.test.ts +0 -139
  276. package/tests/core.test.ts +0 -417
  277. package/tests/costcert.test.ts +0 -95
  278. package/tests/crossbrowser.test.ts +0 -115
  279. package/tests/cspaudit.test.ts +0 -79
  280. package/tests/dashdone.test.ts +0 -615
  281. package/tests/datagrid.test.ts +0 -137
  282. package/tests/doccheck.test.ts +0 -107
  283. package/tests/dombatch.test.ts +0 -109
  284. package/tests/emulation.test.ts +0 -282
  285. package/tests/environments.test.ts +0 -259
  286. package/tests/evidenceviews.test.ts +0 -88
  287. package/tests/exporttools.test.ts +0 -185
  288. package/tests/firefoxprep.test.ts +0 -134
  289. package/tests/flowlibrary.test.ts +0 -242
  290. package/tests/flowrun.test.ts +0 -303
  291. package/tests/forensics.test.ts +0 -439
  292. package/tests/gateway.test.ts +0 -939
  293. package/tests/headlesslib.test.ts +0 -171
  294. package/tests/httpstream.test.ts +0 -277
  295. package/tests/immutablelog.test.ts +0 -157
  296. package/tests/inboundguard.test.ts +0 -177
  297. package/tests/lazyload.test.ts +0 -72
  298. package/tests/librarymodes.test.ts +0 -623
  299. package/tests/llm.test.ts +0 -696
  300. package/tests/manifest.test.ts +0 -226
  301. package/tests/maskinputs.test.ts +0 -150
  302. package/tests/matrixverify.test.ts +0 -111
  303. package/tests/mavenpack.test.ts +0 -75
  304. package/tests/mcpserver.test.ts +0 -704
  305. package/tests/memory.test.ts +0 -4155
  306. package/tests/memorycare.test.ts +0 -230
  307. package/tests/migrateplan.test.ts +0 -349
  308. package/tests/minimization.test.ts +0 -414
  309. package/tests/modelroute.test.ts +0 -112
  310. package/tests/multiagent.test.ts +0 -440
  311. package/tests/nativebridge.test.ts +0 -807
  312. package/tests/navigation.test.ts +0 -284
  313. package/tests/nugetpack.test.ts +0 -83
  314. package/tests/orchestration.test.ts +0 -449
  315. package/tests/originpolicy.test.ts +0 -405
  316. package/tests/outputcompare.test.ts +0 -97
  317. package/tests/package.test.ts +0 -91
  318. package/tests/pagebridge.test.ts +0 -5909
  319. package/tests/pentest.test.ts +0 -75
  320. package/tests/perfrecords.test.ts +0 -257
  321. package/tests/permdiff.test.ts +0 -85
  322. package/tests/phishguard.test.ts +0 -128
  323. package/tests/pickerviews.test.ts +0 -155
  324. package/tests/pipelines.test.ts +0 -381
  325. package/tests/planlint.test.ts +0 -240
  326. package/tests/planreview.test.ts +0 -383
  327. package/tests/platformtargets.test.ts +0 -56
  328. package/tests/policy.test.ts +0 -8284
  329. package/tests/portability.test.ts +0 -96
  330. package/tests/progress.test.ts +0 -1036
  331. package/tests/promptlibrary.test.ts +0 -116
  332. package/tests/protocol.test.ts +0 -4793
  333. package/tests/publishingpipeline.test.ts +0 -314
  334. package/tests/quickactions.test.ts +0 -164
  335. package/tests/readiness.test.ts +0 -78
  336. package/tests/recipes.test.ts +0 -163
  337. package/tests/redactshots.test.ts +0 -216
  338. package/tests/registernvidiakeys.ts +0 -51
  339. package/tests/relayserve.test.ts +0 -304
  340. package/tests/releasenotes.test.ts +0 -276
  341. package/tests/resilience.test.ts +0 -467
  342. package/tests/resourceaware.test.ts +0 -187
  343. package/tests/runbudget.test.ts +0 -125
  344. package/tests/runhistory.test.ts +0 -299
  345. package/tests/runreplay.test.ts +0 -113
  346. package/tests/runresume.test.ts +0 -184
  347. package/tests/runstate.test.ts +0 -288
  348. package/tests/runtimeadapters.test.ts +0 -251
  349. package/tests/safariskeleton.test.ts +0 -115
  350. package/tests/sandboxframe.test.ts +0 -196
  351. package/tests/sbom.test.ts +0 -105
  352. package/tests/secretvault.test.ts +0 -188
  353. package/tests/selcache.test.ts +0 -111
  354. package/tests/sessioninterface.test.ts +0 -791
  355. package/tests/sessions.test.ts +0 -591
  356. package/tests/siteprefs.test.ts +0 -228
  357. package/tests/snapshotdelta.test.ts +0 -165
  358. package/tests/statusviews.test.ts +0 -237
  359. package/tests/stepmeter.test.ts +0 -200
  360. package/tests/streamparse.test.ts +0 -163
  361. package/tests/stylepruning.test.ts +0 -90
  362. package/tests/surfaces.test.ts +0 -282
  363. package/tests/sweep.test.ts +0 -78
  364. package/tests/syncbridge.test.ts +0 -164
  365. package/tests/taskqueue.test.ts +0 -206
  366. package/tests/toolcalls.test.ts +0 -336
  367. package/tests/toolcatalog.test.ts +0 -201
  368. package/tests/torture-agent.test.ts +0 -706
  369. package/tests/torture-auth.test.ts +0 -461
  370. package/tests/torture-commands.test.ts +0 -895
  371. package/tests/torture-data.test.ts +0 -923
  372. package/tests/torture-environments.test.ts +0 -285
  373. package/tests/torture-export.test.ts +0 -263
  374. package/tests/torture-http.test.ts +0 -1379
  375. package/tests/torture-llm.test.ts +0 -808
  376. package/tests/torture-memory.test.ts +0 -1084
  377. package/tests/torture-plan.test.ts +0 -829
  378. package/tests/torture-run.test.ts +0 -626
  379. package/tests/torture-session.test.ts +0 -928
  380. package/tests/torture-swarm.test.ts +0 -1245
  381. package/tests/torture-tools.test.ts +0 -964
  382. package/tests/torture-workflow.test.ts +0 -2354
  383. package/tests/tourviews.test.ts +0 -52
  384. package/tests/transparency.test.ts +0 -147
  385. package/tests/trigger.test.ts +0 -941
  386. package/tests/v1sunset.test.ts +0 -428
  387. package/tests/virtlist.test.ts +0 -94
  388. package/tests/vision.test.ts +0 -540
  389. package/tests/vsixpack.test.ts +0 -145
  390. package/tests/webapi.test.ts +0 -523
  391. package/tests/workflow.test.ts +0 -1785
  392. package/tests/workfloweditor.test.ts +0 -759
  393. package/tests/xpipack.test.ts +0 -128
  394. package/tools.ts +0 -734
  395. package/types.ts +0 -9908
  396. package/umd.ts +0 -9
  397. package/utils.ts +0 -249
  398. package/version.ts +0 -8
  399. package/versionregistry.ts +0 -1310
  400. package/views.ts +0 -2153
  401. package/vitest.config.ts +0 -12
  402. package/web/App.tsx +0 -78
  403. package/web/README.md +0 -47
  404. package/web/capacitor.config.ts +0 -46
  405. package/web/const.ts +0 -19
  406. package/web/control.shell.tsx +0 -66
  407. package/web/dashboardpage.ts +0 -554
  408. package/web/db.ts +0 -493
  409. package/web/design.html +0 -541
  410. package/web/gatewayview/Gateway.tsx +0 -345
  411. package/web/gatewayview/config.ts +0 -1042
  412. package/web/gatewayview/definition.ts +0 -174
  413. package/web/icons.ts +0 -15
  414. package/web/optionspage.ts +0 -691
  415. package/web/popup.ts +0 -1651
  416. package/web/schema.prisma +0 -176
  417. package/web/sidepanel.ts +0 -7941
  418. package/web/sitemanifest.json +0 -13
  419. package/web/terminal-ui.tsx +0 -622
  420. package/web/transparencypage.ts +0 -541
  421. package/web/vite.config.ts +0 -39
  422. package/web/workspace.ts +0 -23
  423. package/workbenchmemory.ts +0 -104
  424. package/workbenchsession.ts +0 -304
  425. package/workflow.ts +0 -4495
  426. /package/{dist/bun.d.ts → bun.d.ts} +0 -0
  427. /package/{dist/cli.d.ts → cli.d.ts} +0 -0
  428. /package/{dist/companion.d.ts → companion.d.ts} +0 -0
  429. /package/{dist/crossbrowser.d.ts → crossbrowser.d.ts} +0 -0
  430. /package/{dist/dashdone.d.ts → dashdone.d.ts} +0 -0
  431. /package/{dist/deno.d.ts → deno.d.ts} +0 -0
  432. /package/{dist/hardening.d.ts → hardening.d.ts} +0 -0
  433. /package/{dist/headless.d.ts → headless.d.ts} +0 -0
  434. /package/{dist/http.d.ts → http.d.ts} +0 -0
  435. /package/{dist/index.d.ts → index.d.ts} +0 -0
  436. /package/{dist/mcp.d.ts → mcp.d.ts} +0 -0
  437. /package/{dist/memory.d.ts → memory.d.ts} +0 -0
  438. /package/{dist/node.d.ts → node.d.ts} +0 -0
  439. /package/{dist/pack.d.ts → pack.d.ts} +0 -0
  440. /package/{dist/policy.d.ts → policy.d.ts} +0 -0
  441. /package/{dist/progress.d.ts → progress.d.ts} +0 -0
  442. /package/{dist/protocol.d.ts → protocol.d.ts} +0 -0
  443. /package/{dist/server.d.ts → server.d.ts} +0 -0
  444. /package/{dist/umd.d.ts → umd.d.ts} +0 -0
package/security.ts DELETED
@@ -1,1673 +0,0 @@
1
- /**
2
- * The security module of the 1.1.90 consolidation: every correlated variation of the trust boundary logic interned in this one file, so the module family carries one surface without duplicate variations.
3
- * The correlation is the trust boundary cluster: originpolicy holds the origin consent posture (the denydefault refusal, the exact origin automation allowlist with no wildcard expansion, the per site originprofiles, the consent windows bound in time, the mid run revocation and the sensitive class classification); inboundguard holds the entry gate of the background (the schemastrict command validation, the origincheck verdicts, the connectallow sender list that ships empty and the ratelimit buckets that defer past bound commands); maskinputs holds the field shape recognizer that keeps typed, form and stored values out of every log, observation and export behind the redaction marker; immutablelog holds the append only hash chained run log with its completion seal and its read time tamper detection; phishguard holds the lookalike watch that blocks credential steps on origins resembling granted ones under the user threshold; secretvault holds the vault seam (values enter through put and leave at the last possible moment before a credential field, records carry the digest only, and the leak scan refuses plaintext secrets in plans); and transparency holds the grant grammar the transparency page renders (the grant rows, the consent window history, the connectallow list and the permdiff of installed updates).
4
- * No window, threshold, shape list, bucket bound or retention is ever hardcoded anywhere in the family: every bound stays the user's choice, no security decision ever widens past the human review, and no secret value ever reaches a storage writer, a log entry, a memory record or an export path.
5
- */
6
-
7
- /* ── Merged from originpolicy.ts ── */
8
-
9
- import type {
10
- actionkind,
11
- automationallowlistentry,
12
- classconsent,
13
- consentwindow,
14
- consentscope,
15
- deniedevidence,
16
- originprofile,
17
- revokerunevent,
18
- sensitiveclass,
19
- toolstep,
20
- } from "./types.js";
21
- import { randomid } from "./memory.js";
22
-
23
- /**
24
- * Origin consent policy logic of the 1.1.61 family.
25
- * The trust boundary rules live here: the denydefault posture that refuses every ungranted origin, the per origin automation allowlist with exact origin matching and no wildcard expansion, the per site originprofiles that grant and deny single action kinds, the consentwindows that bind every grant in time with a named boundary, the mid run revokerun that halts the pending step and every queued step as a terminal session event, and the sensitive class classification that routes payment, credential, delete and publish steps through one fresh consent prompt per class per origin.
26
- * No duration, boundary, kind list or class list is ever hardcoded: every window, profile and consent stays the user's choice, the active tab grant counts as exactly one explicit single origin grant, and no security decision ever bypasses the human review.
27
- */
28
-
29
- /** The documented posture of the 1.1.61 family: every origin the user never granted stays refused by default. */
30
- export const denydefaultposture = "denydefault" as const;
31
-
32
- /** Reads whether two origins match exactly: the allowlist binds every grant to one exact origin with no wildcard expansion, so a star, an empty segment or a scheme wildcard never covers any origin. */
33
- export function exactorigin(origin: string, entry: string): boolean {
34
- return origin.trim() !== "" && origin === entry;
35
- }
36
-
37
- /** Refuses wildcard patterns as allowlist entries before any matching runs: no wildcard expansion exists because every grant binds to one exact origin. */
38
- export function wildcardentry(entry: string): boolean {
39
- return (
40
- entry.includes("*") ||
41
- entry.includes("://*.") ||
42
- entry.trim() === "" ||
43
- entry.trim() === "https://" ||
44
- entry.trim() === "http://"
45
- );
46
- }
47
-
48
- /** Checks one origin against the per origin automation allowlist under the denydefault posture: an origin absent from the allowlist stays refused, the active tab origin counts as exactly one explicit single origin grant, and no wildcard expansion ever widens an entry. */
49
- export function allowlistcheck(input: {
50
- origin: string;
51
- allowlist: automationallowlistentry[];
52
- profileid?: string;
53
- sessionorigin?: string;
54
- }): { allowed: boolean; reason: string } {
55
- if (input.origin.trim() === "") return { allowed: false, reason: "The step needs the exact origin it targets." };
56
- for (const entry of input.allowlist) {
57
- if (wildcardentry(entry.origin))
58
- return {
59
- allowed: false,
60
- reason: `The allowlist entry ${entry.origin} carries a wildcard; every grant binds to one exact origin with no wildcard expansion.`,
61
- };
62
- }
63
- const scoped =
64
- input.profileid === undefined
65
- ? input.allowlist
66
- : input.allowlist.filter((entry) => entry.profileid === input.profileid);
67
- const granted = scoped.some((entry) => exactorigin(input.origin, entry.origin));
68
- if (granted)
69
- return {
70
- allowed: true,
71
- reason: `The origin ${input.origin} sits inside the automation allowlist the user granted.`,
72
- };
73
- if (input.sessionorigin !== undefined && exactorigin(input.origin, input.sessionorigin))
74
- return {
75
- allowed: true,
76
- reason: `The active tab grant covers ${input.origin} as exactly one explicit single origin grant.`,
77
- };
78
- return {
79
- allowed: false,
80
- reason: `The denydefault posture refuses ${input.origin} because the origin sits absent from the automation allowlist; grant the origin first.`,
81
- };
82
- }
83
-
84
- /** Builds one per site origin profile with its kind grants and denials; the profile consults before every sensitive kind on its origin. */
85
- export function originprofileof(input: {
86
- profileid?: string;
87
- origin: string;
88
- grants?: actionkind[];
89
- denials?: actionkind[];
90
- now: number;
91
- }): originprofile {
92
- if (input.origin.trim() === "") throw new Error("The origin profile needs its exact origin.");
93
- return {
94
- profileid: input.profileid ?? randomid(),
95
- origin: input.origin,
96
- grants: [...(input.grants ?? [])],
97
- denials: [...(input.denials ?? [])],
98
- createdat: input.now,
99
- updatedat: input.now,
100
- };
101
- }
102
-
103
- /** Grants or denies one action kind on one origin profile; a kind already sitting on the opposite list moves so the lists never overlap. */
104
- export function profilekind(input: {
105
- profile: originprofile;
106
- kind: actionkind;
107
- decision: "grant" | "deny";
108
- now: number;
109
- }): originprofile {
110
- if (input.profile.grants.includes(input.kind) && input.decision === "grant") return input.profile;
111
- if (input.profile.denials.includes(input.kind) && input.decision === "deny") return input.profile;
112
- const grants =
113
- input.decision === "grant"
114
- ? [...new Set([...input.profile.grants, input.kind])]
115
- : input.profile.grants.filter((kind) => kind !== input.kind);
116
- const denials =
117
- input.decision === "deny"
118
- ? [...new Set([...input.profile.denials, input.kind])]
119
- : input.profile.denials.filter((kind) => kind !== input.kind);
120
- return { ...input.profile, grants, denials, updatedat: input.now };
121
- }
122
-
123
- /** Consults the per origin profile before one sensitive kind: an explicit denial refuses the kind on that origin, an explicit grant allows it while the fresh class consent gate still routes sensitive classes through their prompt, and an absent profile leaves the decision to the class consent gate. */
124
- export function profilegrade(input: { profile: originprofile | undefined; kind: actionkind; sensitive: boolean }): {
125
- allowed: boolean;
126
- consult: boolean;
127
- reason: string;
128
- } {
129
- if (!input.sensitive)
130
- return {
131
- allowed: true,
132
- consult: false,
133
- reason: `The ${input.kind} kind grades non-sensitive and the origin profile needs no consult.`,
134
- };
135
- if (input.profile === undefined)
136
- return {
137
- allowed: true,
138
- consult: true,
139
- reason: `No origin profile exists for the ${input.kind} kind, so the fresh class consent gate alone routes the sensitive step.`,
140
- };
141
- if (input.profile.denials.includes(input.kind))
142
- return {
143
- allowed: false,
144
- consult: true,
145
- reason: `The origin profile of ${input.profile.origin} denies the ${input.kind} kind; a denied kind never runs on that origin.`,
146
- };
147
- if (input.profile.grants.includes(input.kind))
148
- return {
149
- allowed: true,
150
- consult: true,
151
- reason: `The origin profile of ${input.profile.origin} grants the ${input.kind} kind the user reviewed.`,
152
- };
153
- return {
154
- allowed: true,
155
- consult: true,
156
- reason: `The origin profile of ${input.profile.origin} carries no ${input.kind} decision, so the fresh class consent gate alone routes the sensitive step.`,
157
- };
158
- }
159
-
160
- /** Reads the option payload of one step without throwing on malformed json. */
161
- function stepoptions(step: Pick<toolstep, "options">): Record<string, unknown> {
162
- if (!step.options) return {};
163
- try {
164
- const parsed = JSON.parse(step.options);
165
- return Boolean(parsed) && typeof parsed === "object" && !Array.isArray(parsed)
166
- ? (parsed as Record<string, unknown>)
167
- : {};
168
- } catch {
169
- return {};
170
- }
171
- }
172
-
173
- /** The payment kinds of the classification table: card fills and code fills carry money semantics before any option refinement. */
174
- const paymentkinds: ReadonlySet<string> = new Set(["fillcard", "fillcode"]);
175
- /** The credential kinds of the classification table: password consent, api key saves, auth handling and auth flows carry credentials. */
176
- const credentialkinds: ReadonlySet<string> = new Set(["consentpassword", "saveapikey", "handleauth", "authflow"]);
177
- /** The delete kinds of the classification table: discards, pattern closes, cookie clears, attribute removals and artifact cleanups destroy state. */
178
- const deletekinds: ReadonlySet<string> = new Set([
179
- "discardtab",
180
- "closepattern",
181
- "clearcookies",
182
- "removeattribute",
183
- "cleanupartifacts",
184
- ]);
185
- /** The publish kinds of the classification table: form and file posts, messages, submits and mutating calls move content out of the page. */
186
- const publishkinds: ReadonlySet<string> = new Set([
187
- "postform",
188
- "postfiles",
189
- "sendmessage",
190
- "submitform",
191
- "submitsearch",
192
- "writeclipboard",
193
- ]);
194
- /** The kinds the classification marks sensitive by default: uploads, downloads and evaluate grade sensitive regardless of their options. */
195
- const defaultsensitivekinds: ReadonlySet<string> = new Set([
196
- "attachfile",
197
- "uploadfile",
198
- "uploadfiles",
199
- "downloadfile",
200
- "downloadimages",
201
- "batchdownload",
202
- "pausedownload",
203
- "resumedownload",
204
- "quarantinedownload",
205
- "evaluate",
206
- ]);
207
-
208
- /** Classifies one step into the sensitive classes: payment, credential, delete and publish kinds take their table class, the kind options refine the class when they carry card or credential field shapes, and uploads, downloads and evaluate grade sensitive by default without a named class. */
209
- export function sensitiveclassesof(step: Pick<toolstep, "kind" | "value" | "options">): {
210
- classes: sensitiveclass[];
211
- bydefault: boolean;
212
- sensitive: boolean;
213
- reason: string;
214
- } {
215
- const options = stepoptions(step);
216
- const fields = Array.isArray(options.fields)
217
- ? (options.fields.filter((item) => Boolean(item) && typeof item === "object") as Array<Record<string, unknown>>)
218
- : [];
219
- const names = [
220
- ...fields.map((field) => (typeof field.name === "string" ? field.name : "")),
221
- typeof options.field === "string" ? options.field : "",
222
- typeof options.target === "string" ? options.target : "",
223
- ].map((name) => name.toLowerCase());
224
- const carries = (shape: string): boolean => names.some((name) => name.includes(shape));
225
- const classes = new Set<sensitiveclass>();
226
- if (paymentkinds.has(step.kind) || carries("card") || carries("cvc") || carries("cvv")) classes.add("payment");
227
- const credentialshape =
228
- carries("password") || carries("token") || carries("secret") || carries("apikey") || carries("passphrase");
229
- const submits =
230
- step.kind === "submitform" ||
231
- step.kind === "postform" ||
232
- step.kind === "submitsearch" ||
233
- step.kind === "fillform" ||
234
- step.kind === "filllabel" ||
235
- step.kind === "fillplaceholder";
236
- if (credentialkinds.has(step.kind) || (submits && credentialshape)) classes.add("credential");
237
- if (deletekinds.has(step.kind)) classes.add("delete");
238
- if (publishkinds.has(step.kind) || step.kind === "callrest" || step.kind === "callgraphql") {
239
- const verb =
240
- typeof options.method === "string"
241
- ? options.method.trim().toUpperCase()
242
- : typeof options.verb === "string"
243
- ? options.verb.trim().toUpperCase()
244
- : "";
245
- if (step.kind === "callrest" || step.kind === "callgraphql") {
246
- if (verb !== "" && !["GET", "HEAD", "OPTIONS"].includes(verb)) classes.add("publish");
247
- } else classes.add("publish");
248
- }
249
- const bydefault = defaultsensitivekinds.has(step.kind);
250
- const list = [...classes];
251
- if (list.length === 0 && !bydefault)
252
- return {
253
- classes: [],
254
- bydefault: false,
255
- sensitive: false,
256
- reason: `The ${step.kind} kind carries no sensitive class and no default sensitive grade.`,
257
- };
258
- return {
259
- classes: list,
260
- bydefault,
261
- sensitive: true,
262
- reason: `The ${step.kind} kind grades sensitive${list.length > 0 ? ` through the ${list.join(", ")} class${list.length === 1 ? "" : "es"}` : ""}${bydefault ? " by default" : ""}.`,
263
- };
264
- }
265
-
266
- /** Reads whether one fresh class consent covers a class on an origin: each sensitive class needs its own fresh prompt per origin, an expired consent covers nothing and a consent of another class never widens. */
267
- export function classconsentcovers(
268
- consents: classconsent[],
269
- origin: string,
270
- sensitiveclass: sensitiveclass,
271
- now: number,
272
- ): boolean {
273
- return consents.some(
274
- (consent) =>
275
- consent.origin === origin &&
276
- consent.sensitiveclass === sensitiveclass &&
277
- consent.grantedat <= now &&
278
- (consent.expiresat === undefined || now < consent.expiresat),
279
- );
280
- }
281
-
282
- /** Reads which fresh class consents one sensitive step still needs on its origin: every class the classification names needs its own prompt and a default sensitive grade without classes needs the plain sensitive prompt that the window itself carries. */
283
- export function missingclassconsents(input: {
284
- origin: string;
285
- classes: sensitiveclass[];
286
- bydefault: boolean;
287
- consents: classconsent[];
288
- now: number;
289
- }): { needed: boolean; missing: sensitiveclass[]; reason: string } {
290
- const missing = input.classes.filter((kind) => !classconsentcovers(input.consents, input.origin, kind, input.now));
291
- if (missing.length > 0)
292
- return {
293
- needed: true,
294
- missing,
295
- reason: `The sensitive classes ${missing.join(", ")} need one fresh consent prompt each on ${input.origin}.`,
296
- };
297
- if (input.bydefault && input.classes.length === 0)
298
- return {
299
- needed: true,
300
- missing: [],
301
- reason: `The ${input.origin} step grades sensitive by default and needs its fresh consent window prompt.`,
302
- };
303
- return {
304
- needed: false,
305
- missing: [],
306
- reason: `The fresh class consents of ${input.origin} cover every class the step names.`,
307
- };
308
- }
309
-
310
- /** Opens one consent window bound in time: the duration stays the user's positive choice, the boundary names itself in plain language and no window ever defaults to unlimited. */
311
- export function openconsentwindow(input: {
312
- id?: string;
313
- sessionid: string;
314
- origin: string;
315
- duration: number;
316
- kinds: actionkind[];
317
- now: number;
318
- boundary?: string;
319
- }): consentwindow {
320
- if (input.sessionid.trim() === "" || input.origin.trim() === "")
321
- throw new Error("The consent window needs its session and its exact origin.");
322
- if (!Number.isFinite(input.duration) || input.duration <= 0)
323
- throw new Error("The consent window needs its duration as a positive user value; no window defaults to unlimited.");
324
- return {
325
- id: input.id ?? randomid(),
326
- sessionid: input.sessionid,
327
- origin: input.origin,
328
- startedat: input.now,
329
- duration: input.duration,
330
- expiresat: input.now + input.duration,
331
- boundary:
332
- input.boundary?.trim() !== "" && input.boundary !== undefined
333
- ? input.boundary
334
- : `${input.duration} milliseconds the user chose`,
335
- kinds: [...new Set(input.kinds)],
336
- state: "active",
337
- };
338
- }
339
-
340
- /** Reads the state of one consent window at a time: an active window reports its remaining time while a window past its duration boundary reports its expiry. */
341
- export function consentwindowstate(
342
- window: consentwindow,
343
- now: number,
344
- ): { state: "active" | "expired"; remaining: number; reason: string } {
345
- if (window.state === "closed" || now >= window.expiresat)
346
- return {
347
- state: "expired",
348
- remaining: 0,
349
- reason: `The consent window of ${window.origin} closed at its ${window.boundary} boundary; the run suspends until a new explicit prompt renews it.`,
350
- };
351
- return {
352
- state: "active",
353
- remaining: window.expiresat - now,
354
- reason: `The consent window of ${window.origin} stays active with ${window.expiresat - now} milliseconds left of its ${window.boundary} boundary.`,
355
- };
356
- }
357
-
358
- /** Checks one step against its consent window: the window scopes to exactly one session and one origin, an expired window suspends the run mid step, and a step outside the window scope or past its boundary never dispatches. */
359
- export function windowgatesstep(input: {
360
- window: consentwindow | undefined;
361
- sessionid: string;
362
- origin: string;
363
- now: number;
364
- }): { allowed: boolean; suspended: boolean; reason: string } {
365
- if (input.window === undefined)
366
- return {
367
- allowed: false,
368
- suspended: false,
369
- reason: `No active consent window covers ${input.origin}; the consent prompt opens one before any step dispatches.`,
370
- };
371
- if (input.window.sessionid !== input.sessionid)
372
- return {
373
- allowed: false,
374
- suspended: false,
375
- reason: `The consent window scopes to the session ${input.window.sessionid} only and never widens to another session.`,
376
- };
377
- if (input.window.origin !== input.origin)
378
- return {
379
- allowed: false,
380
- suspended: false,
381
- reason: `The consent window scopes to the origin ${input.window.origin} only and never widens to another origin.`,
382
- };
383
- const state = consentwindowstate(input.window, input.now);
384
- if (state.state === "expired") return { allowed: false, suspended: true, reason: state.reason };
385
- return { allowed: true, suspended: false, reason: state.reason };
386
- }
387
-
388
- /** Closes every consent window past its duration boundary: the expired windows keep their records for the audit trail while their grants bind no step anymore. */
389
- export function expireconsentwindows(windows: consentwindow[], now: number): consentwindow[] {
390
- return windows.map((window) =>
391
- window.state === "active" && now >= window.expiresat
392
- ? { ...window, state: "closed" as const, closedat: now }
393
- : window,
394
- );
395
- }
396
-
397
- /** Renews a consent window only through a new explicit prompt: the renewal opens a brand new window record with its own duration and boundary while the old window stays closed in the history. */
398
- export function renewconsentwindow(input: {
399
- window: consentwindow;
400
- duration: number;
401
- kinds: actionkind[];
402
- now: number;
403
- }): { renewed: consentwindow; closed: consentwindow } {
404
- const closed: consentwindow =
405
- input.window.state === "active" ? { ...input.window, state: "closed", closedat: input.now } : input.window;
406
- const renewed = openconsentwindow({
407
- sessionid: input.window.sessionid,
408
- origin: input.window.origin,
409
- duration: input.duration,
410
- kinds: input.kinds.length > 0 ? input.kinds : input.window.kinds,
411
- now: input.now,
412
- });
413
- return { renewed, closed };
414
- }
415
-
416
- /** Revokes one run mid step as a terminal session event: the pending step and every queued step halt without executing, and the record names the acting user and the reason. */
417
- export function revokerun(input: {
418
- id?: string;
419
- sessionid: string;
420
- runid: string;
421
- pendingstepid?: string;
422
- queuedstepids?: string[];
423
- actor: string;
424
- reason?: string;
425
- now: number;
426
- }): revokerunevent {
427
- if (input.sessionid.trim() === "" || input.runid.trim() === "")
428
- throw new Error("The revocation needs its session and run ids.");
429
- if (input.actor.trim() === "") throw new Error("The revocation names the acting user.");
430
- const halted = [...(input.pendingstepid !== undefined ? [input.pendingstepid] : []), ...(input.queuedstepids ?? [])];
431
- if (halted.length === 0) throw new Error("The revocation halts at least the pending step of the run.");
432
- return {
433
- id: input.id ?? randomid(),
434
- sessionid: input.sessionid,
435
- runid: input.runid,
436
- haltedstepids: halted,
437
- actor: input.actor,
438
- reason:
439
- input.reason?.trim() !== "" && input.reason !== undefined
440
- ? input.reason
441
- : "The user revoked the consent mid run.",
442
- at: input.now,
443
- };
444
- }
445
-
446
- /** Reads which steps one revocation halted with the pending step first: the rollback list the confirm dialog names before the user confirms. */
447
- export function haltedstepsof(revocation: revokerunevent): { pending?: string; queued: string[] } {
448
- return {
449
- ...(revocation.haltedstepids.length > 0 ? { pending: revocation.haltedstepids[0] } : {}),
450
- queued: revocation.haltedstepids.slice(1),
451
- };
452
- }
453
-
454
- /** Builds one consent scope grant that names the origin, the kinds and the boundary in a single record; the session start writes it into the immutable log. */
455
- export function scopegrantof(input: {
456
- origin: string;
457
- kinds: actionkind[];
458
- boundary: string;
459
- now: number;
460
- }): consentscope {
461
- if (input.origin.trim() === "") throw new Error("The consent scope needs its exact origin.");
462
- if (input.kinds.length === 0) throw new Error("The consent scope names the kinds it covers.");
463
- if (input.boundary.trim() === "")
464
- throw new Error("The consent scope names its boundary; no grant defaults to unlimited.");
465
- return { origin: input.origin, kinds: [...new Set(input.kinds)], boundary: input.boundary, grantedat: input.now };
466
- }
467
-
468
- /** Builds one denied step evidence without navigation: the origin, the action kind and the deny reason in plain language. */
469
- export function deniedevidenceof(input: { origin: string; kind: string; reason: string; now: number }): deniedevidence {
470
- return { origin: input.origin, kind: input.kind, reason: input.reason, at: input.now };
471
- }
472
-
473
- /** Builds the consent prompt text of one pending sensitive step: the exact origin, the kind, the class badge and the consent window duration the user chooses; the prompt always names a boundary and never defaults to unlimited. */
474
- export function consentprompttext(input: {
475
- origin: string;
476
- kind: string;
477
- classes: sensitiveclass[];
478
- bydefault: boolean;
479
- duration: number;
480
- }): string {
481
- const label =
482
- input.classes.length > 0
483
- ? `the ${input.classes.join(" and ")} class${input.classes.length === 1 ? "" : "es"}`
484
- : "a sensitive by default grade";
485
- return `Allow the ${input.kind} step on ${input.origin} graded as ${label} for ${input.duration} milliseconds? The consent window closes at that boundary; no grant ever defaults to unlimited.`;
486
- }
487
-
488
- /** Builds the denydefault notice of one origin for the popup: ungranted origins stay refused until the user grants them. */
489
- export function denydefaultnotice(origin: string): string {
490
- return `The denydefault posture refuses ${origin} until the user adds the origin to the automation allowlist; no step dispatches without the grant.`;
491
- }
492
-
493
- /** Builds the origin profile summary of the active tab for the popup: the granted and denied kinds the per site profile carries. */
494
- export function profilesummary(profile: originprofile | undefined): string {
495
- if (profile === undefined)
496
- return "No origin profile exists for this origin yet; sensitive steps route through their fresh consent prompts.";
497
- return `The origin profile of ${profile.origin} grants ${profile.grants.length} kind${profile.grants.length === 1 ? "" : "s"} and denies ${profile.denials.length} kind${profile.denials.length === 1 ? "" : "s"} the user reviewed.`;
498
- }
499
-
500
- /** The documented reads only baseline of the safedefaults posture: observation and read kinds grant on an unknown origin while every sensitive class denies; the user extends the grants through the originprofile editor, never the engine. */
501
- const safedefaultreadkinds: ReadonlySet<string> = new Set([
502
- "observe",
503
- "readhtml",
504
- "readtext",
505
- "readlinks",
506
- "readtable",
507
- "readforms",
508
- "readvisible",
509
- "readselection",
510
- "readmeta",
511
- "readlang",
512
- "readoutline",
513
- "readstyle",
514
- "readimages",
515
- "readertree",
516
- ]);
517
-
518
- /** Builds the safedefaults profile of an unknown origin: the initial originprofile the executor applies when no profile exists grants the documented read kinds only and denies every sensitive class kind, so a first visit never runs a payment, credential, delete or publish step until the user edits the profile. */
519
- export function safedefaultprofile(input: { origin: string; now: number; profileid?: string }): originprofile {
520
- if (input.origin.trim() === "") throw new Error("The safedefaults profile needs its exact origin.");
521
- const denials = new Set<string>([
522
- ...paymentkinds,
523
- ...credentialkinds,
524
- ...deletekinds,
525
- ...publishkinds,
526
- ...defaultsensitivekinds,
527
- ]);
528
- return {
529
- profileid: input.profileid ?? randomid(),
530
- origin: input.origin.trim(),
531
- grants: [...safedefaultreadkinds] as actionkind[],
532
- denials: [...denials] as actionkind[],
533
- createdat: input.now,
534
- updatedat: input.now,
535
- };
536
- }
537
-
538
- /** Reads whether one action kind sits inside the reads only baseline of the safedefaults posture. */
539
- export function safedefaultreadkind(kind: actionkind): boolean {
540
- return safedefaultreadkinds.has(kind);
541
- }
542
-
543
- /** Builds the safedefaults notice of an unknown origin for the popup: the notice links to the originprofile editor because only the user widens a safedefaults profile. */
544
- export function safedefaultnotice(origin: string): string {
545
- return `The safedefaults posture profiles ${origin} on its first visit: reads only, every sensitive class denied; open the originprofile editor to widen the profile.`;
546
- }
547
-
548
- /* ── Merged from inboundguard.ts ── */
549
-
550
- import type { connectallowentry, deferredevent, origincheckverdict, ratelimitbucket, schemaerror } from "./types.js";
551
-
552
- /**
553
- * Inbound guard logic of the 1.1.62 family.
554
- * Every message, command and connection that enters the background passes this guard chain: schemastrict validates each inbound command against its declared grammar and rejects unknown fields with the path and the expected shape, origincheck verdicts guard every runtime message and every port connection, connectallow holds the external senders the user allowed and ships empty by default so every unknown sender drops without handler execution, and the ratelimit buckets bound automation commands per origin and per session by deferring every command that exceeds its bucket until the window resets.
555
- * No bound, window or sender list is ever hardcoded: the buckets stay user configured values with no hidden ceiling, and the connectallow list holds user managed entries only.
556
- */
557
-
558
- /** The field shapes schemastrict understands: strings, numbers, booleans, plain objects and arrays, beside the absent marker a field the grammar never declared expects. */
559
- export type schemashape = "string" | "number" | "boolean" | "object" | "array" | "absent";
560
-
561
- /** Reads the shape name of one value for the schemastrict error grammar. */
562
- function shapeof(value: unknown): Exclude<schemashape, "absent"> {
563
- if (typeof value === "string") return "string";
564
- if (typeof value === "number") return "number";
565
- if (typeof value === "boolean") return "boolean";
566
- if (Array.isArray(value)) return "array";
567
- return "object";
568
- }
569
-
570
- /** Validates one inbound command under schemastrict: every field the grammar declares keeps its expected shape, a missing required field errors with its path, and a field the grammar never declared errors with the expected shape absent so the command never dispatches. */
571
- export function schemacheck(input: {
572
- command: Record<string, unknown>;
573
- schema: Record<string, schemashape>;
574
- required?: string[];
575
- }): { valid: boolean; errors: schemaerror[] } {
576
- const errors: schemaerror[] = [];
577
- for (const [field, value] of Object.entries(input.command)) {
578
- const expected = input.schema[field];
579
- if (expected === undefined) {
580
- errors.push({
581
- path: field,
582
- expected: "absent",
583
- found: shapeof(value),
584
- reason: `The ${field} field sits absent from the declared grammar of the command; schemastrict refuses unknown fields before dispatch.`,
585
- });
586
- continue;
587
- }
588
- if (expected === "absent") {
589
- errors.push({
590
- path: field,
591
- expected: "absent",
592
- found: shapeof(value),
593
- reason: `The ${field} field carries no value under the declared grammar; schemastrict refuses it before dispatch.`,
594
- });
595
- continue;
596
- }
597
- if (shapeof(value) !== expected)
598
- errors.push({
599
- path: field,
600
- expected: expected,
601
- found: shapeof(value),
602
- reason: `The ${field} field expects a ${expected} while the command carries a ${shapeof(value)}; schemastrict refuses the shape mismatch before dispatch.`,
603
- });
604
- }
605
- for (const field of input.required ?? []) {
606
- if (input.command[field] === undefined)
607
- errors.push({
608
- path: field,
609
- expected: input.schema[field] ?? "string",
610
- found: "absent",
611
- reason: `The ${field} field is required by the declared grammar and the command carries no value; schemastrict refuses the incomplete command before dispatch.`,
612
- });
613
- }
614
- return { valid: errors.length === 0, errors };
615
- }
616
-
617
- /** Validates one inbound command envelope: the kind must be a non-empty string the dispatch registry knows, so an unknown command kind refuses before any handler runs. */
618
- export function envelopecheck(input: { command: Record<string, unknown>; knownkinds: readonly string[] }): {
619
- valid: boolean;
620
- errors: schemaerror[];
621
- } {
622
- const kind = input.command.kind;
623
- if (typeof kind !== "string" || kind.trim() === "")
624
- return {
625
- valid: false,
626
- errors: [
627
- {
628
- path: "kind",
629
- expected: "string",
630
- found: shapeof(kind),
631
- reason: "Every inbound command names its kind as a non-empty string; a kindless command never dispatches.",
632
- },
633
- ],
634
- };
635
- if (!input.knownkinds.includes(kind))
636
- return {
637
- valid: false,
638
- errors: [
639
- {
640
- path: "kind",
641
- expected: `one of ${input.knownkinds.length} declared command kinds`,
642
- found: kind,
643
- reason: `The ${kind} command kind sits absent from the dispatch registry; schemastrict refuses unknown commands before dispatch.`,
644
- },
645
- ],
646
- };
647
- return { valid: true, errors: [] };
648
- }
649
-
650
- /** Checks one inbound runtime message or port connection through origincheck: an internal sender of this extension accepts, an external sender accepts only when its sender id sits in the connectallow list the user manages, and every other sender drops without handler execution. */
651
- export function origincheckof(input: {
652
- senderid?: string;
653
- senderorigin?: string;
654
- extensionid: string;
655
- connectallow: connectallowentry[];
656
- }): origincheckverdict {
657
- const sender = input.senderid ?? "an unknown sender";
658
- const origin = input.senderorigin ?? "";
659
- if (input.senderid === input.extensionid)
660
- return {
661
- accepted: true,
662
- sender,
663
- origin,
664
- reason: "The sender is this extension itself; the internal surface accepts.",
665
- };
666
- if (
667
- input.senderid !== undefined &&
668
- input.connectallow.some(
669
- (entry) => entry.senderid === input.senderid && (entry.origin === undefined || entry.origin === origin),
670
- )
671
- ) {
672
- return {
673
- accepted: true,
674
- sender,
675
- origin,
676
- reason: `The sender ${sender} sits in the connectallow list the user manages${origin !== "" ? ` for ${origin}` : ""}; the message accepts.`,
677
- };
678
- }
679
- if (input.senderid === undefined)
680
- return {
681
- accepted: false,
682
- sender,
683
- origin,
684
- reason: "The message carries no sender identity; the guard drops it before any handler runs.",
685
- };
686
- return {
687
- accepted: false,
688
- sender,
689
- origin,
690
- reason: `The sender ${sender} sits absent from the connectallow list; the guard drops the message without handler execution.`,
691
- };
692
- }
693
-
694
- /** Checks one port connection through the same origincheck grammar: only ports of this extension or of allowed external senders accept, and every other port closes at the handshake. */
695
- export function portaccept(input: {
696
- portname: string;
697
- senderid?: string;
698
- senderorigin?: string;
699
- extensionid: string;
700
- connectallow: connectallowentry[];
701
- }): origincheckverdict {
702
- const verdict = origincheckof(input);
703
- if (!verdict.accepted)
704
- return { ...verdict, reason: `The port ${input.portname} closes at its handshake: ${verdict.reason}` };
705
- return { ...verdict, reason: `The port ${input.portname} accepted its handshake: ${verdict.reason}` };
706
- }
707
-
708
- /** Builds one connectallow entry: the external sender id the user allows, its display name and its optional origin scope; the list ships empty by default. */
709
- export function connectallowentryof(input: {
710
- senderid: string;
711
- displayname: string;
712
- origin?: string;
713
- now: number;
714
- }): connectallowentry {
715
- if (input.senderid.trim() === "") throw new Error("The connectallow entry needs its sender id.");
716
- if (input.displayname.trim() === "") throw new Error("The connectallow entry needs its display name.");
717
- return {
718
- senderid: input.senderid.trim(),
719
- displayname: input.displayname.trim(),
720
- ...(input.origin !== undefined && input.origin.trim() !== "" ? { origin: input.origin.trim() } : {}),
721
- addedat: input.now,
722
- };
723
- }
724
-
725
- /** The empty connectallow list the extension ships with: no external sender accepts until the user adds its entry. */
726
- export const emptyconnectallow: connectallowentry[] = [];
727
-
728
- /** Validates the user configured ratelimit bucket bounds: the limit and the window stay positive user values because no hidden ceiling exists. */
729
- export function bucketboundsvalid(limit: number, window: number): { valid: boolean; reason: string } {
730
- if (!Number.isFinite(limit) || limit <= 0)
731
- return {
732
- valid: false,
733
- reason: "The ratelimit bucket limit stays a positive user value; no hidden ceiling exists.",
734
- };
735
- if (!Number.isFinite(window) || window <= 0)
736
- return {
737
- valid: false,
738
- reason:
739
- "The ratelimit bucket window stays a positive user value in milliseconds; the window reset stays the user's choice.",
740
- };
741
- return {
742
- valid: true,
743
- reason: `The bucket bound of ${limit} commands per ${window} milliseconds stays the user configured choice with no hidden ceiling.`,
744
- };
745
- }
746
-
747
- /** Builds one ratelimit bucket per origin and per session with its user configured bound and window. */
748
- export function bucketof(input: {
749
- origin: string;
750
- sessionid: string;
751
- limit: number;
752
- window: number;
753
- now: number;
754
- }): ratelimitbucket {
755
- const bounds = bucketboundsvalid(input.limit, input.window);
756
- if (!bounds.valid) throw new Error(bounds.reason);
757
- return {
758
- origin: input.origin,
759
- sessionid: input.sessionid,
760
- limit: input.limit,
761
- window: input.window,
762
- used: 0,
763
- windowstartedat: input.now,
764
- resetsat: input.now + input.window,
765
- };
766
- }
767
-
768
- /** Consumes one command from its bucket: a command inside its bound consumes, a command past its bound defers until the window resets, and a window past its reset starts a fresh window. */
769
- export function bucketconsume(input: { bucket: ratelimitbucket; now: number }): {
770
- allowed: boolean;
771
- deferred: boolean;
772
- bucket: ratelimitbucket;
773
- resetsat: number;
774
- reason: string;
775
- } {
776
- if (input.now >= input.bucket.resetsat) {
777
- const fresh: ratelimitbucket = {
778
- ...input.bucket,
779
- used: 0,
780
- windowstartedat: input.now,
781
- resetsat: input.now + input.bucket.window,
782
- };
783
- return {
784
- allowed: true,
785
- deferred: false,
786
- bucket: { ...fresh, used: 1 },
787
- resetsat: fresh.resetsat,
788
- reason: `The bucket window of ${input.bucket.origin} reset and the command consumes the first slot of ${fresh.limit}.`,
789
- };
790
- }
791
- if (input.bucket.used < input.bucket.limit) {
792
- return {
793
- allowed: true,
794
- deferred: false,
795
- bucket: { ...input.bucket, used: input.bucket.used + 1 },
796
- resetsat: input.bucket.resetsat,
797
- reason: `The command consumes slot ${input.bucket.used + 1} of ${input.bucket.limit} in the bucket of ${input.bucket.origin}.`,
798
- };
799
- }
800
- return {
801
- allowed: false,
802
- deferred: true,
803
- bucket: input.bucket,
804
- resetsat: input.bucket.resetsat,
805
- reason: `The bucket of ${input.bucket.origin} holds its ${input.bucket.limit} command bound; the command defers until the window resets at ${input.bucket.resetsat}.`,
806
- };
807
- }
808
-
809
- /** Builds one deferred command event: the step the bucket deferred with the reason and the reset time it waits for. */
810
- export function deferredeventof(input: {
811
- stepid: string;
812
- kind: string;
813
- origin: string;
814
- reason: string;
815
- resetsat: number;
816
- now: number;
817
- id?: string;
818
- }): deferredevent {
819
- if (input.stepid.trim() === "" || input.kind.trim() === "")
820
- throw new Error("The deferred event needs its step and kind.");
821
- return {
822
- id: input.id ?? randomid(),
823
- stepid: input.stepid,
824
- kind: input.kind,
825
- origin: input.origin,
826
- reason: input.reason,
827
- resetsat: input.resetsat,
828
- at: input.now,
829
- };
830
- }
831
-
832
- /** Reads whether one deferred command may retry: the deferral holds until its bucket window resets. */
833
- export function deferredready(deferred: deferredevent, now: number): boolean {
834
- return now >= deferred.resetsat;
835
- }
836
-
837
- /* ── Merged from maskinputs.ts ── */
838
-
839
- import type { maskrule, observation, runsettings } from "./types.js";
840
-
841
- /**
842
- * Mask inputs logic of the 1.1.61 family.
843
- * The masking rules live here: typed values, form values and stored values never reach a log, an observation or an export unmasked when their field shapes match the documented password, token, card and secret families or the shapes the user configured; the observation schema keeps its field shapes while the values behind masked shapes carry their redaction marker instead.
844
- * No shape list is closed: the documented four families stay the recognition baseline and the user extends them through the global maskshapes setting and the per origin mask rules, because a sensitive field shape stays the user's knowledge, never the engine's guess.
845
- */
846
-
847
- /** The documented mask shape families the recognizer reads by default: password, token, card and secret field shapes; the user extends the list, never narrows the recognition below it. */
848
- export const defaultmaskshapes: readonly string[] = ["password", "token", "card", "secret"];
849
-
850
- /** The redaction marker every masked value carries; the marker stays visible so a review sees that a value existed without reading it. */
851
- export const maskmarker = "[redacted]";
852
-
853
- /** Reads the shape family one field name matches: password, token, card and secret shapes recognize by name while every other field keeps its value. */
854
- export function fieldshapekind(name: string): "password" | "token" | "card" | "secret" | undefined {
855
- const lowered = name.toLowerCase();
856
- if (
857
- lowered.includes("password") ||
858
- lowered.includes("passwd") ||
859
- lowered.includes("pwd") ||
860
- lowered.includes("passphrase")
861
- )
862
- return "password";
863
- if (
864
- lowered.includes("token") ||
865
- lowered.includes("apikey") ||
866
- lowered.includes("api_key") ||
867
- lowered.includes("auth") ||
868
- lowered.includes("bearer")
869
- )
870
- return "token";
871
- if (
872
- lowered.includes("card") ||
873
- lowered.includes("cvc") ||
874
- lowered.includes("cvv") ||
875
- lowered.includes("expiry") ||
876
- lowered.includes("pan")
877
- )
878
- return "card";
879
- if (lowered.includes("secret")) return "secret";
880
- return undefined;
881
- }
882
-
883
- /** Reads the effective mask shapes of one origin: the documented four families, the global user configured shapes and the per origin mask rules join into one recognition list. */
884
- export function shapesof(input: { settings?: runsettings; rules: maskrule[]; origin?: string }): string[] {
885
- const shapes = new Set<string>(defaultmaskshapes);
886
- for (const shape of input.settings?.maskshapes ?? []) if (shape.trim() !== "") shapes.add(shape.trim().toLowerCase());
887
- for (const rule of input.rules) {
888
- const scoped =
889
- rule.origin === undefined || rule.origin === "" || (input.origin !== undefined && rule.origin === input.origin);
890
- if (scoped) for (const shape of rule.shapes) if (shape.trim() !== "") shapes.add(shape.trim().toLowerCase());
891
- }
892
- return [...shapes];
893
- }
894
-
895
- /** Reads whether one field name matches the effective shapes: the documented recognizer runs first and the configured shapes match by substring. */
896
- export function maskingfield(name: string, shapes: string[]): boolean {
897
- if (fieldshapekind(name) !== undefined) return true;
898
- const lowered = name.toLowerCase();
899
- return shapes.some((shape) => shape !== "" && lowered.includes(shape));
900
- }
901
-
902
- /** Masks one value behind the redaction marker; an empty value stays empty because nothing existed to redact. */
903
- export function maskvalue(value: string): string {
904
- return value === "" ? "" : maskmarker;
905
- }
906
-
907
- /** Masks one named field when its shape matches: the field keeps its name while its value carries the redaction marker. */
908
- export function maskfield(input: { name: string; value: string; shapes: string[] }): string {
909
- return maskingfield(input.name, input.shapes) ? maskvalue(input.value) : input.value;
910
- }
911
-
912
- /** Masks the typed values of one record deeply: every key whose shape matches keeps its key while its string value carries the redaction marker, and nested records walk the same rule. */
913
- export function maskrecord(record: Record<string, unknown>, shapes: string[]): Record<string, unknown> {
914
- const masked: Record<string, unknown> = {};
915
- for (const [key, value] of Object.entries(record)) {
916
- if (typeof value === "string") {
917
- const sibling = (record as Record<string, unknown>).name;
918
- masked[key] =
919
- key === "value" && typeof sibling === "string"
920
- ? maskfield({ name: sibling, value, shapes })
921
- : maskfield({ name: key, value, shapes });
922
- } else if (Array.isArray(value))
923
- masked[key] = value.map((item) =>
924
- Boolean(item) && typeof item === "object" && !Array.isArray(item)
925
- ? maskrecord(item as Record<string, unknown>, shapes)
926
- : item,
927
- );
928
- else if (Boolean(value) && typeof value === "object")
929
- masked[key] = maskrecord(value as Record<string, unknown>, shapes);
930
- else masked[key] = value;
931
- }
932
- return masked;
933
- }
934
-
935
- /** Masks the typed value and the reviewed options of one step before they reach any log writer: a step whose target or kind names a masked shape keeps its shape while the typed value carries the redaction marker. */
936
- export function masktypedvalues(input: {
937
- step: Pick<toolstep, "kind" | "target" | "value" | "options">;
938
- shapes: string[];
939
- }): { value?: string; options?: string } {
940
- const sensitive = maskingfield(input.step.target ?? "", input.shapes) || maskingfield(input.step.kind, input.shapes);
941
- const maskedvalue = input.step.value !== undefined && sensitive ? maskvalue(input.step.value) : input.step.value;
942
- let maskedoptions = input.step.options;
943
- if (input.step.options !== undefined) {
944
- try {
945
- const parsed = JSON.parse(input.step.options) as unknown;
946
- if (Boolean(parsed) && typeof parsed === "object" && !Array.isArray(parsed))
947
- maskedoptions = JSON.stringify(maskrecord(parsed as Record<string, unknown>, input.shapes));
948
- } catch {
949
- /* an options payload outside the json grammar keeps its reviewed text */
950
- }
951
- }
952
- return {
953
- ...(maskedvalue !== undefined ? { value: maskedvalue } : {}),
954
- ...(maskedoptions !== undefined ? { options: maskedoptions } : {}),
955
- };
956
- }
957
-
958
- /** Masks one captured form state: every field keeps its name and type while a value behind a masked shape carries the redaction marker, so the observation schema keeps its field shapes but masks their values. */
959
- export function maskformstate<T extends { name: string; value: string }>(
960
- fields: T[],
961
- shapes: string[],
962
- ): Array<T & { value: string }> {
963
- return fields.map((field) => ({ ...field, value: maskfield({ name: field.name, value: field.value, shapes }) }));
964
- }
965
-
966
- /** Masks one observation payload before logging: the schema keeps every field shape while the option lists of fields whose names match a masked shape carry the redaction marker instead of their entries. */
967
- export function maskobservation(shot: observation, shapes: string[]): observation {
968
- return {
969
- ...shot,
970
- forms: shot.forms.map((form) => (maskingfield(form.name, shapes) ? { ...form, options: [maskmarker] } : form)),
971
- };
972
- }
973
-
974
- /** Masks the stored values of one storage record: the keys keep their names while a value behind a masked shape carries the redaction marker. */
975
- export function maskstoredvalues(record: Record<string, string>, shapes: string[]): Record<string, string> {
976
- const masked: Record<string, string> = {};
977
- for (const [key, value] of Object.entries(record)) masked[key] = maskfield({ name: key, value, shapes });
978
- return masked;
979
- }
980
-
981
- /** Excludes masked values from every export of the run record: a value that already carries the redaction marker stays visible as evidence a value existed, while any raw value behind a masked shape carries the marker before the export leaves the browser. */
982
- export function maskexport(record: Record<string, unknown>, shapes: string[]): Record<string, unknown> {
983
- return maskrecord(record, shapes);
984
- }
985
-
986
- /* ── Merged from immutablelog.ts ── */
987
-
988
- import type { immutablelogentry, logeventkind, loghash, sealedlog, storedrunlog } from "./types.js";
989
-
990
- /**
991
- * Immutable run log logic of the 1.1.61 family.
992
- * The append only log lives here: every step transition, grant, expiry, revoke, deny, suspend and resume event lands as one entry whose loghash chains it to the hash of its predecessor at append time with no later rewrite, the completion seal closes the log with a final hash, and the read path verifies the whole chain before returning a single entry so tampering with any link stays detectable.
993
- * There is no update or delete path: a sealed log refuses every append, and an unsealed log only ever grows. The hash algorithm stays sha-256 because a cryptographic primitive is a correctness choice, not a tunable.
994
- */
995
-
996
- /** Derives the sha-256 hex digest of one log payload through the web crypto the platform offers. */
997
- async function sha256(payload: string): Promise<string> {
998
- const bytes = new TextEncoder().encode(payload);
999
- const digest = await crypto.subtle.digest("SHA-256", bytes);
1000
- return [...new Uint8Array(digest)].map((byte) => byte.toString(16).padStart(2, "0")).join("");
1001
- }
1002
-
1003
- /** Reads the canonical body of one log entry: the fields the hash covers, with the chain link itself excluded. */
1004
- function entrybody(entry: Omit<immutablelogentry, "hash">): string {
1005
- return JSON.stringify({
1006
- id: entry.id,
1007
- runid: entry.runid,
1008
- kind: entry.kind,
1009
- summary: entry.summary,
1010
- origin: entry.origin,
1011
- ...(entry.stepid !== undefined ? { stepid: entry.stepid } : {}),
1012
- at: entry.at,
1013
- });
1014
- }
1015
-
1016
- /** Derives the hash chain link of one log entry: the sha-256 over the predecessor hash and the canonical entry body, written once at append time. */
1017
- export async function entryhashof(input: {
1018
- previous: string;
1019
- entry: Omit<immutablelogentry, "hash">;
1020
- }): Promise<loghash> {
1021
- return {
1022
- previous: input.previous,
1023
- current: await sha256(`${input.previous}\n${entrybody(input.entry)}`),
1024
- algorithm: "sha-256",
1025
- };
1026
- }
1027
-
1028
- /** Builds one log entry with its chain link derived at append time: the event kind, the summary, the origin and step provenance and the time. */
1029
- export async function logentryof(input: {
1030
- id?: string;
1031
- runid: string;
1032
- kind: logeventkind;
1033
- summary: string;
1034
- origin: string;
1035
- stepid?: string;
1036
- at: number;
1037
- previous: string;
1038
- }): Promise<immutablelogentry> {
1039
- if (input.summary.trim() === "") throw new Error("The log entry needs its summary in plain language.");
1040
- if (input.origin.trim() === "") throw new Error("The log entry needs its origin provenance.");
1041
- const entry: Omit<immutablelogentry, "hash"> = {
1042
- id: input.id ?? randomid(),
1043
- runid: input.runid,
1044
- kind: input.kind,
1045
- summary: input.summary,
1046
- origin: input.origin,
1047
- ...(input.stepid !== undefined ? { stepid: input.stepid } : {}),
1048
- at: input.at,
1049
- };
1050
- return { ...entry, hash: await entryhashof({ previous: input.previous, entry }) };
1051
- }
1052
-
1053
- /** Opens one empty run log: the first entry of the log chains to the genesis hash of exactly sixty-four zeros. */
1054
- export function openrunlog(input: { runid: string; sessionid: string; now: number }): storedrunlog {
1055
- if (input.runid.trim() === "" || input.sessionid.trim() === "")
1056
- throw new Error("The run log needs its run and session ids.");
1057
- return { runid: input.runid, sessionid: input.sessionid, entries: [], updatedat: input.now };
1058
- }
1059
-
1060
- /** Reads the hash of the last entry of one log: the value the next append chains to, with the genesis hash for an empty log. */
1061
- export function lasthashof(log: storedrunlog): string {
1062
- const entry = log.entries[log.entries.length - 1];
1063
- return entry === undefined ? "0".repeat(64) : entry.hash.current;
1064
- }
1065
-
1066
- /** Appends one event to the run log inside a single storage transaction the caller persists: the entry hash chains to its predecessor and is written at append time, and a sealed log refuses every append because the seal is terminal. */
1067
- export async function appendlogentry(input: {
1068
- log: storedrunlog;
1069
- kind: logeventkind;
1070
- summary: string;
1071
- origin: string;
1072
- stepid?: string;
1073
- at: number;
1074
- id?: string;
1075
- }): Promise<storedrunlog> {
1076
- if (input.log.seal !== undefined)
1077
- throw new Error(
1078
- `The run log of ${input.log.runid} sealed at ${input.log.seal.sealedat} and accepts no append; the seal is terminal.`,
1079
- );
1080
- const entry = await logentryof({
1081
- ...(input.id !== undefined ? { id: input.id } : {}),
1082
- runid: input.log.runid,
1083
- kind: input.kind,
1084
- summary: input.summary,
1085
- origin: input.origin,
1086
- ...(input.stepid !== undefined ? { stepid: input.stepid } : {}),
1087
- at: input.at,
1088
- previous: lasthashof(input.log),
1089
- });
1090
- return { ...input.log, entries: [...input.log.entries, entry], updatedat: input.at };
1091
- }
1092
-
1093
- /** Seals one run log at completion with a final hash: the seal chains to the last entry hash and closes the log with no later append. */
1094
- export async function sealrunlog(log: storedrunlog, now: number): Promise<{ log: storedrunlog; seal: sealedlog }> {
1095
- if (log.seal !== undefined)
1096
- throw new Error(`The run log of ${log.runid} already sealed at ${log.seal.sealedat}; the seal is terminal.`);
1097
- if (log.entries.length === 0) throw new Error("The run log seals at completion with at least one entry.");
1098
- const sealhash = await entryhashof({
1099
- previous: lasthashof(log),
1100
- entry: {
1101
- id: `seal:${log.runid}`,
1102
- runid: log.runid,
1103
- kind: "seal",
1104
- summary: `The run ${log.runid} completed and the log sealed with ${log.entries.length} entries.`,
1105
- origin: log.entries[log.entries.length - 1]?.origin ?? log.runid,
1106
- at: now,
1107
- },
1108
- });
1109
- const seal: sealedlog = { runid: log.runid, entries: log.entries.length, sealhash, sealedat: now };
1110
- return { log: { ...log, seal, updatedat: now }, seal };
1111
- }
1112
-
1113
- /** Verifies the whole hash chain of one log at read time: every entry hash must chain to its predecessor and match its own body, so tampering with any summary, provenance or link stays detectable as tamper evidence. */
1114
- export async function verifylogchain(
1115
- entries: immutablelogentry[],
1116
- ): Promise<{ valid: boolean; brokenat?: number; reason: string }> {
1117
- let previous = "0".repeat(64);
1118
- for (let index = 0; index < entries.length; index += 1) {
1119
- const entry = entries[index];
1120
- if (entry === undefined) continue;
1121
- if (entry.hash.previous !== previous)
1122
- return {
1123
- valid: false,
1124
- brokenat: index,
1125
- reason: `The chain link of entry ${index} carries the previous hash ${entry.hash.previous} while its predecessor hashes to ${previous}; the chain reports tamper evidence.`,
1126
- };
1127
- const expected = await entryhashof({
1128
- previous,
1129
- entry: {
1130
- id: entry.id,
1131
- runid: entry.runid,
1132
- kind: entry.kind,
1133
- summary: entry.summary,
1134
- origin: entry.origin,
1135
- ...(entry.stepid !== undefined ? { stepid: entry.stepid } : {}),
1136
- at: entry.at,
1137
- },
1138
- });
1139
- if (entry.hash.current !== expected.current)
1140
- return {
1141
- valid: false,
1142
- brokenat: index,
1143
- reason: `The entry hash of entry ${index} matches neither its body nor its predecessor hash; the chain reports tamper evidence.`,
1144
- };
1145
- previous = entry.hash.current;
1146
- }
1147
- return {
1148
- valid: true,
1149
- reason: `The hash chain of ${entries.length} entr${entries.length === 1 ? "y" : "ies"} verifies from the genesis hash to the last entry.`,
1150
- };
1151
- }
1152
-
1153
- /** Reads one run log only through chain verification: a verified chain returns its entries while a chain with a broken link refuses the read entirely. */
1154
- export async function readverifiedlog(
1155
- log: storedrunlog,
1156
- ): Promise<{ ok: boolean; entries: immutablelogentry[]; reason: string }> {
1157
- const verification = await verifylogchain(log.entries);
1158
- if (!verification.valid) return { ok: false, entries: [], reason: verification.reason };
1159
- return { ok: true, entries: [...log.entries], reason: verification.reason };
1160
- }
1161
-
1162
- /** Reads the verification status of one stored run log for the panel: the chain validity, the entry count, the broken link when one exists and the seal hash at completion. */
1163
- export async function chainreportof(
1164
- log: storedrunlog,
1165
- ): Promise<{
1166
- runid: string;
1167
- valid: boolean;
1168
- entries: number;
1169
- brokenat?: number;
1170
- reason: string;
1171
- sealhash?: string;
1172
- sealedat?: number;
1173
- }> {
1174
- const verification = await verifylogchain(log.entries);
1175
- if (!verification.valid)
1176
- return {
1177
- runid: log.runid,
1178
- valid: false,
1179
- entries: log.entries.length,
1180
- ...(verification.brokenat !== undefined ? { brokenat: verification.brokenat } : {}),
1181
- reason: verification.reason,
1182
- };
1183
- return {
1184
- runid: log.runid,
1185
- valid: true,
1186
- entries: log.entries.length,
1187
- reason: verification.reason,
1188
- ...(log.seal !== undefined ? { sealhash: log.seal.sealhash.current, sealedat: log.seal.sealedat } : {}),
1189
- };
1190
- }
1191
-
1192
- /** Exports one run log as an audit record only after the chain verifies: the entries, the chain status and the seal hash travel together while a broken link refuses the export. */
1193
- export async function exportlogchain(
1194
- log: storedrunlog,
1195
- ): Promise<{
1196
- runid: string;
1197
- entries: number;
1198
- chainvalid: boolean;
1199
- reason: string;
1200
- sealhash?: string;
1201
- sealedat?: number;
1202
- log: immutablelogentry[];
1203
- }> {
1204
- const read = await readverifiedlog(log);
1205
- if (!read.ok) return { runid: log.runid, entries: 0, chainvalid: false, reason: read.reason, log: [] };
1206
- return {
1207
- runid: log.runid,
1208
- entries: read.entries.length,
1209
- chainvalid: true,
1210
- reason: read.reason,
1211
- ...(log.seal !== undefined ? { sealhash: log.seal.sealhash.current, sealedat: log.seal.sealedat } : {}),
1212
- log: read.entries,
1213
- };
1214
- }
1215
-
1216
- /* ── Merged from phishguard.ts ── */
1217
-
1218
- import type { phishverdict } from "./types.js";
1219
-
1220
- /**
1221
- * Phishguard logic of the 1.1.62 family.
1222
- * The lookalike watch lives here: before a credential step runs on a new origin, the guard compares the login target against every granted origin and measures the lookalike distance between their host labels, so a lookalike origin that sits close to a granted origin under the user threshold blocks the step and the deny event names the matched known origin.
1223
- * The threshold stays the user's choice between zero and one with no engine default, and the distance reads the host label order from the registrable side so a padded subdomain never hides a lookalike.
1224
- */
1225
-
1226
- /** Reads whether one step is a login or credential step: the credential kind families and the field shapes that carry passwords, tokens and secrets mark the step for the phishguard watch. */
1227
- export function credentialstep(step: Pick<toolstep, "kind" | "target" | "value" | "options">): boolean {
1228
- const credentialkinds: ReadonlySet<string> = new Set([
1229
- "consentpassword",
1230
- "saveapikey",
1231
- "handleauth",
1232
- "authflow",
1233
- "fillcard",
1234
- "fillcode",
1235
- ]);
1236
- if (credentialkinds.has(step.kind)) return true;
1237
- const options = stepoptions(step);
1238
- const fields = Array.isArray(options.fields)
1239
- ? (options.fields.filter((item) => Boolean(item) && typeof item === "object") as Array<Record<string, unknown>>)
1240
- : [];
1241
- const names = [
1242
- ...fields.map((field) => (typeof field.name === "string" ? field.name : "")),
1243
- typeof options.field === "string" ? options.field : "",
1244
- step.target ?? "",
1245
- ].map((name) => name.toLowerCase());
1246
- return names.some(
1247
- (name) =>
1248
- name.includes("password") ||
1249
- name.includes("passwd") ||
1250
- name.includes("passphrase") ||
1251
- name.includes("token") ||
1252
- name.includes("secret") ||
1253
- name.includes("apikey"),
1254
- );
1255
- }
1256
-
1257
- /** Splits one origin into its host labels from the registrable side: the comparison reads com.example.pay from https://pay.example.com so the distance weighs the distinguishing labels first. */
1258
- export function originlabels(origin: string): string[] {
1259
- const host =
1260
- origin
1261
- .trim()
1262
- .replace(/^[a-z][a-z0-9+.-]*:\/\//i, "")
1263
- .split("/")[0] ?? "";
1264
- return host
1265
- .split(".")
1266
- .filter((label) => label !== "")
1267
- .reverse();
1268
- }
1269
-
1270
- /** Reads the edit distance between two label sequences; the guard uses it over the registrable ordered labels. */
1271
- function labeldistance(one: string[], two: string[]): number {
1272
- const rows = one.length + 1;
1273
- const columns = two.length + 1;
1274
- let previous = Array.from({ length: columns }, (_, index) => index);
1275
- for (let row = 1; row < rows; row += 1) {
1276
- const current = [row, ...Array.from({ length: columns - 1 }, () => 0)];
1277
- for (let column = 1; column < columns; column += 1) {
1278
- const substitution = (previous[column - 1] ?? 0) + (one[row - 1] === two[column - 1] ? 0 : 1);
1279
- current[column] = Math.min((previous[column] ?? 0) + 1, (current[column - 1] ?? 0) + 1, substitution);
1280
- }
1281
- previous = current;
1282
- }
1283
- return previous[columns - 1] ?? Math.max(one.length, two.length);
1284
- }
1285
-
1286
- /** Measures the normalized lookalike distance between two origins in the zero to one range: identical origins carry zero, and every label edit moves the distance toward one. */
1287
- export function lookalikedistance(one: string, two: string): number {
1288
- if (one.trim() === "" || two.trim() === "") return 1;
1289
- if (one === two) return 0;
1290
- const first = originlabels(one);
1291
- const second = originlabels(two);
1292
- const edits = labeldistance(first, second);
1293
- const longest = Math.max(first.length, second.length);
1294
- if (longest === 0) return 1;
1295
- const distance = edits / longest;
1296
- return Math.min(1, Math.max(0, distance));
1297
- }
1298
-
1299
- /** Validates the phishguard threshold as a user choice between zero and one: no engine default exists and the boundary the user picks stays the only block line. */
1300
- export function phishthresholdvalid(threshold: number): { valid: boolean; reason: string } {
1301
- if (!Number.isFinite(threshold) || threshold <= 0 || threshold >= 1)
1302
- return {
1303
- valid: false,
1304
- reason: "The phishguard threshold stays a user choice between zero and one; the lookalike line never defaults.",
1305
- };
1306
- return {
1307
- valid: true,
1308
- reason: `The lookalike threshold ${threshold} stays the user configured line a login origin crosses at its own risk.`,
1309
- };
1310
- }
1311
-
1312
- /** Renders one phishguard verdict for a login origin: the closest granted origin carries the distance, and a distance that crosses the user threshold blocks the step while the deny event names the matched known origin. */
1313
- export function phishverdictof(input: {
1314
- origin: string;
1315
- granted: string[];
1316
- threshold: number;
1317
- now: number;
1318
- }): phishverdict {
1319
- const threshold = phishthresholdvalid(input.threshold);
1320
- if (!threshold.valid) throw new Error(threshold.reason);
1321
- if (input.granted.includes(input.origin)) {
1322
- return {
1323
- origin: input.origin,
1324
- distance: 0,
1325
- threshold: input.threshold,
1326
- blocked: false,
1327
- reason: `The login origin ${input.origin} sits among the granted origins; no lookalike watch applies.`,
1328
- at: input.now,
1329
- };
1330
- }
1331
- let matchedorigin: string | undefined;
1332
- let distance = 1;
1333
- for (const granted of input.granted) {
1334
- const candidate = lookalikedistance(input.origin, granted);
1335
- if (candidate < distance) {
1336
- distance = candidate;
1337
- matchedorigin = granted;
1338
- }
1339
- }
1340
- if (matchedorigin !== undefined && distance <= input.threshold) {
1341
- return {
1342
- origin: input.origin,
1343
- matchedorigin,
1344
- distance,
1345
- threshold: input.threshold,
1346
- blocked: true,
1347
- reason: `The login origin ${input.origin} sits ${distance} away from the granted origin ${matchedorigin} and crosses the user threshold ${input.threshold}; the credential step blocks and the deny event names ${matchedorigin}.`,
1348
- at: input.now,
1349
- };
1350
- }
1351
- return {
1352
- origin: input.origin,
1353
- ...(matchedorigin !== undefined ? { matchedorigin } : {}),
1354
- distance,
1355
- threshold: input.threshold,
1356
- blocked: false,
1357
- reason:
1358
- matchedorigin !== undefined
1359
- ? `The login origin ${input.origin} sits ${distance} away from its closest granted origin ${matchedorigin} and stays under the user threshold ${input.threshold}.`
1360
- : `The login origin ${input.origin} carries no granted origin to resemble; the watch records the first visit.`,
1361
- at: input.now,
1362
- };
1363
- }
1364
-
1365
- /** Reads whether one stored phishguard verdict stays inside its freshness window: a verdict past the window leaves the live set while its record survives for the audit trail. */
1366
- export function verdictfresh(verdict: phishverdict, now: number, freshness: number | undefined): boolean {
1367
- if (freshness === undefined) return true;
1368
- return now - verdict.at < freshness;
1369
- }
1370
-
1371
- /** Builds the plain language phishguard notice the surfaces show before a login step: the verdict, the matched origin and the threshold in one line. */
1372
- export function phishnotetext(verdict: phishverdict): string {
1373
- if (verdict.blocked) return verdict.reason;
1374
- if (verdict.matchedorigin !== undefined)
1375
- return `The login origin ${verdict.origin} sits ${verdict.distance} from the granted origin ${verdict.matchedorigin}, under the user threshold ${verdict.threshold}.`;
1376
- return `The login origin ${verdict.origin} has no granted lookalike under the user threshold ${verdict.threshold}.`;
1377
- }
1378
-
1379
- /* ── Merged from secretvault.ts ── */
1380
-
1381
- import type { secretvaultentry } from "./types.js";
1382
-
1383
- /**
1384
- * Secret vault logic of the 1.1.62 family.
1385
- * The vault seam lives here: secrets enter through put and leave through fetch at the last possible moment before a credential field, while the persisted records carry only the label, the exact origin scope, the profile workspace, the provenance and the sha-256 verification digest — no plaintext value ever reaches a storage writer, a log entry, a memory record or an export path.
1386
- * The scan paths refuse secrets in plain sight: a step option, a variable or a plan text whose value digests to a stored vault entry is a leak that refuses the plan, and a raw typed value behind a masked field shape never rides a step at all.
1387
- * Honest limit: the browser offers no vault hardware, so the seam rides the session storage area the background wires, which keeps values in memory only and never persists them to disk; the persistence layer stores metadata alone, mirroring the immutablelog seam honesty.
1388
- */
1389
-
1390
- /** The digest prefix every stored vault digest carries so raw values never persist anywhere. */
1391
- export const vaultdigestprefix = "sha256:";
1392
-
1393
- /** Derives the sha-256 verification digest of one secret value through the platform crypto seam; the digest persists while the value never does. */
1394
- export async function vaultdigestof(value: string): Promise<string> {
1395
- const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(value));
1396
- return vaultdigestprefix + [...new Uint8Array(digest)].map((byte) => byte.toString(16).padStart(2, "0")).join("");
1397
- }
1398
-
1399
- /** Builds one vault record from its metadata: the label, the exact origin scope, the profile workspace and the provenance; the digest verifies the value behind the seam without ever carrying it. */
1400
- export function vaultentryof(input: {
1401
- label: string;
1402
- scope: string;
1403
- profileid: string;
1404
- provenance: "user" | "session";
1405
- digest: string;
1406
- now: number;
1407
- vaultid?: string;
1408
- }): secretvaultentry {
1409
- if (input.label.trim() === "") throw new Error("The vault record needs its label; the surfaces show the label only.");
1410
- if (input.scope.trim() === "")
1411
- throw new Error("The vault record needs its exact origin scope; a secret never rides every origin.");
1412
- if (!input.digest.startsWith(vaultdigestprefix))
1413
- throw new Error("The vault record carries its sha-256 digest, never its value.");
1414
- return {
1415
- vaultid: input.vaultid ?? randomid(),
1416
- label: input.label.trim(),
1417
- scope: input.scope.trim(),
1418
- profileid: input.profileid,
1419
- provenance: input.provenance,
1420
- algorithm: "sha-256",
1421
- digest: input.digest,
1422
- createdat: input.now,
1423
- };
1424
- }
1425
-
1426
- /** Creates an in memory vault seam: values live in the map only, which is the honest fallback the background wraps with the session storage area that never persists to disk. */
1427
- export function inmemoryvault(): {
1428
- put(vaultid: string, value: string): Promise<void>;
1429
- fetch(vaultid: string): Promise<string | undefined>;
1430
- drop(vaultid: string): Promise<void>;
1431
- } {
1432
- const values = new Map<string, string>();
1433
- return {
1434
- put: async (vaultid, value) => {
1435
- values.set(vaultid, value);
1436
- },
1437
- fetch: async (vaultid) => values.get(vaultid),
1438
- drop: async (vaultid) => {
1439
- values.delete(vaultid);
1440
- },
1441
- };
1442
- }
1443
-
1444
- /** Stores one secret behind the vault seam and returns its metadata record alone: the value enters the seam and no return path carries it. */
1445
- export async function vaultstore(input: {
1446
- seam: { put(vaultid: string, value: string): Promise<void> };
1447
- label: string;
1448
- scope: string;
1449
- profileid: string;
1450
- provenance: "user" | "session";
1451
- value: string;
1452
- now: number;
1453
- }): Promise<secretvaultentry> {
1454
- if (input.value === "")
1455
- throw new Error("The vault stores a secret value the user supplied; an empty value stores nothing.");
1456
- const entry = vaultentryof({
1457
- label: input.label,
1458
- scope: input.scope,
1459
- profileid: input.profileid,
1460
- provenance: input.provenance,
1461
- digest: await vaultdigestof(input.value),
1462
- now: input.now,
1463
- });
1464
- await input.seam.put(entry.vaultid, input.value);
1465
- return entry;
1466
- }
1467
-
1468
- /** Reads one secret from the vault seam at the last possible moment before its credential field: the value returns to the dispatching caller only, never to a log writer, and the record stamps its last use. */
1469
- export async function vaultvaluefor(input: {
1470
- seam: { fetch(vaultid: string): Promise<string | undefined> };
1471
- entry: secretvaultentry;
1472
- }): Promise<{ ok: boolean; value?: string; reason: string; used?: secretvaultentry }> {
1473
- const value = await input.seam.fetch(input.entry.vaultid);
1474
- if (value === undefined)
1475
- return {
1476
- ok: false,
1477
- reason: `The vault holds no value behind the label ${input.entry.label}; add the secret again.`,
1478
- };
1479
- return {
1480
- ok: true,
1481
- value,
1482
- reason: `The vault released the value behind the label ${input.entry.label} at the last possible moment; the value reaches the credential field only and no log records it.`,
1483
- };
1484
- }
1485
-
1486
- /** Drops one secret from the vault seam; the metadata record leaves with it because a deleted secret leaves no trace a surface could read. */
1487
- export async function vaultdelete(input: {
1488
- seam: { drop(vaultid: string): Promise<void> };
1489
- entry: secretvaultentry;
1490
- }): Promise<{ dropped: boolean; label: string; reason: string }> {
1491
- await input.seam.drop(input.entry.vaultid);
1492
- return {
1493
- dropped: true,
1494
- label: input.entry.label,
1495
- reason: `The vault dropped the secret ${input.entry.label} of ${input.entry.scope}; no value and no copy remains behind the seam.`,
1496
- };
1497
- }
1498
-
1499
- /** Reads whether one vault record covers an origin: the scope binds to exactly one origin, so a secret of one site never rides another site. */
1500
- export function vaultcovers(entry: secretvaultentry, origin: string): boolean {
1501
- return entry.scope === origin;
1502
- }
1503
-
1504
- /** Scans candidate step values, variables and plan texts for leaked secrets: a candidate whose sha-256 digest matches a stored vault digest is a leak the plan refuses, because a plaintext secret outside the vault never rides a step, a variable or a plan. */
1505
- export async function secretleakscan(input: {
1506
- candidates: string[];
1507
- entries: secretvaultentry[];
1508
- }): Promise<{ leaks: string[]; reason: string }> {
1509
- const leaks: string[] = [];
1510
- for (const candidate of input.candidates) {
1511
- if (candidate.trim() === "") continue;
1512
- const digest = await vaultdigestof(candidate);
1513
- if (input.entries.some((entry) => entry.digest === digest)) leaks.push(candidate);
1514
- }
1515
- if (leaks.length > 0)
1516
- return {
1517
- leaks,
1518
- reason: `The plan carries ${leaks.length} plaintext value${leaks.length === 1 ? "" : "s"} that digest to vault records; secrets never ride step options, variables or plan texts, only the vault holds them.`,
1519
- };
1520
- return { leaks: [], reason: "No candidate value digests to a vault record; the plan carries no leaked secret." };
1521
- }
1522
-
1523
- /** Reads whether one step carries a raw typed value behind a masked field shape: a credential step names its vault label instead, because a raw password or token value never rides a step option. */
1524
- export function secretshapecarrying(step: Pick<toolstep, "kind" | "target" | "value" | "options">): {
1525
- carries: boolean;
1526
- reason: string;
1527
- } {
1528
- const options = stepoptions(step);
1529
- const fields = Array.isArray(options.fields)
1530
- ? (options.fields.filter((item) => Boolean(item) && typeof item === "object") as Array<Record<string, unknown>>)
1531
- : [];
1532
- const rawfield = fields.find(
1533
- (field) =>
1534
- typeof field.name === "string" &&
1535
- typeof field.value === "string" &&
1536
- field.value !== "" &&
1537
- maskingfield(field.name, []),
1538
- );
1539
- if (rawfield !== undefined)
1540
- return {
1541
- carries: true,
1542
- reason: `The ${step.kind} step types a raw value into the ${String(rawfield.name)} field; credential steps read their value from the vault at the last possible moment and never carry it in the options.`,
1543
- };
1544
- if (
1545
- typeof options.field === "string" &&
1546
- maskingfield(options.field, []) &&
1547
- step.value !== undefined &&
1548
- step.value !== ""
1549
- )
1550
- return {
1551
- carries: true,
1552
- reason: `The ${step.kind} step types a raw value into the ${options.field} field; credential steps read their value from the vault at the last possible moment and never carry it in the options.`,
1553
- };
1554
- return { carries: false, reason: "The step carries no raw value behind a masked field shape." };
1555
- }
1556
-
1557
- /** Builds the vault view the surfaces render: the labels, scopes, provenance and last use times only, because no surface ever displays or exports a secret value. */
1558
- export function vaultview(
1559
- entries: secretvaultentry[],
1560
- ): Array<{
1561
- vaultid: string;
1562
- label: string;
1563
- scope: string;
1564
- provenance: string;
1565
- createdat: number;
1566
- lastusedat?: number;
1567
- }> {
1568
- return entries.map((entry) => ({
1569
- vaultid: entry.vaultid,
1570
- label: entry.label,
1571
- scope: entry.scope,
1572
- provenance: entry.provenance,
1573
- createdat: entry.createdat,
1574
- ...(entry.lastusedat !== undefined ? { lastusedat: entry.lastusedat } : {}),
1575
- }));
1576
- }
1577
-
1578
- /** Builds the plain language prompt of one credential step: the label the vault carries, never the value. */
1579
- export function vaultprompttext(entry: secretvaultentry, origin: string): string {
1580
- return `Use the credential ${entry.label} of ${entry.scope} on ${origin}? The value stays behind the vault and no surface ever displays it.`;
1581
- }
1582
-
1583
- /* ── Merged from transparency.ts ── */
1584
-
1585
- import type { permdiffrecord, transparencygrant } from "./types.js";
1586
-
1587
- /**
1588
- * Transparency logic of the 1.1.62 family.
1589
- * The transparencypage grammar lives here: every active grant reads as one row with its origin, scope and boundary, every consent window ever granted stays listed with its expiry even after it closed, the connectallow entries list with their senders, and the permdiff of each installed update records the added and removed permissions between two versions.
1590
- * The revoke actions stay one descriptor per grant row, because every listed grant revokes from the same page that shows it.
1591
- */
1592
-
1593
- /** Computes the permdiff between two permission versions: the permissions the new version added, the permissions it removed and the computed time; an unchanged set carries empty lists while the record still lands for the audit trail. */
1594
- export function permissiondiff(input: {
1595
- from: string[];
1596
- to: string[];
1597
- fromversion: string;
1598
- toversion: string;
1599
- now: number;
1600
- }): permdiffrecord {
1601
- if (input.fromversion.trim() === "" || input.toversion.trim() === "")
1602
- throw new Error("The permdiff names the two versions it compares.");
1603
- const added = [...new Set(input.to.filter((permission) => !input.from.includes(permission)))];
1604
- const removed = [...new Set(input.from.filter((permission) => !input.to.includes(permission)))];
1605
- return { fromversion: input.fromversion, toversion: input.toversion, added, removed, computedat: input.now };
1606
- }
1607
-
1608
- /** Reads whether one permdiff record reports a change between its versions; an unchanged permission set still records for the audit trail. */
1609
- export function permdiffchanged(diff: permdiffrecord): boolean {
1610
- return diff.added.length > 0 || diff.removed.length > 0;
1611
- }
1612
-
1613
- /** Builds the plain language summary of one permdiff record for the transparencypage. */
1614
- export function permdiffsummary(diff: permdiffrecord): string {
1615
- if (!permdiffchanged(diff)) return `The update from ${diff.fromversion} to ${diff.toversion} changed no permission.`;
1616
- const parts: string[] = [];
1617
- if (diff.added.length > 0) parts.push(`added ${diff.added.join(", ")}`);
1618
- if (diff.removed.length > 0) parts.push(`removed ${diff.removed.join(", ")}`);
1619
- return `The update from ${diff.fromversion} to ${diff.toversion} ${parts.join(" and ")}.`;
1620
- }
1621
-
1622
- /** Builds the transparency grant rows of every active grant: each allowlist origin carries its profile workspace scope and its grant time, and each originprofile carries its per kind scope. */
1623
- export function transparencygrants(input: {
1624
- allowlist: automationallowlistentry[];
1625
- profiles: originprofile[];
1626
- }): transparencygrant[] {
1627
- const grants: transparencygrant[] = input.allowlist.map((entry) => ({
1628
- origin: entry.origin,
1629
- scope: `automation allowlist of the profile workspace ${entry.profileid}`,
1630
- boundary: "the user revokes the entry or the profile workspace",
1631
- grantedat: entry.grantedat,
1632
- }));
1633
- for (const profile of input.profiles) {
1634
- grants.push({
1635
- origin: profile.origin,
1636
- scope: `origin profile with ${profile.grants.length} granted and ${profile.denials.length} denied kinds`,
1637
- boundary: "the user edits or revokes the profile",
1638
- grantedat: profile.createdat,
1639
- });
1640
- }
1641
- return grants;
1642
- }
1643
-
1644
- /** Builds the revoke action descriptor of one transparency grant row: every listed grant revokes from the same page that shows it. */
1645
- export function revokeaction(grant: transparencygrant): { action: "revoke"; origin: string; scope: string } {
1646
- return { action: "revoke", origin: grant.origin, scope: grant.scope };
1647
- }
1648
-
1649
- /** Lists every consent window ever granted with its expiry: closed windows stay listed beside the active ones because the transparency page shows the whole grant history. */
1650
- export function windowhistory(
1651
- windows: consentwindow[],
1652
- ): Array<{ id: string; origin: string; state: string; boundary: string; startedat: number; expiresat: number }> {
1653
- return windows.map((window) => ({
1654
- id: window.id,
1655
- origin: window.origin,
1656
- state: window.state,
1657
- boundary: window.boundary,
1658
- startedat: window.startedat,
1659
- expiresat: window.expiresat,
1660
- }));
1661
- }
1662
-
1663
- /** Lists the connectallow entries with their senders: the display names, the sender ids and the origin scopes the user allowed. */
1664
- export function connectallowlist(
1665
- entries: connectallowentry[],
1666
- ): Array<{ senderid: string; displayname: string; origin?: string; addedat: number }> {
1667
- return entries.map((entry) => ({
1668
- senderid: entry.senderid,
1669
- displayname: entry.displayname,
1670
- ...(entry.origin !== undefined ? { origin: entry.origin } : {}),
1671
- addedat: entry.addedat,
1672
- }));
1673
- }