@zackbart/connecta 0.37.1 → 0.39.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 (265) hide show
  1. package/CHANGELOG.md +94 -0
  2. package/README.md +5 -4
  3. package/bin/connecta.mjs +1 -39
  4. package/bin/version.mjs +1 -1
  5. package/dist/access-tokens.js +2 -4
  6. package/dist/agent-identity.js +1 -1
  7. package/dist/agent-routes.js +231 -0
  8. package/dist/auth/clerk-sdk/generated.js +2 -1
  9. package/dist/auth/downstream-oauth/consent.js +519 -0
  10. package/dist/auth/downstream-oauth/grant-store.js +439 -0
  11. package/dist/auth/downstream-oauth/refresh-coordinator.js +562 -0
  12. package/dist/auth/downstream-oauth/refresh-records.js +447 -0
  13. package/dist/auth/downstream-oauth/sdk-hooks.js +242 -0
  14. package/dist/auth/downstream-oauth.js +66 -2516
  15. package/dist/auth/downstream-revocation.js +1 -1
  16. package/dist/auth/native.js +27 -0
  17. package/dist/auth/oauth-grant.js +24 -0
  18. package/dist/auth/oauth-token-response.js +1 -1
  19. package/dist/auth/static-oauth.js +153 -131
  20. package/dist/auth/vercel-agent.js +2 -1
  21. package/dist/auth-elicitation.js +9 -3
  22. package/dist/authorization-handoff.js +11 -5
  23. package/dist/branding.d.ts +1 -15
  24. package/dist/branding.js +2 -2
  25. package/dist/bytes.js +42 -0
  26. package/dist/call-admission-context.js +2 -5
  27. package/dist/call-recovery.js +0 -3
  28. package/dist/catalog-cache.js +130 -43
  29. package/dist/catalog-fingerprint.js +2 -18
  30. package/dist/catalog-policy.js +18 -0
  31. package/dist/catalog-service.js +285 -48
  32. package/dist/catalog.js +18 -2
  33. package/dist/config-defaults.d.ts +1 -0
  34. package/dist/config-defaults.js +1 -0
  35. package/dist/config-schema.d.ts +1 -1
  36. package/dist/config-schema.js +6 -2
  37. package/dist/config.d.ts +57 -25
  38. package/dist/config.js +30 -77
  39. package/dist/connector-caller.d.ts +1 -1
  40. package/dist/connector-health.js +59 -0
  41. package/dist/connector-status.js +117 -0
  42. package/dist/connectors/api-connector.d.ts +7 -2
  43. package/dist/connectors/api-connector.js +11 -7
  44. package/dist/connectors/guarded-fetch.d.ts +0 -6
  45. package/dist/connectors/guarded-fetch.js +1 -11
  46. package/dist/connectors/negotiation-cache.js +22 -4
  47. package/dist/connectors/remote-mcp/call.js +164 -0
  48. package/dist/connectors/remote-mcp/catalog.js +278 -0
  49. package/dist/connectors/remote-mcp/config.js +263 -0
  50. package/dist/connectors/remote-mcp/connection.js +671 -0
  51. package/dist/connectors/remote-mcp/errors.js +488 -0
  52. package/dist/connectors/remote-mcp/oauth.js +469 -0
  53. package/dist/connectors/remote-mcp/resources.js +473 -0
  54. package/dist/connectors/remote-mcp/runtime.js +1 -0
  55. package/dist/connectors/remote-mcp/transport.js +544 -0
  56. package/dist/connectors/remote-mcp.js +61 -2898
  57. package/dist/connectors/resource-uri.js +0 -4
  58. package/dist/credential-contract.d.ts +23 -0
  59. package/dist/credential-reads.js +17 -0
  60. package/dist/credential-verification.js +53 -0
  61. package/dist/credentials.d.ts +7 -0
  62. package/dist/credentials.js +60 -25
  63. package/dist/d1.d.ts +0 -89
  64. package/dist/d1.js +0 -34
  65. package/dist/describe-config.d.ts +12 -2
  66. package/dist/describe-config.js +2 -0
  67. package/dist/discovery-health.js +199 -0
  68. package/dist/downstream-input.js +4 -2
  69. package/dist/errors.d.ts +55 -17
  70. package/dist/errors.js +95 -41
  71. package/dist/execute.js +172 -125
  72. package/dist/executor-result.js +5 -2
  73. package/dist/executors/quickjs-runtime.js +2 -25
  74. package/dist/executors/quickjs.js +1 -1
  75. package/dist/guest-functions.js +83 -0
  76. package/dist/guest-runtime.js +63 -20
  77. package/dist/guest-types.d.ts +24 -10
  78. package/dist/http/argument-problems.js +35 -0
  79. package/dist/http/client.js +1 -1
  80. package/dist/http/errors.js +7 -6
  81. package/dist/http/fields.js +192 -13
  82. package/dist/http/hatch-response.js +73 -0
  83. package/dist/http/hatch.js +88 -23
  84. package/dist/http/json-schema.js +1 -1
  85. package/dist/http/node-transport.js +0 -1
  86. package/dist/http/oauth.js +4 -3
  87. package/dist/http/paging.js +6 -9
  88. package/dist/http/provider.js +33 -5
  89. package/dist/http/safety.js +9 -1
  90. package/dist/http/services.js +1 -8
  91. package/dist/http/tools.js +45 -30
  92. package/dist/http/vendor.js +7 -7
  93. package/dist/identity.js +2 -6
  94. package/dist/index.d.ts +1 -1
  95. package/dist/index.js +9 -2
  96. package/dist/invocation-auth.js +2 -1
  97. package/dist/invocation.js +21 -6
  98. package/dist/membership.d.ts +13 -5
  99. package/dist/membership.js +34 -2
  100. package/dist/meta-tools.js +50 -35
  101. package/dist/native-recovery.js +4 -16
  102. package/dist/native-tools.js +6 -2
  103. package/dist/oauth-handoff.js +2 -2
  104. package/dist/oauth-sealing.js +10 -0
  105. package/dist/operation-vocabulary.js +136 -14
  106. package/dist/operator-record.d.ts +2 -2
  107. package/dist/operator-record.js +2 -49
  108. package/dist/operator-ui/contract.d.ts +12 -2
  109. package/dist/operator-ui/generated.js +3 -3
  110. package/dist/operator-ui/model.d.ts +22 -1
  111. package/dist/program-writes.js +3 -8
  112. package/dist/provider-call.js +0 -7
  113. package/dist/provider.d.ts +1 -1
  114. package/dist/providers/_shared/google/kit.js +1 -6
  115. package/dist/providers/_shared/google/service-account.js +1 -3
  116. package/dist/providers/_shared/google/workspace.d.ts +1 -1
  117. package/dist/providers/_shared/guide.js +3 -0
  118. package/dist/providers/_shared/json.js +12 -0
  119. package/dist/providers/_shared/rest/compact-index.js +1 -1
  120. package/dist/providers/_shared/rest/kit.js +1 -3
  121. package/dist/providers/_shared/rest/operation-index.js +49 -53
  122. package/dist/providers/_shared/rest/value-safety.js +106 -28
  123. package/dist/providers/breeze/api.js +9 -8
  124. package/dist/providers/breeze/index.js +4 -2
  125. package/dist/providers/breeze/skill.generated.js +1 -1
  126. package/dist/providers/breeze/tools.js +7 -7
  127. package/dist/providers/ccb/api.js +3 -7
  128. package/dist/providers/ccb/index.js +7 -1
  129. package/dist/providers/ccb/skill.generated.js +1 -1
  130. package/dist/providers/ccb/tools.js +1 -6
  131. package/dist/providers/cloudflare/api.js +10 -3
  132. package/dist/providers/cloudflare/index.js +2 -1
  133. package/dist/providers/cloudflare/safety.js +3 -3
  134. package/dist/providers/cloudflare/skill.generated.js +1 -1
  135. package/dist/providers/docs/lib/schemas.js +24 -22
  136. package/dist/providers/docs/safety.js +2 -0
  137. package/dist/providers/docs/skill.generated.js +1 -1
  138. package/dist/providers/docs/tools.js +4 -14
  139. package/dist/providers/drive/api.js +1 -0
  140. package/dist/providers/drive/lib/contracts.js +8 -5
  141. package/dist/providers/drive/skill.generated.js +1 -1
  142. package/dist/providers/drive/tools.js +23 -41
  143. package/dist/providers/forms/api.js +1 -0
  144. package/dist/providers/forms/skill.generated.js +1 -1
  145. package/dist/providers/forms/tools.js +11 -16
  146. package/dist/providers/github/api.js +1 -0
  147. package/dist/providers/github/lib/discovery.js +1 -1
  148. package/dist/providers/github/skill.generated.js +1 -1
  149. package/dist/providers/github/tools.js +61 -17
  150. package/dist/providers/gmail/lib/schemas.js +60 -50
  151. package/dist/providers/gmail/skill.generated.js +1 -1
  152. package/dist/providers/gmail/tools.js +12 -26
  153. package/dist/providers/granola/index.js +2 -0
  154. package/dist/providers/granola/skill.generated.js +1 -1
  155. package/dist/providers/overflow/api.js +8 -4
  156. package/dist/providers/overflow/index.js +2 -1
  157. package/dist/providers/overflow/skill.generated.js +1 -1
  158. package/dist/providers/overflow/tools.js +3 -7
  159. package/dist/providers/planning-center/api.js +5 -2
  160. package/dist/providers/planning-center/lib/constants.d.ts +1 -1
  161. package/dist/providers/planning-center/lib/json-api.js +1 -9
  162. package/dist/providers/planning-center/skill.generated.js +1 -1
  163. package/dist/providers/planning-center/tools.js +2 -1
  164. package/dist/providers/quickbooks/safety.js +80 -39
  165. package/dist/providers/quickbooks/skill.generated.js +1 -1
  166. package/dist/providers/sheets/api.js +1 -0
  167. package/dist/providers/sheets/skill.generated.js +1 -1
  168. package/dist/providers/sheets/tools.js +14 -23
  169. package/dist/providers/slides/skill.generated.js +1 -1
  170. package/dist/providers/slides/tools.js +19 -26
  171. package/dist/providers/stripe/index.js +2 -1
  172. package/dist/providers/stripe/safety.js +2 -2
  173. package/dist/providers/stripe/skill.generated.js +1 -1
  174. package/dist/providers/tithely/api.js +5 -8
  175. package/dist/providers/tithely/index.js +3 -2
  176. package/dist/providers/tithely/skill.generated.js +1 -1
  177. package/dist/providers/tithely/tools.js +7 -5
  178. package/dist/providers/vercel/api.js +2 -7
  179. package/dist/providers/vercel/lib/pin.js +1 -1
  180. package/dist/providers/vercel/safety.js +1 -1
  181. package/dist/providers/vercel/skill.generated.js +1 -1
  182. package/dist/providers/vercel/tools.js +6 -4
  183. package/dist/redaction-marker.js +2 -2
  184. package/dist/registry.d.ts +11 -5
  185. package/dist/registry.js +46 -20
  186. package/dist/request-principal.js +1 -1
  187. package/dist/request-state.js +2 -2
  188. package/dist/routes/access-tokens.js +1 -1
  189. package/dist/routes/activity-disclosure.js +1 -1
  190. package/dist/routes/connect.js +2 -2
  191. package/dist/routes/credentials.js +130 -61
  192. package/dist/routes/dispatch.js +5 -1
  193. package/dist/routes/mcp.js +34 -17
  194. package/dist/routes/oauth-management.js +1 -1
  195. package/dist/routes/operator.js +6 -5
  196. package/dist/routes/shared.d.ts +13 -8
  197. package/dist/routes/shared.js +15 -9
  198. package/dist/routes/ui-config.js +3 -3
  199. package/dist/routes/ui-data.js +126 -20
  200. package/dist/routes/ui.js +17 -1
  201. package/dist/runtime/call-admission.js +1 -1
  202. package/dist/runtime/call.js +8 -3
  203. package/dist/runtime/native-storage.js +9 -0
  204. package/dist/runtime/request-reads.js +182 -0
  205. package/dist/runtime/request-scope.js +16 -0
  206. package/dist/runtime/settlement.js +1 -1
  207. package/dist/runtime/shared-read.js +2 -15
  208. package/dist/runtime/storage.js +12 -0
  209. package/dist/sent-secrets.d.ts +1 -2
  210. package/dist/sent-secrets.js +3 -8
  211. package/dist/server.js +4 -4
  212. package/dist/skills.js +44 -54
  213. package/dist/sqlite.d.ts +0 -19
  214. package/dist/sqlite.js +2 -83
  215. package/dist/storage/keys.js +69 -128
  216. package/dist/storage/memory.js +8 -2
  217. package/dist/storage/sql-native.js +27 -101
  218. package/dist/storage/sql.d.ts +1 -24
  219. package/dist/storage/sql.js +13 -14
  220. package/dist/storage-digest.js +32 -0
  221. package/dist/timeout.js +8 -0
  222. package/dist/tool-error.js +4 -0
  223. package/dist/types.d.ts +33 -0
  224. package/dist/url-safety.js +0 -4
  225. package/dist/usage-guide.js +18 -37
  226. package/dist/validate.d.ts +3 -0
  227. package/dist/validate.js +220 -158
  228. package/dist/version.d.ts +1 -1
  229. package/dist/version.js +1 -1
  230. package/dist/worker.js +1 -1
  231. package/dist/write-outcome.js +0 -3
  232. package/documentation/README.md +19 -55
  233. package/documentation/architecture.md +436 -515
  234. package/documentation/auth.md +229 -88
  235. package/documentation/code-mode.md +201 -94
  236. package/documentation/deploying.md +25 -43
  237. package/documentation/integrating.md +18 -19
  238. package/documentation/meta-tools.md +225 -51
  239. package/documentation/migration-0.33.md +10 -6
  240. package/documentation/migration-0.34.md +5 -5
  241. package/documentation/migration-retired-layouts.md +74 -0
  242. package/documentation/operator-tests.md +20 -120
  243. package/documentation/operator-ui.md +108 -16
  244. package/documentation/pool-membership-upgrade.md +44 -0
  245. package/documentation/provider-auth-migration.md +4 -10
  246. package/documentation/provider-migration-0.29.md +4 -15
  247. package/documentation/providers.md +105 -0
  248. package/examples/worker/AGENTS.md +14 -16
  249. package/examples/worker/README.md +24 -118
  250. package/examples/worker/src/connecta.config.ts +1 -0
  251. package/guides/connecting-agents.md +9 -31
  252. package/guides/how-it-works.md +6 -4
  253. package/guides/safety-and-trust.md +4 -1
  254. package/package.json +19 -19
  255. package/templates/node/AGENTS.md +13 -12
  256. package/templates/node/README.md +9 -26
  257. package/templates/node/package.json +1 -1
  258. package/templates/node/src/connecta.config.ts +1 -0
  259. package/dist/auth/oauth-v2-migration.js +0 -135
  260. package/dist/provider-call-facts.d.ts +0 -115
  261. package/dist/provider-call.d.ts +0 -112
  262. package/dist/storage/d1-copy.js +0 -255
  263. package/dist/storage/sql-schema.d.ts +0 -6
  264. package/examples/worker/kv-to-d1.wrangler.jsonc +0 -26
  265. package/examples/worker/scripts/copy-kv-to-d1.mjs +0 -162
@@ -0,0 +1,544 @@
1
+ // The transport each connect builds, and write-once dispatch: every write's
2
+ // call reaches the wire at most once (INV-1, INV-9), and session close is bounded.
3
+ import { authView } from "../../auth/native-call.js";
4
+ import { authFlow, authCall, authInvoke, authValue, authMethod, authCatch } from "../../auth/native.js";
5
+ import { isOAuthCredentialRequest } from "../../auth/oauth-token-response.js";
6
+ import { deriveContext } from "../../derived-context.js";
7
+ import { operationContext, viewServices, viewSignal } from "../../runtime/call.js";
8
+ import { trackRemoteClientRequest } from "../../auth/downstream-client-metadata.js";
9
+ import { SdkHttpError, StreamableHTTPClientTransport } from "@modelcontextprotocol/client";
10
+ import { Deferred, Duration, Effect, Exit } from "effect";
11
+ import { codeExchangeResponse, earlyRenewalFetch, OAuthRequestNotSentError, } from "../../auth/downstream-oauth.js";
12
+ import { byteReadResponse } from "../../byte-read-response.js";
13
+ import { sentSecretsFor } from "../../sent-secrets.js";
14
+ import { MAX_SERIALIZED_CATALOG_BYTES } from "../../catalog-limits.js";
15
+ import { ConnectorCallError } from "../../errors.js";
16
+ import { attachFailureFacts, failureRecord, labelErrorClass, logFailure } from "../../operator-record.js";
17
+ import { learnedUrlRefusal } from "../../url-safety.js";
18
+ import { RequestScope } from "../../runtime/request-scope.js";
19
+ import { Context } from "effect";
20
+ import { downstreamWriteContinuation } from "../../downstream-input-context.js";
21
+ import { guardedTransport, sendGuarded, sendCredentialPost } from "../../guarded-transport.js";
22
+ import { dispatchProviderRequest, recordProviderResponse, providerRequest, renewedAfterUnauthorized, } from "../../provider-call.js";
23
+ import { recordRequestField } from "../../provider-call-facts.js";
24
+ import { AUTH_OR_THROTTLE, errorBodyText, jsonRpcErrorBody, keepsModernVerdict, promised } from "./errors.js";
25
+ import { MAX_SKILL_READ_RPC_BYTES, boundedSkillResponse, skillObject } from "./resources.js";
26
+ // The redirect guard keeps its published path; nothing here reads it while
27
+ // modules load, so this import cycle with the entry is inert.
28
+ import { redirectSafeFetch } from "../remote-mcp.js";
29
+ /**
30
+ * How long a downstream gets to answer the session-termination DELETE before
31
+ * teardown stops waiting. This is a network round-trip budget, deliberately
32
+ * independent of the core's 100 ms caller-facing scope-close window: an
33
+ * already-established cross-internet connection avoids setup, but 50 ms is
34
+ * still too short for an ordinary round trip plus modest provider scheduling.
35
+ * The bounded tail is deferred on runtimes that can keep it alive after the
36
+ * response, while callers continue to wait at most 100 ms.
37
+ */
38
+ const TERMINATE_SESSION_BUDGET_MS = 1_000;
39
+ /**
40
+ * How long the local close gets after the DELETE: the second of the two
41
+ * seconds the core's deferred scope-close tail allows (see
42
+ * `src/runtime/connector-scope.ts`). The SDK's own transports close at once;
43
+ * this bounds one that never does.
44
+ */
45
+ export const LOCAL_CLOSE_BUDGET_MS = 1_000;
46
+ /**
47
+ * End the downstream's session before the connection is torn down.
48
+ *
49
+ * `Client.close()` only unwinds our side — it aborts the transport's controller
50
+ * and fires `onclose`. Spec session termination is a separate DELETE carrying
51
+ * `Mcp-Session-Id`, and without it a stateful provider keeps the session alive
52
+ * until its own (often hour-long) timeout, accumulating abandoned sessions.
53
+ *
54
+ * Ordering is load-bearing: the SDK sends that DELETE on the transport's
55
+ * AbortSignal, so calling this *after* close would abort the request on issue
56
+ * and silently do nothing. Everything else is best-effort — a transport with no
57
+ * `terminateSession` (a custom one, or an older SDK), a downstream that refuses
58
+ * (405 is a legal answer), errors, or never replies all fall through to the
59
+ * close with the session left to age out as it did before.
60
+ */
61
+ function terminateSession(transport, logger, connectorId) {
62
+ // SDK v2's Client.close() does not send the legacy session DELETE on our
63
+ // behalf. Connecta's own endpoint creates no protocol session, but a stateful
64
+ // legacy downstream can still issue `Mcp-Session-Id`, and every path that
65
+ // abandons one — scope teardown, credential rotation, OAuth retirement, an
66
+ // abandoned connect — owes it this best-effort, one-second DELETE.
67
+ const terminate = transport.terminateSession;
68
+ if (typeof terminate !== "function")
69
+ return Effect.void;
70
+ const warn = (log) => Effect.sync(() => {
71
+ try {
72
+ log();
73
+ }
74
+ catch {
75
+ // A diagnostic sink cannot make best-effort teardown observable to the
76
+ // caller in the one way this contract forbids: by replacing its result.
77
+ }
78
+ });
79
+ // Whichever finishes first wins and the other is interrupted: an answer
80
+ // clears the timer, and a failure that lands after the timeout is consumed
81
+ // by the interrupted wait rather than warned about a second time.
82
+ return Effect.raceAllFirst([
83
+ // The SDK issues no request at all when no `mcp-session-id` was captured,
84
+ // so a stateless downstream never sees a spurious DELETE.
85
+ promised(authMethod(() => authInvoke(Reflect, "apply", [terminate, transport, []]))).pipe(Effect.catch((error) =>
86
+ // The SDK's error quotes the downstream's status text, and its `data`
87
+ // the body, so the record keeps only the status it answered with.
88
+ warn(() => logFailure(logger, "session termination refused or failed; the downstream session may remain until its provider timeout", failureRecord({ connector: connectorId }, error instanceof SdkHttpError ? attachFailureFacts(error, { httpStatus: error.status }) : error))))),
89
+ Effect.sleep(Duration.millis(TERMINATE_SESSION_BUDGET_MS)).pipe(Effect.andThen(warn(() => logger.warn(`[connecta] connector "${connectorId}" session termination was not ` +
90
+ `acknowledged within ${TERMINATE_SESSION_BUDGET_MS} ms; the ` +
91
+ "downstream may still finish the headers-only DELETE, otherwise " +
92
+ "the session will remain until its provider timeout.")))),
93
+ ]);
94
+ }
95
+ /**
96
+ * The local half of a close: the SDK client's close once a client owns the
97
+ * transport, the bare transport's until then. Never fails, and stops waiting
98
+ * after LOCAL_CLOSE_BUDGET_MS — a transport whose close never settles would
99
+ * otherwise hold every caller that awaits the scope close, the credential Test
100
+ * action among them.
101
+ */
102
+ function closeLocally(client, transport) {
103
+ if (!client && !transport)
104
+ return Effect.void;
105
+ return Effect.raceAllFirst([
106
+ promised(authMethod(() => (client ? authInvoke(client, "close", []) : authValue(() => transport?.close())))).pipe(Effect.ignore),
107
+ Effect.sleep(Duration.millis(LOCAL_CLOSE_BUDGET_MS)),
108
+ ]);
109
+ }
110
+ export const REDIRECT_STATUSES = new Set([301, 302, 303, 307, 308]);
111
+ export class RemoteMcpDestinationError extends OAuthRequestNotSentError {
112
+ constructor(connectorId, reason) {
113
+ super("connector_call_failed", `Connector "${connectorId}" refused an OAuth URL the downstream advertised: ${reason}.`);
114
+ this.name = "RemoteMcpDestinationError";
115
+ }
116
+ }
117
+ labelErrorClass(RemoteMcpDestinationError, "RemoteMcpDestinationError");
118
+ /**
119
+ * Refuse, before any request leaves, a URL the downstream taught the OAuth
120
+ * flow that points somewhere connecta must not go (see `url-safety.ts`).
121
+ *
122
+ * The SDK learns authorization-server, token, and registration URLs from the
123
+ * downstream's own metadata and fetches them through the transport's fetch,
124
+ * so this sits below the refresh coordinator on both send paths. Resource
125
+ * redirects stay same-origin; credential-bearing token requests bypass the
126
+ * redirect wrapper. The typed local refusal proves that this guard sent nothing;
127
+ * failures after an HTTP dispatch keep their permanent ambiguous verdict.
128
+ */
129
+ export function learnedUrlSafeFetch(connectorId, configured, baseFetch) {
130
+ return authMethod((input, init) => authFlow(function* () {
131
+ const reason = learnedUrlRefusal(configured, new URL(input));
132
+ if (reason !== undefined) {
133
+ throw new RemoteMcpDestinationError(connectorId, reason);
134
+ }
135
+ return byteReadResponse((yield authCall(baseFetch, [input, init])));
136
+ }));
137
+ }
138
+ /**
139
+ * The key a `tools/call`'s params carry its dispatch under. Object spread
140
+ * copies a symbol-keyed property, and the pinned SDK only spreads a request's
141
+ * params (to add a progress token or its `_meta` envelope); `JSON.stringify`
142
+ * skips symbol keys, so nothing of it reaches the wire.
143
+ */
144
+ const DISPATCH = Symbol("connecta.dispatch");
145
+ export function newDispatch(write, sourceWrite) {
146
+ return { write, sourceWrite, sent: false, unauthorized: false, redirected: false, resendRefused: false };
147
+ }
148
+ export function withDispatch(params, dispatch) {
149
+ Object.defineProperty(params, DISPATCH, { value: dispatch, enumerable: true });
150
+ return params;
151
+ }
152
+ function dispatchOf(params) {
153
+ return params !== null && typeof params === "object"
154
+ ? params[DISPATCH]
155
+ : undefined;
156
+ }
157
+ /** A further request for a write that already left, stopped before it does. */
158
+ function resendRefused(dispatch) {
159
+ dispatch.resendRefused = true;
160
+ return new ConnectorCallError("connector_call_failed", "This write was already sent once; it was not sent again.", {
161
+ retryable: false,
162
+ writeOutcome: "unknown",
163
+ });
164
+ }
165
+ /** Lost tracking cannot prove whether an earlier message already sent a write. */
166
+ function missingDispatch() {
167
+ return new ConnectorCallError("connector_call_failed", "The downstream call lost its dispatch record and cannot be proven read-only. It was not sent; whether an earlier write took effect is unknown. Check the target before repeating it.", { retryable: false, writeOutcome: "unknown" });
168
+ }
169
+ /** One connector's transports, sent-secret tracking and session close. */
170
+ export function remoteMcpTransport(rt) {
171
+ const { extraHeaders, opts, isOauth, customFetch, destination, id, customSender, requestToken, skillsEnabled, credentialAuth, requestAuth, credentialHeader, } = rt;
172
+ // Each OAuth transport's early renewal: its bound 401 flow, run before a
173
+ // call sends a token about to expire, owned by that call's signal.
174
+ const earlyRenewals = new WeakMap();
175
+ // A cached transport may dispatch concurrent calls and listings. Register
176
+ // its actual sent auth with every operation using that client, including
177
+ // handshake/discovery requests and OAuth rotations. Remove settled users.
178
+ const activeContexts = new WeakMap();
179
+ // Resource headers are non-secret context by contract. They never join the
180
+ // sent set, so arguments and results equal to them pass unchanged.
181
+ // An argument mirrored as `Mcp-Param-*` carrying a resource value is the
182
+ // same context under a name the tracker may read as credential-shaped.
183
+ const resourceValues = new Set(Object.values(extraHeaders ?? {}));
184
+ const withoutResourceHeaders = (input, init) => {
185
+ if (!extraHeaders)
186
+ return init;
187
+ const headers = new Headers(init?.headers ?? (input instanceof Request ? input.headers : undefined));
188
+ for (const [name, value] of Object.entries(extraHeaders))
189
+ if (headers.get(name) === value)
190
+ headers.delete(name);
191
+ const mirrored = [];
192
+ for (const [name, value] of headers)
193
+ if (name.startsWith("mcp-param-") && resourceValues.has(value))
194
+ mirrored.push(name);
195
+ for (const name of mirrored)
196
+ headers.delete(name);
197
+ return { ...init, headers };
198
+ };
199
+ const trackSentRequest = (ctx, input, init) => {
200
+ const active = activeContexts.get(rt.entryFor(ctx));
201
+ const recipients = new Set([sentSecretsFor(ctx), ...[...(active ?? [])].map(sentSecretsFor)]);
202
+ const tracked = withoutResourceHeaders(input, init);
203
+ for (const secrets of recipients) {
204
+ if (opts.auth?.type === "oauth")
205
+ trackRemoteClientRequest(secrets, input, tracked);
206
+ else
207
+ secrets.request(input, tracked);
208
+ }
209
+ };
210
+ const buildTransport = (ctx, provider,
211
+ /**
212
+ * This attempt's assembled header value, for credential auth only — framed
213
+ * by the caller so the raw secret is not passed around twice. Never
214
+ * retained past the transport it configures.
215
+ */
216
+ credentialFramed = null, signal = viewSignal(ctx, true),
217
+ /** The raw value behind `credentialFramed`, under the same terms. */
218
+ credentialValue = null,
219
+ /** A code exchange never transfers ownership to the refresh coordinator. */
220
+ exchangeOnly = false,
221
+ /** The admitted principal `credentialValue` was resolved for, under the same terms. */
222
+ principal = undefined) => {
223
+ const resourceInit = (input, init) => {
224
+ if (!extraHeaders)
225
+ return init;
226
+ const target = new URL(input instanceof Request ? input.url : String(input));
227
+ const resource = new URL(opts.url);
228
+ if (target.origin !== resource.origin || target.pathname !== resource.pathname)
229
+ return init;
230
+ const headers = new Headers(init?.headers ?? (input instanceof Request ? input.headers : undefined));
231
+ for (const [name, value] of Object.entries(extraHeaders))
232
+ headers.set(name, value);
233
+ return { ...init, headers };
234
+ };
235
+ const responseOwner = Context.get(viewServices(ctx), RequestScope);
236
+ const initiatingSignal = ctx.signal;
237
+ ctx = deriveContext(operationContext(ctx), signal ? { signal } : {});
238
+ if (opts._transportFactory)
239
+ return opts._transportFactory(ctx);
240
+ const url = new URL(opts.url);
241
+ // Missing records never acquire a fresh write attempt. Only active calls
242
+ // for this exact tool, all classified read, can prove a recordless message
243
+ // safe. Catalog hints alone cannot prove the invocation's served verdict.
244
+ const untrackedRead = (params) => {
245
+ if (!skillObject(params) || typeof params.name !== "string")
246
+ return false;
247
+ let read = false;
248
+ for (const [dispatch, name] of rt.entryFor(ctx).dispatches) {
249
+ if (name !== params.name)
250
+ continue;
251
+ if (dispatch.write)
252
+ return false;
253
+ read = true;
254
+ }
255
+ return read;
256
+ };
257
+ // This transport's calls on the wire, by JSON-RPC id, while each is sent.
258
+ const flights = new Map();
259
+ const era = {};
260
+ const trackedFetch = authMethod((input, init) => authFlow(function* () {
261
+ let rpc;
262
+ if (typeof init?.body === "string") {
263
+ try {
264
+ rpc = JSON.parse(init.body);
265
+ }
266
+ catch { }
267
+ }
268
+ const toolCall = init?.method === "POST" && skillObject(rpc) && rpc.method === "tools/call" ? rpc : undefined;
269
+ const writeContinuation = downstreamWriteContinuation(ctx.requestScope ?? ctx) && toolCall !== undefined;
270
+ const flight = toolCall && (typeof toolCall.id === "string" || typeof toolCall.id === "number")
271
+ ? flights.get(toolCall.id)
272
+ : undefined;
273
+ if (toolCall && !flight && !untrackedRead(toolCall.params))
274
+ throw missingDispatch();
275
+ const credentialPost = isOauth && (init?.method ?? "GET").toUpperCase() === "POST" && (!skillObject(rpc) || rpc.jsonrpc !== "2.0");
276
+ // A custom transport reaches only the configured origin; redirects are
277
+ // already same-origin, and OAuth cannot be combined with it.
278
+ if (customFetch && new URL(input instanceof Request ? input.url : String(input)).origin !== destination.origin)
279
+ throw new ConnectorCallError("connector_call_failed", `Connector "${id}" refused a request outside its origin; nothing was sent.`, { retryable: false });
280
+ // The deployment's fetch owns the wire: each write is handed over once.
281
+ const sender = flight?.write ? (customSender ?? guardedTransport(true)) : undefined;
282
+ if (flight?.write) {
283
+ if (flight.sent) {
284
+ if (flight.request && flight.authorization)
285
+ renewedAfterUnauthorized(flight.request.entry, flight.authorization, new Headers(init?.headers).get("authorization") ?? flight.authorization);
286
+ throw resendRefused(flight);
287
+ }
288
+ flight.authorization = new Headers(init?.headers).get("authorization");
289
+ }
290
+ const request = flight?.request;
291
+ if (request?.entry.dispatched)
292
+ request.entry = providerRequest(request.ctx, !flight?.write, request.entry.provider, {
293
+ method: init?.method ?? "GET",
294
+ });
295
+ const entry = request?.entry;
296
+ const dispatch = () => {
297
+ if (flight?.write) {
298
+ if (flight.sent)
299
+ throw resendRefused(flight);
300
+ flight.sent = true;
301
+ }
302
+ if (request && entry) {
303
+ if (flight?.sourceWrite)
304
+ recordRequestField(entry, "sourceWrite", true);
305
+ dispatchProviderRequest(request.ctx, entry, init?.headers, init?.method ?? "GET");
306
+ }
307
+ };
308
+ trackSentRequest(ctx, input, init);
309
+ // Attach retained settlement facts at the physical Promise leaf.
310
+ // Cancellation may end the native waiter before it resumes; neither
311
+ // the status nor a 401 verdict may disappear with that live authority.
312
+ const response = (yield authValue(() => {
313
+ const pending = credentialPost
314
+ ? sendCredentialPost(input, init ?? {}, dispatch)
315
+ : sender
316
+ ? sendGuarded(sender, new URL(input instanceof Request ? input.url : String(input)), { ...init, redirect: "manual" }, dispatch)
317
+ : (() => {
318
+ dispatch();
319
+ const send = customFetch ?? fetch;
320
+ return writeContinuation ? send(input, { ...init, redirect: "manual" }) : send(input, init);
321
+ })();
322
+ return pending.then((response) => {
323
+ // RPC fetch adapters can return a disposable Response. Own it at
324
+ // the physical Promise leaf, including a late answer after close,
325
+ // and keep its body live until the existing request boundary.
326
+ const dispose = response[Symbol.dispose];
327
+ if (dispose)
328
+ responseOwner.own(() => dispose.call(response));
329
+ if (entry)
330
+ recordProviderResponse(entry, response.status);
331
+ if (flight?.write) {
332
+ if (response.status === 401)
333
+ flight.unauthorized = true;
334
+ else if (REDIRECT_STATUSES.has(response.status))
335
+ flight.redirected = true;
336
+ }
337
+ if (init?.signal?.aborted || request?.ctx.signal?.aborted)
338
+ void response.body?.cancel().catch(() => { });
339
+ return response;
340
+ });
341
+ }));
342
+ const refused = response.status === 400 ? era.refused : undefined;
343
+ if ((flight?.write || refused) &&
344
+ response.status >= 400 &&
345
+ response.status < 500 &&
346
+ response.status !== 408 &&
347
+ !AUTH_OR_THROTTLE.includes(response.status)) {
348
+ const lifetimes = [init?.signal ?? undefined, request?.ctx.signal, signal];
349
+ const text = (yield authCall(errorBodyText, [response, lifetimes]));
350
+ const ended = lifetimes.find((lifetime) => lifetime?.aborted);
351
+ if (ended) {
352
+ // Nothing will consume the original either: release its source.
353
+ void response.body?.cancel().catch(() => { });
354
+ throw ended.reason;
355
+ }
356
+ const error = jsonRpcErrorBody(text);
357
+ // Only a JSON-RPC error proves the server refused the write.
358
+ if (flight?.write && !error)
359
+ flight.ambiguous = true;
360
+ // Per the transport's backward-compatibility rule, a 400 that is no
361
+ // recognized modern error is a legacy refusal, and an
362
+ // UnsupportedProtocolVersion that drops the selected version leaves
363
+ // the verdict unusable; either ends it. The call itself still fails.
364
+ if (refused &&
365
+ text !== undefined &&
366
+ !keepsModernVerdict(error, new Headers(init?.headers).get("mcp-protocol-version")))
367
+ (yield authCall(refused, []));
368
+ }
369
+ if (response.status === 401 && requestToken?.invalidate && credentialValue !== null) {
370
+ // Invalidate the exact bytes this transport sent, never a newer token.
371
+ // Preserve the original 401 even if an operator hook fails. No replay.
372
+ (yield authCatch(authInvoke(requestToken, "invalidate", principal ? [credentialValue, ctx, Object.freeze({ principal })] : [credentialValue, ctx]), () => { }));
373
+ }
374
+ if (credentialPost && !isOAuthCredentialRequest(init) && REDIRECT_STATUSES.has(response.status)) {
375
+ (yield authCatch(authValue(() => response.body?.cancel()), () => { }));
376
+ throw new ConnectorCallError("connector_call_failed", "OAuth registration endpoint redirected; the request was not repeated.", { retryable: false });
377
+ }
378
+ if (writeContinuation && (response.status === 401 || (response.status >= 300 && response.status < 400))) {
379
+ (yield authCatch(authValue(() => response.body?.cancel()), () => { }));
380
+ if (response.status === 401)
381
+ throw rt.authRequiredError(ctx);
382
+ throw new ConnectorCallError("connector_call_failed", "The downstream redirected a write continuation; it was not repeated.", { retryable: false, writeOutcome: "unknown" });
383
+ }
384
+ if (flight?.redirected) {
385
+ (yield authCatch(authValue(() => response.body?.cancel()), () => { }));
386
+ throw new ConnectorCallError("connector_call_failed", "The downstream answered this write with a redirect; it was not sent again.", { retryable: false, writeOutcome: "unknown" });
387
+ }
388
+ if (skillsEnabled && skillObject(rpc) && (rpc.method === "skills/list" || rpc.method === "resources/read")) {
389
+ return (yield authCall(boundedSkillResponse, [
390
+ response,
391
+ rpc.method === "skills/list" ? MAX_SERIALIZED_CATALOG_BYTES : MAX_SKILL_READ_RPC_BYTES,
392
+ rpc.id,
393
+ init?.signal,
394
+ ]));
395
+ }
396
+ return response;
397
+ }));
398
+ // A runtime refusing the assembled header quotes it, and the transport
399
+ // error below keeps none of what the runtime said. So whether the
400
+ // rejection quoted the credential is decided here, first, and survives
401
+ // as the class of the error rather than as its text.
402
+ const guardedFetch = redirectSafeFetch(id, opts.redirects, credentialFramed === null
403
+ ? trackedFetch
404
+ : authMethod((input, init) => authFlow(function* () {
405
+ try {
406
+ return (yield authCall(trackedFetch, [input, init]));
407
+ }
408
+ catch (err) {
409
+ throw rt.withoutCredential(ctx, err, credentialValue, credentialFramed);
410
+ }
411
+ })));
412
+ if (opts.auth?.type === "oauth") {
413
+ const oauthProvider = provider ?? rt.newProvider(ctx, undefined, signal);
414
+ // The session/provider belong to the request. A renewal belongs to the
415
+ // exact call whose HTTP response challenged it, until claim submission.
416
+ // Carry that ownership on the response the SDK passes to onUnauthorized;
417
+ // a shared mutable "current call" would borrow a concurrent call's lease.
418
+ const renewalSignals = new WeakMap();
419
+ const exchangeFetch = (send) => exchangeOnly
420
+ ? authMethod((input, init) => authView(ctx, authMethod((requestSignal) => authFlow(function* () {
421
+ const response = (yield authCall(send, [
422
+ input,
423
+ {
424
+ ...init,
425
+ signal: init?.signal ? AbortSignal.any([requestSignal, init.signal]) : requestSignal,
426
+ },
427
+ ]));
428
+ if (requestSignal.aborted) {
429
+ void response.body?.cancel().catch(() => { });
430
+ throw requestSignal.reason;
431
+ }
432
+ return codeExchangeResponse(response, requestSignal);
433
+ })), true))
434
+ : send;
435
+ const coordinatedFetch = rt.refreshCoordinatorFor(ctx).coordinatedFetch(oauthProvider, exchangeFetch(learnedUrlSafeFetch(id, url, trackedFetch)), signal, ctx.defer, exchangeFetch(learnedUrlSafeFetch(id, url, authMethod((input, init) => authCall(guardedFetch, [input, resourceInit(input, init)])))), () => {
436
+ guardedTransport(true);
437
+ });
438
+ const transport = new StreamableHTTPClientTransport(url, {
439
+ authProvider: oauthProvider,
440
+ onInsufficientScope: "throw",
441
+ fetch: authMethod((input, init) => authFlow(function* () {
442
+ const response = (yield authCall(coordinatedFetch, [input, init]));
443
+ if (response.status === 401) {
444
+ let owner;
445
+ if (typeof init?.body === "string") {
446
+ try {
447
+ const rpc = JSON.parse(init.body);
448
+ if (skillObject(rpc)) {
449
+ if (rpc.method === "initialize" || rpc.method === "server/discover")
450
+ owner = initiatingSignal;
451
+ else if (typeof rpc.id === "string" || typeof rpc.id === "number")
452
+ owner = flights.get(rpc.id)?.request?.ctx.signal;
453
+ }
454
+ }
455
+ catch { }
456
+ }
457
+ const ownedSignal = owner && init?.signal ? AbortSignal.any([owner, init.signal]) : (owner ?? init?.signal);
458
+ if (ownedSignal)
459
+ renewalSignals.set(response, ownedSignal);
460
+ }
461
+ return response;
462
+ })),
463
+ });
464
+ rt.boundOAuthFlows(ctx, transport, url, [signal, ctx.signal], (response) => renewalSignals.get(response));
465
+ earlyRenewals.set(transport, authMethod((due, owner) => {
466
+ // Exactly the flow a 401 starts, through the same refresh coordinator,
467
+ // with nothing sent to the MCP endpoint. The token request carries
468
+ // `due`, so a rotation another request finished is adopted instead.
469
+ const internals = transport;
470
+ const response = new Response(null, { status: 401 });
471
+ if (owner)
472
+ renewalSignals.set(response, owner);
473
+ return authInvoke(internals._authProvider, "onUnauthorized", [
474
+ { response, serverUrl: url, fetchFn: earlyRenewalFetch(internals._fetchWithInit, due) },
475
+ ]);
476
+ }));
477
+ return sendingEachWriteOnce(transport, flights, untrackedRead, era);
478
+ }
479
+ const headers = opts.auth?.type === "headers"
480
+ ? opts.auth.headers
481
+ : (credentialAuth || requestAuth) && credentialFramed !== null
482
+ ? { ...requestAuth?.headers, [credentialHeader]: credentialFramed }
483
+ : undefined;
484
+ return sendingEachWriteOnce(new StreamableHTTPClientTransport(url, {
485
+ ...(headers ? { requestInit: { headers } } : {}),
486
+ fetch: authMethod((input, init) => authCall(guardedFetch, [input, resourceInit(input, init)])),
487
+ }), flights, untrackedRead, era);
488
+ };
489
+ /**
490
+ * Admit a write's call to the wire once, whatever JSON-RPC id a later
491
+ * message for it carries, and show `trackedFetch` which call each message
492
+ * belongs to while it is sent. A missing record fails closed unless active
493
+ * calls prove the tool is read (INV-1, INV-9).
494
+ */
495
+ const sendingEachWriteOnce = (transport, flights, untrackedRead, era) => {
496
+ eraRefusals.set(transport, era);
497
+ const send = transport.send.bind(transport);
498
+ transport.send = authMethod((message, options) => authFlow(function* () {
499
+ const request = !Array.isArray(message) && "method" in message && message.method === "tools/call" && "id" in message
500
+ ? message
501
+ : undefined;
502
+ if (!request)
503
+ return (yield authCall(send, [message, options]));
504
+ const dispatch = dispatchOf(request.params);
505
+ if (!dispatch) {
506
+ if (!untrackedRead(request.params))
507
+ throw missingDispatch();
508
+ return (yield authCall(send, [message, options]));
509
+ }
510
+ if (dispatch.write && dispatch.sent)
511
+ throw resendRefused(dispatch);
512
+ flights.set(request.id, dispatch);
513
+ try {
514
+ return (yield authCall(send, [message, options]));
515
+ }
516
+ finally {
517
+ flights.delete(request.id);
518
+ }
519
+ }));
520
+ return transport;
521
+ };
522
+ // Detached exits start this bounded best-effort tail immediately;
523
+ // closeScope awaits its own tail so the core
524
+ // can pass it to the runtime's deferred channel. Terminating and closing are
525
+ // each bounded, so no close waits more than two seconds in all.
526
+ const closingSessions = new WeakMap();
527
+ // On a modern connection, adopted from a stored verdict or just negotiated, a 400 whose body is
528
+ // no recognized modern error is a legacy refusal (see `trackedFetch`): the
529
+ // verdict is forgotten and the client retired.
530
+ const eraRefusals = new WeakMap();
531
+ const eraRefused = new WeakSet();
532
+ const closeConnection = (client, transport, logger) => Effect.suspend(() => {
533
+ const previous = transport && closingSessions.get(transport);
534
+ if (previous)
535
+ return Deferred.await(previous);
536
+ const closed = Deferred.makeUnsafe();
537
+ // A connect can acquire a session after an early close. Deduplicate only
538
+ // once that session exists, so its late abandonment still sends DELETE.
539
+ if (transport?.sessionId)
540
+ closingSessions.set(transport, closed);
541
+ return (transport ? terminateSession(transport, logger, id) : Effect.void).pipe(Effect.andThen(closeLocally(client, transport)), Effect.ensuring(Deferred.done(closed, Exit.void)));
542
+ });
543
+ return { trackSentRequest, eraRefused, closeConnection, buildTransport, eraRefusals, earlyRenewals, activeContexts };
544
+ }