@zackbart/connecta 0.24.3 → 0.25.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 +58 -30
- package/CHANGELOG.md +317 -1
- package/README.md +5 -6
- package/dist/advertised-schema.js +53 -0
- package/dist/auth/downstream-oauth.js +490 -106
- package/dist/branding.d.ts +31 -2
- package/dist/branding.js +116 -8
- package/dist/call-admission.js +139 -113
- package/dist/catalog-service.js +141 -105
- package/dist/catalog.js +258 -33
- package/dist/concurrency.js +24 -23
- package/dist/connector-scope.js +6 -37
- package/dist/connectors/api.d.ts +3 -3
- package/dist/connectors/api.js +10 -2
- package/dist/connectors/guarded-fetch.js +34 -4
- package/dist/connectors/remote-mcp.d.ts +6 -5
- package/dist/connectors/remote-mcp.js +325 -245
- 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 +11 -3
- package/dist/errors.js +2 -1
- package/dist/execute.js +363 -257
- package/dist/executor-admission.js +67 -50
- package/dist/executors/quickjs-runtime.js +4 -2
- package/dist/executors/quickjs.js +251 -210
- package/dist/fix-prompt.js +37 -0
- package/dist/index.d.ts +11 -2
- package/dist/index.js +63 -21
- package/dist/invocation.js +324 -285
- package/dist/meta-tools.js +304 -114
- 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 +46 -1
- package/dist/operator-ui/setup-commands.js +53 -0
- package/dist/operator-ui/view.js +109 -2
- package/dist/program-source.js +205 -0
- package/dist/providers/cloudflare.d.ts +14 -2
- package/dist/providers/cloudflare.js +107 -16
- package/dist/providers/linear.d.ts +26 -4
- package/dist/providers/linear.js +19 -4
- package/dist/providers/mixpanel.d.ts +16 -3
- package/dist/providers/mixpanel.js +13 -2
- package/dist/providers/notion.d.ts +8 -1
- package/dist/providers/notion.js +85 -12
- package/dist/providers/revenuecat.d.ts +30 -4
- package/dist/providers/revenuecat.js +70 -5
- package/dist/providers/stripe.d.ts +10 -3
- package/dist/providers/stripe.js +30 -4
- package/dist/providers/vercel.js +12 -2
- package/dist/registry.d.ts +13 -5
- package/dist/registry.js +317 -258
- package/dist/routes/activity.js +120 -137
- package/dist/routes/credentials.js +174 -163
- package/dist/routes/mcp.js +129 -70
- package/dist/routes/oauth-management.js +60 -71
- package/dist/routes/oauth.js +44 -16
- package/dist/routes/operator.js +82 -0
- package/dist/routes/shared.d.ts +2 -0
- package/dist/routes/ui-data.js +210 -0
- package/dist/routes/ui.js +64 -32
- 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/storage.js +66 -0
- package/dist/server.js +111 -89
- package/dist/skills.js +1 -1
- package/dist/storage/file.js +39 -2
- package/dist/storage/memory.js +28 -16
- package/dist/timeout.js +17 -47
- package/dist/types.d.ts +64 -7
- package/dist/ui.d.ts +16 -2
- package/dist/ui.js +68 -176
- package/dist/url-safety.js +157 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/documentation/architecture.md +368 -182
- package/documentation/auth.md +277 -176
- package/documentation/code-mode.md +479 -329
- package/documentation/meta-tools.md +494 -417
- package/ethos.md +44 -47
- package/examples/worker/AGENTS.md +2 -1
- package/examples/worker/README.md +62 -14
- package/examples/worker/src/d1-storage.ts +116 -0
- package/examples/worker/src/index.ts +18 -21
- package/examples/worker/wrangler.jsonc +16 -4
- package/package.json +11 -4
- package/templates/node/.env.example +3 -3
- package/templates/node/AGENTS.md +5 -4
- package/templates/node/README.md +2 -1
- package/templates/node/package.json +1 -1
- package/templates/node/src/index.ts +23 -22
- 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 -92
- 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 -151
- 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/tool-safety.d.ts +0 -9
- package/documentation/call-admission.md +0 -158
- package/documentation/cloudflare.md +0 -471
- package/documentation/connector-guides.md +0 -176
- package/documentation/connectors.md +0 -431
- package/documentation/linear.md +0 -193
- package/documentation/mixpanel.md +0 -160
- package/documentation/notion.md +0 -308
- package/documentation/operations.md +0 -359
- package/documentation/operator-ui.md +0 -135
- package/documentation/optional-modules-upgrade.md +0 -243
- package/documentation/provider-conventions.md +0 -729
- package/documentation/request-admission.md +0 -204
- package/documentation/revenuecat.md +0 -305
- package/documentation/storage-and-credentials.md +0 -254
- package/documentation/stripe.md +0 -262
- package/documentation/upgrading.md +0 -768
- package/documentation/vercel.md +0 -241
package/dist/storage/memory.js
CHANGED
|
@@ -12,27 +12,28 @@ export function memoryStorage() {
|
|
|
12
12
|
}
|
|
13
13
|
return e;
|
|
14
14
|
};
|
|
15
|
+
const write = (key, value, ttlSeconds) => {
|
|
16
|
+
// Rotate through at most 16 existing keys. Live entries cannot keep an
|
|
17
|
+
// expired tail resident forever, and no request starts a background job.
|
|
18
|
+
for (let i = 0; i < 16; i++) {
|
|
19
|
+
const next = sweep.next();
|
|
20
|
+
if (next.done) {
|
|
21
|
+
sweep = map.keys();
|
|
22
|
+
break;
|
|
23
|
+
}
|
|
24
|
+
fresh(next.value);
|
|
25
|
+
}
|
|
26
|
+
map.set(key, {
|
|
27
|
+
value,
|
|
28
|
+
...(ttlSeconds ? { exp: Date.now() + ttlSeconds * 1000 } : {}),
|
|
29
|
+
});
|
|
30
|
+
};
|
|
15
31
|
return {
|
|
16
32
|
async get(key) {
|
|
17
33
|
return fresh(key)?.value ?? null;
|
|
18
34
|
},
|
|
19
35
|
async set(key, value, opts) {
|
|
20
|
-
|
|
21
|
-
// expired tail resident forever, and no request starts a background job.
|
|
22
|
-
for (let i = 0; i < 16; i++) {
|
|
23
|
-
const next = sweep.next();
|
|
24
|
-
if (next.done) {
|
|
25
|
-
sweep = map.keys();
|
|
26
|
-
break;
|
|
27
|
-
}
|
|
28
|
-
fresh(next.value);
|
|
29
|
-
}
|
|
30
|
-
map.set(key, {
|
|
31
|
-
value,
|
|
32
|
-
...(opts?.ttlSeconds
|
|
33
|
-
? { exp: Date.now() + opts.ttlSeconds * 1000 }
|
|
34
|
-
: {}),
|
|
35
|
-
});
|
|
36
|
+
write(key, value, opts?.ttlSeconds);
|
|
36
37
|
},
|
|
37
38
|
async delete(key) {
|
|
38
39
|
map.delete(key);
|
|
@@ -45,5 +46,16 @@ export function memoryStorage() {
|
|
|
45
46
|
})
|
|
46
47
|
.sort();
|
|
47
48
|
},
|
|
49
|
+
// Atomic because nothing between the read and the write yields: the body
|
|
50
|
+
// runs to completion before any other call on this map can start.
|
|
51
|
+
async compareAndSet(key, expected, next, opts) {
|
|
52
|
+
if ((fresh(key)?.value ?? null) !== expected)
|
|
53
|
+
return false;
|
|
54
|
+
if (next === null)
|
|
55
|
+
map.delete(key);
|
|
56
|
+
else
|
|
57
|
+
write(key, next, opts?.ttlSeconds);
|
|
58
|
+
return true;
|
|
59
|
+
},
|
|
48
60
|
};
|
|
49
61
|
}
|
package/dist/timeout.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
// The deadline vocabulary shared by downstream discovery probes and tool calls.
|
|
2
2
|
// One definition keeps those waits bounded consistently.
|
|
3
|
+
import { Effect } from "effect";
|
|
4
|
+
import { runEdge, withDeadlineEffect } from "./runtime/run.js";
|
|
3
5
|
/**
|
|
4
6
|
* Generous default bound for a single downstream probe/catalog call. High enough
|
|
5
7
|
* to trip only on a pathological hang, not a realistically slow probe.
|
|
@@ -12,54 +14,22 @@ export function normalizeTimeoutMs(value) {
|
|
|
12
14
|
}
|
|
13
15
|
return Math.max(1, Math.trunc(value));
|
|
14
16
|
}
|
|
15
|
-
/**
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
};
|
|
27
|
-
const timer = setTimeout(() => finish(true), ms);
|
|
28
|
-
const cancel = () => finish(false);
|
|
29
|
-
signal?.addEventListener("abort", cancel, { once: true });
|
|
30
|
-
if (signal?.aborted)
|
|
31
|
-
cancel();
|
|
32
|
-
});
|
|
33
|
-
}
|
|
17
|
+
/**
|
|
18
|
+
* Run one operation under a deadline and the caller's signal, whichever ends
|
|
19
|
+
* it first.
|
|
20
|
+
*
|
|
21
|
+
* The operation gets a signal of its own: the caller's abort is forwarded to
|
|
22
|
+
* it, and the deadline aborts it with `timeoutError`. Either way the promise
|
|
23
|
+
* rejects with that signal's reason, without waiting for work that ignores
|
|
24
|
+
* the signal. A synchronous throw rejects like an asynchronous one. This is
|
|
25
|
+
* the Promise face of `withDeadlineEffect` (src/runtime/run.ts), which owns
|
|
26
|
+
* the contract; an Effect caller uses that directly.
|
|
27
|
+
*/
|
|
34
28
|
export function withDeadline(operation, options) {
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
forwardAbort();
|
|
40
|
-
let rejectAbort;
|
|
41
|
-
const aborted = new Promise((_, reject) => { rejectAbort = reject; });
|
|
42
|
-
const onAbort = () => rejectAbort(controller.signal.reason ?? options.timeoutError);
|
|
43
|
-
controller.signal.addEventListener("abort", onAbort, { once: true });
|
|
44
|
-
if (controller.signal.aborted)
|
|
45
|
-
onAbort();
|
|
46
|
-
const timer = options.timeoutMs === undefined
|
|
47
|
-
? undefined
|
|
48
|
-
: setTimeout(() => controller.abort(options.timeoutError), options.timeoutMs);
|
|
49
|
-
let work;
|
|
50
|
-
try {
|
|
51
|
-
work = operation(controller.signal);
|
|
52
|
-
}
|
|
53
|
-
catch (error) {
|
|
54
|
-
work = Promise.reject(error);
|
|
55
|
-
}
|
|
56
|
-
return Promise.race([work, aborted]).finally(() => {
|
|
57
|
-
controller.abort();
|
|
58
|
-
if (timer)
|
|
59
|
-
clearTimeout(timer);
|
|
60
|
-
controller.signal.removeEventListener("abort", onAbort);
|
|
61
|
-
options.signal?.removeEventListener("abort", forwardAbort);
|
|
62
|
-
});
|
|
29
|
+
return runEdge(withDeadlineEffect((signal) => Effect.tryPromise({
|
|
30
|
+
try: () => operation(signal),
|
|
31
|
+
catch: (error) => error,
|
|
32
|
+
}), options));
|
|
63
33
|
}
|
|
64
34
|
/**
|
|
65
35
|
* Give one operation a caller-facing deadline and the matching cancellation
|
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;
|
|
@@ -142,7 +162,7 @@ export interface ConnectorContext {
|
|
|
142
162
|
/**
|
|
143
163
|
* Read-only access to this connector's human-managed credential. Present
|
|
144
164
|
* only when the connector declares `credential` and the deployment configures
|
|
145
|
-
* `
|
|
165
|
+
* `vault`.
|
|
146
166
|
*/
|
|
147
167
|
credential?: ConnectorCredentialAccess;
|
|
148
168
|
/**
|
|
@@ -225,7 +245,7 @@ export interface Connector {
|
|
|
225
245
|
* Max inline result size (bytes) for this connector's tools before
|
|
226
246
|
* call_tool truncates and stashes the full text for get_result
|
|
227
247
|
* paging. Overrides `ConnectaConfig.calls.maxResultBytes`;
|
|
228
|
-
* omit to inherit it (which itself defaults to
|
|
248
|
+
* omit to inherit it (which itself defaults to 24_000). Must be a whole
|
|
229
249
|
* number of bytes >= 1; anything else warns at startup and is ignored, so
|
|
230
250
|
* the connector inherits the deployment-wide cap.
|
|
231
251
|
*/
|
|
@@ -248,11 +268,11 @@ export interface Connector {
|
|
|
248
268
|
* configuration; no runtime registration or shared mutable copy exists.
|
|
249
269
|
*/
|
|
250
270
|
usageGuide?: string | ConnectorUsageGuide;
|
|
251
|
-
/** Optional human-managed credential slot rendered
|
|
271
|
+
/** Optional human-managed credential slot rendered in the connection in the operator UI. */
|
|
252
272
|
credential?: ConnectorCredentialConfig;
|
|
253
|
-
/** Optional server-side check used by
|
|
273
|
+
/** Optional server-side check used by the connection's Test action in the operator UI. */
|
|
254
274
|
testCredential?(value: string, ctx: ConnectorContext): Promise<CredentialTestResult>;
|
|
255
|
-
/** Optional multi-field credential check used by
|
|
275
|
+
/** Optional multi-field credential check used by the connection's Test action in the operator UI. */
|
|
256
276
|
testCredentials?(values: ConnectorCredentialValues, ctx: ConnectorContext): Promise<CredentialTestResult>;
|
|
257
277
|
/**
|
|
258
278
|
* Statically-known tool defs, exposed by in-code connectors (`api()`) for
|
|
@@ -519,6 +539,43 @@ export interface ConnectaBranding {
|
|
|
519
539
|
};
|
|
520
540
|
/** `theme-color` meta value. Defaults to "#ffffff". */
|
|
521
541
|
themeColor?: string;
|
|
542
|
+
/**
|
|
543
|
+
* Operator-page appearance. Every field is optional, and a rejected value
|
|
544
|
+
* takes its default rather than failing the page, so `createConnecta` warns
|
|
545
|
+
* at startup about anything it dropped.
|
|
546
|
+
*
|
|
547
|
+
* Five knobs, not a palette. Surfaces, borders, muted text, and the status
|
|
548
|
+
* colors all derive from these, so a deployment sets an accent and gets a
|
|
549
|
+
* readable page instead of thirty chances to break one.
|
|
550
|
+
*/
|
|
551
|
+
theme?: ConnectaTheme;
|
|
552
|
+
}
|
|
553
|
+
/**
|
|
554
|
+
* The operator UI's themeable tokens. These land in a `:root` block on the
|
|
555
|
+
* page, so each is gated by a narrow syntactic check: an unvalidated value
|
|
556
|
+
* here would let deployment config write arbitrary CSS.
|
|
557
|
+
*/
|
|
558
|
+
export interface ConnectaTheme {
|
|
559
|
+
/**
|
|
560
|
+
* The brand color: links, focus rings, primary actions, the active nav item.
|
|
561
|
+
* Hex only (`#rgb`, `#rrggbb`, or `#rrggbbaa`). Defaults to `#2f5fe0`.
|
|
562
|
+
*/
|
|
563
|
+
accent?: string;
|
|
564
|
+
/**
|
|
565
|
+
* Corner rounding for cards, inputs, and buttons. A CSS length (`10px`,
|
|
566
|
+
* `0.5rem`) or a bare number read as pixels. `0` restores square corners.
|
|
567
|
+
* Defaults to `10px`.
|
|
568
|
+
*/
|
|
569
|
+
radius?: string | number;
|
|
570
|
+
/** Body font stack. A plain CSS font-family list. Defaults to a system stack. */
|
|
571
|
+
fontFamily?: string;
|
|
572
|
+
/** Monospace font stack for addresses, ids, and the endpoint URL. */
|
|
573
|
+
monoFamily?: string;
|
|
574
|
+
/**
|
|
575
|
+
* `"system"` (the default) follows the operator's OS setting; `"light"` and
|
|
576
|
+
* `"dark"` pin the page to one palette.
|
|
577
|
+
*/
|
|
578
|
+
colorScheme?: "system" | "light" | "dark";
|
|
522
579
|
}
|
|
523
580
|
/** An inbound authentication provider (bearer token, interactive identity, ...). */
|
|
524
581
|
export interface InboundAuth {
|
package/dist/ui.d.ts
CHANGED
|
@@ -1,14 +1,28 @@
|
|
|
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
5
|
import type { RegistryView } from "./registry.js";
|
|
6
|
-
import type { ConnectaBranding, UiAuthConfig } from "./types.js";
|
|
6
|
+
import type { ConnectaBranding, Connector, ConnectorStatus, ToolDef, UiAuthConfig } from "./types.js";
|
|
7
7
|
export { filterUiConnectors, type CredentialManagementCapability, type UiConnector, type UiData, } from "./operator-ui/model.js";
|
|
8
8
|
export { CONNECTA_FAVICON_SVG, resolveBranding, isSafeHttpUrl, isSafeHttpsUrl, isSafeIconHref } from "./branding.js";
|
|
9
9
|
export type OperatorPage = "connections" | "activity";
|
|
10
10
|
export declare function operatorPageForPath(path: string): OperatorPage | undefined;
|
|
11
11
|
export declare function operatorPageTitle(page: OperatorPage, configuredTitle: string): string;
|
|
12
|
+
/**
|
|
13
|
+
* The badge a tool earns, from the one predicate core enforces with. Computed
|
|
14
|
+
* here so the page reports the rule rather than restating it.
|
|
15
|
+
*/
|
|
16
|
+
export declare function uiToolSafety(definition: ToolDef): UiToolSafety;
|
|
17
|
+
/**
|
|
18
|
+
* The fix-prompt key for a connector that is not usable. Chosen from the
|
|
19
|
+
* status state and the connector's declared auth shape — configuration, never
|
|
20
|
+
* the status message, which can carry a downstream error body.
|
|
21
|
+
*/
|
|
22
|
+
export declare function uiProblemFor(connector: Pick<Connector, "credential" | "startAuth">, status: ConnectorStatus["state"], observed: {
|
|
23
|
+
credentialDrift: boolean;
|
|
24
|
+
catalogFailed: boolean;
|
|
25
|
+
}): UiProblem | undefined;
|
|
12
26
|
export declare function credentialManagementCapability(input: {
|
|
13
27
|
eligibleOperator: boolean;
|
|
14
28
|
hasCredentialSlots: boolean;
|
package/dist/ui.js
CHANGED
|
@@ -1,14 +1,12 @@
|
|
|
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
|
-
import { resolveBranding, isSafeHttpsUrl } from "./branding.js";
|
|
9
|
+
import { resolveBranding, isSafeHttpsUrl, themeCss } from "./branding.js";
|
|
12
10
|
export { CONNECTA_FAVICON_SVG, resolveBranding, isSafeHttpUrl, isSafeHttpsUrl, isSafeIconHref } from "./branding.js";
|
|
13
11
|
/**
|
|
14
12
|
* A JS string literal safe to inline in a script element. Escaping `/` keeps
|
|
@@ -31,6 +29,30 @@ export function operatorPageForPath(path) {
|
|
|
31
29
|
export function operatorPageTitle(page, configuredTitle) {
|
|
32
30
|
return `${OPERATOR_PAGE_LABELS[page]} — ${configuredTitle}`;
|
|
33
31
|
}
|
|
32
|
+
/**
|
|
33
|
+
* The badge a tool earns, from the one predicate core enforces with. Computed
|
|
34
|
+
* here so the page reports the rule rather than restating it.
|
|
35
|
+
*/
|
|
36
|
+
export function uiToolSafety(definition) {
|
|
37
|
+
return isExplicitlyReadOnly(definition) ? "runs_in_programs" : "needs_approval";
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* The fix-prompt key for a connector that is not usable. Chosen from the
|
|
41
|
+
* status state and the connector's declared auth shape — configuration, never
|
|
42
|
+
* the status message, which can carry a downstream error body.
|
|
43
|
+
*/
|
|
44
|
+
export function uiProblemFor(connector, status, observed) {
|
|
45
|
+
if (observed.credentialDrift)
|
|
46
|
+
return "credential_mismatch";
|
|
47
|
+
if (status === "error")
|
|
48
|
+
return "connector_unavailable";
|
|
49
|
+
if (status === "auth_required") {
|
|
50
|
+
if (connector.startAuth)
|
|
51
|
+
return "oauth_required";
|
|
52
|
+
return connector.credential ? "credential_required" : "auth_required";
|
|
53
|
+
}
|
|
54
|
+
return observed.catalogFailed ? "catalog_failed" : undefined;
|
|
55
|
+
}
|
|
34
56
|
export function credentialManagementCapability(input) {
|
|
35
57
|
if (!input.eligibleOperator)
|
|
36
58
|
return "requires_operator";
|
|
@@ -48,169 +70,17 @@ export function credentialManagementCapability(input) {
|
|
|
48
70
|
export async function buildUiData(registry, baseUrl, serverInfo, credentialVault, activityEnabled = false, credentialManagement = credentialVault
|
|
49
71
|
? "available"
|
|
50
72
|
: "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 {
|
|
73
|
+
return runEdge(uiData(registry, baseUrl, {
|
|
207
74
|
serverInfo,
|
|
208
|
-
|
|
209
|
-
connectors,
|
|
75
|
+
credentialVault,
|
|
210
76
|
activityEnabled,
|
|
211
77
|
credentialManagement,
|
|
212
|
-
|
|
213
|
-
|
|
78
|
+
defer,
|
|
79
|
+
oauthManagement,
|
|
80
|
+
discoveryConcurrency,
|
|
81
|
+
personalCredentialOwner,
|
|
82
|
+
...detailOptions,
|
|
83
|
+
}));
|
|
214
84
|
}
|
|
215
85
|
function escapeHtmlAttr(value) {
|
|
216
86
|
return value
|
|
@@ -281,8 +151,14 @@ export function renderUiHtml(uiAuth, mcpUrl = "/mcp", branding, nonce, page = "c
|
|
|
281
151
|
const clerkScript = clerk && clerkScriptOrigin
|
|
282
152
|
? `<script${nonceAttr} crossorigin="anonymous" data-clerk-publishable-key="${escapeHtmlAttr(clerk.publishableKey)}" src="${escapeHtmlAttr(clerkScriptOrigin)}/npm/@clerk/clerk-js@6/dist/clerk.browser.js"></script>`
|
|
283
153
|
: "";
|
|
154
|
+
// A pinned scheme is an attribute, not a stylesheet edit: the dark palette
|
|
155
|
+
// keys off `html[data-scheme]`, so "system" leaves the attribute off and the
|
|
156
|
+
// media query decides.
|
|
157
|
+
const schemeAttr = brand.theme.colorScheme === "system"
|
|
158
|
+
? ""
|
|
159
|
+
: ` data-scheme="${brand.theme.colorScheme}"`;
|
|
284
160
|
return `<!doctype html>
|
|
285
|
-
<html lang="en">
|
|
161
|
+
<html lang="en"${schemeAttr}>
|
|
286
162
|
<head>
|
|
287
163
|
<meta charset="utf-8">
|
|
288
164
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
@@ -292,22 +168,24 @@ export function renderUiHtml(uiAuth, mcpUrl = "/mcp", branding, nonce, page = "c
|
|
|
292
168
|
<link rel="shortcut icon" href="/favicon.ico">
|
|
293
169
|
<title>${escapeHtmlAttr(title)}</title>
|
|
294
170
|
${clerkScript}
|
|
295
|
-
<style>${OPERATOR_UI_CSS}</style>
|
|
171
|
+
<style>${OPERATOR_UI_CSS}${themeCss(brand.theme)}</style>
|
|
296
172
|
</head>
|
|
297
173
|
<body>
|
|
298
174
|
<a class="skip-link" href="#operatorContent">Skip to operator page</a>
|
|
299
|
-
<header class="masthead shell
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
175
|
+
<header class="masthead shell">
|
|
176
|
+
<div class="masthead-inner">
|
|
177
|
+
<div class="mast-nav">
|
|
178
|
+
${owner}
|
|
179
|
+
${product}
|
|
180
|
+
</div>
|
|
303
181
|
<div id="operatorNav"></div>
|
|
304
182
|
</div>
|
|
305
183
|
</header>
|
|
306
184
|
|
|
307
185
|
<main id="operatorContent" class="page shell" tabindex="-1">
|
|
308
|
-
<div class="lead
|
|
309
|
-
<h1
|
|
310
|
-
<div class="
|
|
186
|
+
<div class="lead">
|
|
187
|
+
<h1>${OPERATOR_PAGE_LABELS[page]}</h1>
|
|
188
|
+
<div class="lead-copy">
|
|
311
189
|
<p>${escapeHtmlAttr(brand.description)}</p>
|
|
312
190
|
<noscript><p class="msg">The operator pages need JavaScript. Nothing else here
|
|
313
191
|
does — agents reach this deployment through <span class="mono">/mcp</span>.</p></noscript>
|
|
@@ -327,15 +205,29 @@ ${OPERATOR_UI_SCRIPT}</script>
|
|
|
327
205
|
</body>
|
|
328
206
|
</html>`;
|
|
329
207
|
}
|
|
208
|
+
function ownsOperatorPath(reserved, path) {
|
|
209
|
+
if (path === "/activity")
|
|
210
|
+
return true;
|
|
211
|
+
return reserved.some((pattern) => pattern.endsWith("/*")
|
|
212
|
+
? path.startsWith(pattern.slice(0, -1))
|
|
213
|
+
: path === pattern);
|
|
214
|
+
}
|
|
330
215
|
/** Mount the connection UI without enabling any storage or activity module. */
|
|
331
216
|
export function operatorUi(options = {}) {
|
|
217
|
+
const reservedPaths = ["/", "/ui", "/ui/*", "/favicon.svg", "/favicon.ico"];
|
|
332
218
|
return {
|
|
333
219
|
...options,
|
|
334
|
-
reservedPaths
|
|
220
|
+
reservedPaths,
|
|
335
221
|
credentialHandoffUrl(baseUrl) {
|
|
336
222
|
return new URL("/", baseUrl).toString();
|
|
337
223
|
},
|
|
338
224
|
async handle(context) {
|
|
225
|
+
// Everything else belongs to the server, and reaches it untouched: no
|
|
226
|
+
// route here, and no activity module, runs for a path this surface does
|
|
227
|
+
// not own. The Activity page is the one addition; the server reserves
|
|
228
|
+
// it only when history is readable, and routeUi checks the same thing.
|
|
229
|
+
if (!ownsOperatorPath(reservedPaths, context.path))
|
|
230
|
+
return null;
|
|
339
231
|
const routes = [
|
|
340
232
|
...(context.opts.credentialVault ? [routeCredentials] : []),
|
|
341
233
|
routeOAuthManagement,
|