agent-credential-store.mjs
141 lines 3.9 KB
Raw
sha256:e4c529f14a0bb908c1caaaeb3f95f3623a1a82e636e7e3722ca2cd3dc9821263 security: npm audit fix pre-bridge 2026-07-29 Human 39 days ago
1 /**
2 * Phase C — durable store for scoped REST agent credentials.
3 * Netlify: dedicated blob `gateway-agent-credentials` (not refresh-tokens-v1).
4 * Dev/test: JSON file under KNOWTATION_GATEWAY_DATA_DIR.
5 */
6
7 import fs from 'fs/promises';
8 import path from 'path';
9 import { fileURLToPath } from 'url';
10 import {
11 mintCredential,
12 verifyCredential,
13 revokeCredential,
14 rotateCredential,
15 listCredentialsForSub,
16 } from '../lib/agent-credential-core.mjs';
17
18 const BLOB_KEY = 'agent-credentials-v1';
19 const BLOB_GLOBAL = '__knowtation_gateway_agent_cred_blob';
20
21 let projectRoot;
22 try {
23 const __dirname = path.dirname(fileURLToPath(import.meta.url));
24 projectRoot = path.resolve(__dirname, '..', '..');
25 } catch (_) {
26 projectRoot = process.cwd();
27 }
28
29 function credentialFilePath() {
30 const dataDir = process.env.KNOWTATION_GATEWAY_DATA_DIR || path.join(projectRoot, 'data');
31 return path.join(dataDir, 'hosted_agent_credentials.json');
32 }
33
34 function getBlobStore() {
35 return globalThis[BLOB_GLOBAL];
36 }
37
38 function normalizeRecords(raw) {
39 const credentials =
40 raw && typeof raw === 'object' && raw.credentials && typeof raw.credentials === 'object'
41 ? raw.credentials
42 : {};
43 const out = {};
44 for (const [id, rec] of Object.entries(credentials)) {
45 if (typeof id === 'string' && rec && typeof rec === 'object' && typeof rec.token_hash === 'string') {
46 out[id] = rec;
47 }
48 }
49 return out;
50 }
51
52 async function load() {
53 const store = getBlobStore();
54 if (store) {
55 const raw = await store.get(BLOB_KEY, { type: 'json' });
56 return normalizeRecords(raw);
57 }
58 try {
59 const raw = await fs.readFile(credentialFilePath(), 'utf8');
60 return normalizeRecords(JSON.parse(raw));
61 } catch (e) {
62 if (e && e.code === 'ENOENT') return {};
63 return {};
64 }
65 }
66
67 async function save(records) {
68 const store = getBlobStore();
69 if (store) {
70 await store.setJSON(BLOB_KEY, { credentials: records || {} });
71 return;
72 }
73 const filePath = credentialFilePath();
74 await fs.mkdir(path.dirname(filePath), { recursive: true });
75 const tmpPath = `${filePath}.${process.pid}.${Date.now()}.tmp`;
76 await fs.writeFile(tmpPath, JSON.stringify({ credentials: records || {} }, null, 2), {
77 encoding: 'utf8',
78 mode: 0o600,
79 });
80 await fs.rename(tmpPath, filePath);
81 }
82
83 /**
84 * @returns {{
85 * mint: Function,
86 * verify: Function,
87 * revoke: Function,
88 * rotate: Function,
89 * list: Function,
90 * }}
91 */
92 export function createAgentCredentialStore() {
93 return {
94 mint: async (opts) => {
95 const records = await load();
96 const result = mintCredential(records, opts);
97 await save(result.records);
98 return {
99 credential: result.credential,
100 id: result.id,
101 name: result.record.name,
102 vault_ids: result.record.vault_ids,
103 scopes: result.record.scopes,
104 expires_at: result.record.expires_at,
105 created_at: result.record.created_at,
106 };
107 },
108 verify: async (credential, opts = {}) => {
109 const records = await load();
110 const result = verifyCredential(records, credential, opts);
111 if (result.ok) await save(result.records);
112 return result;
113 },
114 revoke: async (cid, sub) => {
115 const records = await load();
116 const result = revokeCredential(records, cid, sub);
117 if (result.revoked) await save(result.records);
118 return { ok: true, revoked: result.revoked };
119 },
120 rotate: async (cid, sub) => {
121 const records = await load();
122 const result = rotateCredential(records, cid, sub);
123 await save(result.records);
124 return {
125 credential: result.credential,
126 id: result.id,
127 name: result.record.name,
128 vault_ids: result.record.vault_ids,
129 scopes: result.record.scopes,
130 expires_at: result.record.expires_at,
131 created_at: result.record.created_at,
132 };
133 },
134 list: async (sub) => {
135 const records = await load();
136 return listCredentialsForSub(records, sub);
137 },
138 };
139 }
140
141 export { BLOB_GLOBAL };
File History 1 commit
sha256:e4c529f14a0bb908c1caaaeb3f95f3623a1a82e636e7e3722ca2cd3dc9821263 security: npm audit fix pre-bridge 2026-07-29 Human 39 days ago