@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.
@@ -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;
@@ -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
+ }
@@ -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. */
@@ -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
@@ -4,4 +4,4 @@
4
4
  * a bump that forgets this file fails the build rather than shipping a stale
5
5
  * version to `/health` and to downstream MCP handshakes.
6
6
  */
7
- export declare const CONNECTA_VERSION = "0.26.1";
7
+ export declare const CONNECTA_VERSION = "0.26.3";
package/dist/version.js CHANGED
@@ -4,4 +4,4 @@
4
4
  * a bump that forgets this file fails the build rather than shipping a stale
5
5
  * version to `/health` and to downstream MCP handshakes.
6
6
  */
7
- export const CONNECTA_VERSION = "0.26.1";
7
+ export const CONNECTA_VERSION = "0.26.3";
@@ -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 removed `accessTokens` option, a missing executor:
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.
@@ -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 no tokens of its own and serves no token-management routes.
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 | removed | inbound providers authenticate clients; bearer auth stays an optional adapter |
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.1",
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": {
@@ -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.
@@ -15,7 +15,7 @@
15
15
  "typecheck": "tsc --noEmit"
16
16
  },
17
17
  "dependencies": {
18
- "@zackbart/connecta": "0.26.1",
18
+ "@zackbart/connecta": "0.26.3",
19
19
  "quickjs-emscripten": "0.32.0"
20
20
  },
21
21
  "devDependencies": {
@@ -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. The two management permissions default to
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.