@wenathlan/devthink 2.0.13 → 2.0.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (444) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/accounts.ts +2087 -0
  3. package/agent.ts +2331 -0
  4. package/antigravity.ts +2139 -0
  5. package/apifreeze.ts +4146 -0
  6. package/auth.ts +642 -0
  7. package/background.ts +16837 -0
  8. package/bridge.ts +2080 -0
  9. package/build.ts +54 -0
  10. package/bun.ts +26 -0
  11. package/capture.ts +1614 -0
  12. package/checksecrets.ts +72 -0
  13. package/cjs.ts +9 -0
  14. package/cli.ts +3500 -0
  15. package/commands.ts +1101 -0
  16. package/companion.ts +450 -0
  17. package/compatibility.ts +37 -0
  18. package/config.ts +3137 -0
  19. package/constants.ts +943 -0
  20. package/core.ts +1473 -0
  21. package/crossbrowser.ts +1359 -0
  22. package/dashdone.ts +605 -0
  23. package/data.ts +943 -0
  24. package/database.ts +133 -0
  25. package/debug.ts +850 -0
  26. package/deno.ts +27 -0
  27. package/devthink.ts +707 -0
  28. package/dist/accounts.d.ts +409 -0
  29. package/dist/agent.d.ts +837 -0
  30. package/dist/antigravity.d.ts +380 -0
  31. package/dist/apifreeze.d.ts +113 -0
  32. package/dist/auth.d.ts +224 -0
  33. package/{bridge.d.ts → dist/bridge.d.ts} +1 -1
  34. package/dist/capture.d.ts +595 -0
  35. package/dist/cjs.d.ts +6 -0
  36. package/{cli.js → dist/cli.js} +2 -2
  37. package/dist/commands.d.ts +310 -0
  38. package/dist/compatibility.d.ts +17 -0
  39. package/dist/config.d.ts +1853 -0
  40. package/dist/constants.d.ts +637 -0
  41. package/dist/core.d.ts +361 -0
  42. package/dist/data.d.ts +330 -0
  43. package/dist/database.d.ts +24 -0
  44. package/dist/debug.d.ts +270 -0
  45. package/dist/engine.d.ts +74 -0
  46. package/dist/environments.d.ts +244 -0
  47. package/dist/evidence.d.ts +277 -0
  48. package/dist/export.d.ts +311 -0
  49. package/dist/fingerprint.d.ts +594 -0
  50. package/dist/flow.d.ts +300 -0
  51. package/dist/gates.d.ts +103 -0
  52. package/dist/identity.d.ts +51 -0
  53. package/dist/ids.d.ts +7 -0
  54. package/dist/llm.d.ts +652 -0
  55. package/dist/models.d.ts +1807 -0
  56. package/dist/net.d.ts +325 -0
  57. package/dist/neutral.d.ts +41 -0
  58. package/dist/oauth.d.ts +703 -0
  59. package/dist/page.d.ts +1166 -0
  60. package/dist/perf.d.ts +481 -0
  61. package/dist/plan.d.ts +134 -0
  62. package/dist/project.d.ts +395 -0
  63. package/dist/providers.d.ts +37 -0
  64. package/dist/quota.d.ts +453 -0
  65. package/dist/recovery.d.ts +621 -0
  66. package/dist/request.d.ts +1492 -0
  67. package/dist/run.d.ts +779 -0
  68. package/dist/runtime.d.ts +212 -0
  69. package/dist/search.d.ts +375 -0
  70. package/dist/security.d.ts +534 -0
  71. package/dist/serve.d.ts +136 -0
  72. package/dist/session.d.ts +296 -0
  73. package/dist/storage.d.ts +53 -0
  74. package/dist/streaming.d.ts +1119 -0
  75. package/dist/swarm.d.ts +328 -0
  76. package/dist/tools.d.ts +170 -0
  77. package/dist/types.d.ts +8357 -0
  78. package/dist/utils.d.ts +82 -0
  79. package/dist/version.d.ts +7 -0
  80. package/dist/versionregistry.d.ts +406 -0
  81. package/dist/views.d.ts +579 -0
  82. package/dist/workbenchsession.d.ts +69 -0
  83. package/dist/workflow.d.ts +649 -0
  84. package/docs/readme.md +92 -0
  85. package/docs/schemas/antigravity.json +192 -0
  86. package/engine.ts +1524 -0
  87. package/environments.ts +1005 -0
  88. package/evidence.ts +643 -0
  89. package/export.ts +889 -0
  90. package/fingerprint.ts +1458 -0
  91. package/flow.ts +1190 -0
  92. package/gates.ts +307 -0
  93. package/gateway.ts +34 -0
  94. package/hardening.ts +191 -0
  95. package/headless.ts +369 -0
  96. package/http.ts +2110 -0
  97. package/identity.ts +246 -0
  98. package/ids.ts +26 -0
  99. package/index.ts +369 -0
  100. package/llm.ts +2682 -0
  101. package/mcp.ts +1835 -0
  102. package/{memory.js → memory.ts} +3834 -2310
  103. package/models.ts +3805 -0
  104. package/modes.ts +43 -0
  105. package/net.ts +1214 -0
  106. package/neutral.ts +362 -0
  107. package/node.ts +26 -0
  108. package/oauth.ts +2216 -0
  109. package/offscreen.ts +170 -0
  110. package/pack.ts +956 -0
  111. package/package.json +175 -82
  112. package/page.ts +7824 -0
  113. package/pagebridge.ts +1116 -0
  114. package/perf.ts +957 -0
  115. package/plan.ts +672 -0
  116. package/plugin.ts +76 -0
  117. package/{policy.js → policy.ts} +5073 -4679
  118. package/prisma.config.ts +27 -0
  119. package/progress.ts +1308 -0
  120. package/project.ts +1291 -0
  121. package/protocol.ts +4187 -0
  122. package/providers.ts +246 -0
  123. package/quota.ts +1887 -0
  124. package/recovery.ts +2433 -0
  125. package/request.ts +4482 -0
  126. package/run.ts +2135 -0
  127. package/runtime.ts +526 -0
  128. package/search.ts +1760 -0
  129. package/security.ts +1673 -0
  130. package/serve.ts +419 -0
  131. package/server.ts +5538 -0
  132. package/session.ts +1162 -0
  133. package/storage.ts +182 -0
  134. package/streaming.ts +4101 -0
  135. package/swarm.ts +884 -0
  136. package/sync.ts +56 -0
  137. package/tests/agentcert.test.ts +128 -0
  138. package/tests/agentfleet.test.ts +214 -0
  139. package/tests/agentreview.test.ts +241 -0
  140. package/tests/agentstream.test.ts +335 -0
  141. package/tests/agentwork.test.ts +356 -0
  142. package/tests/apifreeze.test.ts +290 -0
  143. package/tests/apimap.test.ts +112 -0
  144. package/tests/approvalgate.test.ts +175 -0
  145. package/tests/artifactmanifest.test.ts +204 -0
  146. package/tests/attentionfeed.test.ts +163 -0
  147. package/tests/backgroundruns.test.ts +100 -0
  148. package/tests/batchscheduling.test.ts +131 -0
  149. package/tests/blackboard.test.ts +238 -0
  150. package/tests/bridge.test.ts +1088 -0
  151. package/tests/browsercoverage.test.ts +134 -0
  152. package/tests/browserpolyfills.test.ts +124 -0
  153. package/tests/build.mjs +1260 -0
  154. package/tests/clientauth.test.ts +223 -0
  155. package/tests/clitools.test.ts +578 -0
  156. package/tests/confirmgates.test.ts +190 -0
  157. package/tests/containerpack.test.ts +143 -0
  158. package/tests/controlflow.test.ts +2120 -0
  159. package/tests/coordination.test.ts +345 -0
  160. package/tests/copyscreen.test.ts +139 -0
  161. package/tests/core.test.ts +417 -0
  162. package/tests/costcert.test.ts +95 -0
  163. package/tests/crossbrowser.test.ts +115 -0
  164. package/tests/cspaudit.test.ts +79 -0
  165. package/tests/dashdone.test.ts +615 -0
  166. package/tests/datagrid.test.ts +137 -0
  167. package/tests/doccheck.test.ts +107 -0
  168. package/tests/dombatch.test.ts +109 -0
  169. package/tests/emulation.test.ts +282 -0
  170. package/tests/environments.test.ts +259 -0
  171. package/tests/evidenceviews.test.ts +88 -0
  172. package/tests/exporttools.test.ts +185 -0
  173. package/tests/firefoxprep.test.ts +134 -0
  174. package/tests/flowlibrary.test.ts +242 -0
  175. package/tests/flowrun.test.ts +303 -0
  176. package/tests/forensics.test.ts +439 -0
  177. package/tests/gateway.test.ts +939 -0
  178. package/tests/headlesslib.test.ts +171 -0
  179. package/tests/httpstream.test.ts +277 -0
  180. package/tests/immutablelog.test.ts +157 -0
  181. package/tests/inboundguard.test.ts +177 -0
  182. package/tests/lazyload.test.ts +72 -0
  183. package/tests/librarymodes.test.ts +623 -0
  184. package/tests/llm.test.ts +696 -0
  185. package/tests/manifest.test.ts +226 -0
  186. package/tests/maskinputs.test.ts +150 -0
  187. package/tests/matrixverify.test.ts +111 -0
  188. package/tests/mavenpack.test.ts +75 -0
  189. package/tests/mcpserver.test.ts +704 -0
  190. package/tests/memory.test.ts +4155 -0
  191. package/tests/memorycare.test.ts +230 -0
  192. package/tests/migrateplan.test.ts +349 -0
  193. package/tests/minimization.test.ts +414 -0
  194. package/tests/modelroute.test.ts +112 -0
  195. package/tests/multiagent.test.ts +440 -0
  196. package/tests/nativebridge.test.ts +807 -0
  197. package/tests/navigation.test.ts +284 -0
  198. package/tests/nugetpack.test.ts +83 -0
  199. package/tests/orchestration.test.ts +449 -0
  200. package/tests/originpolicy.test.ts +405 -0
  201. package/tests/outputcompare.test.ts +97 -0
  202. package/tests/package.test.ts +91 -0
  203. package/tests/pagebridge.test.ts +5909 -0
  204. package/tests/pentest.test.ts +75 -0
  205. package/tests/perfrecords.test.ts +257 -0
  206. package/tests/permdiff.test.ts +85 -0
  207. package/tests/phishguard.test.ts +128 -0
  208. package/tests/pickerviews.test.ts +155 -0
  209. package/tests/pipelines.test.ts +381 -0
  210. package/tests/planlint.test.ts +240 -0
  211. package/tests/planreview.test.ts +383 -0
  212. package/tests/platformtargets.test.ts +56 -0
  213. package/tests/policy.test.ts +8284 -0
  214. package/tests/portability.test.ts +96 -0
  215. package/tests/progress.test.ts +1036 -0
  216. package/tests/promptlibrary.test.ts +116 -0
  217. package/tests/protocol.test.ts +4793 -0
  218. package/tests/publishingpipeline.test.ts +314 -0
  219. package/tests/quickactions.test.ts +164 -0
  220. package/tests/readiness.test.ts +78 -0
  221. package/tests/recipes.test.ts +163 -0
  222. package/tests/redactshots.test.ts +216 -0
  223. package/tests/registernvidiakeys.ts +51 -0
  224. package/tests/relayserve.test.ts +304 -0
  225. package/tests/releasenotes.test.ts +276 -0
  226. package/tests/resilience.test.ts +467 -0
  227. package/tests/resourceaware.test.ts +187 -0
  228. package/tests/runbudget.test.ts +125 -0
  229. package/tests/runhistory.test.ts +299 -0
  230. package/tests/runreplay.test.ts +113 -0
  231. package/tests/runresume.test.ts +184 -0
  232. package/tests/runstate.test.ts +288 -0
  233. package/tests/runtimeadapters.test.ts +251 -0
  234. package/tests/safariskeleton.test.ts +115 -0
  235. package/tests/sandboxframe.test.ts +196 -0
  236. package/tests/sbom.test.ts +105 -0
  237. package/tests/secretvault.test.ts +188 -0
  238. package/tests/selcache.test.ts +111 -0
  239. package/tests/sessioninterface.test.ts +791 -0
  240. package/tests/sessions.test.ts +591 -0
  241. package/tests/siteprefs.test.ts +228 -0
  242. package/tests/snapshotdelta.test.ts +165 -0
  243. package/tests/statusviews.test.ts +237 -0
  244. package/tests/stepmeter.test.ts +200 -0
  245. package/tests/streamparse.test.ts +163 -0
  246. package/tests/stylepruning.test.ts +90 -0
  247. package/tests/surfaces.test.ts +282 -0
  248. package/tests/sweep.test.ts +78 -0
  249. package/tests/syncbridge.test.ts +164 -0
  250. package/tests/taskqueue.test.ts +206 -0
  251. package/tests/toolcalls.test.ts +336 -0
  252. package/tests/toolcatalog.test.ts +201 -0
  253. package/tests/torture-agent.test.ts +706 -0
  254. package/tests/torture-auth.test.ts +461 -0
  255. package/tests/torture-commands.test.ts +895 -0
  256. package/tests/torture-data.test.ts +923 -0
  257. package/tests/torture-environments.test.ts +285 -0
  258. package/tests/torture-export.test.ts +263 -0
  259. package/tests/torture-http.test.ts +1379 -0
  260. package/tests/torture-llm.test.ts +808 -0
  261. package/tests/torture-memory.test.ts +1084 -0
  262. package/tests/torture-plan.test.ts +829 -0
  263. package/tests/torture-run.test.ts +626 -0
  264. package/tests/torture-session.test.ts +928 -0
  265. package/tests/torture-swarm.test.ts +1245 -0
  266. package/tests/torture-tools.test.ts +964 -0
  267. package/tests/torture-workflow.test.ts +2354 -0
  268. package/tests/tourviews.test.ts +52 -0
  269. package/tests/transparency.test.ts +147 -0
  270. package/tests/trigger.test.ts +941 -0
  271. package/tests/v1sunset.test.ts +428 -0
  272. package/tests/virtlist.test.ts +94 -0
  273. package/tests/vision.test.ts +540 -0
  274. package/tests/vsixpack.test.ts +145 -0
  275. package/tests/webapi.test.ts +523 -0
  276. package/tests/workflow.test.ts +1785 -0
  277. package/tests/workfloweditor.test.ts +759 -0
  278. package/tests/xpipack.test.ts +128 -0
  279. package/tools.ts +734 -0
  280. package/types.ts +9908 -0
  281. package/umd.ts +9 -0
  282. package/utils.ts +249 -0
  283. package/version.ts +8 -0
  284. package/versionregistry.ts +1310 -0
  285. package/views.ts +2153 -0
  286. package/vitest.config.ts +12 -0
  287. package/web/App.tsx +78 -0
  288. package/web/README.md +47 -0
  289. package/web/capacitor.config.ts +46 -0
  290. package/web/const.ts +19 -0
  291. package/web/control.shell.tsx +66 -0
  292. package/web/dashboardpage.ts +554 -0
  293. package/web/db.ts +493 -0
  294. package/web/design.html +541 -0
  295. package/web/gatewayview/Gateway.tsx +345 -0
  296. package/web/gatewayview/config.ts +1042 -0
  297. package/web/gatewayview/definition.ts +174 -0
  298. package/web/icons.ts +15 -0
  299. package/{manifest.json → web/manifest.json} +1 -1
  300. package/web/optionspage.ts +691 -0
  301. package/web/popup.ts +1651 -0
  302. package/web/schema.prisma +176 -0
  303. package/web/sidepanel.ts +7941 -0
  304. package/web/sitemanifest.json +13 -0
  305. package/web/terminal-ui.tsx +622 -0
  306. package/web/transparencypage.ts +541 -0
  307. package/web/vite.config.ts +39 -0
  308. package/web/workspace.ts +23 -0
  309. package/workbenchmemory.ts +104 -0
  310. package/workbenchsession.ts +304 -0
  311. package/workflow.ts +4495 -0
  312. package/bridge.d.ts.map +0 -1
  313. package/bridge.js +0 -1877
  314. package/bun.d.ts.map +0 -1
  315. package/bun.js +0 -52329
  316. package/caps/background.json +0 -712
  317. package/caps/cli.json +0 -360
  318. package/caps/library.json +0 -3611
  319. package/caps/mcp.json +0 -126
  320. package/caps/pagebridge.json +0 -377
  321. package/caps/popup.json +0 -51
  322. package/caps/sidepanel.json +0 -522
  323. package/checksums.txt +0 -142
  324. package/cli.d.ts.map +0 -1
  325. package/companion.d.ts.map +0 -1
  326. package/companion.js +0 -389
  327. package/crossbrowser.d.ts.map +0 -1
  328. package/crossbrowser.js +0 -1191
  329. package/dashdone.d.ts.map +0 -1
  330. package/dashdone.js +0 -368
  331. package/deno.d.ts.map +0 -1
  332. package/deno.js +0 -52325
  333. package/devthink.umd.js +0 -52346
  334. package/fixtures/bun.ts +0 -7
  335. package/fixtures/cjs.js +0 -6
  336. package/fixtures/deno.ts +0 -6
  337. package/fixtures/example-org-pagestate.json +0 -22
  338. package/fixtures/importers/automa-workflow.json +0 -18
  339. package/fixtures/importers/selenium-side.json +0 -20
  340. package/fixtures/importers/tabular-plan.csv +0 -7
  341. package/fixtures/importers/uivision-macro.json +0 -10
  342. package/fixtures/importers/v1-plan.json +0 -13
  343. package/fixtures/mcp-client.mjs +0 -14
  344. package/fixtures/pages/checkout.html +0 -192
  345. package/fixtures/pages/dashboard.html +0 -110
  346. package/fixtures/pages/feed.html +0 -113
  347. package/fixtures/pages/product-grid.html +0 -150
  348. package/fixtures/plans/form-inventory-plan.json +0 -11
  349. package/fixtures/plans/release-notes-plan.json +0 -11
  350. package/fixtures/recipes/agentscompete.json +0 -11
  351. package/fixtures/recipes/agentsforms.json +0 -11
  352. package/fixtures/recipes/agentsmonitor.json +0 -11
  353. package/fixtures/recipes/agentsreview.json +0 -29
  354. package/fixtures/recipes/agentswarm.json +0 -11
  355. package/fixtures/recipes/fillcalendar.json +0 -12
  356. package/fixtures/recipes/fillcheckout.json +0 -13
  357. package/fixtures/recipes/filldependentdropdowns.json +0 -12
  358. package/fixtures/recipes/fillfromcsv.json +0 -13
  359. package/fixtures/recipes/fillloginform.json +0 -12
  360. package/fixtures/recipes/fillmultistepwizard.json +0 -13
  361. package/fixtures/recipes/fillregistration.json +0 -12
  362. package/fixtures/recipes/fillsearchfilters.json +0 -12
  363. package/fixtures/recipes/filltypeahead.json +0 -11
  364. package/fixtures/recipes/monitoralerts.json +0 -12
  365. package/fixtures/recipes/monitoravailability.json +0 -11
  366. package/fixtures/recipes/monitornetworkquiet.json +0 -12
  367. package/fixtures/recipes/monitorpagechanges.json +0 -11
  368. package/fixtures/recipes/monitorpricechange.json +0 -11
  369. package/fixtures/recipes/monitorschedule.json +0 -12
  370. package/fixtures/recipes/scrapedynamicprices.json +0 -13
  371. package/fixtures/recipes/scrapeimagegallery.json +0 -12
  372. package/fixtures/recipes/scrapeinfinitefeed.json +0 -13
  373. package/fixtures/recipes/scrapenestedlists.json +0 -12
  374. package/fixtures/recipes/scrapeproductgrid.json +0 -12
  375. package/fixtures/recipes/scrapereviews.json +0 -12
  376. package/fixtures/recipes/scrapesearchresults.json +0 -12
  377. package/fixtures/recipes/scrapetablepagination.json +0 -12
  378. package/fixtures/recipes/testaccessibility.json +0 -34
  379. package/fixtures/recipes/testconsoleerrors.json +0 -12
  380. package/fixtures/recipes/testdeeplinks.json +0 -12
  381. package/fixtures/recipes/testformvalidation.json +0 -13
  382. package/fixtures/recipes/testlayoutshift.json +0 -12
  383. package/fixtures/recipes/testlinkcheck.json +0 -12
  384. package/fixtures/recipes/testloadtiming.json +0 -12
  385. package/fixtures/recipes/testvisualdiff.json +0 -12
  386. package/fixtures/workflows/changelog-digest-workflow.json +0 -13
  387. package/fixtures/workflows/form-survey-workflow.json +0 -13
  388. package/gallery.json +0 -706
  389. package/hardening.d.ts.map +0 -1
  390. package/hardening.js +0 -188
  391. package/headless.d.ts.map +0 -1
  392. package/headless.js +0 -11522
  393. package/http.d.ts.map +0 -1
  394. package/http.js +0 -2165
  395. package/index.cjs +0 -54682
  396. package/index.d.ts.map +0 -1
  397. package/index.js +0 -52310
  398. package/index.neutral.js +0 -46343
  399. package/mcp.d.ts.map +0 -1
  400. package/mcp.js +0 -20515
  401. package/memory.d.ts.map +0 -1
  402. package/nativehost.template.json +0 -10
  403. package/node.cjs +0 -54686
  404. package/node.d.ts.map +0 -1
  405. package/pack.d.ts.map +0 -1
  406. package/pack.js +0 -805
  407. package/policy.d.ts.map +0 -1
  408. package/progress.d.ts.map +0 -1
  409. package/progress.js +0 -645
  410. package/protocol.d.ts.map +0 -1
  411. package/protocol.js +0 -8334
  412. package/schemas/audit.schema.json +0 -226
  413. package/schemas/capability.schema.json +0 -113
  414. package/schemas/envelope.schema.json +0 -162
  415. package/schemas/memory.schema.json +0 -51
  416. package/schemas/observation.schema.json +0 -113
  417. package/schemas/plan.schema.json +0 -208
  418. package/schemas/progress.schema.json +0 -126
  419. package/schemas/proposal.schema.json +0 -68
  420. package/schemas/session.schema.json +0 -82
  421. package/schemas/tool.schema.json +0 -94
  422. package/server.d.ts.map +0 -1
  423. package/server.js +0 -28877
  424. package/umd-example.html +0 -18
  425. package/umd.d.ts.map +0 -1
  426. /package/{bun.d.ts → dist/bun.d.ts} +0 -0
  427. /package/{cli.d.ts → dist/cli.d.ts} +0 -0
  428. /package/{companion.d.ts → dist/companion.d.ts} +0 -0
  429. /package/{crossbrowser.d.ts → dist/crossbrowser.d.ts} +0 -0
  430. /package/{dashdone.d.ts → dist/dashdone.d.ts} +0 -0
  431. /package/{deno.d.ts → dist/deno.d.ts} +0 -0
  432. /package/{hardening.d.ts → dist/hardening.d.ts} +0 -0
  433. /package/{headless.d.ts → dist/headless.d.ts} +0 -0
  434. /package/{http.d.ts → dist/http.d.ts} +0 -0
  435. /package/{index.d.ts → dist/index.d.ts} +0 -0
  436. /package/{mcp.d.ts → dist/mcp.d.ts} +0 -0
  437. /package/{memory.d.ts → dist/memory.d.ts} +0 -0
  438. /package/{node.d.ts → dist/node.d.ts} +0 -0
  439. /package/{pack.d.ts → dist/pack.d.ts} +0 -0
  440. /package/{policy.d.ts → dist/policy.d.ts} +0 -0
  441. /package/{progress.d.ts → dist/progress.d.ts} +0 -0
  442. /package/{protocol.d.ts → dist/protocol.d.ts} +0 -0
  443. /package/{server.d.ts → dist/server.d.ts} +0 -0
  444. /package/{umd.d.ts → dist/umd.d.ts} +0 -0
@@ -0,0 +1,285 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { acceptrenderresult, nonceof, sandboxrenderof, stripscripts, rendermessage } from "../environments.js";
3
+
4
+ /**
5
+ * Torture suite for the sandbox sanitizer of environments.ts.
6
+ * Every payload here is an adversarial markup attack: reassembly, nesting,
7
+ * case tricks, scheme smuggling, malformed bodies and pathological sizes.
8
+ * The contract: no `<script` sequence and no `on...=` handler survives.
9
+ */
10
+
11
+ const now = 1_000;
12
+
13
+ /** Asserts the sanitizer invariant over one payload: no script tag and no event handler remain. */
14
+ function inert(markup: string): void {
15
+ const out = stripscripts(markup);
16
+ expect(out.toLowerCase()).not.toContain("<script");
17
+ expect(/\son[a-z]+\s*=/i.test(out)).toBe(false);
18
+ }
19
+
20
+ describe("torture: stripscripts reassembly and nesting attacks", () => {
21
+ it("drops the split script tag payloads that reassemble after a naive replace", () => {
22
+ inert("<scr<script>ipt>alert(1)</scr</script>ipt>");
23
+ inert("<scri<script>pt>alert(2)</scri</script>pt>");
24
+ inert("<scr\x00ipt>alert(3)</scr\x00ipt>");
25
+ inert("<<script>alert(4)</script>");
26
+ inert("<script<script>>alert(5)</script>");
27
+ inert("<img src=x on<x>error=alert(6)>");
28
+ inert("<a href=jav<x>ascript:alert(7)>x</a>");
29
+ });
30
+
31
+ it("drops script elements with their whole content through the first closing tag", () => {
32
+ expect(stripscripts("<div>a</div><script>var x = '</div>';</script><div>b</div>")).toBe("<div>a</div><div>b</div>");
33
+ expect(stripscripts("<script>deep(<script>nested()</script>)</script>ok")).toBe(")ok");
34
+ expect(stripscripts("<script >alert(1)</script >tail")).toBe("tail");
35
+ expect(stripscripts("<script src=x>fallback</script>kept")).toBe("kept");
36
+ expect(stripscripts("<SCRIPT>alert(1)</SCRIPT>mixed")).toBe("mixed");
37
+ expect(stripscripts("<script/>after")).toBe("after");
38
+ expect(stripscripts("</script>orphan")).toBe("orphan");
39
+ inert("<script>deep(<script>nested()</script>)</script>ok");
40
+ });
41
+
42
+ it("swallows the rest of the markup when a script element never closes", () => {
43
+ expect(stripscripts("before<script>alert(1)")).toBe("before");
44
+ expect(stripscripts("clean<script>forever")).toBe("clean");
45
+ });
46
+
47
+ it("drops a tag whose body carries another open angle so no reassembly survives", () => {
48
+ inert("<a<b>c</a>");
49
+ inert("<img<b>src=x>text");
50
+ inert("<div<>x</div>");
51
+ expect(stripscripts("<a<b>c</a>")).toBe("c</a>");
52
+ });
53
+
54
+ it("never emits an unterminated tag fragment", () => {
55
+ expect(stripscripts("<p>ok</p")).toBe("<p>ok");
56
+ expect(stripscripts("<img src=x")).toBe("");
57
+ expect(stripscripts("text<script>unclosed")).toBe("text");
58
+ });
59
+
60
+ it("keeps well formed tags and their safe attributes exactly", () => {
61
+ expect(stripscripts('<a href="https://example.com/page">link</a>')).toBe(
62
+ '<a href="https://example.com/page">link</a>',
63
+ );
64
+ expect(stripscripts('<img src="/logo.png" alt="Logo" width="32" height="32">')).toBe(
65
+ '<img src="/logo.png" alt="Logo" width="32" height="32">',
66
+ );
67
+ expect(stripscripts("<p>plain paragraph</p>")).toBe("<p>plain paragraph</p>");
68
+ expect(stripscripts("<ul><li>one</li><li>two</li></ul>")).toBe("<ul><li>one</li><li>two</li></ul>");
69
+ });
70
+ });
71
+
72
+ describe("torture: stripscripts event handler smuggling", () => {
73
+ it("drops on handlers in every casing while whitespace split tokens stay inert", () => {
74
+ expect(stripscripts('<div ONMOUSEOVER="x()">n</div>')).toBe("<div>n</div>");
75
+ expect(stripscripts("<div OnClick='x()'>n</div>")).toBe("<div>n</div>");
76
+ expect(stripscripts('<div onFocus="x()">n</div>')).toBe("<div>n</div>");
77
+ expect(stripscripts("<div onCLICK='x()'>n</div>")).toBe("<div>n</div>");
78
+ inert("<div On\tclick='x()'>n</div>");
79
+ inert("<div on\nclick='x()'>n</div>");
80
+ expect(stripscripts("<svg onload=alert(1)>x</svg>")).toBe("<svg>x</svg>");
81
+ expect(stripscripts('<body onload="x()">n</body>')).toBe("<body>n</body>");
82
+ inert("<iframe onload=alert(1) src=x>");
83
+ inert("<object onerror=alert(1)>");
84
+ inert("<details ontoggle=alert(1) open>x</details>");
85
+ inert("<marquee onstart=alert(1)>x</marquee>");
86
+ inert('<style onload="x()">p{}</style>');
87
+ inert("<input onfocus=alert(1) autofocus>");
88
+ inert("<form onsubmit=alert(1)><button>x</button></form>");
89
+ inert("<video><source onerror=alert(1)></video>");
90
+ });
91
+
92
+ it("drops handlers hidden behind attribute value quoting tricks", () => {
93
+ inert('<img src="x" onerror="alert(1)" alt="y">');
94
+ inert("<img src='x' onerror='alert(1)'>");
95
+ inert("<img src=x onerror=alert(1)>");
96
+ inert('<a href="x" ONCLICK="a" onclick="b">t</a>');
97
+ inert('<a href="x" \'onclick="b">t</a>');
98
+ });
99
+
100
+ it("keeps non handler attributes while everything that starts with on drops conservatively", () => {
101
+ expect(stripscripts('<a data-once="kept">x</a>')).toBe('<a data-once="kept">x</a>');
102
+ expect(stripscripts('<td colspan="2" headers="h">x</td>')).toBe('<td colspan="2" headers="h">x</td>');
103
+ expect(stripscripts("<a onlyattr=plain>x</a>")).toBe("<a>x</a>");
104
+ expect(stripscripts("<a one>x</a>")).toBe("<a>x</a>");
105
+ expect(stripscripts('<a on="quoted">x</a>')).toBe('<a on="quoted">x</a>');
106
+ inert("<a onlyattr=plain onclick=x()>x</a>");
107
+ });
108
+ });
109
+
110
+ describe("torture: stripscripts scheme smuggling", () => {
111
+ it("strips javascript, vbscript and data schemes from every attribute value", () => {
112
+ expect(stripscripts('<a href="javascript:alert(1)">x</a>')).toBe('<a href="alert(1)">x</a>');
113
+ expect(stripscripts("<a href=javascript:alert(2)>x</a>")).toBe('<a href="alert(2)">x</a>');
114
+ expect(stripscripts('<a href="JAVASCRIPT:alert(3)">x</a>')).toBe('<a href="alert(3)">x</a>');
115
+ expect(stripscripts("<a href='JaVaScRiPt:alert(4)'>x</a>")).toBe('<a href="alert(4)">x</a>');
116
+ expect(stripscripts('<a href="vbscript:msgbox(1)">x</a>')).toBe('<a href="msgbox(1)">x</a>');
117
+ inert('<iframe src="data:text/html,<script>alert(1)</script>">x</iframe>');
118
+ expect(stripscripts('<a href="javajavascript:script:alert(5)">x</a>')).toBe('<a href="alert(5)">x</a>');
119
+ inert('<a href=" javascript:alert(6)">x</a>');
120
+ inert("<a href=\x00javascript:alert(7)>x</a>");
121
+ inert('<a href="java\tscript:alert(8)">x</a>');
122
+ });
123
+
124
+ it("cleans scheme fragments repeated inside one value until none remain", () => {
125
+ expect(stripscripts('<a href="data:data:image/png;base64,xx">x</a>')).toBe('<a href="image/png;base64,xx">x</a>');
126
+ expect(stripscripts('<a href="vbscript:vbscript:run(1)">x</a>')).toBe('<a href="run(1)">x</a>');
127
+ });
128
+ });
129
+
130
+ describe("torture: stripscripts pathological inputs", () => {
131
+ it("stays linear and inert on a one megabyte payload of crafted tags", () => {
132
+ const payload = "<scr<script>ipt>".repeat(40_000) + "tail";
133
+ const started = performance.now();
134
+ const out = stripscripts(payload);
135
+ const elapsed = performance.now() - started;
136
+ expect(out.toLowerCase()).not.toContain("<script");
137
+ expect(elapsed).toBeLessThan(2_000);
138
+ }, 10_000);
139
+
140
+ it("handles markup with unicode, emoji, rtl text and control characters inertly", () => {
141
+ inert("<p>こんにちは 🌍 שלום \x00\x01</p><script>alert(1)</script>");
142
+ inert("<p>\u2028\u2029 line separators</p><script>alert(2)</script>");
143
+ inert("<div>​零宽空格</div><svg onload=alert(3)>");
144
+ const out = stripscripts("<p>안녕하세요</p>");
145
+ expect(out).toBe("<p>안녕하세요</p>");
146
+ });
147
+
148
+ it("handles empty, whitespace and tag-only inputs without crashing", () => {
149
+ expect(stripscripts("")).toBe("");
150
+ expect(stripscripts(" ")).toBe("");
151
+ expect(stripscripts("<")).toBe("");
152
+ expect(stripscripts(">")).toBe(">");
153
+ expect(stripscripts("<>")).toBe("<>");
154
+ expect(stripscripts("<>>")).toBe("<>>");
155
+ expect(stripscripts("<<<>>>")).toBe(">>");
156
+ expect(stripscripts("plain text only")).toBe("plain text only");
157
+ });
158
+
159
+ it("passes comments and doctypes through because their bodies carry no open angle", () => {
160
+ expect(stripscripts("<!-- a comment --><p>x</p>")).toBe("<!-- a comment --><p>x</p>");
161
+ expect(stripscripts("<!DOCTYPE html><p>x</p>")).toBe("<!DOCTYPE html><p>x</p>");
162
+ });
163
+
164
+ it("treats deeply repeated angle noise inertly", () => {
165
+ inert("<".repeat(5_000));
166
+ inert(">".repeat(5_000));
167
+ inert("<>".repeat(5_000));
168
+ inert("<a".repeat(5_000));
169
+ });
170
+ });
171
+
172
+ describe("torture: sandbox render descriptors and nonce discipline", () => {
173
+ it("tolerates a blank render id while the meaningful fields stay validated", () => {
174
+ expect(() =>
175
+ sandboxrenderof({ id: "", markup: "<p/>", sourceorigin: "https://example.com", stepid: "s1", now }),
176
+ ).not.toThrow();
177
+ expect(() =>
178
+ sandboxrenderof({ id: "r", markup: "", sourceorigin: "https://example.com", stepid: "s1", now }),
179
+ ).toThrow();
180
+ expect(() => sandboxrenderof({ id: "r", markup: "<p/>", sourceorigin: "", stepid: "s1", now })).toThrow();
181
+ expect(() =>
182
+ sandboxrenderof({ id: "r", markup: "<p/>", sourceorigin: "https://example.com", stepid: "", now }),
183
+ ).toThrow();
184
+ });
185
+
186
+ it("sanitizes the markup inside the render descriptor so the frame never sees a live script", () => {
187
+ const render = sandboxrenderof({
188
+ id: "r1",
189
+ markup: "<p>hello</p><script>alert(1)</script>",
190
+ sourceorigin: "https://example.com",
191
+ stepid: "s1",
192
+ now,
193
+ });
194
+ expect(render.markup).toBe("<p>hello</p>");
195
+ const message = rendermessage(render);
196
+ expect(message.markup.toLowerCase()).not.toContain("<script");
197
+ });
198
+
199
+ it("nonces differ for distinct seeds, repeat for the same seed and never leak the seed", () => {
200
+ const one = nonceof("seed-a");
201
+ expect(nonceof("seed-a")).toBe(one);
202
+ expect(nonceof("seed-b")).not.toBe(one);
203
+ expect(nonceof("")).toMatch(/^[a-z0-9]{16}$/);
204
+ expect(nonceof("x".repeat(10_000))).toMatch(/^[a-z0-9]{16}$/);
205
+ const unicode = nonceof("🔥".repeat(100));
206
+ expect(unicode).toMatch(/^[a-z0-9]{16}$/);
207
+ expect(one).not.toContain("seed");
208
+ });
209
+
210
+ it("rejects every replay, cross channel and malformed answer of the sandbox frame", () => {
211
+ const render = sandboxrenderof({
212
+ id: "r1",
213
+ markup: "<p>x</p>",
214
+ sourceorigin: "https://example.com",
215
+ stepid: "s1",
216
+ now,
217
+ });
218
+ const message = {
219
+ channel: "devthinksandbox",
220
+ type: "renderresult",
221
+ nonce: render.nonce,
222
+ ok: true,
223
+ text: "ok",
224
+ summary: "ok",
225
+ } as const;
226
+ const first = acceptrenderresult({ renders: [render], message, now: now + 1 });
227
+ expect(first.accepted).toBe(true);
228
+ expect(first.renders.find((entry) => entry.nonce === render.nonce)?.answeredat).toBe(now + 1);
229
+ const replay = acceptrenderresult({ renders: first.renders, message, now: now + 2 });
230
+ expect(replay.accepted).toBe(false);
231
+ expect(replay.reason).toMatch(/replayed or already answered/i);
232
+ const unthreaded = acceptrenderresult({ renders: [render], message, now: now + 2 });
233
+ expect(unthreaded.accepted).toBe(true);
234
+ expect(acceptrenderresult({ renders: [], message, now: now + 1 }).accepted).toBe(false);
235
+ expect(acceptrenderresult({ renders: [render], message: { ...message, nonce: "" }, now: now + 1 }).accepted).toBe(
236
+ false,
237
+ );
238
+ expect(
239
+ acceptrenderresult({ renders: [render], message: { ...message, nonce: "deadbeefdeadbeef" }, now: now + 1 })
240
+ .accepted,
241
+ ).toBe(false);
242
+ expect(
243
+ acceptrenderresult({ renders: [render], message: { ...message, channel: "devthinkother" }, now: now + 1 })
244
+ .accepted,
245
+ ).toBe(false);
246
+ expect(
247
+ acceptrenderresult({ renders: [render], message: { ...message, type: "anything" }, now: now + 1 }).accepted,
248
+ ).toBe(false);
249
+ expect(
250
+ acceptrenderresult({
251
+ renders: [render],
252
+ message: { channel: "devthinksandbox", type: "renderresult", ok: true, text: "ok", summary: "ok" },
253
+ now: now + 1,
254
+ }).accepted,
255
+ ).toBe(false);
256
+ const { nonce: _dropnonce, ...nononce } = message;
257
+ expect(acceptrenderresult({ renders: [render], message: nononce, now: now + 1 }).accepted).toBe(false);
258
+ });
259
+
260
+ it("strips markup from the answer text so a result never reenters the dom", () => {
261
+ const render = sandboxrenderof({
262
+ id: "r1",
263
+ markup: "<p>x</p>",
264
+ sourceorigin: "https://example.com",
265
+ stepid: "s1",
266
+ now,
267
+ });
268
+ const answer = acceptrenderresult({
269
+ renders: [render],
270
+ message: {
271
+ channel: "devthinksandbox",
272
+ type: "renderresult",
273
+ nonce: render.nonce,
274
+ ok: true,
275
+ text: '<script>alert("evil")</script><b>bold</b> tail',
276
+ summary: "s",
277
+ },
278
+ now: now + 1,
279
+ });
280
+ expect(answer.accepted).toBe(true);
281
+ expect(answer.result?.text).toBe('alert("evil")bold tail');
282
+ expect(answer.result?.text.toLowerCase()).not.toContain("<script");
283
+ expect(answer.result?.text.toLowerCase()).not.toContain("<b>");
284
+ });
285
+ });
@@ -0,0 +1,263 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import {
3
+ csvfield,
4
+ csvof,
5
+ exportdescriptorof,
6
+ exportformats,
7
+ exportrecords,
8
+ exportscopes,
9
+ jsonlinesof,
10
+ markdownfield,
11
+ markdownof,
12
+ secretfieldshapes,
13
+ unmaskedfieldsof,
14
+ } from "../export.js";
15
+ import { maskfield, maskingfield, maskmarker, maskrecord, maskvalue } from "../security.js";
16
+
17
+ /**
18
+ * Torture suite for the export family: csv injection, formula smuggling,
19
+ * secret laundering attempts, markdown escaping and pathological payloads.
20
+ */
21
+
22
+ describe("torture: csv field escaping", () => {
23
+ it("escapes commas, quotes and newlines with doubled inner quotes", () => {
24
+ expect(csvfield("plain")).toBe("plain");
25
+ expect(csvfield("with,comma")).toBe('"with,comma"');
26
+ expect(csvfield('with"quote')).toBe('"with""quote"');
27
+ expect(csvfield("line1\nline2")).toBe('"line1\nline2"');
28
+ expect(csvfield('all"three",chars\nhere')).toBe('"all""three"",chars\nhere"');
29
+ expect(csvfield("")).toBe("");
30
+ expect(csvfield(" ")).toBe(" ");
31
+ expect(csvfield("no special")).toBe("no special");
32
+ });
33
+
34
+ it("keeps tab, cr and unicode unquoted unless they collide with the escape set", () => {
35
+ expect(csvfield("with\ttab")).toBe("with\ttab");
36
+ expect(csvfield("cr\ronly")).toBe("cr\ronly");
37
+ expect(csvfield("こんにちは")).toBe("こんにちは");
38
+ expect(csvfield("emoji 🌍 here")).toBe("emoji 🌍 here");
39
+ expect(csvfield("rtl שלום")).toBe("rtl שלום");
40
+ });
41
+
42
+ it("csv rows sort headers, union fields and fill absent fields empty", () => {
43
+ const csv = csvof([
44
+ { b: "2", a: "1" },
45
+ { a: "x", c: "3" },
46
+ ]);
47
+ const lines = csv.split("\n");
48
+ expect(lines[0]).toBe("a,b,c");
49
+ expect(lines[1]).toBe("1,2,");
50
+ expect(lines[2]).toBe("x,,3");
51
+ expect(csvof([])).toBe("");
52
+ });
53
+
54
+ it("csv round trips adversarial payloads without breaking the column count", () => {
55
+ const rows = [
56
+ { formula: "=cmd|' /C calc'!A0", note: "excel injection" },
57
+ { formula: "+1|2", note: "lotus injection" },
58
+ { formula: "-2+3+cmd|' /C calc'!A0", note: "another form" },
59
+ { formula: "@SUM(1+9)*cmd|' /C calc'!A0", note: "at form" },
60
+ { formula: "\t=hidden", note: "tab led formula" },
61
+ { formula: 'quote"and,comma\nnewline', note: "escape collision" },
62
+ { formula: "\u2028unicode sep", note: "u2028" },
63
+ { formula: "\0null", note: "null byte" },
64
+ ];
65
+ const csv = csvof(rows);
66
+ const parsed = parsecsv(csv);
67
+ expect(parsed.length).toBe(rows.length + 1);
68
+ expect(parsed[0]).toEqual(["formula", "note"]);
69
+ for (const cells of parsed.slice(1)) expect(cells.length).toBe(2);
70
+ expect(parsed[1]?.[0]).toBe("=cmd|' /C calc'!A0");
71
+ expect(parsed[6]?.[0]).toBe('quote"and,comma\nnewline');
72
+ });
73
+
74
+ /** Parses whole csv content honoring quoted cells with embedded newlines and doubled quotes. */
75
+ function parsecsv(content: string): string[][] {
76
+ const rowsout: string[][] = [];
77
+ let row: string[] = [];
78
+ let cell = "";
79
+ let quoted = false;
80
+ for (let index = 0; index < content.length; index += 1) {
81
+ const char = content[index]!;
82
+ if (quoted) {
83
+ if (char === '"') {
84
+ if (content[index + 1] === '"') {
85
+ cell += '"';
86
+ index += 1;
87
+ } else quoted = false;
88
+ } else cell += char;
89
+ } else if (char === '"') quoted = true;
90
+ else if (char === ",") {
91
+ row.push(cell);
92
+ cell = "";
93
+ } else if (char === "\n") {
94
+ row.push(cell);
95
+ rowsout.push(row);
96
+ row = [];
97
+ cell = "";
98
+ } else cell += char;
99
+ }
100
+ if (cell !== "" || row.length > 0) {
101
+ row.push(cell);
102
+ rowsout.push(row);
103
+ }
104
+ return rowsout;
105
+ }
106
+ });
107
+
108
+ describe("torture: markdown escaping", () => {
109
+ it("escapes pipes, backslashes and newlines in order", () => {
110
+ expect(markdownfield("plain")).toBe("plain");
111
+ expect(markdownfield("a|b")).toBe("a\\|b");
112
+ expect(markdownfield("a\\b")).toBe("a\\\\b");
113
+ expect(markdownfield("a\\|b")).toBe("a\\\\\\|b");
114
+ expect(markdownfield("line1\nline2")).toBe("line1<br>line2");
115
+ expect(markdownfield("line1\r\nline2")).toBe("line1<br>line2");
116
+ expect(markdownfield("")).toBe("");
117
+ expect(markdownfield("all|three\\chars\nhere")).toBe("all\\|three\\\\chars<br>here");
118
+ });
119
+
120
+ it("markdown tables keep the column count under adversarial cells", () => {
121
+ const table = markdownof([
122
+ { a: "|injection|", b: "x" },
123
+ { a: "line\nbreak", b: "y" },
124
+ ]);
125
+ const lines = table.split("\n");
126
+ expect(lines[0]).toBe("| a | b |");
127
+ expect(lines[1]).toBe("| --- | --- |");
128
+ for (const line of lines) {
129
+ expect((line.match(/(?<!\\)\|/g) ?? []).length).toBe(line.startsWith("| ---") ? 3 : 3);
130
+ }
131
+ expect(markdownof([])).toBe("");
132
+ });
133
+ });
134
+
135
+ describe("torture: secret field detection and laundering refusal", () => {
136
+ it("detects every secret shape by substring in any casing", () => {
137
+ const shapes = ["password"];
138
+ const rows = [
139
+ { password: "hunter2" },
140
+ { Password: "hunter2" },
141
+ { PASSWORD: "hunter2" },
142
+ { mypasswordfield: "hunter2" },
143
+ { apikey: "abc" },
144
+ { apiKey: "abc" },
145
+ { api_key: "not matched by shape list" },
146
+ { token: "tok" },
147
+ { authauthorization: "z" },
148
+ { secret: "s" },
149
+ ];
150
+ const unmasked = unmaskedfieldsof(rows);
151
+ expect(new Set(unmasked)).toEqual(
152
+ new Set([
153
+ "Password",
154
+ "PASSWORD",
155
+ "password",
156
+ "mypasswordfield",
157
+ "apikey",
158
+ "apiKey",
159
+ "token",
160
+ "authauthorization",
161
+ "secret",
162
+ ]),
163
+ );
164
+ expect(unmasked.length).toBe(9);
165
+ });
166
+
167
+ it("masked values pass while empty and non string values never trip the gate", () => {
168
+ const rows = [
169
+ { password: maskmarker },
170
+ { token: "" },
171
+ { secret: " " },
172
+ { apikey: 42 },
173
+ { apikey: null },
174
+ { apikey: { nested: "x" } },
175
+ { note: "a normal note about passwords as a word" },
176
+ ];
177
+ expect(unmaskedfieldsof(rows)).toEqual([]);
178
+ });
179
+
180
+ it("export refuses the whole file when one unmasked secret rides any format", () => {
181
+ for (const format of exportformats) {
182
+ const descriptor = exportdescriptorof(format, "runs");
183
+ const refused = exportrecords({ descriptor, records: [{ note: "ok", password: "hunter2" }], shapes: [] });
184
+ expect(refused.result.bytes).toBe(0);
185
+ expect(refused.result.rows).toBe(0);
186
+ expect(refused.content).toBe("");
187
+ expect(refused.result.reason).toMatch(/unmasked/i);
188
+ const clean = exportrecords({ descriptor, records: [{ note: "ok", password: maskmarker }], shapes: [] });
189
+ expect(clean.result.rows).toBe(1);
190
+ expect(clean.result.bytes).toBeGreaterThan(0);
191
+ }
192
+ });
193
+
194
+ it("export descriptors refuse unknown formats and scopes", () => {
195
+ expect(() => exportdescriptorof("exe", "runs")).toThrow(/csv, json, log, jsonl and markdown/i);
196
+ expect(() => exportdescriptorof("csv", "passwords")).toThrow(
197
+ /runs, extractions, notes, session, audit and extraction/i,
198
+ );
199
+ expect(exportdescriptorof("csv", "runs").format).toBe("csv");
200
+ expect(exportscopes.length).toBeGreaterThan(0);
201
+ expect(secretfieldshapes).toContain("authorization");
202
+ });
203
+
204
+ it("masked shapes honor the configured list while the documented recognizer always runs", () => {
205
+ expect(maskingfield("password", [])).toBe(true);
206
+ expect(maskingfield("token", [])).toBe(true);
207
+ expect(maskingfield("apikey", [])).toBe(true);
208
+ expect(maskingfield("secret", [])).toBe(true);
209
+ expect(maskingfield("authorization", [])).toBe(true);
210
+ expect(maskingfield("name", [])).toBe(false);
211
+ expect(maskingfield("name", ["name"])).toBe(true);
212
+ expect(maskingfield("username", ["name"])).toBe(true);
213
+ expect(maskingfield("nomatch", ["name"])).toBe(false);
214
+ expect(maskingfield("anything", [""])).toBe(false);
215
+ expect(maskingfield("caseinsensitive", ["ins"])).toBe(true);
216
+ expect(maskfield({ name: "password", value: "x", shapes: [] })).toBe(maskmarker);
217
+ expect(maskfield({ name: "note", value: "x", shapes: [] })).toBe("x");
218
+ expect(maskvalue("")).toBe("");
219
+ expect(maskvalue("anything")).toBe(maskmarker);
220
+ });
221
+
222
+ it("maskrecord walks nested records and arrays deeply", () => {
223
+ const masked = maskrecord({ password: "x", note: "ok", nested: { password: "y", keep: "z" } }, []);
224
+ expect(masked.password).toBe(maskmarker);
225
+ expect(masked.note).toBe("ok");
226
+ expect((masked.nested as Record<string, unknown>).password).toBe(maskmarker);
227
+ expect((masked.nested as Record<string, unknown>).keep).toBe("z");
228
+ });
229
+ });
230
+
231
+ describe("torture: jsonl rendering under adversarial records", () => {
232
+ it("one compact json document per line with no delimiter inside", () => {
233
+ const records = [{ a: "line1\nline2" }, { b: 'quote"inside' }, { c: "こんにちは" }, { d: 42 }, { e: null }];
234
+ const jsonl = jsonlinesof(records);
235
+ const lines = jsonl.split("\n");
236
+ expect(lines.length).toBe(5);
237
+ for (const line of lines) expect(() => JSON.parse(line)).not.toThrow();
238
+ expect(jsonlinesof([])).toBe("");
239
+ });
240
+ });
241
+
242
+ describe("torture: pathological payloads through the export pipeline", () => {
243
+ it("one megabyte values export without losing bytes", () => {
244
+ const huge = "x".repeat(1_000_000);
245
+ const csv = exportrecords({ descriptor: exportdescriptorof("csv", "runs"), records: [{ blob: huge }], shapes: [] });
246
+ expect(csv.content.length).toBeGreaterThan(1_000_000);
247
+ expect(csv.result.rows).toBe(1);
248
+ });
249
+
250
+ it("prototype pollution keys never reach the export through object entries", () => {
251
+ const rows = JSON.parse('{"__proto__":"polluted","constructor":"also","note":"ok"}') as Record<string, string>;
252
+ const csv = exportrecords({ descriptor: exportdescriptorof("csv", "runs"), records: [rows], shapes: [] });
253
+ expect(csv.content).toContain("note");
254
+ expect(csv.content).not.toContain("polluted");
255
+ expect(({} as Record<string, unknown>).polluted).toBeUndefined();
256
+ });
257
+
258
+ it("unicode newlines and separators stay inside their cells", () => {
259
+ const rows = [{ note: "a\u2028b\u2029c" }];
260
+ const csv = exportrecords({ descriptor: exportdescriptorof("csv", "runs"), records: rows, shapes: [] });
261
+ expect(csv.content.split("\n").length).toBe(2);
262
+ });
263
+ });