| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
| Name | Name | Last commit date | ||
|---|---|---|---|---|
Beamio CoNET Chat SDK — runs the gossip transport entirely inside a Web Worker (zero main-thread openpgp decrypt/encrypt, zero ethers.verifyMessage), plus fragmented, symmetrically-encrypted IPFS history. UI-agnostic, reusable across SilentPassUI / bizSite / Alliance / POS.
Motivation: running openpgp.decrypt + ethers.verifyMessage on the main thread caused a "UI freeze for tens of seconds after startup". This SDK moves all inbound decryption, outbound encryption, signing, and SSE connect/reconnect into a Worker. The main thread only orchestrates postMessage and dispatches events.
┌────────────────────────── Main thread (host UI) ──────────────────────────┐
│ BeamioChatClient │
│ • init() → getNodes() → postMessage(init) │
│ • sendMessage / queryPresence / setRoutes / setNodes │
│ • on('message'|'delivery'|'presence'|'status'|'log'|historyBuffer) │
│ • history.load / history.append / history.onBuffer │
│ ▲ plaintext env.line → host's existing addNewMessage serial queue │
└──────────────┬───────────────────────────────▲───────────────────────────┘
postMessage(Command) postMessage(Event/Receipt)
┌──────────────▼───────────────────────────────┴───────────────────────────┐
│ Web Worker (worker/entry.ts) │
│ • GossipCore: SSE connect/reconnect, openpgp decrypt/encrypt, │
│ EIP-191 sign, presence wallet_online_query, delivery ACK │
│ • HistoryStore: master + HKDF + ratcheting fragments + AES-GCM + IPFS │
└──────────────────────────────────────────────────────────────────────────┘
Install as a dependency (per src-subprojects-are-independent: each sub-project owns formal dependencies; never cross-../.. import).
npm install @conet.project/chat-sdkPeer dependencies (provided by the host):
| Package | Version |
|---|---|
| ethers | ^6.0.0 |
| openpgp | ^5.0.0 || ^6.0.0 |
Runtime: a browser / WebView with Worker + crypto.subtle (PWA inside iOS/Android native shells is supported). Node >=18 for build/test only.
The host creates the Worker (the SDK does not hardcode a worker URL, to stay bundler-agnostic):
import { createBeamioChatClient, type BeamioChatConfig } from '@conet.project/chat-sdk'
const config: BeamioChatConfig = {
identity: {
eoaAddress, // 0x… (SDK normalizes internally)
privateKeyHex, // raw EOA private key hex, used only inside the Worker
pgpPrivateKeyArmored, // armored PGP private key (decrypt inbound)
pgpPassphrase: '',
pgpPublicKeyArmored, // armored PGP public key (keyID / diagnostics)
ownRouteArmoredPublicKey, // your mailbox B route public key
},
conetRpcUrl: 'https://rpc1.conet.network',
addressPgpContractAddress: CONET_ADDRESS_PGP,
getNodes: async () => fetchHealthyNodes(), // host owns node discovery/caching
ipfsBaseUrl: 'https://ipfs.conet.network/api',
chainId: 224422, // chainId used in the history-master derivation domain (default 224422 CoNET L1)
}
const client = createBeamioChatClient(config, {
workerFactory: () =>
new Worker(new URL('@conet.project/chat-sdk/worker', import.meta.url), { type: 'module' }),
})
await client.init() // start Worker + inject keys/nodes; the Worker begins listening
// inbound plaintext lines → hand to the host's existing serial checkSign/parse queue
const off = client.on('message', (env) => {
addNewMessage(env.line) // env.line already carries _beamioPgpArmorHash for delivery ACK
})
// later, update the contacts to listen for / probe
client.setRoutes(myContactRoutes)| Environment | workerFactory |
|---|---|
| Vite | () => new Worker(new URL('@conet.project/chat-sdk/worker', import.meta.url), { type: 'module' }) |
| Webpack 5 / CRA (Craco) | same as above; Webpack 5 statically recognizes new Worker(new URL(...)) and emits a separate worker chunk |
| Vendored (see below) | () => new Worker(new URL('../vendor/beamio-chat-sdk/worker/entry.ts', import.meta.url), { type: 'module', name: 'beamio-chat-gossip' }) |
| Field | Type | Description |
|---|---|---|
| identity | ChatIdentity | see below |
| conetRpcUrl | string | CoNET DePIN RPC (reads AddressPGP, etc.) |
| addressPgpContractAddress | string | AddressPGP contract address |
| getNodes | () => Promise<NodeInfo[]> | returns a snapshot of currently healthy nodes (host owns discovery/caching) |
| ipfsBaseUrl | string | IPFS fragment gateway base, e.g. https://ipfs.conet.network/api |
| ipfsWriteBaseUrl? | string | IPFS write base (defaults to ipfsBaseUrl) |
| persistence? | PersistenceAdapter | IndexedDB adapter (optional; history is memory-only without it) |
| runtime? | ChatRuntimeOptions | sendFanout (default 3), reconnectBaseMs (4000), reconnectMaxMs (30000), outerWrap (default true) |
| chainId? | number | chainId in the history-master derivation domain (default 224422 CoNET L1) |
| Field | Description |
|---|---|
| eoaAddress | EOA address |
| privateKeyHex | raw private key hex (with or without 0x). Used only inside the Worker for EIP-191 signing and history-master derivation |
| pgpPrivateKeyArmored | armored PGP private key, decrypts inbound |
| pgpPassphrase? | PGP private key passphrase (if any) |
| pgpPublicKeyArmored? | armored PGP public key |
| ownRouteArmoredPublicKey? | your mailbox B route public key (listen encryption target) |
One-time key generation + registerChatRoute route registration stay on the host (curve25519 generation is fast and not a freeze source). Inject the generated keys via identity.
| Field | Description |
|---|---|
| address | contact EOA (lowercase) |
| userPublicKeyArmored | recipient's user PGP public key — business-message encryption target |
| routerArmoredPublicKey? | recipient's mailbox B route public key — listen / ACK encryption target |
| routePgpKeyID? | route keyID (optional) |
| Method | Description |
|---|---|
| init(): Promise<void> | start Worker, inject config/nodes; the Worker begins listening. Idempotent (no-op if already running) |
| sendMessage(to, payload, opts?) | encrypt and send a business payload to a contact via entry A ≠ B. opts.beamioNoPush wraps { data, NoPush: true } to the contact mailbox route key (sender receipts). HTTP stays { data } only. Returns { sendId } |
| queryPresence(contacts) | probe mailbox listen-pool presence; returns Record<addrLower, boolean> (only ok:true counts) |
| setRoutes(routes) | update the set of contacts to listen for / probe |
| setNodes(nodes) | push a refreshed node snapshot (host owns discovery) |
| postMailboxCommand(routerArmoredPublicKey, command) | encrypt an arbitrary mailbox command (e.g. gossip_delivery_ack) to route B, sent via entry C ≠ B |
| on(event, cb): Unsubscribe | subscribe to events (below); returns an unsubscribe function |
| history | BeamioChatHistory (below) |
| pause() / resume() | pause/resume Worker listening |
| destroy() | tear down the Worker and all listeners (idempotent) |
| Event | Payload | Description |
|---|---|---|
| message | InboundEnvelope | env.line = host-ready JSON plaintext line (carries _beamioPgpArmorHash), handed to the host's serial checkSign/parse queue |
| delivery | DeliveryReceiptEvent | delivery receipt (sendId / deliveredAt / from); host marks its own bubble as delivered |
| presence | PresenceEvent | online: Record<addr, boolean>, reliable results only |
| status | StatusEvent | idle/connecting/listening/reconnecting/paused/error; listening fires on each heartbeat to refresh main-thread staleness |
| log | ChatLogEvent | structured log (never contains private keys / plaintext / full ciphertext) |
| historyBuffer | HistoryBufferEvent | incremental batches during history restore/append, fed to the UI incrementally |
| Method | Description |
|---|---|
| load(options?) | restore encrypted history: locate index (point-${L} / IndexedDB) → decrypt → decrypt the tail first → backfill in the background; emits historyBuffer incrementally |
| append(entry) | write a newly received/sent entry to encrypted history + local mirror |
| onBuffer(cb): Unsubscribe | subscribe to incremental batches during restore/append |
HistoryLoadOptions: peer? (single contact or all), tailCount? (default 60, i.e. "last 2 screens"), localOnly? (IndexedDB only, for instant display).
Follows src/docs/gitbook/l0/si-developer-guide.md, conet-p2p-mailbox-routing-protocol, and beamio-conet-chat-protocol:
master = keccak256( EOA_sign("beamio.chat.history.v1|chainId|eoa") ) // private key never leaves the Worker
locator L = HKDF(master, "index-locator") // hidden in a hash ocean; server-side point-${L} points to the current index
indexKey = HKDF(master, "index-enc") // AES-256-GCM encrypts the ordered index manifest
fragment ratchet k_i = HKDF(master, `frag|${seq}|${cid_{i-1}}`) // cid_{-1}=HKDF(master,"frag-genesis")
cipher_i = AES-GCM(k_i, plaintext_i); cid_i = keccak256(cipher_i) // uploaded as a fragment
Because sub-projects live in separate Git repos and file: deps are awkward on remote builds, SilentPassUI vendors the SDK source into src/vendor/beamio-chat-sdk/, bridged via services/chatWorkerBridge.ts:
// services/chatWorkerBridge.ts (key points)
import { createBeamioChatClient } from '../vendor/beamio-chat-sdk'
function makeGossipWorker(): Worker {
return new Worker(new URL('../vendor/beamio-chat-sdk/worker/entry.ts', import.meta.url), {
type: 'module',
name: 'beamio-chat-gossip',
})
}
const client = createBeamioChatClient(config, { workerFactory: makeGossipWorker })
client.on('message', (env) => { if (env.line) onLine(env.line) }) // → App.tsx addNewMessage
client.on('status', (st) => { if (st.status === 'listening') noteGossipActivity() })
await client.init()In services/chat.ts, connectToGossipNode only parses routes + healthy entries (for delivery ACK) on the main thread; the inbound LISTEN loop is fully delegated to the Worker. Decrypted plaintext lines flow through onLine → newMessage → App.tsx addNewMessage (unchanged serial queue).
Keep the vendored copy in sync with the source; changes to this SDK must be mirrored into src/SilentPassUI/src/vendor/beamio-chat-sdk/. Vendored relative imports strip .js suffixes to match CRA moduleResolution:"node".
npm run build # tsc -p tsconfig.json → dist/ (ESM + .d.ts)
npm run typecheck # tsc --noEmit
npm run clean # rm -rf distsrc/
index.ts # public entry: createBeamioChatClient + types
client.ts # main-thread BeamioChatClient (starts worker + postMessage protocol)
protocol.ts # main ↔ worker postMessage protocol
types.ts # public types (UI-agnostic)
crypto.ts # WebCrypto + keccak/HKDF/AES-GCM (worker & main thread)
envelope.ts # POST body, route-command encrypt, per-entry outer wrap
nodes.ts # node probing + `postUrl` (worker & main thread)
worker/
entry.ts # worker entry (imported only in a Worker context)
gossip-core.ts # SSE + openpgp decrypt/encrypt + sign + presence + ACK
history.ts # encrypted fragmented IPFS history + IndexedDB
Package exports:
| Entry | Usage |
|---|---|
| @conet.project/chat-sdk | main-thread API (createBeamioChatClient + types) |
| @conet.project/chat-sdk/worker | worker entry; only used as the target of new Worker(...) |
MIT (private release, publishConfig.access = restricted).
| Back | FazBrowse Home | New Git URL |