diff --git a/docs/packages/sdk/bsv-sdk.md b/docs/packages/sdk/bsv-sdk.md index 498deade6..8a346051f 100644 --- a/docs/packages/sdk/bsv-sdk.md +++ b/docs/packages/sdk/bsv-sdk.md @@ -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' @@ -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. diff --git a/docs/reference/package-api-migrations.md b/docs/reference/package-api-migrations.md index 6d70a93cb..b9540d91f 100644 --- a/docs/reference/package-api-migrations.md +++ b/docs/reference/package-api-migrations.md @@ -48,7 +48,7 @@ and clean-consumer tests remain the executable type authority. | `@bsv/overlay-topics` | `1.8.4` | `1.8.4` | none | [API and usage](../packages/overlays/overlay-topics.md) | Topic and lookup identifiers and valid canonical wire encodings remain unchanged. Audit historical rows for malformed amounts, noncanonical identifiers, incomplete ownership or admin evidence, and ambiguous outpoint linkage before replay or rebuild. Custom state and screening providers must return exact booleans, compare identity keys case-insensitively where documented, conserve exact safe-integer value, and honor bounded query and result contracts. | | `@bsv/paymail` | `2.4.9` | `2.4.10` | patch | [API and usage](../packages/messaging/paymail.md) | None. ESM consumers are unaffected. | | `@bsv/payment-express-middleware` | `2.1.7` | `2.1.7` | none | [API and usage](../packages/middleware/payment-express-middleware.md) | No wire or public API migration is required; legacy x-bsv-payment JSON behavior remains supported, and Express 4 and 5 applications use their own peer-provided Express installation. Wallet adapters must return accepted and optional isMerge as own data properties; inherited/accessor-backed verdicts now fail closed. Overinclusive Atomic BEEF receipts are normalized to the declared subject closure. Production replicas must share one durable atomic replay store, and operators must reconcile a replay-store failure after wallet acceptance before asking a payer to spend again. Distributors must retain THIRD_PARTY_NOTICES.md and LICENSES/ with the package. | -| `@bsv/sdk` | `2.8.1` | `2.8.1` | none | [API and usage](../packages/sdk/bsv-sdk.md) | 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. | +| `@bsv/sdk` | `2.8.1` | `2.8.2` | patch | [API and usage](../packages/sdk/bsv-sdk.md) | 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. | | `@bsv/simple` | `0.6.0` | `0.6.0` | none | [API and usage](../packages/helpers/simple.md) | Replace createServerWalletHandler() deployments with createServerWalletHandler({ authorize: async ({ action, headers }) => authenticatedSessionCanUseAction(headers, action) }). The callback must return literal true for each status, create, request, receive, balance, outputs, or reset action; omission now returns HTTP 403 for every action. Roll out the authentication layer and callback with the package, update anonymous probes or automation, and apply the same policy to every replica. Do not emulate the old public behavior with an unconditional authorize: () => true callback. Valid recipient derivations and authenticated Message Box peers remain supported; malformed, wrong-owner, or transaction-mutated flows now fail closed. New DID, CredentialSchema, and Certifier records use canonical 32-byte types. Current SDK wallet methods reject historical short types, so do not put migration aliases in wallet list, acquire, prove, or relinquish calls. Export affected records through the storage version that created them, authenticate them offline against the exact locally configured identifier, and reissue/import canonical replacements; no legacy certificate is rewritten automatically. Distributors must retain THIRD_PARTY_NOTICES.md and LICENSES/. The internal comparator consolidation requires no migration and is included in the existing unpublished 0.6.0 candidate. | | `@bsv/templates` | `1.10.2` | `1.10.3` | patch | [API and usage](../packages/helpers/templates.md) | None. CommonJS consumers that patched 1.10.2 locally can drop the patch after upgrading to 1.10.3; ESM consumers are unaffected. | | `@bsv/teranode-listener` | `1.1.6` | `1.1.6` | none | [API and usage](../packages/network/teranode-listener.md) | No API migration is required for valid consumers: raw callbacks remain the default and decoding is opt-in with decodeMessages: true. Configuration arrays and callbacks are snapshotted at construction, boolean controls must be literal booleans, and malformed or duplicate topics, addresses, keys, and unsupported properties now fail closed. usePrivateDHT: false now actually omits the DHT service. The published mainnet PNET value is transport compatibility data, not a publisher credential; decoded sender and payload fields remain untrusted and security-critical claims require independent validation. Distributors must retain THIRD_PARTY_NOTICES.md and LICENSES/ with the package. | @@ -359,8 +359,8 @@ CLI entry points: `{"lch":"./dist/cli.js"}`. - Package documentation: [docs/packages/sdk/bsv-sdk.md](../packages/sdk/bsv-sdk.md) - Source: [packages/sdk](https://github.com/bsv-blockchain/ts-stack/tree/main/packages/sdk) -- Release note: 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. +- Release note: 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. | Public subpath | Runtime target(s) | Declaration target(s) | | ---------------------------------- | ---------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------- | diff --git a/docs/reference/stack-facts.md b/docs/reference/stack-facts.md index ce0b3390c..c8434d9ed 100644 --- a/docs/reference/stack-facts.md +++ b/docs/reference/stack-facts.md @@ -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) | diff --git a/governance/package-release-notes.json b/governance/package-release-notes.json index 705c352b2..35d552c9e 100644 --- a/governance/package-release-notes.json +++ b/governance/package-release-notes.json @@ -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", diff --git a/governance/repository-health/baselines.json b/governance/repository-health/baselines.json index 887d609a0..0597883dd 100644 --- a/governance/repository-health/baselines.json +++ b/governance/repository-health/baselines.json @@ -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", diff --git a/packages/sdk/CHANGELOG.md b/packages/sdk/CHANGELOG.md index 9e7c258e7..e4b27cc6e 100644 --- a/packages/sdk/CHANGELOG.md +++ b/packages/sdk/CHANGELOG.md @@ -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 diff --git a/packages/sdk/README.md b/packages/sdk/README.md index 91fd511a9..f4b7385b4 100644 --- a/packages/sdk/README.md +++ b/packages/sdk/README.md @@ -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 diff --git a/packages/sdk/package.json b/packages/sdk/package.json index d70008666..f2289c31f 100644 --- a/packages/sdk/package.json +++ b/packages/sdk/package.json @@ -1,6 +1,6 @@ { "name": "@bsv/sdk", - "version": "2.8.1", + "version": "2.8.2", "sideEffects": false, "engines": { "node": ">=22" diff --git a/packages/sdk/src/wallet/WalletClient.ts b/packages/sdk/src/wallet/WalletClient.ts index 3828dd274..c9159ccc2 100644 --- a/packages/sdk/src/wallet/WalletClient.ts +++ b/packages/sdk/src/wallet/WalletClient.ts @@ -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() @@ -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 } } @@ -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() ) ] @@ -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 diff --git a/packages/sdk/src/wallet/__tests/WalletClient.discovery.test.ts b/packages/sdk/src/wallet/__tests/WalletClient.discovery.test.ts new file mode 100644 index 000000000..e9c2da71c --- /dev/null +++ b/packages/sdk/src/wallet/__tests/WalletClient.discovery.test.ts @@ -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(['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(['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(['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) + } + ) +})