@openparachute/vault 0.4.9-rc.9 → 0.5.0-rc.2

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 (62) hide show
  1. package/README.md +51 -54
  2. package/core/src/core.test.ts +4 -1
  3. package/core/src/indexed-fields.test.ts +151 -0
  4. package/core/src/indexed-fields.ts +98 -0
  5. package/core/src/mcp.ts +66 -43
  6. package/core/src/notes.ts +26 -2
  7. package/core/src/portable-md.test.ts +52 -0
  8. package/core/src/portable-md.ts +48 -0
  9. package/core/src/schema.ts +87 -14
  10. package/core/src/store.ts +117 -0
  11. package/core/src/types.ts +28 -0
  12. package/package.json +2 -2
  13. package/src/auth-hub-jwt.test.ts +191 -11
  14. package/src/auth-status.ts +12 -5
  15. package/src/auth.test.ts +135 -219
  16. package/src/auth.ts +158 -107
  17. package/src/cli.ts +306 -224
  18. package/src/config.ts +12 -4
  19. package/src/export-watch.test.ts +23 -0
  20. package/src/export-watch.ts +14 -0
  21. package/src/git-preflight.test.ts +70 -0
  22. package/src/git-preflight.ts +68 -0
  23. package/src/hub-jwt.test.ts +27 -2
  24. package/src/hub-jwt.ts +10 -0
  25. package/src/init-summary.test.ts +4 -4
  26. package/src/init-summary.ts +36 -10
  27. package/src/mcp-config.test.ts +4 -2
  28. package/src/mcp-http.ts +24 -3
  29. package/src/mcp-install-interactive.test.ts +33 -71
  30. package/src/mcp-install-interactive.ts +23 -76
  31. package/src/mcp-install.test.ts +156 -55
  32. package/src/mcp-install.ts +109 -3
  33. package/src/mcp-tools.ts +249 -74
  34. package/src/mirror-config.test.ts +107 -0
  35. package/src/mirror-config.ts +275 -9
  36. package/src/mirror-credentials.test.ts +168 -17
  37. package/src/mirror-credentials.ts +155 -32
  38. package/src/mirror-deps.ts +25 -16
  39. package/src/mirror-import.test.ts +122 -16
  40. package/src/mirror-import.ts +50 -16
  41. package/src/mirror-manager.test.ts +51 -0
  42. package/src/mirror-manager.ts +116 -22
  43. package/src/mirror-per-vault.test.ts +519 -0
  44. package/src/mirror-registry.ts +91 -14
  45. package/src/mirror-routes.test.ts +81 -21
  46. package/src/mirror-routes.ts +90 -16
  47. package/src/routes.ts +39 -2
  48. package/src/routing.test.ts +203 -118
  49. package/src/routing.ts +46 -59
  50. package/src/scopes.test.ts +0 -86
  51. package/src/scopes.ts +9 -97
  52. package/src/server.ts +102 -34
  53. package/src/storage.test.ts +132 -7
  54. package/src/token-store.test.ts +88 -169
  55. package/src/token-store.ts +123 -249
  56. package/src/vault-create.test.ts +12 -4
  57. package/src/vault.test.ts +408 -103
  58. package/web/ui/dist/assets/index-DDRo6F4u.js +60 -0
  59. package/web/ui/dist/index.html +1 -1
  60. package/src/tokens-routes.test.ts +0 -727
  61. package/src/tokens-routes.ts +0 -392
  62. package/web/ui/dist/assets/index-Degr8snN.js +0 -60
package/src/cli.ts CHANGED
@@ -98,9 +98,8 @@ import {
98
98
  } from "./daemon.ts";
99
99
  import { confirm, ask, askPassword, choose } from "./prompt.ts";
100
100
  import { resolveBindHostname } from "./bind.ts";
101
- import { generateToken, createToken, listTokens, revokeToken, migrateVaultKeys } from "./token-store.ts";
102
- import type { TokenPermission } from "./token-store.ts";
103
- import { resolveCreateTokenFlags, VAULT_SCOPES } from "./scopes.ts";
101
+ import { listTokens, revokeToken, migrateVaultKeys } from "./token-store.ts";
102
+ import { VAULT_SCOPES } from "./scopes.ts";
104
103
  import { validateVaultName, decideInitVaultName } from "./vault-name.ts";
105
104
  import { getVaultStore } from "./vault-store.ts";
106
105
  import { selfRegister } from "./self-register.ts";
@@ -161,7 +160,7 @@ switch (command) {
161
160
  await cmdInit(cmdArgs);
162
161
  break;
163
162
  case "create":
164
- cmdCreate(cmdArgs);
163
+ await cmdCreate(cmdArgs);
165
164
  break;
166
165
  case "list":
167
166
  case "ls":
@@ -222,6 +221,9 @@ switch (command) {
222
221
  case "export":
223
222
  await cmdExport(cmdArgs);
224
223
  break;
224
+ case "schema":
225
+ await cmdSchema(cmdArgs);
226
+ break;
225
227
  case "help":
226
228
  case "--help":
227
229
  case "-h":
@@ -295,12 +297,19 @@ async function cmdInit(args: string[] = []) {
295
297
  // piped installs keep working unchanged.
296
298
  const vaults = listVaults();
297
299
  let apiKey: string | undefined;
300
+ // Guidance carried from the bootstrap-credential step — surfaced at the end
301
+ // when no token could be issued (standalone, no hub) so the operator knows
302
+ // how to make the vault reachable. vault#282 Stage 2.
303
+ let credentialGuidance: string | undefined;
298
304
  if (vaults.length === 0) {
299
305
  const chosenName =
300
306
  nameDecision.kind === "name" ? nameDecision.name : await promptVaultName();
301
307
  console.log(`Creating vault "${chosenName}"...`);
302
- apiKey = createVault(chosenName);
308
+ const credential = await createVault(chosenName);
309
+ apiKey = credential.token ?? undefined;
310
+ credentialGuidance = credential.guidance;
303
311
  console.log(` Created vault: ${chosenName}`);
312
+ console.log(` ${credential.guidance}`);
304
313
  } else {
305
314
  if (vaultNameFlagSupplied) {
306
315
  console.log(
@@ -482,25 +491,29 @@ async function cmdInit(args: string[] = []) {
482
491
  }
483
492
 
484
493
  // Mint a token if we need one (for the claude.json entry and/or for
485
- // prominent display) and don't already have one from vault creation.
486
- // Re-runs of init that opt in will mint a fresh token — old tokens
487
- // continue to work; the user can `tokens revoke` the unused ones.
494
+ // prominent display) and don't already have one from vault creation
495
+ // e.g. a re-run of init against an existing vault. vault#282 Stage 2: vault
496
+ // no longer mints pvt_* tokens, so this is a hub JWT via the operator.token
497
+ // → hub mint-token path (`mintBootstrapCredential`). When no hub is
498
+ // reachable, `apiKey` stays undefined and we carry the guidance to the
499
+ // summary — the operator runs `mcp-install` once a hub is up, or sets
500
+ // VAULT_AUTH_TOKEN.
488
501
  const defaultVault = globalConfig.default_vault || "default";
489
502
  const needToken = addMcp || addToken;
490
503
  if (needToken && !apiKey) {
491
- const store = getVaultStore(defaultVault);
492
- const { fullToken } = generateToken();
493
- createToken(store.db, fullToken, { label: "init", permission: "full" });
494
- apiKey = fullToken;
504
+ const credential = await mintBootstrapCredential(defaultVault);
505
+ apiKey = credential.token ?? undefined;
506
+ credentialGuidance = credential.guidance;
507
+ if (!apiKey) console.log(` ${credential.guidance}`);
495
508
  }
496
509
 
497
510
  if (addMcp) {
498
- // Init's bootstrap path stays on the pvt_* shape so a fresh-install
499
- // without a hub still works out of the box. Operators with a hub can
500
- // re-run `parachute-vault mcp-install` (defaults to hub-mint) to
501
- // upgrade. Goes through `buildMcpEntryPlan` for entryKey + url so this
502
- // path shares the writer-side invariant with `executeMcpInstall` a
503
- // future URL-shape change can't drift between init and mcp-install.
511
+ // Goes through `buildMcpEntryPlan` for entryKey + url so this path shares
512
+ // the writer-side invariant with `executeMcpInstall` a future URL-shape
513
+ // change can't drift between init and mcp-install. The bearer is the hub
514
+ // JWT minted above (omitted when no hub was reachable — the entry is then
515
+ // written unauthenticated, and the operator re-runs `mcp-install` once a
516
+ // hub is up).
504
517
  const target = resolveInstallTarget("user");
505
518
  const { entryKey, url, source } = buildMcpEntryPlan({
506
519
  vaultName: defaultVault,
@@ -531,6 +544,7 @@ async function cmdInit(args: string[] = []) {
531
544
  bindHost,
532
545
  port,
533
546
  mcpUrl,
547
+ noTokenGuidance: credentialGuidance,
534
548
  });
535
549
  for (const line of lines) console.log(line);
536
550
  }
@@ -794,7 +808,7 @@ async function cmd2fa(args: string[]) {
794
808
  process.exit(1);
795
809
  }
796
810
 
797
- function cmdCreate(args: string[]) {
811
+ async function cmdCreate(args: string[]) {
798
812
  // --json: emit a single machine-readable object on stdout instead of the
799
813
  // human-friendly multi-line print. Designed for orchestrators (the hub's
800
814
  // POST /vaults shells out to this CLI and parses stdout). Errors still go
@@ -832,7 +846,7 @@ function cmdCreate(args: string[]) {
832
846
 
833
847
  ensureConfigDirSync();
834
848
  const wasFirst = listVaults().length === 0;
835
- const key = createVault(name);
849
+ const credential = await createVault(name);
836
850
 
837
851
  // If this is the only vault now, make it the default so unscoped routes
838
852
  // (/mcp, /api/*, /oauth/*) target it. Avoids the "single vault named
@@ -867,9 +881,16 @@ function cmdCreate(args: string[]) {
867
881
  });
868
882
 
869
883
  if (jsonMode) {
884
+ // Contract (hub's admin-vaults.ts requires `typeof token === "string"`):
885
+ // emit the minted hub JWT when present. When no hub was reachable
886
+ // (standalone create — no operator.token / no hub origin), `token` is the
887
+ // empty string and `token_guidance` carries the operator's next step. Hub
888
+ // only shells out to `create --json` while IT is the orchestrator (a hub is
889
+ // running, operator.token present), so that path always mints a real JWT.
870
890
  const payload = {
871
891
  name,
872
- token: key,
892
+ token: credential.token ?? "",
893
+ token_guidance: credential.guidance,
873
894
  paths: {
874
895
  vault_dir: vaultDir(name),
875
896
  vault_db: vaultDbPath(name),
@@ -883,8 +904,13 @@ function cmdCreate(args: string[]) {
883
904
 
884
905
  console.log(`Vault "${name}" created.`);
885
906
  console.log(` Path: ${vaultDir(name)}`);
886
- console.log(` API token: ${key}`);
887
- console.log(` Save this — it will not be shown again.`);
907
+ if (credential.token) {
908
+ console.log(` API token: ${credential.token}`);
909
+ console.log(` ${credential.guidance}`);
910
+ console.log(` Save this — it will not be shown again.`);
911
+ } else {
912
+ console.log(` ${credential.guidance}`);
913
+ }
888
914
  if (defaultNote) {
889
915
  console.log(` ${defaultNote}`);
890
916
  }
@@ -922,22 +948,20 @@ function takeArgValue(args: string[], name: string): { value?: string; missingVa
922
948
 
923
949
  /**
924
950
  * `parachute-vault mcp-install` — install the vault MCP server into an
925
- * AI client's config. Three auth modes (mutually exclusive):
951
+ * AI client's config. Two auth modes (mutually exclusive — vault#282 Stage 2
952
+ * dropped the `--legacy-pat` pvt_* mint; vault is a pure hub resource-server):
926
953
  *
927
954
  * --mint (default) Mint a hub JWT via `POST <hub>/api/auth/mint-token`
928
955
  * using the local operator token.
929
- * --token <bearer> Use an existing token (hub JWT, pvt_*, anything).
930
- * --legacy-pat Mint a vault-DB `pvt_*` token (deprecated;
931
- * self-hosted-without-hub setups).
956
+ * --token <bearer> Use an existing bearer (hub JWT or VAULT_AUTH_TOKEN).
932
957
  *
933
958
  * Targeting:
934
959
  * --scope <verb> vault:read | vault:write | vault:admin (default: vault:read).
935
960
  * For --mint, expands to vault:<vault-name>:<verb>.
936
- * vault:admin requires --legacy-pat hub policy makes
937
- * per-vault admin non-requestable via mint-token (it's
938
- * operator-only, minted only through the session-
939
- * cookie-gated admin SPA endpoint), so --mint + admin
940
- * is rejected pre-flight.
961
+ * vault:admin is mintable via --mint as of hub PR-A
962
+ * (hub#449): hub mints per-vault admin when the operator
963
+ * bearer carries parachute:host:admin (the default
964
+ * operator.token does). Requires a hub running PR-A.
941
965
  * --install-scope <s> local (default) | user | project. local writes to
942
966
  * ~/.claude.json under projects[<cwd>].mcpServers
943
967
  * (private, this directory only — matches Claude
@@ -958,7 +982,6 @@ async function cmdMcpInstall(args: string[]): Promise<void> {
958
982
  // dead zone when the function body first executes from that dispatch.
959
983
  const MCP_INSTALL_FLAG_NAMES = [
960
984
  "--mint",
961
- "--legacy-pat",
962
985
  "--token",
963
986
  "--scope",
964
987
  "--install-scope",
@@ -980,22 +1003,21 @@ async function cmdMcpInstall(args: string[]): Promise<void> {
980
1003
  return await cmdMcpInstallInteractive();
981
1004
  }
982
1005
 
983
- // --- Auth-mode parsing (mutually exclusive) ---
1006
+ // --- Auth-mode parsing (mutually exclusive). vault#282 Stage 2 dropped
1007
+ // --legacy-pat (the pvt_* mint) — vault is a pure hub resource-server, so
1008
+ // the only auth modes are --mint (hub JWT, default) and --token (paste). ---
984
1009
  const wantMint = args.includes("--mint");
985
- const wantLegacy = args.includes("--legacy-pat");
986
1010
  const tokenArg = takeArgValue(args, "--token");
987
1011
  if (tokenArg.missingValue) {
988
1012
  console.error("--token requires a value (the bearer token to embed).");
989
1013
  process.exit(1);
990
1014
  }
991
1015
  const wantToken = tokenArg.value !== undefined;
992
- const modesSet = (wantMint ? 1 : 0) + (wantLegacy ? 1 : 0) + (wantToken ? 1 : 0);
993
- if (modesSet > 1) {
994
- console.error("--mint, --token, and --legacy-pat are mutually exclusive.");
1016
+ if (wantMint && wantToken) {
1017
+ console.error("--mint and --token are mutually exclusive.");
995
1018
  process.exit(1);
996
1019
  }
997
- const mode: "mint" | "token" | "legacy-pat" =
998
- wantToken ? "token" : wantLegacy ? "legacy-pat" : "mint";
1020
+ const mode: "mint" | "token" = wantToken ? "token" : "mint";
999
1021
 
1000
1022
  // --- Scope parsing. Default vault:read (least-privilege). ---
1001
1023
  const scopeArg = takeArgValue(args, "--scope");
@@ -1130,7 +1152,7 @@ async function cmdMcpInstallInteractive(): Promise<void> {
1130
1152
  * the canonical synthesizer — the JSON shape is owned here once.
1131
1153
  *
1132
1154
  * No state is mutated; this only emits to stdout. The bearer is read from
1133
- * `--token <pvt_...>` or `PARACHUTE_VAULT_TOKEN` env (deliberate — we don't
1155
+ * `--token <bearer>` or `PARACHUTE_VAULT_TOKEN` env (deliberate — we don't
1134
1156
  * mint here, since this is a stdout-piped subprocess where prompting would
1135
1157
  * deadlock the parent script). If neither is present, we exit 1 with a
1136
1158
  * clear stderr message; runners get a fail-fast.
@@ -1147,7 +1169,7 @@ async function cmdMcpInstallInteractive(): Promise<void> {
1147
1169
  async function cmdMcpConfig(args: string[]): Promise<void> {
1148
1170
  const vaultName = args[0];
1149
1171
  if (!vaultName || vaultName.startsWith("--")) {
1150
- console.error("Usage: parachute-vault mcp-config <vault-name> [--token <pvt_...>] [--base-url <url>] [--env-vars]");
1172
+ console.error("Usage: parachute-vault mcp-config <vault-name> [--token <bearer>] [--base-url <url>] [--env-vars]");
1151
1173
  console.error("");
1152
1174
  console.error("Emits the JSON config consumed by `claude -p --mcp-config '<json>'`.");
1153
1175
  console.error("Pattern: claude -p --mcp-config \"$(parachute-vault mcp-config <name>)\" --strict-mcp-config ...");
@@ -1192,7 +1214,7 @@ async function cmdMcpConfig(args: string[]): Promise<void> {
1192
1214
  const bearer = tokenArg.value ?? process.env.PARACHUTE_VAULT_TOKEN;
1193
1215
  if (!bearer) {
1194
1216
  console.error("No bearer token provided. Pass --token <bearer> or set PARACHUTE_VAULT_TOKEN.");
1195
- console.error(" Mint a token with: parachute-vault tokens create --vault " + vaultName);
1217
+ console.error(" Mint a hub JWT with: parachute-vault mcp-install --vault " + vaultName + " (or `parachute auth mint-token`)");
1196
1218
  console.error(" Or use --env-vars to emit the template form (safe to commit; expands at runtime).");
1197
1219
  process.exit(1);
1198
1220
  }
@@ -1219,7 +1241,7 @@ async function cmdMcpConfig(args: string[]): Promise<void> {
1219
1241
  }
1220
1242
 
1221
1243
  interface ExecuteMcpInstallOpts {
1222
- mode: "mint" | "token" | "legacy-pat";
1244
+ mode: "mint" | "token";
1223
1245
  /** Full scope string (e.g. "vault:read"). The verb segment narrows downstream. */
1224
1246
  rawScope: string;
1225
1247
  installScope: InstallScope;
@@ -1303,59 +1325,24 @@ async function executeMcpInstall(opts: ExecuteMcpInstallOpts): Promise<void> {
1303
1325
  }
1304
1326
  bearer = pastedToken;
1305
1327
  console.log(`Using supplied token (skipping mint).`);
1306
- } else if (mode === "legacy-pat") {
1307
- console.error(
1308
- "Note: --legacy-pat mints a vault-DB pvt_* token. The hub-issued JWT path (--mint, default) " +
1309
- "is the canonical install going forward; pvt_* support is preserved for self-hosted-without-hub " +
1310
- "setups, tracked at vault#288, planned removal 0.6.0.",
1311
- );
1312
- const store = getVaultStore(vaultName);
1313
- const { fullToken } = generateToken();
1314
- // Narrow the pvt_* to the requested verb's scope set when not full-admin.
1315
- // `scopes: undefined` leaves the token at full vault permissions
1316
- // (admin); narrowing to a single-scope array gates it to that verb.
1317
- const createTokenOpts: Parameters<typeof createToken>[2] = {
1318
- label: "mcp-install",
1319
- permission: verb === "read" ? "read" : "full",
1320
- vault_name: vaultName,
1321
- };
1322
- if (verb !== "admin") {
1323
- createTokenOpts.scopes = [rawScope];
1324
- }
1325
- createToken(store.db, fullToken, createTokenOpts);
1326
- bearer = fullToken;
1327
1328
  } else {
1328
1329
  // mode === "mint"
1329
- // Pre-flight: hub policy rejects `vault:<name>:admin` via the public
1330
- // mint-token endpoint. Per-vault admin is operator-only, mintable
1331
- // only through the session-cookie-gated `/admin/vault-admin-token/:name`
1332
- // SPA path. Calling hub with admin would surface a 400:
1333
- // "Hub mint-token rejected (HTTP 400, invalid_scope):
1334
- // scope vault:default:admin is not requestable via mint-token;
1335
- // use OAuth flow or operator rotation"
1336
- // Fail early with the actionable remediation rather than letting
1337
- // the operator chase the hub's wire-level error. See
1338
- // `parachute-hub/src/scope-explanations.ts` (VAULT_ADMIN_RE) and
1339
- // `parachute-hub/src/api-mint-token.ts` (non-requestable guard).
1340
- if (verb === "admin") {
1341
- console.error(
1342
- "Hub policy: vault:<name>:admin is not requestable via mint-token " +
1343
- "(per-vault admin is operator-only, minted only by the session-cookie-gated " +
1344
- "admin SPA at <hub>/admin/vaults/" + vaultName + ").\n" +
1345
- " Fix: use `--legacy-pat --scope vault:admin` to mint a vault-DB pvt_* with admin scope " +
1346
- "(the right shape for an MCP entry needing schema management).\n" +
1347
- " Or: drop --scope to default to vault:read (least privilege), or use --scope vault:write.",
1348
- );
1349
- process.exit(1);
1350
- }
1330
+ // `vault:<name>:admin` is mintable via the hub mint-token endpoint as
1331
+ // of hub PR-A (hub#449): the hub mints per-vault admin when the calling
1332
+ // operator bearer carries `parachute:host:admin` (which the default
1333
+ // operator.token does). No admin pre-flight reject the narrowScope
1334
+ // below produces `vault:<name>:admin` and the hub honors it. This
1335
+ // requires a hub running PR-A; older hubs reject admin with HTTP 400
1336
+ // invalid_scope, surfaced via the api-error branch below. There is no
1337
+ // local pvt_* fallback anymore (vault#282 Stage 2) without a hub, the
1338
+ // operator pastes an existing bearer via `--token` or sets VAULT_AUTH_TOKEN.
1351
1339
  const operatorToken = readOperatorToken();
1352
1340
  if (!operatorToken) {
1353
1341
  console.error(
1354
1342
  "No operator token found at ~/.parachute/operator.token. The default install path " +
1355
1343
  "(--mint) requires a hub-issued operator token to mint scope-narrow JWTs.\n" +
1356
1344
  " Fix: run `parachute auth rotate-operator` to create one, then re-run.\n" +
1357
- " Or: use `--token <bearer>` to paste an existing token, or `--legacy-pat` to " +
1358
- "mint a vault-DB pvt_* token (self-hosted-without-hub).",
1345
+ " Or: use `--token <bearer>` to paste an existing token, or set VAULT_AUTH_TOKEN.",
1359
1346
  );
1360
1347
  process.exit(1);
1361
1348
  }
@@ -1367,7 +1354,7 @@ async function executeMcpInstall(opts: ExecuteMcpInstallOpts): Promise<void> {
1367
1354
  "Hub-mint (--mint) needs a real hub URL to call. Either:\n" +
1368
1355
  " - Start the hub and set PARACHUTE_HUB_ORIGIN, OR\n" +
1369
1356
  " - Bring up an exposure (`parachute expose tailnet`), OR\n" +
1370
- " - Use --legacy-pat to mint a vault-DB pvt_* token instead.",
1357
+ " - Use --token <bearer> to paste an existing token instead.",
1371
1358
  );
1372
1359
  process.exit(1);
1373
1360
  }
@@ -1384,13 +1371,24 @@ async function executeMcpInstall(opts: ExecuteMcpInstallOpts): Promise<void> {
1384
1371
  console.error(
1385
1372
  `Hub unreachable at ${result.origin} — ${result.cause}.\n` +
1386
1373
  ` Fix: verify the hub is running and PARACHUTE_HUB_ORIGIN is set, ` +
1387
- `or use --legacy-pat to skip hub-mint.`,
1374
+ `or use --token <bearer> to skip hub-mint.`,
1388
1375
  );
1389
1376
  break;
1390
1377
  case "api-error":
1391
1378
  console.error(
1392
1379
  `Hub mint-token rejected (HTTP ${result.status}, ${result.error}): ${result.description}`,
1393
1380
  );
1381
+ // Older hubs (pre-hub#449) reject vault:<name>:admin as a
1382
+ // non-requestable scope with HTTP 400 invalid_scope. Surface an
1383
+ // actionable hint so the operator reaches for the upgrade rather
1384
+ // than chasing the wire-level error.
1385
+ if (result.status === 400 && verb === "admin") {
1386
+ console.error(
1387
+ " Hint: minting vault:admin requires a hub with per-vault admin mint support " +
1388
+ "(hub#449). Your hub may predate it — upgrade the hub, or use --token <bearer> " +
1389
+ "to paste an existing admin token instead.",
1390
+ );
1391
+ }
1394
1392
  break;
1395
1393
  }
1396
1394
  process.exit(1);
@@ -1580,94 +1578,29 @@ function cmdTokens(args: string[]) {
1580
1578
  }
1581
1579
 
1582
1580
  if (!anyTokens) {
1583
- console.log("No tokens found. Create one: parachute-vault tokens create");
1581
+ console.log(
1582
+ "No vault-DB tokens found. Vault tokens are now hub-issued JWTs — " +
1583
+ "run `parachute-vault mcp-install` to mint one.",
1584
+ );
1584
1585
  }
1585
1586
  return;
1586
1587
  }
1587
1588
 
1588
- // parachute-vault tokens create [--vault <name> | --all]
1589
- // [--scope vault:read,vault:write | --read | --permission full|read]
1590
- // [--expires <duration>] [--label <label>]
1591
- //
1592
- // Per-vault binding (v16): the minted token is pinned to <vaultName>
1593
- // unless --all is passed, in which case the token is server-wide
1594
- // (vault_name = NULL) and authenticates against any vault. --all is
1595
- // the explicit opt-out — there's no implicit fall-through to server-wide.
1589
+ // `tokens create` was removed at 0.5.0 (vault#282 Stage 2). Vault no longer
1590
+ // mints its own (pvt_*) tokens — it's a pure hub resource-server. Tokens are
1591
+ // now hub-issued JWTs: run `parachute-vault mcp-install` to mint + wire one
1592
+ // for an MCP client, or `parachute auth mint-token --scope vault:<name>:<verb>`
1593
+ // for scripts. `tokens list` / `tokens revoke` remain for cleaning up any
1594
+ // vestigial pre-0.5.0 rows.
1596
1595
  if (subcmd === "create") {
1597
- const vaultFlag = args.indexOf("--vault");
1598
- const allFlag = args.includes("--all");
1599
- if (allFlag && vaultFlag !== -1) {
1600
- console.error("--vault and --all are mutually exclusive.");
1601
- process.exit(1);
1602
- }
1603
- const vaultName = vaultFlag !== -1 ? args[vaultFlag + 1] : (readGlobalConfig().default_vault || "default");
1604
- if (!vaultName) {
1605
- console.error("--vault requires a value.");
1606
- process.exit(1);
1607
- }
1608
-
1609
- const vc = readVaultConfig(vaultName);
1610
- if (!vc) {
1611
- console.error(`Vault "${vaultName}" not found.`);
1612
- process.exit(1);
1613
- }
1614
-
1615
- // Combining --scope / --read / --permission is always an error: a
1616
- // user minting a token expects exactly one narrowing signal, and
1617
- // silently picking one would mint the opposite of what the other
1618
- // reading intended. See resolveCreateTokenFlags.
1619
- const resolved = resolveCreateTokenFlags(args);
1620
- if (resolved.error) {
1621
- console.error(resolved.error);
1622
- process.exit(1);
1623
- }
1624
- const scopes = resolved.scopes;
1625
- const permission: TokenPermission = resolved.permission;
1626
-
1627
- const expiresFlag = args.indexOf("--expires");
1628
- let expiresAt: string | null = null;
1629
- if (expiresFlag !== -1) {
1630
- const dur = args[expiresFlag + 1];
1631
- if (!dur) {
1632
- console.error("--expires requires a value (e.g. 7d, 30d, 24h, 1y).");
1633
- process.exit(1);
1634
- }
1635
- expiresAt = parseDuration(dur);
1636
- if (!expiresAt) {
1637
- console.error(`Invalid duration: ${dur}. Use format like 7d, 30d, 24h, 1y.`);
1638
- process.exit(1);
1639
- }
1640
- }
1641
-
1642
- const labelFlag = args.indexOf("--label");
1643
- const label = (labelFlag !== -1 ? args[labelFlag + 1] : undefined) ?? "default";
1644
-
1645
- const store = getVaultStore(vaultName);
1646
- const { fullToken } = generateToken();
1647
- createToken(store.db, fullToken, {
1648
- label,
1649
- permission,
1650
- scopes,
1651
- expires_at: expiresAt,
1652
- // v16 binding: pin to the vault we minted in unless --all was passed
1653
- // (which leaves vault_name NULL = legacy server-wide).
1654
- vault_name: allFlag ? null : vaultName,
1655
- });
1656
-
1657
- const displayScopes = scopes ?? [...VAULT_SCOPES];
1658
- const heading = allFlag
1659
- ? `Created server-wide token (authenticates against any vault):`
1660
- : `Created token for vault "${vaultName}":`;
1661
- console.log(heading);
1662
- console.log(` Token: ${fullToken}`);
1663
- console.log(` Permission: ${permission}`);
1664
- console.log(` Scopes: ${displayScopes.join(" ")}`);
1665
- if (expiresAt) console.log(` Expires: ${expiresAt}`);
1666
- console.log(` Label: ${label}`);
1667
- if (!allFlag) console.log(` Vault: ${vaultName}`);
1668
- console.log();
1669
- console.log("Save this token — it will not be shown again.");
1670
- return;
1596
+ console.error(
1597
+ "`parachute-vault tokens create` was removed at 0.5.0 — vault no longer mints its own tokens.\n" +
1598
+ " Mint a hub-issued JWT instead:\n" +
1599
+ " parachute-vault mcp-install --scope vault:<verb> # wire an MCP client\n" +
1600
+ " parachute auth mint-token --scope vault:<name>:<verb> # for scripts\n" +
1601
+ " See UPGRADING.md (pvt_* token removal, vault#282).",
1602
+ );
1603
+ process.exit(1);
1671
1604
  }
1672
1605
 
1673
1606
  // parachute-vault tokens revoke <token-id> --vault <name>
@@ -1706,23 +1639,6 @@ function cmdTokens(args: string[]) {
1706
1639
  process.exit(1);
1707
1640
  }
1708
1641
 
1709
- function parseDuration(dur: string): string | null {
1710
- const match = dur.match(/^(\d+)(h|d|w|m|y)$/);
1711
- if (!match) return null;
1712
- const n = parseInt(match[1]!, 10);
1713
- const unit = match[2]!;
1714
- const now = new Date();
1715
- switch (unit) {
1716
- case "h": now.setHours(now.getHours() + n); break;
1717
- case "d": now.setDate(now.getDate() + n); break;
1718
- case "w": now.setDate(now.getDate() + n * 7); break;
1719
- case "m": now.setMonth(now.getMonth() + n); break;
1720
- case "y": now.setFullYear(now.getFullYear() + n); break;
1721
- default: return null;
1722
- }
1723
- return now.toISOString();
1724
- }
1725
-
1726
1642
  async function cmdServe() {
1727
1643
  await import("./server.ts");
1728
1644
  }
@@ -3217,6 +3133,92 @@ async function cmdExport(args: string[]) {
3217
3133
  await new Promise(() => {});
3218
3134
  }
3219
3135
 
3136
+ // ---------------------------------------------------------------------------
3137
+ // Schema maintenance — `parachute-vault schema <subcommand>`
3138
+ // ---------------------------------------------------------------------------
3139
+
3140
+ /**
3141
+ * `parachute-vault schema prune` — drop orphaned indexed-field columns +
3142
+ * indexes whose declaring tags no longer exist (the gitcoin orphaned-fields
3143
+ * bug). Dry-run by default; `--apply` (alias `--yes`) executes. A field
3144
+ * co-declared by a still-live tag is never dropped — only the dead declarers
3145
+ * are trimmed. Dropping a generated column loses only the index; the source
3146
+ * values stay in notes.metadata, so the column rebuilds when the field is
3147
+ * declared again.
3148
+ */
3149
+ async function cmdSchema(args: string[] = []) {
3150
+ const sub = args[0];
3151
+ if (sub !== "prune") {
3152
+ console.error("Usage: parachute-vault schema prune [--vault <name>] [--dry-run|--apply|--yes]");
3153
+ console.error("\nSubcommands:");
3154
+ console.error(" prune Drop orphaned indexed-field columns whose declaring tags are gone.");
3155
+ console.error(" Dry-run by default (--dry-run is an explicit alias); pass --apply (or --yes) to execute.");
3156
+ process.exit(1);
3157
+ }
3158
+
3159
+ let vaultName = "default";
3160
+ let apply = false;
3161
+ for (let i = 1; i < args.length; i++) {
3162
+ const arg = args[i]!;
3163
+ if (arg === "--vault") {
3164
+ const v = args[++i];
3165
+ if (!v) {
3166
+ console.error("--vault requires a value.");
3167
+ process.exit(1);
3168
+ }
3169
+ vaultName = v;
3170
+ } else if (arg === "--apply" || arg === "--yes") {
3171
+ apply = true;
3172
+ } else if (arg === "--dry-run") {
3173
+ // Dry-run is the default; accept the flag as an explicit affirmative
3174
+ // for convention + scriptability. --apply wins if both are passed.
3175
+ } else {
3176
+ console.error(`Unknown flag for \`schema prune\`: ${arg}`);
3177
+ process.exit(1);
3178
+ }
3179
+ }
3180
+
3181
+ const config = readVaultConfig(vaultName);
3182
+ if (!config) {
3183
+ console.error(`Vault "${vaultName}" not found.`);
3184
+ process.exit(1);
3185
+ }
3186
+
3187
+ const { getVaultStore } = await import("./vault-store.ts");
3188
+ const store = getVaultStore(vaultName);
3189
+ const plan = await store.pruneIndexedFields({ dryRun: !apply });
3190
+
3191
+ const dropped = plan.filter((p) => p.dropped);
3192
+ const trimmed = plan.filter((p) => !p.dropped);
3193
+
3194
+ if (plan.length === 0) {
3195
+ console.log(`No orphaned indexed fields in vault "${vaultName}". Nothing to prune.`);
3196
+ return;
3197
+ }
3198
+
3199
+ console.log(
3200
+ apply
3201
+ ? `Pruned orphaned indexed fields in vault "${vaultName}":`
3202
+ : `Would prune orphaned indexed fields in vault "${vaultName}" (dry-run):`,
3203
+ );
3204
+ for (const p of dropped) {
3205
+ console.log(
3206
+ ` ${apply ? "DROPPED" : "drop "} ${p.field} (dead declarers: ${p.deadDeclarers.join(", ")})`,
3207
+ );
3208
+ }
3209
+ for (const p of trimmed) {
3210
+ console.log(
3211
+ ` ${apply ? "TRIMMED" : "trim "} ${p.field} (removed dead declarers: ${p.deadDeclarers.join(", ")}; column kept — still co-declared)`,
3212
+ );
3213
+ }
3214
+ console.log(
3215
+ `\n${apply ? "Dropped" : "Would drop"} ${dropped.length} orphaned field(s); ${apply ? "trimmed" : "would trim"} ${trimmed.length} co-declared field(s).`,
3216
+ );
3217
+ if (!apply) {
3218
+ console.log("Re-run with --apply (or --yes) to execute.");
3219
+ }
3220
+ }
3221
+
3220
3222
  // ---------------------------------------------------------------------------
3221
3223
  // Export-watch glue. The git-shell + commit-message logic lives in
3222
3224
  // `./export-watch.ts` for unit-testability; cli.ts just wires it in.
@@ -3269,7 +3271,86 @@ async function firstChangedNoteTitle(
3269
3271
  // Helpers
3270
3272
  // ---------------------------------------------------------------------------
3271
3273
 
3272
- function createVault(name: string): string {
3274
+ /**
3275
+ * Outcome of bootstrapping a fresh vault's first credential (vault#282 Stage 2).
3276
+ *
3277
+ * Vault no longer mints `pvt_*` tokens. The first credential for a new vault is
3278
+ * a hub-issued JWT, minted via the same operator.token → hub mint-token path
3279
+ * `mcp-install --mint` uses (cli.ts ~`cmdMcpInstall`). When no hub is reachable
3280
+ * (standalone install, no operator.token, or no real hub origin), `token` is
3281
+ * null and `guidance` carries the operator's next step.
3282
+ */
3283
+ interface VaultCredential {
3284
+ /** Hub-issued JWT scoped to `vault:<name>:admin`, or null when no hub is reachable. */
3285
+ token: string | null;
3286
+ /** Human-readable note: how the token was issued, or why it wasn't. */
3287
+ guidance: string;
3288
+ }
3289
+
3290
+ /**
3291
+ * Mint the first credential for a freshly-created vault.
3292
+ *
3293
+ * Decision (vault#282 Stage 2): when a hub is reachable (operator.token present
3294
+ * AND a real hub origin resolves), mint a `vault:<name>:admin` hub JWT and
3295
+ * return it as the bootstrap credential — preserving the `create --json`
3296
+ * `token` string contract hub's admin-vaults.ts requires. When no hub is
3297
+ * reachable, return `token: null` plus explicit standalone guidance. There is
3298
+ * no local pvt_* fallback anymore.
3299
+ */
3300
+ async function mintBootstrapCredential(name: string): Promise<VaultCredential> {
3301
+ const operatorToken = readOperatorToken();
3302
+ if (!operatorToken) {
3303
+ return {
3304
+ token: null,
3305
+ guidance:
3306
+ "No token issued — no hub operator token at ~/.parachute/operator.token. " +
3307
+ "Install the hub (`bun add -g @openparachute/hub` + `parachute init`) and re-run, " +
3308
+ "or set VAULT_AUTH_TOKEN for an operator-channel bearer.",
3309
+ };
3310
+ }
3311
+ const port = readGlobalConfig().port || DEFAULT_PORT;
3312
+ const hub = chooseHubOrigin(port);
3313
+ if (hub.source === "loopback") {
3314
+ return {
3315
+ token: null,
3316
+ guidance:
3317
+ "No token issued — no hub origin configured (PARACHUTE_HUB_ORIGIN unset, no active " +
3318
+ "expose-state). Start the hub and set PARACHUTE_HUB_ORIGIN (or bring up an exposure), " +
3319
+ "then run `parachute-vault mcp-install`, or set VAULT_AUTH_TOKEN.",
3320
+ };
3321
+ }
3322
+ const result = await mintHubJwt({
3323
+ hubOrigin: hub.url,
3324
+ operatorToken,
3325
+ scope: `vault:${name}:admin`,
3326
+ subject: "parachute-vault-bootstrap",
3327
+ });
3328
+ if ("kind" in result) {
3329
+ const detail =
3330
+ result.kind === "network"
3331
+ ? `hub unreachable at ${result.origin} — ${result.cause}`
3332
+ : `hub mint-token rejected (HTTP ${result.status}, ${result.error}): ${result.description}`;
3333
+ return {
3334
+ token: null,
3335
+ guidance:
3336
+ `No token issued — ${detail}. Verify the hub is running (hub#449 for vault:admin mint), ` +
3337
+ "then run `parachute-vault mcp-install`, or set VAULT_AUTH_TOKEN.",
3338
+ };
3339
+ }
3340
+ return {
3341
+ token: result.token,
3342
+ guidance: `Minted hub JWT (jti=${result.jti}, expires ${result.expires_at}, scope ${result.scope}).`,
3343
+ };
3344
+ }
3345
+
3346
+ /**
3347
+ * Create a vault's config + DB and mint its first credential.
3348
+ *
3349
+ * Returns the bootstrap credential (a hub JWT, or null + guidance when no hub
3350
+ * is reachable — vault#282 Stage 2). The DB is created lazily via
3351
+ * `getVaultStore` so migrations + schema run; we no longer write any pvt_* row.
3352
+ */
3353
+ async function createVault(name: string): Promise<VaultCredential> {
3273
3354
  const config: VaultConfig = {
3274
3355
  name,
3275
3356
  api_keys: [],
@@ -3277,11 +3358,10 @@ function createVault(name: string): string {
3277
3358
  };
3278
3359
  writeVaultConfig(config);
3279
3360
 
3280
- // Create a pvt_ token in the vault's DB
3281
- const store = getVaultStore(name);
3282
- const { fullToken } = generateToken();
3283
- createToken(store.db, fullToken, { label: "default", permission: "full" });
3284
- return fullToken;
3361
+ // Touch the store so the vault's SQLite DB + schema are created. No token
3362
+ // row is written — vault is a pure hub resource-server post-0.5.0.
3363
+ getVaultStore(name);
3364
+ return mintBootstrapCredential(name);
3285
3365
  }
3286
3366
 
3287
3367
  interface InstallMcpConfigOpts {
@@ -3388,7 +3468,7 @@ Vaults:
3388
3468
  parachute-vault create <name> [--json] Create a new vault (--json: emit { name, token, paths, set_as_default })
3389
3469
  parachute-vault list List all vaults
3390
3470
  parachute-vault remove <name> [--yes] Remove a vault
3391
- parachute-vault mcp-install [--mint|--token <t>|--legacy-pat]
3471
+ parachute-vault mcp-install [--mint|--token <t>]
3392
3472
  [--scope vault:read|vault:write|vault:admin]
3393
3473
  [--install-scope local|user|project]
3394
3474
  [--vault <name>] [--client claude-code]
@@ -3406,16 +3486,17 @@ Vaults:
3406
3486
  matches Claude Code's claude-mcp-add
3407
3487
  default) with vault:read scope.
3408
3488
  --token <t>: paste an existing bearer
3409
- (any shape) instead of minting.
3410
- --legacy-pat: mint a vault-DB pvt_*
3411
- token (deprecated; for self-hosted-
3412
- without-hub setups). Also the only
3413
- path for --scope vault:admin hub
3414
- policy reserves per-vault admin for
3415
- operator-only minting (the admin SPA
3416
- session-cookie path), so --mint
3417
- --scope vault:admin is rejected
3418
- pre-flight.
3489
+ (hub JWT or VAULT_AUTH_TOKEN) instead of
3490
+ minting. (vault#282 Stage 2 removed the
3491
+ --legacy-pat pvt_* mint — vault is a pure
3492
+ hub resource-server now; without a hub,
3493
+ paste a bearer or set VAULT_AUTH_TOKEN.)
3494
+ --scope vault:admin IS mintable via
3495
+ --mint (hub#449): hub mints
3496
+ vault:<name>:admin when the operator
3497
+ bearer carries parachute:host:admin
3498
+ (the default operator.token does).
3499
+ Requires a hub running hub#449.
3419
3500
  --install-scope local (default) writes
3420
3501
  ~/.claude.json under
3421
3502
  projects[<cwd>].mcpServers (this
@@ -3437,7 +3518,7 @@ Vaults:
3437
3518
  without touching disk or hitting the
3438
3519
  hub. Useful for probing.
3439
3520
 
3440
- parachute-vault mcp-config <vault-name> [--token <pvt_...>] [--base-url <url>]
3521
+ parachute-vault mcp-config <vault-name> [--token <bearer>] [--base-url <url>]
3441
3522
  [--env-vars]
3442
3523
  Emit the JSON config consumed by
3443
3524
  \`claude -p --mcp-config '<json>'\`.
@@ -3455,22 +3536,13 @@ Vaults:
3455
3536
  \${PARACHUTE_VAULT_TOKEN} placeholders
3456
3537
  (safe to commit; expanded at runtime).
3457
3538
 
3458
- Tokens:
3459
- parachute-vault tokens List tokens (every vault)
3539
+ Tokens (vault#282 Stage 2 — vault is a pure hub resource-server; it no longer
3540
+ mints its own tokens. Mint a hub-issued JWT with \`parachute-vault mcp-install\`
3541
+ or \`parachute auth mint-token --scope vault:<name>:<verb>\`. \`list\` / \`revoke\`
3542
+ below operate on any vestigial pre-0.5.0 rows for cleanup.):
3543
+ parachute-vault tokens List vault-DB tokens (every vault)
3460
3544
  parachute-vault tokens list --vault <name> List tokens for one vault only
3461
- parachute-vault tokens create Create a vault-bound token in the default vault
3462
- parachute-vault tokens create --vault <name> Create a token bound to a specific vault
3463
- parachute-vault tokens create --all Create a server-wide token (vault_name=NULL).
3464
- Authenticates against any vault — use sparingly,
3465
- for cross-vault automation only.
3466
- parachute-vault tokens create --read Read-only token (shorthand for --scope vault:read)
3467
- parachute-vault tokens create --scope vault:write
3468
- Narrow the token's scopes. Accepts a comma-separated
3469
- list or repeated --scope flags. Valid scopes:
3470
- vault:read, vault:write, vault:admin.
3471
- parachute-vault tokens create --label x Set a label
3472
- parachute-vault tokens create --expires 30d Expiring token
3473
- parachute-vault tokens revoke <token-id> Revoke a token (default vault)
3545
+ parachute-vault tokens revoke <token-id> Revoke a vestigial token (default vault)
3474
3546
 
3475
3547
  OAuth — owner password + 2FA (LEGACY):
3476
3548
  Vault's standalone OAuth consent page was retired in 0.4.x (workstream E).
@@ -3513,6 +3585,16 @@ Import/Export:
3513
3585
  template via --git-message-template;
3514
3586
  --git-push to push after commit)
3515
3587
 
3588
+ Schema maintenance:
3589
+ parachute-vault schema prune [--vault <name>] Drop orphaned indexed-field columns +
3590
+ indexes whose declaring tags no longer
3591
+ exist. Dry-run by default (--dry-run is an
3592
+ explicit alias) — prints the drop plan
3593
+ without changing anything.
3594
+ parachute-vault schema prune --apply Execute the prune (alias: --yes). Co-declared
3595
+ fields keep their column; a drop loses only
3596
+ the index (data lives in notes.metadata).
3597
+
3516
3598
  ── Advanced / standalone ──────────────────────────────────────────────
3517
3599
 
3518
3600
  Direct daemon controls. For normal use, prefer the Parachute Hub wrappers