@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,469 @@
1
+ // The downstream OAuth surface: bounded SDK flows, early renewal before a
2
+ // call, unchallenged consent starts, and the connector's auth operations.
3
+ import { authView } from "../../auth/native-call.js";
4
+ import { authFlow, authCall, authInvoke, authMethod } from "../../auth/native.js";
5
+ import { viewSignal } from "../../runtime/call.js";
6
+ import { OAuthError, RegistrationRejectedError, extractWWWAuthenticateParams, UnauthorizedError, } from "@modelcontextprotocol/client";
7
+ import { Effect } from "effect";
8
+ import { authorizingContext, EarlyRenewals } from "../../auth/downstream-oauth.js";
9
+ import { byteReadResponse } from "../../byte-read-response.js";
10
+ import { requestSentSecrets, sentSecretsFor } from "../../sent-secrets.js";
11
+ import { ConnectorCallError, resendAdvice, WithheldTextError } from "../../errors.js";
12
+ import { attachFailureFacts, carryFailureFacts, OAUTH_ERROR_CODES } from "../../operator-record.js";
13
+ import { detach } from "../../runtime/run.js";
14
+ import { retryAfterMs } from "../../http-policy.js";
15
+ import { sendCredentialPost } from "../../guarded-transport.js";
16
+ import { UnboundOAuthFlowsError, downstreamCallError, errorKind, hasSdkPayload, ownAbortReason, requiresAuthorization, startMessage, withheldAs, } from "./errors.js";
17
+ import { RemoteMcpDestinationError, learnedUrlSafeFetch } from "./transport.js";
18
+ /**
19
+ * Record each OAuth request on `trail` before sending it. Pinned against
20
+ * `@modelcontextprotocol/client` 2.3.1: every request the SDK sends anywhere
21
+ * but the MCP endpoint is a discovery GET, a token request (a form carrying
22
+ * `grant_type`), or the JSON POST of dynamic client registration.
23
+ */
24
+ function tracedOAuthFetch(trail, endpoint, baseFetch) {
25
+ return authMethod((input, init) => authFlow(function* () {
26
+ const url = new URL(input);
27
+ if (url.href === endpoint.href) {
28
+ trail.last = undefined;
29
+ }
30
+ else {
31
+ const body = init?.body;
32
+ const step = (init?.method ?? "GET").toUpperCase() === "GET"
33
+ ? "discovery"
34
+ : body instanceof URLSearchParams ||
35
+ (typeof body === "string" && new URLSearchParams(body).has("grant_type"))
36
+ ? "token request"
37
+ : "client registration";
38
+ trail.last = { step, host: url.origin };
39
+ if (step === "client registration")
40
+ trail.registration = url.origin;
41
+ }
42
+ const leg = trail.last;
43
+ const response = (yield authCall(baseFetch, [input, init]));
44
+ if (leg) {
45
+ leg.httpStatus = response.status;
46
+ const wait = retryAfterMs(response.headers);
47
+ if (wait !== undefined)
48
+ leg.retryAfterMs = wait;
49
+ }
50
+ return byteReadResponse(response);
51
+ }));
52
+ }
53
+ function registrationErrorCode(body) {
54
+ try {
55
+ const code = JSON.parse(body)?.error;
56
+ return typeof code === "string" && OAUTH_ERROR_CODES.has(code) ? code : undefined;
57
+ }
58
+ catch {
59
+ return undefined;
60
+ }
61
+ }
62
+ /** One connector's OAuth flows, renewals and authorization operations. */
63
+ export function remoteMcpOAuth(rt) {
64
+ const { destination, staticClient, id, opts } = rt;
65
+ // A connection opened earlier in the request may still hold a replaced
66
+ // token, and one that lives under a minute is due again at once: the
67
+ // invocation's check renews it, and `callTool` sends it while it is unexpired.
68
+ const earlyRenewed = new EarlyRenewals();
69
+ /**
70
+ * Tell a failure of one OAuth flow in connecta's words.
71
+ *
72
+ * The SDK builds these errors from what a downstream or its authorization
73
+ * server sent: a refused registration quotes the response body whole, an
74
+ * issuer mismatch quotes the issuers, a discovery failure the URL or the
75
+ * metadata it could not accept. Any of them can echo what the request
76
+ * carried, or text the server planted for an agent to read. What survives
77
+ * is the step and host of the flow's last request, the classification, and
78
+ * for a registration the status and a registered OAuth error code.
79
+ * Anything not already in connecta's words is withheld, including a store's
80
+ * error while the flow saves what it discovered, which can quote it.
81
+ */
82
+ const withoutAuthorizationServerText = authMethod((ctx, err, trail, signals) => authFlow(function* () {
83
+ const safeOrigin = authMethod((host) => authFlow(function* () {
84
+ if (!host || sentSecretsFor(ctx).contains(host, true))
85
+ return undefined;
86
+ if (host === destination.origin || (staticClient && host === new URL(staticClient.issuer).origin))
87
+ return host;
88
+ try {
89
+ return host ===
90
+ (yield authInvoke(rt.newProvider(ctx), "validatedIssuerOrigin", []))
91
+ ? host
92
+ : undefined;
93
+ }
94
+ catch {
95
+ return undefined;
96
+ }
97
+ }));
98
+ if (ownAbortReason(err, signals) && !hasSdkPayload(err))
99
+ return err;
100
+ if (err instanceof RegistrationRejectedError) {
101
+ const code = registrationErrorCode(err.body);
102
+ const origin = (yield authCall(safeOrigin, [trail.registration]));
103
+ const facts = {
104
+ step: "OAuth client registration",
105
+ ...(origin ? { origin } : {}),
106
+ httpStatus: err.status,
107
+ ...(code ? { oauthError: code } : {}),
108
+ };
109
+ return attachFailureFacts(carryFailureFacts(err, resendAdvice(withheldAs(`Connector "${id}" could not register an OAuth client with ` +
110
+ `${origin ?? "its authorization server"}: the ` +
111
+ `registration endpoint answered HTTP ${err.status}` +
112
+ `${code ? ` with OAuth error ${code}` : ""}. Its response is ` +
113
+ "withheld because it can quote the request or anything the server " +
114
+ "chose to add. Check the server's client registration policy.", downstreamCallError(err, undefined, trail.last?.retryAfterMs)), ", then retry authorization")), facts);
115
+ }
116
+ const leg = trail.last;
117
+ const origin = (yield authCall(safeOrigin, [leg?.host]));
118
+ const facts = leg
119
+ ? {
120
+ step: `OAuth ${leg.step}`,
121
+ ...(origin ? { origin } : {}),
122
+ ...(leg.httpStatus ? { httpStatus: leg.httpStatus } : {}),
123
+ }
124
+ : { step: "OAuth flow" };
125
+ if (err instanceof UnauthorizedError || err instanceof ConnectorCallError || err instanceof WithheldTextError) {
126
+ return attachFailureFacts(downstreamCallError(err), facts);
127
+ }
128
+ const classified = downstreamCallError(err, leg?.httpStatus, leg?.retryAfterMs, leg?.step);
129
+ if (err instanceof OAuthError ||
130
+ (classified instanceof ConnectorCallError && classified.code === "provider_permission_denied")) {
131
+ return attachFailureFacts(carryFailureFacts(err, classified), facts);
132
+ }
133
+ return attachFailureFacts(carryFailureFacts(err, resendAdvice(withheldAs(`Connector "${id}" OAuth ${leg ? `${leg.step}${origin ? ` with ${origin}` : ""}` : "flow"} ` +
134
+ `failed${errorKind(err)}. The error is withheld because its text can ` +
135
+ "quote what the server sent. Check the server's OAuth metadata.", classified), ", then retry authorization")), facts);
136
+ }));
137
+ /**
138
+ * Run every OAuth flow the SDK starts on `transport` inside a boundary of
139
+ * its own: a trail only that flow's requests write to, and
140
+ * `withoutAuthorizationServerText` on whatever the flow throws. Calls on
141
+ * one client can each start a flow at once, so no flow's failure is ever
142
+ * told by another flow's last request.
143
+ *
144
+ * Pinned against `@modelcontextprotocol/client` 2.3.1, whose transport
145
+ * offers no public seam for this. A flow starts in exactly two places, each
146
+ * handing `auth()` the fetch to use: the adapted provider's
147
+ * `onUnauthorized` (a 401, at connect or on a live client), given the fetch
148
+ * in its context, and `_stepUpAuthorize` (a 403 `insufficient_scope`),
149
+ * which reads `this._fetchWithInit`. The callback's code exchange runs
150
+ * outside both, and the callback route repeats nothing it throws. If an
151
+ * upgrade moves either seam, this refuses to build the transport rather
152
+ * than run flows unbounded.
153
+ */
154
+ const boundOAuthFlows = (ctx, transport, endpoint,
155
+ /** The request's and connection's signals: only their reasons pass as written. */
156
+ signals, renewalSignal) => {
157
+ const internals = transport;
158
+ const adapted = internals._authProvider;
159
+ const onUnauthorized = adapted?.onUnauthorized;
160
+ const stepUp = internals._stepUpAuthorize;
161
+ if (!adapted || typeof onUnauthorized !== "function" || typeof stepUp !== "function") {
162
+ // Its own class, so that a status record, which carries no message, says so.
163
+ throw new UnboundOAuthFlowsError(`[connecta] connector "${id}": the MCP client's transport no longer ` +
164
+ "has the OAuth flow seams this release is pinned against, so its " +
165
+ "flows cannot be bounded. Pin @modelcontextprotocol/client to the " +
166
+ "version this release ships with.");
167
+ }
168
+ const flow = authMethod((start, signal) => authFlow(function* () {
169
+ const trail = {};
170
+ try {
171
+ return (yield authCall(start, [
172
+ (fetchFn) => tracedOAuthFetch(trail, endpoint, authMethod((input, init) => authFlow(function* () {
173
+ rt.trackSentRequest(ctx, input, init);
174
+ return (yield authCall(fetchFn, [
175
+ input,
176
+ signal
177
+ ? {
178
+ ...init,
179
+ signal: init?.signal ? AbortSignal.any([signal, init.signal]) : signal,
180
+ }
181
+ : init,
182
+ ]));
183
+ }))),
184
+ ]));
185
+ }
186
+ catch (err) {
187
+ throw (yield authCall(withoutAuthorizationServerText, [ctx, err, trail, [...signals, signal]]));
188
+ }
189
+ }));
190
+ adapted.onUnauthorized = authMethod((ctx) => authCall(flow, [
191
+ authMethod((trace) => authFlow(function* () {
192
+ // This seam receives every 401, during status, starts, and live
193
+ // calls. Use the SDK's parser so the check covers exactly the
194
+ // challenged URL it would honor. The provider separately drops
195
+ // foreign cached resource metadata on every SDK discovery read,
196
+ // including callbacks and flows without a challenge.
197
+ const { resourceMetadataUrl } = extractWWWAuthenticateParams(ctx.response);
198
+ if (resourceMetadataUrl && resourceMetadataUrl.origin !== destination.origin) {
199
+ throw new RemoteMcpDestinationError(id, "resource metadata outside the configured resource origin is not allowed");
200
+ }
201
+ yield authInvoke(Reflect, "apply", [
202
+ onUnauthorized,
203
+ adapted,
204
+ [{ ...ctx, fetchFn: trace(ctx.fetchFn ?? fetch) }],
205
+ ]);
206
+ })),
207
+ renewalSignal(ctx.response),
208
+ ]));
209
+ internals._stepUpAuthorize = authMethod((challenge, retries) => authCall(flow, [
210
+ authMethod((trace) => authFlow(function* () {
211
+ const traced = trace(internals._fetchWithInit ?? fetch);
212
+ const self = new Proxy(transport, {
213
+ get: (target, key) => key === "_fetchWithInit" ? traced : Reflect.get(target, key, target),
214
+ set: (target, key, value) => Reflect.set(target, key, value, target),
215
+ });
216
+ return (yield authInvoke(Reflect, "apply", [stepUp, self, [challenge, retries]]));
217
+ })),
218
+ ]));
219
+ };
220
+ const disconnectAuthorization = authMethod((ctx, state, operatorDisconnected = false, preserveClient = false) => authFlow(function* () {
221
+ const provider = rt.newProvider(ctx, state);
222
+ const reset = operatorDisconnected
223
+ ? authInvoke(provider, "disconnectAuthorization", [
224
+ learnedUrlSafeFetch(id, new URL(opts.url), authMethod((input, init) => authFlow(function* () {
225
+ rt.trackSentRequest(ctx, input, init);
226
+ return (yield authCall(sendCredentialPost, [input, init ?? {}]));
227
+ }))),
228
+ ])
229
+ : authInvoke(provider, "resetAuthorization", [false, preserveClient]);
230
+ try {
231
+ yield reset;
232
+ }
233
+ finally {
234
+ rt.closeHalf(state);
235
+ }
236
+ }));
237
+ /**
238
+ * Whether the owner's grant holds tokens, read the way the invocation gate
239
+ * reads it. A downstream may answer its handshake and listing without
240
+ * credentials, so a healthy connection proves nothing about the grant.
241
+ * Read after connecting: connect has bound the epoch, migrated an older
242
+ * layout, and refused a disconnected one within the scope's lifetime.
243
+ */
244
+ const granted = authMethod((provider) => authFlow(function* () {
245
+ return Boolean(yield authInvoke(provider, "tokens", []));
246
+ }));
247
+ /**
248
+ * Renew `due`, an access token within a minute of its stored expiry,
249
+ * through `transport`'s bound 401 flow and the shared refresh coordinator.
250
+ * Nothing is sent to the MCP endpoint. Only calls come here: status,
251
+ * listing and discovery never renew (INV-10).
252
+ */
253
+ const renewEarly = authMethod((ctx, transport, provider, due) => authFlow(function* () {
254
+ const renew = rt.earlyRenewals.get(transport);
255
+ if (!renew)
256
+ return;
257
+ try {
258
+ (yield authCall(renew, [due, ctx.signal]));
259
+ const fresh = provider.adoptedRefresh();
260
+ if (fresh !== undefined)
261
+ earlyRenewed.record(ctx.requestScope ?? ctx, due, fresh, provider.observedExpiry(fresh));
262
+ }
263
+ catch (err) {
264
+ if (ownAbortReason(err, [ctx.signal]) && !hasSdkPayload(err))
265
+ throw err;
266
+ throw err instanceof UnauthorizedError
267
+ ? carryFailureFacts(err, rt.authRequiredError(ctx))
268
+ : downstreamCallError(err, undefined, undefined, undefined, requestSentSecrets(ctx));
269
+ }
270
+ }));
271
+ /**
272
+ * The invocation's renewal, before it enters `callTool`: a failure there is
273
+ * the call's authorization verdict with nothing sent, so a write never
274
+ * draws the 401 it could not be repeated after (INV-9). The transport is
275
+ * built for its flow and never started, so it holds nothing to close.
276
+ */
277
+ const renewBeforeCall = authMethod((ctx, due) => authFlow(function* () {
278
+ if (opts._transportFactory)
279
+ return;
280
+ const provider = rt.newProvider(ctx);
281
+ (yield authInvoke(provider, "beginFlow", []));
282
+ (yield authCall(renewEarly, [ctx, rt.buildTransport(ctx, provider), provider, due]));
283
+ }));
284
+ /**
285
+ * A connection reused later in the request renews again once its token
286
+ * comes due, before `callTool` sends it. The grant's last read decides
287
+ * (`KvOAuthProvider.renewalDue`, memory only).
288
+ */
289
+ const renewBeforeDispatch = authMethod((ctx, state, client) => authFlow(function* () {
290
+ const provider = rt.connectedProviders.get(client);
291
+ const due = provider?.renewalDue();
292
+ if (due === undefined || !provider || !client.transport)
293
+ return;
294
+ if (earlyRenewed.settled(ctx.requestScope ?? ctx, due))
295
+ return;
296
+ try {
297
+ (yield authCall(renewEarly, [ctx, client.transport, provider, due]));
298
+ }
299
+ catch (err) {
300
+ if (requiresAuthorization(err) && state.client === client)
301
+ state.authRequired = true;
302
+ throw err;
303
+ }
304
+ }));
305
+ /**
306
+ * Start consent for a downstream that answered without challenging. The
307
+ * transport's bound 401 seam runs the SDK flow exactly as a challenge
308
+ * without `resource_metadata` would: RFC 9728 discovery at the resource's
309
+ * well-known URLs, the same issuer and resource checks, consent state, and
310
+ * CIMD/DCR/static client order. Nothing is sent to the MCP endpoint.
311
+ */
312
+ const startUnchallenged = authMethod((ctx, connectedProvider) => authFlow(function* () {
313
+ const required = {
314
+ state: "auth_required",
315
+ message: "Authorization required \u2014 open the URL to connect.",
316
+ };
317
+ // A test transport carries no OAuth flow to start.
318
+ if (opts._transportFactory)
319
+ return required;
320
+ // A cached connection may belong to a passive status or call. Give this
321
+ // explicit start its own authorization-capable provider without widening
322
+ // the transport's authority or adopting a Restart's replacement epoch.
323
+ const epoch = (yield authInvoke(connectedProvider, "flowEpoch", []));
324
+ const provider = rt.newProvider(ctx);
325
+ (yield authInvoke(provider, "beginFlow", [epoch]));
326
+ // Never started, this transport holds no session or stream to close.
327
+ const transport = rt.buildTransport(ctx, provider);
328
+ const internals = transport;
329
+ try {
330
+ (yield authInvoke(internals._authProvider, "onUnauthorized", [
331
+ {
332
+ response: new Response(null, { status: 401 }),
333
+ serverUrl: new URL(opts.url),
334
+ fetchFn: internals._fetchWithInit,
335
+ },
336
+ ]));
337
+ // Authorized without consent: nothing is pending to hand back.
338
+ return { state: "ok", message: "Already authorized \u2014 connection is healthy." };
339
+ }
340
+ catch (err) {
341
+ if (!(err instanceof UnauthorizedError))
342
+ throw err;
343
+ }
344
+ const authorizationUrl = (yield authInvoke(provider, "consentUrl", [viewSignal(ctx, true)]));
345
+ return { ...required, ...(authorizationUrl !== undefined ? { authorizationUrl } : {}) };
346
+ }));
347
+ const oauthSurface = (connector) => {
348
+ connector.finishAuth = authMethod((code, ctx, callbackParams) => authView(ctx, authMethod((signal) => authFlow(function* () {
349
+ const state = rt.stateFor(ctx);
350
+ const provider = rt.newProvider(ctx, state);
351
+ const consent = callbackParams?.get("state") ?? null;
352
+ if (!consent || !(yield authInvoke(provider, "verifyCallbackState", [consent]))) {
353
+ throw new ConnectorCallError("connector_call_failed", `Connector "${id}" authorization callback matches no pending consent; nothing was exchanged.`);
354
+ }
355
+ provider.validateCallbackIssuer(callbackParams?.get("iss") ?? null);
356
+ (yield authInvoke(provider, "bindFlow", []));
357
+ signal.throwIfAborted();
358
+ const t = rt.buildTransport(ctx, provider, null, signal, null, true);
359
+ const trail = {};
360
+ const internals = t;
361
+ const traced = tracedOAuthFetch(trail, new URL(opts.url), internals._fetchWithInit ?? fetch);
362
+ const exchange = new Proxy(t, {
363
+ get: (target, key) => key === "_fetchWithInit" ? traced : Reflect.get(target, key, target),
364
+ set: (target, key, value) => Reflect.set(target, key, value, target),
365
+ });
366
+ try {
367
+ (yield authInvoke(exchange, "finishAuth", [callbackParams]));
368
+ rt.closeHalf(state);
369
+ }
370
+ catch (err) {
371
+ throw (yield authCall(withoutAuthorizationServerText, [
372
+ ctx,
373
+ err,
374
+ trail,
375
+ [signal, ctx.signal],
376
+ ]));
377
+ }
378
+ finally {
379
+ detach(rt.closeConnection(null, t, ctx.logger));
380
+ }
381
+ })), true));
382
+ connector.verifyState = authMethod((oauthState, ctx) => authFlow(function* () {
383
+ const state = rt.stateFor(ctx);
384
+ return (yield authInvoke(rt.newProvider(ctx, state), "verifyState", [oauthState]));
385
+ }));
386
+ connector.verifyCallbackIssuer = authMethod((issuer, ctx) => Effect.try({
387
+ try: () => {
388
+ rt.newProvider(ctx, rt.stateFor(ctx)).validateCallbackIssuer(issuer);
389
+ return true;
390
+ },
391
+ catch: (error) => error,
392
+ }));
393
+ connector.consumeAuthError = authMethod((ctx) => authFlow(function* () {
394
+ (yield authInvoke(rt.newProvider(ctx, rt.stateFor(ctx)), "consumeAuthError", []));
395
+ }));
396
+ connector.disconnectAuth = authMethod((ctx) => authFlow(function* () {
397
+ (yield authCall(disconnectAuthorization, [ctx, rt.stateFor(ctx), true]));
398
+ }));
399
+ connector.startAuth = authMethod((ctx, startOpts) => authFlow(function* () {
400
+ ctx = authorizingContext(ctx);
401
+ const state = rt.stateFor(ctx);
402
+ state.provider = null;
403
+ const p = rt.newProvider(ctx, state);
404
+ if (ctx.signal?.aborted)
405
+ throw ctx.signal.reason;
406
+ const disconnected = startOpts?.force
407
+ ? false
408
+ : (yield authInvoke(p, "operatorDisconnected", []));
409
+ if (ctx.signal?.aborted)
410
+ throw ctx.signal.reason;
411
+ if (startOpts?.force || disconnected) {
412
+ (yield authCall(disconnectAuthorization, [ctx, state, false, startOpts?.force === true]));
413
+ }
414
+ else {
415
+ const pending = (yield authInvoke(p, "reusablePendingAuthorizationUrl", []));
416
+ if (pending) {
417
+ return {
418
+ state: "auth_required",
419
+ authorizationUrl: pending,
420
+ authorizationReused: true,
421
+ message: "Authorization required \u2014 open the URL to connect.",
422
+ };
423
+ }
424
+ }
425
+ if (ctx.signal?.aborted)
426
+ throw ctx.signal.reason;
427
+ try {
428
+ // A passive call's refusal stays latched for status and calls. A
429
+ // successful connection with no tokens still permits explicit
430
+ // consent, bound to that connection rather than the live grant.
431
+ if (state.authRequired && state.client) {
432
+ const provider = rt.connectedProviders.get(state.client);
433
+ if (!(yield authCall(granted, [provider])))
434
+ return (yield authCall(startUnchallenged, [ctx, provider]));
435
+ }
436
+ // The handshake runs first, so a 401 challenge's
437
+ // `resource_metadata` starts consent as it always has.
438
+ const client = (yield authCall(rt.ensureConnected, [ctx, state]));
439
+ const provider = rt.connectedProviders.get(client);
440
+ if (!(yield authCall(granted, [provider])))
441
+ return (yield authCall(startUnchallenged, [ctx, provider]));
442
+ return {
443
+ state: "ok",
444
+ message: "Already authorized \u2014 connection is healthy.",
445
+ };
446
+ }
447
+ catch (err) {
448
+ if (state.authRequired && requiresAuthorization(err)) {
449
+ let authorizationUrl;
450
+ try {
451
+ authorizationUrl = (yield state.attemptProvider
452
+ ? authInvoke(state.attemptProvider, "consentUrl", [viewSignal(ctx, true)])
453
+ : Effect.succeed(undefined));
454
+ }
455
+ catch (readErr) {
456
+ return { state: "error", message: startMessage(readErr) };
457
+ }
458
+ return {
459
+ state: "auth_required",
460
+ ...(authorizationUrl !== undefined ? { authorizationUrl } : {}),
461
+ message: "Authorization required \u2014 open the URL to connect.",
462
+ };
463
+ }
464
+ return { state: "error", message: startMessage(err) };
465
+ }
466
+ }));
467
+ };
468
+ return { boundOAuthFlows, renewBeforeDispatch, granted, oauthSurface, renewBeforeCall };
469
+ }