run402 4.63.1 → 4.65.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.
- package/gitvault-surface.json +2 -1
- package/lib/doctor.mjs +28 -15
- package/lib/gitvault-capabilities.mjs +8 -0
- package/lib/gitvault-daemon.mjs +30 -1
- package/lib/remote-helper-session.mjs +168 -33
- package/lib/repos.mjs +122 -9
- package/package.json +1 -1
- package/sdk/dist/errors.d.ts +4 -2
- package/sdk/dist/errors.d.ts.map +1 -1
- package/sdk/dist/errors.js.map +1 -1
- package/sdk/dist/index.d.ts +1 -1
- package/sdk/dist/index.d.ts.map +1 -1
- package/sdk/dist/index.js +1 -1
- package/sdk/dist/index.js.map +1 -1
- package/sdk/dist/namespaces/gitvault.crypto.d.ts +80 -0
- package/sdk/dist/namespaces/gitvault.crypto.d.ts.map +1 -1
- package/sdk/dist/namespaces/gitvault.crypto.js +80 -0
- package/sdk/dist/namespaces/gitvault.crypto.js.map +1 -1
- package/sdk/dist/namespaces/gitvault.d.ts +219 -3
- package/sdk/dist/namespaces/gitvault.d.ts.map +1 -1
- package/sdk/dist/namespaces/gitvault.js +290 -6
- package/sdk/dist/namespaces/gitvault.js.map +1 -1
- package/sdk/dist/node/gitvault-byo-config.d.ts +29 -0
- package/sdk/dist/node/gitvault-byo-config.d.ts.map +1 -0
- package/sdk/dist/node/gitvault-byo-config.js +100 -0
- package/sdk/dist/node/gitvault-byo-config.js.map +1 -0
- package/sdk/dist/node/gitvault-byo-probe.d.ts +20 -0
- package/sdk/dist/node/gitvault-byo-probe.d.ts.map +1 -0
- package/sdk/dist/node/gitvault-byo-probe.js +57 -0
- package/sdk/dist/node/gitvault-byo-probe.js.map +1 -0
- package/sdk/dist/node/gitvault-creation-journal.d.ts +55 -1
- package/sdk/dist/node/gitvault-creation-journal.d.ts.map +1 -1
- package/sdk/dist/node/gitvault-creation-journal.js +34 -3
- package/sdk/dist/node/gitvault-creation-journal.js.map +1 -1
- package/sdk/dist/node/gitvault-degraded-read.d.ts +120 -0
- package/sdk/dist/node/gitvault-degraded-read.d.ts.map +1 -0
- package/sdk/dist/node/gitvault-degraded-read.js +164 -0
- package/sdk/dist/node/gitvault-degraded-read.js.map +1 -0
- package/sdk/dist/node/gitvault-keystore.d.ts +34 -1
- package/sdk/dist/node/gitvault-keystore.d.ts.map +1 -1
- package/sdk/dist/node/gitvault-keystore.js +43 -0
- package/sdk/dist/node/gitvault-keystore.js.map +1 -1
- package/sdk/dist/node/gitvault-mirror-backend.d.ts +73 -0
- package/sdk/dist/node/gitvault-mirror-backend.d.ts.map +1 -1
- package/sdk/dist/node/gitvault-mirror-backend.js +114 -8
- package/sdk/dist/node/gitvault-mirror-backend.js.map +1 -1
- package/sdk/dist/node/gitvault-mirror-config.d.ts +17 -1
- package/sdk/dist/node/gitvault-mirror-config.d.ts.map +1 -1
- package/sdk/dist/node/gitvault-mirror-config.js +21 -0
- package/sdk/dist/node/gitvault-mirror-config.js.map +1 -1
- package/sdk/dist/node/gitvault-mirror.d.ts +42 -0
- package/sdk/dist/node/gitvault-mirror.d.ts.map +1 -1
- package/sdk/dist/node/gitvault-mirror.js +178 -2
- package/sdk/dist/node/gitvault-mirror.js.map +1 -1
- package/sdk/dist/node/gitvault-prewarm.d.ts +31 -0
- package/sdk/dist/node/gitvault-prewarm.d.ts.map +1 -1
- package/sdk/dist/node/gitvault-prewarm.js +67 -0
- package/sdk/dist/node/gitvault-prewarm.js.map +1 -1
- package/sdk/dist/node/gitvault-publication.d.ts +227 -6
- package/sdk/dist/node/gitvault-publication.d.ts.map +1 -1
- package/sdk/dist/node/gitvault-publication.js +563 -77
- package/sdk/dist/node/gitvault-publication.js.map +1 -1
- package/sdk/dist/node/index.d.ts +1 -1
- package/sdk/dist/node/index.d.ts.map +1 -1
- package/sdk/dist/node/index.js +1 -1
- package/sdk/dist/node/index.js.map +1 -1
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
* never persisted into an agent-surface result store, and never logged.
|
|
30
30
|
*/
|
|
31
31
|
import { LocalError, isRun402Error } from "../errors.js";
|
|
32
|
-
import { GITVAULT_DURABILITY_STATEMENT, GITVAULT_MIRROR_KEYSTORE_STILL_REQUIRED_STATEMENT, GITVAULT_MIRROR_VALIDITY_NOT_FRESHNESS_STATEMENT, GITVAULT_TERMINAL_LOSS_DOCTOR_TEXT, GITVAULT_TERMINAL_LOSS_STATEMENT, } from "./gitvault.crypto.js";
|
|
32
|
+
import { GITVAULT_BYO_UNMIRRORED_REMEDY_STATEMENT, GITVAULT_DEGRADED_READ_STATEMENT, GITVAULT_DURABILITY_STATEMENT, GITVAULT_MIRROR_KEYSTORE_STILL_REQUIRED_STATEMENT, GITVAULT_MIRROR_VALIDITY_NOT_FRESHNESS_STATEMENT, GITVAULT_TERMINAL_LOSS_DOCTOR_TEXT, GITVAULT_TERMINAL_LOSS_STATEMENT, GITVAULT_UNMIRRORED_FINDING_STATEMENT, } from "./gitvault.crypto.js";
|
|
33
33
|
/** A keystore path, or `null` when there is no id to derive it from (or it is malformed). */
|
|
34
34
|
function safePath(derive, repoId) {
|
|
35
35
|
if (!repoId)
|
|
@@ -53,6 +53,17 @@ async function nodeOnly(load, verb) {
|
|
|
53
53
|
function mib(bytes) {
|
|
54
54
|
return `${(bytes / (1024 * 1024)).toFixed(1)} MiB`;
|
|
55
55
|
}
|
|
56
|
+
/**
|
|
57
|
+
* gitvault-byo-primary-bucket task 3.3 — the number of `{key, object_kind}`
|
|
58
|
+
* entries `GITVAULT_BYO_OBJECT_MISSING`'s `details.missing` lists before
|
|
59
|
+
* truncating (`details.missing_count` always carries the true total, and
|
|
60
|
+
* `details.missing_truncated` says whether the list above was cut). A
|
|
61
|
+
* chain-referenced object list can be arbitrarily large; an error envelope
|
|
62
|
+
* is not the place to reproduce it whole. Matches the platform's existing
|
|
63
|
+
* `sample_keys`-class cap (asset-sync's plan response, `PLATFORM_INCIDENT_
|
|
64
|
+
* FANOUT_CAP`) rather than inventing a new number.
|
|
65
|
+
*/
|
|
66
|
+
export const GITVAULT_BYO_OBJECT_MISSING_LIST_CAP = 50;
|
|
56
67
|
/**
|
|
57
68
|
* D210 (rev 44) — the decision function behind `fsck`'s best-effort
|
|
58
69
|
* proof-of-open submission, factored out of {@link Gitvault}'s private
|
|
@@ -117,6 +128,81 @@ export async function computeOpenProofOutcome(input) {
|
|
|
117
128
|
};
|
|
118
129
|
}
|
|
119
130
|
}
|
|
131
|
+
/**
|
|
132
|
+
* gitvault-byo-primary-bucket task 3.3 (design D6) — the decision function
|
|
133
|
+
* behind `fsck`'s BYO presence check, factored out of {@link Gitvault}'s
|
|
134
|
+
* private `#checkByoPresence` the same way {@link computeOpenProofOutcome}
|
|
135
|
+
* is factored out of `#submitFsckOpenProof`, so it is directly unit-testable
|
|
136
|
+
* with an injected (fake) `hasLocalConfig`/`verifyPresence`, independent of
|
|
137
|
+
* a real keystore or HTTP client. `#checkByoPresence` supplies the REAL
|
|
138
|
+
* dependencies (`readByoConfig` against the real keystore,
|
|
139
|
+
* `verifyByoObjectsPresent` against the real client) — this function
|
|
140
|
+
* contains no filesystem/HTTP code of its own.
|
|
141
|
+
*
|
|
142
|
+
* Runs AUTOMATICALLY for a `storage_profile: "byo"` vault — never behind a
|
|
143
|
+
* new flag, so a returning agent's ordinary `fsck` catches a
|
|
144
|
+
* silently-emptied bucket without having to know to ask for it. Returns
|
|
145
|
+
* `undefined` for a managed vault (`vault?.storage_profile !== "byo"` is
|
|
146
|
+
* the FIRST check, before `hasLocalConfig`/`verifyPresence` are ever
|
|
147
|
+
* called) — a managed vault pays zero extra network/filesystem work and
|
|
148
|
+
* this task changes nothing about it (design D6/D9).
|
|
149
|
+
*
|
|
150
|
+
* NO local credentials configured (`hasLocalConfig()` returns `false`) is
|
|
151
|
+
* reported, never thrown — a BYO vault this machine cannot reach is an
|
|
152
|
+
* honest "not checked", not a failure that should break `fsck` for a
|
|
153
|
+
* credential-less returning agent (see {@link GitvaultFsckByoPresence}'s
|
|
154
|
+
* own doc comment). A confirmed absence is the opposite: it THROWS
|
|
155
|
+
* `GITVAULT_BYO_OBJECT_MISSING` — the same severity class as {@link
|
|
156
|
+
* Gitvault.mirrorVerify}'s own `GITVAULT_MIRROR_NOT_CONFIGURED` throw when
|
|
157
|
+
* `--mirror` is requested against an unconfigured vault — this is exactly
|
|
158
|
+
* the honesty D6 names: "we can tell the customer what SHOULD exist and
|
|
159
|
+
* does not", stated as a real refusal rather than a silently-embedded
|
|
160
|
+
* finding. The listed entries are capped at
|
|
161
|
+
* {@link GITVAULT_BYO_OBJECT_MISSING_LIST_CAP} (a chain-referenced object
|
|
162
|
+
* list can be arbitrarily large and this is an error envelope, not a
|
|
163
|
+
* report); `missing_count` always carries the true total regardless of how
|
|
164
|
+
* many are listed.
|
|
165
|
+
*
|
|
166
|
+
* `verifyPresence()` is a pure read (HEAD checks only, via
|
|
167
|
+
* `verifyByoObjectsPresent`) — this function persists nothing itself, so
|
|
168
|
+
* it behaves identically whether `fsck` was called with `write: true` or
|
|
169
|
+
* `write: false`.
|
|
170
|
+
*/
|
|
171
|
+
export async function computeByoPresenceOutcome(input) {
|
|
172
|
+
if (input.vault?.storage_profile !== "byo")
|
|
173
|
+
return undefined;
|
|
174
|
+
const destination = input.vault.byo_destination ?? null;
|
|
175
|
+
if (!input.hasLocalConfig()) {
|
|
176
|
+
return {
|
|
177
|
+
verified: false,
|
|
178
|
+
destination,
|
|
179
|
+
checked_count: 0,
|
|
180
|
+
not_checked_reason: "no local BYO destination credentials are configured for this vault on this machine — presence could not be verified; configure the same destination (`run402 repos create --byo <destination>` again, or the equivalent local BYO config) to enable it",
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
const report = await input.verifyPresence();
|
|
184
|
+
if (report.missing.length > 0) {
|
|
185
|
+
const missing = report.missing.slice(0, GITVAULT_BYO_OBJECT_MISSING_LIST_CAP);
|
|
186
|
+
throw new LocalError(`the BYO destination for ${input.repoId} (${report.destination}) is missing ${report.missing.length} object(s) run402's own signed chain says should exist`, "running gitvault fsck", {
|
|
187
|
+
code: "GITVAULT_BYO_OBJECT_MISSING",
|
|
188
|
+
details: {
|
|
189
|
+
repo_id: input.repoId,
|
|
190
|
+
destination: report.destination,
|
|
191
|
+
checked_count: report.checked,
|
|
192
|
+
missing_count: report.missing.length,
|
|
193
|
+
missing,
|
|
194
|
+
missing_truncated: report.missing.length > missing.length,
|
|
195
|
+
},
|
|
196
|
+
next_actions: [
|
|
197
|
+
{
|
|
198
|
+
action: "restore the listed object(s) to the destination bucket from your own backup, or run `run402 repos mirror <destination>` to add a second customer-held copy",
|
|
199
|
+
why: "run402 holds no payload copy for a BYO vault — it can only tell you what SHOULD exist from the signed chain, never restore it",
|
|
200
|
+
},
|
|
201
|
+
],
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
return { verified: true, destination: report.destination, checked_count: report.checked, not_checked_reason: null };
|
|
205
|
+
}
|
|
120
206
|
/**
|
|
121
207
|
* {@link Gitvault.deploy}'s post-push glue: attach the best-effort
|
|
122
208
|
* mirror/reconcile hooks to a deploy result, gated on whether this deploy
|
|
@@ -377,7 +463,14 @@ export class Gitvault {
|
|
|
377
463
|
const [{ GitvaultVault, createGitvaultHttpTransport }, { GitvaultKeystore }] = await Promise.all([this.#publication(), this.#keystore()]);
|
|
378
464
|
const repoId = await this.#resolveRepoId(options);
|
|
379
465
|
const keystore = new GitvaultKeystore(options.keystore_root !== undefined ? { rootDir: options.keystore_root } : {});
|
|
380
|
-
|
|
466
|
+
// gitvault-object-host-predial (design D1, task 1.2): this is the ONE
|
|
467
|
+
// transport whose object reads back the returned handle's vault, so
|
|
468
|
+
// wiring the origin-observation hook here (and nowhere else `open()`
|
|
469
|
+
// is reached from — `openOrCreate`'s own allocation-only transport
|
|
470
|
+
// never serves object reads) covers every read path.
|
|
471
|
+
const transport = createGitvaultHttpTransport(this.#client, {
|
|
472
|
+
onObjectStoreOriginObserved: (rid, origins) => keystore.recordObjectStoreOrigins(rid, origins),
|
|
473
|
+
});
|
|
381
474
|
const vault = new GitvaultVault({
|
|
382
475
|
keystore,
|
|
383
476
|
transport,
|
|
@@ -526,14 +619,28 @@ export class Gitvault {
|
|
|
526
619
|
*/
|
|
527
620
|
async init(options) {
|
|
528
621
|
const [{ createGitvaultHttpTransport }, { GitvaultKeystore }, { createGitvault }] = await Promise.all([this.#publication(), this.#keystore(), this.#creation()]);
|
|
622
|
+
const { parseMirrorDestinationUrl, formatMirrorDestination } = await this.#mirrorConfig();
|
|
529
623
|
const keystore = new GitvaultKeystore(options.keystore_root !== undefined ? { rootDir: options.keystore_root } : {});
|
|
624
|
+
const transport = createGitvaultHttpTransport(this.#client);
|
|
625
|
+
let byoWriteTarget;
|
|
626
|
+
let byoDestinationAddress;
|
|
627
|
+
if (options.byo) {
|
|
628
|
+
const destination = parseMirrorDestinationUrl(options.byo.destination_url, { region: options.byo.region, endpoint: options.byo.endpoint });
|
|
629
|
+
byoWriteTarget = { destination, ...(options.byo.credential ? { credential: options.byo.credential } : {}) };
|
|
630
|
+
byoDestinationAddress = formatMirrorDestination(destination);
|
|
631
|
+
// gitvault-byo-primary-bucket task 3.1 (design D6) — probe BEFORE any
|
|
632
|
+
// allocation request; a failed probe throws and nothing is created.
|
|
633
|
+
const { probeGitvaultByoDestination } = await this.#byoProbe();
|
|
634
|
+
await probeGitvaultByoDestination(destination, options.byo.credential);
|
|
635
|
+
}
|
|
530
636
|
const created = await createGitvault({
|
|
531
637
|
keystore,
|
|
532
|
-
transport
|
|
638
|
+
transport,
|
|
533
639
|
org_id: options.org_id,
|
|
534
640
|
project_id: options.project_id,
|
|
535
641
|
...(options.client_creation_id !== undefined ? { client_creation_id: options.client_creation_id } : {}),
|
|
536
642
|
...(options.service_public_key !== undefined ? { service_public_key: options.service_public_key } : {}),
|
|
643
|
+
...(byoWriteTarget ? { storage_profile: "byo", byo_destination: byoDestinationAddress, byo_write_target: byoWriteTarget } : {}),
|
|
537
644
|
});
|
|
538
645
|
let remote = null;
|
|
539
646
|
if (options.scaffold_git !== false && options.repo_dir) {
|
|
@@ -545,6 +652,31 @@ export class Gitvault {
|
|
|
545
652
|
...(options.remote_url !== undefined ? { remote_url: options.remote_url } : {}),
|
|
546
653
|
});
|
|
547
654
|
}
|
|
655
|
+
// gitvault-byo-primary-bucket task 3.1/3.5 — read back the AUTHORITATIVE
|
|
656
|
+
// storage_profile from the vault record rather than trusting what THIS
|
|
657
|
+
// call requested: an idempotent replay against a pre-existing vault
|
|
658
|
+
// (a different client_creation_id, a different machine) can legitimately
|
|
659
|
+
// resolve to a vault whose real profile disagrees with what was just
|
|
660
|
+
// asked for (`allocateVault`'s own doc comment: "a replay reads the
|
|
661
|
+
// EXISTING row's profile, never re-derives it"). Only when the SERVER
|
|
662
|
+
// confirms "byo" is the local write-credential config actually saved —
|
|
663
|
+
// never speculatively, and never when it disagrees.
|
|
664
|
+
let storageProfile = "managed";
|
|
665
|
+
let byoDestination = null;
|
|
666
|
+
if (byoWriteTarget) {
|
|
667
|
+
const record = await transport.getVaultRecord({ repo_id: created.repo_id });
|
|
668
|
+
storageProfile = record.storage_profile ?? "managed";
|
|
669
|
+
byoDestination = record.byo_destination ?? null;
|
|
670
|
+
if (storageProfile === "byo") {
|
|
671
|
+
const { saveByoConfig } = await this.#byoConfig();
|
|
672
|
+
saveByoConfig(keystore, { repo_id: created.repo_id, destination: byoWriteTarget.destination, ...(byoWriteTarget.credential ? { credential: byoWriteTarget.credential } : {}) });
|
|
673
|
+
}
|
|
674
|
+
// storageProfile !== "byo" here means this project's vault already
|
|
675
|
+
// existed as a MANAGED vault under a different creation attempt —
|
|
676
|
+
// nothing is saved locally, and the result below reports the TRUE
|
|
677
|
+
// profile so the caller can tell the user honestly rather than
|
|
678
|
+
// silently writing a bogus BYO config for a managed vault.
|
|
679
|
+
}
|
|
548
680
|
return {
|
|
549
681
|
repo_id: created.repo_id,
|
|
550
682
|
project_id: options.project_id,
|
|
@@ -553,6 +685,8 @@ export class Gitvault {
|
|
|
553
685
|
remote,
|
|
554
686
|
deduplicated: created.how === "reconciled",
|
|
555
687
|
terminal_loss_statement: GITVAULT_TERMINAL_LOSS_STATEMENT,
|
|
688
|
+
storage_profile: storageProfile,
|
|
689
|
+
byo_destination: byoDestination,
|
|
556
690
|
};
|
|
557
691
|
}
|
|
558
692
|
/**
|
|
@@ -966,12 +1100,16 @@ export class Gitvault {
|
|
|
966
1100
|
// above (already returned/committed) — a mirror failure is a named
|
|
967
1101
|
// pending finding reported BESIDE the vault result, on its own field.
|
|
968
1102
|
const mirrorPush = await this.#tryMirrorPush(handle.repo_id, handle.keystore);
|
|
1103
|
+
// gitvault-byo-primary-bucket task 3.3 — the SAME non-blocking contract,
|
|
1104
|
+
// fired right beside the mirror hook (skipped_no_mirror with no network
|
|
1105
|
+
// call for any managed vault or any machine with no local BYO config).
|
|
1106
|
+
const byoChainCopy = await this.#tryByoChainCopyPush(handle.repo_id, handle.keystore);
|
|
969
1107
|
// Deploy-time reconcile hook: fires on every successful push,
|
|
970
1108
|
// best-effort — a reconcile failure (including a read-only principal
|
|
971
1109
|
// with no signing key) is reported BESIDE the vault result, never a
|
|
972
1110
|
// `push()` throw, same non-blocking contract as the mirror hook above.
|
|
973
1111
|
const reconcileRecipients = await this.#tryReconcileEnvelopeRecipients(handle.vault);
|
|
974
|
-
return { ...result, snapshot, gitvault_commit: snapshot.oid, gitvault_commit_line: line, mirror_push: mirrorPush, reconcile_recipients: reconcileRecipients };
|
|
1112
|
+
return { ...result, snapshot, gitvault_commit: snapshot.oid, gitvault_commit_line: line, mirror_push: mirrorPush, byo_chain_copy: byoChainCopy, reconcile_recipients: reconcileRecipients };
|
|
975
1113
|
}
|
|
976
1114
|
/** Best-effort dual-push: catches EVERYTHING, including the lazy module import itself, so a mirror problem can never surface as a `push()` throw. */
|
|
977
1115
|
/**
|
|
@@ -1020,6 +1158,23 @@ export class Gitvault {
|
|
|
1020
1158
|
return { attempted: false, outcome: "skipped_no_mirror", error: e instanceof Error ? e.message : String(e) };
|
|
1021
1159
|
}
|
|
1022
1160
|
}
|
|
1161
|
+
/**
|
|
1162
|
+
* gitvault-byo-primary-bucket task 3.3 — the chain's every-push dual-write
|
|
1163
|
+
* into a BYO vault's own destination, mirroring {@link #tryMirrorPush}'s
|
|
1164
|
+
* exact contract byte-for-byte (best-effort, catches EVERYTHING, never
|
|
1165
|
+
* throws, never alters the vault outcome already committed above — a
|
|
1166
|
+
* chain-copy failure is a named pending finding reported BESIDE the vault
|
|
1167
|
+
* result on its own field, never a `push()`/`deploy()` throw).
|
|
1168
|
+
*/
|
|
1169
|
+
async #tryByoChainCopyPush(repoId, keystore) {
|
|
1170
|
+
try {
|
|
1171
|
+
const { byoChainCopyPushForGeneration } = await this.#mirror();
|
|
1172
|
+
return await byoChainCopyPushForGeneration(this.#client, repoId, { keystore });
|
|
1173
|
+
}
|
|
1174
|
+
catch (e) {
|
|
1175
|
+
return { attempted: false, outcome: "skipped_no_mirror", error: e instanceof Error ? e.message : String(e) };
|
|
1176
|
+
}
|
|
1177
|
+
}
|
|
1023
1178
|
/** Best-effort envelope-recipient reconcile: catches EVERYTHING so a reconcile problem can never surface as a `push()` throw (mirrors {@link #tryMirrorPush}'s contract exactly). */
|
|
1024
1179
|
async #tryReconcileEnvelopeRecipients(vault) {
|
|
1025
1180
|
try {
|
|
@@ -1657,6 +1812,14 @@ export class Gitvault {
|
|
|
1657
1812
|
* to 7, `GITVAULT_EPOCH_NOT_OPENABLE` at epoch 2/rotation …") is exactly
|
|
1658
1813
|
* what this split makes representable. `refs`/`head_target`/
|
|
1659
1814
|
* `pin_after.highest_materialized` all reflect `decryptable_to_generation`.
|
|
1815
|
+
*
|
|
1816
|
+
* **gitvault-byo-primary-bucket task 3.3 (design D6).** For a
|
|
1817
|
+
* `storage_profile: "byo"` vault, `fsck` ALSO adjudicates the customer's
|
|
1818
|
+
* own bucket against run402's signed chain — see {@link
|
|
1819
|
+
* #checkByoPresence} for the full contract (automatic, never a flag;
|
|
1820
|
+
* fails soft with no local credentials; throws
|
|
1821
|
+
* `GITVAULT_BYO_OBJECT_MISSING` on a confirmed absence). A managed vault
|
|
1822
|
+
* is byte-identical to before this task.
|
|
1660
1823
|
*/
|
|
1661
1824
|
async fsck(options = {}) {
|
|
1662
1825
|
const before = await this.status(options);
|
|
@@ -1701,6 +1864,14 @@ export class Gitvault {
|
|
|
1701
1864
|
chain_verified_to_generation: state.generation,
|
|
1702
1865
|
decryptable_to_generation: decryptableToGeneration,
|
|
1703
1866
|
});
|
|
1867
|
+
// gitvault-byo-primary-bucket task 3.3 (design D6) — LAST, deliberately:
|
|
1868
|
+
// everything above (chain verify + its pin persist, retained-refs
|
|
1869
|
+
// reconcile, open-proof submission) is real, valid work regardless of
|
|
1870
|
+
// what the customer's own bucket holds — the signed chain is run402-
|
|
1871
|
+
// authoritative either way (design D9) — so a missing-object refusal
|
|
1872
|
+
// below must never cost any of it. See `#checkByoPresence`'s own doc
|
|
1873
|
+
// comment for the throw/no-throw split.
|
|
1874
|
+
const byoPresence = await this.#checkByoPresence(before.vault, repoId, handle.keystore);
|
|
1704
1875
|
return {
|
|
1705
1876
|
repo_id: repoId,
|
|
1706
1877
|
write,
|
|
@@ -1717,6 +1888,7 @@ export class Gitvault {
|
|
|
1717
1888
|
mirror,
|
|
1718
1889
|
retained_refs: retainedRefs,
|
|
1719
1890
|
open_proof: openProof,
|
|
1891
|
+
...(byoPresence !== undefined ? { byo_presence: byoPresence } : {}),
|
|
1720
1892
|
};
|
|
1721
1893
|
}
|
|
1722
1894
|
/**
|
|
@@ -1749,6 +1921,28 @@ export class Gitvault {
|
|
|
1749
1921
|
}),
|
|
1750
1922
|
});
|
|
1751
1923
|
}
|
|
1924
|
+
/**
|
|
1925
|
+
* gitvault-byo-primary-bucket task 3.3 (design D6) — `repos fsck`'s
|
|
1926
|
+
* wiring of the shipped read-half primitive
|
|
1927
|
+
* {@link import("../node/gitvault-mirror.js").verifyByoObjectsPresent}.
|
|
1928
|
+
* Thin wrapper over {@link computeByoPresenceOutcome} (the testable
|
|
1929
|
+
* decision function, factored out the same way {@link
|
|
1930
|
+
* computeOpenProofOutcome} is factored out of `#submitFsckOpenProof`)
|
|
1931
|
+
* supplying THIS instance's real `hasLocalConfig`/`verifyPresence`
|
|
1932
|
+
* dependencies (`readByoConfig` against the real keystore,
|
|
1933
|
+
* `verifyByoObjectsPresent` against the real HTTP client). See that
|
|
1934
|
+
* function's own doc comment for the full gating/throw contract.
|
|
1935
|
+
*/
|
|
1936
|
+
async #checkByoPresence(vault, repoId, keystore) {
|
|
1937
|
+
const { readByoConfig } = await this.#byoConfig();
|
|
1938
|
+
const { verifyByoObjectsPresent } = await this.#mirror();
|
|
1939
|
+
return computeByoPresenceOutcome({
|
|
1940
|
+
repoId,
|
|
1941
|
+
vault,
|
|
1942
|
+
hasLocalConfig: () => readByoConfig(keystore, repoId) !== null,
|
|
1943
|
+
verifyPresence: () => verifyByoObjectsPresent(this.#client, repoId, { keystore }),
|
|
1944
|
+
});
|
|
1945
|
+
}
|
|
1752
1946
|
/**
|
|
1753
1947
|
* Wrap this vault's current epoch key to every org member who has
|
|
1754
1948
|
* published an encryption key but has no `key_envelope` on this vault yet
|
|
@@ -2001,7 +2195,15 @@ export class Gitvault {
|
|
|
2001
2195
|
// failure in a log line that assumed ordering. Extracted to a standalone
|
|
2002
2196
|
// function so the outcome-gating is unit-testable with fake thunks,
|
|
2003
2197
|
// without standing up a live vault — see gitvault-deploy-hooks.test.ts.
|
|
2004
|
-
|
|
2198
|
+
const hooked = await attachGitvaultDeployHooks(result, () => this.#tryMirrorPush(handle.repo_id, handle.keystore), () => this.#tryReconcileEnvelopeRecipients(handle.vault));
|
|
2199
|
+
// gitvault-byo-primary-bucket task 3.3 — composed AFTER, not inside,
|
|
2200
|
+
// `attachGitvaultDeployHooks` (kept untouched so its own unit-tested
|
|
2201
|
+
// outcome-gating contract stays byte-for-byte): the SAME "did this
|
|
2202
|
+
// deploy actually land a generation" gate `mirror_push`'s presence
|
|
2203
|
+
// already encodes, so no new outcome-inspection logic here.
|
|
2204
|
+
if (hooked.mirror_push === undefined)
|
|
2205
|
+
return hooked;
|
|
2206
|
+
return { ...hooked, byo_chain_copy: await this.#tryByoChainCopyPush(handle.repo_id, handle.keystore) };
|
|
2005
2207
|
}
|
|
2006
2208
|
/**
|
|
2007
2209
|
* Drain every unvaulted-override journal on this machine: push the exact
|
|
@@ -2023,6 +2225,35 @@ export class Gitvault {
|
|
|
2023
2225
|
const out = await handle.vault.restoreObjectsInto(options.target_dir);
|
|
2024
2226
|
return { refs: out.refs, generation: out.generation, retained_refs: out.retained_refs };
|
|
2025
2227
|
}
|
|
2228
|
+
// ── degraded read (gitvault-byo-primary-bucket, design D4, task 3.4 — mirror half) ──
|
|
2229
|
+
/**
|
|
2230
|
+
* Wrap an ALREADY-RESOLVED vault read with degraded-read fallback: run
|
|
2231
|
+
* `options.attemptLive` (the caller's own live call — its
|
|
2232
|
+
* `vault.materialize()` for a `list`-shaped read, or
|
|
2233
|
+
* `vault.restoreObjectsInto(dir)` for a `fetch`-shaped one), and on a
|
|
2234
|
+
* network-class failure (never on an authorization refusal or any other
|
|
2235
|
+
* 4xx — see `isNetworkClassGitvaultReadError`), fall back to the vault's
|
|
2236
|
+
* configured mirror via the SAME `r402s-recover` engine `recover()` uses.
|
|
2237
|
+
*
|
|
2238
|
+
* Deliberately takes an already-open `vault`/`keystore`/`repo_id` rather
|
|
2239
|
+
* than resolving them itself: named-address resolution (slug-form vs.
|
|
2240
|
+
* id-form), stale-pin recovery, and cross-command vault-instance reuse
|
|
2241
|
+
* within one `git-remote-run402` session are CLI-layer concerns that
|
|
2242
|
+
* already live in `cli/lib/remote-helper-session.mjs` — this method owns
|
|
2243
|
+
* only the trigger discipline and the fallback engine, never vault
|
|
2244
|
+
* resolution, so it composes with that existing flow instead of
|
|
2245
|
+
* duplicating it.
|
|
2246
|
+
*
|
|
2247
|
+
* `out_dir` is REQUIRED for the fallback to ever run — pass the resolved,
|
|
2248
|
+
* git-proven repository directory (`resolveGitInvocationRepo`'s own
|
|
2249
|
+
* `repo_dir`, never a guess from `cwd`). `null` (no resolvable repository,
|
|
2250
|
+
* e.g. a bare `git ls-remote` outside any checkout) disables the fallback
|
|
2251
|
+
* entirely: the original gateway error surfaces exactly as it always did.
|
|
2252
|
+
*/
|
|
2253
|
+
async withDegradedRead(options) {
|
|
2254
|
+
const { tryGitvaultDegradedRead } = await this.#degradedRead();
|
|
2255
|
+
return tryGitvaultDegradedRead(options);
|
|
2256
|
+
}
|
|
2026
2257
|
// ── mirror (gitvault-mirror-and-recover, design D1/D2/D7) ─────────────────
|
|
2027
2258
|
/**
|
|
2028
2259
|
* Configure (or replace) the customer-owned mirror destination for one
|
|
@@ -2060,8 +2291,19 @@ export class Gitvault {
|
|
|
2060
2291
|
const keystore = new GitvaultKeystore(options.keystore_root !== undefined ? { rootDir: options.keystore_root } : {});
|
|
2061
2292
|
const config = readMirrorConfig(keystore, repoId);
|
|
2062
2293
|
const base = { repo_id: repoId, validity_not_freshness: GITVAULT_MIRROR_VALIDITY_NOT_FRESHNESS_STATEMENT, keystore_still_required: GITVAULT_MIRROR_KEYSTORE_STILL_REQUIRED_STATEMENT };
|
|
2294
|
+
const lastSuccessAt = config?.last_success_at ?? null;
|
|
2063
2295
|
if (!config) {
|
|
2064
|
-
|
|
2296
|
+
// gitvault-mirror-default: the unconfigured branch stays gateway-blind
|
|
2297
|
+
// by construction — it returns before ANY network call, finding included.
|
|
2298
|
+
// `options.is_byo` is a caller-supplied hint (gitvault-byo-primary-bucket
|
|
2299
|
+
// task 3.5) — NEVER derived here, which is what keeps this branch
|
|
2300
|
+
// network-call-free; `repos view` already has `storage_profile` from
|
|
2301
|
+
// its own `status()` read and passes it in.
|
|
2302
|
+
return {
|
|
2303
|
+
...base, configured: false, destination: null, credential_kind: null, mirrored_generation: null, newest_generation: null, is_current: null, closing_command: null,
|
|
2304
|
+
last_success_at: lastSuccessAt,
|
|
2305
|
+
finding: gitvaultUnmirroredFinding({ configured: false, last_success_at: lastSuccessAt, mirrored_generation: null, is_byo: options.is_byo }),
|
|
2306
|
+
};
|
|
2065
2307
|
}
|
|
2066
2308
|
let mirroredGeneration = null;
|
|
2067
2309
|
try {
|
|
@@ -2084,6 +2326,8 @@ export class Gitvault {
|
|
|
2084
2326
|
...base, configured: true, destination: formatMirrorDestination(config.destination), credential_kind: config.credential?.kind ?? null,
|
|
2085
2327
|
mirrored_generation: mirroredGeneration, newest_generation: newestGeneration, is_current: isCurrent,
|
|
2086
2328
|
closing_command: isCurrent === false ? "run402 repos mirror --backfill" : null,
|
|
2329
|
+
last_success_at: lastSuccessAt,
|
|
2330
|
+
finding: gitvaultUnmirroredFinding({ configured: true, last_success_at: lastSuccessAt, mirrored_generation: mirroredGeneration, is_byo: options.is_byo }),
|
|
2087
2331
|
};
|
|
2088
2332
|
}
|
|
2089
2333
|
/** List the vault's stored objects, diff against the mirror, fetch + hash-verify + write what's missing, in admission order. Resumable and idempotent. */
|
|
@@ -2202,6 +2446,15 @@ export class Gitvault {
|
|
|
2202
2446
|
#recovery() {
|
|
2203
2447
|
return nodeOnly(() => import("../node/gitvault-recover.js"), "recover");
|
|
2204
2448
|
}
|
|
2449
|
+
#degradedRead() {
|
|
2450
|
+
return nodeOnly(() => import("../node/gitvault-degraded-read.js"), "list");
|
|
2451
|
+
}
|
|
2452
|
+
#byoConfig() {
|
|
2453
|
+
return nodeOnly(() => import("../node/gitvault-byo-config.js"), "init");
|
|
2454
|
+
}
|
|
2455
|
+
#byoProbe() {
|
|
2456
|
+
return nodeOnly(() => import("../node/gitvault-byo-probe.js"), "init");
|
|
2457
|
+
}
|
|
2205
2458
|
}
|
|
2206
2459
|
// ─── D7 — progressive terminal-loss warning (repo-first-onramp task 2.7) ────
|
|
2207
2460
|
//
|
|
@@ -2331,6 +2584,37 @@ export function gitvaultKeystoreBackupReminderMessage(trip, coveringRecipients)
|
|
|
2331
2584
|
return (`this vault has accrued real value at risk (${reasons.join(", ")}), but it is covered by ${coveringRecipients} recipients today — not the single-principal case. ` +
|
|
2332
2585
|
`${GITVAULT_DURABILITY_STATEMENT} Back up this machine's keystore anyway: losing it does not lose the vault, but it does lose YOUR access to it.`);
|
|
2333
2586
|
}
|
|
2587
|
+
/**
|
|
2588
|
+
* gitvault-mirror-default — the pure `vault_unmirrored` computation, in ONE
|
|
2589
|
+
* place so doctor and `repos view` echo the same finding instead of each
|
|
2590
|
+
* deriving their own (the loss-warning pattern above). Present when no mirror
|
|
2591
|
+
* is configured, OR when one is configured but has no success evidence yet —
|
|
2592
|
+
* either the local `last_success_at` stamp (survives a transiently unreachable
|
|
2593
|
+
* mirror) or a chain-verified `mirrored_generation` read from the mirror
|
|
2594
|
+
* itself (covers mirrors synced before the stamp existed). Informational,
|
|
2595
|
+
* never blocking; every input is client-local or read from the CUSTOMER'S
|
|
2596
|
+
* mirror — nothing here touches the gateway.
|
|
2597
|
+
*/
|
|
2598
|
+
export function gitvaultUnmirroredFinding(state) {
|
|
2599
|
+
if (state.configured && (state.last_success_at !== null || state.mirrored_generation !== null))
|
|
2600
|
+
return null;
|
|
2601
|
+
return {
|
|
2602
|
+
kind: "vault_unmirrored",
|
|
2603
|
+
message: state.is_byo ? GITVAULT_BYO_UNMIRRORED_REMEDY_STATEMENT : GITVAULT_UNMIRRORED_FINDING_STATEMENT,
|
|
2604
|
+
setup_command: state.configured ? "run402 repos mirror --backfill" : "run402 repos mirror <destination>",
|
|
2605
|
+
};
|
|
2606
|
+
}
|
|
2607
|
+
/**
|
|
2608
|
+
* gitvault-byo-primary-bucket (design D4) — the ONE stderr line a degraded
|
|
2609
|
+
* chain/payload read prints: the fallback's own destination (never a
|
|
2610
|
+
* credential) plus the canonical, mechanism-only statement. `list`/`fetch`
|
|
2611
|
+
* degrading in the SAME `git-remote-run402` session each print their own
|
|
2612
|
+
* line (one per degraded READ, not one per session) — see
|
|
2613
|
+
* `Gitvault.withDegradedRead`'s own doc comment.
|
|
2614
|
+
*/
|
|
2615
|
+
export function gitvaultDegradedReadNote(source) {
|
|
2616
|
+
return `degraded read from ${source.destination}: ${GITVAULT_DEGRADED_READ_STATEMENT}`;
|
|
2617
|
+
}
|
|
2334
2618
|
/** `run402::<org_id>/<project_id>` — what `git-remote-run402` resolves. */
|
|
2335
2619
|
export function gitvaultRemoteUrl(orgId, projectId) {
|
|
2336
2620
|
return `run402::${orgId}/${projectId}`;
|