Rename project from Nexa to Osias

Renames the npm package (nexa-extension -> osias-extension), the
extension manifest name and Firefox gecko id
([email protected]), the internal runtime message-type
namespace and storage keys ('nexa:...' -> 'osias:...'), the
NexaMessage type, and all remaining Nexa branding in source comments,
UI copy, and CLAUDE.md to match the new osias.trade domain and the
osias-trade GitHub org.

No wire-protocol changes — purely internal naming plus user-facing
strings.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Tn7uDYjTuZEbLPwpEiSCUw
This commit is contained in:
2026-09-08 12:32:53 +02:00
co-authored by claude
parent ca66c2e1b3
commit 59157c7e76
21 changed files with 129 additions and 129 deletions
+9 -9
View File
@@ -22,7 +22,7 @@ There is no lint/test setup yet — add one when the project needs it rather tha
## What this project is ## What this project is
Nexa is a blockchain-powered trading companion focused on the emotional/behavioral side of trading, not the trades themselves. Trades are already visible on-chain, so Nexa observes wallet activity rather than requiring manual logging. This repo is the **open-source frontend**: a browser extension. The backend (wallet/RPC listening via Solana/Helius, loss detection, auth, WebSocket push) is a separate private service and is out of scope here — treat it as an external API. Osias is a blockchain-powered trading companion focused on the emotional/behavioral side of trading, not the trades themselves. Trades are already visible on-chain, so Osias observes wallet activity rather than requiring manual logging. This repo is the **open-source frontend**: a browser extension. The backend (wallet/RPC listening via Solana/Helius, loss detection, auth, WebSocket push) is a separate private service and is out of scope here — treat it as an external API.
License: Apache 2.0. The backend stays closed-source; this frontend repo is the OSS component. License: Apache 2.0. The backend stays closed-source; this frontend repo is the OSS component.
@@ -93,7 +93,7 @@ Note the naming mismatch with the wire protocol below: the frontend's internal `
- `src/background/wallet-auth.ts` — `handleWalletConnected()` runs the REST auth flow (`POST /auth/nonce` → Phantom signature → `POST /auth/verify` → session token) once a content script reports a connected wallet. Also `requestWalletReconnect()` (silent reconnect after the backend invalidates a session), `requestWalletDisconnect()` (sign-out), `requestAccountSwitch()` (explicit account switch) — all three just message whichever tabs are on a supported site; `wallet-connect.ts` in the content script does the actual work. - `src/background/wallet-auth.ts` — `handleWalletConnected()` runs the REST auth flow (`POST /auth/nonce` → Phantom signature → `POST /auth/verify` → session token) once a content script reports a connected wallet. Also `requestWalletReconnect()` (silent reconnect after the backend invalidates a session), `requestWalletDisconnect()` (sign-out), `requestAccountSwitch()` (explicit account switch) — all three just message whichever tabs are on a supported site; `wallet-connect.ts` in the content script does the actual work.
- `src/background/ws-client.ts` — the WS client described above: connects to `/ws?token=...`. `connectWsClient()` returns a controller with `reconnectNow()` so the background script can short-circuit the backoff wait right after a fresh token arrives. Auth failures (`4001` close, `auth_expired`/`session_revoked` errors) do **not** auto-retry with backoff — they call `onAuthExpired()` instead, since retrying with a known-bad token can't succeed; only real disconnects (network drop, backgrounded browser) use the protocol's suggested backoff schedule. - `src/background/ws-client.ts` — the WS client described above: connects to `/ws?token=...`. `connectWsClient()` returns a controller with `reconnectNow()` so the background script can short-circuit the backoff wait right after a fresh token arrives. Auth failures (`4001` close, `auth_expired`/`session_revoked` errors) do **not** auto-retry with backoff — they call `onAuthExpired()` instead, since retrying with a known-bad token can't succeed; only real disconnects (network drop, backgrounded browser) use the protocol's suggested backoff schedule.
- `src/background/connection-status.ts` — separate from lock state; the popup surfaces this (connecting/connected/disconnected/auth-error) alongside the lock state so a broken connection isn't silently indistinguishable from "unlocked". - `src/background/connection-status.ts` — separate from lock state; the popup surfaces this (connecting/connected/disconnected/auth-error) alongside the lock state so a broken connection isn't silently indistinguishable from "unlocked".
- `src/background/settings-client.ts` — `GET`/`PATCH /me/settings` (per-user loss-detection thresholds; see backend/CLAUDE.md, "Account settings"), same direct-fetch-with-bearer-token shape as `wallet-auth.ts`. Popup -> background dispatch is `nexa:get-settings`/`nexa:update-settings` in `background.ts`, responses shaped `{ settings } | { error }` (`SettingsResult` in `shared/messaging.ts`). The popup's "Loss thresholds" panel (`entrypoints/popup/App.tsx`) is the only place percent fields (`near_full_exit_fraction` and both loss thresholds) get converted between the backend's fractional wire representation and the whole-number percentages shown in the form — same "convert only at the seam" rule as `LockScope`'s kebab/snake split below. - `src/background/settings-client.ts` — `GET`/`PATCH /me/settings` (per-user loss-detection thresholds; see backend/CLAUDE.md, "Account settings"), same direct-fetch-with-bearer-token shape as `wallet-auth.ts`. Popup -> background dispatch is `osias:get-settings`/`osias:update-settings` in `background.ts`, responses shaped `{ settings } | { error }` (`SettingsResult` in `shared/messaging.ts`). The popup's "Loss thresholds" panel (`entrypoints/popup/App.tsx`) is the only place percent fields (`near_full_exit_fraction` and both loss thresholds) get converted between the backend's fractional wire representation and the whole-number percentages shown in the form — same "convert only at the seam" rule as `LockScope`'s kebab/snake split below.
### Wallet auth (Phantom) — implemented ### Wallet auth (Phantom) — implemented
@@ -101,20 +101,20 @@ Real wallet signing, not a stub keypair. Phantom (and any wallet injecting a com
- `src/entrypoints/wallet-bridge.content.ts` + `src/content-scripts/wallet-bridge/inject.ts` — a **second, `world: 'MAIN'`** content script on axiom.trade (Manifest V3 native main-world injection — no `web_accessible_resources`/dynamic `<script>` tag needed). This is the only file that touches `window.solana`/`window.phantom.solana` directly: `connect()` and `signMessage()`. Talks back to the isolated world via `window.postMessage` (channel-tagged messages, matched by request id) — **not `CustomEvent`**, see the callout below. - `src/entrypoints/wallet-bridge.content.ts` + `src/content-scripts/wallet-bridge/inject.ts` — a **second, `world: 'MAIN'`** content script on axiom.trade (Manifest V3 native main-world injection — no `web_accessible_resources`/dynamic `<script>` tag needed). This is the only file that touches `window.solana`/`window.phantom.solana` directly: `connect()` and `signMessage()`. Talks back to the isolated world via `window.postMessage` (channel-tagged messages, matched by request id) — **not `CustomEvent`**, see the callout below.
- `src/content-scripts/wallet-bridge/relay.ts` — isolated-world side of that bridge: `callWallet(action, payload)` posts the request message and returns a promise that resolves/rejects on the matching result message, with a 30s timeout. - `src/content-scripts/wallet-bridge/relay.ts` — isolated-world side of that bridge: `callWallet(action, payload)` posts the request message and returns a promise that resolves/rejects on the matching result message, with a 30s timeout.
- `src/content-scripts/wallet-bridge/banner.ts` — minimal plain-DOM "Connect your wallet to Nexa" prompt injected onto the page (bottom-right, fixed position) when a wallet isn't already trusted for this origin. No framework, kept deliberately tiny since it's living on someone else's page. - `src/content-scripts/wallet-bridge/banner.ts` — minimal plain-DOM "Connect your wallet to Osias" prompt injected onto the page (bottom-right, fixed position) when a wallet isn't already trusted for this origin. No framework, kept deliberately tiny since it's living on someone else's page.
- `src/content-scripts/wallet-connect.ts` — orchestrates the above, wired into `content-index.ts` (runs independently of site-adapter matching): on load, tries `connect({ onlyIfTrusted: true })` silently (succeeds with no user interaction if the user already approved this origin in Phantom before); on failure, shows the banner and only calls plain `connect()` from the banner button's own click handler, since **a real user gesture is required for Phantom to show its approval popup on a first-ever connect** — this is why the banner exists in the page rather than the extension popup (a click in the popup's UI doesn't count as a gesture on the axiom.trade page by the time it reaches the wallet, since it crosses an extension-messaging boundary asynchronously). Once connected, reports `{ walletAddress }` to the background via `nexa:wallet-connected`. Also handles, all from the background: `nexa:wallet-sign-request` (sign a nonce), `nexa:request-wallet-connect` (retry the silent connect, e.g. after a session was invalidated), `nexa:wallet-disconnect-request` (sign-out) and `nexa:switch-account-request` (explicit account switch) — see the sign-out/switch-account bullet below for those two. - `src/content-scripts/wallet-connect.ts` — orchestrates the above, wired into `content-index.ts` (runs independently of site-adapter matching): on load, tries `connect({ onlyIfTrusted: true })` silently (succeeds with no user interaction if the user already approved this origin in Phantom before); on failure, shows the banner and only calls plain `connect()` from the banner button's own click handler, since **a real user gesture is required for Phantom to show its approval popup on a first-ever connect** — this is why the banner exists in the page rather than the extension popup (a click in the popup's UI doesn't count as a gesture on the axiom.trade page by the time it reaches the wallet, since it crosses an extension-messaging boundary asynchronously). Once connected, reports `{ walletAddress }` to the background via `osias:wallet-connected`. Also handles, all from the background: `osias:wallet-sign-request` (sign a nonce), `osias:request-wallet-connect` (retry the silent connect, e.g. after a session was invalidated), `osias:wallet-disconnect-request` (sign-out) and `osias:switch-account-request` (explicit account switch) — see the sign-out/switch-account bullet below for those two.
- **Known gap**: this only works while an axiom.trade tab is open — there's no wallet connection path from the popup alone. That's intentional for now (matches the "only live while on a supported site" framing in backend/CLAUDE.md's RPC-subscription note), not an oversight. - **Known gap**: this only works while an axiom.trade tab is open — there's no wallet connection path from the popup alone. That's intentional for now (matches the "only live while on a supported site" framing in backend/CLAUDE.md's RPC-subscription note), not an oversight.
- **Sign out and account switching (implemented) — both explicit user actions, not automatic.** An earlier version of this also wired Phantom's own `accountChanged` provider event to trigger re-auth automatically, but calling `connect()` ourselves also fires that same event — so a normal silent reconnect raced its own event-triggered handler and produced *two* competing "wallet connected" reports, each independently asking Phantom to sign a nonce (a popup every time). Removed entirely; `wallet-bridge/inject.ts` does not listen for any Phantom provider events, only responds to our own explicit calls. See its doc comment for the full story if this is ever reconsidered. - **Sign out and account switching (implemented) — both explicit user actions, not automatic.** An earlier version of this also wired Phantom's own `accountChanged` provider event to trigger re-auth automatically, but calling `connect()` ourselves also fires that same event — so a normal silent reconnect raced its own event-triggered handler and produced *two* competing "wallet connected" reports, each independently asking Phantom to sign a nonce (a popup every time). Removed entirely; `wallet-bridge/inject.ts` does not listen for any Phantom provider events, only responds to our own explicit calls. See its doc comment for the full story if this is ever reconsidered.
- "Sign out" (popup button → `nexa:sign-out` → `background.ts`'s `signOut()`) clears the stored session, force-closes the WS connection (`ws-client.ts`'s `disconnect()`, distinct from `reconnectNow()` — it also suppresses auto-reconnect until a new wallet connects), and asks the content script to call `provider.disconnect()`, which revokes Phantom's trust for the origin so the next silent connect correctly fails until the user reconnects. - "Sign out" (popup button → `osias:sign-out` → `background.ts`'s `signOut()`) clears the stored session, force-closes the WS connection (`ws-client.ts`'s `disconnect()`, distinct from `reconnectNow()` — it also suppresses auto-reconnect until a new wallet connects), and asks the content script to call `provider.disconnect()`, which revokes Phantom's trust for the origin so the next silent connect correctly fails until the user reconnects.
- "Switch account" (popup button → `nexa:switch-account` → `wallet-connect.ts`'s `switchAccount()`) explicitly disconnects then immediately reconnects, so Phantom shows its connect approval UI for whichever account is currently active there. Not fully verified whether Phantom requires a fresh user gesture for this non-`onlyIfTrusted` connect call relayed from the popup (vs. a direct page click) — if it does, `switchAccount()` falls back to the on-page banner so the user can complete it with a real click. - "Switch account" (popup button → `osias:switch-account` → `wallet-connect.ts`'s `switchAccount()`) explicitly disconnects then immediately reconnects, so Phantom shows its connect approval UI for whichever account is currently active there. Not fully verified whether Phantom requires a fresh user gesture for this non-`onlyIfTrusted` connect call relayed from the popup (vs. a direct page click) — if it does, `switchAccount()` falls back to the on-page banner so the user can complete it with a real click.
- `background.ts`'s `nexa:wallet-connected` handler distinguishes "already signed in as this wallet" (no-op) from "signed in as a *different* wallet" (re-authenticate) by comparing the reported wallet address against `backend-client.ts`'s stored `{ token, walletAddress }` pair (`storeSession()`, not just a bare token) — a bare "do we have a token" check can't tell those apart and was the reason the account-switch case needed fixing in the first place. - `background.ts`'s `osias:wallet-connected` handler distinguishes "already signed in as this wallet" (no-op) from "signed in as a *different* wallet" (re-authenticate) by comparing the reported wallet address against `backend-client.ts`'s stored `{ token, walletAddress }` pair (`storeSession()`, not just a bare token) — a bare "do we have a token" check can't tell those apart and was the reason the account-switch case needed fixing in the first place.
- **Firefox gotcha (hit during dev, now fixed)**: the isolated↔main-world bridge originally used `CustomEvent`s dispatched on `window`. That works on Chromium but throws `Uncaught Error: Permission denied to access property "id"` on Firefox — a `CustomEvent.detail` object created in one world can't have its properties read from the other (an Xray-wrapper security restriction specific to Firefox's extension model). Fixed by switching to `window.postMessage` for this bridge, which structured-clones its payload across the boundary correctly on both browsers — the same technique Phantom's own inpage↔content-script bridge uses. If you're extending this bridge, don't reach for `CustomEvent` again for isolated↔main-world data; `postMessage` (with a `channel` field to disambiguate from the page's own postMessage traffic, and an `event.source === window` check) is the pattern here. - **Firefox gotcha (hit during dev, now fixed)**: the isolated↔main-world bridge originally used `CustomEvent`s dispatched on `window`. That works on Chromium but throws `Uncaught Error: Permission denied to access property "id"` on Firefox — a `CustomEvent.detail` object created in one world can't have its properties read from the other (an Xray-wrapper security restriction specific to Firefox's extension model). Fixed by switching to `window.postMessage` for this bridge, which structured-clones its payload across the boundary correctly on both browsers — the same technique Phantom's own inpage↔content-script bridge uses. If you're extending this bridge, don't reach for `CustomEvent` again for isolated↔main-world data; `postMessage` (with a `channel` field to disambiguate from the page's own postMessage traffic, and an `event.source === window` check) is the pattern here.
- **Verified working end-to-end against real Phantom** on Zen: connect → sign → `/auth/verify` → session token stored, confirmed via the `[nexa/wallet-*]` debug logs. `world: 'MAIN'` also needs Firefox 128+; confirmed fine on Zen's base version. - **Verified working end-to-end against real Phantom** on Zen: connect → sign → `/auth/verify` → session token stored, confirmed via the `[osias/wallet-*]` debug logs. `world: 'MAIN'` also needs Firefox 128+; confirmed fine on Zen's base version.
- **A content script reports `wallet-connected` on every page load** (it always tries a silent `onlyIfTrusted` connect first), and `signMessage()` shows a fresh Phantom approval popup every single time it's called, unlike `connect()`, which is silent once trusted — so the re-auth gate on that message matters a lot for not spamming signature prompts. An earlier version of the gate only checked "is there any token stored," which correctly avoided re-signing on ordinary page loads but broke account switching (see above — it's now a wallet-address comparison instead). - **A content script reports `wallet-connected` on every page load** (it always tries a silent `onlyIfTrusted` connect first), and `signMessage()` shows a fresh Phantom approval popup every single time it's called, unlike `connect()`, which is silent once trusted — so the re-auth gate on that message matters a lot for not spamming signature prompts. An earlier version of the gate only checked "is there any token stored," which correctly avoided re-signing on ordinary page loads but broke account switching (see above — it's now a wallet-address comparison instead).
## WebSocket protocol (backend wire contract) ## WebSocket protocol (backend wire contract)
Single source of truth for the wire protocol between the Nexa backend (Rust/Axum) and this extension's background script. Backend and frontend repos both implement against this section — not against each other's internal types. If either side needs to change a message shape, this doc changes first. Single source of truth for the wire protocol between the Osias backend (Rust/Axum) and this extension's background script. Backend and frontend repos both implement against this section — not against each other's internal types. If either side needs to change a message shape, this doc changes first.
**Transport** **Transport**
- WebSocket, endpoint `/ws` on the backend. - WebSocket, endpoint `/ws` on the backend.
+2 -2
View File
@@ -1,11 +1,11 @@
{ {
"name": "nexa-extension", "name": "osias-extension",
"version": "0.1.0", "version": "0.1.0",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "nexa-extension", "name": "osias-extension",
"version": "0.1.0", "version": "0.1.0",
"hasInstallScript": true, "hasInstallScript": true,
"dependencies": { "dependencies": {
+2 -2
View File
@@ -1,8 +1,8 @@
{ {
"name": "nexa-extension", "name": "osias-extension",
"private": true, "private": true,
"version": "0.1.0", "version": "0.1.0",
"description": "Nexa browser extension frontend — your blockchain powered agent to help with your trading emotions.", "description": "Osias browser extension frontend — your blockchain powered agent to help with your trading emotions.",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "wxt", "dev": "wxt",
+1 -1
View File
@@ -1,6 +1,6 @@
import { browser } from 'wxt/browser'; import { browser } from 'wxt/browser';
const SESSION_STORAGE_KEY = 'nexa:session'; const SESSION_STORAGE_KEY = 'osias:session';
interface StoredSession { interface StoredSession {
token: string; token: string;
+2 -2
View File
@@ -1,6 +1,6 @@
import { browser } from 'wxt/browser'; import { browser } from 'wxt/browser';
import { DEFAULT_CONNECTION_STATUS, type ConnectionStatus } from '@/shared/types'; import { DEFAULT_CONNECTION_STATUS, type ConnectionStatus } from '@/shared/types';
import type { NexaMessage } from '@/shared/messaging'; import type { OsiasMessage } from '@/shared/messaging';
/** In-memory only (not persisted) — always reflects the live WS connection, not a stale guess. */ /** In-memory only (not persisted) — always reflects the live WS connection, not a stale guess. */
let currentStatus: ConnectionStatus = DEFAULT_CONNECTION_STATUS; let currentStatus: ConnectionStatus = DEFAULT_CONNECTION_STATUS;
@@ -13,6 +13,6 @@ export async function setConnectionStatus(next: ConnectionStatus): Promise<void>
if (next === currentStatus) return; if (next === currentStatus) return;
currentStatus = next; currentStatus = next;
const message: NexaMessage = { type: 'nexa:connection-status-changed', status: next }; const message: OsiasMessage = { type: 'osias:connection-status-changed', status: next };
await browser.runtime.sendMessage(message).catch(() => undefined); await browser.runtime.sendMessage(message).catch(() => undefined);
} }
+2 -2
View File
@@ -1,6 +1,6 @@
import { browser } from 'wxt/browser'; import { browser } from 'wxt/browser';
import { DEFAULT_LOCK_STATE, type LockState } from '@/shared/types'; import { DEFAULT_LOCK_STATE, type LockState } from '@/shared/types';
import { LOCK_STATE_STORAGE_KEY, type NexaMessage } from '@/shared/messaging'; import { LOCK_STATE_STORAGE_KEY, type OsiasMessage } from '@/shared/messaging';
/** /**
* Single isolated seam for lock state. Nothing else in the extension reads * Single isolated seam for lock state. Nothing else in the extension reads
@@ -38,7 +38,7 @@ export async function applyLockState(next: LockState): Promise<void> {
} }
async function broadcastLockState(state: LockState): Promise<void> { async function broadcastLockState(state: LockState): Promise<void> {
const message: NexaMessage = { type: 'nexa:lock-state-changed', state }; const message: OsiasMessage = { type: 'osias:lock-state-changed', state };
const tabs = await browser.tabs.query({}); const tabs = await browser.tabs.query({});
await Promise.allSettled( await Promise.allSettled(
+11 -11
View File
@@ -1,6 +1,6 @@
import { browser } from 'wxt/browser'; import { browser } from 'wxt/browser';
import { BACKEND_HTTP_URL } from '@/shared/config'; import { BACKEND_HTTP_URL } from '@/shared/config';
import type { NexaMessage, WalletSignResult } from '@/shared/messaging'; import type { OsiasMessage, WalletSignResult } from '@/shared/messaging';
import { storeSession } from './backend-client'; import { storeSession } from './backend-client';
/** /**
@@ -13,7 +13,7 @@ import { storeSession } from './backend-client';
* ws-client's own retry loop to keep trying getSessionToken()). * ws-client's own retry loop to keep trying getSessionToken()).
*/ */
export async function handleWalletConnected(tabId: number | undefined, walletAddress: string): Promise<void> { export async function handleWalletConnected(tabId: number | undefined, walletAddress: string): Promise<void> {
console.debug('[nexa/wallet-auth] wallet connected', walletAddress, 'tab', tabId); console.debug('[osias/wallet-auth] wallet connected', walletAddress, 'tab', tabId);
if (tabId == null) throw new Error('wallet-connected message had no source tab'); if (tabId == null) throw new Error('wallet-connected message had no source tab');
const nonceRes = await fetch(`${BACKEND_HTTP_URL}/auth/nonce`, { const nonceRes = await fetch(`${BACKEND_HTTP_URL}/auth/nonce`, {
@@ -23,14 +23,14 @@ export async function handleWalletConnected(tabId: number | undefined, walletAdd
}); });
if (!nonceRes.ok) throw new Error(`nonce request failed: ${nonceRes.status}`); if (!nonceRes.ok) throw new Error(`nonce request failed: ${nonceRes.status}`);
const { nonce } = (await nonceRes.json()) as { nonce: string }; const { nonce } = (await nonceRes.json()) as { nonce: string };
console.debug('[nexa/wallet-auth] got nonce, asking tab to sign'); console.debug('[osias/wallet-auth] got nonce, asking tab to sign');
const signResult = (await browser.tabs.sendMessage(tabId, { const signResult = (await browser.tabs.sendMessage(tabId, {
type: 'nexa:wallet-sign-request', type: 'osias:wallet-sign-request',
nonce, nonce,
} satisfies NexaMessage)) as WalletSignResult; } satisfies OsiasMessage)) as WalletSignResult;
if ('error' in signResult) throw new Error(signResult.error); if ('error' in signResult) throw new Error(signResult.error);
console.debug('[nexa/wallet-auth] got signature, verifying'); console.debug('[osias/wallet-auth] got signature, verifying');
const verifyRes = await fetch(`${BACKEND_HTTP_URL}/auth/verify`, { const verifyRes = await fetch(`${BACKEND_HTTP_URL}/auth/verify`, {
method: 'POST', method: 'POST',
@@ -41,10 +41,10 @@ export async function handleWalletConnected(tabId: number | undefined, walletAdd
const { session_token: sessionToken } = (await verifyRes.json()) as { session_token: string }; const { session_token: sessionToken } = (await verifyRes.json()) as { session_token: string };
await storeSession(sessionToken, walletAddress); await storeSession(sessionToken, walletAddress);
console.debug('[nexa/wallet-auth] session token stored'); console.debug('[osias/wallet-auth] session token stored');
} }
async function messageAllSupportedTabs(message: NexaMessage): Promise<void> { async function messageAllSupportedTabs(message: OsiasMessage): Promise<void> {
const tabs = await browser.tabs.query({ url: 'https://axiom.trade/*' }); const tabs = await browser.tabs.query({ url: 'https://axiom.trade/*' });
await Promise.allSettled( await Promise.allSettled(
tabs tabs
@@ -55,15 +55,15 @@ async function messageAllSupportedTabs(message: NexaMessage): Promise<void> {
/** Prompts any open supported-site tab to attempt a silent (onlyIfTrusted) reconnect — used after the session is invalidated server-side. */ /** Prompts any open supported-site tab to attempt a silent (onlyIfTrusted) reconnect — used after the session is invalidated server-side. */
export async function requestWalletReconnect(): Promise<void> { export async function requestWalletReconnect(): Promise<void> {
await messageAllSupportedTabs({ type: 'nexa:request-wallet-connect' }); await messageAllSupportedTabs({ type: 'osias:request-wallet-connect' });
} }
/** Sign-out: tells any open supported-site tab to disconnect from Phantom (revokes this origin's trust, so the next silent connect correctly fails until the user reconnects). */ /** Sign-out: tells any open supported-site tab to disconnect from Phantom (revokes this origin's trust, so the next silent connect correctly fails until the user reconnects). */
export async function requestWalletDisconnect(): Promise<void> { export async function requestWalletDisconnect(): Promise<void> {
await messageAllSupportedTabs({ type: 'nexa:wallet-disconnect-request' }); await messageAllSupportedTabs({ type: 'osias:wallet-disconnect-request' });
} }
/** Explicit "use a different account" — a deliberate user action (popup button), not something inferred from a wallet-provider event. See wallet-connect.ts's switchAccount(). */ /** Explicit "use a different account" — a deliberate user action (popup button), not something inferred from a wallet-provider event. See wallet-connect.ts's switchAccount(). */
export async function requestAccountSwitch(): Promise<void> { export async function requestAccountSwitch(): Promise<void> {
await messageAllSupportedTabs({ type: 'nexa:switch-account-request' }); await messageAllSupportedTabs({ type: 'osias:switch-account-request' });
} }
+4 -4
View File
@@ -8,7 +8,7 @@ import { observeForElements } from './observer';
import { initWalletConnect } from './wallet-connect'; import { initWalletConnect } from './wallet-connect';
import { showLockToast, hideLockToast } from '@/notifications/toast'; import { showLockToast, hideLockToast } from '@/notifications/toast';
import { DEFAULT_LOCK_STATE, type LockState } from '@/shared/types'; import { DEFAULT_LOCK_STATE, type LockState } from '@/shared/types';
import type { NexaMessage } from '@/shared/messaging'; import type { OsiasMessage } from '@/shared/messaging';
// Registering a new site = adding it here. Nothing below this line needs to // Registering a new site = adding it here. Nothing below this line needs to
// change to support another adapter. // change to support another adapter.
@@ -97,12 +97,12 @@ export function initContentScript(): void {
const handle = observeForElements(document.body, () => adapter.findBuyElements(document.body), reconcile); const handle = observeForElements(document.body, () => adapter.findBuyElements(document.body), reconcile);
browser.runtime browser.runtime
.sendMessage({ type: 'nexa:get-lock-state' } satisfies NexaMessage) .sendMessage({ type: 'osias:get-lock-state' } satisfies OsiasMessage)
.then((state: LockState) => applyLockState(state)) .then((state: LockState) => applyLockState(state))
.catch(() => undefined); .catch(() => undefined);
browser.runtime.onMessage.addListener((message: NexaMessage) => { browser.runtime.onMessage.addListener((message: OsiasMessage) => {
if (message?.type === 'nexa:lock-state-changed') { if (message?.type === 'osias:lock-state-changed') {
applyLockState(message.state); applyLockState(message.state);
} }
}); });
@@ -61,7 +61,7 @@ export const blurDisable: Intervention = {
} }
const overlay = document.createElement('div'); const overlay = document.createElement('div');
overlay.dataset.nexaOverlay = 'true'; overlay.dataset.osiasOverlay = 'true';
overlay.style.cssText = OVERLAY_STYLE; overlay.style.cssText = OVERLAY_STYLE;
overlay.innerHTML = LOCK_ICON_SVG; overlay.innerHTML = LOCK_ICON_SVG;
@@ -9,7 +9,7 @@ export const fullBlock: Intervention = {
id: 'full-block', id: 'full-block',
apply(_container, _reason, _onLockedInteraction) { apply(_container, _reason, _onLockedInteraction) {
console.warn('[nexa] full-block intervention is a v1 stub and is not yet implemented.'); console.warn('[osias] full-block intervention is a v1 stub and is not yet implemented.');
}, },
remove(_container) {}, remove(_container) {},
+2 -2
View File
@@ -6,7 +6,7 @@ export function showConnectBanner(onConnect: () => void): void {
if (bannerEl) return; if (bannerEl) return;
bannerEl = document.createElement('div'); bannerEl = document.createElement('div');
bannerEl.id = 'nexa-connect-banner'; bannerEl.id = 'osias-connect-banner';
Object.assign(bannerEl.style, { Object.assign(bannerEl.style, {
position: 'fixed', position: 'fixed',
bottom: '16px', bottom: '16px',
@@ -24,7 +24,7 @@ export function showConnectBanner(onConnect: () => void): void {
}); });
const label = document.createElement('span'); const label = document.createElement('span');
label.textContent = 'Connect your wallet to Nexa'; label.textContent = 'Connect your wallet to Osias';
const button = document.createElement('button'); const button = document.createElement('button');
button.type = 'button'; button.type = 'button';
+6 -6
View File
@@ -21,8 +21,8 @@ import bs58 from 'bs58';
* separate, explicit action instead (see wallet-connect.ts's `switchAccount`). * separate, explicit action instead (see wallet-connect.ts's `switchAccount`).
*/ */
const CALL_CHANNEL = 'nexa:wallet-call'; const CALL_CHANNEL = 'osias:wallet-call';
const RESULT_CHANNEL = 'nexa:wallet-result'; const RESULT_CHANNEL = 'osias:wallet-result';
interface WalletCallMessage { interface WalletCallMessage {
channel: typeof CALL_CHANNEL; channel: typeof CALL_CHANNEL;
@@ -53,7 +53,7 @@ function respond(id: string, ok: boolean, dataOrError: unknown): void {
} }
export function initWalletBridgeInjected(): void { export function initWalletBridgeInjected(): void {
console.debug('[nexa/wallet-bridge] injected script active on', window.location.href); console.debug('[osias/wallet-bridge] injected script active on', window.location.href);
window.addEventListener('message', (event) => { window.addEventListener('message', (event) => {
if (event.source !== window) return; // ignore iframes/other windows if (event.source !== window) return; // ignore iframes/other windows
@@ -61,7 +61,7 @@ export function initWalletBridgeInjected(): void {
if (message?.channel !== CALL_CHANNEL) return; // not ours — the page may postMessage for its own reasons if (message?.channel !== CALL_CHANNEL) return; // not ours — the page may postMessage for its own reasons
const { id, action, payload } = message as WalletCallMessage; const { id, action, payload } = message as WalletCallMessage;
console.debug('[nexa/wallet-bridge] call', action, payload); console.debug('[osias/wallet-bridge] call', action, payload);
void (async () => { void (async () => {
try { try {
@@ -71,7 +71,7 @@ export function initWalletBridgeInjected(): void {
if (action === 'connect') { if (action === 'connect') {
const { onlyIfTrusted } = payload as { onlyIfTrusted?: boolean }; const { onlyIfTrusted } = payload as { onlyIfTrusted?: boolean };
const result = await provider.connect(onlyIfTrusted ? { onlyIfTrusted: true } : undefined); const result = await provider.connect(onlyIfTrusted ? { onlyIfTrusted: true } : undefined);
console.debug('[nexa/wallet-bridge] connect ok', result.publicKey.toString()); console.debug('[osias/wallet-bridge] connect ok', result.publicKey.toString());
respond(id, true, { walletAddress: result.publicKey.toString() }); respond(id, true, { walletAddress: result.publicKey.toString() });
} else if (action === 'signMessage') { } else if (action === 'signMessage') {
const { message: nonce } = payload as { message: string }; const { message: nonce } = payload as { message: string };
@@ -84,7 +84,7 @@ export function initWalletBridgeInjected(): void {
throw new Error(`Unknown wallet action: ${String(action)}`); throw new Error(`Unknown wallet action: ${String(action)}`);
} }
} catch (err) { } catch (err) {
console.debug('[nexa/wallet-bridge] call failed:', err); console.debug('[osias/wallet-bridge] call failed:', err);
respond(id, false, err instanceof Error ? err.message : String(err)); respond(id, false, err instanceof Error ? err.message : String(err));
} }
})(); })();
+2 -2
View File
@@ -8,8 +8,8 @@
* Firefox and Chromium. See wallet-bridge/inject.ts for the other side. * Firefox and Chromium. See wallet-bridge/inject.ts for the other side.
*/ */
const CALL_CHANNEL = 'nexa:wallet-call'; const CALL_CHANNEL = 'osias:wallet-call';
const RESULT_CHANNEL = 'nexa:wallet-result'; const RESULT_CHANNEL = 'osias:wallet-result';
interface WalletResultMessage { interface WalletResultMessage {
channel: typeof RESULT_CHANNEL; channel: typeof RESULT_CHANNEL;
+20 -20
View File
@@ -1,5 +1,5 @@
import { browser } from 'wxt/browser'; import { browser } from 'wxt/browser';
import type { NexaMessage } from '@/shared/messaging'; import type { OsiasMessage } from '@/shared/messaging';
import { callWallet } from './wallet-bridge/relay'; import { callWallet } from './wallet-bridge/relay';
import { hideConnectBanner, setConnectBannerError, showConnectBanner } from './wallet-bridge/banner'; import { hideConnectBanner, setConnectBannerError, showConnectBanner } from './wallet-bridge/banner';
@@ -12,30 +12,30 @@ interface SignResult {
} }
async function reportConnected(walletAddress: string): Promise<void> { async function reportConnected(walletAddress: string): Promise<void> {
console.debug('[nexa/wallet-connect] connected', walletAddress); console.debug('[osias/wallet-connect] connected', walletAddress);
hideConnectBanner(); hideConnectBanner();
await browser.runtime await browser.runtime
.sendMessage({ type: 'nexa:wallet-connected', walletAddress } satisfies NexaMessage) .sendMessage({ type: 'osias:wallet-connected', walletAddress } satisfies OsiasMessage)
.catch((err) => console.debug('[nexa/wallet-connect] failed to notify background:', err)); .catch((err) => console.debug('[osias/wallet-connect] failed to notify background:', err));
} }
/** Sign-out or a failed switch — clears the backend session and re-shows the connect prompt. */ /** Sign-out or a failed switch — clears the backend session and re-shows the connect prompt. */
async function reportDisconnected(): Promise<void> { async function reportDisconnected(): Promise<void> {
console.debug('[nexa/wallet-connect] disconnected'); console.debug('[osias/wallet-connect] disconnected');
await browser.runtime await browser.runtime
.sendMessage({ type: 'nexa:wallet-disconnected' } satisfies NexaMessage) .sendMessage({ type: 'osias:wallet-disconnected' } satisfies OsiasMessage)
.catch((err) => console.debug('[nexa/wallet-connect] failed to notify background:', err)); .catch((err) => console.debug('[osias/wallet-connect] failed to notify background:', err));
showConnectBanner(() => void connectWithGesture()); showConnectBanner(() => void connectWithGesture());
} }
/** Real user gesture (banner button click) — required for Phantom to show its connect approval popup on a first-ever connect. */ /** Real user gesture (banner button click) — required for Phantom to show its connect approval popup on a first-ever connect. */
async function connectWithGesture(): Promise<void> { async function connectWithGesture(): Promise<void> {
console.debug('[nexa/wallet-connect] banner clicked, calling connect()'); console.debug('[osias/wallet-connect] banner clicked, calling connect()');
try { try {
const { walletAddress } = await callWallet<ConnectResult>('connect', {}); const { walletAddress } = await callWallet<ConnectResult>('connect', {});
await reportConnected(walletAddress); await reportConnected(walletAddress);
} catch (err) { } catch (err) {
console.debug('[nexa/wallet-connect] connect() failed:', err); console.debug('[osias/wallet-connect] connect() failed:', err);
setConnectBannerError(err instanceof Error ? err.message : String(err)); setConnectBannerError(err instanceof Error ? err.message : String(err));
} }
} }
@@ -46,12 +46,12 @@ async function connectWithGesture(): Promise<void> {
* connect. Falls back to the on-page banner (a real click) only when it isn't. * connect. Falls back to the on-page banner (a real click) only when it isn't.
*/ */
async function attemptSilentConnect(): Promise<void> { async function attemptSilentConnect(): Promise<void> {
console.debug('[nexa/wallet-connect] attempting silent connect'); console.debug('[osias/wallet-connect] attempting silent connect');
try { try {
const { walletAddress } = await callWallet<ConnectResult>('connect', { onlyIfTrusted: true }); const { walletAddress } = await callWallet<ConnectResult>('connect', { onlyIfTrusted: true });
await reportConnected(walletAddress); await reportConnected(walletAddress);
} catch (err) { } catch (err) {
console.debug('[nexa/wallet-connect] silent connect failed, showing banner:', err); console.debug('[osias/wallet-connect] silent connect failed, showing banner:', err);
showConnectBanner(() => void connectWithGesture()); showConnectBanner(() => void connectWithGesture());
} }
} }
@@ -66,46 +66,46 @@ async function attemptSilentConnect(): Promise<void> {
* to the same on-page banner as a first-time connect would. * to the same on-page banner as a first-time connect would.
*/ */
async function switchAccount(): Promise<void> { async function switchAccount(): Promise<void> {
console.debug('[nexa/wallet-connect] switch account requested'); console.debug('[osias/wallet-connect] switch account requested');
try { try {
await callWallet('disconnect', {}).catch(() => undefined); // best-effort — fine if already disconnected await callWallet('disconnect', {}).catch(() => undefined); // best-effort — fine if already disconnected
const { walletAddress } = await callWallet<ConnectResult>('connect', {}); const { walletAddress } = await callWallet<ConnectResult>('connect', {});
await reportConnected(walletAddress); await reportConnected(walletAddress);
} catch (err) { } catch (err) {
console.debug('[nexa/wallet-connect] switch account failed:', err); console.debug('[osias/wallet-connect] switch account failed:', err);
showConnectBanner(() => void connectWithGesture()); showConnectBanner(() => void connectWithGesture());
} }
} }
/** Wires wallet connect/sign into the page. Independent of any site adapter — runs regardless of whether a buy-button adapter matched. */ /** Wires wallet connect/sign into the page. Independent of any site adapter — runs regardless of whether a buy-button adapter matched. */
export function initWalletConnect(): void { export function initWalletConnect(): void {
console.debug('[nexa/wallet-connect] init on', window.location.href); console.debug('[osias/wallet-connect] init on', window.location.href);
void attemptSilentConnect(); void attemptSilentConnect();
browser.runtime.onMessage.addListener((message: NexaMessage, _sender, sendResponse) => { browser.runtime.onMessage.addListener((message: OsiasMessage, _sender, sendResponse) => {
if (message?.type === 'nexa:wallet-sign-request') { if (message?.type === 'osias:wallet-sign-request') {
callWallet<SignResult>('signMessage', { message: message.nonce }) callWallet<SignResult>('signMessage', { message: message.nonce })
.then((result) => sendResponse(result)) .then((result) => sendResponse(result))
.catch((err) => sendResponse({ error: err instanceof Error ? err.message : String(err) })); .catch((err) => sendResponse({ error: err instanceof Error ? err.message : String(err) }));
return true; return true;
} }
if (message?.type === 'nexa:request-wallet-connect') { if (message?.type === 'osias:request-wallet-connect') {
void attemptSilentConnect(); void attemptSilentConnect();
return false; return false;
} }
if (message?.type === 'nexa:wallet-disconnect-request') { if (message?.type === 'osias:wallet-disconnect-request') {
// Sign-out, initiated from the popup (see background/wallet-auth.ts). // Sign-out, initiated from the popup (see background/wallet-auth.ts).
// Phantom's disconnect() revokes this origin's trust, so the next // Phantom's disconnect() revokes this origin's trust, so the next
// onlyIfTrusted attempt correctly fails until the user reconnects. // onlyIfTrusted attempt correctly fails until the user reconnects.
callWallet('disconnect', {}) callWallet('disconnect', {})
.catch((err) => console.debug('[nexa/wallet-connect] disconnect() failed:', err)) .catch((err) => console.debug('[osias/wallet-connect] disconnect() failed:', err))
.finally(() => void reportDisconnected()); .finally(() => void reportDisconnected());
return false; return false;
} }
if (message?.type === 'nexa:switch-account-request') { if (message?.type === 'osias:switch-account-request') {
void switchAccount(); void switchAccount();
return false; return false;
} }
+12 -12
View File
@@ -9,7 +9,7 @@ import {
} from '@/background/wallet-auth'; } from '@/background/wallet-auth';
import { getSettings, updateSettings } from '@/background/settings-client'; import { getSettings, updateSettings } from '@/background/settings-client';
import { connectWsClient } from '@/background/ws-client'; import { connectWsClient } from '@/background/ws-client';
import type { NexaMessage, SettingsResult } from '@/shared/messaging'; import type { OsiasMessage, SettingsResult } from '@/shared/messaging';
export default defineBackground(() => { export default defineBackground(() => {
const ws = connectWsClient({ const ws = connectWsClient({
@@ -27,23 +27,23 @@ export default defineBackground(() => {
await requestWalletDisconnect(); await requestWalletDisconnect();
} }
browser.runtime.onMessage.addListener((message: NexaMessage, sender, sendResponse) => { browser.runtime.onMessage.addListener((message: OsiasMessage, sender, sendResponse) => {
switch (message?.type) { switch (message?.type) {
case 'nexa:get-lock-state': case 'osias:get-lock-state':
getLockState().then(sendResponse); getLockState().then(sendResponse);
return true; // keep the message channel open for the async response return true; // keep the message channel open for the async response
case 'nexa:get-connection-status': case 'osias:get-connection-status':
sendResponse(getConnectionStatus()); sendResponse(getConnectionStatus());
return false; return false;
case 'nexa:open-popup': case 'osias:open-popup':
// Best-effort: not all browsers/contexts allow programmatic popup // Best-effort: not all browsers/contexts allow programmatic popup
// opening outside a direct user gesture on the action icon. // opening outside a direct user gesture on the action icon.
browser.action.openPopup().catch(() => undefined); browser.action.openPopup().catch(() => undefined);
return false; return false;
case 'nexa:wallet-connected': case 'osias:wallet-connected':
// A content script reports this on every page load (it always tries // A content script reports this on every page load (it always tries
// a silent onlyIfTrusted connect first), and after an explicit // a silent onlyIfTrusted connect first), and after an explicit
// account switch. Only re-authenticate (which means asking Phantom // account switch. Only re-authenticate (which means asking Phantom
@@ -55,30 +55,30 @@ export default defineBackground(() => {
if (session?.walletAddress === message.walletAddress) return; if (session?.walletAddress === message.walletAddress) return;
return handleWalletConnected(sender.tab?.id, message.walletAddress).then(() => ws.reconnectNow()); return handleWalletConnected(sender.tab?.id, message.walletAddress).then(() => ws.reconnectNow());
}) })
.catch((err) => console.debug('[nexa/background] wallet auth failed:', err)); // ws-client's own retry loop keeps trying regardless .catch((err) => console.debug('[osias/background] wallet auth failed:', err)); // ws-client's own retry loop keeps trying regardless
return false; return false;
case 'nexa:wallet-disconnected': case 'osias:wallet-disconnected':
// Disconnected directly in Phantom's UI (not via our own sign-out // Disconnected directly in Phantom's UI (not via our own sign-out
// flow, which already clears/disconnects itself) — treat the same way. // flow, which already clears/disconnects itself) — treat the same way.
void clearSessionToken().then(() => ws.disconnect()); void clearSessionToken().then(() => ws.disconnect());
return false; return false;
case 'nexa:sign-out': case 'osias:sign-out':
void signOut(); void signOut();
return false; return false;
case 'nexa:switch-account': case 'osias:switch-account':
void requestAccountSwitch(); void requestAccountSwitch();
return false; return false;
case 'nexa:get-settings': case 'osias:get-settings':
getSettings() getSettings()
.then((settings) => sendResponse({ settings } satisfies SettingsResult)) .then((settings) => sendResponse({ settings } satisfies SettingsResult))
.catch((err) => sendResponse({ error: String(err) } satisfies SettingsResult)); .catch((err) => sendResponse({ error: String(err) } satisfies SettingsResult));
return true; // keep the message channel open for the async response return true; // keep the message channel open for the async response
case 'nexa:update-settings': case 'osias:update-settings':
updateSettings(message.settings) updateSettings(message.settings)
.then((settings) => sendResponse({ settings } satisfies SettingsResult)) .then((settings) => sendResponse({ settings } satisfies SettingsResult))
.catch((err) => sendResponse({ error: String(err) } satisfies SettingsResult)); .catch((err) => sendResponse({ error: String(err) } satisfies SettingsResult));
+11 -11
View File
@@ -8,7 +8,7 @@ import {
type LockState, type LockState,
type ThresholdSettings, type ThresholdSettings,
} from '@/shared/types'; } from '@/shared/types';
import type { NexaMessage, SettingsResult } from '@/shared/messaging'; import type { OsiasMessage, SettingsResult } from '@/shared/messaging';
const CONNECTION_LABELS: Record<ConnectionStatus, string> = { const CONNECTION_LABELS: Record<ConnectionStatus, string> = {
connecting: 'Connecting…', connecting: 'Connecting…',
@@ -23,17 +23,17 @@ export function App() {
useEffect(() => { useEffect(() => {
browser.runtime browser.runtime
.sendMessage({ type: 'nexa:get-lock-state' } satisfies NexaMessage) .sendMessage({ type: 'osias:get-lock-state' } satisfies OsiasMessage)
.then((state: LockState | undefined) => setLockState(state ?? DEFAULT_LOCK_STATE)); .then((state: LockState | undefined) => setLockState(state ?? DEFAULT_LOCK_STATE));
browser.runtime browser.runtime
.sendMessage({ type: 'nexa:get-connection-status' } satisfies NexaMessage) .sendMessage({ type: 'osias:get-connection-status' } satisfies OsiasMessage)
.then((status: ConnectionStatus | undefined) => setConnectionStatus(status ?? DEFAULT_CONNECTION_STATUS)); .then((status: ConnectionStatus | undefined) => setConnectionStatus(status ?? DEFAULT_CONNECTION_STATUS));
const listener = (message: NexaMessage) => { const listener = (message: OsiasMessage) => {
if (message?.type === 'nexa:lock-state-changed') { if (message?.type === 'osias:lock-state-changed') {
setLockState(message.state); setLockState(message.state);
} else if (message?.type === 'nexa:connection-status-changed') { } else if (message?.type === 'osias:connection-status-changed') {
setConnectionStatus(message.status); setConnectionStatus(message.status);
} }
}; };
@@ -42,17 +42,17 @@ export function App() {
}, []); }, []);
function signOut(): void { function signOut(): void {
void browser.runtime.sendMessage({ type: 'nexa:sign-out' } satisfies NexaMessage); void browser.runtime.sendMessage({ type: 'osias:sign-out' } satisfies OsiasMessage);
} }
function switchAccount(): void { function switchAccount(): void {
void browser.runtime.sendMessage({ type: 'nexa:switch-account' } satisfies NexaMessage); void browser.runtime.sendMessage({ type: 'osias:switch-account' } satisfies OsiasMessage);
} }
return ( return (
<> <>
<header> <header>
<h1>Nexa</h1> <h1>Osias</h1>
<p className="subtitle">Trading companion</p> <p className="subtitle">Trading companion</p>
</header> </header>
@@ -140,7 +140,7 @@ function ThresholdsPanel() {
setStatus('loading'); setStatus('loading');
setError(undefined); setError(undefined);
browser.runtime browser.runtime
.sendMessage({ type: 'nexa:get-settings' } satisfies NexaMessage) .sendMessage({ type: 'osias:get-settings' } satisfies OsiasMessage)
.then((result: SettingsResult | undefined) => { .then((result: SettingsResult | undefined) => {
if (result && 'settings' in result) { if (result && 'settings' in result) {
setForm(toForm(result.settings)); setForm(toForm(result.settings));
@@ -160,7 +160,7 @@ function ThresholdsPanel() {
setStatus('saving'); setStatus('saving');
setError(undefined); setError(undefined);
browser.runtime browser.runtime
.sendMessage({ type: 'nexa:update-settings', settings: fromForm(form) } satisfies NexaMessage) .sendMessage({ type: 'osias:update-settings', settings: fromForm(form) } satisfies OsiasMessage)
.then((result: SettingsResult | undefined) => { .then((result: SettingsResult | undefined) => {
setStatus('idle'); setStatus('idle');
if (result && 'settings' in result) { if (result && 'settings' in result) {
+1 -1
View File
@@ -2,7 +2,7 @@
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<title>Nexa</title> <title>Osias</title>
<link rel="stylesheet" href="./style.css" /> <link rel="stylesheet" href="./style.css" />
</head> </head>
<body> <body>
+17 -17
View File
@@ -1,8 +1,8 @@
import { browser } from 'wxt/browser'; import { browser } from 'wxt/browser';
import type { NexaMessage } from '@/shared/messaging'; import type { OsiasMessage } from '@/shared/messaging';
const TOAST_ID = 'nexa-lock-toast'; const TOAST_ID = 'osias-lock-toast';
const STYLE_ID = 'nexa-lock-toast-styles'; const STYLE_ID = 'osias-lock-toast-styles';
const AUTO_HIDE_MS = 5000; const AUTO_HIDE_MS = 5000;
let hideTimer: ReturnType<typeof setTimeout> | undefined; let hideTimer: ReturnType<typeof setTimeout> | undefined;
@@ -30,18 +30,18 @@ function ensureStyles(): void {
font: 13px/1.4 -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; font: 13px/1.4 -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.35); box-shadow: 0 8px 24px rgba(0, 0, 0, 0.35);
} }
#${TOAST_ID} .nexa-toast-icon { flex: none; margin-top: 1px; } #${TOAST_ID} .osias-toast-icon { flex: none; margin-top: 1px; }
#${TOAST_ID} .nexa-toast-body { flex: 1; } #${TOAST_ID} .osias-toast-body { flex: 1; }
#${TOAST_ID} .nexa-toast-title { font-weight: 600; margin-bottom: 2px; } #${TOAST_ID} .osias-toast-title { font-weight: 600; margin-bottom: 2px; }
#${TOAST_ID} .nexa-toast-reason { opacity: 0.85; } #${TOAST_ID} .osias-toast-reason { opacity: 0.85; }
#${TOAST_ID} .nexa-toast-action { #${TOAST_ID} .osias-toast-action {
margin-top: 8px; margin-top: 8px;
display: inline-block; display: inline-block;
font-weight: 600; font-weight: 600;
color: #7ab8ff; color: #7ab8ff;
cursor: pointer; cursor: pointer;
} }
#${TOAST_ID} .nexa-toast-action:hover { text-decoration: underline; } #${TOAST_ID} .osias-toast-action:hover { text-decoration: underline; }
`; `;
document.head.appendChild(style); document.head.appendChild(style);
} }
@@ -55,18 +55,18 @@ function ensureToast(): HTMLElement {
el = document.createElement('div'); el = document.createElement('div');
el.id = TOAST_ID; el.id = TOAST_ID;
el.innerHTML = ` el.innerHTML = `
<div class="nexa-toast-icon">🔒</div> <div class="osias-toast-icon">🔒</div>
<div class="nexa-toast-body"> <div class="osias-toast-body">
<div class="nexa-toast-title">Nexa: buying locked</div> <div class="osias-toast-title">Osias: buying locked</div>
<div class="nexa-toast-reason"></div> <div class="osias-toast-reason"></div>
<div class="nexa-toast-action">View details</div> <div class="osias-toast-action">View details</div>
</div> </div>
`; `;
// No manual close button — the toast only goes away on its own (auto-hide // No manual close button — the toast only goes away on its own (auto-hide
// timer below) or when the lock state actually changes; clicking it never // timer below) or when the lock state actually changes; clicking it never
// unlocks anything. // unlocks anything.
el.querySelector('.nexa-toast-action')?.addEventListener('click', () => { el.querySelector('.osias-toast-action')?.addEventListener('click', () => {
const message: NexaMessage = { type: 'nexa:open-popup' }; const message: OsiasMessage = { type: 'osias:open-popup' };
browser.runtime.sendMessage(message).catch(() => undefined); browser.runtime.sendMessage(message).catch(() => undefined);
}); });
@@ -76,7 +76,7 @@ function ensureToast(): HTMLElement {
export function showLockToast(reason: string | undefined): void { export function showLockToast(reason: string | undefined): void {
const el = ensureToast(); const el = ensureToast();
const reasonEl = el.querySelector('.nexa-toast-reason'); const reasonEl = el.querySelector('.osias-toast-reason');
if (reasonEl) { if (reasonEl) {
reasonEl.textContent = reason ?? 'Buy actions are temporarily locked.'; reasonEl.textContent = reason ?? 'Buy actions are temporarily locked.';
} }
+1 -1
View File
@@ -1,5 +1,5 @@
/** /**
* Nexa backend location. `host_permissions` in wxt.config.ts must be kept in * Osias backend location. `host_permissions` in wxt.config.ts must be kept in
* sync with this host. * sync with this host.
*/ */
export const BACKEND_HTTP_URL = 'https://api.osias.trade'; export const BACKEND_HTTP_URL = 'https://api.osias.trade';
+19 -19
View File
@@ -1,7 +1,7 @@
import type { ConnectionStatus, LockState, ThresholdSettings } from './types'; import type { ConnectionStatus, LockState, ThresholdSettings } from './types';
/** browser.storage.local key backing the lock state seam in the background script. */ /** browser.storage.local key backing the lock state seam in the background script. */
export const LOCK_STATE_STORAGE_KEY = 'nexa:lockState'; export const LOCK_STATE_STORAGE_KEY = 'osias:lockState';
/** /**
* Typed message contracts between background <-> content scripts <-> popup. * Typed message contracts between background <-> content scripts <-> popup.
@@ -12,30 +12,30 @@ export const LOCK_STATE_STORAGE_KEY = 'nexa:lockState';
* in ../../CLAUDE.md) — only the WS client (ws-client.ts) may apply a new * in ../../CLAUDE.md) — only the WS client (ws-client.ts) may apply a new
* lock state, and only because the server told it to. * lock state, and only because the server told it to.
*/ */
export type NexaMessage = export type OsiasMessage =
| { type: 'nexa:get-lock-state' } | { type: 'osias:get-lock-state' }
| { type: 'nexa:lock-state-changed'; state: LockState } | { type: 'osias:lock-state-changed'; state: LockState }
| { type: 'nexa:get-connection-status' } | { type: 'osias:get-connection-status' }
| { type: 'nexa:connection-status-changed'; status: ConnectionStatus } | { type: 'osias:connection-status-changed'; status: ConnectionStatus }
| { type: 'nexa:open-popup' } | { type: 'osias:open-popup' }
// Wallet auth (content script <-> background). See // Wallet auth (content script <-> background). See
// content-scripts/wallet-connect.ts and background/wallet-auth.ts. // content-scripts/wallet-connect.ts and background/wallet-auth.ts.
| { type: 'nexa:wallet-connected'; walletAddress: string } | { type: 'osias:wallet-connected'; walletAddress: string }
| { type: 'nexa:wallet-disconnected' } | { type: 'osias:wallet-disconnected' }
| { type: 'nexa:wallet-sign-request'; nonce: string } | { type: 'osias:wallet-sign-request'; nonce: string }
| { type: 'nexa:request-wallet-connect' } | { type: 'osias:request-wallet-connect' }
| { type: 'nexa:wallet-disconnect-request' } | { type: 'osias:wallet-disconnect-request' }
| { type: 'nexa:switch-account-request' } | { type: 'osias:switch-account-request' }
// Sign-out / switch account (popup -> background). See entrypoints/popup/App.tsx. // Sign-out / switch account (popup -> background). See entrypoints/popup/App.tsx.
| { type: 'nexa:sign-out' } | { type: 'osias:sign-out' }
| { type: 'nexa:switch-account' } | { type: 'osias:switch-account' }
// Loss-threshold settings (popup -> background -> GET/PATCH /me/settings). // Loss-threshold settings (popup -> background -> GET/PATCH /me/settings).
// See background/settings-client.ts and entrypoints/popup/App.tsx. // See background/settings-client.ts and entrypoints/popup/App.tsx.
| { type: 'nexa:get-settings' } | { type: 'osias:get-settings' }
| { type: 'nexa:update-settings'; settings: Partial<ThresholdSettings> }; | { type: 'osias:update-settings'; settings: Partial<ThresholdSettings> };
/** Response shape for 'nexa:get-settings'/'nexa:update-settings', returned via sendResponse. */ /** Response shape for 'osias:get-settings'/'osias:update-settings', returned via sendResponse. */
export type SettingsResult = { settings: ThresholdSettings } | { error: string }; export type SettingsResult = { settings: ThresholdSettings } | { error: string };
/** Response shape for 'nexa:wallet-sign-request', returned via sendResponse (not a dispatched NexaMessage). */ /** Response shape for 'osias:wallet-sign-request', returned via sendResponse (not a dispatched OsiasMessage). */
export type WalletSignResult = { signature: string } | { error: string }; export type WalletSignResult = { signature: string } | { error: string };
+3 -3
View File
@@ -8,15 +8,15 @@ export default defineConfig({
// Target Manifest V3 on both Chromium and Firefox (modern Firefox / Zen support it). // Target Manifest V3 on both Chromium and Firefox (modern Firefox / Zen support it).
manifestVersion: 3, manifestVersion: 3,
manifest: { manifest: {
name: 'Nexa', name: 'Osias',
description: 'Your blockchain powered agent to help with your trading emotions.', description: 'Your blockchain powered agent to help with your trading emotions.',
permissions: ['storage'], permissions: ['storage'],
// axiom.trade: the site adapter target. The other entry is the Nexa backend. // axiom.trade: the site adapter target. The other entry is the Osias backend.
host_permissions: ['https://axiom.trade/*', BACKEND_HTTP_URL + "/*"], host_permissions: ['https://axiom.trade/*', BACKEND_HTTP_URL + "/*"],
browser_specific_settings: { browser_specific_settings: {
gecko: { gecko: {
// Placeholder id for local/dev builds; replace before publishing to AMO. // Placeholder id for local/dev builds; replace before publishing to AMO.
id: 'nexa-extension@nexa-sol.dev', id: 'osias-extension@osias.trade',
}, },
}, },
}, },