OverviewFilesCommitsIssuesDocsReleasesPeers

atproto-bridge / pds / lib / swarm-keypair.js

51 lines · javascript ref:
// Persistent Hyperswarm keypair for the MEMBRANE.
//
// The bridge is a CLIENT of every fronted peer, and hyper-proto-peer's privileged device-surface
// RPCs (apply-write, delegate, pointer-diag, get-service-auth) gate on `canWrite(remote)` — an
// allowlist of client Noise public keys (PEER_WRITERS, device-node.js:500-501). An allowlist is
// only meaningful against a STABLE key.
//
// `new Hyperswarm()` mints a RANDOM keypair per boot, so the bridge's Noise public key differed on
// every restart and was persisted nowhere. Anything added to PEER_WRITERS therefore authorised
// exactly one boot and then silently stopped — the worst available failure mode, because the
// handler's refusal looks identical to "not allowlisted yet". That is what blocked
// app.bsky.notification.listNotifications end to end (atproto-bridge#21 / hyper-social#21).
//
// This is a TRANSPORT identity, not account key material: it is a DHT/Noise identity that never
// signs a repo commit, never touches a DID, and grants nothing on its own — it only makes the
// bridge NAMEABLE so a peer's operator can choose to trust it. Kept as a plain file beside the
// front-store (like jwt-secret and relay-seq-base) so it survives a corestore reset.
//
// Same utility + rationale as hyper-proto-peer's lib/swarm-keypair.js; written against the
// #node:* import map so it runs under both bare (prod) and node (tests) rather than lifted verbatim.

import fs from '#node:fs'
import path from '#node:path'
import b4a from 'b4a'
import crypto from 'hypercore-crypto'

const KEYPAIR_FILE = 'swarm-keypair.json'

// Load the membrane's persistent swarm keypair from <storage>/swarm-keypair.json, creating and
// persisting one on first boot. Returns a hypercore-crypto keypair ({ publicKey, secretKey }).
// `name` selects WHICH stable identity, so a second Hyperswarm (a tool, a probe) can be separately
// allowlistable rather than borrowing — two swarms on one keypair put two DHT identities behind the
// same key.
export function loadOrCreateSwarmKeyPair (storage, { name = KEYPAIR_FILE } = {}) {
  if (typeof storage !== 'string') throw new Error('loadOrCreateSwarmKeyPair: storage must be a directory path')
  const file = path.join(storage, name)
  try {
    const saved = JSON.parse(fs.readFileSync(file, 'utf8'))
    return { publicKey: b4a.from(saved.publicKey, 'hex'), secretKey: b4a.from(saved.secretKey, 'hex') }
  } catch {
    const keyPair = crypto.keyPair()
    try { fs.mkdirSync(storage, { recursive: true }) } catch {}
    fs.writeFileSync(file, JSON.stringify({
      publicKey: b4a.toString(keyPair.publicKey, 'hex'),
      secretKey: b4a.toString(keyPair.secretKey, 'hex'),
      createdAt: Date.now()
    }, null, 2), { mode: 0o600 })
    return keyPair
  }
}