agent-credentials-unit.test.mjs
113 lines 4.2 KB
Raw
sha256:e4c529f14a0bb908c1caaaeb3f95f3623a1a82e636e7e3722ca2cd3dc9821263 security: npm audit fix pre-bridge 2026-07-29 Human 41 days ago
1 /**
2 * Phase C — unit tier: parse, hash, scopes, propose paths, aud checks.
3 */
4
5 import { describe, it } from 'node:test';
6 import assert from 'node:assert/strict';
7 import {
8 parseAgentCredential,
9 hashSecret,
10 mintCredential,
11 verifyCredential,
12 agentScopesPermitMethod,
13 assertAgentVaultAllowed,
14 normalizeAgentRequestPath,
15 AGENT_ACCESS_TTL_SECONDS,
16 DEFAULT_AGENT_SCOPES,
17 applyScopeCeiling,
18 } from '../hub/lib/agent-credential-core.mjs';
19 import {
20 subFromVerifiedPayload,
21 isAgentAccessPayload,
22 resolveActorTokenClass,
23 mayApplyAdminAllowlistOverride,
24 } from '../hub/gateway/access-token-authz.mjs';
25
26 describe('Phase C unit — agent credentials', () => {
27 it('parses kt_agent_ wire format and rejects browser-style refresh', () => {
28 const ok = parseAgentCredential('kt_agent_abc.def');
29 assert.equal(ok.id, 'abc');
30 assert.equal(ok.secret, 'def');
31 assert.equal(parseAgentCredential('abc.def'), null);
32 assert.equal(parseAgentCredential('kt_agent_'), null);
33 });
34
35 it('hashSecret is stable and non-reversible shape', () => {
36 const h = hashSecret('secret');
37 assert.equal(h, hashSecret('secret'));
38 assert.notEqual(h, 'secret');
39 });
40
41 it('access TTL is 900s', () => {
42 assert.equal(AGENT_ACCESS_TTL_SECONDS, 900);
43 });
44
45 it('default scopes are propose + vault:read', () => {
46 assert.deepEqual([...DEFAULT_AGENT_SCOPES], ['propose', 'vault:read']);
47 });
48
49 it('mint + verify round-trip without consume-on-use', () => {
50 const { records, credential, id } = mintCredential({}, {
51 sub: 'google:1',
52 name: 'trend',
53 vault_ids: ['default'],
54 scopes: ['propose', 'vault:read'],
55 now: 1_000_000,
56 });
57 const v1 = verifyCredential(records, credential, { now: 1_000_001 });
58 assert.equal(v1.ok, true);
59 assert.equal(v1.id, id);
60 const v2 = verifyCredential(v1.records, credential, { now: 1_000_002 });
61 assert.equal(v2.ok, true);
62 });
63
64 it('applyScopeCeiling keeps propose and drops write without role write', () => {
65 const withWrite = applyScopeCeiling(['propose', 'vault:write'], ['vault:read', 'vault:write']);
66 assert.ok(withWrite.includes('propose'));
67 assert.ok(withWrite.includes('vault:write'));
68 const capped = applyScopeCeiling(['propose', 'vault:write'], ['vault:read']);
69 assert.deepEqual(capped, ['propose']);
70 assert.throws(
71 () => applyScopeCeiling(['vault:write'], ['vault:read']),
72 (e) => e && e.code === 'AGENT_SCOPE_CEILING'
73 );
74 });
75
76 it('agentScopesPermitMethod allows propose create paths only', () => {
77 const scopes = ['propose', 'vault:read'];
78 assert.equal(agentScopesPermitMethod(scopes, 'GET', '/api/v1/notes'), true);
79 assert.equal(agentScopesPermitMethod(scopes, 'POST', '/api/v1/proposals'), true);
80 assert.equal(agentScopesPermitMethod(scopes, 'POST', 'api/v1/proposals'), true);
81 assert.equal(agentScopesPermitMethod(scopes, 'POST', '/api/v1/notes'), false);
82 assert.equal(agentScopesPermitMethod(scopes, 'POST', '/api/v1/proposals/x/approve'), false);
83 });
84
85 it('normalizeAgentRequestPath strips query and leading slash', () => {
86 assert.equal(normalizeAgentRequestPath('/api/v1/proposals?x=1'), 'api/v1/proposals');
87 });
88
89 it('assertAgentVaultAllowed enforces vault_ids', () => {
90 const payload = { type: 'agent_access', vault_ids: ['v1'] };
91 assert.equal(assertAgentVaultAllowed(payload, 'v1'), true);
92 assert.equal(assertAgentVaultAllowed(payload, 'default'), false);
93 assert.equal(assertAgentVaultAllowed({ type: 'session' }, 'default'), true);
94 });
95
96 it('subFromVerifiedPayload requires aud+typ for agent_access', () => {
97 const good = {
98 sub: 'google:1',
99 type: 'agent_access',
100 typ: 'kt_agent_access',
101 aud: 'knowtation-hub-rest',
102 scopes: ['propose', 'vault:read'],
103 };
104 assert.equal(subFromVerifiedPayload(good, { method: 'POST', path: '/api/v1/proposals' }), 'google:1');
105 assert.equal(
106 subFromVerifiedPayload({ ...good, aud: 'wrong' }, { method: 'POST', path: '/api/v1/proposals' }),
107 null
108 );
109 assert.equal(isAgentAccessPayload(good), true);
110 assert.equal(resolveActorTokenClass(good), 'agent_access');
111 assert.equal(mayApplyAdminAllowlistOverride(good), false);
112 });
113 });
File History 1 commit
sha256:e4c529f14a0bb908c1caaaeb3f95f3623a1a82e636e7e3722ca2cd3dc9821263 security: npm audit fix pre-bridge 2026-07-29 Human 41 days ago