@zackbart/connecta 0.24.3 → 0.24.4

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 (67) hide show
  1. package/AGENTS.md +18 -20
  2. package/CHANGELOG.md +64 -1
  3. package/README.md +5 -6
  4. package/dist/branding.d.ts +31 -2
  5. package/dist/branding.js +116 -8
  6. package/dist/connectors/api.d.ts +1 -1
  7. package/dist/connectors/api.js +10 -2
  8. package/dist/connectors/guarded-fetch.d.ts +5 -1
  9. package/dist/connectors/guarded-fetch.js +34 -4
  10. package/dist/connectors/remote-mcp.js +8 -4
  11. package/dist/errors.d.ts +11 -3
  12. package/dist/errors.js +2 -1
  13. package/dist/index.d.ts +1 -1
  14. package/dist/index.js +12 -1
  15. package/dist/meta-tools.js +105 -29
  16. package/dist/operator-ui/generated.js +2 -2
  17. package/dist/operator-ui/view.d.ts +38 -1
  18. package/dist/operator-ui/view.js +71 -0
  19. package/dist/providers/cloudflare.d.ts +14 -2
  20. package/dist/providers/cloudflare.js +107 -16
  21. package/dist/providers/linear.d.ts +26 -4
  22. package/dist/providers/linear.js +19 -4
  23. package/dist/providers/mixpanel.d.ts +16 -3
  24. package/dist/providers/mixpanel.js +13 -2
  25. package/dist/providers/notion.d.ts +8 -1
  26. package/dist/providers/notion.js +83 -10
  27. package/dist/providers/revenuecat.d.ts +30 -4
  28. package/dist/providers/revenuecat.js +42 -4
  29. package/dist/providers/stripe.d.ts +7 -1
  30. package/dist/providers/stripe.js +30 -4
  31. package/dist/providers/vercel.js +11 -1
  32. package/dist/registry.d.ts +12 -4
  33. package/dist/registry.js +22 -8
  34. package/dist/types.d.ts +37 -0
  35. package/dist/ui.js +18 -10
  36. package/dist/version.d.ts +1 -1
  37. package/dist/version.js +1 -1
  38. package/documentation/architecture.md +193 -181
  39. package/documentation/auth.md +197 -176
  40. package/documentation/code-mode.md +426 -321
  41. package/documentation/meta-tools.md +356 -416
  42. package/examples/worker/AGENTS.md +2 -1
  43. package/examples/worker/README.md +12 -10
  44. package/examples/worker/src/index.ts +12 -15
  45. package/package.json +1 -2
  46. package/templates/node/.env.example +3 -3
  47. package/templates/node/AGENTS.md +5 -4
  48. package/templates/node/README.md +2 -1
  49. package/templates/node/package.json +1 -1
  50. package/templates/node/src/index.ts +23 -22
  51. package/documentation/call-admission.md +0 -158
  52. package/documentation/cloudflare.md +0 -471
  53. package/documentation/connector-guides.md +0 -176
  54. package/documentation/connectors.md +0 -431
  55. package/documentation/linear.md +0 -193
  56. package/documentation/mixpanel.md +0 -160
  57. package/documentation/notion.md +0 -308
  58. package/documentation/operations.md +0 -359
  59. package/documentation/operator-ui.md +0 -135
  60. package/documentation/optional-modules-upgrade.md +0 -243
  61. package/documentation/provider-conventions.md +0 -729
  62. package/documentation/request-admission.md +0 -204
  63. package/documentation/revenuecat.md +0 -305
  64. package/documentation/storage-and-credentials.md +0 -254
  65. package/documentation/stripe.md +0 -262
  66. package/documentation/upgrading.md +0 -768
  67. package/documentation/vercel.md +0 -241
@@ -3,7 +3,12 @@ import { vettedCatalog, withVettedCatalog } from "../catalog-drift.js";
3
3
  import { defined } from "../connectors/api.js";
4
4
  /** RevenueCat publishes one hosted MCP endpoint, streamable HTTP. */
5
5
  export const REVENUECAT_MCP_ENDPOINT = "https://mcp.revenuecat.ai/mcp";
6
- /** Reviewed reads; see `documentation/revenuecat.md` and convention P5. */
6
+ /**
7
+ * Reviewed reads, every `Read` row of RevenueCat's tool reference as read on
8
+ * 2026-08-30 (P5). The list is a superset: plan, platform, and beta enrollment
9
+ * gate parts of the catalog, so a name this account never returns costs
10
+ * nothing while an unlisted new one fails closed.
11
+ */
7
12
  const READ_ONLY_TOOLS = new Set([
8
13
  // Projects and apps
9
14
  "get-account-billing",
@@ -70,26 +75,48 @@ const READ_ONLY_TOOLS = new Set([
70
75
  // *start* one are writes and sit below.
71
76
  "get-paywall-ai-task",
72
77
  ]);
73
- /** Reviewed writes and verb exceptions: `documentation/revenuecat.md`. */
78
+ /**
79
+ * Reviewed writes with their destructive verdict. Most follow the verb, and
80
+ * additive writes stay additive because `readOnlyHint: false` already routes
81
+ * them through the approval path — asserting a destruction that does not happen
82
+ * only inflates the copy a human reads. The verdicts the verb does not decide
83
+ * carry their reason on the row.
84
+ *
85
+ * `render-paywall-screenshot` is deliberately in neither map: RevenueCat's
86
+ * reference gives it no access column, so the live annotation stands and a
87
+ * catalog that omits one fails closed. Guessing from a harmless-sounding name
88
+ * is what P5 exists to prevent.
89
+ */
74
90
  const WRITE_TOOLS = new Map([
75
91
  // Projects and apps
76
92
  ["create-app", "additive"],
77
93
  ["create-project", "additive"],
78
94
  ["update-app", "destructive"],
79
95
  ["update-project-ui-config", "destructive"],
96
+ // Filed `Write` by RevenueCat, so it cannot take the read path, but it leaves
97
+ // the saved credentials alone and only records a check's outcome.
80
98
  ["validate-app-credentials", "additive"],
81
99
  // Products and prices
82
100
  ["archive-product", "destructive"],
83
101
  ["create-product", "additive"],
102
+ // Named `create-`, but "Configure prices for a product": the price set
103
+ // already exists, so this overwrites it, and it is money-facing.
84
104
  ["create-product-prices", "destructive"],
105
+ // "Fills *missing* App Store subscription territory prices" — by
106
+ // RevenueCat's own word it writes only where nothing is set.
85
107
  ["equalize-subscription-prices", "additive"],
108
+ // An upsert.
86
109
  ["set-product-store-state", "destructive"],
110
+ // Sends products to Apple for review.
87
111
  ["submit-products-to-store", "destructive"],
88
112
  ["unarchive-product", "destructive"],
89
113
  ["update-product", "destructive"],
114
+ // Reserves a new App Store Connect review screenshot slot; replaces nothing.
90
115
  ["upload-product-store-state-screenshot", "additive"],
91
116
  // Entitlements
92
117
  ["archive-entitlement", "destructive"],
118
+ // Attach adds membership and removes nothing; detach is the destructive half.
119
+ // Filing both destructive would make the pair read identically to a human.
93
120
  ["attach-products-to-entitlement", "additive"],
94
121
  ["create-entitlement", "additive"],
95
122
  ["detach-products-from-entitlement", "destructive"],
@@ -97,6 +124,7 @@ const WRITE_TOOLS = new Map([
97
124
  ["update-entitlement", "destructive"],
98
125
  // Offerings and packages
99
126
  ["archive-offering", "destructive"],
127
+ // The attach/detach argument again, one level down.
100
128
  ["attach-products-to-package", "additive"],
101
129
  ["create-offering", "additive"],
102
130
  ["create-packages", "additive"],
@@ -110,6 +138,7 @@ const WRITE_TOOLS = new Map([
110
138
  // Paywalls
111
139
  ["attach-offering-to-paywall", "destructive"],
112
140
  ["detach-offering-from-paywall", "destructive"],
141
+ // Copies an existing paywall's current draft; the original is untouched.
113
142
  ["duplicate-paywall", "additive"],
114
143
  ["publish-paywall", "destructive"],
115
144
  ["unpublish-paywall", "destructive"],
@@ -130,14 +159,23 @@ const WRITE_TOOLS = new Map([
130
159
  ["unarchive-virtual-currency", "destructive"],
131
160
  ["update-virtual-currency", "destructive"],
132
161
  // Integrations and webhooks
162
+ // Destructive on consequence, not on the verb: no existing integration
163
+ // changes, but with filters omitted the new one starts delivering every
164
+ // customer event in the project to a URL the caller typed.
133
165
  ["create-webhook-integration", "destructive"],
134
166
  ["delete-webhook-integration", "destructive"],
135
167
  ["update-webhook-integration", "destructive"],
136
- // Paywall editing
168
+ // Paywall editing. Both start an async task; the difference is what the task
169
+ // touches — a new paywall, or a draft that already exists.
137
170
  ["create-paywall-ai", "additive"],
138
171
  ["edit-paywall-ai", "destructive"],
139
172
  ]);
140
- /** Release-reviewed manifest; see provider conventions P5 and P13. */
173
+ /**
174
+ * One release-reviewed manifest, used both to classify a live tool and as the
175
+ * baseline the drift check compares against, so the annotation a caller gets
176
+ * and the verdict a check reads can never disagree. Names and verdicts only —
177
+ * no schemas are vendored; the live `tools/list` response stays authoritative.
178
+ */
141
179
  export const REVENUECAT_VETTED_CATALOG = vettedCatalog({
142
180
  reads: READ_ONLY_TOOLS,
143
181
  writes: WRITE_TOOLS,
@@ -40,7 +40,13 @@ export interface StripeHeaderOptions extends StripeCommonOptions {
40
40
  connectedAccount?: string;
41
41
  }
42
42
  export type StripeOptions = StripeOAuthOptions | StripeHeaderOptions;
43
- /** Release-reviewed manifest; see provider conventions P5 and P13. */
43
+ /**
44
+ * One release-reviewed manifest, used both to classify a live tool and as the
45
+ * baseline the drift check compares against, so the annotation a caller gets and
46
+ * the verdict a check reads can never disagree. Stripe publishes no stability or
47
+ * deprecation policy for this tool set, so treat it as unversioned: an
48
+ * unclassified, unannotated name fails closed onto the approval path (P5).
49
+ */
44
50
  export declare const STRIPE_VETTED_CATALOG: import("../catalog-drift.js").VettedCatalog;
45
51
  /** A maintained Stripe hosted-MCP connection. */
46
52
  export declare function stripe(id: string, options: StripeOptions): Connector;
@@ -3,7 +3,14 @@ import { vettedCatalog, withVettedCatalog } from "../catalog-drift.js";
3
3
  import { defined } from "../connectors/api.js";
4
4
  /** Stripe publishes one hosted MCP endpoint for every account and mode. */
5
5
  export const STRIPE_MCP_ENDPOINT = "https://mcp.stripe.com/";
6
- /** Account limits and local concurrency rationale: `documentation/stripe.md`. */
6
+ /**
7
+ * Stripe documents no MCP-specific limit, so these transcribe the account limit
8
+ * MCP traffic spends: 100 requests per second live, 25 in a sandbox. OAuth takes
9
+ * the sandbox rule (`mode ?? "sandbox"` below) because one session can reach
10
+ * either mode and the policy is fixed before the account-scoped call begins.
11
+ * The `maxConcurrency` figures are Connecta's own choice — Stripe says
12
+ * per-account and per-endpoint concurrency limits exist but publishes no number.
13
+ */
7
14
  const STRIPE_ADMISSION = {
8
15
  production: {
9
16
  rules: [
@@ -47,16 +54,30 @@ const READ_ONLY_TOOLS = new Set([
47
54
  "manage_stripe_accounts",
48
55
  "search_stripe_documentation",
49
56
  ]);
50
- /** Reviewed writes, including mixed read/create tools: `documentation/stripe.md`. */
57
+ /**
58
+ * Reviewed writes with their destructive verdict. `stripe_api_write` is the
59
+ * sibling of `stripe_api_read` and carries every `POST`, `PATCH`, `PUT`, and
60
+ * `DELETE`. Additive writes leave `destructiveHint` unset: `readOnlyHint: false`
61
+ * already routes them through the approval path, and asserting destruction they
62
+ * do not perform would misstate their effect.
63
+ */
51
64
  const WRITE_TOOLS = new Map([
52
65
  ["stripe_api_write", "destructive"],
53
66
  ["create_refund", "destructive"],
67
+ // Creates and continues provider-side guide state; destroys nothing.
54
68
  ["stripe_implementation_planner", "additive"],
69
+ // Retrieval mixed with query-run creation behind one tool.
55
70
  ["stripe_analytics", "additive"],
56
71
  ["stripe_report", "additive"],
57
72
  ["send_stripe_mcp_feedback", "additive"],
58
73
  ]);
59
- /** Release-reviewed manifest; see provider conventions P5 and P13. */
74
+ /**
75
+ * One release-reviewed manifest, used both to classify a live tool and as the
76
+ * baseline the drift check compares against, so the annotation a caller gets and
77
+ * the verdict a check reads can never disagree. Stripe publishes no stability or
78
+ * deprecation policy for this tool set, so treat it as unversioned: an
79
+ * unclassified, unannotated name fails closed onto the approval path (P5).
80
+ */
60
81
  export const STRIPE_VETTED_CATALOG = vettedCatalog({
61
82
  reads: READ_ONLY_TOOLS,
62
83
  writes: WRITE_TOOLS,
@@ -64,7 +85,12 @@ export const STRIPE_VETTED_CATALOG = vettedCatalog({
64
85
  /** Stripe key prefixes carry their own mode; only a clear reading counts. */
65
86
  const LIVE_KEY = /\b(?:sk|rk|pk)_live_/;
66
87
  const TEST_KEY = /\b(?:sk|rk|pk)_test_/;
67
- /** Refuse a recognizable key/mode mismatch; see `documentation/stripe.md`. */
88
+ /**
89
+ * Refuse a recognizable key/mode mismatch. This is the only mode guard Connecta
90
+ * can offer, and it needs the key as a literal header to read the prefix: an
91
+ * operator-managed key is not in the deployment file, so its declared mode
92
+ * stands alone and a key pointed at the other environment fails at Stripe.
93
+ */
68
94
  function assertModeMatchesKey(id, mode, auth) {
69
95
  if (auth.type !== "headers")
70
96
  return;
@@ -1,4 +1,14 @@
1
- /** See documentation/vercel.md#no-sdk-on-purpose. */
1
+ /**
2
+ * No `@vercel/sdk` on purpose — not a dependency and not an optional peer.
3
+ * Direct fetch keeps the root Workers-safe, avoids shipping the SDK's generated
4
+ * model graph, and lets the reviewed named operations and the REST hatches share
5
+ * one guarded transport. The trade is API drift, handled explicitly rather than
6
+ * by the SDK's version bumps: `scripts/drift/vercel-endpoints.json` records the
7
+ * method, versioned path, spec revision, and request/response digest for every
8
+ * fixed endpoint, and `npm run providers:check -- --provider vercel` compares
9
+ * those rows with Vercel's published OpenAPI document at
10
+ * https://openapi.vercel.sh/ without needing a credential.
11
+ */
2
12
  import { api, defined } from "../connectors/api.js";
3
13
  import { remoteMcp } from "../connectors/remote-mcp.js";
4
14
  import { vettedCatalog, withVettedCatalog } from "../catalog-drift.js";
@@ -107,8 +107,8 @@ export interface RegistryView {
107
107
  signal?: AbortSignal;
108
108
  }): Promise<CallAdmissionPermit>;
109
109
  resultsStorage(): KVStorage;
110
- /** Reserve runtime-wide capacity before writing a paging envelope. */
111
- stashResult(key: string, value: string, ttlSeconds: number): Promise<boolean>;
110
+ /** Reserve runtime-wide capacity before writing a paging envelope's chunks. */
111
+ stashResult(key: string, chunks: readonly string[], ttlSeconds: number): Promise<boolean>;
112
112
  /** Local declared-vs-stored credential mismatch, with no downstream I/O. */
113
113
  credentialDriftFor(id: string): Promise<string | undefined>;
114
114
  /** Value-free shape learned from successful calls, never a provider declaration. */
@@ -240,8 +240,16 @@ export declare class Registry implements RegistryView {
240
240
  private observeCatalogDrift;
241
241
  /** Reject queued/future downstream admission; active permits release safely. */
242
242
  closeCallAdmission(): void;
243
- /** Reserve capacity and write one ASCII paging envelope in this runtime. */
244
- stashResult(key: string, value: string, ttlSeconds: number, prefix?: string): Promise<boolean>;
243
+ /**
244
+ * Reserve capacity and write one ASCII paging envelope in this runtime.
245
+ *
246
+ * `chunks[0]` lands on `<prefix><key>` and `chunks[n]` on `<prefix><key>#<n>`
247
+ * — the layout get_result reads back, so a page fetches only the chunks it
248
+ * covers instead of the whole stored result (issue #540). Chunking is a
249
+ * read-cost decision, not a capacity one: however many keys an envelope
250
+ * occupies, it is one stash entry charged its total ASCII length.
251
+ */
252
+ stashResult(key: string, chunks: readonly string[], ttlSeconds: number, prefix?: string): Promise<boolean>;
245
253
  /**
246
254
  * Storage namespaced to the meta-tool result store (`results:` prefix), kept
247
255
  * separate from any connector's `conn:<id>:` namespace. Backs get_result.
package/dist/registry.js CHANGED
@@ -446,12 +446,20 @@ export class Registry {
446
446
  for (const registry of this.personalRegistries.values())
447
447
  registry.closeCallAdmission();
448
448
  }
449
- /** Reserve capacity and write one ASCII paging envelope in this runtime. */
450
- async stashResult(key, value, ttlSeconds, prefix = "results:") {
449
+ /**
450
+ * Reserve capacity and write one ASCII paging envelope in this runtime.
451
+ *
452
+ * `chunks[0]` lands on `<prefix><key>` and `chunks[n]` on `<prefix><key>#<n>`
453
+ * — the layout get_result reads back, so a page fetches only the chunks it
454
+ * covers instead of the whole stored result (issue #540). Chunking is a
455
+ * read-cost decision, not a capacity one: however many keys an envelope
456
+ * occupies, it is one stash entry charged its total ASCII length.
457
+ */
458
+ async stashResult(key, chunks, ttlSeconds, prefix = "results:") {
451
459
  const maxBytes = this.opts.results?.maxStashBytes ?? 8 * 1024 * 1024;
452
460
  const maxEntries = this.opts.results?.maxStashEntries ?? 64;
453
461
  // The paging envelope is ASCII, so its string length is its stored byte count.
454
- const bytes = value.length;
462
+ const bytes = chunks.reduce((sum, chunk) => sum + chunk.length, 0);
455
463
  if (bytes > maxBytes || maxEntries === 0)
456
464
  return false;
457
465
  const now = Date.now();
@@ -462,7 +470,8 @@ export class Registry {
462
470
  try {
463
471
  // TTL alone cannot reclaim a lazy backend. Keep the charge until deletion
464
472
  // succeeds, including writes which persisted before throwing.
465
- await this.opts.storage.delete(oldKey);
473
+ for (const staleKey of entry.keys)
474
+ await this.opts.storage.delete(staleKey);
466
475
  this.resultStash.delete(oldKey);
467
476
  this.resultStashBytes -= entry.bytes;
468
477
  }
@@ -473,11 +482,16 @@ export class Registry {
473
482
  if (this.resultStash.size >= maxEntries || this.resultStashBytes + bytes > maxBytes)
474
483
  return false;
475
484
  const fullKey = prefix + key;
476
- const entry = { bytes, expiresAt: Infinity, busy: true };
485
+ const keys = chunks.map((_, index) => index === 0 ? fullKey : `${fullKey}#${index}`);
486
+ const entry = { keys, bytes, expiresAt: Infinity, busy: true };
477
487
  this.resultStash.set(fullKey, entry);
478
488
  this.resultStashBytes += bytes;
479
489
  try {
480
- await this.opts.storage.set(fullKey, value, { ttlSeconds });
490
+ // Trailing chunks first: the header chunk is what makes an id readable, so
491
+ // a write that fails midway leaves no envelope pointing at absent chunks.
492
+ for (let index = keys.length - 1; index >= 0; index--) {
493
+ await this.opts.storage.set(keys[index], chunks[index], { ttlSeconds });
494
+ }
481
495
  entry.expiresAt = Date.now() + ttlSeconds * 1000;
482
496
  return true;
483
497
  }
@@ -1201,8 +1215,8 @@ class ScopedRegistryView {
1201
1215
  }
1202
1216
  return registry.admitCall(...args);
1203
1217
  }
1204
- stashResult(key, value, ttlSeconds) {
1205
- return this.root.stashResult(key, value, ttlSeconds, this.scope.subjectKey ? `subject:${this.scope.subjectKey}:` : "results:");
1218
+ stashResult(key, chunks, ttlSeconds) {
1219
+ return this.root.stashResult(key, chunks, ttlSeconds, this.scope.subjectKey ? `subject:${this.scope.subjectKey}:` : "results:");
1206
1220
  }
1207
1221
  resultsStorage() {
1208
1222
  return this.scope.subjectKey
package/dist/types.d.ts CHANGED
@@ -519,6 +519,43 @@ export interface ConnectaBranding {
519
519
  };
520
520
  /** `theme-color` meta value. Defaults to "#ffffff". */
521
521
  themeColor?: string;
522
+ /**
523
+ * Operator-page appearance. Every field is optional, and a rejected value
524
+ * takes its default rather than failing the page, so `createConnecta` warns
525
+ * at startup about anything it dropped.
526
+ *
527
+ * Five knobs, not a palette. Surfaces, borders, muted text, and the status
528
+ * colors all derive from these, so a deployment sets an accent and gets a
529
+ * readable page instead of thirty chances to break one.
530
+ */
531
+ theme?: ConnectaTheme;
532
+ }
533
+ /**
534
+ * The operator UI's themeable tokens. These land in a `:root` block on the
535
+ * page, so each is gated by a narrow syntactic check: an unvalidated value
536
+ * here would let deployment config write arbitrary CSS.
537
+ */
538
+ export interface ConnectaTheme {
539
+ /**
540
+ * The brand color: links, focus rings, primary actions, the active nav item.
541
+ * Hex only (`#rgb`, `#rrggbb`, or `#rrggbbaa`). Defaults to `#2f5fe0`.
542
+ */
543
+ accent?: string;
544
+ /**
545
+ * Corner rounding for cards, inputs, and buttons. A CSS length (`10px`,
546
+ * `0.5rem`) or a bare number read as pixels. `0` restores square corners.
547
+ * Defaults to `10px`.
548
+ */
549
+ radius?: string | number;
550
+ /** Body font stack. A plain CSS font-family list. Defaults to a system stack. */
551
+ fontFamily?: string;
552
+ /** Monospace font stack for addresses, ids, and the endpoint URL. */
553
+ monoFamily?: string;
554
+ /**
555
+ * `"system"` (the default) follows the operator's OS setting; `"light"` and
556
+ * `"dark"` pin the page to one palette.
557
+ */
558
+ colorScheme?: "system" | "light" | "dark";
522
559
  }
523
560
  /** An inbound authentication provider (bearer token, interactive identity, ...). */
524
561
  export interface InboundAuth {
package/dist/ui.js CHANGED
@@ -8,7 +8,7 @@ import { mapSettledWithConcurrency, resolveDiscoveryConcurrency, } from "./concu
8
8
  import { OPERATOR_UI_CSS, OPERATOR_UI_SCRIPT, } from "./operator-ui/generated.js";
9
9
  import { CONNECTA_VERSION } from "./version.js";
10
10
  export { filterUiConnectors, } from "./operator-ui/model.js";
11
- import { resolveBranding, isSafeHttpsUrl } from "./branding.js";
11
+ import { resolveBranding, isSafeHttpsUrl, themeCss } from "./branding.js";
12
12
  export { CONNECTA_FAVICON_SVG, resolveBranding, isSafeHttpUrl, isSafeHttpsUrl, isSafeIconHref } from "./branding.js";
13
13
  /**
14
14
  * A JS string literal safe to inline in a script element. Escaping `/` keeps
@@ -281,8 +281,14 @@ export function renderUiHtml(uiAuth, mcpUrl = "/mcp", branding, nonce, page = "c
281
281
  const clerkScript = clerk && clerkScriptOrigin
282
282
  ? `<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
283
  : "";
284
+ // A pinned scheme is an attribute, not a stylesheet edit: the dark palette
285
+ // keys off `html[data-scheme]`, so "system" leaves the attribute off and the
286
+ // media query decides.
287
+ const schemeAttr = brand.theme.colorScheme === "system"
288
+ ? ""
289
+ : ` data-scheme="${brand.theme.colorScheme}"`;
284
290
  return `<!doctype html>
285
- <html lang="en">
291
+ <html lang="en"${schemeAttr}>
286
292
  <head>
287
293
  <meta charset="utf-8">
288
294
  <meta name="viewport" content="width=device-width, initial-scale=1">
@@ -292,22 +298,24 @@ export function renderUiHtml(uiAuth, mcpUrl = "/mcp", branding, nonce, page = "c
292
298
  <link rel="shortcut icon" href="/favicon.ico">
293
299
  <title>${escapeHtmlAttr(title)}</title>
294
300
  ${clerkScript}
295
- <style>${OPERATOR_UI_CSS}</style>
301
+ <style>${OPERATOR_UI_CSS}${themeCss(brand.theme)}</style>
296
302
  </head>
297
303
  <body>
298
304
  <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}
305
+ <header class="masthead shell">
306
+ <div class="masthead-inner">
307
+ <div class="mast-nav">
308
+ ${owner}
309
+ ${product}
310
+ </div>
303
311
  <div id="operatorNav"></div>
304
312
  </div>
305
313
  </header>
306
314
 
307
315
  <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">
316
+ <div class="lead">
317
+ <h1>${OPERATOR_PAGE_LABELS[page]}</h1>
318
+ <div class="lead-copy">
311
319
  <p>${escapeHtmlAttr(brand.description)}</p>
312
320
  <noscript><p class="msg">The operator pages need JavaScript. Nothing else here
313
321
  does — agents reach this deployment through <span class="mono">/mcp</span>.</p></noscript>
package/dist/version.d.ts CHANGED
@@ -4,4 +4,4 @@
4
4
  * a bump that forgets this file fails the build rather than shipping a stale
5
5
  * version to `/health` and to downstream MCP handshakes.
6
6
  */
7
- export declare const CONNECTA_VERSION = "0.24.3";
7
+ export declare const CONNECTA_VERSION = "0.24.4";
package/dist/version.js CHANGED
@@ -4,4 +4,4 @@
4
4
  * a bump that forgets this file fails the build rather than shipping a stale
5
5
  * version to `/health` and to downstream MCP handshakes.
6
6
  */
7
- export const CONNECTA_VERSION = "0.24.3";
7
+ export const CONNECTA_VERSION = "0.24.4";