Skip to content

SecureStorageAdapter: replace JSON KEY_INDEX with per-key markers to remove RMW race #6

Description

@LiranCohen

Summary

src/lib/enbox/storage-adapter.ts tracks the set of stored keys in a single JSON-encoded list under `enbox:keys`. Every `set()` and `remove()` does a read-modify-write on that list (trackKey / untrackKey), so concurrent in-process writes race on the index — entries that landed between a read and the matching write are lost.

The header comment of the file already documents the caveat and proposes a per-key marker pattern as the long-term fix. This issue tracks that work.

Current Impact

  • clear() iterates the (possibly drifted) index, so dropped index entries leak past a `clear()` call.
  • Individual get / set / remove on the actual key path are unaffected.
  • useAgentStore.reset() works around the race by serializing all SecureStorage writes (sequential for loop, never Promise.all). Every other caller that does parallel writes is at risk.

Proposed Direction

  • Replace the JSON list with per-key marker keys (e.g. enbox:__index__:<key>'1').
  • set() becomes two single-key writes; remove() becomes two single-key deletes; both are idempotent and free of read-modify-write.
  • clear() either:
    1. Enumerates marker keys via a native enumeration API where available, or
    2. Reads each marker by deterministic prefix (requires native support) — failing that, keep a separate enumeration channel that only the cleanup path consumes.

Acceptance Criteria

  • No path in SecureStorageAdapter performs a read-modify-write on a shared list.
  • clear() reliably removes every key written via set(), even under concurrent set / remove workloads.
  • Migration path for existing installs that already wrote a JSON KEY_INDEX (read once on first run, convert, delete the legacy entry).
  • Tests cover concurrent writes against a fake native backend.

Out of Scope

  • Changing the enbox: key prefix.
  • Replacing SecureStorageAdapter with a different abstraction.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions