@wenathlan/devthink 2.0.15 → 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 +45 -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/net.ts DELETED
@@ -1,1214 +0,0 @@
1
- /**
2
- * The net module of the 1.1.90 consolidation: every correlated variation of the network logic interned in this one file, so the module family carries one surface without duplicate variations.
3
- * The correlation is the network plane of a run, ordered from observation through transport to control: netwatch holds the derived request observation of the run tab (the timing buffer facts, the correlation ids that join request and response pairs, the header allowlists with redaction, the body capture inside reviewed byte ceilings and the page api map with its ranking and replay extraction); socketbus holds the transport plane (the websocket channel lifecycle with reconnection backoff, the multiplexed named message streams with their channel scoped sequences, the message filter matching of waitmessage steps, the server sent events parsing with last event id resume and the long polling cursor loop with its stop conditions); and netcontrol holds the control plane (request blocking, response mocking, header rewriting, cookie scoping, proxy routing, the rate limit reads and the retry after waits, every rule run scoped and reverted the moment a run ends, fails or is cancelled).
4
- * No byte ceiling, reconnect budget, backoff base, rate window or poll bound is ever hardcoded anywhere in the family: every bound stays the user's choice, header values redact before storage, body bytes never enter any audit summary, and no network control rule ever bypasses the human review.
5
- */
6
-
7
- /* ── Merged from netwatch.ts ── */
8
-
9
- import type {
10
- apimapentry,
11
- apireplayspec,
12
- bodyfilter,
13
- bodyrecord,
14
- exchangerecord,
15
- headerfilter,
16
- responseentry,
17
- } from "./types.js";
18
- import { readpath } from "./http.js";
19
-
20
- /**
21
- * Network observation part two watch logics for the 1.1.43 family.
22
- * Every correlated rule for the derived request observation of the run tab, the correlation ids that join request and response pairs, the header reading with allowlists and redaction, the response body capture inside reviewed byte ceilings, the page api mapping with frequency, json share and payload stability ranking and the api replay extraction paths lives in this file.
23
- * The page observation derives from the timing and navigation buffers the page itself exposes, so request and response headers and bodies exist only for exchanges captured through the extension context; header values are redacted before storage and body bytes never enter any audit summary.
24
- */
25
-
26
- /** The netwatch kinds, listed among the available capabilities of every proposal request. */
27
- export const netwatchkinds: string[] = ["watchrequests", "readheaders", "capturebodies", "mapapi", "extractapi"];
28
-
29
- /** One derived request fact of the page timing buffers: url, initiator, timing, transfer size, protocol and the status a navigation entry exposes. */
30
- export interface resourcefact {
31
- url: string;
32
- initiator: string;
33
- entrytype: string;
34
- start: number;
35
- duration: number;
36
- transfer: number;
37
- protocol: string;
38
- status?: number;
39
- /** Explicit failure flag a navigation buffer reports, for example a net error. */
40
- failed?: boolean;
41
- }
42
-
43
- /** Normalizes raw timing buffer entries into derived request facts; navigation entries contribute the response status they expose. */
44
- export function resourcefacts(entries: Array<Record<string, unknown>>): resourcefact[] {
45
- const facts: resourcefact[] = [];
46
- for (const entry of entries) {
47
- const url = typeof entry.name === "string" ? entry.name : "";
48
- if (!url) continue;
49
- const entrytype = typeof entry.entryType === "string" ? entry.entryType : "resource";
50
- if (entrytype !== "resource" && entrytype !== "navigation") continue;
51
- facts.push({
52
- url,
53
- initiator: typeof entry.initiatorType === "string" ? entry.initiatorType : "",
54
- entrytype,
55
- start: typeof entry.startTime === "number" && Number.isFinite(entry.startTime) ? entry.startTime : 0,
56
- duration: typeof entry.duration === "number" && Number.isFinite(entry.duration) ? entry.duration : 0,
57
- transfer: typeof entry.transferSize === "number" && Number.isFinite(entry.transferSize) ? entry.transferSize : 0,
58
- protocol: typeof entry.nextHopProtocol === "string" ? entry.nextHopProtocol : "",
59
- ...(typeof entry.responseStatus === "number" && Number.isInteger(entry.responseStatus)
60
- ? { status: entry.responseStatus }
61
- : {}),
62
- ...(entry.failed === true ? { failed: true } : {}),
63
- });
64
- }
65
- return facts;
66
- }
67
-
68
- /** Classifies one derived request failure: an exposed status of four hundred or more is an http error and an explicit failure flag or a zero transfer fetch without a protocol is a network error; everything else reports no error class. */
69
- export function failureclass(fact: resourcefact): { errorclass?: "httperror" | "networkerror"; status: number } {
70
- if (fact.status !== undefined && fact.status >= 400) return { errorclass: "httperror", status: fact.status };
71
- if (fact.failed === true) return { errorclass: "networkerror", status: 0 };
72
- if (
73
- (fact.initiator === "fetch" || fact.initiator === "xmlhttprequest") &&
74
- fact.duration > 0 &&
75
- fact.transfer === 0 &&
76
- fact.protocol === ""
77
- )
78
- return { errorclass: "networkerror", status: 0 };
79
- return { status: fact.status ?? 0 };
80
- }
81
-
82
- /** Assigns one correlation id per run request, ordered by first observation. */
83
- export function correlationid(runid: string, index: number): string {
84
- return `${runid}-${index + 1}`;
85
- }
86
-
87
- /** Creates one observed exchange record from a derived request fact: the method stays honest with a question mark for fetch initiators the timing buffers cannot name, and the status and error class come from what the buffers expose. */
88
- export function newexchange(input: {
89
- id: string;
90
- runid: string;
91
- stepid: string;
92
- correlationid: string;
93
- fact: resourcefact;
94
- at: number;
95
- }): exchangerecord {
96
- const verdict = failureclass(input.fact);
97
- let origin = "";
98
- try {
99
- origin = new URL(input.fact.url).origin;
100
- } catch {
101
- origin = "";
102
- }
103
- const method = input.fact.initiator === "fetch" || input.fact.initiator === "xmlhttprequest" ? "?" : "GET";
104
- const statusclass =
105
- verdict.status >= 100 && verdict.status < 600
106
- ? verdict.status >= 200 && verdict.status < 300
107
- ? "success"
108
- : verdict.status >= 300 && verdict.status < 400
109
- ? "redirect"
110
- : verdict.status >= 400 && verdict.status < 500
111
- ? "clienterror"
112
- : verdict.status >= 500
113
- ? "servererror"
114
- : "informational"
115
- : "unknown";
116
- return {
117
- id: input.id,
118
- runid: input.runid,
119
- stepid: input.stepid,
120
- correlationid: input.correlationid,
121
- url: input.fact.url,
122
- origin,
123
- method,
124
- status: verdict.status,
125
- statusclass,
126
- ...(verdict.errorclass !== undefined ? { errorclass: verdict.errorclass } : {}),
127
- source: "page",
128
- ...(input.fact.initiator ? { initiator: input.fact.initiator } : {}),
129
- timing: Math.round(input.fact.duration),
130
- bytes: input.fact.transfer,
131
- at: input.at,
132
- };
133
- }
134
-
135
- /** Joins one captured response entry onto its exchange through the shared correlation id; a mismatching correlation id is refused instead of paired. */
136
- export function pairexchange(exchange: exchangerecord, response: responseentry): exchangerecord {
137
- if (exchange.correlationid !== response.correlationid)
138
- throw new Error(
139
- `The response ${response.correlationid} does not pair with the exchange ${exchange.correlationid}.`,
140
- );
141
- return {
142
- ...exchange,
143
- status: response.status,
144
- statusclass:
145
- response.status >= 200 && response.status < 300
146
- ? "success"
147
- : response.status >= 400 && response.status < 500
148
- ? "clienterror"
149
- : response.status >= 500
150
- ? "servererror"
151
- : response.status >= 300 && response.status < 400
152
- ? "redirect"
153
- : "unknown",
154
- bytes: response.bytes,
155
- ...(response.mime !== undefined ? { mime: response.mime } : {}),
156
- ...(response.bodyref !== undefined ? { bodyref: response.bodyref } : {}),
157
- ...(Object.keys(response.headers).length > 0 ? { responseheaders: response.headers } : {}),
158
- };
159
- }
160
-
161
- /** Filters observed exchanges by run, origin and status; the status filter accepts one status code or the failed class of every exchange with an error class. */
162
- export function filterexchanges(
163
- exchanges: exchangerecord[],
164
- filter: { runid?: string; origin?: string; status?: number | "failed" },
165
- ): exchangerecord[] {
166
- return exchanges.filter(
167
- (exchange) =>
168
- (filter.runid === undefined || exchange.runid === filter.runid) &&
169
- (filter.origin === undefined || exchange.origin === filter.origin) &&
170
- (filter.status === undefined ||
171
- (filter.status === "failed" ? exchange.errorclass !== undefined : exchange.status === filter.status)),
172
- );
173
- }
174
-
175
- /** Normalizes a reviewed header filter: the name allowlist and the redaction list, both lowercased; the redaction list is required before any header value is stored. */
176
- export function headerfilterof(value: unknown): headerfilter {
177
- if (!value || typeof value !== "object" || Array.isArray(value)) return { allow: [], redact: [] };
178
- const entry = value as Record<string, unknown>;
179
- const names = (source: unknown): string[] =>
180
- Array.isArray(source)
181
- ? source
182
- .filter((name): name is string => typeof name === "string" && name.trim().length > 0)
183
- .map((name) => name.trim().toLowerCase())
184
- : [];
185
- return { allow: names(entry.allow), redact: names(entry.redact) };
186
- }
187
-
188
- /** Applies one header filter to captured headers: only allowlisted names survive and redaction listed names keep their name with the value replaced by the redaction marker before storage. */
189
- export function capturedheaders(headers: Record<string, string>, filter: headerfilter): Record<string, string> {
190
- const result: Record<string, string> = {};
191
- for (const [name, value] of Object.entries(headers)) {
192
- const key = name.trim().toLowerCase();
193
- if (filter.allow.length > 0 && !filter.allow.includes(key)) continue;
194
- result[key] = filter.redact.includes(key) ? "[redacted]" : value;
195
- }
196
- return result;
197
- }
198
-
199
- /** Normalizes a reviewed body filter: url pattern, mime list and the byte ceiling of one captured body; the ceiling stays a user configured value with no code ceiling. */
200
- export function bodyfilterof(value: unknown): bodyfilter {
201
- if (!value || typeof value !== "object" || Array.isArray(value)) return {};
202
- const entry = value as Record<string, unknown>;
203
- const filter: bodyfilter = {};
204
- if (typeof entry.urlpattern === "string" && entry.urlpattern.trim()) filter.urlpattern = entry.urlpattern.trim();
205
- if (Array.isArray(entry.mimes))
206
- filter.mimes = entry.mimes
207
- .filter((mime): mime is string => typeof mime === "string" && mime.trim().length > 0)
208
- .map((mime) => mime.trim().toLowerCase());
209
- if (typeof entry.ceiling === "number" && Number.isFinite(entry.ceiling) && entry.ceiling >= 0)
210
- filter.ceiling = entry.ceiling;
211
- return filter;
212
- }
213
-
214
- /** True when one exchange matches the reviewed body filter: the url pattern appears inside the exchange url and the mime list, when reviewed, carries the exchange mime. */
215
- export function bodymatches(filter: bodyfilter, exchange: { url: string; mime?: string }): boolean {
216
- if (filter.urlpattern !== undefined && !exchange.url.includes(filter.urlpattern)) return false;
217
- if (filter.mimes !== undefined && filter.mimes.length > 0) {
218
- const mime = ((exchange.mime ?? "").split(";")[0] ?? "").trim().toLowerCase();
219
- if (!filter.mimes.includes(mime)) return false;
220
- }
221
- return true;
222
- }
223
-
224
- /** Mime types whose bodies carry private document or data payloads; a capturebodies step that lists one of them grades sensitive. */
225
- const privatemimes = new Set([
226
- "text/html",
227
- "text/plain",
228
- "text/xml",
229
- "application/xml",
230
- "application/json",
231
- "text/json",
232
- "application/x-www-form-urlencoded",
233
- "application/graphql",
234
- "multipart/form-data",
235
- ]);
236
-
237
- /** True when a mime type carries private document or data payloads instead of public static assets. */
238
- export function privatemime(mime: string): boolean {
239
- return privatemimes.has((mime.split(";")[0] ?? "").trim().toLowerCase());
240
- }
241
-
242
- /** Captures one response body inside the reviewed byte ceiling: the stored body truncates at the ceiling and the record keeps the stored byte size with its correlation linkage. */
243
- export function capturebody(input: {
244
- ref: string;
245
- runid: string;
246
- exchange: exchangerecord;
247
- body: string;
248
- mime: string;
249
- filter: bodyfilter;
250
- at: number;
251
- }): { record: bodyrecord; truncated: boolean } | { refused: string } {
252
- if (!bodymatches(input.filter, input.exchange))
253
- return { refused: `The exchange ${input.exchange.correlationid} does not match the reviewed body filter.` };
254
- const ceiling = input.filter.ceiling;
255
- const stored = ceiling !== undefined && input.body.length > ceiling ? input.body.slice(0, ceiling) : input.body;
256
- return {
257
- record: {
258
- ref: input.ref,
259
- runid: input.runid,
260
- correlationid: input.exchange.correlationid,
261
- url: input.exchange.url,
262
- mime: input.mime,
263
- bytes: stored.length,
264
- body: stored,
265
- at: input.at,
266
- },
267
- truncated: stored.length < input.body.length,
268
- };
269
- }
270
-
271
- /** Reads the payload shape of one captured body: the field names of a json object in first seen order, with array bodies reporting the shape of their first element. */
272
- export function payloadshapeof(body: string | undefined): string[] {
273
- if (body === undefined) return [];
274
- try {
275
- const parsed = JSON.parse(body) as unknown;
276
- const shape = (value: unknown): string[] =>
277
- value && typeof value === "object" && !Array.isArray(value) ? Object.keys(value as Record<string, unknown>) : [];
278
- if (Array.isArray(parsed)) return parsed.length > 0 ? shape(parsed[0]) : [];
279
- return shape(parsed);
280
- } catch {
281
- return [];
282
- }
283
- }
284
-
285
- /** True when one exchange looks like a page api call: a fetch or xmlhttprequest initiator, a captured body or an api shaped url path. */
286
- function isapicandidate(exchange: exchangerecord): boolean {
287
- if (exchange.initiator === "fetch" || exchange.initiator === "xmlhttprequest") return true;
288
- if (exchange.bodyref !== undefined) return true;
289
- try {
290
- return /\/api\/|\/graphql|\.json($|\?)|\/v\d+\//i.test(new URL(exchange.url).pathname);
291
- } catch {
292
- return false;
293
- }
294
- }
295
-
296
- /** Builds the page api map of the run from observed exchanges and captured bodies: endpoint, method, mime, frequency, json share and payload stability with their correlation ids. */
297
- export function apientries(exchanges: exchangerecord[], bodies: bodyrecord[]): apimapentry[] {
298
- const bodybyref = new Map(bodies.map((body) => [body.correlationid, body]));
299
- const groups = new Map<
300
- string,
301
- {
302
- endpoint: string;
303
- method: string;
304
- origin: string;
305
- mimes: Map<string, number>;
306
- frequency: number;
307
- json: number;
308
- captured: number;
309
- shapes: Map<string, number>;
310
- correlationids: string[];
311
- }
312
- >();
313
- for (const exchange of exchanges) {
314
- if (!isapicandidate(exchange)) continue;
315
- let endpoint = exchange.url;
316
- let origin = exchange.origin;
317
- try {
318
- const parsed = new URL(exchange.url);
319
- endpoint = `${parsed.origin}${parsed.pathname}`;
320
- origin = parsed.origin;
321
- } catch {
322
- /* an unparsable url keeps the raw endpoint */
323
- }
324
- const key = `${exchange.method} ${endpoint}`;
325
- const group = groups.get(key) ?? {
326
- endpoint,
327
- method: exchange.method,
328
- origin,
329
- mimes: new Map<string, number>(),
330
- frequency: 0,
331
- json: 0,
332
- captured: 0,
333
- shapes: new Map<string, number>(),
334
- correlationids: [],
335
- };
336
- group.frequency += 1;
337
- group.correlationids.push(exchange.correlationid);
338
- const body = exchange.bodyref !== undefined ? bodybyref.get(exchange.correlationid) : undefined;
339
- const mime = body?.mime ?? exchange.mime ?? "";
340
- group.mimes.set(mime, (group.mimes.get(mime) ?? 0) + 1);
341
- if (body !== undefined) {
342
- group.captured += 1;
343
- const shape = payloadshapeof(body.body);
344
- if (shape.length > 0) group.json += 1;
345
- const shapekey = shape.join(",");
346
- group.shapes.set(shapekey, (group.shapes.get(shapekey) ?? 0) + 1);
347
- }
348
- groups.set(key, group);
349
- }
350
- return [...groups.values()].map((group) => {
351
- const mime = [...group.mimes.entries()].sort((left, right) => right[1] - left[1])[0]?.[0] ?? "";
352
- const modalshape = [...group.shapes.entries()].sort((left, right) => right[1] - left[1])[0];
353
- return {
354
- endpoint: group.endpoint,
355
- method: group.method,
356
- mime,
357
- frequency: group.frequency,
358
- payloadshape: (modalshape?.[0] ?? "").split(",").filter(Boolean),
359
- jsonshare: group.captured > 0 ? group.json / group.captured : 0,
360
- stability: group.captured > 0 ? (modalshape?.[1] ?? 0) / group.captured : 0,
361
- origin: group.origin,
362
- correlationids: group.correlationids,
363
- };
364
- });
365
- }
366
-
367
- /** Ranks page api entries by frequency, json share and payload stability so the most used stable json endpoints surface first. */
368
- export function rankapis(entries: apimapentry[]): apimapentry[] {
369
- const score = (entry: apimapentry): number => entry.frequency * (1 + entry.jsonshare + entry.stability);
370
- return [...entries].sort((left, right) => score(right) - score(left) || right.frequency - left.frequency);
371
- }
372
-
373
- /** Normalizes a reviewed api replay spec: the captured endpoint, the replay verb, the parameter overrides and the dotted extraction paths. */
374
- export function apireplayspecof(value: unknown): apireplayspec | undefined {
375
- if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
376
- const entry = value as Record<string, unknown>;
377
- if (typeof entry.endpoint !== "string" || !entry.endpoint.trim()) return undefined;
378
- const spec: apireplayspec = { endpoint: entry.endpoint.trim() };
379
- if (typeof entry.verb === "string" && entry.verb.trim()) spec.verb = entry.verb.trim().toUpperCase();
380
- if (
381
- entry.overrides !== undefined &&
382
- entry.overrides !== null &&
383
- typeof entry.overrides === "object" &&
384
- !Array.isArray(entry.overrides)
385
- ) {
386
- const overrides: Record<string, string> = {};
387
- for (const [name, override] of Object.entries(entry.overrides as Record<string, unknown>)) {
388
- if (typeof override === "string") overrides[name] = override;
389
- }
390
- spec.overrides = overrides;
391
- }
392
- if (Array.isArray(entry.paths))
393
- spec.paths = entry.paths.filter((path): path is string => typeof path === "string" && path.trim().length > 0);
394
- return spec;
395
- }
396
-
397
- /** Builds the replay url of one captured endpoint with the reviewed parameter overrides applied as query parameters. */
398
- export function replayurl(spec: apireplayspec): string {
399
- const url = new URL(spec.endpoint);
400
- for (const [name, value] of Object.entries(spec.overrides ?? {})) url.searchParams.set(name, value);
401
- return url.toString();
402
- }
403
-
404
- /** Maps one replayed response body onto the reviewed dotted extraction paths, reporting a miss instead of crashing when a path finds nothing. */
405
- export function extractvalues(
406
- body: string,
407
- paths: string[],
408
- ): Array<{ path: string; value?: unknown; missing?: boolean }> {
409
- let parsed: unknown;
410
- try {
411
- parsed = JSON.parse(body);
412
- } catch {
413
- return paths.map((path) => ({ path, missing: true }));
414
- }
415
- const fields = readpath(
416
- parsed,
417
- paths.map((path) => ({ name: path, path, kind: "json" as const })),
418
- );
419
- return fields.map((field) => ({
420
- path: field.path,
421
- ...(field.value !== undefined ? { value: field.value } : {}),
422
- ...(field.missing ? { missing: true } : {}),
423
- }));
424
- }
425
-
426
- /* ── Merged from socketbus.ts ── */
427
-
428
- import type {
429
- channelrecord,
430
- channelstate,
431
- messageenvelope,
432
- messagefilter,
433
- pollcursor,
434
- socketoptions,
435
- } from "./types.js";
436
-
437
- /**
438
- * Network observation part two socket logics for the 1.1.43 family.
439
- * Every correlated rule for the websocket channel lifecycle with reconnection backoff, the multiplexing of named message streams over one channel with channel scoped sequence numbers, the message filter matching of waitmessage steps, the server sent events parsing with last event id resume and the long polling cursor loop with its stop conditions lives in this file.
440
- * Message payloads are opaque reviewed text; no payload value enters any audit summary built from these results.
441
- */
442
-
443
- /** The socket and stream kinds, listed among the available capabilities of every proposal request. */
444
- export const socketkinds: string[] = ["opensocket", "sendmessage", "waitmessage", "subscribesse", "longpoll"];
445
-
446
- /** Connect seam signature: one reviewed channel url with its protocols; the extension executor wires the browser websocket, tests wire plain fixtures. */
447
- export type socketconnect = (
448
- url: string,
449
- protocols: string[],
450
- ) => Promise<{ open: boolean; code?: number; error?: string }>;
451
-
452
- /** Resolves the reviewed https origin behind a channel url: wss channels map onto their https origin so the origin grants cover both. */
453
- export function channelorigin(url: string): string {
454
- try {
455
- const parsed = new URL(url);
456
- const protocol = parsed.protocol === "wss:" ? "https:" : parsed.protocol === "ws:" ? "http:" : parsed.protocol;
457
- return `${protocol}//${parsed.host}`;
458
- } catch {
459
- return "";
460
- }
461
- }
462
-
463
- /** Normalizes reviewed channel open options: url plus the socket options of protocols, reconnect budget, backoff base and backoff ceiling; every bound stays a user choice with no code ceiling. */
464
- export function channeloptionsof(value: unknown): { url: string; options: socketoptions } | undefined {
465
- if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
466
- const entry = value as Record<string, unknown>;
467
- if (typeof entry.url !== "string" || !entry.url.trim()) return undefined;
468
- const options: socketoptions = {};
469
- if (Array.isArray(entry.protocols))
470
- options.protocols = entry.protocols.filter(
471
- (item): item is string => typeof item === "string" && item.trim().length > 0,
472
- );
473
- if (typeof entry.reconnect === "number" && Number.isFinite(entry.reconnect)) options.reconnect = entry.reconnect;
474
- if (typeof entry.backoff === "number" && Number.isFinite(entry.backoff)) options.backoff = entry.backoff;
475
- if (typeof entry.backoffceiling === "number" && Number.isFinite(entry.backoffceiling))
476
- options.backoffceiling = entry.backoffceiling;
477
- if (typeof entry.lifetime === "number" && Number.isFinite(entry.lifetime)) options.lifetime = entry.lifetime;
478
- return { url: entry.url.trim(), options };
479
- }
480
-
481
- /** Creates one channel record in the connecting state with its origin, protocols and zeroed message counters. */
482
- export function newchannel(input: {
483
- id: string;
484
- runid: string;
485
- stepid: string;
486
- kind: "websocket" | "sse";
487
- url: string;
488
- protocols?: string[];
489
- at: number;
490
- }): channelrecord {
491
- return {
492
- id: input.id,
493
- runid: input.runid,
494
- stepid: input.stepid,
495
- kind: input.kind,
496
- url: input.url,
497
- origin: channelorigin(input.url),
498
- state: "connecting",
499
- openedat: input.at,
500
- sent: 0,
501
- received: 0,
502
- reconnects: 0,
503
- ...(input.protocols !== undefined && input.protocols.length > 0 ? { protocols: [...input.protocols] } : {}),
504
- };
505
- }
506
-
507
- /** Builds the exponential backoff waits of the reviewed reconnect budget: each wait doubles the base and growth stops at the user configured ceiling, so the budget bounds the growth and no code ceiling exists. */
508
- export function reconnectwaits(attempts: number, base: number, ceiling: number | undefined): number[] {
509
- const count = Math.max(0, Math.floor(attempts));
510
- const waits: number[] = [];
511
- let wait = Math.max(0, base);
512
- for (let index = 0; index < count; index += 1) {
513
- waits.push(wait);
514
- const next = wait * 2;
515
- wait = ceiling !== undefined && Number.isFinite(ceiling) && ceiling >= 0 ? Math.min(next, ceiling) : next;
516
- }
517
- return waits;
518
- }
519
-
520
- /**
521
- * Opens one reviewed channel through the connect seam with the reviewed reconnect budget and backoff: failed attempts wait the exponential backoff before the retry, the channel opens on the first success and a budget exhausted without a connection fails the channel with the last error class.
522
- */
523
- export async function openchannel(input: {
524
- record: channelrecord;
525
- options: socketoptions;
526
- connect: socketconnect;
527
- sleep?: (milliseconds: number) => Promise<void>;
528
- now?: () => number;
529
- }): Promise<channelrecord> {
530
- const sleep =
531
- input.sleep ??
532
- ((milliseconds: number): Promise<void> => new Promise((resolve) => setTimeout(resolve, Math.max(0, milliseconds))));
533
- const now = input.now ?? Date.now;
534
- const attempts = Math.max(1, Math.floor(input.options.reconnect ?? 0) + 1);
535
- const waits = reconnectwaits(attempts - 1, input.options.backoff ?? 0, input.options.backoffceiling);
536
- let record: channelrecord = { ...input.record, state: "connecting" };
537
- let lasterror = "";
538
- for (let attempt = 0; attempt < attempts; attempt += 1) {
539
- try {
540
- const result = await input.connect(record.url, record.protocols ?? []);
541
- if (result.open) return { ...record, state: "open", openedat: now() };
542
- lasterror = result.error ?? `closed with code ${result.code ?? 0}`;
543
- } catch (error) {
544
- lasterror = error instanceof Error ? error.message : String(error);
545
- }
546
- if (attempt < attempts - 1) {
547
- const wait = waits[attempt] ?? 0;
548
- if (wait > 0) await sleep(wait);
549
- record = { ...record, reconnects: record.reconnects + 1 };
550
- }
551
- }
552
- return { ...record, state: "failed", error: lasterror };
553
- }
554
-
555
- /** Closes one channel record cleanly with its close time and an optional error class, keeping every counter for the audit trail. */
556
- export function closechannel(record: channelrecord, at: number, error?: string): channelrecord {
557
- const state: channelstate = error !== undefined ? "failed" : "closed";
558
- return { ...record, state, closedat: at, ...(error !== undefined ? { error } : {}) };
559
- }
560
-
561
- /** Live multiplexing state of one run: the per channel sequence counters and the inbound message queue. */
562
- export interface busstate {
563
- sequences: Record<string, number>;
564
- queue: messageenvelope[];
565
- }
566
-
567
- /** Tags one message with the channel scoped sequence number: every stream multiplexed over the channel shares one strictly increasing sequence. */
568
- function tagmessage(
569
- state: busstate,
570
- channelid: string,
571
- stream: string,
572
- payload: string,
573
- at: number,
574
- ): { state: busstate; envelope: messageenvelope } {
575
- const sequence = (state.sequences[channelid] ?? 0) + 1;
576
- const envelope: messageenvelope = { channelid, stream, payload, sequence, at };
577
- return { state: { sequences: { ...state.sequences, [channelid]: sequence }, queue: state.queue }, envelope };
578
- }
579
-
580
- /** Publishes one reviewed payload on a named stream of an open channel and returns the tagged envelope. */
581
- export function publishmessage(
582
- state: busstate,
583
- channelid: string,
584
- stream: string,
585
- payload: string,
586
- at: number,
587
- ): { state: busstate; envelope: messageenvelope } {
588
- return tagmessage(state, channelid, stream, payload, at);
589
- }
590
-
591
- /** Receives one inbound channel message on a named stream, tags it with the next channel sequence and queues it for the waitmessage matchers. */
592
- export function receivemessage(
593
- state: busstate,
594
- channelid: string,
595
- stream: string,
596
- payload: string,
597
- at: number,
598
- ): { state: busstate; envelope: messageenvelope } {
599
- const tagged = tagmessage(state, channelid, stream, payload, at);
600
- return { state: { ...tagged.state, queue: [...state.queue, tagged.envelope] }, envelope: tagged.envelope };
601
- }
602
-
603
- /** Resolves one dotted path segment against an object or an array index. */
604
- function pathstep(current: unknown, segment: string): unknown {
605
- if (Array.isArray(current) && /^\d+$/.test(segment)) return current[Number.parseInt(segment, 10)];
606
- if (current && typeof current === "object" && !Array.isArray(current))
607
- return (current as Record<string, unknown>)[segment];
608
- return undefined;
609
- }
610
-
611
- /** True when one message envelope matches the reviewed filter: the stream name when reviewed, the dotted json path when reviewed and always the sequence integrity of the channel. */
612
- export function matchmessage(filter: messagefilter | undefined, envelope: messageenvelope): boolean {
613
- if (!filter) return true;
614
- if (filter.stream !== undefined && filter.stream !== envelope.stream) return false;
615
- if (filter.path !== undefined) {
616
- try {
617
- const parsed = JSON.parse(envelope.payload) as unknown;
618
- let current: unknown = parsed;
619
- let missing = false;
620
- for (const segment of filter.path.split(".")) {
621
- const next = pathstep(current, segment);
622
- if (next === undefined) {
623
- missing = true;
624
- break;
625
- }
626
- current = next;
627
- }
628
- if (missing) return false;
629
- } catch {
630
- return false;
631
- }
632
- }
633
- return true;
634
- }
635
-
636
- /** Collects the queued messages of one channel matching the reviewed filter up to the reviewed match limit, removing the matched envelopes from the queue. */
637
- export function collectmessages(
638
- state: busstate,
639
- channelid: string,
640
- filter: messagefilter | undefined,
641
- ): { state: busstate; matched: messageenvelope[] } {
642
- const limit =
643
- filter?.limit !== undefined && Number.isFinite(filter.limit) && filter.limit >= 1
644
- ? Math.floor(filter.limit)
645
- : Number.POSITIVE_INFINITY;
646
- const matched: messageenvelope[] = [];
647
- const queue: messageenvelope[] = [];
648
- for (const envelope of state.queue) {
649
- if (envelope.channelid === channelid && matched.length < limit && matchmessage(filter, envelope))
650
- matched.push(envelope);
651
- else queue.push(envelope);
652
- }
653
- return { state: { sequences: state.sequences, queue }, matched };
654
- }
655
-
656
- /** Verifies the sequence integrity of a message stream: per channel the sequences must form one strictly increasing run from one with no gaps. */
657
- export function sequenceintegrity(envelopes: messageenvelope[]): {
658
- ok: boolean;
659
- gaps: Array<{ channelid: string; expected: number; found: number }>;
660
- } {
661
- const last = new Map<string, number>();
662
- const gaps: Array<{ channelid: string; expected: number; found: number }> = [];
663
- for (const envelope of envelopes) {
664
- const expected = (last.get(envelope.channelid) ?? 0) + 1;
665
- if (envelope.sequence !== expected)
666
- gaps.push({ channelid: envelope.channelid, expected, found: envelope.sequence });
667
- last.set(envelope.channelid, Math.max(envelope.sequence, expected));
668
- }
669
- return { ok: gaps.length === 0, gaps };
670
- }
671
-
672
- /** Normalizes a reviewed message filter: stream name, dotted json path and the reviewed match limit with no code ceiling. */
673
- export function messagefilterof(value: unknown): messagefilter {
674
- if (!value || typeof value !== "object" || Array.isArray(value)) return {};
675
- const entry = value as Record<string, unknown>;
676
- const filter: messagefilter = {};
677
- if (typeof entry.stream === "string" && entry.stream.trim()) filter.stream = entry.stream.trim();
678
- if (typeof entry.path === "string" && entry.path.trim()) filter.path = entry.path.trim();
679
- if (typeof entry.limit === "number" && Number.isFinite(entry.limit) && entry.limit >= 1)
680
- filter.limit = Math.floor(entry.limit);
681
- return filter;
682
- }
683
-
684
- /** One parsed server sent event: id, event name, joined data lines and the reviewed retry hint. */
685
- export interface sseevent {
686
- id?: string;
687
- event?: string;
688
- data: string;
689
- retry?: number;
690
- }
691
-
692
- /** Parses one server sent events text chunk into complete events with id, event name, data and retry fields, returning the incomplete trailing block as the rest buffer; comment lines stay ignored. */
693
- export function parsessetext(text: string): { events: sseevent[]; rest: string } {
694
- const separator = text.lastIndexOf("\n\n");
695
- const complete = separator === -1 ? "" : text.slice(0, separator + 2);
696
- const rest = separator === -1 ? text : text.slice(separator + 2);
697
- const events: sseevent[] = [];
698
- for (const block of complete.split(/\n\n/)) {
699
- const id: string[] = [];
700
- const names: string[] = [];
701
- const data: string[] = [];
702
- let retry: number | undefined;
703
- for (const line of block.split("\n")) {
704
- if (line === "" || line.startsWith(":")) continue;
705
- const colon = line.indexOf(":");
706
- const field = colon === -1 ? line : line.slice(0, colon);
707
- let value = colon === -1 ? "" : line.slice(colon + 1);
708
- if (value.startsWith(" ")) value = value.slice(1);
709
- if (field === "id" && value !== "") id.push(value);
710
- if (field === "event" && value !== "") names.push(value);
711
- if (field === "data") data.push(value);
712
- if (field === "retry" && /^\d+$/.test(value)) retry = Number.parseInt(value, 10);
713
- }
714
- if (id.length === 0 && names.length === 0 && data.length === 0) continue;
715
- events.push({
716
- ...(id.length > 0 ? { id: id[id.length - 1] } : {}),
717
- ...(names.length > 0 ? { event: names[names.length - 1] } : {}),
718
- data: data.join("\n"),
719
- ...(retry !== undefined ? { retry } : {}),
720
- });
721
- }
722
- return { events, rest };
723
- }
724
-
725
- /** Builds the request headers of one server sent events subscription, resuming from the last event id after a reconnect. */
726
- export function sserequestheaders(record: { lasteventid?: string }): Record<string, string> {
727
- return {
728
- accept: "text/event-stream",
729
- ...(record.lasteventid !== undefined && record.lasteventid !== "" ? { "last-event-id": record.lasteventid } : {}),
730
- };
731
- }
732
-
733
- /** Normalizes reviewed subscription options: the event stream url, the lifetime window, the resume point and the reviewed cancellation path. */
734
- export function subscriptionoptionsof(
735
- value: unknown,
736
- ):
737
- | {
738
- url: string;
739
- lifetime?: number;
740
- lasteventid?: string;
741
- cancel: { kind: "stop" | "lifetime"; value: string | number };
742
- }
743
- | undefined {
744
- if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
745
- const entry = value as Record<string, unknown>;
746
- if (typeof entry.url !== "string" || !entry.url.trim()) return undefined;
747
- const cancel = entry.cancel;
748
- if (!cancel || typeof cancel !== "object" || Array.isArray(cancel)) return undefined;
749
- const cancelrecord = cancel as Record<string, unknown>;
750
- if (cancelrecord.kind !== "stop" && cancelrecord.kind !== "lifetime") return undefined;
751
- if (typeof cancelrecord.value !== "string" && typeof cancelrecord.value !== "number") return undefined;
752
- const result: {
753
- url: string;
754
- lifetime?: number;
755
- lasteventid?: string;
756
- cancel: { kind: "stop" | "lifetime"; value: string | number };
757
- } = { url: entry.url.trim(), cancel: { kind: cancelrecord.kind, value: cancelrecord.value } };
758
- if (typeof entry.lifetime === "number" && Number.isFinite(entry.lifetime) && entry.lifetime > 0)
759
- result.lifetime = entry.lifetime;
760
- if (typeof entry.lasteventid === "string" && entry.lasteventid.trim()) result.lasteventid = entry.lasteventid.trim();
761
- return result;
762
- }
763
-
764
- /** Normalizes a reviewed long poll cursor: url, cursor field, interval, stop condition, optional poll ceiling and the cursor query parameter; every bound stays a user choice with no code ceiling. */
765
- export function pollcursorof(value: unknown): pollcursor | undefined {
766
- if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
767
- const entry = value as Record<string, unknown>;
768
- if (typeof entry.url !== "string" || !entry.url.trim()) return undefined;
769
- if (typeof entry.cursorfield !== "string" || !entry.cursorfield.trim()) return undefined;
770
- if (typeof entry.interval !== "number" || !Number.isFinite(entry.interval) || entry.interval <= 0) return undefined;
771
- const stop = entry.stop;
772
- if (!stop || typeof stop !== "object" || Array.isArray(stop)) return undefined;
773
- const stoprecord = stop as Record<string, unknown>;
774
- if (typeof stoprecord.field !== "string" || !stoprecord.field.trim()) return undefined;
775
- if (typeof stoprecord.equals !== "string") return undefined;
776
- const cursor: pollcursor = {
777
- url: entry.url.trim(),
778
- cursorfield: entry.cursorfield.trim(),
779
- interval: entry.interval,
780
- stop: { field: stoprecord.field.trim(), equals: stoprecord.equals },
781
- };
782
- if (typeof entry.maxpolls === "number" && Number.isFinite(entry.maxpolls) && entry.maxpolls >= 1)
783
- cursor.maxpolls = Math.floor(entry.maxpolls);
784
- if (typeof entry.param === "string" && entry.param.trim()) cursor.param = entry.param.trim();
785
- return cursor;
786
- }
787
-
788
- /** Reads one cursor value from a polled response body by its dotted field path. */
789
- export function cursorfrom(response: unknown, field: string): string | undefined {
790
- let current: unknown = response;
791
- for (const segment of field.split(".")) {
792
- const next = pathstep(current, segment);
793
- if (next === undefined) return undefined;
794
- current = next;
795
- }
796
- return current === undefined || current === null ? undefined : String(current);
797
- }
798
-
799
- /** Builds the next poll url of a cursor loop: the cursor rides the reviewed query parameter when one is set and posts inside the request body otherwise. */
800
- export function pollurl(cursor: pollcursor, value: string | undefined): { url: string; body?: string } {
801
- if (cursor.param === undefined || value === undefined) {
802
- return {
803
- url: cursor.url,
804
- ...(value !== undefined ? { body: JSON.stringify({ [cursor.cursorfield]: value }) } : {}),
805
- };
806
- }
807
- const url = new URL(cursor.url);
808
- url.searchParams.set(cursor.param, value);
809
- return { url: url.toString() };
810
- }
811
-
812
- /**
813
- * Decides one long poll iteration: the loop stops on the reviewed stop condition, the cancellation flag, the plan expiry or the reviewed poll ceiling, and otherwise returns the next poll url with the reviewed interval wait.
814
- */
815
- export function polldecision(input: {
816
- cursor: pollcursor;
817
- polls: number;
818
- response: unknown;
819
- cancelled?: () => boolean;
820
- expiresat?: number;
821
- now: number;
822
- }): { continue: boolean; reason: string; cursor?: string; next?: { url: string; body?: string; wait: number } } {
823
- if (input.cancelled?.() === true) return { continue: false, reason: "The long poll loop was cancelled." };
824
- if (input.expiresat !== undefined && input.now >= input.expiresat)
825
- return { continue: false, reason: "The long poll loop stopped at the reviewed plan expiry." };
826
- const stopvalue = cursorfrom(input.response, input.cursor.stop.field);
827
- if (stopvalue !== undefined && stopvalue === input.cursor.stop.equals)
828
- return { continue: false, reason: `The stop condition matched ${input.cursor.stop.field} ${stopvalue}.` };
829
- if (input.cursor.maxpolls !== undefined && input.polls + 1 >= input.cursor.maxpolls)
830
- return {
831
- continue: false,
832
- reason: `The long poll loop reached the reviewed poll ceiling of ${input.cursor.maxpolls}.`,
833
- };
834
- const value = cursorfrom(input.response, input.cursor.cursorfield);
835
- const next = pollurl(input.cursor, value);
836
- return {
837
- continue: true,
838
- reason: "The long poll loop continues.",
839
- ...(value !== undefined ? { cursor: value } : {}),
840
- next: { ...next, wait: input.cursor.interval },
841
- };
842
- }
843
-
844
- /* ── Merged from netcontrol.ts ── */
845
-
846
- /** Correlated network control logics of the 1.1.44 family: request blocking, response mocking, header rewriting, cookie scoping, proxy routing and the rate limiter, all run scoped and reverted the moment a run ends, fails or is cancelled. */
847
- import type { blockrule, cookierecord, headerule, mockspec, proxyroute, ratelimitread } from "./types.js";
848
-
849
- /** Every reviewed network control kind of the 1.1.44 family. */
850
- export const controlkinds: string[] = [
851
- "blockrequest",
852
- "mockresponse",
853
- "rewriteheaders",
854
- "setcookies",
855
- "readcookies",
856
- "clearcookies",
857
- "authflow",
858
- "saveapikey",
859
- "routeproxy",
860
- "postform",
861
- "postfiles",
862
- ];
863
-
864
- /** Resolves the origin a reviewed url pattern names; a pattern without an https origin prefix names no origin and is refused. */
865
- export function patternorigin(pattern: string): string | undefined {
866
- const trimmed = pattern.trim();
867
- if (!trimmed.startsWith("https://")) return undefined;
868
- const rest = trimmed.slice("https://".length);
869
- const host = rest.split("/")[0] ?? "";
870
- if (!host.trim()) return undefined;
871
- return `https://${host.toLowerCase()}`;
872
- }
873
-
874
- /** Matches a reviewed url pattern against one request url: the pattern must name its https origin, then an optional path pattern where a single star matches within one segment and a double star matches across segments. */
875
- export function matchurlpattern(pattern: string, url: string): boolean {
876
- const origin = patternorigin(pattern);
877
- if (!origin) return false;
878
- let parsed: URL;
879
- try {
880
- parsed = new URL(url);
881
- } catch {
882
- return false;
883
- }
884
- if (parsed.origin !== origin) return false;
885
- const patternpath = pattern.trim().slice(origin.length);
886
- if (patternpath === "" || patternpath === "/") return true;
887
- const segments = patternpath.split("/").filter((segment) => segment !== "");
888
- if (segments.includes("**")) return true;
889
- const pathsegments = parsed.pathname.split("/").filter((segment) => segment !== "");
890
- if (segments.length !== pathsegments.length) return false;
891
- return segments.every(
892
- (segment, index) =>
893
- segment === pathsegments[index] ||
894
- (segment.includes("*") &&
895
- new RegExp(
896
- `^${segment
897
- .split("*")
898
- .map((part) => part.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"))
899
- .join(".*")}$`,
900
- ).test(pathsegments[index] ?? "")),
901
- );
902
- }
903
-
904
- /** Parses one reviewed block rule from step options: the url pattern and an optional resource type list. */
905
- export function blockruleof(value: unknown): { urlpattern: string; resourcetypes?: string[] } | undefined {
906
- if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
907
- const options = value as Record<string, unknown>;
908
- if (typeof options.urlpattern !== "string" || !options.urlpattern.trim()) return undefined;
909
- const rule: { urlpattern: string; resourcetypes?: string[] } = { urlpattern: options.urlpattern.trim() };
910
- if (Array.isArray(options.resourcetypes)) {
911
- const types = options.resourcetypes.filter(
912
- (item): item is string => typeof item === "string" && item.trim().length > 0,
913
- );
914
- if (types.length === 0) return undefined;
915
- rule.resourcetypes = types;
916
- }
917
- return rule;
918
- }
919
-
920
- /** Creates one registered block rule of a run with its hit counter at zero; every rule removes itself at run end. */
921
- export function newblockrule(input: {
922
- id: string;
923
- runid: string;
924
- stepid: string;
925
- urlpattern: string;
926
- resourcetypes?: string[];
927
- at: number;
928
- }): blockrule {
929
- return {
930
- id: input.id,
931
- runid: input.runid,
932
- stepid: input.stepid,
933
- urlpattern: input.urlpattern,
934
- ...(input.resourcetypes !== undefined ? { resourcetypes: input.resourcetypes } : {}),
935
- hits: 0,
936
- registeredat: input.at,
937
- };
938
- }
939
-
940
- /** Parses one reviewed mock fixture from step options: url pattern, status, headers and either the reviewed body or the reference of a captured body of the 1.1.43 body store the fixture replays. */
941
- export function mockspecof(
942
- value: unknown,
943
- ):
944
- | {
945
- urlpattern: string;
946
- status: number;
947
- headers?: Record<string, string>;
948
- body?: string;
949
- bodyref?: string;
950
- reviewed?: boolean;
951
- }
952
- | undefined {
953
- if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
954
- const options = value as Record<string, unknown>;
955
- if (typeof options.urlpattern !== "string" || !options.urlpattern.trim()) return undefined;
956
- if (
957
- typeof options.status !== "number" ||
958
- !Number.isInteger(options.status) ||
959
- options.status < 100 ||
960
- options.status > 599
961
- )
962
- return undefined;
963
- const hasbody = typeof options.body === "string";
964
- const bodyref = typeof options.bodyref === "string" ? options.bodyref.trim() : "";
965
- if (!hasbody && bodyref === "") return undefined;
966
- const spec: {
967
- urlpattern: string;
968
- status: number;
969
- headers?: Record<string, string>;
970
- body?: string;
971
- bodyref?: string;
972
- reviewed?: boolean;
973
- } = { urlpattern: options.urlpattern.trim(), status: options.status };
974
- if (hasbody) spec.body = options.body as string;
975
- if (bodyref !== "") spec.bodyref = bodyref;
976
- if (options.headers && typeof options.headers === "object" && !Array.isArray(options.headers))
977
- spec.headers = options.headers as Record<string, string>;
978
- if (options.reviewed === true) spec.reviewed = true;
979
- return spec;
980
- }
981
-
982
- /** Creates one registered mock fixture of a run; the fixture serves matched requests and clears at run end. */
983
- export function newmockspec(input: {
984
- id: string;
985
- runid: string;
986
- stepid: string;
987
- urlpattern: string;
988
- status: number;
989
- headers?: Record<string, string>;
990
- body?: string;
991
- bodyref?: string;
992
- reviewed: boolean;
993
- at: number;
994
- }): mockspec {
995
- return {
996
- id: input.id,
997
- runid: input.runid,
998
- stepid: input.stepid,
999
- urlpattern: input.urlpattern,
1000
- status: input.status,
1001
- ...(input.headers !== undefined ? { headers: input.headers } : {}),
1002
- ...(input.body !== undefined ? { body: input.body } : {}),
1003
- ...(input.bodyref !== undefined ? { bodyref: input.bodyref } : {}),
1004
- reviewed: input.reviewed,
1005
- hits: 0,
1006
- registeredat: input.at,
1007
- };
1008
- }
1009
-
1010
- /** Returns the first active mock fixture that matches the url; matched fixtures count their hits for the step result. */
1011
- export function mockfor(url: string, specs: mockspec[]): mockspec | undefined {
1012
- return specs.find((spec) => spec.revertedat === undefined && matchurlpattern(spec.urlpattern, url));
1013
- }
1014
-
1015
- /** Parses one reviewed header rewrite rule from step options: url pattern, header name, operation and value. */
1016
- export function headeruleof(
1017
- value: unknown,
1018
- ): { urlpattern: string; name: string; operation: "set" | "append" | "remove"; value?: string } | undefined {
1019
- if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
1020
- const options = value as Record<string, unknown>;
1021
- if (typeof options.urlpattern !== "string" || !options.urlpattern.trim()) return undefined;
1022
- if (typeof options.name !== "string" || !options.name.trim()) return undefined;
1023
- if (options.operation !== "set" && options.operation !== "append" && options.operation !== "remove") return undefined;
1024
- if (options.operation === "remove" && options.value !== undefined) return undefined;
1025
- if (options.operation !== "remove" && typeof options.value !== "string") return undefined;
1026
- const rule: { urlpattern: string; name: string; operation: "set" | "append" | "remove"; value?: string } = {
1027
- urlpattern: options.urlpattern.trim(),
1028
- name: options.name.trim(),
1029
- operation: options.operation,
1030
- };
1031
- if (options.operation !== "remove") rule.value = typeof options.value === "string" ? options.value : "";
1032
- return rule;
1033
- }
1034
-
1035
- /** Creates one registered header rewrite rule of a run; the rule reverts at run end. */
1036
- export function newheaderule(input: {
1037
- id: string;
1038
- runid: string;
1039
- stepid: string;
1040
- urlpattern: string;
1041
- name: string;
1042
- operation: "set" | "append" | "remove";
1043
- value?: string;
1044
- at: number;
1045
- }): headerule {
1046
- return {
1047
- id: input.id,
1048
- runid: input.runid,
1049
- stepid: input.stepid,
1050
- urlpattern: input.urlpattern,
1051
- name: input.name,
1052
- operation: input.operation,
1053
- ...(input.value !== undefined ? { value: input.value } : {}),
1054
- hits: 0,
1055
- registeredat: input.at,
1056
- };
1057
- }
1058
-
1059
- /** Applies the active header rewrite rules of one outgoing url: set overwrites, append joins with a comma and remove drops the header, returning the rewritten headers with the provenance of every applied rule for the audit trail. */
1060
- export function applyheaderules(
1061
- url: string,
1062
- headers: Record<string, string>,
1063
- rules: headerule[],
1064
- ): { headers: Record<string, string>; applied: headerule[] } {
1065
- const rewritten: Record<string, string> = { ...headers };
1066
- const applied: headerule[] = [];
1067
- for (const rule of rules) {
1068
- if (rule.revertedat !== undefined) continue;
1069
- if (!matchurlpattern(rule.urlpattern, url)) continue;
1070
- const name = rule.name;
1071
- if (rule.operation === "remove") {
1072
- delete rewritten[name];
1073
- applied.push(rule);
1074
- continue;
1075
- }
1076
- const value = rule.value ?? "";
1077
- if (rule.operation === "set") rewritten[name] = value;
1078
- else rewritten[name] = rewritten[name] !== undefined ? `${rewritten[name]}, ${value}` : value;
1079
- applied.push(rule);
1080
- }
1081
- return { headers: rewritten, applied };
1082
- }
1083
-
1084
- /** Marks one registered rule as reverted at the given time; reversion is idempotent and never touches the hit counter. */
1085
- export function revertrule<T extends blockrule | mockspec | headerule | proxyroute>(rule: T, at: number): T {
1086
- if (rule.revertedat !== undefined) return rule;
1087
- return { ...rule, revertedat: at };
1088
- }
1089
-
1090
- /** Parses one reviewed cookie record from step options: name, domain, path, value and optional expiry. */
1091
- export function cookierecordof(value: unknown): cookierecord | undefined {
1092
- if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
1093
- const options = value as Record<string, unknown>;
1094
- if (typeof options.name !== "string" || !options.name.trim()) return undefined;
1095
- if (typeof options.domain !== "string" || !options.domain.trim()) return undefined;
1096
- if (typeof options.path !== "string" || !options.path.trim()) return undefined;
1097
- if (typeof options.value !== "string") return undefined;
1098
- const record: cookierecord = {
1099
- name: options.name.trim(),
1100
- domain: options.domain.trim().toLowerCase(),
1101
- path: options.path.trim(),
1102
- value: options.value,
1103
- };
1104
- if (typeof options.expiresat === "number" && Number.isFinite(options.expiresat)) record.expiresat = options.expiresat;
1105
- return record;
1106
- }
1107
-
1108
- /** True when the reviewed cookie domain stays inside the granted hosts: the domain equals a granted origin host or sits beneath it as a subdomain; every other domain is refused. */
1109
- export function cookiedomaingranted(domain: string, grants: string[]): boolean {
1110
- const host = domain.trim().toLowerCase().replace(/^\./, "");
1111
- return grants.some((grant) => {
1112
- let granthost = "";
1113
- try {
1114
- granthost = new URL(grant).hostname.toLowerCase();
1115
- } catch {
1116
- return false;
1117
- }
1118
- return host === granthost || host.endsWith(`.${granthost}`);
1119
- });
1120
- }
1121
-
1122
- /** Redacts the values of reviewed cookies for the audit trail: only names, domains, paths and expiry windows survive. */
1123
- export function redactedcookies(
1124
- records: cookierecord[],
1125
- ): Array<{ name: string; domain: string; path: string; expiresat?: number }> {
1126
- return records.map((record) => ({
1127
- name: record.name,
1128
- domain: record.domain,
1129
- path: record.path,
1130
- ...(record.expiresat !== undefined ? { expiresat: record.expiresat } : {}),
1131
- }));
1132
- }
1133
-
1134
- /** Parses one reviewed proxy route from step options: scheme, host, port and the required bypass list of origins that stay direct. */
1135
- export function proxyrouteof(
1136
- value: unknown,
1137
- ): { scheme: "http" | "https" | "socks4" | "socks5"; host: string; port: number; bypass: string[] } | undefined {
1138
- if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
1139
- const options = value as Record<string, unknown>;
1140
- if (
1141
- options.scheme !== "http" &&
1142
- options.scheme !== "https" &&
1143
- options.scheme !== "socks4" &&
1144
- options.scheme !== "socks5"
1145
- )
1146
- return undefined;
1147
- if (typeof options.host !== "string" || !options.host.trim()) return undefined;
1148
- if (typeof options.port !== "number" || !Number.isInteger(options.port) || options.port < 1 || options.port > 65535)
1149
- return undefined;
1150
- if (
1151
- !Array.isArray(options.bypass) ||
1152
- options.bypass.length === 0 ||
1153
- !options.bypass.every((item): item is string => typeof item === "string" && item.trim().length > 0)
1154
- )
1155
- return undefined;
1156
- return {
1157
- scheme: options.scheme,
1158
- host: options.host.trim(),
1159
- port: options.port,
1160
- bypass: options.bypass.map((item) => item.trim()),
1161
- };
1162
- }
1163
-
1164
- /** Parses one rate limit read from response headers: the remaining and limit counts plus the reset time; a reset value beyond the read time counts as epoch seconds and a smaller value as seconds remaining. */
1165
- export function ratelimitreadof(
1166
- headers: Record<string, string>,
1167
- origin: string,
1168
- now: number,
1169
- ): ratelimitread | undefined {
1170
- const pick = (name: string): number | undefined => {
1171
- for (const key of Object.keys(headers)) {
1172
- if (key.toLowerCase() !== name) continue;
1173
- const value = Number(headers[key]);
1174
- return Number.isFinite(value) && value >= 0 ? value : undefined;
1175
- }
1176
- return undefined;
1177
- };
1178
- const remaining = pick("x-ratelimit-remaining");
1179
- const limit = pick("x-ratelimit-limit");
1180
- const reset = pick("x-ratelimit-reset");
1181
- if (remaining === undefined && limit === undefined && reset === undefined) return undefined;
1182
- const read: ratelimitread = {
1183
- origin,
1184
- ...(remaining !== undefined ? { remaining } : {}),
1185
- ...(limit !== undefined ? { limit } : {}),
1186
- resetat: now,
1187
- at: now,
1188
- };
1189
- if (reset !== undefined) read.resetat = reset > Math.floor(now / 1000) ? reset * 1000 : now + reset * 1000;
1190
- return read;
1191
- }
1192
-
1193
- /** Resolves the retry after wait of one 429 or 503 response in milliseconds from the reviewed Retry-After header; a seconds value counts from now and an HTTP date resolves to its absolute time. */
1194
- export function retryafterof(status: number, headers: Record<string, string>): number | undefined {
1195
- if (status !== 429 && status !== 503) return undefined;
1196
- for (const key of Object.keys(headers)) {
1197
- if (key.toLowerCase() !== "retry-after") continue;
1198
- const raw = headers[key];
1199
- if (raw === undefined) continue;
1200
- const value = raw.trim();
1201
- const seconds = Number(value);
1202
- if (Number.isFinite(seconds) && seconds >= 0) return seconds * 1000;
1203
- const date = Date.parse(value);
1204
- if (Number.isFinite(date)) return Math.max(0, date - Date.now());
1205
- return undefined;
1206
- }
1207
- return undefined;
1208
- }
1209
-
1210
- /** Resolves the rate limit wait in milliseconds until the reset window passes; an absent or passed state waits nothing and the wait itself stays a user configured behavior with no code ceiling. */
1211
- export function ratelimitwait(state: ratelimitread | undefined, now: number): number {
1212
- if (!state) return 0;
1213
- return Math.max(0, state.resetat - now);
1214
- }