@zackbart/connecta 0.28.1 → 0.30.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 (332) hide show
  1. package/CHANGELOG.md +855 -5
  2. package/README.md +125 -116
  3. package/bin/connecta.mjs +121 -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/argument-redaction.js +230 -0
  12. package/dist/auth/clerk-sdk/client.d.ts +65 -0
  13. package/dist/auth/clerk-sdk/client.js +5 -0
  14. package/dist/auth/clerk-sdk/generated.js +1845 -0
  15. package/dist/auth/clerk-transport.js +19 -0
  16. package/dist/auth/clerk.d.ts +17 -7
  17. package/dist/auth/clerk.js +189 -107
  18. package/dist/auth/cloudflare-access.js +25 -26
  19. package/dist/auth/downstream-client-metadata.d.ts +29 -0
  20. package/dist/auth/downstream-client-metadata.js +133 -0
  21. package/dist/auth/downstream-oauth.js +1584 -1580
  22. package/dist/auth/downstream-revocation.js +106 -0
  23. package/dist/auth/oauth-token-response.js +231 -0
  24. package/dist/auth/oauth-v2-migration.js +116 -0
  25. package/dist/auth/static-oauth.js +93 -71
  26. package/dist/auth-elicitation.js +288 -0
  27. package/dist/authorization-handoff.js +111 -0
  28. package/dist/branding.d.ts +8 -16
  29. package/dist/branding.js +26 -59
  30. package/dist/byte-read-response.js +39 -0
  31. package/dist/call-admission.js +12 -16
  32. package/dist/catalog-cache.js +739 -0
  33. package/dist/catalog-drift.d.ts +50 -15
  34. package/dist/catalog-drift.js +365 -128
  35. package/dist/catalog-fingerprint.js +2 -4
  36. package/dist/catalog-freshness.js +13 -0
  37. package/dist/catalog-limits.js +2 -2
  38. package/dist/catalog-service.js +409 -225
  39. package/dist/catalog.js +43 -84
  40. package/dist/concurrency.js +3 -4
  41. package/dist/config-defaults.d.ts +39 -0
  42. package/dist/config-defaults.js +46 -0
  43. package/dist/config-schema.d.ts +181 -0
  44. package/dist/config-schema.js +435 -0
  45. package/dist/config-value-sources.js +102 -0
  46. package/dist/config.d.ts +633 -0
  47. package/dist/config.js +508 -0
  48. package/dist/connector-access.d.ts +1 -0
  49. package/dist/connector-access.js +13 -15
  50. package/dist/connector-caller.d.ts +8 -0
  51. package/dist/connector-caller.js +15 -6
  52. package/dist/connector-scope.js +1 -1
  53. package/dist/connectors/api-connector.d.ts +59 -7
  54. package/dist/connectors/api-connector.js +106 -19
  55. package/dist/connectors/api.js +4 -1
  56. package/dist/connectors/guarded-fetch.d.ts +144 -0
  57. package/dist/connectors/guarded-fetch.js +87 -38
  58. package/dist/connectors/negotiation-cache.js +46 -0
  59. package/dist/connectors/option-shapes.js +27 -0
  60. package/dist/connectors/remote-mcp.d.ts +30 -4
  61. package/dist/connectors/remote-mcp.js +1774 -320
  62. package/dist/connectors/resource-uri.js +335 -0
  63. package/dist/credential-contract.d.ts +10 -0
  64. package/dist/credential-rules.js +1 -3
  65. package/dist/credential-url.js +108 -0
  66. package/dist/credentials.d.ts +6 -1
  67. package/dist/credentials.js +27 -18
  68. package/dist/d1.d.ts +130 -0
  69. package/dist/d1.js +290 -0
  70. package/dist/describe-config.d.ts +165 -0
  71. package/dist/describe-config.js +315 -0
  72. package/dist/described.js +106 -0
  73. package/dist/downstream-input-context.js +128 -0
  74. package/dist/downstream-input.js +325 -0
  75. package/dist/errors.d.ts +132 -31
  76. package/dist/errors.js +175 -87
  77. package/dist/execute.js +798 -318
  78. package/dist/executor-admission.d.ts +1 -0
  79. package/dist/executor-admission.js +11 -15
  80. package/dist/executor-contract.d.ts +20 -0
  81. package/dist/executor-contract.js +48 -0
  82. package/dist/executor-result.js +30 -13
  83. package/dist/executors/quickjs-child.js +15 -9
  84. package/dist/executors/quickjs-protocol.js +5 -6
  85. package/dist/executors/quickjs-runtime.js +152 -59
  86. package/dist/executors/quickjs.js +49 -39
  87. package/dist/favicon.js +1 -2
  88. package/dist/fix-prompt.js +1 -3
  89. package/dist/guest-runtime.js +238 -0
  90. package/dist/guest-types.d.ts +218 -0
  91. package/dist/guest-types.js +1 -0
  92. package/dist/html-security.js +24 -0
  93. package/dist/identity.js +1 -3
  94. package/dist/inbound-credential.js +52 -0
  95. package/dist/index.d.ts +24 -256
  96. package/dist/index.js +65 -478
  97. package/dist/invocation-auth.js +69 -0
  98. package/dist/invocation.js +262 -79
  99. package/dist/json-schema.d.ts +1 -1
  100. package/dist/mcp-client-context.js +35 -0
  101. package/dist/mcp-result.js +11 -7
  102. package/dist/meta-output.js +61 -0
  103. package/dist/meta-tool-names.d.ts +2 -0
  104. package/dist/meta-tool-names.js +3 -0
  105. package/dist/meta-tools.js +419 -272
  106. package/dist/module-contracts.d.ts +8 -23
  107. package/dist/node.d.ts +0 -2
  108. package/dist/node.js +2 -2
  109. package/dist/oauth-callback-outcome.js +1 -3
  110. package/dist/oauth-handoff.js +194 -0
  111. package/dist/oauth-sealing.js +20 -0
  112. package/dist/operator-record.d.ts +137 -0
  113. package/dist/operator-record.js +420 -0
  114. package/dist/operator-ui/assets.js +31 -0
  115. package/dist/operator-ui/contract.d.ts +72 -0
  116. package/dist/operator-ui/contract.js +1 -0
  117. package/dist/operator-ui/fix-prompts.d.ts +18 -0
  118. package/dist/operator-ui/generated.js +5 -4
  119. package/dist/operator-ui/model.d.ts +12 -87
  120. package/dist/operator-ui/model.js +2 -9
  121. package/dist/operator-ui/setup-commands.js +1 -3
  122. package/dist/operator-ui/view.d.ts +323 -0
  123. package/dist/operator-ui/view.js +62 -189
  124. package/dist/page-styles.js +2 -3
  125. package/dist/program-result.js +5 -0
  126. package/dist/program-source.js +14 -2
  127. package/dist/{exempt-writes.js → program-writes.js} +60 -19
  128. package/dist/provider.d.ts +142 -0
  129. package/dist/provider.js +187 -0
  130. package/dist/providers/_shared/google/result-size.js +57 -0
  131. package/dist/providers/_shared/google/service-account.d.ts +58 -0
  132. package/dist/providers/_shared/google/service-account.js +500 -0
  133. package/dist/providers/_shared/google/workspace.d.ts +276 -0
  134. package/dist/providers/_shared/google/workspace.js +518 -0
  135. package/dist/providers/_shared/request-token.js +83 -0
  136. package/dist/providers/{basecamp.d.ts → basecamp/index.d.ts} +6 -28
  137. package/dist/providers/basecamp/index.js +997 -0
  138. package/dist/providers/basecamp/skill.generated.js +9 -0
  139. package/dist/providers/{breeze.d.ts → breeze/index.d.ts} +2 -2
  140. package/dist/providers/{breeze.js → breeze/index.js} +184 -140
  141. package/dist/providers/breeze/skill.generated.js +9 -0
  142. package/dist/providers/{ccb.d.ts → ccb/index.d.ts} +2 -2
  143. package/dist/providers/{ccb.js → ccb/index.js} +271 -116
  144. package/dist/providers/ccb/skill.generated.js +9 -0
  145. package/dist/providers/{cloudflare.d.ts → cloudflare/index.d.ts} +8 -7
  146. package/dist/providers/{cloudflare.js → cloudflare/index.js} +171 -738
  147. package/dist/providers/cloudflare/skill.generated.js +10 -0
  148. package/dist/providers/docs/index.d.ts +54 -0
  149. package/dist/providers/docs/index.js +1237 -0
  150. package/dist/providers/docs/skill.generated.js +9 -0
  151. package/dist/providers/drive/index.d.ts +53 -0
  152. package/dist/providers/drive/index.js +1523 -0
  153. package/dist/providers/drive/skill.generated.js +9 -0
  154. package/dist/providers/forms/index.d.ts +54 -0
  155. package/dist/providers/forms/index.js +1217 -0
  156. package/dist/providers/forms/skill.generated.js +11 -0
  157. package/dist/providers/github/auth.d.ts +32 -0
  158. package/dist/providers/github/auth.js +361 -0
  159. package/dist/providers/github/hosted.js +218 -0
  160. package/dist/providers/github/index.d.ts +13 -0
  161. package/dist/providers/github/index.js +140 -0
  162. package/dist/providers/github/key.js +100 -0
  163. package/dist/providers/github/rest.js +321 -0
  164. package/dist/providers/github/scope.d.ts +29 -0
  165. package/dist/providers/github/scope.js +124 -0
  166. package/dist/providers/github/skill.generated.js +8 -0
  167. package/dist/providers/gmail/index.d.ts +51 -0
  168. package/dist/providers/gmail/index.js +1436 -0
  169. package/dist/providers/gmail/skill.generated.js +9 -0
  170. package/dist/providers/infisical/index.d.ts +7 -0
  171. package/dist/providers/infisical/index.js +713 -0
  172. package/dist/providers/infisical/skill.generated.js +8 -0
  173. package/dist/providers/{linear.d.ts → linear/index.d.ts} +11 -14
  174. package/dist/providers/linear/index.js +352 -0
  175. package/dist/providers/linear/skill.generated.js +8 -0
  176. package/dist/providers/{mixpanel.d.ts → mixpanel/index.d.ts} +5 -10
  177. package/dist/providers/mixpanel/index.js +416 -0
  178. package/dist/providers/mixpanel/skill.generated.js +9 -0
  179. package/dist/providers/{notion.d.ts → notion/index.d.ts} +8 -8
  180. package/dist/providers/{notion.js → notion/index.js} +226 -239
  181. package/dist/providers/notion/skill.generated.js +10 -0
  182. package/dist/providers/{overflow.d.ts → overflow/index.d.ts} +2 -2
  183. package/dist/providers/{overflow.js → overflow/index.js} +74 -111
  184. package/dist/providers/overflow/skill.generated.js +8 -0
  185. package/dist/providers/{planning-center.d.ts → planning-center/index.d.ts} +2 -2
  186. package/dist/providers/{planning-center.js → planning-center/index.js} +264 -104
  187. package/dist/providers/planning-center/skill.generated.js +10 -0
  188. package/dist/providers/{revenuecat.d.ts → revenuecat/index.d.ts} +5 -10
  189. package/dist/providers/revenuecat/index.js +557 -0
  190. package/dist/providers/revenuecat/skill.generated.js +11 -0
  191. package/dist/providers/sheets/index.d.ts +54 -0
  192. package/dist/providers/sheets/index.js +1237 -0
  193. package/dist/providers/sheets/skill.generated.js +10 -0
  194. package/dist/providers/slides/index.d.ts +53 -0
  195. package/dist/providers/slides/index.js +2645 -0
  196. package/dist/providers/slides/skill.generated.js +9 -0
  197. package/dist/providers/{stripe.d.ts → stripe/index.d.ts} +5 -12
  198. package/dist/providers/stripe/index.js +266 -0
  199. package/dist/providers/stripe/skill.generated.js +11 -0
  200. package/dist/providers/{tithely.d.ts → tithely/index.d.ts} +2 -2
  201. package/dist/providers/{tithely.js → tithely/index.js} +60 -91
  202. package/dist/providers/tithely/skill.generated.js +8 -0
  203. package/dist/providers/{vercel.d.ts → vercel/index.d.ts} +8 -8
  204. package/dist/providers/vercel/index.js +1038 -0
  205. package/dist/providers/vercel/mcp-ownership.js +212 -0
  206. package/dist/providers/vercel/skill.generated.js +11 -0
  207. package/dist/registry.d.ts +110 -171
  208. package/dist/registry.js +588 -927
  209. package/dist/request-state.js +21 -0
  210. package/dist/resource-read.js +103 -0
  211. package/dist/result-shapes.js +17 -49
  212. package/dist/retired-toolkits.js +1 -1
  213. package/dist/routes/access-tokens.js +3 -6
  214. package/dist/routes/activity-context.js +21 -0
  215. package/dist/routes/activity-disclosure.js +40 -0
  216. package/dist/routes/activity.js +59 -21
  217. package/dist/routes/actor-labels.js +3 -50
  218. package/dist/routes/connect.js +221 -0
  219. package/dist/routes/credentials.js +15 -27
  220. package/dist/routes/mcp.js +365 -130
  221. package/dist/routes/oauth-client-metadata.js +15 -0
  222. package/dist/routes/oauth-management.js +45 -107
  223. package/dist/routes/oauth.js +110 -57
  224. package/dist/routes/operator.js +3 -3
  225. package/dist/routes/shared.d.ts +25 -64
  226. package/dist/routes/shared.js +160 -80
  227. package/dist/routes/skills.js +30 -0
  228. package/dist/routes/ui-config.js +225 -0
  229. package/dist/routes/ui-data.js +40 -35
  230. package/dist/routes/ui.js +66 -56
  231. package/dist/runtime/admission.js +4 -2
  232. package/dist/runtime/call-admission.js +3 -1
  233. package/dist/runtime/connector-scope.js +1 -4
  234. package/dist/runtime/run.js +10 -39
  235. package/dist/runtime/services.js +12 -117
  236. package/dist/runtime/shared-read.js +1 -3
  237. package/dist/runtime/storage.js +8 -1
  238. package/dist/sent-secrets.d.ts +93 -0
  239. package/dist/sent-secrets.js +1013 -0
  240. package/dist/server.js +28 -71
  241. package/dist/skills.js +416 -174
  242. package/dist/sqlite.d.ts +48 -0
  243. package/dist/sqlite.js +181 -0
  244. package/dist/storage/keys.js +443 -0
  245. package/dist/storage/memory.js +16 -4
  246. package/dist/storage/sql.d.ts +66 -0
  247. package/dist/storage/sql.js +543 -0
  248. package/dist/timeout.js +4 -2
  249. package/dist/tool-name.js +17 -0
  250. package/dist/tool-safety.d.ts +9 -35
  251. package/dist/tool-safety.js +20 -40
  252. package/dist/types.d.ts +336 -56
  253. package/dist/types.js +0 -1
  254. package/dist/ui.d.ts +7 -19
  255. package/dist/ui.js +76 -90
  256. package/dist/url-safety.js +3 -6
  257. package/dist/usage-guide.js +146 -0
  258. package/dist/validate.d.ts +7 -0
  259. package/dist/validate.js +309 -47
  260. package/dist/version.d.ts +1 -7
  261. package/dist/version.js +3 -7
  262. package/dist/worker.d.ts +11 -0
  263. package/dist/worker.js +435 -0
  264. package/documentation/README.md +63 -0
  265. package/documentation/architecture.md +696 -231
  266. package/documentation/auth.md +1069 -546
  267. package/documentation/code-mode.md +662 -356
  268. package/documentation/deploying.md +146 -0
  269. package/documentation/integrating.md +159 -0
  270. package/documentation/meta-tools.md +570 -216
  271. package/documentation/operating.md +135 -0
  272. package/documentation/operator-tests.md +97 -0
  273. package/documentation/operator-ui.md +132 -0
  274. package/documentation/provider-migration-0.29.md +65 -0
  275. package/examples/worker/AGENTS.md +34 -12
  276. package/examples/worker/README.md +293 -255
  277. package/examples/worker/kv-to-d1.wrangler.jsonc +26 -0
  278. package/examples/worker/scripts/copy-kv-to-d1.mjs +162 -0
  279. package/examples/worker/src/connecta.config.ts +151 -0
  280. package/examples/worker/src/index.ts +14 -197
  281. package/examples/worker/wrangler.jsonc +15 -35
  282. package/package.json +126 -54
  283. package/templates/node/.dockerignore +1 -2
  284. package/templates/node/.env.example +20 -21
  285. package/templates/node/AGENTS.md +21 -17
  286. package/templates/node/Dockerfile +3 -5
  287. package/templates/node/README.md +140 -58
  288. package/templates/node/docker-compose.yml +11 -12
  289. package/templates/node/package.json +4 -2
  290. package/templates/node/src/connecta.config.ts +119 -0
  291. package/templates/node/src/index.ts +9 -154
  292. package/templates/node/src/provision-token.ts +17 -0
  293. package/AGENTS.md +0 -170
  294. package/dist/artifacts/connector.d.ts +0 -58
  295. package/dist/artifacts/connector.js +0 -730
  296. package/dist/artifacts/document.js +0 -86
  297. package/dist/artifacts/guide.js +0 -140
  298. package/dist/artifacts/html-scan.js +0 -329
  299. package/dist/artifacts/json.js +0 -78
  300. package/dist/artifacts/kv-store.d.ts +0 -20
  301. package/dist/artifacts/kv-store.js +0 -140
  302. package/dist/artifacts/markdown.js +0 -478
  303. package/dist/artifacts/module.d.ts +0 -37
  304. package/dist/artifacts/module.js +0 -66
  305. package/dist/artifacts/operations.d.ts +0 -197
  306. package/dist/artifacts/operations.js +0 -1002
  307. package/dist/artifacts/refresh.d.ts +0 -55
  308. package/dist/artifacts/refresh.js +0 -236
  309. package/dist/artifacts/routes.js +0 -206
  310. package/dist/artifacts/types.d.ts +0 -214
  311. package/dist/artifacts/types.js +0 -22
  312. package/dist/artifacts/validate.d.ts +0 -62
  313. package/dist/artifacts/validate.js +0 -651
  314. package/dist/artifacts.d.ts +0 -6
  315. package/dist/artifacts.js +0 -9
  316. package/dist/auth/bearer.d.ts +0 -11
  317. package/dist/auth/bearer.js +0 -50
  318. package/dist/providers/basecamp.js +0 -445
  319. package/dist/providers/linear.js +0 -217
  320. package/dist/providers/mixpanel.js +0 -247
  321. package/dist/providers/revenuecat.js +0 -317
  322. package/dist/providers/stripe.js +0 -253
  323. package/dist/providers/vercel.js +0 -1253
  324. package/dist/storage/file.d.ts +0 -15
  325. package/dist/storage/file.js +0 -455
  326. package/ethos.md +0 -93
  327. package/examples/worker/src/cloudflare-kv.ts +0 -43
  328. package/examples/worker/src/d1-activity-row.ts +0 -155
  329. package/examples/worker/src/d1-activity.ts +0 -147
  330. package/examples/worker/src/d1-storage.ts +0 -138
  331. package/examples/worker/src/r2-artifact-blobs.ts +0 -29
  332. package/templates/node/src/file-activity.ts +0 -182
@@ -1,6 +1,7 @@
1
+ import { authorizationCredential, authorizationRequest, isMachineCredential } from "../inbound-credential.js";
2
+ import { htmlSecurityHeaders } from "../html-security.js";
1
3
  import { parseConnectorAccess } from "../connector-access.js";
2
4
  import { identityStorageKey, validIdentityReference } from "../identity.js";
3
- import { isExplicitlyReadOnly } from "../tool-safety.js";
4
5
  export { msg } from "../errors.js";
5
6
  export function privateJson(body, init = {}) {
6
7
  const headers = new Headers(init.headers);
@@ -31,8 +32,75 @@ export function activityActorNamespace(provider) {
31
32
  ? provider.activityActorNamespace
32
33
  : undefined;
33
34
  }
34
- export async function authorize(request, baseUrl, auth, runtimeContext, identityConfig, partitionIdentity = true) {
35
- if (auth.length === 0) {
35
+ /** Metadata routing and challenge selection use the same first actual answer. */
36
+ export async function authMetadata(request, baseUrl, auth) {
37
+ for (const provider of auth) {
38
+ const response = await provider.handleMetadata?.(request, baseUrl);
39
+ if (response)
40
+ return { provider, response };
41
+ }
42
+ return null;
43
+ }
44
+ async function providerChallenge(response, request, baseUrl, auth, refusingProvider) {
45
+ if (response.status !== 401)
46
+ return response;
47
+ if (!response.headers.has("WWW-Authenticate")) {
48
+ const headers = new Headers(response.headers);
49
+ headers.set("WWW-Authenticate", refusingProvider?.challenge?.(request, baseUrl) ?? "Bearer");
50
+ response = new Response(response.body, { status: response.status, statusText: response.statusText, headers });
51
+ }
52
+ const path = new URL(request.url).pathname;
53
+ const pool = /^\/mcp\/([a-z0-9_-]+)$/.exec(path)?.[1];
54
+ const metadataRequest = new Request(new URL(`/.well-known/oauth-protected-resource${pool ? `/mcp/${pool}` : ""}`, baseUrl), { signal: request.signal });
55
+ const owner = await authMetadata(metadataRequest, baseUrl, auth);
56
+ if (!owner)
57
+ return response;
58
+ // Metadata is an ordinary bounded response, never retained across requests.
59
+ await owner.response.body?.cancel();
60
+ const challenge = owner.provider.challenge?.(request, baseUrl);
61
+ if (!challenge)
62
+ return response;
63
+ const headers = new Headers(response.headers);
64
+ headers.set("WWW-Authenticate", challenge);
65
+ return new Response(response.body, { status: response.status, statusText: response.statusText, headers });
66
+ }
67
+ function recognizedProvider(request, auth, runtimeContext) {
68
+ for (const provider of auth) {
69
+ if (!provider.recognizesCredential)
70
+ continue;
71
+ const recognized = provider.recognizesCredential(request, runtimeContext);
72
+ if (typeof recognized !== "boolean") {
73
+ // A misimplemented hook may return a rejected Promise. Refuse without
74
+ // letting its arbitrary rejection reach an unhandled-rejection sink.
75
+ void Promise.resolve(recognized).catch(() => { });
76
+ throw new Error("invalid credential recognition verdict");
77
+ }
78
+ if (recognized)
79
+ return provider;
80
+ }
81
+ return undefined;
82
+ }
83
+ export async function authorize(request, baseUrl, auth, runtimeContext, identityConfig, partitionIdentity = true, interactiveOnly = false) {
84
+ const credential = authorizationCredential(request);
85
+ const explicit = credential.kind !== "absent";
86
+ if (credential.kind === "invalid") {
87
+ const challenge = auth.find((provider) => provider.kind !== "access_token")?.challenge?.(request, baseUrl) ?? "Bearer";
88
+ return {
89
+ ok: false,
90
+ response: await providerChallenge(privateJson({ error: "unauthorized" }, {
91
+ status: 401,
92
+ headers: { "WWW-Authenticate": challenge },
93
+ }), request, baseUrl, auth),
94
+ };
95
+ }
96
+ request = authorizationRequest(request);
97
+ if (explicit && runtimeContext)
98
+ runtimeContext = { waitUntil: runtimeContext.waitUntil.bind(runtimeContext) };
99
+ const machineCredential = isMachineCredential(request);
100
+ if (interactiveOnly && machineCredential) {
101
+ return { ok: false, response: privateJson({ error: "authenticated user required" }, { status: 403 }) };
102
+ }
103
+ if (auth.length === 0 && !interactiveOnly && !explicit) {
36
104
  const actor = { kind: "anonymous" };
37
105
  const identity = { actor, interactive: false };
38
106
  let access;
@@ -45,28 +113,63 @@ export async function authorize(request, baseUrl, auth, runtimeContext, identity
45
113
  response: privateJson({ error: "identity access resolution failed" }, { status: 403 }),
46
114
  };
47
115
  }
48
- return { ok: true, actor, identity, ...access, operator: false, accessTokenManagement: false, credentialAdministration: "none", personalConnection: "none" };
116
+ return {
117
+ ok: true,
118
+ actor,
119
+ identity,
120
+ ...access,
121
+ operator: false,
122
+ accessTokenManagement: false,
123
+ credentialAdministration: "none",
124
+ personalConnection: "none",
125
+ };
126
+ }
127
+ let recognized;
128
+ try {
129
+ recognized = recognizedProvider(request, machineCredential ? auth.filter((provider) => provider.kind === "access_token") : auth, runtimeContext);
130
+ }
131
+ catch {
132
+ return { ok: false, response: privateJson({ error: "credential recognition failed" }, { status: 403 }) };
133
+ }
134
+ if (machineCredential && !recognized) {
135
+ return {
136
+ ok: false,
137
+ response: await providerChallenge(privateJson({ error: "unauthorized" }, {
138
+ status: 401,
139
+ headers: { "WWW-Authenticate": "Bearer" },
140
+ }), request, baseUrl, auth),
141
+ };
142
+ }
143
+ if (interactiveOnly && recognized && !recognized.interactiveOperator) {
144
+ return { ok: false, response: privateJson({ error: "authenticated user required" }, { status: 403 }) };
49
145
  }
50
146
  let lastResponse = null;
51
- for (const provider of auth) {
147
+ const eligible = auth.filter((provider) => (!interactiveOnly || provider.interactiveOperator) &&
148
+ (!explicit || machineCredential || provider.kind !== "access_token" || !provider.recognizesCredential));
149
+ // A header owns one verdict even for adapters without a recognition hook.
150
+ const candidates = recognized ? [recognized] : explicit ? eligible.slice(0, 1) : eligible;
151
+ for (const provider of candidates) {
52
152
  const result = await provider.authorize(request, baseUrl, runtimeContext);
53
153
  if (result.ok) {
154
+ if (interactiveOnly && !result.userId) {
155
+ return { ok: false, response: privateJson({ error: "authenticated user required" }, { status: 403 }) };
156
+ }
54
157
  const subjectId = result.subjectId ?? result.userId;
55
158
  const actorNamespace = activityActorNamespace(provider);
56
- const derivedPrincipal = result.userId && actorNamespace
57
- ? { namespace: actorNamespace, id: result.userId }
58
- : undefined;
59
- const principal = validIdentityReference(result.principal)
60
- ? result.principal
61
- : derivedPrincipal;
159
+ const derivedPrincipal = result.userId && actorNamespace ? { namespace: actorNamespace, id: result.userId } : undefined;
160
+ const principal = validIdentityReference(result.principal) ? result.principal : derivedPrincipal;
62
161
  const subject = subjectId
63
162
  ? { namespace: actorNamespace ?? `connecta:auth:${provider.kind}`, id: subjectId }
64
163
  : principal;
65
164
  const interactive = Boolean(result.userId && provider.interactiveOperator);
165
+ // Principal-only API callers still need a stable typed actor for history
166
+ // ownership. Never collapse distinct personal partitions into { kind }.
167
+ const actorId = subjectId ?? principal?.id;
168
+ const recordNamespace = actorNamespace ?? (subjectId === undefined ? principal?.namespace : undefined);
66
169
  const actor = {
67
170
  kind: provider.kind,
68
- ...(subjectId ? { id: subjectId } : {}),
69
- ...(subjectId && actorNamespace ? { namespace: actorNamespace } : {}),
171
+ ...(actorId ? { id: actorId } : {}),
172
+ ...(actorId && recordNamespace ? { namespace: recordNamespace } : {}),
70
173
  };
71
174
  const identity = {
72
175
  actor,
@@ -81,22 +184,30 @@ export async function authorize(request, baseUrl, auth, runtimeContext, identity
81
184
  let access;
82
185
  try {
83
186
  if (identityConfig?.activityAccess) {
84
- operator = interactive && principal
85
- ? await identityConfig.activityAccess(principal)
86
- : false;
187
+ operator = principal ? await identityConfig.activityAccess(principal) : false;
87
188
  }
88
189
  access = parseConnectorAccess(identityConfig?.connectorAccess ? await identityConfig.connectorAccess(identity) : "all", { allowReadOnly: true });
89
190
  if (interactive) {
90
- accessTokenManagement = identityConfig?.accessTokenManagement ? await identityConfig.accessTokenManagement(identity) : false;
191
+ accessTokenManagement = identityConfig?.accessTokenManagement
192
+ ? await identityConfig.accessTokenManagement(identity)
193
+ : false;
91
194
  if (typeof accessTokenManagement !== "boolean")
92
195
  throw new Error("invalid token management permission");
93
- credentialAdministration = identityConfig?.credentialAdministration ? await identityConfig.credentialAdministration(identity) : "none";
94
- personalConnection = principal && identityConfig?.personalConnection ? await identityConfig.personalConnection(identity) : "none";
196
+ credentialAdministration = identityConfig?.credentialAdministration
197
+ ? await identityConfig.credentialAdministration(identity)
198
+ : "none";
199
+ personalConnection =
200
+ principal && identityConfig?.personalConnection
201
+ ? await identityConfig.personalConnection(identity)
202
+ : "none";
95
203
  }
96
204
  if (typeof operator !== "boolean")
97
205
  throw new Error("invalid activity permission");
98
206
  for (const permission of [credentialAdministration, personalConnection]) {
99
- if (permission !== "all" && permission !== "none" && (!Array.isArray(permission) || !permission.every(id => typeof id === "string" && /^[a-z0-9_-]+$/.test(id))))
207
+ if (permission !== "all" &&
208
+ permission !== "none" &&
209
+ (!Array.isArray(permission) ||
210
+ !permission.every((id) => typeof id === "string" && /^[a-z0-9_-]+$/.test(id))))
100
211
  throw new Error("invalid identity permission");
101
212
  }
102
213
  }
@@ -110,57 +221,39 @@ export async function authorize(request, baseUrl, auth, runtimeContext, identity
110
221
  ok: true,
111
222
  actor,
112
223
  identity,
113
- ...(subject && partitionIdentity
114
- ? { subjectKey: await identityStorageKey(subject) }
115
- : {}),
116
- ...(principal && partitionIdentity
117
- ? { principalKey: await identityStorageKey(principal) }
118
- : {}),
224
+ ...(result.sessionCookies?.length ? { sessionCookies: result.sessionCookies } : {}),
225
+ ...(subject && partitionIdentity ? { subjectKey: await identityStorageKey(subject) } : {}),
226
+ ...(principal && partitionIdentity ? { principalKey: await identityStorageKey(principal) } : {}),
119
227
  ...access,
120
228
  accessTokenManagement,
121
229
  credentialAdministration,
122
230
  personalConnection,
123
231
  operator,
124
- ...(operator ? { uiAdminEligible: true } : {}),
232
+ ...(interactive && operator ? { uiAdminEligible: true } : {}),
125
233
  };
126
234
  }
127
235
  lastResponse = result.response;
236
+ if (recognized || result.response.status !== 401)
237
+ break;
128
238
  }
129
239
  return {
130
240
  ok: false,
131
- response: lastResponse ??
132
- new Response(JSON.stringify({ error: "unauthorized" }), {
241
+ response: await providerChallenge(lastResponse ??
242
+ privateJson({ error: "unauthorized" }, {
133
243
  status: 401,
134
- headers: {
135
- "Content-Type": "application/json",
136
- "WWW-Authenticate": "Bearer",
137
- },
138
- }),
244
+ headers: { "WWW-Authenticate": "Bearer" },
245
+ }), request, baseUrl, auth, explicit ? candidates[0] : undefined),
139
246
  };
140
247
  }
248
+ /** Human routes reject machine credentials before any verification or storage I/O. */
141
249
  export async function authorizeUiIdentity(request, baseUrl, auth, purpose, runtimeContext, identityConfig) {
142
- const providers = auth.filter((candidate) => candidate.interactiveOperator);
143
- if (providers.length === 0) {
250
+ if (!auth.some((provider) => provider.interactiveOperator)) {
144
251
  return {
145
252
  ok: false,
146
253
  response: privateJson({ error: `${purpose} requires interactive user authentication` }, { status: 403 }),
147
254
  };
148
255
  }
149
- let lastResponse;
150
- for (const provider of providers) {
151
- const authz = await authorize(request, baseUrl, [provider], runtimeContext, identityConfig);
152
- if (!authz.ok) {
153
- lastResponse = authz.response;
154
- continue;
155
- }
156
- if (authz.identity.interactive)
157
- return authz;
158
- lastResponse = privateJson({ error: "authenticated user required" }, { status: 403 });
159
- }
160
- return {
161
- ok: false,
162
- response: lastResponse ?? privateJson({ error: `${purpose} requires interactive user authentication` }, { status: 403 }),
163
- };
256
+ return authorize(request, baseUrl, auth, runtimeContext, identityConfig, true, true);
164
257
  }
165
258
  export function isSameOrigin(request, baseUrl) {
166
259
  const origin = request.headers.get("origin");
@@ -174,7 +267,9 @@ export function isSameOrigin(request, baseUrl) {
174
267
  }
175
268
  }
176
269
  export function withSecurityHeaders(response, requestUrl, _path) {
177
- const headers = new Headers(response.headers);
270
+ const headers = response.headers.get("Content-Type")?.toLowerCase().startsWith("text/html")
271
+ ? htmlSecurityHeaders(response.headers)
272
+ : new Headers(response.headers);
178
273
  headers.set("X-Content-Type-Options", "nosniff");
179
274
  headers.set("Referrer-Policy", "no-referrer");
180
275
  if (requestUrl.protocol === "https:") {
@@ -188,44 +283,29 @@ export function withSecurityHeaders(response, requestUrl, _path) {
188
283
  }
189
284
  /** Unknown configured ids refuse the complete view, including management rights. */
190
285
  export function validateAuthPermissions(authz, registry) {
191
- for (const value of [
192
- authz.connectorIds,
193
- authz.credentialAdministration,
194
- authz.personalConnection,
195
- ]) {
286
+ for (const value of [authz.connectorIds, authz.credentialAdministration, authz.personalConnection]) {
196
287
  if (value === "all" || value === "none")
197
288
  continue;
198
- if (!Array.isArray(value) || value.some(id => !registry.getConnector(id))) {
289
+ if (!Array.isArray(value) || value.some((id) => !registry.getConnector(id))) {
199
290
  throw new Error("invalid identity permission connector ids");
200
291
  }
201
292
  }
202
293
  }
203
- /**
204
- * Whether this identity may open artifact pages: exactly whoever may read the
205
- * `artifacts` connector — it is visible to them and `get_artifact` is among
206
- * its tools they were granted. Viewing grants nothing a program could not
207
- * already read (ethos, "Human routes manage auth, never capability").
208
- */
209
- export function mayViewArtifacts(authz, registry) {
210
- if (authz.connectorIds !== "all" && !authz.connectorIds.includes("artifacts")) {
211
- return false;
212
- }
213
- const tools = authz.toolAccess?.get("artifacts");
214
- if (tools && !tools.has("get_artifact"))
215
- return false;
216
- if (!authz.guardedToolAccess?.get("artifacts")?.has("get_artifact"))
217
- return true;
218
- return registry.getConnector("artifacts")?.staticTools?.some((tool) => tool.name === "get_artifact" && isExplicitlyReadOnly(tool)) === true;
219
- }
220
294
  export function mayManageConnector(authz, connector) {
221
295
  if (!authz.identity.interactive)
222
296
  return false;
223
297
  if (authz.connectorIds !== "all" && !authz.connectorIds.includes(connector.id)) {
224
298
  return false;
225
299
  }
226
- const permission = connector.authScope === "personal"
227
- ? authz.personalConnection
228
- : authz.credentialAdministration;
229
- return permission === "all" ||
230
- (permission !== "none" && permission.includes(connector.id));
300
+ const permission = connector.authScope === "personal" ? authz.personalConnection : authz.credentialAdministration;
301
+ return permission === "all" || (permission !== "none" && permission.includes(connector.id));
302
+ }
303
+ /** Preserve refreshed browser cookies on the redirect or completion page. */
304
+ export function withSessionCookies(response, cookies) {
305
+ if (!cookies?.length)
306
+ return response;
307
+ const headers = new Headers(response.headers);
308
+ for (const cookie of cookies)
309
+ headers.append("Set-Cookie", cookie);
310
+ return new Response(response.body, { status: response.status, statusText: response.statusText, headers });
231
311
  }
@@ -0,0 +1,30 @@
1
+ import { ProtocolError } from "@modelcontextprotocol/server";
2
+ import { z } from "zod";
3
+ import { ConnectorCallError } from "../errors.js";
4
+ import { SkillsRegistry } from "../skills.js";
5
+ /** Native Skills methods use the same request-view registry as both text readers. */
6
+ export function registerSkills(server, view, baseUrl, options) {
7
+ // McpServer's resource constructor installs its static registry and turns
8
+ // listChanged on. Declare the capability on the low-level server instead.
9
+ server.server.registerCapabilities({ resources: {} });
10
+ const registry = new SkillsRegistry(view, baseUrl, options);
11
+ const listParams = z.object({ cursor: z.string().max(128).optional() });
12
+ const getParams = z.object({ uri: z.string().min(1).max(32_768).startsWith("skill://") });
13
+ const result = z.record(z.string(), z.unknown());
14
+ const safely = async (read) => {
15
+ try {
16
+ return await read();
17
+ }
18
+ catch (error) {
19
+ if (error instanceof ConnectorCallError && (error.code === "not_found" || error.code === "invalid_args")) {
20
+ throw new ProtocolError(-32602, "Unknown skill/file or invalid skills cursor.");
21
+ }
22
+ // No raw downstream cause can become an SDK/operator diagnostic.
23
+ throw new ProtocolError(-32603, "Skills are unavailable.");
24
+ }
25
+ };
26
+ server.server.setRequestHandler("skills/list", { params: listParams, result }, (params) => safely(() => registry.list(params.cursor)));
27
+ server.server.setRequestHandler("skills/get", { params: getParams, result }, (params) => safely(() => registry.get(params.uri)));
28
+ server.server.setRequestHandler("resources/list", { params: listParams, result }, (params) => safely(() => registry.resources(params.cursor)));
29
+ server.server.setRequestHandler("resources/read", { params: getParams, result }, (params) => safely(() => registry.read(params.uri)));
30
+ }
@@ -0,0 +1,225 @@
1
+ import { activityEventVisible } from "./activity-disclosure.js";
2
+ import { operatorActivityContext } from "./activity-context.js";
3
+ import { bindActivityRequest } from "../activity-request.js";
4
+ import { configValuePaths } from "../config-value-sources.js";
5
+ import { Effect } from "effect";
6
+ import { intersectAccess } from "../connector-access.js";
7
+ import { describedTools } from "../described.js";
8
+ import { resolveDiscoveryConcurrency } from "../concurrency.js";
9
+ import { recordedToolName } from "../operator-record.js";
10
+ import { closeScope } from "../runtime/connector-scope.js";
11
+ import { withDeadlineEffect } from "../runtime/run.js";
12
+ import { uiProblemFor } from "../ui.js";
13
+ import { authorized, serveOperator, visibleRegistry } from "./operator.js";
14
+ import { mayManageConnector, privateJson } from "./shared.js";
15
+ const attempt = (operation) => Effect.tryPromise({ try: operation, catch: (error) => error });
16
+ const validName = (name) => recordedToolName({ name }) === name;
17
+ function grants(access, ids) {
18
+ return ids
19
+ .filter((id) => access.connectorIds === "all" || access.connectorIds.includes(id))
20
+ .map((connectorId) => {
21
+ const tools = access.toolAccess?.get(connectorId);
22
+ return {
23
+ connectorId,
24
+ tools: tools
25
+ ? [...tools].filter(validName).map((name) => ({
26
+ name,
27
+ requireReadOnly: access.guardedToolAccess?.get(connectorId)?.has(name) === true,
28
+ }))
29
+ : "all",
30
+ };
31
+ });
32
+ }
33
+ function toolVisible(authz, id, name, classification) {
34
+ return (validName(name) &&
35
+ (!authz.toolAccess?.get(id) || authz.toolAccess.get(id).has(name)) &&
36
+ (!authz.guardedToolAccess?.get(id)?.has(name) || classification === "read"));
37
+ }
38
+ function observe(context, registry, connector, activity) {
39
+ const { opts, baseUrl, request, defer } = context;
40
+ const scope = {};
41
+ if (activity)
42
+ bindActivityRequest(scope, activity);
43
+ const id = connector.id;
44
+ return withDeadlineEffect((signal) => Effect.gen(function* () {
45
+ const drift = yield* attempt(() => registry.credentialDriftFor(id));
46
+ const status = drift
47
+ ? { state: "auth_required" }
48
+ : yield* attempt(() => registry.statusFor(id, baseUrl, scope, { signal, ...(defer ? { defer } : {}) }));
49
+ let catalogFailed = false;
50
+ const tools = status.state === "ok"
51
+ ? yield* attempt(() => registry.getTools(id, baseUrl, scope, { signal, ...(defer ? { defer } : {}) })).pipe(Effect.catch(() => {
52
+ catalogFailed = true;
53
+ return Effect.succeed([]);
54
+ }))
55
+ : [];
56
+ const problem = uiProblemFor(connector, status.state, { credentialDrift: Boolean(drift), catalogFailed });
57
+ return {
58
+ id,
59
+ status: status.state,
60
+ ...(status.registrationPath ? { auth: { registrationPath: status.registrationPath } } : {}),
61
+ ...(problem ? { problem } : {}),
62
+ // Catalog metadata is allowed for authenticated operators. Only the name
63
+ // and its derived address are withheld when they fail the record grammar.
64
+ tools: describedTools(tools).map((tool) => {
65
+ const name = recordedToolName(tool);
66
+ return {
67
+ ...tool,
68
+ name,
69
+ address: `${id}.${name}`,
70
+ classification: tool.classification === "read" ? "read" : "write",
71
+ };
72
+ }),
73
+ catalogAgeMs: registry.catalogAgeMs(id),
74
+ lastCall: null,
75
+ };
76
+ }), {
77
+ timeoutMs: opts.config.discovery.probeTimeoutMs,
78
+ signal: request.signal,
79
+ timeoutError: new Error("Operator config observation timed out."),
80
+ }).pipe(Effect.catchDefect((defect) => Effect.fail(defect)), Effect.catch(() => Effect.succeed({
81
+ id,
82
+ status: "error",
83
+ problem: "connector_unavailable",
84
+ tools: [],
85
+ catalogAgeMs: registry.catalogAgeMs(id),
86
+ lastCall: null,
87
+ })), Effect.ensuring(Effect.suspend(() => closeScope(connector, registry.contextFor(id, baseUrl, scope), defer))));
88
+ }
89
+ const OUTCOMES = new Set([
90
+ "success",
91
+ "error",
92
+ "timeout",
93
+ "cancelled",
94
+ "paused",
95
+ "approved",
96
+ ]);
97
+ const isTimestamp = (value) => /^\d{4}-\d\d-\d\dT\d\d:\d\d:\d\d\.\d{3}Z$/.test(value) && Number.isFinite(Date.parse(value));
98
+ function lastCalls(context, authz, registry, rows, admittedPools) {
99
+ const activity = context.opts.config.activity;
100
+ if (!activity?.store.list)
101
+ return Effect.succeed("unconfigured");
102
+ if (!authz.operator)
103
+ return Effect.succeed("forbidden");
104
+ return Effect.gen(function* () {
105
+ const permitted = activity.readGate
106
+ ? yield* attempt(async () => (await activity.readGate(authz.actor)) === true).pipe(Effect.orElseSucceed(() => false))
107
+ : true;
108
+ if (!permitted)
109
+ return "forbidden";
110
+ return yield* Effect.gen(function* () {
111
+ const byId = new Map(rows.map((row) => [row.id, row]));
112
+ let cursor;
113
+ const seen = new Set();
114
+ // Bound the read even when a custom reader repeats a cursor or ignores
115
+ // its requested page size. Each page belongs to the request fiber, so
116
+ // cancellation cannot start the next page.
117
+ for (let pageIndex = 0; pageIndex < 10; pageIndex++) {
118
+ const page = yield* attempt(() => activity.store.list({ limit: 100, ...(cursor ? { cursor } : {}) }));
119
+ for (const event of page.events.slice(0, 100)) {
120
+ if (event.kind === "catalog_drift")
121
+ continue;
122
+ const row = byId.get(event.connectorId);
123
+ if (!row || !isTimestamp(event.occurredAt) || !OUTCOMES.has(event.outcome))
124
+ continue;
125
+ if (!activityEventVisible(context, authz, registry, admittedPools, event))
126
+ continue;
127
+ if (!row.lastCall || event.occurredAt > row.lastCall.at)
128
+ row.lastCall = { at: event.occurredAt, outcome: event.outcome };
129
+ }
130
+ cursor = page.nextCursor;
131
+ if (!cursor || seen.has(cursor))
132
+ break;
133
+ seen.add(cursor);
134
+ }
135
+ return "available";
136
+ }).pipe(Effect.catchDefect((defect) => Effect.fail(defect)), Effect.orElseSucceed(() => {
137
+ for (const row of rows)
138
+ row.lastCall = null;
139
+ return "unavailable";
140
+ }));
141
+ });
142
+ }
143
+ function configRead(context) {
144
+ const { opts } = context;
145
+ return Effect.gen(function* () {
146
+ const authz = yield* authorized(context);
147
+ const registry = yield* visibleRegistry(context, authz);
148
+ const visible = registry.listConnectors();
149
+ const ids = visible.map((c) => c.id);
150
+ const pools = [];
151
+ for (const [name, pool] of opts.pools) {
152
+ const admitted = yield* attempt(async () => (await pool.grant(authz.identity)) === true).pipe(Effect.orElseSucceed(() => false));
153
+ if (admitted)
154
+ pools.push({
155
+ name,
156
+ path: `/mcp/${name}`,
157
+ trust: pool.trust,
158
+ grants: grants(intersectAccess(authz, pool.access), ids),
159
+ });
160
+ }
161
+ const config = structuredClone(opts.configDescription);
162
+ config.connectors = config.connectors
163
+ .filter((c) => ids.includes(c.id))
164
+ .map((c) => ({
165
+ ...c,
166
+ ...(c.tools
167
+ ? { tools: c.tools.filter((tool) => toolVisible(authz, c.id, tool.name, tool.classification)) }
168
+ : {}),
169
+ }));
170
+ config.classification = Object.fromEntries(Object.entries(config.classification)
171
+ .filter(([id]) => ids.includes(id))
172
+ .map(([id, tools]) => [
173
+ id,
174
+ Object.fromEntries(Object.entries(tools).filter(([name, classification]) => toolVisible(authz, id, name, classification))),
175
+ ]));
176
+ config.pools = config.pools.flatMap((pool) => {
177
+ const admitted = pools.find((p) => p.name === pool.name);
178
+ if (!admitted)
179
+ return [];
180
+ return [
181
+ {
182
+ ...pool,
183
+ tools: admitted.grants.flatMap((grant) => grant.tools === "all"
184
+ ? [grant.connectorId]
185
+ : grant.tools.map((tool) => `${grant.connectorId}.${tool.name}`)),
186
+ },
187
+ ];
188
+ });
189
+ const requestActivity = operatorActivityContext(context, authz);
190
+ const rows = yield* Effect.forEach(visible, (c) => observe(context, registry, c, requestActivity), {
191
+ concurrency: resolveDiscoveryConcurrency(opts.config.discovery.concurrency),
192
+ });
193
+ const activity = yield* lastCalls(context, authz, registry, rows, new Set(pools.map((pool) => pool.name)));
194
+ const contract = {
195
+ schemaVersion: 1,
196
+ config,
197
+ configSources: Object.fromEntries(configValuePaths(config).map((path) => [path, opts.configValueSources?.[path] ?? "config"])),
198
+ live: { connectors: rows, activity },
199
+ you: {
200
+ interactive: authz.identity.interactive,
201
+ grants: grants(authz, ids),
202
+ trust: opts.config.trust,
203
+ pools,
204
+ permissions: {
205
+ activity: activity === "available" || activity === "unavailable",
206
+ accessTokenManagement: Boolean(opts.config.accessTokens && authz.accessTokenManagement && authz.identity.principal),
207
+ connectors: visible.map((c) => ({
208
+ id: c.id,
209
+ use: true,
210
+ manageSharedAuth: c.authScope !== "personal" && mayManageConnector(authz, c),
211
+ connectPersonal: c.authScope === "personal" && mayManageConnector(authz, c),
212
+ })),
213
+ },
214
+ },
215
+ };
216
+ return privateJson(contract);
217
+ });
218
+ }
219
+ export async function routeUiConfig(context) {
220
+ if (context.path !== "/ui/api/config")
221
+ return null;
222
+ if (context.request.method !== "GET")
223
+ return privateJson({ error: "method not allowed" }, { status: 405 });
224
+ return serveOperator(configRead(context), context.request.signal);
225
+ }