@zackbart/connecta 0.28.1 → 0.29.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 (319) hide show
  1. package/CHANGELOG.md +788 -5
  2. package/README.md +62 -42
  3. package/bin/connecta.mjs +110 -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/auth/clerk-sdk/client.d.ts +65 -0
  12. package/dist/auth/clerk-sdk/client.js +5 -0
  13. package/dist/auth/clerk-sdk/generated.js +1845 -0
  14. package/dist/auth/clerk-transport.js +19 -0
  15. package/dist/auth/clerk.d.ts +17 -7
  16. package/dist/auth/clerk.js +189 -107
  17. package/dist/auth/cloudflare-access.js +25 -26
  18. package/dist/auth/downstream-client-metadata.d.ts +29 -0
  19. package/dist/auth/downstream-client-metadata.js +133 -0
  20. package/dist/auth/downstream-oauth.js +1584 -1580
  21. package/dist/auth/downstream-revocation.js +106 -0
  22. package/dist/auth/oauth-token-response.js +231 -0
  23. package/dist/auth/oauth-v2-migration.js +116 -0
  24. package/dist/auth/static-oauth.js +93 -71
  25. package/dist/auth-elicitation.js +283 -0
  26. package/dist/branding.d.ts +8 -16
  27. package/dist/branding.js +26 -59
  28. package/dist/byte-read-response.js +39 -0
  29. package/dist/call-admission.js +12 -16
  30. package/dist/catalog-cache.js +739 -0
  31. package/dist/catalog-drift.d.ts +50 -15
  32. package/dist/catalog-drift.js +365 -128
  33. package/dist/catalog-fingerprint.js +2 -4
  34. package/dist/catalog-freshness.js +13 -0
  35. package/dist/catalog-limits.js +2 -2
  36. package/dist/catalog-service.js +368 -224
  37. package/dist/catalog.js +43 -84
  38. package/dist/concurrency.js +3 -4
  39. package/dist/config-defaults.d.ts +39 -0
  40. package/dist/config-defaults.js +46 -0
  41. package/dist/config-schema.d.ts +181 -0
  42. package/dist/config-schema.js +435 -0
  43. package/dist/config-value-sources.js +102 -0
  44. package/dist/config.d.ts +631 -0
  45. package/dist/config.js +503 -0
  46. package/dist/connector-access.d.ts +1 -0
  47. package/dist/connector-access.js +13 -15
  48. package/dist/connector-caller.d.ts +8 -0
  49. package/dist/connector-caller.js +15 -6
  50. package/dist/connector-scope.js +1 -1
  51. package/dist/connectors/api-connector.d.ts +59 -7
  52. package/dist/connectors/api-connector.js +106 -19
  53. package/dist/connectors/api.js +4 -1
  54. package/dist/connectors/guarded-fetch.d.ts +144 -0
  55. package/dist/connectors/guarded-fetch.js +84 -35
  56. package/dist/connectors/negotiation-cache.js +46 -0
  57. package/dist/connectors/option-shapes.js +27 -0
  58. package/dist/connectors/remote-mcp.d.ts +30 -4
  59. package/dist/connectors/remote-mcp.js +1774 -320
  60. package/dist/connectors/resource-uri.js +335 -0
  61. package/dist/credential-contract.d.ts +10 -0
  62. package/dist/credential-rules.js +1 -3
  63. package/dist/credential-url.js +108 -0
  64. package/dist/credentials.d.ts +6 -1
  65. package/dist/credentials.js +27 -18
  66. package/dist/d1.d.ts +130 -0
  67. package/dist/d1.js +290 -0
  68. package/dist/describe-config.d.ts +165 -0
  69. package/dist/describe-config.js +315 -0
  70. package/dist/described.js +106 -0
  71. package/dist/downstream-input-context.js +119 -0
  72. package/dist/downstream-input.js +323 -0
  73. package/dist/errors.d.ts +97 -20
  74. package/dist/errors.js +158 -80
  75. package/dist/execute.js +453 -305
  76. package/dist/executor-admission.d.ts +1 -0
  77. package/dist/executor-admission.js +11 -15
  78. package/dist/executor-contract.d.ts +20 -0
  79. package/dist/executor-contract.js +48 -0
  80. package/dist/executor-result.js +24 -9
  81. package/dist/executors/quickjs-child.js +3 -4
  82. package/dist/executors/quickjs-runtime.js +147 -59
  83. package/dist/executors/quickjs.js +29 -35
  84. package/dist/favicon.js +1 -2
  85. package/dist/fix-prompt.js +1 -3
  86. package/dist/guest-runtime.js +188 -0
  87. package/dist/guest-types.d.ts +176 -0
  88. package/dist/guest-types.js +1 -0
  89. package/dist/html-security.js +24 -0
  90. package/dist/identity.js +1 -3
  91. package/dist/inbound-credential.js +52 -0
  92. package/dist/index.d.ts +24 -256
  93. package/dist/index.js +65 -478
  94. package/dist/invocation-auth.js +69 -0
  95. package/dist/invocation.js +230 -73
  96. package/dist/json-schema.d.ts +1 -1
  97. package/dist/mcp-client-context.js +35 -0
  98. package/dist/mcp-result.js +11 -7
  99. package/dist/meta-output.js +76 -0
  100. package/dist/meta-tool-names.d.ts +2 -0
  101. package/dist/meta-tool-names.js +3 -0
  102. package/dist/meta-tools.js +285 -209
  103. package/dist/module-contracts.d.ts +8 -23
  104. package/dist/node.d.ts +0 -2
  105. package/dist/node.js +2 -2
  106. package/dist/oauth-callback-outcome.js +1 -3
  107. package/dist/oauth-handoff.js +117 -0
  108. package/dist/oauth-sealing.js +20 -0
  109. package/dist/operator-record.d.ts +137 -0
  110. package/dist/operator-record.js +420 -0
  111. package/dist/operator-ui/assets.js +31 -0
  112. package/dist/operator-ui/contract.d.ts +72 -0
  113. package/dist/operator-ui/contract.js +1 -0
  114. package/dist/operator-ui/fix-prompts.d.ts +18 -0
  115. package/dist/operator-ui/generated.js +5 -4
  116. package/dist/operator-ui/model.d.ts +12 -87
  117. package/dist/operator-ui/model.js +2 -9
  118. package/dist/operator-ui/setup-commands.js +1 -3
  119. package/dist/operator-ui/view.d.ts +323 -0
  120. package/dist/operator-ui/view.js +62 -189
  121. package/dist/page-styles.js +2 -3
  122. package/dist/program-source.js +14 -2
  123. package/dist/{exempt-writes.js → program-writes.js} +55 -18
  124. package/dist/provider.d.ts +142 -0
  125. package/dist/provider.js +187 -0
  126. package/dist/providers/_shared/google/result-size.js +57 -0
  127. package/dist/providers/_shared/google/service-account.d.ts +58 -0
  128. package/dist/providers/_shared/google/service-account.js +500 -0
  129. package/dist/providers/_shared/google/workspace.d.ts +276 -0
  130. package/dist/providers/_shared/google/workspace.js +518 -0
  131. package/dist/providers/{basecamp.d.ts → basecamp/index.d.ts} +6 -28
  132. package/dist/providers/basecamp/index.js +997 -0
  133. package/dist/providers/basecamp/skill.generated.js +9 -0
  134. package/dist/providers/{breeze.d.ts → breeze/index.d.ts} +2 -2
  135. package/dist/providers/{breeze.js → breeze/index.js} +184 -140
  136. package/dist/providers/breeze/skill.generated.js +9 -0
  137. package/dist/providers/{ccb.d.ts → ccb/index.d.ts} +2 -2
  138. package/dist/providers/{ccb.js → ccb/index.js} +271 -116
  139. package/dist/providers/ccb/skill.generated.js +9 -0
  140. package/dist/providers/{cloudflare.d.ts → cloudflare/index.d.ts} +8 -7
  141. package/dist/providers/{cloudflare.js → cloudflare/index.js} +171 -738
  142. package/dist/providers/cloudflare/skill.generated.js +10 -0
  143. package/dist/providers/docs/index.d.ts +54 -0
  144. package/dist/providers/docs/index.js +1237 -0
  145. package/dist/providers/docs/skill.generated.js +9 -0
  146. package/dist/providers/drive/index.d.ts +53 -0
  147. package/dist/providers/drive/index.js +1523 -0
  148. package/dist/providers/drive/skill.generated.js +9 -0
  149. package/dist/providers/forms/index.d.ts +54 -0
  150. package/dist/providers/forms/index.js +1217 -0
  151. package/dist/providers/forms/skill.generated.js +11 -0
  152. package/dist/providers/github/auth.d.ts +32 -0
  153. package/dist/providers/github/auth.js +361 -0
  154. package/dist/providers/github/hosted.js +218 -0
  155. package/dist/providers/github/index.d.ts +13 -0
  156. package/dist/providers/github/index.js +140 -0
  157. package/dist/providers/github/key.js +100 -0
  158. package/dist/providers/github/rest.js +321 -0
  159. package/dist/providers/github/scope.d.ts +29 -0
  160. package/dist/providers/github/scope.js +124 -0
  161. package/dist/providers/github/skill.generated.js +8 -0
  162. package/dist/providers/gmail/index.d.ts +51 -0
  163. package/dist/providers/gmail/index.js +1436 -0
  164. package/dist/providers/gmail/skill.generated.js +9 -0
  165. package/dist/providers/{linear.d.ts → linear/index.d.ts} +11 -14
  166. package/dist/providers/linear/index.js +352 -0
  167. package/dist/providers/linear/skill.generated.js +8 -0
  168. package/dist/providers/{mixpanel.d.ts → mixpanel/index.d.ts} +5 -10
  169. package/dist/providers/mixpanel/index.js +416 -0
  170. package/dist/providers/mixpanel/skill.generated.js +9 -0
  171. package/dist/providers/{notion.d.ts → notion/index.d.ts} +8 -8
  172. package/dist/providers/{notion.js → notion/index.js} +226 -239
  173. package/dist/providers/notion/skill.generated.js +10 -0
  174. package/dist/providers/{overflow.d.ts → overflow/index.d.ts} +2 -2
  175. package/dist/providers/{overflow.js → overflow/index.js} +74 -111
  176. package/dist/providers/overflow/skill.generated.js +8 -0
  177. package/dist/providers/{planning-center.d.ts → planning-center/index.d.ts} +2 -2
  178. package/dist/providers/{planning-center.js → planning-center/index.js} +264 -104
  179. package/dist/providers/planning-center/skill.generated.js +10 -0
  180. package/dist/providers/{revenuecat.d.ts → revenuecat/index.d.ts} +5 -10
  181. package/dist/providers/revenuecat/index.js +557 -0
  182. package/dist/providers/revenuecat/skill.generated.js +11 -0
  183. package/dist/providers/sheets/index.d.ts +54 -0
  184. package/dist/providers/sheets/index.js +1237 -0
  185. package/dist/providers/sheets/skill.generated.js +10 -0
  186. package/dist/providers/slides/index.d.ts +53 -0
  187. package/dist/providers/slides/index.js +2645 -0
  188. package/dist/providers/slides/skill.generated.js +9 -0
  189. package/dist/providers/{stripe.d.ts → stripe/index.d.ts} +5 -12
  190. package/dist/providers/stripe/index.js +266 -0
  191. package/dist/providers/stripe/skill.generated.js +11 -0
  192. package/dist/providers/{tithely.d.ts → tithely/index.d.ts} +2 -2
  193. package/dist/providers/{tithely.js → tithely/index.js} +60 -91
  194. package/dist/providers/tithely/skill.generated.js +8 -0
  195. package/dist/providers/{vercel.d.ts → vercel/index.d.ts} +8 -8
  196. package/dist/providers/vercel/index.js +1038 -0
  197. package/dist/providers/vercel/mcp-ownership.js +212 -0
  198. package/dist/providers/vercel/skill.generated.js +11 -0
  199. package/dist/registry.d.ts +108 -171
  200. package/dist/registry.js +513 -925
  201. package/dist/request-state.js +21 -0
  202. package/dist/resource-read.js +100 -0
  203. package/dist/result-shapes.js +17 -49
  204. package/dist/retired-toolkits.js +1 -1
  205. package/dist/routes/access-tokens.js +3 -6
  206. package/dist/routes/activity-context.js +21 -0
  207. package/dist/routes/activity-disclosure.js +40 -0
  208. package/dist/routes/activity.js +59 -21
  209. package/dist/routes/actor-labels.js +3 -50
  210. package/dist/routes/connect.js +221 -0
  211. package/dist/routes/credentials.js +15 -27
  212. package/dist/routes/mcp.js +356 -130
  213. package/dist/routes/oauth-client-metadata.js +15 -0
  214. package/dist/routes/oauth-management.js +45 -107
  215. package/dist/routes/oauth.js +110 -57
  216. package/dist/routes/operator.js +3 -3
  217. package/dist/routes/shared.d.ts +25 -64
  218. package/dist/routes/shared.js +160 -80
  219. package/dist/routes/skills.js +30 -0
  220. package/dist/routes/ui-config.js +225 -0
  221. package/dist/routes/ui-data.js +40 -35
  222. package/dist/routes/ui.js +66 -56
  223. package/dist/runtime/admission.js +4 -2
  224. package/dist/runtime/call-admission.js +3 -1
  225. package/dist/runtime/connector-scope.js +1 -4
  226. package/dist/runtime/run.js +10 -39
  227. package/dist/runtime/services.js +12 -117
  228. package/dist/runtime/shared-read.js +1 -3
  229. package/dist/runtime/storage.js +8 -1
  230. package/dist/sent-secrets.d.ts +52 -0
  231. package/dist/sent-secrets.js +474 -0
  232. package/dist/server.js +28 -71
  233. package/dist/skills.js +406 -174
  234. package/dist/sqlite.d.ts +48 -0
  235. package/dist/sqlite.js +166 -0
  236. package/dist/storage/keys.js +428 -0
  237. package/dist/storage/memory.js +7 -0
  238. package/dist/storage/sql.d.ts +66 -0
  239. package/dist/storage/sql.js +534 -0
  240. package/dist/timeout.js +4 -2
  241. package/dist/tool-name.js +17 -0
  242. package/dist/tool-safety.d.ts +9 -35
  243. package/dist/tool-safety.js +20 -40
  244. package/dist/types.d.ts +320 -54
  245. package/dist/types.js +0 -1
  246. package/dist/ui.d.ts +7 -19
  247. package/dist/ui.js +76 -90
  248. package/dist/url-safety.js +3 -6
  249. package/dist/usage-guide.js +137 -0
  250. package/dist/validate.d.ts +7 -0
  251. package/dist/validate.js +309 -47
  252. package/dist/version.d.ts +1 -7
  253. package/dist/version.js +3 -7
  254. package/dist/worker.d.ts +11 -0
  255. package/dist/worker.js +434 -0
  256. package/documentation/architecture.md +664 -231
  257. package/documentation/auth.md +936 -546
  258. package/documentation/code-mode.md +501 -312
  259. package/documentation/meta-tools.md +449 -200
  260. package/documentation/operator-tests.md +93 -0
  261. package/documentation/provider-migration-0.29.md +60 -0
  262. package/examples/worker/AGENTS.md +34 -12
  263. package/examples/worker/README.md +286 -255
  264. package/examples/worker/kv-to-d1.wrangler.jsonc +26 -0
  265. package/examples/worker/scripts/copy-kv-to-d1.mjs +162 -0
  266. package/examples/worker/src/connecta.config.ts +151 -0
  267. package/examples/worker/src/index.ts +14 -197
  268. package/examples/worker/wrangler.jsonc +15 -35
  269. package/package.json +120 -54
  270. package/templates/node/.dockerignore +1 -2
  271. package/templates/node/.env.example +20 -21
  272. package/templates/node/AGENTS.md +21 -17
  273. package/templates/node/Dockerfile +3 -5
  274. package/templates/node/README.md +133 -58
  275. package/templates/node/docker-compose.yml +11 -12
  276. package/templates/node/package.json +4 -2
  277. package/templates/node/src/connecta.config.ts +119 -0
  278. package/templates/node/src/index.ts +9 -154
  279. package/templates/node/src/provision-token.ts +17 -0
  280. package/AGENTS.md +0 -170
  281. package/dist/artifacts/connector.d.ts +0 -58
  282. package/dist/artifacts/connector.js +0 -730
  283. package/dist/artifacts/document.js +0 -86
  284. package/dist/artifacts/guide.js +0 -140
  285. package/dist/artifacts/html-scan.js +0 -329
  286. package/dist/artifacts/json.js +0 -78
  287. package/dist/artifacts/kv-store.d.ts +0 -20
  288. package/dist/artifacts/kv-store.js +0 -140
  289. package/dist/artifacts/markdown.js +0 -478
  290. package/dist/artifacts/module.d.ts +0 -37
  291. package/dist/artifacts/module.js +0 -66
  292. package/dist/artifacts/operations.d.ts +0 -197
  293. package/dist/artifacts/operations.js +0 -1002
  294. package/dist/artifacts/refresh.d.ts +0 -55
  295. package/dist/artifacts/refresh.js +0 -236
  296. package/dist/artifacts/routes.js +0 -206
  297. package/dist/artifacts/types.d.ts +0 -214
  298. package/dist/artifacts/types.js +0 -22
  299. package/dist/artifacts/validate.d.ts +0 -62
  300. package/dist/artifacts/validate.js +0 -651
  301. package/dist/artifacts.d.ts +0 -6
  302. package/dist/artifacts.js +0 -9
  303. package/dist/auth/bearer.d.ts +0 -11
  304. package/dist/auth/bearer.js +0 -50
  305. package/dist/providers/basecamp.js +0 -445
  306. package/dist/providers/linear.js +0 -217
  307. package/dist/providers/mixpanel.js +0 -247
  308. package/dist/providers/revenuecat.js +0 -317
  309. package/dist/providers/stripe.js +0 -253
  310. package/dist/providers/vercel.js +0 -1253
  311. package/dist/storage/file.d.ts +0 -15
  312. package/dist/storage/file.js +0 -455
  313. package/ethos.md +0 -93
  314. package/examples/worker/src/cloudflare-kv.ts +0 -43
  315. package/examples/worker/src/d1-activity-row.ts +0 -155
  316. package/examples/worker/src/d1-activity.ts +0 -147
  317. package/examples/worker/src/d1-storage.ts +0 -138
  318. package/examples/worker/src/r2-artifact-blobs.ts +0 -29
  319. package/templates/node/src/file-activity.ts +0 -182
@@ -13,6 +13,7 @@
13
13
  * public surface. Web APIs only.
14
14
  */
15
15
  import { ConnectorCallError, unavailableCallError } from "../errors.js";
16
+ import { sentSecretsFor } from "../sent-secrets.js";
16
17
  /** Parse delta-seconds or an HTTP-date into a non-negative wait window. */
17
18
  export function retryAfterMs(headers) {
18
19
  const raw = headers.get("retry-after");
@@ -43,25 +44,22 @@ export function retryAfterMs(headers) {
43
44
  * fence, not the only one.
44
45
  */
45
46
  export function oauthBearer(provider) {
46
- return {
47
- authenticate: () => ({}),
48
- fetch: (url, init, ctx) => {
49
- const grant = ctx.oauth;
50
- if (!grant) {
51
- // A wiring bug, not an outage: the transport was built for a grant the
52
- // connector does not have, so no request can be authenticated.
53
- throw new ConnectorCallError("connector_call_failed", `The ${provider} transport authenticates through the connector's OAuth grant, and this call's context carries none.`, { retryable: false });
54
- }
55
- return grant.fetch(url, init);
56
- },
47
+ const send = (url, init, ctx) => {
48
+ const grant = ctx.oauth;
49
+ if (!grant) {
50
+ // A wiring bug, not an outage: the transport was built for a grant the
51
+ // connector does not have, so no request can be authenticated.
52
+ throw new ConnectorCallError("connector_call_failed", `The ${provider} transport authenticates through the connector's OAuth grant, and this call's context carries none.`, { retryable: false });
53
+ }
54
+ return grant.fetch(url, init);
57
55
  };
56
+ return { authenticate: () => ({}), fetch: send };
58
57
  }
59
58
  /** Statuses that instruct a client to re-send somewhere else. Never followed. */
60
59
  const REDIRECT_STATUSES = new Set([301, 302, 303, 307, 308]);
61
60
  /** Statuses the HTTP spec defines as carrying no body at all. */
62
61
  const BODILESS_STATUSES = new Set([204, 205, 304]);
63
62
  const decoder = new TextDecoder();
64
- const encoder = new TextEncoder();
65
63
  /**
66
64
  * A base URL is deployment configuration, so a bad one is a structural mistake
67
65
  * and throws where it is written rather than on the first call that uses it.
@@ -74,9 +72,7 @@ function confinementBase(provider, baseUrl) {
74
72
  catch {
75
73
  throw new Error(`${provider} baseUrl must be an absolute URL: ${baseUrl}`);
76
74
  }
77
- const loopback = base.hostname === "localhost" ||
78
- base.hostname === "127.0.0.1" ||
79
- base.hostname === "[::1]";
75
+ const loopback = base.hostname === "localhost" || base.hostname === "127.0.0.1" || base.hostname === "[::1]";
80
76
  if (base.protocol !== "https:" && !(base.protocol === "http:" && loopback)) {
81
77
  throw new Error(`${provider} baseUrl must be https (http is allowed only for a loopback proxy or test double): ${baseUrl}`);
82
78
  }
@@ -111,8 +107,7 @@ function confinedUrl(provider, base, basePath, request) {
111
107
  catch {
112
108
  throw new ConnectorCallError("invalid_args", `A ${provider} request path did not resolve to a valid URL.`);
113
109
  }
114
- if (url.origin !== base.origin ||
115
- !`${url.pathname}/`.startsWith(`${basePath}/`)) {
110
+ if (url.origin !== base.origin || !`${url.pathname}/`.startsWith(`${basePath}/`)) {
116
111
  throw new ConnectorCallError("invalid_args", `A ${provider} request path escaped the connector's base URL.`);
117
112
  }
118
113
  for (const [key, value] of Object.entries(request.query ?? {})) {
@@ -134,9 +129,7 @@ function oversized(provider, limit, detail) {
134
129
  /** The response body as a `ReadableStream`, or null when there is none to read. */
135
130
  function readableBody(response) {
136
131
  const body = response.body;
137
- return body && typeof body.getReader === "function"
138
- ? body
139
- : null;
132
+ return body && typeof body.getReader === "function" ? body : null;
140
133
  }
141
134
  /** Read a stream to the ceiling and stop there — never past it. */
142
135
  async function drain(provider, stream, limit) {
@@ -172,9 +165,9 @@ async function drain(provider, stream, limit) {
172
165
  /**
173
166
  * Every read is capped at `maxResponseBytes`: a streaming body is abandoned at
174
167
  * the ceiling rather than buffered past it, and where a runtime gives no body
175
- * stream the text is measured in UTF-8 bytes before it is accepted, with JSON
176
- * parsed from that same bounded text. Such a runtime still buffers internally,
177
- * but cannot return an oversized body as a successful result.
168
+ * stream the buffered bytes are measured before they are accepted, with text
169
+ * and JSON decoded from them. Such a runtime still buffers internally, but
170
+ * cannot return an oversized body as a successful result.
178
171
  *
179
172
  * A mapper must re-throw `ConnectorCallError` and swallow only what it
180
173
  * recognizes: a bare `catch` around `response.json()` eats this refusal along
@@ -192,9 +185,22 @@ function boundedResponse(provider, response, limit) {
192
185
  text: async () => "",
193
186
  json: emptyJson,
194
187
  jsonResult: () => jsonResult(emptyJson),
188
+ prefix: async () => ({ bytes: new Uint8Array(), truncated: false }),
195
189
  };
196
190
  }
197
191
  const stream = readableBody(response);
192
+ const prefix = async (maxBytes) => {
193
+ const keep = Math.min(Math.max(0, Math.trunc(maxBytes)), limit);
194
+ if (stream)
195
+ return await readPrefix(stream, keep);
196
+ // A runtime with no body stream buffers the body regardless; the
197
+ // ceiling still bounds what is accepted.
198
+ const buffer = new Uint8Array(await response.arrayBuffer());
199
+ if (buffer.byteLength > limit && keep === limit) {
200
+ throw oversized(provider, limit, `${buffer.byteLength} bytes`);
201
+ }
202
+ return { bytes: buffer.slice(0, keep), truncated: buffer.byteLength > keep };
203
+ };
198
204
  let read;
199
205
  const bytes = () => {
200
206
  read ??= stream
@@ -209,14 +215,9 @@ function boundedResponse(provider, response, limit) {
209
215
  };
210
216
  let readText;
211
217
  const text = () => {
212
- readText ??= stream
213
- ? bytes().then((body) => decoder.decode(body))
214
- : response.text().then((body) => {
215
- const size = encoder.encode(body).length;
216
- if (size > limit)
217
- throw oversized(provider, limit, `${size} bytes`);
218
- return body;
219
- });
218
+ // Bytes, then UTF-8, never `response.text()`: workerd quotes a text
219
+ // read's non-text Content-Type in its own log (INV-6).
220
+ readText ??= bytes().then((body) => decoder.decode(body));
220
221
  return readText;
221
222
  };
222
223
  const json = async () => {
@@ -231,8 +232,51 @@ function boundedResponse(provider, response, limit) {
231
232
  text,
232
233
  json,
233
234
  jsonResult: () => jsonResult(json),
235
+ prefix,
234
236
  };
235
237
  }
238
+ /**
239
+ * Read until a chunk crosses `keep` — at most one chunk past it — keep the
240
+ * first `keep` bytes, and cancel the stream: the source stops producing
241
+ * instead of being drained to the ceiling for bytes nobody will look at.
242
+ * A BYOB reader could stop at `keep + 1` exactly, but not every runtime's
243
+ * fetch body offers one, and one chunk is a bounded overrun.
244
+ */
245
+ async function readPrefix(stream, keep) {
246
+ const reader = stream.getReader();
247
+ const chunks = [];
248
+ let total = 0;
249
+ let truncated = false;
250
+ try {
251
+ while (true) {
252
+ const { done, value } = await reader.read();
253
+ if (done)
254
+ break;
255
+ if (!value)
256
+ continue;
257
+ if (total + value.byteLength > keep) {
258
+ chunks.push(value.subarray(0, keep - total));
259
+ total = keep;
260
+ truncated = true;
261
+ break;
262
+ }
263
+ chunks.push(value);
264
+ total += value.byteLength;
265
+ }
266
+ if (truncated)
267
+ await reader.cancel().catch(() => { });
268
+ }
269
+ finally {
270
+ reader.releaseLock();
271
+ }
272
+ const bytes = new Uint8Array(total);
273
+ let offset = 0;
274
+ for (const chunk of chunks) {
275
+ bytes.set(chunk, offset);
276
+ offset += chunk.byteLength;
277
+ }
278
+ return { bytes, truncated };
279
+ }
236
280
  async function jsonResult(read) {
237
281
  try {
238
282
  return { value: await read() };
@@ -280,10 +324,11 @@ export function guardedFetch(options) {
280
324
  throw new ConnectorCallError("invalid_args", `A ${provider} request may not set the ${shadow} header; authentication is connector-owned.`);
281
325
  }
282
326
  headers[name] = value;
327
+ sentSecretsFor(ctx).header(value);
283
328
  }
284
329
  let response;
285
330
  try {
286
- response = await send(url.toString(), {
331
+ const init = {
287
332
  method: request.method,
288
333
  headers,
289
334
  ...(request.body !== undefined
@@ -296,19 +341,23 @@ export function guardedFetch(options) {
296
341
  // never one. Refused below rather than followed.
297
342
  redirect: "manual",
298
343
  ...(ctx.signal ? { signal: ctx.signal } : {}),
299
- }, ctx);
344
+ };
345
+ sentSecretsFor(ctx).request(url, init);
346
+ response = await send(url.toString(), init, ctx);
300
347
  }
301
348
  catch (cause) {
302
349
  if (cause instanceof ConnectorCallError)
303
350
  throw cause;
304
- throw unavailableCallError(cause, url.href, `Could not reach the ${provider} API.`);
351
+ throw unavailableCallError(cause, url.href, `Could not reach the ${provider} API.`, ctx.signal);
305
352
  }
306
353
  if (REDIRECT_STATUSES.has(response.status)) {
307
354
  await response.body?.cancel().catch(() => { });
308
355
  throw new ConnectorCallError("connector_call_failed", `${provider} answered HTTP ${response.status} with a redirect; this connector talks to exactly one origin and never forwards its credential to another.`, { retryable: false });
309
356
  }
310
357
  const declared = Number(response.headers.get("content-length"));
311
- if (Number.isFinite(declared) && declared > limit) {
358
+ // A prefix read takes only what it was asked for, so a long body is not
359
+ // a reason to refuse it; the ceiling still bounds what is read.
360
+ if (Number.isFinite(declared) && declared > limit && !(request.prefixOnly && response.ok)) {
312
361
  await response.body?.cancel().catch(() => { });
313
362
  throw oversized(provider, limit, `a declared ${declared} bytes`);
314
363
  }
@@ -0,0 +1,46 @@
1
+ import { isSpecType } from "@modelcontextprotocol/client";
2
+ import { failureRecord, logFailure } from "../operator-record.js";
3
+ import { sentSecretsForRequest } from "../sent-secrets.js";
4
+ import { assertDownstreamOutputSafe } from "../downstream-input-context.js";
5
+ import { negotiationKeys, NEGOTIATION_TTL_SECONDS } from "../storage/keys.js";
6
+ /** Verdicts carry data only. Clients, signals and secrets remain request-local. */
7
+ export async function readNegotiation(ctx, digest) {
8
+ let prior;
9
+ try {
10
+ const raw = await ctx.storage.get(negotiationKeys.verdict(digest));
11
+ if (!raw)
12
+ return;
13
+ const value = JSON.parse(raw);
14
+ if (!Number.isFinite(value.expiresAt) || value.expiresAt <= Date.now())
15
+ return;
16
+ const candidate = value.prior;
17
+ if ((candidate?.kind === "legacy" && Object.keys(candidate).length === 1) ||
18
+ (candidate?.kind === "modern" && isSpecType.DiscoverResult(candidate.discover)))
19
+ prior = candidate;
20
+ }
21
+ catch (error) {
22
+ logFailure(ctx.logger, "negotiation cache read failed", failureRecord({}, error));
23
+ }
24
+ // Privacy refusals are not cache misses and must not enter cache diagnostics.
25
+ if (prior)
26
+ assertDownstreamOutputSafe(ctx.requestScope ?? ctx, prior);
27
+ return prior;
28
+ }
29
+ export async function storeNegotiation(ctx, digest, prior) {
30
+ if (ctx.signal?.aborted)
31
+ return;
32
+ assertDownstreamOutputSafe(ctx.requestScope ?? ctx, prior);
33
+ // DiscoverResult instructions, metadata and capability keys can echo auth.
34
+ const clean = sentSecretsForRequest(ctx.requestScope ?? ctx).redact(prior);
35
+ if (clean.kind === "modern" && !isSpecType.DiscoverResult(clean.discover))
36
+ return;
37
+ try {
38
+ await ctx.storage.set(negotiationKeys.verdict(digest), JSON.stringify({
39
+ prior: clean,
40
+ expiresAt: Date.now() + NEGOTIATION_TTL_SECONDS * 1000,
41
+ }), { ttlSeconds: NEGOTIATION_TTL_SECONDS });
42
+ }
43
+ catch (error) {
44
+ logFailure(ctx.logger, "negotiation cache write failed", failureRecord({}, error));
45
+ }
46
+ }
@@ -0,0 +1,27 @@
1
+ // Closed option shapes shared by the built-in connector factories: `api()`,
2
+ // `remoteMcp()`, and every maintained provider. Each factory walks its options
3
+ // against its shape with `assertKnownOptions` before reading a value, so a
4
+ // misspelled `maxResultByte` or `requireHTTPs` throws at construction with its
5
+ // path instead of silently doing nothing (INV-11). Values stay the factory's
6
+ // to validate; handlers, schemas, and loggers pass through as `opaque()`.
7
+ import { array, keys, optionsOf, strings, variants } from "../config-schema.js";
8
+ export { CALL_ADMISSION, PROVIDER_COMMON } from "../provider.js";
9
+ export const CREDENTIAL = optionsOf()({
10
+ ...keys("label", "description", "placeholder"),
11
+ fields: array(optionsOf()(keys("name", "label", "description", "placeholder", "inputType"))),
12
+ });
13
+ /** The structured form; a string guide has no keys to check. */
14
+ export const USAGE_GUIDE = optionsOf()(keys("content", "summary", "required"));
15
+ /** `remoteMcp()`'s `auth`, closed per `type`. */
16
+ export const REMOTE_MCP_AUTH = variants("type", {
17
+ request: optionsOf()({ ...keys("type", "token"), headers: strings() }).shape,
18
+ headers: optionsOf()({ ...keys("type"), headers: strings() }).shape,
19
+ credential: optionsOf()({
20
+ ...keys("type", "header", "scheme"),
21
+ credential: CREDENTIAL,
22
+ }).shape,
23
+ oauth: optionsOf()({
24
+ ...keys("type", "clientMetadataUrl", "scope"),
25
+ client: optionsOf()(keys("issuer", "clientId", "clientSecret", "tokenEndpointAuthMethod")),
26
+ }).shape,
27
+ });
@@ -1,6 +1,7 @@
1
+ import { type RemoteOAuthClient } from "../auth/downstream-client-metadata.js";
1
2
  import type { FetchLike, Transport } from "@modelcontextprotocol/client";
2
3
  import { ConnectorCallError } from "../errors.js";
3
- import type { Connector, ConnectorCallAdmissionPolicy, ConnectorContext, ConnectorCredentialConfig, ConnectorUsageGuide, Logger } from "../types.js";
4
+ import type { Connector, ConnectorCallAdmissionPolicy, ConnectorContext, ConnectorCredentialConfig, ConnectorUsageGuide, Logger, ToolClassification } from "../types.js";
4
5
  /**
5
6
  * A static downstream credential supplied through the connection in the
6
7
  * operator UI rather than baked into deployment source.
@@ -34,12 +35,20 @@ interface RemoteMcpCredentialAuth {
34
35
  scheme?: string | null;
35
36
  }
36
37
  export type RemoteMcpAuth = {
38
+ type: "request";
39
+ /** Resolve a Bearer token inside this request. Never persisted or described. */
40
+ token: (ctx: ConnectorContext) => Promise<string>;
41
+ /** Static protocol/catalog headers, never Authorization. */
42
+ headers?: Record<string, string>;
43
+ } | {
37
44
  type: "headers";
38
45
  headers: Record<string, string>;
39
46
  } | RemoteMcpCredentialAuth | {
40
47
  type: "oauth";
41
48
  /** Public HTTPS client metadata document, for servers supporting URL-based client IDs. */
42
49
  clientMetadataUrl?: string;
50
+ /** Pre-registered client, bound to its configured issuer; excludes clientMetadataUrl. */
51
+ client?: RemoteOAuthClient;
43
52
  /** Space-separated default scopes when the resource server does not advertise them. */
44
53
  scope?: string;
45
54
  };
@@ -67,10 +76,10 @@ export interface RemoteMcpOptions {
67
76
  authScope?: "shared" | "personal";
68
77
  /**
69
78
  * Max inline result size (bytes) for this connector's tools before
70
- * call_tool truncates and stashes the full text for get_result
79
+ * call_tool truncates and stashes the full text for connecta.result
71
80
  * paging. Overrides the deployment's `calls.maxResultBytes`; omit to inherit
72
- * it. Must be a whole number of bytes >= 1; anything else warns at startup
73
- * and is ignored.
81
+ * it. Must be a whole number of bytes >= 1; anything else refuses to
82
+ * construct.
74
83
  */
75
84
  maxResultBytes?: number;
76
85
  /** Optional per-runtime downstream call-admission policy. */
@@ -81,7 +90,24 @@ export interface RemoteMcpOptions {
81
90
  * discovery metadata. See `Connector.usageGuide`.
82
91
  */
83
92
  usageGuide?: string | ConnectorUsageGuide;
93
+ /** Opt in to downstream Skills listing and resource reads. Defaults to false. */
94
+ skills?: boolean;
84
95
  auth?: RemoteMcpAuth;
96
+ /**
97
+ * Reviewed read/write verdicts for this downstream's tools, by exact name.
98
+ * Validated at construction. A listed read fills downstream silence but
99
+ * never overrules an explicit write annotation; a listed write or
100
+ * destructive tool stays a write whatever the downstream claims; an unlisted
101
+ * tool is read-only only when it says so explicitly; a listed tool whose
102
+ * `schemaDigest` no longer matches, or cannot be checked, is served as a
103
+ * write. Setting it also reports
104
+ * catalog drift against the list: unclassified, unserved, contradicted, and
105
+ * schema-changed tools, as counts. The connector carries it as
106
+ * `classification`, and its `listTools` returns the downstream's listing
107
+ * unclassified: the registry classifies every read. Omit it to keep the
108
+ * downstream's own annotations, which still fail closed.
109
+ */
110
+ classify?: ToolClassification | undefined;
85
111
  /**
86
112
  * Downstream MCP version-negotiation mode. Defaults to `"auto"`, which
87
113
  * probes with `server/discover` and falls back to the legacy lifecycle when