@openparachute/vault 0.5.0-rc.3 → 0.5.0-rc.4

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.5.0-rc.3",
3
+ "version": "0.5.0-rc.4",
4
4
  "description": "Agent-native knowledge graph. Notes, tags, links over MCP.",
5
5
  "module": "src/cli.ts",
6
6
  "type": "module",
@@ -0,0 +1,55 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { handleAuthorizationServer, handleProtectedResource } from "./oauth-discovery.ts";
3
+
4
+ /**
5
+ * Regression: the per-vault discovery documents MUST advertise resource-narrowed
6
+ * `vault:<name>:<verb>` scopes, never broad `vault:<verb>`. A broad scope makes a
7
+ * spec-following MCP client (Claude) request `vault:read`, which the hub stamps
8
+ * `aud=vault`, which the per-vault MCP endpoint rejects ("audience mismatch:
9
+ * expected vault.<name>, got vault") — the live "Authorization with the MCP
10
+ * server failed" bug. See oauth-discovery.ts + auth.ts findBroadVaultScopes.
11
+ */
12
+ describe("oauth-discovery per-vault scopes_supported is resource-narrowed", () => {
13
+ const req = new Request("https://hub.example.com/vault/default/.well-known/oauth-protected-resource");
14
+
15
+ test("protected-resource metadata advertises vault:<name>:<verb>, never broad", async () => {
16
+ const body = (await handleProtectedResource(req, "default").json()) as {
17
+ resource: string;
18
+ scopes_supported: string[];
19
+ };
20
+ expect(body.scopes_supported).toEqual([
21
+ "vault:default:read",
22
+ "vault:default:write",
23
+ "vault:default:admin",
24
+ ]);
25
+ // No broad scope leaks through — vault rejects those on audience mismatch.
26
+ for (const s of body.scopes_supported) {
27
+ expect(s).not.toBe("vault:read");
28
+ expect(s).not.toBe("vault:write");
29
+ expect(s).not.toBe("vault:admin");
30
+ }
31
+ expect(body.resource).toContain("/vault/default/mcp");
32
+ });
33
+
34
+ test("scopes are narrowed to the specific vault name", async () => {
35
+ const body = (await handleProtectedResource(req, "work-notes").json()) as {
36
+ scopes_supported: string[];
37
+ };
38
+ expect(body.scopes_supported).toEqual([
39
+ "vault:work-notes:read",
40
+ "vault:work-notes:write",
41
+ "vault:work-notes:admin",
42
+ ]);
43
+ });
44
+
45
+ test("authorization-server forwarding doc also advertises narrowed scopes", async () => {
46
+ const body = (await handleAuthorizationServer(req, "default").json()) as {
47
+ scopes_supported: string[];
48
+ };
49
+ expect(body.scopes_supported).toEqual([
50
+ "vault:default:read",
51
+ "vault:default:write",
52
+ "vault:default:admin",
53
+ ]);
54
+ });
55
+ });
@@ -28,8 +28,27 @@
28
28
 
29
29
  import { getHubOrigin } from "./hub-jwt.ts";
30
30
 
31
- /** OAuth scopes vault publishes through discovery; see scopes.ts for enforcement. */
32
- const SCOPES_SUPPORTED = ["vault:read", "vault:write", "vault:admin"];
31
+ /**
32
+ * OAuth scopes vault publishes through discovery, RESOURCE-NARROWED to the
33
+ * specific vault instance the metadata document describes.
34
+ *
35
+ * This MUST be narrowed (`vault:<name>:<verb>`), not broad (`vault:<verb>`).
36
+ * Post auth-unification (vault#282/#412), vault is a pure hub resource server
37
+ * that REJECTS broad `vault:<verb>` tokens (`findBroadVaultScopes` → 401) and
38
+ * requires `vault:<name>:<verb>` scopes carrying `aud=vault.<name>`. A
39
+ * spec-following MCP client (e.g. Claude) reads `scopes_supported` from this
40
+ * PRM and requests exactly those scopes; if we advertise broad `vault:read`
41
+ * the client gets a token stamped `aud=vault` and the per-vault MCP endpoint
42
+ * rejects it ("audience mismatch: expected vault.<name>, got vault") — the
43
+ * "Authorization with the MCP server failed" symptom. Advertising the narrowed
44
+ * shape makes the client request the scope vault will actually accept. The
45
+ * hub's RFC 8707 resource-binding narrows too, but only when the client echoes
46
+ * the `resource` param — advertising narrowed scopes here is the belt that
47
+ * works regardless. See scopes.ts for enforcement.
48
+ */
49
+ function scopesSupportedFor(vaultName: string): string[] {
50
+ return [`vault:${vaultName}:read`, `vault:${vaultName}:write`, `vault:${vaultName}:admin`];
51
+ }
33
52
 
34
53
  /**
35
54
  * Public-facing base URL of the server. Honors `x-forwarded-*` headers so a
@@ -63,7 +82,7 @@ export function handleProtectedResource(req: Request, vaultName: string): Respon
63
82
  return Response.json({
64
83
  resource: `${base}${prefix}/mcp`,
65
84
  authorization_servers: [getHubOrigin()],
66
- scopes_supported: SCOPES_SUPPORTED,
85
+ scopes_supported: scopesSupportedFor(vaultName),
67
86
  bearer_methods_supported: ["header"],
68
87
  });
69
88
  }
@@ -78,7 +97,7 @@ export function handleProtectedResource(req: Request, vaultName: string): Respon
78
97
  * land here and discover the hub's actual endpoints; conformant clients that
79
98
  * probe AS metadata directly at the vault path get the same answer.
80
99
  */
81
- export function handleAuthorizationServer(_req: Request, _vaultName: string): Response {
100
+ export function handleAuthorizationServer(_req: Request, vaultName: string): Response {
82
101
  const hub = getHubOrigin();
83
102
  return Response.json({
84
103
  issuer: hub,
@@ -90,6 +109,6 @@ export function handleAuthorizationServer(_req: Request, _vaultName: string): Re
90
109
  code_challenge_methods_supported: ["S256"],
91
110
  grant_types_supported: ["authorization_code", "refresh_token"],
92
111
  token_endpoint_auth_methods_supported: ["none", "client_secret_post"],
93
- scopes_supported: SCOPES_SUPPORTED,
112
+ scopes_supported: scopesSupportedFor(vaultName),
94
113
  });
95
114
  }