@openparachute/vault 0.2.2 → 0.2.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/.claude/settings.local.json +31 -0
- package/.playwright-mcp/console-2026-04-14T04-17-25-395Z.log +2 -0
- package/.playwright-mcp/console-2026-04-14T04-18-11-767Z.log +1 -0
- package/.playwright-mcp/console-2026-04-14T04-19-07-733Z.log +2 -0
- package/.playwright-mcp/console-2026-04-14T04-20-45-440Z.log +2 -0
- package/.playwright-mcp/page-2026-04-14T04-17-25-536Z.yml +1 -0
- package/.playwright-mcp/page-2026-04-14T04-18-11-816Z.yml +1 -0
- package/.playwright-mcp/page-2026-04-14T04-18-31-674Z.yml +211 -0
- package/.playwright-mcp/page-2026-04-14T04-19-07-795Z.yml +59 -0
- package/.playwright-mcp/page-2026-04-14T04-19-36-239Z.yml +232 -0
- package/.playwright-mcp/page-2026-04-14T04-19-58-327Z.yml +182 -0
- package/.playwright-mcp/page-2026-04-14T04-20-10-517Z.yml +91 -0
- package/.playwright-mcp/page-2026-04-14T04-20-14-796Z.yml +70 -0
- package/.playwright-mcp/page-2026-04-14T04-20-45-509Z.yml +59 -0
- package/CHANGELOG.md +13 -0
- package/core/src/core.test.ts +12 -0
- package/core/src/notes.ts +4 -0
- package/core/src/types.ts +1 -0
- package/package.json +1 -1
- package/religions-abrahamic-filter.png +0 -0
- package/religions-buddhism-v2.png +0 -0
- package/religions-buddhism.png +0 -0
- package/religions-final.png +0 -0
- package/religions-v1.png +0 -0
- package/religions-v2.png +0 -0
- package/religions-zen.png +0 -0
- package/src/routing.test.ts +183 -0
- package/src/routing.ts +37 -1
- package/web/README.md +73 -0
- package/web/bun.lock +827 -0
- package/web/eslint.config.js +23 -0
- package/web/index.html +15 -0
- package/web/package.json +36 -0
- package/web/public/favicon.svg +1 -0
- package/web/public/icons.svg +24 -0
- package/web/src/App.tsx +149 -0
- package/web/src/Graph.tsx +200 -0
- package/web/src/NoteView.tsx +155 -0
- package/web/src/Sidebar.tsx +186 -0
- package/web/src/api.ts +21 -0
- package/web/src/index.css +50 -0
- package/web/src/main.tsx +10 -0
- package/web/src/types.ts +37 -0
- package/web/src/utils.ts +107 -0
- package/web/tsconfig.app.json +25 -0
- package/web/tsconfig.json +7 -0
- package/web/tsconfig.node.json +24 -0
- package/web/vite.config.ts +16 -0
package/src/routing.test.ts
CHANGED
|
@@ -476,3 +476,186 @@ describe("MCP 401 WWW-Authenticate challenge (RFC 9728)", () => {
|
|
|
476
476
|
);
|
|
477
477
|
});
|
|
478
478
|
});
|
|
479
|
+
|
|
480
|
+
// ---------------------------------------------------------------------------
|
|
481
|
+
// RFC 8414 §3.1 / RFC 9728 §3 path-insertion discovery.
|
|
482
|
+
//
|
|
483
|
+
// For a resource at `/vaults/<name>/mcp`, the spec-mandated metadata URLs are
|
|
484
|
+
// /.well-known/oauth-authorization-server/vaults/<name>[/mcp]
|
|
485
|
+
// /.well-known/oauth-protected-resource/vaults/<name>[/mcp]
|
|
486
|
+
// rather than the path-append form
|
|
487
|
+
// /vaults/<name>/.well-known/<type>
|
|
488
|
+
// that PR #111 also ships. Strict clients (including Claude Code's MCP OAuth
|
|
489
|
+
// SDK) probe only the path-insertion form; lax clients try path-append. We
|
|
490
|
+
// serve both so any conformant probe hits a live endpoint.
|
|
491
|
+
// ---------------------------------------------------------------------------
|
|
492
|
+
|
|
493
|
+
describe("path-insertion OAuth discovery (RFC 8414 §3.1 / RFC 9728 §3)", () => {
|
|
494
|
+
test("/.well-known/oauth-authorization-server/vaults/<name> returns vault-scoped AS metadata", async () => {
|
|
495
|
+
createVault("journal");
|
|
496
|
+
const path = "/.well-known/oauth-authorization-server/vaults/journal";
|
|
497
|
+
const res = await route(new Request(`http://localhost:1940${path}`), path);
|
|
498
|
+
expect(res.status).toBe(200);
|
|
499
|
+
const body = (await res.json()) as {
|
|
500
|
+
issuer: string;
|
|
501
|
+
authorization_endpoint: string;
|
|
502
|
+
token_endpoint: string;
|
|
503
|
+
registration_endpoint: string;
|
|
504
|
+
};
|
|
505
|
+
// All four endpoints must be vault-scoped — otherwise Claude Code's
|
|
506
|
+
// registration_endpoint falls back to root `/register` and cascades 404.
|
|
507
|
+
expect(body.issuer).toBe("http://localhost:1940/vaults/journal");
|
|
508
|
+
expect(body.authorization_endpoint).toBe("http://localhost:1940/vaults/journal/oauth/authorize");
|
|
509
|
+
expect(body.token_endpoint).toBe("http://localhost:1940/vaults/journal/oauth/token");
|
|
510
|
+
expect(body.registration_endpoint).toBe("http://localhost:1940/vaults/journal/oauth/register");
|
|
511
|
+
});
|
|
512
|
+
|
|
513
|
+
test("/.well-known/oauth-authorization-server/vaults/<name>/mcp (longer form) also returns AS metadata", async () => {
|
|
514
|
+
// Aaron's log shows Claude Code probes this longer form too; cheap to
|
|
515
|
+
// support since it resolves to the same AS for the same vault.
|
|
516
|
+
createVault("journal");
|
|
517
|
+
const path = "/.well-known/oauth-authorization-server/vaults/journal/mcp";
|
|
518
|
+
const res = await route(new Request(`http://localhost:1940${path}`), path);
|
|
519
|
+
expect(res.status).toBe(200);
|
|
520
|
+
const body = (await res.json()) as { issuer: string; registration_endpoint: string };
|
|
521
|
+
expect(body.issuer).toBe("http://localhost:1940/vaults/journal");
|
|
522
|
+
expect(body.registration_endpoint).toBe("http://localhost:1940/vaults/journal/oauth/register");
|
|
523
|
+
});
|
|
524
|
+
|
|
525
|
+
test("/.well-known/oauth-protected-resource/vaults/<name> returns vault-scoped PRM", async () => {
|
|
526
|
+
createVault("journal");
|
|
527
|
+
const path = "/.well-known/oauth-protected-resource/vaults/journal";
|
|
528
|
+
const res = await route(new Request(`http://localhost:1940${path}`), path);
|
|
529
|
+
expect(res.status).toBe(200);
|
|
530
|
+
const body = (await res.json()) as { resource: string; authorization_servers: string[] };
|
|
531
|
+
expect(body.resource).toBe("http://localhost:1940/vaults/journal/mcp");
|
|
532
|
+
expect(body.authorization_servers).toEqual(["http://localhost:1940/vaults/journal"]);
|
|
533
|
+
});
|
|
534
|
+
|
|
535
|
+
test("/.well-known/oauth-protected-resource/vaults/<name>/mcp (longer form) also returns PRM", async () => {
|
|
536
|
+
createVault("journal");
|
|
537
|
+
const path = "/.well-known/oauth-protected-resource/vaults/journal/mcp";
|
|
538
|
+
const res = await route(new Request(`http://localhost:1940${path}`), path);
|
|
539
|
+
expect(res.status).toBe(200);
|
|
540
|
+
const body = (await res.json()) as { resource: string };
|
|
541
|
+
expect(body.resource).toBe("http://localhost:1940/vaults/journal/mcp");
|
|
542
|
+
});
|
|
543
|
+
|
|
544
|
+
test("path-insertion and path-append forms return identical metadata", async () => {
|
|
545
|
+
// The coherence guarantee: a client that follows either spec shape MUST
|
|
546
|
+
// land on the same AS config. If these drift, a mixed-toolchain deploy
|
|
547
|
+
// (CLI using one form, daemon using the other) would mint tokens
|
|
548
|
+
// against inconsistent endpoints.
|
|
549
|
+
createVault("journal");
|
|
550
|
+
|
|
551
|
+
// AS metadata
|
|
552
|
+
const insertAsPath = "/.well-known/oauth-authorization-server/vaults/journal";
|
|
553
|
+
const appendAsPath = "/vaults/journal/.well-known/oauth-authorization-server";
|
|
554
|
+
const insertAsRes = await route(new Request(`http://localhost:1940${insertAsPath}`), insertAsPath);
|
|
555
|
+
const appendAsRes = await route(new Request(`http://localhost:1940${appendAsPath}`), appendAsPath);
|
|
556
|
+
expect(await insertAsRes.json()).toEqual(await appendAsRes.json());
|
|
557
|
+
|
|
558
|
+
// PRM
|
|
559
|
+
const insertPrmPath = "/.well-known/oauth-protected-resource/vaults/journal";
|
|
560
|
+
const appendPrmPath = "/vaults/journal/.well-known/oauth-protected-resource";
|
|
561
|
+
const insertPrmRes = await route(new Request(`http://localhost:1940${insertPrmPath}`), insertPrmPath);
|
|
562
|
+
const appendPrmRes = await route(new Request(`http://localhost:1940${appendPrmPath}`), appendPrmPath);
|
|
563
|
+
expect(await insertPrmRes.json()).toEqual(await appendPrmRes.json());
|
|
564
|
+
});
|
|
565
|
+
|
|
566
|
+
test("unknown vault in path-insertion URL returns 404, not boilerplate metadata", async () => {
|
|
567
|
+
// Don't leak metadata for phantom vaults. The equivalent path-append
|
|
568
|
+
// route also 404s when the vault doesn't exist (`readVaultConfig` miss
|
|
569
|
+
// at the vault-scoped routes branch); path-insertion must match.
|
|
570
|
+
createVault("journal");
|
|
571
|
+
for (const path of [
|
|
572
|
+
"/.well-known/oauth-authorization-server/vaults/nonexistent",
|
|
573
|
+
"/.well-known/oauth-authorization-server/vaults/nonexistent/mcp",
|
|
574
|
+
"/.well-known/oauth-protected-resource/vaults/nonexistent",
|
|
575
|
+
"/.well-known/oauth-protected-resource/vaults/nonexistent/mcp",
|
|
576
|
+
]) {
|
|
577
|
+
const res = await route(new Request(`http://localhost:1940${path}`), path);
|
|
578
|
+
expect(res.status).toBe(404);
|
|
579
|
+
}
|
|
580
|
+
});
|
|
581
|
+
|
|
582
|
+
test("x-forwarded-* headers propagate into the generated metadata URLs", async () => {
|
|
583
|
+
// Same contract as the WWW-Authenticate challenge and the root/append
|
|
584
|
+
// discovery endpoints: metadata must match the public-facing origin so
|
|
585
|
+
// a Cloudflare Tunnel / Tailscale Funnel deployment doesn't advertise
|
|
586
|
+
// internal localhost:1940 URLs.
|
|
587
|
+
createVault("journal");
|
|
588
|
+
const path = "/.well-known/oauth-authorization-server/vaults/journal";
|
|
589
|
+
const res = await route(
|
|
590
|
+
new Request(`http://127.0.0.1:1940${path}`, {
|
|
591
|
+
headers: {
|
|
592
|
+
"x-forwarded-host": "vault.example.com",
|
|
593
|
+
"x-forwarded-proto": "https",
|
|
594
|
+
},
|
|
595
|
+
}),
|
|
596
|
+
path,
|
|
597
|
+
);
|
|
598
|
+
expect(res.status).toBe(200);
|
|
599
|
+
const body = (await res.json()) as { issuer: string; registration_endpoint: string };
|
|
600
|
+
expect(body.issuer).toBe("https://vault.example.com/vaults/journal");
|
|
601
|
+
expect(body.registration_endpoint).toBe(
|
|
602
|
+
"https://vault.example.com/vaults/journal/oauth/register",
|
|
603
|
+
);
|
|
604
|
+
});
|
|
605
|
+
|
|
606
|
+
test("end-to-end flow: WWW-Authenticate → PRM → AS metadata → registration_endpoint is live", async () => {
|
|
607
|
+
// The actual Claude-Code bug: on 401, follow the challenge to the PRM,
|
|
608
|
+
// then follow PRM.authorization_servers[0] to the AS metadata (via
|
|
609
|
+
// path-insertion), then hit the `registration_endpoint`. Every hop
|
|
610
|
+
// must resolve — before the fix, the AS-metadata-via-path-insertion
|
|
611
|
+
// step 404'd and the SDK fell back to `/register` which also 404'd.
|
|
612
|
+
createVault("journal");
|
|
613
|
+
|
|
614
|
+
// Step 1: unauthenticated MCP → 401 + WWW-Authenticate.
|
|
615
|
+
const mcpRes = await route(
|
|
616
|
+
new Request("http://localhost:1940/vaults/journal/mcp"),
|
|
617
|
+
"/vaults/journal/mcp",
|
|
618
|
+
);
|
|
619
|
+
expect(mcpRes.status).toBe(401);
|
|
620
|
+
const challenge = mcpRes.headers.get("WWW-Authenticate")!;
|
|
621
|
+
const prmUrl = challenge.match(/resource_metadata="([^"]+)"/)![1];
|
|
622
|
+
|
|
623
|
+
// Step 2: fetch PRM. The challenge points at the path-append form, but
|
|
624
|
+
// a strict client might also try path-insertion — both must work.
|
|
625
|
+
// Follow the advertised URL (path-append in this case) and note the
|
|
626
|
+
// authorization_servers pointer.
|
|
627
|
+
const prmPath = new URL(prmUrl).pathname;
|
|
628
|
+
const prmRes = await route(new Request(`http://localhost:1940${prmPath}`), prmPath);
|
|
629
|
+
expect(prmRes.status).toBe(200);
|
|
630
|
+
const prm = (await prmRes.json()) as { authorization_servers: string[] };
|
|
631
|
+
const asBase = prm.authorization_servers[0]; // "http://localhost:1940/vaults/journal"
|
|
632
|
+
|
|
633
|
+
// Step 3: strict-client path-insertion probe for AS metadata.
|
|
634
|
+
const asBasePath = new URL(asBase).pathname; // "/vaults/journal"
|
|
635
|
+
const asInsertPath = `/.well-known/oauth-authorization-server${asBasePath}`;
|
|
636
|
+
const asRes = await route(
|
|
637
|
+
new Request(`http://localhost:1940${asInsertPath}`),
|
|
638
|
+
asInsertPath,
|
|
639
|
+
);
|
|
640
|
+
// This was the 404 before the fix — the reason Claude Code's SDK gave
|
|
641
|
+
// up and cascade-404'd on `/register`.
|
|
642
|
+
expect(asRes.status).toBe(200);
|
|
643
|
+
const asMeta = (await asRes.json()) as { registration_endpoint: string };
|
|
644
|
+
|
|
645
|
+
// Step 4: the advertised registration_endpoint must be live (POST-only).
|
|
646
|
+
const regPath = new URL(asMeta.registration_endpoint).pathname;
|
|
647
|
+
const regRes = await route(
|
|
648
|
+
new Request(`http://localhost:1940${regPath}`, {
|
|
649
|
+
method: "POST",
|
|
650
|
+
headers: { "Content-Type": "application/json" },
|
|
651
|
+
body: JSON.stringify({
|
|
652
|
+
client_name: "Test",
|
|
653
|
+
redirect_uris: ["https://example.com/cb"],
|
|
654
|
+
}),
|
|
655
|
+
}),
|
|
656
|
+
regPath,
|
|
657
|
+
);
|
|
658
|
+
// Successful DCR is 201; anything but 404 proves the endpoint is wired.
|
|
659
|
+
expect(regRes.status).toBe(201);
|
|
660
|
+
});
|
|
661
|
+
});
|
package/src/routing.ts
CHANGED
|
@@ -115,7 +115,43 @@ export async function route(
|
|
|
115
115
|
path: string,
|
|
116
116
|
clientIp?: string,
|
|
117
117
|
): Promise<Response> {
|
|
118
|
-
// OAuth discovery endpoints (no auth required)
|
|
118
|
+
// OAuth discovery endpoints (no auth required).
|
|
119
|
+
//
|
|
120
|
+
// RFC 8414 §3.1 and RFC 9728 §3 specify the discovery URL shape when an
|
|
121
|
+
// authorization server (or protected resource) has a path component `/p`:
|
|
122
|
+
//
|
|
123
|
+
// Path-insertion (spec-mandated):
|
|
124
|
+
// <host>/.well-known/<metadata-type>/p
|
|
125
|
+
// Path-append (widespread in the wild, shipped in PR #111):
|
|
126
|
+
// <host>/p/.well-known/<metadata-type>
|
|
127
|
+
//
|
|
128
|
+
// Strict clients — including Claude Code's MCP OAuth SDK — probe only the
|
|
129
|
+
// path-insertion form. Lax clients try path-append. We serve both so any
|
|
130
|
+
// conformant probe hits a live endpoint. Unscoped root forms
|
|
131
|
+
// (`/.well-known/oauth-*`) are the third accepted shape, and the
|
|
132
|
+
// path-append branch for scoped discovery lives further down alongside the
|
|
133
|
+
// other `/vaults/{name}/*` routing.
|
|
134
|
+
const protectedResourceInsert = path.match(
|
|
135
|
+
/^\/\.well-known\/oauth-protected-resource\/vaults\/([^/]+)(?:\/mcp)?$/,
|
|
136
|
+
);
|
|
137
|
+
if (protectedResourceInsert) {
|
|
138
|
+
const vaultName = protectedResourceInsert[1];
|
|
139
|
+
if (!readVaultConfig(vaultName)) {
|
|
140
|
+
return Response.json({ error: "Vault not found", vault: vaultName }, { status: 404 });
|
|
141
|
+
}
|
|
142
|
+
return handleProtectedResource(req, `/vaults/${vaultName}/mcp`, `/vaults/${vaultName}`);
|
|
143
|
+
}
|
|
144
|
+
const authServerInsert = path.match(
|
|
145
|
+
/^\/\.well-known\/oauth-authorization-server\/vaults\/([^/]+)(?:\/mcp)?$/,
|
|
146
|
+
);
|
|
147
|
+
if (authServerInsert) {
|
|
148
|
+
const vaultName = authServerInsert[1];
|
|
149
|
+
if (!readVaultConfig(vaultName)) {
|
|
150
|
+
return Response.json({ error: "Vault not found", vault: vaultName }, { status: 404 });
|
|
151
|
+
}
|
|
152
|
+
return handleAuthorizationServer(req, vaultName);
|
|
153
|
+
}
|
|
154
|
+
|
|
119
155
|
if (path === "/.well-known/oauth-protected-resource") {
|
|
120
156
|
return handleProtectedResource(req);
|
|
121
157
|
}
|
package/web/README.md
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# React + TypeScript + Vite
|
|
2
|
+
|
|
3
|
+
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
|
|
4
|
+
|
|
5
|
+
Currently, two official plugins are available:
|
|
6
|
+
|
|
7
|
+
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Oxc](https://oxc.rs)
|
|
8
|
+
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/)
|
|
9
|
+
|
|
10
|
+
## React Compiler
|
|
11
|
+
|
|
12
|
+
The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation).
|
|
13
|
+
|
|
14
|
+
## Expanding the ESLint configuration
|
|
15
|
+
|
|
16
|
+
If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules:
|
|
17
|
+
|
|
18
|
+
```js
|
|
19
|
+
export default defineConfig([
|
|
20
|
+
globalIgnores(['dist']),
|
|
21
|
+
{
|
|
22
|
+
files: ['**/*.{ts,tsx}'],
|
|
23
|
+
extends: [
|
|
24
|
+
// Other configs...
|
|
25
|
+
|
|
26
|
+
// Remove tseslint.configs.recommended and replace with this
|
|
27
|
+
tseslint.configs.recommendedTypeChecked,
|
|
28
|
+
// Alternatively, use this for stricter rules
|
|
29
|
+
tseslint.configs.strictTypeChecked,
|
|
30
|
+
// Optionally, add this for stylistic rules
|
|
31
|
+
tseslint.configs.stylisticTypeChecked,
|
|
32
|
+
|
|
33
|
+
// Other configs...
|
|
34
|
+
],
|
|
35
|
+
languageOptions: {
|
|
36
|
+
parserOptions: {
|
|
37
|
+
project: ['./tsconfig.node.json', './tsconfig.app.json'],
|
|
38
|
+
tsconfigRootDir: import.meta.dirname,
|
|
39
|
+
},
|
|
40
|
+
// other options...
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
])
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules:
|
|
47
|
+
|
|
48
|
+
```js
|
|
49
|
+
// eslint.config.js
|
|
50
|
+
import reactX from 'eslint-plugin-react-x'
|
|
51
|
+
import reactDom from 'eslint-plugin-react-dom'
|
|
52
|
+
|
|
53
|
+
export default defineConfig([
|
|
54
|
+
globalIgnores(['dist']),
|
|
55
|
+
{
|
|
56
|
+
files: ['**/*.{ts,tsx}'],
|
|
57
|
+
extends: [
|
|
58
|
+
// Other configs...
|
|
59
|
+
// Enable lint rules for React
|
|
60
|
+
reactX.configs['recommended-typescript'],
|
|
61
|
+
// Enable lint rules for React DOM
|
|
62
|
+
reactDom.configs.recommended,
|
|
63
|
+
],
|
|
64
|
+
languageOptions: {
|
|
65
|
+
parserOptions: {
|
|
66
|
+
project: ['./tsconfig.node.json', './tsconfig.app.json'],
|
|
67
|
+
tsconfigRootDir: import.meta.dirname,
|
|
68
|
+
},
|
|
69
|
+
// other options...
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
])
|
|
73
|
+
```
|