run402 4.68.2 → 4.69.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 (138) hide show
  1. package/cli.mjs +2 -2
  2. package/core-dist/allowance-auth.js +1 -1
  3. package/core-dist/allowance.js +1 -1
  4. package/core-dist/config.js +4 -4
  5. package/core-dist/keystore.js +7 -7
  6. package/core-dist/profiles.js +1 -1
  7. package/core-dist/redact.js +6 -7
  8. package/git-remote-run402.mjs +2 -2
  9. package/gitvault-surface.json +2 -1
  10. package/lib/agent.mjs +1 -1
  11. package/lib/allowance.mjs +3 -4
  12. package/lib/argparse.mjs +7 -9
  13. package/lib/billing.mjs +33 -35
  14. package/lib/buzz-notifications.mjs +3 -2
  15. package/lib/buzz.mjs +14 -8
  16. package/lib/buzz.test.mjs +1 -1
  17. package/lib/command-manifest.mjs +38 -20
  18. package/lib/config.mjs +1 -1
  19. package/lib/deploy-v2.mjs +3 -3
  20. package/lib/doctor-source-scan.mjs +1 -1
  21. package/lib/doctor.mjs +42 -25
  22. package/lib/email.mjs +2 -3
  23. package/lib/harness-context.mjs +1 -2
  24. package/lib/init.mjs +1 -1
  25. package/lib/next-actions.mjs +1 -1
  26. package/lib/org-context.mjs +83 -2
  27. package/lib/org.mjs +231 -114
  28. package/lib/projects.mjs +2 -2
  29. package/lib/remote-helper-session.mjs +26 -32
  30. package/lib/repos.mjs +117 -15
  31. package/lib/rooms-context.mjs +5 -5
  32. package/lib/sdk-errors.mjs +1 -2
  33. package/lib/status.mjs +1 -1
  34. package/lib/update-check.test.mjs +1 -1
  35. package/lib/wallet-context.mjs +10 -10
  36. package/lib/wallet-context.test.mjs +6 -7
  37. package/lib/wallets.mjs +2 -2
  38. package/lib/webhooks.mjs +2 -2
  39. package/package.json +1 -1
  40. package/sdk/core-dist/allowance-auth.js +1 -1
  41. package/sdk/core-dist/allowance.js +1 -1
  42. package/sdk/core-dist/config.js +4 -4
  43. package/sdk/core-dist/keystore.js +7 -7
  44. package/sdk/core-dist/profiles.js +1 -1
  45. package/sdk/core-dist/redact.js +6 -7
  46. package/sdk/dist/delegate-credentials.d.ts +1 -1
  47. package/sdk/dist/delegate-credentials.js +1 -1
  48. package/sdk/dist/errors.d.ts +10 -4
  49. package/sdk/dist/errors.d.ts.map +1 -1
  50. package/sdk/dist/errors.js +2 -3
  51. package/sdk/dist/errors.js.map +1 -1
  52. package/sdk/dist/kernel.js +1 -1
  53. package/sdk/dist/kernel.js.map +1 -1
  54. package/sdk/dist/namespaces/allowance.d.ts +1 -1
  55. package/sdk/dist/namespaces/allowance.js +1 -1
  56. package/sdk/dist/namespaces/allowance.js.map +1 -1
  57. package/sdk/dist/namespaces/assets.js +1 -1
  58. package/sdk/dist/namespaces/assets.js.map +1 -1
  59. package/sdk/dist/namespaces/assets.types.d.ts +1 -1
  60. package/sdk/dist/namespaces/ci.d.ts.map +1 -1
  61. package/sdk/dist/namespaces/ci.js +4 -6
  62. package/sdk/dist/namespaces/ci.js.map +1 -1
  63. package/sdk/dist/namespaces/deploy.js +7 -8
  64. package/sdk/dist/namespaces/deploy.js.map +1 -1
  65. package/sdk/dist/namespaces/gitvault.crypto.d.ts +86 -3
  66. package/sdk/dist/namespaces/gitvault.crypto.d.ts.map +1 -1
  67. package/sdk/dist/namespaces/gitvault.crypto.js +102 -3
  68. package/sdk/dist/namespaces/gitvault.crypto.js.map +1 -1
  69. package/sdk/dist/namespaces/gitvault.d.ts +197 -17
  70. package/sdk/dist/namespaces/gitvault.d.ts.map +1 -1
  71. package/sdk/dist/namespaces/gitvault.js +698 -58
  72. package/sdk/dist/namespaces/gitvault.js.map +1 -1
  73. package/sdk/dist/namespaces/gitvault.types.d.ts +91 -3
  74. package/sdk/dist/namespaces/gitvault.types.d.ts.map +1 -1
  75. package/sdk/dist/namespaces/gitvault.types.js.map +1 -1
  76. package/sdk/dist/namespaces/operator.d.ts +1 -1
  77. package/sdk/dist/namespaces/operator.js +1 -1
  78. package/sdk/dist/namespaces/org.d.ts +14 -1
  79. package/sdk/dist/namespaces/org.d.ts.map +1 -1
  80. package/sdk/dist/namespaces/org.js +15 -0
  81. package/sdk/dist/namespaces/org.js.map +1 -1
  82. package/sdk/dist/namespaces/org.types.d.ts +30 -0
  83. package/sdk/dist/namespaces/org.types.d.ts.map +1 -1
  84. package/sdk/dist/namespaces/projects.types.d.ts +1 -1
  85. package/sdk/dist/namespaces/subdomains.js +1 -1
  86. package/sdk/dist/namespaces/subdomains.js.map +1 -1
  87. package/sdk/dist/namespaces/transfers.d.ts +3 -4
  88. package/sdk/dist/namespaces/transfers.d.ts.map +1 -1
  89. package/sdk/dist/namespaces/transfers.js +1 -2
  90. package/sdk/dist/namespaces/transfers.js.map +1 -1
  91. package/sdk/dist/node/actions-node.d.ts.map +1 -1
  92. package/sdk/dist/node/actions-node.js +3 -4
  93. package/sdk/dist/node/actions-node.js.map +1 -1
  94. package/sdk/dist/node/deploy-manifest.js +2 -2
  95. package/sdk/dist/node/deploy-manifest.js.map +1 -1
  96. package/sdk/dist/node/gitvault-address.js +2 -2
  97. package/sdk/dist/node/gitvault-address.js.map +1 -1
  98. package/sdk/dist/node/gitvault-creation-journal.d.ts +1 -1
  99. package/sdk/dist/node/gitvault-creation-journal.js +2 -2
  100. package/sdk/dist/node/gitvault-creation-journal.js.map +1 -1
  101. package/sdk/dist/node/gitvault-deploy.d.ts +2 -2
  102. package/sdk/dist/node/gitvault-deploy.js +2 -2
  103. package/sdk/dist/node/gitvault-handoff.d.ts +142 -3
  104. package/sdk/dist/node/gitvault-handoff.d.ts.map +1 -1
  105. package/sdk/dist/node/gitvault-handoff.js +251 -21
  106. package/sdk/dist/node/gitvault-handoff.js.map +1 -1
  107. package/sdk/dist/node/gitvault-keystore.d.ts +78 -3
  108. package/sdk/dist/node/gitvault-keystore.d.ts.map +1 -1
  109. package/sdk/dist/node/gitvault-keystore.js +46 -5
  110. package/sdk/dist/node/gitvault-keystore.js.map +1 -1
  111. package/sdk/dist/node/gitvault-mirror.d.ts.map +1 -1
  112. package/sdk/dist/node/gitvault-mirror.js +3 -5
  113. package/sdk/dist/node/gitvault-mirror.js.map +1 -1
  114. package/sdk/dist/node/gitvault-profile-scan.js +1 -1
  115. package/sdk/dist/node/gitvault-publication.d.ts +379 -21
  116. package/sdk/dist/node/gitvault-publication.d.ts.map +1 -1
  117. package/sdk/dist/node/gitvault-publication.js +664 -41
  118. package/sdk/dist/node/gitvault-publication.js.map +1 -1
  119. package/sdk/dist/node/gitvault-recover.d.ts.map +1 -1
  120. package/sdk/dist/node/gitvault-recover.js +7 -10
  121. package/sdk/dist/node/gitvault-recover.js.map +1 -1
  122. package/sdk/dist/node/gitvault-snapshot.d.ts +1 -2
  123. package/sdk/dist/node/gitvault-snapshot.d.ts.map +1 -1
  124. package/sdk/dist/node/gitvault-snapshot.js +1 -2
  125. package/sdk/dist/node/gitvault-snapshot.js.map +1 -1
  126. package/sdk/dist/node/gitvault-writer-state.d.ts +201 -0
  127. package/sdk/dist/node/gitvault-writer-state.d.ts.map +1 -0
  128. package/sdk/dist/node/gitvault-writer-state.js +357 -0
  129. package/sdk/dist/node/gitvault-writer-state.js.map +1 -0
  130. package/sdk/dist/node/index.d.ts +4 -0
  131. package/sdk/dist/node/index.d.ts.map +1 -1
  132. package/sdk/dist/node/index.js +20 -1
  133. package/sdk/dist/node/index.js.map +1 -1
  134. package/sdk/dist/node/paid-fetch.d.ts.map +1 -1
  135. package/sdk/dist/node/paid-fetch.js +3 -4
  136. package/sdk/dist/node/paid-fetch.js.map +1 -1
  137. package/sdk/dist/node/sites-node.js +1 -1
  138. package/sdk/dist/node/sites-node.js.map +1 -1
package/cli.mjs CHANGED
@@ -533,8 +533,8 @@ switch (cmd) {
533
533
  ]);
534
534
  const closest = typeof cmd === "string" ? closestWord(cmd, [...families]) : null;
535
535
  // `cmd` is the first bare positional — could be anything a script passes
536
- // by mistake (kychee-com/run402-private#640 is the demonstrated risk for
537
- // this shape of value), so it must not be echoed verbatim.
536
+ // by mistake, and a secret is a demonstrated risk for this shape of
537
+ // value, so it must not be echoed verbatim.
538
538
  const shownCmd = describeRejectedValue(cmd);
539
539
  fail({
540
540
  code: "UNKNOWN_COMMAND",
@@ -115,7 +115,7 @@ export function buildSIWxAuthHeaders(opts) {
115
115
  * @param path - API path (e.g. "/projects/v1") used to build the SIWE uri field.
116
116
  */
117
117
  export function getAllowanceAuthHeaders(path, allowancePath) {
118
- // GH-194: readAllowance throws on a malformed-shape allowance file. The
118
+ // readAllowance throws on a malformed-shape allowance file. The
119
119
  // CLI's higher-level readAllowance wrapper surfaces this as a structured
120
120
  // BAD_ALLOWANCE_FILE envelope; here we preserve the public contract that
121
121
  // this helper returns SIWxAuthHeaders | null. Re-throw so callers above
@@ -14,7 +14,7 @@ const PRIVATE_KEY_RE = /^0x[a-fA-F0-9]{64}$/;
14
14
  * - the file exists but is not parseable JSON (preserve existing UX —
15
15
  * consumers print "no_allowance" and tell the user to run init)
16
16
  *
17
- * Throws a structured `Error` (GH-194) when the file parses as JSON but the
17
+ * Throws a structured `Error` when the file parses as JSON but the
18
18
  * shape is wrong (missing/wrong-type/wrong-length fields). Without this guard
19
19
  * downstream callers crash with raw stack traces:
20
20
  * - `cli/lib/status.mjs` reaches for `allowance.address.toLowerCase()`
@@ -83,10 +83,10 @@ function assertSafeProfileName(name) {
83
83
  return;
84
84
  if (!isValidProfileName(name)) {
85
85
  // A value that failed this check is a value we know nothing about — the
86
- // most likely mistake is a typo, but kychee-com/run402-private#640 was a
87
- // Base-mainnet private key pasted into RUN402_WALLET (a NAME field), and
88
- // the raw value used to be echoed straight into this message and
89
- // wherever it propagated (terminal, logs, session transcripts).
86
+ // most likely mistake is a typo, but a Base-mainnet private key pasted
87
+ // into RUN402_WALLET (a NAME field) is a demonstrated case, and echoing
88
+ // the raw value propagates it into terminals, logs, and session
89
+ // transcripts.
90
90
  // describeRejectedValue() shows short/plain values in full (useful for
91
91
  // an actual typo) and redacts anything long or hex-shaped enough to be
92
92
  // a secret instead of a name.
@@ -190,15 +190,15 @@ export function removeProject(projectId, path) {
190
190
  * bucket, not "unknown") and PERMANENTLY shadows every later wallet-scoped
191
191
  * activation for every caller that reads through this module: `resolveProjectId`
192
192
  * / `resolveProject` (cli/lib/config.mjs), `projects current`, and every
193
- * gitvault-verb target resolution. That is the root cause behind
194
- * kychee-com/run402#559(a) `repos create` DID call `projects.provision`,
195
- * whose `creds.setActiveProject` correctly persisted the new project under
196
- * the real wallet's scoped bucket AND the flat fallback, but every CLI read
197
- * of "the active project" kept resolving the OLD "unknown"-bucket entry
198
- * first and never fell through to the freshly-updated flat value.
193
+ * gitvault-verb target resolution. The failure it causes: `repos create`
194
+ * calls `projects.provision`, whose `creds.setActiveProject` correctly
195
+ * persists the new project under the real wallet's scoped bucket AND the
196
+ * flat fallback, yet every CLI read of "the active project" resolves the
197
+ * stale "unknown"-bucket entry first and never falls through to the
198
+ * freshly-updated flat value.
199
199
  *
200
200
  * Best-effort: an unreadable/malformed allowance degrades to `null` (the
201
- * same "unknown" bucket callers already tolerated before this fix), never a
201
+ * "unknown" bucket callers tolerate), never a
202
202
  * throw — this is a read-scoping concern, not an allowance-validity one.
203
203
  */
204
204
  function currentPrincipal() {
@@ -154,7 +154,7 @@ export function removeProfile(name) {
154
154
  */
155
155
  export function renameProfile(oldName, newName) {
156
156
  if (!isValidProfileName(newName)) {
157
- // See describeRejectedValue()'s doc comment (kychee-com/run402-private#640):
157
+ // See describeRejectedValue()'s doc comment:
158
158
  // a rejected value may be a secret pasted into a name field, so it is
159
159
  // never safe to echo verbatim here.
160
160
  throw new Error(`Invalid wallet name ${JSON.stringify(describeRejectedValue(newName))}.`);
@@ -4,11 +4,10 @@
4
4
  * A value rejected by a format/existence check is a value we know nothing
5
5
  * about — the CLI has only established what it is NOT. The likeliest
6
6
  * failure is an ordinary typo, but the second likeliest, demonstrated in
7
- * practice, is a credential pasted into the wrong slot:
8
- * kychee-com/run402-private#640 a Base-mainnet private key holding real
9
- * funds landed in `RUN402_WALLET` (which takes a wallet NAME, not a key),
10
- * failed the name check, and was printed verbatim to a terminal, a log, and
11
- * a session transcript.
7
+ * practice, is a credential pasted into the wrong slot: a Base-mainnet
8
+ * private key holding real funds lands in `RUN402_WALLET` (which takes a
9
+ * wallet NAME, not a key), fails the name check, and is printed verbatim to
10
+ * a terminal, a log, and a session transcript.
12
11
  *
13
12
  * `describeRejectedValue` is the one place that decides what a rejected
14
13
  * value is safe to show, so every call site — wallet names, org ids, room
@@ -35,11 +34,11 @@
35
34
  // canonical shape of an org id, and *itself* a public, harmless-to-echo
36
35
  // identifier even when it's the "wrong" one in a conflict, not a secret).
37
36
  // Secrets that land in the wrong slot (private keys, API tokens, JWTs) are
38
- // almost always well past it — the private key in #640 was 64-66 characters.
37
+ // almost always well past it — a private key runs 64-66 characters.
39
38
  const MAX_SAFE_ECHO_LENGTH = 40;
40
39
  // A contiguous hex run (with or without a `0x` prefix) reads as key
41
40
  // material or a hash rather than a human-typed identifier, even when short
42
- // enough to pass the length check above — the exact shape of #640.
41
+ // enough to pass the length check above.
43
42
  const HEX_RUN_RE = /^(0x)?[0-9a-fA-F]{16,}$/;
44
43
  function looksSecretShaped(str) {
45
44
  return str.length > MAX_SAFE_ECHO_LENGTH || HEX_RUN_RE.test(str);
@@ -20,8 +20,8 @@
20
20
  * builtins + `./lib/daemon-path.mjs` only, pinned by a source-level
21
21
  * gate test (design D7). The heavy graph loads only on fallback.
22
22
  * - `RUN402_DAEMON=0` disables the daemon entirely (dev/CI toggle,
23
- * approved 2026-08-30 — positively named, default on, only "0"
24
- * disables); any other value behaves as unset.
23
+ * positively named, default on, only "0" disables); any other value
24
+ * behaves as unset.
25
25
  *
26
26
  * The remote-helper doctrine (fail-closed repository resolution, wallet
27
27
  * selection, protocol semantics) lives with the session implementation in
@@ -1,5 +1,5 @@
1
1
  {
2
- "surface_version": "4.68.2",
2
+ "surface_version": "4.69.0",
3
3
  "verbs": [
4
4
  "repos create",
5
5
  "repos list",
@@ -17,6 +17,7 @@
17
17
  "repos access repair",
18
18
  "repos access revoke-key",
19
19
  "repos access declare-exposure",
20
+ "repos access sync",
20
21
  "repos recover",
21
22
  "repos recovery-bundle"
22
23
  ],
package/lib/agent.mjs CHANGED
@@ -93,7 +93,7 @@ async function contact(args) {
93
93
  if (!name) {
94
94
  fail({ code: "BAD_USAGE", message: "Missing --name <name>" });
95
95
  }
96
- // GH-192: validate webhook scheme locally BEFORE the allowance check so
96
+ // validate webhook scheme locally BEFORE the allowance check so
97
97
  // bad URLs fail fast even without an allowance configured. No-op when
98
98
  // --webhook is omitted (it's optional).
99
99
  validateWebhookUrl(webhook, "--webhook");
package/lib/allowance.mjs CHANGED
@@ -92,10 +92,9 @@ async function status() {
92
92
  address: data.address,
93
93
  created: data.created,
94
94
  configured: data.configured,
95
- // GH-109: `funded` used to leak an on-disk marker that only tracks
96
- // faucet invocation, not actual pay-readiness. Renamed to `faucet_used`
97
- // to match its real semantics. For a true "can this account pay right
98
- // now" check, use `run402 allowance balance`.
95
+ // `faucet_used` is named for what it is: an on-disk marker that
96
+ // tracks faucet invocation, not pay-readiness. For a true "can this
97
+ // account pay right now" check, use `run402 allowance balance`.
99
98
  faucet_used: !!data.faucet_used,
100
99
  rail: w?.rail || "x402",
101
100
  path: data.path ?? allowanceFile(),
package/lib/argparse.mjs CHANGED
@@ -141,9 +141,8 @@ export function validateEvmAddress(value, fieldName = "address") {
141
141
  }
142
142
  }
143
143
 
144
- // A rejected positional could be anything the caller typed or piped in see
145
- // core-dist/redact.js's doc comment (kychee-com/run402-private#640) so it
146
- // is never safe to echo verbatim.
144
+ // A rejected positional could be anything the caller typed or piped in (see
145
+ // core-dist/redact.js's doc comment), so it is never safe to echo verbatim.
147
146
  export function failBadProjectId(value) {
148
147
  fail({
149
148
  code: "BAD_PROJECT_ID",
@@ -156,7 +155,7 @@ export function failBadProjectId(value) {
156
155
  /**
157
156
  * Validate a webhook URL: parse it locally and reject non-https:// schemes.
158
157
  *
159
- * Scope (GH-192): scheme-only validation. Reject `javascript:`, `file:`,
158
+ * Scope: scheme-only validation. Reject `javascript:`, `file:`,
160
159
  * `http:`, `data:`, `ftp:`, etc. before the request leaves the CLI process.
161
160
  * Server-side SSRF defenses (private-IP filtering, DNS rebinding, IMDS
162
161
  * blocking) live on the gateway, not here — this helper is the cheap
@@ -198,9 +197,8 @@ export function validateWebhookUrl(url, fieldName = "--url") {
198
197
 
199
198
  /**
200
199
  * Validate that a CLI flag pointing at a filesystem path resolves to an
201
- * existing regular file. Replaces the GH-195 inline pattern that was
202
- * duplicated across `functions deploy`, `secrets set`, `projects sql`,
203
- * and `projects apply-expose` (GH-233).
200
+ * existing regular file. Shared by `functions deploy`, `secrets set`,
201
+ * `projects sql`, and `projects apply-expose`.
204
202
  *
205
203
  * Without this guard, `readFileSync` against a missing path leaks a raw
206
204
  * `node:fs` ENOENT/EISDIR stack to stderr (with the V8 source pointer),
@@ -439,8 +437,8 @@ export function failUnknownSubcommand(family, sub, { hint, label, extraSubcomman
439
437
  const known = [...new Set([...knownSubcommands(family), ...extraSubcommands])].sort();
440
438
  // closestWord() itself never echoes — it only ever returns one of the
441
439
  // known, safe `known` candidates — but `sub` is an arbitrary positional
442
- // (could be anything piped or mistyped in; kychee-com/run402-private#640
443
- // is the demonstrated risk), so the ECHO of `sub` below still needs
440
+ // (could be anything piped or mistyped in, a leaked secret included),
441
+ // so the ECHO of `sub` below still needs
444
442
  // redaction even though the matching runs on the raw value.
445
443
  const closest = typeof sub === "string" ? closestWord(sub, known) : null;
446
444
  const shownSub = describeRejectedValue(sub);
package/lib/billing.mjs CHANGED
@@ -1,6 +1,7 @@
1
1
  import { getSdk } from "./sdk.mjs";
2
2
  import { reportSdkError, fail } from "./sdk-errors.mjs";
3
3
  import { assertKnownFlags, flagValue, normalizeArgv, parseIntegerFlag, positionalArgs, failUnknownSubcommand } from "./argparse.mjs";
4
+ import { takeOrgPositional, requireRest } from "./org-context.mjs";
4
5
 
5
6
  const HELP = `run402 billing — Email organizations and org checkouts
6
7
 
@@ -9,9 +10,9 @@ Usage:
9
10
 
10
11
  Subcommands:
11
12
  create-email <email> Create an email organization
12
- link-wallet <org_id> <wallet_address> Link a wallet to an email organization
13
+ link-wallet [<org_id>] <wallet_address> Link a wallet to an email organization
13
14
  checkout <identifier> --product <p> Create an org checkout
14
- auto-recharge <org_id> <on|off> [--threshold <n>]
15
+ auto-recharge [<org_id>] <on|off> [--threshold <n>]
15
16
  balance <identifier> Balance by organization id (UUID), wallet (0x...), or email
16
17
  history <identifier> [--limit <n>] Ledger history by organization id (UUID), wallet, or email
17
18
 
@@ -47,7 +48,7 @@ Examples:
47
48
  "auto-recharge": `run402 billing auto-recharge — Toggle email-pack auto-recharge
48
49
 
49
50
  Usage:
50
- run402 billing auto-recharge <org_id> <on|off> [--threshold <n>]
51
+ run402 billing auto-recharge [<org_id>] <on|off> [--threshold <n>]
51
52
 
52
53
  Arguments:
53
54
  <org_id> Organization ID
@@ -114,7 +115,7 @@ Examples:
114
115
  "link-wallet": `run402 billing link-wallet — Link a wallet to an email organization
115
116
 
116
117
  Usage:
117
- run402 billing link-wallet <org_id> <wallet_address>
118
+ run402 billing link-wallet [<org_id>] <wallet_address>
118
119
 
119
120
  Arguments:
120
121
  <org_id> Organization ID (e.g. org_abc123)
@@ -232,22 +233,16 @@ async function createEmail(args) {
232
233
 
233
234
  async function linkWallet(args) {
234
235
  const parsedArgs = normalizeArgv(args);
235
- assertKnownFlags(parsedArgs, ["--wallet", "--help", "-h"], ["--wallet"]);
236
- const walletFlag = flagValue(parsedArgs, "--wallet");
237
- const positionals = positionalArgs(parsedArgs, ["--wallet"]);
238
- const organizationId = positionals[0];
239
- const wallet = walletFlag ?? positionals[1];
240
- const max = walletFlag ? 1 : 2;
241
- if (positionals.length > max) {
242
- fail({ code: "BAD_USAGE", message: `Unexpected argument for billing link-wallet: ${positionals[max]}` });
243
- }
244
- if (!organizationId || !wallet) {
245
- fail({
246
- code: "BAD_USAGE",
247
- message: "Missing <org_id> and/or <wallet_address>.",
248
- hint: "run402 billing link-wallet <org_id> --wallet <wallet_address>",
249
- });
250
- }
236
+ assertKnownFlags(parsedArgs, ["--org", "--address", "--help", "-h"], ["--org", "--address"]);
237
+ const walletFlag = flagValue(parsedArgs, "--address");
238
+ const { orgId: organizationId, rest } = await takeOrgPositional(parsedArgs, ["--org", "--address"], { cmd: "billing" });
239
+ const count = walletFlag ? 0 : 1;
240
+ requireRest(rest, {
241
+ min: count, max: count,
242
+ command: "run402 billing link-wallet [<org_id>] <wallet_address>",
243
+ missing: "Missing <wallet_address> (positional, or --address).",
244
+ });
245
+ const wallet = walletFlag ?? rest[0];
251
246
  try {
252
247
  const data = await getSdk().billing.linkWallet(organizationId, wallet);
253
248
  const output = {
@@ -264,27 +259,30 @@ async function linkWallet(args) {
264
259
 
265
260
  async function autoRecharge(args) {
266
261
  const parsedArgs = normalizeArgv(args);
267
- const valueFlags = ["--threshold", "--state"];
262
+ const valueFlags = ["--org", "--threshold", "--state"];
268
263
  assertKnownFlags(parsedArgs, [...valueFlags, "--help", "-h"], valueFlags);
269
264
  const stateFlag = flagValue(parsedArgs, "--state");
270
- const positionals = positionalArgs(parsedArgs, valueFlags);
271
- const organizationId = positionals[0];
272
- const state = stateFlag ?? positionals[1];
273
- const max = stateFlag ? 1 : 2;
274
- if (positionals.length > max) {
275
- fail({ code: "BAD_USAGE", message: `Unexpected argument for billing auto-recharge: ${positionals[max]}` });
276
- }
277
- if (!organizationId || !state || !["on", "off"].includes(state)) {
278
- fail({
279
- code: "BAD_USAGE",
280
- message: "Missing <org_id> and/or <on|off>.",
281
- hint: "run402 billing auto-recharge <org_id> --state <on|off> [--threshold <n>]",
282
- });
283
- }
265
+ // Local validation FIRST — a malformed --threshold is refused before any org
266
+ // resolution (which may cost a network hop via --project).
284
267
  const thresholdStr = flagValue(parsedArgs, "--threshold");
285
268
  const threshold = parsedArgs.includes("--threshold")
286
269
  ? parseIntegerFlag("--threshold", thresholdStr, { min: 0 })
287
270
  : undefined;
271
+ const { orgId: organizationId, rest } = await takeOrgPositional(parsedArgs, valueFlags, { cmd: "billing" });
272
+ const count = stateFlag ? 0 : 1;
273
+ requireRest(rest, {
274
+ min: count, max: count,
275
+ command: "run402 billing auto-recharge [<org_id>] --state <on|off> [--threshold <n>]",
276
+ missing: "Missing <on|off> (positional, or --state).",
277
+ });
278
+ const state = stateFlag ?? rest[0];
279
+ if (!["on", "off"].includes(state)) {
280
+ fail({
281
+ code: "BAD_USAGE",
282
+ message: `Expected on|off, got ${JSON.stringify(state)}.`,
283
+ hint: "run402 billing auto-recharge [<org_id>] --state <on|off> [--threshold <n>]",
284
+ });
285
+ }
288
286
  try {
289
287
  await getSdk().billing.setAutoRecharge({
290
288
  organizationId: organizationId,
@@ -7,6 +7,7 @@
7
7
  */
8
8
  import { getSdk } from "./sdk.mjs";
9
9
  import { fail, reportSdkError } from "./sdk-errors.mjs";
10
+ import { resolveOrgId } from "./org-context.mjs";
10
11
  import {
11
12
  normalizeArgv,
12
13
  hasHelp,
@@ -129,7 +130,7 @@ async function configure(args) {
129
130
  const eventTypes = flagValues(a, "--event-type");
130
131
  const eventClasses = flagValues(a, "--event-class");
131
132
  try {
132
- const created = await getSdk().buzz.notifications.createRoute(requiredFlag(a, "--org"), {
133
+ const created = await getSdk().buzz.notifications.createRoute(await resolveOrgId(a, { cmd: "buzz" }), {
133
134
  installationId: requiredFlag(a, "--installation"),
134
135
  routeName: requiredFlag(a, "--name"),
135
136
  buzzChannelId: requiredFlag(a, "--channel"),
@@ -161,7 +162,7 @@ async function status(args) {
161
162
  out(await sdk.buzz.notifications.get(positionals[0]));
162
163
  return;
163
164
  }
164
- const routes = await sdk.buzz.notifications.list(requiredFlag(a, "--org"));
165
+ const routes = await sdk.buzz.notifications.list(await resolveOrgId(a, { cmd: "buzz" }));
165
166
  out({ buzz_project_event_routes: routes });
166
167
  } catch (err) {
167
168
  reportSdkError(err);
package/lib/buzz.mjs CHANGED
@@ -1,6 +1,7 @@
1
1
  import { readFileSync } from "node:fs";
2
2
  import { getSdk } from "./sdk.mjs";
3
3
  import { fail, reportSdkError } from "./sdk-errors.mjs";
4
+ import { resolveOrgId } from "./org-context.mjs";
4
5
  import {
5
6
  assertKnownFlags,
6
7
  failUnknownSubcommand,
@@ -130,7 +131,8 @@ async function adopt(args) {
130
131
  const a = normalizeArgv(rest);
131
132
  assertKnownFlags(a, ["--org"], ["--org"]);
132
133
  requirePositionalCount(a, ["--org"], { min: 0, max: 0, command: "run402 buzz adopt list --org <org_id>" });
133
- return invoke(() => getSdk().buzz.humanAdoptions.list(requiredFlag(a, "--org")));
134
+ const orgId = await resolveOrgId(a, { cmd: "buzz" });
135
+ return invoke(() => getSdk().buzz.humanAdoptions.list(orgId));
134
136
  }
135
137
  if (operation === "show") {
136
138
  const a = normalizeArgv(rest);
@@ -143,9 +145,10 @@ async function adopt(args) {
143
145
  const a = normalizeArgv(directArgs);
144
146
  const values = ["--org", "--identity-link", "--idempotency-key"];
145
147
  assertKnownFlags(a, values, values);
146
- requirePositionalCount(a, values, { min: 0, max: 0, command: "run402 buzz adopt direct --org <org_id> --identity-link <idlnk_id>" });
148
+ requirePositionalCount(a, values, { min: 0, max: 0, command: "run402 buzz adopt direct [--org <org_id>] --identity-link <idlnk_id>" });
149
+ const organizationId = await resolveOrgId(a, { cmd: "buzz" });
147
150
  return invoke(() => getSdk({ authMode: "wallet" }).buzz.adopt({
148
- organizationId: requiredFlag(a, "--org"),
151
+ organizationId,
149
152
  identityLinkId: requiredFlag(a, "--identity-link"),
150
153
  idempotencyKey: flagValue(a, "--idempotency-key") ?? undefined,
151
154
  }));
@@ -169,8 +172,9 @@ async function adoptionOffer(args) {
169
172
  const a = normalizeArgv(args);
170
173
  const values = ["--org", "--identity-link", "--deployment-context-file", "--idempotency-key"];
171
174
  assertKnownFlags(a, values, values);
172
- requirePositionalCount(a, values, { min: 0, max: 0, command: "run402 buzz adopt offer --org <org_id> --identity-link <idlnk_id>" });
175
+ requirePositionalCount(a, values, { min: 0, max: 0, command: "run402 buzz adopt offer [--org <org_id>] --identity-link <idlnk_id>" });
173
176
  const deploymentPath = flagValue(a, "--deployment-context-file");
177
+ const organizationId = await resolveOrgId(a, { cmd: "buzz" });
174
178
  return invoke(async () => {
175
179
  const client = getSdk({ authMode: "wallet" });
176
180
  const status = await client.buzz.status();
@@ -183,7 +187,7 @@ async function adoptionOffer(args) {
183
187
  });
184
188
  }
185
189
  return client.buzz.offerAdoption({
186
- organizationId: requiredFlag(a, "--org"),
190
+ organizationId,
187
191
  identityLinkId: requiredFlag(a, "--identity-link"),
188
192
  deploymentContext: deploymentPath ? readJsonFile(deploymentPath, "--deployment-context-file") : undefined,
189
193
  idempotencyKey: flagValue(a, "--idempotency-key") ?? undefined,
@@ -227,7 +231,8 @@ async function install(args) {
227
231
  const a = normalizeArgv(rest);
228
232
  assertKnownFlags(a, ["--org"], ["--org"]);
229
233
  requirePositionalCount(a, ["--org"], { min: 0, max: 0, command: "run402 buzz install list --org <org_id>" });
230
- return invoke(() => getSdk().buzz.communityInstallations.list(requiredFlag(a, "--org")));
234
+ const orgId = await resolveOrgId(a, { cmd: "buzz" });
235
+ return invoke(() => getSdk().buzz.communityInstallations.list(orgId));
231
236
  }
232
237
  if (operation === "discover") {
233
238
  const a = normalizeArgv(rest);
@@ -247,10 +252,11 @@ async function install(args) {
247
252
  const a = normalizeArgv(args);
248
253
  const values = ["--org", "--community", "--authority", "--policy-file", "--idempotency-key"];
249
254
  assertKnownFlags(a, values, values);
250
- requirePositionalCount(a, values, { min: 0, max: 0, command: "run402 buzz install --org <org_id> --community <subject> --authority <hex>" });
255
+ requirePositionalCount(a, values, { min: 0, max: 0, command: "run402 buzz install [--org <org_id>] --community <subject> --authority <hex>" });
251
256
  const policyFile = flagValue(a, "--policy-file");
257
+ const organizationId = await resolveOrgId(a, { cmd: "buzz" });
252
258
  return invoke(() => getSdk().buzz.install({
253
- organizationId: requiredFlag(a, "--org"),
259
+ organizationId,
254
260
  buzzCommunitySubject: requiredFlag(a, "--community"),
255
261
  buzzCommunityAuthoritySubject: requiredFlag(a, "--authority"),
256
262
  enrollmentPolicy: policyFile ? readJsonFile(policyFile, "--policy-file") : undefined,
package/lib/buzz.test.mjs CHANGED
@@ -92,7 +92,7 @@ describe("run402 buzz CLI", () => {
92
92
  try {
93
93
  await run("adopt", [
94
94
  "offer",
95
- "--org", `org_${"2".repeat(32)}`,
95
+ "--org", "22222222-2222-4222-8222-222222222222",
96
96
  "--identity-link", `idlnk_${"3".repeat(32)}`,
97
97
  "--deployment-context-file", contextPath,
98
98
  "--idempotency-key", "offer-after-demo-1",
@@ -15,6 +15,15 @@
15
15
  * projectScoped command operates on a project and accepts
16
16
  * `--project <id>` (precedence: --project > legacy
17
17
  * leading prj_ positional > active project)
18
+ * orgScoped command acts ON an organization and resolves it
19
+ * through the ONE shared chain (cli-org-context):
20
+ * an optional leading <org_id> positional, else
21
+ * --org, else RUN402_ORG, else the .run402.json
22
+ * binding, else `org use`. The gate drives every
23
+ * such entry through --org AND RUN402_ORG with no
24
+ * positional and fails on any "Missing <org_id>".
25
+ * Any entry with an `org_id` positional MUST be
26
+ * orgScoped and that positional MUST be optional.
18
27
  * legacyPositionalProject still accepts the legacy leading `prj_...`
19
28
  * positional project selector (compat, no warning)
20
29
  * minimalArgs args (beyond path) that satisfy LOCAL validation
@@ -40,6 +49,14 @@
40
49
 
41
50
  const p = (name, { required = true, variadic = false } = {}) => ({ name, required, variadic });
42
51
 
52
+ /**
53
+ * The org id the gate uses for every orgScoped entry — a well-formed UUID so
54
+ * the shared resolver accepts it from any rung (positional, --org, RUN402_ORG).
55
+ * The behavioral org-scope gate strips it from minimalArgs and re-supplies the
56
+ * org through each rung in turn.
57
+ */
58
+ export const GATE_ORG = "11111111-2222-3333-4444-555555555555";
59
+
43
60
  export const COMMAND_MANIFEST = [
44
61
  // ── external agent identity links ────────────────────────────────────────
45
62
  { path: ["identity", "link", "nostr", "begin"], positionals: [], projectScoped: false, legacyPositionalProject: false, minimalArgs: ["--pubkey", "4f355bdcb7cc0af728ef3cceb9615d90684bb5b2ca5f859ab0f0b704075871aa", "--visibility", "public"], skipBehavioral: "requires a live wallet and gateway challenge" },
@@ -50,16 +67,16 @@ export const COMMAND_MANIFEST = [
50
67
 
51
68
  // ── Buzz community ↔ Run402 control plane ───────────────────────────────
52
69
  { path: ["buzz", "status"], positionals: [], projectScoped: false, legacyPositionalProject: false, minimalArgs: [], skipBehavioral: "requires live principal authentication" },
53
- { path: ["buzz", "adopt", "offer"], positionals: [], projectScoped: false, legacyPositionalProject: false, minimalArgs: ["--org", "org_11111111111111111111111111111111", "--identity-link", "idlnk_11111111111111111111111111111111"], skipBehavioral: "requires eligible sole agent owner, live Buzz identity, and offer-capable gateway" },
54
- { path: ["buzz", "adopt", "direct"], positionals: [], projectScoped: false, legacyPositionalProject: false, minimalArgs: ["--org", "org_11111111111111111111111111111111", "--identity-link", "idlnk_11111111111111111111111111111111"], skipBehavioral: "advanced compatibility flow requiring eligible sole agent owner and live Buzz identity" },
55
- { path: ["buzz", "install"], positionals: [], projectScoped: false, legacyPositionalProject: false, minimalArgs: ["--org", "org_11111111111111111111111111111111", "--community", "buzz:community:relay.example", "--authority", "11".repeat(32)], skipBehavioral: "requires owner step-up and a compatible live Buzz relay" },
70
+ { path: ["buzz", "adopt", "offer"], positionals: [], projectScoped: false, orgScoped: true, legacyPositionalProject: false, minimalArgs: ["--identity-link", "idlnk_11111111111111111111111111111111"], skipBehavioral: "requires eligible sole agent owner, live Buzz identity, and offer-capable gateway" },
71
+ { path: ["buzz", "adopt", "direct"], positionals: [], projectScoped: false, orgScoped: true, legacyPositionalProject: false, minimalArgs: ["--identity-link", "idlnk_11111111111111111111111111111111"], skipBehavioral: "advanced compatibility flow requiring eligible sole agent owner and live Buzz identity" },
72
+ { path: ["buzz", "install"], positionals: [], projectScoped: false, orgScoped: true, legacyPositionalProject: false, minimalArgs: ["--community", "buzz:community:relay.example", "--authority", "11".repeat(32)], skipBehavioral: "requires owner step-up and a compatible live Buzz relay" },
56
73
  { path: ["buzz", "enroll"], positionals: [], projectScoped: false, legacyPositionalProject: false, minimalArgs: ["--installation", "buzzci_11111111111111111111111111111111", "--identity-link", "idlnk_11111111111111111111111111111111", "--grants-file", "__FIXTURE_FILE__", "--expires-at", "2026-08-01T00:00:00.000Z"], skipBehavioral: "requires live relay membership evidence and a grant fixture" },
57
74
  { path: ["buzz", "approve"], positionals: [p("buzz_agent_enrollment_id")], projectScoped: false, legacyPositionalProject: false, minimalArgs: ["buzzae_11111111111111111111111111111111", "--grants-file", "__FIXTURE_FILE__", "--descriptor-revision", "1", "--policy-revision", "1"], skipBehavioral: "requires owner step-up and a live pending enrollment" },
58
75
  { path: ["buzz", "deny"], positionals: [p("buzz_agent_enrollment_id")], projectScoped: false, legacyPositionalProject: false, minimalArgs: ["buzzae_11111111111111111111111111111111"], skipBehavioral: "requires owner step-up and a live pending enrollment" },
59
76
  { path: ["buzz", "revoke"], positionals: [p("buzz_agent_enrollment_id")], projectScoped: false, legacyPositionalProject: false, minimalArgs: ["buzzae_11111111111111111111111111111111"], skipBehavioral: "requires a live enrollment" },
60
77
  // Project-event routing into a Buzz channel (add-buzz-project-event-routing).
61
78
  // org ids on this surface are BARE dashed UUIDs, never org_-prefixed.
62
- { path: ["buzz", "notifications", "configure"], positionals: [], projectScoped: false, legacyPositionalProject: false, minimalArgs: ["--org", "11111111-1111-4111-8111-111111111111", "--installation", "buzzci_11111111111111111111111111111111", "--name", "deploys", "--channel", "22222222-2222-4222-8222-222222222222"], skipBehavioral: "requires live owner authentication and an active Buzz installation (and its repeatable --project scope flag is reserved by the gate)" },
79
+ { path: ["buzz", "notifications", "configure"], positionals: [], projectScoped: false, orgScoped: true, legacyPositionalProject: false, minimalArgs: ["--installation", "buzzci_11111111111111111111111111111111", "--name", "deploys", "--channel", "22222222-2222-4222-8222-222222222222"], skipBehavioral: "requires live owner authentication and an active Buzz installation (and its repeatable --project scope flag is reserved by the gate)" },
63
80
  { path: ["buzz", "notifications", "status"], positionals: [p("buzz_project_event_route_id", { required: false })], projectScoped: false, legacyPositionalProject: false, minimalArgs: ["--org", "11111111-1111-4111-8111-111111111111"], skipBehavioral: "requires live owner authentication" },
64
81
  { path: ["buzz", "notifications", "test"], positionals: [p("buzz_project_event_route_id")], projectScoped: false, legacyPositionalProject: false, minimalArgs: ["buzzper_11111111111111111111111111111111"], skipBehavioral: "requires live owner authentication and a live route" },
65
82
  { path: ["buzz", "notifications", "deliveries"], positionals: [p("buzz_project_event_route_id")], projectScoped: false, legacyPositionalProject: false, minimalArgs: ["buzzper_11111111111111111111111111111111"], skipBehavioral: "requires live owner authentication and a live route" },
@@ -194,28 +211,28 @@ export const COMMAND_MANIFEST = [
194
211
  // ── org ──────────────────────────────────────────────────────────────────
195
212
  { path: ["org", "create"], positionals: [], projectScoped: false, legacyPositionalProject: false, minimalArgs: [] },
196
213
  { path: ["org", "list"], positionals: [], projectScoped: false, legacyPositionalProject: false, minimalArgs: [] },
197
- { path: ["org", "get"], positionals: [p("org_id")], projectScoped: false, legacyPositionalProject: false, minimalArgs: ["org_gate1"] },
198
- { path: ["org", "rename"], positionals: [p("org_id")], projectScoped: false, legacyPositionalProject: false, minimalArgs: ["org_gate1", "--name", "Gate"] },
199
- { path: ["org", "payout-wallet"], positionals: [p("org_id")], projectScoped: false, legacyPositionalProject: false, minimalArgs: ["org_gate1", "--wallet", "0x1111111111111111111111111111111111111111"] },
214
+ { path: ["org", "get"], positionals: [p("org_id", { required: false })], projectScoped: false, orgScoped: true, legacyPositionalProject: false, minimalArgs: [GATE_ORG] },
215
+ { path: ["org", "rename"], positionals: [p("org_id", { required: false })], projectScoped: false, orgScoped: true, legacyPositionalProject: false, minimalArgs: [GATE_ORG, "--name", "Gate"] },
216
+ { path: ["org", "payout-wallet"], positionals: [p("org_id", { required: false })], projectScoped: false, orgScoped: true, legacyPositionalProject: false, minimalArgs: [GATE_ORG, "0x1111111111111111111111111111111111111111"] },
200
217
  // `--org` (not a positional org_id
201
218
  // like rename/payout-wallet above) goes through resolveOrg's SHAPE
202
219
  // validation (a real UUID) — "org_gate1" fails that locally, so this needs
203
220
  // the same UUID-shaped fixture `org use` below already established.
204
221
  { path: ["org", "slug"], positionals: [p("slug")], projectScoped: false, legacyPositionalProject: false, minimalArgs: ["acme", "--org", "11111111-2222-3333-4444-555555555555"] },
205
222
  { path: ["org", "whoami"], positionals: [], projectScoped: false, legacyPositionalProject: false, minimalArgs: [] },
206
- { path: ["org", "audit"], positionals: [p("org_id")], projectScoped: false, legacyPositionalProject: false, minimalArgs: ["org_gate1"] },
223
+ { path: ["org", "audit"], positionals: [p("org_id", { required: false })], projectScoped: false, orgScoped: true, legacyPositionalProject: false, minimalArgs: [GATE_ORG] },
207
224
  { path: ["org", "use"], positionals: [p("org_id")], projectScoped: false, legacyPositionalProject: false, minimalArgs: ["11111111-2222-3333-4444-555555555555"] },
208
225
  { path: ["org", "current"], positionals: [], projectScoped: false, legacyPositionalProject: false, minimalArgs: [] },
209
226
  { path: ["org", "clear"], positionals: [], projectScoped: false, legacyPositionalProject: false, minimalArgs: [] },
210
227
  { path: ["org", "bind"], positionals: [], projectScoped: false, legacyPositionalProject: false, minimalArgs: ["--org", "11111111-2222-3333-4444-555555555555"] },
211
228
  { path: ["org", "unbind"], positionals: [], projectScoped: false, legacyPositionalProject: false, minimalArgs: [] },
212
- { path: ["org", "member", "list"], positionals: [p("org_id")], projectScoped: false, legacyPositionalProject: false, minimalArgs: ["org_gate1"] },
213
- { path: ["org", "member", "add"], positionals: [p("org_id")], projectScoped: false, legacyPositionalProject: false, minimalArgs: ["org_gate1", "--wallet", "0x1111111111111111111111111111111111111111"] },
214
- { path: ["org", "member", "role"], positionals: [p("org_id")], projectScoped: false, legacyPositionalProject: false, minimalArgs: ["org_gate1", "--principal", "prn_gate1", "--role", "viewer"] },
215
- { path: ["org", "member", "rm"], positionals: [p("org_id")], projectScoped: false, legacyPositionalProject: false, minimalArgs: ["org_gate1", "--principal", "prn_gate1"] },
216
- { path: ["org", "invite", "list"], positionals: [p("org_id")], projectScoped: false, legacyPositionalProject: false, minimalArgs: ["org_gate1"] },
217
- { path: ["org", "invite", "create"], positionals: [p("org_id")], projectScoped: false, legacyPositionalProject: false, minimalArgs: ["org_gate1", "--email", "gate@example.com"] },
218
- { path: ["org", "invite", "rm"], positionals: [p("org_id")], projectScoped: false, legacyPositionalProject: false, minimalArgs: ["org_gate1", "--principal", "prn_gate1"] },
229
+ { path: ["org", "member", "list"], positionals: [p("org_id", { required: false })], projectScoped: false, orgScoped: true, legacyPositionalProject: false, minimalArgs: [GATE_ORG] },
230
+ { path: ["org", "member", "add"], positionals: [p("org_id", { required: false })], projectScoped: false, orgScoped: true, legacyPositionalProject: false, minimalArgs: [GATE_ORG, "0x1111111111111111111111111111111111111111"] },
231
+ { path: ["org", "member", "role"], positionals: [p("org_id", { required: false })], projectScoped: false, orgScoped: true, legacyPositionalProject: false, minimalArgs: [GATE_ORG, "--principal", "prn_gate1", "--role", "viewer"] },
232
+ { path: ["org", "member", "rm"], positionals: [p("org_id", { required: false })], projectScoped: false, orgScoped: true, legacyPositionalProject: false, minimalArgs: [GATE_ORG, "--principal", "prn_gate1"] },
233
+ { path: ["org", "invite", "list"], positionals: [p("org_id", { required: false })], projectScoped: false, orgScoped: true, legacyPositionalProject: false, minimalArgs: [GATE_ORG] },
234
+ { path: ["org", "invite", "create"], positionals: [p("org_id", { required: false })], projectScoped: false, orgScoped: true, legacyPositionalProject: false, minimalArgs: [GATE_ORG, "--email", "gate@example.com"] },
235
+ { path: ["org", "invite", "rm"], positionals: [p("org_id", { required: false })], projectScoped: false, orgScoped: true, legacyPositionalProject: false, minimalArgs: [GATE_ORG, "--principal", "prn_gate1"] },
219
236
 
220
237
  // ── grants ───────────────────────────────────────────────────────────────
221
238
  { path: ["grants", "create"], positionals: [p("wallet")], projectScoped: true, legacyPositionalProject: true, minimalArgs: ["0x1111111111111111111111111111111111111111", "--capability", "deploy"] },
@@ -285,13 +302,14 @@ export const COMMAND_MANIFEST = [
285
302
  { path: ["repos", "mirror"], positionals: [p("destination", { required: false })], projectScoped: true, legacyPositionalProject: false, minimalArgs: [], runStyle: "sub", skipBehavioral: "reads/writes mirror destination config beside the keystore and may move real bytes into a customer-owned bucket" },
286
303
  { path: ["repos", "fsck"], positionals: [], projectScoped: true, legacyPositionalProject: false, minimalArgs: [], runStyle: "sub", skipBehavioral: "walks the live head chain and materializes the ref map against the keystore's local pins" },
287
304
  { path: ["repos", "gc"], positionals: [], projectScoped: true, legacyPositionalProject: false, minimalArgs: [], runStyle: "sub", skipBehavioral: "publishes a checkpoint under a maintenance lease and materializes the live vault head to enumerate retention roots" },
288
- // `access repair`/`revoke-key`/`declare-exposure` are nested sub-verb
289
- // literals — one manifest entry each, all dispatched through the ONE
290
- // `run("access", ...)` case in repos.mjs.
305
+ // `access repair`/`revoke-key`/`declare-exposure`/`sync` are nested
306
+ // sub-verb literals — one manifest entry each, all dispatched through the
307
+ // ONE `run("access", ...)` case in repos.mjs.
291
308
  { path: ["repos", "access"], positionals: [], projectScoped: true, legacyPositionalProject: false, minimalArgs: [], runStyle: "sub", skipBehavioral: "reads the live org encryption-key directory + vault envelope recipients" },
292
309
  { path: ["repos", "access", "repair"], positionals: [], projectScoped: true, legacyPositionalProject: false, minimalArgs: ["--recipient-state-version", "0", "--recipient-revocation-version", "0"], runStyle: "sub", skipBehavioral: "owner+step-up-gated epoch rotation (D193-D203, rev 42) — samples a fresh epoch key and re-seals a live vault's recipients" },
293
310
  { path: ["repos", "access", "revoke-key"], positionals: [p("principal_id")], projectScoped: true, legacyPositionalProject: false, minimalArgs: ["prin_00000000000000000000000000000000"], runStyle: "sub", skipBehavioral: "owner+step-up-gated: declares a recipient's key revoked (org-scoped watermark) and drives a real epoch rotation off it" },
294
311
  { path: ["repos", "access", "declare-exposure"], positionals: [], projectScoped: true, legacyPositionalProject: false, minimalArgs: [], runStyle: "sub", skipBehavioral: "owner+step-up-gated: declares this vault's epoch secret exposed, forcing every subsequent ordinary push to refuse until a rotation lands" },
312
+ { path: ["repos", "access", "sync"], positionals: [], projectScoped: true, legacyPositionalProject: false, minimalArgs: [], runStyle: "sub", skipBehavioral: "gitvault-multi-writer (rev 47): on-demand writer-admission reconcile — resolves pending candidates' signing keys via the live org encryption-key directory and publishes a real add_writer_key head per candidate" },
295
313
  { path: ["repos", "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" },
296
314
  // Principal-scoped (one bundle covers every vault you can read), so no
297
315
  // --project — deliberately unlike its eleven vault-scoped siblings.
@@ -424,9 +442,9 @@ export const COMMAND_MANIFEST = [
424
442
 
425
443
  // ── billing ──────────────────────────────────────────────────────────────
426
444
  { path: ["billing", "create-email"], positionals: [p("email")], projectScoped: false, legacyPositionalProject: false, minimalArgs: ["gate@example.com"] },
427
- { path: ["billing", "link-wallet"], positionals: [p("org_id")], projectScoped: false, legacyPositionalProject: false, minimalArgs: ["00000000-0000-4000-8000-000000000001", "--wallet", "0x1111111111111111111111111111111111111111"] },
445
+ { path: ["billing", "link-wallet"], positionals: [p("org_id", { required: false })], projectScoped: false, orgScoped: true, legacyPositionalProject: false, minimalArgs: [GATE_ORG, "0x1111111111111111111111111111111111111111"] },
428
446
  { path: ["billing", "checkout"], positionals: [p("identifier")], projectScoped: false, legacyPositionalProject: false, minimalArgs: ["00000000-0000-4000-8000-000000000001", "--product", "email-pack"] },
429
- { path: ["billing", "auto-recharge"], positionals: [p("org_id")], projectScoped: false, legacyPositionalProject: false, minimalArgs: ["00000000-0000-4000-8000-000000000001", "--state", "on"] },
447
+ { path: ["billing", "auto-recharge"], positionals: [p("org_id", { required: false })], projectScoped: false, orgScoped: true, legacyPositionalProject: false, minimalArgs: [GATE_ORG, "--state", "on"] },
430
448
  { path: ["billing", "balance"], positionals: [p("identifier")], projectScoped: false, legacyPositionalProject: false, minimalArgs: ["gate@example.com"] },
431
449
  { path: ["billing", "history"], positionals: [p("identifier")], projectScoped: false, legacyPositionalProject: false, minimalArgs: ["gate@example.com"] },
432
450
 
package/lib/config.mjs CHANGED
@@ -55,7 +55,7 @@ export const API = getApiBase();
55
55
 
56
56
  /**
57
57
  * Wraps core's `readAllowance()` and converts the malformed-shape throw
58
- * (GH-194) into the canonical CLI failure envelope. Without this guard, every
58
+ * into the canonical CLI failure envelope. Without this guard, every
59
59
  * CLI subcommand that touches the allowance leaks a Node stack trace and
60
60
  * source paths the moment a user has a malformed `allowance.json`.
61
61
  *