Skip to content

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:

✗ UNVERIFIED

This record could not be verified.
Contact UCCA directly.

ucca.online

Chain summary (public — minimal):

Below the HASH field, add one line only:

CHAIN   [n] events · intact

or

CHAIN   [n] events · BROKEN

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

  1. ledger_entries has parent_hash, node_hash, event_type columns ✓
  2. Existing entries backfilled ✓
  3. New registration produces minimum 3 chain events: contact_registeredhash_generatedsms_confirmed / email_confirmed
  4. Verification tap appends endpoints_verified + trust_l1_assigned
  5. GET /api/chain/:hash on ucca-keys returns valid chain JSON ✓
  6. Chain verification recomputes and confirms each node hash ✓
  7. Keys card: dark palette, no network/location, VERIFIED badge green ✓
  8. Keys card: chain summary line shows event count + intact/broken ✓
  9. Full test registration → verify → check keys card shows CHAIN [n] events · intact ✓

Deploy order

  1. D1 schema changes first (ALTER TABLE + backfill)
  2. Deploy ucca-ir with chain functions + auto-events
  3. Deploy ucca-keys with chain endpoint + card redesign
  4. Full end-to-end test registration
  5. Paste chain JSON from /api/chain/[hash] for review
  6. Commit and push

Puppeteer screenshot of new keys card at 1440×900 and 430×932 before deploying ucca-keys.