run402 4.42.0 → 4.43.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/lib/command-manifest.mjs +6 -3
- package/lib/deploy-v2.mjs +27 -4
- package/lib/next-actions.mjs +5 -2
- package/lib/repos.mjs +466 -117
- package/lib/stats.mjs +57 -0
- package/package.json +1 -1
- package/sdk/dist/index.d.ts +10 -1
- package/sdk/dist/index.d.ts.map +1 -1
- package/sdk/dist/index.js +11 -0
- package/sdk/dist/index.js.map +1 -1
- package/sdk/dist/kernel.d.ts +31 -0
- package/sdk/dist/kernel.d.ts.map +1 -1
- package/sdk/dist/kernel.js +116 -3
- package/sdk/dist/kernel.js.map +1 -1
- package/sdk/dist/namespaces/gitvault.crypto.d.ts +178 -5
- package/sdk/dist/namespaces/gitvault.crypto.d.ts.map +1 -1
- package/sdk/dist/namespaces/gitvault.crypto.js +266 -7
- package/sdk/dist/namespaces/gitvault.crypto.js.map +1 -1
- package/sdk/dist/namespaces/gitvault.d.ts +103 -1
- package/sdk/dist/namespaces/gitvault.d.ts.map +1 -1
- package/sdk/dist/namespaces/gitvault.js +175 -7
- package/sdk/dist/namespaces/gitvault.js.map +1 -1
- package/sdk/dist/namespaces/gitvault.types.d.ts +184 -9
- package/sdk/dist/namespaces/gitvault.types.d.ts.map +1 -1
- package/sdk/dist/namespaces/gitvault.types.js +2 -1
- package/sdk/dist/namespaces/gitvault.types.js.map +1 -1
- package/sdk/dist/node/gitvault-apply.d.ts +8 -0
- package/sdk/dist/node/gitvault-apply.d.ts.map +1 -1
- package/sdk/dist/node/gitvault-apply.js +1 -0
- package/sdk/dist/node/gitvault-apply.js.map +1 -1
- package/sdk/dist/node/gitvault-deploy.d.ts +13 -0
- package/sdk/dist/node/gitvault-deploy.d.ts.map +1 -1
- package/sdk/dist/node/gitvault-deploy.js +28 -0
- package/sdk/dist/node/gitvault-deploy.js.map +1 -1
- package/sdk/dist/node/gitvault-keystore.d.ts +54 -4
- package/sdk/dist/node/gitvault-keystore.d.ts.map +1 -1
- package/sdk/dist/node/gitvault-keystore.js +20 -0
- package/sdk/dist/node/gitvault-keystore.js.map +1 -1
- package/sdk/dist/node/gitvault-publication.d.ts +328 -4
- package/sdk/dist/node/gitvault-publication.d.ts.map +1 -1
- package/sdk/dist/node/gitvault-publication.js +560 -14
- package/sdk/dist/node/gitvault-publication.js.map +1 -1
- package/sdk/dist/node/gitvault-snapshot.d.ts +40 -7
- package/sdk/dist/node/gitvault-snapshot.d.ts.map +1 -1
- package/sdk/dist/node/gitvault-snapshot.js +67 -19
- package/sdk/dist/node/gitvault-snapshot.js.map +1 -1
package/lib/repos.mjs
CHANGED
|
@@ -33,6 +33,7 @@ import { resolveGitvaultTarget } from "./gitvault-target.mjs";
|
|
|
33
33
|
import { nextAction, claimOrgSlugAction, claimRepoNameAction } from "./next-actions.mjs";
|
|
34
34
|
import { printKeystoreLocation } from "./gitvault.mjs";
|
|
35
35
|
import { gitvaultRemoteUrlForRepo } from "#sdk";
|
|
36
|
+
import { sdkStats, printVerboseStats } from "./stats.mjs";
|
|
36
37
|
import {
|
|
37
38
|
normalizeArgv,
|
|
38
39
|
hasHelp,
|
|
@@ -55,28 +56,34 @@ Usage:
|
|
|
55
56
|
Common:
|
|
56
57
|
run402 repos create [name] [--org <org_id>] [--dir <path>] [--tier <tier>] [--project <id>]
|
|
57
58
|
run402 repos view [--project <id>] [--repo <repo_id>] [--human]
|
|
58
|
-
run402 repos list [--org <org_id>]
|
|
59
|
+
run402 repos list [--org <org_id>] [--human]
|
|
59
60
|
|
|
60
61
|
Then plain git, forever:
|
|
61
62
|
git push
|
|
62
63
|
git clone run402::<org>/<repo>
|
|
63
64
|
|
|
64
65
|
Occasional:
|
|
65
|
-
run402 repos snapshot [--project <id>] [--repo <repo_id>] [--message <text>] [--checkpoint] [--dry-run]
|
|
66
|
+
run402 repos snapshot [--project <id>] [--repo <repo_id>] [--message <text>] [--checkpoint] [--dry-run] [--allow-dirty]
|
|
66
67
|
run402 repos mirror [<destination>] [--off] [--backfill] [--profile <name> | --ambient] [--region <r>] [--endpoint <url>] [--project <id>] [--repo <repo_id>]
|
|
67
|
-
run402 repos recover <source> --out <dir> [--repo <repo_id>] [--profile <name> | --ambient] [--region <r>] [--endpoint <url>]
|
|
68
|
+
run402 repos recover <source> --out <dir> [--repo <repo_id>] [--profile <name> | --ambient] [--region <r>] [--endpoint <url>] [--human]
|
|
68
69
|
|
|
69
70
|
Lifecycle:
|
|
70
71
|
run402 repos rename <new_name> [--repo <repo_id> | --project <project_id>]
|
|
71
72
|
run402 repos delete [--project <id>] [--repo <repo_id>] [--force]
|
|
72
73
|
|
|
73
74
|
Maintenance:
|
|
74
|
-
run402 repos fsck [--project <id>] [--repo <repo_id>] [--mirror] [--budget <n>] [--no-write]
|
|
75
|
+
run402 repos fsck [--project <id>] [--repo <repo_id>] [--mirror] [--budget <n>] [--no-write] [--human]
|
|
75
76
|
run402 repos gc [--project <id>] [--repo <repo_id>] [--submit --intent-core <path> --verifier-receipt <path> [--wait]]
|
|
76
|
-
run402 repos access [--project <id>] [--repo <repo_id>]
|
|
77
|
-
run402 repos access repair [--project <id>] [--repo <repo_id>]
|
|
77
|
+
run402 repos access [--project <id>] [--repo <repo_id>] [--human]
|
|
78
|
+
run402 repos access repair [--project <id>] [--repo <repo_id>] --recipient-state-version <n> --recipient-revocation-version <n>
|
|
79
|
+
run402 repos access revoke-key <principal_id> [--project <id>] [--repo <repo_id>]
|
|
80
|
+
run402 repos access declare-exposure [--project <id>] [--repo <repo_id>]
|
|
78
81
|
run402 repos policy <required|grandfathered> [--project <id>] [--repo <repo_id>] [--reason <why>]
|
|
79
82
|
|
|
83
|
+
Every verb above also accepts -v/--verbose (a stderr summary line of request
|
|
84
|
+
stats — round trips, wire time, bytes — coexists with --human) and always
|
|
85
|
+
carries a \`stats\` block in its JSON result.
|
|
86
|
+
|
|
80
87
|
Subcommands:
|
|
81
88
|
create Provision (or, with --project, ADOPT an existing project), ALLOCATE
|
|
82
89
|
its vault (mints key material and, on first allocation, a one-shot
|
|
@@ -101,7 +108,8 @@ Subcommands:
|
|
|
101
108
|
vaults-by-org read when the gateway has it (one round trip);
|
|
102
109
|
gracefully falls back to the older per-project walk when it
|
|
103
110
|
404s. Not every project in the org — ones with no vault are
|
|
104
|
-
omitted.
|
|
111
|
+
omitted. \`--human\` renders a compact roster (address,
|
|
112
|
+
generation, bytes, policy) instead of JSON.
|
|
105
113
|
rename Claim or rename the repo's per-org-unique, address-form name
|
|
106
114
|
(the <name> half of run402::<org-slug>/<name>). Address by
|
|
107
115
|
--repo or --project (not both).
|
|
@@ -120,6 +128,12 @@ Subcommands:
|
|
|
120
128
|
publishing. Push-to-creates through a slug-form remote
|
|
121
129
|
(run402::<org-slug>/<name>) the same way \`git push\` does.
|
|
122
130
|
\`--dry-run\` previews the real local pipeline without publishing.
|
|
131
|
+
A DIRTY tree (modified/staged tracked paths, or untracked-not-
|
|
132
|
+
ignored paths) REFUSES by default (SNAPSHOT_DIRTY_TREE, before any
|
|
133
|
+
object is created) — commit and retry, or pass \`--allow-dirty\` to
|
|
134
|
+
capture it as-is; the result then discloses exactly what was
|
|
135
|
+
swept in (modified_captured / untracked_captured), printed to
|
|
136
|
+
stderr too. \`--dry-run\` surfaces the same refusal.
|
|
123
137
|
mirror ONE flag-driven verb for the client-side, customer-
|
|
124
138
|
owned ciphertext mirror — run402 never holds a credential to it.
|
|
125
139
|
No argument: READ the configured destination + a keyless
|
|
@@ -136,7 +150,7 @@ Subcommands:
|
|
|
136
150
|
path (normal retrieval is plain \`git clone run402::<org>/<repo>\`,
|
|
137
151
|
no \`repos clone\` verb exists). Proves this mirror's validity,
|
|
138
152
|
never freshness — read both honesty statements before relying on
|
|
139
|
-
the result.
|
|
153
|
+
the result. \`--human\` renders a short summary instead of JSON.
|
|
140
154
|
fsck Walks the head chain AND materializes
|
|
141
155
|
the ref map, advancing BOTH
|
|
142
156
|
local trust pins — reported EXPLICITLY as local_state_changed +
|
|
@@ -147,6 +161,7 @@ Subcommands:
|
|
|
147
161
|
normal writing mode, so a budget-exceeded run resumes). \`--mirror\`
|
|
148
162
|
additionally runs the keyless mirror integrity probe — it proves
|
|
149
163
|
the mirror's VALIDITY, never its FRESHNESS, and says so.
|
|
164
|
+
\`--human\` renders a short summary instead of JSON.
|
|
150
165
|
gc \`git gc\`'s own two halves — checkpoint publication (compact) and
|
|
151
166
|
prune planning — in one verb, NOT described as "exactly git gc":
|
|
152
167
|
the deletion ceremony is stricter. Plans and checkpoints by
|
|
@@ -163,20 +178,37 @@ Subcommands:
|
|
|
163
178
|
pending_removal, from the gateway's desired-recipient-state
|
|
164
179
|
substrate), and (best-effort, this machine only) each principal's
|
|
165
180
|
local TOFU pin. stale_access names removed members whose access
|
|
166
|
-
|
|
167
|
-
not enforcement, until
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
rotation work, in fold under adversarial review.
|
|
181
|
+
has NOT yet been rotated away — pending_removal is honest
|
|
182
|
+
bookkeeping, not enforcement, until \`access repair\`/\`revoke-key\`
|
|
183
|
+
actually rotates. history_scope (which epochs each recipient can
|
|
184
|
+
read) is not reported by this read — see the \`gap\` field.
|
|
185
|
+
\`--human\` renders a compact roster instead of JSON (the read
|
|
186
|
+
form only — repair/revoke-key/declare-exposure stay JSON-only).
|
|
173
187
|
access repair
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
188
|
+
Epoch rotation (D193-D203, rev 42) with reason:"elective_rekey" —
|
|
189
|
+
re-keys this vault's CURRENT epoch away from every stale_access
|
|
190
|
+
principal at once, and clears a pre-existing vault's one-time
|
|
191
|
+
migration requirement. \`reconcile\`, the workaround this
|
|
192
|
+
replaces, is REMOVED (it never wrapped a key correctly-scoped to
|
|
193
|
+
"from here forward" — this does). Needs
|
|
194
|
+
--recipient-state-version and --recipient-revocation-version:
|
|
195
|
+
the gateway exposes no read route for these two counters outside
|
|
196
|
+
\`revoke-key\`'s own response, so this verb needs them supplied
|
|
197
|
+
explicitly today — refuses cleanly, naming exactly this, when
|
|
198
|
+
omitted. Owner + step-up.
|
|
199
|
+
access revoke-key <principal_id>
|
|
200
|
+
The ONE fully self-contained rotation entry point: declares
|
|
201
|
+
reason:"recipient_key_revoked" for one principal and rotates off
|
|
202
|
+
that declaration's OWN returned counters — no flags needed.
|
|
203
|
+
Owner + step-up. The rekey remedy for "this specific principal's
|
|
204
|
+
key should no longer be trusted."
|
|
205
|
+
access declare-exposure
|
|
206
|
+
Declares reason:"epoch_secret_exposed" for THIS vault
|
|
207
|
+
(vault-scoped, not org-wide) — the rekey remedy for a leaked
|
|
208
|
+
K_repo/K_e. The declaration itself lands immediately; the
|
|
209
|
+
follow-up rotation it authorizes is not auto-run (same counter
|
|
210
|
+
gap as \`access repair\`) — this prints exactly what to do next.
|
|
211
|
+
Owner + step-up.
|
|
180
212
|
policy Set the activation policy — \`required\` (a deploy must present a
|
|
181
213
|
vaulted capture) or \`grandfathered\` (it need not). Owner +
|
|
182
214
|
step-up, audited. \`grandfathered\` is the documented way out of a
|
|
@@ -195,9 +227,17 @@ Options:
|
|
|
195
227
|
--idempotency-key <key>
|
|
196
228
|
create: re-running with the same key resolves to the
|
|
197
229
|
same project instead of creating a second one — new
|
|
198
|
-
projects only (default: derived from the name)
|
|
199
|
-
|
|
200
|
-
|
|
230
|
+
projects only (default: derived from the name).
|
|
231
|
+
access repair/revoke-key: the rotation attempt's OWN
|
|
232
|
+
client_idempotency_key (32-hex) — default: a fresh
|
|
233
|
+
CSPRNG value each call, never resumed across processes.
|
|
234
|
+
--recipient-state-version <n>
|
|
235
|
+
--recipient-revocation-version <n>
|
|
236
|
+
access repair: the D194 frozen watermark pair this
|
|
237
|
+
rotation attempt is fenced against. Required — see
|
|
238
|
+
\`run402 repos access repair --help\` for why.
|
|
239
|
+
--human view/list/access/fsck/recover: a short summary on stdout
|
|
240
|
+
instead of the JSON dump. Rejected together with --json.
|
|
201
241
|
--force delete: proceed even though the repo holds generations
|
|
202
242
|
that would be permanently and irrecoverably lost. Never
|
|
203
243
|
overrides the non-repo-infrastructure refusal.
|
|
@@ -205,7 +245,13 @@ Options:
|
|
|
205
245
|
tree produces (a clean tree pushes HEAD itself, unused)
|
|
206
246
|
--checkpoint snapshot: force the checkpoint-bearing form regardless of delta size
|
|
207
247
|
--dry-run snapshot: a REAL preview — runs the actual local pipeline
|
|
208
|
-
and reports what would publish. Publishes nothing.
|
|
248
|
+
and reports what would publish. Publishes nothing. A
|
|
249
|
+
dirty tree still refuses SNAPSHOT_DIRTY_TREE here (a
|
|
250
|
+
preview that hid the refusal would lie).
|
|
251
|
+
--allow-dirty snapshot: capture a dirty tree as-is instead of refusing.
|
|
252
|
+
The result discloses exactly what was swept in
|
|
253
|
+
(modified_captured / untracked_captured) — even this
|
|
254
|
+
override never captures silently.
|
|
209
255
|
--off mirror: remove the configured destination (config only)
|
|
210
256
|
--backfill mirror: copy every object the configured mirror is missing
|
|
211
257
|
--profile <name> mirror / recover: the AWS credential profile name for an
|
|
@@ -234,6 +280,10 @@ Options:
|
|
|
234
280
|
signed completion appears, instead of returning immediately
|
|
235
281
|
--reason <why> policy: why the policy is changing — recorded in the
|
|
236
282
|
audit event. REQUIRED for \`grandfathered\`.
|
|
283
|
+
-v, --verbose Print one stderr summary line of this call's request
|
|
284
|
+
stats (round trips, wire time, bytes). Coexists with
|
|
285
|
+
--human. The JSON result always carries a \`stats\` block
|
|
286
|
+
regardless of this flag.
|
|
237
287
|
--json No-op: stdout is already JSON.
|
|
238
288
|
|
|
239
289
|
Terminal loss (protocol §0):
|
|
@@ -247,15 +297,16 @@ Examples:
|
|
|
247
297
|
run402 repos create --project prj_1a2b3c # allocate for an existing project
|
|
248
298
|
git push -u origin HEAD # the printed next_action, verbatim
|
|
249
299
|
run402 repos view --human
|
|
250
|
-
run402 repos list --org org_1a2b3c
|
|
300
|
+
run402 repos list --org org_1a2b3c --human
|
|
251
301
|
run402 repos rename my-notes --project prj_1a2b3c
|
|
252
302
|
run402 repos snapshot --dry-run
|
|
303
|
+
run402 repos snapshot --allow-dirty
|
|
253
304
|
run402 repos mirror s3://acme-vault-mirror --profile acme
|
|
254
305
|
run402 repos mirror --backfill
|
|
255
|
-
run402 repos fsck --mirror
|
|
306
|
+
run402 repos fsck --mirror --human
|
|
256
307
|
run402 repos gc
|
|
257
|
-
run402 repos access
|
|
258
|
-
run402 repos recover s3://acme-vault-mirror --out ./restored
|
|
308
|
+
run402 repos access --human
|
|
309
|
+
run402 repos recover s3://acme-vault-mirror --out ./restored --human
|
|
259
310
|
run402 repos delete --project prj_xyz --force
|
|
260
311
|
`;
|
|
261
312
|
|
|
@@ -305,6 +356,19 @@ function printTerminalLoss(status) {
|
|
|
305
356
|
console.error("");
|
|
306
357
|
}
|
|
307
358
|
|
|
359
|
+
/**
|
|
360
|
+
* Print a verb's JSON result with the always-on `stats` block (Observability:
|
|
361
|
+
* RUN402_TRACE + always-on stats + -v). `sdk.stats()` reflects only calls
|
|
362
|
+
* made through THIS `sdk` instance — every verb below resolves one `sdk =
|
|
363
|
+
* getSdk()` and reuses it for its own direct calls so the count is accurate
|
|
364
|
+
* for the work this function did; calls a shared cross-cutting helper
|
|
365
|
+
* (org/wallet resolution) makes through its own internal instance are not
|
|
366
|
+
* reflected (see `cli/lib/stats.mjs`'s doc comment).
|
|
367
|
+
*/
|
|
368
|
+
function printJson(sdk, payload) {
|
|
369
|
+
console.log(JSON.stringify({ ...payload, stats: sdkStats(sdk) }, null, 2));
|
|
370
|
+
}
|
|
371
|
+
|
|
308
372
|
const LARGE_OUTPUT_THRESHOLD_BYTES = 100 * 1024;
|
|
309
373
|
|
|
310
374
|
/**
|
|
@@ -507,17 +571,17 @@ async function inferRepoName(dir) {
|
|
|
507
571
|
|
|
508
572
|
const CREATE_VALUE_FLAGS = ["--org", "--dir", "--tier", "--idempotency-key", "--project"];
|
|
509
573
|
|
|
510
|
-
async function printCreateResult({ projectId, vault, adopted, name }) {
|
|
574
|
+
async function printCreateResult({ sdk, projectId, vault, adopted, name, verboseArgv }) {
|
|
511
575
|
let address = null;
|
|
512
576
|
let orgSlug = null;
|
|
513
577
|
try {
|
|
514
578
|
const owningOrg = await resolveOwningOrgId(projectId);
|
|
515
|
-
const orgRecord = owningOrg ? await
|
|
579
|
+
const orgRecord = owningOrg ? await sdk.org(owningOrg).get() : null;
|
|
516
580
|
orgSlug = orgRecord?.slug ?? null;
|
|
517
581
|
if (orgSlug && name) {
|
|
518
582
|
const candidate = slugifyRepoName(name);
|
|
519
583
|
if (candidate) {
|
|
520
|
-
const named = await
|
|
584
|
+
const named = await sdk.projects.setRepoName(projectId, candidate);
|
|
521
585
|
address = gitvaultRemoteUrlForRepo(orgSlug, named.repo_name);
|
|
522
586
|
}
|
|
523
587
|
}
|
|
@@ -546,13 +610,13 @@ async function printCreateResult({ projectId, vault, adopted, name }) {
|
|
|
546
610
|
deployed: false,
|
|
547
611
|
next_actions: nextActions,
|
|
548
612
|
};
|
|
549
|
-
|
|
613
|
+
printJson(sdk, out);
|
|
550
614
|
console.error(
|
|
551
615
|
`project ${projectId} ${adopted ? "adopted" : "provisioned"}; repo ${vault.repo_id} ` +
|
|
552
616
|
(vault.deduplicated ? "already existed — nothing was re-allocated" : `allocated (genesis ${vault.genesis_sha256})`),
|
|
553
617
|
);
|
|
554
618
|
if (address) console.error(`address: ${address}`);
|
|
555
|
-
else if (!orgSlug) console.error("no named address yet — claim an org slug (run402 org slug <slug
|
|
619
|
+
else if (!orgSlug) console.error("no named address yet — claim an org slug (run402 org slug <slug>) to get run402::<slug>/<name> addresses");
|
|
556
620
|
else console.error(`no address claimed — run 'run402 repos rename <name> --project ${projectId}' to claim one`);
|
|
557
621
|
if (vault.remote) console.error(`remote '${vault.remote.name}' -> ${vault.remote.url} (${vault.remote.reason})`);
|
|
558
622
|
if (pushAction) console.error(`next: ${pushAction.command}`);
|
|
@@ -561,9 +625,11 @@ async function printCreateResult({ projectId, vault, adopted, name }) {
|
|
|
561
625
|
await printKeystoreLocation();
|
|
562
626
|
console.error("");
|
|
563
627
|
console.error("nothing was deployed — this is a vault-only repo. Deploy later with `run402 deploy apply`, or never.");
|
|
628
|
+
printVerboseStats(verboseArgv, sdk);
|
|
564
629
|
}
|
|
565
630
|
|
|
566
631
|
async function createAdopt(projectId, dir, a) {
|
|
632
|
+
const sdk = getSdk();
|
|
567
633
|
const orgId = flagValue(a, "--org") ?? await resolveOwningOrgId(projectId);
|
|
568
634
|
if (!orgId) {
|
|
569
635
|
fail({
|
|
@@ -574,14 +640,15 @@ async function createAdopt(projectId, dir, a) {
|
|
|
574
640
|
});
|
|
575
641
|
}
|
|
576
642
|
try {
|
|
577
|
-
const vault = await
|
|
578
|
-
await printCreateResult({ projectId, vault, adopted: true, name: null });
|
|
643
|
+
const vault = await sdk.gitvault.init({ org_id: orgId, project_id: projectId, repo_dir: dir });
|
|
644
|
+
await printCreateResult({ sdk, projectId, vault, adopted: true, name: null, verboseArgv: a });
|
|
579
645
|
} catch (err) {
|
|
580
646
|
reportSdkError(err);
|
|
581
647
|
}
|
|
582
648
|
}
|
|
583
649
|
|
|
584
650
|
async function createProvision(name, dir, a) {
|
|
651
|
+
const sdk = getSdk();
|
|
585
652
|
const tier = flagValue(a, "--tier") ?? "prototype";
|
|
586
653
|
const idempotencyKey = flagValue(a, "--idempotency-key") ?? `repos-create:${name}`;
|
|
587
654
|
// `optional: true` — a fresh wallet with no org yet is the cold-start path
|
|
@@ -593,7 +660,7 @@ async function createProvision(name, dir, a) {
|
|
|
593
660
|
let provisioned;
|
|
594
661
|
try {
|
|
595
662
|
provisioned = await withAutoApprove(() =>
|
|
596
|
-
|
|
663
|
+
sdk.projects.provision({ tier, name, ...(orgId ? { orgId } : {}), idempotencyKey }),
|
|
597
664
|
);
|
|
598
665
|
} catch (err) {
|
|
599
666
|
reportSdkError(err);
|
|
@@ -612,8 +679,8 @@ async function createProvision(name, dir, a) {
|
|
|
612
679
|
}
|
|
613
680
|
|
|
614
681
|
try {
|
|
615
|
-
const vault = await
|
|
616
|
-
await printCreateResult({ projectId: provisioned.project_id, vault, adopted: false, name });
|
|
682
|
+
const vault = await sdk.gitvault.init({ org_id: effectiveOrgId, project_id: provisioned.project_id, repo_dir: dir });
|
|
683
|
+
await printCreateResult({ sdk, projectId: provisioned.project_id, vault, adopted: false, name, verboseArgv: a });
|
|
617
684
|
} catch (err) {
|
|
618
685
|
reportSdkError(err);
|
|
619
686
|
}
|
|
@@ -621,7 +688,7 @@ async function createProvision(name, dir, a) {
|
|
|
621
688
|
|
|
622
689
|
async function create(args) {
|
|
623
690
|
const a = normalizeArgv(args);
|
|
624
|
-
assertKnownFlags(a, [...CREATE_VALUE_FLAGS, "--help", "-h"], CREATE_VALUE_FLAGS);
|
|
691
|
+
assertKnownFlags(a, [...CREATE_VALUE_FLAGS, "--help", "-h", "-v", "--verbose"], CREATE_VALUE_FLAGS);
|
|
625
692
|
const positionals = requirePositionalCount(a, CREATE_VALUE_FLAGS, {
|
|
626
693
|
min: 0, max: 1, command: "run402 repos create [name]", missing: "",
|
|
627
694
|
});
|
|
@@ -655,8 +722,8 @@ async function create(args) {
|
|
|
655
722
|
// ─── list ───────────────────────────────────────────────────────────────────
|
|
656
723
|
|
|
657
724
|
/** The FROZEN bulk-read shape (task 2.4) — one round trip. */
|
|
658
|
-
async function listViaBulkRead(orgId) {
|
|
659
|
-
const result = await
|
|
725
|
+
async function listViaBulkRead(sdk, orgId) {
|
|
726
|
+
const result = await sdk.gitvault.listByOrg(orgId);
|
|
660
727
|
return Array.isArray(result.vaults) ? result.vaults : [];
|
|
661
728
|
}
|
|
662
729
|
|
|
@@ -667,14 +734,14 @@ async function listViaBulkRead(orgId) {
|
|
|
667
734
|
* function once the bulk route has shipped long enough that no gateway
|
|
668
735
|
* still 404s it.
|
|
669
736
|
*/
|
|
670
|
-
async function listViaFallback(orgId) {
|
|
671
|
-
const result = await
|
|
737
|
+
async function listViaFallback(sdk, orgId) {
|
|
738
|
+
const result = await sdk.projects.list({ org: orgId });
|
|
672
739
|
const projects = Array.isArray(result.projects) ? result.projects : [];
|
|
673
740
|
const repos = [];
|
|
674
741
|
for (const p of projects) {
|
|
675
742
|
let status;
|
|
676
743
|
try {
|
|
677
|
-
status = await
|
|
744
|
+
status = await sdk.gitvault.status({ project_id: p.id });
|
|
678
745
|
} catch {
|
|
679
746
|
continue;
|
|
680
747
|
}
|
|
@@ -695,21 +762,38 @@ async function listViaFallback(orgId) {
|
|
|
695
762
|
return repos;
|
|
696
763
|
}
|
|
697
764
|
|
|
765
|
+
/** `repos list --human`: a compact roster — one line per repo (address, generation, bytes, policy). */
|
|
766
|
+
async function formatRepoListHuman(orgSlug, repos) {
|
|
767
|
+
if (repos.length === 0) return "(no vault-bearing repos in this organization)";
|
|
768
|
+
const { generationToBigInt } = await import("#sdk/node");
|
|
769
|
+
const decimal = (g) => (g ? generationToBigInt(g).toString() : "none");
|
|
770
|
+
const lines = repos.map((r) => {
|
|
771
|
+
const address = orgSlug && r.repo_name ? `run402::${orgSlug}/${r.repo_name}` : (r.repo_name ?? r.project_id);
|
|
772
|
+
return `${address} gen=${decimal(r.newest_generation)} ${r.source_bytes} byte(s) policy=${r.gitvault_policy ?? "(none)"} (${r.repo_id})`;
|
|
773
|
+
});
|
|
774
|
+
return lines.join("\n");
|
|
775
|
+
}
|
|
776
|
+
|
|
698
777
|
async function list(args) {
|
|
699
778
|
const a = normalizeArgv(args);
|
|
700
|
-
assertKnownFlags(a, ["--org", "--help", "-h"], ["--org"]);
|
|
779
|
+
assertKnownFlags(a, ["--org", "--human", "-v", "--verbose", "--help", "-h"], ["--org"]);
|
|
701
780
|
requirePositionalCount(a, ["--org"], { min: 0, max: 0, command: "run402 repos list", missing: "" });
|
|
781
|
+
const human = a.includes("--human");
|
|
782
|
+
if (human && a.includes("--json")) {
|
|
783
|
+
fail({ code: "BAD_USAGE", message: "--human cannot be combined with --json.", details: { flags: a.filter((arg) => arg === "--human" || arg === "--json") } });
|
|
784
|
+
}
|
|
785
|
+
const sdk = getSdk();
|
|
702
786
|
const orgId = await resolveOrgId(a, { cmd: "repos" });
|
|
703
787
|
|
|
704
788
|
let repos;
|
|
705
789
|
let usedFallback = false;
|
|
706
790
|
try {
|
|
707
|
-
repos = await listViaBulkRead(orgId);
|
|
791
|
+
repos = await listViaBulkRead(sdk, orgId);
|
|
708
792
|
} catch (err) {
|
|
709
793
|
if (err?.status === 404) {
|
|
710
794
|
usedFallback = true;
|
|
711
795
|
try {
|
|
712
|
-
repos = await listViaFallback(orgId);
|
|
796
|
+
repos = await listViaFallback(sdk, orgId);
|
|
713
797
|
} catch (fallbackErr) {
|
|
714
798
|
reportSdkError(fallbackErr);
|
|
715
799
|
return;
|
|
@@ -723,42 +807,50 @@ async function list(args) {
|
|
|
723
807
|
let orgSlug = repos.find((r) => r.org_slug)?.org_slug ?? null;
|
|
724
808
|
if (orgSlug == null) {
|
|
725
809
|
try {
|
|
726
|
-
orgSlug = (await
|
|
810
|
+
orgSlug = (await sdk.org(orgId).get()).slug;
|
|
727
811
|
} catch {
|
|
728
812
|
// best-effort — `list` must not fail over an org-slug lookup
|
|
729
813
|
}
|
|
730
814
|
}
|
|
731
815
|
|
|
732
|
-
|
|
816
|
+
if (human) {
|
|
817
|
+
console.log(await formatRepoListHuman(orgSlug, repos));
|
|
818
|
+
printVerboseStats(a, sdk);
|
|
819
|
+
return;
|
|
820
|
+
}
|
|
821
|
+
printJson(sdk, { org_id: orgId, org_slug: orgSlug, repos });
|
|
733
822
|
console.error(`${repos.length} vault-bearing project(s) in this organization${usedFallback ? " (per-project fallback read — the bulk vaults-by-org route is not live on this gateway yet)" : ""}`);
|
|
734
823
|
if (orgSlug) console.error(`org slug: ${orgSlug} — a repo with a claimed address-form name is reachable at run402::${orgSlug}/<name>`);
|
|
824
|
+
printVerboseStats(a, sdk);
|
|
735
825
|
}
|
|
736
826
|
|
|
737
827
|
// ─── view ───────────────────────────────────────────────────────────────────
|
|
738
828
|
|
|
739
829
|
async function view(args) {
|
|
740
830
|
const a = normalizeArgv(args);
|
|
741
|
-
assertKnownFlags(a, [...COMMON_VALUE_FLAGS, "--human", "--help", "-h"], COMMON_VALUE_FLAGS);
|
|
831
|
+
assertKnownFlags(a, [...COMMON_VALUE_FLAGS, "--human", "-v", "--verbose", "--help", "-h"], COMMON_VALUE_FLAGS);
|
|
742
832
|
requirePositionalCount(a, COMMON_VALUE_FLAGS, { min: 0, max: 0, command: "run402 repos view", missing: "" });
|
|
743
833
|
const human = a.includes("--human");
|
|
744
834
|
if (human && a.includes("--json")) {
|
|
745
835
|
fail({ code: "BAD_USAGE", message: "--human cannot be combined with --json.", details: { flags: a.filter((arg) => arg === "--human" || arg === "--json") } });
|
|
746
836
|
}
|
|
837
|
+
const sdk = getSdk();
|
|
747
838
|
const target = await vaultTarget(a);
|
|
748
839
|
try {
|
|
749
840
|
// Design D3: `view` NEVER passes `refs: true` — it is side-effect-free
|
|
750
841
|
// by construction, not by convention. Materialization belongs to `fsck`.
|
|
751
|
-
const s = await
|
|
842
|
+
const s = await sdk.gitvault.status(target);
|
|
752
843
|
let mirror = null;
|
|
753
844
|
if (s.repo_id) {
|
|
754
845
|
try {
|
|
755
|
-
mirror = await
|
|
846
|
+
mirror = await sdk.gitvault.mirrorStatus({ ...target, repo_id: s.repo_id });
|
|
756
847
|
} catch {
|
|
757
848
|
// best-effort — a mirror read failure never fails `view`
|
|
758
849
|
}
|
|
759
850
|
}
|
|
760
851
|
if (human) {
|
|
761
852
|
console.log(await formatRepoHuman(s, mirror));
|
|
853
|
+
printVerboseStats(a, sdk);
|
|
762
854
|
return;
|
|
763
855
|
}
|
|
764
856
|
const verifyRefsAction = nextAction("verify_refs", { command: "run402 repos fsck", why: "Walk the signed chain and materialize verified refs." });
|
|
@@ -769,7 +861,7 @@ async function view(args) {
|
|
|
769
861
|
mirror,
|
|
770
862
|
next_actions: combinedNextActions,
|
|
771
863
|
};
|
|
772
|
-
|
|
864
|
+
printJson(sdk, out);
|
|
773
865
|
printTerminalLoss(s);
|
|
774
866
|
if (s.remote) {
|
|
775
867
|
const suffix =
|
|
@@ -787,6 +879,7 @@ async function view(args) {
|
|
|
787
879
|
}
|
|
788
880
|
for (const w of s.warnings) console.error(`warning (${w.kind}): ${w.message}`);
|
|
789
881
|
for (const n of combinedNextActions) console.error(`next: ${n.why ?? n.action ?? n.type}${n.command ? ` — ${n.command}` : ""}`);
|
|
882
|
+
printVerboseStats(a, sdk);
|
|
790
883
|
} catch (err) {
|
|
791
884
|
reportSdkError(err);
|
|
792
885
|
}
|
|
@@ -796,11 +889,12 @@ async function view(args) {
|
|
|
796
889
|
|
|
797
890
|
async function rename(args) {
|
|
798
891
|
const a = normalizeArgv(args);
|
|
799
|
-
assertKnownFlags(a, [...COMMON_VALUE_FLAGS, "--help", "-h"], COMMON_VALUE_FLAGS);
|
|
892
|
+
assertKnownFlags(a, [...COMMON_VALUE_FLAGS, "-v", "--verbose", "--help", "-h"], COMMON_VALUE_FLAGS);
|
|
800
893
|
const [repoName] = requirePositionalCount(a, COMMON_VALUE_FLAGS, {
|
|
801
894
|
min: 1, max: 1, command: "run402 repos rename <new_name> [--repo <repo_id> | --project <project_id>]",
|
|
802
895
|
missing: "run402 repos rename <new_name>: a new name is required",
|
|
803
896
|
});
|
|
897
|
+
const sdk = getSdk();
|
|
804
898
|
const repoFlag = flagValue(a, "--repo");
|
|
805
899
|
const projectFlag = flagValue(a, "--project");
|
|
806
900
|
if (repoFlag != null && projectFlag != null) {
|
|
@@ -809,7 +903,7 @@ async function rename(args) {
|
|
|
809
903
|
let projectId;
|
|
810
904
|
if (repoFlag != null) {
|
|
811
905
|
try {
|
|
812
|
-
projectId = (await
|
|
906
|
+
projectId = (await sdk.gitvault.get(repoFlag)).project_id;
|
|
813
907
|
} catch (err) {
|
|
814
908
|
reportSdkError(err);
|
|
815
909
|
return;
|
|
@@ -818,16 +912,16 @@ async function rename(args) {
|
|
|
818
912
|
projectId = resolveProjectId(projectFlag);
|
|
819
913
|
}
|
|
820
914
|
try {
|
|
821
|
-
const result = await
|
|
915
|
+
const result = await sdk.projects.setRepoName(projectId, repoName);
|
|
822
916
|
let address = null;
|
|
823
917
|
try {
|
|
824
918
|
const owningOrg = await resolveOwningOrgId(projectId);
|
|
825
|
-
const orgSlug = owningOrg ? (await
|
|
919
|
+
const orgSlug = owningOrg ? (await sdk.org(owningOrg).get()).slug : null;
|
|
826
920
|
if (orgSlug) address = gitvaultRemoteUrlForRepo(orgSlug, result.repo_name);
|
|
827
921
|
} catch {
|
|
828
922
|
// The claim itself already succeeded — a failed address-preview lookup is never fatal.
|
|
829
923
|
}
|
|
830
|
-
|
|
924
|
+
printJson(sdk, { ...result, address });
|
|
831
925
|
console.error(
|
|
832
926
|
result.previous_repo_name && result.previous_repo_name !== result.repo_name
|
|
833
927
|
? `renamed from "${result.previous_repo_name}" to "${result.repo_name}"`
|
|
@@ -835,6 +929,7 @@ async function rename(args) {
|
|
|
835
929
|
);
|
|
836
930
|
if (address) console.error(`address: ${address}`);
|
|
837
931
|
else console.error("this org has no slug yet — claim one with `run402 org slug <slug>` to get a full run402::<slug>/<name> address");
|
|
932
|
+
printVerboseStats(a, sdk);
|
|
838
933
|
} catch (err) {
|
|
839
934
|
reportSdkError(err);
|
|
840
935
|
}
|
|
@@ -863,22 +958,22 @@ async function checkResource(read, resourceName, countOf) {
|
|
|
863
958
|
* that fails for a reason OTHER than "genuinely absent" (404) is reported
|
|
864
959
|
* `unknown` and REFUSES delete too — D9 never guesses its way to yes.
|
|
865
960
|
*/
|
|
866
|
-
async function checkNonRepoResources(projectId) {
|
|
961
|
+
async function checkNonRepoResources(sdk, projectId) {
|
|
867
962
|
const refused = [];
|
|
868
963
|
try {
|
|
869
|
-
const detail = await
|
|
964
|
+
const detail = await sdk.projects.get(projectId);
|
|
870
965
|
if (Array.isArray(detail.mailbox) && detail.mailbox.length > 0) refused.push({ resource: "mailbox", status: "present", count: detail.mailbox.length });
|
|
871
966
|
if (Array.isArray(detail.custom_domains) && detail.custom_domains.length > 0) refused.push({ resource: "custom_domains", status: "present", count: detail.custom_domains.length });
|
|
872
967
|
} catch (err) {
|
|
873
968
|
refused.push({ resource: "project_detail", status: "unknown", reason: err?.message ?? String(err) });
|
|
874
969
|
}
|
|
875
|
-
const schema = await checkResource(() =>
|
|
970
|
+
const schema = await checkResource(() => sdk.projects.getSchema(projectId), "database_schema", (s) => (Array.isArray(s?.tables) ? s.tables.length : 0));
|
|
876
971
|
if (schema) refused.push(schema);
|
|
877
|
-
const functions = await checkResource(() =>
|
|
972
|
+
const functions = await checkResource(() => sdk.functions.list(projectId), "functions", (r) => (Array.isArray(r?.functions) ? r.functions.length : 0));
|
|
878
973
|
if (functions) refused.push(functions);
|
|
879
|
-
const secrets = await checkResource(() =>
|
|
974
|
+
const secrets = await checkResource(() => sdk.secrets.list(projectId), "secrets", (r) => (Array.isArray(r?.secrets) ? r.secrets.length : 0));
|
|
880
975
|
if (secrets) refused.push(secrets);
|
|
881
|
-
const subdomains = await checkResource(() =>
|
|
976
|
+
const subdomains = await checkResource(() => sdk.subdomains.list(projectId), "subdomains", (r) => (Array.isArray(r) ? r.length : 0));
|
|
882
977
|
if (subdomains) refused.push(subdomains);
|
|
883
978
|
return refused;
|
|
884
979
|
}
|
|
@@ -893,7 +988,8 @@ function stripFlag(args, flag) {
|
|
|
893
988
|
|
|
894
989
|
async function del(args) {
|
|
895
990
|
const a = normalizeArgv(args);
|
|
896
|
-
assertKnownFlags(a, ["--project", "--repo", "--force", "--help", "-h"], ["--project", "--repo"]);
|
|
991
|
+
assertKnownFlags(a, ["--project", "--repo", "--force", "-v", "--verbose", "--help", "-h"], ["--project", "--repo"]);
|
|
992
|
+
const sdk = getSdk();
|
|
897
993
|
const repoFlag = flagValue(a, "--repo");
|
|
898
994
|
let projectId;
|
|
899
995
|
let rest;
|
|
@@ -902,7 +998,7 @@ async function del(args) {
|
|
|
902
998
|
fail({ code: "BAD_USAGE", message: "pass --repo or --project, not both." });
|
|
903
999
|
}
|
|
904
1000
|
try {
|
|
905
|
-
projectId = (await
|
|
1001
|
+
projectId = (await sdk.gitvault.get(repoFlag)).project_id;
|
|
906
1002
|
} catch (err) {
|
|
907
1003
|
reportSdkError(err);
|
|
908
1004
|
return;
|
|
@@ -916,7 +1012,7 @@ async function del(args) {
|
|
|
916
1012
|
|
|
917
1013
|
let status;
|
|
918
1014
|
try {
|
|
919
|
-
status = await
|
|
1015
|
+
status = await sdk.gitvault.status({ project_id: projectId });
|
|
920
1016
|
} catch (err) {
|
|
921
1017
|
reportSdkError(err);
|
|
922
1018
|
return;
|
|
@@ -927,7 +1023,7 @@ async function del(args) {
|
|
|
927
1023
|
|
|
928
1024
|
// D9, checked FIRST and unconditionally: --force below overrides only the
|
|
929
1025
|
// vault-history confirmation, never this refusal.
|
|
930
|
-
const refusedResources = await checkNonRepoResources(projectId);
|
|
1026
|
+
const refusedResources = await checkNonRepoResources(sdk, projectId);
|
|
931
1027
|
if (refusedResources.length > 0) {
|
|
932
1028
|
fail({
|
|
933
1029
|
code: "PROJECT_HAS_NON_REPO_RESOURCES",
|
|
@@ -957,13 +1053,14 @@ async function del(args) {
|
|
|
957
1053
|
}
|
|
958
1054
|
|
|
959
1055
|
try {
|
|
960
|
-
await
|
|
961
|
-
|
|
1056
|
+
await sdk.projects.delete(projectId);
|
|
1057
|
+
printJson(sdk, {
|
|
962
1058
|
project_id: projectId,
|
|
963
1059
|
deleted: true,
|
|
964
1060
|
deleted_resources: ["project", ...(vault ? ["vault_history"] : [])],
|
|
965
1061
|
vault: vault ? { repo_id: status.repo_id, admitted_generations: admittedGenerations, source_bytes: sourceBytes } : null,
|
|
966
|
-
}
|
|
1062
|
+
});
|
|
1063
|
+
printVerboseStats(a, sdk);
|
|
967
1064
|
} catch (err) {
|
|
968
1065
|
reportSdkError(err);
|
|
969
1066
|
}
|
|
@@ -998,12 +1095,26 @@ async function detectSlugFormRemote(a, repoDir) {
|
|
|
998
1095
|
return null;
|
|
999
1096
|
}
|
|
1000
1097
|
|
|
1098
|
+
/**
|
|
1099
|
+
* Dirty-tree disclosure (help people not make mistakes): even an explicit
|
|
1100
|
+
* `--allow-dirty` override never captures silently — every modified/staged
|
|
1101
|
+
* tracked path and every untracked-not-ignored path that got swept into the
|
|
1102
|
+
* capture is named on stderr, one per line.
|
|
1103
|
+
*/
|
|
1104
|
+
function printDirtyDisclosure(snapshot) {
|
|
1105
|
+
if (!snapshot) return;
|
|
1106
|
+
for (const p of snapshot.modified_captured ?? []) console.error(`captured (modified): ${p}`);
|
|
1107
|
+
for (const p of snapshot.untracked_captured ?? []) console.error(`captured (untracked): ${p}`);
|
|
1108
|
+
}
|
|
1109
|
+
|
|
1001
1110
|
async function snapshot(args) {
|
|
1002
1111
|
const a = normalizeArgv(args);
|
|
1003
|
-
assertKnownFlags(a, [...SNAPSHOT_VALUE_FLAGS, "--checkpoint", "--dry-run", "--help", "-h"], SNAPSHOT_VALUE_FLAGS);
|
|
1112
|
+
assertKnownFlags(a, [...SNAPSHOT_VALUE_FLAGS, "--checkpoint", "--dry-run", "--allow-dirty", "-v", "--verbose", "--help", "-h"], SNAPSHOT_VALUE_FLAGS);
|
|
1004
1113
|
requirePositionalCount(a, SNAPSHOT_VALUE_FLAGS, { min: 0, max: 0, command: "run402 repos snapshot", missing: "" });
|
|
1114
|
+
const sdk = getSdk();
|
|
1005
1115
|
const dryRun = a.includes("--dry-run");
|
|
1006
1116
|
const message = flagValue(a, "--message");
|
|
1117
|
+
const allowDirty = a.includes("--allow-dirty");
|
|
1007
1118
|
const repoDir = process.cwd();
|
|
1008
1119
|
const address = await detectSlugFormRemote(a, repoDir);
|
|
1009
1120
|
const target = address ? { repo_dir: repoDir } : await vaultTarget(a);
|
|
@@ -1021,12 +1132,15 @@ async function snapshot(args) {
|
|
|
1021
1132
|
console.error("");
|
|
1022
1133
|
},
|
|
1023
1134
|
};
|
|
1024
|
-
|
|
1135
|
+
const snapshotOpts = {};
|
|
1136
|
+
if (message != null) snapshotOpts.message = message;
|
|
1137
|
+
if (allowDirty) snapshotOpts.allowDirty = true;
|
|
1138
|
+
if (Object.keys(snapshotOpts).length > 0) opts.snapshot = snapshotOpts;
|
|
1025
1139
|
if (a.includes("--checkpoint")) opts.checkpoint = true;
|
|
1026
1140
|
try {
|
|
1027
1141
|
if (dryRun) {
|
|
1028
|
-
const plan = await
|
|
1029
|
-
|
|
1142
|
+
const plan = await sdk.gitvault.planPush(opts);
|
|
1143
|
+
printJson(sdk, plan);
|
|
1030
1144
|
if (plan.allocation_needed) {
|
|
1031
1145
|
console.error("dry-run: no repo allocated for this project yet — a real snapshot would allocate one first; object/byte sizing is not knowable until then");
|
|
1032
1146
|
} else {
|
|
@@ -1035,16 +1149,20 @@ async function snapshot(args) {
|
|
|
1035
1149
|
`${plan.object_count} object(s), ${plan.encrypted_bytes} encrypted byte(s) (${plan.raw_bytes} raw)`,
|
|
1036
1150
|
);
|
|
1037
1151
|
}
|
|
1152
|
+
printDirtyDisclosure(plan.snapshot);
|
|
1153
|
+
printVerboseStats(a, sdk);
|
|
1038
1154
|
return;
|
|
1039
1155
|
}
|
|
1040
|
-
const result = await
|
|
1041
|
-
|
|
1156
|
+
const result = await sdk.gitvault.push(opts);
|
|
1157
|
+
printJson(sdk, result);
|
|
1042
1158
|
console.error(`published generation ${result.generation} (${result.form})`);
|
|
1043
1159
|
if (result.mirror_push?.outcome === "pushed") {
|
|
1044
1160
|
console.error(`mirror: pushed generation ${result.generation} (${result.mirror_push.summary?.objects_copied ?? 0} object(s) copied)`);
|
|
1045
1161
|
} else if (result.mirror_push?.outcome === "failed") {
|
|
1046
1162
|
console.error(`mirror: dual-push FAILED (deploy is unaffected) — ${result.mirror_push.error ?? "see mirror_push.summary.errors"}`);
|
|
1047
1163
|
}
|
|
1164
|
+
printDirtyDisclosure(result.snapshot);
|
|
1165
|
+
printVerboseStats(a, sdk);
|
|
1048
1166
|
} catch (err) {
|
|
1049
1167
|
reportSdkError(err);
|
|
1050
1168
|
}
|
|
@@ -1055,7 +1173,7 @@ async function snapshot(args) {
|
|
|
1055
1173
|
async function policy(args) {
|
|
1056
1174
|
const a = normalizeArgv(args);
|
|
1057
1175
|
const valueFlags = [...COMMON_VALUE_FLAGS, "--reason"];
|
|
1058
|
-
assertKnownFlags(a, [...valueFlags, "--help", "-h"], valueFlags);
|
|
1176
|
+
assertKnownFlags(a, [...valueFlags, "-v", "--verbose", "--help", "-h"], valueFlags);
|
|
1059
1177
|
const [requested] = requirePositionalCount(a, valueFlags, {
|
|
1060
1178
|
min: 1, max: 1, command: "run402 repos policy <required|grandfathered>",
|
|
1061
1179
|
missing: "Missing <policy>. Expected `required` or `grandfathered`.",
|
|
@@ -1083,13 +1201,14 @@ async function policy(args) {
|
|
|
1083
1201
|
const sdk = getSdk();
|
|
1084
1202
|
const repoId = target.repo_id ?? (await sdk.gitvault.forProject(target.project_id)).repo_id;
|
|
1085
1203
|
const result = await sdk.gitvault.setPolicy(repoId, { gitvault_policy: requested, ...(reason != null ? { reason } : {}) });
|
|
1086
|
-
|
|
1204
|
+
printJson(sdk, { repo_id: repoId, ...result });
|
|
1087
1205
|
console.error(
|
|
1088
1206
|
result.changed
|
|
1089
1207
|
? `gitvault_policy is now ${result.gitvault_policy} (version ${result.gitvault_policy_version})`
|
|
1090
1208
|
: `gitvault_policy was already ${result.gitvault_policy} — nothing changed`,
|
|
1091
1209
|
);
|
|
1092
1210
|
for (const w of result.warnings ?? []) console.error(`warning (${w.kind}): ${w.message}`);
|
|
1211
|
+
printVerboseStats(a, sdk);
|
|
1093
1212
|
} catch (err) {
|
|
1094
1213
|
reportSdkError(err);
|
|
1095
1214
|
}
|
|
@@ -1099,10 +1218,11 @@ async function policy(args) {
|
|
|
1099
1218
|
|
|
1100
1219
|
const MIRROR_VALUE_FLAGS = [...COMMON_VALUE_FLAGS, "--profile", "--region", "--endpoint"];
|
|
1101
1220
|
|
|
1102
|
-
async function mirrorRead(target) {
|
|
1221
|
+
async function mirrorRead(target, a) {
|
|
1222
|
+
const sdk = getSdk();
|
|
1103
1223
|
try {
|
|
1104
|
-
const result = await
|
|
1105
|
-
|
|
1224
|
+
const result = await sdk.gitvault.mirrorStatus(target);
|
|
1225
|
+
printJson(sdk, result);
|
|
1106
1226
|
if (!result.configured) {
|
|
1107
1227
|
console.error(`no mirror configured for ${result.repo_id}. Configure one: run402 repos mirror <destination>`);
|
|
1108
1228
|
} else {
|
|
@@ -1110,49 +1230,55 @@ async function mirrorRead(target) {
|
|
|
1110
1230
|
console.error(`mirror ${result.destination}: mirrored generation ${result.mirrored_generation ?? "(none)"}, vault newest ${result.newest_generation ?? "(none)"} — ${currency}`);
|
|
1111
1231
|
}
|
|
1112
1232
|
printMirrorHonesty(result);
|
|
1233
|
+
printVerboseStats(a, sdk);
|
|
1113
1234
|
} catch (err) {
|
|
1114
1235
|
reportSdkError(err);
|
|
1115
1236
|
}
|
|
1116
1237
|
}
|
|
1117
1238
|
|
|
1118
1239
|
async function mirrorSet(target, destination, a) {
|
|
1240
|
+
const sdk = getSdk();
|
|
1119
1241
|
const credential = resolveMirrorCredential(a);
|
|
1120
1242
|
const region = flagValue(a, "--region");
|
|
1121
1243
|
const endpoint = flagValue(a, "--endpoint");
|
|
1122
1244
|
try {
|
|
1123
|
-
const result = await
|
|
1245
|
+
const result = await sdk.gitvault.mirrorSet({
|
|
1124
1246
|
...target,
|
|
1125
1247
|
destination_url: destination,
|
|
1126
1248
|
...(credential ? { credential } : {}),
|
|
1127
1249
|
...(region != null ? { region } : {}),
|
|
1128
1250
|
...(endpoint != null ? { endpoint } : {}),
|
|
1129
1251
|
});
|
|
1130
|
-
|
|
1252
|
+
printJson(sdk, result);
|
|
1131
1253
|
console.error(`mirror configured for ${result.repo_id} -> ${formatMirrorDestination(result.destination)}`);
|
|
1132
1254
|
console.error("run `run402 repos mirror --backfill` to catch it up now, then every publish dual-pushes automatically.");
|
|
1255
|
+
printVerboseStats(a, sdk);
|
|
1133
1256
|
} catch (err) {
|
|
1134
1257
|
reportSdkError(err);
|
|
1135
1258
|
}
|
|
1136
1259
|
}
|
|
1137
1260
|
|
|
1138
|
-
async function mirrorOff(target) {
|
|
1261
|
+
async function mirrorOff(target, a) {
|
|
1262
|
+
const sdk = getSdk();
|
|
1139
1263
|
try {
|
|
1140
|
-
const result = await
|
|
1141
|
-
|
|
1264
|
+
const result = await sdk.gitvault.mirrorRemove(target);
|
|
1265
|
+
printJson(sdk, result);
|
|
1142
1266
|
console.error(
|
|
1143
1267
|
result.removed
|
|
1144
1268
|
? `mirror config removed for ${result.repo_id} — the mirror's OWN bytes were not touched`
|
|
1145
1269
|
: `no mirror was configured for ${result.repo_id} — nothing to remove`,
|
|
1146
1270
|
);
|
|
1271
|
+
printVerboseStats(a, sdk);
|
|
1147
1272
|
} catch (err) {
|
|
1148
1273
|
reportSdkError(err);
|
|
1149
1274
|
}
|
|
1150
1275
|
}
|
|
1151
1276
|
|
|
1152
|
-
async function mirrorBackfill(target) {
|
|
1277
|
+
async function mirrorBackfill(target, a) {
|
|
1278
|
+
const sdk = getSdk();
|
|
1153
1279
|
try {
|
|
1154
|
-
const result = await
|
|
1155
|
-
|
|
1280
|
+
const result = await sdk.gitvault.mirrorSync(target);
|
|
1281
|
+
printJson(sdk, result);
|
|
1156
1282
|
await spillIfLarge(result.repo_id, "mirror-backfill", result);
|
|
1157
1283
|
console.error(
|
|
1158
1284
|
`mirror backfill for ${result.repo_id}: ${result.objects_copied} copied, ${result.objects_already_present} already present` +
|
|
@@ -1161,6 +1287,7 @@ async function mirrorBackfill(target) {
|
|
|
1161
1287
|
);
|
|
1162
1288
|
for (const e of result.errors) console.error(` failed: ${e.key} — ${e.error}`);
|
|
1163
1289
|
printMirrorHonesty(result);
|
|
1290
|
+
printVerboseStats(a, sdk);
|
|
1164
1291
|
} catch (err) {
|
|
1165
1292
|
reportSdkError(err);
|
|
1166
1293
|
}
|
|
@@ -1168,7 +1295,7 @@ async function mirrorBackfill(target) {
|
|
|
1168
1295
|
|
|
1169
1296
|
async function mirror(args) {
|
|
1170
1297
|
const a = normalizeArgv(args);
|
|
1171
|
-
assertKnownFlags(a, [...MIRROR_VALUE_FLAGS, "--off", "--backfill", "--ambient", "--help", "-h"], MIRROR_VALUE_FLAGS);
|
|
1298
|
+
assertKnownFlags(a, [...MIRROR_VALUE_FLAGS, "--off", "--backfill", "--ambient", "-v", "--verbose", "--help", "-h"], MIRROR_VALUE_FLAGS);
|
|
1172
1299
|
const positionals = requirePositionalCount(a, MIRROR_VALUE_FLAGS, {
|
|
1173
1300
|
min: 0, max: 1, command: "run402 repos mirror [<destination>]", missing: "",
|
|
1174
1301
|
});
|
|
@@ -1185,26 +1312,57 @@ async function mirror(args) {
|
|
|
1185
1312
|
}
|
|
1186
1313
|
const target = await vaultTarget(a);
|
|
1187
1314
|
if (destination != null) return mirrorSet(target, destination, a);
|
|
1188
|
-
if (off) return mirrorOff(target);
|
|
1189
|
-
if (backfill) return mirrorBackfill(target);
|
|
1190
|
-
return mirrorRead(target);
|
|
1315
|
+
if (off) return mirrorOff(target, a);
|
|
1316
|
+
if (backfill) return mirrorBackfill(target, a);
|
|
1317
|
+
return mirrorRead(target, a);
|
|
1191
1318
|
}
|
|
1192
1319
|
|
|
1193
1320
|
// ─── fsck (verify the head chain + materialize refs) ──────────────────
|
|
1194
1321
|
|
|
1322
|
+
/** `repos fsck --human`: the same verdict the stderr lines already carry, condensed into one block. */
|
|
1323
|
+
function formatFsckHuman(result, mirrorRequested) {
|
|
1324
|
+
const lines = [`Repo: ${result.repo_id}`];
|
|
1325
|
+
lines.push(
|
|
1326
|
+
!result.write
|
|
1327
|
+
? `Verified through generation ${result.verified_to_generation} — audit mode, nothing local was persisted.`
|
|
1328
|
+
: result.local_state_changed
|
|
1329
|
+
? `Verified through generation ${result.verified_to_generation} — local pin advanced from ${result.pin_before.highest_authenticated ?? "genesis"} to ${result.pin_after.highest_authenticated}.`
|
|
1330
|
+
: `Verified through generation ${result.verified_to_generation} — already at the newest verified generation.`,
|
|
1331
|
+
);
|
|
1332
|
+
if (mirrorRequested && result.mirror) {
|
|
1333
|
+
lines.push(
|
|
1334
|
+
`Mirror: recoverable generation ${result.mirror.recovered_generation}` +
|
|
1335
|
+
(result.mirror.chain_break ? ` (chain break at ${result.mirror.chain_break.generation}: ${result.mirror.chain_break.reason})` : "") +
|
|
1336
|
+
(result.mirror.data_loss_detected ? ` — DATA LOSS DETECTED (${result.mirror.absences.filter((x) => x.adjudication === "unexplained_absence").length} unexplained absence(s))` : ""),
|
|
1337
|
+
);
|
|
1338
|
+
}
|
|
1339
|
+
return lines.join("\n");
|
|
1340
|
+
}
|
|
1341
|
+
|
|
1195
1342
|
async function fsck(args) {
|
|
1196
1343
|
const a = normalizeArgv(args);
|
|
1197
1344
|
const valueFlags = [...COMMON_VALUE_FLAGS, "--budget"];
|
|
1198
|
-
assertKnownFlags(a, [...valueFlags, "--mirror", "--no-write", "--help", "-h"], valueFlags);
|
|
1345
|
+
assertKnownFlags(a, [...valueFlags, "--mirror", "--no-write", "--human", "-v", "--verbose", "--help", "-h"], valueFlags);
|
|
1199
1346
|
requirePositionalCount(a, valueFlags, { min: 0, max: 0, command: "run402 repos fsck", missing: "" });
|
|
1347
|
+
const human = a.includes("--human");
|
|
1348
|
+
if (human && a.includes("--json")) {
|
|
1349
|
+
fail({ code: "BAD_USAGE", message: "--human cannot be combined with --json.", details: { flags: a.filter((arg) => arg === "--human" || arg === "--json") } });
|
|
1350
|
+
}
|
|
1351
|
+
const sdk = getSdk();
|
|
1200
1352
|
const target = await vaultTarget(a);
|
|
1201
1353
|
const budget = flagValue(a, "--budget");
|
|
1202
1354
|
if (budget != null) target.verification_budget = parseIntegerFlag("--budget", budget, { min: 1 });
|
|
1203
1355
|
const write = !a.includes("--no-write");
|
|
1204
1356
|
const mirrorRequested = a.includes("--mirror");
|
|
1205
1357
|
try {
|
|
1206
|
-
const result = await
|
|
1207
|
-
|
|
1358
|
+
const result = await sdk.gitvault.fsck({ ...target, write, mirror: mirrorRequested });
|
|
1359
|
+
if (human) {
|
|
1360
|
+
console.log(formatFsckHuman(result, mirrorRequested));
|
|
1361
|
+
if (mirrorRequested && result.mirror) printMirrorHonesty(result.mirror);
|
|
1362
|
+
printVerboseStats(a, sdk);
|
|
1363
|
+
return;
|
|
1364
|
+
}
|
|
1365
|
+
printJson(sdk, result);
|
|
1208
1366
|
await spillIfLarge(result.repo_id, "fsck", result);
|
|
1209
1367
|
if (!write) {
|
|
1210
1368
|
console.error(`--no-write: verified through generation ${result.verified_to_generation} — nothing local was persisted (pin_before === pin_after).`);
|
|
@@ -1220,6 +1378,7 @@ async function fsck(args) {
|
|
|
1220
1378
|
}
|
|
1221
1379
|
printMirrorHonesty(result.mirror);
|
|
1222
1380
|
}
|
|
1381
|
+
printVerboseStats(a, sdk);
|
|
1223
1382
|
} catch (err) {
|
|
1224
1383
|
reportSdkError(err);
|
|
1225
1384
|
}
|
|
@@ -1231,8 +1390,9 @@ const GC_VALUE_FLAGS = [...COMMON_VALUE_FLAGS, "--intent-core", "--verifier-rece
|
|
|
1231
1390
|
|
|
1232
1391
|
async function gc(args) {
|
|
1233
1392
|
const a = normalizeArgv(args);
|
|
1234
|
-
assertKnownFlags(a, [...GC_VALUE_FLAGS, "--submit", "--wait", "--help", "-h"], GC_VALUE_FLAGS);
|
|
1393
|
+
assertKnownFlags(a, [...GC_VALUE_FLAGS, "--submit", "--wait", "-v", "--verbose", "--help", "-h"], GC_VALUE_FLAGS);
|
|
1235
1394
|
requirePositionalCount(a, GC_VALUE_FLAGS, { min: 0, max: 0, command: "run402 repos gc", missing: "" });
|
|
1395
|
+
const sdk = getSdk();
|
|
1236
1396
|
const submitting = a.includes("--submit");
|
|
1237
1397
|
const corePath = flagValue(a, "--intent-core");
|
|
1238
1398
|
const receiptPath = flagValue(a, "--verifier-receipt");
|
|
@@ -1252,9 +1412,9 @@ async function gc(args) {
|
|
|
1252
1412
|
if (submitting) {
|
|
1253
1413
|
const opts = { ...target, submit: { core: readJsonFile("--intent-core", corePath), verifier_receipt: readJsonFile("--verifier-receipt", receiptPath) } };
|
|
1254
1414
|
if (a.includes("--wait")) opts.submit.wait = {};
|
|
1255
|
-
const prune = await
|
|
1415
|
+
const prune = await sdk.gitvault.prune(opts);
|
|
1256
1416
|
const out = { phase: "submitted", prune };
|
|
1257
|
-
|
|
1417
|
+
printJson(sdk, out);
|
|
1258
1418
|
if (prune.confirmation?.outcome) {
|
|
1259
1419
|
console.error(
|
|
1260
1420
|
`submitted — the signed completion reports ${prune.confirmation.deleted.length} deleted, ` +
|
|
@@ -1265,11 +1425,12 @@ async function gc(args) {
|
|
|
1265
1425
|
console.error("submitted — no completion yet. Nothing is deleted until the control-plane-signed completion says so; re-run with --wait or poll the intent.");
|
|
1266
1426
|
}
|
|
1267
1427
|
console.error(prune.note);
|
|
1428
|
+
printVerboseStats(a, sdk);
|
|
1268
1429
|
return;
|
|
1269
1430
|
}
|
|
1270
1431
|
|
|
1271
|
-
const checkpoint = await
|
|
1272
|
-
const prune = await
|
|
1432
|
+
const checkpoint = await sdk.gitvault.compact(target);
|
|
1433
|
+
const prune = await sdk.gitvault.prune(target);
|
|
1273
1434
|
const nextActions = [];
|
|
1274
1435
|
if (!prune.blocked_reason && prune.object_candidates.length > 0) {
|
|
1275
1436
|
// Additive fields beyond the CLI's usual {type, command, why}: the
|
|
@@ -1285,7 +1446,7 @@ async function gc(args) {
|
|
|
1285
1446
|
});
|
|
1286
1447
|
}
|
|
1287
1448
|
const out = { phase: "planned", checkpoint, prune, next_actions: nextActions };
|
|
1288
|
-
|
|
1449
|
+
printJson(sdk, out);
|
|
1289
1450
|
console.error(`checkpoint published at generation ${checkpoint.generation}: ${checkpoint.covered_refs} ref(s), ${checkpoint.covered_roots} retention root(s).`);
|
|
1290
1451
|
if (!checkpoint.cutoff_bound) {
|
|
1291
1452
|
console.error("no retention-cutoff ticket was obtained, so roots were RETAINED — expiry is permissive. The checkpoint published, but no expired root left the map.");
|
|
@@ -1303,6 +1464,7 @@ async function gc(args) {
|
|
|
1303
1464
|
}
|
|
1304
1465
|
}
|
|
1305
1466
|
console.error("`gc` is NOT \"exactly git gc\" — the deletion ceremony is stricter: nothing is removed until a control-plane-signed completion confirms it.");
|
|
1467
|
+
printVerboseStats(a, sdk);
|
|
1306
1468
|
} catch (err) {
|
|
1307
1469
|
reportSdkError(err);
|
|
1308
1470
|
}
|
|
@@ -1310,14 +1472,40 @@ async function gc(args) {
|
|
|
1310
1472
|
|
|
1311
1473
|
// ─── access (read-only; repair gated) ──────────────────────
|
|
1312
1474
|
|
|
1475
|
+
/** `repos access --human`: a compact roster of directory recipients and their coverage. */
|
|
1476
|
+
function formatAccessHuman(result) {
|
|
1477
|
+
const lines = [`Repo: ${result.repo_id}`];
|
|
1478
|
+
lines.push(`Recipients: ${result.recipients.length} directory, ${result.recipients.filter((r) => r.covered).length} covered`);
|
|
1479
|
+
for (const r of result.recipients) {
|
|
1480
|
+
lines.push(` ${r.covered ? "covered" : "NOT covered"} ${r.display_name ?? r.principal_id}${r.envelope_state ? ` (${r.envelope_state})` : ""}`);
|
|
1481
|
+
}
|
|
1482
|
+
if (result.this_keystore) lines.push(`This machine's own keystore also covers (writing principal, not in org directory): ${result.this_keystore.fingerprint}`);
|
|
1483
|
+
if (result.unmatched_covered_fingerprints.length > 0) lines.push(`Orphaned/external coverage: ${result.unmatched_covered_fingerprints.join(", ")}`);
|
|
1484
|
+
if (Array.isArray(result.stale_access) && result.stale_access.length > 0) {
|
|
1485
|
+
lines.push(`Stale access (removed members that still decrypt): ${result.stale_access.map((s) => s.display_name ?? s.principal_id).join(", ")}`);
|
|
1486
|
+
}
|
|
1487
|
+
lines.push(result.gap);
|
|
1488
|
+
return lines.join("\n");
|
|
1489
|
+
}
|
|
1490
|
+
|
|
1313
1491
|
async function accessRead(args) {
|
|
1314
1492
|
const a = normalizeArgv(args);
|
|
1315
|
-
assertKnownFlags(a, [...COMMON_VALUE_FLAGS, "--help", "-h"], COMMON_VALUE_FLAGS);
|
|
1493
|
+
assertKnownFlags(a, [...COMMON_VALUE_FLAGS, "--human", "-v", "--verbose", "--help", "-h"], COMMON_VALUE_FLAGS);
|
|
1316
1494
|
requirePositionalCount(a, COMMON_VALUE_FLAGS, { min: 0, max: 0, command: "run402 repos access", missing: "" });
|
|
1495
|
+
const human = a.includes("--human");
|
|
1496
|
+
if (human && a.includes("--json")) {
|
|
1497
|
+
fail({ code: "BAD_USAGE", message: "--human cannot be combined with --json.", details: { flags: a.filter((arg) => arg === "--human" || arg === "--json") } });
|
|
1498
|
+
}
|
|
1499
|
+
const sdk = getSdk();
|
|
1317
1500
|
const target = await vaultTarget(a);
|
|
1318
1501
|
try {
|
|
1319
|
-
const result = await
|
|
1320
|
-
|
|
1502
|
+
const result = await sdk.gitvault.access(target);
|
|
1503
|
+
if (human) {
|
|
1504
|
+
console.log(formatAccessHuman(result));
|
|
1505
|
+
printVerboseStats(a, sdk);
|
|
1506
|
+
return;
|
|
1507
|
+
}
|
|
1508
|
+
printJson(sdk, result);
|
|
1321
1509
|
await spillIfLarge(result.repo_id, "access", result);
|
|
1322
1510
|
console.error(`${result.recipients.length} directory recipient(s), ${result.recipients.filter((r) => r.covered).length} covered on this repo.`);
|
|
1323
1511
|
if (result.this_keystore) {
|
|
@@ -1331,35 +1519,183 @@ async function accessRead(args) {
|
|
|
1331
1519
|
console.error(`${result.stale_access.length} removed member(s) STILL decrypt this vault (not yet revocable — no epoch rotation in v0): ${names}`);
|
|
1332
1520
|
}
|
|
1333
1521
|
console.error(result.gap);
|
|
1522
|
+
printVerboseStats(a, sdk);
|
|
1334
1523
|
} catch (err) {
|
|
1335
1524
|
reportSdkError(err);
|
|
1336
1525
|
}
|
|
1337
1526
|
}
|
|
1338
1527
|
|
|
1528
|
+
const ROTATION_VALUE_FLAGS = [...COMMON_VALUE_FLAGS, "--recipient-state-version", "--recipient-revocation-version", "--idempotency-key"];
|
|
1529
|
+
|
|
1530
|
+
/**
|
|
1531
|
+
* `run402 repos access repair` (D193-D203, rev 42) — a general re-key of
|
|
1532
|
+
* this vault's CURRENT epoch, dropping every principal in `stale_access`
|
|
1533
|
+
* (`pending_removal`, still covered) and clearing a pre-existing vault's
|
|
1534
|
+
* one-time migration requirement. Drives `rotateEpoch({reason:"elective_rekey"})`.
|
|
1535
|
+
*
|
|
1536
|
+
* `--recipient-state-version`/`--recipient-revocation-version` are the D194
|
|
1537
|
+
* frozen watermarks this attempt must be fenced against. They are NOT
|
|
1538
|
+
* discovered automatically here: the live gateway exposes NO general read
|
|
1539
|
+
* route for `internal.gitvault_recipient_state_counters` outside the
|
|
1540
|
+
* `key-revocation` declare route's own response (see
|
|
1541
|
+
* `GitvaultVault.rotateEpoch`'s doc comment, `sdk/src/node/gitvault-
|
|
1542
|
+
* publication.ts`, for the confirmed source-level finding). Until that
|
|
1543
|
+
* route ships, this verb needs the pair supplied explicitly — refusing
|
|
1544
|
+
* cleanly and naming exactly this when they are omitted, rather than
|
|
1545
|
+
* guessing and either failing opaquely or (worse) never converging.
|
|
1546
|
+
*
|
|
1547
|
+
* **`elective_rekey` refuses ANY exclusion** (`EPOCH_ROTATION_INCOMPLETE_ENROLLMENT`
|
|
1548
|
+
* on even one keyless/unconfirmed desired principal) — so a pending
|
|
1549
|
+
* `/confirm`/`/repin` receipt does NOT help here: folding it into THIS
|
|
1550
|
+
* rotation's `pending_confirmations` still leaves that principal
|
|
1551
|
+
* `excluded_unconfirmed` for THIS rotation (D196 — same-head manifest
|
|
1552
|
+
* updates never self-authorize), which `elective_rekey`'s own
|
|
1553
|
+
* completeness check then refuses on. If a directory principal is
|
|
1554
|
+
* unconfirmed when this vault needs to clear its migration requirement,
|
|
1555
|
+
* use `run402 repos access revoke-key`/`declare-exposure` instead (an
|
|
1556
|
+
* urgent reason, which admits with a nonempty partial target set) and
|
|
1557
|
+
* fold the pending receipt into THAT rotation.
|
|
1558
|
+
*/
|
|
1339
1559
|
async function accessRepair(args) {
|
|
1340
1560
|
const a = normalizeArgv(args);
|
|
1341
|
-
assertKnownFlags(a, [...
|
|
1342
|
-
requirePositionalCount(a,
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1561
|
+
assertKnownFlags(a, [...ROTATION_VALUE_FLAGS, "-v", "--verbose", "--help", "-h"], ROTATION_VALUE_FLAGS);
|
|
1562
|
+
requirePositionalCount(a, ROTATION_VALUE_FLAGS, { min: 0, max: 0, command: "run402 repos access repair", missing: "" });
|
|
1563
|
+
const recipientStateVersion = flagValue(a, "--recipient-state-version");
|
|
1564
|
+
const recipientRevocationVersion = flagValue(a, "--recipient-revocation-version");
|
|
1565
|
+
if (recipientStateVersion == null || recipientRevocationVersion == null) {
|
|
1566
|
+
fail({
|
|
1567
|
+
code: "ROTATION_COUNTERS_REQUIRED",
|
|
1568
|
+
message: "`run402 repos access repair` needs --recipient-state-version and --recipient-revocation-version — the gateway does not yet expose a read route for these two counters outside the key-revocation declare route.",
|
|
1569
|
+
hint: "If you know a specific principal whose key should be revoked, use `run402 repos access revoke-key <principal_id>` instead — it is fully self-contained (no flags needed). `access repair` is the general re-key for clearing stale_access / a first-ever migration and needs these two values from platform staff or direct DB access until a gateway read route ships.",
|
|
1570
|
+
next_actions: [nextAction("edit_request", { command: "run402 repos access revoke-key <principal_id>", why: "the ONE fully self-contained rotation entry point today — no counters needed" })],
|
|
1571
|
+
});
|
|
1572
|
+
}
|
|
1573
|
+
const sdk = getSdk();
|
|
1574
|
+
const target = await vaultTarget(a);
|
|
1575
|
+
try {
|
|
1576
|
+
const result = await sdk.gitvault.rotateEpoch({
|
|
1577
|
+
...target,
|
|
1578
|
+
reason: "elective_rekey",
|
|
1579
|
+
recipient_state_version: recipientStateVersion,
|
|
1580
|
+
recipient_revocation_version: recipientRevocationVersion,
|
|
1581
|
+
...(flagValue(a, "--idempotency-key") != null ? { client_idempotency_key: flagValue(a, "--idempotency-key") } : {}),
|
|
1582
|
+
});
|
|
1583
|
+
printJson(sdk, result);
|
|
1584
|
+
await spillIfLarge(result.rotation_id, "access-repair", result);
|
|
1585
|
+
console.error(`rotated to epoch ${result.new_epoch} at generation ${result.generation}: ${result.included.length} recipient(s) included, ${result.excluded_keyless_principal_ids.length} keyless, ${result.excluded_unconfirmed_principal_ids.length} unconfirmed.`);
|
|
1586
|
+
console.error(`self_check: ${result.self_check}${result.self_check === "not_a_recipient" ? " (this machine's own principal is not itself a vault recipient — nothing to self-verify)" : " (this machine's own opened envelope reproduced the committed epoch key)"}.`);
|
|
1587
|
+
printVerboseStats(a, sdk);
|
|
1588
|
+
} catch (err) {
|
|
1589
|
+
reportSdkError(err);
|
|
1590
|
+
}
|
|
1591
|
+
}
|
|
1592
|
+
|
|
1593
|
+
/**
|
|
1594
|
+
* `run402 repos access revoke-key <principal_id>` (D199) — the ONE fully
|
|
1595
|
+
* self-contained rotation entry point: declares
|
|
1596
|
+
* `reason:"recipient_key_revoked"` for `principal_id` (owner + step-up)
|
|
1597
|
+
* and drives the rotation off that declaration's OWN returned counters.
|
|
1598
|
+
* No flags needed — this is the reason value with a real, working
|
|
1599
|
+
* gateway-side counter read.
|
|
1600
|
+
*/
|
|
1601
|
+
async function accessRevokeKey(args) {
|
|
1602
|
+
const a = normalizeArgv(args);
|
|
1603
|
+
assertKnownFlags(a, [...COMMON_VALUE_FLAGS, "--idempotency-key", "-v", "--verbose", "--help", "-h"], [...COMMON_VALUE_FLAGS, "--idempotency-key"]);
|
|
1604
|
+
const [principalId] = requirePositionalCount(a, [...COMMON_VALUE_FLAGS, "--idempotency-key"], {
|
|
1605
|
+
min: 1, max: 1, command: "run402 repos access revoke-key <principal_id>",
|
|
1606
|
+
missing: "Missing <principal_id>. This is the principal whose current key should no longer be trusted — the next rotation excludes them from the new epoch.",
|
|
1348
1607
|
});
|
|
1608
|
+
const sdk = getSdk();
|
|
1609
|
+
const target = await vaultTarget(a);
|
|
1610
|
+
try {
|
|
1611
|
+
const result = await sdk.gitvault.rotateEpochForKeyRevocation(principalId, {
|
|
1612
|
+
...target,
|
|
1613
|
+
...(flagValue(a, "--idempotency-key") != null ? { client_idempotency_key: flagValue(a, "--idempotency-key") } : {}),
|
|
1614
|
+
});
|
|
1615
|
+
printJson(sdk, result);
|
|
1616
|
+
await spillIfLarge(result.rotation_id, "access-revoke-key", result);
|
|
1617
|
+
console.error(`declared ${principalId}'s key revoked and rotated to epoch ${result.new_epoch} at generation ${result.generation}: ${result.included.length} recipient(s) included going forward.`);
|
|
1618
|
+
console.error(`self_check: ${result.self_check}.`);
|
|
1619
|
+
printVerboseStats(a, sdk);
|
|
1620
|
+
} catch (err) {
|
|
1621
|
+
reportSdkError(err);
|
|
1622
|
+
}
|
|
1623
|
+
}
|
|
1624
|
+
|
|
1625
|
+
/**
|
|
1626
|
+
* `run402 repos access declare-exposure` (D199) — declares
|
|
1627
|
+
* `reason:"epoch_secret_exposed"` admissible for THIS vault (owner +
|
|
1628
|
+
* step-up), vault-scoped (one vault's leaked key is not evidence any
|
|
1629
|
+
* sibling vault is compromised). The DECLARATION itself is real and
|
|
1630
|
+
* self-contained; the FOLLOW-UP rotation it authorizes is NOT auto-run
|
|
1631
|
+
* here, because — same confirmed gap as `access repair` — the D194
|
|
1632
|
+
* counters it must be fenced against have no client-visible read for this
|
|
1633
|
+
* reason value either. This is the rekey remedy the exposed-key incident
|
|
1634
|
+
* needs: declare here, then rotate (via `--recipient-state-version`/
|
|
1635
|
+
* `--recipient-revocation-version` once known, e.g. from platform staff).
|
|
1636
|
+
*
|
|
1637
|
+
* **If a `/confirm`/`/repin` receipt is already pending** (a directory
|
|
1638
|
+
* principal was confirmed BEFORE this declaration, or gets confirmed while
|
|
1639
|
+
* the rotation is outstanding), do NOT call `publishPinManifestUpdate`
|
|
1640
|
+
* separately — that call is itself an ORDINARY admission and is itself
|
|
1641
|
+
* refused `EPOCH_ROTATION_REQUIRED` for as long as this declaration stays
|
|
1642
|
+
* outstanding (reproduced live in production 2026-08-27). Pass the receipt
|
|
1643
|
+
* to `r.gitvault.rotateEpoch({..., pending_confirmations: [{principal_id,
|
|
1644
|
+
* ek_fingerprint, receipt}]})` instead — it rides the SAME head as the
|
|
1645
|
+
* rotation this declaration requires, publishing durably without needing a
|
|
1646
|
+
* second, separately-gated admission. See `GitvaultVault.rotateEpoch`'s
|
|
1647
|
+
* doc comment for what this does NOT do: the folded principal is still
|
|
1648
|
+
* excluded from THIS rotation's own envelope set (D196) and becomes
|
|
1649
|
+
* eligible starting at the NEXT rotation.
|
|
1650
|
+
*/
|
|
1651
|
+
async function accessDeclareExposure(args) {
|
|
1652
|
+
const a = normalizeArgv(args);
|
|
1653
|
+
assertKnownFlags(a, [...COMMON_VALUE_FLAGS, "-v", "--verbose", "--help", "-h"], COMMON_VALUE_FLAGS);
|
|
1654
|
+
requirePositionalCount(a, COMMON_VALUE_FLAGS, { min: 0, max: 0, command: "run402 repos access declare-exposure", missing: "" });
|
|
1655
|
+
const target = await vaultTarget(a);
|
|
1656
|
+
try {
|
|
1657
|
+
const sdk = getSdk();
|
|
1658
|
+
const repoId = target.repo_id ?? (await sdk.gitvault.forProject(target.project_id)).repo_id;
|
|
1659
|
+
const result = await sdk.gitvault.declareEpochSecretExposed(repoId);
|
|
1660
|
+
printJson(sdk, result);
|
|
1661
|
+
console.error(`declared epoch_secret_exposed for ${repoId} (epoch_secret_exposure_version now ${result.epoch_secret_exposure_version}).`);
|
|
1662
|
+
console.error("THIS DECLARATION DOES NOT ROTATE THE VAULT BY ITSELF — the next ordinary push now refuses EPOCH_ROTATION_REQUIRED until a rotate_epoch with reason:\"epoch_secret_exposed\" commits.");
|
|
1663
|
+
console.error("submit that rotation via r.gitvault.rotateEpoch({repo_id, reason: \"epoch_secret_exposed\", recipient_state_version, recipient_revocation_version}) once you have the two counter values (no CLI shortcut exists for this reason yet — see `run402 repos access repair --help`).");
|
|
1664
|
+
console.error("if a /confirm or /repin receipt is already pending for a directory principal, do NOT publish it separately (publishPinManifestUpdate is itself gated the same way) — pass it as rotateEpoch's pending_confirmations instead so it rides the SAME head as this rotation.");
|
|
1665
|
+
printVerboseStats(a, sdk);
|
|
1666
|
+
} catch (err) {
|
|
1667
|
+
reportSdkError(err);
|
|
1668
|
+
}
|
|
1349
1669
|
}
|
|
1350
1670
|
|
|
1351
1671
|
async function access(args) {
|
|
1352
1672
|
const a = normalizeArgv(args);
|
|
1353
1673
|
if (a[0] === "repair") return accessRepair(a.slice(1));
|
|
1674
|
+
if (a[0] === "revoke-key") return accessRevokeKey(a.slice(1));
|
|
1675
|
+
if (a[0] === "declare-exposure") return accessDeclareExposure(a.slice(1));
|
|
1354
1676
|
return accessRead(a);
|
|
1355
1677
|
}
|
|
1356
1678
|
|
|
1357
1679
|
// ─── recover ─────────────────────
|
|
1358
1680
|
|
|
1681
|
+
/** `repos recover --human`: the same verdict the stderr lines already carry, condensed into one block. */
|
|
1682
|
+
function formatRecoverHuman(result, outDir) {
|
|
1683
|
+
const lines = [
|
|
1684
|
+
`Repo: ${result.repo_id}`,
|
|
1685
|
+
`Recovered generation ${result.recovered_generation} into ${outDir}` +
|
|
1686
|
+
(result.chain_break ? ` (chain break at ${result.chain_break.generation} — fell back to the newest fully-verified generation)` : "") + ".",
|
|
1687
|
+
];
|
|
1688
|
+
if (result.data_loss_detected) {
|
|
1689
|
+
lines.push(`DATA LOSS DETECTED: ${result.absences.filter((x) => x.adjudication === "unexplained_absence").length} object(s) are unexplained absences.`);
|
|
1690
|
+
}
|
|
1691
|
+
lines.push(`Layout: ${result.layout}` + (result.layout === "bare" ? " (no working files — not a failed recovery)" : ""));
|
|
1692
|
+
return lines.join("\n");
|
|
1693
|
+
}
|
|
1694
|
+
|
|
1359
1695
|
async function recover(args) {
|
|
1360
1696
|
const a = normalizeArgv(args);
|
|
1361
1697
|
const valueFlags = ["--out", "--repo", "--profile", "--region", "--endpoint"];
|
|
1362
|
-
assertKnownFlags(a, [...valueFlags, "--ambient", "--help", "-h"], valueFlags);
|
|
1698
|
+
assertKnownFlags(a, [...valueFlags, "--ambient", "--human", "-v", "--verbose", "--help", "-h"], valueFlags);
|
|
1363
1699
|
const [source] = requirePositionalCount(a, valueFlags, {
|
|
1364
1700
|
min: 1, max: 1, command: "run402 repos recover <source> --out <dir>",
|
|
1365
1701
|
missing: "Missing <source>. Expected s3://<bucket>[/<prefix>] or a directory path.",
|
|
@@ -1368,19 +1704,31 @@ async function recover(args) {
|
|
|
1368
1704
|
if (outDir == null) {
|
|
1369
1705
|
fail({ code: "BAD_USAGE", message: "run402 repos recover needs --out <dir>.", hint: "Where to materialize the recovered repository, e.g. --out ./restored" });
|
|
1370
1706
|
}
|
|
1707
|
+
const human = a.includes("--human");
|
|
1708
|
+
if (human && a.includes("--json")) {
|
|
1709
|
+
fail({ code: "BAD_USAGE", message: "--human cannot be combined with --json.", details: { flags: a.filter((arg) => arg === "--human" || arg === "--json") } });
|
|
1710
|
+
}
|
|
1711
|
+
const sdk = getSdk();
|
|
1371
1712
|
const credential = resolveMirrorCredential(a);
|
|
1372
1713
|
const repoId = flagValue(a, "--repo");
|
|
1373
1714
|
const region = flagValue(a, "--region");
|
|
1374
1715
|
const endpoint = flagValue(a, "--endpoint");
|
|
1375
1716
|
try {
|
|
1376
|
-
const result = await
|
|
1717
|
+
const result = await sdk.gitvault.recover({
|
|
1377
1718
|
source, out_dir: outDir,
|
|
1378
1719
|
...(repoId != null ? { repo_id: repoId } : {}),
|
|
1379
1720
|
...(credential ? { credential } : {}),
|
|
1380
1721
|
...(region != null ? { region } : {}),
|
|
1381
1722
|
...(endpoint != null ? { endpoint } : {}),
|
|
1382
1723
|
});
|
|
1383
|
-
|
|
1724
|
+
if (human) {
|
|
1725
|
+
console.log(formatRecoverHuman(result, outDir));
|
|
1726
|
+
if (result.layout === "bare") for (const n of result.next_actions ?? []) console.error(`next: ${n.action} — ${n.command}`);
|
|
1727
|
+
printMirrorHonesty(result);
|
|
1728
|
+
printVerboseStats(a, sdk);
|
|
1729
|
+
return;
|
|
1730
|
+
}
|
|
1731
|
+
printJson(sdk, result);
|
|
1384
1732
|
await spillIfLarge(result.repo_id, "recover", result);
|
|
1385
1733
|
console.error(`recovered generation ${result.recovered_generation} for ${result.repo_id} into ${outDir}` + (result.chain_break ? ` (chain break at ${result.chain_break.generation} — fell back to the newest fully-verified generation)` : "") + ".");
|
|
1386
1734
|
if (result.data_loss_detected) {
|
|
@@ -1391,6 +1739,7 @@ async function recover(args) {
|
|
|
1391
1739
|
for (const n of result.next_actions ?? []) console.error(`next: ${n.action} — ${n.command}`);
|
|
1392
1740
|
}
|
|
1393
1741
|
printMirrorHonesty(result);
|
|
1742
|
+
printVerboseStats(a, sdk);
|
|
1394
1743
|
} catch (err) {
|
|
1395
1744
|
reportSdkError(err);
|
|
1396
1745
|
}
|