@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,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
+ }
@@ -0,0 +1,43 @@
1
+ import type { ConnectorCallAdmissionPolicy } from "../../types.js";
2
+ /**
3
+ * Basecamp's one hosted MCP endpoint, streamable HTTP.
4
+ *
5
+ * Live but undocumented: as of 2026-10-05 37signals has announced no MCP
6
+ * server, and basecamp.com/agents (where the endpoint's own 401 body points)
7
+ * describes the CLI and SDKs without naming it. What the server does publish is
8
+ * its OAuth discovery — protected-resource metadata naming
9
+ * `https://app.basecamp.com` as the authorization server — and that is what
10
+ * this provider is written against, beside a captured live `tools/list`.
11
+ */
12
+ export declare const BASECAMP_MCP_ENDPOINT = "https://mcp.basecamp.com/mcp";
13
+ export interface BasecampOptions {
14
+ /** Human-readable display name; defaults to "Basecamp". */
15
+ title?: string;
16
+ /**
17
+ * Downstream auth ownership. Defaults to one shared deployment grant. With
18
+ * `"personal"` each principal authorizes separately and may pick a
19
+ * different Basecamp account at consent, which the guide says.
20
+ */
21
+ authScope?: "shared" | "personal";
22
+ /** Which Basecamp account this is and what decisions it answers. */
23
+ purpose: string;
24
+ /** Optional external CIMD URL. Defaults to this deployment's public HTTPS document. */
25
+ clientMetadataUrl?: string;
26
+ /** Account-specific conventions appended to the maintained provider guide. */
27
+ instructions?: string;
28
+ /** Connector-specific inline result limit; omit to inherit the deployment. */
29
+ maxResultBytes?: number;
30
+ /**
31
+ * Optional per-runtime policy. There is no default: Basecamp's API documents
32
+ * several dynamically adjusted limits (GET versus POST, per second, hour,
33
+ * and day) and quotes only "currently 50 requests per 10 second period per
34
+ * IP address" for scale — metered at the IP that calls the API, which is
35
+ * Basecamp's own MCP server rather than this runtime. A per-runtime counter
36
+ * approximates none of that, so the operator picks (P12).
37
+ */
38
+ callAdmission?: ConnectorCallAdmissionPolicy;
39
+ }
40
+ /** A maintained Basecamp hosted-MCP connection. */
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;