#!/usr/bin/env node
/**
* Mini web example for trust-map
*
* SSR dashboard for browsing and managing the trust map. CLI ↔ Web operate
* on the same Hyperbee, so changes from the CLI show up in the web UI on
* refresh (and vice versa).
*
* Usage: node examples/web.js [--port 9094] [--storage <path>]
*/
import http from 'http'
import Corestore from 'corestore'
import { TrustMap } from '../index.js'
import b4a from 'b4a'
const args = process.argv.slice(2)
function getFlag (name) {
const i = args.indexOf('--' + name)
return i !== -1 && args[i + 1] ? args[i + 1] : null
}
const port = parseInt(getFlag('port')) || 9094
// NOTE: distinct default from examples/cli.js — they don't share state.
// See cli.js header for the rationale (RocksDB single-writer lock).
const storagePath = getFlag('storage') || './.trust-map-web'
const store = new Corestore(storagePath)
await store.ready()
const core = store.get({ name: 'trust-map' })
await core.ready()
const tm = new TrustMap({ core })
await tm.ready()
function escapeHtml (s) {
return String(s).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"')
}
function collectBody (req) {
return new Promise((resolve, reject) => {
const parts = []
req.on('data', chunk => parts.push(chunk))
req.on('end', () => resolve(b4a.concat(parts).toString('utf8')))
req.on('error', reject)
})
}
const httpServer = http.createServer(async (req, res) => {
const url = req.url.split('?')[0]
try {
if (req.method === 'GET' && url === '/') {
const entries = await tm.list()
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' })
res.end(renderPage(entries))
return
}
if (req.method === 'POST' && url === '/grant') {
const body = await collectBody(req)
const params = new URLSearchParams(body)
const pubkey = params.get('pubkey')
const resource = params.get('resource')
const permsStr = params.get('perms')
if (pubkey && resource && permsStr) {
const perms = permsStr.split(',').map(s => s.trim()).filter(Boolean)
if (perms.length) await tm.grant(pubkey, resource, perms)
}
res.writeHead(303, { Location: '/' })
res.end()
return
}
if (req.method === 'POST' && url === '/revoke') {
const body = await collectBody(req)
const params = new URLSearchParams(body)
const pubkey = params.get('pubkey')
const resource = params.get('resource')
if (pubkey && resource) {
await tm.revoke(pubkey, resource)
}
res.writeHead(303, { Location: '/' })
res.end()
return
}
res.writeHead(404, { 'Content-Type': 'text/plain' })
res.end('Not found')
} catch (err) {
console.error(err)
res.writeHead(500, { 'Content-Type': 'text/plain' })
res.end(err.message)
}
})
function renderPage (entries) {
const grouped = {}
for (const e of entries) {
if (!grouped[e.pubkey]) grouped[e.pubkey] = []
grouped[e.pubkey].push(e)
}
const pubkeys = Object.keys(grouped).sort()
return `<!DOCTYPE html>
<html><head><title>trust-map</title>
<style>
body { font-family: monospace; background: #0d1117; color: #c9d1d9; padding: 2rem; max-width: 900px; margin: 0 auto; }
h1 { color: #58a6ff; }
h2 { font-size: 1rem; color: #58a6ff; margin-top: 1.5rem; }
.card { background: #161b22; border: 1px solid #30363d; border-radius: 6px; padding: 1rem; margin: 1rem 0; }
.card h2 { margin: 0 0 0.5rem; font-size: 0.75rem; text-transform: uppercase; color: #8b949e; }
table { width: 100%; border-collapse: collapse; font-size: 0.85rem; }
th { text-align: left; color: #8b949e; padding: 0.4rem 0.5rem; border-bottom: 1px solid #30363d; }
td { padding: 0.4rem 0.5rem; border-bottom: 1px solid #21262d; }
.pubkey { font-family: monospace; color: #79c0ff; font-size: 0.8rem; }
.perm { display: inline-block; background: #1f6feb22; border: 1px solid #1f6feb44; color: #58a6ff; padding: 0.1rem 0.4rem; border-radius: 3px; font-size: 0.75rem; margin-right: 0.3rem; }
.perm-admin { background: #f8514922; border-color: #f8514944; color: #f85149; }
.perm-write { background: #d29922aa; border-color: #d2992244; color: #d29922; }
input, button { background: #0d1117; color: #c9d1d9; border: 1px solid #30363d; padding: 0.4rem 0.6rem; border-radius: 4px; font-family: monospace; font-size: 0.85rem; }
input { margin-right: 0.3rem; }
input.long { width: 30rem; }
button { background: #21262d; cursor: pointer; }
button:hover { background: #30363d; }
button.btn-primary { background: #1f6feb; border-color: #1f6feb; color: white; }
button.btn-primary:hover { background: #1158c7; }
button.btn-danger { border-color: #f8514966; color: #f85149; padding: 0.2rem 0.5rem; font-size: 0.75rem; }
.empty { color: #484f58; font-style: italic; padding: 1rem; }
</style></head><body>
<h1>trust-map</h1>
<div class="card">
<h2>Stats</h2>
<div>${pubkeys.length} pubkey(s) · ${entries.length} grant(s)</div>
</div>
<div class="card">
<h2>Grant permissions</h2>
<form method="POST" action="/grant">
<div style="margin-bottom: 0.5rem">
<input class="long" name="pubkey" placeholder="pubkey (hex or base64)" required>
</div>
<div style="margin-bottom: 0.5rem">
<input name="resource" placeholder="resource name (or *)" required>
<input name="perms" placeholder="perms (comma-separated, e.g. read,write)" required>
<button class="btn-primary" type="submit">Grant</button>
</div>
</form>
</div>
<div class="card">
<h2>Trust map (${entries.length} entries)</h2>
${pubkeys.length === 0 ? '<div class="empty">(empty — grant something to get started)</div>' : pubkeys.map(pk => `
<h2 style="margin-top: 1rem">${escapeHtml(pk)}</h2>
<table>
<thead><tr><th>Resource</th><th>Permissions</th><th></th></tr></thead>
<tbody>
${grouped[pk].map(e => `
<tr>
<td>${escapeHtml(e.resource)}</td>
<td>${e.perms.map(p => `<span class="perm ${p === 'admin' ? 'perm-admin' : p === 'write' ? 'perm-write' : ''}">${escapeHtml(p)}</span>`).join('')}</td>
<td>
<form method="POST" action="/revoke" style="margin: 0">
<input type="hidden" name="pubkey" value="${escapeHtml(pk)}">
<input type="hidden" name="resource" value="${escapeHtml(e.resource)}">
<button class="btn-danger" type="submit">revoke</button>
</form>
</td>
</tr>`).join('')}
</tbody>
</table>`).join('')}
</div>
<div class="card">
<h2>How this works</h2>
<p style="font-size: 0.85rem; line-height: 1.6">
This dashboard manages a <code>trust-map</code> instance backed by a Hyperbee.
It shares storage with the CLI example — anything you grant here is visible
to <code>cli.js list</code>, and vice versa. Wildcard resource <code>*</code>
grants apply to all resources for that pubkey.
</p>
<p style="font-size: 0.85rem; line-height: 1.6">
For chain verification (hyperident attestations), the trust-map module's
<code>checkChain()</code> method is exercised by the unit tests. The web
UI shows direct grants only.
</p>
</div>
</body></html>`
}
httpServer.listen(port, () => {
console.log('trust-map dashboard:')
console.log(' http://localhost:%d', port)
console.log(' storage: %s', storagePath)
})