@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.
- package/AGENTS.md +18 -20
- package/CHANGELOG.md +64 -1
- package/README.md +5 -6
- package/dist/branding.d.ts +31 -2
- package/dist/branding.js +116 -8
- package/dist/connectors/api.d.ts +1 -1
- package/dist/connectors/api.js +10 -2
- package/dist/connectors/guarded-fetch.d.ts +5 -1
- package/dist/connectors/guarded-fetch.js +34 -4
- package/dist/connectors/remote-mcp.js +8 -4
- package/dist/errors.d.ts +11 -3
- package/dist/errors.js +2 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +12 -1
- package/dist/meta-tools.js +105 -29
- package/dist/operator-ui/generated.js +2 -2
- package/dist/operator-ui/view.d.ts +38 -1
- package/dist/operator-ui/view.js +71 -0
- package/dist/providers/cloudflare.d.ts +14 -2
- package/dist/providers/cloudflare.js +107 -16
- package/dist/providers/linear.d.ts +26 -4
- package/dist/providers/linear.js +19 -4
- package/dist/providers/mixpanel.d.ts +16 -3
- package/dist/providers/mixpanel.js +13 -2
- package/dist/providers/notion.d.ts +8 -1
- package/dist/providers/notion.js +83 -10
- package/dist/providers/revenuecat.d.ts +30 -4
- package/dist/providers/revenuecat.js +42 -4
- package/dist/providers/stripe.d.ts +7 -1
- package/dist/providers/stripe.js +30 -4
- package/dist/providers/vercel.js +11 -1
- package/dist/registry.d.ts +12 -4
- package/dist/registry.js +22 -8
- package/dist/types.d.ts +37 -0
- package/dist/ui.js +18 -10
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/documentation/architecture.md +193 -181
- package/documentation/auth.md +197 -176
- package/documentation/code-mode.md +426 -321
- package/documentation/meta-tools.md +356 -416
- package/examples/worker/AGENTS.md +2 -1
- package/examples/worker/README.md +12 -10
- package/examples/worker/src/index.ts +12 -15
- package/package.json +1 -2
- package/templates/node/.env.example +3 -3
- package/templates/node/AGENTS.md +5 -4
- package/templates/node/README.md +2 -1
- package/templates/node/package.json +1 -1
- package/templates/node/src/index.ts +23 -22
- package/documentation/call-admission.md +0 -158
- package/documentation/cloudflare.md +0 -471
- package/documentation/connector-guides.md +0 -176
- package/documentation/connectors.md +0 -431
- package/documentation/linear.md +0 -193
- package/documentation/mixpanel.md +0 -160
- package/documentation/notion.md +0 -308
- package/documentation/operations.md +0 -359
- package/documentation/operator-ui.md +0 -135
- package/documentation/optional-modules-upgrade.md +0 -243
- package/documentation/provider-conventions.md +0 -729
- package/documentation/request-admission.md +0 -204
- package/documentation/revenuecat.md +0 -305
- package/documentation/storage-and-credentials.md +0 -254
- package/documentation/stripe.md +0 -262
- package/documentation/upgrading.md +0 -768
- package/documentation/vercel.md +0 -241
package/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
|
|
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/) —
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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`
|
|
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
|
|
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
|
|
100
|
-
|
|
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.
|
|
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
|
-
|
|
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`, `
|
|
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
|
|
111
|
-
|
|
112
|
-
|
|
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
|
|
package/dist/branding.d.ts
CHANGED
|
@@ -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. `
|
|
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-
|
|
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
|
|
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
|
-
...(
|
|
176
|
+
...(isSetValue(branding.productUrl) && !resolved.productUrl
|
|
69
177
|
? ["productUrl"]
|
|
70
178
|
: []),
|
|
71
|
-
...(
|
|
179
|
+
...(isSetValue(branding.ownerUrl) && !resolved.ownerUrl
|
|
72
180
|
? ["ownerUrl"]
|
|
73
181
|
: []),
|
|
74
|
-
...(
|
|
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. `
|
|
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
|
-
...(
|
|
277
|
+
...(isSetValue(uiAuth.signInUrl) && !isSafeHttpsUrl(uiAuth.signInUrl)
|
|
170
278
|
? ["uiAuth.signInUrl"]
|
|
171
279
|
: []),
|
|
172
|
-
...(
|
|
280
|
+
...(isSetValue(uiAuth.signUpUrl) && !isSafeHttpsUrl(uiAuth.signUpUrl)
|
|
173
281
|
? ["uiAuth.signUpUrl"]
|
|
174
282
|
: []),
|
|
175
283
|
];
|
package/dist/connectors/api.d.ts
CHANGED
|
@@ -68,5 +68,5 @@ export interface ApiOptions {
|
|
|
68
68
|
validateArgs?: boolean;
|
|
69
69
|
tools: ApiTool[];
|
|
70
70
|
}
|
|
71
|
-
/** A static connector;
|
|
71
|
+
/** A static connector; every tool passes {@link checkToolContract} first. */
|
|
72
72
|
export declare function api(id: string, opts: ApiOptions): Connector;
|
package/dist/connectors/api.js
CHANGED
|
@@ -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
|
-
/**
|
|
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;
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
//
|
|
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
|
-
//
|
|
109
|
-
//
|
|
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
|
|
871
|
-
//
|
|
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
|
-
/**
|
|
9
|
-
* the
|
|
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
|
-
//
|
|
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
|