@maudecode/talaria-web 1.11.0 → 1.13.0

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 (263) hide show
  1. package/LICENSE +22 -0
  2. package/_release.json +4 -4
  3. package/dist/api/chat-router.d.ts +68 -4
  4. package/dist/api/chat-router.js +11 -8
  5. package/dist/api/chat-router.js.map +1 -1
  6. package/dist/api/router.d.ts +10 -0
  7. package/dist/api/router.js +2 -1
  8. package/dist/api/router.js.map +1 -1
  9. package/dist/api/sessions-router.d.ts +407 -51
  10. package/dist/api/sessions-router.js +2 -2
  11. package/dist/api/sessions-router.js.map +1 -1
  12. package/dist/api/sse-routes.js +52 -8
  13. package/dist/api/sse-routes.js.map +1 -1
  14. package/dist/api/tools-router.d.ts +443 -0
  15. package/dist/api/tools-router.js +99 -4
  16. package/dist/api/tools-router.js.map +1 -1
  17. package/dist/app.d.ts +926 -55
  18. package/dist/auth/oidc.js +2 -0
  19. package/dist/auth/oidc.js.map +1 -1
  20. package/dist/auth/passkeys.js +1 -1
  21. package/dist/auth/passkeys.js.map +1 -1
  22. package/dist/auth/store.d.ts +17 -0
  23. package/dist/auth/store.js +77 -19
  24. package/dist/auth/store.js.map +1 -1
  25. package/dist/cli/ctl.d.ts +6 -3
  26. package/dist/cli/ctl.js +25 -16
  27. package/dist/cli/ctl.js.map +1 -1
  28. package/dist/cli/launcher.js +2 -2
  29. package/dist/cli/launcher.js.map +1 -1
  30. package/dist/fs/atomic.d.ts +8 -1
  31. package/dist/fs/atomic.js +52 -3
  32. package/dist/fs/atomic.js.map +1 -1
  33. package/dist/http/context.d.ts +2 -0
  34. package/dist/http/context.js.map +1 -1
  35. package/dist/mcp/server.js +1 -1
  36. package/dist/mcp/server.js.map +1 -1
  37. package/dist/redact.d.ts +15 -0
  38. package/dist/redact.js +1393 -9
  39. package/dist/redact.js.map +1 -1
  40. package/dist/release.js +3 -1
  41. package/dist/release.js.map +1 -1
  42. package/dist/runtime.js +20 -9
  43. package/dist/runtime.js.map +1 -1
  44. package/dist/server.d.ts +1 -1
  45. package/dist/server.js +12 -6
  46. package/dist/server.js.map +1 -1
  47. package/dist/sessions/anchor.d.ts +2 -0
  48. package/dist/sessions/anchor.js +22 -17
  49. package/dist/sessions/anchor.js.map +1 -1
  50. package/dist/sessions/export.js +1 -1
  51. package/dist/sessions/export.js.map +1 -1
  52. package/dist/sessions/file-changes.d.ts +15 -0
  53. package/dist/sessions/file-changes.js +58 -0
  54. package/dist/sessions/file-changes.js.map +1 -0
  55. package/dist/sessions/journal.d.ts +6 -1
  56. package/dist/sessions/journal.js +9 -15
  57. package/dist/sessions/journal.js.map +1 -1
  58. package/dist/sessions/list.d.ts +21 -1
  59. package/dist/sessions/list.js +81 -17
  60. package/dist/sessions/list.js.map +1 -1
  61. package/dist/sessions/merge.d.ts +66 -2
  62. package/dist/sessions/merge.js +291 -9
  63. package/dist/sessions/merge.js.map +1 -1
  64. package/dist/sessions/pending.d.ts +11 -2
  65. package/dist/sessions/pending.js +52 -3
  66. package/dist/sessions/pending.js.map +1 -1
  67. package/dist/sessions/relay.js +1 -1
  68. package/dist/sessions/relay.js.map +1 -1
  69. package/dist/sessions/service.d.ts +16 -3
  70. package/dist/sessions/service.js +92 -54
  71. package/dist/sessions/service.js.map +1 -1
  72. package/dist/sessions/state-db.d.ts +5 -0
  73. package/dist/sessions/state-db.js +34 -0
  74. package/dist/sessions/state-db.js.map +1 -1
  75. package/dist/sessions/streams.d.ts +4 -1
  76. package/dist/sessions/streams.js +3 -0
  77. package/dist/sessions/streams.js.map +1 -1
  78. package/dist/sessions/tool-display.d.ts +21 -0
  79. package/dist/sessions/tool-display.js +113 -0
  80. package/dist/sessions/tool-display.js.map +1 -0
  81. package/dist/sessions/turn.d.ts +25 -7
  82. package/dist/sessions/turn.js +153 -90
  83. package/dist/sessions/turn.js.map +1 -1
  84. package/dist/spa.d.ts +2 -0
  85. package/dist/spa.js +9 -0
  86. package/dist/spa.js.map +1 -1
  87. package/dist/test/harness.d.ts +2 -0
  88. package/dist/test/harness.js +1 -1
  89. package/dist/test/harness.js.map +1 -1
  90. package/dist/tools/terminal.d.ts +6 -0
  91. package/dist/tools/terminal.js +24 -3
  92. package/dist/tools/terminal.js.map +1 -1
  93. package/dist/tools/update-notifications.d.ts +133 -0
  94. package/dist/tools/update-notifications.js +407 -0
  95. package/dist/tools/update-notifications.js.map +1 -0
  96. package/dist/tools/updates.d.ts +53 -16
  97. package/dist/tools/updates.js +314 -351
  98. package/dist/tools/updates.js.map +1 -1
  99. package/package.json +3 -2
  100. package/sidecar/scripts/replay_sidecar.py +17 -0
  101. package/sidecar/talaria_sidecar/__init__.py +1 -1
  102. package/sidecar/talaria_sidecar/home.py +39 -16
  103. package/sidecar/talaria_sidecar/methods/chat.py +113 -4
  104. package/static/dist/FILES.txt +116 -116
  105. package/static/dist/THIRD_PARTY_LICENSES.md +5170 -0
  106. package/static/dist/assets/AppShell-irdqom6e.js +1 -0
  107. package/static/dist/assets/ChatPage-ldhqevjn.js +20 -0
  108. package/static/dist/assets/HubRoute-hq24vhj8.js +2 -0
  109. package/static/dist/assets/{Markdown-ftyfzds0.js → Markdown-bx4n7qs4.js} +7 -7
  110. package/static/dist/assets/{RightPanel-febk3dkd.js → RightPanel-ds4majj3.js} +1 -1
  111. package/static/dist/assets/Select-gpqmt6x1.js +1 -0
  112. package/static/dist/assets/SessionListPanel-hkvipik4.js +1 -0
  113. package/static/dist/assets/{SettingsLayout-pdpmhy1o.js → SettingsLayout-oigbg9rl.js} +1 -1
  114. package/static/dist/assets/States-fn4o2wdf.js +1 -0
  115. package/static/dist/assets/Toaster-cwnut53k.js +1 -0
  116. package/static/dist/assets/_app-h6ago6y6.js +1 -0
  117. package/static/dist/assets/_app.ext._extensionId-erqnfff5.js +1 -0
  118. package/static/dist/assets/_app.index-cq2fxmda.js +1 -0
  119. package/static/dist/assets/_app.insights-nzhld9gu.js +1 -0
  120. package/static/dist/assets/_app.kanban-m5uggety.js +1 -0
  121. package/static/dist/assets/_app.logs-d2kk9lza.js +1 -0
  122. package/static/dist/assets/_app.memory-gcmop1e7.js +1 -0
  123. package/static/dist/assets/_app.profiles-e9b2my7x.js +1 -0
  124. package/static/dist/assets/{_app.session._sessionId-n1ggcf5e.js → _app.session._sessionId-ciix6itb.js} +1 -1
  125. package/static/dist/assets/_app.settings-kcjum3c1.js +1 -0
  126. package/static/dist/assets/_app.settings._section-bi4jjex0.js +1 -0
  127. package/static/dist/assets/_app.skills-e4e61l23.js +1 -0
  128. package/static/dist/assets/_app.tasks-f1evdhbl.js +2 -0
  129. package/static/dist/assets/_app.todos-o7ijtk42.js +1 -0
  130. package/static/dist/assets/_app.workspaces-o18voh0w.js +1 -0
  131. package/static/dist/assets/{abnfDiagram-VCTEODGH-i4bgzu83.js → abnfDiagram-VCTEODGH-krt0yaru.js} +1 -1
  132. package/static/dist/assets/architecture-7GRP2DOG-notdhmpv.js +1 -0
  133. package/static/dist/assets/{architectureDiagram-5GKGNRK7-mx21lwqa.js → architectureDiagram-5GKGNRK7-i54t9ywl.js} +1 -1
  134. package/static/dist/assets/{blockDiagram-I7D4REHJ-o1hhchxv.js → blockDiagram-I7D4REHJ-fevbz4mz.js} +1 -1
  135. package/static/dist/assets/{c4Diagram-7LVT6UL2-jygqtvd8.js → c4Diagram-7LVT6UL2-np8eclmc.js} +1 -1
  136. package/static/dist/assets/channel-jpk1nas2.js +1 -0
  137. package/static/dist/assets/{chunk-4HAMMTFA-hjcpubgk.js → chunk-4HAMMTFA-owoi036m.js} +1 -1
  138. package/static/dist/assets/{chunk-75Z2AOVW-eibnex6m.js → chunk-75Z2AOVW-md44zf1n.js} +1 -1
  139. package/static/dist/assets/{chunk-DU6HZSFF-k1tp3qnp.js → chunk-DU6HZSFF-ka3rw8d6.js} +3 -3
  140. package/static/dist/assets/{chunk-F27PBJKO-lvcgn16f.js → chunk-F27PBJKO-iasnjsky.js} +1 -1
  141. package/static/dist/assets/{chunk-GMAD6QVW-ldlzruv1.js → chunk-GMAD6QVW-ex1up6tz.js} +1 -1
  142. package/static/dist/assets/{chunk-GVQU2GXP-hs1mva9d.js → chunk-GVQU2GXP-d4kmmg9p.js} +1 -1
  143. package/static/dist/assets/{chunk-IMKFNOWR-o7eqx3yb.js → chunk-IMKFNOWR-mwy42t85.js} +1 -1
  144. package/static/dist/assets/{chunk-L3NEJ4N5-bp1ofl70.js → chunk-L3NEJ4N5-fv7ry213.js} +1 -1
  145. package/static/dist/assets/{chunk-OSK3NFVY-o8m6mdz5.js → chunk-OSK3NFVY-jg9ir8ac.js} +1 -1
  146. package/static/dist/assets/{chunk-P2QGCYS3-kx83fti2.js → chunk-P2QGCYS3-pe9ms3ua.js} +1 -1
  147. package/static/dist/assets/{chunk-POPQ4Y6H-mwl99e9y.js → chunk-POPQ4Y6H-dimsjrn1.js} +1 -1
  148. package/static/dist/assets/{chunk-PWAF6VOD-oriuokxy.js → chunk-PWAF6VOD-m4hv98dy.js} +1 -1
  149. package/static/dist/assets/{chunk-SHT3W25Y-ab5cf00v.js → chunk-SHT3W25Y-gkjgl6qe.js} +1 -1
  150. package/static/dist/assets/{chunk-SVP7TREG-kqdrpr17.js → chunk-SVP7TREG-bc8qss5l.js} +1 -1
  151. package/static/dist/assets/{chunk-TICWLB2K-kfbnjabk.js → chunk-TICWLB2K-k9wzw38w.js} +1 -1
  152. package/static/dist/assets/classDiagram-ZZMXUADV-hld301gc.js +1 -0
  153. package/static/dist/assets/classDiagram-v2-VYDZK3BY-hld301gc.js +1 -0
  154. package/static/dist/assets/{copied-dp33szhq.js → copied-d5joolw7.js} +1 -1
  155. package/static/dist/assets/{cynefin-OW5HDTMX-mm5sj9s7.js → cynefin-OW5HDTMX-hueklikk.js} +1 -1
  156. package/static/dist/assets/{cynefinDiagram-5FMLGOSQ-nfon5q38.js → cynefinDiagram-5FMLGOSQ-e83o1wr1.js} +1 -1
  157. package/static/dist/assets/{dagre-GXQ25YYZ-dzdp9ypb.js → dagre-GXQ25YYZ-ir1tj2ar.js} +1 -1
  158. package/static/dist/assets/{diagram-S7CK7UJ4-ckxzqo01.js → diagram-S7CK7UJ4-o5218p2q.js} +1 -1
  159. package/static/dist/assets/{diagram-UQ7AKVKN-hw16h1um.js → diagram-UQ7AKVKN-ciad4o1i.js} +1 -1
  160. package/static/dist/assets/{diagram-VSXAHHWV-mu7wu2zg.js → diagram-VSXAHHWV-oharqjbt.js} +1 -1
  161. package/static/dist/assets/{diagram-VX7I27RA-k9uprwtj.js → diagram-VX7I27RA-wkwy62iv.js} +1 -1
  162. package/static/dist/assets/{diagram-Z3DM3KII-csdjzrg9.js → diagram-Z3DM3KII-bl252i7r.js} +1 -1
  163. package/static/dist/assets/{ebnfDiagram-PWID7BFC-n8mbw2jj.js → ebnfDiagram-PWID7BFC-nm0rcly7.js} +1 -1
  164. package/static/dist/assets/endpoints-d8n28yf3.js +64 -0
  165. package/static/dist/assets/{erDiagram-RLTQ6QDP-fsfe1uj0.js → erDiagram-RLTQ6QDP-m1w0vta2.js} +1 -1
  166. package/static/dist/assets/eventmodeling-NTZA5JFV-eaqs19mj.js +1 -0
  167. package/static/dist/assets/flowDiagram-HODETNUW-eli5764n.js +1 -0
  168. package/static/dist/assets/{ganttDiagram-EL5Y4UJY-gsba3iq9.js → ganttDiagram-EL5Y4UJY-czxo6iew.js} +1 -1
  169. package/static/dist/assets/{gitGraph-4MIJSDKK-na86q1pu.js → gitGraph-4MIJSDKK-ip9j1rph.js} +1 -1
  170. package/static/dist/assets/{gitGraphDiagram-WWUBYQGX-g72ew3xz.js → gitGraphDiagram-WWUBYQGX-kalxdole.js} +1 -1
  171. package/static/dist/assets/{highlighted-body-KPVGNVTW-okn18cp1.js → highlighted-body-KPVGNVTW-lgsj6fsk.js} +1 -1
  172. package/static/dist/assets/index-l08181vr.js +11 -0
  173. package/static/dist/assets/index-pdtcl20t.css +2 -0
  174. package/static/dist/assets/{info-A6RAGUB7-i01jeltk.js → info-A6RAGUB7-fv4fbc5m.js} +1 -1
  175. package/static/dist/assets/{infoDiagram-27XIBGKW-cayiv6fa.js → infoDiagram-27XIBGKW-kinx5jsl.js} +1 -1
  176. package/static/dist/assets/{ishikawaDiagram-5VMMS53U-g4rf14bv.js → ishikawaDiagram-5VMMS53U-kozvlqdv.js} +1 -1
  177. package/static/dist/assets/{journeyDiagram-3NMN7TZE-fusb2424.js → journeyDiagram-3NMN7TZE-lgpouxoe.js} +1 -1
  178. package/static/dist/assets/{kanban-definition-UXKFOSKX-g831pwlr.js → kanban-definition-UXKFOSKX-b320wo7b.js} +1 -1
  179. package/static/dist/assets/{lazyRouteComponent-ol965zwk.js → lazyRouteComponent-hy8doeye.js} +1 -1
  180. package/static/dist/assets/{line-md6em6gc.js → line-gxfwyhjq.js} +1 -1
  181. package/static/dist/assets/{login-kfclsqd2.js → login-llplsuey.js} +1 -1
  182. package/static/dist/assets/mermaid-HWGCJPDP-f3scrlds.js +1 -0
  183. package/static/dist/assets/{mermaid-parser.core-mq3uns9m.js → mermaid-parser.core-loxeqsu3.js} +3 -3
  184. package/static/dist/assets/{mindmap-definition-YA3MSWOX-oxafrned.js → mindmap-definition-YA3MSWOX-edi534gc.js} +1 -1
  185. package/static/dist/assets/{onboarding-fo6hnkyv.js → onboarding-n1xhlgym.js} +1 -1
  186. package/static/dist/assets/{packet-AYTQ26CC-ggxyvbym.js → packet-AYTQ26CC-gmb3yy71.js} +1 -1
  187. package/static/dist/assets/{pegDiagram-XKGWAZYB-nk7xcqgp.js → pegDiagram-XKGWAZYB-fxy5j8cx.js} +1 -1
  188. package/static/dist/assets/{pie-WAS4IAKB-ito2razh.js → pie-WAS4IAKB-do2pm4s6.js} +1 -1
  189. package/static/dist/assets/{pieDiagram-E7YTZNPT-g02xexar.js → pieDiagram-E7YTZNPT-mgjvb365.js} +1 -1
  190. package/static/dist/assets/{quadrantDiagram-AXDQQJYC-d4jn4xq5.js → quadrantDiagram-AXDQQJYC-dhok6m1j.js} +1 -1
  191. package/static/dist/assets/{radar-RG4KPBEZ-nebvfdzt.js → radar-RG4KPBEZ-n5giw3vb.js} +1 -1
  192. package/static/dist/assets/{railroad-74A4TZTK-ny3gebse.js → railroad-74A4TZTK-h6nfs9bj.js} +1 -1
  193. package/static/dist/assets/railroad-abnf-HS5TGJTU-d5gaaud9.js +1 -0
  194. package/static/dist/assets/railroad-ebnf-LZEXJU2U-ihudnbec.js +1 -0
  195. package/static/dist/assets/railroad-peg-WCYAUIDC-lgpvhel7.js +1 -0
  196. package/static/dist/assets/{railroadDiagram-O6MQD6OU-m09mmu67.js → railroadDiagram-O6MQD6OU-lfwg92r4.js} +1 -1
  197. package/static/dist/assets/{refresh-cw-eitd7yva.js → refresh-cw-bg6oc499.js} +1 -1
  198. package/static/dist/assets/{requirementDiagram-BXWQKSXE-evggpcbk.js → requirementDiagram-BXWQKSXE-c5k5pg07.js} +1 -1
  199. package/static/dist/assets/{sankeyDiagram-P5KCCOFB-bun89039.js → sankeyDiagram-P5KCCOFB-o9auio4y.js} +1 -1
  200. package/static/dist/assets/{saved-culx14ii.js → saved-j5wyhbik.js} +1 -1
  201. package/static/dist/assets/{search-nu6kzq55.js → search-ipa17zlr.js} +1 -1
  202. package/static/dist/assets/{sequenceDiagram-WJ2MYXX4-hhs9f09o.js → sequenceDiagram-WJ2MYXX4-pfgodnmk.js} +1 -1
  203. package/static/dist/assets/{settings_section_extensions_title-mj0gcozh.js → settings_section_extensions_title-fcfjf6l8.js} +1 -1
  204. package/static/dist/assets/share._token-i339gqj4.js +2 -0
  205. package/static/dist/assets/{sliders-horizontal-b9ljga1o.js → sliders-horizontal-bfrh4rch.js} +1 -1
  206. package/static/dist/assets/{stateDiagram-D77RDMKH-gi7t1tro.js → stateDiagram-D77RDMKH-m1y2cr3q.js} +1 -1
  207. package/static/dist/assets/stateDiagram-v2-MP3YSRHH-hehvfv55.js +1 -0
  208. package/static/dist/assets/{swimlanes-42K2YHIH-ea5urvph.js → swimlanes-42K2YHIH-hzai6fwo.js} +1 -1
  209. package/static/dist/assets/swimlanesDiagram-VR7AAH4N-nfk9mtst.js +8 -0
  210. package/static/dist/assets/{timeline-definition-24CTP7MA-mq64rm3b.js → timeline-definition-24CTP7MA-gwefy9gi.js} +1 -1
  211. package/static/dist/assets/todoStore-fhvdsen0.js +1 -0
  212. package/static/dist/assets/{treeView-Q6P3EWNA-cimbtxlz.js → treeView-Q6P3EWNA-7q91p0dn.js} +1 -1
  213. package/static/dist/assets/{treemap-WGGIJYW6-wig5gi1y.js → treemap-WGGIJYW6-occom39a.js} +1 -1
  214. package/static/dist/assets/{untitled-mbwemvxf.js → untitled-cgw2enra.js} +1 -1
  215. package/static/dist/assets/{useForm-li4bljlv.js → useForm-jwbfla68.js} +1 -1
  216. package/static/dist/assets/useLocale-gieo1fn7.js +1 -0
  217. package/static/dist/assets/{vennDiagram-4TSXK5OY-ez63kh7n.js → vennDiagram-4TSXK5OY-ew4bhldi.js} +1 -1
  218. package/static/dist/assets/{wardley-WFR3VGLG-r7rftz31.js → wardley-WFR3VGLG-njdacy0u.js} +1 -1
  219. package/static/dist/assets/{wardleyDiagram-VM6X3IG4-mvqg9eqc.js → wardleyDiagram-VM6X3IG4-mkvxx7u9.js} +1 -1
  220. package/static/dist/assets/{xychartDiagram-S5SC5T6Z-eqxdfc91.js → xychartDiagram-S5SC5T6Z-8uriotdw.js} +1 -1
  221. package/static/dist/index.html +1 -1
  222. package/static/dist/manifest.webmanifest +1 -1
  223. package/static/dist/sw.js +114 -152
  224. package/static/dist/assets/AppShell-hikzkcql.js +0 -1
  225. package/static/dist/assets/ChatPage-ofqn89h9.js +0 -21
  226. package/static/dist/assets/HubRoute-f2zrs9si.js +0 -2
  227. package/static/dist/assets/Select-lvx8gihl.js +0 -1
  228. package/static/dist/assets/SessionListPanel-o70ljmik.js +0 -1
  229. package/static/dist/assets/States-i46tz8an.js +0 -1
  230. package/static/dist/assets/Toaster-fklyeeyh.js +0 -1
  231. package/static/dist/assets/_app-j3rors25.js +0 -1
  232. package/static/dist/assets/_app.ext._extensionId-c0ey5ec9.js +0 -1
  233. package/static/dist/assets/_app.index-coy9jrto.js +0 -1
  234. package/static/dist/assets/_app.insights-ebd2k0i5.js +0 -1
  235. package/static/dist/assets/_app.kanban-e2p7ypi5.js +0 -1
  236. package/static/dist/assets/_app.logs-ml21mfyz.js +0 -1
  237. package/static/dist/assets/_app.memory-lhu5mm47.js +0 -1
  238. package/static/dist/assets/_app.profiles-egx0fgjo.js +0 -1
  239. package/static/dist/assets/_app.settings-d0vqtyid.js +0 -1
  240. package/static/dist/assets/_app.settings._section-cnzj6pli.js +0 -1
  241. package/static/dist/assets/_app.skills-kkug8obu.js +0 -1
  242. package/static/dist/assets/_app.tasks-nf285d2o.js +0 -2
  243. package/static/dist/assets/_app.todos-dxalqp1e.js +0 -1
  244. package/static/dist/assets/_app.workspaces-bqtduujs.js +0 -1
  245. package/static/dist/assets/architecture-7GRP2DOG-o8zqevhv.js +0 -1
  246. package/static/dist/assets/channel-mw8l793e.js +0 -1
  247. package/static/dist/assets/classDiagram-ZZMXUADV-egajr1wp.js +0 -1
  248. package/static/dist/assets/classDiagram-v2-VYDZK3BY-egajr1wp.js +0 -1
  249. package/static/dist/assets/endpoints-f6ngsyu5.js +0 -64
  250. package/static/dist/assets/eventmodeling-NTZA5JFV-k3zhqcyl.js +0 -1
  251. package/static/dist/assets/flowDiagram-HODETNUW-bdxes51f.js +0 -1
  252. package/static/dist/assets/index-ab0wj9ik.css +0 -2
  253. package/static/dist/assets/index-fd27xooo.js +0 -11
  254. package/static/dist/assets/mermaid-HWGCJPDP-ejk0d88t.js +0 -1
  255. package/static/dist/assets/railroad-abnf-HS5TGJTU-gtd38edw.js +0 -1
  256. package/static/dist/assets/railroad-ebnf-LZEXJU2U-o238oax2.js +0 -1
  257. package/static/dist/assets/railroad-peg-WCYAUIDC-c4acqp2j.js +0 -1
  258. package/static/dist/assets/share._token-k4pcnxqu.js +0 -2
  259. package/static/dist/assets/stateDiagram-v2-MP3YSRHH-jtadbol7.js +0 -1
  260. package/static/dist/assets/swimlanesDiagram-VR7AAH4N-ki7weepw.js +0 -8
  261. package/static/dist/assets/todoStore-azvfuqbv.js +0 -1
  262. package/static/dist/assets/trash-osyo895g.js +0 -1
  263. package/static/dist/assets/useLocale-kdew6auf.js +0 -1
package/dist/redact.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { str } from './util.js';
2
+ import { snapshotArgs, toolArgs, toolDisplay, toolName } from './sessions/tool-display.js';
2
3
  /**
3
4
  * Credential redaction and the public session projection (Python
4
5
  * `api/helpers.py`). API responses are a hard boundary: transcript-bearing
@@ -22,7 +23,7 @@ const CRED_RE = new RegExp('(?<![A-Za-z0-9_-])(' +
22
23
  '|fc-[A-Za-z0-9]{10,}' +
23
24
  '|bb_live_[A-Za-z0-9_-]{10,}' +
24
25
  '|gAAAA[A-Za-z0-9_=-]{20,}' +
25
- '|AKIA[A-Z0-9]{16}' +
26
+ '|(?:AKIA|ASIA)[A-Z0-9]{16}' +
26
27
  '|sk_live_[A-Za-z0-9]{10,}' +
27
28
  '|sk_test_[A-Za-z0-9]{10,}' +
28
29
  '|rk_live_[A-Za-z0-9]{10,}' +
@@ -44,15 +45,142 @@ const CRED_RE = new RegExp('(?<![A-Za-z0-9_-])(' +
44
45
  '|mem0_[A-Za-z0-9]{10,}' +
45
46
  '|brv_[A-Za-z0-9]{10,}' +
46
47
  ')(?![A-Za-z0-9_-])', 'g');
47
- const AUTH_HDR_RE = /(Authorization:\s*(?:Bearer|Bot)\s+)([^\s'",\])]+)/gi;
48
- const EMBEDDED_AWS_RE = /AKIA[A-Z0-9]{16}/g;
48
+ /** One `name=value` auth parameter: an escaped-quoted, quoted or bare value. */
49
+ const AUTH_PARAM = String.raw `[A-Za-z0-9_-]+=(?:\\"(?:[^"\\\r\n]|\\[^"])*\\"|"(?:[^"\\\r\n]|\\.)*"|'[^'\r\n]*'|[^\s,"'\\]*)`;
50
+ /**
51
+ * The credential of an `Authorization:` header, after an optional scheme token read whole (`Bearer`, `ApiKey`,
52
+ * `AWS4-HMAC-SHA256`, `Custom_Scheme`, `2FA`, ...; RFC 7235 token characters other than the shell's quotes; an
53
+ * all-asterisk word is a mask, not a scheme).
54
+ * A parameterized credential (`Digest username="bob", response="..."`, `Credential=..., Signature=...`) is masked whole.
55
+ */
56
+ const AUTH_HDR_RE = new RegExp(String.raw `(Authorization:\s*(?:(?!\*+\s)[A-Za-z0-9!#$%&*+.^_|~-]+\s+(?=[^\s,\])]))?)(${AUTH_PARAM}(?:\s*,\s*${AUTH_PARAM})*|[^\s,\])][^\s'",\])]*)`, 'gi');
57
+ /** A JSON Web Token anywhere (`eyJ<header>.<payload>.<signature>`). */
58
+ const JWT_RE = /\beyJ[A-Za-z0-9_-]{4,}\.[A-Za-z0-9_-]{4,}\.[A-Za-z0-9_-]{4,}/g;
59
+ /** A bearer credential in any header or text (`X-Auth: Bearer ...`); `AUTH_HDR_RE` owns the `Authorization:` header. */
60
+ const BEARER_RE = /((?<!Authorization:\s{0,8})\bBearer\s+)([^\s,\])][^\s'",\])]*)/gi;
61
+ /** One shell-quoted piece, which may span lines: `'...'`, `"..."` (with backslash escapes), bash `$'...'` / `$"..."`, or JSON escaped inside a shell string (`\"...\"`). */
62
+ const QUOTED = String.raw `\$?'[^']*'|\$?"(?:[^"\\]|\\[\s\S])*"|\\"(?:[^"\\]|\\[^"])*\\"`;
63
+ /** A `"` / `'` not escaped by an odd run of backslashes. */
64
+ const UNESCAPED_QUOTE_RE = [/(?:^|[^\\])(?:\\\\)*"/, /(?:^|[^\\])(?:\\\\)*'/];
65
+ /** A quoted value's delimiters and content, or null for a bare value. */
66
+ function splitQuoted(value) {
67
+ const m = /^(\$?(?:\\"|"|'))([\s\S]*?)(\\"|"|')$/.exec(value);
68
+ if (!m || m[1].replace('$', '') !== m[3])
69
+ return null;
70
+ const [, open = '', inner = '', close = ''] = m;
71
+ // One piece only: its own delimiter never appears unescaped inside (`'bob':'pw'` is two pieces). A plain `'…'` has no
72
+ // escapes; `"…"` and `$'…'` escape with a backslash; escaped JSON (`\"…\"`) is left to its scanner.
73
+ if (open === "'" && inner.includes("'"))
74
+ return null;
75
+ if ((open === '"' || open === '$"' || open === "$'") && UNESCAPED_QUOTE_RE[close === "'" ? 1 : 0].test(inner))
76
+ return null;
77
+ return { open, inner, close };
78
+ }
79
+ /**
80
+ * A `Cookie:` / `Set-Cookie:` header's whole value (session cookies are credentials), up to the quote that encloses the
81
+ * header (`-H 'Cookie: a="b c"'`, `-H "Cookie: a=\"b c\""`), or to the line end when it is not quoted.
82
+ */
83
+ const COOKIE_ANSI_RE = /(\$'(?:Set-)?Cookie:\s*)((?:[^'\\\r\n]|\\.)*)/gi;
84
+ const COOKIE_SQ_RE = /((?<!\$)'(?:Set-)?Cookie:\s*)([^'\r\n]*)/gi;
85
+ const COOKIE_DQ_RE = /("(?:Set-)?Cookie:\s*)((?:[^"\\\r\n]|\\.)*)/gi;
86
+ const COOKIE_BARE_RE = new RegExp(String.raw `((?<!['"])\b(?:Set-)?Cookie:\s*)((?:${QUOTED}|[^'"\\\r\n]|\\(?!"))+)`, 'gi');
87
+ /**
88
+ * `user:password@host` with no scheme (curl reads it as a URL), or after an expansion that may supply one
89
+ * (`${SCHEME}bob:pw@host`). The password runs from the first `:` to the `@`, colons included (`bob:hunter:2@`);
90
+ * `git@host:org/repo` has no password before its `@`.
91
+ */
92
+ const BARE_USERINFO_RE = /(?<![^\s'"=(<,})\x60])([A-Za-z0-9._%+-]+:)([^\s@/'"\\]+)(?=@[^\s@/'"\\])/g;
93
+ /** A `key=value` whose key is percent-encoded (`api%5Fkey=`): the destination decodes the key once, so it is checked decoded. */
94
+ const PERCENT_KEY_RE = /(?<![A-Za-z0-9_.%-])((?=[A-Za-z0-9_.%-]*%[0-9A-Fa-f]{2})[A-Za-z0-9_.%-]+=)([^&#\s"'<>]*)/g;
95
+ const percentDecode = (text) => text.replace(/%([0-9A-Fa-f]{2})/g, (_, hex) => String.fromCharCode(parseInt(hex, 16)));
96
+ /**
97
+ * A listed argv (`['login', '--password', 'hunter2']`, JSON or a Python repr): a quoted flag, then its value (quoted, a
98
+ * bare scalar such as `123456`, or a flat list or dict).
99
+ */
100
+ const LISTED_FLAG_RE = /(["'])(-{1,2})([A-Za-z0-9_][A-Za-z0-9_.-]*)\1(\s*,\s*)(?:(["'])((?:\\.|(?!\5)[^\\])*)\5|(\[[^[\]]*\]|\{[^{}]*\}|[^\s,\])}'"[{]+))/g;
101
+ const EMBEDDED_AWS_RE = /(?:AKIA|ASIA)[A-Z0-9]{16}/g;
49
102
  const ENV_RE = /([A-Z0-9_]{0,50}(?:API_?KEY|TOKEN|SECRET|PASSWORD|PASSWD|CREDENTIAL|AUTH)[A-Z0-9_]{0,50})\s*=\s*(['"]?)(\S+)\2/g;
103
+ /**
104
+ * `scheme://user:secret@host` (database and basic-auth URLs): the password is masked, the user and host stay. The user
105
+ * and password may be assembled from quoted and escaped shell pieces (`bob:hun'ter2'@`), and every delimiter may be
106
+ * shell-escaped (`https\:\/\/`, `bob\:hunter2`, `hunter2\@`). Any scheme is accepted, a computed one included
107
+ * (`${SCHEME}://`, `$(printf https)://`), since `://` then `user:password@` is userinfo whatever precedes it.
108
+ */
109
+ const URL_USERINFO_RE = /(\\?:\\?\/\\?\/(?:[^\s:@/'"\\]|'[^'\n:@/]*'|"[^"\n:@/]*"|\\[^\s:@/])*\\?:)((?:[^\s@/'"\\]|'[^'\n@/]*'|"[^"\n@/]*"|\\[^\s@/])+)(?=\\?@)/g;
110
+ /** Credential key names in any case and naming style (`access_token`, `clientSecret`, `aws_secret_access_key`, `X-Api-Key`). */
111
+ const CRED_KEY_NAME = String.raw `(?:(?:access|refresh|id|auth)[_-]?token|api[_-]?key|access[_-]?key[_-]?id|client[_-]?secret|(?:private|access|secret|session)[_-]?key|credentials?|authorization|signature|cookie|bearer|secret[_-]?input|key[_-]?material|pass[_-]?phrase|pass(?:in|out)|secret|token|password|passwd)`;
112
+ /** The prefilter's view of the same key names, so it never skips text the credential rule would mask. */
113
+ const CRED_KEY_NAME_RE = new RegExp(CRED_KEY_NAME, 'i');
114
+ /** A credential name matched against a whole `_`-joined word run (`secret_access_key`, `session_token`). */
115
+ const CRED_KEY_NAME_WORDS_RE = new RegExp(String.raw `^${CRED_KEY_NAME}$`, 'i');
116
+ /**
117
+ * A shell word with its quote and escape characters removed, as the shell passes it (`--pass'word'` → `--password`),
118
+ * and a JSON `\uXXXX` escape decoded as a JSON parser does (`"pass\u0077ord"` → `password`).
119
+ */
120
+ const dequote = (text) => text.replace(/\\\r?\n/g, '').replace(/\\u([0-9A-Fa-f]{4})/g, (_, hex) => String.fromCharCode(parseInt(hex, 16))).replace(/\$(?=['"])|['"\\]/g, '');
121
+ /**
122
+ * A key names a credential when any of its path segments (`auth.token`, `database.password`, `auth[password]`,
123
+ * `auth["password"]`) ends in a credential name at a word boundary, however deep its namespace, once dequoted
124
+ * (`--pass'word'`)
125
+ * (`COMPANY_PROD_EU_AWS_SECRET_ACCESS_KEY`, `companyProdEuAwsSessionToken`).
126
+ */
127
+ function isCredentialKey(key) {
128
+ return dequote(key).split(/[.:/[\]]/).some((segment) => {
129
+ const words = segment.replace(/^-+/, '').split(/[_-]+|(?<=[a-z0-9])(?=[A-Z])|(?<=[A-Z])(?=[A-Z][a-z])/).filter(Boolean).slice(-8);
130
+ for (let i = 0; i < words.length; i += 1)
131
+ if (CRED_KEY_NAME_WORDS_RE.test(words.slice(i).join('_')))
132
+ return true;
133
+ return false;
134
+ });
135
+ }
136
+ /**
137
+ * A `key=value`, `key: value` or `--flag value` in text. Any identifier matches; the loop keeps only those
138
+ * `isCredentialKey` accepts (`access_token`, `"clientSecret"`, `X-Api-Key`, `--companyProdEuAwsSecretAccessKey`,
139
+ * `--auth["password"]`, the shell-composed `--pass'word'` / `--pass\word`), fails closed on a key the shell computes
140
+ * (`--pass$(printf word)`, `--pass$W`), and consumes a value only for those, so a non-credential key never swallows the
141
+ * text after it. The whole identifier is
142
+ * read, however long, and matched whole even without a separator: the scan never restarts inside an identifier, so it
143
+ * stays linear.
144
+ */
145
+ /**
146
+ * A piece of a key the shell computes: `$(…)` (flat, unquoted), `` `…` `` (no outer spaces), `${…}`, `$NAME`, a
147
+ * positional or special parameter (`$1`, `$@`, `$#`), or an
148
+ * ANSI-C `$'…'` with a backslash escape (`$'\x77ord'`; like any key piece, without spaces, `=` or `:`).
149
+ */
150
+ const SUBST_PIECE = String.raw `\$'(?=[^'\s=:]*\\)(?:[^'\\\s=:]|\\\S)*'|\$\([^()\n'"\x60\\]*\)|\x60(?![\s\x60])(?:[^\x60\n\\]|\\.)*(?<![\s\\])\x60|\$\{[^{}\n]*\}|\$[A-Za-z_][A-Za-z0-9_]*|\$[0-9@*#?$!-]`;
151
+ const CRED_PARAM_RE = new RegExp(String.raw `(?<![A-Za-z0-9_.[\]-])(-{0,2})((?:[A-Za-z0-9_]|${SUBST_PIECE}|(?<=-)\{(?=[^{}\s]*(?:,|\.\.))[^{}\s]*\}|(?<=-)(?=\$[({]|\x60|\{))(?:\[\\?["'][A-Za-z0-9_.-]*\\?["']\]|[A-Za-z0-9_.[\]-]|\$?'[A-Za-z0-9_.-]*'|\$?"[A-Za-z0-9_.-]*"|\\[A-Za-z0-9_.-]|${SUBST_PIECE}|\{(?=[^{}\s]*(?:,|\.\.))[^{}\s]*\})*)((?:\\?["'])?\s*\+?\\?[=:]\s*|\s+|)`, 'g');
152
+ /**
153
+ * A substitution, variable or brace-expansion piece of a key (`$(…)`, `` `…` ``, `${…}`, `$NAME`, `{a,b}`, `{1..3}`) right
154
+ * after an identifier character.
155
+ */
156
+ /** Where a key's first computed piece starts (`$…`, an escaped ANSI-C `$'…'`, a backtick, a brace expansion). */
157
+ const COMPUTED_PIECE_RE = /\$(?:[({A-Za-z_0-9@*#?$!-]|'(?=[^'\s=:]*\\))|\x60|\{(?=[^{}\s]*(?:,|\.\.))/;
158
+ /** Every computed piece of a key, to tell whether static text follows its first one. */
159
+ const COMPUTED_PIECES_RE = new RegExp(String.raw `${SUBST_PIECE}|\{(?=[^{}\s]*(?:,|\.\.))[^{}\s]*\}`, 'g');
160
+ const DYNAMIC_KEY_PIECE_RE = /[A-Za-z0-9_](?:\$[({A-Za-z_0-9@*#?$!-]|\$'(?=[^'\s=:]*\\)|`|\{(?=[^{}\s]*(?:,|\.\.)[^{}\s]*\}))/;
161
+ /** An `Authorization` value's first word when it is a scheme token (`Basic`), and the gap to the credential after it. */
162
+ const AUTH_SCHEME_WORD_RE = /^(?!\*+$)[A-Za-z0-9!#$%&*+.^_|~-]+$/;
163
+ const AUTH_SCHEME_GAP_RE = /[ \t]+(?=\S)/y;
164
+ const ENV_KEY_NAME_RE = /API_?KEY|TOKEN|SECRET|PASSWORD|PASSWD|CREDENTIAL|AUTH/;
165
+ /**
166
+ * `curl -u user:secret` / `-uuser:secret` / `--user user:secret`, and the proxy forms `-U` / `--proxy-user` (and the
167
+ * unique abbreviations curl accepts, `--proxy-u` / `--proxy-us` / `--proxy-use`); a quoted
168
+ * pair or quoted secret is masked through its closing quote.
169
+ */
170
+ const USER_FLAG_RE = /(?<![A-Za-z0-9-])(?:-[uU][ \t]*|--(?:user|proxy-u(?:s(?:e(?:r)?)?)?)[ \t]+)(?=\S)/g;
171
+ /** The prefilter's view of `USER_FLAG_RE`. */
172
+ const USER_FLAG_TEST_RE = new RegExp(USER_FLAG_RE.source);
173
+ const QUERY_KEY_RE = /([?&]key=)([^\s"'&#]+)/gi;
50
174
  const PRIVKEY_RE = /-----BEGIN[A-Z ]*PRIVATE KEY-----[\s\S]*?-----END[A-Z ]*PRIVATE KEY-----/g;
175
+ /** A private key whose end marker is missing (a display cap cut it off): masked to the end of the text. */
176
+ const PRIVKEY_OPEN_RE = /-----BEGIN[A-Z ]*PRIVATE KEY-----[\s\S]*$/;
51
177
  const CODE_ENV_KEY_LITERAL_RE = /([A-Z0-9_]{0,50}(?:API_?KEY|TOKEN|SECRET|PASSWORD|PASSWD|CREDENTIAL|AUTH)[A-Z0-9_]{0,50}=)(["'][)\]:,]+|[)\]:,]+)/y;
52
178
  const ENV_KEY_PREFIX_RE = /([A-Z0-9_]{0,50}(?:API_?KEY|TOKEN|SECRET|PASSWORD|PASSWD|CREDENTIAL|AUTH)[A-Z0-9_]{0,50}=)/g;
53
179
  const REDACTED_ENV_VALUE_RE = /(?:\*{3,}|[A-Za-z0-9][A-Za-z0-9_.:/+-]{0,32}\.\.\.[A-Za-z0-9_.:/+-]{1,16})/y;
54
180
  function mask(token) {
55
- return token.length >= 18 ? `${token.slice(0, 6)}...${token.slice(-4)}` : '***';
181
+ // By code point, so a partial mask never splits a surrogate pair into invalid JSON.
182
+ const chars = Array.from(token);
183
+ return chars.length >= 18 ? `${chars.slice(0, 6).join('')}...${chars.slice(-4).join('')}` : '***';
56
184
  }
57
185
  /** Restore `KEY=)` style code literals the env regex would otherwise mangle. */
58
186
  function restoreCodeEnvKeyLiterals(original, redacted) {
@@ -91,25 +219,1139 @@ function restoreCodeEnvKeyLiterals(original, redacted) {
91
219
  pieces.push(redacted.slice(last));
92
220
  return pieces.join('');
93
221
  }
222
+ /**
223
+ * The index of the quote closing an enclosing `quote` (`'`, `"` or ANSI-C `$'`) at or after `from` (`-1` if none),
224
+ * skipping escaped quotes inside `"…"` and `$'…'`, memoized per quoted region so many values inside one long quoted
225
+ * argument share one scan.
226
+ */
227
+ function enclosingClose(text) {
228
+ let cachedQuote = '';
229
+ let cachedFrom = -1;
230
+ let cachedClose = -1;
231
+ return (from, quote) => {
232
+ if (quote === cachedQuote && from >= cachedFrom && (cachedClose === -1 || from <= cachedClose))
233
+ return cachedClose;
234
+ const close = quote.slice(-1);
235
+ let k = from;
236
+ while (k < text.length && text[k] !== close)
237
+ k += quote !== "'" && text[k] === '\\' ? 2 : 1;
238
+ cachedQuote = quote;
239
+ cachedFrom = from;
240
+ cachedClose = k < text.length ? k : -1;
241
+ return cachedClose;
242
+ };
243
+ }
244
+ /**
245
+ * The index of the `\"` closing a JSON string escaped inside a double-quoted shell string, scanning from `from` (just after
246
+ * the opening `\"`): shell escapes decode first (`\\` is a JSON backslash, `\"` a JSON quote), then a JSON backslash escapes
247
+ * the next JSON character, so `\\\"` inside the value is an escaped quote, not the close. `-1` if none.
248
+ */
249
+ function escapedJsonClose(text, from) {
250
+ let jsonEscape = false;
251
+ for (let k = from; k < text.length;) {
252
+ let json;
253
+ let width;
254
+ if (text[k] === '\\' && (text[k + 1] === '\\' || text[k + 1] === '"')) {
255
+ json = text[k + 1];
256
+ width = 2;
257
+ }
258
+ else {
259
+ json = text[k];
260
+ width = 1;
261
+ }
262
+ if (jsonEscape)
263
+ jsonEscape = false;
264
+ else if (json === '\\')
265
+ jsonEscape = true;
266
+ else if (json === '"')
267
+ return width === 2 ? k : -1;
268
+ k += width;
269
+ }
270
+ return -1;
271
+ }
272
+ /**
273
+ * The end (exclusive) of the shell word starting at `start`: adjacent quoted (`'…'`, `"…"` with escapes, `$'…'`), escaped
274
+ * (`\ `) and bare pieces, a leading `[…]`/`{…}`/`(…)` container (balanced; the sidecar's Python repr of nested args), or JSON escaped inside a shell string (`\"…\"`).
275
+ * It stops at shell metacharacters outside a container, at `, ] }` that follow the word, and at the quote that encloses the argument
276
+ * (`-H "X-Api-Key: value"`). An unterminated quote at the word's start runs to the line end; one mid-word ends the word.
277
+ * One pass, so redaction stays linear.
278
+ */
279
+ function shellWordEnd(text, start, enclosing, closeOf = enclosingClose(text)) {
280
+ const lineEnd = (from) => { const n = text.indexOf('\n', from); return n === -1 ? text.length : n; };
281
+ // Inside an enclosing quote a bare value is literal up to that quote's close, spaces and newlines included; a quoted,
282
+ // container or escaped-JSON value keeps its own piece structure.
283
+ if (enclosing && !/^(?:\$?["'`]|[[{(]|\\")/.test(text.slice(start, start + 2))) {
284
+ const close = closeOf(start, enclosing);
285
+ if (close === -1)
286
+ return lineEnd(start);
287
+ // The word goes on past the enclosing quote when an adjacent piece follows (`'--password=foo'bar`).
288
+ // An empty continuation (`"…", "x"`: JSON structure) leaves the word, and the enclosing quote, at the close.
289
+ const next = text[close + 1] ?? '';
290
+ const after = next && !/[\s&;|<>()]/.test(next) ? shellWordEnd(text, close + 1, '', closeOf) : close;
291
+ return after === close + 1 ? close : after;
292
+ }
293
+ let depth = 0;
294
+ let i = start;
295
+ while (i < text.length) {
296
+ const c = text[i];
297
+ if (i !== start && c === enclosing.slice(-1))
298
+ return i;
299
+ // `\"…\"` is a piece (JSON escaped inside a shell string) at the value's start or in a container; mid-word, `\"` is
300
+ // an escaped quote like any other escape.
301
+ if (c === '\\' && text[i + 1] === '"' && i !== start && depth === 0)
302
+ i += 2;
303
+ else if (c === '\\' && text[i + 1] === '"') {
304
+ const close = escapedJsonClose(text, i + 2);
305
+ if (close === -1)
306
+ return lineEnd(i);
307
+ i = close + 2;
308
+ }
309
+ else if (c === '\\')
310
+ i += 2;
311
+ // ANSI-C `$'…'`: a backslash escapes the next character, `\'` included.
312
+ else if (c === '$' && text[i + 1] === "'") {
313
+ let k = i + 2;
314
+ while (k < text.length && text[k] !== "'")
315
+ k += text[k] === '\\' ? 2 : 1;
316
+ if (k >= text.length)
317
+ return i === start ? lineEnd(i) : i;
318
+ i = k + 1;
319
+ }
320
+ else if (c === '$' && text[i + 1] === '"')
321
+ i += 1;
322
+ // A command substitution (`$(…)`, backticks) cannot be bounded without a shell parser (`case` patterns carry unmatched
323
+ // `)`, backticks nest by escaping, and either may span lines): mask to the end of the text. `${…}` is balanced, quote-
324
+ // and escape-aware.
325
+ else if (c === '$' && text[i + 1] === '(')
326
+ return text.length;
327
+ // Process substitution (`<(…)`, `>(…)`) likewise.
328
+ else if ((c === '<' || c === '>') && text[i + 1] === '(')
329
+ return text.length;
330
+ else if (c === '$' && text[i + 1] === '{') {
331
+ let k = i + 2;
332
+ let d = 1;
333
+ while (k < text.length && d > 0) {
334
+ const ch = text[k];
335
+ if (ch === '\\')
336
+ k += 2;
337
+ else if (ch === "'") {
338
+ const q = text.indexOf("'", k + 1);
339
+ k = q === -1 ? text.length : q + 1;
340
+ }
341
+ else if (ch === '"') {
342
+ k += 1;
343
+ while (k < text.length && text[k] !== '"')
344
+ k += text[k] === '\\' ? 2 : 1;
345
+ k += 1;
346
+ }
347
+ // A command substitution inside the expansion can hold a literal `}`: mask to the line end.
348
+ else if ((ch === '$' && text[k + 1] === '(') || ch === '`')
349
+ return text.length;
350
+ else {
351
+ if (ch === '{')
352
+ d += 1;
353
+ else if (ch === '}')
354
+ d -= 1;
355
+ k += 1;
356
+ }
357
+ }
358
+ if (d > 0)
359
+ return lineEnd(i);
360
+ i = k;
361
+ }
362
+ else if (c === '`')
363
+ return text.length;
364
+ else if (c === "'") {
365
+ const close = text.indexOf("'", i + 1);
366
+ if (close === -1)
367
+ return i === start ? lineEnd(i) : i;
368
+ i = close + 1;
369
+ }
370
+ else if (c === '"') {
371
+ let k = i + 1;
372
+ while (k < text.length && text[k] !== '"')
373
+ k += text[k] === '\\' ? 2 : 1;
374
+ if (k >= text.length)
375
+ return i === start ? lineEnd(i) : i;
376
+ i = k + 1;
377
+ }
378
+ else if ((c === '[' || c === '{' || (c === '(' && i === start)) && (i === start || depth > 0)) {
379
+ depth += 1;
380
+ i += 1;
381
+ }
382
+ else if (c === '(' && depth > 0) {
383
+ depth += 1;
384
+ i += 1;
385
+ }
386
+ else if ((c === ']' || c === '}' || c === ')') && depth > 0) {
387
+ depth -= 1;
388
+ i += 1;
389
+ // A leading container is the whole value.
390
+ if (depth === 0)
391
+ return i;
392
+ }
393
+ else if (c === '\n')
394
+ return i;
395
+ // Shell metacharacters end a word outside a container.
396
+ else if (depth === 0 && /[\s&;|<>()]/.test(c))
397
+ return i;
398
+ // `,` `]` `}` inside a bare word are part of it (`correct]horse`); before a space, quote or the end they are structure.
399
+ else if (depth === 0 && /[,\]}]/.test(c) && !/^[^\s,\]})"'\\]/.test(text[i + 1] ?? ''))
400
+ return i;
401
+ else
402
+ i += 1;
403
+ }
404
+ return Math.min(i, text.length);
405
+ }
406
+ /**
407
+ * The shell quote open at each position, read left to right once: `advance(index)` returns the quote (`'` or `"`) that
408
+ * encloses `index`, or `''`. Quotes span lines (a multi-line argument); an apostrophe inside a word is prose.
409
+ */
410
+ function quoteTracker(text) {
411
+ let pos = 0;
412
+ let state = '';
413
+ let escaped = -1;
414
+ return (index) => {
415
+ for (; pos < index; pos += 1) {
416
+ const c = text[pos];
417
+ if (c === '\\' && state !== "'")
418
+ escaped = pos += 1;
419
+ // An apostrophe between two letters is prose (`don't`), not a quote.
420
+ else if (c === "'" && /\p{L}/u.test(text[pos - 1] ?? '') && /\p{L}/u.test(text[pos + 1] ?? ''))
421
+ continue;
422
+ // `$'…'` is ANSI-C quoted: a backslash inside escapes the next character, `\'` included.
423
+ else if (state === '' && c === "'" && text[pos - 1] === '$' && escaped !== pos - 1)
424
+ state = "$'";
425
+ else if (state === '' && (c === "'" || c === '"'))
426
+ state = c;
427
+ else if (c === state.slice(-1))
428
+ state = '';
429
+ }
430
+ return state;
431
+ };
432
+ }
433
+ /** A masked shell word: a single quoted piece keeps its quotes (`'***'`), anything else is `***`. */
434
+ function maskShellWord(value) {
435
+ const quoted = splitQuoted(value);
436
+ return quoted && !/^\$?(["']).*\1.+/s.test(value) ? `${quoted.open}***${quoted.close}` : '***';
437
+ }
438
+ /** A shell word's content, for the "nothing to mask" checks: the inside of a single quoted piece, else the word. */
439
+ const shellWordInner = (value) => splitQuoted(value)?.inner ?? value;
440
+ /** Credential parameters (`CRED_PARAM_RE`) with their whole shell-word value fully masked. */
441
+ function redactCredentialParams(text) {
442
+ let out = '';
443
+ let last = 0;
444
+ const quoteAt = quoteTracker(text);
445
+ const closeOf = enclosingClose(text);
446
+ let queryWordEnd = -1;
447
+ CRED_PARAM_RE.lastIndex = 0;
448
+ for (let m = CRED_PARAM_RE.exec(text); m; m = CRED_PARAM_RE.exec(text)) {
449
+ const [head, dash = '', key = '', sep = ''] = m;
450
+ const keyEnd = m.index + head.length;
451
+ // A computed piece may supply the separator itself (`--password${SEP}hunter2` with `SEP='='`) or the name
452
+ // (`--${KEY}${SEP}hunter2`): from the key's first computed piece, the rest of the key is masked (and the value after a
453
+ // real `=`/`:`) when the static part before it names a credential, or when a flag's name goes on after it.
454
+ const computedAt = key.search(COMPUTED_PIECE_RE);
455
+ const staticTail = computedAt >= 0 && key.slice(computedAt).replace(COMPUTED_PIECES_RE, '') !== '';
456
+ // A long flag computed whole (`--${KEY} hunter2`) may name a credential and take the next word; a short one
457
+ // (`ls -$OPTS dir`) is an option bundle.
458
+ const computedLongFlag = computedAt === 0 && dash === '--' && /^\s+$/.test(sep);
459
+ if (computedAt >= 0 && ((computedAt > 0 && isCredentialKey(key.slice(0, computedAt))) || (dash && staticTail) || computedLongFlag)) {
460
+ // A flag whose name ends in the computed piece (`--password${X} hunter2`) takes its value from the next word.
461
+ const nextWord = dash && !staticTail && /^\s+$/.test(sep);
462
+ const end = /[=:]/.test(sep) || nextWord ? shellWordEnd(text, keyEnd, quoteAt(keyEnd), closeOf) : m.index + dash.length + key.length;
463
+ out += `${text.slice(last, m.index + dash.length + computedAt)}***${nextWord ? `${sep}***` : ''}`;
464
+ last = end;
465
+ CRED_PARAM_RE.lastIndex = Math.max(last, CRED_PARAM_RE.lastIndex);
466
+ continue;
467
+ }
468
+ // A key the shell computes from its first piece (`--$(printf password)=`, `--${KEY}=`, a header `${HEADER}: x`) is one
469
+ // only with `=` or a `:` and a space: `$HOST:$PORT`, `-$OPTS dir` and a Markdown `` `code`: `` are not assignments. A
470
+ // leading backtick needs a flag besides.
471
+ const computedStart = /^(?:\$[({A-Za-z_0-9@*#?$!-]|\$'(?=[^'\s=:]*\\)|\x60|\{)/.test(key);
472
+ // A `-H`/`--header` argument is a header even with the value tight against its colon (`-H "${HEADER}:Basic x"`).
473
+ const headerArg = sep.startsWith(':') && /(?:^|\s)(?:-H|--header)[ \t]*["']?$/.test(text.slice(Math.max(0, m.index - 16), m.index));
474
+ if (computedStart && ((!/=|:\s/.test(sep) && !headerArg) || (key.startsWith('\x60') && !dash)) && sep)
475
+ continue;
476
+ // A substitution the key grammar cannot parse (`$(` nested or quoted, `${` nested) may still build a credential name:
477
+ // fail closed to the end of the text, as `shellWordEnd` does for a substitution in a value. An unclosed backtick or a
478
+ // nested brace expansion does so only after a flag: in prose a backtick is a Markdown code span's close
479
+ // (`` `code` ``). A backtick piece inside a key has no outer spaces, so the prose between two code spans
480
+ // (`` ` and ` ``) is never read as one.
481
+ if (!sep && ((/^\$[({]/.test(text.slice(keyEnd, keyEnd + 2)) && (dash || !computedStart)) || (dash && /^[\x60{]/.test(text[keyEnd] ?? '')))) {
482
+ out += `${text.slice(last, keyEnd)}***`;
483
+ last = text.length;
484
+ break;
485
+ }
486
+ // No separator: the identifier is matched whole anyway, so the scan never restarts inside it (`a'a'a'…` stays linear).
487
+ if (!sep)
488
+ continue;
489
+ // Prose (`secret sauce`): a bare space only separates a CLI flag from its value.
490
+ if (!/[=:]/.test(sep) && !dash)
491
+ continue;
492
+ // A key with a substitution or variable piece may name a credential once the shell expands it: fail closed.
493
+ if (!computedStart && !DYNAMIC_KEY_PIECE_RE.test(key) && !isCredentialKey(key))
494
+ continue;
495
+ const valueStart = m.index + head.length;
496
+ // A bare `Authorization: <scheme> <credential>` header is `AUTH_HDR_RE`'s (decided before scanning the value).
497
+ if (/authorization$/i.test(key) && /^:\s*$/.test(sep) && !/^\$?["']/.test(text.slice(valueStart, valueStart + 2)))
498
+ continue;
499
+ const query = /[?&]/.test(text[m.index - 1] ?? '') && sep === '=';
500
+ // A URL query parameter's value ends at the next `&` or `#` inside its shell word (`?token=foo'bar&x=1` passes
501
+ // `foobar`). The URL's word end is shared by its parameters, so many parameters still scan it once.
502
+ const wordEnd = query && valueStart < queryWordEnd ? queryWordEnd : shellWordEnd(text, valueStart, quoteAt(valueStart), closeOf);
503
+ if (query)
504
+ queryWordEnd = wordEnd;
505
+ const queryCut = query ? text.slice(valueStart, wordEnd).search(/[&#]/) : -1;
506
+ let valueEnd = queryCut === -1 ? wordEnd : valueStart + queryCut;
507
+ // An `Authorization` header `AUTH_HDR_RE` cannot read (`Authorization : Basic x`, `'Authoriz'ation': Basic x`): its
508
+ // scheme word and the credential after it are masked together.
509
+ if (!query && sep.includes(':') && /authorization$/i.test(dequote(key)) && AUTH_SCHEME_WORD_RE.test(text.slice(valueStart, valueEnd))) {
510
+ AUTH_SCHEME_GAP_RE.lastIndex = valueEnd;
511
+ if (AUTH_SCHEME_GAP_RE.exec(text))
512
+ valueEnd = shellWordEnd(text, AUTH_SCHEME_GAP_RE.lastIndex, quoteAt(AUTH_SCHEME_GAP_RE.lastIndex), closeOf);
513
+ }
514
+ const value = text.slice(valueStart, valueEnd);
515
+ const inner = shellWordInner(value);
516
+ const quoted = /["']/.test(value);
517
+ // Nothing to mask: empty or already masked.
518
+ if (!inner.trim() || inner === '***')
519
+ continue;
520
+ // `ENV_RE` masks an unquoted upper-case `KEY=value` it covers when the whole value is one plain `\S+` token.
521
+ if (!quoted && sep.includes('=') && !sep.includes('+') && key === key.toUpperCase() && !/["'\\$`]/.test(key) && ENV_KEY_NAME_RE.test(key) && /[A-Za-z0-9]/.test(inner) && /^[^\s\\]+$/.test(value))
522
+ continue;
523
+ // Fully masked: a partial mask would leak part of a password or passphrase.
524
+ out += text.slice(last, valueStart) + (/^[[{(]/.test(value) ? '***' : maskShellWord(value));
525
+ last = valueEnd;
526
+ CRED_PARAM_RE.lastIndex = Math.max(valueEnd, CRED_PARAM_RE.lastIndex);
527
+ }
528
+ return out + text.slice(last);
529
+ }
530
+ /**
531
+ * `curl -u user:secret` (also `-uuser:secret`, `--user user:secret`, `-U` / `--proxy-user`): the whole argument is read
532
+ * with the shell-word scanner, and everything after its first `:` is masked. A single quoted pair keeps its quotes
533
+ * (`"bob:***"`).
534
+ */
535
+ function redactUserFlags(text) {
536
+ let out = '';
537
+ let last = 0;
538
+ const quoteAt = quoteTracker(text);
539
+ const closeOf = enclosingClose(text);
540
+ USER_FLAG_RE.lastIndex = 0;
541
+ for (let m = USER_FLAG_RE.exec(text); m; m = USER_FLAG_RE.exec(text)) {
542
+ const wordStart = m.index + m[0].length;
543
+ const wordEnd = shellWordEnd(text, wordStart, quoteAt(wordStart), closeOf);
544
+ const word = text.slice(wordStart, wordEnd);
545
+ // One quoted pair (`"bob:pw"`) is split inside its quotes; otherwise the word's first `:` separates user and secret
546
+ // (`'bob':'pw'`, `bob:'pw'`).
547
+ const quoted = splitQuoted(word);
548
+ const body = quoted ? quoted.inner : word;
549
+ const colon = body.indexOf(':');
550
+ // No literal `:`, but an expansion may supply it (`bob${SEP}hunter2`): literal text after it may be the password.
551
+ const at = colon === -1 ? firstExpansion(word) : -1;
552
+ if (at >= 0 && expansionEnd(word, at) < word.length) {
553
+ out += `${text.slice(last, wordStart)}${word.slice(0, at)}***`;
554
+ last = wordEnd;
555
+ USER_FLAG_RE.lastIndex = Math.max(wordEnd, USER_FLAG_RE.lastIndex);
556
+ continue;
557
+ }
558
+ const secret = colon === -1 ? '' : body.slice(colon + 1);
559
+ if (!secret || secret === '***' || shellWordInner(secret) === '***')
560
+ continue;
561
+ const masked = quoted ? `${quoted.open}${body.slice(0, colon + 1)}***${quoted.close}` : `${body.slice(0, colon + 1)}${maskShellWord(secret)}`;
562
+ out += text.slice(last, wordStart) + masked;
563
+ last = wordEnd;
564
+ USER_FLAG_RE.lastIndex = Math.max(wordEnd, USER_FLAG_RE.lastIndex);
565
+ }
566
+ return out + text.slice(last);
567
+ }
568
+ /**
569
+ * Header credentials (`AUTH_HDR_RE`, `BEARER_RE`: a head group, then the credential). A parameterized credential is masked
570
+ * whole; a single credential is read to the end of its shell word, so an adjacent quoted piece (`Bearer foo'bar`, which
571
+ * the shell passes as `foobar`) is masked with it.
572
+ */
573
+ function redactHeaderCredentials(text, re) {
574
+ let out = '';
575
+ let last = 0;
576
+ const quoteAt = quoteTracker(text);
577
+ const closeOf = enclosingClose(text);
578
+ re.lastIndex = 0;
579
+ for (let m = re.exec(text); m; m = re.exec(text)) {
580
+ const [whole, head = '', token = ''] = m;
581
+ const start = m.index + head.length;
582
+ const matchEnd = m.index + whole.length;
583
+ let masked;
584
+ let end = matchEnd;
585
+ if (/^[A-Za-z0-9_-]+=/.test(token))
586
+ masked = '***';
587
+ else {
588
+ end = Math.max(matchEnd, shellWordEnd(text, start, quoteAt(start), closeOf));
589
+ masked = end === matchEnd && !/["'\\]/.test(token) ? mask(token) : maskShellWord(text.slice(start, end));
590
+ }
591
+ out += text.slice(last, start) + masked;
592
+ last = end;
593
+ re.lastIndex = Math.max(end, re.lastIndex);
594
+ }
595
+ return out + text.slice(last);
596
+ }
597
+ /** Stands in for a quoted or escaped space in a dequoted word, so the rules read it as part of one token. */
598
+ const WORD_SPACE = '\u2423';
599
+ /**
600
+ * A shell word as the program receives it: quotes and `$` quote prefixes removed, escapes resolved (ANSI-C escapes
601
+ * decoded), and a quoted or escaped space kept inside the token as `WORD_SPACE`.
602
+ */
603
+ /** An ANSI-C `$'…'` escape after its backslash (at `j`): the character Bash decodes and how many characters it spans. */
604
+ function ansiEscape(word, j) {
605
+ const c = word[j];
606
+ const named = { a: '\x07', b: '\b', e: '\x1b', E: '\x1b', f: '\f', n: '\n', r: '\r', t: '\t', v: '\v', '\\': '\\', "'": "'", '"': '"', '?': '?' };
607
+ if (named[c] !== undefined)
608
+ return [named[c], 1];
609
+ const hex = (max) => /^[0-9A-Fa-f]+/.exec(word.slice(j + 1, j + 1 + max))?.[0] ?? '';
610
+ if (c === 'x' || c === 'u' || c === 'U') {
611
+ const digits = hex(c === 'x' ? 2 : c === 'u' ? 4 : 8);
612
+ if (digits)
613
+ return [String.fromCodePoint(Math.min(parseInt(digits, 16), 0x10ffff)), 1 + digits.length];
614
+ }
615
+ const octal = /^[0-7]{1,3}/.exec(word.slice(j, j + 3))?.[0];
616
+ if (octal)
617
+ return [String.fromCharCode(parseInt(octal, 8) & 0xff), octal.length];
618
+ if (c === 'c' && j + 1 < word.length)
619
+ return [String.fromCharCode(word.charCodeAt(j + 1) & 0x1f), 2];
620
+ return [`\\${c}`, 1];
621
+ }
622
+ function shellDequote(word) {
623
+ let out = '';
624
+ let quote = '';
625
+ for (let i = 0; i < word.length; i += 1) {
626
+ const c = word[i];
627
+ // ANSI-C `$'…'`: escapes are decoded (`\x3d` is `=`), so an escaped key name or delimiter reads as the program's.
628
+ if (!quote && c === '$' && word[i + 1] === "'") {
629
+ for (i += 2; i < word.length && word[i] !== "'"; i += 1) {
630
+ let ch = word[i];
631
+ if (ch === '\\' && i + 1 < word.length) {
632
+ const [decoded, span] = ansiEscape(word, i + 1);
633
+ ch = decoded;
634
+ i += span;
635
+ }
636
+ out += /\s/.test(ch) ? WORD_SPACE : ch;
637
+ }
638
+ continue;
639
+ }
640
+ if (!quote && c === '$' && word[i + 1] === '"')
641
+ continue;
642
+ if (!quote && (c === "'" || c === '"'))
643
+ quote = c;
644
+ else if (quote && c === quote)
645
+ quote = '';
646
+ else if (c === '\\' && quote !== "'" && i + 1 < word.length) {
647
+ i += 1;
648
+ const escaped = word[i];
649
+ // A line continuation (`\` then a newline) is removed, joining the word's pieces.
650
+ if (escaped !== '\n')
651
+ out += /\s/.test(escaped) ? WORD_SPACE : escaped;
652
+ }
653
+ else
654
+ out += quote && /\s/.test(c) ? WORD_SPACE : c;
655
+ }
656
+ return out;
657
+ }
658
+ /** A text's word tokens, counted. */
659
+ function tokenCounts(text) {
660
+ const counts = new Map();
661
+ for (const token of text.split(/[^\p{L}\p{N}_]+/u))
662
+ if (token)
663
+ counts.set(token, (counts.get(token) ?? 0) + 1);
664
+ return counts;
665
+ }
666
+ /** Whether `got` shows a whole token of `plain` more often than `wanted` does: a credential the rules masked in `wanted` survives. */
667
+ function revealsMore(plain, got, wanted) {
668
+ const source = tokenCounts(plain);
669
+ const allowed = tokenCounts(wanted);
670
+ for (const [token, count] of tokenCounts(got))
671
+ if (source.has(token) && count > (allowed.get(token) ?? 0))
672
+ return true;
673
+ return false;
674
+ }
675
+ /** The index of a shell word's first parameter or command expansion outside single and ANSI-C quotes (`-1` if none). */
676
+ function firstExpansion(word, from = 0) {
677
+ let quote = '';
678
+ for (let i = from; i < word.length; i += 1) {
679
+ const c = word[i];
680
+ if (quote === "'") {
681
+ if (c === "'")
682
+ quote = '';
683
+ continue;
684
+ }
685
+ if (c === '\\') {
686
+ i += 1;
687
+ continue;
688
+ }
689
+ if (!quote && c === '$' && word[i + 1] === "'") {
690
+ for (i += 2; i < word.length && word[i] !== "'"; i += word[i] === '\\' ? 2 : 1)
691
+ ;
692
+ continue;
693
+ }
694
+ if (c === '"')
695
+ quote = quote ? '' : '"';
696
+ else if (!quote && c === "'")
697
+ quote = "'";
698
+ else if (c === '`' || (c === '$' && /[({A-Za-z_0-9@*#?$!-]/.test(word[i + 1] ?? '')))
699
+ return i;
700
+ }
701
+ return -1;
702
+ }
703
+ /**
704
+ * A word whose expansion may supply a delimiter, masked from where the expansion could reveal a credential: after a
705
+ * credential key (`--password"${SEP}"hunter2`), or in a URL's authority after a `:` and non-port text
706
+ * (`https://bob:hunter2${AT}host`). `null` when neither applies (`$HOST:8080`, `$USER:$PASS@host`, `--token-file=$HOME`).
707
+ */
708
+ function redactExpansionWord(word, at) {
709
+ // Only a quoted expansion after the key: an unquoted one is the computed-key rules' (`--password${SEP}x`).
710
+ const before = shellDequote(word.slice(0, at)).replaceAll(WORD_SPACE, ' ');
711
+ const key = /(?:^|[^A-Za-z0-9_.[\]-])-{0,2}([A-Za-z0-9_][A-Za-z0-9_.[\]-]*)[=:]?$/.exec(before);
712
+ if (key && /["']/.test(word.slice(0, at)) && isCredentialKey(key[1]))
713
+ return `${word.slice(0, at)}***`;
714
+ const url = /:\/\/([^\s/]*)/.exec(word);
715
+ if (!url)
716
+ return expansionBeforeAt(word);
717
+ const authEnd = url.index + url[0].length;
718
+ const authStart = authEnd - url[1].length;
719
+ const inAuthority = firstExpansion(word.slice(authStart, authEnd));
720
+ if (inAuthority < 0)
721
+ return expansionBeforeAt(word);
722
+ const expansion = authStart + inAuthority;
723
+ const colon = word.lastIndexOf(':', expansion - 1);
724
+ if (colon >= authStart && /[^0-9]/.test(word.slice(colon + 1, expansion)))
725
+ return `${word.slice(0, colon + 1)}***${word.slice(authEnd)}`;
726
+ // The expansion may supply the user/password `:` itself (`bob${SEP}hunter2@host`): static text between it and a later
727
+ // `@` or expansion may be a password (`$HOST:8080`, `$SUB.example.com` and `${U}:${P}@` hold none).
728
+ const after = expansionEnd(word, expansion);
729
+ const stop = word.slice(after, authEnd).search(/\\?@|\$[({A-Za-z_0-9@*#?$!-]|`/);
730
+ if (stop <= 0 || !/[^:]/.test(word.slice(after, after + stop)))
731
+ return expansionBeforeAt(word);
732
+ return word[after + stop] === '$' || word[after + stop] === '`' ? `${word.slice(0, expansion)}***${word.slice(authEnd)}` : `${word.slice(0, expansion)}***${word.slice(after + stop)}`;
733
+ }
734
+ /**
735
+ * Literal text between a word's last expansion before its `@` and that `@` (`${SCHEME}${USER}hunter2@host`): the
736
+ * expansions may supply the scheme and the user/password `:`, so it may be a password (`${U}:${P}@`, `$HOME/a@b` hold
737
+ * none).
738
+ */
739
+ function expansionBeforeAt(word) {
740
+ const atSign = word.search(/\\?@/);
741
+ if (atSign <= 0)
742
+ return null;
743
+ let last = -1;
744
+ for (let k = firstExpansion(word); k >= 0 && k < atSign; k = firstExpansion(word, expansionEnd(word, k)))
745
+ last = k;
746
+ if (last < 0)
747
+ return null;
748
+ const end = expansionEnd(word, last);
749
+ const segment = word.slice(end, atSign);
750
+ if (!segment || !/[^:]/.test(segment) || /[\s/]/.test(segment))
751
+ return null;
752
+ // A literal `:` in it separates a visible user from the password (`${SCHEME}bob:hunter2@`).
753
+ const keep = end + segment.indexOf(':') + 1;
754
+ return `${word.slice(0, keep)}***${word.slice(atSign)}`;
755
+ }
756
+ /** The end (exclusive) of the parameter or command expansion starting at `i`. */
757
+ function expansionEnd(word, i) {
758
+ const next = word[i + 1] ?? '';
759
+ const close = (c, from) => { const k = word.indexOf(c, from); return k === -1 ? word.length : k + 1; };
760
+ if (word[i] === '`')
761
+ return close('`', i + 1);
762
+ if (next === '{')
763
+ return close('}', i + 2);
764
+ if (next === '(') {
765
+ let depth = 0;
766
+ for (let k = i + 1; k < word.length; k += 1) {
767
+ if (word[k] === '(')
768
+ depth += 1;
769
+ else if (word[k] === ')' && --depth === 0)
770
+ return k + 1;
771
+ }
772
+ return word.length;
773
+ }
774
+ if (/[A-Za-z_]/.test(next))
775
+ return i + 1 + (/^[A-Za-z0-9_]*/.exec(word.slice(i + 1))?.[0].length ?? 0);
776
+ return i + 2;
777
+ }
778
+ /**
779
+ * Shell words whose quoting or escapes compose a delimiter, key or credential (`--password'='x`, `bob:pw'@'host`): each
780
+ * is redacted as the program receives it. When the word as written, once redacted, still shows a token the dequoted
781
+ * redaction masks, the redacted dequoted form replaces it, so no quoting variant hides a credential from the rules. A
782
+ * quoted flag is read with the value word after it (`'--password' x`). A single quoted argument and a data container
783
+ * (JSON, a Python repr) are not composed words and keep their quoting. A word with an expansion is not what the program
784
+ * receives once dequoted: it fails closed where the expansion may supply a delimiter (`redactExpansionWord`), and a
785
+ * substitution is otherwise the computed-key rules'.
786
+ */
787
+ function redactComposedWords(text) {
788
+ if (!/["'\\$`]/.test(text))
789
+ return text;
790
+ const closeOf = enclosingClose(text);
791
+ const words = [];
792
+ for (let i = 0; i < text.length;) {
793
+ if (/\s/.test(text[i])) {
794
+ i += 1;
795
+ continue;
796
+ }
797
+ const end = Math.max(i + 1, shellWordEnd(text, i, '', closeOf));
798
+ words.push([i, end]);
799
+ i = end;
800
+ }
801
+ let out = '';
802
+ let last = 0;
803
+ for (let k = 0; k < words.length; k += 1) {
804
+ const [start, end] = words[k];
805
+ const word = text.slice(start, end);
806
+ if (/^[[{(]/.test(word))
807
+ continue;
808
+ const at = firstExpansion(word);
809
+ const expanded = at >= 0 ? redactExpansionWord(word, at) : null;
810
+ if (expanded !== null) {
811
+ out += text.slice(last, start) + expanded;
812
+ last = end;
813
+ continue;
814
+ }
815
+ if (!/["'\\]/.test(word) || /\$[({]|`/.test(word))
816
+ continue;
817
+ const glued = shellDequote(word);
818
+ // A quoted flag (`'--password'`, `"--us"er`) takes its value from the next word: the two are read together.
819
+ const next = words[k + 1];
820
+ const flag = /^-[^=:]*$/.test(glued) && next !== undefined && /^[ \t]+$/.test(text.slice(end, next[0]));
821
+ // A single quoted argument keeps its quoting, unless it is ANSI-C quoted with escapes (`$'--password\x3dx'`).
822
+ if (!flag && (!/["'\\]/.test(word.slice(1)) || (splitQuoted(word) && !/\$'[^']*\\/.test(word))))
823
+ continue;
824
+ const spanEnd = flag ? next[1] : end;
825
+ const span = text.slice(start, spanEnd);
826
+ const unit = flag ? `${glued} ${shellDequote(text.slice(next[0], next[1]))}` : glued;
827
+ // The leak check reads quoted spaces as spaces (prose apostrophes pair up across words); the replacement keeps them
828
+ // inside the token, so a quoted value is masked whole.
829
+ const plain = unit.replaceAll(WORD_SPACE, ' ');
830
+ const wanted = redactRules(plain);
831
+ if (wanted === plain)
832
+ continue;
833
+ const asWritten = shellDequote(redactRules(span)).replaceAll(WORD_SPACE, ' ');
834
+ if (!revealsMore(plain, asWritten, wanted))
835
+ continue;
836
+ out += text.slice(last, start) + redactRules(unit).replaceAll(WORD_SPACE, ' ');
837
+ last = spanEnd;
838
+ if (flag)
839
+ k += 1;
840
+ }
841
+ return out + text.slice(last);
842
+ }
843
+ /**
844
+ * `NAME=` at a word start: an assignment wherever the shell may read one (after `;`, `{`, `then`, `export`, as a prefix
845
+ * …). Reading more assignments than the shell runs only adds masks.
846
+ */
847
+ const ASSIGNMENT_RE = /(?<![^\s;&|(){}!\x60])([A-Za-z_][A-Za-z0-9_]*)(\+?)=/g;
848
+ /** `$NAME` or `${NAME}`; a parameter operator (`${NAME:-x}`, `${#NAME}`) is not a plain reference. */
849
+ const VAR_REF_RE = /\$(?:\{([A-Za-z_][A-Za-z0-9_]*)\}|([A-Za-z_][A-Za-z0-9_]*))/y;
850
+ const VAR_REFS_RE = new RegExp(VAR_REF_RE.source, 'g');
851
+ /** Blanks before the next word. */
852
+ const BLANK_RUN_RE = /[ \t]+(?=\S)/y;
853
+ /** A builtin that takes `NAME=value` operands, quoted ones included (`export "KEY=x"`). */
854
+ const ASSIGNMENT_BUILTIN_RE = /(?<![^\s;&|(){}!\x60])(?:export|declare|typeset|readonly|local)(?=[ \t])/g;
855
+ /** The end of the `$NAME` / `${NAME}` reference at `at`. */
856
+ const VAR_REF_END = (text, at) => {
857
+ VAR_REF_RE.lastIndex = at;
858
+ return VAR_REF_RE.exec(text) ? VAR_REF_RE.lastIndex : at + 1;
859
+ };
860
+ /**
861
+ * The end of an assignment's value word, and whether it is literal (bare, quoted and escaped pieces) or a template that
862
+ * also holds plain references (`$OPT`, `"${A}x"`). Any other expansion, a substitution or an unterminated quote makes it
863
+ * neither, and the scan stops there, so no value is scanned twice.
864
+ */
865
+ function assignedWord(text, start) {
866
+ const unknown = (end) => ({ end, literal: false, template: false });
867
+ if (text[start] === '(')
868
+ return unknown(start);
869
+ let template = false;
870
+ let i = start;
871
+ while (i < text.length) {
872
+ const c = text[i];
873
+ if (/[\s;&|<>()]/.test(c))
874
+ break;
875
+ if (c === '\\')
876
+ i += 2;
877
+ else if (c === '$' && text[i + 1] === "'") {
878
+ let k = i + 2;
879
+ while (k < text.length && text[k] !== "'")
880
+ k += text[k] === '\\' ? 2 : 1;
881
+ if (k >= text.length)
882
+ return unknown(i);
883
+ i = k + 1;
884
+ }
885
+ else if (c === '$' || c === '`') {
886
+ VAR_REF_RE.lastIndex = i;
887
+ if (c === '`' || !VAR_REF_RE.exec(text))
888
+ return unknown(i);
889
+ template = true;
890
+ i = VAR_REF_RE.lastIndex;
891
+ }
892
+ else if (c === "'") {
893
+ const close = text.indexOf("'", i + 1);
894
+ if (close === -1)
895
+ return unknown(i);
896
+ i = close + 1;
897
+ }
898
+ else if (c === '"') {
899
+ let k = i + 1;
900
+ while (k < text.length && text[k] !== '"') {
901
+ if (text[k] === '`')
902
+ return unknown(k);
903
+ if (text[k] === '$') {
904
+ VAR_REF_RE.lastIndex = k;
905
+ if (!VAR_REF_RE.exec(text))
906
+ return unknown(k);
907
+ template = true;
908
+ k = VAR_REF_RE.lastIndex;
909
+ }
910
+ else
911
+ k += text[k] === '\\' ? 2 : 1;
912
+ }
913
+ if (k >= text.length)
914
+ return unknown(i);
915
+ i = k + 1;
916
+ }
917
+ else
918
+ i += 1;
919
+ }
920
+ return { end: Math.min(i, text.length), literal: !template, template };
921
+ }
922
+ /**
923
+ * A template value (`"${A}x"`) with its references replaced through `lookup`, dequoted; `undefined` when a reference is
924
+ * unknown or the value grows past `cap`.
925
+ */
926
+ function expandTemplate(word, lookup, cap) {
927
+ let out = '';
928
+ let last = 0;
929
+ let quote = '';
930
+ for (let i = 0; i < word.length; i += 1) {
931
+ const c = word[i];
932
+ if (c === '\\')
933
+ i += 1;
934
+ else if (!quote && c === "'")
935
+ i = Math.max(i, word.indexOf("'", i + 1));
936
+ else if (!quote && c === '$' && word[i + 1] === "'")
937
+ for (i += 2; i < word.length && word[i] !== "'"; i += word[i] === '\\' ? 2 : 1)
938
+ ;
939
+ else if (c === '"')
940
+ quote = quote ? '' : '"';
941
+ else if (c === '$') {
942
+ VAR_REF_RE.lastIndex = i;
943
+ const ref = VAR_REF_RE.exec(word);
944
+ const value = ref ? lookup(ref[1] ?? ref[2]) : undefined;
945
+ if (value === undefined || out.length + value.length > cap)
946
+ return undefined;
947
+ out += word.slice(last, i) + substitutedWord(value, quote);
948
+ last = VAR_REF_RE.lastIndex;
949
+ i = last - 1;
950
+ }
951
+ }
952
+ return shellDequote(out + word.slice(last)).replaceAll(WORD_SPACE, ' ');
953
+ }
954
+ /**
955
+ * The assignments (`NAME=value`) outside quotes, in text order. A value is one shell word, dequoted, or a template of
956
+ * plain references; one with another expansion, a substitution or an append (`+=`) is unknown. One pass.
957
+ */
958
+ function inlineAssignments(text) {
959
+ const found = [];
960
+ const quoteAt = quoteTracker(text);
961
+ ASSIGNMENT_RE.lastIndex = 0;
962
+ for (let m = ASSIGNMENT_RE.exec(text); m; m = ASSIGNMENT_RE.exec(text)) {
963
+ const start = m.index + m[0].length;
964
+ // An assignment inside a quoted argument, or after an escaped separator (`A=foo\;B=x` is one word), is text.
965
+ if (quoteAt(m.index))
966
+ continue;
967
+ let escapes = 0;
968
+ for (let k = m.index - 2; k >= 0 && text[k] === '\\'; k -= 1)
969
+ escapes += 1;
970
+ if (escapes % 2)
971
+ continue;
972
+ const { end, literal, template } = assignedWord(text, start);
973
+ const word = text.slice(start, end);
974
+ // An append (`OPT+=u`) is a template of the previous value and the word.
975
+ if (!literal && !template)
976
+ found.push({ name: m[1], start, end, at: start, value: undefined });
977
+ else if (m[2])
978
+ found.push({ name: m[1], start, end, at: end, value: undefined, template: `\${${m[1]}}${word}` });
979
+ else
980
+ found.push({ name: m[1], start, end, at: end, value: literal ? shellDequote(word).replaceAll(WORD_SPACE, ' ') : undefined, ...(template ? { template: word } : {}) });
981
+ }
982
+ // A quoted operand of an assignment builtin (`export "KEY=--password"`) is an assignment too; the whole operand is its
983
+ // value word.
984
+ const closeOf = enclosingClose(text);
985
+ const builtinAt = quoteTracker(text);
986
+ ASSIGNMENT_BUILTIN_RE.lastIndex = 0;
987
+ for (let m = ASSIGNMENT_BUILTIN_RE.exec(text); m; m = ASSIGNMENT_BUILTIN_RE.exec(text)) {
988
+ if (builtinAt(m.index))
989
+ continue;
990
+ let i = m.index + m[0].length;
991
+ for (;;) {
992
+ while (text[i] === ' ' || text[i] === '\t')
993
+ i += 1;
994
+ if (i >= text.length || /[;&|<>()\n#]/.test(text[i]))
995
+ break;
996
+ const end = Math.max(i + 1, shellWordEnd(text, i, '', closeOf));
997
+ const word = text.slice(i, end);
998
+ const operand = /^\$?["']/.test(word) ? /^([A-Za-z_][A-Za-z0-9_]*)(\+?)=([\s\S]*)$/.exec(shellDequote(word).replaceAll(WORD_SPACE, ' ')) : null;
999
+ if (operand && firstExpansion(word) >= 0)
1000
+ found.push({ name: operand[1], start: i, end, at: i, value: undefined });
1001
+ else if (operand?.[2])
1002
+ found.push({ name: operand[1], start: i, end, at: end, value: undefined, template: `\${${operand[1]}}${substitutedWord(operand[3], '')}` });
1003
+ else if (operand)
1004
+ found.push({ name: operand[1], start: i, end, at: end, value: operand[3] });
1005
+ i = end;
1006
+ }
1007
+ ASSIGNMENT_BUILTIN_RE.lastIndex = Math.max(i, ASSIGNMENT_BUILTIN_RE.lastIndex);
1008
+ }
1009
+ return found.sort((a, b) => a.start - b.start);
1010
+ }
1011
+ /** The text with each shell comment (`#` at a word start, outside quotes, to the line end) replaced by spaces. */
1012
+ function blankComments(text) {
1013
+ let out = '';
1014
+ let last = 0;
1015
+ let quote = '';
1016
+ for (let i = 0; i < text.length; i += 1) {
1017
+ const c = text[i];
1018
+ if (c === '\\' && quote !== "'")
1019
+ i += 1;
1020
+ else if (quote) {
1021
+ if (c === quote.slice(-1))
1022
+ quote = '';
1023
+ }
1024
+ else if (c === "'" || c === '"')
1025
+ quote = text[i - 1] === '$' && c === "'" ? "$'" : c;
1026
+ else if (c === '#' && /^[\s;&|()]?$/.test(text[i - 1] ?? '')) {
1027
+ const end = text.indexOf('\n', i);
1028
+ const stop = end === -1 ? text.length : end;
1029
+ out += text.slice(last, i) + ' '.repeat(stop - i);
1030
+ last = i = stop;
1031
+ }
1032
+ }
1033
+ return last ? out + text.slice(last) : text;
1034
+ }
1035
+ /**
1036
+ * A substituted value as the shell passes it: escaped inside `"…"`; outside, split into fields at blanks, each field
1037
+ * single-quoted unless it is plain.
1038
+ */
1039
+ function substitutedWord(value, quote) {
1040
+ if (quote)
1041
+ return value.replace(/["\\$`]/g, '\\$&');
1042
+ return value.split(/[ \t\n]+/).filter(Boolean).map((field) => (/^[\w.:/@%+,=-]*$/.test(field) ? field : `'${field.replaceAll("'", `'\\''`)}'`)).join(' ');
1043
+ }
1044
+ /**
1045
+ * The text with each `$NAME` / `${NAME}` replaced by a value assigned to it earlier, outside single and ANSI-C quotes,
1046
+ * as the shell expands it, and where each substituted value sits in it. `pick` chooses among the name's assignments so
1047
+ * far (in text order, an unknown one included) and returns `-1` to leave the reference as written, as unknown names and
1048
+ * parameter operators are. One pass. A substitution past the size cap (a long value referenced many times) is left as
1049
+ * written, and its assignment is reported, as is each reference to a `watch`ed name.
1050
+ */
1051
+ function expandAssignments(text, assignments, pick, watch = new Set()) {
1052
+ const spans = [];
1053
+ const watched = [];
1054
+ const overflow = new Set();
1055
+ const history = new Map();
1056
+ const cap = text.length * 2 + 4096;
1057
+ // A template's value is resolved in this view when it takes effect.
1058
+ const resolved = new Map();
1059
+ // ponytail: templates share one size budget; past it a template is unknown.
1060
+ let templateBudget = cap;
1061
+ const valueOf = (n) => (assignments[n].template === undefined ? assignments[n].value : resolved.get(n));
1062
+ let next = 0;
1063
+ let quote = '';
1064
+ let out = '';
1065
+ let last = 0;
1066
+ for (let i = 0; i < text.length; i += 1) {
1067
+ for (; next < assignments.length && assignments[next].at <= i; next += 1) {
1068
+ const { name, template } = assignments[next];
1069
+ if (template !== undefined) {
1070
+ const value = expandTemplate(template, (ref) => { const refs = history.get(ref); const n = refs ? pick(ref, refs) : -1; return n >= 0 ? valueOf(n) : undefined; }, templateBudget);
1071
+ templateBudget -= value?.length ?? 0;
1072
+ resolved.set(next, value);
1073
+ }
1074
+ const past = history.get(name);
1075
+ if (past)
1076
+ past.push(next);
1077
+ else
1078
+ history.set(name, [next]);
1079
+ }
1080
+ const c = text[i];
1081
+ if (c === '\\')
1082
+ i += 1;
1083
+ else if (!quote && c === "'") {
1084
+ const close = text.indexOf("'", i + 1);
1085
+ if (close === -1)
1086
+ break;
1087
+ i = close;
1088
+ }
1089
+ else if (!quote && c === '$' && text[i + 1] === "'") {
1090
+ for (i += 2; i < text.length && text[i] !== "'"; i += text[i] === '\\' ? 2 : 1)
1091
+ ;
1092
+ }
1093
+ else if (c === '"')
1094
+ quote = quote ? '' : '"';
1095
+ else if (c === '$') {
1096
+ VAR_REF_RE.lastIndex = i;
1097
+ const ref = VAR_REF_RE.exec(text);
1098
+ const name = ref ? ref[1] ?? ref[2] : '';
1099
+ if (watch.has(name))
1100
+ watched.push(i);
1101
+ const assignment = ref && history.has(name) ? pick(name, history.get(name)) : -1;
1102
+ const value = assignment < 0 ? undefined : valueOf(assignment);
1103
+ if (value === undefined)
1104
+ continue;
1105
+ const word = out.length + value.length > cap ? '' : substitutedWord(value, quote);
1106
+ if (!word && value) {
1107
+ overflow.add(assignment);
1108
+ continue;
1109
+ }
1110
+ out += text.slice(last, i);
1111
+ spans.push([out.length, out.length + word.length, assignment, value]);
1112
+ out += word;
1113
+ last = VAR_REF_RE.lastIndex;
1114
+ i = last - 1;
1115
+ }
1116
+ }
1117
+ return { expanded: out + text.slice(last), spans, overflow, watched };
1118
+ }
1119
+ /** A word, and a shell-word unit (a run between shell and URL delimiters, `hunter2!!!`); `*` is a mask's. */
1120
+ const SECRET_WORD_RE = /[\p{L}\p{N}_]+/gu;
1121
+ const SECRET_UNIT_RE = /[^\s'"`$\\=:@/;&|<>(){}[\],*]+/gu;
1122
+ const HAS_SECRET_UNIT_RE = new RegExp(SECRET_UNIT_RE.source, 'u');
1123
+ /** A text's words and units, counted together. */
1124
+ function secretTokenCounts(text) {
1125
+ const counts = new Map();
1126
+ for (const re of [SECRET_WORD_RE, SECRET_UNIT_RE])
1127
+ for (const [token] of text.matchAll(re))
1128
+ counts.set(token, (counts.get(token) ?? 0) + 1);
1129
+ return counts;
1130
+ }
1131
+ /**
1132
+ * Threat model: the redactor reads only the tool's text. Variables the text itself assigns (`SEP=:; curl -u bob${SEP}x`)
1133
+ * are resolved in text order; shell control flow (conditionals, loops, functions, `read`, `unset`) is not modelled.
1134
+ * Variables from the environment or from earlier tool calls are unknown: the fail-closed rules (computed key tails, `-u`
1135
+ * expansions, URL authorities built from expansions) are the only guard for them, and a secret assembled entirely from
1136
+ * unknown variables can still be published.
1137
+ */
94
1138
  export function redactSensitive(text) {
1139
+ if (!text)
1140
+ return text;
1141
+ // Line continuations join their lines as the shell runs them (`--user \⏎ bob:pw`, `--pass\⏎word=x`): that view is
1142
+ // redacted, and a text with nothing to mask keeps its lines as written.
1143
+ const joined = text.replace(/\\\r?\n/g, '');
1144
+ const out = redactRules(redactComposedWords(joined));
1145
+ const redacted = joined !== text && out === joined ? text : out;
1146
+ if (!joined.includes('=') || !joined.includes('$'))
1147
+ return redacted;
1148
+ // The views with the text's own assignments resolved only add masks to the redaction above, which keeps its own
1149
+ // fail-closed masks (an assignment the shell never runs, `false && KEY=x`, can unmask nothing): the whole value of an
1150
+ // assignment whose substitution a view masks, and every word and unit a view masks, wherever it appears.
1151
+ // Comments are blanked, keeping every position, so a quote in one (`# don't`) cannot hide what follows.
1152
+ const code = blankComments(joined);
1153
+ const assignments = inlineAssignments(code);
1154
+ const secretAssignments = new Set();
1155
+ const secrets = new Set();
1156
+ const delimiterSecrets = new Set();
1157
+ const secretValues = new Set();
1158
+ // Control flow is not modelled, so a reassigned name may hold any of its values, or an unknown one (`OPT=-u; false &&
1159
+ // OPT=echo`, `false && OPT=$(x)`). Besides the latest values, each combination of the reassigned names' values gets a
1160
+ // view: from its first assignment on, a name holds the chosen value.
1161
+ // Candidates are told apart by literal value, template text, or being unknown.
1162
+ const byName = new Map();
1163
+ for (const [n, { name, value, template }] of assignments.entries()) {
1164
+ const values = byName.get(name) ?? new Map();
1165
+ const key = value !== undefined ? `=${value}` : template !== undefined ? `$${template}` : '?';
1166
+ if (!values.has(key))
1167
+ values.set(key, n);
1168
+ byName.set(name, values);
1169
+ }
1170
+ const reassigned = [...byName].filter(([, values]) => values.size > 1);
1171
+ // ponytail: past 8 combinations (2 in a text over 50k characters, to keep redaction fast), a word with a reassigned
1172
+ // name's reference fails closed instead.
1173
+ const budget = joined.length > 50_000 ? 2 : 8;
1174
+ let combinations = [new Map()];
1175
+ for (const [name, values] of reassigned) {
1176
+ combinations = combinations.flatMap((chosen) => [...values.values()].map((n) => new Map(chosen).set(name, n)));
1177
+ if (combinations.length > budget)
1178
+ break;
1179
+ }
1180
+ const latest = (_, history) => history.at(-1);
1181
+ const views = [latest];
1182
+ const failClosed = combinations.length > budget;
1183
+ if (reassigned.length && !failClosed)
1184
+ for (const chosen of combinations)
1185
+ views.push((name, history) => { const n = chosen.get(name); return n !== undefined && history.at(-1) >= n ? n : history.at(-1); });
1186
+ // Failing closed watches the reassigned names and, in text order, every name whose template refers to a watched one
1187
+ // (`ARG=$OPT`).
1188
+ // A name the text computes (`OPT=$(printf -- -u)`) is always watched.
1189
+ const watchedNames = new Set(failClosed ? reassigned.map(([name]) => name) : []);
1190
+ for (const { name, value, template } of assignments)
1191
+ if (value === undefined && template === undefined)
1192
+ watchedNames.add(name);
1193
+ if (watchedNames.size)
1194
+ for (const { name, template } of assignments)
1195
+ if (template !== undefined && [...template.matchAll(VAR_REFS_RE)].some((ref) => watchedNames.has(ref[1] ?? ref[2])))
1196
+ watchedNames.add(name);
1197
+ // Ranges of the text masked before redaction: secret assignment values, and words that fail closed.
1198
+ const masks = [];
1199
+ for (const [v, pick] of views.entries()) {
1200
+ const { expanded, spans, overflow, watched } = expandAssignments(code, assignments, pick, v === 0 ? watchedNames : undefined);
1201
+ // A reference to a watched name is masked to the end of its word, and a whole-word one with the next word
1202
+ // (`$OPT bob:hunter2`, `-H "${H}: x"`).
1203
+ if (watched.length) {
1204
+ const quoteAt = quoteTracker(code);
1205
+ const closeOf = enclosingClose(code);
1206
+ for (const at of watched) {
1207
+ const quote = quoteAt(at);
1208
+ let end = shellWordEnd(code, at, quote, closeOf);
1209
+ if (quote && code[end] === quote.slice(-1))
1210
+ end += 1;
1211
+ // A word of references and quotes only (`$OPT`, `"$OPT"`, `$A$B`) may be an option that takes the next word.
1212
+ if (/(?:^|\s)\$?["']*$/.test(code.slice(Math.max(0, at - 4), at)) && /^(?:["']|\$\{[A-Za-z_]\w*\}|\$[A-Za-z_]\w*)*$/.test(code.slice(VAR_REF_END(code, at), end))) {
1213
+ BLANK_RUN_RE.lastIndex = end;
1214
+ if (BLANK_RUN_RE.exec(code))
1215
+ end = shellWordEnd(code, BLANK_RUN_RE.lastIndex, '', closeOf);
1216
+ }
1217
+ // A reference inside the previous mask extends it (`$OPT $OPT2 bob:hunter2`).
1218
+ const previous = masks.at(-1);
1219
+ if (previous && at < previous[1] && previous[0] <= at)
1220
+ previous[1] = Math.max(previous[1], end);
1221
+ else
1222
+ masks.push([at, end]);
1223
+ }
1224
+ }
1225
+ // Past the expansion cap, a value is taken as a secret.
1226
+ for (const n of overflow)
1227
+ secretAssignments.add(n);
1228
+ if (!spans.length)
1229
+ continue;
1230
+ const view = redactRules(redactComposedWords(expanded));
1231
+ const shown = secretTokenCounts(view);
1232
+ const found = new Set();
1233
+ for (const [token, count] of secretTokenCounts(expanded))
1234
+ if (count > (shown.get(token) ?? 0))
1235
+ found.add(token);
1236
+ // A masked unit that a substitution built (`P=hunt; …${P}er2`) masks its value's assignment, and its literal pieces.
1237
+ let k = 0;
1238
+ for (const m of expanded.matchAll(SECRET_UNIT_RE)) {
1239
+ if (!found.has(m[0]))
1240
+ continue;
1241
+ const end = m.index + m[0].length;
1242
+ while (k < spans.length && spans[k][1] <= m.index)
1243
+ k += 1;
1244
+ // A piece after an unbraced reference reads as one unit with its name (`$P!!!`).
1245
+ let cut = m.index;
1246
+ let name = '';
1247
+ for (let j = k; j < spans.length && spans[j][0] < end; j += 1) {
1248
+ const piece = expanded.slice(cut, Math.max(spans[j][0], m.index));
1249
+ if (piece)
1250
+ secrets.add(piece).add(name + piece);
1251
+ secretAssignments.add(spans[j][2]);
1252
+ secretValues.add(spans[j][3]);
1253
+ name = assignments[spans[j][2]].name;
1254
+ cut = Math.min(spans[j][1], end);
1255
+ }
1256
+ if (cut > m.index && cut < end)
1257
+ secrets.add(expanded.slice(cut, end)).add(name + expanded.slice(cut, end));
1258
+ }
1259
+ for (const token of found)
1260
+ secrets.add(token);
1261
+ // A value of delimiters only (`P='@@@'`) has no unit to count: it is a secret when the view shows it fewer times,
1262
+ // counted as substituted, escapes included (`"$P"` with `P='$$$'` is `\$\$\$`). This fails closed on a delimiter a
1263
+ // masked credential also held (`SEP='='` beside `--token=x`).
1264
+ // ponytail: past 32 such values, each is taken as a secret rather than counted.
1265
+ const delimiterValues = new Map();
1266
+ for (const [start, end, n, value] of spans) {
1267
+ const word = expanded.slice(start, end);
1268
+ if (!value || HAS_SECRET_UNIT_RE.test(value))
1269
+ continue;
1270
+ let secret = delimiterValues.get(word);
1271
+ if (secret === undefined)
1272
+ delimiterValues.set(word, (secret = delimiterValues.size >= 32 || expanded.split(word).length > view.split(word).length));
1273
+ if (secret) {
1274
+ secretAssignments.add(n);
1275
+ delimiterSecrets.add(value);
1276
+ }
1277
+ }
1278
+ }
1279
+ // A secret value is masked at every assignment of it, however spelled (`P=$'hunter\x32'`), and a delimiter-only one
1280
+ // wherever it stands as a whole word (`echo @@@`).
1281
+ for (const n of secretAssignments) {
1282
+ const { value } = assignments[n];
1283
+ if (value !== undefined)
1284
+ secretValues.add(value);
1285
+ }
1286
+ for (const value of delimiterSecrets)
1287
+ secretValues.add(value);
1288
+ for (const [n, { value }] of assignments.entries())
1289
+ if (value !== undefined && secretValues.has(value))
1290
+ secretAssignments.add(n);
1291
+ secrets.delete('');
1292
+ if (!secrets.size && !secretAssignments.size && !masks.length && !delimiterSecrets.size)
1293
+ return redacted;
1294
+ for (const [n, { start, end }] of assignments.entries())
1295
+ if (secretAssignments.has(n))
1296
+ masks.push([start, end]);
1297
+ let base = out;
1298
+ if (masks.length) {
1299
+ let masked = '';
1300
+ let last = 0;
1301
+ for (const [start, end] of masks.sort((a, b) => a[0] - b[0])) {
1302
+ if (end <= last)
1303
+ continue;
1304
+ const from = Math.max(start, last);
1305
+ masked += joined.slice(last, from) + maskShellWord(joined.slice(from, end));
1306
+ last = end;
1307
+ }
1308
+ base = redactRules(redactComposedWords(masked + joined.slice(last)));
1309
+ }
1310
+ if (delimiterSecrets.size)
1311
+ base = base.replace(new RegExp(String.raw `(?<![^\s'"=])(?:${[...delimiterSecrets].map((value) => value.replace(/[.*+?^${}()|[\]\\/]/g, '\\$&')).join('|')})(?![^\s'"])`, 'g'), '***');
1312
+ const masked = base.replace(SECRET_UNIT_RE, (unit) => (secrets.has(unit) ? '***' : unit.replace(SECRET_WORD_RE, (word) => (secrets.has(word) ? '***' : word))));
1313
+ return masked === out ? redacted : masked;
1314
+ }
1315
+ function redactRules(text) {
95
1316
  if (!text)
96
1317
  return text;
97
1318
  let out = text.replace(CRED_RE, (_, t) => mask(t));
98
1319
  out = out.replace(EMBEDDED_AWS_RE, (t) => mask(t));
99
- out = out.replace(AUTH_HDR_RE, (_, head, token) => head + mask(token));
1320
+ out = redactHeaderCredentials(out, AUTH_HDR_RE);
1321
+ out = out.replace(JWT_RE, (t) => mask(t));
1322
+ out = redactHeaderCredentials(out, BEARER_RE);
1323
+ for (const re of [COOKIE_ANSI_RE, COOKIE_SQ_RE, COOKIE_DQ_RE, COOKIE_BARE_RE])
1324
+ out = out.replace(re, (whole, head, value) => (/[A-Za-z0-9]/.test(value) ? `${head}***` : whole));
1325
+ out = redactCredentialParams(out);
100
1326
  out = out.replace(ENV_RE, (whole, key, quote, value) => (/[A-Za-z0-9]/.test(value) ? `${key}=${quote}${mask(value)}${quote}` : whole));
101
- out = out.replace(PRIVKEY_RE, '[REDACTED PRIVATE KEY]');
1327
+ out = out.replace(LISTED_FLAG_RE, (whole, q, dash, key, gap, vq, quotedValue, bare) => {
1328
+ // An unquoted value (a number, `True`, a nested list) is masked whole.
1329
+ if (bare !== undefined)
1330
+ return bare !== '***' && (ARGV_USER_FLAG_RE.test(dash + key) || isCredentialKey(key)) ? `${q}${dash}${key}${q}${gap}***` : whole;
1331
+ const value = quotedValue ?? '';
1332
+ if (!value || value === '***')
1333
+ return whole;
1334
+ if (ARGV_USER_FLAG_RE.test(dash + key))
1335
+ return value.includes(':') ? `${q}${dash}${key}${q}${gap}${vq}${value.replace(/:.*/s, ':***')}${vq}` : whole;
1336
+ return isCredentialKey(key) ? `${q}${dash}${key}${q}${gap}${vq}***${vq}` : whole;
1337
+ });
1338
+ out = out.replace(PERCENT_KEY_RE, (whole, head, value) => (value && value !== '***' && isCredentialKey(percentDecode(head.slice(0, -1))) ? `${head}***` : whole));
1339
+ out = out.replace(BARE_USERINFO_RE, (_, head, secret) => head + mask(secret));
1340
+ out = out.replace(URL_USERINFO_RE, (_, head, secret) => head + (/['"\\]/.test(secret) ? '***' : mask(secret)));
1341
+ out = redactUserFlags(out);
1342
+ out = out.replace(QUERY_KEY_RE, (whole, head, value) => (/[A-Za-z0-9]/.test(value) ? head + mask(value) : whole));
1343
+ out = out.replace(PRIVKEY_RE, '[REDACTED PRIVATE KEY]').replace(PRIVKEY_OPEN_RE, '[REDACTED PRIVATE KEY]');
102
1344
  return restoreCodeEnvKeyLiterals(text, out);
103
1345
  }
104
1346
  const CASE_MARKERS = [
105
- 'sk-', 'ghp_', 'github_pat_', 'gho_', 'ghu_', 'ghs_', 'ghr_', 'AKIA', 'xoxb-', 'xoxa-', 'xoxp-', 'xoxr-', 'xoxs-', 'AIza', 'pplx-', 'fal_', 'fc-',
1347
+ 'sk-', 'ghp_', 'github_pat_', 'gho_', 'ghu_', 'ghs_', 'ghr_', 'AKIA', 'ASIA', 'xoxb-', 'xoxa-', 'xoxp-', 'xoxr-', 'xoxs-', 'AIza', 'pplx-', 'fal_', 'fc-',
106
1348
  'bb_live_', 'gAAAA', 'sk_live_', 'sk_test_', 'rk_live_', 'SG.', 'hf_', 'r8_', 'npm_', 'pypi-', 'dop_v1_', 'doo_v1_', 'am_', 'sk_', 'tvly-', 'exa_',
107
1349
  'gsk_', 'syt_', 'retaindb_', 'hsk-', 'mem0_', 'brv_', 'eyJ', '-----BEGIN',
108
1350
  ];
109
1351
  const LOWER_MARKERS = [
110
1352
  'authorization: bearer ', 'authorization: bot ', 'private key', 'postgres://', 'postgresql://', 'mysql://', 'mongodb://', 'redis://', 'amqp://', '://',
111
1353
  'access_token', 'refresh_token', 'id_token', 'api_key', 'apikey', 'client_secret', 'auth_token', 'raw_secret', 'secret_input', 'key_material',
112
- 'x-amz-signature', 'token=', 'secret=', 'password=', 'authorization=', 'key=', '"token"', '"secret"', '"password"', '"bearer"',
1354
+ 'x-amz-signature', 'token=', 'secret=', 'password=', 'passwd', 'password', 'secret', 'token', 'api-key', 'apikey', 'clientsecret', 'private_key', 'credential', ' -u ', '--user ', 'authorization', 'signature', 'bearer ', 'cookie:', 'authorization=', 'key=', '"token"', '"secret"', '"password"', '"bearer"',
113
1355
  ];
114
1356
  const TELEGRAM_RE = /(?:bot)?\d{8,}:[-A-Za-z0-9_]{30,}/;
115
1357
  const DISCORD_RE = /<@!?\d{17,20}>/;
@@ -122,6 +1364,22 @@ export function mightContainSensitiveText(text) {
122
1364
  const lower = text.toLowerCase();
123
1365
  if (LOWER_MARKERS.some((m) => lower.includes(m)))
124
1366
  return true;
1367
+ if (CRED_KEY_NAME_RE.test(text))
1368
+ return true;
1369
+ // A key or URL the shell assembles from pieces (`--pass'word'`, `https\:\/\/`) is recognizable only once dequoted.
1370
+ if (/["'\\]/.test(text)) {
1371
+ const plain = dequote(text);
1372
+ if (CRED_KEY_NAME_RE.test(plain) || plain.includes('://'))
1373
+ return true;
1374
+ }
1375
+ if (/%[0-9A-Fa-f]{2}/.test(text) && CRED_KEY_NAME_RE.test(percentDecode(text)))
1376
+ return true;
1377
+ if (text.includes('@') && new RegExp(BARE_USERINFO_RE.source).test(text))
1378
+ return true;
1379
+ if (DYNAMIC_KEY_PIECE_RE.test(text) || /\$[({A-Za-z_0-9@*#?$!'-]|`/.test(text))
1380
+ return true;
1381
+ if (USER_FLAG_TEST_RE.test(text))
1382
+ return true;
125
1383
  if (text.includes(':') && TELEGRAM_RE.test(text))
126
1384
  return true;
127
1385
  if (text.includes('<@') && DISCORD_RE.test(text))
@@ -286,15 +1544,141 @@ function publicMessageProjection(message, enabled, activeTurnToken) {
286
1544
  }
287
1545
  if (isActive)
288
1546
  item._active_turn_user = true;
1547
+ return withMessageToolDisplay(scrubbed, item, enabled);
1548
+ }
1549
+ /**
1550
+ * A redacted call with its server `kind` and `target`, taken from the raw call's parsed args redacted once (never a
1551
+ * redacted JSON string, which masks a different span), so a call shows one target live, after replay and after reload.
1552
+ */
1553
+ function withToolDisplay(raw, redacted, enabled) {
1554
+ if (!raw || typeof raw !== 'object' || Array.isArray(raw) || !redacted || typeof redacted !== 'object' || Array.isArray(redacted))
1555
+ return redacted;
1556
+ const record = raw;
1557
+ const out = { ...redacted };
1558
+ if (record.args !== undefined)
1559
+ out.args = redactArgs(record.args, enabled);
1560
+ if (record.input !== undefined)
1561
+ out.input = redactArgs(record.input, enabled);
1562
+ // Structured results are redacted by key too (`{ result: { token } }`); text results keep the text redaction.
1563
+ for (const key of ['result', 'output'])
1564
+ if (record[key] && typeof record[key] === 'object')
1565
+ out[key] = redactArgs(record[key], enabled);
1566
+ const fnArgs = record.function && typeof record.function === 'object' ? record.function.arguments : undefined;
1567
+ if (enabled && fnArgs !== undefined && out.function && typeof out.function === 'object') {
1568
+ // A JSON-string `arguments` is redacted as parsed args; an object-valued one (`ToolCallSchema` accepts any JSON) directly.
1569
+ if (typeof fnArgs !== 'string')
1570
+ out.function = { ...out.function, arguments: redactArgs(fnArgs, enabled) };
1571
+ else
1572
+ try {
1573
+ out.function = { ...out.function, arguments: JSON.stringify(redactArgs(JSON.parse(fnArgs), enabled)) };
1574
+ }
1575
+ catch { /* unparseable: the text redaction stands */ }
1576
+ }
1577
+ // The target is derived in the live order: the sidecar's snapshot (first four arguments, capped) before redaction.
1578
+ return { ...out, ...toolDisplay(toolName(record), redactArgs(snapshotArgs(toolArgs(record)), enabled)) };
1579
+ }
1580
+ /** Every non-empty scalar of a credential value masked, keeping its shape (`{ password: ['x'] }` → `['***']`). */
1581
+ function maskLeaves(value) {
1582
+ if (Array.isArray(value))
1583
+ return value.map(maskLeaves);
1584
+ if (value && typeof value === 'object')
1585
+ return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, maskLeaves(item)]));
1586
+ return (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') && String(value) !== '' ? '***' : value;
1587
+ }
1588
+ /** An argv element that is a flag (`--password`), and one that takes `user:password` (`-u`, `--user`, `--proxy-user`). */
1589
+ const ARGV_FLAG_RE = /^-{1,2}([A-Za-z0-9_][A-Za-z0-9_.-]*)$/;
1590
+ const ARGV_USER_FLAG_RE = /^(?:-[uU]|--user|--proxy-u(?:s(?:e(?:r)?)?)?)$/;
1591
+ /** The fields that name a header in a `{ name: 'Authorization', value }` record. */
1592
+ const HEADER_LABEL_FIELDS = new Set(['name', 'key', 'header']);
1593
+ /** Tool arguments redacted like any value, plus every scalar under a credential-named key (`{ password: 'x' }`). */
1594
+ function redactArgs(value, enabled) {
1595
+ if (!enabled)
1596
+ return value;
1597
+ if (Array.isArray(value)) {
1598
+ // A `[name, value]` header tuple naming a credential.
1599
+ if (value.length === 2 && typeof value[0] === 'string' && isCredentialKey(value[0]))
1600
+ return [value[0], maskLeaves(value[1])];
1601
+ // An argv array (`['login', '--password', 'hunter2']`): a credential flag's value is the next element.
1602
+ return value.map((item, i) => {
1603
+ const flag = value[i - 1];
1604
+ if (typeof flag === 'string' && item !== '' && item !== null && item !== undefined) {
1605
+ if (ARGV_USER_FLAG_RE.test(flag))
1606
+ return typeof item === 'string' ? item.replace(/:.*/s, ':***') : maskLeaves(item);
1607
+ const key = ARGV_FLAG_RE.exec(flag)?.[1];
1608
+ // Any value: a numeric password or a nested list is a credential too.
1609
+ if (key !== undefined && isCredentialKey(key))
1610
+ return maskLeaves(item);
1611
+ }
1612
+ return redactArgs(item, enabled);
1613
+ });
1614
+ }
1615
+ if (value && typeof value === 'object') {
1616
+ const record = value;
1617
+ // A `{ name: 'Authorization', value: ... }` pair (HAR and similar header lists).
1618
+ // Any label field naming a credential labels the value (`{ name: 'metadata', header: 'Authorization', value }`).
1619
+ const labelled = [...HEADER_LABEL_FIELDS].some((field) => typeof record[field] === 'string' && isCredentialKey(record[field]));
1620
+ // A labelled credential header masks every payload field (`value`, `values`, ...); its label fields stay.
1621
+ return Object.fromEntries(Object.entries(record).map(([key, item]) => [key, isCredentialKey(key) || (labelled && !HEADER_LABEL_FIELDS.has(key)) ? maskLeaves(item) : redactArgs(item, enabled)]));
1622
+ }
1623
+ return redactValue(value, enabled);
1624
+ }
1625
+ const isToolUse = (part) => Boolean(part && typeof part === 'object' && part.type === 'tool_use');
1626
+ /** Every tool call a redacted message carries (OpenAI `tool_calls`, `tool_use` blocks, scene rows), stamped from its raw twin. */
1627
+ function withMessageToolDisplay(raw, item, enabled) {
1628
+ if (Array.isArray(raw.tool_calls) && Array.isArray(item.tool_calls)) {
1629
+ const calls = raw.tool_calls;
1630
+ item.tool_calls = item.tool_calls.map((call, i) => withToolDisplay(calls[i], call, enabled));
1631
+ }
1632
+ if (Array.isArray(raw.content) && Array.isArray(item.content)) {
1633
+ const parts = raw.content;
1634
+ item.content = item.content.map((part, i) => (isToolUse(parts[i]) ? withToolDisplay(parts[i], part, enabled) : part));
1635
+ }
1636
+ const rawScene = raw._anchor_activity_scene;
1637
+ const scene = item._anchor_activity_scene;
1638
+ if (rawScene && typeof rawScene === 'object' && scene && typeof scene === 'object' && Array.isArray(scene.activity_rows)) {
1639
+ item._anchor_activity_scene = { ...scene, activity_rows: withSceneToolDisplay(rawScene.activity_rows, scene.activity_rows, enabled) };
1640
+ }
289
1641
  return item;
290
1642
  }
1643
+ /** Redacted scene rows (the detail preview and the paged rows) stamped from their raw twins the same way. */
1644
+ export function withSceneToolDisplay(rawRows, rows, enabled) {
1645
+ const raws = Array.isArray(rawRows) ? rawRows : [];
1646
+ return rows.map((row, i) => {
1647
+ const rawTool = raws[i]?.tool;
1648
+ if (!row || typeof row !== 'object' || Array.isArray(row) || !rawTool)
1649
+ return row;
1650
+ return { ...row, tool: withToolDisplay(rawTool, row.tool, enabled) };
1651
+ });
1652
+ }
291
1653
  function redactMessages(messages, enabled, activeTurnToken) {
292
1654
  if (!Array.isArray(messages))
293
1655
  return redactValue(messages, enabled);
294
1656
  return messages.map((m) => publicMessageProjection(m, enabled, activeTurnToken));
295
1657
  }
296
1658
  function redactToolCalls(toolCalls, enabled) {
297
- return redactValue(scrubInternalReplayFields(toolCalls, { messageRecords: false }), enabled);
1659
+ const scrubbed = scrubInternalReplayFields(toolCalls, { messageRecords: false });
1660
+ const redacted = redactValue(scrubbed, enabled);
1661
+ return Array.isArray(scrubbed) && Array.isArray(redacted) ? redacted.map((call, i) => withToolDisplay(scrubbed[i], call, enabled)) : redacted;
1662
+ }
1663
+ /** A live `tool` / `tool_complete` frame as it leaves the server (SSE, journal, legacy replay): redacted like session detail, then stamped. */
1664
+ export function publicToolFrame(data, enabled) {
1665
+ const frame = withToolDisplay(data, redactValue(data, enabled), enabled);
1666
+ // A frame without a displayable argument (a bare completion, or `args: {}`) has no target of its own: omitting it keeps
1667
+ // the one its start frame set.
1668
+ if (!frame.target)
1669
+ delete frame.target;
1670
+ return frame;
1671
+ }
1672
+ /** The open call a tool completion settles: the one with its Agent id, else the newest id-less call of the same name; -1 when none. */
1673
+ export function completedToolIndex(calls, tid, name) {
1674
+ const exact = tid ? calls.findLastIndex((call) => !call.done && call.tid === tid) : -1;
1675
+ return exact >= 0 ? exact : calls.findLastIndex((call) => !call.done && !call.tid && call.name === name);
1676
+ }
1677
+ /** A tool frame carrying its public call `id` in place of the sidecar-internal `tid`. */
1678
+ export function withToolId(data, id) {
1679
+ const frame = { ...data, id };
1680
+ delete frame.tid;
1681
+ return frame;
298
1682
  }
299
1683
  function redactNestedMessageContainers(value, enabled) {
300
1684
  const scrubbed = scrubInternalReplayFields(value);