@zackbart/connecta 0.22.3 → 0.24.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 (94) hide show
  1. package/AGENTS.md +5 -0
  2. package/CHANGELOG.md +82 -0
  3. package/README.md +19 -11
  4. package/dist/activity-friction.d.ts +3 -0
  5. package/dist/activity-friction.js +19 -0
  6. package/dist/activity.d.ts +11 -2
  7. package/dist/activity.js +15 -19
  8. package/dist/auth/downstream-oauth.d.ts +2 -1
  9. package/dist/auth/downstream-oauth.js +10 -1
  10. package/dist/branding.d.ts +67 -0
  11. package/dist/branding.js +176 -0
  12. package/dist/catalog-service.d.ts +1 -6
  13. package/dist/catalog-service.js +3 -53
  14. package/dist/connectors/remote-mcp.js +3 -5
  15. package/dist/credential-contract.d.ts +24 -0
  16. package/dist/credential-contract.js +1 -0
  17. package/dist/credential-rules.d.ts +85 -0
  18. package/dist/credential-rules.js +107 -0
  19. package/dist/credentials.d.ts +4 -100
  20. package/dist/credentials.js +3 -107
  21. package/dist/errors.d.ts +1 -1
  22. package/dist/execute.d.ts +5 -52
  23. package/dist/execute.js +40 -347
  24. package/dist/executors/quickjs-protocol.d.ts +0 -7
  25. package/dist/executors/quickjs-protocol.js +2 -10
  26. package/dist/executors/quickjs.js +1 -1
  27. package/dist/index.d.ts +23 -56
  28. package/dist/index.js +30 -58
  29. package/dist/invocation.d.ts +0 -33
  30. package/dist/invocation.js +56 -124
  31. package/dist/meta-tools.d.ts +7 -6
  32. package/dist/meta-tools.js +14 -21
  33. package/dist/module-contracts.d.ts +19 -0
  34. package/dist/module-contracts.js +1 -0
  35. package/dist/operator-ui/generated.js +2 -2
  36. package/dist/operator-ui/model.d.ts +6 -3
  37. package/dist/operator-ui/view.d.ts +2 -18
  38. package/dist/operator-ui/view.js +3 -20
  39. package/dist/registry.d.ts +4 -1
  40. package/dist/registry.js +8 -6
  41. package/dist/routes/activity.js +1 -1
  42. package/dist/routes/credentials.js +5 -2
  43. package/dist/routes/mcp.js +7 -51
  44. package/dist/routes/oauth-management.d.ts +2 -0
  45. package/dist/routes/oauth-management.js +108 -0
  46. package/dist/routes/oauth.d.ts +0 -1
  47. package/dist/routes/oauth.js +21 -121
  48. package/dist/routes/shared.d.ts +19 -17
  49. package/dist/routes/shared.js +48 -44
  50. package/dist/routes/ui.js +36 -33
  51. package/dist/server.d.ts +1 -2
  52. package/dist/server.js +7 -45
  53. package/dist/skills.d.ts +1 -1
  54. package/dist/skills.js +55 -19
  55. package/dist/types.d.ts +3 -15
  56. package/dist/ui.d.ts +15 -70
  57. package/dist/ui.js +176 -317
  58. package/dist/version.d.ts +1 -1
  59. package/dist/version.js +1 -1
  60. package/documentation/architecture.md +31 -26
  61. package/documentation/auth.md +65 -111
  62. package/documentation/call-admission.md +10 -11
  63. package/documentation/cloudflare.md +1 -1
  64. package/documentation/code-mode.md +65 -260
  65. package/documentation/connectors.md +8 -6
  66. package/documentation/linear.md +1 -1
  67. package/documentation/meta-tools.md +37 -18
  68. package/documentation/mixpanel.md +1 -1
  69. package/documentation/notion.md +2 -2
  70. package/documentation/operations.md +19 -23
  71. package/documentation/operator-ui.md +82 -104
  72. package/documentation/optional-modules-upgrade.md +243 -0
  73. package/documentation/provider-conventions.md +7 -5
  74. package/documentation/revenuecat.md +1 -1
  75. package/documentation/storage-and-credentials.md +59 -40
  76. package/documentation/stripe.md +1 -1
  77. package/documentation/upgrading.md +83 -8
  78. package/ethos.md +36 -43
  79. package/examples/worker/AGENTS.md +3 -1
  80. package/examples/worker/README.md +68 -84
  81. package/examples/worker/src/d1-activity.ts +1 -1
  82. package/examples/worker/src/index.ts +11 -6
  83. package/package.json +18 -2
  84. package/templates/node/AGENTS.md +8 -6
  85. package/templates/node/README.md +56 -67
  86. package/templates/node/package.json +1 -1
  87. package/templates/node/src/file-activity.ts +1 -1
  88. package/templates/node/src/index.ts +11 -12
  89. package/dist/access-tokens.d.ts +0 -31
  90. package/dist/access-tokens.js +0 -236
  91. package/dist/apps-shell.d.ts +0 -37
  92. package/dist/apps-shell.js +0 -174
  93. package/dist/routes/access-tokens.d.ts +0 -6
  94. package/dist/routes/access-tokens.js +0 -83
package/dist/ui.d.ts CHANGED
@@ -1,75 +1,12 @@
1
- import type { CredentialVault } from "./credentials.js";
1
+ import type { OperatorSurface } from "./module-contracts.js";
2
+ import type { CredentialVault } from "./credential-contract.js";
2
3
  import { type DeferredWork } from "./connector-scope.js";
3
- import { type CredentialManagementCapability, type AccessTokenManagementCapability, type UiData } from "./operator-ui/model.js";
4
+ import { type CredentialManagementCapability, type UiData } from "./operator-ui/model.js";
4
5
  import type { RegistryView } from "./registry.js";
5
6
  import type { ConnectaBranding, UiAuthConfig } from "./types.js";
6
- export { filterUiConnectors, type AccessTokenManagementCapability, type CredentialManagementCapability, type UiConnector, type UiData, } from "./operator-ui/model.js";
7
- /** Connecta's default monochrome "C" mark. */
8
- export declare const CONNECTA_FAVICON_SVG = "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 32 32\">\n <style>\n .fg { fill: #000 }\n @media (prefers-color-scheme: dark) { .fg { fill: #fff } }\n </style>\n <path class=\"fg\" d=\"M27 9.4A13 13 0 1 0 27 22.6l-4.4-2.5a8 8 0 1 1 0-8.2z\"/>\n</svg>";
9
- interface ResolvedBranding {
10
- productName: string;
11
- productUrl?: string;
12
- ownerName?: string;
13
- ownerUrl?: string;
14
- description: string;
15
- /** Browser tab title and page meta name. */
16
- pageTitle: string;
17
- /** href for the page's icon link. */
18
- faviconHref: string;
19
- themeColor: string;
20
- }
21
- export declare function resolveBranding(branding?: ConnectaBranding): ResolvedBranding;
22
- /**
23
- * Names of the branding URLs the operator set that failed their gate and were
24
- * replaced by a default. Lives beside the gates so the startup warning cannot
25
- * drift from them, and takes `unknown` fields for the same reason
26
- * `resolveBranding` does — a warning helper must never throw.
27
- */
28
- export declare function droppedBrandingUrls(branding?: ConnectaBranding): string[];
29
- export declare function isSafeHttpUrl(url: unknown): boolean;
30
- /**
31
- * True for values allowed in the page's `<link rel="icon" href>`: an absolute
32
- * `http(s)` URL (an icon the operator hosts elsewhere) or a path rooted at this
33
- * origin. The relative carve-out is deliberate rather than accidental — the
34
- * default href is the relative `/favicon.svg`, which `isSafeHttpUrl` alone would
35
- * reject — and it is kept narrow on both ends.
36
- *
37
- * Root-relative only, because operator and OAuth callback pages sit at
38
- * different depths and a document-relative path would resolve differently.
39
- *
40
- * "Root-relative" is enforced structurally: exactly one leading `/` followed by
41
- * a character that is neither `/` nor `\`. Both of those would make the value an
42
- * authority (`//host`, and `/\host` because the URL parser folds `\` to `/` in
43
- * special schemes), pointing at an origin this server does not control. The test
44
- * runs on a copy with tab/newline/CR removed, since the parser strips those
45
- * anywhere and `/\t/host` would otherwise slip through as single-slash. The
46
- * origin comparison that follows is defense in depth, not the authority check —
47
- * on its own it would accept an authority that happened to equal the probe host.
48
- */
49
- export declare function isSafeIconHref(href: unknown): boolean;
50
- /** Absolute HTTPS gate for the `UiAuthConfig` URL fields documented in types.ts. */
51
- export declare function isSafeHttpsUrl(url: unknown): boolean;
52
- /**
53
- * Names of the `uiAuth` URLs an inbound-auth provider supplied that failed their
54
- * gate. Lives beside the gate for the same reason `droppedBrandingUrls` does: the
55
- * startup warning cannot then drift from what rendering actually drops. Every
56
- * field is read defensively rather than trusted, because a custom `InboundAuth`
57
- * is untyped at a JS call site — `isSafeHttpsUrl` takes `unknown`, and a
58
- * `uiAuth` that is not the clerk shape is reported as nothing to warn about.
59
- *
60
- * `frontendApiUrl` is required, so anything that fails its gate is a drop.
61
- * `signInUrl` and `signUpUrl` are optional, so only a value the operator
62
- * *supplied* and the gate then rejected is worth a warning — an unset field
63
- * took no default away from anyone. `isSetUrlValue` decides that, the same way
64
- * and for the same reasons it decides it for the branding URLs: a warning that
65
- * fires for one and not the other would be reporting on the field rather than
66
- * on the operator's intent. Rendering is not consulted for this: it drops on
67
- * the gate alone, and a blank string fails that gate too — it is simply not
68
- * *reported*, because a blank is indistinguishable from leaving the field
69
- * alone.
70
- */
71
- export declare function droppedUiAuthUrls(uiAuth?: UiAuthConfig): string[];
72
- export type OperatorPage = "connections" | "credentials" | "tokens" | "activity";
7
+ export { filterUiConnectors, type CredentialManagementCapability, type UiConnector, type UiData, } from "./operator-ui/model.js";
8
+ export { CONNECTA_FAVICON_SVG, resolveBranding, isSafeHttpUrl, isSafeHttpsUrl, isSafeIconHref } from "./branding.js";
9
+ export type OperatorPage = "connections" | "activity";
73
10
  export declare function operatorPageForPath(path: string): OperatorPage | undefined;
74
11
  export declare function operatorPageTitle(page: OperatorPage, configuredTitle: string): string;
75
12
  export declare function credentialManagementCapability(input: {
@@ -85,9 +22,17 @@ export declare function credentialManagementCapability(input: {
85
22
  export declare function buildUiData(registry: RegistryView, baseUrl: string, serverInfo: {
86
23
  name: string;
87
24
  version: string;
88
- }, credentialVault?: CredentialVault, activityEnabled?: boolean, credentialManagement?: CredentialManagementCapability, defer?: DeferredWork, oauthManagement?: boolean, discoveryConcurrency?: number, accessTokenManagement?: AccessTokenManagementCapability, personalCredentialOwner?: string): Promise<UiData>;
25
+ }, credentialVault?: CredentialVault, activityEnabled?: boolean, credentialManagement?: CredentialManagementCapability, defer?: DeferredWork, oauthManagement?: boolean, discoveryConcurrency?: number, personalCredentialOwner?: string, detailOptions?: {
26
+ mayManage?: (id: string) => boolean;
27
+ timeoutMs?: number;
28
+ signal?: AbortSignal;
29
+ }): Promise<UiData>;
89
30
  /**
90
31
  * Every operator page serves this same data-free shell. Connector, credential,
91
32
  * and activity data arrives only through the authenticated `/ui/*` APIs.
92
33
  */
93
34
  export declare function renderUiHtml(uiAuth?: UiAuthConfig, mcpUrl?: string, branding?: ConnectaBranding, nonce?: string, page?: OperatorPage): string;
35
+ /** Mount the connection UI without enabling any storage or activity module. */
36
+ export declare function operatorUi(options?: {
37
+ branding?: ConnectaBranding;
38
+ }): OperatorSurface;
package/dist/ui.js CHANGED
@@ -1,64 +1,15 @@
1
- import { credentialTestRule, describeUndeclaredCredentialFields, storedCredentialShape, } from "./credentials.js";
1
+ import { routeUi } from "./routes/ui.js";
2
+ import { routeCredentials } from "./routes/credentials.js";
3
+ import { routeOAuthManagement } from "./routes/oauth-management.js";
4
+ import { withDeadline } from "./timeout.js";
5
+ import { credentialTestRule, describeUndeclaredCredentialFields, storedCredentialShape, } from "./credential-rules.js";
2
6
  import { closeConnectorScope, } from "./connector-scope.js";
3
7
  import { mapSettledWithConcurrency, resolveDiscoveryConcurrency, } from "./concurrency.js";
4
8
  import { OPERATOR_UI_CSS, OPERATOR_UI_SCRIPT, } from "./operator-ui/generated.js";
5
9
  import { CONNECTA_VERSION } from "./version.js";
6
10
  export { filterUiConnectors, } from "./operator-ui/model.js";
7
- /** Connecta's default monochrome "C" mark. */
8
- export const CONNECTA_FAVICON_SVG = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
9
- <style>
10
- .fg { fill: #000 }
11
- @media (prefers-color-scheme: dark) { .fg { fill: #fff } }
12
- </style>
13
- <path class="fg" d="M27 9.4A13 13 0 1 0 27 22.6l-4.4-2.5a8 8 0 1 1 0-8.2z"/>
14
- </svg>`;
15
- const DEFAULT_FAVICON_HREF = "/favicon.svg";
16
- /**
17
- * Branding arrives from operator config, which is untyped at a JS call site, so
18
- * every field is treated as `unknown`: a non-string is read as unset rather than
19
- * throwing on `.trim()`. Rendering must degrade to defaults for a malformed
20
- * value, never fail — `createConnecta` calls this during construction.
21
- */
22
- function trimmedString(value) {
23
- return typeof value === "string" ? value.trim() || undefined : undefined;
24
- }
25
- export function resolveBranding(branding) {
26
- const productName = trimmedString(branding?.productName) ?? "Connecta";
27
- const ownerName = trimmedString(branding?.ownerName);
28
- // Operator branding URLs become masthead/callback hrefs, so a non-http(s)
29
- // scheme (javascript:, data:) is dropped the same as an unset URL — the
30
- // callers already render a <span> instead of an <a> when it is absent.
31
- const productUrl = trimmedString(branding?.productUrl);
32
- const ownerUrl = trimmedString(branding?.ownerUrl);
33
- const faviconHref = trimmedString(branding?.favicon?.href);
34
- return {
35
- productName,
36
- ...(productUrl && isSafeHttpUrl(productUrl) ? { productUrl } : {}),
37
- ...(ownerName ? { ownerName } : {}),
38
- ...(ownerUrl && isSafeHttpUrl(ownerUrl) ? { ownerUrl } : {}),
39
- description: trimmedString(branding?.description) ??
40
- `Manage the services this ${productName} instance makes available to agents.`,
41
- pageTitle: trimmedString(branding?.pageTitle) ??
42
- (ownerName ? `${productName} — ${ownerName}` : productName),
43
- faviconHref: faviconHref && isSafeIconHref(faviconHref)
44
- ? faviconHref
45
- : DEFAULT_FAVICON_HREF,
46
- themeColor: trimmedString(branding?.themeColor) ?? "#ffffff",
47
- };
48
- }
49
- /**
50
- * Whether the operator meant to supply a value here — the question every
51
- * dropped-URL warning asks before naming a field, and one definition so the
52
- * branding and `uiAuth` warnings cannot answer it differently. A non-string
53
- * counts as set: the intent was there and is exactly what the warning reports
54
- * on. A blank or whitespace-only string does not; that is indistinguishable
55
- * from leaving the field alone, and both take the default silently.
56
- */
57
- function isSetUrlValue(value) {
58
- return typeof value === "string"
59
- ? trimmedString(value) !== undefined
60
- : value !== undefined && value !== null;
61
- }
11
+ import { resolveBranding, isSafeHttpsUrl } from "./branding.js";
12
+ export { CONNECTA_FAVICON_SVG, resolveBranding, isSafeHttpUrl, isSafeHttpsUrl, isSafeIconHref } from "./branding.js";
62
13
  /**
63
14
  * A JS string literal safe to inline in a script element. Escaping `/` keeps
64
15
  * an operator-supplied `</script>` from terminating the element early.
@@ -66,140 +17,13 @@ function isSetUrlValue(value) {
66
17
  function stringForInlineScript(value) {
67
18
  return JSON.stringify(value).replace(/\//g, "\\/");
68
19
  }
69
- /**
70
- * Names of the branding URLs the operator set that failed their gate and were
71
- * replaced by a default. Lives beside the gates so the startup warning cannot
72
- * drift from them, and takes `unknown` fields for the same reason
73
- * `resolveBranding` does — a warning helper must never throw.
74
- */
75
- export function droppedBrandingUrls(branding) {
76
- if (!branding)
77
- return [];
78
- const resolved = resolveBranding(branding);
79
- const faviconHref = branding.favicon?.href;
80
- return [
81
- ...(isSetUrlValue(branding.productUrl) && !resolved.productUrl
82
- ? ["productUrl"]
83
- : []),
84
- ...(isSetUrlValue(branding.ownerUrl) && !resolved.ownerUrl
85
- ? ["ownerUrl"]
86
- : []),
87
- ...(isSetUrlValue(faviconHref) &&
88
- trimmedString(faviconHref) !== resolved.faviconHref
89
- ? ["favicon.href"]
90
- : []),
91
- ];
92
- }
93
- /**
94
- * True only for absolute `http:`/`https:` URLs. Downstream connectors control
95
- * their `authorizationUrl`, so a hostile/misconfigured one could hand back a
96
- * `javascript:` (or other) scheme; gate it before it can become an href.
97
- */
98
- function safeUrl(url, schemes) {
99
- if (typeof url !== "string")
100
- return false;
101
- try {
102
- return schemes.includes(new URL(url).protocol);
103
- }
104
- catch {
105
- return false;
106
- }
107
- }
108
- export function isSafeHttpUrl(url) {
109
- return safeUrl(url, ["http:", "https:"]);
110
- }
111
- /**
112
- * Only the second check's base; any origin works because the check is whether
113
- * the href stays on whatever origin it is resolved against. It is deliberately
114
- * never the sole gate: a value whose own authority equals this host (say
115
- * `//connecta.invalid/x`) would resolve to this exact origin and pass, so the
116
- * structural check below runs first and is what actually rejects `//host`.
117
- */
118
- const SAME_ORIGIN_PROBE = "https://connecta.invalid";
119
- /** Removed anywhere in a URL by the parser, so a gate must ignore them too. */
120
- const URL_STRIPPED_CHARS = /[\t\n\r]/g;
121
- /**
122
- * True for values allowed in the page's `<link rel="icon" href>`: an absolute
123
- * `http(s)` URL (an icon the operator hosts elsewhere) or a path rooted at this
124
- * origin. The relative carve-out is deliberate rather than accidental — the
125
- * default href is the relative `/favicon.svg`, which `isSafeHttpUrl` alone would
126
- * reject — and it is kept narrow on both ends.
127
- *
128
- * Root-relative only, because operator and OAuth callback pages sit at
129
- * different depths and a document-relative path would resolve differently.
130
- *
131
- * "Root-relative" is enforced structurally: exactly one leading `/` followed by
132
- * a character that is neither `/` nor `\`. Both of those would make the value an
133
- * authority (`//host`, and `/\host` because the URL parser folds `\` to `/` in
134
- * special schemes), pointing at an origin this server does not control. The test
135
- * runs on a copy with tab/newline/CR removed, since the parser strips those
136
- * anywhere and `/\t/host` would otherwise slip through as single-slash. The
137
- * origin comparison that follows is defense in depth, not the authority check —
138
- * on its own it would accept an authority that happened to equal the probe host.
139
- */
140
- export function isSafeIconHref(href) {
141
- if (typeof href !== "string")
142
- return false;
143
- if (isSafeHttpUrl(href))
144
- return true;
145
- if (!/^\/(?![/\\])/.test(href.replace(URL_STRIPPED_CHARS, "")))
146
- return false;
147
- try {
148
- return new URL(href, SAME_ORIGIN_PROBE).origin === SAME_ORIGIN_PROBE;
149
- }
150
- catch {
151
- return false;
152
- }
153
- }
154
- /** Absolute HTTPS gate for the `UiAuthConfig` URL fields documented in types.ts. */
155
- export function isSafeHttpsUrl(url) {
156
- return safeUrl(url, ["https:"]);
157
- }
158
- /**
159
- * Names of the `uiAuth` URLs an inbound-auth provider supplied that failed their
160
- * gate. Lives beside the gate for the same reason `droppedBrandingUrls` does: the
161
- * startup warning cannot then drift from what rendering actually drops. Every
162
- * field is read defensively rather than trusted, because a custom `InboundAuth`
163
- * is untyped at a JS call site — `isSafeHttpsUrl` takes `unknown`, and a
164
- * `uiAuth` that is not the clerk shape is reported as nothing to warn about.
165
- *
166
- * `frontendApiUrl` is required, so anything that fails its gate is a drop.
167
- * `signInUrl` and `signUpUrl` are optional, so only a value the operator
168
- * *supplied* and the gate then rejected is worth a warning — an unset field
169
- * took no default away from anyone. `isSetUrlValue` decides that, the same way
170
- * and for the same reasons it decides it for the branding URLs: a warning that
171
- * fires for one and not the other would be reporting on the field rather than
172
- * on the operator's intent. Rendering is not consulted for this: it drops on
173
- * the gate alone, and a blank string fails that gate too — it is simply not
174
- * *reported*, because a blank is indistinguishable from leaving the field
175
- * alone.
176
- */
177
- export function droppedUiAuthUrls(uiAuth) {
178
- if (!uiAuth || uiAuth.kind !== "clerk")
179
- return [];
180
- return [
181
- ...(isSafeHttpsUrl(uiAuth.frontendApiUrl) ? [] : ["uiAuth.frontendApiUrl"]),
182
- ...(isSetUrlValue(uiAuth.signInUrl) && !isSafeHttpsUrl(uiAuth.signInUrl)
183
- ? ["uiAuth.signInUrl"]
184
- : []),
185
- ...(isSetUrlValue(uiAuth.signUpUrl) && !isSafeHttpsUrl(uiAuth.signUpUrl)
186
- ? ["uiAuth.signUpUrl"]
187
- : []),
188
- ];
189
- }
190
20
  const OPERATOR_PAGE_LABELS = {
191
21
  connections: "Connections",
192
- credentials: "Credentials",
193
- tokens: "Access tokens",
194
22
  activity: "Activity",
195
23
  };
196
24
  export function operatorPageForPath(path) {
197
25
  if (path === "/")
198
26
  return "connections";
199
- if (path === "/credentials")
200
- return "credentials";
201
- if (path === "/tokens")
202
- return "tokens";
203
27
  if (path === "/activity")
204
28
  return "activity";
205
29
  return undefined;
@@ -223,151 +47,157 @@ export function credentialManagementCapability(input) {
223
47
  */
224
48
  export async function buildUiData(registry, baseUrl, serverInfo, credentialVault, activityEnabled = false, credentialManagement = credentialVault
225
49
  ? "available"
226
- : "requires_operator", defer, oauthManagement = false, discoveryConcurrency, accessTokenManagement = "not_configured", personalCredentialOwner) {
50
+ : "requires_operator", defer, oauthManagement = false, discoveryConcurrency, personalCredentialOwner, detailOptions = {}) {
227
51
  const requestScope = {};
228
52
  const connectorSet = registry.listConnectors();
229
53
  const concurrency = resolveDiscoveryConcurrency(discoveryConcurrency);
230
54
  const settled = await mapSettledWithConcurrency(connectorSet, concurrency, async (c) => {
231
- const drift = await registry.credentialDriftFor(c.id);
232
- const status = drift
233
- ? { state: "auth_required", message: drift }
234
- : await registry.statusFor(c.id, baseUrl, requestScope);
235
- if (status.authorizationUrl) {
236
- await registry.bindOAuthHandoff(c.id, status.authorizationUrl);
237
- }
238
- let tools = [];
239
- // `status()` on an unauthenticated remote connector starts OAuth and
240
- // stores its state + PKCE verifier. Probing listTools immediately
241
- // afterward would start a second flow, overwrite that state, and return
242
- // the now-stale first URL to the operator. Only inspect tools after
243
- // status proves the connector is healthy.
244
- if (status.state === "ok") {
245
- try {
246
- tools = (await registry.getTools(c.id, baseUrl, requestScope)).map((t) => ({
247
- name: t.name,
248
- address: `${c.id}.${t.name}`,
249
- ...(t.description !== undefined
250
- ? { description: t.description }
251
- : {}),
252
- }));
253
- }
254
- catch {
255
- // broken connector: reported via status "error", tools stay empty
256
- }
257
- }
258
- let credential;
259
- const mayManageAuth = c.authScope === "personal"
260
- ? Boolean(personalCredentialOwner)
261
- : oauthManagement;
262
- if (c.credential && credentialVault && mayManageAuth) {
263
- // One rule, shared with the test route: only the hook matching the
264
- // declared credential shape can run, so the button is offered only
265
- // where a click can succeed (src/credentials.ts).
266
- const testRule = credentialTestRule(c);
267
- const credentialFields = (metadata) => c.credential?.fields?.map((field) => {
268
- const fieldMetadata = metadata?.fields?.[field.name];
55
+ try {
56
+ return await withDeadline(async (outerSignal) => {
57
+ const drift = await registry.credentialDriftFor(c.id);
58
+ outerSignal.throwIfAborted();
59
+ let tools = [];
60
+ let status;
61
+ try {
62
+ status = await withDeadline(async (signal) => {
63
+ if (drift)
64
+ return { state: "auth_required", message: drift };
65
+ const current = await registry.statusFor(c.id, baseUrl, requestScope, { signal });
66
+ if (current.state === "ok" && !signal.aborted) {
67
+ try {
68
+ tools = (await registry.getTools(c.id, baseUrl, requestScope, { signal })).map(t => ({ name: t.name, address: `${c.id}.${t.name}`, ...(t.description ? { description: t.description } : {}) }));
69
+ }
70
+ catch { /* The registry owns the failed catalog observation. */ }
71
+ }
72
+ return current;
73
+ }, { timeoutMs: detailOptions.timeoutMs ?? 30_000, signal: outerSignal, timeoutError: new Error("Connection details timed out. Retry this connection.") });
74
+ }
75
+ catch (error) {
76
+ status = { state: "error", message: error instanceof Error ? error.message : "Connection details unavailable" };
77
+ }
78
+ let credential;
79
+ const mayManageAuth = detailOptions.mayManage?.(c.id) ?? (c.authScope === "personal" ? Boolean(personalCredentialOwner) : oauthManagement);
80
+ if (c.credential && credentialVault && mayManageAuth) {
81
+ // One rule, shared with the test route: only the hook matching the
82
+ // declared credential shape can run, so the button is offered only
83
+ // where a click can succeed (src/credentials.ts).
84
+ const testRule = credentialTestRule(c);
85
+ const credentialFields = (metadata) => c.credential?.fields?.map((field) => {
86
+ const fieldMetadata = metadata?.fields?.[field.name];
87
+ return {
88
+ name: field.name,
89
+ label: field.label,
90
+ ...(field.description
91
+ ? { description: field.description }
92
+ : {}),
93
+ ...(field.placeholder
94
+ ? { placeholder: field.placeholder }
95
+ : {}),
96
+ inputType: field.inputType ?? "password",
97
+ configured: Boolean(fieldMetadata),
98
+ ...(fieldMetadata
99
+ ? {
100
+ lastFour: fieldMetadata.lastFour,
101
+ updatedAt: fieldMetadata.updatedAt,
102
+ }
103
+ : {}),
104
+ };
105
+ });
106
+ const credentialCard = {
107
+ label: c.credential.label,
108
+ ...(c.credential.description
109
+ ? { description: c.credential.description }
110
+ : {}),
111
+ ...(c.credential.placeholder
112
+ ? { placeholder: c.credential.placeholder }
113
+ : {}),
114
+ };
115
+ try {
116
+ const metadata = await credentialVault.metadata(c.id, c.authScope === "personal" ? personalCredentialOwner : undefined);
117
+ const fields = credentialFields(metadata);
118
+ const shape = storedCredentialShape(c.credential, metadata?.fields ?? null);
119
+ credential = {
120
+ ...credentialCard,
121
+ ...(fields?.length ? { fields } : {}),
122
+ configured: shape.state === "valid",
123
+ removable: Boolean(metadata),
124
+ ...(metadata
125
+ ? {
126
+ lastFour: metadata.lastFour,
127
+ updatedAt: metadata.updatedAt,
128
+ }
129
+ : {}),
130
+ testable: testRule.mode !== null && shape.state !== "mismatch",
131
+ ...(shape.state === "mismatch"
132
+ ? { error: shape.message }
133
+ : {}),
134
+ // A dropped field leaves its secret in the vault, and the field
135
+ // list below only renders fields the connector still declares —
136
+ // so without this line there is nowhere an operator could see it.
137
+ ...(shape.state === "valid" && shape.undeclared.length
138
+ ? {
139
+ notice: describeUndeclaredCredentialFields(shape.undeclared),
140
+ }
141
+ : {}),
142
+ };
143
+ }
144
+ catch {
145
+ const fields = credentialFields();
146
+ credential = {
147
+ ...credentialCard,
148
+ ...(fields?.length ? { fields } : {}),
149
+ configured: false,
150
+ removable: true,
151
+ testable: testRule.mode !== null,
152
+ error: "Stored credential could not be read.",
153
+ };
154
+ }
155
+ }
156
+ outerSignal.throwIfAborted();
269
157
  return {
270
- name: field.name,
271
- label: field.label,
272
- ...(field.description
273
- ? { description: field.description }
158
+ id: c.id,
159
+ authScope: c.authScope ?? "shared",
160
+ ...(c.title ? { title: c.title } : {}),
161
+ ...(c.description !== undefined
162
+ ? { description: c.description }
274
163
  : {}),
275
- ...(field.placeholder
276
- ? { placeholder: field.placeholder }
277
- : {}),
278
- inputType: field.inputType ?? "password",
279
- configured: Boolean(fieldMetadata),
280
- ...(fieldMetadata
281
- ? {
282
- lastFour: fieldMetadata.lastFour,
283
- updatedAt: fieldMetadata.updatedAt,
284
- }
164
+ status: status.state,
165
+ ...(status.message ? { message: status.message } : {}),
166
+ toolCount: tools.length,
167
+ tools,
168
+ // Counts only, and only when a refresh in this runtime produced them.
169
+ // `Registry.statusFor` already rebuilt the report through
170
+ // `boundedCatalogDrift`, so what lands here cannot carry a name or a
171
+ // schema even if the plugin seam returned one.
172
+ ...(status.catalogDrift ? { catalogDrift: status.catalogDrift } : {}),
173
+ ...(status.catalogAccess
174
+ ? { catalogAccess: status.catalogAccess }
285
175
  : {}),
176
+ oauth: Boolean(c.startAuth && c.disconnectAuth),
177
+ ...(credential ? { credential } : {}),
286
178
  };
179
+ }, {
180
+ timeoutMs: detailOptions.timeoutMs ?? 30_000,
181
+ ...(detailOptions.signal ? { signal: detailOptions.signal } : {}),
182
+ timeoutError: new Error("Connection details timed out. Retry this connection."),
287
183
  });
288
- const credentialCard = {
289
- label: c.credential.label,
290
- ...(c.credential.description
291
- ? { description: c.credential.description }
292
- : {}),
293
- ...(c.credential.placeholder
294
- ? { placeholder: c.credential.placeholder }
295
- : {}),
184
+ }
185
+ catch (error) {
186
+ return {
187
+ id: c.id,
188
+ ...(c.title ? { title: c.title } : {}),
189
+ authScope: c.authScope ?? "shared",
190
+ status: "error",
191
+ oauth: Boolean(c.startAuth && c.disconnectAuth),
192
+ message: error instanceof Error ? error.message : "Connection details unavailable",
193
+ toolCount: 0,
194
+ tools: [],
296
195
  };
297
- try {
298
- const metadata = await credentialVault.metadata(c.id, c.authScope === "personal" ? personalCredentialOwner : undefined);
299
- const fields = credentialFields(metadata);
300
- const shape = storedCredentialShape(c.credential, metadata?.fields ?? null);
301
- credential = {
302
- ...credentialCard,
303
- ...(fields?.length ? { fields } : {}),
304
- configured: shape.state === "valid",
305
- removable: Boolean(metadata),
306
- ...(metadata
307
- ? {
308
- lastFour: metadata.lastFour,
309
- updatedAt: metadata.updatedAt,
310
- }
311
- : {}),
312
- testable: testRule.mode !== null && shape.state !== "mismatch",
313
- ...(shape.state === "mismatch"
314
- ? { error: shape.message }
315
- : {}),
316
- // A dropped field leaves its secret in the vault, and the field
317
- // list below only renders fields the connector still declares —
318
- // so without this line there is nowhere an operator could see it.
319
- ...(shape.state === "valid" && shape.undeclared.length
320
- ? {
321
- notice: describeUndeclaredCredentialFields(shape.undeclared),
322
- }
323
- : {}),
324
- };
325
- }
326
- catch {
327
- const fields = credentialFields();
328
- credential = {
329
- ...credentialCard,
330
- ...(fields?.length ? { fields } : {}),
331
- configured: false,
332
- removable: true,
333
- testable: testRule.mode !== null,
334
- error: "Stored credential could not be read.",
335
- };
336
- }
337
196
  }
338
- return {
339
- id: c.id,
340
- authScope: c.authScope ?? "shared",
341
- ...(c.title ? { title: c.title } : {}),
342
- ...(c.description !== undefined
343
- ? { description: c.description }
344
- : {}),
345
- status: status.state,
346
- ...(status.message ? { message: status.message } : {}),
347
- ...(isSafeHttpUrl(status.authorizationUrl)
348
- ? { authorizationUrl: status.authorizationUrl }
349
- : {}),
350
- toolCount: tools.length,
351
- tools,
352
- // Counts only, and only when a refresh in this runtime produced them.
353
- // `Registry.statusFor` already rebuilt the report through
354
- // `boundedCatalogDrift`, so what lands here cannot carry a name or a
355
- // schema even if the plugin seam returned one.
356
- ...(status.catalogDrift ? { catalogDrift: status.catalogDrift } : {}),
357
- ...(status.catalogAccess
358
- ? { catalogAccess: status.catalogAccess }
359
- : {}),
360
- ...(c.disconnectAuth &&
361
- c.startAuth &&
362
- (oauthManagement ||
363
- c.authScope === "personal" ||
364
- !personalCredentialOwner)
365
- ? { oauth: true }
366
- : {}),
367
- ...(credential ? { credential } : {}),
368
- };
197
+ finally {
198
+ await closeConnectorScope(c, registry.contextFor(c.id, baseUrl, requestScope), defer);
199
+ }
369
200
  });
370
- await mapSettledWithConcurrency(connectorSet, concurrency, (connector) => closeConnectorScope(connector, registry.contextFor(connector.id, baseUrl, requestScope), defer));
371
201
  const connectors = settled.map((result) => {
372
202
  if (result.status === "rejected")
373
203
  throw result.reason;
@@ -379,7 +209,6 @@ export async function buildUiData(registry, baseUrl, serverInfo, credentialVault
379
209
  connectors,
380
210
  activityEnabled,
381
211
  credentialManagement,
382
- accessTokenManagement,
383
212
  oauthManagement: oauthManagement || Boolean(personalCredentialOwner),
384
213
  };
385
214
  }
@@ -498,3 +327,33 @@ ${OPERATOR_UI_SCRIPT}</script>
498
327
  </body>
499
328
  </html>`;
500
329
  }
330
+ /** Mount the connection UI without enabling any storage or activity module. */
331
+ export function operatorUi(options = {}) {
332
+ return {
333
+ ...options,
334
+ reservedPaths: ["/", "/ui", "/ui/*", "/favicon.svg", "/favicon.ico"],
335
+ credentialHandoffUrl(baseUrl) {
336
+ return new URL("/", baseUrl).toString();
337
+ },
338
+ async handle(context) {
339
+ const routes = [
340
+ ...(context.opts.credentialVault ? [routeCredentials] : []),
341
+ routeOAuthManagement,
342
+ routeUi,
343
+ ];
344
+ for (const route of routes) {
345
+ const response = await route(context);
346
+ if (response) {
347
+ if (operatorPageForPath(context.path) || context.path === "/ui") {
348
+ response.headers.set("X-Frame-Options", "DENY");
349
+ if (!response.headers.has("Content-Security-Policy")) {
350
+ response.headers.set("Content-Security-Policy", "frame-ancestors 'none'");
351
+ }
352
+ }
353
+ return response;
354
+ }
355
+ }
356
+ return context.opts.activityModule?.handle(context) ?? null;
357
+ },
358
+ };
359
+ }
package/dist/version.d.ts CHANGED
@@ -4,4 +4,4 @@
4
4
  * a bump that forgets this file fails the build rather than shipping a stale
5
5
  * version to `/health` and to downstream MCP handshakes.
6
6
  */
7
- export declare const CONNECTA_VERSION = "0.22.3";
7
+ export declare const CONNECTA_VERSION = "0.24.0";