Wallet access is read-only
- The EVM layer (wagmi + viem) is configured with connect and
signMessageonly. No transaction, approval or typed-data signing path exists in the codebase. - The Solana bridge exposes connect, signMessage and disconnect for Phantom and Solflare. Nothing else.
- Watch-only addresses can be added without any signature; their assets are labelled “source-linked” rather than “verified”.
Sign-in challenge
A challenge is issued by POST /api/auth/wallet/nonce, stored with a 10-minute expiry, and consumed on first use. The server re-derives the expected statement and rejects any signature over a different message. EVM signatures are verified with viem; Solana signatures are verified as Ed25519 with node:crypto.
Encryption of private fields
- Algorithm: AES-256-GCM with a random 96-bit IV per value and a 128-bit authentication tag. Implemented with
node:crypto— no custom cryptography. - Key:
WINGSY_ENCRYPTION_KEY(32 bytes). If unset, a key is derived fromAUTH_SECRETwith SHA-256 and a domain-separation label. Set the dedicated key in production so secrets can rotate independently. - Additional authenticated data binds each ciphertext to the owning user and column. Copying a value between rows fails to decrypt.
- Serial numbers are searchable through an HMAC blind index — exact match only, no decryption needed.
Authorization
Every private handler calls requireUser() and scopes the query with the session user id. Objects are looked up with where: { id, userId }, so an id that belongs to someone else is indistinguishable from a missing row.
Transport and headers
- Session cookies are httpOnly, SameSite=Lax and Secure in production.
- Private routes and API responses are served with Cache-Control: private, no-store.
- X-Content-Type-Options, X-Frame-Options, Referrer-Policy and Permissions-Policy headers are set globally.
- Robots are told not to index /app, /asset and /api.
Reporting
Found a problem? Open an issue on the repository or contact the maintainers privately before disclosure.