@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,14 +0,0 @@
1
- const hasOwn = (value: object, key: PropertyKey): boolean =>
2
- Object.prototype.hasOwnProperty.call(value, key);
3
-
4
- /** Fail loudly when JavaScript callers pass the retired audience-scoping keys. */
5
- export function assertNoRetiredToolkitOptions(
6
- source: string,
7
- options: object,
8
- ): void {
9
- if (!hasOwn(options, "toolkits") && !hasOwn(options, "unscoped")) return;
10
- throw new Error(
11
- `${source} options \`toolkits\` and \`unscoped\` were removed in issue ` +
12
- "#178. Deploy one connecta instance per audience instead; see ethos.md.",
13
- );
14
- }
@@ -1,115 +0,0 @@
1
- import {
2
- authorizeUiAdmin,
3
- isSameOrigin,
4
- msg,
5
- privateJson,
6
- type RouteContext,
7
- } from "./shared.js";
8
-
9
- async function readName(
10
- request: Request,
11
- ): Promise<
12
- { ok: true; name: unknown } | { ok: false; response: Response }
13
- > {
14
- if (
15
- !request.headers
16
- .get("content-type")
17
- ?.toLowerCase()
18
- .startsWith("application/json")
19
- ) {
20
- return {
21
- ok: false,
22
- response: privateJson(
23
- { error: "Content-Type must be application/json" },
24
- { status: 415 },
25
- ),
26
- };
27
- }
28
- const raw = await request.text();
29
- if (raw.length > 1_000) {
30
- return {
31
- ok: false,
32
- response: privateJson(
33
- { error: "request body is too large" },
34
- { status: 413 },
35
- ),
36
- };
37
- }
38
- try {
39
- const body = JSON.parse(raw) as { name?: unknown };
40
- return { ok: true, name: body.name };
41
- } catch {
42
- return {
43
- ok: false,
44
- response: privateJson({ error: "invalid JSON body" }, { status: 400 }),
45
- };
46
- }
47
- }
48
-
49
- /**
50
- * Clerk-only lifecycle for deployment access tokens. The access token itself
51
- * is deliberately never an administrator credential and cannot reach here.
52
- */
53
- export async function routeAccessTokens(
54
- context: RouteContext,
55
- ): Promise<Response | null> {
56
- const match =
57
- /^\/ui\/access-tokens(?:\/([0-9a-f-]{36}))?$/.exec(context.path);
58
- if (!match) return null;
59
- const { request, baseUrl, opts } = context;
60
- if (request.method === "OPTIONS") {
61
- return privateJson({ error: "CORS is not allowed" }, { status: 403 });
62
- }
63
- if (!opts.accessTokens) {
64
- return privateJson(
65
- { error: "access token management is not configured" },
66
- { status: 404 },
67
- );
68
- }
69
- const mutating = request.method !== "GET";
70
- if (mutating && !isSameOrigin(request, baseUrl)) {
71
- return privateJson(
72
- { error: "same-origin request required" },
73
- { status: 403 },
74
- );
75
- }
76
- const admin = await authorizeUiAdmin(
77
- request,
78
- baseUrl,
79
- opts.auth,
80
- "access token management",
81
- );
82
- if (!admin.ok) return admin.response;
83
-
84
- const id = match[1];
85
- try {
86
- if (!id && request.method === "GET") {
87
- return privateJson({ accessTokens: await opts.accessTokens.list() });
88
- }
89
- if (!id && request.method === "POST") {
90
- const input = await readName(request);
91
- if (!input.ok) return input.response;
92
- return privateJson(
93
- await opts.accessTokens.create(input.name, admin.userId),
94
- { status: 201 },
95
- );
96
- }
97
- if (id && request.method === "PUT") {
98
- const input = await readName(request);
99
- if (!input.ok) return input.response;
100
- const accessToken = await opts.accessTokens.rename(id, input.name);
101
- return accessToken
102
- ? privateJson({ accessToken })
103
- : privateJson({ error: "unknown access token" }, { status: 404 });
104
- }
105
- if (id && request.method === "DELETE") {
106
- const accessToken = await opts.accessTokens.revoke(id, admin.userId);
107
- return accessToken
108
- ? privateJson({ accessToken })
109
- : privateJson({ error: "unknown access token" }, { status: 404 });
110
- }
111
- return privateJson({ error: "method not allowed" }, { status: 405 });
112
- } catch (error) {
113
- return privateJson({ error: msg(error) }, { status: 400 });
114
- }
115
- }
@@ -1,215 +0,0 @@
1
- import {
2
- InvalidActivityCursorError,
3
- type ActivityActor,
4
- type ActivityPage,
5
- type ActivityReadPage,
6
- } from "../activity.js";
7
- import type { InboundAuth } from "../types.js";
8
- import {
9
- activityActorNamespace,
10
- authorize,
11
- privateJson,
12
- type RouteContext,
13
- } from "./shared.js";
14
-
15
- const ACTIVITY_LABEL_CONCURRENCY = 8;
16
- const ACTIVITY_LABEL_PAGE_BUDGET_MS = 1_500;
17
- const ACTIVITY_LABEL_MAX_LENGTH = 160;
18
-
19
- function cleanActivityActorLabel(value: unknown): string | undefined {
20
- if (typeof value !== "string") return undefined;
21
- const compact = value.replace(/\s+/gu, " ").trim();
22
- if (!compact) return undefined;
23
- return Array.from(compact).slice(0, ACTIVITY_LABEL_MAX_LENGTH).join("");
24
- }
25
-
26
- async function boundedActivityActorLabel(
27
- hook: NonNullable<InboundAuth["activityActorLabel"]>,
28
- id: string,
29
- budgetMs: number,
30
- ): Promise<string | undefined> {
31
- let timer: number | undefined;
32
- try {
33
- return await Promise.race([
34
- Promise.resolve(hook(id))
35
- .then(cleanActivityActorLabel)
36
- .catch(() => undefined),
37
- new Promise<undefined>((resolve) => {
38
- timer = setTimeout(resolve, budgetMs);
39
- }),
40
- ]);
41
- } catch {
42
- return undefined;
43
- } finally {
44
- if (timer !== undefined) clearTimeout(timer);
45
- }
46
- }
47
-
48
- /**
49
- * Add display-only actor labels to one authorized activity page. Resolution is
50
- * best-effort, bounded, and read-time only: stored events retain stable ids and
51
- * a profile-provider outage falls back to those ids without failing the page.
52
- */
53
- async function enrichActivityActorLabels(
54
- page: ActivityPage,
55
- auth: readonly InboundAuth[],
56
- ): Promise<ActivityReadPage> {
57
- const identities = new Map<
58
- string,
59
- { kind: string; id: string; namespace?: string }
60
- >();
61
- for (const event of page.events) {
62
- if (!event.actor.id) continue;
63
- identities.set(
64
- JSON.stringify([
65
- event.actor.kind,
66
- event.actor.namespace,
67
- event.actor.id,
68
- ]),
69
- {
70
- kind: event.actor.kind,
71
- id: event.actor.id,
72
- ...(event.actor.namespace
73
- ? { namespace: event.actor.namespace }
74
- : {}),
75
- },
76
- );
77
- }
78
- const queue = [...identities.entries()];
79
- const labels = new Map<string, string>();
80
- let next = 0;
81
- const deadline = Date.now() + ACTIVITY_LABEL_PAGE_BUDGET_MS;
82
- const workers = Array.from(
83
- { length: Math.min(ACTIVITY_LABEL_CONCURRENCY, queue.length) },
84
- async () => {
85
- while (next < queue.length) {
86
- const entry = queue[next++];
87
- if (!entry) return;
88
- const [key, identity] = entry;
89
- const sameKindProviders = auth
90
- .map((provider, index) => ({ provider, index }))
91
- .filter(({ provider }) => provider.kind === identity.kind);
92
- const candidates = sameKindProviders.filter(({ provider }) =>
93
- Boolean(provider.activityActorLabel),
94
- );
95
- const eligible = identity.namespace
96
- ? candidates.filter(
97
- ({ provider }) =>
98
- activityActorNamespace(provider) === identity.namespace,
99
- )
100
- : (() => {
101
- const directoryKey = ({
102
- provider,
103
- index,
104
- }: (typeof sameKindProviders)[number]) => {
105
- const namespace = activityActorNamespace(provider);
106
- return namespace === undefined
107
- ? `provider:${index}`
108
- : `namespace:${namespace}`;
109
- };
110
- // Every same-kind provider participates in the ambiguity check,
111
- // even if it cannot resolve labels. Otherwise a legacy ID owned
112
- // by a provider without a resolver could be disclosed to a
113
- // different provider that happens to have one.
114
- const directories = new Set(
115
- sameKindProviders.map(directoryKey),
116
- );
117
- if (directories.size !== 1) return [];
118
- const [directory] = directories;
119
- return candidates.filter(
120
- (candidate) => directoryKey(candidate) === directory,
121
- );
122
- })();
123
- // One namespace is one directory. Use its first configured resolver so
124
- // duplicate gate adapters over the same Clerk instance do not multiply
125
- // the provider-level concurrency cap.
126
- const provider = eligible[0]?.provider;
127
- if (!provider) continue;
128
- const remaining = deadline - Date.now();
129
- if (remaining <= 0) return;
130
- const label = await boundedActivityActorLabel(
131
- provider.activityActorLabel!.bind(provider),
132
- identity.id,
133
- remaining,
134
- );
135
- if (label) labels.set(key, label);
136
- }
137
- },
138
- );
139
- await Promise.all(workers);
140
- return {
141
- ...page,
142
- events: page.events.map((event) => {
143
- const resolved = event.actor.id
144
- ? labels.get(
145
- JSON.stringify([
146
- event.actor.kind,
147
- event.actor.namespace,
148
- event.actor.id,
149
- ]),
150
- )
151
- : undefined;
152
- // Never trust or echo a `label` supplied by storage. The persisted event
153
- // schema has no label; only this authenticated read path may add one.
154
- const actor: ActivityActor = {
155
- kind: event.actor.kind,
156
- ...(event.actor.id ? { id: event.actor.id } : {}),
157
- ...(event.actor.namespace
158
- ? { namespace: event.actor.namespace }
159
- : {}),
160
- };
161
- return {
162
- ...event,
163
- actor: resolved ? { ...actor, label: resolved } : actor,
164
- };
165
- }),
166
- };
167
- }
168
-
169
- export async function routeActivity(
170
- context: RouteContext,
171
- ): Promise<Response | null> {
172
- const { path, request, url, baseUrl, opts } = context;
173
- if (path !== "/ui/activity") return null;
174
- if (request.method !== "GET") {
175
- return privateJson({ error: "method not allowed" }, { status: 405 });
176
- }
177
- const authz = await authorize(request, baseUrl, opts.auth);
178
- if (!authz.ok) return authz.response;
179
- if (
180
- opts.activityReadGate &&
181
- !(await opts.activityReadGate(authz.actor))
182
- ) {
183
- return privateJson({ error: "forbidden" }, { status: 403 });
184
- }
185
- if (!opts.activity?.list) {
186
- return privateJson(
187
- { error: "activity history is not configured" },
188
- { status: 404 },
189
- );
190
- }
191
- const cursor = url.searchParams.get("cursor") ?? undefined;
192
- if (cursor && cursor.length > 500) {
193
- return privateJson({ error: "invalid cursor" }, { status: 400 });
194
- }
195
- const requestedLimit = Number(url.searchParams.get("limit") ?? "50");
196
- const limit = Number.isFinite(requestedLimit)
197
- ? Math.min(100, Math.max(1, Math.trunc(requestedLimit)))
198
- : 50;
199
- try {
200
- const page = await opts.activity.list({
201
- ...(cursor !== undefined ? { cursor } : {}),
202
- limit,
203
- });
204
- return privateJson(await enrichActivityActorLabels(page, opts.auth));
205
- } catch (error) {
206
- if (error instanceof InvalidActivityCursorError) {
207
- return privateJson({ error: error.message }, { status: 400 });
208
- }
209
- opts.logger.error("[connecta] activity read failed", error);
210
- return privateJson(
211
- { error: "activity history is temporarily unavailable" },
212
- { status: 503 },
213
- );
214
- }
215
- }
@@ -1,254 +0,0 @@
1
- import {
2
- credentialTestRule,
3
- describeCredentialTestMismatch,
4
- storedCredentialShape,
5
- } from "../credentials.js";
6
- import type {
7
- ConnectorCredentialConfig,
8
- ConnectorCredentialValues,
9
- } from "../types.js";
10
- import {
11
- authorizeUiAdmin,
12
- isSameOrigin,
13
- msg,
14
- privateJson,
15
- type RouteContext,
16
- } from "./shared.js";
17
-
18
- type CredentialInput =
19
- | { kind: "single"; value: string }
20
- | { kind: "multiple"; values: ConnectorCredentialValues };
21
-
22
- async function readCredentialInput(
23
- request: Request,
24
- config: ConnectorCredentialConfig,
25
- ): Promise<
26
- { ok: true; input: CredentialInput } | { ok: false; response: Response }
27
- > {
28
- if (
29
- !request.headers
30
- .get("content-type")
31
- ?.toLowerCase()
32
- .startsWith("application/json")
33
- ) {
34
- return {
35
- ok: false,
36
- response: privateJson(
37
- { error: "Content-Type must be application/json" },
38
- { status: 415 },
39
- ),
40
- };
41
- }
42
- const raw = await request.text();
43
- if (raw.length > 20_000) {
44
- return {
45
- ok: false,
46
- response: privateJson(
47
- { error: "request body is too large" },
48
- { status: 413 },
49
- ),
50
- };
51
- }
52
- try {
53
- const body = JSON.parse(raw) as {
54
- value?: unknown;
55
- values?: unknown;
56
- };
57
- if (!config.fields?.length) {
58
- if (typeof body.value !== "string" || !body.value.trim()) {
59
- return {
60
- ok: false,
61
- response: privateJson(
62
- { error: "value must be a non-empty string" },
63
- { status: 400 },
64
- ),
65
- };
66
- }
67
- return { ok: true, input: { kind: "single", value: body.value } };
68
- }
69
- if (
70
- !body.values ||
71
- typeof body.values !== "object" ||
72
- Array.isArray(body.values)
73
- ) {
74
- return {
75
- ok: false,
76
- response: privateJson(
77
- { error: "values must be an object" },
78
- { status: 400 },
79
- ),
80
- };
81
- }
82
- const rawValues = body.values as Record<string, unknown>;
83
- const expected = new Set(config.fields.map((field) => field.name));
84
- const unexpected = Object.keys(rawValues).find(
85
- (field) => !expected.has(field),
86
- );
87
- if (unexpected) {
88
- return {
89
- ok: false,
90
- response: privateJson(
91
- { error: `unexpected credential field "${unexpected}"` },
92
- { status: 400 },
93
- ),
94
- };
95
- }
96
- const values: ConnectorCredentialValues = {};
97
- for (const field of config.fields) {
98
- const value = rawValues[field.name];
99
- if (typeof value !== "string" || !value.trim()) {
100
- return {
101
- ok: false,
102
- response: privateJson(
103
- { error: `${field.name} must be a non-empty string` },
104
- { status: 400 },
105
- ),
106
- };
107
- }
108
- values[field.name] = value;
109
- }
110
- return { ok: true, input: { kind: "multiple", values } };
111
- } catch {
112
- return {
113
- ok: false,
114
- response: privateJson({ error: "invalid JSON body" }, { status: 400 }),
115
- };
116
- }
117
- }
118
-
119
- async function handleCredentialRequest(
120
- context: RouteContext,
121
- connectorId: string,
122
- action: string | undefined,
123
- ): Promise<Response> {
124
- const { request, baseUrl, opts } = context;
125
- if (!opts.credentialVault) {
126
- return privateJson(
127
- { error: "credential storage is not configured" },
128
- { status: 503 },
129
- );
130
- }
131
- if (!isSameOrigin(request, baseUrl)) {
132
- return privateJson(
133
- { error: "same-origin request required" },
134
- { status: 403 },
135
- );
136
- }
137
- const admin = await authorizeUiAdmin(
138
- request,
139
- baseUrl,
140
- opts.auth,
141
- );
142
- if (!admin.ok) return admin.response;
143
-
144
- const connector = opts.registry.getConnector(connectorId);
145
- if (!connector?.credential) {
146
- return privateJson({ error: "unknown credential slot" }, { status: 404 });
147
- }
148
-
149
- if (action === "test") {
150
- if (request.method !== "POST") {
151
- return privateJson({ error: "method not allowed" }, { status: 405 });
152
- }
153
- // The declared credential shape picks the hook — the same single rule the
154
- // Credentials page asks for its Test affordance, so a shown button reaches
155
- // that reads the shape the credential was stored in.
156
- const rule = credentialTestRule(connector);
157
- if (!rule.mode) {
158
- return privateJson(
159
- {
160
- error: rule.mismatch
161
- ? "this connector cannot test its credential: " +
162
- describeCredentialTestMismatch(rule.mismatch)
163
- : "this connector does not support credential testing",
164
- },
165
- { status: 400 },
166
- );
167
- }
168
- try {
169
- const values = await opts.credentialVault.getAll(connectorId);
170
- const shape = storedCredentialShape(connector.credential, values);
171
- if (shape.state === "missing") {
172
- return privateJson(
173
- {
174
- error:
175
- rule.mode === "multiple"
176
- ? "configure the credentials before testing them"
177
- : "configure the credential before testing it",
178
- },
179
- { status: 409 },
180
- );
181
- }
182
- if (shape.state === "mismatch") {
183
- return privateJson({ error: shape.message }, { status: 409 });
184
- }
185
- const storedValues = values!;
186
- const ctx = opts.registry.contextFor(connectorId, baseUrl);
187
- const result =
188
- rule.mode === "multiple"
189
- ? await connector.testCredentials!(storedValues, ctx)
190
- : await connector.testCredential!(
191
- // The single-value shape check above guarantees this key.
192
- storedValues.value!,
193
- ctx,
194
- );
195
- return privateJson(result);
196
- } catch (err) {
197
- return privateJson({ ok: false, message: msg(err) });
198
- }
199
- }
200
-
201
- if (action) {
202
- return privateJson({ error: "not found" }, { status: 404 });
203
- }
204
- if (request.method === "PUT") {
205
- const input = await readCredentialInput(request, connector.credential);
206
- if (!input.ok) return input.response;
207
- try {
208
- const metadata =
209
- input.input.kind === "single"
210
- ? await opts.credentialVault.set(
211
- connectorId,
212
- input.input.value,
213
- admin.userId,
214
- )
215
- : await opts.credentialVault.setAll(
216
- connectorId,
217
- input.input.values,
218
- admin.userId,
219
- );
220
- await opts.registry.invalidateStored(connectorId);
221
- return privateJson({ credential: metadata });
222
- } catch (err) {
223
- return privateJson({ error: msg(err) }, { status: 400 });
224
- }
225
- }
226
- if (request.method === "DELETE") {
227
- await opts.credentialVault.delete(connectorId);
228
- await opts.registry.invalidateStored(connectorId);
229
- return new Response(null, {
230
- status: 204,
231
- headers: {
232
- "Cache-Control": "no-store",
233
- "Referrer-Policy": "no-referrer",
234
- },
235
- });
236
- }
237
- return privateJson({ error: "method not allowed" }, { status: 405 });
238
- }
239
-
240
- export async function routeCredentials(
241
- context: RouteContext,
242
- ): Promise<Response | null> {
243
- const match =
244
- /^\/ui\/credentials\/([a-z0-9_-]+)(?:\/([a-z]+))?$/.exec(context.path);
245
- if (!match) return null;
246
- const connectorId = match[1];
247
- if (!connectorId) return null;
248
- // Never opt these mutation routes into the server's wildcard CORS
249
- // preflight behavior.
250
- if (context.request.method === "OPTIONS") {
251
- return privateJson({ error: "method not allowed" }, { status: 405 });
252
- }
253
- return handleCredentialRequest(context, connectorId, match[2]);
254
- }