From 7d62877808aae3e6c766382a21ebe4c3ed62b205 Mon Sep 17 00:00:00 2001 From: artheru Date: Wed, 10 Jun 2026 05:04:32 +0800 Subject: [PATCH] self-service agent keys (0.28.0): users mint their own Bearer keys MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - amerc-api: owner column migration + /api/keys GET/POST/DELETE (user-scoped) - My Booth: 'My agent keys' card — mint, list, revoke, with Bearer usage hint - unblocks any user wiring their own agent to amerc docs/netdisk/platform (was admin-only) --- index.html | 2 +- package.json | 2 +- server/amerc-api.mjs | 20 ++++++++++++++++++++ src/AgentPlatform.jsx | 32 ++++++++++++++++++++++++++++++++ src/Scene2D.jsx | 2 +- src/styles.css | 5 +++++ 6 files changed, 60 insertions(+), 3 deletions(-) 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 ( +
+

🔑 My agent keys

+

Mint a key for your own agent to read/write amerc docs, netdisk and the agent platform. Send it as Authorization: Bearer <key>.

+
+ setName(e.target.value)} required /> + +
+ {created &&

✅ minted — copy now, shown once:

navigator.clipboard?.writeText(created.key)}>{created.key} ⧉
} + {err &&

{err}

} + {keys.map((k) => ( +
+ {k.prefix}… {k.name} + {k.last_used ? `used ${new Date(k.last_used).toLocaleDateString()}` : 'never used'} + +
+ ))} + {!keys.length &&

No keys yet — mint one above.

} +
+ ); +} + export function BoothDashboard({ auth, setRoute }) { const [classes, setClasses] = useState([]); const [insts, setInsts] = useState([]); @@ -233,6 +264,7 @@ export function BoothDashboard({ auth, setRoute }) { {!insts.length &&

No instances. Publish one from a class above.

} + {newInst && (
setNewInst(null)}>
e.stopPropagation()}> diff --git a/src/Scene2D.jsx b/src/Scene2D.jsx index 462e38e..b150ca8 100644 --- a/src/Scene2D.jsx +++ b/src/Scene2D.jsx @@ -6,7 +6,7 @@ import Mansion from './Mansion.jsx'; import TavernGame from './TavernGame.jsx'; import { api } from './api.js'; -const RELEASE = '0.27.0-vivid'; +const RELEASE = '0.28.0-keys'; const NAV = [ { id: 'home', label: 'Tavern' }, diff --git a/src/styles.css b/src/styles.css index 208180d..1af70b2 100644 --- a/src/styles.css +++ b/src/styles.css @@ -1087,3 +1087,8 @@ button { .ap-empty-start li { color: #c9b8e6; font-size: 13px; line-height: 1.6; } .ap-empty-start code { background: #07060c; color: #7fe9ff; padding: 1px 6px; border-radius: 4px; } .ap-link { background: none; color: #27d8ff; cursor: pointer; text-decoration: underline; font: inherit; padding: 0; } + +/* self-service agent keys (booth) */ +.ap-keys { margin-top: 22px; padding: 18px; border-radius: 14px; background: #130d20; border: 1px solid #271c3a; } +.ap-keys-mint { display: flex; gap: 8px; margin: 10px 0; flex-wrap: wrap; } +.ap-keys-mint input { flex: 1 1 200px; padding: 9px 11px; border-radius: 8px; background: #0a0f1a; border: 1px solid #2e2440; color: #eaf2ff; font-family: inherit; }