@zackbart/connecta 0.15.1 → 0.16.1

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 (335) hide show
  1. package/AGENTS.md +40 -20
  2. package/CHANGELOG.md +653 -0
  3. package/README.md +25 -5
  4. package/bin/connecta.mjs +44 -3
  5. package/dist/access-tokens.d.ts +0 -1
  6. package/dist/access-tokens.js +0 -1
  7. package/dist/activity.d.ts +50 -2
  8. package/dist/activity.js +34 -1
  9. package/dist/apps-shell.d.ts +0 -1
  10. package/dist/apps-shell.js +0 -1
  11. package/dist/auth/bearer.d.ts +0 -1
  12. package/dist/auth/bearer.js +0 -1
  13. package/dist/auth/clerk.d.ts +0 -1
  14. package/dist/auth/clerk.js +45 -9
  15. package/dist/auth/downstream-oauth.d.ts +0 -1
  16. package/dist/auth/downstream-oauth.js +0 -1
  17. package/dist/call-admission.d.ts +0 -1
  18. package/dist/call-admission.js +0 -1
  19. package/dist/catalog-drift.d.ts +99 -0
  20. package/dist/catalog-drift.js +251 -0
  21. package/dist/catalog-fingerprint.d.ts +0 -1
  22. package/dist/catalog-fingerprint.js +0 -1
  23. package/dist/catalog-limits.d.ts +0 -1
  24. package/dist/catalog-limits.js +0 -1
  25. package/dist/catalog-service.d.ts +0 -1
  26. package/dist/catalog-service.js +41 -5
  27. package/dist/catalog.d.ts +11 -1
  28. package/dist/catalog.js +20 -1
  29. package/dist/concurrency.d.ts +0 -1
  30. package/dist/concurrency.js +0 -1
  31. package/dist/connector-scope.d.ts +0 -1
  32. package/dist/connector-scope.js +0 -1
  33. package/dist/connectors/api.d.ts +29 -21
  34. package/dist/connectors/api.js +45 -19
  35. package/dist/connectors/guarded-fetch.d.ts +106 -0
  36. package/dist/connectors/guarded-fetch.js +289 -0
  37. package/dist/connectors/remote-mcp.d.ts +0 -1
  38. package/dist/connectors/remote-mcp.js +0 -1
  39. package/dist/credentials.d.ts +0 -1
  40. package/dist/credentials.js +0 -1
  41. package/dist/errors.d.ts +30 -3
  42. package/dist/errors.js +1 -1
  43. package/dist/execute.d.ts +0 -1
  44. package/dist/execute.js +0 -1
  45. package/dist/executor-admission.d.ts +11 -1
  46. package/dist/executor-admission.js +28 -1
  47. package/dist/executor-result.d.ts +0 -1
  48. package/dist/executor-result.js +0 -1
  49. package/dist/executors/quickjs-child.d.ts +0 -1
  50. package/dist/executors/quickjs-child.js +0 -1
  51. package/dist/executors/quickjs-protocol.d.ts +0 -1
  52. package/dist/executors/quickjs-protocol.js +0 -1
  53. package/dist/executors/quickjs-runtime.d.ts +0 -1
  54. package/dist/executors/quickjs-runtime.js +0 -1
  55. package/dist/executors/quickjs.d.ts +0 -1
  56. package/dist/executors/quickjs.js +2 -1
  57. package/dist/favicon.d.ts +0 -1
  58. package/dist/favicon.js +0 -1
  59. package/dist/index.d.ts +2 -3
  60. package/dist/index.js +24 -7
  61. package/dist/invocation.d.ts +0 -1
  62. package/dist/invocation.js +0 -1
  63. package/dist/json-schema.d.ts +0 -1
  64. package/dist/json-schema.js +0 -1
  65. package/dist/mcp-result.d.ts +0 -1
  66. package/dist/mcp-result.js +0 -1
  67. package/dist/meta-tools.d.ts +0 -1
  68. package/dist/meta-tools.js +0 -1
  69. package/dist/node.d.ts +0 -1
  70. package/dist/node.js +0 -1
  71. package/dist/operator-ui/generated.d.ts +2 -3
  72. package/dist/operator-ui/generated.js +3 -4
  73. package/dist/operator-ui/model.d.ts +10 -1
  74. package/dist/operator-ui/model.js +0 -1
  75. package/dist/operator-ui/view.d.ts +167 -0
  76. package/dist/operator-ui/view.js +270 -0
  77. package/dist/providers/cloudflare.d.ts +0 -1
  78. package/dist/providers/cloudflare.js +261 -299
  79. package/dist/providers/linear.d.ts +26 -5
  80. package/dist/providers/linear.js +32 -61
  81. package/dist/providers/mixpanel.d.ts +35 -4
  82. package/dist/providers/mixpanel.js +53 -79
  83. package/dist/providers/notion.d.ts +0 -1
  84. package/dist/providers/notion.js +87 -42
  85. package/dist/providers/stripe.d.ts +13 -1
  86. package/dist/providers/stripe.js +31 -60
  87. package/dist/registry.d.ts +34 -2
  88. package/dist/registry.js +85 -5
  89. package/dist/retired-toolkits.d.ts +0 -1
  90. package/dist/retired-toolkits.js +0 -1
  91. package/dist/routes/access-tokens.d.ts +0 -1
  92. package/dist/routes/access-tokens.js +0 -1
  93. package/dist/routes/activity.d.ts +0 -1
  94. package/dist/routes/activity.js +0 -1
  95. package/dist/routes/credentials.d.ts +0 -1
  96. package/dist/routes/credentials.js +0 -1
  97. package/dist/routes/mcp.d.ts +0 -1
  98. package/dist/routes/mcp.js +0 -1
  99. package/dist/routes/oauth.d.ts +0 -1
  100. package/dist/routes/oauth.js +0 -1
  101. package/dist/routes/shared.d.ts +2 -1
  102. package/dist/routes/shared.js +0 -1
  103. package/dist/routes/ui.d.ts +0 -1
  104. package/dist/routes/ui.js +0 -1
  105. package/dist/server.d.ts +0 -1
  106. package/dist/server.js +11 -1
  107. package/dist/skills.d.ts +0 -1
  108. package/dist/skills.js +0 -1
  109. package/dist/storage/file.d.ts +0 -1
  110. package/dist/storage/file.js +0 -1
  111. package/dist/storage/memory.d.ts +0 -1
  112. package/dist/storage/memory.js +0 -1
  113. package/dist/timeout.d.ts +0 -1
  114. package/dist/timeout.js +0 -1
  115. package/dist/tool-safety.d.ts +0 -1
  116. package/dist/tool-safety.js +0 -1
  117. package/dist/types.d.ts +45 -1
  118. package/dist/types.js +0 -1
  119. package/dist/ui.d.ts +0 -1
  120. package/dist/ui.js +13 -137
  121. package/dist/validate.d.ts +13 -15
  122. package/dist/validate.js +24 -11
  123. package/dist/version.d.ts +1 -2
  124. package/dist/version.js +1 -2
  125. package/documentation/architecture.md +186 -5
  126. package/documentation/auth.md +11 -0
  127. package/documentation/call-admission.md +142 -6
  128. package/documentation/cloudflare.md +92 -10
  129. package/documentation/code-first-exploration.md +2 -2
  130. package/documentation/code-mode.md +14 -14
  131. package/documentation/connector-guides.md +172 -5
  132. package/documentation/connectors.md +89 -0
  133. package/documentation/linear.md +20 -2
  134. package/documentation/mcp-ui-design.md +2 -2
  135. package/documentation/meta-tools.md +7 -1
  136. package/documentation/mixpanel.md +42 -9
  137. package/documentation/notion.md +23 -2
  138. package/documentation/operations.md +325 -5
  139. package/documentation/operator-ui.md +120 -5
  140. package/documentation/provider-audit.md +160 -0
  141. package/documentation/provider-conventions.md +667 -0
  142. package/documentation/request-admission.md +167 -5
  143. package/documentation/rich-output-design.md +1 -2
  144. package/documentation/stripe.md +15 -0
  145. package/documentation/upgrading.md +454 -0
  146. package/ethos.md +14 -9
  147. package/examples/worker/README.md +102 -12
  148. package/examples/worker/src/index.ts +46 -1
  149. package/examples/worker/wrangler.jsonc +14 -1
  150. package/package.json +10 -6
  151. package/templates/node/.dockerignore +10 -0
  152. package/templates/node/.env.example +40 -3
  153. package/templates/node/AGENTS.md +16 -1
  154. package/templates/node/Dockerfile +49 -0
  155. package/templates/node/README.md +117 -2
  156. package/templates/node/docker-compose.yml +39 -0
  157. package/templates/node/package.json +1 -1
  158. package/templates/node/src/file-activity.ts +182 -0
  159. package/templates/node/src/index.ts +80 -4
  160. package/assets/connecta-clay-hero.png +0 -0
  161. package/dist/access-tokens.d.ts.map +0 -1
  162. package/dist/access-tokens.js.map +0 -1
  163. package/dist/activity.d.ts.map +0 -1
  164. package/dist/activity.js.map +0 -1
  165. package/dist/apps-shell.d.ts.map +0 -1
  166. package/dist/apps-shell.js.map +0 -1
  167. package/dist/auth/bearer.d.ts.map +0 -1
  168. package/dist/auth/bearer.js.map +0 -1
  169. package/dist/auth/clerk.d.ts.map +0 -1
  170. package/dist/auth/clerk.js.map +0 -1
  171. package/dist/auth/downstream-oauth.d.ts.map +0 -1
  172. package/dist/auth/downstream-oauth.js.map +0 -1
  173. package/dist/call-admission.d.ts.map +0 -1
  174. package/dist/call-admission.js.map +0 -1
  175. package/dist/catalog-fingerprint.d.ts.map +0 -1
  176. package/dist/catalog-fingerprint.js.map +0 -1
  177. package/dist/catalog-limits.d.ts.map +0 -1
  178. package/dist/catalog-limits.js.map +0 -1
  179. package/dist/catalog-service.d.ts.map +0 -1
  180. package/dist/catalog-service.js.map +0 -1
  181. package/dist/catalog.d.ts.map +0 -1
  182. package/dist/catalog.js.map +0 -1
  183. package/dist/concurrency.d.ts.map +0 -1
  184. package/dist/concurrency.js.map +0 -1
  185. package/dist/connector-scope.d.ts.map +0 -1
  186. package/dist/connector-scope.js.map +0 -1
  187. package/dist/connectors/api.d.ts.map +0 -1
  188. package/dist/connectors/api.js.map +0 -1
  189. package/dist/connectors/remote-mcp.d.ts.map +0 -1
  190. package/dist/connectors/remote-mcp.js.map +0 -1
  191. package/dist/credentials.d.ts.map +0 -1
  192. package/dist/credentials.js.map +0 -1
  193. package/dist/errors.d.ts.map +0 -1
  194. package/dist/errors.js.map +0 -1
  195. package/dist/execute.d.ts.map +0 -1
  196. package/dist/execute.js.map +0 -1
  197. package/dist/executor-admission.d.ts.map +0 -1
  198. package/dist/executor-admission.js.map +0 -1
  199. package/dist/executor-result.d.ts.map +0 -1
  200. package/dist/executor-result.js.map +0 -1
  201. package/dist/executors/quickjs-child.d.ts.map +0 -1
  202. package/dist/executors/quickjs-child.js.map +0 -1
  203. package/dist/executors/quickjs-protocol.d.ts.map +0 -1
  204. package/dist/executors/quickjs-protocol.js.map +0 -1
  205. package/dist/executors/quickjs-runtime.d.ts.map +0 -1
  206. package/dist/executors/quickjs-runtime.js.map +0 -1
  207. package/dist/executors/quickjs.d.ts.map +0 -1
  208. package/dist/executors/quickjs.js.map +0 -1
  209. package/dist/favicon.d.ts.map +0 -1
  210. package/dist/favicon.js.map +0 -1
  211. package/dist/index.d.ts.map +0 -1
  212. package/dist/index.js.map +0 -1
  213. package/dist/invocation.d.ts.map +0 -1
  214. package/dist/invocation.js.map +0 -1
  215. package/dist/json-schema.d.ts.map +0 -1
  216. package/dist/json-schema.js.map +0 -1
  217. package/dist/mcp-result.d.ts.map +0 -1
  218. package/dist/mcp-result.js.map +0 -1
  219. package/dist/meta-tools.d.ts.map +0 -1
  220. package/dist/meta-tools.js.map +0 -1
  221. package/dist/node.d.ts.map +0 -1
  222. package/dist/node.js.map +0 -1
  223. package/dist/operator-ui/generated.d.ts.map +0 -1
  224. package/dist/operator-ui/generated.js.map +0 -1
  225. package/dist/operator-ui/model.d.ts.map +0 -1
  226. package/dist/operator-ui/model.js.map +0 -1
  227. package/dist/providers/cloudflare.d.ts.map +0 -1
  228. package/dist/providers/cloudflare.js.map +0 -1
  229. package/dist/providers/linear.d.ts.map +0 -1
  230. package/dist/providers/linear.js.map +0 -1
  231. package/dist/providers/mixpanel.d.ts.map +0 -1
  232. package/dist/providers/mixpanel.js.map +0 -1
  233. package/dist/providers/notion.d.ts.map +0 -1
  234. package/dist/providers/notion.js.map +0 -1
  235. package/dist/providers/stripe.d.ts.map +0 -1
  236. package/dist/providers/stripe.js.map +0 -1
  237. package/dist/registry.d.ts.map +0 -1
  238. package/dist/registry.js.map +0 -1
  239. package/dist/retired-toolkits.d.ts.map +0 -1
  240. package/dist/retired-toolkits.js.map +0 -1
  241. package/dist/routes/access-tokens.d.ts.map +0 -1
  242. package/dist/routes/access-tokens.js.map +0 -1
  243. package/dist/routes/activity.d.ts.map +0 -1
  244. package/dist/routes/activity.js.map +0 -1
  245. package/dist/routes/credentials.d.ts.map +0 -1
  246. package/dist/routes/credentials.js.map +0 -1
  247. package/dist/routes/mcp.d.ts.map +0 -1
  248. package/dist/routes/mcp.js.map +0 -1
  249. package/dist/routes/oauth.d.ts.map +0 -1
  250. package/dist/routes/oauth.js.map +0 -1
  251. package/dist/routes/shared.d.ts.map +0 -1
  252. package/dist/routes/shared.js.map +0 -1
  253. package/dist/routes/ui.d.ts.map +0 -1
  254. package/dist/routes/ui.js.map +0 -1
  255. package/dist/server.d.ts.map +0 -1
  256. package/dist/server.js.map +0 -1
  257. package/dist/skills.d.ts.map +0 -1
  258. package/dist/skills.js.map +0 -1
  259. package/dist/storage/file.d.ts.map +0 -1
  260. package/dist/storage/file.js.map +0 -1
  261. package/dist/storage/memory.d.ts.map +0 -1
  262. package/dist/storage/memory.js.map +0 -1
  263. package/dist/timeout.d.ts.map +0 -1
  264. package/dist/timeout.js.map +0 -1
  265. package/dist/tool-safety.d.ts.map +0 -1
  266. package/dist/tool-safety.js.map +0 -1
  267. package/dist/types.d.ts.map +0 -1
  268. package/dist/types.js.map +0 -1
  269. package/dist/ui.d.ts.map +0 -1
  270. package/dist/ui.js.map +0 -1
  271. package/dist/validate.d.ts.map +0 -1
  272. package/dist/validate.js.map +0 -1
  273. package/dist/version.d.ts.map +0 -1
  274. package/dist/version.js.map +0 -1
  275. package/examples/node/README.md +0 -52
  276. package/examples/node/src/index.ts +0 -71
  277. package/src/access-tokens.ts +0 -289
  278. package/src/activity.ts +0 -234
  279. package/src/apps-shell.ts +0 -370
  280. package/src/auth/bearer.ts +0 -61
  281. package/src/auth/clerk.ts +0 -553
  282. package/src/auth/downstream-oauth.ts +0 -575
  283. package/src/call-admission.ts +0 -521
  284. package/src/catalog-fingerprint.ts +0 -52
  285. package/src/catalog-limits.ts +0 -11
  286. package/src/catalog-service.ts +0 -1153
  287. package/src/catalog.ts +0 -790
  288. package/src/concurrency.ts +0 -39
  289. package/src/connector-scope.ts +0 -66
  290. package/src/connectors/api.ts +0 -159
  291. package/src/connectors/remote-mcp.ts +0 -1090
  292. package/src/credentials.ts +0 -428
  293. package/src/errors.ts +0 -364
  294. package/src/execute.ts +0 -1406
  295. package/src/executor-admission.ts +0 -328
  296. package/src/executor-result.ts +0 -107
  297. package/src/executors/quickjs-child.ts +0 -172
  298. package/src/executors/quickjs-protocol.ts +0 -99
  299. package/src/executors/quickjs-runtime.ts +0 -470
  300. package/src/executors/quickjs.ts +0 -776
  301. package/src/favicon.ts +0 -53
  302. package/src/index.ts +0 -666
  303. package/src/invocation.ts +0 -655
  304. package/src/json-schema.ts +0 -11
  305. package/src/mcp-result.ts +0 -43
  306. package/src/meta-tools.ts +0 -1607
  307. package/src/node.ts +0 -206
  308. package/src/operator-ui/browser.css +0 -423
  309. package/src/operator-ui/browser.ts +0 -1281
  310. package/src/operator-ui/generated.ts +0 -4
  311. package/src/operator-ui/model.ts +0 -111
  312. package/src/providers/cloudflare.ts +0 -3999
  313. package/src/providers/linear.ts +0 -302
  314. package/src/providers/mixpanel.ts +0 -229
  315. package/src/providers/notion.ts +0 -1879
  316. package/src/providers/stripe.ts +0 -307
  317. package/src/registry.ts +0 -1085
  318. package/src/retired-toolkits.ts +0 -14
  319. package/src/routes/access-tokens.ts +0 -115
  320. package/src/routes/activity.ts +0 -215
  321. package/src/routes/credentials.ts +0 -254
  322. package/src/routes/mcp.ts +0 -429
  323. package/src/routes/oauth.ts +0 -322
  324. package/src/routes/shared.ts +0 -247
  325. package/src/routes/ui.ts +0 -152
  326. package/src/server.ts +0 -195
  327. package/src/skills.ts +0 -271
  328. package/src/storage/file.ts +0 -123
  329. package/src/storage/memory.ts +0 -44
  330. package/src/timeout.ts +0 -84
  331. package/src/tool-safety.ts +0 -15
  332. package/src/types.ts +0 -523
  333. package/src/ui.ts +0 -744
  334. package/src/validate.ts +0 -387
  335. package/src/version.ts +0 -7
@@ -1,322 +0,0 @@
1
- import { oauthValueStorageKey } from "../auth/downstream-oauth.js";
2
- import { closeConnectorScope } from "../connector-scope.js";
3
- import type {
4
- ConnectorContext,
5
- ConnectorStatus,
6
- ConnectaBranding,
7
- } from "../types.js";
8
- import { isSafeHttpUrl, resolveBranding } from "../ui.js";
9
- import {
10
- authorizeUiAdmin,
11
- isSameOrigin,
12
- loggableValue,
13
- msg,
14
- privateJson,
15
- type RouteContext,
16
- } from "./shared.js";
17
-
18
- async function handleOAuthManagementRequest(
19
- context: RouteContext,
20
- connectorId: string,
21
- ): Promise<Response> {
22
- const { request, baseUrl, opts, defer } = context;
23
- if (!isSameOrigin(request, baseUrl)) {
24
- return privateJson(
25
- { error: "same-origin request required" },
26
- { status: 403 },
27
- );
28
- }
29
- const admin = await authorizeUiAdmin(
30
- request,
31
- baseUrl,
32
- opts.auth,
33
- "OAuth management",
34
- );
35
- if (!admin.ok) return admin.response;
36
-
37
- const connector = opts.registry.getConnector(connectorId);
38
- if (!connector?.disconnectAuth || !connector.startAuth) {
39
- return privateJson(
40
- { error: "unknown OAuth connector" },
41
- { status: 404 },
42
- );
43
- }
44
- if (request.method !== "DELETE" && request.method !== "POST") {
45
- return privateJson({ error: "method not allowed" }, { status: 405 });
46
- }
47
-
48
- const requestScope = {};
49
- const ctx = opts.registry.contextFor(connectorId, baseUrl, requestScope);
50
- try {
51
- let result: ConnectorStatus | undefined;
52
- let operationError: unknown;
53
- try {
54
- if (request.method === "DELETE") {
55
- await connector.disconnectAuth(ctx);
56
- } else {
57
- result = await connector.startAuth(ctx, { force: true });
58
- }
59
- } catch (error) {
60
- operationError = error;
61
- }
62
-
63
- // The old grant and its cached catalog are invalid after either operation,
64
- // including a partially failed physical cleanup whose epoch fence succeeded.
65
- try {
66
- await opts.registry.invalidateStored(connectorId);
67
- } catch (error) {
68
- operationError ??= error;
69
- }
70
- if (operationError) {
71
- return privateJson({ error: msg(operationError) }, { status: 400 });
72
- }
73
- if (request.method === "DELETE") {
74
- return new Response(null, {
75
- status: 204,
76
- headers: {
77
- "Cache-Control": "no-store",
78
- "Referrer-Policy": "no-referrer",
79
- },
80
- });
81
- }
82
-
83
- const authorizationUrl = isSafeHttpUrl(result!.authorizationUrl)
84
- ? result!.authorizationUrl
85
- : undefined;
86
- if (result!.state === "error") {
87
- return privateJson(
88
- { error: result!.message || "OAuth authorization could not start" },
89
- { status: 502 },
90
- );
91
- }
92
- if (result!.state === "auth_required" && !authorizationUrl) {
93
- return privateJson(
94
- {
95
- error:
96
- result!.message ||
97
- "OAuth authorization requires consent but no safe URL is available",
98
- },
99
- { status: 502 },
100
- );
101
- }
102
- return privateJson({
103
- state: result!.state,
104
- ...(result!.message ? { message: result!.message } : {}),
105
- ...(authorizationUrl ? { authorizationUrl } : {}),
106
- });
107
- } finally {
108
- await closeConnectorScope(connector, ctx, defer);
109
- }
110
- }
111
-
112
- export async function routeOAuthManagement(
113
- context: RouteContext,
114
- ): Promise<Response | null> {
115
- const match = /^\/ui\/oauth\/([a-z0-9_-]+)$/.exec(context.path);
116
- if (!match) return null;
117
- const connectorId = match[1];
118
- if (!connectorId) return null;
119
- if (context.request.method === "OPTIONS") {
120
- return privateJson({ error: "method not allowed" }, { status: 405 });
121
- }
122
- return handleOAuthManagementRequest(context, connectorId);
123
- }
124
-
125
- function escapeHtml(value: string): string {
126
- return value
127
- .replaceAll("&", "&amp;")
128
- .replaceAll("<", "&lt;")
129
- .replaceAll(">", "&gt;")
130
- .replaceAll('"', "&quot;");
131
- }
132
-
133
- /** `body` is escaped — callback params and error messages are attacker-influenced. */
134
- function html(
135
- body: string,
136
- status = 200,
137
- branding?: ConnectaBranding,
138
- ): Response {
139
- const brand = resolveBranding(branding);
140
- const title = brand.pageTitle;
141
- const owner = brand.ownerName
142
- ? brand.ownerUrl
143
- ? `<a class="brand" href="${escapeHtml(brand.ownerUrl)}">${escapeHtml(brand.ownerName)}</a>`
144
- : `<span class="brand">${escapeHtml(brand.ownerName)}</span>`
145
- : brand.productUrl
146
- ? `<a class="brand" href="${escapeHtml(brand.productUrl)}">${escapeHtml(brand.productName)}</a>`
147
- : `<span class="brand">${escapeHtml(brand.productName)}</span>`;
148
- const product = brand.ownerName
149
- ? brand.productUrl
150
- ? `<a class="product" href="${escapeHtml(brand.productUrl)}">${escapeHtml(brand.productName)}</a>`
151
- : `<span class="product">${escapeHtml(brand.productName)}</span>`
152
- : "";
153
- return new Response(
154
- `<!doctype html>
155
- <html lang="en">
156
- <head>
157
- <meta charset="utf-8">
158
- <meta name="viewport" content="width=device-width,initial-scale=1">
159
- <meta name="theme-color" content="${escapeHtml(brand.themeColor)}">
160
- <link rel="icon" href="${escapeHtml(brand.faviconHref)}" type="image/svg+xml">
161
- <link rel="shortcut icon" href="/favicon.ico">
162
- <title>${escapeHtml(title)}</title>
163
- <style>
164
- * { border-radius: 0; box-sizing: border-box; }
165
- html { color: #000; background: #fff; font: 16px/1.5 "Helvetica Neue",
166
- Helvetica, Arial, sans-serif; -webkit-font-smoothing: antialiased; }
167
- body { margin: 0; min-height: 100vh; }
168
- ::selection { color: #fff; background: #000; }
169
- .shell { margin: 0 auto; max-width: 70rem; padding: 1rem; }
170
- .grid { display: grid; gap: 1rem 1.5rem;
171
- grid-template-columns: repeat(3, minmax(0, 1fr)); }
172
- .brand { font-weight: 500; grid-column: 1; text-decoration: none; }
173
- .product { grid-column: 2 / -1; }
174
- main { margin-top: 5rem; }
175
- h1, p { font: inherit; margin: 0; }
176
- h1 { grid-column: 1; }
177
- .copy { grid-column: 2 / -1; max-width: 34em; }
178
- .copy > * + * { margin-top: 1.5rem; }
179
- a { color: inherit; text-decoration: underline; text-decoration-thickness: 1.5px;
180
- text-underline-offset: .22em; }
181
- a:hover { text-decoration-color: transparent; }
182
- a:focus-visible { outline: 1px solid #000; outline-offset: 2px; }
183
- @media (max-width: 36.99rem) {
184
- .grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
185
- .product { grid-column: 2; }
186
- main { margin-top: 3rem; }
187
- h1, .copy { grid-column: 1 / -1; }
188
- }
189
- </style>
190
- </head>
191
- <body>
192
- <header class="shell grid">
193
- ${owner}
194
- ${product}
195
- </header>
196
- <main class="shell grid">
197
- <h1>Connection status</h1>
198
- <div class="copy">
199
- <p>${escapeHtml(body)}</p>
200
- <p><a href="/">Return to ${escapeHtml(brand.productName)}</a></p>
201
- </div>
202
- </main>
203
- </body>
204
- </html>`,
205
- { status, headers: { "Content-Type": "text/html; charset=utf-8" } },
206
- );
207
- }
208
-
209
- /**
210
- * Pay the storage read a real downstream-OAuth refusal pays, on the refusal
211
- * paths that would otherwise pay nothing.
212
- *
213
- * Identical bodies do not hide a connector id if the clock still sorts them.
214
- * `KvOAuthProvider.verifyState` reads `oauth:state` and its generation before
215
- * it can reject a mismatched value, so a configured id costs two storage round
216
- * trips on the ordinary path while an id naming nothing used to touch no I/O.
217
- * That gap is an oracle: sample the two and a wordlist recovers the connector
218
- * list the flat 400 was meant to withhold. So zero-I/O refusals read the same
219
- * keys in the same `conn:<id>:` namespace, where an unconfigured id gets misses.
220
- *
221
- * This is deliberately *not* a constant-time claim, and documentation/connectors.md says
222
- * so in prose: a hit and a miss are not identical in a KV store, and a connector
223
- * shipping its own `verifyState` may do more or less work. What it
224
- * removes is the order-of-magnitude "no I/O versus a round trip" difference,
225
- * which is the only part of the signal that makes enumeration cheap.
226
- *
227
- * A throwing read is swallowed: the refusal is the answer either way, and
228
- * turning it into a 500 would hand back exactly the distinguishable response
229
- * this whole path exists to deny.
230
- */
231
- async function equalizeRefusalCost(
232
- context: ConnectorContext,
233
- ): Promise<void> {
234
- try {
235
- const generation = await context.storage.get("oauth:generation");
236
- await context.storage.get(
237
- oauthValueStorageKey("oauth:state", generation),
238
- );
239
- } catch {
240
- // Deliberately ignored — see above.
241
- }
242
- }
243
-
244
- export async function routeOAuthCallback(
245
- context: RouteContext,
246
- ): Promise<Response | null> {
247
- const { path, url, baseUrl, opts } = context;
248
- if (!path.startsWith("/oauth/callback/")) return null;
249
- const error = url.searchParams.get("error");
250
- if (error) return html(`Authorization denied: ${error}`, 400, opts.branding);
251
- const code = url.searchParams.get("code");
252
- if (!code) return html("Missing authorization code.", 400, opts.branding);
253
- const id = path.slice("/oauth/callback/".length);
254
- const connector = opts.registry.getConnector(id);
255
- // Safe to build before we know the id names anything: `contextFor` is a pure
256
- // constructor — a namespaced storage view over `conn:<id>:` and, only for a
257
- // connector that declares one, a lazy credential accessor. It neither throws
258
- // nor touches storage for an unknown id, which is what lets the refusals
259
- // below borrow it to equalize their cost.
260
- const connectorContext = opts.registry.contextFor(id, baseUrl);
261
- const refused = () =>
262
- html(
263
- "Authorization could not be completed. Re-run authorization from " +
264
- "connecta and try again.",
265
- 400,
266
- opts.branding,
267
- );
268
- if (!connector || !connector.finishAuth) {
269
- await equalizeRefusalCost(connectorContext);
270
- return refused();
271
- }
272
- // CSRF / login-fixation guard: this route is intentionally public, so verify
273
- // the `state` matches the flow connecta started BEFORE exchanging the code.
274
- if (!connector.verifyState) {
275
- await equalizeRefusalCost(connectorContext);
276
- opts.logger.warn(
277
- `[connecta] refused an OAuth callback for connector ` +
278
- `${loggableValue(id)} with 400: it implements finishAuth but no ` +
279
- "verifyState, so connecta cannot establish that it started this flow. " +
280
- "No authorization code was exchanged. Implement verifyState before " +
281
- "trying again.",
282
- );
283
- return refused();
284
- }
285
- const state = url.searchParams.get("state");
286
- let stateMatches: boolean;
287
- try {
288
- stateMatches = await connector.verifyState(state, connectorContext);
289
- } catch (err) {
290
- opts.logger.warn(
291
- `[connecta] refused an OAuth callback for connector ` +
292
- `${loggableValue(id)} with 400: verifyState threw ` +
293
- `${loggableValue(msg(err))}. No authorization code was exchanged. ` +
294
- "Re-run authorization from connecta and check the verifier if it " +
295
- "fails again.",
296
- );
297
- return refused();
298
- }
299
- if (!stateMatches) {
300
- opts.logger.warn(
301
- `[connecta] refused an OAuth callback for connector ` +
302
- `${loggableValue(id)} with 400: ` +
303
- (state === null
304
- ? "the state parameter was missing"
305
- : "the state did not match the pending authorization flow") +
306
- ". No authorization code was exchanged. Re-run authorization from " +
307
- "connecta and try again.",
308
- );
309
- return refused();
310
- }
311
- try {
312
- await connector.finishAuth(code, connectorContext, url.searchParams);
313
- await opts.registry.invalidateStored(id);
314
- return html(
315
- `Connected "${id}". You can close this window.`,
316
- 200,
317
- opts.branding,
318
- );
319
- } catch (err) {
320
- return html(`Authorization failed: ${msg(err)}`, 500, opts.branding);
321
- }
322
- }
@@ -1,247 +0,0 @@
1
- import type { Implementation } from "@modelcontextprotocol/server";
2
- import type { AccessTokenManager } from "../access-tokens.js";
3
- import type { ActivityActor, ActivityReadGate, ActivityStore } from "../activity.js";
4
- import type { CredentialVault } from "../credentials.js";
5
- import type { DeferredWork } from "../connector-scope.js";
6
- import type { AdmissionController } from "../executor-admission.js";
7
- import type { Registry } from "../registry.js";
8
- import type {
9
- ConnectaBranding,
10
- Executor,
11
- InboundAuth,
12
- Logger,
13
- } from "../types.js";
14
- import { operatorPageForPath } from "../ui.js";
15
-
16
- export interface ServerOptions {
17
- registry: Registry;
18
- auth: InboundAuth[];
19
- publicUrl?: string;
20
- // The SDK's Implementation shape: name/version plus optional title,
21
- // websiteUrl, and icons (MCP icons spec) that clients may render.
22
- serverInfo: Implementation;
23
- logger: Logger;
24
- activity?: ActivityStore;
25
- activityReadGate?: ActivityReadGate;
26
- activityDeploymentId?: string;
27
- deploymentInfo?: Record<string, unknown>;
28
- /** Deadline for call_tool/call_destructive_tool calls that pass no timeoutMs. Off when unset. */
29
- defaultToolTimeoutMs?: number;
30
- /** Per-connector deadline for the search/describe probe fan-out. Default 30_000. */
31
- probeTimeoutMs?: number;
32
- /** Maximum simultaneous connector discovery operations. Default 4. */
33
- discoveryConcurrency?: number;
34
- /** Aggregate serialized-byte budget for connecta.emit per run. Default 4_000_000. */
35
- maxEmittedBytes?: number;
36
- /** Block-count budget for connecta.emit per run. Default 32. */
37
- maxEmittedBlocks?: number;
38
- /** Required sandbox backing the execute_code meta-tool. */
39
- executor: Executor;
40
- /** Global FIFO boundary for all non-preflight `/mcp` requests. */
41
- requestAdmission: AdmissionController;
42
- /** Encrypted connector-credential storage backing the Credentials page. */
43
- credentialVault?: CredentialVault;
44
- /** Hashed deployment access tokens backing MCP admission and the Tokens page. */
45
- accessTokens?: AccessTokenManager;
46
- /** Optional browser UI and OAuth result-page labels. */
47
- branding?: ConnectaBranding;
48
- }
49
-
50
- export interface RuntimeExecutionContext {
51
- waitUntil(promise: Promise<unknown>): void;
52
- }
53
-
54
- export interface RouteContext {
55
- request: Request;
56
- url: URL;
57
- path: string;
58
- baseUrl: string;
59
- opts: ServerOptions;
60
- defer: DeferredWork | undefined;
61
- runtimeContext: RuntimeExecutionContext | undefined;
62
- }
63
-
64
- export function msg(err: unknown): string {
65
- return err instanceof Error ? err.message : String(err);
66
- }
67
-
68
- export function privateJson(
69
- body: unknown,
70
- init: ResponseInit = {},
71
- ): Response {
72
- const headers = new Headers(init.headers);
73
- headers.set("Content-Type", "application/json");
74
- headers.set("Cache-Control", "no-store");
75
- headers.set("Referrer-Policy", "no-referrer");
76
- return new Response(JSON.stringify(body), { ...init, headers });
77
- }
78
-
79
- const MAX_LOGGED_VALUE_LENGTH = 64;
80
-
81
- /**
82
- * Bounded, escaped form of a caller-influenced value (an identity id or OAuth
83
- * callback connector id) for the operator log. Goes
84
- * through JSON.stringify so a caller-controlled newline or control character
85
- * cannot forge a log line, plus a hand-rolled escape for U+2028/U+2029, which
86
- * JSON.stringify leaves raw even though a log reader treats them as line
87
- * terminators. Truncated to a small shared cap so an oversized value cannot
88
- * flood the log either.
89
- */
90
- export function loggableValue(requested: string): string {
91
- const bounded = requested.slice(0, MAX_LOGGED_VALUE_LENGTH);
92
- const escaped = JSON.stringify(bounded).replace(
93
- /[\u2028\u2029]/g,
94
- (ch) => `\\u${ch.charCodeAt(0).toString(16)}`,
95
- );
96
- return escaped + (bounded.length < requested.length ? " (truncated)" : "");
97
- }
98
-
99
- const ACTIVITY_ACTOR_NAMESPACE_RE = /^[\x21-\x7e]{1,256}$/;
100
-
101
- export function activityActorNamespace(
102
- provider: InboundAuth,
103
- ): string | undefined {
104
- return typeof provider.activityActorNamespace === "string" &&
105
- ACTIVITY_ACTOR_NAMESPACE_RE.test(provider.activityActorNamespace)
106
- ? provider.activityActorNamespace
107
- : undefined;
108
- }
109
-
110
- export async function authorize(
111
- request: Request,
112
- baseUrl: string,
113
- auth: InboundAuth[],
114
- ): Promise<
115
- | {
116
- ok: true;
117
- actor: ActivityActor;
118
- /** True only when the admitting provider can also authorize UI mutation. */
119
- uiAdminEligible?: boolean;
120
- }
121
- | { ok: false; response: Response }
122
- > {
123
- if (auth.length === 0) {
124
- return { ok: true, actor: { kind: "anonymous" } };
125
- }
126
- let lastResponse: Response | null = null;
127
- for (const provider of auth) {
128
- const result = await provider.authorize(request, baseUrl);
129
- if (result.ok) {
130
- const subjectId = result.subjectId ?? result.userId;
131
- const actorNamespace = activityActorNamespace(provider);
132
- return {
133
- ok: true,
134
- actor: {
135
- kind: provider.kind,
136
- ...(subjectId ? { id: subjectId } : {}),
137
- ...(subjectId && actorNamespace
138
- ? { namespace: actorNamespace }
139
- : {}),
140
- },
141
- ...(result.userId && provider.uiAuth?.kind === "clerk"
142
- ? { uiAdminEligible: true }
143
- : {}),
144
- };
145
- }
146
- lastResponse = result.response;
147
- }
148
- return {
149
- ok: false,
150
- response:
151
- lastResponse ??
152
- new Response(JSON.stringify({ error: "unauthorized" }), {
153
- status: 401,
154
- headers: {
155
- "Content-Type": "application/json",
156
- "WWW-Authenticate": "Bearer",
157
- },
158
- }),
159
- };
160
- }
161
-
162
- export async function authorizeUiAdmin(
163
- request: Request,
164
- baseUrl: string,
165
- auth: InboundAuth[],
166
- purpose = "credential management",
167
- ): Promise<{ ok: true; userId: string } | { ok: false; response: Response }> {
168
- // Operator mutation is intentionally narrower than /mcp and /ui/data: only
169
- // an interactive Clerk provider may admit it. A static bearer token is useful
170
- // for headless tool calls but must not become a deployment-admin key.
171
- //
172
- // Every Clerk provider gets a turn, the way the /mcp gate does. Stopping at
173
- // the first would make admission depend on config order: a failed gate or
174
- // missing user may simply mean a later provider is the one meant to admit.
175
- // The last refusal is returned if none do.
176
- const providers = auth.filter(
177
- (candidate) => candidate.uiAuth?.kind === "clerk",
178
- );
179
- if (providers.length === 0) {
180
- return {
181
- ok: false,
182
- response: privateJson(
183
- { error: `${purpose} requires Clerk authentication` },
184
- { status: 403 },
185
- ),
186
- };
187
- }
188
- let lastResponse: Response | null = null;
189
- for (const provider of providers) {
190
- const result = await provider.authorize(request, baseUrl);
191
- if (!result.ok) {
192
- lastResponse = result.response;
193
- continue;
194
- }
195
- if (!result.userId) {
196
- lastResponse = privateJson(
197
- { error: "authenticated user required" },
198
- { status: 403 },
199
- );
200
- continue;
201
- }
202
- return { ok: true, userId: result.userId };
203
- }
204
- return {
205
- ok: false,
206
- response:
207
- lastResponse ??
208
- privateJson({ error: "forbidden" }, { status: 403 }),
209
- };
210
- }
211
-
212
- export function isSameOrigin(request: Request, baseUrl: string): boolean {
213
- const origin = request.headers.get("origin");
214
- if (!origin) return false;
215
- try {
216
- return new URL(origin).origin === new URL(baseUrl).origin;
217
- } catch {
218
- return false;
219
- }
220
- }
221
-
222
- export function withSecurityHeaders(
223
- response: Response,
224
- requestUrl: URL,
225
- path: string,
226
- ): Response {
227
- const headers = new Headers(response.headers);
228
- headers.set("X-Content-Type-Options", "nosniff");
229
- headers.set("Referrer-Policy", "no-referrer");
230
- if (requestUrl.protocol === "https:") {
231
- headers.set("Strict-Transport-Security", "max-age=31536000");
232
- }
233
- if (operatorPageForPath(path) || path === "/ui") {
234
- // Operator HTML responses ship their own nonce-based script CSP (which
235
- // already includes frame-ancestors 'none'); only fall back to the
236
- // framing-only directive when no CSP is present (for example redirects).
237
- if (!headers.has("Content-Security-Policy")) {
238
- headers.set("Content-Security-Policy", "frame-ancestors 'none'");
239
- }
240
- headers.set("X-Frame-Options", "DENY");
241
- }
242
- return new Response(response.body, {
243
- status: response.status,
244
- statusText: response.statusText,
245
- headers,
246
- });
247
- }