@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.
Files changed (48) hide show
  1. package/.claude/settings.local.json +31 -0
  2. package/.playwright-mcp/console-2026-04-14T04-17-25-395Z.log +2 -0
  3. package/.playwright-mcp/console-2026-04-14T04-18-11-767Z.log +1 -0
  4. package/.playwright-mcp/console-2026-04-14T04-19-07-733Z.log +2 -0
  5. package/.playwright-mcp/console-2026-04-14T04-20-45-440Z.log +2 -0
  6. package/.playwright-mcp/page-2026-04-14T04-17-25-536Z.yml +1 -0
  7. package/.playwright-mcp/page-2026-04-14T04-18-11-816Z.yml +1 -0
  8. package/.playwright-mcp/page-2026-04-14T04-18-31-674Z.yml +211 -0
  9. package/.playwright-mcp/page-2026-04-14T04-19-07-795Z.yml +59 -0
  10. package/.playwright-mcp/page-2026-04-14T04-19-36-239Z.yml +232 -0
  11. package/.playwright-mcp/page-2026-04-14T04-19-58-327Z.yml +182 -0
  12. package/.playwright-mcp/page-2026-04-14T04-20-10-517Z.yml +91 -0
  13. package/.playwright-mcp/page-2026-04-14T04-20-14-796Z.yml +70 -0
  14. package/.playwright-mcp/page-2026-04-14T04-20-45-509Z.yml +59 -0
  15. package/CHANGELOG.md +13 -0
  16. package/core/src/core.test.ts +12 -0
  17. package/core/src/notes.ts +4 -0
  18. package/core/src/types.ts +1 -0
  19. package/package.json +1 -1
  20. package/religions-abrahamic-filter.png +0 -0
  21. package/religions-buddhism-v2.png +0 -0
  22. package/religions-buddhism.png +0 -0
  23. package/religions-final.png +0 -0
  24. package/religions-v1.png +0 -0
  25. package/religions-v2.png +0 -0
  26. package/religions-zen.png +0 -0
  27. package/src/routing.test.ts +183 -0
  28. package/src/routing.ts +37 -1
  29. package/web/README.md +73 -0
  30. package/web/bun.lock +827 -0
  31. package/web/eslint.config.js +23 -0
  32. package/web/index.html +15 -0
  33. package/web/package.json +36 -0
  34. package/web/public/favicon.svg +1 -0
  35. package/web/public/icons.svg +24 -0
  36. package/web/src/App.tsx +149 -0
  37. package/web/src/Graph.tsx +200 -0
  38. package/web/src/NoteView.tsx +155 -0
  39. package/web/src/Sidebar.tsx +186 -0
  40. package/web/src/api.ts +21 -0
  41. package/web/src/index.css +50 -0
  42. package/web/src/main.tsx +10 -0
  43. package/web/src/types.ts +37 -0
  44. package/web/src/utils.ts +107 -0
  45. package/web/tsconfig.app.json +25 -0
  46. package/web/tsconfig.json +7 -0
  47. package/web/tsconfig.node.json +24 -0
  48. package/web/vite.config.ts +16 -0
@@ -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
+ ```