@zackbart/connecta 0.21.1 → 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 +83 -0
- package/README.md +7 -0
- package/dist/access-tokens.d.ts +2 -2
- package/dist/access-tokens.js +14 -2
- package/dist/auth/downstream-oauth.d.ts +65 -2
- package/dist/auth/downstream-oauth.js +408 -20
- 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 +14 -4
- package/dist/credentials.d.ts +6 -6
- package/dist/credentials.js +25 -21
- package/dist/executors/quickjs.js +4 -0
- 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 +31 -8
- package/documentation/auth.md +90 -10
- package/documentation/code-mode.md +4 -4
- package/documentation/connectors.md +13 -0
- package/documentation/meta-tools.md +4 -3
- package/documentation/operations.md +5 -3
- package/documentation/operator-ui.md +13 -4
- package/documentation/request-admission.md +2 -1
- package/documentation/storage-and-credentials.md +77 -4
- package/documentation/upgrading.md +38 -7
- package/ethos.md +8 -8
- package/examples/worker/AGENTS.md +44 -0
- package/examples/worker/README.md +63 -14
- package/examples/worker/src/index.ts +26 -22
- package/package.json +1 -1
- package/templates/node/README.md +7 -0
- package/templates/node/package.json +1 -1
- package/templates/node/src/index.ts +13 -4
package/dist/credentials.js
CHANGED
|
@@ -110,8 +110,10 @@ export function describeCredentialTestMismatch(mismatch) {
|
|
|
110
110
|
"`testCredential(value, ctx)` can test, but implements " +
|
|
111
111
|
"`testCredentials`";
|
|
112
112
|
}
|
|
113
|
-
function storageKey(connectorId) {
|
|
114
|
-
return
|
|
113
|
+
function storageKey(connectorId, owner) {
|
|
114
|
+
return owner
|
|
115
|
+
? `principal:${owner}:conn:${connectorId}:credential:v1`
|
|
116
|
+
: `conn:${connectorId}:credential:v1`;
|
|
115
117
|
}
|
|
116
118
|
function bytesToBase64(bytes) {
|
|
117
119
|
let binary = "";
|
|
@@ -205,11 +207,13 @@ export class CredentialVault {
|
|
|
205
207
|
}
|
|
206
208
|
this.key = crypto.subtle.importKey("raw", raw, { name: "AES-GCM" }, false, ["encrypt", "decrypt"]);
|
|
207
209
|
}
|
|
208
|
-
additionalData(connectorId) {
|
|
209
|
-
return encoder.encode(
|
|
210
|
+
additionalData(connectorId, owner) {
|
|
211
|
+
return encoder.encode(owner
|
|
212
|
+
? `connecta:credential:principal:${owner}:${connectorId}:v1`
|
|
213
|
+
: `connecta:credential:${connectorId}:v1`);
|
|
210
214
|
}
|
|
211
|
-
async read(connectorId) {
|
|
212
|
-
const raw = await this.storage.get(storageKey(connectorId));
|
|
215
|
+
async read(connectorId, owner) {
|
|
216
|
+
const raw = await this.storage.get(storageKey(connectorId, owner));
|
|
213
217
|
if (!raw)
|
|
214
218
|
return null;
|
|
215
219
|
const envelope = parseEnvelope(raw);
|
|
@@ -217,7 +221,7 @@ export class CredentialVault {
|
|
|
217
221
|
const plaintext = await crypto.subtle.decrypt({
|
|
218
222
|
name: "AES-GCM",
|
|
219
223
|
iv: base64ToBytes(envelope.iv),
|
|
220
|
-
additionalData: this.additionalData(connectorId),
|
|
224
|
+
additionalData: this.additionalData(connectorId, owner),
|
|
221
225
|
}, await this.key, base64ToBytes(envelope.ciphertext));
|
|
222
226
|
return parsePlaintext(decoder.decode(plaintext));
|
|
223
227
|
}
|
|
@@ -225,15 +229,15 @@ export class CredentialVault {
|
|
|
225
229
|
throw new Error("Stored credential could not be decrypted");
|
|
226
230
|
}
|
|
227
231
|
}
|
|
228
|
-
async get(connectorId, field = "value") {
|
|
229
|
-
return (await this.read(connectorId))?.values[field] ?? null;
|
|
232
|
+
async get(connectorId, field = "value", owner) {
|
|
233
|
+
return (await this.read(connectorId, owner))?.values[field] ?? null;
|
|
230
234
|
}
|
|
231
|
-
async getAll(connectorId) {
|
|
232
|
-
const credential = await this.read(connectorId);
|
|
235
|
+
async getAll(connectorId, owner) {
|
|
236
|
+
const credential = await this.read(connectorId, owner);
|
|
233
237
|
return credential ? { ...credential.values } : null;
|
|
234
238
|
}
|
|
235
|
-
async metadata(connectorId) {
|
|
236
|
-
const credential = await this.read(connectorId);
|
|
239
|
+
async metadata(connectorId, owner) {
|
|
240
|
+
const credential = await this.read(connectorId, owner);
|
|
237
241
|
if (!credential)
|
|
238
242
|
return null;
|
|
239
243
|
const fields = Object.fromEntries(Object.entries(credential.values).map(([field, value]) => [
|
|
@@ -254,13 +258,13 @@ export class CredentialVault {
|
|
|
254
258
|
fields,
|
|
255
259
|
};
|
|
256
260
|
}
|
|
257
|
-
async set(connectorId, value, updatedBy) {
|
|
261
|
+
async set(connectorId, value, updatedBy, owner) {
|
|
258
262
|
// `await` (not a bare promise return) so a validation throw inside setAll
|
|
259
263
|
// never sits handler-less for the thenable-adoption microtask — workerd
|
|
260
264
|
// reports that gap as an unhandled rejection.
|
|
261
|
-
return await this.setAll(connectorId, { value }, updatedBy);
|
|
265
|
+
return await this.setAll(connectorId, { value }, updatedBy, owner);
|
|
262
266
|
}
|
|
263
|
-
async setAll(connectorId, values, updatedBy) {
|
|
267
|
+
async setAll(connectorId, values, updatedBy, owner) {
|
|
264
268
|
const normalized = validateValues(values);
|
|
265
269
|
const plaintext = {
|
|
266
270
|
values: normalized,
|
|
@@ -271,7 +275,7 @@ export class CredentialVault {
|
|
|
271
275
|
const ciphertext = await crypto.subtle.encrypt({
|
|
272
276
|
name: "AES-GCM",
|
|
273
277
|
iv,
|
|
274
|
-
additionalData: this.additionalData(connectorId),
|
|
278
|
+
additionalData: this.additionalData(connectorId, owner),
|
|
275
279
|
}, await this.key, encoder.encode(JSON.stringify(plaintext)));
|
|
276
280
|
const envelope = {
|
|
277
281
|
version: 1,
|
|
@@ -279,10 +283,10 @@ export class CredentialVault {
|
|
|
279
283
|
iv: bytesToBase64(iv),
|
|
280
284
|
ciphertext: bytesToBase64(new Uint8Array(ciphertext)),
|
|
281
285
|
};
|
|
282
|
-
await this.storage.set(storageKey(connectorId), JSON.stringify(envelope));
|
|
283
|
-
return (await this.metadata(connectorId));
|
|
286
|
+
await this.storage.set(storageKey(connectorId, owner), JSON.stringify(envelope));
|
|
287
|
+
return (await this.metadata(connectorId, owner));
|
|
284
288
|
}
|
|
285
|
-
async delete(connectorId) {
|
|
286
|
-
await this.storage.delete(storageKey(connectorId));
|
|
289
|
+
async delete(connectorId, owner) {
|
|
290
|
+
await this.storage.delete(storageKey(connectorId, owner));
|
|
287
291
|
}
|
|
288
292
|
}
|
|
@@ -278,6 +278,10 @@ class QuickJsChildPool {
|
|
|
278
278
|
"@zackbart/connecta/quickjs) when bundling the server.");
|
|
279
279
|
}
|
|
280
280
|
const child = fork(childPath, [], {
|
|
281
|
+
// The child needs only its entry path, exec arguments, and IPC channel.
|
|
282
|
+
// Do not copy deployment credentials or Node startup configuration into
|
|
283
|
+
// the process that contains the guest runtime.
|
|
284
|
+
env: {},
|
|
281
285
|
execArgv: sourceMode ? ["--import", "tsx"] : [],
|
|
282
286
|
stdio: ["ignore", "ignore", "pipe", "ipc"],
|
|
283
287
|
});
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { IdentityReference } from "./types.js";
|
|
2
|
+
export declare function validIdentityReference(value: IdentityReference | undefined): value is IdentityReference;
|
|
3
|
+
/** Deterministic pseudonymous partition; raw emails and ids do not enter keys. */
|
|
4
|
+
export declare function identityStorageKey(identity: IdentityReference): Promise<string>;
|
package/dist/identity.js
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
const IDENTITY_PART_RE = /^[\x21-\x7e]{1,256}$/;
|
|
2
|
+
const encoder = new TextEncoder();
|
|
3
|
+
export function validIdentityReference(value) {
|
|
4
|
+
return Boolean(value &&
|
|
5
|
+
IDENTITY_PART_RE.test(value.namespace) &&
|
|
6
|
+
IDENTITY_PART_RE.test(value.id));
|
|
7
|
+
}
|
|
8
|
+
function bytesToHex(bytes) {
|
|
9
|
+
return [...bytes]
|
|
10
|
+
.map((byte) => byte.toString(16).padStart(2, "0"))
|
|
11
|
+
.join("");
|
|
12
|
+
}
|
|
13
|
+
/** Deterministic pseudonymous partition; raw emails and ids do not enter keys. */
|
|
14
|
+
export async function identityStorageKey(identity) {
|
|
15
|
+
const digest = await crypto.subtle.digest("SHA-256", encoder.encode(`${identity.namespace}\n${identity.id}`));
|
|
16
|
+
return bytesToHex(new Uint8Array(digest));
|
|
17
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Registry } from "./registry.js";
|
|
2
2
|
import type { ActivityReadGate, ActivityStore } from "./activity.js";
|
|
3
|
-
import type { Connector, ConnectaBranding, Executor, InboundAuth, KVStorage, Logger } from "./types.js";
|
|
3
|
+
import type { AuthenticatedIdentity, Connector, ConnectaBranding, Executor, IdentityReference, InboundAuth, KVStorage, Logger } from "./types.js";
|
|
4
4
|
/** Payload-free activity storage and operator-read policy. */
|
|
5
5
|
export interface ConnectaActivityConfig {
|
|
6
6
|
/**
|
|
@@ -115,10 +115,24 @@ export interface ConnectaAdmissionConfig {
|
|
|
115
115
|
*/
|
|
116
116
|
code?: AdmissionPoolConfig;
|
|
117
117
|
}
|
|
118
|
+
/** Config-owned identity rules for one deployment and tenant. */
|
|
119
|
+
export interface ConnectaIdentityConfig {
|
|
120
|
+
/** Connector ids this admitted identity may discover and call. */
|
|
121
|
+
connectorAccess?(identity: Readonly<AuthenticatedIdentity>): "all" | readonly string[] | Promise<"all" | readonly string[]>;
|
|
122
|
+
/**
|
|
123
|
+
* Whether a human may manage deployment access tokens and global activity.
|
|
124
|
+
* Connector access already permits that human to manage the visible
|
|
125
|
+
* connector's shared or personal auth. Omit to preserve the existing
|
|
126
|
+
* all-interactive-humans operator rule.
|
|
127
|
+
*/
|
|
128
|
+
operatorAccess?(principal: Readonly<IdentityReference>): boolean | Promise<boolean>;
|
|
129
|
+
}
|
|
118
130
|
export interface ConnectaConfig {
|
|
119
131
|
connectors: Connector[];
|
|
120
132
|
/** Inbound auth adapters. Includes bearerToken(...); omit for open (dev). */
|
|
121
133
|
auth?: InboundAuth | InboundAuth[];
|
|
134
|
+
/** Identity-derived connector visibility and deployment operator membership. */
|
|
135
|
+
identity?: ConnectaIdentityConfig;
|
|
122
136
|
/** KVStorage impl. Defaults to memoryStorage(). */
|
|
123
137
|
storage?: KVStorage;
|
|
124
138
|
/**
|
|
@@ -192,6 +206,6 @@ export { CONNECTA_VERSION } from "./version.js";
|
|
|
192
206
|
export type { Registry } from "./registry.js";
|
|
193
207
|
export type { RemoteMcpOptions, RemoteMcpAuth, RemoteMcpRedirectPolicy, } from "./connectors/remote-mcp.js";
|
|
194
208
|
export type { ApiOptions, ApiTool } from "./connectors/api.js";
|
|
195
|
-
export type { CatalogDriftCounts, CatalogDriftReport, Connector, ConnectorCallAdmissionInput, ConnectorCallAdmissionPolicy, ConnectorCallAdmissionRule, ConnectorRollingWindowBudget, ConnectaBranding, ConnectorCredentialAccess, ConnectorCredentialConfig, ConnectorCredentialFieldConfig, ConnectorCredentialValues, ConnectorContext, ConnectorUsageGuide, ConnectorStatus, CredentialTestResult, AdmittingExecutor, AdmissionSnapshot, ExecuteResult, Executor, ExecutorLease, ExecutorProvider, InboundAuth, InboundAuthRuntimeContext, UiAuthConfig, AuthResult, JsonSchema, KVStorage, Logger, ToolDef, ToolAnnotations, } from "./types.js";
|
|
209
|
+
export type { CatalogDriftCounts, CatalogDriftReport, Connector, ConnectorCallAdmissionInput, ConnectorCallAdmissionPolicy, ConnectorCallAdmissionRule, ConnectorRollingWindowBudget, ConnectaBranding, ConnectorCredentialAccess, ConnectorCredentialConfig, ConnectorCredentialFieldConfig, ConnectorCredentialValues, ConnectorContext, ConnectorUsageGuide, ConnectorStatus, CredentialTestResult, AdmittingExecutor, AdmissionSnapshot, ExecuteResult, Executor, ExecutorLease, ExecutorProvider, InboundAuth, InboundAuthRuntimeContext, UiAuthConfig, AuthResult, AuthenticatedIdentity, IdentityReference, JsonSchema, KVStorage, Logger, ToolDef, ToolAnnotations, } from "./types.js";
|
|
196
210
|
export type { ActivityActor, ActivityCallSource, ActivityOutcome, ActivityPage, ActivityReadActor, ActivityReadEvent, ActivityReader, ActivityReadGate, ActivityReadPage, ActivitySink, ActivityStore, AgentFriction, CatalogDriftActivityEvent, ToolCallActivityEvent, } from "./activity.js";
|
|
197
211
|
export { InvalidActivityCursorError } from "./activity.js";
|
package/dist/index.js
CHANGED
|
@@ -51,6 +51,10 @@ const admissionPoolSchema = {
|
|
|
51
51
|
const CONFIG_SCHEMA = {
|
|
52
52
|
connectors: null,
|
|
53
53
|
auth: null,
|
|
54
|
+
identity: {
|
|
55
|
+
connectorAccess: null,
|
|
56
|
+
operatorAccess: null,
|
|
57
|
+
},
|
|
54
58
|
storage: null,
|
|
55
59
|
publicUrl: null,
|
|
56
60
|
activity: {
|
|
@@ -250,7 +254,7 @@ export function createConnecta(config) {
|
|
|
250
254
|
const credentialConnectors = config.connectors.filter((c) => c.credential);
|
|
251
255
|
const encryptionKey = config.credentials?.encryptionKey;
|
|
252
256
|
if (credentialConnectors.length > 0 && !encryptionKey) {
|
|
253
|
-
logger.warn("
|
|
257
|
+
logger.warn("Human-managed credentials are unavailable because " +
|
|
254
258
|
"credentials.encryptionKey is not configured for connectors: " +
|
|
255
259
|
credentialConnectors.map((c) => c.id).join(", "));
|
|
256
260
|
}
|
|
@@ -310,6 +314,7 @@ export function createConnecta(config) {
|
|
|
310
314
|
const handler = createFetchHandler({
|
|
311
315
|
registry,
|
|
312
316
|
auth: inboundAuth,
|
|
317
|
+
identity: config.identity,
|
|
313
318
|
publicUrl: config.publicUrl,
|
|
314
319
|
serverInfo,
|
|
315
320
|
logger,
|
package/dist/meta-tools.js
CHANGED
|
@@ -479,13 +479,18 @@ export function createMetaTools(registry, baseUrl, opts = {}) {
|
|
|
479
479
|
},
|
|
480
480
|
operatorUrl: new URL("/credentials", baseUrl).toString(),
|
|
481
481
|
instructions: "Have the operator open operatorUrl, set and test the credential, " +
|
|
482
|
-
"then retry the original call. No redeploy is needed.
|
|
483
|
-
|
|
482
|
+
"then retry the original call. No redeploy is needed. " +
|
|
483
|
+
(connector.authScope === "personal"
|
|
484
|
+
? "Credential mutation requires the signed-in principal who owns this connection."
|
|
485
|
+
: "Shared credential mutation requires a signed-in human with access to this connector."),
|
|
484
486
|
});
|
|
485
487
|
}
|
|
486
488
|
const ctx = registry.contextFor(connector.id, baseUrl, requestScope);
|
|
487
489
|
try {
|
|
488
490
|
const status = await connector.startAuth(ctx, args.force !== undefined ? { force: args.force } : {});
|
|
491
|
+
if (status.authorizationUrl) {
|
|
492
|
+
await registry.bindOAuthHandoff(connector.id, status.authorizationUrl);
|
|
493
|
+
}
|
|
489
494
|
if (status.state === "auth_required" && !status.authorizationUrl) {
|
|
490
495
|
// auth_required with nothing to open is a dead end for the operator.
|
|
491
496
|
return errorResult(`Connector "${connector.id}": authorization required but no URL is available — retry authorize_connector.`);
|