@zackbart/connecta 0.34.1 → 0.36.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 (441) hide show
  1. package/CHANGELOG.md +418 -0
  2. package/README.md +65 -105
  3. package/SECURITY.md +3 -2
  4. package/bin/connecta.mjs +41 -16
  5. package/bin/version.mjs +1 -1
  6. package/dist/access-tokens.d.ts +2 -1
  7. package/dist/access-tokens.js +66 -30
  8. package/dist/activity.d.ts +1 -1
  9. package/dist/activity.js +2 -0
  10. package/dist/agent-config.d.ts +23 -0
  11. package/dist/agent-config.js +59 -0
  12. package/dist/agent-description.js +16 -0
  13. package/dist/agent-identity.js +4 -4
  14. package/dist/agent-observation.js +39 -0
  15. package/dist/auth/clerk-sdk/client.d.ts +2 -0
  16. package/dist/auth/clerk-sdk/generated.js +554 -563
  17. package/dist/auth/clerk-transport.js +70 -3
  18. package/dist/auth/clerk.d.ts +13 -0
  19. package/dist/auth/clerk.js +653 -246
  20. package/dist/auth/cloudflare-access.js +46 -42
  21. package/dist/auth/downstream-client-metadata.js +2 -2
  22. package/dist/auth/downstream-oauth.js +2044 -1161
  23. package/dist/auth/downstream-revocation.js +88 -88
  24. package/dist/auth/native-call.js +20 -0
  25. package/dist/auth/native.js +184 -0
  26. package/dist/auth/oauth-start-reset.js +11 -5
  27. package/dist/auth/oauth-token-response.js +121 -102
  28. package/dist/auth/oauth-v2-migration.js +97 -78
  29. package/dist/auth/renewable-grant.js +10 -0
  30. package/dist/auth/static-oauth.js +751 -281
  31. package/dist/auth/vercel-agent.js +266 -0
  32. package/dist/auth-admission.js +204 -0
  33. package/dist/auth-elicitation.js +350 -241
  34. package/dist/authorization-handoff.js +164 -90
  35. package/dist/call-admission-context.js +3 -10
  36. package/dist/call-admission.js +2 -0
  37. package/dist/call-classification.js +9 -0
  38. package/dist/call-recovery.js +32 -13
  39. package/dist/canonical-json.js +8 -0
  40. package/dist/catalog-cache.js +369 -213
  41. package/dist/catalog-service.js +659 -530
  42. package/dist/catalog.js +5 -1
  43. package/dist/concurrency.js +0 -25
  44. package/dist/config-defaults.d.ts +1 -0
  45. package/dist/config-defaults.js +1 -0
  46. package/dist/config-schema.d.ts +3 -1
  47. package/dist/config-schema.js +13 -3
  48. package/dist/config.d.ts +40 -6
  49. package/dist/config.js +40 -26
  50. package/dist/connector-caller.d.ts +4 -0
  51. package/dist/connector-caller.js +17 -4
  52. package/dist/connectors/api-connector.d.ts +29 -7
  53. package/dist/connectors/api-connector.js +145 -57
  54. package/dist/connectors/guarded-fetch.d.ts +51 -17
  55. package/dist/connectors/guarded-fetch.js +215 -121
  56. package/dist/connectors/negotiation-cache.js +20 -10
  57. package/dist/connectors/remote-mcp.d.ts +10 -30
  58. package/dist/connectors/remote-mcp.js +955 -563
  59. package/dist/d1.js +10 -243
  60. package/dist/derived-context.js +5 -0
  61. package/dist/describe-config.d.ts +3 -1
  62. package/dist/describe-config.js +2 -0
  63. package/dist/downstream-input.js +347 -260
  64. package/dist/errors.d.ts +87 -6
  65. package/dist/errors.js +114 -4
  66. package/dist/execute.js +87 -50
  67. package/dist/executor-admission.js +3 -0
  68. package/dist/executors/quickjs.js +163 -112
  69. package/dist/guarded-transport.js +50 -0
  70. package/dist/guest-runtime.js +1 -1
  71. package/dist/http/client.js +439 -0
  72. package/dist/http/errors.js +558 -0
  73. package/dist/http/fields.js +49 -0
  74. package/dist/http/hatch-paging.js +318 -0
  75. package/dist/http/hatch.js +1279 -0
  76. package/dist/http/json-schema.js +267 -0
  77. package/dist/http/node-transport.js +241 -0
  78. package/dist/http/oauth.js +162 -0
  79. package/dist/http/paging.js +227 -0
  80. package/dist/http/provider.js +221 -0
  81. package/dist/http/safety.js +82 -0
  82. package/dist/http/services.js +79 -0
  83. package/dist/http/tools.js +379 -0
  84. package/dist/http/vendor-index.js +138 -0
  85. package/dist/http/vendor.js +448 -0
  86. package/dist/http/write-request.js +66 -0
  87. package/dist/http-policy.d.ts +11 -0
  88. package/dist/http-policy.js +45 -0
  89. package/dist/inbound-credential.js +1 -1
  90. package/dist/index.d.ts +2 -2
  91. package/dist/index.js +4 -0
  92. package/dist/invocation-auth.js +30 -19
  93. package/dist/invocation.js +218 -85
  94. package/dist/mcp-result.js +0 -4
  95. package/dist/membership.d.ts +4 -0
  96. package/dist/meta-output.js +1 -1
  97. package/dist/meta-tools.js +531 -406
  98. package/dist/native-recovery.js +52 -0
  99. package/dist/native-tools.js +382 -0
  100. package/dist/node.d.ts +4 -3
  101. package/dist/node.js +45 -8
  102. package/dist/oauth-handoff.js +177 -104
  103. package/dist/oauth-partition.js +6 -20
  104. package/dist/oauth-sealing.js +6 -7
  105. package/dist/operation-vocabulary.js +101 -0
  106. package/dist/operator-record.d.ts +2 -2
  107. package/dist/operator-record.js +17 -4
  108. package/dist/operator-ui/contract.d.ts +8 -3
  109. package/dist/operator-ui/generated.js +4 -4
  110. package/dist/operator-ui/model.d.ts +2 -3
  111. package/dist/program-writes.js +47 -26
  112. package/dist/provider-call-facts.d.ts +115 -0
  113. package/dist/provider-call-facts.js +182 -0
  114. package/dist/provider-call.d.ts +112 -0
  115. package/dist/provider-call.js +271 -0
  116. package/dist/provider.d.ts +65 -31
  117. package/dist/provider.js +116 -39
  118. package/dist/providers/_shared/google/hatch.js +91 -0
  119. package/dist/providers/_shared/google/kit.js +427 -0
  120. package/dist/providers/_shared/google/result-size.js +15 -2
  121. package/dist/providers/_shared/google/service-account.d.ts +2 -1
  122. package/dist/providers/_shared/google/service-account.js +8 -4
  123. package/dist/providers/_shared/google/workspace.d.ts +6 -210
  124. package/dist/providers/_shared/google/workspace.js +6 -425
  125. package/dist/providers/_shared/rest/compact-index.js +86 -0
  126. package/dist/providers/_shared/rest/kit.js +146 -0
  127. package/dist/providers/_shared/rest/operation-index.js +230 -14
  128. package/dist/providers/_shared/rest/response-media.js +11 -0
  129. package/dist/providers/_shared/rest/value-safety.js +353 -195
  130. package/dist/providers/_shared/sensitive-fields.js +27 -0
  131. package/dist/providers/breeze/api.js +584 -0
  132. package/dist/providers/breeze/index.d.ts +1 -3
  133. package/dist/providers/breeze/index.js +67 -1420
  134. package/dist/providers/breeze/openapi.generated.js +60 -0
  135. package/dist/providers/breeze/skill.generated.js +2 -6
  136. package/dist/providers/breeze/tools.js +696 -0
  137. package/dist/providers/ccb/api.js +496 -0
  138. package/dist/providers/ccb/index.d.ts +15 -37
  139. package/dist/providers/ccb/index.js +77 -1702
  140. package/dist/providers/ccb/lib/constants.d.ts +34 -0
  141. package/dist/providers/ccb/lib/constants.js +74 -0
  142. package/dist/providers/ccb/lib/guide.js +9 -0
  143. package/dist/providers/ccb/lib/verdicts.js +387 -0
  144. package/dist/providers/ccb/openapi.generated.js +397 -0
  145. package/dist/providers/ccb/safety.js +295 -0
  146. package/dist/providers/ccb/skill.generated.js +2 -6
  147. package/dist/providers/ccb/tools.js +853 -0
  148. package/dist/providers/cloudflare/api.js +1066 -0
  149. package/dist/providers/cloudflare/index.d.ts +5 -22
  150. package/dist/providers/cloudflare/index.js +79 -197
  151. package/dist/providers/cloudflare/lib/config.d.ts +16 -0
  152. package/dist/providers/cloudflare/lib/config.js +2 -0
  153. package/dist/providers/cloudflare/lib/discovery.js +29 -0
  154. package/dist/providers/cloudflare/{graphql.js → lib/graphql.js} +1 -1
  155. package/dist/providers/cloudflare/lib/hatch.js +103 -0
  156. package/dist/providers/cloudflare/lib/path-bounds.js +28 -0
  157. package/dist/providers/cloudflare/lib/response-media.js +75 -0
  158. package/dist/providers/cloudflare/openapi.generated.js +4 -3
  159. package/dist/providers/cloudflare/{value-safety.js → safety.js} +487 -0
  160. package/dist/providers/cloudflare/skill.generated.js +2 -10
  161. package/dist/providers/cloudflare/tools.js +360 -0
  162. package/dist/providers/docs/api.js +51 -0
  163. package/dist/providers/docs/index.d.ts +1 -12
  164. package/dist/providers/docs/index.js +53 -1165
  165. package/dist/providers/docs/lib/constants.d.ts +18 -0
  166. package/dist/providers/docs/lib/constants.js +18 -0
  167. package/dist/providers/docs/lib/discovery.js +26 -0
  168. package/dist/providers/docs/lib/requests.js +48 -0
  169. package/dist/providers/docs/lib/schemas.js +168 -0
  170. package/dist/providers/docs/openapi.generated.js +16 -0
  171. package/dist/providers/docs/safety.js +54 -0
  172. package/dist/providers/docs/skill.generated.js +2 -6
  173. package/dist/providers/docs/tools.js +801 -0
  174. package/dist/providers/drive/api.js +177 -0
  175. package/dist/providers/drive/index.d.ts +1 -12
  176. package/dist/providers/drive/index.js +38 -1448
  177. package/dist/providers/drive/lib/constants.d.ts +6 -0
  178. package/dist/providers/drive/lib/constants.js +6 -0
  179. package/dist/providers/drive/lib/contracts.js +532 -0
  180. package/dist/providers/drive/lib/hatch.js +197 -0
  181. package/dist/providers/drive/openapi.generated.js +77 -0
  182. package/dist/providers/drive/safety.js +90 -0
  183. package/dist/providers/drive/skill.generated.js +2 -6
  184. package/dist/providers/drive/tools.js +1290 -0
  185. package/dist/providers/forms/api.js +71 -0
  186. package/dist/providers/forms/index.d.ts +1 -12
  187. package/dist/providers/forms/index.js +49 -1145
  188. package/dist/providers/forms/lib/constants.d.ts +14 -0
  189. package/dist/providers/forms/lib/constants.js +19 -0
  190. package/dist/providers/forms/lib/hatch.js +72 -0
  191. package/dist/providers/forms/openapi.generated.js +23 -0
  192. package/dist/providers/forms/safety.js +34 -0
  193. package/dist/providers/forms/skill.generated.js +2 -8
  194. package/dist/providers/forms/tools.js +1023 -0
  195. package/dist/providers/github/api.js +150 -0
  196. package/dist/providers/github/index.d.ts +4 -5
  197. package/dist/providers/github/index.js +30 -87
  198. package/dist/providers/github/{auth.d.ts → lib/auth.d.ts} +5 -3
  199. package/dist/providers/github/{auth.js → lib/auth.js} +40 -43
  200. package/dist/providers/github/lib/discovery.js +132 -0
  201. package/dist/providers/github/lib/operations.js +58 -0
  202. package/dist/providers/github/{scope.d.ts → lib/scope.d.ts} +2 -0
  203. package/dist/providers/github/{scope.js → lib/scope.js} +25 -1
  204. package/dist/providers/github/openapi.generated.js +1216 -0
  205. package/dist/providers/github/safety.js +216 -0
  206. package/dist/providers/github/skill.generated.js +2 -5
  207. package/dist/providers/github/tools.js +506 -0
  208. package/dist/providers/gmail/api.js +114 -0
  209. package/dist/providers/gmail/index.d.ts +1 -12
  210. package/dist/providers/gmail/index.js +73 -1621
  211. package/dist/providers/gmail/lib/constants.d.ts +13 -0
  212. package/dist/providers/gmail/lib/constants.js +16 -0
  213. package/dist/providers/gmail/lib/index.js +173 -0
  214. package/dist/providers/gmail/lib/schemas.js +176 -0
  215. package/dist/providers/gmail/lib/threading.js +120 -0
  216. package/dist/providers/gmail/openapi.generated.js +92 -0
  217. package/dist/providers/gmail/safety.js +21 -0
  218. package/dist/providers/gmail/skill.generated.js +2 -6
  219. package/dist/providers/gmail/tools.js +1435 -0
  220. package/dist/providers/overflow/api.js +1008 -0
  221. package/dist/providers/overflow/index.d.ts +1 -17
  222. package/dist/providers/overflow/index.js +60 -1488
  223. package/dist/providers/overflow/skill.generated.js +2 -5
  224. package/dist/providers/overflow/tools.js +873 -0
  225. package/dist/providers/planning-center/api.js +467 -0
  226. package/dist/providers/planning-center/index.d.ts +4 -35
  227. package/dist/providers/planning-center/index.js +69 -2445
  228. package/dist/providers/planning-center/lib/constants.d.ts +58 -0
  229. package/dist/providers/planning-center/lib/constants.js +83 -0
  230. package/dist/providers/planning-center/lib/discovery.js +40 -0
  231. package/dist/providers/planning-center/lib/guide.js +39 -0
  232. package/dist/providers/planning-center/lib/json-api.js +83 -0
  233. package/dist/providers/planning-center/lib/verdicts.js +1758 -0
  234. package/dist/providers/planning-center/openapi.generated.js +1764 -0
  235. package/dist/providers/planning-center/safety.js +188 -0
  236. package/dist/providers/planning-center/skill.generated.js +2 -27
  237. package/dist/providers/planning-center/tools.js +1010 -0
  238. package/dist/providers/quickbooks/api.js +299 -0
  239. package/dist/providers/quickbooks/index.d.ts +13 -0
  240. package/dist/providers/quickbooks/index.js +93 -0
  241. package/dist/providers/quickbooks/lib/query.js +118 -0
  242. package/dist/providers/quickbooks/lib/writes.js +168 -0
  243. package/dist/providers/quickbooks/safety.js +222 -0
  244. package/dist/providers/quickbooks/skill.generated.js +5 -0
  245. package/dist/providers/quickbooks/tools.js +117 -0
  246. package/dist/providers/sheets/api.js +133 -0
  247. package/dist/providers/sheets/index.d.ts +4 -16
  248. package/dist/providers/sheets/index.js +61 -1172
  249. package/dist/providers/sheets/lib/constants.d.ts +14 -0
  250. package/dist/providers/sheets/lib/constants.js +14 -0
  251. package/dist/providers/sheets/lib/hatch.js +163 -0
  252. package/dist/providers/sheets/lib/requests.js +105 -0
  253. package/dist/providers/sheets/lib/schemas.js +138 -0
  254. package/dist/providers/sheets/openapi.generated.js +30 -0
  255. package/dist/providers/sheets/safety.js +27 -0
  256. package/dist/providers/sheets/skill.generated.js +2 -7
  257. package/dist/providers/sheets/tools.js +733 -0
  258. package/dist/providers/slides/api.js +147 -0
  259. package/dist/providers/slides/index.d.ts +2 -10
  260. package/dist/providers/slides/index.js +48 -2555
  261. package/dist/providers/slides/lib/constants.d.ts +11 -0
  262. package/dist/providers/slides/lib/constants.js +11 -0
  263. package/dist/providers/slides/lib/requests.js +60 -0
  264. package/dist/providers/slides/lib/schemas.js +565 -0
  265. package/dist/providers/slides/openapi.generated.js +18 -0
  266. package/dist/providers/slides/skill.generated.js +2 -6
  267. package/dist/providers/slides/tools.js +1968 -0
  268. package/dist/providers/stripe/api.js +401 -0
  269. package/dist/providers/stripe/index.d.ts +5 -27
  270. package/dist/providers/stripe/index.js +74 -210
  271. package/dist/providers/stripe/lib/config.d.ts +4 -0
  272. package/dist/providers/stripe/lib/config.js +2 -0
  273. package/dist/providers/stripe/openapi.generated.js +4 -3
  274. package/dist/providers/stripe/{value-safety.js → safety.js} +5 -5
  275. package/dist/providers/stripe/skill.generated.js +2 -11
  276. package/dist/providers/stripe/tools.js +77 -0
  277. package/dist/providers/tithely/api.js +531 -0
  278. package/dist/providers/tithely/index.d.ts +17 -0
  279. package/dist/providers/tithely/index.js +86 -965
  280. package/dist/providers/tithely/skill.generated.js +2 -5
  281. package/dist/providers/tithely/tools.js +426 -0
  282. package/dist/providers/vercel/api.js +551 -0
  283. package/dist/providers/vercel/index.d.ts +9 -25
  284. package/dist/providers/vercel/index.js +65 -260
  285. package/dist/providers/vercel/lib/config.d.ts +2 -0
  286. package/dist/providers/vercel/lib/config.js +2 -0
  287. package/dist/providers/vercel/lib/index.js +117 -0
  288. package/dist/providers/vercel/lib/logs.js +100 -0
  289. package/dist/providers/vercel/lib/pin.js +270 -0
  290. package/dist/providers/vercel/openapi.generated.js +10 -5
  291. package/dist/providers/vercel/{value-safety.js → safety.js} +247 -38
  292. package/dist/providers/vercel/skill.generated.js +2 -7
  293. package/dist/providers/vercel/tools.js +642 -0
  294. package/dist/redaction-identity.js +47 -0
  295. package/dist/redaction-marker.js +125 -0
  296. package/dist/registry.d.ts +21 -15
  297. package/dist/registry.js +409 -376
  298. package/dist/routes/access-tokens.js +1 -1
  299. package/dist/routes/actor-labels.js +1 -1
  300. package/dist/routes/connect.js +1 -1
  301. package/dist/routes/dispatch.js +103 -0
  302. package/dist/routes/mcp.js +170 -79
  303. package/dist/routes/oauth.js +6 -1
  304. package/dist/routes/operator.js +2 -2
  305. package/dist/routes/shared.d.ts +5 -4
  306. package/dist/routes/shared.js +244 -177
  307. package/dist/routes/skills.js +10 -8
  308. package/dist/routes/ui-config.js +27 -3
  309. package/dist/routes/ui-data.js +21 -20
  310. package/dist/routes/ui.js +1 -1
  311. package/dist/runtime/call-admission.js +0 -26
  312. package/dist/runtime/call-entry.js +30 -0
  313. package/dist/runtime/call.js +552 -0
  314. package/dist/runtime/connector-scope.js +7 -0
  315. package/dist/runtime/credential-probe.js +34 -0
  316. package/dist/runtime/executor.js +40 -0
  317. package/dist/runtime/native-storage.js +63 -0
  318. package/dist/runtime/registry.js +27 -0
  319. package/dist/runtime/request-scope.js +72 -0
  320. package/dist/runtime/run.js +15 -7
  321. package/dist/runtime/shared-read.js +34 -5
  322. package/dist/runtime/storage.js +34 -21
  323. package/dist/sent-secrets.d.ts +194 -37
  324. package/dist/sent-secrets.js +1743 -340
  325. package/dist/server.js +5 -56
  326. package/dist/skills.js +322 -223
  327. package/dist/sqlite.js +15 -14
  328. package/dist/storage/d1-copy.js +255 -0
  329. package/dist/storage/d1-driver.js +15 -0
  330. package/dist/storage/keys.js +28 -0
  331. package/dist/storage/sql-driver.js +88 -0
  332. package/dist/storage/sql-native.js +544 -0
  333. package/dist/storage/sql-schema.d.ts +6 -0
  334. package/dist/storage/sql-schema.js +15 -0
  335. package/dist/storage/sql.d.ts +14 -20
  336. package/dist/storage/sql.js +29 -535
  337. package/dist/timeout.js +0 -26
  338. package/dist/tool-safety.js +20 -10
  339. package/dist/types.d.ts +9 -109
  340. package/dist/ui.d.ts +1 -1
  341. package/dist/ui.js +0 -1
  342. package/dist/upward.js +3 -0
  343. package/dist/usage-guide.js +3 -1
  344. package/dist/validate.d.ts +4 -2
  345. package/dist/validate.js +86 -9
  346. package/dist/version.d.ts +1 -1
  347. package/dist/version.js +1 -1
  348. package/dist/worker.js +312 -292
  349. package/dist/workspace-subject.js +5 -1
  350. package/dist/write-outcome.js +454 -34
  351. package/documentation/README.md +28 -7
  352. package/documentation/architecture.md +2409 -255
  353. package/documentation/auth.md +867 -114
  354. package/documentation/code-mode.md +49 -16
  355. package/documentation/deploying.md +164 -20
  356. package/documentation/hosted-mcp-migration.md +460 -0
  357. package/documentation/integrating.md +129 -144
  358. package/documentation/meta-tools.md +409 -32
  359. package/documentation/operating.md +27 -3
  360. package/documentation/operator-tests.md +3 -0
  361. package/documentation/operator-ui.md +84 -48
  362. package/documentation/provider-auth-migration.md +16 -63
  363. package/documentation/provider-migration-0.29.md +10 -5
  364. package/documentation/provider-migration-0.35.md +975 -0
  365. package/documentation/providers.md +1347 -0
  366. package/examples/worker/AGENTS.md +23 -9
  367. package/examples/worker/README.md +254 -135
  368. package/examples/worker/alchemy.run.ts +116 -0
  369. package/examples/worker/gitignore +10 -0
  370. package/examples/worker/src/connecta.config.ts +23 -21
  371. package/examples/worker/wrangler.jsonc +14 -5
  372. package/guides/connecting-agents.md +157 -0
  373. package/guides/how-it-works.md +156 -0
  374. package/guides/safety-and-trust.md +153 -0
  375. package/package.json +30 -50
  376. package/templates/node/.env.example +3 -0
  377. package/templates/node/README.md +22 -2
  378. package/templates/node/docker-compose.yml +1 -0
  379. package/templates/node/package.json +3 -2
  380. package/templates/node/src/connecta.config.ts +8 -0
  381. package/dist/catalog-drift.d.ts +0 -138
  382. package/dist/catalog-drift.js +0 -578
  383. package/dist/operator-ui/agents.d.ts +0 -74
  384. package/dist/operator-ui/agents.js +0 -104
  385. package/dist/operator-ui/config-summary.js +0 -565
  386. package/dist/operator-ui/connection-map.js +0 -376
  387. package/dist/operator-ui/endpoints.js +0 -221
  388. package/dist/operator-ui/fix-prompts.d.ts +0 -18
  389. package/dist/operator-ui/fix-prompts.js +0 -87
  390. package/dist/operator-ui/health.js +0 -52
  391. package/dist/operator-ui/member-page.js +0 -97
  392. package/dist/operator-ui/overview.js +0 -123
  393. package/dist/operator-ui/setup-commands.js +0 -51
  394. package/dist/operator-ui/view.d.ts +0 -401
  395. package/dist/operator-ui/view.js +0 -705
  396. package/dist/providers/_shared/request-token.js +0 -83
  397. package/dist/providers/_shared/rest/dispatch.js +0 -41
  398. package/dist/providers/_shared/rest/operation-index.d.ts +0 -133
  399. package/dist/providers/_shared/rest/tools.d.ts +0 -141
  400. package/dist/providers/_shared/rest/tools.js +0 -569
  401. package/dist/providers/_shared/rest/value-safety.d.ts +0 -201
  402. package/dist/providers/basecamp/index.d.ts +0 -45
  403. package/dist/providers/basecamp/index.js +0 -997
  404. package/dist/providers/basecamp/skill.generated.js +0 -9
  405. package/dist/providers/cloudflare/rest.d.ts +0 -37
  406. package/dist/providers/cloudflare/rest.js +0 -1097
  407. package/dist/providers/github/hosted.js +0 -218
  408. package/dist/providers/github/rest.js +0 -321
  409. package/dist/providers/infisical/index.d.ts +0 -7
  410. package/dist/providers/infisical/index.js +0 -713
  411. package/dist/providers/infisical/skill.generated.js +0 -8
  412. package/dist/providers/intercom/index.d.ts +0 -24
  413. package/dist/providers/intercom/index.js +0 -121
  414. package/dist/providers/intercom/skill.generated.js +0 -8
  415. package/dist/providers/linear/index.d.ts +0 -55
  416. package/dist/providers/linear/index.js +0 -537
  417. package/dist/providers/linear/skill.generated.js +0 -8
  418. package/dist/providers/mixpanel/index.d.ts +0 -59
  419. package/dist/providers/mixpanel/index.js +0 -472
  420. package/dist/providers/mixpanel/operations.js +0 -20
  421. package/dist/providers/mixpanel/rest.js +0 -603
  422. package/dist/providers/mixpanel/skill.generated.js +0 -10
  423. package/dist/providers/mixpanel/value-safety.js +0 -16
  424. package/dist/providers/notion/index.d.ts +0 -67
  425. package/dist/providers/notion/index.js +0 -368
  426. package/dist/providers/notion/openapi.generated.js +0 -75
  427. package/dist/providers/notion/rest.d.ts +0 -24
  428. package/dist/providers/notion/rest.js +0 -1226
  429. package/dist/providers/notion/skill.generated.js +0 -11
  430. package/dist/providers/notion/value-safety.js +0 -56
  431. package/dist/providers/revenuecat/index.d.ts +0 -51
  432. package/dist/providers/revenuecat/index.js +0 -697
  433. package/dist/providers/revenuecat/skill.generated.js +0 -11
  434. package/dist/providers/stripe/rest.d.ts +0 -14
  435. package/dist/providers/stripe/rest.js +0 -402
  436. package/dist/providers/vercel/rest.d.ts +0 -20
  437. package/dist/providers/vercel/rest.js +0 -1020
  438. package/dist/providers/wispr-flow/index.d.ts +0 -14
  439. package/dist/providers/wispr-flow/index.js +0 -117
  440. package/dist/providers/wispr-flow/skill.generated.js +0 -8
  441. /package/dist/providers/github/{key.js → lib/key.js} +0 -0
@@ -1,24 +1,41 @@
1
+ import { authCall, authMethod } from "./auth/native.js";
2
+ import { Effect } from "effect";
1
3
  // Credentials and submitted private argument values used by one upstream request. Memory
2
4
  // only; never part of a context's public shape, a failure record, storage, or a log. Web APIs only.
3
- import { carryFailureFacts } from "./operator-record.js";
4
- import { ConnectorCallError } from "./errors.js";
5
+ import { classificationCode, ConnectorCallError } from "./errors.js";
5
6
  import { credentialUrlViews } from "./credential-url.js";
7
+ import { RequestScope, requestOwner } from "./runtime/request-scope.js";
8
+ import { viewSecrets, wrapViewCredentials } from "./runtime/call.js";
6
9
  import { visitPrivateCallArguments } from "./argument-redaction.js";
7
- const REDACTED = "[redacted]";
10
+ import { keptErrorName, redactedCopyOf } from "./redaction-identity.js";
11
+ import { REDACTED } from "./redaction-marker.js";
8
12
  const encoder = new TextEncoder();
9
- const contexts = new WeakMap();
10
- const requests = new WeakMap();
11
- const wrappedCredentials = new WeakSet();
12
13
  const sensitiveName = /key|token|secret|password|auth|signature|session/i;
13
14
  const explicitSecretName = /secret|password/i;
15
+ const identifierName = /(?:id|user(?:name)?|login|email)$|^public/i;
16
+ // Continuation cursors are named tokens but are public result state, returned changed on the
17
+ // next page; matching their fragments would corrupt the cursor the agent pages with.
18
+ const continuationName = /^(?:next_?)?page_?token$|^(?:next|continuation|sync)_?token$/i;
14
19
  const headerLine = /(^|[\r\n])([\t ]*(?:cookie|set-cookie|[a-z0-9-]*(?:key|token|secret|auth|signature|session)[a-z0-9-]*)\s*:\s*)[^\r\n]*/gi;
15
20
  // Short credentials can match protocol fields or legitimate configuration.
16
21
  const MIN_SECRET_LENGTH = 8;
17
- // Form kinds, weakest first. A short private value redacts exact structured leaves and
18
- // withholds prose; longer private values and credentials are replaced wherever they occur.
19
- const SHORT = 1;
20
- const PRIVATE = 2;
21
- const CREDENTIAL = 3;
22
+ // K: every contiguous run this long of a registered form is matched wherever it appears. It
23
+ // equals MIN_SECRET_LENGTH, so every registered credential and every private value of at least K
24
+ // units is fragment-indexed. Eight units keeps ordinary words, dates, counts and auth framing out
25
+ // of the index: a public run must repeat eight consecutive units of a secret's form to match.
26
+ const FRAGMENT_LENGTH = MIN_SECRET_LENGTH;
27
+ // A private value shorter than this is matched only as a whole field value: a leaf, key, number,
28
+ // boolean or decoded view equal to one of its forms. One to three units recur inside ordinary
29
+ // words, counts and ids, so a substring match would rewrite them everywhere.
30
+ const SUBSTRING_LENGTH = 4;
31
+ // Form kinds, weakest first, by the length of the value they came from. A tiny private value
32
+ // (1-3 units) is matched whole only. A short one (4-7) is also matched as a substring of payload
33
+ // text and decoded views. Longer private values and credentials are matched whole and by every
34
+ // K-unit run, wherever they occur.
35
+ const TINY = 1;
36
+ const SHORT = 2;
37
+ const PRIVATE = 3;
38
+ const CREDENTIAL = 4;
22
39
  // Submitted values register at most this many form code units and argument nodes. A form
23
40
  // that does not fit withholds every string long enough to contain it.
24
41
  const MAX_PRIVATE_FORM_CHARS = 524_288;
@@ -40,26 +57,103 @@ const POWERS = new Int32Array(FORM_PREFIX + 1);
40
57
  POWERS[0] = 1;
41
58
  for (let length = 1; length <= FORM_PREFIX; length++)
42
59
  POWERS[length] = Math.imul(POWERS[length - 1], HASH_BASE);
60
+ // Fragments are indexed by two independent polynomial hashes, at most MAX_FRAGMENTS distinct ones
61
+ // (8 bytes each, in a table at most half full); a larger set withholds every run of K units.
62
+ const SECOND_BASE = 0x5b_d1_e9_95;
63
+ const MAX_FRAGMENTS = 524_288;
64
+ // A unit's weight in a K-unit window, first and second position, under each base.
65
+ const [FIRST_TOP, FIRST_NEXT] = [POWERS[FRAGMENT_LENGTH - 1], POWERS[FRAGMENT_LENGTH - 2]];
66
+ let [SECOND_TOP, SECOND_NEXT] = [1, 1];
67
+ for (let length = 1; length < FRAGMENT_LENGTH; length++) {
68
+ SECOND_TOP = Math.imul(SECOND_TOP, SECOND_BASE);
69
+ if (length < FRAGMENT_LENGTH - 1)
70
+ SECOND_NEXT = Math.imul(SECOND_NEXT, SECOND_BASE);
71
+ }
43
72
  // One redaction call visits at most this many structured entries, this deep. A string is
44
73
  // decoded at most MAX_DECODE_LEVELS JSON layers, counting nested JSON strings it was parsed from.
45
74
  const MAX_WALK_NODES = 1_048_576;
46
75
  const MAX_WALK_DEPTH = 512;
47
76
  const MAX_DECODE_LEVELS = 8;
77
+ // A decoded layer is scanned only around the escapes it decoded, this far past the widest form
78
+ // on each side: a match holding no newly decoded unit was already found a layer up. Wider forms
79
+ // scan the whole layer.
80
+ const MAX_WINDOW_UNITS = 4_096;
81
+ // Leaves too large to match joined within the work budget are matched alone; a run crossing
82
+ // two of them is matched this many units deep on each side of their seam.
83
+ const SEAM_UNITS = 512;
84
+ // Base64 or base64url text no declared field names is decoded and checked too: a whole view of
85
+ // at least ENCODED_FIELD_UNITS, or a run inside one long enough to carry the smallest piece the
86
+ // scan matches, up to MAX_ENCODED_LEVELS encodings deep. A run is a maximal span of base64 and
87
+ // base64url units, decoded from its first. L units decode to floor(3L/4) bytes, so p bytes that
88
+ // start a bytes into the encoding (a = 0, 1 or 2, its alignment) need L >= ceil(4(a + p)/3),
89
+ // least at a = 0. Padding follows a run and is never counted, so a padded and an unpadded
90
+ // encoding decode alike. The smallest piece is a short private value, four units and so at least
91
+ // four bytes, when payload rules apply and one is registered: ceil(16/3) = 6 units. Otherwise it
92
+ // is K units, at least eight bytes: ceil(32/3) = 11. A whole view needs no more than the first.
93
+ const ENCODED_FIELD_UNITS = 6;
94
+ const SHORT_RUN_UNITS = 6;
95
+ const FRAGMENT_RUN_UNITS = 11;
96
+ const MAX_ENCODED_LEVELS = 3;
97
+ // Checked encoded content this long is remembered for the request, so a later pass over the
98
+ // same bytes under the same set is not decoded and scanned again.
99
+ const MEMO_UNITS = 4_096;
48
100
  // Work for one redaction call, across parsing and every scanned view. Credential-only sets
49
101
  // keep the multi-MiB document and skill-file reads they always allowed.
50
102
  const PRIVATE_SCAN_WORK = 64 * 1_048_576;
51
103
  const CREDENTIAL_SCAN_WORK = 1_024 * 1_048_576;
52
- // Connecta and MCP framing: these names, tag values, and typed facts are copied unchanged,
53
- // so a submitted value such as "text" or "true" cannot rewrite an envelope or its outcome.
104
+ // Connecta, MCP and JSON-RPC framing: these names and tag values are fixed words, copied
105
+ // unchanged, so a submitted value such as "text" or one holding the word "resource" cannot
106
+ // rename an envelope member or rewrite a content type. They carry no downstream-chosen content
107
+ // beyond the word itself. Any other key, and every other value, is checked like text.
54
108
  const FRAMING_KEYS = new Set(("content structuredContent isError _meta resultType dev.connecta/format type text data mimeType resource uri " +
55
109
  "blob annotations ok format valueFormat error code message stack name cause details retryable retryAfterMs " +
56
- "nextAction uncertainCall result logs failure inputRequests requestState method params mode requestedSchema url").split(" "));
110
+ "nextAction uncertainCall result logs failure inputRequests requestState method params mode requestedSchema url " +
111
+ "jsonrpc id tools resources resourceTemplates uriTemplate contents prompts messages role arguments nextCursor " +
112
+ "title description inputSchema outputSchema size writeOutcome").split(" "));
57
113
  const TAGS = new Set(("type:text type:image type:audio type:resource type:resource_link format:json format:text format:paged " +
58
114
  "valueFormat:json valueFormat:text dev.connecta/format:json dev.connecta/format:text resultType:complete " +
59
- "resultType:input_required mode:form mode:url method:elicitation/create jsonrpc:2.0").split(" "));
60
- const FACTS = new Set(["isError", "ok", "retryable", "retryAfterMs", "truncated", "hasMore"]);
61
- // Downstream and guest prose: a short private value anywhere in it withholds the whole string.
62
- const PROSE_KEYS = new Set(["message", "stack", "error", "logs", "text", "result"]);
115
+ "resultType:input_required mode:form mode:url method:elicitation/create jsonrpc:2.0 writeOutcome:unknown").split(" "));
116
+ // Outcome flags: a boolean under these names is kept, since a boolean carries one bit whatever
117
+ // produced it. A number or string under them is matched like any other.
118
+ const FACTS = new Set(["isError", "ok", "retryable", "truncated", "hasMore"]);
119
+ // An error's diagnostics. Its own accessors under these names, such as V8's stack, are read once
120
+ // into its copy, as is whatever its chain answers for them; any other own accessor is never
121
+ // invoked. Accessors its class chain defines are read once too.
122
+ const ERROR_ACCESSORS = new Set(["stack", "message", "cause", "code", "name"]);
123
+ // Parts the checker reads: text; a number's canonical text, withheld whole when it changes; a
124
+ // declared base64 field, also decoded whole and withheld as the encoded marker; and the bytes a
125
+ // typed array or ArrayBuffer holds, as byte-valued units, withheld whole when anything matches.
126
+ const TEXT = 0;
127
+ const NUMBER = 1;
128
+ const BINARY = 2;
129
+ const BYTES = 3;
130
+ /** An accessor met while looking up an inherited field: never invoked. */
131
+ const WITHHELD_ACCESSOR = Symbol("accessor");
132
+ /** The engine's own getters for what a typed array, DataView or ArrayBuffer holds, so no
133
+ * subclass answers for them, and the built-in element types by the name a typed array reports. */
134
+ const getter = (prototype, name) => Object.getOwnPropertyDescriptor(prototype, name).get;
135
+ const TYPED = Object.getPrototypeOf(Uint8Array.prototype);
136
+ const TYPED_NAME = getter(TYPED, Symbol.toStringTag);
137
+ const TYPED_BUFFER = getter(TYPED, "buffer");
138
+ const TYPED_OFFSET = getter(TYPED, "byteOffset");
139
+ const TYPED_LENGTH = getter(TYPED, "byteLength");
140
+ const VIEW_BUFFER = getter(DataView.prototype, "buffer");
141
+ const VIEW_OFFSET = getter(DataView.prototype, "byteOffset");
142
+ const VIEW_LENGTH = getter(DataView.prototype, "byteLength");
143
+ const BUFFER_LENGTH = getter(ArrayBuffer.prototype, "byteLength");
144
+ // Typed arrays whose elements are not their bytes; a Uint8Array's and a Uint8ClampedArray's are.
145
+ const ELEMENTS = new Map(("Int8Array Int16Array Uint16Array Int32Array Uint32Array Float16Array Float32Array Float64Array " +
146
+ "BigInt64Array BigUint64Array")
147
+ .split(" ")
148
+ .map((name) => [name, globalThis[name]])
149
+ .filter((entry) => typeof entry[1] === "function"));
150
+ const DECIMAL = Array.from({ length: 256 }, (_, byte) => String(byte));
151
+ /** Wrapper objects JSON reads as their primitive, read from the engine's slot. */
152
+ const BOXED = [
153
+ [String, String.prototype.valueOf],
154
+ [Number, Number.prototype.valueOf],
155
+ [Boolean, Boolean.prototype.valueOf],
156
+ ];
63
157
  /** Spend work that fits the call's remaining budget. Work that does not fit is refused
64
158
  * without spending, so its field is withheld while smaller fields can still be checked. */
65
159
  function spend(scan, work) {
@@ -68,15 +162,199 @@ function spend(scan, work) {
68
162
  scan.budget.work -= work;
69
163
  return true;
70
164
  }
71
- // Walk markers: a container was opened, or an entry is dropped from the copy.
72
- const OPEN = Symbol("open");
73
- const DROP = Symbol("drop");
74
- /** Define one entry on a copy. An own accessor here is Error.stack, already snapshotted. */
75
- function define(copy, item, key, name, value) {
76
- const descriptor = Object.getOwnPropertyDescriptor(item, key);
77
- Object.defineProperty(copy, name, "value" in descriptor
78
- ? { ...descriptor, value, ...(name !== key ? { configurable: true } : {}) }
79
- : { value, configurable: true, writable: true });
165
+ /** Base64 fields MCP and Connecta declare: resource blobs, image and audio data, content a
166
+ * provider marks `format: "base64"`, and a REST body's `base64` beside its `contentType`. They
167
+ * are checked as the bytes they carry. `field` reads the snapshot, never the original again. */
168
+ function binaryField(field, key) {
169
+ return ((key === "blob" && typeof field("uri") === "string") ||
170
+ (key === "data" && (field("type") === "image" || field("type") === "audio")) ||
171
+ (key === "content" && field("format") === "base64") ||
172
+ (key === "base64" && typeof field("contentType") === "string"));
173
+ }
174
+ function parse(text) {
175
+ try {
176
+ return { json: JSON.parse(text) };
177
+ }
178
+ catch {
179
+ return undefined; // Prose rather than serialized JSON.
180
+ }
181
+ }
182
+ /** A data property found on `item` or its prototypes, or undefined. An accessor on the way is
183
+ * never invoked: it reads as `accessor`. */
184
+ function inherited(item, name, accessor) {
185
+ for (let at = item; at; at = Object.getPrototypeOf(at)) {
186
+ const descriptor = Object.getOwnPropertyDescriptor(at, name);
187
+ if (descriptor)
188
+ return "value" in descriptor ? descriptor.value : accessor;
189
+ }
190
+ return undefined;
191
+ }
192
+ /** The bytes a typed array, DataView or ArrayBuffer holds, copied into a fresh built-in
193
+ * Uint8Array through the engine's slots, with the element type of a typed array whose elements
194
+ * are not those bytes. Undefined for any other value. */
195
+ function binaryOf(item) {
196
+ let held;
197
+ let elements;
198
+ if (ArrayBuffer.isView(item)) {
199
+ const name = TYPED_NAME.call(item);
200
+ const typed = name !== undefined;
201
+ held = new Uint8Array((typed ? TYPED_BUFFER : VIEW_BUFFER).call(item), (typed ? TYPED_OFFSET : VIEW_OFFSET).call(item), (typed ? TYPED_LENGTH : VIEW_LENGTH).call(item));
202
+ elements = ELEMENTS.get(name);
203
+ }
204
+ else {
205
+ if (!(item instanceof ArrayBuffer))
206
+ return undefined;
207
+ try {
208
+ BUFFER_LENGTH.call(item);
209
+ }
210
+ catch {
211
+ return undefined; // Its prototype, not an ArrayBuffer.
212
+ }
213
+ held = new Uint8Array(item);
214
+ }
215
+ const bytes = new Uint8Array(held.length);
216
+ bytes.set(held);
217
+ return { bytes, elements };
218
+ }
219
+ /** A String, Number or Boolean wrapper's primitive, as JSON reads it; undefined otherwise. */
220
+ function unboxed(item) {
221
+ for (const [type, valueOf] of BOXED) {
222
+ if (!(item instanceof type))
223
+ continue;
224
+ try {
225
+ return { value: valueOf.call(item) };
226
+ }
227
+ catch {
228
+ return undefined; // Its prototype, not a wrapper.
229
+ }
230
+ }
231
+ return undefined;
232
+ }
233
+ /** Bytes as a string of byte-valued code units, in bounded batches. */
234
+ function latin1(bytes) {
235
+ const chunks = [];
236
+ for (let offset = 0; offset < bytes.length; offset += 0x8000)
237
+ chunks.push(String.fromCharCode(...bytes.subarray(offset, offset + 0x8000)));
238
+ return chunks.join("");
239
+ }
240
+ /** The UTF-8 text a string of byte-valued code units decodes to, invalid sequences replaced. */
241
+ function utf8(binary) {
242
+ const bytes = new Uint8Array(binary.length);
243
+ for (let index = 0; index < binary.length; index++)
244
+ bytes[index] = binary.charCodeAt(index);
245
+ return new TextDecoder("utf-8", { fatal: false, ignoreBOM: true }).decode(bytes);
246
+ }
247
+ /** Base64 or base64url text decoded to byte-valued code units, or undefined when it is not. */
248
+ function decodeBase64(text) {
249
+ let standard = text.includes("-") || text.includes("_") ? text.replace(/-/g, "+").replace(/_/g, "/") : text;
250
+ // An unpadded encoding cut one unit past a whole quad still decodes up to that quad.
251
+ let bare = standard.length;
252
+ while (bare > 0 && standard.charCodeAt(bare - 1) === 0x3d)
253
+ bare--;
254
+ if (bare % 4 === 1)
255
+ standard = standard.slice(0, bare - 1);
256
+ try {
257
+ return atob(standard);
258
+ }
259
+ catch {
260
+ return undefined;
261
+ }
262
+ }
263
+ /** Base64 text without its trailing padding. */
264
+ function unpadded(text) {
265
+ let end = text.length;
266
+ while (end > 0 && text.charCodeAt(end - 1) === 0x3d)
267
+ end--;
268
+ return end === text.length ? text : text.slice(0, end);
269
+ }
270
+ const BASE64_UNIT = new Uint8Array(65_536);
271
+ for (const unit of "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/-_")
272
+ BASE64_UNIT[unit.charCodeAt(0)] = 1;
273
+ /** Base64 or base64url runs worth decoding, padding aside: the whole text when it is one, at
274
+ * least ENCODED_FIELD_UNITS long, or each run inside it at least `minimum` long. */
275
+ function encodedRuns(text, minimum) {
276
+ const runs = [];
277
+ for (let at = 0; at < text.length;) {
278
+ if (!BASE64_UNIT[text.charCodeAt(at)]) {
279
+ at++;
280
+ continue;
281
+ }
282
+ let end = at + 1;
283
+ while (end < text.length && BASE64_UNIT[text.charCodeAt(end)])
284
+ end++;
285
+ const run = end - at;
286
+ let padded = end;
287
+ while (padded < text.length && padded - end < 2 && text.charCodeAt(padded) === 0x3d)
288
+ padded++;
289
+ if (at === 0 && padded === text.length ? run >= ENCODED_FIELD_UNITS : run >= minimum)
290
+ runs.push(text.slice(at, end));
291
+ at = padded;
292
+ }
293
+ return runs;
294
+ }
295
+ /** Percent escapes decoded to the byte-valued units they name, or undefined when nothing
296
+ * decodes. With `form`, a `+` is a space too, as form encoding writes one, with or without a
297
+ * percent escape beside it. */
298
+ function percentDecoded(text, form) {
299
+ let found = false;
300
+ const decoded = text.replace(form ? /%([0-9A-Fa-f]{2})|\+/g : /%([0-9A-Fa-f]{2})/g, (_, digits) => {
301
+ found = true;
302
+ return digits === undefined ? " " : String.fromCharCode(parseInt(digits, 16));
303
+ });
304
+ return found ? decoded : undefined;
305
+ }
306
+ function unescapeLayer(text) {
307
+ const parts = [];
308
+ const layer = { text: "", at: [], from: [], width: [] };
309
+ let [cursor, length] = [0, 0];
310
+ for (let index = text.indexOf("\\"); index >= 0 && index < text.length - 1; index = text.indexOf("\\", index)) {
311
+ const code = text.charCodeAt(index + 1);
312
+ let unit = shortEscape[code] ?? -1;
313
+ let width = 2;
314
+ if (unit < 0 && code === 0x75 && index + 6 <= text.length) {
315
+ unit = 0;
316
+ for (let offset = 2; offset < 6 && unit >= 0; offset++) {
317
+ const digit = hex(text.charCodeAt(index + offset));
318
+ unit = digit < 0 ? -1 : unit * 16 + digit;
319
+ }
320
+ width = 6;
321
+ }
322
+ if (unit < 0) {
323
+ index++;
324
+ continue;
325
+ }
326
+ parts.push(text.slice(cursor, index), String.fromCharCode(unit));
327
+ length += index - cursor;
328
+ layer.at.push(length++);
329
+ layer.from.push(index);
330
+ layer.width.push(width);
331
+ cursor = index + width;
332
+ index = cursor;
333
+ }
334
+ if (!layer.at.length)
335
+ return undefined;
336
+ parts.push(text.slice(cursor));
337
+ layer.text = parts.join("");
338
+ return layer;
339
+ }
340
+ /** The unit a decoded layer's unit came from in the text above it: the first unit of its
341
+ * escape, or with `last` the escape's final unit. */
342
+ function above(layer, unit, last) {
343
+ const { at, from, width } = layer;
344
+ let [low, high] = [0, at.length];
345
+ while (low < high) {
346
+ const middle = (low + high) >>> 1;
347
+ if (at[middle] <= unit)
348
+ low = middle + 1;
349
+ else
350
+ high = middle;
351
+ }
352
+ const escape = low - 1;
353
+ if (escape < 0)
354
+ return unit;
355
+ if (at[escape] === unit)
356
+ return last ? from[escape] + width[escape] - 1 : from[escape];
357
+ return from[escape] + width[escape] + (unit - at[escape] - 1);
80
358
  }
81
359
  /** One payload-free warning per configured connector, regardless of requests. */
82
360
  export function shortSecretWarning() {
@@ -186,6 +464,158 @@ class Unescaped {
186
464
  return this.take(1, code);
187
465
  }
188
466
  }
467
+ /** A view that sets aside an incomplete JSON escape its text ends with, keeping source
468
+ * offsets, and records where it began. Wrapped around every decoded layer, so an escape that
469
+ * only a later layer reveals is set aside there too. Only the final six units can be one. */
470
+ class ClosedView {
471
+ from;
472
+ start = 0;
473
+ end = 0;
474
+ open = -1;
475
+ pending = [];
476
+ ended = false;
477
+ constructor(from) {
478
+ this.from = from;
479
+ }
480
+ next() {
481
+ while (!this.ended && this.pending.length < 7) {
482
+ const code = this.from.next();
483
+ if (code < 0) {
484
+ this.ended = true;
485
+ const tail = String.fromCharCode(...this.pending.map((unit) => unit.code));
486
+ const match = /\\(?:u[0-9a-fA-F]{0,3})?$/.exec(tail);
487
+ if (match) {
488
+ this.open = this.pending[match.index].start;
489
+ this.pending.length = match.index;
490
+ }
491
+ }
492
+ else
493
+ this.pending.push({ code, start: this.from.start, end: this.from.end });
494
+ }
495
+ const unit = this.pending.shift();
496
+ if (!unit)
497
+ return -1;
498
+ this.start = unit.start;
499
+ this.end = unit.end;
500
+ return unit.code;
501
+ }
502
+ }
503
+ function hexLetter(code) {
504
+ const lower = code | 0x20;
505
+ return lower >= 0x61 && lower <= 0x66;
506
+ }
507
+ /** Every K-unit run of every form of a value at least K long, as two independent 32-bit
508
+ * polynomial hashes in an open-addressed table: a match needs both to agree, so a false one is
509
+ * vanishingly rare and only ever redacts more. Index and scan are linear in the forms and the
510
+ * scanned text, whatever the number of values. A form's leading auth framing (`Bearer `,
511
+ * `token `) is public, so runs that start inside it are not indexed: a run holding K units of
512
+ * the value is indexed from the value's own forms. A run may start inside a percent escape
513
+ * whose `%` was cut away, or right after a `%` that is not the value's, so both cases of its
514
+ * first two hex letters are indexed. */
515
+ class Fragments {
516
+ complete = true;
517
+ size = 0;
518
+ firsts = new Int32Array(1_024);
519
+ seconds = new Int32Array(1_024);
520
+ shift = 22;
521
+ /** One bit per first hash's top 16 mixed bits: a clear bit rules a run out in one load, so
522
+ * only a run that might match computes its second hash and probes the table. */
523
+ filter = new Int32Array(2_048);
524
+ constructor(forms) {
525
+ for (const [form, frame] of forms) {
526
+ let first = 0;
527
+ let second = 0;
528
+ for (let index = 0; index < form.length; index++) {
529
+ const old = index >= FRAGMENT_LENGTH ? form.charCodeAt(index - FRAGMENT_LENGTH) : 0;
530
+ const code = form.charCodeAt(index);
531
+ first = (Math.imul(first - Math.imul(old, FIRST_TOP), HASH_BASE) + code) | 0;
532
+ second = (Math.imul(second - Math.imul(old, SECOND_TOP), SECOND_BASE) + code) | 0;
533
+ const at = index + 1 - FRAGMENT_LENGTH;
534
+ if (at < frame)
535
+ continue;
536
+ const [lead, next] = [form.charCodeAt(at), form.charCodeAt(at + 1)];
537
+ for (const a of hexLetter(lead) ? [0, (lead ^ 0x20) - lead] : [0])
538
+ for (const b of hexLetter(next) ? [0, (next ^ 0x20) - next] : [0])
539
+ this.insert((first + Math.imul(a, FIRST_TOP) + Math.imul(b, FIRST_NEXT)) | 0, (second + Math.imul(a, SECOND_TOP) + Math.imul(b, SECOND_NEXT)) | 0);
540
+ // Past capacity, every run of K units is withheld instead.
541
+ if (this.size > MAX_FRAGMENTS) {
542
+ [this.complete, this.size, this.firsts, this.seconds] = [false, 0, new Int32Array(1), new Int32Array(1)];
543
+ return;
544
+ }
545
+ }
546
+ }
547
+ }
548
+ /** Add one fragment; repeated runs, as in a self-similar credential, are stored once. */
549
+ insert(first, second) {
550
+ const mixed = Math.imul(first, 0x9e_37_79_b1);
551
+ this.filter[mixed >>> 21] |= 1 << ((mixed >>> 16) & 31);
552
+ const key = second || 1;
553
+ const mask = this.firsts.length - 1;
554
+ for (let slot = Math.imul(first, 0x9e_37_79_b1) >>> this.shift;; slot = (slot + 1) & mask) {
555
+ if (!this.seconds[slot]) {
556
+ this.firsts[slot] = first;
557
+ this.seconds[slot] = key;
558
+ // At most half full, so a probe ends quickly; past capacity the index is abandoned.
559
+ if (++this.size * 2 > this.firsts.length && this.size <= MAX_FRAGMENTS)
560
+ this.grow();
561
+ return;
562
+ }
563
+ if (this.firsts[slot] === first && this.seconds[slot] === key)
564
+ return;
565
+ }
566
+ }
567
+ grow() {
568
+ const [firsts, seconds] = [this.firsts, this.seconds];
569
+ [this.firsts, this.seconds, this.shift, this.size] = [
570
+ new Int32Array(firsts.length * 2),
571
+ new Int32Array(firsts.length * 2),
572
+ this.shift - 1,
573
+ 0,
574
+ ];
575
+ for (let slot = 0; slot < firsts.length; slot++)
576
+ if (seconds[slot])
577
+ this.insert(firsts[slot], seconds[slot]);
578
+ }
579
+ has(first, second) {
580
+ const key = second || 1;
581
+ const mask = this.firsts.length - 1;
582
+ for (let slot = Math.imul(first, 0x9e_37_79_b1) >>> this.shift; this.seconds[slot]; slot = (slot + 1) & mask)
583
+ if (this.firsts[slot] === first && this.seconds[slot] === key)
584
+ return true;
585
+ return false;
586
+ }
587
+ /** Scan text from `from` to `to`, percent hex folded as forms are, reporting each run's span. */
588
+ scan(text, from, to, spans, marks) {
589
+ if (!this.size)
590
+ return;
591
+ const codes = new Uint16Array(FRAGMENT_LENGTH);
592
+ const { filter } = this;
593
+ const base = spans.length;
594
+ let [first, percent, slot] = [0, 0, 0];
595
+ for (let at = from; at < to; at++) {
596
+ let code = text.charCodeAt(at);
597
+ if (code === 0x25)
598
+ percent = 2;
599
+ else if (percent) {
600
+ percent--;
601
+ if (code >= 0x61 && code <= 0x66)
602
+ code -= 0x20;
603
+ }
604
+ first = (Math.imul(first - Math.imul(codes[slot], FIRST_TOP), HASH_BASE) + code) | 0;
605
+ codes[slot] = code;
606
+ slot = (slot + 1) & (FRAGMENT_LENGTH - 1);
607
+ const mixed = Math.imul(first, 0x9e_37_79_b1);
608
+ if ((filter[mixed >>> 21] & (1 << ((mixed >>> 16) & 31))) === 0 || at + 1 - from < FRAGMENT_LENGTH)
609
+ continue;
610
+ // The window's units, oldest first, are the ring from the next slot round.
611
+ let second = 0;
612
+ for (let unit = 0; unit < FRAGMENT_LENGTH; unit++)
613
+ second = (Math.imul(second, SECOND_BASE) + codes[(slot + unit) & (FRAGMENT_LENGTH - 1)]) | 0;
614
+ if (this.has(first, second))
615
+ push(spans, base, at + 1 - FRAGMENT_LENGTH, at + 1, marks);
616
+ }
617
+ }
618
+ }
189
619
  /** Merge one view's spans as they arrive, ends ascending. A span wholly inside an emitted
190
620
  * placeholder is that placeholder; a private value containing its text spans more. */
191
621
  function push(spans, base, start, end, marks) {
@@ -217,23 +647,144 @@ function markers(text) {
217
647
  marks.push(at);
218
648
  return marks;
219
649
  }
650
+ /** Spans from every view and scanner, ordered and merged. */
651
+ function merged(spans) {
652
+ const order = Array.from({ length: spans.length / 2 }, (_, index) => index * 2).sort((a, b) => spans[a] - spans[b]);
653
+ const out = [];
654
+ for (const at of order) {
655
+ if (out.length && out[out.length - 1] >= spans[at])
656
+ out[out.length - 1] = Math.max(out[out.length - 1], spans[at + 1]);
657
+ else
658
+ out.push(spans[at], spans[at + 1]);
659
+ }
660
+ return out;
661
+ }
220
662
  /** Replace each merged span once, from the original text: no replacement can split another
221
663
  * view's match. Header-line scrubbing belongs to an actual echo. */
222
664
  function replace(text, spans) {
223
- const order = Array.from({ length: spans.length / 2 }, (_, index) => index * 2).sort((a, b) => spans[a] - spans[b]);
665
+ const ranges = merged(headerValues(text, spans));
224
666
  const parts = [];
225
667
  let cursor = 0;
226
- for (let index = 0; index < order.length;) {
227
- const start = spans[order[index]];
228
- let end = spans[order[index] + 1];
229
- while (++index < order.length && spans[order[index]] <= end)
230
- end = Math.max(end, spans[order[index] + 1]);
231
- parts.push(text.slice(cursor, start), REDACTED);
232
- cursor = end;
668
+ for (let index = 0; index < ranges.length; index += 2) {
669
+ parts.push(text.slice(cursor, ranges[index]), REDACTED);
670
+ cursor = ranges[index + 1];
233
671
  }
234
672
  parts.push(text.slice(cursor));
235
673
  return parts.join("").replace(headerLine, `$1$2${REDACTED}`);
236
674
  }
675
+ /** A unit of a header line's name, as `headerLine` reads one. */
676
+ function headerNameUnit(code) {
677
+ return ((code >= 0x61 && code <= 0x7a) || (code >= 0x41 && code <= 0x5a) || (code >= 0x30 && code <= 0x39) || code === 0x2d);
678
+ }
679
+ /** Whitespace as `headerLine`'s `\s` reads it around a header's `:`. */
680
+ function headerSpace(text, at) {
681
+ return /\s/.test(text[at]);
682
+ }
683
+ /**
684
+ * The echo's spans with the value of every sensitive header line (`headerLine`'s names) added,
685
+ * read on the text before the echo is replaced (#703). An echoed private value can take part of a
686
+ * header's name, its `:`, or a credential prefix such as `Bearer `, so the line cannot be read
687
+ * after replacement; read here, the whole value goes, every cookie pair and any short tail with
688
+ * it. A line that is not a sensitive header keeps its text. One forward pass; nothing is kept.
689
+ */
690
+ function headerValues(text, spans) {
691
+ const out = [...spans];
692
+ for (let line = 0; line < text.length;) {
693
+ let at = line;
694
+ while (at < text.length && (text[at] === " " || text[at] === "\t"))
695
+ at++;
696
+ const name = at;
697
+ while (at < text.length && headerNameUnit(text.charCodeAt(at)))
698
+ at++;
699
+ let next = at;
700
+ while (next < text.length && text[next] !== "\n" && text[next] !== "\r")
701
+ next++;
702
+ if (at > name) {
703
+ const word = text.slice(name, at).toLowerCase();
704
+ if (word === "cookie" || word === "set-cookie" || /key|token|secret|auth|signature|session/.test(word)) {
705
+ let colon = at;
706
+ while (colon < text.length && headerSpace(text, colon))
707
+ colon++;
708
+ if (text[colon] === ":") {
709
+ let value = colon + 1;
710
+ while (value < text.length && headerSpace(text, value))
711
+ value++;
712
+ let end = value;
713
+ while (end < text.length && text[end] !== "\n" && text[end] !== "\r")
714
+ end++;
715
+ if (end > value)
716
+ out.push(value, end);
717
+ next = Math.max(next, end);
718
+ }
719
+ }
720
+ }
721
+ line = next + 1;
722
+ }
723
+ return out;
724
+ }
725
+ /** Spans of the joined pieces, replaced in each piece they touch: a match across pieces leaves
726
+ * every other piece, and the structure around them, intact. */
727
+ function replaceParts(parts, spans) {
728
+ if (!spans.length)
729
+ return parts;
730
+ const ranges = merged(spans);
731
+ let offset = 0;
732
+ let at = 0;
733
+ return parts.map((part) => {
734
+ const end = offset + part.length;
735
+ while (at < ranges.length && ranges[at + 1] <= offset)
736
+ at += 2;
737
+ const local = [];
738
+ for (let index = at; part && index < ranges.length && ranges[index] < end; index += 2)
739
+ local.push(Math.max(0, ranges[index] - offset), Math.min(part.length, ranges[index + 1] - offset));
740
+ offset = end;
741
+ return local.length ? replace(part, local) : part;
742
+ });
743
+ }
744
+ /** Spans of serialized JSON's leaf tokens, in order: each string's contents and each number. */
745
+ function jsonTokens(text) {
746
+ const tokens = [];
747
+ for (let at = 0; at < text.length; at++) {
748
+ const code = text.charCodeAt(at);
749
+ if (code === 0x22) {
750
+ const start = at + 1;
751
+ for (at = start; at < text.length && text.charCodeAt(at) !== 0x22; at++)
752
+ if (text.charCodeAt(at) === 0x5c)
753
+ at++;
754
+ tokens.push(start, at);
755
+ }
756
+ else if (code === 0x2d || (code >= 0x30 && code <= 0x39)) {
757
+ const start = at;
758
+ while (at + 1 < text.length && /[-+.0-9eE]/.test(text[at + 1]))
759
+ at++;
760
+ tokens.push(start, at + 1);
761
+ }
762
+ }
763
+ return tokens;
764
+ }
765
+ /** Pieces this set cannot fully check: withhold each one at least `limit` long, and every run
766
+ * of adjacent shorter pieces that together reach it. */
767
+ function withholdRuns(parts, safe, limit) {
768
+ const out = [...safe];
769
+ let [start, length] = [0, 0];
770
+ const close = (end) => {
771
+ if (length >= limit)
772
+ for (let index = start; index < end; index++)
773
+ if (parts[index])
774
+ out[index] = REDACTED;
775
+ };
776
+ for (let index = 0; index < parts.length; index++) {
777
+ if (parts[index].length < limit) {
778
+ length += parts[index].length;
779
+ continue;
780
+ }
781
+ close(index);
782
+ out[index] = REDACTED;
783
+ [start, length] = [index + 1, 0];
784
+ }
785
+ close(parts.length);
786
+ return out;
787
+ }
237
788
  /** A run of one occurrence, whose step is set by the next occurrence that extends it. */
238
789
  const UNEXTENDED = { step: 0, stride: 0, stepHash: 0, stepPower: 0 };
239
790
  /** Runs ordered by their head check's due count: a binary min-heap. */
@@ -283,6 +834,8 @@ function hash(text) {
283
834
  class Matcher {
284
835
  /** The shortest form left unindexed; any string this long might contain it. */
285
836
  unindexed = Infinity;
837
+ /** The longest indexed form, whole: a match is never wider. */
838
+ widest = 0;
286
839
  root = new Int32Array(65_536);
287
840
  branches = new Map();
288
841
  child;
@@ -293,10 +846,10 @@ class Matcher {
293
846
  truncated = new Map();
294
847
  fail;
295
848
  longest;
296
- short;
297
849
  starts;
298
850
  hashes;
299
- constructor(forms) {
851
+ /** Forms of at least `spanKind` report spans; weaker ones only steer the open-tail search. */
852
+ constructor(forms, spanKind = PRIVATE) {
300
853
  // Credentials are indexed first; sorting is stable, so registration order is otherwise kept.
301
854
  const indexed = [];
302
855
  let units = 0;
@@ -307,12 +860,13 @@ class Matcher {
307
860
  else {
308
861
  units += length;
309
862
  indexed.push(entry);
863
+ this.widest = Math.max(this.widest, entry[0].length);
310
864
  }
311
865
  }
312
866
  const size = units + 1;
313
867
  [this.child, this.fail] = [new Int32Array(size), new Int32Array(size)];
314
868
  [this.unit, this.depth, this.longest] = [new Uint16Array(size), new Uint16Array(size), new Uint16Array(size)];
315
- [this.kind, this.short] = [new Uint8Array(size), new Uint8Array(size)];
869
+ this.kind = new Uint8Array(size);
316
870
  const parent = new Int32Array(size);
317
871
  let states = 1;
318
872
  let longestForm = 0;
@@ -367,8 +921,7 @@ class Matcher {
367
921
  const state = order[index];
368
922
  const fail = this.depth[state] > 1 ? this.step(this.fail[parent[state]], this.unit[state]) : 0;
369
923
  this.fail[state] = fail;
370
- this.longest[state] = this.kind[state] >= PRIVATE ? this.depth[state] : this.longest[fail];
371
- this.short[state] = this.kind[state] === SHORT ? 1 : this.short[fail];
924
+ this.longest[state] = this.kind[state] >= spanKind ? this.depth[state] : this.longest[fail];
372
925
  }
373
926
  this.starts = new Int32Array(1 << (32 - Math.clz32(longestForm)));
374
927
  this.hashes = new Int32Array(this.starts.length);
@@ -390,7 +943,7 @@ class Matcher {
390
943
  }
391
944
  /** Scan one view, mapping each match back to the source units it came from. False when its
392
945
  * long-form checks exceed the remaining work or MAX_PENDING_RUNS, so the view is withheld. */
393
- scan(units, found, budget, marks) {
946
+ scan(text, from, to, found, budget, marks) {
394
947
  let state = 0;
395
948
  let count = 0;
396
949
  let percent = 0;
@@ -399,7 +952,8 @@ class Matcher {
399
952
  let tails;
400
953
  const base = found.spans.length;
401
954
  const mask = this.starts.length - 1;
402
- for (let code = units.next(); code >= 0; code = units.next()) {
955
+ for (let at = from; at < to; at++) {
956
+ let code = text.charCodeAt(at);
403
957
  if (code === 0x25)
404
958
  percent = 2;
405
959
  else if (percent) {
@@ -407,15 +961,13 @@ class Matcher {
407
961
  if (code >= 0x61 && code <= 0x66)
408
962
  code -= 0x20;
409
963
  }
410
- this.starts[count & mask] = units.start;
964
+ this.starts[count & mask] = at;
411
965
  this.hashes[count++ & mask] = rolling;
412
966
  rolling = (Math.imul(rolling, HASH_BASE) + code) | 0;
413
967
  state = this.step(state, code);
414
968
  const length = this.longest[state];
415
969
  if (length)
416
- push(found.spans, base, this.starts[(count - length) & mask], units.end, marks);
417
- if (this.short[state])
418
- found.short = true;
970
+ push(found.spans, base, this.starts[(count - length) & mask], at + 1, marks);
419
971
  const groups = this.depth[state] === FORM_PREFIX ? this.truncated.get(state) : undefined;
420
972
  if (groups) {
421
973
  if (CHECK_WORK * groups.length > budget.work)
@@ -440,7 +992,7 @@ class Matcher {
440
992
  while (runs?.items.length && runs.items[0].due === count) {
441
993
  const run = runs.items[0];
442
994
  if (run.group.hashes.has((rolling - Math.imul(run.before, run.group.power)) | 0))
443
- push(found.spans, base, run.start, units.end, marks);
995
+ push(found.spans, base, run.start, at + 1, marks);
444
996
  run.count--;
445
997
  run.due += run.step;
446
998
  run.before = (Math.imul(run.before, run.stepPower) + run.stepHash) | 0;
@@ -448,10 +1000,44 @@ class Matcher {
448
1000
  runs.settle();
449
1001
  }
450
1002
  }
451
- if (this.kind[state] === SHORT && this.depth[state] === count)
452
- found.exact = true;
453
1003
  return true;
454
1004
  }
1005
+ /** The source start of the earliest suffix of a view that a form begins with but the view
1006
+ * ends inside: any nonempty prefix of an indexed form, or a long form whose
1007
+ * indexed prefix matched and whose length runs past the view. -1 when there is none. */
1008
+ openAt(units) {
1009
+ let state = 0;
1010
+ let count = 0;
1011
+ let percent = 0;
1012
+ const mask = this.starts.length - 1;
1013
+ // Long-form occurrences, as source start and the count by which the form would end.
1014
+ const longs = [];
1015
+ for (let code = units.next(); code >= 0; code = units.next()) {
1016
+ if (code === 0x25)
1017
+ percent = 2;
1018
+ else if (percent) {
1019
+ percent--;
1020
+ if (code >= 0x61 && code <= 0x66)
1021
+ code -= 0x20;
1022
+ }
1023
+ this.starts[count++ & mask] = units.start;
1024
+ state = this.step(state, code);
1025
+ const groups = this.depth[state] === FORM_PREFIX ? this.truncated.get(state) : undefined;
1026
+ if (groups && longs.length < 2 * MAX_PENDING_RUNS) {
1027
+ const length = Math.max(...groups.map((group) => group.length));
1028
+ longs.push(this.starts[(count - FORM_PREFIX) & mask], count - FORM_PREFIX + length);
1029
+ }
1030
+ }
1031
+ const depth = this.depth[state];
1032
+ let open = depth > 0 ? this.starts[(count - depth) & mask] : -1;
1033
+ for (let index = 0; index < longs.length; index += 2) {
1034
+ if (longs[index + 1] > count || longs.length >= 2 * MAX_PENDING_RUNS) {
1035
+ open = open < 0 ? longs[index] : Math.min(open, longs[index]);
1036
+ break;
1037
+ }
1038
+ }
1039
+ return open;
1040
+ }
455
1041
  /** Add an occurrence to a run when it continues the run's step and stride. The prefix has
456
1042
  * period `step`, so the text between occurrences is the prefix's first `step` units. */
457
1043
  extend(run, count, start) {
@@ -474,51 +1060,113 @@ class Matcher {
474
1060
  return true;
475
1061
  }
476
1062
  }
1063
+ /** Spans of a decoded layer's units, in the source text. */
1064
+ function toSource(layers, unit, last) {
1065
+ for (let index = layers.length - 1; index >= 0; index--)
1066
+ unit = above(layers[index], unit, last);
1067
+ return unit;
1068
+ }
1069
+ /** The ranges of a decoded layer to scan: `reach` units on each side of every escape it decoded,
1070
+ * merged, or the whole layer when that is no larger. */
1071
+ function windows(layer, reach) {
1072
+ const length = layer.text.length;
1073
+ if (reach > MAX_WINDOW_UNITS)
1074
+ return [0, length];
1075
+ const ranges = [];
1076
+ let units = 0;
1077
+ for (const unit of layer.at) {
1078
+ const [start, end] = [Math.max(0, unit - reach + 1), Math.min(length, unit + reach)];
1079
+ if (ranges.length && ranges[ranges.length - 1] >= start) {
1080
+ units += end - ranges[ranges.length - 1];
1081
+ ranges[ranges.length - 1] = end;
1082
+ }
1083
+ else {
1084
+ units += end - start;
1085
+ ranges.push(start, end);
1086
+ }
1087
+ if (units >= length)
1088
+ return [0, length];
1089
+ }
1090
+ return ranges;
1091
+ }
477
1092
  export class SentSecrets {
478
1093
  /** Normalized form to its strongest kind. */
479
1094
  forms = new Map();
1095
+ /** A form's leading units that encode only public auth framing; absent is none. */
1096
+ frames = new Map();
480
1097
  recipients = new Set();
1098
+ mintedStates = new Set();
1099
+ /** Encoded content this set already found clean, and whether under payload rules, which
1100
+ * cover envelope ones. Cleared whenever the set changes. */
1101
+ memo = new Map();
481
1102
  privateChars = 0;
1103
+ /** The longest form; whether any comes from a value shorter than K; whether any is not ASCII. */
1104
+ widestForm = 0;
1105
+ smallForms = false;
1106
+ unicodeForms = false;
482
1107
  /** The shortest form left unregistered; any string this long might contain it. */
483
1108
  unmatched = Infinity;
484
- unicode = false;
485
1109
  views = {};
486
- store(form, kind) {
1110
+ store(form, kind, frame = 0) {
487
1111
  const prior = this.forms.get(form) ?? 0;
488
- if (prior >= kind)
1112
+ const priorFrame = prior ? (this.frames.get(form) ?? 0) : Infinity;
1113
+ const nextFrame = Math.min(frame, priorFrame);
1114
+ if (prior >= kind && nextFrame === priorFrame)
489
1115
  return;
490
1116
  if (kind !== CREDENTIAL && !prior) {
491
1117
  if (this.privateChars + form.length > MAX_PRIVATE_FORM_CHARS)
492
1118
  return this.unregistered(form.length);
493
1119
  this.privateChars += form.length;
494
1120
  }
495
- this.forms.set(form, kind);
496
- this.unicode ||= /[\u0080-\uffff]/.test(form);
1121
+ this.forms.set(form, Math.max(prior, kind));
1122
+ if (nextFrame)
1123
+ this.frames.set(form, nextFrame);
1124
+ else
1125
+ this.frames.delete(form);
1126
+ this.widestForm = Math.max(this.widestForm, form.length);
1127
+ this.smallForms ||= kind < PRIVATE;
1128
+ this.unicodeForms ||= /[\u0080-\uffff]/.test(form);
497
1129
  this.views = {};
1130
+ this.memo = new Map();
498
1131
  for (const recipient of this.recipients)
499
- recipient.store(form, kind);
1132
+ recipient.store(form, kind, frame);
500
1133
  }
501
1134
  unregistered(length) {
502
1135
  if (length >= this.unmatched)
503
1136
  return;
504
1137
  this.unmatched = length;
505
1138
  this.views = {};
1139
+ this.memo = new Map();
506
1140
  for (const recipient of this.recipients)
507
1141
  recipient.unregistered(length);
508
1142
  }
509
- register(value, kind) {
510
- const forms = [value, new URLSearchParams({ value }).toString().slice(6)];
511
- try {
512
- forms.push(encodeURIComponent(value), encodeURI(value));
513
- }
514
- catch {
515
- // A lone surrogate has no URI form; its other forms remain.
516
- }
1143
+ /** Every form of `value`. Its first `frame` units are public auth framing, which no form's
1144
+ * fragment index starts in: a base64 form's framing ends at its last unit encoding only it. */
1145
+ register(value, kind, frame = 0) {
1146
+ const encodings = (text) => {
1147
+ const forms = [text, new URLSearchParams({ value: text }).toString().slice(6)];
1148
+ try {
1149
+ forms.push(encodeURIComponent(text), encodeURI(text));
1150
+ }
1151
+ catch {
1152
+ // A lone surrogate has no URI form; its other forms remain.
1153
+ }
1154
+ return forms;
1155
+ };
1156
+ const forms = encodings(value);
1157
+ const partial = frame > 0 && frame < Infinity;
1158
+ const framing = partial ? encodings(value.slice(0, frame)).map((form) => form.length) : [];
517
1159
  const encoded = base64(value);
518
- forms.push(encoded, encoded.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, ""));
519
- for (const form of forms)
1160
+ const bytes = partial ? Math.floor((encoder.encode(value.slice(0, frame)).length * 4) / 3) : 0;
1161
+ for (const form of [encoded, encoded.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "")]) {
1162
+ forms.push(form);
1163
+ framing.push(bytes);
1164
+ }
1165
+ forms.forEach((form, index) => {
1166
+ const framed = frame === Infinity ? frame : frame ? (framing[index] ?? 0) : 0;
520
1167
  for (const variant of wireForms(form))
521
- this.store(variant, kind);
1168
+ this.store(variant, kind, framed);
1169
+ });
522
1170
  }
523
1171
  /** The request receives existing and future secrets from every context. */
524
1172
  include(source) {
@@ -526,19 +1174,27 @@ export class SentSecrets {
526
1174
  return;
527
1175
  source.recipients.add(this);
528
1176
  for (const [form, kind] of source.forms)
529
- this.store(form, kind);
1177
+ this.store(form, kind, source.frames.get(form) ?? 0);
530
1178
  this.unregistered(source.unmatched);
531
1179
  }
532
- add(value) {
1180
+ /** A credential, as sent and in the auth framings a vendor may echo it with. Its first
1181
+ * `frame` units, such as a header's `Basic `, are framing too. */
1182
+ add(value, frame = 0) {
533
1183
  if (value.length < MIN_SECRET_LENGTH)
534
1184
  return;
535
- for (const form of [value, `Bearer ${value}`, `token ${value}`])
536
- this.register(form, CREDENTIAL);
1185
+ this.register(value, CREDENTIAL, frame);
1186
+ for (const framing of ["Bearer ", "token "])
1187
+ this.register(framing + value, CREDENTIAL, framing.length + frame);
537
1188
  }
538
1189
  /** Explicit secrets use the same floor as every other credential. */
539
1190
  secret(value) {
540
1191
  this.add(value);
541
1192
  }
1193
+ /** An identifier sent beside a credential, such as a confidential client's ID or a Basic
1194
+ * username: matched whole only, since consent URLs and metadata carry its parts by design. */
1195
+ identifier(value) {
1196
+ this.add(value, Infinity);
1197
+ }
542
1198
  /** Submitted writeOnly strings and numbers join this request's set, including property
543
1199
  * names inside a private object. Empty strings, booleans, and null carry no text. */
544
1200
  arguments(args, schema) {
@@ -550,7 +1206,7 @@ export class SentSecrets {
550
1206
  return;
551
1207
  if (value.length > MAX_PRIVATE_FORM_CHARS)
552
1208
  return this.unregistered(value.length);
553
- this.register(value, value.length < MIN_SECRET_LENGTH ? SHORT : PRIVATE);
1209
+ this.register(value, value.length < SUBSTRING_LENGTH ? TINY : value.length < MIN_SECRET_LENGTH ? SHORT : PRIVATE);
554
1210
  };
555
1211
  const collect = (value, depth) => {
556
1212
  if (++nodes > MAX_ARGUMENT_NODES || depth > MAX_ARGUMENT_DEPTH)
@@ -570,37 +1226,42 @@ export class SentSecrets {
570
1226
  };
571
1227
  visitPrivateCallArguments(args, schema, (value) => collect(value, 0));
572
1228
  }
573
- /** Structural fields must be refused, never repaired into different URLs. */
1229
+ /** Structural fields must be refused, never repaired into different URLs. A field the checker
1230
+ * would change under the boundary's rules is refused: one holding a whole form or any K-unit
1231
+ * run of one, in any view of the lattice. With `ignoreCase`, as for a host URL parsing
1232
+ * lowercased, the lowercased field is also checked against lowercased forms. */
574
1233
  contains(value, ignoreCase = false) {
575
- if (value.length >= this.unmatched)
1234
+ if (value.length >= Math.min(this.unmatched, this.unmatched < Infinity ? FRAGMENT_LENGTH : Infinity))
576
1235
  return true;
577
1236
  if (!this.forms.size)
578
1237
  return false;
579
- const found = { spans: [], short: false, exact: false };
580
- const lower = () => new Matcher([...this.forms].flatMap(([form, kind]) => wireForms(form.toLowerCase()).map((low) => [low, kind])));
581
- const matcher = ignoreCase ? (this.views.lower ??= lower()) : this.matcher();
582
- if (value.length >= matcher.unindexed)
1238
+ if (this.checked([value], [TEXT], this.scan(false))[0] !== value)
583
1239
  return true;
584
- const scanned = matcher.scan(new Source(ignoreCase ? value.toLowerCase() : value), found, { work: Infinity });
585
- return !scanned || found.spans.length > 0;
1240
+ return ignoreCase && this.folded().contains(value.toLowerCase());
586
1241
  }
587
1242
  containsUrl(value) {
588
1243
  const { hosts, components, joined } = credentialUrlViews(value);
589
1244
  return (hosts.some((host) => this.contains(host, true)) || [...components, ...joined].some((view) => this.contains(view)));
590
1245
  }
591
1246
  header(value) {
592
- this.add(value);
593
1247
  const framed = /^(?:Bearer|token|Basic)\s+(.+)$/i.exec(value);
1248
+ this.add(value, framed ? value.length - framed[1].length : 0);
594
1249
  if (!framed)
595
1250
  return;
596
1251
  this.add(framed[1]);
597
1252
  if (/^Basic\s/i.test(value)) {
598
1253
  try {
599
1254
  const decoded = atob(framed[1].replace(/-/g, "+").replace(/_/g, "/"));
600
- this.add(decoded);
601
1255
  const colon = decoded.indexOf(":");
1256
+ // Beside a password long enough to register, the username is an identifier, such as
1257
+ // a client ID; beside none or a placeholder, it is the credential itself.
1258
+ const paired = colon !== -1 && decoded.length - colon - 1 >= MIN_SECRET_LENGTH;
1259
+ this.add(decoded, paired ? colon + 1 : 0);
602
1260
  if (colon !== -1) {
603
- this.add(decoded.slice(0, colon));
1261
+ if (paired)
1262
+ this.identifier(decoded.slice(0, colon));
1263
+ else
1264
+ this.add(decoded.slice(0, colon));
604
1265
  this.secret(decoded.slice(colon + 1));
605
1266
  }
606
1267
  }
@@ -630,7 +1291,7 @@ export class SentSecrets {
630
1291
  for (const [name, value] of url.searchParams) {
631
1292
  if (explicitSecretName.test(name))
632
1293
  this.secret(value);
633
- else if (sensitiveName.test(name))
1294
+ else if (sensitiveName.test(name) && !continuationName.test(name))
634
1295
  this.add(value);
635
1296
  }
636
1297
  // The OAuth SDK sends token requests as form data. Do not read a body
@@ -647,15 +1308,80 @@ export class SentSecrets {
647
1308
  }
648
1309
  /** Serialized wire text about to be returned, stored, or paged, under payload rules. */
649
1310
  text(value) {
650
- return this.idle() ? value : this.wire(value, this.scan(true), 0, true, 0);
1311
+ return this.idle() ? value : this.project(value, this.scan(true)).value;
651
1312
  }
652
- /** Copy only what changes, including non-enumerable Error fields; never retain a raw cause.
653
- * Payload rules apply where downstream or guest data enters; envelopes repeat literal
654
- * replacement. */
1313
+ /** A plain-data snapshot of `value` with every match removed: own enumerable data properties
1314
+ * (an error's own fields, non-enumerable ones included), read once each. Accessors are never
1315
+ * invoked, except an error's stack and the accessors its class defines, read once into the
1316
+ * copy. The original object is never returned, so nothing read later can differ from what was
1317
+ * checked. Payload rules apply where downstream or guest data enters; the envelope pass at the
1318
+ * agent boundary repeats every check except short private values, which would rewrite
1319
+ * Connecta's own identifiers, counts and addresses. `values` applies payload rules to every
1320
+ * value of a parsed vendor body but keeps its object keys, which result safety and projections
1321
+ * match by name: a renamed key would hide a field from the rule that withholds it. JSON inside
1322
+ * a string is a value, so its keys are still checked. The connector's result and failures are
1323
+ * checked again under payload rules, keys included, at the `api()` boundary and the invocation. */
655
1324
  redact(value, mode = "payload") {
656
1325
  if (this.idle())
657
1326
  return value;
658
- return this.walk(value, this.scan(mode !== "envelope"), mode === "prose" || typeof value === "string", 0, 0);
1327
+ return this.project(value, this.scan(mode !== "envelope"), mode === "values").value;
1328
+ }
1329
+ /** Whether payload redaction would change anything in `value`, for fields that must be
1330
+ * refused rather than rewritten. */
1331
+ changes(value) {
1332
+ return !this.idle() && this.project(value, this.scan(true)).changed;
1333
+ }
1334
+ /** Relay state and links Connecta minted and authenticates are framing, not payload: they
1335
+ * hold sealed downstream state or Connecta's own facts, never downstream text or arguments,
1336
+ * and a coincidental match must not corrupt them. Never admit downstream-authored state
1337
+ * here. Exact value membership survives JSON serialization within this request. */
1338
+ mintedState(value) {
1339
+ this.mintedStates.add(value);
1340
+ return value;
1341
+ }
1342
+ /** Base64, or base64url with `url`, of bytes Connecta returns to an agent. No later pass
1343
+ * reads through an encoding the output does not declare, so the bytes are checked first: as
1344
+ * byte-valued units, as UTF-8 text, and through any base64 they carry, then the encoding
1345
+ * itself. Content holding a match encodes the placeholder instead. Downstream content takes
1346
+ * payload rules. `claims` is JSON Connecta assembled, such as a page cursor: its own names,
1347
+ * counts and fingerprints beside downstream values it already redacted under payload rules.
1348
+ * Every string in it is checked under payload rules, since each downstream value a cursor
1349
+ * holds is one, and the whole text and its encoding under the boundary's, so Connecta's keys
1350
+ * and counts never meet a short private value. Checked so, it is remembered as clean for later
1351
+ * passes over the same set. */
1352
+ encode(bytes, { url = false, claims = false } = {}) {
1353
+ const binary = latin1(bytes);
1354
+ const finish = (text) => {
1355
+ const encoded = btoa(text);
1356
+ return url ? encoded.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "") : encoded;
1357
+ };
1358
+ if (this.idle())
1359
+ return finish(binary);
1360
+ const encoded = finish(binary);
1361
+ const clean = claims
1362
+ ? this.claimsClean(binary, encoded)
1363
+ : this.checked([encoded], [BINARY], this.scan(true))[0] === encoded;
1364
+ if (!clean)
1365
+ return finish(REDACTED);
1366
+ if (claims || encoded.length >= MEMO_UNITS)
1367
+ this.memo.set(encoded, true);
1368
+ return encoded;
1369
+ }
1370
+ /** Text pieces a provider reads as one lane, such as rich-text runs with metadata between
1371
+ * them: checked joined, each match replaced in every piece it touches. */
1372
+ parts(parts) {
1373
+ if (this.idle())
1374
+ return parts;
1375
+ return this.checked(parts, parts.map(() => TEXT), this.scan(true));
1376
+ }
1377
+ /** Complete and unfinished forms in bounded text, all found against its original bytes.
1378
+ * Replacing a complete match first could hide a longer open match containing it. Even a
1379
+ * one-unit shared prefix is withheld: a read boundary gives no evidence it is public text.
1380
+ * Each decoded layer sets aside a trailing escape the read cut short, which is withheld. */
1381
+ redactOpenTail(text) {
1382
+ if (this.idle())
1383
+ return text;
1384
+ return this.checked([text], [TEXT], this.scan(true), true)[0];
659
1385
  }
660
1386
  /** Credentials alone: later calls keep the private arguments they submit, and cached
661
1387
  * catalogs never depend on one request's arguments. */
@@ -672,6 +1398,25 @@ export class SentSecrets {
672
1398
  matcher() {
673
1399
  return (this.views.matcher ??= new Matcher(this.forms));
674
1400
  }
1401
+ /** Short private forms, matched as substrings of payload text. */
1402
+ shortMatcher() {
1403
+ if (this.views.short === undefined) {
1404
+ const forms = [...this.forms].filter(([, kind]) => kind === SHORT);
1405
+ this.views.short = forms.length ? new Matcher(forms, SHORT) : null;
1406
+ }
1407
+ return this.views.short;
1408
+ }
1409
+ /** Forms with no indexed fragment, such as identifiers, matched whole. */
1410
+ wholeMatcher() {
1411
+ if (this.views.whole === undefined) {
1412
+ const forms = [...this.forms].filter(([form, kind]) => kind >= PRIVATE && (this.frames.get(form) ?? 0) > form.length - FRAGMENT_LENGTH);
1413
+ this.views.whole = forms.length ? new Matcher(forms) : null;
1414
+ }
1415
+ return this.views.whole;
1416
+ }
1417
+ fragments() {
1418
+ return (this.views.fragments ??= new Fragments([...this.forms].filter(([, kind]) => kind >= PRIVATE).map(([form]) => [form, this.frames.get(form) ?? 0])));
1419
+ }
675
1420
  credentials() {
676
1421
  if (!this.privateChars && this.unmatched === Infinity)
677
1422
  return this;
@@ -679,266 +1424,912 @@ export class SentSecrets {
679
1424
  const view = new SentSecrets();
680
1425
  for (const [form, kind] of this.forms)
681
1426
  if (kind === CREDENTIAL)
682
- view.store(form, kind);
1427
+ view.store(form, kind, this.frames.get(form) ?? 0);
683
1428
  this.views.credentials = view;
684
1429
  }
685
1430
  return this.views.credentials;
686
1431
  }
687
- /** Strings at least this long might hold a form that was never matched. */
688
- floor(scan) {
689
- return Math.min(scan.payload ? this.unmatched : Infinity, this.matcher().unindexed);
1432
+ /** Every form lowercased, for fields such as hosts that reach output lowercased. */
1433
+ folded() {
1434
+ if (!this.views.folded) {
1435
+ const view = new SentSecrets();
1436
+ for (const [form, kind] of this.forms)
1437
+ for (const low of wireForms(form.toLowerCase()))
1438
+ view.store(low, kind, this.frames.get(form) ?? 0);
1439
+ this.views.folded = view;
1440
+ }
1441
+ return this.views.folded;
1442
+ }
1443
+ /** The shortest text that might hold something this set cannot check, which is withheld: a
1444
+ * K-unit run of a value too large to register (or a whole short one), or of a form past the
1445
+ * fragment index's or a matcher's capacity. Envelope scans rely on the payload scans for
1446
+ * unregistered values. */
1447
+ limit(scan) {
1448
+ let limit = this.fragments().complete ? Infinity : FRAGMENT_LENGTH;
1449
+ limit = Math.min(limit, this.wholeMatcher()?.unindexed ?? Infinity);
1450
+ if (scan.payload && this.unmatched < Infinity)
1451
+ limit = Math.min(limit, this.unmatched, FRAGMENT_LENGTH);
1452
+ if (scan.payload)
1453
+ limit = Math.min(limit, this.shortMatcher()?.unindexed ?? Infinity);
1454
+ return limit;
690
1455
  }
691
- /** Every raw and JSON-unescaped match, mapped to source spans before any replacement. Text
692
- * decodes one more layer while the last one could still hold an escape, up to
693
- * MAX_DECODE_LEVELS counting the JSON strings it came from. Work beyond the call's budget
694
- * or that depth returns undefined, which withholds the string. */
695
- find(text, scan, escapes, level) {
696
- const found = { spans: [], short: false, exact: false };
1456
+ /** Whether an open tail could begin a form this set never registered or indexed. */
1457
+ uncheckedTail() {
1458
+ return this.unmatched < Infinity || this.matcher().unindexed < Infinity;
1459
+ }
1460
+ /** A form whole: a leaf, key, number, boolean or decoded view equal to one, as it stands or
1461
+ * under JSON escapes. This is how a tiny private value is matched. */
1462
+ whole(text) {
1463
+ let view = text;
1464
+ for (let layer = 0; view !== undefined && layer <= MAX_DECODE_LEVELS; layer++) {
1465
+ if (view.length <= this.widestForm && this.forms.has(view.includes("%") ? wireForms(view)[0] : view))
1466
+ return true;
1467
+ // An escape is at most six units for one, so a longer text cannot decode to a form.
1468
+ view = view.length <= 6 * this.widestForm && view.includes("\\") ? unescapeLayer(view)?.text : undefined;
1469
+ }
1470
+ return false;
1471
+ }
1472
+ /** Every K-unit run of a registered form, every identifier form, and in payload text every
1473
+ * short private form, in the raw text and each JSON-unescaped layer, mapped to source spans
1474
+ * before any replacement. A layer decodes while the one above holds an escape; each is scanned
1475
+ * only around the escapes it decoded, since any other match was found a layer up. Work beyond
1476
+ * the call's budget returns undefined, which withholds the text. */
1477
+ spansOf(text, scan, short) {
1478
+ const spans = [];
1479
+ const fragments = this.fragments();
1480
+ const matchers = [this.wholeMatcher(), short ? this.shortMatcher() : null].filter((matcher) => matcher !== null);
1481
+ const scanners = Number(fragments.size > 0) + matchers.length;
1482
+ if (!scanners)
1483
+ return { spans, deep: false };
697
1484
  const marks = markers(text);
698
- for (let layer = 0, more = true; more; layer++) {
699
- if (level + layer > MAX_DECODE_LEVELS || !spend(scan, 2 * text.length * (layer + 1)))
1485
+ if (!spend(scan, 2 * text.length * scanners))
1486
+ return undefined;
1487
+ fragments.scan(text, 0, text.length, spans, marks);
1488
+ for (const matcher of matchers)
1489
+ if (!matcher.scan(text, 0, text.length, { spans }, scan.budget, marks))
700
1490
  return undefined;
701
- let units = new Source(text);
702
- for (let depth = 0; depth < layer; depth++)
703
- units = new Unescaped(units);
704
- if (!this.matcher().scan(units, found, scan.budget, marks))
1491
+ const reach = Math.max(FRAGMENT_LENGTH, ...matchers.map((matcher) => matcher.widest));
1492
+ const layers = [];
1493
+ for (let upper = text; upper.includes("\\");) {
1494
+ // Decoding a layer copies text past its escapes, far cheaper per unit than scanning it.
1495
+ if (!spend(scan, Math.ceil(upper.length / 8)))
1496
+ return undefined;
1497
+ const layer = unescapeLayer(upper);
1498
+ if (!layer)
1499
+ break;
1500
+ if (layers.length === MAX_DECODE_LEVELS)
1501
+ return { spans, deep: true };
1502
+ layers.push(layer);
1503
+ const ranges = windows(layer, reach);
1504
+ let units = 0;
1505
+ for (let at = 0; at < ranges.length; at += 2)
1506
+ units += ranges[at + 1] - ranges[at];
1507
+ if (!spend(scan, 2 * units * scanners))
705
1508
  return undefined;
706
- more = escapes && (units instanceof Unescaped ? units.decoded > 0 && units.slashes > 0 : text.includes("\\"));
1509
+ const local = [];
1510
+ for (let at = 0; at < ranges.length; at += 2) {
1511
+ const [start, end] = [ranges[at], ranges[at + 1]];
1512
+ fragments.scan(layer.text, start, end, local, undefined);
1513
+ for (const matcher of matchers)
1514
+ if (!matcher.scan(layer.text, start, end, { spans: local }, scan.budget))
1515
+ return undefined;
1516
+ }
1517
+ const ordered = merged(local);
1518
+ const base = spans.length;
1519
+ for (let at = 0; at < ordered.length; at += 2)
1520
+ push(spans, base, toSource(layers, ordered[at], false), toSource(layers, ordered[at + 1] - 1, true) + 1, marks);
1521
+ upper = layer.text;
1522
+ }
1523
+ // Auth framing is never indexed, but an echo carrying it goes whole, as a header line does.
1524
+ for (let at = 0; at < spans.length; at += 2) {
1525
+ const framing = /(?:Bearer|token)(?: |\+|%20)$/.exec(text.slice(Math.max(0, spans[at] - 9), spans[at]));
1526
+ if (framing)
1527
+ spans[at] -= framing[0].length;
707
1528
  }
708
- return found;
1529
+ return { spans, deep: false };
709
1530
  }
710
- /** A structured leaf, property name, or prose. A short private form withholds prose
711
- * wherever it occurs, and a leaf only when the whole leaf is that form. */
712
- string(value, scan, prose, level = 0) {
713
- if (value.length >= this.floor(scan))
714
- return REDACTED;
715
- const found = this.find(value, scan, true, level);
716
- if (!found || (scan.payload && (prose ? found.short : found.exact)))
717
- return REDACTED;
718
- return found.spans.length ? replace(value, found.spans) : value;
1531
+ /** Add the span of an unfinished form the text ends inside, at any decoded layer. */
1532
+ openSpans(text, spans) {
1533
+ let open = -1;
1534
+ for (let layer = 0, more = true; more; layer++) {
1535
+ if (layer > MAX_DECODE_LEVELS) {
1536
+ spans.push(0, text.length);
1537
+ return;
1538
+ }
1539
+ let units = new Source(text);
1540
+ const closed = [];
1541
+ let decoded;
1542
+ for (let depth = 0; depth <= layer; depth++) {
1543
+ const view = new ClosedView(units);
1544
+ closed.push(view);
1545
+ units = depth < layer ? (decoded = new Unescaped(view)) : view;
1546
+ }
1547
+ const at = this.matcher().openAt(units);
1548
+ for (const start of [at, ...closed.map((view) => view.open)])
1549
+ if (start >= 0)
1550
+ open = open < 0 ? start : Math.min(open, start);
1551
+ more = layer === 0 ? text.includes("\\") : decoded.decoded > 0 && decoded.slashes > 0;
1552
+ }
1553
+ if (open >= 0)
1554
+ spans.push(open, text.length);
719
1555
  }
720
- /** A number, boolean, or null matching by its canonical text becomes a placeholder string. */
721
- scalar(value, scan) {
722
- const text = String(value);
723
- if ((typeof value === "number" || typeof value === "bigint") && text.length >= this.floor(scan))
724
- return REDACTED;
725
- const found = this.find(text, scan, false, 0);
726
- return !found || found.exact || found.spans.length ? REDACTED : value;
727
- }
728
- /** JSON objects, arrays, and string literals are parsed and walked, nested JSON strings
729
- * included; their serialized bytes must then hold no literal the walk cannot see, such as
730
- * one spanning tokens. Other text is prose or a structured leaf. */
731
- wire(value, scan, depth, prose, level) {
732
- if (value.length >= this.floor(scan))
733
- return REDACTED;
734
- if (level < MAX_DECODE_LEVELS && /^\s*["[{]/.test(value) && spend(scan, value.length)) {
735
- let parsed;
736
- try {
737
- parsed = { json: JSON.parse(value) };
1556
+ /** The one string checker. Every string that can reach an agent, an executor or storage is
1557
+ * checked here, under the whole lattice: projection leaves and numbers, object keys, declared
1558
+ * binary fields, the bytes of typed arrays and ArrayBuffers, cursor claims and every encoding
1559
+ * Connecta produces, error fields, provider lanes, bounded reads and structural fields.
1560
+ * `parts` is one lane: matched joined, raw and through its JSON layers, each match replaced in
1561
+ * every part it touches, a tiny private value withholding a part it equals, and every part
1562
+ * withheld that the set cannot fully check. Then every text and declared binary part is
1563
+ * decoded in each of its text views (see textViews), its base64 and base64url runs and a
1564
+ * declared field's whole content are decoded and checked as bytes, UTF-8 and nested base64,
1565
+ * and so are byte parts; a part a decoded view matches is withheld whole. Each decode is
1566
+ * charged to the call's budget, and a part it cannot pay for is withheld. A `bounded` lane is
1567
+ * text cut short, which also loses an unfinished tail. Nothing else matches a string that is
1568
+ * returned: the matching steps below are called only from here. */
1569
+ checked(parts, kinds, scan, bounded = false) {
1570
+ const safe = this.safeParts(parts, kinds, scan, bounded);
1571
+ this.encodedLeaves(parts, kinds, safe, scan);
1572
+ return safe;
1573
+ }
1574
+ /** Cursor claims: the whole text and its encoding under the boundary's rules, then every
1575
+ * string in the JSON under payload rules. Keys and numbers are Connecta's own names and
1576
+ * counts: a downstream value enters a cursor only as a string. */
1577
+ claimsClean(binary, encoded) {
1578
+ if (this.checked([encoded], [BINARY], this.scan(false))[0] !== encoded)
1579
+ return false;
1580
+ const text = utf8(binary);
1581
+ const parsed = parse(text);
1582
+ const strings = [];
1583
+ const collect = (value) => {
1584
+ if (typeof value === "string")
1585
+ strings.push(value);
1586
+ else if (value !== null && typeof value === "object")
1587
+ for (const item of Object.values(value))
1588
+ collect(item);
1589
+ };
1590
+ if (parsed)
1591
+ collect(parsed.json);
1592
+ else
1593
+ strings.push(text);
1594
+ const scan = this.scan(true);
1595
+ return strings.every((claim) => !this.project(claim, scan).changed);
1596
+ }
1597
+ /** The shortest base64 run inside a view that is decoded: long enough to carry the smallest
1598
+ * piece this scan matches (see SHORT_RUN_UNITS). */
1599
+ runUnits(scan) {
1600
+ return scan.payload && this.shortMatcher() ? SHORT_RUN_UNITS : FRAGMENT_RUN_UNITS;
1601
+ }
1602
+ /** The text views base64 is found in: the text; one layer of its JSON escapes decoded; its
1603
+ * percent escapes decoded; and its form decoding, a `+` read as a space with any percent
1604
+ * escapes decoded, whether or not it holds one. Two decodes chained, such as percent escapes
1605
+ * inside JSON escapes, are not a view. Undefined when the work budget cannot pay for them. */
1606
+ textViews(text, scan) {
1607
+ const escaped = text.includes("\\");
1608
+ const percent = text.includes("%");
1609
+ const plus = text.includes("+");
1610
+ const decodes = Number(escaped) + Number(percent) + Number(plus);
1611
+ if (!decodes)
1612
+ return [text];
1613
+ if (!spend(scan, decodes * Math.ceil(text.length / 8)))
1614
+ return undefined;
1615
+ const views = [text];
1616
+ for (const view of [
1617
+ escaped ? unescapeLayer(text)?.text : undefined,
1618
+ percent ? percentDecoded(text, false) : undefined,
1619
+ plus ? percentDecoded(text, true) : undefined,
1620
+ ])
1621
+ if (view !== undefined && !views.includes(view))
1622
+ views.push(view);
1623
+ return views;
1624
+ }
1625
+ /** Text and declared binary parts carrying base64 or base64url, in any text view, the whole
1626
+ * view or a run inside it, and byte parts, whose decoded bytes or their UTF-8 text hold a
1627
+ * match are withheld whole: no later pass reads through an encoding the output does not
1628
+ * declare. A declared field is also decoded whole, as its reader decodes it. Every part's
1629
+ * decoded views are matched together, as one lane between unmatched separators. Each decode
1630
+ * is paid for from the call's work budget; a part the budget cannot check is withheld. */
1631
+ encodedLeaves(parts, kinds, safe, scan) {
1632
+ // A run decodes to a group of views, once per call however many parts repeat it, and so do
1633
+ // byte parts; each part's groups are recorded as (part, group) pairs. Group -1 is content
1634
+ // the work budget could not check, which withholds every part holding it.
1635
+ const views = [];
1636
+ const viewGroups = [];
1637
+ const groups = new Map();
1638
+ const byteGroups = new Map();
1639
+ const pairLeaves = [];
1640
+ const pairGroups = [];
1641
+ let size = 0;
1642
+ const minimum = this.runUnits(scan);
1643
+ /** Views of bytes: the bytes themselves unless the lane already matched them, their UTF-8
1644
+ * text when a form is not ASCII, and the base64 they carry. */
1645
+ const add = (binary, group, level, self = true) => {
1646
+ if (self) {
1647
+ views.push(binary);
1648
+ viewGroups.push(group);
738
1649
  }
739
- catch {
740
- // Not JSON.
1650
+ if (this.unicodeForms && /[\x80-\xff]/.test(binary)) {
1651
+ if (!spend(scan, Math.ceil(binary.length / 8)))
1652
+ return false;
1653
+ views.push(utf8(binary));
1654
+ viewGroups.push(group);
1655
+ }
1656
+ if (level < MAX_ENCODED_LEVELS)
1657
+ for (const run of encodedRuns(binary, minimum)) {
1658
+ if (!spend(scan, Math.ceil(run.length / 8)))
1659
+ return false;
1660
+ const inner = decodeBase64(run);
1661
+ if (inner && !add(inner, group, level + 1))
1662
+ return false;
1663
+ }
1664
+ return true;
1665
+ };
1666
+ const groupOf = (run) => {
1667
+ let group = groups.get(run);
1668
+ if (group === undefined) {
1669
+ group = size++;
1670
+ const bytes = spend(scan, Math.ceil(run.length / 8)) ? decodeBase64(run) : null;
1671
+ if (bytes === null || (bytes && !add(bytes, group, 1)))
1672
+ group = -1;
1673
+ groups.set(run, group);
741
1674
  }
742
- if (parsed) {
743
- const { json } = parsed;
744
- const walked = this.walk(json, scan, prose && typeof json === "string", depth, level + 1);
745
- const text = walked === json ? value : JSON.stringify(walked);
746
- const found = this.find(text, scan, false, level);
747
- return found && !found.spans.length && text.length < this.floor(scan) ? text : REDACTED;
1675
+ return group;
1676
+ };
1677
+ const bytesGroup = (bytes) => {
1678
+ let group = byteGroups.get(bytes);
1679
+ if (group === undefined) {
1680
+ group = size++;
1681
+ if (!add(bytes, group, 1, false))
1682
+ group = -1;
1683
+ byteGroups.set(bytes, group);
748
1684
  }
1685
+ return group;
1686
+ };
1687
+ leaves: for (let index = 0; index < parts.length; index++) {
1688
+ // The part as text matching left it: a run it replaced in place, such as a registered base64
1689
+ // form, leaves no decodable remnant of K units; a match only decoding reveals withholds all.
1690
+ // Binary and bytes any match changed are withheld whole already.
1691
+ const kind = kinds[index];
1692
+ const text = safe[index];
1693
+ if (kind === NUMBER || text === REDACTED || this.memo.get(text) === true)
1694
+ continue;
1695
+ if ((!scan.payload && this.memo.has(text)) || (kind !== TEXT && text !== parts[index]))
1696
+ continue;
1697
+ const pair = (group) => {
1698
+ if (group < 0) {
1699
+ safe[index] = REDACTED;
1700
+ return false;
1701
+ }
1702
+ pairLeaves.push(index);
1703
+ pairGroups.push(group);
1704
+ return true;
1705
+ };
1706
+ if (kind === BYTES) {
1707
+ pair(bytesGroup(text));
1708
+ continue;
1709
+ }
1710
+ const textViews = this.textViews(text, scan);
1711
+ if (!textViews) {
1712
+ safe[index] = REDACTED;
1713
+ continue;
1714
+ }
1715
+ for (const view of textViews)
1716
+ for (const run of encodedRuns(view, minimum))
1717
+ if (!pair(groupOf(run)))
1718
+ continue leaves;
1719
+ if (kind === BINARY)
1720
+ pair(groupOf(unpadded(text)));
749
1721
  }
750
- return this.string(value, scan, prose, level);
1722
+ if (!views.length)
1723
+ return;
1724
+ const matched = new Uint8Array(size);
1725
+ const withhold = (at) => (matched[viewGroups[at]] = 1);
1726
+ try {
1727
+ if (scan.payload && this.smallForms)
1728
+ views.forEach((view, at) => this.whole(view) && withhold(at));
1729
+ const lane = views.join("￿");
1730
+ const matches = this.spansOf(lane, scan, scan.payload);
1731
+ if (!matches)
1732
+ return views.forEach((_, at) => withhold(at));
1733
+ if (matches.deep)
1734
+ views.forEach((view, at) => view.includes("\\") && withhold(at));
1735
+ const starts = [];
1736
+ for (let at = 0, offset = 0; at < views.length; offset += views[at++].length + 1)
1737
+ starts.push(offset);
1738
+ const ranges = merged(matches.spans);
1739
+ for (let at = 0; at < ranges.length; at += 2) {
1740
+ let [low, high] = [0, starts.length];
1741
+ while (low < high) {
1742
+ const middle = (low + high) >>> 1;
1743
+ if (starts[middle] <= ranges[at])
1744
+ low = middle + 1;
1745
+ else
1746
+ high = middle;
1747
+ }
1748
+ for (let view = Math.max(0, low - 1); view < starts.length && starts[view] < ranges[at + 1]; view++)
1749
+ withhold(view);
1750
+ }
1751
+ }
1752
+ finally {
1753
+ for (let pair = 0; pair < pairLeaves.length; pair++)
1754
+ if (matched[pairGroups[pair]])
1755
+ safe[pairLeaves[pair]] = REDACTED;
1756
+ }
1757
+ }
1758
+ /** The parts' joined text, matched once, with each part's replacement. A tiny or short
1759
+ * private value equal to a whole part withholds it. A number or a serialized container that
1760
+ * a match touches is withheld whole. A bounded lane also loses an unfinished tail, and is
1761
+ * withheld whole when the set could not tell where one begins. */
1762
+ safeParts(parts, kinds, scan, bounded) {
1763
+ if (!parts.length)
1764
+ return parts;
1765
+ const text = parts.join("");
1766
+ // A form too large to register or index could begin anywhere in what was read.
1767
+ const matches = bounded && this.uncheckedTail() ? undefined : this.spansOf(text, scan, scan.payload);
1768
+ if (bounded && (!matches || matches.deep))
1769
+ return parts.map((part) => part && REDACTED);
1770
+ if (bounded)
1771
+ this.openSpans(text, matches.spans);
1772
+ let safe = matches ? replaceParts(parts, matches.spans) : this.leafByLeaf(parts, scan);
1773
+ if (matches?.deep)
1774
+ safe = safe.map((next, index) => (parts[index].includes("\\") ? REDACTED : next));
1775
+ if (scan.payload && this.smallForms)
1776
+ safe = safe.map((next, index) => (this.whole(parts[index]) ? REDACTED : next));
1777
+ const limit = this.limit(scan);
1778
+ if (limit < Infinity)
1779
+ safe = withholdRuns(parts, safe, limit);
1780
+ return safe.map((next, index) => (next !== parts[index] && kinds[index] === NUMBER ? REDACTED : next));
1781
+ }
1782
+ /** Leaves too large to match joined within the work budget: each is matched alone, in order,
1783
+ * and one that does not fit is withheld without spending what a smaller one needs. A run
1784
+ * crossing from one kept leaf into the next, through any short leaves between them, is then
1785
+ * matched SEAM_UNITS deep on each side of the seam. */
1786
+ leafByLeaf(parts, scan) {
1787
+ const spans = parts.map((part) => {
1788
+ const matches = this.spansOf(part, scan, scan.payload);
1789
+ return !matches || (matches.deep && part.includes("\\")) ? undefined : matches.spans;
1790
+ });
1791
+ let pieces = [];
1792
+ const seam = () => {
1793
+ const text = pieces.map((piece) => piece.text).join("");
1794
+ const matches = pieces.length > 1 ? this.spansOf(text, scan, scan.payload) : { spans: [], deep: false };
1795
+ const ranges = matches ? merged(matches.spans) : [];
1796
+ let offset = 0;
1797
+ for (const piece of pieces) {
1798
+ const end = offset + piece.text.length;
1799
+ if (!matches || matches.deep)
1800
+ spans[piece.index] = undefined;
1801
+ for (let at = 0; at < ranges.length; at += 2)
1802
+ if (ranges[at] < end && ranges[at + 1] > offset)
1803
+ spans[piece.index]?.push(piece.start + Math.max(0, ranges[at] - offset), piece.start + Math.min(piece.text.length, ranges[at + 1] - offset));
1804
+ offset = end;
1805
+ }
1806
+ pieces = [];
1807
+ };
1808
+ parts.forEach((part, index) => {
1809
+ if (!spans[index])
1810
+ return seam();
1811
+ if (part.length <= 2 * SEAM_UNITS)
1812
+ return void pieces.push({ index, start: 0, text: part });
1813
+ pieces.push({ index, start: 0, text: part.slice(0, SEAM_UNITS) });
1814
+ seam();
1815
+ pieces.push({ index, start: part.length - SEAM_UNITS, text: part.slice(-SEAM_UNITS) });
1816
+ });
1817
+ seam();
1818
+ return parts.map((part, index) => {
1819
+ const own = spans[index];
1820
+ return !own ? part && REDACTED : own.length ? replace(part, own) : part;
1821
+ });
751
1822
  }
752
- /** Iterative and copy-on-write: only containers on a changed path are copied. A container's
753
- * entries are counted against the node budget before it is walked; a cycle or a container
754
- * past MAX_WALK_DEPTH is withheld, and one past the node budget is checked as serialized text. */
755
- walk(value, scan, prose, depth, level) {
756
- const stack = [];
1823
+ /** Serialized JSON whose keys, syntax and leaves together hold a K-unit run of a form: one
1824
+ * spanning tokens, which no leaf holds alone, withholds the whole text. A run inside one
1825
+ * string or number is a leaf's, already matched in the projection. */
1826
+ spansTokens(text, scan) {
1827
+ if (!this.fragments().size)
1828
+ return false;
1829
+ if (!spend(scan, 2 * text.length))
1830
+ return true;
1831
+ const spans = [];
1832
+ this.fragments().scan(text, 0, text.length, spans, undefined);
1833
+ if (!spans.length)
1834
+ return false;
1835
+ const tokens = jsonTokens(text);
1836
+ const ranges = merged(spans);
1837
+ for (let at = 0; at < ranges.length; at += 2) {
1838
+ let [low, high] = [0, tokens.length / 2];
1839
+ while (low < high) {
1840
+ const middle = (low + high) >>> 1;
1841
+ if (tokens[middle * 2 + 1] <= ranges[at])
1842
+ low = middle + 1;
1843
+ else
1844
+ high = middle;
1845
+ }
1846
+ if (low * 2 >= tokens.length || tokens[low * 2] > ranges[at] || tokens[low * 2 + 1] < ranges[at + 1])
1847
+ return true;
1848
+ }
1849
+ return false;
1850
+ }
1851
+ /** The output boundary. One walk reads every field once, from its descriptor, and projects
1852
+ * every string and number leaf in serialization order, nested JSON strings parsed and errors'
1853
+ * own fields included, declared binary fields and the bytes and numbers typed binary holds
1854
+ * among them; the one checker matches the joined projection, so a form split across adjacent
1855
+ * leaves is found, and only the affected piece of each leaf is written back. Keys and scalars
1856
+ * go through the same checker alone. Fixed framing words and tags, outcome booleans, and on an
1857
+ * error its classification code and a name its registered class gives it (redaction-identity's
1858
+ * fixed allowlist) are kept: nothing downstream chooses them. Every other field is matched,
1859
+ * whatever its name; an error's other name is projected like any field and withheld whole when
1860
+ * anything changes any part of it.
1861
+ *
1862
+ * What returns is what was checked, and only JSON data: strings, numbers, booleans, null,
1863
+ * plain objects and arrays, a fresh built-in Uint8Array, and error copies. A caller's container
1864
+ * is returned as it stands only as verified plain data, so a later read returns the same
1865
+ * values: its prototype is Object.prototype, Array.prototype or null, nothing on its chain is a
1866
+ * toJSON, it holds no symbol field and an array no field but its indices (a hole checked
1867
+ * through its prototypes), every field is a data property this walk read, and nothing in it
1868
+ * changed. Anything else is a copy. An error always is, of its own class, with the accessors in
1869
+ * ERROR_ACCESSORS and those its chain defines read once into fields and a toJSON shadowed, never
1870
+ * read; an object of any other class is plain data, its chain's accessors unread; an array that
1871
+ * is not plain is a plain array of its indices; a String, Number or Boolean wrapper is its
1872
+ * primitive. A typed array, DataView or ArrayBuffer becomes a fresh Uint8Array of the bytes it
1873
+ * held, which are checked as bytes, and as numbers with any other elements it held. A function
1874
+ * or symbol is withheld. A container's own accessor is never invoked: its copy leaves it out,
1875
+ * or holds the placeholder at an array index. A toJSON is called once, as serialization would,
1876
+ * and replaced by its checked result. A change copies every container on its path on first
1877
+ * write, so no caller's value is ever mutated, and a container past the node budget is
1878
+ * withheld whole. Copies hold writable, configurable data properties and keep the identity
1879
+ * facts registered in redaction-identity. */
1880
+ project(value, scan, keepKeys = false) {
1881
+ const parts = [];
1882
+ const kinds = [];
1883
+ const frames = [];
1884
+ const slots = [];
1885
+ const owners = [];
1886
+ // Structural changes found while walking: a withheld or checked field, a copied container.
1887
+ const replaced = [];
1888
+ // Caller's arrays that are not plain data: copied even where nothing in them changed.
1889
+ const copied = [];
1890
+ const nested = [];
757
1891
  const active = new Set();
758
- const visit = (item, prose, depth) => {
759
- if (typeof item === "string")
760
- return this.wire(item, scan, depth, prose, level);
761
- if (item === null || typeof item === "number" || typeof item === "bigint" || typeof item === "boolean")
762
- return scan.payload ? this.scalar(item, scan) : item;
1892
+ // Keys and scalars repeat across rows; each is checked once per call.
1893
+ const seen = { keys: new Map(), scalars: new Map() };
1894
+ // Errors' unregistered names, as walked: a copy whose name changed holds the placeholder.
1895
+ const named = [];
1896
+ // Typed binary: where its copy was put, and the leaves its numbers and bytes span.
1897
+ const binaries = [];
1898
+ // The accessors each prototype's chain defines, found once per call.
1899
+ const chains = new Map();
1900
+ let changed = false;
1901
+ const touch = (owner) => {
1902
+ changed = true;
1903
+ if (owner >= 0)
1904
+ nested[owner].dirty = true;
1905
+ };
1906
+ const withheld = (owner) => {
1907
+ touch(owner);
1908
+ return REDACTED;
1909
+ };
1910
+ /** A key or scalar's text, through the one checker alone. */
1911
+ const check = (cache, text, kind) => {
1912
+ let result = cache.get(text);
1913
+ if (result === undefined)
1914
+ cache.set(text, (result = this.checked([text], [kind], scan)[0]));
1915
+ return result;
1916
+ };
1917
+ const replace = (frame, slot, next, owner) => {
1918
+ replaced.push([frame, slot, next]);
1919
+ touch(owner);
1920
+ };
1921
+ const leaf = (text, kind, frame, slot, owner) => {
1922
+ parts.push(text);
1923
+ kinds.push(kind);
1924
+ frames.push(frame);
1925
+ slots.push(slot);
1926
+ owners.push(owner);
1927
+ };
1928
+ /** A field put on a copy or on parsed JSON: an array index by assignment, any other key as a
1929
+ * writable, configurable data property, enumerable as it was. A frozen or read-only original
1930
+ * never makes a write throw, and no inherited setter runs. */
1931
+ const put = (holder, slot, next, enumerable = true) => {
1932
+ if (typeof slot === "number")
1933
+ holder[slot] = next;
1934
+ else
1935
+ Object.defineProperty(holder, slot, {
1936
+ value: next,
1937
+ writable: true,
1938
+ configurable: true,
1939
+ enumerable: Object.getOwnPropertyDescriptor(holder, slot)?.enumerable ?? enumerable,
1940
+ });
1941
+ };
1942
+ /** A caller's container copied from its descriptors, never invoking an accessor: an array as
1943
+ * a plain array of its indices, a hole staying one and an accessor the placeholder; a plain
1944
+ * object as its own data fields. */
1945
+ const shallow = (frame) => {
1946
+ const item = frame.target;
1947
+ if (Array.isArray(item)) {
1948
+ const length = item.length;
1949
+ const out = [];
1950
+ out.length = length;
1951
+ for (let index = 0; index < length; index++) {
1952
+ const descriptor = Object.getOwnPropertyDescriptor(item, index);
1953
+ if (descriptor)
1954
+ out[index] = "value" in descriptor ? descriptor.value : REDACTED;
1955
+ }
1956
+ return redactedCopyOf(item, out);
1957
+ }
1958
+ const out = Object.create(Object.getPrototypeOf(item));
1959
+ for (const name of Object.getOwnPropertyNames(item)) {
1960
+ const descriptor = Object.getOwnPropertyDescriptor(item, name);
1961
+ if ("value" in descriptor)
1962
+ put(out, name, descriptor.value, descriptor.enumerable);
1963
+ }
1964
+ return redactedCopyOf(item, out);
1965
+ };
1966
+ // toJSON results, which replace their container in its slot and may be copied after.
1967
+ const serialized = new Set();
1968
+ /** A caller's container walked in a slot. A toJSON result is remembered there, so a copy it
1969
+ * gets replaces it in that slot. */
1970
+ const walked = (frame, item, slot, owned) => {
1971
+ const own = { target: item, shared: !owned, parent: frame, slot };
1972
+ if (!owned && serialized.has(item))
1973
+ (frame.children ??= new Map()).set(slot, own);
1974
+ return own;
1975
+ };
1976
+ const writable = (frame) => {
1977
+ if (!frame.shared)
1978
+ return frame.target;
1979
+ if (!frame.copy) {
1980
+ frame.copy = shallow(frame);
1981
+ put(writable(frame.parent), frame.slot, frame.copy);
1982
+ }
1983
+ return frame.copy;
1984
+ };
1985
+ /** The accessors a prototype's chain defines below Object.prototype, by name. */
1986
+ const accessorsOf = (prototype) => {
1987
+ let names = chains.get(prototype);
1988
+ if (!names) {
1989
+ names = [];
1990
+ for (let at = prototype; at && at !== Object.prototype; at = Object.getPrototypeOf(at))
1991
+ for (const name of Object.getOwnPropertyNames(at)) {
1992
+ const descriptor = Object.getOwnPropertyDescriptor(at, name);
1993
+ if (descriptor && !("value" in descriptor) && !names.includes(name))
1994
+ names.push(name);
1995
+ }
1996
+ chains.set(prototype, names);
1997
+ }
1998
+ return names;
1999
+ };
2000
+ const visit = (item, frame, slot, depth, level, owner, owned) => {
2001
+ if (typeof item === "string") {
2002
+ if (this.mintedStates.has(item))
2003
+ return item;
2004
+ if (level < MAX_DECODE_LEVELS && /^\s*["[{]/.test(item) && spend(scan, item.length)) {
2005
+ const parsed = parse(item);
2006
+ if (parsed) {
2007
+ const entry = { frame, slot, json: undefined, owner, crossing: false, dirty: false };
2008
+ entry.crossing = this.spansTokens(item, scan);
2009
+ const index = nested.push(entry) - 1;
2010
+ // Parsed JSON is this walk's own, so it is written in place.
2011
+ const holder = { target: entry, shared: false, parent: undefined, slot: "json" };
2012
+ entry.json = visit(parsed.json, holder, "json", depth + 1, level + 1, index, true);
2013
+ return item;
2014
+ }
2015
+ }
2016
+ leaf(item, TEXT, frame, slot, owner);
2017
+ return item;
2018
+ }
2019
+ if (typeof item === "number" || typeof item === "bigint") {
2020
+ leaf(String(item), NUMBER, frame, slot, owner);
2021
+ return item;
2022
+ }
2023
+ if (typeof item === "boolean" || item === null)
2024
+ return check(seen.scalars, String(item), NUMBER) === String(item) ? item : withheld(owner);
2025
+ // A function carries no data, and every exit's serializer would call a toJSON it holds; a
2026
+ // symbol carries none either. Both are withheld.
2027
+ if (typeof item === "function" || typeof item === "symbol")
2028
+ return withheld(owner);
763
2029
  if (typeof item !== "object")
764
2030
  return item;
765
2031
  if (active.has(item) || depth > MAX_WALK_DEPTH)
766
- return REDACTED;
767
- const names = Array.isArray(item) ? undefined : Object.getOwnPropertyNames(item);
768
- const size = names ? names.length : item.length;
2032
+ return withheld(owner);
2033
+ active.add(item);
2034
+ try {
2035
+ return container(item, frame, slot, depth, level, owner, owned);
2036
+ }
2037
+ catch {
2038
+ return withheld(owner); // A proxy trap or a toJSON that threw.
2039
+ }
2040
+ finally {
2041
+ active.delete(item);
2042
+ }
2043
+ };
2044
+ /** A child that changed: written into parsed JSON this walk owns, or recorded as a change
2045
+ * to a caller's container. */
2046
+ const settle = (frame, slot, next, owner) => {
2047
+ if (frame.shared)
2048
+ return replace(frame, slot, next, owner);
2049
+ put(frame.target, slot, next);
2050
+ touch(owner);
2051
+ };
2052
+ /** Entries within the node budget are walked; a container past it is withheld whole. */
2053
+ const fits = (size) => {
769
2054
  if (size > scan.budget.nodes)
770
- return this.serialized(item, scan, level);
2055
+ return false;
771
2056
  scan.budget.nodes -= size;
772
- if (!size)
773
- return item;
774
- active.add(item);
775
- const typed = item instanceof Error || typeof item.retryable === "boolean";
776
- stack.push({ item, names, size, index: 0, prose, depth, typed, key: "", name: "", field: undefined });
777
- return OPEN;
2057
+ return true;
778
2058
  };
779
- // Settle the top frame's next entry, drop it, or open its container.
780
- const enter = (frame) => {
781
- const { item, names } = frame;
782
- if (!names)
783
- return visit((frame.field = item[frame.index]), frame.prose, frame.depth + 1);
784
- const key = (frame.key = frame.name = names[frame.index]);
785
- const descriptor = Object.getOwnPropertyDescriptor(item, key);
786
- let prose = PROSE_KEYS.has(key);
787
- if (descriptor && "value" in descriptor)
788
- frame.field = descriptor.value;
789
- // V8 lazily renders Error.stack through an own accessor. Snapshot it
790
- // before rebuilding an error, so its diagnostic is redacted too.
791
- else if (key === "stack" && item instanceof Error) {
792
- frame.field = item.stack;
793
- prose = true;
794
- }
795
- // JSON and errors carry data properties. A downstream-authored getter
796
- // is not a safe way to expose a diagnostic to an agent.
797
- else
798
- return DROP;
799
- const field = frame.field;
800
- if (!FRAMING_KEYS.has(key))
801
- frame.name = this.string(key, scan, false);
802
- if ((typeof field === "string" && TAGS.has(`${key}:${field}`)) ||
803
- (FACTS.has(key) && (typeof field === "boolean" || typeof field === "number")))
804
- return field;
805
- if (frame.typed && (key === "code" || key === "name") && typeof field === "string")
806
- return this.credentials().string(field, { ...scan, payload: false }, false);
807
- if (key === "blob" && "uri" in item && typeof field === "string")
808
- return this.blob(field, scan);
809
- const child = key === "content" ? this.joinedContent(field, scan, frame.depth, level) : field;
810
- return visit(child, prose, frame.depth + 1);
2059
+ /** A declared base64 field: a leaf the checker also decodes whole, unless this set already
2060
+ * found the same content clean. */
2061
+ const declared = (text, frame, slot, owner) => {
2062
+ if (this.memo.get(text) !== true && (scan.payload || !this.memo.has(text)))
2063
+ leaf(text, BINARY, frame, slot, owner);
2064
+ return text;
811
2065
  };
812
- // Record a settled entry. The first change copies the container's earlier entries.
813
- const settle = (frame, value) => {
814
- const { item, names, index } = frame;
815
- if (!frame.copy) {
816
- if (value === frame.field && frame.name === frame.key)
817
- return;
818
- if (!names)
819
- frame.copy = item.slice(0, index);
2066
+ /** Typed binary as the fresh Uint8Array of its bytes. Its leaves are the numbers JSON shows
2067
+ * for it and for its copy, each element's and each byte's, and its bytes; a match in any
2068
+ * withholds it whole, as does a size past the node budget. */
2069
+ const typed = ({ bytes, elements }, frame, slot, owner) => {
2070
+ const numbers = elements ? new elements(bytes.buffer) : undefined;
2071
+ if (!fits(bytes.length + (numbers?.length ?? 0) + 1) || !spend(scan, Math.ceil(bytes.length / 8)))
2072
+ return withheld(owner);
2073
+ // The leaves write nowhere: the copy's slot is settled once they are checked.
2074
+ const sink = { target: [], shared: false, parent: undefined, slot };
2075
+ const start = parts.length;
2076
+ if (numbers)
2077
+ for (let index = 0; index < numbers.length; index++)
2078
+ leaf(String(numbers[index]), NUMBER, sink, index, owner);
2079
+ for (let index = 0; index < bytes.length; index++)
2080
+ leaf(DECIMAL[bytes[index]], NUMBER, sink, index, owner);
2081
+ leaf(latin1(bytes), BYTES, sink, 0, owner);
2082
+ binaries.push([frame, slot, owner, start, parts.length]);
2083
+ return bytes;
2084
+ };
2085
+ const container = (item, frame, slot, depth, level, owner, owned) => {
2086
+ const error = item instanceof Error;
2087
+ if (!error && !owned) {
2088
+ const held = binaryOf(item);
2089
+ if (held)
2090
+ return typed(held, frame, slot, owner);
2091
+ const primitive = unboxed(item);
2092
+ if (primitive)
2093
+ return visit(primitive.value, frame, slot, depth + 1, level, owner, false);
2094
+ // What serialization would read: a toJSON result, checked in place of the container.
2095
+ const toJSON = inherited(item, "toJSON", WITHHELD_ACCESSOR);
2096
+ if (toJSON === WITHHELD_ACCESSOR)
2097
+ return withheld(owner);
2098
+ if (typeof toJSON === "function") {
2099
+ const result = toJSON.call(item, String(slot));
2100
+ if (result !== null && typeof result === "object")
2101
+ serialized.add(result);
2102
+ return visit(result, frame, slot, depth + 1, level, owner, false);
2103
+ }
2104
+ }
2105
+ return Array.isArray(item)
2106
+ ? list(item, frame, slot, depth, level, owner, owned)
2107
+ : record(item, error, frame, slot, depth, level, owner, owned);
2108
+ };
2109
+ /** An array's indices, each read once. A caller's array stands only as verified plain data;
2110
+ * one that is not is copied as a plain array of its indices, which drops other fields. */
2111
+ const list = (item, frame, slot, depth, level, owner, owned) => {
2112
+ const length = item.length;
2113
+ if (!fits(length))
2114
+ return withheld(owner);
2115
+ const own = walked(frame, item, slot, owned);
2116
+ for (let index = 0; index < length; index++) {
2117
+ const descriptor = Object.getOwnPropertyDescriptor(item, index);
2118
+ if (!descriptor) {
2119
+ // A hole serializes as what its prototypes hold at its index: a data field is checked
2120
+ // and copied in, an accessor is withheld unread, and nothing leaves the hole as it is.
2121
+ const found = owned ? undefined : inherited(item, String(index), WITHHELD_ACCESSOR);
2122
+ if (found === WITHHELD_ACCESSOR)
2123
+ replace(own, index, REDACTED, owner);
2124
+ else if (found !== undefined) {
2125
+ replaced.push([own, index, found]);
2126
+ const next = visit(found, own, index, depth + 1, level, owner, owned);
2127
+ if (next !== found)
2128
+ settle(own, index, next, owner);
2129
+ }
2130
+ continue;
2131
+ }
2132
+ if (!("value" in descriptor)) {
2133
+ replace(own, index, REDACTED, owner);
2134
+ continue;
2135
+ }
2136
+ const next = visit(descriptor.value, own, index, depth + 1, level, owner, owned);
2137
+ if (next !== descriptor.value)
2138
+ settle(own, index, next, owner);
2139
+ }
2140
+ // Fields other than its indices are not read: JSON, which the MCP wire, the result stash and
2141
+ // every executor marshal with, never serializes them, and listing them would allocate a
2142
+ // string per index.
2143
+ if (!owned && (Object.getPrototypeOf(item) !== Array.prototype || Object.getOwnPropertySymbols(item).length > 0))
2144
+ copied.push(own);
2145
+ return item;
2146
+ };
2147
+ const record = (item, error, frame, slot, depth, level, owner, owned) => {
2148
+ const names = Object.getOwnPropertyNames(item);
2149
+ if (!fits(names.length))
2150
+ return withheld(owner);
2151
+ const prototype = Object.getPrototypeOf(item);
2152
+ let plain = owned ||
2153
+ (!error &&
2154
+ (prototype === Object.prototype || prototype === null) &&
2155
+ Object.getOwnPropertySymbols(item).length === 0);
2156
+ // Every field is read once, and every decision below reads what was read.
2157
+ const keys = [];
2158
+ const values = [];
2159
+ const visible = [];
2160
+ for (const name of names) {
2161
+ const descriptor = Object.getOwnPropertyDescriptor(item, name);
2162
+ if (!descriptor)
2163
+ continue;
2164
+ // An error is never replaced by its toJSON: its copy shadows one.
2165
+ if ("value" in descriptor)
2166
+ values.push(error && name === "toJSON" ? undefined : descriptor.value);
2167
+ else if (error && descriptor.get && ERROR_ACCESSORS.has(name)) {
2168
+ // V8 renders Error.stack through an own accessor: it is read once into the copy.
2169
+ try {
2170
+ values.push(Reflect.apply(descriptor.get, item, []));
2171
+ }
2172
+ catch {
2173
+ continue; // A getter that throws leaves its field out of the copy.
2174
+ }
2175
+ }
820
2176
  else {
821
- frame.copy = item instanceof Error ? Object.create(Object.getPrototypeOf(item)) : {};
822
- for (const key of names.slice(0, index))
823
- define(frame.copy, item, key, key, item[key]);
2177
+ // Never invoked: the container is copied without it.
2178
+ plain = false;
2179
+ continue;
824
2180
  }
2181
+ keys.push(name);
2182
+ visible.push(descriptor.enumerable === true);
825
2183
  }
826
- if (!names)
827
- frame.copy[index] = value;
828
- else if (value !== DROP)
829
- define(frame.copy, item, frame.key, frame.name, value);
2184
+ // A key is checked like any text; an error's toJSON shadow keeps its name, so it shadows.
2185
+ // `keepKeys` leaves the walked value's own keys, never those of JSON inside a string.
2186
+ const rename = (key) => FRAMING_KEYS.has(key) || (error && key === "toJSON") || (keepKeys && level === 0 && !error)
2187
+ ? key
2188
+ : check(seen.keys, key, TEXT);
2189
+ const renamed = keys.map(rename);
2190
+ if (renamed.some((name, index) => name !== keys[index]))
2191
+ plain = false;
2192
+ // An error's copy is of its class. Every accessor its chain defines, such as DOMException's
2193
+ // name and message or an SDK error's status, and whatever the chain answers for the names
2194
+ // in ERROR_ACCESSORS, is read once and held as a checked field under its checked name, so
2195
+ // no later read of the copy invokes a getter. A toJSON, data or accessor, is never read: the
2196
+ // copy shadows it, so no serializer can run. Any other copy is plain data.
2197
+ let extras;
2198
+ if (error && prototype) {
2199
+ for (const name of [...ERROR_ACCESSORS, ...accessorsOf(prototype)]) {
2200
+ if (name === "toJSON" || keys.includes(name) || extras?.some(([taken]) => taken === name))
2201
+ continue;
2202
+ if (!(name in item))
2203
+ continue;
2204
+ try {
2205
+ (extras ??= []).push([name, Reflect.get(item, name)]);
2206
+ }
2207
+ catch {
2208
+ (extras ??= []).push([name, undefined]); // A getter that throws leaves its field shadowed.
2209
+ }
2210
+ }
2211
+ if (!keys.includes("toJSON") && inherited(item, "toJSON", WITHHELD_ACCESSOR) !== undefined)
2212
+ (extras ??= []).push(["toJSON", undefined]);
2213
+ }
2214
+ // A container walked as it stands: a caller's, copied on first write, or parsed JSON this
2215
+ // walk owns, written in place. Any other is copied now, field by field.
2216
+ const own = plain
2217
+ ? walked(frame, item, slot, owned)
2218
+ : {
2219
+ target: Object.create(error || prototype === null ? prototype : Object.prototype),
2220
+ shared: false,
2221
+ parent: frame,
2222
+ slot,
2223
+ };
2224
+ const out = own.target;
2225
+ const field = (name) => values[keys.indexOf(name)];
2226
+ const count = keys.length + (extras?.length ?? 0);
2227
+ for (let index = 0; index < count; index++) {
2228
+ const extra = index >= keys.length ? extras[index - keys.length] : undefined;
2229
+ const key = extra ? extra[0] : keys[index];
2230
+ const value = extra ? extra[1] : values[index];
2231
+ const name = extra ? rename(key) : renamed[index];
2232
+ // Two keys renamed alike keep the first; the second's leaves write nowhere.
2233
+ const target = !plain && Object.hasOwn(out, name) ? { target: {}, shared: false, parent: undefined, slot: name } : own;
2234
+ let next;
2235
+ if ((typeof value === "string" && TAGS.has(`${key}:${value}`)) ||
2236
+ (typeof value === "boolean" && FACTS.has(key)) ||
2237
+ (error && typeof value === "string" && key === "code" && classificationCode(value) !== undefined))
2238
+ next = value;
2239
+ // An error's name is kept only as one its registered class gives it. Any other is a field
2240
+ // like any other, projected with its neighbours, and withheld whole once anything changes
2241
+ // any part of it; the copy keeps its class.
2242
+ else if (error && key === "name" && typeof value === "string" && keptErrorName(prototype, value))
2243
+ next = value;
2244
+ else if (typeof value === "string" && binaryField(field, key))
2245
+ next = declared(value, target, name, owner);
2246
+ else {
2247
+ next = visit(value, target, name, depth + 1, level, owner, owned);
2248
+ if (error && key === "name" && typeof value === "string" && target === own)
2249
+ named.push([out, name, value]);
2250
+ }
2251
+ if (target !== own)
2252
+ continue;
2253
+ if (!plain)
2254
+ put(out, name, next, extra ? false : visible[index]);
2255
+ else if (next !== value)
2256
+ settle(own, name, next, owner);
2257
+ }
2258
+ if (plain)
2259
+ return item;
2260
+ touch(owner);
2261
+ return redactedCopyOf(item, out);
830
2262
  };
831
- let settled = visit(value, prose, depth);
832
- while (stack.length) {
833
- const frame = stack[stack.length - 1];
834
- if (settled === OPEN)
835
- settled = enter(frame);
836
- if (settled === OPEN)
837
- continue;
838
- settle(frame, settled);
839
- settled = OPEN;
840
- if (++frame.index < frame.size)
841
- continue;
842
- stack.pop();
843
- active.delete(frame.item);
844
- const { item, copy } = frame;
845
- settled = !copy ? item : item instanceof Error ? carryFailureFacts(item, copy) : copy;
2263
+ const root = { target: { value: undefined }, shared: false, parent: undefined, slot: "value" };
2264
+ root.target["value"] = visit(value, root, "value", 0, 0, -1, false);
2265
+ const safe = this.checked(parts, kinds, scan);
2266
+ // Structure first, then the leaves inside it and typed binary, then nested JSON from the
2267
+ // innermost out.
2268
+ // A slot replaced by a toJSON result walked there takes the copy that result already got,
2269
+ // never the original.
2270
+ for (const [frame, slot, next] of replaced) {
2271
+ const child = frame.children?.get(slot);
2272
+ put(writable(frame), slot, child && child.target === next && child.copy ? child.copy : next);
846
2273
  }
847
- return settled;
848
- }
849
- /** A container larger than the node budget is scanned as serialized text, Error fields
850
- * included: kept unchanged when nothing registered appears, otherwise withheld whole. */
851
- serialized(item, scan, level) {
852
- let text;
853
- try {
854
- text = JSON.stringify(item, (_key, value) => value instanceof Error
855
- ? Object.fromEntries(Object.getOwnPropertyNames(value).map((name) => [name, Reflect.get(value, name)]))
856
- : value);
857
- }
858
- catch {
859
- // A cycle or bigint cannot be serialized, so the container is withheld.
2274
+ for (const frame of copied)
2275
+ writable(frame);
2276
+ let marker;
2277
+ for (let index = 0; index < parts.length; index++) {
2278
+ const next = safe[index];
2279
+ const kind = kinds[index];
2280
+ if (next === parts[index]) {
2281
+ if (kind === BINARY && next.length >= MEMO_UNITS)
2282
+ this.memo.set(next, scan.payload);
2283
+ continue;
2284
+ }
2285
+ const holder = writable(frames[index]);
2286
+ // Declared binary is withheld as the encoded marker, itself checked; a typed number on an
2287
+ // error is dropped rather than turned into text.
2288
+ if (kind === BINARY) {
2289
+ const encoded = btoa(REDACTED);
2290
+ marker ??= this.checked([encoded], [BINARY], scan)[0] === encoded ? encoded : "";
2291
+ }
2292
+ put(holder, slots[index], kind === BINARY ? marker : kind === NUMBER && holder instanceof Error ? undefined : next);
2293
+ touch(owners[index]);
860
2294
  }
861
- if (text === undefined || text.length >= this.floor(scan))
862
- return REDACTED;
863
- const found = this.find(text, scan, true, level);
864
- return found && !found.spans.length && !(scan.payload && found.short) ? item : REDACTED;
865
- }
866
- /** Skill supporting files may contain arbitrary bytes, including echoes. */
867
- blob(value, scan) {
868
- let binary;
869
- try {
870
- binary = atob(value);
2295
+ for (const [frame, slot, owner, start, end] of binaries) {
2296
+ let clean = true;
2297
+ for (let index = start; clean && index < end; index++)
2298
+ clean = safe[index] === parts[index];
2299
+ if (clean)
2300
+ continue;
2301
+ put(writable(frame), slot, REDACTED);
2302
+ touch(owner);
871
2303
  }
872
- catch {
873
- return this.string(value, scan, false);
874
- }
875
- // Byte-valued code units match ASCII forms directly. Bytes are data, never prose.
876
- let redacted = this.string(binary, scan, false);
877
- if (this.unicode && redacted !== REDACTED) {
878
- // A UTF-8 view also detects non-ASCII and mixed escaped echoes. Never re-encode that
879
- // view: an arbitrary supporting file may contain invalid UTF-8 or a leading BOM.
880
- const view = new TextDecoder("utf-8", { fatal: false, ignoreBOM: true }).decode(Uint8Array.from(redacted, (char) => char.charCodeAt(0)));
881
- if (this.string(view, scan, false) !== view)
882
- redacted = REDACTED;
883
- }
884
- const encoded = redacted === binary ? value : btoa(redacted);
885
- // A file's encoding can itself equal a secret. Withhold that file as
886
- // a valid encoded placeholder, never insert prose into its base64 field.
887
- if (this.string(encoded, scan, false) === encoded)
888
- return encoded;
889
- const withheld = btoa(REDACTED);
890
- return this.string(withheld, scan, false) === withheld ? withheld : "";
891
- }
892
- /** Match across blocks before isError/JSON unwrapping concatenates them. */
893
- joinedContent(value, scan, depth, level) {
894
- if (!Array.isArray(value))
895
- return value;
896
- const blocks = value.filter((block) => block?.type === "text" && typeof block.text === "string");
897
- if (blocks.length < 2)
898
- return value;
899
- for (const separator of ["", "\n"]) {
900
- const joined = blocks.map((block) => block.text).join(separator);
901
- const redacted = this.wire(joined, scan, depth, true, level);
902
- if (redacted === blocks.map((block) => this.wire(block.text, scan, depth, true, level)).join(separator))
2304
+ for (let index = nested.length - 1; index >= 0; index--) {
2305
+ const entry = nested[index];
2306
+ if (!entry.crossing && !entry.dirty)
903
2307
  continue;
904
- let first = true;
905
- return value.map((block) => {
906
- if (!blocks.includes(block))
907
- return block;
908
- const text = first ? redacted : "";
909
- first = false;
910
- return { ...block, text };
911
- });
2308
+ put(writable(entry.frame), entry.slot, entry.crossing ? REDACTED : JSON.stringify(entry.json));
2309
+ touch(entry.owner);
912
2310
  }
913
- return value;
2311
+ for (const [copy, slot, name] of named)
2312
+ if (copy[slot] !== name)
2313
+ put(copy, slot, REDACTED);
2314
+ return { value: root.target["value"], changed };
914
2315
  }
915
2316
  }
916
2317
  export function sentSecretsFor(ctx) {
917
- let secrets = contexts.get(ctx);
918
- if (!secrets) {
919
- secrets = new SentSecrets();
920
- contexts.set(ctx, secrets);
921
- }
922
- sentSecretsForRequest(ctx.requestScope ?? ctx).include(secrets);
923
- return secrets;
2318
+ return viewSecrets(ctx);
924
2319
  }
925
2320
  /** One identity per upstream request, including all discovery and call work. */
926
2321
  export function sentSecretsForRequest(scope, secrets) {
927
- let request = requests.get(scope);
928
- if (!request) {
929
- request = secrets ?? new SentSecrets();
930
- requests.set(scope, request);
931
- }
932
- else if (secrets) {
2322
+ const retained = requestOwner(scope).retained;
2323
+ const request = (retained.secrets ??= secrets ?? new SentSecrets());
2324
+ if (secrets)
933
2325
  request.include(secrets);
934
- }
935
2326
  return request;
936
2327
  }
937
2328
  /** The agent-facing choke point. Apply to values before bridge serialization
938
2329
  * and to serialized wire text, so JSON escapes and structured strings share
939
2330
  * the same rule. Payloads were redacted where they entered; this boundary
940
- * repeats credential and literal private-value replacement around Connecta's
941
- * own envelopes, whose identifiers and counts are never private data. */
2331
+ * repeats every check but short private values around Connecta's own
2332
+ * envelopes, whose identifiers and counts are never private data. */
942
2333
  export function redactAgentOutput(secrets, value) {
943
2334
  return secrets.redact(value, "envelope");
944
2335
  }
@@ -948,50 +2339,62 @@ export function redactAgentOutput(secrets, value) {
948
2339
  export function agentOutputOperations(create, requestScope) {
949
2340
  return Object.fromEntries(Object.keys(create(requestScope ?? {})).map((name) => [
950
2341
  name,
951
- async (...args) => {
2342
+ authMethod((...args) => Effect.suspend(() => {
952
2343
  const scope = requestScope ?? {};
953
2344
  const secrets = sentSecretsForRequest(scope);
954
- try {
955
- return redactAgentOutput(secrets, await create(scope)[name](...args));
956
- }
957
- catch (error) {
958
- throw redactAgentOutput(secrets, error);
959
- }
960
- },
2345
+ const owner = requestOwner(scope);
2346
+ if (!owner.open)
2347
+ return Effect.fail(owner.controller.signal.reason);
2348
+ return authCall(create(scope)[name], args).pipe(Effect.provideService(RequestScope, requestOwner(scope)), Effect.map((value) => redactAgentOutput(secrets, value)), Effect.mapError((error) => redactAgentOutput(secrets, error)));
2349
+ })),
961
2350
  ]));
962
2351
  }
963
- /** Slot reads cover custom handlers too, including keys put in query strings. */
2352
+ /** Slot reads cover custom handlers too, including keys put in query strings. A field named
2353
+ * as an identifier (an application or client ID, a username, a public key) is matched whole. */
2354
+ export function registerCredentialValues(secrets, values) {
2355
+ for (const [field, value] of Object.entries(values ?? {})) {
2356
+ if (explicitSecretName.test(field))
2357
+ secrets.secret(value);
2358
+ else if (identifierName.test(field))
2359
+ secrets.identifier(value);
2360
+ else
2361
+ secrets.add(value);
2362
+ }
2363
+ }
964
2364
  export function trackCredentialReads(ctx) {
965
- if (!ctx.credential || wrappedCredentials.has(ctx))
966
- return;
967
- wrappedCredentials.add(ctx);
968
- const credential = ctx.credential;
969
- ctx.credential = {
970
- async get(field) {
971
- const value = await credential.get(field);
972
- if (value) {
973
- const secrets = sentSecretsFor(ctx);
974
- if (field && explicitSecretName.test(field))
975
- secrets.secret(value);
976
- else
977
- secrets.add(value);
978
- }
979
- return value;
980
- },
981
- async getAll() {
982
- const values = await credential.getAll();
983
- for (const [field, value] of Object.entries(values ?? {})) {
984
- if (explicitSecretName.test(field))
985
- sentSecretsFor(ctx).secret(value);
986
- else
987
- sentSecretsFor(ctx).add(value);
988
- }
989
- return values;
990
- },
991
- };
2365
+ wrapViewCredentials(ctx);
992
2366
  }
2367
+ /** A connector's result or error as it enters: every connector's credentials and the
2368
+ * request's private arguments, under payload rules. */
993
2369
  export function redactSentSecrets(ctx, value) {
994
- return sentSecretsFor(ctx).redact(value);
2370
+ return requestSentSecrets(ctx).redact(value);
2371
+ }
2372
+ /** Everything the context's request sent: every connector's credentials and the
2373
+ * request's private arguments, which a context's own set does not hold. */
2374
+ export function requestSentSecrets(ctx) {
2375
+ sentSecretsFor(ctx);
2376
+ return sentSecretsForRequest(ctx.requestScope ?? ctx);
2377
+ }
2378
+ /** Downstream text, or a parsed body, about to be cut short, trimmed, normalized or
2379
+ * extracted from. Redact it whole first: a cut through an echoed form can leave a remnant
2380
+ * shorter than any fragment the output boundary matches, and a normalized number or
2381
+ * extracted substring may no longer hold the form at all. */
2382
+ export function redactBeforeCut(ctx, value) {
2383
+ return requestSentSecrets(ctx).redact(value);
2384
+ }
2385
+ /** Base64, or base64url with `url`, of bytes or UTF-8 text Connecta returns to an agent: a
2386
+ * download, an attachment, a REST body, a page cursor. Every such encoding goes through here,
2387
+ * since no later pass reads through an encoding the output does not declare: the content is
2388
+ * checked first with the request's set, and content holding a match encodes the placeholder.
2389
+ * A cursor passes `claims`: connecta's own counts and fingerprints beside downstream values it
2390
+ * already redacted (`redactSentSecrets`), checked under the boundary's rules. */
2391
+ export function encodeForAgent(ctx, content, options) {
2392
+ return requestSentSecrets(ctx).encode(typeof content === "string" ? encoder.encode(content) : content, options);
2393
+ }
2394
+ /** Downstream text read only up to a bound, as a field an agent reads: whole forms are
2395
+ * replaced, and so is a tail that begins one the read stopped inside. */
2396
+ export function redactBoundedRead(ctx, text) {
2397
+ return requestSentSecrets(ctx).redactOpenTail(text);
995
2398
  }
996
2399
  /** Sanitize listing facts before any cache or consumer receives them. A name
997
2400
  * cannot be rewritten without changing dispatch, and dropping one entry would