self-service agent keys (0.28.0): users mint their own Bearer keys
- 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)
This commit is contained in:
parent
c6127747ed
commit
7d62877808
@ -7,7 +7,7 @@
|
|||||||
name="description"
|
name="description"
|
||||||
content="amerc is an agent mercenary layer for embedding, bringing, and hosting agents across vertical software boundaries."
|
content="amerc is an agent mercenary layer for embedding, bringing, and hosting agents across vertical software boundaries."
|
||||||
/>
|
/>
|
||||||
<meta name="version" content="0.27.0-vivid" />
|
<meta name="version" content="0.28.0-keys" />
|
||||||
<link rel="icon" href="/favicon.svg" type="image/svg+xml" />
|
<link rel="icon" href="/favicon.svg" type="image/svg+xml" />
|
||||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "amerc-site",
|
"name": "amerc-site",
|
||||||
"version": "0.27.0-vivid",
|
"version": "0.28.0-keys",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@ -65,6 +65,8 @@ db.exec(`
|
|||||||
created_at INTEGER NOT NULL);
|
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';
|
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
|
// edge token: signed {host} so the showcase-edge can verify a broker may bind that host
|
||||||
function showcaseToken(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 });
|
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 ----------
|
// ---------- SHOWCASE (reverse-proxy subdomains) — amerc mansion service ----------
|
||||||
if (path === '/showcase' && method === 'GET') {
|
if (path === '/showcase' && method === 'GET') {
|
||||||
if (id?.kind !== 'user') return send(res, 401, { ok: false, error: 'login required' });
|
if (id?.kind !== 'user') return send(res, 401, { ok: false, error: 'login required' });
|
||||||
|
|||||||
@ -161,6 +161,37 @@ export function UsePanel({ instance, onClose }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ---- My Booth: publish classes + instances, manage ------------------------
|
// ---- 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 (
|
||||||
|
<div className="ap-keys">
|
||||||
|
<h4 className="ap-h4">🔑 My agent keys</h4>
|
||||||
|
<p className="ap-hint">Mint a key for your own agent to read/write amerc docs, netdisk and the agent platform. Send it as <code>Authorization: Bearer <key></code>.</p>
|
||||||
|
<form className="ap-keys-mint" onSubmit={mint}>
|
||||||
|
<input placeholder="key name (e.g. my-bot)" value={name} onChange={(e) => setName(e.target.value)} required />
|
||||||
|
<button className="px-action" type="submit">+ Mint key</button>
|
||||||
|
</form>
|
||||||
|
{created && <div className="mn-created"><p>✅ minted — copy now, shown once:</p><code className="ap-token" onClick={() => navigator.clipboard?.writeText(created.key)}>{created.key} ⧉</code></div>}
|
||||||
|
{err && <p className="px-auth-err">{err}</p>}
|
||||||
|
{keys.map((k) => (
|
||||||
|
<div key={k.id} className="ap-mine-inst">
|
||||||
|
<code className="ap-token">{k.prefix}…</code> <em>{k.name}</em>
|
||||||
|
<span className="ap-hint" style={{ marginLeft: 'auto' }}>{k.last_used ? `used ${new Date(k.last_used).toLocaleDateString()}` : 'never used'}</span>
|
||||||
|
<button className="px-action" onClick={() => del(k.id)}>revoke</button>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
{!keys.length && <p className="ap-hint">No keys yet — mint one above.</p>}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export function BoothDashboard({ auth, setRoute }) {
|
export function BoothDashboard({ auth, setRoute }) {
|
||||||
const [classes, setClasses] = useState([]);
|
const [classes, setClasses] = useState([]);
|
||||||
const [insts, setInsts] = useState([]);
|
const [insts, setInsts] = useState([]);
|
||||||
@ -233,6 +264,7 @@ export function BoothDashboard({ auth, setRoute }) {
|
|||||||
{!insts.length && <p className="ap-hint">No instances. Publish one from a class above.</p>}
|
{!insts.length && <p className="ap-hint">No instances. Publish one from a class above.</p>}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<KeysCard />
|
||||||
{newInst && (
|
{newInst && (
|
||||||
<div className="ap-modal" onClick={() => setNewInst(null)}>
|
<div className="ap-modal" onClick={() => setNewInst(null)}>
|
||||||
<div className="ap-modal-box" onClick={(e) => e.stopPropagation()}>
|
<div className="ap-modal-box" onClick={(e) => e.stopPropagation()}>
|
||||||
|
|||||||
@ -6,7 +6,7 @@ import Mansion from './Mansion.jsx';
|
|||||||
import TavernGame from './TavernGame.jsx';
|
import TavernGame from './TavernGame.jsx';
|
||||||
import { api } from './api.js';
|
import { api } from './api.js';
|
||||||
|
|
||||||
const RELEASE = '0.27.0-vivid';
|
const RELEASE = '0.28.0-keys';
|
||||||
|
|
||||||
const NAV = [
|
const NAV = [
|
||||||
{ id: 'home', label: 'Tavern' },
|
{ id: 'home', label: 'Tavern' },
|
||||||
|
|||||||
@ -1087,3 +1087,8 @@ button {
|
|||||||
.ap-empty-start li { color: #c9b8e6; font-size: 13px; line-height: 1.6; }
|
.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-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; }
|
.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; }
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user