@zackbart/connecta 0.21.2 → 0.22.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 +53 -0
- package/README.md +7 -0
- package/dist/access-tokens.d.ts +2 -2
- package/dist/access-tokens.js +14 -2
- package/dist/connectors/api.d.ts +2 -0
- package/dist/connectors/api.js +1 -0
- package/dist/connectors/remote-mcp.d.ts +2 -0
- package/dist/connectors/remote-mcp.js +6 -0
- package/dist/credentials.d.ts +6 -6
- package/dist/credentials.js +25 -21
- package/dist/identity.d.ts +4 -0
- package/dist/identity.js +17 -0
- package/dist/index.d.ts +16 -2
- package/dist/index.js +6 -1
- package/dist/meta-tools.js +7 -2
- package/dist/operator-ui/generated.js +1 -1
- package/dist/operator-ui/model.d.ts +4 -2
- package/dist/operator-ui/view.js +1 -1
- package/dist/providers/cloudflare.d.ts +2 -0
- package/dist/providers/cloudflare.js +1 -0
- package/dist/providers/linear.d.ts +2 -0
- package/dist/providers/linear.js +1 -0
- package/dist/providers/mixpanel.d.ts +2 -0
- package/dist/providers/mixpanel.js +1 -0
- package/dist/providers/notion.d.ts +2 -0
- package/dist/providers/notion.js +1 -0
- package/dist/providers/revenuecat.d.ts +2 -0
- package/dist/providers/revenuecat.js +1 -0
- package/dist/providers/stripe.d.ts +2 -0
- package/dist/providers/stripe.js +1 -0
- package/dist/registry.d.ts +25 -0
- package/dist/registry.js +200 -4
- package/dist/routes/access-tokens.js +2 -2
- package/dist/routes/activity.js +4 -1
- package/dist/routes/credentials.js +31 -12
- package/dist/routes/mcp.js +17 -2
- package/dist/routes/oauth.js +55 -11
- package/dist/routes/shared.d.ts +20 -4
- package/dist/routes/shared.js +92 -24
- package/dist/routes/ui.js +32 -13
- package/dist/types.d.ts +28 -2
- package/dist/ui.d.ts +3 -3
- package/dist/ui.js +18 -5
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/documentation/architecture.md +14 -8
- package/documentation/auth.md +89 -9
- package/documentation/code-mode.md +2 -2
- package/documentation/connectors.md +13 -0
- package/documentation/meta-tools.md +4 -3
- package/documentation/operations.md +3 -1
- package/documentation/operator-ui.md +13 -4
- package/documentation/request-admission.md +2 -1
- package/documentation/storage-and-credentials.md +42 -4
- package/documentation/upgrading.md +35 -4
- package/ethos.md +8 -8
- package/examples/worker/AGENTS.md +44 -0
- package/examples/worker/README.md +63 -14
- package/examples/worker/src/index.ts +26 -22
- package/package.json +1 -1
- package/templates/node/README.md +7 -0
- package/templates/node/package.json +1 -1
- package/templates/node/src/index.ts +13 -4
package/dist/routes/shared.d.ts
CHANGED
|
@@ -5,11 +5,13 @@ import type { CredentialVault } from "../credentials.js";
|
|
|
5
5
|
import type { DeferredWork } from "../connector-scope.js";
|
|
6
6
|
import type { AdmissionController } from "../executor-admission.js";
|
|
7
7
|
import type { Registry } from "../registry.js";
|
|
8
|
-
import type { ConnectaBranding, Executor, InboundAuth, InboundAuthRuntimeContext, Logger } from "../types.js";
|
|
8
|
+
import type { AuthenticatedIdentity, ConnectaBranding, Executor, IdentityReference, InboundAuth, InboundAuthRuntimeContext, Logger } from "../types.js";
|
|
9
|
+
import type { ConnectaIdentityConfig } from "../index.js";
|
|
9
10
|
export { msg } from "../errors.js";
|
|
10
11
|
export interface ServerOptions {
|
|
11
12
|
registry: Registry;
|
|
12
13
|
auth: InboundAuth[];
|
|
14
|
+
identity?: ConnectaIdentityConfig | undefined;
|
|
13
15
|
publicUrl?: string | undefined;
|
|
14
16
|
serverInfo: Implementation;
|
|
15
17
|
logger: Logger;
|
|
@@ -64,21 +66,35 @@ export declare function privateJson(body: unknown, init?: ResponseInit): Respons
|
|
|
64
66
|
*/
|
|
65
67
|
export declare function loggableValue(requested: string): string;
|
|
66
68
|
export declare function activityActorNamespace(provider: InboundAuth): string | undefined;
|
|
67
|
-
export declare function authorize(request: Request, baseUrl: string, auth: InboundAuth[], runtimeContext?: RuntimeExecutionContext): Promise<{
|
|
69
|
+
export declare function authorize(request: Request, baseUrl: string, auth: InboundAuth[], runtimeContext?: RuntimeExecutionContext, identityConfig?: ConnectaIdentityConfig, partitionIdentity?: boolean): Promise<{
|
|
68
70
|
ok: true;
|
|
69
71
|
actor: ActivityActor;
|
|
70
|
-
|
|
72
|
+
identity: AuthenticatedIdentity;
|
|
73
|
+
subjectKey?: string;
|
|
74
|
+
principalKey?: string;
|
|
75
|
+
connectorIds: "all" | readonly string[];
|
|
76
|
+
operator: boolean;
|
|
77
|
+
/** Backward-compatible name used by operator views. */
|
|
71
78
|
uiAdminEligible?: boolean;
|
|
72
79
|
} | {
|
|
73
80
|
ok: false;
|
|
74
81
|
response: Response;
|
|
75
82
|
}>;
|
|
76
|
-
export declare function authorizeUiAdmin(request: Request, baseUrl: string, auth: InboundAuth[], purpose?: string, runtimeContext?: RuntimeExecutionContext): Promise<{
|
|
83
|
+
export declare function authorizeUiAdmin(request: Request, baseUrl: string, auth: InboundAuth[], purpose?: string, runtimeContext?: RuntimeExecutionContext, identityConfig?: ConnectaIdentityConfig): Promise<{
|
|
77
84
|
ok: true;
|
|
78
85
|
userId: string;
|
|
86
|
+
principal?: IdentityReference;
|
|
87
|
+
principalKey?: string;
|
|
88
|
+
connectorIds: "all" | readonly string[];
|
|
79
89
|
} | {
|
|
80
90
|
ok: false;
|
|
81
91
|
response: Response;
|
|
82
92
|
}>;
|
|
93
|
+
export declare function authorizeUiIdentity(request: Request, baseUrl: string, auth: InboundAuth[], purpose: string, runtimeContext?: RuntimeExecutionContext, identityConfig?: ConnectaIdentityConfig): Promise<Extract<Awaited<ReturnType<typeof authorize>>, {
|
|
94
|
+
ok: true;
|
|
95
|
+
}> | {
|
|
96
|
+
ok: false;
|
|
97
|
+
response: Response;
|
|
98
|
+
}>;
|
|
83
99
|
export declare function isSameOrigin(request: Request, baseUrl: string): boolean;
|
|
84
100
|
export declare function withSecurityHeaders(response: Response, requestUrl: URL, path: string): Response;
|
package/dist/routes/shared.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { identityStorageKey, validIdentityReference } from "../identity.js";
|
|
1
2
|
import { operatorPageForPath } from "../ui.js";
|
|
2
3
|
export { msg } from "../errors.js";
|
|
3
4
|
export function privateJson(body, init = {}) {
|
|
@@ -29,9 +30,21 @@ export function activityActorNamespace(provider) {
|
|
|
29
30
|
? provider.activityActorNamespace
|
|
30
31
|
: undefined;
|
|
31
32
|
}
|
|
32
|
-
export async function authorize(request, baseUrl, auth, runtimeContext) {
|
|
33
|
+
export async function authorize(request, baseUrl, auth, runtimeContext, identityConfig, partitionIdentity = true) {
|
|
33
34
|
if (auth.length === 0) {
|
|
34
|
-
|
|
35
|
+
const actor = { kind: "anonymous" };
|
|
36
|
+
const identity = { actor, interactive: false };
|
|
37
|
+
let connectorIds = "all";
|
|
38
|
+
try {
|
|
39
|
+
connectorIds = await identityConfig?.connectorAccess?.(identity) ?? "all";
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
return {
|
|
43
|
+
ok: false,
|
|
44
|
+
response: privateJson({ error: "identity access resolution failed" }, { status: 403 }),
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
return { ok: true, actor, identity, connectorIds, operator: false };
|
|
35
48
|
}
|
|
36
49
|
let lastResponse = null;
|
|
37
50
|
for (const provider of auth) {
|
|
@@ -39,18 +52,56 @@ export async function authorize(request, baseUrl, auth, runtimeContext) {
|
|
|
39
52
|
if (result.ok) {
|
|
40
53
|
const subjectId = result.subjectId ?? result.userId;
|
|
41
54
|
const actorNamespace = activityActorNamespace(provider);
|
|
55
|
+
const subject = subjectId && actorNamespace
|
|
56
|
+
? { namespace: actorNamespace, id: subjectId }
|
|
57
|
+
: undefined;
|
|
58
|
+
const derivedPrincipal = result.userId && actorNamespace
|
|
59
|
+
? { namespace: actorNamespace, id: result.userId }
|
|
60
|
+
: undefined;
|
|
61
|
+
const principal = validIdentityReference(result.principal)
|
|
62
|
+
? result.principal
|
|
63
|
+
: derivedPrincipal;
|
|
64
|
+
const interactive = Boolean(result.userId && provider.interactiveOperator);
|
|
65
|
+
const actor = {
|
|
66
|
+
kind: provider.kind,
|
|
67
|
+
...(subjectId ? { id: subjectId } : {}),
|
|
68
|
+
...(subject ? { namespace: subject.namespace } : {}),
|
|
69
|
+
};
|
|
70
|
+
const identity = {
|
|
71
|
+
actor,
|
|
72
|
+
...(subject ? { subject } : {}),
|
|
73
|
+
...(principal ? { principal } : {}),
|
|
74
|
+
interactive,
|
|
75
|
+
};
|
|
76
|
+
let operator = interactive;
|
|
77
|
+
let connectorIds = "all";
|
|
78
|
+
try {
|
|
79
|
+
if (identityConfig?.operatorAccess) {
|
|
80
|
+
operator = interactive && principal
|
|
81
|
+
? await identityConfig.operatorAccess(principal)
|
|
82
|
+
: false;
|
|
83
|
+
}
|
|
84
|
+
connectorIds = await identityConfig?.connectorAccess?.(identity) ?? "all";
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
return {
|
|
88
|
+
ok: false,
|
|
89
|
+
response: privateJson({ error: "identity access resolution failed" }, { status: 403 }),
|
|
90
|
+
};
|
|
91
|
+
}
|
|
42
92
|
return {
|
|
43
93
|
ok: true,
|
|
44
|
-
actor
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
...(result.userId && provider.interactiveOperator
|
|
52
|
-
? { uiAdminEligible: true }
|
|
94
|
+
actor,
|
|
95
|
+
identity,
|
|
96
|
+
...(subject && partitionIdentity
|
|
97
|
+
? { subjectKey: await identityStorageKey(subject) }
|
|
98
|
+
: {}),
|
|
99
|
+
...(principal && partitionIdentity
|
|
100
|
+
? { principalKey: await identityStorageKey(principal) }
|
|
53
101
|
: {}),
|
|
102
|
+
connectorIds,
|
|
103
|
+
operator,
|
|
104
|
+
...(operator ? { uiAdminEligible: true } : {}),
|
|
54
105
|
};
|
|
55
106
|
}
|
|
56
107
|
lastResponse = result.response;
|
|
@@ -67,7 +118,7 @@ export async function authorize(request, baseUrl, auth, runtimeContext) {
|
|
|
67
118
|
}),
|
|
68
119
|
};
|
|
69
120
|
}
|
|
70
|
-
export async function authorizeUiAdmin(request, baseUrl, auth, purpose = "credential management", runtimeContext) {
|
|
121
|
+
export async function authorizeUiAdmin(request, baseUrl, auth, purpose = "credential management", runtimeContext, identityConfig) {
|
|
71
122
|
// Operator mutation is intentionally narrower than /mcp and /ui/data: only
|
|
72
123
|
// an interactive provider may admit it. A static bearer token is useful
|
|
73
124
|
// for headless tool calls but must not become a deployment-admin key.
|
|
@@ -76,30 +127,47 @@ export async function authorizeUiAdmin(request, baseUrl, auth, purpose = "creden
|
|
|
76
127
|
// Stopping at the first would make admission depend on config order: a failed gate or
|
|
77
128
|
// missing user may simply mean a later provider is the one meant to admit.
|
|
78
129
|
// The last refusal is returned if none do.
|
|
130
|
+
const authz = await authorizeUiIdentity(request, baseUrl, auth, purpose, runtimeContext, identityConfig);
|
|
131
|
+
if (!authz.ok)
|
|
132
|
+
return authz;
|
|
133
|
+
if (!authz.operator || !authz.actor.id) {
|
|
134
|
+
return {
|
|
135
|
+
ok: false,
|
|
136
|
+
response: privateJson({ error: `${purpose} requires operator access` }, { status: 403 }),
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
return {
|
|
140
|
+
ok: true,
|
|
141
|
+
userId: authz.identity.principal?.id ?? authz.actor.id,
|
|
142
|
+
...(authz.identity.principal
|
|
143
|
+
? { principal: authz.identity.principal }
|
|
144
|
+
: {}),
|
|
145
|
+
...(authz.principalKey ? { principalKey: authz.principalKey } : {}),
|
|
146
|
+
connectorIds: authz.connectorIds,
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
export async function authorizeUiIdentity(request, baseUrl, auth, purpose, runtimeContext, identityConfig) {
|
|
79
150
|
const providers = auth.filter((candidate) => candidate.interactiveOperator);
|
|
80
151
|
if (providers.length === 0) {
|
|
81
152
|
return {
|
|
82
153
|
ok: false,
|
|
83
|
-
response: privateJson({ error: `${purpose} requires interactive
|
|
154
|
+
response: privateJson({ error: `${purpose} requires interactive user authentication` }, { status: 403 }),
|
|
84
155
|
};
|
|
85
156
|
}
|
|
86
|
-
let lastResponse
|
|
157
|
+
let lastResponse;
|
|
87
158
|
for (const provider of providers) {
|
|
88
|
-
const
|
|
89
|
-
if (!
|
|
90
|
-
lastResponse =
|
|
159
|
+
const authz = await authorize(request, baseUrl, [provider], runtimeContext, identityConfig);
|
|
160
|
+
if (!authz.ok) {
|
|
161
|
+
lastResponse = authz.response;
|
|
91
162
|
continue;
|
|
92
163
|
}
|
|
93
|
-
if (
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
}
|
|
97
|
-
return { ok: true, userId: result.userId };
|
|
164
|
+
if (authz.identity.interactive)
|
|
165
|
+
return authz;
|
|
166
|
+
lastResponse = privateJson({ error: "authenticated user required" }, { status: 403 });
|
|
98
167
|
}
|
|
99
168
|
return {
|
|
100
169
|
ok: false,
|
|
101
|
-
response: lastResponse ??
|
|
102
|
-
privateJson({ error: "forbidden" }, { status: 403 }),
|
|
170
|
+
response: lastResponse ?? privateJson({ error: `${purpose} requires interactive user authentication` }, { status: 403 }),
|
|
103
171
|
};
|
|
104
172
|
}
|
|
105
173
|
export function isSameOrigin(request, baseUrl) {
|
package/dist/routes/ui.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { CONNECTA_FAVICON_ICO } from "../favicon.js";
|
|
2
2
|
import { buildUiData, CONNECTA_FAVICON_SVG, credentialManagementCapability, operatorPageForPath, renderUiHtml, } from "../ui.js";
|
|
3
|
-
import { authorize, privateJson, } from "./shared.js";
|
|
3
|
+
import { authorize, msg, privateJson, } from "./shared.js";
|
|
4
4
|
/**
|
|
5
5
|
* Headers that make an operator-supplied favicon body inert on this origin.
|
|
6
6
|
* The SVG route is the sharp one: `image/svg+xml` is an *active* content type,
|
|
@@ -99,17 +99,35 @@ export async function routeUi(context) {
|
|
|
99
99
|
}
|
|
100
100
|
if (path !== "/ui/data")
|
|
101
101
|
return null;
|
|
102
|
-
const authz = await authorize(request, baseUrl, opts.auth, runtimeContext);
|
|
102
|
+
const authz = await authorize(request, baseUrl, opts.auth, runtimeContext, opts.identity);
|
|
103
103
|
if (!authz.ok)
|
|
104
104
|
return authz.response;
|
|
105
|
+
let registry;
|
|
106
|
+
try {
|
|
107
|
+
registry = opts.registry.scoped({
|
|
108
|
+
connectorIds: authz.connectorIds,
|
|
109
|
+
...(authz.subjectKey ? { subjectKey: authz.subjectKey } : {}),
|
|
110
|
+
...(authz.principalKey ? { principalKey: authz.principalKey } : {}),
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
catch (error) {
|
|
114
|
+
return privateJson({ error: msg(error) }, { status: 403 });
|
|
115
|
+
}
|
|
105
116
|
const eligibleOperator = authz.uiAdminEligible === true;
|
|
106
|
-
const
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
117
|
+
const interactiveManager = authz.identity.interactive;
|
|
118
|
+
const personalManager = Boolean(interactiveManager && authz.principalKey);
|
|
119
|
+
const visibleConnectors = registry.listConnectors();
|
|
120
|
+
const hasManageableCredentialSlot = visibleConnectors.some((connector) => Boolean(connector.credential) &&
|
|
121
|
+
(connector.authScope !== "personal" || personalManager));
|
|
122
|
+
const credentialManagement = interactiveManager && hasManageableCredentialSlot
|
|
123
|
+
? opts.credentialVault
|
|
124
|
+
? "available"
|
|
125
|
+
: "vault_not_configured"
|
|
126
|
+
: credentialManagementCapability({
|
|
127
|
+
eligibleOperator: interactiveManager,
|
|
128
|
+
hasCredentialSlots: visibleConnectors.some((connector) => Boolean(connector.credential)),
|
|
129
|
+
hasCredentialVault: Boolean(opts.credentialVault),
|
|
130
|
+
});
|
|
113
131
|
// As with connector credentials, a Bearer-authenticated observer learns
|
|
114
132
|
// only that an interactive operator is required, not whether this deployment has opted into
|
|
115
133
|
// token issuance. Configuration topology is operator data.
|
|
@@ -118,9 +136,10 @@ export async function routeUi(context) {
|
|
|
118
136
|
: opts.accessTokens
|
|
119
137
|
? "available"
|
|
120
138
|
: "not_configured";
|
|
121
|
-
const data = await buildUiData(
|
|
122
|
-
//
|
|
123
|
-
//
|
|
124
|
-
|
|
139
|
+
const data = await buildUiData(registry, baseUrl, opts.serverInfo,
|
|
140
|
+
// A static headless bearer may read connector health, but only an
|
|
141
|
+
// interactive human receives credential metadata for visible connectors.
|
|
142
|
+
interactiveManager ? opts.credentialVault : undefined, Boolean(opts.activity?.list) &&
|
|
143
|
+
(!opts.identity?.operatorAccess || eligibleOperator), credentialManagement, defer, interactiveManager, opts.discoveryConcurrency, accessTokenManagement, personalManager ? authz.principalKey : undefined);
|
|
125
144
|
return privateJson(data);
|
|
126
145
|
}
|
package/dist/types.d.ts
CHANGED
|
@@ -138,7 +138,7 @@ export interface ConnectorContext {
|
|
|
138
138
|
/** Public base URL of this deployment (origin), used for OAuth callbacks. */
|
|
139
139
|
baseUrl: string;
|
|
140
140
|
/**
|
|
141
|
-
* Read-only access to this connector's
|
|
141
|
+
* Read-only access to this connector's human-managed credential. Present
|
|
142
142
|
* only when the connector declares `credential` and the deployment configures
|
|
143
143
|
* `credentials.encryptionKey`.
|
|
144
144
|
*/
|
|
@@ -208,6 +208,12 @@ export interface ConnectorStatus {
|
|
|
208
208
|
/** The whole plugin contract — the one open seam. */
|
|
209
209
|
export interface Connector {
|
|
210
210
|
id: string;
|
|
211
|
+
/**
|
|
212
|
+
* Who owns this connector's downstream authentication. `shared` keeps one
|
|
213
|
+
* deployment-wide grant. `personal` isolates storage and credentials by the
|
|
214
|
+
* authenticated human principal. Defaults to `shared`.
|
|
215
|
+
*/
|
|
216
|
+
authScope?: "shared" | "personal";
|
|
211
217
|
/** Human-readable display name; the stable `id` remains the tool-address prefix. */
|
|
212
218
|
title?: string;
|
|
213
219
|
/** How call_tool wraps results. "mcp" passes the content array through; anything else is JSON-wrapped. */
|
|
@@ -240,7 +246,7 @@ export interface Connector {
|
|
|
240
246
|
* configuration; no runtime registration or shared mutable copy exists.
|
|
241
247
|
*/
|
|
242
248
|
usageGuide?: string | ConnectorUsageGuide;
|
|
243
|
-
/** Optional
|
|
249
|
+
/** Optional human-managed credential slot rendered on /credentials. */
|
|
244
250
|
credential?: ConnectorCredentialConfig;
|
|
245
251
|
/** Optional server-side check used by /credentials' Test action. */
|
|
246
252
|
testCredential?(value: string, ctx: ConnectorContext): Promise<CredentialTestResult>;
|
|
@@ -420,10 +426,30 @@ export type AuthResult = {
|
|
|
420
426
|
ok: true;
|
|
421
427
|
userId?: string;
|
|
422
428
|
subjectId?: string;
|
|
429
|
+
/** Human owner represented by a non-interactive access credential. */
|
|
430
|
+
principal?: IdentityReference;
|
|
423
431
|
} | {
|
|
424
432
|
ok: false;
|
|
425
433
|
response: Response;
|
|
426
434
|
};
|
|
435
|
+
/** Stable identity inside one configured authentication directory. */
|
|
436
|
+
export interface IdentityReference {
|
|
437
|
+
namespace: string;
|
|
438
|
+
id: string;
|
|
439
|
+
}
|
|
440
|
+
/** Identity data passed to config-owned access resolvers. */
|
|
441
|
+
export interface AuthenticatedIdentity {
|
|
442
|
+
actor: {
|
|
443
|
+
kind: string;
|
|
444
|
+
id?: string;
|
|
445
|
+
namespace?: string;
|
|
446
|
+
};
|
|
447
|
+
/** Any stable admitted caller, including service identities and tokens. */
|
|
448
|
+
subject?: IdentityReference;
|
|
449
|
+
/** Human owner of personal connector authentication. */
|
|
450
|
+
principal?: IdentityReference;
|
|
451
|
+
interactive: boolean;
|
|
452
|
+
}
|
|
427
453
|
/** Public browser-auth configuration exposed to connecta's status UI. */
|
|
428
454
|
export type UiAuthConfig = {
|
|
429
455
|
kind: "cloudflare-access";
|
package/dist/ui.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { CredentialVault } from "./credentials.js";
|
|
2
2
|
import { type DeferredWork } from "./connector-scope.js";
|
|
3
3
|
import { type CredentialManagementCapability, type AccessTokenManagementCapability, type UiData } from "./operator-ui/model.js";
|
|
4
|
-
import type {
|
|
4
|
+
import type { RegistryView } from "./registry.js";
|
|
5
5
|
import type { ConnectaBranding, UiAuthConfig } from "./types.js";
|
|
6
6
|
export { filterUiConnectors, type AccessTokenManagementCapability, type CredentialManagementCapability, type UiConnector, type UiData, } from "./operator-ui/model.js";
|
|
7
7
|
/** Connecta's default monochrome "C" mark. */
|
|
@@ -82,10 +82,10 @@ export declare function credentialManagementCapability(input: {
|
|
|
82
82
|
* isolated: they surface status "error" with an empty tool list rather than
|
|
83
83
|
* failing the whole payload.
|
|
84
84
|
*/
|
|
85
|
-
export declare function buildUiData(registry:
|
|
85
|
+
export declare function buildUiData(registry: RegistryView, baseUrl: string, serverInfo: {
|
|
86
86
|
name: string;
|
|
87
87
|
version: string;
|
|
88
|
-
}, credentialVault?: CredentialVault, activityEnabled?: boolean, credentialManagement?: CredentialManagementCapability, defer?: DeferredWork, oauthManagement?: boolean, discoveryConcurrency?: number, accessTokenManagement?: AccessTokenManagementCapability): Promise<UiData>;
|
|
88
|
+
}, credentialVault?: CredentialVault, activityEnabled?: boolean, credentialManagement?: CredentialManagementCapability, defer?: DeferredWork, oauthManagement?: boolean, discoveryConcurrency?: number, accessTokenManagement?: AccessTokenManagementCapability, personalCredentialOwner?: string): Promise<UiData>;
|
|
89
89
|
/**
|
|
90
90
|
* Every operator page serves this same data-free shell. Connector, credential,
|
|
91
91
|
* and activity data arrives only through the authenticated `/ui/*` APIs.
|
package/dist/ui.js
CHANGED
|
@@ -223,7 +223,7 @@ export function credentialManagementCapability(input) {
|
|
|
223
223
|
*/
|
|
224
224
|
export async function buildUiData(registry, baseUrl, serverInfo, credentialVault, activityEnabled = false, credentialManagement = credentialVault
|
|
225
225
|
? "available"
|
|
226
|
-
: "requires_operator", defer, oauthManagement = false, discoveryConcurrency, accessTokenManagement = "not_configured") {
|
|
226
|
+
: "requires_operator", defer, oauthManagement = false, discoveryConcurrency, accessTokenManagement = "not_configured", personalCredentialOwner) {
|
|
227
227
|
const requestScope = {};
|
|
228
228
|
const connectorSet = registry.listConnectors();
|
|
229
229
|
const concurrency = resolveDiscoveryConcurrency(discoveryConcurrency);
|
|
@@ -232,6 +232,9 @@ export async function buildUiData(registry, baseUrl, serverInfo, credentialVault
|
|
|
232
232
|
const status = drift
|
|
233
233
|
? { state: "auth_required", message: drift }
|
|
234
234
|
: await registry.statusFor(c.id, baseUrl, requestScope);
|
|
235
|
+
if (status.authorizationUrl) {
|
|
236
|
+
await registry.bindOAuthHandoff(c.id, status.authorizationUrl);
|
|
237
|
+
}
|
|
235
238
|
let tools = [];
|
|
236
239
|
// `status()` on an unauthenticated remote connector starts OAuth and
|
|
237
240
|
// stores its state + PKCE verifier. Probing listTools immediately
|
|
@@ -253,7 +256,10 @@ export async function buildUiData(registry, baseUrl, serverInfo, credentialVault
|
|
|
253
256
|
}
|
|
254
257
|
}
|
|
255
258
|
let credential;
|
|
256
|
-
|
|
259
|
+
const mayManageAuth = c.authScope === "personal"
|
|
260
|
+
? Boolean(personalCredentialOwner)
|
|
261
|
+
: oauthManagement;
|
|
262
|
+
if (c.credential && credentialVault && mayManageAuth) {
|
|
257
263
|
// One rule, shared with the test route: only the hook matching the
|
|
258
264
|
// declared credential shape can run, so the button is offered only
|
|
259
265
|
// where a click can succeed (src/credentials.ts).
|
|
@@ -289,7 +295,7 @@ export async function buildUiData(registry, baseUrl, serverInfo, credentialVault
|
|
|
289
295
|
: {}),
|
|
290
296
|
};
|
|
291
297
|
try {
|
|
292
|
-
const metadata = await credentialVault.metadata(c.id);
|
|
298
|
+
const metadata = await credentialVault.metadata(c.id, c.authScope === "personal" ? personalCredentialOwner : undefined);
|
|
293
299
|
const fields = credentialFields(metadata);
|
|
294
300
|
const shape = storedCredentialShape(c.credential, metadata?.fields ?? null);
|
|
295
301
|
credential = {
|
|
@@ -331,6 +337,7 @@ export async function buildUiData(registry, baseUrl, serverInfo, credentialVault
|
|
|
331
337
|
}
|
|
332
338
|
return {
|
|
333
339
|
id: c.id,
|
|
340
|
+
authScope: c.authScope ?? "shared",
|
|
334
341
|
...(c.title ? { title: c.title } : {}),
|
|
335
342
|
...(c.description !== undefined
|
|
336
343
|
? { description: c.description }
|
|
@@ -350,7 +357,13 @@ export async function buildUiData(registry, baseUrl, serverInfo, credentialVault
|
|
|
350
357
|
...(status.catalogAccess
|
|
351
358
|
? { catalogAccess: status.catalogAccess }
|
|
352
359
|
: {}),
|
|
353
|
-
...(c.disconnectAuth &&
|
|
360
|
+
...(c.disconnectAuth &&
|
|
361
|
+
c.startAuth &&
|
|
362
|
+
(oauthManagement ||
|
|
363
|
+
c.authScope === "personal" ||
|
|
364
|
+
!personalCredentialOwner)
|
|
365
|
+
? { oauth: true }
|
|
366
|
+
: {}),
|
|
354
367
|
...(credential ? { credential } : {}),
|
|
355
368
|
};
|
|
356
369
|
});
|
|
@@ -367,7 +380,7 @@ export async function buildUiData(registry, baseUrl, serverInfo, credentialVault
|
|
|
367
380
|
activityEnabled,
|
|
368
381
|
credentialManagement,
|
|
369
382
|
accessTokenManagement,
|
|
370
|
-
oauthManagement,
|
|
383
|
+
oauthManagement: oauthManagement || Boolean(personalCredentialOwner),
|
|
371
384
|
};
|
|
372
385
|
}
|
|
373
386
|
function escapeHtmlAttr(value) {
|
package/dist/version.d.ts
CHANGED
package/dist/version.js
CHANGED
|
@@ -70,7 +70,7 @@ read top to bottom.
|
|
|
70
70
|
| 2 | `OPTIONS` | Each auth provider's `handleMetadata` gets a chance (CORS preflight for browser MCP clients); otherwise 204 with MCP CORS. |
|
|
71
71
|
| 3 | `/.well-known/*` | Auth providers' `handleMetadata`, open. 404 when none handles it. |
|
|
72
72
|
| 4 | `/health` | Open JSON: status, connector count, `serverInfo`, the configured executor's sanitized name when it has one, catalog-drift counts, admission snapshots, reserved route names, and `deployment` when `deploymentInfo` is set. Payload-free by construction, and it never joins the MCP queue. |
|
|
73
|
-
| 5 | `/oauth/callback/<connectorId>` | Downstream-OAuth completion, open, `verifyState` before `finishAuth`. |
|
|
73
|
+
| 5 | `/oauth/callback/<connectorId>` | Downstream-OAuth completion, open, `verifyState` before `finishAuth`. Personal flows first resolve the short-lived state hash to the principal partition. |
|
|
74
74
|
| 6 | `/favicon.*`, `/ui` → `/`, the operator shells, `/ui/data` | The operator surface ([operator UI](./operator-ui.md)). The shells are open and data-free; `/ui/data` behind them is gated. Built-ins are matched before connector routes, so a connector cannot shadow a page. |
|
|
75
75
|
| 7 | `/ui/activity` | Gated, plus the optional `activity.readGate`. `GET` only; 404 with no `activity.store.list`. |
|
|
76
76
|
| 8 | `/mcp` | **Admission before auth**, then the auth gate, then a fresh MCP server. |
|
|
@@ -93,11 +93,15 @@ any one file and a reordering reads like a harmless refactor.
|
|
|
93
93
|
before interactive providers. First `ok` admits; if all fail, the last provider's challenge
|
|
94
94
|
response is returned. No providers configured means open — development
|
|
95
95
|
only, and it warns at construction.
|
|
96
|
-
3. **
|
|
96
|
+
3. **Derive the registry view.** Auth supplies a namespaced subject and, for a
|
|
97
|
+
human, a principal. `identity.connectorAccess` selects declared connector
|
|
98
|
+
ids. Personal connectors use the principal partition; result paging uses
|
|
99
|
+
the subject partition. No caller parameter selects either.
|
|
100
|
+
4. **Refuse `?toolkit=`.** Caller-selected toolkits were removed ([#178](https://github.com/zackbart/connecta/issues/178))
|
|
97
101
|
but the URLs naming them were handed out, so the parameter is a 404 rather
|
|
98
102
|
than silently serving the full registry. Retiring a scoping boundary into
|
|
99
103
|
fail-open is the one outcome worse than the 404.
|
|
100
|
-
|
|
104
|
+
5. **Serve.** A fresh `McpServer` per request, the seven meta-tools registered
|
|
101
105
|
against the registry, the Apps shell resource registered (and
|
|
102
106
|
`resources/list` deliberately answering with nothing), and the response
|
|
103
107
|
handed back.
|
|
@@ -109,7 +113,7 @@ owns or hands out, and a change usually belongs in exactly one of them:
|
|
|
109
113
|
|
|
110
114
|
| Module | Owns |
|
|
111
115
|
| --- | --- |
|
|
112
|
-
| `src/registry.ts` | The connector set, address resolution, catalog TTL/persistence/completeness,
|
|
116
|
+
| `src/registry.ts` | The connector set, identity-scoped views, personal storage partitions, address resolution, catalog TTL/persistence/completeness, refresh single-flight, connector health, per-connector call limiters, and drift. Construction-time refusals live here. |
|
|
113
117
|
| `src/catalog-service.ts` | Request-local tool listing, search, and describe. It coalesces reads inside one request and opts agent reads into the runtime's deferred catalog channel when one exists. |
|
|
114
118
|
| `src/invocation.ts` | One tool call: argument validation, call admission, per-attempt timeout, retry with the connector's own `Retry-After` honoured exactly or declined, result unwrapping, size capping, and the activity record. |
|
|
115
119
|
| `src/catalog.ts` | Ranking, description summarizing, and the compact schema renderer discovery shows. |
|
|
@@ -184,10 +188,12 @@ src/
|
|
|
184
188
|
|
|
185
189
|
## Sharp edges
|
|
186
190
|
|
|
187
|
-
- **The registry is shared;
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
+
- **The root registry is shared; identity views are partitioned.** Shared
|
|
192
|
+
connector caches are visible to later requests in the isolate. Personal
|
|
193
|
+
connectors use a bounded principal registry, and transient results use the
|
|
194
|
+
authenticated subject. Anything cached per request still dies with it.
|
|
195
|
+
Putting a downstream client or credential on the wrong side of those lines
|
|
196
|
+
is the highest-severity mistake available here.
|
|
191
197
|
- **Route order is behavior.** Moving a built-in below the connector dispatch
|
|
192
198
|
hands a connector the ability to shadow it. Moving a mutation route below the
|
|
193
199
|
wildcard `OPTIONS` opts it into CORS preflight.
|
package/documentation/auth.md
CHANGED
|
@@ -6,6 +6,63 @@ Access identities on Workers, or a mixture. Static bearers are checked first;
|
|
|
6
6
|
the remaining providers keep configuration order. The first successful
|
|
7
7
|
identity owns the activity actor for that request.
|
|
8
8
|
|
|
9
|
+
## Principals, visibility, and operators
|
|
10
|
+
|
|
11
|
+
Connecta distinguishes three identities. The actor is the exact caller written
|
|
12
|
+
to activity. The subject is any stable authenticated caller and owns transient
|
|
13
|
+
results such as `get_result` pages. The principal is the human owner of personal
|
|
14
|
+
connector auth. An interactive Clerk or Access user supplies all three. A
|
|
15
|
+
Cloudflare service identity has an actor and subject but no principal. A
|
|
16
|
+
connecta access token has its own actor and subject and inherits the principal
|
|
17
|
+
that created it, so agents using that token reach the creator's personal
|
|
18
|
+
connections without becoming operators.
|
|
19
|
+
|
|
20
|
+
`identity.connectorAccess` derives the connector ids a caller may discover and
|
|
21
|
+
invoke. The resolver receives authenticated identity data, never request input,
|
|
22
|
+
and returns `"all"` or a list of ids declared in `connectors`. An unknown id or
|
|
23
|
+
a thrown resolver fails the request closed.
|
|
24
|
+
|
|
25
|
+
`identity.connectorAccess` is also the credential-management boundary. A
|
|
26
|
+
signed-in human may save, test, disconnect, and authorize every visible
|
|
27
|
+
connector: personal auth changes only that principal's partition, while shared
|
|
28
|
+
auth changes the deployment-wide grant for everyone who can see the connector.
|
|
29
|
+
Use `authScope: "personal"` when one member must not rotate another member's
|
|
30
|
+
connection.
|
|
31
|
+
|
|
32
|
+
`identity.operatorAccess` separately reserves deployment-wide administration:
|
|
33
|
+
access-token creation and global activity history. Omit the resolver to
|
|
34
|
+
preserve the prior rule that every interactive human is an operator. When it is
|
|
35
|
+
configured, activity history is operator-only because its global event stream
|
|
36
|
+
contains other principals' connector names and actors.
|
|
37
|
+
|
|
38
|
+
```ts
|
|
39
|
+
createConnecta({
|
|
40
|
+
auth: cloudflareAccessAuth(),
|
|
41
|
+
identity: {
|
|
42
|
+
connectorAccess: ({ principal }) =>
|
|
43
|
+
principal?.id === "user_a"
|
|
44
|
+
? ["shared_docs", "personal_linear"]
|
|
45
|
+
: ["shared_docs"],
|
|
46
|
+
operatorAccess: ({ id }) => id === "user_a",
|
|
47
|
+
},
|
|
48
|
+
connectors: [
|
|
49
|
+
remoteMcp("shared_docs", { url: "https://example.com/mcp" }),
|
|
50
|
+
remoteMcp("personal_linear", {
|
|
51
|
+
url: "https://mcp.linear.app/mcp",
|
|
52
|
+
authScope: "personal",
|
|
53
|
+
auth: { type: "oauth" },
|
|
54
|
+
}),
|
|
55
|
+
],
|
|
56
|
+
executor,
|
|
57
|
+
});
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Identity namespaces matter. Built-in Clerk and Access providers supply one.
|
|
61
|
+
A custom interactive provider must set `activityActorNamespace` before its
|
|
62
|
+
users can own personal auth. It may still use the legacy operator behavior
|
|
63
|
+
without one, but connecta will not merge unnamespaced users into personal
|
|
64
|
+
storage.
|
|
65
|
+
|
|
9
66
|
## Cloudflare Access on Workers
|
|
10
67
|
|
|
11
68
|
[`cloudflareAccessAuth()`](https://developers.cloudflare.com/workers/configuration/cloudflare-access/)
|
|
@@ -35,7 +92,8 @@ also means it is deliberately not a Node or `cloudflared` origin adapter, and
|
|
|
35
92
|
it does not survive a Service Binding hop: those shapes need their own explicit
|
|
36
93
|
trust boundary.
|
|
37
94
|
|
|
38
|
-
A human identity gets MCP and
|
|
95
|
+
A human identity gets MCP and personal-connection access. It gets operator
|
|
96
|
+
access unless `identity.operatorAccess` says otherwise. A Cloudflare service-token
|
|
39
97
|
identity gets MCP access and a stable activity subject, but no `userId`, so it
|
|
40
98
|
cannot write credentials, run downstream OAuth mutations, or issue connecta
|
|
41
99
|
tokens. Access policy decides who reaches the Worker; connecta does not mirror
|
|
@@ -48,7 +106,27 @@ the Worker. Enable [**Managed OAuth**](https://developers.cloudflare.com/cloudfl
|
|
|
48
106
|
on that Worker-level application for interactive MCP clients.
|
|
49
107
|
Cloudflare then owns the unauthenticated challenge and `/.well-known/`
|
|
50
108
|
metadata, issues opaque RFC 8707 tokens, and resolves them into the same trusted
|
|
51
|
-
Worker identity.
|
|
109
|
+
Worker identity. Managed OAuth allows no hosted client callback by default, so
|
|
110
|
+
enable Dynamic Client Registration and add all three values to **Allowed
|
|
111
|
+
redirect URIs**:
|
|
112
|
+
|
|
113
|
+
```text
|
|
114
|
+
https://claude.ai/api/mcp/auth_callback
|
|
115
|
+
https://chatgpt.com/connector_platform_oauth_redirect
|
|
116
|
+
https://chatgpt.com/connector/oauth/*
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
Cloudflare exposes that list as
|
|
120
|
+
`oauth_configuration.dynamic_client_registration.allowed_uris`. It belongs to
|
|
121
|
+
the Access application's Managed OAuth settings, not the Access policy that
|
|
122
|
+
selects admitted identities. Claude uses the fixed first value. ChatGPT may use
|
|
123
|
+
its stable callback or a callback-id path covered by the third value. If a
|
|
124
|
+
client registers a different redirect, add that exact URI or the narrowest path
|
|
125
|
+
wildcard that covers it; do not allow the client's whole origin. Without these
|
|
126
|
+
entries discovery succeeds and client registration fails later, which makes a
|
|
127
|
+
missing allowlist look like a broken MCP server.
|
|
128
|
+
|
|
129
|
+
Do not add a bypass for the discovery routes. A fully
|
|
52
130
|
automated client instead uses a [Cloudflare Access service token](https://developers.cloudflare.com/cloudflare-one/access-controls/service-credentials/service-tokens/)
|
|
53
131
|
through the
|
|
54
132
|
`CF-Access-Client-Id` and `CF-Access-Client-Secret` headers.
|
|
@@ -102,7 +180,9 @@ secure.
|
|
|
102
180
|
Each token has an immutable ID. Activity records store that ID and resolve its
|
|
103
181
|
current friendly name only while an authorized operator reads activity.
|
|
104
182
|
Revoked records remain as metadata tombstones so historical calls keep their
|
|
105
|
-
friendly attribution.
|
|
183
|
+
friendly attribution. New tokens also retain the creating principal. Their MCP
|
|
184
|
+
requests use that principal's connector visibility and personal auth while the
|
|
185
|
+
token itself remains the activity actor and result owner.
|
|
106
186
|
|
|
107
187
|
Access tokens authenticate MCP clients; they are never operator credentials.
|
|
108
188
|
Creation, rename, and revocation require the same eligible human identity and
|
|
@@ -113,20 +193,20 @@ Issuance and revocation inherit the consistency guarantees of the configured
|
|
|
113
193
|
storage adapter. Use strongly consistent storage when either change must take
|
|
114
194
|
effect globally without a convergence window.
|
|
115
195
|
|
|
116
|
-
|
|
196
|
+
Human credential mutation is a separate, narrower boundary. The
|
|
117
197
|
`/credentials` shell contains no secret data before authentication, and the
|
|
118
|
-
mutation API requires same-origin requests from an admitted
|
|
119
|
-
|
|
120
|
-
connector.
|
|
198
|
+
mutation API requires same-origin requests from an admitted interactive human.
|
|
199
|
+
That human may mutate only visible connector slots. An MCP bearer is never
|
|
200
|
+
treated as a browser credential, even when it can call every connector.
|
|
121
201
|
|
|
122
202
|
This split is visible in recovery:
|
|
123
203
|
|
|
124
204
|
- a bearer-authenticated agent may receive `recovery: "operator_config"` and
|
|
125
205
|
pass its `operatorUrl` to a human;
|
|
126
|
-
- an interactive
|
|
206
|
+
- an interactive human with connector access opens that URL, signs in, and updates the
|
|
127
207
|
credential; and
|
|
128
208
|
- a bearer-only deployment still returns the handoff honestly, but mutation
|
|
129
|
-
remains unavailable until interactive
|
|
209
|
+
remains unavailable until interactive user auth is configured.
|
|
130
210
|
|
|
131
211
|
See [meta-tools](./meta-tools.md#authorization-recovery) for the stable recovery
|
|
132
212
|
envelope and [storage and credentials](./storage-and-credentials.md) for vault
|