{"_id":"@aikofy/client-db-sync","_rev":"8-495c9e9e4876792dbdc0f2322bfe0db4","name":"@aikofy/client-db-sync","dist-tags":{"latest":"2.1.1"},"versions":{"0.1.0":{"name":"@aikofy/client-db-sync","version":"0.1.0","keywords":["webrtc","signaling","websocket","p2p","offline-first","jwt","ed25519"],"author":{"name":"Lwin Maung Maung"},"license":"MIT","_id":"@aikofy/client-db-sync@0.1.0","maintainers":[{"name":"lwinmgmg","email":"lwinmaungmaung.ytu@gmail.com"}],"bin":{"client-db-sync":"dist/cli.js","client-db-sync-keygen":"dist/scripts/generate-keys.js"},"dist":{"shasum":"9bf5fcb63a2b8c7a627ed8e77493f4fe25791763","tarball":"https://registry.npmjs.org/@aikofy/client-db-sync/-/client-db-sync-0.1.0.tgz","fileCount":12,"integrity":"sha512-Y7aYK6IROk7bESRQVJ2QsZ5BUT9Q3mQEQ/Gq8gWbzV6F/rY+GxDGZe9T9oN471xiJe7GiP9P3ccm1QXaJh3O5g==","signatures":[{"sig":"MEYCIQCTjNjC9SG0igabY+UxBZE3AAr4f4mCnktUstSTlqjRGwIhAOX8yvKy9DY4dH7pKg8xEvN6lg2fAXmX2tZIX1Q5AWSm","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":65986},"main":"./dist/index.cjs","type":"module","types":"./dist/index.d.ts","module":"./dist/index.js","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js","require":"./dist/index.cjs"}},"scripts":{"dev":"bunx tsup --watch & node --watch dist/index.js","build":"bunx tsup","start":"node dist/index.js","keygen":"bunx tsx scripts/generate-keys.ts","typecheck":"bunx tsc --noEmit"},"_npmUser":{"name":"lwinmgmg","email":"lwinmaungmaung.ytu@gmail.com"},"_npmVersion":"10.9.3","description":"Minimal WebRTC signaling server for @aikofy/client-db with Ed25519 JWT auth","directories":{},"_nodeVersion":"22.20.0","dependencies":{"jose":"^5.0.0","uuid":"^14.0.0","fastify":"^5.0.0","@fastify/websocket":"^11.0.0"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"tsx":"^4.0.0","tsup":"^8.0.0","typescript":"^5.5.0","@types/node":"^22.0.0","@types/uuid":"^11.0.0"},"_npmOperationalInternal":{"tmp":"tmp/client-db-sync_0.1.0_1778496563204_0.5819999048584812","host":"s3://npm-registry-packages-npm-production"}},"0.1.1":{"name":"@aikofy/client-db-sync","version":"0.1.1","keywords":["webrtc","signaling","websocket","p2p","offline-first","jwt","ed25519"],"author":{"name":"Lwin Maung Maung"},"license":"MIT","_id":"@aikofy/client-db-sync@0.1.1","maintainers":[{"name":"lwinmgmg","email":"lwinmaungmaung.ytu@gmail.com"}],"bin":{"client-db-sync":"dist/cli.js","client-db-sync-keygen":"dist/scripts/generate-keys.js"},"dist":{"shasum":"24358f94a51e525d12e3b02ad82e6c5dbc6491bf","tarball":"https://registry.npmjs.org/@aikofy/client-db-sync/-/client-db-sync-0.1.1.tgz","fileCount":12,"integrity":"sha512-AM/5XrNCcO4MgE3HX7gh1sU7Cli0ookh4RKZ3RgfO5J9Y+eleTFw285AJUYAtXZWEpzz0fT1WsYQPau/Qe30MQ==","signatures":[{"sig":"MEUCIFbapT+O8uEvXKFFTNzcGIBhUOZUWUOi2EN+kMJEfhd+AiEAkYNdwgNYSjSzBpR6tXtI09/oiJ1PQ753B4Ho8/s0jx0=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":65986},"main":"./dist/index.cjs","type":"module","types":"./dist/index.d.ts","module":"./dist/index.js","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js","require":"./dist/index.cjs"}},"scripts":{"dev":"bunx tsup --watch & node --watch dist/index.js","build":"bunx tsup","start":"node dist/index.js","keygen":"bunx tsx scripts/generate-keys.ts","typecheck":"bunx tsc --noEmit"},"_npmUser":{"name":"lwinmgmg","email":"lwinmaungmaung.ytu@gmail.com"},"_npmVersion":"10.9.3","description":"Minimal WebRTC signaling server for @aikofy/client-db with Ed25519 JWT auth","directories":{},"_nodeVersion":"22.20.0","dependencies":{"jose":"^5.0.0","uuid":"^14.0.0","fastify":"^5.0.0","@fastify/websocket":"^11.0.0"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"tsx":"^4.0.0","tsup":"^8.0.0","typescript":"^5.5.0","@types/node":"^22.0.0","@types/uuid":"^11.0.0"},"_npmOperationalInternal":{"tmp":"tmp/client-db-sync_0.1.1_1778523436560_0.16915166616163724","host":"s3://npm-registry-packages-npm-production"}},"0.1.2":{"name":"@aikofy/client-db-sync","version":"0.1.2","keywords":["webrtc","signaling","websocket","p2p","offline-first","jwt","ed25519"],"author":{"name":"Lwin Maung Maung"},"license":"MIT","_id":"@aikofy/client-db-sync@0.1.2","maintainers":[{"name":"lwinmgmg","email":"lwinmaungmaung.ytu@gmail.com"}],"bin":{"client-db-sync":"dist/cli.js","client-db-sync-keygen":"dist/scripts/generate-keys.js"},"dist":{"shasum":"3fdb00615130c53c4e954611d3db3574a8b31a40","tarball":"https://registry.npmjs.org/@aikofy/client-db-sync/-/client-db-sync-0.1.2.tgz","fileCount":12,"integrity":"sha512-5ir15QBSeRCShYlO4cKWpWy/lEIpDC+lNr0UmPArPV2Zq6WV7MdqDGxxE7yj+Om5UKGiQzEsmst+ZrenqKJhmQ==","signatures":[{"sig":"MEYCIQDc0gtFLCz9SncmxKKs+B9Ikc5pliY3RuuTvg8jNWn1BwIhAIy+pCNaGqKSP6cRbDn9M3YCgy8DxVihLCHN/JHlwelL","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":71620},"main":"./dist/index.cjs","type":"module","types":"./dist/index.d.ts","module":"./dist/index.js","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js","require":"./dist/index.cjs"}},"scripts":{"dev":"bunx tsup --watch & node --watch dist/index.js","build":"bunx tsup","start":"node dist/index.js","keygen":"bunx tsx scripts/generate-keys.ts","typecheck":"bunx tsc --noEmit"},"_npmUser":{"name":"lwinmgmg","email":"lwinmaungmaung.ytu@gmail.com"},"_npmVersion":"10.9.3","description":"Minimal WebRTC signaling server for @aikofy/client-db with Ed25519 JWT auth","directories":{},"_nodeVersion":"22.20.0","dependencies":{"jose":"^5.0.0","uuid":"^14.0.0","fastify":"^5.0.0","@fastify/websocket":"^11.0.0"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"tsx":"^4.0.0","tsup":"^8.0.0","typescript":"^5.5.0","@types/node":"^22.0.0","@types/uuid":"^11.0.0"},"_npmOperationalInternal":{"tmp":"tmp/client-db-sync_0.1.2_1778523700907_0.29549225271192237","host":"s3://npm-registry-packages-npm-production"}},"0.1.3":{"name":"@aikofy/client-db-sync","version":"0.1.3","keywords":["webrtc","signaling","websocket","p2p","offline-first","jwt","ed25519"],"author":{"name":"Lwin Maung Maung"},"license":"MIT","_id":"@aikofy/client-db-sync@0.1.3","maintainers":[{"name":"lwinmgmg","email":"lwinmaungmaung.ytu@gmail.com"}],"bin":{"client-db-sync":"dist/cli.js","client-db-sync-keygen":"dist/scripts/generate-keys.js"},"dist":{"shasum":"697eac5c95b7d40c2179e2f489387c37d8e715d6","tarball":"https://registry.npmjs.org/@aikofy/client-db-sync/-/client-db-sync-0.1.3.tgz","fileCount":12,"integrity":"sha512-wmdgtvJfIfDa668deyaxdu15JgeMKBJDxCk2kqZts+v3uxLcOTsJQoZMXNTOgbVGG9Qjs4sNVTBxAG9JWgVdZw==","signatures":[{"sig":"MEQCIEbHGMkowKUVB6fTMfyFVC8InJu/DP6xgBiL00/VamXzAiBMA6ZoirZdtqiQs2q3mQdWor7qbrw/T/TdlO5FpAsueQ==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":73653},"main":"./dist/index.cjs","type":"module","types":"./dist/index.d.ts","module":"./dist/index.js","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js","require":"./dist/index.cjs"}},"gitHead":"1b21724cd00d77faf273a16fbbf6f45abf1c8e3d","scripts":{"dev":"bunx tsup --watch & node --watch dist/index.js","build":"bunx tsup","start":"node dist/index.js","keygen":"bunx tsx scripts/generate-keys.ts","typecheck":"bunx tsc --noEmit"},"_npmUser":{"name":"lwinmgmg","email":"lwinmaungmaung.ytu@gmail.com"},"_npmVersion":"11.13.0","description":"Minimal WebRTC signaling server for @aikofy/client-db with Ed25519 JWT auth","directories":{},"_nodeVersion":"24.16.0","dependencies":{"jose":"^5.0.0","uuid":"^14.0.0","fastify":"^5.0.0","@fastify/websocket":"^11.0.0"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"tsx":"^4.0.0","tsup":"^8.0.0","typescript":"^5.5.0","@types/node":"^22.0.0","@types/uuid":"^11.0.0"},"_npmOperationalInternal":{"tmp":"tmp/client-db-sync_0.1.3_1779717370477_0.908478986847082","host":"s3://npm-registry-packages-npm-production"}},"0.1.4":{"name":"@aikofy/client-db-sync","version":"0.1.4","keywords":["webrtc","signaling","websocket","p2p","offline-first","jwt","ed25519"],"author":{"name":"Lwin Maung Maung"},"license":"MIT","_id":"@aikofy/client-db-sync@0.1.4","maintainers":[{"name":"lwinmgmg","email":"lwinmaungmaung.ytu@gmail.com"}],"bin":{"client-db-sync":"dist/cli.js","client-db-sync-keygen":"dist/scripts/generate-keys.js"},"dist":{"shasum":"9fe6c26d4da9d8bcc602924328f1bea6073a001d","tarball":"https://registry.npmjs.org/@aikofy/client-db-sync/-/client-db-sync-0.1.4.tgz","fileCount":12,"integrity":"sha512-Rr6/uUSXPANzrltCE1fJTL5aZglvkYvdAo3OIctychV4S+9FNqT8Wy82h8cHtuIBE8oGrSQO3YwaCSOZrsIB1g==","signatures":[{"sig":"MEUCIQDZ0rluF73bXz4rlZxSE9+7NzK6S8w5pHdaFIUQwPLl3QIgIqApymMXwOGv1gL3ltjcXJOuaHTTnLFp0VRWwdLdIjc=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":74830},"main":"./dist/index.cjs","type":"module","types":"./dist/index.d.ts","module":"./dist/index.js","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js","require":"./dist/index.cjs"}},"gitHead":"b44079ac5ed018e8f97d597aa7aba6abb71e18f7","scripts":{"dev":"bunx tsup --watch & node --watch dist/index.js","build":"bunx tsup","start":"node dist/index.js","keygen":"bunx tsx scripts/generate-keys.ts","typecheck":"bunx tsc --noEmit"},"_npmUser":{"name":"lwinmgmg","email":"lwinmaungmaung.ytu@gmail.com"},"_npmVersion":"11.13.0","description":"Minimal WebRTC signaling server for @aikofy/client-db with Ed25519 JWT auth","directories":{},"_nodeVersion":"24.16.0","dependencies":{"jose":"^5.0.0","uuid":"^14.0.0","fastify":"^5.0.0","@fastify/websocket":"^11.0.0"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"tsx":"^4.0.0","tsup":"^8.0.0","typescript":"^5.5.0","@types/node":"^22.0.0","@types/uuid":"^11.0.0"},"_npmOperationalInternal":{"tmp":"tmp/client-db-sync_0.1.4_1779723742324_0.8221172515319151","host":"s3://npm-registry-packages-npm-production"}},"2.0.0":{"name":"@aikofy/client-db-sync","version":"2.0.0","keywords":["webrtc","signaling","websocket","p2p","offline-first","jwt","ed25519","rpc","consumer-client","load-balancer"],"author":{"name":"Lwin Maung Maung"},"license":"MIT","_id":"@aikofy/client-db-sync@2.0.0","maintainers":[{"name":"lwinmgmg","email":"lwinmaungmaung.ytu@gmail.com"}],"bin":{"client-db-sync":"dist/cli.js","client-db-sync-keygen":"dist/scripts/generate-keys.js"},"dist":{"shasum":"6a9360a566a98a3ed11a4f638e78ebe9d9bdf0b9","tarball":"https://registry.npmjs.org/@aikofy/client-db-sync/-/client-db-sync-2.0.0.tgz","fileCount":12,"integrity":"sha512-2nVruGvhhSEZJ8zbdHgvT6GosFhp2fpkAcBSOX73A/PxEVnBa6GlDMJtTMGrhShUTKaTrBV3UTt6HT4xg9ZvaA==","signatures":[{"sig":"MEUCIH8UK4HslRSBTu0870T/mhP+wJsTOHdne3b+hS70YnB1AiEAl54KyeDOTCBLd6rkrX5034lUCBmbt/bj8iFYuM66ak8=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":139403},"main":"./dist/index.cjs","type":"module","types":"./dist/index.d.ts","module":"./dist/index.js","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js","require":"./dist/index.cjs"}},"gitHead":"dcb9bdc8790d1eb25585df06bede0bbb31e805ac","scripts":{"dev":"bunx tsup --watch & node --watch dist/index.js","test":"bunx vitest run","build":"bunx tsup","start":"node dist/index.js","keygen":"bunx tsx scripts/generate-keys.ts","typecheck":"bunx tsc --noEmit"},"_npmUser":{"name":"lwinmgmg","email":"lwinmaungmaung.ytu@gmail.com"},"_npmVersion":"11.13.0","description":"WebRTC signaling + load-balancing director for @aikofy/client-db: room-based gossip peering, role-aware Consumer Client support, Ed25519 room tokens, and IdP-verified consumer tokens","directories":{},"_nodeVersion":"24.16.0","dependencies":{"jose":"^5.0.0","uuid":"^14.0.0","fastify":"^5.0.0","@fastify/websocket":"^11.0.0"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"tsx":"^4.0.0","tsup":"^8.0.0","vitest":"^2.0.0","typescript":"^5.5.0","@types/node":"^22.0.0","@types/uuid":"^11.0.0"},"_npmOperationalInternal":{"tmp":"tmp/client-db-sync_2.0.0_1780336961304_0.5862586081963521","host":"s3://npm-registry-packages-npm-production"}},"2.1.0":{"name":"@aikofy/client-db-sync","version":"2.1.0","keywords":["webrtc","signaling","websocket","p2p","offline-first","jwt","ed25519","rpc","consumer-client","load-balancer"],"author":{"name":"Lwin Maung Maung"},"license":"MIT","_id":"@aikofy/client-db-sync@2.1.0","maintainers":[{"name":"lwinmgmg","email":"lwinmaungmaung.ytu@gmail.com"}],"bin":{"client-db-sync":"dist/cli.js","client-db-sync-keygen":"dist/scripts/generate-keys.js"},"dist":{"shasum":"32d0085f05fa03ed56d67a9d242e2b909badfaeb","tarball":"https://registry.npmjs.org/@aikofy/client-db-sync/-/client-db-sync-2.1.0.tgz","fileCount":22,"integrity":"sha512-d7lRiJBWtK0DmRwPRWFCM+DLyWAHDwckrzVJ8whky/A8wOVc4kWVF8kPUJSu6/DZUVkosqfo4LCTN6BViuJgkA==","signatures":[{"sig":"MEQCIEkO8EXMAwJTG+DVJ4/4gNcQCLLtRETKBzxexuketCWzAiBnSxTnia1nvYh5gLjKh4DPrVe2reYvyo8/xGoOaptRRw==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":228945},"main":"./dist/index.cjs","type":"module","types":"./dist/index.d.ts","module":"./dist/index.js","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js","require":"./dist/index.cjs"},"./embed":{"types":"./dist/embed.d.ts","import":"./dist/embed.js","require":"./dist/embed.cjs"}},"gitHead":"13fecbd041d2a9466f5baf293544a0173c7c9d7b","scripts":{"dev":"bunx tsup --watch & node --watch dist/index.js","test":"bunx vitest run","build":"bunx tsup","start":"node dist/index.js","keygen":"bunx tsx scripts/generate-keys.ts","typecheck":"bunx tsc --noEmit"},"_npmUser":{"name":"lwinmgmg","email":"lwinmaungmaung.ytu@gmail.com"},"_npmVersion":"11.13.0","description":"WebRTC signaling + load-balancing director for @aikofy/client-db: room-based gossip peering, role-aware Consumer Client support, Ed25519 room tokens, and IdP-verified consumer tokens","directories":{},"_nodeVersion":"24.16.0","dependencies":{"ws":"^8.18.0","jose":"^5.0.0","uuid":"^14.0.0","fastify":"^5.0.0","@fastify/websocket":"^11.0.0"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"tsx":"^4.0.0","tsup":"^8.0.0","vitest":"^2.0.0","@types/ws":"^8.18.1","typescript":"^5.5.0","@types/node":"^22.0.0","@types/uuid":"^11.0.0"},"_npmOperationalInternal":{"tmp":"tmp/client-db-sync_2.1.0_1787045156515_0.0659234537713056","host":"s3://npm-registry-packages-npm-production"}},"2.1.1":{"name":"@aikofy/client-db-sync","version":"2.1.1","description":"WebRTC signaling + load-balancing director for @aikofy/client-db: room-based gossip peering, role-aware Consumer Client support, Ed25519 room tokens, and IdP-verified consumer tokens","type":"module","main":"./dist/index.cjs","module":"./dist/index.js","types":"./dist/index.d.ts","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js","require":"./dist/index.cjs"},"./embed":{"types":"./dist/embed.d.ts","import":"./dist/embed.js","require":"./dist/embed.cjs"}},"bin":{"client-db-sync":"dist/cli.js","client-db-sync-keygen":"dist/scripts/generate-keys.js"},"scripts":{"build":"bunx tsup","dev":"bunx tsup --watch & node --watch dist/index.js","start":"node dist/index.js","keygen":"bunx tsx scripts/generate-keys.ts","test":"bunx vitest run","typecheck":"bunx tsc --noEmit"},"dependencies":{"@fastify/websocket":"^11.0.0","fastify":"^5.0.0","jose":"^5.0.0","uuid":"^14.0.0","ws":"^8.18.0"},"devDependencies":{"@types/node":"^22.0.0","@types/uuid":"^11.0.0","@types/ws":"^8.18.1","tsup":"^8.0.0","tsx":"^4.0.0","typescript":"^5.5.0","vitest":"^2.0.0"},"author":{"name":"Lwin Maung Maung"},"license":"MIT","publishConfig":{"access":"public"},"keywords":["webrtc","signaling","websocket","p2p","offline-first","jwt","ed25519","rpc","consumer-client","load-balancer"],"gitHead":"6299a2dc410fbe27e9d7b0bc2cbbb90207578ead","_id":"@aikofy/client-db-sync@2.1.1","_nodeVersion":"24.16.0","_npmVersion":"11.13.0","dist":{"integrity":"sha512-EDmL+dIe9YLTmtMZRG5gapul6Y88i6KDp8CXsnMEi7eePUBJU5miRHRBd7bPGhBElh6hCadnTfveUTJJtOFPRQ==","shasum":"00909629df80b44ab120d120635a723f32c6d1a9","tarball":"https://registry.npmjs.org/@aikofy/client-db-sync/-/client-db-sync-2.1.1.tgz","fileCount":22,"unpackedSize":233184,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEUCIQDkSFdeS00kluaPGtW+IYPCANgb0AuYB8dUxNolmVZuywIgQ+NJsn73OwZl+Pwv4tMhXEfFs3sgBFWr1c2lelYFnfQ="}]},"_npmUser":{"name":"lwinmgmg","email":"lwinmaungmaung.ytu@gmail.com"},"directories":{},"maintainers":[{"name":"lwinmgmg","email":"lwinmaungmaung.ytu@gmail.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/client-db-sync_2.1.1_1787092133829_0.5871867900633736"},"_hasShrinkwrap":false}},"time":{"created":"2026-05-11T10:49:23.036Z","modified":"2026-08-18T22:28:54.153Z","0.1.0":"2026-05-11T10:49:23.359Z","0.1.1":"2026-05-11T18:17:16.731Z","0.1.2":"2026-05-11T18:21:41.094Z","0.1.3":"2026-05-25T13:56:10.620Z","0.1.4":"2026-05-25T15:42:22.454Z","2.0.0":"2026-06-01T18:02:41.490Z","2.1.0":"2026-08-18T09:25:56.709Z","2.1.1":"2026-08-18T22:28:53.980Z"},"author":{"name":"Lwin Maung Maung"},"license":"MIT","keywords":["webrtc","signaling","websocket","p2p","offline-first","jwt","ed25519","rpc","consumer-client","load-balancer"],"description":"WebRTC signaling + load-balancing director for @aikofy/client-db: room-based gossip peering, role-aware Consumer Client support, Ed25519 room tokens, and IdP-verified consumer tokens","maintainers":[{"name":"lwinmgmg","email":"lwinmaungmaung.ytu@gmail.com"}],"readme":"# @aikofy/client-db-sync\n\nMinimal WebRTC signaling server for [`@aikofy/client-db`](https://www.npmjs.com/package/@aikofy/client-db).\n\nHandles peer discovery (offer/answer/ICE exchange) over WebSocket with **room-based isolation** — peers only see other peers in the same room. Protected by **Ed25519 JWT tokens** with configurable TTL.\n\n**New in 2.0.0:** role-aware support for `@aikofy/client-db` **Consumer Clients** — thin clients that call a Normal Client's RPC functions instead of replicating. The server keeps Consumers out of the gossip mesh and acts as a load-balancing **director** (see [Consumer Clients](#consumer-clients)). Normal-Client-only sync is unchanged and needs no configuration change.\n\n[![npm version](https://img.shields.io/npm/v/@aikofy/client-db-sync)](https://www.npmjs.com/package/@aikofy/client-db-sync)\n[![license](https://img.shields.io/npm/l/@aikofy/client-db-sync)](./LICENSE)\n\n---\n\n## How It Works\n\n```\nYour backend ──POST /token──► Signaling server (issues JWT scoped to a room)\n                                      │\nClient A ──WS /signal?token=JWT──►   │   ◄──WS /signal?token=JWT── Client B\n              (room = user-123)       │         (room = user-123)\n                    ◄── peer-list ────┤  (only peers in same room)\n                    ── offer ────────►│──────────────────────────►\n                    ◄── answer ───────│◄──────────────────────────\n                    ── ice-candidate ►│──────────────────────────►\n```\n\nAfter the WebRTC handshake, all sync data flows **directly peer-to-peer** — nothing passes through this server.\n\n### Room isolation\n\nEach client connects to a **room** identified by their DB name. Peers in different rooms are completely invisible to each other — they receive no peer-list entries and cannot exchange signals.\n\nWhen auth is enabled, the room is enforced by the JWT token: a token issued with `subject: \"user-123\"` can **only** join room `user-123`. Knowing a room name (e.g. a userID) is not enough to join it without a valid token.\n\n### Registry rules\n\nThe peer registry is keyed by `(room, nodeId)`. A second connection arriving with the **same `nodeId` in the same room** evicts the previous one with WebSocket close code `1000 'replaced by new connection'`.\n\nThis is a feature — it lets a reconnecting client take over from a stale socket without waiting for the previous one to time out. It becomes a problem when two *different* clients (e.g. two browser tabs of the same user) accidentally share a `nodeId`: each new connection kicks the other, producing a reconnect loop.\n\n**Always pass a per-tab unique `nodeId`** when running multiple tabs of the same browser. `@aikofy/client-db` supports this via `sync.nodeId` — see its README for the `sessionStorage` pattern.\n\n---\n\n## Consumer Clients\n\n`@aikofy/client-db` 2.0.0 adds a second client role. **Normal Clients** hold a full replica and\ngossip with each other (as above). **Consumer Clients** hold no data — they connect to a Normal\nClient and call its RPC functions. This server brokers both and keeps them apart:\n\n- A client declares its role in the `register` message (`role: \"normal\" | \"consumer\"`; default\n  `\"normal\"`, so 1.x clients are unaffected).\n- **Normal Clients** get a `peer-list` of *other Normal Clients only* — Consumers are never gossip\n  peers, and consumer↔consumer offers are never relayed.\n- **Consumer Clients** get a `server-list`: the healthy, opted-in Normal Clients they may call,\n  **rotated round-robin** so independent Consumers spread across nodes. Removing a dead node from\n  the list is what drives a Consumer's failover to another node.\n- Relayed offers are stamped with `fromRole` so the answerer can branch early.\n\n### Authenticating Consumers\n\nA Consumer connects **without** a URL `?token=` and instead presents its IdP access token in the\n`register` message. The server verifies it as a **defense-in-depth admission gate** (the Normal\nClient re-verifies it authoritatively on the RPC data channel). Configure your IdP's public key(s):\n\n| Variable | Description |\n|----------|-------------|\n| `CONSUMER_PUBLIC_KEY_JWK` | Base64-encoded JWK, JWK array, or `{ keys: [...] }` JWKS of your IdP's public key(s). **Enables Consumer support.** |\n| `CONSUMER_ISSUER` | Expected token `iss` (optional) |\n| `CONSUMER_AUDIENCE` | Expected token `aud` (optional) |\n\nTokens are verified with **ES256/RS256** (matching `@aikofy/client-db`'s `createTokenVerifier`);\n`alg:none` is rejected. When auth is enabled and `CONSUMER_PUBLIC_KEY_JWK` is **not** set, Consumer\nregistrations are refused (Normal-Client sync still works). With `AUTH_DISABLED=true` (dev),\nConsumers are accepted without verification.\n\n> These are separate from the server's own Ed25519 **room tokens** (which gate Normal Clients via\n> `POST /token`). Consumer tokens are issued by *your* IdP and only verified here.\n\n---\n\n## Quick Start\n\n### 1. Generate keys\n\n```bash\nnpx @aikofy/client-db-sync keygen\n\n# After `npm install @aikofy/client-db-sync` in this project:\nnpx client-db-sync-keygen\n```\n\nThere is no package named `@aikofy/client-db-sync-keygen`. `keygen` is a command of\n`@aikofy/client-db-sync`. `bun run keygen` only exists in this repo's `package.json`, not in an\napp that depends on the package.\n\nCopy the output into your `.env` file.\n\n### 2. Configure\n\n**Production:**\n\n```bash\n# .env\nPRIVATE_KEY_JWK=<output from keygen>\nPUBLIC_KEY_JWK=<output from keygen>\nADMIN_SECRET=a-strong-random-secret\nPORT=8080\n```\n\n**Local development (no auth):**\n\n```bash\n# .env\nAUTH_DISABLED=true\nPORT=8080\n```\n\n> ⚠️ `AUTH_DISABLED=true` lets any client connect without a token. Rooms still provide namespace separation but offer no security guarantee. Never use it in production. The server logs a warning at startup when this is set.\n\n### 3. Run\n\n```bash\n# With npx (no install needed)\nnpx @aikofy/client-db-sync\n\n# Or install globally\nnpm install -g @aikofy/client-db-sync\nclient-db-sync\n\n# Or as a local dependency\nnpm install @aikofy/client-db-sync\nnode node_modules/@aikofy/client-db-sync/dist/index.js\n```\n\n---\n\n### Runtime: Node or Bun\n\nThe server runs on **Node 18+** or **Bun 1.x**. Both are supported. Key generation (`npx @aikofy/client-db-sync keygen`) requires `0.1.3` or newer when running under Bun — earlier versions hit a `non-extractable CryptoKey` error because Bun resolves jose's `browser` export, which creates non-extractable keys by default.\n\nIf you see that error, upgrade:\n\n```bash\nnpm install @aikofy/client-db-sync@latest\n```\n\n---\n\n## Environment Variables\n\n| Variable | Required | Description |\n|----------|----------|-------------|\n| `AUTH_DISABLED` | No | Set to `true` to disable JWT auth (dev only) |\n| `PRIVATE_KEY_JWK` | Yes (auth on) | Base64-encoded Ed25519 private key JWK |\n| `PUBLIC_KEY_JWK` | Yes (auth on) | Base64-encoded Ed25519 public key JWK |\n| `ADMIN_SECRET` | Yes (auth on) | Secret for the `POST /token` endpoint |\n| `CONSUMER_PUBLIC_KEY_JWK` | No | Base64 JWK/JWKS of your IdP's public key(s) — enables Consumer Client support (see [Consumer Clients](#consumer-clients)) |\n| `CONSUMER_ISSUER` | No | Expected `iss` for consumer tokens |\n| `CONSUMER_AUDIENCE` | No | Expected `aud` for consumer tokens |\n| `PORT` | No | Server port (default `8080`) |\n\n---\n\n## Docker\n\n```dockerfile\nFROM node:22-alpine\nRUN npm install -g @aikofy/client-db-sync\nEXPOSE 8080\nCMD [\"client-db-sync\"]\n```\n\n```bash\ndocker run -p 8080:8080 \\\n  -e PRIVATE_KEY_JWK=\"...\" \\\n  -e PUBLIC_KEY_JWK=\"...\" \\\n  -e ADMIN_SECRET=\"...\" \\\n  your-image\n```\n\n---\n\n## API\n\n### `GET /health`\n\nReturns server status and total connected peer count. No authentication required.\n\n```json\n{ \"status\": \"ok\", \"auth\": \"enabled\", \"peers\": 3, \"ts\": \"2026-01-01T00:00:00.000Z\" }\n```\n\n---\n\n### `GET /public-key`\n\nReturns the Ed25519 public key as a JWK. No authentication required.\n\nClients can use this to verify tokens locally if needed.\n\n```json\n{\n  \"alg\": \"EdDSA\",\n  \"crv\": \"Ed25519\",\n  \"kty\": \"OKP\",\n  \"x\": \"...\"\n}\n```\n\n---\n\n### `POST /token`\n\nIssues a signed JWT scoped to a room. **Requires the admin secret.**\n\nThe `subject` field becomes the room name — clients using a token can only join the room that matches their token's subject. Set `subject` to the user's DB name (typically their userID).\n\n**Headers:**\n\n```\nx-admin-secret: <your ADMIN_SECRET>\n```\n\nor:\n\n```\nAuthorization: Bearer <your ADMIN_SECRET>\n```\n\n**Body:**\n\n```json\n{\n  \"ttl\": \"24h\",\n  \"subject\": \"user-123\"\n}\n```\n\n| Field | Type | Description |\n|-------|------|-------------|\n| `ttl` | `string` | Token lifetime — e.g. `\"1h\"`, `\"7d\"`, `\"30m\"` |\n| `subject` | `string` (optional) | The room this token authorizes. Set to the DB name / userID. Defaults to `\"default\"`. |\n\n**Response `201`:**\n\n```json\n{\n  \"token\": \"eyJ...\",\n  \"expiresAt\": \"2026-01-02T00:00:00.000Z\",\n  \"subject\": \"user-123\"\n}\n```\n\n---\n\n### `WS /signal?token=<jwt>&room=<room>&nodeId=<uuid>`\n\nWebSocket endpoint for WebRTC signaling.\n\n| Query param | Required | Description |\n|-------------|----------|-------------|\n| `token` | Yes (auth on) | JWT issued by `POST /token` |\n| `room` | Yes | The room to join. Must match `token.sub` when auth is enabled. Set automatically by `@aikofy/client-db` from the DB name. |\n| `nodeId` | No | Client's node UUID (falls back to JWT subject) |\n\n**Room validation (auth enabled):** If the `room` param does not match the token's `subject`, the connection is closed with code `4003`.\n\n**Auth disabled:** The `room` param is accepted as-is. If omitted, defaults to `\"default\"`. No token required.\n\n**Close codes:**\n\n| Code | Reason |\n|------|--------|\n| `4001` | Missing token (auth enabled) |\n| `4003` | Invalid or expired token, or room does not match token subject |\n| `1000` | Replaced by a new connection from the same nodeId |\n\n---\n\n## Connecting from `@aikofy/client-db`\n\nThe `room` param is appended automatically from the DB name — you only need to pass the token:\n\n```typescript\n// Backend (Node.js / any server)\nconst res = await fetch('https://your-signal-server.example.com/token', {\n  method: 'POST',\n  headers: {\n    'Content-Type': 'application/json',\n    'x-admin-secret': process.env.ADMIN_SECRET,\n  },\n  // subject must equal the DB name the client will use\n  body: JSON.stringify({ ttl: '24h', subject: req.user.id }),\n});\nconst { token } = await res.json();\n// Return token to the client\n```\n\n```typescript\n// Client (React / browser)\nimport { createDB } from '@aikofy/client-db';\n\nconst db = await createDB({\n  name: currentUser.id,   // DB name = room — must match the token subject\n  version: 1,\n  collections: { todos: { indexes: ['status'] } },\n  sync: {\n    // The library appends ?room=<dbName> automatically\n    signalingServer: `wss://your-signal-server.example.com/signal?token=${token}`,\n    iceServers: [{ urls: 'stun:stun.l.google.com:19302' }],\n  },\n});\n```\n\n---\n\n## Programmatic API\n\nTwo entrypoints, depending on whether you want your own HTTP server.\n\n### Standalone — `createServer`\n\nRuns the whole thing: `/health`, `/public-key`, `POST /token` and `WS /signal` on a Fastify\ninstance you listen on yourself. This is what the CLI uses.\n\n```typescript\nimport { createServer, generateKeyPairJwk } from '@aikofy/client-db-sync';\n\n// Generate keys once and store them in env/secrets manager\nconst { privateKeyJwk, publicKeyJwk } = await generateKeyPairJwk();\n\nconst app = await createServer({\n  port: 8080,\n  authEnabled: true,\n  adminSecret: 'my-secret',\n  privateKeyJwk,\n  publicKeyJwk,\n});\n\nawait app.listen({ port: 8080, host: '0.0.0.0' });\n```\n\n### Embedded — `createSignalingHandler`\n\nMounts signaling on an HTTP server you already have, **sharing its port** with whatever else is\nrunning there (socket.io, Express, Hono, …). No second service, no second port, no admin secret:\nthe process holds the signing key, so it mints tokens with a direct call.\n\nImport from `@aikofy/client-db-sync/embed` to keep Fastify out of your dependency graph — that\nentrypoint pulls in only `ws`, `jose` and `uuid`.\n\n```typescript\nimport { createServer as createHttp } from 'node:http';\nimport { Server as IOServer } from 'socket.io';\nimport { createSignalingHandler } from '@aikofy/client-db-sync/embed';\n\nconst signaling = await createSignalingHandler({\n  authEnabled: true,\n  privateKeyJwk,\n  publicKeyJwk,\n  // path: '/signal' by default; pass null to claim every upgrade you hand it\n});\n\nconst httpServer = createHttp(myApp);\n\n// Share the port with socket.io. `destroyUpgrade: false` stops engine.io from\n// reaping upgrades on paths it does not own — see the note below.\nconst io = new IOServer(httpServer, { destroyUpgrade: false });\n\nhttpServer.on('upgrade', (req, socket, head) => {\n  if (signaling.handleUpgrade(req, socket, head)) return;   // claimed /signal\n  if (!(req.url ?? '').startsWith('/socket.io/')) socket.destroy();\n});\n\nhttpServer.listen(3001);\n```\n\nMint room tokens for your own authenticated users — no HTTP hop, no `ADMIN_SECRET`:\n\n```typescript\nconst { token, expiresAt, subject } = await signaling.issueToken({\n  ttl: '24h',\n  subject: `mmgr-user-${userId}`,   // MUST equal the client's room name\n});\n```\n\nExpose signaling health on your existing health endpoint:\n\n```typescript\napp.get('/healthz', () => ({ ...myStats, signaling: signaling.stats() }));\n// { auth: 'enabled', peers: 12, rooms: 5 }\n```\n\n#### Two rules when embedding\n\n1. **Call `handleUpgrade` synchronously** from the `upgrade` listener. It takes the socket over\n   immediately, so no client bytes are lost; any `await` before it and the handshake dies. All\n   async work (token verification) already happens after the upgrade, inside the handler.\n2. **If socket.io shares the port, pass `destroyUpgrade: false`.** By default engine.io arms a 1 s\n   timer on every upgrade for a path it does not own and destroys the socket. Your own fallback\n   branch (above) should destroy unknown paths instead.\n\n`handleUpgrade` returns `false` without touching the socket when the path is not this handler's,\nso your routing stays in charge.\n\n### Full embedded surface\n\n| Member | Purpose |\n|---|---|\n| `handleUpgrade(req, socket, head)` | Claim a raw upgrade. Sync-safe. Returns `false` on a path mismatch. |\n| `handleConnection(ws, requestUrl)` | Drive an already-upgraded socket (hosts that handshake themselves). |\n| `issueToken({ ttl, subject })` | Mint a room token locally. Throws when `authEnabled` is false. |\n| `publicJwk()` | The public JWK, for your own `/public-key`. `null` when auth is off. |\n| `stats()` | `{ auth, peers, rooms }`. |\n| `registry` | The underlying `SignalingRegistry`. |\n| `close()` | Close all sockets and the WebSocket server. |\n\n---\n\n## Security Notes\n\n- **Never expose `PRIVATE_KEY_JWK`** — only the server needs it\n- **`PUBLIC_KEY_JWK`** is safe to share with clients for local token verification\n- **`ADMIN_SECRET`** should only be known by your backend — never sent to browsers\n- Tokens are signed with Ed25519 (`EdDSA`) and verified on every WebSocket connection\n- **Room enforcement** — a token for room `user-123` cannot join room `user-456`, even if the attacker knows the room name. Security depends on your backend's auth, not the secrecy of the room identifier.\n- Expired tokens are rejected — reconnect logic in `@aikofy/client-db` handles token refresh\n\n---\n\n## Project Structure\n\n```\nsrc/\n  types.ts       # Message types + config interfaces\n  keys.ts        # Ed25519 key pair loading and generation\n  auth.ts        # JWT issuance and verification\n  signaling.ts   # Room-keyed peer registry + message routing\n  handler.ts     # Framework-agnostic signaling core (upgrade + connection logic)\n  server.ts      # Standalone Fastify server + HTTP routes (wraps handler.ts)\n  cli.ts         # CLI entry point (bin: client-db-sync)\n  index.ts       # Programmatic exports\n  embed.ts       # Fastify-free exports (@aikofy/client-db-sync/embed)\nscripts/\n  generate-keys.ts  # Key generation helper (bin: client-db-sync-keygen)\n```\n\n---\n\n## Contributing\n\n```bash\nbun install\nbun run build\nbun run typecheck\n```\n\n---\n\n## License\n\nMIT © Lwin Maung Maung\n","readmeFilename":"README.md"}