run402 4.39.0 → 4.40.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 (45) hide show
  1. package/cli.mjs +1 -1
  2. package/git-remote-run402.mjs +15 -1
  3. package/lib/assets.mjs +19 -26
  4. package/lib/cdn.mjs +4 -6
  5. package/lib/command-manifest.mjs +20 -0
  6. package/lib/config.mjs +30 -0
  7. package/lib/deploy-v2.mjs +15 -0
  8. package/lib/doctor.mjs +33 -0
  9. package/lib/gitvault.mjs +348 -1
  10. package/package.json +1 -1
  11. package/sdk/dist/namespaces/gitvault.crypto.d.ts +10 -0
  12. package/sdk/dist/namespaces/gitvault.crypto.d.ts.map +1 -1
  13. package/sdk/dist/namespaces/gitvault.crypto.js +10 -0
  14. package/sdk/dist/namespaces/gitvault.crypto.js.map +1 -1
  15. package/sdk/dist/namespaces/gitvault.d.ts +141 -4
  16. package/sdk/dist/namespaces/gitvault.d.ts.map +1 -1
  17. package/sdk/dist/namespaces/gitvault.js +226 -3
  18. package/sdk/dist/namespaces/gitvault.js.map +1 -1
  19. package/sdk/dist/node/gitvault-keystore.d.ts +11 -1
  20. package/sdk/dist/node/gitvault-keystore.d.ts.map +1 -1
  21. package/sdk/dist/node/gitvault-keystore.js.map +1 -1
  22. package/sdk/dist/node/gitvault-mirror-backend.d.ts +88 -0
  23. package/sdk/dist/node/gitvault-mirror-backend.d.ts.map +1 -0
  24. package/sdk/dist/node/gitvault-mirror-backend.js +374 -0
  25. package/sdk/dist/node/gitvault-mirror-backend.js.map +1 -0
  26. package/sdk/dist/node/gitvault-mirror-config.d.ts +53 -0
  27. package/sdk/dist/node/gitvault-mirror-config.d.ts.map +1 -0
  28. package/sdk/dist/node/gitvault-mirror-config.js +112 -0
  29. package/sdk/dist/node/gitvault-mirror-config.js.map +1 -0
  30. package/sdk/dist/node/gitvault-mirror.d.ts +120 -0
  31. package/sdk/dist/node/gitvault-mirror.d.ts.map +1 -0
  32. package/sdk/dist/node/gitvault-mirror.js +464 -0
  33. package/sdk/dist/node/gitvault-mirror.js.map +1 -0
  34. package/sdk/dist/node/gitvault-publication.d.ts +131 -0
  35. package/sdk/dist/node/gitvault-publication.d.ts.map +1 -1
  36. package/sdk/dist/node/gitvault-publication.js +145 -1
  37. package/sdk/dist/node/gitvault-publication.js.map +1 -1
  38. package/sdk/dist/node/gitvault-recover.d.ts +136 -0
  39. package/sdk/dist/node/gitvault-recover.d.ts.map +1 -0
  40. package/sdk/dist/node/gitvault-recover.js +412 -0
  41. package/sdk/dist/node/gitvault-recover.js.map +1 -0
  42. package/sdk/dist/node/gitvault-snapshot.d.ts +8 -0
  43. package/sdk/dist/node/gitvault-snapshot.d.ts.map +1 -1
  44. package/sdk/dist/node/gitvault-snapshot.js +11 -0
  45. package/sdk/dist/node/gitvault-snapshot.js.map +1 -1
package/cli.mjs CHANGED
@@ -84,7 +84,7 @@ PLATFORM — everything else, and the things still finding a home
84
84
  transfer Two-party project transfer (init, preview, list, accept, cancel)
85
85
  cloud Cloud portability archive export (archives create/download/status)
86
86
  archives Inspect and verify portable project archives locally
87
- gitvault Host-blind encrypted Git remote (init/status/push/policy/compact/prune/verify)
87
+ gitvault Host-blind encrypted Git remote (init/status/push/policy/compact/prune/verify/mirror/recover)
88
88
  buzz Buzz human/community/agent control-plane workflows
89
89
  apps Browse and manage the app marketplace
90
90
  ai AI translation and moderation tools
@@ -98,6 +98,7 @@
98
98
  * the set `git ls-remote <url>` outside a checkout needs.
99
99
  */
100
100
 
101
+ import { realpathSync } from "node:fs";
101
102
  import { createInterface } from "node:readline";
102
103
  import { pathToFileURL } from "node:url";
103
104
  import { getSdk } from "./lib/sdk.mjs";
@@ -666,7 +667,20 @@ async function main(argv) {
666
667
  // directly; without this guard that import would block on stdin forever).
667
668
  const invokedDirectly = (() => {
668
669
  try {
669
- return process.argv[1] !== undefined && import.meta.url === pathToFileURL(process.argv[1]).href;
670
+ if (process.argv[1] === undefined) return false;
671
+ // SYMLINK-SAFE, or every real install is dead (4.39.0 shipped without
672
+ // this and the helper silently no-opped for everyone): npm installs the
673
+ // bin as a SYMLINK (`/opt/homebrew/bin/git-remote-run402 -> ../lib/...`),
674
+ // and Node's ESM loader resolves `import.meta.url` through the symlink
675
+ // to the REAL file while `process.argv[1]` keeps the symlink path — a
676
+ // naive equality check therefore fails exactly and only in production,
677
+ // where `git push` then reads zero capabilities and aborts the session.
678
+ // Compare realpaths on both sides; a vanished argv[1] path falls through
679
+ // to the plain comparison rather than crashing the guard.
680
+ const argvReal = (() => {
681
+ try { return realpathSync(process.argv[1]); } catch { return process.argv[1]; }
682
+ })();
683
+ return import.meta.url === pathToFileURL(argvReal).href;
670
684
  } catch {
671
685
  return false;
672
686
  }
package/lib/assets.mjs CHANGED
@@ -28,7 +28,7 @@ import {
28
28
  import { basename, dirname, resolve as resolvePath } from "node:path";
29
29
  import { pipeline } from "node:stream/promises";
30
30
 
31
- import { resolveProjectId } from "./config.mjs";
31
+ import { resolveProjectIdAllowingLegacyEnv } from "./config.mjs";
32
32
  import { getSdk } from "./sdk.mjs";
33
33
  import { reportSdkError, fail } from "./sdk-errors.mjs";
34
34
  import { assertKnownFlags, hasHelp, normalizeArgv, parseIntegerFlag, failUnknownSubcommand } from "./argparse.mjs";
@@ -472,10 +472,9 @@ function computeDestKey(filePath, keyOpt) {
472
472
  return keyOpt;
473
473
  }
474
474
 
475
- async function put(projectId, argv) {
475
+ async function put(argv) {
476
476
  const opts = parseArgs(argv);
477
- opts.project = opts.project || projectId;
478
- const resolvedId = resolveProjectId(opts.project);
477
+ const resolvedId = resolveProjectIdAllowingLegacyEnv(opts.project);
479
478
 
480
479
  if (opts.positional.length === 0) die("At least one file path is required");
481
480
  if (opts.positional.length > 1 && opts.key && !opts.key.endsWith("/")) {
@@ -499,10 +498,9 @@ async function put(projectId, argv) {
499
498
  // get
500
499
  // ---------------------------------------------------------------------------
501
500
 
502
- async function get(projectId, argv) {
501
+ async function get(argv) {
503
502
  const opts = parseArgs(argv);
504
- opts.project = opts.project || projectId;
505
- const resolvedId = resolveProjectId(opts.project);
503
+ const resolvedId = resolveProjectIdAllowingLegacyEnv(opts.project);
506
504
  if (opts.positional.length === 0) die("Key required");
507
505
  if (opts.positional.length > 1) die("blob get expects exactly one key");
508
506
  if (!opts.output) die("--output <file> required");
@@ -526,10 +524,9 @@ async function get(projectId, argv) {
526
524
  // ls
527
525
  // ---------------------------------------------------------------------------
528
526
 
529
- async function ls(projectId, argv) {
527
+ async function ls(argv) {
530
528
  const opts = parseArgs(argv);
531
- opts.project = opts.project || projectId;
532
- const resolvedId = resolveProjectId(opts.project);
529
+ const resolvedId = resolveProjectIdAllowingLegacyEnv(opts.project);
533
530
 
534
531
  try {
535
532
  const data = await getSdk().assets.ls(resolvedId, {
@@ -552,10 +549,9 @@ async function ls(projectId, argv) {
552
549
  // rm
553
550
  // ---------------------------------------------------------------------------
554
551
 
555
- async function rm(projectId, argv) {
552
+ async function rm(argv) {
556
553
  const opts = parseArgs(argv);
557
- opts.project = opts.project || projectId;
558
- const resolvedId = resolveProjectId(opts.project);
554
+ const resolvedId = resolveProjectIdAllowingLegacyEnv(opts.project);
559
555
  if (opts.positional.length === 0) die("Key required");
560
556
  if (opts.positional.length > 1) die("blob rm expects exactly one key");
561
557
  const key = opts.positional[0];
@@ -572,10 +568,9 @@ async function rm(projectId, argv) {
572
568
  // sign
573
569
  // ---------------------------------------------------------------------------
574
570
 
575
- async function diagnose(projectId, argv) {
571
+ async function diagnose(argv) {
576
572
  const opts = parseArgs(argv);
577
- opts.project = opts.project || projectId;
578
- const resolvedId = resolveProjectId(opts.project);
573
+ const resolvedId = resolveProjectIdAllowingLegacyEnv(opts.project);
579
574
  if (opts.positional.length === 0) die("URL required");
580
575
  if (opts.positional.length > 1) die("blob diagnose expects exactly one URL");
581
576
  const url = opts.positional[0];
@@ -623,10 +618,9 @@ function toCliDiagnoseEnvelope(env) {
623
618
  };
624
619
  }
625
620
 
626
- async function sign(projectId, argv) {
621
+ async function sign(argv) {
627
622
  const opts = parseArgs(argv);
628
- opts.project = opts.project || projectId;
629
- const resolvedId = resolveProjectId(opts.project);
623
+ const resolvedId = resolveProjectIdAllowingLegacyEnv(opts.project);
630
624
  if (opts.positional.length === 0) die("Key required");
631
625
  if (opts.positional.length > 1) die("blob sign expects exactly one key");
632
626
  const key = opts.positional[0];
@@ -676,14 +670,13 @@ export async function run(sub, args) {
676
670
  console.log(SUB_HELP[sub] || HELP);
677
671
  process.exit(0);
678
672
  }
679
- const defaultProject = process.env.RUN402_PROJECT ?? null;
680
673
  switch (sub) {
681
- case "put": await put(defaultProject, args); break;
682
- case "get": await get(defaultProject, args); break;
683
- case "ls": await ls(defaultProject, args); break;
684
- case "rm": await rm(defaultProject, args); break;
685
- case "sign": await sign(defaultProject, args); break;
686
- case "diagnose": await diagnose(defaultProject, args); break;
674
+ case "put": await put(args); break;
675
+ case "get": await get(args); break;
676
+ case "ls": await ls(args); break;
677
+ case "rm": await rm(args); break;
678
+ case "sign": await sign(args); break;
679
+ case "diagnose": await diagnose(args); break;
687
680
  default:
688
681
  failUnknownSubcommand("assets", sub);
689
682
  }
package/lib/cdn.mjs CHANGED
@@ -13,7 +13,7 @@
13
13
  * are bound to a SHA at upload time and never previously cached.
14
14
  */
15
15
 
16
- import { resolveProjectId } from "./config.mjs";
16
+ import { resolveProjectIdAllowingLegacyEnv } from "./config.mjs";
17
17
  import { getSdk } from "./sdk.mjs";
18
18
  import { reportSdkError, fail } from "./sdk-errors.mjs";
19
19
  import { assertKnownFlags, flagValue, normalizeArgv, parseIntegerFlag, positionalArgs, failUnknownSubcommand } from "./argparse.mjs";
@@ -91,10 +91,9 @@ function parseArgs(args) {
91
91
  return opts;
92
92
  }
93
93
 
94
- async function waitFresh(projectId, argv) {
94
+ async function waitFresh(argv) {
95
95
  const opts = parseArgs(argv);
96
- opts.project = opts.project || projectId;
97
- const resolvedId = resolveProjectId(opts.project);
96
+ const resolvedId = resolveProjectIdAllowingLegacyEnv(opts.project);
98
97
  if (opts.positional.length === 0) die("URL required");
99
98
  const url = opts.positional[0];
100
99
  if (!opts.sha) die("--sha is required");
@@ -136,10 +135,9 @@ export async function run(sub, args) {
136
135
  console.log(SUB_HELP[sub] || HELP);
137
136
  process.exit(0);
138
137
  }
139
- const defaultProject = process.env.RUN402_PROJECT ?? null;
140
138
  switch (sub) {
141
139
  case "wait-fresh":
142
- await waitFresh(defaultProject, args);
140
+ await waitFresh(args);
143
141
  break;
144
142
  default:
145
143
  failUnknownSubcommand("cdn", sub);
@@ -271,6 +271,26 @@ export const COMMAND_MANIFEST = [
271
271
  { path: ["gitvault", "compact"], positionals: [], projectScoped: true, legacyPositionalProject: false, minimalArgs: [], runStyle: "sub", skipBehavioral: "takes a maintenance lease and builds a checkpoint from the local repository" },
272
272
  { path: ["gitvault", "prune"], positionals: [], projectScoped: true, legacyPositionalProject: false, minimalArgs: [], runStyle: "sub", skipBehavioral: "materializes the live vault head to enumerate retention roots" },
273
273
  { path: ["gitvault", "verify"], positionals: [], projectScoped: true, legacyPositionalProject: false, minimalArgs: [], runStyle: "sub", skipBehavioral: "walks the live head chain against the keystore's authenticated pin" },
274
+ { path: ["gitvault", "reconcile"], positionals: [], projectScoped: true, legacyPositionalProject: false, minimalArgs: [], runStyle: "sub", skipBehavioral: "reads the live org encryption-key directory + vault envelope recipients, and may publish new key_envelope objects" },
275
+
276
+ // ── gitvault mirror (gitvault-mirror-and-recover) ────────────────────────
277
+ // The customer-owned ciphertext mirror — client-side only, never a server
278
+ // call except `sync` (lists the live vault's objects) and `status`/`verify`
279
+ // (a keyless read against the mirror + one live vault-record read). All
280
+ // five need a real keystore + (for anything but `remove`) a configured
281
+ // mirror destination, so — same as the gitvault family above — the gate
282
+ // runs structural checks only.
283
+ { path: ["gitvault", "mirror", "set"], positionals: [p("destination")], projectScoped: true, legacyPositionalProject: false, minimalArgs: ["s3://example-mirror-bucket"], runStyle: "sub", skipBehavioral: "writes mirror destination config beside the keystore (client-side only)" },
284
+ { path: ["gitvault", "mirror", "remove"], positionals: [], projectScoped: true, legacyPositionalProject: false, minimalArgs: [], runStyle: "sub", skipBehavioral: "removes mirror destination config beside the keystore (client-side only)" },
285
+ { path: ["gitvault", "mirror", "status"], positionals: [], projectScoped: true, legacyPositionalProject: false, minimalArgs: [], runStyle: "sub", skipBehavioral: "reads the configured mirror + the live vault record's newest_generation" },
286
+ { path: ["gitvault", "mirror", "sync"], positionals: [], projectScoped: true, legacyPositionalProject: false, minimalArgs: [], runStyle: "sub", skipBehavioral: "lists the live vault's stored objects and reconciles them against the configured mirror" },
287
+ { path: ["gitvault", "mirror", "verify"], positionals: [], projectScoped: true, legacyPositionalProject: false, minimalArgs: [], runStyle: "sub", skipBehavioral: "keyless discovery + chain verification against the configured mirror; touches no key material" },
288
+
289
+ // ── gitvault recover (gitvault-mirror-and-recover, design D4) ────────────
290
+ // `r402s-recover`: offline, no server call at all — reads only from the
291
+ // mirror source named on the command line. Not project-scoped (the source
292
+ // URL, not the active project, addresses the vault to recover).
293
+ { path: ["gitvault", "recover"], positionals: [p("source")], projectScoped: false, legacyPositionalProject: false, minimalArgs: ["s3://example-mirror-bucket", "--out", "__SCRATCH_DIR__/recover-out"], runStyle: "sub", skipBehavioral: "materializes a git repository from a mirror source, offline, with no server call" },
274
294
 
275
295
  // ── repos (vault-only porcelain, repo-first-onramp D8, task 2.6) ────────
276
296
  // `create` writes real git state into cwd and allocates a vault; `list`
package/lib/config.mjs CHANGED
@@ -139,6 +139,36 @@ export function resolveProjectId(id) {
139
139
  return projectId;
140
140
  }
141
141
 
142
+ /**
143
+ * `resolveProjectId`, but also honors the deprecated `RUN402_PROJECT` alias
144
+ * as a last-resort fallback.
145
+ *
146
+ * `RUN402_PROJECT_ID` is the canonical env var everywhere in the CLI — it's
147
+ * what `resolveProjectId`, `run402 doctor`, `org-context.mjs`, and every
148
+ * other project-scoped command read. Two commands (`cdn wait-fresh` and
149
+ * every `assets` subcommand) historically read the DIFFERENT, undocumented
150
+ * `RUN402_PROJECT` instead, so exporting the canonical `RUN402_PROJECT_ID`
151
+ * and running one of those two commands was a silent no-op: the export did
152
+ * nothing, and resolution quietly fell through to the active project.
153
+ *
154
+ * Precedence, highest first: an explicit `id` (e.g. `--project`) >
155
+ * `RUN402_PROJECT_ID` > the deprecated `RUN402_PROJECT` alias > the active
156
+ * project. The alias is only ever consulted when `RUN402_PROJECT_ID` is
157
+ * unset AND no explicit id was given — so it never silently overrides the
158
+ * canonical var or a flag. When the alias is what actually resolves the
159
+ * project, exactly one deprecation line goes to stderr (never stdout — the
160
+ * pipe contract keeps stdout pure JSON).
161
+ */
162
+ export function resolveProjectIdAllowingLegacyEnv(id) {
163
+ if (!id && !process.env.RUN402_PROJECT_ID && process.env.RUN402_PROJECT) {
164
+ process.stderr.write(
165
+ "warning: RUN402_PROJECT is deprecated and will be removed; set RUN402_PROJECT_ID instead.\n",
166
+ );
167
+ return resolveProjectId(process.env.RUN402_PROJECT);
168
+ }
169
+ return resolveProjectId(id);
170
+ }
171
+
142
172
  // Re-export core keystore functions for direct use
143
173
  export {
144
174
  configureApiBase,
package/lib/deploy-v2.mjs CHANGED
@@ -1281,9 +1281,24 @@ async function applyCmd(args) {
1281
1281
  ...("generation" in vaulted ? { generation: vaulted.generation } : {}),
1282
1282
  ...("push_error" in vaulted ? { push_error: vaulted.push_error } : {}),
1283
1283
  ...("deploy_error" in vaulted ? { deploy_error: vaulted.deploy_error } : {}),
1284
+ // Design D5/D6 (gitvault-human-envelopes task 4.1 + gitvault-mirror):
1285
+ // present only when this deploy landed a new generation — the SDK
1286
+ // omits both fields entirely on the other three outcomes rather than
1287
+ // faking a `skipped_*` value for something that never had a chance to
1288
+ // run. See `Gitvault.deploy`'s doc comment (sdk/src/namespaces/gitvault.ts).
1289
+ ...("mirror_push" in vaulted ? { mirror_push: vaulted.mirror_push } : {}),
1290
+ ...("reconcile_recipients" in vaulted ? { reconcile_recipients: vaulted.reconcile_recipients } : {}),
1284
1291
  next_actions: vaulted.next_actions,
1285
1292
  deploy: outcome.deploy,
1286
1293
  }, null, 2));
1294
+ // Design D6: the mirror result is reported BESIDE the vault outcome
1295
+ // above, on its own stderr line — a mirror failure never blocks the
1296
+ // deploy (mirrors `run402 gitvault snapshot`'s reporting).
1297
+ if (vaulted.mirror_push?.outcome === "pushed") {
1298
+ console.error(`mirror: pushed generation ${vaulted.generation} (${vaulted.mirror_push.summary?.objects_copied ?? 0} object(s) copied)`);
1299
+ } else if (vaulted.mirror_push?.outcome === "failed") {
1300
+ console.error(`mirror: dual-push FAILED (deploy is unaffected) — ${vaulted.mirror_push.error ?? "see mirror_push.summary.errors"}`);
1301
+ }
1287
1302
  // A non-activating outcome is a failed deploy even though it resolved
1288
1303
  // rather than threw: the five outcomes are a result type, not an error
1289
1304
  // channel, so the exit code has to carry the verdict.
package/lib/doctor.mjs CHANGED
@@ -593,6 +593,39 @@ export async function run(sub, args = []) {
593
593
  // Echoed exactly as the SDK reported them — including the
594
594
  // doctor-persistent `grandfathered` advisory it owns.
595
595
  for (const w of gv.warnings ?? []) gaps.push(`${w.kind}: ${w.message}`);
596
+
597
+ // gitvault-mirror-and-recover task 4.3: mirror currency, reported
598
+ // ALONGSIDE (never in place of) the deploy-related gaps above, and
599
+ // never blocking `run402 deploy`'s own gate — the vault lane's
600
+ // outcome is unaffected regardless of mirror state (design D6).
601
+ // `mirror_currency` mirrors `mirror status`'s own tri-state:
602
+ // `current` / `stale` / `unknown` (mirror unreachable or vault
603
+ // unread) only STALE is actionable enough to become a warning; NO
604
+ // mirror configured is a purely informational, ungated advisory —
605
+ // most vaults have never opted in, and that is a normal shape.
606
+ if (gv.vault !== null && value.repo_id) {
607
+ try {
608
+ const mirrorStatus = await getSdk().gitvault.mirrorStatus({ repo_id: value.repo_id });
609
+ value.gitvault_mirror = {
610
+ configured: mirrorStatus.configured,
611
+ destination: mirrorStatus.destination,
612
+ mirrored_generation: mirrorStatus.mirrored_generation,
613
+ newest_generation: mirrorStatus.newest_generation,
614
+ is_current: mirrorStatus.is_current,
615
+ validity_not_freshness: mirrorStatus.validity_not_freshness,
616
+ keystore_still_required: mirrorStatus.keystore_still_required,
617
+ };
618
+ if (!mirrorStatus.configured) {
619
+ value.gitvault_mirror.advisory = "no ciphertext mirror is configured for this vault — the exit ramp is opt-in; 'run402 gitvault mirror set <destination>' to configure one.";
620
+ } else if (mirrorStatus.is_current === false) {
621
+ gaps.push(`the ciphertext mirror at ${mirrorStatus.destination} is STALE (mirrored generation ${mirrorStatus.mirrored_generation ?? "(none)"}, vault newest ${mirrorStatus.newest_generation ?? "(none)"}) — ${mirrorStatus.closing_command}`);
622
+ }
623
+ } catch {
624
+ // Best-effort: a mirror status read failing is never a doctor
625
+ // failure, and never touches the deploy-related gaps above.
626
+ }
627
+ }
628
+
596
629
  checks.push({
597
630
  name: "gitvault",
598
631
  status: gaps.length > 0 ? "warning" : "ok",