run402 4.63.0 → 4.64.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 (53) hide show
  1. package/gitvault-surface.json +2 -1
  2. package/lib/doctor.mjs +28 -15
  3. package/lib/gitvault-capabilities.mjs +8 -0
  4. package/lib/remote-helper-session.mjs +166 -32
  5. package/lib/repos.mjs +122 -9
  6. package/package.json +1 -1
  7. package/sdk/dist/errors.d.ts +4 -2
  8. package/sdk/dist/errors.d.ts.map +1 -1
  9. package/sdk/dist/errors.js.map +1 -1
  10. package/sdk/dist/index.d.ts +1 -1
  11. package/sdk/dist/index.d.ts.map +1 -1
  12. package/sdk/dist/index.js +1 -1
  13. package/sdk/dist/index.js.map +1 -1
  14. package/sdk/dist/namespaces/gitvault.crypto.d.ts +80 -0
  15. package/sdk/dist/namespaces/gitvault.crypto.d.ts.map +1 -1
  16. package/sdk/dist/namespaces/gitvault.crypto.js +80 -0
  17. package/sdk/dist/namespaces/gitvault.crypto.js.map +1 -1
  18. package/sdk/dist/namespaces/gitvault.d.ts +219 -3
  19. package/sdk/dist/namespaces/gitvault.d.ts.map +1 -1
  20. package/sdk/dist/namespaces/gitvault.js +282 -5
  21. package/sdk/dist/namespaces/gitvault.js.map +1 -1
  22. package/sdk/dist/node/gitvault-byo-config.d.ts +29 -0
  23. package/sdk/dist/node/gitvault-byo-config.d.ts.map +1 -0
  24. package/sdk/dist/node/gitvault-byo-config.js +100 -0
  25. package/sdk/dist/node/gitvault-byo-config.js.map +1 -0
  26. package/sdk/dist/node/gitvault-byo-probe.d.ts +20 -0
  27. package/sdk/dist/node/gitvault-byo-probe.d.ts.map +1 -0
  28. package/sdk/dist/node/gitvault-byo-probe.js +57 -0
  29. package/sdk/dist/node/gitvault-byo-probe.js.map +1 -0
  30. package/sdk/dist/node/gitvault-creation-journal.d.ts +43 -0
  31. package/sdk/dist/node/gitvault-creation-journal.d.ts.map +1 -1
  32. package/sdk/dist/node/gitvault-creation-journal.js +34 -3
  33. package/sdk/dist/node/gitvault-creation-journal.js.map +1 -1
  34. package/sdk/dist/node/gitvault-degraded-read.d.ts +120 -0
  35. package/sdk/dist/node/gitvault-degraded-read.d.ts.map +1 -0
  36. package/sdk/dist/node/gitvault-degraded-read.js +164 -0
  37. package/sdk/dist/node/gitvault-degraded-read.js.map +1 -0
  38. package/sdk/dist/node/gitvault-mirror-backend.d.ts +73 -0
  39. package/sdk/dist/node/gitvault-mirror-backend.d.ts.map +1 -1
  40. package/sdk/dist/node/gitvault-mirror-backend.js +114 -8
  41. package/sdk/dist/node/gitvault-mirror-backend.js.map +1 -1
  42. package/sdk/dist/node/gitvault-mirror-config.d.ts +17 -1
  43. package/sdk/dist/node/gitvault-mirror-config.d.ts.map +1 -1
  44. package/sdk/dist/node/gitvault-mirror-config.js +21 -0
  45. package/sdk/dist/node/gitvault-mirror-config.js.map +1 -1
  46. package/sdk/dist/node/gitvault-mirror.d.ts +42 -0
  47. package/sdk/dist/node/gitvault-mirror.d.ts.map +1 -1
  48. package/sdk/dist/node/gitvault-mirror.js +162 -1
  49. package/sdk/dist/node/gitvault-mirror.js.map +1 -1
  50. package/sdk/dist/node/gitvault-publication.d.ts +51 -1
  51. package/sdk/dist/node/gitvault-publication.d.ts.map +1 -1
  52. package/sdk/dist/node/gitvault-publication.js +88 -6
  53. package/sdk/dist/node/gitvault-publication.js.map +1 -1
@@ -38,6 +38,7 @@ import { tmpdir } from "node:os";
38
38
  import { join } from "node:path";
39
39
  import { LocalError, isRun402Error } from "../errors.js";
40
40
  import { fetchGitvaultObjectBytes } from "./gitvault-edge-fetch.js";
41
+ import { openGitvaultDestinationBackend } from "./gitvault-mirror-backend.js";
41
42
  import { GITVAULT_FORMAT, GITVAULT_GENESIS_EPOCH, GITVAULT_GENESIS_GENERATION, GITVAULT_HEX16_RE, GITVAULT_OID40_RE, GITVAULT_SUITE, attemptKeyCommitment, bytesToHex, checkFreshEpochKeyAgainstPriorKeys, checkHPartition, computeRotationId, computeTargetPartitionDigest, deriveDigestKey, deriveObjectKey, ekFingerprint, epochRotationKeyCommitment, formatGitvaultTimestamp, fromBase64url, hexToBytes, jcs, keyEnvelopeLedgerId, keyedCommitment, newGitvaultId, newHex32, nextEpoch, objectsetContent, openBindingPreimage, openEpochRotationForRecipient, openFrame, parseGitvaultStrict, parseRotateEpochPayload, pinManifestLedgerId, randomBytes, sealFrame, sealKeyEnvelope, sha256Hex, signGitvaultObject, storedBytes, storedBytesSha256, toBase64url, verifyGitvaultObject, } from "../namespaces/gitvault.crypto.js";
42
43
  import { GITVAULT_ZERO_SHA256_SENTINEL } from "../namespaces/gitvault.types.js";
43
44
  import { gitvaultCheckpointStaleness } from "../namespaces/gitvault.js";
@@ -961,14 +962,20 @@ export function createGitvaultHttpTransport(client, options = {}) {
961
962
  return { path: o.path, object_id: o.object_id, sha256: r.ciphertext_sha256 ?? r.stored_bytes_sha256 ?? "", size_bytes: r.size_bytes };
962
963
  });
963
964
  }
964
- async function upload(repoId, objects, resourceBinding) {
965
+ async function upload(repoId, objects, resourceBinding, byo) {
965
966
  if (objects.length === 0)
966
967
  return [];
967
968
  // The manifest is closed-key: `path` is client-local and MUST NOT ride the
968
969
  // wire — the control plane derives the bucket key itself and refuses an
969
970
  // entry carrying an unexpected member.
970
971
  const entries = objects.map((o) => gitvaultManifestEntry(o));
971
- if (gitvaultInlineUploadEligible(objects)) {
972
+ // gitvault-byo-primary-bucket task 3.2: the inline (bytes-in-body) fast
973
+ // path is NEVER used for a BYO vault, even when the payload is small
974
+ // enough to be eligible — that transport writes bytes into run402's own
975
+ // bucket, exactly what the credential model forbids (the gateway
976
+ // refuses it too, VALIDATION_FAILED, so skipping it here is purely a
977
+ // wasted-round-trip avoidance, not a correctness dependency).
978
+ if (!byo && gitvaultInlineUploadEligible(objects)) {
972
979
  // gitvault-composite-state-read design D2: every object fits under the
973
980
  // caps — one POST, bytes verified + written server-side, and the
974
981
  // response IS the finalize response (no session, no PUTs, no separate
@@ -987,6 +994,14 @@ export function createGitvaultHttpTransport(client, options = {}) {
987
994
  context: "opening gitvault upload session",
988
995
  });
989
996
  const issued = new Map(session.objects.map((u) => [gitvaultLedgerId(u), u]));
997
+ // gitvault-byo-primary-bucket task 3.2 — one backend, opened ONCE, for
998
+ // every BYO payload object this session names; `null` `put` never
999
+ // appears in the session response unless `byo` was supplied (the
1000
+ // gateway only mints `put: null` for a vault whose storage_profile is
1001
+ // "byo", which is exactly when THIS caller must have supplied `byo`
1002
+ // too — see the resolution in `GitvaultVault.uploadAll`).
1003
+ const byoBackend = byo ? openGitvaultDestinationBackend(byo.destination, byo.credential) : null;
1004
+ const byoAttestations = [];
990
1005
  // Independent create-only PUTs within one session (design D2) — bounded
991
1006
  // concurrency, same limit as the batched object reads below.
992
1007
  await mapBounded(objects, GITVAULT_TRANSPORT_CONCURRENCY, async (o, i) => {
@@ -994,6 +1009,36 @@ export function createGitvaultHttpTransport(client, options = {}) {
994
1009
  const target = issued.get(id);
995
1010
  if (!target)
996
1011
  fail("GITVAULT_UPLOAD_SESSION_INVALID", `the session issued no upload for ${id}`, "uploading gitvault objects", { object_id: id, path: o.path });
1012
+ if (target.put === null) {
1013
+ // BYO payload object (task 2.2's GITVAULT_BYO_PAYLOAD_KINDS
1014
+ // partition, mirrored client-side by the server's own `put: null` +
1015
+ // `key` shape) — the client writes DIRECTLY to its own bucket. No
1016
+ // request to run402 ever carries a customer-bucket credential; no
1017
+ // request to run402 ever carries these bytes either.
1018
+ if (!byoBackend || !target.key) {
1019
+ fail("GITVAULT_BYO_BUCKET_WRITE_REFUSED", `the session named ${o.path} as a BYO-written object but no BYO write target was resolved`, "uploading gitvault objects", { path: o.path, object_id: id });
1020
+ }
1021
+ let put;
1022
+ try {
1023
+ put = await byoBackend.putCreateOnly(target.key, o.bytes);
1024
+ }
1025
+ catch (e) {
1026
+ fail("GITVAULT_BYO_BUCKET_WRITE_REFUSED", `writing ${o.path} to the BYO destination failed: ${e instanceof Error ? e.message : String(e)}`, "uploading gitvault objects", { path: o.path, key: target.key });
1027
+ }
1028
+ if (!put.created) {
1029
+ // create-only: the key already exists — legal ONLY if it is
1030
+ // byte-identical (an idempotent retry of THIS SAME write; a
1031
+ // permanent object id is never legitimately reused for
1032
+ // different bytes — matches the managed presigned-PUT read-and-
1033
+ // compare branch below, and the gateway's own attestation rule).
1034
+ const existing = await byoBackend.get(target.key);
1035
+ if (!existing || sha256Hex(existing) !== o.sha256) {
1036
+ fail("GITVAULT_BYO_BUCKET_WRITE_REFUSED", `${o.path} already exists in the BYO destination with different bytes`, "uploading gitvault objects", { path: o.path, key: target.key });
1037
+ }
1038
+ }
1039
+ byoAttestations.push({ key: target.key, sha256: o.sha256, size_bytes: o.size_bytes, create_only_result: "created" });
1040
+ return;
1041
+ }
997
1042
  // `If-None-Match: *` (create-only) and the FULL_OBJECT `x-amz-checksum-sha256`
998
1043
  // are SIGNED INTO the presigned URL, so they must go out exactly as the
999
1044
  // server issued them — a dropped or altered header is a signature
@@ -1015,7 +1060,13 @@ export function createGitvaultHttpTransport(client, options = {}) {
1015
1060
  if (!r.ok)
1016
1061
  fail("GITVAULT_UPLOAD_FAILED", `presigned PUT failed (HTTP ${r.status}) for ${o.path}`, "uploading gitvault objects", { path: o.path, status: r.status });
1017
1062
  });
1018
- const fin = await client.request(`${base(repoId)}/upload-sessions/${encodeURIComponent(session.upload_session_id)}/finalize`, { method: "POST", body: {}, context: "finalizing gitvault upload session" });
1063
+ const fin = await client.request(`${base(repoId)}/upload-sessions/${encodeURIComponent(session.upload_session_id)}/finalize`, {
1064
+ method: "POST",
1065
+ // Only a BYO session ever carries attestations — an ordinary managed
1066
+ // session's finalize body stays `{}`, byte-identical to today.
1067
+ body: byo ? { attestations: byoAttestations } : {},
1068
+ context: "finalizing gitvault upload session",
1069
+ });
1019
1070
  return receiptsFromFinalize(objects, entries, fin);
1020
1071
  }
1021
1072
  async function admit(repoId, generation, bytes, hash, extra = {}) {
@@ -1046,7 +1097,7 @@ export function createGitvaultHttpTransport(client, options = {}) {
1046
1097
  return res.allocation ?? res;
1047
1098
  },
1048
1099
  async putObject(request) {
1049
- const [r] = await upload(request.repo_id, [{ path: request.path, object_kind: "key_envelope", object_id: null, bytes: request.bytes, sha256: request.expected_sha256, size_bytes: request.expected_size_bytes }]);
1100
+ const [r] = await upload(request.repo_id, [{ path: request.path, object_kind: "key_envelope", object_id: null, bytes: request.bytes, sha256: request.expected_sha256, size_bytes: request.expected_size_bytes }], undefined, request.byo);
1050
1101
  return { stored_bytes_sha256: r.sha256, size_bytes: r.size_bytes };
1051
1102
  },
1052
1103
  getObject: ({ repo_id, path }) => getObjectBytes(repo_id, path),
@@ -1075,7 +1126,7 @@ export function createGitvaultHttpTransport(client, options = {}) {
1075
1126
  qs.set("cursor", request.cursor);
1076
1127
  return client.request(`${base(request.repo_id)}/heads?${qs.toString()}`, { context: "listing gitvault heads" });
1077
1128
  },
1078
- uploadObjects: ({ repo_id, objects, resource_binding }) => upload(repo_id, objects, resource_binding),
1129
+ uploadObjects: ({ repo_id, objects, resource_binding, byo }) => upload(repo_id, objects, resource_binding, byo),
1079
1130
  admitHead: (r) => admit(r.repo_id, r.generation, r.stored_bytes, r.stored_bytes_sha256),
1080
1131
  // Both routes SHIPPED (`routes/gitvault-admission.ts`). `retention-cutoffs`
1081
1132
  // answers `{ticket, receipt, next_actions}`, which IS
@@ -1413,6 +1464,16 @@ export class GitvaultVault {
1413
1464
  retries;
1414
1465
  servicePublicKey;
1415
1466
  genesisCache = null;
1467
+ /**
1468
+ * gitvault-byo-primary-bucket task 3.2 — this vault's resolved BYO write
1469
+ * target, cached after the first resolution (storage_profile/
1470
+ * byo_destination are immutable-at-allocation in v1, so a single
1471
+ * read-once-per-instance is safe). `undefined` = not yet resolved;
1472
+ * `null` = this machine has no LOCAL BYO write config for this repo
1473
+ * (either an ordinary managed vault, or a BYO vault this machine has not
1474
+ * been configured to write — see {@link resolveByoWriteTarget}).
1475
+ */
1476
+ byoResolution = undefined;
1416
1477
  constructor(options) {
1417
1478
  this.keystore = options.keystore;
1418
1479
  this.transport = options.transport;
@@ -2379,12 +2440,14 @@ export class GitvaultVault {
2379
2440
  created_at: formatGitvaultTimestamp(this.now()),
2380
2441
  });
2381
2442
  try {
2443
+ const byo = await this.resolveByoWriteTarget();
2382
2444
  await this.transport.putObject({
2383
2445
  repo_id: this.repoId,
2384
2446
  path: gitvaultPaths.envelope(epoch, sealed.receipt.recipient_fingerprint),
2385
2447
  bytes: sealed.stored_bytes,
2386
2448
  expected_sha256: sealed.stored_bytes_sha256,
2387
2449
  expected_size_bytes: sealed.size_bytes,
2450
+ ...(byo ? { byo } : {}),
2388
2451
  });
2389
2452
  wrapped.push({ principal_id: entry.principal_id, ek_fingerprint: entry.ek_fingerprint });
2390
2453
  }
@@ -2751,8 +2814,27 @@ export class GitvaultVault {
2751
2814
  return out;
2752
2815
  }
2753
2816
  // ── upload with receipt-compare ──
2817
+ /**
2818
+ * gitvault-byo-primary-bucket task 3.2 — resolve (once, cached) this
2819
+ * machine's local BYO write config for THIS vault. `null` when none is
2820
+ * configured locally — either because this is an ordinary managed vault
2821
+ * (the common case; deliberately never confirmed with a network call, so
2822
+ * a managed vault's push pays zero extra round trips), or because this
2823
+ * machine has not been configured to write a BYO vault it nonetheless
2824
+ * belongs to (surfaced downstream as `GITVAULT_BYO_BUCKET_WRITE_REFUSED`
2825
+ * when the session actually names a `put: null` object with no target).
2826
+ */
2827
+ async resolveByoWriteTarget() {
2828
+ if (this.byoResolution !== undefined)
2829
+ return this.byoResolution;
2830
+ const { readByoConfig } = await import("./gitvault-byo-config.js");
2831
+ const local = readByoConfig(this.keystore, this.repoId);
2832
+ this.byoResolution = local ? { destination: local.destination, ...(local.credential ? { credential: local.credential } : {}) } : null;
2833
+ return this.byoResolution;
2834
+ }
2754
2835
  async uploadAll(objects) {
2755
- const receipts = await this.transport.uploadObjects({ repo_id: this.repoId, objects });
2836
+ const byo = await this.resolveByoWriteTarget();
2837
+ const receipts = await this.transport.uploadObjects({ repo_id: this.repoId, objects, ...(byo ? { byo } : {}) });
2756
2838
  if (receipts.length !== objects.length)
2757
2839
  fail("GITVAULT_RECEIPT_MISMATCH", `${receipts.length} receipts for ${objects.length} objects`, "comparing finalization receipts");
2758
2840
  const mismatches = objects.filter((o, i) => receipts[i].path !== o.path || receipts[i].sha256 !== o.sha256 || receipts[i].size_bytes !== o.size_bytes);