@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.
- package/AGENTS.md +5 -0
- package/CHANGELOG.md +82 -0
- package/README.md +19 -11
- package/dist/activity-friction.d.ts +3 -0
- package/dist/activity-friction.js +19 -0
- package/dist/activity.d.ts +11 -2
- package/dist/activity.js +15 -19
- package/dist/auth/downstream-oauth.d.ts +2 -1
- package/dist/auth/downstream-oauth.js +10 -1
- package/dist/branding.d.ts +67 -0
- package/dist/branding.js +176 -0
- package/dist/catalog-service.d.ts +1 -6
- package/dist/catalog-service.js +3 -53
- package/dist/connectors/remote-mcp.js +3 -5
- package/dist/credential-contract.d.ts +24 -0
- package/dist/credential-contract.js +1 -0
- package/dist/credential-rules.d.ts +85 -0
- package/dist/credential-rules.js +107 -0
- package/dist/credentials.d.ts +4 -100
- package/dist/credentials.js +3 -107
- package/dist/errors.d.ts +1 -1
- package/dist/execute.d.ts +5 -52
- package/dist/execute.js +40 -347
- package/dist/executors/quickjs-protocol.d.ts +0 -7
- package/dist/executors/quickjs-protocol.js +2 -10
- package/dist/executors/quickjs.js +1 -1
- package/dist/index.d.ts +23 -56
- package/dist/index.js +30 -58
- package/dist/invocation.d.ts +0 -33
- package/dist/invocation.js +56 -124
- package/dist/meta-tools.d.ts +7 -6
- package/dist/meta-tools.js +14 -21
- package/dist/module-contracts.d.ts +19 -0
- package/dist/module-contracts.js +1 -0
- package/dist/operator-ui/generated.js +2 -2
- package/dist/operator-ui/model.d.ts +6 -3
- package/dist/operator-ui/view.d.ts +2 -18
- package/dist/operator-ui/view.js +3 -20
- package/dist/registry.d.ts +4 -1
- package/dist/registry.js +8 -6
- package/dist/routes/activity.js +1 -1
- package/dist/routes/credentials.js +5 -2
- package/dist/routes/mcp.js +7 -51
- package/dist/routes/oauth-management.d.ts +2 -0
- package/dist/routes/oauth-management.js +108 -0
- package/dist/routes/oauth.d.ts +0 -1
- package/dist/routes/oauth.js +21 -121
- package/dist/routes/shared.d.ts +19 -17
- package/dist/routes/shared.js +48 -44
- package/dist/routes/ui.js +36 -33
- package/dist/server.d.ts +1 -2
- package/dist/server.js +7 -45
- package/dist/skills.d.ts +1 -1
- package/dist/skills.js +55 -19
- package/dist/types.d.ts +3 -15
- package/dist/ui.d.ts +15 -70
- package/dist/ui.js +176 -317
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/documentation/architecture.md +31 -26
- package/documentation/auth.md +65 -111
- package/documentation/call-admission.md +10 -11
- package/documentation/cloudflare.md +1 -1
- package/documentation/code-mode.md +65 -260
- package/documentation/connectors.md +8 -6
- package/documentation/linear.md +1 -1
- package/documentation/meta-tools.md +37 -18
- package/documentation/mixpanel.md +1 -1
- package/documentation/notion.md +2 -2
- package/documentation/operations.md +19 -23
- package/documentation/operator-ui.md +82 -104
- package/documentation/optional-modules-upgrade.md +243 -0
- package/documentation/provider-conventions.md +7 -5
- package/documentation/revenuecat.md +1 -1
- package/documentation/storage-and-credentials.md +59 -40
- package/documentation/stripe.md +1 -1
- package/documentation/upgrading.md +83 -8
- package/ethos.md +36 -43
- package/examples/worker/AGENTS.md +3 -1
- package/examples/worker/README.md +68 -84
- package/examples/worker/src/d1-activity.ts +1 -1
- package/examples/worker/src/index.ts +11 -6
- package/package.json +18 -2
- package/templates/node/AGENTS.md +8 -6
- package/templates/node/README.md +56 -67
- package/templates/node/package.json +1 -1
- package/templates/node/src/file-activity.ts +1 -1
- package/templates/node/src/index.ts +11 -12
- package/dist/access-tokens.d.ts +0 -31
- package/dist/access-tokens.js +0 -236
- package/dist/apps-shell.d.ts +0 -37
- package/dist/apps-shell.js +0 -174
- package/dist/routes/access-tokens.d.ts +0 -6
- package/dist/routes/access-tokens.js +0 -83
package/dist/ui.d.ts
CHANGED
|
@@ -1,75 +1,12 @@
|
|
|
1
|
-
import type {
|
|
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
|
|
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
|
|
7
|
-
|
|
8
|
-
export
|
|
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,
|
|
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 {
|
|
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
|
-
|
|
8
|
-
export
|
|
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,
|
|
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
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
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
|
-
|
|
271
|
-
|
|
272
|
-
...(
|
|
273
|
-
|
|
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
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
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
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
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
|
-
|
|
339
|
-
|
|
340
|
-
|
|
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