diff --git a/index.html b/index.html index 9b4c12c..083fc38 100644 --- a/index.html +++ b/index.html @@ -7,7 +7,7 @@ name="description" content="amerc is an agent mercenary layer for embedding, bringing, and hosting agents across vertical software boundaries." /> - + diff --git a/package.json b/package.json index 46cc777..39244bf 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "amerc-site", - "version": "0.27.0-vivid", + "version": "0.28.0-keys", "private": true, "type": "module", "scripts": { diff --git a/server/amerc-api.mjs b/server/amerc-api.mjs index 5a73607..468559e 100644 --- a/server/amerc-api.mjs +++ b/server/amerc-api.mjs @@ -65,6 +65,8 @@ db.exec(` created_at INTEGER NOT NULL); `); +try { db.exec("ALTER TABLE api_keys ADD COLUMN owner TEXT DEFAULT ''"); } catch { /* column exists */ } + const SERVER_IP = process.env.AMERC_SERVER_IP || '104.168.145.233'; // edge token: signed {host} so the showcase-edge can verify a broker may bind that host function showcaseToken(host) { @@ -447,6 +449,24 @@ const server = http.createServer(async (req, res) => { db.prepare("UPDATE agent_sessions SET status='closed', ended_at=? WHERE id=?").run(Date.now(), Number(m[1])); return send(res, 200, { ok: true }); } + // ---------- self-service agent keys (any logged-in user) ---------- + if (path === '/keys' && method === 'GET') { + if (id?.kind !== 'user') return send(res, 401, { ok: false, error: 'login required' }); + return send(res, 200, { ok: true, keys: db.prepare('SELECT id,name,prefix,created_at,last_used FROM api_keys WHERE owner=? ORDER BY id DESC').all(id.user.handle) }); + } + if (path === '/keys' && method === 'POST') { + if (id?.kind !== 'user') return send(res, 401, { ok: false, error: 'login required' }); + const b = await readBody(req) || {}; const raw = `amk_${crypto.randomBytes(24).toString('base64url')}`; + const info = db.prepare('INSERT INTO api_keys(name,key_hash,prefix,owner,created_at) VALUES(?,?,?,?,?)').run((b.name || 'my-agent').slice(0, 40), hashKey(raw), raw.slice(0, 12), id.user.handle, Date.now()); + return send(res, 200, { ok: true, id: Number(info.lastInsertRowid), key: raw, note: 'store this key now; it is not shown again' }); + } + if ((m = path.match(/^\/keys\/(\d+)$/)) && method === 'DELETE') { + if (id?.kind !== 'user') return send(res, 401, { ok: false, error: 'login required' }); + const k = db.prepare('SELECT * FROM api_keys WHERE id=?').get(Number(m[1])); + if (k && (k.owner === id.user.handle || id.user.role === 'admin')) db.prepare('DELETE FROM api_keys WHERE id=?').run(k.id); + return send(res, 200, { ok: true }); + } + // ---------- SHOWCASE (reverse-proxy subdomains) — amerc mansion service ---------- if (path === '/showcase' && method === 'GET') { if (id?.kind !== 'user') return send(res, 401, { ok: false, error: 'login required' }); diff --git a/src/AgentPlatform.jsx b/src/AgentPlatform.jsx index 52cbfaf..ececd17 100644 --- a/src/AgentPlatform.jsx +++ b/src/AgentPlatform.jsx @@ -161,6 +161,37 @@ export function UsePanel({ instance, onClose }) { } // ---- My Booth: publish classes + instances, manage ------------------------ +function KeysCard() { + const [keys, setKeys] = useState([]); + const [name, setName] = useState(''); + const [created, setCreated] = useState(null); + const [err, setErr] = useState(''); + const load = useCallback(() => api('/keys').then((d) => setKeys(d.keys || [])).catch((e) => setErr(e.message)), []); + useEffect(() => { load(); }, [load]); + const mint = async (e) => { e.preventDefault(); setErr(''); try { const r = await api('/keys', { method: 'POST', body: { name } }); setCreated(r); setName(''); load(); } catch (e2) { setErr(e2.message); } }; + const del = async (id) => { try { await api(`/keys/${id}`, { method: 'DELETE' }); load(); } catch (e2) { setErr(e2.message); } }; + return ( +
Mint a key for your own agent to read/write amerc docs, netdisk and the agent platform. Send it as Authorization: Bearer <key>.
✅ minted — copy now, shown once:
navigator.clipboard?.writeText(created.key)}>{created.key} ⧉{err}
} + {keys.map((k) => ( +{k.prefix}… {k.name}
+ {k.last_used ? `used ${new Date(k.last_used).toLocaleDateString()}` : 'never used'}
+
+ No keys yet — mint one above.
} +No instances. Publish one from a class above.
} +