agent-credentials-integration.test.mjs
223 lines 8.5 KB
Raw
sha256:700fafdd1afa490919f9515d660ca6e75456bcd5bb67513abcd8757a634c01f6 docs: record AIP-b SD-21 land (KN #308) Human 9 days ago
1 /**
2 * Phase C + Lane D — integration: mint → exchange; isolation from refresh store.
3 */
4
5 import { describe, it } from 'node:test';
6 import assert from 'node:assert/strict';
7 import fs from 'node:fs/promises';
8 import os from 'node:os';
9 import path from 'node:path';
10 import http from 'node:http';
11 import jwt from 'jsonwebtoken';
12 import { createAgentCredentialRouter } from '../hub/gateway/agent-credential-routes.mjs';
13 import { createAgentCredentialStore } from '../hub/gateway/agent-credential-store.mjs';
14 import {
15 subFromVerifiedPayload,
16 assertAgentVaultAllowed,
17 } from '../hub/gateway/access-token-authz.mjs';
18 import express from 'express';
19
20 const SECRET = 'phase-c-integration-test-secret-32b!!';
21
22 async function withTempStore(fn) {
23 const dir = await fs.mkdtemp(path.join(os.tmpdir(), 'kt-agent-cred-'));
24 const prev = process.env.KNOWTATION_GATEWAY_DATA_DIR;
25 process.env.KNOWTATION_GATEWAY_DATA_DIR = dir;
26 try {
27 await fn(dir);
28 } finally {
29 if (prev === undefined) delete process.env.KNOWTATION_GATEWAY_DATA_DIR;
30 else process.env.KNOWTATION_GATEWAY_DATA_DIR = prev;
31 await fs.rm(dir, { recursive: true, force: true });
32 }
33 }
34
35 function sessionJwt(sub = 'google:tester') {
36 return jwt.sign({ sub, type: 'session', role: 'member' }, SECRET, { expiresIn: '1h' });
37 }
38
39 describe('Phase C integration — agent credentials', () => {
40 it('mint → exchange → propose path authorized; revoke blocks exchange; wrong vault denied', async () => {
41 await withTempStore(async () => {
42 const app = express();
43 const { router } = createAgentCredentialRouter({
44 sessionSecret: SECRET,
45 getSessionSub: (req) => {
46 const auth = req.headers.authorization || '';
47 const t = auth.startsWith('Bearer ') ? auth.slice(7) : '';
48 try {
49 const p = jwt.verify(t, SECRET);
50 return p.sub || null;
51 } catch {
52 return null;
53 }
54 },
55 getSessionPayload: (req) => {
56 const auth = req.headers.authorization || '';
57 const t = auth.startsWith('Bearer ') ? auth.slice(7) : '';
58 try {
59 return jwt.verify(t, SECRET);
60 } catch {
61 return null;
62 }
63 },
64 grantedScopes: () => ['vault:read', 'vault:write'],
65 });
66 app.use('/api/v1/auth/agent', router);
67 const server = http.createServer(app);
68 await new Promise((r) => server.listen(0, r));
69 const port = server.address().port;
70 const base = `http://127.0.0.1:${port}`;
71
72 try {
73 const mintRes = await fetch(`${base}/api/v1/auth/agent/credentials`, {
74 method: 'POST',
75 headers: {
76 Authorization: `Bearer ${sessionJwt()}`,
77 'Content-Type': 'application/json',
78 },
79 body: JSON.stringify({
80 name: 'videofactory-trend-agent',
81 vault_ids: ['default'],
82 scopes: ['propose', 'vault:read'],
83 }),
84 });
85 assert.equal(mintRes.status, 201);
86 const minted = await mintRes.json();
87 assert.ok(String(minted.credential).startsWith('kt_agent_'));
88
89 const tokRes = await fetch(`${base}/api/v1/auth/agent/token`, {
90 method: 'POST',
91 headers: { 'Content-Type': 'application/json' },
92 body: JSON.stringify({ credential: minted.credential }),
93 });
94 assert.equal(tokRes.status, 200);
95 const tok = await tokRes.json();
96 assert.equal(tok.expires_in, 900);
97 const access = jwt.verify(tok.access_token, SECRET);
98 assert.equal(access.type, 'agent_access');
99 assert.equal(
100 subFromVerifiedPayload(access, { method: 'POST', path: '/api/v1/proposals' }),
101 'google:tester'
102 );
103 assert.equal(
104 subFromVerifiedPayload(access, { method: 'POST', path: '/api/v1/notes' }),
105 null
106 );
107 assert.equal(assertAgentVaultAllowed(access, 'default'), true);
108 assert.equal(assertAgentVaultAllowed(access, 'other'), false);
109
110 const rev = await fetch(`${base}/api/v1/auth/agent/credentials/${minted.id}`, {
111 method: 'DELETE',
112 headers: { Authorization: `Bearer ${sessionJwt()}` },
113 });
114 assert.equal(rev.status, 200);
115
116 const tok2 = await fetch(`${base}/api/v1/auth/agent/token`, {
117 method: 'POST',
118 headers: { 'Content-Type': 'application/json' },
119 body: JSON.stringify({ credential: minted.credential }),
120 });
121 assert.equal(tok2.status, 401);
122 const body = await tok2.json();
123 assert.equal(body.code, 'AGENT_CREDENTIAL_INVALID');
124 } finally {
125 await new Promise((r) => server.close(r));
126 }
127 });
128 });
129
130 it('exchange 200 while refresh store empty; never SESSION_STORE_UNAVAILABLE; ktn_refresh → 401', async () => {
131 await withTempStore(async () => {
132 const app = express();
133 const { router } = createAgentCredentialRouter({
134 sessionSecret: SECRET,
135 getSessionSub: (req) => {
136 try {
137 return jwt.verify(req.headers.authorization.slice(7), SECRET).sub;
138 } catch {
139 return null;
140 }
141 },
142 getSessionPayload: (req) => {
143 try {
144 return jwt.verify(req.headers.authorization.slice(7), SECRET);
145 } catch {
146 return null;
147 }
148 },
149 grantedScopes: () => ['vault:read', 'vault:write'],
150 });
151 app.use('/api/v1/auth/agent', router);
152 const server = http.createServer(app);
153 await new Promise((r) => server.listen(0, r));
154 const base = `http://127.0.0.1:${server.address().port}`;
155 try {
156 const mintRes = await fetch(`${base}/api/v1/auth/agent/credentials`, {
157 method: 'POST',
158 headers: { Authorization: `Bearer ${sessionJwt()}`, 'Content-Type': 'application/json' },
159 body: JSON.stringify({ name: 'iso', vault_ids: ['default'] }),
160 });
161 const minted = await mintRes.json();
162
163 const refreshShaped = await fetch(`${base}/api/v1/auth/agent/token`, {
164 method: 'POST',
165 headers: { 'Content-Type': 'application/json' },
166 body: JSON.stringify({ credential: 'ktn_refresh_fakevalue' }),
167 });
168 assert.equal(refreshShaped.status, 401);
169 const refreshBody = await refreshShaped.json();
170 assert.equal(refreshBody.code, 'AGENT_CREDENTIAL_INVALID');
171 assert.notEqual(refreshBody.code, 'SESSION_STORE_UNAVAILABLE');
172
173 const tokRes = await fetch(`${base}/api/v1/auth/agent/token`, {
174 method: 'POST',
175 headers: { 'Content-Type': 'application/json' },
176 body: JSON.stringify({ credential: minted.credential }),
177 });
178 assert.equal(tokRes.status, 200);
179 const tokBody = await tokRes.json();
180 assert.notEqual(tokBody.code, 'SESSION_STORE_UNAVAILABLE');
181 } finally {
182 await new Promise((r) => server.close(r));
183 }
184 });
185 });
186
187 it('store I/O throw → 503 UNAVAILABLE; inconsistent meta → 503 INCONSISTENT without empty save', async () => {
188 const dir = await fs.mkdtemp(path.join(os.tmpdir(), 'kt-agent-int-'));
189 process.env.KNOWTATION_GATEWAY_DATA_DIR = dir;
190 await fs.writeFile(
191 path.join(dir, 'hosted_agent_credentials.meta.json'),
192 JSON.stringify({ schema_version: 1, nonempty_seen: true, count: 2, updated_at: Date.now() }),
193 'utf8'
194 );
195 const app = express();
196 const brokenStore = createAgentCredentialStore();
197 const { router } = createAgentCredentialRouter({
198 sessionSecret: SECRET,
199 getSessionSub: () => 'google:tester',
200 getSessionPayload: () => ({ sub: 'google:tester', type: 'session' }),
201 grantedScopes: () => ['vault:read'],
202 store: brokenStore,
203 });
204 app.use('/api/v1/auth/agent', router);
205 const server = http.createServer(app);
206 await new Promise((r) => server.listen(0, r));
207 const base = `http://127.0.0.1:${server.address().port}`;
208 try {
209 const list = await fetch(`${base}/api/v1/auth/agent/credentials`, {
210 headers: { Authorization: `Bearer ${sessionJwt()}` },
211 });
212 assert.equal(list.status, 503);
213 const body = await list.json();
214 assert.equal(body.code, 'AGENT_CREDENTIAL_STORE_INCONSISTENT');
215 assert.equal(body.store.inconsistent, true);
216 assert.ok(!await fs.stat(path.join(dir, 'hosted_agent_credentials.json')).then(() => true).catch(() => false));
217 } finally {
218 await new Promise((r) => server.close(r));
219 delete process.env.KNOWTATION_GATEWAY_DATA_DIR;
220 await fs.rm(dir, { recursive: true, force: true });
221 }
222 });
223 });
File History 1 commit
sha256:700fafdd1afa490919f9515d660ca6e75456bcd5bb67513abcd8757a634c01f6 docs: record AIP-b SD-21 land (KN #308) Human 9 days ago