{"id":50758585,"url":"https://github.com/flammafex/rendezvous","last_synced_at":"2026-06-11T07:33:15.168Z","repository":{"id":328997659,"uuid":"1116462155","full_name":"flammafex/rendezvous","owner":"flammafex","description":null,"archived":false,"fork":false,"pushed_at":"2025-12-18T10:09:09.000Z","size":1995,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2025-12-20T08:30:37.658Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/flammafex.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2025-12-14T22:35:56.000Z","updated_at":"2025-12-18T10:09:14.000Z","dependencies_parsed_at":"2025-12-20T10:01:37.677Z","dependency_job_id":null,"html_url":"https://github.com/flammafex/rendezvous","commit_stats":null,"previous_names":["flammafex/rendezvous"],"tags_count":null,"template":false,"template_full_name":null,"purl":"pkg:github/flammafex/rendezvous","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/flammafex%2Frendezvous","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/flammafex%2Frendezvous/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/flammafex%2Frendezvous/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/flammafex%2Frendezvous/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/flammafex","download_url":"https://codeload.github.com/flammafex/rendezvous/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/flammafex%2Frendezvous/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":34188272,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-11T02:00:06.485Z","response_time":57,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2026-06-11T07:33:14.425Z","updated_at":"2026-06-11T07:33:15.143Z","avatar_url":"https://github.com/flammafex.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# 🤝 Rendezvous\n\nTwo parties can discover if they mutually selected each other without revealing:\n- Who the person selected (if not mutual)\n- Who rejected whom\n- That you even participated\n\n**Use cases:** Dating, co-founder matching, hackathon team building, roommate search, mentor pairing, any N-to-N selection where privacy matters.\n\n## How It Works\n\nThe key insight: Diffie-Hellman produces the same shared secret from either side.\n\n```\nAlice wants Bob:\n  shared = DH(alice_private, bob_public)\n  token = H(shared || pool_id || \"match\")\n\nBob wants Alice:\n  shared = DH(bob_private, alice_public)  // Same shared secret!\n  token = H(shared || pool_id || \"match\")  // Same token!\n```\n\nIf both submit, the token appears twice → match detected.\nIf only one submits, token appears once → no information leaked.\n\n## Features\n\n### Core Privacy\n- **Privacy-preserving matching** - Only mutual matches are revealed\n- **Reveal on match** - Encrypted contact info decryptable only by mutual matches\n- **Privacy delay** - Random 30s-3min delay before match computation prevents timing analysis\n- **Decoy tokens** - Hides your true selection count from the server\n- **Response padding** - All API responses padded to 8KB blocks to prevent size analysis\n- **Ephemeral mode** - Auto-delete participant profiles after pool closes\n\n### Private Set Intersection (PSI)\n- **Owner-held key PSI** - Pool owners can use PSI to process match queries without learning joiner preferences\n- **WASM-based** - Uses @openmined/psi.js for client-side intersection computation\n- **Cardinality-only mode** - Option to reveal only match count, not identities\n\n### Federation\n- **Cross-instance pools** - Discover and join pools across federated Rendezvous servers\n- **Peer-to-peer sync** - Rendezvous instances connect directly via WebSocket (default port 3001)\n- **Automerge CRDTs** - Pool metadata synced using conflict-free replicated data types\n- **Anonymous messaging** - All federation messages use unlinkable Freebird tokens\n- **Timing noise** - Random delays on federation messages to frustrate traffic analysis\n\n### Access Control\n- **Freebird integration** - Unlinkable eligibility proofs for pool creation and joining\n- **Witness integration** - Timestamp attestation for match results\n- **Invite-gated pools** - Require valid invite codes to join\n- **Owner signatures** - Pool actions verified via Ed25519 signatures\n\n### User Experience\n- **QR code invites** - Share pools via scannable QR codes\n- **Multi-device sync** - Transfer keys between devices via encrypted QR\n- **PWA support** - Install as a mobile app, works offline\n\n## Installation\n\n```bash\nnpm install\nnpm run build\n```\n\nRequires Node.js 20+.\n\n## Quick Start\n\n```bash\n# Seed the database with sample pools\nnpm run seed\n\n# Start the web server\nnpm run server\n\n# Open http://localhost:3000\n```\n\nThe seed script creates demo pools with participants who have pre-selected the test user. Use the keypair printed by the seed script to get guaranteed matches.\n\n## Configuration\n\nEnvironment variables:\n\n| Variable | Description | Default |\n|----------|-------------|---------|\n| `PORT` | HTTP server port | `3000` |\n| `RENDEZVOUS_DATA_DIR` | Database directory | `./data` |\n| `FREEBIRD_VERIFIER_URL` | Freebird verifier for invite codes | _(disabled)_ |\n| `WITNESS_GATEWAY_URL` | Witness gateway for timestamps | _(disabled)_ |\n| `FEDERATION_ENABLED` | Enable federation | `false` |\n| `FEDERATION_PORT` | WebSocket port for federation | `3001` |\n| `FEDERATION_PEERS` | Comma-separated peer endpoints | _(none)_ |\n| `FREEBIRD_ISSUER_URL` | Required for federation auth | _(none)_ |\n\nWhen `FREEBIRD_VERIFIER_URL` is set, pool creation requires a valid invite code. When unset, pool creation is open (development mode).\n\n## Web UI\n\nThe web interface provides a complete flow:\n\n1. **Pools** - Browse and create matching pools\n2. **Join** - Register, browse participants, and make selections\n3. **Discover** - Find your mutual matches after pool closes\n4. **Keys** - Generate keypairs, manage identities, sync across devices\n\n### Reveal on Match\n\nWhen confirming your selections, you can add contact info and a message. This data is encrypted using the match token as the key—only someone who mutually selected you can decrypt it. The server stores only encrypted blobs.\n\n## Programmatic Usage\n\n```typescript\nimport {\n  createRendezvous,\n  generateKeypair,\n  deriveMatchTokens,\n  deriveNullifier\n} from 'rendezvous';\n\n// Create instance with optional adapters\nconst rv = createRendezvous({\n  dbPath: './data/rendezvous.db',\n  // freebird: new HttpFreebirdAdapter({ verifierUrl: '...' }),\n  // witness: new HttpWitnessAdapter({ gatewayUrl: '...' }),\n});\n\n// Create a pool\nconst pool = rv.createPool({\n  name: 'Team Matching',\n  creatorPublicKey: creatorKey,\n  creatorSigningKey: signingKey,\n  revealDeadline: new Date(Date.now() + 24 * 3600000),\n  ephemeral: true,\n});\n\n// Generate your keypair\nconst me = generateKeypair();\n\n// Submit preferences\nconst theirKeys = ['abc...', 'def...'];\nconst tokens = deriveMatchTokens(me.privateKey, theirKeys, pool.id);\nconst nullifier = deriveNullifier(me.privateKey, pool.id);\n\nrv.submitPreferences({\n  poolId: pool.id,\n  matchTokens: tokens,\n  nullifier,\n  revealData: [\n    { matchToken: tokens[0], encryptedReveal: '...' },\n  ],\n});\n\n// After pool closes, detect matches (async for witness attestation)\nrv.closePool(pool.id);\nconst result = await rv.detectMatches(pool.id);\n\n// Discover your matches locally\nconst myMatches = rv.discoverMyMatches(pool.id, me.privateKey, theirKeys);\nfor (const match of myMatches) {\n  console.log(`Matched with: ${match.matchedPublicKey}`);\n}\n```\n\n## Protocol Phases\n\n1. **Pool Creation**: Operator creates pool with eligibility rules and deadline\n2. **Commit Phase** (optional): Participants submit H(tokens) to prevent timing attacks\n3. **Reveal Phase**: Participants submit actual tokens (+ optional encrypted contact info)\n4. **Privacy Delay**: Random 30s-3min delay before match computation\n5. **Detection**: Count token occurrences. Duplicates = matches.\n6. **Discovery**: Each participant locally checks which of their tokens matched\n\n## Privacy Features\n\n### Decoy Tokens\nWhen submitting preferences, clients add random decoy tokens. This hides your true selection count from the server.\n\n### Privacy Delay\nAfter a pool closes, match computation is delayed by a random 30s-3min interval. This prevents timing analysis that could correlate submission times with results.\n\n### Response Padding\nAll API responses are padded to 8KB block boundaries. This prevents attackers from inferring information based on response sizes.\n\n### Pseudonym Rotation\nGenerate a fresh keypair for each pool. This prevents correlation of your identity across pools—even if someone is in multiple pools with you, they can't link your profiles.\n\n### Ephemeral Pools\nPool creators can enable ephemeral mode, which deletes all participant profiles after match detection. Only anonymous match tokens remain.\n\n## Anti-Gaming Measures\n\n- **Fishing attacks**: Limited by `maxPreferencesPerParticipant`\n- **Timing attacks**: Prevented by commit-reveal phases and privacy delay\n- **Sybil attacks**: Freebird nullifiers ensure one submission per identity\n- **Eligibility gates**: Freebird tokens, invite lists, composite rules\n\n## Project Structure\n\n```\nsrc/\n├── rendezvous/\n│   ├── types.ts          # Core type definitions\n│   ├── crypto.ts         # DH tokens, encryption, signatures\n│   ├── storage.ts        # SQLite persistence\n│   ├── pool.ts           # Pool management\n│   ├── submission.ts     # Preference submission\n│   ├── detection.ts      # Match detection\n│   ├── gates/            # Eligibility gates\n│   ├── adapters/         # Freebird \u0026 Witness HTTP clients\n│   └── index.ts          # Public API\n├── psi/\n│   ├── types.ts          # PSI type definitions\n│   └── service.ts        # PSI operations (@openmined/psi.js)\n├── federation/\n│   ├── types.ts          # Federation message types\n│   ├── manager.ts        # CRDT sync \u0026 peer management\n│   └── freebird-client.ts # Anonymous auth tokens\n├── server/\n│   └── index.ts          # REST API \u0026 WebSocket server\n├── scripts/\n│   └── seed.ts           # Demo data seeder\n├── cli/\n│   └── index.ts          # CLI commands\n└── index.ts              # Main entry point\npublic/\n├── index.html            # Web UI\n├── js/modules/           # Modular frontend components\n├── css/                  # Stylesheets\n├── sw.js                 # Service worker for PWA\n└── manifest.json         # PWA manifest\n```\n\n## Testing\n\n```bash\nnpm test\n```\n\n## API Endpoints\n\n### Pools\n- `GET /api/pools` - List pools\n- `POST /api/pools` - Create pool (requires invite if Freebird configured)\n- `GET /api/pools/:id` - Get pool details\n- `POST /api/pools/:id/close` - Close pool (owner-only, signed)\n\n### Participants\n- `POST /api/pools/:id/participants` - Register in pool\n- `GET /api/pools/:id/participants` - List participants\n\n### Preferences\n- `POST /api/pools/:id/submit` - Submit match tokens\n- `POST /api/pools/:id/reveal` - Reveal committed preferences\n\n### PSI (Owner-Held Key)\n- `POST /api/pools/:id/psi/owner-setup` - Owner creates PSI setup\n- `POST /api/pools/:id/psi/request` - Client submits PSI request\n- `GET /api/pools/:id/psi/pending` - Owner polls pending requests\n- `POST /api/pools/:id/psi/responses` - Owner submits responses\n- `GET /api/psi/response/:requestId` - Client polls for result\n\n### PSI Client Helpers\n- `POST /api/psi/create-request` - Create PSI request from inputs\n- `POST /api/psi/compute-intersection` - Compute intersection locally\n- `POST /api/psi/compute-cardinality` - Compute match count only (more private)\n\n### Federation\n- `GET /api/federation` - Federation status\n- `GET /api/federation/pools` - List federated pools\n- `POST /api/federation/announce/:poolId` - Announce pool to federation\n- `POST /api/federation/join/:poolId` - Join federated pool\n\n## License\n\nApache-2.0\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fflammafex%2Frendezvous","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fflammafex%2Frendezvous","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fflammafex%2Frendezvous/lists"}