@zackbart/connecta 0.24.4 → 0.26.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 (155) hide show
  1. package/AGENTS.md +43 -12
  2. package/CHANGELOG.md +524 -0
  3. package/README.md +32 -15
  4. package/SECURITY.md +6 -5
  5. package/bin/connecta.mjs +5 -1
  6. package/dist/activity.d.ts +10 -3
  7. package/dist/activity.js +10 -1
  8. package/dist/advertised-schema.js +53 -0
  9. package/dist/artifacts/connector.d.ts +55 -0
  10. package/dist/artifacts/connector.js +724 -0
  11. package/dist/artifacts/document.js +76 -0
  12. package/dist/artifacts/guide.js +140 -0
  13. package/dist/artifacts/html-scan.js +329 -0
  14. package/dist/artifacts/json.js +68 -0
  15. package/dist/artifacts/kv-store.d.ts +20 -0
  16. package/dist/artifacts/kv-store.js +140 -0
  17. package/dist/artifacts/markdown.js +449 -0
  18. package/dist/artifacts/module.d.ts +37 -0
  19. package/dist/artifacts/module.js +60 -0
  20. package/dist/artifacts/operations.d.ts +195 -0
  21. package/dist/artifacts/operations.js +944 -0
  22. package/dist/artifacts/refresh.d.ts +53 -0
  23. package/dist/artifacts/refresh.js +235 -0
  24. package/dist/artifacts/routes.js +194 -0
  25. package/dist/artifacts/types.d.ts +214 -0
  26. package/dist/artifacts/types.js +22 -0
  27. package/dist/artifacts/validate.d.ts +62 -0
  28. package/dist/artifacts/validate.js +652 -0
  29. package/dist/artifacts.d.ts +6 -0
  30. package/dist/artifacts.js +9 -0
  31. package/dist/auth/downstream-oauth.js +490 -106
  32. package/dist/call-admission.js +139 -113
  33. package/dist/catalog-service.js +152 -106
  34. package/dist/catalog.js +297 -38
  35. package/dist/concurrency.js +24 -23
  36. package/dist/connector-access.d.ts +18 -4
  37. package/dist/connector-access.js +65 -20
  38. package/dist/connector-caller.d.ts +8 -0
  39. package/dist/connector-caller.js +19 -0
  40. package/dist/connector-scope.js +6 -37
  41. package/dist/connectors/api.d.ts +2 -2
  42. package/dist/connectors/remote-mcp.d.ts +6 -5
  43. package/dist/connectors/remote-mcp.js +330 -240
  44. package/dist/credential-contract.d.ts +9 -0
  45. package/dist/credential-rules.js +2 -2
  46. package/dist/credentials.d.ts +6 -1
  47. package/dist/credentials.js +45 -3
  48. package/dist/errors.d.ts +16 -1
  49. package/dist/errors.js +28 -0
  50. package/dist/execute.js +745 -282
  51. package/dist/executor-admission.d.ts +11 -1
  52. package/dist/executor-admission.js +145 -63
  53. package/dist/executors/quickjs-runtime.js +4 -2
  54. package/dist/executors/quickjs.js +255 -212
  55. package/dist/fix-prompt.js +37 -0
  56. package/dist/index.d.ts +72 -7
  57. package/dist/index.js +281 -26
  58. package/dist/invocation.js +371 -286
  59. package/dist/meta-tools.js +213 -94
  60. package/dist/module-contracts.d.ts +21 -1
  61. package/dist/oauth-callback-outcome.js +94 -0
  62. package/dist/oauth-sealing.js +29 -0
  63. package/dist/operator-ui/fix-prompts.js +87 -0
  64. package/dist/operator-ui/generated.js +2 -2
  65. package/dist/operator-ui/model.d.ts +103 -1
  66. package/dist/operator-ui/setup-commands.js +53 -0
  67. package/dist/operator-ui/view.js +128 -3
  68. package/dist/program-source.js +321 -0
  69. package/dist/providers/notion.js +2 -2
  70. package/dist/providers/revenuecat.js +32 -5
  71. package/dist/providers/stripe.d.ts +3 -2
  72. package/dist/providers/vercel.js +1 -1
  73. package/dist/registry.d.ts +43 -7
  74. package/dist/registry.js +434 -288
  75. package/dist/resumable.js +1291 -0
  76. package/dist/routes/activity.js +74 -154
  77. package/dist/routes/actor-labels.js +114 -0
  78. package/dist/routes/credentials.js +196 -163
  79. package/dist/routes/mcp.js +228 -112
  80. package/dist/routes/oauth-management.js +73 -71
  81. package/dist/routes/oauth.js +44 -16
  82. package/dist/routes/operator.js +83 -0
  83. package/dist/routes/shared.d.ts +25 -1
  84. package/dist/routes/shared.js +20 -2
  85. package/dist/routes/ui-data.js +211 -0
  86. package/dist/routes/ui.js +76 -35
  87. package/dist/run-journal.js +347 -0
  88. package/dist/runtime/admission.js +43 -0
  89. package/dist/runtime/call-admission.js +67 -0
  90. package/dist/runtime/connector-scope.js +72 -0
  91. package/dist/runtime/run.js +174 -0
  92. package/dist/runtime/services.js +130 -0
  93. package/dist/runtime/shared-read.js +106 -0
  94. package/dist/runtime/storage.js +66 -0
  95. package/dist/server.js +137 -89
  96. package/dist/skills.js +28 -9
  97. package/dist/storage/file.js +39 -2
  98. package/dist/storage/memory.js +28 -16
  99. package/dist/timeout.js +39 -43
  100. package/dist/tool-safety.d.ts +27 -1
  101. package/dist/tool-safety.js +31 -0
  102. package/dist/types.d.ts +45 -7
  103. package/dist/ui.d.ts +25 -4
  104. package/dist/ui.js +77 -167
  105. package/dist/url-safety.js +157 -0
  106. package/dist/version.d.ts +1 -1
  107. package/dist/version.js +1 -1
  108. package/documentation/architecture.md +367 -16
  109. package/documentation/auth.md +163 -2
  110. package/documentation/code-mode.md +274 -56
  111. package/documentation/meta-tools.md +200 -44
  112. package/ethos.md +46 -47
  113. package/examples/worker/AGENTS.md +6 -0
  114. package/examples/worker/README.md +224 -14
  115. package/examples/worker/src/d1-activity-row.ts +14 -2
  116. package/examples/worker/src/d1-activity.ts +4 -2
  117. package/examples/worker/src/d1-storage.ts +116 -0
  118. package/examples/worker/src/index.ts +42 -7
  119. package/examples/worker/src/r2-artifact-blobs.ts +29 -0
  120. package/examples/worker/wrangler.jsonc +21 -5
  121. package/package.json +16 -4
  122. package/templates/node/AGENTS.md +9 -2
  123. package/templates/node/README.md +31 -3
  124. package/templates/node/package.json +1 -1
  125. package/templates/node/src/index.ts +27 -0
  126. package/dist/auth/downstream-oauth.d.ts +0 -200
  127. package/dist/catalog-fingerprint.d.ts +0 -19
  128. package/dist/catalog-limits.d.ts +0 -9
  129. package/dist/catalog-service.d.ts +0 -263
  130. package/dist/catalog.d.ts +0 -87
  131. package/dist/concurrency.d.ts +0 -3
  132. package/dist/connectors/guarded-fetch.d.ts +0 -96
  133. package/dist/credential-rules.d.ts +0 -85
  134. package/dist/execute.d.ts +0 -154
  135. package/dist/executor-result.d.ts +0 -10
  136. package/dist/executors/quickjs-child.d.ts +0 -1
  137. package/dist/executors/quickjs-protocol.d.ts +0 -67
  138. package/dist/favicon.d.ts +0 -1
  139. package/dist/identity.d.ts +0 -4
  140. package/dist/invocation.d.ts +0 -74
  141. package/dist/mcp-result.d.ts +0 -8
  142. package/dist/meta-tools.d.ts +0 -129
  143. package/dist/operator-ui/generated.d.ts +0 -2
  144. package/dist/operator-ui/view.d.ts +0 -188
  145. package/dist/result-shapes.d.ts +0 -13
  146. package/dist/retired-toolkits.d.ts +0 -2
  147. package/dist/routes/activity.d.ts +0 -2
  148. package/dist/routes/credentials.d.ts +0 -2
  149. package/dist/routes/mcp.d.ts +0 -10
  150. package/dist/routes/oauth-management.d.ts +0 -2
  151. package/dist/routes/oauth.d.ts +0 -2
  152. package/dist/routes/ui.d.ts +0 -2
  153. package/dist/server.d.ts +0 -9
  154. package/dist/skills.d.ts +0 -44
  155. package/dist/timeout.d.ts +0 -21
@@ -20,5 +20,14 @@ export interface CredentialVault {
20
20
  set(connectorId: string, value: string, updatedBy: string, owner?: string): Promise<CredentialMetadata>;
21
21
  setAll(connectorId: string, values: ConnectorCredentialValues, updatedBy: string, owner?: string): Promise<CredentialMetadata>;
22
22
  delete(connectorId: string, owner?: string): Promise<void>;
23
+ /**
24
+ * Encrypt downstream OAuth state (tokens, a registered client, a PKCE
25
+ * verifier) before it enters storage. `purpose` is the storage key the value
26
+ * is written under, so the result is bound to one connector, owner, and key.
27
+ * Without both `seal` and `open`, that state is stored as plaintext.
28
+ */
29
+ seal?(connectorId: string, purpose: string, plaintext: string, owner?: string): Promise<string>;
30
+ /** Reverse `seal` for the same connector, purpose, and owner; rejects otherwise. */
31
+ open?(connectorId: string, purpose: string, sealed: string, owner?: string): Promise<string>;
23
32
  }
24
33
  export {};
@@ -3,7 +3,7 @@ export const STORED_CREDENTIAL_SHAPE_MISMATCH_ERROR = "Stored credential fields
3
3
  /**
4
4
  * Compare a connector's current declaration with the keys in its stored
5
5
  * credential. Values are deliberately ignored: callers may pass decrypted
6
- * values or `/credentials`' masked field metadata and get the same answer.
6
+ * values or the operator UI's masked credential field metadata and get the same answer.
7
7
  *
8
8
  * The test is CONTAINMENT, not equality: the stored key set is compatible when
9
9
  * it holds every field currently declared — the reserved `value` key for a
@@ -65,7 +65,7 @@ export function describeUndeclaredCredentialFields(fields) {
65
65
  }
66
66
  /**
67
67
  * The one rule deciding whether a connector's credential can be tested — read
68
- * by /credentials' `testable` flag, by `POST /ui/credentials/<id>/test` when
68
+ * by the operator UI's credential `testable` flag, by `POST /ui/credentials/<id>/test` when
69
69
  * it picks a hook, and by the construction-time mismatch warning, so those
70
70
  * three cannot drift apart.
71
71
  *
@@ -3,7 +3,8 @@ import type { ConnectorCredentialValues, KVStorage } from "./types.js";
3
3
  /**
4
4
  * Encrypted, connector-scoped credential vault over the deployment's existing
5
5
  * KVStorage. Only ciphertext enters KV; the AES-GCM key remains an environment
6
- * secret outside the store.
6
+ * secret outside the store. The same key seals downstream OAuth state, which
7
+ * the OAuth provider stores itself.
7
8
  */
8
9
  export declare class CredentialVault implements Vault {
9
10
  private readonly storage;
@@ -17,5 +18,9 @@ export declare class CredentialVault implements Vault {
17
18
  set(connectorId: string, value: string, updatedBy: string, owner?: string): Promise<CredentialMetadata>;
18
19
  setAll(connectorId: string, values: ConnectorCredentialValues, updatedBy: string, owner?: string): Promise<CredentialMetadata>;
19
20
  delete(connectorId: string, owner?: string): Promise<void>;
21
+ /** Encrypt downstream OAuth state under the vault key; nothing is stored. */
22
+ seal(connectorId: string, purpose: string, plaintext: string, owner?: string): Promise<string>;
23
+ /** Decrypt what `seal` produced for this connector, purpose, and owner. */
24
+ open(connectorId: string, purpose: string, sealed: string, owner?: string): Promise<string>;
20
25
  }
21
26
  export declare function encryptedCredentialVault(storage: KVStorage, encryptionKey: string): Vault;
@@ -3,6 +3,16 @@ const IV_BYTES = 12;
3
3
  const MAX_CREDENTIAL_BYTES = 16_384;
4
4
  const encoder = new TextEncoder();
5
5
  const decoder = new TextDecoder();
6
+ /** Version tag of the compact `seal` output: `v1.<iv>.<ciphertext>`. */
7
+ const SEALED_VERSION = "v1";
8
+ /**
9
+ * Binds sealed OAuth state to its connector, owner, and physical storage key
10
+ * (which carries the authorization epoch), so ciphertext copied anywhere else
11
+ * does not open. Connector ids cannot contain `|`, and the owner is a hash.
12
+ */
13
+ function oauthStateAdditionalData(connectorId, purpose, owner) {
14
+ return encoder.encode(`connecta:oauth-state:v1|${connectorId}|${owner ?? ""}|${purpose}`);
15
+ }
6
16
  function storageKey(connectorId, owner) {
7
17
  return owner
8
18
  ? `principal:${owner}:conn:${connectorId}:credential:v1`
@@ -20,7 +30,7 @@ function base64ToBytes(value) {
20
30
  binary = atob(value);
21
31
  }
22
32
  catch {
23
- throw new Error("credentials.encryptionKey must be a base64-encoded 32-byte key");
33
+ throw new Error("The credential vault encryption key must be a base64-encoded 32-byte key");
24
34
  }
25
35
  return Uint8Array.from(binary, (char) => char.charCodeAt(0));
26
36
  }
@@ -87,7 +97,8 @@ function validateValues(values) {
87
97
  /**
88
98
  * Encrypted, connector-scoped credential vault over the deployment's existing
89
99
  * KVStorage. Only ciphertext enters KV; the AES-GCM key remains an environment
90
- * secret outside the store.
100
+ * secret outside the store. The same key seals downstream OAuth state, which
101
+ * the OAuth provider stores itself.
91
102
  */
92
103
  export class CredentialVault {
93
104
  storage;
@@ -96,7 +107,7 @@ export class CredentialVault {
96
107
  this.storage = storage;
97
108
  const raw = base64ToBytes(encryptionKey.trim());
98
109
  if (raw.byteLength !== KEY_BYTES) {
99
- throw new Error("credentials.encryptionKey must be a base64-encoded 32-byte key");
110
+ throw new Error("The credential vault encryption key must be a base64-encoded 32-byte key");
100
111
  }
101
112
  this.key = crypto.subtle.importKey("raw", raw, { name: "AES-GCM" }, false, ["encrypt", "decrypt"]);
102
113
  }
@@ -182,6 +193,37 @@ export class CredentialVault {
182
193
  async delete(connectorId, owner) {
183
194
  await this.storage.delete(storageKey(connectorId, owner));
184
195
  }
196
+ /** Encrypt downstream OAuth state under the vault key; nothing is stored. */
197
+ async seal(connectorId, purpose, plaintext, owner) {
198
+ const iv = crypto.getRandomValues(new Uint8Array(IV_BYTES));
199
+ const ciphertext = await crypto.subtle.encrypt({
200
+ name: "AES-GCM",
201
+ iv,
202
+ additionalData: oauthStateAdditionalData(connectorId, purpose, owner),
203
+ }, await this.key, encoder.encode(plaintext));
204
+ return `${SEALED_VERSION}.${bytesToBase64(iv)}.${bytesToBase64(new Uint8Array(ciphertext))}`;
205
+ }
206
+ /** Decrypt what `seal` produced for this connector, purpose, and owner. */
207
+ async open(connectorId, purpose, sealed, owner) {
208
+ const [version, iv, ciphertext, ...rest] = sealed.split(".");
209
+ if (version !== SEALED_VERSION ||
210
+ iv === undefined ||
211
+ ciphertext === undefined ||
212
+ rest.length > 0) {
213
+ throw new Error("Sealed OAuth state is invalid or corrupted");
214
+ }
215
+ try {
216
+ const plaintext = await crypto.subtle.decrypt({
217
+ name: "AES-GCM",
218
+ iv: base64ToBytes(iv),
219
+ additionalData: oauthStateAdditionalData(connectorId, purpose, owner),
220
+ }, await this.key, base64ToBytes(ciphertext));
221
+ return decoder.decode(plaintext);
222
+ }
223
+ catch {
224
+ throw new Error("Sealed OAuth state could not be decrypted");
225
+ }
226
+ }
185
227
  }
186
228
  export function encryptedCredentialVault(storage, encryptionKey) {
187
229
  return new CredentialVault(storage, encryptionKey);
package/dist/errors.d.ts CHANGED
@@ -15,7 +15,14 @@ export type ConnectorCallErrorCode = "timeout" | "auth_required" | "rate_limited
15
15
  * code stays `connector_call_failed` or `auth_required` with a message that
16
16
  * states the ambiguity (H11).
17
17
  */
18
- | "not_found" | "input_required_unsupported" | "connector_call_failed";
18
+ | "not_found"
19
+ /**
20
+ * The write named a base version or revision that is no longer current:
21
+ * someone else changed the thing first. The caller re-reads, reapplies its
22
+ * change, and retries with the base `current` reports — repeating the same
23
+ * call can never succeed, and waiting will not help either.
24
+ */
25
+ | "conflict" | "input_required_unsupported" | "connector_call_failed";
19
26
  /** One bounded, payload-free explanation of an input-schema mismatch. */
20
27
  export interface ArgumentValidationIssue {
21
28
  /** JSON Pointer into the submitted arguments; "/" means the root value. */
@@ -88,12 +95,18 @@ export declare class ConnectorCallError extends Error {
88
95
  readonly validation: ArgumentValidationDetails | undefined;
89
96
  /** Sanitized transport diagnostics for `unavailable` only. */
90
97
  readonly details: UnavailableDetails | undefined;
98
+ /**
99
+ * For `conflict` only: where each thing the write touched stands now, e.g.
100
+ * `{ revision: 12, view: 7 }`. Bounded to 20 whole-number entries.
101
+ */
102
+ readonly current: Readonly<Record<string, number>> | undefined;
91
103
  constructor(code: ConnectorCallErrorCode, message: string, opts?: {
92
104
  retryable?: boolean;
93
105
  retryAfterMs?: number;
94
106
  cause?: unknown;
95
107
  validation?: ArgumentValidationDetails;
96
108
  details?: UnavailableDetails;
109
+ current?: Readonly<Record<string, number>>;
97
110
  });
98
111
  }
99
112
  /** The `error` object surfaced in value-mode call results and rejected promises. */
@@ -107,6 +120,8 @@ export interface CallErrorDetails {
107
120
  retryAfterMs?: number;
108
121
  /** Bounded input-schema findings; paths and expectations, never values. */
109
122
  validation?: ArgumentValidationDetails;
123
+ /** On a `conflict`: where each thing the write touched stands now. */
124
+ current?: Readonly<Record<string, number>>;
110
125
  /** Connector whose failed operation needs recovery. */
111
126
  connector?: string;
112
127
  /** Canonical downstream address the agent may retry after recovery. */
package/dist/errors.js CHANGED
@@ -162,9 +162,30 @@ const RETRYABLE_BY_CODE = {
162
162
  auth_required: false,
163
163
  invalid_args: false,
164
164
  not_found: false,
165
+ conflict: false,
165
166
  input_required_unsupported: false,
166
167
  connector_call_failed: false,
167
168
  };
169
+ /** Entries a conflict's `current` map may carry. */
170
+ const MAX_CURRENT_ENTRIES = 20;
171
+ const MAX_CURRENT_KEY_CHARS = 128;
172
+ /**
173
+ * Where each thing a conflicting write touched stands now: short names to
174
+ * whole non-negative numbers, at most 20 of them. Anything else is dropped
175
+ * whole rather than clipped, because a truncated key names a different thing.
176
+ */
177
+ function boundedCurrent(current) {
178
+ if (!current || typeof current !== "object")
179
+ return undefined;
180
+ const entries = Object.entries(current).filter(([key, value]) => key.length > 0 &&
181
+ key.length <= MAX_CURRENT_KEY_CHARS &&
182
+ key !== "__proto__" &&
183
+ Number.isSafeInteger(value) &&
184
+ value >= 0);
185
+ if (entries.length === 0)
186
+ return undefined;
187
+ return Object.freeze(Object.fromEntries(entries.slice(0, MAX_CURRENT_ENTRIES)));
188
+ }
168
189
  /** Non-negative integer milliseconds, or undefined for anything else. */
169
190
  function normalizeRetryAfterMs(value) {
170
191
  if (value === undefined)
@@ -201,6 +222,11 @@ export class ConnectorCallError extends Error {
201
222
  validation;
202
223
  /** Sanitized transport diagnostics for `unavailable` only. */
203
224
  details;
225
+ /**
226
+ * For `conflict` only: where each thing the write touched stands now, e.g.
227
+ * `{ revision: 12, view: 7 }`. Bounded to 20 whole-number entries.
228
+ */
229
+ current;
204
230
  constructor(code, message, opts = {}) {
205
231
  super(message, opts.cause !== undefined ? { cause: opts.cause } : undefined);
206
232
  this.name = "ConnectorCallError";
@@ -211,6 +237,7 @@ export class ConnectorCallError extends Error {
211
237
  code === "unavailable" ? sanitizedUnavailableDetails(opts.details) : undefined;
212
238
  this.validation =
213
239
  code === "invalid_args" ? boundedValidation(opts.validation) : undefined;
240
+ this.current = code === "conflict" ? boundedCurrent(opts.current) : undefined;
214
241
  }
215
242
  }
216
243
  /**
@@ -262,6 +289,7 @@ export function classifyCallError(err, fallbackCode = "connector_call_failed") {
262
289
  : {}),
263
290
  ...(err.validation ? { validation: err.validation } : {}),
264
291
  ...(err.details ? { details: err.details } : {}),
292
+ ...(err.current ? { current: err.current } : {}),
265
293
  };
266
294
  }
267
295
  // An aborted fetch rejects with a DOMException named "AbortError" whose