@zackbart/connecta 0.23.0 → 0.24.1

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.
Files changed (81) hide show
  1. package/AGENTS.md +5 -0
  2. package/CHANGELOG.md +53 -0
  3. package/README.md +18 -10
  4. package/dist/activity-friction.d.ts +3 -0
  5. package/dist/activity-friction.js +19 -0
  6. package/dist/activity.d.ts +11 -2
  7. package/dist/activity.js +15 -19
  8. package/dist/auth/downstream-oauth.d.ts +2 -1
  9. package/dist/auth/downstream-oauth.js +10 -1
  10. package/dist/branding.d.ts +67 -0
  11. package/dist/branding.js +176 -0
  12. package/dist/connectors/remote-mcp.js +3 -5
  13. package/dist/credential-contract.d.ts +24 -0
  14. package/dist/credential-contract.js +1 -0
  15. package/dist/credential-rules.d.ts +85 -0
  16. package/dist/credential-rules.js +107 -0
  17. package/dist/credentials.d.ts +4 -100
  18. package/dist/credentials.js +3 -107
  19. package/dist/execute.d.ts +6 -0
  20. package/dist/execute.js +17 -7
  21. package/dist/index.d.ts +35 -56
  22. package/dist/index.js +34 -58
  23. package/dist/invocation.js +19 -4
  24. package/dist/meta-tools.d.ts +4 -0
  25. package/dist/meta-tools.js +8 -4
  26. package/dist/module-contracts.d.ts +19 -0
  27. package/dist/module-contracts.js +1 -0
  28. package/dist/operator-ui/generated.js +2 -2
  29. package/dist/operator-ui/model.d.ts +6 -3
  30. package/dist/operator-ui/view.d.ts +2 -18
  31. package/dist/operator-ui/view.js +3 -20
  32. package/dist/registry.d.ts +4 -1
  33. package/dist/registry.js +4 -6
  34. package/dist/routes/activity.js +1 -1
  35. package/dist/routes/credentials.js +5 -2
  36. package/dist/routes/mcp.js +13 -3
  37. package/dist/routes/oauth-management.d.ts +2 -0
  38. package/dist/routes/oauth-management.js +108 -0
  39. package/dist/routes/oauth.d.ts +0 -1
  40. package/dist/routes/oauth.js +21 -121
  41. package/dist/routes/shared.d.ts +23 -17
  42. package/dist/routes/shared.js +48 -44
  43. package/dist/routes/ui.js +36 -33
  44. package/dist/server.js +6 -26
  45. package/dist/types.d.ts +2 -0
  46. package/dist/ui.d.ts +15 -70
  47. package/dist/ui.js +176 -317
  48. package/dist/version.d.ts +1 -1
  49. package/dist/version.js +1 -1
  50. package/documentation/architecture.md +26 -17
  51. package/documentation/auth.md +61 -106
  52. package/documentation/cloudflare.md +1 -1
  53. package/documentation/code-mode.md +2 -2
  54. package/documentation/connectors.md +1 -1
  55. package/documentation/linear.md +1 -1
  56. package/documentation/meta-tools.md +6 -4
  57. package/documentation/mixpanel.md +1 -1
  58. package/documentation/notion.md +2 -2
  59. package/documentation/operations.md +14 -14
  60. package/documentation/operator-ui.md +82 -104
  61. package/documentation/optional-modules-upgrade.md +243 -0
  62. package/documentation/provider-conventions.md +5 -3
  63. package/documentation/revenuecat.md +1 -1
  64. package/documentation/storage-and-credentials.md +59 -40
  65. package/documentation/stripe.md +1 -1
  66. package/documentation/upgrading.md +33 -4
  67. package/ethos.md +22 -30
  68. package/examples/worker/AGENTS.md +3 -1
  69. package/examples/worker/README.md +68 -84
  70. package/examples/worker/src/d1-activity.ts +1 -1
  71. package/examples/worker/src/index.ts +11 -6
  72. package/package.json +17 -1
  73. package/templates/node/AGENTS.md +8 -6
  74. package/templates/node/README.md +56 -67
  75. package/templates/node/package.json +1 -1
  76. package/templates/node/src/file-activity.ts +1 -1
  77. package/templates/node/src/index.ts +11 -12
  78. package/dist/access-tokens.d.ts +0 -31
  79. package/dist/access-tokens.js +0 -236
  80. package/dist/routes/access-tokens.d.ts +0 -6
  81. package/dist/routes/access-tokens.js +0 -83
@@ -1,13 +1,9 @@
1
1
  export const OPERATOR_PAGES = [
2
2
  "connections",
3
- "credentials",
4
- "tokens",
5
3
  "activity",
6
4
  ];
7
5
  export const PAGE_META = {
8
6
  connections: { path: "/", label: "Connections" },
9
- credentials: { path: "/credentials", label: "Credentials" },
10
- tokens: { path: "/tokens", label: "Access tokens" },
11
7
  activity: { path: "/activity", label: "Activity" },
12
8
  };
13
9
  export function pageForPath(path) {
@@ -45,12 +41,6 @@ function identityScopedState() {
45
41
  credentialNotice: null,
46
42
  credentialEditing: null,
47
43
  credentialBusy: null,
48
- tokenPhase: "idle",
49
- tokenNotice: null,
50
- tokens: [],
51
- createdToken: null,
52
- tokenRenaming: null,
53
- tokenBusy: false,
54
44
  activityPhase: "idle",
55
45
  activityNotice: null,
56
46
  activityEvents: [],
@@ -82,9 +72,6 @@ export function withPage(state, page) {
82
72
  return {
83
73
  ...state,
84
74
  page,
85
- createdToken: null,
86
- tokenRenaming: null,
87
- tokenNotice: null,
88
75
  credentialEditing: null,
89
76
  credentialNotice: null,
90
77
  };
@@ -94,17 +81,13 @@ export function credentialUnavailableCopy(capability) {
94
81
  return "No connectors declare operator-managed credential slots. Connector credentials remain configuration-as-code until a slot is declared.";
95
82
  }
96
83
  if (capability === "vault_not_configured") {
97
- return "Credential storage is not configured. Set credentials.encryptionKey before managing connector credentials here.";
84
+ return "Credential storage is not configured. Configure a vault before managing connector credentials here.";
98
85
  }
99
86
  return "Credential management requires an interactive user. Bearer-authenticated sessions can inspect connections but cannot manage stored credentials.";
100
87
  }
101
- export function accessTokenUnavailableCopy(capability) {
102
- if (capability === "not_configured") {
103
- return "Access tokens are not configured for this deployment. Add accessTokens to the deployment configuration to enable them.";
104
- }
105
- return "Access token management requires an eligible interactive operator. A Bearer token can connect to MCP, but it cannot create or revoke other tokens.";
106
- }
107
88
  export function connectorStatusLabel(status) {
89
+ if (status === "loading")
90
+ return "Loading details";
108
91
  if (status === "ok")
109
92
  return "Connected";
110
93
  if (status === "auth_required")
@@ -1,7 +1,7 @@
1
+ import type { CredentialVault } from "./credential-contract.js";
1
2
  import type { CatalogDriftReport, Connector, ConnectorContext, ConnectorStatus, KVStorage, Logger, ToolDef } from "./types.js";
2
3
  import { type DeferredWork } from "./connector-scope.js";
3
4
  import { type CatalogDriftActivityContext } from "./activity.js";
4
- import { type CredentialVault } from "./credentials.js";
5
5
  import { type CallAdmissionPermit, type ConnectorCallAdmissionSnapshot } from "./call-admission.js";
6
6
  /**
7
7
  * Split `"<connectorId>.<toolName>"` on the first dot. Connector ids contain
@@ -47,6 +47,7 @@ export interface RegistryOptions {
47
47
  storage: KVStorage;
48
48
  logger: Logger;
49
49
  credentialVault?: CredentialVault | undefined;
50
+ credentialUi?: boolean | undefined;
50
51
  /** Internal owner partition used by a personal registry. */
51
52
  credentialOwner?: string | undefined;
52
53
  /** Internal child registries skip deployment-wide construction warnings. */
@@ -84,6 +85,7 @@ export interface CatalogReadOptions {
84
85
  * or its construction-only methods.
85
86
  */
86
87
  export interface RegistryView {
88
+ credentialUiAvailable(): boolean;
87
89
  /** Deployment-wide result-size cap threaded to the meta-tools. */
88
90
  readonly maxResultBytes: number;
89
91
  listConnectors(): Connector[];
@@ -217,6 +219,7 @@ export declare class Registry implements RegistryView {
217
219
  * separate from any connector's `conn:<id>:` namespace. Backs get_result.
218
220
  */
219
221
  resultsStorage(): KVStorage;
222
+ credentialUiAvailable(): boolean;
220
223
  bindOAuthHandoff(): Promise<void>;
221
224
  observedOutputSchema(connectorId: string, definition: ToolDef): ToolDef["outputSchema"] | undefined;
222
225
  observeOutputShape(connectorId: string, definition: ToolDef, value: unknown): void;
package/dist/registry.js CHANGED
@@ -1,6 +1,5 @@
1
1
  import { closeConnectorScope, } from "./connector-scope.js";
2
- import { recordCatalogDriftActivity, } from "./activity.js";
3
- import { storedCredentialShape, } from "./credentials.js";
2
+ import { storedCredentialShape, } from "./credential-rules.js";
4
3
  import { ConnectorCallError, msg } from "./errors.js";
5
4
  import { ConnectorCallAdmissionController, } from "./call-admission.js";
6
5
  import { boundedCatalogDrift } from "./catalog-drift.js";
@@ -393,7 +392,7 @@ export class Registry {
393
392
  this.reportedDrift.set(connector.id, counts);
394
393
  if (previous === undefined && clean)
395
394
  return;
396
- recordCatalogDriftActivity(this.opts.catalogDriftActivity
395
+ this.opts.catalogDriftActivity?.recordDrift?.(this.opts.catalogDriftActivity
397
396
  ? { ...this.opts.catalogDriftActivity, logger: this.opts.logger }
398
397
  : undefined, { connectorId: connector.id, ...counts });
399
398
  }
@@ -409,6 +408,7 @@ export class Registry {
409
408
  resultsStorage() {
410
409
  return namespaced(this.opts.storage, "results:");
411
410
  }
411
+ credentialUiAvailable() { return Boolean(this.opts.credentialUi); }
412
412
  async bindOAuthHandoff() {
413
413
  // Shared OAuth already resolves in deployment-wide connector storage.
414
414
  }
@@ -990,9 +990,6 @@ export class Registry {
990
990
  // observation when this runtime has not made one.
991
991
  const boundedStatus = {
992
992
  state: status.state,
993
- ...(status.authorizationUrl !== undefined
994
- ? { authorizationUrl: status.authorizationUrl }
995
- : {}),
996
993
  ...(status.message !== undefined ? { message: status.message } : {}),
997
994
  };
998
995
  return {
@@ -1127,6 +1124,7 @@ class ScopedRegistryView {
1127
1124
  const registry = this.registryFor(id);
1128
1125
  return registry ? registry.invalidateStored(id) : Promise.resolve();
1129
1126
  }
1127
+ credentialUiAvailable() { return this.root.credentialUiAvailable(); }
1130
1128
  async bindOAuthHandoff(id, authorizationUrl) {
1131
1129
  const connector = this.getConnector(id);
1132
1130
  if (connector?.authScope !== "personal" || !this.scope.principalKey)
@@ -137,7 +137,7 @@ export async function routeActivity(context) {
137
137
  const authz = await authorize(request, baseUrl, opts.auth, runtimeContext, opts.identity, false);
138
138
  if (!authz.ok)
139
139
  return authz.response;
140
- if (opts.identity?.operatorAccess && !authz.operator) {
140
+ if (!authz.operator) {
141
141
  return privateJson({ error: "operator access required" }, { status: 403 });
142
142
  }
143
143
  if (opts.activityReadGate &&
@@ -1,5 +1,5 @@
1
- import { credentialTestRule, describeCredentialTestMismatch, storedCredentialShape, } from "../credentials.js";
2
- import { authorizeUiIdentity, isSameOrigin, msg, privateJson, } from "./shared.js";
1
+ import { credentialTestRule, describeCredentialTestMismatch, storedCredentialShape, } from "../credential-rules.js";
2
+ import { authorizeUiIdentity, mayManageConnector, validateAuthPermissions, isSameOrigin, msg, privateJson, } from "./shared.js";
3
3
  async function readCredentialInput(request, config) {
4
4
  if (!request.headers
5
5
  .get("content-type")
@@ -78,6 +78,7 @@ async function handleCredentialRequest(context, connectorId, action) {
78
78
  return authz.response;
79
79
  let registry;
80
80
  try {
81
+ validateAuthPermissions(authz, opts.registry);
81
82
  registry = opts.registry.scoped({
82
83
  connectorIds: authz.connectorIds,
83
84
  ...(authz.subjectKey ? { subjectKey: authz.subjectKey } : {}),
@@ -91,6 +92,8 @@ async function handleCredentialRequest(context, connectorId, action) {
91
92
  if (!connector?.credential) {
92
93
  return privateJson({ error: "unknown credential slot" }, { status: 404 });
93
94
  }
95
+ if (!mayManageConnector(authz, connector))
96
+ return privateJson({ error: "credential management is not permitted" }, { status: 403 });
94
97
  const personal = connector.authScope === "personal";
95
98
  if (personal && !authz.principalKey) {
96
99
  return privateJson({ error: "forbidden" }, { status: 403 });
@@ -4,7 +4,7 @@ import { ExecutorAdmissionError, } from "../executor-admission.js";
4
4
  import { registerMetaTools } from "../meta-tools.js";
5
5
  import { instructionsFor } from "../skills.js";
6
6
  import { msg } from "../errors.js";
7
- import { authorize, } from "./shared.js";
7
+ import { authorize, mayManageConnector, validateAuthPermissions, } from "./shared.js";
8
8
  export const MCP_CORS_HEADERS = {
9
9
  "Access-Control-Allow-Origin": "*",
10
10
  "Access-Control-Allow-Methods": "GET, POST, DELETE, OPTIONS",
@@ -148,7 +148,7 @@ function toolkitRetired(logger) {
148
148
  },
149
149
  });
150
150
  }
151
- async function serveMcp(request, opts, baseUrl, actor, registry, runtimeContext) {
151
+ async function serveMcp(request, opts, baseUrl, actor, registry, canManageAuth, runtimeContext) {
152
152
  const createServer = () => {
153
153
  const server = new McpServer(opts.serverInfo, {
154
154
  instructions: instructionsFor(),
@@ -162,6 +162,7 @@ async function serveMcp(request, opts, baseUrl, actor, registry, runtimeContext)
162
162
  const activity = opts.activity
163
163
  ? {
164
164
  sink: opts.activity,
165
+ recordTool: opts.activityModule?.recordTool,
165
166
  actor,
166
167
  requestId: crypto.randomUUID(),
167
168
  serverInfo: opts.serverInfo,
@@ -176,6 +177,8 @@ async function serveMcp(request, opts, baseUrl, actor, registry, runtimeContext)
176
177
  : undefined;
177
178
  registerMetaTools(server, registry, {
178
179
  baseUrl,
180
+ canManageAuth,
181
+ credentialHandoffUrl: opts.ui?.credentialHandoffUrl(baseUrl),
179
182
  ...(activity ? { activity } : {}),
180
183
  ...(opts.defaultToolTimeoutMs !== undefined
181
184
  ? { defaultToolTimeoutMs: opts.defaultToolTimeoutMs }
@@ -212,6 +215,12 @@ async function serveMcp(request, opts, baseUrl, actor, registry, runtimeContext)
212
215
  ...(opts.maxEmittedBlocks !== undefined
213
216
  ? { maxEmittedBlocks: opts.maxEmittedBlocks }
214
217
  : {}),
218
+ ...(opts.maxHostCalls !== undefined
219
+ ? { maxHostCalls: opts.maxHostCalls }
220
+ : {}),
221
+ ...(opts.hostCallTimeoutMs !== undefined
222
+ ? { hostCallTimeoutMs: opts.hostCallTimeoutMs }
223
+ : {}),
215
224
  });
216
225
  return server;
217
226
  };
@@ -288,6 +297,7 @@ export function createMcpRoute(opts) {
288
297
  }
289
298
  let scopedRegistry;
290
299
  try {
300
+ validateAuthPermissions(authz, opts.registry);
291
301
  scopedRegistry = opts.registry.scoped({
292
302
  connectorIds: authz.connectorIds,
293
303
  ...(authz.subjectKey ? { subjectKey: authz.subjectKey } : {}),
@@ -303,7 +313,7 @@ export function createMcpRoute(opts) {
303
313
  if (new URL(request.url).searchParams.has("toolkit")) {
304
314
  return releaseAdmissionWithResponse(withMcpCors(toolkitRetired(opts.logger)), admission, request.signal);
305
315
  }
306
- return releaseAdmissionWithResponse(withMcpCors(await serveMcp(request, opts, baseUrl, authz.actor, scopedRegistry, runtimeContext)), admission, request.signal);
316
+ return releaseAdmissionWithResponse(withMcpCors(await serveMcp(request, opts, baseUrl, authz.actor, scopedRegistry, id => { const connector = scopedRegistry.getConnector(id); return Boolean(connector && mayManageConnector(authz, connector)); }, runtimeContext)), admission, request.signal);
307
317
  }
308
318
  catch (error) {
309
319
  admission.release();
@@ -0,0 +1,2 @@
1
+ import { type RouteContext } from "./shared.js";
2
+ export declare function routeOAuthManagement(context: RouteContext): Promise<Response | null>;
@@ -0,0 +1,108 @@
1
+ import { closeConnectorScope } from "../connector-scope.js";
2
+ import { isSafeHttpUrl } from "../ui.js";
3
+ import { authorizeUiIdentity, mayManageConnector, validateAuthPermissions, isSameOrigin, msg, privateJson, } from "./shared.js";
4
+ async function handleOAuthManagementRequest(context, connectorId) {
5
+ const { request, baseUrl, opts, defer } = context;
6
+ if (!isSameOrigin(request, baseUrl)) {
7
+ return privateJson({ error: "same-origin request required" }, { status: 403 });
8
+ }
9
+ const authz = await authorizeUiIdentity(request, baseUrl, opts.auth, "OAuth management", context.runtimeContext, opts.identity);
10
+ if (!authz.ok)
11
+ return authz.response;
12
+ let registry;
13
+ try {
14
+ validateAuthPermissions(authz, opts.registry);
15
+ registry = opts.registry.scoped({
16
+ connectorIds: authz.connectorIds,
17
+ ...(authz.subjectKey ? { subjectKey: authz.subjectKey } : {}),
18
+ ...(authz.principalKey ? { principalKey: authz.principalKey } : {}),
19
+ });
20
+ }
21
+ catch (error) {
22
+ return privateJson({ error: msg(error) }, { status: 403 });
23
+ }
24
+ const connector = registry.getConnector(connectorId);
25
+ if (!connector?.disconnectAuth || !connector.startAuth) {
26
+ return privateJson({ error: "unknown OAuth connector" }, { status: 404 });
27
+ }
28
+ if (!mayManageConnector(authz, connector))
29
+ return privateJson({ error: "credential management is not permitted" }, { status: 403 });
30
+ if (connector.authScope === "personal" && !authz.principalKey) {
31
+ return privateJson({ error: "forbidden" }, { status: 403 });
32
+ }
33
+ if (request.method !== "DELETE" && request.method !== "POST") {
34
+ return privateJson({ error: "method not allowed" }, { status: 405 });
35
+ }
36
+ const requestScope = {};
37
+ const ctx = registry.contextFor(connectorId, baseUrl, requestScope);
38
+ try {
39
+ let result;
40
+ let operationError;
41
+ try {
42
+ if (request.method === "DELETE") {
43
+ await connector.disconnectAuth(ctx);
44
+ }
45
+ else {
46
+ result = await connector.startAuth(ctx, { force: true });
47
+ if (result.authorizationUrl) {
48
+ await registry.bindOAuthHandoff(connectorId, result.authorizationUrl);
49
+ }
50
+ }
51
+ }
52
+ catch (error) {
53
+ operationError = error;
54
+ }
55
+ // The old grant and its cached catalog are invalid after either operation,
56
+ // including a partially failed physical cleanup whose epoch fence succeeded.
57
+ try {
58
+ await registry.invalidateStored(connectorId);
59
+ }
60
+ catch (error) {
61
+ operationError ??= error;
62
+ }
63
+ if (operationError) {
64
+ return privateJson({ error: msg(operationError) }, { status: 400 });
65
+ }
66
+ if (request.method === "DELETE") {
67
+ return new Response(null, {
68
+ status: 204,
69
+ headers: {
70
+ "Cache-Control": "no-store",
71
+ "Referrer-Policy": "no-referrer",
72
+ },
73
+ });
74
+ }
75
+ const authorizationUrl = isSafeHttpUrl(result.authorizationUrl)
76
+ ? result.authorizationUrl
77
+ : undefined;
78
+ if (result.state === "error") {
79
+ return privateJson({ error: result.message || "OAuth authorization could not start" }, { status: 502 });
80
+ }
81
+ if (result.state === "auth_required" && !authorizationUrl) {
82
+ return privateJson({
83
+ error: result.message ||
84
+ "OAuth authorization requires consent but no safe URL is available",
85
+ }, { status: 502 });
86
+ }
87
+ return privateJson({
88
+ state: result.state,
89
+ ...(result.message ? { message: result.message } : {}),
90
+ ...(authorizationUrl ? { authorizationUrl } : {}),
91
+ });
92
+ }
93
+ finally {
94
+ await closeConnectorScope(connector, ctx, defer);
95
+ }
96
+ }
97
+ export async function routeOAuthManagement(context) {
98
+ const match = /^\/ui\/oauth\/([a-z0-9_-]+)$/.exec(context.path);
99
+ if (!match)
100
+ return null;
101
+ const connectorId = match[1];
102
+ if (!connectorId)
103
+ return null;
104
+ if (context.request.method === "OPTIONS") {
105
+ return privateJson({ error: "method not allowed" }, { status: 405 });
106
+ }
107
+ return handleOAuthManagementRequest(context, connectorId);
108
+ }
@@ -1,3 +1,2 @@
1
1
  import { type RouteContext } from "./shared.js";
2
- export declare function routeOAuthManagement(context: RouteContext): Promise<Response | null>;
3
2
  export declare function routeOAuthCallback(context: RouteContext): Promise<Response | null>;
@@ -1,109 +1,6 @@
1
1
  import { oauthValueStorageKey } from "../auth/downstream-oauth.js";
2
- import { closeConnectorScope } from "../connector-scope.js";
3
- import { isSafeHttpUrl, resolveBranding } from "../ui.js";
4
- import { authorizeUiIdentity, isSameOrigin, loggableValue, msg, privateJson, } from "./shared.js";
5
- async function handleOAuthManagementRequest(context, connectorId) {
6
- const { request, baseUrl, opts, defer } = context;
7
- if (!isSameOrigin(request, baseUrl)) {
8
- return privateJson({ error: "same-origin request required" }, { status: 403 });
9
- }
10
- const authz = await authorizeUiIdentity(request, baseUrl, opts.auth, "OAuth management", context.runtimeContext, opts.identity);
11
- if (!authz.ok)
12
- return authz.response;
13
- let registry;
14
- try {
15
- registry = opts.registry.scoped({
16
- connectorIds: authz.connectorIds,
17
- ...(authz.subjectKey ? { subjectKey: authz.subjectKey } : {}),
18
- ...(authz.principalKey ? { principalKey: authz.principalKey } : {}),
19
- });
20
- }
21
- catch (error) {
22
- return privateJson({ error: msg(error) }, { status: 403 });
23
- }
24
- const connector = registry.getConnector(connectorId);
25
- if (!connector?.disconnectAuth || !connector.startAuth) {
26
- return privateJson({ error: "unknown OAuth connector" }, { status: 404 });
27
- }
28
- if (connector.authScope === "personal" && !authz.principalKey) {
29
- return privateJson({ error: "forbidden" }, { status: 403 });
30
- }
31
- if (request.method !== "DELETE" && request.method !== "POST") {
32
- return privateJson({ error: "method not allowed" }, { status: 405 });
33
- }
34
- const requestScope = {};
35
- const ctx = registry.contextFor(connectorId, baseUrl, requestScope);
36
- try {
37
- let result;
38
- let operationError;
39
- try {
40
- if (request.method === "DELETE") {
41
- await connector.disconnectAuth(ctx);
42
- }
43
- else {
44
- result = await connector.startAuth(ctx, { force: true });
45
- if (result.authorizationUrl) {
46
- await registry.bindOAuthHandoff(connectorId, result.authorizationUrl);
47
- }
48
- }
49
- }
50
- catch (error) {
51
- operationError = error;
52
- }
53
- // The old grant and its cached catalog are invalid after either operation,
54
- // including a partially failed physical cleanup whose epoch fence succeeded.
55
- try {
56
- await registry.invalidateStored(connectorId);
57
- }
58
- catch (error) {
59
- operationError ??= error;
60
- }
61
- if (operationError) {
62
- return privateJson({ error: msg(operationError) }, { status: 400 });
63
- }
64
- if (request.method === "DELETE") {
65
- return new Response(null, {
66
- status: 204,
67
- headers: {
68
- "Cache-Control": "no-store",
69
- "Referrer-Policy": "no-referrer",
70
- },
71
- });
72
- }
73
- const authorizationUrl = isSafeHttpUrl(result.authorizationUrl)
74
- ? result.authorizationUrl
75
- : undefined;
76
- if (result.state === "error") {
77
- return privateJson({ error: result.message || "OAuth authorization could not start" }, { status: 502 });
78
- }
79
- if (result.state === "auth_required" && !authorizationUrl) {
80
- return privateJson({
81
- error: result.message ||
82
- "OAuth authorization requires consent but no safe URL is available",
83
- }, { status: 502 });
84
- }
85
- return privateJson({
86
- state: result.state,
87
- ...(result.message ? { message: result.message } : {}),
88
- ...(authorizationUrl ? { authorizationUrl } : {}),
89
- });
90
- }
91
- finally {
92
- await closeConnectorScope(connector, ctx, defer);
93
- }
94
- }
95
- export async function routeOAuthManagement(context) {
96
- const match = /^\/ui\/oauth\/([a-z0-9_-]+)$/.exec(context.path);
97
- if (!match)
98
- return null;
99
- const connectorId = match[1];
100
- if (!connectorId)
101
- return null;
102
- if (context.request.method === "OPTIONS") {
103
- return privateJson({ error: "method not allowed" }, { status: 405 });
104
- }
105
- return handleOAuthManagementRequest(context, connectorId);
106
- }
2
+ import { resolveBranding } from "../branding.js";
3
+ import { authorizeUiIdentity, mayManageConnector, validateAuthPermissions, loggableValue, msg, } from "./shared.js";
107
4
  function escapeHtml(value) {
108
5
  return value
109
6
  .replaceAll("&", "&amp;")
@@ -112,7 +9,7 @@ function escapeHtml(value) {
112
9
  .replaceAll('"', "&quot;");
113
10
  }
114
11
  /** `body` is escaped — callback params and error messages are attacker-influenced. */
115
- function html(body, status = 200, branding) {
12
+ function html(body, status = 200, branding, hasUi = false) {
116
13
  const brand = resolveBranding(branding);
117
14
  const title = brand.pageTitle;
118
15
  const owner = brand.ownerName
@@ -173,7 +70,7 @@ function html(body, status = 200, branding) {
173
70
  <h1>Connection status</h1>
174
71
  <div class="copy">
175
72
  <p>${escapeHtml(body)}</p>
176
- <p><a href="/">Return to ${escapeHtml(brand.productName)}</a></p>
73
+ ${hasUi ? `<p><a href="/">Return to ${escapeHtml(brand.productName)}</a></p>` : ""}
177
74
  </div>
178
75
  </main>
179
76
  </body>
@@ -216,10 +113,10 @@ export async function routeOAuthCallback(context) {
216
113
  return null;
217
114
  const error = url.searchParams.get("error");
218
115
  if (error)
219
- return html(`Authorization denied: ${error}`, 400, opts.branding);
116
+ return html(`Authorization denied: ${error}`, 400, opts.branding, Boolean(opts.ui));
220
117
  const code = url.searchParams.get("code");
221
118
  if (!code)
222
- return html("Missing authorization code.", 400, opts.branding);
119
+ return html("Missing authorization code.", 400, opts.branding, Boolean(opts.ui));
223
120
  const id = path.slice("/oauth/callback/".length);
224
121
  const state = url.searchParams.get("state");
225
122
  const callbackTarget = await opts.registry.oauthCallbackView(id, state);
@@ -234,22 +131,25 @@ export async function routeOAuthCallback(context) {
234
131
  ? callbackRegistry.contextFor(id, baseUrl)
235
132
  : opts.registry.contextFor(id, baseUrl);
236
133
  const refused = () => html("Authorization could not be completed. Re-run authorization from " +
237
- "connecta and try again.", 400, opts.branding);
134
+ "connecta and try again.", 400, opts.branding, Boolean(opts.ui));
238
135
  if (!connector || !connector.finishAuth) {
239
136
  await equalizeRefusalCost(connectorContext);
240
137
  return refused();
241
138
  }
242
139
  const expectedPrincipalKey = callbackTarget?.principalKey;
243
- if (expectedPrincipalKey) {
244
- const browserIdentity = await authorizeUiIdentity(context.request, baseUrl, opts.auth, "OAuth callback", context.runtimeContext, opts.identity);
245
- if (browserIdentity.ok &&
246
- browserIdentity.principalKey !== expectedPrincipalKey) {
247
- opts.logger.warn(`[connecta] refused an OAuth callback for connector ` +
248
- `${loggableValue(id)} with 400: the authenticated browser identity ` +
249
- "did not start this personal authorization flow. No authorization " +
250
- "code was exchanged.");
140
+ const browserIdentity = await authorizeUiIdentity(context.request, baseUrl, opts.auth, "OAuth callback", context.runtimeContext, opts.identity);
141
+ if (browserIdentity.ok) {
142
+ try {
143
+ validateAuthPermissions(browserIdentity, opts.registry);
144
+ }
145
+ catch {
251
146
  return refused();
252
147
  }
148
+ if (!mayManageConnector(browserIdentity, connector) || (expectedPrincipalKey && browserIdentity.principalKey !== expectedPrincipalKey))
149
+ return refused();
150
+ }
151
+ else if (browserIdentity.response.status === 403 && opts.auth.some(provider => provider.interactiveOperator)) {
152
+ return refused();
253
153
  }
254
154
  // CSRF / login-fixation guard: this route is intentionally public, so verify
255
155
  // the `state` matches the flow connecta started BEFORE exchanging the code.
@@ -292,15 +192,15 @@ export async function routeOAuthCallback(context) {
292
192
  opts.logger.warn(`[connecta] refused an OAuth callback for connector ` +
293
193
  `${loggableValue(id)} with 500: its principal handoff could not be ` +
294
194
  `consumed (${loggableValue(msg(err))}). No authorization code was exchanged.`);
295
- return html("Authorization could not be completed.", 500, opts.branding);
195
+ return html("Authorization could not be completed.", 500, opts.branding, Boolean(opts.ui));
296
196
  }
297
197
  }
298
198
  try {
299
199
  await connector.finishAuth(code, connectorContext, url.searchParams);
300
200
  await callbackRegistry.invalidateStored(id);
301
- return html(`Connected "${id}". You can close this window.`, 200, opts.branding);
201
+ return html(`Connected "${id}". You can close this window.`, 200, opts.branding, Boolean(opts.ui));
302
202
  }
303
203
  catch (err) {
304
- return html(`Authorization failed: ${msg(err)}`, 500, opts.branding);
204
+ return html(`Authorization failed: ${msg(err)}`, 500, opts.branding, Boolean(opts.ui));
305
205
  }
306
206
  }
@@ -1,12 +1,12 @@
1
1
  import type { Implementation } from "@modelcontextprotocol/server";
2
- import type { AccessTokenManager } from "../access-tokens.js";
2
+ import type { ActivityModule, OperatorSurface } from "../module-contracts.js";
3
3
  import type { ActivityActor, ActivityReadGate, ActivityStore } from "../activity.js";
4
- import type { CredentialVault } from "../credentials.js";
4
+ import type { CredentialVault } from "../credential-contract.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 { AuthenticatedIdentity, ConnectaBranding, Executor, IdentityReference, InboundAuth, InboundAuthRuntimeContext, Logger } from "../types.js";
9
- import type { ConnectaIdentityConfig } from "../index.js";
8
+ import type { AuthenticatedIdentity, ConnectaBranding, Executor, InboundAuth, InboundAuthRuntimeContext, Logger } from "../types.js";
9
+ import type { ConnectorPermission, ConnectaIdentityConfig } from "../index.js";
10
10
  export { msg } from "../errors.js";
11
11
  export interface ServerOptions {
12
12
  registry: Registry;
@@ -29,6 +29,10 @@ export interface ServerOptions {
29
29
  maxEmittedBytes?: number | undefined;
30
30
  /** Block-count budget for connecta.emit per run. Default 32. */
31
31
  maxEmittedBlocks?: number | undefined;
32
+ /** Host calls one execute_code program may make. Default 20. */
33
+ maxHostCalls?: number | undefined;
34
+ /** Deadline per execute_code host call. Default 15_000. */
35
+ hostCallTimeoutMs?: number | undefined;
32
36
  /** Required sandbox backing the execute_code meta-tool. */
33
37
  executor: Executor;
34
38
  /** Sanitized identity of the configured sandbox, when it has one. */
@@ -37,8 +41,9 @@ export interface ServerOptions {
37
41
  requestAdmission: AdmissionController;
38
42
  /** Encrypted connector-credential storage backing the Credentials page. */
39
43
  credentialVault?: CredentialVault | undefined;
40
- /** Hashed deployment access tokens backing MCP admission and the Tokens page. */
41
- accessTokens?: AccessTokenManager | undefined;
44
+ /** Optional browser routes, with no implementation import in core. */
45
+ ui?: OperatorSurface | undefined;
46
+ activityModule?: ActivityModule | undefined;
42
47
  /** Optional browser UI and OAuth result-page labels. */
43
48
  branding?: ConnectaBranding | undefined;
44
49
  }
@@ -74,22 +79,14 @@ export declare function authorize(request: Request, baseUrl: string, auth: Inbou
74
79
  principalKey?: string;
75
80
  connectorIds: "all" | readonly string[];
76
81
  operator: boolean;
82
+ credentialAdministration: ConnectorPermission;
83
+ personalConnection: ConnectorPermission;
77
84
  /** Backward-compatible name used by operator views. */
78
85
  uiAdminEligible?: boolean;
79
86
  } | {
80
87
  ok: false;
81
88
  response: Response;
82
89
  }>;
83
- export declare function authorizeUiAdmin(request: Request, baseUrl: string, auth: InboundAuth[], purpose?: string, runtimeContext?: RuntimeExecutionContext, identityConfig?: ConnectaIdentityConfig): Promise<{
84
- ok: true;
85
- userId: string;
86
- principal?: IdentityReference;
87
- principalKey?: string;
88
- connectorIds: "all" | readonly string[];
89
- } | {
90
- ok: false;
91
- response: Response;
92
- }>;
93
90
  export declare function authorizeUiIdentity(request: Request, baseUrl: string, auth: InboundAuth[], purpose: string, runtimeContext?: RuntimeExecutionContext, identityConfig?: ConnectaIdentityConfig): Promise<Extract<Awaited<ReturnType<typeof authorize>>, {
94
91
  ok: true;
95
92
  }> | {
@@ -97,4 +94,13 @@ export declare function authorizeUiIdentity(request: Request, baseUrl: string, a
97
94
  response: Response;
98
95
  }>;
99
96
  export declare function isSameOrigin(request: Request, baseUrl: string): boolean;
100
- export declare function withSecurityHeaders(response: Response, requestUrl: URL, path: string): Response;
97
+ export declare function withSecurityHeaders(response: Response, requestUrl: URL, _path: string): Response;
98
+ type AuthorizedIdentity = Extract<Awaited<ReturnType<typeof authorize>>, {
99
+ ok: true;
100
+ }>;
101
+ /** Unknown configured ids refuse the complete view, including management rights. */
102
+ export declare function validateAuthPermissions(authz: AuthorizedIdentity, registry: Registry): void;
103
+ export declare function mayManageConnector(authz: AuthorizedIdentity, connector: {
104
+ id: string;
105
+ authScope?: "shared" | "personal";
106
+ }): boolean;