@run402/sdk 4.95.0 → 4.97.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/README.md +27 -27
- package/core-dist/keystore.d.ts +1 -1
- package/core-dist/keystore.js +1 -1
- package/dist/errors.d.ts +5 -5
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +2 -2
- package/dist/errors.js.map +1 -1
- package/dist/grant-key-credentials.d.ts +38 -0
- package/dist/grant-key-credentials.d.ts.map +1 -0
- package/dist/grant-key-credentials.js +42 -0
- package/dist/grant-key-credentials.js.map +1 -0
- package/dist/index.d.ts +16 -23
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +14 -21
- package/dist/index.js.map +1 -1
- package/dist/namespaces/billing.d.ts +1 -1
- package/dist/namespaces/billing.js +1 -1
- package/dist/namespaces/buzz.js +1 -1
- package/dist/namespaces/buzz.js.map +1 -1
- package/dist/namespaces/credentials.d.ts +4 -4
- package/dist/namespaces/credentials.js +4 -4
- package/dist/namespaces/deploy.d.ts +5 -5
- package/dist/namespaces/deploy.d.ts.map +1 -1
- package/dist/namespaces/deploy.js +25 -25
- package/dist/namespaces/deploy.js.map +1 -1
- package/dist/namespaces/deploy.types.d.ts +17 -17
- package/dist/namespaces/deploy.types.d.ts.map +1 -1
- package/dist/namespaces/domains.d.ts +1 -1
- package/dist/namespaces/domains.js +1 -1
- package/dist/namespaces/errors.d.ts +1 -1
- package/dist/namespaces/errors.js +2 -2
- package/dist/namespaces/escalations.d.ts +2 -2
- package/dist/namespaces/escalations.js +2 -2
- package/dist/namespaces/escalations.types.d.ts +3 -3
- package/dist/namespaces/escalations.types.d.ts.map +1 -1
- package/dist/namespaces/events.d.ts +1 -1
- package/dist/namespaces/events.js +1 -1
- package/dist/namespaces/functions.js +1 -1
- package/dist/namespaces/functions.js.map +1 -1
- package/dist/namespaces/grants.d.ts +43 -9
- package/dist/namespaces/grants.d.ts.map +1 -1
- package/dist/namespaces/grants.js +99 -10
- package/dist/namespaces/grants.js.map +1 -1
- package/dist/namespaces/grants.types.d.ts +120 -15
- package/dist/namespaces/grants.types.d.ts.map +1 -1
- package/dist/namespaces/grants.types.js +12 -3
- package/dist/namespaces/grants.types.js.map +1 -1
- package/dist/namespaces/identity-links.types.d.ts +2 -2
- package/dist/namespaces/identity-links.types.d.ts.map +1 -1
- package/dist/namespaces/org.d.ts +3 -3
- package/dist/namespaces/org.js +3 -3
- package/dist/namespaces/org.types.d.ts +3 -3
- package/dist/namespaces/pay.js +1 -1
- package/dist/namespaces/projects.d.ts +1 -1
- package/dist/namespaces/projects.js +1 -1
- package/dist/namespaces/{gitvault.d.ts → repos.d.ts} +436 -449
- package/dist/namespaces/repos.d.ts.map +1 -0
- package/dist/namespaces/{gitvault.js → repos.js} +452 -460
- package/dist/namespaces/repos.js.map +1 -0
- package/dist/namespaces/rooms.d.ts +3 -3
- package/dist/namespaces/rooms.js +5 -5
- package/dist/namespaces/rooms.js.map +1 -1
- package/dist/namespaces/session.d.ts +2 -2
- package/dist/namespaces/session.d.ts.map +1 -1
- package/dist/namespaces/session.js +2 -2
- package/dist/namespaces/session.js.map +1 -1
- package/dist/namespaces/tier.d.ts +3 -3
- package/dist/namespaces/tier.d.ts.map +1 -1
- package/dist/namespaces/{gitvault.crypto.d.ts → vault.crypto.d.ts} +141 -141
- package/dist/namespaces/vault.crypto.d.ts.map +1 -0
- package/dist/namespaces/{gitvault.crypto.js → vault.crypto.js} +173 -173
- package/dist/namespaces/vault.crypto.js.map +1 -0
- package/dist/namespaces/{gitvault.types.d.ts → vault.types.d.ts} +136 -136
- package/dist/namespaces/vault.types.d.ts.map +1 -0
- package/dist/namespaces/{gitvault.types.js → vault.types.js} +4 -4
- package/dist/namespaces/vault.types.js.map +1 -0
- package/dist/node/bearer-redeem-key.d.ts +1 -1
- package/dist/node/bearer-redeem-key.js +5 -5
- package/dist/node/bearer-redeem-key.js.map +1 -1
- package/dist/node/credentials.d.ts +9 -9
- package/dist/node/credentials.d.ts.map +1 -1
- package/dist/node/credentials.js +12 -12
- package/dist/node/credentials.js.map +1 -1
- package/dist/node/http-dispatcher.js +5 -5
- package/dist/node/http-dispatcher.js.map +1 -1
- package/dist/node/index.d.ts +34 -34
- package/dist/node/index.d.ts.map +1 -1
- package/dist/node/index.js +41 -41
- package/dist/node/index.js.map +1 -1
- package/dist/node/{gitvault-address.d.ts → vault-address.d.ts} +42 -42
- package/dist/node/vault-address.d.ts.map +1 -0
- package/dist/node/{gitvault-address.js → vault-address.js} +43 -43
- package/dist/node/vault-address.js.map +1 -0
- package/dist/node/{gitvault-apply.d.ts → vault-apply.d.ts} +27 -27
- package/dist/node/vault-apply.d.ts.map +1 -0
- package/dist/node/{gitvault-apply.js → vault-apply.js} +38 -38
- package/dist/node/vault-apply.js.map +1 -0
- package/dist/node/vault-byo-config.d.ts +29 -0
- package/dist/node/vault-byo-config.d.ts.map +1 -0
- package/dist/node/{gitvault-byo-config.js → vault-byo-config.js} +18 -18
- package/dist/node/vault-byo-config.js.map +1 -0
- package/dist/node/vault-byo-probe.d.ts +20 -0
- package/dist/node/vault-byo-probe.d.ts.map +1 -0
- package/dist/node/vault-byo-probe.js +57 -0
- package/dist/node/vault-byo-probe.js.map +1 -0
- package/dist/node/{gitvault-creation-journal.d.ts → vault-creation-journal.d.ts} +69 -69
- package/dist/node/vault-creation-journal.d.ts.map +1 -0
- package/dist/node/{gitvault-creation-journal.js → vault-creation-journal.js} +54 -54
- package/dist/node/vault-creation-journal.js.map +1 -0
- package/dist/node/{gitvault-degraded-read.d.ts → vault-degraded-read.d.ts} +34 -34
- package/dist/node/vault-degraded-read.d.ts.map +1 -0
- package/dist/node/{gitvault-degraded-read.js → vault-degraded-read.js} +24 -24
- package/dist/node/vault-degraded-read.js.map +1 -0
- package/dist/node/{gitvault-deploy.d.ts → vault-deploy.d.ts} +67 -67
- package/dist/node/vault-deploy.d.ts.map +1 -0
- package/dist/node/{gitvault-deploy.js → vault-deploy.js} +27 -27
- package/dist/node/vault-deploy.js.map +1 -0
- package/dist/node/{gitvault-edge-fetch.d.ts → vault-edge-fetch.d.ts} +9 -9
- package/dist/node/vault-edge-fetch.d.ts.map +1 -0
- package/dist/node/{gitvault-edge-fetch.js → vault-edge-fetch.js} +11 -11
- package/dist/node/vault-edge-fetch.js.map +1 -0
- package/dist/node/{gitvault-handoff.d.ts → vault-handoff.d.ts} +9 -9
- package/dist/node/vault-handoff.d.ts.map +1 -0
- package/dist/node/{gitvault-handoff.js → vault-handoff.js} +24 -24
- package/dist/node/vault-handoff.js.map +1 -0
- package/dist/node/{gitvault-keystore.d.ts → vault-keystore.d.ts} +87 -77
- package/dist/node/vault-keystore.d.ts.map +1 -0
- package/dist/node/{gitvault-keystore.js → vault-keystore.js} +105 -68
- package/dist/node/vault-keystore.js.map +1 -0
- package/dist/node/{gitvault-member-bundle.d.ts → vault-member-bundle.d.ts} +17 -17
- package/dist/node/vault-member-bundle.d.ts.map +1 -0
- package/dist/node/{gitvault-member-bundle.js → vault-member-bundle.js} +11 -11
- package/dist/node/vault-member-bundle.js.map +1 -0
- package/dist/node/{gitvault-mirror-backend.d.ts → vault-mirror-backend.d.ts} +26 -26
- package/dist/node/vault-mirror-backend.d.ts.map +1 -0
- package/dist/node/{gitvault-mirror-backend.js → vault-mirror-backend.js} +25 -25
- package/dist/node/vault-mirror-backend.js.map +1 -0
- package/dist/node/{gitvault-mirror-config.d.ts → vault-mirror-config.d.ts} +23 -23
- package/dist/node/vault-mirror-config.d.ts.map +1 -0
- package/dist/node/{gitvault-mirror-config.js → vault-mirror-config.js} +16 -16
- package/dist/node/vault-mirror-config.js.map +1 -0
- package/dist/node/{gitvault-mirror.d.ts → vault-mirror.d.ts} +36 -36
- package/dist/node/vault-mirror.d.ts.map +1 -0
- package/dist/node/{gitvault-mirror.js → vault-mirror.js} +75 -75
- package/dist/node/vault-mirror.js.map +1 -0
- package/dist/node/{gitvault-native-crypto.d.ts → vault-native-crypto.d.ts} +10 -10
- package/dist/node/vault-native-crypto.d.ts.map +1 -0
- package/dist/node/{gitvault-native-crypto.js → vault-native-crypto.js} +16 -16
- package/dist/node/vault-native-crypto.js.map +1 -0
- package/dist/node/{gitvault-open-or-create.d.ts → vault-open-or-create.d.ts} +16 -16
- package/dist/node/vault-open-or-create.d.ts.map +1 -0
- package/dist/node/{gitvault-open-or-create.js → vault-open-or-create.js} +10 -10
- package/dist/node/vault-open-or-create.js.map +1 -0
- package/dist/node/{gitvault-prewarm.d.ts → vault-prewarm.d.ts} +13 -13
- package/dist/node/vault-prewarm.d.ts.map +1 -0
- package/dist/node/{gitvault-prewarm.js → vault-prewarm.js} +25 -25
- package/dist/node/vault-prewarm.js.map +1 -0
- package/dist/node/{gitvault-profile-scan.d.ts → vault-profile-scan.d.ts} +3 -3
- package/dist/node/vault-profile-scan.d.ts.map +1 -0
- package/dist/node/{gitvault-profile-scan.js → vault-profile-scan.js} +12 -12
- package/dist/node/vault-profile-scan.js.map +1 -0
- package/dist/node/{gitvault-prune.d.ts → vault-prune.d.ts} +61 -61
- package/dist/node/vault-prune.d.ts.map +1 -0
- package/dist/node/{gitvault-prune.js → vault-prune.js} +34 -34
- package/dist/node/vault-prune.js.map +1 -0
- package/dist/node/{gitvault-publication.d.ts → vault-publication.d.ts} +399 -399
- package/dist/node/vault-publication.d.ts.map +1 -0
- package/dist/node/{gitvault-publication.js → vault-publication.js} +584 -584
- package/dist/node/vault-publication.js.map +1 -0
- package/dist/node/{gitvault-push-to-create.d.ts → vault-push-to-create.d.ts} +16 -16
- package/dist/node/vault-push-to-create.d.ts.map +1 -0
- package/dist/node/{gitvault-push-to-create.js → vault-push-to-create.js} +9 -9
- package/dist/node/vault-push-to-create.js.map +1 -0
- package/dist/node/{gitvault-recover.d.ts → vault-recover.d.ts} +50 -50
- package/dist/node/vault-recover.d.ts.map +1 -0
- package/dist/node/{gitvault-recover.js → vault-recover.js} +84 -84
- package/dist/node/vault-recover.js.map +1 -0
- package/dist/node/{gitvault-restore.d.ts → vault-restore.d.ts} +8 -8
- package/dist/node/vault-restore.d.ts.map +1 -0
- package/dist/node/{gitvault-restore.js → vault-restore.js} +11 -11
- package/dist/node/vault-restore.js.map +1 -0
- package/dist/node/{gitvault-snapshot.d.ts → vault-snapshot.d.ts} +48 -48
- package/dist/node/vault-snapshot.d.ts.map +1 -0
- package/dist/node/{gitvault-snapshot.js → vault-snapshot.js} +61 -61
- package/dist/node/vault-snapshot.js.map +1 -0
- package/dist/node/{gitvault-writer-state.d.ts → vault-writer-state.d.ts} +8 -8
- package/dist/node/vault-writer-state.d.ts.map +1 -0
- package/dist/node/{gitvault-writer-state.js → vault-writer-state.js} +15 -15
- package/dist/node/vault-writer-state.js.map +1 -0
- package/dist/node/workflow-output.js +4 -4
- package/dist/node/workflow-output.js.map +1 -1
- package/dist/project-auth-classification.d.ts +8 -8
- package/dist/project-auth-classification.d.ts.map +1 -1
- package/dist/project-auth-classification.js +7 -7
- package/dist/project-auth-classification.js.map +1 -1
- package/dist/scoped.d.ts +6 -14
- package/dist/scoped.d.ts.map +1 -1
- package/dist/scoped.js +10 -21
- package/dist/scoped.js.map +1 -1
- package/package.json +1 -1
- package/dist/delegate-credentials.d.ts +0 -37
- package/dist/delegate-credentials.d.ts.map +0 -1
- package/dist/delegate-credentials.js +0 -41
- package/dist/delegate-credentials.js.map +0 -1
- package/dist/namespaces/delegates.d.ts +0 -50
- package/dist/namespaces/delegates.d.ts.map +0 -1
- package/dist/namespaces/delegates.js +0 -99
- package/dist/namespaces/delegates.js.map +0 -1
- package/dist/namespaces/delegates.types.d.ts +0 -88
- package/dist/namespaces/delegates.types.d.ts.map +0 -1
- package/dist/namespaces/delegates.types.js +0 -10
- package/dist/namespaces/delegates.types.js.map +0 -1
- package/dist/namespaces/gitvault.crypto.d.ts.map +0 -1
- package/dist/namespaces/gitvault.crypto.js.map +0 -1
- package/dist/namespaces/gitvault.d.ts.map +0 -1
- package/dist/namespaces/gitvault.js.map +0 -1
- package/dist/namespaces/gitvault.types.d.ts.map +0 -1
- package/dist/namespaces/gitvault.types.js.map +0 -1
- package/dist/node/gitvault-address.d.ts.map +0 -1
- package/dist/node/gitvault-address.js.map +0 -1
- package/dist/node/gitvault-apply.d.ts.map +0 -1
- package/dist/node/gitvault-apply.js.map +0 -1
- package/dist/node/gitvault-byo-config.d.ts +0 -29
- package/dist/node/gitvault-byo-config.d.ts.map +0 -1
- package/dist/node/gitvault-byo-config.js.map +0 -1
- package/dist/node/gitvault-byo-probe.d.ts +0 -20
- package/dist/node/gitvault-byo-probe.d.ts.map +0 -1
- package/dist/node/gitvault-byo-probe.js +0 -57
- package/dist/node/gitvault-byo-probe.js.map +0 -1
- package/dist/node/gitvault-creation-journal.d.ts.map +0 -1
- package/dist/node/gitvault-creation-journal.js.map +0 -1
- package/dist/node/gitvault-degraded-read.d.ts.map +0 -1
- package/dist/node/gitvault-degraded-read.js.map +0 -1
- package/dist/node/gitvault-deploy.d.ts.map +0 -1
- package/dist/node/gitvault-deploy.js.map +0 -1
- package/dist/node/gitvault-edge-fetch.d.ts.map +0 -1
- package/dist/node/gitvault-edge-fetch.js.map +0 -1
- package/dist/node/gitvault-handoff.d.ts.map +0 -1
- package/dist/node/gitvault-handoff.js.map +0 -1
- package/dist/node/gitvault-keystore.d.ts.map +0 -1
- package/dist/node/gitvault-keystore.js.map +0 -1
- package/dist/node/gitvault-member-bundle.d.ts.map +0 -1
- package/dist/node/gitvault-member-bundle.js.map +0 -1
- package/dist/node/gitvault-mirror-backend.d.ts.map +0 -1
- package/dist/node/gitvault-mirror-backend.js.map +0 -1
- package/dist/node/gitvault-mirror-config.d.ts.map +0 -1
- package/dist/node/gitvault-mirror-config.js.map +0 -1
- package/dist/node/gitvault-mirror.d.ts.map +0 -1
- package/dist/node/gitvault-mirror.js.map +0 -1
- package/dist/node/gitvault-native-crypto.d.ts.map +0 -1
- package/dist/node/gitvault-native-crypto.js.map +0 -1
- package/dist/node/gitvault-open-or-create.d.ts.map +0 -1
- package/dist/node/gitvault-open-or-create.js.map +0 -1
- package/dist/node/gitvault-prewarm.d.ts.map +0 -1
- package/dist/node/gitvault-prewarm.js.map +0 -1
- package/dist/node/gitvault-profile-scan.d.ts.map +0 -1
- package/dist/node/gitvault-profile-scan.js.map +0 -1
- package/dist/node/gitvault-prune.d.ts.map +0 -1
- package/dist/node/gitvault-prune.js.map +0 -1
- package/dist/node/gitvault-publication.d.ts.map +0 -1
- package/dist/node/gitvault-publication.js.map +0 -1
- package/dist/node/gitvault-push-to-create.d.ts.map +0 -1
- package/dist/node/gitvault-push-to-create.js.map +0 -1
- package/dist/node/gitvault-recover.d.ts.map +0 -1
- package/dist/node/gitvault-recover.js.map +0 -1
- package/dist/node/gitvault-restore.d.ts.map +0 -1
- package/dist/node/gitvault-restore.js.map +0 -1
- package/dist/node/gitvault-snapshot.d.ts.map +0 -1
- package/dist/node/gitvault-snapshot.js.map +0 -1
- package/dist/node/gitvault-writer-state.d.ts.map +0 -1
- package/dist/node/gitvault-writer-state.js.map +0 -1
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `r.
|
|
3
|
-
* API (add-
|
|
2
|
+
* `r.repos` — the host-blind encrypted Git remote, as the SDK's programmatic
|
|
3
|
+
* API (add-vault task 5.10).
|
|
4
4
|
*
|
|
5
5
|
* ARCHITECTURAL LAW (client-surface spec, "All protocol logic lives in the
|
|
6
6
|
* SDK"): every piece of vault protocol behaviour — crypto core, keystore,
|
|
7
7
|
* creation journal, snapshot + capture, publication state machines, ref
|
|
8
8
|
* transactions, verification budget, token exchange, repair — is implemented
|
|
9
|
-
* ONCE here. `run402
|
|
9
|
+
* ONCE here. `run402 repos …`, `git-remote-run402`, and the MCP tools are
|
|
10
10
|
* adapters over this namespace: argument parsing, TTY output, exit codes, and
|
|
11
11
|
* local file I/O only. Anything the CLI can do is reachable programmatically
|
|
12
12
|
* with identical semantics.
|
|
@@ -29,9 +29,9 @@
|
|
|
29
29
|
* never persisted into an agent-surface result store, and never logged.
|
|
30
30
|
*/
|
|
31
31
|
import { LocalError, isRun402Error, isNetworkError } from "../errors.js";
|
|
32
|
-
import {
|
|
32
|
+
import { VAULT_BYO_UNMIRRORED_REMEDY_STATEMENT, VAULT_DEGRADED_READ_STATEMENT, VAULT_DURABILITY_STATEMENT, VAULT_MIRROR_KEYSTORE_STILL_REQUIRED_STATEMENT, VAULT_MIRROR_VALIDITY_NOT_FRESHNESS_STATEMENT, VAULT_TERMINAL_LOSS_DOCTOR_TEXT, VAULT_TERMINAL_LOSS_STATEMENT, VAULT_UNMIRRORED_FINDING_STATEMENT, bytesToHex, computeKeystorePossessionProof, computeSigningKeyPossessionSignature, ed25519PublicKey, fromBase64url, hexToBytes, jcs, parseVaultStrict, randomBytes, sha256Hex, verifyVaultObject, toBase64url, } from "./vault.crypto.js";
|
|
33
33
|
/**
|
|
34
|
-
*
|
|
34
|
+
* vault-agent-envelopes — per-process memo for the session-start
|
|
35
35
|
* fulfilment (`<keystore root>|<repo_id>`) and the enroll-if-absent step
|
|
36
36
|
* (`<keystore root>`), so a process that opens the same vault many times
|
|
37
37
|
* (a `git push` runs several verbs) reconciles and enrolls once.
|
|
@@ -45,8 +45,8 @@ const SESSION_RECONCILED = new Set();
|
|
|
45
45
|
* best-effort.
|
|
46
46
|
*/
|
|
47
47
|
const RECONCILE_FATAL_CODES = new Set([
|
|
48
|
-
"
|
|
49
|
-
"
|
|
48
|
+
"VAULT_ENVELOPE_ALTERED",
|
|
49
|
+
"VAULT_SIGNATURE_INVALID",
|
|
50
50
|
"VAULT_CREATION_CONFLICT",
|
|
51
51
|
]);
|
|
52
52
|
function rethrowFatalReconcile(e) {
|
|
@@ -70,7 +70,7 @@ async function nodeOnly(load, verb) {
|
|
|
70
70
|
return await load();
|
|
71
71
|
}
|
|
72
72
|
catch (e) {
|
|
73
|
-
throw new LocalError(`\`r.
|
|
73
|
+
throw new LocalError(`\`r.repos.${verb}\` needs the Node runtime (keystore + git); import it from a Node process, or use the read-only vault methods in a browser.`, `running vault ${verb}`, { code: "VAULT_NODE_ONLY", details: { cause: e instanceof Error ? e.message : String(e) } });
|
|
74
74
|
}
|
|
75
75
|
}
|
|
76
76
|
/** Byte counts in the refusal text read as MiB — the unit an operator's tier is quoted in. */
|
|
@@ -80,7 +80,7 @@ function mib(bytes) {
|
|
|
80
80
|
/**
|
|
81
81
|
* A client-generated handoff id (RFC 4122 v4), minted BEFORE the mint call
|
|
82
82
|
* (kygit-handoff design D3): `auth_secret`/`wrap_key` derive off it, so it
|
|
83
|
-
* cannot be gateway-assigned the way `internal.
|
|
83
|
+
* cannot be gateway-assigned the way `internal.vault_claims.id` alone
|
|
84
84
|
* would suggest — this SDK supplies it, mirroring the SAME
|
|
85
85
|
* client_creation_id/client_open_id convention this protocol family
|
|
86
86
|
* already uses elsewhere for idempotent creation. `handoff()` refuses if
|
|
@@ -96,8 +96,8 @@ function randomHandoffUuid() {
|
|
|
96
96
|
/**
|
|
97
97
|
* The gateway names the vault by its three ids on the wire — `repo_id`,
|
|
98
98
|
* `org_id`, `project_id` (docs/style.md's API-boundary vocabulary) — on
|
|
99
|
-
* BOTH the handoff mint (`POST /
|
|
100
|
-
* claim (`POST /
|
|
99
|
+
* BOTH the handoff mint (`POST /vaults/v1/:vault_id/handoffs`) and
|
|
100
|
+
* claim (`POST /vaults/v1/handoffs/:handoff_id/redeem`) responses. The SDK
|
|
101
101
|
* groups them under `vault` with the `organization_id` spelling every other
|
|
102
102
|
* SDK result uses. Neither response carries a slug-form address, so
|
|
103
103
|
* `address` is `null` unless the caller already knows one (a slug-form
|
|
@@ -122,8 +122,8 @@ export function handoffMembershipFromWire(wire) {
|
|
|
122
122
|
*/
|
|
123
123
|
export const INVITE_DEFAULT_ROLE = "developer";
|
|
124
124
|
/**
|
|
125
|
-
*
|
|
126
|
-
* entries `
|
|
125
|
+
* vault-byo-primary-bucket task 3.3 — the number of `{key, object_kind}`
|
|
126
|
+
* entries `VAULT_BYO_OBJECT_MISSING`'s `details.missing` lists before
|
|
127
127
|
* truncating (`details.missing_count` always carries the true total, and
|
|
128
128
|
* `details.missing_truncated` says whether the list above was cut). A
|
|
129
129
|
* chain-referenced object list can be arbitrarily large; an error envelope
|
|
@@ -131,14 +131,14 @@ export const INVITE_DEFAULT_ROLE = "developer";
|
|
|
131
131
|
* `sample_keys`-class cap (asset-sync's plan response, `PLATFORM_INCIDENT_
|
|
132
132
|
* FANOUT_CAP`) rather than inventing a new number.
|
|
133
133
|
*/
|
|
134
|
-
export const
|
|
134
|
+
export const VAULT_BYO_OBJECT_MISSING_LIST_CAP = 50;
|
|
135
135
|
/**
|
|
136
136
|
* D210 (rev 44) — the decision function behind `fsck`'s best-effort
|
|
137
|
-
* proof-of-open submission, factored out of {@link
|
|
137
|
+
* proof-of-open submission, factored out of {@link Repos}'s private
|
|
138
138
|
* `#submitFsckOpenProof` so it is directly unit-testable with injected
|
|
139
139
|
* (fake) `resolvePrincipalId`/`submit` dependencies, independent of a real
|
|
140
140
|
* HTTP-backed vault. `fsck()` itself supplies the REAL dependencies (`GET
|
|
141
|
-
* /agent/v1/whoami`, {@link
|
|
141
|
+
* /agent/v1/whoami`, {@link Repos.submitProofOfOpen}) — this function
|
|
142
142
|
* contains no HTTP/transport code of its own.
|
|
143
143
|
*
|
|
144
144
|
* Gating (all LOCAL, no network call made when either holds):
|
|
@@ -173,8 +173,8 @@ export async function computeOpenProofOutcome(input) {
|
|
|
173
173
|
deduplicated: null,
|
|
174
174
|
receipt: null,
|
|
175
175
|
error: {
|
|
176
|
-
code: "
|
|
177
|
-
message: "GET /agent/v1/whoami returned no resolvable principal for this credential (
|
|
176
|
+
code: "VAULT_PROOF_OF_OPEN_PRINCIPAL_UNRESOLVED",
|
|
177
|
+
message: "GET /agent/v1/whoami returned no resolvable principal for this credential (grant-key bearers are not accepted by whoami) — submit via r.repos.submitProofOfOpen(repoId, principalId, evidence) directly instead",
|
|
178
178
|
},
|
|
179
179
|
};
|
|
180
180
|
}
|
|
@@ -197,8 +197,8 @@ export async function computeOpenProofOutcome(input) {
|
|
|
197
197
|
}
|
|
198
198
|
}
|
|
199
199
|
/**
|
|
200
|
-
*
|
|
201
|
-
* behind `fsck`'s BYO presence check, factored out of {@link
|
|
200
|
+
* vault-byo-primary-bucket task 3.3 (design D6) — the decision function
|
|
201
|
+
* behind `fsck`'s BYO presence check, factored out of {@link Repos}'s
|
|
202
202
|
* private `#checkByoPresence` the same way {@link computeOpenProofOutcome}
|
|
203
203
|
* is factored out of `#submitFsckOpenProof`, so it is directly unit-testable
|
|
204
204
|
* with an injected (fake) `hasLocalConfig`/`verifyPresence`, independent of
|
|
@@ -218,15 +218,15 @@ export async function computeOpenProofOutcome(input) {
|
|
|
218
218
|
* NO local credentials configured (`hasLocalConfig()` returns `false`) is
|
|
219
219
|
* reported, never thrown — a BYO vault this machine cannot reach is an
|
|
220
220
|
* honest "not checked", not a failure that should break `fsck` for a
|
|
221
|
-
* credential-less returning agent (see {@link
|
|
221
|
+
* credential-less returning agent (see {@link VaultFsckByoPresence}'s
|
|
222
222
|
* own doc comment). A confirmed absence is the opposite: it THROWS
|
|
223
|
-
* `
|
|
224
|
-
*
|
|
223
|
+
* `VAULT_BYO_OBJECT_MISSING` — the same severity class as {@link
|
|
224
|
+
* Repos.mirrorVerify}'s own `VAULT_MIRROR_NOT_CONFIGURED` throw when
|
|
225
225
|
* `--mirror` is requested against an unconfigured vault — this is exactly
|
|
226
226
|
* the honesty D6 names: "we can tell the customer what SHOULD exist and
|
|
227
227
|
* does not", stated as a real refusal rather than a silently-embedded
|
|
228
228
|
* finding. The listed entries are capped at
|
|
229
|
-
* {@link
|
|
229
|
+
* {@link VAULT_BYO_OBJECT_MISSING_LIST_CAP} (a chain-referenced object
|
|
230
230
|
* list can be arbitrarily large and this is an error envelope, not a
|
|
231
231
|
* report); `missing_count` always carries the true total regardless of how
|
|
232
232
|
* many are listed.
|
|
@@ -250,9 +250,9 @@ export async function computeByoPresenceOutcome(input) {
|
|
|
250
250
|
}
|
|
251
251
|
const report = await input.verifyPresence();
|
|
252
252
|
if (report.missing.length > 0) {
|
|
253
|
-
const missing = report.missing.slice(0,
|
|
254
|
-
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
|
|
255
|
-
code: "
|
|
253
|
+
const missing = report.missing.slice(0, VAULT_BYO_OBJECT_MISSING_LIST_CAP);
|
|
254
|
+
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 vault fsck", {
|
|
255
|
+
code: "VAULT_BYO_OBJECT_MISSING",
|
|
256
256
|
details: {
|
|
257
257
|
repo_id: input.repoId,
|
|
258
258
|
destination: report.destination,
|
|
@@ -272,7 +272,7 @@ export async function computeByoPresenceOutcome(input) {
|
|
|
272
272
|
return { verified: true, destination: report.destination, checked_count: report.checked, not_checked_reason: null };
|
|
273
273
|
}
|
|
274
274
|
/**
|
|
275
|
-
* {@link
|
|
275
|
+
* {@link Repos.deploy}'s post-push glue: attach the best-effort
|
|
276
276
|
* mirror/reconcile hooks to a deploy result, gated on whether this deploy
|
|
277
277
|
* actually landed a new generation in the vault. Extracted as a standalone
|
|
278
278
|
* function (rather than inlined in `deploy()`) purely so the gating can be
|
|
@@ -289,10 +289,10 @@ export async function computeByoPresenceOutcome(input) {
|
|
|
289
289
|
* `reconcile_recipients` are OMITTED rather than a faked `skipped_*` value —
|
|
290
290
|
* there is nothing this deploy did that either hook could report on.
|
|
291
291
|
*/
|
|
292
|
-
export async function
|
|
292
|
+
export async function attachVaultDeployHooks(result, mirror, reconcile) {
|
|
293
293
|
if (result.outcome !== "DEPLOYED_AND_VAULTED" && result.outcome !== "DEPLOY_FAILED_VAULTED")
|
|
294
294
|
return result;
|
|
295
|
-
// Sequential, not Promise.all — same ordering
|
|
295
|
+
// Sequential, not Promise.all — same ordering capture() uses, so a reconcile
|
|
296
296
|
// failure can never be misread as a mirror failure in a log line that
|
|
297
297
|
// assumed ordering.
|
|
298
298
|
const mirrorPush = await mirror();
|
|
@@ -300,7 +300,7 @@ export async function attachGitvaultDeployHooks(result, mirror, reconcile) {
|
|
|
300
300
|
return { ...result, mirror_push: mirrorPush, reconcile_recipients: reconcileRecipients };
|
|
301
301
|
}
|
|
302
302
|
// ─── The namespace ───────────────────────────────────────────────────────────
|
|
303
|
-
export class
|
|
303
|
+
export class Repos {
|
|
304
304
|
#client;
|
|
305
305
|
constructor(client) {
|
|
306
306
|
this.#client = client;
|
|
@@ -308,7 +308,7 @@ export class Gitvault {
|
|
|
308
308
|
// ── Control-plane reads (isomorphic) ──────────────────────────────────────
|
|
309
309
|
/** The vault record — policy, allocation generation, storage + maintenance state. */
|
|
310
310
|
async get(repoId) {
|
|
311
|
-
return this.#client.request(`/
|
|
311
|
+
return this.#client.request(`/vaults/v1/${encodeURIComponent(repoId)}`, { context: "reading the vault record" });
|
|
312
312
|
}
|
|
313
313
|
/**
|
|
314
314
|
* Resolve a project's vault with no local state. This is the cold-restart
|
|
@@ -316,12 +316,12 @@ export class Gitvault {
|
|
|
316
316
|
* the project learns its `repo_id` here.
|
|
317
317
|
*/
|
|
318
318
|
async forProject(projectId) {
|
|
319
|
-
return this.#client.request(`/
|
|
319
|
+
return this.#client.request(`/vaults/v1?project_id=${encodeURIComponent(projectId)}`, { context: "resolving the project's vault" });
|
|
320
320
|
}
|
|
321
321
|
/**
|
|
322
322
|
* Every vault the organization owns, one round trip — `repos list`'s bulk
|
|
323
323
|
* read. See {@link
|
|
324
|
-
*
|
|
324
|
+
* VaultOrgVaultsListing}'s doc comment for the FROZEN response shape and
|
|
325
325
|
* the 404-until-shipped fallback contract.
|
|
326
326
|
*/
|
|
327
327
|
async listByOrg(orgId) {
|
|
@@ -334,7 +334,7 @@ export class Gitvault {
|
|
|
334
334
|
const all = [];
|
|
335
335
|
let cursor;
|
|
336
336
|
for (let page = 0; page < 100; page++) {
|
|
337
|
-
const url = `/
|
|
337
|
+
const url = `/vaults/v1?org_id=${encodeURIComponent(orgId)}` + (cursor ? `&cursor=${encodeURIComponent(cursor)}` : "");
|
|
338
338
|
const res = await this.#client.request(url, { context: "listing the organization's vaults" });
|
|
339
339
|
all.push(...res.vaults);
|
|
340
340
|
if (!res.has_more || !res.next_cursor)
|
|
@@ -345,18 +345,18 @@ export class Gitvault {
|
|
|
345
345
|
}
|
|
346
346
|
/**
|
|
347
347
|
* Resolve a vault by its address-form `org-slug/name` —
|
|
348
|
-
* `GET /
|
|
348
|
+
* `GET /vaults/v1?repo=<org-slug>/<name>`.
|
|
349
349
|
* `RESOURCE_NOT_FOUND` for no such org OR no such name (deliberately
|
|
350
350
|
* collapsed — see the design's slug-namespace-probing note);
|
|
351
|
-
* `SLUG_RELEASED` (read it with {@link
|
|
351
|
+
* `SLUG_RELEASED` (read it with {@link vaultSlugReleasedInfo}) while the
|
|
352
352
|
* slug is in its post-rename cooldown — never auto-followed.
|
|
353
353
|
*/
|
|
354
354
|
async forRepo(address) {
|
|
355
355
|
const repo = `${address.org_slug}/${address.repo_name}`;
|
|
356
|
-
return this.#client.request(`/
|
|
356
|
+
return this.#client.request(`/vaults/v1?repo=${encodeURIComponent(repo)}`, { context: "resolving the vault by repo address" });
|
|
357
357
|
}
|
|
358
358
|
/**
|
|
359
|
-
* Resolve a parsed remote address (`
|
|
359
|
+
* Resolve a parsed remote address (`parseVaultRemoteUrl`'s output),
|
|
360
360
|
* dispatching on its form: id-form resolves exactly like
|
|
361
361
|
* {@link forProject}; slug-form resolves via {@link forRepo}. A pure read —
|
|
362
362
|
* no pinning, no creation. Node-only callers wanting BOTH should use
|
|
@@ -364,7 +364,7 @@ export class Gitvault {
|
|
|
364
364
|
* and (opt-in) push-to-create.
|
|
365
365
|
*/
|
|
366
366
|
async resolveAddress(address) {
|
|
367
|
-
return
|
|
367
|
+
return vaultRemoteAddressForm(address) === "id"
|
|
368
368
|
? this.forProject(address.project_id)
|
|
369
369
|
: this.forRepo({ org_slug: address.org_id, repo_name: address.project_id });
|
|
370
370
|
}
|
|
@@ -384,7 +384,7 @@ export class Gitvault {
|
|
|
384
384
|
const qs = new URLSearchParams({ after_generation: request.after_generation, limit: request.limit });
|
|
385
385
|
if (request.cursor !== undefined)
|
|
386
386
|
qs.set("cursor", request.cursor);
|
|
387
|
-
return this.#client.request(`/
|
|
387
|
+
return this.#client.request(`/vaults/v1/${encodeURIComponent(repoId)}/heads?${qs.toString()}`, { context: "listing vault heads" });
|
|
388
388
|
}
|
|
389
389
|
/**
|
|
390
390
|
* Walk the whole listing above the anchor, verifying page coupling, ordering,
|
|
@@ -413,10 +413,10 @@ export class Gitvault {
|
|
|
413
413
|
* `required`.
|
|
414
414
|
*/
|
|
415
415
|
async setPolicy(repoId, input) {
|
|
416
|
-
return this.#client.request(`/
|
|
416
|
+
return this.#client.request(`/vaults/v1/${encodeURIComponent(repoId)}/policy`, {
|
|
417
417
|
method: "PATCH",
|
|
418
|
-
body: {
|
|
419
|
-
context: "setting the
|
|
418
|
+
body: { vault_policy: input.vault_policy, ...(input.reason !== undefined ? { reason: input.reason } : {}) },
|
|
419
|
+
context: "setting the vault activation policy",
|
|
420
420
|
});
|
|
421
421
|
}
|
|
422
422
|
/**
|
|
@@ -425,7 +425,7 @@ export class Gitvault {
|
|
|
425
425
|
* clears it.
|
|
426
426
|
*/
|
|
427
427
|
async completeOverride(repoId, input) {
|
|
428
|
-
return this.#client.request(`/
|
|
428
|
+
return this.#client.request(`/vaults/v1/${encodeURIComponent(repoId)}/override-completions`, { method: "POST", body: input, context: "submitting the vault override completion" });
|
|
429
429
|
}
|
|
430
430
|
/**
|
|
431
431
|
* Take the owner's maintenance reservation for a compact/prune cycle.
|
|
@@ -440,22 +440,22 @@ export class Gitvault {
|
|
|
440
440
|
// `gitvault.rotate`) — same "direct #client.request" shape as `setPolicy`/
|
|
441
441
|
// `completeOverride` above. The actual ROTATION (sampling K_e, sealing
|
|
442
442
|
// envelopes, submitting the head) needs the Node-only keystore + crypto
|
|
443
|
-
// core and lives on `
|
|
443
|
+
// core and lives on `Vault` (`sdk/src/node/vault-publication.ts`);
|
|
444
444
|
// see {@link rotateEpoch} / {@link rotateEpochForKeyRevocation} below.
|
|
445
445
|
/** `POST …/recipients/:principal_id/confirm` (D197) — first-seen pin confirmation. */
|
|
446
446
|
async confirmRecipient(repoId, principalId, newFingerprint) {
|
|
447
|
-
return this.#client.request(`/
|
|
448
|
-
method: "POST", body: { new_fingerprint: newFingerprint }, context: "confirming a
|
|
447
|
+
return this.#client.request(`/vaults/v1/${encodeURIComponent(repoId)}/recipients/${encodeURIComponent(principalId)}/confirm`, {
|
|
448
|
+
method: "POST", body: { new_fingerprint: newFingerprint }, context: "confirming a vault recipient's first pin",
|
|
449
449
|
});
|
|
450
450
|
}
|
|
451
451
|
/** `POST …/recipients/:principal_id/repin` (D197) — re-pin ceremony. */
|
|
452
452
|
async repinRecipient(repoId, principalId, input) {
|
|
453
|
-
return this.#client.request(`/
|
|
454
|
-
method: "POST", body: input, context: "re-pinning a
|
|
453
|
+
return this.#client.request(`/vaults/v1/${encodeURIComponent(repoId)}/recipients/${encodeURIComponent(principalId)}/repin`, {
|
|
454
|
+
method: "POST", body: input, context: "re-pinning a vault recipient",
|
|
455
455
|
});
|
|
456
456
|
}
|
|
457
457
|
/**
|
|
458
|
-
*
|
|
458
|
+
* vault-agent-envelopes D3 — a KEY-HOLDER explicitly accepts a recipient's
|
|
459
459
|
* CHANGED key. The session-start reconcile refuses to wrap under a
|
|
460
460
|
* fingerprint that differs from this keystore's TOFU pin
|
|
461
461
|
* (`pinned_key_mismatch`) — that refusal is the substitution defence and
|
|
@@ -471,11 +471,11 @@ export class Gitvault {
|
|
|
471
471
|
*/
|
|
472
472
|
async acceptRecipientKeyChange(options) {
|
|
473
473
|
const repoId = await this.#resolveRepoId(options);
|
|
474
|
-
const {
|
|
475
|
-
const keystore = new
|
|
474
|
+
const { VaultKeystore } = await this.#keystore();
|
|
475
|
+
const keystore = new VaultKeystore(options.keystore_root !== undefined ? { rootDir: options.keystore_root } : {});
|
|
476
476
|
const repo = keystore.readRepo(repoId);
|
|
477
477
|
if (!repo)
|
|
478
|
-
throw new LocalError("this keystore holds no repo state for the vault — only a key-holder can accept a recipient's key change", "accepting a recipient key change", { code: "
|
|
478
|
+
throw new LocalError("this keystore holds no repo state for the vault — only a key-holder can accept a recipient's key change", "accepting a recipient key change", { code: "VAULT_REPO_STATE_MISSING", details: { repo_id: repoId } });
|
|
479
479
|
const record = await this.get(repoId);
|
|
480
480
|
const directory = await this.#client.request(`/orgs/v1/${encodeURIComponent(record.org_id)}/encryption-keys`, { context: "reading the org encryption-key directory" });
|
|
481
481
|
const served = directory.keys.find((k) => k.principal_id === options.principal_id)?.ek_fingerprint ?? null;
|
|
@@ -504,8 +504,8 @@ export class Gitvault {
|
|
|
504
504
|
* fully self-contained entry point.
|
|
505
505
|
*/
|
|
506
506
|
async declareRecipientKeyRevoked(repoId, principalId) {
|
|
507
|
-
return this.#client.request(`/
|
|
508
|
-
method: "POST", body: {}, context: "declaring a
|
|
507
|
+
return this.#client.request(`/vaults/v1/${encodeURIComponent(repoId)}/recipients/${encodeURIComponent(principalId)}/key-revocation`, {
|
|
508
|
+
method: "POST", body: {}, context: "declaring a vault recipient key revoked",
|
|
509
509
|
});
|
|
510
510
|
}
|
|
511
511
|
/**
|
|
@@ -519,11 +519,11 @@ export class Gitvault {
|
|
|
519
519
|
* comment on the confirmed gap in what the gateway exposes today).
|
|
520
520
|
*/
|
|
521
521
|
async declareEpochSecretExposed(repoId) {
|
|
522
|
-
return this.#client.request(`/
|
|
522
|
+
return this.#client.request(`/vaults/v1/${encodeURIComponent(repoId)}/epoch-secret-exposure`, { method: "POST", body: {}, context: "declaring a vault epoch secret exposed" });
|
|
523
523
|
}
|
|
524
524
|
/** `POST …/writer-authority/declare-unavailable` (D202) — an explicit, audited fact that the writer signing key is gone. */
|
|
525
525
|
async declareWriterAuthorityUnavailable(repoId) {
|
|
526
|
-
return this.#client.request(`/
|
|
526
|
+
return this.#client.request(`/vaults/v1/${encodeURIComponent(repoId)}/writer-authority/declare-unavailable`, { method: "POST", body: {}, context: "declaring vault writer authority unavailable" });
|
|
527
527
|
}
|
|
528
528
|
/**
|
|
529
529
|
* `POST …/recipients/:principal_id/proof-of-open` (D210, rev 44) — submit
|
|
@@ -538,18 +538,18 @@ export class Gitvault {
|
|
|
538
538
|
*
|
|
539
539
|
* Self-match only: the gateway requires `principalId` to equal the
|
|
540
540
|
* AUTHENTICATED caller, never overridable by any credential class — a
|
|
541
|
-
* mismatch is the ordinary 403 `
|
|
541
|
+
* mismatch is the ordinary 403 `VAULT_ACCESS_DENIED`. Idempotent on
|
|
542
542
|
* `(repo_id, principal_id, ek_fingerprint, decryptable_to_generation)` —
|
|
543
543
|
* `deduplicated: true` means the gateway returned the tuple's EXISTING
|
|
544
544
|
* receipt (HTTP 200) rather than minting a fresh one (HTTP 201).
|
|
545
545
|
*/
|
|
546
546
|
async submitProofOfOpen(repoId, principalId, evidence) {
|
|
547
|
-
const res = await this.#client.requestWithResponse(`/
|
|
547
|
+
const res = await this.#client.requestWithResponse(`/vaults/v1/${encodeURIComponent(repoId)}/recipients/${encodeURIComponent(principalId)}/proof-of-open`, { method: "POST", body: evidence, context: "submitting a vault proof-of-open receipt" });
|
|
548
548
|
return { receipt: res.body, deduplicated: res.status === 200 };
|
|
549
549
|
}
|
|
550
550
|
async acquireMaintenanceLease(request) {
|
|
551
551
|
const { repo_id, ...body } = request;
|
|
552
|
-
return this.#client.request(`/
|
|
552
|
+
return this.#client.request(`/vaults/v1/${encodeURIComponent(repo_id)}/maintenance-leases`, {
|
|
553
553
|
method: "POST",
|
|
554
554
|
body: {
|
|
555
555
|
base_head_sha256: body.base_head_sha256,
|
|
@@ -559,7 +559,7 @@ export class Gitvault {
|
|
|
559
559
|
p_before_c1_size_bytes: body.p_before_c1_size_bytes ?? "0",
|
|
560
560
|
p_before_c2_size_bytes: body.p_before_c2_size_bytes ?? "0",
|
|
561
561
|
},
|
|
562
|
-
context: "acquiring the
|
|
562
|
+
context: "acquiring the vault maintenance lease",
|
|
563
563
|
});
|
|
564
564
|
}
|
|
565
565
|
// ── Node-only verbs ───────────────────────────────────────────────────────
|
|
@@ -569,23 +569,23 @@ export class Gitvault {
|
|
|
569
569
|
* surface (ref transactions, repair, checkpoint building).
|
|
570
570
|
*/
|
|
571
571
|
async open(options = {}) {
|
|
572
|
-
const [{
|
|
572
|
+
const [{ Vault, createVaultHttpTransport }, { VaultKeystore }] = await Promise.all([this.#publication(), this.#keystore()]);
|
|
573
573
|
const repoId = await this.#resolveRepoId(options);
|
|
574
|
-
const keystore = new
|
|
575
|
-
//
|
|
574
|
+
const keystore = new VaultKeystore(options.keystore_root !== undefined ? { rootDir: options.keystore_root } : {});
|
|
575
|
+
// vault-object-host-predial (design D1, task 1.2): this is the ONE
|
|
576
576
|
// transport whose object reads back the returned handle's vault, so
|
|
577
577
|
// wiring the origin-observation hook here (and nowhere else `open()`
|
|
578
578
|
// is reached from — `openOrCreate`'s own allocation-only transport
|
|
579
579
|
// never serves object reads) covers every read path.
|
|
580
|
-
const transport =
|
|
580
|
+
const transport = createVaultHttpTransport(this.#client, {
|
|
581
581
|
onObjectStoreOriginObserved: (rid, origins) => keystore.recordObjectStoreOrigins(rid, origins),
|
|
582
582
|
byoBackend: await this.#byoBackendResolver(keystore),
|
|
583
583
|
});
|
|
584
|
-
//
|
|
584
|
+
// vault-agent-envelopes D3: enroll the keystore's key if this principal
|
|
585
585
|
// has none (publish + possession proof, one round trip each); a differing
|
|
586
586
|
// current key REFUSES here — rotation is never automatic.
|
|
587
587
|
const enrollment = await this.#ensureEnrolled(keystore);
|
|
588
|
-
const vault = new
|
|
588
|
+
const vault = new Vault({
|
|
589
589
|
keystore,
|
|
590
590
|
transport,
|
|
591
591
|
repo_id: repoId,
|
|
@@ -594,13 +594,13 @@ export class Gitvault {
|
|
|
594
594
|
...(options.service_public_key !== undefined ? { service_public_key: options.service_public_key } : {}),
|
|
595
595
|
});
|
|
596
596
|
// D4: a cold keystore restores from its own envelope (or fails
|
|
597
|
-
//
|
|
597
|
+
// VAULT_ENVELOPE_PENDING with the exact next actions). The lazy
|
|
598
598
|
// fallback is an ALLOWLIST of typed transport/absence states, not a
|
|
599
599
|
// denylist of known verdicts (consult round 2 §5): a network fault, an
|
|
600
600
|
// older gateway missing the route (404/501), or a vault with no admitted
|
|
601
601
|
// genesis yet (CHAIN_BROKEN — not born, nothing to restore or disclose)
|
|
602
602
|
// leave the handle lazy so the verb fails exactly where it always did
|
|
603
|
-
// (`
|
|
603
|
+
// (`VAULT_REPO_STATE_MISSING` at first use). EVERYTHING else —
|
|
604
604
|
// integrity failures, parse failures, programming errors — propagates.
|
|
605
605
|
// A keystore with NO identity has nothing to restore and nothing that
|
|
606
606
|
// could be disclosed — stay lazy and let the verb hit the ordinary
|
|
@@ -674,7 +674,7 @@ export class Gitvault {
|
|
|
674
674
|
}
|
|
675
675
|
}
|
|
676
676
|
}
|
|
677
|
-
//
|
|
677
|
+
// vault-multi-writer (task 5.7) — the SAME "session-start"/"read"
|
|
678
678
|
// wiring point as the envelope reconcile above, but a genuinely simpler
|
|
679
679
|
// policy: no disclosure risk, so only `forbidden` is honored — no
|
|
680
680
|
// custody gate, no `deferred`/memoization branch.
|
|
@@ -682,7 +682,7 @@ export class Gitvault {
|
|
|
682
682
|
return { repo_id: repoId, keystore, transport, vault, restored, reconcile_recipients: reconcileRecipients, writer_reconcile: writerReconcile, enrollment };
|
|
683
683
|
}
|
|
684
684
|
/**
|
|
685
|
-
*
|
|
685
|
+
* vault-agent-envelopes D3 — enroll-if-absent, checked FRESH on every
|
|
686
686
|
* open (consult round 2 §4: memoizing this decision let a long-lived
|
|
687
687
|
* process keep trusting a key an owner had since revoked). Reads
|
|
688
688
|
* `GET /agent/v1/whoami` (the gateway folds the principal's current key
|
|
@@ -693,7 +693,7 @@ export class Gitvault {
|
|
|
693
693
|
* - current key = this keystore's key, still `pending` → finish the
|
|
694
694
|
* activation (a crashed earlier enrollment);
|
|
695
695
|
* - current key = this keystore's key, `active` → no-op;
|
|
696
|
-
* - current key ≠ this keystore's key → `
|
|
696
|
+
* - current key ≠ this keystore's key → `VAULT_KEY_ROTATION_REQUIRED`.
|
|
697
697
|
* Never published as a rotation: a stolen wallet key must not silently
|
|
698
698
|
* replace a member's decryption identity. The remedies are the
|
|
699
699
|
* gateway's — restore the keystore backup; an org owner revokes the
|
|
@@ -710,7 +710,7 @@ export class Gitvault {
|
|
|
710
710
|
catch (e) {
|
|
711
711
|
// An older gateway, a credential whoami refuses, or a transport fault:
|
|
712
712
|
// enrollment is best-effort on the way IN — the verb itself still runs
|
|
713
|
-
// (and a cold open will say `
|
|
713
|
+
// (and a cold open will say `VAULT_ENVELOPE_PENDING` honestly if it
|
|
714
714
|
// needed an envelope this principal never enrolled for). A keystore
|
|
715
715
|
// with no identity reaches the ordinary KEYSTORE_MISSING wall unchanged.
|
|
716
716
|
return { outcome: identity ? "skipped_error" : "skipped_no_identity", ek_fingerprint: identity?.encryption_fingerprint ?? null, signing_fingerprint: identity?.signing_fingerprint ?? null, error: e instanceof Error ? e.message : String(e) };
|
|
@@ -725,7 +725,7 @@ export class Gitvault {
|
|
|
725
725
|
return { outcome: "skipped_not_enrollable", ek_fingerprint: identity?.encryption_fingerprint ?? null, signing_fingerprint: identity?.signing_fingerprint ?? null };
|
|
726
726
|
}
|
|
727
727
|
if (identity?.enrolled_principal_id && identity.enrolled_principal_id !== whoami.principal.id) {
|
|
728
|
-
throw new LocalError(`this keystore identity enrolled as principal ${identity.enrolled_principal_id}, but the active credential resolves to ${whoami.principal.id} — refusing cross-profile reuse of a custody identity`, "enrolling the keystore encryption key", { code: "
|
|
728
|
+
throw new LocalError(`this keystore identity enrolled as principal ${identity.enrolled_principal_id}, but the active credential resolves to ${whoami.principal.id} — refusing cross-profile reuse of a custody identity`, "enrolling the keystore encryption key", { code: "VAULT_IDENTITY_PROFILE_MISMATCH", details: { enrolled_principal_id: identity.enrolled_principal_id, principal_id: whoami.principal.id, keystore_root: keystore.rootDir } });
|
|
729
729
|
}
|
|
730
730
|
const current = whoami.encryption_key ?? null;
|
|
731
731
|
// A member joining from a FRESH machine has no keystore identity yet.
|
|
@@ -739,27 +739,27 @@ export class Gitvault {
|
|
|
739
739
|
identity = keystore.ensureIdentity();
|
|
740
740
|
if (!identity) {
|
|
741
741
|
throw new LocalError(`this principal has a published encryption key (${current.ek_fingerprint}) but this keystore holds no identity — the keystore that enrolled it is not this one; rotation is never automatic`, "enrolling the keystore encryption key", {
|
|
742
|
-
code: "
|
|
742
|
+
code: "VAULT_KEY_ROTATION_REQUIRED",
|
|
743
743
|
details: { current_ek_fingerprint: current.ek_fingerprint, current_state: current.state, local_ek_fingerprint: null, keystore_root: keystore.rootDir },
|
|
744
744
|
next_actions: [
|
|
745
|
-
{ type: "edit_request", why: "restore the keystore backup that holds the current key (~/.config/run402/
|
|
746
|
-
{ type: "edit_request", why: "have an org owner revoke the stale key: DELETE /orgs/v1/:org_id/members/:principal_id/encryption-key (owner + step-up); your next
|
|
745
|
+
{ type: "edit_request", why: "restore the keystore backup that holds the current key (~/.config/run402/vault or the wallet profile's vault dir)" },
|
|
746
|
+
{ type: "edit_request", why: "have an org owner revoke the stale key: DELETE /orgs/v1/:org_id/members/:principal_id/encryption-key (owner + step-up); your next vault operation then enrolls a fresh key" },
|
|
747
747
|
{ type: "edit_request", why: "if this principal is the sole custody-eligible member of every org it belongs to: POST /agent/v1/whoami/encryption-key with replace_current: true" },
|
|
748
748
|
],
|
|
749
749
|
});
|
|
750
750
|
}
|
|
751
751
|
if (current && current.ek_fingerprint !== identity.encryption_fingerprint) {
|
|
752
752
|
throw new LocalError(`this principal's published encryption key (${current.ek_fingerprint}) is not this keystore's key (${identity.encryption_fingerprint}); rotation is never automatic`, "enrolling the keystore encryption key", {
|
|
753
|
-
code: "
|
|
753
|
+
code: "VAULT_KEY_ROTATION_REQUIRED",
|
|
754
754
|
details: { current_ek_fingerprint: current.ek_fingerprint, current_state: current.state, local_ek_fingerprint: identity.encryption_fingerprint, keystore_root: keystore.rootDir },
|
|
755
755
|
next_actions: [
|
|
756
|
-
{ type: "edit_request", why: "restore the keystore backup that holds the current key (~/.config/run402/
|
|
757
|
-
{ type: "edit_request", why: "have an org owner revoke the stale key: DELETE /orgs/v1/:org_id/members/:principal_id/encryption-key (owner + step-up); your next
|
|
756
|
+
{ type: "edit_request", why: "restore the keystore backup that holds the current key (~/.config/run402/vault or the wallet profile's vault dir)" },
|
|
757
|
+
{ type: "edit_request", why: "have an org owner revoke the stale key: DELETE /orgs/v1/:org_id/members/:principal_id/encryption-key (owner + step-up); your next vault operation then enrolls this key" },
|
|
758
758
|
{ type: "edit_request", why: "if this principal is the sole custody-eligible member of every org it belongs to: POST /agent/v1/whoami/encryption-key with replace_current: true" },
|
|
759
759
|
],
|
|
760
760
|
});
|
|
761
761
|
}
|
|
762
|
-
//
|
|
762
|
+
// vault-multi-writer rev 47 (task 5.3, design D9) — the signing half's
|
|
763
763
|
// OWN continuity check, parallel to the encryption half's: does the
|
|
764
764
|
// directory's currently-published signing_fingerprint already equal
|
|
765
765
|
// this keystore's? UNLIKE the encryption half this is never a rotation
|
|
@@ -773,7 +773,7 @@ export class Gitvault {
|
|
|
773
773
|
}
|
|
774
774
|
const keypair = keystore.encryptionKeypair(identity);
|
|
775
775
|
if (!keypair) {
|
|
776
|
-
throw new LocalError("the keystore identity has no X25519 private key — it cannot enroll or open envelopes", "enrolling the keystore encryption key", { code: "VAULT_UNRECOVERABLE", details: { statement:
|
|
776
|
+
throw new LocalError("the keystore identity has no X25519 private key — it cannot enroll or open envelopes", "enrolling the keystore encryption key", { code: "VAULT_UNRECOVERABLE", details: { statement: VAULT_TERMINAL_LOSS_STATEMENT } });
|
|
777
777
|
}
|
|
778
778
|
// The signing half rides the SAME publish call as the encryption half
|
|
779
779
|
// (task 4.1's gateway contract: one round trip, both halves, all-or-
|
|
@@ -838,7 +838,7 @@ export class Gitvault {
|
|
|
838
838
|
* allocation on first push).
|
|
839
839
|
*
|
|
840
840
|
* This is the ONE primitive the remote helper and the capture lane
|
|
841
|
-
* (`run402
|
|
841
|
+
* (`run402 repos capture`/`snapshot`) drive so that a push against an
|
|
842
842
|
* unallocated project runs the six-stage creation journal inline instead of
|
|
843
843
|
* refusing: it resolves `repo_id` from `project_id` exactly like
|
|
844
844
|
* {@link open}, and when that resolution fails, tries to create the vault —
|
|
@@ -858,19 +858,19 @@ export class Gitvault {
|
|
|
858
858
|
*/
|
|
859
859
|
async openOrCreate(options) {
|
|
860
860
|
if (!options.repo_id && !options.project_id) {
|
|
861
|
-
throw new LocalError("pass repo_id, or project_id to resolve it from the control plane", "opening the
|
|
861
|
+
throw new LocalError("pass repo_id, or project_id to resolve it from the control plane", "opening the vault", { code: "VAULT_UNRESOLVED" });
|
|
862
862
|
}
|
|
863
863
|
// An explicit repo_id addresses the vault directly — there is nothing to
|
|
864
864
|
// resolve or lazily create from an id alone, so this degrades to a plain
|
|
865
865
|
// open (matches `open()`'s own precedence: repo_id always wins).
|
|
866
866
|
if (options.repo_id) {
|
|
867
867
|
const handle = await this.open(options);
|
|
868
|
-
return { handle, found: true, created: null, terminal_loss_statement:
|
|
868
|
+
return { handle, found: true, created: null, terminal_loss_statement: VAULT_TERMINAL_LOSS_STATEMENT };
|
|
869
869
|
}
|
|
870
|
-
const [{
|
|
871
|
-
const keystore = new
|
|
872
|
-
const transport =
|
|
873
|
-
const result = await
|
|
870
|
+
const [{ createVaultHttpTransport }, { VaultKeystore }, { openOrCreateVault }] = await Promise.all([this.#publication(), this.#keystore(), this.#openOrCreate()]);
|
|
871
|
+
const keystore = new VaultKeystore(options.keystore_root !== undefined ? { rootDir: options.keystore_root } : {});
|
|
872
|
+
const transport = createVaultHttpTransport(this.#client, { byoBackend: await this.#byoBackendResolver(keystore) });
|
|
873
|
+
const result = await openOrCreateVault({
|
|
874
874
|
keystore,
|
|
875
875
|
transport,
|
|
876
876
|
project_id: options.project_id,
|
|
@@ -880,29 +880,29 @@ export class Gitvault {
|
|
|
880
880
|
});
|
|
881
881
|
const handle = await this.open({ ...options, repo_id: result.repo_id });
|
|
882
882
|
return result.found
|
|
883
|
-
? { handle, found: true, created: null, terminal_loss_statement:
|
|
884
|
-
: { handle, found: false, created: result.created, terminal_loss_statement:
|
|
883
|
+
? { handle, found: true, created: null, terminal_loss_statement: VAULT_TERMINAL_LOSS_STATEMENT }
|
|
884
|
+
: { handle, found: false, created: result.created, terminal_loss_statement: VAULT_TERMINAL_LOSS_STATEMENT };
|
|
885
885
|
}
|
|
886
886
|
/**
|
|
887
887
|
* Resolve a parsed remote address to an OPEN handle, pinning `repo_id` in
|
|
888
888
|
* local git state on the first successful resolution of EITHER form
|
|
889
|
-
* (
|
|
889
|
+
* (vault-client-round-trips design D4 widens the original slug-form-only
|
|
890
890
|
* pin to id-form too — an id-form address needs no pin to survive a
|
|
891
891
|
* rename, but the resolution round trip it skips is the same either way),
|
|
892
892
|
* and — when `allow_create` is set and resolution misses — push-to-create
|
|
893
|
-
* it. This is what the remote helper and `
|
|
893
|
+
* it. This is what the remote helper and `vault snapshot` drive for a
|
|
894
894
|
* `run402::<org>/<name>` remote, whichever form it names, so a caller need
|
|
895
895
|
* not branch on the address's form itself. A pinned id/address that no
|
|
896
896
|
* longer resolves (404) clears the pin and re-resolves once.
|
|
897
897
|
*
|
|
898
898
|
* `SLUG_RELEASED` is NEVER auto-followed — it rethrows unchanged; read it
|
|
899
|
-
* with {@link
|
|
899
|
+
* with {@link vaultSlugReleasedInfo} for the successor slug and cooldown.
|
|
900
900
|
*/
|
|
901
901
|
async resolveOrCreateAddress(options) {
|
|
902
|
-
const [{
|
|
903
|
-
const keystore = new
|
|
904
|
-
const transport =
|
|
905
|
-
const resolution = await
|
|
902
|
+
const [{ createVaultHttpTransport }, { VaultKeystore }, { resolveVaultAddress }] = await Promise.all([this.#publication(), this.#keystore(), this.#address()]);
|
|
903
|
+
const keystore = new VaultKeystore(options.keystore_root !== undefined ? { rootDir: options.keystore_root } : {});
|
|
904
|
+
const transport = createVaultHttpTransport(this.#client, { byoBackend: await this.#byoBackendResolver(keystore) });
|
|
905
|
+
const resolution = await resolveVaultAddress({
|
|
906
906
|
keystore,
|
|
907
907
|
transport,
|
|
908
908
|
address: options.address,
|
|
@@ -917,13 +917,13 @@ export class Gitvault {
|
|
|
917
917
|
handle,
|
|
918
918
|
found: resolution.via !== "created",
|
|
919
919
|
created: resolution.created,
|
|
920
|
-
terminal_loss_statement:
|
|
920
|
+
terminal_loss_statement: VAULT_TERMINAL_LOSS_STATEMENT,
|
|
921
921
|
resolution,
|
|
922
922
|
};
|
|
923
923
|
}
|
|
924
924
|
/**
|
|
925
925
|
* Stale-pin recovery for OFFLINE address resolutions
|
|
926
|
-
* (
|
|
926
|
+
* (vault-force-spelling-and-pin-fold): an id-carrying pin resolves with
|
|
927
927
|
* zero network reads, so a pin gone stale (its vault deleted and the
|
|
928
928
|
* project re-allocated a new one) surfaces as the VERB's first repo-scoped
|
|
929
929
|
* read failing rather than as a resolution failure. Call this with that
|
|
@@ -935,10 +935,10 @@ export class Gitvault {
|
|
|
935
935
|
* an unauthorized caller learns).
|
|
936
936
|
*/
|
|
937
937
|
async recoverStalePin(options) {
|
|
938
|
-
const [{
|
|
939
|
-
const keystore = new
|
|
940
|
-
const transport =
|
|
941
|
-
const fresh = await
|
|
938
|
+
const [{ createVaultHttpTransport }, { VaultKeystore }, { recoverStaleVaultPin }] = await Promise.all([this.#publication(), this.#keystore(), this.#address()]);
|
|
939
|
+
const keystore = new VaultKeystore(options.keystore_root !== undefined ? { rootDir: options.keystore_root } : {});
|
|
940
|
+
const transport = createVaultHttpTransport(this.#client, { byoBackend: await this.#byoBackendResolver(keystore) });
|
|
941
|
+
const fresh = await recoverStaleVaultPin({
|
|
942
942
|
keystore,
|
|
943
943
|
transport,
|
|
944
944
|
address: options.address,
|
|
@@ -953,7 +953,7 @@ export class Gitvault {
|
|
|
953
953
|
handle,
|
|
954
954
|
found: true,
|
|
955
955
|
created: null,
|
|
956
|
-
terminal_loss_statement:
|
|
956
|
+
terminal_loss_statement: VAULT_TERMINAL_LOSS_STATEMENT,
|
|
957
957
|
resolution: fresh,
|
|
958
958
|
};
|
|
959
959
|
}
|
|
@@ -970,22 +970,22 @@ export class Gitvault {
|
|
|
970
970
|
* named `run402` is added.
|
|
971
971
|
*/
|
|
972
972
|
async init(options) {
|
|
973
|
-
const [{
|
|
973
|
+
const [{ createVaultHttpTransport }, { VaultKeystore }, { createVault }] = await Promise.all([this.#publication(), this.#keystore(), this.#creation()]);
|
|
974
974
|
const { parseMirrorDestinationUrl, formatMirrorDestination } = await this.#mirrorConfig();
|
|
975
|
-
const keystore = new
|
|
976
|
-
const transport =
|
|
975
|
+
const keystore = new VaultKeystore(options.keystore_root !== undefined ? { rootDir: options.keystore_root } : {});
|
|
976
|
+
const transport = createVaultHttpTransport(this.#client, { byoBackend: await this.#byoBackendResolver(keystore) });
|
|
977
977
|
let byoWriteTarget;
|
|
978
978
|
let byoDestinationAddress;
|
|
979
979
|
if (options.byo) {
|
|
980
980
|
const destination = parseMirrorDestinationUrl(options.byo.destination_url, { region: options.byo.region, endpoint: options.byo.endpoint });
|
|
981
981
|
byoWriteTarget = { destination, ...(options.byo.credential ? { credential: options.byo.credential } : {}) };
|
|
982
982
|
byoDestinationAddress = formatMirrorDestination(destination);
|
|
983
|
-
//
|
|
983
|
+
// vault-byo-primary-bucket task 3.1 (design D6) — probe BEFORE any
|
|
984
984
|
// allocation request; a failed probe throws and nothing is created.
|
|
985
|
-
const {
|
|
986
|
-
await
|
|
985
|
+
const { probeVaultByoDestination } = await this.#byoProbe();
|
|
986
|
+
await probeVaultByoDestination(destination, options.byo.credential);
|
|
987
987
|
}
|
|
988
|
-
const created = await
|
|
988
|
+
const created = await createVault({
|
|
989
989
|
keystore,
|
|
990
990
|
transport,
|
|
991
991
|
org_id: options.org_id,
|
|
@@ -1005,7 +1005,7 @@ export class Gitvault {
|
|
|
1005
1005
|
...(options.remote_url !== undefined ? { remote_url: options.remote_url } : {}),
|
|
1006
1006
|
});
|
|
1007
1007
|
}
|
|
1008
|
-
//
|
|
1008
|
+
// vault-byo-primary-bucket task 3.1/3.5 — read back the AUTHORITATIVE
|
|
1009
1009
|
// storage_profile from the vault record rather than trusting what THIS
|
|
1010
1010
|
// call requested: an idempotent replay against a pre-existing vault
|
|
1011
1011
|
// (a different client_creation_id, a different machine) can legitimately
|
|
@@ -1037,7 +1037,7 @@ export class Gitvault {
|
|
|
1037
1037
|
genesis_sha256: created.genesis_sha256,
|
|
1038
1038
|
remote,
|
|
1039
1039
|
deduplicated: created.how === "reconciled",
|
|
1040
|
-
terminal_loss_statement:
|
|
1040
|
+
terminal_loss_statement: VAULT_TERMINAL_LOSS_STATEMENT,
|
|
1041
1041
|
storage_profile: storageProfile,
|
|
1042
1042
|
byo_destination: byoDestination,
|
|
1043
1043
|
};
|
|
@@ -1078,7 +1078,7 @@ export class Gitvault {
|
|
|
1078
1078
|
*/
|
|
1079
1079
|
async scaffoldRemote(options) {
|
|
1080
1080
|
const { hardenedGit } = await this.#snapshot();
|
|
1081
|
-
const url = options.remote_url ??
|
|
1081
|
+
const url = options.remote_url ?? vaultRemoteUrl(options.org_id, options.project_id);
|
|
1082
1082
|
let createdRepository = false;
|
|
1083
1083
|
let enclosingToplevel = null;
|
|
1084
1084
|
// The scaffold acts on the APP ROOT only (first-deploy-agent-dx, design
|
|
@@ -1218,7 +1218,7 @@ export class Gitvault {
|
|
|
1218
1218
|
* Append `/<relative path of nestedDir from toplevel>/` to the enclosing
|
|
1219
1219
|
* repository's LOCAL `.git/info/exclude` — never `.gitignore`, which is
|
|
1220
1220
|
* tracked content (the same discipline as `excludeMessagingCacheFromGit`
|
|
1221
|
-
* in `
|
|
1221
|
+
* in `vault-restore.ts`). Located via `rev-parse --git-path
|
|
1222
1222
|
* info/exclude` so a linked worktree resolves to its common dir. Creates
|
|
1223
1223
|
* the file when absent; idempotent when the line is already there.
|
|
1224
1224
|
* Returns whether the line is in place — `false` (never a throw) when the
|
|
@@ -1264,9 +1264,9 @@ export class Gitvault {
|
|
|
1264
1264
|
* is stated verbatim exactly as for any other vault.
|
|
1265
1265
|
*/
|
|
1266
1266
|
async status(options = {}) {
|
|
1267
|
-
const {
|
|
1267
|
+
const { VaultKeystore } = await this.#keystore();
|
|
1268
1268
|
const { listPendingOverrideJournals } = await this.#deploy();
|
|
1269
|
-
const keystore = new
|
|
1269
|
+
const keystore = new VaultKeystore(options.keystore_root !== undefined ? { rootDir: options.keystore_root } : {});
|
|
1270
1270
|
let repoId = options.repo_id ?? null;
|
|
1271
1271
|
let record = null;
|
|
1272
1272
|
if (!repoId && options.project_id) {
|
|
@@ -1324,7 +1324,7 @@ export class Gitvault {
|
|
|
1324
1324
|
let coveringRecipients = null;
|
|
1325
1325
|
if (repoId && record) {
|
|
1326
1326
|
try {
|
|
1327
|
-
const coverage = await this.#client.request(`/
|
|
1327
|
+
const coverage = await this.#client.request(`/vaults/v1/${encodeURIComponent(repoId)}/envelope-recipients`, { context: "reading the vault envelope recipients" });
|
|
1328
1328
|
coveringRecipients = coverage.recipient_fingerprints.length;
|
|
1329
1329
|
}
|
|
1330
1330
|
catch {
|
|
@@ -1336,7 +1336,7 @@ export class Gitvault {
|
|
|
1336
1336
|
if (keystorePresent && !canSign) {
|
|
1337
1337
|
warnings.push({ kind: "read_only", message: "the signing key is missing from identity.json — this principal can decrypt and verify but cannot publish a new head" });
|
|
1338
1338
|
}
|
|
1339
|
-
if (record?.
|
|
1339
|
+
if (record?.vault_policy === "grandfathered") {
|
|
1340
1340
|
warnings.push({ kind: "policy_grandfathered", message: "activation does not require vault admission on this project; return it to `required` when the migration is done" });
|
|
1341
1341
|
}
|
|
1342
1342
|
// D7 (repo-first-onramp task 2.7): a vault that has accrued enough value
|
|
@@ -1349,13 +1349,13 @@ export class Gitvault {
|
|
|
1349
1349
|
// claims terminal loss (dogfood item 2: the single-principal premise
|
|
1350
1350
|
// behind `terminal_loss_risk` is provably false for this vault).
|
|
1351
1351
|
if (record) {
|
|
1352
|
-
const trip =
|
|
1353
|
-
if (
|
|
1352
|
+
const trip = vaultLossWarningTrip(record);
|
|
1353
|
+
if (vaultLossWarningTripped(trip)) {
|
|
1354
1354
|
if (isMultiPrincipal) {
|
|
1355
|
-
warnings.push({ kind: "keystore_backup_reminder", message:
|
|
1355
|
+
warnings.push({ kind: "keystore_backup_reminder", message: vaultKeystoreBackupReminderMessage(trip, coveringRecipients) });
|
|
1356
1356
|
}
|
|
1357
1357
|
else {
|
|
1358
|
-
warnings.push({ kind: "terminal_loss_risk", message:
|
|
1358
|
+
warnings.push({ kind: "terminal_loss_risk", message: vaultLossWarningMessage(trip) });
|
|
1359
1359
|
}
|
|
1360
1360
|
}
|
|
1361
1361
|
}
|
|
@@ -1369,8 +1369,8 @@ export class Gitvault {
|
|
|
1369
1369
|
// `.run402/` — a pure read, computed alongside the pin.
|
|
1370
1370
|
let messagingCacheExcluded = null;
|
|
1371
1371
|
if (options.repo_dir) {
|
|
1372
|
-
const {
|
|
1373
|
-
const p = await
|
|
1372
|
+
const { readPinnedVaultRepo } = await this.#address();
|
|
1373
|
+
const p = await readPinnedVaultRepo(options.repo_dir);
|
|
1374
1374
|
pinned = p ? { repo_id: p.repo_id, resolved_from: p.resolved_from, room: p.room } : null;
|
|
1375
1375
|
const { isMessagingCacheExcludedFromGit } = await this.#restore();
|
|
1376
1376
|
messagingCacheExcluded = await isMessagingCacheExcludedFromGit(options.repo_dir);
|
|
@@ -1400,10 +1400,16 @@ export class Gitvault {
|
|
|
1400
1400
|
}
|
|
1401
1401
|
if (!url)
|
|
1402
1402
|
continue;
|
|
1403
|
-
|
|
1403
|
+
let parsed;
|
|
1404
|
+
try {
|
|
1405
|
+
parsed = parseVaultRemoteUrl(url);
|
|
1406
|
+
}
|
|
1407
|
+
catch {
|
|
1408
|
+
continue; // another scheme's remote — not a vault remote
|
|
1409
|
+
}
|
|
1404
1410
|
if (!parsed)
|
|
1405
1411
|
continue;
|
|
1406
|
-
if (
|
|
1412
|
+
if (vaultRemoteAddressForm(parsed) === "id") {
|
|
1407
1413
|
// Id-form: the URL's second half IS the real project id — the
|
|
1408
1414
|
// pre-existing, always-correct comparison.
|
|
1409
1415
|
remote = { name, url, matches: project === null || parsed.project_id === project, reason: null };
|
|
@@ -1440,7 +1446,7 @@ export class Gitvault {
|
|
|
1440
1446
|
else if (options.refs === true && !holdsRepoKey) {
|
|
1441
1447
|
warnings.push({ kind: "refs_unavailable", message: "this machine does not hold K_repo for the vault, so its ref map cannot be decrypted here" });
|
|
1442
1448
|
}
|
|
1443
|
-
//
|
|
1449
|
+
// vault-agent-envelopes D3/D5: a KEY-HOLDER's ordinary read is a
|
|
1444
1450
|
// session start — enroll this keystore's key if absent and fulfil every
|
|
1445
1451
|
// pending desired recipient once per process (best-effort, reported,
|
|
1446
1452
|
// never folded into the observation above). `status` on a machine that
|
|
@@ -1464,7 +1470,7 @@ export class Gitvault {
|
|
|
1464
1470
|
if (!record)
|
|
1465
1471
|
nextActions.push({ action: "allocate the project's vault", command: "run402 repos create --project <project_id>" });
|
|
1466
1472
|
else if (pending.length > 0)
|
|
1467
|
-
nextActions.push({ action: `complete ${pending.length} unvaulted-override journal(s)`, command: "run402 repos
|
|
1473
|
+
nextActions.push({ action: `complete ${pending.length} unvaulted-override journal(s)`, command: "run402 repos capture" });
|
|
1468
1474
|
else if (record && !holdsRepoKey)
|
|
1469
1475
|
nextActions.push({ action: "this machine holds no key for the vault — allocate resolves to the existing vault and is idempotent", command: "run402 repos create --project <project_id>" });
|
|
1470
1476
|
return {
|
|
@@ -1497,12 +1503,12 @@ export class Gitvault {
|
|
|
1497
1503
|
refs,
|
|
1498
1504
|
head_target: headTarget,
|
|
1499
1505
|
pins: { highest_authenticated: authenticated, highest_materialized: materialized },
|
|
1500
|
-
|
|
1506
|
+
vault_policy: record?.vault_policy ?? null,
|
|
1501
1507
|
pending_overrides: pending.length,
|
|
1502
1508
|
covering_recipients: coveringRecipients,
|
|
1503
|
-
terminal_loss_statement: isMultiPrincipal ? null :
|
|
1504
|
-
terminal_loss_detail: isMultiPrincipal ? null :
|
|
1505
|
-
durability_statement: isMultiPrincipal ?
|
|
1509
|
+
terminal_loss_statement: isMultiPrincipal ? null : VAULT_TERMINAL_LOSS_STATEMENT,
|
|
1510
|
+
terminal_loss_detail: isMultiPrincipal ? null : VAULT_TERMINAL_LOSS_DOCTOR_TEXT,
|
|
1511
|
+
durability_statement: isMultiPrincipal ? VAULT_DURABILITY_STATEMENT : null,
|
|
1506
1512
|
warnings,
|
|
1507
1513
|
next_actions: nextActions,
|
|
1508
1514
|
};
|
|
@@ -1520,7 +1526,7 @@ export class Gitvault {
|
|
|
1520
1526
|
* charges with reproducing them ("the helper SHALL reproduce the exact set of
|
|
1521
1527
|
* objects reachable from the declared canonical refs (`refs/heads/*`,
|
|
1522
1528
|
* `refs/tags/*`, protocol-owned `refs/run402/*`), plus the `HEAD` target" —
|
|
1523
|
-
*
|
|
1529
|
+
* vault-client-surface, "Faithful helper"; `run402 repos capture` is
|
|
1524
1530
|
* separately defined as "capture and push outside a deploy" under "Source
|
|
1525
1531
|
* verbs"). The split is forced by §6.6: a dirty tree captures as a SYNTHETIC
|
|
1526
1532
|
* commit that sits on no branch, so moving `refs/heads/main` onto it would
|
|
@@ -1536,8 +1542,8 @@ export class Gitvault {
|
|
|
1536
1542
|
* against its expected manifest and reads the admitted head back from
|
|
1537
1543
|
* storage. A 200 alone is never enough (§0 client obligations).
|
|
1538
1544
|
*/
|
|
1539
|
-
async
|
|
1540
|
-
const [{ deployRefTransaction }, { captureSnapshot,
|
|
1545
|
+
async capture(options) {
|
|
1546
|
+
const [{ deployRefTransaction }, { captureSnapshot, vaultCommitLine }] = await Promise.all([this.#publication(), this.#snapshot()]);
|
|
1541
1547
|
const openedByAddress = options.address ? await this.resolveOrCreateAddress({ ...options, address: options.address, allow_create: true }) : null;
|
|
1542
1548
|
const opened = openedByAddress ?? (await this.openOrCreate(options));
|
|
1543
1549
|
const addressResolution = openedByAddress?.resolution ?? null;
|
|
@@ -1546,7 +1552,7 @@ export class Gitvault {
|
|
|
1546
1552
|
let handle = opened.handle;
|
|
1547
1553
|
const repoDir = options.repo_dir ?? process.cwd();
|
|
1548
1554
|
const snapshot = await captureSnapshot({ dir: repoDir, ...(options.snapshot ?? {}) });
|
|
1549
|
-
const line =
|
|
1555
|
+
const line = vaultCommitLine(snapshot);
|
|
1550
1556
|
options.onCommitLine?.(line);
|
|
1551
1557
|
// An OFFLINE (id-carrying pin) resolution discovers a stale pin on this
|
|
1552
1558
|
// first repo-scoped read — recover once and retry against the fresh
|
|
@@ -1580,28 +1586,28 @@ export class Gitvault {
|
|
|
1580
1586
|
// above (already returned/committed) — a mirror failure is a named
|
|
1581
1587
|
// pending finding reported BESIDE the vault result, on its own field.
|
|
1582
1588
|
const mirrorPush = await this.#tryMirrorPush(handle.repo_id, handle.keystore);
|
|
1583
|
-
//
|
|
1589
|
+
// vault-byo-primary-bucket task 3.3 — the SAME non-blocking contract,
|
|
1584
1590
|
// fired right beside the mirror hook (skipped_no_mirror with no network
|
|
1585
1591
|
// call for any managed vault or any machine with no local BYO config).
|
|
1586
1592
|
const byoChainCopy = await this.#tryByoChainCopyPush(handle.repo_id, handle.keystore);
|
|
1587
1593
|
// Deploy-time reconcile hook: fires on every successful push,
|
|
1588
1594
|
// best-effort — a reconcile failure (including a read-only principal
|
|
1589
1595
|
// with no signing key) is reported BESIDE the vault result, never a
|
|
1590
|
-
// `
|
|
1596
|
+
// `capture()` throw, same non-blocking contract as the mirror hook above.
|
|
1591
1597
|
const reconcileRecipients = await this.#tryReconcileEnvelopeRecipients(handle.vault);
|
|
1592
|
-
//
|
|
1598
|
+
// vault-multi-writer (task 5.7) — the SAME best-effort contract,
|
|
1593
1599
|
// fired right beside the encryption-recipient reconcile above: a fresh
|
|
1594
1600
|
// push proves this session's own key is an active writer, so this is
|
|
1595
1601
|
// the natural place a just-admitted member's own next push clears
|
|
1596
1602
|
// whatever candidacy it left behind for OTHER pending members too.
|
|
1597
1603
|
const writerReconcile = await this.#tryReconcileWriterAdmissions(handle.vault);
|
|
1598
|
-
return { ...result, snapshot,
|
|
1604
|
+
return { ...result, snapshot, vault_commit: snapshot.oid, vault_commit_line: line, mirror_push: mirrorPush, byo_chain_copy: byoChainCopy, reconcile_recipients: reconcileRecipients, writer_reconcile: writerReconcile };
|
|
1599
1605
|
}
|
|
1600
1606
|
// ── Handoff / resume (kygit-handoff design D1-D10) ─────────────────────────
|
|
1601
1607
|
/**
|
|
1602
1608
|
* Mint a Handoff Key: capture a stash-shaped checkpoint (design D1),
|
|
1603
|
-
* push it (retained, on no branch — {@link
|
|
1604
|
-
* it exactly like an ordinary `
|
|
1609
|
+
* push it (retained, on no branch — {@link VAULT_DEPLOY_REF} carries
|
|
1610
|
+
* it exactly like an ordinary `capture()`, never `refs/heads/*`), seal the
|
|
1605
1611
|
* vault's current epoch key under a fresh `wrap_key`, and mint through
|
|
1606
1612
|
* the gateway. The assembled `kgh1_…` key is returned exactly ONCE —
|
|
1607
1613
|
* nothing here or downstream persists it.
|
|
@@ -1617,10 +1623,10 @@ export class Gitvault {
|
|
|
1617
1623
|
const repoDir = options.repo_dir ?? process.cwd();
|
|
1618
1624
|
const repoFile = handle.keystore.readRepo(handle.repo_id);
|
|
1619
1625
|
if (!repoFile) {
|
|
1620
|
-
throw new LocalError(`no local key material for ${handle.repo_id} — this principal is not yet a member with a materialized envelope (push once first)`, "minting a handoff", { code: "
|
|
1626
|
+
throw new LocalError(`no local key material for ${handle.repo_id} — this principal is not yet a member with a materialized envelope (push once first)`, "minting a handoff", { code: "VAULT_UNRESOLVED" });
|
|
1621
1627
|
}
|
|
1622
1628
|
const kRepo = hexToBytes(repoFile.k_repo_hex);
|
|
1623
|
-
//
|
|
1629
|
+
// vault-multi-writer rev 47 (task 5.5, design D4): only an ACTIVE
|
|
1624
1630
|
// WRITER may mint — the gateway enforces this authoritatively at mint
|
|
1625
1631
|
// time (`403 HANDOFF_MINT_REQUIRES_WRITER`), but failing that late
|
|
1626
1632
|
// would mean this call already paid for a checkpoint capture + push +
|
|
@@ -1632,20 +1638,20 @@ export class Gitvault {
|
|
|
1632
1638
|
const identity = handle.keystore.readIdentity();
|
|
1633
1639
|
const localWriterKeyId = identity?.signing_fingerprint ?? null;
|
|
1634
1640
|
if (!localWriterKeyId || !repoFile.writer_set_pin?.writers.some((w) => w.writer_key_id === localWriterKeyId)) {
|
|
1635
|
-
throw new LocalError("this keystore's signing key is not an admitted writer of this vault — minting a handoff requires an ACTIVE writer (design D4); an existing writer must add this key first (`run402 orgs members add` / repos access sync), or push once to reconcile if a pending admission already exists", "minting a handoff", { code: "
|
|
1641
|
+
throw new LocalError("this keystore's signing key is not an admitted writer of this vault — minting a handoff requires an ACTIVE writer (design D4); an existing writer must add this key first (`run402 orgs members add` / repos access sync), or push once to reconcile if a pending admission already exists", "minting a handoff", { code: "VAULT_WRITER_NOT_ADMITTED" });
|
|
1636
1642
|
}
|
|
1637
1643
|
// The grant's `minted_role` must EXACTLY predict what the gateway's own
|
|
1638
|
-
// role-attenuation (`services/
|
|
1644
|
+
// role-attenuation (`services/vault/claims.ts mintHandoff`) will
|
|
1639
1645
|
// compute, or the grant fails VALIDATION_FAILED after this call has
|
|
1640
1646
|
// already paid for the checkpoint push below — `predictMintedRole`
|
|
1641
1647
|
// mirrors that formula against the SAME `ORG_ROLE_RANK` lattice
|
|
1642
|
-
// `
|
|
1648
|
+
// `vault-writer-state.ts` already uses elsewhere in this codebase.
|
|
1643
1649
|
const { predictMintedRole } = await this.#writerState();
|
|
1644
1650
|
const vaultRecord = await this.get(handle.repo_id);
|
|
1645
1651
|
const who = await this.#client.request("/agent/v1/whoami", { context: "resolving this principal's org role for the handoff grant" });
|
|
1646
1652
|
const membership = who.memberships.find((m) => m.org_id === vaultRecord.org_id && m.status === "active");
|
|
1647
1653
|
if (!membership) {
|
|
1648
|
-
throw new LocalError(`this principal has no active membership on ${vaultRecord.org_id} — an active writer must also be an active org member to mint a handoff`, "minting a handoff", { code: "
|
|
1654
|
+
throw new LocalError(`this principal has no active membership on ${vaultRecord.org_id} — an active writer must also be an active org member to mint a handoff`, "minting a handoff", { code: "VAULT_ACCESS_DENIED" });
|
|
1649
1655
|
}
|
|
1650
1656
|
const grantMintedRole = predictMintedRole(options.role, membership.role);
|
|
1651
1657
|
const snapshot = await captureHandoffSnapshot({
|
|
@@ -1686,7 +1692,7 @@ export class Gitvault {
|
|
|
1686
1692
|
const handoffId = randomHandoffUuid();
|
|
1687
1693
|
const { key, handoff_id_bytes, master_secret } = assembleHandoffKey(handoffId, randomBytes(32));
|
|
1688
1694
|
const secrets = deriveHandoffSecrets(handoff_id_bytes, master_secret);
|
|
1689
|
-
//
|
|
1695
|
+
// vault-multi-writer rev 47 (task 5.5, design D4) — the MINTER's own
|
|
1690
1696
|
// writer key signs `writer_admission_grant`, authorizing whoever redeems
|
|
1691
1697
|
// this handoff to become a writer (D224/D225). This is a HARD
|
|
1692
1698
|
// requirement distinct from the earlier `writer_set_pin` presence
|
|
@@ -1732,7 +1738,7 @@ export class Gitvault {
|
|
|
1732
1738
|
// the pre-rotation generations too ("membership grants FULL history"),
|
|
1733
1739
|
// and no rotation ever re-wraps old epochs to a principal that did not
|
|
1734
1740
|
// exist then. Found live: a recipient redeemed a post-removal handoff
|
|
1735
|
-
// and its clone died
|
|
1741
|
+
// and its clone died VAULT_EPOCH_NOT_OPENABLE at generation 1.
|
|
1736
1742
|
epoch_keys: { ...(repoFile.epoch_keys ?? {}), [repoFile.epoch]: repoFile.k_repo_hex },
|
|
1737
1743
|
checkpoint: { generation: pushResult.generation, commit_oid: snapshot.oid },
|
|
1738
1744
|
note_schema: "kygit.handoff-note.v1",
|
|
@@ -1743,7 +1749,7 @@ export class Gitvault {
|
|
|
1743
1749
|
// `project_id`, a verbatim `warning` sentence plus its machine-readable
|
|
1744
1750
|
// `warnings[]` twin. Read those names exactly — an SDK-side spelling
|
|
1745
1751
|
// that the gateway never sends surfaces as "role undefined" at the CLI.
|
|
1746
|
-
const response = await this.#client.request(`/
|
|
1752
|
+
const response = await this.#client.request(`/vaults/v1/${encodeURIComponent(handle.repo_id)}/handoffs`, {
|
|
1747
1753
|
method: "POST",
|
|
1748
1754
|
body: {
|
|
1749
1755
|
handoff_id: handoffId,
|
|
@@ -1775,7 +1781,7 @@ export class Gitvault {
|
|
|
1775
1781
|
expires_at: response.expires_at,
|
|
1776
1782
|
// A slug-form remote is the one address the minter already knows;
|
|
1777
1783
|
// an id-form one carries nothing a directory name should be built from.
|
|
1778
|
-
vault: handoffVaultFromWire(response, options.address &&
|
|
1784
|
+
vault: handoffVaultFromWire(response, options.address && vaultRemoteAddressForm(options.address) === "slug" ? `${options.address.org_id}/${options.address.project_id}` : null),
|
|
1779
1785
|
checkpoint: response.checkpoint,
|
|
1780
1786
|
capture: {
|
|
1781
1787
|
modified_captured: snapshot.modified_captured.length,
|
|
@@ -1791,17 +1797,17 @@ export class Gitvault {
|
|
|
1791
1797
|
/** List a vault's handoffs (ids, kind, state, role, expiry, redeemed_by — never the hash or envelope). */
|
|
1792
1798
|
async listHandoffs(options) {
|
|
1793
1799
|
const repoId = await this.#resolveRepoId(options);
|
|
1794
|
-
return this.#client.request(`/
|
|
1800
|
+
return this.#client.request(`/vaults/v1/${encodeURIComponent(repoId)}/handoffs`, { context: "listing handoffs" });
|
|
1795
1801
|
}
|
|
1796
1802
|
/** Revoke a handoff (idempotent — a second revoke of an already-revoked/redeemed/expired row still answers `200`). */
|
|
1797
1803
|
async revokeHandoff(handoffId, options) {
|
|
1798
1804
|
const repoId = await this.#resolveRepoId(options);
|
|
1799
|
-
return this.#client.request(`/
|
|
1805
|
+
return this.#client.request(`/vaults/v1/${encodeURIComponent(repoId)}/handoffs/${encodeURIComponent(handoffId)}`, { method: "DELETE", context: "revoking a handoff" });
|
|
1800
1806
|
}
|
|
1801
1807
|
// ── Invite / join (kygit-invite design D1-D10) ─────────────────────────────
|
|
1802
1808
|
/**
|
|
1803
1809
|
* Mint an Invite Key: capture a stash-shaped checkpoint exactly as
|
|
1804
|
-
* {@link
|
|
1810
|
+
* {@link Repos.handoff} does (design D1), push it (retained, on no
|
|
1805
1811
|
* branch), register the INVITER's own presence in the invite's room
|
|
1806
1812
|
* (design D4 — before the mint, so the row can carry
|
|
1807
1813
|
* `inviter_presence_id`; a registration failure is reported and the mint
|
|
@@ -1821,16 +1827,16 @@ export class Gitvault {
|
|
|
1821
1827
|
*/
|
|
1822
1828
|
async invite(options) {
|
|
1823
1829
|
const [{ deployRefTransaction }, { captureHandoffSnapshot, snapshotCommitment }] = await Promise.all([this.#publication(), this.#snapshot()]);
|
|
1824
|
-
const ho = await nodeOnly(() => import("../node/
|
|
1830
|
+
const ho = await nodeOnly(() => import("../node/vault-handoff.js"), "invite");
|
|
1825
1831
|
const { assembleInviteKey, deriveInviteSecrets, sealInviteEnvelope, assertInviteNoteHasNoSecret, buildWriterAdmissionGrant, deriveInviteWriterAdmissionSeed, INVITE_ENVELOPE_V2_KIND } = ho;
|
|
1826
1832
|
const handle = options.address ? (await this.resolveOrCreateAddress({ ...options, address: options.address, allow_create: false })).handle : await this.open(options);
|
|
1827
1833
|
const repoDir = options.repo_dir ?? process.cwd();
|
|
1828
1834
|
const repoFile = handle.keystore.readRepo(handle.repo_id);
|
|
1829
1835
|
if (!repoFile) {
|
|
1830
|
-
throw new LocalError(`no local key material for ${handle.repo_id} — this principal is not yet a member with a materialized envelope (push once first)`, "minting an invite", { code: "
|
|
1836
|
+
throw new LocalError(`no local key material for ${handle.repo_id} — this principal is not yet a member with a materialized envelope (push once first)`, "minting an invite", { code: "VAULT_UNRESOLVED" });
|
|
1831
1837
|
}
|
|
1832
1838
|
const kRepo = hexToBytes(repoFile.k_repo_hex);
|
|
1833
|
-
//
|
|
1839
|
+
// vault-multi-writer rev 47 (kygit-invite design D4): only an ACTIVE
|
|
1834
1840
|
// WRITER may mint — the gateway enforces it authoritatively
|
|
1835
1841
|
// (`403 INVITE_MINT_REQUIRES_WRITER`, carrying `request_writer_sync`),
|
|
1836
1842
|
// but failing that late would mean this call already paid for a
|
|
@@ -1841,7 +1847,7 @@ export class Gitvault {
|
|
|
1841
1847
|
const identity = handle.keystore.readIdentity();
|
|
1842
1848
|
const localWriterKeyId = identity?.signing_fingerprint ?? null;
|
|
1843
1849
|
if (!localWriterKeyId || !repoFile.writer_set_pin?.writers.some((w) => w.writer_key_id === localWriterKeyId)) {
|
|
1844
|
-
throw new LocalError("this keystore's signing key is not an admitted writer of this vault — minting an invite requires an ACTIVE writer (the gateway's own refusal is INVITE_MINT_REQUIRES_WRITER); run `run402 repos access sync` from a live writer, or push once to reconcile if a pending admission already exists", "minting an invite", { code: "
|
|
1850
|
+
throw new LocalError("this keystore's signing key is not an admitted writer of this vault — minting an invite requires an ACTIVE writer (the gateway's own refusal is INVITE_MINT_REQUIRES_WRITER); run `run402 repos access sync` from a live writer, or push once to reconcile if a pending admission already exists", "minting an invite", { code: "VAULT_WRITER_NOT_ADMITTED", details: { next_action: "request_writer_sync", command: "run402 repos access sync" } });
|
|
1845
1851
|
}
|
|
1846
1852
|
// The grant's `minted_role` must EXACTLY predict what the gateway's own
|
|
1847
1853
|
// role attenuation will compute (invite defaults to `developer` rather
|
|
@@ -1853,7 +1859,7 @@ export class Gitvault {
|
|
|
1853
1859
|
const whoMints = await this.#client.request("/agent/v1/whoami", { context: "resolving this principal's org role for the invite grant" });
|
|
1854
1860
|
const inviterMembership = whoMints.memberships.find((m) => m.org_id === vaultRecord.org_id && m.status === "active");
|
|
1855
1861
|
if (!inviterMembership) {
|
|
1856
|
-
throw new LocalError(`this principal has no active membership on ${vaultRecord.org_id} — an active writer must also be an active org member to mint an invite`, "minting an invite", { code: "
|
|
1862
|
+
throw new LocalError(`this principal has no active membership on ${vaultRecord.org_id} — an active writer must also be an active org member to mint an invite`, "minting an invite", { code: "VAULT_ACCESS_DENIED" });
|
|
1857
1863
|
}
|
|
1858
1864
|
const grantMintedRole = predictMintedRole(options.role ?? INVITE_DEFAULT_ROLE, inviterMembership.role);
|
|
1859
1865
|
const snapshot = await captureHandoffSnapshot({
|
|
@@ -1912,7 +1918,7 @@ export class Gitvault {
|
|
|
1912
1918
|
const inviteId = randomHandoffUuid();
|
|
1913
1919
|
const { key, invite_id_bytes, master_secret } = assembleInviteKey(inviteId, randomBytes(32));
|
|
1914
1920
|
const secrets = deriveInviteSecrets(invite_id_bytes, master_secret);
|
|
1915
|
-
//
|
|
1921
|
+
// vault-multi-writer rev 47 (kygit-invite design D4) — the MINTER's
|
|
1916
1922
|
// own writer key signs `writer_admission_grant`, authorizing whoever
|
|
1917
1923
|
// redeems this invite to become a writer under its OWN key. Built and
|
|
1918
1924
|
// SIGNED BEFORE the envelope is sealed ("no hash cycle: grant first,
|
|
@@ -1960,10 +1966,10 @@ export class Gitvault {
|
|
|
1960
1966
|
writer_admission_grant_sha256: grantSha256Local,
|
|
1961
1967
|
});
|
|
1962
1968
|
// The wire shape mirrors the handoff mint's documented one (llms-full.txt
|
|
1963
|
-
// "Handoff / resume",
|
|
1969
|
+
// "Handoff / resume", vault-invite spec): `role` (the minted role),
|
|
1964
1970
|
// `repo_id` / `org_id` / `project_id`, `room`, a verbatim `warning`
|
|
1965
1971
|
// sentence plus its machine-readable `warnings[]` twin.
|
|
1966
|
-
const response = await this.#client.request(`/
|
|
1972
|
+
const response = await this.#client.request(`/vaults/v1/${encodeURIComponent(handle.repo_id)}/invites`, {
|
|
1967
1973
|
method: "POST",
|
|
1968
1974
|
body: {
|
|
1969
1975
|
invite_id: inviteId,
|
|
@@ -2015,14 +2021,10 @@ export class Gitvault {
|
|
|
2015
2021
|
}
|
|
2016
2022
|
const nextActions = [...(response.next_actions ?? [])];
|
|
2017
2023
|
if (!nextActions.some((a) => a.type === "join_invite")) {
|
|
2018
|
-
// design D9: "CLI-synthesized, the recipient's exact command"
|
|
2019
|
-
// rendered by DOOR, same as every other remote-facing command this
|
|
2020
|
-
// module renders (`gitvaultRemoteScheme()` is the one place that
|
|
2021
|
-
// decides `run402` vs `kygit`, per `RUN402_REMOTE_SCHEME`).
|
|
2022
|
-
const door = gitvaultRemoteScheme();
|
|
2024
|
+
// design D9: "CLI-synthesized, the recipient's exact command".
|
|
2023
2025
|
nextActions.push({
|
|
2024
2026
|
type: "join_invite",
|
|
2025
|
-
command:
|
|
2027
|
+
command: `run402 repos join ${key}`,
|
|
2026
2028
|
why: "Run this on the other agent's machine to redeem the invite.",
|
|
2027
2029
|
safe_to_auto_execute: false,
|
|
2028
2030
|
});
|
|
@@ -2036,7 +2038,7 @@ export class Gitvault {
|
|
|
2036
2038
|
kind: response.kind,
|
|
2037
2039
|
minted_role: response.role,
|
|
2038
2040
|
expires_at: response.expires_at,
|
|
2039
|
-
vault: handoffVaultFromWire(response, options.address &&
|
|
2041
|
+
vault: handoffVaultFromWire(response, options.address && vaultRemoteAddressForm(options.address) === "slug" ? `${options.address.org_id}/${options.address.project_id}` : null),
|
|
2040
2042
|
room: { organization_id: response.room?.org_id ?? repoFile.org_id, room_key: response.room?.room_key ?? roomKey },
|
|
2041
2043
|
checkpoint: response.checkpoint,
|
|
2042
2044
|
capture: {
|
|
@@ -2055,12 +2057,12 @@ export class Gitvault {
|
|
|
2055
2057
|
/** List a vault's invites (ids, kind, state, role, room, expiry, redeemed_by — never the hash or envelope). */
|
|
2056
2058
|
async listInvites(options) {
|
|
2057
2059
|
const repoId = await this.#resolveRepoId(options);
|
|
2058
|
-
return this.#client.request(`/
|
|
2060
|
+
return this.#client.request(`/vaults/v1/${encodeURIComponent(repoId)}/invites`, { context: "listing invites" });
|
|
2059
2061
|
}
|
|
2060
2062
|
/** Revoke an invite (idempotent — a second revoke of an already-revoked/redeemed/expired row still answers `200`). */
|
|
2061
2063
|
async revokeInvite(inviteId, options) {
|
|
2062
2064
|
const repoId = await this.#resolveRepoId(options);
|
|
2063
|
-
return this.#client.request(`/
|
|
2065
|
+
return this.#client.request(`/vaults/v1/${encodeURIComponent(repoId)}/invites/${encodeURIComponent(inviteId)}`, { method: "DELETE", context: "revoking an invite" });
|
|
2064
2066
|
}
|
|
2065
2067
|
/**
|
|
2066
2068
|
* Create this machine's wallet (the wallet file) when the credentials
|
|
@@ -2089,7 +2091,7 @@ export class Gitvault {
|
|
|
2089
2091
|
* envelope supersedes the bearer one.
|
|
2090
2092
|
*/
|
|
2091
2093
|
/**
|
|
2092
|
-
*
|
|
2094
|
+
* vault-multi-writer rev 47 (task 5.6, design D4/D5) — order: parse →
|
|
2093
2095
|
* ensure wallet → ensure identity → derive auth/wrap/admission → build
|
|
2094
2096
|
* acceptance → redeem → verify grant → open envelope → check the grant
|
|
2095
2097
|
* hash → persist repo file with `pending_writer_admission` → clone →
|
|
@@ -2112,14 +2114,14 @@ export class Gitvault {
|
|
|
2112
2114
|
* "not yet a writer" refuses; here "already a writer" means skip.
|
|
2113
2115
|
*/
|
|
2114
2116
|
async resume(options) {
|
|
2115
|
-
const [ho, {
|
|
2117
|
+
const [ho, { VaultKeystore }, restore] = await Promise.all([this.#handoff(), this.#keystore(), this.#restore()]);
|
|
2116
2118
|
const { parseHandoffKey, deriveHandoffSecrets, deriveWriterAdmissionSeed, buildWriterAcceptance, openHandoffEnvelopeV2 } = ho;
|
|
2117
|
-
const {
|
|
2119
|
+
const { cloneVaultRemote, applyHandoffCheckpoint, resolveResumeTargetDir, readGitCommitMessage, excludeMessagingCacheFromGit } = restore;
|
|
2118
2120
|
// parse
|
|
2119
2121
|
const parsed = parseHandoffKey(options.key);
|
|
2120
2122
|
// ensure wallet — A fresh machine has no wallet, and the redeem route
|
|
2121
2123
|
// accepts ONLY a SIWX wallet signature (a control-plane session,
|
|
2122
|
-
//
|
|
2124
|
+
// grant key, or service key is refused HANDOFF_REDEEM_REQUIRES_WALLET —
|
|
2123
2125
|
// the keystore key the redemption publishes is what makes the recipient a
|
|
2124
2126
|
// real key-holder). Nothing upstream creates the wallet for an
|
|
2125
2127
|
// unpaid request, so `resume` does it here, exactly as `repos resume
|
|
@@ -2131,11 +2133,11 @@ export class Gitvault {
|
|
|
2131
2133
|
await this.#ensureLocalWallet(options.onLine);
|
|
2132
2134
|
// ensure identity — BEFORE redeem (D5): the acceptance below needs this
|
|
2133
2135
|
// checkout's own signing key, which the redeem REQUEST body carries.
|
|
2134
|
-
const keystore = new
|
|
2136
|
+
const keystore = new VaultKeystore(options.keystore_root !== undefined ? { rootDir: options.keystore_root } : {});
|
|
2135
2137
|
const identity = keystore.ensureIdentity();
|
|
2136
2138
|
const signingKeypair = keystore.signingKeypair(identity);
|
|
2137
2139
|
if (!signingKeypair) {
|
|
2138
|
-
throw new LocalError("this keystore has no local signing seed — resuming a
|
|
2140
|
+
throw new LocalError("this keystore has no local signing seed — resuming a vault-multi-writer handoff requires signing writer_acceptance with a full Ed25519 seed, which a read-only recovery identity does not hold", "resuming a handoff", { code: "VAULT_UNRECOVERABLE" });
|
|
2139
2141
|
}
|
|
2140
2142
|
const redeemerEncryptionPubkeyRaw = fromBase64url(identity.encryption_pubkey, "identity.encryption_pubkey");
|
|
2141
2143
|
// derive auth/wrap/admission
|
|
@@ -2152,7 +2154,7 @@ export class Gitvault {
|
|
|
2152
2154
|
redeemer_encryption_pubkey_raw: redeemerEncryptionPubkeyRaw,
|
|
2153
2155
|
});
|
|
2154
2156
|
// claim
|
|
2155
|
-
const redeem = await this.#client.request(`/
|
|
2157
|
+
const redeem = await this.#client.request(`/vaults/v1/handoffs/${encodeURIComponent(parsed.handoff_id)}/redeem`, {
|
|
2156
2158
|
method: "POST",
|
|
2157
2159
|
// Base64url, per the documented wire contract (openapi: "Base64url — the
|
|
2158
2160
|
// HKDF-derived auth_secret half of the parsed kgh1_ key"). The gateway
|
|
@@ -2193,7 +2195,7 @@ export class Gitvault {
|
|
|
2193
2195
|
// open envelope — v2 (task 5.4): the v2 payload's own
|
|
2194
2196
|
// writer_admission_grant_sha256 is what the next step cross-checks;
|
|
2195
2197
|
// opening under the SAME v1/v2-requiring rule that refuses a
|
|
2196
|
-
// pre-
|
|
2198
|
+
// pre-vault-multi-writer mint's envelope outright.
|
|
2197
2199
|
const payload = openHandoffEnvelopeV2(parsed.handoff_id_bytes, secrets.wrap_key, redeem.sealed_envelope, redeem.envelope_kind);
|
|
2198
2200
|
if (payload.repo_id !== vault.vault_id) {
|
|
2199
2201
|
throw new LocalError("the opened envelope's repo_id does not match the redeem response's vault — refusing", "resuming a handoff", { code: "HANDOFF_ENVELOPE_INVALID" });
|
|
@@ -2207,18 +2209,18 @@ export class Gitvault {
|
|
|
2207
2209
|
throw new LocalError(`the redeem response's writer_admission_grant (sha256 ${grantSha256}) does not match the hash sealed into the envelope at mint time (${payload.writer_admission_grant_sha256}) — refusing to activate under a substituted grant`, "resuming a handoff", { code: "HANDOFF_REDEEM_WRITER_KEY_MISMATCH", details: { expected: payload.writer_admission_grant_sha256, received: grantSha256 } });
|
|
2208
2210
|
}
|
|
2209
2211
|
// Genesis must be pinned before ANY materialize call can succeed
|
|
2210
|
-
// (`
|
|
2212
|
+
// (`Vault.genesis()` requires a keystore repo file — this is
|
|
2211
2213
|
// the one read that happens BEFORE one exists, via the transport
|
|
2212
2214
|
// directly, mirroring `restoreRepoFromEnvelope`'s own signature check).
|
|
2213
|
-
const {
|
|
2214
|
-
const transport =
|
|
2215
|
+
const { createVaultHttpTransport } = await this.#publication();
|
|
2216
|
+
const transport = createVaultHttpTransport(this.#client);
|
|
2215
2217
|
const genesisBytes = await transport.getGenesis({ repo_id: vault.vault_id });
|
|
2216
2218
|
if (!genesisBytes) {
|
|
2217
2219
|
throw new LocalError("the vault has no admitted genesis", "resuming a handoff", { code: "CHAIN_BROKEN", details: { repo_id: vault.vault_id } });
|
|
2218
2220
|
}
|
|
2219
|
-
const genesis =
|
|
2220
|
-
if (!
|
|
2221
|
-
throw new LocalError("vault_genesis signature does not verify", "resuming a handoff", { code: "
|
|
2221
|
+
const genesis = parseVaultStrict(new TextDecoder().decode(genesisBytes));
|
|
2222
|
+
if (!verifyVaultObject(genesis, genesis.creator_signing_pubkey)) {
|
|
2223
|
+
throw new LocalError("vault_genesis signature does not verify", "resuming a handoff", { code: "VAULT_SIGNATURE_INVALID", details: { repo_id: vault.vault_id } });
|
|
2222
2224
|
}
|
|
2223
2225
|
const genesisSha = sha256Hex(genesisBytes);
|
|
2224
2226
|
// persist repo file with pending_writer_admission — BEFORE clone/chain
|
|
@@ -2245,14 +2247,14 @@ export class Gitvault {
|
|
|
2245
2247
|
});
|
|
2246
2248
|
const targetDir = await resolveResumeTargetDir(options.to, vault.address, vault.vault_id);
|
|
2247
2249
|
options.onLine?.(`resuming into ${targetDir}`);
|
|
2248
|
-
const remoteUrl =
|
|
2249
|
-
await
|
|
2250
|
+
const remoteUrl = vaultRemoteUrl(vault.organization_id, vault.project_id);
|
|
2251
|
+
await cloneVaultRemote(remoteUrl, targetDir);
|
|
2250
2252
|
// Local-only pins (design D10) — never a worktree file, never the
|
|
2251
2253
|
// global active project. Reuses the SAME pin-writer every other
|
|
2252
|
-
//
|
|
2253
|
-
const {
|
|
2254
|
+
// vault resolution path uses, which also writes `r402.room`.
|
|
2255
|
+
const { pinVaultRepo } = await this.#address();
|
|
2254
2256
|
const addressParts = vault.address ? vault.address.split("/") : null;
|
|
2255
|
-
await
|
|
2257
|
+
await pinVaultRepo(targetDir, vault.vault_id, addressParts && addressParts.length === 2 ? { org_slug: addressParts[0], repo_name: addressParts[1] } : undefined, { project_id: vault.project_id, org_id: vault.organization_id });
|
|
2256
2258
|
// kygit-invite design D5: restore is kind-agnostic, so the same
|
|
2257
2259
|
// `.git/info/exclude` write `join` performs happens on `resume` too —
|
|
2258
2260
|
// best-effort, never a `resume()` throw.
|
|
@@ -2262,7 +2264,7 @@ export class Gitvault {
|
|
|
2262
2264
|
catch {
|
|
2263
2265
|
// Best-effort: an unwritable checkout still completes the resume.
|
|
2264
2266
|
}
|
|
2265
|
-
// verify chain — `open()` here mainly constructs the `
|
|
2267
|
+
// verify chain — `open()` here mainly constructs the `Vault`
|
|
2266
2268
|
// instance: its own `ensureRepoState()` (the cold-open restore path)
|
|
2267
2269
|
// no-ops the instant it sees a repo file already on disk — and the
|
|
2268
2270
|
// `saveRepo` call above just wrote one. The chain walk this method
|
|
@@ -2294,10 +2296,10 @@ export class Gitvault {
|
|
|
2294
2296
|
// Clearing pending_writer_admission now that the activation head is
|
|
2295
2297
|
// (or already was — the idempotent-skip case) admitted mirrors
|
|
2296
2298
|
// writer_status flipping to "active" at the same moment, per this
|
|
2297
|
-
// field's own doc comment in
|
|
2299
|
+
// field's own doc comment in vault-keystore.ts.
|
|
2298
2300
|
keystore.updateRepo(vault.vault_id, { pending_writer_admission: null });
|
|
2299
2301
|
// reconcile principal envelope — the bearer envelope is superseded
|
|
2300
|
-
// within minutes of use; run the same reconcile `
|
|
2302
|
+
// within minutes of use; run the same reconcile `capture()` runs,
|
|
2301
2303
|
// best-effort (never a `resume()` throw), NOW that this checkout is an
|
|
2302
2304
|
// admitted writer and the reconcile's own wrap step is meaningful.
|
|
2303
2305
|
const reconcile = await this.#tryReconcileEnvelopeRecipients(handle.vault);
|
|
@@ -2346,7 +2348,7 @@ export class Gitvault {
|
|
|
2346
2348
|
/**
|
|
2347
2349
|
* Join an Invite Key: parse (refusing a `kgh1_` handoff key by name,
|
|
2348
2350
|
* pointing at `resume` — design D9) → ensure this machine has a wallet
|
|
2349
|
-
* (design D5, mirroring {@link
|
|
2351
|
+
* (design D5, mirroring {@link Repos.resume}'s own bare-wallet
|
|
2350
2352
|
* backstop; the CALLER folds the fuller cold-start chain — wallet,
|
|
2351
2353
|
* faucet, one x402 prototype payment — before invoking this, never
|
|
2352
2354
|
* blocking the redemption itself) → redeem → open the sealed envelope → write
|
|
@@ -2360,14 +2362,14 @@ export class Gitvault {
|
|
|
2360
2362
|
* the most recent messages for the arrival view.
|
|
2361
2363
|
*/
|
|
2362
2364
|
async join(options) {
|
|
2363
|
-
const [ho, {
|
|
2364
|
-
nodeOnly(() => import("../node/
|
|
2365
|
+
const [ho, { VaultKeystore }, { createVaultHttpTransport }] = await Promise.all([
|
|
2366
|
+
nodeOnly(() => import("../node/vault-handoff.js"), "join"),
|
|
2365
2367
|
this.#keystore(),
|
|
2366
2368
|
this.#publication(),
|
|
2367
2369
|
]);
|
|
2368
|
-
const restore = await nodeOnly(() => import("../node/
|
|
2370
|
+
const restore = await nodeOnly(() => import("../node/vault-restore.js"), "join");
|
|
2369
2371
|
const { parseInviteKey, deriveInviteSecrets, deriveInviteWriterAdmissionSeed, buildWriterAcceptance, openInviteEnvelope } = ho;
|
|
2370
|
-
const {
|
|
2372
|
+
const { cloneVaultRemote, applyHandoffCheckpoint, resolveResumeTargetDir, readGitCommitMessage, excludeMessagingCacheFromGit } = restore;
|
|
2371
2373
|
const parsed = parseInviteKey(options.key);
|
|
2372
2374
|
const secrets = deriveInviteSecrets(parsed.invite_id_bytes, parsed.master_secret);
|
|
2373
2375
|
// Bare-wallet backstop, exactly like `resume` (design D5) — the claim
|
|
@@ -2379,7 +2381,7 @@ export class Gitvault {
|
|
|
2379
2381
|
// below needs THIS checkout's own signing key, and the redeem REQUEST
|
|
2380
2382
|
// body carries it. Nothing about the joiner's key is copied from the
|
|
2381
2383
|
// inviter — the joiner generates it and pushes under it.
|
|
2382
|
-
const keystore = new
|
|
2384
|
+
const keystore = new VaultKeystore(options.keystore_root !== undefined ? { rootDir: options.keystore_root } : {});
|
|
2383
2385
|
const identity = keystore.ensureIdentity();
|
|
2384
2386
|
const signingKeypair = keystore.signingKeypair(identity);
|
|
2385
2387
|
if (!signingKeypair) {
|
|
@@ -2399,7 +2401,7 @@ export class Gitvault {
|
|
|
2399
2401
|
redeemer_signing_seed: signingKeypair.seed,
|
|
2400
2402
|
redeemer_encryption_pubkey_raw: redeemerEncryptionPubkeyRaw,
|
|
2401
2403
|
});
|
|
2402
|
-
const redeem = await this.#client.request(`/
|
|
2404
|
+
const redeem = await this.#client.request(`/vaults/v1/invites/${encodeURIComponent(parsed.invite_id)}/redeem`, {
|
|
2403
2405
|
method: "POST",
|
|
2404
2406
|
// Base64url, per the documented wire contract — the gateway decodes
|
|
2405
2407
|
// base64/base64url and substitutes 32 zero bytes for anything else,
|
|
@@ -2452,14 +2454,14 @@ export class Gitvault {
|
|
|
2452
2454
|
// Genesis must be pinned before ANY materialize call can succeed — the
|
|
2453
2455
|
// one read that happens BEFORE a keystore repo file exists, via the
|
|
2454
2456
|
// transport directly (mirrors `resume`'s own signature check).
|
|
2455
|
-
const transport =
|
|
2457
|
+
const transport = createVaultHttpTransport(this.#client);
|
|
2456
2458
|
const genesisBytes = await transport.getGenesis({ repo_id: vault.vault_id });
|
|
2457
2459
|
if (!genesisBytes) {
|
|
2458
2460
|
throw new LocalError("the vault has no admitted genesis", "joining an invite", { code: "CHAIN_BROKEN", details: { repo_id: vault.vault_id } });
|
|
2459
2461
|
}
|
|
2460
|
-
const genesis =
|
|
2461
|
-
if (!
|
|
2462
|
-
throw new LocalError("vault_genesis signature does not verify", "joining an invite", { code: "
|
|
2462
|
+
const genesis = parseVaultStrict(new TextDecoder().decode(genesisBytes));
|
|
2463
|
+
if (!verifyVaultObject(genesis, genesis.creator_signing_pubkey)) {
|
|
2464
|
+
throw new LocalError("vault_genesis signature does not verify", "joining an invite", { code: "VAULT_SIGNATURE_INVALID", details: { repo_id: vault.vault_id } });
|
|
2463
2465
|
}
|
|
2464
2466
|
const genesisSha = sha256Hex(genesisBytes);
|
|
2465
2467
|
// Write the repo file to the keystore BEFORE touching disk (design D5),
|
|
@@ -2484,18 +2486,18 @@ export class Gitvault {
|
|
|
2484
2486
|
});
|
|
2485
2487
|
const targetDir = await resolveResumeTargetDir(options.to, vault.address, vault.vault_id);
|
|
2486
2488
|
options.onLine?.(`joining into ${targetDir}`);
|
|
2487
|
-
const remoteUrl =
|
|
2488
|
-
await
|
|
2489
|
+
const remoteUrl = vaultRemoteUrl(vault.organization_id, vault.project_id);
|
|
2490
|
+
await cloneVaultRemote(remoteUrl, targetDir);
|
|
2489
2491
|
// The ROW's room key — a named room, or the project id when the mint
|
|
2490
2492
|
// omitted one — is what makes `messages wait` in the joined checkout
|
|
2491
2493
|
// address the right room with zero flags (design D5).
|
|
2492
2494
|
const roomKey = redeem.room?.room_key ?? vault.project_id ?? "";
|
|
2493
2495
|
// Local-only pins (design D5) — never a worktree file, never the global
|
|
2494
|
-
// active project. Reuses the SAME pin-writer every other
|
|
2496
|
+
// active project. Reuses the SAME pin-writer every other vault
|
|
2495
2497
|
// resolution path uses, this time with the invite's OWN room key.
|
|
2496
|
-
const {
|
|
2498
|
+
const { pinVaultRepo } = await this.#address();
|
|
2497
2499
|
const addressParts = vault.address ? vault.address.split("/") : null;
|
|
2498
|
-
await
|
|
2500
|
+
await pinVaultRepo(targetDir, vault.vault_id, addressParts && addressParts.length === 2 ? { org_slug: addressParts[0], repo_name: addressParts[1] } : undefined, { project_id: vault.project_id, org_id: vault.organization_id, room_key: roomKey });
|
|
2499
2501
|
// design D5/D9 risk list: `.git/info/exclude` rather than `.gitignore`
|
|
2500
2502
|
// — the ignore file is part of the captured tree and touching it would
|
|
2501
2503
|
// break exact-state on the first `git status`. Best-effort.
|
|
@@ -2505,7 +2507,7 @@ export class Gitvault {
|
|
|
2505
2507
|
catch {
|
|
2506
2508
|
// Best-effort: an unwritable checkout still completes the join.
|
|
2507
2509
|
}
|
|
2508
|
-
// verify chain — `open()` here mainly constructs the `
|
|
2510
|
+
// verify chain — `open()` here mainly constructs the `Vault`
|
|
2509
2511
|
// instance (its cold-open restore path no-ops the instant it sees the
|
|
2510
2512
|
// repo file `saveRepo` just wrote). The chain walk this method needs
|
|
2511
2513
|
// happens the FIRST time `submitWriterActivationHead` calls
|
|
@@ -2517,7 +2519,7 @@ export class Gitvault {
|
|
|
2517
2519
|
// design D5: activate as a writer BEFORE `join()` returns, through the
|
|
2518
2520
|
// SAME `add_writer_key` door `resume` drives — so the joiner's first
|
|
2519
2521
|
// `git push` is an ordinary push under its OWN key, not a
|
|
2520
|
-
// `
|
|
2522
|
+
// `VAULT_WRITER_NOT_ADMITTED` refusal. `added_writer.principal_id`
|
|
2521
2523
|
// names the redeemer's own control-plane principal, which the claim
|
|
2522
2524
|
// response never carries (its `membership` block names the ORG), so
|
|
2523
2525
|
// resolve it fresh here.
|
|
@@ -2552,7 +2554,7 @@ export class Gitvault {
|
|
|
2552
2554
|
writerActivation = { outcome: "pending", writer_key_id: myWriterKeyId, reason: e instanceof Error ? e.message : String(e) };
|
|
2553
2555
|
}
|
|
2554
2556
|
// The bearer envelope is superseded within minutes of use — run the
|
|
2555
|
-
// same reconcile `
|
|
2557
|
+
// same reconcile `capture()` runs, best-effort (never a `join()` throw),
|
|
2556
2558
|
// NOW that this checkout is an admitted writer and the reconcile's own
|
|
2557
2559
|
// wrap step is meaningful.
|
|
2558
2560
|
const reconcile = await this.#tryReconcileEnvelopeRecipients(handle.vault);
|
|
@@ -2675,7 +2677,7 @@ export class Gitvault {
|
|
|
2675
2677
|
next_actions: nextActions,
|
|
2676
2678
|
};
|
|
2677
2679
|
}
|
|
2678
|
-
/** Best-effort dual-push: catches EVERYTHING, including the lazy module import itself, so a mirror problem can never surface as a `
|
|
2680
|
+
/** Best-effort dual-push: catches EVERYTHING, including the lazy module import itself, so a mirror problem can never surface as a `capture()` throw. */
|
|
2679
2681
|
/**
|
|
2680
2682
|
* `EPOCH_ROTATION_REQUIRED` (D193) is left THROWN — never swallowed into a
|
|
2681
2683
|
* silent auto-rotation — because this call site cannot legally decide the
|
|
@@ -2683,7 +2685,7 @@ export class Gitvault {
|
|
|
2683
2685
|
* a rotation attempt must be fenced against: no shipped gateway route
|
|
2684
2686
|
* exposes them for `reason:"member_removed"`/`"elective_rekey"`/
|
|
2685
2687
|
* `"epoch_secret_exposed"` (verified against the live gateway route
|
|
2686
|
-
* source — see `
|
|
2688
|
+
* source — see `Vault.rotateEpoch`'s own doc comment). Submitting
|
|
2687
2689
|
* a GUESSED pair would either fail loudly (`RECIPIENT_SET_MISMATCH`, the
|
|
2688
2690
|
* honest outcome) or — worse — never be reachable at all for a fresh
|
|
2689
2691
|
* counter row. Rather than let a caller decode `details.migration_required`/
|
|
@@ -2698,7 +2700,7 @@ export class Gitvault {
|
|
|
2698
2700
|
if (!isRun402Error(e) || e.code !== "EPOCH_ROTATION_REQUIRED")
|
|
2699
2701
|
return e;
|
|
2700
2702
|
// Mirrors `epochRotationRequiredNextActions` in
|
|
2701
|
-
// `../node/
|
|
2703
|
+
// `../node/vault-deploy.ts` byte-for-byte (same three causes, same
|
|
2702
2704
|
// three commands) — kept as a small standalone copy rather than a
|
|
2703
2705
|
// shared import so this namespace-level module (isomorphic — Deno/Bun/V8
|
|
2704
2706
|
// isolates, no Node-only imports) never has to pull in the Node-only
|
|
@@ -2711,14 +2713,14 @@ export class Gitvault {
|
|
|
2711
2713
|
nextActions.push({ action: "run402 repos access revoke-key <principal_id>", why: "an org membership removal or key revocation is outstanding for this vault; a surviving writer's push completes a membership removal automatically (reason member_removed), so this push was refused because that rotation could not run here — an owner can rotate explicitly with this command (owner + step-up)" });
|
|
2712
2714
|
if (details.exposure_outstanding)
|
|
2713
2715
|
nextActions.push({ action: "run402 repos access declare-exposure", why: "this vault's own epoch secret has been declared exposed (owner + step-up)" });
|
|
2714
|
-
return new LocalError(`this vault requires a rotate_epoch admission before an ordinary push is admissible (repo_id ${repoId})`, "pushing to
|
|
2716
|
+
return new LocalError(`this vault requires a rotate_epoch admission before an ordinary push is admissible (repo_id ${repoId})`, "pushing to vault", { code: "EPOCH_ROTATION_REQUIRED", details, next_actions: nextActions.length > 0 ? nextActions : undefined, cause: e });
|
|
2715
2717
|
}
|
|
2716
2718
|
/**
|
|
2717
2719
|
* The capture-time copies a plain `git push` through the remote helper
|
|
2718
|
-
* owes exactly as {@link
|
|
2720
|
+
* owes exactly as {@link capture} and {@link deploy} do: the opt-in mirror
|
|
2719
2721
|
* dual-push and, on a BYO vault, the signed-chain copy into the customer
|
|
2720
|
-
* bucket (
|
|
2721
|
-
* through `
|
|
2722
|
+
* bucket (vault-byo-primary-bucket task 3.3). The helper publishes
|
|
2723
|
+
* through `Vault.push` directly, so it calls this right after a
|
|
2722
2724
|
* generation lands. Same contract as the private hooks it composes:
|
|
2723
2725
|
* best-effort, NEVER throws, never alters the push already committed —
|
|
2724
2726
|
* each outcome is reported on its own field (`skipped_no_mirror` costs no
|
|
@@ -2739,12 +2741,12 @@ export class Gitvault {
|
|
|
2739
2741
|
}
|
|
2740
2742
|
}
|
|
2741
2743
|
/**
|
|
2742
|
-
*
|
|
2744
|
+
* vault-byo-primary-bucket task 3.3 — the chain's every-push dual-write
|
|
2743
2745
|
* into a BYO vault's own destination, mirroring {@link #tryMirrorPush}'s
|
|
2744
2746
|
* exact contract byte-for-byte (best-effort, catches EVERYTHING, never
|
|
2745
2747
|
* throws, never alters the vault outcome already committed above — a
|
|
2746
2748
|
* chain-copy failure is a named pending finding reported BESIDE the vault
|
|
2747
|
-
* result on its own field, never a `
|
|
2749
|
+
* result on its own field, never a `capture()`/`deploy()` throw).
|
|
2748
2750
|
*/
|
|
2749
2751
|
async #tryByoChainCopyPush(repoId, keystore) {
|
|
2750
2752
|
try {
|
|
@@ -2755,7 +2757,7 @@ export class Gitvault {
|
|
|
2755
2757
|
return { attempted: false, outcome: "skipped_no_mirror", error: e instanceof Error ? e.message : String(e) };
|
|
2756
2758
|
}
|
|
2757
2759
|
}
|
|
2758
|
-
/** Best-effort envelope-recipient reconcile: availability and local-policy failures never surface as a `
|
|
2760
|
+
/** Best-effort envelope-recipient reconcile: availability and local-policy failures never surface as a `capture()` throw (mirroring {@link #tryMirrorPush}), but INTEGRITY verdicts do — `RECONCILE_FATAL_CODES` propagates tampering evidence instead of demoting it to `skipped_error` (round 3 blocker 2). */
|
|
2759
2761
|
async #tryReconcileEnvelopeRecipients(vault) {
|
|
2760
2762
|
try {
|
|
2761
2763
|
const result = await vault.reconcileEnvelopeRecipients();
|
|
@@ -2767,12 +2769,12 @@ export class Gitvault {
|
|
|
2767
2769
|
}
|
|
2768
2770
|
}
|
|
2769
2771
|
/**
|
|
2770
|
-
*
|
|
2772
|
+
* vault-multi-writer (task 5.7) — best-effort writer-admission
|
|
2771
2773
|
* reconcile, the SAME non-blocking contract as {@link
|
|
2772
2774
|
* #tryReconcileEnvelopeRecipients} beside it: availability failures
|
|
2773
2775
|
* (network, an older gateway missing `pending_writers`/`signing_pubkey`,
|
|
2774
|
-
* a CAS-conflict-exhausted admission) never surface as a `
|
|
2775
|
-
* `deploy()`/`open()` throw — `
|
|
2776
|
+
* a CAS-conflict-exhausted admission) never surface as a `capture()`/
|
|
2777
|
+
* `deploy()`/`open()` throw — `Vault.reconcileWriterAdmissions()`
|
|
2776
2778
|
* ALREADY returns an empty result rather than throwing for the ordinary
|
|
2777
2779
|
* "I am not a writer yet" case, so `skipped_error` here is reserved for
|
|
2778
2780
|
* genuinely unexpected failures (a transport fault mid-loop, a thrown
|
|
@@ -2794,14 +2796,14 @@ export class Gitvault {
|
|
|
2794
2796
|
}
|
|
2795
2797
|
}
|
|
2796
2798
|
/**
|
|
2797
|
-
* A REAL preview of what {@link
|
|
2798
|
-
* — never publishes anything, and never allocates. `run402
|
|
2799
|
-
*
|
|
2799
|
+
* A REAL preview of what {@link capture} would publish
|
|
2800
|
+
* — never publishes anything, and never allocates. `run402 repos
|
|
2801
|
+
* capture --dry-run` and `git-remote-run402`'s `option dry-run true` are
|
|
2800
2802
|
* both thin adapters over this method.
|
|
2801
2803
|
*
|
|
2802
2804
|
* "Refusing beats fake success" (this file's own architectural law) governs
|
|
2803
2805
|
* the shape of what happens when the vault does not exist yet: unlike
|
|
2804
|
-
* {@link
|
|
2806
|
+
* {@link capture}, this NEVER allocates one (push-to-create must not allocate
|
|
2805
2807
|
* on a dry run) — it resolves READ-ONLY (`open`/`resolveOrCreateAddress`
|
|
2806
2808
|
* with `allow_create: false`), and when that resolution misses, returns
|
|
2807
2809
|
* `allocation_needed: true` with every sizing field `null` rather than a
|
|
@@ -2812,15 +2814,15 @@ export class Gitvault {
|
|
|
2812
2814
|
*
|
|
2813
2815
|
* Everything else is the SAME real local pipeline `push` runs — capture,
|
|
2814
2816
|
* pack building, sealing/encryption — computed by
|
|
2815
|
-
* {@link import("../node/
|
|
2817
|
+
* {@link import("../node/vault-publication.js").Vault.planPush},
|
|
2816
2818
|
* which is what actually stops short of the two network mutations
|
|
2817
2819
|
* (`uploadObjects`, `admitHead`). See its doc comment for what
|
|
2818
2820
|
* `would_admit_generation` does and does not promise.
|
|
2819
2821
|
*/
|
|
2820
|
-
async
|
|
2821
|
-
const { captureSnapshot,
|
|
2822
|
+
async planCapture(options) {
|
|
2823
|
+
const { captureSnapshot, vaultCommitLine } = await this.#snapshot();
|
|
2822
2824
|
const repoDir = options.repo_dir ?? process.cwd();
|
|
2823
|
-
// READ-ONLY resolution — mirrors `
|
|
2825
|
+
// READ-ONLY resolution — mirrors `capture()`'s own dispatch on `address`,
|
|
2824
2826
|
// but with `allow_create: false` and no `openOrCreate` fallback: a dry
|
|
2825
2827
|
// run must never mutate the control plane, and allocation is the one
|
|
2826
2828
|
// push-time mutation that happens before a single byte is built.
|
|
@@ -2836,14 +2838,14 @@ export class Gitvault {
|
|
|
2836
2838
|
handle = null; // no vault to preview a push against yet
|
|
2837
2839
|
}
|
|
2838
2840
|
const snapshot = await captureSnapshot({ dir: repoDir, ...(options.snapshot ?? {}) });
|
|
2839
|
-
const line =
|
|
2841
|
+
const line = vaultCommitLine(snapshot);
|
|
2840
2842
|
options.onCommitLine?.(line);
|
|
2841
2843
|
if (!handle) {
|
|
2842
2844
|
return {
|
|
2843
2845
|
base_generation: null, would_admit_generation: null, would_admit_generation_decimal: null,
|
|
2844
2846
|
form: null, refs: {}, head_target: null, objects: [], object_count: null,
|
|
2845
2847
|
encrypted_bytes: null, raw_bytes: null,
|
|
2846
|
-
allocation_needed: true, snapshot,
|
|
2848
|
+
allocation_needed: true, snapshot, vault_commit: snapshot.oid, vault_commit_line: line,
|
|
2847
2849
|
};
|
|
2848
2850
|
}
|
|
2849
2851
|
const { deployRefTransaction } = await this.#publication();
|
|
@@ -2851,15 +2853,15 @@ export class Gitvault {
|
|
|
2851
2853
|
const push = {
|
|
2852
2854
|
transaction: deployRefTransaction(materialized.refs, snapshot.oid),
|
|
2853
2855
|
head_target: snapshot.head,
|
|
2854
|
-
protocol_refs: "allow", // same opt-in `
|
|
2856
|
+
protocol_refs: "allow", // same opt-in `capture()` makes for the SAME move — see its doc comment.
|
|
2855
2857
|
...(options.checkpoint ? { checkpoint: true } : {}),
|
|
2856
2858
|
};
|
|
2857
2859
|
const plan = await handle.vault.planPush(push);
|
|
2858
|
-
return { ...plan, allocation_needed: false, snapshot,
|
|
2860
|
+
return { ...plan, allocation_needed: false, snapshot, vault_commit: snapshot.oid, vault_commit_line: line };
|
|
2859
2861
|
}
|
|
2860
2862
|
/**
|
|
2861
2863
|
* Compaction's transient-storage preflight
|
|
2862
|
-
* (
|
|
2864
|
+
* (vault-compaction-headroom-preflight, design D1/D2).
|
|
2863
2865
|
*
|
|
2864
2866
|
* Three postures, in order of information quality:
|
|
2865
2867
|
* 1. the check answers "won't fit" → typed refusal, before any upload;
|
|
@@ -2890,11 +2892,11 @@ export class Gitvault {
|
|
|
2890
2892
|
return this.#readCompactHeadroom(await this.#resolveRepoId(options));
|
|
2891
2893
|
}
|
|
2892
2894
|
/**
|
|
2893
|
-
* Open this vault's compaction headroom grant directly (
|
|
2895
|
+
* Open this vault's compaction headroom grant directly (vault-checkpoint-cadence
|
|
2894
2896
|
* design D3) — `compact()` already does this internally; this standalone
|
|
2895
2897
|
* entry point exists for callers that need to inspect or drive the grant
|
|
2896
2898
|
* without also running a full compaction cycle (tests; a future
|
|
2897
|
-
* operator/diagnostic surface). Throws `
|
|
2899
|
+
* operator/diagnostic surface). Throws `VAULT_COMPACTION_GRANT_ACTIVE`
|
|
2898
2900
|
* (409) verbatim when another compaction already holds this project's
|
|
2899
2901
|
* grant.
|
|
2900
2902
|
*/
|
|
@@ -2910,7 +2912,7 @@ export class Gitvault {
|
|
|
2910
2912
|
/**
|
|
2911
2913
|
* The arithmetic, with no policy: the block, or `null` when unanswerable.
|
|
2912
2914
|
*
|
|
2913
|
-
* `effectiveLimitOverride` (
|
|
2915
|
+
* `effectiveLimitOverride` (vault-checkpoint-cadence design D3): when a
|
|
2914
2916
|
* compaction grant is active for this cycle, `compact()` passes the
|
|
2915
2917
|
* grant's `effective_pool_limit_bytes` here so `ok`/`projected_transient_bytes`
|
|
2916
2918
|
* reflect the RAISED limit — `pool_limit_bytes` itself always stays the
|
|
@@ -2973,8 +2975,8 @@ export class Gitvault {
|
|
|
2973
2975
|
throw new LocalError(`compaction needs about ${mib(sourceBytes)} of transient headroom and the org's pooled storage has ${mib(Math.max(limitForMessage - poolUsed, 0))} free ` +
|
|
2974
2976
|
`(${mib(poolUsed)} used of ${mib(limitForMessage)}${effectiveLimit !== undefined ? ` — includes an active compaction grant raising the ${mib(poolLimit)} tier limit` : ""}; projected ${mib(projected)}). Compaction transiently holds BOTH the new checkpoint and the ` +
|
|
2975
2977
|
`not-yet-pruned history — roughly 2x source_bytes — until a prune completes. Free storage, raise the tier, or re-run with --force-headroom ` +
|
|
2976
|
-
`(the platform's own quota enforcement stays authoritative either way).`, "preflighting
|
|
2977
|
-
code: "
|
|
2978
|
+
`(the platform's own quota enforcement stays authoritative either way).`, "preflighting vault compaction storage headroom", {
|
|
2979
|
+
code: "VAULT_COMPACT_INSUFFICIENT_HEADROOM",
|
|
2978
2980
|
details: {
|
|
2979
2981
|
pool_used_bytes: poolUsed,
|
|
2980
2982
|
pool_limit_bytes: poolLimit,
|
|
@@ -2987,10 +2989,10 @@ export class Gitvault {
|
|
|
2987
2989
|
}
|
|
2988
2990
|
return { ...headroom, overridden: !ok && ignore };
|
|
2989
2991
|
}
|
|
2990
|
-
/** A 409 `
|
|
2992
|
+
/** A 409 `VAULT_COMPACTION_GRANT_ACTIVE` refusal — another compaction already holds this project's grant. */
|
|
2991
2993
|
#isCompactionGrantActive(e) {
|
|
2992
2994
|
const err = e;
|
|
2993
|
-
return Boolean(err && err.code === "
|
|
2995
|
+
return Boolean(err && err.code === "VAULT_COMPACTION_GRANT_ACTIVE");
|
|
2994
2996
|
}
|
|
2995
2997
|
/**
|
|
2996
2998
|
* Open this vault's compaction headroom grant, shared by `compact()` and
|
|
@@ -2998,7 +3000,7 @@ export class Gitvault {
|
|
|
2998
3000
|
* objects into the vault and both need the same single-flight-per-vault
|
|
2999
3001
|
* headroom slot. Four outcomes:
|
|
3000
3002
|
* - opened: the grant is returned, carrying `effective_pool_limit_bytes`.
|
|
3001
|
-
* - 409 `
|
|
3003
|
+
* - 409 `VAULT_COMPACTION_GRANT_ACTIVE`: another maintenance op (this
|
|
3002
3004
|
* process, another process, auto-gc, or a manual `repos gc`) already
|
|
3003
3005
|
* holds this project's one-at-a-time grant — refuse rather than race
|
|
3004
3006
|
* it, surfaced as the identical single-flight-conflict `LocalError`
|
|
@@ -3017,7 +3019,7 @@ export class Gitvault {
|
|
|
3017
3019
|
}
|
|
3018
3020
|
catch (e) {
|
|
3019
3021
|
if (this.#isCompactionGrantActive(e)) {
|
|
3020
|
-
throw new LocalError(`another compaction is already in progress for this vault (its headroom grant expires ${e.details?.expires_at ?? "soon"}) — refusing to race it.`, context, { code: "
|
|
3022
|
+
throw new LocalError(`another compaction is already in progress for this vault (its headroom grant expires ${e.details?.expires_at ?? "soon"}) — refusing to race it.`, context, { code: "VAULT_COMPACTION_IN_PROGRESS", details: { expires_at: e.details?.expires_at ?? null } });
|
|
3021
3023
|
}
|
|
3022
3024
|
// 404/absent-route or any other failure: proceed ungranted.
|
|
3023
3025
|
return null;
|
|
@@ -3035,14 +3037,14 @@ export class Gitvault {
|
|
|
3035
3037
|
*/
|
|
3036
3038
|
async compact(options = {}) {
|
|
3037
3039
|
const handle = await this.open(options);
|
|
3038
|
-
//
|
|
3040
|
+
// vault-checkpoint-cadence design D3: open the compaction headroom
|
|
3039
3041
|
// grant BEFORE the preflight (which needs its raised limit) and before
|
|
3040
3042
|
// any checkpoint is built or uploaded — same ordering rule as the
|
|
3041
3043
|
// preflight itself. Outcomes are documented on
|
|
3042
|
-
// {@link
|
|
3043
|
-
const grant = await this.#openMaintenanceGrantOrConflict(handle, "opening the
|
|
3044
|
+
// {@link VaultNamespace.#openMaintenanceGrantOrConflict}.
|
|
3045
|
+
const grant = await this.#openMaintenanceGrantOrConflict(handle, "opening the vault compaction headroom grant");
|
|
3044
3046
|
// Everything from here on is bracketed by the grant's own lifetime —
|
|
3045
|
-
// including the preflight's OWN possible throw (`
|
|
3047
|
+
// including the preflight's OWN possible throw (`VAULT_COMPACT_
|
|
3046
3048
|
// INSUFFICIENT_HEADROOM`) — so a refusal still closes a grant this call
|
|
3047
3049
|
// opened rather than leaking it until the gateway's hourly sweep.
|
|
3048
3050
|
try {
|
|
@@ -3056,7 +3058,7 @@ export class Gitvault {
|
|
|
3056
3058
|
/** The rest of `compact()`, run under an already-opened-or-skipped grant — split out so the grant's `finally` above covers every exit path, including the preflight's own throw. */
|
|
3057
3059
|
async #compactAfterGrant(handle, options, grant) {
|
|
3058
3060
|
// BEFORE any checkpoint is built or uploaded (that ordering is the whole
|
|
3059
|
-
// point — see
|
|
3061
|
+
// point — see VaultCompactHeadroom).
|
|
3060
3062
|
const headroom = await this.#compactHeadroomPreflight(handle.repo_id, options.ignoreHeadroom === true, grant?.effective_pool_limit_bytes);
|
|
3061
3063
|
const base = await handle.vault.materialize();
|
|
3062
3064
|
let lease = null;
|
|
@@ -3098,7 +3100,7 @@ export class Gitvault {
|
|
|
3098
3100
|
finally {
|
|
3099
3101
|
if (lease) {
|
|
3100
3102
|
await this.#client
|
|
3101
|
-
.request(`/
|
|
3103
|
+
.request(`/vaults/v1/${encodeURIComponent(handle.repo_id)}/maintenance-leases/${encodeURIComponent(lease.maintenance_lease_id)}`, { method: "DELETE", body: { holder_token: lease.holder_token }, context: "releasing the vault maintenance lease" })
|
|
3102
3104
|
.catch(() => undefined);
|
|
3103
3105
|
}
|
|
3104
3106
|
// The compaction headroom grant (if one was opened) is closed by the
|
|
@@ -3132,13 +3134,13 @@ export class Gitvault {
|
|
|
3132
3134
|
async prune(options = {}) {
|
|
3133
3135
|
const pub = await this.#publication();
|
|
3134
3136
|
const prune = await this.#prune();
|
|
3135
|
-
const { isRootEligibleForRemoval,
|
|
3137
|
+
const { isRootEligibleForRemoval, VAULT_RETENTION_MIN_DAYS } = pub;
|
|
3136
3138
|
const handle = await this.open(options);
|
|
3137
|
-
//
|
|
3139
|
+
// vault-byo-primary-bucket: prune deletion on a BYO vault is not
|
|
3138
3140
|
// shipped (the gateway's worker deletes from run402's own storage, which
|
|
3139
3141
|
// holds none of this vault's payload; client-executed deletion is the
|
|
3140
3142
|
// open design item), and the gateway refuses an intent by name
|
|
3141
|
-
// (
|
|
3143
|
+
// (VAULT_BYO_PRUNE_NOT_SUPPORTED). Say so before any planning or
|
|
3142
3144
|
// ceremony — a plan here would only invite a refused submit.
|
|
3143
3145
|
const record = await handle.vault.transport.getVaultRecord({ repo_id: handle.repo_id });
|
|
3144
3146
|
if (record.storage_profile === "byo") {
|
|
@@ -3148,7 +3150,7 @@ export class Gitvault {
|
|
|
3148
3150
|
retained_count: 0,
|
|
3149
3151
|
object_candidates: [],
|
|
3150
3152
|
deferred_object_count: 0,
|
|
3151
|
-
blocked_reason: "this vault keeps its payload in your own bucket (storage_profile byo); run402 cannot delete from a bucket it holds no credential to, and client-executed prune deletion has not shipped — the gateway refuses a prune intent on a BYO vault by name (
|
|
3153
|
+
blocked_reason: "this vault keeps its payload in your own bucket (storage_profile byo); run402 cannot delete from a bucket it holds no credential to, and client-executed prune deletion has not shipped — the gateway refuses a prune intent on a BYO vault by name (VAULT_BYO_PRUNE_NOT_SUPPORTED); compaction still works and superseded history stays in your bucket",
|
|
3152
3154
|
intent_core: null,
|
|
3153
3155
|
intent_core_sha256: null,
|
|
3154
3156
|
attestation: null,
|
|
@@ -3173,8 +3175,8 @@ export class Gitvault {
|
|
|
3173
3175
|
reason: admittedAt === null
|
|
3174
3176
|
? "this client cannot resolve the drop's effective_admitted_at, which RETAINS the root — expiry is permissive by design"
|
|
3175
3177
|
: eligible
|
|
3176
|
-
? `dropped more than ${
|
|
3177
|
-
: `still inside the ${
|
|
3178
|
+
? `dropped more than ${VAULT_RETENTION_MIN_DAYS} days before the cutoff; removable at the next checkpoint-bearing generation, then at the next successful prune`
|
|
3179
|
+
: `still inside the ${VAULT_RETENTION_MIN_DAYS}-day retention window`,
|
|
3178
3180
|
};
|
|
3179
3181
|
});
|
|
3180
3182
|
const rootView = {
|
|
@@ -3275,8 +3277,8 @@ export class Gitvault {
|
|
|
3275
3277
|
// the SDK never signs an attestation it did not earn.
|
|
3276
3278
|
retention_evolution_ok: rootsEvolution.ok && attestation.object_set_matches && attestation.ref_state_matches && attestation.retention_roots_matches,
|
|
3277
3279
|
candidates_outside_roots_ok: outsideRoots,
|
|
3278
|
-
implementation_id: prune.
|
|
3279
|
-
implementation_version: prune.
|
|
3280
|
+
implementation_id: prune.VAULT_SDK_VERIFIER_IMPLEMENTATION,
|
|
3281
|
+
implementation_version: prune.VAULT_SDK_VERIFIER_VERSION,
|
|
3280
3282
|
}, handle.vault.signer());
|
|
3281
3283
|
const intent = prune.buildPruneIntent(core, [ours, options.submit.verifier_receipt], handle.vault.signer());
|
|
3282
3284
|
// The submission writes ~8 KB of ceremony
|
|
@@ -3287,7 +3289,7 @@ export class Gitvault {
|
|
|
3287
3289
|
// the SAME compaction headroom grant `compact()` uses (design D3, via
|
|
3288
3290
|
// the shared `#openMaintenanceGrantOrConflict`) — opened before the
|
|
3289
3291
|
// upload, closed in a `finally` regardless of outcome.
|
|
3290
|
-
const grant = await this.#openMaintenanceGrantOrConflict(handle, "opening the
|
|
3292
|
+
const grant = await this.#openMaintenanceGrantOrConflict(handle, "opening the vault compaction headroom grant for prune submission");
|
|
3291
3293
|
let stored;
|
|
3292
3294
|
try {
|
|
3293
3295
|
await this.#uploadVerifierReceipts(handle, pub, [ours, options.submit.verifier_receipt]);
|
|
@@ -3320,11 +3322,11 @@ export class Gitvault {
|
|
|
3320
3322
|
}
|
|
3321
3323
|
/** Upload both receipts so the gateway can claim them at the intent's fence. */
|
|
3322
3324
|
async #uploadVerifierReceipts(handle, pub, receipts) {
|
|
3323
|
-
const { storedBytes, sha256Hex } = await import("./
|
|
3325
|
+
const { storedBytes, sha256Hex } = await import("./vault.crypto.js");
|
|
3324
3326
|
const objects = receipts.map((r) => {
|
|
3325
3327
|
const bytes = storedBytes(r);
|
|
3326
3328
|
return {
|
|
3327
|
-
path: pub.
|
|
3329
|
+
path: pub.vaultPaths.verifierReceipt(r.object_id),
|
|
3328
3330
|
object_kind: "verifier_receipt",
|
|
3329
3331
|
object_id: r.object_id,
|
|
3330
3332
|
bytes,
|
|
@@ -3363,7 +3365,7 @@ export class Gitvault {
|
|
|
3363
3365
|
continue; // identical bytes already landed — nothing to do
|
|
3364
3366
|
throw new LocalError(existing
|
|
3365
3367
|
? `verifier receipt ${o.object_id} already exists in the vault with DIFFERENT bytes than this attempt's (stored sha256 ${existingSha256}, this attempt's sha256 ${o.sha256}) — the object id is burned and cannot be reused; re-plan the prune (new core, new nonce, new r402s-verify run).`
|
|
3366
|
-
: `verifier receipt ${o.object_id} was refused as an already-used id, but its stored bytes could not be read back to confirm they match — re-plan the prune.`, "uploading
|
|
3368
|
+
: `verifier receipt ${o.object_id} was refused as an already-used id, but its stored bytes could not be read back to confirm they match — re-plan the prune.`, "uploading vault verifier receipts", { code: "VAULT_RECEIPT_ID_REUSED_DIFFERENT", details: { object_id: o.object_id, existing_sha256: existingSha256, this_attempt_sha256: o.sha256 } });
|
|
3367
3369
|
}
|
|
3368
3370
|
// Every reused id was confirmed identical to ours; anything NOT
|
|
3369
3371
|
// reused (a genuinely new upload, or the other side of a partial
|
|
@@ -3374,7 +3376,7 @@ export class Gitvault {
|
|
|
3374
3376
|
}
|
|
3375
3377
|
/**
|
|
3376
3378
|
* `VALIDATION_FAILED` with `details.reused_object_ids`
|
|
3377
|
-
* (`services/
|
|
3379
|
+
* (`services/vault/upload-sessions.ts`: "an object id in the manifest is
|
|
3378
3380
|
* already used") — object ids a prior attempt already landed. `null` for
|
|
3379
3381
|
* any other failure, so the caller rethrows it untouched.
|
|
3380
3382
|
*/
|
|
@@ -3439,16 +3441,16 @@ export class Gitvault {
|
|
|
3439
3441
|
* bare "verified": `chain_verified_to_generation` and
|
|
3440
3442
|
* `decryptable_to_generation` are reported separately and can genuinely
|
|
3441
3443
|
* differ — the incident-shape truth ("chain verified to 10, decryptable
|
|
3442
|
-
* to 7, `
|
|
3444
|
+
* to 7, `VAULT_EPOCH_NOT_OPENABLE` at epoch 2/rotation …") is exactly
|
|
3443
3445
|
* what this split makes representable. `refs`/`head_target`/
|
|
3444
3446
|
* `pin_after.highest_materialized` all reflect `decryptable_to_generation`.
|
|
3445
3447
|
*
|
|
3446
|
-
* **
|
|
3448
|
+
* **vault-byo-primary-bucket task 3.3 (design D6).** For a
|
|
3447
3449
|
* `storage_profile: "byo"` vault, `fsck` ALSO adjudicates the customer's
|
|
3448
3450
|
* own bucket against run402's signed chain — see {@link
|
|
3449
3451
|
* #checkByoPresence} for the full contract (automatic, never a flag;
|
|
3450
3452
|
* fails soft with no local credentials; throws
|
|
3451
|
-
* `
|
|
3453
|
+
* `VAULT_BYO_OBJECT_MISSING` on a confirmed absence). A managed vault
|
|
3452
3454
|
* is byte-identical to before this task.
|
|
3453
3455
|
*/
|
|
3454
3456
|
async fsck(options = {}) {
|
|
@@ -3457,10 +3459,10 @@ export class Gitvault {
|
|
|
3457
3459
|
if (!repoId) {
|
|
3458
3460
|
throw new LocalError(options.repo_id || options.project_id
|
|
3459
3461
|
? "no vault is allocated for this project yet — nothing to fsck"
|
|
3460
|
-
: "pass repo_id, or project_id to resolve it from the control plane", "running
|
|
3462
|
+
: "pass repo_id, or project_id to resolve it from the control plane", "running vault fsck", { code: "VAULT_UNRESOLVED", details: { project_id: options.project_id ?? null } });
|
|
3461
3463
|
}
|
|
3462
3464
|
const write = options.write ?? true;
|
|
3463
|
-
//
|
|
3465
|
+
// vault-agent-envelopes D5: fsck is observational — it never wraps an
|
|
3464
3466
|
// envelope, in write mode or not. Investigating a suspicious pending
|
|
3465
3467
|
// recipient must not complete the disclosure being investigated.
|
|
3466
3468
|
const handle = await this.open({ ...options, repo_id: repoId, reconcile: "forbidden" });
|
|
@@ -3497,7 +3499,7 @@ export class Gitvault {
|
|
|
3497
3499
|
chain_verified_to_generation: state.generation,
|
|
3498
3500
|
decryptable_to_generation: decryptableToGeneration,
|
|
3499
3501
|
});
|
|
3500
|
-
//
|
|
3502
|
+
// vault-byo-primary-bucket task 3.3 (design D6) — LAST, deliberately:
|
|
3501
3503
|
// everything above (chain verify + its pin persist, retained-refs
|
|
3502
3504
|
// reconcile, open-proof submission) is real, valid work regardless of
|
|
3503
3505
|
// what the customer's own bucket holds — the signed chain is run402-
|
|
@@ -3538,13 +3540,13 @@ export class Gitvault {
|
|
|
3538
3540
|
evidence,
|
|
3539
3541
|
resolvePrincipalId: async () => {
|
|
3540
3542
|
const who = await this.#client.request("/agent/v1/whoami", {
|
|
3541
|
-
context: "resolving this principal's identity for a
|
|
3543
|
+
context: "resolving this principal's identity for a vault proof-of-open submission",
|
|
3542
3544
|
});
|
|
3543
3545
|
return who.principal?.id ?? null;
|
|
3544
3546
|
},
|
|
3545
3547
|
readerEntrypoint: async () => {
|
|
3546
|
-
const {
|
|
3547
|
-
return
|
|
3548
|
+
const { vaultReaderEntrypoint } = await this.#publication();
|
|
3549
|
+
return vaultReaderEntrypoint("fsck");
|
|
3548
3550
|
},
|
|
3549
3551
|
submit: (principalId, ekFingerprint, ev) => this.submitProofOfOpen(repoId, principalId, {
|
|
3550
3552
|
ek_fingerprint: ekFingerprint,
|
|
@@ -3555,9 +3557,9 @@ export class Gitvault {
|
|
|
3555
3557
|
});
|
|
3556
3558
|
}
|
|
3557
3559
|
/**
|
|
3558
|
-
*
|
|
3560
|
+
* vault-byo-primary-bucket task 3.3 (design D6) — `repos fsck`'s
|
|
3559
3561
|
* wiring of the shipped read-half primitive
|
|
3560
|
-
* {@link import("../node/
|
|
3562
|
+
* {@link import("../node/vault-mirror.js").verifyByoObjectsPresent}.
|
|
3561
3563
|
* Thin wrapper over {@link computeByoPresenceOutcome} (the testable
|
|
3562
3564
|
* decision function, factored out the same way {@link
|
|
3563
3565
|
* computeOpenProofOutcome} is factored out of `#submitFsckOpenProof`)
|
|
@@ -3579,23 +3581,21 @@ export class Gitvault {
|
|
|
3579
3581
|
/**
|
|
3580
3582
|
* Wrap this vault's current epoch key to every org member who has
|
|
3581
3583
|
* published an encryption key but has no `key_envelope` on this vault yet
|
|
3582
|
-
* —
|
|
3583
|
-
* import("../node/
|
|
3584
|
+
* — vault-human-envelopes task 4.1's ADD-path workaround. See {@link
|
|
3585
|
+
* import("../node/vault-publication.js").Vault.
|
|
3584
3586
|
* reconcileEnvelopeRecipients} for the full design (TOFU pinning, the
|
|
3585
3587
|
* gateway `public_key` gap, and why this is a workaround rather than the
|
|
3586
3588
|
* eventual epoch-rotation design).
|
|
3587
3589
|
*
|
|
3588
|
-
*
|
|
3589
|
-
*
|
|
3590
|
-
* pointing at `repos access` for inspection. This method itself stays:
|
|
3591
|
-
* `deploy()` runs it, best-effort,
|
|
3590
|
+
* No CLI verb runs this directly — `repos access` is the inspection
|
|
3591
|
+
* read. `deploy()` runs it, best-effort,
|
|
3592
3592
|
* whenever a deploy lands a new generation in the vault — design D5's
|
|
3593
3593
|
* "deploy time" hook, "the same 'one command every agent runs' argument
|
|
3594
|
-
* that decided deploy-implies-capture." `
|
|
3594
|
+
* that decided deploy-implies-capture." `capture()` (capture-and-publish
|
|
3595
3595
|
* outside a deploy) runs the identical hook after every successful
|
|
3596
3596
|
* publish, for the vault-only-project cadence. See
|
|
3597
3597
|
* `#tryReconcileEnvelopeRecipients` below for both call sites, and
|
|
3598
|
-
* {@link
|
|
3598
|
+
* {@link Repos.access} for the READ half repo-surface-consolidation
|
|
3599
3599
|
* ships in its place.
|
|
3600
3600
|
*/
|
|
3601
3601
|
async reconcileEnvelopeRecipients(options = {}) {
|
|
@@ -3603,12 +3603,12 @@ export class Gitvault {
|
|
|
3603
3603
|
return handle.vault.reconcileEnvelopeRecipients();
|
|
3604
3604
|
}
|
|
3605
3605
|
/**
|
|
3606
|
-
*
|
|
3606
|
+
* vault-multi-writer (task 5.7) — admit every eligible `pending_writers[]`
|
|
3607
3607
|
* candidate (active org membership at role developer+, a published,
|
|
3608
3608
|
* possession-verified signing key, not yet in the writer set) via a fresh
|
|
3609
3609
|
* `add_writer_key{"writer"}` head per candidate. This session's OWN key
|
|
3610
3610
|
* must already be an active writer — see {@link
|
|
3611
|
-
* import("../node/
|
|
3611
|
+
* import("../node/vault-publication.js").Vault.
|
|
3612
3612
|
* reconcileWriterAdmissions}'s doc comment for why that is checked
|
|
3613
3613
|
* locally, once, rather than surfaced as N identical gateway refusals.
|
|
3614
3614
|
*
|
|
@@ -3631,7 +3631,7 @@ export class Gitvault {
|
|
|
3631
3631
|
* `rotation_attempt_descriptor`, submit the `rotate_epoch` head, verify
|
|
3632
3632
|
* this principal's own envelope opens to the committed key (when it is
|
|
3633
3633
|
* itself a recipient), and advance the local keystore's epoch pointer.
|
|
3634
|
-
* See {@link import("../node/
|
|
3634
|
+
* See {@link import("../node/vault-publication.js").Vault.
|
|
3635
3635
|
* rotateEpoch}'s own doc comment for the full obligations and — load-
|
|
3636
3636
|
* bearing — the confirmed gap in what the gateway exposes for
|
|
3637
3637
|
* `recipient_state_version`/`recipient_revocation_version` outside the
|
|
@@ -3654,9 +3654,9 @@ export class Gitvault {
|
|
|
3654
3654
|
*/
|
|
3655
3655
|
/**
|
|
3656
3656
|
* The writer-capable rotation that completes an org membership removal
|
|
3657
|
-
* (`reason:"member_removed"`,
|
|
3657
|
+
* (`reason:"member_removed"`, vault-multi-writer D6): counters come off
|
|
3658
3658
|
* the envelope-recipients read, no declaration, no owner step-up — any
|
|
3659
|
-
* surviving writer can run it. `
|
|
3659
|
+
* surviving writer can run it. `capture()` runs it automatically when the
|
|
3660
3660
|
* gate names an outstanding removal; this is the explicit entry point
|
|
3661
3661
|
* (`run402 orgs members rm` drives it on every vault the caller can).
|
|
3662
3662
|
*/
|
|
@@ -3693,19 +3693,19 @@ export class Gitvault {
|
|
|
3693
3693
|
/**
|
|
3694
3694
|
* `EPOCH_ROTATION_REQUIRED` from a `publishPinManifestUpdate` call names
|
|
3695
3695
|
* the SAME three causes {@link #enrichEpochRotationRequired} decodes for
|
|
3696
|
-
* `
|
|
3696
|
+
* `capture()`, but the remedy is different: a manifest-only publish should
|
|
3697
3697
|
* fold into the NEXT `rotateEpoch` call via `pending_confirmations`
|
|
3698
3698
|
* rather than run a separate migration/revocation/exposure rotation
|
|
3699
3699
|
* first and publish again afterward — one admission instead of two, and
|
|
3700
3700
|
* the only path that works at all while EVERY predecessor-confirmed
|
|
3701
|
-
* principal is already exhausted (see `
|
|
3701
|
+
* principal is already exhausted (see `Vault.rotateEpoch`'s own
|
|
3702
3702
|
* doc comment for the honest residual: this does not rescue a vault with
|
|
3703
3703
|
* zero ever-confirmed principals, which needs an operator-side fix).
|
|
3704
3704
|
*/
|
|
3705
3705
|
#enrichEpochRotationRequiredForPinManifest(e, repoId) {
|
|
3706
3706
|
if (!isRun402Error(e) || e.code !== "EPOCH_ROTATION_REQUIRED")
|
|
3707
3707
|
return e;
|
|
3708
|
-
return new LocalError(`this vault requires a rotate_epoch admission before an ordinary pin-manifest publish is admissible (repo_id ${repoId}) — fold this SAME receipt into rotateEpoch({..., pending_confirmations: [{principal_id, ek_fingerprint, receipt}]}) instead of retrying this call`, "publishing a recipient_pin_manifest update", { code: "EPOCH_ROTATION_REQUIRED", details: e.details, next_actions: [{ action: "r.
|
|
3708
|
+
return new LocalError(`this vault requires a rotate_epoch admission before an ordinary pin-manifest publish is admissible (repo_id ${repoId}) — fold this SAME receipt into rotateEpoch({..., pending_confirmations: [{principal_id, ek_fingerprint, receipt}]}) instead of retrying this call`, "publishing a recipient_pin_manifest update", { code: "EPOCH_ROTATION_REQUIRED", details: e.details, next_actions: [{ action: "r.repos.rotateEpoch({..., pending_confirmations: [{principal_id, ek_fingerprint, receipt}]})", why: "the manifest publish rides the SAME head as the required rotation, which is EPOCH_ROTATION_REQUIRED's own escape valve — a standalone publish never is" }], cause: e });
|
|
3709
3709
|
}
|
|
3710
3710
|
/**
|
|
3711
3711
|
* `repos access` (repo-surface-consolidation D5/D10) — a READ-ONLY report
|
|
@@ -3713,15 +3713,15 @@ export class Gitvault {
|
|
|
3713
3713
|
* surface exposes today. Never wraps, never mutates a `key_envelope` —
|
|
3714
3714
|
* that mutating half stays {@link reconcileEnvelopeRecipients}, reachable
|
|
3715
3715
|
* only through the deploy/push best-effort hooks until `access repair`
|
|
3716
|
-
* ships (gated on `
|
|
3717
|
-
* {@link
|
|
3716
|
+
* ships (gated on `vault-human-envelopes`' epoch-rotation work). See
|
|
3717
|
+
* {@link VaultAccessResult}'s doc comment for the honest gap this
|
|
3718
3718
|
* reports rather than invents.
|
|
3719
3719
|
*/
|
|
3720
3720
|
async access(options = {}) {
|
|
3721
3721
|
const repoId = await this.#resolveRepoId(options);
|
|
3722
3722
|
const record = await this.get(repoId).catch(() => null);
|
|
3723
3723
|
const orgId = record?.org_id ?? null;
|
|
3724
|
-
//
|
|
3724
|
+
// vault-agent-envelopes D3/D5: `access` is one of the ordinary reads a
|
|
3725
3725
|
// KEY-HOLDER's session starts with — enroll this keystore's key if absent
|
|
3726
3726
|
// and fulfil every pending desired recipient once per process, BEFORE the
|
|
3727
3727
|
// roster below is read, so the roster reflects the fulfilment. Reported,
|
|
@@ -3731,8 +3731,8 @@ export class Gitvault {
|
|
|
3731
3731
|
let enrollment = null;
|
|
3732
3732
|
if (options.reconcile !== "forbidden") {
|
|
3733
3733
|
try {
|
|
3734
|
-
const {
|
|
3735
|
-
const probe = new
|
|
3734
|
+
const { VaultKeystore } = await this.#keystore();
|
|
3735
|
+
const probe = new VaultKeystore(options.keystore_root !== undefined ? { rootDir: options.keystore_root } : {});
|
|
3736
3736
|
if (probe.readRepo(repoId)) {
|
|
3737
3737
|
const handle = await this.open({ ...options, repo_id: repoId });
|
|
3738
3738
|
reconcileRecipients = handle.reconcile_recipients;
|
|
@@ -3747,10 +3747,10 @@ export class Gitvault {
|
|
|
3747
3747
|
orgId
|
|
3748
3748
|
? this.#client.request(`/orgs/v1/${encodeURIComponent(orgId)}/encryption-keys`, { context: "reading the org encryption-key directory" })
|
|
3749
3749
|
: Promise.resolve({ org_id: "", keys: [] }),
|
|
3750
|
-
this.#client.request(`/
|
|
3750
|
+
this.#client.request(`/vaults/v1/${encodeURIComponent(repoId)}/envelope-recipients`, { context: "reading the vault envelope recipients" }),
|
|
3751
3751
|
]);
|
|
3752
3752
|
const covered = new Set(coverage.recipient_fingerprints);
|
|
3753
|
-
// `desired` is OPTIONAL on the wire (see
|
|
3753
|
+
// `desired` is OPTIONAL on the wire (see VaultEnvelopeRecipientsResponse):
|
|
3754
3754
|
// absent means an older gateway that predates this field — genuinely
|
|
3755
3755
|
// unknown, not "no desired recipients." Distinguish "absent" from
|
|
3756
3756
|
// "present and empty" via the array itself, not a boolean flag, so a
|
|
@@ -3770,8 +3770,8 @@ export class Gitvault {
|
|
|
3770
3770
|
let pins = {};
|
|
3771
3771
|
let ownFingerprint = null;
|
|
3772
3772
|
try {
|
|
3773
|
-
const {
|
|
3774
|
-
const keystore = new
|
|
3773
|
+
const { VaultKeystore } = await this.#keystore();
|
|
3774
|
+
const keystore = new VaultKeystore(options.keystore_root !== undefined ? { rootDir: options.keystore_root } : {});
|
|
3775
3775
|
pins = keystore.readRepo(repoId)?.envelope_recipient_pins ?? {};
|
|
3776
3776
|
ownFingerprint = keystore.readIdentity()?.encryption_fingerprint ?? null;
|
|
3777
3777
|
}
|
|
@@ -3794,8 +3794,8 @@ export class Gitvault {
|
|
|
3794
3794
|
const directoryFingerprints = new Set(directory.keys.map((k) => k.ek_fingerprint));
|
|
3795
3795
|
// Removed members (desired status pending_removal) whose fingerprint is
|
|
3796
3796
|
// STILL covered: real, continuing access that membership removal did not
|
|
3797
|
-
// revoke, because
|
|
3798
|
-
//
|
|
3797
|
+
// revoke, because vault v0 has no epoch-rotation mechanism yet. See
|
|
3798
|
+
// VaultAccessResult's doc comment.
|
|
3799
3799
|
const staleAccess = (desiredList ?? [])
|
|
3800
3800
|
.filter((d) => d.status === "pending_removal" && d.ek_fingerprint != null && covered.has(d.ek_fingerprint))
|
|
3801
3801
|
.map((d) => ({ principal_id: d.principal_id, display_name: d.display_name, fingerprint: d.ek_fingerprint }));
|
|
@@ -3810,7 +3810,7 @@ export class Gitvault {
|
|
|
3810
3810
|
// not orphaned/external, it is provably this machine, so it is broken
|
|
3811
3811
|
// out into `this_keystore` instead of left to read as a misconfiguration.
|
|
3812
3812
|
const unmatchedRaw = coverage.recipient_fingerprints.filter((fp) => !directoryFingerprints.has(fp) && !staleAccessFingerprints.has(fp));
|
|
3813
|
-
//
|
|
3813
|
+
// vault-agent-envelopes: THIS machine's keystore is reported for every
|
|
3814
3814
|
// principal type — enrolled or not, covered or not — never `null` for a
|
|
3815
3815
|
// real keystore (only when there is no local identity at all). The
|
|
3816
3816
|
// directory now lists agent keys too, so "own fingerprint in the
|
|
@@ -3837,7 +3837,7 @@ export class Gitvault {
|
|
|
3837
3837
|
...(!covered.has(ownFingerprint) && publishState === "active"
|
|
3838
3838
|
? { next_actions: [
|
|
3839
3839
|
{ action: "run402 repos access", why: "poll — covered_on_this_vault flips true once a key-holder has wrapped this vault to your key" },
|
|
3840
|
-
{ action: "ask a key-holder to run any
|
|
3840
|
+
{ action: "ask a key-holder to run any vault operation (run402 repos view / git push)", why: "a key-holding client wraps every pending desired recipient on its next operation" },
|
|
3841
3841
|
] }
|
|
3842
3842
|
: {}),
|
|
3843
3843
|
...(publishState === "rotation_required"
|
|
@@ -3856,7 +3856,7 @@ export class Gitvault {
|
|
|
3856
3856
|
"covered:true they still decrypt this vault until an owner runs `repos access repair` (or, for a targeted key, `repos access " +
|
|
3857
3857
|
"revoke-key`); see stale_access for exactly who, and next_actions for the exact command."
|
|
3858
3858
|
: "the gateway did not report desired-recipient state (desired[]) for this vault_id — likely an older gateway than this SDK " +
|
|
3859
|
-
"expects, so per-recipient envelope_state and stale_access are unavailable. history_scope is unavailable regardless:
|
|
3859
|
+
"expects, so per-recipient envelope_state and stale_access are unavailable. history_scope is unavailable regardless: vault " +
|
|
3860
3860
|
"protocol v0 pins a single fixed epoch, so there is no per-epoch scope to report. This reports what the read surface has today: " +
|
|
3861
3861
|
"the org's directory of encryption-key-holding members, which of the vault's current envelope-recipient fingerprints match a " +
|
|
3862
3862
|
"directory entry (covered), and this machine's own local TOFU pin per principal when it has ever wrapped one (tofu_pin) — never a server-side fact.";
|
|
@@ -3895,31 +3895,31 @@ export class Gitvault {
|
|
|
3895
3895
|
* `run402 deploy` is design D5's PRIMARY envelope-recipient reconcile hook
|
|
3896
3896
|
* ("the same 'one command every agent runs' argument that decided
|
|
3897
3897
|
* deploy-implies-capture") and design D6's primary dual-push mirror trigger
|
|
3898
|
-
* — both fire HERE, not inside {@link
|
|
3899
|
-
* {@link
|
|
3898
|
+
* — both fire HERE, not inside {@link runVaultDeploy} itself, mirroring
|
|
3899
|
+
* {@link capture}'s exact non-blocking contract: best-effort, NEVER throw,
|
|
3900
3900
|
* NEVER alter the deploy outcome already resolved above, reported BESIDE it
|
|
3901
3901
|
* on `mirror_push` / `reconcile_recipients`. They fire only when this
|
|
3902
3902
|
* deploy actually landed a new generation in the vault
|
|
3903
3903
|
* (`DEPLOYED_AND_VAULTED` / `DEPLOY_FAILED_VAULTED` — the outcomes carrying
|
|
3904
3904
|
* a `generation`); the other three outcomes published nothing new, so
|
|
3905
3905
|
* there is nothing to mirror or reconcile against ({@link
|
|
3906
|
-
* import("../node/
|
|
3906
|
+
* import("../node/vault-mirror.js").mirrorPushForGeneration}'s own
|
|
3907
3907
|
* contract: "fires after an ordinary vault push/deploy produces a new
|
|
3908
3908
|
* generation"). Both fields are therefore OMITTED — never a faked
|
|
3909
3909
|
* `skipped_*` outcome — when no generation landed.
|
|
3910
3910
|
*/
|
|
3911
3911
|
async deploy(options) {
|
|
3912
|
-
const {
|
|
3912
|
+
const { runVaultDeploy } = await this.#deploy();
|
|
3913
3913
|
const handle = await this.open(options);
|
|
3914
|
-
const result = await
|
|
3915
|
-
// Same ordering
|
|
3914
|
+
const result = await runVaultDeploy({ ...options, vault: handle.vault, repo_dir: options.repo_dir ?? process.cwd() });
|
|
3915
|
+
// Same ordering capture() uses (mirror, then reconcile) — sequential, not
|
|
3916
3916
|
// Promise.all, so a reconcile failure can never be misread as a mirror
|
|
3917
3917
|
// failure in a log line that assumed ordering. Extracted to a standalone
|
|
3918
3918
|
// function so the outcome-gating is unit-testable with fake thunks,
|
|
3919
|
-
// without standing up a live vault — see
|
|
3920
|
-
const hooked = await
|
|
3921
|
-
//
|
|
3922
|
-
// — composed AFTER, not inside, `
|
|
3919
|
+
// without standing up a live vault — see repos-deploy-hooks.test.ts.
|
|
3920
|
+
const hooked = await attachVaultDeployHooks(result, () => this.#tryMirrorPush(handle.repo_id, handle.keystore), () => this.#tryReconcileEnvelopeRecipients(handle.vault));
|
|
3921
|
+
// vault-byo-primary-bucket task 3.3 / vault-multi-writer task 5.7
|
|
3922
|
+
// — composed AFTER, not inside, `attachVaultDeployHooks` (kept
|
|
3923
3923
|
// untouched so its own unit-tested outcome-gating contract stays
|
|
3924
3924
|
// byte-for-byte): the SAME "did this deploy actually land a generation"
|
|
3925
3925
|
// gate `mirror_push`'s presence already encodes, so no new
|
|
@@ -3948,14 +3948,14 @@ export class Gitvault {
|
|
|
3948
3948
|
const out = await handle.vault.restoreObjectsInto(options.target_dir);
|
|
3949
3949
|
return { refs: out.refs, generation: out.generation, retained_refs: out.retained_refs };
|
|
3950
3950
|
}
|
|
3951
|
-
// ── degraded read (
|
|
3951
|
+
// ── degraded read (vault-byo-primary-bucket, design D4, task 3.4 — mirror half) ──
|
|
3952
3952
|
/**
|
|
3953
3953
|
* Wrap an ALREADY-RESOLVED vault read with degraded-read fallback: run
|
|
3954
3954
|
* `options.attemptLive` (the caller's own live call — its
|
|
3955
3955
|
* `vault.materialize()` for a `list`-shaped read, or
|
|
3956
3956
|
* `vault.restoreObjectsInto(dir)` for a `fetch`-shaped one), and on a
|
|
3957
3957
|
* network-class failure (never on an authorization refusal or any other
|
|
3958
|
-
* 4xx — see `
|
|
3958
|
+
* 4xx — see `isNetworkClassVaultReadError`), fall back to the vault's
|
|
3959
3959
|
* configured mirror via the SAME `r402s-recover` engine `recover()` uses.
|
|
3960
3960
|
*
|
|
3961
3961
|
* Deliberately takes an already-open `vault`/`keystore`/`repo_id` rather
|
|
@@ -3974,17 +3974,17 @@ export class Gitvault {
|
|
|
3974
3974
|
* entirely: the original gateway error surfaces exactly as it always did.
|
|
3975
3975
|
*/
|
|
3976
3976
|
async withDegradedRead(options) {
|
|
3977
|
-
const {
|
|
3978
|
-
return
|
|
3977
|
+
const { tryVaultDegradedRead } = await this.#degradedRead();
|
|
3978
|
+
return tryVaultDegradedRead(options);
|
|
3979
3979
|
}
|
|
3980
3980
|
/**
|
|
3981
|
-
*
|
|
3981
|
+
* vault-byo-primary-bucket (design D4): the degraded read for the OPEN
|
|
3982
3982
|
* itself. {@link withDegradedRead} wraps a handle's live read, but a plain
|
|
3983
3983
|
* `git fetch`/`clone` needs the gateway once BEFORE any read — to open the
|
|
3984
3984
|
* handle (enrollment, the vault record). When that open fails
|
|
3985
3985
|
* network-class and this checkout already knows the vault — a pinned
|
|
3986
3986
|
* `repo_id`, or the keystore's own project→repo file for an id-form
|
|
3987
|
-
* address (
|
|
3987
|
+
* address (vault-offline-clone-resolve) — the whole read is served from
|
|
3988
3988
|
* the vault's BYO destination or configured mirror, exactly as
|
|
3989
3989
|
* `withDegradedRead` would have after a failed live read. `null` when this
|
|
3990
3990
|
* cannot help (a non-network failure, no local identity for the vault, no
|
|
@@ -3992,13 +3992,13 @@ export class Gitvault {
|
|
|
3992
3992
|
* Writes are never rerouted.
|
|
3993
3993
|
*/
|
|
3994
3994
|
async degradedOpenFallback(input) {
|
|
3995
|
-
const [{
|
|
3996
|
-
if (!
|
|
3995
|
+
const [{ isNetworkClassVaultReadError, tryVaultDegradedRead }, { VaultKeystore }, { readPinnedVaultRepo, resolveVaultAddress }, { createVaultHttpTransport }] = await Promise.all([this.#degradedRead(), this.#keystore(), this.#address(), this.#publication()]);
|
|
3996
|
+
if (!isNetworkClassVaultReadError(input.error))
|
|
3997
3997
|
return null;
|
|
3998
|
-
const keystore = new
|
|
3998
|
+
const keystore = new VaultKeystore({});
|
|
3999
3999
|
let repoId = input.repo_id ?? null;
|
|
4000
4000
|
if (!repoId) {
|
|
4001
|
-
const pinned = await
|
|
4001
|
+
const pinned = await readPinnedVaultRepo(input.repo_dir).catch(() => null);
|
|
4002
4002
|
repoId = pinned?.repo_id ?? null;
|
|
4003
4003
|
}
|
|
4004
4004
|
if (!repoId && input.address) {
|
|
@@ -4007,7 +4007,7 @@ export class Gitvault {
|
|
|
4007
4007
|
// here for the same reason the open did, and that is the answer: no
|
|
4008
4008
|
// local identity, nothing to serve from.
|
|
4009
4009
|
try {
|
|
4010
|
-
const resolution = await
|
|
4010
|
+
const resolution = await resolveVaultAddress({ keystore, transport: createVaultHttpTransport(this.#client), address: input.address, repo_dir: input.repo_dir, allow_create: false });
|
|
4011
4011
|
if (resolution.offline)
|
|
4012
4012
|
repoId = resolution.repo_id;
|
|
4013
4013
|
}
|
|
@@ -4018,13 +4018,13 @@ export class Gitvault {
|
|
|
4018
4018
|
if (!repoId)
|
|
4019
4019
|
return null;
|
|
4020
4020
|
try {
|
|
4021
|
-
return await
|
|
4021
|
+
return await tryVaultDegradedRead({ attemptLive: () => Promise.reject(input.error), keystore, repo_id: repoId, out_dir: input.repo_dir });
|
|
4022
4022
|
}
|
|
4023
4023
|
catch {
|
|
4024
4024
|
return null;
|
|
4025
4025
|
}
|
|
4026
4026
|
}
|
|
4027
|
-
// ── mirror (
|
|
4027
|
+
// ── mirror (vault-mirror-and-recover, design D1/D2/D7) ─────────────────
|
|
4028
4028
|
/**
|
|
4029
4029
|
* Configure (or replace) the customer-owned mirror destination for one
|
|
4030
4030
|
* vault. Client-side ONLY — the destination + credential NAME are written
|
|
@@ -4033,17 +4033,17 @@ export class Gitvault {
|
|
|
4033
4033
|
* filesystem path needs none.
|
|
4034
4034
|
*/
|
|
4035
4035
|
async mirrorSet(options) {
|
|
4036
|
-
const [{
|
|
4036
|
+
const [{ VaultKeystore }, { parseMirrorDestinationUrl, saveMirrorConfig }] = await Promise.all([this.#keystore(), this.#mirrorConfig()]);
|
|
4037
4037
|
const repoId = await this.#resolveRepoId(options);
|
|
4038
|
-
const keystore = new
|
|
4038
|
+
const keystore = new VaultKeystore(options.keystore_root !== undefined ? { rootDir: options.keystore_root } : {});
|
|
4039
4039
|
const destination = parseMirrorDestinationUrl(options.destination_url, { region: options.region, endpoint: options.endpoint });
|
|
4040
4040
|
return saveMirrorConfig(keystore, { repo_id: repoId, destination, ...(options.credential ? { credential: options.credential } : {}) });
|
|
4041
4041
|
}
|
|
4042
4042
|
/** Remove the mirror config for one vault. Never touches the mirror's own bytes (design: config removal ≠ data deletion). */
|
|
4043
4043
|
async mirrorRemove(options = {}) {
|
|
4044
|
-
const [{
|
|
4044
|
+
const [{ VaultKeystore }, { removeMirrorConfig }] = await Promise.all([this.#keystore(), this.#mirrorConfig()]);
|
|
4045
4045
|
const repoId = await this.#resolveRepoId(options);
|
|
4046
|
-
const keystore = new
|
|
4046
|
+
const keystore = new VaultKeystore(options.keystore_root !== undefined ? { rootDir: options.keystore_root } : {});
|
|
4047
4047
|
return { repo_id: repoId, ...removeMirrorConfig(keystore, repoId) };
|
|
4048
4048
|
}
|
|
4049
4049
|
/**
|
|
@@ -4054,31 +4054,31 @@ export class Gitvault {
|
|
|
4054
4054
|
* command. Both honesty statements (design D8) ride every response.
|
|
4055
4055
|
*/
|
|
4056
4056
|
async mirrorStatus(options = {}) {
|
|
4057
|
-
const [{
|
|
4057
|
+
const [{ VaultKeystore }, { readMirrorConfig, formatMirrorDestination }, { openVaultMirrorBackend }, { verifyVaultMirror }] = await Promise.all([
|
|
4058
4058
|
this.#keystore(), this.#mirrorConfig(), this.#mirrorBackend(), this.#recovery(),
|
|
4059
4059
|
]);
|
|
4060
4060
|
const repoId = await this.#resolveRepoId(options);
|
|
4061
|
-
const keystore = new
|
|
4061
|
+
const keystore = new VaultKeystore(options.keystore_root !== undefined ? { rootDir: options.keystore_root } : {});
|
|
4062
4062
|
const config = readMirrorConfig(keystore, repoId);
|
|
4063
|
-
const base = { repo_id: repoId, validity_not_freshness:
|
|
4063
|
+
const base = { repo_id: repoId, validity_not_freshness: VAULT_MIRROR_VALIDITY_NOT_FRESHNESS_STATEMENT, keystore_still_required: VAULT_MIRROR_KEYSTORE_STILL_REQUIRED_STATEMENT };
|
|
4064
4064
|
const lastSuccessAt = config?.last_success_at ?? null;
|
|
4065
4065
|
if (!config) {
|
|
4066
|
-
//
|
|
4066
|
+
// vault-mirror-default: the unconfigured branch stays gateway-blind
|
|
4067
4067
|
// by construction — it returns before ANY network call, finding included.
|
|
4068
|
-
// `options.is_byo` is a caller-supplied hint (
|
|
4068
|
+
// `options.is_byo` is a caller-supplied hint (vault-byo-primary-bucket
|
|
4069
4069
|
// task 3.5) — NEVER derived here, which is what keeps this branch
|
|
4070
4070
|
// network-call-free; `repos view` already has `storage_profile` from
|
|
4071
4071
|
// its own `status()` read and passes it in.
|
|
4072
4072
|
return {
|
|
4073
4073
|
...base, configured: false, destination: null, credential_kind: null, mirrored_generation: null, newest_generation: null, is_current: null, closing_command: null,
|
|
4074
4074
|
last_success_at: lastSuccessAt,
|
|
4075
|
-
finding:
|
|
4075
|
+
finding: vaultUnmirroredFinding({ configured: false, last_success_at: lastSuccessAt, mirrored_generation: null, is_byo: options.is_byo }),
|
|
4076
4076
|
};
|
|
4077
4077
|
}
|
|
4078
4078
|
let mirroredGeneration = null;
|
|
4079
4079
|
try {
|
|
4080
|
-
const backend =
|
|
4081
|
-
const report = await
|
|
4080
|
+
const backend = openVaultMirrorBackend(config.destination, repoId, config.credential);
|
|
4081
|
+
const report = await verifyVaultMirror(backend, { keystore });
|
|
4082
4082
|
mirroredGeneration = report.recovered_generation;
|
|
4083
4083
|
}
|
|
4084
4084
|
catch {
|
|
@@ -4097,28 +4097,28 @@ export class Gitvault {
|
|
|
4097
4097
|
mirrored_generation: mirroredGeneration, newest_generation: newestGeneration, is_current: isCurrent,
|
|
4098
4098
|
closing_command: isCurrent === false ? "run402 repos mirror --backfill" : null,
|
|
4099
4099
|
last_success_at: lastSuccessAt,
|
|
4100
|
-
finding:
|
|
4100
|
+
finding: vaultUnmirroredFinding({ configured: true, last_success_at: lastSuccessAt, mirrored_generation: mirroredGeneration, is_byo: options.is_byo }),
|
|
4101
4101
|
};
|
|
4102
4102
|
}
|
|
4103
4103
|
/** List the vault's stored objects, diff against the mirror, fetch + hash-verify + write what's missing, in admission order. Resumable and idempotent. */
|
|
4104
4104
|
async mirrorSync(options = {}) {
|
|
4105
|
-
const [{
|
|
4105
|
+
const [{ VaultKeystore }, { mirrorSync }] = await Promise.all([this.#keystore(), this.#mirror()]);
|
|
4106
4106
|
const repoId = await this.#resolveRepoId(options);
|
|
4107
|
-
const keystore = new
|
|
4107
|
+
const keystore = new VaultKeystore(options.keystore_root !== undefined ? { rootDir: options.keystore_root } : {});
|
|
4108
4108
|
return mirrorSync(this.#client, repoId, { keystore });
|
|
4109
4109
|
}
|
|
4110
4110
|
/** Keyless integrity probe against the CONFIGURED mirror: discovery + chain verification + closure/absence adjudication, never decryption (`run402 repos fsck --mirror`). */
|
|
4111
4111
|
async mirrorVerify(options = {}) {
|
|
4112
|
-
const [{
|
|
4112
|
+
const [{ VaultKeystore }, { readMirrorConfig }, { openVaultMirrorBackend }, { verifyVaultMirror }] = await Promise.all([
|
|
4113
4113
|
this.#keystore(), this.#mirrorConfig(), this.#mirrorBackend(), this.#recovery(),
|
|
4114
4114
|
]);
|
|
4115
4115
|
const repoId = await this.#resolveRepoId(options);
|
|
4116
|
-
const keystore = new
|
|
4116
|
+
const keystore = new VaultKeystore(options.keystore_root !== undefined ? { rootDir: options.keystore_root } : {});
|
|
4117
4117
|
const config = readMirrorConfig(keystore, repoId);
|
|
4118
4118
|
if (!config)
|
|
4119
|
-
throw new LocalError(`no mirror is configured for ${repoId}`, "verifying
|
|
4120
|
-
const backend =
|
|
4121
|
-
return
|
|
4119
|
+
throw new LocalError(`no mirror is configured for ${repoId}`, "verifying vault mirror", { code: "VAULT_MIRROR_NOT_CONFIGURED", details: { repo_id: repoId }, next_actions: [{ action: "run402 repos mirror <destination>" }] });
|
|
4120
|
+
const backend = openVaultMirrorBackend(config.destination, repoId, config.credential);
|
|
4121
|
+
return verifyVaultMirror(backend, { keystore });
|
|
4122
4122
|
}
|
|
4123
4123
|
/**
|
|
4124
4124
|
* `r402s-recover` (design D4): rebuild a working git repository straight
|
|
@@ -4127,7 +4127,7 @@ export class Gitvault {
|
|
|
4127
4127
|
* than one mirrored vault, pass `repo_id` explicitly. Recovery proves
|
|
4128
4128
|
* validity, never freshness (both honesty statements ride every result).
|
|
4129
4129
|
*
|
|
4130
|
-
*
|
|
4130
|
+
* vault-recovery-custody: a human member with no keystore recovers with
|
|
4131
4131
|
* `member_bundle` (the exported `r402s-member-recovery-bundle/v1`, or the
|
|
4132
4132
|
* mirror's own `member-recovery-bundles/` sidecar when omitted) +
|
|
4133
4133
|
* `source_recovery_code`; a raw WebAuthn PRF output is NOT a supported
|
|
@@ -4135,22 +4135,22 @@ export class Gitvault {
|
|
|
4135
4135
|
* pass `recovery_receipt` when there is no keystore holding one.
|
|
4136
4136
|
*/
|
|
4137
4137
|
async recover(options) {
|
|
4138
|
-
const [{
|
|
4138
|
+
const [{ VaultKeystore }, { parseMirrorDestinationUrl }, { openVaultMirrorBackend, discoverMirroredRepoIds }, { recoverVaultMirror }] = await Promise.all([
|
|
4139
4139
|
this.#keystore(), this.#mirrorConfig(), this.#mirrorBackend(), this.#recovery(),
|
|
4140
4140
|
]);
|
|
4141
|
-
const keystore = new
|
|
4141
|
+
const keystore = new VaultKeystore(options.keystore_root !== undefined ? { rootDir: options.keystore_root } : {});
|
|
4142
4142
|
const destination = parseMirrorDestinationUrl(options.source, { region: options.region, endpoint: options.endpoint });
|
|
4143
4143
|
let repoId = options.repo_id;
|
|
4144
4144
|
if (!repoId) {
|
|
4145
4145
|
const found = await discoverMirroredRepoIds(destination, options.credential);
|
|
4146
4146
|
if (found.length === 0)
|
|
4147
|
-
throw new LocalError(`${options.source} holds no mirrored vault (no source/<repo_id>/ prefix found)`, "recovering
|
|
4147
|
+
throw new LocalError(`${options.source} holds no mirrored vault (no source/<repo_id>/ prefix found)`, "recovering vault", { code: "VAULT_MIRROR_EMPTY", details: { source: options.source } });
|
|
4148
4148
|
if (found.length > 1)
|
|
4149
|
-
throw new LocalError(`${options.source} holds ${found.length} mirrored vaults; pass repo_id to pick one`, "recovering
|
|
4149
|
+
throw new LocalError(`${options.source} holds ${found.length} mirrored vaults; pass repo_id to pick one`, "recovering vault", { code: "VAULT_MIRROR_AMBIGUOUS", details: { source: options.source, repo_ids: found }, next_actions: found.map((id) => ({ action: `recover --repo ${id}` })) });
|
|
4150
4150
|
repoId = found[0];
|
|
4151
4151
|
}
|
|
4152
|
-
const backend =
|
|
4153
|
-
return
|
|
4152
|
+
const backend = openVaultMirrorBackend(destination, repoId, options.credential);
|
|
4153
|
+
return recoverVaultMirror({
|
|
4154
4154
|
backend, out_dir: options.out_dir, keystore,
|
|
4155
4155
|
...(options.recovery_receipt ? { recovery_receipt: options.recovery_receipt } : {}),
|
|
4156
4156
|
...(options.member_bundle ? { member_bundle: options.member_bundle } : {}),
|
|
@@ -4167,9 +4167,9 @@ export class Gitvault {
|
|
|
4167
4167
|
* with no cache directory at all (a no-op).
|
|
4168
4168
|
*/
|
|
4169
4169
|
async sweepObjectCache(options) {
|
|
4170
|
-
const [{
|
|
4170
|
+
const [{ VaultKeystore }] = await Promise.all([this.#keystore()]);
|
|
4171
4171
|
const repoId = await this.#resolveRepoId(options);
|
|
4172
|
-
const keystore = new
|
|
4172
|
+
const keystore = new VaultKeystore(options.keystore_root !== undefined ? { rootDir: options.keystore_root } : {});
|
|
4173
4173
|
keystore.sweepObjectCache(repoId);
|
|
4174
4174
|
}
|
|
4175
4175
|
// ── internals ─────────────────────────────────────────────────────────────
|
|
@@ -4178,90 +4178,90 @@ export class Gitvault {
|
|
|
4178
4178
|
return options.repo_id;
|
|
4179
4179
|
if (options.project_id)
|
|
4180
4180
|
return (await this.forProject(options.project_id)).repo_id;
|
|
4181
|
-
throw new LocalError("pass repo_id, or project_id to resolve it from the control plane", "opening the
|
|
4181
|
+
throw new LocalError("pass repo_id, or project_id to resolve it from the control plane", "opening the vault", { code: "VAULT_UNRESOLVED" });
|
|
4182
4182
|
}
|
|
4183
4183
|
#publication() {
|
|
4184
|
-
return nodeOnly(() => import("../node/
|
|
4184
|
+
return nodeOnly(() => import("../node/vault-publication.js"), "open");
|
|
4185
4185
|
}
|
|
4186
4186
|
#deploy() {
|
|
4187
|
-
return nodeOnly(() => import("../node/
|
|
4187
|
+
return nodeOnly(() => import("../node/vault-deploy.js"), "deploy");
|
|
4188
4188
|
}
|
|
4189
4189
|
#keystore() {
|
|
4190
|
-
return nodeOnly(() => import("../node/
|
|
4190
|
+
return nodeOnly(() => import("../node/vault-keystore.js"), "open");
|
|
4191
4191
|
}
|
|
4192
4192
|
#creation() {
|
|
4193
|
-
return nodeOnly(() => import("../node/
|
|
4193
|
+
return nodeOnly(() => import("../node/vault-creation-journal.js"), "init");
|
|
4194
4194
|
}
|
|
4195
4195
|
#snapshot() {
|
|
4196
|
-
return nodeOnly(() => import("../node/
|
|
4196
|
+
return nodeOnly(() => import("../node/vault-snapshot.js"), "push");
|
|
4197
4197
|
}
|
|
4198
4198
|
#prune() {
|
|
4199
|
-
return nodeOnly(() => import("../node/
|
|
4199
|
+
return nodeOnly(() => import("../node/vault-prune.js"), "prune");
|
|
4200
4200
|
}
|
|
4201
4201
|
#openOrCreate() {
|
|
4202
|
-
return nodeOnly(() => import("../node/
|
|
4202
|
+
return nodeOnly(() => import("../node/vault-open-or-create.js"), "openOrCreate");
|
|
4203
4203
|
}
|
|
4204
4204
|
#address() {
|
|
4205
|
-
return nodeOnly(() => import("../node/
|
|
4205
|
+
return nodeOnly(() => import("../node/vault-address.js"), "resolveOrCreateAddress");
|
|
4206
4206
|
}
|
|
4207
4207
|
#mirror() {
|
|
4208
|
-
return nodeOnly(() => import("../node/
|
|
4208
|
+
return nodeOnly(() => import("../node/vault-mirror.js"), "mirrorSync");
|
|
4209
4209
|
}
|
|
4210
4210
|
#mirrorConfig() {
|
|
4211
|
-
return nodeOnly(() => import("../node/
|
|
4211
|
+
return nodeOnly(() => import("../node/vault-mirror-config.js"), "mirrorSet");
|
|
4212
4212
|
}
|
|
4213
4213
|
#mirrorBackend() {
|
|
4214
|
-
return nodeOnly(() => import("../node/
|
|
4214
|
+
return nodeOnly(() => import("../node/vault-mirror-backend.js"), "mirrorSet");
|
|
4215
4215
|
}
|
|
4216
4216
|
#recovery() {
|
|
4217
|
-
return nodeOnly(() => import("../node/
|
|
4217
|
+
return nodeOnly(() => import("../node/vault-recover.js"), "recover");
|
|
4218
4218
|
}
|
|
4219
4219
|
#handoff() {
|
|
4220
|
-
return nodeOnly(() => import("../node/
|
|
4220
|
+
return nodeOnly(() => import("../node/vault-handoff.js"), "handoff");
|
|
4221
4221
|
}
|
|
4222
4222
|
#writerState() {
|
|
4223
|
-
return nodeOnly(() => import("../node/
|
|
4223
|
+
return nodeOnly(() => import("../node/vault-writer-state.js"), "handoff");
|
|
4224
4224
|
}
|
|
4225
4225
|
#restore() {
|
|
4226
|
-
return nodeOnly(() => import("../node/
|
|
4226
|
+
return nodeOnly(() => import("../node/vault-restore.js"), "resume");
|
|
4227
4227
|
}
|
|
4228
4228
|
#degradedRead() {
|
|
4229
|
-
return nodeOnly(() => import("../node/
|
|
4229
|
+
return nodeOnly(() => import("../node/vault-degraded-read.js"), "list");
|
|
4230
4230
|
}
|
|
4231
4231
|
#byoConfig() {
|
|
4232
|
-
return nodeOnly(() => import("../node/
|
|
4232
|
+
return nodeOnly(() => import("../node/vault-byo-config.js"), "init");
|
|
4233
4233
|
}
|
|
4234
4234
|
/**
|
|
4235
|
-
*
|
|
4235
|
+
* vault-byo-primary-bucket: the transport's `byoBackend` option — where
|
|
4236
4236
|
* THIS machine reads a BYO vault's payload from (its `byo/<repo_id>.json`
|
|
4237
4237
|
* config; destination + credential NAME). One backend per vault per
|
|
4238
4238
|
* transport; `null` for a vault this machine has no config for, so the
|
|
4239
|
-
* transport refuses `
|
|
4239
|
+
* transport refuses `VAULT_BYO_NOT_CONFIGURED` by name rather than
|
|
4240
4240
|
* reporting a false absence. Managed vaults never reach it.
|
|
4241
4241
|
*/
|
|
4242
4242
|
async #byoBackendResolver(keystore) {
|
|
4243
|
-
const [{ readByoConfig }, {
|
|
4243
|
+
const [{ readByoConfig }, { openVaultDestinationBackend }] = await Promise.all([this.#byoConfig(), this.#mirrorBackend()]);
|
|
4244
4244
|
const cache = new Map();
|
|
4245
4245
|
return (repoId) => {
|
|
4246
4246
|
if (!cache.has(repoId)) {
|
|
4247
4247
|
// The RAW destination backend, keyed by the gateway's full
|
|
4248
4248
|
// `source/<repo_id>/…` key — the same pairing the write path uses
|
|
4249
|
-
// (`
|
|
4249
|
+
// (`openVaultDestinationBackend` + the session's `key`). The
|
|
4250
4250
|
// repo-scoped backend the mirror engine opens would prefix
|
|
4251
4251
|
// `source/<repo_id>/` a second time.
|
|
4252
4252
|
const config = readByoConfig(keystore, repoId);
|
|
4253
|
-
cache.set(repoId, config ?
|
|
4253
|
+
cache.set(repoId, config ? openVaultDestinationBackend(config.destination, config.credential) : null);
|
|
4254
4254
|
}
|
|
4255
4255
|
return cache.get(repoId);
|
|
4256
4256
|
};
|
|
4257
4257
|
}
|
|
4258
4258
|
#byoProbe() {
|
|
4259
|
-
return nodeOnly(() => import("../node/
|
|
4259
|
+
return nodeOnly(() => import("../node/vault-byo-probe.js"), "init");
|
|
4260
4260
|
}
|
|
4261
4261
|
}
|
|
4262
4262
|
// ─── D7 — progressive terminal-loss warning (repo-first-onramp task 2.7) ────
|
|
4263
4263
|
//
|
|
4264
|
-
// The §0 terminal-loss statement (`
|
|
4264
|
+
// The §0 terminal-loss statement (`VAULT_TERMINAL_LOSS_STATEMENT`) never
|
|
4265
4265
|
// changes — this governs only PROMINENCE and CADENCE. A quiet stderr line
|
|
4266
4266
|
// already runs at genesis (the one-shot receipt path in `push`/`openOrCreate`
|
|
4267
4267
|
// callers). This is the OTHER half: once the vault has accrued enough value
|
|
@@ -4274,17 +4274,17 @@ export class Gitvault {
|
|
|
4274
4274
|
* (an agent that pads one dimension to stay under a lone threshold). Shipped
|
|
4275
4275
|
* as defaults; tunable in this ONE place, not a protocol contract.
|
|
4276
4276
|
*/
|
|
4277
|
-
export const
|
|
4277
|
+
export const VAULT_LOSS_WARNING_THRESHOLDS = {
|
|
4278
4278
|
generations: 10,
|
|
4279
4279
|
source_bytes: 10 * 1024 * 1024,
|
|
4280
4280
|
days_since_genesis: 14,
|
|
4281
4281
|
};
|
|
4282
4282
|
/**
|
|
4283
|
-
*
|
|
4283
|
+
* vault-clone-scaling (bench P3): generations-since-checkpoint at or
|
|
4284
4284
|
* above this advises `run402 repos gc`. Same one-place-to-tune convention
|
|
4285
|
-
* as {@link
|
|
4285
|
+
* as {@link VAULT_LOSS_WARNING_THRESHOLDS}; an advisory, never a gate.
|
|
4286
4286
|
*/
|
|
4287
|
-
export const
|
|
4287
|
+
export const VAULT_CHECKPOINT_ADVISORY_GENERATIONS = 25;
|
|
4288
4288
|
/**
|
|
4289
4289
|
* Pure staleness computation over two 16-hex generations. TOTAL: garbage
|
|
4290
4290
|
* or unknown coverage (`null`) yields `{0, advised: false}` — advisory
|
|
@@ -4294,7 +4294,7 @@ export const GITVAULT_CHECKPOINT_ADVISORY_GENERATIONS = 25;
|
|
|
4294
4294
|
* `checkpoint_covers_through` doc), which is deliberately indistinguishable
|
|
4295
4295
|
* from fresh here.
|
|
4296
4296
|
*/
|
|
4297
|
-
export function
|
|
4297
|
+
export function vaultCheckpointStaleness(input) {
|
|
4298
4298
|
try {
|
|
4299
4299
|
if (input.covers_through_generation === null)
|
|
4300
4300
|
return { generations_since_checkpoint: 0, advised: false };
|
|
@@ -4304,7 +4304,7 @@ export function gitvaultCheckpointStaleness(input) {
|
|
|
4304
4304
|
const newest = BigInt(`0x${input.newest_generation}`);
|
|
4305
4305
|
const covered = BigInt(`0x${input.covers_through_generation}`);
|
|
4306
4306
|
const since = newest > covered ? Number(newest - covered) : 0;
|
|
4307
|
-
return { generations_since_checkpoint: since, advised: since >=
|
|
4307
|
+
return { generations_since_checkpoint: since, advised: since >= VAULT_CHECKPOINT_ADVISORY_GENERATIONS };
|
|
4308
4308
|
}
|
|
4309
4309
|
catch {
|
|
4310
4310
|
return { generations_since_checkpoint: 0, advised: false };
|
|
@@ -4324,34 +4324,34 @@ export function gitvaultCheckpointStaleness(input) {
|
|
|
4324
4324
|
* it, ever, in V0. What DOES now exist, one call site up in `status()`, is a
|
|
4325
4325
|
* way to downgrade the COPY once tripped: a locally-provable read of the
|
|
4326
4326
|
* vault's covering-recipient count (the same envelope-recipients read
|
|
4327
|
-
* `
|
|
4327
|
+
* `Repos.access` uses) can show this is no longer the single-principal
|
|
4328
4328
|
* case the message below describes. See
|
|
4329
|
-
* {@link
|
|
4329
|
+
* {@link vaultKeystoreBackupReminderMessage} for that downgraded form.
|
|
4330
4330
|
*/
|
|
4331
|
-
export function
|
|
4331
|
+
export function vaultLossWarningTrip(record, now = new Date()) {
|
|
4332
4332
|
const generations = Number(record.admitted_generations ?? "0");
|
|
4333
4333
|
const sourceBytes = Number(record.storage?.source_bytes ?? "0");
|
|
4334
4334
|
const genesisAt = record.genesis_admitted_at ? new Date(record.genesis_admitted_at) : null;
|
|
4335
4335
|
const daysSinceGenesis = genesisAt && !Number.isNaN(genesisAt.getTime()) ? (now.getTime() - genesisAt.getTime()) / 86_400_000 : null;
|
|
4336
4336
|
return {
|
|
4337
|
-
generations: Number.isFinite(generations) && generations >=
|
|
4338
|
-
source_bytes: Number.isFinite(sourceBytes) && sourceBytes >=
|
|
4339
|
-
days_since_genesis: daysSinceGenesis !== null && daysSinceGenesis >=
|
|
4337
|
+
generations: Number.isFinite(generations) && generations >= VAULT_LOSS_WARNING_THRESHOLDS.generations,
|
|
4338
|
+
source_bytes: Number.isFinite(sourceBytes) && sourceBytes >= VAULT_LOSS_WARNING_THRESHOLDS.source_bytes,
|
|
4339
|
+
days_since_genesis: daysSinceGenesis !== null && daysSinceGenesis >= VAULT_LOSS_WARNING_THRESHOLDS.days_since_genesis,
|
|
4340
4340
|
};
|
|
4341
4341
|
}
|
|
4342
4342
|
/** Any composite metric tripped — the standing warning threshold itself. */
|
|
4343
|
-
export function
|
|
4343
|
+
export function vaultLossWarningTripped(trip) {
|
|
4344
4344
|
return trip.generations || trip.source_bytes || trip.days_since_genesis;
|
|
4345
4345
|
}
|
|
4346
4346
|
/** Shared by both D7 message forms below — which threshold(s) tripped, in prose. */
|
|
4347
|
-
function
|
|
4347
|
+
function vaultLossWarningReasons(trip) {
|
|
4348
4348
|
const reasons = [];
|
|
4349
4349
|
if (trip.generations)
|
|
4350
|
-
reasons.push(`≥${
|
|
4350
|
+
reasons.push(`≥${VAULT_LOSS_WARNING_THRESHOLDS.generations} generations`);
|
|
4351
4351
|
if (trip.source_bytes)
|
|
4352
|
-
reasons.push(`≥${Math.round(
|
|
4352
|
+
reasons.push(`≥${Math.round(VAULT_LOSS_WARNING_THRESHOLDS.source_bytes / (1024 * 1024))} MB of source`);
|
|
4353
4353
|
if (trip.days_since_genesis)
|
|
4354
|
-
reasons.push(`≥${
|
|
4354
|
+
reasons.push(`≥${VAULT_LOSS_WARNING_THRESHOLDS.days_since_genesis} days since genesis`);
|
|
4355
4355
|
return reasons;
|
|
4356
4356
|
}
|
|
4357
4357
|
/**
|
|
@@ -4362,10 +4362,10 @@ function gitvaultLossWarningReasons(trip) {
|
|
|
4362
4362
|
* This is the SINGLE-PRINCIPAL form — use it only when the vault's
|
|
4363
4363
|
* covering-recipient count is unknown or <= 1. Once a caller has locally
|
|
4364
4364
|
* proven >= 2 covering recipients, its premise ("only one principal can open
|
|
4365
|
-
* it") is false; use {@link
|
|
4365
|
+
* it") is false; use {@link vaultKeystoreBackupReminderMessage} instead.
|
|
4366
4366
|
*/
|
|
4367
|
-
export function
|
|
4368
|
-
const reasons =
|
|
4367
|
+
export function vaultLossWarningMessage(trip) {
|
|
4368
|
+
const reasons = vaultLossWarningReasons(trip);
|
|
4369
4369
|
return (`this vault has accrued real value at risk (${reasons.join(", ")}) while only one principal can open it. ` +
|
|
4370
4370
|
"Only a second principal — another keystore, or later a human envelope — demonstrably able to open the vault clears this warning; " +
|
|
4371
4371
|
"this client cannot verify that yet, so it stands until you add one. No attestation or flag clears it.");
|
|
@@ -4373,22 +4373,22 @@ export function gitvaultLossWarningMessage(trip) {
|
|
|
4373
4373
|
/**
|
|
4374
4374
|
* D7's downgraded form (dogfood item 2): once `status()` has locally proven
|
|
4375
4375
|
* this vault carries >= 2 covering recipients (via the same envelope-recipients
|
|
4376
|
-
* read `
|
|
4377
|
-
* {@link
|
|
4376
|
+
* read `Repos.access` uses), the single-principal premise behind
|
|
4377
|
+
* {@link vaultLossWarningMessage} is false for THIS vault — printing it
|
|
4378
4378
|
* anyway would be a false terminal-loss claim. The composite threshold still
|
|
4379
|
-
* fires the SAME way (this function does not touch `
|
|
4380
|
-
* `
|
|
4379
|
+
* fires the SAME way (this function does not touch `vaultLossWarningTrip`/
|
|
4380
|
+
* `vaultLossWarningTripped` at all — a real value-at-risk signal is still
|
|
4381
4381
|
* worth a reminder), but the copy switches to the protocol's own
|
|
4382
4382
|
* keystore-qualified durability sentence instead of asserting single-principal
|
|
4383
4383
|
* risk, and never claims terminal loss.
|
|
4384
4384
|
*/
|
|
4385
|
-
export function
|
|
4386
|
-
const reasons =
|
|
4385
|
+
export function vaultKeystoreBackupReminderMessage(trip, coveringRecipients) {
|
|
4386
|
+
const reasons = vaultLossWarningReasons(trip);
|
|
4387
4387
|
return (`this vault has accrued real value at risk (${reasons.join(", ")}), but it is covered by ${coveringRecipients} recipients today — not the single-principal case. ` +
|
|
4388
|
-
`${
|
|
4388
|
+
`${VAULT_DURABILITY_STATEMENT} Back up this machine's keystore anyway: losing it does not lose the vault, but it does lose YOUR access to it.`);
|
|
4389
4389
|
}
|
|
4390
4390
|
/**
|
|
4391
|
-
*
|
|
4391
|
+
* vault-mirror-default — the pure `vault_unmirrored` computation, in ONE
|
|
4392
4392
|
* place so doctor and `repos view` echo the same finding instead of each
|
|
4393
4393
|
* deriving their own (the loss-warning pattern above). Present when no mirror
|
|
4394
4394
|
* is configured, OR when one is configured but has no success evidence yet —
|
|
@@ -4398,68 +4398,60 @@ export function gitvaultKeystoreBackupReminderMessage(trip, coveringRecipients)
|
|
|
4398
4398
|
* never blocking; every input is client-local or read from the CUSTOMER'S
|
|
4399
4399
|
* mirror — nothing here touches the gateway.
|
|
4400
4400
|
*/
|
|
4401
|
-
export function
|
|
4401
|
+
export function vaultUnmirroredFinding(state) {
|
|
4402
4402
|
if (state.configured && (state.last_success_at !== null || state.mirrored_generation !== null))
|
|
4403
4403
|
return null;
|
|
4404
4404
|
return {
|
|
4405
4405
|
kind: "vault_unmirrored",
|
|
4406
|
-
message: state.is_byo ?
|
|
4406
|
+
message: state.is_byo ? VAULT_BYO_UNMIRRORED_REMEDY_STATEMENT : VAULT_UNMIRRORED_FINDING_STATEMENT,
|
|
4407
4407
|
setup_command: state.configured ? "run402 repos mirror --backfill" : "run402 repos mirror <destination>",
|
|
4408
4408
|
};
|
|
4409
4409
|
}
|
|
4410
4410
|
/**
|
|
4411
|
-
*
|
|
4411
|
+
* vault-byo-primary-bucket (design D4) — the ONE stderr line a degraded
|
|
4412
4412
|
* chain/payload read prints: the fallback's own destination (never a
|
|
4413
4413
|
* credential) plus the canonical, mechanism-only statement. `list`/`fetch`
|
|
4414
4414
|
* degrading in the SAME `git-remote-run402` session each print their own
|
|
4415
4415
|
* line (one per degraded READ, not one per session) — see
|
|
4416
|
-
* `
|
|
4416
|
+
* `Repos.withDegradedRead`'s own doc comment.
|
|
4417
4417
|
*/
|
|
4418
|
-
export function
|
|
4419
|
-
return `degraded read from ${source.destination}: ${
|
|
4418
|
+
export function vaultDegradedReadNote(source) {
|
|
4419
|
+
return `degraded read from ${source.destination}: ${VAULT_DEGRADED_READ_STATEMENT}`;
|
|
4420
4420
|
}
|
|
4421
|
-
/**
|
|
4422
|
-
|
|
4423
|
-
|
|
4424
|
-
* `@kychee/kygit` shim renders once it sets `RUN402_REMOTE_SCHEME=kygit`
|
|
4425
|
-
* before exec). The gateway never sees this — `address` and every registry
|
|
4426
|
-
* `next_actions` command stay `run402::`; only client-side RENDERING reads
|
|
4427
|
-
* it. Any other value falls back to `"run402"` rather than emitting an
|
|
4428
|
-
* unparseable scheme.
|
|
4429
|
-
*/
|
|
4430
|
-
export function gitvaultRemoteScheme() {
|
|
4431
|
-
return typeof process !== "undefined" && process.env?.RUN402_REMOTE_SCHEME === "kygit" ? "kygit" : "run402";
|
|
4432
|
-
}
|
|
4433
|
-
/** `<door>::<org_id>/<project_id>` — what `git-remote-run402`/`git-remote-kygit` resolves. */
|
|
4434
|
-
export function gitvaultRemoteUrl(orgId, projectId) {
|
|
4435
|
-
return `${gitvaultRemoteScheme()}::${orgId}/${projectId}`;
|
|
4421
|
+
/** `run402::<org_id>/<project_id>` — what `git-remote-run402` resolves. */
|
|
4422
|
+
export function vaultRemoteUrl(orgId, projectId) {
|
|
4423
|
+
return `run402::${orgId}/${projectId}`;
|
|
4436
4424
|
}
|
|
4437
4425
|
/**
|
|
4438
|
-
*
|
|
4426
|
+
* `run402::<org-slug>/<repo-name>` — the address-form remote builder
|
|
4439
4427
|
* (repo-first-onramp task 4, design D6). Same string shape as
|
|
4440
|
-
* {@link
|
|
4441
|
-
* — see {@link
|
|
4428
|
+
* {@link vaultRemoteUrl} (the wire slot admits both forms undiscriminated
|
|
4429
|
+
* — see {@link vaultRemoteAddressForm}); kept as its own named function so
|
|
4442
4430
|
* a call site states which form it means rather than reusing the id-form
|
|
4443
|
-
* builder for a semantically different pair of arguments.
|
|
4444
|
-
* {@link gitvaultRemoteScheme} (kygit-handoff design D8) — `run402 repos
|
|
4445
|
-
* create` renders `run402::`, `kygit create` renders `kygit::`.
|
|
4431
|
+
* builder for a semantically different pair of arguments.
|
|
4446
4432
|
*/
|
|
4447
|
-
export function
|
|
4448
|
-
return
|
|
4433
|
+
export function vaultRemoteUrlForRepo(orgSlug, repoName) {
|
|
4434
|
+
return `run402::${orgSlug}/${repoName}`;
|
|
4449
4435
|
}
|
|
4450
4436
|
/**
|
|
4451
|
-
* Parse a `run402::<org>/<project>`
|
|
4452
|
-
*
|
|
4453
|
-
*
|
|
4437
|
+
* Parse a `run402::<org>/<project>` remote URL into its two address halves.
|
|
4438
|
+
* `run402::` is the one remote scheme: `null` for anything that is not a
|
|
4439
|
+
* `<scheme>::` address at all, and a typed `REMOTE_SCHEME_UNSUPPORTED`
|
|
4440
|
+
* refusal naming the scheme for any other `<scheme>::` spelling.
|
|
4454
4441
|
*/
|
|
4455
|
-
export function
|
|
4456
|
-
const
|
|
4457
|
-
|
|
4458
|
-
|
|
4459
|
-
|
|
4442
|
+
export function parseVaultRemoteUrl(url) {
|
|
4443
|
+
const trimmed = url.trim();
|
|
4444
|
+
const m = /^run402::([^/]+)\/(.+)$/.exec(trimmed);
|
|
4445
|
+
if (m)
|
|
4446
|
+
return { org_id: m[1], project_id: m[2] };
|
|
4447
|
+
const other = /^([A-Za-z][A-Za-z0-9+.-]*)::/.exec(trimmed);
|
|
4448
|
+
if (other && other[1] !== "run402") {
|
|
4449
|
+
throw new LocalError(`remote scheme '${other[1]}::' is not supported; the one vault remote scheme is run402::<org>/<name>`, "parsing a vault remote address", { code: "REMOTE_SCHEME_UNSUPPORTED", details: { scheme: other[1] } });
|
|
4450
|
+
}
|
|
4451
|
+
return null;
|
|
4460
4452
|
}
|
|
4461
|
-
const
|
|
4462
|
-
const
|
|
4453
|
+
const VAULT_UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
4454
|
+
const VAULT_PROJECT_ID_RE = /^prj_/;
|
|
4463
4455
|
/**
|
|
4464
4456
|
* Discriminate a parsed address's form (design D6, "resolved"): "org UUIDs
|
|
4465
4457
|
* and `prj_` prefixes make slug-vs-id in the same slot unambiguous." Both
|
|
@@ -4469,8 +4461,8 @@ const GITVAULT_PROJECT_ID_RE = /^prj_/;
|
|
|
4469
4461
|
* `prj_`-prefixed; anything else (an org slug half, or a bare repo-name half)
|
|
4470
4462
|
* is slug-form. Resolution accepts either (task 4.3).
|
|
4471
4463
|
*/
|
|
4472
|
-
export function
|
|
4473
|
-
return
|
|
4464
|
+
export function vaultRemoteAddressForm(address) {
|
|
4465
|
+
return VAULT_UUID_RE.test(address.org_id) && VAULT_PROJECT_ID_RE.test(address.project_id) ? "id" : "slug";
|
|
4474
4466
|
}
|
|
4475
4467
|
/**
|
|
4476
4468
|
* `SLUG_RELEASED` (a renamed/deleted org slug, still inside its ~90-day
|
|
@@ -4478,7 +4470,7 @@ export function gitvaultRemoteAddressForm(address) {
|
|
|
4478
4470
|
* (design D6: "no redirects, deliberately"). `null` when `err` is not a
|
|
4479
4471
|
* `SLUG_RELEASED` refusal.
|
|
4480
4472
|
*/
|
|
4481
|
-
export function
|
|
4473
|
+
export function vaultSlugReleasedInfo(err) {
|
|
4482
4474
|
const e = err;
|
|
4483
4475
|
if (!e || e.code !== "SLUG_RELEASED")
|
|
4484
4476
|
return null;
|
|
@@ -4495,11 +4487,11 @@ function isMissingCutoffRoute(e) {
|
|
|
4495
4487
|
* "No vault exists for this address/project yet" — the same 404 shape as
|
|
4496
4488
|
* {@link isMissingCutoffRoute}, named separately because the two mean
|
|
4497
4489
|
* different things to their callers (an absent OPTIONAL route vs. an absent
|
|
4498
|
-
* RESOURCE). Used by {@link
|
|
4490
|
+
* RESOURCE). Used by {@link Repos.planCapture} to distinguish "nothing to
|
|
4499
4491
|
* preview a push against" from a genuine failure worth rethrowing.
|
|
4500
4492
|
*/
|
|
4501
4493
|
function isVaultResolutionMiss(e) {
|
|
4502
4494
|
const err = e;
|
|
4503
4495
|
return Boolean(err && (err.status === 404 || err.code === "RESOURCE_NOT_FOUND" || err.code === "ROUTE_NOT_FOUND"));
|
|
4504
4496
|
}
|
|
4505
|
-
//# sourceMappingURL=
|
|
4497
|
+
//# sourceMappingURL=repos.js.map
|