@zackbart/connecta 0.28.0 → 0.29.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (320) hide show
  1. package/CHANGELOG.md +961 -5
  2. package/README.md +62 -39
  3. package/bin/connecta.mjs +110 -139
  4. package/bin/meta-tool-names.d.mts +2 -0
  5. package/bin/meta-tool-names.mjs +9 -0
  6. package/bin/version.mjs +3 -0
  7. package/dist/access-tokens.js +53 -63
  8. package/dist/activity-request.js +8 -0
  9. package/dist/activity.d.ts +76 -42
  10. package/dist/activity.js +110 -42
  11. package/dist/auth/clerk-sdk/client.d.ts +65 -0
  12. package/dist/auth/clerk-sdk/client.js +5 -0
  13. package/dist/auth/clerk-sdk/generated.js +1845 -0
  14. package/dist/auth/clerk-transport.js +19 -0
  15. package/dist/auth/clerk.d.ts +17 -7
  16. package/dist/auth/clerk.js +189 -107
  17. package/dist/auth/cloudflare-access.js +25 -26
  18. package/dist/auth/downstream-client-metadata.d.ts +29 -0
  19. package/dist/auth/downstream-client-metadata.js +133 -0
  20. package/dist/auth/downstream-oauth.js +1632 -1509
  21. package/dist/auth/downstream-revocation.js +106 -0
  22. package/dist/auth/oauth-token-response.js +231 -0
  23. package/dist/auth/oauth-v2-migration.js +116 -0
  24. package/dist/auth/static-oauth.js +526 -0
  25. package/dist/auth-elicitation.js +283 -0
  26. package/dist/branding.d.ts +8 -16
  27. package/dist/branding.js +26 -59
  28. package/dist/byte-read-response.js +39 -0
  29. package/dist/call-admission.js +12 -16
  30. package/dist/catalog-cache.js +739 -0
  31. package/dist/catalog-drift.d.ts +50 -15
  32. package/dist/catalog-drift.js +365 -128
  33. package/dist/catalog-fingerprint.js +2 -4
  34. package/dist/catalog-freshness.js +13 -0
  35. package/dist/catalog-limits.js +2 -2
  36. package/dist/catalog-service.js +368 -224
  37. package/dist/catalog.js +43 -84
  38. package/dist/concurrency.js +3 -4
  39. package/dist/config-defaults.d.ts +39 -0
  40. package/dist/config-defaults.js +46 -0
  41. package/dist/config-schema.d.ts +181 -0
  42. package/dist/config-schema.js +435 -0
  43. package/dist/config-value-sources.js +102 -0
  44. package/dist/config.d.ts +631 -0
  45. package/dist/config.js +503 -0
  46. package/dist/connector-access.d.ts +1 -0
  47. package/dist/connector-access.js +13 -15
  48. package/dist/connector-caller.d.ts +8 -0
  49. package/dist/connector-caller.js +15 -6
  50. package/dist/connector-scope.js +1 -1
  51. package/dist/connectors/api-connector.d.ts +220 -0
  52. package/dist/connectors/api-connector.js +214 -0
  53. package/dist/connectors/api.d.ts +8 -71
  54. package/dist/connectors/api.js +13 -99
  55. package/dist/connectors/guarded-fetch.d.ts +144 -0
  56. package/dist/connectors/guarded-fetch.js +108 -25
  57. package/dist/connectors/negotiation-cache.js +46 -0
  58. package/dist/connectors/option-shapes.js +27 -0
  59. package/dist/connectors/remote-mcp.d.ts +30 -4
  60. package/dist/connectors/remote-mcp.js +1780 -351
  61. package/dist/connectors/resource-uri.js +335 -0
  62. package/dist/credential-contract.d.ts +10 -0
  63. package/dist/credential-rules.js +1 -3
  64. package/dist/credential-url.js +108 -0
  65. package/dist/credentials.d.ts +6 -1
  66. package/dist/credentials.js +27 -18
  67. package/dist/d1.d.ts +130 -0
  68. package/dist/d1.js +290 -0
  69. package/dist/describe-config.d.ts +165 -0
  70. package/dist/describe-config.js +315 -0
  71. package/dist/described.js +106 -0
  72. package/dist/downstream-input-context.js +119 -0
  73. package/dist/downstream-input.js +323 -0
  74. package/dist/errors.d.ts +97 -20
  75. package/dist/errors.js +158 -80
  76. package/dist/execute.js +453 -305
  77. package/dist/executor-admission.d.ts +1 -0
  78. package/dist/executor-admission.js +11 -15
  79. package/dist/executor-contract.d.ts +20 -0
  80. package/dist/executor-contract.js +48 -0
  81. package/dist/executor-result.js +24 -9
  82. package/dist/executors/quickjs-child.js +3 -4
  83. package/dist/executors/quickjs-runtime.js +147 -59
  84. package/dist/executors/quickjs.js +29 -35
  85. package/dist/favicon.js +1 -2
  86. package/dist/fix-prompt.js +1 -3
  87. package/dist/guest-runtime.js +188 -0
  88. package/dist/guest-types.d.ts +176 -0
  89. package/dist/guest-types.js +1 -0
  90. package/dist/html-security.js +24 -0
  91. package/dist/identity.js +1 -3
  92. package/dist/inbound-credential.js +52 -0
  93. package/dist/index.d.ts +25 -257
  94. package/dist/index.js +65 -478
  95. package/dist/invocation-auth.js +69 -0
  96. package/dist/invocation.js +230 -73
  97. package/dist/json-schema.d.ts +1 -1
  98. package/dist/mcp-client-context.js +35 -0
  99. package/dist/mcp-result.js +11 -7
  100. package/dist/meta-output.js +76 -0
  101. package/dist/meta-tool-names.d.ts +2 -0
  102. package/dist/meta-tool-names.js +3 -0
  103. package/dist/meta-tools.js +285 -209
  104. package/dist/module-contracts.d.ts +8 -23
  105. package/dist/node.d.ts +0 -2
  106. package/dist/node.js +2 -2
  107. package/dist/oauth-callback-outcome.js +1 -3
  108. package/dist/oauth-handoff.js +117 -0
  109. package/dist/oauth-partition.js +17 -0
  110. package/dist/oauth-sealing.js +20 -0
  111. package/dist/operator-record.d.ts +137 -0
  112. package/dist/operator-record.js +420 -0
  113. package/dist/operator-ui/assets.js +31 -0
  114. package/dist/operator-ui/contract.d.ts +72 -0
  115. package/dist/operator-ui/contract.js +1 -0
  116. package/dist/operator-ui/fix-prompts.d.ts +18 -0
  117. package/dist/operator-ui/generated.js +5 -4
  118. package/dist/operator-ui/model.d.ts +12 -87
  119. package/dist/operator-ui/model.js +2 -9
  120. package/dist/operator-ui/setup-commands.js +1 -3
  121. package/dist/operator-ui/view.d.ts +323 -0
  122. package/dist/operator-ui/view.js +62 -189
  123. package/dist/page-styles.js +2 -3
  124. package/dist/program-source.js +14 -2
  125. package/dist/{exempt-writes.js → program-writes.js} +55 -18
  126. package/dist/provider.d.ts +142 -0
  127. package/dist/provider.js +187 -0
  128. package/dist/providers/_shared/google/result-size.js +57 -0
  129. package/dist/providers/_shared/google/service-account.d.ts +58 -0
  130. package/dist/providers/_shared/google/service-account.js +500 -0
  131. package/dist/providers/_shared/google/workspace.d.ts +276 -0
  132. package/dist/providers/_shared/google/workspace.js +518 -0
  133. package/dist/providers/basecamp/index.d.ts +43 -0
  134. package/dist/providers/basecamp/index.js +997 -0
  135. package/dist/providers/basecamp/skill.generated.js +9 -0
  136. package/dist/providers/breeze/index.d.ts +34 -0
  137. package/dist/providers/breeze/index.js +1446 -0
  138. package/dist/providers/breeze/skill.generated.js +9 -0
  139. package/dist/providers/ccb/index.d.ts +103 -0
  140. package/dist/providers/ccb/index.js +1725 -0
  141. package/dist/providers/ccb/skill.generated.js +9 -0
  142. package/dist/providers/{cloudflare.d.ts → cloudflare/index.d.ts} +8 -7
  143. package/dist/providers/{cloudflare.js → cloudflare/index.js} +171 -738
  144. package/dist/providers/cloudflare/skill.generated.js +10 -0
  145. package/dist/providers/docs/index.d.ts +54 -0
  146. package/dist/providers/docs/index.js +1237 -0
  147. package/dist/providers/docs/skill.generated.js +9 -0
  148. package/dist/providers/drive/index.d.ts +53 -0
  149. package/dist/providers/drive/index.js +1523 -0
  150. package/dist/providers/drive/skill.generated.js +9 -0
  151. package/dist/providers/forms/index.d.ts +54 -0
  152. package/dist/providers/forms/index.js +1217 -0
  153. package/dist/providers/forms/skill.generated.js +11 -0
  154. package/dist/providers/github/auth.d.ts +32 -0
  155. package/dist/providers/github/auth.js +361 -0
  156. package/dist/providers/github/hosted.js +218 -0
  157. package/dist/providers/github/index.d.ts +13 -0
  158. package/dist/providers/github/index.js +140 -0
  159. package/dist/providers/github/key.js +100 -0
  160. package/dist/providers/github/rest.js +321 -0
  161. package/dist/providers/github/scope.d.ts +29 -0
  162. package/dist/providers/github/scope.js +124 -0
  163. package/dist/providers/github/skill.generated.js +8 -0
  164. package/dist/providers/gmail/index.d.ts +51 -0
  165. package/dist/providers/gmail/index.js +1436 -0
  166. package/dist/providers/gmail/skill.generated.js +9 -0
  167. package/dist/providers/{linear.d.ts → linear/index.d.ts} +11 -14
  168. package/dist/providers/linear/index.js +352 -0
  169. package/dist/providers/linear/skill.generated.js +8 -0
  170. package/dist/providers/{mixpanel.d.ts → mixpanel/index.d.ts} +5 -10
  171. package/dist/providers/mixpanel/index.js +416 -0
  172. package/dist/providers/mixpanel/skill.generated.js +9 -0
  173. package/dist/providers/{notion.d.ts → notion/index.d.ts} +8 -8
  174. package/dist/providers/{notion.js → notion/index.js} +226 -239
  175. package/dist/providers/notion/skill.generated.js +10 -0
  176. package/dist/providers/overflow/index.d.ts +60 -0
  177. package/dist/providers/overflow/index.js +1535 -0
  178. package/dist/providers/overflow/skill.generated.js +8 -0
  179. package/dist/providers/planning-center/index.d.ts +59 -0
  180. package/dist/providers/planning-center/index.js +2199 -0
  181. package/dist/providers/planning-center/skill.generated.js +10 -0
  182. package/dist/providers/{revenuecat.d.ts → revenuecat/index.d.ts} +5 -10
  183. package/dist/providers/revenuecat/index.js +557 -0
  184. package/dist/providers/revenuecat/skill.generated.js +11 -0
  185. package/dist/providers/sheets/index.d.ts +54 -0
  186. package/dist/providers/sheets/index.js +1237 -0
  187. package/dist/providers/sheets/skill.generated.js +10 -0
  188. package/dist/providers/slides/index.d.ts +53 -0
  189. package/dist/providers/slides/index.js +2645 -0
  190. package/dist/providers/slides/skill.generated.js +9 -0
  191. package/dist/providers/{stripe.d.ts → stripe/index.d.ts} +5 -12
  192. package/dist/providers/stripe/index.js +266 -0
  193. package/dist/providers/stripe/skill.generated.js +11 -0
  194. package/dist/providers/tithely/index.d.ts +49 -0
  195. package/dist/providers/tithely/index.js +1003 -0
  196. package/dist/providers/tithely/skill.generated.js +8 -0
  197. package/dist/providers/{vercel.d.ts → vercel/index.d.ts} +8 -8
  198. package/dist/providers/vercel/index.js +1038 -0
  199. package/dist/providers/vercel/mcp-ownership.js +212 -0
  200. package/dist/providers/vercel/skill.generated.js +11 -0
  201. package/dist/registry.d.ts +108 -171
  202. package/dist/registry.js +513 -925
  203. package/dist/request-state.js +21 -0
  204. package/dist/resource-read.js +100 -0
  205. package/dist/result-shapes.js +17 -49
  206. package/dist/retired-toolkits.js +1 -1
  207. package/dist/routes/access-tokens.js +3 -6
  208. package/dist/routes/activity-context.js +21 -0
  209. package/dist/routes/activity-disclosure.js +40 -0
  210. package/dist/routes/activity.js +59 -21
  211. package/dist/routes/actor-labels.js +3 -50
  212. package/dist/routes/connect.js +221 -0
  213. package/dist/routes/credentials.js +15 -27
  214. package/dist/routes/mcp.js +356 -130
  215. package/dist/routes/oauth-client-metadata.js +15 -0
  216. package/dist/routes/oauth-management.js +45 -107
  217. package/dist/routes/oauth.js +136 -57
  218. package/dist/routes/operator.js +3 -3
  219. package/dist/routes/shared.d.ts +25 -64
  220. package/dist/routes/shared.js +160 -80
  221. package/dist/routes/skills.js +30 -0
  222. package/dist/routes/ui-config.js +225 -0
  223. package/dist/routes/ui-data.js +40 -35
  224. package/dist/routes/ui.js +66 -56
  225. package/dist/runtime/admission.js +4 -2
  226. package/dist/runtime/call-admission.js +3 -1
  227. package/dist/runtime/connector-scope.js +1 -4
  228. package/dist/runtime/run.js +10 -39
  229. package/dist/runtime/services.js +12 -117
  230. package/dist/runtime/shared-read.js +1 -3
  231. package/dist/runtime/storage.js +8 -1
  232. package/dist/sent-secrets.d.ts +52 -0
  233. package/dist/sent-secrets.js +474 -0
  234. package/dist/server.js +28 -71
  235. package/dist/skills.js +406 -174
  236. package/dist/sqlite.d.ts +48 -0
  237. package/dist/sqlite.js +166 -0
  238. package/dist/storage/keys.js +428 -0
  239. package/dist/storage/memory.js +7 -0
  240. package/dist/storage/sql.d.ts +66 -0
  241. package/dist/storage/sql.js +534 -0
  242. package/dist/timeout.js +4 -2
  243. package/dist/tool-name.js +17 -0
  244. package/dist/tool-safety.d.ts +9 -35
  245. package/dist/tool-safety.js +20 -40
  246. package/dist/types.d.ts +320 -54
  247. package/dist/types.js +0 -1
  248. package/dist/ui.d.ts +7 -19
  249. package/dist/ui.js +76 -90
  250. package/dist/url-safety.js +3 -6
  251. package/dist/usage-guide.js +137 -0
  252. package/dist/validate.d.ts +7 -0
  253. package/dist/validate.js +309 -47
  254. package/dist/version.d.ts +1 -7
  255. package/dist/version.js +3 -7
  256. package/dist/worker.d.ts +11 -0
  257. package/dist/worker.js +434 -0
  258. package/documentation/architecture.md +665 -230
  259. package/documentation/auth.md +962 -458
  260. package/documentation/code-mode.md +501 -312
  261. package/documentation/meta-tools.md +449 -200
  262. package/documentation/operator-tests.md +93 -0
  263. package/documentation/provider-migration-0.29.md +60 -0
  264. package/examples/worker/AGENTS.md +34 -12
  265. package/examples/worker/README.md +286 -255
  266. package/examples/worker/kv-to-d1.wrangler.jsonc +26 -0
  267. package/examples/worker/scripts/copy-kv-to-d1.mjs +162 -0
  268. package/examples/worker/src/connecta.config.ts +151 -0
  269. package/examples/worker/src/index.ts +14 -197
  270. package/examples/worker/wrangler.jsonc +15 -35
  271. package/package.json +125 -35
  272. package/templates/node/.dockerignore +1 -2
  273. package/templates/node/.env.example +20 -21
  274. package/templates/node/AGENTS.md +21 -17
  275. package/templates/node/Dockerfile +3 -5
  276. package/templates/node/README.md +133 -58
  277. package/templates/node/docker-compose.yml +11 -12
  278. package/templates/node/package.json +4 -2
  279. package/templates/node/src/connecta.config.ts +119 -0
  280. package/templates/node/src/index.ts +9 -154
  281. package/templates/node/src/provision-token.ts +17 -0
  282. package/AGENTS.md +0 -170
  283. package/dist/artifacts/connector.d.ts +0 -58
  284. package/dist/artifacts/connector.js +0 -730
  285. package/dist/artifacts/document.js +0 -86
  286. package/dist/artifacts/guide.js +0 -140
  287. package/dist/artifacts/html-scan.js +0 -329
  288. package/dist/artifacts/json.js +0 -78
  289. package/dist/artifacts/kv-store.d.ts +0 -20
  290. package/dist/artifacts/kv-store.js +0 -140
  291. package/dist/artifacts/markdown.js +0 -478
  292. package/dist/artifacts/module.d.ts +0 -37
  293. package/dist/artifacts/module.js +0 -66
  294. package/dist/artifacts/operations.d.ts +0 -197
  295. package/dist/artifacts/operations.js +0 -1002
  296. package/dist/artifacts/refresh.d.ts +0 -55
  297. package/dist/artifacts/refresh.js +0 -236
  298. package/dist/artifacts/routes.js +0 -206
  299. package/dist/artifacts/types.d.ts +0 -214
  300. package/dist/artifacts/types.js +0 -22
  301. package/dist/artifacts/validate.d.ts +0 -62
  302. package/dist/artifacts/validate.js +0 -651
  303. package/dist/artifacts.d.ts +0 -6
  304. package/dist/artifacts.js +0 -9
  305. package/dist/auth/bearer.d.ts +0 -11
  306. package/dist/auth/bearer.js +0 -50
  307. package/dist/providers/linear.js +0 -217
  308. package/dist/providers/mixpanel.js +0 -247
  309. package/dist/providers/revenuecat.js +0 -317
  310. package/dist/providers/stripe.js +0 -253
  311. package/dist/providers/vercel.js +0 -1253
  312. package/dist/storage/file.d.ts +0 -15
  313. package/dist/storage/file.js +0 -455
  314. package/ethos.md +0 -93
  315. package/examples/worker/src/cloudflare-kv.ts +0 -43
  316. package/examples/worker/src/d1-activity-row.ts +0 -155
  317. package/examples/worker/src/d1-activity.ts +0 -147
  318. package/examples/worker/src/d1-storage.ts +0 -138
  319. package/examples/worker/src/r2-artifact-blobs.ts +0 -29
  320. package/templates/node/src/file-activity.ts +0 -182
@@ -1,7 +1,6 @@
1
- import type { ConnectaBranding, Connector } from "./types.js";
2
- import type { ActivityStore, ActivityReadGate, recordToolActivity, recordCatalogDriftActivity } from "./activity.js";
1
+ import type { ConnectaBranding } from "./types.js";
2
+ import type { ActivityStore, ActivityReadGate, recordToolActivity, recordCatalogChangeActivity } from "./activity.js";
3
3
  import type { RouteContext } from "./routes/shared.js";
4
- import type { ArtifactRefreshRuntime } from "./artifacts/refresh.js";
5
4
  /** Construction-time UI contract. Core never imports the implementation. */
6
5
  export interface OperatorSurface {
7
6
  readonly branding?: ConnectaBranding;
@@ -16,29 +15,15 @@ export interface ActivityModule {
16
15
  readonly readGate?: ActivityReadGate;
17
16
  handle(context: RouteContext): Promise<Response | null>;
18
17
  readonly recordTool: typeof recordToolActivity;
19
- readonly recordDrift: typeof recordCatalogDriftActivity;
20
- }
21
- /**
22
- * The artifacts module, created by `artifacts()` from /artifacts. Core appends
23
- * its one prebuilt connector to the configured set — the same catalog,
24
- * invocation, admission, and activity paths as any other — and imports none
25
- * of its implementation.
26
- */
27
- export interface ArtifactsModule {
28
- /** The built-in `artifacts` connector. */
29
- readonly connector: Connector;
30
- /**
31
- * Serve the pages' JSON API (`/artifacts/_api/*`) and the sandboxed frame
32
- * (`/artifacts/_frame`); null for any other path. The operator UI delegates
33
- * here and serves the page shells itself, so without `ui` there are no
34
- * artifact routes at all.
35
- */
36
- handle(context: RouteContext): Promise<Response | null>;
37
- /** Bind the optional refresh runner after core has built its registry and executor. */
38
- bindRefresh?(runtime: ArtifactRefreshRuntime): void;
18
+ readonly recordChange: typeof recordCatalogChangeActivity;
39
19
  }
40
20
  /** Optional client-token authentication and operator lifecycle. */
41
21
  export interface AccessTokensModule {
42
22
  readonly auth: import("./types.js").InboundAuth;
43
23
  handle(context: RouteContext): Promise<Response | null>;
24
+ /** Token policy for `describeConfig()`; never token material. */
25
+ describe?(): {
26
+ maxActive: number;
27
+ optionSources?: Readonly<Record<string, "default" | "config">>;
28
+ };
44
29
  }
package/dist/node.d.ts CHANGED
@@ -1,7 +1,5 @@
1
1
  import type { Server } from "node:http";
2
2
  import type { Connecta } from "./index.js";
3
- export { fileStorage } from "./storage/file.js";
4
- export type { FileStorageOptions } from "./storage/file.js";
5
3
  export interface ListenOptions {
6
4
  port: number;
7
5
  /** Interface to bind. Defaults to Node's own (all interfaces). */
package/dist/node.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { createServer } from "node:http";
2
2
  import { Readable } from "node:stream";
3
3
  import { pipeline } from "node:stream/promises";
4
- export { fileStorage } from "./storage/file.js";
4
+ import { failureRecord, logFailure } from "./operator-record.js";
5
5
  /** 10 MiB. Tool arguments are JSON; nothing legitimate approaches this. */
6
6
  const DEFAULT_MAX_BODY_BYTES = 10 * 1024 * 1024;
7
7
  const BODY_TOO_LARGE = Symbol("body-too-large");
@@ -113,7 +113,7 @@ export function listen(connecta, portOrOptions) {
113
113
  if (controller.signal.aborted)
114
114
  return;
115
115
  // A headless deployment has nothing else to go on; never swallow this.
116
- console.error("[connecta] request failed", error);
116
+ logFailure(console, "request failed", failureRecord({}, error), "error");
117
117
  if (res.headersSent) {
118
118
  // Mid-stream failure: appending an error string here would corrupt
119
119
  // the partial body the client has already begun reading. Cutting
@@ -107,9 +107,7 @@ export function oauthCallbackOutcome(reason, connector, productName = "Connecta"
107
107
  tone: outcome.tone,
108
108
  heading: outcome.heading.replace("{name}", () => name),
109
109
  message: outcome.message.replace("{product}", () => productName),
110
- ...(outcome.fix
111
- ? { fixPrompt: renderFixPrompt(outcome.fix, named?.id) }
112
- : {}),
110
+ ...(outcome.fix ? { fixPrompt: renderFixPrompt(outcome.fix, named?.id) } : {}),
113
111
  };
114
112
  }
115
113
  /** A connector's title when it has a usable one, otherwise its id. */
@@ -0,0 +1,117 @@
1
+ import { oauthConnectKeys } from "./storage/keys.js";
2
+ const HANDOFF_TTL_MS = 15 * 60_000;
3
+ const INTERACTIVE_OAUTH_REQUIRED = "An interactive provider (Clerk or Cloudflare Access) is required to connect OAuth connectors.";
4
+ export function oauthConnectUnavailable(opts) {
5
+ if (!opts.config.auth.some((provider) => provider.interactiveOperator &&
6
+ (provider.uiAuth?.kind === "clerk" || provider.uiAuth?.kind === "cloudflare-access"))) {
7
+ return INTERACTIVE_OAUTH_REQUIRED;
8
+ }
9
+ if (!opts.config.vault?.signOAuthHandoff || !opts.config.vault.verifyOAuthHandoff) {
10
+ return "Connecting OAuth connectors requires a credential vault with a handoff signing key. Configure encryptedCredentialVault(storage, key).";
11
+ }
12
+ return undefined;
13
+ }
14
+ export async function oauthConnectUrl(opts, baseUrl, connectorId, principal, force = false, opaque = false) {
15
+ return (await oauthConnectLink(opts, baseUrl, connectorId, principal, force, opaque)).url;
16
+ }
17
+ export async function oauthConnectLink(opts, baseUrl, connectorId, principal, force = false, opaque = false, after) {
18
+ const unavailable = oauthConnectUnavailable(opts);
19
+ if (unavailable)
20
+ throw new Error(unavailable);
21
+ const connector = opts.registry.getConnector(connectorId);
22
+ if (!connector || !principal)
23
+ throw new Error("OAuth connection requires an initiating user identity.");
24
+ const handoff = {
25
+ connector: connectorId,
26
+ owner: connector.authScope === "personal" ? principal : "shared",
27
+ principal,
28
+ origin: new URL(baseUrl).origin,
29
+ expiresAt: Date.now() + HANDOFF_TTL_MS,
30
+ nonce: crypto.randomUUID(),
31
+ force,
32
+ ...(after ? { after } : {}),
33
+ };
34
+ const vault = opts.config.vault;
35
+ if (opaque && (!vault.seal || !vault.open))
36
+ throw new Error("URL elicitation requires a sealing credential vault.");
37
+ const payload = opaque
38
+ ? `v2:${btoa(await vault.seal(connectorId, "connecta:connect-link:v2", JSON.stringify(handoff)))}`
39
+ : btoa(JSON.stringify(handoff));
40
+ const signature = await opts.config.vault.signOAuthHandoff(payload);
41
+ const url = new URL(`/connect/${connectorId}`, baseUrl);
42
+ url.searchParams.set("h", `${payload}.${signature}`);
43
+ return { url: url.toString(), nonce: handoff.nonce };
44
+ }
45
+ /** A claim precedes OAuth reset; only a completed start can prove recovery. */
46
+ export async function oauthConnectLinkProgress(opts, baseUrl, id, nonce) {
47
+ const stage = await opts.registry.contextFor(id, baseUrl).storage.get(oauthConnectKeys.used(nonce));
48
+ return stage === "used" ? "claimed" : stage === "started" || stage === "failed" ? stage : undefined;
49
+ }
50
+ /** Make outstanding flow links single-use refusals before a completion check. */
51
+ export async function closeOAuthConnectLinks(opts, baseUrl, id, nonces) {
52
+ const storage = opts.registry.contextFor(id, baseUrl).storage;
53
+ for (const nonce of nonces) {
54
+ await storage.compareAndSet(oauthConnectKeys.used(nonce), null, "closed", { ttlSeconds: HANDOFF_TTL_MS / 1000 });
55
+ }
56
+ }
57
+ /** Consuming a round prevents replay from creating sibling retry lineages. */
58
+ export async function claimAuthRetry(opts, baseUrl, id, nonce) {
59
+ return opts.registry
60
+ .contextFor(id, baseUrl)
61
+ .storage.compareAndSet(oauthConnectKeys.retry(nonce), null, "used", { ttlSeconds: HANDOFF_TTL_MS / 1000 });
62
+ }
63
+ export async function verifyOAuthHandoff(opts, baseUrl, connectorId, token) {
64
+ if (!token || token.length > 4096)
65
+ return null;
66
+ const [payload, signature, ...rest] = token.split(".");
67
+ if (!payload || !signature || rest.length)
68
+ return null;
69
+ try {
70
+ if (!(await opts.config.vault?.verifyOAuthHandoff?.(payload, signature)))
71
+ return null;
72
+ const h = JSON.parse(payload.startsWith("v2:")
73
+ ? await opts.config.vault.open(connectorId, "connecta:connect-link:v2", atob(payload.slice(3)))
74
+ : atob(payload));
75
+ const connector = opts.registry.getConnector(connectorId);
76
+ if (!connector ||
77
+ h.connector !== connectorId ||
78
+ typeof h.principal !== "string" ||
79
+ !h.principal ||
80
+ h.owner !== (connector.authScope === "personal" ? h.principal : "shared") ||
81
+ h.origin !== new URL(baseUrl).origin ||
82
+ !Number.isSafeInteger(h.expiresAt) ||
83
+ h.expiresAt <= Date.now() ||
84
+ h.expiresAt > Date.now() + HANDOFF_TTL_MS ||
85
+ typeof h.nonce !== "string" ||
86
+ !h.nonce ||
87
+ typeof h.force !== "boolean")
88
+ return null;
89
+ if (h.after !== undefined &&
90
+ (typeof h.after !== "string" || !/^[a-f0-9-]{36}$/.test(h.after) || h.after === h.nonce || h.force))
91
+ return null;
92
+ if (await opts.registry.contextFor(connectorId, baseUrl).storage.get(oauthConnectKeys.used(h.nonce)))
93
+ return null;
94
+ return h;
95
+ }
96
+ catch {
97
+ return null;
98
+ }
99
+ }
100
+ /** Claim only after browser identity and permissions pass, before starting OAuth. */
101
+ export async function consumeOAuthConnectLink(opts, handoff) {
102
+ if (handoff.expiresAt <= Date.now())
103
+ return false;
104
+ const storage = opts.registry.contextFor(handoff.connector, handoff.origin).storage;
105
+ if (handoff.after && (await storage.get(oauthConnectKeys.used(handoff.after))) !== "started")
106
+ return false;
107
+ const expiry = { ttlSeconds: Math.max(1, Math.ceil((handoff.expiresAt - Date.now()) / 1000)) };
108
+ return storage.compareAndSet(oauthConnectKeys.used(handoff.nonce), null, "used", expiry);
109
+ }
110
+ /** Terminal marker only after the start and every tracked reset have settled. */
111
+ export async function finishOAuthConnectStart(opts, handoff, outcome) {
112
+ if (handoff.expiresAt <= Date.now())
113
+ return false;
114
+ const storage = opts.registry.contextFor(handoff.connector, handoff.origin).storage;
115
+ const expiry = { ttlSeconds: Math.max(1, Math.ceil((handoff.expiresAt - Date.now()) / 1000)) };
116
+ return storage.compareAndSet(oauthConnectKeys.used(handoff.nonce), "used", outcome, expiry);
117
+ }
@@ -19,6 +19,23 @@ export function retainOAuthPartition(partition) {
19
19
  activeWork.set(partition, remaining);
20
20
  };
21
21
  }
22
+ /**
23
+ * Wrap one connector operation so its owner partition stays pinned from the
24
+ * first asynchronous storage read to the end, not only once a refresh flight
25
+ * exists: an idle personal registry can otherwise be evicted mid-flow. Status
26
+ * and auth operations take no call admission, so nothing else holds it.
27
+ */
28
+ export function retainingOAuthPartition(operation, contextIndex) {
29
+ return async (...args) => {
30
+ const release = retainOAuthPartition(oauthPartitionFor(args[contextIndex]));
31
+ try {
32
+ return await operation(...args);
33
+ }
34
+ finally {
35
+ release();
36
+ }
37
+ };
38
+ }
22
39
  export function oauthPartitionIdle(partition) {
23
40
  return !activeWork.has(partition);
24
41
  }
@@ -27,3 +27,23 @@ export function oauthSealerFor(ctx) {
27
27
  export function inheritOAuthSealer(from, to) {
28
28
  return attachOAuthSealer(to, sealers.get(from));
29
29
  }
30
+ /** A separate HMAC key for browser handoffs, derived from the credential key. */
31
+ export async function deriveOAuthHandoffKey(raw) {
32
+ const material = await crypto.subtle.importKey("raw", new Uint8Array(raw), "HKDF", false, ["deriveKey"]);
33
+ return crypto.subtle.deriveKey({
34
+ name: "HKDF",
35
+ hash: "SHA-256",
36
+ salt: new TextEncoder().encode("connecta:oauth-handoff:v1"),
37
+ info: new TextEncoder().encode("browser-connect"),
38
+ }, material, { name: "HMAC", hash: "SHA-256", length: 256 }, false, ["sign", "verify"]);
39
+ }
40
+ /** A purpose-specific MCP retry key, available only to the host's codec. */
41
+ export async function deriveRequestStateKey(raw) {
42
+ const material = await crypto.subtle.importKey("raw", new Uint8Array(raw), "HKDF", false, ["deriveBits"]);
43
+ return new Uint8Array(await crypto.subtle.deriveBits({
44
+ name: "HKDF",
45
+ hash: "SHA-256",
46
+ salt: new TextEncoder().encode("connecta:request-state:v1"),
47
+ info: new TextEncoder().encode("auth-elicitation"),
48
+ }, material, 256));
49
+ }
@@ -0,0 +1,137 @@
1
+ import { type CallErrorDetails } from "./errors.js";
2
+ import type { ConnectorStatus, Logger } from "./types.js";
3
+ /** Where in connecta's work a failure happened. Closed, so it carries no prose. */
4
+ type FailureStep = "MCP handshake" | "tools/list" | "tools/call" | "resources/read" | "OAuth discovery" | "OAuth client registration" | "OAuth token request" | "OAuth flow" | "handler";
5
+ /**
6
+ * Registered OAuth `error` codes: the pinned SDK's `OAuthErrorCode` values
7
+ * (RFC 6749, 6750, 7591, 8707, 9449) and RFC 7591's software-statement pair.
8
+ */
9
+ export declare const OAUTH_ERROR_CODES: ReadonlySet<string>;
10
+ /** Inbound denial facts, never identity text or provider diagnostics. */
11
+ export type AuthDenialReason = "authentication_failed" | "authorization_header_invalid" | "token_type_mismatch" | "oauth_verification_failed" | "oauth_client_not_allowed" | "oauth_binding_mismatch" | "session_origin_mismatch" | "user_missing" | "email_lookup_failed" | "verified_email_invalid" | "email_domain_denied" | "gate_denied" | "gate_failed" | "pool_not_declared" | "pool_grant_denied" | "pool_grant_threw";
12
+ /**
13
+ * Name `ctor`'s instances `label` in records and fixed text. Call once, at
14
+ * module scope, beside the class. An error whose prototype chain reaches no
15
+ * labelled class has no label; its `name` is never consulted.
16
+ */
17
+ export declare function labelErrorClass(ctor: {
18
+ readonly prototype: object;
19
+ }, label: string): void;
20
+ /**
21
+ * The label of the nearest labelled class on `failure`'s prototype chain.
22
+ * A genuine DOMException is told as `AbortError` or `TimeoutError` when its
23
+ * name is exactly one of those, else `DOMException`.
24
+ */
25
+ export declare function errorLabel(failure: unknown): string | undefined;
26
+ /** Facts connecta derived about a failure where it saw it. */
27
+ export interface FailureFacts {
28
+ step?: FailureStep;
29
+ /** Scheme, host, and port of the request that failed. */
30
+ origin?: string;
31
+ /** The HTTP status the downstream answered with. */
32
+ httpStatus?: number;
33
+ /** A registered OAuth `error` code the server named. */
34
+ oauthError?: string;
35
+ }
36
+ /** Note what connecta knows about `failure` for any record made of it later. */
37
+ export declare function attachFailureFacts<T>(failure: T, facts: FailureFacts): T;
38
+ /** The facts and class label of `from`, kept for `to` (its classified form, say). */
39
+ export declare function carryFailureFacts<T>(from: unknown, to: T): T;
40
+ /** What a failure record may be about. Only these fields are read. */
41
+ export interface FailureSubject {
42
+ reason?: AuthDenialReason;
43
+ /** A registered connector's id. */
44
+ connector?: string;
45
+ /**
46
+ * The catalog entry the call resolved to, never a name alone: a tool is
47
+ * named only when connecta found it in that connector's catalog.
48
+ */
49
+ tool?: {
50
+ readonly name: string;
51
+ } | undefined;
52
+ source?: string;
53
+ /** A status state, for a status record. */
54
+ state?: ConnectorStatus["state"] | "failed";
55
+ /** Whether an OAuth start continued or restarted authorization. */
56
+ mode?: "continue" | "restart";
57
+ attempts?: number;
58
+ durationMs?: number;
59
+ }
60
+ declare const recordBrand: unique symbol;
61
+ /** One failure as an operator may read it. Only `failureRecord` makes one. */
62
+ export interface FailureRecord {
63
+ readonly [recordBrand]: true;
64
+ readonly reason?: AuthDenialReason;
65
+ readonly connector?: string;
66
+ readonly tool?: string;
67
+ readonly source?: string;
68
+ readonly state?: string;
69
+ readonly mode?: string;
70
+ readonly attempts?: number;
71
+ readonly durationMs?: number;
72
+ readonly step?: string;
73
+ readonly origin?: string;
74
+ readonly httpStatus?: number;
75
+ readonly oauthError?: string;
76
+ /** Classification code, such as `unavailable` or `connector_call_failed`. */
77
+ readonly code?: string;
78
+ readonly retryable?: boolean;
79
+ /** The error's class label, by identity, such as `TypeError`. */
80
+ readonly errorClass?: string;
81
+ /** A network errno or `timeout`. */
82
+ readonly errno?: string;
83
+ }
84
+ /**
85
+ * How an operator record names a catalog entry: its name when it fits MCP's
86
+ * tool-name grammar, else `<withheld>`. Activity rows and the operator page's
87
+ * catalog use it too, so a withheld name appears nowhere an operator reads.
88
+ */
89
+ export declare function recordedToolName(entry: {
90
+ readonly name: string;
91
+ }): string;
92
+ /** Vouch for `details` as a classification connecta itself computed. */
93
+ export declare function classifiedFailure<T extends CallErrorDetails>(details: T): T;
94
+ /**
95
+ * The record of a failure: the subject's listed fields, each checked; the
96
+ * facts attached to the failure; its class label; and the typed
97
+ * classification it carries (a `ConnectorCallError`'s code, retryability,
98
+ * origin, and errno, or those of a `CallErrorDetails` registered with
99
+ * `classifiedFailure`). Never its
100
+ * text, and nothing else from `subject`.
101
+ */
102
+ export declare function failureRecord(subject: FailureSubject, failure?: unknown): FailureRecord;
103
+ /**
104
+ * The events an operator log reports failures as. Closed: a new one is a
105
+ * reviewed line here, never a string assembled at the call site.
106
+ */
107
+ export type FailureEvent = "negotiation cache read failed" | "negotiation cache write failed" | "connectorAccess grant is unreachable" | "call failed" | "input schema unusable; arguments are not validated" | "catalog read failed" | "catalog persistence failed" | "catalog invalidation failed" | "catalog refresh observation failed" | "catalog refresh failed; serving stale catalog" | "deferred catalog refresh failed" | "deferred catalog refresh could not attach to the runtime" | "catalog drift check failed" | "schema digest check failed; serving digested reviews as writes" | "credential shape read failed" | "credential test failed" | "credential test threw" | "session termination refused or failed; the downstream session may remain until its provider timeout" | "activity record failed" | "catalog drift record failed" | "activity read failed" | "OAuth start failed" | "OAuth disconnect failed" | "OAuth callback verifyState threw; no authorization code was exchanged" | "OAuth callback handoff could not be consumed; no authorization code was exchanged" | "MCP pool request denied" | "OAuth error callback consent could not be consumed; no authorization code was exchanged" | "MCP handler error" | "operator status" | "result paging unavailable" | "request failed" | "Clerk request denied" | "Clerk authentication failed";
108
+ /**
109
+ * Log one failure as `[connecta] <event>` and its record. A record
110
+ * `failureRecord` did not build is replaced, whatever its shape, by a fixed
111
+ * `{ record: "<rejected>" }`. Never throws: it runs on failure paths, and a
112
+ * logger that throws is contained here.
113
+ */
114
+ export declare function logFailure(logger: Pick<Logger, "info" | "warn" | "error">, event: FailureEvent, record: FailureRecord, level?: "info" | "warn" | "error"): void;
115
+ /**
116
+ * A failure in connecta's words, from its record alone:
117
+ * `Connector "<id>" <step> with <origin> failed with HTTP <status> (<class>, <code>, <errno>).`
118
+ * Used for status messages, and for agent-facing text where the error's own
119
+ * may not pass.
120
+ */
121
+ export declare function describeFailure(connectorId: string, failure: unknown): string;
122
+ /** A status whose message connecta wrote, such as an auth_required notice. */
123
+ export declare function ownStatus<T extends ConnectorStatus>(status: T): T;
124
+ /** An error status described from `failure`'s record, remembering the failure. */
125
+ export declare function failureStatus(connectorId: string, failure: unknown): ConnectorStatus;
126
+ /**
127
+ * `status` rebuilt from what connecta approved. A status connecta wrote
128
+ * (`ownStatus`, `failureStatus`) is rebuilt from the snapshot taken then,
129
+ * whatever has since been assigned to it. Any other status contributes its
130
+ * typed state alone: a plugin `status()` seam returns its author's text,
131
+ * which can quote a downstream, and connecta cannot vouch for it whatever it
132
+ * looks like.
133
+ */
134
+ export declare function boundedStatus(status: ConnectorStatus): ConnectorStatus;
135
+ /** The failure connecta saw behind a status it described, if any. */
136
+ export declare function statusFailure(status: ConnectorStatus): unknown;
137
+ export {};