@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,144 @@
1
+ import type { ConnectorContext } from "../types.js";
2
+ /** The methods a hand-written provider surface actually uses. */
3
+ type GuardedMethod = "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
4
+ export interface GuardedRequest {
5
+ method: GuardedMethod;
6
+ /**
7
+ * Provider-relative path beginning with one `/`, resolved against the base
8
+ * URL's own path. Never an absolute URL, and never carrying `?` or `#` —
9
+ * query parameters go in `query`, where they are encoded rather than parsed.
10
+ */
11
+ path: string;
12
+ /**
13
+ * Search parameters; `undefined` values are dropped, others stringified. An
14
+ * array repeats its key once per element, in order — the spelling of the key
15
+ * itself (`ids` or `ids[]`) is the provider's convention, not the helper's.
16
+ */
17
+ query?: Record<string, string | number | boolean | readonly (string | number | boolean)[] | undefined>;
18
+ /**
19
+ * Per-request headers. `undefined` values are dropped, and a header that
20
+ * collides with an authentication header is refused rather than allowed to
21
+ * shadow it.
22
+ */
23
+ headers?: Record<string, string | undefined>;
24
+ /** JSON request body. Serialized here, with the `Content-Type` to match. */
25
+ body?: unknown;
26
+ /**
27
+ * A pre-framed body — `FormData`, text, bytes. Mutually exclusive with
28
+ * `body`, and no `Content-Type` is supplied: multipart needs `fetch` to pick
29
+ * the boundary, and anything else knows its own type.
30
+ */
31
+ rawBody?: BodyInit;
32
+ /**
33
+ * The mapper reads only a bounded prefix of a successful body, with
34
+ * `response.prefix()`, so a declared length past the ceiling is not refused
35
+ * up front: the ceiling still bounds what is read, never what is sent.
36
+ */
37
+ prefixOnly?: boolean;
38
+ }
39
+ /**
40
+ * One downstream response, read under the connector's byte ceiling.
41
+ *
42
+ * The accessors are the whole read surface — there is no escape to the
43
+ * underlying `Response`, because an unbounded `.text()` on it is exactly the
44
+ * mistake this type exists to remove. `status`, `ok`, and `headers` are the
45
+ * raw facts a provider mapper needs to make its own classification.
46
+ */
47
+ interface GuardedResponse {
48
+ readonly status: number;
49
+ readonly ok: boolean;
50
+ readonly headers: Headers;
51
+ /** The body as bytes. */
52
+ bytes(): Promise<Uint8Array>;
53
+ /** The body decoded as UTF-8. */
54
+ text(): Promise<string>;
55
+ /** The body parsed as JSON; `undefined` for an empty body, throws on junk. */
56
+ json(): Promise<unknown>;
57
+ /** Parse JSON while distinguishing malformed content from transport failure. */
58
+ jsonResult(): Promise<{
59
+ value: unknown;
60
+ } | {
61
+ parseError: unknown;
62
+ }>;
63
+ /**
64
+ * At most `maxBytes` of the body (never past the ceiling), then the rest of
65
+ * the stream cancelled unread. Exactly: it retains at most `maxBytes` bytes,
66
+ * and consumes from the transport at most `maxBytes` plus one chunk — the
67
+ * chunk that crosses the bound, whose overflow is how it learns there was
68
+ * more (`truncated`). Chunks are whatever size the runtime delivers (64 KiB
69
+ * is common), so that one chunk is the whole of the overrun. Exclusive with
70
+ * the other readers: a body is read once.
71
+ */
72
+ prefix(maxBytes: number): Promise<{
73
+ bytes: Uint8Array;
74
+ truncated: boolean;
75
+ }>;
76
+ }
77
+ /** Parse delta-seconds or an HTTP-date into a non-negative wait window. */
78
+ export declare function retryAfterMs(headers: Headers): number | undefined;
79
+ /**
80
+ * Turn one response into the provider's own result, or throw the provider's
81
+ * own typed failure. This is where status codes acquire meaning, and it is
82
+ * always the provider's code.
83
+ */
84
+ type GuardedResponseMapper<T> = (response: GuardedResponse, ctx: ConnectorContext) => T | Promise<T>;
85
+ export interface GuardedFetchOptions {
86
+ /**
87
+ * Provider name as it appears in normalized failure prose — "Cloudflare",
88
+ * "Notion". Used for messages only; it is never parsed.
89
+ */
90
+ provider: string;
91
+ /**
92
+ * Absolute base URL. Its origin and path prefix are the confinement: every
93
+ * request resolves beneath it or is refused.
94
+ */
95
+ baseUrl: string;
96
+ /**
97
+ * Headers proving the caller's identity, resolved once per request. Throwing
98
+ * a typed `auth_required` from here is the documented way to report a
99
+ * missing credential — the helper has no opinion about what a credential is.
100
+ */
101
+ authenticate: (ctx: ConnectorContext) => Record<string, string> | Promise<Record<string, string>>;
102
+ /**
103
+ * What actually sends a request, after confinement, framing, and
104
+ * authentication. Defaults to the global `fetch`. A connector that declares
105
+ * `oauth` supplies {@link oauthBearer}'s, so the calling owner's grant — not
106
+ * a header this helper assembled — authenticates the request. A rejection
107
+ * that is already a `ConnectorCallError` passes through unchanged; any other
108
+ * is normalized to `unavailable`, exactly as a global `fetch` failure is.
109
+ */
110
+ fetch?: GuardedFetcher;
111
+ /** Constant headers sent with every request — `Accept`, an API version. */
112
+ headers?: Record<string, string>;
113
+ /**
114
+ * Ceiling on the response body, in bytes. Required, because a default here
115
+ * would be the helper guessing on a provider's behalf: what counts as an
116
+ * absurd response is a fact about the API, not about HTTP.
117
+ */
118
+ maxResponseBytes: number;
119
+ }
120
+ /** Sends one already-guarded request on behalf of one call's context. */
121
+ type GuardedFetcher = (url: string, init: RequestInit, ctx: ConnectorContext) => Promise<Response>;
122
+ /**
123
+ * The transport half of a guarded connector that authenticates through its
124
+ * own `api()` OAuth grant: no header of the helper's making, and every request
125
+ * sent through `ctx.oauth.fetch`, which adds the calling owner's bearer token,
126
+ * refuses any origin outside the grant's `apiOrigins`, and spends one
127
+ * coordinated refresh and one replay on a 401 before answering
128
+ * `auth_required`. Spread it into the options:
129
+ * `guardedFetch({ provider, baseUrl, maxResponseBytes, ...oauthBearer(provider) })`.
130
+ *
131
+ * Confinement still happens here first, so a request reaches the grant only
132
+ * once it is provably beneath `baseUrl`; the grant's origin check is the second
133
+ * fence, not the only one.
134
+ */
135
+ export declare function oauthBearer(provider: string): Pick<GuardedFetchOptions, "authenticate" | "fetch">;
136
+ /** Send one guarded request and map its response with provider knowledge. */
137
+ export type GuardedTransport = <T>(request: GuardedRequest, ctx: ConnectorContext, map: GuardedResponseMapper<T>) => Promise<T>;
138
+ /**
139
+ * Build the transport a connector sends every request through. `maxResponseBytes`
140
+ * is required rather than defaulted: what counts as an absurd response is a fact
141
+ * about the API, not about HTTP.
142
+ */
143
+ export declare function guardedFetch(options: GuardedFetchOptions): GuardedTransport;
144
+ export {};
@@ -13,6 +13,7 @@
13
13
  * public surface. Web APIs only.
14
14
  */
15
15
  import { ConnectorCallError, unavailableCallError } from "../errors.js";
16
+ import { sentSecretsFor } from "../sent-secrets.js";
16
17
  /** Parse delta-seconds or an HTTP-date into a non-negative wait window. */
17
18
  export function retryAfterMs(headers) {
18
19
  const raw = headers.get("retry-after");
@@ -29,12 +30,36 @@ export function retryAfterMs(headers) {
29
30
  const date = Date.parse(raw);
30
31
  return Number.isFinite(date) ? Math.max(0, date - Date.now()) : undefined;
31
32
  }
33
+ /**
34
+ * The transport half of a guarded connector that authenticates through its
35
+ * own `api()` OAuth grant: no header of the helper's making, and every request
36
+ * sent through `ctx.oauth.fetch`, which adds the calling owner's bearer token,
37
+ * refuses any origin outside the grant's `apiOrigins`, and spends one
38
+ * coordinated refresh and one replay on a 401 before answering
39
+ * `auth_required`. Spread it into the options:
40
+ * `guardedFetch({ provider, baseUrl, maxResponseBytes, ...oauthBearer(provider) })`.
41
+ *
42
+ * Confinement still happens here first, so a request reaches the grant only
43
+ * once it is provably beneath `baseUrl`; the grant's origin check is the second
44
+ * fence, not the only one.
45
+ */
46
+ export function oauthBearer(provider) {
47
+ const send = (url, init, ctx) => {
48
+ const grant = ctx.oauth;
49
+ if (!grant) {
50
+ // A wiring bug, not an outage: the transport was built for a grant the
51
+ // connector does not have, so no request can be authenticated.
52
+ throw new ConnectorCallError("connector_call_failed", `The ${provider} transport authenticates through the connector's OAuth grant, and this call's context carries none.`, { retryable: false });
53
+ }
54
+ return grant.fetch(url, init);
55
+ };
56
+ return { authenticate: () => ({}), fetch: send };
57
+ }
32
58
  /** Statuses that instruct a client to re-send somewhere else. Never followed. */
33
59
  const REDIRECT_STATUSES = new Set([301, 302, 303, 307, 308]);
34
60
  /** Statuses the HTTP spec defines as carrying no body at all. */
35
61
  const BODILESS_STATUSES = new Set([204, 205, 304]);
36
62
  const decoder = new TextDecoder();
37
- const encoder = new TextEncoder();
38
63
  /**
39
64
  * A base URL is deployment configuration, so a bad one is a structural mistake
40
65
  * and throws where it is written rather than on the first call that uses it.
@@ -47,9 +72,7 @@ function confinementBase(provider, baseUrl) {
47
72
  catch {
48
73
  throw new Error(`${provider} baseUrl must be an absolute URL: ${baseUrl}`);
49
74
  }
50
- const loopback = base.hostname === "localhost" ||
51
- base.hostname === "127.0.0.1" ||
52
- base.hostname === "[::1]";
75
+ const loopback = base.hostname === "localhost" || base.hostname === "127.0.0.1" || base.hostname === "[::1]";
53
76
  if (base.protocol !== "https:" && !(base.protocol === "http:" && loopback)) {
54
77
  throw new Error(`${provider} baseUrl must be https (http is allowed only for a loopback proxy or test double): ${baseUrl}`);
55
78
  }
@@ -84,13 +107,18 @@ function confinedUrl(provider, base, basePath, request) {
84
107
  catch {
85
108
  throw new ConnectorCallError("invalid_args", `A ${provider} request path did not resolve to a valid URL.`);
86
109
  }
87
- if (url.origin !== base.origin ||
88
- !`${url.pathname}/`.startsWith(`${basePath}/`)) {
110
+ if (url.origin !== base.origin || !`${url.pathname}/`.startsWith(`${basePath}/`)) {
89
111
  throw new ConnectorCallError("invalid_args", `A ${provider} request path escaped the connector's base URL.`);
90
112
  }
91
113
  for (const [key, value] of Object.entries(request.query ?? {})) {
92
114
  if (value === undefined)
93
115
  continue;
116
+ if (Array.isArray(value)) {
117
+ url.searchParams.delete(key);
118
+ for (const item of value)
119
+ url.searchParams.append(key, String(item));
120
+ continue;
121
+ }
94
122
  url.searchParams.set(key, String(value));
95
123
  }
96
124
  return url;
@@ -101,9 +129,7 @@ function oversized(provider, limit, detail) {
101
129
  /** The response body as a `ReadableStream`, or null when there is none to read. */
102
130
  function readableBody(response) {
103
131
  const body = response.body;
104
- return body && typeof body.getReader === "function"
105
- ? body
106
- : null;
132
+ return body && typeof body.getReader === "function" ? body : null;
107
133
  }
108
134
  /** Read a stream to the ceiling and stop there — never past it. */
109
135
  async function drain(provider, stream, limit) {
@@ -139,9 +165,9 @@ async function drain(provider, stream, limit) {
139
165
  /**
140
166
  * Every read is capped at `maxResponseBytes`: a streaming body is abandoned at
141
167
  * the ceiling rather than buffered past it, and where a runtime gives no body
142
- * stream the text is measured in UTF-8 bytes before it is accepted, with JSON
143
- * parsed from that same bounded text. Such a runtime still buffers internally,
144
- * but cannot return an oversized body as a successful result.
168
+ * stream the buffered bytes are measured before they are accepted, with text
169
+ * and JSON decoded from them. Such a runtime still buffers internally, but
170
+ * cannot return an oversized body as a successful result.
145
171
  *
146
172
  * A mapper must re-throw `ConnectorCallError` and swallow only what it
147
173
  * recognizes: a bare `catch` around `response.json()` eats this refusal along
@@ -159,9 +185,22 @@ function boundedResponse(provider, response, limit) {
159
185
  text: async () => "",
160
186
  json: emptyJson,
161
187
  jsonResult: () => jsonResult(emptyJson),
188
+ prefix: async () => ({ bytes: new Uint8Array(), truncated: false }),
162
189
  };
163
190
  }
164
191
  const stream = readableBody(response);
192
+ const prefix = async (maxBytes) => {
193
+ const keep = Math.min(Math.max(0, Math.trunc(maxBytes)), limit);
194
+ if (stream)
195
+ return await readPrefix(stream, keep);
196
+ // A runtime with no body stream buffers the body regardless; the
197
+ // ceiling still bounds what is accepted.
198
+ const buffer = new Uint8Array(await response.arrayBuffer());
199
+ if (buffer.byteLength > limit && keep === limit) {
200
+ throw oversized(provider, limit, `${buffer.byteLength} bytes`);
201
+ }
202
+ return { bytes: buffer.slice(0, keep), truncated: buffer.byteLength > keep };
203
+ };
165
204
  let read;
166
205
  const bytes = () => {
167
206
  read ??= stream
@@ -176,14 +215,9 @@ function boundedResponse(provider, response, limit) {
176
215
  };
177
216
  let readText;
178
217
  const text = () => {
179
- readText ??= stream
180
- ? bytes().then((body) => decoder.decode(body))
181
- : response.text().then((body) => {
182
- const size = encoder.encode(body).length;
183
- if (size > limit)
184
- throw oversized(provider, limit, `${size} bytes`);
185
- return body;
186
- });
218
+ // Bytes, then UTF-8, never `response.text()`: workerd quotes a text
219
+ // read's non-text Content-Type in its own log (INV-6).
220
+ readText ??= bytes().then((body) => decoder.decode(body));
187
221
  return readText;
188
222
  };
189
223
  const json = async () => {
@@ -198,8 +232,51 @@ function boundedResponse(provider, response, limit) {
198
232
  text,
199
233
  json,
200
234
  jsonResult: () => jsonResult(json),
235
+ prefix,
201
236
  };
202
237
  }
238
+ /**
239
+ * Read until a chunk crosses `keep` — at most one chunk past it — keep the
240
+ * first `keep` bytes, and cancel the stream: the source stops producing
241
+ * instead of being drained to the ceiling for bytes nobody will look at.
242
+ * A BYOB reader could stop at `keep + 1` exactly, but not every runtime's
243
+ * fetch body offers one, and one chunk is a bounded overrun.
244
+ */
245
+ async function readPrefix(stream, keep) {
246
+ const reader = stream.getReader();
247
+ const chunks = [];
248
+ let total = 0;
249
+ let truncated = false;
250
+ try {
251
+ while (true) {
252
+ const { done, value } = await reader.read();
253
+ if (done)
254
+ break;
255
+ if (!value)
256
+ continue;
257
+ if (total + value.byteLength > keep) {
258
+ chunks.push(value.subarray(0, keep - total));
259
+ total = keep;
260
+ truncated = true;
261
+ break;
262
+ }
263
+ chunks.push(value);
264
+ total += value.byteLength;
265
+ }
266
+ if (truncated)
267
+ await reader.cancel().catch(() => { });
268
+ }
269
+ finally {
270
+ reader.releaseLock();
271
+ }
272
+ const bytes = new Uint8Array(total);
273
+ let offset = 0;
274
+ for (const chunk of chunks) {
275
+ bytes.set(chunk, offset);
276
+ offset += chunk.byteLength;
277
+ }
278
+ return { bytes, truncated };
279
+ }
203
280
  async function jsonResult(read) {
204
281
  try {
205
282
  return { value: await read() };
@@ -224,7 +301,8 @@ export function guardedFetch(options) {
224
301
  // "" for a root base, so joining never doubles the separator.
225
302
  const basePath = base.pathname.replace(/\/+$/, "");
226
303
  const constantHeaders = { ...options.headers };
227
- return async function send(request, ctx, map) {
304
+ const send = options.fetch ?? ((url, init) => fetch(url, init));
305
+ return async function guarded(request, ctx, map) {
228
306
  if (request.body !== undefined && request.rawBody !== undefined) {
229
307
  throw new Error(`A ${provider} request cannot carry both a JSON body and a raw body.`);
230
308
  }
@@ -246,10 +324,11 @@ export function guardedFetch(options) {
246
324
  throw new ConnectorCallError("invalid_args", `A ${provider} request may not set the ${shadow} header; authentication is connector-owned.`);
247
325
  }
248
326
  headers[name] = value;
327
+ sentSecretsFor(ctx).header(value);
249
328
  }
250
329
  let response;
251
330
  try {
252
- response = await fetch(url.toString(), {
331
+ const init = {
253
332
  method: request.method,
254
333
  headers,
255
334
  ...(request.body !== undefined
@@ -262,19 +341,23 @@ export function guardedFetch(options) {
262
341
  // never one. Refused below rather than followed.
263
342
  redirect: "manual",
264
343
  ...(ctx.signal ? { signal: ctx.signal } : {}),
265
- });
344
+ };
345
+ sentSecretsFor(ctx).request(url, init);
346
+ response = await send(url.toString(), init, ctx);
266
347
  }
267
348
  catch (cause) {
268
349
  if (cause instanceof ConnectorCallError)
269
350
  throw cause;
270
- throw unavailableCallError(cause, url.href, `Could not reach the ${provider} API.`);
351
+ throw unavailableCallError(cause, url.href, `Could not reach the ${provider} API.`, ctx.signal);
271
352
  }
272
353
  if (REDIRECT_STATUSES.has(response.status)) {
273
354
  await response.body?.cancel().catch(() => { });
274
355
  throw new ConnectorCallError("connector_call_failed", `${provider} answered HTTP ${response.status} with a redirect; this connector talks to exactly one origin and never forwards its credential to another.`, { retryable: false });
275
356
  }
276
357
  const declared = Number(response.headers.get("content-length"));
277
- if (Number.isFinite(declared) && declared > limit) {
358
+ // A prefix read takes only what it was asked for, so a long body is not
359
+ // a reason to refuse it; the ceiling still bounds what is read.
360
+ if (Number.isFinite(declared) && declared > limit && !(request.prefixOnly && response.ok)) {
278
361
  await response.body?.cancel().catch(() => { });
279
362
  throw oversized(provider, limit, `a declared ${declared} bytes`);
280
363
  }
@@ -0,0 +1,46 @@
1
+ import { isSpecType } from "@modelcontextprotocol/client";
2
+ import { failureRecord, logFailure } from "../operator-record.js";
3
+ import { sentSecretsForRequest } from "../sent-secrets.js";
4
+ import { assertDownstreamOutputSafe } from "../downstream-input-context.js";
5
+ import { negotiationKeys, NEGOTIATION_TTL_SECONDS } from "../storage/keys.js";
6
+ /** Verdicts carry data only. Clients, signals and secrets remain request-local. */
7
+ export async function readNegotiation(ctx, digest) {
8
+ let prior;
9
+ try {
10
+ const raw = await ctx.storage.get(negotiationKeys.verdict(digest));
11
+ if (!raw)
12
+ return;
13
+ const value = JSON.parse(raw);
14
+ if (!Number.isFinite(value.expiresAt) || value.expiresAt <= Date.now())
15
+ return;
16
+ const candidate = value.prior;
17
+ if ((candidate?.kind === "legacy" && Object.keys(candidate).length === 1) ||
18
+ (candidate?.kind === "modern" && isSpecType.DiscoverResult(candidate.discover)))
19
+ prior = candidate;
20
+ }
21
+ catch (error) {
22
+ logFailure(ctx.logger, "negotiation cache read failed", failureRecord({}, error));
23
+ }
24
+ // Privacy refusals are not cache misses and must not enter cache diagnostics.
25
+ if (prior)
26
+ assertDownstreamOutputSafe(ctx.requestScope ?? ctx, prior);
27
+ return prior;
28
+ }
29
+ export async function storeNegotiation(ctx, digest, prior) {
30
+ if (ctx.signal?.aborted)
31
+ return;
32
+ assertDownstreamOutputSafe(ctx.requestScope ?? ctx, prior);
33
+ // DiscoverResult instructions, metadata and capability keys can echo auth.
34
+ const clean = sentSecretsForRequest(ctx.requestScope ?? ctx).redact(prior);
35
+ if (clean.kind === "modern" && !isSpecType.DiscoverResult(clean.discover))
36
+ return;
37
+ try {
38
+ await ctx.storage.set(negotiationKeys.verdict(digest), JSON.stringify({
39
+ prior: clean,
40
+ expiresAt: Date.now() + NEGOTIATION_TTL_SECONDS * 1000,
41
+ }), { ttlSeconds: NEGOTIATION_TTL_SECONDS });
42
+ }
43
+ catch (error) {
44
+ logFailure(ctx.logger, "negotiation cache write failed", failureRecord({}, error));
45
+ }
46
+ }
@@ -0,0 +1,27 @@
1
+ // Closed option shapes shared by the built-in connector factories: `api()`,
2
+ // `remoteMcp()`, and every maintained provider. Each factory walks its options
3
+ // against its shape with `assertKnownOptions` before reading a value, so a
4
+ // misspelled `maxResultByte` or `requireHTTPs` throws at construction with its
5
+ // path instead of silently doing nothing (INV-11). Values stay the factory's
6
+ // to validate; handlers, schemas, and loggers pass through as `opaque()`.
7
+ import { array, keys, optionsOf, strings, variants } from "../config-schema.js";
8
+ export { CALL_ADMISSION, PROVIDER_COMMON } from "../provider.js";
9
+ export const CREDENTIAL = optionsOf()({
10
+ ...keys("label", "description", "placeholder"),
11
+ fields: array(optionsOf()(keys("name", "label", "description", "placeholder", "inputType"))),
12
+ });
13
+ /** The structured form; a string guide has no keys to check. */
14
+ export const USAGE_GUIDE = optionsOf()(keys("content", "summary", "required"));
15
+ /** `remoteMcp()`'s `auth`, closed per `type`. */
16
+ export const REMOTE_MCP_AUTH = variants("type", {
17
+ request: optionsOf()({ ...keys("type", "token"), headers: strings() }).shape,
18
+ headers: optionsOf()({ ...keys("type"), headers: strings() }).shape,
19
+ credential: optionsOf()({
20
+ ...keys("type", "header", "scheme"),
21
+ credential: CREDENTIAL,
22
+ }).shape,
23
+ oauth: optionsOf()({
24
+ ...keys("type", "clientMetadataUrl", "scope"),
25
+ client: optionsOf()(keys("issuer", "clientId", "clientSecret", "tokenEndpointAuthMethod")),
26
+ }).shape,
27
+ });
@@ -1,6 +1,7 @@
1
+ import { type RemoteOAuthClient } from "../auth/downstream-client-metadata.js";
1
2
  import type { FetchLike, Transport } from "@modelcontextprotocol/client";
2
3
  import { ConnectorCallError } from "../errors.js";
3
- import type { Connector, ConnectorCallAdmissionPolicy, ConnectorContext, ConnectorCredentialConfig, ConnectorUsageGuide, Logger } from "../types.js";
4
+ import type { Connector, ConnectorCallAdmissionPolicy, ConnectorContext, ConnectorCredentialConfig, ConnectorUsageGuide, Logger, ToolClassification } from "../types.js";
4
5
  /**
5
6
  * A static downstream credential supplied through the connection in the
6
7
  * operator UI rather than baked into deployment source.
@@ -34,12 +35,20 @@ interface RemoteMcpCredentialAuth {
34
35
  scheme?: string | null;
35
36
  }
36
37
  export type RemoteMcpAuth = {
38
+ type: "request";
39
+ /** Resolve a Bearer token inside this request. Never persisted or described. */
40
+ token: (ctx: ConnectorContext) => Promise<string>;
41
+ /** Static protocol/catalog headers, never Authorization. */
42
+ headers?: Record<string, string>;
43
+ } | {
37
44
  type: "headers";
38
45
  headers: Record<string, string>;
39
46
  } | RemoteMcpCredentialAuth | {
40
47
  type: "oauth";
41
48
  /** Public HTTPS client metadata document, for servers supporting URL-based client IDs. */
42
49
  clientMetadataUrl?: string;
50
+ /** Pre-registered client, bound to its configured issuer; excludes clientMetadataUrl. */
51
+ client?: RemoteOAuthClient;
43
52
  /** Space-separated default scopes when the resource server does not advertise them. */
44
53
  scope?: string;
45
54
  };
@@ -67,10 +76,10 @@ export interface RemoteMcpOptions {
67
76
  authScope?: "shared" | "personal";
68
77
  /**
69
78
  * Max inline result size (bytes) for this connector's tools before
70
- * call_tool truncates and stashes the full text for get_result
79
+ * call_tool truncates and stashes the full text for connecta.result
71
80
  * paging. Overrides the deployment's `calls.maxResultBytes`; omit to inherit
72
- * it. Must be a whole number of bytes >= 1; anything else warns at startup
73
- * and is ignored.
81
+ * it. Must be a whole number of bytes >= 1; anything else refuses to
82
+ * construct.
74
83
  */
75
84
  maxResultBytes?: number;
76
85
  /** Optional per-runtime downstream call-admission policy. */
@@ -81,7 +90,24 @@ export interface RemoteMcpOptions {
81
90
  * discovery metadata. See `Connector.usageGuide`.
82
91
  */
83
92
  usageGuide?: string | ConnectorUsageGuide;
93
+ /** Opt in to downstream Skills listing and resource reads. Defaults to false. */
94
+ skills?: boolean;
84
95
  auth?: RemoteMcpAuth;
96
+ /**
97
+ * Reviewed read/write verdicts for this downstream's tools, by exact name.
98
+ * Validated at construction. A listed read fills downstream silence but
99
+ * never overrules an explicit write annotation; a listed write or
100
+ * destructive tool stays a write whatever the downstream claims; an unlisted
101
+ * tool is read-only only when it says so explicitly; a listed tool whose
102
+ * `schemaDigest` no longer matches, or cannot be checked, is served as a
103
+ * write. Setting it also reports
104
+ * catalog drift against the list: unclassified, unserved, contradicted, and
105
+ * schema-changed tools, as counts. The connector carries it as
106
+ * `classification`, and its `listTools` returns the downstream's listing
107
+ * unclassified: the registry classifies every read. Omit it to keep the
108
+ * downstream's own annotations, which still fail closed.
109
+ */
110
+ classify?: ToolClassification | undefined;
85
111
  /**
86
112
  * Downstream MCP version-negotiation mode. Defaults to `"auto"`, which
87
113
  * probes with `server/discover` and falls back to the legacy lifecycle when