@zackbart/connecta 0.28.0 → 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 (320) hide show
  1. package/CHANGELOG.md +961 -5
  2. package/README.md +62 -39
  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 +1632 -1509
  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 +526 -0
  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 +220 -0
  52. package/dist/connectors/api-connector.js +214 -0
  53. package/dist/connectors/api.d.ts +8 -71
  54. package/dist/connectors/api.js +13 -99
  55. package/dist/connectors/guarded-fetch.d.ts +144 -0
  56. package/dist/connectors/guarded-fetch.js +108 -25
  57. package/dist/connectors/negotiation-cache.js +46 -0
  58. package/dist/connectors/option-shapes.js +27 -0
  59. package/dist/connectors/remote-mcp.d.ts +30 -4
  60. package/dist/connectors/remote-mcp.js +1780 -351
  61. package/dist/connectors/resource-uri.js +335 -0
  62. package/dist/credential-contract.d.ts +10 -0
  63. package/dist/credential-rules.js +1 -3
  64. package/dist/credential-url.js +108 -0
  65. package/dist/credentials.d.ts +6 -1
  66. package/dist/credentials.js +27 -18
  67. package/dist/d1.d.ts +130 -0
  68. package/dist/d1.js +290 -0
  69. package/dist/describe-config.d.ts +165 -0
  70. package/dist/describe-config.js +315 -0
  71. package/dist/described.js +106 -0
  72. package/dist/downstream-input-context.js +119 -0
  73. package/dist/downstream-input.js +323 -0
  74. package/dist/errors.d.ts +97 -20
  75. package/dist/errors.js +158 -80
  76. package/dist/execute.js +453 -305
  77. package/dist/executor-admission.d.ts +1 -0
  78. package/dist/executor-admission.js +11 -15
  79. package/dist/executor-contract.d.ts +20 -0
  80. package/dist/executor-contract.js +48 -0
  81. package/dist/executor-result.js +24 -9
  82. package/dist/executors/quickjs-child.js +3 -4
  83. package/dist/executors/quickjs-runtime.js +147 -59
  84. package/dist/executors/quickjs.js +29 -35
  85. package/dist/favicon.js +1 -2
  86. package/dist/fix-prompt.js +1 -3
  87. package/dist/guest-runtime.js +188 -0
  88. package/dist/guest-types.d.ts +176 -0
  89. package/dist/guest-types.js +1 -0
  90. package/dist/html-security.js +24 -0
  91. package/dist/identity.js +1 -3
  92. package/dist/inbound-credential.js +52 -0
  93. package/dist/index.d.ts +25 -257
  94. package/dist/index.js +65 -478
  95. package/dist/invocation-auth.js +69 -0
  96. package/dist/invocation.js +230 -73
  97. package/dist/json-schema.d.ts +1 -1
  98. package/dist/mcp-client-context.js +35 -0
  99. package/dist/mcp-result.js +11 -7
  100. package/dist/meta-output.js +76 -0
  101. package/dist/meta-tool-names.d.ts +2 -0
  102. package/dist/meta-tool-names.js +3 -0
  103. package/dist/meta-tools.js +285 -209
  104. package/dist/module-contracts.d.ts +8 -23
  105. package/dist/node.d.ts +0 -2
  106. package/dist/node.js +2 -2
  107. package/dist/oauth-callback-outcome.js +1 -3
  108. package/dist/oauth-handoff.js +117 -0
  109. package/dist/oauth-partition.js +17 -0
  110. package/dist/oauth-sealing.js +20 -0
  111. package/dist/operator-record.d.ts +137 -0
  112. package/dist/operator-record.js +420 -0
  113. package/dist/operator-ui/assets.js +31 -0
  114. package/dist/operator-ui/contract.d.ts +72 -0
  115. package/dist/operator-ui/contract.js +1 -0
  116. package/dist/operator-ui/fix-prompts.d.ts +18 -0
  117. package/dist/operator-ui/generated.js +5 -4
  118. package/dist/operator-ui/model.d.ts +12 -87
  119. package/dist/operator-ui/model.js +2 -9
  120. package/dist/operator-ui/setup-commands.js +1 -3
  121. package/dist/operator-ui/view.d.ts +323 -0
  122. package/dist/operator-ui/view.js +62 -189
  123. package/dist/page-styles.js +2 -3
  124. package/dist/program-source.js +14 -2
  125. package/dist/{exempt-writes.js → program-writes.js} +55 -18
  126. package/dist/provider.d.ts +142 -0
  127. package/dist/provider.js +187 -0
  128. package/dist/providers/_shared/google/result-size.js +57 -0
  129. package/dist/providers/_shared/google/service-account.d.ts +58 -0
  130. package/dist/providers/_shared/google/service-account.js +500 -0
  131. package/dist/providers/_shared/google/workspace.d.ts +276 -0
  132. package/dist/providers/_shared/google/workspace.js +518 -0
  133. package/dist/providers/basecamp/index.d.ts +43 -0
  134. package/dist/providers/basecamp/index.js +997 -0
  135. package/dist/providers/basecamp/skill.generated.js +9 -0
  136. package/dist/providers/breeze/index.d.ts +34 -0
  137. package/dist/providers/breeze/index.js +1446 -0
  138. package/dist/providers/breeze/skill.generated.js +9 -0
  139. package/dist/providers/ccb/index.d.ts +103 -0
  140. package/dist/providers/ccb/index.js +1725 -0
  141. package/dist/providers/ccb/skill.generated.js +9 -0
  142. package/dist/providers/{cloudflare.d.ts → cloudflare/index.d.ts} +8 -7
  143. package/dist/providers/{cloudflare.js → cloudflare/index.js} +171 -738
  144. package/dist/providers/cloudflare/skill.generated.js +10 -0
  145. package/dist/providers/docs/index.d.ts +54 -0
  146. package/dist/providers/docs/index.js +1237 -0
  147. package/dist/providers/docs/skill.generated.js +9 -0
  148. package/dist/providers/drive/index.d.ts +53 -0
  149. package/dist/providers/drive/index.js +1523 -0
  150. package/dist/providers/drive/skill.generated.js +9 -0
  151. package/dist/providers/forms/index.d.ts +54 -0
  152. package/dist/providers/forms/index.js +1217 -0
  153. package/dist/providers/forms/skill.generated.js +11 -0
  154. package/dist/providers/github/auth.d.ts +32 -0
  155. package/dist/providers/github/auth.js +361 -0
  156. package/dist/providers/github/hosted.js +218 -0
  157. package/dist/providers/github/index.d.ts +13 -0
  158. package/dist/providers/github/index.js +140 -0
  159. package/dist/providers/github/key.js +100 -0
  160. package/dist/providers/github/rest.js +321 -0
  161. package/dist/providers/github/scope.d.ts +29 -0
  162. package/dist/providers/github/scope.js +124 -0
  163. package/dist/providers/github/skill.generated.js +8 -0
  164. package/dist/providers/gmail/index.d.ts +51 -0
  165. package/dist/providers/gmail/index.js +1436 -0
  166. package/dist/providers/gmail/skill.generated.js +9 -0
  167. package/dist/providers/{linear.d.ts → linear/index.d.ts} +11 -14
  168. package/dist/providers/linear/index.js +352 -0
  169. package/dist/providers/linear/skill.generated.js +8 -0
  170. package/dist/providers/{mixpanel.d.ts → mixpanel/index.d.ts} +5 -10
  171. package/dist/providers/mixpanel/index.js +416 -0
  172. package/dist/providers/mixpanel/skill.generated.js +9 -0
  173. package/dist/providers/{notion.d.ts → notion/index.d.ts} +8 -8
  174. package/dist/providers/{notion.js → notion/index.js} +226 -239
  175. package/dist/providers/notion/skill.generated.js +10 -0
  176. package/dist/providers/overflow/index.d.ts +60 -0
  177. package/dist/providers/overflow/index.js +1535 -0
  178. package/dist/providers/overflow/skill.generated.js +8 -0
  179. package/dist/providers/planning-center/index.d.ts +59 -0
  180. package/dist/providers/planning-center/index.js +2199 -0
  181. package/dist/providers/planning-center/skill.generated.js +10 -0
  182. package/dist/providers/{revenuecat.d.ts → revenuecat/index.d.ts} +5 -10
  183. package/dist/providers/revenuecat/index.js +557 -0
  184. package/dist/providers/revenuecat/skill.generated.js +11 -0
  185. package/dist/providers/sheets/index.d.ts +54 -0
  186. package/dist/providers/sheets/index.js +1237 -0
  187. package/dist/providers/sheets/skill.generated.js +10 -0
  188. package/dist/providers/slides/index.d.ts +53 -0
  189. package/dist/providers/slides/index.js +2645 -0
  190. package/dist/providers/slides/skill.generated.js +9 -0
  191. package/dist/providers/{stripe.d.ts → stripe/index.d.ts} +5 -12
  192. package/dist/providers/stripe/index.js +266 -0
  193. package/dist/providers/stripe/skill.generated.js +11 -0
  194. package/dist/providers/tithely/index.d.ts +49 -0
  195. package/dist/providers/tithely/index.js +1003 -0
  196. package/dist/providers/tithely/skill.generated.js +8 -0
  197. package/dist/providers/{vercel.d.ts → vercel/index.d.ts} +8 -8
  198. package/dist/providers/vercel/index.js +1038 -0
  199. package/dist/providers/vercel/mcp-ownership.js +212 -0
  200. package/dist/providers/vercel/skill.generated.js +11 -0
  201. package/dist/registry.d.ts +108 -171
  202. package/dist/registry.js +513 -925
  203. package/dist/request-state.js +21 -0
  204. package/dist/resource-read.js +100 -0
  205. package/dist/result-shapes.js +17 -49
  206. package/dist/retired-toolkits.js +1 -1
  207. package/dist/routes/access-tokens.js +3 -6
  208. package/dist/routes/activity-context.js +21 -0
  209. package/dist/routes/activity-disclosure.js +40 -0
  210. package/dist/routes/activity.js +59 -21
  211. package/dist/routes/actor-labels.js +3 -50
  212. package/dist/routes/connect.js +221 -0
  213. package/dist/routes/credentials.js +15 -27
  214. package/dist/routes/mcp.js +356 -130
  215. package/dist/routes/oauth-client-metadata.js +15 -0
  216. package/dist/routes/oauth-management.js +45 -107
  217. package/dist/routes/oauth.js +136 -57
  218. package/dist/routes/operator.js +3 -3
  219. package/dist/routes/shared.d.ts +25 -64
  220. package/dist/routes/shared.js +160 -80
  221. package/dist/routes/skills.js +30 -0
  222. package/dist/routes/ui-config.js +225 -0
  223. package/dist/routes/ui-data.js +40 -35
  224. package/dist/routes/ui.js +66 -56
  225. package/dist/runtime/admission.js +4 -2
  226. package/dist/runtime/call-admission.js +3 -1
  227. package/dist/runtime/connector-scope.js +1 -4
  228. package/dist/runtime/run.js +10 -39
  229. package/dist/runtime/services.js +12 -117
  230. package/dist/runtime/shared-read.js +1 -3
  231. package/dist/runtime/storage.js +8 -1
  232. package/dist/sent-secrets.d.ts +52 -0
  233. package/dist/sent-secrets.js +474 -0
  234. package/dist/server.js +28 -71
  235. package/dist/skills.js +406 -174
  236. package/dist/sqlite.d.ts +48 -0
  237. package/dist/sqlite.js +166 -0
  238. package/dist/storage/keys.js +428 -0
  239. package/dist/storage/memory.js +7 -0
  240. package/dist/storage/sql.d.ts +66 -0
  241. package/dist/storage/sql.js +534 -0
  242. package/dist/timeout.js +4 -2
  243. package/dist/tool-name.js +17 -0
  244. package/dist/tool-safety.d.ts +9 -35
  245. package/dist/tool-safety.js +20 -40
  246. package/dist/types.d.ts +320 -54
  247. package/dist/types.js +0 -1
  248. package/dist/ui.d.ts +7 -19
  249. package/dist/ui.js +76 -90
  250. package/dist/url-safety.js +3 -6
  251. package/dist/usage-guide.js +137 -0
  252. package/dist/validate.d.ts +7 -0
  253. package/dist/validate.js +309 -47
  254. package/dist/version.d.ts +1 -7
  255. package/dist/version.js +3 -7
  256. package/dist/worker.d.ts +11 -0
  257. package/dist/worker.js +434 -0
  258. package/documentation/architecture.md +665 -230
  259. package/documentation/auth.md +962 -458
  260. package/documentation/code-mode.md +501 -312
  261. package/documentation/meta-tools.md +449 -200
  262. package/documentation/operator-tests.md +93 -0
  263. package/documentation/provider-migration-0.29.md +60 -0
  264. package/examples/worker/AGENTS.md +34 -12
  265. package/examples/worker/README.md +286 -255
  266. package/examples/worker/kv-to-d1.wrangler.jsonc +26 -0
  267. package/examples/worker/scripts/copy-kv-to-d1.mjs +162 -0
  268. package/examples/worker/src/connecta.config.ts +151 -0
  269. package/examples/worker/src/index.ts +14 -197
  270. package/examples/worker/wrangler.jsonc +15 -35
  271. package/package.json +125 -35
  272. package/templates/node/.dockerignore +1 -2
  273. package/templates/node/.env.example +20 -21
  274. package/templates/node/AGENTS.md +21 -17
  275. package/templates/node/Dockerfile +3 -5
  276. package/templates/node/README.md +133 -58
  277. package/templates/node/docker-compose.yml +11 -12
  278. package/templates/node/package.json +4 -2
  279. package/templates/node/src/connecta.config.ts +119 -0
  280. package/templates/node/src/index.ts +9 -154
  281. package/templates/node/src/provision-token.ts +17 -0
  282. package/AGENTS.md +0 -170
  283. package/dist/artifacts/connector.d.ts +0 -58
  284. package/dist/artifacts/connector.js +0 -730
  285. package/dist/artifacts/document.js +0 -86
  286. package/dist/artifacts/guide.js +0 -140
  287. package/dist/artifacts/html-scan.js +0 -329
  288. package/dist/artifacts/json.js +0 -78
  289. package/dist/artifacts/kv-store.d.ts +0 -20
  290. package/dist/artifacts/kv-store.js +0 -140
  291. package/dist/artifacts/markdown.js +0 -478
  292. package/dist/artifacts/module.d.ts +0 -37
  293. package/dist/artifacts/module.js +0 -66
  294. package/dist/artifacts/operations.d.ts +0 -197
  295. package/dist/artifacts/operations.js +0 -1002
  296. package/dist/artifacts/refresh.d.ts +0 -55
  297. package/dist/artifacts/refresh.js +0 -236
  298. package/dist/artifacts/routes.js +0 -206
  299. package/dist/artifacts/types.d.ts +0 -214
  300. package/dist/artifacts/types.js +0 -22
  301. package/dist/artifacts/validate.d.ts +0 -62
  302. package/dist/artifacts/validate.js +0 -651
  303. package/dist/artifacts.d.ts +0 -6
  304. package/dist/artifacts.js +0 -9
  305. package/dist/auth/bearer.d.ts +0 -11
  306. package/dist/auth/bearer.js +0 -50
  307. package/dist/providers/linear.js +0 -217
  308. package/dist/providers/mixpanel.js +0 -247
  309. package/dist/providers/revenuecat.js +0 -317
  310. package/dist/providers/stripe.js +0 -253
  311. package/dist/providers/vercel.js +0 -1253
  312. package/dist/storage/file.d.ts +0 -15
  313. package/dist/storage/file.js +0 -455
  314. package/ethos.md +0 -93
  315. package/examples/worker/src/cloudflare-kv.ts +0 -43
  316. package/examples/worker/src/d1-activity-row.ts +0 -155
  317. package/examples/worker/src/d1-activity.ts +0 -147
  318. package/examples/worker/src/d1-storage.ts +0 -138
  319. package/examples/worker/src/r2-artifact-blobs.ts +0 -29
  320. package/templates/node/src/file-activity.ts +0 -182
@@ -0,0 +1,526 @@
1
+ import { auth, OAuthError, UnauthorizedError } from "@modelcontextprotocol/client";
2
+ import { byteReadResponse } from "../byte-read-response.js";
3
+ import { sentSecretsFor, sentSecretsFetch, shortSecretWarning } from "../sent-secrets.js";
4
+ import { ConnectorCallError, msg } from "../errors.js";
5
+ import { oauthPartitionFor, retainOAuthPartition, retainingOAuthPartition } from "../oauth-partition.js";
6
+ import { oauthSealerFor } from "../oauth-sealing.js";
7
+ import { describeFailure } from "../operator-record.js";
8
+ import { assertOAuthScope, authorizingContext, KvOAuthProvider, refreshCoordinatorsByPartition, } from "./downstream-oauth.js";
9
+ import { trackRemoteClientRequest } from "./downstream-client-metadata.js";
10
+ import { trackOAuthStartReset } from "./oauth-start-reset.js";
11
+ /**
12
+ * Downstream OAuth for a hand-written `api()` connector: the authorization
13
+ * code grant against endpoints and a pre-registered client the deployment
14
+ * declares, rather than ones a server advertises.
15
+ *
16
+ * Everything that makes the `remoteMcp()` grant safe is reused, not copied.
17
+ * `StaticOAuthProvider` is a `KvOAuthProvider` — the same grant record and
18
+ * consents, sealing, owner partitions, epoch fence, and refresh-failure
19
+ * verdicts — whose discovery state and client are constants, and the SDK's
20
+ * own `auth()` drives it through the same `OAuthRefreshCoordinator`. What a
21
+ * static configuration subtracts is the network learning: no RFC 9728 or 8414
22
+ * discovery and no dynamic registration, so no URL here was taught to
23
+ * connecta by a downstream.
24
+ */
25
+ /** Parameters the grant itself owns; a deployment may not restate them. */
26
+ const RESERVED_AUTHORIZATION_PARAMS = new Set([
27
+ "response_type",
28
+ "client_id",
29
+ "redirect_uri",
30
+ "state",
31
+ "scope",
32
+ "code_challenge",
33
+ "code_challenge_method",
34
+ "resource",
35
+ ]);
36
+ /** Token-request headers the grant owns: client authentication and framing. */
37
+ const RESERVED_TOKEN_HEADERS = new Set([
38
+ "authorization",
39
+ "content-type",
40
+ "content-length",
41
+ "cookie",
42
+ "host",
43
+ ]);
44
+ /** An RFC 9110 field-name token. */
45
+ const HEADER_NAME = /^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/;
46
+ const CLIENT_AUTHENTICATION = new Set(["client_secret_basic", "client_secret_post", "none"]);
47
+ function isLoopback(hostname) {
48
+ return hostname === "localhost" || hostname === "127.0.0.1" || hostname === "[::1]";
49
+ }
50
+ /** An absolute HTTPS URL (or HTTP on loopback), without credentials or fragment. */
51
+ function configuredUrl(id, option, value) {
52
+ let url;
53
+ try {
54
+ url = new URL(String(value));
55
+ }
56
+ catch {
57
+ // Reported below without echoing the value.
58
+ }
59
+ if (!url ||
60
+ !(url.protocol === "https:" || (url.protocol === "http:" && isLoopback(url.hostname))) ||
61
+ url.username ||
62
+ url.password ||
63
+ url.hash) {
64
+ throw new Error(`[connecta] connector "${id}" oauth.${option} must be an absolute https URL ` +
65
+ "(http only on loopback), without credentials or a fragment.");
66
+ }
67
+ return url;
68
+ }
69
+ /** Structural mistakes throw here, where they are written. */
70
+ function settingsFor(id, config) {
71
+ if (!config || typeof config !== "object") {
72
+ throw new Error(`[connecta] connector "${id}" oauth must be an object.`);
73
+ }
74
+ const authorizationEndpoint = configuredUrl(id, "authorizationEndpoint", config.authorizationEndpoint);
75
+ const tokenEndpoint = configuredUrl(id, "tokenEndpoint", config.tokenEndpoint);
76
+ if (tokenEndpoint.search) {
77
+ throw new Error(`[connecta] connector "${id}" oauth.tokenEndpoint must not carry a query.`);
78
+ }
79
+ if (typeof config.clientId !== "string" || config.clientId.trim() === "") {
80
+ throw new Error(`[connecta] connector "${id}" oauth.clientId must be a non-empty string.`);
81
+ }
82
+ if (config.clientSecret !== undefined && (typeof config.clientSecret !== "string" || config.clientSecret === "")) {
83
+ // Usually an unset environment variable read as "". Never echo it.
84
+ throw new Error(`[connecta] connector "${id}" oauth.clientSecret must be a non-empty string when set.`);
85
+ }
86
+ const clientAuthentication = config.tokenEndpointAuthMethod ?? (config.clientSecret !== undefined ? "client_secret_basic" : "none");
87
+ if (!CLIENT_AUTHENTICATION.has(clientAuthentication)) {
88
+ throw new Error(`[connecta] connector "${id}" oauth.tokenEndpointAuthMethod must be ` +
89
+ '"client_secret_basic", "client_secret_post", or "none".');
90
+ }
91
+ if (clientAuthentication === "none" && config.clientSecret !== undefined) {
92
+ throw new Error(`[connecta] connector "${id}" oauth declares a clientSecret with ` +
93
+ 'tokenEndpointAuthMethod "none"; a public client sends no secret.');
94
+ }
95
+ if (clientAuthentication !== "none" && config.clientSecret === undefined) {
96
+ throw new Error(`[connecta] connector "${id}" oauth.tokenEndpointAuthMethod ` + `"${clientAuthentication}" needs a clientSecret.`);
97
+ }
98
+ assertOAuthScope(id, config.scope);
99
+ const authorizationParams = [];
100
+ for (const [name, value] of Object.entries(config.authorizationParams ?? {})) {
101
+ if (RESERVED_AUTHORIZATION_PARAMS.has(name)) {
102
+ throw new Error(`[connecta] connector "${id}" oauth.authorizationParams may not set ` +
103
+ `"${name}"; the authorization request owns it.`);
104
+ }
105
+ if (typeof value !== "string") {
106
+ throw new Error(`[connecta] connector "${id}" oauth.authorizationParams.${name} must be a string.`);
107
+ }
108
+ authorizationParams.push([name, value]);
109
+ }
110
+ const tokenRequestHeaders = [];
111
+ for (const [name, value] of Object.entries(config.tokenRequestHeaders ?? {})) {
112
+ if (!HEADER_NAME.test(name)) {
113
+ throw new Error(`[connecta] connector "${id}" oauth.tokenRequestHeaders has an invalid header name.`);
114
+ }
115
+ if (RESERVED_TOKEN_HEADERS.has(name.toLowerCase())) {
116
+ throw new Error(`[connecta] connector "${id}" oauth.tokenRequestHeaders may not set ` + `"${name}"; the token request owns it.`);
117
+ }
118
+ if (typeof value !== "string" || value.includes("\r") || value.includes("\n") || value.includes("\0")) {
119
+ throw new Error(`[connecta] connector "${id}" oauth.tokenRequestHeaders.${name} must be a single-line string.`);
120
+ }
121
+ tokenRequestHeaders.push([name, value]);
122
+ }
123
+ const origins = Array.isArray(config.apiOrigins) ? config.apiOrigins : [];
124
+ if (origins.length === 0) {
125
+ throw new Error(`[connecta] connector "${id}" oauth.apiOrigins must name at least one ` +
126
+ "origin; ctx.oauth.fetch sends the access token nowhere else.");
127
+ }
128
+ const apiOrigins = new Set();
129
+ for (const origin of origins) {
130
+ const url = configuredUrl(id, "apiOrigins", origin);
131
+ if (url.origin !== origin) {
132
+ throw new Error(`[connecta] connector "${id}" oauth.apiOrigins entries must be exact ` +
133
+ 'origins, like "https://api.example.com", with no path or trailing slash.');
134
+ }
135
+ apiOrigins.add(url.origin);
136
+ }
137
+ return {
138
+ authorizationEndpoint,
139
+ tokenEndpoint,
140
+ clientId: config.clientId,
141
+ ...(config.clientSecret !== undefined ? { clientSecret: config.clientSecret } : {}),
142
+ clientAuthentication,
143
+ ...(config.scope !== undefined ? { scope: config.scope } : {}),
144
+ pkce: config.pkce ?? true,
145
+ authorizationParams,
146
+ tokenRequestHeaders,
147
+ apiOrigins,
148
+ serverUrl: new URL(origins[0]),
149
+ identity: tokenEndpoint.href,
150
+ };
151
+ }
152
+ /**
153
+ * A `KvOAuthProvider` whose discovery and client are constants. Storage,
154
+ * sealing, epochs, refresh coordination, and the authorization hooks are the
155
+ * base class's, unchanged; only what discovery and registration would have
156
+ * written is answered from configuration and never written.
157
+ */
158
+ class StaticOAuthProvider extends KvOAuthProvider {
159
+ settings;
160
+ /** Set only for the code exchange of a PKCE-less flow. */
161
+ exchangeCode;
162
+ constructor(connectorId, ctx, coordinator, settings) {
163
+ super(connectorId, ctx.storage, `${ctx.baseUrl}/oauth/callback/${connectorId}`, coordinator, ctx.allowAuthorization === true, oauthSealerFor(ctx), ctx.signal,
164
+ // No client binding: the client is configuration, so there is no
165
+ // registration for a restart to carry forward or discard.
166
+ undefined, (reset) => trackOAuthStartReset(ctx.requestScope ?? ctx, reset), undefined, settings.scope, undefined, { secrets: sentSecretsFor(ctx) });
167
+ this.settings = settings;
168
+ }
169
+ /**
170
+ * The pre-registered client, answered from configuration. Its secret is
171
+ * never written to storage, sealed or otherwise: it is deployment config,
172
+ * not grant state, and an operator disconnect cannot delete it.
173
+ */
174
+ async clientInformation() {
175
+ const { settings } = this;
176
+ return {
177
+ client_id: settings.clientId,
178
+ ...(settings.clientSecret !== undefined ? { client_secret: settings.clientSecret } : {}),
179
+ redirect_uris: [this.redirectUrl],
180
+ token_endpoint_auth_method: settings.clientAuthentication,
181
+ issuer: settings.identity,
182
+ };
183
+ }
184
+ configuredAuthorizationBinding() {
185
+ return {
186
+ issuer: this.settings.identity,
187
+ clientId: this.settings.clientId,
188
+ tokenEndpoint: this.settings.tokenEndpoint.href,
189
+ };
190
+ }
191
+ async saveClientInformation() {
192
+ // Nothing to save: the SDK only saves a client it registered or stamped.
193
+ }
194
+ /**
195
+ * Discovery as a constant. Supplying both halves keeps the SDK from
196
+ * fetching either: no protected-resource probe of the API origin, and no
197
+ * metadata request to an authorization server that may not publish any.
198
+ * The metadata names no `issuer`, so the SDK binds grants to the configured
199
+ * token endpoint and does not demand an RFC 9207 `iss` it cannot know.
200
+ */
201
+ async discoveryState() {
202
+ const { settings } = this;
203
+ const metadata = {
204
+ authorization_endpoint: settings.authorizationEndpoint.href,
205
+ token_endpoint: settings.tokenEndpoint.href,
206
+ response_types_supported: ["code"],
207
+ grant_types_supported: ["authorization_code", "refresh_token"],
208
+ code_challenge_methods_supported: ["S256"],
209
+ token_endpoint_auth_methods_supported: [settings.clientAuthentication],
210
+ };
211
+ return {
212
+ authorizationServerUrl: settings.identity,
213
+ authorizationServerMetadata: metadata,
214
+ resourceMetadata: { resource: settings.serverUrl.origin },
215
+ };
216
+ }
217
+ async saveDiscoveryState() {
218
+ // Configuration is the discovery; there is nothing to remember.
219
+ }
220
+ /** No RFC 8707 `resource` parameter: a plain REST API names no resource. */
221
+ async validateResourceURL() {
222
+ return undefined;
223
+ }
224
+ /**
225
+ * The code exchange without `code_verifier`, for a flow whose authorization
226
+ * request carried no challenge. Undefined everywhere else, which hands the
227
+ * SDK back its own request.
228
+ */
229
+ prepareTokenRequest() {
230
+ if (this.settings.pkce || this.exchangeCode === undefined)
231
+ return undefined;
232
+ return new URLSearchParams({
233
+ grant_type: "authorization_code",
234
+ code: this.exchangeCode,
235
+ redirect_uri: this.redirectUrl,
236
+ });
237
+ }
238
+ /** @internal Remember the code a PKCE-less exchange sends. */
239
+ exchanging(code) {
240
+ this.exchangeCode = code;
241
+ }
242
+ async redirectToAuthorization(authorizationUrl) {
243
+ for (const [name, value] of this.settings.authorizationParams) {
244
+ authorizationUrl.searchParams.set(name, value);
245
+ }
246
+ if (!this.settings.pkce) {
247
+ authorizationUrl.searchParams.delete("code_challenge");
248
+ authorizationUrl.searchParams.delete("code_challenge_method");
249
+ }
250
+ await super.redirectToAuthorization(authorizationUrl);
251
+ }
252
+ }
253
+ const AUTH_REQUIRED_MESSAGE = "Authorization required — open the URL to connect.";
254
+ /** The grant behind one `api()` connector's `oauth`, as the hooks it exposes. */
255
+ export function staticOAuth(id, config) {
256
+ const settings = settingsFor(id, config);
257
+ const issuerContext = { issuer: settings.identity };
258
+ const coordinatorFor = refreshCoordinatorsByPartition();
259
+ // Keyed by request scope, so nothing here outlives the request that set it.
260
+ const callbackProviders = new WeakMap();
261
+ const rejectedScopes = new WeakSet();
262
+ const scopeOf = (ctx) => ctx.requestScope ?? ctx;
263
+ const warnShortSecret = shortSecretWarning();
264
+ const providerFor = (ctx) => {
265
+ warnShortSecret(settings.clientSecret, ctx.logger);
266
+ return new StaticOAuthProvider(id, ctx, coordinatorFor(ctx), settings);
267
+ };
268
+ /**
269
+ * The global `fetch`, read per request, with the configured token-request
270
+ * headers laid over the SDK's own. With discovery and registration answered
271
+ * from configuration the token endpoint is the only URL the SDK fetches here;
272
+ * the check keeps the headers on it even if that ever changes.
273
+ */
274
+ const tokenEndpointFetch = async (ctx, input, init = {}) => {
275
+ const url = new URL(input);
276
+ if (settings.tokenRequestHeaders.length === 0 || `${url.origin}${url.pathname}` !== settings.identity) {
277
+ trackRemoteClientRequest(sentSecretsFor(ctx), input, init);
278
+ return byteReadResponse(await sentSecretsFetch(ctx)(input, { ...init, redirect: "manual" }));
279
+ }
280
+ const headers = new Headers(init.headers);
281
+ for (const [name, value] of settings.tokenRequestHeaders)
282
+ headers.set(name, value);
283
+ for (const [, value] of settings.tokenRequestHeaders)
284
+ sentSecretsFor(ctx).header(value);
285
+ trackRemoteClientRequest(sentSecretsFor(ctx), input, { ...init, headers });
286
+ return byteReadResponse(await sentSecretsFetch(ctx)(input, { ...init, headers, redirect: "manual" }));
287
+ };
288
+ /**
289
+ * The SDK's `auth()` over this provider. The token endpoint is fetched
290
+ * through the refresh coordinator, with permanent spent fingerprints,
291
+ * rotation commits, and re-consent verdicts. Credential-bearing requests
292
+ * never follow redirects in either OAuth adapter.
293
+ */
294
+ const runAuth = (provider, ctx, exchange) => auth(provider, {
295
+ serverUrl: settings.serverUrl,
296
+ ...exchange,
297
+ fetchFn: coordinatorFor(ctx).coordinatedFetch(provider, (input, init) => tokenEndpointFetch(ctx, input, init), ctx.signal, ctx.defer),
298
+ });
299
+ // No cause, as in remoteMcp(): the `UnauthorizedError` class is the whole
300
+ // verdict, and a logger rendering the chain renders whatever it says.
301
+ const authRequiredError = () => new ConnectorCallError("downstream_oauth_required", `Connector "${id}" requires authorization — call authorize_connector({ connector: "${id}" }) and open the returned URL.`);
302
+ const disconnectedMessage = `Connector "${id}" was disconnected by an operator — explicitly start authorization to reconnect it.`;
303
+ /**
304
+ * One request through the owner's grant. A 401 earns exactly one recovery:
305
+ * a token another request has already rotated in, or else one coordinated
306
+ * refresh. A second 401 is `auth_required`, latched for the request scope
307
+ * so a program's later calls do not each spend a refresh on it.
308
+ */
309
+ const authorizedFetch = async (ctx, input, init = {}) => {
310
+ const url = new URL(input);
311
+ if (!settings.apiOrigins.has(url.origin)) {
312
+ throw new ConnectorCallError("connector_call_failed", `Connector "${id}" sends its OAuth access token only to its declared ` +
313
+ `apiOrigins, and ${url.origin} is not one of them.`, { retryable: false });
314
+ }
315
+ if (init.body instanceof ReadableStream) {
316
+ throw new TypeError(`Connector "${id}": ctx.oauth.fetch may replay a request once after a ` +
317
+ "refresh, so it cannot send a stream body. Buffer the body first.");
318
+ }
319
+ const headers = new Headers(init.headers);
320
+ if (headers.has("authorization")) {
321
+ throw new ConnectorCallError("connector_call_failed", `Connector "${id}": a request through ctx.oauth.fetch may not set ` +
322
+ "Authorization; the connector's grant owns it.", { retryable: false });
323
+ }
324
+ const signal = init.signal && ctx.signal ? AbortSignal.any([init.signal, ctx.signal]) : (init.signal ?? ctx.signal);
325
+ const send = async (accessToken) => {
326
+ sentSecretsFor(ctx).add(accessToken);
327
+ const sent = new Headers(headers);
328
+ sent.set("Authorization", `Bearer ${accessToken}`);
329
+ // The token rides to a declared origin and no further: a redirect is
330
+ // handed back to the handler unfollowed rather than re-sent anywhere.
331
+ // The handler reads the answer with `.json()` or `.text()`, which read
332
+ // bytes here: workerd quotes a text read's non-text Content-Type in
333
+ // its own log, out of the handler's reach.
334
+ const response = await sentSecretsFetch(ctx)(url, {
335
+ ...init,
336
+ headers: sent,
337
+ redirect: "manual",
338
+ ...(signal ? { signal } : {}),
339
+ });
340
+ return byteReadResponse(response);
341
+ };
342
+ if (rejectedScopes.has(scopeOf(ctx)))
343
+ throw authRequiredError();
344
+ const release = retainOAuthPartition(oauthPartitionFor(ctx));
345
+ try {
346
+ if (signal?.aborted)
347
+ throw signal.reason;
348
+ const provider = providerFor(ctx);
349
+ // Bind this call's reads and writes to the live epoch: a reset landing
350
+ // meanwhile fails the call rather than handing it another flow's grant.
351
+ const epoch = await provider.beginFlow();
352
+ if (provider.isOperatorDisconnectedEpoch(epoch)) {
353
+ throw new ConnectorCallError("downstream_oauth_required", disconnectedMessage);
354
+ }
355
+ const tokens = await provider.tokens(issuerContext);
356
+ if (!tokens)
357
+ throw authRequiredError();
358
+ const first = await send(tokens.access_token);
359
+ if (first.status !== 401)
360
+ return first;
361
+ void first.body?.cancel().catch(() => { });
362
+ let accessToken;
363
+ const current = await provider.tokens(issuerContext);
364
+ if (current && current.access_token !== tokens.access_token) {
365
+ accessToken = current.access_token;
366
+ }
367
+ else {
368
+ if ((await runAuth(provider, ctx)) !== "AUTHORIZED")
369
+ throw authRequiredError();
370
+ const refreshed = await provider.tokens(issuerContext);
371
+ if (!refreshed)
372
+ throw authRequiredError();
373
+ accessToken = refreshed.access_token;
374
+ }
375
+ const second = await send(accessToken);
376
+ if (second.status !== 401)
377
+ return second;
378
+ void second.body?.cancel().catch(() => { });
379
+ rejectedScopes.add(scopeOf(ctx));
380
+ throw authRequiredError();
381
+ }
382
+ catch (error) {
383
+ if (error instanceof UnauthorizedError) {
384
+ rejectedScopes.add(scopeOf(ctx));
385
+ throw authRequiredError();
386
+ }
387
+ throw error;
388
+ }
389
+ finally {
390
+ release();
391
+ }
392
+ };
393
+ const status = async (ctx) => {
394
+ if (rejectedScopes.has(scopeOf(ctx))) {
395
+ return {
396
+ state: "auth_required",
397
+ message: "Authorization required — the downstream rejected this connector's OAuth grant.",
398
+ };
399
+ }
400
+ // A status read never starts authorization: this provider cannot, and
401
+ // nothing here calls the SDK. A stored grant for the configured token
402
+ // endpoint is healthy until a call says otherwise — the downstream is
403
+ // asked nothing.
404
+ const provider = providerFor(ctx);
405
+ if (await provider.operatorDisconnected()) {
406
+ return { state: "auth_required", message: disconnectedMessage };
407
+ }
408
+ return (await provider.tokens(issuerContext))
409
+ ? { state: "ok" }
410
+ : { state: "auth_required", message: AUTH_REQUIRED_MESSAGE };
411
+ };
412
+ const startAuth = async (original, opts) => {
413
+ const ctx = authorizingContext(original);
414
+ const provider = providerFor(ctx);
415
+ if (ctx.signal?.aborted)
416
+ throw ctx.signal.reason;
417
+ const disconnected = opts?.force ? false : await provider.operatorDisconnected();
418
+ if (ctx.signal?.aborted)
419
+ throw ctx.signal.reason;
420
+ if (opts?.force || disconnected) {
421
+ await provider.resetAuthorization();
422
+ rejectedScopes.delete(scopeOf(original));
423
+ }
424
+ else {
425
+ // Hand back a recent consent URL rather than overwrite the verifier
426
+ // the operator may be mid-consent on.
427
+ const pending = await provider.reusablePendingAuthorizationUrl();
428
+ if (pending) {
429
+ return {
430
+ state: "auth_required",
431
+ authorizationUrl: pending,
432
+ authorizationReused: true,
433
+ message: AUTH_REQUIRED_MESSAGE,
434
+ };
435
+ }
436
+ }
437
+ if (ctx.signal?.aborted)
438
+ throw ctx.signal.reason;
439
+ try {
440
+ // A grant from a since-repointed token endpoint reads as absent here,
441
+ // and the consent this start begins replaces it.
442
+ await provider.beginFlow();
443
+ if (await provider.tokens(issuerContext)) {
444
+ return { state: "ok", message: "Already authorized — connection is healthy." };
445
+ }
446
+ if ((await runAuth(provider, ctx)) === "AUTHORIZED") {
447
+ return { state: "ok", message: "Already authorized — connection is healthy." };
448
+ }
449
+ const authorizationUrl = await provider.consentUrl();
450
+ return {
451
+ state: "auth_required",
452
+ ...(authorizationUrl !== undefined ? { authorizationUrl } : {}),
453
+ message: AUTH_REQUIRED_MESSAGE,
454
+ };
455
+ }
456
+ catch (error) {
457
+ if (error instanceof UnauthorizedError) {
458
+ return { state: "auth_required", message: AUTH_REQUIRED_MESSAGE };
459
+ }
460
+ // Reaches the agent through authorize_connector. Text already in
461
+ // connecta's words passes (token responses are rebuilt before the SDK
462
+ // reads them); anything else is told from its record.
463
+ return {
464
+ state: "error",
465
+ message: error instanceof ConnectorCallError || error instanceof OAuthError ? msg(error) : describeFailure(id, error),
466
+ };
467
+ }
468
+ };
469
+ const disconnectAuth = async (ctx) => {
470
+ rejectedScopes.delete(scopeOf(ctx));
471
+ await providerFor(ctx).resetAuthorization(true);
472
+ };
473
+ const verifyState = async (state, ctx) => {
474
+ // The provider that verified the state captured its flow's epoch, and the
475
+ // exchange must write under exactly that one.
476
+ const provider = providerFor(ctx);
477
+ callbackProviders.set(scopeOf(ctx), provider);
478
+ return provider.verifyState(state);
479
+ };
480
+ const finishAuth = async (code, ctx, callbackParams) => {
481
+ const provider = callbackProviders.get(scopeOf(ctx)) ?? providerFor(ctx);
482
+ callbackProviders.delete(scopeOf(ctx));
483
+ const authorizationCode = callbackParams?.get("code") ?? code;
484
+ const iss = callbackParams?.get("iss") ?? undefined;
485
+ // Every exchange names and claims its own consent, including without PKCE.
486
+ const consent = callbackParams?.get("state") ?? null;
487
+ if (!consent || !(await provider.verifyCallbackState(consent))) {
488
+ throw new ConnectorCallError("connector_call_failed", `Connector "${id}" authorization callback matches no pending consent; nothing was exchanged.`);
489
+ }
490
+ provider.validateCallbackIssuer(callbackParams?.get("iss") ?? null);
491
+ provider.exchanging(authorizationCode);
492
+ await provider.bindFlow();
493
+ const result = await runAuth(provider, ctx, {
494
+ authorizationCode,
495
+ ...(iss !== undefined ? { iss } : {}),
496
+ });
497
+ if (result !== "AUTHORIZED") {
498
+ throw new UnauthorizedError("Failed to authorize");
499
+ }
500
+ rejectedScopes.delete(scopeOf(ctx));
501
+ };
502
+ return {
503
+ status: retainingOAuthPartition(status, 0),
504
+ startAuth: retainingOAuthPartition(startAuth, 0),
505
+ disconnectAuth: retainingOAuthPartition(disconnectAuth, 0),
506
+ verifyState: retainingOAuthPartition(verifyState, 1),
507
+ verifyCallbackIssuer: retainingOAuthPartition(async (issuer, ctx) => {
508
+ const provider = callbackProviders.get(scopeOf(ctx));
509
+ if (!provider)
510
+ return false;
511
+ provider.validateCallbackIssuer(issuer);
512
+ return true;
513
+ }, 1),
514
+ consumeAuthError: retainingOAuthPartition(async (ctx) => {
515
+ const provider = callbackProviders.get(scopeOf(ctx));
516
+ callbackProviders.delete(scopeOf(ctx));
517
+ if (!provider)
518
+ throw new ConnectorCallError("connector_call_failed", "OAuth error callback matches no pending consent.");
519
+ await provider.consumeAuthError();
520
+ }, 0),
521
+ finishAuth: retainingOAuthPartition(finishAuth, 1),
522
+ access: (ctx) => ({
523
+ fetch: (input, init) => authorizedFetch(ctx, input, init),
524
+ }),
525
+ };
526
+ }