One compiler: moxie. Two targets: native (relay), JS (browser). No Google Go toolchain. No npm. No Angular. No TypeScript. No Chrome.
Moxie compiles Go to:
Extension: Firefox MV2. Persistent background page. No SW lifecycle. Signer UI: modal inside the sm3sh app. No extension popup.
The relay compiles and runs under moxie with zero code changes. encoding/json and sync.Mutex work in moxie's stdlib overlay.
Moxie fixes applied (in ../moxie):
Build: MOXIEROOT=../moxie ../moxie/moxie build -o smesh .
Test: relay starts, serves HTTP, handles WebSocket + Nostr REQ/EOSE.
┌──────────────────────────────────────────────────────┐
│ smesh relay (moxie → native binary) │
│ Serves static files: *.mjs, *.html, *.css │
│ WebSocket: /ws Blossom: /blossom/ │
└───────────────────────┬──────────────────────────────┘
│ HTTP/WS
┌───────────────────────▼──────────────────────────────┐
│ Browser │
│ │
│ ┌──────────────────────────────────────────────┐ │
│ │ sm3sh app (moxie → .mjs) │ │
│ │ Feed, messages, profiles, relay mgmt │ │
│ │ Signer modal overlay │ │
│ │ Calls window.nostr / window.nostr.smesh │ │
│ └───────┬──────────────────────────────────────┘ │
│ │ BroadcastChannel │
│ ┌───────▼─────────┐ ┌────────────────────────┐ │
│ │ Shell SW (.mjs) │ │ Core SW (.mjs) │ │
│ │ Lifecycle, cache │◄─┤ Relay proxy, IDB cache │ │
│ │ Version monitor │ │ Subscriptions, routing │ │
│ └──────────────────┘ └────────────────────────┘ │
│ │
│ ┌──────────────────────────────────────────────┐ │
│ │ Signer Extension (Firefox MV2) │ │
│ │ │ │
│ │ background page ── signer-bg (.mjs) │ │
│ │ Vault (Argon2id + AES-256-GCM) │ │
│ │ Key store, signing, NIP-04/44 │ │
│ │ Permission evaluation │ │
│ │ Management API (for sm3sh modal) │ │
│ │ │ │
│ │ content-script.js (~60 LOC, bridge) │ │
│ │ injected.js (~120 LOC, window.nostr) │ │
│ │ prompt.js (~100 LOC, shadow DOM) │ │
│ └──────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────┘
The signer's full UI lives inside the sm3sh app as a modal overlay. No extension popup. No separate unlock/options/prompt HTML pages.
The modal communicates with the extension background via
window.nostr.smesh (management API exposed by injected.js).
Secret keys never leave the extension background.
Within sm3sh: signing prompts appear in the modal. The app
registers a prompt handler via window.nostr.smesh.onPrompt(cb).
When the extension needs approval, it sends a PROMPT message and the
app shows the modal with the details.
On other sites: content-script.js injects a shadow DOM prompt (prompt.js). Minimal: backdrop, card, allow/deny buttons.
The extension background is the trust boundary.
TRUSTED (extension background):
Secret keys, Argon2id derivation, AES-256-GCM vault,
event signing, NIP-04/44, permission evaluation
UNTRUSTED (page context):
sm3sh app, signer modal, other Nostr apps
All communicate via message passing, never see keys
Persistent background page. Loads moxie .mjs once, stays alive. No service worker termination. No READY handshakes. No message queuing for dead peers. No callback ID resurrection.
{
"manifest_version": 2,
"name": "Smesh Signer",
"version": "2.0.0",
"background": {
"page": "bg/background.html",
"persistent": true
},
"content_scripts": [{
"matches": ["<all_urls>"],
"js": ["content-script.js"],
"run_at": "document_start"
}],
"web_accessible_resources": ["injected.js", "prompt.js"],
"permissions": ["storage", "activeTab"],
"browser_specific_settings": {
"gecko": { "id": "signer@smesh.lol" }
}
}
If Firefox ever drops MV2, fork the extension API surface. It's a frozen spec, not a moving target. Same approach as Cinnamon/GNOME.
smesh/
go.mod # module smesh.lol (relay)
main.go # relay entry point
pkg/ # relay packages (existing)
acl/ # access control
blossom/ # file uploads
crawler/ # relay discovery
errors/ # error types
find/ # search
grapevine/ # WoT scoring
lol/ # logging
mode/ # runtime mode
neterr/ # network error interface
nostr/ # full Nostr protocol stack
ec/ # secp256k1, schnorr, bech32, base58
envelope/ # 9 envelope types
event/ # create, sign, verify, serialize
filter/ # filter matching
... # hex, ints, keys, kind, signer, tag, text, etc.
ws/ # WebSocket client
relay/ # relay server
graph/ # social graph queries
pipeline/ # event processing pipeline
ratelimit/ # rate limiter
server/ # HTTP + WebSocket server
store/ # storage engine
index/ # sorted index keys
serial/ # serial mapping
sorted/ # sorted flat file
wal/ # write-ahead log
sync/negentropy/ # negentropy sync protocol
typer/ # type interface
web/
common/ # shared Go library for all browser targets
go.mod # module common
nostr/ # lightweight: event, filter, tags, parse
helpers/ # hex, base64, bech32, json
crypto/ # browser crypto (calls WASM secp256k1)
secp256k1/ # Go API → jsruntime/crypto.mjs → WASM
sha256/ # pure Go SHA-256
chacha20/ # ChaCha20 stream cipher
hkdf/ # HKDF key derivation
hmac/ # HMAC
nip04/ # NIP-04 encrypt/decrypt
nip44/ # NIP-44 encrypt/decrypt
jsbridge/ # Go wrappers for moxie jsruntime
dom/ # DOM manipulation via handles
ws/ # WebSocket
sw/ # Service Worker lifecycle + cache
bc/ # BroadcastChannel
idb/ # IndexedDB
localstorage/ # localStorage
registry/ # extension ↔ SW message routing
signer/ # window.nostr calls from Go
subtle/ # Web Crypto (AES-CBC, random)
crypto/ # secp256k1 WASM bridge
wasm/ # WASM bootstrap
ext/ # NEW: browser.storage, browser.runtime
relay/ # browser-side relay client
conn.go # single relay WebSocket
pool.go # connection pool
sub.go # subscription
app/ # sm3sh main app (moxie → .mjs)
go.mod # requires common
main.go # feed, messages, profiles, relay mgmt
qr.go # QR code encoder
signer.go # NEW: signer modal overlay
sw/ # Shell Service Worker (moxie → .mjs)
go.mod
main.go # lifecycle, static cache, version monitor
bus.go # BroadcastChannel to core SW
router.go # subscription routing
state.go # shared state
identity.go # pubkey tracking (no secret key)
decrypt.go # DM display records
sw-core/ # Core Service Worker (moxie → .mjs)
go.mod
main.go # lifecycle
cache.go # IndexedDB event storage
client.go # relay pool / proxy
identity.go # key management
router.go # subscription dispatcher
state.go # DM state, crypto callbacks
signer-bg/ # Extension background (moxie → .mjs)
go.mod # requires common
main.go # entry point, message dispatcher
vault.go # Argon2id + AES-256-GCM
identity.go # multi-identity management
nip07.go # NIP-07 method handler
permissions.go # per-host/method permission store
management.go # management API for sm3sh modal
mls.go # Marmot/MLS (deferred)
ext/ # Extension static files (JS, not compiled)
manifest.json
background.html # <script type="module" src="bg/$entry.mjs">
content-script.js # message bridge (~60 LOC)
injected.js # window.nostr + window.nostr.smesh (~120 LOC)
prompt.js # shadow DOM prompt for non-smesh sites (~100 LOC)
icons/
static/ # Built output served by relay
index.html
sw-register.js
style.css
# moxie outputs: $entry.mjs, smesh3.mjs, common_crypto_*.mjs, etc.
Injected into page context. Exposes:
window.nostr = {
// NIP-07
getPublicKey() → string
signEvent(event) → SignedEvent
getRelays() → RelayMap
nip04.encrypt(pk, pt) → string
nip04.decrypt(pk, ct) → string
nip44.encrypt(pk, pt) → string
nip44.decrypt(pk, ct) → string
// Management (sm3sh modal → extension)
smesh: {
isInstalled() → true
getVaultStatus() → "locked"|"unlocked"|"none"
unlockVault(password) → bool
lockVault() → void
createVault(password) → bool
listIdentities() → [{pubkey, name, active}]
switchIdentity(pubkey) → bool
addIdentity(nsec) → bool
removeIdentity(pubkey) → bool
getPermissions() → [{host, method, policy}]
setPermission(host, method, p) → void
onPrompt(callback) → void
}
}
All methods return Promises. All route through postMessage → content-script → browser.runtime.sendMessage → background.
handler registered (i.e., not on smesh origin)
Shadow DOM prompt for non-smesh sites:
Wraps Firefox WebExtension APIs for the signer background:
// browser.storage.local
export function StorageGet(key, fn) // fn(value)
export function StorageSet(key, value)
export function StorageRemove(key)
// browser.runtime messaging
export function OnMessage(fn) // fn(msg, senderTabId, respond)
export function SendMessageToTab(tabId, msg)
// browser.tabs
export function GetActiveTab(fn) // fn(tabId, url)
Go side: web/common/jsbridge/ext/ext.go calls these.
Current signer uses hash-wasm (npm). Options for moxie:
A — Pure Go Argon2id (recommend first): RFC 9106 implementation in Go, compiled to JS by moxie. Blake2b hashing + memory-hard mixing. Single-threaded is fine for a KDF — slow is the point. Current signer takes ~3s in WASM; pure JS at 2-5x slower = 6-15s, acceptable for vault unlock.
B — Argon2id WASM module: If A is too slow, ship a small WASM binary (same pattern as secp256k1.wasm). Load in background page.
C — PBKDF2 via SubtleCrypto: Hardware-accelerated but less memory-hard. Different vault format. Last resort.
Start with A. Fall back to B if >15s.
MOXIE := ../moxie/moxie
MOXIEJS := ../moxie/moxiejs
JSRUNTIME := ../moxie/jsruntime
# === Relay (native, via LLVM) ===
build-relay:
$(MOXIE) build -o smesh .
# === Frontend (JS, via moxiejs) ===
build-app:
cd web/app && GOWORK=off $(MOXIEJS) -runtime $(JSRUNTIME) -o web/static/ .
build-sw:
cd web/sw && GOWORK=off $(MOXIEJS) -runtime $(JSRUNTIME) -o web/static/$sw/ .
# === Signer extension (JS, via moxiejs) ===
build-signer-bg:
cd web/signer-bg && GOWORK=off $(MOXIEJS) -runtime $(JSRUNTIME) -o web/ext/bg/ .
build-ext: build-signer-bg
cd web/ext && zip -r ../../dist/smesh-signer.xpi \
manifest.json background.html content-script.js \
injected.js prompt.js icons/ bg/
# === All ===
build: build-relay build-app build-sw build-ext
# === Dev (frontend only, relay already running) ===
dev: build-app build-sw
Two tools: moxie (LLVM → native binary) and moxiejs (SSA → ES modules).
No go build anywhere.
No code changes needed in smesh. Moxie's stdlib handles encoding/json and sync.Mutex. Fixes were in moxie itself (see Part 1 above).
Gate: PASSED — moxie build -o smesh ., relay starts, handles
HTTP and WebSocket (REQ/EOSE verified).
Ported from orly.dev/next/common/ → smesh/web/common/. 35 Go files, 3843 lines. All packages vet clean. Module: smesh.lol/web/common. Added jsbridge/ext for extension APIs.
Gate: PASSED — go vet ./... clean on all sub-packages.
Merged sw/ + sw-core/ into single web/sw/. Stripped all MV3 damage: no BroadcastChannel, no READY handshake, no message queuing, no registry. 7 Go files, 1056 lines (down from 2408 across two binaries).
Gate: PASSED — go vet ./... clean.
Ported from orly.dev/next/sm3sh/. 3 Go files (main, qr, version), 4232 lines.
Gate: PASSED — go vet ./... clean.
New: web/app/signer.go (~270 lines). Added management API stubs
to jsbridge/signer/signer.go. Wired signer button into top bar
in main.go, calls initSigner() on app startup.
Gate: PASSED — go vet ./... clean, moxiejs compiles to .mjs.
New: web/signer-bg/ (5 Go files, ~620 lines). main.go: message dispatcher. vault.go: iterated-SHA256 KDF + AES-CBC (Argon2id deferred). nip07.go: NIP-07 handlers. management.go: modal API. permissions.go: per-host/method store.
NIP-44 encrypt/decrypt stubbed (needs ChaCha20+HMAC+padding integration). jsbridge/ext/ext.go: browser.storage + browser.runtime stubs.
Gate: PASSED — go vet ./... clean.
New: web/ext/ (5 files). manifest.json: MV2, persistent background page. injected.js: window.nostr + window.nostr.smesh (~75 lines). content-script.js: page ↔ extension bridge (~30 lines). prompt.js: shadow DOM prompt for non-smesh sites (~90 lines). background.html: loads signer-bg .mjs.
Gate: PASSED — files written, correct MV2 structure.
Wire everything:
Port Marmot from signer/src/mls-engine.ts and sw/ marmot handling. Requires auditing go-mls for moxie or reimplementing MLS in pure Go.
R1 ✓ F0 ✓ F1 ✓ F2 ✓ F3 ✓ F4 ✓ F5 ✓
│
F6 (integration) ── in progress
│
F7 (MLS, deferred)
Remaining: F6 (browser testing, signing flow, DM flow), F7 (MLS).
From orly.dev/next/signer/:
From orly.dev/next/sw/:
From the build:
go build, go test, go mod)moxie build| Stage | Lines | Type |
|---|---|---|
| R1 — Relay moxie compat | ~200 | Fix |
| F0 — Common library | ~4,100 | Port |
| F1 — Service workers | ~1,400 | Port (stripped) |
| F2 — sm3sh app | ~3,000+ | Port |
| F3 — Signer modal | ~600 | New |
| F4 — Extension background | ~1,500 | New |
| F5 — JS shims | ~380 | New (JS) |
| F6 — Integration | ~200 | Glue |
| ext.mjs (jsruntime) | ~80 | New (JS) |
| Total | ~11,500+ |