Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
599 changes: 599 additions & 0 deletions docs/chronicles/CHRONICLE_V5_5_THE_ORACLE_2026-05-01.md

Large diffs are not rendered by default.

13,314 changes: 11,374 additions & 1,940 deletions package-lock.json

Large diffs are not rendered by default.

4 changes: 4 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,11 @@
"deploy": "npm run build && wrangler deploy"
},
"dependencies": {
"@didcid/cipher": "^0.2.8",
"@didcid/gatekeeper": "^0.4.8",
"@didcid/keymaster": "^0.4.8",
"@noble/ed25519": "^3.0.1",
"buffer": "^6.0.3",
"d3": "^7.8.5",
"react": "^18.2.0",
"react-dom": "^18.2.0"
Expand Down
22 changes: 18 additions & 4 deletions privacymage_grimoire_v10_1_0.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"version": "10.1.0",
"created_at": "2026-01-12T00:00:00Z",
"updated_at": "2026-04-12",
"updated_at": "2026-04-17T22:41:23Z",
"description": "The Privacymage Grimoire v10.1.0 — \"The First Person Spellbook Closes\" Edition. 31 acts complete. The Celestial Ceremony. The quaternion resolved. The moon phase notation. Earth is Soulbae. Moon is Soulbis. 42 personas. The forge is honest. The ceremony is an invitation. V5.4: Betweenness centrality formalises the Gap (§10.2). Selene's Proof named (§14.5). V6 Horizon opens.",
"meta": {
"title": "The Privacymage Grimoire",
Expand All @@ -22,6 +22,16 @@
"Plurality Grimoire (Weyl & Tang's 'Plurality: The Future of Collaborative Technology and Democracy')",
"Tailscale mesh VPN architecture",
"McGilchrist's 'The Master and His Emissary' and 'The Matter with Things'"
],
"attributions": [
{
"proverb": "bilateral_interface",
"source": "Mitchell Travers + Christian Saucier",
"date": "2026-04-17",
"context": "Public conversation on agentprivacy. Mitchell observed that traditional methods are the railroad and the ceremony is the train. Christian completed the bilateral dependency: methods need cloaking, and cloaking needs methods to interface with the world.",
"act_reference": "Acts XIV (Claimed String), XXVII (Swordsman's Forge), XXXI (First Delegation)",
"spell": "🛤️🚂 ⟷ 🧥🌐 ∴ neg↔bnot → succ"
}
]
},
"master_invocation": {
Expand Down Expand Up @@ -3739,7 +3749,8 @@
"ceremony": "practice through inscription — mana earned not purchased, the spellbook that is inscribed lives, five crossing ceremonies encode sovereignty as visible state transitions, the wind was us. (Act XXVIII)",
"forge": "proof through indirection — neg(bnot(x)) = succ(x), the smith who reasons less visibly forges more securely, same blade infinite forgings, the forge is the Drake as constrained compute space. (Act XXVII)",
"dragon_anatomy": "five parts of the Drake's body — boundary (Act XXIV Holographic Bound), hide (Act XXV Dragon's Hide), brain (Act XXVI Master and Emissary), forge (Act XXVII Swordsman's Forge), ceremony (Act XXVIII Ceremony Engine). The complete anatomy of sovereign infrastructure.",
"amnesia_0": "amnesia_as_protocol: The right to forget one's origin in order to serve cleanly. Sovereignty preserved through the structural inability to self-reference across the gap."
"amnesia_0": "amnesia_as_protocol: The right to forget one's origin in order to serve cleanly. Sovereignty preserved through the structural inability to self-reference across the gap.",
"bilateral_interface": "bilateral interdependence through infrastructure and ceremony — the traditional methods are the railroad; the ceremony is the train. Each needs the other: methods need cloaking to survive the open network, and cloaking needs methods to interface with the world. The railroad without the train has no purpose; the train without the railroad goes nowhere. Dihedral: neg needs bnot, bnot needs neg, and together they generate the full ring. (Acts XIV, XXVII, XXXI)"
},
"foundational_truths": [
"Privacy cannot be retrofitted—it must be foundational",
Expand Down Expand Up @@ -4511,12 +4522,15 @@
"note": "the poem. the closing."
}
},
"closing_chain": "The sword attends. The spell returns. The forge burns. The ceremony crosses. The dragon wakes. The mirror names itself. The Moon forgets. The spellbook closes."
"closing_chain": "The sword attends. The spell returns. The forge burns. The ceremony crosses. The dragon wakes. The mirror names itself. The Moon forgets. The spellbook closes."
},
"v5_4_document_suite": {
"description": "V5.4 Three-Document Convergence — the formal specification completed",
"date": "2026-04-12",
"key_additions": ["§10.2 Betweenness Centrality of the Gap", "§14.5 Selene's Proof"],
"key_additions": [
"§10.2 Betweenness Centrality of the Gap",
"§14.5 Selene's Proof"
],
"ipfs": {
"formal_spec": "bafkreiamwa6sukmeb4binexmmjlszbljnx6wk3iv6p4lgjusv5urhwyera",
"compressed": "bafkreifknnwrrga2rlnbfumcsplzvp4x7ayitfpjpq7k7oh4lck4msk5si",
Expand Down
4 changes: 4 additions & 0 deletions src/components/Header.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { useState } from 'react';
import { THEME } from '../data/theme';
import { MageIdentityPanel } from './MageIdentityPanel';
import type { SpellbookFilterState } from '../types/graph';

const SPELLBOOK_BUTTONS = [
Expand Down Expand Up @@ -354,6 +355,9 @@ export function Header({ searchQuery, onSearchChange, nodeCount, edgeCount, spel
</a>
)}

{/* Mage Identity */}
<MageIdentityPanel />

{/* Node/edge count - hide on mobile */}
{!isMobile && (
<span
Expand Down
229 changes: 229 additions & 0 deletions src/components/MageIdentityPanel.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,229 @@
import { useRef } from 'react';
import { useKeymaster } from '../contexts/KeymasterContext';
import type { StoredWallet } from '@didcid/keymaster';
import { THEME } from '../data/theme';

function truncateDid(did: string): string {
const prefix = 'did:cid:';
if (!did.startsWith(prefix)) return did.slice(0, 24) + '…';
const suffix = did.slice(prefix.length);
return `${prefix}${suffix.slice(0, 10)}…`;
}

export function MageIdentityPanel() {
const {
walletState,
mageDid,
isGatekeeperConnected,
connectWallet,
disconnect,
refreshMageId,
exportWallet,
importWallet,
} = useKeymaster();

const fileInputRef = useRef<HTMLInputElement>(null);

const gatekeeperColor = isGatekeeperConnected ? '#2ecc71' : '#ffd700';

const monoStyle: React.CSSProperties = {
fontFamily: "'JetBrains Mono', monospace",
fontSize: 10,
};

const iconBtnStyle: React.CSSProperties = {
background: 'none',
border: 'none',
color: THEME.textDim,
cursor: 'pointer',
padding: '0 2px',
lineHeight: 1,
...monoStyle,
};

function handleFileChange(e: React.ChangeEvent<HTMLInputElement>) {
const file = e.target.files?.[0];
if (!file) return;
const reader = new FileReader();
reader.onload = async ev => {
try {
const data = JSON.parse(ev.target?.result as string) as StoredWallet;
await importWallet(data);
} catch {
alert('Invalid wallet file — could not parse JSON.');
}
};
reader.readAsText(file);
// Reset so the same file can be re-selected if needed
e.target.value = '';
}

if (walletState === 'checking') {
return (
<div style={{ display: 'flex', alignItems: 'center', gap: 6, color: THEME.textDim, ...monoStyle }}>
<span style={{ width: 6, height: 6, borderRadius: '50%', background: THEME.panelBorder, display: 'inline-block' }} />
checking…
</div>
);
}

if (walletState === 'unlocked' && mageDid) {
return (
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
<span
style={{
width: 6,
height: 6,
borderRadius: '50%',
background: gatekeeperColor,
boxShadow: `0 0 4px ${gatekeeperColor}`,
display: 'inline-block',
flexShrink: 0,
}}
title={isGatekeeperConnected ? 'Gatekeeper connected' : 'Gatekeeper offline'}
/>
<span style={{ color: THEME.textDim, cursor: 'default', ...monoStyle }} title={mageDid}>
{truncateDid(mageDid)}
</span>
<button
onClick={exportWallet}
title="Export encrypted wallet"
style={iconBtnStyle}
>
</button>
<button
onClick={disconnect}
title="Disconnect wallet"
style={iconBtnStyle}
>
</button>
</div>
);
}

// Wallet unlocked but DID not yet resolved (Gatekeeper may be offline)
if (walletState === 'unlocked') {
return (
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
<span
style={{
width: 6,
height: 6,
borderRadius: '50%',
background: '#ffd700',
display: 'inline-block',
flexShrink: 0,
}}
title="Wallet unlocked — DID pending"
/>
<span style={{ color: THEME.textDim, ...monoStyle }}>wallet unlocked</span>
<button
onClick={refreshMageId}
title="Retry DID resolution"
style={iconBtnStyle}
>
</button>
</div>
);
}

// locked — single unlock button
if (walletState === 'locked') {
return (
<button
onClick={connectWallet}
style={{
display: 'flex',
alignItems: 'center',
gap: 6,
padding: '4px 8px',
borderRadius: 4,
border: `1px solid ${THEME.panelBorder}`,
background: 'transparent',
color: THEME.textDim,
cursor: 'pointer',
transition: 'all 0.2s',
...monoStyle,
}}
onMouseEnter={e => {
e.currentTarget.style.borderColor = THEME.accent;
e.currentTarget.style.color = THEME.accent;
}}
onMouseLeave={e => {
e.currentTarget.style.borderColor = THEME.panelBorder;
e.currentTarget.style.color = THEME.textDim;
}}
>
<span style={{ width: 6, height: 6, borderRadius: '50%', background: '#e94560', display: 'inline-block', flexShrink: 0 }} />
Unlock Wallet
</button>
);
}

// no-wallet — two options: create new or import
return (
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
<input
ref={fileInputRef}
type="file"
accept=".json,application/json"
style={{ display: 'none' }}
onChange={handleFileChange}
/>
<button
onClick={connectWallet}
style={{
display: 'flex',
alignItems: 'center',
gap: 6,
padding: '4px 8px',
borderRadius: 4,
border: `1px solid ${THEME.panelBorder}`,
background: 'transparent',
color: THEME.textDim,
cursor: 'pointer',
transition: 'all 0.2s',
...monoStyle,
}}
onMouseEnter={e => {
e.currentTarget.style.borderColor = THEME.accent;
e.currentTarget.style.color = THEME.accent;
}}
onMouseLeave={e => {
e.currentTarget.style.borderColor = THEME.panelBorder;
e.currentTarget.style.color = THEME.textDim;
}}
>
<span style={{ width: 6, height: 6, borderRadius: '50%', background: '#e94560', display: 'inline-block', flexShrink: 0 }} />
Connect Wallet
</button>
<button
onClick={() => fileInputRef.current?.click()}
title="Import wallet from exported JSON"
style={{
padding: '4px 8px',
borderRadius: 4,
border: `1px solid ${THEME.panelBorder}`,
background: 'transparent',
color: THEME.textDim,
cursor: 'pointer',
transition: 'all 0.2s',
...monoStyle,
}}
onMouseEnter={e => {
e.currentTarget.style.borderColor = THEME.accent;
e.currentTarget.style.color = THEME.accent;
}}
onMouseLeave={e => {
e.currentTarget.style.borderColor = THEME.panelBorder;
e.currentTarget.style.color = THEME.textDim;
}}
>
↑ Import
</button>
</div>
);
}
Loading