@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
package/AGENTS.md CHANGED
@@ -32,8 +32,8 @@ Cloudflare Workers.
32
32
  `check:operator-ui` → `check:lint` → `check:unused` → `typecheck` → `test`
33
33
  (both vitest projects) → `build` → `check:examples`. It is also the `prepack`
34
34
  hook. `npm run release:check` adds `check:security` and `check:package` and is
35
- what CI runs on publish; use it when touching packaging, dependencies, or
36
- exports.
35
+ what CI runs on every pull request, on `main`, and again on publish; use it
36
+ when touching packaging, dependencies, or exports.
37
37
 
38
38
  ## The map
39
39
 
@@ -41,13 +41,13 @@ exports.
41
41
  decisions table, and the invariants every change must preserve. Check it
42
42
  before building something new; "we already decided not to" is a real answer
43
43
  there, and its removed/provisional verdicts override anything staler.
44
- - [`documentation/`](./documentation/) — per-subsystem guides for agents
45
- working on the repo. All of them are written now
46
- ([#348](https://github.com/zackbart/connecta/issues/348)); the retired manual
47
- (`docs/<name>.md`) survives only in git history, which is worth mining for
48
- rationale a rewrite dropped and worth nothing where it disagrees with
49
- `ethos.md`. Start with `architecture.md`, then the guide for the subsystem
50
- you are changing.
44
+ - [`documentation/`](./documentation/) — four guides for agents working on the
45
+ repo: `architecture.md`, `meta-tools.md`, `code-mode.md`, and `auth.md`.
46
+ Everything else — connectors, providers, admission, storage, the operator
47
+ UI, operations is documented where it lives, in the source and its tests.
48
+ The guides that once covered those survive only in git history, which is
49
+ worth mining for rationale and worth nothing where it disagrees with
50
+ `ethos.md` or the code. Start with `architecture.md`.
51
51
  - [`README.md`](./README.md) — the human-facing overview.
52
52
  - [`templates/node/`](./templates/node/) — the one standalone Node deployment
53
53
  shape copied by `connecta init`, Docker-ready rather than Docker-only. Keep
@@ -56,13 +56,14 @@ exports.
56
56
  diff away from one of them is the shape
57
57
  [#344](https://github.com/zackbart/connecta/issues/344) deleted.
58
58
 
59
- **Read `ethos.md` and the subsystem's guide before changing a subsystem.**
59
+ **Read `ethos.md` before changing a subsystem, and its guide when it has one.**
60
60
 
61
61
  ## Deployment setup
62
62
 
63
63
  `connecta init [directory]` is the golden path. It copies `templates/node/`,
64
64
  pins the generated deployment to the CLI package's exact version, restores the
65
- template `.gitignore`, and refuses to merge into an existing path.
65
+ template `.gitignore` and the `CLAUDE.md` symlink, and refuses to merge into an
66
+ existing path.
66
67
  `connecta doctor` verifies a running deployment's health, executor, and exact
67
68
  seven-tool surface. The template carries its own `Dockerfile` and
68
69
  `docker-compose.yml`, so the generated project is the container: setup changes
@@ -96,8 +97,9 @@ Two boundaries CI enforces that are not obvious from reading a file:
96
97
  forbidden is a platform-bound adapter becoming importable from the package,
97
98
  not a file appearing in the artifact. `@clerk/backend` and
98
99
  `quickjs-emscripten` are optional peers behind the `./auth/clerk` and
99
- `./quickjs` subpaths and must never become dependencies or install with
100
- core. Enforced by `test/package-surface.test.ts` and
100
+ `./quickjs` subpaths, and `@cloudflare/codemode` is the third, peered
101
+ without a subpath because a Worker deployment imports it directly. None may
102
+ become a dependency or install with core. Enforced by `test/package-surface.test.ts` and
101
103
  `scripts/check-package.mjs`. Anything heavyweight or platform-bound gets a
102
104
  subpath and an optional peer.
103
105
 
@@ -108,10 +110,7 @@ Every `*.test.ts` belongs to exactly one explicit list: runtime-portable suites
108
110
  in `WORKERS_SUITES`, Node-bound suites in `NODE_ONLY_SUITES` with a reason. The
109
111
  `node` project runs both lists; the `workers` project re-runs the portable list
110
112
  inside workerd. `test/suite-partition.test.ts` walks the directory and fails on
111
- an unclassified, double-classified, stale, or reasonless entry. New behavior
112
- also gets a row in the test map in `documentation/operations.md`, which
113
- classifies every suite — one missing from it is either new and undocumented or
114
- dead, and neither is a state to leave the repository in.
113
+ an unclassified, double-classified, stale, or reasonless entry.
115
114
 
116
115
  ## Conventions
117
116
 
@@ -132,9 +131,8 @@ dead, and neither is a state to leave the repository in.
132
131
  documentation and OpenAPI contracts and never reads a provider credential.
133
132
  Remote MCP schemas remain owned by the live `tools/list` response rather than
134
133
  a vendored copy. Findings are read by a human and become GitHub issues;
135
- nothing files itself.
136
- See
137
- [`documentation/provider-conventions.md`](./documentation/provider-conventions.md#the-maintainer-run-drift-check).
134
+ nothing files itself. `scripts/drift-check.mjs` is the reference for what
135
+ each flag covers.
138
136
  - **Releases.** `npm run release:check`, tag `v<version>` matching
139
137
  `package.json` exactly (the publish workflow verifies this and fails
140
138
  otherwise), and publishing fires on GitHub **Release publication**, not on the
package/CHANGELOG.md CHANGED
@@ -2,6 +2,64 @@
2
2
 
3
3
  All notable changes to this package are documented here.
4
4
 
5
+ ## 0.24.4 — 2026-09-17
6
+
7
+ The operator UI is the release. Its connections page is now a summary line and
8
+ one row per connector instead of a wall of expanded cards, and the stylesheet
9
+ behind it resolves through design tokens a deployment can set with the new
10
+ `branding.theme`. Nothing a deployment configures today changes meaning, no
11
+ storage format moved, and the page shows exactly what it showed before under
12
+ the same gates — an operator who has the page bookmarked will find it reads
13
+ differently, and that is the whole of the upgrade. The documentation cut to
14
+ four guides lands here too, along with one execution-path fix.
15
+
16
+ ### Added
17
+
18
+ - **`branding.theme`.** Five tokens — `accent`, `radius`, `fontFamily`,
19
+ `monoFamily`, and `colorScheme` — set on `operatorUi({ branding })`. Every
20
+ other color on the operator page is mixed from those, so setting one accent
21
+ themes the page rather than leaving half of it on the defaults. Light and
22
+ dark are the same tokens and follow the operator's OS setting unless
23
+ `colorScheme` pins one. Each token is gated the way the branding URLs already
24
+ were: a hex color, a CSS length, a plain font-family list, one of
25
+ `system`/`light`/`dark`. A rejected value takes the default and gets named in
26
+ a startup warning. The gates are narrow because these values land in a
27
+ `:root` block on the page, where anything unvalidated would be CSS injection.
28
+
29
+ ### Changed
30
+
31
+ - **The operator UI reads as a dashboard.** The connections page opens with one
32
+ summary line — connected, anything needing authorization, anything
33
+ unavailable, tools available — and then one row per connector: status dot,
34
+ name, auth scope, tool count, state. What used to be stacked in every card at
35
+ once (the description, the permission line, OAuth actions, the credential
36
+ panel, diagnostics, drift, and the tool list) is now behind a row an operator
37
+ expands, so a deployment with twenty connectors is a screen instead of a
38
+ scroll. Nothing about what the page may show or do changed: same payload,
39
+ same gates, and the drift panel still refuses to render a tool name or a
40
+ schema (#554).
41
+ - **The documentation is four guides.** `documentation/` now ships
42
+ `architecture.md`, `meta-tools.md`, `code-mode.md`, and `auth.md`. The
43
+ operations, upgrading, connector, provider, admission, storage, and operator
44
+ UI guides are removed; the rationale source comments used to defer to them
45
+ for now lives in those comments, and the provider conventions H1–H14 and
46
+ P1–P13 are defined in `test/provider-conventions.test.ts`. Each release's
47
+ opening paragraph here is the upgrade guidance. `records/` and `eval/` are
48
+ removed with the `load:admission` script and the CI job that served them. The
49
+ template and Worker example keep their `AGENTS.md`, which now points at this
50
+ changelog for upgrades (#552, #553).
51
+
52
+ ### Fixed
53
+
54
+ - **`get_result` paging cost.** A stashed result is stored as base64 chunks
55
+ under one key each — 48 KiB of text per chunk, wider for a result over
56
+ roughly 1.5 MB so the key and write count stays bounded — and a page reads
57
+ and decodes only the chunks it covers. Paging a large result no longer reads
58
+ the whole stash — let alone re-encodes it — once per page, so cost tracks the
59
+ page rather than the total. Offsets, `nextOffset`, `totalBytes`, and
60
+ character-boundary alignment are unchanged, and entries stashed in the
61
+ previous formats stay readable for the rest of their 15-minute TTL (#540).
62
+
5
63
  ## 0.24.3 — 2026-09-16
6
64
 
7
65
  A bug-fix release from a full audit of the execution path, invocation, catalog
@@ -163,7 +221,7 @@ returns `"all"` or connector ids and declares no pools.
163
221
  - **Tool-level grants in `identity.connectorAccess`.** Entries may be a
164
222
  connector id (every tool) or an exact `connector.tool` address (that tool
165
223
  only); grants are additive. The scoped registry view filters below the
166
- catalog service, so `search_tools`, `describe_tools`, `call_tool`,
224
+ catalog service, so `search_tools`, `call_tool`,
167
225
  `call_destructive_tool`, a program's `connecta.search` and `connecta.call`,
168
226
  and the connection UI all see the same list, and an ungranted tool fails as
169
227
  `unknown_tool` exactly like an absent one. There is no wildcard: a remote
@@ -173,6 +231,11 @@ returns `"all"` or connector ids and declares no pools.
173
231
 
174
232
  ## 0.24.1 — 2026-09-08
175
233
 
234
+ A small additive release for deployments whose providers run long: two
235
+ optional `execute.*` settings and a log line for failed calls. Nothing breaks
236
+ and nothing needs migrating; a deployment that sets neither option behaves as
237
+ it did on 0.24.0.
238
+
176
239
  ### Added
177
240
 
178
241
  - `execute.maxHostCalls` and `execute.hostCallTimeoutMs` configure the
package/README.md CHANGED
@@ -95,9 +95,7 @@ Cloudflare Access supplies Worker identity; Node can use Clerk or the optional
95
95
  configured bearer adapter. Connecta owns no accounts or groups and issues no
96
96
  client access tokens. Shared-credential administration and personal connection
97
97
  setup require separate explicit permissions, both denied by default. See
98
- [inbound auth](./documentation/auth.md#principals-visibility-and-operators),
99
- [shared and personal auth](./documentation/storage-and-credentials.md#shared-and-personal-auth),
100
- and the [module migration guide](./documentation/upgrading.md#0240-optional-modules).
98
+ [inbound auth](./documentation/auth.md#principals-visibility-and-operators).
101
99
 
102
100
  Connecta is not a platform, a marketplace, a policy engine, or a multi-tenant
103
101
  service. Those are decisions, and the [ethos](./ethos.md) records each one
@@ -107,9 +105,10 @@ and why.
107
105
 
108
106
  Setup is written for an agent. Point yours at [`AGENTS.md`](./AGENTS.md) and
109
107
  ask it to set up a Connecta deployment; the
110
- [documentation](./documentation/) covers every subsystem if you want to go
111
- deeper, and [upgrading](./documentation/upgrading.md) an existing deployment
112
- is its own runbook.
108
+ [documentation](./documentation/) covers the architecture, the seven tools,
109
+ code mode, and inbound auth if you want to go deeper. When upgrading an
110
+ existing deployment, each [changelog](./CHANGELOG.md) release opens with what
111
+ breaks and what a deployment can ignore.
113
112
 
114
113
  ## Status
115
114
 
@@ -1,6 +1,13 @@
1
- import type { ConnectaBranding, UiAuthConfig } from "./types.js";
1
+ import type { ConnectaBranding, ConnectaTheme, UiAuthConfig } from "./types.js";
2
2
  /** Connecta's default monochrome "C" mark. */
3
3
  export declare const CONNECTA_FAVICON_SVG = "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 32 32\">\n <style>\n .fg { fill: #000 }\n @media (prefers-color-scheme: dark) { .fg { fill: #fff } }\n </style>\n <path class=\"fg\" d=\"M27 9.4A13 13 0 1 0 27 22.6l-4.4-2.5a8 8 0 1 1 0-8.2z\"/>\n</svg>";
4
+ export interface ResolvedTheme {
5
+ accent?: string;
6
+ radius?: string;
7
+ fontFamily?: string;
8
+ monoFamily?: string;
9
+ colorScheme: "system" | "light" | "dark";
10
+ }
4
11
  interface ResolvedBranding {
5
12
  productName: string;
6
13
  productUrl?: string;
@@ -12,8 +19,30 @@ interface ResolvedBranding {
12
19
  /** href for the page's icon link. */
13
20
  faviconHref: string;
14
21
  themeColor: string;
22
+ /** Only the tokens that survived their gate; the stylesheet owns the rest. */
23
+ theme: ResolvedTheme;
15
24
  }
16
25
  export declare function resolveBranding(branding?: ConnectaBranding): ResolvedBranding;
26
+ /**
27
+ * Read a theme the way branding URLs are read: gate every field, drop what
28
+ * fails, never throw. This runs during `createConnecta`, so a malformed value
29
+ * has to fall back to the stylesheet default instead of refusing to serve the
30
+ * page. `droppedThemeTokens` names the drops for the startup warning.
31
+ */
32
+ export declare function resolveTheme(theme?: ConnectaTheme): ResolvedTheme;
33
+ /**
34
+ * Names of the theme tokens the operator set that failed their gate. Same
35
+ * contract as `droppedBrandingUrls`: rendering falls back silently, so this is
36
+ * the only place an operator learns their value never reached the page.
37
+ */
38
+ export declare function droppedThemeTokens(theme?: ConnectaTheme): string[];
39
+ /**
40
+ * The resolved theme as a `:root` block, or "" when a deployment configured
41
+ * nothing. It is emitted after the stylesheet so it overrides the defaults.
42
+ * There is no escaping here: every value has already passed a gate above, and
43
+ * anything that would need escaping is dropped rather than rewritten.
44
+ */
45
+ export declare function themeCss(theme: ResolvedTheme): string;
17
46
  /**
18
47
  * Names of the branding URLs the operator set that failed their gate and were
19
48
  * replaced by a default. Lives beside the gates so the startup warning cannot
@@ -55,7 +84,7 @@ export declare function isSafeHttpsUrl(url: unknown): boolean;
55
84
  * `frontendApiUrl` is required, so anything that fails its gate is a drop.
56
85
  * `signInUrl` and `signUpUrl` are optional, so only a value the operator
57
86
  * *supplied* and the gate then rejected is worth a warning — an unset field
58
- * took no default away from anyone. `isSetUrlValue` decides that, the same way
87
+ * took no default away from anyone. `isSetValue` decides that, the same way
59
88
  * and for the same reasons it decides it for the branding URLs: a warning that
60
89
  * fires for one and not the other would be reporting on the field rather than
61
90
  * on the operator's intent. Rendering is not consulted for this: it drops on
package/dist/branding.js CHANGED
@@ -38,17 +38,125 @@ export function resolveBranding(branding) {
38
38
  ? faviconHref
39
39
  : DEFAULT_FAVICON_HREF,
40
40
  themeColor: trimmedString(branding?.themeColor) ?? "#ffffff",
41
+ theme: resolveTheme(branding?.theme),
41
42
  };
42
43
  }
44
+ /**
45
+ * Hex colors only: `#rgb`, `#rrggbb`, `#rrggbbaa`. A hex value cannot carry a
46
+ * `url()`, a `var()`, or a closing brace into the `:root` block it is written
47
+ * into, which is the whole reason the gate is this narrow.
48
+ */
49
+ const HEX_COLOR = /^#(?:[0-9a-f]{3}|[0-9a-f]{4}|[0-9a-f]{6}|[0-9a-f]{8})$/i;
50
+ /** A single non-negative CSS length, or a bare number the caller reads as px. */
51
+ const CSS_LENGTH = /^(?:0|[0-9]{1,3}(?:\.[0-9]{1,3})?)(px|rem|em)?$/;
52
+ /**
53
+ * One family name: bare, or wrapped in matching quotes. The character class
54
+ * excludes everything CSS needs to end a declaration or open a function (`;`,
55
+ * `{`, `}`, `(`, `)`, backslash, `<`, `>`, `@`, `*`, `/`, `:`), and matched
56
+ * quotes mean the value cannot leave an open string that swallows the CSS
57
+ * after it.
58
+ */
59
+ const FONT_NAME = /^(?:"[a-z0-9 ._-]+"|'[a-z0-9 ._-]+'|[a-z][a-z0-9 ._-]*)$/i;
60
+ /** A font-family list: comma-separated names and nothing else. */
61
+ function isFontStack(value) {
62
+ if (value.length > 200)
63
+ return false;
64
+ const names = value.split(",");
65
+ return names.length <= 12 &&
66
+ names.every((name) => FONT_NAME.test(name.trim()));
67
+ }
68
+ const COLOR_SCHEMES = ["system", "light", "dark"];
69
+ /**
70
+ * Read a theme the way branding URLs are read: gate every field, drop what
71
+ * fails, never throw. This runs during `createConnecta`, so a malformed value
72
+ * has to fall back to the stylesheet default instead of refusing to serve the
73
+ * page. `droppedThemeTokens` names the drops for the startup warning.
74
+ */
75
+ export function resolveTheme(theme) {
76
+ const accent = trimmedString(theme?.accent);
77
+ const fontFamily = trimmedString(theme?.fontFamily);
78
+ const monoFamily = trimmedString(theme?.monoFamily);
79
+ const scheme = trimmedString(theme?.colorScheme);
80
+ const radius = radiusLength(theme?.radius);
81
+ return {
82
+ ...(accent && HEX_COLOR.test(accent) ? { accent } : {}),
83
+ ...(radius !== undefined ? { radius } : {}),
84
+ ...(fontFamily && isFontStack(fontFamily) ? { fontFamily } : {}),
85
+ ...(monoFamily && isFontStack(monoFamily) ? { monoFamily } : {}),
86
+ colorScheme: COLOR_SCHEMES.includes(scheme)
87
+ ? scheme
88
+ : "system",
89
+ };
90
+ }
91
+ /**
92
+ * `radius` accepts a number as well as a string, since a config file is more
93
+ * likely to say `10` than `"10px"`. A bare number means pixels; a string must
94
+ * carry its own unit or be zero.
95
+ */
96
+ function radiusLength(radius) {
97
+ if (typeof radius === "number") {
98
+ return Number.isFinite(radius) && radius >= 0 && radius <= 999
99
+ ? `${radius}px`
100
+ : undefined;
101
+ }
102
+ const value = trimmedString(radius);
103
+ if (!value || !CSS_LENGTH.test(value))
104
+ return undefined;
105
+ return /[a-z]$/i.test(value) || value === "0" ? value : `${value}px`;
106
+ }
107
+ /**
108
+ * Names of the theme tokens the operator set that failed their gate. Same
109
+ * contract as `droppedBrandingUrls`: rendering falls back silently, so this is
110
+ * the only place an operator learns their value never reached the page.
111
+ */
112
+ export function droppedThemeTokens(theme) {
113
+ if (!theme)
114
+ return [];
115
+ const resolved = resolveTheme(theme);
116
+ const dropped = [];
117
+ if (isSetValue(theme.accent) && !resolved.accent)
118
+ dropped.push("accent");
119
+ if (isSetValue(theme.radius) && resolved.radius === undefined) {
120
+ dropped.push("radius");
121
+ }
122
+ if (isSetValue(theme.fontFamily) && !resolved.fontFamily) {
123
+ dropped.push("fontFamily");
124
+ }
125
+ if (isSetValue(theme.monoFamily) && !resolved.monoFamily) {
126
+ dropped.push("monoFamily");
127
+ }
128
+ // Compared against the trimmed value the resolver reads, so `" dark "` is
129
+ // not reported as dropped when it was applied.
130
+ if (isSetValue(theme.colorScheme) &&
131
+ trimmedString(theme.colorScheme) !== resolved.colorScheme) {
132
+ dropped.push("colorScheme");
133
+ }
134
+ return dropped.map((token) => `theme.${token}`);
135
+ }
136
+ /**
137
+ * The resolved theme as a `:root` block, or "" when a deployment configured
138
+ * nothing. It is emitted after the stylesheet so it overrides the defaults.
139
+ * There is no escaping here: every value has already passed a gate above, and
140
+ * anything that would need escaping is dropped rather than rewritten.
141
+ */
142
+ export function themeCss(theme) {
143
+ const declarations = [
144
+ theme.accent ? `--accent:${theme.accent}` : "",
145
+ theme.radius !== undefined ? `--radius:${theme.radius}` : "",
146
+ theme.fontFamily ? `--sans:${theme.fontFamily}` : "",
147
+ theme.monoFamily ? `--mono:${theme.monoFamily}` : "",
148
+ ].filter(Boolean);
149
+ return declarations.length ? `:root{${declarations.join(";")}}` : "";
150
+ }
43
151
  /**
44
152
  * Whether the operator meant to supply a value here — the question every
45
- * dropped-URL warning asks before naming a field, and one definition so the
153
+ * dropped-value warning asks before naming a field, and one definition so the
46
154
  * branding and `uiAuth` warnings cannot answer it differently. A non-string
47
155
  * counts as set: the intent was there and is exactly what the warning reports
48
156
  * on. A blank or whitespace-only string does not; that is indistinguishable
49
157
  * from leaving the field alone, and both take the default silently.
50
158
  */
51
- function isSetUrlValue(value) {
159
+ function isSetValue(value) {
52
160
  return typeof value === "string"
53
161
  ? trimmedString(value) !== undefined
54
162
  : value !== undefined && value !== null;
@@ -65,13 +173,13 @@ export function droppedBrandingUrls(branding) {
65
173
  const resolved = resolveBranding(branding);
66
174
  const faviconHref = branding.favicon?.href;
67
175
  return [
68
- ...(isSetUrlValue(branding.productUrl) && !resolved.productUrl
176
+ ...(isSetValue(branding.productUrl) && !resolved.productUrl
69
177
  ? ["productUrl"]
70
178
  : []),
71
- ...(isSetUrlValue(branding.ownerUrl) && !resolved.ownerUrl
179
+ ...(isSetValue(branding.ownerUrl) && !resolved.ownerUrl
72
180
  ? ["ownerUrl"]
73
181
  : []),
74
- ...(isSetUrlValue(faviconHref) &&
182
+ ...(isSetValue(faviconHref) &&
75
183
  trimmedString(faviconHref) !== resolved.faviconHref
76
184
  ? ["favicon.href"]
77
185
  : []),
@@ -153,7 +261,7 @@ export function isSafeHttpsUrl(url) {
153
261
  * `frontendApiUrl` is required, so anything that fails its gate is a drop.
154
262
  * `signInUrl` and `signUpUrl` are optional, so only a value the operator
155
263
  * *supplied* and the gate then rejected is worth a warning — an unset field
156
- * took no default away from anyone. `isSetUrlValue` decides that, the same way
264
+ * took no default away from anyone. `isSetValue` decides that, the same way
157
265
  * and for the same reasons it decides it for the branding URLs: a warning that
158
266
  * fires for one and not the other would be reporting on the field rather than
159
267
  * on the operator's intent. Rendering is not consulted for this: it drops on
@@ -166,10 +274,10 @@ export function droppedUiAuthUrls(uiAuth) {
166
274
  return [];
167
275
  return [
168
276
  ...(isSafeHttpsUrl(uiAuth.frontendApiUrl) ? [] : ["uiAuth.frontendApiUrl"]),
169
- ...(isSetUrlValue(uiAuth.signInUrl) && !isSafeHttpsUrl(uiAuth.signInUrl)
277
+ ...(isSetValue(uiAuth.signInUrl) && !isSafeHttpsUrl(uiAuth.signInUrl)
170
278
  ? ["uiAuth.signInUrl"]
171
279
  : []),
172
- ...(isSetUrlValue(uiAuth.signUpUrl) && !isSafeHttpsUrl(uiAuth.signUpUrl)
280
+ ...(isSetValue(uiAuth.signUpUrl) && !isSafeHttpsUrl(uiAuth.signUpUrl)
173
281
  ? ["uiAuth.signUpUrl"]
174
282
  : []),
175
283
  ];
@@ -68,5 +68,5 @@ export interface ApiOptions {
68
68
  validateArgs?: boolean;
69
69
  tools: ApiTool[];
70
70
  }
71
- /** A static connector; see provider conventions' two construction-time checks. */
71
+ /** A static connector; every tool passes {@link checkToolContract} first. */
72
72
  export declare function api(id: string, opts: ApiOptions): Connector;
@@ -3,7 +3,15 @@ import { compileValidator, validateToolInput } from "../validate.js";
3
3
  export function defined(value) {
4
4
  return Object.fromEntries(Object.entries(value).filter(([, item]) => item !== undefined));
5
5
  }
6
- /** Enforce provider conventions' two construction-time checks. */
6
+ /**
7
+ * Three things a hand-written surface is refused for at construction rather
8
+ * than in production (#340): no description (discovery has nothing to route on
9
+ * and a guess costs a call), no explicit `readOnlyHint` (connecta never infers
10
+ * the safety class, so an unclassified tool is a deployment bug), and an
11
+ * `inputSchema` the validator cannot compile (declaring one is optional;
12
+ * declaring an unenforceable one is not). None of this reaches a proxied
13
+ * catalog — the contract binds the surfaces we write, not the ones we relay.
14
+ */
7
15
  function checkToolContract(id, tool) {
8
16
  const address = `${id}.${tool.name}`;
9
17
  if (typeof tool.description !== "string" || tool.description.trim() === "") {
@@ -20,7 +28,7 @@ function checkToolContract(id, tool) {
20
28
  if (tool.inputSchema)
21
29
  compileValidator(tool.inputSchema, { address });
22
30
  }
23
- /** A static connector; see provider conventions' two construction-time checks. */
31
+ /** A static connector; every tool passes {@link checkToolContract} first. */
24
32
  export function api(id, opts) {
25
33
  for (const t of opts.tools)
26
34
  checkToolContract(id, t);
@@ -87,6 +87,10 @@ export interface GuardedFetchOptions {
87
87
  }
88
88
  /** Send one guarded request and map its response with provider knowledge. */
89
89
  export type GuardedTransport = <T>(request: GuardedRequest, ctx: ConnectorContext, map: GuardedResponseMapper<T>) => Promise<T>;
90
- /** Build the guarded transport described in documentation/connectors.md. */
90
+ /**
91
+ * Build the transport a connector sends every request through. `maxResponseBytes`
92
+ * is required rather than defaulted: what counts as an absurd response is a fact
93
+ * about the API, not about HTTP.
94
+ */
91
95
  export declare function guardedFetch(options: GuardedFetchOptions): GuardedTransport;
92
96
  export {};
@@ -1,4 +1,17 @@
1
- /** See documentation/connectors.md#the-guarded-fetch-transport. Web APIs only. */
1
+ /**
2
+ * The safety machinery every hand-written HTTP surface needs, extracted once
3
+ * (#341): path confinement, encoded query and framed bodies, per-request
4
+ * credential headers applied last, a refused redirect, a bounded read, and
5
+ * network-failure normalization. It owns no *meaning* — it never reads a status
6
+ * code and never invents an auth scheme, because the same 403 means an ungranted
7
+ * capability at one provider and a token scope at another, and those want
8
+ * opposite next moves. The provider's `authenticate` supplies headers and its
9
+ * mapper turns one `GuardedResponse` into a result or a typed failure.
10
+ *
11
+ * Not exported this release: the migrations proved the shape preserves behavior
12
+ * for connectors that already had this machinery, not that it is the right
13
+ * public surface. Web APIs only.
14
+ */
2
15
  import { ConnectorCallError, unavailableCallError } from "../errors.js";
3
16
  /** Parse delta-seconds or an HTTP-date into a non-negative wait window. */
4
17
  export function retryAfterMs(headers) {
@@ -123,7 +136,18 @@ async function drain(provider, stream, limit) {
123
136
  }
124
137
  return body;
125
138
  }
126
- /** See documentation/connectors.md#the-guarded-fetch-transport. */
139
+ /**
140
+ * Every read is capped at `maxResponseBytes`: a streaming body is abandoned at
141
+ * the ceiling rather than buffered past it, and where a runtime gives no body
142
+ * stream the text is measured in UTF-8 bytes before it is accepted, with JSON
143
+ * parsed from that same bounded text. Such a runtime still buffers internally,
144
+ * but cannot return an oversized body as a successful result.
145
+ *
146
+ * A mapper must re-throw `ConnectorCallError` and swallow only what it
147
+ * recognizes: a bare `catch` around `response.json()` eats this refusal along
148
+ * with a parse error and turns a response nobody was allowed to read into an
149
+ * empty success.
150
+ */
127
151
  function boundedResponse(provider, response, limit) {
128
152
  if (BODILESS_STATUSES.has(response.status)) {
129
153
  const emptyJson = async () => undefined;
@@ -186,7 +210,11 @@ async function jsonResult(read) {
186
210
  return { parseError: cause };
187
211
  }
188
212
  }
189
- /** Build the guarded transport described in documentation/connectors.md. */
213
+ /**
214
+ * Build the transport a connector sends every request through. `maxResponseBytes`
215
+ * is required rather than defaulted: what counts as an absurd response is a fact
216
+ * about the API, not about HTTP.
217
+ */
190
218
  export function guardedFetch(options) {
191
219
  const { provider, maxResponseBytes: limit } = options;
192
220
  if (!Number.isInteger(limit) || limit < 1) {
@@ -229,7 +257,9 @@ export function guardedFetch(options) {
229
257
  : request.rawBody !== undefined
230
258
  ? { body: request.rawBody }
231
259
  : {}),
232
- // Rationale: documentation/connectors.md#the-guarded-fetch-transport.
260
+ // A redirect is an instruction to re-send the credential to whatever
261
+ // origin `Location` names, and a confinement a redirect can undo was
262
+ // never one. Refused below rather than followed.
233
263
  redirect: "manual",
234
264
  ...(ctx.signal ? { signal: ctx.signal } : {}),
235
265
  });
@@ -105,8 +105,11 @@ function isCursorShapeError(err) {
105
105
  * close with the session left to age out as it did before.
106
106
  */
107
107
  async function terminateSession(transport, logger, connectorId) {
108
- // See documentation/connectors.md#mcp-version-skew for the legacy DELETE
109
- // that Client.close() does not send.
108
+ // SDK v2's Client.close() does not send the legacy session DELETE on our
109
+ // behalf. Connecta's own endpoint creates no protocol session, but a stateful
110
+ // legacy downstream can still issue `Mcp-Session-Id`, and every path that
111
+ // abandons one — scope teardown, credential rotation, OAuth retirement, an
112
+ // abandoned connect — owes it this best-effort, one-second DELETE.
110
113
  const terminate = transport.terminateSession;
111
114
  if (typeof terminate !== "function")
112
115
  return;
@@ -867,8 +870,9 @@ export function remoteMcp(id, opts) {
867
870
  // the accumulator is returned rather than stored: a cursor is opaque and
868
871
  // session-bound, so nothing here may outlive this call.
869
872
  async listTools(ctx) {
870
- // The complete-catalog rule is documented at
871
- // documentation/connectors.md#catalog-contract.
873
+ // The catalog contract: a downstream catalog is complete or it is a
874
+ // failure. Follow every page to the end of the cursor chain, preserve
875
+ // schemas and annotations, and never cache or serve a partial walk.
872
876
  const state = stateFor(ctx);
873
877
  await ensureConnected(ctx, state);
874
878
  // Bind the client once so the whole walk provably rides one session — a
package/dist/errors.d.ts CHANGED
@@ -2,11 +2,19 @@
2
2
  * Machine-readable classification of a failed connector tool call.
3
3
  *
4
4
  * A code earns its place by changing what the caller does next, never by
5
- * naming a cause — the rule provider conventions call H11.
5
+ * naming a cause — the rule provider conventions call H11, defined in
6
+ * `test/provider-conventions.test.ts`.
6
7
  */
7
8
  export type ConnectorCallErrorCode = "timeout" | "auth_required" | "rate_limited" | "unavailable" | "invalid_args"
8
- /** Provider-owned absence; see provider-conventions.md H11 for the rule and
9
- * the permission-ambiguity exception. */
9
+ /**
10
+ * Provider-owned absence: the caller re-addresses rather than waiting,
11
+ * re-authorizing, or repairing arguments, and a program looping over ids can
12
+ * continue past it where `connector_call_failed` would abort. Map a status
13
+ * here only where the provider distinguishes absence from a permission gap —
14
+ * where it does not (Notion's `object_not_found` covers both), the honest
15
+ * code stays `connector_call_failed` or `auth_required` with a message that
16
+ * states the ambiguity (H11).
17
+ */
10
18
  | "not_found" | "input_required_unsupported" | "connector_call_failed";
11
19
  /** One bounded, payload-free explanation of an input-schema mismatch. */
12
20
  export interface ArgumentValidationIssue {
package/dist/errors.js CHANGED
@@ -20,7 +20,8 @@ function boundedIssueText(value, maxChars) {
20
20
  * the payload lands in both the text content and `structuredContent`. The agent
21
21
  * already holds what it sent; the echo is a convenience, never the record.
22
22
  */
23
- // Shared by argument and text echoes; see meta-tools.md lines 360-372.
23
+ // One budget shared by the argument and text echoes, so a caller cannot make a
24
+ // refusal larger by splitting what it sent across the two.
24
25
  const MAX_ECHOED_BYTES = 512;
25
26
  /**
26
27
  * The same budget spent on caller-authored *text* — the address it mistyped,
package/dist/index.d.ts CHANGED
@@ -222,5 +222,5 @@ export { CONNECTA_VERSION } from "./version.js";
222
222
  export type { Registry } from "./registry.js";
223
223
  export type { RemoteMcpOptions, RemoteMcpAuth, RemoteMcpRedirectPolicy, } from "./connectors/remote-mcp.js";
224
224
  export type { ApiOptions, ApiTool } from "./connectors/api.js";
225
- export type { CatalogDriftCounts, CatalogDriftReport, ConnectaBranding, Connector, ConnectorCallAdmissionInput, ConnectorCallAdmissionPolicy, ConnectorCallAdmissionRule, ConnectorRollingWindowBudget, ConnectorCredentialAccess, ConnectorCredentialConfig, ConnectorCredentialFieldConfig, ConnectorCredentialValues, ConnectorContext, ConnectorUsageGuide, ConnectorStatus, CredentialTestResult, AdmittingExecutor, AdmissionSnapshot, ExecuteResult, Executor, ExecutorLease, ExecutorProvider, InboundAuth, InboundAuthRuntimeContext, UiAuthConfig, AuthResult, AuthenticatedIdentity, IdentityReference, JsonSchema, KVStorage, Logger, ToolDef, ToolAnnotations, } from "./types.js";
225
+ export type { CatalogDriftCounts, CatalogDriftReport, ConnectaBranding, ConnectaTheme, Connector, ConnectorCallAdmissionInput, ConnectorCallAdmissionPolicy, ConnectorCallAdmissionRule, ConnectorRollingWindowBudget, ConnectorCredentialAccess, ConnectorCredentialConfig, ConnectorCredentialFieldConfig, ConnectorCredentialValues, ConnectorContext, ConnectorUsageGuide, ConnectorStatus, CredentialTestResult, AdmittingExecutor, AdmissionSnapshot, ExecuteResult, Executor, ExecutorLease, ExecutorProvider, InboundAuth, InboundAuthRuntimeContext, UiAuthConfig, AuthResult, AuthenticatedIdentity, IdentityReference, JsonSchema, KVStorage, Logger, ToolDef, ToolAnnotations, } from "./types.js";
226
226
  export type { ActivityActor, ActivityCallSource, ActivityOutcome, ActivityPage, ActivityReadActor, ActivityReadEvent, ActivityReader, ActivityReadGate, ActivityReadPage, ActivitySink, ActivityStore, AgentFriction, CatalogDriftActivityEvent, ToolCallActivityEvent, } from "./activity.js";
package/dist/index.js CHANGED
@@ -2,7 +2,7 @@ import { credentialTestRule, describeCredentialTestMismatch, } from "./credentia
2
2
  import { Registry } from "./registry.js";
3
3
  import { parseConnectorAccess, POOL_NAME_RE } from "./connector-access.js";
4
4
  import { createFetchHandler } from "./server.js";
5
- import { droppedBrandingUrls, droppedUiAuthUrls } from "./branding.js";
5
+ import { droppedBrandingUrls, droppedThemeTokens, droppedUiAuthUrls, } from "./branding.js";
6
6
  import { memoryStorage } from "./storage/memory.js";
7
7
  import { CONNECTA_VERSION } from "./version.js";
8
8
  import { AdmissionController, executorName, isAdmittingExecutor, withExecutorAdmission, } from "./executor-admission.js";
@@ -262,6 +262,17 @@ function warnInsecureConfig(config, inboundAuth, logger) {
262
262
  "used as an href, so it must be an absolute http(s) URL (favicon.href " +
263
263
  "may also be a root-relative path). The default is rendered instead.");
264
264
  }
265
+ // Theme tokens are written into a `:root` block, so each is gated
266
+ // syntactically and a rejected value takes the stylesheet's default. Same
267
+ // reason as the branding URLs above: the page still renders, so without this
268
+ // line the only evidence is that the operator's color never showed up.
269
+ const droppedTheme = droppedThemeTokens(config.ui?.branding?.theme);
270
+ if (droppedTheme.length > 0) {
271
+ logger.warn(`[connecta] branding ${droppedTheme.join(", ")} dropped: accent must be ` +
272
+ "a hex color, radius a CSS length, the font families a plain " +
273
+ "font-family list, and colorScheme one of system/light/dark. The " +
274
+ "default is rendered instead.");
275
+ }
265
276
  // Operator shells render exactly one provider's browser sign-in config — the
266
277
  // first that offers one, matching the server route's `find` — and that
267
278
  // provider's URLs reach the browser: frontendApiUrl as the loader's