@openparachute/vault 0.4.9-rc.11 → 0.4.9-rc.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openparachute/vault",
3
- "version": "0.4.9-rc.11",
3
+ "version": "0.4.9-rc.12",
4
4
  "description": "Agent-native knowledge graph. Notes, tags, links over MCP.",
5
5
  "module": "src/cli.ts",
6
6
  "type": "module",
@@ -30,6 +30,7 @@ import { writeVaultConfig, readVaultConfig } from "./config.ts";
30
30
  import { getVaultStore, clearVaultStoreCache } from "./vault-store.ts";
31
31
  import { authenticateVaultRequest, authenticateGlobalRequest } from "./auth.ts";
32
32
  import { resetJwksCache, resetRevocationCache } from "./hub-jwt.ts";
33
+ import { generateToken, createToken } from "./token-store.ts";
33
34
 
34
35
  interface Keypair {
35
36
  privateKey: CryptoKey;
@@ -665,3 +666,117 @@ describe("authenticateVaultRequest — hub JWT tag-scoping (auth-unification C0)
665
666
  });
666
667
  }
667
668
  });
669
+
670
+ // ---------------------------------------------------------------------------
671
+ // pvt_* deprecation warning (vault#282 Stage 1 — soft deprecation, NON-BREAKING)
672
+ //
673
+ // Every successful pvt_* (vault-DB token-store) authentication emits a
674
+ // one-time-per-token deprecation warning signalling that pvt_* tokens will be
675
+ // REJECTED at vault 0.6.0. Auth OUTCOMES are unchanged: the token still
676
+ // validates + authorizes exactly as before. Hub-issued JWTs — the migration
677
+ // target — never trigger the pvt_* warning.
678
+ //
679
+ // The `warnPvtDeprecationOnce` cache is process-global and keyed on the
680
+ // token's display id (`t_<hashprefix>`). Each test mints a fresh random pvt_*,
681
+ // so display ids never collide across tests; the "warns once" assertion holds
682
+ // within a single test by making two requests with the same token.
683
+ // ---------------------------------------------------------------------------
684
+
685
+ /** Mint a fresh pvt_* token directly into a vault's DB and return it. */
686
+ function mintPvtToken(vaultName: string): string {
687
+ const store = getVaultStore(vaultName);
688
+ const { fullToken } = generateToken();
689
+ createToken(store.db, fullToken, { label: "deprecation-test", permission: "full" });
690
+ return fullToken;
691
+ }
692
+
693
+ describe("pvt_* deprecation warning (vault#282 Stage 1)", () => {
694
+ test("pvt_* auth still SUCCEEDS and emits the deprecation warning once", async () => {
695
+ seedVault("journal");
696
+ const token = mintPvtToken("journal");
697
+ const config = readVaultConfig("journal")!;
698
+ const store = getVaultStore("journal");
699
+
700
+ const warnSpy = spyOn(console, "warn").mockImplementation(() => {});
701
+ try {
702
+ // First request: auth outcome unchanged (full permission), warning fires.
703
+ const r1 = await authenticateVaultRequest(bearer(token), config, store.db);
704
+ expect("error" in r1).toBe(false);
705
+ if (!("error" in r1)) {
706
+ expect(r1.permission).toBe("full");
707
+ expect(r1.scopes).toContain("vault:admin");
708
+ }
709
+
710
+ const deprecationCalls = warnSpy.mock.calls.filter((c) =>
711
+ String(c[0]).includes("[deprecation]"),
712
+ );
713
+ expect(deprecationCalls.length).toBe(1);
714
+ const msg = String(deprecationCalls[0]![0]);
715
+ // Shape: names pvt_*, the 0.6.0 rejection, the issue, the mint paths, the guide.
716
+ expect(msg).toContain("pvt_* token");
717
+ expect(msg).toContain("DEPRECATED");
718
+ expect(msg).toContain("REJECTED at vault 0.6.0");
719
+ expect(msg).toContain("vault#282");
720
+ expect(msg).toContain("parachute vault mcp-install");
721
+ expect(msg).toContain("parachute auth mint-token");
722
+ expect(msg).toContain("UPGRADING.md");
723
+ // The display id of the presented token appears in the message.
724
+ expect(msg).toMatch(/pvt_\* token t_[0-9a-f]+ authenticated/);
725
+
726
+ // Second request with the SAME token: still authorizes, no second warn.
727
+ const r2 = await authenticateVaultRequest(bearer(token), config, store.db);
728
+ expect("error" in r2).toBe(false);
729
+ const stillOne = warnSpy.mock.calls.filter((c) =>
730
+ String(c[0]).includes("[deprecation]"),
731
+ );
732
+ expect(stillOne.length).toBe(1);
733
+ } finally {
734
+ warnSpy.mockRestore();
735
+ }
736
+ });
737
+
738
+ test("pvt_* auth on the global (unified) surface also SUCCEEDS and warns once", async () => {
739
+ seedVault("journal");
740
+ const token = mintPvtToken("journal");
741
+
742
+ const warnSpy = spyOn(console, "warn").mockImplementation(() => {});
743
+ try {
744
+ const result = await authenticateGlobalRequest(bearer(token));
745
+ expect("error" in result).toBe(false);
746
+ if (!("error" in result)) expect(result.permission).toBe("full");
747
+
748
+ const deprecationCalls = warnSpy.mock.calls.filter((c) =>
749
+ String(c[0]).includes("[deprecation]"),
750
+ );
751
+ expect(deprecationCalls.length).toBe(1);
752
+ expect(String(deprecationCalls[0]![0])).toContain("vault#282");
753
+ } finally {
754
+ warnSpy.mockRestore();
755
+ }
756
+ });
757
+
758
+ test("hub-JWT auth does NOT emit the pvt_* deprecation warning", async () => {
759
+ seedVault("journal");
760
+ const token = await signJwt(kp, {
761
+ iss: fixture.origin,
762
+ aud: "vault.journal",
763
+ scope: "vault:journal:write",
764
+ });
765
+ const config = readVaultConfig("journal")!;
766
+ const store = getVaultStore("journal");
767
+
768
+ const warnSpy = spyOn(console, "warn").mockImplementation(() => {});
769
+ try {
770
+ const result = await authenticateVaultRequest(bearer(token), config, store.db);
771
+ // Auth succeeds (the migration target works) ...
772
+ expect("error" in result).toBe(false);
773
+ // ... and no pvt_* deprecation warning is emitted for a hub JWT.
774
+ const deprecationCalls = warnSpy.mock.calls.filter((c) =>
775
+ String(c[0]).includes("[deprecation]"),
776
+ );
777
+ expect(deprecationCalls.length).toBe(0);
778
+ } finally {
779
+ warnSpy.mockRestore();
780
+ }
781
+ });
782
+ });
package/src/auth.ts CHANGED
@@ -169,6 +169,40 @@ export function warnLegacyOnce(cacheKey: string, context: string): void {
169
169
  );
170
170
  }
171
171
 
172
+ /**
173
+ * Doc link operators are sent to when a `pvt_*` token authenticates. Points
174
+ * at the pvt_* → hub-JWT migration section of vault's UPGRADING.md.
175
+ */
176
+ const PVT_MIGRATION_DOC =
177
+ "https://github.com/ParachuteComputer/parachute-vault/blob/main/UPGRADING.md#pvt_-token-deprecation--will-be-rejected-at-060";
178
+
179
+ // One-shot pvt_* deprecation warning tracker, keyed by the token's display id
180
+ // (`t_<hashprefix>`) so each distinct token warns exactly once per process.
181
+ const warnedPvtTokens = new Set<string>();
182
+
183
+ /**
184
+ * Log a one-time (per token-hash) deprecation warning for a successfully-
185
+ * authenticated `pvt_*` vault-DB token. Stage 1 of vault#282: pvt_* still
186
+ * AUTHENTICATES and AUTHORIZES exactly as before — this only signals that the
187
+ * credential is on a deprecation clock and will be REJECTED at vault 0.6.0.
188
+ *
189
+ * Keyed on the token's display id (`t_<hashprefix>` — the `jti` ResolvedToken
190
+ * surfaces) so a given token logs once per process regardless of how many
191
+ * requests it makes or whether it carries explicit scopes. Folds in the
192
+ * narrower pre-existing "vault token without scopes column" warning — a legacy-
193
+ * derived pvt_* gets THIS warning, not both, so we never double-warn one token.
194
+ */
195
+ export function warnPvtDeprecationOnce(displayId: string): void {
196
+ if (warnedPvtTokens.has(displayId)) return;
197
+ warnedPvtTokens.add(displayId);
198
+ console.warn(
199
+ `[deprecation] pvt_* token ${displayId} authenticated — pvt_* tokens are DEPRECATED and will be REJECTED at vault 0.6.0 (vault#282). ` +
200
+ "Migrate to a hub-issued JWT: run `parachute vault mcp-install` (MCP clients) or " +
201
+ "`parachute auth mint-token --scope vault:<name>:<verb>` (scripts). " +
202
+ `Guide: ${PVT_MIGRATION_DOC}.`,
203
+ );
204
+ }
205
+
172
206
  /** Read-only tools (the only tools allowed for "read" permission). */
173
207
  const READ_TOOLS = new Set([
174
208
  "query-notes",
@@ -292,9 +326,12 @@ export async function authenticateVaultRequest(
292
326
  ),
293
327
  };
294
328
  }
295
- if (resolved.legacyDerived) {
296
- warnLegacyOnce(`vault-token:${vaultConfig.name ?? ""}`, "vault token without scopes column");
297
- }
329
+ // vault#282 Stage 1: every successful pvt_* (vault-DB token-store)
330
+ // authentication is on a deprecation clock. One-time per token-hash
331
+ // (keyed on the display id). Folds in the old narrower "vault token
332
+ // without scopes column" warning — a legacy-derived pvt_* gets THIS
333
+ // warning, not both. Auth OUTCOME is unchanged; this only signals.
334
+ warnPvtDeprecationOnce(resolved.jti);
298
335
  return {
299
336
  permission: resolved.permission,
300
337
  scopes: resolved.scopes,
@@ -621,9 +658,10 @@ export async function authenticateGlobalRequest(
621
658
  const store = getVaultStore(vaultName);
622
659
  const resolved = resolveToken(store.db, key);
623
660
  if (resolved) {
624
- if (resolved.legacyDerived) {
625
- warnLegacyOnce(`vault-token:${vaultName}`, "vault token without scopes column");
626
- }
661
+ // vault#282 Stage 1: pvt_* resolved on the unified surface is on the
662
+ // same deprecation clock. One-time per token-hash; folds in the old
663
+ // narrower legacy-scopes warning. Auth OUTCOME unchanged.
664
+ warnPvtDeprecationOnce(resolved.jti);
627
665
  return {
628
666
  permission: resolved.permission,
629
667
  scopes: resolved.scopes,