Skip to content

feat: fetch + consume device prekey bundle — GET /users/:userId/devices/:deviceId/key-bundle#276

Open
Andreschuks101 wants to merge 1 commit into
codebestia:devfrom
Andreschuks101:feature/160-key-bundle
Open

feat: fetch + consume device prekey bundle — GET /users/:userId/devices/:deviceId/key-bundle#276
Andreschuks101 wants to merge 1 commit into
codebestia:devfrom
Andreschuks101:feature/160-key-bundle

Conversation

@Andreschuks101

Copy link
Copy Markdown

Fetch + consume prekey bundle — GET /users/:userId/devices/:deviceId/key-bundle

closes #160

Serves a recipient device's public prekey bundle and consumes one one-time prekey
per fetch, so a sender can start an X3DH/Signal-style encrypted session.

Changes

  • Schema/migration: devices (identity public key, registrationId, signed
    prekey + signature, revokedAt) and one_time_pre_keys (consumed flag,
    unique (device_id, key_id), (device_id, consumed) index). Migration
    0007_device_key_bundles. Only public key material is stored.
  • Service fetchAndConsumeKeyBundle: looks up the device, returns 404 if it is
    unknown or revoked, and otherwise returns
    { identityPublicKey, registrationId, signedPreKey, oneTimePreKey | null }.
  • Route: GET /users/:userId/devices/:deviceId/key-bundle, authenticated.

Atomic, race-free consumption

The one-time prekey is claimed in a single statement:

UPDATE one_time_pre_keys SET consumed = true
WHERE id = (SELECT id FROM one_time_pre_keys
            WHERE device_id = $1 AND consumed = false
            ORDER BY key_id FOR UPDATE SKIP LOCKED LIMIT 1)
RETURNING key_id, public_key

FOR UPDATE SKIP LOCKED ensures concurrent senders skip a row another transaction
is already claiming, so the same one-time prekey can never be issued twice. When
the pool is exhausted the bundle is still returned with oneTimePreKey: null.

Acceptance criteria

  • One-time prekey consumption is atomic and race-free under concurrent fetches.
  • Returns oneTimePreKey: null gracefully when the pool is exhausted.
  • Revoked (and unknown) devices return 404.
  • No private key material is ever stored or returned.

Testing

  • New service and route tests: atomic/concurrent distinct-OTP behaviour, graceful
    null on exhaustion, 404 for unknown and revoked devices, and a "no private
    material" shape assertion.
  • Full backend suite passes; lint and prettier clean.

@Andreschuks101 Andreschuks101 changed the base branch from main to dev June 28, 2026 22:08
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.

GET /users/:userId/devices/:deviceId/key-bundle — fetch + consume prekey bundle

1 participant