@fairfox/polly 0.67.0 → 0.70.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/src/client/index.js +17 -20
- package/dist/src/client/index.js.map +4 -4
- package/dist/src/mesh.js +85 -11
- package/dist/src/mesh.js.map +7 -6
- package/dist/src/peer.js +80 -6
- package/dist/src/peer.js.map +7 -6
- package/dist/src/polly-ui/markdown.js +3 -3
- package/dist/src/polly-ui/markdown.js.map +2 -2
- package/dist/src/shared/lib/crdt-specialised.d.ts +6 -0
- package/dist/src/shared/lib/crdt-state.d.ts +8 -1
- package/dist/src/shared/lib/mesh-diagnostics.d.ts +98 -0
- package/dist/src/shared/lib/mesh-network-adapter.d.ts +0 -4
- package/dist/tools/test/src/e2e-mesh/console-allowlist.d.ts +31 -0
- package/dist/tools/test/src/e2e-mesh/index.d.ts +27 -0
- package/dist/tools/test/src/e2e-mesh/index.js +1089 -0
- package/dist/tools/test/src/e2e-mesh/index.js.map +22 -0
- package/dist/tools/test/src/e2e-mesh/keys.d.ts +55 -0
- package/dist/tools/test/src/e2e-mesh/launch-peer.d.ts +70 -0
- package/dist/tools/test/src/e2e-mesh/mesh-assertions.d.ts +53 -0
- package/dist/tools/test/src/e2e-mesh/serve-consumer.d.ts +32 -0
- package/dist/tools/test/src/e2e-mesh/wait-for-convergence.d.ts +38 -0
- package/dist/tools/test/src/e2e-mesh/with-relay.d.ts +53 -0
- package/dist/tools/test/src/visual/index.js +24 -24
- package/dist/tools/test/src/visual/index.js.map +2 -2
- package/dist/tools/verify/src/cli.js +361 -22
- package/dist/tools/verify/src/cli.js.map +6 -6
- package/dist/tools/verify/src/config.d.ts +26 -1
- package/dist/tools/verify/src/config.js +9 -1
- package/dist/tools/verify/src/config.js.map +4 -4
- package/dist/tools/verify/src/primitives/index.d.ts +30 -0
- package/dist/tools/visualize/src/cli.js +43 -1
- package/dist/tools/visualize/src/cli.js.map +3 -3
- package/package.json +11 -8
- package/LICENSE +0 -21
- package/README.md +0 -362
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../src/shared/lib/encryption.ts", "../tools/test/src/e2e-mesh/console-allowlist.ts", "../tools/test/src/e2e-mesh/keys.ts", "../src/shared/lib/signing.ts", "../tools/test/src/e2e-mesh/launch-peer.ts", "node:os", "node:path", "../src/shared/lib/mesh-diagnostics.ts", "../tools/test/src/e2e-mesh/mesh-assertions.ts", "../tools/test/src/e2e-mesh/serve-consumer.ts", "../tools/test/src/e2e-mesh/wait-for-convergence.ts", "../tools/test/src/e2e-mesh/with-relay.ts", "../src/elysia/signaling-server-plugin.ts"],
|
|
4
|
+
"sourcesContent": [
|
|
5
|
+
"/**\n * encryption — symmetric authenticated encryption for Polly's $meshState\n * primitive (Phase 2). Wraps tweetnacl's secretbox (XSalsa20-Poly1305) with\n * a small Polly-flavoured API so the rest of the codebase never imports\n * tweetnacl directly.\n *\n * Every $meshState document has a per-document symmetric key that is\n * provisioned to authorised peers at pairing time and never held by any\n * server. Outgoing operations are encrypted under this key before they\n * touch the network adapter; incoming operations are decrypted on receipt.\n * The signing layer in {@link signing.ts} provides authenticity (proof of\n * who sent the message); this layer provides confidentiality (the bytes\n * are unreadable to anything that does not hold the document key).\n *\n * tweetnacl's secretbox uses a 32-byte symmetric key and a 24-byte nonce.\n * The output of `nacl.secretbox` is the ciphertext concatenated with a\n * 16-byte Poly1305 authentication tag. We package the nonce + ciphertext\n * into a single binary blob using a small length-prefixed envelope so the\n * receiver can recover the nonce without out-of-band coordination.\n *\n * - {@link generateDocumentKey} returns a fresh 32-byte symmetric key.\n *\n * - {@link encrypt} produces a sealed blob from a payload and a key.\n *\n * - {@link decrypt} recovers the payload from a sealed blob and a key.\n * Returns undefined if the blob is malformed or the authentication\n * tag does not match (i.e. wrong key or tampered ciphertext) — the\n * undefined signal lets call sites distinguish \"wrong key\" from\n * \"structurally invalid\" without throwing.\n *\n * - {@link sealEnvelope} and {@link openEnvelope} are convenience helpers\n * that wrap encrypt/decrypt in a structured EncryptedEnvelope shape so\n * the mesh transport layer can handle the binary plumbing uniformly.\n */\n\nimport nacl from \"tweetnacl\";\n\n/** Length in bytes of a secretbox symmetric key. */\nexport const KEY_BYTES = 32;\n/** Length in bytes of a secretbox nonce. */\nexport const NONCE_BYTES = 24;\n/** Length in bytes of the Poly1305 authentication tag. */\nexport const TAG_BYTES = 16;\n\n/**\n * A sealed blob suitable for storage or network transmission. The wire\n * layout is the concatenation of the nonce and the ciphertext+tag from\n * tweetnacl. Callers should not depend on the exact bytes — round-trip\n * through {@link encrypt} / {@link decrypt} or the envelope helpers.\n */\nexport type SealedBytes = Uint8Array;\n\n/** Errors thrown by the encryption subsystem. */\nexport class EncryptionError extends Error {\n readonly code: \"invalid-key-length\" | \"decrypt-failed\" | \"envelope-malformed\";\n constructor(message: string, code: EncryptionError[\"code\"]) {\n super(message);\n this.name = \"EncryptionError\";\n this.code = code;\n }\n}\n\n/**\n * Generate a fresh 32-byte symmetric document key. Calls into tweetnacl's\n * CSPRNG.\n */\nexport function generateDocumentKey(): Uint8Array {\n return nacl.randomBytes(KEY_BYTES);\n}\n\n/**\n * Encrypt a payload under a symmetric key. The returned blob includes a\n * fresh nonce so the receiver does not need any out-of-band coordination\n * to decrypt.\n */\nexport function encrypt(payload: Uint8Array, key: Uint8Array): SealedBytes {\n if (key.length !== KEY_BYTES) {\n throw new EncryptionError(\n `secretbox key must be ${KEY_BYTES} bytes, got ${key.length}.`,\n \"invalid-key-length\"\n );\n }\n const nonce = nacl.randomBytes(NONCE_BYTES);\n const ciphertext = nacl.secretbox(payload, nonce, key);\n const out = new Uint8Array(NONCE_BYTES + ciphertext.length);\n out.set(nonce, 0);\n out.set(ciphertext, NONCE_BYTES);\n return out;\n}\n\n/**\n * Decrypt a sealed blob under a symmetric key. Returns the original\n * payload on success. Returns undefined if the blob is too short to\n * contain a nonce and tag, or if the authentication tag does not match\n * (which indicates either a wrong key or a tampered ciphertext).\n *\n * The undefined return is deliberate: call sites typically want to fall\n * back to a different key or surface a \"could not decrypt\" message rather\n * than catching an exception. Use {@link decryptOrThrow} when an exception\n * is preferred.\n */\nexport function decrypt(sealed: SealedBytes, key: Uint8Array): Uint8Array | undefined {\n if (key.length !== KEY_BYTES) {\n throw new EncryptionError(\n `secretbox key must be ${KEY_BYTES} bytes, got ${key.length}.`,\n \"invalid-key-length\"\n );\n }\n if (sealed.length < NONCE_BYTES + TAG_BYTES) {\n return undefined;\n }\n const nonce = sealed.subarray(0, NONCE_BYTES);\n const ciphertext = sealed.subarray(NONCE_BYTES);\n const opened = nacl.secretbox.open(ciphertext, nonce, key);\n return opened ?? undefined;\n}\n\n/**\n * Decrypt a sealed blob, throwing {@link EncryptionError} on failure\n * instead of returning undefined.\n */\nexport function decryptOrThrow(sealed: SealedBytes, key: Uint8Array): Uint8Array {\n const opened = decrypt(sealed, key);\n if (!opened) {\n throw new EncryptionError(\n `Failed to decrypt sealed blob: wrong key, malformed input, or tampered ciphertext.`,\n \"decrypt-failed\"\n );\n }\n return opened;\n}\n\n/**\n * A high-level structured envelope combining encryption with a small\n * amount of metadata the receiver needs to find the right key. The mesh\n * network adapter uses this shape on the wire.\n */\nexport interface EncryptedEnvelope {\n /** Stable identifier for the document this payload belongs to. The\n * receiver looks this up in its local key store to find the right key. */\n documentId: string;\n /** Sealed blob containing the encrypted payload plus its nonce. */\n sealed: SealedBytes;\n}\n\n/**\n * Encrypt a payload and pack it into an {@link EncryptedEnvelope} along\n * with the document id.\n */\nexport function sealEnvelope(\n payload: Uint8Array,\n documentId: string,\n key: Uint8Array\n): EncryptedEnvelope {\n return {\n documentId,\n sealed: encrypt(payload, key),\n };\n}\n\n/**\n * Decrypt an {@link EncryptedEnvelope} using the given key. Throws on\n * failure for symmetry with {@link sealEnvelope}.\n */\nexport function openEnvelope(envelope: EncryptedEnvelope, key: Uint8Array): Uint8Array {\n return decryptOrThrow(envelope.sealed, key);\n}\n\n/**\n * Serialise an {@link EncryptedEnvelope} to a single binary blob.\n *\n * Wire format:\n *\n * [4 bytes BE: documentId byte length]\n * [N bytes: documentId UTF-8]\n * [remaining: sealed blob (nonce + ciphertext + tag)]\n */\nexport function encodeEncryptedEnvelope(envelope: EncryptedEnvelope): Uint8Array {\n const idBytes = new TextEncoder().encode(envelope.documentId);\n const out = new Uint8Array(4 + idBytes.length + envelope.sealed.length);\n const view = new DataView(out.buffer);\n view.setUint32(0, idBytes.length, false);\n out.set(idBytes, 4);\n out.set(envelope.sealed, 4 + idBytes.length);\n return out;\n}\n\n/**\n * Deserialise a binary envelope produced by {@link encodeEncryptedEnvelope}.\n * Throws on malformed input.\n */\nexport function decodeEncryptedEnvelope(bytes: Uint8Array): EncryptedEnvelope {\n if (bytes.length < 4) {\n throw new EncryptionError(\n `Encrypted envelope too short: ${bytes.length} bytes.`,\n \"envelope-malformed\"\n );\n }\n const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);\n const idLen = view.getUint32(0, false);\n if (bytes.length < 4 + idLen) {\n throw new EncryptionError(\n `Encrypted envelope truncated: declared id length ${idLen}, total ${bytes.length}.`,\n \"envelope-malformed\"\n );\n }\n const documentId = new TextDecoder().decode(bytes.subarray(4, 4 + idLen));\n const sealed = bytes.slice(4 + idLen);\n return { documentId, sealed };\n}\n",
|
|
6
|
+
"/**\n * @fairfox/polly/test/e2e-mesh — canonical console-noise allowlist.\n *\n * Mesh runs emit a small number of benign console lines (Repo lifecycle,\n * Automerge warmup, occasional sync diagnostics). E2e scripts watch the\n * Puppeteer console stream and fail on anything unexpected; without an\n * allowlist every run trips on the same benign noise.\n *\n * The allowlist is owned by polly because polly knows which lines its\n * own code produces. Consumers extend it with their app-specific noise.\n * Entries are tested as substrings against the rendered console message.\n */\n\n/** Match a console line that contains the given substring. */\nexport interface ConsolePattern {\n /** Optional console level filter — \"log\" | \"info\" | \"warn\" | \"error\".\n * Undefined matches any level. */\n level?: \"log\" | \"info\" | \"warn\" | \"error\";\n /** Substring or RegExp the rendered console line must satisfy. */\n match: string | RegExp;\n /** Short reason — surfaces in failure messages when the allowlist\n * evolves and an entry should be removed. */\n reason: string;\n}\n\nexport const MESH_CONSOLE_ALLOWLIST: ReadonlyArray<ConsolePattern> = [\n {\n match: \"[polly#107 H5]\",\n reason:\n \"polly#107 H5 warning fires whenever $meshState resolves against an unconfigured module instance during normal warmup; the issue tracks the longer fix.\",\n },\n {\n match: \"automerge\",\n level: \"log\",\n reason: \"Automerge logs its own version banner at log level on first import; benign.\",\n },\n {\n match: \"using deprecated parameters for `initSync()`\",\n reason:\n \"Automerge WASM bundler emits a deprecation warning on first init; upstream noise polly cannot suppress without a forked bundle.\",\n },\n {\n match: \"Failed to load resource\",\n level: \"error\",\n reason:\n \"Puppeteer logs a 404 for /favicon.ico against the in-tree consumer because the consumer does not ship one; the e2e harness only cares about app-level errors.\",\n },\n];\n\n/**\n * Return true when the console line matches any allowlist entry.\n */\nexport function isAllowedConsoleLine(\n line: { level: string; text: string },\n allowlist: ReadonlyArray<ConsolePattern> = MESH_CONSOLE_ALLOWLIST\n): boolean {\n for (const entry of allowlist) {\n if (entry.level && entry.level !== line.level) continue;\n if (typeof entry.match === \"string\") {\n if (line.text.includes(entry.match)) return true;\n } else if (entry.match.test(line.text)) {\n return true;\n }\n }\n return false;\n}\n",
|
|
7
|
+
"/**\n * @fairfox/polly/test/e2e-mesh — pre-baked keyring generation.\n *\n * E2e scripts that test the *drain* (offline-online, late-join, sync\n * recovery) need two peers who already know each other. The full pairing\n * ceremony is a separate surface tested by its own script; here we\n * pre-bake the keyrings so the script under test isn't paying the cost\n * of pairing on every run.\n *\n * The keys are real — the same generators production uses — so the\n * bootstrap path through `createMeshClient` is the same path a real user\n * takes. We just skip the UI ceremony.\n */\n\nimport { generateDocumentKey } from \"../../../../src/shared/lib/encryption\";\nimport { generateSigningKeyPair } from \"../../../../src/shared/lib/signing\";\n\nexport interface PrebakedPeer {\n peerId: string;\n /** base64-encoded identity secret key (64 bytes). */\n identitySecretKeyB64: string;\n /** base64-encoded identity public key (32 bytes). */\n identityPublicKeyB64: string;\n}\n\nexport interface PrebakedKeyringPair {\n peers: [PrebakedPeer, PrebakedPeer];\n /** base64-encoded shared document key for the default mesh key id. */\n docKeyB64: string;\n}\n\nfunction toBase64(bytes: Uint8Array): string {\n let binary = \"\";\n for (let i = 0; i < bytes.byteLength; i++) {\n binary += String.fromCharCode(bytes[i] as number);\n }\n return btoa(binary);\n}\n\n/**\n * Build two peers that already know each other and share a document key.\n * Peer ids default to \"peer-a\" / \"peer-b\" — override if a script needs\n * specific ids for log readability.\n */\nexport function prebakeKeyringPair(peerIdA = \"peer-a\", peerIdB = \"peer-b\"): PrebakedKeyringPair {\n const set = prebakeKeyringSet([peerIdA, peerIdB]);\n return {\n peers: [set.peers[0] as PrebakedPeer, set.peers[1] as PrebakedPeer],\n docKeyB64: set.docKeyB64,\n };\n}\n\nexport interface PrebakedKeyringSet {\n /** Every peer in the set, each carrying its own identity. */\n peers: PrebakedPeer[];\n /** Shared document key for the default mesh key id. */\n docKeyB64: string;\n}\n\n/**\n * Build N peers that all know each other and share a single document\n * key. Use when a script needs more than two endpoints — three-peer\n * convergence, revocation-over-wire, multi-hop.\n *\n * The result is symmetric: every peer's keyring contains the public\n * keys of every other peer. Scripts that want to test asymmetric\n * topologies (a peer that knows a subset) thin out the knownPeers map\n * per-peer when wiring the bootstrap.\n */\nexport function prebakeKeyringSet(peerIds: ReadonlyArray<string>): PrebakedKeyringSet {\n if (peerIds.length < 2) {\n throw new Error(\"prebakeKeyringSet: at least two peer ids required\");\n }\n const docKey = generateDocumentKey();\n const peers: PrebakedPeer[] = peerIds.map((peerId) => {\n const pair = generateSigningKeyPair();\n return {\n peerId,\n identitySecretKeyB64: toBase64(pair.secretKey),\n identityPublicKeyB64: toBase64(pair.publicKey),\n };\n });\n return { peers, docKeyB64: toBase64(docKey) };\n}\n\n/**\n * Build the `knownPeers` map a single peer's bootstrap needs: every\n * other peer in the set, keyed by peerId, valued by the base64 public\n * key. Scripts call this per peer when wiring `serveConsumer({\n * bootstrap: { ..., knownPeers: knownPeersFor(set, \"peer-a\") } })`.\n */\nexport function knownPeersFor(set: PrebakedKeyringSet, thisPeerId: string): Record<string, string> {\n const out: Record<string, string> = {};\n for (const peer of set.peers) {\n if (peer.peerId === thisPeerId) continue;\n out[peer.peerId] = peer.identityPublicKeyB64;\n }\n return out;\n}\n",
|
|
8
|
+
"/**\n * signing — Ed25519 signing and verification for Polly's $meshState\n * primitive (Phase 2). Wraps tweetnacl with a small Polly-flavoured API\n * so the rest of the codebase never imports tweetnacl directly.\n *\n * Every operation that flows through a $meshState transport is signed by\n * the originating peer's private key before transmission and verified by\n * every receiving peer against a known public-key set before being applied.\n * This is the Byzantine-tolerance mechanism: a peer whose private key is\n * compromised can be revoked through a further signed operation, after\n * which honest peers reject anything signed by the revoked key.\n *\n * tweetnacl uses the Ed25519 curve. Public keys and signatures are 32 and\n * 64 bytes respectively, which keeps the per-op overhead small enough that\n * signing every Automerge sync message is feasible even on mobile.\n *\n * The shape of the wrapper:\n *\n * - {@link generateSigningKeyPair} produces a new Ed25519 keypair. The\n * private key never leaves the device that generated it; the public\n * key is gossiped through the access set.\n *\n * - {@link sign} produces a 64-byte detached signature over a payload.\n *\n * - {@link verify} checks a payload against a signature and a public\n * key. Returns boolean rather than throwing so call sites can handle\n * verification failure as a normal control-flow case.\n *\n * - {@link signEnvelope} and {@link openEnvelope} package payload + sender\n * id + signature into a single binary envelope, which is what the mesh\n * network adapter actually puts on the wire.\n */\n\nimport nacl from \"tweetnacl\";\n\n/** Length in bytes of an Ed25519 public key. */\nexport const PUBLIC_KEY_BYTES = 32;\n/** Length in bytes of an Ed25519 secret (private) key. */\nexport const SECRET_KEY_BYTES = 64;\n/** Length in bytes of an Ed25519 detached signature. */\nexport const SIGNATURE_BYTES = 64;\n\n/**\n * An Ed25519 keypair. The {@link publicKey} is safe to share with peers;\n * the {@link secretKey} must never leave the device.\n */\nexport interface SigningKeyPair {\n publicKey: Uint8Array;\n secretKey: Uint8Array;\n}\n\n/**\n * A signed envelope. The wire format is the concatenation of the sender id\n * length, the sender id bytes, the signature, and the payload. Callers\n * shouldn't rely on the exact layout — use {@link signEnvelope} and\n * {@link openEnvelope} to round-trip.\n */\nexport interface SignedEnvelope {\n /** Stable sender peer identifier (UTF-8 string). The receiving side uses\n * this to look up the sender's public key in the document's access set. */\n senderId: string;\n /** The original payload bytes, untouched. */\n payload: Uint8Array;\n /** 64-byte Ed25519 signature over the payload. */\n signature: Uint8Array;\n}\n\n/** Errors thrown by the signing subsystem. */\nexport class SigningError extends Error {\n readonly code:\n | \"invalid-secret-key\"\n | \"invalid-public-key\"\n | \"invalid-signature-length\"\n | \"envelope-malformed\";\n\n constructor(message: string, code: SigningError[\"code\"]) {\n super(message);\n this.name = \"SigningError\";\n this.code = code;\n }\n}\n\n/**\n * Generate a fresh Ed25519 keypair. Calls into tweetnacl's CSPRNG.\n */\nexport function generateSigningKeyPair(): SigningKeyPair {\n const pair = nacl.sign.keyPair();\n return {\n publicKey: pair.publicKey,\n secretKey: pair.secretKey,\n };\n}\n\n/**\n * Reconstruct a keypair from an existing 64-byte secret key. Useful for\n * loading keys from persistent storage. Throws if the key is the wrong size.\n */\nexport function signingKeyPairFromSecret(secretKey: Uint8Array): SigningKeyPair {\n if (secretKey.length !== SECRET_KEY_BYTES) {\n throw new SigningError(\n `Ed25519 secret key must be ${SECRET_KEY_BYTES} bytes, got ${secretKey.length}.`,\n \"invalid-secret-key\"\n );\n }\n const pair = nacl.sign.keyPair.fromSecretKey(secretKey);\n return {\n publicKey: pair.publicKey,\n secretKey: pair.secretKey,\n };\n}\n\n/**\n * Produce a 64-byte detached signature over the given payload using the\n * supplied secret key.\n */\nexport function sign(payload: Uint8Array, secretKey: Uint8Array): Uint8Array {\n if (secretKey.length !== SECRET_KEY_BYTES) {\n throw new SigningError(\n `Ed25519 secret key must be ${SECRET_KEY_BYTES} bytes, got ${secretKey.length}.`,\n \"invalid-secret-key\"\n );\n }\n return nacl.sign.detached(payload, secretKey);\n}\n\n/**\n * Verify a detached signature against a payload and a public key. Returns\n * true if the signature is valid, false otherwise. Wrong-length keys or\n * signatures throw {@link SigningError} so callers can distinguish a bad\n * signature from a misshapen input.\n */\nexport function verify(payload: Uint8Array, signature: Uint8Array, publicKey: Uint8Array): boolean {\n if (publicKey.length !== PUBLIC_KEY_BYTES) {\n throw new SigningError(\n `Ed25519 public key must be ${PUBLIC_KEY_BYTES} bytes, got ${publicKey.length}.`,\n \"invalid-public-key\"\n );\n }\n if (signature.length !== SIGNATURE_BYTES) {\n throw new SigningError(\n `Ed25519 signature must be ${SIGNATURE_BYTES} bytes, got ${signature.length}.`,\n \"invalid-signature-length\"\n );\n }\n return nacl.sign.detached.verify(payload, signature, publicKey);\n}\n\n/**\n * Sign a payload and pack it into a {@link SignedEnvelope} along with the\n * sender id. The mesh network adapter calls this on every outgoing message\n * before handing it to the transport.\n */\nexport function signEnvelope(\n payload: Uint8Array,\n senderId: string,\n secretKey: Uint8Array\n): SignedEnvelope {\n const signature = sign(payload, secretKey);\n return { senderId, payload, signature };\n}\n\n/**\n * Verify a {@link SignedEnvelope} against the sender's known public key.\n * Returns the inner payload on success, throws on failure. The mesh\n * network adapter calls this on every incoming message before forwarding\n * the payload to the underlying Automerge sync subsystem.\n */\nexport function openEnvelope(envelope: SignedEnvelope, publicKey: Uint8Array): Uint8Array {\n const ok = verify(envelope.payload, envelope.signature, publicKey);\n if (!ok) {\n throw new SigningError(\n `Signature verification failed for envelope from ${envelope.senderId}.`,\n \"envelope-malformed\"\n );\n }\n return envelope.payload;\n}\n\n/**\n * Serialise a {@link SignedEnvelope} to a single binary blob suitable for\n * transmission over a network adapter. Wire format:\n *\n * [4 bytes BE: senderId byte length]\n * [N bytes: senderId UTF-8]\n * [64 bytes: signature]\n * [remaining: payload]\n *\n * Callers should not depend on the exact bytes — they should round-trip\n * through {@link encodeSignedEnvelope} / {@link decodeSignedEnvelope}.\n */\nexport function encodeSignedEnvelope(envelope: SignedEnvelope): Uint8Array {\n const senderBytes = new TextEncoder().encode(envelope.senderId);\n const total = 4 + senderBytes.length + SIGNATURE_BYTES + envelope.payload.length;\n const out = new Uint8Array(total);\n const view = new DataView(out.buffer);\n view.setUint32(0, senderBytes.length, false);\n out.set(senderBytes, 4);\n out.set(envelope.signature, 4 + senderBytes.length);\n out.set(envelope.payload, 4 + senderBytes.length + SIGNATURE_BYTES);\n return out;\n}\n\n/**\n * Deserialise a binary envelope produced by {@link encodeSignedEnvelope}.\n * Throws on malformed input.\n */\nexport function decodeSignedEnvelope(bytes: Uint8Array): SignedEnvelope {\n if (bytes.length < 4 + SIGNATURE_BYTES) {\n throw new SigningError(\n `Envelope too short: ${bytes.length} bytes, need at least ${4 + SIGNATURE_BYTES}.`,\n \"envelope-malformed\"\n );\n }\n const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);\n const senderLen = view.getUint32(0, false);\n if (bytes.length < 4 + senderLen + SIGNATURE_BYTES) {\n throw new SigningError(\n `Envelope truncated: declared sender length ${senderLen}, total ${bytes.length}.`,\n \"envelope-malformed\"\n );\n }\n const senderId = new TextDecoder().decode(bytes.subarray(4, 4 + senderLen));\n const signature = bytes.slice(4 + senderLen, 4 + senderLen + SIGNATURE_BYTES);\n const payload = bytes.slice(4 + senderLen + SIGNATURE_BYTES);\n return { senderId, payload, signature };\n}\n",
|
|
9
|
+
"/**\n * @fairfox/polly/test/e2e-mesh — launchPeer.\n *\n * Boots one Puppeteer-controlled Chrome instance with a fresh profile\n * directory (deleted before launch), navigates to the served consumer,\n * wires console + pageerror handlers, and waits for the consumer to\n * report `ready` status. Returns a handle the e2e script drives.\n *\n * The fresh-profile guarantee is what makes \"cold start\" honest —\n * every run begins with empty IndexedDB, empty localStorage, empty\n * service-worker registrations. A real first-install user sees the\n * same state.\n */\n\nimport { existsSync, mkdirSync, rmSync } from \"node:fs\";\nimport { tmpdir } from \"node:os\";\nimport { resolve } from \"node:path\";\nimport puppeteer, { type Browser, type Page } from \"puppeteer\";\nimport type {\n MeshDiagnostic,\n MeshDiagnosticEvent,\n} from \"../../../../src/shared/lib/mesh-diagnostics\";\nimport {\n type ConsolePattern,\n isAllowedConsoleLine,\n MESH_CONSOLE_ALLOWLIST,\n} from \"./console-allowlist\";\nimport { MeshAssertionError } from \"./mesh-assertions\";\n\nexport interface CapturedConsoleLine {\n level: string;\n text: string;\n /** True when the line matched the supplied allowlist; allowed lines do\n * not contribute to assertNoUnexpectedConsole failures. */\n allowed: boolean;\n}\n\nexport interface LaunchedPeer {\n /** The peerId the consumer was booted with. */\n readonly peerId: string;\n /** The Puppeteer Page handle. Scripts drive this directly. */\n readonly page: Page;\n /** Live capture buffer of console lines seen on this peer. */\n readonly console: ReadonlyArray<CapturedConsoleLine>;\n /** Live capture of page-level errors. */\n readonly pageErrors: ReadonlyArray<string>;\n /** Throws if any captured console line was not allowed. */\n assertNoUnexpectedConsole: () => void;\n /**\n * Read every mesh-diagnostic event the consumer has captured so far.\n * Each call snapshots the live browser-side buffer; the array is\n * detached from the page after read.\n */\n collectDiagnostics: () => Promise<MeshDiagnosticEvent[]>;\n /**\n * Read the captured diagnostics and assert no unexpected silent\n * drops fired. Pass `allow` with the drop-kinds the scenario\n * legitimately expects; anything else fails.\n */\n assertNoSilentDrops: (allow?: ReadonlyArray<MeshDiagnostic[\"kind\"]>) => Promise<void>;\n /** Close the page, browser, and profile dir. Idempotent. */\n close: () => Promise<void>;\n}\n\nexport interface LaunchPeerOptions {\n /** Peer id used in the consumer's display + key wiring. */\n peerId: string;\n /** http://127.0.0.1:<port>/ from serveConsumer. */\n consumerUrl: string;\n /** When true, run Chrome headfully so the developer can watch. Defaults\n * to `process.env.HEADLESS !== \"false\"`. */\n headless?: boolean;\n /** Override the console allowlist; defaults to MESH_CONSOLE_ALLOWLIST. */\n consoleAllowlist?: ReadonlyArray<ConsolePattern>;\n /** Cap how long to wait for the consumer to report status=\"ready\"\n * before throwing. Defaults to 15s. */\n readyTimeoutMs?: number;\n /** Override the profile-dir parent. Defaults to os.tmpdir() / polly-e2e. */\n profileParent?: string;\n}\n\nconst READY_POLL_MS = 100;\n\nfunction profileDir(parent: string, peerId: string): string {\n const safePeerId = peerId.replace(/[^a-zA-Z0-9_-]/g, \"_\");\n return resolve(parent, `polly-e2e-${safePeerId}-${Date.now()}`);\n}\n\n/**\n * Launch one peer and wait until the consumer reports it is connected\n * and rendering. Throws on console-error or pageerror during boot.\n */\nexport async function launchPeer(options: LaunchPeerOptions): Promise<LaunchedPeer> {\n const {\n peerId,\n consumerUrl,\n headless = process.env[\"HEADLESS\"] !== \"false\",\n consoleAllowlist = MESH_CONSOLE_ALLOWLIST,\n readyTimeoutMs = 15_000,\n profileParent = resolve(tmpdir(), \"polly-e2e\"),\n } = options;\n\n if (!existsSync(profileParent)) mkdirSync(profileParent, { recursive: true });\n const userDataDir = profileDir(profileParent, peerId);\n if (existsSync(userDataDir)) {\n rmSync(userDataDir, { recursive: true, force: true });\n }\n\n const browser: Browser = await puppeteer.launch({\n headless,\n userDataDir,\n args: [\"--no-sandbox\", \"--disable-setuid-sandbox\"],\n });\n const page = await browser.newPage();\n\n const consoleLines: CapturedConsoleLine[] = [];\n const pageErrors: string[] = [];\n\n page.on(\"console\", (msg) => {\n const level = msg.type();\n const text = msg.text();\n const allowed = isAllowedConsoleLine({ level, text }, consoleAllowlist);\n consoleLines.push({ level, text, allowed });\n });\n page.on(\"pageerror\", (err) => {\n pageErrors.push(err instanceof Error ? err.message : String(err));\n });\n\n await page.goto(consumerUrl, { waitUntil: \"domcontentloaded\" });\n\n const deadline = Date.now() + readyTimeoutMs;\n let ready = false;\n let lastStatus = \"\";\n while (Date.now() < deadline) {\n lastStatus = await page.evaluate(\n () => document.querySelector(\"[data-e2e='status']\")?.textContent ?? \"\"\n );\n if (lastStatus === \"ready\") {\n ready = true;\n break;\n }\n if (lastStatus.startsWith(\"error\") || lastStatus.startsWith(\"bootstrap-failed\")) {\n await browser.close();\n rmSync(userDataDir, { recursive: true, force: true });\n throw new Error(`launchPeer(${peerId}): consumer reported \"${lastStatus}\"`);\n }\n await new Promise((r) => setTimeout(r, READY_POLL_MS));\n }\n\n if (!ready) {\n await browser.close();\n rmSync(userDataDir, { recursive: true, force: true });\n throw new Error(\n `launchPeer(${peerId}): consumer did not reach \"ready\" within ${readyTimeoutMs}ms (last status: \"${lastStatus}\")`\n );\n }\n\n function assertNoUnexpectedConsole(): void {\n const bad = consoleLines.filter(\n (line) =>\n !line.allowed &&\n (line.level === \"error\" || line.level === \"warn\" || line.level === \"warning\")\n );\n if (bad.length > 0) {\n const summary = bad.map((l) => ` [${l.level}] ${l.text}`).join(\"\\n\");\n throw new Error(`launchPeer(${peerId}): unexpected console output:\\n${summary}`);\n }\n if (pageErrors.length > 0) {\n throw new Error(\n `launchPeer(${peerId}): page errors:\\n${pageErrors.map((e) => ` ${e}`).join(\"\\n\")}`\n );\n }\n }\n\n async function collectDiagnostics(): Promise<MeshDiagnosticEvent[]> {\n const events = await page.evaluate(() => {\n const w = window as unknown as { __pollyE2eDiagnostics?: MeshDiagnosticEvent[] };\n return w.__pollyE2eDiagnostics ? [...w.__pollyE2eDiagnostics] : [];\n });\n return events;\n }\n\n async function assertNoSilentDrops(\n allow: ReadonlyArray<MeshDiagnostic[\"kind\"]> = []\n ): Promise<void> {\n const allowed = new Set(allow);\n const events = await collectDiagnostics();\n const unexpected = events.filter(\n (event) => event.kind.startsWith(\"drop:\") && !allowed.has(event.kind)\n );\n if (unexpected.length === 0) return;\n const summary = unexpected\n .map((event) => {\n const { kind, timestamp: _ts, ...rest } = event;\n return ` ${kind} ${JSON.stringify(rest)}`;\n })\n .join(\"\\n\");\n throw new MeshAssertionError(\n `launchPeer(${peerId}): unexpected silent-drop diagnostics fired during the e2e run.\\n${summary}\\n` +\n `If a drop kind is legitimately expected, pass it to peer.assertNoSilentDrops([...]).`,\n unexpected\n );\n }\n\n let closed = false;\n return {\n peerId,\n page,\n console: consoleLines,\n pageErrors,\n assertNoUnexpectedConsole,\n collectDiagnostics,\n assertNoSilentDrops,\n close: async () => {\n if (closed) return;\n closed = true;\n try {\n await page.close();\n } catch {\n // page may already be detached if browser closed\n }\n try {\n await browser.close();\n } catch {\n // best effort\n }\n try {\n rmSync(userDataDir, { recursive: true, force: true });\n } catch {\n // best effort\n }\n },\n };\n}\n",
|
|
10
|
+
"var endianness=function(){return\"LE\"},hostname=function(){if(typeof location<\"u\")return location.hostname;else return\"\"},loadavg=function(){return[]},uptime=function(){return 0},freemem=function(){return Number.MAX_VALUE},totalmem=function(){return Number.MAX_VALUE},cpus=function(){return[]},type=function(){return\"Browser\"},release=function(){if(typeof navigator<\"u\")return navigator.appVersion;return\"\"},getNetworkInterfaces=function(){return{}},networkInterfaces=getNetworkInterfaces,arch=function(){return\"javascript\"},platform=function(){return\"browser\"},tmpdir=function(){return\"/tmp\"},tmpDir=tmpdir,EOL=`\n`,homedir=function(){return\"/\"};export{uptime,type,totalmem,tmpdir,tmpDir,release,platform,networkInterfaces,loadavg,hostname,homedir,getNetworkInterfaces,freemem,endianness,cpus,arch,EOL};",
|
|
11
|
+
"function assertPath(path){if(typeof path!==\"string\")throw TypeError(\"Path must be a string. Received \"+JSON.stringify(path))}function normalizeStringPosix(path,allowAboveRoot){var res=\"\",lastSegmentLength=0,lastSlash=-1,dots=0,code;for(var i=0;i<=path.length;++i){if(i<path.length)code=path.charCodeAt(i);else if(code===47)break;else code=47;if(code===47){if(lastSlash===i-1||dots===1);else if(lastSlash!==i-1&&dots===2){if(res.length<2||lastSegmentLength!==2||res.charCodeAt(res.length-1)!==46||res.charCodeAt(res.length-2)!==46){if(res.length>2){var lastSlashIndex=res.lastIndexOf(\"/\");if(lastSlashIndex!==res.length-1){if(lastSlashIndex===-1)res=\"\",lastSegmentLength=0;else res=res.slice(0,lastSlashIndex),lastSegmentLength=res.length-1-res.lastIndexOf(\"/\");lastSlash=i,dots=0;continue}}else if(res.length===2||res.length===1){res=\"\",lastSegmentLength=0,lastSlash=i,dots=0;continue}}if(allowAboveRoot){if(res.length>0)res+=\"/..\";else res=\"..\";lastSegmentLength=2}}else{if(res.length>0)res+=\"/\"+path.slice(lastSlash+1,i);else res=path.slice(lastSlash+1,i);lastSegmentLength=i-lastSlash-1}lastSlash=i,dots=0}else if(code===46&&dots!==-1)++dots;else dots=-1}return res}function _format(sep,pathObject){var dir=pathObject.dir||pathObject.root,base=pathObject.base||(pathObject.name||\"\")+(pathObject.ext||\"\");if(!dir)return base;if(dir===pathObject.root)return dir+base;return dir+sep+base}function resolve(){var resolvedPath=\"\",resolvedAbsolute=!1,cwd;for(var i=arguments.length-1;i>=-1&&!resolvedAbsolute;i--){var path;if(i>=0)path=arguments[i];else{if(cwd===void 0)cwd=process.cwd();path=cwd}if(assertPath(path),path.length===0)continue;resolvedPath=path+\"/\"+resolvedPath,resolvedAbsolute=path.charCodeAt(0)===47}if(resolvedPath=normalizeStringPosix(resolvedPath,!resolvedAbsolute),resolvedAbsolute)if(resolvedPath.length>0)return\"/\"+resolvedPath;else return\"/\";else if(resolvedPath.length>0)return resolvedPath;else return\".\"}function normalize(path){if(assertPath(path),path.length===0)return\".\";var isAbsolute=path.charCodeAt(0)===47,trailingSeparator=path.charCodeAt(path.length-1)===47;if(path=normalizeStringPosix(path,!isAbsolute),path.length===0&&!isAbsolute)path=\".\";if(path.length>0&&trailingSeparator)path+=\"/\";if(isAbsolute)return\"/\"+path;return path}function isAbsolute(path){return assertPath(path),path.length>0&&path.charCodeAt(0)===47}function join(){if(arguments.length===0)return\".\";var joined;for(var i=0;i<arguments.length;++i){var arg=arguments[i];if(assertPath(arg),arg.length>0)if(joined===void 0)joined=arg;else joined+=\"/\"+arg}if(joined===void 0)return\".\";return normalize(joined)}function relative(from,to){if(assertPath(from),assertPath(to),from===to)return\"\";if(from=resolve(from),to=resolve(to),from===to)return\"\";var fromStart=1;for(;fromStart<from.length;++fromStart)if(from.charCodeAt(fromStart)!==47)break;var fromEnd=from.length,fromLen=fromEnd-fromStart,toStart=1;for(;toStart<to.length;++toStart)if(to.charCodeAt(toStart)!==47)break;var toEnd=to.length,toLen=toEnd-toStart,length=fromLen<toLen?fromLen:toLen,lastCommonSep=-1,i=0;for(;i<=length;++i){if(i===length){if(toLen>length){if(to.charCodeAt(toStart+i)===47)return to.slice(toStart+i+1);else if(i===0)return to.slice(toStart+i)}else if(fromLen>length){if(from.charCodeAt(fromStart+i)===47)lastCommonSep=i;else if(i===0)lastCommonSep=0}break}var fromCode=from.charCodeAt(fromStart+i),toCode=to.charCodeAt(toStart+i);if(fromCode!==toCode)break;else if(fromCode===47)lastCommonSep=i}var out=\"\";for(i=fromStart+lastCommonSep+1;i<=fromEnd;++i)if(i===fromEnd||from.charCodeAt(i)===47)if(out.length===0)out+=\"..\";else out+=\"/..\";if(out.length>0)return out+to.slice(toStart+lastCommonSep);else{if(toStart+=lastCommonSep,to.charCodeAt(toStart)===47)++toStart;return to.slice(toStart)}}function _makeLong(path){return path}function dirname(path){if(assertPath(path),path.length===0)return\".\";var code=path.charCodeAt(0),hasRoot=code===47,end=-1,matchedSlash=!0;for(var i=path.length-1;i>=1;--i)if(code=path.charCodeAt(i),code===47){if(!matchedSlash){end=i;break}}else matchedSlash=!1;if(end===-1)return hasRoot?\"/\":\".\";if(hasRoot&&end===1)return\"//\";return path.slice(0,end)}function basename(path,ext){if(ext!==void 0&&typeof ext!==\"string\")throw TypeError('\"ext\" argument must be a string');assertPath(path);var start=0,end=-1,matchedSlash=!0,i;if(ext!==void 0&&ext.length>0&&ext.length<=path.length){if(ext.length===path.length&&ext===path)return\"\";var extIdx=ext.length-1,firstNonSlashEnd=-1;for(i=path.length-1;i>=0;--i){var code=path.charCodeAt(i);if(code===47){if(!matchedSlash){start=i+1;break}}else{if(firstNonSlashEnd===-1)matchedSlash=!1,firstNonSlashEnd=i+1;if(extIdx>=0)if(code===ext.charCodeAt(extIdx)){if(--extIdx===-1)end=i}else extIdx=-1,end=firstNonSlashEnd}}if(start===end)end=firstNonSlashEnd;else if(end===-1)end=path.length;return path.slice(start,end)}else{for(i=path.length-1;i>=0;--i)if(path.charCodeAt(i)===47){if(!matchedSlash){start=i+1;break}}else if(end===-1)matchedSlash=!1,end=i+1;if(end===-1)return\"\";return path.slice(start,end)}}function extname(path){assertPath(path);var startDot=-1,startPart=0,end=-1,matchedSlash=!0,preDotState=0;for(var i=path.length-1;i>=0;--i){var code=path.charCodeAt(i);if(code===47){if(!matchedSlash){startPart=i+1;break}continue}if(end===-1)matchedSlash=!1,end=i+1;if(code===46){if(startDot===-1)startDot=i;else if(preDotState!==1)preDotState=1}else if(startDot!==-1)preDotState=-1}if(startDot===-1||end===-1||preDotState===0||preDotState===1&&startDot===end-1&&startDot===startPart+1)return\"\";return path.slice(startDot,end)}function format(pathObject){if(pathObject===null||typeof pathObject!==\"object\")throw TypeError('The \"pathObject\" argument must be of type Object. Received type '+typeof pathObject);return _format(\"/\",pathObject)}function parse(path){assertPath(path);var ret={root:\"\",dir:\"\",base:\"\",ext:\"\",name:\"\"};if(path.length===0)return ret;var code=path.charCodeAt(0),isAbsolute2=code===47,start;if(isAbsolute2)ret.root=\"/\",start=1;else start=0;var startDot=-1,startPart=0,end=-1,matchedSlash=!0,i=path.length-1,preDotState=0;for(;i>=start;--i){if(code=path.charCodeAt(i),code===47){if(!matchedSlash){startPart=i+1;break}continue}if(end===-1)matchedSlash=!1,end=i+1;if(code===46){if(startDot===-1)startDot=i;else if(preDotState!==1)preDotState=1}else if(startDot!==-1)preDotState=-1}if(startDot===-1||end===-1||preDotState===0||preDotState===1&&startDot===end-1&&startDot===startPart+1){if(end!==-1)if(startPart===0&&isAbsolute2)ret.base=ret.name=path.slice(1,end);else ret.base=ret.name=path.slice(startPart,end)}else{if(startPart===0&&isAbsolute2)ret.name=path.slice(1,startDot),ret.base=path.slice(1,end);else ret.name=path.slice(startPart,startDot),ret.base=path.slice(startPart,end);ret.ext=path.slice(startDot,end)}if(startPart>0)ret.dir=path.slice(0,startPart-1);else if(isAbsolute2)ret.dir=\"/\";return ret}var sep=\"/\",delimiter=\":\",posix=((p)=>(p.posix=p,p))({resolve,normalize,isAbsolute,join,relative,_makeLong,dirname,basename,extname,format,parse,sep,delimiter,win32:null,posix:null});var path_default=posix;export{sep,resolve,relative,posix,parse,normalize,join,isAbsolute,format,extname,dirname,delimiter,path_default as default,basename,_makeLong};",
|
|
12
|
+
"/**\n * mesh-diagnostics — typed event stream for observable mesh failures and\n * state transitions.\n *\n * The mesh network adapter's incoming path has seven branches that drop a\n * message and return undefined: a malformed signed envelope, a revoked\n * peer, an unknown peer, a bad signature, a malformed encrypted envelope,\n * a missing document key, and a bad decryption. Each branch is correct —\n * the adapter must not surface tampered or unidentifiable bytes to the\n * Repo — but the drop is invisible to anything observing the application.\n * The classic symptom is \"the other peer typed something and nothing\n * arrived\" with no error anywhere.\n *\n * This module exposes a typed emit-and-subscribe stream that the adapter\n * (and the pairing and revocation paths) write to. Tests subscribe to\n * assert that exactly the expected diagnostics fired and no others;\n * production code can attach an observability sink that turns the stream\n * into telemetry or a user-visible diagnostic surface.\n *\n * The stream is module-level. Listeners are deduplicated by reference;\n * subscribe returns an unsubscribe function. Listener exceptions are\n * caught and dropped so a buggy observer cannot tear the network path.\n */\n\n/** All diagnostic event kinds, discriminated by the `kind` field. */\nexport type MeshDiagnostic =\n // Incoming-message silent drops in MeshNetworkAdapter.tryUnwrap.\n | { kind: \"drop:malformed-signed-envelope\"; reason?: string }\n | { kind: \"drop:revoked-peer\"; senderId: string }\n | { kind: \"drop:unknown-peer\"; senderId: string }\n | { kind: \"drop:bad-signature\"; senderId: string; reason?: string }\n | {\n kind: \"drop:malformed-encrypted-envelope\";\n senderId: string;\n reason?: string;\n }\n | {\n kind: \"drop:missing-doc-key\";\n senderId: string;\n documentId: string;\n }\n | {\n kind: \"drop:bad-decryption\";\n senderId: string;\n documentId: string;\n reason?: string;\n }\n // Pairing-flow transitions.\n | { kind: \"pair:invite-issued\"; peerId: string }\n | { kind: \"pair:invite-accepted\"; peerId: string; issuerId: string }\n // Revocation-flow transitions.\n | { kind: \"revoke:issued\"; revokedPeerId: string; issuerId: string }\n | { kind: \"revoke:applied\"; revokedPeerId: string };\n\n/** A diagnostic event with the wall-clock timestamp the emitter stamped. */\nexport type MeshDiagnosticEvent = MeshDiagnostic & { timestamp: number };\n\n/** Callback shape for subscribers. */\nexport type MeshDiagnosticListener = (event: MeshDiagnosticEvent) => void;\n\nconst listeners = new Set<MeshDiagnosticListener>();\n\n/**\n * Emit a diagnostic to every active subscriber. Synchronous. Listener\n * exceptions are swallowed.\n */\nexport function emitMeshDiagnostic(diagnostic: MeshDiagnostic): void {\n const event: MeshDiagnosticEvent = { ...diagnostic, timestamp: Date.now() };\n for (const listener of listeners) {\n try {\n listener(event);\n } catch {\n // A buggy listener must not break the network path. The diagnostic\n // is intentionally a side-channel; if telemetry collapses, the mesh\n // keeps moving messages.\n }\n }\n}\n\n/**\n * Subscribe a listener. Returns an unsubscribe function. Idempotent on\n * the same listener reference — subscribing the same function twice\n * registers it once.\n */\nexport function subscribeToMeshDiagnostics(listener: MeshDiagnosticListener): () => void {\n listeners.add(listener);\n return () => {\n listeners.delete(listener);\n };\n}\n\n/**\n * Convenience for tests and trace recorders: subscribe, collect every\n * event into an array, return the array and a stop function that\n * unsubscribes. The returned array is the live capture buffer — reads\n * see new events the moment they fire.\n */\nexport function recordMeshDiagnostics(): {\n events: ReadonlyArray<MeshDiagnosticEvent>;\n stop: () => void;\n} {\n const captured: MeshDiagnosticEvent[] = [];\n const stop = subscribeToMeshDiagnostics((event) => {\n captured.push(event);\n });\n return { events: captured, stop };\n}\n\n/**\n * Test-only: drop every subscriber. Use in `afterEach` to guarantee\n * isolation between tests when a stop function was missed. Not exported\n * from the mesh subpath in production builds — tests reach in directly.\n */\nexport function clearMeshDiagnosticListeners(): void {\n listeners.clear();\n}\n",
|
|
13
|
+
"/**\n * @fairfox/polly/test/e2e-mesh — assertions over the mesh-diagnostics\n * stream.\n *\n * Every e2e script that drives the mesh subscribes to the diagnostic\n * stream before it starts and runs the assertions in `assertNoSilentDrops`\n * before exiting. The default is \"no unexpected diagnostics fired\"; a\n * script that legitimately exercises a drop branch (e.g. a revocation\n * test) supplies an allowlist of expected kinds so the gate stays loud\n * about everything else.\n *\n * This is the test-kit half of the diagnostic-obligation move: the lint\n * half (forbidding new silent-drop branches that do not emit) lands\n * separately as `scripts/check-mesh-diagnostics.ts`.\n */\n\nimport {\n type MeshDiagnostic,\n type MeshDiagnosticEvent,\n recordMeshDiagnostics,\n} from \"../../../../src/shared/lib/mesh-diagnostics\";\n\nexport interface MeshAssertionFailure extends Error {\n readonly kind: \"mesh-assertion-failure\";\n readonly unexpected: ReadonlyArray<MeshDiagnosticEvent>;\n}\n\nexport class MeshAssertionError extends Error implements MeshAssertionFailure {\n readonly kind = \"mesh-assertion-failure\";\n readonly unexpected: ReadonlyArray<MeshDiagnosticEvent>;\n constructor(message: string, unexpected: ReadonlyArray<MeshDiagnosticEvent>) {\n super(message);\n this.name = \"MeshAssertionError\";\n this.unexpected = unexpected;\n }\n}\n\nexport interface DiagnosticRecorder {\n /** Live capture buffer — reads see new events the moment they fire. */\n events: ReadonlyArray<MeshDiagnosticEvent>;\n /** Stop subscribing. Idempotent. */\n stop: () => void;\n /**\n * Run the no-silent-drops assertion against the captured buffer.\n * Throws {@link MeshAssertionError} if any unexpected diagnostic fired.\n * Pass `allow` with the kinds the script legitimately expected — the\n * gate fails on anything else.\n */\n assertNoSilentDrops: (allow?: ReadonlyArray<MeshDiagnostic[\"kind\"]>) => void;\n}\n\n/**\n * Begin capturing diagnostics for the duration of a script.\n *\n * @example\n * ```typescript\n * const diag = startDiagnosticRecorder();\n * try {\n * await driveTheScenario();\n * diag.assertNoSilentDrops();\n * } finally {\n * diag.stop();\n * }\n * ```\n */\nexport function startDiagnosticRecorder(): DiagnosticRecorder {\n const { events, stop } = recordMeshDiagnostics();\n\n function assertNoSilentDrops(allow: ReadonlyArray<MeshDiagnostic[\"kind\"]> = []): void {\n const allowed = new Set(allow);\n const unexpected = events.filter(\n (event) => event.kind.startsWith(\"drop:\") && !allowed.has(event.kind)\n );\n if (unexpected.length === 0) return;\n const summary = unexpected\n .map((event) => ` ${event.kind} ${JSON.stringify(eventDetails(event))}`)\n .join(\"\\n\");\n throw new MeshAssertionError(\n `Unexpected silent-drop diagnostics fired during the e2e run.\\n${summary}\\n` +\n `If a drop kind is legitimately expected for this scenario, pass it ` +\n `to assertNoSilentDrops({ allow: [...] }).`,\n unexpected\n );\n }\n\n return { events, stop, assertNoSilentDrops };\n}\n\nfunction eventDetails(event: MeshDiagnosticEvent): Record<string, unknown> {\n // Strip kind + timestamp; keep the rest as context.\n const { kind: _kind, timestamp: _ts, ...rest } = event;\n return rest;\n}\n",
|
|
14
|
+
"/**\n * @fairfox/polly/test/e2e-mesh — bundle and serve the e2e consumer.\n *\n * Bun.build compiles `examples/e2e-consumer/main.ts` for the browser\n * target with the Automerge base64 fix (same plugin the existing browser\n * runner uses). Bun.serve then hands the HTML on \"/\" with a bootstrap\n * shim injected, and the JS on \"/main.js\". Puppeteer points at the\n * returned URL.\n *\n * The kit owns this so every e2e script gets the same boot path: build\n * the in-tree consumer, serve from a fresh ephemeral port, inject the\n * peer-specific bootstrap. No script should call Bun.build directly —\n * keeping it in one place means the Automerge plugin and the bootstrap\n * shape stay coherent across the suite.\n */\n\nimport { readFileSync } from \"node:fs\";\nimport { resolve } from \"node:path\";\nimport type { BunPlugin } from \"bun\";\n\n// __dirname here is packages/polly/tools/test/src/e2e-mesh. The consumer\n// lives at packages/polly/examples/e2e-consumer so it inherits polly's\n// node_modules at bundle time; placing it at the monorepo root would\n// require it to declare its own workspace entry plus polly's runtime\n// dependencies, which is more coupling than the test deserves.\nconst pollyRoot = resolve(__dirname, \"../../../..\");\nconst consumerEntry = resolve(pollyRoot, \"examples/e2e-consumer/main.ts\");\nconst consumerHtml = resolve(pollyRoot, \"examples/e2e-consumer/index.html\");\nconst automergeBase64Path = resolve(\n pollyRoot,\n \"node_modules/@automerge/automerge/dist/mjs/entrypoints/fullfat_base64.js\"\n);\n\nconst automergeBase64Plugin: BunPlugin = {\n name: \"automerge-base64\",\n setup(build) {\n build.onResolve({ filter: /^@automerge\\/automerge(\\/slim)?$/ }, () => ({\n path: automergeBase64Path,\n }));\n },\n};\n\nexport interface ServeConsumerOptions {\n /** The bootstrap object that the page reads from window.__pollyE2eBootstrap. */\n bootstrap: Record<string, unknown>;\n}\n\nexport interface ServeConsumerResult {\n /** http://127.0.0.1:<port>/ — pass to puppeteer page.goto. */\n url: string;\n /** Stop the server. Idempotent. */\n close: () => Promise<void>;\n}\n\n/**\n * Bundle the consumer and serve it on an ephemeral port. The HTML's\n * `<script type=\"module\" src=\"./main.js\">` resolves to the freshly built\n * bundle; the bootstrap shim is inserted right before it so the global\n * is set by the time `main.ts` reads it.\n */\nexport async function serveConsumer(options: ServeConsumerOptions): Promise<ServeConsumerResult> {\n const buildResult = await Bun.build({\n entrypoints: [consumerEntry],\n target: \"browser\",\n format: \"esm\",\n minify: false,\n sourcemap: \"inline\",\n plugins: [automergeBase64Plugin],\n });\n if (!buildResult.success) {\n const logs = buildResult.logs.map((log) => String(log)).join(\"\\n\");\n throw new Error(`serveConsumer: build failed:\\n${logs}`);\n }\n const jsText = await buildResult.outputs[0]?.text();\n if (!jsText) {\n throw new Error(\"serveConsumer: build produced no output\");\n }\n\n const rawHtml = readFileSync(consumerHtml, \"utf-8\");\n const bootstrapJson = JSON.stringify(options.bootstrap);\n const bootstrapShim = `<script>window.__pollyE2eBootstrap = ${bootstrapJson};</script>`;\n const html = rawHtml.replace(\n /<script type=\"module\" src=\"\\.\\/main\\.js\"><\\/script>/,\n `${bootstrapShim}\\n <script type=\"module\" src=\"./main.js\"></script>`\n );\n\n const server = Bun.serve({\n port: 0,\n fetch(req) {\n const url = new URL(req.url);\n if (url.pathname === \"/\" || url.pathname === \"/index.html\") {\n return new Response(html, { headers: { \"Content-Type\": \"text/html\" } });\n }\n if (url.pathname === \"/main.js\") {\n return new Response(jsText, {\n headers: { \"Content-Type\": \"application/javascript\" },\n });\n }\n return new Response(\"not found\", { status: 404 });\n },\n });\n\n return {\n url: `http://127.0.0.1:${server.port}/`,\n close: async () => {\n server.stop();\n },\n };\n}\n",
|
|
15
|
+
"/**\n * @fairfox/polly/test/e2e-mesh — waitForConvergence.\n *\n * Polls a consumer-supplied predicate against every launched peer until\n * the predicate returns true on all of them, or the timeout expires.\n * The predicate runs in the *node* side (the script) and is handed a\n * snapshot reader function that reads from the peer's page DOM.\n *\n * Typical use: assert every peer's `[data-e2e='items']` UL contains the\n * expected set of values.\n */\n\nimport type { LaunchedPeer } from \"./launch-peer\";\n\nexport interface PeerSnapshot {\n peerId: string;\n /** Items currently rendered in the consumer's [data-e2e='items'] list. */\n items: string[];\n /** Connected peer count reported by the consumer. */\n peerCount: number;\n /** Status text the consumer currently displays. */\n status: string;\n}\n\nexport type ConvergencePredicate = (snapshot: PeerSnapshot) => boolean;\n\nexport interface WaitForConvergenceOptions {\n /** Cap wait time before throwing. Defaults to 20s. */\n timeoutMs?: number;\n /** Poll interval. Defaults to 200ms. */\n pollMs?: number;\n}\n\nasync function readPeerSnapshot(peer: LaunchedPeer): Promise<PeerSnapshot> {\n return peer.page\n .evaluate(() => {\n const itemEls = Array.from(document.querySelectorAll(\"[data-e2e-item]\"));\n const items = itemEls.map((el) => el.textContent ?? \"\");\n const peerCountText = document.querySelector(\"[data-e2e='peer-count']\")?.textContent ?? \"0\";\n const status = document.querySelector(\"[data-e2e='status']\")?.textContent ?? \"\";\n return { items, peerCount: Number(peerCountText) || 0, status };\n })\n .then((data) => ({ peerId: peer.peerId, ...data }));\n}\n\n/**\n * Block until the predicate is true for every peer, or throw.\n */\nexport async function waitForConvergence(\n peers: ReadonlyArray<LaunchedPeer>,\n predicate: ConvergencePredicate,\n options: WaitForConvergenceOptions = {}\n): Promise<void> {\n const { timeoutMs = 20_000, pollMs = 200 } = options;\n const deadline = Date.now() + timeoutMs;\n let lastSnapshots: PeerSnapshot[] = [];\n\n while (Date.now() < deadline) {\n const snapshots = await Promise.all(peers.map(readPeerSnapshot));\n lastSnapshots = snapshots;\n if (snapshots.every(predicate)) return;\n await new Promise((r) => setTimeout(r, pollMs));\n }\n\n const summary = lastSnapshots\n .map(\n (s) =>\n ` ${s.peerId}: status=\"${s.status}\" peerCount=${s.peerCount} items=${JSON.stringify(s.items)}`\n )\n .join(\"\\n\");\n throw new Error(\n `waitForConvergence: predicate did not hold for every peer within ${timeoutMs}ms.\\n${summary}`\n );\n}\n\n/**\n * Convenience: wait until every peer reports it sees at least N connected\n * peers. Used right after launching to confirm the WebRTC handshake\n * completed before driving any user-facing flow.\n */\nexport async function waitForMeshConnected(\n peers: ReadonlyArray<LaunchedPeer>,\n options: WaitForConvergenceOptions = {}\n): Promise<void> {\n const minPeers = peers.length - 1;\n await waitForConvergence(peers, (snapshot) => snapshot.peerCount >= minPeers, options);\n}\n",
|
|
16
|
+
"/**\n * @fairfox/polly/test/e2e-mesh — withRelay helper.\n *\n * Boots a polly signalling relay on an ephemeral port for e2e scripts.\n * The relay is the same Elysia plugin polly ships at runtime, so a script\n * driving the relay through `withRelay` exercises the production protocol\n * with no shimming. Two modes:\n *\n * - \"embedded\" (default): start an Elysia app on a random port and return\n * its URL plus a close callback. The hermetic mode the suite runs in\n * by default; depending on a staging relay would couple CI reliability\n * to a network service we do not control.\n *\n * - \"env\": read SIGNALING_URL from the environment and return that\n * alongside a no-op close. The override the nightly cron uses to smoke\n * the production protocol against the live relay.\n *\n * Lift source: tests/integration/signaling-server.test.ts is the existing\n * recipe; this helper centralises it so every e2e script wires the relay\n * the same way.\n */\n\nimport { Elysia } from \"elysia\";\nimport { signalingServer } from \"../../../../src/elysia/signaling-server-plugin\";\n\nexport interface WithRelayResult {\n /** WebSocket URL of the signalling endpoint, ready to be passed to\n * `createMeshClient({ signaling: { url, peerId } })`. */\n url: string;\n /** Stop the relay. Idempotent; safe to call after a failed boot. */\n close: () => Promise<void>;\n}\n\nexport interface WithRelayOptions {\n /**\n * \"embedded\" boots a fresh relay on an ephemeral port and returns its\n * URL. \"env\" reads SIGNALING_URL from the environment and returns it\n * without booting anything. Defaults to \"embedded\".\n */\n mode?: \"embedded\" | \"env\";\n /** Path under which the signalling endpoint is mounted. Defaults to\n * \"/polly/signaling\" — the same default the SPA wiring uses. */\n path?: string;\n}\n\nfunction pickPort(): number {\n // Same window the integration suite uses. Random port collision is\n // possible but rare; e2e scripts run sequentially by default so the\n // failure surface is small.\n return 30000 + Math.floor(Math.random() * 10000);\n}\n\n/**\n * Start a signalling relay for the duration of an e2e script.\n *\n * @example\n * ```typescript\n * const relay = await withRelay();\n * try {\n * // boot peers pointing signaling.url at relay.url ...\n * } finally {\n * await relay.close();\n * }\n * ```\n */\nexport async function withRelay(options: WithRelayOptions = {}): Promise<WithRelayResult> {\n const mode = options.mode ?? \"embedded\";\n const path = options.path ?? \"/polly/signaling\";\n\n if (mode === \"env\") {\n const url = process.env[\"SIGNALING_URL\"];\n if (!url) {\n throw new Error(\n \"withRelay({ mode: 'env' }) requires SIGNALING_URL to be set in the environment.\"\n );\n }\n return {\n url,\n close: async () => {\n // env mode doesn't own a server, so close is a no-op.\n },\n };\n }\n\n const port = pickPort();\n const app = new Elysia().use(signalingServer({ path })).listen(port);\n const url = `ws://127.0.0.1:${port}${path}`;\n\n return {\n url,\n close: async () => {\n // Elysia exposes the underlying Bun server here. The .stop(true)\n // force-closes any open connections so a hung peer cannot keep the\n // server alive past the script's intended lifetime.\n (\n app as unknown as {\n server?: { stop?: (force?: boolean) => void };\n }\n ).server?.stop?.(true);\n },\n };\n}\n",
|
|
17
|
+
"// @ts-nocheck - Optional peer dependencies (elysia, @elysiajs/eden)\n/**\n * signalingServer — Phase 2 Elysia plugin that exposes a stateless\n * WebSocket route for SDP/ICE relay between $meshState peers.\n *\n * The mesh transport is a star-of-data-channels: peers establish direct\n * WebRTC connections to each other and exchange document operations\n * peer-to-peer once those channels are open. WebRTC connection setup\n * needs an out-of-band channel for SDP offer/answer and ICE candidate\n * exchange, and that channel is what this plugin provides. The plugin\n * does not own any document state, does not hold any encryption keys,\n * and never inspects the contents of the messages it relays. It is a\n * pure pub-sub by peer id.\n *\n * Once two peers have completed the SDP exchange and opened a direct\n * data channel, the signalling server is no longer on the critical\n * path — the peers talk directly. The signalling server's role is\n * therefore intermittent: peers connect to it only during the brief\n * windows when they are establishing or re-establishing connections.\n *\n * Wire protocol:\n *\n * Client → server (join):\n * { type: \"join\", peerId: \"peer-alice\" }\n *\n * Client → server (signal to another peer):\n * { type: \"signal\", peerId: \"peer-alice\", targetPeerId: \"peer-bob\",\n * payload: { ... } }\n *\n * Server → client (delivered signal):\n * { type: \"signal\", peerId: \"peer-alice\", targetPeerId: \"peer-bob\",\n * payload: { ... } }\n *\n * Server → client (notification of unknown target):\n * { type: \"error\", reason: \"unknown-target\", targetPeerId: \"...\" }\n *\n * The `payload` is opaque to the signalling server — typically it\n * carries an SDP offer, SDP answer, or ICE candidate. Applications can\n * also use the relay for any other peer-to-peer message that needs an\n * intermediary, such as the initial handshake of a pairing flow.\n *\n * @example\n * ```ts\n * import { Elysia } from \"elysia\";\n * import { signalingServer } from \"@fairfox/polly/elysia\";\n *\n * const app = new Elysia()\n * .use(signalingServer({ path: \"/polly/signaling\" }))\n * .listen(8080);\n * ```\n */\n\nimport { Elysia } from \"elysia\";\n\n/** A signalling message. The `type` discriminates between client-to-server\n * requests (join, signal), the error envelope, and the server-to-client\n * discovery frames (peers-present, peer-joined, peer-left) that let\n * peers learn about each other without polling. */\nexport type SignalingMessage =\n | {\n type: \"join\";\n /** The peer registering itself with the signalling server. */\n peerId: string;\n }\n | {\n type: \"signal\";\n /** The peer sending the signal. */\n peerId: string;\n /** The peer the signal is being relayed to. */\n targetPeerId: string;\n /** Opaque payload, typically SDP or ICE. */\n payload: unknown;\n }\n | {\n type: \"error\";\n reason: \"unknown-target\" | \"not-joined\" | \"malformed\";\n targetPeerId?: string;\n }\n | {\n /** Sent to a newcomer immediately after it joins, listing every\n * peer that was already joined at that moment. Empty for a lone\n * newcomer. */\n type: \"peers-present\";\n peerIds: string[];\n }\n | {\n /** Broadcast to every incumbent when a new peer joins. */\n type: \"peer-joined\";\n peerId: string;\n }\n | {\n /** Broadcast to every remaining incumbent when a joined peer\n * closes its socket. Never emitted for a connection that never\n * sent a join frame. */\n type: \"peer-left\";\n peerId: string;\n };\n\n/** A frame whose `type` is outside the built-in signalling vocabulary.\n * Consumers who pass an {@link SignalingServerOptions.onCustomFrame}\n * handler receive these on the server side; everything else — including\n * routing them to a specific peer, storing a session, or rejecting the\n * frame — is the consumer's call. Polly does not touch the body. */\nexport interface CustomSignalingFrame {\n type: string;\n [key: string]: unknown;\n}\n\n/** Minimal surface the custom-frame handler receives in place of the\n * Elysia-specific `ws` object so the plugin stays portable. Exposes the\n * `data` bag (used to stash the authenticated peerId under the existing\n * join handshake) and a `send` method. */\nexport interface CustomFrameSocket {\n data: Record<string, unknown>;\n send: (msg: unknown) => void;\n}\n\nexport interface SignalingServerOptions {\n /** WebSocket route path. Defaults to \"/polly/signaling\". */\n path?: string;\n /** Optional hook for frames whose `type` is outside the built-in\n * vocabulary. The plugin invokes it in place of returning a\n * `malformed` error, so consumers can layer their own application\n * protocol (pairing return tokens, presence pings, etc.) on the\n * existing socket. The `peerId` argument is the sender's\n * authenticated peer id from their prior `join` frame, or\n * `undefined` if they haven't joined yet. */\n onCustomFrame?: (\n socket: CustomFrameSocket,\n frame: CustomSignalingFrame,\n peerId: string | undefined\n ) => void;\n}\n\n/**\n * Construct the signalling-server Elysia plugin. The plugin keeps a\n * per-instance map of peer id → WebSocket connection so that incoming\n * \"signal\" messages can be routed to the right target socket. The map\n * is local to the plugin instance, not shared across processes; for\n * multi-instance deployments behind a load balancer, applications need\n * sticky connection routing or a shared backplane (Redis pub-sub or\n * similar). That is a follow-up.\n */\nexport function signalingServer(options: SignalingServerOptions = {}) {\n const path = options.path ?? \"/polly/signaling\";\n const onCustomFrame = options.onCustomFrame;\n // Per-peer-id map of joined sockets. The inverse mapping is stored\n // directly on ws.data (a mutable property bag that Elysia preserves\n // across message callbacks for a given connection); the webrtc-p2p-chat\n // example in examples/ confirms this pattern is stable under Bun.\n const peerSockets = new Map<string, { send: (msg: unknown) => void }>();\n\n // Intentionally unnamed — Elysia deduplicates plugins by name, and each\n // signalingServer() call needs its own closure-captured peer map.\n const parseMessage = (raw: unknown): SignalingMessage | undefined => {\n try {\n return typeof raw === \"string\" ? JSON.parse(raw) : (raw as unknown as SignalingMessage);\n } catch {\n return undefined;\n }\n };\n\n const handleJoin = (ws: unknown, peerId: string): void => {\n const newcomer = ws as unknown as { send: (m: unknown) => void };\n // Collect the peers that were already joined so we can (a) tell the\n // newcomer who is present and (b) tell each of them about the\n // newcomer. A rejoin with the same peerId replaces the prior entry\n // but is otherwise treated as a fresh arrival.\n const incumbents: Array<{ peerId: string; socket: { send: (m: unknown) => void } }> = [];\n for (const [existingPeerId, existingSocket] of peerSockets) {\n if (existingPeerId === peerId) continue;\n incumbents.push({ peerId: existingPeerId, socket: existingSocket });\n }\n peerSockets.set(peerId, newcomer);\n const wsWithData = ws as unknown as { data: Record<string, unknown> };\n wsWithData.data.peerId = peerId;\n\n newcomer.send({\n type: \"peers-present\",\n peerIds: incumbents.map((i) => i.peerId),\n } as unknown as SignalingMessage);\n\n for (const incumbent of incumbents) {\n try {\n incumbent.socket.send({ type: \"peer-joined\", peerId } as unknown as SignalingMessage);\n } catch {\n // If a send fails we leave the stale socket to its own close\n // handler to evict. Dropping here would open a window where\n // the next signal to this peer still thinks it's alive.\n }\n }\n };\n\n const sendUnknownTarget = (ws: unknown, targetPeerId: string): void => {\n (ws as unknown as { send: (m: unknown) => void }).send({\n type: \"error\",\n reason: \"unknown-target\",\n targetPeerId,\n } as unknown as SignalingMessage);\n };\n\n /** Look up a target socket and confirm it is still open. */\n const findOpenTarget = (targetPeerId: string): { send: (msg: unknown) => void } | undefined => {\n const target = peerSockets.get(targetPeerId);\n if (!target) return undefined;\n const readyState = (target as unknown as { readyState?: number }).readyState;\n const OPEN = 1;\n if (readyState !== undefined && readyState !== OPEN) {\n peerSockets.delete(targetPeerId);\n return undefined;\n }\n return target;\n };\n\n const handleSignal = (ws: unknown, msg: Extract<SignalingMessage, { type: \"signal\" }>): void => {\n const wsWithData = ws as unknown as {\n data: Record<string, unknown>;\n send: (m: unknown) => void;\n };\n const senderId = wsWithData.data.peerId as unknown as string | undefined;\n if (!senderId) {\n wsWithData.send({ type: \"error\", reason: \"not-joined\" } as unknown as SignalingMessage);\n return;\n }\n const target = findOpenTarget(msg.targetPeerId);\n if (!target) {\n sendUnknownTarget(ws, msg.targetPeerId);\n return;\n }\n const relayed: SignalingMessage = {\n type: \"signal\",\n peerId: senderId,\n targetPeerId: msg.targetPeerId,\n payload: msg.payload,\n };\n try {\n target.send(relayed);\n } catch {\n peerSockets.delete(msg.targetPeerId);\n sendUnknownTarget(ws, msg.targetPeerId);\n }\n };\n\n return new Elysia().ws(path, {\n message(ws, raw) {\n const msg = parseMessage(raw);\n if (!msg) {\n ws.send({ type: \"error\", reason: \"malformed\" } as unknown as SignalingMessage);\n return;\n }\n if (msg.type === \"join\") {\n handleJoin(ws, msg.peerId);\n return;\n }\n if (msg.type === \"signal\") {\n handleSignal(ws, msg);\n return;\n }\n // Unknown types route to the consumer's custom-frame hook when\n // one is configured. Without a hook they still fall through to\n // the `malformed` error — same behaviour as before this branch\n // existed.\n if (onCustomFrame !== undefined) {\n const wsWithData = ws as unknown as CustomFrameSocket;\n const senderId = wsWithData.data[\"peerId\"];\n const peerId = typeof senderId === \"string\" ? senderId : undefined;\n onCustomFrame(wsWithData, msg as unknown as CustomSignalingFrame, peerId);\n return;\n }\n ws.send({ type: \"error\", reason: \"malformed\" } as unknown as SignalingMessage);\n },\n\n close(ws) {\n const peerId = (ws.data as unknown as Record<string, unknown>).peerId as unknown as\n | string\n | undefined;\n if (!peerId) {\n // Connection that never sent a join — nothing to broadcast and\n // nothing to evict. A bystander coming and going leaves no trace.\n return;\n }\n // Only evict if the map still points at *this* socket. A stale\n // close after the same peerId rejoined on a new socket should not\n // take the fresh entry with it. The comparison uses the `data` bag\n // Elysia attaches to each connection because it is preserved across\n // message and close callbacks, unlike the `ws` wrapper object which\n // Elysia may or may not reuse.\n const mapped = peerSockets.get(peerId);\n const wsData = (ws as unknown as { data: Record<string, unknown> }).data;\n const mappedData = (mapped as unknown as { data?: Record<string, unknown> } | undefined)\n ?.data;\n if (mapped === undefined || mappedData !== wsData) {\n return;\n }\n peerSockets.delete(peerId);\n for (const [_incumbentId, incumbentSocket] of peerSockets) {\n try {\n incumbentSocket.send({ type: \"peer-left\", peerId } as unknown as SignalingMessage);\n } catch {\n // Incumbent socket is gone; its own close handler will tidy.\n }\n }\n },\n });\n}\n"
|
|
18
|
+
],
|
|
19
|
+
"mappings": "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",
|
|
20
|
+
"debugId": "7F448F738B995B3064756E2164756E21",
|
|
21
|
+
"names": []
|
|
22
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fairfox/polly/test/e2e-mesh — pre-baked keyring generation.
|
|
3
|
+
*
|
|
4
|
+
* E2e scripts that test the *drain* (offline-online, late-join, sync
|
|
5
|
+
* recovery) need two peers who already know each other. The full pairing
|
|
6
|
+
* ceremony is a separate surface tested by its own script; here we
|
|
7
|
+
* pre-bake the keyrings so the script under test isn't paying the cost
|
|
8
|
+
* of pairing on every run.
|
|
9
|
+
*
|
|
10
|
+
* The keys are real — the same generators production uses — so the
|
|
11
|
+
* bootstrap path through `createMeshClient` is the same path a real user
|
|
12
|
+
* takes. We just skip the UI ceremony.
|
|
13
|
+
*/
|
|
14
|
+
export interface PrebakedPeer {
|
|
15
|
+
peerId: string;
|
|
16
|
+
/** base64-encoded identity secret key (64 bytes). */
|
|
17
|
+
identitySecretKeyB64: string;
|
|
18
|
+
/** base64-encoded identity public key (32 bytes). */
|
|
19
|
+
identityPublicKeyB64: string;
|
|
20
|
+
}
|
|
21
|
+
export interface PrebakedKeyringPair {
|
|
22
|
+
peers: [PrebakedPeer, PrebakedPeer];
|
|
23
|
+
/** base64-encoded shared document key for the default mesh key id. */
|
|
24
|
+
docKeyB64: string;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Build two peers that already know each other and share a document key.
|
|
28
|
+
* Peer ids default to "peer-a" / "peer-b" — override if a script needs
|
|
29
|
+
* specific ids for log readability.
|
|
30
|
+
*/
|
|
31
|
+
export declare function prebakeKeyringPair(peerIdA?: string, peerIdB?: string): PrebakedKeyringPair;
|
|
32
|
+
export interface PrebakedKeyringSet {
|
|
33
|
+
/** Every peer in the set, each carrying its own identity. */
|
|
34
|
+
peers: PrebakedPeer[];
|
|
35
|
+
/** Shared document key for the default mesh key id. */
|
|
36
|
+
docKeyB64: string;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Build N peers that all know each other and share a single document
|
|
40
|
+
* key. Use when a script needs more than two endpoints — three-peer
|
|
41
|
+
* convergence, revocation-over-wire, multi-hop.
|
|
42
|
+
*
|
|
43
|
+
* The result is symmetric: every peer's keyring contains the public
|
|
44
|
+
* keys of every other peer. Scripts that want to test asymmetric
|
|
45
|
+
* topologies (a peer that knows a subset) thin out the knownPeers map
|
|
46
|
+
* per-peer when wiring the bootstrap.
|
|
47
|
+
*/
|
|
48
|
+
export declare function prebakeKeyringSet(peerIds: ReadonlyArray<string>): PrebakedKeyringSet;
|
|
49
|
+
/**
|
|
50
|
+
* Build the `knownPeers` map a single peer's bootstrap needs: every
|
|
51
|
+
* other peer in the set, keyed by peerId, valued by the base64 public
|
|
52
|
+
* key. Scripts call this per peer when wiring `serveConsumer({
|
|
53
|
+
* bootstrap: { ..., knownPeers: knownPeersFor(set, "peer-a") } })`.
|
|
54
|
+
*/
|
|
55
|
+
export declare function knownPeersFor(set: PrebakedKeyringSet, thisPeerId: string): Record<string, string>;
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fairfox/polly/test/e2e-mesh — launchPeer.
|
|
3
|
+
*
|
|
4
|
+
* Boots one Puppeteer-controlled Chrome instance with a fresh profile
|
|
5
|
+
* directory (deleted before launch), navigates to the served consumer,
|
|
6
|
+
* wires console + pageerror handlers, and waits for the consumer to
|
|
7
|
+
* report `ready` status. Returns a handle the e2e script drives.
|
|
8
|
+
*
|
|
9
|
+
* The fresh-profile guarantee is what makes "cold start" honest —
|
|
10
|
+
* every run begins with empty IndexedDB, empty localStorage, empty
|
|
11
|
+
* service-worker registrations. A real first-install user sees the
|
|
12
|
+
* same state.
|
|
13
|
+
*/
|
|
14
|
+
import { type Page } from "puppeteer";
|
|
15
|
+
import type { MeshDiagnostic, MeshDiagnosticEvent } from "../../../../src/shared/lib/mesh-diagnostics";
|
|
16
|
+
import { type ConsolePattern } from "./console-allowlist";
|
|
17
|
+
export interface CapturedConsoleLine {
|
|
18
|
+
level: string;
|
|
19
|
+
text: string;
|
|
20
|
+
/** True when the line matched the supplied allowlist; allowed lines do
|
|
21
|
+
* not contribute to assertNoUnexpectedConsole failures. */
|
|
22
|
+
allowed: boolean;
|
|
23
|
+
}
|
|
24
|
+
export interface LaunchedPeer {
|
|
25
|
+
/** The peerId the consumer was booted with. */
|
|
26
|
+
readonly peerId: string;
|
|
27
|
+
/** The Puppeteer Page handle. Scripts drive this directly. */
|
|
28
|
+
readonly page: Page;
|
|
29
|
+
/** Live capture buffer of console lines seen on this peer. */
|
|
30
|
+
readonly console: ReadonlyArray<CapturedConsoleLine>;
|
|
31
|
+
/** Live capture of page-level errors. */
|
|
32
|
+
readonly pageErrors: ReadonlyArray<string>;
|
|
33
|
+
/** Throws if any captured console line was not allowed. */
|
|
34
|
+
assertNoUnexpectedConsole: () => void;
|
|
35
|
+
/**
|
|
36
|
+
* Read every mesh-diagnostic event the consumer has captured so far.
|
|
37
|
+
* Each call snapshots the live browser-side buffer; the array is
|
|
38
|
+
* detached from the page after read.
|
|
39
|
+
*/
|
|
40
|
+
collectDiagnostics: () => Promise<MeshDiagnosticEvent[]>;
|
|
41
|
+
/**
|
|
42
|
+
* Read the captured diagnostics and assert no unexpected silent
|
|
43
|
+
* drops fired. Pass `allow` with the drop-kinds the scenario
|
|
44
|
+
* legitimately expects; anything else fails.
|
|
45
|
+
*/
|
|
46
|
+
assertNoSilentDrops: (allow?: ReadonlyArray<MeshDiagnostic["kind"]>) => Promise<void>;
|
|
47
|
+
/** Close the page, browser, and profile dir. Idempotent. */
|
|
48
|
+
close: () => Promise<void>;
|
|
49
|
+
}
|
|
50
|
+
export interface LaunchPeerOptions {
|
|
51
|
+
/** Peer id used in the consumer's display + key wiring. */
|
|
52
|
+
peerId: string;
|
|
53
|
+
/** http://127.0.0.1:<port>/ from serveConsumer. */
|
|
54
|
+
consumerUrl: string;
|
|
55
|
+
/** When true, run Chrome headfully so the developer can watch. Defaults
|
|
56
|
+
* to `process.env.HEADLESS !== "false"`. */
|
|
57
|
+
headless?: boolean;
|
|
58
|
+
/** Override the console allowlist; defaults to MESH_CONSOLE_ALLOWLIST. */
|
|
59
|
+
consoleAllowlist?: ReadonlyArray<ConsolePattern>;
|
|
60
|
+
/** Cap how long to wait for the consumer to report status="ready"
|
|
61
|
+
* before throwing. Defaults to 15s. */
|
|
62
|
+
readyTimeoutMs?: number;
|
|
63
|
+
/** Override the profile-dir parent. Defaults to os.tmpdir() / polly-e2e. */
|
|
64
|
+
profileParent?: string;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Launch one peer and wait until the consumer reports it is connected
|
|
68
|
+
* and rendering. Throws on console-error or pageerror during boot.
|
|
69
|
+
*/
|
|
70
|
+
export declare function launchPeer(options: LaunchPeerOptions): Promise<LaunchedPeer>;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fairfox/polly/test/e2e-mesh — assertions over the mesh-diagnostics
|
|
3
|
+
* stream.
|
|
4
|
+
*
|
|
5
|
+
* Every e2e script that drives the mesh subscribes to the diagnostic
|
|
6
|
+
* stream before it starts and runs the assertions in `assertNoSilentDrops`
|
|
7
|
+
* before exiting. The default is "no unexpected diagnostics fired"; a
|
|
8
|
+
* script that legitimately exercises a drop branch (e.g. a revocation
|
|
9
|
+
* test) supplies an allowlist of expected kinds so the gate stays loud
|
|
10
|
+
* about everything else.
|
|
11
|
+
*
|
|
12
|
+
* This is the test-kit half of the diagnostic-obligation move: the lint
|
|
13
|
+
* half (forbidding new silent-drop branches that do not emit) lands
|
|
14
|
+
* separately as `scripts/check-mesh-diagnostics.ts`.
|
|
15
|
+
*/
|
|
16
|
+
import { type MeshDiagnostic, type MeshDiagnosticEvent } from "../../../../src/shared/lib/mesh-diagnostics";
|
|
17
|
+
export interface MeshAssertionFailure extends Error {
|
|
18
|
+
readonly kind: "mesh-assertion-failure";
|
|
19
|
+
readonly unexpected: ReadonlyArray<MeshDiagnosticEvent>;
|
|
20
|
+
}
|
|
21
|
+
export declare class MeshAssertionError extends Error implements MeshAssertionFailure {
|
|
22
|
+
readonly kind = "mesh-assertion-failure";
|
|
23
|
+
readonly unexpected: ReadonlyArray<MeshDiagnosticEvent>;
|
|
24
|
+
constructor(message: string, unexpected: ReadonlyArray<MeshDiagnosticEvent>);
|
|
25
|
+
}
|
|
26
|
+
export interface DiagnosticRecorder {
|
|
27
|
+
/** Live capture buffer — reads see new events the moment they fire. */
|
|
28
|
+
events: ReadonlyArray<MeshDiagnosticEvent>;
|
|
29
|
+
/** Stop subscribing. Idempotent. */
|
|
30
|
+
stop: () => void;
|
|
31
|
+
/**
|
|
32
|
+
* Run the no-silent-drops assertion against the captured buffer.
|
|
33
|
+
* Throws {@link MeshAssertionError} if any unexpected diagnostic fired.
|
|
34
|
+
* Pass `allow` with the kinds the script legitimately expected — the
|
|
35
|
+
* gate fails on anything else.
|
|
36
|
+
*/
|
|
37
|
+
assertNoSilentDrops: (allow?: ReadonlyArray<MeshDiagnostic["kind"]>) => void;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Begin capturing diagnostics for the duration of a script.
|
|
41
|
+
*
|
|
42
|
+
* @example
|
|
43
|
+
* ```typescript
|
|
44
|
+
* const diag = startDiagnosticRecorder();
|
|
45
|
+
* try {
|
|
46
|
+
* await driveTheScenario();
|
|
47
|
+
* diag.assertNoSilentDrops();
|
|
48
|
+
* } finally {
|
|
49
|
+
* diag.stop();
|
|
50
|
+
* }
|
|
51
|
+
* ```
|
|
52
|
+
*/
|
|
53
|
+
export declare function startDiagnosticRecorder(): DiagnosticRecorder;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fairfox/polly/test/e2e-mesh — bundle and serve the e2e consumer.
|
|
3
|
+
*
|
|
4
|
+
* Bun.build compiles `examples/e2e-consumer/main.ts` for the browser
|
|
5
|
+
* target with the Automerge base64 fix (same plugin the existing browser
|
|
6
|
+
* runner uses). Bun.serve then hands the HTML on "/" with a bootstrap
|
|
7
|
+
* shim injected, and the JS on "/main.js". Puppeteer points at the
|
|
8
|
+
* returned URL.
|
|
9
|
+
*
|
|
10
|
+
* The kit owns this so every e2e script gets the same boot path: build
|
|
11
|
+
* the in-tree consumer, serve from a fresh ephemeral port, inject the
|
|
12
|
+
* peer-specific bootstrap. No script should call Bun.build directly —
|
|
13
|
+
* keeping it in one place means the Automerge plugin and the bootstrap
|
|
14
|
+
* shape stay coherent across the suite.
|
|
15
|
+
*/
|
|
16
|
+
export interface ServeConsumerOptions {
|
|
17
|
+
/** The bootstrap object that the page reads from window.__pollyE2eBootstrap. */
|
|
18
|
+
bootstrap: Record<string, unknown>;
|
|
19
|
+
}
|
|
20
|
+
export interface ServeConsumerResult {
|
|
21
|
+
/** http://127.0.0.1:<port>/ — pass to puppeteer page.goto. */
|
|
22
|
+
url: string;
|
|
23
|
+
/** Stop the server. Idempotent. */
|
|
24
|
+
close: () => Promise<void>;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Bundle the consumer and serve it on an ephemeral port. The HTML's
|
|
28
|
+
* `<script type="module" src="./main.js">` resolves to the freshly built
|
|
29
|
+
* bundle; the bootstrap shim is inserted right before it so the global
|
|
30
|
+
* is set by the time `main.ts` reads it.
|
|
31
|
+
*/
|
|
32
|
+
export declare function serveConsumer(options: ServeConsumerOptions): Promise<ServeConsumerResult>;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fairfox/polly/test/e2e-mesh — waitForConvergence.
|
|
3
|
+
*
|
|
4
|
+
* Polls a consumer-supplied predicate against every launched peer until
|
|
5
|
+
* the predicate returns true on all of them, or the timeout expires.
|
|
6
|
+
* The predicate runs in the *node* side (the script) and is handed a
|
|
7
|
+
* snapshot reader function that reads from the peer's page DOM.
|
|
8
|
+
*
|
|
9
|
+
* Typical use: assert every peer's `[data-e2e='items']` UL contains the
|
|
10
|
+
* expected set of values.
|
|
11
|
+
*/
|
|
12
|
+
import type { LaunchedPeer } from "./launch-peer";
|
|
13
|
+
export interface PeerSnapshot {
|
|
14
|
+
peerId: string;
|
|
15
|
+
/** Items currently rendered in the consumer's [data-e2e='items'] list. */
|
|
16
|
+
items: string[];
|
|
17
|
+
/** Connected peer count reported by the consumer. */
|
|
18
|
+
peerCount: number;
|
|
19
|
+
/** Status text the consumer currently displays. */
|
|
20
|
+
status: string;
|
|
21
|
+
}
|
|
22
|
+
export type ConvergencePredicate = (snapshot: PeerSnapshot) => boolean;
|
|
23
|
+
export interface WaitForConvergenceOptions {
|
|
24
|
+
/** Cap wait time before throwing. Defaults to 20s. */
|
|
25
|
+
timeoutMs?: number;
|
|
26
|
+
/** Poll interval. Defaults to 200ms. */
|
|
27
|
+
pollMs?: number;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Block until the predicate is true for every peer, or throw.
|
|
31
|
+
*/
|
|
32
|
+
export declare function waitForConvergence(peers: ReadonlyArray<LaunchedPeer>, predicate: ConvergencePredicate, options?: WaitForConvergenceOptions): Promise<void>;
|
|
33
|
+
/**
|
|
34
|
+
* Convenience: wait until every peer reports it sees at least N connected
|
|
35
|
+
* peers. Used right after launching to confirm the WebRTC handshake
|
|
36
|
+
* completed before driving any user-facing flow.
|
|
37
|
+
*/
|
|
38
|
+
export declare function waitForMeshConnected(peers: ReadonlyArray<LaunchedPeer>, options?: WaitForConvergenceOptions): Promise<void>;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fairfox/polly/test/e2e-mesh — withRelay helper.
|
|
3
|
+
*
|
|
4
|
+
* Boots a polly signalling relay on an ephemeral port for e2e scripts.
|
|
5
|
+
* The relay is the same Elysia plugin polly ships at runtime, so a script
|
|
6
|
+
* driving the relay through `withRelay` exercises the production protocol
|
|
7
|
+
* with no shimming. Two modes:
|
|
8
|
+
*
|
|
9
|
+
* - "embedded" (default): start an Elysia app on a random port and return
|
|
10
|
+
* its URL plus a close callback. The hermetic mode the suite runs in
|
|
11
|
+
* by default; depending on a staging relay would couple CI reliability
|
|
12
|
+
* to a network service we do not control.
|
|
13
|
+
*
|
|
14
|
+
* - "env": read SIGNALING_URL from the environment and return that
|
|
15
|
+
* alongside a no-op close. The override the nightly cron uses to smoke
|
|
16
|
+
* the production protocol against the live relay.
|
|
17
|
+
*
|
|
18
|
+
* Lift source: tests/integration/signaling-server.test.ts is the existing
|
|
19
|
+
* recipe; this helper centralises it so every e2e script wires the relay
|
|
20
|
+
* the same way.
|
|
21
|
+
*/
|
|
22
|
+
export interface WithRelayResult {
|
|
23
|
+
/** WebSocket URL of the signalling endpoint, ready to be passed to
|
|
24
|
+
* `createMeshClient({ signaling: { url, peerId } })`. */
|
|
25
|
+
url: string;
|
|
26
|
+
/** Stop the relay. Idempotent; safe to call after a failed boot. */
|
|
27
|
+
close: () => Promise<void>;
|
|
28
|
+
}
|
|
29
|
+
export interface WithRelayOptions {
|
|
30
|
+
/**
|
|
31
|
+
* "embedded" boots a fresh relay on an ephemeral port and returns its
|
|
32
|
+
* URL. "env" reads SIGNALING_URL from the environment and returns it
|
|
33
|
+
* without booting anything. Defaults to "embedded".
|
|
34
|
+
*/
|
|
35
|
+
mode?: "embedded" | "env";
|
|
36
|
+
/** Path under which the signalling endpoint is mounted. Defaults to
|
|
37
|
+
* "/polly/signaling" — the same default the SPA wiring uses. */
|
|
38
|
+
path?: string;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Start a signalling relay for the duration of an e2e script.
|
|
42
|
+
*
|
|
43
|
+
* @example
|
|
44
|
+
* ```typescript
|
|
45
|
+
* const relay = await withRelay();
|
|
46
|
+
* try {
|
|
47
|
+
* // boot peers pointing signaling.url at relay.url ...
|
|
48
|
+
* } finally {
|
|
49
|
+
* await relay.close();
|
|
50
|
+
* }
|
|
51
|
+
* ```
|
|
52
|
+
*/
|
|
53
|
+
export declare function withRelay(options?: WithRelayOptions): Promise<WithRelayResult>;
|