Skip to content

feat: pluggable Store trait for custom storage backends#184

Open
lewiscasewell wants to merge 1 commit intoopen-wallet-standard:mainfrom
lewiscasewell:feat/pluggable-store
Open

feat: pluggable Store trait for custom storage backends#184
lewiscasewell wants to merge 1 commit intoopen-wallet-standard:mainfrom
lewiscasewell:feat/pluggable-store

Conversation

@lewiscasewell
Copy link
Copy Markdown

@lewiscasewell lewiscasewell commented Apr 4, 2026

Summary

Introduces a minimal key-value Store trait so OWS can work with any storage backend — not just the filesystem. This enables React Native (Keychain/SecureStore), browser (IndexedDB), server (database/Redis), and in-memory (testing) use cases.

The trait is 3 required methods:

trait Store: Send + Sync {
    fn get(&self, key: &str) -> Result<Option<String>, StoreError>;
    fn set(&self, key: &str, value: &str) -> Result<(), StoreError>;
    fn remove(&self, key: &str) -> Result<(), StoreError>;
    fn list(&self, prefix: &str) -> Result<Vec<String>, StoreError>; // optional, has default
}

The library owns key naming (wallets/{id}, keys/{id}, policies/{id}) and serialization. The store just moves strings by key.

No breaking changes. All 33 existing vault_path: Option<&Path> functions remain with identical signatures. They become thin wrappers around new _with_store variants using FsStore. Default behavior (None~/.ows/) is unchanged.

What's included

  • ows-core: Store trait, StoreError, InMemoryStore, index helpers (store_set_indexed/store_remove_indexed)
  • ows-lib: FsStore (filesystem impl extracted from vault/key_store/policy_store), _with_store variants for all 17 public ops functions
  • Node binding: createOws() factory accepting a JS store object { get, set, remove, list? } via raw NAPI bridge
  • Python binding: OWS class accepting a Python store object

Usage

// Existing API unchanged
const wallet = createWallet('my-wallet');

// New: custom store
const ows = createOws({
  store: {
    get: (key) => SecureStore.getItem(key),
    set: (key, value) => SecureStore.setItem(key, value),
    remove: (key) => SecureStore.deleteItem(key),
  }
});
const wallet = ows.createWallet('my-wallet');

Motivation

We're looking at using OWS as the wallet layer in a React Native app, replacing a JS-based wallet stack (bip39 + viem + CryptoJS + per-chain signing libs). The pluggable store is what makes this possible — the app provides Keychain/Keystore-backed storage while OWS handles all the crypto in Rust.

Test plan

  • 53 new Rust tests (trait, InMemoryStore, index helpers, FsStore, characterization, store-aware CRUD, ops _with_store)
  • 5 Node integration tests (create/list/export/sign/index-only with JS Map store)
  • All 243 existing tests pass unchanged
  • Both Node and Python bindings compile clean
  • cargo test --workspace — 577 tests, 0 failures

🤖 Generated with Claude Code


Note

Medium Risk
Refactors persistence and key/policy/wallet CRUD to route through a new Store abstraction and adds cross-language store bridges; bugs here could affect wallet/key discovery (indexing) and data durability, though defaults remain filesystem-backed.

Overview
Introduces a new pluggable Store abstraction in ows-core (plus index helpers and an InMemoryStore) so wallets, policies, and API keys can be persisted to non-filesystem backends.

Refactors ows-lib operations and storage modules to add _with_store variants for wallet/key/policy CRUD and signing flows, with existing vault-path APIs now delegating to a new FsStore implementation (including stricter UNIX perms for sensitive data).

Extends Node and Python bindings to accept custom store objects: Node adds createOws({ store, vaultPath }) backed by a NAPI JsStore bridge (with index-based fallback when list is missing), Python adds an OWS class backed by a PyStore; new Node integration tests cover custom-store create/list/export/sign behavior.

Reviewed by Cursor Bugbot for commit 55d97b1. Bugbot is set up for automated code reviews on this repo. Configure here.

Introduce a minimal key-value Store trait (get/set/remove + optional list)
so OWS can work with any storage backend — not just the filesystem.

This enables React Native (Keychain/SecureStore), browser (IndexedDB),
server (database/Redis), and in-memory (testing) backends without
modifying the core wallet, signing, or policy logic.

- Add Store trait, StoreError, InMemoryStore, and index helpers to ows-core
- Add FsStore in ows-lib (extracts filesystem logic from vault/key_store/policy_store)
- Add _with_store variants for all 17 public ops/key_ops functions
- Original vault_path signatures unchanged (thin wrappers over _with_store)
- Node binding: createOws() factory with JS callback store bridge
- Python binding: OWS class accepting a Python store object
- 53 new Rust tests + 5 Node integration tests, all 243 existing tests pass

No breaking changes to existing API.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@lewiscasewell lewiscasewell requested a review from njdawn as a code owner April 4, 2026 09:49
@vercel
Copy link
Copy Markdown

vercel bot commented Apr 4, 2026

@lewiscasewell is attempting to deploy a commit to the MoonPay Team on Vercel.

A member of the Team first needs to authorize it.

Copy link
Copy Markdown

@cursor cursor bot left a comment

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 2 potential issues.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 55d97b1. Configure here.

.ok()
.and_then(|v| v.coerce_to_string().ok())
.and_then(|s| s.into_utf8().ok())
.and_then(|u| u.as_str().ok().map(|s| s.to_string()));
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

vaultPath coercion turns undefined into literal string

Medium Severity

When createOws({}) is called with an empty options object (or any object without store or vaultPath), get_named_property::<JsUnknown>("vaultPath") returns Ok(undefined) (NAPI returns undefined for non-existent properties, not an error). Then coerce_to_string() converts JavaScript undefined to the literal string "undefined". This results in FsStore using a relative path ./undefined/ as the vault directory instead of the default ~/.ows/. The same issue occurs with null values. Wallets would be silently stored in the wrong location and existing wallets become invisible.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 55d97b1. Configure here.

if let Ok(w) = serde_json::from_str::<EncryptedWallet>(&json) {
return Ok(w);
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Direct key lookup enables path traversal in FsStore

Low Severity

The new load_wallet_by_name_or_id_with_store adds a "fast path" direct lookup using format!("wallets/{name_or_id}") where name_or_id is user-supplied. For FsStore, this maps to a filesystem path via key_to_path, so a crafted input like ../../etc/secret would resolve to a file outside the vault directory. The original load_wallet_by_name_or_id didn't have this issue because it listed all wallets first and searched in-memory.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 55d97b1. Configure here.

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