@zackbart/connecta 0.28.1 → 0.29.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (319) hide show
  1. package/CHANGELOG.md +788 -5
  2. package/README.md +62 -42
  3. package/bin/connecta.mjs +110 -139
  4. package/bin/meta-tool-names.d.mts +2 -0
  5. package/bin/meta-tool-names.mjs +9 -0
  6. package/bin/version.mjs +3 -0
  7. package/dist/access-tokens.js +53 -63
  8. package/dist/activity-request.js +8 -0
  9. package/dist/activity.d.ts +76 -42
  10. package/dist/activity.js +110 -42
  11. package/dist/auth/clerk-sdk/client.d.ts +65 -0
  12. package/dist/auth/clerk-sdk/client.js +5 -0
  13. package/dist/auth/clerk-sdk/generated.js +1845 -0
  14. package/dist/auth/clerk-transport.js +19 -0
  15. package/dist/auth/clerk.d.ts +17 -7
  16. package/dist/auth/clerk.js +189 -107
  17. package/dist/auth/cloudflare-access.js +25 -26
  18. package/dist/auth/downstream-client-metadata.d.ts +29 -0
  19. package/dist/auth/downstream-client-metadata.js +133 -0
  20. package/dist/auth/downstream-oauth.js +1584 -1580
  21. package/dist/auth/downstream-revocation.js +106 -0
  22. package/dist/auth/oauth-token-response.js +231 -0
  23. package/dist/auth/oauth-v2-migration.js +116 -0
  24. package/dist/auth/static-oauth.js +93 -71
  25. package/dist/auth-elicitation.js +283 -0
  26. package/dist/branding.d.ts +8 -16
  27. package/dist/branding.js +26 -59
  28. package/dist/byte-read-response.js +39 -0
  29. package/dist/call-admission.js +12 -16
  30. package/dist/catalog-cache.js +739 -0
  31. package/dist/catalog-drift.d.ts +50 -15
  32. package/dist/catalog-drift.js +365 -128
  33. package/dist/catalog-fingerprint.js +2 -4
  34. package/dist/catalog-freshness.js +13 -0
  35. package/dist/catalog-limits.js +2 -2
  36. package/dist/catalog-service.js +368 -224
  37. package/dist/catalog.js +43 -84
  38. package/dist/concurrency.js +3 -4
  39. package/dist/config-defaults.d.ts +39 -0
  40. package/dist/config-defaults.js +46 -0
  41. package/dist/config-schema.d.ts +181 -0
  42. package/dist/config-schema.js +435 -0
  43. package/dist/config-value-sources.js +102 -0
  44. package/dist/config.d.ts +631 -0
  45. package/dist/config.js +503 -0
  46. package/dist/connector-access.d.ts +1 -0
  47. package/dist/connector-access.js +13 -15
  48. package/dist/connector-caller.d.ts +8 -0
  49. package/dist/connector-caller.js +15 -6
  50. package/dist/connector-scope.js +1 -1
  51. package/dist/connectors/api-connector.d.ts +59 -7
  52. package/dist/connectors/api-connector.js +106 -19
  53. package/dist/connectors/api.js +4 -1
  54. package/dist/connectors/guarded-fetch.d.ts +144 -0
  55. package/dist/connectors/guarded-fetch.js +84 -35
  56. package/dist/connectors/negotiation-cache.js +46 -0
  57. package/dist/connectors/option-shapes.js +27 -0
  58. package/dist/connectors/remote-mcp.d.ts +30 -4
  59. package/dist/connectors/remote-mcp.js +1774 -320
  60. package/dist/connectors/resource-uri.js +335 -0
  61. package/dist/credential-contract.d.ts +10 -0
  62. package/dist/credential-rules.js +1 -3
  63. package/dist/credential-url.js +108 -0
  64. package/dist/credentials.d.ts +6 -1
  65. package/dist/credentials.js +27 -18
  66. package/dist/d1.d.ts +130 -0
  67. package/dist/d1.js +290 -0
  68. package/dist/describe-config.d.ts +165 -0
  69. package/dist/describe-config.js +315 -0
  70. package/dist/described.js +106 -0
  71. package/dist/downstream-input-context.js +119 -0
  72. package/dist/downstream-input.js +323 -0
  73. package/dist/errors.d.ts +97 -20
  74. package/dist/errors.js +158 -80
  75. package/dist/execute.js +453 -305
  76. package/dist/executor-admission.d.ts +1 -0
  77. package/dist/executor-admission.js +11 -15
  78. package/dist/executor-contract.d.ts +20 -0
  79. package/dist/executor-contract.js +48 -0
  80. package/dist/executor-result.js +24 -9
  81. package/dist/executors/quickjs-child.js +3 -4
  82. package/dist/executors/quickjs-runtime.js +147 -59
  83. package/dist/executors/quickjs.js +29 -35
  84. package/dist/favicon.js +1 -2
  85. package/dist/fix-prompt.js +1 -3
  86. package/dist/guest-runtime.js +188 -0
  87. package/dist/guest-types.d.ts +176 -0
  88. package/dist/guest-types.js +1 -0
  89. package/dist/html-security.js +24 -0
  90. package/dist/identity.js +1 -3
  91. package/dist/inbound-credential.js +52 -0
  92. package/dist/index.d.ts +24 -256
  93. package/dist/index.js +65 -478
  94. package/dist/invocation-auth.js +69 -0
  95. package/dist/invocation.js +230 -73
  96. package/dist/json-schema.d.ts +1 -1
  97. package/dist/mcp-client-context.js +35 -0
  98. package/dist/mcp-result.js +11 -7
  99. package/dist/meta-output.js +76 -0
  100. package/dist/meta-tool-names.d.ts +2 -0
  101. package/dist/meta-tool-names.js +3 -0
  102. package/dist/meta-tools.js +285 -209
  103. package/dist/module-contracts.d.ts +8 -23
  104. package/dist/node.d.ts +0 -2
  105. package/dist/node.js +2 -2
  106. package/dist/oauth-callback-outcome.js +1 -3
  107. package/dist/oauth-handoff.js +117 -0
  108. package/dist/oauth-sealing.js +20 -0
  109. package/dist/operator-record.d.ts +137 -0
  110. package/dist/operator-record.js +420 -0
  111. package/dist/operator-ui/assets.js +31 -0
  112. package/dist/operator-ui/contract.d.ts +72 -0
  113. package/dist/operator-ui/contract.js +1 -0
  114. package/dist/operator-ui/fix-prompts.d.ts +18 -0
  115. package/dist/operator-ui/generated.js +5 -4
  116. package/dist/operator-ui/model.d.ts +12 -87
  117. package/dist/operator-ui/model.js +2 -9
  118. package/dist/operator-ui/setup-commands.js +1 -3
  119. package/dist/operator-ui/view.d.ts +323 -0
  120. package/dist/operator-ui/view.js +62 -189
  121. package/dist/page-styles.js +2 -3
  122. package/dist/program-source.js +14 -2
  123. package/dist/{exempt-writes.js → program-writes.js} +55 -18
  124. package/dist/provider.d.ts +142 -0
  125. package/dist/provider.js +187 -0
  126. package/dist/providers/_shared/google/result-size.js +57 -0
  127. package/dist/providers/_shared/google/service-account.d.ts +58 -0
  128. package/dist/providers/_shared/google/service-account.js +500 -0
  129. package/dist/providers/_shared/google/workspace.d.ts +276 -0
  130. package/dist/providers/_shared/google/workspace.js +518 -0
  131. package/dist/providers/{basecamp.d.ts → basecamp/index.d.ts} +6 -28
  132. package/dist/providers/basecamp/index.js +997 -0
  133. package/dist/providers/basecamp/skill.generated.js +9 -0
  134. package/dist/providers/{breeze.d.ts → breeze/index.d.ts} +2 -2
  135. package/dist/providers/{breeze.js → breeze/index.js} +184 -140
  136. package/dist/providers/breeze/skill.generated.js +9 -0
  137. package/dist/providers/{ccb.d.ts → ccb/index.d.ts} +2 -2
  138. package/dist/providers/{ccb.js → ccb/index.js} +271 -116
  139. package/dist/providers/ccb/skill.generated.js +9 -0
  140. package/dist/providers/{cloudflare.d.ts → cloudflare/index.d.ts} +8 -7
  141. package/dist/providers/{cloudflare.js → cloudflare/index.js} +171 -738
  142. package/dist/providers/cloudflare/skill.generated.js +10 -0
  143. package/dist/providers/docs/index.d.ts +54 -0
  144. package/dist/providers/docs/index.js +1237 -0
  145. package/dist/providers/docs/skill.generated.js +9 -0
  146. package/dist/providers/drive/index.d.ts +53 -0
  147. package/dist/providers/drive/index.js +1523 -0
  148. package/dist/providers/drive/skill.generated.js +9 -0
  149. package/dist/providers/forms/index.d.ts +54 -0
  150. package/dist/providers/forms/index.js +1217 -0
  151. package/dist/providers/forms/skill.generated.js +11 -0
  152. package/dist/providers/github/auth.d.ts +32 -0
  153. package/dist/providers/github/auth.js +361 -0
  154. package/dist/providers/github/hosted.js +218 -0
  155. package/dist/providers/github/index.d.ts +13 -0
  156. package/dist/providers/github/index.js +140 -0
  157. package/dist/providers/github/key.js +100 -0
  158. package/dist/providers/github/rest.js +321 -0
  159. package/dist/providers/github/scope.d.ts +29 -0
  160. package/dist/providers/github/scope.js +124 -0
  161. package/dist/providers/github/skill.generated.js +8 -0
  162. package/dist/providers/gmail/index.d.ts +51 -0
  163. package/dist/providers/gmail/index.js +1436 -0
  164. package/dist/providers/gmail/skill.generated.js +9 -0
  165. package/dist/providers/{linear.d.ts → linear/index.d.ts} +11 -14
  166. package/dist/providers/linear/index.js +352 -0
  167. package/dist/providers/linear/skill.generated.js +8 -0
  168. package/dist/providers/{mixpanel.d.ts → mixpanel/index.d.ts} +5 -10
  169. package/dist/providers/mixpanel/index.js +416 -0
  170. package/dist/providers/mixpanel/skill.generated.js +9 -0
  171. package/dist/providers/{notion.d.ts → notion/index.d.ts} +8 -8
  172. package/dist/providers/{notion.js → notion/index.js} +226 -239
  173. package/dist/providers/notion/skill.generated.js +10 -0
  174. package/dist/providers/{overflow.d.ts → overflow/index.d.ts} +2 -2
  175. package/dist/providers/{overflow.js → overflow/index.js} +74 -111
  176. package/dist/providers/overflow/skill.generated.js +8 -0
  177. package/dist/providers/{planning-center.d.ts → planning-center/index.d.ts} +2 -2
  178. package/dist/providers/{planning-center.js → planning-center/index.js} +264 -104
  179. package/dist/providers/planning-center/skill.generated.js +10 -0
  180. package/dist/providers/{revenuecat.d.ts → revenuecat/index.d.ts} +5 -10
  181. package/dist/providers/revenuecat/index.js +557 -0
  182. package/dist/providers/revenuecat/skill.generated.js +11 -0
  183. package/dist/providers/sheets/index.d.ts +54 -0
  184. package/dist/providers/sheets/index.js +1237 -0
  185. package/dist/providers/sheets/skill.generated.js +10 -0
  186. package/dist/providers/slides/index.d.ts +53 -0
  187. package/dist/providers/slides/index.js +2645 -0
  188. package/dist/providers/slides/skill.generated.js +9 -0
  189. package/dist/providers/{stripe.d.ts → stripe/index.d.ts} +5 -12
  190. package/dist/providers/stripe/index.js +266 -0
  191. package/dist/providers/stripe/skill.generated.js +11 -0
  192. package/dist/providers/{tithely.d.ts → tithely/index.d.ts} +2 -2
  193. package/dist/providers/{tithely.js → tithely/index.js} +60 -91
  194. package/dist/providers/tithely/skill.generated.js +8 -0
  195. package/dist/providers/{vercel.d.ts → vercel/index.d.ts} +8 -8
  196. package/dist/providers/vercel/index.js +1038 -0
  197. package/dist/providers/vercel/mcp-ownership.js +212 -0
  198. package/dist/providers/vercel/skill.generated.js +11 -0
  199. package/dist/registry.d.ts +108 -171
  200. package/dist/registry.js +513 -925
  201. package/dist/request-state.js +21 -0
  202. package/dist/resource-read.js +100 -0
  203. package/dist/result-shapes.js +17 -49
  204. package/dist/retired-toolkits.js +1 -1
  205. package/dist/routes/access-tokens.js +3 -6
  206. package/dist/routes/activity-context.js +21 -0
  207. package/dist/routes/activity-disclosure.js +40 -0
  208. package/dist/routes/activity.js +59 -21
  209. package/dist/routes/actor-labels.js +3 -50
  210. package/dist/routes/connect.js +221 -0
  211. package/dist/routes/credentials.js +15 -27
  212. package/dist/routes/mcp.js +356 -130
  213. package/dist/routes/oauth-client-metadata.js +15 -0
  214. package/dist/routes/oauth-management.js +45 -107
  215. package/dist/routes/oauth.js +110 -57
  216. package/dist/routes/operator.js +3 -3
  217. package/dist/routes/shared.d.ts +25 -64
  218. package/dist/routes/shared.js +160 -80
  219. package/dist/routes/skills.js +30 -0
  220. package/dist/routes/ui-config.js +225 -0
  221. package/dist/routes/ui-data.js +40 -35
  222. package/dist/routes/ui.js +66 -56
  223. package/dist/runtime/admission.js +4 -2
  224. package/dist/runtime/call-admission.js +3 -1
  225. package/dist/runtime/connector-scope.js +1 -4
  226. package/dist/runtime/run.js +10 -39
  227. package/dist/runtime/services.js +12 -117
  228. package/dist/runtime/shared-read.js +1 -3
  229. package/dist/runtime/storage.js +8 -1
  230. package/dist/sent-secrets.d.ts +52 -0
  231. package/dist/sent-secrets.js +474 -0
  232. package/dist/server.js +28 -71
  233. package/dist/skills.js +406 -174
  234. package/dist/sqlite.d.ts +48 -0
  235. package/dist/sqlite.js +166 -0
  236. package/dist/storage/keys.js +428 -0
  237. package/dist/storage/memory.js +7 -0
  238. package/dist/storage/sql.d.ts +66 -0
  239. package/dist/storage/sql.js +534 -0
  240. package/dist/timeout.js +4 -2
  241. package/dist/tool-name.js +17 -0
  242. package/dist/tool-safety.d.ts +9 -35
  243. package/dist/tool-safety.js +20 -40
  244. package/dist/types.d.ts +320 -54
  245. package/dist/types.js +0 -1
  246. package/dist/ui.d.ts +7 -19
  247. package/dist/ui.js +76 -90
  248. package/dist/url-safety.js +3 -6
  249. package/dist/usage-guide.js +137 -0
  250. package/dist/validate.d.ts +7 -0
  251. package/dist/validate.js +309 -47
  252. package/dist/version.d.ts +1 -7
  253. package/dist/version.js +3 -7
  254. package/dist/worker.d.ts +11 -0
  255. package/dist/worker.js +434 -0
  256. package/documentation/architecture.md +664 -231
  257. package/documentation/auth.md +936 -546
  258. package/documentation/code-mode.md +501 -312
  259. package/documentation/meta-tools.md +449 -200
  260. package/documentation/operator-tests.md +93 -0
  261. package/documentation/provider-migration-0.29.md +60 -0
  262. package/examples/worker/AGENTS.md +34 -12
  263. package/examples/worker/README.md +286 -255
  264. package/examples/worker/kv-to-d1.wrangler.jsonc +26 -0
  265. package/examples/worker/scripts/copy-kv-to-d1.mjs +162 -0
  266. package/examples/worker/src/connecta.config.ts +151 -0
  267. package/examples/worker/src/index.ts +14 -197
  268. package/examples/worker/wrangler.jsonc +15 -35
  269. package/package.json +120 -54
  270. package/templates/node/.dockerignore +1 -2
  271. package/templates/node/.env.example +20 -21
  272. package/templates/node/AGENTS.md +21 -17
  273. package/templates/node/Dockerfile +3 -5
  274. package/templates/node/README.md +133 -58
  275. package/templates/node/docker-compose.yml +11 -12
  276. package/templates/node/package.json +4 -2
  277. package/templates/node/src/connecta.config.ts +119 -0
  278. package/templates/node/src/index.ts +9 -154
  279. package/templates/node/src/provision-token.ts +17 -0
  280. package/AGENTS.md +0 -170
  281. package/dist/artifacts/connector.d.ts +0 -58
  282. package/dist/artifacts/connector.js +0 -730
  283. package/dist/artifacts/document.js +0 -86
  284. package/dist/artifacts/guide.js +0 -140
  285. package/dist/artifacts/html-scan.js +0 -329
  286. package/dist/artifacts/json.js +0 -78
  287. package/dist/artifacts/kv-store.d.ts +0 -20
  288. package/dist/artifacts/kv-store.js +0 -140
  289. package/dist/artifacts/markdown.js +0 -478
  290. package/dist/artifacts/module.d.ts +0 -37
  291. package/dist/artifacts/module.js +0 -66
  292. package/dist/artifacts/operations.d.ts +0 -197
  293. package/dist/artifacts/operations.js +0 -1002
  294. package/dist/artifacts/refresh.d.ts +0 -55
  295. package/dist/artifacts/refresh.js +0 -236
  296. package/dist/artifacts/routes.js +0 -206
  297. package/dist/artifacts/types.d.ts +0 -214
  298. package/dist/artifacts/types.js +0 -22
  299. package/dist/artifacts/validate.d.ts +0 -62
  300. package/dist/artifacts/validate.js +0 -651
  301. package/dist/artifacts.d.ts +0 -6
  302. package/dist/artifacts.js +0 -9
  303. package/dist/auth/bearer.d.ts +0 -11
  304. package/dist/auth/bearer.js +0 -50
  305. package/dist/providers/basecamp.js +0 -445
  306. package/dist/providers/linear.js +0 -217
  307. package/dist/providers/mixpanel.js +0 -247
  308. package/dist/providers/revenuecat.js +0 -317
  309. package/dist/providers/stripe.js +0 -253
  310. package/dist/providers/vercel.js +0 -1253
  311. package/dist/storage/file.d.ts +0 -15
  312. package/dist/storage/file.js +0 -455
  313. package/ethos.md +0 -93
  314. package/examples/worker/src/cloudflare-kv.ts +0 -43
  315. package/examples/worker/src/d1-activity-row.ts +0 -155
  316. package/examples/worker/src/d1-activity.ts +0 -147
  317. package/examples/worker/src/d1-storage.ts +0 -138
  318. package/examples/worker/src/r2-artifact-blobs.ts +0 -29
  319. package/templates/node/src/file-activity.ts +0 -182
@@ -0,0 +1,518 @@
1
+ /**
2
+ * Google Workspace access as the person calling, with no per-user consent:
3
+ * a service account with domain-wide delegation mints a token *as* the user
4
+ * the deployment maps the admitted caller to. The foundation each Workspace
5
+ * product provider (`gmail()` first) builds on; never its own export.
6
+ *
7
+ * Whose account. `ConnectorContext` carries no identity, and a deployment's
8
+ * own connector never learns its caller (`src/connector-caller.ts`). A
9
+ * maintained Workspace provider is the second sanctioned reader of that
10
+ * channel (PRINCIPLES.md INV-3, #678): deployment config supplies
11
+ * `subject`, a function from the admitted `AuthenticatedIdentity` to a
12
+ * Workspace address, and the provider reads the identity core attached beside
13
+ * the context. No tool argument, header, or program can name the subject —
14
+ * every product's base URL is confined beneath `users/me`, or its product's
15
+ * equivalent, so the token's subject is the only account a request reaches.
16
+ * A call with no admitted caller, one an open deployment admitted without
17
+ * authentication (the mapping is never asked about it), or one the mapping
18
+ * answers `undefined` for, fails `auth_required` before anything leaves the
19
+ * host. Catalogs need no subject: tools are static.
20
+ *
21
+ * A product builds on three calls: `workspaceConnection(factory, options)` at
22
+ * construction, `googleWorkspaceClient({...})` once, and the client's `json`,
23
+ * `bytes`, or `text` per request — plus `googleReasonsOf(error)` where a
24
+ * product must decide on a Google reason the shared mapping leaves generic.
25
+ * Nothing else here is the products' to touch.
26
+ *
27
+ * Setup, once per Workspace, shared by every product:
28
+ *
29
+ * 1. In a Google Cloud project, enable each product's API (Gmail API, Google
30
+ * Drive API, …).
31
+ * 2. Create a service account. It needs no IAM role: delegation, not project
32
+ * permission, is what reaches user data.
33
+ * 3. Create a JSON key for it. An organization policy
34
+ * (`iam.disableServiceAccountKeyCreation`, on by default for newer
35
+ * organizations) may refuse; an org policy administrator can override it
36
+ * for this project alone.
37
+ * 4. Copy the service account's numeric client ID (Details → Advanced
38
+ * settings, or the key's `client_id`).
39
+ * 5. As a Workspace super admin: Admin console → Security → Access and data
40
+ * control → API controls → Manage Domain Wide Delegation → Add new, with
41
+ * that client ID and *exactly* the scopes each product provider lists, comma
42
+ * separated. A grant can take up to 24 hours to apply.
43
+ * 6. Pass the key to the provider from the deployment's secrets, and map the
44
+ * admitted identity to a Workspace address in `subject`.
45
+ */
46
+ import { guardedFetch, retryAfterMs } from "../../../connectors/guarded-fetch.js";
47
+ import { keys, optionsOf } from "../../../config-schema.js";
48
+ import { callerOf } from "../../../connector-caller.js";
49
+ import { CALL_ADMISSION } from "../../../connectors/option-shapes.js";
50
+ import { ConnectorCallError } from "../../../errors.js";
51
+ import { delegatedToken, forgetDelegatedToken, parseServiceAccount, } from "./service-account.js";
52
+ /** The closed options every Workspace provider accepts; see `assertKnownOptions`. */
53
+ export const GOOGLE_WORKSPACE_OPTIONS = optionsOf()({
54
+ ...keys("purpose", "subject", "title", "instructions", "maxResultBytes", "baseUrl"),
55
+ // A string is the key's JSON; only the object form has keys to check.
56
+ serviceAccount: optionsOf()(keys("clientEmail", "privateKey", "clientId")),
57
+ callAdmission: CALL_ADMISSION,
58
+ });
59
+ /** A plausible mailbox: one `@`, no whitespace or control characters. */
60
+ const ADDRESS = /^[^\s@<>()",;:\\[\]]+@[^\s@<>()",;:\\[\]]+\.[^\s@<>()",;:\\[\]]+$/;
61
+ /** Validate the shared options at construction; throws on a structural mistake. */
62
+ export function workspaceConnection(factory, options) {
63
+ const purpose = options?.purpose?.trim();
64
+ if (!purpose)
65
+ throw new Error(`${factory}() requires a non-empty purpose.`);
66
+ const account = parseServiceAccount(`${factory}()`, options.serviceAccount);
67
+ const subject = options.subject;
68
+ if (typeof subject === "string") {
69
+ if (!ADDRESS.test(subject.trim())) {
70
+ throw new Error(`${factory}() subject must be a Workspace email address or a function of the caller.`);
71
+ }
72
+ return { purpose, account, subject: subject.trim() };
73
+ }
74
+ if (typeof subject !== "function") {
75
+ throw new Error(`${factory}() requires subject: a function from the admitted identity to a Workspace address, or one fixed address. Delegated access never guesses whose account to open.`);
76
+ }
77
+ return { purpose, account, subject };
78
+ }
79
+ /**
80
+ * The Workspace address this call acts as, resolved from config and the
81
+ * admitted caller only. Throws before any network call when there is none.
82
+ */
83
+ async function subjectFor(provider, subject, ctx) {
84
+ if (typeof subject === "string")
85
+ return subject;
86
+ const caller = callerOf(ctx);
87
+ if (!caller) {
88
+ throw new ConnectorCallError("auth_required", `${provider} acts as the signed-in Workspace user, and this call has no admitted caller (a scheduled run, or a context no request admitted). Use a fixed subject for work with no caller.`);
89
+ }
90
+ // An open deployment admits every request as the anonymous actor. That is
91
+ // a caller, but not anyone, and the mapping never gets to decide otherwise.
92
+ if (!caller.authenticated) {
93
+ throw new ConnectorCallError("auth_required", `${provider} acts as the signed-in Workspace user, and this deployment admitted the call without authentication. Configure inbound auth, or a fixed subject for shared use.`);
94
+ }
95
+ let mapped;
96
+ try {
97
+ mapped = await subject(caller.identity, ctx.signal ? { signal: ctx.signal } : {});
98
+ }
99
+ catch (cause) {
100
+ if (ctx.signal?.aborted)
101
+ throw ctx.signal.reason;
102
+ throw new ConnectorCallError("connector_call_failed", `The deployment's ${provider} subject mapping threw while resolving this caller's Workspace account.`, { retryable: false, cause });
103
+ }
104
+ if (mapped === undefined) {
105
+ throw new ConnectorCallError("auth_required", `This caller has no Google Workspace account mapped for ${provider}. An operator maps signed-in identities to Workspace addresses in the deployment's subject config; nothing a call sends can choose one.`);
106
+ }
107
+ if (typeof mapped !== "string" || !ADDRESS.test(mapped.trim())) {
108
+ throw new ConnectorCallError("connector_call_failed", `The deployment's ${provider} subject mapping returned something that is not an email address.`, { retryable: false });
109
+ }
110
+ return mapped.trim();
111
+ }
112
+ // --- API failures ------------------------------------------------------------------
113
+ function asRecord(value) {
114
+ return value && typeof value === "object" && !Array.isArray(value) ? value : {};
115
+ }
116
+ function asArray(value) {
117
+ return Array.isArray(value) ? value : [];
118
+ }
119
+ /**
120
+ * The machine-readable reasons in a Google API error body: the legacy
121
+ * `errors[].reason`, the AIP-193 `details[].reason`, and the canonical
122
+ * `status`. Classification reads these and nothing else; `message` is prose
123
+ * for the reader.
124
+ */
125
+ function reasonsOf(error) {
126
+ const reasons = new Set();
127
+ for (const entry of [...asArray(error["errors"]), ...asArray(error["details"])]) {
128
+ const reason = asRecord(entry)["reason"];
129
+ if (typeof reason === "string")
130
+ reasons.add(reason);
131
+ }
132
+ if (typeof error["status"] === "string")
133
+ reasons.add(error["status"]);
134
+ return reasons;
135
+ }
136
+ /** The canonical and legacy spellings of a refused precondition. */
137
+ const PRECONDITION_REASONS = ["FAILED_PRECONDITION", "failedPrecondition"];
138
+ /** Concurrency aborts, which a revision-checked write also answers with. */
139
+ const ABORTED_REASONS = ["ABORTED", "aborted"];
140
+ /** A reason token as Google spells one; anything else is not passed on. */
141
+ const REASON_TOKEN = /^[A-Za-z0-9_.]{1,64}$/;
142
+ /** More than this many reasons on one error is noise, not information. */
143
+ const MAX_REASONS = 8;
144
+ /** Google's reasons for each failure this layer mapped, beside the error. */
145
+ const googleReasons = new WeakMap();
146
+ /**
147
+ * Google's machine-readable reasons for a failure the shared client mapped:
148
+ * `errors[].reason`, `details[].reason`, and the canonical `status`, in that
149
+ * order, deduplicated, at most eight, and only tokens of `[A-Za-z0-9_.]` up to
150
+ * 64 characters — never a message or any other part of the payload. An empty
151
+ * array for anything else, including a failure that never reached Google.
152
+ *
153
+ * For a product that needs a decision the shared mapping does not make, such
154
+ * as Drive telling `cannotDownloadAbusiveFile` from another refusal. Classify
155
+ * on these, never on the error's message.
156
+ */
157
+ export function googleReasonsOf(error) {
158
+ return error instanceof ConnectorCallError ? (googleReasons.get(error) ?? []) : [];
159
+ }
160
+ const googleOutcomes = new WeakMap();
161
+ function outcome(error, facts) {
162
+ if (error !== null && typeof error === "object")
163
+ googleOutcomes.set(error, Object.freeze({ ...facts }));
164
+ return error;
165
+ }
166
+ /**
167
+ * How far the request behind a failure from `json`, `bytes`, or `text` got —
168
+ * see {@link GoogleOutcome}. `undefined` for anything the client did not
169
+ * throw. Read it beside the error, the way `googleReasonsOf` is read; the
170
+ * error itself already carries the right retry verdict.
171
+ */
172
+ export function googleOutcomeOf(error) {
173
+ return error !== null && typeof error === "object" ? googleOutcomes.get(error) : undefined;
174
+ }
175
+ function apiFailure(context, status, headers, payload, options) {
176
+ const failure = classifyApiFailure(context, status, headers, payload, options);
177
+ const error = asRecord(asRecord(payload)["error"]);
178
+ const reasons = [...reasonsOf(error)].filter((reason) => REASON_TOKEN.test(reason)).slice(0, MAX_REASONS);
179
+ googleReasons.set(failure, Object.freeze(reasons));
180
+ return failure;
181
+ }
182
+ function classifyApiFailure(context, status, headers, payload, options) {
183
+ const { provider, api, scopes } = context;
184
+ const error = asRecord(asRecord(payload)["error"]);
185
+ const message = typeof error["message"] === "string" ? error["message"].trim().slice(0, 500) : "";
186
+ const detail = message ? `${provider}: ${message}` : `${provider} returned HTTP ${status}.`;
187
+ const reasons = reasonsOf(error);
188
+ const precondition = PRECONDITION_REASONS.some((reason) => reasons.has(reason));
189
+ // Order matters. Google's own specific reasons decide first, whatever the
190
+ // status and whatever the request asked for: a guarded write refused
191
+ // because the API is off, or because of a quota, must not read as "re-read
192
+ // and retry". Only a refused precondition or an abort with nothing more
193
+ // specific to say is left for the revision guard, then for the status.
194
+ if (status === 401) {
195
+ return new ConnectorCallError("auth_required", `${detail} Google rejected a freshly minted delegated token. The domain-wide delegation grant or the service account may have been revoked.`);
196
+ }
197
+ // The API is off in the service account's project.
198
+ if (reasons.has("accessNotConfigured") || reasons.has("SERVICE_DISABLED")) {
199
+ return new ConnectorCallError("connector_call_failed", `${detail} The ${api} is not enabled in the service account's Google Cloud project; an operator enables it there.`, { retryable: false });
200
+ }
201
+ if (status === 429 ||
202
+ reasons.has("rateLimitExceeded") ||
203
+ reasons.has("userRateLimitExceeded") ||
204
+ reasons.has("RATE_LIMIT_EXCEEDED")) {
205
+ const wait = retryAfterMs(headers);
206
+ return new ConnectorCallError("rate_limited", `${detail} Google meters this API per user and per project; wait before retrying.`, wait === undefined ? {} : { retryAfterMs: wait });
207
+ }
208
+ if (reasons.has("insufficientPermissions") || reasons.has("ACCESS_TOKEN_SCOPE_INSUFFICIENT")) {
209
+ return new ConnectorCallError("provider_permission_denied", `${detail} The delegated token lacks a scope this call needs. The Admin console's domain-wide delegation entry must list exactly: ${scopes.join(",")}.`);
210
+ }
211
+ // Where Google names the refusal precisely, so does the message.
212
+ if (reasons.has("exportSizeLimitExceeded")) {
213
+ return new ConnectorCallError("connector_call_failed", `${detail} The item is larger than Google will export in this format. Download the original file instead, or export a smaller part.`, { retryable: false });
214
+ }
215
+ if (reasons.has("domainPolicy")) {
216
+ return new ConnectorCallError("provider_permission_denied", `${detail} A Workspace domain policy forbids this action for this account; only a Workspace administrator can change that.`, { retryable: false });
217
+ }
218
+ if (reasons.has("insufficientFilePermissions") || reasons.has("forbidden")) {
219
+ return new ConnectorCallError("provider_permission_denied", `${detail} This account does not have the permission on this item that the action needs — it may be shared view-only, or not shared with this user. Its owner can grant more.`, { retryable: false });
220
+ }
221
+ // A write that named the revision it was made against hears "the item moved
222
+ // on" as a refused precondition or an abort. Fixed words, so no provider
223
+ // ever matches on Google's prose or on this file's.
224
+ if (options.revisionGuarded === true &&
225
+ (status === 400 || status === 409) &&
226
+ (precondition || ABORTED_REASONS.some((reason) => reasons.has(reason)))) {
227
+ return new ConnectorCallError("conflict", `${provider} refused the write because the item changed after the revision it named. Re-read it for the current revision, reapply the change, and retry.`);
228
+ }
229
+ if (status === 403) {
230
+ return new ConnectorCallError("provider_permission_denied", `${detail} Google refused the request; the account may lack access to this item, or a Workspace policy may block it. Google does not say which. Ask the item owner to grant access or a Workspace administrator to review the policy.`, { retryable: false });
231
+ }
232
+ if (status === 404) {
233
+ return context.notFound === "absent"
234
+ ? new ConnectorCallError("not_found", `${detail} Re-read the id from a list or search tool.`)
235
+ : new ConnectorCallError("connector_call_failed", `${detail} The id is unknown or not visible to this account; Google does not distinguish them.`, { retryable: false });
236
+ }
237
+ // Google uses the same reason for a product switched off for this user
238
+ // (Gmail's "Mail service not enabled") and for a stale write, so the
239
+ // classification asserts neither; Google's own message says which.
240
+ if (status === 400 && precondition) {
241
+ return new ConnectorCallError("connector_call_failed", `${detail} Google refused the request's precondition; its message says which.`, { retryable: false });
242
+ }
243
+ if (status === 400 || status === 422) {
244
+ return new ConnectorCallError("invalid_args", detail);
245
+ }
246
+ if (status >= 500) {
247
+ const wait = retryAfterMs(headers);
248
+ return new ConnectorCallError("unavailable", `${detail} Google is failing upstream.`, wait === undefined ? {} : { retryAfterMs: wait });
249
+ }
250
+ return new ConnectorCallError("connector_call_failed", detail, { retryable: false });
251
+ }
252
+ /** The token the API refused, carried out of the mapper for one replay. */
253
+ class TokenRejected extends Error {
254
+ failure;
255
+ constructor(failure) {
256
+ super(failure.message);
257
+ this.failure = failure;
258
+ }
259
+ }
260
+ /** A Range request against an empty file: Google's verified "nothing here". */
261
+ function isEmptyRange(response) {
262
+ return response.status === 416 && /^bytes \*\/0$/i.test(response.headers.get("content-range")?.trim() ?? "");
263
+ }
264
+ function charsetOf(contentType) {
265
+ return /charset="?([^";\s]+)"?/i.exec(contentType ?? "")?.[1] ?? "utf-8";
266
+ }
267
+ /** Build the delegated, confined, Google-error-mapped transport for one product. */
268
+ export function googleWorkspaceClient(options) {
269
+ const { provider, scopes, connection } = options;
270
+ const failure = {
271
+ provider,
272
+ api: options.api,
273
+ scopes,
274
+ notFound: options.notFound,
275
+ };
276
+ // One resolution per call context: a tool that sends several requests acts
277
+ // as one account throughout, and the mapping runs once rather than per
278
+ // request. The context dies with its call, and so does the entry.
279
+ const subjects = new WeakMap();
280
+ const tokenRequest = async (ctx) => {
281
+ let subject = subjects.get(ctx);
282
+ if (!subject) {
283
+ subject = subjectFor(provider, connection.subject, ctx);
284
+ subjects.set(ctx, subject);
285
+ }
286
+ return { account: connection.account, subject: await subject, scopes };
287
+ };
288
+ // The transport is bound to one token, so the request that a 401 answers
289
+ // names exactly the token to forget. Building one validates the base URL
290
+ // and ceiling at construction, where a structural mistake belongs.
291
+ const transport = (token, facts = { dispatched: false }) => guardedFetch({
292
+ provider,
293
+ baseUrl: options.baseUrl,
294
+ maxResponseBytes: options.maxResponseBytes,
295
+ authenticate: () => ({ Authorization: `Bearer ${token}` }),
296
+ // The one place a request leaves and an answer arrives, recorded as
297
+ // they happen so a failure can say how far the request got.
298
+ fetch: async (url, init) => {
299
+ facts.dispatched = true;
300
+ const response = await fetch(url, init);
301
+ // Recorded before the transport validates anything: a ceiling or a
302
+ // redirect check can fail an answer, but not change what it said.
303
+ facts.status = response.status;
304
+ facts.headers = response.headers;
305
+ return response;
306
+ },
307
+ });
308
+ transport("");
309
+ /**
310
+ * What a failure means for a request that may have changed something. A
311
+ * write Google accepted, or one that left with no answer, may have landed:
312
+ * telling the agent to retry it invites a duplicate, so it is reported as
313
+ * an uncertain outcome that is not retried — the same verdict core reaches
314
+ * for a trusted-pool program write (`write_outcome_unknown`). A read is safe to
315
+ * repeat and keeps its retryable classification.
316
+ */
317
+ /**
318
+ * A 5xx answering a write that is not idempotent. Google can fail after a
319
+ * change commits, so "unavailable, retry" would invite a duplicate insert
320
+ * or a second draft: the outcome is unknown and the write is not retried,
321
+ * whatever reason the 5xx names. Its reasons stay readable beside it.
322
+ */
323
+ function serverErrorOnWrite(mapped, status, method, requestOptions) {
324
+ // Any 5xx, whatever reason it names: a quota reason on a 503 does not
325
+ // prove the write was turned away before it committed. Only a 429, or a
326
+ // 4xx naming a quota, is a rejection that applied nothing.
327
+ if (status < 500 || method === "GET" || requestOptions.idempotent === true) {
328
+ return undefined;
329
+ }
330
+ const unknown = new ConnectorCallError("connector_call_failed", `${provider} answered HTTP ${status} after receiving the request, so its outcome is unknown — it may have been applied. Re-read its target before repeating it.`, { retryable: false, cause: mapped });
331
+ const reasons = googleReasons.get(mapped);
332
+ if (reasons)
333
+ googleReasons.set(unknown, reasons);
334
+ return unknown;
335
+ }
336
+ // The errors built below keep no cause: a stream's or parser's error can
337
+ // quote the body it failed on, and the phase and status already say what
338
+ // happened (#695).
339
+ function settled(error, facts, method, ctx, requestOptions) {
340
+ const write = method !== "GET";
341
+ const status = facts.status;
342
+ if (facts.serverError) {
343
+ return outcome(error, { dispatched: true, phase: "server-error", ...(status === undefined ? {} : { status }) });
344
+ }
345
+ if (facts.refused) {
346
+ return outcome(error, { dispatched: true, phase: "refused", ...(status === undefined ? {} : { status }) });
347
+ }
348
+ // An error status keeps its meaning whatever went wrong reading its body
349
+ // — a body past the ceiling, a stream that broke off: the status alone
350
+ // still says what to do next.
351
+ if (status !== undefined && status >= 400 && !ctx.signal?.aborted) {
352
+ const mapped = apiFailure(failure, status, facts.headers ?? new Headers(), undefined, requestOptions);
353
+ const unknown = serverErrorOnWrite(mapped, status, method, requestOptions);
354
+ return unknown
355
+ ? outcome(unknown, { dispatched: true, status, phase: "server-error" })
356
+ : outcome(mapped, { dispatched: true, status, phase: "refused" });
357
+ }
358
+ if (!facts.dispatched)
359
+ return outcome(error, { dispatched: false, phase: "before-send" });
360
+ // The caller's own cancellation is core's to classify, not this layer's —
361
+ // but what Google had already said by then is still what it said.
362
+ if (ctx.signal?.aborted) {
363
+ const phase = status === undefined
364
+ ? "awaiting-response"
365
+ : status >= 500 && write && requestOptions.idempotent !== true
366
+ ? "server-error"
367
+ : status >= 400
368
+ ? "refused"
369
+ : status >= 300
370
+ ? "redirected"
371
+ : "reading-body";
372
+ return outcome(error, { dispatched: true, phase, ...(status === undefined ? {} : { status }) });
373
+ }
374
+ if (status === undefined) {
375
+ const mapped = write
376
+ ? new ConnectorCallError("connector_call_failed", `${provider} was sent the request but no answer came back, so it may or may not have been applied. Re-read its target before repeating it.`, { retryable: false })
377
+ : error;
378
+ return outcome(mapped, { dispatched: true, phase: "awaiting-response" });
379
+ }
380
+ // A redirect, which the transport refuses to follow. It shows neither
381
+ // that a write applied nor that it did not, so a write's outcome is
382
+ // unknown — never "probably applied". A read keeps the transport's own
383
+ // refusal.
384
+ if (status >= 300 && status < 400) {
385
+ const redirected = write
386
+ ? new ConnectorCallError("connector_call_failed", `${provider} answered HTTP ${status} with a redirect, which this connection does not follow, so whether the request was applied is unknown. Re-read its target before repeating it.`, { retryable: false })
387
+ : error;
388
+ return outcome(redirected, { dispatched: true, status, phase: "redirected" });
389
+ }
390
+ // Google accepted the request — a 2xx — and its reply broke off,
391
+ // overflowed, or would not parse.
392
+ const mapped = write
393
+ ? new ConnectorCallError("connector_call_failed", `${provider} accepted the request but its reply could not be read; the change probably applied. Re-read before repeating it.`, { retryable: false })
394
+ : error instanceof ConnectorCallError
395
+ ? error
396
+ : new ConnectorCallError("unavailable", `${provider}'s reply broke off while it was being read; reading again is safe.`);
397
+ return outcome(mapped, { dispatched: true, status, phase: "reading-body" });
398
+ }
399
+ async function call(request, ctx, accept, read, requestOptions = {}) {
400
+ // A 401 is answered by sending the same request again, so its body must
401
+ // survive being sent twice. A stream is consumed by the first send; it
402
+ // is a provider's wiring mistake, refused before anything leaves.
403
+ if (typeof ReadableStream !== "undefined" && request.rawBody instanceof ReadableStream) {
404
+ throw outcome(new TypeError(`${provider}: a Workspace request body must be replayable — pass a string, Uint8Array, ArrayBuffer, Blob, FormData, or URLSearchParams, never a ReadableStream.`), { dispatched: false, phase: "before-send" });
405
+ }
406
+ const headed = { ...request, headers: { Accept: accept, ...request.headers } };
407
+ for (let attempt = 0;; attempt += 1) {
408
+ const facts = { dispatched: false };
409
+ let delegated;
410
+ let token;
411
+ try {
412
+ // Subject first: no caller, or no mapped account, fails before any
413
+ // token is minted or request sent.
414
+ delegated = await tokenRequest(ctx);
415
+ token = await delegatedToken(delegated, ctx);
416
+ }
417
+ catch (error) {
418
+ throw outcome(error, { dispatched: false, phase: "before-send" });
419
+ }
420
+ try {
421
+ return await transport(token, facts)(headed, ctx, async (response) => {
422
+ // Every 3xx — the ones the transport refuses and the 300, 304, 305,
423
+ // and 306 it lets through — is a redirect before it is anything
424
+ // else: no body or reason it carries makes it a refusal.
425
+ if (response.status >= 300 && response.status < 400) {
426
+ throw new ConnectorCallError("connector_call_failed", `${provider} answered HTTP ${response.status} with a redirect; this connection talks to exactly one origin and never forwards its credential to another.`, { retryable: false });
427
+ }
428
+ // A bounded read answers an empty file's 416 as an empty result.
429
+ if (!response.ok && !(headed.prefixOnly === true && isEmptyRange(response))) {
430
+ const parsed = await response.jsonResult();
431
+ const payload = "value" in parsed ? parsed.value : undefined;
432
+ const mapped = apiFailure(failure, response.status, response.headers, payload, requestOptions);
433
+ const unknown = serverErrorOnWrite(mapped, response.status, headed.method, requestOptions);
434
+ if (unknown) {
435
+ facts.serverError = true;
436
+ throw unknown;
437
+ }
438
+ facts.refused = true;
439
+ throw response.status === 401 ? new TokenRejected(mapped) : mapped;
440
+ }
441
+ return await read(response);
442
+ });
443
+ }
444
+ catch (error) {
445
+ // Every 401 Google sent earns the refresh and the one replay, whether
446
+ // its body was read or failed the transport's checks first: what the
447
+ // status said about the token does not depend on the body.
448
+ // A caller that has left gets neither: the 401 it saw is reported as
449
+ // it was, and no new token is minted for a request nobody awaits.
450
+ const failed = error instanceof TokenRejected ? error.failure : error;
451
+ const unauthorized = !ctx.signal?.aborted && (error instanceof TokenRejected || facts.status === 401);
452
+ if (!unauthorized)
453
+ throw settled(failed, facts, headed.method, ctx, requestOptions);
454
+ // Only the token this request carried; a newer one stays.
455
+ await forgetDelegatedToken(delegated, token);
456
+ if (attempt > 0 || ctx.signal?.aborted) {
457
+ throw settled(failed, facts, headed.method, ctx, requestOptions);
458
+ }
459
+ }
460
+ }
461
+ }
462
+ return {
463
+ json: (request, ctx, requestOptions) => call(request, ctx, "application/json", async (response) => {
464
+ // Read first, parse second: a body that breaks off while it is being
465
+ // read is a transport failure (a GET may read again), and only text
466
+ // that arrived whole and still is not JSON is malformed.
467
+ const body = await response.text();
468
+ if (body.trim() === "")
469
+ return undefined;
470
+ try {
471
+ return JSON.parse(body);
472
+ }
473
+ catch {
474
+ throw new ConnectorCallError("connector_call_failed", `${provider} returned a successful response that is not JSON.`, { retryable: false });
475
+ }
476
+ }, requestOptions),
477
+ bytes: (request, ctx, accept = "*/*", requestOptions) => read(request, ctx, accept, requestOptions),
478
+ text: async (request, ctx, accept = "text/plain, */*", requestOptions) => {
479
+ const { bytes, ...rest } = await read(request, ctx, accept, requestOptions);
480
+ let text;
481
+ try {
482
+ // A cut prefix may end inside a code point; streaming decode drops
483
+ // the incomplete tail instead of inventing a replacement character.
484
+ text = new TextDecoder(charsetOf(rest.contentType)).decode(bytes, { stream: rest.truncated });
485
+ }
486
+ catch {
487
+ text = new TextDecoder().decode(bytes, { stream: rest.truncated });
488
+ }
489
+ return { text, ...rest };
490
+ },
491
+ };
492
+ /** The body as bytes: whole, or a bounded prefix when `maxBytes` is set. */
493
+ async function read(request, ctx, accept, requestOptions = {}) {
494
+ const { maxBytes } = requestOptions;
495
+ if (maxBytes !== undefined && (!Number.isInteger(maxBytes) || maxBytes < 0)) {
496
+ throw outcome(new TypeError(`${provider}: maxBytes must be a whole number of bytes, zero or more.`), {
497
+ dispatched: false,
498
+ phase: "before-send",
499
+ });
500
+ }
501
+ const bounded = maxBytes !== undefined;
502
+ return call(bounded ? { ...request, prefixOnly: true } : request, ctx, accept, async (response) => {
503
+ const facts = {
504
+ contentType: response.headers.get("content-type") ?? undefined,
505
+ status: response.status,
506
+ contentRange: response.headers.get("content-range") ?? undefined,
507
+ };
508
+ if (!bounded)
509
+ return { bytes: await response.bytes(), truncated: false, ...facts };
510
+ if (!response.ok) {
511
+ // Only the empty-file 416 reaches here; its body is not the file.
512
+ await response.prefix(0);
513
+ return { bytes: new Uint8Array(), truncated: false, ...facts };
514
+ }
515
+ return { ...(await response.prefix(maxBytes)), ...facts };
516
+ }, requestOptions);
517
+ }
518
+ }
@@ -1,4 +1,4 @@
1
- import type { Connector, ConnectorCallAdmissionPolicy } from "../types.js";
1
+ import type { ConnectorCallAdmissionPolicy } from "../../types.js";
2
2
  /**
3
3
  * Basecamp's one hosted MCP endpoint, streamable HTTP.
4
4
  *
@@ -21,18 +21,8 @@ export interface BasecampOptions {
21
21
  authScope?: "shared" | "personal";
22
22
  /** Which Basecamp account this is and what decisions it answers. */
23
23
  purpose: string;
24
- /**
25
- * Required. The HTTPS URL of a Client ID Metadata Document this deployment
26
- * controls: a public JSON document whose `client_id` is its own URL, whose
27
- * `redirect_uris` include `<publicUrl>/oauth/callback/<connector id>`, with
28
- * `token_endpoint_auth_method: "none"` and the `authorization_code` and
29
- * `refresh_token` grants. Basecamp advertises dynamic client registration
30
- * but restricts it for HTTPS redirect URIs, which every deployed connecta
31
- * has; its authorization server accepts a metadata-document client id
32
- * instead (`client_id_metadata_document_supported: true`). Connecta does not
33
- * serve one of its own yet, so the deployment hosts it.
34
- */
35
- clientMetadataUrl: string;
24
+ /** Optional external CIMD URL. Defaults to this deployment's public HTTPS document. */
25
+ clientMetadataUrl?: string;
36
26
  /** Account-specific conventions appended to the maintained provider guide. */
37
27
  instructions?: string;
38
28
  /** Connector-specific inline result limit; omit to inherit the deployment. */
@@ -47,19 +37,7 @@ export interface BasecampOptions {
47
37
  */
48
38
  callAdmission?: ConnectorCallAdmissionPolicy;
49
39
  }
50
- /**
51
- * One release-reviewed manifest, used both to classify a live tool and as the
52
- * baseline the runtime drift check compares against, so the annotation a
53
- * caller gets and the verdict a check reads can never disagree (P13). Names
54
- * and verdicts only — no schemas are vendored; the live `tools/list` response
55
- * stays authoritative.
56
- *
57
- * Basecamp publishes no tool inventory, so this list has no public document
58
- * to be checked against before a release. Catalog drift is visible at runtime
59
- * only — as unclassified-tool and unserved-name counts on connector status and
60
- * `/health` — and the credential-free provider check reads the OAuth
61
- * discovery metadata the server does publish instead.
62
- */
63
- export declare const BASECAMP_VETTED_CATALOG: import("../catalog-drift.js").VettedCatalog;
64
40
  /** A maintained Basecamp hosted-MCP connection. */
65
- export declare function basecamp(id: string, options: BasecampOptions): Connector;
41
+ export declare const basecamp: import("../../provider.js").ProviderFactory<BasecampOptions>;
42
+ /** @deprecated Read `basecamp.definition.classify` instead. Kept for existing imports. */
43
+ export declare const BASECAMP_VETTED_CATALOG: import("../../catalog-drift.js").VettedCatalog;