From 698b2d23a815024918d7c4513a91ecd0406070b9 Mon Sep 17 00:00:00 2001 From: Russell Dempsey <1173416+SgtPooki@users.noreply.github.com> Date: Wed, 6 May 2026 15:32:41 -0400 Subject: [PATCH 1/9] feat(auth): spike OpenWallet Standard wallet support Wires @open-wallet-standard/adapters/viem into the existing AccountConfig path so signing can be delegated to OWS (private key never leaves the OWS core). Adds: - --wallet / OWS_WALLET_ID - --wallet-passphrase / OWS_WALLET_PASSPHRASE Resolver order: --wallet (OWS) takes precedence over --private-key / PRIVATE_KEY. devnet auto-resolve still applies when no auth supplied. Refs #457 --- package.json | 2 ++ pnpm-lock.yaml | 70 +++++++++++++++++++++++++++++++++++++++ src/core/ows/index.ts | 42 +++++++++++++++++++++++ src/core/synapse/index.ts | 3 +- src/utils/cli-auth.ts | 30 ++++++++++++++--- src/utils/cli-options.ts | 12 +++++++ 6 files changed, 153 insertions(+), 6 deletions(-) create mode 100644 src/core/ows/index.ts diff --git a/package.json b/package.json index 6e1250e8..963e919f 100644 --- a/package.json +++ b/package.json @@ -125,6 +125,8 @@ "@libp2p/tcp": "^11.0.17", "@multiformats/multiaddr": "^13.0.1", "@multiformats/multiaddr-to-uri": "^12.0.0", + "@open-wallet-standard/adapters": "^1.3.2", + "@open-wallet-standard/core": "^1.3.2", "@sentry/node": "^10.49.0", "commander": "^14.0.3", "datastore-core": "^11.0.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 99fe1d33..2d45b54b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -44,6 +44,12 @@ importers: '@multiformats/multiaddr-to-uri': specifier: ^12.0.0 version: 12.0.0 + '@open-wallet-standard/adapters': + specifier: ^1.3.2 + version: 1.3.2(viem@2.48.3(typescript@5.9.3)(zod@4.3.6)) + '@open-wallet-standard/core': + specifier: ^1.3.2 + version: 1.3.2 '@sentry/node': specifier: ^10.49.0 version: 10.49.0 @@ -938,6 +944,44 @@ packages: '@octokit/types@16.0.0': resolution: {integrity: sha512-sKq+9r1Mm4efXW1FCk7hFSeJo4QKreL/tTbR0rz/qx/r1Oa2VV83LTA/H/MuCOX7uCIJmQVRKBcbmWoySjAnSg==} + '@open-wallet-standard/adapters@1.3.2': + resolution: {integrity: sha512-49GOsSNcxPmP4ETv4nuPkj7prYFdxO9bdaVHP24vyeV19Wl6iuKgHtjzXyiX/2N0pRN/XVP/yGEmqypFeVNtkQ==} + peerDependencies: + '@solana/web3.js': '>=1.0.0' + '@tetherto/wdk-wallet': '>=1.0.0-beta.0' + viem: '>=2.0.0' + peerDependenciesMeta: + '@solana/web3.js': + optional: true + '@tetherto/wdk-wallet': + optional: true + viem: + optional: true + + '@open-wallet-standard/core-darwin-arm64@1.3.2': + resolution: {integrity: sha512-rvUsGU8eZtIMYFJ047nFKS79ajJeYCydvvh2B6bX0UUGf2NJZ4wUnxGqijxPiflz2xqzvbHXYZNzy3MIO4+fDQ==} + cpu: [arm64] + os: [darwin] + + '@open-wallet-standard/core-darwin-x64@1.3.2': + resolution: {integrity: sha512-a6u3CvWZY8sC1S92AMGnO1WFlMW3m38+3/IoKzhxcFvD8rVY0OA0ebH6qzYLT96/TaouXDwmMQV6yQmb1IjfzQ==} + cpu: [x64] + os: [darwin] + + '@open-wallet-standard/core-linux-arm64-gnu@1.3.2': + resolution: {integrity: sha512-wzyd7euZ5qvDr8dHf3eG2XP7iFgMmBUo35U7cWMLs6Yqjqz103SEjtMfNJEedZRb62f8P3Xhp9jdRLzaJvjk5g==} + cpu: [arm64] + os: [linux] + + '@open-wallet-standard/core-linux-x64-gnu@1.3.2': + resolution: {integrity: sha512-pVtyEP8EqR3rjqdBDCRuB9Rd4xs8+11gRVtWidwPeFz5QPq3c9VGZjGQtTZoiZBHCxKaq5pTQjC78J5lVuA0hg==} + cpu: [x64] + os: [linux] + + '@open-wallet-standard/core@1.3.2': + resolution: {integrity: sha512-Dk8bB9G5PjJfNzAqJd9WT9OfqrrSXRc3gA9+BCaY2qirlNrCkIAZCHaDIE0LnGOAaoSZizR6qnOFbukj+DTzJg==} + hasBin: true + '@opentelemetry/api-logs@0.207.0': resolution: {integrity: sha512-lAb0jQRVyleQQGiuuvCOTDVspc14nx6XJjP4FspJ1sNARo3Regq4ZZbrc3rN4b1TYSuUCvgH+UXUPug4SLOqEQ==} engines: {node: '>=8.0.0'} @@ -5177,6 +5221,32 @@ snapshots: dependencies: '@octokit/openapi-types': 27.0.0 + '@open-wallet-standard/adapters@1.3.2(viem@2.48.3(typescript@5.9.3)(zod@4.3.6))': + dependencies: + '@noble/curves': 1.9.1 + '@open-wallet-standard/core': 1.3.2 + optionalDependencies: + viem: 2.48.3(typescript@5.9.3)(zod@4.3.6) + + '@open-wallet-standard/core-darwin-arm64@1.3.2': + optional: true + + '@open-wallet-standard/core-darwin-x64@1.3.2': + optional: true + + '@open-wallet-standard/core-linux-arm64-gnu@1.3.2': + optional: true + + '@open-wallet-standard/core-linux-x64-gnu@1.3.2': + optional: true + + '@open-wallet-standard/core@1.3.2': + optionalDependencies: + '@open-wallet-standard/core-darwin-arm64': 1.3.2 + '@open-wallet-standard/core-darwin-x64': 1.3.2 + '@open-wallet-standard/core-linux-arm64-gnu': 1.3.2 + '@open-wallet-standard/core-linux-x64-gnu': 1.3.2 + '@opentelemetry/api-logs@0.207.0': dependencies: '@opentelemetry/api': 1.9.1 diff --git a/src/core/ows/index.ts b/src/core/ows/index.ts new file mode 100644 index 00000000..e268969b --- /dev/null +++ b/src/core/ows/index.ts @@ -0,0 +1,42 @@ +/** + * OpenWallet Standard (OWS) integration for filecoin-pin + * + * Resolves an OWS-managed wallet into a viem `Account` that the Synapse SDK + * can sign with directly. Private keys never leave the OWS core; this module + * just hands Synapse a signing surface (signMessage / signTransaction / + * signTypedData) backed by the OWS adapter. + * + * @module core/ows + */ + +import { owsToViemAccount } from '@open-wallet-standard/adapters/viem' +import type { Account, Chain } from 'viem' + +export interface OwsAccountOptions { + /** Wallet name or ID registered with the `ows` CLI / OWS core */ + walletId: string + /** Target Filecoin chain (used to derive CAIP-2 chain ID) */ + chain: Chain + /** Optional passphrase for keystore-encrypted wallets */ + passphrase?: string + /** Optional account index within the wallet (defaults to 0) */ + index?: number + /** Optional override for OWS vault path */ + vaultPath?: string +} + +/** + * Build a viem `Account` backed by an OWS wallet. + * + * The returned account is a `LocalAccount` from viem's perspective; signing + * calls are delegated to the OWS native core, so the private key never + * materializes in the Node process. + */ +export function getOwsAccount(options: OwsAccountOptions): Account { + const chainId = `eip155:${options.chain.id}` + const adapterOptions: Parameters[1] = { chain: chainId } + if (options.passphrase != null) adapterOptions.passphrase = options.passphrase + if (options.index != null) adapterOptions.index = options.index + if (options.vaultPath != null) adapterOptions.vaultPath = options.vaultPath + return owsToViemAccount(options.walletId, adapterOptions) +} diff --git a/src/core/synapse/index.ts b/src/core/synapse/index.ts index da4467ce..90f47c48 100644 --- a/src/core/synapse/index.ts +++ b/src/core/synapse/index.ts @@ -220,7 +220,8 @@ export async function initializeSynapse(config: SynapseSetupConfig, logger?: Log ) } throw new Error( - 'No authentication provided. Supply a private key (--private-key / PRIVATE_KEY), ' + + 'No authentication provided. Supply an OWS wallet (--wallet / OWS_WALLET_ID), ' + + 'private key (--private-key / PRIVATE_KEY), ' + 'wallet address (--wallet-address / WALLET_ADDRESS), or session key (--session-key / SESSION_KEY).' ) } diff --git a/src/utils/cli-auth.ts b/src/utils/cli-auth.ts index 22a5ba01..40703fb1 100644 --- a/src/utils/cli-auth.ts +++ b/src/utils/cli-auth.ts @@ -7,8 +7,9 @@ import type { Chain, Synapse } from '@filoz/synapse-sdk' import { getRpcUrl, NETWORK_CHAINS, resolveDevnetConfig } from '../common/get-rpc-url.js' +import { getOwsAccount } from '../core/ows/index.js' import type { SynapseSetupConfig } from '../core/synapse/index.js' -import { initializeSynapse } from '../core/synapse/index.js' +import { calibration, initializeSynapse } from '../core/synapse/index.js' import { createLogger } from '../logger.js' /** @@ -18,6 +19,10 @@ import { createLogger } from '../logger.js' export interface CLIAuthOptions { /** Private key for standard authentication */ privateKey?: string | undefined + /** OpenWallet Standard wallet name or ID (signs in-process, key stays in vault) */ + wallet?: string | undefined + /** Optional passphrase for an OWS-managed wallet */ + walletPassphrase?: string | undefined /** Wallet address for session key mode */ walletAddress?: string | undefined /** Session key private key */ @@ -49,9 +54,14 @@ export function parseCLIAuth(options: CLIAuthOptions): SynapseSetupConfig { const network = options.network?.toLowerCase().trim() const isDevnet = network === 'devnet' - // For devnet, fall back to the devnet user's private key if none provided - const privateKey = - options.privateKey || process.env.PRIVATE_KEY || (isDevnet ? resolveDevnetConfig().privateKey : undefined) + const owsWalletId = options.wallet || process.env.OWS_WALLET_ID + const owsPassphrase = options.walletPassphrase || process.env.OWS_WALLET_PASSPHRASE + + // For devnet, fall back to the devnet user's private key if none provided. + // OWS wallets take precedence over PRIVATE_KEY when explicitly supplied. + const privateKey = owsWalletId + ? undefined + : options.privateKey || process.env.PRIVATE_KEY || (isDevnet ? resolveDevnetConfig().privateKey : undefined) const walletAddress = options.walletAddress || process.env.WALLET_ADDRESS const sessionKey = options.sessionKey || process.env.SESSION_KEY const viewAddress = options.viewAddress || process.env.VIEW_ADDRESS @@ -72,9 +82,19 @@ export function parseCLIAuth(options: CLIAuthOptions): SynapseSetupConfig { readOnly?: boolean rpcUrl?: string chain?: Chain + account?: ReturnType } = {} - if (privateKey) config.privateKey = privateKey + if (owsWalletId) { + const owsOptions: Parameters[0] = { + walletId: owsWalletId, + chain: chain ?? calibration, + } + if (owsPassphrase != null) owsOptions.passphrase = owsPassphrase + config.account = getOwsAccount(owsOptions) + } else if (privateKey) { + config.privateKey = privateKey + } if (viewAddress) { config.walletAddress = viewAddress config.readOnly = true diff --git a/src/utils/cli-options.ts b/src/utils/cli-options.ts index 92a0a2d1..03839fd2 100644 --- a/src/utils/cli-options.ts +++ b/src/utils/cli-options.ts @@ -43,6 +43,18 @@ import { USDFC_DECIMALS } from '../core/payments/constants.js' export function addAuthOptions(command: Command): Command { command .option('--private-key ', 'Private key for standard auth (can also use PRIVATE_KEY env)') + .addOption( + new Option( + '--wallet ', + 'OpenWallet Standard wallet name or ID (signs via OWS, key never leaves vault). Can also use OWS_WALLET_ID env.' + ).env('OWS_WALLET_ID') + ) + .addOption( + new Option( + '--wallet-passphrase ', + 'Passphrase for the OWS wallet (also OWS_WALLET_PASSPHRASE env)' + ).env('OWS_WALLET_PASSPHRASE') + ) .option('--wallet-address
', 'Wallet address for session key auth (can also use WALLET_ADDRESS env)') .option('--session-key ', 'Session key for session key auth (can also use SESSION_KEY env)') .addOption( From c4e35e2f6b634d3d35aed57a048d219a437fbfbc Mon Sep 17 00:00:00 2001 From: Russell Dempsey <1173416+SgtPooki@users.noreply.github.com> Date: Wed, 6 May 2026 15:54:44 -0400 Subject: [PATCH 2/9] fix(ows): lazy-load adapter via dynamic import for cross-platform support @open-wallet-standard/core ships napi-rs prebuilt binaries only for linux-x64-gnu, linux-arm64-gnu, darwin-x64, and darwin-arm64. A static import of @open-wallet-standard/adapters/viem triggers a require of the native binding at module evaluation time, crashing CLI startup on Windows runners (and any platform without a published prebuilt) even when the user never asks for OWS auth. Load the adapter via ESM dynamic `import()` inside getOwsAccount() so the native binding only resolves when --wallet / OWS_WALLET_ID is actually used. Surface a helpful error if the platform lacks a prebuilt. Makes getOwsAccount() and parseCLIAuth() async; updates the 12 call sites (already inside async action handlers) to await. Refs #457 --- src/add/add.ts | 2 +- src/core/ows/index.ts | 31 +++++++++++++++++++++++++++++-- src/import/import.ts | 2 +- src/payments/auto.ts | 2 +- src/payments/deposit.ts | 2 +- src/payments/fund.ts | 2 +- src/payments/status.ts | 2 +- src/payments/withdraw.ts | 2 +- src/rm/remove-all-pieces.ts | 2 +- src/rm/remove-piece.ts | 2 +- src/utils/cli-auth.ts | 8 ++++---- 11 files changed, 42 insertions(+), 15 deletions(-) diff --git a/src/add/add.ts b/src/add/add.ts index ba35a707..26fe2ead 100644 --- a/src/add/add.ts +++ b/src/add/add.ts @@ -120,7 +120,7 @@ export async function runAdd(options: AddOptions): Promise { // Initialize Synapse SDK spinner.start('Initializing Synapse SDK...') - const config = parseCLIAuth(options) + const config = await parseCLIAuth(options) if (dataSetMetadata) { config.dataSetMetadata = dataSetMetadata } diff --git a/src/core/ows/index.ts b/src/core/ows/index.ts index e268969b..66dbb368 100644 --- a/src/core/ows/index.ts +++ b/src/core/ows/index.ts @@ -6,10 +6,14 @@ * just hands Synapse a signing surface (signMessage / signTransaction / * signTypedData) backed by the OWS adapter. * + * The adapter is loaded via dynamic `import()` because + * `@open-wallet-standard/core` is a napi-rs native binding without prebuilt + * artifacts for Windows or musl. A static import would crash CLI startup on + * those platforms even when the user never asks for OWS auth. + * * @module core/ows */ -import { owsToViemAccount } from '@open-wallet-standard/adapters/viem' import type { Account, Chain } from 'viem' export interface OwsAccountOptions { @@ -25,6 +29,28 @@ export interface OwsAccountOptions { vaultPath?: string } +interface OwsViemAdapter { + owsToViemAccount: ( + walletNameOrId: string, + options?: { chain?: string; passphrase?: string; index?: number; vaultPath?: string } + ) => Account +} + +async function loadAdapter(): Promise { + try { + return (await import('@open-wallet-standard/adapters/viem')) as unknown as OwsViemAdapter + } catch (err) { + const reason = err instanceof Error ? err.message : String(err) + throw new Error( + 'OpenWallet Standard is not available on this platform. ' + + '@open-wallet-standard/core ships napi-rs prebuilt binaries for linux-x64-gnu, ' + + 'linux-arm64-gnu, darwin-x64, and darwin-arm64 only (no Windows or musl/Alpine artifact today). ' + + 'Use --private-key / PRIVATE_KEY instead, or run on a supported platform.\n' + + `Underlying load error: ${reason}` + ) + } +} + /** * Build a viem `Account` backed by an OWS wallet. * @@ -32,7 +58,8 @@ export interface OwsAccountOptions { * calls are delegated to the OWS native core, so the private key never * materializes in the Node process. */ -export function getOwsAccount(options: OwsAccountOptions): Account { +export async function getOwsAccount(options: OwsAccountOptions): Promise { + const { owsToViemAccount } = await loadAdapter() const chainId = `eip155:${options.chain.id}` const adapterOptions: Parameters[1] = { chain: chainId } if (options.passphrase != null) adapterOptions.passphrase = options.passphrase diff --git a/src/import/import.ts b/src/import/import.ts index 7426889c..25dcc8cc 100644 --- a/src/import/import.ts +++ b/src/import/import.ts @@ -192,7 +192,7 @@ export async function runCarImport(options: ImportOptions): Promise try { // Parse and validate authentication - const authConfig = parseCLIAuth(options) + const authConfig = await parseCLIAuth(options) const logger = getCLILogger() const synapse = await initializeSynapse(authConfig, logger) diff --git a/src/payments/deposit.ts b/src/payments/deposit.ts index 8c91215e..114e3fa2 100644 --- a/src/payments/deposit.ts +++ b/src/payments/deposit.ts @@ -49,7 +49,7 @@ export async function runDeposit(options: DepositOptions): Promise { spinner.start('Connecting...') try { // Parse and validate authentication - const authConfig = parseCLIAuth(options) + const authConfig = await parseCLIAuth(options) const logger = getCLILogger() const synapse = await initializeSynapse(authConfig, logger) diff --git a/src/payments/fund.ts b/src/payments/fund.ts index 3a473add..08c92ece 100644 --- a/src/payments/fund.ts +++ b/src/payments/fund.ts @@ -238,7 +238,7 @@ export async function runFund(options: FundOptions): Promise { spinner.start('Connecting...') try { // Parse and validate authentication - const authConfig = parseCLIAuth(options) + const authConfig = await parseCLIAuth(options) const logger = getCLILogger() const synapse = await initializeSynapse(authConfig, logger) diff --git a/src/payments/status.ts b/src/payments/status.ts index 73bede9c..b71b90aa 100644 --- a/src/payments/status.ts +++ b/src/payments/status.ts @@ -81,7 +81,7 @@ export async function showPaymentStatus(options: StatusOptions): Promise { try { // Parse and validate authentication - const authConfig = parseCLIAuth(options) + const authConfig = await parseCLIAuth(options) const logger = getCLILogger() const synapse = await initializeSynapse(authConfig, logger) diff --git a/src/payments/withdraw.ts b/src/payments/withdraw.ts index e3c2de4f..98d379cb 100644 --- a/src/payments/withdraw.ts +++ b/src/payments/withdraw.ts @@ -34,7 +34,7 @@ export async function runWithdraw(options: WithdrawOptions): Promise { spinner.start('Connecting...') try { // Parse and validate authentication - const authConfig = parseCLIAuth(options) + const authConfig = await parseCLIAuth(options) const logger = getCLILogger() const synapse = await initializeSynapse(authConfig, logger) diff --git a/src/rm/remove-all-pieces.ts b/src/rm/remove-all-pieces.ts index b10380fa..08e6d23e 100644 --- a/src/rm/remove-all-pieces.ts +++ b/src/rm/remove-all-pieces.ts @@ -60,7 +60,7 @@ export async function runRmAllPieces(options: RmAllPiecesOptions): Promise { const network = options.network?.toLowerCase().trim() const isDevnet = network === 'devnet' @@ -82,7 +82,7 @@ export function parseCLIAuth(options: CLIAuthOptions): SynapseSetupConfig { readOnly?: boolean rpcUrl?: string chain?: Chain - account?: ReturnType + account?: Awaited> } = {} if (owsWalletId) { @@ -91,7 +91,7 @@ export function parseCLIAuth(options: CLIAuthOptions): SynapseSetupConfig { chain: chain ?? calibration, } if (owsPassphrase != null) owsOptions.passphrase = owsPassphrase - config.account = getOwsAccount(owsOptions) + config.account = await getOwsAccount(owsOptions) } else if (privateKey) { config.privateKey = privateKey } @@ -190,7 +190,7 @@ export function getCLILogger() { } export async function getCliSynapse(options: CLIAuthOptions): Promise { - const authConfig = parseCLIAuth(options) + const authConfig = await parseCLIAuth(options) const logger = getCLILogger() return initializeSynapse(authConfig, logger) } From 6f63e05ed3826d4f303dcdd2022910a771762ab8 Mon Sep 17 00:00:00 2001 From: Russell Dempsey <1173416+SgtPooki@users.noreply.github.com> Date: Wed, 6 May 2026 16:04:25 -0400 Subject: [PATCH 3/9] fix(ows): require eip155 account, document FEVM scope OWS wallets typically expose both an eip155:* (EVM) account and a fil:* (native Filecoin) account derived from the same seed. filecoin-pin signs via FEVM through synapse-sdk and only uses the eip155 entry; the fil:* / f1 address is not used. Users seeing both in `ows wallet list` could reasonably assume the native one is the right pick. Add a defensive check that rejects any non-EVM address returned by the adapter, with an error message pointing at `ows wallet list`. Also document the FEVM-only scope in the module doc comment. Refs #457 --- src/core/ows/index.ts | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/src/core/ows/index.ts b/src/core/ows/index.ts index 66dbb368..01a15e00 100644 --- a/src/core/ows/index.ts +++ b/src/core/ows/index.ts @@ -51,9 +51,16 @@ async function loadAdapter(): Promise { } } +const EVM_ADDRESS_REGEX = /^0x[0-9a-fA-F]{40}$/ + /** * Build a viem `Account` backed by an OWS wallet. * + * filecoin-pin signs via FEVM (Filecoin EVM), so we always request an + * `eip155:*` account from OWS. OWS wallets typically also expose a native + * Filecoin (`fil:*`, f1/f3 address) account derived from the same seed — + * that one is not used here, since synapse-sdk targets FEVM via viem. + * * The returned account is a `LocalAccount` from viem's perspective; signing * calls are delegated to the OWS native core, so the private key never * materializes in the Node process. @@ -65,5 +72,13 @@ export async function getOwsAccount(options: OwsAccountOptions): Promise Date: Mon, 29 Jun 2026 18:39:05 -0400 Subject: [PATCH 4/9] fix(ows): load account only when owner signs --- src/utils/cli-auth.ts | 24 +++++++++++++++++------- 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/src/utils/cli-auth.ts b/src/utils/cli-auth.ts index 4b640885..d1a7cf45 100644 --- a/src/utils/cli-auth.ts +++ b/src/utils/cli-auth.ts @@ -91,7 +91,12 @@ export async function parseCLIAuth(options: CLIAuthOptions): Promise> } = {} - if (owsWalletId) { + if (viewAddress) { + config.walletAddress = viewAddress + config.readOnly = true + } else if (walletAddress && sessionKey) { + config.walletAddress = walletAddress + config.sessionKey = sessionKey + } else if (owsWalletId) { const owsOptions: Parameters[0] = { walletId: owsWalletId, chain: chain ?? calibration, @@ -111,14 +122,13 @@ export async function parseCLIAuth(options: CLIAuthOptions): Promise Date: Wed, 22 Jul 2026 13:00:17 -0400 Subject: [PATCH 5/9] fix(ows): probe RPC chain, skip empty passphrase When --rpc-url is set, probe the endpoint's chainId before requesting the OWS account so the eip155 hint matches the target RPC, instead of defaulting to calibration. Treat an empty OWS_WALLET_PASSPHRASE as not provided. --- src/utils/cli-auth.ts | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/src/utils/cli-auth.ts b/src/utils/cli-auth.ts index d1a7cf45..23170ef0 100644 --- a/src/utils/cli-auth.ts +++ b/src/utils/cli-auth.ts @@ -9,7 +9,8 @@ import type { Chain, Synapse } from '@filoz/synapse-sdk' import { getRpcUrl, NETWORK_CHAINS, resolveDevnetConfig } from '../common/get-rpc-url.js' import { getOwsAccount } from '../core/ows/index.js' import type { SynapseSetupConfig } from '../core/synapse/index.js' -import { calibration, initializeSynapse } from '../core/synapse/index.js' +import { createTransport, initializeSynapse } from '../core/synapse/index.js' +import { resolveChainFromRpc } from '../core/synapse/resolve-chain-from-rpc.js' import { createLogger } from '../logger.js' /** @@ -114,11 +115,18 @@ export async function parseCLIAuth(options: CLIAuthOptions): Promise) from + // the chain. With --rpc-url the chain hint is intentionally left undefined + // (initializeSynapse probes the endpoint), so probe here too rather than + // guessing a network and requesting the wrong eip155 account. + const owsChain = chain ?? (rpcUrl ? await resolveChainFromRpc(createTransport(rpcUrl)) : NETWORK_CHAINS.mainnet) const owsOptions: Parameters[0] = { walletId: owsWalletId, - chain: chain ?? calibration, + chain: owsChain, } - if (owsPassphrase != null) owsOptions.passphrase = owsPassphrase + // An empty OWS_WALLET_PASSPHRASE (common in CI env files) means "no + // passphrase", not a real passphrase value. + if (owsPassphrase != null && owsPassphrase !== '') owsOptions.passphrase = owsPassphrase config.account = await getOwsAccount(owsOptions) } else if (privateKey) { config.privateKey = privateKey From 829eff8297640926ea8f53e6f7ec472c96965552 Mon Sep 17 00:00:00 2001 From: Russell Dempsey <1173416+SgtPooki@users.noreply.github.com> Date: Mon, 27 Jul 2026 14:11:16 -0400 Subject: [PATCH 6/9] fix(ows): sign EIP-712 typed data through OWS core The OWS viem adapter serializes typed data with JSON.stringify, which throws on the bigints every synapse-sdk EIP-712 payload carries (clientDataSetId, nonce, pieceIndex, permit value/deadline). Override signTypedData to serialize the message the way the OWS core accepts: bigints as even-length hex, with EIP712Domain injected when absent. signMessage and signTransaction are left to the adapter. Validated byte-identical to viem's privateKeyToAccount, and confirmed on-chain (AddPieces, SchedulePieceRemovals) on Calibration. --- src/core/ows/index.ts | 106 ++++++++++++++++++++++++++--- src/test/unit/ows.test.ts | 139 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 235 insertions(+), 10 deletions(-) create mode 100644 src/test/unit/ows.test.ts diff --git a/src/core/ows/index.ts b/src/core/ows/index.ts index 01a15e00..d118e5d2 100644 --- a/src/core/ows/index.ts +++ b/src/core/ows/index.ts @@ -14,7 +14,8 @@ * @module core/ows */ -import type { Account, Chain } from 'viem' +import type { Account, Chain, TypedDataDefinition } from 'viem' +import { getTypesForEIP712Domain } from 'viem' export interface OwsAccountOptions { /** Wallet name or ID registered with the `ows` CLI / OWS core */ @@ -36,23 +37,68 @@ interface OwsViemAdapter { ) => Account } +interface OwsCore { + signTypedData: ( + wallet: string, + chain: string, + typedDataJson: string, + passphrase?: string | undefined, + index?: number | undefined, + vaultPath?: string | undefined + ) => { signature: string } +} + +const UNAVAILABLE_MESSAGE = + 'OpenWallet Standard is not available on this platform. ' + + '@open-wallet-standard/core ships napi-rs prebuilt binaries for linux-x64-gnu, ' + + 'linux-arm64-gnu, darwin-x64, and darwin-arm64 only (no Windows or musl/Alpine artifact today). ' + + 'Use --private-key / PRIVATE_KEY instead, or run on a supported platform.' + +function unavailable(err: unknown): Error { + const reason = err instanceof Error ? err.message : String(err) + return new Error(`${UNAVAILABLE_MESSAGE}\nUnderlying load error: ${reason}`) +} + async function loadAdapter(): Promise { try { return (await import('@open-wallet-standard/adapters/viem')) as unknown as OwsViemAdapter } catch (err) { - const reason = err instanceof Error ? err.message : String(err) - throw new Error( - 'OpenWallet Standard is not available on this platform. ' + - '@open-wallet-standard/core ships napi-rs prebuilt binaries for linux-x64-gnu, ' + - 'linux-arm64-gnu, darwin-x64, and darwin-arm64 only (no Windows or musl/Alpine artifact today). ' + - 'Use --private-key / PRIVATE_KEY instead, or run on a supported platform.\n' + - `Underlying load error: ${reason}` - ) + throw unavailable(err) + } +} + +async function loadCore(): Promise { + try { + return (await import('@open-wallet-standard/core')) as unknown as OwsCore + } catch (err) { + throw unavailable(err) } } const EVM_ADDRESS_REGEX = /^0x[0-9a-fA-F]{40}$/ +/** + * Encode a bigint as the OWS core's EIP-712 parser expects a uint value. + * + * The parser enforces two constraints: + * - Decimal uint values above 2^128 are rejected ("exceeds u128 range; use hex + * encoding"). synapse-sdk's `clientDataSetId` and `nonce` are `randU256()`, + * uniform over the full uint256 range, so hex is required in the general case. + * - Hex values must have an even number of digits ("bad uint hex: Odd number of + * digits"), so `0x0` and `0x1ab` are not valid. + * + * Even-length hex satisfies both and produces signatures identical to viem + * across 0, 2^128 - 1, 2^128, and 2^256 - 1. + * + * Negative values fall back to decimal. synapse-sdk uses only uint EIP-712 + * fields; the hex form here is unsigned. + */ +function bigintToOwsHex(value: bigint): string { + if (value < 0n) return value.toString() + const hex = value.toString(16) + return `0x${hex.length % 2 === 1 ? `0${hex}` : hex}` +} + /** * Build a viem `Account` backed by an OWS wallet. * @@ -80,5 +126,45 @@ export async function getOwsAccount(options: OwsAccountOptions): Promise => { + // viem's signTypedData action adds EIP712Domain to `types` before an account + // sees the payload; a direct account.signTypedData() call does not. Add it + // here so the core resolves the domain type ("unknown type: EIP712Domain" + // otherwise). A caller-supplied EIP712Domain wins via the spread order. + const withDomain = { + ...typedData, + types: { + EIP712Domain: getTypesForEIP712Domain({ domain: typedData.domain }), + ...typedData.types, + }, + } + const serialized = JSON.stringify(withDomain, (_key, value) => + typeof value === 'bigint' ? bigintToOwsHex(value) : value + ) + const result = core.signTypedData( + options.walletId, + chainId, + serialized, + options.passphrase, + options.index, + options.vaultPath + ) + const signature = result.signature + return (signature.startsWith('0x') ? signature : `0x${signature}`) as `0x${string}` + } + + return { ...account, signTypedData } as Account } diff --git a/src/test/unit/ows.test.ts b/src/test/unit/ows.test.ts new file mode 100644 index 00000000..92d9c347 --- /dev/null +++ b/src/test/unit/ows.test.ts @@ -0,0 +1,139 @@ +import type { Chain } from 'viem' +import { beforeEach, describe, expect, it, vi } from 'vitest' + +// Stub the OWS native binding + adapter so this suite runs on every CI platform +// (the native core has no Windows/musl prebuilt). We assert the serialization +// our getOwsAccount override performs, not the Rust signer itself. +const { owsToViemAccount, coreSignTypedData, adapterSignMessage } = vi.hoisted(() => ({ + owsToViemAccount: vi.fn(), + coreSignTypedData: vi.fn( + ( + _wallet: string, + _chain: string, + _typedDataJson: string, + _passphrase?: string, + _index?: number, + _vault?: string + ) => ({ + signature: 'abcd', + }) + ), + adapterSignMessage: vi.fn(), +})) + +vi.mock('@open-wallet-standard/adapters/viem', () => ({ owsToViemAccount })) +vi.mock('@open-wallet-standard/core', () => ({ signTypedData: coreSignTypedData })) + +import { getOwsAccount } from '../../core/ows/index.js' + +const calibration = { id: 314159, name: 'Filecoin Calibration' } as unknown as Chain + +const domain = { + name: 'FilecoinWarmStorageService', + version: '1', + chainId: 314159, + verifyingContract: '0x1111111111111111111111111111111111111111', +} as const + +beforeEach(() => { + owsToViemAccount.mockReset() + coreSignTypedData.mockReset() + coreSignTypedData.mockReturnValue({ signature: 'abcd' }) + owsToViemAccount.mockReturnValue({ + address: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266', + signMessage: adapterSignMessage, + signTransaction: vi.fn(), + signTypedData: vi.fn(), + }) +}) + +async function signWith(message: Record, types: Record, primaryType: string) { + const account = await getOwsAccount({ walletId: 'fil-test', chain: calibration }) + const signature = await account.signTypedData?.({ domain, types, primaryType, message } as never) + const call = coreSignTypedData.mock.calls[0] + if (call == null) throw new Error('core.signTypedData was not called') + return { signature, serialized: JSON.parse(call[2]), call } +} + +describe('getOwsAccount - signTypedData bigint serialization', () => { + it('injects EIP712Domain and passes eip155 chain + walletId to the core', async () => { + const { call, serialized } = await signWith( + { clientDataSetId: 42n }, + { CreateDataSet: [{ name: 'clientDataSetId', type: 'uint256' }] }, + 'CreateDataSet' + ) + expect(call[0]).toBe('fil-test') + expect(call[1]).toBe('eip155:314159') + expect(serialized.types.EIP712Domain).toBeDefined() + }) + + it('encodes bigints as even-length hex so values above 2^128 are accepted', async () => { + const big = 2n ** 128n // one over the OWS decimal-uint limit + const { serialized } = await signWith( + { clientDataSetId: big, nonce: 0n, small: 42n }, + { + Msg: [ + { name: 'clientDataSetId', type: 'uint256' }, + { name: 'nonce', type: 'uint256' }, + { name: 'small', type: 'uint256' }, + ], + }, + 'Msg' + ) + // Even-length hex, round-trips to the original bigint. + expect(serialized.message.clientDataSetId).toMatch(/^0x([0-9a-f]{2})+$/) + expect(BigInt(serialized.message.clientDataSetId)).toBe(big) + // Zero and small values are also even-padded. + expect(serialized.message.nonce).toBe('0x00') + expect(serialized.message.small).toBe('0x2a') + }) + + it('hex-encodes bigints inside a uint256[] (SchedulePieceRemovals.pieceIds)', async () => { + const { serialized } = await signWith( + { clientDataSetId: 9n, pieceIds: [1n, 2n, 2n ** 200n] }, + { + SchedulePieceRemovals: [ + { name: 'clientDataSetId', type: 'uint256' }, + { name: 'pieceIds', type: 'uint256[]' }, + ], + }, + 'SchedulePieceRemovals' + ) + const ids = serialized.message.pieceIds as string[] + // Each element is even-length hex and round-trips to its original bigint. + expect(ids.every((h) => /^0x([0-9a-f]{2})+$/.test(h))).toBe(true) + expect(ids.map((h) => BigInt(h))).toEqual([1n, 2n, 2n ** 200n]) + }) + + it('handles a max uint256 without throwing', async () => { + const max = 2n ** 256n - 1n + const { serialized } = await signWith( + { clientDataSetId: max }, + { Msg: [{ name: 'clientDataSetId', type: 'uint256' }] }, + 'Msg' + ) + expect(BigInt(serialized.message.clientDataSetId)).toBe(max) + }) + + it('normalizes the returned signature with a 0x prefix', async () => { + coreSignTypedData.mockReturnValue({ signature: 'deadbeef' }) + const { signature } = await signWith( + { clientDataSetId: 1n }, + { Msg: [{ name: 'clientDataSetId', type: 'uint256' }] }, + 'Msg' + ) + expect(signature).toBe('0xdeadbeef') + }) + + it('leaves the adapter signMessage in place (only signTypedData is overridden)', async () => { + const account = await getOwsAccount({ walletId: 'fil-test', chain: calibration }) + expect(account.signMessage).toBe(adapterSignMessage) + }) +}) + +describe('getOwsAccount - address validation', () => { + it('rejects a non-EVM address', async () => { + owsToViemAccount.mockReturnValue({ address: 'f1abc', signMessage: vi.fn() }) + await expect(getOwsAccount({ walletId: 'fil-test', chain: calibration })).rejects.toThrow(/non-EVM address/) + }) +}) From 5b823ec644fc5492d3fce076e7e494cfa6c54222 Mon Sep 17 00:00:00 2001 From: Russell Dempsey <1173416+SgtPooki@users.noreply.github.com> Date: Mon, 27 Jul 2026 14:11:16 -0400 Subject: [PATCH 7/9] fix(auth): resolve auth-mode precedence by source Auth options merged from CLI flags and env vars could let an env var silently beat an explicit flag. Capture each flag's source (cli vs env) in a preAction hook and resolve precedence in parseCLIAuth: an explicit flag wins over env, and two modes from the same tier is a hard error. Session-key mode competes only when both halves are present, so a lone --wallet-address or --session-key no longer outranks a complete mode. --- src/test/unit/cli-auth.test.ts | 287 +++++++++++++++++++++++++++++++++ src/utils/cli-auth.ts | 201 ++++++++++++++++++----- src/utils/cli-options.ts | 40 +++++ 3 files changed, 489 insertions(+), 39 deletions(-) create mode 100644 src/test/unit/cli-auth.test.ts diff --git a/src/test/unit/cli-auth.test.ts b/src/test/unit/cli-auth.test.ts new file mode 100644 index 00000000..e3be70d6 --- /dev/null +++ b/src/test/unit/cli-auth.test.ts @@ -0,0 +1,287 @@ +import { Command } from 'commander' +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' + +// The OWS account resolver loads a native binding; stub it so tests exercise +// precedence without touching the vault. The returned sentinel echoes the +// options it was called with so we can assert chain/passphrase handling. +// vi.mock is hoisted above imports, so the mock fns are created via vi.hoisted. +const { getOwsAccount, resolveChainFromRpc } = vi.hoisted(() => ({ + getOwsAccount: vi.fn(async (opts: unknown) => ({ address: '0xf39Fd6', __ows: opts })), + resolveChainFromRpc: vi.fn(async () => ({ id: 314159, name: 'Filecoin Calibration' })), +})) +vi.mock('../../core/ows/index.js', () => ({ getOwsAccount })) +vi.mock('../../core/synapse/resolve-chain-from-rpc.js', () => ({ resolveChainFromRpc })) + +vi.mock('../../core/synapse/index.js', () => ({ + createTransport: vi.fn(() => ({ transport: true })), + initializeSynapse: vi.fn(), +})) + +// getRpcUrl() calls resolveDevnetConfig internally, so mock it at the source +// module (devnet-config) rather than the get-rpc-url re-export; get-rpc-url's +// internal reference and cli-auth's import both resolve to this mock. +const { resolveDevnetConfig } = vi.hoisted(() => ({ + resolveDevnetConfig: vi.fn(() => ({ + privateKey: '0xdevnetkey', + chain: { id: 31415926, name: 'Devnet', rpcUrls: { default: { http: ['http://127.0.0.1:8545'] } } }, + })), +})) +vi.mock('../../common/devnet-config.js', async (importOriginal) => { + const actual = await importOriginal() + return { ...actual, resolveDevnetConfig } +}) + +import type { AuthOptionSources, CLIAuthOptions } from '../../utils/cli-auth.js' +import { parseCLIAuth } from '../../utils/cli-auth.js' +import { addAuthOptions } from '../../utils/cli-options.js' + +const PK = '0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80' + +/** Build options with an explicit source map so precedence is deterministic. */ +function withSources(base: CLIAuthOptions, sources: AuthOptionSources): CLIAuthOptions { + return { ...base, optionSources: sources } +} + +beforeEach(() => { + getOwsAccount.mockClear() + resolveChainFromRpc.mockClear() +}) + +describe('parseCLIAuth - single auth mode', () => { + it('resolves a private key into a private-key config', async () => { + const config = await parseCLIAuth({ privateKey: PK, network: 'calibration' }) + expect(config).toMatchObject({ privateKey: PK }) + expect(config).not.toHaveProperty('account') + expect(getOwsAccount).not.toHaveBeenCalled() + }) + + it('resolves an OWS wallet into an account config without loading a private key', async () => { + const config = await parseCLIAuth({ wallet: 'fil-test', network: 'calibration' }) + expect(getOwsAccount).toHaveBeenCalledTimes(1) + expect(config).toHaveProperty('account') + expect(config).not.toHaveProperty('privateKey') + }) + + it('resolves --view-address into read-only config', async () => { + const config = await parseCLIAuth({ viewAddress: '0xabc', network: 'calibration' }) + expect(config).toMatchObject({ walletAddress: '0xabc', readOnly: true }) + expect(getOwsAccount).not.toHaveBeenCalled() + }) + + it('resolves wallet-address + session-key into session-key config', async () => { + const config = await parseCLIAuth({ walletAddress: '0xowner', sessionKey: '0xsess', network: 'calibration' }) + expect(config).toMatchObject({ walletAddress: '0xowner', sessionKey: '0xsess' }) + }) + + it('passes through a lone wallet-address so initializeSynapse can report "requires both"', async () => { + const config = await parseCLIAuth({ walletAddress: '0xowner', network: 'calibration' }) + expect(config).toMatchObject({ walletAddress: '0xowner' }) + expect(config).not.toHaveProperty('sessionKey') + }) + + it('never resolves the OWS account for a non-signing mode', async () => { + await parseCLIAuth({ viewAddress: '0xabc', wallet: 'ignored', network: 'calibration' }).catch(() => undefined) + // (view + ows is a conflict; the point is the native adapter is not eagerly loaded) + expect(getOwsAccount).not.toHaveBeenCalled() + }) +}) + +describe('parseCLIAuth - precedence: explicit flag beats env', () => { + it('explicit --private-key wins over an env OWS wallet', async () => { + const config = await parseCLIAuth( + withSources( + { privateKey: PK, wallet: 'env-wallet', network: 'calibration' }, + { privateKey: 'cli', wallet: 'env' } + ) + ) + expect(config).toMatchObject({ privateKey: PK }) + expect(getOwsAccount).not.toHaveBeenCalled() + }) + + it('explicit --wallet wins over an env private key', async () => { + const config = await parseCLIAuth( + withSources( + { privateKey: PK, wallet: 'cli-wallet', network: 'calibration' }, + { privateKey: 'env', wallet: 'cli' } + ) + ) + expect(config).toHaveProperty('account') + expect(config).not.toHaveProperty('privateKey') + }) + + // A lone explicit session-key half must not outrank a complete env-sourced + // mode. Session-key competes only when BOTH halves are present. + it('a lone explicit --wallet-address does not beat an env OWS wallet', async () => { + const config = await parseCLIAuth( + withSources( + { walletAddress: '0xowner', wallet: 'env-wallet', network: 'calibration' }, + { walletAddress: 'cli', wallet: 'env' } + ) + ) + expect(config).toHaveProperty('account') + expect(config).not.toHaveProperty('walletAddress') + }) + + it('a lone explicit --session-key does not beat an env private key', async () => { + const config = await parseCLIAuth( + withSources( + { sessionKey: '0xsess', privateKey: PK, network: 'calibration' }, + { sessionKey: 'cli', privateKey: 'env' } + ) + ) + expect(config).toMatchObject({ privateKey: PK }) + expect(config).not.toHaveProperty('sessionKey') + }) + + it('a complete explicit session key still beats an env private key', async () => { + const config = await parseCLIAuth( + withSources( + { walletAddress: '0xowner', sessionKey: '0xsess', privateKey: PK, network: 'calibration' }, + { walletAddress: 'cli', sessionKey: 'cli', privateKey: 'env' } + ) + ) + expect(config).toMatchObject({ walletAddress: '0xowner', sessionKey: '0xsess' }) + expect(config).not.toHaveProperty('privateKey') + }) +}) + +describe('parseCLIAuth - precedence: conflicts', () => { + it('errors when two modes are supplied by explicit flags', async () => { + await expect( + parseCLIAuth( + withSources({ privateKey: PK, wallet: 'w', network: 'calibration' }, { privateKey: 'cli', wallet: 'cli' }) + ) + ).rejects.toThrow(/Conflicting authentication options/) + }) + + it('errors when two modes are supplied only by env, hinting to pass a flag', async () => { + await expect( + parseCLIAuth( + withSources({ privateKey: PK, wallet: 'w', network: 'calibration' }, { privateKey: 'env', wallet: 'env' }) + ) + ).rejects.toThrow(/Pass an explicit flag to disambiguate/) + }) + + it('treats a programmatic caller (no sources) as all-explicit, so two modes conflict', async () => { + await expect(parseCLIAuth({ privateKey: PK, wallet: 'w', network: 'calibration' })).rejects.toThrow( + /Conflicting authentication options/ + ) + }) + + it('reports conflicts in canonical order (read-only before private key)', async () => { + await expect( + parseCLIAuth( + withSources( + { viewAddress: '0xabc', privateKey: PK, network: 'calibration' }, + { + viewAddress: 'env', + privateKey: 'env', + } + ) + ) + ).rejects.toThrow(/--view-address\/VIEW_ADDRESS and --private-key\/PRIVATE_KEY/) + }) +}) + +describe('parseCLIAuth - OWS chain hint', () => { + it('uses the network chain when --network is set', async () => { + await parseCLIAuth({ wallet: 'fil-test', network: 'calibration' }) + const [opts] = getOwsAccount.mock.calls[0] as [{ chain: { id: number } }] + expect(opts.chain.id).toBe(314159) + expect(resolveChainFromRpc).not.toHaveBeenCalled() + }) + + it('probes the RPC endpoint for the chain when only --rpc-url is set', async () => { + await parseCLIAuth({ wallet: 'fil-test', rpcUrl: 'https://example.invalid/rpc' }) + expect(resolveChainFromRpc).toHaveBeenCalledTimes(1) + const [opts] = getOwsAccount.mock.calls[0] as [{ chain: { id: number } }] + expect(opts.chain.id).toBe(314159) + }) + + it('defaults the chain hint to mainnet when neither network nor rpc-url is set', async () => { + await parseCLIAuth({ wallet: 'fil-test' }) + const [opts] = getOwsAccount.mock.calls[0] as [{ chain: { id: number } }] + expect(opts.chain.id).toBe(314) + expect(resolveChainFromRpc).not.toHaveBeenCalled() + }) +}) + +describe('parseCLIAuth - OWS passphrase', () => { + it('forwards a real passphrase', async () => { + await parseCLIAuth({ wallet: 'fil-test', walletPassphrase: 'hunter2', network: 'calibration' }) + const [opts] = getOwsAccount.mock.calls[0] as [{ passphrase?: string }] + expect(opts.passphrase).toBe('hunter2') + }) + + it('treats an empty passphrase as not provided', async () => { + await parseCLIAuth({ wallet: 'fil-test', walletPassphrase: '', network: 'calibration' }) + const [opts] = getOwsAccount.mock.calls[0] as [{ passphrase?: string }] + expect(opts).not.toHaveProperty('passphrase') + }) +}) + +describe('parseCLIAuth - devnet fallback', () => { + it('uses the devnet key only when no auth mode is supplied', async () => { + const config = await parseCLIAuth({ network: 'devnet' }) + expect(config).toMatchObject({ privateKey: '0xdevnetkey' }) + expect(resolveDevnetConfig).toHaveBeenCalled() + }) + + it('does not use the devnet key when an OWS wallet is supplied', async () => { + const config = await parseCLIAuth({ network: 'devnet', wallet: 'fil-test' }) + expect(config).toHaveProperty('account') + expect(config).not.toHaveProperty('privateKey') + }) +}) + +describe('addAuthOptions - source collection hook', () => { + const ENV_KEYS = [ + 'PRIVATE_KEY', + 'OWS_WALLET_ID', + 'OWS_WALLET_PASSPHRASE', + 'WALLET_ADDRESS', + 'SESSION_KEY', + 'VIEW_ADDRESS', + ] + let saved: Record + + beforeEach(() => { + saved = {} + for (const key of ENV_KEYS) { + saved[key] = process.env[key] + delete process.env[key] + } + }) + + afterEach(() => { + for (const key of ENV_KEYS) { + if (saved[key] === undefined) delete process.env[key] + else process.env[key] = saved[key] + } + }) + + async function captureSources(argv: string[]): Promise { + let captured: AuthOptionSources = {} + const command = new Command('add').exitOverride() + command.action((options) => { + captured = options.optionSources + }) + addAuthOptions(command) + await command.parseAsync(argv, { from: 'user' }) + return captured + } + + it('marks an explicit flag as cli and an env var as env', async () => { + process.env.OWS_WALLET_ID = 'env-wallet' + const sources = await captureSources(['--private-key', PK]) + expect(sources.privateKey).toBe('cli') + expect(sources.wallet).toBe('env') + }) + + it('omits options that were never supplied', async () => { + const sources = await captureSources(['--wallet', 'w']) + expect(sources.wallet).toBe('cli') + expect(sources).not.toHaveProperty('privateKey') + expect(sources).not.toHaveProperty('viewAddress') + }) +}) diff --git a/src/utils/cli-auth.ts b/src/utils/cli-auth.ts index 23170ef0..fd1c46e1 100644 --- a/src/utils/cli-auth.ts +++ b/src/utils/cli-auth.ts @@ -13,6 +13,28 @@ import { createTransport, initializeSynapse } from '../core/synapse/index.js' import { resolveChainFromRpc } from '../core/synapse/resolve-chain-from-rpc.js' import { createLogger } from '../logger.js' +/** + * Where a resolved auth option value came from. Mirrors Commander's + * `getOptionValueSource()`, narrowed to the two sources we distinguish for + * precedence: an explicit command-line flag versus an environment variable. + */ +export type AuthOptionSource = 'cli' | 'env' + +/** + * Per-option provenance for the mutually exclusive auth flags, keyed by the + * Commander attribute name. Populated by the `addAuthOptions` preAction hook + * (see `collectAuthOptionSources` in cli-options.ts) so `parseCLIAuth` can tell + * an explicit flag from an inherited env var. Absent for programmatic callers, + * which are treated as if every supplied value were an explicit flag. + */ +export interface AuthOptionSources { + privateKey?: AuthOptionSource + wallet?: AuthOptionSource + walletAddress?: AuthOptionSource + sessionKey?: AuthOptionSource + viewAddress?: AuthOptionSource +} + /** * Common CLI authentication options interface * Used across all commands that require authentication @@ -30,6 +52,11 @@ export interface CLIAuthOptions { sessionKey?: string | undefined /** View-only wallet address (no signing) */ viewAddress?: string | undefined + /** + * Provenance of the auth flags above, injected by the `addAuthOptions` + * preAction hook. Used only to resolve precedence; never forwarded to the SDK. + */ + optionSources?: AuthOptionSources | undefined /** Filecoin network: mainnet or calibration */ network?: string | undefined /** RPC endpoint URL (overrides network if specified) */ @@ -49,6 +76,62 @@ export interface CLIAuthOptions { dataSetIds?: string[] | undefined } +/** + * The mutually exclusive authentication modes, in precedence order. When more + * than one mode is supplied, resolution is by source, not by this order: an + * explicit flag always wins over an environment variable (see + * {@link resolveAuthMode}). This order only fixes how conflicts are reported and + * documents the canonical hierarchy. + * + * 1. `readOnly` - `--view-address` / `VIEW_ADDRESS` (query only, never signs) + * 2. `sessionKey` - `--wallet-address` + `--session-key` (delegated signer) + * 3. `ows` - `--wallet` / `OWS_WALLET_ID` (OWS-backed owner signer) + * 4. `privateKey` - `--private-key` / `PRIVATE_KEY` (raw-key owner signer) + * + * Devnet auto-resolves the devnet user's private key, but only when none of the + * above is supplied, so it is a fallback rather than a competing mode. + */ +type AuthMode = 'readOnly' | 'sessionKey' | 'ows' | 'privateKey' + +interface AuthModeCandidate { + mode: AuthMode + source: AuthOptionSource + /** Human-readable flag/env pair for conflict messages. */ + label: string +} + +/** + * Resolve which single auth mode to use from the modes that were supplied. + * + * Rules (see {@link AuthMode} for the mode list): + * - An explicit command-line flag always beats an environment variable, so a + * flag disambiguates against any env-sourced mode. + * - Two or more modes from explicit flags is a hard error (contradictory args). + * - With no explicit flag, two or more env-sourced modes is a hard error; the + * user must pass an explicit flag to disambiguate. + * - Exactly one supplied mode wins; none supplied returns `undefined` (caller + * applies the devnet fallback or lets initializeSynapse report missing auth). + * + * Programmatic callers that don't provide `optionSources` have every supplied + * value treated as an explicit flag, so any two modes still conflict. + */ +function resolveAuthMode(candidates: AuthModeCandidate[]): AuthMode | undefined { + const conflict = (modes: AuthModeCandidate[], envOnly: boolean): Error => { + const labels = modes.map((c) => c.label).join(' and ') + const hint = envOnly ? ' Pass an explicit flag to disambiguate.' : '' + return new Error(`Conflicting authentication options: ${labels}. Provide exactly one signing mode.${hint}`) + } + + const explicit = candidates.filter((c) => c.source === 'cli') + if (explicit.length > 1) throw conflict(explicit, false) + const [singleExplicit] = explicit + if (singleExplicit) return singleExplicit.mode + + // No explicit flag: every remaining candidate is env-sourced. + if (candidates.length > 1) throw conflict(candidates, true) + return candidates[0]?.mode +} + /** * Parse CLI authentication options into SynapseSetupConfig * @@ -69,17 +152,45 @@ export async function parseCLIAuth(options: CLIAuthOptions): Promise value != null && value !== '' + // Effective source of one option: its Commander provenance when known, + // otherwise treat a supplied value as an explicit flag (programmatic callers). + const sourceOf = (name: keyof AuthOptionSources, value?: string): AuthOptionSource | undefined => + nonEmpty(value) ? (sources?.[name] ?? 'cli') : undefined + // A mode spanning several options takes the strongest source among them: a + // single explicit flag makes the whole mode explicit. + const strongest = (...srcs: Array): AuthOptionSource | undefined => + srcs.includes('cli') ? 'cli' : srcs.includes('env') ? 'env' : undefined + + // Build the candidate list in canonical priority order (see AuthMode). + const candidates: AuthModeCandidate[] = [] + const readOnlySource = sourceOf('viewAddress', viewAddress) + if (readOnlySource) + candidates.push({ mode: 'readOnly', source: readOnlySource, label: '--view-address/VIEW_ADDRESS' }) + // Session-key mode competes for precedence only when BOTH halves are present. + // A lone --wallet-address or --session-key is a lowest-priority fallback + // (handled in the default branch), so it never outranks a complete mode like + // an OWS wallet or a private key. + const walletAddressSource = sourceOf('walletAddress', walletAddress) + const sessionKeySource = sourceOf('sessionKey', sessionKey) + const sessionSource = + walletAddressSource && sessionKeySource ? strongest(walletAddressSource, sessionKeySource) : undefined + if (sessionSource) + candidates.push({ mode: 'sessionKey', source: sessionSource, label: '--wallet-address/--session-key' }) + const owsSource = sourceOf('wallet', owsWalletId) + if (owsSource) candidates.push({ mode: 'ows', source: owsSource, label: '--wallet/OWS_WALLET_ID' }) + const privateKeySource = sourceOf('privateKey', options.privateKey) + if (privateKeySource) + candidates.push({ mode: 'privateKey', source: privateKeySource, label: '--private-key/PRIVATE_KEY' }) + + const mode = resolveAuthMode(candidates) + // --network and --rpc-url are mutually exclusive at the Commander level. Set the chain hint // only when --network was chosen; otherwise leave it undefined and let initializeSynapse probe // the RPC endpoint. When neither is supplied, default to mainnet. @@ -92,12 +203,9 @@ export async function parseCLIAuth(options: CLIAuthOptions): Promise> } = {} - if (viewAddress) { - config.walletAddress = viewAddress - config.readOnly = true - } else if (walletAddress && sessionKey) { - config.walletAddress = walletAddress - config.sessionKey = sessionKey - } else if (owsWalletId) { - // The OWS adapter derives its CAIP-2 account hint (eip155:) from - // the chain. With --rpc-url the chain hint is intentionally left undefined - // (initializeSynapse probes the endpoint), so probe here too rather than - // guessing a network and requesting the wrong eip155 account. - const owsChain = chain ?? (rpcUrl ? await resolveChainFromRpc(createTransport(rpcUrl)) : NETWORK_CHAINS.mainnet) - const owsOptions: Parameters[0] = { - walletId: owsWalletId, - chain: owsChain, + switch (mode) { + case 'readOnly': + if (nonEmpty(viewAddress)) config.walletAddress = viewAddress + config.readOnly = true + break + case 'sessionKey': + // Both halves are present (that is what made this a competing candidate). + if (nonEmpty(walletAddress)) config.walletAddress = walletAddress + if (nonEmpty(sessionKey)) config.sessionKey = sessionKey + break + case 'ows': { + // The OWS adapter derives its CAIP-2 account hint (eip155:) from + // the chain. With --rpc-url the chain hint is intentionally left undefined + // (initializeSynapse probes the endpoint), so probe here too rather than + // guessing a network and requesting the wrong eip155 account. + const owsChain = chain ?? (rpcUrl ? await resolveChainFromRpc(createTransport(rpcUrl)) : NETWORK_CHAINS.mainnet) + const owsOptions: Parameters[0] = { + walletId: owsWalletId ?? '', + chain: owsChain, + } + // An empty OWS_WALLET_PASSPHRASE (common in CI env files) means "no + // passphrase", not a real passphrase value. + if (nonEmpty(owsPassphrase)) owsOptions.passphrase = owsPassphrase + config.account = await getOwsAccount(owsOptions) + break + } + case 'privateKey': + if (nonEmpty(options.privateKey)) config.privateKey = options.privateKey + break + default: { + // No complete auth mode won. Fallbacks, in priority order: devnet + // auto-key, then a lone session-key half passed through so + // initializeSynapse can emit its targeted "requires both" error. + const devnetKey = isDevnet ? resolveDevnetConfig().privateKey : undefined + if (nonEmpty(devnetKey)) { + config.privateKey = devnetKey + } else if (nonEmpty(walletAddress)) { + config.walletAddress = walletAddress + } else if (nonEmpty(sessionKey)) { + config.sessionKey = sessionKey + } + break } - // An empty OWS_WALLET_PASSPHRASE (common in CI env files) means "no - // passphrase", not a real passphrase value. - if (owsPassphrase != null && owsPassphrase !== '') owsOptions.passphrase = owsPassphrase - config.account = await getOwsAccount(owsOptions) - } else if (privateKey) { - config.privateKey = privateKey - } else if (walletAddress) { - // Only one half of session-key auth supplied; pass it through so - // initializeSynapse can emit its targeted "requires both" error. - config.walletAddress = walletAddress - } else if (sessionKey) { - config.sessionKey = sessionKey } if (rpcUrl) config.rpcUrl = rpcUrl if (chain) config.chain = chain diff --git a/src/utils/cli-options.ts b/src/utils/cli-options.ts index 2ad6bbe2..e5500068 100644 --- a/src/utils/cli-options.ts +++ b/src/utils/cli-options.ts @@ -9,8 +9,39 @@ import { parseUnits } from 'viem' import { MIN_RUNWAY_DAYS } from '../common/constants.js' import { normalizeNetworkName } from '../common/get-rpc-url.js' import { USDFC_DECIMALS } from '../core/payments/constants.js' +import type { AuthOptionSource, AuthOptionSources } from './cli-auth.js' import { log } from './cli-logger.js' +/** + * Commander attribute names for the mutually exclusive auth flags whose + * provenance {@link parseCLIAuth} needs to resolve precedence. + */ +const AUTH_OPTION_NAMES = ['privateKey', 'wallet', 'walletAddress', 'sessionKey', 'viewAddress'] as const + +/** + * Read each auth flag's provenance from a Commander command, narrowed to the + * `cli` (explicit flag) vs `env` (environment variable) distinction that + * precedence resolution cares about. Other sources (`default`, `config`, + * `implied`, unset) are omitted so `parseCLIAuth` treats them as absent. + * + * None of the auth flags declare a Commander `.default()`, which matters: + * parseCLIAuth's `sourceOf` treats a present value with no recorded source as an + * explicit flag. If a default is ever added to an auth option, its value would + * arrive with source `'default'` (dropped here) and then be misread as explicit, + * so record it as `'env'`-tier (or lower) precedence at that point rather than + * omitting it. + */ +export function collectAuthOptionSources(command: Command): AuthOptionSources { + const sources: AuthOptionSources = {} + for (const name of AUTH_OPTION_NAMES) { + const source = command.getOptionValueSource(name) + if (source === 'cli' || source === 'env') { + sources[name] = source as AuthOptionSource + } + } + return sources +} + /** * Option factories for flags declared on more than one command. Each pairs a * flag with its backing env var exactly once; only the description varies per @@ -96,6 +127,15 @@ export function addAuthOptions(command: Command): Command { ) ) + // Capture each auth flag's provenance (explicit flag vs env var) before the + // action runs, and stash it on the parsed options as `optionSources`. This is + // the only point where Commander's per-option source is available, so it lets + // parseCLIAuth() resolve precedence (explicit flag beats env) without every + // command action or runner having to thread the Command through. + command.hook('preAction', (_thisCommand, actionCommand) => { + actionCommand.setOptionValue('optionSources', collectAuthOptionSources(actionCommand)) + }) + return addNetworkOptions(command).addOption( rpcUrlOption('RPC endpoint') // default rpcUrl value is defined in ../common/get-rpc-url.ts From 1953883feb7016d9088e281daa9f48934e79e2d6 Mon Sep 17 00:00:00 2001 From: Russell Dempsey <1173416+SgtPooki@users.noreply.github.com> Date: Mon, 27 Jul 2026 14:11:17 -0400 Subject: [PATCH 8/9] fix(payments): skip key prompt when a signer is set Interactive setup prompted for a private key whenever one was not passed, then handed it to parseCLIAuth. With an OWS wallet supplied that tripped the mutual-exclusion check. Skip the prompt only when a sign-capable mode is present: an OWS wallet or a complete session key. A view address and a lone session-key half cannot run setup, so they still prompt. --- src/payments/interactive.ts | 10 +++- src/test/unit/run-interactive-setup.test.ts | 57 ++++++++++++++++++++- 2 files changed, 65 insertions(+), 2 deletions(-) diff --git a/src/payments/interactive.ts b/src/payments/interactive.ts index a61f7a62..cff9b926 100644 --- a/src/payments/interactive.ts +++ b/src/payments/interactive.ts @@ -51,7 +51,15 @@ export async function runInteractiveSetup(options: PaymentSetupOptions): Promise // Get private key let privateKey = options.privateKey - if (!privateKey) { + // Only prompt for a private key when no signing auth mode was supplied. An + // OWS wallet or a complete session key signs in parseCLIAuth below, so + // prompting would add a private key as a second signing mode and trip the + // mutual-exclusion check. A view address (read-only) and a lone session-key + // half cannot run setup, so they do not suppress the prompt. + const nonEmpty = (value?: string): boolean => value != null && value !== '' + const hasSigningAuth = nonEmpty(options.wallet) || (nonEmpty(options.walletAddress) && nonEmpty(options.sessionKey)) + + if (!privateKey && !hasSigningAuth) { const input = await password({ message: 'Enter your private key', validate: (value) => { diff --git a/src/test/unit/run-interactive-setup.test.ts b/src/test/unit/run-interactive-setup.test.ts index 9b1f79a4..5ce481cc 100644 --- a/src/test/unit/run-interactive-setup.test.ts +++ b/src/test/unit/run-interactive-setup.test.ts @@ -1,10 +1,15 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { runInteractiveSetup } from '../../payments/interactive.js' -const { mockPassword, mockIsCancel, mockIsTTY } = vi.hoisted(() => ({ +const { mockPassword, mockIsCancel, mockIsTTY, mockParseCLIAuth, mockInitializeSynapse } = vi.hoisted(() => ({ mockPassword: vi.fn(), mockIsCancel: vi.fn(() => false), mockIsTTY: vi.fn(() => true), + mockParseCLIAuth: vi.fn(async (_options: { wallet?: string; privateKey?: string }) => ({})), + // Throw right after auth resolves so the flow stops before any network work. + mockInitializeSynapse: vi.fn(async () => { + throw new Error('__stop_after_auth__') + }), })) vi.mock('@clack/prompts', () => ({ @@ -23,6 +28,11 @@ vi.mock('../../utils/cli-logger.js', () => ({ isTTY: mockIsTTY, log: { line: vi.fn(), flush: vi.fn(), indent: vi.fn() }, })) +vi.mock('../../utils/cli-auth.js', () => ({ parseCLIAuth: mockParseCLIAuth })) +vi.mock('../../core/synapse/index.js', () => ({ + initializeSynapse: mockInitializeSynapse, + getClientAddress: vi.fn(() => '0x0000000000000000000000000000000000000000'), +})) describe('runInteractiveSetup exit codes', () => { const originalPrivateKey = process.env.PRIVATE_KEY @@ -49,4 +59,49 @@ describe('runInteractiveSetup exit codes', () => { expect(process.exitCode).toBe(2) }) + + it('does not prompt for a private key when an OWS wallet is supplied', async () => { + // Prompting here would inject a private key alongside --wallet and trip + // parseCLIAuth's mutual-exclusion check for interactive OWS setup. + await runInteractiveSetup({ wallet: 'fil-test', network: 'calibration' } as any).catch(() => undefined) + + expect(mockPassword).not.toHaveBeenCalled() + expect(mockParseCLIAuth).toHaveBeenCalledTimes(1) + const passed = mockParseCLIAuth.mock.calls[0]?.[0] + expect(passed?.wallet).toBe('fil-test') + expect(passed?.privateKey).toBeUndefined() + }) + + it('does not prompt for a private key when a complete session key is supplied', async () => { + await runInteractiveSetup({ walletAddress: '0xowner', sessionKey: '0xsess', network: 'calibration' } as any).catch( + () => undefined + ) + + expect(mockPassword).not.toHaveBeenCalled() + expect(mockParseCLIAuth).toHaveBeenCalledTimes(1) + }) + + it('still prompts for a private key when no auth mode is supplied', async () => { + mockPassword.mockResolvedValueOnce('0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80') + + await runInteractiveSetup({ network: 'calibration' } as any).catch(() => undefined) + + expect(mockPassword).toHaveBeenCalledTimes(1) + }) + + it('still prompts when only a view address is supplied (read-only cannot run setup)', async () => { + mockPassword.mockResolvedValueOnce('0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80') + + await runInteractiveSetup({ viewAddress: '0xreadonly', network: 'calibration' } as any).catch(() => undefined) + + expect(mockPassword).toHaveBeenCalledTimes(1) + }) + + it('still prompts when only a lone wallet-address is supplied (incomplete session key)', async () => { + mockPassword.mockResolvedValueOnce('0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80') + + await runInteractiveSetup({ walletAddress: '0xowner', network: 'calibration' } as any).catch(() => undefined) + + expect(mockPassword).toHaveBeenCalledTimes(1) + }) }) From 9b35c89cc4f92cc2e968af863ef095bd3d6e76a8 Mon Sep 17 00:00:00 2001 From: Russell Dempsey <1173416+SgtPooki@users.noreply.github.com> Date: Mon, 27 Jul 2026 14:11:17 -0400 Subject: [PATCH 9/9] chore(deps): bump @open-wallet-standard to 1.4.2 Bumps @open-wallet-standard/core and /adapters from 1.3.2 to 1.4.2, the release that adds trusted publishing. --- package.json | 4 ++-- pnpm-lock.yaml | 54 +++++++++++++++++++++++++------------------------- 2 files changed, 29 insertions(+), 29 deletions(-) diff --git a/package.json b/package.json index b31e2ad2..48e54d79 100644 --- a/package.json +++ b/package.json @@ -139,8 +139,8 @@ "@libp2p/tcp": "^11.0.17", "@multiformats/multiaddr": "^13.0.1", "@multiformats/multiaddr-to-uri": "^12.0.0", - "@open-wallet-standard/adapters": "^1.3.2", - "@open-wallet-standard/core": "^1.3.2", + "@open-wallet-standard/adapters": "^1.4.2", + "@open-wallet-standard/core": "^1.4.2", "@sentry/node": "^10.49.0", "commander": "^14.0.3", "datastore-core": "^11.0.4", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index cf4de7c2..524a5eaa 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -54,11 +54,11 @@ importers: specifier: ^12.0.0 version: 12.0.0 '@open-wallet-standard/adapters': - specifier: ^1.3.2 - version: 1.3.2(viem@2.48.4(typescript@6.0.3)(zod@4.3.6)) + specifier: ^1.4.2 + version: 1.4.2(viem@2.48.4(typescript@6.0.3)(zod@4.3.6)) '@open-wallet-standard/core': - specifier: ^1.3.2 - version: 1.3.2 + specifier: ^1.4.2 + version: 1.4.2 '@sentry/node': specifier: ^10.49.0 version: 10.49.0 @@ -708,8 +708,8 @@ packages: '@octokit/types@16.0.0': resolution: {integrity: sha512-sKq+9r1Mm4efXW1FCk7hFSeJo4QKreL/tTbR0rz/qx/r1Oa2VV83LTA/H/MuCOX7uCIJmQVRKBcbmWoySjAnSg==} - '@open-wallet-standard/adapters@1.3.2': - resolution: {integrity: sha512-49GOsSNcxPmP4ETv4nuPkj7prYFdxO9bdaVHP24vyeV19Wl6iuKgHtjzXyiX/2N0pRN/XVP/yGEmqypFeVNtkQ==} + '@open-wallet-standard/adapters@1.4.2': + resolution: {integrity: sha512-xXKBRmfNltnJH6BhTyTlpCExWga0bD16GNlx18sr7FbvIOh51CpF+6tn4z0lTNoz7VNAVM/jCS4UNL66V0II5w==} peerDependencies: '@solana/web3.js': '>=1.0.0' '@tetherto/wdk-wallet': '>=1.0.0-beta.0' @@ -722,28 +722,28 @@ packages: viem: optional: true - '@open-wallet-standard/core-darwin-arm64@1.3.2': - resolution: {integrity: sha512-rvUsGU8eZtIMYFJ047nFKS79ajJeYCydvvh2B6bX0UUGf2NJZ4wUnxGqijxPiflz2xqzvbHXYZNzy3MIO4+fDQ==} + '@open-wallet-standard/core-darwin-arm64@1.4.2': + resolution: {integrity: sha512-0bu36U0vGLqIn7rxHAJNA253VvV9xt+AbWLWf8NHxiDoLaZX0RSVyBd79g80Pyi6N9/KkgltDWAq4hfruOaz9A==} cpu: [arm64] os: [darwin] - '@open-wallet-standard/core-darwin-x64@1.3.2': - resolution: {integrity: sha512-a6u3CvWZY8sC1S92AMGnO1WFlMW3m38+3/IoKzhxcFvD8rVY0OA0ebH6qzYLT96/TaouXDwmMQV6yQmb1IjfzQ==} + '@open-wallet-standard/core-darwin-x64@1.4.2': + resolution: {integrity: sha512-RvFMd6PhOT4UDH0NnybYiouLcjH9hPaP0i0I42R/mGnqNZq/ZwQ2h7jhFRu8hj213xV4LYfT2mWCnSp5eviMMg==} cpu: [x64] os: [darwin] - '@open-wallet-standard/core-linux-arm64-gnu@1.3.2': - resolution: {integrity: sha512-wzyd7euZ5qvDr8dHf3eG2XP7iFgMmBUo35U7cWMLs6Yqjqz103SEjtMfNJEedZRb62f8P3Xhp9jdRLzaJvjk5g==} + '@open-wallet-standard/core-linux-arm64-gnu@1.4.2': + resolution: {integrity: sha512-srJiGO5A6mc1j3QU4Br3/sUZbikRi4T9c1wZdT8MBZcSFiJ5MONqyduMHQhfbd60jTJK73MeqDFXdKaBnxU9qg==} cpu: [arm64] os: [linux] - '@open-wallet-standard/core-linux-x64-gnu@1.3.2': - resolution: {integrity: sha512-pVtyEP8EqR3rjqdBDCRuB9Rd4xs8+11gRVtWidwPeFz5QPq3c9VGZjGQtTZoiZBHCxKaq5pTQjC78J5lVuA0hg==} + '@open-wallet-standard/core-linux-x64-gnu@1.4.2': + resolution: {integrity: sha512-0Hqsv/qu/YAlQ9oKJ6c1ZwFhbzf4O58OSb9EK7v6Ji0Ih+SS4VX4X3shuikjQuBD3sNXYer5pLLA0OSGNYmFxw==} cpu: [x64] os: [linux] - '@open-wallet-standard/core@1.3.2': - resolution: {integrity: sha512-Dk8bB9G5PjJfNzAqJd9WT9OfqrrSXRc3gA9+BCaY2qirlNrCkIAZCHaDIE0LnGOAaoSZizR6qnOFbukj+DTzJg==} + '@open-wallet-standard/core@1.4.2': + resolution: {integrity: sha512-alu69/tEeGYJhC1nyc7EGhJM6oqOU8XQv3S1qYe+LFYU/iHHi7QQrnTvNBesUkosccWf8TRZzhmGluRhaE44KQ==} hasBin: true '@opentelemetry/api-logs@0.207.0': @@ -3249,31 +3249,31 @@ snapshots: dependencies: '@octokit/openapi-types': 27.0.0 - '@open-wallet-standard/adapters@1.3.2(viem@2.48.4(typescript@6.0.3)(zod@4.3.6))': + '@open-wallet-standard/adapters@1.4.2(viem@2.48.4(typescript@6.0.3)(zod@4.3.6))': dependencies: '@noble/curves': 1.9.1 - '@open-wallet-standard/core': 1.3.2 + '@open-wallet-standard/core': 1.4.2 optionalDependencies: viem: 2.48.4(typescript@6.0.3)(zod@4.3.6) - '@open-wallet-standard/core-darwin-arm64@1.3.2': + '@open-wallet-standard/core-darwin-arm64@1.4.2': optional: true - '@open-wallet-standard/core-darwin-x64@1.3.2': + '@open-wallet-standard/core-darwin-x64@1.4.2': optional: true - '@open-wallet-standard/core-linux-arm64-gnu@1.3.2': + '@open-wallet-standard/core-linux-arm64-gnu@1.4.2': optional: true - '@open-wallet-standard/core-linux-x64-gnu@1.3.2': + '@open-wallet-standard/core-linux-x64-gnu@1.4.2': optional: true - '@open-wallet-standard/core@1.3.2': + '@open-wallet-standard/core@1.4.2': optionalDependencies: - '@open-wallet-standard/core-darwin-arm64': 1.3.2 - '@open-wallet-standard/core-darwin-x64': 1.3.2 - '@open-wallet-standard/core-linux-arm64-gnu': 1.3.2 - '@open-wallet-standard/core-linux-x64-gnu': 1.3.2 + '@open-wallet-standard/core-darwin-arm64': 1.4.2 + '@open-wallet-standard/core-darwin-x64': 1.4.2 + '@open-wallet-standard/core-linux-arm64-gnu': 1.4.2 + '@open-wallet-standard/core-linux-x64-gnu': 1.4.2 '@opentelemetry/api-logs@0.207.0': dependencies: