@zackbart/connecta 0.28.1 → 0.29.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (319) hide show
  1. package/CHANGELOG.md +788 -5
  2. package/README.md +62 -42
  3. package/bin/connecta.mjs +110 -139
  4. package/bin/meta-tool-names.d.mts +2 -0
  5. package/bin/meta-tool-names.mjs +9 -0
  6. package/bin/version.mjs +3 -0
  7. package/dist/access-tokens.js +53 -63
  8. package/dist/activity-request.js +8 -0
  9. package/dist/activity.d.ts +76 -42
  10. package/dist/activity.js +110 -42
  11. package/dist/auth/clerk-sdk/client.d.ts +65 -0
  12. package/dist/auth/clerk-sdk/client.js +5 -0
  13. package/dist/auth/clerk-sdk/generated.js +1845 -0
  14. package/dist/auth/clerk-transport.js +19 -0
  15. package/dist/auth/clerk.d.ts +17 -7
  16. package/dist/auth/clerk.js +189 -107
  17. package/dist/auth/cloudflare-access.js +25 -26
  18. package/dist/auth/downstream-client-metadata.d.ts +29 -0
  19. package/dist/auth/downstream-client-metadata.js +133 -0
  20. package/dist/auth/downstream-oauth.js +1584 -1580
  21. package/dist/auth/downstream-revocation.js +106 -0
  22. package/dist/auth/oauth-token-response.js +231 -0
  23. package/dist/auth/oauth-v2-migration.js +116 -0
  24. package/dist/auth/static-oauth.js +93 -71
  25. package/dist/auth-elicitation.js +283 -0
  26. package/dist/branding.d.ts +8 -16
  27. package/dist/branding.js +26 -59
  28. package/dist/byte-read-response.js +39 -0
  29. package/dist/call-admission.js +12 -16
  30. package/dist/catalog-cache.js +739 -0
  31. package/dist/catalog-drift.d.ts +50 -15
  32. package/dist/catalog-drift.js +365 -128
  33. package/dist/catalog-fingerprint.js +2 -4
  34. package/dist/catalog-freshness.js +13 -0
  35. package/dist/catalog-limits.js +2 -2
  36. package/dist/catalog-service.js +368 -224
  37. package/dist/catalog.js +43 -84
  38. package/dist/concurrency.js +3 -4
  39. package/dist/config-defaults.d.ts +39 -0
  40. package/dist/config-defaults.js +46 -0
  41. package/dist/config-schema.d.ts +181 -0
  42. package/dist/config-schema.js +435 -0
  43. package/dist/config-value-sources.js +102 -0
  44. package/dist/config.d.ts +631 -0
  45. package/dist/config.js +503 -0
  46. package/dist/connector-access.d.ts +1 -0
  47. package/dist/connector-access.js +13 -15
  48. package/dist/connector-caller.d.ts +8 -0
  49. package/dist/connector-caller.js +15 -6
  50. package/dist/connector-scope.js +1 -1
  51. package/dist/connectors/api-connector.d.ts +59 -7
  52. package/dist/connectors/api-connector.js +106 -19
  53. package/dist/connectors/api.js +4 -1
  54. package/dist/connectors/guarded-fetch.d.ts +144 -0
  55. package/dist/connectors/guarded-fetch.js +84 -35
  56. package/dist/connectors/negotiation-cache.js +46 -0
  57. package/dist/connectors/option-shapes.js +27 -0
  58. package/dist/connectors/remote-mcp.d.ts +30 -4
  59. package/dist/connectors/remote-mcp.js +1774 -320
  60. package/dist/connectors/resource-uri.js +335 -0
  61. package/dist/credential-contract.d.ts +10 -0
  62. package/dist/credential-rules.js +1 -3
  63. package/dist/credential-url.js +108 -0
  64. package/dist/credentials.d.ts +6 -1
  65. package/dist/credentials.js +27 -18
  66. package/dist/d1.d.ts +130 -0
  67. package/dist/d1.js +290 -0
  68. package/dist/describe-config.d.ts +165 -0
  69. package/dist/describe-config.js +315 -0
  70. package/dist/described.js +106 -0
  71. package/dist/downstream-input-context.js +119 -0
  72. package/dist/downstream-input.js +323 -0
  73. package/dist/errors.d.ts +97 -20
  74. package/dist/errors.js +158 -80
  75. package/dist/execute.js +453 -305
  76. package/dist/executor-admission.d.ts +1 -0
  77. package/dist/executor-admission.js +11 -15
  78. package/dist/executor-contract.d.ts +20 -0
  79. package/dist/executor-contract.js +48 -0
  80. package/dist/executor-result.js +24 -9
  81. package/dist/executors/quickjs-child.js +3 -4
  82. package/dist/executors/quickjs-runtime.js +147 -59
  83. package/dist/executors/quickjs.js +29 -35
  84. package/dist/favicon.js +1 -2
  85. package/dist/fix-prompt.js +1 -3
  86. package/dist/guest-runtime.js +188 -0
  87. package/dist/guest-types.d.ts +176 -0
  88. package/dist/guest-types.js +1 -0
  89. package/dist/html-security.js +24 -0
  90. package/dist/identity.js +1 -3
  91. package/dist/inbound-credential.js +52 -0
  92. package/dist/index.d.ts +24 -256
  93. package/dist/index.js +65 -478
  94. package/dist/invocation-auth.js +69 -0
  95. package/dist/invocation.js +230 -73
  96. package/dist/json-schema.d.ts +1 -1
  97. package/dist/mcp-client-context.js +35 -0
  98. package/dist/mcp-result.js +11 -7
  99. package/dist/meta-output.js +76 -0
  100. package/dist/meta-tool-names.d.ts +2 -0
  101. package/dist/meta-tool-names.js +3 -0
  102. package/dist/meta-tools.js +285 -209
  103. package/dist/module-contracts.d.ts +8 -23
  104. package/dist/node.d.ts +0 -2
  105. package/dist/node.js +2 -2
  106. package/dist/oauth-callback-outcome.js +1 -3
  107. package/dist/oauth-handoff.js +117 -0
  108. package/dist/oauth-sealing.js +20 -0
  109. package/dist/operator-record.d.ts +137 -0
  110. package/dist/operator-record.js +420 -0
  111. package/dist/operator-ui/assets.js +31 -0
  112. package/dist/operator-ui/contract.d.ts +72 -0
  113. package/dist/operator-ui/contract.js +1 -0
  114. package/dist/operator-ui/fix-prompts.d.ts +18 -0
  115. package/dist/operator-ui/generated.js +5 -4
  116. package/dist/operator-ui/model.d.ts +12 -87
  117. package/dist/operator-ui/model.js +2 -9
  118. package/dist/operator-ui/setup-commands.js +1 -3
  119. package/dist/operator-ui/view.d.ts +323 -0
  120. package/dist/operator-ui/view.js +62 -189
  121. package/dist/page-styles.js +2 -3
  122. package/dist/program-source.js +14 -2
  123. package/dist/{exempt-writes.js → program-writes.js} +55 -18
  124. package/dist/provider.d.ts +142 -0
  125. package/dist/provider.js +187 -0
  126. package/dist/providers/_shared/google/result-size.js +57 -0
  127. package/dist/providers/_shared/google/service-account.d.ts +58 -0
  128. package/dist/providers/_shared/google/service-account.js +500 -0
  129. package/dist/providers/_shared/google/workspace.d.ts +276 -0
  130. package/dist/providers/_shared/google/workspace.js +518 -0
  131. package/dist/providers/{basecamp.d.ts → basecamp/index.d.ts} +6 -28
  132. package/dist/providers/basecamp/index.js +997 -0
  133. package/dist/providers/basecamp/skill.generated.js +9 -0
  134. package/dist/providers/{breeze.d.ts → breeze/index.d.ts} +2 -2
  135. package/dist/providers/{breeze.js → breeze/index.js} +184 -140
  136. package/dist/providers/breeze/skill.generated.js +9 -0
  137. package/dist/providers/{ccb.d.ts → ccb/index.d.ts} +2 -2
  138. package/dist/providers/{ccb.js → ccb/index.js} +271 -116
  139. package/dist/providers/ccb/skill.generated.js +9 -0
  140. package/dist/providers/{cloudflare.d.ts → cloudflare/index.d.ts} +8 -7
  141. package/dist/providers/{cloudflare.js → cloudflare/index.js} +171 -738
  142. package/dist/providers/cloudflare/skill.generated.js +10 -0
  143. package/dist/providers/docs/index.d.ts +54 -0
  144. package/dist/providers/docs/index.js +1237 -0
  145. package/dist/providers/docs/skill.generated.js +9 -0
  146. package/dist/providers/drive/index.d.ts +53 -0
  147. package/dist/providers/drive/index.js +1523 -0
  148. package/dist/providers/drive/skill.generated.js +9 -0
  149. package/dist/providers/forms/index.d.ts +54 -0
  150. package/dist/providers/forms/index.js +1217 -0
  151. package/dist/providers/forms/skill.generated.js +11 -0
  152. package/dist/providers/github/auth.d.ts +32 -0
  153. package/dist/providers/github/auth.js +361 -0
  154. package/dist/providers/github/hosted.js +218 -0
  155. package/dist/providers/github/index.d.ts +13 -0
  156. package/dist/providers/github/index.js +140 -0
  157. package/dist/providers/github/key.js +100 -0
  158. package/dist/providers/github/rest.js +321 -0
  159. package/dist/providers/github/scope.d.ts +29 -0
  160. package/dist/providers/github/scope.js +124 -0
  161. package/dist/providers/github/skill.generated.js +8 -0
  162. package/dist/providers/gmail/index.d.ts +51 -0
  163. package/dist/providers/gmail/index.js +1436 -0
  164. package/dist/providers/gmail/skill.generated.js +9 -0
  165. package/dist/providers/{linear.d.ts → linear/index.d.ts} +11 -14
  166. package/dist/providers/linear/index.js +352 -0
  167. package/dist/providers/linear/skill.generated.js +8 -0
  168. package/dist/providers/{mixpanel.d.ts → mixpanel/index.d.ts} +5 -10
  169. package/dist/providers/mixpanel/index.js +416 -0
  170. package/dist/providers/mixpanel/skill.generated.js +9 -0
  171. package/dist/providers/{notion.d.ts → notion/index.d.ts} +8 -8
  172. package/dist/providers/{notion.js → notion/index.js} +226 -239
  173. package/dist/providers/notion/skill.generated.js +10 -0
  174. package/dist/providers/{overflow.d.ts → overflow/index.d.ts} +2 -2
  175. package/dist/providers/{overflow.js → overflow/index.js} +74 -111
  176. package/dist/providers/overflow/skill.generated.js +8 -0
  177. package/dist/providers/{planning-center.d.ts → planning-center/index.d.ts} +2 -2
  178. package/dist/providers/{planning-center.js → planning-center/index.js} +264 -104
  179. package/dist/providers/planning-center/skill.generated.js +10 -0
  180. package/dist/providers/{revenuecat.d.ts → revenuecat/index.d.ts} +5 -10
  181. package/dist/providers/revenuecat/index.js +557 -0
  182. package/dist/providers/revenuecat/skill.generated.js +11 -0
  183. package/dist/providers/sheets/index.d.ts +54 -0
  184. package/dist/providers/sheets/index.js +1237 -0
  185. package/dist/providers/sheets/skill.generated.js +10 -0
  186. package/dist/providers/slides/index.d.ts +53 -0
  187. package/dist/providers/slides/index.js +2645 -0
  188. package/dist/providers/slides/skill.generated.js +9 -0
  189. package/dist/providers/{stripe.d.ts → stripe/index.d.ts} +5 -12
  190. package/dist/providers/stripe/index.js +266 -0
  191. package/dist/providers/stripe/skill.generated.js +11 -0
  192. package/dist/providers/{tithely.d.ts → tithely/index.d.ts} +2 -2
  193. package/dist/providers/{tithely.js → tithely/index.js} +60 -91
  194. package/dist/providers/tithely/skill.generated.js +8 -0
  195. package/dist/providers/{vercel.d.ts → vercel/index.d.ts} +8 -8
  196. package/dist/providers/vercel/index.js +1038 -0
  197. package/dist/providers/vercel/mcp-ownership.js +212 -0
  198. package/dist/providers/vercel/skill.generated.js +11 -0
  199. package/dist/registry.d.ts +108 -171
  200. package/dist/registry.js +513 -925
  201. package/dist/request-state.js +21 -0
  202. package/dist/resource-read.js +100 -0
  203. package/dist/result-shapes.js +17 -49
  204. package/dist/retired-toolkits.js +1 -1
  205. package/dist/routes/access-tokens.js +3 -6
  206. package/dist/routes/activity-context.js +21 -0
  207. package/dist/routes/activity-disclosure.js +40 -0
  208. package/dist/routes/activity.js +59 -21
  209. package/dist/routes/actor-labels.js +3 -50
  210. package/dist/routes/connect.js +221 -0
  211. package/dist/routes/credentials.js +15 -27
  212. package/dist/routes/mcp.js +356 -130
  213. package/dist/routes/oauth-client-metadata.js +15 -0
  214. package/dist/routes/oauth-management.js +45 -107
  215. package/dist/routes/oauth.js +110 -57
  216. package/dist/routes/operator.js +3 -3
  217. package/dist/routes/shared.d.ts +25 -64
  218. package/dist/routes/shared.js +160 -80
  219. package/dist/routes/skills.js +30 -0
  220. package/dist/routes/ui-config.js +225 -0
  221. package/dist/routes/ui-data.js +40 -35
  222. package/dist/routes/ui.js +66 -56
  223. package/dist/runtime/admission.js +4 -2
  224. package/dist/runtime/call-admission.js +3 -1
  225. package/dist/runtime/connector-scope.js +1 -4
  226. package/dist/runtime/run.js +10 -39
  227. package/dist/runtime/services.js +12 -117
  228. package/dist/runtime/shared-read.js +1 -3
  229. package/dist/runtime/storage.js +8 -1
  230. package/dist/sent-secrets.d.ts +52 -0
  231. package/dist/sent-secrets.js +474 -0
  232. package/dist/server.js +28 -71
  233. package/dist/skills.js +406 -174
  234. package/dist/sqlite.d.ts +48 -0
  235. package/dist/sqlite.js +166 -0
  236. package/dist/storage/keys.js +428 -0
  237. package/dist/storage/memory.js +7 -0
  238. package/dist/storage/sql.d.ts +66 -0
  239. package/dist/storage/sql.js +534 -0
  240. package/dist/timeout.js +4 -2
  241. package/dist/tool-name.js +17 -0
  242. package/dist/tool-safety.d.ts +9 -35
  243. package/dist/tool-safety.js +20 -40
  244. package/dist/types.d.ts +320 -54
  245. package/dist/types.js +0 -1
  246. package/dist/ui.d.ts +7 -19
  247. package/dist/ui.js +76 -90
  248. package/dist/url-safety.js +3 -6
  249. package/dist/usage-guide.js +137 -0
  250. package/dist/validate.d.ts +7 -0
  251. package/dist/validate.js +309 -47
  252. package/dist/version.d.ts +1 -7
  253. package/dist/version.js +3 -7
  254. package/dist/worker.d.ts +11 -0
  255. package/dist/worker.js +434 -0
  256. package/documentation/architecture.md +664 -231
  257. package/documentation/auth.md +936 -546
  258. package/documentation/code-mode.md +501 -312
  259. package/documentation/meta-tools.md +449 -200
  260. package/documentation/operator-tests.md +93 -0
  261. package/documentation/provider-migration-0.29.md +60 -0
  262. package/examples/worker/AGENTS.md +34 -12
  263. package/examples/worker/README.md +286 -255
  264. package/examples/worker/kv-to-d1.wrangler.jsonc +26 -0
  265. package/examples/worker/scripts/copy-kv-to-d1.mjs +162 -0
  266. package/examples/worker/src/connecta.config.ts +151 -0
  267. package/examples/worker/src/index.ts +14 -197
  268. package/examples/worker/wrangler.jsonc +15 -35
  269. package/package.json +120 -54
  270. package/templates/node/.dockerignore +1 -2
  271. package/templates/node/.env.example +20 -21
  272. package/templates/node/AGENTS.md +21 -17
  273. package/templates/node/Dockerfile +3 -5
  274. package/templates/node/README.md +133 -58
  275. package/templates/node/docker-compose.yml +11 -12
  276. package/templates/node/package.json +4 -2
  277. package/templates/node/src/connecta.config.ts +119 -0
  278. package/templates/node/src/index.ts +9 -154
  279. package/templates/node/src/provision-token.ts +17 -0
  280. package/AGENTS.md +0 -170
  281. package/dist/artifacts/connector.d.ts +0 -58
  282. package/dist/artifacts/connector.js +0 -730
  283. package/dist/artifacts/document.js +0 -86
  284. package/dist/artifacts/guide.js +0 -140
  285. package/dist/artifacts/html-scan.js +0 -329
  286. package/dist/artifacts/json.js +0 -78
  287. package/dist/artifacts/kv-store.d.ts +0 -20
  288. package/dist/artifacts/kv-store.js +0 -140
  289. package/dist/artifacts/markdown.js +0 -478
  290. package/dist/artifacts/module.d.ts +0 -37
  291. package/dist/artifacts/module.js +0 -66
  292. package/dist/artifacts/operations.d.ts +0 -197
  293. package/dist/artifacts/operations.js +0 -1002
  294. package/dist/artifacts/refresh.d.ts +0 -55
  295. package/dist/artifacts/refresh.js +0 -236
  296. package/dist/artifacts/routes.js +0 -206
  297. package/dist/artifacts/types.d.ts +0 -214
  298. package/dist/artifacts/types.js +0 -22
  299. package/dist/artifacts/validate.d.ts +0 -62
  300. package/dist/artifacts/validate.js +0 -651
  301. package/dist/artifacts.d.ts +0 -6
  302. package/dist/artifacts.js +0 -9
  303. package/dist/auth/bearer.d.ts +0 -11
  304. package/dist/auth/bearer.js +0 -50
  305. package/dist/providers/basecamp.js +0 -445
  306. package/dist/providers/linear.js +0 -217
  307. package/dist/providers/mixpanel.js +0 -247
  308. package/dist/providers/revenuecat.js +0 -317
  309. package/dist/providers/stripe.js +0 -253
  310. package/dist/providers/vercel.js +0 -1253
  311. package/dist/storage/file.d.ts +0 -15
  312. package/dist/storage/file.js +0 -455
  313. package/ethos.md +0 -93
  314. package/examples/worker/src/cloudflare-kv.ts +0 -43
  315. package/examples/worker/src/d1-activity-row.ts +0 -155
  316. package/examples/worker/src/d1-activity.ts +0 -147
  317. package/examples/worker/src/d1-storage.ts +0 -138
  318. package/examples/worker/src/r2-artifact-blobs.ts +0 -29
  319. package/templates/node/src/file-activity.ts +0 -182
@@ -0,0 +1,15 @@
1
+ import { selfHostedClientDocument } from "../auth/downstream-client-metadata.js";
2
+ /** Public OAuth client metadata, without auth, storage, discovery, or request-host input. */
3
+ export function routeOAuthClientMetadata(context) {
4
+ if (!context.path.startsWith("/oauth/client-metadata/"))
5
+ return null;
6
+ const id = context.path.slice("/oauth/client-metadata/".length);
7
+ const connector = context.opts.registry.getConnector(id);
8
+ const document = connector &&
9
+ selfHostedClientDocument(connector, context.opts.config.publicUrl, context.opts.config.serverInfo.name);
10
+ if (!document)
11
+ return new Response(null, { status: 404 });
12
+ if (context.request.method !== "GET")
13
+ return new Response(null, { status: 405, headers: { Allow: "GET" } });
14
+ return Response.json(document, { headers: { "Cache-Control": "public, max-age=300" } });
15
+ }
@@ -1,28 +1,17 @@
1
+ import { OAuthRevocationError } from "../auth/downstream-revocation.js";
1
2
  import { Effect, Result } from "effect";
2
- import { drainOAuthStartResets } from "../auth/oauth-start-reset.js";
3
3
  import { closeScope } from "../runtime/connector-scope.js";
4
- import { withDeadlineEffect } from "../runtime/run.js";
5
- import { isSafeHttpUrl } from "../ui.js";
6
- import { authorizedPerson, refuse, serveOperator, visibleRegistry, } from "./operator.js";
7
- import { mayManageConnector, isSameOrigin, msg, privateJson, } from "./shared.js";
8
- /** Bounds the start and its handoff; invalidation and scope close finish after it. */
9
- const OAUTH_START_TIMEOUT_MS = 30_000;
10
- /**
11
- * How a POST starts authorization, from its `mode` query parameter:
12
- * `restart` (the default) resets the connector to a fresh epoch — wiping the
13
- * grant, the client registration, and discovery — before starting over;
14
- * `continue` hands back a recent pending authorization URL when one exists
15
- * and otherwise starts a flow in the current epoch, keeping the stored
16
- * registration. `null` is a malformed request.
17
- */
4
+ import { oauthConnectUrl, oauthConnectUnavailable } from "../oauth-handoff.js";
5
+ import { authorizedPerson, refuse, serveOperator, visibleRegistry, Answer } from "./operator.js";
6
+ import { failureRecord, logFailure } from "../operator-record.js";
7
+ import { mayManageConnector, isSameOrigin, privateJson } from "./shared.js";
8
+ /** A UI action signs its requested mode; the verified /connect visit applies it. */
18
9
  function startMode(url) {
19
10
  const modes = url.searchParams.getAll("mode");
20
11
  if (modes.length === 0)
21
12
  return "restart";
22
13
  const [mode] = modes;
23
- return modes.length === 1 && (mode === "continue" || mode === "restart")
24
- ? mode
25
- : null;
14
+ return modes.length === 1 && (mode === "continue" || mode === "restart") ? mode : null;
26
15
  }
27
16
  function oauthManagementRequest(context, connectorId) {
28
17
  const { request, baseUrl, defer, opts } = context;
@@ -52,101 +41,50 @@ function oauthManagementRequest(context, connectorId) {
52
41
  if (mode === null) {
53
42
  return yield* refuse('mode must be "continue" or "restart"', 400);
54
43
  }
44
+ if (!disconnecting) {
45
+ const unavailable = oauthConnectUnavailable(opts);
46
+ if (unavailable)
47
+ return yield* refuse(unavailable, 403);
48
+ const authorizationUrl = yield* Effect.tryPromise({
49
+ try: () => oauthConnectUrl(opts, baseUrl, connectorId, authz.principalKey, mode === "restart"),
50
+ catch: () => new Answer(privateJson({ error: "OAuth connection link could not be created" }, { status: 502 })),
51
+ });
52
+ // This POST is already an explicit connect action; the browser visit
53
+ // verifies the signed initiator and applies it without another UI hand-off.
54
+ const target = new URL(authorizationUrl);
55
+ if (opts.config.ui)
56
+ target.searchParams.set("start", "1");
57
+ return privateJson({ state: "auth_required", authorizationUrl: target.href });
58
+ }
55
59
  let ctx;
56
60
  // The connector scope this request opened ends with it, however it ends.
57
- yield* Effect.addFinalizer(() => ctx ? closeScope(connector, ctx, defer) : Effect.void);
58
- const timeoutError = new Error("OAuth authorization start timed out");
59
- const operation = yield* Effect.result(withDeadlineEffect((signal) => Effect.tryPromise({
60
- try: async () => {
61
- ctx = registry.contextFor(connectorId, baseUrl, {}, { signal });
62
- if (disconnecting) {
63
- await disconnectAuth(ctx);
64
- return undefined;
65
- }
66
- const started = await startAuth(ctx, { force: mode === "restart" });
67
- if (signal.aborted)
68
- throw signal.reason;
69
- if (started.authorizationUrl) {
70
- await registry.bindOAuthHandoff(connectorId, started.authorizationUrl);
71
- }
72
- return started;
73
- },
61
+ yield* Effect.addFinalizer(() => (ctx ? closeScope(connector, ctx, defer) : Effect.void));
62
+ // A disconnect commits through invalidation even if its hook rejects or
63
+ // the browser leaves. It has no start deadline or request signal.
64
+ ctx = registry.contextFor(connectorId, baseUrl);
65
+ const operation = yield* Effect.result(Effect.tryPromise({
66
+ try: () => disconnectAuth(ctx),
74
67
  catch: (error) => error,
75
- }), {
76
- // A disconnect deliberately ignores both browser cancellation and
77
- // this start deadline so its reset reaches invalidation.
78
- ...(disconnecting ? {} : { timeoutMs: OAUTH_START_TIMEOUT_MS }),
79
- ...(disconnecting ? {} : { signal: request.signal }),
80
- timeoutError,
81
68
  }));
82
- const opened = ctx;
83
- if (Result.isFailure(operation) && opened) {
84
- // Storage has no abort contract. If a reset's generation write is still
85
- // pending, answering now would let it publish over a newer flow.
86
- yield* Effect.promise(() => drainOAuthStartResets(opened.requestScope ?? opened));
87
- }
88
- // A disconnect or a restart invalidates the old grant and its cached
89
- // catalog, even when a partially failed physical cleanup left its epoch
90
- // fence standing. So does a continued start that had to begin a new flow:
91
- // the grant it found was missing or refused. A continued start that
92
- // reused a pending URL or found the connection healthy changed nothing,
93
- // so the catalog stays. (A continue does reset a disconnected connector,
94
- // but the disconnect already invalidated that catalog.)
95
- const started = Result.isSuccess(operation) ? operation.success : undefined;
96
- const reused = mode === "continue" && started?.authorizationReused === true;
97
- const unchanged = mode === "continue" && (reused || started?.state === "ok");
98
- const invalidated = unchanged
99
- ? Result.succeed(undefined)
100
- : yield* Effect.result(Effect.tryPromise({
101
- try: () => registry.invalidateStored(connectorId),
102
- catch: (error) => error,
103
- }));
104
- // Every failure below answers in the route's own fixed words. A hook's
105
- // rejection and a status message can quote a token endpoint's error body
106
- // or a provider's refusal, either of which can quote the secret it was
107
- // sent, so that text goes to the deployment's log and nowhere else.
108
- const failed = (detail, fixed, status) => {
109
- opts.logger.warn(`[connecta] connector "${connectorId}" OAuth ${disconnecting ? "disconnect" : mode} failed: ${detail}`);
110
- return refuse(fixed, status);
111
- };
112
- const couldNot = disconnecting
113
- ? "OAuth disconnect failed"
114
- : "OAuth authorization could not start";
115
- // A Result, not a caught value, decides whether it failed: a hook that
116
- // rejects with no reason at all still failed.
117
- if (Result.isFailure(operation)) {
118
- if (operation.failure === timeoutError) {
119
- return yield* failed(`start exceeded ${OAUTH_START_TIMEOUT_MS} ms`, "OAuth authorization start timed out", 504);
69
+ const invalidated = yield* Effect.result(Effect.tryPromise({
70
+ try: () => registry.invalidateStored(connectorId),
71
+ catch: (error) => error,
72
+ }));
73
+ if (Result.isFailure(operation) || Result.isFailure(invalidated)) {
74
+ const error = Result.isFailure(operation)
75
+ ? operation.failure
76
+ : Result.isFailure(invalidated)
77
+ ? invalidated.failure
78
+ : undefined;
79
+ logFailure(opts.config.logger, "OAuth disconnect failed", failureRecord({ connector: connectorId }, error));
80
+ if (error instanceof OAuthRevocationError && !Result.isFailure(invalidated)) {
81
+ return privateJson({ state: "auth_required", code: "oauth_revocation_failed" });
120
82
  }
121
- return yield* failed(msg(operation.failure), couldNot, 400);
122
- }
123
- if (Result.isFailure(invalidated)) {
124
- return yield* failed(`stored-state invalidation: ${msg(invalidated.failure)}`, couldNot, 400);
125
- }
126
- const result = operation.success;
127
- if (!result) {
128
- return new Response(null, {
129
- status: 204,
130
- headers: {
131
- "Cache-Control": "no-store",
132
- "Referrer-Policy": "no-referrer",
133
- },
134
- });
135
- }
136
- const authorizationUrl = isSafeHttpUrl(result.authorizationUrl)
137
- ? result.authorizationUrl
138
- : undefined;
139
- if (result.state === "error") {
140
- return yield* failed(result.message || "the connector reported an error state", couldNot, 502);
141
- }
142
- if (result.state === "auth_required" && !authorizationUrl) {
143
- return yield* failed(result.message || "consent is required but no safe authorization URL was returned", "OAuth authorization requires consent but no safe URL is available", 502);
83
+ return yield* refuse("OAuth disconnect failed", 400);
144
84
  }
145
- // No message: a status message is the same text the Connections page
146
- // stopped shipping, and the state and the link are the whole answer.
147
- return privateJson({
148
- state: result.state,
149
- ...(authorizationUrl ? { authorizationUrl, reused } : {}),
85
+ return new Response(null, {
86
+ status: 204,
87
+ headers: { "Cache-Control": "no-store", "Referrer-Policy": "no-referrer" },
150
88
  });
151
89
  }).pipe(Effect.scoped);
152
90
  }
@@ -1,9 +1,13 @@
1
1
  import { Effect } from "effect";
2
+ import { htmlSecurityHeaders } from "../html-security.js";
3
+ import { authorizationCredential } from "../inbound-credential.js";
2
4
  import { closeConnectorScope } from "../connector-scope.js";
3
- import { oauthValueStorageKey } from "../auth/downstream-oauth.js";
5
+ import { OAuthCallbackClaimedError, oauthStateDigest } from "../auth/downstream-oauth.js";
6
+ import { failureRecord, logFailure } from "../operator-record.js";
7
+ import { oauthFlowKeys } from "../storage/keys.js";
4
8
  import { escapeHtml, renderPage, resolveBranding, STATUS_ICONS } from "../branding.js";
5
- import { oauthCallbackOutcome, providerErrorReason, } from "../oauth-callback-outcome.js";
6
- import { authorizeUiIdentity, mayManageConnector, validateAuthPermissions, loggableValue, msg, } from "./shared.js";
9
+ import { oauthCallbackOutcome, providerErrorReason } from "../oauth-callback-outcome.js";
10
+ import { authorizeUiIdentity, withSessionCookies, mayManageConnector, validateAuthPermissions, loggableValue, } from "./shared.js";
7
11
  const TONE_MARKS = {
8
12
  ok: { className: " ok", icon: STATUS_ICONS.ok, label: "Connected" },
9
13
  declined: { className: "", icon: STATUS_ICONS.declined, label: "Not connected" },
@@ -24,10 +28,10 @@ const TONE_MARKS = {
24
28
  * under a quiet disclosure rather than in the message.
25
29
  */
26
30
  function html(reason, opts, connector) {
27
- const brand = resolveBranding(opts.branding);
31
+ const brand = resolveBranding(opts.config.ui?.branding);
28
32
  const outcome = oauthCallbackOutcome(reason, connector, brand.productName);
29
33
  const mark = TONE_MARKS[outcome.tone];
30
- const uiMounted = Boolean(opts.ui);
34
+ const uiMounted = Boolean(opts.config.ui);
31
35
  const home = uiMounted
32
36
  ? `<div class="status-actions"><a class="btn${outcome.tone === "ok" ? "" : " primary"}" href="/">Return to ${escapeHtml(brand.productName)}</a></div>`
33
37
  : "";
@@ -50,23 +54,23 @@ function html(reason, opts, connector) {
50
54
  ${details}
51
55
  </section>
52
56
  </main>`;
53
- return new Response(renderPage(opts.branding, {
57
+ return new Response(renderPage(opts.config.ui?.branding, {
54
58
  title: `${outcome.heading} — ${brand.pageTitle}`,
55
59
  uiMounted,
56
60
  body,
57
- }), { status: outcome.status, headers: { "Content-Type": "text/html; charset=utf-8" } });
61
+ }), { status: outcome.status, headers: htmlSecurityHeaders({ "Content-Type": "text/html; charset=utf-8" }) });
58
62
  }
59
63
  /**
60
64
  * Pay the storage read a real downstream-OAuth refusal pays, on the refusal
61
65
  * paths that would otherwise pay nothing.
62
66
  *
63
67
  * Identical bodies do not hide a connector id if the clock still sorts them.
64
- * `KvOAuthProvider.verifyState` reads `oauth:state` and its generation before
65
- * it can reject a mismatched value, so a configured id costs two storage round
66
- * trips on the ordinary path while an id naming nothing used to touch no I/O.
67
- * That gap is an oracle: sample the two and a wordlist recovers the connector
68
- * list the flat 400 was meant to withhold. So zero-I/O refusals read the same
69
- * keys in the same `conn:<id>:` namespace, where an unconfigured id gets misses.
68
+ * `KvOAuthProvider.verifyState` reads the consent the state names before it
69
+ * can reject it, so a configured id costs a storage round trip on the
70
+ * ordinary path while an id naming nothing used to touch no I/O. That gap is
71
+ * an oracle: sample the two and a wordlist recovers the connector list the
72
+ * flat 400 was meant to withhold. So zero-I/O refusals read the same key in
73
+ * the same `conn:<id>:` namespace, where an unconfigured id gets a miss.
70
74
  *
71
75
  * This is deliberately *not* a constant-time claim: a hit and a miss are not
72
76
  * identical in a KV store, and a connector shipping its own `verifyState` may
@@ -78,10 +82,9 @@ function html(reason, opts, connector) {
78
82
  * turning it into a 500 would hand back exactly the distinguishable response
79
83
  * this whole path exists to deny.
80
84
  */
81
- async function equalizeRefusalCost(context) {
85
+ async function equalizeRefusalCost(context, state) {
82
86
  try {
83
- const generation = await context.storage.get("oauth:generation");
84
- await context.storage.get(oauthValueStorageKey("oauth:state", generation));
87
+ await context.storage.get(oauthFlowKeys.flow(await oauthStateDigest(state ?? "")));
85
88
  }
86
89
  catch {
87
90
  // Deliberately ignored — see above.
@@ -100,6 +103,12 @@ async function equalizeRefusalCost(context) {
100
103
  export function routeOAuthCallback(context) {
101
104
  if (!context.path.startsWith("/oauth/callback/"))
102
105
  return Effect.succeed(null);
106
+ // Downstream OAuth uses query-mode redirects; form_post is not supported.
107
+ if (context.request.method !== "GET")
108
+ return Effect.succeed(new Response(null, {
109
+ status: 405,
110
+ headers: { Allow: "GET" },
111
+ }));
103
112
  return Effect.uninterruptible(Effect.promise(() => finishOAuthCallback(context)));
104
113
  }
105
114
  /**
@@ -122,18 +131,22 @@ const TOKEN_ERROR_CODES = new Set([
122
131
  /** ` with OAuth error <code>` for a known code; nothing for anything else. */
123
132
  function exchangeErrorCode(err) {
124
133
  const code = err?.code;
125
- return typeof code === "string" && TOKEN_ERROR_CODES.has(code)
126
- ? ` with OAuth error ${code}`
127
- : "";
134
+ return typeof code === "string" && TOKEN_ERROR_CODES.has(code) ? ` with OAuth error ${code}` : "";
135
+ }
136
+ /** The provider's own refusal of a duplicate callback, wherever it is wrapped. */
137
+ function claimedByAnotherCallback(err) {
138
+ const seen = new Set();
139
+ for (let current = err; current instanceof Error && !seen.has(current); current = current.cause) {
140
+ if (current instanceof OAuthCallbackClaimedError)
141
+ return true;
142
+ seen.add(current);
143
+ }
144
+ return false;
128
145
  }
129
146
  async function finishOAuthCallback(context) {
130
147
  const { path, url, baseUrl, opts } = context;
131
148
  const error = url.searchParams.get("error");
132
- if (error)
133
- return html(providerErrorReason(error), opts);
134
149
  const code = url.searchParams.get("code");
135
- if (!code)
136
- return html("invalid_callback", opts);
137
150
  const id = path.slice("/oauth/callback/".length);
138
151
  const state = url.searchParams.get("state");
139
152
  const callbackTarget = await opts.registry.oauthCallbackView(id, state);
@@ -145,38 +158,40 @@ async function finishOAuthCallback(context) {
145
158
  // nor touches storage for an unknown id, which is what lets the refusals
146
159
  // below borrow it to equalize their cost.
147
160
  const connectorContext = callbackRegistry
148
- ? callbackRegistry.contextFor(id, baseUrl)
149
- : opts.registry.contextFor(id, baseUrl);
161
+ ? callbackRegistry.contextFor(id, baseUrl, {}, context.defer ? { defer: context.defer } : {})
162
+ : opts.registry.contextFor(id, baseUrl, {}, context.defer ? { defer: context.defer } : {});
150
163
  const refused = () => html("invalid_callback", opts);
151
164
  if (!connector || !connector.finishAuth) {
152
- await equalizeRefusalCost(connectorContext);
165
+ await equalizeRefusalCost(connectorContext, state);
153
166
  return refused();
154
167
  }
155
168
  try {
156
169
  const expectedPrincipalKey = callbackTarget?.principalKey;
157
- // A browser returning from consent normally has no MCP Authorization
158
- // header. An interactive bearer provider therefore answers 401 here; state
159
- // and the saved state-to-principal handoff still prove ownership below.
160
- // Rejecting 401 would break that callback. A 403 is an explicit denial.
161
- const browserIdentity = await authorizeUiIdentity(context.request, baseUrl, opts.auth, "OAuth callback", context.runtimeContext, opts.identity);
162
- if (browserIdentity.ok) {
163
- try {
164
- validateAuthPermissions(browserIdentity, opts.registry);
165
- }
166
- catch {
167
- return refused();
168
- }
169
- if (!mayManageConnector(browserIdentity, connector) || (expectedPrincipalKey && browserIdentity.principalKey !== expectedPrincipalKey))
170
- return refused();
170
+ const browserIdentity = await authorizeUiIdentity(context.request, baseUrl, opts.config.auth, "OAuth callback", context.runtimeContext, opts.config.identity);
171
+ if (!browserIdentity.ok) {
172
+ if (browserIdentity.response.status === 401 && authorizationCredential(context.request).kind !== "absent")
173
+ return browserIdentity.response;
174
+ // A Clerk browser handshake refreshes its session and returns to this
175
+ // exact callback. It grants no identity and exchanges no code yet.
176
+ if (browserIdentity.response.status === 307 && browserIdentity.response.headers.has("location"))
177
+ return browserIdentity.response;
178
+ return refused();
179
+ }
180
+ if (!expectedPrincipalKey || browserIdentity.principalKey !== expectedPrincipalKey)
181
+ return refused();
182
+ try {
183
+ validateAuthPermissions(browserIdentity, opts.registry);
171
184
  }
172
- else if (browserIdentity.response.status === 403 && opts.auth.some(provider => provider.interactiveOperator)) {
185
+ catch {
173
186
  return refused();
174
187
  }
188
+ if (!mayManageConnector(browserIdentity, connector))
189
+ return refused();
175
190
  // CSRF / login-fixation guard: this route is intentionally public, so verify
176
191
  // the `state` matches the flow connecta started BEFORE exchanging the code.
177
192
  if (!connector.verifyState) {
178
- await equalizeRefusalCost(connectorContext);
179
- opts.logger.warn(`[connecta] refused an OAuth callback for connector ` +
193
+ await equalizeRefusalCost(connectorContext, state);
194
+ opts.config.logger.warn(`[connecta] refused an OAuth callback for connector ` +
180
195
  `${loggableValue(id)} with 400: it implements finishAuth but no ` +
181
196
  "verifyState, so connecta cannot establish that it started this flow. " +
182
197
  "No authorization code was exchanged. Implement verifyState before " +
@@ -188,15 +203,11 @@ async function finishOAuthCallback(context) {
188
203
  stateMatches = await connector.verifyState(state, connectorContext);
189
204
  }
190
205
  catch (err) {
191
- opts.logger.warn(`[connecta] refused an OAuth callback for connector ` +
192
- `${loggableValue(id)} with 400: verifyState threw ` +
193
- `${loggableValue(msg(err))}. No authorization code was exchanged. ` +
194
- "Re-run authorization from connecta and check the verifier if it " +
195
- "fails again.");
206
+ logFailure(opts.config.logger, "OAuth callback verifyState threw; no authorization code was exchanged", failureRecord({ connector: id }, err));
196
207
  return refused();
197
208
  }
198
209
  if (!stateMatches) {
199
- opts.logger.warn(`[connecta] refused an OAuth callback for connector ` +
210
+ opts.config.logger.warn(`[connecta] refused an OAuth callback for connector ` +
200
211
  `${loggableValue(id)} with 400: ` +
201
212
  (state === null
202
213
  ? "the state parameter was missing"
@@ -205,30 +216,72 @@ async function finishOAuthCallback(context) {
205
216
  "connecta and try again.");
206
217
  return refused();
207
218
  }
208
- if (connector.authScope === "personal") {
219
+ // Error responses are callbacks too: neither their code nor their copy is
220
+ // interpreted until the state and the consent's issuer have been validated.
221
+ if (["state", "iss", "code", "error"].some((name) => url.searchParams.getAll(name).length > 1))
222
+ return refused();
223
+ try {
224
+ const issuer = url.searchParams.get("iss");
225
+ if (connector.verifyCallbackIssuer) {
226
+ if (!(await connector.verifyCallbackIssuer(issuer, connectorContext)))
227
+ return refused();
228
+ }
229
+ else if (issuer !== null) {
230
+ // A custom connector with no issuer validator cannot vouch for this response.
231
+ return refused();
232
+ }
233
+ }
234
+ catch {
235
+ return refused();
236
+ }
237
+ if (error === null && !code)
238
+ return refused();
239
+ if (error !== null && !connector.consumeAuthError)
240
+ return refused();
241
+ {
209
242
  try {
210
- await opts.registry.clearOAuthHandoff(id, state);
243
+ if (!(await opts.registry.consumeOAuthHandoff(id, state, expectedPrincipalKey)))
244
+ return refused();
211
245
  }
212
246
  catch (err) {
213
- opts.logger.warn(`[connecta] refused an OAuth callback for connector ` +
214
- `${loggableValue(id)} with 500: its principal handoff could not be ` +
215
- `consumed (${loggableValue(msg(err))}). No authorization code was exchanged.`);
247
+ logFailure(opts.config.logger, "OAuth callback handoff could not be consumed; no authorization code was exchanged", failureRecord({ connector: id }, err));
216
248
  return html("handoff_failed", opts, connector);
217
249
  }
218
250
  }
251
+ if (error !== null) {
252
+ try {
253
+ await connector.consumeAuthError(connectorContext);
254
+ }
255
+ catch (err) {
256
+ if (claimedByAnotherCallback(err))
257
+ return refused();
258
+ logFailure(opts.config.logger, "OAuth error callback consent could not be consumed; no authorization code was exchanged", failureRecord({ connector: id }, err));
259
+ return withSessionCookies(html("handoff_failed", opts, connector), browserIdentity.sessionCookies);
260
+ }
261
+ return withSessionCookies(html(providerErrorReason(error), opts), browserIdentity.sessionCookies);
262
+ }
219
263
  try {
220
264
  await connector.finishAuth(code, connectorContext, url.searchParams);
221
265
  await callbackRegistry.invalidateStored(id);
222
- return html("connected", opts, connector);
266
+ return withSessionCookies(html("connected", opts, connector), browserIdentity.sessionCookies);
223
267
  }
224
268
  catch (err) {
269
+ // A duplicate of a callback that already claimed this consent sent
270
+ // nothing: it is the already-used link the flat refusal describes, not
271
+ // an exchange the provider rejected.
272
+ if (claimedByAnotherCallback(err)) {
273
+ opts.config.logger.warn(`[connecta] refused an OAuth callback for connector ` +
274
+ `${loggableValue(id)} with 400: another callback had already ` +
275
+ "claimed its state. No authorization code was exchanged.");
276
+ return refused();
277
+ }
225
278
  // Neither the page nor the log repeats what the exchange threw: the SDK
226
279
  // quotes the token endpoint's error_description or raw body, and a
227
280
  // provider echoing a client_secret_post request puts the secret there.
228
- opts.logger.warn(`[connecta] OAuth callback for connector ${loggableValue(id)} failed ` +
281
+ opts.config.logger.warn(`[connecta] OAuth callback for connector ${loggableValue(id)} failed ` +
229
282
  `with 500: the authorization code exchange failed${exchangeErrorCode(err)}. ` +
230
283
  "Check the connector's client configuration and re-run authorization.");
231
- return html("exchange_failed", opts, connector);
284
+ return withSessionCookies(html("exchange_failed", opts, connector), browserIdentity.sessionCookies);
232
285
  }
233
286
  }
234
287
  finally {
@@ -36,15 +36,15 @@ export function refuse(error, status) {
36
36
  return Effect.fail(new Answer(privateJson({ error }, { status })));
37
37
  }
38
38
  function admitted(result) {
39
- return Effect.flatMap(Effect.promise(() => result), (authz) => authz.ok ? Effect.succeed(authz) : Effect.fail(new Answer(authz.response)));
39
+ return Effect.flatMap(Effect.promise(() => result), (authz) => (authz.ok ? Effect.succeed(authz) : Effect.fail(new Answer(authz.response))));
40
40
  }
41
41
  /** Any configured caller. The gate's refusal, challenge headers and all, is the answer. */
42
42
  export function authorized({ request, baseUrl, opts, runtimeContext }, partitionIdentity = true) {
43
- return Effect.suspend(() => admitted(authorize(request, baseUrl, opts.auth, runtimeContext, opts.identity, partitionIdentity)));
43
+ return Effect.suspend(() => admitted(authorize(request, baseUrl, opts.config.auth, runtimeContext, opts.config.identity, partitionIdentity)));
44
44
  }
45
45
  /** A signed-in person, for a route that changes how a connector authenticates. */
46
46
  export function authorizedPerson({ request, baseUrl, opts, runtimeContext }, purpose) {
47
- return Effect.suspend(() => admitted(authorizeUiIdentity(request, baseUrl, opts.auth, purpose, runtimeContext, opts.identity)));
47
+ return Effect.suspend(() => admitted(authorizeUiIdentity(request, baseUrl, opts.config.auth, purpose, runtimeContext, opts.config.identity)));
48
48
  }
49
49
  /** The partition and tool grants an identity carries into a registry view. */
50
50
  export function scopeFor(authz, connectorIds = authz.connectorIds) {
@@ -1,66 +1,30 @@
1
- import type { Implementation } from "@modelcontextprotocol/server";
2
- import type { AccessTokensModule, ActivityModule, ArtifactsModule, OperatorSurface } from "../module-contracts.js";
3
- import type { ActivityActor, ActivityReadGate, ActivityStore } from "../activity.js";
4
- import type { CredentialVault } from "../credential-contract.js";
1
+ import type { ActivityActor } from "../activity.js";
5
2
  import type { DeferredWork } from "../connector-scope.js";
6
3
  import type { AdmissionController } from "../executor-admission.js";
7
4
  import type { Registry, ToolAccess } from "../registry.js";
8
5
  import type { ResolvedPool } from "../connector-access.js";
9
- import type { AuthenticatedIdentity, ConnectaBranding, Executor, InboundAuth, InboundAuthRuntimeContext, Logger } from "../types.js";
10
- import type { ApprovalPolicy } from "../tool-safety.js";
11
- import type { ConnectorPermission, ConnectaIdentityConfig } from "../index.js";
6
+ import type { AuthenticatedIdentity, Executor, InboundAuth, InboundAuthRuntimeContext } from "../types.js";
7
+ import type { ConnectorPermission, ConnectaIdentityConfig, ResolvedConfig } from "../config.js";
12
8
  export { msg } from "../errors.js";
13
9
  export interface ServerOptions {
10
+ /**
11
+ * Everything createConnecta resolved from configuration: auth, identity,
12
+ * URLs, modules, logger, and every limit with its default applied. Routes
13
+ * read it here rather than from fields copied out of it one by one.
14
+ */
15
+ config: ResolvedConfig;
16
+ /** Construction-time allowlisted snapshot, frozen by describeConfig. */
17
+ configDescription: import("../describe-config.js").ConnectaConfigDescription;
18
+ configValueSources?: Readonly<Record<string, "default" | "config">>;
14
19
  registry: Registry;
15
- auth: InboundAuth[];
16
- identity?: ConnectaIdentityConfig | undefined;
17
20
  /** Validated named pools served at `/mcp/<name>`; empty when none declared. */
18
- pools?: ReadonlyMap<string, ResolvedPool> | undefined;
19
- publicUrl?: string | undefined;
20
- artifactOrigin?: string | undefined;
21
- allowedOrigins?: readonly string[] | "*" | undefined;
22
- serverInfo: Implementation;
23
- logger: Logger;
24
- activity?: ActivityStore | undefined;
25
- activityReadGate?: ActivityReadGate | undefined;
26
- activityDeploymentId?: string | undefined;
27
- deploymentInfo?: Record<string, unknown> | undefined;
28
- /** Deadline for call_tool/call_destructive_tool calls that pass no timeoutMs. Off when unset. */
29
- defaultToolTimeoutMs?: number | undefined;
30
- /** Per-connector deadline for the search/describe probe fan-out. Default 30_000. */
31
- probeTimeoutMs?: number | undefined;
32
- /** Maximum simultaneous connector discovery operations. Default 4. */
33
- discoveryConcurrency?: number | undefined;
34
- /** Aggregate serialized-byte budget for connecta.emit per run. Default 4_000_000. */
35
- maxEmittedBytes?: number | undefined;
36
- /** Block-count budget for connecta.emit per run. Default 32. */
37
- maxEmittedBlocks?: number | undefined;
38
- /** Host calls one execute_code program may make. Default 20. */
39
- maxHostCalls?: number | undefined;
40
- /** Deadline per execute_code host call. Default 15_000. */
41
- hostCallTimeoutMs?: number | undefined;
42
- /** Hard ceiling on one execute_code run, outside the sandbox. Default 120_000. */
43
- watchdogMs?: number | undefined;
44
- /** Config approval exemptions (`execute.approval`), resolved at construction. */
45
- approval?: ApprovalPolicy | undefined;
46
- /** Exempt writes one program may send (`execute.maxWrites`). */
47
- maxWrites?: number | undefined;
48
- /** Required sandbox backing the execute_code meta-tool. */
21
+ pools: ReadonlyMap<string, ResolvedPool>;
22
+ /** Required sandbox backing execute_code, wrapped in fallback admission if needed. */
49
23
  executor: Executor;
50
24
  /** Sanitized identity of the configured sandbox, when it has one. */
51
25
  executorName?: string | undefined;
52
26
  /** Global FIFO boundary for all non-preflight `/mcp` requests. */
53
27
  requestAdmission: AdmissionController;
54
- /** Encrypted connector-credential storage backing the Credentials page. */
55
- credentialVault?: CredentialVault | undefined;
56
- /** Optional browser routes, with no implementation import in core. */
57
- ui?: OperatorSurface | undefined;
58
- activityModule?: ActivityModule | undefined;
59
- /** Optional team pages; their routes mount only beside `ui`. */
60
- artifactsModule?: ArtifactsModule | undefined;
61
- accessTokens?: AccessTokensModule | undefined;
62
- /** Optional browser UI and OAuth result-page labels. */
63
- branding?: ConnectaBranding | undefined;
64
28
  }
65
29
  export interface RuntimeExecutionContext extends InboundAuthRuntimeContext {
66
30
  waitUntil(promise: Promise<unknown>): void;
@@ -86,10 +50,16 @@ export declare function privateJson(body: unknown, init?: ResponseInit): Respons
86
50
  */
87
51
  export declare function loggableValue(requested: string): string;
88
52
  export declare function activityActorNamespace(provider: InboundAuth): string | undefined;
89
- export declare function authorize(request: Request, baseUrl: string, auth: InboundAuth[], runtimeContext?: RuntimeExecutionContext, identityConfig?: ConnectaIdentityConfig, partitionIdentity?: boolean): Promise<{
53
+ /** Metadata routing and challenge selection use the same first actual answer. */
54
+ export declare function authMetadata(request: Request, baseUrl: string, auth: readonly InboundAuth[]): Promise<{
55
+ provider: InboundAuth;
56
+ response: Response;
57
+ } | null>;
58
+ export declare function authorize(request: Request, baseUrl: string, auth: readonly InboundAuth[], runtimeContext?: RuntimeExecutionContext, identityConfig?: ConnectaIdentityConfig, partitionIdentity?: boolean, interactiveOnly?: boolean): Promise<{
90
59
  ok: true;
91
60
  actor: ActivityActor;
92
61
  identity: AuthenticatedIdentity;
62
+ sessionCookies?: readonly string[];
93
63
  subjectKey?: string;
94
64
  principalKey?: string;
95
65
  connectorIds: "all" | readonly string[];
@@ -107,12 +77,8 @@ export declare function authorize(request: Request, baseUrl: string, auth: Inbou
107
77
  ok: false;
108
78
  response: Response;
109
79
  }>;
110
- export declare function authorizeUiIdentity(request: Request, baseUrl: string, auth: InboundAuth[], purpose: string, runtimeContext?: RuntimeExecutionContext, identityConfig?: ConnectaIdentityConfig): Promise<Extract<Awaited<ReturnType<typeof authorize>>, {
111
- ok: true;
112
- }> | {
113
- ok: false;
114
- response: Response;
115
- }>;
80
+ /** Human routes reject machine credentials before any verification or storage I/O. */
81
+ export declare function authorizeUiIdentity(request: Request, baseUrl: string, auth: readonly InboundAuth[], purpose: string, runtimeContext?: RuntimeExecutionContext, identityConfig?: ConnectaIdentityConfig): Promise<Awaited<ReturnType<typeof authorize>>>;
116
82
  export declare function isSameOrigin(request: Request, baseUrl: string): boolean;
117
83
  export declare function withSecurityHeaders(response: Response, requestUrl: URL, _path: string): Response;
118
84
  type AuthorizedIdentity = Extract<Awaited<ReturnType<typeof authorize>>, {
@@ -120,14 +86,9 @@ type AuthorizedIdentity = Extract<Awaited<ReturnType<typeof authorize>>, {
120
86
  }>;
121
87
  /** Unknown configured ids refuse the complete view, including management rights. */
122
88
  export declare function validateAuthPermissions(authz: AuthorizedIdentity, registry: Registry): void;
123
- /**
124
- * Whether this identity may open artifact pages: exactly whoever may read the
125
- * `artifacts` connector — it is visible to them and `get_artifact` is among
126
- * its tools they were granted. Viewing grants nothing a program could not
127
- * already read (ethos, "Human routes manage auth, never capability").
128
- */
129
- export declare function mayViewArtifacts(authz: AuthorizedIdentity, registry: Registry): boolean;
130
89
  export declare function mayManageConnector(authz: AuthorizedIdentity, connector: {
131
90
  id: string;
132
91
  authScope?: "shared" | "personal";
133
92
  }): boolean;
93
+ /** Preserve refreshed browser cookies on the redirect or completion page. */
94
+ export declare function withSessionCookies(response: Response, cookies?: readonly string[]): Response;