@zackbart/connecta 0.37.1 → 0.39.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 (265) hide show
  1. package/CHANGELOG.md +94 -0
  2. package/README.md +5 -4
  3. package/bin/connecta.mjs +1 -39
  4. package/bin/version.mjs +1 -1
  5. package/dist/access-tokens.js +2 -4
  6. package/dist/agent-identity.js +1 -1
  7. package/dist/agent-routes.js +231 -0
  8. package/dist/auth/clerk-sdk/generated.js +2 -1
  9. package/dist/auth/downstream-oauth/consent.js +519 -0
  10. package/dist/auth/downstream-oauth/grant-store.js +439 -0
  11. package/dist/auth/downstream-oauth/refresh-coordinator.js +562 -0
  12. package/dist/auth/downstream-oauth/refresh-records.js +447 -0
  13. package/dist/auth/downstream-oauth/sdk-hooks.js +242 -0
  14. package/dist/auth/downstream-oauth.js +66 -2516
  15. package/dist/auth/downstream-revocation.js +1 -1
  16. package/dist/auth/native.js +27 -0
  17. package/dist/auth/oauth-grant.js +24 -0
  18. package/dist/auth/oauth-token-response.js +1 -1
  19. package/dist/auth/static-oauth.js +153 -131
  20. package/dist/auth/vercel-agent.js +2 -1
  21. package/dist/auth-elicitation.js +9 -3
  22. package/dist/authorization-handoff.js +11 -5
  23. package/dist/branding.d.ts +1 -15
  24. package/dist/branding.js +2 -2
  25. package/dist/bytes.js +42 -0
  26. package/dist/call-admission-context.js +2 -5
  27. package/dist/call-recovery.js +0 -3
  28. package/dist/catalog-cache.js +130 -43
  29. package/dist/catalog-fingerprint.js +2 -18
  30. package/dist/catalog-policy.js +18 -0
  31. package/dist/catalog-service.js +285 -48
  32. package/dist/catalog.js +18 -2
  33. package/dist/config-defaults.d.ts +1 -0
  34. package/dist/config-defaults.js +1 -0
  35. package/dist/config-schema.d.ts +1 -1
  36. package/dist/config-schema.js +6 -2
  37. package/dist/config.d.ts +57 -25
  38. package/dist/config.js +30 -77
  39. package/dist/connector-caller.d.ts +1 -1
  40. package/dist/connector-health.js +59 -0
  41. package/dist/connector-status.js +117 -0
  42. package/dist/connectors/api-connector.d.ts +7 -2
  43. package/dist/connectors/api-connector.js +11 -7
  44. package/dist/connectors/guarded-fetch.d.ts +0 -6
  45. package/dist/connectors/guarded-fetch.js +1 -11
  46. package/dist/connectors/negotiation-cache.js +22 -4
  47. package/dist/connectors/remote-mcp/call.js +164 -0
  48. package/dist/connectors/remote-mcp/catalog.js +278 -0
  49. package/dist/connectors/remote-mcp/config.js +263 -0
  50. package/dist/connectors/remote-mcp/connection.js +671 -0
  51. package/dist/connectors/remote-mcp/errors.js +488 -0
  52. package/dist/connectors/remote-mcp/oauth.js +469 -0
  53. package/dist/connectors/remote-mcp/resources.js +473 -0
  54. package/dist/connectors/remote-mcp/runtime.js +1 -0
  55. package/dist/connectors/remote-mcp/transport.js +544 -0
  56. package/dist/connectors/remote-mcp.js +61 -2898
  57. package/dist/connectors/resource-uri.js +0 -4
  58. package/dist/credential-contract.d.ts +23 -0
  59. package/dist/credential-reads.js +17 -0
  60. package/dist/credential-verification.js +53 -0
  61. package/dist/credentials.d.ts +7 -0
  62. package/dist/credentials.js +60 -25
  63. package/dist/d1.d.ts +0 -89
  64. package/dist/d1.js +0 -34
  65. package/dist/describe-config.d.ts +12 -2
  66. package/dist/describe-config.js +2 -0
  67. package/dist/discovery-health.js +199 -0
  68. package/dist/downstream-input.js +4 -2
  69. package/dist/errors.d.ts +55 -17
  70. package/dist/errors.js +95 -41
  71. package/dist/execute.js +172 -125
  72. package/dist/executor-result.js +5 -2
  73. package/dist/executors/quickjs-runtime.js +2 -25
  74. package/dist/executors/quickjs.js +1 -1
  75. package/dist/guest-functions.js +83 -0
  76. package/dist/guest-runtime.js +63 -20
  77. package/dist/guest-types.d.ts +24 -10
  78. package/dist/http/argument-problems.js +35 -0
  79. package/dist/http/client.js +1 -1
  80. package/dist/http/errors.js +7 -6
  81. package/dist/http/fields.js +192 -13
  82. package/dist/http/hatch-response.js +73 -0
  83. package/dist/http/hatch.js +88 -23
  84. package/dist/http/json-schema.js +1 -1
  85. package/dist/http/node-transport.js +0 -1
  86. package/dist/http/oauth.js +4 -3
  87. package/dist/http/paging.js +6 -9
  88. package/dist/http/provider.js +33 -5
  89. package/dist/http/safety.js +9 -1
  90. package/dist/http/services.js +1 -8
  91. package/dist/http/tools.js +45 -30
  92. package/dist/http/vendor.js +7 -7
  93. package/dist/identity.js +2 -6
  94. package/dist/index.d.ts +1 -1
  95. package/dist/index.js +9 -2
  96. package/dist/invocation-auth.js +2 -1
  97. package/dist/invocation.js +21 -6
  98. package/dist/membership.d.ts +13 -5
  99. package/dist/membership.js +34 -2
  100. package/dist/meta-tools.js +50 -35
  101. package/dist/native-recovery.js +4 -16
  102. package/dist/native-tools.js +6 -2
  103. package/dist/oauth-handoff.js +2 -2
  104. package/dist/oauth-sealing.js +10 -0
  105. package/dist/operation-vocabulary.js +136 -14
  106. package/dist/operator-record.d.ts +2 -2
  107. package/dist/operator-record.js +2 -49
  108. package/dist/operator-ui/contract.d.ts +12 -2
  109. package/dist/operator-ui/generated.js +3 -3
  110. package/dist/operator-ui/model.d.ts +22 -1
  111. package/dist/program-writes.js +3 -8
  112. package/dist/provider-call.js +0 -7
  113. package/dist/provider.d.ts +1 -1
  114. package/dist/providers/_shared/google/kit.js +1 -6
  115. package/dist/providers/_shared/google/service-account.js +1 -3
  116. package/dist/providers/_shared/google/workspace.d.ts +1 -1
  117. package/dist/providers/_shared/guide.js +3 -0
  118. package/dist/providers/_shared/json.js +12 -0
  119. package/dist/providers/_shared/rest/compact-index.js +1 -1
  120. package/dist/providers/_shared/rest/kit.js +1 -3
  121. package/dist/providers/_shared/rest/operation-index.js +49 -53
  122. package/dist/providers/_shared/rest/value-safety.js +106 -28
  123. package/dist/providers/breeze/api.js +9 -8
  124. package/dist/providers/breeze/index.js +4 -2
  125. package/dist/providers/breeze/skill.generated.js +1 -1
  126. package/dist/providers/breeze/tools.js +7 -7
  127. package/dist/providers/ccb/api.js +3 -7
  128. package/dist/providers/ccb/index.js +7 -1
  129. package/dist/providers/ccb/skill.generated.js +1 -1
  130. package/dist/providers/ccb/tools.js +1 -6
  131. package/dist/providers/cloudflare/api.js +10 -3
  132. package/dist/providers/cloudflare/index.js +2 -1
  133. package/dist/providers/cloudflare/safety.js +3 -3
  134. package/dist/providers/cloudflare/skill.generated.js +1 -1
  135. package/dist/providers/docs/lib/schemas.js +24 -22
  136. package/dist/providers/docs/safety.js +2 -0
  137. package/dist/providers/docs/skill.generated.js +1 -1
  138. package/dist/providers/docs/tools.js +4 -14
  139. package/dist/providers/drive/api.js +1 -0
  140. package/dist/providers/drive/lib/contracts.js +8 -5
  141. package/dist/providers/drive/skill.generated.js +1 -1
  142. package/dist/providers/drive/tools.js +23 -41
  143. package/dist/providers/forms/api.js +1 -0
  144. package/dist/providers/forms/skill.generated.js +1 -1
  145. package/dist/providers/forms/tools.js +11 -16
  146. package/dist/providers/github/api.js +1 -0
  147. package/dist/providers/github/lib/discovery.js +1 -1
  148. package/dist/providers/github/skill.generated.js +1 -1
  149. package/dist/providers/github/tools.js +61 -17
  150. package/dist/providers/gmail/lib/schemas.js +60 -50
  151. package/dist/providers/gmail/skill.generated.js +1 -1
  152. package/dist/providers/gmail/tools.js +12 -26
  153. package/dist/providers/granola/index.js +2 -0
  154. package/dist/providers/granola/skill.generated.js +1 -1
  155. package/dist/providers/overflow/api.js +8 -4
  156. package/dist/providers/overflow/index.js +2 -1
  157. package/dist/providers/overflow/skill.generated.js +1 -1
  158. package/dist/providers/overflow/tools.js +3 -7
  159. package/dist/providers/planning-center/api.js +5 -2
  160. package/dist/providers/planning-center/lib/constants.d.ts +1 -1
  161. package/dist/providers/planning-center/lib/json-api.js +1 -9
  162. package/dist/providers/planning-center/skill.generated.js +1 -1
  163. package/dist/providers/planning-center/tools.js +2 -1
  164. package/dist/providers/quickbooks/safety.js +80 -39
  165. package/dist/providers/quickbooks/skill.generated.js +1 -1
  166. package/dist/providers/sheets/api.js +1 -0
  167. package/dist/providers/sheets/skill.generated.js +1 -1
  168. package/dist/providers/sheets/tools.js +14 -23
  169. package/dist/providers/slides/skill.generated.js +1 -1
  170. package/dist/providers/slides/tools.js +19 -26
  171. package/dist/providers/stripe/index.js +2 -1
  172. package/dist/providers/stripe/safety.js +2 -2
  173. package/dist/providers/stripe/skill.generated.js +1 -1
  174. package/dist/providers/tithely/api.js +5 -8
  175. package/dist/providers/tithely/index.js +3 -2
  176. package/dist/providers/tithely/skill.generated.js +1 -1
  177. package/dist/providers/tithely/tools.js +7 -5
  178. package/dist/providers/vercel/api.js +2 -7
  179. package/dist/providers/vercel/lib/pin.js +1 -1
  180. package/dist/providers/vercel/safety.js +1 -1
  181. package/dist/providers/vercel/skill.generated.js +1 -1
  182. package/dist/providers/vercel/tools.js +6 -4
  183. package/dist/redaction-marker.js +2 -2
  184. package/dist/registry.d.ts +11 -5
  185. package/dist/registry.js +46 -20
  186. package/dist/request-principal.js +1 -1
  187. package/dist/request-state.js +2 -2
  188. package/dist/routes/access-tokens.js +1 -1
  189. package/dist/routes/activity-disclosure.js +1 -1
  190. package/dist/routes/connect.js +2 -2
  191. package/dist/routes/credentials.js +130 -61
  192. package/dist/routes/dispatch.js +5 -1
  193. package/dist/routes/mcp.js +34 -17
  194. package/dist/routes/oauth-management.js +1 -1
  195. package/dist/routes/operator.js +6 -5
  196. package/dist/routes/shared.d.ts +13 -8
  197. package/dist/routes/shared.js +15 -9
  198. package/dist/routes/ui-config.js +3 -3
  199. package/dist/routes/ui-data.js +126 -20
  200. package/dist/routes/ui.js +17 -1
  201. package/dist/runtime/call-admission.js +1 -1
  202. package/dist/runtime/call.js +8 -3
  203. package/dist/runtime/native-storage.js +9 -0
  204. package/dist/runtime/request-reads.js +182 -0
  205. package/dist/runtime/request-scope.js +16 -0
  206. package/dist/runtime/settlement.js +1 -1
  207. package/dist/runtime/shared-read.js +2 -15
  208. package/dist/runtime/storage.js +12 -0
  209. package/dist/sent-secrets.d.ts +1 -2
  210. package/dist/sent-secrets.js +3 -8
  211. package/dist/server.js +4 -4
  212. package/dist/skills.js +44 -54
  213. package/dist/sqlite.d.ts +0 -19
  214. package/dist/sqlite.js +2 -83
  215. package/dist/storage/keys.js +69 -128
  216. package/dist/storage/memory.js +8 -2
  217. package/dist/storage/sql-native.js +27 -101
  218. package/dist/storage/sql.d.ts +1 -24
  219. package/dist/storage/sql.js +13 -14
  220. package/dist/storage-digest.js +32 -0
  221. package/dist/timeout.js +8 -0
  222. package/dist/tool-error.js +4 -0
  223. package/dist/types.d.ts +33 -0
  224. package/dist/url-safety.js +0 -4
  225. package/dist/usage-guide.js +18 -37
  226. package/dist/validate.d.ts +3 -0
  227. package/dist/validate.js +220 -158
  228. package/dist/version.d.ts +1 -1
  229. package/dist/version.js +1 -1
  230. package/dist/worker.js +1 -1
  231. package/dist/write-outcome.js +0 -3
  232. package/documentation/README.md +19 -55
  233. package/documentation/architecture.md +436 -515
  234. package/documentation/auth.md +229 -88
  235. package/documentation/code-mode.md +201 -94
  236. package/documentation/deploying.md +25 -43
  237. package/documentation/integrating.md +18 -19
  238. package/documentation/meta-tools.md +225 -51
  239. package/documentation/migration-0.33.md +10 -6
  240. package/documentation/migration-0.34.md +5 -5
  241. package/documentation/migration-retired-layouts.md +74 -0
  242. package/documentation/operator-tests.md +20 -120
  243. package/documentation/operator-ui.md +108 -16
  244. package/documentation/pool-membership-upgrade.md +44 -0
  245. package/documentation/provider-auth-migration.md +4 -10
  246. package/documentation/provider-migration-0.29.md +4 -15
  247. package/documentation/providers.md +105 -0
  248. package/examples/worker/AGENTS.md +14 -16
  249. package/examples/worker/README.md +24 -118
  250. package/examples/worker/src/connecta.config.ts +1 -0
  251. package/guides/connecting-agents.md +9 -31
  252. package/guides/how-it-works.md +6 -4
  253. package/guides/safety-and-trust.md +4 -1
  254. package/package.json +19 -19
  255. package/templates/node/AGENTS.md +13 -12
  256. package/templates/node/README.md +9 -26
  257. package/templates/node/package.json +1 -1
  258. package/templates/node/src/connecta.config.ts +1 -0
  259. package/dist/auth/oauth-v2-migration.js +0 -135
  260. package/dist/provider-call-facts.d.ts +0 -115
  261. package/dist/provider-call.d.ts +0 -112
  262. package/dist/storage/d1-copy.js +0 -255
  263. package/dist/storage/sql-schema.d.ts +0 -6
  264. package/examples/worker/kv-to-d1.wrangler.jsonc +0 -26
  265. package/examples/worker/scripts/copy-kv-to-d1.mjs +0 -162
@@ -26,10 +26,6 @@ export function resourceUriMatchesTemplates(uri, templates, note = () => { }) {
26
26
  }
27
27
  return matched ? { matched } : { matched, ...(refusal ? { refusal } : {}) };
28
28
  }
29
- export function resourceUriMatchesTemplate(uri, template) {
30
- const parsed = parse(template);
31
- return typeof parsed === "object" && matches(uri, parsed);
32
- }
33
29
  function parse(template) {
34
30
  if (template.length > MAX_URI_LENGTH)
35
31
  return;
@@ -10,6 +10,12 @@ export interface CredentialMetadata {
10
10
  /** Backward-compatible metadata for the reserved single credential field. */
11
11
  lastFour?: string;
12
12
  updatedAt: string;
13
+ /**
14
+ * Opaque and unique to the save this metadata describes. Credential
15
+ * verification is recorded only against it, so a vault that omits it
16
+ * stores credentials that are never shown as verified.
17
+ */
18
+ revision?: string;
13
19
  /** Per-field masked metadata for named multi-value credentials. */
14
20
  fields?: Record<string, CredentialFieldMetadata>;
15
21
  }
@@ -20,6 +26,15 @@ export interface CredentialVault {
20
26
  set(connectorId: string, value: string, updatedBy: string, owner?: string): Promise<CredentialMetadata>;
21
27
  setAll(connectorId: string, values: ConnectorCredentialValues, updatedBy: string, owner?: string): Promise<CredentialMetadata>;
22
28
  delete(connectorId: string, owner?: string): Promise<void>;
29
+ /**
30
+ * The values and metadata of one save, read together so neither can come
31
+ * from a later save. Without it, a Test of the stored credential records
32
+ * no verification.
33
+ */
34
+ snapshot?(connectorId: string, owner?: string): Promise<{
35
+ values: ConnectorCredentialValues;
36
+ metadata: CredentialMetadata;
37
+ } | null>;
23
38
  /**
24
39
  * Encrypt downstream OAuth state (tokens, a registered client, a PKCE
25
40
  * verifier) before it enters storage. `purpose` is the storage key the value
@@ -37,6 +52,14 @@ export interface CredentialVault {
37
52
  * exposes the vault or this key through a connector context or guest API.
38
53
  */
39
54
  requestStateKey?(): Promise<Uint8Array>;
55
+ /**
56
+ * Host-only HMAC-SHA-256 signing key for stored remote catalog,
57
+ * negotiation-verdict and connector-health keys, so those partitions are
58
+ * deployment-keyed. Must be stable across the deployment's processes,
59
+ * purpose-specific, and never stored. Without it, those keys use a random
60
+ * per-process key, and such entries are reused only within one process.
61
+ */
62
+ storageDigestKey?(): Promise<CryptoKey>;
40
63
  /** Reverse `seal` for the same connector, purpose, and owner; rejects otherwise. */
41
64
  open?(connectorId: string, purpose: string, sealed: string, owner?: string): Promise<string>;
42
65
  }
@@ -0,0 +1,17 @@
1
+ const readers = new WeakMap();
2
+ /** `canonical` is the base class's own prototype methods, never the instance's. */
3
+ export function registerCredentialReads(vault, canonical, memo) {
4
+ readers.set(vault, { canonical, memo });
5
+ }
6
+ /** A `fresh` reader always reads storage and records what it saw. */
7
+ export function credentialReads(vault, reads, fresh = false) {
8
+ const reader = readers.get(vault);
9
+ return {
10
+ getAll: (connectorId, owner) => reader && vault.getAll === reader.canonical.getAll
11
+ ? reader.memo.getAll(connectorId, owner, reads, fresh)
12
+ : vault.getAll(connectorId, owner),
13
+ metadata: (connectorId, owner) => reader && vault.metadata === reader.canonical.metadata
14
+ ? reader.memo.metadata(connectorId, owner, reads, fresh)
15
+ : vault.metadata(connectorId, owner),
16
+ };
17
+ }
@@ -0,0 +1,53 @@
1
+ // The last verification of a connector's operator-managed credential: a typed,
2
+ // payload-free observation stored per connector and owner (the connector's
3
+ // `ctx.storage` is already partitioned by both). Credential tests and saves
4
+ // write it; the operator page only reads it (INV-10).
5
+ import { classifyCallError } from "./errors.js";
6
+ import { verificationKeys } from "./storage/keys.js";
7
+ const FAILURES = ["rejected", "scope", "unreachable", "timeout", "failed"];
8
+ const OUTCOMES = ["ok", "unverified", "rejected", "scope", "unreachable", "timeout", "failed"];
9
+ /** Each typed code that names a class; every other code is `failed`. */
10
+ const CLASS_OF = {
11
+ auth_required: "rejected",
12
+ downstream_oauth_required: "rejected",
13
+ provider_permission_denied: "scope",
14
+ timeout: "timeout",
15
+ unavailable: "unreachable",
16
+ rate_limited: "unreachable",
17
+ };
18
+ /** A thrown failure's class, read from its typed code alone, never its text. */
19
+ export const credentialFailureClass = (error) => CLASS_OF[classifyCallError(error).code] ?? "failed";
20
+ /** A hook's answer as an outcome. Only its typed fields count; `message` never does. */
21
+ export const credentialTestOutcome = (result) => result?.ok === true ? "ok" : FAILURES.includes(result?.failure) ? result.failure : "failed";
22
+ /** A checked copy, or undefined: nothing here trusts stored bytes. */
23
+ function checkedVerification(value) {
24
+ const { outcome, observedAt } = (value ?? {});
25
+ return OUTCOMES.includes(outcome) &&
26
+ typeof observedAt === "string" &&
27
+ observedAt.length < 64 &&
28
+ !Number.isNaN(Date.parse(observedAt))
29
+ ? { outcome: outcome, observedAt }
30
+ : undefined;
31
+ }
32
+ /**
33
+ * The stored observation, or undefined when there is none or it cannot be
34
+ * read. With `credential`, only one made about that vault credential (its
35
+ * `updatedAt`) counts: a replacement saved since is unverified.
36
+ */
37
+ export async function readVerification(storage, credential) {
38
+ try {
39
+ const stored = JSON.parse((await storage.get(verificationKeys.record)) ?? "null");
40
+ return credential === undefined || stored?.credential === credential ? checkedVerification(stored) : undefined;
41
+ }
42
+ catch {
43
+ return undefined;
44
+ }
45
+ }
46
+ /** Record an outcome, bound to the vault credential's `updatedAt` when it is about one; `null` removes the record. */
47
+ export async function writeVerification(storage, outcome, credential) {
48
+ if (!outcome)
49
+ return void (await storage.delete(verificationKeys.record));
50
+ const record = { outcome, observedAt: new Date().toISOString() };
51
+ await storage.set(verificationKeys.record, JSON.stringify({ ...record, credential }));
52
+ return record;
53
+ }
@@ -11,15 +11,22 @@ export declare class CredentialVault implements Vault {
11
11
  private readonly key;
12
12
  private readonly handoffKey;
13
13
  private readonly retryKey;
14
+ private readonly digestKey;
14
15
  constructor(storage: KVStorage, encryptionKey: string);
15
16
  signOAuthHandoff(payload: string): Promise<string>;
16
17
  requestStateKey(): Promise<Uint8Array>;
18
+ storageDigestKey(): Promise<CryptoKey>;
17
19
  verifyOAuthHandoff(payload: string, signature: string): Promise<boolean>;
18
20
  private additionalData;
19
21
  private read;
20
22
  get(connectorId: string, field?: string, owner?: string): Promise<string | null>;
21
23
  getAll(connectorId: string, owner?: string): Promise<ConnectorCredentialValues | null>;
22
24
  metadata(connectorId: string, owner?: string): Promise<CredentialMetadata | null>;
25
+ /** The values and metadata of one save, from a single envelope read. */
26
+ snapshot(connectorId: string, owner?: string): Promise<{
27
+ values: ConnectorCredentialValues;
28
+ metadata: CredentialMetadata;
29
+ } | null>;
23
30
  set(connectorId: string, value: string, updatedBy: string, owner?: string): Promise<CredentialMetadata>;
24
31
  setAll(connectorId: string, values: ConnectorCredentialValues, updatedBy: string, owner?: string): Promise<CredentialMetadata>;
25
32
  delete(connectorId: string, owner?: string): Promise<void>;
@@ -1,6 +1,7 @@
1
1
  import { advanceCatalogRevision } from "./catalog-revision.js";
2
- import { deriveOAuthHandoffKey, deriveRequestStateKey } from "./oauth-sealing.js";
2
+ import { deriveOAuthHandoffKey, deriveRequestStateKey, deriveStorageDigestKey } from "./oauth-sealing.js";
3
3
  import { credentialKeys } from "./storage/keys.js";
4
+ import { registerCredentialReads } from "./credential-reads.js";
4
5
  const KEY_BYTES = 32;
5
6
  const IV_BYTES = 12;
6
7
  const MAX_CREDENTIAL_BYTES = 16_384;
@@ -50,7 +51,7 @@ function parseEnvelope(raw) {
50
51
  throw new Error("Stored credential is invalid or corrupted");
51
52
  }
52
53
  }
53
- function parsePlaintext(raw) {
54
+ function parsePlaintext(raw, iv) {
54
55
  try {
55
56
  const parsed = JSON.parse(raw);
56
57
  const values = typeof parsed.value === "string" ? { value: parsed.value } : parsed.values;
@@ -66,6 +67,7 @@ function parsePlaintext(raw) {
66
67
  values: { ...values },
67
68
  updatedAt: parsed.updatedAt,
68
69
  updatedBy: parsed.updatedBy,
70
+ revision: typeof parsed.revision === "string" ? parsed.revision : iv,
69
71
  };
70
72
  }
71
73
  catch {
@@ -91,6 +93,28 @@ function validateValues(values) {
91
93
  }
92
94
  return normalized;
93
95
  }
96
+ function metadataOf(credential) {
97
+ if (!credential)
98
+ return null;
99
+ const fields = Object.fromEntries(Object.entries(credential.values).map(([field, value]) => [
100
+ field,
101
+ {
102
+ configured: true,
103
+ // Only reveal the tail on values comfortably longer than four chars;
104
+ // for a short secret those four would be half of it.
105
+ ...(value.length >= 12 ? { lastFour: value.slice(-4) } : {}),
106
+ updatedAt: credential.updatedAt,
107
+ },
108
+ ]));
109
+ const single = fields.value;
110
+ return {
111
+ configured: true,
112
+ ...(single?.lastFour ? { lastFour: single.lastFour } : {}),
113
+ updatedAt: credential.updatedAt,
114
+ revision: credential.revision,
115
+ fields,
116
+ };
117
+ }
94
118
  /**
95
119
  * Encrypted, connector-scoped credential vault over the deployment's existing
96
120
  * KVStorage. Only ciphertext enters storage; the AES-GCM key remains an environment
@@ -102,6 +126,7 @@ export class CredentialVault {
102
126
  key;
103
127
  handoffKey;
104
128
  retryKey;
129
+ digestKey;
105
130
  constructor(storage, encryptionKey) {
106
131
  this.storage = storage;
107
132
  const raw = base64ToBytes(encryptionKey.trim());
@@ -110,7 +135,16 @@ export class CredentialVault {
110
135
  }
111
136
  this.handoffKey = deriveOAuthHandoffKey(raw);
112
137
  this.retryKey = deriveRequestStateKey(raw);
138
+ this.digestKey = deriveStorageDigestKey(raw);
113
139
  this.key = crypto.subtle.importKey("raw", raw, { name: "AES-GCM" }, false, ["encrypt", "decrypt"]);
140
+ // The base class's original methods, so an override is never replaced.
141
+ registerCredentialReads(this, canonicalReads, {
142
+ getAll: async (connectorId, owner, reads, fresh) => {
143
+ const credential = await this.read(connectorId, owner, { reads, fresh });
144
+ return credential ? { ...credential.values } : null;
145
+ },
146
+ metadata: async (connectorId, owner, reads, fresh) => metadataOf(await this.read(connectorId, owner, { reads, fresh })),
147
+ });
114
148
  }
115
149
  async signOAuthHandoff(payload) {
116
150
  return bytesToBase64(new Uint8Array(await crypto.subtle.sign("HMAC", await this.handoffKey, encoder.encode(payload))));
@@ -118,6 +152,9 @@ export class CredentialVault {
118
152
  async requestStateKey() {
119
153
  return new Uint8Array(await this.retryKey);
120
154
  }
155
+ storageDigestKey() {
156
+ return this.digestKey;
157
+ }
121
158
  async verifyOAuthHandoff(payload, signature) {
122
159
  try {
123
160
  return await crypto.subtle.verify("HMAC", await this.handoffKey, base64ToBytes(signature), encoder.encode(payload));
@@ -129,8 +166,11 @@ export class CredentialVault {
129
166
  additionalData(connectorId, owner) {
130
167
  return encoder.encode(owner ? `connecta:credential:principal:${owner}:${connectorId}:v1` : `connecta:credential:${connectorId}:v1`);
131
168
  }
132
- async read(connectorId, owner) {
133
- const raw = await this.storage.get(storageKey(connectorId, owner));
169
+ async read(connectorId, owner, memo) {
170
+ const key = storageKey(connectorId, owner);
171
+ const raw = memo
172
+ ? await memo.reads.readPromise(this.storage, key, () => this.storage.get(key), memo.fresh)
173
+ : await this.storage.get(key);
134
174
  if (!raw)
135
175
  return null;
136
176
  const envelope = parseEnvelope(raw);
@@ -140,7 +180,7 @@ export class CredentialVault {
140
180
  iv: base64ToBytes(envelope.iv),
141
181
  additionalData: this.additionalData(connectorId, owner),
142
182
  }, await this.key, base64ToBytes(envelope.ciphertext));
143
- return parsePlaintext(decoder.decode(plaintext));
183
+ return parsePlaintext(decoder.decode(plaintext), envelope.iv);
144
184
  }
145
185
  catch {
146
186
  throw new Error("Stored credential could not be decrypted");
@@ -155,26 +195,12 @@ export class CredentialVault {
155
195
  return credential ? { ...credential.values } : null;
156
196
  }
157
197
  async metadata(connectorId, owner) {
198
+ return metadataOf(await this.read(connectorId, owner));
199
+ }
200
+ /** The values and metadata of one save, from a single envelope read. */
201
+ async snapshot(connectorId, owner) {
158
202
  const credential = await this.read(connectorId, owner);
159
- if (!credential)
160
- return null;
161
- const fields = Object.fromEntries(Object.entries(credential.values).map(([field, value]) => [
162
- field,
163
- {
164
- configured: true,
165
- // Only reveal the tail on values comfortably longer than four chars;
166
- // for a short secret those four would be half of it.
167
- ...(value.length >= 12 ? { lastFour: value.slice(-4) } : {}),
168
- updatedAt: credential.updatedAt,
169
- },
170
- ]));
171
- const single = fields.value;
172
- return {
173
- configured: true,
174
- ...(single?.lastFour ? { lastFour: single.lastFour } : {}),
175
- updatedAt: credential.updatedAt,
176
- fields,
177
- };
203
+ return credential && { values: { ...credential.values }, metadata: metadataOf(credential) };
178
204
  }
179
205
  async set(connectorId, value, updatedBy, owner) {
180
206
  // `await` (not a bare promise return) so a validation throw inside setAll
@@ -188,6 +214,7 @@ export class CredentialVault {
188
214
  values: normalized,
189
215
  updatedAt: new Date().toISOString(),
190
216
  updatedBy,
217
+ revision: crypto.randomUUID(),
191
218
  };
192
219
  const iv = crypto.getRandomValues(new Uint8Array(IV_BYTES));
193
220
  const ciphertext = await crypto.subtle.encrypt({
@@ -203,7 +230,9 @@ export class CredentialVault {
203
230
  };
204
231
  await this.storage.set(storageKey(connectorId, owner), JSON.stringify(envelope));
205
232
  advanceCatalogRevision(this);
206
- return (await this.metadata(connectorId, owner));
233
+ // What this write stored, never a re-read: a concurrent save may already
234
+ // have replaced it, and its revision must not be reported as this one's.
235
+ return metadataOf(plaintext);
207
236
  }
208
237
  async delete(connectorId, owner) {
209
238
  await this.storage.delete(storageKey(connectorId, owner));
@@ -238,6 +267,12 @@ export class CredentialVault {
238
267
  }
239
268
  }
240
269
  }
270
+ // Captured once at definition: a later subclass, instance or prototype
271
+ // override of either method is always called itself (credential-reads.ts).
272
+ const canonicalReads = {
273
+ getAll: CredentialVault.prototype.getAll,
274
+ metadata: CredentialVault.prototype.metadata,
275
+ };
241
276
  export function encryptedCredentialVault(storage, encryptionKey) {
242
277
  return new CredentialVault(storage, encryptionKey);
243
278
  }
package/dist/d1.d.ts CHANGED
@@ -38,93 +38,4 @@ export declare function d1Storage(db: D1DatabaseBinding): KVStorage;
38
38
  * write prunes a bounded batch of rows older than `retentionDays`.
39
39
  */
40
40
  export declare function d1ActivityStore(db: D1DatabaseBinding, options?: D1ActivityOptions): ActivityStore;
41
- /**
42
- * The part of a Workers KV binding `copyKvToD1` reads. Structural, like
43
- * `D1DatabaseBinding`; a Worker's `KVNamespace` satisfies it.
44
- */
45
- export interface KVNamespaceBinding {
46
- list(options?: {
47
- limit?: number;
48
- cursor?: string | null;
49
- }): Promise<{
50
- keys: readonly {
51
- name: string;
52
- expiration?: number;
53
- }[];
54
- list_complete: boolean;
55
- cursor?: string;
56
- }>;
57
- get(key: string, type: "text"): Promise<string | null>;
58
- }
59
- export interface KvToD1CopyOptions {
60
- /** The source KV namespace id. Resume tokens are bound to this identity. */
61
- source: string;
62
- /** Resume against the same source and database within seven days. */
63
- cursor?: string;
64
- /** Explicit families whose conflicting D1 rows the operator judged stale. */
65
- overwriteFamilies?: readonly string[];
66
- /** Compare value hashes and absolute expiries without copying entries. */
67
- verify?: boolean;
68
- /** Override the cutover guard only after assessing stale KV state. */
69
- allowStale?: boolean;
70
- /** Keys read per call. Default 500; use a smaller budget for large rows. */
71
- maxKeys?: number;
72
- }
73
- /** What a copy did with one family's keys. */
74
- export interface KvToD1Counts {
75
- /** Written where D1 held no live entry. */
76
- copied: number;
77
- /** D1 already held the same value. */
78
- unchanged: number;
79
- /** D1 held a different value or expiry and kept it. */
80
- conflicts: number;
81
- /** D1 held a different value or expiry, replaced under `overwriteFamilies`. */
82
- overwritten: number;
83
- /** Expired or deleted in Workers KV before it could be read. */
84
- expired: number;
85
- /** A key/value/row D1 cannot hold, left in Workers KV. */
86
- invalid: number;
87
- /** Exact value hash and expiry match during a verification pass. */
88
- verified: number;
89
- /** Missing or different value/expiry during a verification pass. */
90
- mismatches: number;
91
- }
92
- export interface KvToD1CopyResult {
93
- /** True once the whole namespace has been read. */
94
- done: boolean;
95
- /**
96
- * Pass back as `options.cursor` to continue; absent when `done`. A random
97
- * token: the Workers KV cursor it stands for stays in D1, because that
98
- * cursor can spell the last key listed.
99
- */
100
- cursor?: string;
101
- /**
102
- * Counts by key family (`src/storage/keys.ts`), plus `connector-owned` and
103
- * `unclassified`. Never a key or a value.
104
- */
105
- families: Record<string, KvToD1Counts>;
106
- }
107
- /**
108
- * A copy that stopped. The message is fixed wording that names a family at
109
- * most, never a key or value, and the binding's own error is not attached:
110
- * KV and D1 errors can quote either. Entries before `cursor` are copied;
111
- * rerunning from it is safe while the maintenance window still holds.
112
- */
113
- export declare class KvToD1CopyError extends Error {
114
- /** Resume point; absent means start over, which is also safe. */
115
- readonly cursor: string | undefined;
116
- readonly name = "KvToD1CopyError";
117
- constructor(message: string,
118
- /** Resume point; absent means start over, which is also safe. */
119
- cursor: string | undefined);
120
- }
121
- /** Mark cutover before reopening traffic. The marker stays in D1 permanently. */
122
- export declare function markKvToD1Live(db: D1DatabaseBinding, source: string): Promise<void>;
123
- /**
124
- * Copy live KV entries verbatim while all writers and traffic are stopped.
125
- * A live D1 row is kept unless its family is explicitly allowed to overwrite.
126
- * `verify` reads hashes and absolute expiries without changing copied entries.
127
- * Results contain counts only. Resume tokens are source-bound atomic claims.
128
- */
129
- export declare function copyKvToD1(kv: KVNamespaceBinding, db: D1DatabaseBinding, options: KvToD1CopyOptions): Promise<KvToD1CopyResult>;
130
41
  export {};
package/dist/d1.js CHANGED
@@ -3,12 +3,8 @@
3
3
  // One D1 database holds everything: `d1Storage(db)` is the deployment's
4
4
  // `KVStorage`, and `d1ActivityStore(db)` its activity history. Both create
5
5
  // their tables on first use, so binding the database is the whole setup.
6
- // `copyKvToD1` moves a 0.28 deployment's Workers KV state into it once.
7
- import { runEdge } from "./runtime/run.js";
8
6
  import { d1Driver } from "./storage/d1-driver.js";
9
- import { copyKvToD1Effect, markKvToD1LiveEffect } from "./storage/d1-copy.js";
10
7
  import { sqlActivityStore, sqlStorage } from "./storage/sql.js";
11
- import { keepErrorName } from "./redaction-identity.js";
12
8
  /**
13
9
  * `KVStorage` over a D1 database, with the atomic `compareAndSet` every
14
10
  * subsystem relies on. The `connecta_kv` table is created on first use; a
@@ -25,33 +21,3 @@ export function d1Storage(db) {
25
21
  export function d1ActivityStore(db, options) {
26
22
  return sqlActivityStore(d1Driver(db), "d1", options);
27
23
  }
28
- /**
29
- * A copy that stopped. The message is fixed wording that names a family at
30
- * most, never a key or value, and the binding's own error is not attached:
31
- * KV and D1 errors can quote either. Entries before `cursor` are copied;
32
- * rerunning from it is safe while the maintenance window still holds.
33
- */
34
- export class KvToD1CopyError extends Error {
35
- cursor;
36
- name = "KvToD1CopyError";
37
- constructor(message,
38
- /** Resume point; absent means start over, which is also safe. */
39
- cursor) {
40
- super(message);
41
- this.cursor = cursor;
42
- }
43
- }
44
- keepErrorName(KvToD1CopyError, "KvToD1CopyError");
45
- /** Mark cutover before reopening traffic. The marker stays in D1 permanently. */
46
- export function markKvToD1Live(db, source) {
47
- return runEdge(markKvToD1LiveEffect(db, source));
48
- }
49
- /**
50
- * Copy live KV entries verbatim while all writers and traffic are stopped.
51
- * A live D1 row is kept unless its family is explicitly allowed to overwrite.
52
- * `verify` reads hashes and absolute expiries without changing copied entries.
53
- * Results contain counts only. Resume tokens are source-bound atomic claims.
54
- */
55
- export function copyKvToD1(kv, db, options) {
56
- return runEdge(copyKvToD1Effect(kv, db, options));
57
- }
@@ -1,6 +1,6 @@
1
1
  import { CONFIG_DEFAULTS } from "./config-defaults.js";
2
2
  import type { ConnectaConfig, ResolvedConfig } from "./config.js";
3
- import type { ResolvedPool } from "./membership.js";
3
+ import { type ResolvedPool } from "./membership.js";
4
4
  import type { ConnectorAuthDescription, ConnectorDescription, ConnectorToolDescription, DescribedEndpoint, IdentityReference } from "./types.js";
5
5
  /** Where a limit's value came from. */
6
6
  export type ConfigValueSource = "default" | "config";
@@ -100,11 +100,21 @@ export interface ConnectaConfigDescription {
100
100
  };
101
101
  /** Whether the default /mcp endpoint refuses every write, and exact classification overrides from deployment code. */
102
102
  readOnly: boolean;
103
- /** The principals plain `/mcp` admits, resolved, in declaration order. Empty on an open deployment. */
103
+ /**
104
+ * The people plain `/mcp` serves the whole catalog, resolved, in declaration
105
+ * order. Empty on an open deployment. Every other person reaches plain
106
+ * `/mcp` only through exactly one pool's `members`, which it then serves.
107
+ */
104
108
  maintainers: Array<{
105
109
  namespace: string;
106
110
  id: string;
107
111
  }>;
112
+ /** Non-maintainer people listed in several pools, whom plain `/mcp` refuses. */
113
+ peopleInSeveralPools: Array<{
114
+ namespace: string;
115
+ id: string;
116
+ pools: string[];
117
+ }>;
108
118
  classification: Record<string, Record<string, "read" | "write" | "hidden">>;
109
119
  /** Inbound auth providers in the order they are tried. */
110
120
  auth: Array<{
@@ -15,6 +15,7 @@ import { resolveBranding } from "./branding.js";
15
15
  import { CONFIG_DEFAULTS } from "./config-defaults.js";
16
16
  import { describedEndpoint, describedHref, describedOrigin, describedTools, describedUrl } from "./described.js";
17
17
  import { connectorGuideRequired, connectorGuideSummary } from "./skills.js";
18
+ import { peopleInSeveralPools } from "./membership.js";
18
19
  import { CONNECTA_VERSION } from "./version.js";
19
20
  const str = (value) => (typeof value === "string" ? value : undefined);
20
21
  const num = (value) => typeof value === "number" && Number.isFinite(value) ? value : undefined;
@@ -235,6 +236,7 @@ export function describeConfig(input) {
235
236
  },
236
237
  readOnly: config.readOnly,
237
238
  maintainers: input.maintainers.map(({ namespace, id }) => ({ namespace: String(namespace), id: String(id) })),
239
+ peopleInSeveralPools: peopleInSeveralPools(input.pools, input.maintainers),
238
240
  classification: structuredClone(config.classification ?? {}),
239
241
  auth: config.auth.map((provider) => ({
240
242
  kind: String(provider.kind),