@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
|
@@ -5,6 +5,8 @@ export declare const REVENUECAT_MCP_ENDPOINT = "https://mcp.revenuecat.ai/mcp";
|
|
|
5
5
|
export interface RevenueCatOptions {
|
|
6
6
|
/** Display name; scope defaults are in `documentation/revenuecat.md`. */
|
|
7
7
|
title?: string;
|
|
8
|
+
/** Downstream auth ownership. Defaults to one shared deployment grant. */
|
|
9
|
+
authScope?: "shared" | "personal";
|
|
8
10
|
/**
|
|
9
11
|
* Which project this connector is for and what decisions it answers. With
|
|
10
12
|
* headers auth this is the only place the project a key reaches is named,
|
|
@@ -207,6 +207,7 @@ export function revenuecat(id, options) {
|
|
|
207
207
|
const scoped = auth.type !== "oauth";
|
|
208
208
|
const connector = remoteMcp(id, {
|
|
209
209
|
url: REVENUECAT_MCP_ENDPOINT,
|
|
210
|
+
...(options.authScope ? { authScope: options.authScope } : {}),
|
|
210
211
|
// The scope shape rides the title because browse-time discovery renders
|
|
211
212
|
// the title and the guide summary and nothing else, and reaching one
|
|
212
213
|
// project versus every project the account has is the fact an agent must
|
|
@@ -7,6 +7,8 @@ export declare const STRIPE_MCP_ENDPOINT = "https://mcp.stripe.com/";
|
|
|
7
7
|
interface StripeCommonOptions {
|
|
8
8
|
/** Human-readable display name; defaults to "Stripe" for OAuth. */
|
|
9
9
|
title?: string;
|
|
10
|
+
/** Downstream auth ownership. Defaults to one shared deployment grant. */
|
|
11
|
+
authScope?: "shared" | "personal";
|
|
10
12
|
/** Which business purpose and Stripe context this connector is for. */
|
|
11
13
|
purpose: string;
|
|
12
14
|
/** Connector-specific conventions appended to the maintained provider guide. */
|
package/dist/providers/stripe.js
CHANGED
|
@@ -192,6 +192,7 @@ export function stripe(id, options) {
|
|
|
192
192
|
const copy = mode === undefined ? undefined : MODE_COPY[mode];
|
|
193
193
|
const connector = remoteMcp(id, {
|
|
194
194
|
url: STRIPE_MCP_ENDPOINT,
|
|
195
|
+
...(options.authScope ? { authScope: options.authScope } : {}),
|
|
195
196
|
title: options.title ?? copy?.title ?? "Stripe",
|
|
196
197
|
description: mode === undefined
|
|
197
198
|
? `Stripe payments (live and sandbox accounts) — ${purpose}`
|
package/dist/registry.d.ts
CHANGED
|
@@ -47,6 +47,10 @@ export interface RegistryOptions {
|
|
|
47
47
|
storage: KVStorage;
|
|
48
48
|
logger: Logger;
|
|
49
49
|
credentialVault?: CredentialVault | undefined;
|
|
50
|
+
/** Internal owner partition used by a personal registry. */
|
|
51
|
+
credentialOwner?: string | undefined;
|
|
52
|
+
/** Internal child registries skip deployment-wide construction warnings. */
|
|
53
|
+
constructionChecks?: boolean | undefined;
|
|
50
54
|
toolCacheTtlSeconds?: number | undefined;
|
|
51
55
|
persistToolCatalog?: boolean | undefined;
|
|
52
56
|
toolCatalogStaleSeconds?: number | undefined;
|
|
@@ -105,6 +109,13 @@ export interface RegistryView {
|
|
|
105
109
|
observeOutputShape(connectorId: string, definition: ToolDef, value: unknown): void;
|
|
106
110
|
statusFor(id: string, baseUrl: string, requestScope?: object, callOptions?: ConnectorOperationOptions): Promise<ConnectorStatus>;
|
|
107
111
|
invalidateStored(id: string): Promise<void>;
|
|
112
|
+
/** Bind returned OAuth state to this view's personal storage partition. */
|
|
113
|
+
bindOAuthHandoff(id: string, authorizationUrl: string): Promise<void>;
|
|
114
|
+
}
|
|
115
|
+
export interface RegistryScope {
|
|
116
|
+
connectorIds: "all" | readonly string[];
|
|
117
|
+
subjectKey?: string;
|
|
118
|
+
principalKey?: string;
|
|
108
119
|
}
|
|
109
120
|
/**
|
|
110
121
|
* Holds the connector set, resolves addresses, and caches per-connector tool
|
|
@@ -136,7 +147,20 @@ export declare class Registry implements RegistryView {
|
|
|
136
147
|
private readonly persistToolCatalog;
|
|
137
148
|
/** Result-size guard cap threaded to the meta-tools. */
|
|
138
149
|
readonly maxResultBytes: number;
|
|
150
|
+
private readonly configuredConnectors;
|
|
151
|
+
private readonly personalRegistries;
|
|
139
152
|
constructor(connectors: Connector[], opts: RegistryOptions);
|
|
153
|
+
personalRegistry(principalKey: string): Registry;
|
|
154
|
+
/** Build the only connector view an authenticated request receives. */
|
|
155
|
+
scoped(scope: RegistryScope): RegistryView;
|
|
156
|
+
scopedStorage(subjectKey: string): KVStorage;
|
|
157
|
+
private oauthHandoffKey;
|
|
158
|
+
storeOAuthHandoff(connectorId: string, state: string, principalKey: string): Promise<void>;
|
|
159
|
+
oauthCallbackView(connectorId: string, state: string | null): Promise<{
|
|
160
|
+
registry: RegistryView;
|
|
161
|
+
principalKey?: string;
|
|
162
|
+
} | null>;
|
|
163
|
+
clearOAuthHandoff(connectorId: string, state: string | null): Promise<void>;
|
|
140
164
|
/**
|
|
141
165
|
* Warn once per unusable result cap, at construction time — the same
|
|
142
166
|
* "runs fine but is surely unintended" channel as the insecure-config
|
|
@@ -193,6 +217,7 @@ export declare class Registry implements RegistryView {
|
|
|
193
217
|
* separate from any connector's `conn:<id>:` namespace. Backs get_result.
|
|
194
218
|
*/
|
|
195
219
|
resultsStorage(): KVStorage;
|
|
220
|
+
bindOAuthHandoff(): Promise<void>;
|
|
196
221
|
observedOutputSchema(connectorId: string, definition: ToolDef): ToolDef["outputSchema"] | undefined;
|
|
197
222
|
observeOutputShape(connectorId: string, definition: ToolDef, value: unknown): void;
|
|
198
223
|
/** Resolve "<connectorId>.<toolName>" → connector + tool name. */
|
package/dist/registry.js
CHANGED
|
@@ -73,8 +73,21 @@ function namespaced(storage, prefix) {
|
|
|
73
73
|
get: (k) => storage.get(prefix + k),
|
|
74
74
|
set: (k, v, o) => storage.set(prefix + k, v, o),
|
|
75
75
|
delete: (k) => storage.delete(prefix + k),
|
|
76
|
+
...(storage.list
|
|
77
|
+
? {
|
|
78
|
+
list: async (keyPrefix) => (await storage.list(prefix + keyPrefix)).map((key) => key.slice(prefix.length)),
|
|
79
|
+
}
|
|
80
|
+
: {}),
|
|
76
81
|
};
|
|
77
82
|
}
|
|
83
|
+
const MAX_PERSONAL_REGISTRIES = 1_024;
|
|
84
|
+
const OAUTH_HANDOFF_TTL_SECONDS = 15 * 60;
|
|
85
|
+
async function sha256Hex(value) {
|
|
86
|
+
const bytes = new Uint8Array(await crypto.subtle.digest("SHA-256", encoder.encode(value)));
|
|
87
|
+
return [...bytes]
|
|
88
|
+
.map((byte) => byte.toString(16).padStart(2, "0"))
|
|
89
|
+
.join("");
|
|
90
|
+
}
|
|
78
91
|
/**
|
|
79
92
|
* Holds the connector set, resolves addresses, and caches per-connector tool
|
|
80
93
|
* lists in memory with a TTL. Connector failures are isolated: a broken
|
|
@@ -105,8 +118,11 @@ export class Registry {
|
|
|
105
118
|
persistToolCatalog;
|
|
106
119
|
/** Result-size guard cap threaded to the meta-tools. */
|
|
107
120
|
maxResultBytes;
|
|
121
|
+
configuredConnectors;
|
|
122
|
+
personalRegistries = new Map();
|
|
108
123
|
constructor(connectors, opts) {
|
|
109
124
|
this.opts = opts;
|
|
125
|
+
this.configuredConnectors = [...connectors];
|
|
110
126
|
this.observedOutputSchemas = new ObservedOutputSchemas();
|
|
111
127
|
this.ttlMs =
|
|
112
128
|
(opts.toolCacheTtlSeconds ?? DEFAULT_TTL_SECONDS) * 1000;
|
|
@@ -121,6 +137,11 @@ export class Registry {
|
|
|
121
137
|
if (this.connectors.has(c.id)) {
|
|
122
138
|
throw new Error(`Duplicate connector id "${c.id}"`);
|
|
123
139
|
}
|
|
140
|
+
if (c.authScope !== undefined &&
|
|
141
|
+
c.authScope !== "shared" &&
|
|
142
|
+
c.authScope !== "personal") {
|
|
143
|
+
throw new Error(`Invalid authScope on connector "${c.id}": expected "shared" or "personal"`);
|
|
144
|
+
}
|
|
124
145
|
const configuredGuideSummary = typeof c.usageGuide === "object"
|
|
125
146
|
? normalizeGuideSummary(c.usageGuide.summary ?? "")
|
|
126
147
|
: undefined;
|
|
@@ -136,8 +157,82 @@ export class Registry {
|
|
|
136
157
|
this.callAdmission.set(c.id, new ConnectorCallAdmissionController(c.id, c.callAdmission));
|
|
137
158
|
}
|
|
138
159
|
}
|
|
139
|
-
|
|
140
|
-
|
|
160
|
+
if (opts.constructionChecks !== false) {
|
|
161
|
+
this.checkConventions(opts.logger);
|
|
162
|
+
this.checkResultCaps(opts.logger, opts.maxResultBytes);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
personalRegistry(principalKey) {
|
|
166
|
+
const existing = this.personalRegistries.get(principalKey);
|
|
167
|
+
if (existing) {
|
|
168
|
+
this.personalRegistries.delete(principalKey);
|
|
169
|
+
this.personalRegistries.set(principalKey, existing);
|
|
170
|
+
return existing;
|
|
171
|
+
}
|
|
172
|
+
const registry = new Registry(this.configuredConnectors.filter((connector) => connector.authScope === "personal"), {
|
|
173
|
+
...this.opts,
|
|
174
|
+
storage: namespaced(this.opts.storage, `principal:${principalKey}:`),
|
|
175
|
+
credentialOwner: principalKey,
|
|
176
|
+
constructionChecks: false,
|
|
177
|
+
});
|
|
178
|
+
this.personalRegistries.set(principalKey, registry);
|
|
179
|
+
const oldest = this.personalRegistries.keys().next().value;
|
|
180
|
+
if (this.personalRegistries.size > MAX_PERSONAL_REGISTRIES &&
|
|
181
|
+
typeof oldest === "string") {
|
|
182
|
+
this.personalRegistries.delete(oldest);
|
|
183
|
+
}
|
|
184
|
+
return registry;
|
|
185
|
+
}
|
|
186
|
+
/** Build the only connector view an authenticated request receives. */
|
|
187
|
+
scoped(scope) {
|
|
188
|
+
const requested = scope.connectorIds === "all"
|
|
189
|
+
? new Set(this.connectors.keys())
|
|
190
|
+
: new Set(scope.connectorIds);
|
|
191
|
+
for (const id of requested) {
|
|
192
|
+
if (!this.connectors.has(id)) {
|
|
193
|
+
throw new Error(`Identity access resolver returned unknown connector "${id}"`);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
return new ScopedRegistryView(this, requested, scope);
|
|
197
|
+
}
|
|
198
|
+
scopedStorage(subjectKey) {
|
|
199
|
+
return namespaced(this.opts.storage, `subject:${subjectKey}:`);
|
|
200
|
+
}
|
|
201
|
+
oauthHandoffKey(connectorId, stateHash) {
|
|
202
|
+
return `oauth-handoff:v1:${connectorId}:${stateHash}`;
|
|
203
|
+
}
|
|
204
|
+
async storeOAuthHandoff(connectorId, state, principalKey) {
|
|
205
|
+
const key = this.oauthHandoffKey(connectorId, await sha256Hex(state));
|
|
206
|
+
const existing = await this.opts.storage.get(key);
|
|
207
|
+
if (existing && existing !== principalKey) {
|
|
208
|
+
throw new Error(`Connector "${connectorId}" reused one OAuth state across principals`);
|
|
209
|
+
}
|
|
210
|
+
await this.opts.storage.set(key, principalKey, { ttlSeconds: OAUTH_HANDOFF_TTL_SECONDS });
|
|
211
|
+
}
|
|
212
|
+
async oauthCallbackView(connectorId, state) {
|
|
213
|
+
const connector = this.connectors.get(connectorId);
|
|
214
|
+
if (!connector)
|
|
215
|
+
return null;
|
|
216
|
+
if (connector.authScope !== "personal")
|
|
217
|
+
return { registry: this };
|
|
218
|
+
if (!state)
|
|
219
|
+
return null;
|
|
220
|
+
const principalKey = await this.opts.storage.get(this.oauthHandoffKey(connectorId, await sha256Hex(state)));
|
|
221
|
+
if (!principalKey)
|
|
222
|
+
return null;
|
|
223
|
+
return {
|
|
224
|
+
registry: this.scoped({
|
|
225
|
+
connectorIds: [connectorId],
|
|
226
|
+
subjectKey: principalKey,
|
|
227
|
+
principalKey,
|
|
228
|
+
}),
|
|
229
|
+
principalKey,
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
async clearOAuthHandoff(connectorId, state) {
|
|
233
|
+
if (!state)
|
|
234
|
+
return;
|
|
235
|
+
await this.opts.storage.delete(this.oauthHandoffKey(connectorId, await sha256Hex(state)));
|
|
141
236
|
}
|
|
142
237
|
/**
|
|
143
238
|
* Warn once per unusable result cap, at construction time — the same
|
|
@@ -202,7 +297,7 @@ export class Registry {
|
|
|
202
297
|
if (this.opts.credentialVault && credentialConfig) {
|
|
203
298
|
const vault = this.opts.credentialVault;
|
|
204
299
|
const readValues = async () => {
|
|
205
|
-
const values = await vault.getAll(id);
|
|
300
|
+
const values = await vault.getAll(id, this.opts.credentialOwner);
|
|
206
301
|
const shape = storedCredentialShape(credentialConfig, values);
|
|
207
302
|
if (shape.state === "mismatch") {
|
|
208
303
|
throw new ConnectorCallError("auth_required", shape.message);
|
|
@@ -310,6 +405,9 @@ export class Registry {
|
|
|
310
405
|
resultsStorage() {
|
|
311
406
|
return namespaced(this.opts.storage, "results:");
|
|
312
407
|
}
|
|
408
|
+
async bindOAuthHandoff() {
|
|
409
|
+
// Shared OAuth already resolves in deployment-wide connector storage.
|
|
410
|
+
}
|
|
313
411
|
observedOutputSchema(connectorId, definition) {
|
|
314
412
|
return this.observedOutputSchemas.get(connectorId, definition);
|
|
315
413
|
}
|
|
@@ -858,7 +956,7 @@ export class Registry {
|
|
|
858
956
|
if (!credential || !vault)
|
|
859
957
|
return undefined;
|
|
860
958
|
try {
|
|
861
|
-
const values = await vault.getAll(id);
|
|
959
|
+
const values = await vault.getAll(id, this.opts.credentialOwner);
|
|
862
960
|
const shape = storedCredentialShape(credential, values);
|
|
863
961
|
return shape.state === "mismatch" ? shape.message : undefined;
|
|
864
962
|
}
|
|
@@ -943,3 +1041,101 @@ export class Registry {
|
|
|
943
1041
|
await this.deleteStoredCatalog(id);
|
|
944
1042
|
}
|
|
945
1043
|
}
|
|
1044
|
+
class ScopedRegistryView {
|
|
1045
|
+
root;
|
|
1046
|
+
allowed;
|
|
1047
|
+
scope;
|
|
1048
|
+
maxResultBytes;
|
|
1049
|
+
personal;
|
|
1050
|
+
constructor(root, allowed, scope) {
|
|
1051
|
+
this.root = root;
|
|
1052
|
+
this.allowed = allowed;
|
|
1053
|
+
this.scope = scope;
|
|
1054
|
+
this.maxResultBytes = root.maxResultBytes;
|
|
1055
|
+
this.personal = scope.principalKey
|
|
1056
|
+
? root.personalRegistry(scope.principalKey)
|
|
1057
|
+
: undefined;
|
|
1058
|
+
}
|
|
1059
|
+
registryFor(id) {
|
|
1060
|
+
if (!this.allowed.has(id))
|
|
1061
|
+
return undefined;
|
|
1062
|
+
const connector = this.root.getConnector(id);
|
|
1063
|
+
if (!connector)
|
|
1064
|
+
return undefined;
|
|
1065
|
+
return connector.authScope === "personal" ? this.personal : this.root;
|
|
1066
|
+
}
|
|
1067
|
+
listConnectors() {
|
|
1068
|
+
return this.root.listConnectors().filter((connector) => this.registryFor(connector.id) !== undefined);
|
|
1069
|
+
}
|
|
1070
|
+
getConnector(id) {
|
|
1071
|
+
return this.registryFor(id)?.getConnector(id);
|
|
1072
|
+
}
|
|
1073
|
+
resolveAddress(address) {
|
|
1074
|
+
const parsed = splitAddress(address);
|
|
1075
|
+
if (!parsed)
|
|
1076
|
+
return null;
|
|
1077
|
+
const connector = this.getConnector(parsed.connectorId);
|
|
1078
|
+
return connector ? { connector, toolName: parsed.toolName } : null;
|
|
1079
|
+
}
|
|
1080
|
+
getTools(...args) {
|
|
1081
|
+
const registry = this.registryFor(args[0]);
|
|
1082
|
+
if (!registry) {
|
|
1083
|
+
return Promise.reject(new Error(`Unknown connector "${args[0]}"`));
|
|
1084
|
+
}
|
|
1085
|
+
return registry.getTools(...args);
|
|
1086
|
+
}
|
|
1087
|
+
contextFor(...args) {
|
|
1088
|
+
const registry = this.registryFor(args[0]);
|
|
1089
|
+
if (!registry)
|
|
1090
|
+
throw new Error(`Unknown connector "${args[0]}"`);
|
|
1091
|
+
return registry.contextFor(...args);
|
|
1092
|
+
}
|
|
1093
|
+
admitCall(...args) {
|
|
1094
|
+
if (!this.registryFor(args[0])) {
|
|
1095
|
+
return Promise.reject(new Error(`Unknown connector "${args[0]}"`));
|
|
1096
|
+
}
|
|
1097
|
+
return this.root.admitCall(...args);
|
|
1098
|
+
}
|
|
1099
|
+
resultsStorage() {
|
|
1100
|
+
return this.scope.subjectKey
|
|
1101
|
+
? this.root.scopedStorage(this.scope.subjectKey)
|
|
1102
|
+
: this.root.resultsStorage();
|
|
1103
|
+
}
|
|
1104
|
+
credentialDriftFor(id) {
|
|
1105
|
+
const registry = this.registryFor(id);
|
|
1106
|
+
return registry
|
|
1107
|
+
? registry.credentialDriftFor(id)
|
|
1108
|
+
: Promise.resolve(undefined);
|
|
1109
|
+
}
|
|
1110
|
+
observedOutputSchema(connectorId, definition) {
|
|
1111
|
+
return this.registryFor(connectorId)?.observedOutputSchema(connectorId, definition);
|
|
1112
|
+
}
|
|
1113
|
+
observeOutputShape(connectorId, definition, value) {
|
|
1114
|
+
this.registryFor(connectorId)?.observeOutputShape(connectorId, definition, value);
|
|
1115
|
+
}
|
|
1116
|
+
statusFor(...args) {
|
|
1117
|
+
const registry = this.registryFor(args[0]);
|
|
1118
|
+
return registry
|
|
1119
|
+
? registry.statusFor(...args)
|
|
1120
|
+
: Promise.resolve({ state: "error", message: "Unknown connector" });
|
|
1121
|
+
}
|
|
1122
|
+
invalidateStored(id) {
|
|
1123
|
+
const registry = this.registryFor(id);
|
|
1124
|
+
return registry ? registry.invalidateStored(id) : Promise.resolve();
|
|
1125
|
+
}
|
|
1126
|
+
async bindOAuthHandoff(id, authorizationUrl) {
|
|
1127
|
+
const connector = this.getConnector(id);
|
|
1128
|
+
if (connector?.authScope !== "personal" || !this.scope.principalKey)
|
|
1129
|
+
return;
|
|
1130
|
+
let state = null;
|
|
1131
|
+
try {
|
|
1132
|
+
state = new URL(authorizationUrl).searchParams.get("state");
|
|
1133
|
+
}
|
|
1134
|
+
catch {
|
|
1135
|
+
return;
|
|
1136
|
+
}
|
|
1137
|
+
if (state) {
|
|
1138
|
+
await this.root.storeOAuthHandoff(id, state, this.scope.principalKey);
|
|
1139
|
+
}
|
|
1140
|
+
}
|
|
1141
|
+
}
|
|
@@ -46,7 +46,7 @@ export async function routeAccessTokens(context) {
|
|
|
46
46
|
if (mutating && !isSameOrigin(request, baseUrl)) {
|
|
47
47
|
return privateJson({ error: "same-origin request required" }, { status: 403 });
|
|
48
48
|
}
|
|
49
|
-
const admin = await authorizeUiAdmin(request, baseUrl, opts.auth, "access token management", context.runtimeContext);
|
|
49
|
+
const admin = await authorizeUiAdmin(request, baseUrl, opts.auth, "access token management", context.runtimeContext, opts.identity);
|
|
50
50
|
if (!admin.ok)
|
|
51
51
|
return admin.response;
|
|
52
52
|
const id = match[1];
|
|
@@ -58,7 +58,7 @@ export async function routeAccessTokens(context) {
|
|
|
58
58
|
const input = await readName(request);
|
|
59
59
|
if (!input.ok)
|
|
60
60
|
return input.response;
|
|
61
|
-
return privateJson(await opts.accessTokens.create(input.name, admin.userId), { status: 201 });
|
|
61
|
+
return privateJson(await opts.accessTokens.create(input.name, admin.principal ?? admin.userId), { status: 201 });
|
|
62
62
|
}
|
|
63
63
|
if (id && request.method === "PUT") {
|
|
64
64
|
const input = await readName(request);
|
package/dist/routes/activity.js
CHANGED
|
@@ -134,9 +134,12 @@ export async function routeActivity(context) {
|
|
|
134
134
|
if (request.method !== "GET") {
|
|
135
135
|
return privateJson({ error: "method not allowed" }, { status: 405 });
|
|
136
136
|
}
|
|
137
|
-
const authz = await authorize(request, baseUrl, opts.auth, runtimeContext);
|
|
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) {
|
|
141
|
+
return privateJson({ error: "operator access required" }, { status: 403 });
|
|
142
|
+
}
|
|
140
143
|
if (opts.activityReadGate &&
|
|
141
144
|
!(await opts.activityReadGate(authz.actor))) {
|
|
142
145
|
return privateJson({ error: "forbidden" }, { status: 403 });
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { credentialTestRule, describeCredentialTestMismatch, storedCredentialShape, } from "../credentials.js";
|
|
2
|
-
import {
|
|
2
|
+
import { authorizeUiIdentity, isSameOrigin, msg, privateJson, } from "./shared.js";
|
|
3
3
|
async function readCredentialInput(request, config) {
|
|
4
4
|
if (!request.headers
|
|
5
5
|
.get("content-type")
|
|
@@ -73,13 +73,32 @@ async function handleCredentialRequest(context, connectorId, action) {
|
|
|
73
73
|
if (!isSameOrigin(request, baseUrl)) {
|
|
74
74
|
return privateJson({ error: "same-origin request required" }, { status: 403 });
|
|
75
75
|
}
|
|
76
|
-
const
|
|
77
|
-
if (!
|
|
78
|
-
return
|
|
79
|
-
|
|
76
|
+
const authz = await authorizeUiIdentity(request, baseUrl, opts.auth, "credential management", context.runtimeContext, opts.identity);
|
|
77
|
+
if (!authz.ok)
|
|
78
|
+
return authz.response;
|
|
79
|
+
let registry;
|
|
80
|
+
try {
|
|
81
|
+
registry = opts.registry.scoped({
|
|
82
|
+
connectorIds: authz.connectorIds,
|
|
83
|
+
...(authz.subjectKey ? { subjectKey: authz.subjectKey } : {}),
|
|
84
|
+
...(authz.principalKey ? { principalKey: authz.principalKey } : {}),
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
catch (error) {
|
|
88
|
+
return privateJson({ error: msg(error) }, { status: 403 });
|
|
89
|
+
}
|
|
90
|
+
const connector = registry.getConnector(connectorId);
|
|
80
91
|
if (!connector?.credential) {
|
|
81
92
|
return privateJson({ error: "unknown credential slot" }, { status: 404 });
|
|
82
93
|
}
|
|
94
|
+
const personal = connector.authScope === "personal";
|
|
95
|
+
if (personal && !authz.principalKey) {
|
|
96
|
+
return privateJson({ error: "forbidden" }, { status: 403 });
|
|
97
|
+
}
|
|
98
|
+
const owner = personal ? authz.principalKey : undefined;
|
|
99
|
+
const updatedBy = authz.identity.principal
|
|
100
|
+
? `${authz.identity.principal.namespace}:${authz.identity.principal.id}`
|
|
101
|
+
: authz.actor.id ?? authz.actor.kind;
|
|
83
102
|
if (action === "test") {
|
|
84
103
|
if (request.method !== "POST") {
|
|
85
104
|
return privateJson({ error: "method not allowed" }, { status: 405 });
|
|
@@ -97,7 +116,7 @@ async function handleCredentialRequest(context, connectorId, action) {
|
|
|
97
116
|
}, { status: 400 });
|
|
98
117
|
}
|
|
99
118
|
try {
|
|
100
|
-
const values = await opts.credentialVault.getAll(connectorId);
|
|
119
|
+
const values = await opts.credentialVault.getAll(connectorId, owner);
|
|
101
120
|
const shape = storedCredentialShape(connector.credential, values);
|
|
102
121
|
if (shape.state === "missing") {
|
|
103
122
|
return privateJson({
|
|
@@ -110,7 +129,7 @@ async function handleCredentialRequest(context, connectorId, action) {
|
|
|
110
129
|
return privateJson({ error: shape.message }, { status: 409 });
|
|
111
130
|
}
|
|
112
131
|
const storedValues = values;
|
|
113
|
-
const ctx =
|
|
132
|
+
const ctx = registry.contextFor(connectorId, baseUrl);
|
|
114
133
|
const result = rule.mode === "multiple"
|
|
115
134
|
? await connector.testCredentials(storedValues, ctx)
|
|
116
135
|
: await connector.testCredential(
|
|
@@ -131,9 +150,9 @@ async function handleCredentialRequest(context, connectorId, action) {
|
|
|
131
150
|
return input.response;
|
|
132
151
|
try {
|
|
133
152
|
const metadata = input.input.kind === "single"
|
|
134
|
-
? await opts.credentialVault.set(connectorId, input.input.value,
|
|
135
|
-
: await opts.credentialVault.setAll(connectorId, input.input.values,
|
|
136
|
-
await
|
|
153
|
+
? await opts.credentialVault.set(connectorId, input.input.value, updatedBy, owner)
|
|
154
|
+
: await opts.credentialVault.setAll(connectorId, input.input.values, updatedBy, owner);
|
|
155
|
+
await registry.invalidateStored(connectorId);
|
|
137
156
|
return privateJson({ credential: metadata });
|
|
138
157
|
}
|
|
139
158
|
catch (err) {
|
|
@@ -141,8 +160,8 @@ async function handleCredentialRequest(context, connectorId, action) {
|
|
|
141
160
|
}
|
|
142
161
|
}
|
|
143
162
|
if (request.method === "DELETE") {
|
|
144
|
-
await opts.credentialVault.delete(connectorId);
|
|
145
|
-
await
|
|
163
|
+
await opts.credentialVault.delete(connectorId, owner);
|
|
164
|
+
await registry.invalidateStored(connectorId);
|
|
146
165
|
return new Response(null, {
|
|
147
166
|
status: 204,
|
|
148
167
|
headers: {
|
package/dist/routes/mcp.js
CHANGED
|
@@ -4,6 +4,7 @@ import { registerExecuteTool } from "../execute.js";
|
|
|
4
4
|
import { ExecutorAdmissionError, } from "../executor-admission.js";
|
|
5
5
|
import { registerMetaTools } from "../meta-tools.js";
|
|
6
6
|
import { instructionsFor } from "../skills.js";
|
|
7
|
+
import { msg } from "../errors.js";
|
|
7
8
|
import { authorize, } from "./shared.js";
|
|
8
9
|
export const MCP_CORS_HEADERS = {
|
|
9
10
|
"Access-Control-Allow-Origin": "*",
|
|
@@ -329,14 +330,28 @@ export function createMcpRoute(opts) {
|
|
|
329
330
|
throw error;
|
|
330
331
|
}
|
|
331
332
|
try {
|
|
332
|
-
const authz = await authorize(request, baseUrl, opts.auth, runtimeContext);
|
|
333
|
+
const authz = await authorize(request, baseUrl, opts.auth, runtimeContext, opts.identity);
|
|
333
334
|
if (!authz.ok) {
|
|
334
335
|
return releaseAdmissionWithResponse(withMcpCors(authz.response), admission, request.signal);
|
|
335
336
|
}
|
|
337
|
+
let scopedRegistry;
|
|
338
|
+
try {
|
|
339
|
+
scopedRegistry = opts.registry.scoped({
|
|
340
|
+
connectorIds: authz.connectorIds,
|
|
341
|
+
...(authz.subjectKey ? { subjectKey: authz.subjectKey } : {}),
|
|
342
|
+
...(authz.principalKey ? { principalKey: authz.principalKey } : {}),
|
|
343
|
+
});
|
|
344
|
+
}
|
|
345
|
+
catch (error) {
|
|
346
|
+
return releaseAdmissionWithResponse(withMcpCors(new Response(JSON.stringify({ error: msg(error) }), {
|
|
347
|
+
status: 403,
|
|
348
|
+
headers: { "Content-Type": "application/json" },
|
|
349
|
+
})), admission, request.signal);
|
|
350
|
+
}
|
|
336
351
|
if (new URL(request.url).searchParams.has("toolkit")) {
|
|
337
352
|
return releaseAdmissionWithResponse(withMcpCors(toolkitRetired(opts.logger)), admission, request.signal);
|
|
338
353
|
}
|
|
339
|
-
return releaseAdmissionWithResponse(withMcpCors(await serveMcp(request, opts, baseUrl, authz.actor,
|
|
354
|
+
return releaseAdmissionWithResponse(withMcpCors(await serveMcp(request, opts, baseUrl, authz.actor, scopedRegistry, runtimeContext)), admission, request.signal);
|
|
340
355
|
}
|
|
341
356
|
catch (error) {
|
|
342
357
|
admission.release();
|
package/dist/routes/oauth.js
CHANGED
|
@@ -1,24 +1,38 @@
|
|
|
1
1
|
import { oauthValueStorageKey } from "../auth/downstream-oauth.js";
|
|
2
2
|
import { closeConnectorScope } from "../connector-scope.js";
|
|
3
3
|
import { isSafeHttpUrl, resolveBranding } from "../ui.js";
|
|
4
|
-
import {
|
|
4
|
+
import { authorizeUiIdentity, isSameOrigin, loggableValue, msg, privateJson, } from "./shared.js";
|
|
5
5
|
async function handleOAuthManagementRequest(context, connectorId) {
|
|
6
6
|
const { request, baseUrl, opts, defer } = context;
|
|
7
7
|
if (!isSameOrigin(request, baseUrl)) {
|
|
8
8
|
return privateJson({ error: "same-origin request required" }, { status: 403 });
|
|
9
9
|
}
|
|
10
|
-
const
|
|
11
|
-
if (!
|
|
12
|
-
return
|
|
13
|
-
|
|
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);
|
|
14
25
|
if (!connector?.disconnectAuth || !connector.startAuth) {
|
|
15
26
|
return privateJson({ error: "unknown OAuth connector" }, { status: 404 });
|
|
16
27
|
}
|
|
28
|
+
if (connector.authScope === "personal" && !authz.principalKey) {
|
|
29
|
+
return privateJson({ error: "forbidden" }, { status: 403 });
|
|
30
|
+
}
|
|
17
31
|
if (request.method !== "DELETE" && request.method !== "POST") {
|
|
18
32
|
return privateJson({ error: "method not allowed" }, { status: 405 });
|
|
19
33
|
}
|
|
20
34
|
const requestScope = {};
|
|
21
|
-
const ctx =
|
|
35
|
+
const ctx = registry.contextFor(connectorId, baseUrl, requestScope);
|
|
22
36
|
try {
|
|
23
37
|
let result;
|
|
24
38
|
let operationError;
|
|
@@ -28,6 +42,9 @@ async function handleOAuthManagementRequest(context, connectorId) {
|
|
|
28
42
|
}
|
|
29
43
|
else {
|
|
30
44
|
result = await connector.startAuth(ctx, { force: true });
|
|
45
|
+
if (result.authorizationUrl) {
|
|
46
|
+
await registry.bindOAuthHandoff(connectorId, result.authorizationUrl);
|
|
47
|
+
}
|
|
31
48
|
}
|
|
32
49
|
}
|
|
33
50
|
catch (error) {
|
|
@@ -36,7 +53,7 @@ async function handleOAuthManagementRequest(context, connectorId) {
|
|
|
36
53
|
// The old grant and its cached catalog are invalid after either operation,
|
|
37
54
|
// including a partially failed physical cleanup whose epoch fence succeeded.
|
|
38
55
|
try {
|
|
39
|
-
await
|
|
56
|
+
await registry.invalidateStored(connectorId);
|
|
40
57
|
}
|
|
41
58
|
catch (error) {
|
|
42
59
|
operationError ??= error;
|
|
@@ -204,19 +221,36 @@ export async function routeOAuthCallback(context) {
|
|
|
204
221
|
if (!code)
|
|
205
222
|
return html("Missing authorization code.", 400, opts.branding);
|
|
206
223
|
const id = path.slice("/oauth/callback/".length);
|
|
207
|
-
const
|
|
224
|
+
const state = url.searchParams.get("state");
|
|
225
|
+
const callbackTarget = await opts.registry.oauthCallbackView(id, state);
|
|
226
|
+
const callbackRegistry = callbackTarget?.registry;
|
|
227
|
+
const connector = callbackRegistry?.getConnector(id);
|
|
208
228
|
// Safe to build before we know the id names anything: `contextFor` is a pure
|
|
209
229
|
// constructor — a namespaced storage view over `conn:<id>:` and, only for a
|
|
210
230
|
// connector that declares one, a lazy credential accessor. It neither throws
|
|
211
231
|
// nor touches storage for an unknown id, which is what lets the refusals
|
|
212
232
|
// below borrow it to equalize their cost.
|
|
213
|
-
const connectorContext =
|
|
233
|
+
const connectorContext = callbackRegistry
|
|
234
|
+
? callbackRegistry.contextFor(id, baseUrl)
|
|
235
|
+
: opts.registry.contextFor(id, baseUrl);
|
|
214
236
|
const refused = () => html("Authorization could not be completed. Re-run authorization from " +
|
|
215
237
|
"connecta and try again.", 400, opts.branding);
|
|
216
238
|
if (!connector || !connector.finishAuth) {
|
|
217
239
|
await equalizeRefusalCost(connectorContext);
|
|
218
240
|
return refused();
|
|
219
241
|
}
|
|
242
|
+
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.");
|
|
251
|
+
return refused();
|
|
252
|
+
}
|
|
253
|
+
}
|
|
220
254
|
// CSRF / login-fixation guard: this route is intentionally public, so verify
|
|
221
255
|
// the `state` matches the flow connecta started BEFORE exchanging the code.
|
|
222
256
|
if (!connector.verifyState) {
|
|
@@ -228,7 +262,6 @@ export async function routeOAuthCallback(context) {
|
|
|
228
262
|
"trying again.");
|
|
229
263
|
return refused();
|
|
230
264
|
}
|
|
231
|
-
const state = url.searchParams.get("state");
|
|
232
265
|
let stateMatches;
|
|
233
266
|
try {
|
|
234
267
|
stateMatches = await connector.verifyState(state, connectorContext);
|
|
@@ -251,9 +284,20 @@ export async function routeOAuthCallback(context) {
|
|
|
251
284
|
"connecta and try again.");
|
|
252
285
|
return refused();
|
|
253
286
|
}
|
|
287
|
+
if (connector.authScope === "personal") {
|
|
288
|
+
try {
|
|
289
|
+
await opts.registry.clearOAuthHandoff(id, state);
|
|
290
|
+
}
|
|
291
|
+
catch (err) {
|
|
292
|
+
opts.logger.warn(`[connecta] refused an OAuth callback for connector ` +
|
|
293
|
+
`${loggableValue(id)} with 500: its principal handoff could not be ` +
|
|
294
|
+
`consumed (${loggableValue(msg(err))}). No authorization code was exchanged.`);
|
|
295
|
+
return html("Authorization could not be completed.", 500, opts.branding);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
254
298
|
try {
|
|
255
299
|
await connector.finishAuth(code, connectorContext, url.searchParams);
|
|
256
|
-
await
|
|
300
|
+
await callbackRegistry.invalidateStored(id);
|
|
257
301
|
return html(`Connected "${id}". You can close this window.`, 200, opts.branding);
|
|
258
302
|
}
|
|
259
303
|
catch (err) {
|