@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,221 @@
1
+ import { ConnectorCallError } from "../errors.js";
2
+ import { authorizationCredential } from "../inbound-credential.js";
3
+ import { Effect } from "effect";
4
+ import { escapeHtml, renderPage } from "../branding.js";
5
+ import { htmlSecurityHeaders } from "../html-security.js";
6
+ import { closeConnectorScope } from "../connector-scope.js";
7
+ import { drainOAuthStartResets } from "../auth/oauth-start-reset.js";
8
+ import { finishOAuthConnectStart, consumeOAuthConnectLink, oauthConnectUnavailable, verifyOAuthHandoff, } from "../oauth-handoff.js";
9
+ import { failureRecord, logFailure } from "../operator-record.js";
10
+ import { runEdge, withDeadlineEffect } from "../runtime/run.js";
11
+ import { authorizeUiIdentity, mayManageConnector, privateJson, validateAuthPermissions, withSessionCookies, } from "./shared.js";
12
+ const START_TIMEOUT_MS = 30_000;
13
+ /** Clerk establishes a browser session on this origin before retrying the link. */
14
+ function clerkSignIn(context) {
15
+ const clerk = context.opts.config.auth.find((provider) => provider.uiAuth?.kind === "clerk")?.uiAuth;
16
+ if (clerk?.kind !== "clerk")
17
+ return undefined;
18
+ const returnUrl = new URL(context.path, context.baseUrl);
19
+ returnUrl.search = context.url.search;
20
+ if (clerk.signInUrl) {
21
+ try {
22
+ const target = new URL(clerk.signInUrl);
23
+ if (target.protocol === "https:" && !target.username && !target.password) {
24
+ target.searchParams.set("redirect_url", returnUrl.href);
25
+ return new Response(null, { status: 302, headers: { Location: target.href, "Cache-Control": "no-store" } });
26
+ }
27
+ }
28
+ catch {
29
+ /* Use the local sign-in page. */
30
+ }
31
+ }
32
+ let origin;
33
+ try {
34
+ const frontend = new URL(clerk.frontendApiUrl);
35
+ if (frontend.protocol !== "https:" || frontend.username || frontend.password)
36
+ return undefined;
37
+ origin = frontend.origin;
38
+ }
39
+ catch {
40
+ return undefined;
41
+ }
42
+ const nonce = crypto.randomUUID();
43
+ const target = JSON.stringify(returnUrl.href).replace(/</g, "\\u003c");
44
+ const body = `<main class="page shell"><h1>Sign in to connect</h1><p>Sign in as the user who requested this connection.</p><div id="signin"></div></main>
45
+ <script nonce="${nonce}" crossorigin="anonymous" data-clerk-publishable-key="${escapeHtml(clerk.publishableKey)}" src="${escapeHtml(origin)}/npm/@clerk/clerk-js@6/dist/clerk.browser.js"></script>
46
+ <script nonce="${nonce}">window.Clerk.load().then(() => {
47
+ if (window.Clerk.session) { window.location.replace(${target}); return; }
48
+ window.Clerk.mountSignIn(document.getElementById("signin"), { routing: "hash", forceRedirectUrl: ${target}, signUpForceRedirectUrl: ${target} });
49
+ }).catch(() => { document.getElementById("signin").textContent = "Sign-in could not load. Try again."; });</script>`;
50
+ return new Response(renderPage(context.opts.config.ui?.branding, {
51
+ title: "Sign in to connect",
52
+ uiMounted: Boolean(context.opts.config.ui),
53
+ body,
54
+ }), {
55
+ headers: htmlSecurityHeaders({
56
+ "Content-Type": "text/html; charset=utf-8",
57
+ "Cache-Control": "no-store",
58
+ }, { clerkOrigin: origin, nonce }),
59
+ });
60
+ }
61
+ export function routeConnect(context) {
62
+ if (!context.path.startsWith("/connect/"))
63
+ return Effect.succeed(null);
64
+ return Effect.promise(() => connect(context));
65
+ }
66
+ async function connect(context) {
67
+ const { opts, request, baseUrl, runtimeContext } = context;
68
+ const refuse = (error, status = 403) => privateJson({ error }, { status });
69
+ if (request.method !== "GET")
70
+ return privateJson({ error: "method not allowed" }, { status: 405, headers: { Allow: "GET" } });
71
+ const unavailable = oauthConnectUnavailable(opts);
72
+ if (unavailable)
73
+ return refuse(unavailable);
74
+ const id = context.path.slice("/connect/".length);
75
+ const handoff = await verifyOAuthHandoff(opts, baseUrl, id, context.url.searchParams.get("h"));
76
+ if (!handoff)
77
+ return refuse("Invalid or expired connection link. Request a new link from connecta.", 400);
78
+ const authPage = () => {
79
+ const target = new URL(`/connectors/${id}`, baseUrl);
80
+ target.searchParams.set("h", context.url.searchParams.get("h"));
81
+ target.hash = "auth";
82
+ return new Response(null, {
83
+ status: 302,
84
+ headers: { Location: target.href, "Cache-Control": "no-store", "Referrer-Policy": "no-referrer" },
85
+ });
86
+ };
87
+ const authz = await authorizeUiIdentity(request, baseUrl, opts.config.auth, "OAuth connection", runtimeContext, opts.config.identity);
88
+ if (!authz.ok) {
89
+ // Access sign-in is enforced at the edge. Clerk needs its own sign-in page.
90
+ if (authz.response.status === 401 &&
91
+ authorizationCredential(request).kind === "absent" &&
92
+ !runtimeContext?.access) {
93
+ if (opts.config.ui)
94
+ return authPage();
95
+ const signIn = clerkSignIn(context);
96
+ if (signIn)
97
+ return signIn;
98
+ }
99
+ return authz.response;
100
+ }
101
+ try {
102
+ validateAuthPermissions(authz, opts.registry);
103
+ }
104
+ catch {
105
+ return refuse("forbidden");
106
+ }
107
+ const connector = opts.registry.getConnector(id);
108
+ if (authz.principalKey !== handoff.principal || !mayManageConnector(authz, connector)) {
109
+ return refuse("Sign in as the user who requested this connection and has permission to manage it.");
110
+ }
111
+ if (!connector.startAuth) {
112
+ if (!connector.credential || !opts.config.ui || !opts.config.vault)
113
+ return refuse("unknown OAuth connector", 404);
114
+ let target;
115
+ try {
116
+ target = new URL(opts.config.ui.credentialHandoffUrl(baseUrl), baseUrl);
117
+ }
118
+ catch {
119
+ return refuse("Credential management URL is unavailable", 503);
120
+ }
121
+ if (target.origin !== new URL(baseUrl).origin || target.username || target.password)
122
+ return refuse("Credential management URL is unavailable", 503);
123
+ if (!(await consumeOAuthConnectLink(opts, handoff)))
124
+ return refuse("Invalid or expired connection link. Request a new link from connecta.", 400);
125
+ if (!(await finishOAuthConnectStart(opts, handoff, "started")))
126
+ return refuse("Invalid or expired connection link. Request a new link from connecta.", 400);
127
+ // Credential entry stays in the authenticated operator UI on this origin.
128
+ return withSessionCookies(new Response(null, {
129
+ status: 302,
130
+ headers: {
131
+ Location: target.href,
132
+ "Cache-Control": "no-store",
133
+ },
134
+ }), authz.sessionCookies);
135
+ }
136
+ if (opts.config.ui && context.url.searchParams.get("start") !== "1")
137
+ return withSessionCookies(authPage(), authz.sessionCookies);
138
+ const registry = opts.registry.scoped({
139
+ connectorIds: [id],
140
+ principalKey: authz.principalKey,
141
+ ...(authz.subjectKey ? { subjectKey: authz.subjectKey } : {}),
142
+ });
143
+ const scope = {};
144
+ let ctx = registry.contextFor(id, baseUrl, scope, context.defer ? { defer: context.defer } : {});
145
+ const timeoutError = new ConnectorCallError("timeout", "OAuth authorization start timed out");
146
+ let claimed = false;
147
+ try {
148
+ if (!(await consumeOAuthConnectLink(opts, handoff)))
149
+ return refuse("Invalid or expired connection link. Request a new link from connecta.", 400);
150
+ claimed = true;
151
+ const status = await runEdge(withDeadlineEffect((signal) => Effect.tryPromise({
152
+ try: async () => {
153
+ ctx = registry.contextFor(id, baseUrl, scope, {
154
+ signal,
155
+ ...(context.defer ? { defer: context.defer } : {}),
156
+ });
157
+ const started = await connector.startAuth(ctx, { force: handoff.force });
158
+ if (signal.aborted)
159
+ throw signal.reason;
160
+ if (started.authorizationUrl) {
161
+ let target;
162
+ try {
163
+ target = new URL(started.authorizationUrl);
164
+ }
165
+ catch {
166
+ /* Refused below. */
167
+ }
168
+ if (target && ["https:", "http:"].includes(target.protocol) && !target.username && !target.password) {
169
+ await registry.bindOAuthHandoff(id, target.href);
170
+ }
171
+ else {
172
+ return { ...started, authorizationUrl: undefined };
173
+ }
174
+ }
175
+ return started;
176
+ },
177
+ catch: (error) => error,
178
+ }), { timeoutMs: START_TIMEOUT_MS, signal: request.signal, timeoutError }));
179
+ await drainOAuthStartResets(scope);
180
+ if (handoff.force || (!status.authorizationReused && status.state !== "ok"))
181
+ await registry.invalidateStored(id);
182
+ if (status.state === "ok") {
183
+ if (!(await finishOAuthConnectStart(opts, handoff, "started")))
184
+ return refuse("Invalid or expired connection link. Request a new link from connecta.", 400);
185
+ return withSessionCookies(new Response("This connector is already connected.", { headers: { "Cache-Control": "no-store" } }), authz.sessionCookies);
186
+ }
187
+ if (status.state !== "auth_required" || !status.authorizationUrl) {
188
+ // The start's message can be a downstream's refusal, which an agent
189
+ // may read but a log may not (INV-6): the record keeps the checked
190
+ // state only, since a plugin's `startAuth` returns whatever it likes.
191
+ logFailure(opts.config.logger, "OAuth start failed", failureRecord({
192
+ connector: id,
193
+ mode: handoff.force ? "restart" : "continue",
194
+ state: status.state,
195
+ }));
196
+ return refuse(status.state === "auth_required"
197
+ ? "OAuth authorization requires consent but no safe URL is available"
198
+ : "OAuth authorization could not start", 502);
199
+ }
200
+ const target = new URL(status.authorizationUrl);
201
+ if (!["https:", "http:"].includes(target.protocol) || target.username || target.password)
202
+ return refuse("OAuth authorization returned no safe URL", 502);
203
+ if (!(await finishOAuthConnectStart(opts, handoff, "started")))
204
+ return refuse("Invalid or expired connection link. Request a new link from connecta.", 400);
205
+ return withSessionCookies(new Response(null, { status: 302, headers: { Location: target.href, "Cache-Control": "no-store" } }), authz.sessionCookies);
206
+ }
207
+ catch (error) {
208
+ await drainOAuthStartResets(scope);
209
+ await registry.invalidateStored(id);
210
+ if (error === timeoutError)
211
+ return refuse("OAuth authorization start timed out", 504);
212
+ logFailure(opts.config.logger, "OAuth start failed", failureRecord({ connector: id }, error));
213
+ return refuse("OAuth authorization could not start", 400);
214
+ }
215
+ finally {
216
+ await drainOAuthStartResets(scope);
217
+ await closeConnectorScope(connector, ctx, context.defer);
218
+ if (claimed)
219
+ await finishOAuthConnectStart(opts, handoff, "failed");
220
+ }
221
+ }
@@ -1,8 +1,9 @@
1
+ import { failureRecord, logFailure } from "../operator-record.js";
1
2
  import { Effect } from "effect";
2
3
  import { claimConnectorScopeCleanup, closeConnectorScope } from "../connector-scope.js";
3
- import { credentialTestRule, describeCredentialTestMismatch, storedCredentialShape, } from "../credential-rules.js";
4
- import { authorizedPerson, refuse, serveOperator, visibleRegistry, } from "./operator.js";
5
- import { mayManageConnector, isSameOrigin, msg, privateJson, } from "./shared.js";
4
+ import { credentialTestRule, describeCredentialTestMismatch, storedCredentialShape } from "../credential-rules.js";
5
+ import { authorizedPerson, refuse, serveOperator, visibleRegistry } from "./operator.js";
6
+ import { mayManageConnector, isSameOrigin, msg, privateJson } from "./shared.js";
6
7
  /** The longest body, in characters, a credential write accepts. */
7
8
  const MAX_BODY_CHARS = 20_000;
8
9
  /**
@@ -48,9 +49,7 @@ function credentialInput(body, config) {
48
49
  }
49
50
  return Effect.succeed({ kind: "single", value: body.value });
50
51
  }
51
- if (!body.values ||
52
- typeof body.values !== "object" ||
53
- Array.isArray(body.values)) {
52
+ if (!body.values || typeof body.values !== "object" || Array.isArray(body.values)) {
54
53
  return refuse("values must be an object", 400);
55
54
  }
56
55
  const rawValues = body.values;
@@ -71,10 +70,7 @@ function credentialInput(body, config) {
71
70
  }
72
71
  function readCredentialInput(request, config) {
73
72
  return Effect.gen(function* () {
74
- if (!request.headers
75
- .get("content-type")
76
- ?.toLowerCase()
77
- .startsWith("application/json")) {
73
+ if (!request.headers.get("content-type")?.toLowerCase().startsWith("application/json")) {
78
74
  return yield* refuse("Content-Type must be application/json", 415);
79
75
  }
80
76
  const raw = yield* Effect.promise(() => boundedText(request));
@@ -96,7 +92,7 @@ function readCredentialInput(request, config) {
96
92
  function credentialRequest(context, connectorId, action) {
97
93
  const { request, baseUrl, opts } = context;
98
94
  return Effect.gen(function* () {
99
- const vault = opts.credentialVault;
95
+ const vault = opts.config.vault;
100
96
  if (!vault)
101
97
  return yield* refuse("credential storage is not configured", 503);
102
98
  if (!isSameOrigin(request, baseUrl)) {
@@ -117,7 +113,7 @@ function credentialRequest(context, connectorId, action) {
117
113
  const owner = personal ? authz.principalKey : undefined;
118
114
  const updatedBy = authz.identity.principal
119
115
  ? `${authz.identity.principal.namespace}:${authz.identity.principal.id}`
120
- : authz.actor.id ?? authz.actor.kind;
116
+ : (authz.actor.id ?? authz.actor.kind);
121
117
  if (action === "test") {
122
118
  if (request.method !== "POST")
123
119
  return yield* refuse("method not allowed", 405);
@@ -127,25 +123,16 @@ function credentialRequest(context, connectorId, action) {
127
123
  const { mode, mismatch } = credentialTestRule(connector);
128
124
  if (!mode) {
129
125
  return yield* refuse(mismatch
130
- ? "this connector cannot test its credential: " +
131
- describeCredentialTestMismatch(mismatch)
126
+ ? "this connector cannot test its credential: " + describeCredentialTestMismatch(mismatch)
132
127
  : "this connector does not support credential testing", 400);
133
128
  }
134
129
  // Whatever goes wrong past this point is the test's result, not the
135
130
  // route's: the page shows it beside the Test button. Only `ok` leaves
136
131
  // the host. A hook's message, and anything it throws, can quote the
137
132
  // downstream's reply, and that reply can quote the credential it just
138
- // rejected — so the text goes to the deployment's log and the page
139
- // says a fixed sentence for the outcome instead.
140
- const logged = (ok, detail) => {
141
- if (!detail)
142
- return;
143
- const line = `[connecta] connector "${connectorId}" credential test ${ok ? "passed" : "failed"}: ${detail}`;
144
- if (ok)
145
- opts.logger.info(line);
146
- else
147
- opts.logger.warn(line);
148
- };
133
+ // rejected — so neither the page nor the log repeats it (INV-6): the
134
+ // page says a fixed sentence for the outcome, and the log records a
135
+ // failure's class.
149
136
  return yield* Effect.tryPromise({
150
137
  try: async () => {
151
138
  const values = await vault.getAll(connectorId, owner);
@@ -173,7 +160,8 @@ function credentialRequest(context, connectorId, action) {
173
160
  // The single-value shape check above guarantees this key.
174
161
  storedValues.value, ctx);
175
162
  const ok = result?.ok === true;
176
- logged(ok, result?.message);
163
+ if (!ok)
164
+ logFailure(opts.config.logger, "credential test failed", failureRecord({ connector: connectorId }, undefined));
177
165
  return privateJson({ ok });
178
166
  }
179
167
  finally {
@@ -183,7 +171,7 @@ function credentialRequest(context, connectorId, action) {
183
171
  },
184
172
  catch: (error) => error,
185
173
  }).pipe(Effect.catch((error) => {
186
- logged(false, msg(error));
174
+ logFailure(opts.config.logger, "credential test threw", failureRecord({ connector: connectorId }, error));
187
175
  return Effect.succeed(privateJson({ ok: false }));
188
176
  }));
189
177
  }