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
package/gitvault-surface.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"surface_version": "4.
|
|
2
|
+
"surface_version": "4.65.0",
|
|
3
3
|
"verbs": [
|
|
4
4
|
"repos create",
|
|
5
5
|
"repos list",
|
|
@@ -68,6 +68,7 @@
|
|
|
68
68
|
"revocation_live": true,
|
|
69
69
|
"human_envelope_add_live": true,
|
|
70
70
|
"mirror_live": true,
|
|
71
|
+
"byo_live": true,
|
|
71
72
|
"recover_live": true,
|
|
72
73
|
"allocation": "create_immediate",
|
|
73
74
|
"snapshot_dirty_default": "refuse"
|
package/lib/doctor.mjs
CHANGED
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
14
|
import { existsSync, statSync } from "node:fs";
|
|
15
|
+
import { GITVAULT_BYO_NO_PAYLOAD_COPY_STATEMENT } from "#sdk";
|
|
15
16
|
import { configDir, readAllowance, loadKeyStore } from "./config.mjs";
|
|
16
17
|
import { resolveGitvaultTarget } from "./gitvault-target.mjs";
|
|
17
18
|
import { getSdk } from "./sdk.mjs";
|
|
@@ -665,6 +666,10 @@ export async function run(sub, args = []) {
|
|
|
665
666
|
project_id: gv.project_id ?? projectId,
|
|
666
667
|
repo_id: gv.repo_id,
|
|
667
668
|
vault: gv.vault === null ? null : "allocated",
|
|
669
|
+
// gitvault-byo-primary-bucket task 3.5 — absent-or-"managed" is
|
|
670
|
+
// byte-identical to before this fold for every non-BYO vault.
|
|
671
|
+
storage_profile: gv.vault?.storage_profile ?? null,
|
|
672
|
+
byo_destination: gv.vault?.byo_destination ?? null,
|
|
668
673
|
gitvault_policy: gv.gitvault_policy,
|
|
669
674
|
keystore_root: gv.keystore.root,
|
|
670
675
|
can_sign: gv.keystore.can_sign,
|
|
@@ -704,30 +709,34 @@ export async function run(sub, args = []) {
|
|
|
704
709
|
// doctor-persistent `grandfathered` advisory it owns.
|
|
705
710
|
for (const w of gv.warnings ?? []) gaps.push(`${w.kind}: ${w.message}`);
|
|
706
711
|
|
|
707
|
-
// gitvault-mirror-and-recover task 4.3
|
|
708
|
-
// ALONGSIDE (never in place of) the
|
|
709
|
-
// never blocking `run402 deploy`'s
|
|
710
|
-
// outcome is unaffected regardless of
|
|
711
|
-
// `mirror_currency` mirrors `mirror
|
|
712
|
-
// `current` / `stale` / `unknown` (mirror
|
|
713
|
-
// unread)
|
|
714
|
-
//
|
|
715
|
-
//
|
|
712
|
+
// gitvault-mirror-and-recover task 4.3 + gitvault-mirror-default:
|
|
713
|
+
// mirror currency, reported ALONGSIDE (never in place of) the
|
|
714
|
+
// deploy-related gaps above, and never blocking `run402 deploy`'s
|
|
715
|
+
// own gate — the vault lane's outcome is unaffected regardless of
|
|
716
|
+
// mirror state (design D6). `mirror_currency` mirrors `mirror
|
|
717
|
+
// status`'s own tri-state: `current` / `stale` / `unknown` (mirror
|
|
718
|
+
// unreachable or vault unread). Only STALE is actionable enough to
|
|
719
|
+
// become a warning gap; a vault with no successful mirror copy yet
|
|
720
|
+
// carries the SDK-computed `vault_unmirrored` finding — named and
|
|
721
|
+
// standing (gitvault-mirror-default supersedes the old anonymous
|
|
722
|
+
// `advisory` string), echoed verbatim, and deliberately NOT pushed
|
|
723
|
+
// into `gaps`: informational, never blocking, computed client-side
|
|
724
|
+
// only, cleared by the first successful mirror write or sync.
|
|
716
725
|
if (gv.vault !== null && value.repo_id) {
|
|
717
726
|
try {
|
|
718
|
-
const mirrorStatus = await getSdk().gitvault.mirrorStatus({ repo_id: value.repo_id });
|
|
727
|
+
const mirrorStatus = await getSdk().gitvault.mirrorStatus({ repo_id: value.repo_id, is_byo: value.storage_profile === "byo" });
|
|
719
728
|
value.gitvault_mirror = {
|
|
720
729
|
configured: mirrorStatus.configured,
|
|
721
730
|
destination: mirrorStatus.destination,
|
|
722
731
|
mirrored_generation: mirrorStatus.mirrored_generation,
|
|
723
732
|
newest_generation: mirrorStatus.newest_generation,
|
|
724
733
|
is_current: mirrorStatus.is_current,
|
|
734
|
+
last_success_at: mirrorStatus.last_success_at,
|
|
735
|
+
finding: mirrorStatus.finding,
|
|
725
736
|
validity_not_freshness: mirrorStatus.validity_not_freshness,
|
|
726
737
|
keystore_still_required: mirrorStatus.keystore_still_required,
|
|
727
738
|
};
|
|
728
|
-
if (
|
|
729
|
-
value.gitvault_mirror.advisory = "no ciphertext mirror is configured for this vault — the exit ramp is opt-in; 'run402 repos mirror <destination>' to configure one.";
|
|
730
|
-
} else if (mirrorStatus.is_current === false) {
|
|
739
|
+
if (mirrorStatus.is_current === false) {
|
|
731
740
|
gaps.push(`the ciphertext mirror at ${mirrorStatus.destination} is STALE (mirrored generation ${mirrorStatus.mirrored_generation ?? "(none)"}, vault newest ${mirrorStatus.newest_generation ?? "(none)"}) — ${mirrorStatus.closing_command}`);
|
|
732
741
|
}
|
|
733
742
|
} catch {
|
|
@@ -736,15 +745,19 @@ export async function run(sub, args = []) {
|
|
|
736
745
|
}
|
|
737
746
|
}
|
|
738
747
|
|
|
748
|
+
// gitvault-byo-primary-bucket task 3.5 — unconditional, independent
|
|
749
|
+
// of mirror status (D7); imported from the canonical constants, never
|
|
750
|
+
// paraphrased.
|
|
751
|
+
const byoDisclosure = value.storage_profile === "byo" ? ` Storage: byo (${value.byo_destination ?? "(unknown)"}) — ${GITVAULT_BYO_NO_PAYLOAD_COPY_STATEMENT}` : "";
|
|
739
752
|
checks.push({
|
|
740
753
|
name: "gitvault",
|
|
741
754
|
status: gaps.length > 0 ? "warning" : "ok",
|
|
742
755
|
value: gaps.length > 0 ? { ...value, gaps } : value,
|
|
743
|
-
hint: gv.vault === null
|
|
756
|
+
hint: (gv.vault === null
|
|
744
757
|
? `No vault for this project (that is a normal shape). Allocate one with 'run402 repos create --project <id>'. Keystore: ${gv.keystore.root}`
|
|
745
758
|
: gv.durability_statement
|
|
746
759
|
? `Back up ${gv.keystore.root} anyway — ${gv.durability_statement} (covering_recipients: ${gv.covering_recipients})`
|
|
747
|
-
: `Back up ${gv.keystore.root} — whole-machine or whole-keystore loss is terminal for vault history
|
|
760
|
+
: `Back up ${gv.keystore.root} — whole-machine or whole-keystore loss is terminal for vault history.`) + byoDisclosure,
|
|
748
761
|
});
|
|
749
762
|
} catch (err) {
|
|
750
763
|
// A gateway without gitvault, an unreachable API, or a project this
|
|
@@ -28,6 +28,14 @@ export const GITVAULT_CAPABILITIES = {
|
|
|
28
28
|
// `repos mirror` reads/writes a mirror destination beside the keystore and
|
|
29
29
|
// moves real bytes into a customer-owned bucket.
|
|
30
30
|
mirror_live: true,
|
|
31
|
+
// `repos create --byo <s3://bucket/prefix>` allocates a
|
|
32
|
+
// `storage_profile: "byo"` vault whose payload ciphertext is written by
|
|
33
|
+
// the client straight to the customer's own bucket (create-only probe
|
|
34
|
+
// fail-closed before allocation; chain-copy dual-write; attested finalize
|
|
35
|
+
// with `storage_verification: "client_attested"`; degraded reads from the
|
|
36
|
+
// destination; fsck absence check) — shipped end to end in the same
|
|
37
|
+
// change that flips this flag (gitvault-byo-primary-bucket, Phase 3).
|
|
38
|
+
byo_live: true,
|
|
31
39
|
// `repos recover` materializes a git repository from a mirror source,
|
|
32
40
|
// offline, with no server call.
|
|
33
41
|
recover_live: true,
|
package/lib/gitvault-daemon.mjs
CHANGED
|
@@ -53,9 +53,16 @@ export async function runDaemon() {
|
|
|
53
53
|
// Load the heavy module ONCE — this is the entire point of residency.
|
|
54
54
|
// Its top-level await pulls the SDK graph; the prewarm dials the API
|
|
55
55
|
// origin so the first forwarded session rides a warm h2 connection.
|
|
56
|
-
const { prewarmGitvaultConnection, kickGitvaultConnection } = await import("../sdk/dist/node/gitvault-prewarm.js");
|
|
56
|
+
const { prewarmGitvaultConnection, kickGitvaultConnection, predialGitvaultObjectStore } = await import("../sdk/dist/node/gitvault-prewarm.js");
|
|
57
57
|
prewarmGitvaultConnection();
|
|
58
58
|
const { runHelperSession } = await import("./remote-helper-session.mjs");
|
|
59
|
+
// gitvault-object-host-predial task 2.2 — narrow, session-scoped imports
|
|
60
|
+
// (not the whole `#sdk/node` barrel) for the OFFLINE repo resolution the
|
|
61
|
+
// session-accept predial needs: a local git subprocess (`rev-parse
|
|
62
|
+
// --absolute-git-dir`) plus a local git-config read, never a network
|
|
63
|
+
// call (design D6).
|
|
64
|
+
const { resolveGitInvocationRepo } = await import("../sdk/dist/node/gitvault-snapshot.js");
|
|
65
|
+
const { readPinnedGitvaultRepo } = await import("../sdk/dist/node/gitvault-address.js");
|
|
59
66
|
|
|
60
67
|
const socketPath = daemonSocketPath();
|
|
61
68
|
if (process.platform !== "win32") {
|
|
@@ -184,6 +191,28 @@ export async function runDaemon() {
|
|
|
184
191
|
// (never throws, never awaited, never delays a session).
|
|
185
192
|
const acceptedAt = Date.now();
|
|
186
193
|
kickGitvaultConnection();
|
|
194
|
+
// gitvault-object-host-predial task 2.2 — the addressed repo's
|
|
195
|
+
// persisted object-store origin(s), predialed on the SAME
|
|
196
|
+
// pre-connect timing as the API-origin kick above: resolved from
|
|
197
|
+
// THIS session's OWN `msg.cwd`/`msg.env` (not `process.cwd()`/
|
|
198
|
+
// `process.env`, which still name the daemon's PRIOR session
|
|
199
|
+
// until the swap below runs) via the same offline, no-network
|
|
200
|
+
// mechanism the session module itself uses once it takes over —
|
|
201
|
+
// `resolveGitInvocationRepo` (a local `git rev-parse
|
|
202
|
+
// --absolute-git-dir`) then `readPinnedGitvaultRepo` (a local
|
|
203
|
+
// `git config --local` read). Any resolution failure (no
|
|
204
|
+
// `GIT_DIR`, not a repository, no pin yet) predials nothing —
|
|
205
|
+
// never a network read to learn where to predial (design D6),
|
|
206
|
+
// and never allowed to delay or fail this session's accept.
|
|
207
|
+
void (async () => {
|
|
208
|
+
try {
|
|
209
|
+
const { repo_dir } = await resolveGitInvocationRepo(msg.env ?? {}, msg.cwd);
|
|
210
|
+
const pinned = await readPinnedGitvaultRepo(repo_dir);
|
|
211
|
+
if (pinned) predialGitvaultObjectStore(pinned.repo_id);
|
|
212
|
+
} catch {
|
|
213
|
+
/* best-effort: must never surface from session accept */
|
|
214
|
+
}
|
|
215
|
+
})();
|
|
187
216
|
busy = true;
|
|
188
217
|
sessionsServed += 1;
|
|
189
218
|
const stdin = new PassThrough();
|
|
@@ -112,6 +112,23 @@
|
|
|
112
112
|
* is refused rather than silently pointed at the current directory. Only
|
|
113
113
|
* `capabilities`, `option` and `list` work without one, which is exactly
|
|
114
114
|
* the set `git ls-remote <url>` outside a checkout needs.
|
|
115
|
+
*
|
|
116
|
+
* DEGRADED READ MODE (gitvault-byo-primary-bucket, design D4). `list` and
|
|
117
|
+
* `fetch` each wrap their own live gateway read in
|
|
118
|
+
* `getSdk().gitvault.withDegradedRead` — on a NETWORK-CLASS failure (never a
|
|
119
|
+
* 401/403/404 or any other 4xx; one bounded retry runs first so a transient
|
|
120
|
+
* blip does not flap between sources) with a mirror configured for this
|
|
121
|
+
* vault, the read is served from it instead via the SAME `r402s-recover`
|
|
122
|
+
* engine `run402 repos recover` uses — `git fetch`/`clone` keep working
|
|
123
|
+
* while run402 is down. The result is marked `degraded: true` with source
|
|
124
|
+
* provenance, and exactly one stderr line names the fallback and its source
|
|
125
|
+
* (`gitvaultDegradedReadNote`). Local trust pins never advance past what the
|
|
126
|
+
* mirror copy itself chain-verifies (the fallback engine never touches the
|
|
127
|
+
* live vault's own keystore pins at all — see `gitvault-degraded-read.ts`'s
|
|
128
|
+
* own doc comment). A vault with no mirror configured is BYTE-IDENTICAL to
|
|
129
|
+
* today: the original gateway error surfaces unchanged. `push` is UNTOUCHED
|
|
130
|
+
* by this — writes are never rerouted; admission always requires the
|
|
131
|
+
* gateway.
|
|
115
132
|
*/
|
|
116
133
|
|
|
117
134
|
import { createInterface } from "node:readline";
|
|
@@ -127,8 +144,8 @@ const nodeModP = import("#sdk/node");
|
|
|
127
144
|
const configModP = import("./config.mjs");
|
|
128
145
|
const { getSdk } = await sdkModP;
|
|
129
146
|
const { resolveWalletCore, enforceWalletExistsCore, WalletSelectionError } = await walletModP;
|
|
130
|
-
const { gitvaultRemoteAddressForm, gitvaultSlugReleasedInfo, parseGitvaultRemoteUrl } = await isoModP;
|
|
131
|
-
const { GITVAULT_R402_REF_NAMESPACE, hardenedGit, resolveGitInvocationRepo, readPinnedGitvaultRepo, pinGitvaultRepo, readGitvaultRestoreMarker, readGitvaultAutoGcThreshold } = await nodeModP;
|
|
147
|
+
const { gitvaultRemoteAddressForm, gitvaultSlugReleasedInfo, parseGitvaultRemoteUrl, gitvaultDegradedReadNote } = await isoModP;
|
|
148
|
+
const { GITVAULT_R402_REF_NAMESPACE, hardenedGit, resolveGitInvocationRepo, readPinnedGitvaultRepo, pinGitvaultRepo, readGitvaultRestoreMarker, readGitvaultAutoGcThreshold, predialGitvaultObjectStore } = await nodeModP;
|
|
132
149
|
const { allowanceFile, projectCredentialsFile, profileStateFile } = await configModP;
|
|
133
150
|
|
|
134
151
|
/**
|
|
@@ -461,7 +478,27 @@ async function main(argv, { onBackgroundWork } = {}) {
|
|
|
461
478
|
*/
|
|
462
479
|
let resolvedRepo = null;
|
|
463
480
|
async function requireRepo() {
|
|
464
|
-
if (!resolvedRepo)
|
|
481
|
+
if (!resolvedRepo) {
|
|
482
|
+
resolvedRepo = await resolveGitInvocationRepo(process.env, process.cwd());
|
|
483
|
+
// gitvault-object-host-predial task 2.1 (trigger (a), the in-process
|
|
484
|
+
// fallback path — this module IS that fallback; a daemon-forwarded
|
|
485
|
+
// session reaches the same code and gets the same predial for free).
|
|
486
|
+
// The repository is now known OFFLINE (no network read, matching
|
|
487
|
+
// design D6) — fire-and-forget dial the addressed repo's persisted
|
|
488
|
+
// object-store origin(s), if the local keystore has learned any, so
|
|
489
|
+
// it overlaps the rest of this session's local work exactly like the
|
|
490
|
+
// API-origin prewarm that already raced module load above. A repo
|
|
491
|
+
// with no local pin (never opened here before) or nothing persisted
|
|
492
|
+
// yet predials nothing — never a source of a new failure mode.
|
|
493
|
+
void (async () => {
|
|
494
|
+
try {
|
|
495
|
+
const pinned = await readPinnedGitvaultRepo(resolvedRepo.repo_dir);
|
|
496
|
+
if (pinned) predialGitvaultObjectStore(pinned.repo_id);
|
|
497
|
+
} catch {
|
|
498
|
+
/* best-effort: must never surface from repository resolution */
|
|
499
|
+
}
|
|
500
|
+
})();
|
|
501
|
+
}
|
|
465
502
|
return resolvedRepo.repo_dir;
|
|
466
503
|
}
|
|
467
504
|
|
|
@@ -524,7 +561,7 @@ async function main(argv, { onBackgroundWork } = {}) {
|
|
|
524
561
|
*/
|
|
525
562
|
const openVault = async (repoDir) => {
|
|
526
563
|
const result = await getCachedSdk().gitvault.resolveOrCreateAddress({ address, allow_create: false, ...(repoDir ? { repo_dir: repoDir } : {}) });
|
|
527
|
-
return { vault: result.handle.vault, resolution: result.resolution };
|
|
564
|
+
return { vault: result.handle.vault, keystore: result.handle.keystore, repo_id: result.handle.repo_id, resolution: result.resolution };
|
|
528
565
|
};
|
|
529
566
|
|
|
530
567
|
/**
|
|
@@ -569,7 +606,10 @@ async function main(argv, { onBackgroundWork } = {}) {
|
|
|
569
606
|
note(`vault ${result.handle.repo_id} allocated (genesis ${result.created.genesis_sha256}) — one-shot recovery receipt, keep many copies:`);
|
|
570
607
|
note(JSON.stringify(result.created.recovery_receipt));
|
|
571
608
|
try {
|
|
572
|
-
const { getGitvaultKeystoreRoot } = await import("#sdk/node");
|
|
609
|
+
const { getGitvaultKeystoreRoot, GITVAULT_MIRROR_SETUP_HINT } = await import("#sdk/node");
|
|
610
|
+
// gitvault-mirror-default: every allocation teaches the mirror door
|
|
611
|
+
// beside the recovery receipt, lazy push-to-create included.
|
|
612
|
+
note(GITVAULT_MIRROR_SETUP_HINT);
|
|
573
613
|
note(`keystore: ${getGitvaultKeystoreRoot()} — back this up; whole-machine or whole-keystore loss is terminal for vault history until human envelopes ship`);
|
|
574
614
|
} catch {
|
|
575
615
|
// Never let a diagnostic line fail a push that already allocated successfully.
|
|
@@ -612,14 +652,46 @@ async function main(argv, { onBackgroundWork } = {}) {
|
|
|
612
652
|
fetchMarker = null;
|
|
613
653
|
}
|
|
614
654
|
}
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
655
|
+
// gitvault-restore-recipe design D2/D6: a marker-absent fetch target is,
|
|
656
|
+
// by construction, the WHOLESALE restore shape (a fresh `git clone`, or
|
|
657
|
+
// a target that never completed a restore) — declare restore intent
|
|
658
|
+
// here so a capable gateway's plan rides THIS state read, reused by
|
|
659
|
+
// `fetch` below via `sharedListSession` (never a second round trip). A
|
|
660
|
+
// repo-free `list` (bare `git ls-remote`) has no target to restore into
|
|
661
|
+
// at all, so neither `since` nor `restore` is sent — same gate as today.
|
|
662
|
+
const materializeOpts = fetchMarker ? { deltaSince: fetchMarker.generation } : repoDir ? { restore: true } : {};
|
|
663
|
+
let opened;
|
|
618
664
|
try {
|
|
619
|
-
|
|
620
|
-
|
|
665
|
+
opened = await openVault(repoDir ?? undefined);
|
|
666
|
+
} catch (err) {
|
|
667
|
+
// An unallocated vault is not an error here: `list` is the read half of
|
|
668
|
+
// the protocol dance and must never create anything on its own (D2
|
|
669
|
+
// scopes lazy creation to `push`). Reporting it as an EMPTY ref set is
|
|
670
|
+
// exactly what a fresh repository looks like to git, and `push` still
|
|
671
|
+
// runs `list` first either way — this is what lets a first push land in
|
|
672
|
+
// one command instead of `list` failing the whole exchange before
|
|
673
|
+
// `push` ever gets a turn. Nothing to share with `push` either
|
|
674
|
+
// (design D7): an unallocated vault has no base to reuse.
|
|
675
|
+
if (isVaultNotFound(err)) {
|
|
676
|
+
endBlock();
|
|
677
|
+
return;
|
|
678
|
+
}
|
|
679
|
+
throw err;
|
|
680
|
+
}
|
|
681
|
+
let vault = opened.vault;
|
|
682
|
+
|
|
683
|
+
// gitvault-byo-primary-bucket (design D4, task 3.4 — mirror half): the
|
|
684
|
+
// live materialize (with its own stale-pin retry, unchanged from before
|
|
685
|
+
// this change) is the "attemptLive" `withDegradedRead` wraps — on a
|
|
686
|
+
// NETWORK-CLASS failure (never 4xx) with a mirror configured for this
|
|
687
|
+
// vault, it falls back to `r402s-recover`'s engine against it instead of
|
|
688
|
+
// failing the whole `list`/`clone`. `repoDir` is required for the
|
|
689
|
+
// fallback to materialize into (`out_dir`); a repo-free `list` (bare
|
|
690
|
+
// `git ls-remote`) simply has none, so the fallback never runs and the
|
|
691
|
+
// original error surfaces exactly as it always did.
|
|
692
|
+
const attemptLive = async () => {
|
|
621
693
|
try {
|
|
622
|
-
|
|
694
|
+
return await vault.materialize(materializeOpts);
|
|
623
695
|
} catch (err) {
|
|
624
696
|
// An OFFLINE (id-carrying pin) resolution discovers a stale pin on
|
|
625
697
|
// its FIRST repo-scoped read (client-surface spec, id-pinning
|
|
@@ -634,30 +706,54 @@ async function main(argv, { onBackgroundWork } = {}) {
|
|
|
634
706
|
if (!recovered) throw err;
|
|
635
707
|
note(`pinned vault ${opened.resolution.repo_id} no longer resolves — re-resolved to ${recovered.resolution.repo_id}, retrying`);
|
|
636
708
|
vault = recovered.handle.vault;
|
|
637
|
-
|
|
709
|
+
opened = { ...opened, vault: recovered.handle.vault, keystore: recovered.handle.keystore, repo_id: recovered.handle.repo_id, resolution: recovered.resolution };
|
|
710
|
+
return await vault.materialize(materializeOpts);
|
|
638
711
|
}
|
|
712
|
+
};
|
|
713
|
+
|
|
714
|
+
let outcome;
|
|
715
|
+
try {
|
|
716
|
+
outcome = await getCachedSdk().gitvault.withDegradedRead({ attemptLive, keystore: opened.keystore, repo_id: opened.repo_id, out_dir: repoDir });
|
|
639
717
|
} catch (err) {
|
|
640
|
-
//
|
|
641
|
-
//
|
|
642
|
-
//
|
|
643
|
-
//
|
|
644
|
-
//
|
|
645
|
-
// one command instead of `list` failing the whole exchange before
|
|
646
|
-
// `push` ever gets a turn. Nothing to share with `push` either
|
|
647
|
-
// (design D7): an unallocated vault has no base to reuse.
|
|
718
|
+
// Same "an unallocated vault is not an error" reasoning as above — a
|
|
719
|
+
// 404 discovered only once materialize actually runs takes this same
|
|
720
|
+
// empty-ref-set path (network-class errors never reach here at all:
|
|
721
|
+
// `withDegradedRead` only rethrows them after exhausting its own
|
|
722
|
+
// fallback, and a 404 is never network-class).
|
|
648
723
|
if (isVaultNotFound(err)) {
|
|
649
724
|
endBlock();
|
|
650
725
|
return;
|
|
651
726
|
}
|
|
652
727
|
throw err;
|
|
653
728
|
}
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
729
|
+
|
|
730
|
+
let refs;
|
|
731
|
+
let head;
|
|
732
|
+
if (outcome.degraded) {
|
|
733
|
+
// Exactly ONE stderr line naming the degraded read and its source
|
|
734
|
+
// (design D4) — the validity-not-freshness limit rides inside the
|
|
735
|
+
// canonical statement this composes, verbatim.
|
|
736
|
+
note(gitvaultDegradedReadNote(outcome.result.source));
|
|
737
|
+
refs = outcome.result.refs ?? {};
|
|
738
|
+
head = outcome.result.head_target;
|
|
739
|
+
// No live vault instance materialized this pass — nothing safe to
|
|
740
|
+
// share with a following `push` (design D7's own "unallocated vault"
|
|
741
|
+
// reasoning applies equally to a degraded one: writes are never
|
|
742
|
+
// rerouted, so `push` must resolve and materialize live on its own).
|
|
743
|
+
} else {
|
|
744
|
+
const state = outcome.live;
|
|
745
|
+
refs = state.refs ?? {};
|
|
746
|
+
head = state.head_target;
|
|
747
|
+
// gitvault-session-state-reuse design D1: `fetchState`/`fetchMarker`
|
|
748
|
+
// extend the SAME session-scoped handoff `push` already reuses (`base`)
|
|
749
|
+
// — the `fetch` phase of THIS session reuses this response instead of
|
|
750
|
+
// issuing its own state read. Session-scoped only: dropped the moment a
|
|
751
|
+
// push admits in this same session (see `runPush`'s reset below).
|
|
752
|
+
// `keystore`/`repo_id` ride along so `fetch`'s own degraded-read wrap
|
|
753
|
+
// can fall back without re-resolving.
|
|
754
|
+
if (repoDir) sharedListSession = { repoDir, walletName: resolvedWallet?.name ?? null, vault, keystore: opened.keystore, repo_id: opened.repo_id, base: state, fetchMarker, fetchState: state };
|
|
755
|
+
}
|
|
756
|
+
|
|
661
757
|
for (const ref of Object.keys(refs).sort()) out(`${refs[ref]} ${ref}`);
|
|
662
758
|
// A snapshot-only vault holds protocol refs but no branch heads, so a
|
|
663
759
|
// plain `git clone` prints "cloned an empty repository" with no hint the
|
|
@@ -667,7 +763,6 @@ async function main(argv, { onBackgroundWork } = {}) {
|
|
|
667
763
|
note(`this vault has no branch heads yet — its history lives on ${refNames.sort()[0]}`);
|
|
668
764
|
note(`fetch it with: git fetch <remote> '+${refNames.sort()[0]}:${refNames.sort()[0]}' && git checkout -b restored ${refNames.sort()[0]}`);
|
|
669
765
|
}
|
|
670
|
-
const head = state.head_target;
|
|
671
766
|
// A symref is only advertised when its target is actually present:
|
|
672
767
|
// pointing HEAD at a ref that does not exist is what an empty repository
|
|
673
768
|
// looks like, and git reads the empty list correctly on its own.
|
|
@@ -700,15 +795,55 @@ async function main(argv, { onBackgroundWork } = {}) {
|
|
|
700
795
|
// different repository/wallet, or a push that already admitted in this
|
|
701
796
|
// same session — see `runPush`'s reset) falls back to the vault's own
|
|
702
797
|
// read, unchanged.
|
|
798
|
+
//
|
|
799
|
+
// gitvault-byo-primary-bucket (design D4, task 3.4 — mirror half):
|
|
800
|
+
// either path's actual chain/payload read is the "attemptLive" that
|
|
801
|
+
// `withDegradedRead` wraps — `gitvault.restore(...)` is exactly `open()`
|
|
802
|
+
// followed by `handle.vault.restoreObjectsInto(target_dir)` (see that
|
|
803
|
+
// method's own one-line body), decomposed here so the SECOND half can be
|
|
804
|
+
// wrapped, with byte-identical resolution to today on the live-success
|
|
805
|
+
// path. On a NETWORK-CLASS failure (never 4xx) with a mirror configured
|
|
806
|
+
// for this vault, the fetch is served from it instead of failing the
|
|
807
|
+
// whole clone; the destination it materialized into is the SAME
|
|
808
|
+
// `repoDir` git already prepared, so `git fetch`/`clone` completes
|
|
809
|
+
// exactly as if the live path had run.
|
|
703
810
|
const shared = sharedListSession && sharedListSession.repoDir === repoDir && sharedListSession.walletName === (resolvedWallet?.name ?? null) ? sharedListSession : null;
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
811
|
+
let attemptLive;
|
|
812
|
+
let degradedKeystore;
|
|
813
|
+
let degradedRepoId;
|
|
814
|
+
if (shared) {
|
|
815
|
+
attemptLive = () => shared.vault.restoreObjectsInto(repoDir, { marker: shared.fetchMarker, state: shared.fetchState });
|
|
816
|
+
degradedKeystore = shared.keystore;
|
|
817
|
+
degradedRepoId = shared.repo_id;
|
|
818
|
+
} else {
|
|
819
|
+
const handle = await getCachedSdk().gitvault.open({ ...target, repo_dir: repoDir });
|
|
820
|
+
attemptLive = () => handle.vault.restoreObjectsInto(repoDir);
|
|
821
|
+
degradedKeystore = handle.keystore;
|
|
822
|
+
degradedRepoId = handle.repo_id;
|
|
823
|
+
}
|
|
824
|
+
const outcome = await getCachedSdk().gitvault.withDegradedRead({
|
|
825
|
+
attemptLive,
|
|
826
|
+
keystore: degradedKeystore,
|
|
827
|
+
repo_id: degradedRepoId,
|
|
828
|
+
out_dir: repoDir,
|
|
829
|
+
});
|
|
830
|
+
let restored;
|
|
831
|
+
if (outcome.degraded) {
|
|
832
|
+
// Exactly ONE stderr line naming the degraded read and its source
|
|
833
|
+
// (design D4) — the validity-not-freshness limit rides inside the
|
|
834
|
+
// canonical statement this composes, verbatim. A later `git push`
|
|
835
|
+
// still requires the gateway unchanged — this function only ever
|
|
836
|
+
// serves reads; `runPush` below is untouched by this change.
|
|
837
|
+
note(gitvaultDegradedReadNote(outcome.result.source));
|
|
838
|
+
restored = { generation: outcome.result.generation, retained_refs: outcome.result.retained_refs };
|
|
839
|
+
} else {
|
|
840
|
+
restored = { generation: outcome.live.generation, retained_refs: outcome.live.retained_refs };
|
|
841
|
+
}
|
|
707
842
|
if (verbosity >= 1) note(`restored generation ${restored.generation}`);
|
|
708
843
|
// clone-installs-retained-refs D3: a bookkeeping failure here degrades to
|
|
709
844
|
// exactly today's (pre-change) behavior — one stderr note, fetch still
|
|
710
|
-
// completes. `restored.retained_refs` is never absent (the
|
|
711
|
-
//
|
|
845
|
+
// completes. `restored.retained_refs` is never absent (both the live and
|
|
846
|
+
// the degraded path always return a result, never throw for this step).
|
|
712
847
|
if (restored.retained_refs?.warning) note(restored.retained_refs.warning);
|
|
713
848
|
else if (verbosity >= 1 && (restored.retained_refs?.written.length > 0 || restored.retained_refs?.deleted.length > 0)) {
|
|
714
849
|
note(`refs/r402/retain: +${restored.retained_refs.written.length} -${restored.retained_refs.deleted.length} (${restored.retained_refs.retained_count} retained tip(s) total)`);
|