@zackbart/connecta 0.24.4 → 0.26.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 +43 -12
- package/CHANGELOG.md +524 -0
- package/README.md +32 -15
- package/SECURITY.md +6 -5
- package/bin/connecta.mjs +5 -1
- package/dist/activity.d.ts +10 -3
- package/dist/activity.js +10 -1
- package/dist/advertised-schema.js +53 -0
- package/dist/artifacts/connector.d.ts +55 -0
- package/dist/artifacts/connector.js +724 -0
- package/dist/artifacts/document.js +76 -0
- package/dist/artifacts/guide.js +140 -0
- package/dist/artifacts/html-scan.js +329 -0
- package/dist/artifacts/json.js +68 -0
- package/dist/artifacts/kv-store.d.ts +20 -0
- package/dist/artifacts/kv-store.js +140 -0
- package/dist/artifacts/markdown.js +449 -0
- package/dist/artifacts/module.d.ts +37 -0
- package/dist/artifacts/module.js +60 -0
- package/dist/artifacts/operations.d.ts +195 -0
- package/dist/artifacts/operations.js +944 -0
- package/dist/artifacts/refresh.d.ts +53 -0
- package/dist/artifacts/refresh.js +235 -0
- package/dist/artifacts/routes.js +194 -0
- package/dist/artifacts/types.d.ts +214 -0
- package/dist/artifacts/types.js +22 -0
- package/dist/artifacts/validate.d.ts +62 -0
- package/dist/artifacts/validate.js +652 -0
- package/dist/artifacts.d.ts +6 -0
- package/dist/artifacts.js +9 -0
- package/dist/auth/downstream-oauth.js +490 -106
- package/dist/call-admission.js +139 -113
- package/dist/catalog-service.js +152 -106
- package/dist/catalog.js +297 -38
- package/dist/concurrency.js +24 -23
- package/dist/connector-access.d.ts +18 -4
- package/dist/connector-access.js +65 -20
- package/dist/connector-caller.d.ts +8 -0
- package/dist/connector-caller.js +19 -0
- package/dist/connector-scope.js +6 -37
- package/dist/connectors/api.d.ts +2 -2
- package/dist/connectors/remote-mcp.d.ts +6 -5
- package/dist/connectors/remote-mcp.js +330 -240
- package/dist/credential-contract.d.ts +9 -0
- package/dist/credential-rules.js +2 -2
- package/dist/credentials.d.ts +6 -1
- package/dist/credentials.js +45 -3
- package/dist/errors.d.ts +16 -1
- package/dist/errors.js +28 -0
- package/dist/execute.js +745 -282
- package/dist/executor-admission.d.ts +11 -1
- package/dist/executor-admission.js +145 -63
- package/dist/executors/quickjs-runtime.js +4 -2
- package/dist/executors/quickjs.js +255 -212
- package/dist/fix-prompt.js +37 -0
- package/dist/index.d.ts +72 -7
- package/dist/index.js +281 -26
- package/dist/invocation.js +371 -286
- package/dist/meta-tools.js +213 -94
- package/dist/module-contracts.d.ts +21 -1
- package/dist/oauth-callback-outcome.js +94 -0
- package/dist/oauth-sealing.js +29 -0
- package/dist/operator-ui/fix-prompts.js +87 -0
- package/dist/operator-ui/generated.js +2 -2
- package/dist/operator-ui/model.d.ts +103 -1
- package/dist/operator-ui/setup-commands.js +53 -0
- package/dist/operator-ui/view.js +128 -3
- package/dist/program-source.js +321 -0
- package/dist/providers/notion.js +2 -2
- package/dist/providers/revenuecat.js +32 -5
- package/dist/providers/stripe.d.ts +3 -2
- package/dist/providers/vercel.js +1 -1
- package/dist/registry.d.ts +43 -7
- package/dist/registry.js +434 -288
- package/dist/resumable.js +1291 -0
- package/dist/routes/activity.js +74 -154
- package/dist/routes/actor-labels.js +114 -0
- package/dist/routes/credentials.js +196 -163
- package/dist/routes/mcp.js +228 -112
- package/dist/routes/oauth-management.js +73 -71
- package/dist/routes/oauth.js +44 -16
- package/dist/routes/operator.js +83 -0
- package/dist/routes/shared.d.ts +25 -1
- package/dist/routes/shared.js +20 -2
- package/dist/routes/ui-data.js +211 -0
- package/dist/routes/ui.js +76 -35
- package/dist/run-journal.js +347 -0
- package/dist/runtime/admission.js +43 -0
- package/dist/runtime/call-admission.js +67 -0
- package/dist/runtime/connector-scope.js +72 -0
- package/dist/runtime/run.js +174 -0
- package/dist/runtime/services.js +130 -0
- package/dist/runtime/shared-read.js +106 -0
- package/dist/runtime/storage.js +66 -0
- package/dist/server.js +137 -89
- package/dist/skills.js +28 -9
- package/dist/storage/file.js +39 -2
- package/dist/storage/memory.js +28 -16
- package/dist/timeout.js +39 -43
- package/dist/tool-safety.d.ts +27 -1
- package/dist/tool-safety.js +31 -0
- package/dist/types.d.ts +45 -7
- package/dist/ui.d.ts +25 -4
- package/dist/ui.js +77 -167
- package/dist/url-safety.js +157 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/documentation/architecture.md +367 -16
- package/documentation/auth.md +163 -2
- package/documentation/code-mode.md +274 -56
- package/documentation/meta-tools.md +200 -44
- package/ethos.md +46 -47
- package/examples/worker/AGENTS.md +6 -0
- package/examples/worker/README.md +224 -14
- package/examples/worker/src/d1-activity-row.ts +14 -2
- package/examples/worker/src/d1-activity.ts +4 -2
- package/examples/worker/src/d1-storage.ts +116 -0
- package/examples/worker/src/index.ts +42 -7
- package/examples/worker/src/r2-artifact-blobs.ts +29 -0
- package/examples/worker/wrangler.jsonc +21 -5
- package/package.json +16 -4
- package/templates/node/AGENTS.md +9 -2
- package/templates/node/README.md +31 -3
- package/templates/node/package.json +1 -1
- package/templates/node/src/index.ts +27 -0
- package/dist/auth/downstream-oauth.d.ts +0 -200
- package/dist/catalog-fingerprint.d.ts +0 -19
- package/dist/catalog-limits.d.ts +0 -9
- package/dist/catalog-service.d.ts +0 -263
- package/dist/catalog.d.ts +0 -87
- package/dist/concurrency.d.ts +0 -3
- package/dist/connectors/guarded-fetch.d.ts +0 -96
- package/dist/credential-rules.d.ts +0 -85
- package/dist/execute.d.ts +0 -154
- package/dist/executor-result.d.ts +0 -10
- package/dist/executors/quickjs-child.d.ts +0 -1
- package/dist/executors/quickjs-protocol.d.ts +0 -67
- package/dist/favicon.d.ts +0 -1
- package/dist/identity.d.ts +0 -4
- package/dist/invocation.d.ts +0 -74
- package/dist/mcp-result.d.ts +0 -8
- package/dist/meta-tools.d.ts +0 -129
- package/dist/operator-ui/generated.d.ts +0 -2
- package/dist/operator-ui/view.d.ts +0 -188
- package/dist/result-shapes.d.ts +0 -13
- package/dist/retired-toolkits.d.ts +0 -2
- package/dist/routes/activity.d.ts +0 -2
- package/dist/routes/credentials.d.ts +0 -2
- package/dist/routes/mcp.d.ts +0 -10
- package/dist/routes/oauth-management.d.ts +0 -2
- package/dist/routes/oauth.d.ts +0 -2
- package/dist/routes/ui.d.ts +0 -2
- package/dist/server.d.ts +0 -9
- package/dist/skills.d.ts +0 -44
- package/dist/timeout.d.ts +0 -21
package/dist/types.d.ts
CHANGED
|
@@ -12,6 +12,26 @@ export interface KVStorage {
|
|
|
12
12
|
* subsystems that need independent, enumerable records require it explicitly.
|
|
13
13
|
*/
|
|
14
14
|
list?(prefix: string): Promise<string[]>;
|
|
15
|
+
/**
|
|
16
|
+
* Atomic compare-and-set: write `next` only if the key's current value is
|
|
17
|
+
* exactly `expected`, and report whether the write happened. `expected:
|
|
18
|
+
* null` means "absent", and an expired entry is absent. `next: null`
|
|
19
|
+
* deletes; otherwise `next` is stored with `options.ttlSeconds`, or with no
|
|
20
|
+
* expiry when that is omitted, exactly as `set` would. On `false` nothing
|
|
21
|
+
* changed.
|
|
22
|
+
*
|
|
23
|
+
* Atomic means linearizable against every other operation on the same
|
|
24
|
+
* store, `set` and `delete` included: of N concurrent `compareAndSet(key,
|
|
25
|
+
* null, value)` claims on an absent key, exactly one returns true.
|
|
26
|
+
*
|
|
27
|
+
* Optional, because not every backend can promise it: an eventually
|
|
28
|
+
* consistent store such as Cloudflare Workers KV must omit the method
|
|
29
|
+
* rather than emulate it with a read followed by a write. Subsystems that
|
|
30
|
+
* need an atomic claim require it explicitly.
|
|
31
|
+
*/
|
|
32
|
+
compareAndSet?(key: string, expected: string | null, next: string | null, options?: {
|
|
33
|
+
ttlSeconds?: number;
|
|
34
|
+
}): Promise<boolean>;
|
|
15
35
|
}
|
|
16
36
|
export interface Logger {
|
|
17
37
|
debug(...args: unknown[]): void;
|
|
@@ -106,7 +126,7 @@ export interface ConnectorCredentialFieldConfig {
|
|
|
106
126
|
name: string;
|
|
107
127
|
/** Short field label, e.g. "Account email". */
|
|
108
128
|
label: string;
|
|
109
|
-
/** Plain-language guidance shown in
|
|
129
|
+
/** Plain-language guidance shown in the connection's credential form in the operator UI. Never include the credential itself. */
|
|
110
130
|
description?: string;
|
|
111
131
|
/** Input placeholder, e.g. "you@example.com". */
|
|
112
132
|
placeholder?: string;
|
|
@@ -117,7 +137,7 @@ export interface ConnectorCredentialFieldConfig {
|
|
|
117
137
|
export interface ConnectorCredentialConfig {
|
|
118
138
|
/** Short group or field label, e.g. "API token" or "Service credentials". */
|
|
119
139
|
label: string;
|
|
120
|
-
/** Plain-language guidance shown in
|
|
140
|
+
/** Plain-language guidance shown in the connection's credential form in the operator UI. Never include the credential itself. */
|
|
121
141
|
description?: string;
|
|
122
142
|
/** Password-field placeholder, e.g. "Paste API token". */
|
|
123
143
|
placeholder?: string;
|
|
@@ -129,6 +149,11 @@ export interface ConnectorCredentialConfig {
|
|
|
129
149
|
}
|
|
130
150
|
export interface CredentialTestResult {
|
|
131
151
|
ok: boolean;
|
|
152
|
+
/**
|
|
153
|
+
* Detail for the deployment's log. The operator page shows fixed copy for
|
|
154
|
+
* `ok` and never this text, since a downstream's reply quoted here can
|
|
155
|
+
* quote the credential it rejected.
|
|
156
|
+
*/
|
|
132
157
|
message?: string;
|
|
133
158
|
}
|
|
134
159
|
export interface ConnectorContext {
|
|
@@ -142,7 +167,7 @@ export interface ConnectorContext {
|
|
|
142
167
|
/**
|
|
143
168
|
* Read-only access to this connector's human-managed credential. Present
|
|
144
169
|
* only when the connector declares `credential` and the deployment configures
|
|
145
|
-
* `
|
|
170
|
+
* `vault`.
|
|
146
171
|
*/
|
|
147
172
|
credential?: ConnectorCredentialAccess;
|
|
148
173
|
/**
|
|
@@ -220,12 +245,23 @@ export interface Connector {
|
|
|
220
245
|
title?: string;
|
|
221
246
|
/** How call_tool wraps results. "mcp" passes the content array through; anything else is JSON-wrapped. */
|
|
222
247
|
kind?: "mcp" | "api";
|
|
248
|
+
/**
|
|
249
|
+
* This connector's own default for its tools that are not explicitly
|
|
250
|
+
* read-only, inside `execute_code`: `"never"` lets a program call them
|
|
251
|
+
* without pausing for approval. Reserved for connectors connecta ships
|
|
252
|
+
* whose writes are cheap to undo (the planned artifacts connector, whose
|
|
253
|
+
* every write is a new immutable version); a deployment's own connectors
|
|
254
|
+
* leave it unset and exempt through `execute.approval`, which overrides
|
|
255
|
+
* this either way. Never read from a downstream catalog — a server cannot
|
|
256
|
+
* exempt its own tools — and never makes a tool read-only anywhere else.
|
|
257
|
+
*/
|
|
258
|
+
approval?: "never";
|
|
223
259
|
description?: string;
|
|
224
260
|
/**
|
|
225
261
|
* Max inline result size (bytes) for this connector's tools before
|
|
226
262
|
* call_tool truncates and stashes the full text for get_result
|
|
227
263
|
* paging. Overrides `ConnectaConfig.calls.maxResultBytes`;
|
|
228
|
-
* omit to inherit it (which itself defaults to
|
|
264
|
+
* omit to inherit it (which itself defaults to 24_000). Must be a whole
|
|
229
265
|
* number of bytes >= 1; anything else warns at startup and is ignored, so
|
|
230
266
|
* the connector inherits the deployment-wide cap.
|
|
231
267
|
*/
|
|
@@ -248,11 +284,11 @@ export interface Connector {
|
|
|
248
284
|
* configuration; no runtime registration or shared mutable copy exists.
|
|
249
285
|
*/
|
|
250
286
|
usageGuide?: string | ConnectorUsageGuide;
|
|
251
|
-
/** Optional human-managed credential slot rendered
|
|
287
|
+
/** Optional human-managed credential slot rendered in the connection in the operator UI. */
|
|
252
288
|
credential?: ConnectorCredentialConfig;
|
|
253
|
-
/** Optional server-side check used by
|
|
289
|
+
/** Optional server-side check used by the connection's Test action in the operator UI. */
|
|
254
290
|
testCredential?(value: string, ctx: ConnectorContext): Promise<CredentialTestResult>;
|
|
255
|
-
/** Optional multi-field credential check used by
|
|
291
|
+
/** Optional multi-field credential check used by the connection's Test action in the operator UI. */
|
|
256
292
|
testCredentials?(values: ConnectorCredentialValues, ctx: ConnectorContext): Promise<CredentialTestResult>;
|
|
257
293
|
/**
|
|
258
294
|
* Statically-known tool defs, exposed by in-code connectors (`api()`) for
|
|
@@ -374,6 +410,8 @@ export interface AdmissionSnapshot {
|
|
|
374
410
|
maxQueueSize: number;
|
|
375
411
|
queueTimeoutMs: number;
|
|
376
412
|
retryAfterMs: number;
|
|
413
|
+
/** Hard admitted-request lifetime, present only on the /mcp pool. */
|
|
414
|
+
maxDurationMs?: number;
|
|
377
415
|
active: number;
|
|
378
416
|
queued: number;
|
|
379
417
|
closed: boolean;
|
package/dist/ui.d.ts
CHANGED
|
@@ -1,14 +1,31 @@
|
|
|
1
1
|
import type { OperatorSurface } from "./module-contracts.js";
|
|
2
2
|
import type { CredentialVault } from "./credential-contract.js";
|
|
3
3
|
import { type DeferredWork } from "./connector-scope.js";
|
|
4
|
-
import { type CredentialManagementCapability, type UiData } from "./operator-ui/model.js";
|
|
4
|
+
import { type CredentialManagementCapability, type UiData, type UiProblem, type UiToolSafety } from "./operator-ui/model.js";
|
|
5
|
+
import { type ApprovalPolicy } from "./tool-safety.js";
|
|
5
6
|
import type { RegistryView } from "./registry.js";
|
|
6
|
-
import type { ConnectaBranding, UiAuthConfig } from "./types.js";
|
|
7
|
+
import type { ConnectaBranding, Connector, ConnectorStatus, ToolDef, UiAuthConfig } from "./types.js";
|
|
7
8
|
export { filterUiConnectors, type CredentialManagementCapability, type UiConnector, type UiData, } from "./operator-ui/model.js";
|
|
8
9
|
export { CONNECTA_FAVICON_SVG, resolveBranding, isSafeHttpUrl, isSafeHttpsUrl, isSafeIconHref } from "./branding.js";
|
|
9
|
-
export type OperatorPage = "connections" | "activity";
|
|
10
|
+
export type OperatorPage = "connections" | "activity" | "artifacts" | "artifact";
|
|
10
11
|
export declare function operatorPageForPath(path: string): OperatorPage | undefined;
|
|
11
12
|
export declare function operatorPageTitle(page: OperatorPage, configuredTitle: string): string;
|
|
13
|
+
/**
|
|
14
|
+
* The badge a tool earns, from the predicates core enforces with. Computed
|
|
15
|
+
* here so the page reports the rule rather than restating it. `exempt` is the
|
|
16
|
+
* config exemption (#566), which `isApprovalExempt` already refuses to grant
|
|
17
|
+
* a read-only tool.
|
|
18
|
+
*/
|
|
19
|
+
export declare function uiToolSafety(definition: ToolDef, exempt?: boolean): UiToolSafety;
|
|
20
|
+
/**
|
|
21
|
+
* The fix-prompt key for a connector that is not usable. Chosen from the
|
|
22
|
+
* status state and the connector's declared auth shape — configuration, never
|
|
23
|
+
* the status message, which can carry a downstream error body.
|
|
24
|
+
*/
|
|
25
|
+
export declare function uiProblemFor(connector: Pick<Connector, "credential" | "startAuth">, status: ConnectorStatus["state"], observed: {
|
|
26
|
+
credentialDrift: boolean;
|
|
27
|
+
catalogFailed: boolean;
|
|
28
|
+
}): UiProblem | undefined;
|
|
12
29
|
export declare function credentialManagementCapability(input: {
|
|
13
30
|
eligibleOperator: boolean;
|
|
14
31
|
hasCredentialSlots: boolean;
|
|
@@ -26,12 +43,16 @@ export declare function buildUiData(registry: RegistryView, baseUrl: string, ser
|
|
|
26
43
|
mayManage?: (id: string) => boolean;
|
|
27
44
|
timeoutMs?: number;
|
|
28
45
|
signal?: AbortSignal;
|
|
46
|
+
approval?: ApprovalPolicy;
|
|
29
47
|
}): Promise<UiData>;
|
|
30
48
|
/**
|
|
31
49
|
* Every operator page serves this same data-free shell. Connector, credential,
|
|
32
50
|
* and activity data arrives only through the authenticated `/ui/*` APIs.
|
|
33
51
|
*/
|
|
34
|
-
export declare function renderUiHtml(uiAuth?: UiAuthConfig, mcpUrl?: string, branding?: ConnectaBranding, nonce?: string, page?: OperatorPage
|
|
52
|
+
export declare function renderUiHtml(uiAuth?: UiAuthConfig, mcpUrl?: string, branding?: ConnectaBranding, nonce?: string, page?: OperatorPage, options?: {
|
|
53
|
+
/** Where the Connections page lives, when this shell is on another origin. */
|
|
54
|
+
homeUrl?: string;
|
|
55
|
+
}): string;
|
|
35
56
|
/** Mount the connection UI without enabling any storage or activity module. */
|
|
36
57
|
export declare function operatorUi(options?: {
|
|
37
58
|
branding?: ConnectaBranding;
|
package/dist/ui.js
CHANGED
|
@@ -1,12 +1,10 @@
|
|
|
1
1
|
import { routeUi } from "./routes/ui.js";
|
|
2
2
|
import { routeCredentials } from "./routes/credentials.js";
|
|
3
3
|
import { routeOAuthManagement } from "./routes/oauth-management.js";
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import { mapSettledWithConcurrency, resolveDiscoveryConcurrency, } from "./concurrency.js";
|
|
4
|
+
import { uiData } from "./routes/ui-data.js";
|
|
5
|
+
import { runEdge } from "./runtime/run.js";
|
|
6
|
+
import { isExplicitlyReadOnly } from "./tool-safety.js";
|
|
8
7
|
import { OPERATOR_UI_CSS, OPERATOR_UI_SCRIPT, } from "./operator-ui/generated.js";
|
|
9
|
-
import { CONNECTA_VERSION } from "./version.js";
|
|
10
8
|
export { filterUiConnectors, } from "./operator-ui/model.js";
|
|
11
9
|
import { resolveBranding, isSafeHttpsUrl, themeCss } from "./branding.js";
|
|
12
10
|
export { CONNECTA_FAVICON_SVG, resolveBranding, isSafeHttpUrl, isSafeHttpsUrl, isSafeIconHref } from "./branding.js";
|
|
@@ -20,17 +18,57 @@ function stringForInlineScript(value) {
|
|
|
20
18
|
const OPERATOR_PAGE_LABELS = {
|
|
21
19
|
connections: "Connections",
|
|
22
20
|
activity: "Activity",
|
|
21
|
+
artifacts: "Artifacts",
|
|
22
|
+
artifact: "Artifact",
|
|
23
23
|
};
|
|
24
|
+
/** `/artifacts/<id>` and its snapshots, `/artifacts/<id>/v/<version>`. */
|
|
25
|
+
const ARTIFACT_PAGE = /^\/artifacts\/[a-z0-9](?:[a-z0-9-]{0,62}[a-z0-9])?(?:\/v\/\d{1,9})?$/;
|
|
24
26
|
export function operatorPageForPath(path) {
|
|
25
27
|
if (path === "/")
|
|
26
28
|
return "connections";
|
|
27
29
|
if (path === "/activity")
|
|
28
30
|
return "activity";
|
|
31
|
+
if (path === "/artifacts")
|
|
32
|
+
return "artifacts";
|
|
33
|
+
if (ARTIFACT_PAGE.test(path))
|
|
34
|
+
return "artifact";
|
|
29
35
|
return undefined;
|
|
30
36
|
}
|
|
37
|
+
/** Everything artifact pages own: shells, their API, and the frame. */
|
|
38
|
+
function isArtifactPath(path) {
|
|
39
|
+
return path === "/artifacts" || path.startsWith("/artifacts/");
|
|
40
|
+
}
|
|
31
41
|
export function operatorPageTitle(page, configuredTitle) {
|
|
32
42
|
return `${OPERATOR_PAGE_LABELS[page]} — ${configuredTitle}`;
|
|
33
43
|
}
|
|
44
|
+
/**
|
|
45
|
+
* The badge a tool earns, from the predicates core enforces with. Computed
|
|
46
|
+
* here so the page reports the rule rather than restating it. `exempt` is the
|
|
47
|
+
* config exemption (#566), which `isApprovalExempt` already refuses to grant
|
|
48
|
+
* a read-only tool.
|
|
49
|
+
*/
|
|
50
|
+
export function uiToolSafety(definition, exempt = false) {
|
|
51
|
+
if (isExplicitlyReadOnly(definition))
|
|
52
|
+
return "runs_in_programs";
|
|
53
|
+
return exempt ? "exempt" : "needs_approval";
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* The fix-prompt key for a connector that is not usable. Chosen from the
|
|
57
|
+
* status state and the connector's declared auth shape — configuration, never
|
|
58
|
+
* the status message, which can carry a downstream error body.
|
|
59
|
+
*/
|
|
60
|
+
export function uiProblemFor(connector, status, observed) {
|
|
61
|
+
if (observed.credentialDrift)
|
|
62
|
+
return "credential_mismatch";
|
|
63
|
+
if (status === "error")
|
|
64
|
+
return "connector_unavailable";
|
|
65
|
+
if (status === "auth_required") {
|
|
66
|
+
if (connector.startAuth)
|
|
67
|
+
return "oauth_required";
|
|
68
|
+
return connector.credential ? "credential_required" : "auth_required";
|
|
69
|
+
}
|
|
70
|
+
return observed.catalogFailed ? "catalog_failed" : undefined;
|
|
71
|
+
}
|
|
34
72
|
export function credentialManagementCapability(input) {
|
|
35
73
|
if (!input.eligibleOperator)
|
|
36
74
|
return "requires_operator";
|
|
@@ -48,169 +86,17 @@ export function credentialManagementCapability(input) {
|
|
|
48
86
|
export async function buildUiData(registry, baseUrl, serverInfo, credentialVault, activityEnabled = false, credentialManagement = credentialVault
|
|
49
87
|
? "available"
|
|
50
88
|
: "requires_operator", defer, oauthManagement = false, discoveryConcurrency, personalCredentialOwner, detailOptions = {}) {
|
|
51
|
-
|
|
52
|
-
const connectorSet = registry.listConnectors();
|
|
53
|
-
const concurrency = resolveDiscoveryConcurrency(discoveryConcurrency);
|
|
54
|
-
const settled = await mapSettledWithConcurrency(connectorSet, concurrency, async (c) => {
|
|
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();
|
|
157
|
-
return {
|
|
158
|
-
id: c.id,
|
|
159
|
-
authScope: c.authScope ?? "shared",
|
|
160
|
-
...(c.title ? { title: c.title } : {}),
|
|
161
|
-
...(c.description !== undefined
|
|
162
|
-
? { description: c.description }
|
|
163
|
-
: {}),
|
|
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 }
|
|
175
|
-
: {}),
|
|
176
|
-
oauth: Boolean(c.startAuth && c.disconnectAuth),
|
|
177
|
-
...(credential ? { credential } : {}),
|
|
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."),
|
|
183
|
-
});
|
|
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: [],
|
|
195
|
-
};
|
|
196
|
-
}
|
|
197
|
-
finally {
|
|
198
|
-
await closeConnectorScope(c, registry.contextFor(c.id, baseUrl, requestScope), defer);
|
|
199
|
-
}
|
|
200
|
-
});
|
|
201
|
-
const connectors = settled.map((result) => {
|
|
202
|
-
if (result.status === "rejected")
|
|
203
|
-
throw result.reason;
|
|
204
|
-
return result.value;
|
|
205
|
-
});
|
|
206
|
-
return {
|
|
89
|
+
return runEdge(uiData(registry, baseUrl, {
|
|
207
90
|
serverInfo,
|
|
208
|
-
|
|
209
|
-
connectors,
|
|
91
|
+
credentialVault,
|
|
210
92
|
activityEnabled,
|
|
211
93
|
credentialManagement,
|
|
212
|
-
|
|
213
|
-
|
|
94
|
+
defer,
|
|
95
|
+
oauthManagement,
|
|
96
|
+
discoveryConcurrency,
|
|
97
|
+
personalCredentialOwner,
|
|
98
|
+
...detailOptions,
|
|
99
|
+
}));
|
|
214
100
|
}
|
|
215
101
|
function escapeHtmlAttr(value) {
|
|
216
102
|
return value
|
|
@@ -229,7 +115,7 @@ function jsonForInlineScript(value) {
|
|
|
229
115
|
* Every operator page serves this same data-free shell. Connector, credential,
|
|
230
116
|
* and activity data arrives only through the authenticated `/ui/*` APIs.
|
|
231
117
|
*/
|
|
232
|
-
export function renderUiHtml(uiAuth, mcpUrl = "/mcp", branding, nonce, page = "connections") {
|
|
118
|
+
export function renderUiHtml(uiAuth, mcpUrl = "/mcp", branding, nonce, page = "connections", options = {}) {
|
|
233
119
|
const clerk = uiAuth?.kind === "clerk" ? uiAuth : undefined;
|
|
234
120
|
// The Clerk loader's origin. A value that fails the gate is dropped rather
|
|
235
121
|
// than escaped into the page: the loader tag is simply not emitted, the gate
|
|
@@ -327,6 +213,7 @@ ${clerkScript}
|
|
|
327
213
|
const AUTH = ${jsonForInlineScript(auth)};
|
|
328
214
|
const MCP_URL = ${jsonForInlineScript(mcpUrl)};
|
|
329
215
|
const INITIAL_PAGE = ${jsonForInlineScript(page)};
|
|
216
|
+
const HOME_URL = ${jsonForInlineScript(options.homeUrl ?? "/")};
|
|
330
217
|
const TITLE_SUFFIX = ${jsonForInlineScript(brand.pageTitle)};
|
|
331
218
|
const PRODUCT_NAME = ${stringForInlineScript(brand.productName)};
|
|
332
219
|
const PRODUCT_DESCRIPTION = ${stringForInlineScript(brand.description)};
|
|
@@ -335,15 +222,38 @@ ${OPERATOR_UI_SCRIPT}</script>
|
|
|
335
222
|
</body>
|
|
336
223
|
</html>`;
|
|
337
224
|
}
|
|
225
|
+
function ownsOperatorPath(reserved, path) {
|
|
226
|
+
if (path === "/activity")
|
|
227
|
+
return true;
|
|
228
|
+
return reserved.some((pattern) => pattern.endsWith("/*")
|
|
229
|
+
? path.startsWith(pattern.slice(0, -1))
|
|
230
|
+
: path === pattern);
|
|
231
|
+
}
|
|
338
232
|
/** Mount the connection UI without enabling any storage or activity module. */
|
|
339
233
|
export function operatorUi(options = {}) {
|
|
234
|
+
const reservedPaths = ["/", "/ui", "/ui/*", "/favicon.svg", "/favicon.ico"];
|
|
340
235
|
return {
|
|
341
236
|
...options,
|
|
342
|
-
reservedPaths
|
|
237
|
+
reservedPaths,
|
|
343
238
|
credentialHandoffUrl(baseUrl) {
|
|
344
239
|
return new URL("/", baseUrl).toString();
|
|
345
240
|
},
|
|
346
241
|
async handle(context) {
|
|
242
|
+
// Everything else belongs to the server, and reaches it untouched: no
|
|
243
|
+
// route here, and no activity module, runs for a path this surface does
|
|
244
|
+
// not own. The Activity page is the one addition; the server reserves
|
|
245
|
+
// it only when history is readable, and routeUi checks the same thing.
|
|
246
|
+
// Artifact pages are the other: they exist only beside the module.
|
|
247
|
+
const artifacts = context.opts.artifactsModule;
|
|
248
|
+
const artifactPath = Boolean(artifacts) && isArtifactPath(context.path);
|
|
249
|
+
if (!artifactPath && !ownsOperatorPath(reservedPaths, context.path))
|
|
250
|
+
return null;
|
|
251
|
+
if (artifactPath && !operatorPageForPath(context.path)) {
|
|
252
|
+
// The pages' JSON API and the sandboxed frame; the module owns both,
|
|
253
|
+
// and this bundle imports none of it.
|
|
254
|
+
return (await artifacts?.handle(context)) ??
|
|
255
|
+
new Response("Not Found", { status: 404 });
|
|
256
|
+
}
|
|
347
257
|
const routes = [
|
|
348
258
|
...(context.opts.credentialVault ? [routeCredentials] : []),
|
|
349
259
|
routeOAuthManagement,
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Syntactic destination checks for URLs a downstream hands connecta to fetch.
|
|
3
|
+
*
|
|
4
|
+
* A remote MCP server's OAuth metadata names further URLs — authorization
|
|
5
|
+
* servers, token and registration endpoints — that connecta then requests
|
|
6
|
+
* server-side. Config is the security model: the connector's configured
|
|
7
|
+
* origin is trusted because an operator wrote it down. Everything else was
|
|
8
|
+
* learned from the downstream, and a compromised downstream must not be able
|
|
9
|
+
* to steer connecta at the host's own network (cloud metadata, a LAN admin
|
|
10
|
+
* panel, a sidecar on loopback).
|
|
11
|
+
*
|
|
12
|
+
* The check is by syntax only. The WHATWG URL parser has already folded every
|
|
13
|
+
* legacy IPv4 spelling (`2130706433`, `0x7f.1`, `0177.0.0.1`) into dotted
|
|
14
|
+
* decimal and every IPv6 literal into compressed hex, so one canonical form is
|
|
15
|
+
* all this module reads. It never resolves a name: that needs DNS, which the
|
|
16
|
+
* Workers-safe core cannot do, and a name that resolves somewhere private — or
|
|
17
|
+
* rebinds after the check — is out of scope. Pure Web API, no `node:` import.
|
|
18
|
+
*/
|
|
19
|
+
function classifyIpv4(octets) {
|
|
20
|
+
const [a = 0, b = 0] = octets;
|
|
21
|
+
if (a === 127)
|
|
22
|
+
return "loopback";
|
|
23
|
+
if (a === 0 ||
|
|
24
|
+
a === 10 ||
|
|
25
|
+
(a === 172 && b >= 16 && b <= 31) ||
|
|
26
|
+
(a === 192 && b === 168) ||
|
|
27
|
+
(a === 169 && b === 254) ||
|
|
28
|
+
(a === 100 && b >= 64 && b <= 127)) {
|
|
29
|
+
return "private";
|
|
30
|
+
}
|
|
31
|
+
return "public";
|
|
32
|
+
}
|
|
33
|
+
const IPV4 = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/;
|
|
34
|
+
const HEXTET = /^[0-9a-f]{1,4}$/;
|
|
35
|
+
function ipv4Octets(host) {
|
|
36
|
+
const match = IPV4.exec(host);
|
|
37
|
+
if (!match)
|
|
38
|
+
return undefined;
|
|
39
|
+
const octets = match.slice(1).map(Number);
|
|
40
|
+
return octets.every((octet) => octet <= 255) ? octets : undefined;
|
|
41
|
+
}
|
|
42
|
+
/** Eight 16-bit pieces of a bracketed IPv6 literal, or undefined. */
|
|
43
|
+
function ipv6Pieces(host) {
|
|
44
|
+
const body = host.slice(1, -1);
|
|
45
|
+
const halves = body.split("::");
|
|
46
|
+
if (halves.length > 2)
|
|
47
|
+
return undefined;
|
|
48
|
+
const pieces = (part) => {
|
|
49
|
+
if (!part)
|
|
50
|
+
return [];
|
|
51
|
+
const parsed = part.split(":").map((piece) => HEXTET.test(piece) ? Number.parseInt(piece, 16) : Number.NaN);
|
|
52
|
+
return parsed.some(Number.isNaN) ? undefined : parsed;
|
|
53
|
+
};
|
|
54
|
+
const head = pieces(halves[0]);
|
|
55
|
+
const tail = pieces(halves[1]);
|
|
56
|
+
if (!head || !tail)
|
|
57
|
+
return undefined;
|
|
58
|
+
const missing = 8 - head.length - tail.length;
|
|
59
|
+
if (halves.length === 1 ? missing !== 0 : missing < 0)
|
|
60
|
+
return undefined;
|
|
61
|
+
return [...head, ...Array.from({ length: missing }, () => 0), ...tail];
|
|
62
|
+
}
|
|
63
|
+
function classifyIpv6(pieces) {
|
|
64
|
+
const [first = 0] = pieces;
|
|
65
|
+
const leadingZeros = pieces.slice(0, 5).every((piece) => piece === 0);
|
|
66
|
+
if (leadingZeros && pieces[5] === 0 && pieces[6] === 0) {
|
|
67
|
+
if (pieces[7] === 0)
|
|
68
|
+
return "private"; // ::
|
|
69
|
+
if (pieces[7] === 1)
|
|
70
|
+
return "loopback"; // ::1
|
|
71
|
+
}
|
|
72
|
+
// ::ffff:a.b.c.d reaches the mapped IPv4 destination; classify that.
|
|
73
|
+
if (leadingZeros && pieces[5] === 0xffff) {
|
|
74
|
+
const high = pieces[6] ?? 0;
|
|
75
|
+
const low = pieces[7] ?? 0;
|
|
76
|
+
return classifyIpv4([high >> 8, high & 0xff, low >> 8, low & 0xff]);
|
|
77
|
+
}
|
|
78
|
+
if ((first & 0xfe00) === 0xfc00)
|
|
79
|
+
return "private"; // fc00::/7
|
|
80
|
+
if ((first & 0xffc0) === 0xfe80)
|
|
81
|
+
return "private"; // fe80::/10
|
|
82
|
+
return "public";
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* The hostname the WHATWG parser settles on. Idempotent for a `URL.hostname`;
|
|
86
|
+
* anything else — a bare IPv6 literal, a legacy IPv4 spelling — is folded the
|
|
87
|
+
* same way the parser would fold it on the way to a fetch.
|
|
88
|
+
*/
|
|
89
|
+
function canonicalHost(hostname) {
|
|
90
|
+
if (/[/\\?#@]/.test(hostname))
|
|
91
|
+
return undefined;
|
|
92
|
+
const bracketed = hostname.includes(":") && !hostname.startsWith("[")
|
|
93
|
+
? `[${hostname}]`
|
|
94
|
+
: hostname;
|
|
95
|
+
try {
|
|
96
|
+
return new URL(`http://${bracketed}/`).hostname;
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
return undefined;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Classify a URL host by syntax. A host that does not parse is `private`:
|
|
104
|
+
* the check fails closed rather than guessing.
|
|
105
|
+
*/
|
|
106
|
+
export function classifyHost(hostname) {
|
|
107
|
+
const host = canonicalHost(hostname);
|
|
108
|
+
if (!host)
|
|
109
|
+
return "private";
|
|
110
|
+
if (host.startsWith("[")) {
|
|
111
|
+
const pieces = ipv6Pieces(host);
|
|
112
|
+
return pieces ? classifyIpv6(pieces) : "private";
|
|
113
|
+
}
|
|
114
|
+
const octets = ipv4Octets(host);
|
|
115
|
+
if (octets)
|
|
116
|
+
return classifyIpv4(octets);
|
|
117
|
+
const name = host.endsWith(".") ? host.slice(0, -1) : host;
|
|
118
|
+
if (name === "localhost" || name.endsWith(".localhost"))
|
|
119
|
+
return "loopback";
|
|
120
|
+
return "public";
|
|
121
|
+
}
|
|
122
|
+
/** True for loopback and private hosts alike. */
|
|
123
|
+
export function isPrivateHost(hostname) {
|
|
124
|
+
return classifyHost(hostname) !== "public";
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Why connecta must not fetch `target`, a URL the downstream advertised, for a
|
|
128
|
+
* connector configured at `configured` — or undefined when it may.
|
|
129
|
+
*
|
|
130
|
+
* - The configured origin is trusted outright: the operator chose it.
|
|
131
|
+
* - Anything else must be `https` on a host that is not private by syntax.
|
|
132
|
+
* - A loopback-configured connector (local development) may also learn
|
|
133
|
+
* loopback URLs, over `http` too, so a local MCP server with a local
|
|
134
|
+
* authorization server keeps working. It still may not learn a LAN or
|
|
135
|
+
* link-local address.
|
|
136
|
+
*
|
|
137
|
+
* The reason names only the host, never the path, query, or credentials a
|
|
138
|
+
* learned URL may carry.
|
|
139
|
+
*/
|
|
140
|
+
export function learnedUrlRefusal(configured, target) {
|
|
141
|
+
if (target.protocol !== "https:" && target.protocol !== "http:") {
|
|
142
|
+
return `${target.host || "the target"} uses ${target.protocol}, not HTTPS`;
|
|
143
|
+
}
|
|
144
|
+
if (target.origin === configured.origin)
|
|
145
|
+
return undefined;
|
|
146
|
+
const targetClass = classifyHost(target.hostname);
|
|
147
|
+
if (targetClass === "loopback" &&
|
|
148
|
+
classifyHost(configured.hostname) === "loopback") {
|
|
149
|
+
return undefined;
|
|
150
|
+
}
|
|
151
|
+
if (targetClass !== "public") {
|
|
152
|
+
return `${target.host} is a private, loopback, or link-local address`;
|
|
153
|
+
}
|
|
154
|
+
if (target.protocol !== "https:")
|
|
155
|
+
return `${target.host} is not HTTPS`;
|
|
156
|
+
return undefined;
|
|
157
|
+
}
|
package/dist/version.d.ts
CHANGED
package/dist/version.js
CHANGED