Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 13 additions & 1 deletion docs/packages/sdk/bsv-sdk.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ id: bsv-sdk
title: '@bsv/sdk'
kind: package
domain: sdk
version: '2.8.1'
version: '2.8.2'
npm: '@bsv/sdk'
last_updated: '2026-09-23'
last_verified: '2026-09-23'
Expand Down Expand Up @@ -394,3 +394,15 @@ If every slot is authenticated, new handshakes fail until a session expires or
is removed. Only a successful locally initiated handshake can select the
implicit destination for a later `Peer` call; inbound messages cannot retarget
it.

### Wallet discovery deadlines in the 2.8.2 candidate

Automatic React Native and XDM probes retain their short discovery deadlines
and remove listeners when unavailable. A successful probe creates a separate
operational connection so authentication, permission approval and valid slow
responses do not inherit the one-second/200-millisecond discovery deadline.
Explicitly configured substrate `responseTimeout` values remain enforced, and
response validation and origin checks are unchanged. No API or wire migration
is needed. Applications must update their bundled SDK; updating only a wallet
cannot replace a web application's SDK. Publication remains a separate
protected-workflow action.
6 changes: 3 additions & 3 deletions docs/reference/package-api-migrations.md

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion docs/reference/stack-facts.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ authorized release action.
| overlays | `@bsv/overlay-discovery-services` | `2.2.5` | node-library | node-cjs, node-esm | node | `>=22` | [packages/overlays/overlay-discovery-services](https://github.com/bsv-blockchain/ts-stack/tree/main/packages/overlays/overlay-discovery-services) |
| overlays | `@bsv/overlay-express` | `2.7.2` | node-library | node-cjs, node-esm | node | `>=22` | [packages/overlays/overlay-express](https://github.com/bsv-blockchain/ts-stack/tree/main/packages/overlays/overlay-express) |
| overlays | `@bsv/overlay-topics` | `1.8.4` | node-library | node-esm | node | `>=22` | [packages/overlays/topics](https://github.com/bsv-blockchain/ts-stack/tree/main/packages/overlays/topics) |
| sdk | `@bsv/sdk` | `2.8.1` | browser-library | browser-bundler, browser-esm, node-cjs, node-esm, umd-global | browser, node, umd | `>=22` | [packages/sdk](https://github.com/bsv-blockchain/ts-stack/tree/main/packages/sdk) |
| sdk | `@bsv/sdk` | `2.8.2` | browser-library | browser-bundler, browser-esm, node-cjs, node-esm, umd-global | browser, node, umd | `>=22` | [packages/sdk](https://github.com/bsv-blockchain/ts-stack/tree/main/packages/sdk) |
| sdk | `@bsv/verifast` | `0.3.6` | wasm-library | browser-bundler, browser-esm, node-cjs, node-esm, umd-global, wasm-worker | browser, node, umd, wasm, worker | `>=22` | [packages/verifast](https://github.com/bsv-blockchain/ts-stack/tree/main/packages/verifast) |
| wallet | `@bsv/btms` | `1.2.3` | node-library | node-cjs, node-esm | node | `>=22` | [packages/wallet/btms](https://github.com/bsv-blockchain/ts-stack/tree/main/packages/wallet/btms) |
| wallet | `@bsv/btms-permission-module` | `1.2.1` | node-library | node-esm | node | `>=22` | [packages/wallet/btms-permission-module](https://github.com/bsv-blockchain/ts-stack/tree/main/packages/wallet/btms-permission-module) |
Expand Down
6 changes: 3 additions & 3 deletions governance/package-release-notes.json
Original file line number Diff line number Diff line change
Expand Up @@ -167,9 +167,9 @@
{
"name": "@bsv/sdk",
"publishedVersion": "2.8.1",
"releaseType": "none",
"summary": "Fixes portable AES-GCM decryption of valid authenticated empty plaintext, including empty encrypted wallet fields. Preserves historical encryption bytes and full tag verification, and adds native/portable compatibility and negative tests for all supported AES key sizes, IV lengths, block boundaries and SymmetricKey envelopes. SDK 2.8.1 was published through the protected single-package release workflow with verified registry integrity and provenance.",
"migration": "None. Public APIs, ciphertext bytes and stored account data are unchanged. Upgrade browser/mobile consumers to SDK 2.8.1 to decrypt valid empty fields. Missing, truncated or altered authentication tags and wrong keys or IVs still fail closed."
"releaseType": "patch",
"summary": "Separates automatic React Native and XDM wallet discovery deadlines from subsequent wallet operations. Successful probes now create an operational substrate without the probe-only timeout, so user approval and valid slow responses can complete. Unresponsive discovery remains bounded with listener cleanup, explicitly configured operation timeouts remain enforced, and all response validation and origin checks are unchanged. Source candidate 2.8.2 is not published until the protected npm workflow completes.",
"migration": "No API or wire migration. Upgrade applications that use WalletClient auto-discovery to 2.8.2; earlier auto-discovered React Native/XDM connections can time out later calls at the one-second/200-millisecond discovery deadline. Explicit substrate responseTimeout values still apply. Wallet implementation upgrades alone do not update the SDK bundled by a web application."
},
{
"name": "@bsv/simple",
Expand Down
2 changes: 1 addition & 1 deletion governance/repository-health/baselines.json
Original file line number Diff line number Diff line change
Expand Up @@ -322,7 +322,7 @@
"@bsv/overlay-discovery-services": "2.2.5",
"@bsv/overlay-express": "2.7.2",
"@bsv/overlay-topics": "1.8.4",
"@bsv/sdk": "2.8.1",
"@bsv/sdk": "2.8.2",
"@bsv/verifast": "0.3.6",
"@bsv/btms": "1.2.3",
"@bsv/btms-permission-module": "1.2.1",
Expand Down
10 changes: 10 additions & 0 deletions packages/sdk/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,16 @@ All notable changes to this project will be documented in this file. The format

## [Unreleased]

### 2.8.2 candidate — wallet discovery timeout lifecycle

- Keep React Native and XDM discovery bounded without carrying the short probe
timeout into later wallet operations that may wait for user approval.
- Preserve caller-configured operation timeouts, listener cleanup, response
validation and origin checks. No API or wire migration is required.
- Applications using automatic discovery must upgrade their bundled SDK; a
wallet-only upgrade does not change the SDK served by an application.
- This source candidate is not published until the protected npm workflow completes.

### 2.8.1 candidate — portable authenticated empty fields

- Fix portable AES-GCM decryption of valid authenticated empty plaintext. This
Expand Down
18 changes: 12 additions & 6 deletions packages/sdk/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,12 +45,18 @@ in UMD; their reviewed ceilings are 742,500 and 556,000 bytes respectively.
The combined sync and security candidate measures 560,560 raw bytes with esbuild;
its reviewed raw ceiling is 561,000 bytes. Compression ceilings are unchanged.

The 2.8.1 candidate fixes portable AES-GCM decryption of authenticated empty
plaintext, including empty encrypted wallet fields. Browser/mobile runtimes and
Node now accept the same valid envelopes. Encryption bytes and full 16-byte tag
verification are unchanged; invalid tags, keys and IVs remain rejected. No API,
wire, account-data or ciphertext migration is required. This source candidate is
not published until the protected npm release workflow completes.
SDK 2.8.1 fixes portable AES-GCM decryption of authenticated empty plaintext.
Encryption bytes and full 16-byte tag verification are unchanged; invalid tags,
keys and IVs remain rejected.

The 2.8.2 candidate separates wallet discovery timeouts from normal operations.
Automatic React Native and XDM discovery remains bounded, while subsequent
calls can wait for user approval without inheriting the one-second/200-millisecond
probe deadline. Explicit substrate `responseTimeout` values remain enforced.
Applications using `WalletClient` auto-discovery must upgrade their bundled SDK;
updating the wallet alone does not update a web application's SDK. No API, wire
or account-data migration is required. This source candidate is not published
until the protected npm release workflow completes.

## Table of Contents

Expand Down
2 changes: 1 addition & 1 deletion packages/sdk/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@bsv/sdk",
"version": "2.8.1",
"version": "2.8.2",
"sideEffects": false,
"engines": {
"node": ">=22"
Expand Down
12 changes: 8 additions & 4 deletions packages/sdk/src/wallet/WalletClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,8 @@ export default class WalletClient implements WalletInterface {

const attemptSubstrate = async (
factory: () => WalletInterface,
timeout?: number
timeout?: number,
connectedFactory?: () => WalletInterface
): Promise<{ success: boolean; sub?: WalletInterface }> => {
try {
const sub = factory()
Expand All @@ -146,7 +147,8 @@ export default class WalletClient implements WalletInterface {
result = await sub.getVersion({})
}
validateWalletResult('getVersion', result)
return { success: true, sub }
// Probe deadlines bound discovery, not later calls that may await user approval.
return { success: true, sub: connectedFactory?.() ?? sub }
} catch {
return { success: false }
}
Expand All @@ -166,7 +168,8 @@ export default class WalletClient implements WalletInterface {
attemptSubstrate(() => new HTTPWalletJSON(this.originator), MAX_FAST_SUBSTRATE_RESPONSE_WAIT),
attemptSubstrate(
() => new ReactNativeWebView('*', MAX_FAST_SUBSTRATE_RESPONSE_WAIT),
MAX_FAST_SUBSTRATE_RESPONSE_WAIT
MAX_FAST_SUBSTRATE_RESPONSE_WAIT,
() => new ReactNativeWebView()
)
]

Expand All @@ -186,7 +189,8 @@ export default class WalletClient implements WalletInterface {
// Fall back to slower XDM substrate
const xdmResult = await attemptSubstrate(
() => new XDMSubstrate('*', MAX_XDM_RESPONSE_WAIT),
MAX_XDM_RESPONSE_WAIT
MAX_XDM_RESPONSE_WAIT,
() => new XDMSubstrate()
)
if (xdmResult.success && xdmResult.sub !== undefined) {
this.substrate = xdmResult.sub
Expand Down
146 changes: 146 additions & 0 deletions packages/sdk/src/wallet/__tests/WalletClient.discovery.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
import WalletClient from '../WalletClient'
import ReactNativeWebView from '../substrates/ReactNativeWebView'
import XDMSubstrate from '../substrates/XDM'
import WindowCWISubstrate from '../substrates/window.CWI'

type Request = { id: string; call: string }
type Transport = 'react-native' | 'xdm'

/** Only the browser/native environment is simulated; SDK discovery and transports are real. */
function walletHost(transport: Transport, respond = true) {
const listeners = new Set<(event: MessageEvent) => void>()
const requests: Request[] = []
const deliver = (request: Request) => {
requests.push(request)
if (!respond) return
setTimeout(
() => {
const data = {
type: 'CWI',
isInvocation: false,
id: request.id,
status: 'success',
result: request.call === 'getVersion' ? { version: '1.0.0.0' } : { authenticated: true }
}
const event = {
data: transport === 'react-native' ? JSON.stringify(data) : data,
source: transport === 'react-native' ? null : window.parent,
origin: 'https://wallet.example',
isTrusted: true
} as MessageEvent
for (const listener of listeners) listener(event)
},
request.call === 'getVersion' ? 10 : 10000
)
}
const host = {
addEventListener: (_name: string, listener: (event: MessageEvent) => void) =>
listeners.add(listener),
removeEventListener: (_name: string, listener: (event: MessageEvent) => void) =>
listeners.delete(listener),
postMessage: transport === 'xdm' ? deliver : jest.fn(),
...(transport === 'react-native'
? { ReactNativeWebView: { postMessage: (raw: string) => deliver(JSON.parse(raw)) } }
: {})
}
global.window = host as unknown as Window & typeof globalThis
Object.defineProperty(window, 'parent', { value: window })
return { listeners, requests }
}

describe('WalletClient discovery timeout lifecycle', () => {
const originalWindow = global.window

beforeEach(() => {
jest.useFakeTimers()
jest.spyOn(globalThis, 'fetch').mockRejectedValue(new Error('No HTTP wallet in this test'))
})

afterEach(() => {
global.window = originalWindow
jest.restoreAllMocks()
jest.useRealTimers()
})

it('retains the discovered window.CWI binding for subsequent operations', async () => {
const getVersion = jest.fn(async () => ({ version: '1.0.0.0' }))
const waitForAuthentication = jest.fn(async () => ({ authenticated: true }))
global.window = {
CWI: { getVersion, waitForAuthentication }
} as unknown as Window & typeof globalThis
const client = new WalletClient()
await client.connectToSubstrate()
const connected = client.substrate
expect(connected).toBeInstanceOf(WindowCWISubstrate)

// Discovery has already bound this host; later window changes must not replace it.
Object.defineProperty(window, 'CWI', { value: undefined })
await expect(client.waitForAuthentication()).resolves.toEqual({ authenticated: true })
expect(client.substrate).toBe(connected)
expect(getVersion).toHaveBeenCalledTimes(1)
expect(waitForAuthentication).toHaveBeenCalledTimes(1)
expect(jest.getTimerCount()).toBe(0)
})

it.each<Transport>(['react-native', 'xdm'])(
'does not impose the %s discovery deadline on subsequent user approval',
async transport => {
const { listeners, requests } = walletHost(transport)
const client = new WalletClient()
const connected = client.connectToSubstrate()
await jest.advanceTimersByTimeAsync(10)
await connected
expect(client.substrate).toBeInstanceOf(
transport === 'react-native' ? ReactNativeWebView : XDMSubstrate
)
expect(listeners.size).toBe(0)
expect(jest.getTimerCount()).toBe(0)

const result = client.waitForAuthentication().then(
value => ({ value }),
error => ({ error })
)
await jest.advanceTimersByTimeAsync(10000)
expect(await result).toEqual({ value: { authenticated: true } })
expect(requests.map(request => request.call)).toEqual(['getVersion', 'waitForAuthentication'])
expect(listeners.size).toBe(0)
expect(jest.getTimerCount()).toBe(0)
}
)

it.each<Transport>(['react-native', 'xdm'])(
'still bounds an unresponsive %s discovery and removes its listeners',
async transport => {
const { listeners } = walletHost(transport, false)
const result = new WalletClient().connectToSubstrate().catch(error => error)
await jest.advanceTimersByTimeAsync(1200)
expect(await result).toEqual(
expect.objectContaining({
message: expect.stringContaining('No wallet available')
})
)
expect(listeners.size).toBe(0)
expect(jest.getTimerCount()).toBe(0)
}
)

it.each<Transport>(['react-native', 'xdm'])(
'preserves an explicitly configured %s operation timeout',
async transport => {
const { listeners } = walletHost(transport, false)
const substrate =
transport === 'react-native' ? new ReactNativeWebView('*', 50) : new XDMSubstrate('*', 50)
const client = new WalletClient(substrate)
const result = client.waitForAuthentication().catch(error => error)
await jest.advanceTimersByTimeAsync(50)
expect(await result).toEqual(
expect.objectContaining({
message: expect.stringContaining('response timed out')
})
)
expect(client.substrate).toBe(substrate)
expect(listeners.size).toBe(0)
expect(jest.getTimerCount()).toBe(0)
}
)
})
Loading