run402 4.24.0 → 4.25.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (114) hide show
  1. package/cli.mjs +6 -6
  2. package/git-remote-run402.mjs +270 -0
  3. package/lib/command-manifest.mjs +11 -1
  4. package/lib/credentials.mjs +4 -0
  5. package/lib/domains.mjs +0 -14
  6. package/lib/gitvault.mjs +370 -0
  7. package/lib/init.mjs +143 -1
  8. package/lib/pay.test.mjs +0 -4
  9. package/package.json +8 -2
  10. package/sdk/dist/index.d.ts +20 -3
  11. package/sdk/dist/index.d.ts.map +1 -1
  12. package/sdk/dist/index.js +22 -3
  13. package/sdk/dist/index.js.map +1 -1
  14. package/sdk/dist/namespaces/admin.d.ts +0 -5
  15. package/sdk/dist/namespaces/admin.d.ts.map +1 -1
  16. package/sdk/dist/namespaces/admin.js +0 -9
  17. package/sdk/dist/namespaces/admin.js.map +1 -1
  18. package/sdk/dist/namespaces/assets.d.ts +1 -20
  19. package/sdk/dist/namespaces/assets.d.ts.map +1 -1
  20. package/sdk/dist/namespaces/assets.js +0 -27
  21. package/sdk/dist/namespaces/assets.js.map +1 -1
  22. package/sdk/dist/namespaces/auth.js +1 -1
  23. package/sdk/dist/namespaces/auth.js.map +1 -1
  24. package/sdk/dist/namespaces/domains.d.ts +0 -19
  25. package/sdk/dist/namespaces/domains.d.ts.map +1 -1
  26. package/sdk/dist/namespaces/domains.js +0 -19
  27. package/sdk/dist/namespaces/domains.js.map +1 -1
  28. package/sdk/dist/namespaces/gitvault.crypto.d.ts +319 -0
  29. package/sdk/dist/namespaces/gitvault.crypto.d.ts.map +1 -0
  30. package/sdk/dist/namespaces/gitvault.crypto.js +690 -0
  31. package/sdk/dist/namespaces/gitvault.crypto.js.map +1 -0
  32. package/sdk/dist/namespaces/gitvault.d.ts +473 -0
  33. package/sdk/dist/namespaces/gitvault.d.ts.map +1 -0
  34. package/sdk/dist/namespaces/gitvault.js +714 -0
  35. package/sdk/dist/namespaces/gitvault.js.map +1 -0
  36. package/sdk/dist/namespaces/gitvault.types.d.ts +481 -0
  37. package/sdk/dist/namespaces/gitvault.types.d.ts.map +1 -0
  38. package/sdk/dist/namespaces/gitvault.types.js +19 -0
  39. package/sdk/dist/namespaces/gitvault.types.js.map +1 -0
  40. package/sdk/dist/namespaces/org.d.ts +1 -3
  41. package/sdk/dist/namespaces/org.d.ts.map +1 -1
  42. package/sdk/dist/namespaces/org.js +6 -10
  43. package/sdk/dist/namespaces/org.js.map +1 -1
  44. package/sdk/dist/namespaces/pay.d.ts +0 -8
  45. package/sdk/dist/namespaces/pay.d.ts.map +1 -1
  46. package/sdk/dist/namespaces/pay.js.map +1 -1
  47. package/sdk/dist/namespaces/projects.d.ts +2 -2
  48. package/sdk/dist/namespaces/projects.d.ts.map +1 -1
  49. package/sdk/dist/namespaces/projects.js +4 -12
  50. package/sdk/dist/namespaces/projects.js.map +1 -1
  51. package/sdk/dist/namespaces/secrets.d.ts +0 -2
  52. package/sdk/dist/namespaces/secrets.d.ts.map +1 -1
  53. package/sdk/dist/namespaces/secrets.js +3 -10
  54. package/sdk/dist/namespaces/secrets.js.map +1 -1
  55. package/sdk/dist/namespaces/subdomains.d.ts +0 -2
  56. package/sdk/dist/namespaces/subdomains.d.ts.map +1 -1
  57. package/sdk/dist/namespaces/subdomains.js +5 -16
  58. package/sdk/dist/namespaces/subdomains.js.map +1 -1
  59. package/sdk/dist/namespaces/transfers.d.ts +0 -2
  60. package/sdk/dist/namespaces/transfers.d.ts.map +1 -1
  61. package/sdk/dist/namespaces/transfers.js +8 -10
  62. package/sdk/dist/namespaces/transfers.js.map +1 -1
  63. package/sdk/dist/namespaces/wallets.d.ts +0 -12
  64. package/sdk/dist/namespaces/wallets.d.ts.map +1 -1
  65. package/sdk/dist/namespaces/wallets.js +0 -14
  66. package/sdk/dist/namespaces/wallets.js.map +1 -1
  67. package/sdk/dist/node/gitvault-creation-journal.d.ts +222 -0
  68. package/sdk/dist/node/gitvault-creation-journal.d.ts.map +1 -0
  69. package/sdk/dist/node/gitvault-creation-journal.js +365 -0
  70. package/sdk/dist/node/gitvault-creation-journal.js.map +1 -0
  71. package/sdk/dist/node/gitvault-deploy.d.ts +211 -0
  72. package/sdk/dist/node/gitvault-deploy.d.ts.map +1 -0
  73. package/sdk/dist/node/gitvault-deploy.js +292 -0
  74. package/sdk/dist/node/gitvault-deploy.js.map +1 -0
  75. package/sdk/dist/node/gitvault-keystore.d.ts +207 -0
  76. package/sdk/dist/node/gitvault-keystore.d.ts.map +1 -0
  77. package/sdk/dist/node/gitvault-keystore.js +529 -0
  78. package/sdk/dist/node/gitvault-keystore.js.map +1 -0
  79. package/sdk/dist/node/gitvault-prune.d.ts +341 -0
  80. package/sdk/dist/node/gitvault-prune.d.ts.map +1 -0
  81. package/sdk/dist/node/gitvault-prune.js +410 -0
  82. package/sdk/dist/node/gitvault-prune.js.map +1 -0
  83. package/sdk/dist/node/gitvault-publication.d.ts +730 -0
  84. package/sdk/dist/node/gitvault-publication.d.ts.map +1 -0
  85. package/sdk/dist/node/gitvault-publication.js +1475 -0
  86. package/sdk/dist/node/gitvault-publication.js.map +1 -0
  87. package/sdk/dist/node/gitvault-snapshot.d.ts +190 -0
  88. package/sdk/dist/node/gitvault-snapshot.d.ts.map +1 -0
  89. package/sdk/dist/node/gitvault-snapshot.js +505 -0
  90. package/sdk/dist/node/gitvault-snapshot.js.map +1 -0
  91. package/sdk/dist/node/index.d.ts +11 -0
  92. package/sdk/dist/node/index.d.ts.map +1 -1
  93. package/sdk/dist/node/index.js +9 -0
  94. package/sdk/dist/node/index.js.map +1 -1
  95. package/sdk/dist/node/paid-fetch.d.ts.map +1 -1
  96. package/sdk/dist/node/paid-fetch.js +0 -4
  97. package/sdk/dist/node/paid-fetch.js.map +1 -1
  98. package/sdk/dist/node/sites-node.d.ts +1 -8
  99. package/sdk/dist/node/sites-node.d.ts.map +1 -1
  100. package/sdk/dist/node/sites-node.js +1 -7
  101. package/sdk/dist/node/sites-node.js.map +1 -1
  102. package/sdk/dist/scoped.d.ts +4 -35
  103. package/sdk/dist/scoped.d.ts.map +1 -1
  104. package/sdk/dist/scoped.js +9 -74
  105. package/sdk/dist/scoped.js.map +1 -1
  106. package/lib/sender-domain.mjs +0 -36
  107. package/sdk/dist/deprecate.d.ts +0 -23
  108. package/sdk/dist/deprecate.d.ts.map +0 -1
  109. package/sdk/dist/deprecate.js +0 -42
  110. package/sdk/dist/deprecate.js.map +0 -1
  111. package/sdk/dist/namespaces/sender-domain.d.ts +0 -47
  112. package/sdk/dist/namespaces/sender-domain.d.ts.map +0 -1
  113. package/sdk/dist/namespaces/sender-domain.js +0 -45
  114. package/sdk/dist/namespaces/sender-domain.js.map +0 -1
@@ -0,0 +1,370 @@
1
+ /**
2
+ * `run402 gitvault` — the host-blind encrypted Git remote (wire tag `r402s/v0`).
3
+ *
4
+ * ARCHITECTURAL LAW (gitvault-client-surface, "All protocol logic lives in the
5
+ * SDK"): every piece of vault protocol behaviour — crypto core, keystore,
6
+ * creation journal, snapshot + capture, publication state machines, ref
7
+ * transactions, verification budget, token exchange, repair — lives ONCE in
8
+ * `@run402/sdk` under `r.gitvault`. This module is a THIN ADAPTER: argument
9
+ * parsing, TTY output, exit codes, local file I/O. It adds zero protocol
10
+ * behaviour, and imports only the SDK (via `./sdk.mjs`) plus the CLI's own
11
+ * argument/error helpers — never a crypto, HTTP, or git library.
12
+ *
13
+ * Pipe contract (docs/style.md): the payload is JSON on stdout; every human
14
+ * line — progress, the terminal-loss statement, advisories — goes to stderr, so
15
+ * `run402 gitvault status | jq` stays clean.
16
+ *
17
+ * Run these from inside the git working tree: `repo_dir` is `process.cwd()`.
18
+ */
19
+ import { readFileSync } from "node:fs";
20
+ import { resolveProjectId } from "./config.mjs";
21
+ import { getSdk } from "./sdk.mjs";
22
+ import { reportSdkError, fail } from "./sdk-errors.mjs";
23
+ import {
24
+ normalizeArgv,
25
+ hasHelp,
26
+ assertKnownFlags,
27
+ parseIntegerFlag,
28
+ flagValue,
29
+ requirePositionalCount,
30
+ failUnknownSubcommand,
31
+ } from "./argparse.mjs";
32
+
33
+ /** Value-taking flags every gitvault subcommand accepts. */
34
+ const COMMON_VALUE_FLAGS = ["--project", "--repo"];
35
+
36
+ export const HELP = `run402 gitvault — your source, encrypted before it leaves the machine
37
+
38
+ Usage:
39
+ run402 gitvault status [--project <id>] [--repo <repo_id>]
40
+ run402 gitvault push [--project <id>] [--repo <repo_id>] [--message <text>] [--checkpoint]
41
+ run402 gitvault compact [--project <id>] [--repo <repo_id>]
42
+ run402 gitvault prune [--project <id>] [--repo <repo_id>]
43
+ [--submit --intent-core <path> --verifier-receipt <path> [--wait]]
44
+ run402 gitvault verify [--project <id>] [--repo <repo_id>] [--budget <n>]
45
+
46
+ Subcommands:
47
+ status What this machine and the control plane each believe about the
48
+ vault: allocation, policy, whether this keystore can sign, the
49
+ authenticated and materialized pins, and any pending
50
+ unvaulted-override journals. Never reports key material.
51
+ push Capture the working tree and publish it. This is NOT gated on a
52
+ deploy — a vault-only project pushes for months without one.
53
+ Before reporting a push as landed the SDK compares finalization
54
+ receipts against the expected manifest and reads the admitted head
55
+ back from storage; a 200 alone is never enough.
56
+ compact Publish a checkpoint covering the canonical refs, every root
57
+ unexpired at the cutoff, and the HEAD target, under a maintenance
58
+ lease so a concurrent cycle cannot race it.
59
+ prune Plan a prune, and — with both verifier receipts — submit it.
60
+ Two phases, because the protocol is two-phase; see below.
61
+ verify Verify the head chain from the authenticated pin up to the newest
62
+ listed generation. Fails closed on a regression, a gap, or a
63
+ transition descriptor this client cannot validate.
64
+
65
+ Options:
66
+ --project <id> Project whose vault to act on (defaults to the active project)
67
+ --repo <repo_id> Address the vault directly by id, skipping project lookup
68
+ --message <text> push: commit message for the synthetic commit a dirty tree
69
+ produces (a clean tree pushes HEAD itself, no message used)
70
+ --checkpoint push: force the checkpoint-bearing form regardless of delta size
71
+ --budget <n> verify: heads to verify in this call. The verified prefix is
72
+ persisted, so a budget-exceeded run resumes where it stopped
73
+ instead of restarting.
74
+ --submit prune: submit the planned intent. Requires --intent-core and
75
+ --verifier-receipt.
76
+ --intent-core <path>
77
+ prune: the plan's \`intent_core\`, saved verbatim from a prior
78
+ planning run. A rebuilt core carries a different nonce, so
79
+ the r402s-verify receipt would no longer bind to it.
80
+ --verifier-receipt <path>
81
+ prune: r402s-verify's \`verifier_receipt\` over that core.
82
+ --wait prune: poll the submitted intent until the control-plane-
83
+ signed completion appears, instead of returning immediately.
84
+ --json No-op: stdout is already JSON.
85
+
86
+ prune is TWO PHASES, because the protocol is:
87
+ 1. \`run402 gitvault prune\` plans. It walks the verified chain, computes the GC
88
+ root set, subtracts it, and prints a SIGNED \`intent_core\` plus its
89
+ \`intent_core_sha256\`. Nothing is submitted and nothing is deleted.
90
+ 2. Run \`r402s-verify\` against that core, then re-run with
91
+ \`--submit --intent-core <core.json> --verifier-receipt <receipt.json>\`.
92
+ The intent carries TWO receipts over the same core, one per implementation:
93
+ this CLI produces the \`run402-cli\` half by restoring the latest checkpoint
94
+ and recomputing its commitments, and \`r402s-verify\` produces the other.
95
+ A second receipt from this lineage would prove nothing, so it is never
96
+ synthesized here.
97
+ Only the control-plane-signed completion says what was deleted, and only its
98
+ \`deleted\` result means the bytes are gone — \`present_after_attempt\` is a
99
+ FAILED deletion, never counted as a success. There is deliberately no purge
100
+ verb in V0 at all. Retention is an operational promise of the platform, not a
101
+ cryptographic guarantee against it.
102
+
103
+ Expiry is permissive, by design:
104
+ A retention root whose \`effective_admitted_at\` this client cannot resolve is
105
+ RETAINED, and a compact that cannot obtain a retention-cutoff ticket keeps
106
+ every root. That costs storage, never history.
107
+
108
+ Terminal loss (protocol §0):
109
+ In V0-A, whole-machine or whole-keystore loss is terminal for vault history
110
+ until human envelopes ship. \`status\` prints the full statement verbatim on
111
+ stderr and carries it in its JSON — read it before you rely on this.
112
+
113
+ Examples:
114
+ run402 gitvault status
115
+ run402 gitvault push --message "wip: refactor the parser"
116
+ run402 gitvault verify --budget 500
117
+ run402 gitvault prune --project prj_1a2b3c
118
+ `;
119
+
120
+ /**
121
+ * Resolve which vault to act on, plus the local git tree.
122
+ *
123
+ * `--repo` addresses the vault directly (the cold-restart path: an agent that
124
+ * knows its repo_id needs no project lookup). Otherwise the project is
125
+ * resolved the CLI-wide way — `--project`, then RUN402_PROJECT_ID, then the
126
+ * active project — and the SDK resolves the vault from it.
127
+ */
128
+ function vaultTarget(a) {
129
+ const repoId = flagValue(a, "--repo");
130
+ const project = flagValue(a, "--project");
131
+ const target = { repo_dir: process.cwd() };
132
+ if (repoId != null) target.repo_id = repoId;
133
+ // Only demand a project when one is actually needed: `--repo` alone is a
134
+ // complete address, and requiring an active project on top of it would make
135
+ // the cold-restart path fail for no reason.
136
+ if (repoId == null || project != null) target.project_id = resolveProjectId(project);
137
+ return target;
138
+ }
139
+
140
+ /**
141
+ * Print the protocol §0 terminal-loss statement.
142
+ *
143
+ * NORMATIVE COPY, printed verbatim straight from the SDK's own constants and
144
+ * never paraphrased, summarized, or reassembled here. Both lines also ride in
145
+ * the JSON payload on stdout.
146
+ */
147
+ function printTerminalLoss(status) {
148
+ console.error("");
149
+ console.error(status.terminal_loss_statement);
150
+ console.error(status.terminal_loss_detail);
151
+ console.error("");
152
+ }
153
+
154
+ async function status(args) {
155
+ const a = normalizeArgv(args);
156
+ assertKnownFlags(a, [...COMMON_VALUE_FLAGS, "--help", "-h"], COMMON_VALUE_FLAGS);
157
+ requirePositionalCount(a, COMMON_VALUE_FLAGS, {
158
+ min: 0, max: 0, command: "run402 gitvault status", missing: "",
159
+ });
160
+ try {
161
+ const s = await getSdk().gitvault.status(vaultTarget(a));
162
+ console.log(JSON.stringify(s, null, 2));
163
+ printTerminalLoss(s);
164
+ // Advisories are echoed EXACTLY as the SDK reported them. Nothing is
165
+ // synthesized here — in particular a project that has never deployed gets
166
+ // no deploy-related warning, because a vault-only project is a first-class
167
+ // shape (protocol D183), not a half-configured deploy.
168
+ for (const w of s.warnings) console.error(`warning (${w.kind}): ${w.message}`);
169
+ for (const n of s.next_actions) console.error(`next: ${n.action}${n.command ? ` — ${n.command}` : ""}`);
170
+ } catch (err) {
171
+ reportSdkError(err);
172
+ }
173
+ }
174
+
175
+ async function push(args) {
176
+ const a = normalizeArgv(args);
177
+ const valueFlags = [...COMMON_VALUE_FLAGS, "--message"];
178
+ assertKnownFlags(a, [...valueFlags, "--checkpoint", "--help", "-h"], valueFlags);
179
+ requirePositionalCount(a, valueFlags, {
180
+ min: 0, max: 0, command: "run402 gitvault push", missing: "",
181
+ });
182
+ const message = flagValue(a, "--message");
183
+ const opts = {
184
+ ...vaultTarget(a),
185
+ // The gitvault_commit line is progress, not payload: print it the moment
186
+ // the snapshot exists, well before the publication round-trips finish, so
187
+ // a human watching a slow push sees what is being pushed.
188
+ onCommitLine: (line) => console.error(line),
189
+ };
190
+ // The message rides on `snapshot`, which is what `captureSnapshot` reads —
191
+ // and, since 5.12b removed the dead top-level `push({ message })` field, is
192
+ // the ONE place it can ride.
193
+ if (message != null) opts.snapshot = { message };
194
+ if (a.includes("--checkpoint")) opts.checkpoint = true;
195
+ try {
196
+ const result = await getSdk().gitvault.push(opts);
197
+ console.log(JSON.stringify(result, null, 2));
198
+ console.error(`published generation ${result.generation} (${result.form})`);
199
+ } catch (err) {
200
+ reportSdkError(err);
201
+ }
202
+ }
203
+
204
+ async function compact(args) {
205
+ const a = normalizeArgv(args);
206
+ assertKnownFlags(a, [...COMMON_VALUE_FLAGS, "--help", "-h"], COMMON_VALUE_FLAGS);
207
+ requirePositionalCount(a, COMMON_VALUE_FLAGS, {
208
+ min: 0, max: 0, command: "run402 gitvault compact", missing: "",
209
+ });
210
+ try {
211
+ const result = await getSdk().gitvault.compact(vaultTarget(a));
212
+ console.log(JSON.stringify(result, null, 2));
213
+ console.error(
214
+ `checkpoint published at generation ${result.generation}: ` +
215
+ `${result.covered_refs} ref(s), ${result.covered_roots} retention root(s).`,
216
+ );
217
+ if (!result.cutoff_bound) {
218
+ // Say what actually happened rather than reporting a clean compaction:
219
+ // without a ticket no root can leave the map, so this run reclaimed
220
+ // nothing from expiry.
221
+ console.error(
222
+ "no retention-cutoff ticket was obtained, so roots were RETAINED — expiry is permissive. " +
223
+ "The checkpoint published, but no expired root left the map; re-run compact once the ticket route answers.",
224
+ );
225
+ }
226
+ } catch (err) {
227
+ reportSdkError(err);
228
+ }
229
+ }
230
+
231
+ /** Read a protocol object a prior planning run (or r402s-verify) wrote to disk. */
232
+ function readJsonFile(flag, path) {
233
+ let text;
234
+ try {
235
+ text = readFileSync(path, "utf8");
236
+ } catch (err) {
237
+ fail({
238
+ code: "BAD_USAGE",
239
+ message: `${flag} ${path} could not be read: ${err?.message ?? String(err)}`,
240
+ hint: "Point it at the file a prior `run402 gitvault prune` (or r402s-verify) wrote.",
241
+ });
242
+ }
243
+ try {
244
+ return JSON.parse(text);
245
+ } catch (err) {
246
+ fail({
247
+ code: "BAD_USAGE",
248
+ message: `${flag} ${path} is not valid JSON: ${err?.message ?? String(err)}`,
249
+ hint: "Pass the file verbatim; do not reformat or re-serialize it.",
250
+ });
251
+ }
252
+ }
253
+
254
+ async function prune(args) {
255
+ const a = normalizeArgv(args);
256
+ const valueFlags = [...COMMON_VALUE_FLAGS, "--intent-core", "--verifier-receipt"];
257
+ assertKnownFlags(a, [...valueFlags, "--submit", "--wait", "--help", "-h"], valueFlags);
258
+ requirePositionalCount(a, valueFlags, {
259
+ min: 0, max: 0, command: "run402 gitvault prune", missing: "",
260
+ });
261
+ const submitting = a.includes("--submit");
262
+ const corePath = flagValue(a, "--intent-core");
263
+ const receiptPath = flagValue(a, "--verifier-receipt");
264
+ // Refuse the half-specified submit here rather than planning and silently
265
+ // discarding the flags — an agent that typed --submit meant to submit.
266
+ if (submitting && (corePath == null || receiptPath == null)) {
267
+ fail({
268
+ code: "BAD_USAGE",
269
+ message: "run402 gitvault prune --submit needs both --intent-core and --verifier-receipt.",
270
+ hint: "Plan first (`run402 gitvault prune`), save its `intent_core`, run r402s-verify against it, then submit both.",
271
+ });
272
+ }
273
+ if (!submitting && (corePath != null || receiptPath != null)) {
274
+ fail({
275
+ code: "BAD_USAGE",
276
+ message: "--intent-core / --verifier-receipt only apply with --submit.",
277
+ hint: "Add --submit, or drop the flags to plan.",
278
+ });
279
+ }
280
+ const opts = vaultTarget(a);
281
+ if (submitting) {
282
+ opts.submit = {
283
+ core: readJsonFile("--intent-core", corePath),
284
+ verifier_receipt: readJsonFile("--verifier-receipt", receiptPath),
285
+ };
286
+ if (a.includes("--wait")) opts.submit.wait = {};
287
+ }
288
+ try {
289
+ const result = await getSdk().gitvault.prune(opts);
290
+ console.log(JSON.stringify(result, null, 2));
291
+ // Never imply a deletion. State what actually happened, then reproduce the
292
+ // SDK's own note verbatim rather than summarizing it.
293
+ if (!result.submitted) {
294
+ console.error(
295
+ result.blocked_reason
296
+ ? `planned — nothing to submit: ${result.blocked_reason}`
297
+ : `planned — nothing submitted. ${result.object_candidates.length} object(s) proposed for deletion` +
298
+ `${result.deferred_object_count > 0 ? ` (${result.deferred_object_count} more deferred to a later intent)` : ""}` +
299
+ `; ${result.eligible_count} retention root(s) past their window, ${result.retained_count} retained.`,
300
+ );
301
+ if (result.intent_core_sha256) {
302
+ console.error(`intent_core_sha256: ${result.intent_core_sha256} — run r402s-verify against this core, then re-run with --submit.`);
303
+ }
304
+ } else if (result.confirmation?.outcome) {
305
+ console.error(
306
+ `submitted — the signed completion reports ${result.confirmation.deleted.length} deleted, ` +
307
+ `${result.confirmation.present.length} still present` +
308
+ `${result.confirmation.unadjudicated.length > 0 ? `, ${result.confirmation.unadjudicated.length} unadjudicated` : ""}.`,
309
+ );
310
+ } else {
311
+ 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.");
312
+ }
313
+ console.error(result.note);
314
+ } catch (err) {
315
+ reportSdkError(err);
316
+ }
317
+ }
318
+
319
+ async function verify(args) {
320
+ const a = normalizeArgv(args);
321
+ const valueFlags = [...COMMON_VALUE_FLAGS, "--budget"];
322
+ assertKnownFlags(a, [...valueFlags, "--help", "-h"], valueFlags);
323
+ requirePositionalCount(a, valueFlags, {
324
+ min: 0, max: 0, command: "run402 gitvault verify", missing: "",
325
+ });
326
+ const target = vaultTarget(a);
327
+ const budget = flagValue(a, "--budget");
328
+ if (budget != null) target.verification_budget = parseIntegerFlag("--budget", budget, { min: 1 });
329
+ try {
330
+ const state = await getSdk().gitvault.verify(target);
331
+ console.log(JSON.stringify(state, null, 2));
332
+ console.error(`verified through generation ${state.generation}`);
333
+ } catch (err) {
334
+ reportSdkError(err);
335
+ }
336
+ }
337
+
338
+ export async function run(sub, args) {
339
+ const argv = Array.isArray(args) ? args : [];
340
+ if (!sub || hasHelp([sub, ...argv])) {
341
+ console.log(HELP);
342
+ process.exit(0);
343
+ }
344
+ switch (sub) {
345
+ case "status": {
346
+ await status(argv);
347
+ break;
348
+ }
349
+ case "push": {
350
+ await push(argv);
351
+ break;
352
+ }
353
+ case "compact": {
354
+ await compact(argv);
355
+ break;
356
+ }
357
+ case "prune": {
358
+ await prune(argv);
359
+ break;
360
+ }
361
+ case "verify": {
362
+ await verify(argv);
363
+ break;
364
+ }
365
+ default:
366
+ failUnknownSubcommand("gitvault", sub, {
367
+ hint: "Run `run402 gitvault --help` for usage.",
368
+ });
369
+ }
370
+ }
package/lib/init.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { readAllowance, saveAllowance, loadKeyStore, configDir, configureApiBase } from "./config.mjs";
1
+ import { readAllowance, saveAllowance, loadKeyStore, configDir, configureApiBase, getActiveProjectId, getProject, updateProject } from "./config.mjs";
2
2
  import { getSdk } from "./sdk.mjs";
3
3
  import { fail } from "./sdk-errors.mjs";
4
4
  import { setTierAction, deployAction } from "./next-actions.mjs";
@@ -37,12 +37,20 @@ Options:
37
37
  --switch-rail Confirm switching the persisted payment rail. Re-running
38
38
  init with the SAME rail as the existing allowance is always
39
39
  idempotent and does not need this flag.
40
+ --git-remote Also 'git init' the current directory when it is not a
41
+ repository yet, so the gitvault remote can be added there.
42
+ Opt-in on purpose: init is often run outside a project
43
+ directory and must never create a repository somewhere you
44
+ did not ask it to. Inside an EXISTING repository the remote
45
+ is added without this flag (see below).
40
46
 
41
47
  Output:
42
48
  Stdout is a JSON summary { config_dir, wallet, rail, network, balances,
43
49
  tier, projects_saved, next_step }. Progress lines (Config / Allowance /
44
50
  Balance / Tier / Next) go to stderr so a human re-running interactively
45
51
  sees what's happening while a script piping stdout to jq stays clean.
52
+ With --git-remote the summary also carries { gitvault } (the scaffolded
53
+ remote) or { gitvault: null, gitvault_error } when it could not be added.
46
54
 
47
55
  Steps (idempotent when re-run with the same rail; pass --switch-rail to change rails):
48
56
  1. Creates config directory (~/.config/run402)
@@ -123,6 +131,17 @@ function parseVoucherFlag(args) {
123
131
  return { value: null, args };
124
132
  }
125
133
 
134
+ /**
135
+ * Pull the boolean `--git-remote` out of argv before the rail/positional logic
136
+ * runs, so `run402 init --git-remote mpp` still selects the mpp rail (the same
137
+ * reason `--voucher` is stripped first).
138
+ */
139
+ function parseGitRemoteFlag(args) {
140
+ const idx = args.indexOf("--git-remote");
141
+ if (idx === -1) return { value: false, args };
142
+ return { value: true, args: [...args.slice(0, idx), ...args.slice(idx + 1)] };
143
+ }
144
+
126
145
  function sameOrigin(a, b) {
127
146
  try {
128
147
  return new URL(a).origin === new URL(b).origin;
@@ -154,6 +173,45 @@ function errorMessage(err) {
154
173
  return err?.message || String(err);
155
174
  }
156
175
 
176
+ /**
177
+ * The owning org of the locally-active project — from the KEYSTORE first, and
178
+ * only then from the named inventory.
179
+ *
180
+ * Why the cache exists (task 5.12c): the gitvault scaffold is the one part of
181
+ * `run402 init` the client-surface spec says adds no network dependency to the
182
+ * cold-start path, and resolving the org through `projects.list()` quietly made
183
+ * that untrue. `org_id` is a non-secret routing identifier the control plane
184
+ * already hands back with every listing, so the honest fix is to remember it:
185
+ * the FIRST init on a machine still asks, every RETURNING init reads it locally
186
+ * and the scaffold really is network-free.
187
+ *
188
+ * Best-effort by construction — the caller treats `null` as "do not scaffold" —
189
+ * and an EXACT id match is required: a near-miss must never make init add a
190
+ * remote pointing at somebody else's project. `updateProject` is a no-op for a
191
+ * project this machine holds no credentials for, so such a project asks again
192
+ * next time rather than being silently mis-cached.
193
+ */
194
+ async function resolveOwningOrgId(projectId) {
195
+ const cached = getProject(projectId)?.org_id;
196
+ if (typeof cached === "string" && cached.length > 0) return cached;
197
+ try {
198
+ const listed = await getSdk().projects.list();
199
+ const rows = Array.isArray(listed?.projects) ? listed.projects : [];
200
+ const row = rows.find((p) => (p?.id ?? p?.project_id) === projectId);
201
+ const orgId = row?.org_id;
202
+ if (typeof orgId !== "string" || orgId.length === 0) return null;
203
+ try {
204
+ updateProject(projectId, { org_id: orgId });
205
+ } catch {
206
+ // Caching is an optimization; a read-only or contended keystore costs a
207
+ // round trip next time and must never fail the scaffold.
208
+ }
209
+ return orgId;
210
+ } catch {
211
+ return null;
212
+ }
213
+ }
214
+
157
215
  export async function run(args = []) {
158
216
  // Capability `astro-ssr-runtime` (v1.52): scaffold an Astro project.
159
217
  // Sub-routes when first positional is 'astro'. Handle BEFORE the
@@ -177,6 +235,10 @@ export async function run(args = []) {
177
235
  args = parsedVoucher.args;
178
236
  const voucherCode = parsedVoucher.value;
179
237
 
238
+ const parsedGitRemote = parseGitRemoteFlag(args);
239
+ args = parsedGitRemote.args;
240
+ const scaffoldGitRemote = parsedGitRemote.value;
241
+
180
242
  const parsedApiBase = parseApiBaseFlag(args);
181
243
  if (parsedApiBase.value) {
182
244
  if (parsedApiBase.args.some((arg) => typeof arg === "string" && !arg.startsWith("--"))) {
@@ -197,6 +259,16 @@ export async function run(args = []) {
197
259
  hint: "Configure the target first (`run402 init --api-base=…`), then redeem against Run402 Cloud with `run402 redeem <code>`.",
198
260
  });
199
261
  }
262
+ // Same reasoning as --voucher: this branch configures a target and returns
263
+ // without an active project, so a git scaffold has nothing to point at.
264
+ // Say so rather than accepting the flag and silently dropping it.
265
+ if (scaffoldGitRemote) {
266
+ fail({
267
+ code: "BAD_USAGE",
268
+ message: "run402 init --api-base cannot be combined with --git-remote.",
269
+ hint: "Configure the target first (`run402 init --api-base=…`), provision a project, then run `run402 init --git-remote` from the project directory.",
270
+ });
271
+ }
200
272
  const CONFIG_DIR = configDir();
201
273
  const detected = await detectTarget(parsedApiBase.value);
202
274
  const config = configureApiBase(parsedApiBase.value, {
@@ -479,6 +551,76 @@ export async function run(args = []) {
479
551
  summary.projects_saved = Object.keys(store.projects).length;
480
552
  line("Projects", `${summary.projects_saved} saved`);
481
553
 
554
+ // 5b. gitvault git remote (gitvault-client-surface, task 5.7).
555
+ //
556
+ // Purely LOCAL git. No vault is allocated and no key material is written
557
+ // here — the spec is explicit that neither exists until first capture, so the
558
+ // cold-start path gains no prompt and no new failure mode. Allocation happens
559
+ // on the first `run402 gitvault push` (or deploy).
560
+ //
561
+ // Adding the remote is the DEFAULT inside a repository that already exists,
562
+ // because it is pure addition: `origin` is never modified or claimed, no file
563
+ // is created, nothing is rewritten. CREATING a repository is NOT the default
564
+ // — `run402 init` is routinely run outside a project directory, and
565
+ // `git init`-ing whatever directory the user happened to be in would be a
566
+ // genuinely bad surprise. `--git-remote` opts into that one step.
567
+ //
568
+ // NON-FATAL in every branch: a missing git, a directory that is not a
569
+ // repository, an unreachable gateway, or a `run402` remote already pointing
570
+ // somewhere else must warn and let setup finish.
571
+ const activeProjectId = getActiveProjectId();
572
+ if (activeProjectId) {
573
+ summary.gitvault = null;
574
+ try {
575
+ // Dynamic import: the scaffold is the only thing here that needs the
576
+ // Node SDK's hardened git runner, and a top-level import would drag it
577
+ // into every init invocation (and every test that mocks ./sdk.mjs).
578
+ const { hardenedGit } = await import("#sdk/node");
579
+ let insideRepo = true;
580
+ try {
581
+ await hardenedGit(process.cwd(), ["rev-parse", "--git-dir"]);
582
+ } catch {
583
+ insideRepo = false;
584
+ }
585
+ if (!insideRepo && !scaffoldGitRemote) {
586
+ summary.gitvault_skipped = "not a git repository — re-run with --git-remote to create one and add the remote";
587
+ line("Gitvault", "skipped — not a git repository (--git-remote creates one)");
588
+ } else {
589
+ const orgId = await resolveOwningOrgId(activeProjectId);
590
+ if (!orgId) {
591
+ summary.gitvault_skipped = `could not resolve the owning org for ${activeProjectId} — the run402 remote was not added`;
592
+ line("Gitvault", "skipped — owning org unresolved");
593
+ } else {
594
+ const remote = await getSdk().gitvault.scaffoldRemote({
595
+ repo_dir: process.cwd(),
596
+ org_id: orgId,
597
+ project_id: activeProjectId,
598
+ });
599
+ // `allocated: false` is stated, not left to be inferred: this was
600
+ // local git only, and no vault exists for the project yet.
601
+ summary.gitvault = { ...remote, allocated: false };
602
+ if (remote.already_present && remote.existing_url !== remote.url) {
603
+ // Left exactly as it was. Name the URL that is actually in place
604
+ // rather than implying the remote now points at this project.
605
+ line("Gitvault", `remote '${remote.name}' already points at ${remote.existing_url} — left unchanged`);
606
+ } else if (remote.already_present) {
607
+ line("Gitvault", `remote '${remote.name}' already set (${remote.url})`);
608
+ } else {
609
+ line("Gitvault", `${remote.created_repository ? "initialized a repository and added" : "added"} remote '${remote.name}' -> ${remote.url}`);
610
+ }
611
+ }
612
+ }
613
+ } catch (err) {
614
+ const reason = errorMessage(err);
615
+ summary.gitvault = null;
616
+ summary.gitvault_error = {
617
+ code: err?.body?.code ?? err?.code ?? "GITVAULT_SCAFFOLD_FAILED",
618
+ message: reason,
619
+ };
620
+ line("Gitvault", `remote not added: ${reason}`);
621
+ }
622
+ }
623
+
482
624
  // 6. Next step — canonical typed action(s); `next_step` is the back-compat
483
625
  // string mirror of the first action's command (one spelling, surface-wide).
484
626
  write("");
package/lib/pay.test.mjs CHANGED
@@ -53,11 +53,7 @@ describe("run402 pay", () => {
53
53
  headers: { "content-type": "application/json" },
54
54
  }),
55
55
  payment: {
56
- amount_usd_micros: 10_000,
57
- pay_to: "0xseller",
58
56
  network: "eip155:8453",
59
- tx_ref: "0xtx",
60
- url,
61
57
  paymentId: "txp_cli_1",
62
58
  amountUsdMicros: 10_000,
63
59
  asset: "USDC",
package/package.json CHANGED
@@ -1,12 +1,14 @@
1
1
  {
2
2
  "name": "run402",
3
- "version": "4.24.0",
3
+ "version": "4.25.0",
4
4
  "description": "CLI for Run402 — provision Postgres databases, deploy static sites, generate images, and manage wallets via x402 and MPP micropayments.",
5
5
  "type": "module",
6
6
  "bin": {
7
- "run402": "cli.mjs"
7
+ "run402": "cli.mjs",
8
+ "git-remote-run402": "git-remote-run402.mjs"
8
9
  },
9
10
  "imports": {
11
+ "#sdk": "./sdk/dist/index.js",
10
12
  "#sdk/node": "./sdk/dist/node/index.js"
11
13
  },
12
14
  "scripts": {
@@ -14,13 +16,17 @@
14
16
  },
15
17
  "files": [
16
18
  "cli.mjs",
19
+ "git-remote-run402.mjs",
17
20
  "lib/",
18
21
  "fixtures/",
19
22
  "core-dist/",
20
23
  "sdk/"
21
24
  ],
22
25
  "dependencies": {
26
+ "@hpke/chacha20poly1305": "^1.8.0",
27
+ "@hpke/core": "^1.9.0",
23
28
  "@humanwhocodes/momoa": "3.3.10",
29
+ "@noble/ciphers": "^2.3.0",
24
30
  "@noble/curves": "^2.0.1",
25
31
  "@noble/hashes": "^2.0.1",
26
32
  "@scure/base": "1.2.6",
@@ -20,7 +20,6 @@ import { Tier } from "./namespaces/tier.js";
20
20
  import { Allowance } from "./namespaces/allowance.js";
21
21
  import { Ai } from "./namespaces/ai.js";
22
22
  import { Auth } from "./namespaces/auth.js";
23
- import { SenderDomain } from "./namespaces/sender-domain.js";
24
23
  import { Billing } from "./namespaces/billing.js";
25
24
  import { Vouchers } from "./namespaces/vouchers.js";
26
25
  import { Wallets, ScopedWallet } from "./namespaces/wallets.js";
@@ -46,6 +45,7 @@ import { Errors } from "./namespaces/errors.js";
46
45
  import { Pay, type PayExecutor } from "./namespaces/pay.js";
47
46
  import { IdentityLinks } from "./namespaces/identity-links.js";
48
47
  import { Buzz } from "./namespaces/buzz.js";
48
+ import { Gitvault } from "./namespaces/gitvault.js";
49
49
  import type { ContentSource, FileSet } from "./namespaces/deploy.types.js";
50
50
  import { ScopedRun402 } from "./scoped.js";
51
51
  export interface Run402Options {
@@ -86,7 +86,6 @@ export declare class Run402 {
86
86
  readonly ai: Ai;
87
87
  readonly image: Ai;
88
88
  readonly auth: Auth;
89
- readonly senderDomain: SenderDomain;
90
89
  readonly billing: Billing;
91
90
  readonly vouchers: Vouchers;
92
91
  readonly wallets: Wallets;
@@ -164,6 +163,20 @@ export declare class Run402 {
164
163
  readonly identityLinks: IdentityLinks;
165
164
  /** Buzz human adoption, community installation, and bounded agent enrollment workflows. */
166
165
  readonly buzz: Buzz;
166
+ /**
167
+ * gitvault — the host-blind encrypted Git remote (`r402s/v0`). Vault reads
168
+ * (record, heads listing, policy) run anywhere; the verbs that touch a git
169
+ * tree or the on-disk keystore (`init`, `push`, `compact`, `verify`,
170
+ * `deploy`, `restore`) are Node-only and load `@run402/sdk/node` lazily.
171
+ *
172
+ * This namespace is where ALL vault protocol logic lives: the CLI's
173
+ * `run402 gitvault …`, `git-remote-run402`, and the MCP tools are adapters
174
+ * over it and add no behaviour of their own.
175
+ *
176
+ * Durability is keystore-qualified: the vault protects source history from
177
+ * host-side loss while a principal keystore survives.
178
+ */
179
+ readonly gitvault: Gitvault;
167
180
  readonly idempotency: {
168
181
  fromParts: typeof fromParts;
169
182
  };
@@ -341,11 +354,15 @@ export { Buzz, BuzzHumanAdoptions, BuzzHumanAdoptionOffers, BuzzCommunityInstall
341
354
  export type * from "./namespaces/buzz.types.js";
342
355
  export type * from "./namespaces/buzz-notifications.types.js";
343
356
  export type * from "./namespaces/secrets.js";
344
- export type * from "./namespaces/sender-domain.js";
345
357
  export type * from "./namespaces/service.js";
346
358
  export type * from "./namespaces/sites.js";
347
359
  export type * from "./namespaces/subdomains.js";
348
360
  export type * from "./namespaces/tier.js";
349
361
  export { ScopedWallet } from "./namespaces/wallets.js";
350
362
  export type * from "./namespaces/wallets.js";
363
+ export * from "./namespaces/gitvault.crypto.js";
364
+ export type * from "./namespaces/gitvault.crypto.js";
365
+ export type * from "./namespaces/gitvault.types.js";
366
+ export { Gitvault, gitvaultRemoteUrl, parseGitvaultRemoteUrl } from "./namespaces/gitvault.js";
367
+ export type * from "./namespaces/gitvault.js";
351
368
  //# sourceMappingURL=index.d.ts.map