@zackbart/connecta 0.4.1 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +527 -0
- package/README.md +83 -7
- package/SECURITY.md +10 -6
- package/dist/activity.d.ts +8 -0
- package/dist/activity.d.ts.map +1 -1
- package/dist/activity.js +1 -0
- package/dist/activity.js.map +1 -1
- package/dist/auth/bearer.d.ts +10 -3
- package/dist/auth/bearer.d.ts.map +1 -1
- package/dist/auth/bearer.js +21 -0
- package/dist/auth/bearer.js.map +1 -1
- package/dist/auth/clerk.d.ts +26 -1
- package/dist/auth/clerk.d.ts.map +1 -1
- package/dist/auth/clerk.js +161 -4
- package/dist/auth/clerk.js.map +1 -1
- package/dist/connectors/api.d.ts +13 -0
- package/dist/connectors/api.d.ts.map +1 -1
- package/dist/connectors/api.js +2 -0
- package/dist/connectors/api.js.map +1 -1
- package/dist/connectors/remote-mcp.d.ts +13 -0
- package/dist/connectors/remote-mcp.d.ts.map +1 -1
- package/dist/connectors/remote-mcp.js +10 -0
- package/dist/connectors/remote-mcp.js.map +1 -1
- package/dist/credential-health.d.ts +212 -0
- package/dist/credential-health.d.ts.map +1 -0
- package/dist/credential-health.js +535 -0
- package/dist/credential-health.js.map +1 -0
- package/dist/execute.d.ts +4 -4
- package/dist/execute.d.ts.map +1 -1
- package/dist/execute.js +16 -4
- package/dist/execute.js.map +1 -1
- package/dist/index.d.ts +77 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +112 -2
- package/dist/index.js.map +1 -1
- package/dist/meta-tools.d.ts +76 -7
- package/dist/meta-tools.d.ts.map +1 -1
- package/dist/meta-tools.js +328 -98
- package/dist/meta-tools.js.map +1 -1
- package/dist/registry.d.ts +245 -2
- package/dist/registry.d.ts.map +1 -1
- package/dist/registry.js +377 -27
- package/dist/registry.js.map +1 -1
- package/dist/server.d.ts +7 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +342 -27
- package/dist/server.js.map +1 -1
- package/dist/skills.d.ts +53 -2
- package/dist/skills.d.ts.map +1 -1
- package/dist/skills.js +162 -2
- package/dist/skills.js.map +1 -1
- package/dist/timeout.d.ts +16 -0
- package/dist/timeout.d.ts.map +1 -0
- package/dist/timeout.js +38 -0
- package/dist/timeout.js.map +1 -0
- package/dist/toolkits.d.ts +138 -0
- package/dist/toolkits.d.ts.map +1 -0
- package/dist/toolkits.js +319 -0
- package/dist/toolkits.js.map +1 -0
- package/dist/types.d.ts +90 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/ui.d.ts +63 -0
- package/dist/ui.d.ts.map +1 -1
- package/dist/ui.js +176 -11
- package/dist/ui.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +5 -2
- package/src/activity.ts +9 -0
- package/src/auth/bearer.ts +35 -1
- package/src/auth/clerk.ts +202 -5
- package/src/connectors/api.ts +15 -0
- package/src/connectors/remote-mcp.ts +24 -0
- package/src/credential-health.ts +736 -0
- package/src/execute.ts +32 -8
- package/src/index.ts +226 -2
- package/src/meta-tools.ts +397 -119
- package/src/registry.ts +540 -29
- package/src/server.ts +431 -25
- package/src/skills.ts +185 -2
- package/src/timeout.ts +49 -0
- package/src/toolkits.ts +450 -0
- package/src/types.ts +96 -2
- package/src/ui.ts +190 -11
- package/src/version.ts +1 -1
package/src/ui.ts
CHANGED
|
@@ -24,32 +24,74 @@ interface ResolvedBranding {
|
|
|
24
24
|
themeColor: string;
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
+
export const DEFAULT_FAVICON_HREF = "/favicon.svg";
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Branding arrives from operator config, which is untyped at a JS call site, so
|
|
31
|
+
* every field is treated as `unknown`: a non-string is read as unset rather than
|
|
32
|
+
* throwing on `.trim()`. Rendering must degrade to defaults for a malformed
|
|
33
|
+
* value, never fail — `createConnecta` calls this during construction.
|
|
34
|
+
*/
|
|
35
|
+
function trimmedString(value: unknown): string | undefined {
|
|
36
|
+
return typeof value === "string" ? value.trim() || undefined : undefined;
|
|
37
|
+
}
|
|
38
|
+
|
|
27
39
|
export function resolveBranding(
|
|
28
40
|
branding?: ConnectaBranding,
|
|
29
41
|
): ResolvedBranding {
|
|
30
|
-
const productName = branding?.productName
|
|
31
|
-
const ownerName = branding?.ownerName
|
|
42
|
+
const productName = trimmedString(branding?.productName) ?? "Connecta";
|
|
43
|
+
const ownerName = trimmedString(branding?.ownerName);
|
|
32
44
|
// Operator branding URLs become masthead/callback hrefs, so a non-http(s)
|
|
33
45
|
// scheme (javascript:, data:) is dropped the same as an unset URL — the
|
|
34
46
|
// callers already render a <span> instead of an <a> when it is absent.
|
|
35
|
-
const productUrl = branding?.productUrl
|
|
36
|
-
const ownerUrl = branding?.ownerUrl
|
|
47
|
+
const productUrl = trimmedString(branding?.productUrl);
|
|
48
|
+
const ownerUrl = trimmedString(branding?.ownerUrl);
|
|
49
|
+
const faviconHref = trimmedString(branding?.favicon?.href);
|
|
37
50
|
return {
|
|
38
51
|
productName,
|
|
39
52
|
...(productUrl && isSafeHttpUrl(productUrl) ? { productUrl } : {}),
|
|
40
53
|
...(ownerName ? { ownerName } : {}),
|
|
41
54
|
...(ownerUrl && isSafeHttpUrl(ownerUrl) ? { ownerUrl } : {}),
|
|
42
55
|
description:
|
|
43
|
-
branding?.description
|
|
56
|
+
trimmedString(branding?.description) ??
|
|
44
57
|
`Manage the services this ${productName} instance makes available to agents.`,
|
|
45
58
|
pageTitle:
|
|
46
|
-
branding?.pageTitle
|
|
59
|
+
trimmedString(branding?.pageTitle) ??
|
|
47
60
|
(ownerName ? `${productName} — ${ownerName}` : productName),
|
|
48
|
-
faviconHref:
|
|
49
|
-
|
|
61
|
+
faviconHref:
|
|
62
|
+
faviconHref && isSafeIconHref(faviconHref)
|
|
63
|
+
? faviconHref
|
|
64
|
+
: DEFAULT_FAVICON_HREF,
|
|
65
|
+
themeColor: trimmedString(branding?.themeColor) ?? "#ffffff",
|
|
50
66
|
};
|
|
51
67
|
}
|
|
52
68
|
|
|
69
|
+
/**
|
|
70
|
+
* Names of the branding URLs the operator set that failed their gate and were
|
|
71
|
+
* replaced by a default. Lives beside the gates so the startup warning cannot
|
|
72
|
+
* drift from them, and takes `unknown` fields for the same reason
|
|
73
|
+
* `resolveBranding` does — a warning helper must never throw.
|
|
74
|
+
*/
|
|
75
|
+
export function droppedBrandingUrls(branding?: ConnectaBranding): string[] {
|
|
76
|
+
if (!branding) return [];
|
|
77
|
+
const resolved = resolveBranding(branding);
|
|
78
|
+
// A non-string still counts as "set": the operator meant to supply a URL, and
|
|
79
|
+
// that intent is exactly what the warning reports on. A blank string does not.
|
|
80
|
+
const isSet = (value: unknown) =>
|
|
81
|
+
typeof value === "string"
|
|
82
|
+
? trimmedString(value) !== undefined
|
|
83
|
+
: value !== undefined && value !== null;
|
|
84
|
+
const faviconHref = branding.favicon?.href;
|
|
85
|
+
return [
|
|
86
|
+
...(isSet(branding.productUrl) && !resolved.productUrl ? ["productUrl"] : []),
|
|
87
|
+
...(isSet(branding.ownerUrl) && !resolved.ownerUrl ? ["ownerUrl"] : []),
|
|
88
|
+
...(isSet(faviconHref) &&
|
|
89
|
+
trimmedString(faviconHref) !== resolved.faviconHref
|
|
90
|
+
? ["favicon.href"]
|
|
91
|
+
: []),
|
|
92
|
+
];
|
|
93
|
+
}
|
|
94
|
+
|
|
53
95
|
/**
|
|
54
96
|
* A JS string literal safe to inline in a <script> block. JSON.stringify alone
|
|
55
97
|
* leaves `/` untouched, so a value containing "</script>" would close the
|
|
@@ -74,6 +116,86 @@ export function isSafeHttpUrl(url: unknown): boolean {
|
|
|
74
116
|
}
|
|
75
117
|
}
|
|
76
118
|
|
|
119
|
+
/**
|
|
120
|
+
* Only the second check's base; any origin works because the check is whether
|
|
121
|
+
* the href stays on whatever origin it is resolved against. It is deliberately
|
|
122
|
+
* never the sole gate: a value whose own authority equals this host (say
|
|
123
|
+
* `//connecta.invalid/x`) would resolve to this exact origin and pass, so the
|
|
124
|
+
* structural check below runs first and is what actually rejects `//host`.
|
|
125
|
+
*/
|
|
126
|
+
const SAME_ORIGIN_PROBE = "https://connecta.invalid";
|
|
127
|
+
|
|
128
|
+
/** Removed anywhere in a URL by the parser, so a gate must ignore them too. */
|
|
129
|
+
const URL_STRIPPED_CHARS = /[\t\n\r]/g;
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* True for values allowed in the page's `<link rel="icon" href>`: an absolute
|
|
133
|
+
* `http(s)` URL (an icon the operator hosts elsewhere) or a path rooted at this
|
|
134
|
+
* origin. The relative carve-out is deliberate rather than accidental — the
|
|
135
|
+
* default href is the relative `/favicon.svg`, which `isSafeHttpUrl` alone would
|
|
136
|
+
* reject — and it is kept narrow on both ends.
|
|
137
|
+
*
|
|
138
|
+
* Root-relative only, because `/ui` and `/oauth/callback/<id>` sit at different
|
|
139
|
+
* depths and a document-relative path would resolve differently on each.
|
|
140
|
+
*
|
|
141
|
+
* "Root-relative" is enforced structurally: exactly one leading `/` followed by
|
|
142
|
+
* a character that is neither `/` nor `\`. Both of those would make the value an
|
|
143
|
+
* authority (`//host`, and `/\host` because the URL parser folds `\` to `/` in
|
|
144
|
+
* special schemes), pointing at an origin this server does not control. The test
|
|
145
|
+
* runs on a copy with tab/newline/CR removed, since the parser strips those
|
|
146
|
+
* anywhere and `/\t/host` would otherwise slip through as single-slash. The
|
|
147
|
+
* origin comparison that follows is defense in depth, not the authority check —
|
|
148
|
+
* on its own it would accept an authority that happened to equal the probe host.
|
|
149
|
+
*/
|
|
150
|
+
export function isSafeIconHref(href: unknown): boolean {
|
|
151
|
+
if (typeof href !== "string") return false;
|
|
152
|
+
if (isSafeHttpUrl(href)) return true;
|
|
153
|
+
if (!/^\/(?![/\\])/.test(href.replace(URL_STRIPPED_CHARS, ""))) return false;
|
|
154
|
+
try {
|
|
155
|
+
return new URL(href, SAME_ORIGIN_PROBE).origin === SAME_ORIGIN_PROBE;
|
|
156
|
+
} catch {
|
|
157
|
+
return false;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* True only for an absolute `https:` URL — the gate for `uiAuth.frontendApiUrl`,
|
|
163
|
+
* the last operator-config value that lands in a URL-valued HTML position (the
|
|
164
|
+
* `<script src>` of `/ui`'s sign-in loader). `javascript:` in a `src` does not
|
|
165
|
+
* execute, so this closes a hole in the *invariant* rather than a live vector:
|
|
166
|
+
* every operator value reaching an `href`/`src` is validated, with no exception
|
|
167
|
+
* left to remember.
|
|
168
|
+
*
|
|
169
|
+
* Stricter than `isSafeHttpUrl` on purpose. There is no `http:` carve-out and no
|
|
170
|
+
* loopback carve-out, because nobody types this value: the shipped Clerk adapter
|
|
171
|
+
* derives it from the publishable key and Clerk's Frontend API is always https.
|
|
172
|
+
* A cleartext script source on the dashboard would be a downgrade even where a
|
|
173
|
+
* browser's mixed-content rules had not already blocked it.
|
|
174
|
+
*/
|
|
175
|
+
export function isSafeScriptSrcUrl(url: unknown): boolean {
|
|
176
|
+
if (typeof url !== "string") return false;
|
|
177
|
+
try {
|
|
178
|
+
return new URL(url).protocol === "https:";
|
|
179
|
+
} catch {
|
|
180
|
+
return false;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Names of the `uiAuth` URLs an inbound-auth provider supplied that failed their
|
|
186
|
+
* gate. Lives beside the gate for the same reason `droppedBrandingUrls` does: the
|
|
187
|
+
* startup warning cannot then drift from what rendering actually drops. Every
|
|
188
|
+
* field is read defensively rather than trusted, because a custom `InboundAuth`
|
|
189
|
+
* is untyped at a JS call site — `isSafeScriptSrcUrl` takes `unknown`, and a
|
|
190
|
+
* `uiAuth` that is not the clerk shape is reported as nothing to warn about.
|
|
191
|
+
*/
|
|
192
|
+
export function droppedUiAuthUrls(uiAuth?: UiAuthConfig): string[] {
|
|
193
|
+
if (!uiAuth || uiAuth.kind !== "clerk") return [];
|
|
194
|
+
return isSafeScriptSrcUrl(uiAuth.frontendApiUrl)
|
|
195
|
+
? []
|
|
196
|
+
: ["uiAuth.frontendApiUrl"];
|
|
197
|
+
}
|
|
198
|
+
|
|
77
199
|
export interface UiTool {
|
|
78
200
|
name: string;
|
|
79
201
|
address: string;
|
|
@@ -89,6 +211,17 @@ export interface UiConnector {
|
|
|
89
211
|
authorizationUrl?: string;
|
|
90
212
|
toolCount: number;
|
|
91
213
|
tools: UiTool[];
|
|
214
|
+
/**
|
|
215
|
+
* Verdict of the last proactive credential liveness check (issue #24), for the
|
|
216
|
+
* connectors that hold a credential connecta stores. Shown beside the live
|
|
217
|
+
* status so an operator can tell "checked just now" from "last verified an
|
|
218
|
+
* hour ago", and see a dead credential the page's own probe may not reach.
|
|
219
|
+
*/
|
|
220
|
+
credentialCheck?: {
|
|
221
|
+
state: "ok" | "auth_required" | "error";
|
|
222
|
+
checkedAt: string;
|
|
223
|
+
message?: string;
|
|
224
|
+
};
|
|
92
225
|
credential?: {
|
|
93
226
|
label: string;
|
|
94
227
|
description?: string;
|
|
@@ -173,6 +306,7 @@ export async function buildUiData(
|
|
|
173
306
|
const connectors = await Promise.all(
|
|
174
307
|
registry.listConnectors().map(async (c): Promise<UiConnector> => {
|
|
175
308
|
const status = await registry.statusFor(c.id, baseUrl, requestScope);
|
|
309
|
+
const credentialCheck = await registry.credentialHealthFor(c.id);
|
|
176
310
|
let tools: UiTool[] = [];
|
|
177
311
|
// `status()` on an unauthenticated remote connector starts OAuth and
|
|
178
312
|
// stores its state + PKCE verifier. Probing listTools immediately
|
|
@@ -271,6 +405,17 @@ export async function buildUiData(
|
|
|
271
405
|
: {}),
|
|
272
406
|
toolCount: tools.length,
|
|
273
407
|
tools,
|
|
408
|
+
...(credentialCheck
|
|
409
|
+
? {
|
|
410
|
+
credentialCheck: {
|
|
411
|
+
state: credentialCheck.state,
|
|
412
|
+
checkedAt: credentialCheck.checkedAt,
|
|
413
|
+
...(credentialCheck.message
|
|
414
|
+
? { message: credentialCheck.message }
|
|
415
|
+
: {}),
|
|
416
|
+
},
|
|
417
|
+
}
|
|
418
|
+
: {}),
|
|
274
419
|
...(credential ? { credential } : {}),
|
|
275
420
|
};
|
|
276
421
|
}),
|
|
@@ -305,7 +450,28 @@ export function renderUiHtml(
|
|
|
305
450
|
branding?: ConnectaBranding,
|
|
306
451
|
nonce?: string,
|
|
307
452
|
): string {
|
|
308
|
-
const
|
|
453
|
+
const clerk = uiAuth?.kind === "clerk" ? uiAuth : undefined;
|
|
454
|
+
// The Clerk loader's origin. A value that fails the gate is dropped rather
|
|
455
|
+
// than escaped into the page: the loader tag is simply not emitted, the gate
|
|
456
|
+
// reports that Clerk could not load, and the rest of the shell still renders —
|
|
457
|
+
// the same fallback-and-warn posture the branding URLs take, with the drop
|
|
458
|
+
// named in a startup warning (see `droppedUiAuthUrls`).
|
|
459
|
+
const clerkScriptOrigin =
|
|
460
|
+
clerk && isSafeScriptSrcUrl(clerk.frontendApiUrl)
|
|
461
|
+
? clerk.frontendApiUrl
|
|
462
|
+
: undefined;
|
|
463
|
+
// Enumerated field by field, because this object is serialized into the page's
|
|
464
|
+
// inline script: a rejected frontendApiUrl must not reach the document through
|
|
465
|
+
// `AUTH` after being kept out of the `<script src>`.
|
|
466
|
+
const auth = clerk
|
|
467
|
+
? {
|
|
468
|
+
kind: clerk.kind,
|
|
469
|
+
publishableKey: clerk.publishableKey,
|
|
470
|
+
...(clerkScriptOrigin ? { frontendApiUrl: clerkScriptOrigin } : {}),
|
|
471
|
+
...(clerk.signInUrl ? { signInUrl: clerk.signInUrl } : {}),
|
|
472
|
+
...(clerk.signUpUrl ? { signUpUrl: clerk.signUpUrl } : {}),
|
|
473
|
+
}
|
|
474
|
+
: (uiAuth ?? { kind: "bearer" as const });
|
|
309
475
|
const brand = resolveBranding(branding);
|
|
310
476
|
const title = brand.pageTitle;
|
|
311
477
|
// When the /ui response ships a nonce-based CSP, every <script> it emits must
|
|
@@ -327,8 +493,8 @@ export function renderUiHtml(
|
|
|
327
493
|
: `<span class="product">${escapeHtmlAttr(brand.productName)}</span>`
|
|
328
494
|
: "";
|
|
329
495
|
const clerkScript =
|
|
330
|
-
|
|
331
|
-
? `<script${nonceAttr} defer crossorigin="anonymous" data-clerk-publishable-key="${escapeHtmlAttr(
|
|
496
|
+
clerk && clerkScriptOrigin
|
|
497
|
+
? `<script${nonceAttr} defer crossorigin="anonymous" data-clerk-publishable-key="${escapeHtmlAttr(clerk.publishableKey)}" src="${escapeHtmlAttr(clerkScriptOrigin)}/npm/@clerk/clerk-js@6/dist/clerk.browser.js"></script>`
|
|
332
498
|
: "";
|
|
333
499
|
|
|
334
500
|
return `<!doctype html>
|
|
@@ -907,6 +1073,19 @@ function render() {
|
|
|
907
1073
|
if (c.message) {
|
|
908
1074
|
head += '<p class="connector-message msg">' + esc(c.message) + "</p>";
|
|
909
1075
|
}
|
|
1076
|
+
if (c.credentialCheck) {
|
|
1077
|
+
const check = c.credentialCheck;
|
|
1078
|
+
const verdict = check.state === "ok"
|
|
1079
|
+
? "credential verified"
|
|
1080
|
+
: check.state === "auth_required"
|
|
1081
|
+
? "credential needs authorization"
|
|
1082
|
+
: "credential check failed";
|
|
1083
|
+
head += '<p class="connector-check meta">Credential check: ' +
|
|
1084
|
+
esc(verdict) + " · " + esc(formatDate(check.checkedAt)) +
|
|
1085
|
+
(check.message && check.message !== c.message
|
|
1086
|
+
? " — " + esc(check.message)
|
|
1087
|
+
: "") + "</p>";
|
|
1088
|
+
}
|
|
910
1089
|
if (c.authorizationUrl) {
|
|
911
1090
|
const safe = safeHttp(c.authorizationUrl);
|
|
912
1091
|
head += safe
|
package/src/version.ts
CHANGED