Security updates and patches are provided exclusively for the latest stable release on the main branch of Chenpilot Client (gear5labs/chenpilot-client).
| Version | Supported |
|---|---|
main |
✅ |
< 1.0 |
❌ |
If you discover a security vulnerability within Chenpilot Client, please do not open a public GitHub issue. Public disclosures can put users and wallet assets at risk before a patch is available.
- Private Reporting Channel: Please report vulnerabilities via GitHub Security Advisories (Private Vulnerability Reporting) or by emailing our security team at
security@gear5labs.example(replace with official contact). - Expected Response Time: We acknowledge receipt of vulnerability reports within 24 to 48 hours, and provide an initial remediation assessment within 5 business days.
To maintain the security of credentials, API keys, and user assets, all contributors must adhere to the following rules:
.envFile Handling:- Never commit
.env,.env.local, or any file containing actual secrets to version control. - Always use
.env.exampleas the canonical template containing safe placeholder values.
- Never commit
- Issue-Publishing & Release Tokens:
- The automated release and publishing script reads
GITHUB_TOKENlocally from your un-tracked.envor.env.local. - Never hardcode personal access tokens or API keys into source files or commit histories.
- The automated release and publishing script reads
- Log Redaction:
- Ensure that logging utilities never output sensitive data such as authentication headers, bearer tokens, private keys, session cookies, or user PII.
- Testnet-Only Practices & Credential Safeguards:
- Mainnet Prohibited: Never commit, configure, or use mainnet secret keys (
S...), seed phrases, or production wallet credentials in this repository. - Testnet-Only Test Data: All automated tests, fixtures, and mock seeds must target the Stellar Testnet exclusively.
- Deterministic Test Mocking: Network requests in unit and E2E suites must use deterministic mocks (
vi.mock('axios')or Playwright route interception). Do not rely on live network credentials during automated testing.
- Mainnet Prohibited: Never commit, configure, or use mainnet secret keys (
Because Chenpilot Client signs and prepares transactions:
- Transaction Confirmation UX:
- The user must always be presented with an explicit confirmation step before any transaction or smart contract call is submitted.
- The confirmation UI must clearly state the recipient address, asset code, exact numerical amount, estimated fee, and network type (Testnet).
- Precision & Overflow Safeguards:
- Stellar amounts must respect 7-decimal precision (1 stroop =
0.0000001XLM). - Floating-point arithmetic must never be used for financial accounting or transaction construction. Amounts must be represented as strings or BigInt integers to prevent truncation and precision exploits.
- Stellar amounts must respect 7-decimal precision (1 stroop =
Because Chenpilot Client operates as a web-based client workspace and wallet application:
- Storage Profile: Sensitive authentication tokens and session states are stored in browser storage mechanisms (
localStorage/ encrypted cookies). - Security Implications: Stored tokens are susceptible to theft if the client environment is compromised via Cross-Site Scripting (XSS). Contributors must maintain rigorous Content Security Policies (CSP), sanitize all user inputs, and keep third-party dependencies updated.
- Contributing Guidelines & Wallet Flow Checklist (
CONTRIBUTING.md) - Backend API Contract (
docs/API_CONTRACT.md) - Testing & Quality Guide (
docs/TESTING.md)
These are all the keys the client reads or writes. The source of truth is STORAGE_INVENTORY in src/services/storageInventory.ts. storageInventory.test.ts fails if code uses a key that isn't listed there, so add new keys to both the inventory and this table.
| Key | Area | Owner | Lifetime | Cleared on logout | Sensitivity | Contents |
|---|---|---|---|---|---|---|
auth_token |
localStorage | services/api.ts, store/slices/authSlice.ts |
Until logout, refresh failure or token rejection | ✅ | Credential | Bearer JWT |
user_data |
localStorage | store/slices/authSlice.ts |
Until logout or refresh failure | ✅ | Personal | Minimized profile: id, name, email, account flags. No wallet address or public key (see below) |
session_expiration |
localStorage | services/sessionService.ts |
Until logout or session end | ✅ | Personal | Expiry timestamp for cross-tab session sync |
refresh_token |
localStorage | services/api.ts |
Legacy: never written, only removed | ✅ | Credential | Refresh tokens are HTTP-only cookies |
chat_history |
localStorage | store/slices/chatSlice.ts |
Until logout or chat deletion | ✅ | Personal | Messages per conversation; prompts can include wallet addresses and amounts |
conversations |
localStorage | store/slices/chatSlice.ts |
Until logout or conversation deletion | ✅ | Personal | Conversation titles and metadata |
messageQueue |
localStorage | store/slices/chatSlice.ts |
Until sent after reconnecting, or logout | ✅ | Personal | Prompts queued while offline |
chat_drafts |
localStorage | lib/accountDeletion.ts |
Until logout or account deletion | ✅ | Personal | Unsent chat drafts |
mfa_secret |
localStorage + sessionStorage | services/auth.service.ts |
Never written; removed defensively when MFA setup is cancelled | ✅ | Credential | Must never hold an MFA secret |
mfa_recovery_codes |
localStorage + sessionStorage | services/auth.service.ts |
Never written; removed defensively when MFA setup is cancelled | ✅ | Credential | Must never hold MFA recovery codes |
theme |
localStorage | store/slices/uiSlice.ts, ThemeProvider |
Persistent | ❌ | Preference | light / dark |
language |
localStorage | store/slices/uiSlice.ts, lib/i18n |
Persistent | ❌ | Preference | UI language code |
| (all keys) | sessionStorage | none currently | Tab | ✅ (sessionStorage.clear()) |
— | — |
STORAGE_KEYS.SIDEBAR_STATE and STORAGE_KEYS.USER_PREFERENCES in src/constants are declared but never used.
Logout cleanup. clearUserScopedStorage() removes every user-scoped key above and clears sessionStorage. Both apiService.logout() and the logout thunk call it, and chatSlice resets in-memory chat state on logout.fulfilled. Device preferences (theme, language) are kept. Before this audit, chat history, conversations and the offline queue survived logout, so the next person using a shared browser could read them.
Reduced persistence. user_data used to store the full User, including the wallet address and publicKey. It now stores only toPersistedUser(user). Wallet identifiers are restored as empty strings and re-fetched from GET /auth/me / fetchProfile (the settings page fetches them on mount).
Rules:
- Never persist secrets, private keys, seed phrases or passwords (see "Wallet Key Ownership & Signing Boundary").
- Anything tied to a user must be marked
userScoped: true, so logout removes it. - Store the minimum needed to restore the UI; re-fetch the rest from the API.
This section describes what the browser client actually does today. Re-check it against the code whenever a flow that touches keys, signing or funds changes. src/security/signingBoundary.test.ts fails if signing code or secret-key handling is added to the client without updating this policy.
| Asset / operation | Browser client (this repo) | Backend |
|---|---|---|
| Account secret key / private key / seed | Never generated, received, stored or logged. The client has no signing library (@stellar/stellar-sdk Keypair, starknet signers, wallet extensions) as a dependency. |
Generates and custodies keys for user accounts created at registration. |
| Public key / account address | Received from POST /auth/register, GET /auth/me and GET /auth/starknet/status; displayed and sent back as an identifier. |
Source of truth. |
| Transaction building & signing | None. | Builds, signs and submits every transaction. |
| Agent actions (transfer, swap, lend, borrow, ...) | Sends the natural-language prompt to POST /query and renders the result. result.metadata.requiresConfirmation is display-only; the client does not sign or co-sign. |
Interprets the intent, enforces limits, signs and submits. |
| Account deployment | Calls POST /auth/starknet/deploy. |
Deploys and signs. |
| Account funding | Calls POST /auth/funding/* and POST /api/fund. Testnet only: when the backend funding call fails, requestAccountFunding calls Stellar Friendbot directly with the public key (no secret involved). |
Funds from its own accounts. |
| Session credential | Bearer JWT in localStorage (auth_token), plus refresh/logout cookies sent with withCredentials. |
Issues, validates and revokes tokens. |
Every check the client makes exists only for UX. None of them is a security control, and the backend must enforce each one itself:
- route guards and
isAuthenticatedchecks - form validation (password strength, email format)
- the agent's supported-actions list
- amount and asset checks
- hiding or disabling buttons
- the retry/status checks in chat
An attacker controls the browser and can call any API directly with a valid token, so the backend must authorize every request on its own: ownership of the account, limits, and that the intent was confirmed.
- Do not add code that creates, imports, derives, stores or transmits a secret key, private key or seed phrase in the browser. If client-side signing is ever needed (e.g. a wallet extension), it needs a design review and an update to this section first.
- Treat the bearer token as the user's signing authority: anyone holding it can ask the backend to move funds. Never log it, put it in URLs, or send it to a host other than the configured API (see "Log Redaction" above and
src/utils/logger.ts). - Agent output is untrusted input. Never turn it into a signing request, navigation or script on the client.
- Any new endpoint that changes on-chain state must be documented in
docs/API_CONTRACT.mdwith the backend's authorization rule.
- After a terminal auth error (an HTTP 4xx other than 408/429, or an unclassified failure), the login and registration forms clear the password field and focus it. Email and name are kept so the user only re-enters the password.
- After a transient error (network failure, timeout, 408, 429, 5xx), the password is kept so the user can resubmit.
- The password field is also cleared when the form unmounts (client-side navigation), on
pagehide, and when the page is restored from the back/forward cache (pageshowwithpersisted). A submitted password is therefore never restored into the form. autocompleteis not disabled (current-passwordfor login,new-passwordfor registration), so password managers keep working.- Implementation:
src/components/auth/passwordHygiene.ts; tests insrc/components/auth/passwordHygiene.test.tsx.