@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.
Files changed (145) hide show
  1. package/AGENTS.md +58 -30
  2. package/CHANGELOG.md +317 -1
  3. package/README.md +5 -6
  4. package/dist/advertised-schema.js +53 -0
  5. package/dist/auth/downstream-oauth.js +490 -106
  6. package/dist/branding.d.ts +31 -2
  7. package/dist/branding.js +116 -8
  8. package/dist/call-admission.js +139 -113
  9. package/dist/catalog-service.js +141 -105
  10. package/dist/catalog.js +258 -33
  11. package/dist/concurrency.js +24 -23
  12. package/dist/connector-scope.js +6 -37
  13. package/dist/connectors/api.d.ts +3 -3
  14. package/dist/connectors/api.js +10 -2
  15. package/dist/connectors/guarded-fetch.js +34 -4
  16. package/dist/connectors/remote-mcp.d.ts +6 -5
  17. package/dist/connectors/remote-mcp.js +325 -245
  18. package/dist/credential-contract.d.ts +9 -0
  19. package/dist/credential-rules.js +2 -2
  20. package/dist/credentials.d.ts +6 -1
  21. package/dist/credentials.js +45 -3
  22. package/dist/errors.d.ts +11 -3
  23. package/dist/errors.js +2 -1
  24. package/dist/execute.js +363 -257
  25. package/dist/executor-admission.js +67 -50
  26. package/dist/executors/quickjs-runtime.js +4 -2
  27. package/dist/executors/quickjs.js +251 -210
  28. package/dist/fix-prompt.js +37 -0
  29. package/dist/index.d.ts +11 -2
  30. package/dist/index.js +63 -21
  31. package/dist/invocation.js +324 -285
  32. package/dist/meta-tools.js +304 -114
  33. package/dist/oauth-callback-outcome.js +94 -0
  34. package/dist/oauth-sealing.js +29 -0
  35. package/dist/operator-ui/fix-prompts.js +87 -0
  36. package/dist/operator-ui/generated.js +2 -2
  37. package/dist/operator-ui/model.d.ts +46 -1
  38. package/dist/operator-ui/setup-commands.js +53 -0
  39. package/dist/operator-ui/view.js +109 -2
  40. package/dist/program-source.js +205 -0
  41. package/dist/providers/cloudflare.d.ts +14 -2
  42. package/dist/providers/cloudflare.js +107 -16
  43. package/dist/providers/linear.d.ts +26 -4
  44. package/dist/providers/linear.js +19 -4
  45. package/dist/providers/mixpanel.d.ts +16 -3
  46. package/dist/providers/mixpanel.js +13 -2
  47. package/dist/providers/notion.d.ts +8 -1
  48. package/dist/providers/notion.js +85 -12
  49. package/dist/providers/revenuecat.d.ts +30 -4
  50. package/dist/providers/revenuecat.js +70 -5
  51. package/dist/providers/stripe.d.ts +10 -3
  52. package/dist/providers/stripe.js +30 -4
  53. package/dist/providers/vercel.js +12 -2
  54. package/dist/registry.d.ts +13 -5
  55. package/dist/registry.js +317 -258
  56. package/dist/routes/activity.js +120 -137
  57. package/dist/routes/credentials.js +174 -163
  58. package/dist/routes/mcp.js +129 -70
  59. package/dist/routes/oauth-management.js +60 -71
  60. package/dist/routes/oauth.js +44 -16
  61. package/dist/routes/operator.js +82 -0
  62. package/dist/routes/shared.d.ts +2 -0
  63. package/dist/routes/ui-data.js +210 -0
  64. package/dist/routes/ui.js +64 -32
  65. package/dist/runtime/admission.js +43 -0
  66. package/dist/runtime/call-admission.js +67 -0
  67. package/dist/runtime/connector-scope.js +72 -0
  68. package/dist/runtime/run.js +174 -0
  69. package/dist/runtime/services.js +130 -0
  70. package/dist/runtime/storage.js +66 -0
  71. package/dist/server.js +111 -89
  72. package/dist/skills.js +1 -1
  73. package/dist/storage/file.js +39 -2
  74. package/dist/storage/memory.js +28 -16
  75. package/dist/timeout.js +17 -47
  76. package/dist/types.d.ts +64 -7
  77. package/dist/ui.d.ts +16 -2
  78. package/dist/ui.js +68 -176
  79. package/dist/url-safety.js +157 -0
  80. package/dist/version.d.ts +1 -1
  81. package/dist/version.js +1 -1
  82. package/documentation/architecture.md +368 -182
  83. package/documentation/auth.md +277 -176
  84. package/documentation/code-mode.md +479 -329
  85. package/documentation/meta-tools.md +494 -417
  86. package/ethos.md +44 -47
  87. package/examples/worker/AGENTS.md +2 -1
  88. package/examples/worker/README.md +62 -14
  89. package/examples/worker/src/d1-storage.ts +116 -0
  90. package/examples/worker/src/index.ts +18 -21
  91. package/examples/worker/wrangler.jsonc +16 -4
  92. package/package.json +11 -4
  93. package/templates/node/.env.example +3 -3
  94. package/templates/node/AGENTS.md +5 -4
  95. package/templates/node/README.md +2 -1
  96. package/templates/node/package.json +1 -1
  97. package/templates/node/src/index.ts +23 -22
  98. package/dist/auth/downstream-oauth.d.ts +0 -200
  99. package/dist/catalog-fingerprint.d.ts +0 -19
  100. package/dist/catalog-limits.d.ts +0 -9
  101. package/dist/catalog-service.d.ts +0 -263
  102. package/dist/catalog.d.ts +0 -87
  103. package/dist/concurrency.d.ts +0 -3
  104. package/dist/connectors/guarded-fetch.d.ts +0 -92
  105. package/dist/credential-rules.d.ts +0 -85
  106. package/dist/execute.d.ts +0 -154
  107. package/dist/executor-result.d.ts +0 -10
  108. package/dist/executors/quickjs-child.d.ts +0 -1
  109. package/dist/executors/quickjs-protocol.d.ts +0 -67
  110. package/dist/favicon.d.ts +0 -1
  111. package/dist/identity.d.ts +0 -4
  112. package/dist/invocation.d.ts +0 -74
  113. package/dist/mcp-result.d.ts +0 -8
  114. package/dist/meta-tools.d.ts +0 -129
  115. package/dist/operator-ui/generated.d.ts +0 -2
  116. package/dist/operator-ui/view.d.ts +0 -151
  117. package/dist/result-shapes.d.ts +0 -13
  118. package/dist/retired-toolkits.d.ts +0 -2
  119. package/dist/routes/activity.d.ts +0 -2
  120. package/dist/routes/credentials.d.ts +0 -2
  121. package/dist/routes/mcp.d.ts +0 -10
  122. package/dist/routes/oauth-management.d.ts +0 -2
  123. package/dist/routes/oauth.d.ts +0 -2
  124. package/dist/routes/ui.d.ts +0 -2
  125. package/dist/server.d.ts +0 -9
  126. package/dist/skills.d.ts +0 -44
  127. package/dist/timeout.d.ts +0 -21
  128. package/dist/tool-safety.d.ts +0 -9
  129. package/documentation/call-admission.md +0 -158
  130. package/documentation/cloudflare.md +0 -471
  131. package/documentation/connector-guides.md +0 -176
  132. package/documentation/connectors.md +0 -431
  133. package/documentation/linear.md +0 -193
  134. package/documentation/mixpanel.md +0 -160
  135. package/documentation/notion.md +0 -308
  136. package/documentation/operations.md +0 -359
  137. package/documentation/operator-ui.md +0 -135
  138. package/documentation/optional-modules-upgrade.md +0 -243
  139. package/documentation/provider-conventions.md +0 -729
  140. package/documentation/request-admission.md +0 -204
  141. package/documentation/revenuecat.md +0 -305
  142. package/documentation/storage-and-credentials.md +0 -254
  143. package/documentation/stripe.md +0 -262
  144. package/documentation/upgrading.md +0 -768
  145. package/documentation/vercel.md +0 -241
@@ -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
- // Rotate through at most 16 existing keys. Live entries cannot keep an
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
- /** Resolve after `ms`, or false when the caller aborts first. */
16
- export function sleep(ms, signal) {
17
- return new Promise((resolve) => {
18
- let settled = false;
19
- const finish = (value) => {
20
- if (settled)
21
- return;
22
- settled = true;
23
- clearTimeout(timer);
24
- signal?.removeEventListener("abort", cancel);
25
- resolve(value);
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
- const controller = new AbortController();
36
- const forwardAbort = () => controller.abort(options.signal?.reason);
37
- options.signal?.addEventListener("abort", forwardAbort, { once: true });
38
- if (options.signal?.aborted)
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 /credentials. Never include the credential itself. */
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 /credentials. Never include the credential itself. */
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
- * `credentials.encryptionKey`.
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 50_000). Must be a whole
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 on /credentials. */
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 /credentials' Test action. */
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 /credentials' Test action. */
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 { withDeadline } from "./timeout.js";
5
- import { credentialTestRule, describeUndeclaredCredentialFields, storedCredentialShape, } from "./credential-rules.js";
6
- import { closeConnectorScope, } from "./connector-scope.js";
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
- const requestScope = {};
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
- connectaVersion: CONNECTA_VERSION,
209
- connectors,
75
+ credentialVault,
210
76
  activityEnabled,
211
77
  credentialManagement,
212
- oauthManagement: oauthManagement || Boolean(personalCredentialOwner),
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 pgrid">
300
- ${owner}
301
- <div class="mast-nav">
302
- ${product}
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 pgrid">
309
- <h1 class="pcap">${OPERATOR_PAGE_LABELS[page]}</h1>
310
- <div class="pbody lead-copy">
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: ["/", "/ui", "/ui/*", "/favicon.svg", "/favicon.ico"],
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,