bridge-hosted-context-settings.test.mjs
87 lines 3.9 KB
Raw
sha256:6f47d53a6adbcf105ba1b9cfc126c788d6a0f461d197f84f78794914305b4bd5 fix(mcp): bound hosted discovery context Human patch 6 days ago
1 /**
2 * Hosted bridge context latency guard:
3 * explicit delegated vault access is authoritative for the access gate, so the
4 * bridge must not block MCP/session setup on canister vault enumeration.
5 */
6
7 import { test } from 'node:test';
8 import assert from 'node:assert/strict';
9 import fs from 'node:fs';
10 import os from 'node:os';
11 import http from 'node:http';
12 import path from 'node:path';
13 import crypto from 'node:crypto';
14 import { fileURLToPath, pathToFileURL } from 'node:url';
15
16 const __dirname = path.dirname(fileURLToPath(import.meta.url));
17 const projectRoot = path.resolve(__dirname, '..');
18 const SECRET = 'bridge-hosted-context-settings-secret-32';
19
20 function signTestJwt(payload) {
21 const header = Buffer.from(JSON.stringify({ alg: 'HS256', typ: 'JWT' })).toString('base64url');
22 const body = Buffer.from(JSON.stringify(payload)).toString('base64url');
23 const data = `${header}.${body}`;
24 const sig = crypto.createHmac('sha256', SECRET).update(data).digest('base64url');
25 return `${data}.${sig}`;
26 }
27
28 test('bridge hosted-context/settings returns delegated Business allowlist without canister vault fetch', async (t) => {
29 const dataDir = fs.mkdtempSync(path.join(os.tmpdir(), 'knowtation-bridge-context-'));
30 t.after(() => fs.rmSync(dataDir, { recursive: true, force: true }));
31 fs.writeFileSync(path.join(dataDir, 'hub_workspace.json'), JSON.stringify({ owner_user_id: 'google:owner' }));
32 fs.writeFileSync(path.join(dataDir, 'hub_roles.json'), JSON.stringify({ roles: { 'google:user456': 'evaluator' } }));
33 fs.writeFileSync(path.join(dataDir, 'hub_vault_access.json'), JSON.stringify({ 'google:user456': ['Business'] }));
34 fs.writeFileSync(path.join(dataDir, 'hub_evaluator_may_approve.json'), JSON.stringify({ 'google:user456': true }));
35
36 const origFetch = globalThis.fetch.bind(globalThis);
37 t.after(() => {
38 globalThis.fetch = origFetch;
39 });
40 let canisterVaultFetches = 0;
41 globalThis.fetch = async (url) => {
42 if (String(url) === 'https://mock-canister.test/api/v1/vaults') {
43 canisterVaultFetches += 1;
44 throw new Error('explicit delegated access must not fetch canister vaults');
45 }
46 return { ok: false, status: 404, json: async () => ({}), text: async () => '' };
47 };
48
49 process.env.NETLIFY = '1';
50 process.env.DATA_DIR = dataDir;
51 process.env.CANISTER_URL = 'https://mock-canister.test';
52 process.env.SESSION_SECRET = SECRET;
53 delete process.env.HUB_EVALUATOR_MAY_APPROVE;
54
55 const bridgeEntry = pathToFileURL(path.join(projectRoot, 'hub', 'bridge', 'server.mjs')).href;
56 const { app } = await import(`${bridgeEntry}?contextsettings=${Date.now()}`);
57 const srv = http.createServer(app);
58 await new Promise((resolve, reject) => {
59 srv.listen(0, '127.0.0.1', (err) => (err ? reject(err) : resolve()));
60 });
61 t.after(() => new Promise((resolve) => srv.close(() => resolve())));
62
63 const port = srv.address().port;
64 const token = signTestJwt({ sub: 'google:user456', role: 'evaluator' });
65 const settingsRes = await origFetch(`http://127.0.0.1:${port}/api/v1/hosted-context/settings`, {
66 headers: { Authorization: `Bearer ${token}` },
67 });
68 const settingsText = await settingsRes.text();
69 assert.equal(settingsRes.status, 200, settingsText);
70 const settings = JSON.parse(settingsText);
71 assert.equal(settings.effective_canister_user_id, 'google:owner');
72 assert.equal(settings.delegating, true);
73 assert.deepEqual(settings.allowed_vault_ids, ['Business']);
74 assert.equal(settings.role, 'evaluator');
75 assert.equal(settings.may_approve_proposals, true);
76
77 const businessRes = await origFetch(`http://127.0.0.1:${port}/api/v1/hosted-context`, {
78 headers: { Authorization: `Bearer ${token}`, 'X-Vault-Id': 'Business' },
79 });
80 assert.equal(businessRes.status, 200, await businessRes.text());
81
82 const defaultRes = await origFetch(`http://127.0.0.1:${port}/api/v1/hosted-context`, {
83 headers: { Authorization: `Bearer ${token}`, 'X-Vault-Id': 'default' },
84 });
85 assert.equal(defaultRes.status, 403);
86 assert.equal(canisterVaultFetches, 0);
87 });
File History 1 commit
sha256:6f47d53a6adbcf105ba1b9cfc126c788d6a0f461d197f84f78794914305b4bd5 fix(mcp): bound hosted discovery context Human patch 6 days ago