@zackbart/connecta 0.28.1 → 0.30.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 (332) hide show
  1. package/CHANGELOG.md +855 -5
  2. package/README.md +125 -116
  3. package/bin/connecta.mjs +121 -139
  4. package/bin/meta-tool-names.d.mts +2 -0
  5. package/bin/meta-tool-names.mjs +9 -0
  6. package/bin/version.mjs +3 -0
  7. package/dist/access-tokens.js +53 -63
  8. package/dist/activity-request.js +8 -0
  9. package/dist/activity.d.ts +76 -42
  10. package/dist/activity.js +110 -42
  11. package/dist/argument-redaction.js +230 -0
  12. package/dist/auth/clerk-sdk/client.d.ts +65 -0
  13. package/dist/auth/clerk-sdk/client.js +5 -0
  14. package/dist/auth/clerk-sdk/generated.js +1845 -0
  15. package/dist/auth/clerk-transport.js +19 -0
  16. package/dist/auth/clerk.d.ts +17 -7
  17. package/dist/auth/clerk.js +189 -107
  18. package/dist/auth/cloudflare-access.js +25 -26
  19. package/dist/auth/downstream-client-metadata.d.ts +29 -0
  20. package/dist/auth/downstream-client-metadata.js +133 -0
  21. package/dist/auth/downstream-oauth.js +1584 -1580
  22. package/dist/auth/downstream-revocation.js +106 -0
  23. package/dist/auth/oauth-token-response.js +231 -0
  24. package/dist/auth/oauth-v2-migration.js +116 -0
  25. package/dist/auth/static-oauth.js +93 -71
  26. package/dist/auth-elicitation.js +288 -0
  27. package/dist/authorization-handoff.js +111 -0
  28. package/dist/branding.d.ts +8 -16
  29. package/dist/branding.js +26 -59
  30. package/dist/byte-read-response.js +39 -0
  31. package/dist/call-admission.js +12 -16
  32. package/dist/catalog-cache.js +739 -0
  33. package/dist/catalog-drift.d.ts +50 -15
  34. package/dist/catalog-drift.js +365 -128
  35. package/dist/catalog-fingerprint.js +2 -4
  36. package/dist/catalog-freshness.js +13 -0
  37. package/dist/catalog-limits.js +2 -2
  38. package/dist/catalog-service.js +409 -225
  39. package/dist/catalog.js +43 -84
  40. package/dist/concurrency.js +3 -4
  41. package/dist/config-defaults.d.ts +39 -0
  42. package/dist/config-defaults.js +46 -0
  43. package/dist/config-schema.d.ts +181 -0
  44. package/dist/config-schema.js +435 -0
  45. package/dist/config-value-sources.js +102 -0
  46. package/dist/config.d.ts +633 -0
  47. package/dist/config.js +508 -0
  48. package/dist/connector-access.d.ts +1 -0
  49. package/dist/connector-access.js +13 -15
  50. package/dist/connector-caller.d.ts +8 -0
  51. package/dist/connector-caller.js +15 -6
  52. package/dist/connector-scope.js +1 -1
  53. package/dist/connectors/api-connector.d.ts +59 -7
  54. package/dist/connectors/api-connector.js +106 -19
  55. package/dist/connectors/api.js +4 -1
  56. package/dist/connectors/guarded-fetch.d.ts +144 -0
  57. package/dist/connectors/guarded-fetch.js +87 -38
  58. package/dist/connectors/negotiation-cache.js +46 -0
  59. package/dist/connectors/option-shapes.js +27 -0
  60. package/dist/connectors/remote-mcp.d.ts +30 -4
  61. package/dist/connectors/remote-mcp.js +1774 -320
  62. package/dist/connectors/resource-uri.js +335 -0
  63. package/dist/credential-contract.d.ts +10 -0
  64. package/dist/credential-rules.js +1 -3
  65. package/dist/credential-url.js +108 -0
  66. package/dist/credentials.d.ts +6 -1
  67. package/dist/credentials.js +27 -18
  68. package/dist/d1.d.ts +130 -0
  69. package/dist/d1.js +290 -0
  70. package/dist/describe-config.d.ts +165 -0
  71. package/dist/describe-config.js +315 -0
  72. package/dist/described.js +106 -0
  73. package/dist/downstream-input-context.js +128 -0
  74. package/dist/downstream-input.js +325 -0
  75. package/dist/errors.d.ts +132 -31
  76. package/dist/errors.js +175 -87
  77. package/dist/execute.js +798 -318
  78. package/dist/executor-admission.d.ts +1 -0
  79. package/dist/executor-admission.js +11 -15
  80. package/dist/executor-contract.d.ts +20 -0
  81. package/dist/executor-contract.js +48 -0
  82. package/dist/executor-result.js +30 -13
  83. package/dist/executors/quickjs-child.js +15 -9
  84. package/dist/executors/quickjs-protocol.js +5 -6
  85. package/dist/executors/quickjs-runtime.js +152 -59
  86. package/dist/executors/quickjs.js +49 -39
  87. package/dist/favicon.js +1 -2
  88. package/dist/fix-prompt.js +1 -3
  89. package/dist/guest-runtime.js +238 -0
  90. package/dist/guest-types.d.ts +218 -0
  91. package/dist/guest-types.js +1 -0
  92. package/dist/html-security.js +24 -0
  93. package/dist/identity.js +1 -3
  94. package/dist/inbound-credential.js +52 -0
  95. package/dist/index.d.ts +24 -256
  96. package/dist/index.js +65 -478
  97. package/dist/invocation-auth.js +69 -0
  98. package/dist/invocation.js +262 -79
  99. package/dist/json-schema.d.ts +1 -1
  100. package/dist/mcp-client-context.js +35 -0
  101. package/dist/mcp-result.js +11 -7
  102. package/dist/meta-output.js +61 -0
  103. package/dist/meta-tool-names.d.ts +2 -0
  104. package/dist/meta-tool-names.js +3 -0
  105. package/dist/meta-tools.js +419 -272
  106. package/dist/module-contracts.d.ts +8 -23
  107. package/dist/node.d.ts +0 -2
  108. package/dist/node.js +2 -2
  109. package/dist/oauth-callback-outcome.js +1 -3
  110. package/dist/oauth-handoff.js +194 -0
  111. package/dist/oauth-sealing.js +20 -0
  112. package/dist/operator-record.d.ts +137 -0
  113. package/dist/operator-record.js +420 -0
  114. package/dist/operator-ui/assets.js +31 -0
  115. package/dist/operator-ui/contract.d.ts +72 -0
  116. package/dist/operator-ui/contract.js +1 -0
  117. package/dist/operator-ui/fix-prompts.d.ts +18 -0
  118. package/dist/operator-ui/generated.js +5 -4
  119. package/dist/operator-ui/model.d.ts +12 -87
  120. package/dist/operator-ui/model.js +2 -9
  121. package/dist/operator-ui/setup-commands.js +1 -3
  122. package/dist/operator-ui/view.d.ts +323 -0
  123. package/dist/operator-ui/view.js +62 -189
  124. package/dist/page-styles.js +2 -3
  125. package/dist/program-result.js +5 -0
  126. package/dist/program-source.js +14 -2
  127. package/dist/{exempt-writes.js → program-writes.js} +60 -19
  128. package/dist/provider.d.ts +142 -0
  129. package/dist/provider.js +187 -0
  130. package/dist/providers/_shared/google/result-size.js +57 -0
  131. package/dist/providers/_shared/google/service-account.d.ts +58 -0
  132. package/dist/providers/_shared/google/service-account.js +500 -0
  133. package/dist/providers/_shared/google/workspace.d.ts +276 -0
  134. package/dist/providers/_shared/google/workspace.js +518 -0
  135. package/dist/providers/_shared/request-token.js +83 -0
  136. package/dist/providers/{basecamp.d.ts → basecamp/index.d.ts} +6 -28
  137. package/dist/providers/basecamp/index.js +997 -0
  138. package/dist/providers/basecamp/skill.generated.js +9 -0
  139. package/dist/providers/{breeze.d.ts → breeze/index.d.ts} +2 -2
  140. package/dist/providers/{breeze.js → breeze/index.js} +184 -140
  141. package/dist/providers/breeze/skill.generated.js +9 -0
  142. package/dist/providers/{ccb.d.ts → ccb/index.d.ts} +2 -2
  143. package/dist/providers/{ccb.js → ccb/index.js} +271 -116
  144. package/dist/providers/ccb/skill.generated.js +9 -0
  145. package/dist/providers/{cloudflare.d.ts → cloudflare/index.d.ts} +8 -7
  146. package/dist/providers/{cloudflare.js → cloudflare/index.js} +171 -738
  147. package/dist/providers/cloudflare/skill.generated.js +10 -0
  148. package/dist/providers/docs/index.d.ts +54 -0
  149. package/dist/providers/docs/index.js +1237 -0
  150. package/dist/providers/docs/skill.generated.js +9 -0
  151. package/dist/providers/drive/index.d.ts +53 -0
  152. package/dist/providers/drive/index.js +1523 -0
  153. package/dist/providers/drive/skill.generated.js +9 -0
  154. package/dist/providers/forms/index.d.ts +54 -0
  155. package/dist/providers/forms/index.js +1217 -0
  156. package/dist/providers/forms/skill.generated.js +11 -0
  157. package/dist/providers/github/auth.d.ts +32 -0
  158. package/dist/providers/github/auth.js +361 -0
  159. package/dist/providers/github/hosted.js +218 -0
  160. package/dist/providers/github/index.d.ts +13 -0
  161. package/dist/providers/github/index.js +140 -0
  162. package/dist/providers/github/key.js +100 -0
  163. package/dist/providers/github/rest.js +321 -0
  164. package/dist/providers/github/scope.d.ts +29 -0
  165. package/dist/providers/github/scope.js +124 -0
  166. package/dist/providers/github/skill.generated.js +8 -0
  167. package/dist/providers/gmail/index.d.ts +51 -0
  168. package/dist/providers/gmail/index.js +1436 -0
  169. package/dist/providers/gmail/skill.generated.js +9 -0
  170. package/dist/providers/infisical/index.d.ts +7 -0
  171. package/dist/providers/infisical/index.js +713 -0
  172. package/dist/providers/infisical/skill.generated.js +8 -0
  173. package/dist/providers/{linear.d.ts → linear/index.d.ts} +11 -14
  174. package/dist/providers/linear/index.js +352 -0
  175. package/dist/providers/linear/skill.generated.js +8 -0
  176. package/dist/providers/{mixpanel.d.ts → mixpanel/index.d.ts} +5 -10
  177. package/dist/providers/mixpanel/index.js +416 -0
  178. package/dist/providers/mixpanel/skill.generated.js +9 -0
  179. package/dist/providers/{notion.d.ts → notion/index.d.ts} +8 -8
  180. package/dist/providers/{notion.js → notion/index.js} +226 -239
  181. package/dist/providers/notion/skill.generated.js +10 -0
  182. package/dist/providers/{overflow.d.ts → overflow/index.d.ts} +2 -2
  183. package/dist/providers/{overflow.js → overflow/index.js} +74 -111
  184. package/dist/providers/overflow/skill.generated.js +8 -0
  185. package/dist/providers/{planning-center.d.ts → planning-center/index.d.ts} +2 -2
  186. package/dist/providers/{planning-center.js → planning-center/index.js} +264 -104
  187. package/dist/providers/planning-center/skill.generated.js +10 -0
  188. package/dist/providers/{revenuecat.d.ts → revenuecat/index.d.ts} +5 -10
  189. package/dist/providers/revenuecat/index.js +557 -0
  190. package/dist/providers/revenuecat/skill.generated.js +11 -0
  191. package/dist/providers/sheets/index.d.ts +54 -0
  192. package/dist/providers/sheets/index.js +1237 -0
  193. package/dist/providers/sheets/skill.generated.js +10 -0
  194. package/dist/providers/slides/index.d.ts +53 -0
  195. package/dist/providers/slides/index.js +2645 -0
  196. package/dist/providers/slides/skill.generated.js +9 -0
  197. package/dist/providers/{stripe.d.ts → stripe/index.d.ts} +5 -12
  198. package/dist/providers/stripe/index.js +266 -0
  199. package/dist/providers/stripe/skill.generated.js +11 -0
  200. package/dist/providers/{tithely.d.ts → tithely/index.d.ts} +2 -2
  201. package/dist/providers/{tithely.js → tithely/index.js} +60 -91
  202. package/dist/providers/tithely/skill.generated.js +8 -0
  203. package/dist/providers/{vercel.d.ts → vercel/index.d.ts} +8 -8
  204. package/dist/providers/vercel/index.js +1038 -0
  205. package/dist/providers/vercel/mcp-ownership.js +212 -0
  206. package/dist/providers/vercel/skill.generated.js +11 -0
  207. package/dist/registry.d.ts +110 -171
  208. package/dist/registry.js +588 -927
  209. package/dist/request-state.js +21 -0
  210. package/dist/resource-read.js +103 -0
  211. package/dist/result-shapes.js +17 -49
  212. package/dist/retired-toolkits.js +1 -1
  213. package/dist/routes/access-tokens.js +3 -6
  214. package/dist/routes/activity-context.js +21 -0
  215. package/dist/routes/activity-disclosure.js +40 -0
  216. package/dist/routes/activity.js +59 -21
  217. package/dist/routes/actor-labels.js +3 -50
  218. package/dist/routes/connect.js +221 -0
  219. package/dist/routes/credentials.js +15 -27
  220. package/dist/routes/mcp.js +365 -130
  221. package/dist/routes/oauth-client-metadata.js +15 -0
  222. package/dist/routes/oauth-management.js +45 -107
  223. package/dist/routes/oauth.js +110 -57
  224. package/dist/routes/operator.js +3 -3
  225. package/dist/routes/shared.d.ts +25 -64
  226. package/dist/routes/shared.js +160 -80
  227. package/dist/routes/skills.js +30 -0
  228. package/dist/routes/ui-config.js +225 -0
  229. package/dist/routes/ui-data.js +40 -35
  230. package/dist/routes/ui.js +66 -56
  231. package/dist/runtime/admission.js +4 -2
  232. package/dist/runtime/call-admission.js +3 -1
  233. package/dist/runtime/connector-scope.js +1 -4
  234. package/dist/runtime/run.js +10 -39
  235. package/dist/runtime/services.js +12 -117
  236. package/dist/runtime/shared-read.js +1 -3
  237. package/dist/runtime/storage.js +8 -1
  238. package/dist/sent-secrets.d.ts +93 -0
  239. package/dist/sent-secrets.js +1013 -0
  240. package/dist/server.js +28 -71
  241. package/dist/skills.js +416 -174
  242. package/dist/sqlite.d.ts +48 -0
  243. package/dist/sqlite.js +181 -0
  244. package/dist/storage/keys.js +443 -0
  245. package/dist/storage/memory.js +16 -4
  246. package/dist/storage/sql.d.ts +66 -0
  247. package/dist/storage/sql.js +543 -0
  248. package/dist/timeout.js +4 -2
  249. package/dist/tool-name.js +17 -0
  250. package/dist/tool-safety.d.ts +9 -35
  251. package/dist/tool-safety.js +20 -40
  252. package/dist/types.d.ts +336 -56
  253. package/dist/types.js +0 -1
  254. package/dist/ui.d.ts +7 -19
  255. package/dist/ui.js +76 -90
  256. package/dist/url-safety.js +3 -6
  257. package/dist/usage-guide.js +146 -0
  258. package/dist/validate.d.ts +7 -0
  259. package/dist/validate.js +309 -47
  260. package/dist/version.d.ts +1 -7
  261. package/dist/version.js +3 -7
  262. package/dist/worker.d.ts +11 -0
  263. package/dist/worker.js +435 -0
  264. package/documentation/README.md +63 -0
  265. package/documentation/architecture.md +696 -231
  266. package/documentation/auth.md +1069 -546
  267. package/documentation/code-mode.md +662 -356
  268. package/documentation/deploying.md +146 -0
  269. package/documentation/integrating.md +159 -0
  270. package/documentation/meta-tools.md +570 -216
  271. package/documentation/operating.md +135 -0
  272. package/documentation/operator-tests.md +97 -0
  273. package/documentation/operator-ui.md +132 -0
  274. package/documentation/provider-migration-0.29.md +65 -0
  275. package/examples/worker/AGENTS.md +34 -12
  276. package/examples/worker/README.md +293 -255
  277. package/examples/worker/kv-to-d1.wrangler.jsonc +26 -0
  278. package/examples/worker/scripts/copy-kv-to-d1.mjs +162 -0
  279. package/examples/worker/src/connecta.config.ts +151 -0
  280. package/examples/worker/src/index.ts +14 -197
  281. package/examples/worker/wrangler.jsonc +15 -35
  282. package/package.json +126 -54
  283. package/templates/node/.dockerignore +1 -2
  284. package/templates/node/.env.example +20 -21
  285. package/templates/node/AGENTS.md +21 -17
  286. package/templates/node/Dockerfile +3 -5
  287. package/templates/node/README.md +140 -58
  288. package/templates/node/docker-compose.yml +11 -12
  289. package/templates/node/package.json +4 -2
  290. package/templates/node/src/connecta.config.ts +119 -0
  291. package/templates/node/src/index.ts +9 -154
  292. package/templates/node/src/provision-token.ts +17 -0
  293. package/AGENTS.md +0 -170
  294. package/dist/artifacts/connector.d.ts +0 -58
  295. package/dist/artifacts/connector.js +0 -730
  296. package/dist/artifacts/document.js +0 -86
  297. package/dist/artifacts/guide.js +0 -140
  298. package/dist/artifacts/html-scan.js +0 -329
  299. package/dist/artifacts/json.js +0 -78
  300. package/dist/artifacts/kv-store.d.ts +0 -20
  301. package/dist/artifacts/kv-store.js +0 -140
  302. package/dist/artifacts/markdown.js +0 -478
  303. package/dist/artifacts/module.d.ts +0 -37
  304. package/dist/artifacts/module.js +0 -66
  305. package/dist/artifacts/operations.d.ts +0 -197
  306. package/dist/artifacts/operations.js +0 -1002
  307. package/dist/artifacts/refresh.d.ts +0 -55
  308. package/dist/artifacts/refresh.js +0 -236
  309. package/dist/artifacts/routes.js +0 -206
  310. package/dist/artifacts/types.d.ts +0 -214
  311. package/dist/artifacts/types.js +0 -22
  312. package/dist/artifacts/validate.d.ts +0 -62
  313. package/dist/artifacts/validate.js +0 -651
  314. package/dist/artifacts.d.ts +0 -6
  315. package/dist/artifacts.js +0 -9
  316. package/dist/auth/bearer.d.ts +0 -11
  317. package/dist/auth/bearer.js +0 -50
  318. package/dist/providers/basecamp.js +0 -445
  319. package/dist/providers/linear.js +0 -217
  320. package/dist/providers/mixpanel.js +0 -247
  321. package/dist/providers/revenuecat.js +0 -317
  322. package/dist/providers/stripe.js +0 -253
  323. package/dist/providers/vercel.js +0 -1253
  324. package/dist/storage/file.d.ts +0 -15
  325. package/dist/storage/file.js +0 -455
  326. package/ethos.md +0 -93
  327. package/examples/worker/src/cloudflare-kv.ts +0 -43
  328. package/examples/worker/src/d1-activity-row.ts +0 -155
  329. package/examples/worker/src/d1-activity.ts +0 -147
  330. package/examples/worker/src/d1-storage.ts +0 -138
  331. package/examples/worker/src/r2-artifact-blobs.ts +0 -29
  332. package/templates/node/src/file-activity.ts +0 -182
@@ -0,0 +1,1013 @@
1
+ // Credentials and submitted private argument values used by one upstream request. Memory
2
+ // 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 { credentialUrlViews } from "./credential-url.js";
6
+ import { visitPrivateCallArguments } from "./argument-redaction.js";
7
+ const REDACTED = "[redacted]";
8
+ const encoder = new TextEncoder();
9
+ const contexts = new WeakMap();
10
+ const requests = new WeakMap();
11
+ const wrappedCredentials = new WeakSet();
12
+ const sensitiveName = /key|token|secret|password|auth|signature|session/i;
13
+ const explicitSecretName = /secret|password/i;
14
+ 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
+ // Short credentials can match protocol fields or legitimate configuration.
16
+ 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
+ // Submitted values register at most this many form code units and argument nodes. A form
23
+ // that does not fit withholds every string long enough to contain it.
24
+ const MAX_PRIVATE_FORM_CHARS = 524_288;
25
+ const MAX_ARGUMENT_NODES = 4_096;
26
+ const MAX_ARGUMENT_DEPTH = 32;
27
+ // The matcher indexes each form's first FORM_PREFIX units, credentials first, up to
28
+ // MAX_MATCHER_UNITS for every registration together. A longer form is confirmed by a rolling
29
+ // hash once its prefix matches. A form left out withholds every string long enough to contain it.
30
+ // Forms sharing a prefix and a length share each check, and overlapping prefix occurrences in
31
+ // self-similar text share one run of checks. A scanned view holds at most MAX_PENDING_RUNS, and
32
+ // each check, which costs about as much as scanning two code units, is paid for from the call's
33
+ // work budget before it is queued; a view that needs more is withheld.
34
+ const FORM_PREFIX = 256;
35
+ const MAX_MATCHER_UNITS = 524_288;
36
+ const MAX_PENDING_RUNS = 16_384;
37
+ const CHECK_WORK = 4;
38
+ const HASH_BASE = 0x01_00_01_93;
39
+ const POWERS = new Int32Array(FORM_PREFIX + 1);
40
+ POWERS[0] = 1;
41
+ for (let length = 1; length <= FORM_PREFIX; length++)
42
+ POWERS[length] = Math.imul(POWERS[length - 1], HASH_BASE);
43
+ // One redaction call visits at most this many structured entries, this deep. A string is
44
+ // decoded at most MAX_DECODE_LEVELS JSON layers, counting nested JSON strings it was parsed from.
45
+ const MAX_WALK_NODES = 1_048_576;
46
+ const MAX_WALK_DEPTH = 512;
47
+ const MAX_DECODE_LEVELS = 8;
48
+ // Work for one redaction call, across parsing and every scanned view. Credential-only sets
49
+ // keep the multi-MiB document and skill-file reads they always allowed.
50
+ const PRIVATE_SCAN_WORK = 64 * 1_048_576;
51
+ 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.
54
+ const FRAMING_KEYS = new Set(("content structuredContent isError _meta resultType dev.connecta/format type text data mimeType resource uri " +
55
+ "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(" "));
57
+ const TAGS = new Set(("type:text type:image type:audio type:resource type:resource_link format:json format:text format:paged " +
58
+ "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"]);
63
+ /** Spend work that fits the call's remaining budget. Work that does not fit is refused
64
+ * without spending, so its field is withheld while smaller fields can still be checked. */
65
+ function spend(scan, work) {
66
+ if (work > scan.budget.work)
67
+ return false;
68
+ scan.budget.work -= work;
69
+ return true;
70
+ }
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 });
80
+ }
81
+ /** One payload-free warning per configured connector, regardless of requests. */
82
+ export function shortSecretWarning() {
83
+ let warned = false;
84
+ return (value, logger) => {
85
+ if (warned || !value || value.length >= MIN_SECRET_LENGTH)
86
+ return;
87
+ warned = true;
88
+ const fact = { code: "short_secret_not_redacted" };
89
+ logger.warn("[connecta] Credentials shorter than 8 characters are not redacted from echoes; use longer secrets.", fact);
90
+ };
91
+ }
92
+ /** Percent escapes compare case-insensitively: hex letters within two units after `%` fold
93
+ * to uppercase in forms and scanned text alike. A match can begin inside that pair, so a
94
+ * form's first one or two letters may arrive folded as well. */
95
+ function wireForms(value) {
96
+ const fold = (text) => text.replace(/[a-f]/g, (letter) => letter.toUpperCase());
97
+ const form = value.replace(/%[^%]?[^%]?/g, fold);
98
+ return [...new Set([form, fold(form.slice(0, 1)) + form.slice(1), fold(form.slice(0, 2)) + form.slice(2)])];
99
+ }
100
+ function base64(value) {
101
+ let binary = "";
102
+ for (const byte of encoder.encode(value))
103
+ binary += String.fromCharCode(byte);
104
+ return btoa(binary);
105
+ }
106
+ class Source {
107
+ text;
108
+ start = 0;
109
+ end = 0;
110
+ constructor(text) {
111
+ this.text = text;
112
+ }
113
+ next() {
114
+ if (this.end >= this.text.length)
115
+ return -1;
116
+ this.start = this.end++;
117
+ return this.text.charCodeAt(this.start);
118
+ }
119
+ }
120
+ const shortEscape = { 34: 34, 47: 47, 92: 92, 98: 8, 102: 12, 110: 10, 114: 13, 116: 9 };
121
+ /** A hex digit's value, or -1. */
122
+ function hex(code) {
123
+ const lower = code | 0x20;
124
+ return code >= 0x30 && code <= 0x39 ? code - 0x30 : lower >= 0x61 && lower <= 0x66 ? lower - 0x57 : -1;
125
+ }
126
+ /** One layer of JSON string escapes decoded. It counts the escapes it decoded and the
127
+ * backslashes it produced, which tell whether another layer could decode more. */
128
+ class Unescaped {
129
+ from;
130
+ start = 0;
131
+ end = 0;
132
+ decoded = 0;
133
+ slashes = 0;
134
+ codes = new Int32Array(8);
135
+ starts = new Int32Array(8);
136
+ ends = new Int32Array(8);
137
+ head = 0;
138
+ size = 0;
139
+ constructor(from) {
140
+ this.from = from;
141
+ }
142
+ fill(count) {
143
+ for (; this.size < count; this.size++) {
144
+ const code = this.from.next();
145
+ if (code < 0)
146
+ return false;
147
+ const at = (this.head + this.size) & 7;
148
+ this.codes[at] = code;
149
+ this.starts[at] = this.from.start;
150
+ this.ends[at] = this.from.end;
151
+ }
152
+ return true;
153
+ }
154
+ at(offset) {
155
+ return this.codes[(this.head + offset) & 7];
156
+ }
157
+ take(count, code) {
158
+ this.start = this.starts[this.head];
159
+ this.end = this.ends[(this.head + count - 1) & 7];
160
+ this.head = (this.head + count) & 7;
161
+ this.size -= count;
162
+ if (count > 1)
163
+ this.decoded++;
164
+ if (code === 0x5c)
165
+ this.slashes++;
166
+ return code;
167
+ }
168
+ next() {
169
+ if (!this.fill(1))
170
+ return -1;
171
+ const code = this.at(0);
172
+ if (code === 0x5c && this.fill(2)) {
173
+ const escaped = shortEscape[this.at(1)];
174
+ if (escaped !== undefined)
175
+ return this.take(2, escaped);
176
+ if (this.at(1) === 0x75 && this.fill(6)) {
177
+ let unit = 0;
178
+ for (let offset = 2; offset < 6 && unit >= 0; offset++) {
179
+ const digit = hex(this.at(offset));
180
+ unit = digit < 0 ? -1 : unit * 16 + digit;
181
+ }
182
+ if (unit >= 0)
183
+ return this.take(6, unit);
184
+ }
185
+ }
186
+ return this.take(1, code);
187
+ }
188
+ }
189
+ /** Merge one view's spans as they arrive, ends ascending. A span wholly inside an emitted
190
+ * placeholder is that placeholder; a private value containing its text spans more. */
191
+ function push(spans, base, start, end, marks) {
192
+ if (marks) {
193
+ let low = 0;
194
+ let high = marks.length;
195
+ while (low < high) {
196
+ const middle = (low + high) >>> 1;
197
+ if (marks[middle] <= start)
198
+ low = middle + 1;
199
+ else
200
+ high = middle;
201
+ }
202
+ if (low && end <= marks[low - 1] + REDACTED.length)
203
+ return;
204
+ }
205
+ while (spans.length > base && spans[spans.length - 1] >= start) {
206
+ start = Math.min(start, spans[spans.length - 2]);
207
+ spans.length -= 2;
208
+ }
209
+ spans.push(start, end);
210
+ }
211
+ function markers(text) {
212
+ let at = text.indexOf(REDACTED);
213
+ if (at < 0)
214
+ return undefined;
215
+ const marks = [];
216
+ for (; at >= 0; at = text.indexOf(REDACTED, at + REDACTED.length))
217
+ marks.push(at);
218
+ return marks;
219
+ }
220
+ /** Replace each merged span once, from the original text: no replacement can split another
221
+ * view's match. Header-line scrubbing belongs to an actual echo. */
222
+ function replace(text, spans) {
223
+ const order = Array.from({ length: spans.length / 2 }, (_, index) => index * 2).sort((a, b) => spans[a] - spans[b]);
224
+ const parts = [];
225
+ 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;
233
+ }
234
+ parts.push(text.slice(cursor));
235
+ return parts.join("").replace(headerLine, `$1$2${REDACTED}`);
236
+ }
237
+ /** A run of one occurrence, whose step is set by the next occurrence that extends it. */
238
+ const UNEXTENDED = { step: 0, stride: 0, stepHash: 0, stepPower: 0 };
239
+ /** Runs ordered by their head check's due count: a binary min-heap. */
240
+ class Runs {
241
+ items = [];
242
+ add(run) {
243
+ let at = this.items.length;
244
+ for (let parent = (at - 1) >> 1; at && this.items[parent].due > run.due; parent = (at - 1) >> 1) {
245
+ this.items[at] = this.items[parent];
246
+ at = parent;
247
+ }
248
+ this.items[at] = run;
249
+ }
250
+ /** Restore the order after the earliest run's head advanced, dropping it once empty. */
251
+ settle() {
252
+ // An empty run leaves, and the last run sifts down from the root in its place.
253
+ const run = this.items[0].count ? this.items[0] : this.items.pop();
254
+ const size = this.items.length;
255
+ if (!size)
256
+ return;
257
+ let at = 0;
258
+ for (let child = 1; child < size; child = at * 2 + 1) {
259
+ if (child + 1 < size && this.items[child + 1].due < this.items[child].due)
260
+ child++;
261
+ if (this.items[child].due >= run.due)
262
+ break;
263
+ this.items[at] = this.items[child];
264
+ at = child;
265
+ }
266
+ this.items[at] = run;
267
+ }
268
+ }
269
+ /** A polynomial hash of code units, modulo 2^32. Equal text always hashes equally, so a
270
+ * collision can only redact more. */
271
+ function hash(text) {
272
+ let value = 0;
273
+ for (let index = 0; index < text.length; index++)
274
+ value = (Math.imul(value, HASH_BASE) + text.charCodeAt(index)) | 0;
275
+ return value;
276
+ }
277
+ /** Aho-Corasick over registered forms: amortized constant work per scanned code unit, however
278
+ * many forms are registered, reporting the longest form ending at each unit. Only a bounded
279
+ * prefix of each form is indexed. A longer form is confirmed where its prefix matches by the
280
+ * view's rolling hash once the form's last unit arrives, so no text is read twice; checks are
281
+ * budgeted, and their queue is bounded. States live in typed arrays: the root's transitions in a
282
+ * table, each state's first child inline, and only further branches in a map. */
283
+ class Matcher {
284
+ /** The shortest form left unindexed; any string this long might contain it. */
285
+ unindexed = Infinity;
286
+ root = new Int32Array(65_536);
287
+ branches = new Map();
288
+ child;
289
+ unit;
290
+ depth;
291
+ kind;
292
+ /** Forms longer than FORM_PREFIX, by the state their prefix reaches. */
293
+ truncated = new Map();
294
+ fail;
295
+ longest;
296
+ short;
297
+ starts;
298
+ hashes;
299
+ constructor(forms) {
300
+ // Credentials are indexed first; sorting is stable, so registration order is otherwise kept.
301
+ const indexed = [];
302
+ let units = 0;
303
+ for (const entry of [...forms].sort((a, b) => b[1] - a[1])) {
304
+ const length = Math.min(entry[0].length, FORM_PREFIX);
305
+ if (units + length > MAX_MATCHER_UNITS)
306
+ this.unindexed = Math.min(this.unindexed, entry[0].length);
307
+ else {
308
+ units += length;
309
+ indexed.push(entry);
310
+ }
311
+ }
312
+ const size = units + 1;
313
+ [this.child, this.fail] = [new Int32Array(size), new Int32Array(size)];
314
+ [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)];
316
+ const parent = new Int32Array(size);
317
+ let states = 1;
318
+ let longestForm = 0;
319
+ for (const [form, kind] of indexed) {
320
+ const length = Math.min(form.length, FORM_PREFIX);
321
+ let state = 0;
322
+ for (let index = 0; index < length; index++) {
323
+ const code = form.charCodeAt(index);
324
+ let next = this.next(state, code);
325
+ if (!next) {
326
+ next = states++;
327
+ if (!state)
328
+ this.root[code] = next;
329
+ else if (!this.child[state])
330
+ this.child[state] = next;
331
+ else
332
+ this.branches.set(state * 65_536 + code, next);
333
+ parent[next] = state;
334
+ this.unit[next] = code;
335
+ this.depth[next] = this.depth[state] + 1;
336
+ }
337
+ state = next;
338
+ }
339
+ if (length === form.length)
340
+ this.kind[state] = Math.max(this.kind[state], kind);
341
+ else {
342
+ const groups = this.truncated.get(state) ?? [];
343
+ this.truncated.set(state, groups);
344
+ const group = groups.find((group) => group.length === form.length);
345
+ if (group)
346
+ group.hashes.add(hash(form));
347
+ else {
348
+ let power = 1;
349
+ for (let index = 0; index < form.length; index++)
350
+ power = Math.imul(power, HASH_BASE);
351
+ groups.push({ length: form.length, power, hashes: new Set([hash(form)]) });
352
+ }
353
+ }
354
+ longestForm = Math.max(longestForm, length);
355
+ }
356
+ // Breadth-first, by a counting sort on depth: a failure link and its outputs depend only on
357
+ // shallower states.
358
+ const offsets = new Int32Array(FORM_PREFIX + 2);
359
+ for (let state = 1; state < states; state++)
360
+ offsets[this.depth[state] + 1]++;
361
+ for (let depth = 1; depth < offsets.length; depth++)
362
+ offsets[depth] += offsets[depth - 1];
363
+ const order = new Int32Array(states);
364
+ for (let state = 1; state < states; state++)
365
+ order[offsets[this.depth[state]]++] = state;
366
+ for (let index = 0; index < states - 1; index++) {
367
+ const state = order[index];
368
+ const fail = this.depth[state] > 1 ? this.step(this.fail[parent[state]], this.unit[state]) : 0;
369
+ 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];
372
+ }
373
+ this.starts = new Int32Array(1 << (32 - Math.clz32(longestForm)));
374
+ this.hashes = new Int32Array(this.starts.length);
375
+ }
376
+ /** The child reached by one code unit, or 0. A first child is stored with its unit. */
377
+ next(state, code) {
378
+ if (!state)
379
+ return this.root[code];
380
+ const child = this.child[state];
381
+ return child && this.unit[child] === code ? child : (this.branches.get(state * 65_536 + code) ?? 0);
382
+ }
383
+ step(state, code) {
384
+ for (;;) {
385
+ const next = this.next(state, code);
386
+ if (next || !state)
387
+ return next;
388
+ state = this.fail[state];
389
+ }
390
+ }
391
+ /** Scan one view, mapping each match back to the source units it came from. False when its
392
+ * long-form checks exceed the remaining work or MAX_PENDING_RUNS, so the view is withheld. */
393
+ scan(units, found, budget, marks) {
394
+ let state = 0;
395
+ let count = 0;
396
+ let percent = 0;
397
+ let rolling = 0;
398
+ let runs;
399
+ let tails;
400
+ const base = found.spans.length;
401
+ const mask = this.starts.length - 1;
402
+ for (let code = units.next(); code >= 0; code = units.next()) {
403
+ if (code === 0x25)
404
+ percent = 2;
405
+ else if (percent) {
406
+ percent--;
407
+ if (code >= 0x61 && code <= 0x66)
408
+ code -= 0x20;
409
+ }
410
+ this.starts[count & mask] = units.start;
411
+ this.hashes[count++ & mask] = rolling;
412
+ rolling = (Math.imul(rolling, HASH_BASE) + code) | 0;
413
+ state = this.step(state, code);
414
+ const length = this.longest[state];
415
+ if (length)
416
+ push(found.spans, base, this.starts[(count - length) & mask], units.end, marks);
417
+ if (this.short[state])
418
+ found.short = true;
419
+ const groups = this.depth[state] === FORM_PREFIX ? this.truncated.get(state) : undefined;
420
+ if (groups) {
421
+ if (CHECK_WORK * groups.length > budget.work)
422
+ return false;
423
+ budget.work -= CHECK_WORK * groups.length;
424
+ const before = this.hashes[(count - FORM_PREFIX) & mask];
425
+ const start = this.starts[(count - FORM_PREFIX) & mask];
426
+ runs ??= new Runs();
427
+ tails ??= new Map();
428
+ for (const group of groups) {
429
+ const tail = tails.get(group);
430
+ if (tail?.count && this.extend(tail, count, start))
431
+ continue;
432
+ if (runs.items.length >= MAX_PENDING_RUNS)
433
+ return false;
434
+ const due = count - FORM_PREFIX + group.length;
435
+ const run = { group, count: 1, due, before, start, last: count, lastStart: start, ...UNEXTENDED };
436
+ tails.set(group, run);
437
+ runs.add(run);
438
+ }
439
+ }
440
+ while (runs?.items.length && runs.items[0].due === count) {
441
+ const run = runs.items[0];
442
+ if (run.group.hashes.has((rolling - Math.imul(run.before, run.group.power)) | 0))
443
+ push(found.spans, base, run.start, units.end, marks);
444
+ run.count--;
445
+ run.due += run.step;
446
+ run.before = (Math.imul(run.before, run.stepPower) + run.stepHash) | 0;
447
+ run.start += run.stride;
448
+ runs.settle();
449
+ }
450
+ }
451
+ if (this.kind[state] === SHORT && this.depth[state] === count)
452
+ found.exact = true;
453
+ return true;
454
+ }
455
+ /** Add an occurrence to a run when it continues the run's step and stride. The prefix has
456
+ * period `step`, so the text between occurrences is the prefix's first `step` units. */
457
+ extend(run, count, start) {
458
+ const step = count - run.last;
459
+ const stride = start - run.lastStart;
460
+ if (run.step ? step !== run.step || stride !== run.stride : step > FORM_PREFIX)
461
+ return false;
462
+ if (!run.step) {
463
+ const mask = this.starts.length - 1;
464
+ run.step = step;
465
+ run.stride = stride;
466
+ run.stepPower = POWERS[step];
467
+ // The rolling hashes before both occurrences differ by the units between them.
468
+ const previous = Math.imul(this.hashes[(run.last - FORM_PREFIX) & mask], run.stepPower);
469
+ run.stepHash = (this.hashes[(count - FORM_PREFIX) & mask] - previous) | 0;
470
+ }
471
+ run.count++;
472
+ run.last = count;
473
+ run.lastStart = start;
474
+ return true;
475
+ }
476
+ }
477
+ export class SentSecrets {
478
+ /** Normalized form to its strongest kind. */
479
+ forms = new Map();
480
+ recipients = new Set();
481
+ privateChars = 0;
482
+ /** The shortest form left unregistered; any string this long might contain it. */
483
+ unmatched = Infinity;
484
+ unicode = false;
485
+ views = {};
486
+ store(form, kind) {
487
+ const prior = this.forms.get(form) ?? 0;
488
+ if (prior >= kind)
489
+ return;
490
+ if (kind !== CREDENTIAL && !prior) {
491
+ if (this.privateChars + form.length > MAX_PRIVATE_FORM_CHARS)
492
+ return this.unregistered(form.length);
493
+ this.privateChars += form.length;
494
+ }
495
+ this.forms.set(form, kind);
496
+ this.unicode ||= /[\u0080-\uffff]/.test(form);
497
+ this.views = {};
498
+ for (const recipient of this.recipients)
499
+ recipient.store(form, kind);
500
+ }
501
+ unregistered(length) {
502
+ if (length >= this.unmatched)
503
+ return;
504
+ this.unmatched = length;
505
+ this.views = {};
506
+ for (const recipient of this.recipients)
507
+ recipient.unregistered(length);
508
+ }
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
+ }
517
+ const encoded = base64(value);
518
+ forms.push(encoded, encoded.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, ""));
519
+ for (const form of forms)
520
+ for (const variant of wireForms(form))
521
+ this.store(variant, kind);
522
+ }
523
+ /** The request receives existing and future secrets from every context. */
524
+ include(source) {
525
+ if (source === this)
526
+ return;
527
+ source.recipients.add(this);
528
+ for (const [form, kind] of source.forms)
529
+ this.store(form, kind);
530
+ this.unregistered(source.unmatched);
531
+ }
532
+ add(value) {
533
+ if (value.length < MIN_SECRET_LENGTH)
534
+ return;
535
+ for (const form of [value, `Bearer ${value}`, `token ${value}`])
536
+ this.register(form, CREDENTIAL);
537
+ }
538
+ /** Explicit secrets use the same floor as every other credential. */
539
+ secret(value) {
540
+ this.add(value);
541
+ }
542
+ /** Submitted writeOnly strings and numbers join this request's set, including property
543
+ * names inside a private object. Empty strings, booleans, and null carry no text. */
544
+ arguments(args, schema) {
545
+ if (!schema)
546
+ return;
547
+ let nodes = 0;
548
+ const submit = (value) => {
549
+ if (!value)
550
+ return;
551
+ if (value.length > MAX_PRIVATE_FORM_CHARS)
552
+ return this.unregistered(value.length);
553
+ this.register(value, value.length < MIN_SECRET_LENGTH ? SHORT : PRIVATE);
554
+ };
555
+ const collect = (value, depth) => {
556
+ if (++nodes > MAX_ARGUMENT_NODES || depth > MAX_ARGUMENT_DEPTH)
557
+ return this.unregistered(1);
558
+ if (typeof value === "string")
559
+ submit(value);
560
+ else if (typeof value === "number" || typeof value === "bigint")
561
+ submit(String(value));
562
+ else if (value !== null && typeof value === "object")
563
+ for (const key of Object.keys(value)) {
564
+ if (nodes > MAX_ARGUMENT_NODES)
565
+ return;
566
+ if (!Array.isArray(value))
567
+ submit(key);
568
+ collect(value[key], depth + 1);
569
+ }
570
+ };
571
+ visitPrivateCallArguments(args, schema, (value) => collect(value, 0));
572
+ }
573
+ /** Structural fields must be refused, never repaired into different URLs. */
574
+ contains(value, ignoreCase = false) {
575
+ if (value.length >= this.unmatched)
576
+ return true;
577
+ if (!this.forms.size)
578
+ 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)
583
+ return true;
584
+ const scanned = matcher.scan(new Source(ignoreCase ? value.toLowerCase() : value), found, { work: Infinity });
585
+ return !scanned || found.spans.length > 0;
586
+ }
587
+ containsUrl(value) {
588
+ const { hosts, components, joined } = credentialUrlViews(value);
589
+ return (hosts.some((host) => this.contains(host, true)) || [...components, ...joined].some((view) => this.contains(view)));
590
+ }
591
+ header(value) {
592
+ this.add(value);
593
+ const framed = /^(?:Bearer|token|Basic)\s+(.+)$/i.exec(value);
594
+ if (!framed)
595
+ return;
596
+ this.add(framed[1]);
597
+ if (/^Basic\s/i.test(value)) {
598
+ try {
599
+ const decoded = atob(framed[1].replace(/-/g, "+").replace(/_/g, "/"));
600
+ this.add(decoded);
601
+ const colon = decoded.indexOf(":");
602
+ if (colon !== -1) {
603
+ this.add(decoded.slice(0, colon));
604
+ this.secret(decoded.slice(colon + 1));
605
+ }
606
+ }
607
+ catch {
608
+ /* An invalid Basic value is still registered verbatim. */
609
+ }
610
+ }
611
+ else
612
+ this.secret(framed[1]);
613
+ }
614
+ request(input, init) {
615
+ const headers = new Headers(init?.headers ?? (input instanceof Request ? input.headers : undefined));
616
+ for (const [name, value] of headers) {
617
+ if (explicitSecretName.test(name))
618
+ this.secret(value);
619
+ else if (name === "cookie" || sensitiveName.test(name))
620
+ this.header(value);
621
+ if (name === "cookie") {
622
+ for (const cookie of value.split(";")) {
623
+ const equals = cookie.indexOf("=");
624
+ if (equals !== -1)
625
+ this.add(cookie.slice(equals + 1).trim());
626
+ }
627
+ }
628
+ }
629
+ const url = new URL(input instanceof Request ? input.url : String(input));
630
+ for (const [name, value] of url.searchParams) {
631
+ if (explicitSecretName.test(name))
632
+ this.secret(value);
633
+ else if (sensitiveName.test(name))
634
+ this.add(value);
635
+ }
636
+ // The OAuth SDK sends token requests as form data. Do not read a body
637
+ // stream or clone a Request: registration must not consume its payload.
638
+ if (headers.get("content-type")?.startsWith("application/x-www-form-urlencoded") &&
639
+ (typeof init?.body === "string" || init?.body instanceof URLSearchParams)) {
640
+ for (const [name, value] of new URLSearchParams(init.body)) {
641
+ if (/^(?:client_secret|password)$/i.test(name))
642
+ this.secret(value);
643
+ else if (/^(?:refresh_token|code|client_assertion)$/i.test(name))
644
+ this.add(value);
645
+ }
646
+ }
647
+ }
648
+ /** Serialized wire text about to be returned, stored, or paged, under payload rules. */
649
+ text(value) {
650
+ return this.idle() ? value : this.wire(value, this.scan(true), 0, true, 0);
651
+ }
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. */
655
+ redact(value, mode = "payload") {
656
+ if (this.idle())
657
+ return value;
658
+ return this.walk(value, this.scan(mode !== "envelope"), mode === "prose" || typeof value === "string", 0, 0);
659
+ }
660
+ /** Credentials alone: later calls keep the private arguments they submit, and cached
661
+ * catalogs never depend on one request's arguments. */
662
+ redactCredentials(value) {
663
+ return this.credentials().redact(value, "envelope");
664
+ }
665
+ idle() {
666
+ return !this.forms.size && this.unmatched === Infinity;
667
+ }
668
+ scan(payload) {
669
+ const work = this.privateChars || this.unmatched < Infinity ? PRIVATE_SCAN_WORK : CREDENTIAL_SCAN_WORK;
670
+ return { budget: { work, nodes: MAX_WALK_NODES }, payload };
671
+ }
672
+ matcher() {
673
+ return (this.views.matcher ??= new Matcher(this.forms));
674
+ }
675
+ credentials() {
676
+ if (!this.privateChars && this.unmatched === Infinity)
677
+ return this;
678
+ if (!this.views.credentials) {
679
+ const view = new SentSecrets();
680
+ for (const [form, kind] of this.forms)
681
+ if (kind === CREDENTIAL)
682
+ view.store(form, kind);
683
+ this.views.credentials = view;
684
+ }
685
+ return this.views.credentials;
686
+ }
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);
690
+ }
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 };
697
+ 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)))
700
+ 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))
705
+ return undefined;
706
+ more = escapes && (units instanceof Unescaped ? units.decoded > 0 && units.slashes > 0 : text.includes("\\"));
707
+ }
708
+ return found;
709
+ }
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;
719
+ }
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) };
738
+ }
739
+ catch {
740
+ // Not JSON.
741
+ }
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;
748
+ }
749
+ }
750
+ return this.string(value, scan, prose, level);
751
+ }
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 = [];
757
+ 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;
763
+ if (typeof item !== "object")
764
+ return item;
765
+ 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;
769
+ if (size > scan.budget.nodes)
770
+ return this.serialized(item, scan, level);
771
+ 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;
778
+ };
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);
811
+ };
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);
820
+ 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]);
824
+ }
825
+ }
826
+ if (!names)
827
+ frame.copy[index] = value;
828
+ else if (value !== DROP)
829
+ define(frame.copy, item, frame.key, frame.name, value);
830
+ };
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;
846
+ }
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.
860
+ }
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);
871
+ }
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))
903
+ 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
+ });
912
+ }
913
+ return value;
914
+ }
915
+ }
916
+ 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;
924
+ }
925
+ /** One identity per upstream request, including all discovery and call work. */
926
+ 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) {
933
+ request.include(secrets);
934
+ }
935
+ return request;
936
+ }
937
+ /** The agent-facing choke point. Apply to values before bridge serialization
938
+ * and to serialized wire text, so JSON escapes and structured strings share
939
+ * 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. */
942
+ export function redactAgentOutput(secrets, value) {
943
+ return secrets.redact(value, "envelope");
944
+ }
945
+ /** Wrap the complete operation table, including rejections. Adding an operation
946
+ * cannot add an unredacted exit. A direct invocation owns a fresh scope unless
947
+ * its HTTP request supplied one. */
948
+ export function agentOutputOperations(create, requestScope) {
949
+ return Object.fromEntries(Object.keys(create(requestScope ?? {})).map((name) => [
950
+ name,
951
+ async (...args) => {
952
+ const scope = requestScope ?? {};
953
+ 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
+ },
961
+ ]));
962
+ }
963
+ /** Slot reads cover custom handlers too, including keys put in query strings. */
964
+ 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
+ };
992
+ }
993
+ export function redactSentSecrets(ctx, value) {
994
+ return sentSecretsFor(ctx).redact(value);
995
+ }
996
+ /** Sanitize listing facts before any cache or consumer receives them. A name
997
+ * cannot be rewritten without changing dispatch, and dropping one entry would
998
+ * publish a partial catalog, so refuse the complete listing instead. */
999
+ export function redactCatalog(ctx, tools) {
1000
+ sentSecretsFor(ctx);
1001
+ const redacted = sentSecretsForRequest(ctx.requestScope ?? ctx).redactCredentials(tools);
1002
+ if (redacted.some((tool, index) => tool.name !== tools[index].name)) {
1003
+ throw new ConnectorCallError("connector_call_failed", "Downstream catalog contains a tool name that echoes a sent credential; refusing the complete catalog.", { retryable: false });
1004
+ }
1005
+ return redacted;
1006
+ }
1007
+ /** Register only after the transport has assembled the request it sends. */
1008
+ export function sentSecretsFetch(ctx, send = fetch) {
1009
+ return (input, init) => {
1010
+ sentSecretsFor(ctx).request(input, init);
1011
+ return send(input, init);
1012
+ };
1013
+ }