@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.
Files changed (66) hide show
  1. package/CHANGELOG.md +83 -0
  2. package/README.md +7 -0
  3. package/dist/access-tokens.d.ts +2 -2
  4. package/dist/access-tokens.js +14 -2
  5. package/dist/auth/downstream-oauth.d.ts +65 -2
  6. package/dist/auth/downstream-oauth.js +408 -20
  7. package/dist/connectors/api.d.ts +2 -0
  8. package/dist/connectors/api.js +1 -0
  9. package/dist/connectors/remote-mcp.d.ts +2 -0
  10. package/dist/connectors/remote-mcp.js +14 -4
  11. package/dist/credentials.d.ts +6 -6
  12. package/dist/credentials.js +25 -21
  13. package/dist/executors/quickjs.js +4 -0
  14. package/dist/identity.d.ts +4 -0
  15. package/dist/identity.js +17 -0
  16. package/dist/index.d.ts +16 -2
  17. package/dist/index.js +6 -1
  18. package/dist/meta-tools.js +7 -2
  19. package/dist/operator-ui/generated.js +1 -1
  20. package/dist/operator-ui/model.d.ts +4 -2
  21. package/dist/operator-ui/view.js +1 -1
  22. package/dist/providers/cloudflare.d.ts +2 -0
  23. package/dist/providers/cloudflare.js +1 -0
  24. package/dist/providers/linear.d.ts +2 -0
  25. package/dist/providers/linear.js +1 -0
  26. package/dist/providers/mixpanel.d.ts +2 -0
  27. package/dist/providers/mixpanel.js +1 -0
  28. package/dist/providers/notion.d.ts +2 -0
  29. package/dist/providers/notion.js +1 -0
  30. package/dist/providers/revenuecat.d.ts +2 -0
  31. package/dist/providers/revenuecat.js +1 -0
  32. package/dist/providers/stripe.d.ts +2 -0
  33. package/dist/providers/stripe.js +1 -0
  34. package/dist/registry.d.ts +25 -0
  35. package/dist/registry.js +200 -4
  36. package/dist/routes/access-tokens.js +2 -2
  37. package/dist/routes/activity.js +4 -1
  38. package/dist/routes/credentials.js +31 -12
  39. package/dist/routes/mcp.js +17 -2
  40. package/dist/routes/oauth.js +55 -11
  41. package/dist/routes/shared.d.ts +20 -4
  42. package/dist/routes/shared.js +92 -24
  43. package/dist/routes/ui.js +32 -13
  44. package/dist/types.d.ts +28 -2
  45. package/dist/ui.d.ts +3 -3
  46. package/dist/ui.js +18 -5
  47. package/dist/version.d.ts +1 -1
  48. package/dist/version.js +1 -1
  49. package/documentation/architecture.md +31 -8
  50. package/documentation/auth.md +90 -10
  51. package/documentation/code-mode.md +4 -4
  52. package/documentation/connectors.md +13 -0
  53. package/documentation/meta-tools.md +4 -3
  54. package/documentation/operations.md +5 -3
  55. package/documentation/operator-ui.md +13 -4
  56. package/documentation/request-admission.md +2 -1
  57. package/documentation/storage-and-credentials.md +77 -4
  58. package/documentation/upgrading.md +38 -7
  59. package/ethos.md +8 -8
  60. package/examples/worker/AGENTS.md +44 -0
  61. package/examples/worker/README.md +63 -14
  62. package/examples/worker/src/index.ts +26 -22
  63. package/package.json +1 -1
  64. package/templates/node/README.md +7 -0
  65. package/templates/node/package.json +1 -1
  66. package/templates/node/src/index.ts +13 -4
@@ -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 `conn:${connectorId}:credential:v1`;
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(`connecta:credential:${connectorId}:v1`);
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>;
@@ -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("Operator-managed credentials are unavailable because " +
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,
@@ -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. Credential " +
483
- "mutation requires a Clerk-authenticated operator.",
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.`);