diff --git a/.agents/skills/oneshot-embedded-wallet/SKILL.md b/.agents/skills/oneshot-embedded-wallet/SKILL.md index fba5b6d..f308d31 100644 --- a/.agents/skills/oneshot-embedded-wallet/SKILL.md +++ b/.agents/skills/oneshot-embedded-wallet/SKILL.md @@ -80,7 +80,10 @@ await proxy.rpc("setStyle", options); | `theme.border` / `accent` / `accentForeground` | string | chrome | | `theme.radius` | string | `--radius` (e.g. `"0.625rem"`) | | `theme.fontSans` | string | `--font-sans` | -| `allowedChains` | `string[]` (hex `0x…` chain ids) | Restrict Network dropdown to these catalog chains; omit or `[]` ⇒ all enabled | +| `features.hideCloseBox` | boolean | Hide chrome Close (X); default `false`. Use in Inline hosts (e.g. extension) | +| `features.disableCredentials` | boolean | Hide Credentials tab; default `false`. Host credential flows still work | +| `features.disableDelegations` | boolean | Hide Delegations tab; default `false`. Host delegation flows still work | +| `features.allowedChains` | `string[]` (hex `0x…` chain ids) | Restrict Network dropdown to these catalog chains; omit or `[]` ⇒ all enabled | | `copy.productName` | string | titles / chrome | | `copy.tagline` | string | supporting line | | `copy.connect.title` | string | connect modal title | @@ -228,7 +231,7 @@ proxy.showWallet(); Returns `{ ok: true, chainId, assetAddress }` when the user accepts. -Users can also add assets from the Balances tab without a host RPC. The Balances list shows tracked assets for the currently selected network only (USDC is always tracked per supported chain). +Users can also add assets from the Balances tab without a host RPC. The Balances list shows tracked assets for the currently selected network only (USDC is always tracked where listed; USDG on Robinhood). ## Custom RPC — `createAccount` diff --git a/.agents/skills/ows-branding-layer/references/tasks.md b/.agents/skills/ows-branding-layer/references/tasks.md index 8f24895..a5cf82b 100644 --- a/.agents/skills/ows-branding-layer/references/tasks.md +++ b/.agents/skills/ows-branding-layer/references/tasks.md @@ -71,7 +71,7 @@ Host `OWSProxy` shows a lower-right opaque flyout (no modal backdrop). **Reference wallet path (EIP-1193):** -1. `src/ows/registerAccountConnect.ts` — `eth_accounts` / `eth_requestAccounts` (cached addresses; connect consent + `ensureReady`). Emit `wallet.providerEvents.emit("accountsChanged", [evm])` after a new connect. +1. `src/ows/registerAccountConnect.ts` — `eth_accounts` / `eth_requestAccounts` (grant-gated cache; connect consent + `ensureReady`). Emit `accountsChanged` / `connect` only after a fresh approval — not on silent reconnect. 2. `RpcHelper` for JSON-RPC reads / `wallet_switchEthereumChain` (`src/ows/demoChains.ts`, construct in `WalletProvider.tsx`). Forward `rpc.events.on("chainChanged", …)` to `wallet.providerEvents.emit("chainChanged", next)` so hosts listening on `proxy.ethereum.on("chainChanged", …)` stay in sync. 3. `SignHelper` for `personal_sign` / typed data (task 5). diff --git a/.gitignore b/.gitignore index a8299ab..cd084b4 100644 --- a/.gitignore +++ b/.gitignore @@ -142,6 +142,10 @@ vite.config.js.timestamp-* vite.config.ts.timestamp-* .vite/ +# WXT browser extension (generated) +extension/.wxt/ +extension/.output/ + # Local HTTPS certs (mkcert) for host tester host/certs/*.pem diff --git a/AGENTS.md b/AGENTS.md index dae7592..0027f98 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -13,6 +13,8 @@ Prefer **clean code over backwards compatibility**. Do not add legacy redirects, | `/` | Branding Layer (React SPA) | | `/signer/` | Signing Layer (`@1shotapi/ows-signer`) | | `/create/` | First-party Host for Safari passkey create (`createAccount` RPC) | +| `/mobile/` | First-party Host PWA + WalletConnect (Inline OWSProxy ↔ Reown WalletKit) | +| `extension/` | MV3 Chrome/Firefox extension (side-panel Inline OWSProxy + MAIN-world EIP-1193 shim) | | `src/lib/types/primitives/` | Wallet-local branded types (one file each) | | `src/lib/types/enum/` | Domain enums (`EAssetType`, `EWalletEventKind`, …) | | `src/lib/types/domain/` | Domain DTOs (e.g. `KnownAsset`, `TrackedAsset`, `WalletConfig`) | @@ -21,7 +23,7 @@ Prefer **clean code over backwards compatibility**. Do not add legacy redirects, | `src/lib/implementations/{business,data,utils}/` | Layer implementations | | `src/assets/` | Static media only (SVGs, images) | -Test Host Layer: `host/` (`npm run dev:host`). Style via Host RPC `setStyle`, not in-wallet debug knobs. +Test Host Layer: `host/` (`npm run dev:host`). Browser extension: `extension/` (`npm run dev:extension`) — see [`extension/README.md`](extension/README.md). Style via Host RPC `setStyle`, not in-wallet debug knobs. ### Form validation UX diff --git a/Dockerfile b/Dockerfile index 0083a63..e6d675a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -10,6 +10,8 @@ RUN npm ci COPY index.html vite.config.ts tsconfig.json tsconfig.node.json components.json ./ COPY src ./src COPY create ./create +COPY mobile ./mobile +COPY public ./public COPY scripts ./scripts COPY signer-static ./signer-static diff --git a/README.md b/README.md index 3500726..1c40162 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,7 @@ Safari create (first-party): /create/ → embeds Branding + createAccount RPC | `/` | React Branding Layer (Vite bundle) | | `/signer/` | Static `@1shotapi/ows-signer` ES modules | | `/create/` | First-party Host page for Safari passkey create | +| `extension/` | MV3 Chrome/Firefox extension (MetaMask-style host) | Production deliverable: a static **nginx** Docker image (no server-side runtime). @@ -37,6 +38,7 @@ cp .env.example .env # set NGROK_AUTHTOKEN (and optional NGROK_DOMAIN) npm run dev # Branding Layer + ngrok HTTPS tunnel npm run dev:local # Branding Layer, local HTTP only npm run dev:host # Test Host Layer (setStyle knobs + EIP-1193) +npm run dev:extension # Browser extension (side panel + EIP-1193 shim) ``` | Service | Local URL | diff --git a/create/main.ts b/create/main.ts index a606656..a6f5d82 100644 --- a/create/main.ts +++ b/create/main.ts @@ -37,24 +37,19 @@ function readHandoff(): string | null { return value && value.length > 0 ? value : null; } -function notifyOpener(message: AccountCreateHandoffMessage): void { - const opener = window.opener; - if (!opener || opener.closed) { - console.warn("[create] cannot notify opener", { - hasOpener: Boolean(opener), - closed: opener?.closed, - type: message.type, - }); - return; - } - console.info("[create] postMessage → opener", { +function notifyWallet(message: AccountCreateHandoffMessage): void { + const opener = window.opener as Window | null; + const hasOpener = Boolean(opener) && !opener!.closed; + console.info("[create] notify wallet", { type: message.type, handoff: message.handoff, + hasOpener, credentialId: message.credentialId ? "(present)" : undefined, cosePublicKey: message.cosePublicKey ? "(present)" : undefined, - targetOrigin: window.location.origin, }); - postAccountCreateHandoff(opener, message); + // Always BroadcastChannel (same-origin Branding iframe). Also postMessage + // when opener exists — extension-opened tabs often have opener === null. + postAccountCreateHandoff(hasOpener ? opener : null, message); } async function closeOrPrompt(): Promise { @@ -71,18 +66,12 @@ async function main(): Promise { return; } - if (!window.opener) { - setStatus( - "This page must be opened from the wallet. Return to the app and try Create again.", - true, - ); - return; - } - + const opener = window.opener as Window | null; + const hasOpener = Boolean(opener) && !opener!.closed; console.info("[create] start", { handoff, origin: window.location.origin, - hasOpener: true, + hasOpener, }); const walletUrl = new URL("/", window.location.origin).href; @@ -120,7 +109,7 @@ async function main(): Promise { hasCosePublicKey: true, }); - notifyOpener({ + notifyWallet({ type: OWS_ACCOUNT_CREATED, handoff, credentialId: result.credentialId, @@ -137,7 +126,7 @@ async function main(): Promise { console.warn("[create] createAccount failed", { cancelled, message, error }); - notifyOpener({ + notifyWallet({ type: cancelled ? OWS_ACCOUNT_CREATE_CANCELLED : OWS_ACCOUNT_CREATE_FAILED, @@ -155,7 +144,7 @@ main().catch((error: unknown) => { console.error("[create] failed", error); const handoff = readHandoff(); if (handoff) { - notifyOpener({ + notifyWallet({ type: OWS_ACCOUNT_CREATE_FAILED, handoff, message: error instanceof Error ? error.message : String(error), diff --git a/extension/README.md b/extension/README.md new file mode 100644 index 0000000..1c5c39f --- /dev/null +++ b/extension/README.md @@ -0,0 +1,148 @@ +# 1Shot Wallet browser extension + +MetaMask-style host for the 1Shot Branding Layer: a thin MAIN-world EIP-1193 / EIP-6963 shim on the dApp page, with `OWSProxy` hosted **Inline** in a Chrome side panel / Firefox sidebar. + +This avoids page `frame-src` CSP, hostile host CSS, and per-dApp third-party storage partitions. + +## Requirements + +- Node.js 22+ +- Chrome 116+ (side panel + `scripting.executeScript` MAIN world) +- Firefox 128+ (MV3 + MAIN world + sidebar) + +## Develop + +From the embedded-wallet repo root: + +```bash +npm install +npm run dev:extension # Chrome HMR → extension/dist/chrome-mv3-dev +``` + +### Firefox (important) + +Do **not** use Vite serve (`firefox-mv3-dev`) for day-to-day Firefox testing. +Firefox’s extension CSP blocks Vite’s `eval` / `wss://localhost:3000` HMR, so +sidepanel/options modules often never run (buttons appear dead, sidebar stuck on +“Loading wallet…”). + +```bash +npm run build:firefox -w @1shotapi/oneshot-wallet-extension +# same as: npm run dev:firefox -w @1shotapi/oneshot-wallet-extension +# → extension/dist/firefox-mv3 (self-contained CSS/JS) +``` + +Then `about:debugging` → Load Temporary Add-on → +`extension/dist/firefox-mv3/manifest.json`. After code changes, rebuild and +click **Reload** on the temporary add-on (or remove/re-add). + +Legacy Vite serve (broken under Firefox CSP) remains as +`npm run dev:firefox:vite -w @1shotapi/oneshot-wallet-extension` → +`firefox-mv3-dev` only. + +### Point at a local / ngrok wallet + +Open the extension **Settings** and set **Wallet iframe URL** to a full URL +(e.g. `https://immune-sheep-light.ngrok-free.app/`). Default is +`https://wallet.1shotapi.com/`. + +Load the unpacked extension: + +- **Chrome (dev):** `chrome://extensions` → Load unpacked → `extension/dist/chrome-mv3-dev` +- **Chrome (stable build):** `extension/dist/chrome-mv3` +- **Firefox (recommended):** load `extension/dist/firefox-mv3/manifest.json` + (from `build:firefox` / `dev:firefox`) +- **Firefox Vite-dev (unsupported):** `extension/dist/firefox-mv3-dev` — CSS may + load after CSP tweaks, but entrypoint JS regularly dies; prefer `firefox-mv3`. + +## Use + +The **side panel / sidebar** is the wallet UI (Branding iframe). It does **not** +automatically put a provider on the dApp. Injection is explicit: + +1. Open the dApp tab (or the host **Injected** playground). +2. In the extension side panel top bar, click **Inject** (or the toolbar + popup → **Inject on this page**). Grant host permission if prompted. +3. Optionally **Always** / **Always inject on this origin** for reload auto-inject. +4. Confirm in the page console: + - `window.ethereum?.is1Shot === true`, and/or + - EIP-6963 announce for `com.1shotapi.wallet` +5. Connect in the dApp. If MetaMask is also installed, enable **Prefer 1Shot as + window.ethereum** in extension Settings, or pick 1Shot from an EIP-6963 wallet + list (Uniswap’s shortcut strip may only highlight MetaMask). + +Approve / sign stays in the side panel. + +**Firefox note:** Keep the side panel open while connecting. The extension only +opens the sidebar when it has no live panel connection — calling +`sidebarAction.open()` on an already-open panel can reload it and drop Connect RPCs. + +### Easiest local test + +```bash +npm run dev:host +npm run build -w @1shotapi/oneshot-wallet-extension # reload the temp add-on +``` + +Open the host → sidebar mode **Injected** → extension **Inject** on that tab → +**Connect**. That page never creates `OWSProxy`; it only talks to the injected +provider. + + +## Build / pack + +```bash +npm run build:extension # Chrome MV3 → extension/dist/chrome-mv3 +npm run build:firefox -w @1shotapi/oneshot-wallet-extension +npm run pack:chrome -w @1shotapi/oneshot-wallet-extension # zip for CWS +npm run pack:firefox -w @1shotapi/oneshot-wallet-extension # zip for AMO +``` + +### Chrome Web Store (unlisted / test) + +1. `npm run pack:chrome -w @1shotapi/oneshot-wallet-extension` +2. Upload the zip from `extension/.output/` (WXT zip output) in the [Chrome Developer Dashboard](https://chrome.google.com/webstore/devconsole). +3. Publish as **Unlisted** for testers. + +### Firefox AMO self-distribution + +1. `npm run pack:firefox -w @1shotapi/oneshot-wallet-extension` +2. Sign with [AMO](https://addons.mozilla.org/developers/) “On your own” / self-distributed listing (JWT API credentials), or upload for signing and download the `.xpi`. +3. Extension id: `wallet-extension@1shotapi.com` (see `browser_specific_settings.gecko`). + +## Architecture + +``` +dApp MAIN world content script service worker side panel +───────────────── ────────────── ────────────── ────────── +inpage.js shim ←post→ bridge ←msg→ router / queue ←port→ OWSProxy Inline +window.ethereum openWalletUi() Branding iframe +EIP-6963 announce allowlist inject +``` + +- `@1shotapi/ows-provider` is **only** bundled into the side panel page. +- MAIN-world inject uses `chrome.scripting.executeScript({ world: "MAIN" })` so page `script-src` CSP does not block the shim. + +## Privacy + +- No page scraping or analytics beyond what the Branding iframe already does for wallet UX. +- Scripts inject only after user action or for allowlisted origins the user added. +- Use **Always** (side panel) so the provider re-injects after a dApp tab refresh. + One-shot **Inject** does not survive reload — without Always you must Inject again + before Connect will find 1Shot. +- Optional host permissions are requested per origin when injecting. + +## Limitations + +- Closing the side panel destroys the `OWSProxy` session; the next RPC reopens the panel and recreates the proxy (wallet storage under the extension top-level partition should restore accounts). +- Extension page CSP must allow framing your wallet URL (`https:` and localhost are allowed in this test build). +- Safari `/create/` handoff still opens from Branding inside the panel iframe — allow popups from the extension page if prompted. The create tab does not require `window.opener` (Safari Web Extension often omits it); the result returns on a same-origin `BroadcastChannel`. + +## Manual test checklist + +- [ ] Inject on a page with strict `frame-src 'self'` — Connect Wallet still works (iframe is only in the side panel). +- [ ] MetaMask installed: EIP-6963 lists 1Shot; Prefer 1Shot toggles `window.ethereum`. +- [ ] MetaMask absent: `window.ethereum` is 1Shot after inject. +- [ ] Allowlist origin → reload → shim present without clicking Inject. +- [ ] Settings wallet URL → ngrok Branding → panel loads and unlock/connect works. +- [ ] `eth_requestAccounts` / send opens and focuses the side panel. diff --git a/extension/entrypoints/background.ts b/extension/entrypoints/background.ts new file mode 100644 index 0000000..72c18bd --- /dev/null +++ b/extension/entrypoints/background.ts @@ -0,0 +1,383 @@ +import { injectProviderIntoTab } from "../src/shared/inject"; +import { openWalletUi } from "../src/shared/openWalletUi"; +import { + isInteractiveMethod, + type ExtEip1193ResponseMessage, + type ExtEip1193RoutedRequestMessage, + type ExtRuntimeMessage, + type ExtStatusResponse, +} from "../src/shared/protocol"; +import { + addAllowlistOrigin, + getSettings, + isOriginAllowlisted, +} from "../src/shared/storage"; + +type PendingRequest = { + message: ExtEip1193RoutedRequestMessage; + resolve: (value: ExtEip1193ResponseMessage) => void; + reject: (error: Error) => void; + timer: ReturnType; +}; + +const REQUEST_TIMEOUT_MS = 120_000; +const SIDEPANEL_WAIT_MS = 15_000; + +let sidepanelPort: ReturnType | null = null; +const pendingById = new Map(); +const queuedWhilePanelBootstraps: PendingRequest[] = []; +let sidepanelReadyWaiters: Array<() => void> = []; + +/** Tabs we successfully injected (for event fan-out). */ +const injectedTabs = new Set(); + +function notifySidepanelReady(): void { + const waiters = sidepanelReadyWaiters; + sidepanelReadyWaiters = []; + for (const resolve of waiters) { + resolve(); + } +} + +function waitForSidepanelPort(timeoutMs: number): Promise { + if (sidepanelPort) { + return Promise.resolve(true); + } + return new Promise((resolve) => { + const timer = setTimeout(() => { + sidepanelReadyWaiters = sidepanelReadyWaiters.filter((w) => w !== onReady); + resolve(false); + }, timeoutMs); + const onReady = () => { + clearTimeout(timer); + resolve(true); + }; + sidepanelReadyWaiters.push(onReady); + }); +} + +function attachSidepanelPort( + port: ReturnType, +): void { + sidepanelPort = port; + void browser.action.setBadgeText({ text: "" }); + notifySidepanelReady(); + + port.onMessage.addListener((raw) => { + const msg = raw as ExtRuntimeMessage; + if (msg.type === "eip1193-response") { + const pending = pendingById.get(msg.id); + if (pending) { + clearTimeout(pending.timer); + pendingById.delete(msg.id); + pending.resolve(msg); + } + return; + } + if (msg.type === "eip1193-event") { + void broadcastEvent(msg.event, msg.params); + } + }); + + port.onDisconnect.addListener(() => { + if (sidepanelPort === port) { + sidepanelPort = null; + } + // Do not retry in-flight RPCs: Branding may already have submitted + // (eth_sendTransaction, etc.) and only the response was lost with the port. + for (const [, pending] of pendingById) { + failPending( + pending, + "Wallet UI disconnected before the request completed. Retry the request.", + ); + } + pendingById.clear(); + }); + + flushBootstrapQueue(); +} + +function failPending(pending: PendingRequest, message: string): void { + clearTimeout(pending.timer); + pending.resolve({ + type: "eip1193-response", + tabId: pending.message.tabId, + id: pending.message.id, + error: { + code: 4900, + message, + }, + }); +} + +function flushBootstrapQueue(): void { + while (queuedWhilePanelBootstraps.length > 0 && sidepanelPort) { + const next = queuedWhilePanelBootstraps.shift()!; + forwardToSidepanel(next); + } +} + +function forwardToSidepanel(pending: PendingRequest): void { + if (!sidepanelPort) { + queuedWhilePanelBootstraps.push(pending); + return; + } + pendingById.set(pending.message.id, pending); + try { + sidepanelPort.postMessage(pending.message); + } catch (error) { + console.warn("[1Shot] sidepanel postMessage failed; re-queueing", error); + pendingById.delete(pending.message.id); + sidepanelPort = null; + queuedWhilePanelBootstraps.push(pending); + } +} + +/** + * Ensure a live sidepanel port before forwarding RPC. + * + * Important: do NOT call openWalletUi() when a port is already connected — + * Firefox sidebarAction.open() can reload the panel and drop in-flight + * postMessage traffic (Connect then spins forever with no Branding UI). + */ +async function ensureSidepanelForRequest( + message: ExtEip1193RoutedRequestMessage, +): Promise { + if (sidepanelPort) { + return; + } + + const interactive = isInteractiveMethod(message.method); + try { + await openWalletUi(); + if (interactive) { + await browser.action.setBadgeText({ text: "" }); + } + } catch (error) { + console.warn("[1Shot] openWalletUi failed", error); + if (interactive) { + try { + await browser.action.setBadgeText({ text: "!" }); + await browser.action.setBadgeBackgroundColor({ color: "#3d8bfd" }); + } catch { + // ignore + } + } + } + + const ready = await waitForSidepanelPort(SIDEPANEL_WAIT_MS); + if (!ready) { + console.warn("[1Shot] sidepanel did not connect in time"); + } +} + +async function handleEip1193Request( + message: ExtEip1193RoutedRequestMessage, +): Promise { + await ensureSidepanelForRequest(message); + + return new Promise((resolve, reject) => { + const timer = setTimeout(() => { + pendingById.delete(message.id); + const idx = queuedWhilePanelBootstraps.findIndex( + (p) => p.message.id === message.id, + ); + if (idx >= 0) queuedWhilePanelBootstraps.splice(idx, 1); + resolve({ + type: "eip1193-response", + tabId: message.tabId, + id: message.id, + error: { + code: -32603, + message: + "Wallet UI timed out — open the 1Shot side panel (toolbar → Open wallet panel) and try again.", + }, + }); + }, REQUEST_TIMEOUT_MS); + + const pending: PendingRequest = { message, resolve, reject, timer }; + forwardToSidepanel(pending); + }); +} + +async function broadcastEvent(event: string, params: unknown[]): Promise { + const payload: ExtRuntimeMessage = { + type: "eip1193-event", + event, + params, + }; + for (const tabId of injectedTabs) { + try { + await browser.tabs.sendMessage(tabId, payload); + } catch { + injectedTabs.delete(tabId); + } + } +} + +async function statusForTab(tabId?: number): Promise { + const settings = await getSettings(); + if (tabId == null) { + return { + ok: true, + injected: false, + allowlisted: false, + walletUrl: settings.walletUrl, + preferOneshot: settings.preferOneshot, + }; + } + try { + const tab = await browser.tabs.get(tabId); + const origin = tab.url ? new URL(tab.url).origin : undefined; + return { + ok: true, + injected: injectedTabs.has(tabId), + origin, + allowlisted: origin + ? isOriginAllowlisted(origin, settings.allowlist) + : false, + walletUrl: settings.walletUrl, + preferOneshot: settings.preferOneshot, + }; + } catch { + return { + ok: false, + injected: false, + allowlisted: false, + walletUrl: settings.walletUrl, + preferOneshot: settings.preferOneshot, + error: "Tab not found", + }; + } +} + +async function maybeAutoInject(tabId: number, url?: string): Promise { + if (!url) return; + let origin: string; + try { + origin = new URL(url).origin; + } catch { + return; + } + const settings = await getSettings(); + if (!isOriginAllowlisted(origin, settings.allowlist)) return; + + const result = await injectProviderIntoTab(tabId); + if (result.ok) { + injectedTabs.add(tabId); + } +} + +export default defineBackground(() => { + try { + const chromeApi = ( + globalThis as unknown as { + chrome?: { + sidePanel?: { + setPanelBehavior: (options: { + openPanelOnActionClick: boolean; + }) => Promise; + }; + }; + } + ).chrome; + void chromeApi?.sidePanel?.setPanelBehavior?.({ + openPanelOnActionClick: false, + }); + } catch { + // Firefox + } + + browser.runtime.onConnect.addListener((port) => { + if (port.name === "sidepanel") { + attachSidepanelPort(port); + } + }); + + browser.runtime.onMessage.addListener( + (raw: ExtRuntimeMessage, sender, sendResponse) => { + const respond = (value: unknown) => { + sendResponse(value); + }; + + void (async () => { + try { + switch (raw.type) { + case "get-status": { + respond(await statusForTab(raw.tabId ?? sender.tab?.id)); + break; + } + case "open-wallet-ui": { + await openWalletUi(raw.windowId); + respond({ ok: true }); + break; + } + case "add-allowlist-origin": { + await addAllowlistOrigin(raw.origin); + respond({ ok: true }); + break; + } + case "inject-tab": { + const result = await injectProviderIntoTab(raw.tabId); + if (result.ok) injectedTabs.add(raw.tabId); + respond(result); + break; + } + case "eip1193-request": { + const tabId = sender.tab?.id; + if (typeof tabId !== "number" || tabId <= 0) { + respond({ + type: "eip1193-response", + tabId: -1, + id: raw.id, + error: { + code: -32603, + message: + "EIP-1193 request has no valid tab id (content script sender.tab missing)", + }, + } satisfies ExtEip1193ResponseMessage); + break; + } + const message: ExtEip1193RoutedRequestMessage = { + ...raw, + tabId, + }; + injectedTabs.add(tabId); + console.info("[1Shot] eip1193-request", message.method, { + tabId, + id: message.id, + hasPanel: Boolean(sidepanelPort), + }); + const response = await handleEip1193Request(message); + respond(response); + break; + } + case "sidepanel-ready": { + respond({ ok: true }); + break; + } + default: + respond({ ok: false, error: "Unknown message" }); + } + } catch (error) { + respond({ + ok: false, + error: error instanceof Error ? error.message : String(error), + }); + } + })(); + + return true; + }, + ); + + browser.tabs.onUpdated.addListener((tabId, changeInfo, tab) => { + if (changeInfo.status === "complete") { + void maybeAutoInject(tabId, tab.url); + } + }); + + browser.tabs.onRemoved.addListener((tabId) => { + injectedTabs.delete(tabId); + }); +}); diff --git a/extension/entrypoints/content.ts b/extension/entrypoints/content.ts new file mode 100644 index 0000000..5aac1d7 --- /dev/null +++ b/extension/entrypoints/content.ts @@ -0,0 +1,157 @@ +import { + CONTENT_SOURCE, + PROTOCOL_VERSION, + PROVIDER_ICON_DATA_URI, +} from "../src/shared/constants"; +import { + isInpageMessage, + type ContentToInpageConfig, + type ContentToInpageEvent, + type ContentToInpageResponse, + type ExtEip1193EventMessage, + type ExtEip1193RequestMessage, + type ExtEip1193ResponseMessage, + type ExtRuntimeMessage, +} from "../src/shared/protocol"; +import { getSettings } from "../src/shared/storage"; + +const MARKER = "data-oneshot-content"; +const BRIDGE_FLAG = "__ONESHOT_CONTENT_BRIDGE__"; + +type BridgeGlobal = typeof globalThis & { + [BRIDGE_FLAG]?: boolean; +}; + +function postToPage( + message: ContentToInpageResponse | ContentToInpageEvent | ContentToInpageConfig, +): void { + window.postMessage(message, "*"); +} + +async function sendConfig(): Promise { + const settings = await getSettings(); + postToPage({ + source: CONTENT_SOURCE, + version: PROTOCOL_VERSION, + type: "config", + preferOneshot: settings.preferOneshot, + iconUrl: PROVIDER_ICON_DATA_URI, + }); +} + +function installBridge(): void { + const g = globalThis as BridgeGlobal; + if (g[BRIDGE_FLAG]) { + return; + } + g[BRIDGE_FLAG] = true; + + window.addEventListener("message", (event) => { + if (event.source !== window) return; + if (!isInpageMessage(event.data)) return; + + if (event.data.type === "ready") { + void sendConfig(); + return; + } + + if (event.data.type !== "request") return; + + const { id, method, params } = event.data; + + void (async () => { + try { + const response = (await browser.runtime.sendMessage({ + type: "eip1193-request", + id, + method, + params, + } satisfies ExtEip1193RequestMessage)) as unknown; + + if ( + !response || + typeof response !== "object" || + !("type" in response) || + response.type !== "eip1193-response" + ) { + const errorMessage = + response && + typeof response === "object" && + "error" in response && + typeof (response as { error: unknown }).error === "string" + ? (response as { error: string }).error + : "No response from wallet"; + postToPage({ + source: CONTENT_SOURCE, + version: PROTOCOL_VERSION, + type: "response", + id, + error: { + code: -32603, + message: errorMessage, + }, + }); + return; + } + + const ok = response as ExtEip1193ResponseMessage; + postToPage({ + source: CONTENT_SOURCE, + version: PROTOCOL_VERSION, + type: "response", + id: ok.id, + result: ok.result, + error: ok.error, + }); + } catch (error) { + postToPage({ + source: CONTENT_SOURCE, + version: PROTOCOL_VERSION, + type: "response", + id, + error: { + code: -32603, + message: error instanceof Error ? error.message : String(error), + }, + }); + } + })(); + }); + + browser.runtime.onMessage.addListener((message: ExtRuntimeMessage) => { + if (message.type === "eip1193-event") { + const eventMsg = message as ExtEip1193EventMessage; + postToPage({ + source: CONTENT_SOURCE, + version: PROTOCOL_VERSION, + type: "event", + event: eventMsg.event, + params: eventMsg.params, + }); + } + if (message.type === "eip1193-response") { + postToPage({ + source: CONTENT_SOURCE, + version: PROTOCOL_VERSION, + type: "response", + id: message.id, + result: message.result, + error: message.error, + }); + } + }); +} + +export default defineContentScript({ + matches: [""], + registration: "runtime", + async main() { + // Document marker is for humans/devtools; the isolated-world flag is what + // prevents double-binding within one content-script context. When WXT + // invalidates an old context and injects a new one, the flag is gone and + // we must re-bind — even if the DOM marker remains. + document.documentElement.setAttribute(MARKER, "1"); + installBridge(); + await sendConfig(); + }, +}); diff --git a/extension/entrypoints/inpage.ts b/extension/entrypoints/inpage.ts new file mode 100644 index 0000000..795be73 --- /dev/null +++ b/extension/entrypoints/inpage.ts @@ -0,0 +1,212 @@ +import { + CONTENT_SOURCE, + INPAGE_SOURCE, + PROTOCOL_VERSION, + PROVIDER_ICON_DATA_URI, + PROVIDER_INFO, +} from "../src/shared/constants"; +import { + isContentMessage, + type ContentToInpageConfig, + type InpageToContentRequest, +} from "../src/shared/protocol"; + +declare global { + interface Window { + ethereum?: EthereumProvider; + __ONESHOT_OWS_INJECTED__?: boolean; + } +} + +type EthereumProvider = { + request: (args: { method: string; params?: unknown }) => Promise; + on: (event: string, listener: (...args: unknown[]) => void) => void; + removeListener: (event: string, listener: (...args: unknown[]) => void) => void; + providers?: EthereumProvider[]; + is1Shot?: boolean; + isMetaMask?: boolean; +}; + +type Pending = { + resolve: (value: unknown) => void; + reject: (error: Error) => void; +}; + +const pending = new Map(); +const listeners = new Map void>>(); + +let preferOneshot = false; +let iconUrl = ""; +let announced = false; +let installedProvider: EthereumProvider | null = null; + +function nextId(): string { + return crypto.randomUUID(); +} + +function postToContent( + message: InpageToContentRequest | { source: typeof INPAGE_SOURCE; version: typeof PROTOCOL_VERSION; type: "ready" }, +): void { + window.postMessage(message, "*"); +} + +function emit(event: string, ...params: unknown[]): void { + const set = listeners.get(event); + if (!set) return; + for (const listener of set) { + try { + listener(...params); + } catch { + // Host listener errors must not break the provider. + } + } +} + +function createProvider(): EthereumProvider { + const provider: EthereumProvider = { + is1Shot: true, + isMetaMask: false, + request({ method, params }) { + const id = nextId(); + return new Promise((resolve, reject) => { + pending.set(id, { resolve, reject }); + postToContent({ + source: INPAGE_SOURCE, + version: PROTOCOL_VERSION, + type: "request", + id, + method, + params, + }); + }); + }, + on(event, listener) { + let set = listeners.get(event); + if (!set) { + set = new Set(); + listeners.set(event, set); + } + set.add(listener); + }, + removeListener(event, listener) { + listeners.get(event)?.delete(listener); + }, + }; + return provider; +} + +function installWindowEthereum(provider: EthereumProvider): void { + const existing = window.ethereum; + if (!existing) { + window.ethereum = provider; + return; + } + if (preferOneshot) { + const providers = existing.providers ? [...existing.providers] : [existing]; + if (!providers.includes(provider)) { + providers.unshift(provider); + } + provider.providers = providers; + window.ethereum = provider; + return; + } + // Coexist via providers[] (EIP-1193 multi-injected wallet discovery). + if (Array.isArray(existing.providers)) { + if (!existing.providers.includes(provider)) { + existing.providers.push(provider); + } + return; + } + // Wallet present but no providers[] — create one so 1Shot is still reachable + // when MetaMask (or similar) is installed without exposing the array. + try { + existing.providers = [existing, provider]; + } catch { + // Some providers freeze/seal window.ethereum; EIP-6963 still announces us. + } +} + +function announceEip6963(provider: EthereumProvider): void { + if (announced) return; + announced = true; + + const info = { + uuid: PROVIDER_INFO.uuid, + name: PROVIDER_INFO.name, + icon: iconUrl || PROVIDER_ICON_DATA_URI, + rdns: PROVIDER_INFO.rdns, + }; + + const announce = () => { + window.dispatchEvent( + new CustomEvent("eip6963:announceProvider", { + detail: Object.freeze({ info, provider }), + }), + ); + }; + + announce(); + window.addEventListener("eip6963:requestProvider", announce); +} + +function applyConfig(config: ContentToInpageConfig): void { + preferOneshot = config.preferOneshot; + iconUrl = config.iconUrl; + if (!installedProvider) { + installedProvider = createProvider(); + installWindowEthereum(installedProvider); + announceEip6963(installedProvider); + return; + } + // Content sends config on startup and again on inpage `ready`. Reuse the + // same provider so `window.ethereum.providers` and EIP-6963 stay single-entry. + if (preferOneshot && window.ethereum !== installedProvider) { + installWindowEthereum(installedProvider); + } +} + +function onMessage(event: MessageEvent): void { + if (event.source !== window) return; + if (!isContentMessage(event.data)) return; + + if (event.data.type === "config") { + applyConfig(event.data); + return; + } + + if (event.data.type === "response") { + const entry = pending.get(event.data.id); + if (!entry) return; + pending.delete(event.data.id); + if (event.data.error) { + const err = new Error(event.data.error.message) as Error & { + code?: number; + data?: unknown; + }; + err.code = event.data.error.code; + err.data = event.data.error.data; + entry.reject(err); + } else { + entry.resolve(event.data.result); + } + return; + } + + if (event.data.type === "event") { + emit(event.data.event, ...event.data.params); + } +} + +export default defineUnlistedScript(() => { + if (window.__ONESHOT_OWS_INJECTED__) { + return; + } + window.__ONESHOT_OWS_INJECTED__ = true; + + window.addEventListener("message", onMessage); + postToContent({ + source: INPAGE_SOURCE, + version: PROTOCOL_VERSION, + type: "ready", + }); +}); diff --git a/extension/entrypoints/options/index.html b/extension/entrypoints/options/index.html new file mode 100644 index 0000000..f6dfb08 --- /dev/null +++ b/extension/entrypoints/options/index.html @@ -0,0 +1,40 @@ + + + + + + 1Shot Wallet — Settings + + + +
+

1Shot Wallet settings

+ + +

+ Default is https://wallet.1shotapi.com/. Use your ngrok or + local branding URL while developing. +

+ + + + +

Example: https://app.uniswap.org

+ +
+ + +
+
+ + + diff --git a/extension/entrypoints/options/main.ts b/extension/entrypoints/options/main.ts new file mode 100644 index 0000000..5d76796 --- /dev/null +++ b/extension/entrypoints/options/main.ts @@ -0,0 +1,58 @@ +import { getSettings, setSettings } from "../../src/shared/storage"; + +const walletUrlInput = document.getElementById("wallet-url") as HTMLInputElement; +const preferInput = document.getElementById( + "prefer-oneshot", +) as HTMLInputElement; +const allowlistInput = document.getElementById("allowlist") as HTMLTextAreaElement; +const saveBtn = document.getElementById("save-btn") as HTMLButtonElement; +const statusEl = document.getElementById("status")!; + +function setStatus(message: string, kind: "ok" | "error" | "" = ""): void { + statusEl.textContent = message; + statusEl.classList.toggle("ok", kind === "ok"); + statusEl.classList.toggle("error", kind === "error"); +} + +function parseAllowlist(text: string): string[] { + const origins = new Set(); + for (const line of text.split(/\r?\n/)) { + const trimmed = line.trim(); + if (!trimmed) continue; + try { + origins.add(new URL(trimmed).origin); + } catch { + throw new Error(`Invalid origin: ${trimmed}`); + } + } + return [...origins]; +} + +async function load(): Promise { + const settings = await getSettings(); + walletUrlInput.value = settings.walletUrl; + preferInput.checked = settings.preferOneshot; + allowlistInput.value = settings.allowlist.join("\n"); +} + +saveBtn.addEventListener("click", () => { + void (async () => { + try { + new URL(walletUrlInput.value.trim()); + const allowlist = parseAllowlist(allowlistInput.value); + await setSettings({ + walletUrl: walletUrlInput.value.trim(), + preferOneshot: preferInput.checked, + allowlist, + }); + setStatus("Saved", "ok"); + } catch (error) { + setStatus( + error instanceof Error ? error.message : String(error), + "error", + ); + } + })(); +}); + +void load(); diff --git a/extension/entrypoints/options/style.css b/extension/entrypoints/options/style.css new file mode 100644 index 0000000..809176e --- /dev/null +++ b/extension/entrypoints/options/style.css @@ -0,0 +1,101 @@ +:root { + color-scheme: dark; + --bg: #0b0f14; + --fg: #e8eef5; + --muted: #9aabbd; + --accent: #3d8bfd; + --border: #243041; + --field: #121820; +} + +* { + box-sizing: border-box; +} + +body { + margin: 0; + min-height: 100vh; + background: var(--bg); + color: var(--fg); + font: 14px/1.45 system-ui, sans-serif; +} + +main { + max-width: 560px; + margin: 0 auto; + padding: 28px 20px 48px; + display: flex; + flex-direction: column; + gap: 12px; +} + +h1 { + margin: 0 0 8px; + font-size: 1.35rem; +} + +label { + display: flex; + flex-direction: column; + gap: 6px; + font-weight: 600; +} + +label.row { + flex-direction: row; + align-items: center; + gap: 10px; + font-weight: 500; +} + +input[type="url"], +textarea { + width: 100%; + border: 1px solid var(--border); + border-radius: 8px; + background: var(--field); + color: var(--fg); + padding: 10px 12px; + font: inherit; +} + +.hint { + margin: -4px 0 4px; + color: var(--muted); + font-size: 12px; +} + +code { + font-size: 0.92em; +} + +.actions { + display: flex; + align-items: center; + gap: 12px; + margin-top: 8px; +} + +button { + appearance: none; + border: 0; + border-radius: 8px; + padding: 10px 16px; + background: var(--accent); + color: white; + font: inherit; + font-weight: 600; + cursor: pointer; +} + +.status { + color: var(--muted); +} + +.status.ok { + color: #7ddea2; +} + +.status.error { + color: #ff8e8e; +} diff --git a/extension/entrypoints/popup/index.html b/extension/entrypoints/popup/index.html new file mode 100644 index 0000000..55e9aa0 --- /dev/null +++ b/extension/entrypoints/popup/index.html @@ -0,0 +1,32 @@ + + + + + + 1Shot Wallet + + + +
+ +
+ 1Shot Wallet +

No active tab

+
+
+ +
+

+ + + + Settings +
+ + + + diff --git a/extension/entrypoints/popup/main.ts b/extension/entrypoints/popup/main.ts new file mode 100644 index 0000000..74866d2 --- /dev/null +++ b/extension/entrypoints/popup/main.ts @@ -0,0 +1,109 @@ +import { queryActiveDappTab } from "../../src/shared/activeTab"; +import type { + ExtRuntimeMessage, + ExtStatusResponse, +} from "../../src/shared/protocol"; + +const originLine = document.getElementById("origin-line")!; +const statusEl = document.getElementById("status")!; +const injectBtn = document.getElementById("inject-btn") as HTMLButtonElement; +const allowlistBtn = document.getElementById( + "allowlist-btn", +) as HTMLButtonElement; +const openWalletBtn = document.getElementById( + "open-wallet-btn", +) as HTMLButtonElement; +const optionsLink = document.getElementById("options-link") as HTMLAnchorElement; + +let activeTabId: number | undefined; +let activeOrigin: string | undefined; + +function setStatus(message: string, kind: "ok" | "error" | "" = ""): void { + statusEl.textContent = message; + statusEl.classList.toggle("error", kind === "error"); + statusEl.classList.toggle("ok", kind === "ok"); +} + +async function refresh(): Promise { + const tab = await queryActiveDappTab(); + activeTabId = tab?.id; + try { + activeOrigin = tab?.url ? new URL(tab.url).origin : undefined; + } catch { + activeOrigin = undefined; + } + originLine.textContent = activeOrigin ?? "Unsupported page"; + + const status = (await browser.runtime.sendMessage({ + type: "get-status", + tabId: activeTabId, + } satisfies ExtRuntimeMessage)) as ExtStatusResponse; + + if (!status?.ok) { + setStatus(status?.error ?? "Status unavailable", "error"); + return; + } + + const bits = [ + status.injected ? "Provider injected" : "Not injected", + status.allowlisted ? "allowlisted" : null, + ].filter(Boolean); + setStatus(bits.join(" · ")); + allowlistBtn.disabled = !activeOrigin; + injectBtn.disabled = activeTabId == null; +} + +injectBtn.addEventListener("click", () => { + void (async () => { + if (activeTabId == null) return; + setStatus("Injecting…"); + const result = (await browser.runtime.sendMessage({ + type: "inject-tab", + tabId: activeTabId, + } satisfies ExtRuntimeMessage)) as { ok: boolean; error?: string }; + if (!result.ok) { + setStatus(result.error ?? "Inject failed", "error"); + return; + } + setStatus("Provider injected", "ok"); + await refresh(); + })(); +}); + +allowlistBtn.addEventListener("click", () => { + void (async () => { + if (!activeOrigin || activeTabId == null) return; + await browser.runtime.sendMessage({ + type: "add-allowlist-origin", + origin: activeOrigin, + } satisfies ExtRuntimeMessage); + const result = (await browser.runtime.sendMessage({ + type: "inject-tab", + tabId: activeTabId, + } satisfies ExtRuntimeMessage)) as { ok: boolean; error?: string }; + if (!result.ok) { + setStatus(result.error ?? "Allowlisted but inject failed", "error"); + return; + } + setStatus(`Always inject on ${activeOrigin}`, "ok"); + await refresh(); + })(); +}); + +openWalletBtn.addEventListener("click", () => { + void (async () => { + const win = await browser.windows.getCurrent(); + await browser.runtime.sendMessage({ + type: "open-wallet-ui", + windowId: win.id, + } satisfies ExtRuntimeMessage); + })(); +}); + +optionsLink.href = browser.runtime.getURL("/options.html"); +optionsLink.addEventListener("click", (event) => { + event.preventDefault(); + void browser.runtime.openOptionsPage(); +}); + +void refresh(); diff --git a/extension/entrypoints/popup/style.css b/extension/entrypoints/popup/style.css new file mode 100644 index 0000000..02e1629 --- /dev/null +++ b/extension/entrypoints/popup/style.css @@ -0,0 +1,90 @@ +:root { + color-scheme: dark; + --bg: #0b0f14; + --fg: #e8eef5; + --muted: #9aabbd; + --accent: #3d8bfd; + --border: #243041; +} + +* { + box-sizing: border-box; +} + +body { + margin: 0; + width: 320px; + padding: 14px; + background: var(--bg); + color: var(--fg); + font: 13px/1.4 system-ui, sans-serif; +} + +header { + display: flex; + gap: 10px; + align-items: center; + margin-bottom: 14px; +} + +header strong { + display: block; + font-size: 14px; +} + +.muted { + margin: 2px 0 0; + color: var(--muted); + font-size: 11px; + word-break: break-all; +} + +.stack { + display: flex; + flex-direction: column; + gap: 8px; +} + +button { + appearance: none; + border: 0; + border-radius: 8px; + padding: 10px 12px; + background: var(--accent); + color: white; + font: inherit; + font-weight: 600; + cursor: pointer; +} + +button.secondary { + background: transparent; + color: var(--fg); + border: 1px solid var(--border); +} + +button:disabled { + opacity: 0.5; + cursor: not-allowed; +} + +.status { + min-height: 1.2em; + color: var(--muted); + margin: 0; +} + +.status.error { + color: #ff8e8e; +} + +.status.ok { + color: #7ddea2; +} + +a { + color: var(--accent); + text-decoration: none; + font-size: 12px; + margin-top: 4px; +} diff --git a/extension/entrypoints/sidepanel/index.html b/extension/entrypoints/sidepanel/index.html new file mode 100644 index 0000000..4c6d9af --- /dev/null +++ b/extension/entrypoints/sidepanel/index.html @@ -0,0 +1,29 @@ + + + + + + 1Shot Wallet + + + +
+
+ 1Shot + No active tab +
+
+ + +
+

+
+
+
Loading wallet…
+
+
+ + + diff --git a/extension/entrypoints/sidepanel/main.ts b/extension/entrypoints/sidepanel/main.ts new file mode 100644 index 0000000..b9b3c57 --- /dev/null +++ b/extension/entrypoints/sidepanel/main.ts @@ -0,0 +1,273 @@ +import { + EWalletPresentationMode, + OWSProxy, +} from "@1shotapi/ows-provider"; +import { queryActiveDappTab } from "../../src/shared/activeTab"; +import type { + ExtEip1193ResponseMessage, + ExtEip1193RoutedRequestMessage, + ExtRuntimeMessage, + ExtStatusResponse, +} from "../../src/shared/protocol"; +import { getSettings } from "../../src/shared/storage"; + +const statusEl = document.getElementById("status")!; +const container = document.getElementById("wallet-container")!; +const originLine = document.getElementById("origin-line")!; +const injectStatus = document.getElementById("inject-status")!; +const injectBtn = document.getElementById("inject-btn") as HTMLButtonElement; +const allowlistBtn = document.getElementById( + "allowlist-btn", +) as HTMLButtonElement; + +function setStatus(message: string, isError = false): void { + statusEl.textContent = message; + statusEl.classList.toggle("error", isError); + statusEl.classList.toggle("hidden", false); +} + +function hideStatus(): void { + statusEl.classList.add("hidden"); +} + +function setInjectStatus( + message: string, + kind: "ok" | "error" | "" = "", +): void { + injectStatus.textContent = message; + injectStatus.classList.toggle("error", kind === "error"); + injectStatus.classList.toggle("ok", kind === "ok"); +} + +let proxy: OWSProxy | null = null; +let connecting: Promise | null = null; +let activeTabId: number | undefined; +let activeOrigin: string | undefined; +const port = browser.runtime.connect({ name: "sidepanel" }); + +async function refreshActiveTab(): Promise { + const tab = await queryActiveDappTab(); + activeTabId = tab?.id; + try { + activeOrigin = tab?.url ? new URL(tab.url).origin : undefined; + } catch { + activeOrigin = undefined; + } + originLine.textContent = activeOrigin ?? "No active tab — focus a dApp window"; + injectBtn.disabled = activeTabId == null; + allowlistBtn.disabled = !activeOrigin || activeTabId == null; + + if (activeTabId != null) { + const status = (await browser.runtime.sendMessage({ + type: "get-status", + tabId: activeTabId, + } satisfies ExtRuntimeMessage)) as ExtStatusResponse; + if (status?.ok) { + setInjectStatus( + [ + status.injected ? "Provider injected" : "Not injected on this tab", + status.allowlisted ? "allowlisted" : null, + ] + .filter(Boolean) + .join(" · "), + ); + } + } else { + setInjectStatus("Focus a normal browser tab (not this sidebar), then retry"); + } +} + +async function ensureProxy(): Promise { + if (proxy) return proxy; + if (connecting) return connecting; + + connecting = (async () => { + const settings = await getSettings(); + setStatus(`Connecting to ${settings.walletUrl}…`); + + const created = await OWSProxy.create(container, settings.walletUrl, { + presentationMode: EWalletPresentationMode.Inline, + classList: ["oneshot-ows-extension-host"], + }); + + try { + await created.rpc("setStyle", { + copy: { productName: "1Shot Wallet" }, + features: { hideCloseBox: true }, + }); + } catch (error) { + console.warn("[1Shot sidepanel] setStyle failed", error); + } + + created.ethereum.on("accountsChanged", (...params: unknown[]) => { + port.postMessage({ + type: "eip1193-event", + event: "accountsChanged", + params, + } satisfies ExtRuntimeMessage); + }); + created.ethereum.on("chainChanged", (...params: unknown[]) => { + port.postMessage({ + type: "eip1193-event", + event: "chainChanged", + params, + } satisfies ExtRuntimeMessage); + }); + created.ethereum.on("connect", (...params: unknown[]) => { + port.postMessage({ + type: "eip1193-event", + event: "connect", + params, + } satisfies ExtRuntimeMessage); + }); + created.ethereum.on("disconnect", (...params: unknown[]) => { + port.postMessage({ + type: "eip1193-event", + event: "disconnect", + params, + } satisfies ExtRuntimeMessage); + }); + + proxy = created; + hideStatus(); + void browser.runtime.sendMessage({ type: "sidepanel-ready" }); + return created; + })().catch((error) => { + proxy = null; + connecting = null; + throw error; + }); + + return connecting; +} + +async function handleRequest( + message: ExtEip1193RoutedRequestMessage, +): Promise { + const reply = (payload: ExtEip1193ResponseMessage) => { + port.postMessage(payload); + }; + + try { + const live = await ensureProxy(); + console.info("[1Shot sidepanel] eip1193", message.method, message.id); + const result = await live.ethereum.request({ + method: message.method, + params: message.params as never, + }); + console.info("[1Shot sidepanel] eip1193 ok", message.method, message.id); + reply({ + type: "eip1193-response", + tabId: message.tabId, + id: message.id, + result, + }); + } catch (error) { + const err = error as { code?: number; message?: string; data?: unknown }; + console.warn("[1Shot sidepanel] eip1193 error", message.method, err); + reply({ + type: "eip1193-response", + tabId: message.tabId, + id: message.id, + error: { + code: typeof err.code === "number" ? err.code : 4001, + message: + err.message || + (error instanceof Error ? error.message : String(error)), + data: err.data, + }, + }); + } +} + +port.onMessage.addListener((raw) => { + const message = raw as ExtRuntimeMessage; + if (message.type !== "eip1193-request") return; + if (typeof message.tabId !== "number" || message.tabId <= 0) { + console.warn("[1Shot sidepanel] dropping eip1193-request without tab id"); + return; + } + void handleRequest(message as ExtEip1193RoutedRequestMessage); +}); + +injectBtn.addEventListener("click", () => { + void (async () => { + await refreshActiveTab(); + if (activeTabId == null) { + setInjectStatus("No dApp tab focused — click the Uniswap tab first", "error"); + return; + } + setInjectStatus("Injecting…"); + const result = (await browser.runtime.sendMessage({ + type: "inject-tab", + tabId: activeTabId, + } satisfies ExtRuntimeMessage)) as { ok: boolean; error?: string }; + if (!result.ok) { + setInjectStatus(result.error ?? "Inject failed", "error"); + return; + } + setInjectStatus( + "Injected — reload the dApp tab if Connect still hides 1Shot", + "ok", + ); + })(); +}); + +allowlistBtn.addEventListener("click", () => { + void (async () => { + await refreshActiveTab(); + if (!activeOrigin || activeTabId == null) { + setInjectStatus("No dApp tab focused — click the Uniswap tab first", "error"); + return; + } + await browser.runtime.sendMessage({ + type: "add-allowlist-origin", + origin: activeOrigin, + } satisfies ExtRuntimeMessage); + const result = (await browser.runtime.sendMessage({ + type: "inject-tab", + tabId: activeTabId, + } satisfies ExtRuntimeMessage)) as { ok: boolean; error?: string }; + if (!result.ok) { + setInjectStatus(result.error ?? "Allowlisted but inject failed", "error"); + return; + } + setInjectStatus(`Always inject on ${activeOrigin}`, "ok"); + })(); +}); + +browser.tabs.onActivated.addListener(() => { + void refreshActiveTab(); +}); +browser.tabs.onUpdated.addListener((_tabId, changeInfo) => { + if (changeInfo.status === "complete" || changeInfo.url) { + void refreshActiveTab(); + } +}); +browser.windows.onFocusChanged.addListener(() => { + void refreshActiveTab(); +}); + +void (async () => { + void refreshActiveTab(); + try { + await ensureProxy(); + } catch (error) { + setStatus( + error instanceof Error + ? `Failed to load wallet: ${error.message}` + : "Failed to load wallet", + true, + ); + } +})(); + +/** Surface hung Postmate/iframe loads instead of infinite “Loading wallet…” */ +window.setTimeout(() => { + if (proxy || statusEl.classList.contains("hidden")) return; + if (statusEl.classList.contains("error")) return; + setStatus( + `${statusEl.textContent || "Loading wallet…"} — still waiting. Check Settings → Wallet iframe URL (must be https://…), then reopen the sidebar.`, + true, + ); +}, 20_000); \ No newline at end of file diff --git a/extension/entrypoints/sidepanel/style.css b/extension/entrypoints/sidepanel/style.css new file mode 100644 index 0000000..2394158 --- /dev/null +++ b/extension/entrypoints/sidepanel/style.css @@ -0,0 +1,144 @@ +*, +*::before, +*::after { + box-sizing: border-box; +} + +html, +body { + margin: 0; + height: 100%; + width: 100%; + overflow: hidden; + background: #0b0f14; + color: #e8eef5; + font-family: system-ui, sans-serif; + display: flex; + flex-direction: column; +} + +/* + * Keep Inject/Always above the Inline OWSProxy iframe. ows-provider applies + * absolute fill styles to #wallet-container; without a sticky chrome + + * isolated wrap those styles can paint over the toolbar after handshake. + */ +.chrome { + position: sticky; + top: 0; + flex: 0 0 auto; + display: grid; + grid-template-columns: 1fr auto; + grid-template-rows: auto auto; + gap: 6px 8px; + padding: 8px 10px; + border-bottom: 1px solid #243041; + background: #121820; + z-index: 100; +} + +.chrome-meta { + display: flex; + flex-direction: column; + gap: 2px; + min-width: 0; +} + +.chrome-meta strong { + font-size: 12px; +} + +.muted { + color: #9aabbd; + font-size: 10px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.chrome-actions { + display: flex; + gap: 6px; + align-items: start; +} + +.chrome button { + appearance: none; + border: 0; + border-radius: 6px; + padding: 6px 10px; + background: #3d8bfd; + color: white; + font: inherit; + font-size: 11px; + font-weight: 600; + cursor: pointer; + white-space: nowrap; +} + +.chrome button.secondary { + background: transparent; + color: #e8eef5; + border: 1px solid #243041; +} + +.chrome button:disabled { + opacity: 0.5; + cursor: not-allowed; +} + +.inject-status { + grid-column: 1 / -1; + margin: 0; + min-height: 1em; + color: #9aabbd; + font-size: 10px; +} + +.inject-status.error { + color: #ff8e8e; +} + +.inject-status.ok { + color: #7ddea2; +} + +#wallet-wrap { + position: relative; + flex: 1 1 auto; + min-height: 0; + width: 100%; + overflow: hidden; + isolation: isolate; + z-index: 0; +} + +.status { + position: absolute; + inset: 0; + display: grid; + place-items: center; + padding: 1.5rem; + text-align: center; + font-size: 0.9rem; + z-index: 2; + pointer-events: none; + background: #0b0f14; +} + +.status.error { + color: #ff8e8e; + pointer-events: auto; +} + +.status.hidden { + display: none; +} + +.wallet-container { + position: absolute; + inset: 0; + width: 100%; + height: 100%; + overflow: hidden; + z-index: 1; +} diff --git a/extension/package.json b/extension/package.json new file mode 100644 index 0000000..44573c9 --- /dev/null +++ b/extension/package.json @@ -0,0 +1,30 @@ +{ + "name": "@1shotapi/oneshot-wallet-extension", + "version": "0.1.0", + "private": true, + "description": "1Shot Wallet browser extension - MetaMask-style EIP-1193 shim + side-panel Inline OWSProxy host", + "type": "module", + "scripts": { + "dev": "wxt", + "dev:firefox": "wxt build -b firefox --mv3", + "dev:firefox:vite": "wxt -b firefox", + "build": "wxt build", + "build:firefox": "wxt build -b firefox --mv3", + "zip": "wxt zip", + "zip:firefox": "wxt zip -b firefox --mv3", + "pack:chrome": "wxt zip", + "pack:firefox": "wxt zip -b firefox --mv3", + "lint": "tsc -p tsconfig.json --noEmit", + "clean": "node scripts/clean.mjs", + "postinstall": "wxt prepare" + }, + "dependencies": { + "@1shotapi/ows-provider": "^0.4.2", + "@1shotapi/ows-types": "^0.5.1" + }, + "devDependencies": { + "@types/chrome": "^0.0.287", + "typescript": "~5.8.0", + "wxt": "^0.21.3" + } +} diff --git a/extension/public/icon/128.png b/extension/public/icon/128.png new file mode 100644 index 0000000..82efcf9 Binary files /dev/null and b/extension/public/icon/128.png differ diff --git a/extension/public/icon/16.png b/extension/public/icon/16.png new file mode 100644 index 0000000..82efcf9 Binary files /dev/null and b/extension/public/icon/16.png differ diff --git a/extension/public/icon/48.png b/extension/public/icon/48.png new file mode 100644 index 0000000..82efcf9 Binary files /dev/null and b/extension/public/icon/48.png differ diff --git a/extension/public/icon/icon.svg b/extension/public/icon/icon.svg new file mode 100644 index 0000000..f91a366 --- /dev/null +++ b/extension/public/icon/icon.svg @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/extension/scripts/clean.mjs b/extension/scripts/clean.mjs new file mode 100644 index 0000000..cc09e9f --- /dev/null +++ b/extension/scripts/clean.mjs @@ -0,0 +1,9 @@ +import fs from "node:fs"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; + +const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); +for (const dir of ["dist", ".output", ".wxt"]) { + fs.rmSync(path.join(root, dir), { recursive: true, force: true }); +} +console.log("Removed extension dist / .output / .wxt"); diff --git a/extension/src/shared/activeTab.ts b/extension/src/shared/activeTab.ts new file mode 100644 index 0000000..02791f6 --- /dev/null +++ b/extension/src/shared/activeTab.ts @@ -0,0 +1,62 @@ +/** + * Resolve the dApp tab the user is working on. + * + * Firefox sidebars / Chrome side panels are not normal browser windows, so + * `tabs.query({ active: true, currentWindow: true })` often returns nothing + * ("No active tab"). Prefer the last-focused browser window instead. + */ +export async function queryActiveDappTab(): Promise<{ + id?: number; + url?: string; +} | undefined> { + try { + const [fromLastFocused] = await browser.tabs.query({ + active: true, + lastFocusedWindow: true, + }); + if (fromLastFocused?.id != null && isInjectableTabUrl(fromLastFocused.url)) { + return fromLastFocused; + } + } catch { + // fall through + } + + try { + const [fromCurrent] = await browser.tabs.query({ + active: true, + currentWindow: true, + }); + if (fromCurrent?.id != null && isInjectableTabUrl(fromCurrent.url)) { + return fromCurrent; + } + } catch { + // fall through + } + + try { + const windows = await browser.windows.getAll({ + populate: true, + windowTypes: ["normal"], + }); + const focused = windows.find((win) => win.focused) ?? windows[0]; + const tab = focused?.tabs?.find((t) => t.active); + if (tab?.id != null && isInjectableTabUrl(tab.url)) { + return tab; + } + } catch { + // fall through + } + + return undefined; +} + +function isInjectableTabUrl(url: string | undefined): boolean { + if (!url) return true; // may fill in after tabs permission / pending load + return !( + url.startsWith("about:") || + url.startsWith("chrome:") || + url.startsWith("chrome-extension:") || + url.startsWith("moz-extension:") || + url.startsWith("devtools:") + ); +} diff --git a/extension/src/shared/constants.ts b/extension/src/shared/constants.ts new file mode 100644 index 0000000..d92b4c5 --- /dev/null +++ b/extension/src/shared/constants.ts @@ -0,0 +1,30 @@ +/** Default Branding Layer URL (production). */ +export const DEFAULT_WALLET_URL = "https://wallet.1shotapi.com/"; + +/** + * EIP-6963 `info.icon` must be a data: URI (or https). + * Page CSP blocks `chrome-extension:` / `moz-extension:` icon URLs (Uniswap etc.). + * Source: `public/icon/icon.svg` (teal target). + */ +export const PROVIDER_ICON_DATA_URI = + "data:image/svg+xml," + + encodeURIComponent( + '' + + '' + + '' + + "", + ); + +/** EIP-6963 provider info. */ +export const PROVIDER_INFO = { + uuid: "a1b2c3d4-e5f6-7890-abcd-ef1234567890", + name: "1Shot Wallet", + rdns: "com.1shotapi.wallet", +} as const; + +/** postMessage source tags (page ↔ content). */ +export const INPAGE_SOURCE = "oneshot-inpage" as const; +export const CONTENT_SOURCE = "oneshot-content" as const; + +/** Channel version for protocol sniffing. */ +export const PROTOCOL_VERSION = 1 as const; diff --git a/extension/src/shared/inject.ts b/extension/src/shared/inject.ts new file mode 100644 index 0000000..723954c --- /dev/null +++ b/extension/src/shared/inject.ts @@ -0,0 +1,80 @@ +/** + * Ensure the extension can inject scripts into the tab (optional host permission). + */ +export async function ensureHostPermissionForTab( + tabId: number, +): Promise<{ ok: true } | { ok: false; error: string }> { + const tab = await browser.tabs.get(tabId); + const url = tab.url; + if (!url) { + return { ok: false, error: "Tab has no URL" }; + } + let origin: string; + try { + origin = new URL(url).origin; + } catch { + return { ok: false, error: "Invalid tab URL" }; + } + if ( + origin.startsWith("chrome:") || + origin.startsWith("about:") || + origin.startsWith("moz-extension:") || + origin.startsWith("chrome-extension:") || + origin === "null" + ) { + return { ok: false, error: "Cannot inject into browser-internal pages" }; + } + + const originPattern = `${origin}/*`; + const has = await browser.permissions.contains({ + origins: [originPattern], + }); + if (has) { + return { ok: true }; + } + + const granted = await browser.permissions.request({ + origins: [originPattern], + }); + if (!granted) { + return { ok: false, error: "Host permission denied" }; + } + return { ok: true }; +} + +export async function injectProviderIntoTab( + tabId: number, +): Promise<{ ok: true } | { ok: false; error: string }> { + const perm = await ensureHostPermissionForTab(tabId); + if (!perm.ok) { + return perm; + } + + try { + // Isolated content bridge + await browser.scripting.executeScript({ + target: { tabId }, + files: ["/content-scripts/content.js"], + }); + } catch (error) { + // Already injected is fine; continue to MAIN world + const message = error instanceof Error ? error.message : String(error); + if (!/already|duplicate|Cannot access/i.test(message)) { + // Still try MAIN; content may already be present + console.warn("[1Shot] content inject:", message); + } + } + + try { + await browser.scripting.executeScript({ + target: { tabId }, + files: ["/inpage.js"], + world: "MAIN", + }); + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + return { ok: false, error: `MAIN-world inject failed: ${message}` }; + } + + return { ok: true }; +} diff --git a/extension/src/shared/openWalletUi.ts b/extension/src/shared/openWalletUi.ts new file mode 100644 index 0000000..a4d2dfe --- /dev/null +++ b/extension/src/shared/openWalletUi.ts @@ -0,0 +1,53 @@ +/** + * Open / focus the wallet UI: Chrome sidePanel or Firefox sidebarAction. + */ +export async function openWalletUi(windowId?: number): Promise { + const chromeSidePanel = ( + globalThis as unknown as { + chrome?: { + sidePanel?: { + open: (options: { windowId: number }) => Promise; + setOptions?: (options: { + path?: string; + enabled?: boolean; + }) => Promise; + }; + }; + } + ).chrome?.sidePanel; + + if (chromeSidePanel?.open) { + let id = windowId; + if (id == null) { + const win = await browser.windows.getCurrent(); + id = win.id; + } + if (id == null) { + throw new Error("No window id for sidePanel.open"); + } + await chromeSidePanel.open({ windowId: id }); + return; + } + + // Firefox MV3 sidebar + const sidebar = ( + browser as unknown as { + sidebarAction?: { + open: () => Promise; + }; + } + ).sidebarAction; + + if (sidebar?.open) { + await sidebar.open(); + return; + } + + // Last resort: open sidepanel.html in a popup window + await browser.windows.create({ + url: browser.runtime.getURL("/sidepanel.html"), + type: "popup", + width: 400, + height: 640, + }); +} diff --git a/extension/src/shared/protocol.ts b/extension/src/shared/protocol.ts new file mode 100644 index 0000000..517d755 --- /dev/null +++ b/extension/src/shared/protocol.ts @@ -0,0 +1,165 @@ +import { CONTENT_SOURCE, INPAGE_SOURCE, PROTOCOL_VERSION } from "./constants"; + +/** EIP-1193 methods that require opening the side panel for user interaction. */ +export const INTERACTIVE_METHODS = new Set([ + "eth_requestAccounts", + "eth_sendTransaction", + "eth_sendRawTransaction", + "eth_sign", + "eth_signTransaction", + "eth_signTypedData", + "eth_signTypedData_v3", + "eth_signTypedData_v4", + "personal_sign", + "wallet_requestPermissions", + "wallet_addEthereumChain", + "wallet_switchEthereumChain", + "wallet_watchAsset", +]); + +export function isInteractiveMethod(method: string): boolean { + return INTERACTIVE_METHODS.has(method); +} + +// --- Page ↔ content (window.postMessage) --- + +export type InpageToContentRequest = { + source: typeof INPAGE_SOURCE; + version: typeof PROTOCOL_VERSION; + type: "request"; + id: string; + method: string; + params?: unknown; +}; + +export type InpageToContentReady = { + source: typeof INPAGE_SOURCE; + version: typeof PROTOCOL_VERSION; + type: "ready"; +}; + +export type ContentToInpageResponse = { + source: typeof CONTENT_SOURCE; + version: typeof PROTOCOL_VERSION; + type: "response"; + id: string; + result?: unknown; + error?: { code: number; message: string; data?: unknown }; +}; + +export type ContentToInpageEvent = { + source: typeof CONTENT_SOURCE; + version: typeof PROTOCOL_VERSION; + type: "event"; + event: string; + params: unknown[]; +}; + +export type ContentToInpageConfig = { + source: typeof CONTENT_SOURCE; + version: typeof PROTOCOL_VERSION; + type: "config"; + preferOneshot: boolean; + iconUrl: string; +}; + +export function isInpageMessage( + data: unknown, +): data is InpageToContentRequest | InpageToContentReady { + if (!data || typeof data !== "object") return false; + const msg = data as Partial; + return ( + msg.source === INPAGE_SOURCE && + msg.version === PROTOCOL_VERSION && + (msg.type === "request" || msg.type === "ready") + ); +} + +export function isContentMessage( + data: unknown, +): data is ContentToInpageResponse | ContentToInpageEvent | ContentToInpageConfig { + if (!data || typeof data !== "object") return false; + const msg = data as Partial; + return ( + msg.source === CONTENT_SOURCE && + msg.version === PROTOCOL_VERSION && + (msg.type === "response" || msg.type === "event" || msg.type === "config") + ); +} + +// --- Extension runtime messages --- + +export type ExtInjectTabMessage = { + type: "inject-tab"; + tabId: number; +}; + +export type ExtOpenWalletUiMessage = { + type: "open-wallet-ui"; + windowId?: number; +}; + +export type ExtAddAllowlistOriginMessage = { + type: "add-allowlist-origin"; + origin: string; +}; + +export type ExtGetStatusMessage = { + type: "get-status"; + tabId?: number; +}; + +export type ExtEip1193RequestMessage = { + type: "eip1193-request"; + /** + * Optional from content scripts — background fills from `sender.tab.id`. + * Never send `0` (not a valid WebExtensions tab id). + */ + tabId?: number; + id: string; + method: string; + params?: unknown; +}; + +/** EIP-1193 request after background has bound a real tab id. */ +export type ExtEip1193RoutedRequestMessage = ExtEip1193RequestMessage & { + tabId: number; +}; + +export type ExtEip1193ResponseMessage = { + type: "eip1193-response"; + tabId: number; + id: string; + result?: unknown; + error?: { code: number; message: string; data?: unknown }; +}; + +export type ExtEip1193EventMessage = { + type: "eip1193-event"; + event: string; + params: unknown[]; +}; + +export type ExtSidepanelReadyMessage = { + type: "sidepanel-ready"; +}; + +export type ExtStatusResponse = { + ok: boolean; + injected: boolean; + origin?: string; + allowlisted: boolean; + walletUrl: string; + preferOneshot: boolean; + error?: string; +}; + +export type ExtRuntimeMessage = + | ExtInjectTabMessage + | ExtOpenWalletUiMessage + | ExtAddAllowlistOriginMessage + | ExtGetStatusMessage + | ExtEip1193RequestMessage + | ExtEip1193ResponseMessage + | ExtEip1193EventMessage + | ExtSidepanelReadyMessage; diff --git a/extension/src/shared/storage.ts b/extension/src/shared/storage.ts new file mode 100644 index 0000000..1d65b6f --- /dev/null +++ b/extension/src/shared/storage.ts @@ -0,0 +1,63 @@ +import { DEFAULT_WALLET_URL } from "./constants"; + +export type ExtensionSettings = { + walletUrl: string; + /** Origins that auto-inject the provider shim (exact origin). */ + allowlist: string[]; + /** When true, set/overwrite window.ethereum even if another provider exists. */ + preferOneshot: boolean; +}; + +const SETTINGS_KEY = "settings"; + +const FALLBACK: ExtensionSettings = { + walletUrl: DEFAULT_WALLET_URL, + allowlist: [], + preferOneshot: false, +}; + +function normalize(value: Partial | undefined): ExtensionSettings { + return { + walletUrl: value?.walletUrl?.trim() || FALLBACK.walletUrl, + allowlist: Array.isArray(value?.allowlist) ? value.allowlist : [], + preferOneshot: Boolean(value?.preferOneshot), + }; +} + +export async function getSettings(): Promise { + const result = await browser.storage.local.get(SETTINGS_KEY); + return normalize(result[SETTINGS_KEY] as Partial | undefined); +} + +export async function setSettings( + patch: Partial, +): Promise { + const current = await getSettings(); + const next: ExtensionSettings = { + walletUrl: patch.walletUrl?.trim() || current.walletUrl, + allowlist: patch.allowlist ?? current.allowlist, + preferOneshot: + patch.preferOneshot !== undefined + ? patch.preferOneshot + : current.preferOneshot, + }; + await browser.storage.local.set({ [SETTINGS_KEY]: next }); + return next; +} + +export async function addAllowlistOrigin( + origin: string, +): Promise { + const current = await getSettings(); + if (current.allowlist.includes(origin)) { + return current; + } + return setSettings({ allowlist: [...current.allowlist, origin] }); +} + +export function isOriginAllowlisted( + origin: string, + allowlist: string[], +): boolean { + return allowlist.includes(origin); +} diff --git a/extension/test-pages/csp-hostile.html b/extension/test-pages/csp-hostile.html new file mode 100644 index 0000000..cba149d --- /dev/null +++ b/extension/test-pages/csp-hostile.html @@ -0,0 +1,78 @@ + + + + + + 1Shot extension CSP-hostile test page + + + +

CSP-hostile dApp stub

+

+ This page sets frame-src 'self' so an in-page Branding iframe + would fail. The extension side-panel architecture should still work after + you inject the 1Shot provider. +

+

+ Serve with any static server (e.g. + npx serve extension/test-pages) over + http://localhost, then Inject from the extension popup. +

+ + +
(no output yet)
+ + + diff --git a/extension/tsconfig.json b/extension/tsconfig.json new file mode 100644 index 0000000..ebc00db --- /dev/null +++ b/extension/tsconfig.json @@ -0,0 +1,12 @@ +{ + "extends": "./.wxt/tsconfig.json", + "compilerOptions": { + "strict": true, + "noEmit": true, + "skipLibCheck": true, + "paths": { + "@/*": ["./src/*"] + } + }, + "include": ["entrypoints/**/*.ts", "src/**/*.ts", ".wxt/**/*.ts"] +} diff --git a/extension/wxt.config.ts b/extension/wxt.config.ts new file mode 100644 index 0000000..de78705 --- /dev/null +++ b/extension/wxt.config.ts @@ -0,0 +1,117 @@ +import { defineConfig } from "wxt"; + +/** + * Production CSP: styles/scripts from the extension package only. + * + * Dev (`wxt` / `wxt -b firefox`): HTML entrypoints load CSS/JS from the Vite + * server (http://localhost:3000). WXT auto-adds that origin to `script-src`, + * but not `style-src` — without localhost in style-src, Firefox blocks + * sidepanel/options CSS and the Inline wallet iframe covers the unstyled + * Inject chrome. + */ +function extensionPagesCsp(command: "build" | "serve"): string { + const isDev = command === "serve"; + return [ + isDev + ? "script-src 'self' 'unsafe-eval' 'wasm-unsafe-eval'" + : "script-src 'self'", + "object-src 'self'", + // Branding iframe: prod + common local / tunnel hosts (test extension). + "frame-src 'self' https: http://localhost:* http://127.0.0.1:*", + "img-src 'self' data: https:", + isDev + ? "style-src 'self' 'unsafe-inline' http://localhost:* http://127.0.0.1:*" + : "style-src 'self' 'unsafe-inline'", + ].join("; "); +} + +export default defineConfig({ + srcDir: ".", + entrypointsDir: "entrypoints", + outDir: "dist", + modulesDir: "modules", + manifestVersion: 3, + suppressWarnings: { + firefoxDataCollection: true, + }, + manifest: ({ browser, command }) => ({ + name: "1Shot Wallet", + description: + "Use the 1Shot embedded wallet as a MetaMask-style provider on any dApp (EIP-1193 + EIP-6963).", + version: "0.1.0", + permissions: + browser === "firefox" + ? ["storage", "scripting", "activeTab", "tabs"] + : ["storage", "scripting", "sidePanel", "activeTab", "tabs"], + optional_host_permissions: [""], + icons: { + 16: "icon/16.png", + 48: "icon/48.png", + 128: "icon/128.png", + }, + action: { + default_title: "1Shot Wallet", + default_icon: { + 16: "icon/16.png", + 48: "icon/48.png", + 128: "icon/128.png", + }, + }, + options_ui: { + open_in_tab: true, + }, + content_security_policy: { + extension_pages: extensionPagesCsp(command), + }, + browser_specific_settings: { + gecko: { + id: "wallet-extension@1shotapi.com", + strict_min_version: "128.0", + }, + }, + }), + hooks: { + "build:manifestGenerated": (wxt, manifest) => { + if (manifest.options_ui) { + manifest.options_ui.open_in_tab = true; + } + + // Firefox does not use the Chromium `sandbox` CSP bucket; WXT still + // injects it in serve mode and about:debugging warns on it. + if ( + wxt.config.browser === "firefox" && + manifest.content_security_policy && + "sandbox" in manifest.content_security_policy + ) { + delete (manifest.content_security_policy as { sandbox?: string }) + .sandbox; + } + + // Belt-and-suspenders: after WXT adds script-src localhost, ensure + // style-src also allows the exact Vite origin (port-specific). + if (wxt.config.command !== "serve") { + return; + } + const origin = wxt.server?.origin; + const pages = manifest.content_security_policy?.extension_pages; + if (!origin || typeof pages !== "string") { + return; + } + if (pages.includes("style-src") && !pages.includes(origin)) { + manifest.content_security_policy!.extension_pages = pages.replace( + /style-src [^;]+/, + (match) => `${match} ${origin}`, + ); + } + }, + }, + vite: () => ({ + build: { + rollupOptions: { + output: { + manualChunks: undefined, + }, + }, + }, + }), +}); diff --git a/host/src/App.tsx b/host/src/App.tsx index 9754665..fe5e4fd 100644 --- a/host/src/App.tsx +++ b/host/src/App.tsx @@ -8,6 +8,7 @@ import { AnalyticsPanel } from "./components/AnalyticsPanel"; import { AppHeader } from "./components/AppHeader"; import { AppSidebar, type HostMode } from "./components/AppSidebar"; import { DesignPanel } from "./components/DesignPanel"; +import { InjectedPanel } from "./components/InjectedPanel"; import { TestPanel } from "./components/TestPanel"; import { SidebarInset, SidebarProvider } from "./components/ui/sidebar"; import { TooltipProvider } from "./components/ui/tooltip"; @@ -41,7 +42,16 @@ export function App() { // Presentation is create-time only. Switching Test ↔ Design destroys and // recreates the proxy against the right container (reparenting breaks Postmate). + // Injected mode intentionally skips OWSProxy — extension provides window.ethereum. useEffect(() => { + if (mode === "injected") { + setReady(false); + setWalletVisible(false); + proxyRef.current?.destroy(); + proxyRef.current = null; + return; + } + if (mode === "design" && !previewMount) { return; } @@ -185,6 +195,8 @@ export function App() { onApplyStyle={handleApplyStyle} previewMountRef={setPreviewMount} /> + ) : mode === "injected" ? ( + ) : (
diff --git a/host/src/components/InjectedPanel.tsx b/host/src/components/InjectedPanel.tsx new file mode 100644 index 0000000..a70f4dc --- /dev/null +++ b/host/src/components/InjectedPanel.tsx @@ -0,0 +1,233 @@ +import { useEffect, useState } from "react"; +import { Button } from "@/components/ui/button"; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@/components/ui/card"; + +type EthereumProvider = { + request: (args: { method: string; params?: unknown }) => Promise; + on?: (event: string, listener: (...args: unknown[]) => void) => void; + removeListener?: ( + event: string, + listener: (...args: unknown[]) => void, + ) => void; + is1Shot?: boolean; + isMetaMask?: boolean; +}; + +type Eip6963ProviderDetail = { + info: { uuid: string; name: string; icon: string; rdns: string }; + provider: EthereumProvider; +}; + +declare global { + interface Window { + ethereum?: EthereumProvider & { providers?: EthereumProvider[] }; + } +} + +function formatError(error: unknown): string { + if (error instanceof Error) return error.message; + if (typeof error === "object" && error && "message" in error) { + return String((error as { message: unknown }).message); + } + return String(error); +} + +export function InjectedPanel() { + const [providers, setProviders] = useState([]); + const [selectedRdns, setSelectedRdns] = useState(null); + const [accounts, setAccounts] = useState([]); + const [chainId, setChainId] = useState(null); + const [status, setStatus] = useState( + "Waiting for EIP-6963 announce / window.ethereum…", + ); + const [log, setLog] = useState([]); + + const appendLog = (line: string) => { + setLog((prev) => [line, ...prev].slice(0, 40)); + }; + + useEffect(() => { + const seen = new Map(); + + const onAnnounce = (event: Event) => { + const detail = (event as CustomEvent).detail; + if (!detail?.info?.rdns || !detail.provider) return; + seen.set(detail.info.rdns, detail); + const next = [...seen.values()]; + setProviders(next); + setSelectedRdns( + (current) => + current ?? + next.find((p) => p.info.rdns === "com.1shotapi.wallet")?.info.rdns ?? + next[0]?.info.rdns ?? + null, + ); + setStatus( + `EIP-6963: ${next.map((p) => p.info.name).join(", ") || "(none)"}`, + ); + }; + + window.addEventListener( + "eip6963:announceProvider", + onAnnounce as EventListener, + ); + window.dispatchEvent(new Event("eip6963:requestProvider")); + + if (window.ethereum) { + appendLog( + `window.ethereum present (is1Shot=${String(window.ethereum.is1Shot)}, isMetaMask=${String(window.ethereum.isMetaMask)})`, + ); + } else { + appendLog( + "window.ethereum absent — inject the 1Shot extension on this tab", + ); + } + + return () => { + window.removeEventListener( + "eip6963:announceProvider", + onAnnounce as EventListener, + ); + }; + }, []); + + const activeProvider = (): EthereumProvider | undefined => { + const from6963 = providers.find((p) => p.info.rdns === selectedRdns); + if (from6963) return from6963.provider; + return window.ethereum; + }; + + const connect = async () => { + const eth = activeProvider(); + if (!eth) { + setStatus("No provider — use the extension Inject button on this tab first"); + return; + } + try { + setStatus("eth_requestAccounts…"); + const result = (await eth.request({ + method: "eth_requestAccounts", + })) as string[]; + setAccounts(result); + const chain = (await eth.request({ method: "eth_chainId" })) as string; + setChainId(chain); + setStatus(`Connected ${result[0] ?? "(no account)"} on ${chain}`); + appendLog(`accounts: ${JSON.stringify(result)}`); + } catch (error) { + setStatus(formatError(error)); + appendLog(`error: ${formatError(error)}`); + } + }; + + const personalSign = async () => { + const eth = activeProvider(); + const account = accounts[0]; + if (!eth || !account) { + setStatus("Connect an account first"); + return; + } + try { + const message = `1Shot inject test ${new Date().toISOString()}`; + setStatus("personal_sign…"); + const sig = await eth.request({ + method: "personal_sign", + params: [message, account], + }); + appendLog(`personal_sign → ${String(sig).slice(0, 42)}…`); + setStatus("Signed"); + } catch (error) { + setStatus(formatError(error)); + appendLog(`error: ${formatError(error)}`); + } + }; + + const refreshProviders = () => { + setProviders([]); + window.dispatchEvent(new Event("eip6963:requestProvider")); + setStatus("Re-requested EIP-6963 announce"); + }; + + return ( +
+ + + Injected provider playground + + No OWSProxy on this page — only{" "} + window.ethereum / EIP-6963 from the browser extension. + Open this host tab, use the extension Inject control, + then Connect here. + + + +

{status}

+ {chainId ? ( +

+ chainId={chainId} + {accounts[0] ? ` · ${accounts[0]}` : null} +

+ ) : null} + +
+ + + +
+ +
+

Detected providers

+ {providers.length === 0 ? ( +

+ None yet. Inject 1Shot on this tab, then click Refresh EIP-6963. + With MetaMask installed, enable Prefer 1Shot in extension + Settings if you need window.ethereum to be 1Shot. +

+ ) : ( +
    + {providers.map((p) => ( +
  • + +
  • + ))} +
+ )} +
+ +
+            {log.join("\n") || "(log empty)"}
+          
+
+
+
+ ); +} diff --git a/host/src/components/WalletActions.tsx b/host/src/components/WalletActions.tsx index 22f55b5..0fe7936 100644 --- a/host/src/components/WalletActions.tsx +++ b/host/src/components/WalletActions.tsx @@ -114,6 +114,7 @@ export function WalletActions({ onGetGrantedPermissions, }: IWalletActionsProps) { const meta = hostChainMeta(chainId); + const tokenSymbol = meta?.tokenSymbol ?? "USDC"; const accountLabel = account ? `${account.slice(0, 6)}…${account.slice(-4)}` : ready @@ -271,11 +272,11 @@ export function WalletActions({ Check Balance - Send USDC + Send {tokenSymbol}

- {meta ? `USDC: ${meta.usdc}` : "USDC: unsupported chain"} + {meta ? `${tokenSymbol}: ${meta.usdc}` : "Token: unsupported chain"}

@@ -319,7 +320,7 @@ export function WalletActions({ disabled={!ready || busy} onClick={onUsdcAction} > - {usdcMode === "send" ? "Send USDC" : "Check Balance"} + {usdcMode === "send" ? `Send ${tokenSymbol}` : "Check Balance"} diff --git a/host/src/components/WalletConfigurator.tsx b/host/src/components/WalletConfigurator.tsx index aafb3c2..9df68a0 100644 --- a/host/src/components/WalletConfigurator.tsx +++ b/host/src/components/WalletConfigurator.tsx @@ -87,44 +87,104 @@ export function WalletConfigurator({ value={form.tagline} onChange={(value) => patch("tagline", value)} /> -
- - Allowed chains - -

- Leave all unchecked to allow every catalog network. Checking any - restricts the wallet Network dropdown via{" "} - setStyle.allowedChains. -

-
- {CATALOG_CHAIN_OPTIONS.map((chain) => { - const checked = form.allowedChainIds.includes(chain.chainId); - return ( - - ); - })} +
+ Features +
+
+ +

+ Hides the chrome Close (X). Use for Inline hosts where hide + is a no-op. +

+
+ patch("hideCloseBox", checked)} + /> +
+
+
+ +

+ Hides the Credentials tab. Host credential flows still work. +

+
+ + patch("disableCredentials", checked) + } + /> +
+
+
+ +

+ Hides the Delegations tab. Host delegation flows still work. +

+
+ + patch("disableDelegations", checked) + } + /> +
+
+

Allowed chains

+

+ Leave all unchecked to allow every catalog network. Checking any + restricts the wallet Network dropdown via{" "} + + setStyle.features.allowedChains + + . +

+
+ {CATALOG_CHAIN_OPTIONS.map((chain) => { + const checked = form.allowedChainIds.includes(chain.chainId); + return ( + + ); + })} +
diff --git a/host/src/components/WalletConfiguratorTextTabSigningSections.tsx b/host/src/components/WalletConfiguratorTextTabSigningSections.tsx index 9865058..ea03d96 100644 --- a/host/src/components/WalletConfiguratorTextTabSigningSections.tsx +++ b/host/src/components/WalletConfiguratorTextTabSigningSections.tsx @@ -75,6 +75,18 @@ export function WalletConfiguratorTextTabSigningSections({ value={form.setupCancel} onChange={(value) => patch("setupCancel", value)} /> + patch("setupPasskeyTimeoutError", value)} + /> + patch("setupPasskeyFailedError", value)} + /> diff --git a/host/src/components/hostChains.ts b/host/src/components/hostChains.ts index 1aeec3b..a260c6b 100644 --- a/host/src/components/hostChains.ts +++ b/host/src/components/hostChains.ts @@ -3,26 +3,37 @@ export const HOST_CHAINS = [ value: "0x4cef52", label: "Arc Testnet", usdc: "0x3600000000000000000000000000000000000000", + tokenSymbol: "USDC", blockExplorerUrl: "https://testnet.arcscan.app", }, { value: "0xaa36a7", label: "Sepolia", usdc: "0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238", + tokenSymbol: "USDC", blockExplorerUrl: "https://sepolia.etherscan.io", }, { value: "0x14a34", label: "Base Sepolia", usdc: "0x036CbD53842c5426634e7929541eC2318f3dCF7e", + tokenSymbol: "USDC", blockExplorerUrl: "https://sepolia.basescan.org", }, { value: "0x2105", label: "Base", usdc: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", + tokenSymbol: "USDC", blockExplorerUrl: "https://basescan.org", }, + { + value: "0x1237", + label: "Robinhood", + usdc: "0x5fc5360D0400a0Fd4f2af552ADD042D716F1d168", + tokenSymbol: "USDG", + blockExplorerUrl: "https://robinhoodchain.blockscout.com", + }, ] as const; /** Focus demo: Arc Testnet USDC. */ diff --git a/host/src/styleForm.ts b/host/src/styleForm.ts index 4dbdc39..911f39f 100644 --- a/host/src/styleForm.ts +++ b/host/src/styleForm.ts @@ -19,10 +19,16 @@ export interface IStyleFormState { fontSans: string; dark: boolean; /** - * Hex chain ids to pass as `allowedChains`. + * Hex chain ids to pass as `features.allowedChains`. * Empty ⇒ omit (all catalog-enabled chains). */ allowedChainIds: string[]; + /** Pass as `features.hideCloseBox` (Inline hosts). */ + hideCloseBox: boolean; + /** Pass as `features.disableCredentials`. */ + disableCredentials: boolean; + /** Pass as `features.disableDelegations`. */ + disableDelegations: boolean; // Text — Connect connectTitle: string; @@ -36,6 +42,8 @@ export interface IStyleFormState { setupCreate: string; setupLogin: string; setupCancel: string; + setupPasskeyTimeoutError: string; + setupPasskeyFailedError: string; // Text — Account shell (network + address chips) selectNetworkTitle: string; @@ -200,6 +208,7 @@ export const CATALOG_CHAIN_OPTIONS: ReadonlyArray<{ { chainId: "0x82", label: "Unichain" }, { chainId: "0x8f", label: "Monad" }, { chainId: "0xa4ec", label: "Celo" }, + { chainId: "0x1237", label: "Robinhood" }, ]; export const ACME_PRESET: IStyleFormState = { @@ -219,6 +228,9 @@ export const ACME_PRESET: IStyleFormState = { fontSans: "", dark: false, allowedChainIds: [], + hideCloseBox: false, + disableCredentials: false, + disableDelegations: false, connectTitle: "Connect to Acme", connectBody: "Acme is requesting your wallet address.", connectContinue: "Allow", @@ -228,6 +240,10 @@ export const ACME_PRESET: IStyleFormState = { setupCreate: "Get started", setupLogin: "Log in", setupCancel: "Cancel", + setupPasskeyTimeoutError: + "Passkey confirmation timed out. Please try again.", + setupPasskeyFailedError: + "Could not complete passkey authentication. Please try again.", selectNetworkTitle: "Select network", selectNetworkCancelLabel: "Cancel", copyAddressLabel: "Copy address", @@ -536,6 +552,8 @@ export function buildSetStylePayload( put(walletSetup, "createLabel", form.setupCreate); put(walletSetup, "loginLabel", form.setupLogin); put(walletSetup, "cancelLabel", form.setupCancel); + put(walletSetup, "passkeyTimeoutError", form.setupPasskeyTimeoutError); + put(walletSetup, "passkeyFailedError", form.setupPasskeyFailedError); if (Object.keys(walletSetup).length > 0) copy.walletSetup = walletSetup; const account: Record = {}; @@ -763,8 +781,11 @@ export function buildSetStylePayload( const payload: Record = { dark: form.dark }; if (Object.keys(theme).length > 0) payload.theme = theme; if (Object.keys(copy).length > 0) payload.copy = copy; - if (form.allowedChainIds.length > 0) { - payload.allowedChains = [...form.allowedChainIds]; - } + payload.features = { + hideCloseBox: form.hideCloseBox, + disableCredentials: form.disableCredentials, + disableDelegations: form.disableDelegations, + allowedChains: [...form.allowedChainIds], + }; return payload; } diff --git a/mobile/constants.ts b/mobile/constants.ts new file mode 100644 index 0000000..e376847 --- /dev/null +++ b/mobile/constants.ts @@ -0,0 +1,24 @@ +/** Reown Cloud project for the 1Shot Wallet mobile / WalletConnect host. */ +export const REOWN_PROJECT_ID = "5ff565827d1b5822cecb7104706521d5"; + +export const WALLET_METADATA = { + name: "1Shot Wallet", + description: "Passkey-native embedded wallet — WalletConnect host", + url: "https://wallet.1shotapi.com/mobile/", + icons: ["https://wallet.1shotapi.com/mobile/icons/icon-192.png"], +} as const; + +export function walletMetadataForOrigin(origin: string): { + name: string; + description: string; + url: string; + icons: string[]; +} { + const base = origin.replace(/\/$/, ""); + return { + name: WALLET_METADATA.name, + description: WALLET_METADATA.description, + url: `${base}/mobile/`, + icons: [`${base}/mobile/icons/icon-192.png`], + }; +} diff --git a/mobile/index.html b/mobile/index.html new file mode 100644 index 0000000..97566b5 --- /dev/null +++ b/mobile/index.html @@ -0,0 +1,138 @@ + + + + + + + + + + + + 1Shot Wallet + + + +
+
+
+

1Shot Wallet

+
+

Starting…

+
+ + +
+
+
+
+
+
+ + + diff --git a/mobile/main.ts b/mobile/main.ts new file mode 100644 index 0000000..3396907 --- /dev/null +++ b/mobile/main.ts @@ -0,0 +1,113 @@ +import { + EWalletPresentationMode, + OWSProxy, +} from "@1shotapi/ows-provider"; +import { + readWalletConnectUriFromLocation, + startWalletConnectBridge, + type WalletConnectBridge, +} from "./walletConnect"; + +/** + * First-party Host Layer page: mobile PWA + WalletConnect target. + * Embeds Branding via Inline OWSProxy and bridges WC ↔ EIP-1193. + */ + +const statusEl = document.getElementById("status")!; +const container = document.getElementById("wallet-container")!; +const uriInput = document.getElementById("uri-input") as HTMLInputElement; +const pairBtn = document.getElementById("pair-btn") as HTMLButtonElement; + +function setStatus(message: string, isError = false): void { + statusEl.textContent = message; + statusEl.classList.toggle("error", isError); +} + +function registerServiceWorker(): void { + if (!("serviceWorker" in navigator)) return; + if (!window.location.pathname.startsWith("/mobile")) return; + void navigator.serviceWorker.register("/mobile/sw.js", { + scope: "/mobile/", + }).catch((error) => { + console.warn("[mobile] service worker registration failed", error); + }); +} + +async function pairFromInput(bridge: WalletConnectBridge): Promise { + const uri = uriInput.value.trim(); + if (!uri) { + setStatus("Paste a wc: URI first", true); + return; + } + pairBtn.disabled = true; + try { + await bridge.pair(uri); + } catch (error) { + setStatus( + error instanceof Error ? error.message : String(error), + true, + ); + } finally { + pairBtn.disabled = false; + } +} + +async function main(): Promise { + registerServiceWorker(); + + setStatus("Connecting to wallet…"); + const walletUrl = new URL("/", window.location.origin).href; + + const proxy = await OWSProxy.create(container, walletUrl, { + presentationMode: EWalletPresentationMode.Inline, + classList: ["oneshot-ows-mobile-host"], + }); + + try { + await proxy.rpc("setStyle", { + copy: { productName: "1Shot Wallet" }, + features: { hideCloseBox: true }, + }); + } catch (error) { + console.warn("[mobile] setStyle failed", error); + } + + setStatus("Starting WalletConnect…"); + const bridge = await startWalletConnectBridge(proxy, setStatus); + setStatus( + bridge.getActiveSessionCount() > 0 + ? `${bridge.getActiveSessionCount()} active session(s)` + : "Ready — paste a wc: URI or open with ?uri=", + ); + + pairBtn.addEventListener("click", () => { + void pairFromInput(bridge); + }); + uriInput.addEventListener("keydown", (event) => { + if (event.key === "Enter") { + void pairFromInput(bridge); + } + }); + + const fromLocation = readWalletConnectUriFromLocation(); + if (fromLocation) { + uriInput.value = fromLocation; + try { + await bridge.pair(fromLocation); + } catch (error) { + setStatus( + error instanceof Error ? error.message : String(error), + true, + ); + } + } +} + +void main().catch((error) => { + console.error("[mobile] boot failed", error); + setStatus( + error instanceof Error ? error.message : String(error), + true, + ); + pairBtn.disabled = true; +}); diff --git a/mobile/namespaces.ts b/mobile/namespaces.ts new file mode 100644 index 0000000..fd429dd --- /dev/null +++ b/mobile/namespaces.ts @@ -0,0 +1,245 @@ +/** + * EIP-155 chain ids (decimal) matching Branding HardcodedChainRepository catalog. + * Keep in sync when the wallet catalog changes. + */ +export const SUPPORTED_EIP155_CHAIN_IDS: readonly number[] = [ + 0x4cef52, // Arc Testnet + 0xaa36a7, // Sepolia + 0x14a34, // Base Sepolia + 0x1, // Ethereum + 0xe708, // Linea + 0xa4b1, // Arbitrum + 0xa, // Optimism + 0x38, // BSC + 0x2105, // Base + 0x89, // Polygon + 0x92, // Sonic + 0x82, // Unichain + 0x8f, // Monad + 0xa4ec, // Celo + 0x1237, // Robinhood +]; + +export const EIP155_METHODS = [ + "eth_accounts", + "eth_requestAccounts", + "eth_chainId", + "eth_sendTransaction", + "eth_signTransaction", + "eth_sign", + "personal_sign", + "eth_signTypedData", + "eth_signTypedData_v3", + "eth_signTypedData_v4", + "wallet_switchEthereumChain", + "wallet_getCapabilities", +] as const; + +export const EIP155_EVENTS = ["accountsChanged", "chainChanged"] as const; + +export function hexChainIdToDecimal(chainId: string): number { + const normalized = chainId.trim().toLowerCase(); + if (normalized.startsWith("0x")) { + return Number.parseInt(normalized, 16); + } + return Number.parseInt(normalized, 10); +} + +export function caip2ChainId(decimalChainId: number): string { + return `eip155:${decimalChainId}`; +} + +export function caip10Account(decimalChainId: number, address: string): string { + return `eip155:${decimalChainId}:${address}`; +} + +export function supportedCaip2Chains(): string[] { + return SUPPORTED_EIP155_CHAIN_IDS.map(caip2ChainId); +} + +type NamespaceInput = { + chains?: string[]; + accounts?: string[]; + methods?: string[]; + events?: string[]; +}; + +export type ApprovedEip155Namespace = { + chains: string[]; + accounts: string[]; + methods: string[]; + events: string[]; +}; + +/** WalletConnect `getSdkError` keys for an unsatisfiable session proposal. */ +export type NamespaceApprovalSdkError = + | "UNSUPPORTED_CHAINS" + | "UNSUPPORTED_METHODS" + | "UNSUPPORTED_EVENTS" + | "UNSUPPORTED_NAMESPACE_KEY"; + +export class NamespaceApprovalError extends Error { + constructor( + message: string, + public readonly sdkError: NamespaceApprovalSdkError, + ) { + super(message); + this.name = "NamespaceApprovalError"; + } +} + +const EIP155_CHAIN_RE = /^eip155:\d+$/; + +function isEip155NamespaceKey(key: string): boolean { + return key === "eip155" || EIP155_CHAIN_RE.test(key); +} + +function eip155ChainsFrom(key: string, ns: NamespaceInput): string[] { + const chains: string[] = []; + if (EIP155_CHAIN_RE.test(key)) { + chains.push(key); + } + for (const chain of ns.chains ?? []) { + if (EIP155_CHAIN_RE.test(chain)) { + chains.push(chain); + } + } + for (const account of ns.accounts ?? []) { + const match = /^(eip155:\d+):/u.exec(account); + if (match) { + chains.push(match[1]); + } + } + return chains; +} + +function collectEip155( + namespaces: Record, + options: { required: boolean }, +): { + chains: Set; + methods: Set; + events: Set; +} { + const chains = new Set(); + const methods = new Set(); + const events = new Set(); + for (const [key, ns] of Object.entries(namespaces)) { + if (!isEip155NamespaceKey(key)) { + if (options.required) { + throw new NamespaceApprovalError( + `Required namespace ${key} is not supported`, + "UNSUPPORTED_NAMESPACE_KEY", + ); + } + continue; + } + for (const chain of eip155ChainsFrom(key, ns)) { + chains.add(chain); + } + for (const method of ns.methods ?? []) { + methods.add(method); + } + for (const event of ns.events ?? []) { + events.add(event); + } + } + return { chains, methods, events }; +} + +/** + * Build approved EIP-155 namespaces from a session proposal ∩ wallet support. + * Required namespaces must be fully satisfiable; optional non-eip155 keys and + * unsupported optional chains/methods/events are dropped. Throws + * {@link NamespaceApprovalError} instead of approving a mismatched session. + */ +export function buildApprovedNamespaces(params: { + requiredNamespaces?: Record; + optionalNamespaces?: Record; + address: string; +}): Record { + const walletChains = new Set(supportedCaip2Chains()); + const walletMethods = new Set(EIP155_METHODS); + const walletEvents = new Set(EIP155_EVENTS); + + const required = collectEip155(params.requiredNamespaces ?? {}, { + required: true, + }); + const optional = collectEip155(params.optionalNamespaces ?? {}, { + required: false, + }); + + for (const chain of required.chains) { + if (!walletChains.has(chain)) { + throw new NamespaceApprovalError( + `Required chain ${chain} is not supported`, + "UNSUPPORTED_CHAINS", + ); + } + } + for (const method of required.methods) { + if (!walletMethods.has(method)) { + throw new NamespaceApprovalError( + `Required method ${method} is not supported`, + "UNSUPPORTED_METHODS", + ); + } + } + for (const event of required.events) { + if (!walletEvents.has(event)) { + throw new NamespaceApprovalError( + `Required event ${event} is not supported`, + "UNSUPPORTED_EVENTS", + ); + } + } + + const chains = new Set(required.chains); + for (const chain of optional.chains) { + if (walletChains.has(chain)) { + chains.add(chain); + } + } + if (chains.size === 0) { + throw new NamespaceApprovalError( + "Proposal has no supported eip155 chains", + "UNSUPPORTED_CHAINS", + ); + } + + const methods = new Set(required.methods); + for (const method of optional.methods) { + if (walletMethods.has(method)) { + methods.add(method); + } + } + const events = new Set(required.events); + for (const event of optional.events) { + if (walletEvents.has(event)) { + events.add(event); + } + } + if (methods.size === 0) { + for (const method of EIP155_METHODS) { + methods.add(method); + } + } + if (events.size === 0) { + for (const event of EIP155_EVENTS) { + events.add(event); + } + } + + const chainList = [...chains]; + return { + eip155: { + chains: chainList, + accounts: chainList.map((chain) => { + const decimal = Number(chain.slice("eip155:".length)); + return caip10Account(decimal, params.address); + }), + methods: [...methods], + events: [...events], + }, + }; +} diff --git a/mobile/walletConnect.ts b/mobile/walletConnect.ts new file mode 100644 index 0000000..9880279 --- /dev/null +++ b/mobile/walletConnect.ts @@ -0,0 +1,252 @@ +import type { OWSProxy } from "@1shotapi/ows-provider"; +import { EVMChainId } from "@1shotapi/ows-types"; +import { Core } from "@walletconnect/core"; +import { WalletKit, type WalletKitTypes } from "@reown/walletkit"; +import { getSdkError } from "@walletconnect/utils"; +import { + buildApprovedNamespaces, + hexChainIdToDecimal, + NamespaceApprovalError, +} from "./namespaces"; +import { REOWN_PROJECT_ID, walletMetadataForOrigin } from "./constants"; + +export type WalletConnectBridge = { + pair: (uri: string) => Promise; + getActiveSessionCount: () => number; +}; + +type StatusFn = (message: string, isError?: boolean) => void; + +function normalizeRequestParams(params: unknown): unknown[] | undefined { + if (params == null) return undefined; + if (Array.isArray(params)) return params; + return [params]; +} + +function isUserRejected(error: unknown): boolean { + if (!error || typeof error !== "object") return false; + const err = error as { code?: number; name?: string; message?: string }; + if (err.code === 4001) return true; + const hay = `${err.name ?? ""} ${err.message ?? ""}`.toLowerCase(); + return hay.includes("rejected") || hay.includes("denied"); +} + +/** + * WalletConnect v2 (Reown WalletKit) ↔ OWSProxy EIP-1193 bridge. + */ +export async function startWalletConnectBridge( + proxy: OWSProxy, + setStatus: StatusFn, +): Promise { + const core = new Core({ projectId: REOWN_PROJECT_ID }); + const walletKit = await WalletKit.init({ + core, + metadata: walletMetadataForOrigin(window.location.origin), + }); + + const emitToSessions = async ( + event: "accountsChanged" | "chainChanged", + chainIdHex: string, + data: unknown, + ): Promise => { + const decimal = hexChainIdToDecimal(chainIdHex); + const chainId = `eip155:${decimal}`; + const sessions = Object.values(walletKit.getActiveSessions()); + await Promise.all( + sessions.map(async (session) => { + try { + await walletKit.emitSessionEvent({ + topic: session.topic, + event: { name: event, data }, + chainId, + }); + } catch (error) { + console.warn("[mobile/wc] emitSessionEvent failed", event, error); + } + }), + ); + }; + + let lastChainId = "0x1"; + try { + lastChainId = String( + await proxy.ethereum.request({ method: "eth_chainId" }), + ); + } catch { + // Branding may not be unlocked yet. + } + + proxy.ethereum.on("chainChanged", (...params: unknown[]) => { + const next = typeof params[0] === "string" ? params[0] : lastChainId; + lastChainId = next; + void emitToSessions("chainChanged", next, next); + }); + + proxy.ethereum.on("accountsChanged", (...params: unknown[]) => { + const accounts = Array.isArray(params[0]) ? params[0] : []; + void emitToSessions("accountsChanged", lastChainId, accounts); + }); + + walletKit.on( + "session_proposal", + async (proposal: WalletKitTypes.SessionProposal) => { + setStatus( + `Session proposal from ${proposal.params.proposer.metadata.name}…`, + ); + try { + const accounts = (await proxy.ethereum.request({ + method: "eth_requestAccounts", + })) as string[]; + const address = accounts[0]; + if (!address) { + throw new Error("No account available after connect"); + } + + const chainIdHex = String( + await proxy.ethereum.request({ method: "eth_chainId" }), + ); + lastChainId = chainIdHex; + + const namespaces = buildApprovedNamespaces({ + requiredNamespaces: proposal.params.requiredNamespaces, + optionalNamespaces: proposal.params.optionalNamespaces, + address, + }); + + await walletKit.approveSession({ + id: proposal.id, + namespaces, + }); + setStatus(`Connected to ${proposal.params.proposer.metadata.name}`); + } catch (error) { + const reason = isUserRejected(error) + ? getSdkError("USER_REJECTED") + : error instanceof NamespaceApprovalError + ? getSdkError(error.sdkError) + : getSdkError("USER_REJECTED_METHODS"); + try { + await walletKit.rejectSession({ + id: proposal.id, + reason, + }); + } catch { + // already gone + } + setStatus( + error instanceof Error ? error.message : "Session proposal failed", + true, + ); + } + }, + ); + + walletKit.on( + "session_request", + async (event: WalletKitTypes.SessionRequest) => { + const { topic, params, id } = event; + const { request, chainId } = params; + setStatus(`Request: ${request.method}`); + + try { + if (chainId?.startsWith("eip155:")) { + const requestedHex = `0x${Number( + chainId.slice("eip155:".length), + ).toString(16)}`; + const current = String( + await proxy.ethereum.request({ method: "eth_chainId" }), + ); + if (current.toLowerCase() !== requestedHex.toLowerCase()) { + await proxy.ethereum.request({ + method: "wallet_switchEthereumChain", + params: [ + { chainId: EVMChainId(requestedHex as `0x${string}`) }, + ], + }); + lastChainId = requestedHex; + } + } + + const result = await proxy.ethereum.request({ + method: request.method, + params: normalizeRequestParams(request.params) as never, + }); + await walletKit.respondSessionRequest({ + topic, + response: { + id, + jsonrpc: "2.0", + result, + }, + }); + setStatus(`Completed ${request.method}`); + } catch (error) { + const err = error as { code?: number; message?: string }; + await walletKit.respondSessionRequest({ + topic, + response: { + id, + jsonrpc: "2.0", + error: { + code: typeof err.code === "number" ? err.code : 5000, + message: + err.message || + (error instanceof Error ? error.message : String(error)), + }, + }, + }); + setStatus(err.message || "Request failed", true); + } + }, + ); + + walletKit.on("session_delete", () => { + const count = Object.keys(walletKit.getActiveSessions()).length; + setStatus( + count > 0 + ? `${count} active WalletConnect session(s)` + : "No active sessions", + ); + }); + + return { + async pair(uri: string) { + const trimmed = uri.trim(); + if (!trimmed.startsWith("wc:")) { + throw new Error("URI must start with wc:"); + } + setStatus("Pairing…"); + await walletKit.pair({ uri: trimmed }); + setStatus("Waiting for session proposal…"); + }, + getActiveSessionCount() { + return Object.keys(walletKit.getActiveSessions()).length; + }, + }; +} + +export function readWalletConnectUriFromLocation(): string | null { + const params = new URLSearchParams(window.location.search); + const fromQuery = params.get("uri"); + if (fromQuery?.startsWith("wc:")) { + return fromQuery; + } + if (fromQuery) { + try { + const decoded = decodeURIComponent(fromQuery); + if (decoded.startsWith("wc:")) return decoded; + } catch { + // ignore + } + } + + const hash = window.location.hash.replace(/^#/, ""); + if (hash.startsWith("wc:")) { + return hash; + } + const hashParams = new URLSearchParams(hash); + const fromHash = hashParams.get("uri"); + if (fromHash?.startsWith("wc:")) { + return fromHash; + } + return null; +} diff --git a/nginx.conf b/nginx.conf index 86755dc..73db703 100644 --- a/nginx.conf +++ b/nginx.conf @@ -42,6 +42,15 @@ server { return 301 /create/; } + # First-party mobile PWA + WalletConnect host + location /mobile/ { + try_files $uri $uri/ /mobile/index.html; + } + + location = /mobile { + return 301 /mobile/; + } + # HTML entry points — avoid long CDN caches so deploys pick up new asset hashes location = /index.html { add_header Cache-Control "no-cache" always; @@ -51,7 +60,11 @@ server { add_header Cache-Control "no-cache" always; } - # Branding Layer SPA at domain root (/signer/, /create/, /assets/ above take precedence) + location = /mobile/index.html { + add_header Cache-Control "no-cache" always; + } + + # Branding Layer SPA at domain root (/signer/, /create/, /mobile/, /assets/ above take precedence) location / { try_files $uri $uri/ /index.html; } diff --git a/package-lock.json b/package-lock.json index 7e2d765..afa339c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,19 +8,23 @@ "name": "@1shotapi/embedded-wallet", "version": "0.0.0", "workspaces": [ - "host" + "host", + "extension" ], "dependencies": { "@1shotapi/ows-oid4": "^0.4.0", "@1shotapi/ows-provider": "^0.4.2", - "@1shotapi/ows-signer": "^0.4.0", - "@1shotapi/ows-signer-utils": "^0.5.0", + "@1shotapi/ows-signer": "^0.4.2", + "@1shotapi/ows-signer-utils": "^0.5.1", "@1shotapi/ows-types": "^0.5.1", "@1shotapi/ows-wallet-utils": "^0.4.1", "@fontsource-variable/geist": "^5.2.9", "@metamask/smart-accounts-kit": "^1.7.0", + "@reown/walletkit": "^1.5.6", "@simplewebauthn/browser": "^13.3.0", "@tanstack/react-table": "^8.21.3", + "@walletconnect/core": "^2.23.10", + "@walletconnect/utils": "^2.23.10", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "lucide-react": "^0.525.0", @@ -52,6 +56,20 @@ "vite": "^7.0.4" } }, + "extension": { + "name": "@1shotapi/oneshot-wallet-extension", + "version": "0.1.0", + "hasInstallScript": true, + "dependencies": { + "@1shotapi/ows-provider": "^0.4.2", + "@1shotapi/ows-types": "^0.5.1" + }, + "devDependencies": { + "@types/chrome": "^0.0.287", + "typescript": "~5.8.0", + "wxt": "^0.21.3" + } + }, "host": { "name": "@1shotapi/oneshot-wallet-host", "version": "0.0.0", @@ -91,6 +109,21 @@ "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, + "node_modules/@1natsu/wait-element": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@1natsu/wait-element/-/wait-element-4.2.0.tgz", + "integrity": "sha512-Om0Q+WE9mNrpY4AwMTvkFiYHv8VM7TML3PvOqXy+w6kAjLjKhGYHYX+305+a6J8RVpds9s7IF2Z5aOPYwULFNw==", + "dev": true, + "license": "MIT", + "dependencies": { + "defu": "^6.1.4", + "many-keys-map": "^3.0.0" + } + }, + "node_modules/@1shotapi/oneshot-wallet-extension": { + "resolved": "extension", + "link": true + }, "node_modules/@1shotapi/oneshot-wallet-host": { "resolved": "host", "link": true @@ -117,15 +150,15 @@ } }, "node_modules/@1shotapi/ows-signer": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/@1shotapi/ows-signer/-/ows-signer-0.4.0.tgz", - "integrity": "sha512-Q9x0BxXg7XrsHjFN0ypSZtWP7TmrUcC1QKHhnqURzIw+CfOKtKBzLLskCfF/2C41Ab5xQAadr98lmX+8dnL6nw==", + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@1shotapi/ows-signer/-/ows-signer-0.4.2.tgz", + "integrity": "sha512-q10zEKjZlfl/e6hJAJlBVlidveDfjx4UFql17Tc6jPYmlNLhCxxjIQp/0SBQTVRL5HRWadqFytWZAId8SATd9A==", "license": "MIT" }, "node_modules/@1shotapi/ows-signer-utils": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/@1shotapi/ows-signer-utils/-/ows-signer-utils-0.5.0.tgz", - "integrity": "sha512-52t4sG5uHSAdBfmN5tNfv0poqY2GryFfZr0EqSp/eVGEi1P4X7UqVnZnRz4u2gMK0EtfDbI9iq6xLfBQ7zXvQA==", + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/@1shotapi/ows-signer-utils/-/ows-signer-utils-0.5.1.tgz", + "integrity": "sha512-EC2jJ04Aak5kLqmwfkPGYGsGy+tyzKw0rkAqCKeTQkixGdOfHL1zrsi9sGensFaFvGjgyPjCoupBKVs/QdR1qg==", "license": "MIT", "dependencies": { "@1shotapi/ows-types": "*", @@ -166,6 +199,214 @@ "integrity": "sha512-nhCBV3quEgesuf7c7KYfperqSS14T8bYuvJ8PcLJp6znkZpFc0AuW4qBtr8eKVyPPe/8RSr7sglCWPU5eaxwKQ==", "license": "MIT" }, + "node_modules/@aklinker1/rollup-plugin-visualizer": { + "version": "5.12.0", + "resolved": "https://registry.npmjs.org/@aklinker1/rollup-plugin-visualizer/-/rollup-plugin-visualizer-5.12.0.tgz", + "integrity": "sha512-X24LvEGw6UFmy0lpGJDmXsMyBD58XmX1bbwsaMLhNoM+UMQfQ3b2RtC+nz4b/NoRK5r6QJSKJHBNVeUdwqybaQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "open": "^8.4.0", + "picomatch": "^2.3.1", + "source-map": "^0.7.4", + "yargs": "^17.5.1" + }, + "bin": { + "rollup-plugin-visualizer": "dist/bin/cli.js" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "rollup": "2.x || 3.x || 4.x" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@aklinker1/rollup-plugin-visualizer/node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@aklinker1/rollup-plugin-visualizer/node_modules/define-lazy-prop": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz", + "integrity": "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@aklinker1/rollup-plugin-visualizer/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/@aklinker1/rollup-plugin-visualizer/node_modules/is-docker": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", + "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", + "dev": true, + "license": "MIT", + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@aklinker1/rollup-plugin-visualizer/node_modules/is-wsl": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", + "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-docker": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@aklinker1/rollup-plugin-visualizer/node_modules/open": { + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/open/-/open-8.4.2.tgz", + "integrity": "sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-lazy-prop": "^2.0.0", + "is-docker": "^2.1.1", + "is-wsl": "^2.2.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@aklinker1/rollup-plugin-visualizer/node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/@aklinker1/rollup-plugin-visualizer/node_modules/source-map": { + "version": "0.7.6", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.6.tgz", + "integrity": "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">= 12" + } + }, + "node_modules/@aklinker1/rollup-plugin-visualizer/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@aklinker1/rollup-plugin-visualizer/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/@aklinker1/rollup-plugin-visualizer/node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/@aklinker1/rollup-plugin-visualizer/node_modules/yargs": { + "version": "17.7.3", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.3.tgz", + "integrity": "sha512-GZtjxm/J/4TSxuL3FNYjCmLktBTnIw/rVmKSIyKeYAZpmJB2ig9VauCC5xsa82GNKVKDAqpOn3KVzNt0zmrU0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@aklinker1/rollup-plugin-visualizer/node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/@aklinker1/zero-zip": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@aklinker1/zero-zip/-/zero-zip-1.0.1.tgz", + "integrity": "sha512-07a596Bd1QO0bi3tIyt2xruq6vKk7hCUt9enHBwggvipB0iiycoJ9/CqzN6UFawaKbOi6NBfMlUxUXzIOZ7Dsg==", + "dev": true, + "license": "MIT" + }, "node_modules/@alcalzone/ansi-tokenize": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/@alcalzone/ansi-tokenize/-/ansi-tokenize-0.3.0.tgz", @@ -1979,6 +2220,15 @@ } } }, + "node_modules/@msgpack/msgpack": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@msgpack/msgpack/-/msgpack-3.1.3.tgz", + "integrity": "sha512-47XIizs9XZXvuJgoaJUIE2lFoID8ugvc0jzSHP+Ptfk8nTbnR8g788wv48N03Kx0UkAv559HWRQ3yzOgzlRNUA==", + "license": "ISC", + "engines": { + "node": ">= 18" + } + }, "node_modules/@napi-rs/wasm-runtime": { "version": "1.1.6", "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.6.tgz", @@ -4894,6 +5144,22 @@ "integrity": "sha512-xnXE7wG13PI+cxieVssYXlQJuYVRhH9NBoxt3KNwzghDIA69GMm7d4wXRouHIYjE+KvS6U/MsMO73NdS2MH9ZA==", "license": "MIT" }, + "node_modules/@reown/walletkit": { + "version": "1.5.6", + "resolved": "https://registry.npmjs.org/@reown/walletkit/-/walletkit-1.5.6.tgz", + "integrity": "sha512-pTjarXQ57ImH8s5kUWRT3V++RNCZMGp9FQKODwobyPxmOPkfre3+s7v2PwwH2fDcoNwGRi2eblqoPx3qb9+NrQ==", + "license": "SEE LICENSE IN LICENSE.md", + "dependencies": { + "@walletconnect/core": "2.23.10", + "@walletconnect/jsonrpc-provider": "1.0.14", + "@walletconnect/jsonrpc-utils": "1.0.8", + "@walletconnect/logger": "3.0.2", + "@walletconnect/pay": "1.0.9", + "@walletconnect/sign-client": "2.23.10", + "@walletconnect/types": "2.23.10", + "@walletconnect/utils": "2.23.10" + } + }, "node_modules/@rolldown/pluginutils": { "version": "1.0.0-beta.27", "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", @@ -5761,6 +6027,16 @@ "url": "https://github.com/sponsors/tannerlinsley" } }, + "node_modules/@topcli/prompts": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@topcli/prompts/-/prompts-4.0.0.tgz", + "integrity": "sha512-kkKYPb4k/6kRdnESt77B5vJrrYnYHIYczwv0P1c+tfN/IWEx3KZeKEHoUq7sImIyBEXWdA2uakIroD9pmRJziQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=22.0.0" + } + }, "node_modules/@ts-morph/common": { "version": "0.27.0", "resolved": "https://registry.npmjs.org/@ts-morph/common/-/common-0.27.0.tgz", @@ -5828,6 +6104,17 @@ "@babel/types": "^7.28.2" } }, + "node_modules/@types/chrome": { + "version": "0.0.287", + "resolved": "https://registry.npmjs.org/@types/chrome/-/chrome-0.0.287.tgz", + "integrity": "sha512-wWhBNPNXZHwycHKNYnexUcpSbrihVZu++0rdp6GEk5ZgAglenLx+RwdEouh6FrHS0XQiOxSd62yaujM1OoQlZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/filesystem": "*", + "@types/har-format": "*" + } + }, "node_modules/@types/debug": { "version": "4.1.13", "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.13.tgz", @@ -5851,6 +6138,30 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/filesystem": { + "version": "0.0.36", + "resolved": "https://registry.npmjs.org/@types/filesystem/-/filesystem-0.0.36.tgz", + "integrity": "sha512-vPDXOZuannb9FZdxgHnqSwAG/jvdGM8Wq+6N4D/d80z+D4HWH+bItqsZaVRQykAn6WEVeEkLm2oQigyHtgb0RA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/filewriter": "*" + } + }, + "node_modules/@types/filewriter": { + "version": "0.0.33", + "resolved": "https://registry.npmjs.org/@types/filewriter/-/filewriter-0.0.33.tgz", + "integrity": "sha512-xFU8ZXTw4gd358lb2jw25nxY9QAgqn2+bKKjKOYfNCzN4DKCFetK7sPtrlpg66Ywe3vWY9FNxprZawAh9wfJ3g==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/har-format": { + "version": "1.2.16", + "resolved": "https://registry.npmjs.org/@types/har-format/-/har-format-1.2.16.tgz", + "integrity": "sha512-fluxdy7ryD3MV6h8pTfTYpy/xQzCFC7m89nOH9y94cNqJ1mDIDPut7MnRHI3F6qRmh/cT2fUjG1MLdCNb4hE9A==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/json-schema": { "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", @@ -5952,52 +6263,539 @@ "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" } }, - "node_modules/abitype": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/abitype/-/abitype-1.2.3.tgz", - "integrity": "sha512-Ofer5QUnuUdTFsBRwARMoWKOH1ND5ehwYhJ3OJ/BQO+StkwQjHw0XyVh4vDttzHB7QOFhPHa/o413PJ82gU/Tg==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/wevm" - }, - "peerDependencies": { - "typescript": ">=5.0.4", - "zod": "^3.22.0 || ^4.0.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - }, - "zod": { - "optional": true - } - } - }, - "node_modules/accepts": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", - "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", - "license": "MIT", + "node_modules/@walletconnect/core": { + "version": "2.23.10", + "resolved": "https://registry.npmjs.org/@walletconnect/core/-/core-2.23.10.tgz", + "integrity": "sha512-Qq2btHEoCgruvkZCWLSrVsvg/dYbM9Z045qeClwhJR4meL32jbIRT0mKWjf0HkRc2LA82MsnszVnfuZl3yWl5A==", + "license": "SEE LICENSE IN LICENSE.md", "dependencies": { - "mime-types": "^3.0.0", - "negotiator": "^1.0.0" + "@walletconnect/heartbeat": "1.2.2", + "@walletconnect/jsonrpc-provider": "1.0.14", + "@walletconnect/jsonrpc-types": "1.0.4", + "@walletconnect/jsonrpc-utils": "1.0.8", + "@walletconnect/jsonrpc-ws-connection": "1.0.16", + "@walletconnect/keyvaluestorage": "1.1.1", + "@walletconnect/logger": "3.0.2", + "@walletconnect/relay-api": "1.0.11", + "@walletconnect/relay-auth": "1.1.0", + "@walletconnect/safe-json": "1.0.2", + "@walletconnect/time": "1.0.2", + "@walletconnect/types": "2.23.10", + "@walletconnect/utils": "2.23.10", + "@walletconnect/window-getters": "1.0.1", + "es-toolkit": "1.45.1", + "events": "3.3.0", + "uint8arrays": "3.1.1" }, "engines": { - "node": ">= 0.6" + "node": ">=18.20.8" } }, - "node_modules/acorn": { - "version": "8.17.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.17.0.tgz", - "integrity": "sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==", - "dev": true, + "node_modules/@walletconnect/core/node_modules/es-toolkit": { + "version": "1.45.1", + "resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.45.1.tgz", + "integrity": "sha512-/jhoOj/Fx+A+IIyDNOvO3TItGmlMKhtX8ISAHKE90c4b/k1tqaqEZ+uUqfpU8DMnW5cgNJv606zS55jGvza0Xw==", "license": "MIT", - "peer": true, - "bin": { - "acorn": "bin/acorn" - }, - "engines": { - "node": ">=0.4.0" + "workspaces": [ + "docs", + "benchmarks" + ] + }, + "node_modules/@walletconnect/environment": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@walletconnect/environment/-/environment-1.0.1.tgz", + "integrity": "sha512-T426LLZtHj8e8rYnKfzsw1aG6+M0BT1ZxayMdv/p8yM0MU+eJDISqNY3/bccxRr4LrF9csq02Rhqt08Ibl0VRg==", + "license": "MIT", + "dependencies": { + "tslib": "1.14.1" + } + }, + "node_modules/@walletconnect/environment/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "license": "0BSD" + }, + "node_modules/@walletconnect/events": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@walletconnect/events/-/events-1.0.1.tgz", + "integrity": "sha512-NPTqaoi0oPBVNuLv7qPaJazmGHs5JGyO8eEAk5VGKmJzDR7AHzD4k6ilox5kxk1iwiOnFopBOOMLs86Oa76HpQ==", + "license": "MIT", + "dependencies": { + "keyvaluestorage-interface": "^1.0.0", + "tslib": "1.14.1" + } + }, + "node_modules/@walletconnect/events/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "license": "0BSD" + }, + "node_modules/@walletconnect/heartbeat": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@walletconnect/heartbeat/-/heartbeat-1.2.2.tgz", + "integrity": "sha512-uASiRmC5MwhuRuf05vq4AT48Pq8RMi876zV8rr8cV969uTOzWdB/k+Lj5yI2PBtB1bGQisGen7MM1GcZlQTBXw==", + "license": "MIT", + "dependencies": { + "@walletconnect/events": "^1.0.1", + "@walletconnect/time": "^1.0.2", + "events": "^3.3.0" + } + }, + "node_modules/@walletconnect/jsonrpc-provider": { + "version": "1.0.14", + "resolved": "https://registry.npmjs.org/@walletconnect/jsonrpc-provider/-/jsonrpc-provider-1.0.14.tgz", + "integrity": "sha512-rtsNY1XqHvWj0EtITNeuf8PHMvlCLiS3EjQL+WOkxEOA4KPxsohFnBDeyPYiNm4ZvkQdLnece36opYidmtbmow==", + "license": "MIT", + "dependencies": { + "@walletconnect/jsonrpc-utils": "^1.0.8", + "@walletconnect/safe-json": "^1.0.2", + "events": "^3.3.0" + } + }, + "node_modules/@walletconnect/jsonrpc-types": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@walletconnect/jsonrpc-types/-/jsonrpc-types-1.0.4.tgz", + "integrity": "sha512-P6679fG/M+wuWg9TY8mh6xFSdYnFyFjwFelxyISxMDrlbXokorEVXYOxiqEbrU3x1BmBoCAJJ+vtEaEoMlpCBQ==", + "license": "MIT", + "dependencies": { + "events": "^3.3.0", + "keyvaluestorage-interface": "^1.0.0" + } + }, + "node_modules/@walletconnect/jsonrpc-utils": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@walletconnect/jsonrpc-utils/-/jsonrpc-utils-1.0.8.tgz", + "integrity": "sha512-vdeb03bD8VzJUL6ZtzRYsFMq1eZQcM3EAzT0a3st59dyLfJ0wq+tKMpmGH7HlB7waD858UWgfIcudbPFsbzVdw==", + "license": "MIT", + "dependencies": { + "@walletconnect/environment": "^1.0.1", + "@walletconnect/jsonrpc-types": "^1.0.3", + "tslib": "1.14.1" + } + }, + "node_modules/@walletconnect/jsonrpc-utils/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "license": "0BSD" + }, + "node_modules/@walletconnect/jsonrpc-ws-connection": { + "version": "1.0.16", + "resolved": "https://registry.npmjs.org/@walletconnect/jsonrpc-ws-connection/-/jsonrpc-ws-connection-1.0.16.tgz", + "integrity": "sha512-G81JmsMqh5nJheE1mPst1W0WfVv0SG3N7JggwLLGnI7iuDZJq8cRJvQwLGKHn5H1WTW7DEPCo00zz5w62AbL3Q==", + "license": "MIT", + "dependencies": { + "@walletconnect/jsonrpc-utils": "^1.0.6", + "@walletconnect/safe-json": "^1.0.2", + "events": "^3.3.0", + "ws": "^7.5.1" + } + }, + "node_modules/@walletconnect/jsonrpc-ws-connection/node_modules/ws": { + "version": "7.5.13", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.13.tgz", + "integrity": "sha512-rsKI6xDBFVf4r/x8XyChGK04QR/XHroxs/jUcoWvtEZM8TPU/X/uIY9B1CsSzYws9ZJb/6bbBu7dPhFW00CAoA==", + "license": "MIT", + "engines": { + "node": ">=8.3.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/@walletconnect/keyvaluestorage": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@walletconnect/keyvaluestorage/-/keyvaluestorage-1.1.1.tgz", + "integrity": "sha512-V7ZQq2+mSxAq7MrRqDxanTzu2RcElfK1PfNYiaVnJgJ7Q7G7hTVwF8voIBx92qsRyGHZihrwNPHuZd1aKkd0rA==", + "license": "MIT", + "dependencies": { + "@walletconnect/safe-json": "^1.0.1", + "idb-keyval": "^6.2.1", + "unstorage": "^1.9.0" + }, + "peerDependencies": { + "@react-native-async-storage/async-storage": "1.x" + }, + "peerDependenciesMeta": { + "@react-native-async-storage/async-storage": { + "optional": true + } + } + }, + "node_modules/@walletconnect/logger": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@walletconnect/logger/-/logger-3.0.2.tgz", + "integrity": "sha512-7wR3wAwJTOmX4gbcUZcFMov8fjftY05+5cO/d4cpDD8wDzJ+cIlKdYOXaXfxHLSYeDazMXIsxMYjHYVDfkx+nA==", + "license": "MIT", + "dependencies": { + "@walletconnect/safe-json": "^1.0.2", + "pino": "10.0.0" + } + }, + "node_modules/@walletconnect/pay": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@walletconnect/pay/-/pay-1.0.9.tgz", + "integrity": "sha512-09L6KhM6IeWKNdYefuuWwVxWpAsTdjCPETl1TQT5bWzyO9TiDSov70Y6knaZD19R173sTqKTyNfego/Y+iscDg==", + "license": "SEE LICENSE IN LICENSE.md", + "dependencies": { + "@walletconnect/logger": "3.0.2", + "@walletconnect/types": "2.23.10", + "@walletconnect/utils": "2.23.10", + "brotli": "1.3.3" + }, + "peerDependencies": { + "react-native": ">=0.64.0" + }, + "peerDependenciesMeta": { + "react-native": { + "optional": true + } + } + }, + "node_modules/@walletconnect/relay-api": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@walletconnect/relay-api/-/relay-api-1.0.11.tgz", + "integrity": "sha512-tLPErkze/HmC9aCmdZOhtVmYZq1wKfWTJtygQHoWtgg722Jd4homo54Cs4ak2RUFUZIGO2RsOpIcWipaua5D5Q==", + "license": "MIT", + "dependencies": { + "@walletconnect/jsonrpc-types": "^1.0.2" + } + }, + "node_modules/@walletconnect/relay-auth": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@walletconnect/relay-auth/-/relay-auth-1.1.0.tgz", + "integrity": "sha512-qFw+a9uRz26jRCDgL7Q5TA9qYIgcNY8jpJzI1zAWNZ8i7mQjaijRnWFKsCHAU9CyGjvt6RKrRXyFtFOpWTVmCQ==", + "license": "MIT", + "dependencies": { + "@noble/curves": "1.8.0", + "@noble/hashes": "1.7.0", + "@walletconnect/safe-json": "^1.0.1", + "@walletconnect/time": "^1.0.2", + "uint8arrays": "^3.0.0" + } + }, + "node_modules/@walletconnect/relay-auth/node_modules/@noble/curves": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.8.0.tgz", + "integrity": "sha512-j84kjAbzEnQHaSIhRPUmB3/eVXu2k3dKPl2LOrR8fSOIL+89U+7lV117EWHtq/GHM3ReGHM46iRBdZfpc4HRUQ==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "1.7.0" + }, + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@walletconnect/relay-auth/node_modules/@noble/hashes": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.7.0.tgz", + "integrity": "sha512-HXydb0DgzTpDPwbVeDGCG1gIu7X6+AuU6Zl6av/E/KG8LMsvPntvq+w17CHRpKBmN6Ybdrt1eP3k4cj8DJa78w==", + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@walletconnect/safe-json": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@walletconnect/safe-json/-/safe-json-1.0.2.tgz", + "integrity": "sha512-Ogb7I27kZ3LPC3ibn8ldyUr5544t3/STow9+lzz7Sfo808YD7SBWk7SAsdBFlYgP2zDRy2hS3sKRcuSRM0OTmA==", + "license": "MIT", + "dependencies": { + "tslib": "1.14.1" + } + }, + "node_modules/@walletconnect/safe-json/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "license": "0BSD" + }, + "node_modules/@walletconnect/sign-client": { + "version": "2.23.10", + "resolved": "https://registry.npmjs.org/@walletconnect/sign-client/-/sign-client-2.23.10.tgz", + "integrity": "sha512-vO7DGRRmKo+rykmjVyQR1aM4I2nbk9kJ6olbxgjFRR6Jdhy+Kz+zgN7Ce5xVhPfWYVu4bV/XhOQxhvnQw7S5ng==", + "license": "SEE LICENSE IN LICENSE.md", + "dependencies": { + "@walletconnect/core": "2.23.10", + "@walletconnect/jsonrpc-utils": "1.0.8", + "@walletconnect/logger": "3.0.2", + "@walletconnect/time": "1.0.2", + "@walletconnect/types": "2.23.10", + "@walletconnect/utils": "2.23.10", + "events": "3.3.0" + } + }, + "node_modules/@walletconnect/time": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@walletconnect/time/-/time-1.0.2.tgz", + "integrity": "sha512-uzdd9woDcJ1AaBZRhqy5rNC9laqWGErfc4dxA9a87mPdKOgWMD85mcFo9dIYIts/Jwocfwn07EC6EzclKubk/g==", + "license": "MIT", + "dependencies": { + "tslib": "1.14.1" + } + }, + "node_modules/@walletconnect/time/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "license": "0BSD" + }, + "node_modules/@walletconnect/types": { + "version": "2.23.10", + "resolved": "https://registry.npmjs.org/@walletconnect/types/-/types-2.23.10.tgz", + "integrity": "sha512-XP8d41979anTrc1OJF3ISF+g81cvp1wim+ObdNnbcaT/jhwLwv+0T7rRe9VwRv+h8EaRgLyeb5YGy7oJ49vxVg==", + "license": "SEE LICENSE IN LICENSE.md", + "dependencies": { + "@walletconnect/events": "1.0.1", + "@walletconnect/heartbeat": "1.2.2", + "@walletconnect/jsonrpc-types": "1.0.4", + "@walletconnect/keyvaluestorage": "1.1.1", + "@walletconnect/logger": "3.0.2", + "events": "3.3.0" + } + }, + "node_modules/@walletconnect/utils": { + "version": "2.23.10", + "resolved": "https://registry.npmjs.org/@walletconnect/utils/-/utils-2.23.10.tgz", + "integrity": "sha512-b1c9FRF2g7vNnz66oLW5WZD2VCMrbu9xhpmwJJwqGarBiGW7cY8NbUtS9/w2/qc0vsBVKJ/bzDn4TGjpELU6aQ==", + "license": "SEE LICENSE IN LICENSE.md", + "dependencies": { + "@msgpack/msgpack": "3.1.3", + "@noble/ciphers": "1.3.0", + "@noble/curves": "1.9.7", + "@noble/hashes": "1.8.0", + "@scure/base": "1.2.6", + "@walletconnect/jsonrpc-utils": "1.0.8", + "@walletconnect/keyvaluestorage": "1.1.1", + "@walletconnect/logger": "3.0.2", + "@walletconnect/relay-api": "1.0.11", + "@walletconnect/relay-auth": "1.1.0", + "@walletconnect/safe-json": "1.0.2", + "@walletconnect/time": "1.0.2", + "@walletconnect/types": "2.23.10", + "@walletconnect/window-getters": "1.0.1", + "@walletconnect/window-metadata": "1.0.1", + "blakejs": "1.2.1", + "detect-browser": "5.3.0", + "ox": "0.9.3", + "uint8arrays": "3.1.1" + } + }, + "node_modules/@walletconnect/utils/node_modules/@noble/curves": { + "version": "1.9.7", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.9.7.tgz", + "integrity": "sha512-gbKGcRUYIjA3/zCCNaWDciTMFI0dCkvou3TL8Zmy5Nc7sJ47a0jtOeZoTaMxkuqRo9cRhjOdZJXegxYE5FN/xw==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "1.8.0" + }, + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@walletconnect/utils/node_modules/ox": { + "version": "0.9.3", + "resolved": "https://registry.npmjs.org/ox/-/ox-0.9.3.tgz", + "integrity": "sha512-KzyJP+fPV4uhuuqrTZyok4DC7vFzi7HLUFiUNEmpbyh59htKWkOC98IONC1zgXJPbHAhQgqs6B0Z6StCGhmQvg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/wevm" + } + ], + "license": "MIT", + "dependencies": { + "@adraffy/ens-normalize": "^1.11.0", + "@noble/ciphers": "^1.3.0", + "@noble/curves": "1.9.1", + "@noble/hashes": "^1.8.0", + "@scure/bip32": "^1.7.0", + "@scure/bip39": "^1.6.0", + "abitype": "^1.0.9", + "eventemitter3": "5.0.1" + }, + "peerDependencies": { + "typescript": ">=5.4.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@walletconnect/utils/node_modules/ox/node_modules/@noble/curves": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.9.1.tgz", + "integrity": "sha512-k11yZxZg+t+gWvBbIswW0yoJlu8cHOC7dhunwOzoWH/mXGBiYyR4YY6hAEK/3EUs4UpB8la1RfdRpeGsFHkWsA==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "1.8.0" + }, + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@walletconnect/window-getters": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@walletconnect/window-getters/-/window-getters-1.0.1.tgz", + "integrity": "sha512-vHp+HqzGxORPAN8gY03qnbTMnhqIwjeRJNOMOAzePRg4xVEEE2WvYsI9G2NMjOknA8hnuYbU3/hwLcKbjhc8+Q==", + "license": "MIT", + "dependencies": { + "tslib": "1.14.1" + } + }, + "node_modules/@walletconnect/window-getters/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "license": "0BSD" + }, + "node_modules/@walletconnect/window-metadata": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@walletconnect/window-metadata/-/window-metadata-1.0.1.tgz", + "integrity": "sha512-9koTqyGrM2cqFRW517BPY/iEtUDx2r1+Pwwu5m7sJ7ka79wi3EyqhqcICk/yDmv6jAS1rjKgTKXlEhanYjijcA==", + "license": "MIT", + "dependencies": { + "@walletconnect/window-getters": "^1.0.1", + "tslib": "1.14.1" + } + }, + "node_modules/@walletconnect/window-metadata/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "license": "0BSD" + }, + "node_modules/@webext-core/fake-browser": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@webext-core/fake-browser/-/fake-browser-2.0.1.tgz", + "integrity": "sha512-4x5z1z8F0KU8ShF4ForXJ8qnA8oZTy7HYjI91Mbpdzp3H3hU9HR6TNPUxfWtSpFz2FwgEircuJKQg0qFIn6RLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@wxt-dev/browser": "*", + "lodash.merge": "^4.6.2" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@webext-core/isolated-element": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@webext-core/isolated-element/-/isolated-element-3.0.0.tgz", + "integrity": "sha512-PmSIBMSe+rFw6eXzpJoV+glc1HPO40clcT9FJ7rSfCQzNewK8/nQZoBYWen2cMqdXdBNM9to/IbNNu5mdQLaPg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-potential-custom-element-name": "^1.0.1" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@webext-core/match-patterns": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@webext-core/match-patterns/-/match-patterns-2.0.0.tgz", + "integrity": "sha512-EsyMMKfQuSE4rWCjP/TifwvUWlhC/OhiGn5OK3p9F0d57UmtLib0nzguzPKyxM1/kZV7SkE+jYXkE8gsdPRLmw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/@wxt-dev/browser": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/@wxt-dev/browser/-/browser-0.2.5.tgz", + "integrity": "sha512-676RhVsOFNSZnisfeM9U40DdYjZmx5whJuBHJ7v4R2sR6e/SYfuIuzmJf3owr2n82d2vCDwGNbvt4nLU14sGZg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/filesystem": "*", + "@types/har-format": "*" + } + }, + "node_modules/@wxt-dev/storage": { + "version": "1.2.9", + "resolved": "https://registry.npmjs.org/@wxt-dev/storage/-/storage-1.2.9.tgz", + "integrity": "sha512-dh9TJwvLBM2x4wyy9nE7eYE3wA8pgz1TXSyz7RbdjkJxLfFfWkbeIqiU7VkGy7Vx8sJBmQjoSpk9zSUb3Bjy0Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@wxt-dev/browser": ">=0.1", + "superlock": "^1.3.2" + }, + "funding": { + "url": "https://github.com/sponsors/wxt-dev" + } + }, + "node_modules/abitype": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/abitype/-/abitype-1.2.3.tgz", + "integrity": "sha512-Ofer5QUnuUdTFsBRwARMoWKOH1ND5ehwYhJ3OJ/BQO+StkwQjHw0XyVh4vDttzHB7QOFhPHa/o413PJ82gU/Tg==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/wevm" + }, + "peerDependencies": { + "typescript": ">=5.0.4", + "zod": "^3.22.0 || ^4.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + }, + "zod": { + "optional": true + } + } + }, + "node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "license": "MIT", + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/acorn": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.18.0.tgz", + "integrity": "sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" } }, "node_modules/acorn-jsx": { @@ -6111,6 +6909,31 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/anymatch/node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", @@ -6151,6 +6974,15 @@ "astring": "bin/astring" } }, + "node_modules/atomic-sleep": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz", + "integrity": "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==", + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, "node_modules/atomically": { "version": "1.7.0", "resolved": "https://registry.npmjs.org/atomically/-/atomically-1.7.0.tgz", @@ -6182,6 +7014,26 @@ "node": "18 || 20 || >=22" } }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, "node_modules/baseline-browser-mapping": { "version": "2.10.43", "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.43.tgz", @@ -6194,6 +7046,12 @@ "node": ">=6.0.0" } }, + "node_modules/blakejs": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/blakejs/-/blakejs-1.2.1.tgz", + "integrity": "sha512-QXUSXI3QVc/gJME0dBpXrag1kbzOqCjCX8/b54ntNyW6sjtoqxqRk3LTmXzaJoh71zMsDCjM+47jS7XiwN/+fQ==", + "license": "MIT" + }, "node_modules/body-parser": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.3.0.tgz", @@ -6231,6 +7089,20 @@ "url": "https://opencollective.com/express" } }, + "node_modules/boolbase": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-2.0.0.tgz", + "integrity": "sha512-DkVaaQHymRhpYEYo9x1oo7Q7B0Y6KJUsjm3c9eTyFDby4MHLBTwZ6ZDWBel5zrYxj1WsZgC5oLpiz+93MluXeA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=20.19.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + }, "node_modules/brace-expansion": { "version": "5.0.9", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz", @@ -6255,6 +7127,15 @@ "node": ">=8" } }, + "node_modules/brotli": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/brotli/-/brotli-1.3.3.tgz", + "integrity": "sha512-oTKjJdShmDuGW94SyyaoQvAjf30dZaHnjJ8uAF+u2/vGJkJbJPJAT1gDiOJP5v1Zb6f9KEyW/1HpuaWIXtGHPg==", + "license": "MIT", + "dependencies": { + "base64-js": "^1.1.2" + } + }, "node_modules/browserslist": { "version": "4.28.6", "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.6.tgz", @@ -6312,6 +7193,58 @@ "node": ">= 0.8" } }, + "node_modules/c12": { + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/c12/-/c12-3.3.4.tgz", + "integrity": "sha512-cM0ApFQSBXuourJejzwv/AuPRvAxordTyParRVcHjjtXirtkzM0uK2L9TTn9s0cXZbG7E55jCivRQzoxYmRAlA==", + "dev": true, + "license": "MIT", + "dependencies": { + "chokidar": "^5.0.0", + "confbox": "^0.2.4", + "defu": "^6.1.6", + "dotenv": "^17.3.1", + "exsolve": "^1.0.8", + "giget": "^3.2.0", + "jiti": "^2.6.1", + "ohash": "^2.0.11", + "pathe": "^2.0.3", + "perfect-debounce": "^2.1.0", + "pkg-types": "^2.3.0", + "rc9": "^3.0.1" + }, + "peerDependencies": { + "magicast": "*" + }, + "peerDependenciesMeta": { + "magicast": { + "optional": true + } + } + }, + "node_modules/c12/node_modules/dotenv": { + "version": "17.4.2", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.4.2.tgz", + "integrity": "sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/cac": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/cac/-/cac-7.0.0.tgz", + "integrity": "sha512-tixWYgm5ZoOD+3g6UTea91eow5z6AAHaho3g0V9CNSNb45gM8SmflpAc+GRd1InC4AqN/07Unrgp56Y94N9hJQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20.19.0" + } + }, "node_modules/call-bind-apply-helpers": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", @@ -6422,6 +7355,28 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/chokidar": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-5.0.0.tgz", + "integrity": "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==", + "license": "MIT", + "dependencies": { + "readdirp": "^5.0.0" + }, + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/citty": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/citty/-/citty-0.2.2.tgz", + "integrity": "sha512-+6vJA3L98yv+IdfKGZHBNiGW5KHn22e/JwID0Strsz8h4S/csAu/OuICwxrg44k5MRiZHWIo8XXuJgQTriRP4w==", + "dev": true, + "license": "MIT" + }, "node_modules/cjs-module-lexer": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-2.2.0.tgz", @@ -6696,6 +7651,16 @@ "dev": true, "license": "MIT" }, + "node_modules/consola": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/consola/-/consola-3.4.2.tgz", + "integrity": "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.18.0 || >=16.10.0" + } + }, "node_modules/content-disposition": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.1.0.tgz", @@ -6743,6 +7708,12 @@ "node": ">= 0.6" } }, + "node_modules/cookie-es": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/cookie-es/-/cookie-es-1.2.3.tgz", + "integrity": "sha512-lXVyvUvrNXblMqzIRrxHb57UUVmqsSWlxqt3XIjCkUP0wDAf6uicO6KMbEgYrMNtEvWgWHwe42CKxPu9MYAnWw==", + "license": "MIT" + }, "node_modules/cookie-signature": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", @@ -6842,6 +7813,50 @@ "node": ">= 8" } }, + "node_modules/crossws": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/crossws/-/crossws-0.3.5.tgz", + "integrity": "sha512-ojKiDvcmByhwa8YYqbQI/hg7MEU0NC03+pSdEq4ZUnZR9xXpwk7E43SMNGkn+JxJGPFtNvQ48+vV2p+P1ml5PA==", + "license": "MIT", + "dependencies": { + "uncrypto": "^0.1.3" + } + }, + "node_modules/css-select": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-7.0.0.tgz", + "integrity": "sha512-snmjEVXy+1LnwXdxhYvTMj1d9tOh4HxkA1YmoayVBeeyR2C14Pum7fcxJIm4SswYspVy866eYNwlH6xC3/VH5g==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^2.0.0", + "css-what": "^8.0.0", + "domhandler": "^6.0.1", + "domutils": "^4.0.2", + "nth-check": "^3.0.1" + }, + "engines": { + "node": ">=20.19.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/css-what": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-8.0.0.tgz", + "integrity": "sha512-DH0Bqq3DNp5tdOReuNyAA+Ev4Y2GS5FMbZpeTLP6C4CDi0h5nL0BmUPChXw3o/qbHLDWHl49sbNqQVY7bMSDdw==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=20.19.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + }, "node_modules/cssesc": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", @@ -6854,6 +7869,13 @@ "node": ">=4" } }, + "node_modules/cssom": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.5.0.tgz", + "integrity": "sha512-iKuQcq+NdHqlAcwUY0o/HL69XQrUaQdMjmStJ8JFmUaiiQErlhrmuigkg/CU4E2J0IyUKUrMAgl36TvN67MqTw==", + "dev": true, + "license": "MIT" + }, "node_modules/csstype": { "version": "3.2.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", @@ -6973,6 +7995,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/defu": { + "version": "6.1.7", + "resolved": "https://registry.npmjs.org/defu/-/defu-6.1.7.tgz", + "integrity": "sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ==", + "license": "MIT" + }, "node_modules/depd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", @@ -6997,6 +8025,18 @@ "typescript": ">=5.0.4 <6" } }, + "node_modules/destr": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/destr/-/destr-2.0.5.tgz", + "integrity": "sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==", + "license": "MIT" + }, + "node_modules/detect-browser": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/detect-browser/-/detect-browser-5.3.0.tgz", + "integrity": "sha512-53rsFbGdwMwlF7qvCt0ypLM5V5/Mbl0szB7GPN8y9NCcbknYOeVVXdrXEq+90IwAfrrzt6Hd+u2E2ntakICU8w==", + "license": "MIT" + }, "node_modules/detect-libc": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", @@ -7028,6 +8068,77 @@ "integrity": "sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==", "license": "MIT" }, + "node_modules/dom-serializer": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-3.1.1.tgz", + "integrity": "sha512-4MEa38/QexBob6gFNwu+EGdWvhJ1OKuNwdYY3Y3NyeWDQfnGeDYQUDfIRzWu5B5gsv03so2Uxd28YC6zrsx3Lw==", + "dev": true, + "license": "MIT", + "dependencies": { + "domelementtype": "^3.0.0", + "domhandler": "^6.0.0", + "entities": "^8.0.0" + }, + "engines": { + "node": ">=20.19.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/domelementtype": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-3.0.0.tgz", + "integrity": "sha512-umCQid3jKbDmVjx8jGaW7uUykm4DEUeyV21hPxNMo2nV955DhUThwqyOIDtreepP31hl84X7G5U9ZfsWvIB3Pg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "BSD-2-Clause", + "engines": { + "node": ">=20.19.0" + } + }, + "node_modules/domhandler": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-6.0.1.tgz", + "integrity": "sha512-gYzvtM72ZtxQO0T048kd6HWSbbGCNOUwcnfQ01cqIJ4X2IYKFFHZ5mKvrQETcFXxsRObZulDaKmy//R7TPtsBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "domelementtype": "^3.0.0" + }, + "engines": { + "node": ">=20.19.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/domutils": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-4.0.2.tgz", + "integrity": "sha512-qI4JLRKnSzqFqr7hAlS5xQDusBCjKSEG4t4+7aNrIQMHBcsC2TGEhuyABJdYkgSewL57PNLYEiibY2iPKhKpaA==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "dom-serializer": "^3.0.0", + "domelementtype": "^3.0.0", + "domhandler": "^6.0.0" + }, + "engines": { + "node": ">=20.19.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, "node_modules/dot-prop": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-6.0.1.tgz", @@ -7056,6 +8167,35 @@ "url": "https://dotenvx.com" } }, + "node_modules/dotenv-expand": { + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-13.0.0.tgz", + "integrity": "sha512-aBfBS8eYIeXmpHI9ThIlA7/WLq+SLt18iXUZhb52rW89QLKQFoIpPG1bPeewoPZsTyjSSO3T7234FBVUM1V2rA==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "dotenv": "^17.4.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dotenv-expand/node_modules/dotenv": { + "version": "17.4.2", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.4.2.tgz", + "integrity": "sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, "node_modules/dunder-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", @@ -7124,6 +8264,19 @@ "node": ">=8.6" } }, + "node_modules/entities": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-8.0.0.tgz", + "integrity": "sha512-zwfzJecQ/Uej6tusMqwAqU/6KL2XaB2VZ2Jg54Je6ahNBGNH6Ek6g3jjNCF0fG9EWQKGZNddNjU5F1ZQn/sBnA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=20.19.0" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/env-paths": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", @@ -7574,6 +8727,16 @@ "node": ">=4.0" } }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, "node_modules/esutils": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", @@ -7672,6 +8835,15 @@ "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==", "license": "MIT" }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "license": "MIT", + "engines": { + "node": ">=0.8.x" + } + }, "node_modules/eventsource": { "version": "3.0.7", "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.7.tgz", @@ -7780,6 +8952,13 @@ "express": ">= 4.11" } }, + "node_modules/exsolve": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.1.1.tgz", + "integrity": "sha512-9U/jZUgjnSGyntRr6y5Muu1MJcwFl6kPu7k8qLF0IMNfLqvw0NZ4nnVDq0RVoZ0RvCyumib4Ez3KYrVfilrw+g==", + "dev": true, + "license": "MIT" + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -7889,6 +9068,16 @@ "node": ">=16.0.0" } }, + "node_modules/filesize": { + "version": "11.0.22", + "resolved": "https://registry.npmjs.org/filesize/-/filesize-11.0.22.tgz", + "integrity": "sha512-RlCVs9CY+oSsRnNZn95J9vDXjNjOwddKyTFjOYtA4yxYVIxBnwiVVGJX+TFhsmu3uUf81JDGyijtYL9xgawlTw==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">= 10.8.0" + } + }, "node_modules/fill-range": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", @@ -8094,6 +9283,13 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/get-port-please": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/get-port-please/-/get-port-please-3.2.0.tgz", + "integrity": "sha512-I9QVvBw5U/hw3RmWpYKRumUeaDgxTPd401x364rLmWBJcOQ753eov1eTgzDqRG9bqFIfDc7gfzcQEWrUri3o1A==", + "dev": true, + "license": "MIT" + }, "node_modules/get-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", @@ -8123,6 +9319,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/giget": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/giget/-/giget-3.3.1.tgz", + "integrity": "sha512-r+mvuDjrjMpsdw46Kmeydb8bdHm7wOKw8wNBtTndkjbPjgAp5oUJUxRE76wZFknxIPokfWvep2qSXK37aXE6zg==", + "dev": true, + "license": "MIT", + "bin": { + "giget": "dist/cli.mjs" + } + }, "node_modules/glob-parent": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", @@ -8153,6 +9359,23 @@ "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", "license": "ISC" }, + "node_modules/h3": { + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/h3/-/h3-1.15.11.tgz", + "integrity": "sha512-L3THSe2MPeBwgIZVSH5zLdBBU90TOxarvhK9d04IDY2AmVS8j2Jz2LIWtwsGOU3lu2I5jCN7FNvVfY2+XyF+mg==", + "license": "MIT", + "dependencies": { + "cookie-es": "^1.2.3", + "crossws": "^0.3.5", + "defu": "^6.1.6", + "destr": "^2.0.5", + "iron-webcrypto": "^1.2.1", + "node-mock-http": "^1.0.4", + "radix3": "^1.1.2", + "ufo": "^1.6.3", + "uncrypto": "^0.1.3" + } + }, "node_modules/has-symbols": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", @@ -8203,6 +9426,125 @@ "node": ">=16.9.0" } }, + "node_modules/hookable": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/hookable/-/hookable-6.1.1.tgz", + "integrity": "sha512-U9LYDy1CwhMCnprUfeAZWZGByVbhd54hwepegYTK7Pi5NvqEj63ifz5z+xukznehT7i6NIZRu89Ay1AZmRsLEQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/html-escaper": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-3.0.3.tgz", + "integrity": "sha512-RuMffC89BOWQoY0WKGpIhn5gX3iI54O6nRA0yC124NYVtzjmFWBIiFd8M0x+ZdX0P9R4lADg1mgP8C7PxGOWuQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/htmlparser2": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-10.1.0.tgz", + "integrity": "sha512-VTZkM9GWRAtEpveh7MSF6SjjrpNVNNVJfFup7xTY3UpFtm67foy9HDVXneLtFVt4pMz5kZtgNcvCniNFb1hlEQ==", + "dev": true, + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.2.2", + "entities": "^7.0.1" + } + }, + "node_modules/htmlparser2/node_modules/dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "dev": true, + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/htmlparser2/node_modules/dom-serializer/node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/htmlparser2/node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "BSD-2-Clause" + }, + "node_modules/htmlparser2/node_modules/domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "domelementtype": "^2.3.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/htmlparser2/node_modules/domutils": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz", + "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/htmlparser2/node_modules/entities": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz", + "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/http-errors": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", @@ -8248,6 +9590,12 @@ "url": "https://opencollective.com/express" } }, + "node_modules/idb-keyval": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/idb-keyval/-/idb-keyval-6.3.0.tgz", + "integrity": "sha512-um+2dgAWmYsu615EXpWVwSmapJhON0G43t3Ka/EVaohzPQXSMqKEqeDK/oIW3Ow+BXaF2PvSc+oBTFp793A5Ow==", + "license": "Apache-2.0" + }, "node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", @@ -8520,6 +9868,15 @@ "node": ">= 0.10" } }, + "node_modules/iron-webcrypto": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/iron-webcrypto/-/iron-webcrypto-1.2.1.tgz", + "integrity": "sha512-feOM6FaSr6rEABp/eDfVseKyTMDt+KGpeB35SkVn9Tyn0CqvVsY3EwI0v5i8nMHyJnzCIQf7nsy3p41TPkJZhg==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/brc-dd" + } + }, "node_modules/is-arrayish": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", @@ -8659,6 +10016,13 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/is-potential-custom-element-name": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", + "dev": true, + "license": "MIT" + }, "node_modules/is-promise": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", @@ -8875,6 +10239,12 @@ "json-buffer": "3.0.1" } }, + "node_modules/keyvaluestorage-interface": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/keyvaluestorage-interface/-/keyvaluestorage-interface-1.0.0.tgz", + "integrity": "sha512-8t6Q3TclQ4uZynJY9IGr2+SsIGwK9JHcO6ootkHCGA0CrQCRy+VkouYNO2xicET6b9al7QKzpebNow+gkpCL8g==", + "license": "MIT" + }, "node_modules/kleur": { "version": "4.1.5", "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz", @@ -9166,6 +10536,49 @@ "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", "license": "MIT" }, + "node_modules/linkedom": { + "version": "0.18.13", + "resolved": "https://registry.npmjs.org/linkedom/-/linkedom-0.18.13.tgz", + "integrity": "sha512-ES/o9qotMpzpN2MHs+Iq/JcVoOj8Fa5wiQYrTdFpvAnwXL0g66XHHUc9WUMk6nAlBtGsFQ24ne+SYnvnaQ2FSw==", + "dev": true, + "license": "ISC", + "dependencies": { + "css-select": "^7.0.0", + "cssom": "^0.5.0", + "html-escaper": "^3.0.3", + "htmlparser2": "^10.1.0", + "uhyphen": "^0.2.0" + }, + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "canvas": ">= 2" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/local-pkg": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-1.2.1.tgz", + "integrity": "sha512-++gUqRDEvcnN6Zhqrr+y/CkVEHhlrR96vZn3nZZPYzMcBUyBtTKzB9NadClFIsIVSsu+3i9tfk/erqy9kAmt7Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "mlly": "^1.7.4", + "pkg-types": "^2.3.0", + "quansync": "^0.2.11" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, "node_modules/locate-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", @@ -9185,6 +10598,13 @@ "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", "license": "MIT" }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, "node_modules/log-symbols": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-6.0.0.tgz", @@ -9253,6 +10673,19 @@ "source-map-js": "^1.2.1" } }, + "node_modules/many-keys-map": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/many-keys-map/-/many-keys-map-3.0.3.tgz", + "integrity": "sha512-1DiZmDHPXMBgMRjeUtHy1q1VYmeJscHxhIAexX9z/zjRMP80+0ETuPfssi8z+kMY4DwUgsKuHqpjxgmeA9gBNA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/fregante" + } + }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -9409,6 +10842,38 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/mlly": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.8.2.tgz", + "integrity": "sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.16.0", + "pathe": "^2.0.3", + "pkg-types": "^1.3.1", + "ufo": "^1.6.3" + } + }, + "node_modules/mlly/node_modules/confbox": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.1.8.tgz", + "integrity": "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/mlly/node_modules/pkg-types": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.3.1.tgz", + "integrity": "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "confbox": "^0.1.8", + "mlly": "^1.7.4", + "pathe": "^2.0.1" + } + }, "node_modules/module-details-from-path": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/module-details-from-path/-/module-details-from-path-1.0.4.tgz", @@ -9422,6 +10887,25 @@ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "license": "MIT" }, + "node_modules/multiformats": { + "version": "9.9.0", + "resolved": "https://registry.npmjs.org/multiformats/-/multiformats-9.9.0.tgz", + "integrity": "sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg==", + "license": "(Apache-2.0 AND MIT)" + }, + "node_modules/nano-spawn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/nano-spawn/-/nano-spawn-2.1.0.tgz", + "integrity": "sha512-yTW+2okrElHiH4fsiz/+/zc0EDo9BDDoC3iKk8dpv1GeRc9nUWzUZHx6TofMWErchhUQR8hY9/Eu1Uja9x1nqA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20.17" + }, + "funding": { + "url": "https://github.com/sindresorhus/nano-spawn?sponsor=1" + } + }, "node_modules/nanoid": { "version": "3.3.18", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.18.tgz", @@ -9440,6 +10924,16 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, + "node_modules/nanospinner": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/nanospinner/-/nanospinner-1.2.2.tgz", + "integrity": "sha512-Zt/AmG6qRU3e+WnzGGLuMCEAO/dAu45stNbHY223tUxldaDAeE+FxSPsd9Q+j+paejmm0ZbrNVs5Sraqy3dRxA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picocolors": "^1.1.1" + } + }, "node_modules/natural-compare": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", @@ -9457,6 +10951,12 @@ "node": ">= 0.6" } }, + "node_modules/node-fetch-native": { + "version": "1.6.7", + "resolved": "https://registry.npmjs.org/node-fetch-native/-/node-fetch-native-1.6.7.tgz", + "integrity": "sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q==", + "license": "MIT" + }, "node_modules/node-gyp-build-optional-packages": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/node-gyp-build-optional-packages/-/node-gyp-build-optional-packages-5.1.1.tgz", @@ -9472,6 +10972,12 @@ "node-gyp-build-optional-packages-test": "build-test.js" } }, + "node_modules/node-mock-http": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/node-mock-http/-/node-mock-http-1.0.5.tgz", + "integrity": "sha512-KQyt/wLjG3TAc7DOUhpqWzgd4ERxR80JOlTK5VE5R1S12IaPVN5qkj4klBce9HPG1Njuup4Sb5bljaT34lIyjw==", + "license": "MIT" + }, "node_modules/node-releases": { "version": "2.0.51", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.51.tgz", @@ -9481,6 +10987,15 @@ "node": ">=18" } }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/npm-run-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-6.0.0.tgz", @@ -9509,6 +11024,41 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/nth-check": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-3.0.1.tgz", + "integrity": "sha512-GX0gsdbGVCgnRgbeGaubfjpBXyYRWOOCVeYh08bSQvDZqxz5ndXs1OTfAt/h36G1xvI94YIspsI0sVFqAV9+RQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^2.0.0" + }, + "engines": { + "node": ">=20.19.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/fb55/nth-check?sponsor=1" + } + }, + "node_modules/nypm": { + "version": "0.6.9", + "resolved": "https://registry.npmjs.org/nypm/-/nypm-0.6.9.tgz", + "integrity": "sha512-zxlE2yvSWZWmHcNdT3+5zV2lrCogeE9YOklHrR3dFjqutq5wO7GFDYLFDRXLsYnJzwvy/im9fYoxePvS0VTW0w==", + "dev": true, + "license": "MIT", + "dependencies": { + "citty": "^0.2.2", + "pathe": "^2.0.3", + "tinyexec": "^1.2.4" + }, + "bin": { + "nypm": "dist/cli.mjs" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -9539,6 +11089,33 @@ "node": ">= 10" } }, + "node_modules/ofetch": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/ofetch/-/ofetch-1.5.1.tgz", + "integrity": "sha512-2W4oUZlVaqAPAil6FUg/difl6YhqhUR7x2eZY4bQCko22UXg3hptq9KLQdqFClV+Wu85UX7hNtdGTngi/1BxcA==", + "license": "MIT", + "dependencies": { + "destr": "^2.0.5", + "node-fetch-native": "^1.6.7", + "ufo": "^1.6.1" + } + }, + "node_modules/ohash": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/ohash/-/ohash-2.0.11.tgz", + "integrity": "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/on-exit-leak-free": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/on-exit-leak-free/-/on-exit-leak-free-2.1.2.tgz", + "integrity": "sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==", + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/on-finished": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", @@ -9999,6 +11576,20 @@ "url": "https://opencollective.com/express" } }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/perfect-debounce": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-2.1.0.tgz", + "integrity": "sha512-LjgdTytVFXeUgtHZr9WYViYSM/g8MkcTPYDlPa3cDqMirHjKiSZPYd6DoL7pK8AJQr+uWkQvCjHNdiMqsrJs+g==", + "dev": true, + "license": "MIT" + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -10017,6 +11608,43 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/pino": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/pino/-/pino-10.0.0.tgz", + "integrity": "sha512-eI9pKwWEix40kfvSzqEP6ldqOoBIN7dwD/o91TY5z8vQI12sAffpR/pOqAD1IVVwIVHDpHjkq0joBPdJD0rafA==", + "license": "MIT", + "dependencies": { + "atomic-sleep": "^1.0.0", + "on-exit-leak-free": "^2.1.0", + "pino-abstract-transport": "^2.0.0", + "pino-std-serializers": "^7.0.0", + "process-warning": "^5.0.0", + "quick-format-unescaped": "^4.0.3", + "real-require": "^0.2.0", + "safe-stable-stringify": "^2.3.1", + "slow-redact": "^0.3.0", + "sonic-boom": "^4.0.1", + "thread-stream": "^3.0.0" + }, + "bin": { + "pino": "bin.js" + } + }, + "node_modules/pino-abstract-transport": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-2.0.0.tgz", + "integrity": "sha512-F63x5tizV6WCh4R6RHyi2Ml+M70DNRXt/+HANowMflpgGFMAym/VKm6G7ZOQRjqN7XbGxK1Lg9t6ZrtzOaivMw==", + "license": "MIT", + "dependencies": { + "split2": "^4.0.0" + } + }, + "node_modules/pino-std-serializers": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-7.1.0.tgz", + "integrity": "sha512-BndPH67/JxGExRgiX1dX0w1FvZck5Wa4aal9198SrRhZjH3GxKQUKIBnYJTdj2HDN3UQAS06HlfcSbQj2OHmaw==", + "license": "MIT" + }, "node_modules/pkce-challenge": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.1.tgz", @@ -10026,6 +11654,18 @@ "node": ">=16.20.0" } }, + "node_modules/pkg-types": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-2.3.1.tgz", + "integrity": "sha512-y+ichcgc2LrADuhLNAx8DFjVfgz91pRxfZdI3UDhxHvcVEZsenLO+7XaU5vOp0u/7V/wZ+plyuQxtrDlZJ+yeg==", + "dev": true, + "license": "MIT", + "dependencies": { + "confbox": "^0.2.4", + "exsolve": "^1.0.8", + "pathe": "^2.0.3" + } + }, "node_modules/pkg-up": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/pkg-up/-/pkg-up-3.1.0.tgz", @@ -10141,6 +11781,22 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/process-warning": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-5.1.0.tgz", + "integrity": "sha512-jQSaVHsPgtyw60e1rQ/A+/ArPEj/S8pS/vFnyGa/gYFXrKk/6RuDkoqVDQ5NI5MmS01698ltlAk0NoDBNLujRw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT" + }, "node_modules/prompts": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", @@ -10176,6 +11832,24 @@ "node": ">= 0.10" } }, + "node_modules/publish-browser-extension": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/publish-browser-extension/-/publish-browser-extension-6.1.1.tgz", + "integrity": "sha512-ovfBOz+cZIOBHZ+oxTfpv1kSGU7ruzy8CXoFGvhXsUbJuty45srGzf1ufEePs69virOWXfFbhZRoNQUdgPvwXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@topcli/prompts": "^4.0.0", + "cac": "^7.0.0", + "tasuku": "^2.3.0" + }, + "bin": { + "publish-extension": "bin/publish-extension.mjs" + }, + "engines": { + "node": ">=22.0.0" + } + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -10220,6 +11894,23 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/quansync": { + "version": "0.2.11", + "resolved": "https://registry.npmjs.org/quansync/-/quansync-0.2.11.tgz", + "integrity": "sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/antfu" + }, + { + "type": "individual", + "url": "https://github.com/sponsors/sxzz" + } + ], + "license": "MIT" + }, "node_modules/queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", @@ -10240,6 +11931,12 @@ ], "license": "MIT" }, + "node_modules/quick-format-unescaped": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz", + "integrity": "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==", + "license": "MIT" + }, "node_modules/radix-ui": { "version": "1.6.2", "resolved": "https://registry.npmjs.org/radix-ui/-/radix-ui-1.6.2.tgz", @@ -10317,6 +12014,12 @@ } } }, + "node_modules/radix3": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/radix3/-/radix3-1.1.2.tgz", + "integrity": "sha512-b484I/7b8rDEdSDKckSSBA8knMpcdsXudlE/LNL639wFoHKwLbEkQFZHWEYwDC0wa0FKUcCY+GAF73Z7wxNVFA==", + "license": "MIT" + }, "node_modules/range-parser": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.3.0.tgz", @@ -10345,6 +12048,17 @@ "node": ">= 0.10" } }, + "node_modules/rc9": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/rc9/-/rc9-3.0.1.tgz", + "integrity": "sha512-gMDyleLWVE+i6Sgtc0QbbY6pEKqYs97NGi6isHQPqYlLemPoO8dxQ3uGi0f4NiP98c+jMW6cG1Kx9dDwfvqARQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "defu": "^6.1.6", + "destr": "^2.0.5" + } + }, "node_modules/react": { "version": "19.2.7", "resolved": "https://registry.npmjs.org/react/-/react-19.2.7.tgz", @@ -10610,6 +12324,28 @@ } } }, + "node_modules/readdirp": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-5.1.1.tgz", + "integrity": "sha512-Kko+Y5XQ6fM+Ce3dq3m9YGxnacYZYl9cA1wZjaF3Vbry2L3i1qVg8+CAgNPsXRArPMUMCaOR7oa9Nqntc43JKA==", + "license": "MIT", + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/real-require": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/real-require/-/real-require-0.2.0.tgz", + "integrity": "sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==", + "license": "MIT", + "engines": { + "node": ">= 12.13.0" + } + }, "node_modules/recast": { "version": "0.23.12", "resolved": "https://registry.npmjs.org/recast/-/recast-0.23.12.tgz", @@ -10810,6 +12546,15 @@ "queue-microtask": "^1.2.2" } }, + "node_modules/safe-stable-stringify": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz", + "integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, "node_modules/safer-buffer": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", @@ -10822,6 +12567,13 @@ "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", "license": "MIT" }, + "node_modules/scule": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/scule/-/scule-1.3.0.tgz", + "integrity": "sha512-6FtHJEvt+pVMIB9IBY+IcCJ6Z5f1iQnytgyfKMhDKgmzYG+TeH/wx1y3l27rshSbLiSanrR9ffZDrEsmjlQF2g==", + "dev": true, + "license": "MIT" + }, "node_modules/semifies": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/semifies/-/semifies-1.0.0.tgz", @@ -11107,6 +12859,21 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/slow-redact": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/slow-redact/-/slow-redact-0.3.2.tgz", + "integrity": "sha512-MseHyi2+E/hBRqdOi5COy6wZ7j7DxXRz9NkseavNYSvvWC06D8a5cidVZX3tcG5eCW3NIyVU4zT63hw0Q486jw==", + "license": "MIT" + }, + "node_modules/sonic-boom": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-4.2.1.tgz", + "integrity": "sha512-w6AxtubXa2wTXAUsZMMWERrsIRAdrK0Sc+FUytWvYAhBJLyuI4llrMIC1DtlNSdI99EI86KZum2MMq3EAZlF9Q==", + "license": "MIT", + "dependencies": { + "atomic-sleep": "^1.0.0" + } + }, "node_modules/source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", @@ -11125,6 +12892,15 @@ "node": ">=0.10.0" } }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "license": "ISC", + "engines": { + "node": ">= 10.x" + } + }, "node_modules/stack-utils": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", @@ -11275,6 +13051,26 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/strip-literal": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-4.0.0.tgz", + "integrity": "sha512-PaqAvfUZKBwc/SLmNZtHmzK+v19Z4O4eS3cKPeGvbIv/U3pnyEq4Tuw3/4v/FwfM8VQaEawsyCcOQ0P+kpwWWw==", + "dev": true, + "license": "MIT", + "dependencies": { + "js-tokens": "^10.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/strip-literal/node_modules/js-tokens": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-10.0.0.tgz", + "integrity": "sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==", + "dev": true, + "license": "MIT" + }, "node_modules/stubborn-fs": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/stubborn-fs/-/stubborn-fs-2.0.0.tgz", @@ -11292,6 +13088,16 @@ "dev": true, "license": "MIT" }, + "node_modules/superlock": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/superlock/-/superlock-1.3.5.tgz", + "integrity": "sha512-XpWNthvezZnWp0u7/UL8rBbBOnq2Qx39fw+0RNMC/+eotOd81glzsmIWVH0ejarhTeDQihxOKSbjm2XXFo5a8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, "node_modules/systeminformation": { "version": "5.31.17", "resolved": "https://registry.npmjs.org/systeminformation/-/systeminformation-5.31.17.tgz", @@ -11362,6 +13168,16 @@ "url": "https://opencollective.com/webpack" } }, + "node_modules/tasuku": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/tasuku/-/tasuku-2.3.0.tgz", + "integrity": "sha512-9Jtk+XAnttslCw4i9RceSZGr2PT5TLn0vUyWnoP2SdlSYzkiYRY6kB5qnPi5IQ/Em8e4oBow1rpaA39iijTIrA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/tasuku?sponsor=1" + } + }, "node_modules/terminal-size": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/terminal-size/-/terminal-size-4.0.1.tgz", @@ -11375,12 +13191,38 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/thread-stream": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-3.2.0.tgz", + "integrity": "sha512-zLBvqpwr4Esa0kRjcrzGU6zL25lePWaCLMx0RQFrmteozIfeNdaMLpG5U7PeHzvlFkAWaRKA9/KVW4F60iB+qw==", + "license": "MIT", + "dependencies": { + "real-require": "^0.2.0" + } + }, "node_modules/tiny-invariant": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==", "license": "MIT" }, + "node_modules/tiny-open": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/tiny-open/-/tiny-open-1.3.0.tgz", + "integrity": "sha512-GUFS8yjJZq0oWqCKCJVHcBgMpmi2WEGXY1le3E5ncR0DsgTII5uUyxtfk8/vGyDDGBE42UYHR6Ocvlk8mkXSRg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.3.0.tgz", + "integrity": "sha512-QKAl9m8gWWGHV8jZcPeym6j+XULi6tOf1mT83WYJ4Lk2ytW/uwAWkrP0uFsdoYMdueVJ0qs26wZ+23xeB4ibNQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/tinyglobby": { "version": "0.2.17", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", @@ -11559,6 +13401,19 @@ "node": ">=14.17" } }, + "node_modules/ufo": { + "version": "1.6.4", + "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.4.tgz", + "integrity": "sha512-JFNbkD1Svwe0KvGi8GOeLcP4kAWQ609twvCdcHxq1oSL8svv39ZuSvajcD8B+5D0eL4+s1Is2D/O6KN3qcTeRA==", + "license": "MIT" + }, + "node_modules/uhyphen": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/uhyphen/-/uhyphen-0.2.0.tgz", + "integrity": "sha512-qz3o9CHXmJJPGBdqzab7qAYuW8kQGKNEuoHFYrBwV6hWIMcpAmxDLXojcHfFr9US1Pe6zUswEIJIbLI610fuqA==", + "dev": true, + "license": "ISC" + }, "node_modules/uint8array-extras": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/uint8array-extras/-/uint8array-extras-1.5.0.tgz", @@ -11572,6 +13427,21 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/uint8arrays": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/uint8arrays/-/uint8arrays-3.1.1.tgz", + "integrity": "sha512-+QJa8QRnbdXVpHYjLoTpJIdCTiw9Ir62nocClWuXIq2JIh4Uta0cQsTSpFL678p2CN8B+XSApwcU+pQEqVpKWg==", + "license": "MIT", + "dependencies": { + "multiformats": "^9.4.2" + } + }, + "node_modules/uncrypto": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/uncrypto/-/uncrypto-0.1.3.tgz", + "integrity": "sha512-Ql87qFHB3s/De2ClA9e0gsnS6zXG27SkTiSJwjCc9MebbfapQfuPzumMIUMi38ezPZVNFcHI9sUIepeQfw8J8Q==", + "license": "MIT" + }, "node_modules/undici": { "version": "7.29.0", "resolved": "https://registry.npmjs.org/undici/-/undici-7.29.0.tgz", @@ -11600,6 +13470,67 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/unimport": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/unimport/-/unimport-6.4.0.tgz", + "integrity": "sha512-JJOOuNMFq8b4ZPBKwQUxEcba4MplskDzYI1Lvrf8rJfWphZTWvPNXWa493qsPngHUmub89w6C7j+SeLWTE/UIQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.18.0", + "escape-string-regexp": "^5.0.0", + "estree-walker": "^3.0.3", + "local-pkg": "^1.2.1", + "magic-string": "^1.1.0", + "mlly": "^1.8.2", + "pathe": "^2.0.3", + "picomatch": "^4.0.5", + "pkg-types": "^2.3.1", + "scule": "^1.3.0", + "strip-literal": "^4.0.0", + "tinyglobby": "^0.2.17", + "unplugin": "^3.3.0", + "unplugin-utils": "^0.3.2" + }, + "engines": { + "node": ">=18.12.0" + }, + "peerDependencies": { + "oxc-parser": "*", + "rolldown": "^1.0.0" + }, + "peerDependenciesMeta": { + "oxc-parser": { + "optional": true + }, + "rolldown": { + "optional": true + } + } + }, + "node_modules/unimport/node_modules/escape-string-regexp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", + "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/unimport/node_modules/magic-string": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-1.1.0.tgz", + "integrity": "sha512-kS3VHe0nEPST2saQV4Rbkchcd3UBRkVTQHo1D3h/ZTwFDhai/mfKkmtPAtD129EOI7K3HlHIsFOt0WrI2/oU9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, "node_modules/universalify": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", @@ -11618,6 +13549,183 @@ "node": ">= 0.8" } }, + "node_modules/unplugin": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/unplugin/-/unplugin-3.3.0.tgz", + "integrity": "sha512-qa66K+crbfyE6JK10GjvbJeRrOsuC/JpbnHctfyp/i4oBTxWOzJfRZyDiOk1PtErMFRu8JhsU/wPvOdBNWe5Rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.5", + "picomatch": "^4.0.4", + "webpack-virtual-modules": "^0.6.2" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "@farmfe/core": "*", + "@rspack/core": "*", + "bun-types-no-globals": "*", + "esbuild": "*", + "rolldown": "*", + "rollup": "*", + "unloader": "*", + "vite": "*", + "webpack": "*" + }, + "peerDependenciesMeta": { + "@farmfe/core": { + "optional": true + }, + "@rspack/core": { + "optional": true + }, + "bun-types-no-globals": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "rolldown": { + "optional": true + }, + "rollup": { + "optional": true + }, + "unloader": { + "optional": true + }, + "vite": { + "optional": true + }, + "webpack": { + "optional": true + } + } + }, + "node_modules/unplugin-utils": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/unplugin-utils/-/unplugin-utils-0.3.2.tgz", + "integrity": "sha512-xVToRh2CTmLk2HnEG7ac4rl1MJTT3RFkpS8B++/SnB0kXvuaavD+n3m/vrzyWQOdJNSZQACnbz01pnppbwV5BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pathe": "^2.0.3", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=20.19.0" + }, + "funding": { + "url": "https://github.com/sponsors/sxzz" + } + }, + "node_modules/unstorage": { + "version": "1.17.5", + "resolved": "https://registry.npmjs.org/unstorage/-/unstorage-1.17.5.tgz", + "integrity": "sha512-0i3iqvRfx29hkNntHyQvJTpf5W9dQ9ZadSoRU8+xVlhVtT7jAX57fazYO9EHvcRCfBCyi5YRya7XCDOsbTgkPg==", + "license": "MIT", + "dependencies": { + "anymatch": "^3.1.3", + "chokidar": "^5.0.0", + "destr": "^2.0.5", + "h3": "^1.15.10", + "lru-cache": "^11.2.7", + "node-fetch-native": "^1.6.7", + "ofetch": "^1.5.1", + "ufo": "^1.6.3" + }, + "peerDependencies": { + "@azure/app-configuration": "^1.8.0", + "@azure/cosmos": "^4.2.0", + "@azure/data-tables": "^13.3.0", + "@azure/identity": "^4.6.0", + "@azure/keyvault-secrets": "^4.9.0", + "@azure/storage-blob": "^12.26.0", + "@capacitor/preferences": "^6 || ^7 || ^8", + "@deno/kv": ">=0.9.0", + "@netlify/blobs": "^6.5.0 || ^7.0.0 || ^8.1.0 || ^9.0.0 || ^10.0.0", + "@planetscale/database": "^1.19.0", + "@upstash/redis": "^1.34.3", + "@vercel/blob": ">=0.27.1", + "@vercel/functions": "^2.2.12 || ^3.0.0", + "@vercel/kv": "^1 || ^2 || ^3", + "aws4fetch": "^1.0.20", + "db0": ">=0.2.1", + "idb-keyval": "^6.2.1", + "ioredis": "^5.4.2", + "uploadthing": "^7.4.4" + }, + "peerDependenciesMeta": { + "@azure/app-configuration": { + "optional": true + }, + "@azure/cosmos": { + "optional": true + }, + "@azure/data-tables": { + "optional": true + }, + "@azure/identity": { + "optional": true + }, + "@azure/keyvault-secrets": { + "optional": true + }, + "@azure/storage-blob": { + "optional": true + }, + "@capacitor/preferences": { + "optional": true + }, + "@deno/kv": { + "optional": true + }, + "@netlify/blobs": { + "optional": true + }, + "@planetscale/database": { + "optional": true + }, + "@upstash/redis": { + "optional": true + }, + "@vercel/blob": { + "optional": true + }, + "@vercel/functions": { + "optional": true + }, + "@vercel/kv": { + "optional": true + }, + "aws4fetch": { + "optional": true + }, + "db0": { + "optional": true + }, + "idb-keyval": { + "optional": true + }, + "ioredis": { + "optional": true + }, + "uploadthing": { + "optional": true + } + } + }, + "node_modules/unstorage/node_modules/lru-cache": { + "version": "11.5.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.2.tgz", + "integrity": "sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==", + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, "node_modules/update-browserslist-db": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", @@ -11900,6 +14008,13 @@ "dev": true, "license": "MIT" }, + "node_modules/webpack-virtual-modules": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/webpack-virtual-modules/-/webpack-virtual-modules-0.6.2.tgz", + "integrity": "sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==", + "dev": true, + "license": "MIT" + }, "node_modules/when-exit": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/when-exit/-/when-exit-2.1.5.tgz", @@ -12078,6 +14193,78 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/wxt": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/wxt/-/wxt-0.21.3.tgz", + "integrity": "sha512-UXvzDmgYVdoZiqZyWuoJ5zpjaxCsBRz1fr3q9gVFfge4AeJr0SOA/ijzVRY3ot0L/INZviP4p1by5dCnnwt6bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@1natsu/wait-element": "^4.1.2", + "@aklinker1/rollup-plugin-visualizer": "5.12.0", + "@aklinker1/zero-zip": "^1.0.1", + "@topcli/prompts": "^4.0.0", + "@webext-core/fake-browser": "^2.0.1", + "@webext-core/isolated-element": "^1.1.3 || ^2 || ^3", + "@webext-core/match-patterns": "^2.0.0", + "@wxt-dev/browser": "^0.2.2", + "@wxt-dev/storage": "^1.0.0", + "c12": "^3.3.4", + "cac": "^6.7.14 || ^7.0.0", + "chokidar": "^5.0.0", + "consola": "^3.4.2", + "defu": "^6.1.4", + "dotenv-expand": "^13.0.0", + "filesize": "^11.0.17", + "get-port-please": "^3.2.0", + "giget": "^1.2.3 || ^2.0.0 || ^3.0.0", + "hookable": "^6.1.0", + "is-wsl": "^3.1.1", + "json5": "^2.2.3", + "linkedom": "^0.18.12", + "magicast": "^0.5.2", + "nano-spawn": "^2.0.0", + "nanospinner": "^1.2.2", + "normalize-path": "^3.0.0", + "nypm": "^0.6.5", + "ohash": "^2.0.11", + "picomatch": "^4.0.3", + "publish-browser-extension": "^5.1.0 || ^6.0.0", + "scule": "^1.3.0", + "superlock": "^1.3.2", + "tiny-open": "^1.3.0", + "tinyglobby": "^0.2.16", + "unimport": "^3.13.1 || ^4.0.0 || ^5.0.0 || ^6.0.0" + }, + "bin": { + "wxt": "bin/wxt.mjs", + "wxt-publish-extension": "bin/wxt-publish-extension.mjs" + }, + "engines": { + "bun": ">=1.2.0", + "node": ">=22" + }, + "funding": { + "url": "https://github.com/sponsors/wxt-dev" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=5.4", + "vite": "^6.3.4 || ^7.0.0 || ^8.0.0-0", + "web-ext": ">=9.2.0" + }, + "peerDependenciesMeta": { + "eslint": { + "optional": true + }, + "typescript": { + "optional": true + }, + "web-ext": { + "optional": true + } + } + }, "node_modules/y18n": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", diff --git a/package.json b/package.json index b01e48d..ceb58a6 100644 --- a/package.json +++ b/package.json @@ -5,15 +5,18 @@ "description": "1Shot Wallet — OWS Branding Layer (React + Vite + Tailwind)", "type": "module", "workspaces": [ - "host" + "host", + "extension" ], "scripts": { "dev": "node scripts/dev.mjs", "dev:local": "node scripts/dev.mjs --no-tunnel", "dev:host": "npm run dev -w @1shotapi/oneshot-wallet-host", + "dev:extension": "npm run dev -w @1shotapi/oneshot-wallet-extension", "build": "tsc -p tsconfig.json --noEmit && vite build && node scripts/copy-signer.mjs", "build:host": "npm run build -w @1shotapi/oneshot-wallet-host", - "clean": "node scripts/clean.mjs && npm run clean -w @1shotapi/oneshot-wallet-host", + "build:extension": "npm run build -w @1shotapi/oneshot-wallet-extension", + "clean": "node scripts/clean.mjs && npm run clean -w @1shotapi/oneshot-wallet-host && npm run clean -w @1shotapi/oneshot-wallet-extension", "lint": "tsc -p tsconfig.json --noEmit", "test": "tsx --tsconfig tsconfig.test.json --test \"test/**/*.test.ts\"", "preview": "vite preview", @@ -23,14 +26,17 @@ "dependencies": { "@1shotapi/ows-oid4": "^0.4.0", "@1shotapi/ows-provider": "^0.4.2", - "@1shotapi/ows-signer": "^0.4.0", - "@1shotapi/ows-signer-utils": "^0.5.0", + "@1shotapi/ows-signer": "^0.4.2", + "@1shotapi/ows-signer-utils": "^0.5.1", "@1shotapi/ows-types": "^0.5.1", "@1shotapi/ows-wallet-utils": "^0.4.1", "@fontsource-variable/geist": "^5.2.9", "@metamask/smart-accounts-kit": "^1.7.0", + "@reown/walletkit": "^1.5.6", "@simplewebauthn/browser": "^13.3.0", "@tanstack/react-table": "^8.21.3", + "@walletconnect/core": "^2.23.10", + "@walletconnect/utils": "^2.23.10", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "lucide-react": "^0.525.0", diff --git a/public/mobile/icons/icon-192.png b/public/mobile/icons/icon-192.png new file mode 100644 index 0000000..049cba2 Binary files /dev/null and b/public/mobile/icons/icon-192.png differ diff --git a/public/mobile/icons/icon-512.png b/public/mobile/icons/icon-512.png new file mode 100644 index 0000000..39c874b Binary files /dev/null and b/public/mobile/icons/icon-512.png differ diff --git a/public/mobile/manifest.webmanifest b/public/mobile/manifest.webmanifest new file mode 100644 index 0000000..27f4460 --- /dev/null +++ b/public/mobile/manifest.webmanifest @@ -0,0 +1,31 @@ +{ + "name": "1Shot Wallet", + "short_name": "1Shot", + "description": "Passkey-native 1Shot Wallet — WalletConnect mobile host", + "start_url": "/mobile/", + "scope": "/mobile/", + "display": "standalone", + "orientation": "portrait-primary", + "background_color": "#0a0a0a", + "theme_color": "#0a0a0a", + "icons": [ + { + "src": "/mobile/icons/icon-192.png", + "sizes": "192x192", + "type": "image/png", + "purpose": "any" + }, + { + "src": "/mobile/icons/icon-512.png", + "sizes": "512x512", + "type": "image/png", + "purpose": "any" + }, + { + "src": "/mobile/icons/icon-512.png", + "sizes": "512x512", + "type": "image/png", + "purpose": "maskable" + } + ] +} diff --git a/public/mobile/sw.js b/public/mobile/sw.js new file mode 100644 index 0000000..d35ed86 --- /dev/null +++ b/public/mobile/sw.js @@ -0,0 +1,13 @@ +/* Minimal service worker for Chromium PWA installability (/mobile/ scope). */ +self.addEventListener("install", (event) => { + event.waitUntil(self.skipWaiting()); +}); + +self.addEventListener("activate", (event) => { + event.waitUntil(self.clients.claim()); +}); + +self.addEventListener("fetch", (event) => { + // Network-only — do not cache wallet/Signing assets. + event.respondWith(fetch(event.request)); +}); diff --git a/scripts/dev.mjs b/scripts/dev.mjs index 5f7aa37..5036d4c 100644 --- a/scripts/dev.mjs +++ b/scripts/dev.mjs @@ -71,16 +71,24 @@ function normalizeNgrokDomain(value) { function printUrls(tunnelUrl) { const localWallet = `http://localhost:${listenPort}/`; const localSigner = `http://localhost:${listenPort}/signer/`; + const localMobile = `http://localhost:${listenPort}/mobile/`; + const localCreate = `http://localhost:${listenPort}/create/`; console.log(`1Shot Wallet dev server: http://localhost:${listenPort}`); console.log(` Branding Layer (local): ${localWallet}`); console.log(` Signing Layer (local): ${localSigner}`); + console.log(` Host /mobile (local): ${localMobile}`); + console.log(` Host /create (local): ${localCreate}`); if (tunnelUrl) { const walletUrl = new URL("/", tunnelUrl).href; const signerUrl = new URL("/signer/", tunnelUrl).href; + const mobileUrl = new URL("/mobile/", tunnelUrl).href; + const createUrl = new URL("/create/", tunnelUrl).href; console.log(` Branding Layer (ngrok): ${walletUrl}`); console.log(` Signing Layer (ngrok): ${signerUrl}`); + console.log(` Host /mobile (ngrok): ${mobileUrl}`); + console.log(` Host /create (ngrok): ${createUrl}`); console.log(` Host env: WALLET_IFRAME_URL=${walletUrl}`); } } diff --git a/scripts/generate-mobile-icons.mjs b/scripts/generate-mobile-icons.mjs new file mode 100644 index 0000000..c466e1e --- /dev/null +++ b/scripts/generate-mobile-icons.mjs @@ -0,0 +1,70 @@ +import { mkdirSync, writeFileSync } from "node:fs"; +import path from "node:path"; +import { deflateSync } from "node:zlib"; + +function crcTable() { + const table = new Uint32Array(256); + for (let n = 0; n < 256; n++) { + let c = n; + for (let k = 0; k < 8; k++) { + c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1; + } + table[n] = c; + } + return table; +} + +const CRC = crcTable(); + +function crc32(buf) { + let c = 0xffffffff; + for (let i = 0; i < buf.length; i++) { + c = CRC[(c ^ buf[i]) & 0xff] ^ (c >>> 8); + } + return (c ^ 0xffffffff) >>> 0; +} + +function chunk(type, data) { + const len = Buffer.alloc(4); + len.writeUInt32BE(data.length); + const typeBuf = Buffer.from(type); + const crcBuf = Buffer.alloc(4); + crcBuf.writeUInt32BE(crc32(Buffer.concat([typeBuf, data]))); + return Buffer.concat([len, typeBuf, data, crcBuf]); +} + +function png(size, rgb) { + const [r, g, b] = rgb; + const raw = Buffer.alloc((size * 3 + 1) * size); + for (let y = 0; y < size; y++) { + const row = y * (size * 3 + 1); + raw[row] = 0; + for (let x = 0; x < size; x++) { + const i = row + 1 + x * 3; + raw[i] = r; + raw[i + 1] = g; + raw[i + 2] = b; + } + } + const ihdr = Buffer.alloc(13); + ihdr.writeUInt32BE(size, 0); + ihdr.writeUInt32BE(size, 4); + ihdr[8] = 8; + ihdr[9] = 2; + ihdr[10] = 0; + ihdr[11] = 0; + ihdr[12] = 0; + return Buffer.concat([ + Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]), + chunk("IHDR", ihdr), + chunk("IDAT", deflateSync(raw)), + chunk("IEND", Buffer.alloc(0)), + ]); +} + +const dir = path.join("public", "mobile", "icons"); +mkdirSync(dir, { recursive: true }); +const teal = [0x0d, 0x94, 0x88]; +writeFileSync(path.join(dir, "icon-192.png"), png(192, teal)); +writeFileSync(path.join(dir, "icon-512.png"), png(512, teal)); +console.log("wrote", dir); diff --git a/skills/oneshot-embedded-wallet/SKILL.md b/skills/oneshot-embedded-wallet/SKILL.md index 6d511ce..d907eb4 100644 --- a/skills/oneshot-embedded-wallet/SKILL.md +++ b/skills/oneshot-embedded-wallet/SKILL.md @@ -228,7 +228,7 @@ proxy.showWallet(); Returns `{ ok: true, chainId, assetAddress }` when the user accepts. -Users can also add assets from the Balances tab without a host RPC. The Balances list shows tracked assets for the currently selected network only (USDC is always tracked per supported chain). +Users can also add assets from the Balances tab without a host RPC. The Balances list shows tracked assets for the currently selected network only (USDC is always tracked where listed; USDG on Robinhood). ## Custom RPC — `createAccount` diff --git a/src/App.tsx b/src/App.tsx index 66182b1..a4465b7 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -19,7 +19,9 @@ export function App() { })), ); - const showOnboarding = embedded && !walletCreated && !unlocked; + // Returning sessions with a complete address cache skip Login. + // Incomplete `ows-wallet-created` (no evm/solana cache) is cleared on hydrate. + const showOnboarding = !unlocked && !walletCreated; return (
diff --git a/src/assets/images/chains/robinhood-logo.png b/src/assets/images/chains/robinhood-logo.png new file mode 100644 index 0000000..5a424a4 Binary files /dev/null and b/src/assets/images/chains/robinhood-logo.png differ diff --git a/src/components/MainPanel.tsx b/src/components/MainPanel.tsx index 96ce478..9790862 100644 --- a/src/components/MainPanel.tsx +++ b/src/components/MainPanel.tsx @@ -98,9 +98,18 @@ export function MainPanel() { const [selectedAsset, setSelectedAsset] = useState( null, ); + const [mainTab, setMainTab] = useState("balances"); const [copyState, setCopyState] = useState("idle"); const copyResetRef = useRef | null>(null); + const showCredentials = !style.features.disableCredentials; + const showDelegations = !style.features.disableDelegations; + const activeMainTab = + (mainTab === "credentials" && !showCredentials) || + (mainTab === "delegations" && !showDelegations) + ? "balances" + : mainTab; + useEffect(() => { return () => { if (copyResetRef.current) clearTimeout(copyResetRef.current); @@ -215,27 +224,39 @@ export function MainPanel() { /> - + {style.copy.balances.tabLabel} - - {style.copy.credentials.tabLabel} - - - {style.copy.delegations.tabLabel} - + {showCredentials ? ( + + {style.copy.credentials.tabLabel} + + ) : null} + {showDelegations ? ( + + {style.copy.delegations.tabLabel} + + ) : null} - - - - - - + {showCredentials ? ( + + + + ) : null} + {showDelegations ? ( + + + + ) : null} {networkModalOpen ? ( diff --git a/src/components/OnboardingPanel.tsx b/src/components/OnboardingPanel.tsx index 90f99ec..1100e3c 100644 --- a/src/components/OnboardingPanel.tsx +++ b/src/components/OnboardingPanel.tsx @@ -1,6 +1,8 @@ -import { useState } from "react"; +import { useEffect, useRef, useState } from "react"; import { Button } from "@/components/ui/button"; import { useWallet } from "../wallet/WalletProvider"; +import { useWalletSessionStore } from "../wallet/sessionStore"; +import { formatWalletSetupError } from "../wallet/formatWalletSetupError"; import { useStyle } from "../style/StyleProvider"; import { BrandLogo } from "./BrandLogo"; @@ -14,14 +16,55 @@ function taglineLines(tagline: string): string[] { export function OnboardingPanel() { const { loginWithPasskey, createNewWalletFromUi, openImportPrivateKey } = useWallet(); + const signerReady = useWalletSessionStore((state) => state.signerReady); const { style } = useStyle(); const { productName, tagline, logoUrl, walletSetup, advancedOptions } = style.copy; const lines = taglineLines(tagline); const [showAdvanced, setShowAdvanced] = useState(false); + const [error, setError] = useState(null); + const [toast, setToast] = useState(null); + const [busy, setBusy] = useState(false); + const toastTimerRef = useRef | null>(null); + const actionsDisabled = busy || !signerReady; + + useEffect(() => { + return () => { + if (toastTimerRef.current) { + clearTimeout(toastTimerRef.current); + } + }; + }, []); + + const showSetupError = (err: unknown) => { + const message = formatWalletSetupError(err, walletSetup); + setError(message); + setToast(message); + if (toastTimerRef.current) { + clearTimeout(toastTimerRef.current); + } + toastTimerRef.current = setTimeout(() => { + setToast(null); + toastTimerRef.current = null; + }, 4_500); + }; + + const runSetup = async (action: () => Promise, label: string) => { + if (actionsDisabled) return; + setBusy(true); + setError(null); + try { + await action(); + } catch (err: unknown) { + console.error(`[wallet-setup] ${label} failed`, err); + showSetupError(err); + } finally { + setBusy(false); + } + }; return ( -
+
{ - void loginWithPasskey().catch((error: unknown) => { - console.error("[wallet-setup] embedded login failed", error); - }); + void runSetup(loginWithPasskey, "embedded login"); }} > {walletSetup.loginLabel} @@ -58,19 +100,28 @@ export function OnboardingPanel() { variant="outline" size="lg" className="h-11 w-full text-[0.95rem] font-semibold" + disabled={actionsDisabled} onClick={() => { - void createNewWalletFromUi().catch((error: unknown) => { - console.error("[wallet-setup] embedded create failed", error); - }); + void runSetup(createNewWalletFromUi, "embedded create"); }} > {walletSetup.createLabel} + {error ? ( +

+ {error} +

+ ) : null} + {!showAdvanced ? (
); } diff --git a/src/components/WalletChrome.tsx b/src/components/WalletChrome.tsx index 13ee673..c45f076 100644 --- a/src/components/WalletChrome.tsx +++ b/src/components/WalletChrome.tsx @@ -37,17 +37,19 @@ export function WalletChrome() { > - + {!style.features.hideCloseBox ? ( + + ) : null}
); diff --git a/src/lib/implementations/data/HardcodedChainRepository.ts b/src/lib/implementations/data/HardcodedChainRepository.ts index 9dff1b2..c4641f4 100644 --- a/src/lib/implementations/data/HardcodedChainRepository.ts +++ b/src/lib/implementations/data/HardcodedChainRepository.ts @@ -17,6 +17,7 @@ import lineaLogo from "../../../assets/images/chains/linea-logo.png"; import monadLogo from "../../../assets/images/chains/monad-logo.png"; import optimismLogo from "../../../assets/images/chains/optimism-logo.png"; import polygonLogo from "../../../assets/images/chains/polygon-logo.png"; +import robinhoodLogo from "../../../assets/images/chains/robinhood-logo.png"; import sonicLogo from "../../../assets/images/chains/sonic-logo.png"; import unichainLogo from "../../../assets/images/chains/unichain-logo.png"; @@ -27,7 +28,7 @@ const DEVELOPMENT_RELAYER_URL = "https://relayer.1shotapi.dev"; const ALCHEMY_KEY = "jqLUTbHeN_cVsIX2W7tJk"; /** - * Public Relayer docs networks + Arc Testnet. + * Public Relayer docs networks + Arc Testnet (dev relayer) + Robinhood. * @see https://1shotapi.com/docs/relayer/get-started/overview */ const CATALOG: readonly SupportedChain[] = [ @@ -35,7 +36,7 @@ const CATALOG: readonly SupportedChain[] = [ EVMChainId("0x4cef52"), EChainNetworkType.Testnet, DEVELOPMENT_RELAYER_URL, - false, + true, arcLogo, true, `https://arc-testnet.g.alchemy.com/v2/${ALCHEMY_KEY}`, @@ -185,6 +186,17 @@ const CATALOG: readonly SupportedChain[] = [ "Celo", "https://celoscan.io", ), + new SupportedChain( + EVMChainId("0x1237"), + EChainNetworkType.Mainnet, + PRODUCTION_RELAYER_URL, + true, + robinhoodLogo, + true, + "https://rpc.mainnet.chain.robinhood.com", + "Robinhood", + "https://robinhoodchain.blockscout.com", + ), ]; /** Default chain for a fresh session (Arc Testnet). */ diff --git a/src/lib/implementations/data/HardcodedKnownAssetRepository.ts b/src/lib/implementations/data/HardcodedKnownAssetRepository.ts index 1be5f29..53c016f 100644 --- a/src/lib/implementations/data/HardcodedKnownAssetRepository.ts +++ b/src/lib/implementations/data/HardcodedKnownAssetRepository.ts @@ -18,24 +18,26 @@ const BY_KEY = new Map( ]), ); -const DEFAULT_TRACKED_USDC_CHAIN_IDS = new Set([ - "0x4cef52", - "0xaa36a7", - "0x14a34", - "0x2105", +/** Chain → pinned default stablecoin symbol (always shown, not removable). */ +const DEFAULT_TRACKED_STABLE_BY_CHAIN = new Map([ + ["0x4cef52", "USDC"], + ["0xaa36a7", "USDC"], + ["0x14a34", "USDC"], + ["0x2105", "USDC"], + ["0x1237", "USDG"], ]); /** - * USDC on every supported demo chain — always shown in Balances (not removable). + * Default stablecoin on demo chains — always shown in Balances (not removable). + * USDC where listed; USDG on Robinhood (USDC is not deployed there). */ export const DEFAULT_TRACKED_USDC: readonly NewTrackedAsset[] = - RELAYER_KNOWN_ASSETS.filter( - (asset) => - asset.symbol === "USDC" && - DEFAULT_TRACKED_USDC_CHAIN_IDS.has( - String(asset.chainId).toLowerCase(), - ), - ).map((asset) => NewTrackedAsset.fromKnown(asset)); + RELAYER_KNOWN_ASSETS.filter((asset) => { + const expected = DEFAULT_TRACKED_STABLE_BY_CHAIN.get( + String(asset.chainId).toLowerCase(), + ); + return expected != null && asset.symbol === expected; + }).map((asset) => NewTrackedAsset.fromKnown(asset)); const DEFAULT_TRACKED_USDC_KEYS = new Set( DEFAULT_TRACKED_USDC.map((asset) => diff --git a/src/lib/implementations/data/relayerKnownAssets.ts b/src/lib/implementations/data/relayerKnownAssets.ts index 901d525..783aa3e 100644 --- a/src/lib/implementations/data/relayerKnownAssets.ts +++ b/src/lib/implementations/data/relayerKnownAssets.ts @@ -33,11 +33,12 @@ function seed(row: ISeedRow): KnownAsset { } /** - * Static snapshot from `relayer_getCapabilities` (prod + dev) plus Arc Testnet USDC. + * Static snapshot from `relayer_getCapabilities` (prod + dev), including + * Arc Testnet USDC and Robinhood USDG. * @see https://www.1shotapi.com/docs/relayer/get-started/overview */ const SEED_ROWS: readonly ISeedRow[] = [ - // Arc Testnet — demo network, not returned by relayer. + // Arc Testnet (5042002) — native USDC { chainId: EVMChainId("0x4cef52"), address: EVMAccountAddress( @@ -47,6 +48,16 @@ const SEED_ROWS: readonly ISeedRow[] = [ name: "USDC", decimals: 6, }, + // Robinhood (4663) — official USDG (USDC is not deployed) + { + chainId: EVMChainId("0x1237"), + address: EVMAccountAddress( + "0x5fc5360D0400a0Fd4f2af552ADD042D716F1d168", + ), + symbol: "USDG", + name: "Global Dollar", + decimals: 6, + }, // Ethereum mainnet (1) { chainId: EVMChainId("0x1"), diff --git a/src/ows/registerAccountConnect.ts b/src/ows/registerAccountConnect.ts index 321266c..a355166 100644 --- a/src/ows/registerAccountConnect.ts +++ b/src/ows/registerAccountConnect.ts @@ -12,37 +12,71 @@ export type AccountConnectStorage = { evm: EVMAccountAddress, solana?: SolanaAccountAddress, ) => void; + /** True after the user has approved eth_accounts at least once this store. */ + loadAccountsPermissionGranted: () => boolean; + saveAccountsPermissionGranted: () => void; }; export type RegisterAccountConnectOptions = { storage: AccountConnectStorage; ensureReady: () => Promise; requestConnectApproval: () => Promise; + /** Optional — used for EIP-1193 `connect` event payload. */ + getChainId?: () => Promise | string; }; +type WalletPermission = { + parentCapability: string; + date?: number; +}; + +type DisplayHandle = { + hide: () => Promise; +}; + +/** Host Inline (extension side panel) can miss a second displayReady; don't block connect UX. */ +const DISPLAY_ACQUIRE_TIMEOUT_MS = 2_000; + /** - * Register `eth_accounts` / `eth_requestAccounts` on the wallet (pre-`start()`). + * Register `eth_accounts` / `eth_requestAccounts` (+ MetaMask-style permission + * methods) on the wallet (pre-`start()`). + * + * EIP-1193 notes: + * - `eth_accounts` returns the address only after a grant (not merely a cache). + * - Reconnect (`eth_requestAccounts` with grant + cache) returns silently — + * no `accountsChanged` / `connect` spam on every dApp refresh. + * - Those events fire only when the user newly approves a connect. */ export function registerAccountConnect( wallet: OWSWallet, signer: OWSSigner, options: RegisterAccountConnectOptions, ): void { - wallet.registerEip1193("eth_accounts", async () => { - const cached = options.storage.loadCachedEvmAddress(); - if (cached) { - return [cached]; - } - return []; - }); + const hasGrantedAccounts = (): boolean => + options.storage.loadAccountsPermissionGranted(); - wallet.registerEip1193("eth_requestAccounts", async () => { + const markAccountsGranted = (): void => { + options.storage.saveAccountsPermissionGranted(); + }; + + /** Emit connect notifications after a fresh user approval. */ + const announceConnected = async ( + address: EVMAccountAddress, + ): Promise => { + wallet.providerEvents.emit("accountsChanged", [address]); + await emitConnect(wallet, options); + return [address]; + }; + + const resolveAccounts = async (): Promise => { const cached = options.storage.loadCachedEvmAddress(); - if (cached) { + + // Already approved — MetaMask-like silent return (no display, no events). + if (cached && hasGrantedAccounts()) { return [cached]; } - const display = await wallet.requestDisplay(); + const display = await acquireDisplay(wallet); try { const approved = await options.requestConnectApproval(); if (!approved) { @@ -51,14 +85,102 @@ export function registerAccountConnect( ); } + markAccountsGranted(); + + if (cached) { + return announceConnected(cached); + } + await options.ensureReady(); const evm = await signer.evm.getAccountAddress(); const solana = await signer.solana.getAccountAddress(); options.storage.saveCachedAddresses(evm, solana); - wallet.providerEvents.emit("accountsChanged", [evm]); - return [evm]; + return announceConnected(evm); } finally { - await display.hide(); + try { + await display.hide(); + } catch { + // Hide ack can stall on Inline hosts; connect already succeeded. + } + } + }; + + wallet.registerEip1193("eth_accounts", async () => { + const cached = options.storage.loadCachedEvmAddress(); + if (cached && hasGrantedAccounts()) { + return [cached]; + } + return []; + }); + + wallet.registerEip1193("eth_requestAccounts", async () => resolveAccounts()); + + // MetaMask / Uniswap often call these instead of / before eth_requestAccounts. + wallet.registerEip1193("wallet_requestPermissions", async (params) => { + const requested = normalizeRequestedPermissions(params); + if (!requested.includes("eth_accounts")) { + return []; + } + await resolveAccounts(); + return [ + { + parentCapability: "eth_accounts", + date: Date.now(), + } satisfies WalletPermission, + ]; + }); + + wallet.registerEip1193("wallet_getPermissions", async () => { + const cached = options.storage.loadCachedEvmAddress(); + if (!cached || !hasGrantedAccounts()) { + return []; } + return [ + { + parentCapability: "eth_accounts", + date: Date.now(), + } satisfies WalletPermission, + ]; }); } + +async function acquireDisplay(wallet: OWSWallet): Promise { + try { + const session = await Promise.race([ + wallet.requestDisplay(), + new Promise((resolve) => { + setTimeout(() => resolve(null), DISPLAY_ACQUIRE_TIMEOUT_MS); + }), + ]); + if (session) { + return session; + } + } catch { + // Fall through to a no-op handle — panel may already be Inline-visible. + } + return { + hide: async () => {}, + }; +} + +async function emitConnect( + wallet: OWSWallet, + options: RegisterAccountConnectOptions, +): Promise { + try { + const chainId = await options.getChainId?.(); + if (chainId) { + wallet.providerEvents.emit("connect", { chainId }); + } + } catch { + // Best-effort; account return is enough for most dApps. + } +} + +function normalizeRequestedPermissions(params: unknown[]): string[] { + const first = params[0]; + if (!first || typeof first !== "object") { + return ["eth_accounts"]; + } + return Object.keys(first as Record); +} diff --git a/src/storage.ts b/src/storage.ts index 3726e76..1d643f6 100644 --- a/src/storage.ts +++ b/src/storage.ts @@ -17,11 +17,56 @@ const EVM_ADDRESS_KEY = "ows-evm-address"; const SOLANA_ADDRESS_KEY = "ows-solana-address"; /** Cached secp256k1 public key (0x-hex) so LocalAccount builds without Unlock. */ const SECP256K1_PUBLIC_KEY_KEY = "ows-secp256k1-public-key"; +/** EIP-1193 eth_accounts was approved at least once (MetaMask-style reconnect). */ +const ETH_ACCOUNTS_GRANTED_KEY = "ows-eth-accounts-granted"; export function isWalletCreated(): boolean { return localStorage.getItem(WALLET_CREATED_KEY) === "true"; } +export function clearWalletCreated(): void { + localStorage.removeItem(WALLET_CREATED_KEY); +} + +const PLACEHOLDER_EVM = EVMAccountAddress("0x0"); +const PLACEHOLDER_SOLANA = SolanaAccountAddress("—"); + +function isUsableEvmAddress( + address: EVMAccountAddress | undefined, +): address is EVMAccountAddress { + return ( + address !== undefined && + address !== PLACEHOLDER_EVM && + /^0x[0-9a-fA-F]{40}$/.test(address) + ); +} + +function isUsableSolanaAddress( + address: SolanaAccountAddress | undefined, +): address is SolanaAccountAddress { + return address !== undefined && address !== PLACEHOLDER_SOLANA; +} + +/** + * Returning-session cache: `ows-wallet-created` plus usable EVM + Solana + * addresses. Incomplete cache (created flag without addresses) is cleared so + * onboarding runs instead of MainPanel with placeholder `0x0`. + */ +export function reconcileCachedWalletSession(): { + walletCreated: boolean; + evmAddress: EVMAccountAddress | undefined; + solanaAddress: SolanaAccountAddress | undefined; +} { + const evmAddress = loadCachedEvmAddress(); + const solanaAddress = loadCachedSolanaAddress(); + const created = isWalletCreated(); + if (created && (!isUsableEvmAddress(evmAddress) || !isUsableSolanaAddress(solanaAddress))) { + clearWalletCreated(); + return { walletCreated: false, evmAddress, solanaAddress }; + } + return { walletCreated: created, evmAddress, solanaAddress }; +} + export function loadCredentialId(): CredentialId | undefined { const handle = localStorage.getItem(PASSKEY_HANDLE_KEY); if (handle) return CredentialId(handle); @@ -74,6 +119,14 @@ export function saveCachedAddresses( } } +export function loadAccountsPermissionGranted(): boolean { + return localStorage.getItem(ETH_ACCOUNTS_GRANTED_KEY) === "1"; +} + +export function saveAccountsPermissionGranted(): void { + localStorage.setItem(ETH_ACCOUNTS_GRANTED_KEY, "1"); +} + export function loadCachedSecp256k1PublicKey(): `0x${string}` | undefined { const value = localStorage.getItem(SECP256K1_PUBLIC_KEY_KEY); if (!value || !value.startsWith("0x")) { @@ -93,6 +146,7 @@ export function clearWalletStorage(): void { localStorage.removeItem(EVM_ADDRESS_KEY); localStorage.removeItem(SOLANA_ADDRESS_KEY); localStorage.removeItem(SECP256K1_PUBLIC_KEY_KEY); + localStorage.removeItem(ETH_ACCOUNTS_GRANTED_KEY); // Legacy keys from earlier passkey-public-key caching (no longer used). localStorage.removeItem("ows-passkey-public-key"); localStorage.removeItem("ows-relayer-passkey-registered"); diff --git a/src/style/applyStyle.ts b/src/style/applyStyle.ts index 94b43b7..4dfbb72 100644 --- a/src/style/applyStyle.ts +++ b/src/style/applyStyle.ts @@ -142,12 +142,26 @@ export function mergeStyle( }, }, dark: patch.dark === undefined ? current.dark : patch.dark, - allowedChains: - patch.allowedChains === undefined - ? current.allowedChains - : patch.allowedChains.length === 0 - ? null - : [...patch.allowedChains], + features: { + hideCloseBox: + patch.features?.hideCloseBox === undefined + ? current.features.hideCloseBox + : patch.features.hideCloseBox, + disableCredentials: + patch.features?.disableCredentials === undefined + ? current.features.disableCredentials + : patch.features.disableCredentials, + disableDelegations: + patch.features?.disableDelegations === undefined + ? current.features.disableDelegations + : patch.features.disableDelegations, + allowedChains: + patch.features?.allowedChains === undefined + ? current.features.allowedChains + : patch.features.allowedChains.length === 0 + ? null + : [...patch.features.allowedChains], + }, }; } @@ -197,7 +211,12 @@ function cloneDefaultStyle(): IResolvedStyle { importPrivateKey: { ...DEFAULT_STYLE.copy.importPrivateKey }, advancedOptions: { ...DEFAULT_STYLE.copy.advancedOptions }, }, - allowedChains: null, + features: { + hideCloseBox: DEFAULT_STYLE.features.hideCloseBox, + disableCredentials: DEFAULT_STYLE.features.disableCredentials, + disableDelegations: DEFAULT_STYLE.features.disableDelegations, + allowedChains: null, + }, }; } diff --git a/src/style/defaults.ts b/src/style/defaults.ts index 2ab52eb..9769f42 100644 --- a/src/style/defaults.ts +++ b/src/style/defaults.ts @@ -15,7 +15,12 @@ export const DEFAULT_STYLE: IResolvedStyle = { radius: "0.625rem", fontSans: "'Geist Variable', ui-sans-serif, system-ui, sans-serif", }, - allowedChains: null, + features: { + hideCloseBox: false, + disableCredentials: false, + disableDelegations: false, + allowedChains: null, + }, copy: { productName: "1Shot Wallet", tagline: @@ -40,6 +45,10 @@ export const DEFAULT_STYLE: IResolvedStyle = { cancelLabel: "Cancel", loginLabel: "Login with passkey ->", createLabel: "Create account", + passkeyTimeoutError: + "Passkey confirmation timed out. Please try again.", + passkeyFailedError: + "Could not complete passkey authentication. Please try again.", }, passkeyName: { title: "Name your passkey", diff --git a/src/style/index.ts b/src/style/index.ts index a0142c4..4f8636f 100644 --- a/src/style/index.ts +++ b/src/style/index.ts @@ -1,5 +1,7 @@ export type { IStyleOptions, + IStyleFeaturesOptions, + IResolvedStyleFeatures, IStyleThemeOptions, IStyleCopyOptions, IStyleCopyConnect, diff --git a/src/style/registerSetStyle.ts b/src/style/registerSetStyle.ts index b2f7fb9..e07bd60 100644 --- a/src/style/registerSetStyle.ts +++ b/src/style/registerSetStyle.ts @@ -45,6 +45,8 @@ const walletSetupCopySchema = z.strictObject({ cancelLabel: z.string().optional(), loginLabel: z.string().optional(), createLabel: z.string().optional(), + passkeyTimeoutError: z.string().optional(), + passkeyFailedError: z.string().optional(), }) .optional(); @@ -386,13 +388,20 @@ const copySchema = z.strictObject({ .optional(); export const setStyleParamsSchema = z.strictObject({ - theme: themeSchema, - copy: copySchema, - dark: z.boolean().optional(), - allowedChains: z - .array(z.string().regex(/^0x[0-9a-fA-F]+$/)) - .optional(), - }); + theme: themeSchema, + copy: copySchema, + dark: z.boolean().optional(), + features: z + .strictObject({ + hideCloseBox: z.boolean().optional(), + disableCredentials: z.boolean().optional(), + disableDelegations: z.boolean().optional(), + allowedChains: z + .array(z.string().regex(/^0x[0-9a-fA-F]+$/)) + .optional(), + }) + .optional(), +}); export type ISetStyleParams = z.infer; @@ -405,14 +414,14 @@ export function registerSetStyleRpc( async (params) => { const styleParams = params as ISetStyleParams; const resolved = styleController.merge(styleParams); - if (styleParams.allowedChains !== undefined) { + if (styleParams.features?.allowedChains !== undefined) { const catalogIds = new Set( chainRepository .getCatalog() .map((chain) => String(chain.chainId).toLowerCase()), ); const valid: ReturnType[] = []; - for (const id of styleParams.allowedChains) { + for (const id of styleParams.features.allowedChains) { const lower = id.toLowerCase(); if (catalogIds.has(lower)) { valid.push(EVMChainId(lower as `0x${string}`)); diff --git a/src/style/types.ts b/src/style/types.ts index 3876f24..4a78e7d 100644 --- a/src/style/types.ts +++ b/src/style/types.ts @@ -42,6 +42,10 @@ export interface IStyleCopyWalletSetup { cancelLabel: string; loginLabel: string; createLabel: string; + /** Passkey ceremony timed out (e.g. Signer RPC `getPublicKey`). */ + passkeyTimeoutError: string; + /** Generic passkey login/create failure after cancel or other errors. */ + passkeyFailedError: string; } /** Name passkey modal (create-account flow). */ @@ -495,11 +499,16 @@ export interface IResolvedCopy { advancedOptions: IStyleCopyAdvancedOptions; } -export interface IStyleOptions { - theme?: IStyleThemeOptions; - copy?: IStyleCopyOptions; - /** When true, add `.dark` on ; when false, remove it; omit = unchanged */ - dark?: boolean; +export interface IStyleFeaturesOptions { + /** + * When true, hide the wallet chrome Close (X) control. + * Useful for Inline hosts (extension side panel) where hide is a no-op. + */ + hideCloseBox?: boolean; + /** When true, hide the Credentials tab (host-driven credential flows still work). */ + disableCredentials?: boolean; + /** When true, hide the Delegations tab (host-driven delegation flows still work). */ + disableDelegations?: boolean; /** * Hex EVM chain ids the Network dropdown may show. * Omit or empty ⇒ all catalog-enabled chains. @@ -507,11 +516,26 @@ export interface IStyleOptions { allowedChains?: string[]; } +export interface IResolvedStyleFeatures { + hideCloseBox: boolean; + disableCredentials: boolean; + disableDelegations: boolean; + /** `null` means no host allowlist (all enabled catalog chains). */ + allowedChains: string[] | null; +} + +export interface IStyleOptions { + theme?: IStyleThemeOptions; + copy?: IStyleCopyOptions; + /** When true, add `.dark` on ; when false, remove it; omit = unchanged */ + dark?: boolean; + features?: IStyleFeaturesOptions; +} + /** Fully resolved style after merging defaults + setStyle patches. */ export interface IResolvedStyle { theme: Required; copy: IResolvedCopy; dark: boolean; - /** `null` means no host allowlist (all enabled catalog chains). */ - allowedChains: string[] | null; + features: IResolvedStyleFeatures; } diff --git a/src/wallet/WalletProvider.tsx b/src/wallet/WalletProvider.tsx index ef0f124..f258f5e 100644 --- a/src/wallet/WalletProvider.tsx +++ b/src/wallet/WalletProvider.tsx @@ -85,6 +85,8 @@ import type { DelegationId } from "../lib/types/primitives/DelegationId"; import type { TrackedAssetId } from "../lib/types/primitives"; import { loadCachedEvmAddress, + loadAccountsPermissionGranted, + saveAccountsPermissionGranted, saveCachedAddresses, clearWalletStorage, } from "../storage"; @@ -170,6 +172,8 @@ const walletStorage: AccountConnectStorage = { session.setAddresses(evm, session.solanaAddress); } }, + loadAccountsPermissionGranted, + saveAccountsPermissionGranted, }; /** Imperative wallet APIs that need refs / boot (not UI session state). */ diff --git a/src/wallet/createAccountHandoff.ts b/src/wallet/createAccountHandoff.ts index 9c5d8c1..592b184 100644 --- a/src/wallet/createAccountHandoff.ts +++ b/src/wallet/createAccountHandoff.ts @@ -6,6 +6,8 @@ import { newCreateHandoffNonce, OWS_ACCOUNT_CREATED, OWS_ACCOUNT_CREATE_CANCELLED, + subscribeAccountCreateHandoff, + type AccountCreateHandoffMessage, } from "./createAccountHandoffMessages"; export { @@ -45,12 +47,15 @@ export async function createAccountViaFirstPartyTab(): Promise((resolve, reject) => { let settled = false; let popup: Window | null = null; + let sawPopupOpen = false; let pollId: ReturnType | undefined; let timeoutId: ReturnType | undefined; let closedGraceId: ReturnType | undefined; + let unsubscribeBroadcast = () => {}; const cleanup = () => { window.removeEventListener("message", onMessage); + unsubscribeBroadcast(); if (pollId !== undefined) clearInterval(pollId); if (timeoutId !== undefined) clearTimeout(timeoutId); if (closedGraceId !== undefined) clearTimeout(closedGraceId); @@ -76,36 +81,33 @@ export async function createAccountViaFirstPartyTab(): Promise { - if (event.origin !== window.location.origin) { - return; - } - if (!isAccountCreateHandoffMessage(event.data)) { - return; - } - + const handleHandoff = ( + data: AccountCreateHandoffMessage, + meta: { via: "postMessage" | "broadcast"; sourceIsPopup?: boolean | string }, + ) => { console.info("[create-handoff] message received", { - type: event.data.type, - handoff: event.data.handoff, + type: data.type, + handoff: data.handoff, expectedHandoff: handoff, - handoffMatch: event.data.handoff === handoff, - sourceIsPopup: popup ? event.source === popup : "(no popup ref)", - hasCredentialId: Boolean(event.data.credentialId), - hasCosePublicKey: Boolean(event.data.cosePublicKey), + handoffMatch: data.handoff === handoff, + via: meta.via, + sourceIsPopup: meta.sourceIsPopup, + hasCredentialId: Boolean(data.credentialId), + hasCosePublicKey: Boolean(data.cosePublicKey), }); - if (event.data.handoff !== handoff) return; + if (data.handoff !== handoff) return; // Handoff nonce is authoritative. Do not require event.source === popup — // some browsers (notably after close) lose WindowProxy identity and would // drop a valid success message, leaving the opener on the login screen. - if (event.data.type === OWS_ACCOUNT_CREATED) { - if (!event.data.credentialId) { + if (data.type === OWS_ACCOUNT_CREATED) { + if (!data.credentialId) { finishReject(new Error("Account created but credential id missing")); return; } - if (!event.data.cosePublicKey) { + if (!data.cosePublicKey) { finishReject( new Error( "Account created but authenticator public key missing — cannot register with relayer", @@ -114,33 +116,53 @@ export async function createAccountViaFirstPartyTab(): Promise { + if (event.origin !== window.location.origin) { + return; + } + if (!isAccountCreateHandoffMessage(event.data)) { + return; + } + handleHandoff(event.data, { + via: "postMessage", + sourceIsPopup: popup ? event.source === popup : "(no popup ref)", + }); }; window.addEventListener("message", onMessage); + unsubscribeBroadcast = subscribeAccountCreateHandoff((data) => { + handleHandoff(data, { via: "broadcast" }); + }); timeoutId = setTimeout(() => { finishReject(new Error("Passkey creation timed out")); }, HANDOFF_TIMEOUT_MS); pollId = setInterval(() => { - if (!popup || !popup.closed || settled) return; - if (closedGraceId !== undefined) return; + if (!popup || settled) return; + if (!popup.closed) { + sawPopupOpen = true; + return; + } + // Extension-opened tabs sometimes return a WindowProxy that is already + // `.closed` while the real tab is open — only treat close after we saw open. + if (!sawPopupOpen || closedGraceId !== undefined) return; console.info( "[create-handoff] popup closed; waiting for in-flight message", @@ -153,7 +175,8 @@ export async function createAccountViaFirstPartyTab(): Promise window.open(url, "ows-create-account"); diff --git a/src/wallet/createAccountHandoffMessages.ts b/src/wallet/createAccountHandoffMessages.ts index 501aac5..bab06e3 100644 --- a/src/wallet/createAccountHandoffMessages.ts +++ b/src/wallet/createAccountHandoffMessages.ts @@ -32,11 +32,52 @@ export function isAccountCreateHandoffMessage( ); } +/** + * Same-origin channel for `/create` → Branding handoff. + * + * Safari (and Chrome) extension sidebars often open a tab with + * `window.opener === null`, so `postMessage` to the opener never runs. + * Both documents are the wallet origin — the extension page is not in this + * channel and does not need to be an allowed postMessage origin. + */ +const CREATE_HANDOFF_CHANNEL = "ows:account-create-handoff"; + +export function subscribeAccountCreateHandoff( + listener: (message: AccountCreateHandoffMessage) => void, +): () => void { + if (typeof BroadcastChannel === "undefined") { + return () => {}; + } + const channel = new BroadcastChannel(CREATE_HANDOFF_CHANNEL); + const onMessage = (event: MessageEvent) => { + if (isAccountCreateHandoffMessage(event.data)) { + listener(event.data); + } + }; + channel.addEventListener("message", onMessage); + return () => { + channel.removeEventListener("message", onMessage); + channel.close(); + }; +} + export function postAccountCreateHandoff( - target: Window, + target: Window | null, message: AccountCreateHandoffMessage, ): void { - target.postMessage(message, window.location.origin); + if (target && !target.closed) { + try { + target.postMessage(message, window.location.origin); + } catch { + // Inaccessible opener (common when the tab was opened from an extension). + } + } + if (typeof BroadcastChannel === "undefined") { + return; + } + const channel = new BroadcastChannel(CREATE_HANDOFF_CHANNEL); + channel.postMessage(message); + channel.close(); } export function newCreateHandoffNonce(): string { diff --git a/src/wallet/formatWalletSetupError.ts b/src/wallet/formatWalletSetupError.ts new file mode 100644 index 0000000..c70074f --- /dev/null +++ b/src/wallet/formatWalletSetupError.ts @@ -0,0 +1,34 @@ +/** + * Map passkey login/create failures to host-overridable `style.copy.walletSetup` strings. + */ +export function formatWalletSetupError( + error: unknown, + copy: { + passkeyTimeoutError: string; + passkeyFailedError: string; + }, +): string { + if (!(error instanceof Error)) { + return copy.passkeyFailedError; + } + const name = error.name; + const message = error.message; + if ( + name === "OwsTimeoutError" || + message.includes("timed out") || + message.includes("Signer RPC timed out") + ) { + return copy.passkeyTimeoutError; + } + // User dismissed the signer Confirm UI or OS passkey sheet — soft message. + if ( + name === "OwsSignDeniedError" || + name === "OwsNotAllowedError" || + message.includes("signDenied") || + message.includes("NotAllowed") || + message.includes("ceremonyCancelled") + ) { + return copy.passkeyFailedError; + } + return copy.passkeyFailedError; +} diff --git a/src/wallet/sessionStore.ts b/src/wallet/sessionStore.ts index 478f712..9161875 100644 --- a/src/wallet/sessionStore.ts +++ b/src/wallet/sessionStore.ts @@ -5,11 +5,7 @@ import { SolanaAccountAddress, } from "@1shotapi/ows-types"; import { DEFAULT_CHAIN_ID } from "../lib/implementations/data/HardcodedChainRepository"; -import { - isWalletCreated, - loadCachedEvmAddress, - loadCachedSolanaAddress, -} from "../storage"; +import { reconcileCachedWalletSession } from "../storage"; /** Host-controlled shell mode — users cannot switch between these. */ export enum EWalletMode { @@ -19,6 +15,8 @@ export enum EWalletMode { export interface IWalletSessionState { ready: boolean; + /** Signing Layer iframe loaded (`OWSSigner.create` resolved). */ + signerReady: boolean; bootError: string | null; embedded: boolean; unlocked: boolean; @@ -33,6 +31,7 @@ export interface IWalletSessionState { focusedAssetAddress: EVMAccountAddress | null; setReady: (ready: boolean) => void; + setSignerReady: (ready: boolean) => void; setBootError: (error: string | null) => void; setUnlocked: (unlocked: boolean) => void; setWalletCreated: (created: boolean) => void; @@ -56,32 +55,40 @@ function initialEmbedded(): boolean { return typeof window !== "undefined" && window.parent !== window; } -function initialWalletCreated(): boolean { - return typeof window !== "undefined" ? isWalletCreated() : false; -} - -function initialEvmAddress(): EVMAccountAddress { +function hydrateSessionFromCache(): { + walletCreated: boolean; + unlocked: boolean; + evmAddress: EVMAccountAddress; + solanaAddress: SolanaAccountAddress; +} { if (typeof window === "undefined") { - return EVMAccountAddress("0x0"); + return { + walletCreated: false, + unlocked: false, + evmAddress: EVMAccountAddress("0x0"), + solanaAddress: SolanaAccountAddress("—"), + }; } - return loadCachedEvmAddress() ?? EVMAccountAddress("0x0"); + const cached = reconcileCachedWalletSession(); + return { + walletCreated: cached.walletCreated, + unlocked: cached.walletCreated, + evmAddress: cached.evmAddress ?? EVMAccountAddress("0x0"), + solanaAddress: cached.solanaAddress ?? SolanaAccountAddress("—"), + }; } -function initialSolanaAddress(): SolanaAccountAddress { - if (typeof window === "undefined") { - return SolanaAccountAddress("—"); - } - return loadCachedSolanaAddress() ?? SolanaAccountAddress("—"); -} +const hydratedSession = hydrateSessionFromCache(); export const useWalletSessionStore = create((set) => ({ ready: false, + signerReady: false, bootError: null, embedded: initialEmbedded(), - unlocked: false, - walletCreated: initialWalletCreated(), - evmAddress: initialEvmAddress(), - solanaAddress: initialSolanaAddress(), + unlocked: hydratedSession.unlocked, + walletCreated: hydratedSession.walletCreated, + evmAddress: hydratedSession.evmAddress, + solanaAddress: hydratedSession.solanaAddress, chainId: DEFAULT_CHAIN_ID, credentialCount: 0, trackedAssetCount: 0, @@ -89,6 +96,7 @@ export const useWalletSessionStore = create((set) => ({ focusedAssetAddress: null, setReady: (ready) => set({ ready }), + setSignerReady: (signerReady) => set({ signerReady }), setBootError: (bootError) => set({ bootError }), setUnlocked: (unlocked) => set({ unlocked }), setWalletCreated: (walletCreated) => set({ walletCreated }), diff --git a/src/wallet/useWalletAuth.ts b/src/wallet/useWalletAuth.ts index 9cd51e4..717bb76 100644 --- a/src/wallet/useWalletAuth.ts +++ b/src/wallet/useWalletAuth.ts @@ -60,6 +60,25 @@ export function useWalletAuth({ useWalletSessionStore.getState().setUnlocked(value); }, []); + /** + * Serialize post-create / unlock work with {@link ensureReady} so a host + * cannot start a second passkey ceremony after `saveWalletCreated` flips + * `isWalletCreated()` but before `unlocked` is true. + */ + const runWhileUnlockInFlight = useCallback(async (work: () => Promise) => { + if (unlockInFlightRef.current) { + await unlockInFlightRef.current; + } + unlockInFlightRef.current = (async () => { + await work(); + })(); + try { + await unlockInFlightRef.current; + } finally { + unlockInFlightRef.current = undefined; + } + }, []); + const refreshAddresses = useCallback(async () => { const signer = signerRef.current; if (!signer) return; @@ -97,32 +116,53 @@ export function useWalletAuth({ })); }, []); - const loginWithPasskey = useCallback(async () => { - const signer = signerRef.current; - if (!signer) throw new Error("Signer not ready"); + /** + * One PRF/get ceremony that optionally binds a relayer challenge so a + * following `registerPasskey` / `refreshFromRelayer` can reuse the assertion + * via {@link IRelayerCredentialsClient.takeAssertion} (no second unlock). + */ + const getPublicKeyCachingRelayerAssertion = useCallback( + async (opts: { + credentialId?: CredentialId; + discoverable?: boolean; + logLabel: string; + }) => { + const signer = signerRef.current; + if (!signer) throw new Error("Signer not ready"); - let challengeId: ChallengeId | null = null; - let challenge: HexString | undefined; - try { - const minted = await relayerCredentialsClient.getChallenge(); - challengeId = minted.challengeId; - challenge = minted.challenge; - } catch (error: unknown) { - console.warn( - "[login] relayer challenge mint failed; login without assertion cache", - error, - ); - } + let challengeId: ChallengeId | null = null; + let challenge: HexString | undefined; + try { + const minted = await relayerCredentialsClient.getChallenge(); + challengeId = minted.challengeId; + challenge = minted.challenge; + } catch (error: unknown) { + console.warn( + `[${opts.logLabel}] relayer challenge mint failed; continuing without assertion cache`, + error, + ); + } + + const { logLabel: _logLabel, ...getOpts } = opts; + const result = await signer.getPublicKey({ + ...getOpts, + ...(challenge ? { challenge } : {}), + }); + if (challengeId && result.assertion) { + relayerCredentialsClient.setAssertion(challengeId, result.assertion); + } + return result; + }, + [relayerCredentialsClient, signerRef], + ); - const result = await signer.getPublicKey({ + const loginWithPasskey = useCallback(async () => { + const result = await getPublicKeyCachingRelayerAssertion({ discoverable: true, - ...(challenge ? { challenge } : {}), + logLabel: "login", }); - if (challengeId && result.assertion) { - relayerCredentialsClient.setAssertion(challengeId, result.assertion); - } - const credentialId = result.credentialId ?? signer.getCredentialId(); + const credentialId = result.credentialId ?? signerRef.current?.getCredentialId(); if (!credentialId) { throw new Error("Passkey login succeeded but credential id missing"); } @@ -141,9 +181,9 @@ export function useWalletAuth({ setUnlocked(true); }, [ credentialRepository, + getPublicKeyCachingRelayerAssertion, refreshAddresses, refreshCredentialCount, - relayerCredentialsClient, setUnlocked, signerRef, ]); @@ -154,25 +194,33 @@ export function useWalletAuth({ credentialIdPrefix: credentialId.slice(0, 8), hasCosePublicKey: Boolean(cosePublicKey), }); - saveWalletCreated(credentialId); - useWalletSessionStore.getState().setWalletCreated(true); const signer = signerRef.current; if (!signer) throw new Error("Signer not ready"); - // Unlock on this signer session (PRF get) — deferred from /create. - await signer.getPublicKey({ credentialId }); - await refreshAddresses(); - // Relayer register — also deferred from /create (one assertion here). - saveCosePublicKey(COSEPublicKey(cosePublicKey)); - await credentialRepository.registerPasskey(COSEPublicKey(cosePublicKey)); - try { - await refreshCredentialCount(); - } catch (error: unknown) { - console.warn( - "[credentials] refresh after first-party create failed", - error, - ); - } - setUnlocked(true); + + await runWhileUnlockInFlight(async () => { + // Persist id only inside the in-flight lock so ensureReady cannot + // unlock in parallel (isWalletCreated becomes true here). + saveWalletCreated(credentialId); + // Unlock + cache relayer assertion in one ceremony (deferred from /create). + await getPublicKeyCachingRelayerAssertion({ + credentialId, + logLabel: "create-handoff", + }); + await refreshAddresses(); + saveCosePublicKey(COSEPublicKey(cosePublicKey)); + await credentialRepository.registerPasskey(COSEPublicKey(cosePublicKey)); + try { + await refreshCredentialCount(); + } catch (error: unknown) { + console.warn( + "[credentials] refresh after first-party create failed", + error, + ); + } + setUnlocked(true); + useWalletSessionStore.getState().setWalletCreated(true); + }); + const address = await signer.evm.getAccountAddress(); const { hostDomain } = await configProvider.getConfig(); eventBus.emitAnalytics(new AccountCreatedEvent(hostDomain, address)); @@ -182,8 +230,10 @@ export function useWalletAuth({ configProvider, credentialRepository, eventBus, + getPublicKeyCachingRelayerAssertion, refreshAddresses, refreshCredentialCount, + runWhileUnlockInFlight, setUnlocked, signerRef, ], @@ -220,9 +270,8 @@ export function useWalletAuth({ "Passkey created but authenticator public key missing — cannot register with relayer", ); } - saveWalletCreated(credentialId); - saveCosePublicKey(created.cosePublicKey); - // Do not unlock or register — opener adopts via handoff. + // Do not mark wallet created or unlock here — the opener adopts via + // handoff under unlockInFlight so ensureReady cannot race a second get. return { credentialId, cosePublicKey: created.cosePublicKey, @@ -259,12 +308,19 @@ export function useWalletAuth({ "Passkey created but authenticator public key missing — cannot register with relayer", ); } - saveWalletCreated(credentialId); - saveCosePublicKey(created.cosePublicKey); - await credentialRepository.registerPasskey(created.cosePublicKey); - useWalletSessionStore.getState().setWalletCreated(true); - await refreshAddresses(); - setUnlocked(true); + await runWhileUnlockInFlight(async () => { + saveWalletCreated(credentialId); + saveCosePublicKey(created.cosePublicKey!); + // One get with relayer challenge — registerPasskey reuses the assertion. + await getPublicKeyCachingRelayerAssertion({ + credentialId, + logLabel: "create", + }); + await credentialRepository.registerPasskey(created.cosePublicKey!); + await refreshAddresses(); + setUnlocked(true); + useWalletSessionStore.getState().setWalletCreated(true); + }); const address = await signer.evm.getAccountAddress(); eventBus.emitAnalytics(new AccountCreatedEvent(hostDomain, address)); } catch (error: unknown) { @@ -283,7 +339,9 @@ export function useWalletAuth({ configProvider, credentialRepository, eventBus, + getPublicKeyCachingRelayerAssertion, refreshAddresses, + runWhileUnlockInFlight, setUnlocked, signerRef, ], @@ -333,28 +391,11 @@ export function useWalletAuth({ if (!signer) throw new Error("Signer not ready"); const storedCredentialId = loadCredentialId(); if (storedCredentialId) { - let challengeId: ChallengeId | null = null; - let challenge: HexString | undefined; - try { - const minted = await relayerCredentialsClient.getChallenge(); - challengeId = minted.challengeId; - challenge = minted.challenge; - } catch (error: unknown) { - console.warn( - "[unlock] relayer challenge mint failed; unlocking without assertion cache", - error, - ); - } - - const result = await signer.getPublicKey({ + const result = await getPublicKeyCachingRelayerAssertion({ credentialId: storedCredentialId, - ...(challenge ? { challenge } : {}), + logLabel: "unlock", }); - if (challengeId && result.assertion) { - relayerCredentialsClient.setAssertion(challengeId, result.assertion); - } - const credentialId = result.credentialId ?? signer.getCredentialId(); if (!credentialId) { throw new Error("Passkey unlock succeeded but credential id missing"); @@ -370,9 +411,9 @@ export function useWalletAuth({ } await loginWithPasskey(); }, [ + getPublicKeyCachingRelayerAssertion, loginWithPasskey, refreshAddresses, - relayerCredentialsClient, setUnlocked, signerRef, ]); diff --git a/src/wallet/useWalletBoot.ts b/src/wallet/useWalletBoot.ts index d23c4a8..9d78074 100644 --- a/src/wallet/useWalletBoot.ts +++ b/src/wallet/useWalletBoot.ts @@ -280,6 +280,7 @@ export function useWalletBoot({ kind: "connect", resolve, })), + getChainId: () => useWalletSessionStore.getState().chainId, }); const catalog = chainRepository.getCatalog(); @@ -790,13 +791,18 @@ export function useWalletBoot({ .setBootError(error instanceof Error ? error.message : String(error)); }); - void awaitSigner().catch((error: unknown) => { - if (cancelled) return; - console.error("[oneshot-wallet] Signing Layer failed to load", error); - useWalletSessionStore - .getState() - .setBootError(error instanceof Error ? error.message : String(error)); - }); + void awaitSigner() + .then(() => { + if (cancelled) return; + useWalletSessionStore.getState().setSignerReady(true); + }) + .catch((error: unknown) => { + if (cancelled) return; + console.error("[oneshot-wallet] Signing Layer failed to load", error); + useWalletSessionStore + .getState() + .setBootError(error instanceof Error ? error.message : String(error)); + }); const listed = await credentialRepository.list(); if (cancelled) return; diff --git a/test/mobile/namespaces.test.ts b/test/mobile/namespaces.test.ts new file mode 100644 index 0000000..ca25eac --- /dev/null +++ b/test/mobile/namespaces.test.ts @@ -0,0 +1,190 @@ +import assert from "node:assert/strict"; +import { describe, it } from "node:test"; +import { + buildApprovedNamespaces, + caip10Account, + EIP155_EVENTS, + EIP155_METHODS, + NamespaceApprovalError, +} from "../../mobile/namespaces.ts"; + +const ADDRESS = "0x0000000000000000000000000000000000000001"; + +describe("buildApprovedNamespaces", () => { + it("approves the intersection of optional eip155 chains with the catalog", () => { + const approved = buildApprovedNamespaces({ + optionalNamespaces: { + eip155: { + chains: ["eip155:1", "eip155:8453", "eip155:999999"], + methods: ["eth_sendTransaction", "personal_sign"], + events: ["chainChanged"], + }, + }, + address: ADDRESS, + }); + + assert.deepEqual(approved.eip155.chains.sort(), ["eip155:1", "eip155:8453"]); + assert.deepEqual( + approved.eip155.accounts.sort(), + [ + caip10Account(1, ADDRESS), + caip10Account(8453, ADDRESS), + ].sort(), + ); + assert.deepEqual( + [...approved.eip155.methods].sort(), + ["eth_sendTransaction", "personal_sign"].sort(), + ); + assert.deepEqual(approved.eip155.events, ["chainChanged"]); + }); + + it("rejects unsupported required chains instead of dropping them", () => { + assert.throws( + () => + buildApprovedNamespaces({ + requiredNamespaces: { + eip155: { + chains: ["eip155:1", "eip155:999999"], + methods: ["eth_sendTransaction"], + events: ["chainChanged"], + }, + }, + address: ADDRESS, + }), + (error: unknown) => + error instanceof NamespaceApprovalError && + error.sdkError === "UNSUPPORTED_CHAINS", + ); + }); + + it("rejects required non-eip155 namespaces", () => { + assert.throws( + () => + buildApprovedNamespaces({ + requiredNamespaces: { + solana: { + chains: ["solana:mainnet"], + methods: ["solana_signTransaction"], + events: [], + }, + }, + optionalNamespaces: { + eip155: { chains: ["eip155:1"] }, + }, + address: ADDRESS, + }), + (error: unknown) => + error instanceof NamespaceApprovalError && + error.sdkError === "UNSUPPORTED_NAMESPACE_KEY", + ); + }); + + it("does not copy methods from optional non-eip155 namespaces into eip155", () => { + const approved = buildApprovedNamespaces({ + optionalNamespaces: { + eip155: { + chains: ["eip155:1"], + methods: ["personal_sign"], + events: ["accountsChanged"], + }, + solana: { + chains: ["solana:mainnet"], + methods: ["solana_signTransaction"], + events: ["connect"], + }, + }, + address: ADDRESS, + }); + + assert.deepEqual(approved.eip155.chains, ["eip155:1"]); + assert.deepEqual(approved.eip155.methods, ["personal_sign"]); + assert.ok(!approved.eip155.methods.includes("solana_signTransaction")); + assert.ok(!approved.eip155.events.includes("connect")); + assert.equal(Object.keys(approved).join(","), "eip155"); + }); + + it("does not fall back to every catalog chain when nothing matches", () => { + assert.throws( + () => + buildApprovedNamespaces({ + optionalNamespaces: { + eip155: { + chains: ["eip155:999999"], + methods: ["personal_sign"], + }, + }, + address: ADDRESS, + }), + (error: unknown) => + error instanceof NamespaceApprovalError && + error.sdkError === "UNSUPPORTED_CHAINS", + ); + }); + + it("rejects required methods this host cannot serve", () => { + assert.throws( + () => + buildApprovedNamespaces({ + requiredNamespaces: { + eip155: { + chains: ["eip155:1"], + methods: ["eth_sendTransaction", "wallet_watchAsset"], + events: ["chainChanged"], + }, + }, + address: ADDRESS, + }), + (error: unknown) => + error instanceof NamespaceApprovalError && + error.sdkError === "UNSUPPORTED_METHODS", + ); + }); + + it("unions required eip155 with supported optional eip155 chains", () => { + const approved = buildApprovedNamespaces({ + requiredNamespaces: { + eip155: { + chains: ["eip155:1"], + methods: ["eth_sendTransaction"], + events: ["chainChanged"], + }, + }, + optionalNamespaces: { + eip155: { + chains: ["eip155:8453", "eip155:999999"], + methods: ["personal_sign", "wallet_watchAsset"], + events: ["accountsChanged", "disconnect"], + }, + }, + address: ADDRESS, + }); + + assert.deepEqual(approved.eip155.chains.sort(), ["eip155:1", "eip155:8453"]); + assert.deepEqual( + [...approved.eip155.methods].sort(), + ["eth_sendTransaction", "personal_sign"].sort(), + ); + assert.deepEqual( + [...approved.eip155.events].sort(), + ["accountsChanged", "chainChanged"].sort(), + ); + assert.ok(!approved.eip155.methods.includes("wallet_watchAsset")); + }); + + it("defaults to host methods when the proposal lists no eip155 methods", () => { + const approved = buildApprovedNamespaces({ + optionalNamespaces: { + eip155: { chains: ["eip155:1"] }, + }, + address: ADDRESS, + }); + assert.deepEqual( + [...approved.eip155.methods].sort(), + [...EIP155_METHODS].sort(), + ); + assert.deepEqual( + [...approved.eip155.events].sort(), + [...EIP155_EVENTS].sort(), + ); + }); +}); diff --git a/tsconfig.json b/tsconfig.json index f9ab4cc..a78f0d0 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -23,5 +23,5 @@ "viem/*": ["./node_modules/viem/*"] } }, - "include": ["src/**/*", "create/**/*", "vite.config.ts"] + "include": ["src/**/*", "create/**/*", "mobile/**/*", "vite.config.ts"] } diff --git a/tsconfig.test.json b/tsconfig.test.json index 4acd160..d1b8879 100644 --- a/tsconfig.test.json +++ b/tsconfig.test.json @@ -5,5 +5,5 @@ "noEmit": true, "rootDir": "." }, - "include": ["src/**/*", "test/**/*", "create/**/*", "vite.config.ts"] + "include": ["src/**/*", "test/**/*", "create/**/*", "mobile/**/*", "vite.config.ts"] } diff --git a/vite.config.ts b/vite.config.ts index 14f67c7..2db5d25 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -90,6 +90,9 @@ function sendFile(res: ServerResponse, filePath: string): void { res.statusCode = 200; res.setHeader("Content-Type", contentType(filePath)); res.setHeader("Content-Security-Policy", SIGNER_CSP); + // Signer JS changes often during local sibling-package work; avoid sticky + // browser caches that serve stale ceremony-lock logic across refreshes. + res.setHeader("Cache-Control", "no-store"); stream.pipe(res); }); stream.once("error", (error: NodeJS.ErrnoException) => { @@ -176,10 +179,54 @@ function serveSignerPlugin(): Plugin { }; } +/** + * Host Layer MPA entries (`/mobile/`, `/create/`) must not get Vite's default + * SPA fallback to Branding `index.html`. That served Branding at the top level + * (no OWSProxy), so nesting became Branding→Signing only and the signer + * rejected with "Must be embedded in a wallet iframe (double iframe)." + */ +function serveHostMpaPlugin(): Plugin { + const entries: Record = { + "/mobile": "/mobile/index.html", + "/mobile/": "/mobile/index.html", + "/create": "/create/index.html", + "/create/": "/create/index.html", + }; + + return { + name: "ows-serve-host-mpa", + configureServer(server) { + server.middlewares.use((req: IncomingMessage, res: ServerResponse, next) => { + const raw = req.url ?? ""; + const q = raw.indexOf("?"); + const pathname = q === -1 ? raw : raw.slice(0, q); + const search = q === -1 ? "" : raw.slice(q); + const target = entries[pathname]; + if (!target) { + next(); + return; + } + if (pathname === "/mobile" || pathname === "/create") { + res.statusCode = 301; + res.setHeader("Location", `${pathname}/${search}`); + res.end(); + return; + } + req.url = `${target}${search}`; + next(); + }); + }, + }; +} + export default defineConfig({ + // MPA: Branding (`/`), Host create (`/create/`), Host mobile (`/mobile/`). + // Default `spa` history-fallback would rewrite `/mobile` to Branding index + // and break Host → Branding → Signing double-iframe nesting. + appType: "mpa", base: "/", publicDir: "public", - plugins: [react(), tailwindcss(), serveSignerPlugin()], + plugins: [react(), tailwindcss(), serveHostMpaPlugin(), serveSignerPlugin()], resolve: { alias: { "@": path.resolve(__dirname, "./src"), @@ -217,6 +264,7 @@ export default defineConfig({ input: { main: path.resolve(__dirname, "index.html"), create: path.resolve(__dirname, "create/index.html"), + mobile: path.resolve(__dirname, "mobile/index.html"), }, }, },