PLAN.md raw

Smesh Build Plan

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.

Part 1 — Relay: moxie compatibility (DONE)

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.

Part 2 — Frontend & signer

Architecture

┌──────────────────────────────────────────────────────┐
│ 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)   │    │
│  └──────────────────────────────────────────────┘    │
└──────────────────────────────────────────────────────┘

Signer modal

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.

Trust boundary

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

Firefox MV2

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.


Directory layout

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.

JS shims (the only non-Go browser code)

injected.js (~120 LOC)

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.

content-script.js (~60 LOC)

handler registered (i.e., not on smesh origin)

prompt.js (~100 LOC)

Shadow DOM prompt for non-smesh sites:

New moxie jsruntime module: ext.mjs

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.


Argon2id

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.

Build pipeline

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.


Stages

R1 — Relay moxie compat (DONE)

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).

F0 — Common library (DONE)

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.

F1 — Service workers (DONE)

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.

F2 — sm3sh app (DONE)

Ported from orly.dev/next/sm3sh/. 3 Go files (main, qr, version), 4232 lines.

Gate: PASSED — go vet ./... clean.

F3 — Signer modal (DONE)

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.

F4 — Extension background (DONE)

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.

F5 — JS shims (DONE)

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.

F6 — Integration (IN PROGRESS)

Wire everything:

  1. Relay serves web/static/ ✓ (Fallback handler + SMESHSTATICDIR)
  2. Makefile fixed: uses moxiejs (not moxie build -target=js) ✓
  3. index.html fixed: id="app-root" to match Go code ✓
  4. style.css: CSS variables + signer modal styles ✓
  5. All three frontend packages compile with moxiejs ✓
  6. sm3sh detects extension, modal works — needs browser test
  7. Signing: compose → modal prompt → sign → publish
  8. DMs: encrypt via extension → publish → receive → decrypt
  9. Vault import/export through modal

F7 — MLS/Marmot (deferred)

Port Marmot from signer/src/mls-engine.ts and sw/ marmot handling. Requires auditing go-mls for moxie or reimplementing MLS in pure Go.

Execution order

R1 ✓  F0 ✓  F1 ✓  F2 ✓  F3 ✓  F4 ✓  F5 ✓
                                         │
                              F6 (integration) ── in progress
                                         │
                              F7 (MLS, deferred)

Remaining: F6 (browser testing, signing flow, DM flow), F7 (MLS).


What gets eliminated

From orly.dev/next/signer/:

From orly.dev/next/sw/:

From the build:

Lines estimate

StageLinesType
R1 — Relay moxie compat~200Fix
F0 — Common library~4,100Port
F1 — Service workers~1,400Port (stripped)
F2 — sm3sh app~3,000+Port
F3 — Signer modal~600New
F4 — Extension background~1,500New
F5 — JS shims~380New (JS)
F6 — Integration~200Glue
ext.mjs (jsruntime)~80New (JS)
Total~11,500+