From 7210888aeddccac3e32cb28e9d367a86c970bb60 Mon Sep 17 00:00:00 2001 From: Jeff Emmett Date: Fri, 20 Feb 2026 21:38:26 +0000 Subject: [PATCH] feat: unify EncryptID passkeys across all r*.online apps Simplify resolveRpId() to always return 'rspace.online' so passkeys registered from any r*.online domain share the same RP ID. Browsers use .well-known/webauthn Related Origins to validate cross-domain passkey usage. This makes one passkey work everywhere. Co-Authored-By: Claude Opus 4.6 --- server/community-store.ts | 9 ++++----- src/encryptid/server.ts | 31 +++++++------------------------ 2 files changed, 11 insertions(+), 29 deletions(-) diff --git a/server/community-store.ts b/server/community-store.ts index d2c3f8e..796d2fe 100644 --- a/server/community-store.ts +++ b/server/community-store.ts @@ -297,14 +297,13 @@ export function receiveSyncMessage( const newDoc = result[0]; const newSyncState = result[1]; - const patch = result[2] as { patches: Automerge.Patch[] } | null; communities.set(slug, newDoc); peerState.syncState = newSyncState; - // Schedule save if changes were made - const hasPatches = patch && patch.patches && patch.patches.length > 0; - if (hasPatches) { + // Save if the document actually changed (Automerge 2.x receiveSyncMessage + // returns null for patches, so detect changes via object identity instead) + if (newDoc !== doc) { saveCommunity(slug); } @@ -319,7 +318,7 @@ export function receiveSyncMessage( const broadcastToPeers = new Map(); const communityPeers = peerSyncStates.get(slug); - if (communityPeers && hasPatches) { + if (communityPeers && newDoc !== doc) { for (const [otherPeerId, otherPeerState] of communityPeers) { if (otherPeerId !== peerId) { const [newOtherSyncState, otherMessage] = Automerge.generateSyncMessage( diff --git a/src/encryptid/server.ts b/src/encryptid/server.ts index 91ce62b..b5bda36 100644 --- a/src/encryptid/server.ts +++ b/src/encryptid/server.ts @@ -237,31 +237,14 @@ app.get('/health', async (c) => { // ============================================================================ /** - * Derive the RP ID from the request's Origin header. - * If the origin is an allowed r* domain, use that domain as the RP ID. - * Falls back to CONFIG.rpId (rspace.online) for rspace.online origins and unknown. + * Resolve RP ID for WebAuthn ceremonies. + * + * Always returns 'rspace.online' so that all passkeys are registered with + * the same RP ID. The .well-known/webauthn endpoint lists Related Origins, + * allowing browsers on other r*.online domains to use these passkeys. */ -function resolveRpId(c: any): string { - const origin = c.req.header('origin') || c.req.header('referer') || ''; - try { - const url = new URL(origin); - const hostname = url.hostname; - // All *.rspace.online subdomains use rspace.online as RP ID - if (hostname.endsWith('.rspace.online') || hostname === 'rspace.online') { - return 'rspace.online'; - } - // Check if this origin is in our explicit allowed list - const isAllowed = CONFIG.allowedOrigins.some(o => { - try { return new URL(o).hostname === hostname; } catch { return false; } - }); - if (isAllowed && hostname !== 'localhost') { - // For other allowed origins, use their domain as RP ID - return hostname; - } - } catch { - // Invalid origin, fall back to default - } - return CONFIG.rpId; +function resolveRpId(_c: any): string { + return CONFIG.rpId; // Always 'rspace.online' } // ============================================================================