@agentconnect.md/setup 1.37.0-rc.4 → 1.37.0-rc.5

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/dist/index.js CHANGED
@@ -56521,6 +56521,26 @@ async function disconnectPrisma() {
56521
56521
  await singleton?.$disconnect();
56522
56522
  singleton = void 0;
56523
56523
  }
56524
+ //#endregion
56525
+ //#region ../control-plane/dist/secrets/scope.js
56526
+ /** Deployment-owned material (the deployment config document's secrets). */
56527
+ const DEPLOYMENT_SCOPE = { kind: "deployment" };
56528
+ /**
56529
+ * Envelope marker for values sealed under a SCOPED key. It carries a format
56530
+ * version and nothing else — deliberately not the tenant id (see above). Its
56531
+ * one job is to let `open` tell a scoped value apart from a pre-scoping one, so
56532
+ * the rollout needs no backfill.
56533
+ */
56534
+ const SECRET_ENVELOPE_PREFIX = "acv1:";
56535
+ /**
56536
+ * Org key names default to `<deployment key>-org-`, so they inherit whatever
56537
+ * namespace the deployment key already occupies. Deployments commonly share one
56538
+ * transit mount and rely on key naming alone to stay separated; a fixed prefix
56539
+ * would collide their org keys, a derived one cannot.
56540
+ */
56541
+ function effectiveOrgKeyPrefix(transitKey, configured) {
56542
+ return configured ?? `${transitKey}-org-`;
56543
+ }
56524
56544
  const HttpUrlSchema = string().url().superRefine((value, ctx) => {
56525
56545
  const url = new URL(value);
56526
56546
  if (url.protocol !== "http:" && url.protocol !== "https:") ctx.addIssue({
@@ -56775,7 +56795,7 @@ var DeploymentConfigService = class {
56775
56795
  for (const [rawKey, value] of Object.entries(secretPatch)) {
56776
56796
  const key = DeploymentSecretKeySchema.parse(rawKey);
56777
56797
  prepared[key] = value === null ? null : {
56778
- sealedValue: await this.cipher.seal(value),
56798
+ sealedValue: await this.cipher.seal(value, DEPLOYMENT_SCOPE),
56779
56799
  fingerprint: fingerprint(value)
56780
56800
  };
56781
56801
  }
@@ -56796,7 +56816,7 @@ var DeploymentConfigService = class {
56796
56816
  const secrets = {};
56797
56817
  for (const secret of row.secrets) {
56798
56818
  const parsed = DeploymentSecretKeySchema.safeParse(secret.key);
56799
- if (parsed.success && openedKeys.has(parsed.data)) secrets[parsed.data] = await this.cipher.open(secret.sealedValue);
56819
+ if (parsed.success && openedKeys.has(parsed.data)) secrets[parsed.data] = await this.cipher.open(secret.sealedValue, DEPLOYMENT_SCOPE);
56800
56820
  }
56801
56821
  return {
56802
56822
  schemaVersion: 1,
@@ -56992,12 +57012,19 @@ var PgDeploymentConfigStore = class extends DeploymentConfigService {
56992
57012
  *
56993
57013
  * Envelope encryption as a service: the data key never leaves Vault; the CP
56994
57014
  * sends base64 plaintext to `transit/encrypt/<key>` and stores the returned
56995
- * self-describing `vault:vN:…` ciphertext in the existing text columns.
57015
+ * ciphertext, behind this deployment's envelope tag, in the existing text
57016
+ * columns. WHICH key is chosen comes from the {@link SecretScope} the caller
57017
+ * passes — the deployment's key, or `<orgKeyPrefix><orgId>` — so an
57018
+ * organization's material can be destroyed with its key
57019
+ * (docs/designs/per-org-secret-encryption.md).
56996
57020
  *
56997
57021
  * Contract (pinned on the port):
56998
- * - `open` PASSES THROUGH values it did not seal (no `vault:vN:` prefix
56999
- * return as-is): existing plaintext rows keep reading after the flip, and the
57000
- * next write re-seals them — the rollout is online, no backfill required.
57022
+ * - `open` PASSES THROUGH values it did not seal (neither the envelope tag nor
57023
+ * a bare `vault:vN:` prefix ⇒ return as-is): existing plaintext rows keep
57024
+ * reading after the flip, and the next write re-seals them — the rollout is
57025
+ * online, no backfill required.
57026
+ * - A value sealed under one scope does NOT open under another: Transit rejects
57027
+ * a ciphertext presented to a different key, which is the cross-tenant fence.
57001
57028
  * - No argument or response body is ever logged; errors carry only the HTTP
57002
57029
  * status and Vault's `errors[]` strings.
57003
57030
  *
@@ -57008,11 +57035,13 @@ var PgDeploymentConfigStore = class extends DeploymentConfigService {
57008
57035
  * that exact login wire shape ({role, jwt}), so nothing here is bound to
57009
57036
  * Kubernetes — a k8s ServiceAccount token is just the common jwtPath.
57010
57037
  *
57011
- * `open` results are cached in-process keyed by ciphertext (bounded, insertion-
57012
- * order eviction): reconcile opens every owned agent's secrets per register, and
57013
- * Transit ciphertexts are stable until re-sealed, so the cache turns that into
57014
- * one network call per distinct value. `seal` is never cached — Transit returns
57015
- * fresh ciphertext per call by design.
57038
+ * `open` results are cached in-process keyed by key name AND ciphertext
57039
+ * (bounded, insertion-order eviction): reconcile opens every owned agent's
57040
+ * secrets per register, and Transit ciphertexts are stable until re-sealed, so
57041
+ * the cache turns that into one network call per distinct value. The key name
57042
+ * belongs in the cache key because the stored string alone no longer implies
57043
+ * one. `seal` is never cached — Transit returns fresh ciphertext per call by
57044
+ * design.
57016
57045
  */
57017
57046
  /** Transit ciphertext is self-describing: `vault:v<key-version>:<base64>`. */
57018
57047
  const CIPHERTEXT_RE = /^vault:v\d+:/;
@@ -57021,6 +57050,7 @@ const RENEW_FRACTION = .8;
57021
57050
  var VaultTransitSecretCipher = class {
57022
57051
  base;
57023
57052
  key;
57053
+ orgKeyPrefix;
57024
57054
  mount;
57025
57055
  namespace;
57026
57056
  auth;
@@ -57033,6 +57063,7 @@ var VaultTransitSecretCipher = class {
57033
57063
  constructor(opts) {
57034
57064
  this.base = `${opts.addr.replace(/\/+$/, "")}/v1`;
57035
57065
  this.key = opts.key;
57066
+ this.orgKeyPrefix = opts.orgKeyPrefix;
57036
57067
  this.mount = opts.mount ?? "transit";
57037
57068
  this.namespace = opts.namespace;
57038
57069
  this.auth = opts.auth;
@@ -57040,19 +57071,31 @@ var VaultTransitSecretCipher = class {
57040
57071
  this.now = opts.now ?? Date.now;
57041
57072
  this.openCacheMax = opts.openCacheMax ?? 5e3;
57042
57073
  }
57043
- async seal(plaintext) {
57044
- const data = await this.transit("encrypt", { plaintext: Buffer.from(plaintext, "utf8").toString("base64") });
57045
- if (typeof data.ciphertext !== "string") throw new Error("vault transit encrypt: no ciphertext in response");
57046
- return data.ciphertext;
57074
+ /**
57075
+ * The key a scope resolves to. Org keys are created lazily by Transit on the
57076
+ * first encrypt (the policy must permit creation on that path); nothing here
57077
+ * is coupled to organization creation.
57078
+ */
57079
+ keyFor(scope) {
57080
+ return scope.kind === "deployment" ? this.key : `${this.orgKeyPrefix}${scope.orgId}`;
57047
57081
  }
57048
- async open(stored) {
57049
- if (!CIPHERTEXT_RE.test(stored)) return stored;
57050
- const hit = this.openCache.get(stored);
57082
+ async seal(plaintext, scope) {
57083
+ const data = await this.transit("encrypt", this.keyFor(scope), { plaintext: Buffer.from(plaintext, "utf8").toString("base64") });
57084
+ if (typeof data.ciphertext !== "string") throw new Error("vault transit encrypt: no ciphertext in response");
57085
+ return `${SECRET_ENVELOPE_PREFIX}${data.ciphertext}`;
57086
+ }
57087
+ async open(stored, scope) {
57088
+ const scoped = stored.startsWith(SECRET_ENVELOPE_PREFIX);
57089
+ if (!scoped && !CIPHERTEXT_RE.test(stored)) return stored;
57090
+ const key = scoped ? this.keyFor(scope) : this.key;
57091
+ const ciphertext = scoped ? stored.slice(5) : stored;
57092
+ const cacheKey = `${key}\u0000${ciphertext}`;
57093
+ const hit = this.openCache.get(cacheKey);
57051
57094
  if (hit !== void 0) return hit;
57052
- const data = await this.transit("decrypt", { ciphertext: stored });
57095
+ const data = await this.transit("decrypt", key, { ciphertext });
57053
57096
  if (typeof data.plaintext !== "string") throw new Error("vault transit decrypt: no plaintext in response");
57054
57097
  const value = Buffer.from(data.plaintext, "base64").toString("utf8");
57055
- this.cacheOpen(stored, value);
57098
+ this.cacheOpen(cacheKey, value);
57056
57099
  return value;
57057
57100
  }
57058
57101
  cacheOpen(ciphertext, plaintext) {
@@ -57062,8 +57105,8 @@ var VaultTransitSecretCipher = class {
57062
57105
  }
57063
57106
  this.openCache.set(ciphertext, plaintext);
57064
57107
  }
57065
- async transit(op, body) {
57066
- const path = `${this.mount}/${op}/${this.key}`;
57108
+ async transit(op, key, body) {
57109
+ const path = `${this.mount}/${op}/${key}`;
57067
57110
  let res = await this.post(path, body, await this.token());
57068
57111
  if (res.status === 403 && this.auth.method === "jwt") {
57069
57112
  this.clientToken = void 0;
@@ -57139,11 +57182,21 @@ async function describeError(res) {
57139
57182
  * as Vault Transit stores ciphertext. The composition root selects one provider,
57140
57183
  * so every store switches together.
57141
57184
  *
57185
+ * Every call carries a {@link SecretScope} naming WHOSE key to use — the
57186
+ * deployment's, or one organization's (docs/designs/per-org-secret-encryption.md).
57187
+ * The scope comes from the caller, never from the stored value.
57188
+ *
57142
57189
  * Contract for an encrypting implementation (e.g. Vault Transit):
57143
- * - `seal` returns a self-describing value (Transit's `vault:v1:…` is already one).
57190
+ * - `seal` returns a self-describing value carrying the envelope version
57191
+ * (`acv1:` + Transit's own `vault:v1:…`).
57144
57192
  * - `open` MUST pass through values it did not seal (no recognizable prefix ⇒
57145
57193
  * return as-is): existing plaintext rows keep working, and re-sealing on the
57146
57194
  * next write migrates them lazily — the flip is online, no backfill required.
57195
+ * Values sealed before scoping existed (a bare `vault:vN:`) open under the
57196
+ * deployment key whatever `scope` says; only the envelope-tagged arm is
57197
+ * scoped.
57198
+ * - A value sealed under one scope MUST NOT open under another. Failing closed
57199
+ * there is the point of passing the scope in.
57147
57200
  * - Neither side ever logs its argument.
57148
57201
  *
57149
57202
  * This is NOT the C5 `SecretsProvider` lease broker (`providers/provider.ts`),
@@ -57184,6 +57237,7 @@ function makeSecretCipher(config) {
57184
57237
  return new VaultTransitSecretCipher({
57185
57238
  addr: config.VAULT_ADDR,
57186
57239
  key: config.VAULT_TRANSIT_KEY,
57240
+ orgKeyPrefix: effectiveOrgKeyPrefix(config.VAULT_TRANSIT_KEY, config.VAULT_TRANSIT_ORG_KEY_PREFIX),
57187
57241
  mount: config.VAULT_TRANSIT_MOUNT,
57188
57242
  ...config.VAULT_NAMESPACE ? { namespace: config.VAULT_NAMESPACE } : {},
57189
57243
  auth