IR Brief — VCC Phase 1: Chain Infrastructure + Keys Card Redesign¶
SURFACE: ucca-ir (chain logic) + ucca-keys (keys card + chain verification) DO NOT TOUCH: rtopacks.com.au, ucca.online, ucca-engine, any other repo
Context¶
We are building the UCCA Verified Contact Chain (VCC). Every interaction between UCCA and a contact produces a cryptographically chained record. Each new event appends a child hash derived from the previous node. The chain is tamper-evident — modify any node and every subsequent hash breaks.
This is Phase 1: chain infrastructure in D1 + automatic event wiring + keys card redesign.
Part 1 — D1 Schema Changes (engine-db: 0efa8970-0053-4623-8436-4e877af10887)¶
1a. Update ledger_entries table¶
Add two columns:
ALTER TABLE ledger_entries ADD COLUMN parent_hash TEXT;
ALTER TABLE ledger_entries ADD COLUMN node_hash TEXT;
ALTER TABLE ledger_entries ADD COLUMN event_type TEXT;
parent_hash — the short_hash of the previous node (NULL for root entry)
node_hash — this node's hash, derived as: HMAC-SHA256(parent_hash + event_type + timestamp, KEY_SERVER_SECRET), truncated to 8 hex chars
event_type — machine-readable event label (see event types below)
1b. Backfill existing ledger entries¶
For existing entries, set:
- event_type = 'contact_registered'
- parent_hash = NULL (they are root nodes)
- node_hash = their existing short_hash
UPDATE ledger_entries
SET event_type = 'contact_registered',
parent_hash = NULL,
node_hash = short_hash
WHERE event_type IS NULL;
Part 2 — Chain Functions (ucca-ir Worker)¶
2a. Hash computation¶
Add this utility function to the Worker:
async function computeNodeHash(parentHash, eventType, timestamp, secret) {
const input = `${parentHash || 'root'}:${eventType}:${timestamp}`;
const key = await crypto.subtle.importKey(
'raw',
new TextEncoder().encode(secret),
{ name: 'HMAC', hash: 'SHA-256' },
false,
['sign']
);
const sig = await crypto.subtle.sign('HMAC', key, new TextEncoder().encode(input));
const hex = Array.from(new Uint8Array(sig)).map(b => b.toString(16).padStart(2, '0')).join('');
return hex.slice(0, 8);
}
2b. Append chain event function¶
async function appendChainEvent(db, secret, contactHash, eventType) {
// Get the most recent node for this contact
const last = await db.prepare(
`SELECT node_hash FROM ledger_entries
WHERE hash = ?
ORDER BY timestamp DESC LIMIT 1`
).bind(contactHash).first();
const parentHash = last?.node_hash || contactHash;
const timestamp = new Date().toISOString();
const nodeHash = await computeNodeHash(parentHash, eventType, timestamp, secret);
await db.prepare(
`INSERT INTO ledger_entries
(hash, short_hash, node_hash, parent_hash, event_type, timestamp, source, intent, verified)
VALUES (?, ?, ?, ?, ?, ?, 'system', 'chain', 0)`
).bind(contactHash, nodeHash, nodeHash, parentHash, eventType, timestamp).run();
return nodeHash;
}
2c. Wire automatic events to registration flow¶
In the existing registration handler, after the root ledger entry is created, append these chain events in sequence:
// After root entry created:
await appendChainEvent(db, env.KEY_SERVER_SECRET, hash, 'hash_generated');
// After SMS sent successfully:
await appendChainEvent(db, env.KEY_SERVER_SECRET, hash, 'sms_confirmed');
// After email sent successfully:
await appendChainEvent(db, env.KEY_SERVER_SECRET, hash, 'email_confirmed');
In the existing verification handler (when contact taps the verify link):
await appendChainEvent(db, env.KEY_SERVER_SECRET, hash, 'endpoints_verified');
await appendChainEvent(db, env.KEY_SERVER_SECRET, hash, 'trust_l1_assigned');
Part 3 — Chain Verification Endpoint (ucca-keys Worker)¶
Add GET /api/chain/:hash to ucca-keys Worker.
This endpoint:
1. Fetches all ledger entries for the contact ordered by timestamp ASC
2. Recomputes each node hash from scratch using computeNodeHash
3. Compares computed vs stored for each node
4. Returns chain status + all entries
// Response shape:
{
valid: true, // false if any node hash doesn't match
contact: {
hash: "ca120eae...",
short_hash: "ca120eae",
name: "Tim Smith",
registered: "2026-03-12T..."
},
chain: [
{
node_hash: "ca120eae",
parent_hash: null,
event_type: "contact_registered",
timestamp: "2026-03-12T...",
verified: true // computed hash matches stored hash
},
{
node_hash: "3ff44dfb",
parent_hash: "ca120eae",
event_type: "hash_generated",
timestamp: "2026-03-12T...",
verified: true
}
// ... etc
],
trust_level: 1 // derived from highest trust event in chain
}
This endpoint requires KEY_SERVER_SECRET — it is NOT public. It is called by the Authenticator app (future) and the ops console. The public keys card calls a separate read-only endpoint.
Part 4 — Keys Card Redesign (ucca-keys Worker)¶
The public keys card at keys.ucca.online/verify/[hash] needs a full redesign.
Remove these fields:¶
- NETWORK (ISP name)
- LOCATION (city, country)
These stay in D1 for ops use but never surface publicly.
New aesthetic — dark, matches ir.ucca.online:¶
Palette:
- Background: #0a0a0f
- Text primary: #e8e8f0
- Text secondary: rgba(232,232,240,0.5)
- Accent: #22c55e (green — same as ir.ucca.online keyline)
- Border: rgba(255,255,255,0.08)
- Font: IBM Plex Mono for data fields, existing serif for UCCA mark
Layout — single centred card, max-width 560px:
[UCCA mark — white on dark, same as ir.ucca.online logo]
─────────────────────────────
✓ VERIFIED [green accent]
─────────────────────────────
NAME
[Full name]
REF
[short_hash]
RECORDED
[DD MMM YYYY]
─────────────────────────────
HASH
[full hash — monospace, small, wrap if needed]
─────────────────────────────
UCCA INC · WILMINGTON · DELAWARE
DE File No. 7824354 · D-U-N-S 119-199-377
USPTO Reg. No. 7,619,705 · EIN 84-4522608
UCCA.ONLINE
─────────────────────────────
If chain is broken or hash not found:
Chain summary (public — minimal):¶
Below the HASH field, add one line only:
or
No event detail on the public card. Detail is app-only.
Event Type Reference¶
For the event_type field — use these exact strings:
| String | Human label |
|---|---|
contact_registered |
registered via [source] |
hash_generated |
hash generated |
sms_confirmed |
SMS confirmed |
email_confirmed |
email confirmed |
endpoints_verified |
endpoints verified |
trust_l1_assigned |
trust level 1 assigned |
trust_l2_assigned |
trust level 2 assigned |
trust_l3_assigned |
trust level 3 assigned |
chain_accessed |
record accessed |
review_initiated |
review initiated |
contact_known |
contact known |
nda_executed |
NDA executed |
meeting_held |
meeting held |
diligence_commenced |
diligence commenced |
Acceptance Criteria¶
ledger_entrieshasparent_hash,node_hash,event_typecolumns ✓- Existing entries backfilled ✓
- New registration produces minimum 3 chain events:
contact_registered→hash_generated→sms_confirmed/email_confirmed✓ - Verification tap appends
endpoints_verified+trust_l1_assigned✓ GET /api/chain/:hashon ucca-keys returns valid chain JSON ✓- Chain verification recomputes and confirms each node hash ✓
- Keys card: dark palette, no network/location, VERIFIED badge green ✓
- Keys card: chain summary line shows event count + intact/broken ✓
- Full test registration → verify → check keys card shows CHAIN [n] events · intact ✓
Deploy order¶
- D1 schema changes first (ALTER TABLE + backfill)
- Deploy ucca-ir with chain functions + auto-events
- Deploy ucca-keys with chain endpoint + card redesign
- Full end-to-end test registration
- Paste chain JSON from
/api/chain/[hash]for review - Commit and push
Puppeteer screenshot of new keys card at 1440×900 and 430×932 before deploying ucca-keys.