Skip to content

fix(sync): the requesting device must never show an approval prompt (+ backfill legacy initialized flag)#12

Merged
khokonm merged 1 commit into
mainfrom
fix/sync-requester-never-approves
Jun 25, 2026
Merged

fix(sync): the requesting device must never show an approval prompt (+ backfill legacy initialized flag)#12
khokonm merged 1 commit into
mainfrom
fix/sync-requester-never-approves

Conversation

@khokonm

@khokonm khokonm commented Jun 25, 2026

Copy link
Copy Markdown
Contributor

Symptom (after #11)

The brand-new device that's requesting to join also shows an approve/deny prompt — i.e. it appears to approve its own join. Both devices end up prompting each other.

Root cause

Two issues, both stemming from the per-chain initialized flag introduced in #11:

  1. Legacy devices have no flag. The existing device (the origin) was paired before fix(sync): classify transfer-need by explicit join intent + auto-request source so approval prompt appears #11, so it has no notes-sync-initialized-room entry. After fix(sync): classify transfer-need by explicit join intent + auto-request source so approval prompt appears #11 it is therefore treated as "not initialized" → it declares needsTransfer: true → it auto-requests a transfer too. With both devices requesting, the server relays a transfer-requested to each, so the new (empty) device is shown an approval prompt for a join it can't possibly satisfy.
  2. No guard against an empty device approving. A device that itself still needs its initial copy has no notes to send, yet nothing stopped it from being shown the prompt — including for its own request.

(A fresh device starts with zero notes — confirmed in useNotes.loadNotes — so "holds notes" is a reliable signal here, unlike the earlier hasData attempt where the test device wasn't actually empty.)

Fix

  1. Legacy backfill (one-time). On startup, a device paired before the flag existed is grandfathered as initialized iff it already holds notes (it's the source of truth for the chain and must never request a transfer). Empty devices are left to request normally. Gated by a marker so it runs at most once and never re-grandfathers a device that legitimately needs a transfer.
  2. Hard guards (belt-and-suspenders), independent of any flag state:
    • A device that still needs an initial transfer ignores incoming transfer-requested messages — it has nothing to send, so it never shows an approval prompt.
    • A device never approves its own request (requesterId === self).
    • A device never requests a transfer from itself (client guard + server rejects a self-targeted request-transfer-from).

Net effect for the reported case: the existing device (has notes) is correctly the approver and stops auto-requesting; the new empty device is the requester and is never shown an approve/deny prompt.

Files changed

  • src/utils/sync.tsmigrateInitFlagIfNeeded() (run in startSync); ignore transfer-requested while we still need a transfer or when it's our own; never request from self.
  • server/src/index.ts — reject a request-transfer-from whose target is the requester itself.

Verification

  • Walked the reported scenario: existing device (has notes) → backfilled initializedneedsTransfer:false → no longer auto-requests, shows the prompt; new empty device → needsTransfer:true → requests, and the guard means it never shows a prompt. Both devices prompting each other is no longer possible.
  • Not run locally: tsc/build/lint — sandbox has no installed toolchain for either workspace and installs are blocked (registry 403). Changes use only existing in-scope symbols.

Manual test plan

  1. On your existing devices (already paired): just reload the app on the device that has your notes — it should show Connected and NOT ask to sync.
  2. On the new (empty) device, open the app with both online → the existing device shows the approve/deny prompt; the new device does not.
  3. Approve → new device receives notes, shows Connected. Reload new device → stays Connected (doesn't re-ask).
  4. Fresh pair from scratch (clear the new device first): generate on A, enter on B → only A is prompted; B never shows an approval dialog.

…ed flag for legacy devices

After #11, the existing origin device had no per-chain initialized flag (it was
paired before the flag existed), so it was treated as needsTransfer=true and
auto-requested too — causing the brand-new requesting device to be shown an
approve/deny prompt for its own join.

Fixes:
- One-time legacy backfill in startSync: a device that already holds notes is
  grandfathered as initialized (it's the chain's source of truth) so it stops
  auto-requesting. Empty devices still request normally. Marker-gated.
- Hard guards regardless of flag state: a device that still needs an initial
  transfer ignores transfer-requested (nothing to send), never approves its own
  request, and never requests a transfer from itself (client + server reject).
@khokonm khokonm merged commit c9883a8 into main Jun 25, 2026
3 checks passed
@khokonm khokonm deleted the fix/sync-requester-never-approves branch June 25, 2026 09:43
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.

1 participant