@prefig/ember
v0.1.1
Published
Expiring membership credentials for in-person communities, verifiable offline.
Maintainers
Readme
ember
Temporary scope membership, renewed in presence. A demo of cryptographically temporary group membership: credentials expire, and the only renewal path shown here is an in-person QR handshake between two phones. No server, no registry, fully offline verification. See DESIGN.md for the protocol and threat model.
found a scope renew in presence verify offline
┌───────────┐ grant ┌────────────┐ prove ┌────────────┐
│ founder │ ── QR ──▶ │ member │ ── QR ──▶ │ door check │
└───────────┘ └────────────┘ └────────────┘
│ ▲
ttl runs out│ │same handshake,
▼ │in person again
┌────────────┐
│ lapsed │ no revocation lists:
└────────────┘ not renewed = not a memberStatic files, no build step, no dependencies beyond three vendored libraries.
Run it
Any static file server works. For local development:
cd ember
python3 -m http.server 8489
# open http://localhost:8489Camera access needs a secure context: localhost works, a LAN IP over plain HTTP does not. For two real phones, deploy the directory to any HTTPS static host (GitHub Pages, Cloudflare Pages). The service worker caches everything on first load, so the app keeps working when venue wifi does not.
Tests
node --test test/*.test.mjs # protocol, portfolio, presentation tests, no browser neededtest/e2e.html runs the full handshake in a browser, with every message round-tripped through a rendered QR image and decoded back: open it and look for "E2E PASS".
Demo runbook (two phones plus one optional door phone)
Before the session: load the app on each phone once, over HTTPS, so the service worker caches it. Set screen brightness high.
- Found. Phone A: "found a scope", name it, renewal window "2 minutes (live demo)", 3 hops. A is the root.
- Join. Phone B: "join / renew" shows a request QR. Phone A: "grant / renew someone", scan B, check the fingerprint out loud, grant. A now shows the credential QR; B taps "scan their grant" and scans it. B is a member with a visibly draining 2-minute window.
- Verify. Door phone (or A): "verify someone" shows a challenge. B taps "prove membership", scans the challenge, shows the proof; door phone scans it: member, 1 hop from founder, time remaining.
- Lapse. Talk for two minutes. B's card burns down to ash on stage. Re-verify: refused.
- Rekindle. Same handshake as step 2, B's card relights. If a third phone joined via B earlier, show depth 2, and show that it survives B lapsing (validity at issuance).
The 30-second clock-skew grace means a "2 minute" window lapses about two and a half minutes after granting.
Layout
js/protocol.js the protocol: genesis, links, credentials, verification
js/portfolio.js the member's keyring: one credential per scope
js/presentation.js multi-scope presentations (OpenID4VP-shaped)
js/codec.js fixed-field binary encoding, base45, multipart QR framing
js/crypto.js Ed25519 via vendored @noble/ed25519
js/gate.js optional at-rest gate: passkey/PRF or PIN
js/qr.js QR display (rotating parts when large) and camera scanning
js/app.js the demo UI
js/store.js IndexedDB persistence, legacy localStorage migration
vendor/ noble-ed25519, qrcode-generator, nimiq qr-scanner
test/ node protocol tests, browser e2e harness