Skip to content

Phase 6: Peer Admission Protocol#11

Merged
wmxscott merged 6 commits into
mainfrom
claude/implement-phase-6-uOUBo
Mar 23, 2026
Merged

Phase 6: Peer Admission Protocol#11
wmxscott merged 6 commits into
mainfrom
claude/implement-phase-6-uOUBo

Conversation

@wmxscott
Copy link
Copy Markdown
Owner

Summary

Implements the Peer Admission Protocol (Phase 6) as specified in the implementation plan.

  • src/lib/admission/index.ts — Core admission logic:

    • isOwnerSelfAdmit: Checks all four conditions for owner self-admission
    • ownerSelfAdmit: Idempotent self-admission writing to Yjs shared doc
    • issueNonce: Verifier-issued challenge nonce (prevents replay attacks)
    • evaluateAdmission: Full Round 4 evaluation — verifies JWT, nonce proof, wraps room key for moderators via RSA-OAEP. Guests never receive a room key. Reconnect detected via burned JTI registry, PUBKEY_MISMATCH prevents key-swap attacks.
  • src/lib/room/useSession.ts — React hook resolving session state:

    • Owner self-admits instantly when alone
    • Moderators/guests without owner online → needsLobby: true
    • Owner present → PENDING_HANDSHAKE for event-driven WebRTC handshake
    • Generates and persists signing/OAEP keypairs on first visit
  • src/constants/index.ts — Added GUEST_PEER_ID storage key

  • src/lib/crypto/storage.ts — Added loadSigningPublicKeyB64, loadGuestPeerId, saveGuestPeerId

Test plan

  • Build passes (pnpm run build)
  • Lint passes (pnpm run lint)
  • Format applied (pnpm run format)
  • All 170 tests pass (pnpm test)
  • Owner self-admits with ready: true, role: owner
  • Guest/moderator without owner -> needsLobby: true
  • Valid token + correct nonce sig -> admitted; moderator receives wrapped room key
  • Tampered JWT -> INVALID_SIGNATURE; expired -> TOKEN_EXPIRED; wrong key -> PUBKEY_MISMATCH
  • Guest never receives encryptedRoomKey (TypeScript enforces string | null)
  • Reconnecting moderator admitted without second room key wrap

https://claude.ai/code/session_01V93TGVeBjjHgdWZpTsVBWH

claude and others added 3 commits March 23, 2026 03:14
… hook

Implements the peer admission protocol as specified in the implementation plan:

- `src/lib/admission/index.ts`: Core admission logic with `isOwnerSelfAdmit`,
  `ownerSelfAdmit`, `issueNonce`, and `evaluateAdmission`. The owner evaluates
  peer admission requests, verifying JWT signatures, nonce proofs, and wrapping
  the AES-GCM room key for admitted moderators via RSA-OAEP. Guests never
  receive the room key. Reconnecting peers are verified against the burned JTI
  registry to prevent public key swaps.

- `src/lib/room/useSession.ts`: React hook that resolves the current peer's
  session state. Owner self-admits instantly when no other peers are connected.
  Moderators and guests without an owner online enter the lobby. When the owner
  is present the hook signals PENDING_HANDSHAKE for the event-driven WebRTC
  handshake to complete.

- `src/constants/index.ts`: Added `GUEST_PEER_ID` storage key for ephemeral
  guest identity persistence.

- `src/lib/crypto/storage.ts`: Added `loadSigningPublicKeyB64`,
  `loadGuestPeerId`, and `saveGuestPeerId` helpers required by the session hook.

Comprehensive tests cover all acceptance criteria: owner self-admit, lobby
states, first admission and reconnection flows, tampered/expired token
rejection, PUBKEY_MISMATCH on key-swap attempts, and the invariant that guests
never receive an encrypted room key.

https://claude.ai/code/session_01V93TGVeBjjHgdWZpTsVBWH
Run prettier after final test edits — previously format was run before
the test fixes, so the final edits were not formatted before the commit.

https://claude.ai/code/session_01V93TGVeBjjHgdWZpTsVBWH
Copy link
Copy Markdown
Owner Author

@wmxscott wmxscott left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good overall, some feedback on some changes below.

Also, remove the .gitkeep from lib/admission as no longer needed.

Also, try to move some of the logic from useSession.ts to the lib/admission package.

Comment thread src/constants/index.ts Outdated
ROOM_KEY: 'ratifyd:roomkey', // AES-GCM room key (owner + moderators)
PEER_ID: 'ratifyd:peerId', // Stable identity UUID
PEER_ID: 'ratifyd:peerId', // Stable identity UUID (owner + moderators)
GUEST_PEER_ID: 'ratifyd:guestPeerId', // Ephemeral guest identity UUID
Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No need for this, use PEER_ID instead

Comment thread src/lib/admission/index.ts Outdated
roomId: string
}

export interface AdmissionResult {
Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I prefer this type to be more specific so that you cannot pass admitted as true or encrypted room key if the admission fails. Set it to:

{
  admitted: true
  role: Role
  encryptedRoomKey: string | null // only for moderators on first admission
  reason: null
} | {
  admitted: false
  role: null
  encryptedRoomKey: null
  reason: string
}

Comment thread src/lib/admission/index.ts Outdated

if (payload.room !== roomId) return fail('WRONG_ROOM')

const isReconnect = shared.burnedJTIs.has(payload.jti)
Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Evaluate the isReconnect immediately before the if usage, and after the JWT signature has been verified. In fact, don't create an unnecessary constant, just evaluate the value right within the if condition

Comment thread src/lib/admission/index.ts Outdated
import type { JWTPayload } from '../../constants'
import type { SharedTypes } from '../yjs/doc'

export interface AdmissionParams {
Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rename to AdmissionRequest instead of AdmissionParams

Comment thread src/lib/admission/index.ts Outdated
roomId: string
}

export interface AdmissionResult {
Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rename to AdmissionResponse instead of AdmissionResult

Comment thread src/lib/crypto/storage.ts Outdated
return localStorage.getItem(STORAGE_KEYS.PEER_ID)
}

export function saveGuestPeerId(peerId: string): void {
Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No need for this, use savePeerId instead

Comment thread src/lib/crypto/storage.ts Outdated
localStorage.setItem(STORAGE_KEYS.GUEST_PEER_ID, peerId)
}

export function loadGuestPeerId(): string | null {
Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No need for this, use loadPeerId instead

claude and others added 3 commits March 23, 2026 21:55
- Rename AdmissionParams → AdmissionRequest, AdmissionResult → AdmissionResponse
- Make AdmissionResponse a discriminated union for precise typing
- Inline isReconnect check directly into if condition
- Remove GUEST_PEER_ID constant; guests now share STORAGE_KEYS.PEER_ID
- Remove saveGuestPeerId/loadGuestPeerId; callers use savePeerId/loadPeerId

https://claude.ai/code/session_01V93TGVeBjjHgdWZpTsVBWH
Document that all roles share STORAGE_KEYS.PEER_ID and that
GUEST_PEER_ID / saveGuestPeerId / loadGuestPeerId must not be re-added.

https://claude.ai/code/session_01V93TGVeBjjHgdWZpTsVBWH
@wmxscott wmxscott merged commit fd00cea into main Mar 23, 2026
2 checks passed
@wmxscott wmxscott deleted the claude/implement-phase-6-uOUBo branch March 23, 2026 22:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants