@zackbart/connecta 0.26.1 → 0.26.3
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 +39 -0
- package/README.md +11 -0
- package/dist/access-tokens.d.ts +38 -0
- package/dist/access-tokens.js +294 -0
- package/dist/auth/downstream-oauth.js +9 -1
- package/dist/connectors/remote-mcp.d.ts +4 -0
- package/dist/connectors/remote-mcp.js +19 -1
- package/dist/identity.js +2 -0
- package/dist/index.d.ts +6 -2
- package/dist/index.js +13 -2
- package/dist/module-contracts.d.ts +5 -0
- package/dist/operator-ui/generated.js +2 -2
- package/dist/operator-ui/model.d.ts +1 -0
- package/dist/operator-ui/view.js +16 -0
- package/dist/routes/access-tokens.js +110 -0
- package/dist/routes/shared.d.ts +3 -1
- package/dist/routes/shared.js +6 -1
- package/dist/routes/ui.js +2 -0
- package/dist/server.js +1 -0
- package/dist/ui.d.ts +1 -1
- package/dist/ui.js +7 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/documentation/architecture.md +1 -1
- package/documentation/auth.md +86 -2
- package/ethos.md +1 -1
- package/package.json +5 -1
- package/templates/node/README.md +11 -0
- package/templates/node/package.json +1 -1
- package/templates/node/src/index.ts +5 -1
|
@@ -108,6 +108,7 @@ export interface UiConnector {
|
|
|
108
108
|
}
|
|
109
109
|
export type CredentialManagementCapability = "available" | "requires_operator" | "vault_not_configured" | "no_slots";
|
|
110
110
|
export interface UiData {
|
|
111
|
+
accessTokenManagement?: "available" | "requires_operator";
|
|
111
112
|
serverInfo: {
|
|
112
113
|
name: string;
|
|
113
114
|
version: string;
|
package/dist/operator-ui/view.js
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
/** Pages the nav lists. A single artifact is reached from the library, not the nav. */
|
|
2
2
|
export const OPERATOR_PAGES = [
|
|
3
3
|
"connections",
|
|
4
|
+
"tokens",
|
|
4
5
|
"activity",
|
|
5
6
|
"artifacts",
|
|
6
7
|
];
|
|
7
8
|
export const PAGE_META = {
|
|
9
|
+
tokens: { path: "/tokens", label: "Access tokens" },
|
|
8
10
|
connections: { path: "/", label: "Connections" },
|
|
9
11
|
activity: { path: "/activity", label: "Activity" },
|
|
10
12
|
artifacts: { path: "/artifacts", label: "Artifacts" },
|
|
@@ -198,6 +200,11 @@ export function confirmCopy(action, name) {
|
|
|
198
200
|
confirm: "Remove",
|
|
199
201
|
};
|
|
200
202
|
}
|
|
203
|
+
export function accessTokenUnavailableCopy(capability) {
|
|
204
|
+
return capability === undefined
|
|
205
|
+
? "Access tokens are not configured for this deployment."
|
|
206
|
+
: "Token management requires an interactive sign-in and explicit permission.";
|
|
207
|
+
}
|
|
201
208
|
export function initialState(page) {
|
|
202
209
|
return {
|
|
203
210
|
page,
|
|
@@ -230,6 +237,12 @@ function identityScopedState() {
|
|
|
230
237
|
credentialNoticeFor: null,
|
|
231
238
|
credentialEditing: null,
|
|
232
239
|
credentialBusy: null,
|
|
240
|
+
tokenPhase: "idle",
|
|
241
|
+
tokenNotice: null,
|
|
242
|
+
tokens: [],
|
|
243
|
+
createdToken: null,
|
|
244
|
+
tokenRenaming: null,
|
|
245
|
+
tokenBusy: false,
|
|
233
246
|
activityPhase: "idle",
|
|
234
247
|
activityNotice: null,
|
|
235
248
|
activityEvents: [],
|
|
@@ -270,6 +283,9 @@ export function withPage(state, page) {
|
|
|
270
283
|
return {
|
|
271
284
|
...state,
|
|
272
285
|
page,
|
|
286
|
+
createdToken: null,
|
|
287
|
+
tokenRenaming: null,
|
|
288
|
+
tokenNotice: null,
|
|
273
289
|
credentialEditing: null,
|
|
274
290
|
credentialNotice: null,
|
|
275
291
|
credentialNoticeFor: null,
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { authorizeUiIdentity, isSameOrigin, privateJson, } from "./shared.js";
|
|
2
|
+
async function readName(request) {
|
|
3
|
+
if (!request.headers
|
|
4
|
+
.get("content-type")
|
|
5
|
+
?.toLowerCase()
|
|
6
|
+
.startsWith("application/json")) {
|
|
7
|
+
return {
|
|
8
|
+
ok: false,
|
|
9
|
+
response: privateJson({ error: "Content-Type must be application/json" }, { status: 415 }),
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
const reader = request.body?.getReader();
|
|
13
|
+
const chunks = [];
|
|
14
|
+
let size = 0;
|
|
15
|
+
if (reader) {
|
|
16
|
+
try {
|
|
17
|
+
while (true) {
|
|
18
|
+
const { done, value } = await reader.read();
|
|
19
|
+
if (done)
|
|
20
|
+
break;
|
|
21
|
+
size += value.byteLength;
|
|
22
|
+
if (size > 1_000) {
|
|
23
|
+
reader.cancel().catch(() => { });
|
|
24
|
+
return { ok: false, response: privateJson({ error: "request body is too large" }, { status: 413 }) };
|
|
25
|
+
}
|
|
26
|
+
chunks.push(value);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
finally {
|
|
30
|
+
reader.releaseLock();
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
const bytes = new Uint8Array(size);
|
|
34
|
+
let offset = 0;
|
|
35
|
+
for (const chunk of chunks) {
|
|
36
|
+
bytes.set(chunk, offset);
|
|
37
|
+
offset += chunk.byteLength;
|
|
38
|
+
}
|
|
39
|
+
const raw = new TextDecoder().decode(bytes);
|
|
40
|
+
if (raw.length > 1_000) {
|
|
41
|
+
return {
|
|
42
|
+
ok: false,
|
|
43
|
+
response: privateJson({ error: "request body is too large" }, { status: 413 }),
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
try {
|
|
47
|
+
const body = JSON.parse(raw);
|
|
48
|
+
return { ok: true, name: body.name };
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
return {
|
|
52
|
+
ok: false,
|
|
53
|
+
response: privateJson({ error: "invalid JSON body" }, { status: 400 }),
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Interactive-operator lifecycle for deployment access tokens. The token itself
|
|
59
|
+
* is deliberately never an administrator credential and cannot reach here.
|
|
60
|
+
*/
|
|
61
|
+
export async function routeAccessTokens(context, manager) {
|
|
62
|
+
const match = /^\/ui\/access-tokens(?:\/([0-9a-f-]{36}))?$/.exec(context.path);
|
|
63
|
+
if (!match)
|
|
64
|
+
return null;
|
|
65
|
+
const { request, baseUrl, opts } = context;
|
|
66
|
+
if (request.method === "OPTIONS") {
|
|
67
|
+
return privateJson({ error: "CORS is not allowed" }, { status: 403 });
|
|
68
|
+
}
|
|
69
|
+
const mutating = request.method !== "GET";
|
|
70
|
+
if (mutating && !isSameOrigin(request, baseUrl)) {
|
|
71
|
+
return privateJson({ error: "same-origin request required" }, { status: 403 });
|
|
72
|
+
}
|
|
73
|
+
const admin = await authorizeUiIdentity(request, baseUrl, opts.auth, "access token management", context.runtimeContext, opts.identity);
|
|
74
|
+
if (!admin.ok)
|
|
75
|
+
return admin.response;
|
|
76
|
+
if (!admin.accessTokenManagement || !admin.identity.principal) {
|
|
77
|
+
return privateJson({ error: "access token management permission required" }, { status: 403 });
|
|
78
|
+
}
|
|
79
|
+
const id = match[1];
|
|
80
|
+
try {
|
|
81
|
+
if (!id && request.method === "GET") {
|
|
82
|
+
return privateJson({ accessTokens: await manager.list() });
|
|
83
|
+
}
|
|
84
|
+
if (!id && request.method === "POST") {
|
|
85
|
+
const input = await readName(request);
|
|
86
|
+
if (!input.ok)
|
|
87
|
+
return input.response;
|
|
88
|
+
return privateJson(await manager.create(input.name, admin.identity.principal), { status: 201 });
|
|
89
|
+
}
|
|
90
|
+
if (id && request.method === "PUT") {
|
|
91
|
+
const input = await readName(request);
|
|
92
|
+
if (!input.ok)
|
|
93
|
+
return input.response;
|
|
94
|
+
const accessToken = await manager.rename(id, input.name);
|
|
95
|
+
return accessToken
|
|
96
|
+
? privateJson({ accessToken })
|
|
97
|
+
: privateJson({ error: "unknown access token" }, { status: 404 });
|
|
98
|
+
}
|
|
99
|
+
if (id && request.method === "DELETE") {
|
|
100
|
+
const accessToken = await manager.revoke(id, `${admin.identity.principal.namespace}:${admin.identity.principal.id}`);
|
|
101
|
+
return accessToken
|
|
102
|
+
? privateJson({ accessToken })
|
|
103
|
+
: privateJson({ error: "unknown access token" }, { status: 404 });
|
|
104
|
+
}
|
|
105
|
+
return privateJson({ error: "method not allowed" }, { status: 405 });
|
|
106
|
+
}
|
|
107
|
+
catch {
|
|
108
|
+
return privateJson({ error: "Access token operation failed; check the name, capacity, and storage" }, { status: 400 });
|
|
109
|
+
}
|
|
110
|
+
}
|
package/dist/routes/shared.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { Implementation } from "@modelcontextprotocol/server";
|
|
2
|
-
import type { ActivityModule, ArtifactsModule, OperatorSurface } from "../module-contracts.js";
|
|
2
|
+
import type { AccessTokensModule, ActivityModule, ArtifactsModule, OperatorSurface } from "../module-contracts.js";
|
|
3
3
|
import type { ActivityActor, ActivityReadGate, ActivityStore } from "../activity.js";
|
|
4
4
|
import type { CredentialVault } from "../credential-contract.js";
|
|
5
5
|
import type { DeferredWork } from "../connector-scope.js";
|
|
@@ -63,6 +63,7 @@ export interface ServerOptions {
|
|
|
63
63
|
activityModule?: ActivityModule | undefined;
|
|
64
64
|
/** Optional team pages; their routes mount only beside `ui`. */
|
|
65
65
|
artifactsModule?: ArtifactsModule | undefined;
|
|
66
|
+
accessTokens?: AccessTokensModule | undefined;
|
|
66
67
|
/** Optional browser UI and OAuth result-page labels. */
|
|
67
68
|
branding?: ConnectaBranding | undefined;
|
|
68
69
|
}
|
|
@@ -102,6 +103,7 @@ export declare function authorize(request: Request, baseUrl: string, auth: Inbou
|
|
|
102
103
|
/** Granted addresses that still require the catalog's read-only hint. */
|
|
103
104
|
guardedToolAccess?: ToolAccess;
|
|
104
105
|
operator: boolean;
|
|
106
|
+
accessTokenManagement: boolean;
|
|
105
107
|
credentialAdministration: ConnectorPermission;
|
|
106
108
|
personalConnection: ConnectorPermission;
|
|
107
109
|
/** Backward-compatible name used by operator views. */
|
package/dist/routes/shared.js
CHANGED
|
@@ -45,7 +45,7 @@ export async function authorize(request, baseUrl, auth, runtimeContext, identity
|
|
|
45
45
|
response: privateJson({ error: "identity access resolution failed" }, { status: 403 }),
|
|
46
46
|
};
|
|
47
47
|
}
|
|
48
|
-
return { ok: true, actor, identity, ...access, operator: false, credentialAdministration: "none", personalConnection: "none" };
|
|
48
|
+
return { ok: true, actor, identity, ...access, operator: false, accessTokenManagement: false, credentialAdministration: "none", personalConnection: "none" };
|
|
49
49
|
}
|
|
50
50
|
let lastResponse = null;
|
|
51
51
|
for (const provider of auth) {
|
|
@@ -75,6 +75,7 @@ export async function authorize(request, baseUrl, auth, runtimeContext, identity
|
|
|
75
75
|
interactive,
|
|
76
76
|
};
|
|
77
77
|
let operator = interactive;
|
|
78
|
+
let accessTokenManagement = false;
|
|
78
79
|
let credentialAdministration = "none";
|
|
79
80
|
let personalConnection = "none";
|
|
80
81
|
let access;
|
|
@@ -86,6 +87,9 @@ export async function authorize(request, baseUrl, auth, runtimeContext, identity
|
|
|
86
87
|
}
|
|
87
88
|
access = parseConnectorAccess(identityConfig?.connectorAccess ? await identityConfig.connectorAccess(identity) : "all", { allowReadOnly: true });
|
|
88
89
|
if (interactive) {
|
|
90
|
+
accessTokenManagement = identityConfig?.accessTokenManagement ? await identityConfig.accessTokenManagement(identity) : false;
|
|
91
|
+
if (typeof accessTokenManagement !== "boolean")
|
|
92
|
+
throw new Error("invalid token management permission");
|
|
89
93
|
credentialAdministration = identityConfig?.credentialAdministration ? await identityConfig.credentialAdministration(identity) : "none";
|
|
90
94
|
personalConnection = principal && identityConfig?.personalConnection ? await identityConfig.personalConnection(identity) : "none";
|
|
91
95
|
}
|
|
@@ -113,6 +117,7 @@ export async function authorize(request, baseUrl, auth, runtimeContext, identity
|
|
|
113
117
|
? { principalKey: await identityStorageKey(principal) }
|
|
114
118
|
: {}),
|
|
115
119
|
...access,
|
|
120
|
+
accessTokenManagement,
|
|
116
121
|
credentialAdministration,
|
|
117
122
|
personalConnection,
|
|
118
123
|
operator,
|
package/dist/routes/ui.js
CHANGED
|
@@ -74,6 +74,7 @@ export async function routeUi(context) {
|
|
|
74
74
|
const artifactPage = operatorPage === "artifacts" || operatorPage === "artifact";
|
|
75
75
|
if (operatorPage &&
|
|
76
76
|
(operatorPage !== "activity" || opts.activity?.list) &&
|
|
77
|
+
(operatorPage !== "tokens" || opts.accessTokens) &&
|
|
77
78
|
(!artifactPage || opts.artifactsModule)) {
|
|
78
79
|
if (request.method !== "GET" && request.method !== "HEAD") {
|
|
79
80
|
return privateJson({ error: "method not allowed" }, { status: 405 });
|
|
@@ -187,6 +188,7 @@ function summary(context) {
|
|
|
187
188
|
serverInfo: opts.serverInfo,
|
|
188
189
|
connectaVersion: CONNECTA_VERSION,
|
|
189
190
|
activityEnabled,
|
|
191
|
+
...(opts.accessTokens ? { accessTokenManagement: authz.accessTokenManagement && authz.identity.principal ? "available" : "requires_operator" } : {}),
|
|
190
192
|
...(opts.artifactsModule && mayViewArtifacts(authz, opts.registry) ? { artifactsEnabled: true } : {}),
|
|
191
193
|
credentialManagement,
|
|
192
194
|
oauthManagement: visible.some(c => mayManage(c.id)),
|
package/dist/server.js
CHANGED
|
@@ -77,6 +77,7 @@ export function createFetchHandler(opts) {
|
|
|
77
77
|
"/health",
|
|
78
78
|
...(opts.ui?.reservedPaths ?? []),
|
|
79
79
|
...(opts.ui && opts.activity?.list ? ["/activity"] : []),
|
|
80
|
+
...(opts.ui && opts.accessTokens ? ["/tokens"] : []),
|
|
80
81
|
...(opts.ui && opts.artifactsModule ? ["/artifacts", "/artifacts/*"] : []),
|
|
81
82
|
],
|
|
82
83
|
},
|
package/dist/ui.d.ts
CHANGED
|
@@ -7,7 +7,7 @@ import type { RegistryView } from "./registry.js";
|
|
|
7
7
|
import type { ConnectaBranding, Connector, ConnectorStatus, ToolDef, UiAuthConfig } from "./types.js";
|
|
8
8
|
export { filterUiConnectors, type CredentialManagementCapability, type UiConnector, type UiData, } from "./operator-ui/model.js";
|
|
9
9
|
export { CONNECTA_FAVICON_SVG, resolveBranding, isSafeHttpUrl, isSafeHttpsUrl, isSafeIconHref } from "./branding.js";
|
|
10
|
-
export type OperatorPage = "connections" | "activity" | "artifacts" | "artifact";
|
|
10
|
+
export type OperatorPage = "tokens" | "connections" | "activity" | "artifacts" | "artifact";
|
|
11
11
|
export declare function operatorPageForPath(path: string): OperatorPage | undefined;
|
|
12
12
|
export declare function operatorPageTitle(page: OperatorPage, configuredTitle: string): string;
|
|
13
13
|
/**
|
package/dist/ui.js
CHANGED
|
@@ -17,6 +17,7 @@ function stringForInlineScript(value) {
|
|
|
17
17
|
}
|
|
18
18
|
const OPERATOR_PAGE_LABELS = {
|
|
19
19
|
connections: "Connections",
|
|
20
|
+
tokens: "Access tokens",
|
|
20
21
|
activity: "Activity",
|
|
21
22
|
artifacts: "Artifacts",
|
|
22
23
|
artifact: "Artifact",
|
|
@@ -24,6 +25,8 @@ const OPERATOR_PAGE_LABELS = {
|
|
|
24
25
|
/** `/artifacts/<id>` and its snapshots, `/artifacts/<id>/v/<version>`. */
|
|
25
26
|
const ARTIFACT_PAGE = /^\/artifacts\/[a-z0-9](?:[a-z0-9-]{0,62}[a-z0-9])?(?:\/v\/\d{1,9})?$/;
|
|
26
27
|
export function operatorPageForPath(path) {
|
|
28
|
+
if (path === "/tokens")
|
|
29
|
+
return "tokens";
|
|
27
30
|
if (path === "/")
|
|
28
31
|
return "connections";
|
|
29
32
|
if (path === "/activity")
|
|
@@ -180,7 +183,7 @@ ${OPERATOR_UI_SCRIPT}</script>`,
|
|
|
180
183
|
});
|
|
181
184
|
}
|
|
182
185
|
function ownsOperatorPath(reserved, path) {
|
|
183
|
-
if (path === "/activity")
|
|
186
|
+
if (path === "/activity" || path === "/tokens")
|
|
184
187
|
return true;
|
|
185
188
|
return reserved.some((pattern) => pattern.endsWith("/*")
|
|
186
189
|
? path.startsWith(pattern.slice(0, -1))
|
|
@@ -211,6 +214,9 @@ export function operatorUi(options = {}) {
|
|
|
211
214
|
return (await artifacts?.handle(context)) ??
|
|
212
215
|
notFoundResponse(context.request, context.opts);
|
|
213
216
|
}
|
|
217
|
+
const tokenResponse = await context.opts.accessTokens?.handle(context);
|
|
218
|
+
if (tokenResponse)
|
|
219
|
+
return tokenResponse;
|
|
214
220
|
const routes = [
|
|
215
221
|
...(context.opts.credentialVault ? [routeCredentials] : []),
|
|
216
222
|
routeOAuthManagement,
|
package/dist/version.d.ts
CHANGED
package/dist/version.js
CHANGED
|
@@ -614,6 +614,6 @@ compiling and configuring the real thing.
|
|
|
614
614
|
connector limiters, then the executor, then the Connecta's runtime; Node's `listen()` calls it on
|
|
615
615
|
SIGTERM/SIGINT.
|
|
616
616
|
- **Structural mistakes throw at construction.** A duplicate connector id, an
|
|
617
|
-
invalid admission rule, the
|
|
617
|
+
invalid admission rule, the old boolean `accessTokens` option, a missing executor:
|
|
618
618
|
all refuse to boot (`test/config.test.ts`, `test/registry.test.ts`). Starting
|
|
619
619
|
in the wrong shape is worse than not starting.
|
package/documentation/auth.md
CHANGED
|
@@ -7,7 +7,7 @@ static bearers are checked first, then other providers in configuration order.
|
|
|
7
7
|
An `InboundAuth` provider's `authorize(request, baseUrl, runtimeContext)`
|
|
8
8
|
returns either `{ ok: true, userId?, subjectId?, principal? }` or a refusal
|
|
9
9
|
carrying its own `Response`, so the provider owns its challenge. Connecta
|
|
10
|
-
issues
|
|
10
|
+
issues managed client tokens only when the optional `accessTokens` module is configured.
|
|
11
11
|
|
|
12
12
|
The bearer adapter challenges with `WWW-Authenticate: Bearer` and deliberately
|
|
13
13
|
omits `resource_metadata`: its credential is configured out of band, so it has no
|
|
@@ -16,6 +16,67 @@ or the edge own OAuth discovery. An open deployment with any connector warns at
|
|
|
16
16
|
construction — including API connectors carrying static auth headers, and with
|
|
17
17
|
sharper wording for credential and OAuth connectors.
|
|
18
18
|
|
|
19
|
+
## Managed client tokens and upgrading from v0.23
|
|
20
|
+
|
|
21
|
+
Import `accessTokens` from `@zackbart/connecta/auth/access-tokens` and pass its
|
|
22
|
+
module to `createConnecta`. It installs its inbound adapter and, beside `ui`,
|
|
23
|
+
the Access tokens page and `/ui/access-tokens` lifecycle routes. Omitting the
|
|
24
|
+
module loads none of its implementation and serves none of those routes.
|
|
25
|
+
|
|
26
|
+
```ts
|
|
27
|
+
import { accessTokens } from "@zackbart/connecta/auth/access-tokens";
|
|
28
|
+
|
|
29
|
+
createConnecta({
|
|
30
|
+
storage,
|
|
31
|
+
accessTokens: accessTokens(storage),
|
|
32
|
+
auth: clerkAuth({ publishableKey, secretKey }),
|
|
33
|
+
ui: operatorUi(),
|
|
34
|
+
identity: {
|
|
35
|
+
connectorAccess, // Keep the existing principal and token-id grant rules.
|
|
36
|
+
accessTokenManagement: ({ principal }) =>
|
|
37
|
+
principal?.namespace === "clerk:your-existing-namespace" &&
|
|
38
|
+
principal.id === "your-operator-id",
|
|
39
|
+
},
|
|
40
|
+
connectors,
|
|
41
|
+
executor,
|
|
42
|
+
});
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
For a v0.23 deployment, replace the old `accessTokens: true` or options object
|
|
46
|
+
with `accessTokens: accessTokens(storage)`, using **the same storage and key
|
|
47
|
+
namespace**. Keep the existing `access-token:v1:record:*` and
|
|
48
|
+
`access-token:v1:lookup:*` records. Their unrevoked `cta_…` secrets keep working;
|
|
49
|
+
no secret recovery, rewrite, or client rotation is needed. Preserve the existing
|
|
50
|
+
identity namespaces and connector/pool grant callbacks too. Storage compatibility
|
|
51
|
+
does not translate deployment configuration or invent replacement grants.
|
|
52
|
+
|
|
53
|
+
The adapter preserves `access_token` actor ids, the
|
|
54
|
+
`connecta:access-tokens:v1` activity namespace, friendly names, and any stored
|
|
55
|
+
principal. A token without a principal stays unbound. Every request evaluates
|
|
56
|
+
current identity/tool/pool grants; a token never becomes an interactive operator.
|
|
57
|
+
Names are labels, never permissions. New UI-issued tokens belong to the issuing
|
|
58
|
+
human's principal, and any explicitly permitted token manager can list, rename,
|
|
59
|
+
or revoke deployment tokens. Static and managed bearers cannot manage tokens or
|
|
60
|
+
connection credentials. Operators need an interactive auth provider.
|
|
61
|
+
|
|
62
|
+
Storage must implement `list`. Existing-token verification, rename, and revoke
|
|
63
|
+
also work on older adapters without `compareAndSet`; **new issuance requires
|
|
64
|
+
atomic `compareAndSet`**, because counting records before writing admits too many
|
|
65
|
+
concurrent creates. Active capacity defaults to 100, configurable with
|
|
66
|
+
`accessTokens(storage, { maxActive: 200 })`, up to 1,000. A durable reservation
|
|
67
|
+
counts before a secret is written. An interrupted create can consume capacity
|
|
68
|
+
without returning a token; it is never automatically retried or released after
|
|
69
|
+
an uncertain write. Avoid creating new tokens through old-version instances once
|
|
70
|
+
new-version issuance has started, since those instances do not honor reservations.
|
|
71
|
+
|
|
72
|
+
Secrets contain 256 random bits and only their SHA-256 digests persist. Creation
|
|
73
|
+
returns the secret once; list and rename never return it. Revocation removes its
|
|
74
|
+
lookup before updating metadata, and authorization has no token cache. A strongly
|
|
75
|
+
consistent store makes revocation effective on the next authorization; an
|
|
76
|
+
eventually consistent backend retains its own propagation delay. Requests already
|
|
77
|
+
admitted are not recalled. Management writes require an exact same-origin Origin,
|
|
78
|
+
and responses are private and non-cacheable.
|
|
79
|
+
|
|
19
80
|
## Origins
|
|
20
81
|
|
|
21
82
|
`allowedOrigins?: readonly string[] | "*"` bounds which browsers may speak to
|
|
@@ -409,7 +470,7 @@ from caller input.
|
|
|
409
470
|
interactive human, the one default here that is open, because a single-operator
|
|
410
471
|
deployment would otherwise be locked out of its own event stream. Team
|
|
411
472
|
deployments should set it. There is no general administrator role and no
|
|
412
|
-
token-management authority.
|
|
473
|
+
implicit token-management authority. `identity.accessTokenManagement` is a separate boolean permission, false by default, evaluated only for interactive humans. Lifecycle routes also require a stable principal.
|
|
413
474
|
|
|
414
475
|
```ts
|
|
415
476
|
createConnecta({
|
|
@@ -554,3 +615,26 @@ present, must match that owner and may then manage the connector, while an
|
|
|
554
615
|
interactive provider's explicit 403 still refuses the flow. See
|
|
555
616
|
[meta-tools](./meta-tools.md#authorization-recovery) for the recovery shapes a
|
|
556
617
|
caller actually receives.
|
|
618
|
+
|
|
619
|
+
## URL-based downstream OAuth clients
|
|
620
|
+
|
|
621
|
+
`remoteMcp` accepts `auth: { type: "oauth", clientMetadataUrl, scope }`.
|
|
622
|
+
`clientMetadataUrl` names a public HTTPS OAuth client metadata document with a
|
|
623
|
+
non-root path. It must not contain credentials or a fragment. The document
|
|
624
|
+
must include its own URL as `client_id`, the deployment's exact
|
|
625
|
+
`/oauth/callback/<connector-id>` in `redirect_uris`, authorization-code and
|
|
626
|
+
refresh-token grants, and `token_endpoint_auth_method: "none"`. Hosting that
|
|
627
|
+
public document belongs to the deployment; Connecta does not expose a public
|
|
628
|
+
route through inbound authentication.
|
|
629
|
+
|
|
630
|
+
The SDK uses the URL as the client ID only when the authorization server
|
|
631
|
+
advertises `client_id_metadata_document_supported`. Otherwise it keeps the
|
|
632
|
+
existing dynamic registration flow. State validation, PKCE, issuer binding,
|
|
633
|
+
encrypted token storage, refresh, and disconnect follow the same code paths.
|
|
634
|
+
The metadata URL and scopes participate in the saved-client configuration
|
|
635
|
+
binding, so restarting after either changes cannot reuse an old registration.
|
|
636
|
+
|
|
637
|
+
`scope` supplies space-separated default OAuth scopes through client metadata.
|
|
638
|
+
A downstream challenge or protected-resource scope declaration takes precedence,
|
|
639
|
+
and the SDK adds `offline_access` when advertised for refresh-token grants.
|
|
640
|
+
Omitting both settings preserves the existing discovery and registration flow.
|
package/ethos.md
CHANGED
|
@@ -40,7 +40,7 @@ carries them.
|
|
|
40
40
|
| Optional deployment modules | accepted | typed slots select UI, activity, vault, and inbound auth; core keeps discovery, execution, invocation, and enforcement |
|
|
41
41
|
| Artifacts module | accepted | a built-in connector for team-only sandboxed pages over stored JSON; immutable versions let writes skip approval. Supersedes [#287](https://github.com/zackbart/connecta/issues/287) |
|
|
42
42
|
| Plugin lifecycle, provider registry, or marketplace | refused | modules are deployment code, not runtime installs; prebuilt connections are imports, discovered in docs ([#297](https://github.com/zackbart/connecta/issues/297)) |
|
|
43
|
-
| Connecta-issued access tokens |
|
|
43
|
+
| Connecta-issued access tokens | accepted | optional /auth/access-tokens preserves v0.23 secrets; config owns grants, interactive management needs explicit permission ([#619](https://github.com/zackbart/connecta/issues/619)) |
|
|
44
44
|
| Expanded Notion page create/update options | refused | different workflows, not missing fields; use `api()` ([#408](https://github.com/zackbart/connecta/issues/408)) |
|
|
45
45
|
| Resources, prompts, and downstream MCP Apps templates | refused | tools only; clients own presentation ([#266](https://github.com/zackbart/connecta/issues/266)) |
|
|
46
46
|
| Protocol sessions, server push, elicitation passthrough | refused | stateless per request; elicitation has no route |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zackbart/connecta",
|
|
3
|
-
"version": "0.26.
|
|
3
|
+
"version": "0.26.3",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"sideEffects": false,
|
|
6
6
|
"description": "One MCP to rule them all — a single MCP endpoint aggregating many downstream connectors behind a code-first surface of eight meta-tools.",
|
|
@@ -114,6 +114,10 @@
|
|
|
114
114
|
"./artifacts": {
|
|
115
115
|
"types": "./dist/artifacts.d.ts",
|
|
116
116
|
"import": "./dist/artifacts.js"
|
|
117
|
+
},
|
|
118
|
+
"./auth/access-tokens": {
|
|
119
|
+
"types": "./dist/access-tokens.d.ts",
|
|
120
|
+
"import": "./dist/access-tokens.js"
|
|
117
121
|
}
|
|
118
122
|
},
|
|
119
123
|
"scripts": {
|
package/templates/node/README.md
CHANGED
|
@@ -179,3 +179,14 @@ Doctor verifies the MCP contract. Verify UI behavior separately: sign in at `/`,
|
|
|
179
179
|
confirm the visible connections and their permitted auth controls, and check
|
|
180
180
|
Activity only when you enabled a readable history store. A missing optional
|
|
181
181
|
feature should not leave a tab behind.
|
|
182
|
+
|
|
183
|
+
### Existing client tokens
|
|
184
|
+
|
|
185
|
+
Upgrading from v0.23 does not require rotating managed `cta_…` tokens. Import
|
|
186
|
+
`accessTokens` from `@zackbart/connecta/auth/access-tokens`, replace the old
|
|
187
|
+
`accessTokens: true` with `accessTokens: accessTokens(storage)`, and keep the
|
|
188
|
+
same persistent storage namespace and identity/tool/pool grant rules. Enable
|
|
189
|
+
`identity.accessTokenManagement` only for the interactive operators who should
|
|
190
|
+
manage tokens. New issuance needs storage with atomic `compareAndSet`; older
|
|
191
|
+
storage adapters can still verify existing tokens. See the package's
|
|
192
|
+
`documentation/auth.md` for the migration and storage requirements.
|
|
@@ -27,6 +27,7 @@ import { operatorUi } from "@zackbart/connecta/ui";
|
|
|
27
27
|
import { api, createConnecta } from "@zackbart/connecta";
|
|
28
28
|
import { fileStorage, listen } from "@zackbart/connecta/node";
|
|
29
29
|
import { quickJsExecutor } from "@zackbart/connecta/quickjs";
|
|
30
|
+
// import { accessTokens } from "@zackbart/connecta/auth/access-tokens";
|
|
30
31
|
// import { artifacts, kvArtifactStore } from "@zackbart/connecta/artifacts";
|
|
31
32
|
// Operator sign-in. Needs `npm install @clerk/backend` — it is an optional
|
|
32
33
|
// peer, so it does not install with Connecta.
|
|
@@ -63,6 +64,8 @@ const storage = fileStorage(stateFile);
|
|
|
63
64
|
|
|
64
65
|
const connecta = createConnecta({
|
|
65
66
|
storage,
|
|
67
|
+
// Reuse this storage to keep v0.23 client tokens without rotating secrets.
|
|
68
|
+
// accessTokens: accessTokens(storage),
|
|
66
69
|
auth: [
|
|
67
70
|
bearerToken(token, { subjectId: "operator" }),
|
|
68
71
|
// clerkAuth({
|
|
@@ -74,7 +77,7 @@ const connecta = createConnecta({
|
|
|
74
77
|
// // allowedDomains: ["acme.com"],
|
|
75
78
|
// }),
|
|
76
79
|
],
|
|
77
|
-
// Code-owned identity resolvers.
|
|
80
|
+
// Code-owned identity resolvers. Connection management permissions default to
|
|
78
81
|
// none, so the template grants them. The commented pair is the optional
|
|
79
82
|
// member/operator split for Clerk-backed deployments: connector access is
|
|
80
83
|
// derived from the authenticated identity and cannot be selected by an MCP
|
|
@@ -86,6 +89,7 @@ const connecta = createConnecta({
|
|
|
86
89
|
// connectorAccess: ({ principal }) =>
|
|
87
90
|
// principal?.id === "user_admin" ? "all" : ["time"],
|
|
88
91
|
// activityAccess: ({ id }) => id === "user_admin",
|
|
92
|
+
// accessTokenManagement: ({ principal }) => principal?.id === "user_admin",
|
|
89
93
|
},
|
|
90
94
|
publicUrl,
|
|
91
95
|
// Required: model-written programs run in a bounded QuickJS child.
|